Publish to npm #27
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Publish to npm | |
| concurrency: | |
| group: publish-${{ github.event.workflow_run.head_sha || github.sha }} | |
| cancel-in-progress: true | |
| on: | |
| workflow_run: | |
| workflows: | |
| - CI | |
| types: | |
| - completed | |
| workflow_dispatch: | |
| inputs: | |
| tag: | |
| description: 'npm tag (latest, beta, next)' | |
| required: false | |
| default: 'latest' | |
| type: choice | |
| options: | |
| - latest | |
| - beta | |
| - next | |
| jobs: | |
| prepare: | |
| if: github.event_name == 'workflow_dispatch' || (github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'push' && !github.event.workflow_run.head_branch) | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| outputs: | |
| should_publish: ${{ steps.meta.outputs.should_publish }} | |
| release_tag: ${{ steps.meta.outputs.release_tag }} | |
| npm_tag: ${{ steps.meta.outputs.npm_tag }} | |
| prerelease: ${{ steps.meta.outputs.prerelease }} | |
| version: ${{ steps.meta.outputs.version }} | |
| ref_sha: ${{ steps.meta.outputs.ref_sha }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v5 | |
| with: | |
| fetch-depth: 0 | |
| fetch-tags: true | |
| ref: ${{ github.event.workflow_run.head_sha || github.sha }} | |
| - name: Determine release metadata | |
| id: meta | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| REF_SHA="${{ github.event.workflow_run.head_sha || github.sha }}" | |
| VERSION="$(node -p "require('./package.json').version")" | |
| if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then | |
| RELEASE_TAG="v${VERSION}" | |
| NPM_TAG="${{ inputs.tag }}" | |
| SHOULD_PUBLISH="true" | |
| if echo "$VERSION" | grep -q "-"; then | |
| PRERELEASE="true" | |
| else | |
| PRERELEASE="false" | |
| fi | |
| else | |
| RELEASE_TAG="$(git tag --points-at "$REF_SHA" | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+([.-].*)?$' | head -n1 || true)" | |
| if [ -z "$RELEASE_TAG" ]; then | |
| echo "No semver tag found for commit $REF_SHA. Skipping publish." | |
| SHOULD_PUBLISH="false" | |
| NPM_TAG="" | |
| PRERELEASE="false" | |
| else | |
| SHOULD_PUBLISH="true" | |
| if echo "$VERSION" | grep -q "-"; then | |
| NPM_TAG="next" | |
| PRERELEASE="true" | |
| else | |
| NPM_TAG="latest" | |
| PRERELEASE="false" | |
| fi | |
| fi | |
| fi | |
| echo "should_publish=$SHOULD_PUBLISH" >> "$GITHUB_OUTPUT" | |
| echo "release_tag=$RELEASE_TAG" >> "$GITHUB_OUTPUT" | |
| echo "npm_tag=$NPM_TAG" >> "$GITHUB_OUTPUT" | |
| echo "prerelease=$PRERELEASE" >> "$GITHUB_OUTPUT" | |
| echo "version=$VERSION" >> "$GITHUB_OUTPUT" | |
| echo "ref_sha=$REF_SHA" >> "$GITHUB_OUTPUT" | |
| publish: | |
| if: needs.prepare.outputs.should_publish == 'true' | |
| needs: prepare | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| packages: write | |
| id-token: write | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v5 | |
| with: | |
| ref: ${{ needs.prepare.outputs.ref_sha }} | |
| fetch-depth: 0 | |
| fetch-tags: true | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: '24' | |
| registry-url: 'https://registry.npmjs.org' | |
| - name: Enable Corepack (Yarn 3) | |
| run: | | |
| corepack enable | |
| corepack prepare yarn@3.6.1 --activate | |
| - name: Install dependencies | |
| run: yarn install --immutable | |
| - name: Run tests | |
| run: yarn test | |
| - name: Lint | |
| run: yarn lint | |
| - name: Typecheck | |
| run: yarn typecheck | |
| - name: Build package | |
| run: yarn build | |
| - name: Check if version already exists on npm | |
| id: check_version | |
| run: | | |
| PKG_VERSION="${{ needs.prepare.outputs.version }}" | |
| if npm view "$(node -p "require('./package.json').name")@${PKG_VERSION}" version >/dev/null 2>&1; then | |
| echo "exists=true" >> "$GITHUB_OUTPUT" | |
| echo "Version ${PKG_VERSION} already exists on npm, skipping publish" | |
| else | |
| echo "exists=false" >> "$GITHUB_OUTPUT" | |
| echo "Version ${PKG_VERSION} does not exist on npm, will publish" | |
| fi | |
| - name: Publish to npm | |
| if: steps.check_version.outputs.exists == 'false' | |
| run: npm publish --provenance --tag ${{ needs.prepare.outputs.npm_tag }} | |
| - name: Extract changelog for current version | |
| id: changelog | |
| run: | | |
| VERSION="${{ needs.prepare.outputs.version }}" | |
| if [ -f "CHANGELOG.md" ]; then | |
| CHANGELOG=$(awk "/^## .*${VERSION}/,/^## [^#]/" CHANGELOG.md | sed '1d;$d') | |
| if [ -n "$CHANGELOG" ]; then | |
| echo "found=true" >> "$GITHUB_OUTPUT" | |
| echo "$CHANGELOG" > /tmp/changelog.txt | |
| else | |
| echo "found=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| else | |
| echo "found=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Create GitHub Release | |
| uses: actions/github-script@v8 | |
| env: | |
| TAG: ${{ needs.prepare.outputs.release_tag }} | |
| PRERELEASE: ${{ needs.prepare.outputs.prerelease }} | |
| CHANGELOG_FOUND: ${{ steps.changelog.outputs.found }} | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| const { TAG, PRERELEASE, CHANGELOG_FOUND } = process.env; | |
| let body = ''; | |
| if (CHANGELOG_FOUND === 'true') { | |
| try { | |
| body = fs.readFileSync('/tmp/changelog.txt', 'utf8').trim(); | |
| core.info('Using changelog from CHANGELOG.md'); | |
| } catch (error) { | |
| core.warning('Could not read changelog file'); | |
| } | |
| } | |
| if (!body) { | |
| core.info('Generating changelog from commits'); | |
| const tags = await github.rest.repos.listTags({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| per_page: 100 | |
| }); | |
| const currentIndex = tags.data.findIndex(t => t.name === TAG); | |
| const previousTag = currentIndex >= 0 && currentIndex < tags.data.length - 1 | |
| ? tags.data[currentIndex + 1].name | |
| : null; | |
| if (previousTag) { | |
| const comparison = await github.rest.repos.compareCommits({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| base: previousTag, | |
| head: TAG | |
| }); | |
| body = '### Changes\n\n'; | |
| comparison.data.commits.forEach(commit => { | |
| const message = commit.commit.message.split('\n')[0]; | |
| const sha = commit.sha.substring(0, 7); | |
| body += `- ${message} ([${sha}](${commit.html_url}))\n`; | |
| }); | |
| body += `\n**Full Changelog**: https://github.com/${context.repo.owner}/${context.repo.repo}/compare/${previousTag}...${TAG}`; | |
| } | |
| } | |
| if (!body) { | |
| body = `Release ${TAG}`; | |
| } | |
| await github.rest.repos.createRelease({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| tag_name: TAG, | |
| name: TAG, | |
| body, | |
| draft: false, | |
| prerelease: PRERELEASE === 'true', | |
| }); | |
| core.info(`GitHub release created for ${TAG}`); |