Skip to content

Commit 6d95200

Browse files
authored
runs changed examples again (#1111)
* incomplete WIP * move secret into step that needs it, use script instead of action * not .py, as module * commit a python file to test diff workflow * remove unneeded dummy python file * adjust event name handling * add test event with no changed files
1 parent 3e3cba1 commit 6d95200

File tree

4 files changed

+301
-0
lines changed

4 files changed

+301
-0
lines changed

Diff for: .github/workflows/run-examples.yml

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
name: Run
2+
3+
on:
4+
pull_request:
5+
branches:
6+
- main
7+
paths:
8+
- "**.py"
9+
push:
10+
branches:
11+
- main
12+
paths:
13+
- "**.py"
14+
workflow_dispatch:
15+
16+
# Cancel previous runs of the same PR but do not cancel previous runs on main
17+
concurrency:
18+
group: ${{ github.workflow }}-${{ github.ref }}
19+
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
20+
21+
env:
22+
TERM: linux
23+
TERMINFO: /etc/terminfo
24+
MODAL_ENVIRONMENT: examples
25+
26+
jobs:
27+
# Output all changed files in a JSON format compatible with GitHub Actions job matrices
28+
diff-matrix:
29+
name: Generate matrix of changed examples
30+
runs-on: ubuntu-24.04
31+
outputs:
32+
matrix: ${{ steps.diff.outputs.all_changed_files }}
33+
34+
steps:
35+
- uses: actions/checkout@v3
36+
with:
37+
fetch-depth: 0
38+
39+
- name: Find changed examples
40+
id: diff
41+
run: python3 -m internal.generate_diff_matrix
42+
43+
# Run each changed example, using the output of the previous step as a job matrix
44+
run-changed:
45+
name: Run changed example
46+
needs: [diff-matrix]
47+
if:
48+
${{ needs.diff-matrix.outputs.matrix != '[]' &&
49+
needs.diff-matrix.outputs.matrix != '' }}
50+
runs-on: ubuntu-24.04
51+
strategy:
52+
matrix:
53+
file: ${{ fromJson(needs.diff-matrix.outputs.matrix) }}
54+
fail-fast: false
55+
56+
steps:
57+
- name: Checkout Repository
58+
uses: actions/checkout@v3
59+
with:
60+
fetch-depth: 1
61+
- uses: ./.github/actions/setup
62+
63+
- name: Run example
64+
run: |
65+
echo "Running ${{ matrix.file }}"
66+
stem=$(basename "${{ matrix.file }}" .py)
67+
python3 -m internal.run_example $stem || exit $?
68+
env:
69+
MODAL_TOKEN_ID: ${{ secrets.MODAL_MODAL_LABS_TOKEN_ID }}
70+
MODAL_TOKEN_SECRET: ${{ secrets.MODAL_MODAL_LABS_TOKEN_SECRET }}

Diff for: internal/generate_diff_matrix.py

+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import json
2+
import os
3+
import subprocess
4+
import sys
5+
6+
7+
def load_event():
8+
event_path = os.environ.get("GITHUB_EVENT_PATH")
9+
if not event_path:
10+
print("GITHUB_EVENT_PATH not set", file=sys.stderr)
11+
sys.exit(1)
12+
try:
13+
with open(event_path, "r") as f:
14+
return json.load(f)
15+
except Exception as e:
16+
print(f"Error loading event JSON: {e}", file=sys.stderr)
17+
sys.exit(1)
18+
19+
20+
def determine_diff_range(event, event_name):
21+
if event_name == "pull_request":
22+
try:
23+
base = event["pull_request"]["base"]["sha"]
24+
head = event["pull_request"]["head"]["sha"]
25+
except KeyError as e:
26+
print(f"Missing key in pull_request event: {e}", file=sys.stderr)
27+
sys.exit(1)
28+
elif event_name == "push":
29+
base = event.get("before")
30+
head = event.get("after")
31+
else:
32+
print(f"Unsupported event type: {event_name}", file=sys.stderr)
33+
sys.exit(1)
34+
35+
if not base or not head:
36+
print("Could not determine base and head commits", file=sys.stderr)
37+
sys.exit(1)
38+
return base, head
39+
40+
41+
def get_changed_files(base, head):
42+
try:
43+
result = subprocess.run(
44+
["git", "diff", "--name-only", base, head],
45+
capture_output=True,
46+
text=True,
47+
check=True,
48+
)
49+
return result.stdout.splitlines()
50+
except subprocess.CalledProcessError as e:
51+
print(f"Error running git diff: {e}", file=sys.stderr)
52+
sys.exit(1)
53+
54+
55+
def filter_files(files):
56+
return [
57+
f
58+
for f in files
59+
if f.endswith(".py")
60+
and not (f.startswith("internal/") or f.startswith("misc/"))
61+
]
62+
63+
64+
def write_output(key, value):
65+
github_output = os.environ.get("GITHUB_OUTPUT")
66+
if github_output:
67+
try:
68+
with open(github_output, "a") as out:
69+
out.write(f"{key}={value}\n")
70+
except Exception as e:
71+
print(f"Error writing to GITHUB_OUTPUT: {e}", file=sys.stderr)
72+
73+
74+
def main():
75+
event = load_event()
76+
event_name = event.get("event_name") or os.environ.get("GITHUB_EVENT_NAME")
77+
base, head = determine_diff_range(event, event_name)
78+
changed_files = get_changed_files(base, head)
79+
filtered_files = filter_files(changed_files)
80+
json_output = json.dumps(filtered_files)
81+
write_output("all_changed_files", json_output)
82+
print(json_output)
83+
84+
85+
if __name__ == "__main__":
86+
main()

Diff for: internal/test-event.json

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"event_name": "pull_request",
3+
"pull_request": {
4+
"base": {
5+
"ref": "main",
6+
"sha": "3e3cba16881e73a80887c2f09477e86f0522b072"
7+
},
8+
"head": {
9+
"ref": "charlesfrye/run-examples-again",
10+
"sha": "b639aa6e806d2db555cbf4cfc29f2b93c4d50fcb"
11+
}
12+
},
13+
"repository": {
14+
"full_name": "modal-labs/modal-examples"
15+
},
16+
"ref": "refs/pull/1/merge"
17+
}
18+

