No description
Find a file
2026-06-19 17:01:43 +02:00
.forgejo/workflows ci: release app build workflow v1.2.0 2026-06-19 17:01:43 +02:00
README.md ci: release app build workflow v1.2.0 2026-06-19 17:01:43 +02:00

platform/ci

Reusable Forgejo Actions workflows for the alting platform.

The source of truth for this repository's contents lives in the alting monorepo at platform/ci-repo/. Don't edit files directly here — edit in the monorepo, then push + tag (procedure below).

Repositories using this workflow

  • git.delai.dk/platform/meal-tracker — pinned to @v1.0.1 (later tags are no-ops for this app — its Containerfile doesn't declare ARG BUILD_SHA)
  • git.delai.dk/platform/heartbeat — pinned to @v1.0.3
  • git.delai.dk/platform/tyr-cms — pinned to @v1.1.0, the first tag with runner + migrator image publication support
  • git.delai.dk/platform/tyr-web — planned pin @v1.2.0, the first tag with runner publication support for NEXT_PUBLIC_CMS_URL + SITE_URL

The app-build workflow

.forgejo/workflows/app-build.yaml is a reusable workflow that handles checkout, podman build, and registry push for any alting platform app that follows the conventional container contract.

Calling it from an app

# .forgejo/workflows/build.yaml in your app repo
name: build
on:
  push:
    branches: [main]
jobs:
  build:
    uses: platform/ci/.forgejo/workflows/app-build.yaml@v1.0.3
    secrets: inherit

secrets: inherit is required so the reusable workflow can see the caller's REGISTRY_TOKEN actions secret. Alternatively:

    secrets:
      REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}

Inputs

All optional:

Input Default Purpose
containerfile Containerfile Path to the Containerfile relative to repo root
build-context . Build context dir relative to repo root
build-target unset Optional Containerfile target for the runtime image
migration-target unset Optional Containerfile target pushed as :migrator-<sha-short>
next-public-site-url unset Optional NEXT_PUBLIC_SITE_URL build arg for Next.js apps
next-public-cms-url unset Optional NEXT_PUBLIC_CMS_URL build arg for Next.js apps
site-url unset Optional SITE_URL build arg for Next.js apps
image-name derived Override image name. Default: git.delai.dk/${forgejo.repository}

For an app with separate runtime and migration stages, opt in explicitly:

jobs:
  build:
    uses: platform/ci/.forgejo/workflows/app-build.yaml@vNEXT
    with:
      build-target: runner
      migration-target: migrator
      next-public-site-url: https://admin.example.com
    secrets: inherit

That produces repo:<sha-short>, repo:latest, and repo:migrator-<sha-short>. mad migrate <app> <sha-short> and mad deploy with auto-migrate enabled use that same migrator- tag convention.

For a runner-only Next.js public site that bakes both a browser-facing CMS origin and its own public origin:

jobs:
  build:
    uses: platform/ci/.forgejo/workflows/app-build.yaml@vNEXT
    with:
      build-target: runner
      next-public-cms-url: https://admin.example.com
      site-url: https://www.example.com
    secrets: inherit

That produces repo:<sha-short> and repo:latest only; no migration image is created unless migration-target is set.

Outputs

Output Meaning
image The image (no tag) that was built and pushed
sha-short 12-char SHA used as the immutable image tag
migration-image Full repo:migrator-<sha-short> ref, or empty when migration-target is unset

Prerequisites in the app repo

  • Containerfile (or path passed via containerfile: input).
  • REGISTRY_TOKEN Actions secret — a Forgejo PAT with scope write:package. Create at Settings → Applications → Generate new token, then add at the app repo's Settings → Actions → Secrets.

Security boundary

Every CI job builds via the forgejo-runner's forwarded podman socket, which gives the job effective root on the box. This workflow is therefore load-bearing: a compromise of platform/ci@vX.Y.Z ≈ root on every app that pins it.

Mitigations:

  • Pinned immutable tags. Callers pin a semver tag (e.g. @v1.0.0). Tags are never moved or reassigned. Bumping a caller is a deliberate edit in the app repo, not an auto-pickup.
  • Write access is restricted to the Forgejo admin (platform) user. Do not add collaborators with push access to this repo without first reviewing the trust implications across every consuming app.
  • No catalog dependencies. The workflow performs git clone directly against the in-cluster forgejo:3000 rather than using actions/checkout or any other catalog action. This preserves the invariant that nothing external reaches the runner.
  • Public visibility is required by Forgejo. Forgejo 15.x reusable workflows can only be resolved from public repositories; the per-job token does not have cross-repo read access. The repository being public exposes the workflow YAML — review changes accordingly.

