From 33a67dc48ebb019dc8c81d23727c522f6e0a7fae Mon Sep 17 00:00:00 2001 From: Rajender Joshi <2614954+crup@users.noreply.github.com> Date: Tue, 7 Apr 2026 22:03:33 +0530 Subject: [PATCH] ci(release): add stable release pipeline --- .github/workflows/release.yml | 194 +++++++++++++++++--- README.md | 1 + docs-site/docs/project/release-channels.mdx | 31 +++- package.json | 22 +++ 4 files changed, 222 insertions(+), 26 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 169d9c6..ed88903 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,28 +4,58 @@ on: workflow_dispatch: inputs: confirm_stable_release: - description: Type "publish-stable" to publish the latest dist-tag. + description: Type "publish-stable" to publish to npm latest. required: true default: alpha-only + stable_version: + description: Stable version to publish. First stable should be 0.0.1. + required: true + default: 0.0.1 permissions: contents: write - pull-requests: write id-token: write env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true concurrency: - group: release + group: stable-release cancel-in-progress: false jobs: - release: - if: github.ref_name == 'main' && inputs.confirm_stable_release == 'publish-stable' + guard: + name: 1. Release guard + runs-on: ubuntu-latest + outputs: + stable_version: ${{ steps.guard.outputs.stable_version }} + steps: + - name: Validate release inputs + id: guard + 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 + 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 + 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" + + verify: + name: 2. Verify source + needs: guard runs-on: ubuntu-latest steps: - - name: Checkout + - name: Checkout main uses: actions/checkout@v4 - name: Setup pnpm @@ -33,36 +63,154 @@ jobs: with: version: 10.30.2 - - name: Setup Node + - name: Setup Node 24 uses: actions/setup-node@v4 with: node-version: 24 cache: pnpm - registry-url: https://registry.npmjs.org - - name: Install + - name: Install dependencies + id: install run: pnpm install --frozen-lockfile - - name: Verify + - 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: | - pnpm typecheck - pnpm test - pnpm build + { + echo "## Stable release verify" + 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: Create release PR or publish - uses: changesets/action@v1 + - name: Setup Node 24 + uses: actions/setup-node@v4 with: - publish: pnpm release + 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: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - stable-release-locked: - if: inputs.confirm_stable_release != 'publish-stable' + github-release: + name: 4. Create GitHub release + needs: [guard, publish] runs-on: ubuntu-latest steps: - - name: Stable release is locked + - 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 <> "$GITHUB_STEP_SUMMARY" diff --git a/README.md b/README.md index 9d0df5f..b37605c 100644 --- a/README.md +++ b/README.md @@ -297,5 +297,6 @@ Issues, recipes, docs improvements, and focused bug reports are welcome. - Read the docs: https://crup.github.io/react-timer-hook/ - Open an issue: https://github.com/crup/react-timer-hook/issues - See the contributing guide: ./CONTRIBUTING.md +- Release policy: https://crup.github.io/react-timer-hook/project/release-channels/ The package targets Node 18+ and React 18+. diff --git a/docs-site/docs/project/release-channels.mdx b/docs-site/docs/project/release-channels.mdx index 5e87d60..660c0c2 100644 --- a/docs-site/docs/project/release-channels.mdx +++ b/docs-site/docs/project/release-channels.mdx @@ -1,11 +1,11 @@ --- title: Release channels -description: Alpha-only release policy before the first stable version. +description: Alpha and stable release pipeline policy. --- # Release channels -The package is alpha-only until stable publishing is explicitly unlocked. +The package publishes alpha builds from `next` and stable builds from `main`. ```sh npm install @crup/react-timer-hook@alpha @@ -14,6 +14,31 @@ npm install @crup/react-timer-hook@alpha - `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. -Consumers should use `@alpha` until the release policy changes. +## Stable release stages + +The stable 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. | +| 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: + +1. Merge feature work into `next`. +2. Test alpha from `next`. +3. Open and merge `next` into `main`. +4. Run `Release` manually on `main` with: + +```txt +confirm_stable_release=publish-stable +stable_version=0.0.1 +``` + +Consumers should use `@alpha` until the stable workflow has published `latest`. diff --git a/package.json b/package.json index 17f10bd..cceb4fe 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,14 @@ "name": "@crup/react-timer-hook", "version": "0.0.0", "description": "A lightweight React hooks library for building timers, stopwatches, and real-time clocks with minimal boilerplate.", + "homepage": "https://crup.github.io/react-timer-hook/", + "repository": { + "type": "git", + "url": "git+https://github.com/crup/react-timer-hook.git" + }, + "bugs": { + "url": "https://github.com/crup/react-timer-hook/issues" + }, "main": "./dist/index.cjs", "module": "./dist/index.js", "types": "./dist/index.d.ts", @@ -65,11 +73,25 @@ "keywords": [ "react", "hook", + "hooks", "timer", + "react-timer", + "react-timer-hook", + "timer-hook", "stopwatch", + "react-stopwatch", + "stopwatch-hook", "time", "countdown", + "countdown-timer", + "react-countdown", + "clock", + "real-time", "scheduler", + "polling", + "duration", + "timer-group", + "typescript", "react-hooks" ], "author": "Rajender Joshi ",