mkNix2ContainerPublish: add impureBuild + indexMovesLatest modes (cluster #205)

impureBuild: build the consumer's flake attr at run time via
nix build --impure --sandbox false, instead of embedding the image
closure as an eval-time build dep — required for images that fetch
private artifacts with a token at build time (ii-agent). indexMovesLatest:
publish-index also moves :latest, for repos that publish each arch on a
separate CI agent and converge in a final index-only step. Both opt-in,
default-off; existing consumers unchanged. Verified by eval in both modes.
This commit is contained in:
Oleks
2026-06-04 23:00:03 +03:00
parent 089bd03264
commit a56d219418
2 changed files with 76 additions and 3 deletions
+24
View File
@@ -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.<arch>.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.<arch>` 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
".#<attr>.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
+52 -3
View File
@@ -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.<arch>` 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.<arch>` 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 ".#<attr>.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
''}
'';
};