Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,6 @@
.env.production filter=git-crypt-l3 diff=git-crypt-l3
.env.migrator filter=git-crypt-l3 diff=git-crypt-l3
infra/.env.production filter=git-crypt-l3 diff=git-crypt-l3

internal/.env.ci filter=git-crypt-l2 diff=git-crypt-l2
internal/standup-bot/.env.production filter=git-crypt-l3 diff=git-crypt-l3
38 changes: 38 additions & 0 deletions .github/composite/build-image/internal/standup-bot/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: "Build & Upload Docker Image for codebloom-standup-bot"
description: "Build & (optionally) upload Docker Image to Docker Registry"

inputs:
GPG_PRIVATE_KEY:
description: "GPG Private Key"
required: true
GPG_PASSPHRASE:
description: "GPG Passphrase"
required: true
DOCKER_UPLOAD:
description: "Boolean indicating whether the image should be uploaded to Docker registry or not."
required: false
default: true

runs:
using: "composite"
steps:
- name: Setup CI
uses: ./.github/composite/setup-ci
with:
GPG_PRIVATE_KEY: ${{ inputs.GPG_PRIVATE_KEY }}
GPG_PASSPHRASE: ${{ inputs.GPG_PASSPHRASE }}

- name: Set up OpenJDK 25
uses: actions/setup-java@v4
with:
distribution: "temurin"
java-version: "25"

- name: Expose GitHub Runtime
uses: crazy-max/ghaction-github-runtime@v3

- name: Run script
shell: bash
run: bun .github/scripts/build-image/internal/standup-bot
env:
DOCKER_UPLOAD: ${{ inputs.DOCKER_UPLOAD }}
32 changes: 32 additions & 0 deletions .github/composite/redeploy/internal/standup-bot/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: "Re-Deploy codebloom-standup-bot to Coolify"
description: "Trigger a deployment of the standup bot to Coolify."

inputs:
GPG_PRIVATE_KEY:
description: "GPG Private Key"
required: true
GPG_PASSPHRASE:
description: "GPG Passphrase"
required: true

runs:
using: "composite"
steps:
- name: Setup CI
uses: ./.github/composite/setup-ci
with:
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}

- name: Expose GitHub Runtime
uses: crazy-max/ghaction-github-runtime@v3

- name: Run script
shell: bash
run: bun .github/scripts/redeploy/internal/standup-bot
env:
DOCKER_UPLOAD: ${{ inputs.DOCKER_UPLOAD }}
TAG_PREFIX: ${{ inputs.TAG_PREFIX }}
SERVER_PROFILES: ${{ inputs.SERVER_PROFILES }}
ENVIRONMENT: ${{ inputs.ENVIRONMENT }}
SHA: ${{ inputs.SHA }}
87 changes: 87 additions & 0 deletions .github/scripts/build-image/internal/standup-bot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { $ } from "bun";
import { getEnvVariables } from "load-secrets/env/load";

process.env.TZ = "America/New_York";

const shouldDockerUpload = Boolean(process.env.DOCKER_UPLOAD) || false;

async function main() {
const ciEnv = await getEnvVariables(["ci"]);
const { dockerHubPat } = parseCiEnv(ciEnv);

// copy old tz format from build-image.sh
const timestamp = new Date()
.toLocaleString("en-US", {
timeZone: process.env.TZ,
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false,
})
.replace(/(\d+)\/(\d+)\/(\d+),\s(\d+):(\d+):(\d+)/, "$3.$1.$2-$4.$5.$6");

const gitSha = (await $`git rev-parse --short HEAD`.text()).trim();

const tags = [
`tahminator/codebloom-standup-bot:latest`,
`tahminator/codebloom-standup-bot:${timestamp}`,
`tahminator/codebloom-standup-bot:${gitSha}`,
];

console.log("Building image with following tags:");
tags.forEach((tag) => console.log(tag));

if (dockerHubPat) {
console.log("DOCKER_HUB_PAT found");
} else {
console.log("DOCKER_HUB_PAT missing or empty");
}

await $`echo ${dockerHubPat} | docker login -u tahminator --password-stdin`;

try {
await $`docker buildx create --use --name codebloom-standup-bot-builder`;
} catch {
await $`docker buildx use codebloom-standup-bot-builder`;
}

const buildMode = shouldDockerUpload ? "--push" : "--load";

const tagArgs = tags.flatMap((tag) => ["--tag", tag]);

console.log(`cwd is ${process.cwd()}`);

await $`docker buildx build ${buildMode} \
--platform linux/amd64 \
--file internal/standup-bot/Dockerfile \
--cache-from=type=gha \
--cache-to=type=gha,mode=max \
${tagArgs} \
.`;

console.log("Image pushed successfully.");
}

