| .forgejo/workflows | ||
| README.md | ||
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 declareARG BUILD_SHA)git.delai.dk/platform/heartbeat— pinned to@v1.0.3git.delai.dk/platform/tyr-cms— pinned to@v1.1.0, the first tag with runner + migrator image publication supportgit.delai.dk/platform/tyr-web— planned pin@v1.2.0, the first tag with runner publication support forNEXT_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 viacontainerfile:input).REGISTRY_TOKENActions secret — a Forgejo PAT with scopewrite: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 clonedirectly against the in-clusterforgejo:3000rather than usingactions/checkoutor 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.
- Edit files in the alting monorepo under
platform/ci-repo/. Commit on a branch, open a PR, get a review, merge. - After the monorepo PR is merged to
main, cloneplatform/cifromgit.delai.dkto a scratch dir,rsyncthe newplatform/ci-repo/contents into it (excluding.git/), commit, pushmain. - Cut a new immutable tag (
vN.M.K). Tags are never moved; bumping a major or minor is a fresh tag. - 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-urlandsite-urlinputs, passed asNEXT_PUBLIC_CMS_URLandSITE_URLbuild args. This is the first tag suitable fortyr-webrunner publication, while existing callers are unaffected. - v1.1.0 — Add optional
build-target,migration-target, andnext-public-site-urlinputs. Existing callers are unaffected; apps that opt intomigration-targetget a matching:migrator-<sha-short>image formad migrate/ auto-migrate deployments. Next.js apps that inline their public origin can passnext-public-site-urlso the runtime and migrator images are built with the same public URL they receive at runtime. - v1.0.3 — Add
--no-cachetopodman buildto work around podman 4.3.1's layer-cache-key bug that ignored--build-argvalues, soBUILD_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}topodman buildso apps that declareARG 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
pushexit-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.