Reusable Forgejo Actions workflows and the per-repo CI boilerplate every board ports onto (olsitec-foundation-T-0045, T-0052).
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Andreas Niemann 12434834b5
All checks were successful
ci / selftest (push) Successful in 14s
fix(semantic-release): checkout without persisted credentials so release pushes run as ci-bot
actions/checkout persists the Actions job token as http.extraheader, which
overrides the ci-bot credentials in the remote URL. Since
olsitec-foundation-T-0069 protected every default branch (13:00Z), Forgejo
pre-receive fails for the Actions user (uid -2): "Unable to GetUserByID ...
user does not exist", 500, "pre-receive hook declined" on
infra/pulumi_modules_kubernetes run 2845 (14:59Z, rerun failed the same).
ci-bot has write on that repo (permission API, 15:10Z).

Incident fix by olsitec-foundation-lead-073. Undo: git revert of this commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 15:11:18 +00:00
.forgejo/workflows fix(semantic-release): checkout without persisted credentials so release pushes run as ci-bot 2026-10-22 15:11:18 +00:00
ci fix(ci): docker build reusable passes every input to its scripts via env, never spliced 2026-10-21 14:52:11 +00:00
templates fix(ci): pass build-args to the Docker build step via env, never spliced 2026-10-19 09:20:34 +00:00
README.md ci: delete reusable-npm-publish-gitlab.yml, the GitLab npm transition bridge (T-0087) 2026-10-21 15:14:15 +00:00

foundation-ci/ci_workflows — the forge CI every Olsitec repo ports onto

Reusable Forgejo Actions workflows (on: workflow_call), plus copy-ready caller templates. They are the forge port of the GitLab include olsitec/gitlab/ci_templates (that repo stays on GitLab; the port was read at 1.20.7, fef904d).

  • Moved here from foundation/foundation/.forgejo/workflows/ by olsitec-foundation-T-0045, with history: 30 commits replayed with format-patch/am, and the moved blobs are byte-identical to foundation db7645e.
  • Changed here by olsitec-foundation-T-0052 (P-CI). The old copies in foundation/foundation are frozen and stay until a forge-wide census shows no caller left.
  • Porting a board's repos: read the kit at items:task-support/olsitec-foundation-T-0052/ (olsitec-foundation board) first.
uses: foundation-ci/ci_workflows/.forgejo/workflows/reusable-<name>.yml@master   # or @<sha> to freeze

Where this repo lives, and why it is public (olsitec-foundation-T-0055)

This repo is foundation-ci/ci_workflows. It moved here from foundation/ci_workflows on 2026-10-16, by a repo transfer, so the private-registry org foundation could become signed-in only. The foundation-ci org is public on purpose: forgejo-runner (v12.12.0, measured) fetches a cross-org uses: reusable workflow anonymously, so the owner of this repo must stay readable without a token. Keep packages and product code out of foundation-ci.

  • Callers write uses: foundation-ci/ci_workflows/.forgejo/workflows/<file>@<ref>.
  • The old path foundation/ci_workflows still resolves, through Forgejo's transfer redirect. That is a safety net, not an address: port every caller to the new path.
  • Never create a repo named foundation/ci_workflows again. A repo under the old name deletes the redirect, and every caller still on the old path fails at the uses: fetch.

Templates (copy one to .forgejo/workflows/ci.yml)

Template Ports the GitLab includes Jobs
templates/service-image.yml common + lint + container-build + semantic-release lint, test, plan (dry-run), image :<sha> stamped with the planned version, version, version-match, promote :X.Y.Z
templates/library-npm.yml common + lint + semantic-release + npm/bun publish lint, test, version, publish (from the release tag)
templates/lint-only.yml common + lint lint

release-version (service-image.yml)

plan runs semantic-release as a dry-run before image. Its next-version stamps the image (reusable-docker-build input release-version: package.json version rewritten, plus --build-arg RELEASE_VERSION), and version refuses before tagging if it would release anything else (reusable-semantic-release input expected-version). version-match re-checks after the release, and promote needs it. GitLab's kaniko template did the same stamping.

What the guard covers, and what it does not:

  • An empty expected-version means version is unguarded. The template passes plan's next-version, which is empty when plan computed no release. If history then moves between plan and version (a tag deleted, a branch reset) so that version does release, it tags and releases an image stamped with nothing; only version-match goes red, after the tag.

  • "Refuses before tagging" holds for every mismatch present at gate time. The gate's dry-run and the Release step's real semantic-release are two invocations seconds apart; a tag landing between them still yields a tag at the other version (then "Determine released version" fails and no Release object is created).

  • Pin at or after the commit that added release-version/expected-version (or @master). At an older sha both inputs are silently dropped: the image ships the Dockerfile's ARG RELEASE_VERSION default and the previous release's package.json version.

  • A red plan skips image. plan carries the tag-line gate; when it refuses, no :<sha> image is built either (before this change it was). Fix the tag line.

  • A caller that passes RELEASE_VERSION= in build-args owns the version: package.json and the build-arg are then left alone. On that path the image carries the caller's build-arg and the previous release's package.json version, so the two can differ; drop the line to have both stamped with plan's version.

