Skip to content

Stable release to npm latest #7

Stable release to npm latest

Stable release to npm latest #7

Workflow file for this run

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 -e '
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}`);
')"
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: Check bundled MCP server
id: mcp
run: pnpm mcp:check
- 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 "| MCP server check | ${{ steps.mcp.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"