Skip to content
Merged
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
119 changes: 50 additions & 69 deletions .github/scripts/src/build-image/index.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,39 @@
import type { Environment } from "@tahminator/pipeline";

import {
DockerClient,
GitHubClient,
type Environment,
} from "@tahminator/pipeline";
import { $ } from "bun";
import yargs from "yargs";
import { hideBin } from "yargs/helpers";

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

const { environment, githubOutputFile } = await yargs(hideBin(process.argv))
.option("environment", {
type: "string",
choices: ["staging", "production"] satisfies Environment[],
demandOption: true,
})
.option("githubOutputFile", {
type: "string",
describe: "Path to GITHUB_OUTPUT (passed in automatically in CI)",
default: process.env.GITHUB_OUTPUT,
})
.parseAsync();

const tagPrefix = environment === "staging" ? "staging-" : "";
const serverProfiles = environment === "staging" ? "stg" : "prod";
const { environment, getGhaOutput, githubOutputFile, dockerFileName } =
await yargs(hideBin(process.argv))
.option("environment", {
choices: ["staging", "production"] satisfies Environment[],
describe: "Deployment environment (staging or production)",
demandOption: true,
})
.option("getGhaOutput", {
type: "boolean",
describe:
"Enable GitHub Actions output to receive latest built tag version",
default: false,
})
.option("githubOutputFile", {
type: "string",
describe:
"Path to GITHUB_OUTPUT (this will be passed in automatically in CI)",
default: process.env.GITHUB_OUTPUT,
})
.option("dockerFileName", {
type: "string",
default: "Dockerfile",
})
.strict()
.parse();

