fix(release): populate changelog and server-changes on release/dispatch (#4204)

This commit is contained in:
claude[bot]
2026-07-09 17:46:39 +01:00
committed by GitHub
parent 580f94a955
commit 105f48927d
3 changed files with 123 additions and 21 deletions
+15
View File
@@ -143,6 +143,21 @@ jobs:
STEPS_GET_VERSION_OUTPUTS_IS_PRERELEASE: ${{ steps.get_version.outputs.is_prerelease }}
run: |
VERSION="${STEPS_GET_VERSION_OUTPUTS_PACKAGE_VERSION}"
# On the pull_request merge event RELEASE_PR_BODY is the merged "Version Packages"
# PR body, which holds the changelog. On a workflow_dispatch re-run (the recovery
# path after a failed merge-triggered run) there is no pull_request context, so it's
# empty. Fall back to the body of the most recently merged changeset-release/main PR
# so the "What's changed" section is still populated.
if [ -z "${RELEASE_PR_BODY}" ]; then
echo "RELEASE_PR_BODY is empty (workflow_dispatch re-run); fetching merged changeset-release/main PR body"
RELEASE_PR_BODY="$(gh pr list --repo triggerdotdev/trigger.dev \
--head changeset-release/main --state merged \
--json body,mergedAt --limit 10 \
-q 'sort_by(.mergedAt) | reverse | .[0].body')"
fi
export RELEASE_PR_BODY
node scripts/generate-github-release.mjs "$VERSION" > /tmp/release-body.md
PRERELEASE_FLAG=""
if [ "${STEPS_GET_VERSION_OUTPUTS_IS_PRERELEASE}" = "true" ]; then
+93 -21
View File
@@ -183,28 +183,8 @@ async function getPrForCommit(commitSha) {
// --- Parse .server-changes/ files ---
async function parseServerChanges() {
const dir = join(ROOT_DIR, ".server-changes");
const entries = [];
let files;
try {
files = await fs.readdir(dir);
} catch {
return entries;
}
// Collect file info and look up commits in parallel
const fileData = [];
for (const file of files) {
if (!file.endsWith(".md") || file === "README.md") continue;
const filePath = join(".server-changes", file);
const content = await fs.readFile(join(dir, file), "utf-8");
const parsed = parseFrontmatter(content);
if (!parsed.body.trim()) continue;
fileData.push({ filePath, parsed });
}
const fileData = await getServerChangeFileData();
// Look up commits for all files in parallel
const commits = await Promise.all(fileData.map((f) => getCommitForFile(f.filePath)));
@@ -232,6 +212,98 @@ async function parseServerChanges() {
return entries;
}
async function getServerChangeFileData() {
// The changesets version command deletes .server-changes before this script
// enhances the release PR body. We combine files still live on disk with the
// ones recovered from the release branch diff, deduped by filename, rather
// than picking one source or the other. This is additive so a partial cleanup
// (some files deleted, some still live) can't silently drop entries. Live
// files win on collision since they are the current on-disk truth.
const [live, deleted] = await Promise.all([
getLiveServerChangeFileData(),
getDeletedServerChangeFileDataFromReleaseBranch(),
]);
const byName = new Map();
for (const fileData of deleted) {
byName.set(fileData.filePath.split("/").pop(), fileData);
}
for (const fileData of live) {
byName.set(fileData.filePath.split("/").pop(), fileData);
}
return [...byName.values()].sort((a, b) => a.filePath.localeCompare(b.filePath));
}
async function getLiveServerChangeFileData() {
const dir = join(ROOT_DIR, ".server-changes");
let files;
try {
files = await fs.readdir(dir);
} catch {
return [];
}
const fileData = [];
for (const file of files.sort()) {
if (!file.endsWith(".md") || file === "README.md") continue;
const filePath = join(".server-changes", file);
const content = await fs.readFile(join(dir, file), "utf-8");
const parsed = parseFrontmatter(content);
if (!parsed.body.trim()) continue;
fileData.push({ filePath, parsed });
}
return fileData;
}
async function getDeletedServerChangeFileDataFromReleaseBranch() {
const baseRef = process.env.SERVER_CHANGES_BASE_REF || "origin/main";
const releaseRef = process.env.SERVER_CHANGES_RELEASE_REF || "origin/changeset-release/main";
let mergeBase;
let deletedFiles;
try {
mergeBase = await gitExec(["merge-base", baseRef, releaseRef]);
deletedFiles = await gitExec([
"diff",
"--name-only",
"--diff-filter=D",
`${mergeBase}..${releaseRef}`,
"--",
".server-changes",
]);
} catch (err) {
console.error(
"[enhance-release-pr] failed to recover deleted server-changes from release branch:",
err
);
return [];
}
const fileData = [];
for (const filePath of deletedFiles.split("\n").filter(Boolean).sort()) {
const file = filePath.split("/").pop();
if (!file?.endsWith(".md") || file === "README.md") continue;
try {
const content = await gitExec(["show", `${mergeBase}:${filePath}`]);
const parsed = parseFrontmatter(content);
if (!parsed.body.trim()) continue;
fileData.push({ filePath, parsed });
} catch (err) {
// If an individual file cannot be recovered, skip it rather than hiding
// all other server changes.
console.error(`[enhance-release-pr] failed to read deleted server-change ${filePath}:`, err);
}
}
return fileData;
}
function parseFrontmatter(content) {
const match = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
if (!match) return { frontmatter: {}, body: content };
+15
View File
@@ -239,6 +239,21 @@ async function main() {
}
const changesContent = extractChangesFromPrBody(prBody);
// Fail loudly when a stable (non-prerelease) release resolved no changelog. This
// guards against publishing an empty "What's changed" section, which previously
// happened silently on workflow_dispatch re-runs where RELEASE_PR_BODY was empty.
// Prereleases (any version with a hyphen, e.g. 4.5.0-rc.0) are allowed to be empty.
const isPrerelease = version.includes("-");
if (!changesContent && !isPrerelease) {
console.error(
`Error: no "What's changed" content could be resolved for v${version}. ` +
`RELEASE_PR_BODY was empty or contained no changelog entries. ` +
`Refusing to publish a release with an empty changelog.`
);
process.exit(1);
}
const contributors = getContributors(getPreviousVersion(version));
const packages = getPublishedPackages();