mirror of
https://github.com/morluto/rea.git
synced 2026-11-12 17:59:08 +00:00
fix(ci): defer proposals for merged release commits
This commit is contained in:
1 parent
d74549e495
commit
5eef239dfd
10 files changed
+304
-68
No files matched your search
@@ -242,7 +242,8 @@ const PATH_SCOPES = {
|
||||
"scripts/windows-capabilities.mjs",
|
||||
"scripts/prepack.mjs",
|
||||
"scripts/prepare.mjs",
|
||||
"scripts/lib/release-proposal.mjs",
|
||||
"scripts/lib/release-proposal.*",
|
||||
"scripts/lib/release-pull-request.mjs",
|
||||
"patches/release-please+17.3.0.patch",
|
||||
"tests/boundary/filesystem/releaseProposal.test.ts",
|
||||
"scripts/rea.mjs",
|
||||
|
||||
@@ -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 =
|
||||
(
|
||||
|
||||
@@ -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[] }
|
||||
>;
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -4,8 +4,8 @@ import { appendFile } from "node:fs/promises";
|
||||
import { parseArgs, promisify } from "node:util";
|
||||
import {
|
||||
applyReleasePleasePatch,
|
||||
createReleaseProposal,
|
||||
describeReleaseProposalError,
|
||||
releaseProposalSearchDepth,
|
||||
} from "./lib/release-proposal.mjs";
|
||||
|
||||
const execute = promisify(execFile);
|
||||
@@ -27,33 +27,44 @@ async function main() {
|
||||
) {
|
||||
throw new Error("Expected GITHUB_REPOSITORY, GH_TOKEN and --target-branch");
|
||||
}
|
||||
const depth = await releaseProposalSearchDepth(
|
||||
async (command, args) =>
|
||||
(await execute(command, args, { encoding: "utf8" })).stdout,
|
||||
values["source-sha"],
|
||||
);
|
||||
// Patch before import: only proposal generation uses this maintained upstream fix.
|
||||
// Publication continues through the pinned release-please action.
|
||||
await applyReleasePleasePatch();
|
||||
const { GitHub, Manifest } = await import("release-please");
|
||||
const [owner, repo] = repository.split("/");
|
||||
const branch = values["target-branch"];
|
||||
const github = await GitHub.create({
|
||||
owner,
|
||||
repo,
|
||||
token,
|
||||
defaultBranch: branch,
|
||||
});
|
||||
const manifest = await Manifest.fromManifest(
|
||||
github,
|
||||
branch,
|
||||
undefined,
|
||||
undefined,
|
||||
const result = await createReleaseProposal(
|
||||
async (command, args) =>
|
||||
(
|
||||
await execute(command, args, {
|
||||
encoding: "utf8",
|
||||
maxBuffer: 64 * 1024 * 1024,
|
||||
})
|
||||
).stdout,
|
||||
{
|
||||
commitSearchDepth: depth,
|
||||
repository,
|
||||
targetBranch: branch,
|
||||
sourceSha: values["source-sha"],
|
||||
eventName: process.env.GITHUB_EVENT_NAME,
|
||||
},
|
||||
async (depth) => {
|
||||
// Patch before import: publication uses the pinned action separately.
|
||||
await applyReleasePleasePatch();
|
||||
const { GitHub, Manifest } = await import("release-please");
|
||||
const github = await GitHub.create({
|
||||
owner,
|
||||
repo,
|
||||
token,
|
||||
defaultBranch: branch,
|
||||
});
|
||||
return Manifest.fromManifest(github, branch, undefined, undefined, {
|
||||
commitSearchDepth: depth,
|
||||
});
|
||||
},
|
||||
);
|
||||
const prs = (await manifest.createPullRequests()).filter(Boolean);
|
||||
if (result.kind === "release-merge") {
|
||||
const message = `Release PR #${result.pullRequest} merged at ${values["source-sha"]}; the release workflow owns publication. Proposal generation resumes on the next main push.\n`;
|
||||
process.stdout.write(message);
|
||||
if (process.env.GITHUB_STEP_SUMMARY)
|
||||
await appendFile(process.env.GITHUB_STEP_SUMMARY, message);
|
||||
}
|
||||
const prs = result.kind === "proposal" ? result.pullRequests : [];
|
||||
if (process.env.GITHUB_OUTPUT) {
|
||||
await appendFile(
|
||||
process.env.GITHUB_OUTPUT,
|
||||
|
||||
Reference in new issue
Block a user