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

jason810496 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new 785d82da5c0 Cover native Go Dags in the go_sdk e2e test (#74411)
785d82da5c0 is described below

commit 785d82da5c0419aa97b0dc6e9510b0a7bd91a6c6
Author: Jason(Zhe-You) Liu <[email protected]>
AuthorDate: Fri Oct 9 19:50:16 2026 +0800

    Cover native Go Dags in the go_sdk e2e test (#74411)
---
 airflow-e2e-tests/docker/go.yml                    |   7 +
 .../tests/airflow_e2e_tests/conftest.py            |  43 ++--
 .../tests/airflow_e2e_tests/constants.py           |   3 +
 .../airflow_e2e_tests/e2e_test_utils/clients.py    |  30 ++-
 .../go_sdk_tests/test_go_sdk_native_dag.py         | 220 +++++++++++++++++++++
 go-sdk/example/native/main.go                      | 177 +++++++++++++++++
 go-sdk/example/native/report.go                    |  35 ++++
 7 files changed, 498 insertions(+), 17 deletions(-)

diff --git a/airflow-e2e-tests/docker/go.yml b/airflow-e2e-tests/docker/go.yml
index c1426963765..38fde7634d2 100644
--- a/airflow-e2e-tests/docker/go.yml
+++ b/airflow-e2e-tests/docker/go.yml
@@ -22,8 +22,15 @@
 # image can exec it directly -- no extra runtime needs to be installed. We only
 # bind-mount the bundle into the directory the ExecutableCoordinator scans and
 # point the worker at the "golang" queue where @task.stub tasks are routed.
+# The native-Dag bundle sits in the Dags folder, so the Dag processor execs it
+# to parse its Dags, with the same USER and HOME as the worker.
 ---
 services:
+  airflow-dag-processor:
+    environment:
+      USER: airflow
+      HOME: /home/airflow
+
   airflow-worker:
     # The bundle is built with CGO_ENABLED=0, so the SDK's user.Current() call 
at
     # init falls back to Go's pure-Go resolver, which reads $USER / $HOME and
diff --git a/airflow-e2e-tests/tests/airflow_e2e_tests/conftest.py 
b/airflow-e2e-tests/tests/airflow_e2e_tests/conftest.py
index 9814cb5147f..e57a8df764f 100644
--- a/airflow-e2e-tests/tests/airflow_e2e_tests/conftest.py
+++ b/airflow-e2e-tests/tests/airflow_e2e_tests/conftest.py
@@ -44,6 +44,8 @@ from airflow_e2e_tests.constants import (
     GO_SDK_BUNDLE_NAME,
     GO_SDK_DAGS_PATH,
     GO_SDK_EXAMPLE_BUNDLE_PKG,
+    GO_SDK_NATIVE_BUNDLE_NAME,
+    GO_SDK_NATIVE_BUNDLE_PKG,
     GO_SDK_ROOT_PATH,
     GO_SDK_STATE_STORE_RETENTION_DAYS,
     JAVA_COMPOSE_PATH,
@@ -536,10 +538,10 @@ def _setup_java_sdk_integration(dot_env_file, tmp_dir):
     os.environ["ENV_FILE_PATH"] = str(dot_env_file)
 
 
-def _run_go_sdk_pack(output_path, *, capture_output=False, native=False):
+def _run_go_sdk_pack(package, output_path, *, capture_output=False, 
native=False):
     """Run ``go tool airflow-go-pack`` natively or inside the pinned Go 
toolchain container.
 
-    ``go tool airflow-go-pack`` builds the bundle package, reads its
+    ``go tool airflow-go-pack`` builds the bundle *package*, reads its
     --airflow-metadata, and appends the source + airflow-metadata.yaml + the
     AFBNDL01 trailer, writing a single self-contained executable bundle.
     CGO_ENABLED=0 yields a fully static binary that runs on the stock worker.
@@ -569,7 +571,7 @@ def _run_go_sdk_pack(output_path, *, capture_output=False, 
native=False):
             "airflow-go-pack",
             "--output",
             str(output_path),
-            GO_SDK_EXAMPLE_BUNDLE_PKG,
+            package,
         ]
     else:
         cwd = None
@@ -600,20 +602,20 @@ def _run_go_sdk_pack(output_path, *, 
capture_output=False, native=False):
             "airflow-go-pack",
             "--output",
             f"{container_bin_dir}/{output_path.name}",
-            GO_SDK_EXAMPLE_BUNDLE_PKG,
+            package,
         ]
     return subprocess.run(argv, cwd=cwd, env=env, check=True, 
capture_output=capture_output, text=True)
 
 
-def _pack_go_sdk_example_bundle(*, native=False):
-    """Build the Go SDK example bundle, capturing output so a failure prints 
the build log."""
-    output_path = GO_SDK_BIN_PATH / GO_SDK_BUNDLE_NAME
+def _pack_go_sdk_bundle(package, name, *, native=False):
+    """Build a Go SDK bundle, capturing output so a failure prints the build 
log."""
+    output_path = GO_SDK_BIN_PATH / name
     mode_label = "host toolchain" if native else GO_BUILDER_IMAGE
-    console.print(f"[yellow]Building Go SDK example bundle ({mode_label})...")
+    console.print(f"[yellow]Building Go SDK bundle {package} 
({mode_label})...")
     try:
-        completed = _run_go_sdk_pack(output_path, capture_output=True, 
native=native)
+        completed = _run_go_sdk_pack(package, output_path, 
capture_output=True, native=native)
     except subprocess.CalledProcessError as e:
-        console.print("[red]Go SDK example bundle build failed:")
+        console.print(f"[red]Go SDK bundle {package} build failed:")
         console.print(e.stdout, e.stderr, sep="\n", markup=False, 
soft_wrap=True)
         raise
     console.print(completed.stdout, completed.stderr, sep="\n", markup=False, 
soft_wrap=True)
@@ -627,11 +629,16 @@ def _setup_go_sdk_integration(dot_env_file, tmp_dir):
     as the ``go-task-handlers`` Dag bundle, copies the Python stub Dag, and
     writes the coordinator configuration.
 
+    The native-Dag example is packed too, into the Dags folder, where the Dag
+    processor runs it to parse its Dags. Its tasks run on the ``golang`` queue
+    through the same coordinator.
+
     The packed bundle is a statically linked native executable (built with
     ``CGO_ENABLED=0``), so the stock Airflow worker image can exec it directly
     without a Go toolchain or any extra runtime installed -- see ``go.yml``.
     """
-    _pack_go_sdk_example_bundle(native=LANG_SDK_NATIVE_TOOLCHAIN)
+    _pack_go_sdk_bundle(GO_SDK_EXAMPLE_BUNDLE_PKG, GO_SDK_BUNDLE_NAME, 
native=LANG_SDK_NATIVE_TOOLCHAIN)
+    _pack_go_sdk_bundle(GO_SDK_NATIVE_BUNDLE_PKG, GO_SDK_NATIVE_BUNDLE_NAME, 
native=LANG_SDK_NATIVE_TOOLCHAIN)
 
     # Copy the compose override into the temp directory.
     copyfile(GO_COMPOSE_PATH, tmp_dir / "go.yml")
@@ -644,13 +651,25 @@ def _setup_go_sdk_integration(dot_env_file, tmp_dir):
     packed_bundle = go_bundles_dir / GO_SDK_BUNDLE_NAME
     copyfile(GO_SDK_BIN_PATH / GO_SDK_BUNDLE_NAME, packed_bundle)
     os.chmod(packed_bundle, 0o755)
+    # This bundle only registers task handlers, so the Dag processor must not 
parse it if the
+    # directory is ever mounted there. Tasks do not read .airflowignore.
+    (go_bundles_dir / ".airflowignore").write_text("*\n")
+
+    # The native-Dag bundle goes into the Dags folder, where the Dag processor 
runs it to parse.
+    native_dags_dir = tmp_dir / "dags" / "go"
+    native_dags_dir.mkdir()
+    packed_native_bundle = native_dags_dir / GO_SDK_NATIVE_BUNDLE_NAME
+    copyfile(GO_SDK_BIN_PATH / GO_SDK_NATIVE_BUNDLE_NAME, packed_native_bundle)
+    os.chmod(packed_native_bundle, 0o755)
 
     # Copy the Go SDK example stub Dag so Airflow can discover and serialize 
it.
     copyfile(GO_SDK_DAGS_PATH / "go_examples.py", tmp_dir / "dags" / 
"go_examples.py")
 
     # Coordinator registry: maps the logical name "go-sdk" to 
ExecutableCoordinator,
     # which scans the go-task-handlers Dag bundle for the packed bundle by 
dag_id.
-    # Queue mapping: routes tasks on the "golang" queue to "go-sdk".
+    # Queue mapping: routes tasks on the "golang" queue to "go-sdk". It is the 
only
+    # ExecutableCoordinator, so it also parses the native Dags in the Dags 
folder, and runs
+    # their tasks from that bundle, not from go-task-handlers.
     dag_bundle_config = _build_dag_bundle_config({"go-task-handlers": 
"/opt/airflow/go-bundles"})
     coordinator_config = json.dumps(
         {
diff --git a/airflow-e2e-tests/tests/airflow_e2e_tests/constants.py 
b/airflow-e2e-tests/tests/airflow_e2e_tests/constants.py
index d3ca78fe170..87e7fa5bdf6 100644
--- a/airflow-e2e-tests/tests/airflow_e2e_tests/constants.py
+++ b/airflow-e2e-tests/tests/airflow_e2e_tests/constants.py
@@ -92,6 +92,9 @@ GO_SDK_DAGS_PATH = GO_SDK_ROOT_PATH / "dags"
 GO_SDK_EXAMPLE_BUNDLE_PKG = "./example/bundle"
 # Name of the packed bundle binary (matches the example bundle's package dir 
name).
 GO_SDK_BUNDLE_NAME = "example_dags"
+# The native-Dag example: Dags declared in Go and parsed from the binary by 
the Dag processor.
+GO_SDK_NATIVE_BUNDLE_PKG = "./example/native"
+GO_SDK_NATIVE_BUNDLE_NAME = "go_native_dags"
 # Where airflow-go-pack writes the packed bundle inside the repo (go-sdk/bin 
is gitignored).
 GO_SDK_BIN_PATH = GO_SDK_ROOT_PATH / "bin"
 GO_COMPOSE_PATH = AIRFLOW_ROOT_PATH / "airflow-e2e-tests" / "docker" / "go.yml"
diff --git 
a/airflow-e2e-tests/tests/airflow_e2e_tests/e2e_test_utils/clients.py 
b/airflow-e2e-tests/tests/airflow_e2e_tests/e2e_test_utils/clients.py
index 273ab225320..d07cfe7af0d 100644
--- a/airflow-e2e-tests/tests/airflow_e2e_tests/e2e_test_utils/clients.py
+++ b/airflow-e2e-tests/tests/airflow_e2e_tests/e2e_test_utils/clients.py
@@ -19,6 +19,7 @@ from __future__ import annotations
 import time
 from datetime import UTC, datetime
 from functools import cached_property
+from http import HTTPStatus
 
 import boto3
 import requests
@@ -143,11 +144,7 @@ class AirflowClient:
     def wait_for_dag_run(self, dag_id: str, run_id: str, timeout=300, 
check_interval=5):
         start_time = time.time()
         while time.time() - start_time < timeout:
-            response = self._make_request(
-                method="GET",
-                endpoint=f"dags/{dag_id}/dagRuns/{run_id}",
-            )
-            state = response.get("state")
+            state = self.get_dag_run(dag_id=dag_id, run_id=run_id).get("state")
             if state in {"success", "failed"}:
                 return state
             time.sleep(check_interval)
@@ -172,6 +169,29 @@ class AirflowClient:
             endpoint = f"{endpoint}/{key}"
         return self._make_request(method="GET", 
endpoint=f"{endpoint}?map_index={map_index}")
 
+    def set_variable(self, key: str, value: str, description: str | None = 
None):
+        """Create or replace an Airflow Variable via API."""
+        body = {"key": key, "value": value, "description": description}
+        try:
+            return self._make_request(method="POST", endpoint="variables", 
json=body)
+        except requests.HTTPError as exc:
+            # 409 == it already exists, from an earlier run of the same suite.
+            if exc.response is None or exc.response.status_code != 
HTTPStatus.CONFLICT:
+                raise
+            return self._make_request(method="PATCH", 
endpoint=f"variables/{key}", json=body)
+
+    def get_tasks(self, dag_id: str):
+        """List a Dag's tasks, with the edges each one carries."""
+        return self._make_request(method="GET", 
endpoint=f"dags/{dag_id}/tasks")
+
+    def get_dag_source(self, dag_id: str):
+        """Get the source code stored for a Dag's latest version."""
+        return self._make_request(method="GET", 
endpoint=f"dagSources/{dag_id}")
+
+    def get_dag_run(self, dag_id: str, run_id: str):
+        """Get a Dag run, with its state, run type and conf."""
+        return self._make_request(method="GET", 
endpoint=f"dags/{dag_id}/dagRuns/{run_id}")
+
     def trigger_dag_and_wait(self, dag_id: str, json=None):
         """Trigger a DAG and wait for it to complete."""
         self.un_pause_dag(dag_id)
diff --git 
a/airflow-e2e-tests/tests/airflow_e2e_tests/go_sdk_tests/test_go_sdk_native_dag.py
 
b/airflow-e2e-tests/tests/airflow_e2e_tests/go_sdk_tests/test_go_sdk_native_dag.py
new file mode 100644
index 00000000000..43fcff7f4c8
--- /dev/null
+++ 
b/airflow-e2e-tests/tests/airflow_e2e_tests/go_sdk_tests/test_go_sdk_native_dag.py
@@ -0,0 +1,220 @@
+# 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 tests for the Go SDK native Dags of ``go-sdk/example/native``.
+
+Run with::
+
+    E2E_TEST_MODE=go_sdk uv run --project airflow-e2e-tests pytest \\
+        tests/airflow_e2e_tests/go_sdk_tests/test_go_sdk_native_dag.py -xvs
+
+No Python file declares these Dags. ``conftest._setup_go_sdk_integration`` 
packs the example with
+``airflow-go-pack`` into the Dags folder, where the Dag processor claims it 
through the
+``ExecutableCoordinator`` and runs it to parse. The worker runs each task from 
the same binary, on
+the ``golang`` queue the Dag sets.
+
+``go_native_pipeline`` has a nested task group, a fan-in, order-only edges, an 
``If``, a ``Switch``
+and a trigger-rule join. ``go_native_report`` is declared in a second file, so 
its Code view shows
+that file. ``go_native_trigger`` triggers ``go_native_report``.
+"""
+
+from __future__ import annotations
+
+from dataclasses import dataclass
+from datetime import UTC, datetime
+
+import pytest
+
+from airflow_e2e_tests.e2e_test_utils.clients import AirflowClient
+
+# The Dag processor execs the binary to parse it, so the Dags appear some time 
after the stack is up.
+_PARSE_TIMEOUT = 600
+_GO_TASK_TIMEOUT = 600
+
+_PIPELINE_DAG_ID = "go_native_pipeline"
+_REPORT_DAG_ID = "go_native_report"
+_TRIGGER_DAG_ID = "go_native_trigger"
+
+# Read by the example's tasks; see go-sdk/example/native/main.go.
+_GREETING_VARIABLE = "go_native_greeting"
+_CADENCE_VARIABLE = "go_native_cadence"
+_GREETING = "hello from the e2e test"
+# The test_http Connection that conftest._setup_go_sdk_integration defines.
+_CONNECTION_HOST = "example.com"
+
+
+@dataclass
+class _CompletedRun:
+    client: AirflowClient
+    run_id: str
+    state: str
+    ti_states: dict[str, str]
+
+    def xcom(self, task_id: str, key: str = "return_value"):
+        return self.client.get_xcom_value(
+            dag_id=_PIPELINE_DAG_ID, task_id=task_id, run_id=self.run_id, 
key=key
+        ).get("value")
+
+
[email protected](scope="module")
+def parsed_dags() -> AirflowClient:
+    """A client that has waited for the Dag processor to parse and register 
all three Dags."""
+    client = AirflowClient()
+    for dag_id in (_PIPELINE_DAG_ID, _REPORT_DAG_ID, _TRIGGER_DAG_ID):
+        client.wait_for_dag(dag_id, timeout=_PARSE_TIMEOUT)
+    return client
+
+
[email protected](scope="module")
+def completed_run(parsed_dags: AirflowClient) -> _CompletedRun:
+    """Trigger the pipeline once, with the Variables its tasks read."""
+    client = parsed_dags
+    client.set_variable(_GREETING_VARIABLE, _GREETING)
+    client.set_variable(_CADENCE_VARIABLE, "weekly")
+
+    resp = client.trigger_dag(_PIPELINE_DAG_ID, json={"logical_date": 
datetime.now(UTC).isoformat()})
+    run_id = resp["dag_run_id"]
+    state = client.wait_for_dag_run(dag_id=_PIPELINE_DAG_ID, run_id=run_id, 
timeout=_GO_TASK_TIMEOUT)
+    ti_resp = client.get_task_instances(dag_id=_PIPELINE_DAG_ID, run_id=run_id)
+    return _CompletedRun(
+        client=client,
+        run_id=run_id,
+        state=state,
+        ti_states={ti["task_id"]: ti.get("state") for ti in 
ti_resp.get("task_instances", [])},
+    )
+
+
+def _edges(client: AirflowClient, dag_id: str) -> tuple[dict[str, set[str]], 
dict[str, set[str]]]:
+    """Return the downstream and the upstream task ids of every task of 
*dag_id*."""
+    tasks = client.get_tasks(dag_id).get("tasks", [])
+    downstream = {task["task_id"]: set(task["downstream_task_ids"]) for task 
in tasks}
+    # The API reports downstream edges only, so upstream is read by inverting 
them.
+    upstream: dict[str, set[str]] = {task_id: set() for task_id in downstream}
+    for task_id, down in downstream.items():
+        for child in down:
+            upstream.setdefault(child, set()).add(task_id)
+    return downstream, upstream
+
+
+def test_the_dags_the_binary_parsed_are_registered(parsed_dags: AirflowClient):
+    """The Dags exist without any Python file declaring them."""
+    for dag_id in (_PIPELINE_DAG_ID, _REPORT_DAG_ID, _TRIGGER_DAG_ID):
+        dag = parsed_dags.get_dag(dag_id)
+        assert dag["dag_id"] == dag_id
+        assert {tag["name"] for tag in dag.get("tags") or []} >= {"go", 
"native"}
+
+
+def test_the_graph_carries_every_construct(parsed_dags: AirflowClient):
+    """Group prefixes, the group edge, the fan-in, the branches and the join 
survive parsing."""
+    downstream, upstream = _edges(parsed_dags, _PIPELINE_DAG_ID)
+
+    # The nested groups prefixed their tasks, and the task-to-group edge 
expanded onto the
+    # tasks that the group starts with.
+    assert {"extract.regions.north", "extract.regions.south", 
"extract.summarize"} <= set(downstream)
+    assert downstream["seed"] >= {"extract.regions.north", 
"extract.regions.south"}
+    # The fan-in.
+    assert upstream["extract.summarize"] >= {"extract.regions.north", 
"extract.regions.south"}
+    # The condition and the switch reach their candidates.
+    assert downstream["anyRows"] >= {"loadRows", "reportEmpty"}
+    assert downstream["pick"] >= {"publishDaily", "publishWeekly"}
+    # Cleanup sits behind every branch outcome, which is what its trigger rule 
is for.
+    assert upstream["cleanup"] >= {"loadRows", "reportEmpty", "publishDaily", 
"publishWeekly"}
+
+
+def test_dag_run_succeeded(completed_run: _CompletedRun):
+    assert completed_run.state == "success", (
+        f"expected the run to succeed; got {completed_run.state!r}. task 
states: {completed_run.ti_states}"
+    )
+
+
+def test_the_taken_branches_ran_and_the_others_skipped(completed_run: 
_CompletedRun):
+    """A branch is a run-time skip, so the states are what prove it worked."""
+    states = completed_run.ti_states
+    skipped = {"reportEmpty", "publishDaily"}
+    ran = {
+        "seed",
+        "extract.regions.north",
+        "extract.regions.south",
+        "extract.summarize",
+        "loadRows",
+        "anyRows",
+        "pick",
+        "publishWeekly",
+        "cleanup",
+    }
+    assert set(states) == ran | skipped, states
+    for task_id in ran:
+        assert states[task_id] == "success", f"{task_id!r} did not succeed. 
task states: {states}"
+    for task_id in skipped:
+        assert states[task_id] == "skipped", f"{task_id!r} was not skipped. 
task states: {states}"
+
+
+def test_xcoms_flow_between_go_tasks(completed_run: _CompletedRun):
+    """Each task got its literals and upstream results, and its own return 
came back."""
+    assert completed_run.xcom("extract.regions.north") == {"region": "north", 
"rows": 3}
+    assert completed_run.xcom("extract.regions.south") == {"region": "south", 
"rows": 2}
+    assert completed_run.xcom("extract.summarize") == {"total": 5, "regions": 
2}
+    assert completed_run.xcom("loadRows") == "s3://bucket/out"
+
+
+def test_the_decisions_are_recorded(completed_run: _CompletedRun):
+    """A condition returns its boolean, and a switch the task id it chose."""
+    assert completed_run.xcom("anyRows") is True
+    assert completed_run.xcom("pick") == "publishWeekly"
+
+
+def test_the_variable_and_connection_reached_the_go_task(completed_run: 
_CompletedRun):
+    """``seed`` read the REST-set Variable and the Connection that conftest 
defines."""
+    assert completed_run.xcom("seed") == {"greeting": _GREETING, "host": 
_CONNECTION_HOST}
+
+
+def test_a_task_pulls_the_xcom_another_pushed_under_its_own_key(completed_run: 
_CompletedRun):
+    note = f"seeded {_GREETING} from {_CONNECTION_HOST}"
+    assert completed_run.xcom("seed", key="seed_note") == note
+    assert completed_run.xcom("cleanup") == note
+
+
+def test_the_dag_source_is_the_file_that_declares_the_dag(parsed_dags: 
AirflowClient):
+    """The Code view shows the source file of each Dag, not always the 
entrypoint."""
+    report = parsed_dags.get_dag_source(_REPORT_DAG_ID)
+    assert 'airflow.Dag("go_native_report"' in report["content"]
+    assert "func main(" not in report["content"]
+    assert report.get("language") == "go"
+
+    pipeline = parsed_dags.get_dag_source(_PIPELINE_DAG_ID)
+    assert "func main(" in pipeline["content"]
+
+
+def test_the_trigger_started_the_report_run(parsed_dags: AirflowClient):
+    client = parsed_dags
+    # Dags are paused at creation here, so the run the trigger starts would 
stay queued.
+    client.un_pause_dag(_REPORT_DAG_ID)
+    resp = client.trigger_dag(_TRIGGER_DAG_ID, json={"logical_date": 
datetime.now(UTC).isoformat()})
+    trigger_run_id = resp["dag_run_id"]
+    state = client.wait_for_dag_run(dag_id=_TRIGGER_DAG_ID, 
run_id=trigger_run_id, timeout=_GO_TASK_TIMEOUT)
+    assert state == "success"
+
+    report_run_id = client.get_xcom_value(
+        dag_id=_TRIGGER_DAG_ID, task_id="trigger_report", 
run_id=trigger_run_id, key="trigger_run_id"
+    )["value"]
+    report_state = client.wait_for_dag_run(
+        dag_id=_REPORT_DAG_ID, run_id=report_run_id, timeout=_GO_TASK_TIMEOUT
+    )
+    assert report_state == "success"
+
+    report_run = client.get_dag_run(dag_id=_REPORT_DAG_ID, 
run_id=report_run_id)
+    assert report_run["run_type"] == "operator_triggered"
+    assert report_run["conf"] == {"triggered_by": _TRIGGER_DAG_ID}
diff --git a/go-sdk/example/native/main.go b/go-sdk/example/native/main.go
new file mode 100644
index 00000000000..fda85d96eb6
--- /dev/null
+++ b/go-sdk/example/native/main.go
@@ -0,0 +1,177 @@
+// 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.
+
+// Package main is an example of Dags written entirely in Go with airflow.Dag, 
with no Python stub
+// Dag. The Dag processor runs the packed binary to parse them, and the worker 
runs it for each
+// task.
+//
+// Pack it with `go tool airflow-go-pack ./example/native` and put the result 
in a Dag bundle.
+package main
+
+import (
+       "fmt"
+       "log"
+
+       "github.com/apache/airflow/go-sdk/airflow"
+)
+
+type regionRows struct {
+       Region string `json:"region"`
+       Rows   int    `json:"rows"`
+}
+
+type summary struct {
+       Total   int `json:"total"`
+       Regions int `json:"regions"`
+}
+
+// pipeline holds the tasks that the switch chooses from, because a decider 
returns a *TaskRef.
+type pipeline struct {
+       daily, weekly *airflow.TaskRef
+}
+
+func main() {
+       bundle := airflow.Bundle()
+       bundle.Register(
+               newPipelineDag(),
+               newTriggerDag(),
+               newReportDag(),
+       )
+
+       if err := bundle.Serve(); err != nil {
+               log.Fatal(err)
+       }
+}
+
+func newPipelineDag() *airflow.DagRef {
+       dag := airflow.Dag("go_native_pipeline", airflow.DagSpec{
+               Queue: "golang",
+               Tags:  []string{"go", "native"},
+       })
+
+       seeded := dag.Task(seed)
+
+       extract := dag.TaskGroup("extract")
+       regions := extract.TaskGroup("regions")
+       north := regions.Task(
+               countRows,
+               airflow.TaskSpec{TaskID: "north"},
+               airflow.Inputs(airflow.Literal("north"), airflow.Literal(3)),
+       )
+       south := regions.Task(
+               countRows,
+               airflow.TaskSpec{TaskID: "south"},
+               airflow.Inputs(airflow.Literal("south"), airflow.Literal(2)),
+       )
+       summed := extract.Task(summarize, airflow.Inputs(north, south))
+       seeded.Before(airflow.Label(extract, "fan out"))
+
+       loaded := dag.Task(loadRows, airflow.Inputs(summed, 
airflow.Literal("s3://bucket/out")))
+       empty := dag.Task(reportEmpty)
+       dag.If(anyRows, airflow.Inputs(summed)).Then(loaded).Else(empty)
+
+       p := &pipeline{
+               daily:  dag.Task(publishDaily),
+               weekly: dag.Task(publishWeekly),
+       }
+       dag.Switch(p.pick, airflow.Inputs(summed)).Case(p.daily).Case(p.weekly)
+
+       // Each If and Switch skips one side, so cleanup has to run after a 
skipped upstream task.
+       dag.Task(cleanup, airflow.TaskSpec{TriggerRule: 
airflow.TriggerRuleNoneFailedMinOneSuccess}).
+               After(loaded, empty, p.daily, p.weekly)
+
+       return dag
+}
+
+func newTriggerDag() *airflow.DagRef {
+       dag := airflow.Dag(
+               "go_native_trigger",
+               airflow.DagSpec{Queue: "golang", Tags: []string{"go", 
"native"}},
+       )
+       dag.Task(
+               airflow.TriggerDagRun(airflow.TriggerDagRunSpec{
+                       DagID: "go_native_report",
+                       Conf:  map[string]any{"triggered_by": 
"go_native_trigger"},
+               }),
+               airflow.TaskSpec{TaskID: "trigger_report"},
+       )
+       return dag
+}
+
+// seed reads a Variable and a Connection, and leaves a note for cleanup under 
its own XCom key.
+func seed(actx airflow.Context) (map[string]any, error) {
+       client := actx.Client()
+       greeting, err := client.GetVariable(actx, "go_native_greeting")
+       if err != nil {
+               return nil, err
+       }
+       conn, err := client.GetConnection(actx, "test_http")
+       if err != nil {
+               return nil, err
+       }
+       note := fmt.Sprintf("seeded %s from %s", greeting, conn.Host)
+       if err := client.PushXCom(actx, actx.TaskInstance(), "seed_note", 
note); err != nil {
+               return nil, err
+       }
+       return map[string]any{"greeting": greeting, "host": conn.Host}, nil
+}
+
+func countRows(_ airflow.Context, region string, rows int) (regionRows, error) 
{
+       return regionRows{Region: region, Rows: rows}, nil
+}
+
+func summarize(_ airflow.Context, north, south regionRows) (summary, error) {
+       return summary{Total: north.Rows + south.Rows, Regions: 2}, nil
+}
+
+func anyRows(_ airflow.Context, summed summary) (bool, error) {
+       return summed.Total > 0, nil
+}
+
+func loadRows(_ airflow.Context, _ summary, target string) (string, error) {
+       return target, nil
+}
+
+func reportEmpty(airflow.Context) (string, error) {
+       return "no rows", nil
+}
+
+// pick chooses the task that the Variable names.
+func (p *pipeline) pick(actx airflow.Context, _ summary) (*airflow.TaskRef, 
error) {
+       cadence, err := actx.Client().GetVariable(actx, "go_native_cadence")
+       if err != nil {
+               return nil, err
+       }
+       if cadence == "weekly" {
+               return p.weekly, nil
+       }
+       return p.daily, nil
+}
+
+func publishDaily(airflow.Context) (string, error) {
+       return "daily", nil
+}
+
+func publishWeekly(airflow.Context) (string, error) {
+       return "weekly", nil
+}
+
+// cleanup returns the note that seed pushed.
+func cleanup(actx airflow.Context) (any, error) {
+       ti := actx.TaskInstance()
+       return actx.Client().GetXCom(actx, ti.DagID, ti.RunID, "seed", nil, 
"seed_note", nil)
+}
diff --git a/go-sdk/example/native/report.go b/go-sdk/example/native/report.go
new file mode 100644
index 00000000000..1a22c49c387
--- /dev/null
+++ b/go-sdk/example/native/report.go
@@ -0,0 +1,35 @@
+// 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.
+
+package main
+
+import "github.com/apache/airflow/go-sdk/airflow"
+
+// newReportDag lives in its own file, so the Code view of go_native_report 
shows this file and
+// not main.go.
+func newReportDag() *airflow.DagRef {
+       dag := airflow.Dag("go_native_report", airflow.DagSpec{
+               Queue: "golang",
+               Tags:  []string{"go", "native"},
+       })
+       dag.Task(render)
+       return dag
+}
+
+func render(airflow.Context) (string, error) {
+       return "report rendered", nil
+}

Reply via email to