Skip to content

Commit 37ff83b

Browse files
committed
refactor: extract NPM publishing to reusable action
- Create .github/actions/npm-publish for reusable publish logic - CI workflow now publishes pre-releases after all checks pass on main - Publish workflow simplified to just run checks + call action for tags - Publishing only happens after fmt, lint, typecheck, test, build succeed Benefits: - DRY: Single source of truth for version generation and publishing - Safe: Pre-releases only published after all CI checks pass - Clear: Separate concerns (CI checks vs publishing) - Consistent: Same publish logic for both main and tags Workflows: - Push to main → CI runs all checks → publishes 'next' tag (if checks pass) - Push tag → Publish runs all checks → publishes 'latest' tag (if checks pass) - Pull requests → CI runs all checks → no publishing
1 parent bf2c05a commit 37ff83b

3 files changed

Lines changed: 135 additions & 97 deletions

File tree

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
name: Publish to NPM
2+
description: Publish package to NPM with version generation and idempotent checks
3+
4+
inputs:
5+
is-tag:
6+
description: 'Whether this is a tag push (true) or main branch push (false)'
7+
required: true
8+
9+
runs:
10+
using: composite
11+
steps:
12+
- name: Setup Node.js with npm 11+ (for trusted publishing)
13+
uses: actions/setup-node@v4
14+
with:
15+
node-version: '20'
16+
registry-url: 'https://registry.npmjs.org'
17+
18+
- name: Generate unique version from git
19+
id: version
20+
shell: bash
21+
run: |
22+
# Get base version from package.json
23+
BASE_VERSION=$(jq -r .version package.json)
24+
25+
# Generate git describe version
26+
GIT_DESCRIBE=$(git describe --tags --always --dirty 2>/dev/null || echo "unknown")
27+
28+
if [[ "${{ inputs.is-tag }}" == "true" ]]; then
29+
# For tags, use the base version as-is (stable release)
30+
NPM_VERSION="${BASE_VERSION}"
31+
NPM_TAG="latest"
32+
echo "Publishing stable release: ${NPM_VERSION}"
33+
else
34+
# For main branch, create a pre-release version using git describe
35+
# Format: 0.3.0-next.5.g1a2b3c4 (base-next.commits.hash)
36+
GIT_COMMIT=$(git rev-parse --short HEAD)
37+
COMMITS_SINCE_TAG=$(git rev-list --count HEAD ^$(git describe --tags --abbrev=0 2>/dev/null || echo HEAD) 2>/dev/null || echo "0")
38+
NPM_VERSION="${BASE_VERSION}-next.${COMMITS_SINCE_TAG}.g${GIT_COMMIT}"
39+
NPM_TAG="next"
40+
echo "Publishing pre-release: ${NPM_VERSION}"
41+
fi
42+
43+
echo "version=${NPM_VERSION}" >> $GITHUB_OUTPUT
44+
echo "tag=${NPM_TAG}" >> $GITHUB_OUTPUT
45+
46+
# Update package.json with the new version
47+
node -e "const fs = require('fs'); const pkg = JSON.parse(fs.readFileSync('package.json')); pkg.version = '${NPM_VERSION}'; fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n');"
48+
49+
echo "Updated package.json to version ${NPM_VERSION}"
50+
51+
- name: Validate tag matches package.json version
52+
if: inputs.is-tag == 'true'
53+
shell: bash
54+
run: |
55+
# Extract version from package.json
56+
PKG_VERSION=$(jq -r .version package.json)
57+
58+
# Extract version from git tag (strip 'v' prefix)
59+
TAG_VERSION=${GITHUB_REF#refs/tags/v}
60+
61+
echo "Package version: $PKG_VERSION"
62+
echo "Tag version: $TAG_VERSION"
63+
64+
if [ "$PKG_VERSION" != "$TAG_VERSION" ]; then
65+
echo "❌ Error: Version mismatch!"
66+
echo " package.json version: $PKG_VERSION"
67+
echo " Git tag version: $TAG_VERSION"
68+
echo ""
69+
echo "Please ensure the git tag matches the version in package.json"
70+
exit 1
71+
fi
72+
73+
echo "✅ Version validation passed: $PKG_VERSION"
74+
75+
- name: Check if version exists
76+
id: check-exists
77+
shell: bash
78+
run: |
79+
PACKAGE_NAME=$(node -p "require('./package.json').name")
80+
VERSION="${{ steps.version.outputs.version }}"
81+
82+
if npm view "${PACKAGE_NAME}@${VERSION}" version &>/dev/null; then
83+
echo "exists=true" >> $GITHUB_OUTPUT
84+
echo "Version ${VERSION} already exists on npm"
85+
else
86+
echo "exists=false" >> $GITHUB_OUTPUT
87+
echo "Version ${VERSION} does not exist, will publish"
88+
fi
89+
90+
- name: Publish to npm (with OIDC trusted publishing)
91+
if: steps.check-exists.outputs.exists == 'false'
92+
shell: bash
93+
run: npm publish --tag ${{ steps.version.outputs.tag }} --provenance --access public
94+
95+
- name: Update dist-tag (version already exists)
96+
if: steps.check-exists.outputs.exists == 'true' && inputs.is-tag == 'true'
97+
shell: bash
98+
run: |
99+
PACKAGE_NAME=$(node -p "require('./package.json').name")
100+
VERSION="${{ steps.version.outputs.version }}"
101+
TAG="${{ steps.version.outputs.tag }}"
102+
103+
echo "Version ${VERSION} already published, updating dist-tag to ${TAG}"
104+
npm dist-tag add "${PACKAGE_NAME}@${VERSION}" "${TAG}"
105+
106+
- name: Skip (pre-release already exists)
107+
if: steps.check-exists.outputs.exists == 'true' && inputs.is-tag != 'true'
108+
shell: bash
109+
run: |
110+
echo "⏭️ Pre-release version already exists, skipping"

.github/workflows/ci.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,3 +113,23 @@ jobs:
113113

114114
- name: Build library
115115
run: bun run build
116+
117+
publish:
118+
name: publish to npm
119+
runs-on: ubuntu-latest
120+
# Only publish on main branch pushes after all checks pass
121+
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
122+
needs: [fmt, lint, typecheck, test, build]
123+
permissions:
124+
contents: read
125+
id-token: write # Required for OIDC trusted publishing
126+
steps:
127+
- name: Checkout code
128+
uses: actions/checkout@v4
129+
with:
130+
fetch-depth: 0 # Required for git describe to find tags
131+
132+
- name: Publish to NPM
133+
uses: ./.github/actions/npm-publish
134+
with:
135+
is-tag: 'false'

.github/workflows/publish.yml

Lines changed: 5 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,8 @@ name: publish
22

33
on:
44
push:
5-
branches:
6-
- main
75
tags:
8-
- "v*"
9-
workflow_dispatch:
6+
- 'v*'
107

118
permissions:
129
contents: read
@@ -17,7 +14,7 @@ jobs:
1714
name: publish to npm
1815
runs-on: ubuntu-latest
1916
steps:
20-
- name: Checkout code
17+
- name: Checkout tag
2118
uses: actions/checkout@v4
2219
with:
2320
ref: ${{ github.ref }}
@@ -34,61 +31,6 @@ jobs:
3431
with:
3532
version: 0.15.2
3633

37-
- name: Generate unique version from git
38-
id: version
39-
run: |
40-
# Get base version from package.json
41-
BASE_VERSION=$(jq -r .version package.json)
42-
43-
# Generate git describe version
44-
GIT_DESCRIBE=$(git describe --tags --always --dirty 2>/dev/null || echo "unknown")
45-
46-
if [[ $GITHUB_REF == refs/tags/* ]]; then
47-
# For tags, use the base version as-is (stable release)
48-
NPM_VERSION="${BASE_VERSION}"
49-
NPM_TAG="latest"
50-
echo "Publishing stable release: ${NPM_VERSION}"
51-
else
52-
# For main branch, create a pre-release version using git describe
53-
# Format: 0.3.0-next.5.g1a2b3c4 (base-next.commits.hash)
54-
GIT_COMMIT=$(git rev-parse --short HEAD)
55-
COMMITS_SINCE_TAG=$(git rev-list --count HEAD ^$(git describe --tags --abbrev=0 2>/dev/null || echo HEAD) 2>/dev/null || echo "0")
56-
NPM_VERSION="${BASE_VERSION}-next.${COMMITS_SINCE_TAG}.g${GIT_COMMIT}"
57-
NPM_TAG="next"
58-
echo "Publishing pre-release: ${NPM_VERSION}"
59-
fi
60-
61-
echo "version=${NPM_VERSION}" >> $GITHUB_OUTPUT
62-
echo "tag=${NPM_TAG}" >> $GITHUB_OUTPUT
63-
64-
# Update package.json with the new version
65-
node -e "const fs = require('fs'); const pkg = JSON.parse(fs.readFileSync('package.json')); pkg.version = '${NPM_VERSION}'; fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n');"
66-
67-
echo "Updated package.json to version ${NPM_VERSION}"
68-
69-
- name: Validate tag matches package.json version
70-
if: github.ref_type == 'tag'
71-
run: |
72-
# Extract version from package.json
73-
PKG_VERSION=$(jq -r .version package.json)
74-
75-
# Extract version from git tag (strip 'v' prefix)
76-
TAG_VERSION=${GITHUB_REF#refs/tags/v}
77-
78-
echo "Package version: $PKG_VERSION"
79-
echo "Tag version: $TAG_VERSION"
80-
81-
if [ "$PKG_VERSION" != "$TAG_VERSION" ]; then
82-
echo "❌ Error: Version mismatch!"
83-
echo " package.json version: $PKG_VERSION"
84-
echo " Git tag version: $TAG_VERSION"
85-
echo ""
86-
echo "Please ensure the git tag matches the version in package.json"
87-
exit 1
88-
fi
89-
90-
echo "✅ Version validation passed: $PKG_VERSION"
91-
9234
- name: Build WASM
9335
run: ./scripts/build-wasm.sh
9436

@@ -110,41 +52,7 @@ jobs:
11052
- name: Build library
11153
run: bun run build
11254

113-
- name: Setup Node.js with npm 11+ (for trusted publishing)
114-
uses: actions/setup-node@v4
55+
- name: Publish to NPM
56+
uses: ./.github/actions/npm-publish
11557
with:
116-
node-version: '20'
117-
registry-url: 'https://registry.npmjs.org'
118-
119-
- name: Check if version exists
120-
id: check-exists
121-
run: |
122-
PACKAGE_NAME=$(node -p "require('./package.json').name")
123-
VERSION="${{ steps.version.outputs.version }}"
124-
125-
if npm view "${PACKAGE_NAME}@${VERSION}" version &>/dev/null; then
126-
echo "exists=true" >> $GITHUB_OUTPUT
127-
echo "Version ${VERSION} already exists on npm"
128-
else
129-
echo "exists=false" >> $GITHUB_OUTPUT
130-
echo "Version ${VERSION} does not exist, will publish"
131-
fi
132-
133-
- name: Publish to npm (with OIDC trusted publishing)
134-
if: steps.check-exists.outputs.exists == 'false'
135-
run: npm publish --tag ${{ steps.version.outputs.tag }} --provenance --access public
136-
137-
- name: Update dist-tag (version already exists)
138-
if: steps.check-exists.outputs.exists == 'true' && github.ref_type == 'tag'
139-
run: |
140-
PACKAGE_NAME=$(node -p "require('./package.json').name")
141-
VERSION="${{ steps.version.outputs.version }}"
142-
TAG="${{ steps.version.outputs.tag }}"
143-
144-
echo "Version ${VERSION} already published, updating dist-tag to ${TAG}"
145-
npm dist-tag add "${PACKAGE_NAME}@${VERSION}" "${TAG}"
146-
147-
- name: Skip (pre-release already exists)
148-
if: steps.check-exists.outputs.exists == 'true' && github.ref_type != 'tag'
149-
run: |
150-
echo "⏭️ Pre-release version already exists, skipping"
58+
is-tag: 'true'

0 commit comments

Comments
 (0)