Diff for: internal/test_generate_diff_matrix.py

+127
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import json
2+
import subprocess
3+
4+
import generate_diff_matrix as gdm
5+
import pytest
6+
7+
8+
def test_determine_diff_range_push():
9+
event = {"before": "commit1", "after": "commit2"}
10+
base, head = gdm.determine_diff_range(event, "push")
11+
assert base == "commit1"
12+
assert head == "commit2"
13+
14+
15+
def test_determine_diff_range_pull():
16+
event = {
17+
"pull_request": {
18+
"base": {"sha": "base_sha"},
19+
"head": {"sha": "head_sha"},
20+
}
21+
}
22+
base, head = gdm.determine_diff_range(event, "pull_request")
23+
assert base == "base_sha"
24+
assert head == "head_sha"
25+
26+
27+
def test_determine_diff_range_invalid_event():
28+
event = {}
29+
with pytest.raises(SystemExit):
30+
gdm.determine_diff_range(event, "unsupported_event")
31+
32+
33+
def test_filter_files():
34+
files = [
35+
"example.py",
36+
"internal/test.py",
37+
"misc/skip.py",
38+
"script.js",
39+
"dir/another.py",
40+
]
41+
filtered = gdm.filter_files(files)
42+
assert filtered == ["example.py", "dir/another.py"]
43+
44+
45+
def test_get_changed_files(monkeypatch):
46+
class DummyCompletedProcess:
47+
def __init__(self, stdout):
48+
self.stdout = stdout
49+
50+
def fake_run(args, capture_output, text, check):
51+
return DummyCompletedProcess("file1.py\nfile2.py\n")
52+
53+
monkeypatch.setattr(subprocess, "run", fake_run)
54+
files = gdm.get_changed_files("base", "head")
55+
assert files == ["file1.py", "file2.py"]
56+
57+
58+
def test_write_output(tmp_path, monkeypatch):
59+
temp_output = tmp_path / "github_output.txt"
60+
monkeypatch.setenv("GITHUB_OUTPUT", str(temp_output))
61+
62+
gdm.write_output("test_key", "test_value")
63+
64+
with open(temp_output, "r") as f:
65+
content = f.read()
66+
assert "test_key=test_value" in content
67+
68+
69+
def test_main_push(monkeypatch, tmp_path):
70+
# simulate a push event by creating a temporary event JSON
71+
event_data = {"before": "commit1", "after": "commit2"}
72+
event_file = tmp_path / "event.json"
73+
event_file.write_text(json.dumps(event_data))
74+
75+
monkeypatch.setenv("GITHUB_EVENT_PATH", str(event_file))
76+
monkeypatch.setenv("GITHUB_EVENT_NAME", "push")
77+
78+
output_file = tmp_path / "output.txt"
79+
monkeypatch.setenv("GITHUB_OUTPUT", str(output_file))
80+
81+
# override get_changed_files to simulate a git diff call
82+
def fake_get_changed_files(base, head):
83+
return ["file1.py", "internal/ignore.py", "misc/skip.py", "dir/keep.py"]
84+
85+
monkeypatch.setattr(gdm, "get_changed_files", fake_get_changed_files)
86+
87+
gdm.main()
88+
89+
with open(output_file, "r") as f:
90+
output_content = f.read().strip()
91+
expected = json.dumps(["file1.py", "dir/keep.py"])
92+
assert f"all_changed_files={expected}" in output_content
93+
94+
95+
def test_main_pull(monkeypatch, tmp_path):
96+
# simulate a pull_request event
97+
event_data = {
98+
"pull_request": {
99+
"base": {"sha": "base_commit"},
100+
"head": {"sha": "head_commit"},
101+
}
102+
}
103+
event_file = tmp_path / "event.json"
104+
event_file.write_text(json.dumps(event_data))
105+
106+
monkeypatch.setenv("GITHUB_EVENT_PATH", str(event_file))
107+
monkeypatch.setenv("GITHUB_EVENT_NAME", "pull_request")
108+
109+
output_file = tmp_path / "output.txt"
110+
monkeypatch.setenv("GITHUB_OUTPUT", str(output_file))
111+
112+
def fake_get_changed_files(base, head):
113+
return [
114+
"pull_file.py",
115+
"internal/not_this.py",
116+
"misc/also_not.py",
117+
"folder/keep_this.py",
118+
]
119+
120+
monkeypatch.setattr(gdm, "get_changed_files", fake_get_changed_files)
121+
122+
gdm.main()
123+
124+
with open(output_file, "r") as f:
125+
output_content = f.read().strip()
126+
expected = json.dumps(["pull_file.py", "folder/keep_this.py"])
127+
assert f"all_changed_files={expected}" in output_content

0 commit comments

Comments
 (0)