chore: release v6.3.0 #311
Workflow file for this run
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
| # Release workflow: Release PR planning and resumable publication. | |
| # | |
| # Pipeline: | |
| # non-release push to main -> creates or updates the Release PR | |
| # merged Release PR -> reconciles crates, tags, and the public | |
| # zebrad GitHub Release | |
| # workflow_dispatch -> manual readiness check or post-merge recovery | |
| # | |
| # Downstream triggers (independent workflows, not managed here): | |
| # release:released -> release-binaries.yml (Docker Hub) | |
| # release:published -> zfnd-deploy-nodes-gcp.yml (GCP deployment) | |
| name: Release | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| branches: [main] | |
| types: [closed] | |
| workflow_dispatch: | |
| inputs: | |
| operation: | |
| description: "Check an open Release PR or resume a merged one" | |
| required: true | |
| default: check | |
| type: choice | |
| options: | |
| - check | |
| - resume | |
| release_pr_number: | |
| description: "Release PR number" | |
| required: true | |
| type: string | |
| # Deny all at workflow level; each job declares minimum permissions. | |
| permissions: {} | |
| jobs: | |
| # A release commit belongs to the post-merge controller and must not create | |
| # another Release PR. | |
| release-pr: | |
| name: Create or update Release PR | |
| if: >- | |
| github.event_name == 'push' && | |
| !startsWith(github.event.head_commit.message, 'chore: release') | |
| runs-on: ubuntu-latest | |
| concurrency: | |
| group: release-plz-${{ github.ref }} | |
| cancel-in-progress: false | |
| permissions: | |
| contents: write # create and update the release branch | |
| pull-requests: write # create and update the Release PR | |
| steps: | |
| - name: Generate release app token | |
| id: app-token | |
| if: vars.RELEASE_APP_ID != '' | |
| uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 #v3.2.0 | |
| with: | |
| app-id: ${{ vars.RELEASE_APP_ID }} | |
| private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} | |
| permission-contents: write | |
| permission-pull-requests: write | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| token: ${{ steps.app-token.outputs.token || secrets.GITHUB_TOKEN }} | |
| - name: Create or update Release PR | |
| id: release-plz-pr | |
| uses: release-plz/action@2eb1d8bcb770b4c48ccfaad919734b38b51958c9 #v0.5.131 | |
| with: | |
| command: release-pr | |
| version: "0.3.160" | |
| env: | |
| GITHUB_TOKEN: ${{ steps.app-token.outputs.token || secrets.GITHUB_TOKEN }} | |
| - name: Add zebrad version to Release PR title | |
| env: | |
| GH_TOKEN: ${{ steps.app-token.outputs.token || secrets.GITHUB_TOKEN }} | |
| RELEASE_PRS: ${{ steps.release-plz-pr.outputs.prs }} | |
| run: | | |
| set -euo pipefail | |
| ZEBRAD_RELEASE_PR="$(jq -c ' | |
| map(select(any(.releases[]?; .package_name == "zebrad"))) | |
| | first // empty | |
| ' <<< "${RELEASE_PRS:-[]}")" | |
| if [ -z "${ZEBRAD_RELEASE_PR}" ]; then | |
| echo "No zebrad release in release-plz output; keeping the generated title." | |
| exit 0 | |
| fi | |
| PR_NUMBER="$(jq -r '.number' <<< "${ZEBRAD_RELEASE_PR}")" | |
| VERSION="$(jq -r ' | |
| .releases[] | select(.package_name == "zebrad") | .version | |
| ' <<< "${ZEBRAD_RELEASE_PR}")" | |
| if [ -z "${PR_NUMBER}" ] || [ "${PR_NUMBER}" = "null" ] || [ -z "${VERSION}" ] || [ "${VERSION}" = "null" ]; then | |
| echo "::error::release-plz did not return both the PR number and zebrad version: ${ZEBRAD_RELEASE_PR}" | |
| exit 1 | |
| fi | |
| gh pr edit "${PR_NUMBER}" \ | |
| --repo "${GITHUB_REPOSITORY}" \ | |
| --title "chore: release v${VERSION}" | |
| release-target: | |
| name: Verify Release PR | |
| if: >- | |
| github.event_name == 'workflow_dispatch' || | |
| (github.event_name == 'pull_request' && | |
| github.event.pull_request.merged == true && | |
| startsWith(github.event.pull_request.head.ref, 'release-plz-') && | |
| contains(github.event.pull_request.labels.*.name, 'A-release')) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| permissions: | |
| contents: read # resolve the merged release commit and its parent | |
| pull-requests: read # validate the selected Release PR | |
| concurrency: | |
| group: release-target-${{ github.event.pull_request.number || inputs.release_pr_number }} | |
| cancel-in-progress: false | |
| outputs: | |
| base_sha: ${{ steps.release-target.outputs.base_sha }} | |
| operation: ${{ steps.release-target.outputs.operation }} | |
| pr_number: ${{ steps.release-target.outputs.pr_number }} | |
| target_sha: ${{ steps.release-target.outputs.target_sha }} | |
| steps: | |
| - name: Resolve immutable release target | |
| id: release-target | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 #v9.0.0 | |
| env: | |
| INPUT_OPERATION: ${{ inputs.operation }} | |
| INPUT_PR_NUMBER: ${{ inputs.release_pr_number }} | |
| with: | |
| script: | | |
| const requestedOperation = | |
| context.eventName === "workflow_dispatch" | |
| ? process.env.INPUT_OPERATION | |
| : "resume"; | |
| const rawPrNumber = | |
| context.eventName === "workflow_dispatch" | |
| ? process.env.INPUT_PR_NUMBER | |
| : String(context.payload.pull_request?.number ?? ""); | |
| const prNumber = Number(rawPrNumber); | |
| if ( | |
| !Number.isSafeInteger(prNumber) || | |
| prNumber <= 0 || | |
| !["check", "resume"].includes(requestedOperation) | |
| ) { | |
| throw new Error("operation and Release PR number are invalid"); | |
| } | |
| const result = await github.graphql( | |
| `query($owner: String!, $repo: String!, $number: Int!) { | |
| repository(owner: $owner, name: $repo) { | |
| pullRequest(number: $number) { | |
| baseRefName | |
| baseRefOid | |
| headRefName | |
| headRefOid | |
| labels(first: 100) { nodes { name } } | |
| mergeCommit { | |
| oid | |
| parents(first: 1) { nodes { oid } } | |
| } | |
| merged | |
| state | |
| } | |
| } | |
| }`, | |
| { ...context.repo, number: prNumber }, | |
| ); | |
| const pr = result.repository.pullRequest; | |
| const isReleasePr = | |
| pr?.headRefName.startsWith("release-plz-") && | |
| pr.labels.nodes.some(({ name }) => name === "A-release"); | |
| if (!isReleasePr || (pr.state !== "OPEN" && !pr.merged)) { | |
| throw new Error( | |
| `PR #${prNumber} is not a release-plz A-release PR`, | |
| ); | |
| } | |
| if (pr.baseRefName !== "main") { | |
| throw new Error(`PR #${prNumber} does not target main`); | |
| } | |
| if (requestedOperation === "check" && pr.state !== "OPEN") { | |
| throw new Error(`PR #${prNumber} is not open`); | |
| } | |
| if (requestedOperation === "resume" && !pr.merged) { | |
| throw new Error(`PR #${prNumber} is not merged`); | |
| } | |
| let baseSha = pr.baseRefOid; | |
| let targetSha = pr.headRefOid; | |
| if (requestedOperation === "resume") { | |
| baseSha = pr.mergeCommit?.parents.nodes[0]?.oid; | |
| targetSha = pr.mergeCommit?.oid; | |
| if (baseSha === undefined || targetSha === undefined) { | |
| throw new Error(`PR #${prNumber} has no merged release source`); | |
| } | |
| } | |
| core.setOutput("base_sha", baseSha); | |
| core.setOutput("operation", requestedOperation); | |
| core.setOutput("pr_number", String(prNumber)); | |
| core.setOutput("target_sha", targetSha); | |
| release-check: | |
| name: Check release without publishing | |
| needs: release-target | |
| if: needs.release-target.outputs.operation == 'check' | |
| runs-on: ubuntu-latest-xl | |
| timeout-minutes: 90 | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: release-check-${{ needs.release-target.outputs.pr_number }} | |
| cancel-in-progress: true | |
| steps: | |
| - name: Check out immutable release source | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1 | |
| with: | |
| fetch-depth: 0 | |
| path: release-source | |
| persist-credentials: false | |
| ref: ${{ needs.release-target.outputs.target_sha }} | |
| - name: Validate release changelogs | |
| id: changelogs | |
| continue-on-error: true | |
| working-directory: release-source | |
| env: | |
| BASE_SHA: ${{ needs.release-target.outputs.base_sha }} | |
| TARGET_SHA: ${{ needs.release-target.outputs.target_sha }} | |
| run: .github/scripts/validate-release-changelogs.sh "${BASE_SHA}" "${TARGET_SHA}" | |
| - name: Install Cargo 1.91 | |
| uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 #v1.17.0 | |
| with: | |
| toolchain: 1.91.0 | |
| cache-workspaces: release-source -> target | |
| cache-on-failure: true | |
| - name: Install libclang for librocksdb-sys bindgen | |
| run: | | |
| sudo timeout --kill-after=10s --foreground 180s apt-get \ | |
| -o Acquire::Retries=3 \ | |
| -o Acquire::http::Timeout=30 \ | |
| -o Acquire::https::Timeout=30 \ | |
| update | |
| sudo timeout --kill-after=10s --foreground 180s apt-get \ | |
| -o Acquire::Retries=3 \ | |
| -o Acquire::http::Timeout=30 \ | |
| -o Acquire::https::Timeout=30 \ | |
| install -y libclang-dev | |
| - name: Check desired release state | |
| id: cargo-release | |
| continue-on-error: true | |
| uses: ZcashFoundation/cargo-release@0083006dfd267ed560b4cf1aed347ef2f326162e #v0.1.0 | |
| with: | |
| phase: check | |
| source-directory: release-source | |
| base-sha: ${{ needs.release-target.outputs.base_sha }} | |
| target-sha: ${{ needs.release-target.outputs.target_sha }} | |
| config-path: release-source/.github/cargo-release.yml | |
| github-token: ${{ github.token }} | |
| - name: Summarize release check | |
| if: always() | |
| env: | |
| CARGO_RELEASE_RESULT: ${{ steps.cargo-release.outcome }} | |
| CHANGELOG_RESULT: ${{ steps.changelogs.outcome }} | |
| PLAN: ${{ steps.cargo-release.outputs.plan }} | |
| REPORT: ${{ steps.cargo-release.outputs.report }} | |
| run: | | |
| { | |
| echo "## Cargo Release check" | |
| echo "| Validation | Result |" | |
| echo "| --- | --- |" | |
| echo "| Changelogs | \`${CHANGELOG_RESULT}\` |" | |
| echo "| Cargo release | \`${CARGO_RELEASE_RESULT}\` |" | |
| if [[ -n "${PLAN}" ]]; then | |
| echo '### Plan' | |
| echo '```json' | |
| jq . <<< "${PLAN}" || printf '%s\n' "${PLAN}" | |
| echo '```' | |
| fi | |
| if [[ -n "${REPORT}" ]]; then | |
| echo '### Report' | |
| echo '```json' | |
| jq . <<< "${REPORT}" || printf '%s\n' "${REPORT}" | |
| echo '```' | |
| fi | |
| } >> "${GITHUB_STEP_SUMMARY}" | |
| - name: Require successful release checks | |
| if: always() | |
| env: | |
| CARGO_RELEASE_RESULT: ${{ steps.cargo-release.outcome }} | |
| CHANGELOG_RESULT: ${{ steps.changelogs.outcome }} | |
| run: | | |
| if [[ "${CHANGELOG_RESULT}" != "success" || "${CARGO_RELEASE_RESULT}" != "success" ]]; then | |
| echo "::error title=Release check failed::Review each failed validation step and the job summary." | |
| exit 1 | |
| fi | |
| release: | |
| name: Publish crates and GitHub Release | |
| needs: release-target | |
| if: needs.release-target.outputs.operation == 'resume' | |
| runs-on: ubuntu-latest-xl | |
| timeout-minutes: 120 | |
| permissions: | |
| contents: read # inspect release state; the app token performs finalization | |
| id-token: write # authenticate to crates.io through OIDC | |
| environment: release | |
| concurrency: | |
| group: release-publish | |
| cancel-in-progress: false | |
| steps: | |
| - name: Check out immutable release source | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1 | |
| with: | |
| fetch-depth: 0 | |
| path: release-source | |
| persist-credentials: false | |
| ref: ${{ needs.release-target.outputs.target_sha }} | |
| - name: Validate release changelogs | |
| working-directory: release-source | |
| env: | |
| BASE_SHA: ${{ needs.release-target.outputs.base_sha }} | |
| TARGET_SHA: ${{ needs.release-target.outputs.target_sha }} | |
| run: .github/scripts/validate-release-changelogs.sh "${BASE_SHA}" "${TARGET_SHA}" | |
| - name: Install Cargo 1.91 | |
| uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 #v1.17.0 | |
| with: | |
| toolchain: 1.91.0 | |
| cache-workspaces: release-source -> target | |
| cache-on-failure: true | |
| - name: Install libclang for librocksdb-sys bindgen | |
| run: | | |
| sudo timeout --kill-after=10s --foreground 180s apt-get \ | |
| -o Acquire::Retries=3 \ | |
| -o Acquire::http::Timeout=30 \ | |
| -o Acquire::https::Timeout=30 \ | |
| update | |
| sudo timeout --kill-after=10s --foreground 180s apt-get \ | |
| -o Acquire::Retries=3 \ | |
| -o Acquire::http::Timeout=30 \ | |
| -o Acquire::https::Timeout=30 \ | |
| install -y libclang-dev | |
| - name: Generate release app token | |
| id: app-token | |
| if: vars.RELEASE_APP_ID != '' | |
| uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 #v3.2.0 | |
| with: | |
| app-id: ${{ vars.RELEASE_APP_ID }} | |
| private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} | |
| permission-contents: write | |
| - name: Validate release app token | |
| if: steps.app-token.outputs.token == '' | |
| run: | | |
| echo "::error::Configure RELEASE_APP_ID and RELEASE_APP_PRIVATE_KEY. The app token is required before crate publication so finalization can trigger downstream release workflows." | |
| exit 1 | |
| - name: Authenticate to crates.io | |
| id: crates-io-auth | |
| uses: rust-lang/crates-io-auth-action@c6f97d42243bad5fab37ca0427f495c86d5b1a18 #v1.0.5 | |
| - name: Publish and verify missing crates | |
| id: cargo-release-publish | |
| uses: ZcashFoundation/cargo-release@0083006dfd267ed560b4cf1aed347ef2f326162e #v0.1.0 | |
| with: | |
| phase: publish | |
| source-directory: release-source | |
| base-sha: ${{ needs.release-target.outputs.base_sha }} | |
| target-sha: ${{ needs.release-target.outputs.target_sha }} | |
| config-path: release-source/.github/cargo-release.yml | |
| github-token: ${{ github.token }} | |
| env: | |
| CARGO_REGISTRY_TOKEN: ${{ steps.crates-io-auth.outputs.token }} | |
| - name: Read zebrad release version | |
| id: zebrad-release | |
| env: | |
| PLAN: ${{ steps.cargo-release-publish.outputs.plan }} | |
| run: | | |
| version="$(jq -r ' | |
| [.packages[] | select(.name == "zebrad") | .version] | |
| | first // empty | |
| ' <<< "${PLAN}")" | |
| if [[ -z "${version}" ]]; then | |
| echo "required=false" >> "${GITHUB_OUTPUT}" | |
| else | |
| echo "required=true" >> "${GITHUB_OUTPUT}" | |
| echo "version=${version}" >> "${GITHUB_OUTPUT}" | |
| fi | |
| - name: Verify zebrad installs from crates.io | |
| if: steps.zebrad-release.outputs.required == 'true' | |
| timeout-minutes: 30 | |
| env: | |
| VERSION: ${{ steps.zebrad-release.outputs.version }} | |
| run: | | |
| set -euo pipefail | |
| for attempt in 1 2 3; do | |
| if cargo +1.91.0 install --locked --force --version "=${VERSION}" zebrad; then | |
| ~/.cargo/bin/zebrad --version | |
| exit 0 | |
| fi | |
| if [ "${attempt}" -lt 3 ]; then | |
| sleep "$((attempt * 30))" | |
| fi | |
| done | |
| echo "::error::zebrad ${VERSION} did not install from crates.io after 3 attempts." | |
| exit 1 | |
| - name: Create missing tags and GitHub Release | |
| id: cargo-release-finalize | |
| uses: ZcashFoundation/cargo-release@0083006dfd267ed560b4cf1aed347ef2f326162e #v0.1.0 | |
| with: | |
| phase: finalize | |
| source-directory: release-source | |
| base-sha: ${{ needs.release-target.outputs.base_sha }} | |
| target-sha: ${{ needs.release-target.outputs.target_sha }} | |
| config-path: release-source/.github/cargo-release.yml | |
| github-token: ${{ steps.app-token.outputs.token }} | |
| - name: Summarize release | |
| if: always() | |
| env: | |
| FINALIZE_REPORT: ${{ steps.cargo-release-finalize.outputs.report }} | |
| PLAN: ${{ steps.cargo-release-publish.outputs.plan }} | |
| PUBLISH_REPORT: ${{ steps.cargo-release-publish.outputs.report }} | |
| run: | | |
| { | |
| echo "## Cargo Release" | |
| for entry in \ | |
| "Plan:${PLAN}" \ | |
| "Publication report:${PUBLISH_REPORT}" \ | |
| "Finalization report:${FINALIZE_REPORT}" | |
| do | |
| label="${entry%%:*}" | |
| value="${entry#*:}" | |
| if [[ -n "${value}" ]]; then | |
| echo "### ${label}" | |
| echo '```json' | |
| jq . <<< "${value}" || printf '%s\n' "${value}" | |
| echo '```' | |
| fi | |
| done | |
| } >> "${GITHUB_STEP_SUMMARY}" |