From 90226e1343189f1264adabf2a90ff18fb2c75c2f Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 9 Jul 2026 14:00:22 +0000 Subject: [PATCH 1/3] fix(release): populate changelog on workflow_dispatch reruns The "What's changed" section was built solely from github.event.pull_request.body, which is only present on the pull_request merge event. On a workflow_dispatch re-run that context is empty, so the GitHub release published with an empty changelog and failed silently. Resolve RELEASE_PR_BODY robustly: fall back to the body of the most recently merged changeset-release/main PR when the pull_request context is absent. Also fail loudly in generate-github-release.mjs when a non-prerelease resolves no changelog content, so an empty changelog can no longer publish silently. Prereleases remain exempt. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_014CjQVhMqLbRo3VuLaGSLfg --- .github/workflows/release.yml | 15 +++++++++++++++ scripts/generate-github-release.mjs | 15 +++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 15aa7fe50d..f6bef7ac5c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 diff --git a/scripts/generate-github-release.mjs b/scripts/generate-github-release.mjs index 8fc528df63..c7355ef774 100755 --- a/scripts/generate-github-release.mjs +++ b/scripts/generate-github-release.mjs @@ -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(); From a5228722c5efba1ef31ebc173840125a3596e363 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 9 Jul 2026 16:19:56 +0000 Subject: [PATCH 2/3] fix(release): recover deleted server-changes when enhancing release PR Read .server-changes from git when the working tree copy has already been cleaned up, and log recovery failures instead of swallowing them. Folds in the fix from #4209. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_014CjQVhMqLbRo3VuLaGSLfg --- scripts/enhance-release-pr.mjs | 101 ++++++++++++++++++++++++++------- 1 file changed, 80 insertions(+), 21 deletions(-) diff --git a/scripts/enhance-release-pr.mjs b/scripts/enhance-release-pr.mjs index 4c1d458f76..21641d29b4 100644 --- a/scripts/enhance-release-pr.mjs +++ b/scripts/enhance-release-pr.mjs @@ -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,85 @@ async function parseServerChanges() { return entries; } +async function getServerChangeFileData() { + const liveFileData = await getLiveServerChangeFileData(); + if (liveFileData.length > 0) return liveFileData; + + // The changesets version command deletes .server-changes before this script + // enhances the release PR body. Recover those consumed files from the release + // branch diff so they still make it into the release notes handoff. + return getDeletedServerChangeFileDataFromReleaseBranch(); +} + +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 }; From bc44c890467cc40a30c36af84c1250c9f7fb323d Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 9 Jul 2026 16:26:05 +0000 Subject: [PATCH 3/3] fix(release): merge live and recovered server-changes instead of either/or Combine working-tree and git-recovered server-change files (dedup by filename) so a partial cleanup can't drop entries from the release notes. --- scripts/enhance-release-pr.mjs | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/scripts/enhance-release-pr.mjs b/scripts/enhance-release-pr.mjs index 21641d29b4..7571440408 100644 --- a/scripts/enhance-release-pr.mjs +++ b/scripts/enhance-release-pr.mjs @@ -213,13 +213,26 @@ async function parseServerChanges() { } async function getServerChangeFileData() { - const liveFileData = await getLiveServerChangeFileData(); - if (liveFileData.length > 0) return liveFileData; - // The changesets version command deletes .server-changes before this script - // enhances the release PR body. Recover those consumed files from the release - // branch diff so they still make it into the release notes handoff. - return getDeletedServerChangeFileDataFromReleaseBranch(); + // 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() {