Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 40 additions & 9 deletions .github/blocks/bump-monorepo-versions/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -90,19 +90,39 @@ runs:
PKG_COUNT=$(echo "$CHANGED_JSON" | jq 'length')
PROMPT_PARTS=""

# Resolve a valid base once: the last release if it is an ancestor of
# HEAD, else the repo root. Used unguarded below so a genuine git failure
# stops the run loudly instead of publishing on an empty diff.
if git merge-base --is-ancestor "$LAST_SHA" HEAD 2>/dev/null; then
BASE="$LAST_SHA"
else
BASE=$(git rev-list --max-parents=0 HEAD | tail -1)
fi

for i in $(seq 0 $(( PKG_COUNT - 1 ))); do
NAME=$(echo "$CHANGED_JSON" | jq -r ".[$i].name")
PKG_PATH=$(echo "$CHANGED_JSON" | jq -r ".[$i].path")
CURRENT_VER=$(jq -r '.version // "0.0.0"' "$PKG_PATH/package.json")

COMMITS=$(git log --oneline "${LAST_SHA}..HEAD" -- "$PKG_PATH" 2>/dev/null || git log --oneline HEAD~10..HEAD -- "$PKG_PATH")
COMMITS=$(git log --oneline "$BASE..HEAD" -- "$PKG_PATH")
NAME_STATUS=$(git diff --name-status "$BASE..HEAD" -- "$PKG_PATH")
PKG_DIFF=$(git diff "$BASE..HEAD" -- "$PKG_PATH")
CAP=100000
if [ "${#PKG_DIFF}" -gt "$CAP" ]; then
PKG_DIFF="${PKG_DIFF:0:$CAP}
(diff truncated at ${CAP} chars — run: git diff $BASE..HEAD -- ${PKG_PATH} for the full diff)"
fi
Comment on lines +107 to +114

PROMPT_PARTS="${PROMPT_PARTS}
Package: ${NAME}
Current version: ${CURRENT_VER}
Path: ${PKG_PATH}
Commits:
Comment on lines 116 to 120
${COMMITS}
Files changed (A=added M=modified D=deleted R=renamed):
${NAME_STATUS}
Diff:
${PKG_DIFF}
---"
done

Expand All @@ -117,18 +137,29 @@ runs:
with:
openai-api-key: ${{ inputs.openai-api-key }}
safety-strategy: read-only
effort: high
prompt: |
You are a release manager for the "${{ inputs.service-name }}" monorepo.
Prerelease: ${{ inputs.prerelease }}

For each package below, determine the next semantic version based on its commits.
Rules:
- BREAKING CHANGE or ! after type = major bump
- feat: = minor bump
- fix:, perf:, or other = patch bump
- Default to patch if unclear
- Each package is versioned independently
- For prereleases: use same version logic, suffix will be added automatically
For each package below, determine the next semantic version. Base the
decision on the ACTUAL CODE CHANGES — the commits, the changed-file
list, and the diff are all provided per package. Commit messages are a
hint, not the source of truth: most authors do not annotate breaking
changes, so the diff is authoritative.

Rules (apply in order, per package, versioned independently):
- MAJOR — an explicit `BREAKING CHANGE:` footer or `type!:` marker, OR a
diff that is backward-incompatible even without one (a public/exported
symbol or entry point removed or renamed, a schema/required field or
return shape changed, default behavior or a config contract changed).
You are explicitly allowed to bump the major from the diff alone.
- MINOR — backward-compatible new functionality is ADDED (feat:).
- PATCH — fixes, perf, refactors, docs, chores; nothing public removed
or broken.
Be conservative about MAJOR: purely additive changes are MINOR, and when
genuinely unclear pick the lower bump. For prereleases the -rc suffix is
added automatically.

${{ steps.commit-logs.outputs.prompt }}

Expand Down
111 changes: 95 additions & 16 deletions .github/blocks/determine-publish-version/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -64,36 +64,115 @@ runs:
core.setOutput('version', '0.0.0');
}

// For classification: prefer the last stable. Falls back to
// `0.0.0` if no stable release exists yet (initial-release path).
core.setOutput(
'stable_version',
lastStable ? lastStable.tag_name.replace(/^v/, '') : '0.0.0'
);
// Last stable: its SHA is the base for both the diff range and the
// version computation (so rc-to-rc publishes stay on the same core
// line instead of advancing off a prerelease tag), and its version
// is the classification baseline. Empty/0.0.0 on the initial release.
if (lastStable) {
const { data: sref } = await github.rest.git.getRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: `tags/${lastStable.tag_name}`
});
core.setOutput('stable_sha', sref.object.sha);
core.setOutput('stable_version', lastStable.tag_name.replace(/^v/, ''));
} else {
core.setOutput('stable_sha', '');
core.setOutput('stable_version', '0.0.0');
}
} catch (error) {
core.setOutput('sha', context.sha);
core.setOutput('version', '0.0.0');
core.setOutput('stable_sha', '');
core.setOutput('stable_version', '0.0.0');
}

