From 68f56637e97285b646c7499091e579e09714b60b Mon Sep 17 00:00:00 2001 From: Oleks Date: Mon, 1 Jun 2026 23:41:28 +0300 Subject: [PATCH] ci: add publish-arm64 flake app for local parity (emmett#44) Introduce a shared publish-arm64 flake app (archetype oci-image-skopeo) that builds the arm64 docker-archive via Nix and skopeo-copies it to the Gitea OCI registry as :-arm64, mirroring to :latest-arm64. Both .woodpecker.yaml and `nix run .#publish-arm64` invoke the same app so CI and local cannot drift. - dry-run by default; PUBLISH=1 to actually push (safe to run locally) - token via $REGISTRY_TOKEN, fallback pass infra/gitea/personal_access_token_packages_rw - token never printed; no set -x on token-bearing paths - rename CI secret env CI_REGISTRY_TOKEN -> REGISTRY_TOKEN - thin .woodpecker.yaml: one PUBLISH=1 nix run line - --help/--dry-run honored; meta.description set --- .woodpecker.yaml | 32 +++---------- README.md | 21 ++++++++- flake.nix | 116 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 141 insertions(+), 28 deletions(-) diff --git a/.woodpecker.yaml b/.woodpecker.yaml index 711bc3a..b3522f8 100644 --- a/.woodpecker.yaml +++ b/.woodpecker.yaml @@ -24,7 +24,8 @@ steps: environment: GITEA_CLONE_TOKEN: from_secret: gitea_clone_token - CI_REGISTRY_TOKEN: + # Single token env var shared by CI + local parity (emmett#44). + REGISTRY_TOKEN: from_secret: registry_token backend_options: kubernetes: @@ -43,28 +44,7 @@ steps: EOF - if [ -n "$GITEA_CLONE_TOKEN" ]; then echo "machine git.oleks.space login oleks password $GITEA_CLONE_TOKEN" >~/.netrc && chmod 600 ~/.netrc; fi - # Resolve the upstream Angie version and build the image stream script. - - VERSION="$(nix eval --raw .#angieVersion)" - - echo "Building angie $VERSION" - - STREAM="$(nix build .#default --print-out-paths --no-link)" - - # skopeo's containers/image library writes intermediate files under - # /var/tmp (not TMPDIR), and the nix-ci image doesn't seed that path. - - mkdir -p /var/tmp && chmod 1777 /var/tmp - - # Auth + push to Gitea OCI registry under both -arm64 and latest-arm64. - - mkdir -p ~/.config/containers - - | - printf '{"auths":{"git.oleks.space":{"auth":"%s"}}}\n' \ - "$(printf 'oleks:%s' "$CI_REGISTRY_TOKEN" | base64 -w0)" \ - > ~/.config/containers/auth.json - - | - nix run nixpkgs#skopeo -- copy --insecure-policy \ - --authfile ~/.config/containers/auth.json \ - docker-archive:<("$STREAM") \ - docker://git.oleks.space/oleks/angie:$VERSION-arm64 - - | - nix run nixpkgs#skopeo -- copy --insecure-policy \ - --authfile ~/.config/containers/auth.json \ - docker://git.oleks.space/oleks/angie:$VERSION-arm64 \ - docker://git.oleks.space/oleks/angie:latest-arm64 + # Thin front door: CI and `nix run .#publish-arm64` on emmett run the + # exact same shared app (emmett#44, archetype oci-image-skopeo). The app + # is dry-run by default; PUBLISH=1 makes it actually push. + - PUBLISH=1 nix run .#publish-arm64 diff --git a/README.md b/README.md index c9c9e20..5b8969e 100644 --- a/README.md +++ b/README.md @@ -8,14 +8,31 @@ registry. ## Output | Tag | Pushed when | -|---|---| +| --- | --- | | `git.oleks.space/oleks/angie:-arm64` | every successful CI run | | `git.oleks.space/oleks/angie:latest-arm64` | every successful CI run | The version string comes from `pkgs.angie.version` in nixpkgs unstable; bump the flake input to roll it forward. -## Build locally +## Build / publish locally + +CI and local runs share one entrypoint (emmett#44, archetype oci-image-skopeo): + +```bash +# dry-run: build the arm64 image and print the refs it would push (no registry contact) +nix run .#publish-arm64 + +# actually push :-arm64 and mirror to :latest-arm64 +PUBLISH=1 nix run .#publish-arm64 +``` + +The registry token is read from `$REGISTRY_TOKEN`, falling back to +`pass infra/gitea/personal_access_token_packages_rw`. The token is never +printed. `.woodpecker.yaml` runs the exact same app with `PUBLISH=1`, so CI +and local cannot drift. + +Just the raw build (no push): ```bash nix build .#default diff --git a/flake.nix b/flake.nix index 010da9a..db5adeb 100644 --- a/flake.nix +++ b/flake.nix @@ -78,11 +78,127 @@ WorkingDir = "/etc/angie"; }; }; + # Shared build+publish logic for the arm64 OCI leg. This script IS the + # parity code (emmett#44, archetype: oci-image-skopeo): both + # `.woodpecker.yaml` and a local `nix run .#publish-arm64` invoke the + # exact same entrypoint, so CI and local cannot drift. + # + # Safety: DRY-RUN by default. It builds the image stream and prints the + # refs it WOULD push, but performs no registry contact unless PUBLISH=1 + # is set. The token is never printed and this script never runs under + # `set -x` (which would leak the auth header). + publish-arm64 = pkgs.writeShellApplication { + name = "publish-arm64"; + runtimeInputs = with pkgs; [ + skopeo + coreutils + gnused + gitMinimal + ]; + # `pass` is optional (only the local fallback path needs it) and may + # live outside this closure, so it is resolved from PATH at runtime. + text = '' + set -euo pipefail + + # --- usage ------------------------------------------------------- + if [ "''${1:-}" = "--help" ] || [ "''${1:-}" = "-h" ]; then + printf '%s\n' \ + "publish-arm64 — build the angie arm64 OCI image and (optionally) push it." \ + "" \ + "Builds the arm64 docker-archive via Nix and uses skopeo to copy it to" \ + "the Gitea OCI registry as :-arm64, then mirrors that digest to" \ + ":latest-arm64." \ + "" \ + "DRY-RUN by default: prints the refs it would push and exits without" \ + "contacting the registry. Set PUBLISH=1 to actually push." \ + "" \ + "Env:" \ + " PUBLISH=1 actually push (default: dry-run)" \ + " VERSION= override the tag (default: angie.version)" \ + " REGISTRY_TOKEN= registry RW token; if empty, falls back to" \ + " pass infra/gitea/personal_access_token_packages_rw" \ + "" \ + "Flags:" \ + " --help, -h this help" \ + " --dry-run force dry-run even if PUBLISH=1" + exit 0 + fi + + DRY_RUN=0 + if [ "''${PUBLISH:-0}" != "1" ]; then DRY_RUN=1; fi + if [ "''${1:-}" = "--dry-run" ]; then DRY_RUN=1; fi + + REGISTRY="git.oleks.space" + IMAGE="oleks/angie" + + # --- version / tag (identical for CI + local) -------------------- + VERSION="''${VERSION:-$(nix eval --raw .#angieVersion)}" + echo "angie version: $VERSION" + echo "target refs: docker://$REGISTRY/$IMAGE:$VERSION-arm64" + echo " docker://$REGISTRY/$IMAGE:latest-arm64" + + # --- build (cluster-independent) --------------------------------- + echo "building arm64 image stream..." + STREAM="$(nix build .#default --print-out-paths --no-link)" + + if [ "$DRY_RUN" = "1" ]; then + echo "DRY-RUN: built $STREAM" + echo "DRY-RUN: would skopeo-copy docker-archive -> docker://$REGISTRY/$IMAGE:$VERSION-arm64" + echo "DRY-RUN: would mirror :$VERSION-arm64 -> :latest-arm64" + echo "DRY-RUN: set PUBLISH=1 to actually push." + exit 0 + fi + + # --- token resolution (never printed) ---------------------------- + TOKEN="''${REGISTRY_TOKEN:-}" + if [ -z "$TOKEN" ]; then + if command -v pass >/dev/null 2>&1; then + TOKEN="$(pass infra/gitea/personal_access_token_packages_rw)" + fi + fi + if [ -z "$TOKEN" ]; then + echo "ERROR: no registry token. Set REGISTRY_TOKEN or store it at" >&2 + echo " pass infra/gitea/personal_access_token_packages_rw" >&2 + exit 1 + fi + + # skopeo's containers/image library stages under /var/tmp (not TMPDIR). + mkdir -p /var/tmp && chmod 1777 /var/tmp || true + + AUTHFILE="$(mktemp -d)/auth.json" + # shellcheck disable=SC2064 + trap "rm -rf '$(dirname "$AUTHFILE")'" EXIT + printf '{"auths":{"%s":{"auth":"%s"}}}\n' \ + "$REGISTRY" "$(printf 'oleks:%s' "$TOKEN" | base64 -w0)" \ + > "$AUTHFILE" + + echo "pushing :$VERSION-arm64 ..." + skopeo copy --insecure-policy --authfile "$AUTHFILE" \ + docker-archive:<("$STREAM") \ + "docker://$REGISTRY/$IMAGE:$VERSION-arm64" + + echo "mirroring :$VERSION-arm64 -> :latest-arm64 ..." + skopeo copy --insecure-policy --authfile "$AUTHFILE" \ + "docker://$REGISTRY/$IMAGE:$VERSION-arm64" \ + "docker://$REGISTRY/$IMAGE:latest-arm64" + + echo "done." + ''; + }; in { packages.${system} = { default = image; }; + + apps.${system} = { + publish-arm64 = { + type = "app"; + program = "${publish-arm64}/bin/publish-arm64"; + meta.description = "Build and publish the angie arm64 OCI image to git.oleks.space (dry-run by default; PUBLISH=1 to push)."; + }; + }; + # Plain string — read by CI via `nix eval --raw .#angieVersion`. angieVersion = angie.version; };