diff --git a/.forgejo/workflows/.gitkeep b/.forgejo/workflows/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml new file mode 100644 index 0000000..da2ebed --- /dev/null +++ b/.forgejo/workflows/ci.yml @@ -0,0 +1,32 @@ +# CI — foundation self-checks (T14). Runs on the foundation's own runner, in the +# baked foundation-ci image (VERSIONS IMAGE_CI; force_pull:false → local image). +# These two jobs are self-contained (checkout + toolchain only) — no stack state or +# secrets needed, so they gate every push. The stack-state-dependent pipelines +# (pulumi preview, backup-verify) live in their own files and need CI secrets + +# a state fetch (see those workflows' headers). +name: CI +on: + push: + pull_request: + +jobs: + preflight: + runs-on: docker + container: + image: foundation-ci:latest + steps: + - uses: actions/checkout@v4 + - name: Toolchain preflight (tools present + >= VERSIONS pins) + run: ./preflight/preflight.sh tools versions + + typecheck: + runs-on: docker + container: + image: foundation-ci:latest + steps: + - uses: actions/checkout@v4 + - name: Install workspace deps + run: bun install --frozen-lockfile || bun install + - name: Typecheck bootstrap (tsc --noEmit) + working-directory: bootstrap + run: bunx tsc --noEmit diff --git a/VERSIONS b/VERSIONS index cd2c4f0..8fa8ab5 100644 --- a/VERSIONS +++ b/VERSIONS @@ -72,6 +72,13 @@ IMAGE_REGISTRY=registry:2@sha256:PIN_DIGEST # (T04) and backup put/get (T12). RustFS speaks enough of the MinIO admin API. IMAGE_MC=minio/mc:latest@sha256:a7fe349ef4bd8521fb8497f55c6042871b2ae640607cf99d9bede5e9bdf11727 +# CI toolchain image (T14): the baked image the foundation's own .forgejo/workflows +# run in (pulumi/bun/node/docker/git/age/zstd/jq/vault/psql/mc). Built ON the VM from +# containers/ci-image/Dockerfile (like caddy-cloudflare) and used LOCALLY by the runner +# (runner config force_pull:false) — not pulled from a registry, so the tag is the ref. +# Rebuild: scp the Dockerfile + `docker build -t foundation-ci:latest .` on the VM. +IMAGE_CI=foundation-ci:latest + # NOTE on specific images: # IMAGE_RUSTFS uses `latest` because RustFS does not (yet) publish stable # semver tags reliably (PLAN-002 R3 — RustFS is young). MUST be pinned by diff --git a/backup/backup.sh b/backup/backup.sh index 6a99317..11acfe4 100755 --- a/backup/backup.sh +++ b/backup/backup.sh @@ -13,7 +13,7 @@ ROOT="$(cd "$(dirname "$0")/.." && pwd)" DIR="$ROOT/bootstrap" TS="${1:-$(date -u +%Y%m%dT%H%M%SZ)}" export PULUMI_BACKEND_URL="file://${DIR}/state" -export PULUMI_CONFIG_PASSPHRASE="$(pass olsitec-foundation/PULUMI_CONFIG_PASSPHRASE)" +export PULUMI_CONFIG_PASSPHRASE="${PULUMI_CONFIG_PASSPHRASE:-$(pass olsitec-foundation/PULUMI_CONFIG_PASSPHRASE)}" KEY="${SSH_PRIVATE_KEY_PATH:-${HOME}/.ssh/foundation-test_ed25519}" MC_IMAGE="$(grep '^IMAGE_MC=' "$ROOT/VERSIONS" | cut -d= -f2-)" cd "$DIR" diff --git a/backup/restore.sh b/backup/restore.sh index 6206690..9d333e0 100755 --- a/backup/restore.sh +++ b/backup/restore.sh @@ -12,7 +12,7 @@ DIR="$ROOT/bootstrap" TS="${1:?usage: restore.sh [rfs|off]}" SRC="${2:-rfs}" export PULUMI_BACKEND_URL="file://${DIR}/state" -export PULUMI_CONFIG_PASSPHRASE="$(pass olsitec-foundation/PULUMI_CONFIG_PASSPHRASE)" +export PULUMI_CONFIG_PASSPHRASE="${PULUMI_CONFIG_PASSPHRASE:-$(pass olsitec-foundation/PULUMI_CONFIG_PASSPHRASE)}" KEY="${SSH_PRIVATE_KEY_PATH:-${HOME}/.ssh/foundation-test_ed25519}" MC_IMAGE="$(grep '^IMAGE_MC=' "$ROOT/VERSIONS" | cut -d= -f2-)" PG_IMAGE="$(grep '^IMAGE_POSTGRES=' "$ROOT/VERSIONS" | cut -d= -f2-)" diff --git a/bootstrap/components/runner.ts b/bootstrap/components/runner.ts index f4a076d..9c5a8cb 100644 --- a/bootstrap/components/runner.ts +++ b/bootstrap/components/runner.ts @@ -42,6 +42,24 @@ export function deployRunner( { provider, retainOnDelete: true }, // holds .runner registration secret ); + // act_runner config (T14): job containers must join foundation-net to reach + // foundation-forgejo:3000 (checkout) + the data plane, and must NOT force-pull — + // the CI toolchain image (foundation-ci, VERSIONS IMAGE_CI) is built locally on the + // VM, not in a registry. valid_volumes allows jobs to mount the host docker socket + // (docker-label builds). Re-written on every up so config drift self-heals. + const RUNNER_CONFIG = `log: + level: info +runner: + capacity: 2 + timeout: 30m + fetch_interval: 2s +container: + network: foundation-net + force_pull: false + valid_volumes: + - /var/run/docker.sock +`; + const register = new command.remote.Command( "foundation-runner-register", { @@ -51,6 +69,7 @@ VOL=foundation-runner-data IMG='${img}' LABELS='${labels}' docker volume inspect "$VOL" >/dev/null 2>&1 || docker volume create "$VOL" >/dev/null +printf '%s' '${RUNNER_CONFIG}' | docker run --rm -i --entrypoint sh -v "$VOL":/data "$IMG" -c 'cat > /data/config.yaml' if docker run --rm --entrypoint sh -v "$VOL":/data "$IMG" -c '[ -s /data/.runner ]'; then echo "runner already registered" else @@ -60,7 +79,7 @@ else echo "runner registered" fi`, addPreviousOutputInEnv: false, - triggers: [forgejo.ready.id, labels], + triggers: [forgejo.ready.id, labels, RUNNER_CONFIG], }, { dependsOn: [forgejo.ready] }, ); @@ -73,7 +92,7 @@ fi`, hostname: "foundation-runner", restart: "unless-stopped", entrypoints: ["/bin/forgejo-runner"], - command: ["daemon"], + command: ["daemon", "-c", "/data/config.yaml"], // T14 runner config (network/force_pull) // The image runs as uid 1000; add the host docker group (gid of // /var/run/docker.sock) so the daemon can reach the socket without running // as root. NOTE: 996 is THIS host's docker gid — re-check on DR to a new VM diff --git a/bootstrap/run.sh b/bootstrap/run.sh index 9b8c88c..e00231c 100755 --- a/bootstrap/run.sh +++ b/bootstrap/run.sh @@ -5,7 +5,7 @@ DIR="$(cd "$(dirname "$0")" && pwd)" # Pin the backend PER-PROCESS via env — NEVER `pulumi login` (that mutates the # GLOBAL backend pointer in ~/.pulumi and would misdirect other projects' run.sh). export PULUMI_BACKEND_URL="file://${DIR}/state" -export PULUMI_CONFIG_PASSPHRASE="$(pass olsitec-foundation/PULUMI_CONFIG_PASSPHRASE)" +export PULUMI_CONFIG_PASSPHRASE="${PULUMI_CONFIG_PASSPHRASE:-$(pass olsitec-foundation/PULUMI_CONFIG_PASSPHRASE)}" export SSH_PRIVATE_KEY_PATH="${SSH_PRIVATE_KEY_PATH:-${HOME}/.ssh/foundation-test_ed25519}" cd "$DIR" pulumi stack select foundation 2>/dev/null || pulumi stack init foundation diff --git a/containers/ci-image/Dockerfile b/containers/ci-image/Dockerfile new file mode 100644 index 0000000..4269dd9 --- /dev/null +++ b/containers/ci-image/Dockerfile @@ -0,0 +1,61 @@ +# foundation-ci — the baked CI toolchain image (T14). +# +# A single, pinnable image carrying every tool the foundation's own pipelines need +# so jobs don't install a toolchain on each run. Referenced by .forgejo/workflows/* +# via `container: foundation-ci:`. Built on the VM (like caddy-cloudflare) and +# used locally by the runner (force_pull:false) — see runner.ts / VERSIONS IMAGE_CI. +# +# Carries exactly what preflight/checks/tools.sh validates: pulumi, bun, node, +# docker (cli), git, age, zstd, jq, vault, psql, pg_dump, ssh, mc — plus pass-free +# operation (PULUMI_CONFIG_PASSPHRASE + SSH key arrive as CI secrets/env). +FROM node:20-bookworm + +ARG PULUMI_VERSION=3.145.0 +ARG VAULT_VERSION=1.18.5 +ARG MC_RELEASE=RELEASE.2025-04-03T17-07-56Z +ARG TARGETARCH=amd64 + +ENV DEBIAN_FRONTEND=noninteractive +# Install pulumi + bun into /usr/local/bin so they're on PATH for ANY shell/user +# (a login shell resets PATH, and jobs may not run as root). +ENV BUN_INSTALL=/usr/local + +# --- base apt tools: git, ssh, age, zstd, jq, postgresql-client, docker CLI ---------- +RUN set -eux; \ + install -m 0755 -d /etc/apt/keyrings; \ + apt-get update; \ + apt-get install -y --no-install-recommends \ + ca-certificates curl gnupg lsb-release unzip \ + git openssh-client age zstd jq; \ + # docker CE CLI (jobs build/push images via the mounted host socket) + curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc; \ + chmod a+r /etc/apt/keyrings/docker.asc; \ + echo "deb [arch=$TARGETARCH signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian bookworm stable" > /etc/apt/sources.list.d/docker.list; \ + # postgresql-client 15 (psql + pg_dump) from pgdg + curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor -o /etc/apt/keyrings/pgdg.gpg; \ + echo "deb [signed-by=/etc/apt/keyrings/pgdg.gpg] https://apt.postgresql.org/pub/repos/apt bookworm-pgdg main" > /etc/apt/sources.list.d/pgdg.list; \ + apt-get update; \ + apt-get install -y --no-install-recommends docker-ce-cli postgresql-client-16; \ + rm -rf /var/lib/apt/lists/* + +# --- pulumi (pinned) → copy binaries to /usr/local/bin ------------------------------- +RUN set -eux; curl -fsSL https://get.pulumi.com | sh -s -- --version "$PULUMI_VERSION"; \ + cp /root/.pulumi/bin/* /usr/local/bin/; rm -rf /root/.pulumi; \ + pulumi version + +# --- bun (pinned via official installer; BUN_INSTALL=/usr/local) --------------------- +RUN set -eux; curl -fsSL https://bun.sh/install | bash; bun --version + +# --- vault CLI (pinned) -------------------------------------------------------------- +RUN set -eux; \ + curl -fsSL "https://releases.hashicorp.com/vault/${VAULT_VERSION}/vault_${VAULT_VERSION}_linux_${TARGETARCH}.zip" -o /tmp/vault.zip; \ + unzip -d /usr/local/bin /tmp/vault.zip; rm -f /tmp/vault.zip; vault --version + +# --- minio client mc (pinned release) ------------------------------------------------ +RUN set -eux; \ + curl -fsSL "https://dl.min.io/client/mc/release/linux-${TARGETARCH}/archive/mc.${MC_RELEASE}" -o /usr/local/bin/mc; \ + chmod +x /usr/local/bin/mc; mc --version + +# Forgejo Actions overrides the entrypoint with its job script; keep a sane default. +WORKDIR /workspace +CMD ["bash"] diff --git a/dr/restore-to-fresh-vm.sh b/dr/restore-to-fresh-vm.sh index 20e1214..4324ac0 100755 --- a/dr/restore-to-fresh-vm.sh +++ b/dr/restore-to-fresh-vm.sh @@ -29,7 +29,7 @@ while [ $# -gt 0 ]; do case "$1" in [ -n "$TS" ] || { echo "--ts required (a bundle in the offsite bucket)" >&2; exit 2; } export PULUMI_BACKEND_URL="file://${DIR}/state" -export PULUMI_CONFIG_PASSPHRASE="$(pass olsitec-foundation/PULUMI_CONFIG_PASSPHRASE)" +export PULUMI_CONFIG_PASSPHRASE="${PULUMI_CONFIG_PASSPHRASE:-$(pass olsitec-foundation/PULUMI_CONFIG_PASSPHRASE)}" cd "$DIR"; pulumi stack select foundation >/dev/null # Secrets from passphrase-encrypted config (the disaster-survivable set).