Files
bookorbit/.github/workflows/release.yml
T
neon d7556011df build(deps): upgrade Node.js to 26 and pnpm to 11
Update package manager configuration to pnpm 11.22.0 and Node.js requirement to Node 26 across Dockerfile, GitHub Actions workflows, root package.json, and workspace configurations.

Upgrade dependencies, update GitHub Action versions, and remove deprecated scorecard workflow.
2026-08-16 00:41:56 +00:00

410 lines
15 KiB
YAML

name: Release
run-name: >-
${{ github.event_name == 'workflow_dispatch' && format('Release - {0}', github.ref_name) || format('Release checks - PR #{0}', github.event.pull_request.number) }}
on:
pull_request:
branches: [main]
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.event_name == 'workflow_dispatch' && 'release' || format('pr-{0}', github.event.pull_request.number) }}
cancel-in-progress: ${{ github.event_name != 'workflow_dispatch' }}
permissions: read-all
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
NODE_VERSION: "26"
PNPM_VERSION: "11"
jobs:
commitlint:
name: Lint PR Conventions
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
issues: read
pull-requests: read
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.base.sha }}
- name: Classify Crowdin pull request
id: crowdin
shell: bash
env:
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
BASE_REPO: ${{ github.repository }}
HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }}
HEAD_BRANCH: ${{ github.head_ref }}
run: |
set -euo pipefail
args=("$PR_BASE_SHA" "$PR_HEAD_SHA" "$BASE_REPO" "$HEAD_REPO" "$HEAD_BRANCH")
if git cat-file -e "$PR_BASE_SHA:scripts/classify-crowdin-pr.sh" 2>/dev/null; then
is_crowdin="$(git show "$PR_BASE_SHA:scripts/classify-crowdin-pr.sh" | bash -s -- "${args[@]}")"
else
is_crowdin="$(git show "$PR_HEAD_SHA:scripts/classify-crowdin-pr.sh" | bash -s -- "${args[@]}")"
fi
echo "is_crowdin=$is_crowdin" >> "$GITHUB_OUTPUT"
- name: Checkout pull request
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- name: Set up Node.js and pnpm
uses: ./.github/actions/setup-node-pnpm
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
- name: Lint PR title
shell: bash
env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: printf '%s\n' "$PR_TITLE" | pnpm exec commitlint --config commitlint.config.cjs
- name: Lint PR commit headers
shell: bash
env:
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: pnpm exec commitlint --config commitlint.config.cjs --from "$PR_BASE_SHA" --to "$PR_HEAD_SHA" --verbose
- name: Validate referenced issues exist
if: github.event.pull_request.user.login != 'dependabot[bot]' && steps.crowdin.outputs.is_crowdin != 'true'
shell: bash
env:
GH_REPO: ${{ github.repository }}
GH_TOKEN: ${{ github.token }}
PR_BRANCH: ${{ github.head_ref }}
PR_BODY: ${{ github.event.pull_request.body }}
run: |
set -euo pipefail
validate_issue_exists() {
local issue_repo="$1"
local issue_number="$2"
local reference_context="$3"
local response_file
local http_code
local api_url
local state
local reference_label
response_file="$(mktemp)"
api_url="https://api.github.com/repos/${issue_repo}/issues/${issue_number}"
http_code="$(curl -sS -o "${response_file}" -w "%{http_code}" \
-H "Authorization: Bearer ${GH_TOKEN}" \
-H "Accept: application/vnd.github+json" \
"${api_url}")"
if [[ "${issue_repo}" == "${GH_REPO}" ]]; then
reference_label="#${issue_number}"
else
reference_label="${issue_repo}#${issue_number}"
fi
if [[ "${http_code}" != "200" ]]; then
echo "::error::${reference_context} references ${reference_label}, but it does not exist or is not accessible."
rm -f "${response_file}"
return 1
fi
if jq -e '.pull_request != null' "${response_file}" > /dev/null; then
echo "::error::${reference_context} references ${reference_label}, but that number is a pull request, not an issue."
rm -f "${response_file}"
return 1
fi
state="$(jq -r '.state // "unknown"' "${response_file}")"
echo "${reference_context}: issue ${reference_label} exists (state=${state})."
rm -f "${response_file}"
}
if [[ "${PR_BRANCH}" =~ ^BO-([0-9]+)- ]]; then
branch_issue_number="${BASH_REMATCH[1]}"
else
echo "::error::Unable to extract issue number from branch '${PR_BRANCH}'."
exit 1
fi
validate_issue_exists "${GH_REPO}" "${branch_issue_number}" "Branch name"
closing_keyword_pattern='(close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved)'
issue_reference_pattern="(^|[^[:alnum:]_])${closing_keyword_pattern}([[:space:]]*:[[:space:]]*|[[:space:]]+)([A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+)?#[1-9][0-9]*"
issue_matches="$(printf '%s\n' "${PR_BODY}" | grep -Eio "${issue_reference_pattern}" || true)"
if [[ -z "${issue_matches}" ]]; then
echo "::error::PR description must include at least one GitHub closing-keyword issue reference (for example: 'Closes #123', 'Fixes: #123', or 'RESOLVED owner/repo#123')."
exit 1
fi
issue_refs="$(printf '%s\n' "${issue_matches}" \
| tr '[:upper:]' '[:lower:]' \
| sed -E 's/^[^a-z0-9_]*//' \
| sed -E "s/^${closing_keyword_pattern}([[:space:]]*:[[:space:]]*|[[:space:]]+)//" \
| sort -u)"
while IFS= read -r issue_ref; do
[[ -z "${issue_ref}" ]] && continue
if [[ "${issue_ref}" =~ ^([a-z0-9_.-]+/[a-z0-9_.-]+)#([1-9][0-9]*)$ ]]; then
issue_repo="${BASH_REMATCH[1]}"
issue_number="${BASH_REMATCH[2]}"
elif [[ "${issue_ref}" =~ ^#([1-9][0-9]*)$ ]]; then
issue_repo="${GH_REPO}"
issue_number="${BASH_REMATCH[1]}"
else
echo "::error::Unable to parse issue reference '${issue_ref}' in PR description."
exit 1
fi
validate_issue_exists "${issue_repo}" "${issue_number}" "PR description"
done <<< "${issue_refs}"
release-dry-run:
name: Release Dry Run
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: write
issues: write
pull-requests: write
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- name: Set up Node.js and pnpm
uses: ./.github/actions/setup-node-pnpm
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
- name: Semantic release dry run
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: pnpm exec semantic-release --dry-run --no-ci
release:
name: Publish Release
if: github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
actions: read
contents: write
issues: write
pull-requests: write
packages: read
outputs:
published: ${{ steps.semantic.outputs.new_release_published }}
version: ${{ steps.semantic.outputs.new_release_version }}
tag: ${{ steps.semantic.outputs.new_release_git_tag }}
steps:
- name: Guard against non-main branch
shell: bash
env:
GH_REF: ${{ github.ref }}
run: |
if [[ "$GH_REF" != "refs/heads/main" ]]; then
echo "::error::Release workflow must be dispatched from main. Current ref: $GH_REF"
exit 1
fi
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- name: Verify CI success for current SHA
shell: bash
env:
GH_TOKEN: ${{ github.token }}
COMMIT_SHA: ${{ github.sha }}
GH_REPO: ${{ github.repository }}
run: |
set -euo pipefail
echo "Checking CI status for commit ${COMMIT_SHA}..."
success_count=$(gh api --paginate \
"repos/${GH_REPO}/actions/workflows/ci.yml/runs?head_sha=${COMMIT_SHA}&status=completed&event=push&branch=main" \
--jq '[.workflow_runs[] | select(.conclusion == "success")] | length' \
| awk '{sum += $1} END {print sum}')
if [[ "${success_count:-0}" -lt 1 ]]; then
echo "::error::No successful CI run found for commit ${COMMIT_SHA}. Refusing to release."
exit 1
fi
echo "CI verification passed: ${success_count} successful run(s) found."
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
- name: Log in to GHCR
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Verify SHA image exists in GHCR
shell: bash
env:
COMMIT_SHA: ${{ github.sha }}
IMAGE_NAME: ${{ github.repository }}
run: |
set -euo pipefail
SHORT_SHA="${COMMIT_SHA:0:12}"
SOURCE_TAG="ghcr.io/${IMAGE_NAME}:sha-${SHORT_SHA}"
if ! docker buildx imagetools inspect "${SOURCE_TAG}" > /dev/null 2>&1; then
echo "::error::Multi-arch image sha-${SHORT_SHA} not found in GHCR. Wait for the Container Image workflow to complete for this commit before releasing."
exit 1
fi
echo "Image sha-${SHORT_SHA} verified in GHCR."
- name: Set up Node.js and pnpm
uses: ./.github/actions/setup-node-pnpm
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
- name: Run semantic-release
id: semantic
shell: bash
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
BEFORE_TAG=$(git tag --sort=-v:refname | head -1 || true)
pnpm exec semantic-release
git fetch --tags --quiet
AFTER_TAG=$(git tag --sort=-v:refname | head -1 || true)
if [[ -n "$AFTER_TAG" && "$AFTER_TAG" != "$BEFORE_TAG" ]]; then
VERSION="${AFTER_TAG#v}"
echo "new_release_published=true" >> "$GITHUB_OUTPUT"
echo "new_release_version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "new_release_git_tag=${AFTER_TAG}" >> "$GITHUB_OUTPUT"
else
echo "new_release_published=false" >> "$GITHUB_OUTPUT"
fi
- name: Release summary
if: steps.semantic.outputs.new_release_published == 'true'
shell: bash
env:
VERSION: ${{ steps.semantic.outputs.new_release_version }}
TAG: ${{ steps.semantic.outputs.new_release_git_tag }}
run: |
echo "## Release Published" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "- **Version:** ${VERSION}" >> "$GITHUB_STEP_SUMMARY"
echo "- **Tag:** ${TAG}" >> "$GITHUB_STEP_SUMMARY"
docker-promote:
name: Promote Docker Image
needs: [release]
if: needs.release.outputs.published == 'true'
runs-on: ubuntu-latest
timeout-minutes: 60
permissions:
contents: write
packages: write
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
steps:
- name: Checkout release tag
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ needs.release.outputs.tag }}
- name: Set up QEMU
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
- name: Log in to GHCR
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push versioned image
id: build
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with:
context: .
file: ./Dockerfile
push: true
platforms: linux/amd64,linux/arm64
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.release.outputs.version }}
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
build-args: APP_VERSION=v${{ needs.release.outputs.version }}
cache-from: |
type=gha,scope=container-image-yml-amd64
type=gha,scope=container-image-yml-arm64
- name: Enrich release notes
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
TAG: ${{ needs.release.outputs.tag }}
VERSION: ${{ needs.release.outputs.version }}
SHA: ${{ github.sha }}
IMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
DIGEST: ${{ steps.build.outputs.digest }}
run: |
set -euo pipefail
BODY_FILE="$(mktemp)"
gh release view "${TAG}" --repo "${GH_REPO}" --json body --jq '.body' > "${BODY_FILE}"
if grep -q "<!-- release-metadata -->" "${BODY_FILE}"; then
echo "Release metadata already present; skipping."
exit 0
fi
PREV_TAG=$(gh release list \
--repo "${GH_REPO}" \
--exclude-drafts \
--exclude-pre-releases \
--limit 100 \
--json tagName \
--jq '.[].tagName' \
| awk -v current="${TAG}" '$0 != current {print; exit}')
{
echo ""
echo "<!-- release-metadata -->"
echo ""
if [[ -n "${PREV_TAG}" ]]; then
echo "[Full diff](https://github.com/${GH_REPO}/compare/${PREV_TAG}...${TAG})"
echo ""
fi
} >> "${BODY_FILE}"
gh release edit "${TAG}" --repo "${GH_REPO}" --notes-file "${BODY_FILE}"
- name: Docker publish summary
shell: bash
env:
VERSION: ${{ needs.release.outputs.version }}
SHA: ${{ github.sha }}
run: |
echo "## Docker Image Published" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "- **Version:** \`${VERSION}\`" >> "$GITHUB_STEP_SUMMARY"
echo "- **Tags:** \`${VERSION}\`, \`latest\` (linux/amd64, linux/arm64)" >> "$GITHUB_STEP_SUMMARY"
echo "- **APP_VERSION baked:** \`v${VERSION}\`" >> "$GITHUB_STEP_SUMMARY"