mirror of
https://github.com/glittercowboy/get-shit-done
synced 2026-04-25 17:25:23 +02:00
* feat: 3-tier release strategy with hotfix, release, and CI workflows Supersedes PRs #1208 and #1210 with a consolidated approach: - VERSIONING.md: Strategy document with 3 release tiers (patch/minor/major) - hotfix.yml: Emergency patch releases to latest - release.yml: Standard release cycle with RC/beta pre-releases to next - auto-branch.yml: Create branches from issue labels - branch-naming.yml: Convention validation (advisory) - pr-gate.yml: PR size analysis and labeling - stale.yml: Weekly cleanup of inactive issues/PRs - dependabot.yml: Automated dependency updates npm dist-tags: latest (stable) and next (pre-release) only, following Angular/Next.js convention. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address PR review findings for release workflow security and correctness - Move all ${{ }} expression interpolation from run: blocks into env: mappings in both hotfix.yml (~12 instances) and release.yml (~16 instances) to prevent potential command injection via GitHub Actions expression evaluation - Reorder rc job in release.yml to run npm ci and test:coverage before pushing the git tag, preventing broken tagged commits when tests fail - Update VERSIONING.md to accurately describe the implementation: major releases use beta pre-releases only, minor releases use rc pre-releases only (no beta-then-rc progression) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * security: harden release workflows — SHA pinning, provenance, dry-run guards Addresses deep adversarial review + best practices research: HIGH: - Fix release.yml rc/finalize: dry_run now gates tag+push (not just npm publish) - Fix hotfix.yml finalize: reorder tag-before-publish (was publish-before-tag) MEDIUM — Security hardening: - Pin ALL actions to SHA hashes (actions/checkout@11bd7190, actions/setup-node@39370e39, actions/github-script@60a0d830) - Add --provenance --access public to all npm publish commands - Add id-token: write permission for npm provenance OIDC - Add concurrency groups (cancel-in-progress: false) on both workflows - Add branch-naming.yml permissions: {} (deny-all default) - Scope permissions per-job instead of workflow-level where possible MEDIUM — Reliability: - Add post-publish verification (npm view + dist-tag check) after every publish - Add npm publish --dry-run validation step before actual publish - Add branch existence pre-flight check in create jobs LOW: - Fix VERSIONING.md Semver Rules: MINOR = "enhancements" not "new features" (aligns with Release Tiers table) Tests: 1166/1166 pass Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * security: pin actions/stale to SHA hash Last remaining action using a mutable version tag. Now all actions across all workflow files are pinned to immutable SHA hashes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address all Copilot review findings on release strategy workflows - Configure git identity in all committing jobs (hotfix + release) - Base hotfix on latest patch tag instead of vX.Y.0 - Add issues: write permission for PR size labeling - Remove stale size labels before adding new one - Make tagging and PR creation idempotent for reruns - Run dry-run publish validation unconditionally - Paginate listFiles for large PRs - Fix VERSIONING.md table formatting and docs accuracy Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: clean up next dist-tag after finalize in release and hotfix workflows After finalizing a release, the next dist-tag was left pointing at the last RC pre-release. Anyone running npm install @next would get a stale version older than @latest. Now both workflows point next to the stable release after finalize, matching Angular/Next.js convention. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(ci): address blocking issues in 3-tier release workflows - Move back-merge PR creation before npm publish in hotfix/release finalize - Move version bump commit after test step in rc workflow - Gate hotfix create branch push behind dry_run check - Add confirmed-bug and confirmed to stale.yml exempt labels - Fix auto-branch priority: critical prefix collision with hotfix/ naming Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
375 lines
13 KiB
YAML
375 lines
13 KiB
YAML
name: Release
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
inputs:
|
|
action:
|
|
description: 'Action to perform'
|
|
required: true
|
|
type: choice
|
|
options:
|
|
- create
|
|
- rc
|
|
- finalize
|
|
version:
|
|
description: 'Version (e.g., 1.28.0 or 2.0.0)'
|
|
required: true
|
|
type: string
|
|
dry_run:
|
|
description: 'Dry run (skip npm publish, tagging, and push)'
|
|
required: false
|
|
type: boolean
|
|
default: false
|
|
|
|
concurrency:
|
|
group: release-${{ inputs.version }}
|
|
cancel-in-progress: false
|
|
|
|
env:
|
|
NODE_VERSION: 24
|
|
|
|
jobs:
|
|
validate-version:
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 2
|
|
permissions:
|
|
contents: read
|
|
outputs:
|
|
branch: ${{ steps.validate.outputs.branch }}
|
|
is_major: ${{ steps.validate.outputs.is_major }}
|
|
steps:
|
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
with:
|
|
fetch-depth: 0
|
|
|
|
- name: Validate version format
|
|
id: validate
|
|
env:
|
|
VERSION: ${{ inputs.version }}
|
|
run: |
|
|
# Must be X.Y.0 (minor or major release, not patch)
|
|
if ! echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.0$'; then
|
|
echo "::error::Version must end in .0 (e.g., 1.28.0 or 2.0.0). Use hotfix workflow for patch releases."
|
|
exit 1
|
|
fi
|
|
BRANCH="release/${VERSION}"
|
|
# Detect major (X.0.0)
|
|
IS_MAJOR="false"
|
|
if echo "$VERSION" | grep -qE '^[0-9]+\.0\.0$'; then
|
|
IS_MAJOR="true"
|
|
fi
|
|
echo "branch=$BRANCH" >> "$GITHUB_OUTPUT"
|
|
echo "is_major=$IS_MAJOR" >> "$GITHUB_OUTPUT"
|
|
|
|
create:
|
|
needs: validate-version
|
|
if: inputs.action == 'create'
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 5
|
|
permissions:
|
|
contents: write
|
|
steps:
|
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
with:
|
|
fetch-depth: 0
|
|
|
|
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.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 rc/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 release branch
|
|
env:
|
|
BRANCH: ${{ needs.validate-version.outputs.branch }}
|
|
VERSION: ${{ inputs.version }}
|
|
IS_MAJOR: ${{ needs.validate-version.outputs.is_major }}
|
|
run: |
|
|
git checkout -b "$BRANCH"
|
|
npm version "$VERSION" --no-git-tag-version
|
|
git add package.json package-lock.json
|
|
git commit -m "chore: bump version to ${VERSION} for release"
|
|
git push origin "$BRANCH"
|
|
echo "## Release branch created" >> "$GITHUB_STEP_SUMMARY"
|
|
echo "- Branch: \`$BRANCH\`" >> "$GITHUB_STEP_SUMMARY"
|
|
echo "- Version: \`$VERSION\`" >> "$GITHUB_STEP_SUMMARY"
|
|
if [ "$IS_MAJOR" = "true" ]; then
|
|
echo "- Type: **Major** (will start with beta pre-releases)" >> "$GITHUB_STEP_SUMMARY"
|
|
else
|
|
echo "- Type: **Minor** (will start with RC pre-releases)" >> "$GITHUB_STEP_SUMMARY"
|
|
fi
|
|
echo "" >> "$GITHUB_STEP_SUMMARY"
|
|
echo "Next: run this workflow with \`rc\` action to publish a pre-release to \`next\`" >> "$GITHUB_STEP_SUMMARY"
|
|
|
|
rc:
|
|
needs: validate-version
|
|
if: inputs.action == 'rc'
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 10
|
|
permissions:
|
|
contents: write
|
|
id-token: write
|
|
environment: npm-publish
|
|
steps:
|
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
with:
|
|
ref: ${{ needs.validate-version.outputs.branch }}
|
|
fetch-depth: 0
|
|
|
|
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
|
|
with:
|
|
node-version: ${{ env.NODE_VERSION }}
|
|
registry-url: 'https://registry.npmjs.org'
|
|
cache: 'npm'
|
|
|
|
- name: Determine pre-release version
|
|
id: prerelease
|
|
env:
|
|
VERSION: ${{ inputs.version }}
|
|
IS_MAJOR: ${{ needs.validate-version.outputs.is_major }}
|
|
run: |
|
|
# Determine pre-release type: major → beta, minor → rc
|
|
if [ "$IS_MAJOR" = "true" ]; then
|
|
PREFIX="beta"
|
|
else
|
|
PREFIX="rc"
|
|
fi
|
|
# Find next pre-release number by checking existing tags
|
|
N=1
|
|
while git tag -l "v${VERSION}-${PREFIX}.${N}" | grep -q .; do
|
|
N=$((N + 1))
|
|
done
|
|
PRE_VERSION="${VERSION}-${PREFIX}.${N}"
|
|
echo "pre_version=$PRE_VERSION" >> "$GITHUB_OUTPUT"
|
|
echo "prefix=$PREFIX" >> "$GITHUB_OUTPUT"
|
|
|
|
- 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: Bump to pre-release version
|
|
env:
|
|
PRE_VERSION: ${{ steps.prerelease.outputs.pre_version }}
|
|
run: |
|
|
npm version "$PRE_VERSION" --no-git-tag-version
|
|
|
|
- name: Install and test
|
|
run: |
|
|
npm ci
|
|
npm run test:coverage
|
|
|
|
- name: Commit pre-release version bump
|
|
env:
|
|
PRE_VERSION: ${{ steps.prerelease.outputs.pre_version }}
|
|
run: |
|
|
git add package.json package-lock.json
|
|
git commit -m "chore: bump to ${PRE_VERSION}"
|
|
|
|
- name: Dry-run publish validation
|
|
run: npm publish --dry-run --tag next
|
|
env:
|
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
|
|
- name: Tag and push
|
|
if: ${{ !inputs.dry_run }}
|
|
env:
|
|
PRE_VERSION: ${{ steps.prerelease.outputs.pre_version }}
|
|
BRANCH: ${{ needs.validate-version.outputs.branch }}
|
|
run: |
|
|
if git rev-parse -q --verify "refs/tags/v${PRE_VERSION}" >/dev/null; then
|
|
EXISTING_SHA=$(git rev-parse "refs/tags/v${PRE_VERSION}")
|
|
HEAD_SHA=$(git rev-parse HEAD)
|
|
if [ "$EXISTING_SHA" != "$HEAD_SHA" ]; then
|
|
echo "::error::Tag v${PRE_VERSION} already exists pointing to different commit"
|
|
exit 1
|
|
fi
|
|
echo "Tag v${PRE_VERSION} already exists on current commit; skipping tag"
|
|
else
|
|
git tag "v${PRE_VERSION}"
|
|
fi
|
|
git push origin "$BRANCH" --tags
|
|
|
|
- name: Publish to npm (next)
|
|
if: ${{ !inputs.dry_run }}
|
|
run: npm publish --provenance --access public --tag next
|
|
env:
|
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
|
|
- name: Verify publish
|
|
if: ${{ !inputs.dry_run }}
|
|
env:
|
|
PRE_VERSION: ${{ steps.prerelease.outputs.pre_version }}
|
|
run: |
|
|
sleep 10
|
|
PUBLISHED=$(npm view get-shit-done-cc@"$PRE_VERSION" version 2>/dev/null || echo "NOT_FOUND")
|
|
if [ "$PUBLISHED" != "$PRE_VERSION" ]; then
|
|
echo "::error::Published version verification failed. Expected $PRE_VERSION, got $PUBLISHED"
|
|
exit 1
|
|
fi
|
|
echo "✓ Verified: get-shit-done-cc@$PRE_VERSION is live on npm"
|
|
# Also verify dist-tag
|
|
NEXT_TAG=$(npm dist-tag ls get-shit-done-cc 2>/dev/null | grep "next:" | awk '{print $2}')
|
|
echo "✓ next tag points to: $NEXT_TAG"
|
|
|
|
- name: Summary
|
|
env:
|
|
PRE_VERSION: ${{ steps.prerelease.outputs.pre_version }}
|
|
DRY_RUN: ${{ inputs.dry_run }}
|
|
run: |
|
|
echo "## Pre-release v${PRE_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 \`next\`" >> "$GITHUB_STEP_SUMMARY"
|
|
echo "- Install: \`npx get-shit-done-cc@next\`" >> "$GITHUB_STEP_SUMMARY"
|
|
fi
|
|
echo "" >> "$GITHUB_STEP_SUMMARY"
|
|
echo "To publish another pre-release: run \`rc\` again" >> "$GITHUB_STEP_SUMMARY"
|
|
echo "To finalize: run \`finalize\` action" >> "$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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
with:
|
|
ref: ${{ needs.validate-version.outputs.branch }}
|
|
fetch-depth: 0
|
|
|
|
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.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: Set final version
|
|
env:
|
|
VERSION: ${{ inputs.version }}
|
|
run: |
|
|
npm version "$VERSION" --no-git-tag-version --allow-same-version
|
|
git add package.json package-lock.json
|
|
git diff --cached --quiet || git commit -m "chore: finalize v${VERSION}"
|
|
|
|
- name: Install and test
|
|
run: |
|
|
npm ci
|
|
npm run test:coverage
|
|
|
|
- name: Dry-run publish validation
|
|
run: npm publish --dry-run
|
|
env:
|
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
|
|
- name: Create PR to merge release 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 release v${VERSION} to main" \
|
|
--body "Merge release branch back to main after v${VERSION} stable release."
|
|
else
|
|
gh pr create \
|
|
--base main \
|
|
--head "$BRANCH" \
|
|
--title "chore: merge release v${VERSION} to main" \
|
|
--body "Merge release branch back to main after v${VERSION} stable release."
|
|
fi
|
|
|
|
- name: Tag and push
|
|
if: ${{ !inputs.dry_run }}
|
|
env:
|
|
VERSION: ${{ inputs.version }}
|
|
BRANCH: ${{ needs.validate-version.outputs.branch }}
|
|
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 tag"
|
|
else
|
|
git tag "v${VERSION}"
|
|
fi
|
|
git push origin "$BRANCH" --tags
|
|
|
|
- name: Publish to npm (latest)
|
|
if: ${{ !inputs.dry_run }}
|
|
run: npm publish --provenance --access public
|
|
env:
|
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
|
|
- 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"
|
|
# Verify latest tag
|
|
LATEST_TAG=$(npm dist-tag ls get-shit-done-cc 2>/dev/null | grep "latest:" | awk '{print $2}')
|
|
echo "✓ latest tag points to: $LATEST_TAG"
|
|
|
|
- name: Summary
|
|
env:
|
|
VERSION: ${{ inputs.version }}
|
|
DRY_RUN: ${{ inputs.dry_run }}
|
|
run: |
|
|
echo "## Release 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"
|
|
echo "- Install: \`npx get-shit-done-cc@latest\`" >> "$GITHUB_STEP_SUMMARY"
|
|
fi
|