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
108 changes: 96 additions & 12 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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 }} |"
Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
8 changes: 3 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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+.
Expand Down
8 changes: 3 additions & 5 deletions docs-site/docs/getting-started.mdx
Original file line number Diff line number Diff line change
@@ -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+.
Expand Down
4 changes: 2 additions & 2 deletions docs-site/docs/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
51 changes: 34 additions & 17 deletions docs-site/docs/project/release-channels.mdx
Original file line number Diff line number Diff line change
@@ -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`.
4 changes: 2 additions & 2 deletions docs-site/static/llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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+.
Expand Down
2 changes: 1 addition & 1 deletion docs-site/static/llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,7 @@
],
"sideEffects": false,
"publishConfig": {
"access": "public",
"tag": "alpha"
"access": "public"
},
"engines": {
"node": ">=18.0.0"
Expand Down
2 changes: 1 addition & 1 deletion scripts/check-readme.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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',
];
Expand Down
Loading