ci(release): stabilize workflow headings #4
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: Release | ||
| run-name: Stable release to npm latest | ||
| on: | ||
| workflow_dispatch: | ||
| inputs: | ||
| confirm_stable_release: | ||
| description: Publish gate. | ||
| required: true | ||
| type: choice | ||
| default: locked | ||
| options: | ||
| - locked | ||
| - publish-stable | ||
| version_bump: | ||
| description: Stable version strategy. Patch is the normal OSS release default. | ||
| required: true | ||
| type: choice | ||
| default: patch | ||
| options: | ||
| - patch | ||
| - minor | ||
| - major | ||
| - manual | ||
| stable_version: | ||
| description: Manual stable version, used only when version_bump is manual. | ||
| required: false | ||
| default: 0.0.1 | ||
| permissions: | ||
| contents: write | ||
| id-token: write | ||
| env: | ||
| FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true | ||
| concurrency: | ||
| group: stable-release | ||
| cancel-in-progress: false | ||
| jobs: | ||
| guard: | ||
| name: 1. Plan release | ||
| runs-on: ubuntu-latest | ||
| outputs: | ||
| stable_version: ${{ steps.guard.outputs.stable_version }} | ||
| package_name: ${{ steps.guard.outputs.package_name }} | ||
| package_version: ${{ steps.guard.outputs.package_version }} | ||
| current_latest: ${{ steps.guard.outputs.current_latest }} | ||
| steps: | ||
| - name: Checkout main | ||
| uses: actions/checkout@v4 | ||
| - name: Setup Node 24 | ||
| uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: 24 | ||
| - name: Validate release inputs | ||
| id: guard | ||
| env: | ||
| CONFIRM_STABLE_RELEASE: ${{ inputs.confirm_stable_release }} | ||
| VERSION_BUMP: ${{ inputs.version_bump }} | ||
| STABLE_VERSION_INPUT: ${{ inputs.stable_version }} | ||
| run: | | ||
| if [ "${{ github.ref_name }}" != "main" ]; then | ||
| echo "Stable releases must run from main. Current ref: ${{ github.ref_name }}" >&2 | ||
| exit 1 | ||
| fi | ||
| if [ "$CONFIRM_STABLE_RELEASE" != "publish-stable" ]; then | ||
| echo "Stable release is locked. Use confirm_stable_release=publish-stable to publish." >&2 | ||
| exit 1 | ||
| fi | ||
| PACKAGE_NAME="$(node -p "require('./package.json').name")" | ||
| CURRENT_LATEST="$(npm view "$PACKAGE_NAME@latest" version 2>/dev/null || true)" | ||
| if [ "$VERSION_BUMP" = "manual" ]; then | ||
| STABLE_VERSION="$STABLE_VERSION_INPUT" | ||
| else | ||
| STABLE_VERSION="$( | ||
| CURRENT_LATEST="$CURRENT_LATEST" VERSION_BUMP="$VERSION_BUMP" node - <<'NODE' | ||
| const currentLatest = process.env.CURRENT_LATEST || ''; | ||
| const bump = process.env.VERSION_BUMP || 'patch'; | ||
| const match = currentLatest.match(/^(\d+)\.(\d+)\.(\d+)(?:-.+)?$/); | ||
| let major = match ? Number(match[1]) : 0; | ||
| let minor = match ? Number(match[2]) : 0; | ||
| let patch = match ? Number(match[3]) : 0; | ||
| const hasLatest = Boolean(match); | ||
| const isPrerelease = /-/.test(currentLatest); | ||
| if (!hasLatest) { | ||
| patch = 1; | ||
| } else if (isPrerelease) { | ||
| // Promote the current prerelease base to stable, for example 0.0.1-next.18 -> 0.0.1. | ||
| } else if (bump === 'major') { | ||
| major += 1; | ||
| minor = 0; | ||
| patch = 0; | ||
| } else if (bump === 'minor') { | ||
| minor += 1; | ||
| patch = 0; | ||
| } else { | ||
| patch += 1; | ||
| } | ||
| process.stdout.write(`${major}.${minor}.${patch}`); | ||
| NODE | ||
| )" | ||
| fi | ||
| if ! printf '%s' "$STABLE_VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then | ||
| echo "stable_version must be a stable semver version like 0.0.1." >&2 | ||
| exit 1 | ||
| fi | ||
| echo "stable_version=$STABLE_VERSION" >> "$GITHUB_OUTPUT" | ||
| echo "package_name=$PACKAGE_NAME" >> "$GITHUB_OUTPUT" | ||
| echo "package_version=$PACKAGE_NAME@$STABLE_VERSION" >> "$GITHUB_OUTPUT" | ||
| echo "current_latest=${CURRENT_LATEST:-none}" >> "$GITHUB_OUTPUT" | ||
| { | ||
| echo "## Release $STABLE_VERSION" | ||
| echo | ||
| echo "| Field | Value |" | ||
| echo "| --- | --- |" | ||
| echo "| Package | $PACKAGE_NAME |" | ||
| echo "| Current npm latest | ${CURRENT_LATEST:-none} |" | ||
| echo "| Version strategy | $VERSION_BUMP |" | ||
| echo "| Release version | $STABLE_VERSION |" | ||
| echo "| Dist tag | latest |" | ||
| } >> "$GITHUB_STEP_SUMMARY" | ||
| verify: | ||
| name: 2. Verify release | ||
| needs: guard | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Checkout main | ||
| uses: actions/checkout@v4 | ||
| - name: Setup pnpm | ||
| uses: pnpm/action-setup@v4 | ||
| with: | ||
| version: 10.30.2 | ||
| - name: Setup Node 24 | ||
| uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: 24 | ||
| cache: pnpm | ||
| - name: Install dependencies | ||
| id: install | ||
| run: pnpm install --frozen-lockfile | ||
| - name: Typecheck | ||
| id: typecheck | ||
| run: pnpm typecheck | ||
| - name: Tests | ||
| id: test | ||
| run: pnpm test | ||
| - name: Build package | ||
| id: build | ||
| run: pnpm build | ||
| - name: Build docs | ||
| id: docs | ||
| run: pnpm docs:build | ||
| - name: README check | ||
| id: readme | ||
| run: pnpm readme:check | ||
| - name: Size report | ||
| id: size | ||
| run: pnpm size | ||
| - name: Pack dry run | ||
| id: pack | ||
| run: pnpm pack --dry-run | ||
| - name: Verify summary | ||
| if: always() | ||
| run: | | ||
| { | ||
| echo "## Stable release verify" | ||
| echo | ||
| echo "Release ${{ needs.guard.outputs.package_version }}" | ||
| echo | ||
| echo "| Stage | Result |" | ||
| echo "| --- | --- |" | ||
| echo "| Install | ${{ steps.install.outcome }} |" | ||
| echo "| Typecheck | ${{ steps.typecheck.outcome }} |" | ||
| echo "| Tests | ${{ steps.test.outcome }} |" | ||
| echo "| Package build | ${{ steps.build.outcome }} |" | ||
| echo "| Docs build | ${{ steps.docs.outcome }} |" | ||
| echo "| README check | ${{ steps.readme.outcome }} |" | ||
| echo "| Size report | ${{ steps.size.outcome }} |" | ||
| echo "| Pack dry run | ${{ steps.pack.outcome }} |" | ||
| } >> "$GITHUB_STEP_SUMMARY" | ||
| publish: | ||
| name: 3. Publish npm latest | ||
| needs: [guard, verify] | ||
| runs-on: ubuntu-latest | ||
| outputs: | ||
| package_version: ${{ steps.version.outputs.package_version }} | ||
| steps: | ||
| - name: Checkout main | ||
| uses: actions/checkout@v4 | ||
| - name: Setup pnpm | ||
| uses: pnpm/action-setup@v4 | ||
| with: | ||
| version: 10.30.2 | ||
| - name: Setup Node 24 | ||
| uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: 24 | ||
| cache: pnpm | ||
| registry-url: https://registry.npmjs.org | ||
| - name: Install dependencies | ||
| run: pnpm install --frozen-lockfile | ||
| - name: Set stable package version | ||
| id: version | ||
| env: | ||
| STABLE_VERSION: ${{ needs.guard.outputs.stable_version }} | ||
| run: | | ||
| node -e "const fs = require('node:fs'); const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8')); pkg.version = process.env.STABLE_VERSION; pkg.publishConfig = { access: 'public' }; fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n');" | ||
| PACKAGE_NAME=$(node -p "require('./package.json').name") | ||
| PACKAGE_VERSION=$(node -p "require('./package.json').version") | ||
| echo "package_version=$PACKAGE_NAME@$PACKAGE_VERSION" >> "$GITHUB_OUTPUT" | ||
| - name: Fail if version already exists | ||
| env: | ||
| PACKAGE_NAME: ${{ steps.version.outputs.package_version }} | ||
| run: | | ||
| if npm view "$PACKAGE_NAME" version >/dev/null 2>&1; then | ||
| echo "$PACKAGE_NAME is already published." >&2 | ||
| exit 1 | ||
| fi | ||
| - name: Build final package | ||
| run: pnpm build | ||
| - name: Publish latest | ||
| run: pnpm publish --tag latest --access public --no-git-checks | ||
| env: | ||
| NPM_TOKEN: ${{ secrets.NPM_TOKEN }} | ||
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | ||
| github-release: | ||
| name: 4. Create GitHub release | ||
| needs: [guard, publish] | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Checkout main | ||
| uses: actions/checkout@v4 | ||
| - name: Create GitHub release | ||
| env: | ||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| STABLE_VERSION: ${{ needs.guard.outputs.stable_version }} | ||
| PACKAGE_VERSION: ${{ needs.publish.outputs.package_version }} | ||
| run: | | ||
| cat > release-notes.md <<EOF | ||
| Stable npm release for $PACKAGE_VERSION. | ||
| Install: | ||
| \`\`\`sh | ||
| npm install @crup/react-timer-hook@$STABLE_VERSION | ||
| \`\`\` | ||
| Docs: https://crup.github.io/react-timer-hook/ | ||
| EOF | ||
| gh release create "v$STABLE_VERSION" \ | ||
| --title "v$STABLE_VERSION" \ | ||
| --notes-file release-notes.md \ | ||
| --target "${{ github.sha }}" | ||
| - name: Release summary | ||
| run: | | ||
| { | ||
| echo "## Stable release published" | ||
| echo | ||
| echo "| Field | Value |" | ||
| echo "| --- | --- |" | ||
| echo "| Package | ${{ needs.publish.outputs.package_version }} |" | ||
| echo "| Dist tag | latest |" | ||
| echo "| GitHub release | v${{ needs.guard.outputs.stable_version }} |" | ||
| } >> "$GITHUB_STEP_SUMMARY" | ||