async function main() {
const DOCKER_HUB_PAT = process.env.DOCKER_HUB_PAT;
Expand All @@ -29,6 +42,14 @@ async function main() {
throw new Error("Missing DOCKER_HUB_PAT from platform-infra patchats.yaml");
}

const ghClient = await GitHubClient.createWithDefaultCiToken();
await using dockerClient = await DockerClient.create(
"patinanetwork",
DOCKER_HUB_PAT,
);

const tagPrefix = environment === "staging" ? "staging-" : "";

const timestamp = new Date()
.toLocaleString("en-US", {
timeZone: process.env.TZ,
Expand All @@ -44,59 +65,19 @@ async function main() {

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

const tags = [
`patinanetwork/patchats:${tagPrefix}latest`,
`patinanetwork/patchats:${tagPrefix}${timestamp}`,
`patinanetwork/patchats:${tagPrefix}${gitSha}`,
];

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

const proc = Bun.spawn(
["docker", "login", "-u", "patinanetwork", "--password-stdin"],
{
stdin: new TextEncoder().encode(DOCKER_HUB_PAT),
stdout: "inherit",
stderr: "inherit",
},
);

const exitCode = await proc.exited;

if (exitCode !== 0) {
throw new Error("Docker login failed");
}
try {
await $`docker buildx create --use --name patchats-builder`;
} catch {
await $`docker buildx use patchats-builder`;
}

const viteStagingArg =
serverProfiles === "stg" ? ["--build-arg", "VITE_STAGING=true"] : [];

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

await $`docker buildx build --push \
--platform linux/amd64 \
--file infra/Dockerfile \
--build-arg SERVER_PROFILES=${serverProfiles} \
--build-arg COMMIT_SHA=${gitSha} \
--cache-from=type=gha \
--cache-to=type=gha,mode=max \
${viteStagingArg} \
${tagArgs} \
.`;

console.log("Image pushed successfully.");
await dockerClient.buildImage({
dockerRepository: `patchats`,
dockerFileLocation: `infra/${dockerFileName}`,
tags: [`${tagPrefix}${timestamp}`, `${tagPrefix}${gitSha}`],
});

if (githubOutputFile) {
console.log("Outputting image tag...");
const w = Bun.file(githubOutputFile).writer();
await w.write(`tag<<EOF\n${tagPrefix}${gitSha}\nEOF\n`);
await w.flush();
await w.end();
if (getGhaOutput) {
await ghClient.outputToGithubOutput({
overrideGithubOutputFile: githubOutputFile ? githubOutputFile : undefined,
ctx: {
tag: `${tagPrefix}${gitSha}`,
},
});
}
}

Expand Down
165 changes: 17 additions & 148 deletions .github/scripts/src/db/run-local-db/index.ts
Original file line number Diff line number Diff line change
@@ -1,152 +1,21 @@
import { LocalPostgresClient } from "@tahminator/pipeline";
import { $ } from "bun";

const CONTAINER_NAME = "patchats-db";

const DB_USER = "postgres";
const DB_PASSWORD = "postgres";
const DB_NAME = "patchats";
const DB_PORT = "5440";

const MAX_ATTEMPTS = 30;
const RETRY_DELAY_MS = 2000;

type DbEnv = {
DATABASE_HOST: string;
DATABASE_PORT: string;
DATABASE_NAME: string;
DATABASE_USER: string;
DATABASE_PASSWORD: string;
};

async function start(): Promise<DbEnv> {
const env: DbEnv = {
DATABASE_HOST: "localhost",
DATABASE_PORT: DB_PORT,
DATABASE_NAME: DB_NAME,
DATABASE_USER: DB_USER,
DATABASE_PASSWORD: DB_PASSWORD,
};

try {
console.log("Starting Postgres container...");

// Remove a leftover container from a previous run.
await $`docker rm -f ${CONTAINER_NAME}`.quiet().nothrow();

await $`docker run -d \
--name ${CONTAINER_NAME} \
-e POSTGRES_USER=${DB_USER} \
-e POSTGRES_PASSWORD=${DB_PASSWORD} \
-e POSTGRES_DB=${DB_NAME} \
-p ${DB_PORT}:5432 \
mirror.gcr.io/library/postgres:16-alpine`;

const ready = await waitForPostgres();

if (!ready) {
throw new Error("Postgres failed to start in time.");
}

console.log("Postgres started. Running migrations...");

await $.env({
...process.env,
...env,
})`./mvnw flyway:migrate -Dflyway.locations=filesystem:./db`;

console.log("Postgres is ready.");

return env;
} catch (error) {
console.error("Failed to start Postgres:", error);

await end();

throw error;
}
}

async function waitForPostgres(): Promise<boolean> {
console.log("Waiting for Postgres to become ready...");

for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
const running =
await $`docker inspect -f "{{.State.Running}}" ${CONTAINER_NAME}`
.quiet()
.nothrow();

if (running.exitCode !== 0 || running.stdout.toString().trim() !== "true") {
console.error("Postgres container exited before becoming ready.");
return false;
}

const check =
await $`docker exec ${CONTAINER_NAME} pg_isready -U ${DB_USER} -d ${DB_NAME}`
.quiet()
.nothrow();

if (check.exitCode === 0) {
console.log("Postgres is accepting connections.");
return true;
}

console.log(`Waiting for Postgres... (${attempt}/${MAX_ATTEMPTS})`);
await Bun.sleep(RETRY_DELAY_MS);
}

return false;
}

async function end(): Promise<void> {
console.log("Stopping and removing Postgres container...");

try {
await printLogs();
} finally {
const stopResult = await $`docker stop ${CONTAINER_NAME}`.quiet().nothrow();

if (stopResult.exitCode !== 0) {
console.log("Postgres container was not running.");
}

await $`docker rm -f ${CONTAINER_NAME}`.quiet().nothrow();

delete process.env.DATABASE_HOST;
delete process.env.DATABASE_PORT;
delete process.env.DATABASE_NAME;
delete process.env.DATABASE_USER;
delete process.env.DATABASE_PASSWORD;
}
}

async function printLogs(): Promise<void> {
console.log("=== DB LOGS ===");

const logs = await $`docker logs ${CONTAINER_NAME}`.quiet().nothrow();

if (logs.exitCode === 0) {
const stdout = logs.stdout.toString().trim();
const stderr = logs.stderr.toString().trim();

if (stdout) {
console.log(stdout);
}

if (stderr) {
console.error(stderr);
}

if (!stdout && !stderr) {
console.log("DB logs are empty.");
}
} else {
console.log("No DB logs found.");
}

console.log("=== DB LOGS END ===");
async function main() {
await using pgClient = await LocalPostgresClient.create({
database: "patchats",
});

const { database, host, port, password, user } = pgClient.state;

await $.env({
...process.env,
DATABASE_HOST: host,
DATABASE_PORT: String(port),
DATABASE_NAME: database,
DATABASE_USER: user,
DATABASE_PASSWORD: password,
})`just migrate`;
}

export const db = {
start,
end,
};
await main();
Loading