@@ -25,6 +25,7 @@ cd "$(dirname "$0")/.."
2525
2626failures=0
2727WORKFLOW_FILE=.github/workflows/claude-pr-review.yml
28+ TESTS_FILE=.github/workflows/tests.yml
2829
2930# The delimiter, assembled rather than written, so this file does not trip its own scan.
3031OPEN=" \$ {$( printf ' %s' ' {' ) "
@@ -97,6 +98,30 @@ for wf in "${WORKFLOWS[@]}"; do
9798 fi
9899done
99100
101+ # cancel-in-progress cancels whatever else is in the group, so the group needs a key that is
102+ # never empty on any event the workflow accepts. It is keyed on the pull request number, and
103+ # tests.yml calls this workflow on `push: branches: [main]` as well, where there is no pull
104+ # request and that key expands to nothing -- collapsing every main-push run into one constant
105+ # group. Two merges landing close together would then cancel each other, and because the
106+ # cancellation lands on a *called* workflow it takes the caller's whole Tests run with it, so a
107+ # commit on main silently loses its test signal.
108+ #
109+ # Checked as a property of the expression rather than by evaluating it: the PR number must be
110+ # followed by a `||` fallback, so the group stays unique when there is no pull request.
111+ group_line=$( grep -n ' ^ group:' " $WORKFLOW_FILE " | head -1)
112+ if [ -z " $group_line " ]; then
113+ echo " FAIL $WORKFLOW_FILE has no workflow-level concurrency group; this check proves nothing"
114+ failures=$(( failures + 1 ))
115+ elif ! printf ' %s\n' " $group_line " | grep -q ' pull_request\.number[[:space:]]*||' ; then
116+ echo " FAIL the concurrency group keys on the pull request number with no fallback, so on a"
117+ echo " push to main it collapses to a constant and concurrent merges cancel each other's"
118+ echo " Tests run:"
119+ printf ' %s\n' " $group_line " | sed ' s/^/ /'
120+ failures=$(( failures + 1 ))
121+ else
122+ echo " ok the concurrency group stays unique when there is no pull request"
123+ fi
124+
100125# Every read the context step makes needs a permission declared on the job, because the job
101126# declares `permissions:` explicitly and anything unlisted is `none`. That failure is silent
102127# by design -- each block degrades to its "could not read" sentence -- so a missing line here
@@ -142,6 +167,36 @@ check_permission "statusCheckRollup" statuses "the StatusContext half of the CI
142167check_permission " /compare/" contents " the since-last-review comparison"
143168check_permission " /pulls/" pull-requests " the PR reads"
144169
170+ # The table above forces a new API call in the context step to declare its permission on the
171+ # review job. That does nothing for the smoke job in tests.yml, which calls the review workflow
172+ # and has to grant the same set by hand: a caller cannot give a reusable workflow more than it
173+ # holds, so a permission added on one side and not the other fails the smoke job with Actions'
174+ # "is requesting 'x: read', but is only allowed 'x: none'" -- loud, but on a file that looks
175+ # unrelated to the change that caused it. Asserting the two match keeps the claim in tests.yml's
176+ # comment true by construction instead of by review.
177+ #
178+ # Name and value both, so pull-requests: write degrading to read is caught too.
179+ perm_pairs () {
180+ awk ' /^ permissions:$/ { p = 1; next }
181+ p && /^ [a-z-]+:[[:space:]]/ { print $1, $2 }
182+ p && /^ [a-z]/ { exit }' " $1 " | sort
183+ }
184+ review_perms=$( perm_pairs " $WORKFLOW_FILE " )
185+ smoke_perms=$( perm_pairs " $TESTS_FILE " )
186+ if [ -z " $review_perms " ] || [ -z " $smoke_perms " ]; then
187+ echo " FAIL a permissions block came back empty (review: $( printf ' %s' " $review_perms " | wc -l) ," \
188+ " smoke: $( printf ' %s' " $smoke_perms " | wc -l) ); the parity check proves nothing"
189+ failures=$(( failures + 1 ))
190+ elif [ " $review_perms " != " $smoke_perms " ]; then
191+ echo " FAIL the smoke job in $TESTS_FILE does not grant what the review job declares."
192+ echo " A caller cannot grant a reusable workflow more than it holds, so the smoke job"
193+ echo " fails until both sides agree. Difference (< review job, > smoke job):"
194+ diff <( printf ' %s\n' " $review_perms " ) <( printf ' %s\n' " $smoke_perms " ) | sed ' s/^/ /'
195+ failures=$(( failures + 1 ))
196+ else
197+ echo " ok the smoke job grants exactly what the review job declares"
198+ fi
199+
145200# The scan above is a backstop for one class. actionlint checks the schema, the expression
146201# grammar, and the shell; run it when it is on PATH. shellcheck findings are excluded because
147202# the run blocks here intentionally use unquoted word splitting for job ids.
0 commit comments