feat(ci): auto-link pushed image to its repo via the Forgejo package API
All checks were successful
CI / preflight (push) Successful in 4s
CI / typecheck (push) Successful in 13s
ecosystem-selftest / semantic-release-bumptest (push) Successful in 11s
ecosystem-selftest / eslint-gate (push) Successful in 4s
ecosystem-selftest / yamllint-gate (push) Successful in 3s
pulumi-preview / preview (push) Successful in 15s

Forgejo 11 does not auto-link container packages from org.opencontainers.image.source
(verified: label and manifest annotation both leave repo_id=0), so the reusable
workflow now POSTs to the package link API as ci-bot after push. Link persists across
future pushes. Also documents the Forgejo-11 reusable-workflow quirks surfaced this
session: secrets need 'secrets: inherit', and @master reusable refs are stale-parsed
(pin the SHA). Real repo-packages URL is /{owner}/-/packages?repo=<repo>.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Andreas Niemann 2026-07-01 14:24:14 +02:00
parent 2fe9a4d43e
commit 110a199495
2 changed files with 65 additions and 14 deletions

View file

@ -18,7 +18,7 @@ jobs:
## Forgejo 11 quirk (IMPORTANT)
Our forge runs **Forgejo 11.0.15**, where reusable-workflow support is the
**pre-v15 "limited" implementation**. Two rules differ from GitHub / Forgejo ≥ v15:
**pre-v15 "limited" implementation**. These rules differ from GitHub / Forgejo ≥ v15:
1. **The calling job MUST declare `runs-on`** (e.g. `runs-on: docker`). On standard
GitHub you omit `runs-on` on a `uses:` job — do that here and Forgejo **silently
@ -35,6 +35,21 @@ Our forge runs **Forgejo 11.0.15**, where reusable-workflow support is the
e.g. `with: { image: ..., push: false }` on `reusable-docker-build` — rather than
trusting the default.
4. **Secrets are NOT inherited by the called workflow — pass `secrets: inherit`.**
Org-level Actions secrets are visible to the CALLER but `${{ secrets.X }}` is
**empty inside the reusable workflow** unless the caller forwards them. Verified
live: `reusable-docker-build`'s `docker login` failed with `username is empty`
until the caller added `secrets: inherit` (which `runs-on`-jobs accept fine).
5. **`@master` reusable refs are stale-parsed — PIN to a commit SHA.** Forgejo 11
caches the reusable-workflow body: after pushing an updated
`reusable-docker-build.yml` to `foundation` master, callers at `...@master` kept
running the OLD body (no error — caller `with:`/`push` inputs updated, but the
steps didn't). Pin the caller to the commit SHA
(`...reusable-docker-build.yml@<sha>`) to force the new body; bump the SHA when the
reusable workflow changes. (A `foundation-forgejo` restart also clears the cache,
but SHA-pinning is deterministic and the better practice.)
Also pre-v15: the called workflow's logs collapse into a single "Set up job" entry
in the UI. **Forgejo v15.0** (LTS, Apr 2026) reworks this — omit `runs-on` and Forgejo
expands the reusable workflow into its inner jobs with separate logs. On a future v15
@ -70,21 +85,35 @@ proven by `ecosystem-selftest.yml` on the foundation's own runner.
### Pushing images to the forge container registry
`reusable-docker-build` with `push: true` pushes the built image to Forgejo's
built-in container registry and — when `source-url` is passed — links the package to
its repo's `/packages` page (via the `org.opencontainers.image.source` OCI label). It
authenticates as the org-scoped **`ci-bot`** user, reading org-level Actions secrets
`FORGE_REGISTRY_USER` / `FORGE_REGISTRY_TOKEN`. Those secrets + the `ci-bot` identity
are provisioned out-of-band as a Tier-1 step ([`ci-bot/`](../../ci-bot/), peer to
`runners/`), **not** in `bootstrap` — so CI never depends on Vault being unsealed.
A calling repo:
built-in container registry and links the package to its repo. It authenticates as
the org-scoped **`ci-bot`** user, reading org-level Actions secrets
`FORGE_REGISTRY_USER` / `FORGE_REGISTRY_TOKEN` (forwarded via `secrets: inherit`
see quirk 4). Those secrets + the `ci-bot` identity are provisioned out-of-band as a
Tier-1 step ([`ci-bot/`](../../ci-bot/), peer to `runners/`), **not** in `bootstrap`
— so CI never depends on Vault being unsealed. A calling repo:
```yaml
jobs:
image:
runs-on: docker
uses: olsitec/foundation/.forgejo/workflows/reusable-docker-build.yml@master
runs-on: docker # quirk 1
uses: olsitec/foundation/.forgejo/workflows/reusable-docker-build.yml@<sha> # quirk 5: PIN the SHA
secrets: inherit # quirk 4
with:
image: "forge.olsitec.net/olsitec/<repo>:ci"
source-url: "https://forge.olsitec.net/olsitec/<repo>"
push: true
```
Two Forgejo-11 container-registry facts the workflow already handles for you:
- **Single-manifest build.** The build runs `--provenance=false --sbom=false` so the
push is one image manifest, not an OCI index with attestations. An index both hides
the source label and litters the registry with junk `sha256:`-tagged versions.
- **Explicit repo-link (no auto-link from the label).** Forgejo 11 does **not**
auto-link a container package to a repo from `org.opencontainers.image.source`
(verified: label *and* manifest annotation both leave `repo_id=0`). The "Link
package to repo" step calls `POST /api/v1/packages/{owner}/container/{name}/-/link/{repo}`
as ci-bot. The link **persists** across future pushes (Forgejo leaves `repo_id`
untouched on version push). Linked packages appear at
**`/{owner}/-/packages?repo=<repo>`** — Forgejo has no `/{owner}/{repo}/packages`
route (that URL 404s regardless). A Forgejo **v15** upgrade may add auto-link — revisit then.

View file

@ -19,10 +19,11 @@
# PUSH + REPO-LINK: with push:true the image is pushed to Forgejo's built-in
# container registry (forge.olsitec.net/olsitec/<name>) authenticating as the
# org-level `ci-bot` (org Actions secrets FORGE_REGISTRY_USER/FORGE_REGISTRY_TOKEN,
# provisioned out-of-band as a Tier-1 step — see HANDOVER / provision/ci-bot).
# Passing `source-url` stamps the OCI label org.opencontainers.image.source so
# Forgejo links the package to that repo's /packages page (without it the package
# only shows on the org-level packages page).
# provisioned out-of-band as a Tier-1 step — see HANDOVER / ci-bot/). The image
# carries the OCI label org.opencontainers.image.source (from `source-url`), and —
# because Forgejo 11 does NOT auto-link container packages from that label — the
# "Link package to repo" step calls Forgejo's package link API so the package shows
# under the repo (/{owner}/-/packages?repo=<repo>), not just the org page.
name: reusable-docker-build
on:
workflow_call:
@ -90,3 +91,24 @@ jobs:
- name: Push
if: ${{ inputs.push }}
run: docker push "${{ inputs.image }}"
- name: Link package to repo
if: ${{ inputs.push && inputs.source-url != '' }}
# Forgejo 11 does not auto-link container packages from the OCI source
# label, so link explicitly via the package API (owner/repo parsed from
# source-url; package name from the image ref). Idempotent-ish: 201 = newly
# linked, 400 = already linked. Never fails the job — the push already
# succeeded; linking is secondary. The link persists across future pushes.
run: |
src="${{ inputs.source-url }}"; repo="${src##*/}"
rest="${src%/*}"; owner="${rest##*/}"
base="${{ inputs.image }}"; base="${base##*/}"; name="${base%%:*}"
code=$(curl -s -o /tmp/link.out -w '%{http_code}' -X POST \
-H "Authorization: token ${{ secrets.FORGE_REGISTRY_TOKEN }}" \
"https://forge.olsitec.net/api/v1/packages/$owner/container/$name/-/link/$repo")
echo "link $owner/container/$name -> $repo : http=$code"
case "$code" in
201) echo "linked." ;;
400) echo "already linked (400) — ok." ;;
*) echo "WARNING: unexpected link status $code: $(cat /tmp/link.out)" ;;
esac