This is an automated email from the ASF dual-hosted git repository.

jason810496 pushed a commit to branch v3-4-test
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/v3-4-test by this push:
     new 5fbbb908236 [v3-4-test] Cover YAML-generated TypeScript Dags in the 
ts_sdk e2e test (#75054) (#75595)
5fbbb908236 is described below

commit 5fbbb908236610855f8c697ec46f6ed71332ed13
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Sun Oct 25 08:23:31 2026 +0000

    [v3-4-test] Cover YAML-generated TypeScript Dags in the ts_sdk e2e test 
(#75054) (#75595)
    
    The Dag processor runs a native TypeScript Dag's bundle on every
    re-parse, and the worker runs the same bundle for each task. So a
    bundle can build its Dags from a file in the Dag bundle, and swapping
    only that file should change the live Dags with no rebuild. The go_sdk
    and java_sdk e2e tests cover this, but nothing covered it for
    TypeScript.
    (cherry picked from commit f5329a6c8043c10cd5f3e06c436df6295144fc20)
    
    Co-authored-by: Andrew Chang <[email protected]>
---
 airflow-e2e-tests/docker/ts.yml                    |  6 ++
 .../ts_sdk_tests/test_ts_sdk_dynamic_dag.py        | 89 ++++++++++++++++++++++
 ts-sdk/example/package.json                        |  1 +
 ts-sdk/example/src/dynamic.ts                      | 51 +++++++++++++
 ts-sdk/example/src/main.ts                         |  8 +-
 ts-sdk/pnpm-lock.yaml                              |  3 +
 6 files changed, 156 insertions(+), 2 deletions(-)

diff --git a/airflow-e2e-tests/docker/ts.yml b/airflow-e2e-tests/docker/ts.yml
index 9306f57e3dd..90a1642b4e9 100644
--- a/airflow-e2e-tests/docker/ts.yml
+++ b/airflow-e2e-tests/docker/ts.yml
@@ -34,6 +34,12 @@ services:
       - nodejs-bin:/opt/nodejs
 
   airflow-dag-processor:
+    environment:
+      # The dynamic Dag test swaps a YAML file and waits for the Dag IDs to 
change. Re-parse the
+      # bundle every few seconds, and mark a Dag stale soon after it leaves 
the YAML.
+      AIRFLOW__DAG_PROCESSOR__MIN_FILE_PROCESS_INTERVAL: '5'
+      AIRFLOW__DAG_PROCESSOR__STALE_DAG_THRESHOLD: '10'
+      AIRFLOW__SCHEDULER__PARSING_CLEANUP_INTERVAL: '5'
     volumes:
       - ./ts-bundles:/opt/airflow/ts-bundles:ro
       - nodejs-bin:/opt/nodejs:ro
diff --git 
a/airflow-e2e-tests/tests/airflow_e2e_tests/ts_sdk_tests/test_ts_sdk_dynamic_dag.py
 
b/airflow-e2e-tests/tests/airflow_e2e_tests/ts_sdk_tests/test_ts_sdk_dynamic_dag.py
new file mode 100644
index 00000000000..2bdf64661b4
--- /dev/null
+++ 
b/airflow-e2e-tests/tests/airflow_e2e_tests/ts_sdk_tests/test_ts_sdk_dynamic_dag.py
@@ -0,0 +1,89 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""E2E test for the native TypeScript Dags that 
``ts-sdk/example/src/dynamic.ts`` generates from a YAML file.
+
+Run with::
+
+    E2E_TEST_MODE=ts_sdk uv run --project airflow-e2e-tests pytest \\
+        tests/airflow_e2e_tests/ts_sdk_tests/test_ts_sdk_dynamic_dag.py -xvs
+
+``conftest._setup_ts_sdk_integration`` puts the example bundle into the Dags 
folder, with no ``dags.yaml``
+next to it yet. The bundle generates one Dag for each entry of the YAML the 
test writes there, so the test
+swaps only the YAML, with no rebuild, and checks that the live Dag IDs follow 
it and that each live Dag
+runs.
+"""
+
+from __future__ import annotations
+
+from pathlib import Path
+
+from airflow_e2e_tests.e2e_test_utils.clients import AirflowClient
+from airflow_e2e_tests.e2e_test_utils.dynamic_dags import swap_yaml_and_check
+
+# The Dag processor runs node on the bundle to parse it, so the Dags appear 
some time after the stack is up.
+_PARSE_TIMEOUT = 600
+# After that, the Dag processor re-parses every few seconds (see ts.yml), and 
a Dag that left the
+# YAML goes stale a few seconds after that.
+_RELOAD_TIMEOUT = 180
+_TS_TASK_TIMEOUT = 600
+
+_TAGS = ["typescript_dynamic"]
+
+
+def test_swapping_the_yaml_changes_the_live_dags(airflow_dags_path: Path):
+    client = AirflowClient()
+    yaml_path = airflow_dags_path / "typescript" / "dags.yaml"
+
+    live = swap_yaml_and_check(
+        client,
+        yaml_path,
+        {
+            "typescript_dynamic_a": "hello from a",
+            "typescript_dynamic_b": "hello from b",
+            "typescript_dynamic_c": "hello from c",
+        },
+        previous=set(),
+        tags=_TAGS,
+        timeout=_PARSE_TIMEOUT,
+        task_timeout=_TS_TASK_TIMEOUT,
+    )
+    live = swap_yaml_and_check(
+        client,
+        yaml_path,
+        {
+            "typescript_dynamic_a": "hello from a",
+            "typescript_dynamic_b": "hello from b",
+            "typescript_dynamic_c": "hello from c",
+            "typescript_dynamic_d": "hello from d",
+            "typescript_dynamic_e": "hello from e",
+        },
+        previous=live,
+        tags=_TAGS,
+        timeout=_RELOAD_TIMEOUT,
+        task_timeout=_TS_TASK_TIMEOUT,
+    )
+    # a keeps its Dag ID but gets a new greeting. The greeting is not in the 
serialized Dag, so its run
+    # shows that the worker built the Dag from the new YAML, not that the Dag 
processor rebuilt it.
+    swap_yaml_and_check(
+        client,
+        yaml_path,
+        {"typescript_dynamic_a": "hello again from a", "typescript_dynamic_f": 
"hello from f"},
+        previous=live,
+        tags=_TAGS,
+        timeout=_RELOAD_TIMEOUT,
+        task_timeout=_TS_TASK_TIMEOUT,
+    )
diff --git a/ts-sdk/example/package.json b/ts-sdk/example/package.json
index d5e10ab3f57..35c74956eab 100644
--- a/ts-sdk/example/package.json
+++ b/ts-sdk/example/package.json
@@ -16,6 +16,7 @@
   "devDependencies": {
     "@types/node": "^26.1.2",
     "esbuild": "^0.28.2",
+    "js-yaml": "^5.4.1",
     "typescript": "^6.0.2"
   }
 }
diff --git a/ts-sdk/example/src/dynamic.ts b/ts-sdk/example/src/dynamic.ts
new file mode 100644
index 00000000000..fbb0edba580
--- /dev/null
+++ b/ts-sdk/example/src/dynamic.ts
@@ -0,0 +1,51 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+// Native Dags generated from the `dags.yaml` next to the bundle, one per 
entry.
+//
+// Both the Dag processor and the worker run the bundle, and each run reads the
+// YAML again, so swapping only the YAML changes the Dags with no rebuild.
+// Without a `dags.yaml` next to the bundle, as when `airflow-ts-pack` runs it 
to
+// read its metadata, there are no Dags, and the bundle's other Dags still 
parse.
+
+import { existsSync, readFileSync } from "node:fs";
+
+import { Dag } from "apache-airflow-ts-sdk";
+import { load } from "js-yaml";
+
+interface DagsFile {
+  readonly dags?: readonly { readonly dag_id: string; readonly greeting: 
string }[];
+}
+
+export function loadDynamicDags(): Dag[] {
+  const path = new URL("dags.yaml", import.meta.url);
+  if (!existsSync(path)) {
+    return [];
+  }
+  const file = load(readFileSync(path, "utf-8")) as DagsFile;
+  return (file.dags ?? []).map(({ dag_id, greeting }) => {
+    const dag = new Dag(dag_id, {
+      catchup: false,
+      tags: ["typescript", "typescript_dynamic"],
+      queue: "typescript",
+    });
+    dag.task("greet", async () => greeting)();
+    return dag;
+  });
+}
diff --git a/ts-sdk/example/src/main.ts b/ts-sdk/example/src/main.ts
index 756ec0fb762..2aea8ee5875 100644
--- a/ts-sdk/example/src/main.ts
+++ b/ts-sdk/example/src/main.ts
@@ -21,10 +21,12 @@
 //
 // The two Dags in `dags/` are declared in Python with `@task.stub` tasks 
routed to the Node
 // coordinator, so this side only supplies their task bodies. The third, in 
`native.ts`, is
-// declared in TypeScript outright — graph, schedule and all.
+// declared in TypeScript outright — graph, schedule and all. `dynamic.ts` 
adds a native Dag for
+// each entry of a YAML file next to the bundle.
 
 import { Bundle, getClient, getContext, NEVER_EXPIRE, TaskHandler } from 
"apache-airflow-ts-sdk";
 
+import { loadDynamicDags } from "./dynamic.js";
 import { dag as nativeDag } from "./native.js";
 import { buildSummaryMessage, report, summarize } from "./taskflow.js";
 
@@ -108,10 +110,12 @@ export async function readConnection() {
 // is bound to and never by the task_id alone.
 //
 // One bundle serves both authoring modes: handlers for the Dags a Python file 
declares, and the
-// natively declared Dag from `native.ts`, whose graph this side owns outright.
+// natively declared Dag from `native.ts`, whose graph this side owns 
outright. It also serves the
+// native Dags `dynamic.ts` generates from a YAML file next to the bundle, 
when there is one.
 const bundle = new Bundle();
 bundle.register(
   nativeDag,
+  ...loadDynamicDags(),
   new TaskHandler("typescript_example", "build_message", buildMessage),
   new TaskHandler("typescript_example", "read_connection", readConnection),
   new TaskHandler("typescript_example", "write_and_delete_variable", 
writeAndDeleteVariable),
diff --git a/ts-sdk/pnpm-lock.yaml b/ts-sdk/pnpm-lock.yaml
index 99964f27854..072408df53c 100644
--- a/ts-sdk/pnpm-lock.yaml
+++ b/ts-sdk/pnpm-lock.yaml
@@ -58,6 +58,9 @@ importers:
       esbuild:
         specifier: ^0.28.2
         version: 0.28.2
+      js-yaml:
+        specifier: ^5.4.1
+        version: 5.4.1
       typescript:
         specifier: ^6.0.2
         version: 6.0.3

Reply via email to