Skip to content

Commit 8d56c0c

Browse files
committed
ci(release): refine stable release workflow
1 parent d405e7f commit 8d56c0c

File tree

9 files changed

+143
-47
lines changed

9 files changed

+143
-47
lines changed

.github/workflows/release.yml

Lines changed: 96 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,31 @@
11
name: Release
2+
run-name: Release ${{ inputs.version_bump }} to npm latest
23

34
on:
45
workflow_dispatch:
56
inputs:
67
confirm_stable_release:
7-
description: Type "publish-stable" to publish to npm latest.
8+
description: Publish gate.
89
required: true
9-
default: alpha-only
10-
stable_version:
11-
description: Stable version to publish. First stable should be 0.0.1.
10+
type: choice
11+
default: locked
12+
options:
13+
- locked
14+
- publish-stable
15+
version_bump:
16+
description: Stable version strategy. Patch is the normal OSS release default.
1217
required: true
13-
default: 0.0.1
18+
type: choice
19+
default: patch
20+
options:
21+
- patch
22+
- minor
23+
- major
24+
- manual
25+
stable_version:
26+
description: Manual stable version, used only when version_bump is manual.
27+
required: false
28+
default: ''
1429

1530
permissions:
1631
contents: write
@@ -25,33 +40,100 @@ concurrency:
2540

2641
jobs:
2742
guard:
28-
name: 1. Release guard
43+
name: 1. Plan release
2944
runs-on: ubuntu-latest
3045
outputs:
3146
stable_version: ${{ steps.guard.outputs.stable_version }}
47+
package_name: ${{ steps.guard.outputs.package_name }}
48+
package_version: ${{ steps.guard.outputs.package_version }}
49+
current_latest: ${{ steps.guard.outputs.current_latest }}
3250
steps:
51+
- name: Checkout main
52+
uses: actions/checkout@v4
53+
54+
- name: Setup Node 24
55+
uses: actions/setup-node@v4
56+
with:
57+
node-version: 24
58+
3359
- name: Validate release inputs
3460
id: guard
61+
env:
62+
CONFIRM_STABLE_RELEASE: ${{ inputs.confirm_stable_release }}
63+
VERSION_BUMP: ${{ inputs.version_bump }}
64+
STABLE_VERSION_INPUT: ${{ inputs.stable_version }}
3565
run: |
3666
if [ "${{ github.ref_name }}" != "main" ]; then
3767
echo "Stable releases must run from main. Current ref: ${{ github.ref_name }}" >&2
3868
exit 1
3969
fi
4070
41-
if [ "${{ inputs.confirm_stable_release }}" != "publish-stable" ]; then
71+
if [ "$CONFIRM_STABLE_RELEASE" != "publish-stable" ]; then
4272
echo "Stable release is locked. Use confirm_stable_release=publish-stable to publish." >&2
4373
exit 1
4474
fi
4575
46-
if ! printf '%s' "${{ inputs.stable_version }}" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then
76+
PACKAGE_NAME="$(node -p "require('./package.json').name")"
77+
CURRENT_LATEST="$(npm view "$PACKAGE_NAME@latest" version 2>/dev/null || true)"
78+
79+
if [ "$VERSION_BUMP" = "manual" ]; then
80+
STABLE_VERSION="$STABLE_VERSION_INPUT"
81+
else
82+
STABLE_VERSION="$(
83+
CURRENT_LATEST="$CURRENT_LATEST" VERSION_BUMP="$VERSION_BUMP" node - <<'NODE'
84+
const currentLatest = process.env.CURRENT_LATEST || '';
85+
const bump = process.env.VERSION_BUMP || 'patch';
86+
const match = currentLatest.match(/^(\d+)\.(\d+)\.(\d+)(?:-.+)?$/);
87+
let major = match ? Number(match[1]) : 0;
88+
let minor = match ? Number(match[2]) : 0;
89+
let patch = match ? Number(match[3]) : 0;
90+
const hasLatest = Boolean(match);
91+
const isPrerelease = /-/.test(currentLatest);
92+
93+
if (!hasLatest) {
94+
patch = 1;
95+
} else if (isPrerelease) {
96+
// Promote the current prerelease base to stable, for example 0.0.1-next.18 -> 0.0.1.
97+
} else if (bump === 'major') {
98+
major += 1;
99+
minor = 0;
100+
patch = 0;
101+
} else if (bump === 'minor') {
102+
minor += 1;
103+
patch = 0;
104+
} else {
105+
patch += 1;
106+
}
107+
108+
process.stdout.write(`${major}.${minor}.${patch}`);
109+
NODE
110+
)"
111+
fi
112+
113+
if ! printf '%s' "$STABLE_VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then
47114
echo "stable_version must be a stable semver version like 0.0.1." >&2
48115
exit 1
49116
fi
50117

