Repository navigation
Restrict cleanup to an explicitly identified test release #31
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
5 commits
Select commit
Hold shift + click to select a range
cd081cb
fix(ci): restrict cleanup to identified test releases
Jamie-BitFlight 638be8a
test(release): publish and clean up a uniquely owned prerelease
Jamie-BitFlight df13c52
docs(ci): explain exact test release cleanup and recovery
Jamie-BitFlight 56fbf04
fix(ci): account for read-only Dependabot publication tokens
Jamie-BitFlight 1530877
fix(ci): retain publication coverage with configured write permissions
Jamie-BitFlight 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
Some comments aren't visible on the classic Files Changed page.
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
73 changes: 55 additions & 18 deletions
73
.github/workflows/pull_request_cleanup_tags_and_releases.yml
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,27 +1,64 @@ | ||
| name: 🧼 Pull Request - Cleanup Tags and Releases | ||
| name: Clean up a controlled test release | ||
|
|
||
| on: | ||
| delete: | ||
| pull_request: | ||
| types: [closed] | ||
| workflow_call: | ||
| inputs: | ||
| tag: | ||
| required: true | ||
| type: string | ||
| expected_sha: | ||
| required: true | ||
| type: string | ||
| workflow_dispatch: | ||
| inputs: | ||
| regex: | ||
| description: 'Regex to search by' | ||
| required: false | ||
| pr_number: | ||
| description: 'PR Number to search by' | ||
| required: false | ||
| tag: | ||
| description: 'Exact test tag: vMAJOR.MINOR.PATCH-repair.RUN.ATTEMPT' | ||
| required: true | ||
| type: string | ||
| expected_sha: | ||
| description: 'Full commit SHA recorded when the test release was created' | ||
| required: true | ||
| type: string | ||
|
|
||
| permissions: | ||
| contents: write | ||
|
|
||
| concurrency: | ||
| group: cleanup-test-release-${{ inputs.tag }} | ||
| cancel-in-progress: false | ||
|
|
||
| jobs: | ||
| test: | ||
| name: Cleanup Tags and Releases | ||
| runs-on: ubuntu-latest | ||
| cleanup: | ||
| runs-on: ubuntu-24.04 | ||
| steps: | ||
| - name: Remove unused releases and tags | ||
| uses: Broadshield/github-action-cleanup-releases-and-tags@main | ||
| - name: Validate ownership and delete only the named test release and tag | ||
| uses: actions/github-script@v8 | ||
| env: | ||
| GITHUB_TOKEN: ${{ github.token }} | ||
| TEST_TAG: ${{ inputs.tag }} | ||
| EXPECTED_SHA: ${{ inputs.expected_sha }} | ||
| with: | ||
| regex: ${{ github.event.inputs.regex }} | ||
| pr_number: ${{ github.event.inputs.pr_number }} | ||
| script: | | ||
| const tag = process.env.TEST_TAG; | ||
| const expected = process.env.EXPECTED_SHA; | ||
| if (!/^v[0-9]+\.[0-9]+\.[0-9]+-repair\.[0-9]+\.[0-9]+$/.test(tag)) { | ||
| throw new Error('Only explicitly named repair test tags can be removed.'); | ||
| } | ||
| if (!/^[a-f0-9]{40}$/.test(expected)) { | ||
| throw new Error('The expected full commit SHA is required.'); | ||
| } | ||
| const absentOn404 = async (request) => { | ||
| try { return (await request()).data; } | ||
| catch (error) { if (error.status === 404) return null; throw error; } | ||
| }; | ||
| const commit = await absentOn404(() => github.rest.repos.getCommit({ ...context.repo, ref: tag })); | ||
| const release = await absentOn404(() => github.rest.repos.getReleaseByTag({ ...context.repo, tag })); | ||
| if (commit && commit.sha !== expected) throw new Error('Test tag does not target the expected commit.'); | ||
| if (release && (release.tag_name !== tag || !release.prerelease)) { | ||
| throw new Error('The named release must be a prerelease.'); | ||
| } | ||
| if (release && !commit && release.target_commitish !== expected) { | ||
| throw new Error('Cannot verify the target of a release whose tag is missing.'); | ||
| } | ||
| if (release) await github.rest.repos.deleteRelease({ ...context.repo, release_id: release.id }); | ||
| if (commit) await github.rest.git.deleteRef({ ...context.repo, ref: `tags/${tag}` }); | ||
| core.info(`Deleted controlled test release ${tag} at ${expected}.`); | ||
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,91 @@ | ||
| name: Controlled release publication test | ||
| on: | ||
| pull_request: | ||
| paths: | ||
| - '.github/workflows/pull_request_cleanup_tags_and_releases.yml' | ||
| - '.github/workflows/test-release-publication.yml' | ||
| workflow_dispatch: | ||
|
|
||
| permissions: | ||
| contents: write | ||
|
|
||
| jobs: | ||
| publish: | ||
| if: github.event_name == 'workflow_dispatch' || github.event.pull_request.head.repo.full_name == github.repository | ||
| runs-on: ubuntu-24.04 | ||
| outputs: | ||
| tag: ${{ steps.publish.outputs.tag }} | ||
| sha: ${{ steps.publish.outputs.sha }} | ||
| steps: | ||
| - uses: actions/checkout@v6 | ||
| with: | ||
| persist-credentials: false | ||
| - name: Package the exact checked-out action | ||
| run: git archive --format=tar.gz --output=action.tar.gz HEAD action.yml dist package.json README.md LICENSE | ||
| - name: Publish a unique prerelease without moving any existing ref | ||
| id: publish | ||
| uses: actions/github-script@v8 | ||
| with: | ||
| script: | | ||
| const fs = require('node:fs'); | ||
| const { execFileSync } = require('node:child_process'); | ||
| const sha = execFileSync('git', ['rev-parse', 'HEAD'], { encoding: 'utf8' }).trim(); | ||
| const version = JSON.parse(fs.readFileSync('package.json', 'utf8')).version; | ||
| const tag = `v${version}-repair.${context.runId}.${process.env.GITHUB_RUN_ATTEMPT}`; | ||
| // createRef rejects an existing tag; this workflow never updates refs. | ||
| await github.rest.git.createRef({ ...context.repo, ref: `refs/tags/${tag}`, sha }); | ||
| core.setOutput('tag', tag); | ||
| core.setOutput('sha', sha); | ||
| const { data: release } = await github.rest.repos.createRelease({ | ||
| ...context.repo, tag_name: tag, target_commitish: sha, | ||
| name: `Controlled publication test ${tag}`, prerelease: true, make_latest: 'false', | ||
| body: `Disposable publication verification for commit ${sha}.`, | ||
| }); | ||
| await github.rest.repos.uploadReleaseAsset({ | ||
| ...context.repo, release_id: release.id, name: 'action.tar.gz', | ||
| data: fs.readFileSync('action.tar.gz'), | ||
| }); | ||
| const { data: commit } = await github.rest.repos.getCommit({ ...context.repo, ref: tag }); | ||
| const { data: published } = await github.rest.repos.getReleaseByTag({ ...context.repo, tag }); | ||
| if (commit.sha !== sha || published.draft || !published.prerelease || | ||
| published.tag_name !== tag || !published.body.includes(sha) || | ||
| published.assets.length !== 1 || published.assets[0].name !== 'action.tar.gz') { | ||
| throw new Error('Published test release differs from the requested artifact and revision.'); | ||
| } | ||
| - name: Verify the published artifact byte for byte | ||
| env: | ||
| GH_TOKEN: ${{ github.token }} | ||
| TEST_TAG: ${{ steps.publish.outputs.tag }} | ||
| run: | | ||
| gh release download "$TEST_TAG" --repo "$GITHUB_REPOSITORY" --pattern action.tar.gz --dir downloaded | ||
| cmp action.tar.gz downloaded/action.tar.gz | ||
|
|
||
| cleanup: | ||
| if: always() && needs.publish.outputs.tag != '' | ||
| needs: publish | ||
| uses: ./.github/workflows/pull_request_cleanup_tags_and_releases.yml | ||
| with: | ||
| tag: ${{ needs.publish.outputs.tag }} | ||
| expected_sha: ${{ needs.publish.outputs.sha }} | ||
|
|
||
| verify-cleanup: | ||
| needs: [publish, cleanup] | ||
| runs-on: ubuntu-24.04 | ||
| permissions: | ||
| contents: read | ||
| steps: | ||
| - uses: actions/github-script@v8 | ||
| env: | ||
| TEST_TAG: ${{ needs.publish.outputs.tag }} | ||
| with: | ||
| script: | | ||
| const tag = process.env.TEST_TAG; | ||
| for (const lookup of [ | ||
| () => github.rest.repos.getReleaseByTag({ ...context.repo, tag }), | ||
| () => github.rest.git.getRef({ ...context.repo, ref: `tags/${tag}` }), | ||
| ]) { | ||
| try { await lookup(); } | ||
| catch (error) { if (error.status === 404) continue; throw error; } | ||
| throw new Error(`Controlled test artifact ${tag} remains after cleanup.`); | ||
| } | ||
| core.info(`Verified exact test release and tag ${tag} are absent.`); |
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.