feat(ci): baked CI image + runner config + self-check workflow (T14)
Stand up the foundation's own CI on its Forgejo runner. The committed scope here is the self-contained half (toolchain + typecheck); the stack-state-dependent pipelines (pulumi preview, backup-verify) need CI secrets + a state fetch and land next. - containers/ci-image/Dockerfile + VERSIONS IMAGE_CI: one baked image carrying exactly what preflight validates (pulumi/bun/node/docker/git/age/zstd/jq/vault/ psql/mc). Built on the VM (like caddy-cloudflare) and used LOCALLY by the runner. - runner.ts: give act_runner a config.yaml — container.network=foundation-net (so job containers reach foundation-forgejo:3000 for checkout + the data plane) and force_pull=false (use the local foundation-ci image, no registry). Self-heals on up. - .forgejo/workflows/ci.yml: preflight (tools + versions vs VERSIONS pins) + typecheck (bun install + tsc --noEmit on bootstrap). Gates every push. - run.sh / backup.sh / restore.sh / dr: take PULUMI_CONFIG_PASSPHRASE from env when set (CI secret), falling back to `pass` (operator) — so the scripts run pass-free in CI. Reusable-workflows architecture (per the chosen direction) — the ecosystem CI (semantic-release, docker/npm/bun builds, eslint/yamllint over the 999_testing.md candidates) builds on this image + runner next phase. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d807a45c79
commit
dda83bdc87
9 changed files with 125 additions and 6 deletions
32
.forgejo/workflows/ci.yml
Normal file
32
.forgejo/workflows/ci.yml
Normal file
|
|
@ -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
|
||||||
7
VERSIONS
7
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.
|
# (T04) and backup put/get (T12). RustFS speaks enough of the MinIO admin API.
|
||||||
IMAGE_MC=minio/mc:latest@sha256:a7fe349ef4bd8521fb8497f55c6042871b2ae640607cf99d9bede5e9bdf11727
|
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:
|
# NOTE on specific images:
|
||||||
# IMAGE_RUSTFS uses `latest` because RustFS does not (yet) publish stable
|
# IMAGE_RUSTFS uses `latest` because RustFS does not (yet) publish stable
|
||||||
# semver tags reliably (PLAN-002 R3 — RustFS is young). MUST be pinned by
|
# semver tags reliably (PLAN-002 R3 — RustFS is young). MUST be pinned by
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
DIR="$ROOT/bootstrap"
|
DIR="$ROOT/bootstrap"
|
||||||
TS="${1:-$(date -u +%Y%m%dT%H%M%SZ)}"
|
TS="${1:-$(date -u +%Y%m%dT%H%M%SZ)}"
|
||||||
export PULUMI_BACKEND_URL="file://${DIR}/state"
|
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}"
|
KEY="${SSH_PRIVATE_KEY_PATH:-${HOME}/.ssh/foundation-test_ed25519}"
|
||||||
MC_IMAGE="$(grep '^IMAGE_MC=' "$ROOT/VERSIONS" | cut -d= -f2-)"
|
MC_IMAGE="$(grep '^IMAGE_MC=' "$ROOT/VERSIONS" | cut -d= -f2-)"
|
||||||
cd "$DIR"
|
cd "$DIR"
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ DIR="$ROOT/bootstrap"
|
||||||
TS="${1:?usage: restore.sh <UTC-timestamp> [rfs|off]}"
|
TS="${1:?usage: restore.sh <UTC-timestamp> [rfs|off]}"
|
||||||
SRC="${2:-rfs}"
|
SRC="${2:-rfs}"
|
||||||
export PULUMI_BACKEND_URL="file://${DIR}/state"
|
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}"
|
KEY="${SSH_PRIVATE_KEY_PATH:-${HOME}/.ssh/foundation-test_ed25519}"
|
||||||
MC_IMAGE="$(grep '^IMAGE_MC=' "$ROOT/VERSIONS" | cut -d= -f2-)"
|
MC_IMAGE="$(grep '^IMAGE_MC=' "$ROOT/VERSIONS" | cut -d= -f2-)"
|
||||||
PG_IMAGE="$(grep '^IMAGE_POSTGRES=' "$ROOT/VERSIONS" | cut -d= -f2-)"
|
PG_IMAGE="$(grep '^IMAGE_POSTGRES=' "$ROOT/VERSIONS" | cut -d= -f2-)"
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,24 @@ export function deployRunner(
|
||||||
{ provider, retainOnDelete: true }, // holds .runner registration secret
|
{ 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(
|
const register = new command.remote.Command(
|
||||||
"foundation-runner-register",
|
"foundation-runner-register",
|
||||||
{
|
{
|
||||||
|
|
@ -51,6 +69,7 @@ VOL=foundation-runner-data
|
||||||
IMG='${img}'
|
IMG='${img}'
|
||||||
LABELS='${labels}'
|
LABELS='${labels}'
|
||||||
docker volume inspect "$VOL" >/dev/null 2>&1 || docker volume create "$VOL" >/dev/null
|
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
|
if docker run --rm --entrypoint sh -v "$VOL":/data "$IMG" -c '[ -s /data/.runner ]'; then
|
||||||
echo "runner already registered"
|
echo "runner already registered"
|
||||||
else
|
else
|
||||||
|
|
@ -60,7 +79,7 @@ else
|
||||||
echo "runner registered"
|
echo "runner registered"
|
||||||
fi`,
|
fi`,
|
||||||
addPreviousOutputInEnv: false,
|
addPreviousOutputInEnv: false,
|
||||||
triggers: [forgejo.ready.id, labels],
|
triggers: [forgejo.ready.id, labels, RUNNER_CONFIG],
|
||||||
},
|
},
|
||||||
{ dependsOn: [forgejo.ready] },
|
{ dependsOn: [forgejo.ready] },
|
||||||
);
|
);
|
||||||
|
|
@ -73,7 +92,7 @@ fi`,
|
||||||
hostname: "foundation-runner",
|
hostname: "foundation-runner",
|
||||||
restart: "unless-stopped",
|
restart: "unless-stopped",
|
||||||
entrypoints: ["/bin/forgejo-runner"],
|
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
|
// 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
|
// /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
|
// as root. NOTE: 996 is THIS host's docker gid — re-check on DR to a new VM
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
# Pin the backend PER-PROCESS via env — NEVER `pulumi login` (that mutates the
|
# 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).
|
# GLOBAL backend pointer in ~/.pulumi and would misdirect other projects' run.sh).
|
||||||
export PULUMI_BACKEND_URL="file://${DIR}/state"
|
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}"
|
export SSH_PRIVATE_KEY_PATH="${SSH_PRIVATE_KEY_PATH:-${HOME}/.ssh/foundation-test_ed25519}"
|
||||||
cd "$DIR"
|
cd "$DIR"
|
||||||
pulumi stack select foundation 2>/dev/null || pulumi stack init foundation
|
pulumi stack select foundation 2>/dev/null || pulumi stack init foundation
|
||||||
|
|
|
||||||
61
containers/ci-image/Dockerfile
Normal file
61
containers/ci-image/Dockerfile
Normal file
|
|
@ -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:<tag>`. 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"]
|
||||||
|
|
@ -29,7 +29,7 @@ while [ $# -gt 0 ]; do case "$1" in
|
||||||
[ -n "$TS" ] || { echo "--ts <UTC-timestamp> required (a bundle in the offsite bucket)" >&2; exit 2; }
|
[ -n "$TS" ] || { echo "--ts <UTC-timestamp> required (a bundle in the offsite bucket)" >&2; exit 2; }
|
||||||
|
|
||||||
export PULUMI_BACKEND_URL="file://${DIR}/state"
|
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
|
cd "$DIR"; pulumi stack select foundation >/dev/null
|
||||||
|
|
||||||
# Secrets from passphrase-encrypted config (the disaster-survivable set).
|
# Secrets from passphrase-encrypted config (the disaster-survivable set).
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue