diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 892d83a3b..5300fd61c 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -29,11 +29,23 @@ jobs: id: version run: | if [ "${{ github.event_name }}" = "push" ]; then - echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT" + VERSION="${GITHUB_REF_NAME#v}" else BRANCH="${{ github.event.pull_request.head.ref }}" - echo "version=${BRANCH#release/v}" >> "$GITHUB_OUTPUT" + VERSION="${BRANCH#release/v}" fi + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + + # Detect pre-release tag (e.g. 0.1.16-alpha.1 → alpha, 0.1.16-beta.2 → beta) + if [[ "$VERSION" =~ -([a-zA-Z]+) ]]; then + DIST_TAG="${BASH_REMATCH[1]}" + echo "prerelease=true" >> "$GITHUB_OUTPUT" + else + DIST_TAG="latest" + echo "prerelease=false" >> "$GITHUB_OUTPUT" + fi + echo "dist_tag=${DIST_TAG}" >> "$GITHUB_OUTPUT" + echo "Resolved version=${VERSION} dist_tag=${DIST_TAG}" - name: Create release tag if: github.event_name == 'pull_request' @@ -56,6 +68,7 @@ jobs: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} run: | VERSION="${{ steps.version.outputs.version }}" + DIST_TAG="${{ steps.version.outputs.dist_tag }}" FAILED=0 publish_pkg() { @@ -69,7 +82,7 @@ jobs: fi echo "📦 Publishing ${name}@${VERSION}..." - if pnpm --filter "$filter" publish --access public --no-git-checks; then + if pnpm --filter "$filter" publish --access public --no-git-checks --tag "$DIST_TAG"; then echo "✅ ${name}@${VERSION} published" else echo "❌ ${name}@${VERSION} failed to publish" @@ -96,7 +109,7 @@ jobs: fs.writeFileSync(p, JSON.stringify(pkg, null, 2) + '\n'); " echo "📦 Publishing hyperframes@${VERSION}..." - if (cd packages/cli && npm publish --access public); then + if (cd packages/cli && npm publish --access public --tag "$DIST_TAG"); then echo "✅ hyperframes@${VERSION} published" else echo "❌ hyperframes@${VERSION} failed to publish" @@ -114,12 +127,14 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | VERSION="${{ steps.version.outputs.version }}" + PRERELEASE="${{ steps.version.outputs.prerelease }}" # Skip if release already exists (idempotent re-runs) if gh release view "v${VERSION}" --repo "${{ github.repository }}" >/dev/null 2>&1; then echo "Release v${VERSION} already exists — skipping" else - gh release create "v${VERSION}" \ - --repo "${{ github.repository }}" \ - --title "v${VERSION}" \ - --generate-notes + FLAGS=(--repo "${{ github.repository }}" --title "v${VERSION}" --generate-notes) + if [ "$PRERELEASE" = "true" ]; then + FLAGS+=(--prerelease) + fi + gh release create "v${VERSION}" "${FLAGS[@]}" fi diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 22f1d7b36..fecd6aaa1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -59,6 +59,8 @@ Git hooks (via [lefthook](https://github.com/evilmartians/lefthook)) run automat All packages use **fixed versioning** — every release bumps all packages to the same version. +### Stable releases + ```bash bun run set-version 0.2.0 # bumps all packages, commits, and creates git tag git push origin main --tags # triggers the publish workflow @@ -66,6 +68,26 @@ git push origin main --tags # triggers the publish workflow The `set-version` script automatically creates a `chore: release v` commit and a `v` git tag. Pushing the tag triggers CI to publish all packages to npm and create a GitHub Release. +### Pre-releases (alpha / beta / rc) + +Use a pre-release suffix to publish to a named npm dist-tag instead of `latest`: + +```bash +bun run set-version 0.2.0-alpha.1 # first alpha +git push origin v0.2.0-alpha.1 # publishes to npm with --tag alpha + +bun run set-version 0.2.0-alpha.2 # iterate +bun run set-version 0.2.0-beta.1 # promote to beta (--tag beta) +bun run set-version 0.2.0-rc.1 # release candidate (--tag rc) +bun run set-version 0.2.0 # final stable release (--tag latest) +``` + +Consumers install pre-releases with `npm install @hyperframes/core@alpha` (or `@beta`, `@rc`). The `latest` tag is never touched by pre-releases, so `npm install @hyperframes/core` always gets the last stable version. + +Pre-releases also create GitHub Releases marked as **pre-release**. + +### Options + If you need to bump versions without committing (e.g., for a release PR), pass `--no-tag`: ```bash diff --git a/scripts/set-version.ts b/scripts/set-version.ts index f01310ff8..166dced5a 100644 --- a/scripts/set-version.ts +++ b/scripts/set-version.ts @@ -4,10 +4,13 @@ * then create a git commit and tag. * * Usage: - * bun run set-version 0.1.1 # bump, commit, and tag + * bun run set-version 0.1.1 # stable release → npm "latest" tag + * bun run set-version 0.1.1-alpha.1 # pre-release → npm "alpha" tag * bun run set-version 0.1.1 --no-tag # bump only (no commit or tag) * * All packages share a single version number (fixed versioning). + * Pre-release suffixes (-alpha, -beta, -rc, etc.) are detected by the + * publish workflow and published to the corresponding npm dist-tag. */ import { readFileSync, writeFileSync } from "fs"; @@ -79,7 +82,16 @@ function main() { execSync(`git commit -m "chore: release v${version}"`, { cwd: ROOT, stdio: "inherit" }); execSync(`git tag v${version}`, { cwd: ROOT, stdio: "inherit" }); console.log(`\nCreated commit and tag v${version}`); - console.log(`Run 'git push origin main --tags' to trigger the publish workflow.`); + + const isPrerelease = version.includes("-"); + if (isPrerelease) { + const distTag = version.replace(/^.*-([a-zA-Z]+).*$/, "$1"); + console.log(`\nThis is a pre-release — npm dist-tag will be "${distTag}" (not "latest").`); + console.log(`Consumers install with: npm install @hyperframes/core@${distTag}`); + console.log(`\nRun 'git push origin v${version}' to trigger the publish workflow.`); + } else { + console.log(`Run 'git push origin main --tags' to trigger the publish workflow.`); + } } main();