Skip to content

Commit 6dd2233

Browse files
TMHSDigitalclaude
andcommitted
fix(ci): bound every job, skip smoke on manifest-only pushes, deploy Pages once
- Every job now sets timeout-minutes. Before, only the smoke job did, so a hung step could hold a runner for GitHub's 360-minute default. The release gate gets 125 minutes, above its own 25 + 90 minute waits. - Blender Smoke's paths-ignore also skips .claude-plugin/, .cursor-plugin/, live-session/ and .github/ISSUE_TEMPLATE/, which run no Blender code. The MCP registration push waited 24 minutes on a smoke run it could not affect. release-gate.sh mirrors the list, and a new test fails if the two drift apart. - Pages: a push that changes plugin content also cuts a patch release (#350), which dispatches Pages after the bump, so the push-time deploy was the duplicate. The gate now skips it, reading the shipped paths from build_plugin_dist.py --list-paths. - Validate jobs run on actions/setup-python 3.12, not the runner image's moving python3. validate-harness installs requirements-dev.txt up front. Falsified locally: with the old gate pattern, the mirror test and the manifest-only test both fail; the Pages decision was simulated for skills, manifest, docs, examples and feat pushes. Closes #495 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com>
1 parent dfcbd52 commit 6dd2233

9 files changed

Lines changed: 88 additions & 6 deletions

File tree

‎.github/scripts/release-gate.sh‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,9 @@ wait_green() {
4545
}
4646

4747
# smoke_applies: 0 when some changed file falls outside blender-smoke.yml's
48-
# paths-ignore ("**.md", "docs/**", "assets/**"). Unreadable file lists count
48+
# paths-ignore ("**.md", "docs/**", "assets/**", the two plugin manifest dirs,
49+
# "live-session/**", ".github/ISSUE_TEMPLATE/**"; tests/test_release_gate.py
50+
# checks the two lists agree). Unreadable file lists count
4951
# as "applies" so the gate fails toward waiting, not toward skipping. So do
5052
# lists of 300 or more: the compare and commit APIs return at most 300 files,
5153
# in path order, so a big docs/gallery regeneration can hide the code change
@@ -61,7 +63,7 @@ smoke_applies() {
6163
[ "$(printf '%s\n' "$files" | wc -l)" -lt 300 ] || return 0
6264
while IFS= read -r f; do
6365
case "$f" in
64-
*.md|docs/*|assets/*) ;;
66+
*.md|docs/*|assets/*|.claude-plugin/*|.cursor-plugin/*|live-session/*|.github/ISSUE_TEMPLATE/*) ;;
6567
*) return 0 ;;
6668
esac
6769
done <<< "$files"

‎.github/workflows/blender-smoke.yml‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ on:
4545
- "**.md"
4646
- "docs/**"
4747
- "assets/**"
48+
- ".claude-plugin/**"
49+
- ".cursor-plugin/**"
50+
- "live-session/**"
51+
- ".github/ISSUE_TEMPLATE/**"
4852
pull_request:
4953
branches: [main]
5054
types: [opened, synchronize, reopened, labeled]
@@ -54,6 +58,10 @@ on:
5458
- "**.md"
5559
- "docs/**"
5660
- "assets/**"
61+
- ".claude-plugin/**"
62+
- ".cursor-plugin/**"
63+
- "live-session/**"
64+
- ".github/ISSUE_TEMPLATE/**"
5765

5866
permissions:
5967
contents: read
@@ -70,6 +78,7 @@ jobs:
7078
resolve-matrix:
7179
name: Resolve smoke matrix
7280
runs-on: ubuntu-latest
81+
timeout-minutes: 5
7382
if: github.event.action != 'labeled' || github.event.label.name == 'needs-5.1'
7483
outputs:
7584
series: ${{ steps.set.outputs.series }}

‎.github/workflows/drift-check.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ jobs:
1111
drift-check:
1212
name: Ecosystem drift check
1313
runs-on: ubuntu-latest
14+
timeout-minutes: 10
1415
permissions:
1516
contents: read
1617
steps:

‎.github/workflows/label-sync.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ jobs:
1717
# cosmetic. No checkout: every step talks to the API only.
1818
if: github.event.pull_request.head.repo.full_name == github.repository
1919
runs-on: ubuntu-latest
20+
timeout-minutes: 5
2021
env:
2122
GH_REPO: ${{ github.repository }}
2223
steps:

‎.github/workflows/pages.yml‎

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,15 @@ jobs:
4242
# release fails, redeploy by hand: Actions > Deploy GitHub Pages > Run workflow.
4343
gate:
4444
runs-on: ubuntu-latest
45+
timeout-minutes: 5
4546
outputs:
4647
deploy: ${{ steps.decide.outputs.deploy }}
4748
steps:
4849
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
4950
with:
50-
sparse-checkout: .github/scripts
51+
sparse-checkout: |
52+
.github/scripts
53+
scripts/build_plugin_dist.py
5154
- id: decide
5255
env:
5356
EVENT: ${{ github.event_name }}
@@ -59,6 +62,22 @@ jobs:
5962
| bash .github/scripts/bump-kind.sh)" != "none" ]; then
6063
deploy=false
6164
echo "Release-worthy push: release.yml dispatches Pages after the version bump."
65+
elif [ "$EVENT" = "push" ]; then
66+
# A push that touches plugin content also releases (a patch, #350),
67+
# and that release dispatches Pages too, so deploying here would be
68+
# the duplicate (#495). Shipped paths come from the dist build.
69+
shipped=$(python3 scripts/build_plugin_dist.py --list-paths)
70+
while IFS= read -r f; do
71+
f="${f%$'\r'}"
72+
while IFS= read -r p; do
73+
p="${p%$'\r'}"
74+
case "$f" in "$p"|"$p"/*)
75+
deploy=false
76+
echo "Plugin content changed ($f): release.yml dispatches Pages after the patch release." ;;
77+
esac
78+
done <<< "$shipped"
79+
[ "$deploy" = false ] && break
80+
done < <(echo "$COMMITS" | jq -r '.[] | (.added + .modified + .removed)[]')
6281
fi
6382
echo "deploy=$deploy" >> "$GITHUB_OUTPUT"
6483
echo "deploy=$deploy"
@@ -70,6 +89,7 @@ jobs:
7089
name: github-pages
7190
url: ${{ steps.deployment.outputs.page_url }}
7291
runs-on: ubuntu-latest
92+
timeout-minutes: 20
7393
steps:
7494
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
7595
with:

‎.github/workflows/release.yml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ jobs:
2020
# also waits for its own Blender Smoke push run, unless it changed only
2121
# smoke-ignored paths (see release-gate.sh).
2222
runs-on: ubuntu-latest
23+
timeout-minutes: 125
2324
if: "!contains(github.event.head_commit.message, '[skip ci]')"
2425
permissions:
2526
contents: read
@@ -41,6 +42,7 @@ jobs:
4142
name: Bump version, tag, and release
4243
needs: gate
4344
runs-on: ubuntu-latest
45+
timeout-minutes: 30
4446
if: "!contains(github.event.head_commit.message, '[skip ci]')"
4547
permissions:
4648
contents: write

‎.github/workflows/stale.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ permissions:
1212
jobs:
1313
stale:
1414
runs-on: ubuntu-latest
15+
timeout-minutes: 10
1516
steps:
1617
- uses: actions/stale@4391f3da665fdf50b6810c1a66712fb9ba21aa93 # v11.0.0
1718
with:

‎.github/workflows/validate.yml‎

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,12 @@ jobs:
1313
validate:
1414
name: Validate structure and frontmatter
1515
runs-on: ubuntu-latest
16+
timeout-minutes: 15
1617
steps:
1718
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
19+
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
20+
with:
21+
python-version: "3.12" # pinned: the runner image's python3 moves (#495)
1822

1923
- name: Check referenced paths exist
2024
run: |
@@ -137,6 +141,7 @@ jobs:
137141
# The landing page is built only at deploy (docs/index.html is untracked),
138142
# so a template error or a dead link otherwise surfaces in production.
139143
runs-on: ubuntu-latest
144+
timeout-minutes: 15
140145
steps:
141146
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
142147

@@ -160,8 +165,12 @@ jobs:
160165
validate-manifest:
161166
name: Validate plugin manifest
162167
runs-on: ubuntu-latest
168+
timeout-minutes: 15
163169
steps:
164170
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
171+
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
172+
with:
173+
python-version: "3.12" # pinned: the runner image's python3 moves (#495)
165174

166175
- name: Check plugin.json matches filesystem and VERSION
167176
run: |
@@ -258,8 +267,12 @@ jobs:
258267
validate-claude-packaging:
259268
name: Validate Claude Code packaging
260269
runs-on: ubuntu-latest
270+
timeout-minutes: 15
261271
steps:
262272
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
273+
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
274+
with:
275+
python-version: "3.12" # pinned: the runner image's python3 moves (#495)
263276

264277
- name: Check .claude-plugin manifests match VERSION and the skills on disk
265278
run: |
@@ -363,8 +376,12 @@ jobs:
363376
validate-counts:
364377
name: Validate content counts
365378
runs-on: ubuntu-latest
379+
timeout-minutes: 15
366380
steps:
367381
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
382+
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
383+
with:
384+
python-version: "3.12" # pinned: the runner image's python3 moves (#495)
368385

369386
- name: Check content counts match the repo
370387
env:
@@ -436,8 +453,15 @@ jobs:
436453
validate-harness:
437454
name: Validate smoke harness protocol
438455
runs-on: ubuntu-latest
456+
timeout-minutes: 15
439457
steps:
440458
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
459+
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
460+
with:
461+
python-version: "3.12" # pinned: the runner image's python3 moves (#495)
462+
463+
- name: Install pinned dev requirements (the release-gate mirror test reads YAML)
464+
run: pip install -r requirements-dev.txt
441465

442466
- name: Harness unit tests
443467
run: python3 tests/smoke/test_harness.py -v
@@ -464,6 +488,4 @@ jobs:
464488
run: python3 tests/test_falsifier_shard.py -v
465489

466490
- name: Hero-drift tool exit-code tests
467-
run: |
468-
pip install -r requirements-dev.txt
469-
python3 tests/test_measure_hero_drift.py -v
491+
run: python3 tests/test_measure_hero_drift.py -v

‎tests/test_release_gate.py‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,30 @@ def test_docs_only_direct_push_skips_smoke(self):
8686
self.assertEqual(code, 0, out)
8787
self.assertIn("only smoke-ignored paths", out)
8888

89+
def test_manifest_and_mcp_only_direct_push_skips_smoke(self):
90+
# #495: a manifest or live-session change runs no Blender code.
91+
code, out = self.gate(
92+
FAKE_PR="", FAKE_SMOKE="none none", SMOKE_TIMEOUT="0",
93+
FAKE_FILES=r".claude-plugin/plugin.json\nlive-session/mcp.json\n.github/ISSUE_TEMPLATE/bug.yml",
94+
)
95+
self.assertEqual(code, 0, out)
96+
self.assertIn("only smoke-ignored paths", out)
97+
98+
def test_gate_mirrors_smoke_paths_ignore(self):
99+
# release-gate.sh decides "no smoke run to wait for" from its own copy
100+
# of blender-smoke.yml's paths-ignore; the two must not drift.
101+
import re
102+
import yaml
103+
wf = yaml.safe_load((REPO / ".github" / "workflows" / "blender-smoke.yml")
104+
.read_text(encoding="utf-8"))
105+
on = wf.get("on", wf.get(True))
106+
for event in ("push", "pull_request"):
107+
globs = on[event]["paths-ignore"]
108+
want = {g.replace("**.", "*.").replace("/**", "/*") for g in globs}
109+
case = re.search(r"^\s*([^\n]*\*\.md\|[^\n]*)\) ;;", GATE.read_text(encoding="utf-8"), re.M)
110+
self.assertIsNotNone(case, "release-gate.sh smoke-ignored case line not found")
111+
self.assertEqual(set(case.group(1).split("|")), want, event)
112+
89113
def test_truncated_file_list_waits_for_smoke(self):
90114
# 300 docs paths is the API's cap: a code change may sort after them.
91115
many = r"\n".join(f"docs/gallery/p{i:03d}.html" for i in range(300))

0 commit comments

Comments
 (0)