Skip to content

Commit a1f7887

Browse files
authored
feat: send helpful email when strip prefix is incorrect (#144)
1 parent 4b72df8 commit a1f7887

File tree

8 files changed

+188
-111
lines changed

8 files changed

+188
-111
lines changed

e2e/__snapshots__/e2e.spec.ts.snap

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,15 @@ modules/no-prefix/metadata.json
5454
"
5555
`;
5656

57+
exports[`e2e tests [snapshot] error message for incorrect strip prefix 1`] = `
58+
"Failed to publish entry for testorg/[email protected] to the Bazel Central Registry.
59+
60+
Could not find MODULE.bazel in release archive at ./versioned-1.0.0/MODULE.bazel.
61+
Is the strip prefix in source.template.json correct? (currently it's 'versioned-1.0.0')
62+
63+
"
64+
`;
65+
5766
exports[`e2e tests [snapshot] missing strip prefix 1`] = `
5867
"----------------------------------------------------
5968
modules/empty-prefix/1.0.0/MODULE.bazel

e2e/e2e.spec.ts

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,10 @@ describe("e2e tests", () => {
149149
const installationId = fakeGitHub.mockAppInstallation(testOrg, repo);
150150
fakeGitHub.mockAppInstallation(testOrg, "bazel-central-registry");
151151

152-
const releaseArchive = await makeReleaseTarball(repo, "zero-versioned-1.0.0");
152+
const releaseArchive = await makeReleaseTarball(
153+
repo,
154+
"zero-versioned-1.0.0"
155+
);
153156
await fakeGitHub.mockReleaseArchive(
154157
`/${testOrg}/${repo}/archive/refs/tags/${tag}.tar.gz`,
155158
releaseArchive
@@ -466,8 +469,14 @@ describe("e2e tests", () => {
466469
const installationId = fakeGitHub.mockAppInstallation(testOrg, repo);
467470
fakeGitHub.mockAppInstallation(testOrg, "bazel-central-registry");
468471

469-
const releaseArchive1 = await makeReleaseTarball(repo, "module-1.0.0");
470-
const releaseArchive2 = await makeReleaseTarball(repo, "submodule-1.0.0");
472+
const releaseArchive1 = await makeReleaseTarball(
473+
repo,
474+
"multi-module-1.0.0"
475+
);
476+
const releaseArchive2 = await makeReleaseTarball(
477+
repo,
478+
"multi-module-1.0.0"
479+
);
471480

472481
await fakeGitHub.mockReleaseArchive(
473482
`/${testOrg}/${repo}/releases/download/${tag}/module-${tag}.tar.gz`,
@@ -735,6 +744,48 @@ describe("e2e tests", () => {
735744
);
736745
expect(logs.latest?.author_name).toEqual("publish-to-bcr-bot");
737746
});
747+
748+
test("[snapshot] error message for incorrect strip prefix", async () => {
749+
const repo = Fixture.Versioned;
750+
const tag = "v1.0.0";
751+
await setupLocalRemoteRulesetRepo(repo, tag, releaser);
752+
753+
fakeGitHub.mockUser(releaser);
754+
fakeGitHub.mockRepository(testOrg, repo);
755+
fakeGitHub.mockRepository(
756+
testOrg,
757+
"bazel-central-registry",
758+
"bazelbuild",
759+
"bazel-central-registry"
760+
);
761+
const rulesetInstallationId = fakeGitHub.mockAppInstallation(testOrg, repo);
762+
fakeGitHub.mockAppInstallation(testOrg, "bazel-central-registry");
763+
764+
// Strip prefix in release archive doesn't match source.template.json
765+
const releaseArchive = await makeReleaseTarball(repo, "invalid-prefix");
766+
await fakeGitHub.mockReleaseArchive(
767+
`/${testOrg}/${repo}/archive/refs/tags/${tag}.tar.gz`,
768+
releaseArchive
769+
);
770+
771+
const response = await publishReleaseEvent(
772+
cloudFunctions.getBaseUrl(),
773+
secrets.webhookSecret,
774+
rulesetInstallationId,
775+
{
776+
owner: testOrg,
777+
repo,
778+
tag,
779+
releaser,
780+
}
781+
);
782+
783+
expect(response.status).toEqual(200);
784+
785+
const messages = await fetchEmails(emailClient);
786+
expect(messages.length).toEqual(1);
787+
expect(messages[0].text).toMatchSnapshot();
788+
});
738789
});
739790

740791
const testReleaseArchives: string[] = [];
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
22
"integrity": "",
3-
"strip_prefix": "{REPO}-{VERSION}",
3+
"strip_prefix": "{REPO}-{VERSION}/submodule",
44
"url": "https://github.com/{OWNER}/{REPO}/releases/download/{TAG}/module-{TAG}.tar.gz"
55
}

e2e/helpers/email.ts

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,17 +41,10 @@ export async function fetchEmails(
4141
)) {
4242
messages.push(await mailparser.simpleParser(message.source, {}));
4343
}
44+
await emailClient.messageFlagsAdd({ seq: "1:*", seen: false }, ["\\Seen"]);
4445
} finally {
4546
await emailClient.mailboxClose();
4647
}
4748

4849
return messages;
4950
}
50-
51-
export async function deleteAllMail(emailClient: ImapFlow): Promise<void> {
52-
try {
53-
await emailClient.messageDelete("1:*");
54-
} finally {
55-
await emailClient.mailboxClose();
56-
}
57-
}

src/domain/create-entry.spec.ts

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ jest.mock("node:fs");
4040

4141
const mockedFileReads: { [path: string]: string } = {};
4242
const EXTRACTED_MODULE_PATH = "/fake/path/to/MODULE.bazel";
43+
let mockReleaseArchive: ReleaseArchive;
4344

4445
beforeEach(() => {
4546
jest.clearAllMocks();
@@ -79,14 +80,15 @@ beforeEach(() => {
7980
delete mockedFileReads[key];
8081
}
8182

82-
mocked(ReleaseArchive.fetch).mockImplementation(async () => {
83-
return {
84-
extractModuleFile: jest.fn(async () => {
85-
return new ModuleFile(EXTRACTED_MODULE_PATH);
86-
}),
87-
diskPath: path.join(os.tmpdir(), "archive.tar.gz"),
88-
} as unknown as ReleaseArchive;
89-
});
83+
mockReleaseArchive = {
84+
extractModuleFile: jest.fn(async () => {
85+
return new ModuleFile(EXTRACTED_MODULE_PATH);
86+
}),
87+
diskPath: path.join(os.tmpdir(), "archive.tar.gz"),
88+
cleanup: jest.fn(),
89+
} as Partial<ReleaseArchive> as ReleaseArchive;
90+
91+
mocked(ReleaseArchive.fetch).mockResolvedValue(mockReleaseArchive);
9092

9193
mockGitClient = mocked(new GitClient());
9294
mockBcrForkGitHubClient = mocked(new GitHubClient({} as any));
@@ -185,6 +187,18 @@ describe("createEntryFiles", () => {
185187
);
186188
});
187189

190+
test("cleans up the release archive extraction", async () => {
191+
mockRulesetFiles();
192+
193+
const tag = "v1.2.3";
194+
const rulesetRepo = await RulesetRepository.create("repo", "owner", tag);
195+
const bcrRepo = CANONICAL_BCR;
196+
197+
await createEntryService.createEntryFiles(rulesetRepo, bcrRepo, tag, ".");
198+
199+
expect(mockReleaseArchive.cleanup).toHaveBeenCalled();
200+
});
201+
188202
test("creates the required entry files for a different module root", async () => {
189203
mockRulesetFiles({
190204
moduleRoot: "sub/dir",

src/domain/create-entry.ts

Lines changed: 40 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -58,50 +58,55 @@ export class CreateEntryService {
5858
sourceTemplate.url,
5959
sourceTemplate.stripPrefix
6060
);
61-
const integrityHash = computeIntegrityHash(releaseArchive.diskPath);
62-
sourceTemplate.setIntegrityHash(integrityHash);
6361

64-
const moduleFile = await releaseArchive.extractModuleFile(moduleRoot);
62+
try {
63+
const integrityHash = computeIntegrityHash(releaseArchive.diskPath);
64+
sourceTemplate.setIntegrityHash(integrityHash);
6565

66-
const bcrEntryPath = path.resolve(
67-
bcrRepo.diskPath,
68-
"modules",
69-
moduleFile.moduleName
70-
);
71-
const bcrVersionEntryPath = path.join(bcrEntryPath, version);
66+
const moduleFile = await releaseArchive.extractModuleFile(moduleRoot);
7267

73-
if (!fs.existsSync(bcrEntryPath)) {
74-
fs.mkdirSync(bcrEntryPath);
75-
}
68+
const bcrEntryPath = path.resolve(
69+
bcrRepo.diskPath,
70+
"modules",
71+
moduleFile.moduleName
72+
);
73+
const bcrVersionEntryPath = path.join(bcrEntryPath, version);
7674

77-
const metadataTemplate = rulesetRepo.metadataTemplate(moduleRoot);
75+
if (!fs.existsSync(bcrEntryPath)) {
76+
fs.mkdirSync(bcrEntryPath);
77+
}
7878

79-
updateMetadataFile(metadataTemplate, bcrEntryPath, version);
79+
const metadataTemplate = rulesetRepo.metadataTemplate(moduleRoot);
8080

81-
fs.mkdirSync(bcrVersionEntryPath);
81+
updateMetadataFile(metadataTemplate, bcrEntryPath, version);
8282

83-
this.addPatches(
84-
rulesetRepo,
85-
sourceTemplate,
86-
moduleFile,
87-
bcrVersionEntryPath,
88-
moduleRoot
89-
);
83+
fs.mkdirSync(bcrVersionEntryPath);
9084

91-
this.patchModuleVersionIfMismatch(
92-
moduleFile,
93-
version,
94-
sourceTemplate,
95-
bcrVersionEntryPath
96-
);
85+
this.addPatches(
86+
rulesetRepo,
87+
sourceTemplate,
88+
moduleFile,
89+
bcrVersionEntryPath,
90+
moduleRoot
91+
);
9792

98-
sourceTemplate.save(path.join(bcrVersionEntryPath, "source.json"));
99-
moduleFile.save(path.join(bcrVersionEntryPath, "MODULE.bazel"));
93+
this.patchModuleVersionIfMismatch(
94+
moduleFile,
95+
version,
96+
sourceTemplate,
97+
bcrVersionEntryPath
98+
);
10099

101-
fs.copyFileSync(
102-
rulesetRepo.presubmitPath(moduleRoot),
103-
path.join(bcrVersionEntryPath, "presubmit.yml")
104-
);
100+
sourceTemplate.save(path.join(bcrVersionEntryPath, "source.json"));
101+
moduleFile.save(path.join(bcrVersionEntryPath, "MODULE.bazel"));
102+
103+
fs.copyFileSync(
104+
rulesetRepo.presubmitPath(moduleRoot),
105+
path.join(bcrVersionEntryPath, "presubmit.yml")
106+
);
107+
} finally {
108+
releaseArchive.cleanup();
109+
}
105110
}
106111

107112
public async commitEntryToNewBranch(
@@ -232,7 +237,7 @@ export class CreateEntryService {
232237
if (moduleFile.version !== version) {
233238
console.log(
234239
`Archived MODULE.bazel version ${moduleFile.version} does not match release version ${version}.`,
235-
"Creating a version patch.",
240+
"Creating a version patch."
236241
);
237242
const patchFileName = "module_dot_bazel_version.patch";
238243
const existingContent = moduleFile.content;

src/domain/release-archive.spec.ts

Lines changed: 22 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import axios from "axios";
22
import axiosRetry from "axios-retry";
3-
import extractZip from "extract-zip";
43
import { mocked } from "jest-mock";
54
import fs, { WriteStream } from "node:fs";
65
import os from "node:os";
@@ -10,6 +9,7 @@ import { fakeModuleFile } from "../test/mock-template-files";
109
import { expectThrownError } from "../test/util";
1110
import {
1211
ArchiveDownloadError,
12+
MissingModuleFileError,
1313
ReleaseArchive,
1414
UnsupportedArchiveFormat,
1515
} from "./release-archive";
@@ -24,6 +24,7 @@ jest.mock("extract-zip");
2424
const RELEASE_ARCHIVE_URL = "https://foo.bar/rules-foo-v1.2.3.tar.gz";
2525
const STRIP_PREFIX = "rules-foo";
2626
const TEMP_DIR = "/tmp";
27+
const EXTRACT_DIR = `${TEMP_DIR}/archive-1234`;
2728

2829
beforeEach(() => {
2930
jest.clearAllMocks();
@@ -50,6 +51,9 @@ beforeEach(() => {
5051
);
5152

5253
mocked(os.tmpdir).mockReturnValue(TEMP_DIR);
54+
mocked(fs.mkdtempSync).mockReturnValue(EXTRACT_DIR);
55+
56+
mocked(fs.existsSync).mockReturnValue(true); // Existence check on MODULE.bazel
5357
});
5458

5559
describe("fetch", () => {
@@ -73,7 +77,7 @@ describe("fetch", () => {
7377
test("saves the archive to disk", async () => {
7478
await ReleaseArchive.fetch(RELEASE_ARCHIVE_URL, STRIP_PREFIX);
7579

76-
const expectedPath = path.join(TEMP_DIR, "rules-foo-v1.2.3.tar.gz");
80+
const expectedPath = path.join(EXTRACT_DIR, "rules-foo-v1.2.3.tar.gz");
7781
expect(fs.createWriteStream).toHaveBeenCalledWith(expectedPath, {
7882
flags: "w",
7983
});
@@ -94,7 +98,7 @@ describe("fetch", () => {
9498
STRIP_PREFIX
9599
);
96100

97-
const expectedPath = path.join(TEMP_DIR, "rules-foo-v1.2.3.tar.gz");
101+
const expectedPath = path.join(EXTRACT_DIR, "rules-foo-v1.2.3.tar.gz");
98102
expect(releaseArchive.diskPath).toEqual(expectedPath);
99103
});
100104

@@ -161,42 +165,7 @@ describe("extractModuleFile", () => {
161165
expect(tar.x).toHaveBeenCalledWith({
162166
cwd: path.dirname(releaseArchive.diskPath),
163167
file: releaseArchive.diskPath,
164-
strip: 1,
165-
});
166-
});
167-
168-
test("extracts a tarball when the strip_prefix is empty", async () => {
169-
const releaseArchive = await ReleaseArchive.fetch(
170-
"https://foo.bar/rules-foo-v1.2.3.tar.gz",
171-
""
172-
);
173-
await releaseArchive.extractModuleFile(".");
174-
175-
expect(tar.x).toHaveBeenCalledWith({
176-
cwd: path.dirname(releaseArchive.diskPath),
177-
file: releaseArchive.diskPath,
178-
strip: 0,
179-
});
180-
});
181-
182-
test("extracts the full zip archive next to the zip archive", async () => {
183-
const releaseArchive = await ReleaseArchive.fetch(
184-
"https://foo.bar/rules-foo-v1.2.3.zip",
185-
STRIP_PREFIX
186-
);
187-
await releaseArchive.extractModuleFile(".");
188-
189-
expect(extractZip).toHaveBeenCalledWith(releaseArchive.diskPath, {
190-
dir: path.dirname(releaseArchive.diskPath),
191168
});
192-
expect(fs.copyFileSync).toHaveBeenCalledWith(
193-
path.join(
194-
path.dirname(releaseArchive.diskPath),
195-
STRIP_PREFIX,
196-
"MODULE.bazel"
197-
),
198-
path.join(path.dirname(releaseArchive.diskPath), "MODULE.bazel")
199-
);
200169
});
201170

202171
test("loads the extracted MODULE.bazel file", async () => {
@@ -208,6 +177,7 @@ describe("extractModuleFile", () => {
208177

209178
const expectedPath = path.join(
210179
path.dirname(releaseArchive.diskPath),
180+
STRIP_PREFIX,
211181
"MODULE.bazel"
212182
);
213183
expect(fs.readFileSync).toHaveBeenCalledWith(expectedPath, "utf8");
@@ -222,6 +192,7 @@ describe("extractModuleFile", () => {
222192

223193
const expectedPath = path.join(
224194
path.dirname(releaseArchive.diskPath),
195+
STRIP_PREFIX,
225196
"sub",
226197
"dir",
227198
"MODULE.bazel"
@@ -239,4 +210,17 @@ describe("extractModuleFile", () => {
239210
expect(moduleFile.moduleName).toEqual("rules_foo");
240211
expect(moduleFile.version).toEqual("1.2.3");
241212
});
213+
214+
test("throws when MODULE.bazel cannot be found in the release archive", async () => {
215+
const releaseArchive = await ReleaseArchive.fetch(
216+
RELEASE_ARCHIVE_URL,
217+
STRIP_PREFIX
218+
);
219+
220+
mocked(fs.existsSync).mockReturnValue(false);
221+
222+
await expect(releaseArchive.extractModuleFile(".")).rejects.toThrow(
223+
MissingModuleFileError
224+
);
225+
});
242226
});

0 commit comments

Comments
 (0)