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
92 changes: 79 additions & 13 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -1,21 +1,35 @@
name: Publish to npm
name: Publish

on:
release:
types: [published]
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
tag:
description: 'npm dist-tag (alpha, beta, next, latest)'
default: 'alpha'
required: true

jobs:
publish:
name: Publish to npm
name: Publish all @tabmesh/* packages
runs-on: ubuntu-latest

# OIDC trusted publishing — npm exchanges the OIDC token for a
# short-lived publish credential. No long-lived NPM_TOKEN secret.
# See: https://docs.npmjs.com/trusted-publishers
permissions:
contents: read
id-token: write
contents: read

steps:
- uses: actions/checkout@v4

- uses: pnpm/action-setup@v2
with:
version: 9

- uses: actions/setup-node@v4
with:
node-version: 20
Expand All @@ -25,13 +39,65 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Run tests
run: pnpm test
- name: Verify tag matches package versions
if: github.event_name == 'push'
run: |
TAG="${GITHUB_REF_NAME#v}"
CORE=$(node -p "require('./packages/core/package.json').version")
REACT=$(node -p "require('./packages/react/package.json').version")
WS=$(node -p "require('./packages/transport-websocket/package.json').version")
echo "Tag: $TAG"
echo "@tabmesh/core: $CORE"
echo "@tabmesh/react: $REACT"
echo "@tabmesh/transport-websocket: $WS"
if [ "$TAG" != "$CORE" ] || [ "$TAG" != "$REACT" ] || [ "$TAG" != "$WS" ]; then
echo "::error::Tag does not match all three package.json versions"
exit 1
fi

- name: Build all packages
run: pnpm -r build

- name: Run unit tests
run: pnpm exec vitest --run

- name: Determine dist-tag
id: tag
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "tag=${{ inputs.tag }}" >> "$GITHUB_OUTPUT"
else
VER=$(node -p "require('./packages/core/package.json').version")
case "$VER" in
*"-alpha."*) echo "tag=alpha" >> "$GITHUB_OUTPUT" ;;
*"-beta."*) echo "tag=beta" >> "$GITHUB_OUTPUT" ;;
*"-rc."*|*"-next."*) echo "tag=next" >> "$GITHUB_OUTPUT" ;;
*) echo "tag=latest" >> "$GITHUB_OUTPUT" ;;
esac
fi

# Publish in dependency order so the registry resolves @tabmesh/core
# before the dependents are fetched.
- name: Publish @tabmesh/core
run: >
pnpm --filter @tabmesh/core publish
--tag ${{ steps.tag.outputs.tag }}
--access public
--no-git-checks
--provenance

- name: Build packages
run: pnpm build
- name: Publish @tabmesh/transport-websocket
run: >
pnpm --filter @tabmesh/transport-websocket publish
--tag ${{ steps.tag.outputs.tag }}
--access public
--no-git-checks
--provenance

- name: Publish to npm
run: pnpm publish -r --access public --no-git-checks
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Publish @tabmesh/react
run: >
pnpm --filter @tabmesh/react publish
--tag ${{ steps.tag.outputs.tag }}
--access public
--no-git-checks
--provenance
78 changes: 78 additions & 0 deletions scripts/release.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
#!/usr/bin/env node
/**
* Release helper. Bumps all three publishable @tabmesh/* package.json
* versions in lockstep, keeps their peer-dependency on @tabmesh/core
* pinned to the same version, and tags the result.
*
* Usage:
* node scripts/release.mjs 0.1.0-alpha.0
*
* Then push the tag to trigger .github/workflows/publish.yml:
* git push origin main --tags
*
* The workflow re-validates that the tag matches the three package
* versions, then publishes via npm trusted publishing (OIDC, no
* NPM_TOKEN). The dist-tag is inferred from the version suffix:
* x.y.z-alpha.n -> alpha
* x.y.z-beta.n -> beta
* x.y.z-rc.n -> next
* x.y.z -> latest
*
* Idempotency: re-running with the same version is a no-op for files
* but will fail at `git commit` (nothing to commit) and `git tag`
* (tag exists). That's intentional.
*/

import { execSync } from 'node:child_process';
import { readFileSync, writeFileSync } from 'node:fs';
import { resolve } from 'node:path';

const version = process.argv[2];
if (!version || !/^\d+\.\d+\.\d+(-[a-z]+\.\d+)?$/.test(version)) {
console.error('Usage: node scripts/release.mjs <semver>');
console.error('Examples: 0.1.0-alpha.0, 0.1.0-beta.1, 0.1.0, 1.0.0-rc.0');
process.exit(1);
}

const packages = [
{ name: '@tabmesh/core', dir: 'packages/core', peerOfCore: false },
{ name: '@tabmesh/react', dir: 'packages/react', peerOfCore: true },
{
name: '@tabmesh/transport-websocket',
dir: 'packages/transport-websocket',
peerOfCore: true,
},
];

for (const pkg of packages) {
const path = resolve(pkg.dir, 'package.json');
const json = JSON.parse(readFileSync(path, 'utf8'));
json.version = version;
// Pin the peer-dep on @tabmesh/core to the exact version so consumers
// can't end up with a mixed-version install. We're pre-1.0; protocol
// changes between alphas should force an all-three upgrade.
if (pkg.peerOfCore && json.peerDependencies?.['@tabmesh/core']) {
json.peerDependencies['@tabmesh/core'] = version;
}
writeFileSync(path, `${JSON.stringify(json, null, 2)}\n`);
console.log(`bumped ${pkg.name} → ${version}`);
}

// Commit and tag in one shot. Don't push — that's intentional, gives a
// chance to review `git show` before the publish workflow fires.
execSync(`git add ${packages.map((p) => `${p.dir}/package.json`).join(' ')}`);
execSync(`git commit -m "chore(release): v${version}"`);
execSync(`git tag v${version}`);

console.log('');
console.log(`Tagged v${version}. To publish, push the tag:`);
console.log(' git push origin main --tags');
console.log('');
console.log('The publish workflow will:');
console.log(` 1. Verify tag v${version} matches all three package versions`);
console.log(' 2. Build all packages');
console.log(' 3. Run unit tests');
console.log(
` 4. Publish all three with --tag ${version.includes('-alpha.') ? 'alpha' : version.includes('-beta.') ? 'beta' : version.includes('-rc.') || version.includes('-next.') ? 'next' : 'latest'} (inferred from suffix)`
);
console.log(' 5. Each publish carries npm provenance (OIDC trusted publishing)');
Loading