1 06 The CI CD Pipeline
Eric the IT Guy edited this page 2026-08-12 12:49:28 -05:00

Part 6: The CI/CD Pipeline

This is the payoff of the whole build: I commit a Containerfile change, and the runner builds, lints, and pushes new images. The workflow lives at .forgejo/workflows/build.yml.

The shape

Two jobs. The base job builds itg-base first. The children job needs: base and fans out over a matrix, one entry per host image, all building FROM the freshly-pushed base.

name: build-images
on:
  push:
    branches: [main]
  schedule:
    - cron: '0 7 * * *'   # ~2 AM Central
jobs:
  base:
    runs-on: native
    steps:
      - uses: actions/checkout@v4
      - run: podman login forge.itguyeric.com -u itguyeric -p "${{ secrets.FORGE_TOKEN }}"
      - run: podman build -t forge.itguyeric.com/itguyeric/itg-base:latest .
      - run: podman run --rm forge.itguyeric.com/itguyeric/itg-base:latest bootc container lint
      - run: podman push forge.itguyeric.com/itguyeric/itg-base:latest
  children:
    runs-on: native
    needs: base
    strategy:
      matrix:
        include:
          - { dir: bootc-dev, image: itg-dev-bootc }
          - { dir: book,      image: itg-prd-book }
          - { dir: forge,     image: itg-prd-forge }
          - { dir: run,       image: itg-prd-run }
    steps:
      - uses: actions/checkout@v4
      - run: podman login forge.itguyeric.com -u itguyeric -p "${{ secrets.FORGE_TOKEN }}"
      - run: podman build --pull=newer -t forge.itguyeric.com/itguyeric/${{ matrix.image }}:latest ./${{ matrix.dir }}
      - run: podman run --rm forge.itguyeric.com/itguyeric/${{ matrix.image }}:latest bootc container lint
      - run: podman push forge.itguyeric.com/itguyeric/${{ matrix.image }}:latest

The pieces that matter

  • needs: base is the dependency that makes the children rebuild on top of the new base, and --pull=newer on the child builds guarantees they actually grab the just-pushed base rather than a cached layer.
  • Lint gating. Every image runs bootc container lint before it is allowed to push. A broken image never reaches the registry.
  • The base lint bug I hit: I originally wrote the base's lint step using ${{ matrix.image }}, but the base job has no matrix, so it expanded to an empty string and produced forge.itguyeric.com/itguyeric/:latest — an invalid reference. Hardcode itg-base in the base job.
  • FORGE_TOKEN is a Forgejo Actions secret. Important caveat I keep in mind: Actions secrets are for CI, they are not a general password vault. A real secrets vault is a separate, later project.
  • runs-on: native targets my bare-metal runner label rather than a container-in-container executor, which matters because these jobs build images with podman.

A note on renaming a base image

When I renamed the base from fedora-server to itg-base, every child Containerfile's FROM and every workflow reference had to change together. On the Mac I swept it with grep -rl <old> | xargs perl -pi -e 's/old/new/g'. Watch out: BSD grep -Z on macOS does not emit null separators the way GNU does, so -Z | xargs -0 treats the whole file list as one filename. Drop the -Z/-0 and use plain grep -rl | xargs perl -pi.

Lessons

  • needs: plus --pull=newer is what makes "rebuild children on top of the new base" actually true.
  • Lint before push, always.
  • A no-matrix job cannot use matrix.*; it silently becomes empty.
  • CI secrets are not a vault.