From 8d56c0c5c8c451fc85d420b560a4fc87c7af67a3 Mon Sep 17 00:00:00 2001 From: Rajender Joshi <2614954+crup@users.noreply.github.com> Date: Tue, 7 Apr 2026 22:22:47 +0530 Subject: [PATCH] ci(release): refine stable release workflow --- .github/workflows/release.yml | 108 +++++++++++++++++--- README.md | 8 +- docs-site/docs/getting-started.mdx | 8 +- docs-site/docs/index.mdx | 4 +- docs-site/docs/project/release-channels.mdx | 51 ++++++--- docs-site/static/llms-full.txt | 4 +- docs-site/static/llms.txt | 2 +- package.json | 3 +- scripts/check-readme.mjs | 2 +- 9 files changed, 143 insertions(+), 47 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ed88903..5cb6568 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,16 +1,31 @@ name: Release +run-name: Release ${{ inputs.version_bump }} to npm latest on: workflow_dispatch: inputs: confirm_stable_release: - description: Type "publish-stable" to publish to npm latest. + description: Publish gate. required: true - default: alpha-only - stable_version: - description: Stable version to publish. First stable should be 0.0.1. + type: choice + default: locked + options: + - locked + - publish-stable + version_bump: + description: Stable version strategy. Patch is the normal OSS release default. required: true - default: 0.0.1 + 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: '' permissions: contents: write @@ -25,33 +40,100 @@ concurrency: jobs: guard: - name: 1. Release 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 [ "${{ inputs.confirm_stable_release }}" != "publish-stable" ]; then + if [ "$CONFIRM_STABLE_RELEASE" != "publish-stable" ]; then echo "Stable release is locked. Use confirm_stable_release=publish-stable to publish." >&2 exit 1 fi - if ! printf '%s' "${{ inputs.stable_version }}" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then + 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=${{ inputs.stable_version }}" >> "$GITHUB_OUTPUT" + 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 source + name: 2. Verify Release ${{ needs.guard.outputs.stable_version }} needs: guard runs-on: ubuntu-latest steps: @@ -107,6 +189,8 @@ jobs: { echo "## Stable release verify" echo + echo "Release ${{ needs.guard.outputs.package_version }}" + echo echo "| Stage | Result |" echo "| --- | --- |" echo "| Install | ${{ steps.install.outcome }} |" @@ -120,7 +204,7 @@ jobs: } >> "$GITHUB_STEP_SUMMARY" publish: - name: 3. Publish npm latest + name: 3. Publish Release ${{ needs.guard.outputs.stable_version }} to npm latest needs: [guard, verify] runs-on: ubuntu-latest outputs: @@ -173,7 +257,7 @@ jobs: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} github-release: - name: 4. Create GitHub release + name: 4. Create GitHub Release ${{ needs.guard.outputs.stable_version }} needs: [guard, publish] runs-on: ubuntu-latest steps: diff --git a/README.md b/README.md index b37605c..685a555 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ > A lightweight React hooks library for building timers, stopwatches, and real-time clocks with minimal boilerplate. -[![npm alpha](https://img.shields.io/npm/v/%40crup%2Freact-timer-hook/alpha?label=npm%20alpha&color=00b894)](https://www.npmjs.com/package/@crup/react-timer-hook?activeTab=versions) +[![npm](https://img.shields.io/npm/v/%40crup%2Freact-timer-hook?label=npm&color=00b894)](https://www.npmjs.com/package/@crup/react-timer-hook) [![npm downloads](https://img.shields.io/npm/dm/%40crup%2Freact-timer-hook?color=0f766e)](https://www.npmjs.com/package/@crup/react-timer-hook) [![CI](https://github.com/crup/react-timer-hook/actions/workflows/ci.yml/badge.svg)](https://github.com/crup/react-timer-hook/actions/workflows/ci.yml) [![Docs](https://github.com/crup/react-timer-hook/actions/workflows/docs.yml/badge.svg)](https://github.com/crup/react-timer-hook/actions/workflows/docs.yml) @@ -29,11 +29,9 @@ Timers get messy when a product needs pause and resume, countdowns tied to serve ## Install -The project is currently in alpha while the API receives feedback. - ```sh -npm install @crup/react-timer-hook@alpha -pnpm add @crup/react-timer-hook@alpha +npm install @crup/react-timer-hook@latest +pnpm add @crup/react-timer-hook@latest ``` Runtime requirements: Node 18+ and React 18+. diff --git a/docs-site/docs/getting-started.mdx b/docs-site/docs/getting-started.mdx index a9c0d4c..14dd0dd 100644 --- a/docs-site/docs/getting-started.mdx +++ b/docs-site/docs/getting-started.mdx @@ -1,15 +1,13 @@ --- title: Getting started -description: Install the alpha package and wire a timer into a React component. +description: Install the package and wire a timer into a React component. --- # Getting started -Install the alpha build while the API is collecting feedback. - ```sh -npm install @crup/react-timer-hook@alpha -pnpm add @crup/react-timer-hook@alpha +npm install @crup/react-timer-hook@latest +pnpm add @crup/react-timer-hook@latest ``` Runtime requirements: Node 18+ and React 18+. diff --git a/docs-site/docs/index.mdx b/docs-site/docs/index.mdx index e244f8d..d44de1d 100644 --- a/docs-site/docs/index.mdx +++ b/docs-site/docs/index.mdx @@ -14,8 +14,8 @@ import { StopwatchSample, AbsoluteCountdownSample, TimerGroupSample } from '../s Start with the ~1.2 kB core hook, then compose opt-in batteries for schedules, diagnostics, duration helpers, and pages with many independent timers. ```sh -npm install @crup/react-timer-hook@alpha -pnpm add @crup/react-timer-hook@alpha +npm install @crup/react-timer-hook@latest +pnpm add @crup/react-timer-hook@latest ``` ## Live preview diff --git a/docs-site/docs/project/release-channels.mdx b/docs-site/docs/project/release-channels.mdx index 660c0c2..631aa0d 100644 --- a/docs-site/docs/project/release-channels.mdx +++ b/docs-site/docs/project/release-channels.mdx @@ -1,44 +1,61 @@ --- title: Release channels -description: Alpha and stable release pipeline policy. +description: Stable and prerelease pipeline policy. --- # Release channels -The package publishes alpha builds from `next` and stable builds from `main`. +The public install path uses npm `latest`. ```sh -npm install @crup/react-timer-hook@alpha +npm install @crup/react-timer-hook@latest ``` -- `Prerelease` publishes an `0.0.1-alpha.x` version from `next`. -- `Prerelease` updates the `alpha` dist-tag. -- Npm requires a `latest` dist-tag, so the workflow keeps `latest` pointing at the current alpha until stable publishing is unlocked. -- `Release` only runs from `main`. -- `Release` is manually gated and requires `confirm_stable_release=publish-stable`. -- The first stable version should be `0.0.1` to match the existing `0.0.1-alpha.x` prerelease line. +Stable releases are cut from `main`. Prerelease builds can still be published from `next` for testing, but the docs and README should point users at `latest` once stable publishing is enabled. ## Stable release stages -The stable release workflow is intentionally split into visible jobs: +The `Release` workflow is intentionally split into visible jobs: | Stage | What it gates | | --- | --- | -| Release guard | Confirms the workflow is running on `main`, the stable confirmation was typed, and the version is stable semver. | +| Plan release | Confirms the workflow is running on `main`, validates the publish gate, and resolves the release version. | | Verify source | Runs typecheck, tests, package build, docs build, README check, size report, and pack dry run. | | Publish npm latest | Sets the stable version, blocks duplicate publishes, builds the final package, and publishes to npm `latest`. | | Create GitHub release | Creates a `vX.Y.Z` GitHub release for the exact `main` commit. | -Recommended flow: +## Stable release inputs + +| Input | Type | Use | +| --- | --- | --- | +| `confirm_stable_release` | Dropdown | Choose `publish-stable` to unlock npm `latest`. Leave `locked` to block publishing. | +| `version_bump` | Dropdown | Choose `patch`, `minor`, `major`, or `manual`. `patch` is the normal OSS default. | +| `stable_version` | Text | Only used when `version_bump` is `manual`. | + +The first stable version resolves to `0.0.1` because the prerelease line already used the `0.0.1` base. + +After a stable version exists, `patch` resolves the next patch automatically. For example, if npm `latest` is `0.0.1`, the next patch release resolves to `0.0.2`. + +Use this rule of thumb: + +| Change type | Release action | +| --- | --- | +| Documentation, CI-only, internal chore | Usually do not publish a stable npm release. Merge to `main` only. | +| Backward-compatible bug fix | Publish a patch release. | +| Backward-compatible feature | Publish a minor release. | +| Breaking API change | Publish a major release. | + +## Recommended flow 1. Merge feature work into `next`. -2. Test alpha from `next`. +2. Test a prerelease from `next` when needed. 3. Open and merge `next` into `main`. -4. Run `Release` manually on `main` with: +4. Run `Release` manually on `main`. + +For the first stable release, use: ```txt confirm_stable_release=publish-stable -stable_version=0.0.1 +version_bump=patch +stable_version= ``` - -Consumers should use `@alpha` until the stable workflow has published `latest`. diff --git a/docs-site/static/llms-full.txt b/docs-site/static/llms-full.txt index e401118..40ae3e9 100644 --- a/docs-site/static/llms-full.txt +++ b/docs-site/static/llms-full.txt @@ -3,8 +3,8 @@ ## Install ```sh -npm install @crup/react-timer-hook@alpha -pnpm add @crup/react-timer-hook@alpha +npm install @crup/react-timer-hook@latest +pnpm add @crup/react-timer-hook@latest ``` Runtime requirements: Node 18+ and React 18+. diff --git a/docs-site/static/llms.txt b/docs-site/static/llms.txt index 33e613f..f25e4f1 100644 --- a/docs-site/static/llms.txt +++ b/docs-site/static/llms.txt @@ -4,7 +4,7 @@ A lightweight React hooks library for building timers, stopwatches, and real-tim Docs: https://crup.github.io/react-timer-hook/ Package: @crup/react-timer-hook -Install alpha: npm install @crup/react-timer-hook@alpha +Install: npm install @crup/react-timer-hook@latest Runtime: Node 18+ and React 18+ Repository: https://github.com/crup/react-timer-hook diff --git a/package.json b/package.json index cceb4fe..1efa3f0 100644 --- a/package.json +++ b/package.json @@ -47,8 +47,7 @@ ], "sideEffects": false, "publishConfig": { - "access": "public", - "tag": "alpha" + "access": "public" }, "engines": { "node": ">=18.0.0" diff --git a/scripts/check-readme.mjs b/scripts/check-readme.mjs index c72d0d5..e89f898 100644 --- a/scripts/check-readme.mjs +++ b/scripts/check-readme.mjs @@ -7,7 +7,7 @@ const required = [ 'useTimerGroup', 'durationParts', 'https://crup.github.io/react-timer-hook/', - '@crup/react-timer-hook@alpha', + '@crup/react-timer-hook@latest', 'Bundle size', 'AI-friendly', ];