Skip to content

Commit 5533154

Browse files
yanxue06claude
andcommitted
feat(release): determine version from the actual code diff, not just commit messages
The AI version blocks keyed the major bump only on conventional-commit markers (BREAKING CHANGE: / type!), which authors rarely write, so breaking releases shipped as minor/patch (e.g. BlueStatus #11 -> 0.2.0 despite removed endpoints). - Inject the diff into the prompt. openai/codex-action is an agentic `codex exec` that does not auto-attach a diff (on the v0.2.0-rc.11 run the agent only ran `git log`), so a new step pre-computes name-status + churn + the (capped) diff and embeds it; the version is decided on real code now. - Diff-first rules: explicit markers stay authoritative; otherwise the agent may bump the major from a backward-incompatible diff alone. Conservative guardrail keeps purely additive changes minor. - Compute the version and diff range from the last STABLE release, so rc-to-rc publishes stay on one core line instead of advancing off a prerelease tag. - Initial release (no stable) diffs against the empty tree so the root commit is included. - Fail fast (set -euo pipefail, no `|| true` masking) and cap every section so the 1 MB/job GITHUB_OUTPUT budget can't be blown. - effort: high. Same fixes applied to bump-monorepo-versions. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent cc25749 commit 5533154

2 files changed

Lines changed: 135 additions & 25 deletions

File tree

.github/blocks/bump-monorepo-versions/action.yaml

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -90,19 +90,39 @@ runs:
9090
PKG_COUNT=$(echo "$CHANGED_JSON" | jq 'length')
9191
PROMPT_PARTS=""
9292
93+
# Resolve a valid base once: the last release if it is an ancestor of
94+
# HEAD, else the repo root. Used unguarded below so a genuine git failure
95+
# stops the run loudly instead of publishing on an empty diff.
96+
if git merge-base --is-ancestor "$LAST_SHA" HEAD 2>/dev/null; then
97+
BASE="$LAST_SHA"
98+
else
99+
BASE=$(git rev-list --max-parents=0 HEAD | tail -1)
100+
fi
101+
93102
for i in $(seq 0 $(( PKG_COUNT - 1 ))); do
94103
NAME=$(echo "$CHANGED_JSON" | jq -r ".[$i].name")
95104
PKG_PATH=$(echo "$CHANGED_JSON" | jq -r ".[$i].path")
96105
CURRENT_VER=$(jq -r '.version // "0.0.0"' "$PKG_PATH/package.json")
97106
98-
COMMITS=$(git log --oneline "${LAST_SHA}..HEAD" -- "$PKG_PATH" 2>/dev/null || git log --oneline HEAD~10..HEAD -- "$PKG_PATH")
107+
COMMITS=$(git log --oneline "$BASE..HEAD" -- "$PKG_PATH")
108+
NAME_STATUS=$(git diff --name-status "$BASE..HEAD" -- "$PKG_PATH")
109+
PKG_DIFF=$(git diff "$BASE..HEAD" -- "$PKG_PATH")
110+
CAP=100000
111+
if [ "${#PKG_DIFF}" -gt "$CAP" ]; then
112+
PKG_DIFF="${PKG_DIFF:0:$CAP}
113+
(diff truncated at ${CAP} chars — run: git diff $BASE..HEAD -- ${PKG_PATH} for the full diff)"
114+
fi
99115
100116
PROMPT_PARTS="${PROMPT_PARTS}
101117
Package: ${NAME}
102118
Current version: ${CURRENT_VER}
103119
Path: ${PKG_PATH}
104120
Commits:
105121
${COMMITS}
122+
Files changed (A=added M=modified D=deleted R=renamed):
123+
${NAME_STATUS}
124+
Diff:
125+
${PKG_DIFF}
106126
---"
107127
done
108128
@@ -117,18 +137,29 @@ runs:
117137
with:
118138
openai-api-key: ${{ inputs.openai-api-key }}
119139
safety-strategy: read-only
140+
effort: high
120141
prompt: |
121142
You are a release manager for the "${{ inputs.service-name }}" monorepo.
122143
Prerelease: ${{ inputs.prerelease }}
123144
124-
For each package below, determine the next semantic version based on its commits.
125-
Rules:
126-
- BREAKING CHANGE or ! after type = major bump
127-
- feat: = minor bump
128-
- fix:, perf:, or other = patch bump
129-
- Default to patch if unclear
130-
- Each package is versioned independently
131-
- For prereleases: use same version logic, suffix will be added automatically
145+
For each package below, determine the next semantic version. Base the
146+
decision on the ACTUAL CODE CHANGES — the commits, the changed-file
147+
list, and the diff are all provided per package. Commit messages are a
148+
hint, not the source of truth: most authors do not annotate breaking
149+
changes, so the diff is authoritative.
150+
151+
Rules (apply in order, per package, versioned independently):
152+
- MAJOR — an explicit `BREAKING CHANGE:` footer or `type!:` marker, OR a
153+
diff that is backward-incompatible even without one (a public/exported
154+
symbol or entry point removed or renamed, a schema/required field or
155+
return shape changed, default behavior or a config contract changed).
156+
You are explicitly allowed to bump the major from the diff alone.
157+
- MINOR — backward-compatible new functionality is ADDED (feat:).
158+
- PATCH — fixes, perf, refactors, docs, chores; nothing public removed
159+
or broken.
160+
Be conservative about MAJOR: purely additive changes are MINOR, and when
161+
genuinely unclear pick the lower bump. For prereleases the -rc suffix is
162+
added automatically.
132163
133164
${{ steps.commit-logs.outputs.prompt }}
134165

.github/blocks/determine-publish-version/action.yaml

Lines changed: 95 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -64,36 +64,115 @@ runs:
6464
core.setOutput('version', '0.0.0');
6565
}
6666
67-
// For classification: prefer the last stable. Falls back to
68-
// `0.0.0` if no stable release exists yet (initial-release path).
69-
core.setOutput(
70-
'stable_version',
71-
lastStable ? lastStable.tag_name.replace(/^v/, '') : '0.0.0'
72-
);
67+
// Last stable: its SHA is the base for both the diff range and the
68+
// version computation (so rc-to-rc publishes stay on the same core
69+
// line instead of advancing off a prerelease tag), and its version
70+
// is the classification baseline. Empty/0.0.0 on the initial release.
71+
if (lastStable) {
72+
const { data: sref } = await github.rest.git.getRef({
73+
owner: context.repo.owner,
74+
repo: context.repo.repo,
75+
ref: `tags/${lastStable.tag_name}`
76+
});
77+
core.setOutput('stable_sha', sref.object.sha);
78+
core.setOutput('stable_version', lastStable.tag_name.replace(/^v/, ''));
79+
} else {
80+
core.setOutput('stable_sha', '');
81+
core.setOutput('stable_version', '0.0.0');
82+
}
7383
} catch (error) {
7484
core.setOutput('sha', context.sha);
7585
core.setOutput('version', '0.0.0');
86+
core.setOutput('stable_sha', '');
7687
core.setOutput('stable_version', '0.0.0');
7788
}
7889
90+
- name: Gather change context
91+
id: context
92+
shell: bash
93+
env:
94+
BASE_SHA: ${{ steps.last-release.outputs.stable_sha }}
95+
HEAD_SHA: ${{ github.sha }}
96+
run: |
97+
set -euo pipefail
98+
# No prior stable release -> diff against the empty tree so the entire
99+
# history (including the root commit) is included for the initial release.
100+
if [ -n "$BASE_SHA" ]; then
101+
DIFF_BASE="$BASE_SHA"; LOG_RANGE="$BASE_SHA..$HEAD_SHA"
102+
else
103+
DIFF_BASE=$(git hash-object -t tree /dev/null); LOG_RANGE="$HEAD_SHA"
104+
fi
105+
clip() { if [ "${#1}" -gt "$2" ]; then printf '%s\n(...truncated at %s chars; run git diff for the rest...)' "${1:0:$2}" "$2"; else printf '%s' "$1"; fi; }
106+
NAME_STATUS=$(clip "$(git diff --name-status "$DIFF_BASE" "$HEAD_SHA")" 60000)
107+
STAT=$(clip "$(git diff --stat "$DIFF_BASE" "$HEAD_SHA")" 40000)
108+
LOG=$(clip "$(git log --no-merges --format='- %s%n%b' $LOG_RANGE)" 100000)
109+
DIFF=$(clip "$(git diff "$DIFF_BASE" "$HEAD_SHA")" 400000)
110+
MARKER=$(openssl rand -hex 16)
111+
{
112+
echo "context<<${MARKER}"
113+
echo "## Commit messages"
114+
echo "$LOG"
115+
echo
116+
echo "## Files changed (A=added M=modified D=deleted R=renamed)"
117+
echo "$NAME_STATUS"
118+
echo
119+
echo "## Churn summary"
120+
echo "$STAT"
121+
echo
122+
echo "## Full diff"
123+
echo '```diff'
124+
echo "$DIFF"
125+
echo '```'
126+
echo "${MARKER}"
127+
} >> "$GITHUB_OUTPUT"
128+
79129
- name: AI Determine Version
80130
id: ai-version
81131
uses: openai/codex-action@v1
82132
with:
83133
openai-api-key: ${{ inputs.openai-api-key }}
84134
safety-strategy: read-only
135+
effort: high
85136
prompt: |
86-
Analyze commits between ${{ steps.last-release.outputs.sha }} and ${{ github.sha }}.
87-
Current version: ${{ steps.last-release.outputs.version }}
137+
Determine the next semantic version after ${{ steps.last-release.outputs.stable_version }}.
138+
Current version: ${{ steps.last-release.outputs.stable_version }}
88139
Prerelease: ${{ inputs.prerelease }}
89-
90-
Rules:
91-
- BREAKING CHANGE or ! = major bump
92-
- feat: = minor bump
93-
- fix:, perf:, or other = patch bump
94-
- Default to patch if unclear
95-
- For prereleases: use same version logic, suffix will be added automatically
96-
140+
141+
Base your decision on the ACTUAL CODE CHANGES below — the commit
142+
messages, the list of changed files, and the diff are all provided.
143+
Commit messages are a hint, not the source of truth: most authors do
144+
not annotate breaking changes, so the diff is authoritative. If the diff
145+
was truncated you may run `git diff` yourself (read-only) for any file.
146+
147+
How to choose the bump (apply in order):
148+
1. MAJOR — if a commit message carries an explicit `BREAKING CHANGE:`
149+
footer or a `type!:` marker, that is authoritative: bump major.
150+
ALSO bump major if the DIFF itself is backward-incompatible even when
151+
no commit said so — e.g. a public endpoint/route/exported symbol is
152+
removed or renamed, a request/response schema or required field
153+
changes shape, default behavior changes in a way that breaks existing
154+
callers, or a config/runtime contract changes. You are explicitly
155+
allowed to bump the major from the diff alone.
156+
2. MINOR — backward-COMPATIBLE new functionality: a new endpoint, flag,
157+
exported symbol, or capability is ADDED without breaking existing
158+
ones (corresponds to `feat:`).
159+
3. PATCH — bug fixes, performance, internal refactors, docs, chores, or
160+
anything else that neither adds public surface nor breaks it.
161+
162+
Be conservative about MAJOR to avoid false positives: purely additive
163+
changes are MINOR, not MAJOR. Only call it breaking if existing public
164+
behavior is actually removed or changed incompatibly. When genuinely
165+
unclear between two levels, pick the lower one (default to patch).
166+
167+
A major bump of ${{ steps.last-release.outputs.stable_version }} increments
168+
the first number and zeroes the rest (e.g. 0.1.5 -> 1.0.0); minor
169+
increments the second (0.1.5 -> 0.2.0); patch the third (0.1.5 -> 0.1.6).
170+
For prereleases use the same logic — the -rc suffix is added automatically.
171+
172+
=== CHANGES SINCE ${{ steps.last-release.outputs.stable_version }} ===
173+
${{ steps.context.outputs.context }}
174+
=== END CHANGES ===
175+
97176
Respond with ONLY X.Y.Z
98177
99178
- name: Parse Version

0 commit comments

Comments
 (0)