Skip to content

Commit dd27159

Browse files
authored
Go SDK: vendor the schemas it generates from (#74047)
1 parent 5e476d7 commit dd27159

13 files changed

Lines changed: 6135 additions & 55 deletions

File tree

‎.pre-commit-config.yaml‎

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -348,17 +348,38 @@ repos:
348348
# prek run sync-java-sdk-dag-schema --hook-stage manual
349349
stages: ['manual']
350350
files: ^java-sdk/sdk/schema/dag-schema\.json$
351+
- id: sync-go-sdk-schemas
352+
name: Sync Go SDK vendored schemas with the distributions that own them
353+
description: "Copy airflow-core's Dag schema and task-sdk's supervisor schema when go-sdk's vendored copies drift"
354+
entry: ./scripts/ci/prek/sync_go_sdk_schemas.py
355+
language: python
356+
pass_filenames: false
357+
# Re-vendoring is a go-sdk maintainer's deliberate step after a schema change on
358+
# the Python side, not something every commit should do, so this is manual only,
359+
# the way sync-java-sdk-dag-schema above is:
360+
# prek run sync-go-sdk-schemas --hook-stage manual
361+
# Running it on every commit would make a change to a Python schema fail the PR
362+
# of whoever made it, which is the coupling vendoring is here to remove.
363+
stages: ['manual']
364+
files: >
365+
(?x)
366+
^airflow-core/src/airflow/serialization/schema\.json$|
367+
^task-sdk/src/airflow/sdk/execution_time/schema/schema\.json$|
368+
^go-sdk/schema/.*\.json$|
369+
^scripts/ci/prek/sync_go_sdk_schemas\.py$
351370
- id: check-go-sdk-generated-drift
352371
name: Check Go SDK generated files against the schemas they generate from
353372
description: "Regenerate the Dag specs and the coordinator-protocol models, and fail when a committed file differs"
354373
entry: ./scripts/ci/prek/check_go_sdk_generated_drift.py
355374
# golang so prek provisions the toolchain the generators need, the way the
356375
# other checks that shell out to `go` in go-sdk get theirs.
357376
language: golang
377+
# The vendored schemas, not the Python originals: sync-go-sdk-schemas above is
378+
# what ties those to the copies, as a manual step. This one runs on every commit,
379+
# because a refreshed copy that nothing regenerated from is the drift that matters.
358380
files: >
359381
(?x)
360-
^airflow-core/src/airflow/serialization/schema\.json$|
361-
^task-sdk/src/airflow/sdk/execution_time/schema/schema\.json$|
382+
^go-sdk/schema/.*\.json$|
362383
^go-sdk/airflow/spec\.gen\.go$|
363384
^go-sdk/airflow/spec\.go$|
364385
^go-sdk/internal/genspec/.*\.go$|

‎go-sdk/README.md‎

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -380,17 +380,36 @@ Python supervisor / task runner
380380
The Go side of the protocol is implemented in `pkg/execution/`. On the Python side it is the
381381
`ExecutableCoordinator` in `task-sdk/src/airflow/sdk/coordinators/executable/coordinator.py`.
382382

383+
## The vendored schemas
384+
385+
`schema/dag-schema.json` and `schema/supervisor-schema.json` are byte-for-byte copies of schemas
386+
airflow-core and task-sdk own. The generators read the copies, not the originals, so a generated file
387+
is explainable from a file inside this module and a standalone checkout — a published Go module, an
388+
ASF source release — can regenerate and test without the monorepo around it. `ts-sdk` and `java-sdk`
389+
vendor theirs the same way.
390+
391+
Two prek hooks split the work that keeps the copies honest. `sync-go-sdk-schemas` copies an original
392+
over its copy when the two differ and fails, since copying is mechanical; it is a **manual** hook, so
393+
re-vendoring is a deliberate step here rather than something a change to a Python schema forces on
394+
whoever made it — which also means nothing announces on its own that a copy has gone stale.
395+
`check-go-sdk-generated-drift` runs on every commit and regenerates from the copies, failing when the
396+
committed Go differs, since what to do about a new schema construct — a generator rule, an authoring
397+
exclusion — is a decision.
398+
383399
## Regenerating the coordinator-protocol models
384400

385-
The types in `pkg/execution/genmodels/` are generated from the in-tree supervisor schema snapshot
386-
(`task-sdk/src/airflow/sdk/execution_time/schema/schema.json`); do not edit them by hand. To move the
387-
SDK to a newer schema version:
401+
The types in `pkg/execution/genmodels/` are generated from `schema/supervisor-schema.json`, this
402+
module's vendored copy of the supervisor schema snapshot the Python Task SDK owns
403+
(`task-sdk/src/airflow/sdk/execution_time/schema/schema.json`); do not edit either by hand. To move
404+
the SDK to a newer schema version:
388405

389-
1. Set `SupervisorSchemaVersion` in [`pkg/execution/messages.go`](./pkg/execution/messages.go) to the
390-
snapshot's `api_version` date.
391-
2. Run `just generate-models`.
406+
1. Refresh the copy: `prek run sync-go-sdk-schemas --hook-stage manual`, which overwrites
407+
`schema/supervisor-schema.json` from task-sdk's and fails so the change lands in review.
408+
2. Set `SupervisorSchemaVersion` in [`pkg/execution/messages.go`](./pkg/execution/messages.go) to the
409+
copy's `api_version` date.
410+
3. Run `just generate-models`, and commit the copy with what it generated.
392411

393-
`TestSupervisorSchemaVersionMatchesSnapshot` fails when the constant and the snapshot's `api_version`
412+
`TestSupervisorSchemaVersionMatchesSnapshot` fails when the constant and the copy's `api_version`
394413
drift, so a missed bump is caught by `go test`. A snapshot can also grow a field without the
395414
`api_version` moving, which leaves the models behind with nothing failing — msgpack drops a field the
396415
struct does not declare — so the `check-go-sdk-generated-drift` prek hook regenerates them and fails
@@ -404,9 +423,10 @@ the models adds, renames or removes a `DagRunState` constant. The test keeps fai
404423
## Regenerating the Dag and task specs
405424

406425
`airflow.DagSpec` and `airflow.TaskSpec` in [`airflow/spec.gen.go`](./airflow/spec.gen.go) are
407-
generated from airflow-core's Dag serialization schema
408-
(`airflow-core/src/airflow/serialization/schema.json`), which Python owns; do not edit them by hand.
409-
Run `just generate-specs` after changing the schema or the generator.
426+
generated from `schema/dag-schema.json`, this module's vendored copy of airflow-core's Dag
427+
serialization schema (`airflow-core/src/airflow/serialization/schema.json`), which Python owns; do
428+
not edit either by hand. Refresh the copy with `prek run sync-go-sdk-schemas --hook-stage manual`,
429+
then run `just generate-specs` after changing the schema or the generator.
410430

411431
The schema is the serialized shape rather than the authoring one, so
412432
[`internal/genspec/authoring.go`](./internal/genspec/authoring.go) holds the three tables that turn

‎go-sdk/airflow/spec.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ import "reflect"
3535
// the zero value. Serializing a Dag converts the fields; a tag would invite
3636
// json.Marshal(spec) to skip that step and produce a shape core misreads.
3737

38-
//go:generate go run ../internal/genspec -schema ../../airflow-core/src/airflow/serialization/schema.json -out ../../.build/go-sdk/spec.schema.json
38+
//go:generate go run ../internal/genspec -schema ../schema/dag-schema.json -out ../../.build/go-sdk/spec.schema.json
3939
//go:generate go run github.com/atombender/go-jsonschema@v0.23.1 --only-models --struct-name-from-title --tags "" --capitalization ID --capitalization JSON --capitalization MD --capitalization XCom -p airflow -o spec.gen.go ../../.build/go-sdk/spec.schema.json
4040
//go:generate go run ../internal/genspec -license spec.gen.go
4141

‎go-sdk/internal/genspec/main.go‎

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,9 @@
1515
// specific language governing permissions and limitations
1616
// under the License.
1717

18-
// Command genspec rewrites Airflow core's Dag serialization schema
19-
// (airflow-core/src/airflow/serialization/schema.json) into the schema the airflow
20-
// package's DagSpec and TaskSpec generate from, so that the two structs are not
21-
// hand-maintained.
18+
// Command genspec rewrites Airflow core's Dag serialization schema (go-sdk's vendored
19+
// copy of it, schema/dag-schema.json) into the schema the airflow package's DagSpec
20+
// and TaskSpec generate from, so that the two structs are not hand-maintained.
2221
//
2322
// The schema is owned by Python and stays untouched; the rewritten copy is a build
2423
// artifact. genspec rewrites it in two passes: shapeForAuthoring turns the
@@ -45,7 +44,7 @@ func main() {
4544
schemaPath := flag.String(
4645
"schema",
4746
"",
48-
"path to airflow-core's Dag serialization schema.json",
47+
"path to the vendored copy of airflow-core's Dag serialization schema",
4948
)
5049
outPath := flag.String("out", "", "path to write the rewritten schema to")
5150
licensePath := flag.String(

‎go-sdk/internal/genspec/normalize_test.go‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@ import (
2626
"github.com/stretchr/testify/require"
2727
)
2828

29-
// coreSchemaPath is the schema genspec normalizes, reached from this package.
30-
const coreSchemaPath = "../../../airflow-core/src/airflow/serialization/schema.json"
29+
// coreSchemaPath is the schema genspec normalizes: go-sdk's vendored copy of
30+
// airflow-core's, so this test runs in a standalone checkout as well.
31+
const coreSchemaPath = "../../schema/dag-schema.json"
3132

3233
func schemaFrom(t *testing.T, body string) map[string]any {
3334
t.Helper()

‎go-sdk/pkg/execution/genmodels/gen.go‎

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,14 @@
1919
// messages (msgpack-over-socket) exchanged between the Airflow supervisor and a
2020
// Go SDK bundle in coordinator mode.
2121
//
22-
// These files are generated from the supervisor wire-schema snapshot owned by the
23-
// Python Task SDK (task-sdk/src/airflow/sdk/execution_time/schema/schema.json):
22+
// These files are generated from ../../../schema/supervisor-schema.json, go-sdk's
23+
// vendored copy of the supervisor wire-schema snapshot the Python Task SDK owns:
2424
// models.gen.go holds the struct types, discriminators.gen.go the Type<Name>
2525
// constants and EnsureType, defaults.gen.go the DecodeMsgpack methods that seed
2626
// non-zero schema defaults. Don't edit them by hand; change the Pydantic models,
27-
// let the generate-supervisor-schemas-snapshot prek hook refresh the snapshot,
28-
// then re-run `just generate-models` (go generate).
27+
// let the generate-supervisor-schemas-snapshot prek hook refresh the snapshot and
28+
// sync-go-sdk-schemas refresh the vendored copy, then re-run `just generate-models`
29+
// (go generate).
2930
//
3031
// go generate runs go-jsonschema, then the local gen tool, which strips
3132
// go-jsonschema's dead anyOf-branch typedefs, widens concrete int/float/bool
@@ -35,5 +36,5 @@
3536
// discriminators.gen.go and defaults.gen.go.
3637
package genmodels
3738

38-
//go:generate go run github.com/atombender/go-jsonschema@v0.23.1 --only-models --struct-name-from-title --tags msgpack --capitalization ID --capitalization URI --capitalization TI -p genmodels -o models.gen.go ../../../../task-sdk/src/airflow/sdk/execution_time/schema/schema.json
39-
//go:generate go run ./gen -schema ../../../../task-sdk/src/airflow/sdk/execution_time/schema/schema.json -models models.gen.go -out discriminators.gen.go -defaults defaults.gen.go
39+
//go:generate go run github.com/atombender/go-jsonschema@v0.23.1 --only-models --struct-name-from-title --tags msgpack --capitalization ID --capitalization URI --capitalization TI -p genmodels -o models.gen.go ../../../schema/supervisor-schema.json
40+
//go:generate go run ./gen -schema ../../../schema/supervisor-schema.json -models models.gen.go -out discriminators.gen.go -defaults defaults.gen.go

‎go-sdk/pkg/execution/messages_test.go‎

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -30,25 +30,18 @@ import (
3030
"github.com/apache/airflow/go-sdk/pkg/execution/genmodels"
3131
)
3232

33-
// sourceSchemaRelPath locates the canonical supervisor schema snapshot owned by
34-
// the Python Task SDK, relative to this package directory within the monorepo.
35-
// It is the same file genmodels is generated against (referenced by relative
36-
// path rather than vendored into go-sdk).
37-
const sourceSchemaRelPath = "../../../task-sdk/src/airflow/sdk/execution_time/schema/schema.json"
33+
// vendoredSchemaRelPath locates go-sdk's vendored copy of the supervisor schema
34+
// snapshot the Python Task SDK owns, relative to this package directory. It is
35+
// the file genmodels is generated against, and it ships with go-sdk, so this
36+
// test needs no monorepo around it. The sync-go-sdk-schemas prek hook is what
37+
// keeps the copy equal to task-sdk's.
38+
const vendoredSchemaRelPath = "../../schema/supervisor-schema.json"
3839

3940
// TestSupervisorSchemaVersionMatchesSnapshot pins SupervisorSchemaVersion to the
4041
// api_version of the schema the models are generated from, so a schema bump that
41-
// forgets to update the constant (or vice versa) fails loudly. It only runs when
42-
// the task-sdk source is reachable from the checkout, so a standalone go-sdk
43-
// build (without the rest of the monorepo) does not fail.
42+
// forgets to update the constant (or vice versa) fails loudly.
4443
func TestSupervisorSchemaVersionMatchesSnapshot(t *testing.T) {
45-
raw, err := os.ReadFile(sourceSchemaRelPath)
46-
if os.IsNotExist(err) {
47-
t.Skipf(
48-
"task-sdk schema source not reachable at %s; skipping version check",
49-
sourceSchemaRelPath,
50-
)
51-
}
44+
raw, err := os.ReadFile(vendoredSchemaRelPath)
5245
require.NoError(t, err)
5346
var snapshot struct {
5447
APIVersion string `json:"api_version"`

0 commit comments

Comments
 (0)