Push Cargo.lock to release-please PR #1007
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
| name: Push Cargo.lock to release-please PR | |
| # Privileged half of the release-please-PR Cargo.lock pipeline. | |
| # | |
| # Fires after the unprivileged `Build Cargo.lock for release-please | |
| # PR` workflow finishes. Downloads the Cargo.lock artifact it | |
| # produced, validates it, then pushes the file to the PR branch via | |
| # the GitHub Git Data API. | |
| # | |
| # **No `actions/checkout` of the PR branch.** **No `cargo` / | |
| # `npm` / `bun` invocation.** The only code that runs in this | |
| # privileged context is the artifact unzip + a github-script step | |
| # that talks to the API. This is what closes CodeQL's | |
| # `actions/untrusted-checkout/critical` rule: a privileged | |
| # workflow_run job must not execute arbitrary code from a non- | |
| # default ref. | |
| "on": | |
| workflow_run: | |
| workflows: ["Build Cargo.lock for release-please PR"] | |
| types: [completed] | |
| permissions: | |
| contents: write | |
| actions: read | |
| pull-requests: read | |
| jobs: | |
| push: | |
| if: >- | |
| github.event.workflow_run.event == 'pull_request' && | |
| github.event.workflow_run.conclusion == 'success' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Download artifact from upstream workflow run | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| with: | |
| script: | | |
| const artifacts = await github.rest.actions.listWorkflowRunArtifacts({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| run_id: context.payload.workflow_run.id, | |
| }); | |
| const match = artifacts.data.artifacts.find( | |
| (a) => a.name === 'release-please-lockfile', | |
| ); | |
| if (!match) { | |
| core.setFailed('No release-please-lockfile artifact found on upstream run'); | |
| return; | |
| } | |
| const download = await github.rest.actions.downloadArtifact({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| artifact_id: match.id, | |
| archive_format: 'zip', | |
| }); | |
| const fs = require('fs'); | |
| fs.writeFileSync('artifact.zip', Buffer.from(download.data)); | |
| - name: Unzip + validate artifact | |
| # Bail fast (and loudly) on anything unexpected β a privileged | |
| # workflow consuming an artifact from an unprivileged run must | |
| # treat that artifact as untrusted input. We accept it only | |
| # when every expected file is present, the Cargo.lock starts | |
| # with cargo's canonical header, and the file size is within | |
| # a sane envelope (1 KB-5 MB). | |
| run: | | |
| mkdir -p artifact | |
| unzip -q artifact.zip -d artifact | |
| for f in Cargo.lock pr_number pr_branch pr_head_sha; do | |
| test -f "artifact/$f" || { echo "::error::missing $f"; exit 1; } | |
| done | |
| head -1 artifact/Cargo.lock | grep -qE '^# This file is automatically @generated by Cargo' || { | |
| echo "::error::Cargo.lock does not start with the cargo header" | |
| exit 1 | |
| } | |
| size=$(stat -c%s artifact/Cargo.lock) | |
| if [ "$size" -lt 1000 ] || [ "$size" -gt 5000000 ]; then | |
| echo "::error::Cargo.lock size suspicious ($size bytes)" | |
| exit 1 | |
| fi | |
| - name: Validate PR identity + push Cargo.lock via API | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| const prNumber = parseInt( | |
| fs.readFileSync('artifact/pr_number', 'utf8').trim(), | |
| 10, | |
| ); | |
| const branch = fs.readFileSync('artifact/pr_branch', 'utf8').trim(); | |
| const artifactHeadSha = fs | |
| .readFileSync('artifact/pr_head_sha', 'utf8') | |
| .trim(); | |
| // Defensive parse β the artifact came from an unprivileged | |
| // build that ran on attacker-controllable code paths. | |
| if (!Number.isInteger(prNumber) || prNumber <= 0) { | |
| core.setFailed(`invalid PR number: ${prNumber}`); | |
| return; | |
| } | |
| if (!/^release-please--[A-Za-z0-9._\/-]+$/.test(branch)) { | |
| core.setFailed(`branch name does not match release-please pattern: ${branch}`); | |
| return; | |
| } | |
| if (!/^[0-9a-f]{40}$/.test(artifactHeadSha)) { | |
| core.setFailed(`invalid head SHA: ${artifactHeadSha}`); | |
| return; | |
| } | |
| // Verify the PR is still open, still authored by the | |
| // release-please bot, and its head ref still matches the | |
| // artifact's claim. | |
| const pr = await github.rest.pulls.get({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: prNumber, | |
| }); | |
| if (pr.data.state !== 'open') { | |
| core.info(`PR #${prNumber} is ${pr.data.state} β nothing to do`); | |
| return; | |
| } | |
| if (pr.data.user.login !== 'github-actions[bot]') { | |
| core.setFailed( | |
| `PR author is ${pr.data.user.login}, expected github-actions[bot]`, | |
| ); | |
| return; | |
| } | |
| if (pr.data.head.ref !== branch) { | |
| core.setFailed( | |
| `PR head ref ${pr.data.head.ref} does not match artifact branch ${branch}`, | |
| ); | |
| return; | |
| } | |
| // If the PR branch has moved since the artifact was built, | |
| // the Cargo.lock we have was generated against a now-stale | |
| // Cargo.toml. Bail and let the next build-workflow run | |
| // produce a fresh artifact. | |
| const currentHead = pr.data.head.sha; | |
| if (currentHead !== artifactHeadSha) { | |
| core.info( | |
| `PR head moved (${artifactHeadSha} -> ${currentHead}); skipping push, the next build will pick this up`, | |
| ); | |
| return; | |
| } | |
| // Create a blob with the new Cargo.lock content. | |
| const content = fs.readFileSync('artifact/Cargo.lock'); | |
| const blob = await github.rest.git.createBlob({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| content: content.toString('base64'), | |
| encoding: 'base64', | |
| }); | |
| // Build a new tree that swaps src-tauri/Cargo.lock for our | |
| // blob, leaving the rest of the parent tree untouched. | |
| const parent = await github.rest.git.getCommit({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| commit_sha: currentHead, | |
| }); | |
| const tree = await github.rest.git.createTree({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| base_tree: parent.data.tree.sha, | |
| tree: [ | |
| { | |
| path: 'src-tauri/Cargo.lock', | |
| mode: '100644', | |
| type: 'blob', | |
| sha: blob.data.sha, | |
| }, | |
| ], | |
| }); | |
| // Idempotent: if Cargo.lock is already in sync, the new | |
| // tree SHA equals the parent's and we bail. | |
| if (tree.data.sha === parent.data.tree.sha) { | |
| core.info('Cargo.lock already in sync with Cargo.toml β nothing to push'); | |
| return; | |
| } | |
| // Commit + fast-forward the branch ref. | |
| const commit = await github.rest.git.createCommit({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| message: 'chore: bump Cargo.lock', | |
| tree: tree.data.sha, | |
| parents: [currentHead], | |
| }); | |
| await github.rest.git.updateRef({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| ref: `heads/${branch}`, | |
| sha: commit.data.sha, | |
| }); | |
| core.info(`Pushed Cargo.lock commit ${commit.data.sha} to ${branch}`); |