Skip to content

chore: release v6.3.0 #311

chore: release v6.3.0

chore: release v6.3.0 #311

Workflow file for this run

# 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}"