function parseCiEnv(ciEnv: Record<string, string>) {
const dockerHubPat = (() => {
const v = ciEnv["DOCKER_HUB_PAT"];
if (!v) {
throw new Error("Missing DOCKER_HUB_PAT from .env.ci");
}
return v;
})();

return { dockerHubPat };
}

main()
.then(() => {
process.exit(0);
})
.catch((e) => {
console.error(e);
process.exit(1);
});
5 changes: 5 additions & 0 deletions .github/scripts/bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion .github/scripts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
"version": "1.0.0",
"description": "CodeBloom CI Scripts",
"scripts": {
"typecheck": "tsc --noEmit",
"coolify-lib-hack": "find node_modules/coolify -type f -name '*.ts' -exec sh -c 'echo \"// @ts-nocheck\" > /tmp/file.tmp && cat \"$1\" >> /tmp/file.tmp && mv /tmp/file.tmp \"$1\"' _ {} \\;",
"typecheck": "bun run coolify-lib-hack && tsc --noEmit",
"test": "bun run typecheck && bun run prettier && bun run lint",
"fix": "bun run eslint:fix && bun run prettier:fix",
"lint": "bun run eslint",
Expand All @@ -21,6 +22,7 @@
"@notionhq/client": "^5.7.0",
"@octokit/rest": "^22.0.1",
"bun": "^1.3.6",
"coolify": "https://github.com/tahminator/Coolify-TypeScript-SDK",
"octokit": "^5.0.5"
},
"devDependencies": {
Expand Down
8 changes: 8 additions & 0 deletions .github/scripts/redeploy/internal/coolify/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Coolify } from "coolify/src";

export async function initClient(bearerAuth: string, serverURL: string) {
return new Coolify({
bearerAuth,
serverURL,
});
}
117 changes: 117 additions & 0 deletions .github/scripts/redeploy/internal/standup-bot/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { getEnvVariables } from "load-secrets/env/load";
import { initClient } from "redeploy/internal/coolify";
import {
_getOrCreateStandupBotResource,
_triggerStandupBotDeployment,
_updateStandupBotAppEnvs,
} from "redeploy/internal/standup-bot/utils";

async function main() {
// should already be called
// await $`git-crypt unlock`;

const { bearerAuth, serverUrl, projectUuid, serverUuid } = parseCiEnv(
await getEnvVariables(["ci"], {
baseDir: "internal",
}),
);

const client = await initClient(bearerAuth, serverUrl);

const appUuid = await _getOrCreateStandupBotResource({
client,
projectUuid,
serverUuid,
});

const appEnv = await getEnvVariables(["production"], {
baseDir: "internal/standup-bot",
});

await _updateStandupBotAppEnvs({
client,
appUuid,
envs: appEnv,
});

const pendingDeploymentId = await _triggerStandupBotDeployment({
client,
appUuid,
});

console.log("Deployment ID:", pendingDeploymentId);

let ready = false;
const attempts = 60;

for (let i = 1; i <= attempts; i++) {
const res = await client.deployments.get({
uuid: pendingDeploymentId,
});

const phase = res.status;

console.log("Deployment phase:", phase);

if (phase === "finished") {
console.log("Deployment has completed!");
ready = true;
break;
}

if (phase === "failed" || phase === "error" || phase === "cancelled") {
console.log(`Deployment failed with phase ${phase}`);
process.exit(1);
}

console.log(`Waiting for deployment to complete... (${i}/${attempts})`);
await Bun.sleep(10000);
}

if (!ready) {
console.error("Deployment did not reach a valid state within 10 minutes.");
process.exit(1);
}
}

function parseCiEnv(ciEnv: Record<string, string>) {
const bearerAuth = (() => {
const v = ciEnv["COOLIFY_BEARER_AUTH"];
if (!v) {
throw new Error("Missing COOLIFY_BEARER_AUTH from .env.ci");
}
return v;
})();
const serverUrl = (() => {
const v = ciEnv["COOLIFY_SERVER_URL"];
if (!v) {
throw new Error("Missing COOLIFY_SERVER_URL from .env.ci");
}
return v;
})();
const serverUuid = (() => {
const v = ciEnv["COOLIFY_SERVER_UUID"];
if (!v) {
throw new Error("Missing COOLIFY_SERVER_UUID from .env.ci");
}
return v;
})();
const projectUuid = (() => {
const v = ciEnv["COOLIFY_PROJECT_UUID"];
if (!v) {
throw new Error("Missing COOLIFY_PROJECT_UUID from .env.ci");
}
return v;
})();

return { bearerAuth, serverUrl, serverUuid, projectUuid };
}

main()
.then(() => {
process.exit(0);
})
.catch((e) => {
console.error(e);
process.exit(1);
});
Loading
Loading