diff --git a/.github/actions/npm-publish/action.yml b/.github/actions/npm-publish/action.yml new file mode 100644 index 00000000..8f19b417 --- /dev/null +++ b/.github/actions/npm-publish/action.yml @@ -0,0 +1,118 @@ +name: Publish to NPM +description: Publish package to NPM with automatic version generation and idempotent checks + +runs: + using: composite + steps: + - name: Detect trigger type + id: detect + shell: bash + run: | + if [[ $GITHUB_REF == refs/tags/* ]]; then + echo "is_tag=true" >> $GITHUB_OUTPUT + echo "trigger_type=tag" >> $GITHUB_OUTPUT + echo "trigger_name=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT + echo "📦 Detected tag push: ${GITHUB_REF#refs/tags/}" + else + echo "is_tag=false" >> $GITHUB_OUTPUT + echo "trigger_type=branch" >> $GITHUB_OUTPUT + echo "trigger_name=${GITHUB_REF#refs/heads/}" >> $GITHUB_OUTPUT + echo "🚧 Detected branch push: ${GITHUB_REF#refs/heads/}" + fi + + - name: Setup Node.js with npm 11+ (for trusted publishing) + uses: actions/setup-node@v4 + with: + node-version: '20' + registry-url: 'https://registry.npmjs.org' + + - name: Generate version + id: version + shell: bash + run: | + # Get base version from package.json + BASE_VERSION=$(jq -r .version package.json) + + if [[ "${{ steps.detect.outputs.is_tag }}" == "true" ]]; then + # For tags, use the base version as-is (stable release) + NPM_VERSION="${BASE_VERSION}" + NPM_TAG="latest" + echo "📦 Publishing stable release: ${NPM_VERSION}" + else + # For main branch, create a pre-release version using git describe + # Format: 0.3.0-next.5.g1a2b3c4 (base-next.commits.hash) + GIT_COMMIT=$(git rev-parse --short HEAD) + COMMITS_SINCE_TAG=$(git rev-list --count HEAD ^$(git describe --tags --abbrev=0 2>/dev/null || echo HEAD) 2>/dev/null || echo "0") + NPM_VERSION="${BASE_VERSION}-next.${COMMITS_SINCE_TAG}.g${GIT_COMMIT}" + NPM_TAG="next" + echo "🚧 Publishing pre-release: ${NPM_VERSION}" + fi + + echo "version=${NPM_VERSION}" >> $GITHUB_OUTPUT + echo "tag=${NPM_TAG}" >> $GITHUB_OUTPUT + + # Update package.json with the new version + 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');" + + echo "Updated package.json to version ${NPM_VERSION}" + + - name: Validate tag matches package.json version + if: steps.detect.outputs.is_tag == 'true' + shell: bash + run: | + # Extract version from package.json + PKG_VERSION=$(jq -r .version package.json) + + # Extract version from git tag (strip 'v' prefix) + TAG_VERSION=${GITHUB_REF#refs/tags/v} + + echo "Package version: $PKG_VERSION" + echo "Tag version: $TAG_VERSION" + + if [ "$PKG_VERSION" != "$TAG_VERSION" ]; then + echo "❌ Error: Version mismatch!" + echo " package.json version: $PKG_VERSION" + echo " Git tag version: $TAG_VERSION" + echo "" + echo "Please ensure the git tag matches the version in package.json" + exit 1 + fi + + echo "✅ Version validation passed: $PKG_VERSION" + + - name: Check if version exists + id: check-exists + shell: bash + run: | + PACKAGE_NAME=$(node -p "require('./package.json').name") + VERSION="${{ steps.version.outputs.version }}" + + if npm view "${PACKAGE_NAME}@${VERSION}" version &>/dev/null; then + echo "exists=true" >> $GITHUB_OUTPUT + echo "Version ${VERSION} already exists on npm" + else + echo "exists=false" >> $GITHUB_OUTPUT + echo "Version ${VERSION} does not exist, will publish" + fi + + - name: Publish to npm (with OIDC trusted publishing) + if: steps.check-exists.outputs.exists == 'false' + shell: bash + run: npm publish --tag ${{ steps.version.outputs.tag }} --provenance --access public + + - name: Update dist-tag (version already exists) + if: steps.check-exists.outputs.exists == 'true' && steps.detect.outputs.is_tag == 'true' + shell: bash + run: | + PACKAGE_NAME=$(node -p "require('./package.json').name") + VERSION="${{ steps.version.outputs.version }}" + TAG="${{ steps.version.outputs.tag }}" + + echo "Version ${VERSION} already published, updating dist-tag to ${TAG}" + npm dist-tag add "${PACKAGE_NAME}@${VERSION}" "${TAG}" + + - name: Skip (pre-release already exists) + if: steps.check-exists.outputs.exists == 'true' && steps.detect.outputs.is_tag != 'true' + shell: bash + run: | + echo "⏭️ Pre-release version already exists, skipping" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c86f4298..328ba1ad 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -113,3 +113,21 @@ jobs: - name: Build library run: bun run build + + publish: + name: publish to npm + runs-on: ubuntu-latest + # Only publish on main branch pushes after all checks pass + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + needs: [fmt, lint, typecheck, test, build] + permissions: + contents: read + id-token: write # Required for OIDC trusted publishing + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Required for git describe to find tags + + - name: Publish to NPM + uses: ./.github/actions/npm-publish diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 6292e04c..4b97dec6 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -5,18 +5,20 @@ on: tags: - 'v*' +permissions: + contents: read + id-token: write # Required for OIDC trusted publishing + jobs: publish: name: publish to npm runs-on: ubuntu-latest - permissions: - contents: read - id-token: write # Required for OIDC trusted publishing steps: - name: Checkout tag uses: actions/checkout@v4 with: ref: ${{ github.ref }} + fetch-depth: 0 # Required for git describe to find tags submodules: recursive - name: Setup Bun @@ -29,28 +31,6 @@ jobs: with: version: 0.15.2 - - name: Validate tag matches package.json version - run: | - # Extract version from package.json - PKG_VERSION=$(jq -r .version package.json) - - # Extract version from git tag (strip 'v' prefix) - TAG_VERSION=${GITHUB_REF#refs/tags/v} - - echo "Package version: $PKG_VERSION" - echo "Tag version: $TAG_VERSION" - - if [ "$PKG_VERSION" != "$TAG_VERSION" ]; then - echo "❌ Error: Version mismatch!" - echo " package.json version: $PKG_VERSION" - echo " Git tag version: $TAG_VERSION" - echo "" - echo "Please ensure the git tag matches the version in package.json" - exit 1 - fi - - echo "✅ Version validation passed: $PKG_VERSION" - - name: Build WASM run: ./scripts/build-wasm.sh @@ -72,11 +52,5 @@ jobs: - name: Build library run: bun run build - - name: Setup Node.js with npm 11+ (for trusted publishing) - uses: actions/setup-node@v4 - with: - node-version: '20' - registry-url: 'https://registry.npmjs.org' - - - name: Publish to npm (with OIDC trusted publishing) - run: npm publish --provenance --access public + - name: Publish to NPM + uses: ./.github/actions/npm-publish