diff --git a/CHANGELOG.md b/CHANGELOG.md index b8bb9e0..23e3bfa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,30 @@ semantic versioning; the version is a conceptual tag (no git tag is created). ## Unreleased +- **Feature: `mkNix2ContainerPublish` impure-build + index-moves-latest modes + (cluster #205).** Two opt-in args, both default-off so every existing consumer + is byte-unchanged: + - `impureBuild ? false` — for images that are built **impurely** (need a + registry token / network / `--option sandbox false` at build time, e.g. + ii-agent fetching private wheels). The default contract references + `images..copyTo` as an **eval-time store path** — a build dep of the + publish app — which is fatal for an impure image (it would build under the + pure sandbox). In impure mode each `images.` instead carries `attr` + (a flake attribute on the *consumer's own* flake), and `stage`/`verify-digest` + build it at **run time** via `nix build --impure --option sandbox false + ".#.copyTo"`, then read the path back; `publish` reads the staged + copy-to path (no rebuild). The impure closure never becomes a build dep of + the app. + - `indexMovesLatest ? false` — when true, `publish-index` ALSO moves `:latest` + (a dev-guarded digest copy of `:VERSION`) after assembling the index. For + repos that publish each arch on a **separate CI agent** and converge in a + final index-only step (ii-agent's `.woodpecker/{amd64,arm64}.yaml` + + `publish-index`), `:latest` must move there rather than in `publish`. + + Verified by eval (both modes): impure `stage`/`verify-digest` emit `nix build + --impure`, `publish` delegates to the staged path, `publish-index` moves + `:latest`; pure mode emits the store path, no impure build, and `publish-index` + does NOT touch `:latest`. - **Fix: `pipeline-doctor` models branch-deploy repos (#204).** The dev-tag-guard check only accepted a `refs/tags/v*` tag gate, so web-app/CMS repos that deploy on a **branch push** (`event: push` + `branch: develop/staging/production`) and diff --git a/lib/builders.nix b/lib/builders.nix index 8e649d6..89c6e22 100644 --- a/lib/builders.nix +++ b/lib/builders.nix @@ -679,11 +679,44 @@ let registryHost ? "git.oleks.space", registryOwner ? "oleks", passEntry ? "infra/gitea/personal_access_token_packages_rw", + # IMPURE-BUILD mode (cluster #205). Default: each `images.` carries a + # `copyTo` derivation referenced as an EVAL-TIME store path (a build dep of + # the app) — correct for a pure image. But some images are built IMPURELY + # (need a registry token / network / sandbox off at build time, e.g. + # ii-agent fetching private wheels), so the closure CANNOT be a build dep + # of the app. When `impureBuild = true`, each `images.` instead + # carries `attr` (a flake attribute on the CONSUMER's own flake, e.g. + # "frontend-image"), and stage/publish/verify-digest build it at RUN time + # via `nix build --impure --option sandbox false ".#.copyTo"`. + impureBuild ? false, + # When true, `publish-index` ALSO moves `:latest` (a dev-guarded digest + # copy of `:VERSION`) after assembling the index. Default off: `:latest` + # is owned by the `publish` app. Repos that publish each arch on a SEPARATE + # CI agent and converge in a final index-only step (ii-agent) need the + # latest-move to live in `publish-index`. cluster #205. + indexMovesLatest ? false, }: let head = preamble { inherit registryHost registryOwner passEntry; }; localArches = lib.attrNames images; + # Resolve one arch's nix2container copy-to wrapper into shell var $1. + # Pure: an eval-time store path (build dep of the app). Impure (cluster + # #205): build the consumer's own flake attr at RUN time and read the path + # back — the impure layers (token/network/sandbox-off) never become a build + # dep of the app, which would otherwise fail under the pure sandbox. + resolveCopyTo = + arch: var: + if impureBuild then + '' + IMAGE_TAG="$VERSION-${arch}" nix build --impure --no-link \ + --option sandbox false ".#${images.${arch}.attr}.copyTo" >/dev/null + ${var}="$(IMAGE_TAG="$VERSION-${arch}" nix eval --impure --raw \ + ".#${images.${arch}.attr}.copyTo")" + '' + else + "${var}=${lib.escapeShellArg "${images.${arch}.copyTo}"}"; + # per-arch stage: realise the image's copy-to closure locally (no push). mkArchStage = arch: @@ -696,7 +729,7 @@ let echo "→ staging ${imageName}:$VERSION-${arch} (build closure only, no push)" d="$(parity_stage_reset ${lib.escapeShellArg arch})" # Realise the nix2container copyTo wrapper (its closure is the image). - out=${lib.escapeShellArg "${images.${arch}.copyTo}"} + ${resolveCopyTo arch "out"} printf 'oci %s %s %s\n' ${lib.escapeShellArg imageName} "$VERSION" "${arch}" >"$d/.parity-meta" printf '%s\n' "$out" >"$d/copy-to-path" echo " staged copyTo: $out" @@ -715,7 +748,8 @@ let digestArch = arch: '' echo "→ ${imageName}:$VERSION-${arch} (local build, no registry contact)" ocidir="$(mktemp -d)" - ${lib.escapeShellArg "${images.${arch}.copyTo}"}/bin/copy-to "oci:$ocidir:${arch}" >/dev/null + ${resolveCopyTo arch "copyto"} + "$copyto/bin/copy-to" "oci:$ocidir:${arch}" >/dev/null digest="$(skopeo manifest-digest "$ocidir/blobs/sha256/$( jq -r '.manifests[0].digest | sub("sha256:";"")' "$ocidir/index.json" )")" @@ -758,7 +792,10 @@ let tok="$(parity_resolve_token)" parity_registry_preflight creds="$PARITY_REGISTRY_OWNER:$tok" - ${lib.escapeShellArg "${images.${arch}.copyTo}"}/bin/copy-to \ + # Read the copy-to path the stage step just wrote (works for both + # pure and impure modes — stage already realised/built the closure). + copyto="$(cat "$PARITY_STAGE_DIR/${arch}/copy-to-path")" + "$copyto/bin/copy-to" \ --dest-creds "$creds" \ "docker://${imageName}:$VERSION-${arch}" echo "Pushed ${imageName}:$VERSION-${arch}" @@ -809,6 +846,18 @@ let done regctl index create "${imageName}:$VERSION" "''${refs[@]/#/--ref=}" echo "Assembled index ${imageName}:$VERSION" + ${lib.optionalString indexMovesLatest '' + # Repos that converge per-arch legs in this index-only step own the + # ':latest' move here (rather than in `publish`). Dev-guarded; a pure + # digest copy of the just-assembled ':VERSION', done LAST. + case "$VERSION" in + *dev*) echo " dev version $VERSION — refusing to move :latest" ;; + *) + regctl image copy "${imageName}:$VERSION" "${imageName}:latest" + echo "→ ${imageName}:$VERSION -> :latest (digest copy)" + ;; + esac + ''} ''; };