Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for BitBucket #143

Merged
merged 8 commits into from
Oct 30, 2023
86 changes: 86 additions & 0 deletions bbevent/handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// Copyright (c) Fensak, LLC.
// SPDX-License-Identifier: AGPL-3.0-or-later OR BUSL-1.1

import { logger } from "../logging/mod.ts";
import { fensakCfgRepoName } from "../constants/mod.ts";
import type { BitBucketEventPayload } from "../svcdata/mod.ts";

import { appInstalled, appUninstalled } from "./installation.ts";
import { onPush } from "./push.ts";
import { onPullRequest } from "./pullrequest.ts";

/**
* Handles the given BitBucket event.
*
* @return A boolean indicating whether the operation needs to be retried.
*/
export async function handleBitBucketEvent(
msg: BitBucketEventPayload,
): Promise<boolean> {
logger.info(`[${msg.requestID}] Processing bitbucket ${msg.eventName} event`);

let retry = false;
switch (msg.eventName) {
default:
logger.debug(
`[${msg.requestID}] Discarding bitbucket event ${msg.eventName}`,
);
return false;

case "installed":
retry = await appInstalled(msg.requestID, msg.payload);
break;

case "uninstalled":
retry = await appUninstalled(msg.requestID, msg.payload);
break;

case "repo:push":
retry = await onPush(msg.requestID, msg.payload);
break;

case "pullrequest:approved":
case "pullrequest:unapproved":
case "pullrequest:rejected":
case "pullrequest:changes_request_created":
case "pullrequest:created":
retry = await onPullRequest(msg.requestID, msg.payload);
break;
}

logger.info(`[${msg.requestID}] Processed bitbucket ${msg.eventName} event`);
return retry;
}

/**
* Filter out events that can be filtered just by looking at the webhook data. This is useful for rejecting events at
* the web request layer before enqueuing in the work queue, to save on request units for the underlying queue system.
*
* @return A boolean indicating whether the event should be rejected (true for reject, false for keep).
*/
export function fastRejectEvent(
eventName: string,
// deno-lint-ignore no-explicit-any
payload: any,
): boolean {
switch (eventName) {
case "repo:push": {
// Reject if not a push event to a branch of the .fensak repository.
const repoName = payload.repository.name;
if (repoName != fensakCfgRepoName) {
return true;
}
// If any change in pushed through this event has a branch update, then accept the event.
for (const ch of payload.push.changes) {
if (ch.new && ch.new.type === "branch") {
return false;
}
}
// Reaching here means none of the changes pertain to updating a branch, so reject.
return true;
}
}

// At this point, event hasn't been rejected, so allow it.
return false;
}
90 changes: 90 additions & 0 deletions bbevent/installation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// Copyright (c) Fensak, LLC.
// SPDX-License-Identifier: AGPL-3.0-or-later OR BUSL-1.1

import { logger } from "../logging/mod.ts";
import {
BitBucketWorkspace,
getBitBucketWorkspace,
getBitBucketWorkspaceByClientKey,
removeSecurityContextForBitBucketWorkspace,
storeBitBucketWorkspace,
} from "../svcdata/mod.ts";
import type { BitBucketSecurityContext } from "../svcdata/mod.ts";

/**
* Processes an app installed event that comes in through BitBucket.
*
* TODO
* Implement allowlist for workspaces
*
* @return A boolean indicating whether the operation needs to be retried.
*/
export async function appInstalled(
requestID: string,
// deno-lint-ignore no-explicit-any
payload: any,
): Promise<boolean> {
const name = payload.principal.username;
const securityCtx: BitBucketSecurityContext = {
key: payload.key,
clientKey: payload.clientKey,
publicKey: payload.publicKey,
sharedSecret: payload.sharedSecret,
baseApiUrl: payload.baseApiUrl,
};

let bbws: BitBucketWorkspace;
let maybeBBWS = await getBitBucketWorkspace(name);
if (maybeBBWS.value !== null) {
// Make sure the existing security context is removed if exists.
await removeSecurityContextForBitBucketWorkspace(maybeBBWS);

// Update the existing BBWS reference to avoid consistency errors.
maybeBBWS = await getBitBucketWorkspace(name);
// NOTE: This check is not necessary, but is useful to make the compiler happy
if (maybeBBWS.value === null) {
throw new Error("impossible condition");
}

bbws = { ...maybeBBWS.value };
bbws.securityContext = securityCtx;
} else {
bbws = {
name: payload.principal.username,
subscriptionID: null,
securityContext: securityCtx,
};
}

const ok = await storeBitBucketWorkspace(bbws, maybeBBWS);
if (!ok) {
logger.error(
`[${requestID}] Consistency error while storing bitbucket workspace. Retrying event.`,
);
return true;
}

return false;
}

/**
* Processes an app uninstalled event that comes in through BitBucket.
*
* @return A boolean indicating whether the operation needs to be retried.
*/
export async function appUninstalled(
requestID: string,
// deno-lint-ignore no-explicit-any
payload: any,
): Promise<boolean> {
const clientKey = payload.clientKey;
const [_n, maybeBBWS] = await getBitBucketWorkspaceByClientKey(clientKey);
if (!maybeBBWS || !maybeBBWS.value) {
logger.debug(
`[${requestID}] Ignoring uninstall BitBucket app event: already uninstalled.`,
);
return false;
}
await removeSecurityContextForBitBucketWorkspace(maybeBBWS);
return false;
}
8 changes: 8 additions & 0 deletions bbevent/mod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Copyright (c) Fensak, LLC.
// SPDX-License-Identifier: AGPL-3.0-or-later OR BUSL-1.1

/**
* bbevent
* Contains handlers for BitBucket webhook events.
*/
export * from "./handler.ts";
Loading