Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
194 changes: 171 additions & 23 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,65 +4,213 @@ 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
uses: pnpm/action-setup@v4
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 <<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 npm publishing is disabled by default."
echo "Use the Prerelease workflow for alpha releases."
echo "To intentionally publish latest, rerun this workflow with confirm_stable_release=publish-stable on main."
{
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"
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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+.
31 changes: 28 additions & 3 deletions docs-site/docs/project/release-channels.mdx
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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`.
22 changes: 22 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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 <connect@rajender.pro>",
Expand Down
Loading