Skip to content

Commit

Permalink
refactor: use a dependency injection framework
Browse files Browse the repository at this point in the history
  • Loading branch information
kormide committed Jun 17, 2024
1 parent 1361ae7 commit 3c8cf0b
Show file tree
Hide file tree
Showing 20 changed files with 404 additions and 179 deletions.
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "publish-to-bcr",
"private": true,
"type": "module",
"main": "./application/cloudfunction/index.js",
"main": "./application/webhook/index.js",
"engines": {
"node": "^18"
},
Expand All @@ -16,6 +16,8 @@
"dependencies": {
"@google-cloud/functions-framework": "^3.1.2",
"@google-cloud/secret-manager": "^5.0.1",
"@nestjs/common": "^10.3.9",
"@nestjs/core": "^10.3.9",
"@octokit/auth-app": "^4.0.4",
"@octokit/core": "^4.0.4",
"@octokit/rest": "^19.0.3",
Expand All @@ -29,6 +31,8 @@
"extract-zip": "^2.0.1",
"gcp-metadata": "^6.0.0",
"nodemailer": "^6.7.8",
"reflect-metadata": "^0.2.2",
"rxjs": "7.8.1",
"simple-git": "^3.16.0",
"source-map-support": "^0.5.21",
"tar": "^6.2.0",
Expand Down
5 changes: 4 additions & 1 deletion src/application/notifications.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Inject, Injectable } from "@nestjs/common";
import { UserFacingError } from "../domain/error.js";
import { Maintainer } from "../domain/metadata-file.js";
import { Repository } from "../domain/repository.js";
Expand All @@ -6,14 +7,16 @@ import { Authentication, EmailClient } from "../infrastructure/email.js";
import { GitHubClient } from "../infrastructure/github.js";
import { SecretsClient } from "../infrastructure/secrets.js";

