fix(release): populate changelog and server-changes on release/dispatch (#4204)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user