Skip to content

Commit 105f489

Browse files
authored
fix(release): populate changelog and server-changes on release/dispatch (#4204)
1 parent 580f94a commit 105f489

3 files changed

Lines changed: 123 additions & 21 deletions

File tree

.github/workflows/release.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,21 @@ jobs:
143143
STEPS_GET_VERSION_OUTPUTS_IS_PRERELEASE: ${{ steps.get_version.outputs.is_prerelease }}
144144
run: |
145145
VERSION="${STEPS_GET_VERSION_OUTPUTS_PACKAGE_VERSION}"
146+
147+
# On the pull_request merge event RELEASE_PR_BODY is the merged "Version Packages"
148+
# PR body, which holds the changelog. On a workflow_dispatch re-run (the recovery
149+
# path after a failed merge-triggered run) there is no pull_request context, so it's
150+
# empty. Fall back to the body of the most recently merged changeset-release/main PR
151+
# so the "What's changed" section is still populated.
152+
if [ -z "${RELEASE_PR_BODY}" ]; then
153+
echo "RELEASE_PR_BODY is empty (workflow_dispatch re-run); fetching merged changeset-release/main PR body"
154+
RELEASE_PR_BODY="$(gh pr list --repo triggerdotdev/trigger.dev \
155+
--head changeset-release/main --state merged \
156+
--json body,mergedAt --limit 10 \
157+
-q 'sort_by(.mergedAt) | reverse | .[0].body')"
158+
fi
159+
export RELEASE_PR_BODY
160+
146161
node scripts/generate-github-release.mjs "$VERSION" > /tmp/release-body.md
147162
PRERELEASE_FLAG=""
148163
if [ "${STEPS_GET_VERSION_OUTPUTS_IS_PRERELEASE}" = "true" ]; then

scripts/enhance-release-pr.mjs

Lines changed: 93 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -183,28 +183,8 @@ async function getPrForCommit(commitSha) {
183183
// --- Parse .server-changes/ files ---
184184

185185
async function parseServerChanges() {
186-
const dir = join(ROOT_DIR, ".server-changes");
187186
const entries = [];
188-
189-
let files;
190-
try {
191-
files = await fs.readdir(dir);
192-
} catch {
193-
return entries;
194-
}
195-
196-
// Collect file info and look up commits in parallel
197-
const fileData = [];
198-
for (const file of files) {
199-
if (!file.endsWith(".md") || file === "README.md") continue;
200-
201-
const filePath = join(".server-changes", file);
202-
const content = await fs.readFile(join(dir, file), "utf-8");
203-
const parsed = parseFrontmatter(content);
204-
if (!parsed.body.trim()) continue;
205-
206-
fileData.push({ filePath, parsed });
207-
}
187+
const fileData = await getServerChangeFileData();
208188

209189
// Look up commits for all files in parallel
210190
const commits = await Promise.all(fileData.map((f) => getCommitForFile(f.filePath)));
@@ -232,6 +212,98 @@ async function parseServerChanges() {
232212
return entries;
233213
}
234214

215+
async function getServerChangeFileData() {
216+
// The changesets version command deletes .server-changes before this script
217+
// enhances the release PR body. We combine files still live on disk with the
218+
// ones recovered from the release branch diff, deduped by filename, rather
219+
// than picking one source or the other. This is additive so a partial cleanup
220+
// (some files deleted, some still live) can't silently drop entries. Live
221+
// files win on collision since they are the current on-disk truth.
222+
const [live, deleted] = await Promise.all([
223+
getLiveServerChangeFileData(),
224+
getDeletedServerChangeFileDataFromReleaseBranch(),
225+
]);
226+
227+
const byName = new Map();
228+
for (const fileData of deleted) {
229+
byName.set(fileData.filePath.split("/").pop(), fileData);
230+
}
231+
for (const fileData of live) {
232+
byName.set(fileData.filePath.split("/").pop(), fileData);
233+
}
234+
235+
return [...byName.values()].sort((a, b) => a.filePath.localeCompare(b.filePath));
236+
}
237+
238+
async function getLiveServerChangeFileData() {
239+
const dir = join(ROOT_DIR, ".server-changes");
240+
241+
let files;
242+
try {
243+
files = await fs.readdir(dir);
244+
} catch {
245+
return [];
246+
}
247+
248+
const fileData = [];
249+
for (const file of files.sort()) {
250+
if (!file.endsWith(".md") || file === "README.md") continue;
251+
252+
const filePath = join(".server-changes", file);
253+
const content = await fs.readFile(join(dir, file), "utf-8");
254+
const parsed = parseFrontmatter(content);
255+
if (!parsed.body.trim()) continue;
256+
257+
fileData.push({ filePath, parsed });
258+
}
259+
260+
return fileData;
261+
}
262+
263+
async function getDeletedServerChangeFileDataFromReleaseBranch() {
264+
const baseRef = process.env.SERVER_CHANGES_BASE_REF || "origin/main";
265+
const releaseRef = process.env.SERVER_CHANGES_RELEASE_REF || "origin/changeset-release/main";
266+
267+
let mergeBase;
268+
let deletedFiles;
269+
try {
270+
mergeBase = await gitExec(["merge-base", baseRef, releaseRef]);
271+
deletedFiles = await gitExec([
272+
"diff",
273+
"--name-only",
274+
"--diff-filter=D",
275+
`${mergeBase}..${releaseRef}`,
276+
"--",
277+
".server-changes",
278+
]);
279+
} catch (err) {
280+
console.error(
281+
"[enhance-release-pr] failed to recover deleted server-changes from release branch:",
282+
err
283+
);
284+
return [];
285+
}
286+
287+
const fileData = [];
288+
for (const filePath of deletedFiles.split("\n").filter(Boolean).sort()) {
289+
const file = filePath.split("/").pop();
290+
if (!file?.endsWith(".md") || file === "README.md") continue;
291+
292+
try {
293+
const content = await gitExec(["show", `${mergeBase}:${filePath}`]);
294+
const parsed = parseFrontmatter(content);
295+
if (!parsed.body.trim()) continue;
296+
fileData.push({ filePath, parsed });
297+
} catch (err) {
298+
// If an individual file cannot be recovered, skip it rather than hiding
299+
// all other server changes.
300+
console.error(`[enhance-release-pr] failed to read deleted server-change ${filePath}:`, err);
301+
}
302+
}
303+
304+
return fileData;
305+
}
306+
235307
function parseFrontmatter(content) {
236308
const match = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
237309
if (!match) return { frontmatter: {}, body: content };

scripts/generate-github-release.mjs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,21 @@ async function main() {
239239
}
240240

241241
const changesContent = extractChangesFromPrBody(prBody);
242+
243+
// Fail loudly when a stable (non-prerelease) release resolved no changelog. This
244+
// guards against publishing an empty "What's changed" section, which previously
245+
// happened silently on workflow_dispatch re-runs where RELEASE_PR_BODY was empty.
246+
// Prereleases (any version with a hyphen, e.g. 4.5.0-rc.0) are allowed to be empty.
247+
const isPrerelease = version.includes("-");
248+
if (!changesContent && !isPrerelease) {
249+
console.error(
250+
`Error: no "What's changed" content could be resolved for v${version}. ` +
251+
`RELEASE_PR_BODY was empty or contained no changelog entries. ` +
252+
`Refusing to publish a release with an empty changelog.`
253+
);
254+
process.exit(1);
255+
}
256+
242257
const contributors = getContributors(getPreviousVersion(version));
243258
const packages = getPublishedPackages();
244259

0 commit comments

Comments
 (0)