Files
parity-lib/CHANGELOG.md
T
Oleks a56d219418 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.
2026-06-04 23:00:03 +03:00

12 KiB

Changelog

All notable changes to parity-lib are documented here. This project follows 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 tag each push from CI_COMMIT_BRANCH/CI_COMMIT_SHA/CI_PIPELINE_NUMBER (a deterministic per-push tag, no default-version clobber) false-failed. The check now accepts that branch-deploy form as a valid guard. cms-plugins, emdash-kotkanagrilli, kotkanagrilli.fi → 9/9; trio + self-test unchanged; a tagless event: push publish with NO per-push tag still FAILs.

  • Fix: pipeline-doctor now reads a .woodpecker/ DIRECTORY (#202). It only folded the single-file .woodpecker.yaml/.yml into ci_txt; repos whose Woodpecker config is a .woodpecker/ directory (per-arch workflows) had their refs/tags/v* trigger invisible, so the dev-tag-guard check false-failed. Now globs .woodpecker/*.yaml|*.yml too. Effect: commonground-legacy + csi-s3 go 9/9. (cms-plugins still 8/9 — but legitimately: it's a branch-deploy repo with no v* tag at all, a separate heuristic gap tracked elsewhere.)

  • Fix: pipeline-doctor token-heuristic false positives (#199). The audit sweep wrongly FAILED ~9 correctly-converted ci-script repos on two heuristics. (1) The token-contract check only accepted a hard-coded pass <literal-path>; it now also accepts the secure indirection pass "$PASS_ENTRY" / pass "$VAR" (a quoted/unquoted shell var, optionally via pass show). (2) The leak scan flagged the blessed echo "$TOKEN" | docker login … --password-stdin (and --pass-stdin, and helm registry login) idiom — the token goes to STDIN, not the log — because the --password-stdin flag often sits on a \-wrapped continuation line the line-based grep never saw on the echo line. The scan now flattens \-continuations and folds the pipe target onto the echo line, then exempts the … | <cmd> … --password-stdin/--pass-stdin feed. Real leaks still FAIL: a bare echo "$TOKEN" to stdout or a file, and set -x in a token script. Added --self-test: six inline fixtures lock in both fixes and the three must-still-catch leaks. Verified: version-radar, xonsh, common-chronicle, ii-researcher, ironclaw, openclaw now PASS; parity-lib --strict ., gitea-mcp, numpy-s390x still 9/9. (commonground-legacy / cms-plugins / csi-s3 still FAIL one UNRELATED check — dev-tag-guard — because their woodpecker config lives in a .woodpecker/ directory the doctor doesn't yet read; out of scope for #199.)

  • Feature: mkAtticClosurePublish — the attic-closure builder (cluster #198). Models the archetype parity-lib was missing: build a Nix closure and push it to the Attic binary cache (NO registry artifact). Yields stage-<arch> (nix build the closure, no push), publish-<arch>/publish (build + attic login + attic push; dry-run by default, --publish/PUBLISH=1 to push; token via $ATTIC_TOKEN or pass, never echoed), and push-staged. Lets caddy-with-replace (#104) drop its generic-publish over-reach and overlay-xonsh (#105) convert off N/A; flake-hub / woodpecker-peek can retire their bespoke attic wraps.

  • pipeline-doctor non-flake mode (cluster #191/#193). A repo with NO root flake.nix is now a VALID parity form if it ships the ci-script entrypoints (ci/local.sh, or ci/build.sh + ci/publish.sh) called by a thin .woodpecker.yaml — so the non-flake go-binary/helm references (gitea-mcp, helms) PASS the gate instead of failing for lacking a flake. The token-leak scan and the #191 no-set -x-in-token-scripts scan still run on their ci/*.sh in full. Verified: gitea-mcp now 9/9, parity-lib + numpy-s390x still 9/9.

  • Feature: verify-digest for nix2container (cluster #195). mkNix2ContainerPublish now also returns a verify-digest app that builds each locally-buildable arch image and prints its OCI manifest digest with NO registry contact (it copyTos a throwaway local oci: dir and reads the digest skopeo derives). This formalizes the manifest digest as the parity contract: the OCI layers are built reproducible = false ON PURPOSE (the fix for the "Digest did not match" caused by non-reproducible layer deps + nix2container's lazy tar regeneration), so byte-identical-tar parity is NOT promised — but the content-addressed manifest digest the registry stores the image under IS stable. Identical local vs CI digest ⇒ identical registry artifact. We do NOT flip reproducible to true (the inputs are not reproducible); the LOW-RISK digest-as-contract path was chosen instead. Generalized from the claude-plugin-registry prototype (55f2d0b) so every nix2container consumer gets it for free.

  • pipeline-doctor is now GATE-READY (cluster #191/#193). It already exited non-zero on any failing required check; added a --strict mode that ALSO fails on any WARN, so a .woodpecker.yaml step or a server pre-receive hook can call pipeline-doctor --strict <repo> and rely on the exit code. Added a documented ci/local.sh escape-hatch (cluster #196): a repo that must keep a hand-written Dockerfile/BuildKit pipeline may opt out of the archetype / parity-lib asserts (downgraded to warnings) if it ships a ci/local.sh local==CI entrypoint. Fixed a false-negative: the token / dev-tag / dry-run / meta.description contracts are GUARANTEED by parity-lib for a consumer (they live in the generated apps, not the consumer's flake.nix text), so a repo that consumes parity-lib now PASSES those by delegation instead of being penalized for not re-implementing them inline. Self-check stays green and a known-good consumer (numpy-s390x) now passes 9/9.

  • Audit: stage + push-staged uniform across all 8 builders (cluster #194). Verified every archetype builder exposes a stage-<arch> (build-parity, no registry contact, writes ./.parity-stage) AND a push-staged (replay the staged artifact): mkPyPiWheelPublish, mkPyPiWheelPublishMulti, mkS390xNpmPublish, mkS390xNpmPublishMulti, mkGenericBinaryPublish, mkGoBinaryPublish (alias), mkNix2ContainerPublish and mkHelmPublish (stage-chart). All were already complete — no gaps to fill; the build-parity / publish-parity split is uniform.

  • Feature: mkS390xNpmPublishMulti (cluster #192). A multi-version npm builder mirroring the PyPI multi one: publishes a fixed list of { version; file; distTag? } per tag, each staged into its own dir and npm published with its dist-tag (idempotent — "already exists" == success). file may be a .node addon OR a plain binary, and packageJson (with a $VERSION the stage heredoc expands) declares the shape (main vs bin), so it covers both nextjs-swc (16.1.6 @latest + 15.2.0 @next15) and sentry-cli (a binary published as an npm package at two versions). Shared parity_npm_publish_dir helper added to ci/parity-lib.sh.

  • Feature: mkPyPiWheelPublishMulti (cluster #197). A multi-version PyPI builder that publishes a fixed list of { version; wheel; } per tag instead of just the default — the pre-parity behaviour several *-s390x repos rely on. Each wheel's real version is read from its filename (PEP 427), so stage/publish/push-staged need no side-channel map and a re-run is idempotent (409-skip per version). Shared parity_pypi_post / parity_wheel_version helpers added to ci/parity-lib.sh. First consumer: numpy-s390x (5 versions).

  • Fix (safety): dev-tag guard was ineffective. Every publish app body runs VERSION="$(parity_derive_version <default>)" before parity_devtag_guard, so by the time the guard checked $VERSION it was always non-empty (the derived default) and an accidental local --publish with no explicit version and no v* tag still pushed (cluster #194 finding). The guard now reads a source-time snapshot PARITY_VERSION_EXPLICIT captured before any clobber, so it correctly blocks unless the caller set $VERSION or $CI_COMMIT_TAG matches ^v[0-9].

  • pipeline-doctor (cluster #191 security sweep): added a scoped per-file check asserting no set -x in token-bearing ci/*.sh scripts going forward — a script that references a registry token (REGISTRY_TOKEN / CI_REGISTRY_TOKEN / an Authorization: token header) must not enable xtrace, which would echo the token to the build log. Token-free helpers (e.g. version parsers) are not flagged.

v0.1.0

Initial release (cluster #192/#193/#194, emmett#44).

  • lib.mkParityBuilders pkgs plus per-builder wrappers exposing the six archetype publish-app builders:
    • mkPyPiWheelPublish — single-arch Gitea PyPI wheel.
    • mkS390xNpmPublish — single-arch Gitea npm native addon.
    • mkGenericBinaryPublish — single-arch Gitea generic-registry binary.
    • mkGoBinaryPublish — alias of mkGenericBinaryPublish (explicit archetype).
    • mkNix2ContainerPublish — multi-arch OCI image with publish-index and :latest digest copy.
    • mkHelmPublish — Helm chart to an OCI registry.
  • Each builder returns flake apps following the corrected parity standard: stage-<arch> (build-parity, no registry), publish-<arch> (dry-run by default), publish-index (build-free, fail-closed multi-arch assembly via regctl), publish (all local arches + index + :latest last), and push-staged (replay ./.parity-stage).
  • Shared shell library ci/parity-lib.sh (token resolution with $REGISTRY_TOKEN + pass fallback and never printed, dev-tag guard, version derivation, the dry-run gate, registry preflight, stage-dir helpers).
  • packages.pipeline-doctor / apps.pipeline-doctor (cluster #193): static parity-contract checker that prints local-equivalent commands.
  • flake.lock fully pinned; nixpkgs follows the shared fleet-pins nixpkgs-ci.