No description
  • Dockerfile 76.2%
  • Makefile 23.8%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Mirigan 7bc63892d5
All checks were successful
Build & publish the CI image / build (push) Successful in 8s
fix(ci): authenticate the registry push with a PAT, not the job token
Forgejo's per-job token authenticates ("Login Succeeded") but is not
allowed to write packages: the push died on 401 at the blob upload
(forgejo#1296, #7352). Use REGISTRY_USER / REGISTRY_TOKEN repo secrets
instead; the comment records why so nobody retries the job token.

The rest of the first run was green — checkout, docker daemon reachable
from the job, and the full image build.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 10:56:53 +02:00
.forgejo/workflows fix(ci): authenticate the registry push with a PAT, not the job token 2026-08-18 10:56:53 +02:00
Dockerfile feat: CI image sources + build-on-push workflow (infra-00005) 2026-08-18 10:44:55 +02:00
Makefile feat: CI image sources + build-on-push workflow (infra-00005) 2026-08-18 10:44:55 +02:00
README.md feat: CI image sources + build-on-push workflow (infra-00005) 2026-08-18 10:44:55 +02:00

CI image — git.botyglot.tech/botyglot-public/ci-ruby

Custom, slim CI image for the self-hosted Semaphore CI (oscar-00016). Purpose: job pods run with zero runtime installs — the Semaphore toolbox, Erlang, a pinned Node and Chrome are all baked in.

What's inside

  • Ruby 3.4.9 (base ruby:3.4.9-slim-bookworm)
  • Node 22.12.0 (pinned — NODE_VERSION build arg) + yarn 1.22.x
  • Google Chrome stable (capybara/selenium system specs; chromedriver is fetched at runtime by Selenium Manager, which needs network)
  • Semaphore self-hosted toolbox (checkout / cache / artifact / test-results / when / sem-context / retry / spc) in /usr/local/bin
  • Erlang (erl) so the toolbox when CLI installs/runs
  • build-essential, pkg-config, libpq-dev, libffi-dev (fiddle, via sidekiq-ent→einhorn on forge), postgresql-client, git, git-lfs (repos use LFS, e.g. db/specifications), unzip (semaphore/forge/checkout.sh unpacks db/specifications/current.zip), openssh-client, ffmpeg, sudo
  • non-root user semaphore (uid 1000, passwordless sudo), home /home/semaphore — named like on Semaphore's hosted VMs so hosted-era secrets that inject files to absolute /home/semaphore/... paths keep working in job pods
  • GitHub host keys baked into /etc/ssh/ssh_known_hosts (hosted VMs pre-seed them; without this, SSH clones hang on the host-key prompt in a bare container)

Build & push

The image must be linux/amd64 (Hetzner CPX nodes).

Automatic — Forgejo Actions (.forgejo/workflows/build.yml): every push to main touching the Dockerfile, the Makefile or the workflow rebuilds the image on our own runner and publishes it. Authentication uses the per-job token Forgejo injects — no PAT to create or rotate.

Manual fallback (from a workstation; Apple Silicon uses buildx emulation):

docker login git.botyglot.tech -u <user>     # PAT with package write
make release

⚠️ Bump BUILD in the Makefile on every image change. Pinned tags are pulled IfNotPresent on the agent node, so republishing an existing tag silently serves the old image.

No Docker on the agent k3s node (it uses containerd), so the image is built by CI/a workstation, not on the cluster.

Use it in a pipeline

In the project's .semaphore/semaphore.yml:

agent:
  machine:
    type: s1-botykube
  containers:
    - name: main
      image: git.botyglot.tech/botyglot-public/ci-ruby:3.4.9-5
    - name: db
      image: pgvector/pgvector:pg17
      env_vars: [{ name: POSTGRES_HOST_AUTH_METHOD, value: trust }]
    - name: redis
      image: redis:7

The package owner Botyglot-public is a public organisation, so the image is pullable anonymously: no image-pull secret to configure on the cluster, and nothing to rotate.

Scope — what belongs in here

Only what every job needs. A tool used by a single block stays a per-run install in that block's commands:.

Baking a per-block tool would pin its version to the image's rebuild cadence, and for linters that's backwards: brakeman and bundler-audit are wanted at their latest on every run — that's the point of running them. They were briefly baked (f263fca) and reverted for exactly this reason. Same test for anything else: if one block needs it, it does not go in the image.

What it lets us drop (once in use)

  • botyglot-devops initial_setup.sh (container branch): the runtime toolbox + erlang install.
  • semaphore.yml after_pipeline: the toolbox-install prologue.
  • Node-version drift (cimg shipped Node 24; here it's pinned to 22.12.0).

Known gaps (deliberate, see ticket oscar-00016)

  • Fonts: only dejavu + liberation (22 faces) vs. a much richer set in cimg/ruby:*-browsers → the first Percy run will show baseline diffs. Decide (add fonts-noto-core/fonts-noto-color-emoji, or re-baseline as-is) before that run; later font changes invalidate baselines again.
  • postgresql-client is 15 (bookworm) while the DB sidecar is PG 17. psql is fine cross-version; pg_dump/pg_restore refuse to target a newer server. Not on the critical path today (schema_format is :ruby, apartment use_sql = false).
  • No libvips/ImageMagick — image_processing is a dependency of sentinel (~1.2) and chady-erp (~2.0), and Rails 7+ defaults to the :vips variant processor. Not exercised by sentinel (no attachments); add libvips42 when migrating chady.
  • puppeteer-ruby (chady) downloads its own Chromium unless pointed at the baked one via PUPPETEER_EXECUTABLE_PATH=/usr/bin/google-chrome.
  • Do NOT add sem-service or sem-version: initial_setup.sh detects container mode with command -v sem-service, so shipping either flips the detection and re-enables the VM-only code paths.

Maintenance

  • Bump Ruby → change the FROM tag + RUBY_VERSION. Bump Node → NODE_VERSION.
  • Keep in sync with each project's RUBY_VERSION / NODE_VERSION env.
  • Chrome is stable at build time; rebuild periodically for security updates.
  • ⚠️ Republishing the same tag does NOT reach the cluster. Pinned tags are pulled IfNotPresent, so the agent node keeps serving its cached copy — jobs silently run the old image. Either bump the tag (:3.4.9-2), or purge it on the node:
    sudo k3s crictl rmi git.botyglot.tech/botyglot-public/ci-ruby:3.4.9-5
    
    (:latest is pulled Always — handy while iterating on the image, repin afterwards.)