Cutting a release

The release flow is operator-driven and deliberately manual. There is no CI on platform/ci itself.

  1. Edit files in the alting monorepo under platform/ci-repo/. Commit on a branch, open a PR, get a review, merge.
  2. After the monorepo PR is merged to main, clone platform/ci from git.delai.dk to a scratch dir, rsync the new platform/ci-repo/ contents into it (excluding .git/), commit, push main.
  3. Cut a new immutable tag (vN.M.K). Tags are never moved; bumping a major or minor is a fresh tag.
  4. Update each consuming app's caller workflow to pin the new tag. The "Repositories using this workflow" list above is the registry.

The release flow does NOT run automatically when monorepo changes land on main — controlled rollout is the explicit choice (see PR introducing F3 for rationale).

Known quirks

podman 4.3.1 --remote push exit-125 false-negative

The base image (docker.io/library/node:24-bookworm-slim) ships podman 4.3.1, which has a bug where podman --remote push prints failed to parse push results stream, unexpected input: { } and exits 125 even when the push succeeds. The workflow's push step treats exit 125 as informational and verifies the manifest via a registry HEAD request before considering the push successful.

This was discovered when migrating meal-tracker to the reusable workflow at v1.0.0 — the bug existed in the original meal-tracker workflow too, manifesting as the :latest tag never being pushed (the loop aborted after the first push exited 125). v1.0.1 fixes it.

If/when the base image's podman is updated to ≥4.5, the workaround can be simplified but should remain defensive.

podman 4.3.1 --build-arg doesn't invalidate layer cache

podman 4.3.1's layer cache key is based on instruction text only — the resolved value of ${VAR} substitutions is not included. So ENV X=${X} will silently reuse a cached layer with the previous build's value of X even when --build-arg X=newvalue is passed.

This was discovered when shipping v1.0.2: the heartbeat reported "build_sha": "unknown" despite --build-arg BUILD_SHA=<sha> being passed, because the runtime stage's ARG BUILD_SHA=unknown / ENV BUILD_SHA=${BUILD_SHA} layers cache-hit from the prior build.

v1.0.3 adds --no-cache to podman build. Costs ~40s extra per build; guaranteed correctness. When the base image moves to podman ≥4.5, the cache key correctly includes resolved ARG values and --no-cache can be dropped.

Changelog

  • v1.2.0 — Add optional next-public-cms-url and site-url inputs, passed as NEXT_PUBLIC_CMS_URL and SITE_URL build args. This is the first tag suitable for tyr-web runner publication, while existing callers are unaffected.
  • v1.1.0 — Add optional build-target, migration-target, and next-public-site-url inputs. Existing callers are unaffected; apps that opt into migration-target get a matching :migrator-<sha-short> image for mad migrate / auto-migrate deployments. Next.js apps that inline their public origin can pass next-public-site-url so the runtime and migrator images are built with the same public URL they receive at runtime.
  • v1.0.3 — Add --no-cache to podman build to work around podman 4.3.1's layer-cache-key bug that ignored --build-arg values, so BUILD_SHA (and any future build-arg) actually reaches the image. Build time ~+40s per app.
  • v1.0.2 — Pass --build-arg BUILD_SHA=${SHA_SHORT} to podman build so apps that declare ARG BUILD_SHA (e.g. heartbeat) can bake the commit SHA into the image and surface it at runtime. Apps that don't declare the arg are unaffected. Note: ineffective due to the podman cache-key bug fixed in v1.0.3; bumping to v1.0.3 directly is preferred.
  • v1.0.1 — Add registry HEAD-verify fallback for the podman 4.3.1 push exit-125 false-negative (see Known quirks).
  • v1.0.0 — Initial release; reusable build/push workflow extracted from meal-tracker's per-app workflow.

Trivia

The probe.yaml file was removed in v1.0.0; it was a temporary probe used to validate Forgejo Actions reusable-workflow semantics before committing to the design. See the alting monorepo commit history for the probe results and the rationale they produced.