Skip to content

Commit 23efbb2

Browse files
committed
TS SDK: generate Dag and task options from the serialization schema
A Dag authored in TypeScript has to describe itself with the same options Airflow's scheduler reads, and that vocabulary lives in airflow-core's Dag serialization schema. Hand-listing those options in the SDK would leave two copies to keep aligned by hand, and the copy the SDK ships would quietly rot every time core adds, renames, or drops a key. Deriving them instead makes the drift visible: a new scalar key shows up in the regenerated diff or fails generation until someone says why the SDK omits it, and a curated entry that outlives the key it names fails the same way. The serialization version is the one thing the schema cannot supply, so a core bump now fails a test in airflow-core rather than producing Dags stamped with a version core no longer writes. This follows the policy the Go SDK's TaskSpec generator set and ADR-0008 records, so the three language SDKs expose the same Dag surface for the same documented reasons.
1 parent a17d0fa commit 23efbb2

15 files changed

Lines changed: 2029 additions & 91 deletions

File tree

‎.pre-commit-config.yaml‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,16 @@ repos:
308308
additional_dependencies: ['PyYAML>=6.0', 'rich>=13.6.0']
309309
pass_filenames: false
310310
require_serial: true
311+
- id: sync-ts-sdk-dag-schema
312+
name: Sync TypeScript SDK Dag serialization schema with airflow-core
313+
description: "Copy airflow-core's serialization schema when TS SDK's vendored dag-schema.json drifts"
314+
entry: ./ts-sdk/scripts/ci/prek/sync_dag_schema.py
315+
language: python
316+
pass_filenames: false
317+
files: >
318+
(?x)
319+
^airflow-core/src/airflow/serialization/schema\.json$|
320+
^ts-sdk/schema/dag-schema\.json$
311321
- id: check-go-version-in-sync
312322
name: Check Go toolchain version is consistent across build files
313323
entry: ./scripts/ci/prek/check_go_version_in_sync.py

‎airflow-core/docs/authoring-and-scheduling/language-sdks/typescript.rst‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -243,9 +243,9 @@ task instance.
243243
Declaring a Dag in TypeScript
244244
-----------------------------
245245

246-
A ``Dag`` is declared on this side rather than in Python: its tasks and the edges between them are
247-
written in TypeScript. The surface is still growing, so a Dag declared this way is not served to
248-
Airflow yet.
246+
A ``Dag`` is declared on this side rather than in Python: its schedule, its tasks, their options and
247+
the edges between them are all written in TypeScript. The surface is still growing, so a Dag declared
248+
this way is not served to Airflow yet.
249249

250250
``dag.task(taskId, handler)`` returns a *factory*. Calling it places the task in the Dag and supplies the
251251
handler's arguments, so the call graph is the task graph:
@@ -277,7 +277,11 @@ argument itself: one buried inside an array or an object is a literal, and draws
277277
Every task has to be called exactly once. An uncalled task fails when the Dag is read, so none can be
278278
left out of the graph by accident.
279279

280-
``new Dag`` and ``dag.task`` also take a trailing ``spec`` object that is not used yet; do not set it.
280+
``new Dag`` and ``dag.task`` both take a trailing spec carrying the options Python would otherwise own:
281+
``{ schedule: "@daily", tags: ["etl"] }`` for the Dag, ``{ retries: 2, retryDelay: 30 }`` for a task.
282+
Both are generated from Airflow's own Dag serialization schema, so the field names are the schema's keys
283+
in camelCase, every field is optional, and a misspelled one is a compile error. The specs are recorded
284+
but not serialized yet.
281285

282286
Writing tasks
283287
-------------
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The ASF licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
from __future__ import annotations
18+
19+
import re
20+
21+
import pytest
22+
23+
from airflow.serialization.serialized_objects import DagSerialization
24+
25+
from tests_common.pytest_plugin import AIRFLOW_ROOT_PATH
26+
27+
GENERATED_FIELDS_PATH = AIRFLOW_ROOT_PATH / "ts-sdk" / "src" / "generated" / "dag-schema-fields.ts"
28+
29+
SERIALIZATION_VERSION_PATTERN = re.compile(
30+
r"^export const SERIALIZATION_VERSION = (?P<version>\d+);$", re.MULTILINE
31+
)
32+
33+
34+
@pytest.mark.skipif(
35+
not GENERATED_FIELDS_PATH.is_file(),
36+
reason="TypeScript SDK sources are absent (source distribution build)",
37+
)
38+
def test_ts_sdk_pins_the_current_serialization_version():
39+
"""
40+
The schema constrains ``__version`` only to a positive integer, so the TypeScript SDK
41+
pins it by hand. Without this check, bumping the serializer would leave the SDK emitting
42+
Dags stamped with a version core no longer writes.
43+
"""
44+
match = SERIALIZATION_VERSION_PATTERN.search(GENERATED_FIELDS_PATH.read_text(encoding="utf-8"))
45+
assert match is not None, f"No SERIALIZATION_VERSION export found in {GENERATED_FIELDS_PATH}"
46+
47+
assert int(match.group("version")) == DagSerialization.SERIALIZER_VERSION, (
48+
f"{GENERATED_FIELDS_PATH.relative_to(AIRFLOW_ROOT_PATH)} pins serialization version "
49+
f"{match.group('version')}, but DagSerialization.SERIALIZER_VERSION is "
50+
f"{DagSerialization.SERIALIZER_VERSION}. Update SERIALIZATION_VERSION in "
51+
"ts-sdk/scripts/generate-dag-schema.mjs, re-run `pnpm run generate:dag-schema`, and make "
52+
"sure the SDK still emits Dags core can read."
53+
)

‎ts-sdk/.pre-commit-config.yaml‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,18 @@ repos:
4949
additional_dependencies: ['pnpm@10.28.1']
5050
pass_filenames: false
5151
require_serial: true
52+
- id: check-ts-sdk-dag-schema
53+
name: Check TypeScript SDK Dag schema fields are up to date
54+
entry: ./scripts/ci/prek/check_dag_schema.py
55+
language: node
56+
files: >
57+
(?x)
58+
^src/generated/dag-schema-fields\.ts$|
59+
^schema/dag-schema\.json$|
60+
^scripts/generate-dag-schema\.mjs$
61+
additional_dependencies: ['pnpm@10.28.1']
62+
pass_filenames: false
63+
require_serial: true
5264
- id: check-ts-sdk-docs-package-version-in-sync
5365
name: Check ts-sdk/docs package.json versions match ts-sdk/package.json
5466
entry: ../scripts/ci/prek/check_ts_sdk_docs_package_version_in_sync.py

‎ts-sdk/package.json‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@
5151
"build": "pnpm run clean && tsc -p tsconfig.build.json",
5252
"verify:package": "node scripts/verify-package.mjs",
5353
"prepack": "pnpm run build",
54-
"generate:supervisor": "node scripts/generate-supervisor.mjs"
54+
"generate:supervisor": "node scripts/generate-supervisor.mjs",
55+
"generate:dag-schema": "node scripts/generate-dag-schema.mjs"
5556
},
5657
"keywords": [
5758
"airflow",

0 commit comments

Comments
 (0)