mirror of
https://github.com/glittercowboy/get-shit-done
synced 2026-04-25 17:25:23 +02:00
- Update actions/checkout and actions/setup-node to v6 in release.yml and hotfix.yml (Node.js 24 compat, prevents June 2026 breakage) - Add GitHub Release creation to release finalize, release RC, and hotfix finalize steps (populates Releases page automatically) - Extend test.yml push triggers to release/** and hotfix/** branches - Extend security-scan.yml PR triggers to release/** and hotfix/** branches Closes #1955 Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
240 lines
8.3 KiB
YAML
240 lines
8.3 KiB
YAML
name: Hotfix Release
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
inputs:
|
|
action:
|
|
description: 'Action to perform'
|
|
required: true
|
|
type: choice
|
|
options:
|
|
- create
|
|
- finalize
|
|
version:
|
|
description: 'Patch version (e.g., 1.27.1)'
|
|
required: true
|
|
type: string
|
|
dry_run:
|
|
description: 'Dry run (skip npm publish, tagging, and push)'
|
|
required: false
|
|
type: boolean
|
|
default: false
|
|
|
|
concurrency:
|
|
group: hotfix-${{ inputs.version }}
|
|
cancel-in-progress: false
|
|
|
|
env:
|
|
NODE_VERSION: 24
|
|
|
|
jobs:
|
|
validate-version:
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 2
|
|
permissions:
|
|
contents: read
|
|
outputs:
|
|
base_tag: ${{ steps.validate.outputs.base_tag }}
|
|
branch: ${{ steps.validate.outputs.branch }}
|
|
steps:
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
fetch-depth: 0
|
|
|
|
- name: Validate version format
|
|
id: validate
|
|
env:
|
|
VERSION: ${{ inputs.version }}
|
|
run: |
|
|
# Must be X.Y.Z where Z > 0 (patch release)
|
|
if ! echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[1-9][0-9]*$'; then
|
|
echo "::error::Version must be a patch release (e.g., 1.27.1, not 1.28.0)"
|
|
exit 1
|
|
fi
|
|
MAJOR_MINOR=$(echo "$VERSION" | cut -d. -f1-2)
|
|
TARGET_TAG="v${VERSION}"
|
|
BRANCH="hotfix/${VERSION}"
|
|
BASE_TAG=$(git tag -l "v${MAJOR_MINOR}.*" \
|
|
| grep -E "^v[0-9]+\.[0-9]+\.[0-9]+$" \
|
|
| sort -V \
|
|
| awk -v target="$TARGET_TAG" '$1 < target { last=$1 } END { if (last != "") print last }')
|
|
if [ -z "$BASE_TAG" ]; then
|
|
echo "::error::No prior stable tag found for ${MAJOR_MINOR}.x before $TARGET_TAG"
|
|
exit 1
|
|
fi
|
|
echo "base_tag=$BASE_TAG" >> "$GITHUB_OUTPUT"
|
|
echo "branch=$BRANCH" >> "$GITHUB_OUTPUT"
|
|
|
|
create:
|
|
needs: validate-version
|
|
if: inputs.action == 'create'
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 5
|
|
permissions:
|
|
contents: write
|
|
steps:
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
fetch-depth: 0
|
|
|
|
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
|
with:
|
|
node-version: ${{ env.NODE_VERSION }}
|
|
|
|
- name: Check branch doesn't already exist
|
|
env:
|
|
BRANCH: ${{ needs.validate-version.outputs.branch }}
|
|
run: |
|
|
if git ls-remote --exit-code origin "refs/heads/$BRANCH" >/dev/null 2>&1; then
|
|
echo "::error::Branch $BRANCH already exists. Delete it first or use finalize."
|
|
exit 1
|
|
fi
|
|
|
|
- name: Configure git identity
|
|
run: |
|
|
git config user.name "github-actions[bot]"
|
|
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
|
|
- name: Create hotfix branch
|
|
if: inputs.dry_run != 'true'
|
|
env:
|
|
BRANCH: ${{ needs.validate-version.outputs.branch }}
|
|
BASE_TAG: ${{ needs.validate-version.outputs.base_tag }}
|
|
VERSION: ${{ inputs.version }}
|
|
run: |
|
|
git checkout -b "$BRANCH" "$BASE_TAG"
|
|
# Bump version in package.json
|
|
npm version "$VERSION" --no-git-tag-version
|
|
git add package.json package-lock.json
|
|
git commit -m "chore: bump version to $VERSION for hotfix"
|
|
git push origin "$BRANCH"
|
|
echo "## Hotfix branch created" >> "$GITHUB_STEP_SUMMARY"
|
|
echo "- Branch: \`$BRANCH\`" >> "$GITHUB_STEP_SUMMARY"
|
|
echo "- Based on: \`$BASE_TAG\`" >> "$GITHUB_STEP_SUMMARY"
|
|
echo "- Apply your fix, push, then run this workflow again with \`finalize\`" >> "$GITHUB_STEP_SUMMARY"
|
|
|
|
finalize:
|
|
needs: validate-version
|
|
if: inputs.action == 'finalize'
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 10
|
|
permissions:
|
|
contents: write
|
|
pull-requests: write
|
|
id-token: write
|
|
environment: npm-publish
|
|
steps:
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
ref: ${{ needs.validate-version.outputs.branch }}
|
|
fetch-depth: 0
|
|
|
|
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
|
with:
|
|
node-version: ${{ env.NODE_VERSION }}
|
|
registry-url: 'https://registry.npmjs.org'
|
|
cache: 'npm'
|
|
|
|
- name: Configure git identity
|
|
run: |
|
|
git config user.name "github-actions[bot]"
|
|
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
|
|
- name: Install and test
|
|
run: |
|
|
npm ci
|
|
npm run test:coverage
|
|
|
|
- name: Create PR to merge hotfix back to main
|
|
if: ${{ !inputs.dry_run }}
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
BRANCH: ${{ needs.validate-version.outputs.branch }}
|
|
VERSION: ${{ inputs.version }}
|
|
run: |
|
|
EXISTING_PR=$(gh pr list --base main --head "$BRANCH" --state open --json number --jq '.[0].number')
|
|
if [ -n "$EXISTING_PR" ]; then
|
|
echo "PR #$EXISTING_PR already exists; updating"
|
|
gh pr edit "$EXISTING_PR" \
|
|
--title "chore: merge hotfix v${VERSION} back to main" \
|
|
--body "Merge hotfix changes back to main after v${VERSION} release."
|
|
else
|
|
gh pr create \
|
|
--base main \
|
|
--head "$BRANCH" \
|
|
--title "chore: merge hotfix v${VERSION} back to main" \
|
|
--body "Merge hotfix changes back to main after v${VERSION} release."
|
|
fi
|
|
|
|
- name: Tag and push
|
|
if: ${{ !inputs.dry_run }}
|
|
env:
|
|
VERSION: ${{ inputs.version }}
|
|
run: |
|
|
if git rev-parse -q --verify "refs/tags/v${VERSION}" >/dev/null; then
|
|
EXISTING_SHA=$(git rev-parse "refs/tags/v${VERSION}")
|
|
HEAD_SHA=$(git rev-parse HEAD)
|
|
if [ "$EXISTING_SHA" != "$HEAD_SHA" ]; then
|
|
echo "::error::Tag v${VERSION} already exists pointing to different commit"
|
|
exit 1
|
|
fi
|
|
echo "Tag v${VERSION} already exists on current commit; skipping"
|
|
else
|
|
git tag "v${VERSION}"
|
|
git push origin "v${VERSION}"
|
|
fi
|
|
|
|
- name: Publish to npm (latest)
|
|
if: ${{ !inputs.dry_run }}
|
|
run: npm publish --provenance --access public
|
|
env:
|
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
|
|
- name: Create GitHub Release
|
|
if: ${{ !inputs.dry_run }}
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
VERSION: ${{ inputs.version }}
|
|
run: |
|
|
gh release create "v${VERSION}" \
|
|
--title "v${VERSION} (hotfix)" \
|
|
--generate-notes
|
|
|
|
- name: Clean up next dist-tag
|
|
if: ${{ !inputs.dry_run }}
|
|
env:
|
|
VERSION: ${{ inputs.version }}
|
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
run: |
|
|
# Point next to the stable release so @next never returns something
|
|
# older than @latest. This prevents stale pre-release installs.
|
|
npm dist-tag add "get-shit-done-cc@${VERSION}" next 2>/dev/null || true
|
|
echo "✓ next dist-tag updated to v${VERSION}"
|
|
|
|
- name: Verify publish
|
|
if: ${{ !inputs.dry_run }}
|
|
env:
|
|
VERSION: ${{ inputs.version }}
|
|
run: |
|
|
sleep 10
|
|
PUBLISHED=$(npm view get-shit-done-cc@"$VERSION" version 2>/dev/null || echo "NOT_FOUND")
|
|
if [ "$PUBLISHED" != "$VERSION" ]; then
|
|
echo "::error::Published version verification failed. Expected $VERSION, got $PUBLISHED"
|
|
exit 1
|
|
fi
|
|
echo "✓ Verified: get-shit-done-cc@$VERSION is live on npm"
|
|
|
|
- name: Summary
|
|
env:
|
|
VERSION: ${{ inputs.version }}
|
|
DRY_RUN: ${{ inputs.dry_run }}
|
|
run: |
|
|
echo "## Hotfix v${VERSION}" >> "$GITHUB_STEP_SUMMARY"
|
|
if [ "$DRY_RUN" = "true" ]; then
|
|
echo "**DRY RUN** — npm publish, tagging, and push skipped" >> "$GITHUB_STEP_SUMMARY"
|
|
else
|
|
echo "- Published to npm as \`latest\`" >> "$GITHUB_STEP_SUMMARY"
|
|
echo "- Tagged \`v${VERSION}\`" >> "$GITHUB_STEP_SUMMARY"
|
|
echo "- PR created to merge back to main" >> "$GITHUB_STEP_SUMMARY"
|
|
fi
|