mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
fix(release): patch existing release notes by id
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { execFileSync } from "node:child_process";
|
import { execFileSync as nodeExecFileSync } from "node:child_process";
|
||||||
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
@@ -117,21 +117,28 @@ function parseChangelog(changelogText) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasRelease(tag, repo) {
|
function getRelease(tag, repo, execFileSync = nodeExecFileSync) {
|
||||||
try {
|
try {
|
||||||
execFileSync("gh", ["release", "view", tag, "--repo", repo], {
|
const output = execFileSync("gh", ["api", `repos/${repo}/releases/tags/${tag}`], {
|
||||||
stdio: "ignore",
|
encoding: "utf8",
|
||||||
});
|
});
|
||||||
return true;
|
return JSON.parse(output);
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function runGh(args) {
|
function runGh(args, execFileSync = nodeExecFileSync) {
|
||||||
execFileSync("gh", args, { stdio: "inherit" });
|
execFileSync("gh", args, { stdio: "inherit" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateReleaseNotes({ releaseId, repo, notesPath }, execFileSync = nodeExecFileSync) {
|
||||||
|
runGh(
|
||||||
|
["api", "-X", "PATCH", `repos/${repo}/releases/${releaseId}`, "-F", `body=@${notesPath}`],
|
||||||
|
execFileSync,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function buildPrereleaseNotes({ releaseTag, version }) {
|
function buildPrereleaseNotes({ releaseTag, version }) {
|
||||||
const today = new Date().toISOString().slice(0, 10);
|
const today = new Date().toISOString().slice(0, 10);
|
||||||
const lines = [
|
const lines = [
|
||||||
@@ -150,8 +157,9 @@ function buildPrereleaseNotes({ releaseTag, version }) {
|
|||||||
return `${lines.join("\n")}\n`;
|
return `${lines.join("\n")}\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function main() {
|
export function syncReleaseNotes(argv = process.argv.slice(2), deps = {}) {
|
||||||
const args = parseArgs(process.argv.slice(2));
|
const execFileSync = deps.execFileSync ?? nodeExecFileSync;
|
||||||
|
const args = parseArgs(argv);
|
||||||
const changelogPath = path.resolve("CHANGELOG.md");
|
const changelogPath = path.resolve("CHANGELOG.md");
|
||||||
const changelogText = readFileSync(changelogPath, "utf8");
|
const changelogText = readFileSync(changelogPath, "utf8");
|
||||||
const entries = parseChangelog(changelogText);
|
const entries = parseChangelog(changelogText);
|
||||||
@@ -178,7 +186,6 @@ function main() {
|
|||||||
const notesPath = path.join(tempDir, `${targetTag}-notes.md`);
|
const notesPath = path.join(tempDir, `${targetTag}-notes.md`);
|
||||||
writeFileSync(notesPath, notes);
|
writeFileSync(notesPath, notes);
|
||||||
|
|
||||||
const editArgs = ["release", "edit", targetTag, "--repo", args.repo, "--notes-file", notesPath];
|
|
||||||
const createArgs = [
|
const createArgs = [
|
||||||
"release",
|
"release",
|
||||||
"create",
|
"create",
|
||||||
@@ -194,16 +201,11 @@ function main() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (hasRelease(targetTag, args.repo)) {
|
const release = getRelease(targetTag, args.repo, execFileSync);
|
||||||
try {
|
if (release) {
|
||||||
runGh(editArgs);
|
updateReleaseNotes({ releaseId: release.id, repo: args.repo, notesPath }, execFileSync);
|
||||||
console.log(`Updated release notes for ${targetTag}.`);
|
console.log(`Updated release notes for ${targetTag}.`);
|
||||||
return;
|
return;
|
||||||
} catch {
|
|
||||||
console.warn(
|
|
||||||
`Edit failed for ${targetTag} (release may have been recreated by another workflow); falling through to create.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!args.createIfMissing) {
|
if (!args.createIfMissing) {
|
||||||
@@ -214,13 +216,17 @@ function main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
runGh(createArgs);
|
runGh(createArgs, execFileSync);
|
||||||
console.log(`Created release ${targetTag} with changelog notes.`);
|
console.log(`Created release ${targetTag} with changelog notes.`);
|
||||||
} catch (createError) {
|
} catch (createError) {
|
||||||
console.warn(
|
console.warn(
|
||||||
`Release creation failed for ${targetTag}; attempting edit in case another workflow created it concurrently.`,
|
`Release creation failed for ${targetTag}; attempting edit in case another workflow created it concurrently.`,
|
||||||
);
|
);
|
||||||
runGh(editArgs);
|
const raceRelease = getRelease(targetTag, args.repo, execFileSync);
|
||||||
|
if (!raceRelease) {
|
||||||
|
throw createError;
|
||||||
|
}
|
||||||
|
updateReleaseNotes({ releaseId: raceRelease.id, repo: args.repo, notesPath }, execFileSync);
|
||||||
console.log(`Updated release notes for ${targetTag} after create race.`);
|
console.log(`Updated release notes for ${targetTag} after create race.`);
|
||||||
|
|
||||||
if (createError instanceof Error) {
|
if (createError instanceof Error) {
|
||||||
@@ -232,4 +238,6 @@ function main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
main();
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||||
|
syncReleaseNotes();
|
||||||
|
}
|
||||||
|
|||||||
65
scripts/sync-release-notes-from-changelog.test.mjs
Normal file
65
scripts/sync-release-notes-from-changelog.test.mjs
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import test from "node:test";
|
||||||
|
import { syncReleaseNotes } from "./sync-release-notes-from-changelog.mjs";
|
||||||
|
|
||||||
|
function withTempChangelog(fn) {
|
||||||
|
const previousCwd = process.cwd();
|
||||||
|
const tempDir = mkdtempSync(path.join(tmpdir(), "paseo-release-notes-test-"));
|
||||||
|
process.chdir(tempDir);
|
||||||
|
writeFileSync("CHANGELOG.md", "## 0.1.60 - 2026-04-20\n\n- Stable notes.\n");
|
||||||
|
|
||||||
|
try {
|
||||||
|
fn();
|
||||||
|
} finally {
|
||||||
|
process.chdir(previousCwd);
|
||||||
|
rmSync(tempDir, { force: true, recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("updates an existing release body through the release id API", () => {
|
||||||
|
withTempChangelog(() => {
|
||||||
|
const calls = [];
|
||||||
|
|
||||||
|
const execFileSync = (command, args, options) => {
|
||||||
|
calls.push({ args, command, options });
|
||||||
|
|
||||||
|
if (args[0] === "api" && args[1] === "repos/getpaseo/paseo/releases/tags/v0.1.60-rc.3") {
|
||||||
|
return JSON.stringify({ id: 311163621 });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args[0] === "api" && args[1] === "-X" && args[2] === "PATCH") {
|
||||||
|
const notesArg = args.find((arg) => arg.startsWith("body=@"));
|
||||||
|
assert.ok(notesArg, "PATCH should send the notes body from a file");
|
||||||
|
const notesPath = notesArg.slice("body=@".length);
|
||||||
|
assert.match(notesPath, /v0\.1\.60-rc\.3-notes\.md$/);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Unexpected gh call: ${command} ${args.join(" ")}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
syncReleaseNotes(["--repo", "getpaseo/paseo", "--tag", "v0.1.60-rc.3"], {
|
||||||
|
execFileSync,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
calls.some((call) => call.args[0] === "release" && call.args[1] === "edit"),
|
||||||
|
false,
|
||||||
|
"retagged releases should not use gh release edit because it can resend tag_name",
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
calls.some(
|
||||||
|
(call) =>
|
||||||
|
call.args[0] === "api" &&
|
||||||
|
call.args[1] === "-X" &&
|
||||||
|
call.args[2] === "PATCH" &&
|
||||||
|
call.args[3] === "repos/getpaseo/paseo/releases/311163621",
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
"existing releases should be patched by release id",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user