@Injectable()
export class NotificationsService {
private readonly sender: string;
private readonly debugEmail?: string;
private emailAuth: Authentication;

constructor(
private readonly emailClient: EmailClient,
private readonly secretsClient: SecretsClient,
private readonly githubClient: GitHubClient
@Inject("rulesetRepoGitHubClient") private githubClient: GitHubClient
) {
if (process.env.NOTIFICATIONS_EMAIL === undefined) {
throw new Error("Missing NOTIFICATIONS_EMAIL environment variable.");
Expand Down
106 changes: 34 additions & 72 deletions src/application/release-event-handler.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Inject, Injectable } from "@nestjs/common";
import { Octokit } from "@octokit/rest";
import { ReleasePublishedEvent } from "@octokit/webhooks-types";
import { HandlerFunction } from "@octokit/webhooks/dist-types/types";
import { CreateEntryService } from "../domain/create-entry.js";
Expand All @@ -10,24 +12,26 @@ import {
RulesetRepository,
} from "../domain/ruleset-repository.js";
import { User } from "../domain/user.js";
import { EmailClient } from "../infrastructure/email.js";
import { GitClient } from "../infrastructure/git.js";
import { GitHubClient } from "../infrastructure/github.js";
import { SecretsClient } from "../infrastructure/secrets.js";
import { NotificationsService } from "./notifications.js";
import {
createAppAuthorizedOctokit,
createBotAppAuthorizedOctokit,
} from "./octokit.js";

interface PublishAttempt {
readonly successful: boolean;
readonly bcrFork: Repository;
readonly error?: Error;
}

@Injectable()
export class ReleaseEventHandler {
constructor(private readonly secretsClient: SecretsClient) {}
constructor(
@Inject("rulesetRepoGitHubClient")
private rulesetRepoGitHubClient: GitHubClient,
@Inject("appOctokit") private appOctokit: Octokit,
private readonly findRegistryForkService: FindRegistryForkService,
private readonly createEntryService: CreateEntryService,
private readonly publishEntryService: PublishEntryService,
private readonly notificationsService: NotificationsService
) {}

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

// The "app" refers to the public facing GitHub app installed to users'
// ruleset repos and BCR Forks that creates and pushes the entry to the
// fork. The "bot app" refers to the private app only installed to the
// canonical BCR which has reduced permissions and only opens PRs.
const appOctokit = await createAppAuthorizedOctokit(this.secretsClient);
const rulesetGitHubClient = await GitHubClient.forRepoInstallation(
appOctokit,
repository,
event.payload.installation.id
);

const botAppOctokit = await createBotAppAuthorizedOctokit(
this.secretsClient
);
const bcrGitHubClient = await GitHubClient.forRepoInstallation(
botAppOctokit,
bcr
);

const gitClient = new GitClient();
Repository.gitClient = gitClient;

const emailClient = new EmailClient();
const findRegistryForkService = new FindRegistryForkService(
rulesetGitHubClient
);
const publishEntryService = new PublishEntryService(bcrGitHubClient);
const notificationsService = new NotificationsService(
emailClient,
this.secretsClient,
rulesetGitHubClient
);

const repoCanonicalName = `${event.payload.repository.owner.login}/${event.payload.repository.name}`;
let releaser = await rulesetGitHubClient.getRepoUser(
let releaser = await this.rulesetRepoGitHubClient.getRepoUser(
event.payload.sender.login,
repository
);
Expand All @@ -82,8 +53,7 @@ export class ReleaseEventHandler {
const createRepoResult = await this.validateRulesetRepoOrNotifyFailure(
repository,
tag,
releaser,
notificationsService
releaser
);
if (!createRepoResult.successful) {
return;
Expand All @@ -93,18 +63,14 @@ export class ReleaseEventHandler {

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

releaser = await this.overrideReleaser(
releaser,
rulesetRepo,
rulesetGitHubClient
);
releaser = await this.overrideReleaser(releaser, rulesetRepo);

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

const candidateBcrForks =
await findRegistryForkService.findCandidateForks(
await this.findRegistryForkService.findCandidateForks(
rulesetRepo,
releaser
);
Expand All @@ -122,14 +88,9 @@ export class ReleaseEventHandler {

for (let bcrFork of candidateBcrForks) {
const forkGitHubClient = await GitHubClient.forRepoInstallation(
appOctokit,
this.appOctokit,
bcrFork
);
const createEntryService = new CreateEntryService(
gitClient,
forkGitHubClient,
bcrGitHubClient
);

const attempt = await this.attemptPublish(
rulesetRepo,
Expand All @@ -139,8 +100,7 @@ export class ReleaseEventHandler {
moduleRoot,
releaser,
releaseUrl,
createEntryService,
publishEntryService
forkGitHubClient
);
attempts.push(attempt);

Expand All @@ -152,7 +112,7 @@ export class ReleaseEventHandler {

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

await notificationsService.notifyError(
await this.notificationsService.notifyError(
releaser,
[],
Repository.fromCanonicalName(repoCanonicalName),
Expand All @@ -180,8 +140,7 @@ export class ReleaseEventHandler {
private async validateRulesetRepoOrNotifyFailure(
repository: Repository,
tag: string,
releaser: User,
notificationsService: NotificationsService
releaser: User
): Promise<{ rulesetRepo?: RulesetRepository; successful: boolean }> {
try {
const rulesetRepo = await RulesetRepository.create(
Expand Down Expand Up @@ -217,7 +176,7 @@ export class ReleaseEventHandler {
);
}

await notificationsService.notifyError(
await this.notificationsService.notifyError(
releaser,
maintainers,
repository,
Expand All @@ -240,32 +199,36 @@ export class ReleaseEventHandler {
moduleRoot: string,
releaser: User,
releaseUrl: string,
createEntryService: CreateEntryService,
publishEntryService: PublishEntryService
bcrForkGitHubClient: GitHubClient
): Promise<PublishAttempt> {
console.log(`Attempting publish to fork ${bcrFork.canonicalName}.`);

try {
const {moduleName} = await createEntryService.createEntryFiles(
const { moduleName } = await this.createEntryService.createEntryFiles(
rulesetRepo,
bcr,
tag,
moduleRoot
);

const branch = await createEntryService.commitEntryToNewBranch(
const branch = await this.createEntryService.commitEntryToNewBranch(
rulesetRepo,
bcr,
tag,
releaser
);
await createEntryService.pushEntryToFork(bcrFork, bcr, branch);
await this.createEntryService.pushEntryToFork(
bcrFork,
bcr,
branch,
bcrForkGitHubClient
);

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

await publishEntryService.sendRequest(
await this.publishEntryService.sendRequest(
tag,
bcrFork,
bcr,
Expand Down Expand Up @@ -297,8 +260,7 @@ export class ReleaseEventHandler {

private async overrideReleaser(
releaser: User,
rulesetRepo: RulesetRepository,
githubClient: GitHubClient
rulesetRepo: RulesetRepository
): Promise<User> {
// Use the release author unless a fixedReleaser is configured
if (rulesetRepo.config.fixedReleaser) {
Expand All @@ -307,7 +269,7 @@ export class ReleaseEventHandler {
);

// Fetch the releaser to get their name
const fixedReleaser = await githubClient.getRepoUser(
const fixedReleaser = await this.rulesetRepoGitHubClient.getRepoUser(
rulesetRepo.config.fixedReleaser.login,
rulesetRepo
);
Expand Down
33 changes: 33 additions & 0 deletions src/application/webhook/app.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Module } from "@nestjs/common";
import { CreateEntryService } from "../../domain/create-entry.js";
import { FindRegistryForkService } from "../../domain/find-registry-fork.js";
import { PublishEntryService } from "../../domain/publish-entry.js";
import { EmailClient } from "../../infrastructure/email.js";
import { GitClient } from "../../infrastructure/git.js";
import { SecretsClient } from "../../infrastructure/secrets.js";
import { NotificationsService } from "../notifications.js";
import { ReleaseEventHandler } from "../release-event-handler.js";
import {
APP_OCTOKIT_PROVIDER,
BCR_APP_OCTOKIT_PROVIDER,
BCR_GITHUB_CLIENT_PROVIDER,
RULESET_REPO_GITHUB_CLIENT_PROVIDER,
} from "./providers.js";

@Module({
providers: [
SecretsClient,
NotificationsService,
EmailClient,
GitClient,
ReleaseEventHandler,
CreateEntryService,
FindRegistryForkService,
PublishEntryService,
APP_OCTOKIT_PROVIDER,
BCR_APP_OCTOKIT_PROVIDER,
RULESET_REPO_GITHUB_CLIENT_PROVIDER,
BCR_GITHUB_CLIENT_PROVIDER,
],
})
export class AppModule {}
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ sourceMapSupport.install();
// Export all cloud function entrypoints here. The exported symbols
// are inputs to deployed cloud functions and are invoked when the
// function triggers.
export { handleGithubWebhookEvent } from "./github-webhook-entrypoint.js";
export { handleGithubWebhookEvent } from "./main.js";
Original file line number Diff line number Diff line change
@@ -1,27 +1,35 @@
import { HttpFunction } from "@google-cloud/functions-framework";
import { ContextIdFactory, NestFactory } from "@nestjs/core";
import { Webhooks } from "@octokit/webhooks";
import { SecretsClient } from "../../infrastructure/secrets.js";
import { ReleaseEventHandler } from "../release-event-handler.js";
import { AppModule } from "./app.module.js";

// Handle incoming GitHub webhook messages. This is the entrypoint for
// the webhook cloud function.
export const handleGithubWebhookEvent: HttpFunction = async (
request,
response
) => {
// Setup application dependencies using constructor dependency injection.
const secretsClient = new SecretsClient();

const releaseEventHandler = new ReleaseEventHandler(secretsClient);
const app = await NestFactory.createApplicationContext(AppModule);

const secretsClient = app.get(SecretsClient);
const githubWebhookSecret = await secretsClient.accessSecret(
"github-app-webhook-secret"
);

const webhooks = new Webhooks({ secret: githubWebhookSecret });
webhooks.on("release.published", (event) =>
releaseEventHandler.handle(event)
);
webhooks.on("release.published", async (event) => {
// Register the webhook event as the NestJS "request" so that it's available to inject.
const contextId = ContextIdFactory.create();
app.registerRequestByContextId(event, contextId);

const releaseEventHandler = await app.resolve(
ReleaseEventHandler,
contextId
);
await releaseEventHandler.handle(event);
});

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

await app.close();
response.status(200).send();
};
Loading

0 comments on commit 3c8cf0b

Please sign in to comment.