Files
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

170 lines
12 KiB
Markdown

# Changelog
<!-- markdownlint-disable MD013 -->
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
`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 <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 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 <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`.