51-
echo "stable_version=${{ inputs.stable_version }}" >> "$GITHUB_OUTPUT"
118+
echo "stable_version=$STABLE_VERSION" >> "$GITHUB_OUTPUT"
119+
echo "package_name=$PACKAGE_NAME" >> "$GITHUB_OUTPUT"
120+
echo "package_version=$PACKAGE_NAME@$STABLE_VERSION" >> "$GITHUB_OUTPUT"
121+
echo "current_latest=${CURRENT_LATEST:-none}" >> "$GITHUB_OUTPUT"
122+
123+
{
124+
echo "## Release $STABLE_VERSION"
125+
echo
126+
echo "| Field | Value |"
127+
echo "| --- | --- |"
128+
echo "| Package | $PACKAGE_NAME |"
129+
echo "| Current npm latest | ${CURRENT_LATEST:-none} |"
130+
echo "| Version strategy | $VERSION_BUMP |"
131+
echo "| Release version | $STABLE_VERSION |"
132+
echo "| Dist tag | latest |"
133+
} >> "$GITHUB_STEP_SUMMARY"
52134

53135
verify:
54-
name: 2. Verify source
136+
name: 2. Verify Release ${{ needs.guard.outputs.stable_version }}
55137
needs: guard
56138
runs-on: ubuntu-latest
57139
steps:
@@ -107,6 +189,8 @@ jobs:
107189
{
108190
echo "## Stable release verify"
109191
echo
192+
echo "Release ${{ needs.guard.outputs.package_version }}"
193+
echo
110194
echo "| Stage | Result |"
111195
echo "| --- | --- |"
112196
echo "| Install | ${{ steps.install.outcome }} |"
@@ -120,7 +204,7 @@ jobs:
120204
} >> "$GITHUB_STEP_SUMMARY"
121205
122206
publish:
123-
name: 3. Publish npm latest
207+
name: 3. Publish Release ${{ needs.guard.outputs.stable_version }} to npm latest
124208
needs: [guard, verify]
125209
runs-on: ubuntu-latest
126210
outputs:
@@ -173,7 +257,7 @@ jobs:
173257
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
174258

175259
github-release:
176-
name: 4. Create GitHub release
260+
name: 4. Create GitHub Release ${{ needs.guard.outputs.stable_version }}
177261
needs: [guard, publish]
178262
runs-on: ubuntu-latest
179263
steps:

