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 (#1289)
* 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>
This commit is contained in:
25
.github/dependabot.yml
vendored
Normal file
25
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: npm
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
day: monday
|
||||
open-pull-requests-limit: 5
|
||||
labels:
|
||||
- dependencies
|
||||
- type: chore
|
||||
commit-message:
|
||||
prefix: "chore(deps):"
|
||||
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
day: monday
|
||||
open-pull-requests-limit: 5
|
||||
labels:
|
||||
- dependencies
|
||||
- type: chore
|
||||
commit-message:
|
||||
prefix: "chore(ci):"
|
||||
85
.github/workflows/auto-branch.yml
vendored
Normal file
85
.github/workflows/auto-branch.yml
vendored
Normal file
@@ -0,0 +1,85 @@
|
||||
name: Auto-Branch from Issue Label
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [labeled]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
create-branch:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 2
|
||||
if: >-
|
||||
contains(fromJSON('["bug", "enhancement", "priority: critical", "type: chore", "area: docs"]'),
|
||||
github.event.label.name)
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Create branch
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
with:
|
||||
script: |
|
||||
const label = context.payload.label.name;
|
||||
const issue = context.payload.issue;
|
||||
const number = issue.number;
|
||||
|
||||
// Generate slug from title
|
||||
const slug = issue.title
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '')
|
||||
.substring(0, 40);
|
||||
|
||||
// Map label to branch prefix
|
||||
const prefixMap = {
|
||||
'bug': 'fix',
|
||||
'enhancement': 'feat',
|
||||
'priority: critical': 'fix',
|
||||
'type: chore': 'chore',
|
||||
'area: docs': 'docs',
|
||||
};
|
||||
const prefix = prefixMap[label];
|
||||
if (!prefix) return;
|
||||
|
||||
// For priority: critical, use fix/critical-NNN-slug to avoid
|
||||
// colliding with the hotfix workflow's hotfix/X.Y.Z naming.
|
||||
const branch = label === 'priority: critical'
|
||||
? `fix/critical-${number}-${slug}`
|
||||
: `${prefix}/${number}-${slug}`;
|
||||
|
||||
// Check if branch already exists
|
||||
try {
|
||||
await github.rest.git.getRef({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
ref: `heads/${branch}`,
|
||||
});
|
||||
core.info(`Branch ${branch} already exists`);
|
||||
return;
|
||||
} catch (e) {
|
||||
if (e.status !== 404) throw e;
|
||||
}
|
||||
|
||||
// Create branch from main HEAD
|
||||
const mainRef = await github.rest.git.getRef({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
ref: 'heads/main',
|
||||
});
|
||||
|
||||
await github.rest.git.createRef({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
ref: `refs/heads/${branch}`,
|
||||
sha: mainRef.data.object.sha,
|
||||
});
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: number,
|
||||
body: `Branch \`${branch}\` created.\n\n\`\`\`bash\ngit fetch origin && git checkout ${branch}\n\`\`\``,
|
||||
});
|
||||
38
.github/workflows/branch-naming.yml
vendored
Normal file
38
.github/workflows/branch-naming.yml
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
name: Validate Branch Name
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
check-branch:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 1
|
||||
steps:
|
||||
- name: Validate branch naming convention
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
with:
|
||||
script: |
|
||||
const branch = context.payload.pull_request.head.ref;
|
||||
|
||||
const validPrefixes = [
|
||||
'feat/', 'fix/', 'hotfix/', 'docs/', 'chore/',
|
||||
'refactor/', 'test/', 'release/', 'ci/', 'perf/', 'revert/',
|
||||
];
|
||||
|
||||
const alwaysValid = ['main', 'develop'];
|
||||
if (alwaysValid.includes(branch)) return;
|
||||
if (branch.startsWith('dependabot/') || branch.startsWith('renovate/')) return;
|
||||
// GSD auto-created branches
|
||||
if (branch.startsWith('gsd/') || branch.startsWith('claude/')) return;
|
||||
|
||||
const isValid = validPrefixes.some(prefix => branch.startsWith(prefix));
|
||||
if (!isValid) {
|
||||
const prefixList = validPrefixes.map(p => `\`${p}\``).join(', ');
|
||||
core.warning(
|
||||
`Branch "${branch}" doesn't follow naming convention. ` +
|
||||
`Expected prefixes: ${prefixList}`
|
||||
);
|
||||
}
|
||||
229
.github/workflows/hotfix.yml
vendored
Normal file
229
.github/workflows/hotfix.yml
vendored
Normal file
@@ -0,0 +1,229 @@
|
||||
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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.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@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 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@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: 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: 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
|
||||
67
.github/workflows/pr-gate.yml
vendored
Normal file
67
.github/workflows/pr-gate.yml
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
name: PR Gate
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
size-check:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 2
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Check PR size
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
with:
|
||||
script: |
|
||||
const files = await github.paginate(github.rest.pulls.listFiles, {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: context.issue.number,
|
||||
per_page: 100,
|
||||
});
|
||||
|
||||
const additions = files.reduce((sum, f) => sum + f.additions, 0);
|
||||
const deletions = files.reduce((sum, f) => sum + f.deletions, 0);
|
||||
const total = additions + deletions;
|
||||
|
||||
let label = '';
|
||||
if (total <= 50) label = 'size/S';
|
||||
else if (total <= 200) label = 'size/M';
|
||||
else if (total <= 500) label = 'size/L';
|
||||
else label = 'size/XL';
|
||||
|
||||
// Remove existing size labels
|
||||
const existingLabels = context.payload.pull_request.labels || [];
|
||||
const sizeLabels = existingLabels.filter(l => l.name.startsWith('size/'));
|
||||
for (const staleLabel of sizeLabels) {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
name: staleLabel.name
|
||||
}).catch(() => {}); // ignore if already removed
|
||||
}
|
||||
|
||||
// Add size label
|
||||
try {
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
labels: [label],
|
||||
});
|
||||
} catch (e) {
|
||||
core.warning(`Could not add label: ${e.message}`);
|
||||
}
|
||||
|
||||
if (total > 500) {
|
||||
core.warning(`Large PR: ${total} lines changed (${additions}+ / ${deletions}-). Consider splitting.`);
|
||||
}
|
||||
374
.github/workflows/release.yml
vendored
Normal file
374
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,374 @@
|
||||
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
|
||||
34
.github/workflows/stale.yml
vendored
Normal file
34
.github/workflows/stale.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
name: Stale Cleanup
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 9 * * 1' # Monday 9am UTC
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0
|
||||
with:
|
||||
days-before-stale: 28
|
||||
days-before-close: 14
|
||||
stale-issue-message: >
|
||||
This issue has been inactive for 28 days. It will be closed in 14 days
|
||||
if there is no further activity. If this is still relevant, please comment
|
||||
or update to the latest GSD version and retest.
|
||||
stale-pr-message: >
|
||||
This PR has been inactive for 28 days. It will be closed in 14 days
|
||||
if there is no further activity.
|
||||
close-issue-message: >
|
||||
Closed due to inactivity. If this is still relevant, please reopen
|
||||
with updated reproduction steps on the latest GSD version.
|
||||
stale-issue-label: 'stale'
|
||||
stale-pr-label: 'stale'
|
||||
exempt-issue-labels: 'fix-pending,priority: critical,pinned,confirmed-bug,confirmed'
|
||||
exempt-pr-labels: 'fix-pending,priority: critical,pinned,DO NOT MERGE'
|
||||
126
VERSIONING.md
Normal file
126
VERSIONING.md
Normal file
@@ -0,0 +1,126 @@
|
||||
# Versioning & Release Strategy
|
||||
|
||||
GSD follows [Semantic Versioning 2.0.0](https://semver.org/) with three release tiers mapped to npm dist-tags.
|
||||
|
||||
## Release Tiers
|
||||
|
||||
| Tier | What ships | Version format | npm tag | Branch | Install |
|
||||
|------|-----------|---------------|---------|--------|---------|
|
||||
| **Patch** | Bug fixes only | `1.27.1` | `latest` | `hotfix/1.27.1` | `npx get-shit-done-cc@latest` |
|
||||
| **Minor** | Fixes + enhancements | `1.28.0` | `latest` (after RC) | `release/1.28.0` | `npx get-shit-done-cc@next` (RC) |
|
||||
| **Major** | Fixes + enhancements + features | `2.0.0` | `latest` (after beta) | `release/2.0.0` | `npx get-shit-done-cc@next` (beta) |
|
||||
|
||||
## npm Dist-Tags
|
||||
|
||||
Only two tags, following Angular/Next.js convention:
|
||||
|
||||
| Tag | Meaning | Installed by |
|
||||
|-----|---------|-------------|
|
||||
| `latest` | Stable production release | `npm install get-shit-done-cc` (default) |
|
||||
| `next` | Pre-release (RC or beta) | `npm install get-shit-done-cc@next` (opt-in) |
|
||||
|
||||
The version string (`-rc.1` vs `-beta.1`) communicates stability level. Users never get pre-releases unless they explicitly opt in.
|
||||
|
||||
## Semver Rules
|
||||
|
||||
| Increment | When | Examples |
|
||||
|-----------|------|----------|
|
||||
| **PATCH** (1.27.x) | Bug fixes, typo corrections, test additions | Hook filter fix, config corruption fix |
|
||||
| **MINOR** (1.x.0) | Non-breaking enhancements, new commands, new runtime support | New workflow command, discuss-mode feature |
|
||||
| **MAJOR** (x.0.0) | Breaking changes to config format, CLI flags, or runtime API; new features that alter existing behavior | Removing a command, changing config schema |
|
||||
|
||||
## Pre-Release Version Progression
|
||||
|
||||
Major and minor releases use different pre-release types:
|
||||
|
||||
```
|
||||
Minor: 1.28.0-rc.1 → 1.28.0-rc.2 → 1.28.0
|
||||
Major: 2.0.0-beta.1 → 2.0.0-beta.2 → 2.0.0
|
||||
```
|
||||
|
||||
- **beta** (major releases only): Feature-complete but not fully tested. API mostly stable. Used for major releases to signal a longer testing cycle.
|
||||
- **rc** (minor releases only): Production-ready candidate. Only critical fixes expected.
|
||||
- Each version uses one pre-release type throughout its cycle. The `rc` action in the release workflow automatically selects the correct type based on the version.
|
||||
|
||||
## Branch Structure
|
||||
|
||||
```
|
||||
main ← stable, always deployable
|
||||
│
|
||||
├── hotfix/1.27.1 ← patch: cherry-pick fix from main, publish to latest
|
||||
│
|
||||
├── release/1.28.0 ← minor: accumulate fixes + enhancements, RC cycle
|
||||
│ ├── v1.28.0-rc.1 ← tag: published to next
|
||||
│ └── v1.28.0 ← tag: promoted to latest
|
||||
│
|
||||
├── release/2.0.0 ← major: features + breaking changes, beta cycle
|
||||
│ ├── v2.0.0-beta.1 ← tag: published to next
|
||||
│ ├── v2.0.0-beta.2 ← tag: published to next
|
||||
│ └── v2.0.0 ← tag: promoted to latest
|
||||
│
|
||||
├── fix/1200-bug-description ← bug fix branch (merges to main)
|
||||
├── feat/925-feature-name ← feature branch (merges to main)
|
||||
└── chore/1206-maintenance ← maintenance branch (merges to main)
|
||||
```
|
||||
|
||||
## Release Workflows
|
||||
|
||||
### Patch Release (Hotfix)
|
||||
|
||||
For critical bugs that can't wait for the next minor release.
|
||||
|
||||
1. Trigger `hotfix.yml` with version (e.g., `1.27.1`)
|
||||
2. Workflow creates `hotfix/1.27.1` branch from the latest patch tag for that minor version (e.g., `v1.27.0` or `v1.27.1`)
|
||||
3. Cherry-pick or apply fix on the hotfix branch
|
||||
4. Push — CI runs tests automatically
|
||||
5. Trigger `hotfix.yml` finalize action
|
||||
6. Workflow runs full test suite, bumps version, tags, publishes to `latest`
|
||||
7. Merge hotfix branch back to main
|
||||
|
||||
### Minor Release (Standard Cycle)
|
||||
|
||||
For accumulated fixes and enhancements.
|
||||
|
||||
1. Trigger `release.yml` with action `create` and version (e.g., `1.28.0`)
|
||||
2. Workflow creates `release/1.28.0` branch from main, bumps package.json
|
||||
3. Trigger `release.yml` with action `rc` to publish `1.28.0-rc.1` to `next`
|
||||
4. Test the RC: `npx get-shit-done-cc@next`
|
||||
5. If issues found: fix on release branch, publish `rc.2`, `rc.3`, etc.
|
||||
6. Trigger `release.yml` with action `finalize` — publishes `1.28.0` to `latest`
|
||||
7. Merge release branch to main
|
||||
|
||||
### Major Release
|
||||
|
||||
Same as minor but uses `-beta.N` instead of `-rc.N`, signaling a longer testing cycle.
|
||||
|
||||
1. Trigger `release.yml` with action `create` and version (e.g., `2.0.0`)
|
||||
2. Trigger `release.yml` with action `rc` to publish `2.0.0-beta.1` to `next`
|
||||
3. If issues found: fix on release branch, publish `beta.2`, `beta.3`, etc.
|
||||
4. Trigger `release.yml` with action `finalize` -- publishes `2.0.0` to `latest`
|
||||
5. Merge release branch to main
|
||||
|
||||
## Conventional Commits
|
||||
|
||||
Branch names map to commit types:
|
||||
|
||||
| Branch prefix | Commit type | Version bump |
|
||||
|--------------|-------------|-------------|
|
||||
| `fix/` | `fix:` | PATCH |
|
||||
| `feat/` | `feat:` | MINOR |
|
||||
| `hotfix/` | `fix:` | PATCH (immediate) |
|
||||
| `chore/` | `chore:` | none |
|
||||
| `docs/` | `docs:` | none |
|
||||
| `refactor/` | `refactor:` | none |
|
||||
|
||||
## Publishing Commands (Reference)
|
||||
|
||||
```bash
|
||||
# Stable release (sets latest tag automatically)
|
||||
npm publish
|
||||
|
||||
# Pre-release (must use --tag to avoid overwriting latest)
|
||||
npm publish --tag next
|
||||
|
||||
# Verify what latest and next point to
|
||||
npm dist-tag ls get-shit-done-cc
|
||||
```
|
||||
Reference in New Issue
Block a user