ci: publish the container image

New image step after release. It stages the binary the build step made
under ci-bin/ and builds the Containerfile with BIN_STAGE=prebuilt, so the
image ships the binary e2e tested without a second compile. Tags: short sha
and "edge" on every run, plus the tag and "latest" on a tag run.

The Containerfile gains a "prebuilt" stage for that. It is skipped in a
normal build, which is unchanged.

The step talks to the host engine with podman-remote. The server runs
Podman; the docker CLI's buildx builds fail against a Podman socket.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
AuthorKonata <konata@posteo.jp>
Date
Commita443c039648f337991ab23bdb2082b430ff5be53
Parent00e257b
3 files changed, 47 insertions(+), 3 deletions(-)
▾M.gitignore
@@ -10,3 +10,5 @@ filebrowser-ng
# JS deps (CodeMirror bundle source)
/web/node_modules
# binary staged by CI for the image step
ci-bin/
▾M.hearthforge-ci.toml
@@ -54,7 +54,7 @@ to = "/usr/local/bin"
name = "setup"
timeout = 900
run_sh = """
apk add --no-cache musl-dev binaryen just curl libstdc++
apk add --no-cache musl-dev binaryen just curl libstdc++ podman-remote
# The official rust images use rustup's minimal profile, so rustfmt and
# clippy are absent. `just lint` needs both.
@@ -107,3 +107,36 @@ install -Dm755 "${CARGO_TARGET_DIR}/release/filebrowser-ng" \
"dist/filebrowser-ng-${CI_COMMIT_TAG}-x86_64-linux-musl"
"""
publish_gzip = ["/ci/build/project/dist/"]
# ── image ────────────────────────────────────────────────────────────────────
# Packages the binary the build step made, so the image ships exactly what
# e2e tested. The Containerfile's build stage is skipped via BIN_STAGE.
# engine_socket hands this step the host engine, which is Podman on this
# server (needs CI_ENGINE_SOCKET=1). REGISTRY_PASSWORD is a CI secret with the
# admin password. CI_REGISTRY is "<host>/<repo>" on the built-in registry.
# Tags: every run pushes the short sha and "edge"; a tag run also pushes the
# tag and "latest".
[[steps]]
name = "image"
engine_socket = true
timeout = 900
run_sh = """
cd project
mkdir -p ci-bin
cp "${CARGO_TARGET_DIR}/release/filebrowser-ng" ci-bin/filebrowser-ng
echo "$REGISTRY_PASSWORD" | podman-remote login "${CI_REGISTRY%%/*}" -u admin --password-stdin
img="$CI_REGISTRY:$CI_COMMIT_SHORT_SHA"
podman-remote build -f Containerfile -t "$img" --build-arg BIN_STAGE=prebuilt .
podman-remote push "$img"
if [ -n "${CI_COMMIT_TAG:-}" ]; then
tags="$CI_COMMIT_TAG latest"
else
tags="edge"
fi
for t in $tags; do
podman-remote tag "$img" "$CI_REGISTRY:$t"
podman-remote push "$CI_REGISTRY:$t"
done
"""
▾MContainerfile
@@ -32,17 +32,26 @@ COPY . /src
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/src/target \
just build \
&& cp target/release/filebrowser-ng /src/filebrowser-ng
&& cp target/release/filebrowser-ng /filebrowser-ng
# ── prebuilt ─────────────────────────────────────────────────────────────────
# CI has the binary already. It puts it at ci-bin/filebrowser-ng and selects
# this stage with --build-arg BIN_STAGE=prebuilt. BuildKit and buildah do not
# build a stage nobody references, so a normal build needs no ci-bin/.
FROM scratch AS prebuilt
COPY ci-bin/filebrowser-ng /filebrowser-ng
# ── runtime ──────────────────────────────────────────────────────────────────
FROM alpine:${ALPINE_VERSION}
ARG BIN_STAGE=build
# ffmpeg is only for video thumbnails, and it is most of the image: it pulls
# ~120 MiB of codec libraries against 8 MiB for the rest
RUN apk add --no-cache ca-certificates ffmpeg \
&& mkdir -p /data /var/lib/filebrowser /var/cache/filebrowser
COPY --from=build /src/filebrowser-ng /usr/local/bin/filebrowser-ng
COPY --from=${BIN_STAGE} /filebrowser-ng /usr/local/bin/filebrowser-ng
EXPOSE 8080
VOLUME ["/data", "/var/lib/filebrowser"]