diff --git a/.woodpecker/ci-image.yml b/.woodpecker/ci-image.yml new file mode 100644 index 0000000..c89e24a --- /dev/null +++ b/.woodpecker/ci-image.yml @@ -0,0 +1,40 @@ +# Build & push the pre-baked CI base image (Dockerfile.ci) to the Gitea +# registry CI already publishes to. Reuses the exact kaniko + auth pattern +# from publish.yml (REGISTRY_USER/REGISTRY_PASS from_secret, /kaniko/.docker +# config.json). Other pipelines (ci.yml, publish.yml) pull `ci-base:latest` +# for their install step. +# +# Rebuild ONLY when the dependency set or the image recipe changes — a normal +# code push must not trigger a 25-min image build. `path` applies to push/PR +# events; `event: tag` (releases) rebuilds unconditionally so a tagged release +# always ships a fresh base. +when: + - event: tag + - event: [push, manual] + branch: main + path: + include: + - 'pnpm-lock.yaml' + - 'Dockerfile.ci' + +steps: + build-ci-base: + image: gcr.io/kaniko-project/executor:debug + environment: + REGISTRY_USER: + from_secret: gitea_username + REGISTRY_PASS: + from_secret: gitea_password + CI_COMMIT_BRANCH: ${CI_COMMIT_BRANCH} + CI_COMMIT_TAG: ${CI_COMMIT_TAG} + CI_COMMIT_SHA: ${CI_COMMIT_SHA} + commands: + - mkdir -p /kaniko/.docker + - echo "{\"auths\":{\"git.mosaicstack.dev\":{\"username\":\"$REGISTRY_USER\",\"password\":\"$REGISTRY_PASS\"}}}" > /kaniko/.docker/config.json + - | + # Lockfile-hash tag: an immutable identity for the exact dep set baked + # into this image. `:latest` is the mutable pointer pipelines consume. + LOCK_HASH=$(sha256sum pnpm-lock.yaml | cut -c1-12) + DESTINATIONS="--destination git.mosaicstack.dev/mosaicstack/stack/ci-base:latest" + DESTINATIONS="$DESTINATIONS --destination git.mosaicstack.dev/mosaicstack/stack/ci-base:lock-$LOCK_HASH" + /kaniko/executor --context . --dockerfile Dockerfile.ci $DESTINATIONS diff --git a/Dockerfile.ci b/Dockerfile.ci new file mode 100644 index 0000000..b0f8b81 --- /dev/null +++ b/Dockerfile.ci @@ -0,0 +1,43 @@ +# Pre-baked CI base image for Woodpecker pipelines. +# +# Purpose: eliminate the cold `pnpm install` that dominates every pipeline +# (~731s median). This image ships the native toolchain (no per-run `apk add`) +# AND a warm, content-addressable pnpm store with the dependency-tree tarballs +# already fetched at build time. `pnpm fetch` only populates the store from the +# lockfile — it does NOT run the native node-gyp builds (better-sqlite3, +# node-pty, sqlite3, canvas, sharp); those still compile at `pnpm install`, +# which is exactly why the musl toolchain stays baked into this image. A +# pipeline `pnpm install --frozen-lockfile --prefer-offline` then resolves +# tarballs from local hard-links (no network) and compiles natives against the +# already-present toolchain, in tens of seconds instead of ~731s. +# +# Rebuilt only when `pnpm-lock.yaml` or this Dockerfile change +# (see .woodpecker/ci-image.yml). +# +# Node version is intentionally pinned to 22 (Active LTS at time of writing). +# The node:22 -> node:24 bump lands as a SEPARATE follow-up PR so the cache +# change carries zero runtime-version variables. +FROM node:22-alpine + +# Native toolchain required to compile node-gyp deps on musl, plus the +# postgresql-client used by the test step's pg_isready readiness probe. `bash` +# is baked here too — the sanitization step in ci.yml otherwise does a per-run +# `apk add bash`. +RUN apk add --no-cache python3 make g++ postgresql-client bash + +# Pin pnpm to the repo's packageManager version via corepack. +RUN corepack enable && corepack prepare pnpm@10.6.2 --activate + +WORKDIR /app + +# Pin the store location so the pipeline can point `store-dir` at the same path. +ENV PNPM_HOME=/root/.local/share/pnpm +RUN pnpm config set store-dir /root/.local/share/pnpm/store + +# Warm the store. `pnpm fetch` populates the content-addressable store with the +# dependency tarballs directly from the lockfile (no package.json / workspace +# needed), so a baked store stays valid until the lockfile changes. Note: +# `fetch` does NOT compile native modules — that happens later at `pnpm install` +# in the pipeline, against the toolchain baked above. +COPY pnpm-lock.yaml ./ +RUN pnpm fetch --frozen-lockfile