# 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..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 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 `; 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 `… | … --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-` (`nix build` the closure, no push), `publish-`/`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 `copyTo`s 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 ` 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-` (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 publish`ed 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 )"` 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-` (build-parity, no registry), `publish-` (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`.