diff --git a/.agents/skills/deploy/SKILL.md b/.agents/skills/deploy/SKILL.md index 5b84515..5f40697 100644 --- a/.agents/skills/deploy/SKILL.md +++ b/.agents/skills/deploy/SKILL.md @@ -111,36 +111,22 @@ git describe --tags --abbrev=0 2>/dev/null If no tag is found at all (first release), warn: "No version tag found. You must bump the version before deploying." Return to the version bump prompt. Otherwise, use the tag found as the release version. -If the user chose a bump level, map their response to a command: +If the user chose a bump level, map their response to a bump command and run `bump-and-tag.py`, which performs the bump, verifies the resulting tag (creating an annotated fallback if `tag.forceSignAnnotated` silently rejected a lightweight tag), and pushes both the commit and the tag. The resolved version is printed on stdout — capture it for the release step. -| User says | Run | +| User says | `--bump-cmd` | |---|---| | "patch" / anything mentioning patch | `make bump-patch` | | "minor" | `make bump-minor` | | "major" | `make bump-major` | -| A specific version e.g. "2.4.5" | `make set-version V= 2.4.5` | +| A specific version e.g. "2.4.5" | `make set-version V=2.4.5` | -These commands update the version manifest, create a git commit, and create a git tag. - -After the bump command runs, verify the tag was actually created: - -```bash -git tag -l "v" -``` - -If the tag is missing (some git configs like `tag.forceSignAnnotated = true` reject lightweight tags silently), create an annotated tag as a fallback: - -```bash -git tag -a "v" -m "v" -``` - -Push the version bump: ```bash -git push -git push --tags +python3 CodeCannon/skills/github-agile/scripts/bump-and-tag.py \ + --bump-cmd "" \ + --version-read-cmd "cat VERSION" ``` -Both the version bump commit and the tag must be pushed. +If the script exits non-zero, stop and resolve the issue it reports before continuing. On success, the version printed on stdout is the new version — use it as `` in subsequent steps. --- @@ -268,4 +254,4 @@ After the command runs, note the release URL from the output. Tell the user: > "Released vX.Y.Z. Issues #N, #M closed automatically. GitHub Release vX.Y.Z created at ``. Run `make deploy-prod` to ship to production." - + diff --git a/.claude/commands/deploy.md b/.claude/commands/deploy.md index b24d074..085d2d8 100644 --- a/.claude/commands/deploy.md +++ b/.claude/commands/deploy.md @@ -106,36 +106,22 @@ git describe --tags --abbrev=0 2>/dev/null If no tag is found at all (first release), warn: "No version tag found. You must bump the version before deploying." Return to the version bump prompt. Otherwise, use the tag found as the release version. -If the user chose a bump level, map their response to a command: +If the user chose a bump level, map their response to a bump command and run `bump-and-tag.py`, which performs the bump, verifies the resulting tag (creating an annotated fallback if `tag.forceSignAnnotated` silently rejected a lightweight tag), and pushes both the commit and the tag. The resolved version is printed on stdout — capture it for the release step. -| User says | Run | +| User says | `--bump-cmd` | |---|---| | "patch" / anything mentioning patch | `make bump-patch` | | "minor" | `make bump-minor` | | "major" | `make bump-major` | -| A specific version e.g. "2.4.5" | `make set-version V= 2.4.5` | +| A specific version e.g. "2.4.5" | `make set-version V=2.4.5` | -These commands update the version manifest, create a git commit, and create a git tag. - -After the bump command runs, verify the tag was actually created: - -```bash -git tag -l "v" -``` - -If the tag is missing (some git configs like `tag.forceSignAnnotated = true` reject lightweight tags silently), create an annotated tag as a fallback: - -```bash -git tag -a "v" -m "v" -``` - -Push the version bump: ```bash -git push -git push --tags +python3 CodeCannon/skills/github-agile/scripts/bump-and-tag.py \ + --bump-cmd "" \ + --version-read-cmd "cat VERSION" ``` -Both the version bump commit and the tag must be pushed. +If the script exits non-zero, stop and resolve the issue it reports before continuing. On success, the version printed on stdout is the new version — use it as `` in subsequent steps. --- @@ -263,4 +249,4 @@ After the command runs, note the release URL from the output. Tell the user: > "Released vX.Y.Z. Issues #N, #M closed automatically. GitHub Release vX.Y.Z created at ``. Run `make deploy-prod` to ship to production." - + diff --git a/.codecannon.yaml b/.codecannon.yaml index 9e59090..923536d 100644 --- a/.codecannon.yaml +++ b/.codecannon.yaml @@ -35,6 +35,9 @@ config: BUMP_PATCH_CMD: make bump-patch BUMP_MINOR_CMD: make bump-minor BUMP_MAJOR_CMD: make bump-major + # SET_VERSION_CMD is concatenated with the version (no implicit separator), + # so include the trailing separator your command needs: `=` for kwarg-style + # (e.g. "make set-version V="), trailing space for positional (e.g. "npm version "). SET_VERSION_CMD: "make set-version V=" REVIEW_AGENT_PROMPT: .claude/review-agent-prompt.md DEFAULT_REVIEWERS: "@sebastientaggart" diff --git a/.cursor/rules/deploy.mdc b/.cursor/rules/deploy.mdc index f5ba6d2..bb8810f 100644 --- a/.cursor/rules/deploy.mdc +++ b/.cursor/rules/deploy.mdc @@ -112,36 +112,22 @@ git describe --tags --abbrev=0 2>/dev/null If no tag is found at all (first release), warn: "No version tag found. You must bump the version before deploying." Return to the version bump prompt. Otherwise, use the tag found as the release version. -If the user chose a bump level, map their response to a command: +If the user chose a bump level, map their response to a bump command and run `bump-and-tag.py`, which performs the bump, verifies the resulting tag (creating an annotated fallback if `tag.forceSignAnnotated` silently rejected a lightweight tag), and pushes both the commit and the tag. The resolved version is printed on stdout — capture it for the release step. -| User says | Run | +| User says | `--bump-cmd` | |---|---| | "patch" / anything mentioning patch | `make bump-patch` | | "minor" | `make bump-minor` | | "major" | `make bump-major` | -| A specific version e.g. "2.4.5" | `make set-version V= 2.4.5` | +| A specific version e.g. "2.4.5" | `make set-version V=2.4.5` | -These commands update the version manifest, create a git commit, and create a git tag. - -After the bump command runs, verify the tag was actually created: - -```bash -git tag -l "v" -``` - -If the tag is missing (some git configs like `tag.forceSignAnnotated = true` reject lightweight tags silently), create an annotated tag as a fallback: - -```bash -git tag -a "v" -m "v" -``` - -Push the version bump: ```bash -git push -git push --tags +python3 CodeCannon/skills/github-agile/scripts/bump-and-tag.py \ + --bump-cmd "" \ + --version-read-cmd "cat VERSION" ``` -Both the version bump commit and the tag must be pushed. +If the script exits non-zero, stop and resolve the issue it reports before continuing. On success, the version printed on stdout is the new version — use it as `` in subsequent steps. --- @@ -269,4 +255,4 @@ After the command runs, note the release URL from the output. Tell the user: > "Released vX.Y.Z. Issues #N, #M closed automatically. GitHub Release vX.Y.Z created at ``. Run `make deploy-prod` to ship to production." - + diff --git a/.gemini/skills/deploy/SKILL.md b/.gemini/skills/deploy/SKILL.md index 7a9c0e5..b3df59f 100644 --- a/.gemini/skills/deploy/SKILL.md +++ b/.gemini/skills/deploy/SKILL.md @@ -111,36 +111,22 @@ git describe --tags --abbrev=0 2>/dev/null If no tag is found at all (first release), warn: "No version tag found. You must bump the version before deploying." Return to the version bump prompt. Otherwise, use the tag found as the release version. -If the user chose a bump level, map their response to a command: +If the user chose a bump level, map their response to a bump command and run `bump-and-tag.py`, which performs the bump, verifies the resulting tag (creating an annotated fallback if `tag.forceSignAnnotated` silently rejected a lightweight tag), and pushes both the commit and the tag. The resolved version is printed on stdout — capture it for the release step. -| User says | Run | +| User says | `--bump-cmd` | |---|---| | "patch" / anything mentioning patch | `make bump-patch` | | "minor" | `make bump-minor` | | "major" | `make bump-major` | -| A specific version e.g. "2.4.5" | `make set-version V= 2.4.5` | +| A specific version e.g. "2.4.5" | `make set-version V=2.4.5` | -These commands update the version manifest, create a git commit, and create a git tag. - -After the bump command runs, verify the tag was actually created: - -```bash -git tag -l "v" -``` - -If the tag is missing (some git configs like `tag.forceSignAnnotated = true` reject lightweight tags silently), create an annotated tag as a fallback: - -```bash -git tag -a "v" -m "v" -``` - -Push the version bump: ```bash -git push -git push --tags +python3 CodeCannon/skills/github-agile/scripts/bump-and-tag.py \ + --bump-cmd "" \ + --version-read-cmd "cat VERSION" ``` -Both the version bump commit and the tag must be pushed. +If the script exits non-zero, stop and resolve the issue it reports before continuing. On success, the version printed on stdout is the new version — use it as `` in subsequent steps. --- @@ -268,4 +254,4 @@ After the command runs, note the release URL from the output. Tell the user: > "Released vX.Y.Z. Issues #N, #M closed automatically. GitHub Release vX.Y.Z created at ``. Run `make deploy-prod` to ship to production." - + diff --git a/docs/customization.md b/docs/customization.md index 3043d3f..d1a8b92 100644 --- a/docs/customization.md +++ b/docs/customization.md @@ -175,4 +175,6 @@ BUMP_MAJOR_CMD: make bump-major SET_VERSION_CMD: "make set-version V=" # arbitrary version (value appended) ``` +`SET_VERSION_CMD` is concatenated with the version with no implicit separator, so it must include its own trailing separator: `=` for kwarg-style (`"make set-version V="`), or a trailing space for positional-style (`"npm version "`). + These commands are expected to handle the commit and tag creation themselves. `/deploy` calls them and then pushes. diff --git a/skills/github-agile/deploy.md b/skills/github-agile/deploy.md index 0423782..9705096 100644 --- a/skills/github-agile/deploy.md +++ b/skills/github-agile/deploy.md @@ -156,36 +156,22 @@ git describe --tags --abbrev=0 2>/dev/null If no tag is found at all (first release), warn: "No version tag found. You must bump the version before deploying." Return to the version bump prompt. Otherwise, use the tag found as the release version. -If the user chose a bump level, map their response to a command: +If the user chose a bump level, map their response to a bump command and run `bump-and-tag.py`, which performs the bump, verifies the resulting tag (creating an annotated fallback if `tag.forceSignAnnotated` silently rejected a lightweight tag), and pushes both the commit and the tag. The resolved version is printed on stdout — capture it for the release step. -| User says | Run | +| User says | `--bump-cmd` | |---|---| | "patch" / anything mentioning patch | `{{BUMP_PATCH_CMD}}` | | "minor" | `{{BUMP_MINOR_CMD}}` | | "major" | `{{BUMP_MAJOR_CMD}}` | -| A specific version e.g. "2.4.5" | `{{SET_VERSION_CMD}} 2.4.5` | +| A specific version e.g. "2.4.5" | `{{SET_VERSION_CMD}}2.4.5` | -These commands update the version manifest, create a git commit, and create a git tag. - -After the bump command runs, verify the tag was actually created: - -```bash -git tag -l "v" -``` - -If the tag is missing (some git configs like `tag.forceSignAnnotated = true` reject lightweight tags silently), create an annotated tag as a fallback: - -```bash -git tag -a "v" -m "v" -``` - -Push the version bump: ```bash -git push -git push --tags +python3 CodeCannon/skills/github-agile/scripts/bump-and-tag.py \ + --bump-cmd "" \ + --version-read-cmd "{{VERSION_READ_CMD}}" ``` -Both the version bump commit and the tag must be pushed. +If the script exits non-zero, stop and resolve the issue it reports before continuing. On success, the version printed on stdout is the new version — use it as `` in subsequent steps. --- diff --git a/skills/github-agile/scripts/bump-and-tag.py b/skills/github-agile/scripts/bump-and-tag.py new file mode 100755 index 0000000..2f244ed --- /dev/null +++ b/skills/github-agile/scripts/bump-and-tag.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +"""bump-and-tag — run a version bump, verify the tag, and push commit + tag. + +Wraps the bump-verify-push sequence used by /deploy Step 3 so the agent +doesn't narrate it step-by-step. The project-specific bump and version-read +commands are passed in as opaque shell strings (the same template variables +the skill already uses: {{BUMP_PATCH_CMD}}, {{VERSION_READ_CMD}}, etc.), so +this script is agnostic to whether the project uses Make, npm, uv, or a +custom script. + +Defensive behaviour: +1. Run the bump command. Bail on non-zero exit. +2. Re-read the version via the version-read command. +3. Check `git tag -l "v"`. If missing (e.g. tag.forceSignAnnotated + silently rejected a lightweight tag), create an annotated tag as a + fallback. +4. Push the commit and the tag. Both must succeed. + +The resolved version is printed to stdout on success so the calling skill +can capture it for the release step. All diagnostics go to stderr. + +Usage: + python3 .../bump-and-tag.py --bump-cmd --version-read-cmd + +Exit codes: + 0 success (version printed to stdout) + 1 bump command failed + 2 version-read command failed or produced no output + 3 tag creation or push failed + 4 bad arguments +""" + +import argparse +import subprocess +import sys + + +def run_shell(cmd, *, capture=False): + """Run a shell command string. Returns CompletedProcess.""" + return subprocess.run(cmd, shell=True, text=True, capture_output=capture) + + +def main(argv): + parser = argparse.ArgumentParser( + prog=argv[0], + description="Run a version bump, verify the resulting tag, and push.", + ) + parser.add_argument( + '--bump-cmd', + required=True, + help="Shell command that bumps the version, commits, and tags " + "(e.g. 'make bump-patch', 'npm version patch').", + ) + parser.add_argument( + '--version-read-cmd', + required=True, + help="Shell command that prints the current version to stdout " + "(e.g. 'node -p \"require(\\'./package.json\\').version\"').", + ) + args = parser.parse_args(argv[1:]) + + # 1. Run the bump. + bump = run_shell(args.bump_cmd) + if bump.returncode != 0: + print(f"Error: bump command failed: {args.bump_cmd}", file=sys.stderr) + return 1 + + # 2. Re-read the version. + read = run_shell(args.version_read_cmd, capture=True) + if read.returncode != 0: + print( + f"Error: version-read command failed: {args.version_read_cmd}\n" + f" stderr: {read.stderr.strip()}", + file=sys.stderr, + ) + return 2 + version = read.stdout.strip() + if not version: + print( + f"Error: version-read command produced no output: {args.version_read_cmd}", + file=sys.stderr, + ) + return 2 + + tag = f"v{version}" + + # 3. Verify the tag exists; fall back to an annotated tag if not. + check = subprocess.run( + ['git', 'tag', '-l', tag], + capture_output=True, + text=True, + ) + if check.returncode != 0: + print("Error: 'git tag -l' failed.", file=sys.stderr) + return 3 + if not check.stdout.strip(): + print( + f"Tag {tag} not found after bump — creating annotated tag " + "(bump command likely produced a lightweight tag that was " + "rejected by tag.forceSignAnnotated).", + file=sys.stderr, + ) + fallback = subprocess.run(['git', 'tag', '-a', tag, '-m', tag]) + if fallback.returncode != 0: + print(f"Error: failed to create annotated tag {tag}.", file=sys.stderr) + return 3 + + # 4. Push commit and tag. + for cmd in (['git', 'push'], ['git', 'push', '--tags']): + result = subprocess.run(cmd) + if result.returncode != 0: + print(f"Error: command failed: {' '.join(cmd)}", file=sys.stderr) + return 3 + + # Emit the resolved version so the caller can capture it. + print(version) + return 0 + + +if __name__ == '__main__': + sys.exit(main(sys.argv)) diff --git a/templates/codecannon.yaml b/templates/codecannon.yaml index 48dd862..a19ad95 100644 --- a/templates/codecannon.yaml +++ b/templates/codecannon.yaml @@ -58,6 +58,9 @@ config: BUMP_PATCH_CMD: make bump-patch BUMP_MINOR_CMD: make bump-minor BUMP_MAJOR_CMD: make bump-major + # SET_VERSION_CMD is concatenated with the version (no implicit separator), + # so include the trailing separator your command needs: `=` for kwarg-style + # (e.g. "make set-version V="), trailing space for positional (e.g. "npm version "). SET_VERSION_CMD: "make set-version V=" # ── Paths ────────────────────────────────────────────────────────────────────