ci: bind Unity editor gate provenance and retire superseded Unity runs - #326
Merged
Eli Pinkerton (wallstop) merged 7 commits intoJul 31, 2026
Merged
Conversation
Collaborator
Author
|
Cursor (@cursor) review |
…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.
Collaborator
Author
|
Cursor (@cursor) review |
) 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.
Collaborator
Author
|
Cursor (@cursor) review |
There was a problem hiding this comment.
✅ 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.
Collaborator
Author
|
Cursor (@cursor) review |
There was a problem hiding this comment.
✅ 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.
Eli Pinkerton (wallstop)
deleted the
codex/issue-166-editor-gate-provenance
branch
July 31, 2026 19:09
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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)
-CiManagedOnly -RequireHealthyExisting, and a bind step that fails unlesseditorPathis the canonicalRUNNER_TOOL_CACHE\u6-v3\<version>\Editor\Unity.exe.-DiagnosticsPathpoints at aRUNNER_TEMPdirectory, soensure-editor.ps1writes its summary outside the workspace, from its ownfinallyblock. The evidence therefore survives both a failed gate and the checkout that wipes the workspace, and theif: always()upload publishes the same file the bind step trusted.unity-tests,unity-benchmarks,perf-benchmarks,release/unity-checks,release/unitypackage).Superseded Unity runs (closes #311)
cancel-in-progress: falseis 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, andUnity CI Successwas absent on the head that mattered.head-checkjob onubuntu-latestcompares the event head against the live pull-request head, before the lock is in reach, and publishes asupersededoutput that both licensed jobs gate on. A superseded run now costs one cheap job instead of nine self-hosted ones.Require current PR head before setupguard stays; it covers the push that lands afterhead-checkhas already passed.Unity CI Successreports the shape rather than going absent: it requireshead-checkto have succeeded and accepts skipped licensed jobs for a superseded head alongside the fork and Dependabot shapes.Review feedback addressed
${{ runner.temp }}/dx-unity-editor-validationdirectly;if-no-files-found: warnkeeps the previous behaviorensure-editor.ps1writes its summary toRUNNER_TEMPitself, from afinallyblock, so a failed gate and a provenance mismatch both uploadPrettier and yamllintredensure-editor.ps1calls are wrapped again;-InstallRootreturns to one spelling, so the policy validator needs no second accepted formScript testsred onvalidate:js-loc-budgetvalidationStepassertions folded into three, paying for the bind-step and upload-step contractscopilot-pull-request-reviewerreports a reviewer quota limit, not a finding.Where the contracts live
scripts/validate-unity-pr-policy.pyowns 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 stubghfor push, current head, superseded head, and failed lookup.scripts/__tests__/ci-aggregate-workflow.test.jsowns 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, andCI Successpass; the pull request reportsmergeable_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 freshnessruns in 5 seconds and reportedsuperseded=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 uploadedfromE:\actions-runner\_work\_temp/dx-unity-editor-validation-- the summary JSON plus the sibling.txtthatWrite-UnityProvisioningSummarywrites 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 redUnity CI Successon a stale SHA.One leg needed a re-run, and not as a flake write-off.
Unity 2021.3.45f1 playmodefirst failed on the shared build lock, not on tests: it acquired the lock, and 68 seconds later the release reportedLock is already free(acquired=true ... reservation=missing). The predecessor commitabaabb68passed 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:
ci-aggregate-workflow.test.js-InstallRootback to the quoted formci-aggregate-workflow.test.jsshell: pwshwithout-NoProfile -NonInteractiveci-aggregate-workflow.test.jsci-aggregate-workflow.test.jsci-aggregate-workflow.test.jsci-aggregate-workflow.test.jsci-aggregate-workflow.test.jsvalidate-unity-pr-policy.pyvalidate-unity-pr-policy.pyvalidate-unity-pr-policy.pyvalidate-unity-pr-policy.pyvalidate-unity-pr-policy.pyvalidate-unity-pr-policy.py--jq .head.shavalidate-unity-pr-policy.pyLocal:
npm test406/0,npm run validate:allpass,validate-js-loc-budget17500/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.mddescribed amatrix-configjob and anre-actors/alls-greenaggregate forUnity CI Success. Neither exists, andvalidate-unity-pr-policy.pyrejects the latter outright. That section now describes the three validated skip shapes and the gate as written.Self-review, beyond what reviewers raised
# v7comment every otheractions/checkoutpin in the repository carries.editorPath, and[IO.Path]::GetFullPath('')throws anArgumentExceptionrather than saying so. The binder reports(none recorded)through its own mismatch message.ghalso accepted any invocation; it now answers only the live-head lookup.Issues opened while working on this
Require confirmed Unity cleanup, because the classify step skips and the gate reads an empty classification.successwhile it cannot read runner inventory, which is why an eight-hour-stuckPerformance Numbersqueue onmasterwent unreported.Follow-up to #322, which squash-merged before the provenance binder commit was ready.
Closes #311.