#!/usr/bin/env node import { $, quote } from "zx"; import path from "node:path"; import { strict as assert } from "node:assert"; import { fileURLToPath } from "node:url"; // This has to be explicitly set for some reason with the way we install zx in CI $.quote = quote; $.verbose = true; const __dirname = path.dirname(fileURLToPath(import.meta.url)); /**Pushes the image + tags to registry*/ export async function dockerPush(registry: string, registryUser: string, registryToken: string, image: string, fullySpecifiedTags: string[]) { const registryHost = registry.split("/")[0]; console.log(`Logging in to ${registry}...`); await $`echo ${registryToken} | docker login ${registryHost} -u ${registryUser} --password-stdin`; for (const tag of fullySpecifiedTags) { console.log(`Pushing ${tag}...`); await $`docker push ${tag}`; } } /**Builds the image + tags*/ export async function dockerBuild(buildContext: string, dockerfilePath: string, fullySpecifiedTags: string[], buildArgs?: Record) { console.log(`Tagging: ${fullySpecifiedTags.join(", ")}`); const tagArgs = fullySpecifiedTags.flatMap((tag) => ["-t", tag]); const buildArgsBuilt = Object.entries(buildArgs ?? {}) .filter(([k,v]) => v !== undefined && v !== null && v !== "") .flatMap(([k,v]) => [`--build-arg`, `${k}=${v}`]); await $`docker build --progress=plain ${buildArgsBuilt} ${tagArgs} -f ${dockerfilePath} ${buildContext}`; } // Get the current git hash const GIT_HASH = (await $`git rev-parse --short HEAD`).stdout.trim(); const DOTFILES_VERSION = "0.0.1"; const BUILDDIR = path.join(__dirname, ".."); const DOCKERFILE = path.join(BUILDDIR, "dev.Dockerfile"); const REGISTRY = "git.b4t.dev"; const IMAGE = "cobertos/dotfiles"; const REGISTRY_TOKEN = process.env.REGISTRY_TOKEN; const REGISTRY_USER = process.env.GITHUB_ACTOR; const posArgs = process.argv.filter(a => !a.startsWith("-")); // Usage: node build-docker-container.ts [CMD] [NAME] [DOTFILES_ENVIRONMENT] [DOTFILES_ARCHITECTURE] [DOTFILES_USER] [DOTFILES_EMAIL] [DOTFILES_GIT_DEFAULT_BRANCH] const [/*node*/, /*script name*/, CMD, NAME, DOTFILES_ENVIRONMENT, DOTFILES_ARCHITECTURE, DOTFILES_USER, DOTFILES_EMAIL, DOTFILES_GIT_DEFAULT_BRANCH] = posArgs; const envs = { DOTFILES_ENVIRONMENT, DOTFILES_ARCHITECTURE, DOTFILES_USER, DOTFILES_EMAIL, DOTFILES_GIT_DEFAULT_BRANCH }; assert(["all", "build"].includes(CMD), `CMD must be one of "all" or "build". Got "${CMD}"`); const dotfilesUserForPath = DOTFILES_USER ? DOTFILES_USER : "cobertos"; const TAGS = [ `${NAME}-${GIT_HASH}`, `${NAME}-${DOTFILES_VERSION}`, `${NAME}-latest` ] // Convert to final tags .map(t => `${REGISTRY}/${IMAGE}:${t}`); // TODO: Readd build args console.log(`===== BUILDING DOTFILES DOCKERFILE ${NAME} =====`); await dockerBuild(BUILDDIR, DOCKERFILE, TAGS, envs); const imgSizeBytes = await $`docker image inspect ${TAGS[0]} --format '{{.Size}}'`; // .pipe($`numfmt --to=iec --suffix=B`); console.log(`Image built! Image size ${imgSizeBytes}`); if (CMD === "build") { process.exit(0); } console.log(`===== NIX CLOSURE SIZES =====`) // Print closure sizes // -s prints NAR size of the thing itself // -S prints total size of it + all it's dependencies // -r for recursive // we omit -h (human readable sizes) so we can sort and then format after // TODO: Readd // | numfmt --to=iec --field=2 --suffix=B \ // | numfmt --to=iec --field=3 --suffix=B \ // ... // | column -t // Pipe to sed to remove any "/nix/store/ffffff..." prefixes await $`docker run --rm ${TAGS[0]} nix path-info -rsS /home/${dotfilesUserForPath}/.nix-profile` .pipe($`sort -k2 -n`) .pipe($`sed 's|/nix/store/[a-z0-9]\{32\}-||g'`); console.log(`\n\n`); // TODO: Put this somewhere else, I like having it in the build but it makes it // take so longggg // Scan for vulns with vulnix // console.log(`===== RUNNING VULNERABILITY SCAN =====`); // console.log(`Running the vulnerability scan on just runtime dependencies`); // // This runs vulnix in the container against JUST the runtime dependencies, not // // all build dependencies. This is why we use the output of nix-store as vulnix // // doesnt do this otherwise, but it greatly reduces what vulns come out (and is // // much more what I want to check against) // // ~/.nix-profile (the current profile, contains the home manager generation) // // (we have to do all this with sh so we can xargs to vulnix but it has to be in the container) // // ALSO Fuck vulnix but it throws where there's no deriver, so I have to manually // // fetch that with nix-store first and sort those out. UGH this is a pain // await $`docker run --rm ${TAGS[0]} sh -c ${ // `nix-store -q --requisites /home/cobertos/.nix-profile \ // | xargs -n1 nix-store -qd 2>/dev/null \ // | grep -v unknown-deriver \ // | sort -u \ // | xargs nix run nixpkgs#vulnix -- --no-requisites` // }`.nothrow(); // // if this one returns non-zero we break the build // console.log(`\n\n\nEnd vulnerability scan\n===================================`); console.log(`===== PUSHING TO REPOSITORY =====`); // Push to repository if the above passed assert(REGISTRY_TOKEN, "REGISTRY_TOKEN must be set to push, received nothing"); assert(REGISTRY_USER, "GITHUB_ACTOR must be set to push, received nothing"); await dockerPush(REGISTRY, REGISTRY_USER, REGISTRY_TOKEN, IMAGE, TAGS); console.log(`Finished! :3`); console.log(`Image size ${imgSizeBytes}`);