Skip to content

Commit 5126d9d

Browse files
gjtorikianclaude
andcommitted
ci: add changelog fragment inlining + own GitHub Release creation
Port the full release-please workflow from workos-kotlin with changelog fragment inlining and publish-release job. Release-please now skips creating GitHub Releases directly; instead publish-release detects merged release PRs, extracts rich notes from CHANGELOG.md, and creates the tag + release. The publish job now gates on publish-release output. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 296c2d9 commit 5126d9d

1 file changed

Lines changed: 181 additions & 9 deletions

File tree

.github/workflows/release-please.yml

Lines changed: 181 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616
contents: write
1717
pull-requests: write
1818
outputs:
19-
release_created: ${{ steps.release.outputs.release_created }}
19+
pr: ${{ steps.release.outputs.pr }}
2020
steps:
2121
- name: Generate token
2222
id: generate-token
@@ -25,36 +25,208 @@ jobs:
2525
app-id: ${{ vars.SDK_BOT_APP_ID }}
2626
private-key: ${{ secrets.SDK_BOT_PRIVATE_KEY }}
2727

28+
# skip-github-release means release-please opens/updates release
29+
# PRs and updates CHANGELOG.md as usual, but does NOT tag or create
30+
# the GitHub Release on merge. Those are owned by publish-release
31+
# below so we can set the Release body from the rich CHANGELOG.md
32+
# section instead of release-please's terse default rendering.
2833
- name: Run release-please
2934
id: release
3035
uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5.0.0
3136
with:
3237
token: ${{ steps.generate-token.outputs.token }}
38+
skip-github-release: true
3339

3440
- name: Checkout release PR branch
35-
if: steps.release.outputs.prs_created == 'true'
41+
if: steps.release.outputs.pr
3642
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
3743
with:
38-
ref: ${{ fromJSON(steps.release.outputs.prs)[0].headBranchName }}
44+
ref: ${{ fromJSON(steps.release.outputs.pr).headBranchName }}
3945
token: ${{ steps.generate-token.outputs.token }}
4046

4147
- name: Install uv
42-
if: steps.release.outputs.prs_created == 'true'
48+
if: steps.release.outputs.pr
4349
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
4450

4551
- name: Update lockfile
46-
if: steps.release.outputs.prs_created == 'true'
52+
if: steps.release.outputs.pr
4753
run: |
4854
uv lock
49-
git config user.name "workos-bot[bot]"
50-
git config user.email "workos-bot[bot]@users.noreply.github.com"
55+
git config user.name "workos-sdk-automation[bot]"
56+
git config user.email "255426317+workos-sdk-automation[bot]@users.noreply.github.com"
5157
git add uv.lock
5258
git diff --staged --quiet || git commit -m "chore: update uv.lock"
5359
git push
5460
61+
# Inline pending changelog fragments under the version heading
62+
# release-please just wrote in CHANGELOG.md. For PRs that have a
63+
# fragment (the autogen flow always writes one), drop the line
64+
# release-please rendered and use the fragment instead. For PRs
65+
# without a fragment (typical for human-authored PRs), keep what
66+
# release-please wrote. Fragments are deleted in the same commit.
67+
# Idempotent: if no fragments exist, skip silently.
68+
- name: Inline rich changelog fragments
69+
if: steps.release.outputs.pr
70+
env:
71+
PR_JSON: ${{ steps.release.outputs.pr }}
72+
run: |
73+
set -euo pipefail
74+
shopt -s nullglob
75+
fragments=(.changelog-pending/*.md)
76+
if [ ${#fragments[@]} -eq 0 ]; then
77+
echo "No .changelog-pending fragments; leaving release-please CHANGELOG.md as-is."
78+
exit 0
79+
fi
80+
81+
VERSION=$(echo "$PR_JSON" | jq -r '.title' | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')
82+
export VERSION
83+
84+
python3 - <<'PY'
85+
import os, re, pathlib, glob
86+
87+
version = os.environ["VERSION"]
88+
89+
# Load fragments + extract the PR number each one covers from
90+
# its top-line "* [#NN](url) ...".
91+
fragments = []
92+
covered = set()
93+
for path in sorted(glob.glob(".changelog-pending/*.md")):
94+
body = pathlib.Path(path).read_text().rstrip()
95+
m = re.search(r'\[#(\d+)\]', body)
96+
if m:
97+
covered.add(m.group(1))
98+
fragments.append(body)
99+
100+
changelog = pathlib.Path("CHANGELOG.md")
101+
text = changelog.read_text()
102+
section_re = re.compile(
103+
r'(^## \[' + re.escape(version) + r'\][^\n]*\n)(.*?)(?=^## |\Z)',
104+
re.MULTILINE | re.DOTALL,
105+
)
106+
match = section_re.search(text)
107+
if not match:
108+
raise SystemExit(f"Could not find '## [{version}]' heading in CHANGELOG.md")
109+
heading, body = match.group(1), match.group(2)
110+
111+
# Drop any release-please line that references a PR we have a
112+
# fragment for.
113+
kept = []
114+
for line in body.split("\n"):
115+
if any(pr in covered for pr in re.findall(r'\[#(\d+)\]', line)):
116+
continue
117+
kept.append(line)
118+
filtered = "\n".join(kept)
119+
120+
# Collapse "### Heading\n(blank lines)\n" with nothing under
121+
# it. Run repeatedly until stable in case of stacked empties.
122+
empty_section = re.compile(
123+
r'^### [^\n]*\n(?:\s*\n)*(?=^### |\Z)',
124+
re.MULTILINE,
125+
)
126+
while True:
127+
new = empty_section.sub('', filtered)
128+
if new == filtered:
129+
break
130+
filtered = new
131+
filtered = filtered.strip()
132+
133+
parts = []
134+
if filtered:
135+
parts.append(filtered)
136+
parts.extend(fragments)
137+
new_body = "\n\n".join(parts)
138+
139+
new_text = text[:match.start()] + heading + "\n" + new_body + "\n\n" + text[match.end():]
140+
changelog.write_text(new_text)
141+
PY
142+
143+
git config user.name "workos-sdk-automation[bot]"
144+
git config user.email "255426317+workos-sdk-automation[bot]@users.noreply.github.com"
145+
git rm .changelog-pending/*.md
146+
git add CHANGELOG.md
147+
git commit -m "chore: inline release notes from .changelog-pending"
148+
git push
149+
150+
# Detect when a release-please release PR has merged, then tag and
151+
# create the GitHub Release whose body is extracted from CHANGELOG.md
152+
# (now rich, after the inline step above). Runs on every push to main;
153+
# the detect step gates everything else.
154+
publish-release:
155+
runs-on: ubuntu-latest
156+
permissions:
157+
contents: write
158+
outputs:
159+
is-release: ${{ steps.detect.outputs.is-release }}
160+
version: ${{ steps.detect.outputs.version }}
161+
steps:
162+
- name: Generate token
163+
id: generate-token
164+
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # 3.1.1
165+
with:
166+
app-id: ${{ vars.SDK_BOT_APP_ID }}
167+
private-key: ${{ secrets.SDK_BOT_PRIVATE_KEY }}
168+
169+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
170+
with:
171+
fetch-depth: 0
172+
token: ${{ steps.generate-token.outputs.token }}
173+
174+
- name: Detect release PR merge
175+
id: detect
176+
run: |
177+
set -euo pipefail
178+
SUBJECT=$(git log -1 --format=%s)
179+
# release-please's default release PR title:
180+
# chore(main): release X.Y.Z
181+
if [[ "$SUBJECT" =~ ^chore\(.*\):[[:space:]]release[[:space:]]([0-9]+\.[0-9]+\.[0-9]+) ]]; then
182+
VERSION="${BASH_REMATCH[1]}"
183+
echo "is-release=true" >> "$GITHUB_OUTPUT"
184+
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
185+
echo "Detected release PR merge for v$VERSION"
186+
else
187+
echo "Not a release PR merge: $SUBJECT"
188+
echo "is-release=false" >> "$GITHUB_OUTPUT"
189+
fi
190+
191+
- name: Extract release notes from CHANGELOG.md
192+
if: steps.detect.outputs.is-release == 'true'
193+
env:
194+
VERSION: ${{ steps.detect.outputs.version }}
195+
run: |
196+
set -euo pipefail
197+
awk -v v="$VERSION" '
198+
$0 ~ ("^## \\[" v "\\]") { found=1; next }
199+
found && /^## \[/ { exit }
200+
found
201+
' CHANGELOG.md > /tmp/release-notes.md
202+
if [ ! -s /tmp/release-notes.md ]; then
203+
echo "::error::CHANGELOG.md has no body for v$VERSION"
204+
exit 1
205+
fi
206+
207+
- name: Tag and create GitHub Release
208+
if: steps.detect.outputs.is-release == 'true'
209+
env:
210+
GH_TOKEN: ${{ steps.generate-token.outputs.token }}
211+
VERSION: ${{ steps.detect.outputs.version }}
212+
run: |
213+
set -euo pipefail
214+
TAG="v$VERSION"
215+
if gh release view "$TAG" >/dev/null 2>&1; then
216+
echo "Release $TAG already exists; skipping."
217+
exit 0
218+
fi
219+
git config user.name "workos-sdk-automation[bot]"
220+
git config user.email "255426317+workos-sdk-automation[bot]@users.noreply.github.com"
221+
git tag -a "$TAG" -m "Release $TAG"
222+
git push origin "$TAG"
223+
gh release create "$TAG" \
224+
--title "$TAG" \
225+
--notes-file /tmp/release-notes.md
226+
55227
publish:
56-
needs: release-please
57-
if: needs.release-please.outputs.release_created == 'true'
228+
needs: publish-release
229+
if: needs.publish-release.outputs.is-release == 'true'
58230
runs-on: ubuntu-latest
59231
permissions:
60232
id-token: write

0 commit comments

Comments
 (0)