No description
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Damián Búho 8fd770ef08
fix(container-build): heal B19_HOME dir posture lost to buildah --layers diff bug
Twin of the m6e buildah-build heal: a RUN with a cache mount under a
user-owned directory that only copy-ups a pre-existing file there makes
buildah (≤1.42) omit the parent dir entry from the committed layer; the
orphaned child materializes the parent as root:755 in the merged image.
Heal offending dirs back to the declared posture and hand the healed
image ID to the tar export. M6E_BUILDAH_HEAL=N opts out.
2026-07-27 00:16:57 -03:00
container-build fix(container-build): heal B19_HOME dir posture lost to buildah --layers diff bug 2026-07-27 00:16:57 -03:00
container-exec chore: init 2026-07-14 20:13:47 -03:00
docker-cleanup feat(docker-cleanup): add action to reap run-scoped image and network 2026-07-26 20:45:45 -03:00
forgejo-release chore: init 2026-07-14 20:13:47 -03:00
LICENSES fix(run-tool): route quoted run strings through sh -c 2026-07-18 23:45:27 -03:00
oci-push feat(buildah): compress image artifact and cache build layers 2026-07-26 22:24:39 -03:00
run-tool fix(run-tool): route quoted run strings through sh -c 2026-07-18 23:45:27 -03:00
secrets-provision chore: init 2026-07-14 20:13:47 -03:00
CONTRIBUTING.md chore: init 2026-07-14 20:13:47 -03:00
LICENSE fix(run-tool): route quoted run strings through sh -c 2026-07-18 23:45:27 -03:00
projectfile.yaml chore: init 2026-07-14 20:13:47 -03:00
README.md chore: init 2026-07-14 20:13:47 -03:00

projectfile/ci-actions

The external provider library for ci-resolver. One composite action per provider name, pinned and consumed as projectfile/ci-actions/<provider>@v1.

Why this repo exists (Ports & Adapters)

ci-resolver lowers an org.projectfile.ci signal DAG to vendor CI workflows. A provider leaf (e.g. container-build) is not a portable "run a tool in a box" step — it is realised with the platform's own native mechanism. Compiling those recipes into the resolver as Go template partials coupled the vendor-neutral resolver to one target's mechanics and made the recipes un-reviewable as a unit.

So the resolver keeps the dispatch (provider name + DAG ordering) and this library keeps the recipe. A provider leaf lowers to:

- uses: projectfile/ci-actions/container-build/buildx@v1
  with:
    artifact-name: image-${{ matrix.B19_UBUNTU_SERIES }}
    build-args: |
      B19_UBUNTU_SERIES=${{ matrix.B19_UBUNTU_SERIES }}

Adding a provider becomes "publish an action here," not "edit + recompile the resolver."

Build backends (container-build)

container-build ships two drop-in analogues that emit the identical OCI-tar hand-off, so everything downstream is backend-agnostic:

Backend Mechanism Use
container-build/buildx docker buildx (daemon) GHA/Forgejo default (runner-native)
container-build/buildah buildah (daemonless) engines without a daemon (Tekton), rootless

The backend is a HOW-it-runs detail, not part of the gate's meaning, so ci-resolver selects it per target (a Target adapter default) overridable per project via org.projectfile.ci.<target>.builder: buildx|buildah — never in the neutral DAG. Both write <artifact-name>.tar as an OCI archive.

container-build/
├── build-args.sh        shared: newline build-args → --build-arg array (sourced)
├── buildx/{action.yml, build.sh}    docker buildx backend
└── buildah/{action.yml, build.sh}   daemonless buildah backend

Each action's shell lives in a build.sh (not inlined in YAML, so shellcheck lints it directly); the identical arg-parsing is sourced from build-args.sh via $GITHUB_ACTION_PATH/../build-args.sh.

There is no live action (dissolved into run-steps)

live brings the just-built image to LIFE and exercises it, but it is not an action in this library — the fuse model dissolved it. The DAG marks dc-up-d + container-test with fuse: live; because a compose stack cannot span jobs on an ephemeral runner, ci-resolver COLLAPSES those leaves into ONE job and renders each member's own run: command as an ordered step (render.gosteps/fused):

# generated by ci-resolver — no `uses:` here
- run: docker load --input image-${{ matrix.B19_UBUNTU_SERIES }}.tar
- run: docker compose up -d --wait
- run: docker compose exec -T app test.d

The recipe is therefore transparent in the generated workflow, not hidden behind a pinned action. Two consequences worth noting:

  • The OCI-load seam stays: container-build stamps the resolved ref into a type=oci archive (kept OCI so the daemonless image-scan can read the same tar), so the fused job's docker load restores it TAGGED and compose's pull_policy: never finds it — no skopeo, no anonymous image.
  • Daemon-only, daemonless-agnostic: the fused run-steps need a daemon (buildah has no compose), but they are plain shell — no backend split, nothing to mirror to a forge as an action. m6e runs the SAME leaves as plain make recipes (its dev daemon persists between jobs, so no fusion is needed there).

Forge portability

uses: is not forge-portable: a GitHub Actions runner resolves projectfile/ci-actions/<name>@v1 against github.com, a Forgejo runner against its own instance. This library is therefore mirrored to each forge the generated workflows run on, so the same bare ref resolves on both. The ref is a per-target adapter token in ci-resolver (Target.ProviderLib / ProviderVer), so a non-mirrored target can override it with a full-URL ref later.

Pinning

Consumers pin @v1 (a moving major tag the library keeps backward-compatible within a major). This matches the workspace pinned-dependency rule.

Actions

Provider Status Realisation
container-build/buildx landed docker/setup-buildx-action → OCI-tar → cell artifact
container-build/buildah landed buildah build → oci-archive: tar → cell artifact
oci-push landed docker load cell tar → login → (re-tag) → docker push
image-scan planned scanner against oci-archive:<artifact>.tar (daemonless)
secrets-provision landed org.projectfile.ci.secrets declarations → .secrets/ tree (value-write / docker-run dispatcher)

live is intentionally absent — it is rendered as fused run-steps by ci-resolver, not dispatched here (see "There is no live action").

secrets-provision is the cloud half of the org.projectfile.ci.secrets provisioning contract (m6e/secrets-system.md): ci-resolver serializes the secrets: subtree as JSON into declarations:, and this action materialises the .secrets/<dotted-name> tree a compose secrets: block mounts BEFORE dc-up-d. The dispatch is a thin loop — value: writes a literal verbatim, docker: runs docker run <resolved-image> <run> and captures stdout — so a new secret kind is a recipe in the d9t/misc-tools image (m6e-secret-*, where openssl/htpasswd live) plus a docker: { run: ... } declaration, with no ci-actions or ci-resolver redeploy. No outputs: secrets are FILES written into the workspace (the intra-job file hand-off). The m6e half reads the SAME declarations and calls the same dispatch logic.

The build→scan hand-off is an OCI archive tar (<artifact-name>.tar): no registry, no credentials for the intra-pipeline hop. oci-push is the CONSUMER end of that same tar at publish time — ci-resolver emits the download-artifact step, then this action loads, logs in, and pushes. It is the one action that takes credentials, and only by NAME: the leaf declares env: [REGISTRY_USERNAME, REGISTRY_PASSWORD] and ci-resolver's per-target credentials overlay binds those NAMES to secret refs on the push job (a composite action's bash reads the job's OS env, never the secrets context). The push ref is the basename container-build stamped into the tar; a registry: input (the leaf's registry: var) re-tags it under a private host, ABSENT keeps the Docker Hub default.