Skip to content

ci: bind Unity editor gate provenance and retire superseded Unity runs - #326

Merged
Eli Pinkerton (wallstop) merged 7 commits into
masterfrom
codex/issue-166-editor-gate-provenance
Jul 31, 2026
Merged

ci: bind Unity editor gate provenance and retire superseded Unity runs#326
Eli Pinkerton (wallstop) merged 7 commits into
masterfrom
codex/issue-166-editor-gate-provenance

Conversation

@wallstop

@wallstop Eli Pinkerton (wallstop) commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Summary

Binds licensed Unity work to an editor proved by a manual-only gate that runs before the repository checkout, and retires a superseded Unity run before it can reach the build lock.

Editor gate provenance (the original change, plus its fixes)

  • Validate and bind the editor before the main repo checkout, so no later step can replace the validator's evidence or run against an unexpected install.
  • Reject automatic editor fallback: -CiManagedOnly -RequireHealthyExisting, and a bind step that fails unless editorPath is the canonical RUNNER_TOOL_CACHE\u6-v3\<version>\Editor\Unity.exe.
  • -DiagnosticsPath points at a RUNNER_TEMP directory, so ensure-editor.ps1 writes its summary outside the workspace, from its own finally block. The evidence therefore survives both a failed gate and the checkout that wipes the workspace, and the if: always() upload publishes the same file the bind step trusted.
  • The upload names that directory directly rather than an env var written only on the success path.
  • Applied across all five licensed jobs (unity-tests, unity-benchmarks, perf-benchmarks, release/unity-checks, release/unitypackage).

