Skip to content

Release v0.11.0

Release v0.11.0 #15

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"