diff --git a/.woodpecker.yml b/.woodpecker.yml new file mode 100644 index 0000000..6a2294b --- /dev/null +++ b/.woodpecker.yml @@ -0,0 +1,54 @@ +# Woodpecker CI for ZDDC. +# +# This pipeline only runs on `zddc-server-v*` tag pushes — it builds the +# zddc-server runtime container image and publishes it to Codeberg's +# container registry. Other tags (archive-v*, transmittal-v*, etc.) and +# regular pushes are ignored here; the HTML tool releases happen by +# pushing static files to the website (no image involved). +# +# To enable: in Codeberg → repo Settings → Woodpecker → set the secrets +# codeberg_user = your Codeberg username (e.g. VARASYS) +# codeberg_token = a personal token with package:write scope +# Generate the token at https://codeberg.org/user/settings/applications. +# +# After setup, cut a release with: +# git tag zddc-server-v0.0.1 +# git push --tags +# and the pipeline will publish: +# codeberg.org/varasys/zddc-server:0.0.1 +# codeberg.org/varasys/zddc-server:latest + +when: + - event: tag + ref: refs/tags/zddc-server-v* + +steps: + prepare-bundle: + image: docker.io/alpine:3.20 + commands: + # build.sh assembles zddc/dist/web/ from landing and archive + # built outputs (which are committed force-tracked dist files). + # Falls back gracefully when podman isn't present — we don't + # need the cross-compiled binaries here, the runtime container + # builds its own linux/amd64 binary internally. + - sh build.sh + # Image tag = the bare semver after the "zddc-server-v" prefix. + # Plus a "latest" tag for convenience. + - VERSION="${CI_COMMIT_TAG#zddc-server-v}" + - printf '%s\nlatest\n' "$VERSION" > .image-tags + - echo "Will tag image with: $(cat .image-tags | tr '\n' ' ')" + + publish-image: + image: woodpeckerci/plugin-docker-buildx + settings: + registry: codeberg.org + repo: codeberg.org/varasys/zddc-server + dockerfile: zddc/Containerfile + context: zddc + target: server + tags_file: .image-tags + auto_tag: false + username: + from_secret: codeberg_user + password: + from_secret: codeberg_token diff --git a/build.sh b/build.sh index 1440a32..c4c5aa3 100755 --- a/build.sh +++ b/build.sh @@ -14,11 +14,6 @@ sh "$SCRIPT_DIR/classifier/build.sh" "${1:-}" "${2:-}" sh "$SCRIPT_DIR/mdedit/build.sh" "${1:-}" "${2:-}" sh "$SCRIPT_DIR/landing/build.sh" "${1:-}" "${2:-}" -echo "" -echo "=== Building zddc-server binaries ===" -mkdir -p "$SCRIPT_DIR/zddc/dist/web" -podman build --target binaries -o "$SCRIPT_DIR/zddc/dist/" "$SCRIPT_DIR/zddc/" 2>&1 | grep -v "^-->" - echo "" echo "=== Assembling zddc/dist/web/ ===" # Only landing and archive ship inside the server bundle: they call the @@ -26,11 +21,25 @@ echo "=== Assembling zddc/dist/web/ ===" # archive) and are useless without it. transmittal, classifier, and mdedit # are pure client-side tools that work from file:// or any static host; # they are released to website/ for download but not bundled with the server. +mkdir -p "$SCRIPT_DIR/zddc/dist/web" cp "$SCRIPT_DIR/landing/dist/index.html" "$SCRIPT_DIR/zddc/dist/web/index.html" cp "$SCRIPT_DIR/archive/dist/archive.html" "$SCRIPT_DIR/zddc/dist/web/archive.html" echo "Wrote zddc/dist/web/index.html" echo "Wrote zddc/dist/web/archive.html" +# Cross-compiled zddc-server binaries are built via podman if available +# (no-op otherwise — CI builds the runtime container directly via the +# Containerfile's builder stage and doesn't need host-side binaries). +echo "" +echo "=== Building zddc-server binaries ===" +if command -v podman >/dev/null 2>&1; then + podman build --target binaries -o "$SCRIPT_DIR/zddc/dist/" "$SCRIPT_DIR/zddc/" 2>&1 | grep -v "^-->" +else + echo "podman not found — skipping cross-compiled binary build." + echo " (CI builds the container image directly; this step only matters" + echo " for releasing the standalone Linux/macOS/Windows binaries.)" +fi + # ─── Bootstrap zips ────────────────────────────────────────────────────────── # Generated from bootstrap/level{1,2}.html.tmpl on every build so they are # always in sync with the current bootstrap pattern. diff --git a/zddc/Containerfile b/zddc/Containerfile index e96fb7f..37b2b96 100644 --- a/zddc/Containerfile +++ b/zddc/Containerfile @@ -1,4 +1,20 @@ # syntax=docker/dockerfile:1 +# +# Multi-stage build with three useful targets: +# +# --target binaries — scratch image holding cross-compiled binaries. +# Use `podman build --target binaries -o dist/ .` +# to extract zddc-server-{linux,darwin,windows}-* +# to the host. No image published from this stage. +# +# --target server — alpine-based runtime. Default target. Published +# as codeberg.org/varasys/zddc-server:vX.Y.Z. +# +# Build context expectations (when targeting `server`): +# dist/web/index.html and dist/web/archive.html must exist — +# produced by `sh build.sh` from the repo root. CI assembles these +# before invoking podman. See .woodpecker.yml. +# # ─── Stage 1: build ────────────────────────────────────────────────────────── FROM docker.io/library/golang:1.24-alpine AS builder @@ -35,16 +51,51 @@ RUN CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -trimpath -ldflags="-s -w" FROM scratch AS binaries COPY --from=builder /out/ / -# ─── Stage 3: runtime ──────────────────────────────────────────────────────── -FROM docker.io/library/alpine:3.20 +# ─── Stage 3: runtime (published image) ───────────────────────────────────── +FROM docker.io/library/alpine:3.20 AS server -# Non-root user -RUN addgroup -S zddc && adduser -S -G zddc zddc +LABEL org.opencontainers.image.title="zddc-server" \ + org.opencontainers.image.description="HTTP server for ZDDC archives — ACL via .zddc files, virtual archive index, audit logging" \ + org.opencontainers.image.source="https://codeberg.org/VARASYS/ZDDC" \ + org.opencontainers.image.documentation="https://zddc.varasys.io/zddc-server.html" \ + org.opencontainers.image.licenses="AGPL-3.0-only" \ + org.opencontainers.image.vendor="VARASYS" +# wget is in the base image (busybox); explicitly install ca-certificates +# so outbound HTTPS (e.g. an upstream auth check) works if the operator +# adds anything later. Keep the install footprint minimal. +RUN apk add --no-cache ca-certificates && rm -rf /var/cache/apk/* + +# Non-root user. UID/GID are deliberately fixed so volume permissions are +# predictable across hosts. +RUN addgroup -S -g 1000 zddc && adduser -S -u 1000 -G zddc zddc + +# Binary COPY --from=builder /out/zddc-server-linux-amd64 /usr/local/bin/zddc-server +# Bundled landing + archive tools — useful for self-contained demos and as +# a fallback web root. Set ZDDC_ROOT=/opt/zddc-server/web to serve only +# these (no external data). For real archives, mount the data tree at +# /srv (the default ZDDC_ROOT below). +COPY dist/web/index.html /opt/zddc-server/web/index.html +COPY dist/web/archive.html /opt/zddc-server/web/archive.html + +# Conventional mount point for the served archive. Operators mount their +# data here (Azure Files, NFS, hostPath, …). Override with ZDDC_ROOT. +VOLUME /srv + USER zddc +# Default config: data mount at /srv. Override at run time as needed. +# Other env vars (ZDDC_TLS_CERT, ZDDC_EMAIL_HEADER, ZDDC_CORS_ORIGIN, …) +# are intentionally not defaulted — see zddc/README.md. +ENV ZDDC_ROOT=/srv + EXPOSE 8443 +# Liveness probe for `docker run` users. Kubernetes deployments override +# this with their own livenessProbe / readinessProbe. +HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \ + CMD wget --no-check-certificate -q --spider https://localhost:8443/ || exit 1 + ENTRYPOINT ["/usr/local/bin/zddc-server"] diff --git a/zddc/README.md b/zddc/README.md index 669b918..d1c0274 100644 --- a/zddc/README.md +++ b/zddc/README.md @@ -292,12 +292,59 @@ the actual built tool from `install.zip`, or a level-1/level-2 bootstrap stub th it. Then open it via the zddc-server URL; the app will auto-connect and scan the directory tree. -## Building +## Container image -### Run as a container +Each `zddc-server-vX.Y.Z` git tag publishes a runtime image to Codeberg's +container registry via the Woodpecker CI pipeline at `.woodpecker.yml`: + +``` +codeberg.org/varasys/zddc-server:X.Y.Z +codeberg.org/varasys/zddc-server:latest +``` + +Pull and run: ```sh -podman build -t zddc-server . +podman run --rm -p 8443:8443 \ + -v /srv/archive:/srv:Z \ + -e ZDDC_TLS_CERT=none \ + -e ZDDC_ADDR=:8080 \ + -e ZDDC_INSECURE_DIRECT=1 \ + codeberg.org/varasys/zddc-server:latest +``` + +The image: + +- alpine-based, runs as non-root (UID 1000) +- exposes 8443 by default +- defaults `ZDDC_ROOT=/srv` (override or mount your archive there) +- bundles the landing + archive tool HTML at `/opt/zddc-server/web` for + self-contained demos (`ZDDC_ROOT=/opt/zddc-server/web`) +- declares `VOLUME /srv` so the operator's data mount is explicit +- ships a `HEALTHCHECK` for `docker run`; Kubernetes deployments override it + +### Env-var contract (for chart consumers) + +Downstream Helm charts and Compose files should set these explicitly rather +than relying on image defaults: + +| Variable | Typical value (behind ingress + SSO) | Purpose | +|---|---|---| +| `ZDDC_ROOT` | `/srv` | Path of the served archive (volume mount) | +| `ZDDC_TLS_CERT` | `none` | TLS terminated upstream | +| `ZDDC_INSECURE_DIRECT` | `1` | Acknowledge plain HTTP behind a trusted proxy | +| `ZDDC_ADDR` | `:8080` | Match service / probe port | +| `ZDDC_EMAIL_HEADER` | `X-Auth-Request-Email` | Header your auth proxy sets | +| `ZDDC_CORS_ORIGIN` | `https://your-host` | Origins permitted to call back into the server | + +See "Environment Variables" above for the full list. + +## Building + +### Run as a container (build locally) + +```sh +podman build --target server -t zddc-server . ``` ### Compile native binaries (no Go installation required) @@ -346,6 +393,19 @@ git tag zddc-server-v1.0.0 git push --tags ``` +The `zddc-server-v*` tag triggers the `.woodpecker.yml` pipeline which +builds and publishes the container image. See "Container image" above +for the resulting image URLs. + +The first time the pipeline runs you must configure two Woodpecker +secrets in repo Settings → Woodpecker: + +- `codeberg_user` — your Codeberg username (e.g. `VARASYS`) +- `codeberg_token` — a personal token with `package:write` scope, generated at + +These never live in the repo; they are referenced from the pipeline via +`from_secret:`. + --- **Notes:**