README.md

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
> A lightweight React hooks library for building timers, stopwatches, and real-time clocks with minimal boilerplate.
44
5-
[![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)
5+
[![npm](https://img.shields.io/npm/v/%40crup%2Freact-timer-hook?label=npm&color=00b894)](https://www.npmjs.com/package/@crup/react-timer-hook)
66
[![npm downloads](https://img.shields.io/npm/dm/%40crup%2Freact-timer-hook?color=0f766e)](https://www.npmjs.com/package/@crup/react-timer-hook)
77
[![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)
88
[![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
2929

3030
## Install
3131

32-
The project is currently in alpha while the API receives feedback.
33-
3432
```sh
35-
npm install @crup/react-timer-hook@alpha
36-
pnpm add @crup/react-timer-hook@alpha
33+
npm install @crup/react-timer-hook@latest
34+
pnpm add @crup/react-timer-hook@latest
3735
```
3836

3937
Runtime requirements: Node 18+ and React 18+.

docs-site/docs/getting-started.mdx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
---
22
title: Getting started
3-
description: Install the alpha package and wire a timer into a React component.
3+
description: Install the package and wire a timer into a React component.
44
---
55

66
# Getting started
77

8-
Install the alpha build while the API is collecting feedback.
9-
108
```sh
11-
npm install @crup/react-timer-hook@alpha
12-
pnpm add @crup/react-timer-hook@alpha
9+
npm install @crup/react-timer-hook@latest
10+
pnpm add @crup/react-timer-hook@latest
1311
```
1412

1513
Runtime requirements: Node 18+ and React 18+.

docs-site/docs/index.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ import { StopwatchSample, AbsoluteCountdownSample, TimerGroupSample } from '../s
1414
Start with the ~1.2 kB core hook, then compose opt-in batteries for schedules, diagnostics, duration helpers, and pages with many independent timers.
1515

1616
```sh
17-
npm install @crup/react-timer-hook@alpha
18-
pnpm add @crup/react-timer-hook@alpha
17+
npm install @crup/react-timer-hook@latest
18+
pnpm add @crup/react-timer-hook@latest
1919
```
2020

2121
## Live preview

docs-site/docs/project/release-channels.mdx

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,61 @@
11
---
22
title: Release channels
3-
description: Alpha and stable release pipeline policy.
3+
description: Stable and prerelease pipeline policy.
44
---
55

66
# Release channels
77

8-
The package publishes alpha builds from `next` and stable builds from `main`.
8+
The public install path uses npm `latest`.
99

1010
```sh
11-
npm install @crup/react-timer-hook@alpha
11+
npm install @crup/react-timer-hook@latest
1212
```
1313

14-
- `Prerelease` publishes an `0.0.1-alpha.x` version from `next`.
15-
- `Prerelease` updates the `alpha` dist-tag.
16-
- Npm requires a `latest` dist-tag, so the workflow keeps `latest` pointing at the current alpha until stable publishing is unlocked.
17-
- `Release` only runs from `main`.
18-
- `Release` is manually gated and requires `confirm_stable_release=publish-stable`.
19-
- The first stable version should be `0.0.1` to match the existing `0.0.1-alpha.x` prerelease line.
14+
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.
2015

2116
## Stable release stages
2217

23-
The stable release workflow is intentionally split into visible jobs:
18+
The `Release` workflow is intentionally split into visible jobs:
2419

2520
| Stage | What it gates |
2621
| --- | --- |
27-
| Release guard | Confirms the workflow is running on `main`, the stable confirmation was typed, and the version is stable semver. |
22+
| Plan release | Confirms the workflow is running on `main`, validates the publish gate, and resolves the release version. |
2823
| Verify source | Runs typecheck, tests, package build, docs build, README check, size report, and pack dry run. |
2924
| Publish npm latest | Sets the stable version, blocks duplicate publishes, builds the final package, and publishes to npm `latest`. |
3025
| Create GitHub release | Creates a `vX.Y.Z` GitHub release for the exact `main` commit. |
3126

32-
Recommended flow:
27+
## Stable release inputs
28+
29+
| Input | Type | Use |
30+
| --- | --- | --- |
31+
| `confirm_stable_release` | Dropdown | Choose `publish-stable` to unlock npm `latest`. Leave `locked` to block publishing. |
32+
| `version_bump` | Dropdown | Choose `patch`, `minor`, `major`, or `manual`. `patch` is the normal OSS default. |
33+
| `stable_version` | Text | Only used when `version_bump` is `manual`. |
34+
35+
The first stable version resolves to `0.0.1` because the prerelease line already used the `0.0.1` base.
36+
37+
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`.
38+
39+
Use this rule of thumb:
40+
41+
| Change type | Release action |
42+
| --- | --- |
43+
| Documentation, CI-only, internal chore | Usually do not publish a stable npm release. Merge to `main` only. |
44+
| Backward-compatible bug fix | Publish a patch release. |
45+
| Backward-compatible feature | Publish a minor release. |
46+
| Breaking API change | Publish a major release. |
47+
48+
## Recommended flow
3349

3450
1. Merge feature work into `next`.
35-
2. Test alpha from `next`.
51+
2. Test a prerelease from `next` when needed.
3652
3. Open and merge `next` into `main`.
37-
4. Run `Release` manually on `main` with:
53+
4. Run `Release` manually on `main`.
54+
55+
For the first stable release, use:
3856

3957
```txt
4058
confirm_stable_release=publish-stable
41-
stable_version=0.0.1
59+
version_bump=patch
60+
stable_version=
4261
```
43-
44-
Consumers should use `@alpha` until the stable workflow has published `latest`.

docs-site/static/llms-full.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
## Install
44

55
```sh
6-
npm install @crup/react-timer-hook@alpha
7-
pnpm add @crup/react-timer-hook@alpha
6+
npm install @crup/react-timer-hook@latest
7+
pnpm add @crup/react-timer-hook@latest
88
```
99

1010
Runtime requirements: Node 18+ and React 18+.

docs-site/static/llms.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ A lightweight React hooks library for building timers, stopwatches, and real-tim
44

55
Docs: https://crup.github.io/react-timer-hook/
66
Package: @crup/react-timer-hook
7-
Install alpha: npm install @crup/react-timer-hook@alpha
7+
Install: npm install @crup/react-timer-hook@latest
88
Runtime: Node 18+ and React 18+
99
Repository: https://github.com/crup/react-timer-hook
1010

package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,7 @@
4747
],
4848
"sideEffects": false,
4949
"publishConfig": {
50-
"access": "public",
51-
"tag": "alpha"
50+
"access": "public"
5251
},
5352
"engines": {
5453
"node": ">=18.0.0"

scripts/check-readme.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const required = [
77
'useTimerGroup',
88
'durationParts',
99
'https://crup.github.io/react-timer-hook/',
10-
'@crup/react-timer-hook@alpha',
10+
'@crup/react-timer-hook@latest',
1111
'Bundle size',
1212
'AI-friendly',
1313
];

0 commit comments

Comments
 (0)