Skip to content

Pre-release

Pre-release #38

Workflow file for this run

name: Pre-release
on:
workflow_dispatch:
inputs:
version:
description: Release version, for example 0.3.0
required: true
type: string
dist_tag:
description: npm dist-tag (latest / next)
required: false
default: latest
type: choice
options:
- latest
- next
workflow_run:
workflows:
- CI
types:
- completed
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true'
concurrency:
group: pre-release-${{ github.event_name }}-${{ github.event.workflow_run.head_sha || github.ref }}
cancel-in-progress: false
jobs:
prepare-release-pr:
name: Prepare release PR
if: ${{ github.event_name == 'workflow_dispatch' }}
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
issues: write
steps:
- name: Validate release inputs
env:
VERSION: ${{ inputs.version }}
run: |
if ! [[ "${VERSION}" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then
echo "Invalid release version: ${VERSION}"
exit 1
fi
- name: Checkout main
uses: actions/checkout@v5
with:
ref: main
fetch-depth: 0
token: ${{ secrets.RELEASE_PR_TOKEN || github.token }}
- name: Setup Node
uses: actions/setup-node@v5
with:
node-version: 24.3.0
cache: npm
- name: Pin npm version
run: npm i -g npm@11.4.2
- name: Install dependencies
run: npm ci
- name: Compose release metadata
env:
VERSION: ${{ inputs.version }}
run: |
mkdir -p .tmp
npm run -s release:draft-notes:json -- --version "${VERSION}" > .tmp/release-metadata.json
- name: Finalize changelogs and versions
run: npm run -s release:finalize-unreleased -- .tmp/release-metadata.json
- name: Refresh package lockfile
run: npm install --package-lock-only --ignore-scripts
- name: Write release plan
env:
VERSION: ${{ inputs.version }}
DIST_TAG: ${{ inputs.dist_tag }}
run: |
npm run -s release:write-plan -- \
.tmp/release-metadata.json \
"${VERSION}" \
"${DIST_TAG}" \
.github/release-plan.json \
.tmp/release-pr-body.md
- name: Format release PR files
run: |
npx prettier --write \
CHANGELOG.md \
CHANGELOG.zh-CN.md \
.github/release-plan.json \
package-lock.json \
projects/*/CHANGELOG.md \
projects/*/CHANGELOG.zh-CN.md \
projects/*/package.json
- name: Version consistency
run: npm run version:check
- name: Create or update release PR
env:
GH_TOKEN: ${{ secrets.RELEASE_PR_TOKEN || github.token }}
VERSION: ${{ inputs.version }}
run: |
BRANCH="ci/release/v${VERSION}"
TITLE="ci(release): prepare v${VERSION}"
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git switch -C "${BRANCH}"
git add \
CHANGELOG.md \
CHANGELOG.zh-CN.md \
package-lock.json \
.github/release-plan.json \
projects/*/CHANGELOG.md \
projects/*/CHANGELOG.zh-CN.md \
projects/*/package.json
if git diff --cached --quiet; then
echo "No release PR changes detected."
exit 0
fi
git commit -m "${TITLE}"
git push --set-upstream origin "${BRANCH}" --force-with-lease
gh label create auto-release \
--color 0E8A16 \
--description "Automated release preparation PR" || true
gh label create release-pr \
--color 5319E7 \
--description "Release preparation PR" || true
PR_URL="$(gh pr list --base main --head "${BRANCH}" --json url --jq '.[0].url')"
if [[ -z "${PR_URL}" ]]; then
PR_URL="$(gh pr create \
--title "${TITLE}" \
--body-file .tmp/release-pr-body.md \
--base main \
--head "${BRANCH}" \
--label auto-release \
--label release-pr)"
echo "Created release PR: ${PR_URL}"
else
gh pr edit "${PR_URL}" \
--title "${TITLE}" \
--body-file .tmp/release-pr-body.md \
--add-label auto-release \
--add-label release-pr
echo "Updated release PR: ${PR_URL}"
fi
publish-after-merge:
name: Publish merged release
if: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'push' && github.event.workflow_run.head_branch == 'main' }}
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout release commit
uses: actions/checkout@v5
with:
ref: ${{ github.event.workflow_run.head_sha }}
fetch-depth: 0
- name: Resolve merged release plan
id: plan
run: |
git fetch --tags --force
PLAN_FILE=".github/release-plan.json"
if ! git diff-tree --no-commit-id --name-only -r -m HEAD | grep -Fxq "${PLAN_FILE}"; then
echo "skip=true" >> "$GITHUB_OUTPUT"
echo "No merged release plan detected."
exit 0
fi
if [[ ! -f "${PLAN_FILE}" ]]; then
echo "Release plan ${PLAN_FILE} is missing."
exit 1
fi
VERSION="$(node -e "const fs=require('fs'); const plan=JSON.parse(fs.readFileSync(process.argv[1], 'utf8')); console.log(plan.version || '');" "${PLAN_FILE}")"
DIST_TAG="$(node -e "const fs=require('fs'); const plan=JSON.parse(fs.readFileSync(process.argv[1], 'utf8')); console.log(plan.distTag || 'latest');" "${PLAN_FILE}")"
if [[ -z "${VERSION}" ]]; then
echo "Release plan ${PLAN_FILE} is missing version."
exit 1
fi
if git rev-parse "v${VERSION}" >/dev/null 2>&1; then
echo "skip=true" >> "$GITHUB_OUTPUT"
echo "Release tag v${VERSION} already exists; skipping publish."
exit 0
fi
echo "skip=false" >> "$GITHUB_OUTPUT"
echo "plan_path=${PLAN_FILE}" >> "$GITHUB_OUTPUT"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "dist_tag=${DIST_TAG}" >> "$GITHUB_OUTPUT"
- name: Setup Node
if: ${{ steps.plan.outputs.skip != 'true' }}
uses: actions/setup-node@v5
with:
node-version: 24.3.0
cache: npm
- name: Pin npm version
if: ${{ steps.plan.outputs.skip != 'true' }}
run: npm i -g npm@11.4.2
- name: Install dependencies
if: ${{ steps.plan.outputs.skip != 'true' }}
run: npm ci
- name: Build libraries
if: ${{ steps.plan.outputs.skip != 'true' }}
run: npm run build:libs
- name: Configure npm auth
if: ${{ steps.plan.outputs.skip != 'true' }}
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
if [[ -z "${NODE_AUTH_TOKEN}" ]]; then
echo "NPM_TOKEN is not configured."
exit 1
fi
cat > ~/.npmrc <<EOF
//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}
EOF
- name: Publish selected packages
if: ${{ steps.plan.outputs.skip != 'true' }}
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
mkdir -p .tmp
node scripts/release/publish-selected-packages.mjs \
"${{ steps.plan.outputs.plan_path }}" \
.tmp/publish-result.json \
"${{ steps.plan.outputs.dist_tag }}"
- name: Extract release body
if: ${{ steps.plan.outputs.skip != 'true' }}
run: |
npm run -s release:body -- \
"${{ steps.plan.outputs.version }}" \
CHANGELOG.md > .tmp/release-body.md
- name: Create release tag
if: ${{ steps.plan.outputs.skip != 'true' }}
env:
VERSION: ${{ steps.plan.outputs.version }}
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git tag -a "v${VERSION}" -m "Release v${VERSION}"
git push origin "v${VERSION}"
- name: Upsert GitHub Release
if: ${{ steps.plan.outputs.skip != 'true' }}
uses: actions/github-script@v7
env:
VERSION: ${{ steps.plan.outputs.version }}
DIST_TAG: ${{ steps.plan.outputs.dist_tag }}
RELEASE_SHA: ${{ github.event.workflow_run.head_sha }}
with:
script: |
const fs = require('fs');
const version = process.env.VERSION;
const tag = `v${version}`;
const distTag = process.env.DIST_TAG || 'latest';
const body = fs.readFileSync('.tmp/release-body.md', 'utf8');
const releases = await github.paginate(github.rest.repos.listReleases, {
owner: context.repo.owner,
repo: context.repo.repo,
per_page: 100,
});
const existing = releases.find((release) => release.tag_name === tag);
const payload = {
owner: context.repo.owner,
repo: context.repo.repo,
tag_name: tag,
target_commitish: process.env.RELEASE_SHA,
name: tag,
body,
draft: false,
prerelease: distTag !== 'latest',
make_latest: distTag === 'latest' ? 'true' : 'false',
};
if (existing) {
await github.rest.repos.updateRelease({
...payload,
release_id: existing.id,
});
core.info(`Updated GitHub Release ${tag}.`);
} else {
await github.rest.repos.createRelease(payload);
core.info(`Created GitHub Release ${tag}.`);
}