Superseded Unity runs (closes #311)

  • A second push to a pull request queued a second full nine-leg matrix without retiring the first. cancel-in-progress: false is deliberate -- hard cancelling a run holding the organization build lock is what the four-layer license-return guarantee exists to prevent -- so the superseded run kept the concurrency group through every leg while the current head waited, and Unity CI Success was absent on the head that mattered.
  • A new head-check job on ubuntu-latest compares the event head against the live pull-request head, before the lock is in reach, and publishes a superseded output that both licensed jobs gate on. A superseded run now costs one cheap job instead of nine self-hosted ones.
  • Fails open: any event other than a pull request, and any lookup that returns no SHA, runs the full matrix.
  • The per-leg Require current PR head before setup guard stays; it covers the push that lands after head-check has already passed.
  • Unity CI Success reports the shape rather than going absent: it requires head-check to have succeeded and accepts skipped licensed jobs for a superseded head alongside the fork and Dependabot shapes.

Review feedback addressed

Finding Resolution
Cursor Bugbot: empty validation upload path The upload names ${{ runner.temp }}/dx-unity-editor-validation directly; if-no-files-found: warn keeps the previous behavior
Cursor Bugbot: gate failures drop upload evidence ensure-editor.ps1 writes its summary to RUNNER_TEMP itself, from a finally block, so a failed gate and a provenance mismatch both upload
Prettier and yamllint red The single-line ensure-editor.ps1 calls are wrapped again; -InstallRoot returns to one spelling, so the policy validator needs no second accepted form
Script tests red on validate:js-loc-budget JS is net zero: five validationStep assertions folded into three, paying for the bind-step and upload-step contracts

copilot-pull-request-reviewer reports a reviewer quota limit, not a finding.

Where the contracts live

scripts/validate-unity-pr-policy.py owns the superseded gate, because its docstring is "trusted-PR Unity admission, staleness guards, and skip policy" and it already byte-pins the aggregate step's script and executes it against a truth table. Its truth table gains the superseded and failed-decision rows; a second table runs the head-freshness script itself against a stub gh for push, current head, superseded head, and failed lookup.

scripts/__tests__/ci-aggregate-workflow.test.js owns the per-job lifecycle order, which now includes the evidence upload.

Evidence

CI on 1d4563e9: 34 success, 4 skipped, 0 blocking failures. All nine Unity legs, Unity CI Success, and CI Success pass; the pull request reports mergeable_state: clean. The one red check, copilot-pull-request-reviewer, reports a reviewer quota limit rather than a finding, and is not a required status check. Cursor Bugbot on the final commit: "no new issues".

Unity head freshness runs in 5 seconds and reported superseded=false, so the new gate costs a superseded run one cheap job while the full nine-leg matrix still takes about 15 minutes end to end.

The evidence relocation is confirmed on a real runner: the upload step reports With the provided path, there will be 2 files uploaded from E:\actions-runner\_work\_temp/dx-unity-editor-validation -- the summary JSON plus the sibling .txt that Write-UnityProvisioningSummary writes next to it.

#311 observed live. The first push of this session created the exact situation the fix removes, before the fix existed. Run 30642224168 was superseded mid-flight and still scheduled five legs onto the self-hosted runner, each failing at step one on Require current PR head before setup -- five red legs and a red Unity CI Success on a stale SHA.

One leg needed a re-run, and not as a flake write-off. Unity 2021.3.45f1 playmode first failed on the shared build lock, not on tests: it acquired the lock, and 68 seconds later the release reported Lock is already free (acquired=true ... reservation=missing). The predecessor commit abaabb68 passed all nine legs with byte-identical lock code, and no other non-superseded run in the last eight has failed that step. Filed as #329 against the lock action with the full evidence, then the single job was re-run and passed.

Every guard was mutated and confirmed to fail:

Mutation Caught by
upload path back to the env var ci-aggregate-workflow.test.js
-InstallRoot back to the quoted form ci-aggregate-workflow.test.js
shell: pwsh without -NoProfile -NonInteractive ci-aggregate-workflow.test.js
export the editor path before preserving the evidence ci-aggregate-workflow.test.js
case-sensitive canonical-path compare ci-aggregate-workflow.test.js
evidence written back inside the workspace ci-aggregate-workflow.test.js
bind reading an unpreserved copy ci-aggregate-workflow.test.js
inverted superseded comparison validate-unity-pr-policy.py
failed lookup stops failing open validate-unity-pr-policy.py
superseded guard dropped from the licensed jobs validate-unity-pr-policy.py
aggregate stops requiring the decision validate-unity-pr-policy.py
decision moved onto a self-hosted runner validate-unity-pr-policy.py
head-freshness querying a different endpoint validate-unity-pr-policy.py
head-freshness dropping --jq .head.sha validate-unity-pr-policy.py

Local: npm test 406/0, npm run validate:all pass, validate-js-loc-budget 17500/17500, actionlint, yamllint --strict, prettier, markdownlint and cspell clean.

No Unity MCP run: this diff touches no C#, so the local editor signal would prove nothing about it. The real signal is the nine-leg matrix on this pull request.

Docs

docs/runbooks/required-checks.md described a matrix-config job and an re-actors/alls-green aggregate for Unity CI Success. Neither exists, and validate-unity-pr-policy.py rejects the latter outright. That section now describes the three validated skip shapes and the gate as written.

Self-review, beyond what reviewers raised

  • The five new validator checkouts had dropped the # v7 comment every other actions/checkout pin in the repository carries.
  • Three workflows still told a reader that editor validation happens alongside credential validation and lock acquisition; the gate now runs before the repository checkout.
  • A summary written by a failed gate can record no editorPath, and [IO.Path]::GetFullPath('') throws an ArgumentException rather than saying so. The binder reports (none recorded) through its own mismatch message.
  • On Windows the head-freshness truth table returned a hardcoded answer instead of running the script, so its four cases asserted the helper's own logic. The table is skipped there now, matching how the aggregate result-shape table already handles the same constraint. The stub gh also accepted any invocation; it now answers only the live-head lookup.

Issues opened while working on this

Follow-up to #322, which squash-merged before the provenance binder commit was ready.

Closes #311.

Copilot AI review requested due to automatic review settings July 31, 2026 10:41
@wallstop

Copy link
Copy Markdown
Collaborator Author

Cursor (@cursor) review

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Comment thread .github/workflows/unity-tests.yml Outdated
…ditional

The gate binder collapsed each ensure-editor.ps1 call onto one line, which
put five workflow lines past the 200-column yamllint limit and pushed
tracked JS one contract assertion over its budget. Restore the wrapped
backtick form master already used, so -InstallRoot goes back to the single
(Join-Path $env:RUNNER_TOOL_CACHE 'u6-v3') spelling and the policy
validator no longer needs a second accepted form.

The validation-evidence upload took its path from UNITY_EDITOR_VALIDATION_PATH,
which only exists after a successful bind. A failed validator checkout,
ensure-editor run, or bind left that empty, turning an if: always()
diagnostic upload into a second failure exactly where the evidence matters
most. The upload now names the run-scoped RUNNER_TEMP evidence directory
directly, so if-no-files-found: warn keeps its old behavior, and the unused
env export is gone.

The lifecycle contract now covers the upload step and asserts the bind step
in order: prove the canonical editor, preserve the evidence outside the
workspace, then export the path. Five mutations (empty upload path, quoted
install root, dropped -NoProfile, reordered preserve/export, case-sensitive
path compare) each fail the suite.
Copilot AI review requested due to automatic review settings July 31, 2026 15:17
@wallstop

Copy link
Copy Markdown
Collaborator Author

Cursor (@cursor) review

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Comment thread .github/workflows/unity-tests.yml
)

