Files
archery/src/BuildController.ts
Cory Sanin b926b303c5
All checks were successful
Docker Image CI / Build build images (arch) (push) Successful in 6s
Docker Image CI / Build build images (artix) (push) Successful in -42s
NPM Audit Check / Check NPM audit (push) Successful in -2m9s
App Image CI / Build app image (push) Successful in -1m6s
v0.2.4
2025-11-12 01:39:21 -05:00

202 lines
6.2 KiB
TypeScript

import spawn from 'child_process';
import EventEmitter from 'events';
import type { Build, DB } from "./DB.ts";
const docker_images = {
arch: 'corysanin/archery:arch',
artix: 'corysanin/archery:artix',
}
type LogType = 'std' | 'err' | 'finish';
interface BuildEvent {
id: number;
type: LogType;
message: any;
}
interface ControllerConfig {
"arch-mirror"?: string;
"artix-mirror"?: string;
}
function getContainerName(id: number) {
return `archery-build-${id}`;
}
class BuildController extends EventEmitter {
private db: DB;
private running: boolean = false;
private interval: NodeJS.Timeout;
private cancelled: boolean = false;
private config: ControllerConfig;
constructor(config: ControllerConfig = {}) {
super();
this.config = config;
// this.interval = setInterval(this.triggerBuild, 60000);
}
triggerBuild = () => {
if (!this.running) {
this.running = true;
this.kickOffBuild();
return true;
}
return false;
}
private kickOffBuild = async () => {
this.running = true;
this.cancelled = false;
const build = await this.db.dequeue();
if (build === null) {
this.running = false;
return;
}
try {
await this.pullImage(build.distro);
await this.build(build);
}
catch (e) {
console.error(e);
this.db.finishBuild(build.id, 'error');
}
this.kickOffBuild();
}
private pullImage = (distro: string) => {
return new Promise<void>((resolve, reject) => {
if (!(distro in docker_images)) {
return reject();
}
const docker = spawn.spawn('docker', ['pull', docker_images[distro]]);
docker.stdout.on('data', (data) => {
console.log(`${data}`);
});
docker.stderr.on('data', (data) => {
console.error(`${data}`);
});
docker.on('close', (code) => {
if (code === 0) {
resolve();
} else {
reject(code);
}
});
docker.on('error', (err) => {
reject(err);
});
});
}
private build = async (build: Build) => {
return new Promise<void>((resolve, _) => {
const docker = spawn.spawn('docker', this.createBuildParams(build));
docker.on('spawn', () => {
const remainder = {
std: '',
err: ''
}
this.db.startBuild(build.id, docker.pid);
let createLogFunction = (type: LogType) => {
return (data: Buffer | string) => {
const str = data.toString();
const readyToLog = remainder[type] + str.substring(0, str.lastIndexOf('\n'));
remainder[type] = str.substring(str.lastIndexOf('\n') + 1);
this.db.appendLog(build.id, type, readyToLog);
this.emitLog({
id: build.id,
type: type,
message: readyToLog
});
};
};
docker.stdout.on('data', createLogFunction('std'));
docker.stderr.on('data', createLogFunction('err'));
});
docker.on('close', (code) => {
const status = code === 0 ? 'success' : (this.cancelled ? 'cancelled' : 'error');
this.emitLog({
id: build.id,
type: 'finish',
message: status
});
this.db.finishBuild(build.id, status);
resolve();
});
});
}
private emitLog = (msg: BuildEvent) => {
this.emit('log', msg);
}
private createBuildParams = (build: Build) => {
const customMirror = this.config?.[`${build.distro}-mirror`];
const params = ['run', '--rm', '-e', `REPO=${build.repo}`];
if (build.dependencies) {
params.push('-e', `DEP=${build.dependencies}`);
}
if (build.commit) {
params.push('-e', `COMMIT=${build.commit}`);
}
if (build.patch) {
params.push('-e', `PATCH=${build.patch}`);
}
if (customMirror && typeof customMirror === 'string' && customMirror.length > 0) {
params.push('-e', `MIRROR=${customMirror}`);
}
params.push('--name', getContainerName(build.id));
params.push(docker_images[build.distro]);
return params;
}
cancelBuild = async (id: number) => {
const running = this.running;
const build = await this.db.getBuild(id);
if (running && build.status === 'queued') {
await this.db.finishBuild(id, 'cancelled');
return;
}
await new Promise<void>((resolve, reject) => {
const dockerPs = spawn.spawn('docker', ['ps', '--filter', `name=${getContainerName(id)}`, '--format', '{{.ID}}']);
let output = '';
dockerPs.on('spawn', () => {
dockerPs.stdout.on('data', (data: Buffer | string) => {
output += data.toString();
});
});
dockerPs.on('close', (code) => {
if (code > 0) {
return reject('failed to get container id');
}
this.cancelled = true;
const dockerKill = spawn.spawn('docker', ['stop', output.trim()]);
dockerKill.on('close', (code) => {
if (code > 0) {
return reject('failed to kill container');
}
resolve();
});
});
});
}
setDB = (db: DB) => {
this.db = db;
}
close = () => {
if (this.interval) {
clearInterval(this.interval);
}
}
}
export default BuildController;
export { BuildController };
export type { BuildEvent, LogType, ControllerConfig };