- name: Gather change context
id: context
shell: bash
env:
BASE_SHA: ${{ steps.last-release.outputs.stable_sha }}
HEAD_SHA: ${{ github.sha }}
run: |
set -euo pipefail
# No prior stable release -> diff against the empty tree so the entire
# history (including the root commit) is included for the initial release.
if [ -n "$BASE_SHA" ]; then
DIFF_BASE="$BASE_SHA"; LOG_RANGE="$BASE_SHA..$HEAD_SHA"
else
DIFF_BASE=$(git hash-object -t tree /dev/null); LOG_RANGE="$HEAD_SHA"
fi
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; }
NAME_STATUS=$(clip "$(git diff --name-status "$DIFF_BASE" "$HEAD_SHA")" 60000)
STAT=$(clip "$(git diff --stat "$DIFF_BASE" "$HEAD_SHA")" 40000)
LOG=$(clip "$(git log --no-merges --format='- %s%n%b' $LOG_RANGE)" 100000)
DIFF=$(clip "$(git diff "$DIFF_BASE" "$HEAD_SHA")" 400000)
Comment on lines +105 to +109
MARKER=$(openssl rand -hex 16)
{
echo "context<<${MARKER}"
echo "## Commit messages"
echo "$LOG"
echo
echo "## Files changed (A=added M=modified D=deleted R=renamed)"
echo "$NAME_STATUS"
echo
echo "## Churn summary"
echo "$STAT"
echo
echo "## Full diff"
echo '```diff'
echo "$DIFF"
echo '```'
echo "${MARKER}"
} >> "$GITHUB_OUTPUT"

- name: AI Determine Version
id: ai-version
uses: openai/codex-action@v1
with:
openai-api-key: ${{ inputs.openai-api-key }}
safety-strategy: read-only
effort: high
prompt: |
Analyze commits between ${{ steps.last-release.outputs.sha }} and ${{ github.sha }}.
Current version: ${{ steps.last-release.outputs.version }}
Determine the next semantic version after ${{ steps.last-release.outputs.stable_version }}.
Current version: ${{ steps.last-release.outputs.stable_version }}
Prerelease: ${{ inputs.prerelease }}

Rules:
- BREAKING CHANGE or ! = major bump
- feat: = minor bump
- fix:, perf:, or other = patch bump
- Default to patch if unclear
- For prereleases: use same version logic, suffix will be added automatically


Base your decision on the ACTUAL CODE CHANGES below — the commit
messages, the list of changed files, and the diff are all provided.
Commit messages are a hint, not the source of truth: most authors do
not annotate breaking changes, so the diff is authoritative. If the diff
was truncated you may run `git diff` yourself (read-only) for any file.

How to choose the bump (apply in order):
1. MAJOR — if a commit message carries an explicit `BREAKING CHANGE:`
footer or a `type!:` marker, that is authoritative: bump major.
ALSO bump major if the DIFF itself is backward-incompatible even when
no commit said so — e.g. a public endpoint/route/exported symbol is
removed or renamed, a request/response schema or required field
changes shape, default behavior changes in a way that breaks existing
callers, or a config/runtime contract changes. You are explicitly
allowed to bump the major from the diff alone.
2. MINOR — backward-COMPATIBLE new functionality: a new endpoint, flag,
exported symbol, or capability is ADDED without breaking existing
ones (corresponds to `feat:`).
3. PATCH — bug fixes, performance, internal refactors, docs, chores, or
anything else that neither adds public surface nor breaks it.

Be conservative about MAJOR to avoid false positives: purely additive
changes are MINOR, not MAJOR. Only call it breaking if existing public
behavior is actually removed or changed incompatibly. When genuinely
unclear between two levels, pick the lower one (default to patch).

A major bump of ${{ steps.last-release.outputs.stable_version }} increments
the first number and zeroes the rest (e.g. 0.1.5 -> 1.0.0); minor
increments the second (0.1.5 -> 0.2.0); patch the third (0.1.5 -> 0.1.6).
For prereleases use the same logic — the -rc suffix is added automatically.

=== CHANGES SINCE ${{ steps.last-release.outputs.stable_version }} ===
${{ steps.context.outputs.context }}
=== END CHANGES ===

Respond with ONLY X.Y.Z

- name: Parse Version
Expand Down
Loading