Skip to content

feat: auto-detect semver bump in /release skill#496

Merged
carlos-alm merged 14 commits intomainfrom
feat/release-skill-auto-semver
Mar 17, 2026
Merged

feat: auto-detect semver bump in /release skill#496
carlos-alm merged 14 commits intomainfrom
feat/release-skill-auto-semver

Conversation

@carlos-alm
Copy link
Contributor

Summary

  • /release now works without a version argument — it scans commits since the last tag and applies conventional commit rules (breaking → major, feat → minor, else → patch)
  • Explicit version argument (/release 3.2.0) still works as before
  • All downstream steps use the resolved VERSION variable

Test plan

  • Run /release with no argument — verify it detects correct bump from commit history
  • Run /release 3.2.0 with explicit version — verify it uses the provided version
  • Verify SKILL.md renders correctly on GitHub

…in backlog

These two items deliver the highest immediate impact on agent experience
and graph accuracy without requiring Rust porting or TypeScript migration.
They should be implemented before any Phase 4+ roadmap work.

- #83: hook-optimized `codegraph brief` enriches passively-injected context
- #71: basic type inference closes the biggest resolution gap for TS/Java
Add new Phase 4 covering the port of JS-only build phases to Rust:
- 4.1-4.3: AST nodes, CFG, dataflow visitor ports (~587ms savings)
- 4.4: Batch SQLite inserts (~143ms)
- 4.5: Role classification & structure (~42ms)
- 4.6: Complete complexity pre-computation
- 4.7: Fix incremental rebuild data loss on native engine
- 4.8: Incremental rebuild performance (target sub-100ms)

Bump old Phases 4-10 to 5-11 with all cross-references updated.
Benchmark evidence shows ~50% of native build time is spent in
JS visitors that run identically on both engines.
Take main's corrected #57 section anchors; keep HEAD's v2.7.0 version reference.

Impact: 10 functions changed, 11 affected
…ative-acceleration

Impact: 25 functions changed, 46 affected
- Add COMMITS=0 guard in publish.yml to return clean version when HEAD
  is exactly at a tag (mirrors bench-version.js early return)
