Skip to content

Commit 4113b8c

Browse files
TMHSDigitalclaude
andcommitted
ci: gate releases on CI evidence, stand down on stale SHA, SHA-pin actions, group dependabot
- #290: new gate job requires Validate success on the pushed SHA and, for PR merges, every PR check green (Blender Smoke is PR-only); release job needs it. Job-level permissions (workflow default is read); explicit git add paths instead of -A - #226: release exits cleanly when origin/main moved past the checked-out SHA or the bump push is rejected; the queued newer run publishes the whole range - #306: every workflow action pinned to a full commit SHA with a # vX.Y comment; Validate fails on any unpinned uses: - #308: dependabot weekly, grouped, chore(deps) prefix Closes #290, closes #226, closes #306, closes #308 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
1 parent ae0d859 commit 4113b8c

9 files changed

Lines changed: 156 additions & 27 deletions

File tree

‎.github/dependabot.yml‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,22 @@ updates:
33
- package-ecosystem: "github-actions"
44
directory: "/"
55
schedule:
6-
interval: "daily"
6+
interval: "weekly"
77
target-branch: "main"
8+
commit-message:
9+
prefix: "chore(deps)"
10+
groups:
11+
github-actions:
12+
patterns:
13+
- "*"
814
- package-ecosystem: "pip"
915
directory: "/scripts/site"
1016
schedule:
1117
interval: "weekly"
1218
target-branch: "main"
19+
commit-message:
20+
prefix: "chore(deps)"
21+
groups:
22+
site-build:
23+
patterns:
24+
- "*"

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

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
#!/usr/bin/env bash
2+
# Release gate: exit non-zero unless CI evidence for $SHA is green.
3+
#
4+
# 1. Validate (push event) finished and succeeded on exactly this SHA.
5+
# 2. If the SHA was merged from a PR, every check on that PR passed or was
6+
# skipped. Blender Smoke runs on PRs only, so this is its evidence.
7+
#
8+
# Env: GH_TOKEN, SHA, GITHUB_REPOSITORY. Optional: GATE_EVENT (default push),
9+
# GATE_TIMEOUT seconds (default 1500), GATE_POLL seconds (default 20).
10+
set -euo pipefail
11+
12+
repo="${GITHUB_REPOSITORY:?}"
13+
sha="${SHA:?}"
14+
event="${GATE_EVENT:-push}"
15+
deadline=$((SECONDS + ${GATE_TIMEOUT:-1500}))
16+
17+
while :; do
18+
read -r status conclusion < <(
19+
gh run list --repo "$repo" --workflow validate.yml --commit "$sha" \
20+
--event "$event" --limit 1 --json status,conclusion \
21+
--jq '.[0] // {} | "\(.status // "none") \(.conclusion // "none")"'
22+
)
23+
[ "$status" = "completed" ] && break
24+
if [ "$SECONDS" -ge "$deadline" ]; then
25+
echo "::error::Validate did not finish for $sha (last status: $status)"
26+
exit 1
27+
fi
28+
echo "Validate status for $sha: $status; waiting"
29+
sleep "${GATE_POLL:-20}"
30+
done
31+
32+
if [ "$conclusion" != "success" ]; then
33+
echo "::error::Validate concluded '$conclusion' for $sha; not releasing"
34+
exit 1
35+
fi
36+
echo "Validate: success"
37+
38+
pr=$(gh api "repos/$repo/commits/$sha/pulls" --jq '.[0].number // empty')
39+
if [ -z "$pr" ]; then
40+
echo "No PR is associated with $sha (direct push); Validate alone gates this release"
41+
exit 0
42+
fi
43+
44+
# gh exits non-zero while checks are pending or failing even with --json, so
45+
# judge the JSON itself (gh's built-in --jq, no jq binary needed) and treat
46+
# unreadable output as a failed gate.
47+
count=$(gh pr checks "$pr" --repo "$repo" --json name --jq 'length' 2>/dev/null) || true
48+
case "$count" in
49+
''|*[!0-9]*|0)
50+
echo "::error::could not read checks for PR #$pr; not releasing"
51+
exit 1
52+
;;
53+
esac
54+
bad=$(gh pr checks "$pr" --repo "$repo" --json name,bucket --jq '[.[] | select(.bucket != "pass" and .bucket != "skipping") | "\(.name)=\(.bucket)"] | join(", ")' 2>/dev/null) || true
55+
if [ -n "$bad" ]; then
56+
echo "::error::PR #$pr has checks that did not pass: $bad; not releasing"
57+
exit 1
58+
fi
59+
echo "PR #$pr: $count checks passed or were skipped"

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ jobs:
8585
matrix:
8686
series: ${{ fromJSON(needs.resolve-matrix.outputs.series) }}
8787
steps:
88-
- uses: actions/checkout@v7
88+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
8989

9090
- name: Harness protocol unit tests
9191
run: |

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ jobs:
1414
permissions:
1515
contents: read
1616
steps:
17-
- uses: actions/checkout@v7
18-
- uses: TMHSDigital/Developer-Tools-Directory/.github/actions/drift-check@v1.15
17+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
18+
- uses: TMHSDigital/Developer-Tools-Directory/.github/actions/drift-check@9be79799df00ed42a0c4cff39e74a383a493296c # v1.15
1919
with:
2020
mode: self
2121
format: gh-summary

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
name: Auto-label by path
1414
runs-on: ubuntu-latest
1515
steps:
16-
- uses: actions/checkout@v7
16+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
1717

1818
- name: Get changed files
1919
id: changed

‎.github/workflows/pages.yml‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,13 +67,13 @@ jobs:
6767
url: ${{ steps.deployment.outputs.page_url }}
6868
runs-on: ubuntu-latest
6969
steps:
70-
- uses: actions/checkout@v7
70+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
7171
with:
7272
# Full history: the landing page's "Recently added" dates come from
7373
# the commit that first added each example (a shallow clone hides it).
7474
fetch-depth: 0
7575

76-
- uses: actions/setup-python@v7
76+
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
7777
with:
7878
python-version: "3.12"
7979
cache: pip
@@ -92,7 +92,7 @@ jobs:
9292
- name: Check every internal link, anchor and image alt
9393
run: python tests/check_site_links.py
9494

95-
- uses: actions/configure-pages@v6
95+
- uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6
9696

