Skip to content

Commit 4e8b042

Browse files
jason810496claude
andcommitted
Go SDK: reject a templated DagID or RunID of TriggerDagRun
Mark the skipped and deferred states supported, since a TriggerDagRun task now emits both. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent d73904c commit 4e8b042

9 files changed

Lines changed: 48 additions & 20 deletions

File tree

‎airflow-core/tests/unit/dag_processing/lang_sdk_fixtures/go_native.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1637,7 +1637,7 @@
16371637
"conf": "py"
16381638
},
16391639
"trigger_dag_id": "conformance_minimal",
1640-
"trigger_run_id": "triggered_{{ run_id }}",
1640+
"trigger_run_id": "triggered_by_conformance",
16411641
"ui_color": "#ffefeb",
16421642
"wait_for_completion": true
16431643
}

‎go-sdk/README.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -337,8 +337,8 @@ prek hook regenerate it.
337337
| state: `success` | MUST | ✓ | 3.3 | |
338338
| state: `failed` | MUST | ✓ | 3.3 | |
339339
| state: `up_for_retry` | MUST | ✓ | 3.3 | RetryTask |
340-
| state: `skipped` | SHOULD | ✗ | – | runtime does not emit TaskState skipped yet |
341-
| state: `deferred` | MAY | ✗ | – | runtime does not emit DeferTask yet |
340+
| state: `skipped` | SHOULD | ✓ | 3.4 | airflow.TriggerDagRun with SkipWhenAlreadyExists only |
341+
| state: `deferred` | MAY | ✓ | 3.4 | airflow.TriggerDagRun with Deferrable only; resumes in the Go runtime |
342342
| state: `up_for_reschedule` | MAY | ✗ | – | runtime does not emit RescheduleTask yet |
343343
| state: `awaiting_input` | MAY | ✗ | – | runtime does not emit AwaitInputTask yet |
344344
| state: `removed` | MAY | ✓ | 3.3 | |

