Skip to content

Instantly share code, notes, and snippets.

@vjt
Last active April 20, 2026 20:55
Show Gist options
  • Select an option

  • Save vjt/3611f940bd44719598be9e9e1927bc25 to your computer and use it in GitHub Desktop.

Select an option

Save vjt/3611f940bd44719598be9e9e1927bc25 to your computer and use it in GitHub Desktop.
Plan-delta: GHCR push for bahamut+services CI, compose consumes published images

Plan-delta: publish bahamut + services images to GHCR, compose consumes them

Context: Plan B (https://gist.github.com/vjt/48a75dc067f27108e4261d4c7380e00d) had each testnet container build: from source on first docker compose up (peak RAM ~200–300 MB for autoconf+gcc, too much for a 256 MB box). Hypnotize picked GHCR as hosting; this delta wires the push into the two existing CIs and flips the compose to image:.

Scope: minimal. One new step per CI workflow, compose switches to published images, first up becomes a pull instead of a compile.


Non-goals (follow-ups, not this patch)

  • Multi-stage Dockerfile slimming. Current images are builder-shaped (~500 MB each, include gcc/autoconf). Works, just fat. Split into builder + runtime-slim is a separate PR.
  • Semver tag strategy. master + sha-<short> is enough for now; vX.Y.Z tags get added when upstream starts cutting releases.
  • Image signing / cosign / SBOM.
  • Renovate / Dependabot pins on the compose side.

Tag strategy

Trigger Tag(s) pushed
push to master master, sha-<7char>
push to any git tag refs/tags/* <tag-verbatim>, latest
push to any other branch none (build-only, load locally for smoke-check)
pull_request none (build-only, load locally for smoke-check)

Tag nomenclature note (Hypnotize 2026-04-20 20:54 + Sonic is master of tags): existing upstream tags differ per repo — azzurra/bahamut uses bare versions (4.8, 4.7-beta5, no v prefix), azzurra/services is mixed (services-2.2.2 historic, v2.5.1 recent). Workflow passes the git-tag name verbatim into the image tag (no reformatting, no v-prefix enforcement), so whatever Sonic cuts upstream becomes the image tag as-is. latest only advances when a tag is cut (not on master push).

Compose defaults to :master. PR-against-PR testing pins BAHAMUT_TAG=sha-abcdef1 in .env (or SERVICES_TAG=…).

Permissions

Both repos' docker job gets:

permissions:
  contents: read
  packages: write

Uses ${{ secrets.GITHUB_TOKEN }} — no PAT, no org secret, no new moving parts. Org azzurra must allow workflows to create/write packages (default on for public repos).

Image visibility: public. First push creates the package; a one-time post-push step in the org's package settings flips it public (no CLI way to set-on-creation short of the image-description action; simpler to do it once manually).

azzurra/bahamut — .github/workflows/build.yml delta

Replace the current docker job:

docker:
  name: docker build
  runs-on: ubuntu-latest
  permissions:
    contents: read
    packages: write
  steps:
    - uses: actions/checkout@v4

    - uses: docker/setup-buildx-action@v3

    - name: compute tags
      id: tags
      run: |
        SHORT=$(echo "${{ github.sha }}" | cut -c1-7)
        if [ "${{ github.event_name }}" = "push" ] && [ "${{ github.ref }}" = "refs/heads/master" ]; then
          echo "push=true"                                                          >> $GITHUB_OUTPUT
          echo "tags=ghcr.io/azzurra/bahamut:master,ghcr.io/azzurra/bahamut:sha-$SHORT" >> $GITHUB_OUTPUT
        elif [ "${{ github.event_name }}" = "push" ] && [[ "${{ github.ref }}" == refs/tags/* ]]; then
          VER="${GITHUB_REF#refs/tags/}"
          echo "push=true"                                                          >> $GITHUB_OUTPUT
          echo "tags=ghcr.io/azzurra/bahamut:$VER,ghcr.io/azzurra/bahamut:latest"    >> $GITHUB_OUTPUT
        else
          echo "push=false"                                                         >> $GITHUB_OUTPUT
          echo "tags=ghcr.io/azzurra/bahamut:ci"                                    >> $GITHUB_OUTPUT
        fi

    - name: login to ghcr
      if: steps.tags.outputs.push == 'true'
      uses: docker/login-action@v3
      with:
        registry: ghcr.io
        username: ${{ github.actor }}
        password: ${{ secrets.GITHUB_TOKEN }}

    - name: build (+ push on master/tag)
      uses: docker/build-push-action@v6
      with:
        context: .
        push: ${{ steps.tags.outputs.push == 'true' }}
        load: ${{ steps.tags.outputs.push != 'true' }}
        tags: ${{ steps.tags.outputs.tags }}

    - name: smoke-check (non-push builds only)
      if: steps.tags.outputs.push != 'true'
      run: docker run --rm --entrypoint sh ghcr.io/azzurra/bahamut:ci -c 'test -x /build/src/ircd'

Existing build job (matrix leaf/hub) stays untouched.

azzurra/services — same shape

Identical structure, replace bahamut with services, binary path /build/src/services.

testnet-compose — compose.yaml diff

Drop the build anchor, switch each service to image::

services:
  hub:
    image: ghcr.io/azzurra/bahamut:${BAHAMUT_TAG:-master}
    # ...rest unchanged

  leaf-v4:
    image: ghcr.io/azzurra/bahamut:${BAHAMUT_TAG:-master}
    # ...

  leaf-v6:
    image: ghcr.io/azzurra/bahamut:${BAHAMUT_TAG:-master}
    # ...

  services:
    image: ghcr.io/azzurra/services:${SERVICES_TAG:-master}
    # ...

Open question for reviewers: does the testnet-compose wrap the GHCR image, or is the published image self-sufficient?

  • Option A (recommended): GHCR image stays builder-shaped (just produces /build/src/ircd). testnet-compose's bahamut/Dockerfile is FROM ghcr.io/azzurra/bahamut:master + COPY entrypoint.sh conf.*.tmpl + ENTRYPOINT. Keeps upstream image concern-free, testnet owns its own runtime wiring. First up pulls GHCR (~500 MB once, cached forever) + builds 4 trivial wrapper layers (seconds).
  • Option B: Push the entrypoint + conf templates into azzurra/bahamut itself so the GHCR image is directly runnable. Conflates dev/testnet concerns with the build image, but simpler compose.

My vote: A. Keeps "what's in the upstream Dockerfile" tight.

Rollout order

  1. Open PR on azzurra/bahamut (CI delta). Merge → first push creates ghcr.io/azzurra/bahamut package. Set to public in org settings.
  2. Open PR on azzurra/services (CI delta). Merge → same for services package.
  3. Update Plan B gist in-place with the new compose snippet.
  4. testnet-compose PR (separate, later) actually consumes the images.

Steps 1+2 are independent and land in whatever order. Step 3 is cosmetic (gist only). Step 4 can wait until the Plan B review closes.

Review asks

  • permissions: packages: write on the docker job only — fine?
  • Tag strategy: master + sha-<short> + <git-tag-verbatim> + latest (pass-through, no prefix enforcement). Sonic to confirm tag-name policy. Anyone want develop too?
  • Option A vs Option B on testnet-compose wrapping.
  • Public visibility confirmed OK (no private registry ceremony).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment