Skip to content

Commit 3c8cf0b

Browse files
committed
refactor: use a dependency injection framework
1 parent 1361ae7 commit 3c8cf0b

20 files changed

+404
-179
lines changed

package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "publish-to-bcr",
33
"private": true,
44
"type": "module",
5-
"main": "./application/cloudfunction/index.js",
5+
"main": "./application/webhook/index.js",
66
"engines": {
77
"node": "^18"
88
},
@@ -16,6 +16,8 @@
1616
"dependencies": {
1717
"@google-cloud/functions-framework": "^3.1.2",
1818
"@google-cloud/secret-manager": "^5.0.1",
19+
"@nestjs/common": "^10.3.9",
20+
"@nestjs/core": "^10.3.9",
1921
"@octokit/auth-app": "^4.0.4",
2022
"@octokit/core": "^4.0.4",
2123
"@octokit/rest": "^19.0.3",
@@ -29,6 +31,8 @@
2931
"extract-zip": "^2.0.1",
3032
"gcp-metadata": "^6.0.0",
3133
"nodemailer": "^6.7.8",
34+
"reflect-metadata": "^0.2.2",
35+
"rxjs": "7.8.1",
3236
"simple-git": "^3.16.0",
3337
"source-map-support": "^0.5.21",
3438
"tar": "^6.2.0",

src/application/notifications.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Inject, Injectable } from "@nestjs/common";
12
import { UserFacingError } from "../domain/error.js";
23
import { Maintainer } from "../domain/metadata-file.js";
34
import { Repository } from "../domain/repository.js";
@@ -6,14 +7,16 @@ import { Authentication, EmailClient } from "../infrastructure/email.js";
67
import { GitHubClient } from "../infrastructure/github.js";
78
import { SecretsClient } from "../infrastructure/secrets.js";
89