Local GitLab jobs (tests, services, artifacts, schedules) are NOT in any template. You port them yourself, one job each, and list them in the needs: of the release path.

Building blocks

Workflow Does Key inputs → outputs
reusable-lint.yml eslint (pipeline eslint 10) + yamllint gate, advisory hadolint eslint, yamllint, hadolint, package-manager
reusable-node-build.yml strict install + build/test command package-manager, build, workdir
reusable-docker-build.yml install guard, docker build, push, registry read-back of the digest image, push, … → digest
reusable-semantic-release.yml dry-run + tag-line gate, then tag/push/Forgejo Release repo, branch, dry-run → version, next-version, last-version
reusable-image-promote.yml copies the tested digest to :X.Y.Z (+ :X.Y, :X, :latest while newest), no rebuild, never overwrites image, digest, version
reusable-npm-publish.yml strict install, build, npm publish to the forge registry, read-back package-manager, build, ref
reusable-helm.yml helm lint/package/publish: OCI push to oci://forge.olsitec.net/<org> by default (FORGE_REGISTRY_USER/TOKEN), or the org's http helm registry with registry: http (FORGE_PACKAGE_TOKEN); a present version is skipped, never overwritten charts-dir, publish, registry

What T-0052 changed

Each change is enforced by a test in ci/ that ci.yml runs on every push, green and red.

  1. Strict frozen install (olsitec-foundation-T-0019 decided: frozen only, fail closed).

    • The install runs bun install --frozen-lockfile or npm ci only, never || bun install.
    • A missing lockfile is red.
    • A lockfile that still resolves @olsitec/* from the GitLab npm registry is red, so it must be regenerated against the forge registry.
    • ci/test-strict-install.py checks that the block is identical in every reusable and runs it against stub package managers.
  2. One npm registry for every org: https://forge.olsitec.net/api/packages/foundation/npm/ (@olsitec:registry, FORGE_PACKAGE_TOKEN).

  3. Olsitec releaseRules, as on GitLab:

    • ci, doc, docs, chore, build, fix, perf, refactor, style and test give a patch release;
    • feat and feature give a minor release;
    • breaking changes give a major release.
    • The forge copy lacked doc; ci/test-tag-line-gate.py now proves ci and doc both give a patch release, using the real semantic-release.
  4. Tag-line gate. Every release first runs semantic-release --dry-run and refuses when:

    • the next version is not strictly greater than the highest X.Y.Z/vX.Y.Z tag on any branch, or
    • the next version already exists as a tag.

    This catches a migrated line that semantic-release cannot see from the branch. dry-run: true stops after the gate, so publishing stays fenced.

  5. The tested image is the released image.

    • The build pushes :<sha> and outputs the registry digest.
    • reusable-image-promote points :X.Y.Z at that digest. This is GitLab parity (olsicloud4-T-102): the old forge pattern rebuilt the image for the release tag.
    • The promote copies the exact manifest bytes through the registry API. docker buildx imagetools create must not be used here: it wraps a single manifest in a new OCI index with a different digest. That was measured on services_forge-notifier run 6, where the read-back caught it. ci/test-image-promote.py covers this case.
    • Neither the Dockerfile's bun install without --frozen-lockfile nor a GitLab-resolving lock in the build context can get past the docker build.

Forge facts the callers depend on

These are kept from the foundation README; see its history for the measurements.

  • A uses: job needs runs-on:, and secrets: inherit is required; without it ${{ secrets.X }} is empty inside the called workflow.
  • Cross-repo references use the short form owner/repo/.forgejo/workflows/x.yml@ref.
  • A caller's runs-on does not move a uses: job's placement; the reusables run on docker.
  • Filter on: push to the release branch, because the release tag push is itself a push.
  • Runner v12.12.0 fetches @master on each run. A SHA pin freezes behaviour.
  • A nested reusable (a uses: inside a workflow_call) is forbidden, because it ran hollow green on runner v6.4.0. ci/test-no-nested-reusable.py enforces this.
  • An org gets the ci-bot team and the FORGE_* org secrets only when it is ciEnabled in foundation orgs/index.ts.