Pre-release #39
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: 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}.`); | |
| } |