10+
@Injectable()
911
export class NotificationsService {
1012
private readonly sender: string;
1113
private readonly debugEmail?: string;
1214
private emailAuth: Authentication;
15+
1316
constructor(
1417
private readonly emailClient: EmailClient,
1518
private readonly secretsClient: SecretsClient,
16-
private readonly githubClient: GitHubClient
19+
@Inject("rulesetRepoGitHubClient") private githubClient: GitHubClient
1720
) {
1821
if (process.env.NOTIFICATIONS_EMAIL === undefined) {
1922
throw new Error("Missing NOTIFICATIONS_EMAIL environment variable.");

src/application/release-event-handler.ts

Lines changed: 34 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { Inject, Injectable } from "@nestjs/common";
2+
import { Octokit } from "@octokit/rest";
13
import { ReleasePublishedEvent } from "@octokit/webhooks-types";
24
import { HandlerFunction } from "@octokit/webhooks/dist-types/types";
35
import { CreateEntryService } from "../domain/create-entry.js";
@@ -10,24 +12,26 @@ import {
1012
RulesetRepository,
1113
} from "../domain/ruleset-repository.js";
1214
import { User } from "../domain/user.js";
13-
import { EmailClient } from "../infrastructure/email.js";
14-
import { GitClient } from "../infrastructure/git.js";
1515
import { GitHubClient } from "../infrastructure/github.js";
16-
import { SecretsClient } from "../infrastructure/secrets.js";
1716
import { NotificationsService } from "./notifications.js";
18-
import {
19-
createAppAuthorizedOctokit,
20-
createBotAppAuthorizedOctokit,
21-
} from "./octokit.js";
2217

2318
interface PublishAttempt {
2419
readonly successful: boolean;
2520
readonly bcrFork: Repository;
2621
readonly error?: Error;
2722
}
2823

24+
@Injectable()
2925
export class ReleaseEventHandler {
30-
constructor(private readonly secretsClient: SecretsClient) {}
26+
constructor(
27+
@Inject("rulesetRepoGitHubClient")
28+
private rulesetRepoGitHubClient: GitHubClient,
29+
@Inject("appOctokit") private appOctokit: Octokit,
30+
private readonly findRegistryForkService: FindRegistryForkService,
31+
private readonly createEntryService: CreateEntryService,
32+
private readonly publishEntryService: PublishEntryService,
33+
private readonly notificationsService: NotificationsService
34+
) {}
3135

3236
public readonly handle: HandlerFunction<"release.published", unknown> =
3337
async (event) => {
@@ -36,41 +40,8 @@ export class ReleaseEventHandler {
3640
process.env.BAZEL_CENTRAL_REGISTRY
3741
);
3842

39-
// The "app" refers to the public facing GitHub app installed to users'
40-
// ruleset repos and BCR Forks that creates and pushes the entry to the
41-
// fork. The "bot app" refers to the private app only installed to the
42-
// canonical BCR which has reduced permissions and only opens PRs.
43-
const appOctokit = await createAppAuthorizedOctokit(this.secretsClient);
44-
const rulesetGitHubClient = await GitHubClient.forRepoInstallation(
45-
appOctokit,
46-
repository,
47-
event.payload.installation.id
48-
);
49-
50-
const botAppOctokit = await createBotAppAuthorizedOctokit(
51-
this.secretsClient
52-
);
53-
const bcrGitHubClient = await GitHubClient.forRepoInstallation(
54-
botAppOctokit,
55-
bcr
56-
);
57-
58-
const gitClient = new GitClient();
59-
Repository.gitClient = gitClient;
60-
61-
const emailClient = new EmailClient();
62-
const findRegistryForkService = new FindRegistryForkService(
63-
rulesetGitHubClient
64-
);
65-
const publishEntryService = new PublishEntryService(bcrGitHubClient);
66-
const notificationsService = new NotificationsService(
67-
emailClient,
68-
this.secretsClient,
69-
rulesetGitHubClient
70-
);
71-
7243
const repoCanonicalName = `${event.payload.repository.owner.login}/${event.payload.repository.name}`;
73-
let releaser = await rulesetGitHubClient.getRepoUser(
44+
let releaser = await this.rulesetRepoGitHubClient.getRepoUser(
7445
event.payload.sender.login,
7546
repository
7647
);
@@ -82,8 +53,7 @@ export class ReleaseEventHandler {
8253
const createRepoResult = await this.validateRulesetRepoOrNotifyFailure(
8354
repository,
8455
tag,
85-
releaser,
86-
notificationsService
56+
releaser
8757
);
8858
if (!createRepoResult.successful) {
8959
return;
@@ -93,18 +63,14 @@ export class ReleaseEventHandler {
9363

9464
console.log(`Release author: ${releaser.username}`);
9565

96-
releaser = await this.overrideReleaser(
97-
releaser,
98-
rulesetRepo,
99-
rulesetGitHubClient
100-
);
66+
releaser = await this.overrideReleaser(releaser, rulesetRepo);
10167

10268
console.log(
10369
`Release published: ${rulesetRepo.canonicalName}@${tag} by @${releaser.username}`
10470
);
10571

10672
const candidateBcrForks =
107-
await findRegistryForkService.findCandidateForks(
73+
await this.findRegistryForkService.findCandidateForks(
10874
rulesetRepo,
10975
releaser
11076
);
@@ -122,14 +88,9 @@ export class ReleaseEventHandler {
12288

12389
for (let bcrFork of candidateBcrForks) {
12490
const forkGitHubClient = await GitHubClient.forRepoInstallation(
125-
appOctokit,
91+
this.appOctokit,
12692
bcrFork
12793
);
128-
const createEntryService = new CreateEntryService(
129-
gitClient,
130-
forkGitHubClient,
131-
bcrGitHubClient
132-
);
13394

13495
const attempt = await this.attemptPublish(
13596
rulesetRepo,
@@ -139,8 +100,7 @@ export class ReleaseEventHandler {
139100
moduleRoot,
140101
releaser,
141102
releaseUrl,
142-
createEntryService,
143-
publishEntryService
103+
forkGitHubClient
144104
);
145105
attempts.push(attempt);
146106

@@ -152,7 +112,7 @@ export class ReleaseEventHandler {
152112

153113
// Send out error notifications if none of the attempts succeeded
154114
if (!attempts.some((a) => a.successful)) {
155-
await notificationsService.notifyError(
115+
await this.notificationsService.notifyError(
156116
releaser,
157117
rulesetRepo.metadataTemplate(moduleRoot).maintainers,
158118
rulesetRepo,
@@ -165,7 +125,7 @@ export class ReleaseEventHandler {
165125
// Handle any other unexpected errors
166126
console.log(error);
167127

168-
await notificationsService.notifyError(
128+
await this.notificationsService.notifyError(
169129
releaser,
170130
[],
171131
Repository.fromCanonicalName(repoCanonicalName),
@@ -180,8 +140,7 @@ export class ReleaseEventHandler {
180140
private async validateRulesetRepoOrNotifyFailure(
181141
repository: Repository,
182142
tag: string,
183-
releaser: User,
184-
notificationsService: NotificationsService
143+
releaser: User
185144
): Promise<{ rulesetRepo?: RulesetRepository; successful: boolean }> {
186145
try {
187146
const rulesetRepo = await RulesetRepository.create(
@@ -217,7 +176,7 @@ export class ReleaseEventHandler {
217176
);
218177
}
219178

220-
await notificationsService.notifyError(
179+
await this.notificationsService.notifyError(
221180
releaser,
222181
maintainers,
223182
repository,
@@ -240,32 +199,36 @@ export class ReleaseEventHandler {
240199
moduleRoot: string,
241200
releaser: User,
242201
releaseUrl: string,
243-
createEntryService: CreateEntryService,
244-
publishEntryService: PublishEntryService
202+
bcrForkGitHubClient: GitHubClient
245203
): Promise<PublishAttempt> {
246204
console.log(`Attempting publish to fork ${bcrFork.canonicalName}.`);
247205

248206
try {
249-
const {moduleName} = await createEntryService.createEntryFiles(
207+
const { moduleName } = await this.createEntryService.createEntryFiles(
250208
rulesetRepo,
251209
bcr,
252210
tag,
253211
moduleRoot
254212
);
255213

256-
const branch = await createEntryService.commitEntryToNewBranch(
214+
const branch = await this.createEntryService.commitEntryToNewBranch(
257215
rulesetRepo,
258216
bcr,
259217
tag,
260218
releaser
261219
);
262-
await createEntryService.pushEntryToFork(bcrFork, bcr, branch);
220+
await this.createEntryService.pushEntryToFork(
221+
bcrFork,
222+
bcr,
223+
branch,
224+
bcrForkGitHubClient
225+
);
263226

264227
console.log(
265228
`Pushed bcr entry for module '${moduleRoot}' to fork ${bcrFork.canonicalName} on branch ${branch}`
266229
);
267230

268-
await publishEntryService.sendRequest(
231+
await this.publishEntryService.sendRequest(
269232
tag,
270233
bcrFork,
271234
bcr,
@@ -297,8 +260,7 @@ export class ReleaseEventHandler {
297260

298261
private async overrideReleaser(
299262
releaser: User,
300-
rulesetRepo: RulesetRepository,
301-
githubClient: GitHubClient
263+
rulesetRepo: RulesetRepository
302264
): Promise<User> {
303265
// Use the release author unless a fixedReleaser is configured
304266
if (rulesetRepo.config.fixedReleaser) {
@@ -307,7 +269,7 @@ export class ReleaseEventHandler {
307269
);
308270

309271
// Fetch the releaser to get their name
310-
const fixedReleaser = await githubClient.getRepoUser(
272+
const fixedReleaser = await this.rulesetRepoGitHubClient.getRepoUser(
311273
rulesetRepo.config.fixedReleaser.login,
312274
rulesetRepo
313275
);
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { Module } from "@nestjs/common";
2+
import { CreateEntryService } from "../../domain/create-entry.js";
3+
import { FindRegistryForkService } from "../../domain/find-registry-fork.js";
4+
import { PublishEntryService } from "../../domain/publish-entry.js";
5+
import { EmailClient } from "../../infrastructure/email.js";
6+
import { GitClient } from "../../infrastructure/git.js";
7+
import { SecretsClient } from "../../infrastructure/secrets.js";
8+
import { NotificationsService } from "../notifications.js";
9+
import { ReleaseEventHandler } from "../release-event-handler.js";
10+
import {
11+
APP_OCTOKIT_PROVIDER,
12+
BCR_APP_OCTOKIT_PROVIDER,
13+
BCR_GITHUB_CLIENT_PROVIDER,
14+
RULESET_REPO_GITHUB_CLIENT_PROVIDER,
15+
} from "./providers.js";
16+
17+
@Module({
18+
providers: [
19+
SecretsClient,
20+
NotificationsService,
21+
EmailClient,
22+
GitClient,
23+
ReleaseEventHandler,
24+
CreateEntryService,
25+
FindRegistryForkService,
26+
PublishEntryService,
27+
APP_OCTOKIT_PROVIDER,
28+
BCR_APP_OCTOKIT_PROVIDER,
29+
RULESET_REPO_GITHUB_CLIENT_PROVIDER,
30+
BCR_GITHUB_CLIENT_PROVIDER,
31+
],
32+
})
33+
export class AppModule {}

src/application/cloudfunction/index.ts renamed to src/application/webhook/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@ sourceMapSupport.install();
44
// Export all cloud function entrypoints here. The exported symbols
55
// are inputs to deployed cloud functions and are invoked when the
66
// function triggers.
7-
export { handleGithubWebhookEvent } from "./github-webhook-entrypoint.js";
7+
export { handleGithubWebhookEvent } from "./main.js";
Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,35 @@
11
import { HttpFunction } from "@google-cloud/functions-framework";
2+
import { ContextIdFactory, NestFactory } from "@nestjs/core";
23
import { Webhooks } from "@octokit/webhooks";
34
import { SecretsClient } from "../../infrastructure/secrets.js";
45
import { ReleaseEventHandler } from "../release-event-handler.js";
6+
import { AppModule } from "./app.module.js";
57

68
// Handle incoming GitHub webhook messages. This is the entrypoint for
79
// the webhook cloud function.
810
export const handleGithubWebhookEvent: HttpFunction = async (
911
request,
1012
response
1113
) => {
12-
// Setup application dependencies using constructor dependency injection.
13-
const secretsClient = new SecretsClient();
14-
15-
const releaseEventHandler = new ReleaseEventHandler(secretsClient);
14+
const app = await NestFactory.createApplicationContext(AppModule);
1615

16+
const secretsClient = app.get(SecretsClient);
1717
const githubWebhookSecret = await secretsClient.accessSecret(
1818
"github-app-webhook-secret"
1919
);
2020

2121
const webhooks = new Webhooks({ secret: githubWebhookSecret });
22-
webhooks.on("release.published", (event) =>
23-
releaseEventHandler.handle(event)
24-
);
22+
webhooks.on("release.published", async (event) => {
23+
// Register the webhook event as the NestJS "request" so that it's available to inject.
24+
const contextId = ContextIdFactory.create();
25+
app.registerRequestByContextId(event, contextId);
26+
27+
const releaseEventHandler = await app.resolve(
28+
ReleaseEventHandler,
29+
contextId
30+
);
31+
await releaseEventHandler.handle(event);
32+
});
2533

2634
await webhooks.verifyAndReceive({
2735
id: request.headers["x-github-delivery"] as string,
@@ -30,5 +38,6 @@ export const handleGithubWebhookEvent: HttpFunction = async (
3038
signature: request.headers["x-hub-signature-256"] as string,
3139
});
3240

41+
await app.close();
3342
response.status(200).send();
3443
};

0 commit comments

Comments
 (0)