mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
fix(release): reject missing manifest release boundaries
This commit is contained in:
1 parent
160d821ac9
commit
bc143125ed
3 files changed
+167
-28
No files matched your search
@@ -112,6 +112,13 @@ creation or retry mutations. An internal GraphQL error does not establish API
|
|||||||
quota exhaustion or prove that query size caused the failure. Publication
|
quota exhaustion or prove that query size caused the failure. Publication
|
||||||
continues through the pinned release-please action.
|
continues through the pinned release-please action.
|
||||||
|
|
||||||
|
A nonzero manifest version must resolve to a GitHub release or tag before
|
||||||
|
proposal history is collected. If publication has not created that boundary
|
||||||
|
yet, preparation stops before opening or updating a PR; wait for publication
|
||||||
|
before retrying. Missing manifest versions and `0.0.0` still allow an initial
|
||||||
|
release. This prevents historical `Release-As` directives from being replayed
|
||||||
|
as unreleased changes.
|
||||||
|
|
||||||
When upgrading release-please, review the upstream manifest implementation,
|
When upgrading release-please, review the upstream manifest implementation,
|
||||||
refresh the versioned patch and run the release proposal and workflow boundary
|
refresh the versioned patch and run the release proposal and workflow boundary
|
||||||
tests. Version mismatch or a patch that neither applies cleanly nor matches an
|
tests. Version mismatch or a patch that neither applies cleanly nor matches an
|
||||||
|
|||||||
@@ -105,7 +105,22 @@
|
|||||||
}
|
}
|
||||||
--- a/build/src/manifest.js
|
--- a/build/src/manifest.js
|
||||||
+++ b/build/src/manifest.js
|
+++ b/build/src/manifest.js
|
||||||
@@ -261,9 +261,20 @@
|
@@ -248,6 +248,14 @@
|
||||||
|
releasesFound++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
+ // A recorded version requires its release boundary, even while a
|
||||||
|
+ // concurrent publisher has not created the matching release/tag yet.
|
||||||
|
+ for (const path of Object.keys(strategiesByPath)) {
|
||||||
|
+ const expectedVersion = this.releasedVersions[path];
|
||||||
|
+ if (expectedVersion && expectedVersion.toString() !== '0.0.0' && !releasesByPath[path]) {
|
||||||
|
+ throw new Error(`Missing expected release for path ${path} at version ${expectedVersion.toString()}: no matching release or tag found. Refusing to bootstrap an already released package.`);
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
const needsBootstrap = releasesFound < expectedReleases;
|
||||||
|
if (releasesFound < expectedReleases) {
|
||||||
|
this.logger.warn(`Expected ${expectedReleases} releases, only found ${releasesFound}`);
|
||||||
|
@@ -261,9 +269,20 @@
|
||||||
this.logger.info('Collecting commits since all latest releases');
|
this.logger.info('Collecting commits since all latest releases');
|
||||||
const commits = [];
|
const commits = [];
|
||||||
this.logger.debug(`commit search depth: ${this.commitSearchDepth}`);
|
this.logger.debug(`commit search depth: ${this.commitSearchDepth}`);
|
||||||
@@ -127,7 +142,7 @@
|
|||||||
batchSize: this.commitBatchSize,
|
batchSize: this.commitBatchSize,
|
||||||
});
|
});
|
||||||
const releaseShas = new Set(Object.values(releaseShasByPath));
|
const releaseShas = new Set(Object.values(releaseShasByPath));
|
||||||
@@ -272,6 +283,7 @@
|
@@ -272,6 +291,7 @@
|
||||||
// sha => release pull request
|
// sha => release pull request
|
||||||
const releasePullRequestsBySha = {};
|
const releasePullRequestsBySha = {};
|
||||||
let releaseCommitsFound = 0;
|
let releaseCommitsFound = 0;
|
||||||
@@ -135,7 +150,7 @@
|
|||||||
for await (const commit of commitGenerator) {
|
for await (const commit of commitGenerator) {
|
||||||
if (releaseShas.has(commit.sha)) {
|
if (releaseShas.has(commit.sha)) {
|
||||||
if (commit.pullRequest) {
|
if (commit.pullRequest) {
|
||||||
@@ -284,14 +296,17 @@
|
@@ -284,14 +304,17 @@
|
||||||
}
|
}
|
||||||
if (this.lastReleaseSha && this.lastReleaseSha === commit.sha) {
|
if (this.lastReleaseSha && this.lastReleaseSha === commit.sha) {
|
||||||
this.logger.info(`Using configured lastReleaseSha ${this.lastReleaseSha} as last commit.`);
|
this.logger.info(`Using configured lastReleaseSha ${this.lastReleaseSha} as last commit.`);
|
||||||
@@ -153,7 +168,7 @@
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
commits.push({
|
commits.push({
|
||||||
@@ -301,16 +316,18 @@
|
@@ -301,16 +324,18 @@
|
||||||
pullRequest: commit.pullRequest,
|
pullRequest: commit.pullRequest,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -124,21 +124,24 @@ function historyPage(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function releasePage() {
|
function releaseRecord(version: string, sha = baseline) {
|
||||||
|
const tagName = `rea-agents-${version}`;
|
||||||
|
return {
|
||||||
|
name: version,
|
||||||
|
tag: { name: tagName },
|
||||||
|
tagCommit: { oid: sha },
|
||||||
|
description: "",
|
||||||
|
isDraft: false,
|
||||||
|
url: `https://github.com/fixture/rea/releases/tag/${tagName}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function releasePage(releases: ReturnType<typeof releaseRecord>[]) {
|
||||||
return {
|
return {
|
||||||
data: {
|
data: {
|
||||||
repository: {
|
repository: {
|
||||||
releases: {
|
releases: {
|
||||||
nodes: [
|
nodes: releases,
|
||||||
{
|
|
||||||
name: "6.7.0",
|
|
||||||
tag: { name: "rea-agents-6.7.0" },
|
|
||||||
tagCommit: { oid: baseline },
|
|
||||||
description: "",
|
|
||||||
isDraft: false,
|
|
||||||
url: "https://github.com/fixture/rea/releases/tag/rea-agents-6.7.0",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
pageInfo: { hasNextPage: false },
|
pageInfo: { hasNextPage: false },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -158,6 +161,29 @@ const defaultConfiguration = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function releaseFileEntries(
|
||||||
|
configuration: Record<string, unknown>,
|
||||||
|
releasedVersions: Record<string, string>,
|
||||||
|
) {
|
||||||
|
const files: Record<string, unknown> = {
|
||||||
|
"release-please-config.json": configuration,
|
||||||
|
".release-please-manifest.json": releasedVersions,
|
||||||
|
"package.json": {
|
||||||
|
name: "rea-agents",
|
||||||
|
version: releasedVersions["."] ?? "0.0.0",
|
||||||
|
},
|
||||||
|
"pkg/package.json": {
|
||||||
|
name: "rea-agents",
|
||||||
|
version: releasedVersions.pkg ?? "0.0.0",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return Object.entries(files).map(([path, value], index) => ({
|
||||||
|
path,
|
||||||
|
sha: (index + 1).toString(16).padStart(40, "0"),
|
||||||
|
text: JSON.stringify(value),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
async function releaseFixture({
|
async function releaseFixture({
|
||||||
config = {},
|
config = {},
|
||||||
commits = [
|
commits = [
|
||||||
@@ -168,6 +194,9 @@ async function releaseFixture({
|
|||||||
pullRequest = false,
|
pullRequest = false,
|
||||||
historyFailures = [],
|
historyFailures = [],
|
||||||
releaseFailures = [],
|
releaseFailures = [],
|
||||||
|
releasedVersions = { ".": "6.7.0", pkg: "6.7.0" },
|
||||||
|
releases = [releaseRecord("6.7.0")],
|
||||||
|
tags = [],
|
||||||
}: {
|
}: {
|
||||||
config?: Record<string, unknown>;
|
config?: Record<string, unknown>;
|
||||||
commits?: CommitRecord[];
|
commits?: CommitRecord[];
|
||||||
@@ -175,6 +204,9 @@ async function releaseFixture({
|
|||||||
pullRequest?: boolean;
|
pullRequest?: boolean;
|
||||||
historyFailures?: HistoryFailure[];
|
historyFailures?: HistoryFailure[];
|
||||||
releaseFailures?: HistoryFailure["errors"][];
|
releaseFailures?: HistoryFailure["errors"][];
|
||||||
|
releasedVersions?: Record<string, string>;
|
||||||
|
releases?: ReturnType<typeof releaseRecord>[];
|
||||||
|
tags?: { name: string; commit: { sha: string } }[];
|
||||||
} = {}) {
|
} = {}) {
|
||||||
const requests: { method: string; pathname: string }[] = [];
|
const requests: { method: string; pathname: string }[] = [];
|
||||||
const pages: HistoryVariables[] = [];
|
const pages: HistoryVariables[] = [];
|
||||||
@@ -182,6 +214,7 @@ async function releaseFixture({
|
|||||||
const failures = [...historyFailures];
|
const failures = [...historyFailures];
|
||||||
const releaseErrors = [...releaseFailures];
|
const releaseErrors = [...releaseFailures];
|
||||||
const configuration = { ...defaultConfiguration, ...config };
|
const configuration = { ...defaultConfiguration, ...config };
|
||||||
|
const entries = releaseFileEntries(configuration, releasedVersions);
|
||||||
const github = await upstream.GitHub.create({
|
const github = await upstream.GitHub.create({
|
||||||
owner: "fixture",
|
owner: "fixture",
|
||||||
repo: "rea",
|
repo: "rea",
|
||||||
@@ -196,7 +229,7 @@ async function releaseFixture({
|
|||||||
method: options.method ?? "GET",
|
method: options.method ?? "GET",
|
||||||
pathname: url.pathname,
|
pathname: url.pathname,
|
||||||
});
|
});
|
||||||
if (url.pathname === "/repos/fixture/rea/tags") return respond([]);
|
if (url.pathname === "/repos/fixture/rea/tags") return respond(tags);
|
||||||
if (url.pathname === "/repos/fixture/rea/releases/generate-notes") {
|
if (url.pathname === "/repos/fixture/rea/releases/generate-notes") {
|
||||||
return respond({ body: "Generated release notes" });
|
return respond({ body: "Generated release notes" });
|
||||||
}
|
}
|
||||||
@@ -214,7 +247,7 @@ async function releaseFixture({
|
|||||||
if (query.query.includes("query releases(")) {
|
if (query.query.includes("query releases(")) {
|
||||||
const errors = releaseErrors.shift();
|
const errors = releaseErrors.shift();
|
||||||
if (errors) return respond({ errors });
|
if (errors) return respond({ errors });
|
||||||
return respond(releasePage());
|
return respond(releasePage(releases));
|
||||||
}
|
}
|
||||||
if (!query.query.includes("query pullRequestsSince(")) {
|
if (!query.query.includes("query pullRequestsSince(")) {
|
||||||
throw new Error(`Unexpected GraphQL operation: ${query.query}`);
|
throw new Error(`Unexpected GraphQL operation: ${query.query}`);
|
||||||
@@ -234,17 +267,6 @@ async function releaseFixture({
|
|||||||
}
|
}
|
||||||
return respond(historyPage(commits, query.variables, pullRequest));
|
return respond(historyPage(commits, query.variables, pullRequest));
|
||||||
}
|
}
|
||||||
const files: Record<string, unknown> = {
|
|
||||||
"release-please-config.json": configuration,
|
|
||||||
".release-please-manifest.json": { ".": "6.7.0", pkg: "6.7.0" },
|
|
||||||
"package.json": { name: "rea-agents", version: "6.7.0" },
|
|
||||||
"pkg/package.json": { name: "rea-agents", version: "6.7.0" },
|
|
||||||
};
|
|
||||||
const entries = Object.entries(files).map(([path, value], index) => ({
|
|
||||||
path,
|
|
||||||
sha: (index + 1).toString(16).padStart(40, "0"),
|
|
||||||
text: JSON.stringify(value),
|
|
||||||
}));
|
|
||||||
if (url.pathname === "/repos/fixture/rea/git/trees/main") {
|
if (url.pathname === "/repos/fixture/rea/git/trees/main") {
|
||||||
return respond({
|
return respond({
|
||||||
truncated: false,
|
truncated: false,
|
||||||
@@ -503,6 +525,100 @@ it("collects beyond 500 commits and rejects an unobserved known release boundary
|
|||||||
).toBe(true);
|
).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("rejects a missing manifest release before replaying a historical version override", async () => {
|
||||||
|
const fixture = await releaseFixture({
|
||||||
|
releasedVersions: { ".": "6.8.0" },
|
||||||
|
releases: [releaseRecord("6.7.0", "d".repeat(40))],
|
||||||
|
commits: [
|
||||||
|
{ sha: latest, message: "fix: current change", files: ["src/change.ts"] },
|
||||||
|
{
|
||||||
|
sha: baseline,
|
||||||
|
message: "chore: release 6.8.0",
|
||||||
|
files: ["package.json"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sha: "c".repeat(40),
|
||||||
|
message: "fix: historical version override\n\nRelease-As: 0.4.0",
|
||||||
|
files: ["src/historical.ts"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
await expect(fixture.manifest.createPullRequests()).rejects.toThrow(
|
||||||
|
"Missing expected release",
|
||||||
|
);
|
||||||
|
expect(fixture.pages).toEqual([]);
|
||||||
|
expect(
|
||||||
|
fixture.requests.every(
|
||||||
|
(request) => request.method === "GET" || request.pathname === "/graphql",
|
||||||
|
),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses a tag-only manifest release and excludes older version overrides", async () => {
|
||||||
|
const fixture = await releaseFixture({
|
||||||
|
releasedVersions: { ".": "6.8.0" },
|
||||||
|
releases: [],
|
||||||
|
tags: [{ name: "rea-agents-6.8.0", commit: { sha: baseline } }],
|
||||||
|
commits: [
|
||||||
|
{ sha: latest, message: "fix: current change", files: ["src/change.ts"] },
|
||||||
|
{
|
||||||
|
sha: baseline,
|
||||||
|
message: "chore: release 6.8.0",
|
||||||
|
files: ["package.json"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sha: "c".repeat(40),
|
||||||
|
message: "fix: historical version override\n\nRelease-As: 0.4.0",
|
||||||
|
files: ["src/historical.ts"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const candidates = await fixture.manifest.buildPullRequests();
|
||||||
|
expect(candidates).toHaveLength(1);
|
||||||
|
expect(candidates[0]?.body.toString()).toContain("current change");
|
||||||
|
expect(candidates[0]?.body.toString()).not.toContain(
|
||||||
|
"historical version override",
|
||||||
|
);
|
||||||
|
const version = candidates[0]?.updates.find(
|
||||||
|
(update) => update.path === "version.txt",
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
version?.updater.updateContent("6.8.0 // x-release-please-version"),
|
||||||
|
).toContain("6.9.0");
|
||||||
|
expect(
|
||||||
|
fixture.requests.some(
|
||||||
|
(request) => request.pathname === "/repos/fixture/rea/tags",
|
||||||
|
),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each(["omitted", "0.0.0"])(
|
||||||
|
"preserves initial bootstrap with an %s manifest version",
|
||||||
|
async (version) => {
|
||||||
|
const releasedVersions: Record<string, string> =
|
||||||
|
version === "omitted" ? {} : { ".": version };
|
||||||
|
const fixture = await releaseFixture({
|
||||||
|
releasedVersions,
|
||||||
|
releases: [],
|
||||||
|
commits: [
|
||||||
|
{
|
||||||
|
sha: latest,
|
||||||
|
message: "feat: initial change",
|
||||||
|
files: ["src/change.ts"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const candidates = await fixture.manifest.buildPullRequests();
|
||||||
|
expect(candidates).toHaveLength(1);
|
||||||
|
const versionUpdate = candidates[0]?.updates.find(
|
||||||
|
(update) => update.path === "version.txt",
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
versionUpdate?.updater.updateContent("0.0.0 // x-release-please-version"),
|
||||||
|
).toContain("1.0.0");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
{
|
{
|
||||||
name: "timestamped GraphQL internal error",
|
name: "timestamped GraphQL internal error",
|
||||||
@@ -637,6 +753,7 @@ it("preserves a reached explicit last-release override", async () => {
|
|||||||
|
|
||||||
it("preserves complete mixed bootstrap history with all known release boundaries", async () => {
|
it("preserves complete mixed bootstrap history with all known release boundaries", async () => {
|
||||||
const fixture = await releaseFixture({
|
const fixture = await releaseFixture({
|
||||||
|
releasedVersions: { ".": "6.7.0", pkg: "0.0.0" },
|
||||||
config: {
|
config: {
|
||||||
packages: {
|
packages: {
|
||||||
".": { "package-name": "rea-agents" },
|
".": { "package-name": "rea-agents" },
|
||||||
|
|||||||
Reference in new issue
Block a user