Skip to content

Commit afb1667

Browse files
committed
refactor(github): convert github utils to TaskFlow @task and chain
1 parent 2c38a3e commit afb1667

3 files changed

Lines changed: 147 additions & 26 deletions

File tree

dags/multipod/maxtext_e2e_tests.py

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@
1919
"""
2020
import datetime
2121
from airflow import models
22+
from airflow.models.baseoperator import chain
2223
from airflow.models.param import Param
23-
from airflow.operators.python import PythonOperator
2424
from airflow.operators.trigger_dagrun import TriggerDagRunOperator
2525
from airflow.utils.trigger_rule import TriggerRule
2626
from xlml.utils.github import fire_github_callback, validate_git_trigger
@@ -79,24 +79,25 @@
7979
poke_interval=600, # check every 10 minutes for child DAG completion
8080
)
8181

82-
github_callback_pre_training = PythonOperator(
82+
github_callback_pre_training = fire_github_callback.override(
8383
task_id="fire_github_callback_pre_training",
84-
python_callable=fire_github_callback,
85-
op_kwargs={"test_type": "pre_training"},
8684
trigger_rule=TriggerRule.ALL_SUCCESS,
87-
)
85+
)(test_type="pre_training")
8886

89-
github_callback_post_training = PythonOperator(
87+
github_callback_post_training = fire_github_callback.override(
9088
task_id="fire_github_callback_post_training",
91-
python_callable=fire_github_callback,
92-
op_kwargs={"test_type": "post_training"},
9389
trigger_rule=TriggerRule.ALL_SUCCESS,
94-
)
90+
)(test_type="post_training")
9591

96-
validate_task = PythonOperator(
97-
task_id="validate_trigger",
98-
python_callable=validate_git_trigger,
99-
)
92+
validate_task = validate_git_trigger()
10093

101-
(validate_task >> trigger_pre_training >> github_callback_pre_training)
102-
(validate_task >> trigger_post_training >> github_callback_post_training)
94+
chain(
95+
validate_task,
96+
trigger_pre_training,
97+
github_callback_pre_training,
98+
)
99+
chain(
100+
validate_task,
101+
trigger_post_training,
102+
github_callback_post_training,
103+
)

xlml/utils/github.py

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,20 @@
1515
"""Utilities for GitHub API integration."""
1616

1717
import requests
18+
from airflow.decorators import task
1819
from airflow.exceptions import AirflowFailException
20+
from airflow.operators.python import get_current_context
1921

2022

21-
def validate_git_trigger(**context):
23+
@task
24+
def validate_git_trigger() -> None:
2225
"""Validates that the DAG run has required GitHub parameters.
2326
2427
Prevents manual runs from the Airflow UI by ensuring github_run_id,
2528
github_repo, and github_callback_token are present.
2629
"""
27-
params = context["params"]
30+
context = get_current_context()
31+
params = context.get("params", {})
2832
run_id = params.get("github_run_id")
2933
repo = params.get("github_repo")
3034
token = params.get("github_callback_token")
@@ -36,25 +40,37 @@ def validate_git_trigger(**context):
3640
)
3741

3842

39-
def fire_github_callback(test_type: str | None = None, **context):
43+
@task
44+
def fire_github_callback(test_type: str | None = None) -> None:
4045
"""Fires a GitHub repository_dispatch callback with the DAG run result."""
41-
params = context["params"]
42-
dag_run = context["dag_run"]
46+
context = get_current_context()
47+
params = context.get("params", {})
48+
dag_run = context.get("dag_run")
49+
50+
repo = params.get("github_repo")
51+
token = params.get("github_callback_token")
52+
53+
if not repo or not token:
54+
raise AirflowFailException(
55+
"Missing required GitHub parameters (repo, token) to fire callback."
56+
)
4357

4458
client_payload = {
4559
"state": "success",
46-
"dag_id": dag_run.dag_id,
47-
"dag_run_id": dag_run.run_id,
48-
"sha": params["maxtext_sha"],
49-
"github_run_id": params["github_run_id"],
60+
"dag_id": dag_run.dag_id if dag_run else None,
61+
"dag_run_id": dag_run.run_id if dag_run else None,
62+
"sha": params.get("commit_sha")
63+
or params.get("maxtext_sha")
64+
or params.get("sha"),
65+
"github_run_id": params.get("github_run_id"),
5066
}
5167
if test_type:
5268
client_payload["test_type"] = test_type
5369

5470
response = requests.post(
55-
f"https://api.github.com/repos/{params['github_repo']}/dispatches",
71+
f"https://api.github.com/repos/{repo}/dispatches",
5672
headers={
57-
"Authorization": f"Bearer {params['github_callback_token']}",
73+
"Authorization": f"Bearer {token}",
5874
"Accept": "application/vnd.github+json",
5975
"X-GitHub-Api-Version": "2022-11-28",
6076
},

xlml/utils/github_test.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Tests for github.py."""
16+
17+
import unittest
18+
from unittest import mock
19+
from airflow.exceptions import AirflowFailException
20+
from xlml.utils import github
21+
22+
23+
class GithubTest(unittest.TestCase):
24+
25+
@mock.patch("xlml.utils.github.get_current_context")
26+
def test_validate_git_trigger_success(self, mock_context):
27+
mock_context.return_value = {
28+
"params": {
29+
"github_run_id": "12345",
30+
"github_repo": "owner/repo",
31+
"github_callback_token": "secret_token",
32+
}
33+
}
34+
# Should not raise exception
35+
github.validate_git_trigger.function()
36+
37+
@mock.patch("xlml.utils.github.get_current_context")
38+
def test_validate_git_trigger_missing_params(self, mock_context):
39+
mock_context.return_value = {
40+
"params": {
41+
"github_run_id": "12345",
42+
# missing repo and token
43+
}
44+
}
45+
with self.assertRaises(AirflowFailException):
46+
github.validate_git_trigger.function()
47+
48+
@mock.patch("requests.post")
49+
@mock.patch("xlml.utils.github.get_current_context")
50+
def test_fire_github_callback_success(self, mock_context, mock_post):
51+
mock_dag_run = mock.MagicMock()
52+
mock_dag_run.dag_id = "test_dag"
53+
mock_dag_run.run_id = "manual__1"
54+
55+
mock_context.return_value = {
56+
"dag_run": mock_dag_run,
57+
"params": {
58+
"github_repo": "owner/repo",
59+
"github_run_id": "12345",
60+
"github_callback_token": "secret_token",
61+
"maxtext_sha": "abc1234",
62+
},
63+
}
64+
65+
mock_response = mock.MagicMock()
66+
mock_post.return_value = mock_response
67+
68+
github.fire_github_callback.function(test_type="pre_training")
69+
70+
mock_post.assert_called_once_with(
71+
"https://api.github.com/repos/owner/repo/dispatches",
72+
headers={
73+
"Authorization": "Bearer secret_token",
74+
"Accept": "application/vnd.github+json",
75+
"X-GitHub-Api-Version": "2022-11-28",
76+
},
77+
json={
78+
"event_type": "airflow-dag-complete",
79+
"client_payload": {
80+
"state": "success",
81+
"dag_id": "test_dag",
82+
"dag_run_id": "manual__1",
83+
"sha": "abc1234",
84+
"github_run_id": "12345",
85+
"test_type": "pre_training",
86+
},
87+
},
88+
timeout=30,
89+
)
90+
mock_response.raise_for_status.assert_called_once()
91+
92+
@mock.patch("xlml.utils.github.get_current_context")
93+
def test_fire_github_callback_missing_token(self, mock_context):
94+
mock_context.return_value = {
95+
"params": {
96+
"github_repo": "owner/repo",
97+
}
98+
}
99+
with self.assertRaises(AirflowFailException):
100+
github.fire_github_callback.function()
101+
102+
103+
if __name__ == "__main__":
104+
unittest.main()

0 commit comments

Comments
 (0)