A second push to a pull request queues a second full nine-leg matrix and does
not retire the first. `cancel-in-progress: false` is deliberate -- hard
cancelling a run that holds the organization build lock is the exact scenario
the four-layer license-return guarantee exists to prevent -- so the superseded
run keeps the concurrency group until it has burned through every leg, and the
run for the commit that matters cannot start. `Unity CI Success` is then absent
on the current head, which reads as a blocked pull request on a check nobody
misconfigured.

A new `head-check` job compares the event's head SHA against the live
pull-request head on `ubuntu-latest`, before the lock is anywhere in reach, and
publishes a `superseded` output that both licensed jobs gate on. A superseded
run now costs one cheap job instead of nine self-hosted ones. It fails open: any
event other than a pull request, and any lookup that returns no SHA, runs the
full matrix. The per-leg `Require current PR head before setup` guard stays,
because it covers the push that lands after `head-check` has already passed.

`Unity CI Success` reports the new shape rather than going absent: it now
requires `head-check` itself to have succeeded and accepts skipped licensed jobs
for a superseded head alongside the fork and Dependabot shapes.

`validate-unity-pr-policy.py` owns the contract, since it already byte-pins the
aggregate script and executes it against a truth table. Its truth table gains
the superseded rows plus a failed-decision row, and a new table runs the
head-freshness script itself against a stub `gh` for push, current, superseded,
and failed-lookup. Five mutations (inverted comparison, a failed lookup that
stops failing open, a dropped guard on the licensed jobs, an aggregate that
stops requiring the decision, and moving the decision onto a self-hosted runner)
each fail the validator.

The required-checks runbook described a `matrix-config` job and an
`re-actors/alls-green` aggregate; neither exists, and the validator rejects the
latter outright. That section now describes the three validated skip shapes and
the gate as it is actually written.
ensure-editor.ps1 writes its provisioning summary from a finally block, so the
JSON exists after a failed gate -- which is exactly the run whose evidence a
reader wants. The binder wrote it into the workspace and copied it to
RUNNER_TEMP only on the success path, after the canonical-path check, so a
failed gate or a provenance mismatch uploaded nothing at all. Reported by
Cursor Bugbot.

The fix removes the copy instead of guarding it: -DiagnosticsPath now points at
the RUNNER_TEMP evidence directory, so the validator writes outside the
workspace itself, in its finally block, before the repository checkout that
wipes the workspace ever runs. The bind step keeps only the verdict -- read the
preserved summary, prove editorPath is the canonical tool-cache editor, export
it -- and reads the same file the artifact upload publishes, so the uploaded
evidence is by construction the evidence that was trusted. The step drops
"and preserve" from its name for the same reason.

Two mutations (evidence written back inside the workspace, bind reading an
unpreserved copy) fail the contract suite.
Copilot AI review requested due to automatic review settings July 31, 2026 15:36
@wallstop

Copy link
Copy Markdown
Collaborator Author

Cursor (@cursor) review

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@wallstop Eli Pinkerton (wallstop) changed the title ci: bind Unity editor gate provenance ci: bind Unity editor gate provenance and retire superseded Unity runs Jul 31, 2026

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit abaabb6. Configure here.

…ing comments

The five new validator checkouts dropped the '# v7' comment every other
actions/checkout pin in the repository carries, and three workflows still told
a reader that editor validation happens alongside credential validation and
lock acquisition. The gate now runs before the repository checkout, so a
failure there has already failed the job by the time those steps are reached.
A summary written by a failed gate can record no editorPath, and
[IO.Path]::GetFullPath('') throws an ArgumentException rather than saying so.
The binder now reports '(none recorded)' through its own mismatch message.

The head-freshness script wrote 'set -uo pipefail' under a shell GitHub already
starts with -e, which reads as if errexit were off. It says -euo now; behavior
is unchanged.
…stub

On Windows run_head_check returned a hardcoded answer instead of running the
script, so the four cases asserted the helper's own logic rather than the
workflow's. The whole table is now skipped there, matching how the aggregate
result-shape table already handles the same constraint.

The stub gh also accepted any invocation, so a script that queried a different
endpoint would still have passed. It now answers only the live-head lookup and
exits 2 otherwise, which the truth table turns into a failure: querying
repos/{repo}/commits/{sha} instead of the pull request, or dropping
--jq .head.sha, each fail the validator.
Copilot AI review requested due to automatic review settings July 31, 2026 15:59
@wallstop

Copy link
Copy Markdown
Collaborator Author

Cursor (@cursor) review

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 1d4563e. Configure here.

@wallstop
Eli Pinkerton (wallstop) merged commit ac1af7d into master Jul 31, 2026
48 of 51 checks passed
@wallstop
Eli Pinkerton (wallstop) deleted the codex/issue-166-editor-gate-provenance branch July 31, 2026 19:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ci: a superseded push keeps the Unity concurrency group, so the current SHA cannot start

2 participants