‎go-sdk/airflow/bundle_test.go‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -487,7 +487,7 @@ func TestServeLooksUpATriggerDagRunTaskWithACopyOfItsSpec(t *testing.T) {
487487
dag := Dag("native_etl")
488488
dag.Task(TriggerDagRun(TriggerDagRunSpec{
489489
DagID: "downstream_etl",
490-
RunID: "{{ run_id }}",
490+
RunID: "etl_downstream",
491491
Note: "from etl",
492492
Conf: map[string]any{"tables": []any{"rows"}},
493493
LogicalDate: logicalDate,
@@ -511,7 +511,7 @@ func TestServeLooksUpATriggerDagRunTaskWithACopyOfItsSpec(t *testing.T) {
511511
require.True(t, ok, "got %T", task)
512512
assert.Equal(t, bundle.TriggerSpec{
513513
DagID: "downstream_etl",
514-
RunID: "{{ run_id }}",
514+
RunID: "etl_downstream",
515515
Note: "from etl",
516516
Conf: map[string]any{"tables": []any{"rows"}},
517517
LogicalDate: logicalDate,

‎go-sdk/airflow/serialize_test.go‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -905,7 +905,7 @@ func TestSerializeWritesATriggerDagRunAsATriggerDagRunOperator(t *testing.T) {
905905
dag := Dag("etl", DagSpec{Queue: "golang"})
906906
dag.Task(TriggerDagRun(TriggerDagRunSpec{
907907
DagID: "downstream_etl",
908-
RunID: "{{ run_id }}",
908+
RunID: "etl_downstream",
909909
Conf: map[string]any{"rows": 2, "nested": map[string]any{"ok": true}},
910910
LogicalDate: time.Date(2026, 1, 31, 8, 30, 0, 500000000, time.UTC),
911911
RunAfter: time.Date(2026, 2, 1, 0, 0, 0, 0, time.UTC),
@@ -938,7 +938,7 @@ func TestSerializeWritesATriggerDagRunAsATriggerDagRunOperator(t *testing.T) {
938938
"template_fields_renderers": {"conf": "py"},
939939
"_operator_extra_links": {"Triggered DAG": "_link_TriggerDagRunLink"},
940940
"trigger_dag_id": "downstream_etl",
941-
"trigger_run_id": "{{ run_id }}",
941+
"trigger_run_id": "etl_downstream",
942942
"logical_date": "2026-01-31 08:30:00.500000+00:00",
943943
"conf": {"rows": 2, "nested": {"ok": true}},
944944
"wait_for_completion": true,

‎go-sdk/airflow/trigger_dag_run.go‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,9 @@ import (
3838
// leaves its parameter at the Python default. PokeInterval and Deferrable are pointers so that a
3939
// pointer to 0 or false can set that value instead of leaving the default.
4040
//
41-
// The task does not render templates. DagID, RunID, Note and the values in Conf are sent as they
42-
// are, so a string such as "{{ ds }}" reaches the new Dag run unchanged. The task does not offer
41+
// The task does not render templates. Note and the values in Conf are sent as they are, so a
42+
// string such as "{{ ds }}" reaches the new Dag run unchanged. DagID and RunID must not contain
43+
// "{{", since Airflow rejects such a dag_id or run_id. The task does not offer
4344
// openlineage_inject_parent_info, so it adds nothing to Conf.
4445
type TriggerDagRunSpec struct {
4546
// DagID is the dag_id of the Dag to trigger. It is required.
@@ -125,6 +126,16 @@ func copyTriggerDagRunSpec(spec TriggerDagRunSpec) (TriggerDagRunSpec, error) {
125126
if spec.DagID == "" {
126127
return TriggerDagRunSpec{}, errors.New("airflow.TriggerDagRunSpec has no DagID")
127128
}
129+
for _, field := range []struct{ name, value string }{
130+
{"DagID", spec.DagID}, {"RunID", spec.RunID},
131+
} {
132+
if strings.Contains(field.value, "{{") {
133+
return TriggerDagRunSpec{}, fmt.Errorf(
134+
"airflow.TriggerDagRunSpec.%s is %q; the task does not render templates",
135+
field.name, field.value,
136+
)
137+
}
138+
}
128139
if poke := spec.PokeInterval; poke != nil && (*poke < 0 || *poke%time.Second != 0) {
129140
return TriggerDagRunSpec{}, fmt.Errorf(
130141
"airflow.TriggerDagRunSpec.PokeInterval is %v; "+

‎go-sdk/airflow/trigger_dag_run_test.go‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ func TestTriggerDagRunIsATask(t *testing.T) {
3333
gate := dag.Task(extract)
3434
spec := TriggerDagRunSpec{
3535
DagID: "downstream_etl",
36-
RunID: "{{ run_id }}_downstream",
36+
RunID: "etl_downstream",
3737
Conf: map[string]any{"source": "etl"},
3838
LogicalDate: time.Date(2026, 9, 29, 0, 0, 0, 0, time.UTC),
3939
RunAfter: time.Date(2026, 9, 30, 0, 0, 0, 0, time.UTC),
@@ -111,6 +111,20 @@ func TestTriggerDagRunRejectsAnInvalidSpec(t *testing.T) {
111111
trigger: TriggerDagRun(TriggerDagRunSpec{WaitForCompletion: true}),
112112
want: "airflow.TriggerDagRunSpec has no DagID",
113113
},
114+
{
115+
name: "templated DagID",
116+
trigger: TriggerDagRun(TriggerDagRunSpec{DagID: "{{ params.target }}"}),
117+
want: `airflow.TriggerDagRunSpec.DagID is "{{ params.target }}"; ` +
118+
"the task does not render templates",
119+
},
120+
{
121+
name: "templated RunID",
122+
trigger: TriggerDagRun(
123+
TriggerDagRunSpec{DagID: "downstream_etl", RunID: "triggered_{{ run_id }}"},
124+
),
125+
want: `airflow.TriggerDagRunSpec.RunID is "triggered_{{ run_id }}"; ` +
126+
"the task does not render templates",
127+
},
114128
{
115129
name: "negative PokeInterval",
116130
trigger: TriggerDagRun(

‎go-sdk/capabilities.yaml‎

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,9 @@ min_airflow_version: "3.4"
2323
# bundle metadata. The render hook fails if the two disagree.
2424
supervisor_schema_version: "2026-10-30"
2525

26-
# The task runner terminates a task with SucceedTask, RetryTask, or TaskState (failed/removed); it
27-
# does not yet emit skipped, DeferTask, RescheduleTask, or AwaitInputTask.
26+
# The task runner terminates a task with SucceedTask, RetryTask, or TaskState (failed/removed); only
27+
# an airflow.TriggerDagRun task emits TaskState skipped or DeferTask. It does not emit RescheduleTask
28+
# or AwaitInputTask.
2829
states:
2930
success:
3031
supported: true
@@ -37,11 +38,13 @@ states:
3738
since: "3.3"
3839
note: "RetryTask"
3940
skipped:
40-
supported: false
41-
note: "runtime does not emit TaskState skipped yet"
41+
supported: true
42+
since: "3.4"
43+
note: "airflow.TriggerDagRun with SkipWhenAlreadyExists only"
4244
deferred:
43-
supported: false
44-
note: "runtime does not emit DeferTask yet"
45+
supported: true
46+
since: "3.4"
47+
note: "airflow.TriggerDagRun with Deferrable only; resumes in the Go runtime"
4548
up_for_reschedule:
4649
supported: false
4750
note: "runtime does not emit RescheduleTask yet"

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ func TestTriggerDagRunSendsTheOptionsAsWritten(t *testing.T) {
204204
supervisor := &triggerSupervisor{}
205205
spec := bundle.TriggerSpec{
206206
DagID: "downstream",
207-
RunID: "{{ run_id }}_x",
207+
RunID: "downstream_x",
208208
Note: "{{ ds }}",
209209
Conf: map[string]any{"day": "{{ ds }}", "rows": int64(2)},
210210
ResetDagRun: true,
@@ -214,11 +214,11 @@ func TestTriggerDagRunSendsTheOptionsAsWritten(t *testing.T) {
214214

215215
assertSucceedTask(t, result)
216216
sent := supervisor.request(t, "TriggerDagRun")
217-
assert.Equal(t, "{{ run_id }}_x", sent["run_id"], "an explicit run_id is not generated")
217+
assert.Equal(t, "downstream_x", sent["run_id"], "an explicit run_id is not generated")
218218
assert.Equal(t, "{{ ds }}", sent["note"])
219219
assert.Equal(t, map[string]any{"day": "{{ ds }}", "rows": int8(2)}, sent["conf"])
220220
assert.Equal(t, true, sent["reset_dag_run"])
221-
assert.Equal(t, "{{ run_id }}_x", supervisor.sent()[2]["value"])
221+
assert.Equal(t, "downstream_x", supervisor.sent()[2]["value"])
222222
}
223223

224224
func TestTriggerDagRunDatesAndRunID(t *testing.T) {

‎scripts/ci/lang_sdk_serialization/test_dags.yaml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,7 @@ dags:
303303
- task_id: trigger_all
304304
trigger_dag_run:
305305
trigger_dag_id: conformance_minimal
306-
trigger_run_id: "triggered_{{ run_id }}"
306+
trigger_run_id: "triggered_by_conformance"
307307
conf: {rows: 2, ratio: 0.5, nested: {tags: [a, b]}}
308308
logical_date: !datetime 2026-09-30T00:00:00+00:00
309309
wait_for_completion: true

0 commit comments

Comments
 (0)