Repository navigation
Release v0.11.0 #15
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: Maintainer Release Command | |
| on: | |
| issues: | |
| types: [opened] | |
| permissions: | |
| contents: write | |
| issues: write | |
| concurrency: | |
| group: maintainer-release | |
| cancel-in-progress: false | |
| jobs: | |
| release: | |
| if: github.actor == github.repository_owner && startsWith(github.event.issue.title, 'Release v') | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 25 | |
| outputs: | |
| tag: ${{ steps.release.outputs.tag }} | |
| steps: | |
| - name: Check out main | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: main | |
| fetch-depth: 0 | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 22 | |
| - name: Parse and validate release command | |
| id: release | |
| env: | |
| COMMAND: ${{ github.event.issue.body }} | |
| ISSUE_TITLE: ${{ github.event.issue.title }} | |
| shell: bash | |
| run: | | |
| if [[ ! "$COMMAND" =~ ^/release[[:space:]]+(v[0-9]+\.[0-9]+\.[0-9]+)[[:space:]]*$ ]]; then | |
| echo "::error::Issue body must contain only: /release vMAJOR.MINOR.PATCH" | |
| exit 1 | |
| fi | |
| TAG="${BASH_REMATCH[1]}" | |
| if [ "$ISSUE_TITLE" != "Release $TAG" ]; then | |
| echo "::error::Issue title must be exactly: Release $TAG" | |
| exit 1 | |
| fi | |
| PACKAGE_VERSION=$(node -p "require('./package.json').version") | |
| TAG_VERSION="${TAG#v}" | |
| if [ "$PACKAGE_VERSION" != "$TAG_VERSION" ]; then | |
| echo "::error::package.json version ($PACKAGE_VERSION) does not match requested tag ($TAG_VERSION)" | |
| exit 1 | |
| fi | |
| if git ls-remote --exit-code --tags origin "refs/tags/$TAG" >/dev/null 2>&1; then | |
| echo "tag_exists=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "tag_exists=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| echo "tag=$TAG" >> "$GITHUB_OUTPUT" | |
| - name: Run complete release validation | |
| run: npm run release:validate | |
| - name: Create and push annotated release tag | |
| if: steps.release.outputs.tag_exists != 'true' | |
| env: | |
| TAG: ${{ steps.release.outputs.tag }} | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| git tag -a "$TAG" -m "Development Kit $TAG" | |
| git push origin "$TAG" | |
| - name: Verify existing release tag | |
| if: steps.release.outputs.tag_exists == 'true' | |
| env: | |
| TAG: ${{ steps.release.outputs.tag }} | |
| run: | | |
| git fetch origin "refs/tags/$TAG:refs/tags/$TAG" --force | |
| TAG_COMMIT=$(git rev-list -n 1 "$TAG") | |
| if ! git merge-base --is-ancestor "$TAG_COMMIT" HEAD; then | |
| echo "::error::Existing tag $TAG is not contained in the current main branch" | |
| exit 1 | |
| fi | |
| - name: Report release failure | |
| if: failure() || cancelled() | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| ISSUE_NUMBER: ${{ github.event.issue.number }} | |
| run: | | |
| gh issue comment "$ISSUE_NUMBER" --body "Release workflow failed or timed out during validation/tagging. Check GitHub Actions run logs for details." | |
| gh issue close "$ISSUE_NUMBER" --reason "not planned" | |
| publish: | |
| needs: release | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| environment: npm | |
| permissions: | |
| contents: write | |
| issues: write | |
| steps: | |
| - name: Check out release tag | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ needs.release.outputs.tag }} | |
| fetch-depth: 0 | |
| - name: Set up Node.js for npm | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 22 | |
| registry-url: https://registry.npmjs.org | |
| - name: Verify release target | |
| env: | |
| TAG: ${{ needs.release.outputs.tag }} | |
| run: | | |
| TAG_VERSION="${TAG#v}" | |
| PKG_VERSION=$(node -p "require('./package.json').version") | |
| if [ "$TAG_VERSION" != "$PKG_VERSION" ]; then | |
| echo "::error::Tag version ($TAG_VERSION) does not match package.json version ($PKG_VERSION)" | |
| exit 1 | |
| fi | |
| TAG_COMMIT=$(git rev-list -n 1 "$TAG") | |
| HEAD_COMMIT=$(git rev-parse HEAD) | |
| if [ "$TAG_COMMIT" != "$HEAD_COMMIT" ]; then | |
| echo "::error::Checked-out commit ($HEAD_COMMIT) does not match release tag commit ($TAG_COMMIT)" | |
| exit 1 | |
| fi | |
| - name: Create or verify GitHub Release | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| TAG: ${{ needs.release.outputs.tag }} | |
| run: | | |
| if gh release view "$TAG" >/dev/null 2>&1; then | |
| echo "GitHub Release $TAG already exists." | |
| else | |
| gh release create "$TAG" --verify-tag --generate-notes --title "Development Kit $TAG" | |
| fi | |
| - name: Inspect npm publication state | |
| id: npm-state | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| shell: bash | |
| run: | | |
| PACKAGE_NAME=$(node -p "require('./package.json').name") | |
| PACKAGE_VERSION=$(node -p "require('./package.json').version") | |
| if PUBLISHED_VERSION=$(npm view "$PACKAGE_NAME@$PACKAGE_VERSION" version --silent 2>/dev/null) && [ "$PUBLISHED_VERSION" = "$PACKAGE_VERSION" ]; then | |
| echo "published=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "published=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| if [ -n "$NODE_AUTH_TOKEN" ]; then | |
| echo "credentials=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "credentials=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Publish package | |
| if: steps.npm-state.outputs.published != 'true' && steps.npm-state.outputs.credentials == 'true' | |
| run: npm publish --access public | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| - name: Verify npm publication | |
| id: npm-result | |
| env: | |
| PREVIOUSLY_PUBLISHED: ${{ steps.npm-state.outputs.published }} | |
| CREDENTIALS_CONFIGURED: ${{ steps.npm-state.outputs.credentials }} | |
| shell: bash | |
| run: | | |
| PACKAGE_NAME=$(node -p "require('./package.json').name") | |
| PACKAGE_VERSION=$(node -p "require('./package.json').version") | |
| if [ "$PREVIOUSLY_PUBLISHED" = "true" ]; then | |
| echo "status=already-published" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| if [ "$CREDENTIALS_CONFIGURED" != "true" ]; then | |
| echo "status=skipped-no-token" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| for attempt in {1..12}; do | |
| PUBLISHED_VERSION=$(npm view "$PACKAGE_NAME@$PACKAGE_VERSION" version --silent 2>/dev/null || true) | |
| if [ "$PUBLISHED_VERSION" = "$PACKAGE_VERSION" ]; then | |
| echo "status=published" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| echo "npm registry has not exposed $PACKAGE_NAME@$PACKAGE_VERSION yet (attempt $attempt/12)." | |
| if [ "$attempt" -lt 12 ]; then | |
| sleep 5 | |
| fi | |
| done | |
| echo "::error::npm verification failed after 12 attempts: expected $PACKAGE_VERSION" | |
| exit 1 | |
| - name: Report completed release | |
| if: success() | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| ISSUE_NUMBER: ${{ github.event.issue.number }} | |
| TAG: ${{ needs.release.outputs.tag }} | |
| NPM_STATUS: ${{ steps.npm-result.outputs.status }} | |
| run: | | |
| gh issue comment "$ISSUE_NUMBER" --body "Release \`$TAG\` completed. GitHub Release: confirmed. npm status: \`$NPM_STATUS\`." | |
| gh issue close "$ISSUE_NUMBER" --reason completed | |
| - name: Report publish failure | |
| if: failure() || cancelled() | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| ISSUE_NUMBER: ${{ github.event.issue.number }} | |
| run: | | |
| gh issue comment "$ISSUE_NUMBER" --body "Release workflow failed or timed out during GitHub Release/npm publication. Check GitHub Actions run logs for details." | |
| gh issue close "$ISSUE_NUMBER" --reason "not planned" |