Repository navigation
ci(pages): reconcile owned deployment records safely #1029
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
ae796f8
ci(pages): reconcile owned deployment records safely
qnbs a06078d
docs(changelog): link Pages pruning review
qnbs d1d3c9e
ci(pages): prove rollback and publication provenance before pruning
qnbs 0310720
chore(merge): update the Pages pruning branch with main
qnbs 9d75944
ci(pages): reserve candidate cost by observed depth and prove every r…
qnbs c252986
ci(pages): reserve retried reads before the first write and sync the …
qnbs b152763
ci(pages): list every eligible record in the manifest of a blocked run
qnbs ce2c12e
merge: update ci/1028-pages-record-pruning with main (#1031, #1032)
qnbs d4e9d00
ci(pages): bind publication authors by account id, bound refresh chur…
qnbs 026c155
ci(pages): re-read evidence at write boundaries, check the live rate …
qnbs 97206e2
ci(pages): require the classifier's ownership and Pages-status shape …
qnbs cdfd049
ci(pages): gate scan and direct delete on the live allowance, validat…
qnbs 2b1e677
ci(pages): count rate headers of failed requests, bind job evidence t…
qnbs dec972c
ci(pages): gate every read on the live allowance and the CI reserve
qnbs 064e665
ci(pages): a response without a usable rate header leaves the allowan…
qnbs File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| { | ||
| "rollback": [], | ||
| "evidence": [6995722425, 6984593438, 6983983689, 6969003237, 6965361606, 6961183719] | ||
| } |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,96 +1,44 @@ | ||
| name: Prune old GitHub deployment records | ||
|
|
||
| on: | ||
| workflow_run: | ||
| workflows: [CI / CD] | ||
| types: [completed] | ||
| schedule: | ||
| # Weekly cleanup on Sundays at 03:00 UTC | ||
| - cron: '0 3 * * 0' | ||
| # Daily reconciliation includes the Sunday 03:00 UTC backlog pass. | ||
| - cron: '0 3 * * *' | ||
| workflow_dispatch: | ||
|
|
||
| # QNBS-v3: top-level read-only; deployments:write scoped to job (CodeQL Token-Permissions). | ||
| # QNBS-v3: top-level read-only; the job holds no write scope while the destructive gate in scripts/pages-pruning/policy.mjs is closed (CodeQL Token-Permissions). | ||
| permissions: | ||
| contents: read | ||
|
|
||
| concurrency: | ||
| group: github-pages-record-pruning | ||
| cancel-in-progress: false | ||
|
qnbs marked this conversation as resolved.
|
||
|
|
||
| jobs: | ||
| prune: | ||
| name: Keep latest 3 GitHub deployment records | ||
| name: Reconcile GitHub Pages records only | ||
| if: github.repository == 'qnbs/WorldScript-Studio' && github.ref == 'refs/heads/main' | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 10 | ||
| permissions: | ||
| deployments: write | ||
| contents: read | ||
| actions: read | ||
| pages: read | ||
| deployments: read | ||
| steps: | ||
| - name: Delete stale GitHub deployment records — keep latest 3 per environment | ||
| # QNBS-v3: v9.0.0 uses node24 — v7 was node20 (deprecated June 2026) | ||
| - name: Check out immutable trusted main execution tree | ||
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | ||
| with: | ||
| ref: ${{ github.sha }} | ||
| persist-credentials: false | ||
| - name: Plan or reconcile bounded Pages records | ||
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | ||
| with: | ||
| retries: 0 | ||
| script: | | ||
| const KEEP = 3; | ||
|
|
||
| // QNBS-v3: GitHub API only; Vercel artifacts require the protected #627 procedure. | ||
| // QNBS-v3: Paginate all GitHub deployment records to prevent retention drift. | ||
| const allDeployments = await github.paginate( | ||
| github.rest.repos.listDeployments, | ||
| { | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| per_page: 100, | ||
| }, | ||
| ); | ||
|
|
||
| // Group by environment, sort each group newest-first, keep the first KEEP | ||
| const byEnv = {}; | ||
| for (const dep of allDeployments) { | ||
| const env = dep.environment || 'unknown'; | ||
| (byEnv[env] ??= []).push(dep); | ||
| } | ||
|
|
||
| let totalDeleted = 0; | ||
| let totalFailed = 0; | ||
|
|
||
| for (const [env, deps] of Object.entries(byEnv)) { | ||
| const sorted = deps.sort((a, b) => b.id - a.id); | ||
| const toDelete = sorted.slice(KEEP); | ||
|
|
||
| if (toDelete.length === 0) { | ||
| core.info(`[${env}] Nothing to prune — only ${deps.length} deployment(s).`); | ||
| continue; | ||
| } | ||
|
|
||
| core.info(`[${env}] ${deps.length} total → pruning ${toDelete.length}, keeping ${KEEP} latest.`); | ||
|
|
||
| let deleted = 0; | ||
| let failed = 0; | ||
| for (const dep of toDelete) { | ||
| // A deployment must be inactive before it can be deleted | ||
| try { | ||
| await github.rest.repos.createDeploymentStatus({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| deployment_id: dep.id, | ||
| state: 'inactive', | ||
| }); | ||
| } catch (e) { | ||
| core.warning(`[${env}] Could not inactivate ${dep.id}: ${e.message}`); | ||
| } | ||
|
|
||
| try { | ||
| await github.rest.repos.deleteDeployment({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| deployment_id: dep.id, | ||
| }); | ||
| deleted++; | ||
| } catch (e) { | ||
| core.warning(`[${env}] Could not delete ${dep.id}: ${e.message}`); | ||
| failed++; | ||
| } | ||
| } | ||
|
|
||
| core.info(`[${env}] Done: ${deleted} deleted, ${failed} failed.`); | ||
| totalDeleted += deleted; | ||
| totalFailed += failed; | ||
| } | ||
|
|
||
| core.info(`All environments: ${totalDeleted} deleted, ${totalFailed} failed.`); | ||
| const { readFileSync } = require('node:fs'); | ||
| const { pathToFileURL } = require('node:url'); | ||
| const root = process.env.GITHUB_WORKSPACE; | ||
| const { runAction } = await import(pathToFileURL(`${root}/scripts/prune-pages-deployments.mjs`).href); | ||
| const holds = JSON.parse(readFileSync(`${root}/.github/pages-pruning-holds.json`, 'utf8')); | ||
| await runAction({ github, context, core, holds, workflowRef: process.env.GITHUB_WORKFLOW_REF }); | ||
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
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
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.