diff --git a/.github/actions/npm-publish/action.yml b/.github/actions/npm-publish/action.yml deleted file mode 100644 index 7256210..0000000 --- a/.github/actions/npm-publish/action.yml +++ /dev/null @@ -1,127 +0,0 @@ -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' - # Note: OIDC token must be available from parent workflow with id-token: write - - - 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: Debug npm config - shell: bash - run: | - echo "Checking npm configuration..." - npm config list - echo "ACTIONS_ID_TOKEN_REQUEST_URL: ${ACTIONS_ID_TOKEN_REQUEST_URL:-not set}" - echo "ACTIONS_ID_TOKEN_REQUEST_TOKEN: ${ACTIONS_ID_TOKEN_REQUEST_TOKEN:+is set}" - - - 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 b7e9860..c86f429 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -113,32 +113,3 @@ 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 - - - uses: oven-sh/setup-bun@v1 - with: - bun-version: latest - - - name: Install dependencies - run: bun install - - - uses: ./.github/actions/setup-zig - with: - version: 0.15.2 - - - name: Publish to NPM - uses: ./.github/actions/npm-publish diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 4b97dec..2670db9 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -4,6 +4,12 @@ on: push: tags: - 'v*' + workflow_run: + workflows: ['ci'] + types: + - completed + branches: + - main permissions: contents: read @@ -13,8 +19,10 @@ jobs: publish: name: publish to npm runs-on: ubuntu-latest + # Only run if CI workflow succeeded (for workflow_run trigger) + if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'push' }} steps: - - name: Checkout tag + - name: Checkout code uses: actions/checkout@v4 with: ref: ${{ github.ref }} @@ -52,5 +60,111 @@ jobs: - name: Build library run: bun run build - - name: Publish to NPM - uses: ./.github/actions/npm-publish + - name: Setup Node.js for npm + uses: actions/setup-node@v4 + with: + node-version: '20' + registry-url: 'https://registry.npmjs.org' + + # Ensure npm 11.5.1 or later for trusted publishing + - run: npm install -g npm@latest + + - name: Detect trigger type + id: detect + 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: Generate version + id: version + 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' + 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 + 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' + 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' + 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' + run: | + echo "⏭️ Pre-release version already exists, skipping"