Repository navigation
Move publish-db out of the pull-request workflow - #220
Merged
Merged
Conversation
ci.yml runs on pull requests and installs and runs PyPI packages and each certified variant's pins, yet its push-only publish-db job named the Supabase secrets. A runner receives a secret its job names even in a step that never runs, and GitHub scrubs only the secrets not referenced in the workflow, so those runners may have held the key. publish-db.yml now publishes the database after CI completes on a push to main (workflow_run), downloading the artifact CI's pytest job uploaded. Its one job has actions: read, runs no package code, and runs only for ci.yml's own pushes to main. Unset secrets still skip with a notice, a failed upload is still a warning, and curl stays silent. tests/test_ci_gates.py: no workflow that runs on pull requests names any secret. tests/test_publish_db.py pins the trigger, the guard, the artifact and the step scripts' behaviour under stubs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ed since 6 October Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the download
From the independent review:
- latest.db.gz moves only when the CI commit is still main's tip
(GITHUB_SHA in a workflow_run run), so re-running an older main run
no longer rolls it back; <sha>.db.gz is uploaded either way.
- The secrets scanner reads each ${{ }} as the runner's TemplateReader
does, so a }} inside a quoted string no longer hides a reference, and
it scans every event a pull request starts.
- Concurrency is on the job, so a run the guard skips never displaces a
waiting publish.
- download-artifact is pinned to v4.3.0's commit (what v4 names today),
as uk-replay-results.yml pins the actions beside its key.
- The size note: ~196 MB built, ~14 MB gzipped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…about the concurrency group From the second review: issue_comment runs are started by a comment on a pull request, so they join the scanned events. The comments on the job-level concurrency group no longer claim how GitHub treats a job its guard skips, which GitHub does not document; a workflow-level group would hold every run of the file, which it does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis
marked this pull request as ready for review
October 22, 2026 04:19
…currency group GPT-6.1 Sol's review of #220 found three things the first design got wrong: - A re-run of an old publish run keeps its GITHUB_SHA, so comparing that with the CI commit let it move latest.db.gz back. The find step now reads main's tip from the API. - An artifact does not say the attempt that fired this run passed. The find step now reads the pytest job's conclusion for that attempt, as needs: python did. - GitHub cancels a concurrency group's pending job when another enters, so an older completion could drop the publish for main's tip. The group is gone. curl gives up after five minutes, so an upload for an older tip cannot outlast the next commit's CI. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The second review of #220 found the comment overclaimed. curl's five-minute limit covered one upload, but the window after the tip read also holds the download, gzip and a second upload, longer than the fastest CI run. The job now has five minutes in all, and the fastest of main's last 40 successful CI runs took seven. Each upload has two minutes, so a stall still ends in the warning. A real publish takes about ten seconds. The comment also now says latest.db.gz lags main while the tip's pytest job has not passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The third review of #220 found the timing argument behind the dropped concurrency group could not be made a guarantee, and that the job limit added to support it could end a job before its warning. A concurrency group with queue: max gives the ordering directly: one publish runs at a time, and a waiting one survives where the default queue would cancel it. Three pushes on a throwaway branch ran in order with none cancelled (runs 41138146103, 41138172124, 41138195694). The job limit goes, and each upload keeps its own, so a stall warns and frees the queue. A test now pins that the upload is the pytest job's last step, so an artifact of that name passed the job's checks whichever attempt uploaded it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fourth review of #220 found two overstatements. queue: max keeps up to 100 pending publishes, not every one. And with no job limit, a stalled gh call could hold the queue for GitHub's 360-minute default. The find and download steps now have five minutes each, and a stall there fails the run. The publish step keeps only curl's limits, so no limit can end it before its warning. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
Author
|
First publishes on main, both as designed:
Correction to this PR's body: a |
This branch was successfully deployed
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.
No workflow that runs on pull requests names a secret any more.
publish-db, the job that holdsSUPABASE_SCORECARD_URLandSUPABASE_SCORECARD_KEY, moves out ofci.ymlinto a newpublish-db.yml, triggered byworkflow_runwhen CI completes on main.Follow-up to #215, which did the same for the dataset token.
Problem
ci.ymlruns on pull requests. Itspythonandreplay-enginejobs install and run PyPI packages and each certified variant's pins, as a runner user with passwordless sudo. Itspublish-dbjob was push-only, but it named the Supabase secrets.A debug re-run of #215's pull-request CI (job 119587048984) evaluated a secret that its job named only in a skipped step. GitHub scrubs the secrets "not referenced in the workflow". So the pull-request runners in
ci.ymlmay have held the key.Nobody has shown that a job receives a secret named only by another job of the same workflow. Moving the job settles the question without having to prove it either way.
What changes
.github/workflows/publish-db.yml. It runson: workflow_run: workflows: [CI], types: [completed], branches: [main], and has one job,publish db to supabase storage. The job:workflow_runruns main's copy of the file with the repository's secrets after any CI run, a fork's pull request included. Thebranchesfilter names a branch, not an event or a repository. So the job'sifalso requires the triggering run'sevent == 'push',head_branch == 'main',head_repository.full_name == github.repositoryandpath == '.github/workflows/ci.yml'.CI_RUN_ID,CI_ATTEMPTandCI_SHA; no script contains a${{ }}. No script readsGITHUB_SHA: in aworkflow_runrun it is not the CI commit, and a re-run keeps its original value.needs: pythondid, and lists the CI run's artifacts with the image'sgh. Unless that job passed andscorecard-db-<sha>is there, it skips with a notice. Otherwise it downloads the artifact withdownload-artifact(run-id+github-token), now pinned to v4.3.0's commit, the onev4names today.download-artifactand the runner image'sgh,gzipandcurl.actions: readandcontents: read, and nothing else. Reading CI's jobs and artifact takes the first, and reading main's tip the second. There is no checkout.queue: max. A job-level concurrency group stops a publish for an older tip from overwritinglatest.db.gzbehind a newer one. By default GitHub cancels a group's pending job when another enters, so an older run's completion could drop the publish for main's tip;queue: maxkeeps up to 100 waiting publishes. A workflow-level group would hold every run of the file, the ones the guard skips included.ci.ymlloses thepublish-dbjob. Its permissions comment now says where publishing happens.scorecard_db/README.mdsays where publishing runs, and that every publish since 6 October has failed.Why
workflow_run, not a push workflow that rebuilds the databaseThe task offered both. A rebuild (
python -m scorecard_db.build_db) runs the project's PyPI packages. Run in the job that holds the key, they sit next to it. Run in a sibling job of the same workflow, they depend on exactly the boundary that is unproven. A rebuild would also publish a database whose test suite never ran: before,needs: pythonmeant publishing waited for the format check, the determinism check, the no-drift gate, the suite and the replay regeneration.workflow_runkeeps that gate and those exact bytes, and puts the key where no package code runs. Its costs are two read scopes on one job and the guard above.Behaviour
Kept, as in the old step:
::notice::Supabase secrets not set — skipping publish.::warning title=Supabase publish failed::…and a step-summary line, and exits 0.-sfwith its output discarded, so the host in the secret URL is never printed.Changed:
latest.db.gzmoves only for the commit at main's tip, read from the API as the job starts.<sha>.db.gzis uploaded either way, and a notice says whenlatestwas left alone.latestback. Underworkflow_run, any re-run of a CI run triggers a publish, and a re-run keeps its originalGITHUB_SHA, so only the live tip tells them apart.lateston its way out. If the newer commit's pytest job then fails,lateststays one commit further back than it would have before.<sha>.db.gz, as before. Run 39694074043 was cancelled and itspublish-dbstill succeeded.Invariants (tested)
No workflow that runs on pull requests names a secret. This covers
pull_request,pull_request_target,pull_request_review,pull_request_review_comment,issue_commentandmerge_group; the allowed set is empty (tests/test_ci_gates.py).${{ }}as the runner'sTemplateReader.ParseScalardoes, so a}}inside a quoted string doesn't end it. It also reads everyifin full and treats a job'ssecrets:as every secret.secrets.X,secrets['X']andsecrets["X"],toJSON(secrets), computed names, any case and spacing,secrets: inherit, and}}hidden in string literals, with or without an escaped quote.inputs.secrets, the word in a script, and comments.Only
publish-db.ymlnames the Supabase secrets. They appear only in its upload step's environment, and it has no pull-request trigger. It is the onlyworkflow_runworkflow, so a new one gets the same guard and its own test.The guard admits only CI's pushes to main here. It is evaluated over a push to main in this repository (admitted) and seven refused cases:
mainpull_request_targetworkflow_dispatchThe evaluator compares strings case-insensitively, as GitHub does.
It downloads exactly what
ci.yml's pytest job uploaded, and only when that job passed in the attempt that fired it. The name isscorecard-db-<head_sha>and the run is the triggering one. The find step asks for that same name, and for the conclusion of the jobci.ymlnames, so a rename fails a test. The upload is that job's last step, with noifand nocontinue-on-errorin the job, so any artifact of that name passed its checks, whichever attempt uploaded it.No package code and the narrowest token. The job's only action is the pinned
download-artifact. No script calls uv, pip, Python, bun, npm, node, make, bash or sh.github.tokenappears only in the find and download steps, and the workflow and job environments name no secret.Behaviour under
bash -e, the shell the job log shows, with stubghandcurl:.gz.<sha>.db.gzand thenlatest.db.gz, with the old headers and a payload that gunzips back to the database.<sha>.db.gzgoes up, even withGITHUB_SHAequal to the CI commit, as on a re-run of an old publish.-sis dropped, so that check has something to catch.latest.db.gzmoves only to main's tip. It lags main while the tip's pytest job has not passed, or after a push that runs no CI. See 6; no script readsGITHUB_SHA, the job holds aqueue: maxconcurrency group, and neither it nor the publish step has a time limit of its own, so no limit can end it inside an upload, before the warning. Each upload has its own limit, and the find and download steps five minutes each.Each of these 49 mutations fails at least one test:
pull_requesttrigger; dropping the event or repository clause;||in the guardtoJSON(secrets)back inci.yml; renaming CIGITHUB_SHAfor the object name; curl without-sor without its time limit; a changed curl limit; a time limit on the job or the publish step, or none on find or download;exit 1on a failed upload; the unset-secrets skip failing;latestalways, never, or compared withGITHUB_SHA; the tip missing from the step's environmentCI_ATTEMPT${{ head_branch }}in a script; adding a checkout; adding a pip install; counting expired artifacts; downloading unconditionally; another run idactions: write;contents: write; no contents scopequeue: singleorcancel-in-progressci.yml: renaming CI or the pytest job; anifon the upload; a step after it;continue-on-errorin the jobChecked live
gh:found=trueon main's run 40394071052, and on run 39694074043, which a newer push cancelled after its pytest job passed;found=trueon their attempt 2, where pytest passed.event=push,head_branch=main,head_repository.full_name=PolicyEngine/policyengine-scorecardandpath=.github/workflows/ci.yml, the fields the guard reads.curl exit 6.publish-dbjob, skipped on pull-request runs, withrunner_id: nulland no steps. GitHub's docs don't say this.queue: maxat job level. On a throwaway branch, now deleted, three pushes 14 seconds apart ran one after another and none was cancelled (runs 41138146103, 41138172124 and 41138195694). Under the default queue the third would have cancelled the second.queue, and reports only that forpublish-db.yml. Inci.ymlit reports only the existing SC2129 hint. CI doesn't run actionlint.workflow_runworkflows run only from the default branch, so this PR's own CI cannot runpublish-db.yml. It first runs when CI completes on the merge commit; I'll watch that run and expect the job to run and end with the usualcurl exit 6warning.Not addressed here
pull_requestrun takesci.ymlfrom the merge commit with the base, not from the branch, so new runs of open pull requests left the job out as soon as this merged. Run 41141999014 on Certify the policyengine.py 6.11.0 replay results #235 lists no publish job, although that head's ownci.ymlstill has it; the branch's run 30 minutes earlier listed one. GitHub's docs say a re-run keeps its original commit, so a re-run of a run from before this merge still uses the old file.upload-artifactv4 can overwrite an artifact by name, so another job of the same push run could replace it before CI completes.Review
A GPT-6.1 Sol review of the first version requested changes on three points, each now fixed:
latestback, because it keeps itsGITHUB_SHA. The job now reads main's tip.A second review (Opus 5.5, with no shell) found all three gone and requested two more changes, both now made:
latestalways equals the tip. It lags while the tip's pytest job has not passed.A third review (GPT-6.1 Sol) requested three more changes:
queue: max.A fourth review (GPT-6.1 Sol) confirmed the queue reads the tip inside its slot and requested two more changes, both now made:
queue: maxholds up to 100 pending publishes, not every one, so the comments now say that.ghcall could hold the queue for GitHub's 360-minute default. The find and download steps now have five minutes each.A fifth review (GPT-6.1 Sol) approved at 4b297fe. I scoped it to that commit and the whole workflow, with a bar of a broken requirement, a false statement or a line that does nothing, and it found nothing that met the bar. It ran read-only, so it probed the scripts directly and did not run pytest. Live GitHub scheduling, cancellation and timeouts remain unverified beyond the
queue: maxprobe above.Before those, an independent Opus 5.5 review approved, with no blockers (the Codex lanes were closed). This version addresses its findings:
latestand re-runs}}inside string literals in the scanner"Not addressed here" covers the open-branches finding. I left the artifact-overwrite hardening out, as the review called it optional and nothing reads the copy.
A second pass by the same reviewer on the fix commit also approved:
TemplateReader.ParseScalarto Python, and the scanner's reader agreed with it on every short string and on 80,000 random longer ones.latestgate cannot freeze it.Its two remaining nits,
issue_commentand the concurrency comment, are in the last commit.Locally, merged with main at 7591e3c, the publish, CI-gate, replay-queue, determinism, engine-matrix and results-PR test files give 281 passed.
ruff format --checkis clean, andci.ymlnames no secret.axiom: n/a: CI only
🤖 Generated with Claude Code