diff --git a/.opencode/skills/release-workflow/SKILL.md b/.opencode/skills/release-workflow/SKILL.md index 1c7c37c..fa64e7d 100644 --- a/.opencode/skills/release-workflow/SKILL.md +++ b/.opencode/skills/release-workflow/SKILL.md @@ -171,6 +171,34 @@ gh pr merge --squash --delete-branch git checkout main && git pull origin main ``` +### Why squash merge can look "diverged" (expected topology) + +`--squash` creates a new commit on `main`, so original commits from `release/vX.Y.Z` are not direct ancestors of `main`. +This can look like divergence in commit history even when release content is already shipped. + +Use content checks (not commit shape) to decide if this is safe: + +```bash +git diff --stat main..release/vX.Y.Z +``` + +- Empty diff (or only expected metadata differences): usually safe and already represented on `main` +- Non-empty diff in `src/`, `test/`, or `openspec/`: investigate before proceeding + +### Phase 6.5 — Branch Cleanup Verification (CRITICAL) + +After merge, verify the release branch was actually deleted on remote. +`--delete-branch` can fail silently (for example due to protection rules or permissions). + +```bash +git fetch --prune +if git branch -r | grep -q "origin/release/vX.Y.Z"; then + echo "ERROR: origin/release/vX.Y.Z still exists. Delete it before declaring release done." + echo "Run: git push origin --delete release/vX.Y.Z" + exit 1 +fi +``` + ### IMPORTANT: Never use git stash during release **Why**: Stashing before rebase can cause code changes to be lost: @@ -182,6 +210,20 @@ git checkout main && git pull origin main **If you must rebase**: Use `git reset --hard` to discard local changes first, OR commit them before rebasing. +### Divergence Recovery: reset vs rebase + +Choose by intent: + +| Situation | Preferred command | Why | +|---|---|---| +| You only want local `main` to match remote authority | `git fetch origin && git reset --hard origin/main` | Fastest and least ambiguous recovery | +| You must keep local, unpushed commits and replay on top of latest remote | `git pull --rebase origin main` | Preserves your local work with linear history | + +Hard rule for release flow: +- Never run rebase with uncommitted changes +- Never use stash as a release transport mechanism +- If uncertain, reset local `main` to `origin/main` and restart from a clean release branch + --- ## Phase 7 — Tag and Trigger CI Release @@ -380,6 +422,10 @@ gh pr create --title "chore: release vX.Y.Z" --base main --head release/vX.Y.Z gh pr merge --squash --delete-branch git checkout main && git pull origin main +# Phase 6.5 — remote branch cleanup verification +git fetch --prune +git branch -r | grep "origin/release/vX.Y.Z" && git push origin --delete release/vX.Y.Z + # Phase 7 — tag git tag vX.Y.Z HEAD git push origin vX.Y.Z @@ -401,3 +447,4 @@ Release can be declared complete only if all are true: 3. Any user-facing bullet has runtime entrypoint proof (`hooks.tool`/hook path) 4. Tag points to merged implementation commit, not version-only commit 5. Post-release verification confirms npm + GitHub Release +6. `origin/release/vX.Y.Z` has been deleted (or explicitly cleaned up and pruned) diff --git a/docs/DEVELOPMENT_WORKFLOW.md b/docs/DEVELOPMENT_WORKFLOW.md index 54f6fbe..618a967 100644 --- a/docs/DEVELOPMENT_WORKFLOW.md +++ b/docs/DEVELOPMENT_WORKFLOW.md @@ -240,8 +240,42 @@ The skill will guide you through: 4. **Version & Changelog** — Update `package.json` and `CHANGELOG.md` 5. **Release Branch** — Create `release/vX.Y.Z` branch 6. **PR to Main** — Create PR with pre-merge checks -7. **Tag and Trigger CI** — Push tag to trigger npm publish -8. **Post-Release Verification** — Confirm npm + GitHub Release +7. **Branch Cleanup Verification** — Ensure remote `release/vX.Y.Z` branch is deleted/pruned +8. **Tag and Trigger CI** — Push tag to trigger npm publish +9. **Post-Release Verification** — Confirm npm + GitHub Release + +### Important: Squash merge topology is expected + +The release workflow uses `--squash` for clean `main` history. This can make commit history look "diverged" +because original commits on `release/vX.Y.Z` are not direct ancestors of `main`. + +Use content diff (not commit shape) as the safety check: + +```bash +git diff --stat main..release/vX.Y.Z +``` + +- Empty (or only expected metadata differences): usually safe +- Non-empty in `src/`, `test/`, `openspec/`: investigate before release completion + +### Divergence recovery quick rule: reset vs rebase + +- Sync local `main` to remote authority (most common): + +```bash +git fetch origin && git reset --hard origin/main +``` + +- Keep local, unpushed commits and replay on latest `main`: + +```bash +git pull --rebase origin main +``` + +Release safety rules: +- Never rebase with uncommitted changes +- Never use stash as release transport +- If uncertain, reset local `main` to `origin/main` and restart from clean release branch --- @@ -331,8 +365,10 @@ If you're unsure, ask: "Does this need a specification document?" If no → use ### "Working tree is dirty" Either: -1. Stash changes: `git stash` -2. Commit changes: `git add . && git commit` +1. Commit changes: `git add . && git commit` +2. Or discard local changes intentionally: `git reset --hard` + +For release flow specifically, do **not** use `git stash` as a transport mechanism before rebase. ### "Branch protection prevents push"