- Shell 100%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
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. |
||
| container-build | ||
| container-exec | ||
| docker-cleanup | ||
| forgejo-release | ||
| LICENSES | ||
| oci-push | ||
| run-tool | ||
| secrets-provision | ||
| CONTRIBUTING.md | ||
| LICENSE | ||
| projectfile.yaml | ||
| README.md | ||
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.go → steps/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-buildstamps the resolved ref into atype=ociarchive (kept OCI so the daemonlessimage-scancan read the same tar), so the fused job'sdocker loadrestores it TAGGED andcompose'spull_policy: neverfinds 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
makerecipes (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.