feat: enforce issue-linked branches + document versioning/tagging policy (#48)
Formalize the branch↔issue relationship and add a release/version-tagging policy. Branch/issue linkage: - scripts/worktree-start now validates branch names: implementation branches must match (fix|feat|docs|chore)/issue-<number>-<slug>; review branches review/pr-<number>-<slug>. Untraceable names are rejected with a clear error (exit 2). New --allow-unlinked override for genuine exceptions. --dry-run preserved. - Documented issue → branch → worktree → PR → cleanup traceability in the runbook and the portable SKILL, including the claim-comment convention and Closes #n / Refs #n PR-body usage. - Noted that Gitea exposes no native issue→branch API field (only a PR head branch), so linkage is enforced via branch name + claim comment + PR body + cleanup. Versioning / tagging policy (docs only; no release automation yet): - SemVer vMAJOR.MINOR.PATCH (v0.x.y while unstable) with PATCH/MINOR/MAJOR bump rules. - Annotated tags only, from the exact commit on remote master, only after the full suite passes, with release notes referencing merged PRs/issues. Never tag feature branches, dirty worktrees, unreviewed/self-authored work, or commits not on remote master. - Release runbook in the runbook + SKILL, plus a new skills/llm-project-workflow/templates/release-tag.md prompt template. Tests: worktree-start branch validation — accepts fix/feat/docs/chore/issue-* and review/pr-*, rejects fix/random-name / my-branch / non-numeric issue, honors --allow-unlinked, preserves --dry-run. Full suite 291 passed / 0 failures; bash -n clean; git diff --check clean; no secrets. Release-tag automation (a scripts/release-tag helper) intentionally deferred to a later issue to keep this diff narrow and testable. Closes #48. Refs #38, #39, #46. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+29
-2
@@ -35,16 +35,43 @@ class TestWorktreeStart(unittest.TestCase):
|
||||
self.assertEqual(rc, 2)
|
||||
|
||||
def test_refuses_existing_worktree(self):
|
||||
slug = f"zz-refuse-start-{os.getpid()}"
|
||||
branch = f"fix/issue-999-refuse-{os.getpid()}"
|
||||
slug = branch.replace("/", "-")
|
||||
target = BRANCHES / slug
|
||||
target.mkdir(parents=True, exist_ok=True)
|
||||
try:
|
||||
rc, _, err = run("worktree-start", "--dry-run", slug)
|
||||
rc, _, err = run("worktree-start", "--dry-run", branch)
|
||||
self.assertEqual(rc, 1)
|
||||
self.assertIn("Refusing to reuse", err)
|
||||
finally:
|
||||
target.rmdir()
|
||||
|
||||
# -- issue-linked branch validation (#48) --------------------------------
|
||||
|
||||
def test_accepts_issue_linked_impl_branches(self):
|
||||
for branch in ("fix/issue-123-example", "feat/issue-123-example",
|
||||
"docs/issue-123-example", "chore/issue-123-example"):
|
||||
rc, out, err = run("worktree-start", "--dry-run", branch)
|
||||
self.assertEqual(rc, 0, f"{branch}: {err}")
|
||||
self.assertIn(f"branches/{branch.replace('/', '-')}", out)
|
||||
|
||||
def test_accepts_review_branch(self):
|
||||
rc, out, _ = run("worktree-start", "--dry-run", "review/pr-456-example")
|
||||
self.assertEqual(rc, 0)
|
||||
self.assertIn("branches/review-pr-456-example", out)
|
||||
|
||||
def test_rejects_untraceable_branches(self):
|
||||
for branch in ("fix/random-name", "my-branch", "feat/no-issue-here",
|
||||
"fix/issue-abc-x"):
|
||||
rc, _, err = run("worktree-start", "--dry-run", branch)
|
||||
self.assertEqual(rc, 2, branch)
|
||||
self.assertIn("Untraceable branch name", err)
|
||||
|
||||
def test_allow_unlinked_override(self):
|
||||
rc, out, _ = run("worktree-start", "--dry-run", "--allow-unlinked", "my-branch")
|
||||
self.assertEqual(rc, 0)
|
||||
self.assertIn("branches/my-branch", out)
|
||||
|
||||
|
||||
class TestWorktreeReview(unittest.TestCase):
|
||||
|
||||
|
||||
Reference in New Issue
Block a user