ci: release.yml hygiene — job timeouts + release concurrency (queue) - #52
Conversation
Adds two runaway guards to release.yml without touching the gate topology.
1. timeout-minutes on all 6 directly-defined jobs (was: the 6h default).
Values derived from 5 recent successful runs, ~2x observed p95 with a
10-minute floor; build-unity-installer gets 30m as a Unity-editor job.
2. Workflow-level concurrency group `release-${{ github.ref }}` with
`queue: max` and cancel-in-progress deliberately unset, so an in-flight
release always finishes and a queued release is no longer silently
cancelled by the next push.
The 9 test-unity-* jobs call a reusable workflow, where `timeout-minutes`
is not a permitted key — adding it is a hard "Invalid workflow file" error.
Bounding those requires a timeout inside test_unity_plugin.yml, which is
out of scope here.
No rename, no permissions block, no version files, gate topology unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017bgHTA223Fnjci6QbyC1bv
Code review — depth: medium (gating: advisory)Verdict: APPROVE — no blocking findings. This is a clean, well-documented, pure-additive hygiene diff. I independently reproduced every load-bearing claim in the PR body rather than taking it on faith (details below). DoD conformance
Correctness
Risk
Adjacent
Findings
What I verified vs. took on faithVerified directly (not trusted from the PR body): live GitHub docs text for |
* ci: bump_version direct mode (push+dispatch) + release.yml hygiene bump_version.yml gains a `mode` input (choice: pr | direct, default pr). - mode=pr keeps today's behaviour exactly: branch + PR, all six steps untouched. The only edit to that job is the `if:` guard that selects it. - mode=direct commits the same bump (same commands/bump-version.ps1, same argument) straight to the default branch with the job's GITHUB_TOKEN, then dispatches release.yml explicitly. A GITHUB_TOKEN push creates no workflow runs, so the dispatch is the only thing that starts the release, which is why the job carries `actions: write` alongside `contents: write`. The job conditions are `!= 'direct'` / `== 'direct'` so they are exhaustive: a dispatch that omits `mode` falls back to the legacy pr path rather than matching neither job and silently doing nothing. release.yml receives D12 hygiene only, mirroring the merged Unity pilot (IvanMurzak/Unity-AI-Animation#52): - workflow-level concurrency `release-${{ github.ref }}` with `queue: max` and cancel-in-progress deliberately unset. - timeout-minutes 10 on check-version-tag (observed 4-6s) and 15 on publish (observed 26s on the real NUGET_USER path). - the `test` job calls a reusable workflow, where timeout-minutes is a hard validation error, so it is documented rather than bounded. File not renamed; publish OIDC/NuGet gating and gate topology untouched. No version files touched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017bgHTA223Fnjci6QbyC1bv * ci: pass github.ref_name through env in the direct job's push/dispatch Review follow-up on #5. The Push and "Dispatch release.yml" steps inlined `${{ github.ref_name }}` directly into their `run:` bodies, which contradicts the data-not-script-text principle the rest of the direct job follows (and which the PR body claims). Both now take TARGET_REF via `env:` and reference the shell variable instead. No behavioural change: TARGET_REF resolves to the same ref the inline expansion produced. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017bgHTA223Fnjci6QbyC1bv --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Adds two runaway guards to
release.yml. Hygiene attributes only — no rename, no restructuring, no version files, gate topology untouched.1. Job timeouts
Every job previously inherited the 6-hour GitHub default. Derived from the 5 most recent successful full release runs (
30354826261,30201966471,29762726136,29757793994,29704719893) — a hung job could previously burn 6h of Actions time before anyone noticed.Rule applied: ~2× observed p95, floor of 10 minutes, generous for the Unity-editor job. With n=5, p95 ≈ observed max.
check-version-tagprepare-release-notesbuild-signed-upm-packagebuild-unity-installerrelease-unity-plugincleanup-artifactsbuild-unity-installerruns a Unity EditMode test and a package export inside the Unity image, so its variance is dominated by image pull and licence activation rather than by our code — hence 30 min rather than the arithmetic ~15.The 9
test-unity-*jobs deliberately have NO timeout — it is not expressible hereThese call a reusable workflow (
uses: ./.github/workflows/test_unity_plugin.yml).timeout-minutesis not a permitted key on a reusable-workflow-calling job, and adding it is a hard validation error (The workflow is not valid ... Unexpected value 'timeout-minutes') that fails the entire workflow file — every job, including the PR run. Verified three independent ways:name,uses,with,with.<input_id>,secrets,secrets.<secret_id>,secrets.inherit,strategy,needs,if,concurrency,permissions.timeout-minutesis absent.workflow-v1.0.json: theworkflow-jobmapping declares notimeout-minutes, while the ordinaryjob-factorydoes.These are the longest jobs in the pipeline and remain unbounded — observed up to 17m45s (
2023.2.22f1 editmode on windows-mono), with editmode legs routinely 11–17 min. Bounding them requirestimeout-minuteson the jobs insidetest_unity_plugin.yml, which is out of scope for this PR. Suggested follow-up, from the same 5 runs: editmode legs max 17m45s → ~45 min; playmode max 9m54s → ~30 min; standalone max 7m12s → ~30 min;version-consistencymax 10s → 10 min.2. Release concurrency
cancel-in-progressis deliberately unset — an in-flight release must finish, since it may already have created the tag and GitHub Release.queue: maxis the load-bearing part. The documented default (queue: single) keeps at most one pending run and silently cancels and replaces it when another is queued — so back-to-back version bumps could see an earlier bump's release cancelled while never having run.Queue syntax — doc citation
Verified against official sources before authoring:
pendingjob or workflow run to wait in the same concurrency group, use the optionalqueueproperty." Values: "single(default): At most one job or workflow run can bependingin the concurrency group. When a new job or workflow run is queued, any existingpendingjob or workflow run in the same group is canceled and replaced.max: Up to 100 jobs or workflow runs can bependingin the concurrency group." The page's own example is workflow-level and usesqueue: maxverbatim.Also documented and respected here: "The combination of
queue: maxandcancel-in-progress: trueis not allowed and will result in a workflow validation error." Leavingcancel-in-progressunset is required, not stylistic. Queue order is FIFO; overflow past 100 pending is cancelled.Validation performed
actionlint v1.7.12on the modified file → only the known-stalequeueline; clean baseline on the unmodified file beforehand.yaml.safe_loadparse → 15 jobs; the 6stepsjobs carry timeouts, the 9usesjobs carry none.Constraints honoured
permissions:block added anywhere — the workflow had none and relies on default token permissions, sorelease-unity-pluginkeeps itscontents: writecapability untouched. Per the task constraint, when in doubt permissions were not touched.check-version-tag→ tests → atomic all-tests-gated publish. Noneeds:array and noif:condition was modified.The full 27-job PR matrix firing on this PR is expected and one-off.