fix(ci): defer proposals for merged release commits

This commit is contained in:
morluto committed 2026-11-12 05:08:27 -05:00
1 parent d74549e495
commit 5eef239dfd
10 files changed
+304 -68

No files matched your search

+5 -43
View File
@@ -1,5 +1,6 @@
import { valid } from "semver";
import { z } from "zod";
import { readMergedReleasePullRequest } from "./release-pull-request.mjs";
import {
releaseInventorySchema,
verifyInventoryHistory,
@@ -8,17 +9,6 @@ import {
const sha = z.string().regex(/^[a-f0-9]{40}$/u);
const repository = z.string().regex(/^[\w.-]+\/[\w.-]+$/u);
const pullRequest = z.object({
number: z.number().int().positive(),
body: z.string(),
merged_at: z.string().nullable(),
merge_commit_sha: sha.nullable(),
head: z.object({
ref: z.string(),
repo: z.object({ full_name: z.string() }).nullable(),
}),
labels: z.array(z.object({ name: z.string() })),
});
/** Read the reviewed Git snapshot and live PR before Release Please can mutate tags. */
export async function verifyNotesPublication(run, options) {
@@ -39,41 +29,13 @@ export async function verifyNotesPublication(run, options) {
.parse(JSON.parse(await read("package.json")));
if (valid(version) !== version || version.includes("+"))
throw new Error("Invalid reviewed package version");
let candidates;
if (options.pr !== undefined) {
const number = z.coerce.number().int().positive().parse(options.pr);
candidates = [
pullRequest.parse(
JSON.parse(await run("gh", ["api", `repos/${repo}/pulls/${number}`])),
),
];
} else {
candidates = z
.array(z.array(pullRequest))
.parse(
JSON.parse(
await run("gh", [
"api",
"--paginate",
"--slurp",
`repos/${repo}/commits/${source}/pulls?per_page=100`,
]),
),
)
.flat();
}
const matching = candidates.filter(
(pr) =>
pr.merged_at !== null &&
pr.merge_commit_sha === source &&
pr.head.repo?.full_name.toLowerCase() === repo.toLowerCase() &&
pr.head.ref.startsWith("release-please--branches--"),
);
if (matching.length !== 1)
const pr = await readMergedReleasePullRequest(run, { ...options, source });
if (pr === undefined)
throw new Error(
"Expected exactly one merged Release Please PR for the reviewed source",
);
const pr = matching[0];
if (pr.body === null)
throw new Error("The merged release PR has no release notes body");
const path = `docs/releases/${version}.contributions.json`;
const present =
(
+16
View File
@@ -6,3 +6,19 @@ export function releaseProposalSearchDepth(
run: (command: string, args: string[]) => Promise<string>,
sourceSha: string,
): Promise<number>;
export function createReleaseProposal<T>(
run: (command: string, args: string[]) => Promise<string>,
context: {
repository: string;
sourceSha: string;
targetBranch: string;
eventName: string | undefined;
},
loadManifest: (depth: number) => Promise<{
createPullRequests(): Promise<Array<T | undefined>>;
}>,
): Promise<
| { kind: "release-merge"; pullRequest: number }
| { kind: "proposal"; pullRequests: T[] }
>;
+20
View File
@@ -5,6 +5,7 @@ import { dirname, relative, sep } from "node:path";
import { promisify } from "node:util";
import { fileURLToPath } from "node:url";
import { requireCompleteReleaseHistory } from "./release-notes.mjs";
import { readMergedReleasePullRequest } from "./release-pull-request.mjs";
const execute = promisify(execFile);
const require = createRequire(import.meta.url);
@@ -74,3 +75,22 @@ export async function releaseProposalSearchDepth(run, sourceSha) {
}
return count;
}
/** The merged-release workflow owns tag creation; its push needs no proposal. */
export async function createReleaseProposal(run, context, loadManifest) {
if (context.eventName === "push" && context.targetBranch === "main") {
const release = await readMergedReleasePullRequest(run, {
repository: context.repository,
source: context.sourceSha,
targetBranch: context.targetBranch,
});
if (release !== undefined)
return { kind: "release-merge", pullRequest: release.number };
}
const depth = await releaseProposalSearchDepth(run, context.sourceSha);
const manifest = await loadManifest(depth);
return {
kind: "proposal",
pullRequests: (await manifest.createPullRequests()).filter(Boolean),
};
}
+60
View File
@@ -0,0 +1,60 @@
import { z } from "zod";
const sha = z.string().regex(/^[a-f0-9]{40}$/u);
const repository = z.string().regex(/^[\w.-]+\/[\w.-]+$/u);
const pullRequest = z.object({
number: z.number().int().positive(),
body: z.string().nullable(),
merged_at: z.string().nullable(),
merge_commit_sha: sha.nullable(),
base: z.object({ ref: z.string() }),
head: z.object({
ref: z.string(),
repo: z.object({ full_name: z.string() }).nullable(),
}),
labels: z.array(z.object({ name: z.string() })),
});
/** Own REST release-PR identity for proposal admission and publication checks. */
export async function readMergedReleasePullRequest(run, options) {
const repo = repository.parse(options.repository);
const source = sha.parse(options.source);
let candidates;
if (options.pr !== undefined) {
const number = z.coerce.number().int().positive().parse(options.pr);
candidates = [
pullRequest.parse(
JSON.parse(await run("gh", ["api", `repos/${repo}/pulls/${number}`])),
),
];
} else {
candidates = z
.array(z.array(pullRequest))
.parse(
JSON.parse(
await run("gh", [
"api",
"--paginate",
"--slurp",
`repos/${repo}/commits/${source}/pulls?per_page=100`,
]),
),
)
.flat();
}
const matching = candidates.filter(
(pr) =>
pr.merged_at !== null &&
pr.merge_commit_sha === source &&
pr.head.repo?.full_name.toLowerCase() === repo.toLowerCase() &&
pr.head.ref.startsWith("release-please--branches--") &&
(options.targetBranch === undefined ||
(pr.base.ref === options.targetBranch &&
pr.head.ref.startsWith(
`release-please--branches--${options.targetBranch}--`,
))),
);
if (matching.length > 1)
throw new Error("Multiple merged Release Please PRs match the source");
return matching[0];
}