9797
- name: Stage the public site (leave out internal docs and unlinked sheets)
9898
# Everything stays in the repo; only the Pages artifact shrinks. No
@@ -103,9 +103,9 @@ jobs:
103103
rm -rf _site/gallery/contact-sheets _site/gallery/asset-sheets _site/gallery/DESIGN_NOTES.md
104104
rm -f _site/*.md
105105
106-
- uses: actions/upload-pages-artifact@v5
106+
- uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5
107107
with:
108108
path: _site
109109

110-
- uses: actions/deploy-pages@v5
110+
- uses: actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346 # v5
111111
id: deployment

‎.github/workflows/release.yml‎

Lines changed: 57 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,45 @@ on:
66
workflow_dispatch: {}
77

88
permissions:
9-
contents: write
10-
actions: write
9+
contents: read
1110

1211
concurrency:
1312
group: release
1413
cancel-in-progress: false
1514

1615
jobs:
16+
gate:
17+
name: Gate on CI evidence
18+
# A push to main must not publish unless Validate passed on this exact SHA and,
19+
# when it was merged from a PR, every check on that PR passed (Blender Smoke
20+
# runs on PRs only). Direct pushes have no PR; Validate alone gates them.
21+
runs-on: ubuntu-latest
22+
if: "!contains(github.event.head_commit.message, '[skip ci]')"
23+
permissions:
24+
contents: read
25+
actions: read
26+
checks: read
27+
pull-requests: read
28+
steps:
29+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
30+
with:
31+
sparse-checkout: .github/scripts
32+
- name: Require green Validate and PR checks
33+
env:
34+
GH_TOKEN: ${{ github.token }}
35+
SHA: ${{ github.sha }}
36+
run: bash .github/scripts/release-gate.sh
37+
1738
version-and-release:
1839
name: Bump version, tag, and release
40+
needs: gate
1941
runs-on: ubuntu-latest
2042
if: "!contains(github.event.head_commit.message, '[skip ci]')"
43+
permissions:
44+
contents: write
45+
actions: write
2146
steps:
22-
- uses: actions/checkout@v7
47+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
2348
with:
2449
fetch-depth: 0
2550
token: ${{ secrets.GITHUB_TOKEN }}
@@ -117,7 +142,7 @@ jobs:
117142
118143
- name: Sync release docs
119144
if: steps.check.outputs.skip == 'false' && steps.bump.outputs.release == 'true'
120-
uses: TMHSDigital/Developer-Tools-Directory/.github/actions/release-doc-sync@v1
145+
uses: TMHSDigital/Developer-Tools-Directory/.github/actions/release-doc-sync@7886cbe6ef93d57cc73c020b98268fa0dc0bdc98 # v1
121146
with:
122147
plugin-version: ${{ steps.new.outputs.version }}
123148
previous-version: ${{ steps.current.outputs.version }}
@@ -156,20 +181,43 @@ jobs:
156181
PYEOF
157182
158183
- name: Commit version bump
184+
id: commit
159185
if: steps.check.outputs.skip == 'false' && steps.bump.outputs.release == 'true'
160186
run: |
161187
git config user.name "github-actions[bot]"
162188
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
163-
git add -A
189+
190+
# Another PR may have merged after this run checked out (#226). The
191+
# queued run for the newer SHA scans the whole range since the last
192+
# tag and releases everything, so stand down cleanly instead of
193+
# failing with a non-fast-forward push.
194+
git fetch -q origin main
195+
if [ "$(git rev-parse origin/main)" != "$(git rev-parse HEAD)" ]; then
196+
echo "::notice::origin/main moved past $GITHUB_SHA; the newer release run will publish this range"
197+
echo "stale=true" >> "$GITHUB_OUTPUT"
198+
exit 0
199+
fi
200+
201+
# Explicit paths only: the files this workflow owns, never `git add -A`.
202+
git add -- VERSION CHANGELOG.md CLAUDE.md ROADMAP.md \
203+
.cursor-plugin/plugin.json .claude-plugin/plugin.json .claude-plugin/marketplace.json
204+
if [ -n "$(git status --porcelain)" ]; then
205+
echo "::warning::files changed that the release commit does not own:"
206+
git status --porcelain
207+
fi
164208
if git diff --cached --quiet; then
165209
echo "No changes to commit"
166210
else
167211
git commit -s -m "chore: bump version to ${{ steps.new.outputs.version }} [skip ci]"
168-
git push origin main
212+
if ! git push origin main; then
213+
echo "::notice::push rejected (main moved); the newer release run will publish this range"
214+
echo "stale=true" >> "$GITHUB_OUTPUT"
215+
exit 0
216+
fi
169217
fi
170218
171219
- name: Create and push tag
172-
if: steps.check.outputs.skip == 'false' && steps.bump.outputs.release == 'true'
220+
if: steps.check.outputs.skip == 'false' && steps.bump.outputs.release == 'true' && steps.commit.outputs.stale != 'true'
173221
run: |
174222
new_version="${{ steps.new.outputs.version }}"
175223
IFS='.' read -r major minor _patch <<< "$new_version"
@@ -183,7 +231,7 @@ jobs:
183231
git push origin "v$major.$minor" --force
184232
185233
- name: Create GitHub Release
186-
if: steps.check.outputs.skip == 'false' && steps.bump.outputs.release == 'true'
234+
if: steps.check.outputs.skip == 'false' && steps.bump.outputs.release == 'true' && steps.commit.outputs.stale != 'true'
187235
env:
188236
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
189237
run: |
@@ -192,7 +240,7 @@ jobs:
192240
--generate-notes
193241
194242
- name: Dispatch Pages
195-
if: steps.check.outputs.skip == 'false' && steps.bump.outputs.release == 'true'
243+
if: steps.check.outputs.skip == 'false' && steps.bump.outputs.release == 'true' && steps.commit.outputs.stale != 'true'
196244
env:
197245
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
198246
run: gh workflow run pages.yml --ref main

‎.github/workflows/stale.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
stale:
1414
runs-on: ubuntu-latest
1515
steps:
16-
- uses: actions/stale@v10
16+
- uses: actions/stale@1e223db275d687790206a7acac4d1a11bd6fe629 # v10
1717
with:
1818
stale-issue-message: "This issue has been automatically marked as stale due to inactivity. It will be closed in 7 days if no further activity occurs."
1919
stale-pr-message: "This PR has been automatically marked as stale due to inactivity. It will be closed in 7 days if no further activity occurs."

‎.github/workflows/validate.yml‎

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
name: Validate structure and frontmatter
1515
runs-on: ubuntu-latest
1616
steps:
17-
- uses: actions/checkout@v7
17+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
1818

1919
- name: Check referenced paths exist
2020
run: |
@@ -192,9 +192,9 @@ jobs:
192192
# so a template error or a dead link otherwise surfaces in production.
193193
runs-on: ubuntu-latest
194194
steps:
195-
- uses: actions/checkout@v7
195+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
196196

197-
- uses: actions/setup-python@v7
197+
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
198198
with:
199199
python-version: "3.12"
200200
cache: pip
@@ -212,7 +212,7 @@ jobs:
212212
name: Validate plugin manifest
213213
runs-on: ubuntu-latest
214214
steps:
215-
- uses: actions/checkout@v7
215+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
216216

217217
- name: Check plugin.json matches filesystem and VERSION
218218
run: |
@@ -281,7 +281,7 @@ jobs:
281281
name: Validate Claude Code packaging
282282
runs-on: ubuntu-latest
283283
steps:
284-
- uses: actions/checkout@v7
284+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
285285

286286
- name: Check .claude-plugin manifests match VERSION and the skills on disk
287287
run: |
@@ -323,6 +323,16 @@ jobs:
323323
print('Claude Code packaging verified')
324324
PYEOF
325325
326+
- name: Check every workflow action is pinned to a full commit SHA
327+
run: |
328+
bad=$(grep -rnE '^\s*(-\s+)?uses:\s+[^./ ]' .github/workflows \
329+
| grep -vE 'uses:\s+\S+@[0-9a-f]{40}(\s|$)' || true)
330+
if [ -n "$bad" ]; then
331+
echo "$bad"
332+
echo "::error::pin these actions to a full commit SHA with a trailing '# vX.Y' comment (#306)"
333+
exit 1
334+
fi
335+
326336
- name: Check CLAUDE.md carries no personal plugin routing block
327337
run: |
328338
if grep -nE 'context-mode|MANDATORY routing rules|ctx_(execute|batch_execute|search|fetch_and_index)' CLAUDE.md; then
@@ -337,7 +347,7 @@ jobs:
337347
name: Validate content counts
338348
runs-on: ubuntu-latest
339349
steps:
340-
- uses: actions/checkout@v7
350+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
341351

342352
- name: Check content counts match README
343353
env:
@@ -440,7 +450,7 @@ jobs:
440450
name: Validate smoke harness protocol
441451
runs-on: ubuntu-latest
442452
steps:
443-
- uses: actions/checkout@v7
453+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
444454

445455
- name: Harness unit tests
446456
run: python3 tests/smoke/test_harness.py -v

0 commit comments

Comments
 (0)