Skip to content

Commit

Permalink
*: added buildkite command/webhook handling
Browse files Browse the repository at this point in the history
  • Loading branch information
denbeigh2000 committed Oct 9, 2023
1 parent 87e8093 commit 4c7ef3f
Show file tree
Hide file tree
Showing 22 changed files with 1,407 additions and 712 deletions.
1 change: 1 addition & 0 deletions build.nix
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ let
# to run the provided minification + bundling, then write that to the Nix
# store. We can then publish in the next step by running
# publish --no-bundle. This may also require a custom build step(?)

buildPhase = ''
WRANGLER_BIN="${nodeModules}/node_modules/.bin/wrangler2"
NODE_MODULES_PATH="${nodeModules}"
Expand Down
7 changes: 4 additions & 3 deletions build.sh
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
"$WRANGLER_BIN" --version 2>&1 >/dev/null

WRANGLER_TOML="$(mktemp)"
echo "$STUB_WRANGLER_TOML" > "$WRANGLER_TOML"
COMPATIBILITY_DATE="2023-03-02"

ln -s $NODE_MODULES_PATH/node_modules node_modules

"$WRANGLER_BIN" publish \
"$WRANGLER_BIN" \
deploy \
--compatibility-date "$COMPATIBILITY_DATE" \
--name local-build \
--compatibility-date 2023-03-02 \
--minify \
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"eslint-plugin-editorconfig": "^4.0.2",
"prettier": "^2.7.1",
"typescript": "^4.7.3",
"wrangler": "2.0.22"
"wrangler": "^3"
},
"private": true,
"scripts": {
Expand Down
11 changes: 5 additions & 6 deletions release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ CLOUDFLARE_ENV_FILE="$(mktemp)"
# Load our config directly from our encrypted config file
# This is passed in as a file later via a tempfile
WRANGLER_SECRETS_FILE="$("$GIT_BIN" ls-files ":/secrets/wrangler.toml.age")"
WRANGLER_TOML_FILE="$(mktemp)"
"$AGE_BIN" --decrypt --identity "$IDENTITY_FILE" --output "$WRANGLER_TOML_FILE" "$WRANGLER_SECRETS_FILE"
# NOTE: Even though wrangler accepts a --config paramter, it still expects
# wrangler.toml to be in the local directory.
"$AGE_BIN" --decrypt --identity "$IDENTITY_FILE" --output ./wrangler.toml "$WRANGLER_SECRETS_FILE"

# Expected vars:
# - SENTRY_AUTH_TOKEN
Expand Down Expand Up @@ -76,12 +77,10 @@ then
ENV="dev"
fi

ln -sf "$WRANGLER_TOML_FILE" wrangler.toml

"$WRANGLER_BIN" publish \
"$WRANGLER_BIN" deploy \
--env "$ENV" \
--no-bundle \
$BUNDLED_WORKER_PATH/index.js
$BUNDLED_WORKER_PATH/index.js </dev/null

if [[ "$ENV" = "production" ]]
then
Expand Down
Binary file modified secrets/dev.secrets.env.age
Binary file not shown.
Binary file modified secrets/wrangler.toml.age
Binary file not shown.
99 changes: 99 additions & 0 deletions src/buildkite/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { Snowflake } from "discord-api-types/globals";
import { Env } from "../env";
import { BuildkiteErrorShape, Sentry } from "../sentry";
import { Build, BuildInfo, BuildState, Pipeline } from "./common";

export interface Attribution {
user: Snowflake,
message: Snowflake,
}

export interface Environment { [key: string]: string };

export interface BuildParams {
commit: string,
branch: string,
env: Environment,
}

export class BuildkiteClient {
organisation: string;
token: string;
baseURL: string;
sentry: Sentry;

constructor(sentry: Sentry, organisation: string, token: string, baseURL: string = "https://api.buildkite.com/v2/") {
this.organisation = organisation;
this.token = token;
this.baseURL = baseURL;
this.sentry = sentry;
}

async post(endpoint: string, data: any): Promise<Response> {
const body = JSON.stringify(data);
const url = new URL(endpoint, this.baseURL);
const req = new Request(url, {
method: "POST",
body,
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${this.token}`,
},
});

const resp = await fetch(req);
if (resp.status >= 400) {
const body = await resp.json() as BuildkiteErrorShape;
this.sentry.logBuildkiteError(req, resp, body);
throw body.message;
}
return resp;
}

public pipelineURL(pipelineName: string): string {
return `https://buildkite.com/${this.organisation}/${pipelineName}`;
}

public async createBuild(_env: Env, pipelineName: string, params: Partial<BuildParams>, attr: Attribution): Promise<BuildInfo | null> {
const endpoint = `organizations/${this.organisation}/pipelines/${pipelineName}/builds`;
const data = {
...params,
meta_data: {
"source": "discordbot",
"discord_requester_id": attr.user,
"discord_message_id": attr.message,
},
};

const resp = await this.post(endpoint, data);
const body = await resp.json() as any;

const build: Build = {
id: body.id as string,
url: body.web_url as string,
state: body.state as BuildState,
commit: body.commit as string || "HEAD",
number: body.number as number,
branch: body.branch as string || "master",
message: body.message as string || "",
};

const pipeline: Pipeline = {
id: body.pipeline.id as string,
name: body.pipeline.name as string,
slug: body.pipeline.slug as string,
};

const user = (body.author ? body.author : body.creator) || {};
const author = {
name: "name" in user ? user.name : "",
imageUrl: "avatar_url" in user ? user.avatar_url : undefined,
};

return {
build,
pipeline,
author,
};
}
}
59 changes: 59 additions & 0 deletions src/buildkite/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Snowflake } from "discord-api-types/v10"

export interface BuildRequest {
type: "requestedBuild";
user: Snowflake,
// Interaction ID of slash command that triggered build
interaction: Snowflake,

channel: Snowflake,
message: Snowflake,

// NOTE: may need to add more here (e.g., interaction response ID, any
// state we create)
}

export interface IncomingBuild {
type: "build"
buildID: string,
channel: Snowflake,
message: Snowflake,
}

export type BuildSource = IncomingBuild | BuildRequest;

export type BuildState = "running" | "scheduled" | "blocked" | "canceled" | "failed" | "passed" | "skipped" | "canceling" | "not run" | "started";

export interface Author {
name: string,
imageUrl: string | undefined,
}

export interface Build {
id: string,
url: string,
number: number,
branch: string,
commit: string,
message: string,
state: BuildState,
}

export interface BuildInfo {
build: Build,
pipeline: Pipeline,
author: Author,
}

export interface Pipeline {
id: string,
name: string,
slug: string,
}

export interface TrackedBuild {
build: BuildInfo,

// Source of this request (and associated metadata for updates etc)
source: BuildSource;
}
138 changes: 138 additions & 0 deletions src/buildkite/embeds.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { APIEmbed } from "discord-api-types/v10";
import { Sentry } from "../sentry";
import { BuildInfo, BuildState } from "./common";

// #F83F23
const RED = 16269091;
// #FDF5F5
const RED_LIGHT = 16643573;
// #FFBA11
const YELLOW = 16759313;
// #FFF8E7
const YELLOW_LIGHT = 16775399;
// #00BE13
const GREEN = 48659;
// #FAFDFA
const GREEN_LIGHT = 16449018;
// #C2CACE
const GREY = 12765902;
// #F9FAFB
const GREY_LIGHT = 16382715;

// const STALL_IMG = "https://em-content.zobj.net/source/animated-noto-colour-emoji/356/dotted-line-face_1fae5.gif";
// const RUNNING_IMG = "https://i.kym-cdn.com/photos/images/original/002/429/796/96c.gif";
// const SUCCESS_IMG = "https://static-cdn.jtvnw.net/emoticons/v2/emotesv2_d3100900bce94eb99e7251d741926564/animated/light/3.0";
// const FAILURE_IMG = "https://cdn3.emoji.gg/emojis/4438_Pensive_Bread.png";
const STALL_IMG = "https://pub-0faf9f8a28c14050a1d2a3decae82f38.r2.dev/dotted-line-face.gif";
const RUNNING_IMG = "https://pub-0faf9f8a28c14050a1d2a3decae82f38.r2.dev/duck-in-hat.gif";
const SUCCESS_IMG = "https://pub-0faf9f8a28c14050a1d2a3decae82f38.r2.dev/limesDance.gif";
const FAILURE_IMG = "https://pub-0faf9f8a28c14050a1d2a3decae82f38.r2.dev/pensive-bread.png";

// Not sure if this is just for me, but this default gravatar image is very
// unappealing lol.
const DEFAULT_BK_IMG = "https://www.gravatar.com/avatar/3f0e71403ee9fefd2a1cc0df38e14c81";

export interface State {
thumbnail: string,
colour: number,
colourLight: number,
emoji: string,
}

export enum BuildColour {
STALLED = "stalled",
RUNNING = "running",
PASSED = "passed",
FAILED = "failed",
}

export function stateMap(given: BuildState, sentry: Sentry): BuildColour {
switch (given) {
case "running":
case "started":
return BuildColour.RUNNING;
case "scheduled":
case "blocked":
case "skipped":
case "not run":
return BuildColour.STALLED;
case "passed":
return BuildColour.PASSED;
case "failed":
case "canceled":
case "canceling":
return BuildColour.FAILED;
default:
sentry.setExtra("givenState", given);
sentry.sendMessage("unhandled state");
return BuildColour.STALLED;
}
}

export type ColourSet = {
[k in BuildColour]: State
}

export const STATE_COLOURS: ColourSet = {
[BuildColour.STALLED]: {
thumbnail: STALL_IMG,
colour: GREY,
colourLight: GREY_LIGHT,
emoji: "😪",
},
[BuildColour.RUNNING]: {
thumbnail: RUNNING_IMG,
colour: YELLOW,
colourLight: YELLOW_LIGHT,
emoji: "⏱️",
},
[BuildColour.PASSED]: {
thumbnail: SUCCESS_IMG,
colour: GREEN,
colourLight: GREEN_LIGHT,
emoji: "✅"
},
[BuildColour.FAILED]: {
thumbnail: FAILURE_IMG,
colour: RED,
colourLight: RED_LIGHT,
emoji: "❌",
},
};

export function buildEmbed(build: BuildInfo, sentry: Sentry): APIEmbed {
const stateType = stateMap(build.build.state, sentry);
const stateData = STATE_COLOURS[stateType];

const fullCommit = build.build.commit || "HEAD";
const commit = fullCommit.substring(0, 13);
const title = `${stateData.emoji} ${build.pipeline.name} (#${build.build.number})`
const { url } = build.build;
let { message } = build.build;
if (message && message.length > 50) {
message = message.substring(0, 47) + "...";
}

const imageUrl = build.author.imageUrl !== DEFAULT_BK_IMG
? build.author.imageUrl
: undefined;

const msgField = message
? [{ name: "Message", value: message }]
: [];

return {
title,
url,
author: {
name: build.author.name,
icon_url: imageUrl,
},
color: stateData.colour,
thumbnail: { url: stateData.thumbnail },
fields: msgField.concat([
{ name: "Commit", value: `\`${commit}\`` },
{ name: "State", value: `${stateData.emoji} ${build.build.state}`, },
]),
};
}
32 changes: 32 additions & 0 deletions src/buildkite/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Environment, BuildParams, BuildkiteClient, Attribution } from "./client";
import { BuildRequest, BuildSource, TrackedBuild, BuildState, Pipeline, Build, IncomingBuild, BuildInfo } from "./common";
import { Tracker as BuildTracker } from "./tracker";

import { Sentry } from "../sentry";

function discordUserFromWebhook(payload: any, sentry: Sentry): Attribution | null {
const meta = payload.build.meta_data as any;
const { discord_requester_id: user, discord_message_id: message } = meta;
if (user && message) {
return { user, message };
} else if (!user && !message) {
return null;
} else {
// Only partial info here, ideally this shouldn't occur
sentry.setExtra("user", user);
sentry.setExtra("message", message);
sentry.sendMessage("inconsistent user/message in build event", "warning");
return null;
}
}

export {
BuildSource as Associations,
BuildRequest,
BuildInfo,
BuildParams,
BuildTracker,
BuildkiteClient,
Environment,
TrackedBuild,
};
Loading

0 comments on commit 4c7ef3f

Please sign in to comment.