- Change bench-version.js to use PATCH+1-dev.COMMITS format instead of
  PATCH+COMMITS-dev.SHA (mirrors publish.yml's new scheme)
- Fix fallback in bench-version.js to use dev.1 matching publish.yml's
  no-tags COMMITS=1 default

Impact: 1 functions changed, 0 affected
The release skill now scans commit history using conventional commit
rules to determine major/minor/patch automatically. Explicit version
argument still works as before.
@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 17, 2026

Greptile Summary

This PR adds optional auto-detection of the semver bump to the /release skill (scanning conventional commits since the last tag), and fixes a long-standing inconsistency where COMMITS=0 dev builds were emitted as a clean semver string (3.1.5) rather than a pre-release string (3.1.5-dev.0), making them visually indistinguishable from stable releases.

Key changes:

  • SKILL.md — version argument is now optional; Step 1b scans every commit message for BREAKING CHANGE, feat!, feat:, etc. and applies conventional-commit priority rules to compute major/minor/patch; all Step 8 commands are templated against a VERSION variable
  • publish.ymlCOMMITS=0 branch now produces ${MAJOR}.${MINOR}.${PATCH}-dev.0 instead of a clean release version string
  • bench-version.js — aligned with publish.yml for COMMITS=0 (returns X.Y.Z-dev.0); the no-tags fallback now calls git rev-parse --short HEAD for a unique per-build identifier before falling back to a bare -dev suffix, restoring the uniqueness that fc7497d addressed

Notable concern: VERSION in the Step 8 fenced code block is a prose-defined placeholder that requires manual LLM substitution, unlike $ARGUMENTS which was runtime-substituted before the LLM ever saw the text. If the agent runs the gh pr create block verbatim, the release PR will have title chore: release vVERSION and branch release/VERSION.

Confidence Score: 3/5

  • Safe to merge after verifying the VERSION placeholder substitution in SKILL.md Step 8; the workflow and benchmark-version changes are clean fixes.
  • The publish.yml and bench-version.js changes are straightforward single-concern fixes that are consistent with each other and resolve previously flagged issues. The SKILL.md logic for conventional commit detection is correct. The confidence deduction comes from the VERSION placeholder in Step 8's fenced gh pr create command — since $ARGUMENTS was runtime-substituted before, Claude never needed to perform the substitution itself; now it must, and the fenced code block format signals "run this verbatim" to an LLM agent. If the substitution is missed, the release PR/branch will have literal VERSION in its name and title.
  • SKILL.md — the Step 8 fenced code block and inline git commands all use the bare VERSION placeholder; verify that the executing LLM will substitute it correctly before shipping.

Important Files Changed

Filename Overview
.claude/skills/release/SKILL.md Adds optional auto-detection of semver bump from conventional commits; good logic for major/minor/patch rules, but the new VERSION prose placeholder in Step 8's fenced code block may be executed literally by an LLM agent since it is no longer runtime-substituted like $ARGUMENTS was.
.github/workflows/publish.yml Single-line fix: COMMITS=0 now emits MAJOR.MINOR.PATCH-dev.0 instead of a clean semver, preventing dev builds at an exact tag from being indistinguishable from stable releases.
scripts/bench-version.js Two fixes: COMMITS=0 now returns X.Y.Z-dev.0 to align with publish.yml, and the no-tags fallback now tries git rev-parse --short HEAD for a unique SHA before falling back to a bare -dev suffix, restoring per-run uniqueness in benchmark deduplication.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["/release invoked"] --> B{"\$ARGUMENTS provided?"}
    B -- Yes --> C["VERSION = \$ARGUMENTS"]
    B -- No --> D["Step 1a: git log, read package.json, find last tag"]
    D --> E["Step 1b: scan commit messages"]
    E --> F{Commit type?}
    F -- "BREAKING CHANGE / feat! / fix!" --> G["bump = major → (M+1).0.0"]
    F -- "feat: / feat(scope):" --> H["bump = minor → M.(N+1).0"]
    F -- "fix: / chore: / etc." --> I["bump = patch → M.N.(P+1)"]
    G & H & I --> C

    C --> J["Step 2: npm version in package.json + lock"]
    J --> K["Step 3–7: CHANGELOG, ROADMAP, BACKLOG, README"]
    K --> L["Step 8: branch / commit / push / PR"]

    subgraph "publish.yml compute-version (CI)"
        M["git describe last tag"] --> N{COMMITS since tag}
        N -- "= 0" --> O["VERSION = M.N.P-dev.0"]
        N -- "> 0" --> P["VERSION = M.N.(P+1)-dev.COMMITS"]
    end

    subgraph "bench-version.js"
        Q["git describe"] --> R{COMMITS?}
        R -- "= 0" --> S["M.N.P-dev.0"]
        R -- "> 0" --> T["M.N.(P+1)-dev.COMMITS"]
        R -- "no tags" --> U["git rev-parse --short HEAD"]
        U -- success --> V["M.N.(P+1)-dev.SHA"]
        U -- fail --> W["M.N.(P+1)-dev"]
    end
Loading

Last reviewed commit: 7185f9b

Comment on lines 47 to 52
// Fallback: no git or no tags — match publish.yml's no-tags behavior (COMMITS=1)
const parts = pkgVersion.split('.');
if (parts.length === 3) {
const [major, minor, patch] = parts;
try {
const hash = execFileSync('git', ['rev-parse', '--short', 'HEAD'], { cwd, ...GIT_OPTS }).trim();
return `${major}.${minor}.${Number(patch) + 1}-dev.${hash}`;
} catch {
return `${major}.${minor}.${Number(patch) + 1}-dev`;
}
return `${major}.${minor}.${Number(patch) + 1}-dev.1`;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Hardcoded dev.1 breaks benchmark deduplication uniqueness

The file comment explicitly states the version string is used to deduplicate benchmark reports. The previous fallback tried to fetch the short SHA to keep builds unique; if that failed it returned a bare -dev suffix. The new fallback always returns dev.1, so every run in a no-tags environment (CI machines that haven't fetched tags, fresh checkouts, etc.) will produce the same version string, causing later benchmark runs to silently overwrite the data of earlier ones.

Suggested change
// Fallback: no git or no tags — match publish.yml's no-tags behavior (COMMITS=1)
const parts = pkgVersion.split('.');
if (parts.length === 3) {
const [major, minor, patch] = parts;
try {
const hash = execFileSync('git', ['rev-parse', '--short', 'HEAD'], { cwd, ...GIT_OPTS }).trim();
return `${major}.${minor}.${Number(patch) + 1}-dev.${hash}`;
} catch {
return `${major}.${minor}.${Number(patch) + 1}-dev`;
}
return `${major}.${minor}.${Number(patch) + 1}-dev.1`;
}
const parts = pkgVersion.split('.');
if (parts.length === 3) {
const [major, minor, patch] = parts;
// Try to get a unique SHA so repeated runs don't collide in benchmark reports
try {
const hash = execFileSync('git', ['rev-parse', '--short', 'HEAD'], { cwd, ...GIT_OPTS }).trim();
return `${major}.${minor}.${Number(patch) + 1}-dev.${hash}`;
} catch {
return `${major}.${minor}.${Number(patch) + 1}-dev`;
}
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in fc7497d — restored SHA-based uniqueness in the no-tags fallback. The fallback now tries git rev-parse --short HEAD for a unique identifier, falling back to a bare -dev suffix only if git is completely unavailable.

Comment on lines +80 to +84
if [ "$COMMITS" -eq 0 ]; then
VERSION="${MAJOR}.${MINOR}.${PATCH}"
else
VERSION="${MAJOR}.${MINOR}.$((PATCH + 1))-dev.${COMMITS}"
fi
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 COMMITS=0 emits a clean semver dev build, no -dev suffix

When COMMITS is 0 (HEAD is exactly at a release tag), the new branch produces VERSION="${MAJOR}.${MINOR}.${PATCH}" with NPM_TAG="dev". The old code always appended -dev.${SHORT_SHA}, so dev builds could never be confused with a stable release version.

While the preflight condition filters commits whose message starts with chore: release v, this protection is fragile — a manually-placed tag on any non-release commit (or a squash-merge with a custom message) can still set COMMITS=0 without triggering the skip. If the publish-dev job then fires, the pre-release on GitHub is tagged dev-v${MAJOR}.${MINOR}.${PATCH}, which visually looks identical to a stable v${MAJOR}.${MINOR}.${PATCH} release and could mislead users picking up tarballs.

Suggested change
if [ "$COMMITS" -eq 0 ]; then
VERSION="${MAJOR}.${MINOR}.${PATCH}"
else
VERSION="${MAJOR}.${MINOR}.$((PATCH + 1))-dev.${COMMITS}"
fi
if [ "$COMMITS" -eq 0 ]; then
VERSION="${MAJOR}.${MINOR}.${PATCH}-dev.0"
else
VERSION="${MAJOR}.${MINOR}.$((PATCH + 1))-dev.${COMMITS}"
fi

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 7185f9b — COMMITS=0 now emits MAJOR.MINOR.PATCH-dev.0 in publish.yml, and the matching change in bench-version.js (fc7497d). Dev builds at an exact tag are no longer confused with stable releases.

Comment on lines +41 to +42
// Dev build: MAJOR.MINOR.(PATCH+1)-dev.COMMITS
return `${major}.${minor}.${Number(patch) + 1}-dev.${commits}`;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Commit count is not unique across cherry-picks or rebases

Replacing the short SHA with the commit count removes per-commit uniqueness. Two different commits with the same count since the last tag (possible after a revert-and-redo, interactive rebase, or cherry-pick) will produce the same version string, causing the benchmark report to silently overwrite earlier data. The SHA guaranteed uniqueness; the count only guarantees monotonicity on a linear history.

This isn't critical for normal CI runs on main, but it is worth noting as a trade-off compared to the previous behavior — especially since the whole purpose of this identifier (as stated in the file's header comment) is to prevent overwriting benchmark data.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged — the commit count trades per-commit uniqueness for human-readable ordering. This is acceptable for normal CI on main (linear history). The fallback path now uses SHA for uniqueness (fc7497d), and the COMMITS=0 edge case is handled with -dev.0 (7185f9b).

Impact: 71 functions changed, 255 affected
@carlos-alm
Copy link
Contributor Author

@greptileai

…dev.0 suffix

The no-tags fallback hardcoded dev.1, causing repeated benchmark runs
to silently overwrite each other. Restore short SHA for uniqueness.
Also add -dev.0 suffix when COMMITS=0 so dev builds at an exact tag
are never confused with stable releases.

Impact: 1 functions changed, 0 affected
When HEAD is exactly at a release tag, the dev build emitted a clean
semver string identical to the stable release. Add -dev.0 suffix to
prevent confusion between dev pre-releases and stable releases.
@carlos-alm
Copy link
Contributor Author

@greptileai

@carlos-alm carlos-alm merged commit 974a3b5 into main Mar 17, 2026
13 checks passed
@carlos-alm carlos-alm deleted the feat/release-skill-auto-semver branch March 17, 2026 11:24
@github-actions github-actions bot locked and limited conversation to collaborators Mar 17, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant