putitoutthere 0.2.28 → 0.2.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +40 -0
- package/MIGRATIONS.md +1274 -0
- package/README.md +343 -41
- package/action.yml +9 -0
- package/dist/action.d.ts.map +1 -1
- package/dist/action.js +28 -6
- package/dist/action.js.map +1 -1
- package/dist/cascade.js +14 -7
- package/dist/cascade.js.map +1 -1
- package/dist/check-crate-size.d.ts +26 -0
- package/dist/check-crate-size.d.ts.map +1 -0
- package/dist/check-crate-size.js +99 -0
- package/dist/check-crate-size.js.map +1 -0
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +116 -16
- package/dist/check.js.map +1 -1
- package/dist/cli.d.ts +17 -4
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +156 -19
- package/dist/cli.js.map +1 -1
- package/dist/completeness.js +6 -3
- package/dist/completeness.js.map +1 -1
- package/dist/config.d.ts +2 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +20 -5
- package/dist/config.js.map +1 -1
- package/dist/ensure-tag.d.ts +17 -0
- package/dist/ensure-tag.d.ts.map +1 -0
- package/dist/ensure-tag.js +28 -0
- package/dist/ensure-tag.js.map +1 -0
- package/dist/env.js +6 -3
- package/dist/env.js.map +1 -1
- package/dist/error-codes.d.ts +89 -0
- package/dist/error-codes.d.ts.map +1 -1
- package/dist/error-codes.js +102 -0
- package/dist/error-codes.js.map +1 -1
- package/dist/git.d.ts +5 -0
- package/dist/git.d.ts.map +1 -1
- package/dist/git.js +19 -6
- package/dist/git.js.map +1 -1
- package/dist/glob.d.ts +9 -0
- package/dist/glob.d.ts.map +1 -1
- package/dist/glob.js +29 -1
- package/dist/glob.js.map +1 -1
- package/dist/handlers/crates.d.ts +15 -0
- package/dist/handlers/crates.d.ts.map +1 -1
- package/dist/handlers/crates.js +84 -9
- package/dist/handlers/crates.js.map +1 -1
- package/dist/handlers/npm-platform.d.ts +11 -0
- package/dist/handlers/npm-platform.d.ts.map +1 -1
- package/dist/handlers/npm-platform.js +91 -8
- package/dist/handlers/npm-platform.js.map +1 -1
- package/dist/handlers/npm.d.ts.map +1 -1
- package/dist/handlers/npm.js +59 -9
- package/dist/handlers/npm.js.map +1 -1
- package/dist/handlers/pypi.d.ts +0 -6
- package/dist/handlers/pypi.d.ts.map +1 -1
- package/dist/handlers/pypi.js +28 -10
- package/dist/handlers/pypi.js.map +1 -1
- package/dist/log.js +16 -8
- package/dist/log.js.map +1 -1
- package/dist/normalize-artifacts.js +8 -4
- package/dist/normalize-artifacts.js.map +1 -1
- package/dist/plan.d.ts +8 -0
- package/dist/plan.d.ts.map +1 -1
- package/dist/plan.js +125 -25
- package/dist/plan.js.map +1 -1
- package/dist/preflight.d.ts +79 -0
- package/dist/preflight.d.ts.map +1 -1
- package/dist/preflight.js +715 -33
- package/dist/preflight.js.map +1 -1
- package/dist/publish.d.ts +7 -0
- package/dist/publish.d.ts.map +1 -1
- package/dist/publish.js +49 -18
- package/dist/publish.js.map +1 -1
- package/dist/python-versions.d.ts +40 -0
- package/dist/python-versions.d.ts.map +1 -0
- package/dist/python-versions.js +142 -0
- package/dist/python-versions.js.map +1 -0
- package/dist/reconcile-types.d.ts +36 -0
- package/dist/reconcile-types.d.ts.map +1 -0
- package/dist/reconcile-types.js +9 -0
- package/dist/reconcile-types.js.map +1 -0
- package/dist/reconcile.d.ts +21 -0
- package/dist/reconcile.d.ts.map +1 -0
- package/dist/reconcile.js +54 -0
- package/dist/reconcile.js.map +1 -0
- package/dist/release-packages.d.ts +33 -0
- package/dist/release-packages.d.ts.map +1 -0
- package/dist/release-packages.js +74 -0
- package/dist/release-packages.js.map +1 -0
- package/dist/resolve-tag-commit.d.ts +22 -0
- package/dist/resolve-tag-commit.d.ts.map +1 -0
- package/dist/resolve-tag-commit.js +26 -0
- package/dist/resolve-tag-commit.js.map +1 -0
- package/dist/retry.js +14 -7
- package/dist/retry.js.map +1 -1
- package/dist/status-classify.d.ts +17 -0
- package/dist/status-classify.d.ts.map +1 -0
- package/dist/status-classify.js +37 -0
- package/dist/status-classify.js.map +1 -0
- package/dist/status-format.d.ts +9 -0
- package/dist/status-format.d.ts.map +1 -0
- package/dist/status-format.js +19 -0
- package/dist/status-format.js.map +1 -0
- package/dist/status-types.d.ts +31 -0
- package/dist/status-types.d.ts.map +1 -0
- package/dist/status-types.js +8 -0
- package/dist/status-types.js.map +1 -0
- package/dist/status.d.ts +28 -0
- package/dist/status.d.ts.map +1 -0
- package/dist/status.js +72 -0
- package/dist/status.js.map +1 -0
- package/dist/tag-template.js +2 -1
- package/dist/tag-template.js.map +1 -1
- package/dist/trailer.js +16 -9
- package/dist/trailer.js.map +1 -1
- package/dist/types.d.ts +11 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +4 -2
- package/dist/types.js.map +1 -1
- package/dist/verbose.js +7 -4
- package/dist/verbose.js.map +1 -1
- package/dist/wheel-abi.d.ts +37 -0
- package/dist/wheel-abi.d.ts.map +1 -0
- package/dist/wheel-abi.js +118 -0
- package/dist/wheel-abi.js.map +1 -0
- package/dist/write-crate-version.d.ts +32 -0
- package/dist/write-crate-version.d.ts.map +1 -0
- package/dist/write-crate-version.js +52 -0
- package/dist/write-crate-version.js.map +1 -0
- package/dist/write-launcher.d.ts +82 -0
- package/dist/write-launcher.d.ts.map +1 -0
- package/dist/write-launcher.js +236 -0
- package/dist/write-launcher.js.map +1 -0
- package/dist/write-version.js +2 -1
- package/dist/write-version.js.map +1 -1
- package/package.json +4 -2
package/CHANGELOG.md
CHANGED
|
@@ -10,9 +10,33 @@ are prefixed `**BREAKING**` and link to the matching section in
|
|
|
10
10
|
|
|
11
11
|
## Unreleased
|
|
12
12
|
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- Changed: `_matrix.yml`'s build job now primes a cargo cache via `Swatinem/rust-cache@v2` on every row that invokes cargo (pypi/maturin non-sdist, npm/napi, npm/bundled-cli non-main), partitioned by `matrix.target` via `shared-key`. Previously every per-target matrix cell cold-compiled its full Cargo dep graph on every PR — observed at 4-6 min per cell and ~8 min wall-clock on a downstream consumer's `release-precheck` run (thekevinscott/dirsql run #125, a typical no-Rust-change PR), leaving the gate one bad runner-queue minute away from tripping a 10-min CI budget with no headroom. The cache step is gated to skip rows that produce no cargo work (pypi sdist, pure-Python hatch wheels, npm vanilla, bundled-cli `main`), and the per-target `shared-key` prevents one target's writer from blowing away the next cell's cache slot. `workspaces` enumerates both `matrix.path` (the consumer's package crate) and `matrix.bundle_cli.crate_path` (the bundle_cli crate when it lives in a separate dir), so the dirsql shape and single-crate layouts both cache their target dirs. #391. (verified by: unit/ubuntu-latest)
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- Fixed: `putitoutthere publish` now writes a package's git tag when that version is already live on the registry but untagged, instead of skipping it silently. The publish loop's already-published branch (`isPublished` → true) returned *before* the tag-creation step, so a version that reached the registry on a half-failed run (the run died after publishing but before tagging — e.g. a cancelled downstream job) was left published-but-untagged and never self-healed: piot derives "last released version" from git tags, so on every later run the package looked unreleased, fell back to `first_version` (already live), skipped again, and stayed stuck forever — while its dependents kept bumping and publishing, opening unflagged version skew. The skip branch now calls a shared, idempotent `ensureTag` (new `src/ensure-tag.ts`; a no-op when the tag already exists, and reused by the normal post-publish tag step), so a stuck package self-heals on its next release run. See [MIGRATIONS.md](./MIGRATIONS.md#publish-path-auto-heal-missing-tags). #407. (verified by: integration, unit/ubuntu-latest)
|
|
20
|
+
- Fixed: a `kind = "pypi"` `build = "maturin"` package whose wheel is **Python-version-independent** — `[tool.maturin].bindings = "bin"` (a Rust-binary wheel tagged `py3-none`) or a pyo3 `abi3` / `abi3-pyXY` extension (a stable-ABI wheel tagged `cp3x-abi3`) — no longer fans its wheel build across every CPython version `requires-python` / `python_versions` resolves to. Such a wheel is byte-identical regardless of the interpreter that built it, so the fan produced N duplicate wheels: each fanned row uploaded under its own `-py<ver>` artifact, and the documented consumer `pypi-publish` recipe's `actions/download-artifact … merge-multiple: true` then extracted N copies of the same wheel filename onto one path concurrently — a torn write that failed `twine check` with `zipfile.BadZipFile` (observed on a `bindings = "bin"`, `requires-python = ">=3.9"` package: six identical wheels per platform, the x86_64-linux copy losing its extraction race while macOS/aarch64 won theirs). The planner (`src/plan.ts`) now collapses the fan to a single wheel row per target for these packages — built once on the newest resolved interpreter, keeping the historical unsuffixed `<pkg>-wheel-<triple>` artifact name — and the new `src/wheel-abi.ts` detects version independence from the package's real `pyproject.toml` (`[tool.maturin].bindings`, `[tool.maturin].features`) and `Cargo.toml` (an `abi3` / `abi3-pyXY` feature on the `pyo3` / `pyo3-ffi` dependency). Ordinary per-version extension modules (no abi3, no `bindings = "bin"`) still fan and keep their `-py<ver>` suffixes; the sdist row is unchanged. Also saves the redundant N−1 wheel builds. Detection is conservative — an unrecognized abi3 setup (workspace-inherited `pyo3`, a target-specific dependency table) falls back to fanning, never worse than before. See [README → `kind = "pypi"`](./README.md#kind--pypi) and [MIGRATIONS.md](./MIGRATIONS.md#pypi-version-independent-wheels-build-once). #401. (verified by: integration, unit/ubuntu-latest)
|
|
21
|
+
- Fixed: `kind = "npm"` releases no longer abort mid-matrix on npm's Sigstore/Rekor transparency-log dedupe (`TLOG_CREATE_ENTRY_ERROR`, HTTP 409, "an equivalent entry already exists in the transparency log"). npm's retry-on-transient-network-error re-submits a byte-identical `--provenance` attestation after a flaky response, and Rekor rejects the duplicate with a 409. Previously only the registry-PUT edition of that race (`E403` "cannot publish over the previously published versions", matched by `looksLikePublishOverRace`) was tolerated, so the attestation edition fell through to the generic `throw` and killed the publish job — for a multi-platform (`napi` / `bundled-cli`) package that meant the first sub-package published, the next 409'd, and the remaining sub-packages **and the main package** were left unpublished (a partial release; observed on a 3-language repo's first `0.0.1`). Unlike the E403 shape, a 409 from Rekor does **not** by itself prove the artifact landed (the first submit may have written the transparency-log entry but still failed the registry upload), so both the main-package (`src/handlers/npm.ts`) and per-platform (`src/handlers/npm-platform.ts`) publish catches now re-probe `npm view` to disambiguate: present on the registry ⇒ treated as a benign duplicate (`already-published` / counted as published, and the cascade's `optionalDependencies` rewrite still runs); absent ⇒ a genuine partial publish, surfaced as an actionable error telling the operator to re-run the release so a fresh attestation (new `runID`/attempt ⇒ new Rekor entry) gets past the dedupe. The engine cannot retry the attestation in-process. See [MIGRATIONS.md](./MIGRATIONS.md#npm-tlog_create_entry_error-409-provenance-retry-race). #399. (verified by: unit/ubuntu-latest)
|
|
22
|
+
- Fixed: `kind = "npm"` `build = "bundled-cli"` packages that omit `[package.bundle_cli]` no longer crash the release at the `write-launcher` step. #298 kept the table opt-in — a bundled-cli npm package may either declare it (the reusable workflow cross-compiles the Rust binary and the engine authors `bin/<bin>.js`) or ship its own `scripts/build.cjs` + hand-authored launcher (the legacy path, which the cross-compile step skips by gating on `matrix.bundle_cli`). But #299 moved launcher generation into the engine and ran it on every bundled-cli main row while unconditionally dereferencing the optional table (`bundle_cli.bin`), so any package without it died with `TypeError: Cannot read properties of undefined (reading 'bin')` on the reusable workflow's `write-launcher` step — including any freshly-scaffolded consumer that hasn't adopted the declarative block, and the config shape carried by putitoutthere's own `polyglot-everything` fixture (the e2e mirror never runs `write-launcher`, so CI never caught it). `writeLauncherFromConfig` now no-ops launcher generation when `[package.bundle_cli]` is absent (the gate lives in the engine because the main row the step runs on never carries `bundle_cli` — `plan.ts` attaches it only to per-target bundled-cli rows), leaving the consumer's committed launcher and `package.json#bin` untouched; declarative consumers still get the generated launcher. See [MIGRATIONS.md](./MIGRATIONS.md#bundled-cli-launcher-generation-no-ops-without-packagebundle_cli). #299. (verified by: unit/ubuntu-latest)
|
|
23
|
+
- Fixed: `kind = "npm"` `build = "bundled-cli"` `bundle_cli — add Rust target`, `cargo build`, and `stage binary` steps now correctly map npm-flavor triples (`linux-x64-gnu`, `darwin-arm64`, `win32-x64-msvc`, …) to their Rust equivalents (`x86_64-unknown-linux-musl`, `aarch64-apple-darwin`, `x86_64-pc-windows-msvc`, …) before calling `rustup target add` and `cargo build --target`. Previously the bare `${TARGET//-linux-gnu/-linux-musl}` substitution was a no-op on npm-flavor triples — the substring `-linux-gnu` does not appear in `linux-x64-gnu` — so `rustup` received the raw npm triple and all five bundle_cli matrix rows failed immediately: `error: toolchain '...' does not support target 'linux-x64-gnu'`. (verified by: unit/bundle-cli-musl-target)
|
|
24
|
+
- Fixed: `kind = "npm"` `build = "bundled-cli"` Linux binary is now always statically linked (musl). The engine's `bundle_cli — stage binary` step previously ran **before** `npm run build --if-present`. A consumer build script that also runs `cargo build --target $TARGET` (the raw `-linux-gnu` triple) and copies the result to `build/<triple>/` would overwrite the engine's musl binary with a glibc-linked one; the `bundle_cli — verify` existence check passed silently and the glibc artifact was uploaded to the registry. The stage step now runs **after** `npm run build --if-present` in both the reusable workflow (`_matrix.yml`) and the e2e mirror (`e2e-fixture-job.yml`), so the engine's musl binary always wins. The `bundle_cli — verify` step in `_matrix.yml` also now asserts static linking (mirrors the check present in `e2e-fixture-job.yml`). (verified by: e2e/js-bundled-cli, unit/bundle-cli-musl-target)
|
|
25
|
+
|
|
13
26
|
### Added
|
|
14
27
|
|
|
28
|
+
- **`putitoutthere status` — a read-only command that reports registry-vs-tag drift, per package.** The registry is the source of truth and the planner derives "last released version" from git tags, so a half-failed run that publishes a version but never tags it strands the package: it then looks unreleased, skips its already-live `first_version` forever, can never bump, and its dependents drift ahead into unflagged version skew. `status` reconciles each package's latest tag against the registry's latest published version over the public registry APIs (crates.io `/api/v1/crates/<c>`, npm `registry.npmjs.org/<p>`, PyPI `/pypi/<p>/json`; no auth) and flags drift — `published, untagged`, `tagged, unpublished`, `version mismatch`, plus `in sync` / `unreleased` / `registry unreachable` (a network blip, reported but never gated). `--check` exits non-zero on any drift state (a CI gate); `--json` emits the rows as machine-readable JSON. It reuses the same `lastTag` resolver the release path runs, so the report cannot disagree with what a real release would see. See [README → Release health](./README.md#release-health) and [MIGRATIONS.md](./MIGRATIONS.md#status-registry-vs-tag-drift-report). #403. (verified by: integration, unit/ubuntu-latest)
|
|
29
|
+
- **`putitoutthere reconcile` — backfill the missing git tag for every package that is live on its registry but untagged.** The on-demand companion to the publish-path auto-heal (#407): auto-heal writes the tag on the already-published skip branch, but only fires for a package that is *in a publish run*, so a package whose globs never change again never enters one and stays stuck forever (no baseline tag → falls back to `first_version` → already published → skips forever → can never bump). `reconcile` heals it without a release, runnable in CI with the release job's permissions — replacing the manual `release_packages` bump or hand-rolled `git tag` ref-surgery that was the only prior recovery. It is a thin reader over the existing engine (the anti-drift rule in [non-goal #7](./notes/design-commitments.md#non-goals)): the same `computeStatus` (#403) that `status` reports *detects* the `published, untagged` drift, and the same idempotent `ensureTag` (#407) the publish path heals with *writes* the tag — so reconcile can never act on a drift `status` wouldn't show. The backfilled tag points at a sibling package already tagged at that version (the real release commit — e.g. a crate left untagged while its npm/PyPI siblings tagged the same merge) when one exists, else `HEAD`, so piot's "changed since last release" diff stays honest. Idempotent (a re-run is a no-op); `--dry-run` previews without writing; `--json` emits the actions. `--dry-run` is now accepted on `reconcile` while remaining rejected on `plan` / `publish` (unchanged from #244). See [README → Release health](./README.md#release-health) and [MIGRATIONS.md](./MIGRATIONS.md#reconcile-backfill-missing-tags). #403. (verified by: integration, unit/ubuntu-latest)
|
|
30
|
+
- **Preflight check: every cascaded `kind = "npm"` package's `package.json` `name` must match the configured `[[package]].name` (or the `npm` override).** Completes the #301 name-shape parity: pypi (`PIOT_PYPI_NAME_MISMATCH`) and crates (`PIOT_CRATES_NAME_MISMATCH`) already enforced this, but npm did not — an npm `package.json` whose `name` diverged from the configured identifier (with no `npm` override) was silently accepted. `npm publish` packs the manifest `name`, but the engine's idempotency probe (`npm view <name>`) and the tag / release-URL bookkeeping use the configured name (`npm` override ?? `[[package]].name`), so a divergence silently breaks idempotency and can publish under an unexpected name — the npm analogue of the pypi mismatch that failed a downstream release ~12 minutes in. The new `requirePackageJsonShape` runs alongside `requirePyprojectShape` / `requireCargoShape` in `src/publish.ts` before any side effects, and also via `runChecks` at PR time (`check.yml`); findings aggregate across every failing package so consumers fix them all in one round-trip. Scoped names are compared verbatim (`npm = "@scope/foo"` matches `package.json` `name = "@scope/foo"`). A missing or malformed `package.json` is skipped (a separate failure surface). Surfaces a new stable error code, `PIOT_NPM_NAME_MISMATCH`. Documented in [README → `kind = "npm"`](./README.md#kind--npm). See [MIGRATIONS.md](./MIGRATIONS.md#preflight-npm-package-name-must-match-configured-name). #301. (verified by: unit/ubuntu-latest, integration)
|
|
31
|
+
- **Manual release: the reusable workflow accepts a `release_packages` input that releases an explicit list of packages, bypassing change detection.** Releases were change-driven only — `plan` (`src/plan.ts`) diffs each package's `globs` against its last tag, so a repo with no new commits produced an empty matrix and could not release. That left no path for the re-release case: putitoutthere ships a release-pipeline bug, the bug is fixed, and downstream consumers must publish the affected packages again even though their own repos have no new code. `release.yml` now declares an optional `release_packages` `workflow_call` input — a comma-separated list of `name[@<bump|version>]` entries, e.g. `lib-core@minor, lib-py@1.4.0, lib-js`. Each entry is a package name optionally suffixed with `@<patch|minor|major>` (a bump applied to the last tag) or an explicit `@<X.Y.Z>` semver (used verbatim); a bare name defaults to a patch bump. When the input is non-empty, `plan` skips change detection and `depends_on` cascade entirely and emits a matrix for exactly the named packages — no incidental change-detected package is pulled in. The spec is plumbed through `_matrix.yml` to the `plan` job and through `release.yml`'s `publish` job (which re-runs `plan` internally and so must see the same spec) so both phases agree on the matrix. Consumers wire the input to a `workflow_dispatch` trigger in their own caller workflow to get a manual "release these packages" button. An explicit version is intentionally not compared against the last tag — re-releasing the same version is valid when a prior publish failed before reaching the registry, and the publish-phase already-published check is the right place to refuse a genuine collision. A named package absent from `putitoutthere.toml` is a hard error. Empty (the default) leaves the normal change-detected release path byte-identical. See [README → Manual release](./README.md#manual-release) and [MIGRATIONS.md](./MIGRATIONS.md#manual-release-via-release_packages). (verified by: unit/ubuntu-latest)
|
|
32
|
+
- **`kind = "pypi"` builds a wheel for every supported Python version, inferred from `requires-python`.** Previously every pypi package built exactly one wheel — for the single `python_version` workflow input (default `3.12`) — so a consumer whose `pyproject.toml` declared `requires-python = ">=3.10"` shipped a cp312-only wheel and `uvx <pkg>` failed on every other interpreter (the bug observed on `dirsql` 0.3.5). The planner (`src/plan.ts`) now resolves a per-package CPython version set and fans the pypi build matrix across it: each pypi matrix row carries a new `python_version` field, and `maturin` per-target wheel rows are emitted once per resolved version. The set is resolved by `src/python-versions.ts` — an explicit `python_versions` array in `putitoutthere.toml` wins; otherwise `[project].requires-python` is read from the package's `pyproject.toml` and expanded against the released CPython set discovered by the reusable workflow (`>=3.10` now includes `3.14`, supporting `>=`/`>`/`<=`/`<`/`==`/`!=`/`~=` clauses and `==3.*` wildcards); otherwise a single default (`3.12`) preserves the prior single-wheel behavior. The common case needs **zero configuration**. `_matrix.yml` feeds each row's `python_version` to `actions/setup-python` and, for `maturin`, to the build's `--interpreter` selection. Multi-version maturin wheel rows gain a `-py<ver>` artifact-name suffix (`<pkg>-wheel-<triple>-py3.10`) so per-version wheels for the same triple don't collide on upload; a single planned version keeps the historical unsuffixed `<pkg>-wheel-<triple>` name. The sdist and a pure-Python `hatch` wheel are version-agnostic and still built once. The `python_version` input on `release.yml` / `build.yml` / `_matrix.yml` is now **deprecated** — it no longer affects pypi builds and is retained only so existing callers don't break. See [README → `kind = "pypi"`](./README.md#kind--pypi) and [MIGRATIONS.md](./MIGRATIONS.md#pypi-multi-version-wheels). #369. (verified by: unit/ubuntu-latest)
|
|
33
|
+
- **Pre-merge crate-size check: `putitoutthere check` flags a `kind = "crates"` package whose `cargo package` `.crate` exceeds crates.io's 10 MiB upload limit.** crates.io rejects any `.crate` over `10485760` bytes with `413 Payload Too Large`, but `cargo publish` surfaces that only mid-release — after the verification build has compiled the crate and every transitive dep — which is exactly the "release surprise" the [no-surprises design commitment](./notes/design-commitments.md) exists to eliminate. `runChecks` now runs `cargo package --no-verify` for every cascaded `kind = "crates"` package at PR time and reports a finding carrying a new stable error code, `PIOT_CRATES_PACKAGE_TOO_LARGE`, when the resulting tarball is over the limit — so a tracked symlink dragging a build tree into the crate, or a missing `[package].exclude`, is caught on the PR that introduces it rather than on a release run weeks later. The check degrades to a no-op when `cargo` is unavailable or rejects the manifest; it never invents a size finding. See [MIGRATIONS.md](./MIGRATIONS.md#pre-merge-crate-size-check). #362. (verified by: integration, unit/ubuntu-latest)
|
|
34
|
+
- **Preflight checks: every cascaded package's manifest repository URL must resolve to the same `owner/repo` as `GITHUB_REPOSITORY`, and the GitHub repository running the workflow must be public.** Two new `requireRepoUrlMatch` / `requireRepoPublic` checks live in `src/preflight.ts`, fire alongside the existing `requireAuth` / `requireProvenanceMetadata` / `requireCratesMetadata` chain in `src/publish.ts` before any side effects, and the URL-match check also runs via `runChecks` at PR time (`check.yml`). The URL-match check parses `package.json#repository` (object + legacy string form), `Cargo.toml [package].repository`, and `pyproject.toml [project.urls]` against the GHA-provided `GITHUB_REPOSITORY` env var, normalising common GitHub URL shapes (`git+https://`, `https://`, `git@github.com:owner/repo`, with/without `.git`, with/without trailing slash). Catches the exact npm provenance 422 (`"repository.url is X, expected to match Y from provenance"`) that surfaces after artifact upload. The visibility check calls `https://api.github.com/repos/{owner}/{repo}` and hard-fails when the API reports `private: true` (or 404, which is indistinguishable from "private and the token can't see it" for our purposes); private repos break npm provenance attestation visibility for consumers and degrade the trusted-publisher story across the registries. Both checks no-op when `GITHUB_REPOSITORY` is unset (local CLI runs outside GHA) so `putitoutthere check` from a developer machine doesn't false-positive. Two new stable error codes: `PIOT_REPO_URL_MISMATCH`, `PIOT_REPO_PRIVATE`. (verified by: unit/ubuntu-latest)
|
|
35
|
+
- **Engine detects crates.io's first-publish Trusted-Publishing rejection and surfaces a `CARGO_REGISTRY_TOKEN` bootstrap hint.** crates.io's Trusted Publishing binds to an already-published crate: the OIDC mint succeeds and the exchanged token reaches cargo, but the registry returns 404 ("crate `<name>` does not exist or you do not have permission to publish to it") on the very first release of a new crate name. The previous fallthrough to "cargo publish failed" pushed consumers down a credentials rabbit-hole when the actual fix is one bootstrap publish via the classic-token fallback (already wired in #283). The crates handler now anchors on the 404-status line plus the registry's prose, surfaces a new stable error code `PIOT_CRATES_FIRST_PUBLISH_TP_REJECTED`, and prints the bootstrap hint inline alongside cargo's full stderr. Suppressed under the e2e seam (`PIOT_CRATES_REGISTRY_PRIMARY`) — the alt-registry doesn't model TP, so a 404 there is a different bug. The companion fixture catalogue ([`notes/upstream-behaviors.md`](./notes/upstream-behaviors.md)) indexes the response shape; the [`test/integration/fixtures/registry-responses/crates-io/publish-first-publish-tp-rejected.txt`](./test/integration/fixtures/registry-responses/crates-io/publish-first-publish-tp-rejected.txt) fixture pins the contract. See [MIGRATIONS.md](./MIGRATIONS.md#crates-first-publish-tp-rejection-detected). #284 (parent #296).
|
|
36
|
+
- **Registry-auth response fixtures + replay tests catalogue.** A new `test/integration/fixtures/registry-responses/` directory captures sanitised real-world response shapes for crates.io, npm, and PyPI auth flows the engine depends on (crates.io first-publish TP rejection #284, npm E403 over-publish race #281, npm 422 missing-repository rejection #281, PyPI mint-token invalid-publisher #252). Each fixture has a matching test in [`test/integration/registry-auth.integration.test.ts`](./test/integration/registry-auth.integration.test.ts) that replays the response and asserts the engine's reaction (or its architectural avoidance, in PyPI's case). The catalogue lives at [`notes/upstream-behaviors.md`](./notes/upstream-behaviors.md) and is the source of truth for "which upstream quirk forced this bit of engine code." New auth-related response shapes land as fixture + test + catalogue row; the existing publish-shape suites (#293/#294/#295) continue to cover what the registry *receives*. No consumer-visible behaviour change beyond the crates.io detector above. #296.
|
|
37
|
+
- **`kind = "npm"` `build = "bundled-cli"` consumers no longer need to author `bin/<bin>.js` — the reusable workflow generates it at build time.** The launcher's only per-consumer inputs are the package name and the configured `targets` list, both of which the engine has at plan time, so every consumer's hand-authored launcher was byte-identical modulo two values. `_matrix.yml`'s build job now invokes a new internal `putitoutthere write-launcher` CLI subcommand on the main row of each bundled-cli npm package (before `npm run build --if-present` runs); the subcommand writes `bin/<bundle_cli.bin>.js` and adds `package.json#bin` in place. Both writes are guarded by an "only if absent" check — existing consumer-authored launchers and existing `bin` fields are preserved, so the override path is the same file you'd already have committed. The generated launcher inlines the resolved platform-package name template (`{name}`, `{scope}`, `{base}` substituted at generation time; `{triple}` substituted at install time) into a backtick template literal; the Node `${platform}-${arch}` → triple table is the only per-install branching. Same shape as the README example bundled-cli consumers wrote by hand pre-#299, just authored by the workflow. Together with #298 (which absorbed the build script), bundled-cli npm's consumer surface is now: declare the package in `putitoutthere.toml`, register Trusted Publishers, push. The README's bundled-cli recipe loses its launcher code block; it gains a one-line "the workflow generates this" note plus the override hint. The CLI subcommand is internal (consumers compose with the reusable workflow, not directly with the CLI). See [README → Recipes → Bundled-CLI npm family](./README.md#bundled-cli-npm-family) and [MIGRATIONS.md](./MIGRATIONS.md#bundled-cli-launcher-generated-by-the-workflow). #299.
|
|
15
38
|
- **`kind = "npm"` `build = "bundled-cli"` now supports `[package.bundle_cli]` so the reusable workflow runs the Rust cross-compile itself.** Mirror of the pypi `[package.bundle_cli]` recipe shipped in #282 — the reusable workflow now, for every per-target row with `matrix.kind == 'npm' && matrix.build == 'bundled-cli' && matrix.bundle_cli && matrix.target != 'main'`: (1) `rustup target add ${{ matrix.target }}`, (2) `cargo build --release --target ${{ matrix.target }} --bin ${{ matrix.bundle_cli.bin }}` against `crate_path` (with optional `--features` / `--no-default-features` for crates that gate the CLI behind a Cargo feature), (3) copies the resulting binary (`.exe` on Windows) into the per-target staging directory the engine's npm-platform handler reads (`${{ matrix.artifact_path }}`, which is `<pkg.path>/build/<triple>` for single-mode rows and `<pkg.path>/build/<mode>-<triple>` for multi-mode rows), and (4) runs a permanent defense-in-depth build-content guard that asserts the staged binary exists before `actions/upload-artifact` runs. Schema: `bin` (required), `crate_path` (default `"."`), `features` (default `[]`), `no_default_features` (default `false`). The block is only valid when at least one bundled-cli entry exists in `build` and `targets` is non-empty. Existing consumers who authored a `scripts/build.cjs` keep working — the workflow runs first, then `npm run build --if-present` runs the consumer's script (which sees `build/<triple>/` already populated and probably no-ops); migrating to the declarative block is opt-in. Every bundled-cli npm consumer to date has written essentially the same cross-compile script and hit bugs at the seam between that script and the engine (#287 was the most recent). Absorbing the script closes the largest remaining piece of the consumer integration surface that exists for no architectural reason. See [README → Recipes → Bundled-CLI npm family](./README.md#bundled-cli-npm-family) and [MIGRATIONS.md](./MIGRATIONS.md#npm-bundle_cli-absorbed-into-the-reusable-workflow). #298.
|
|
39
|
+
- **Preflight checks: every cascaded `kind = "pypi"` package's `pyproject.toml` and every cascaded `kind = "crates"` (plus `bundle_cli` on `kind = "pypi"`) `Cargo.toml` must match the configured shape.** Mirrors the #280 / #290 pattern. The maturin / setuptools / hatchling / cargo CLIs surface these mismatches 10-20 minutes into a release run, deep into the verification build, with errors that don't name the precondition that failed; the new `requirePyprojectShape` + `requireCargoShape` checks live in `src/preflight.ts`, fire alongside `requireAuth` / `requireProvenanceMetadata` / `requireCratesMetadata` in `src/publish.ts` before any side effects, and also run via `runChecks` at PR time (`check.yml`). Findings aggregate across every failing package so consumers fix them all in one round-trip. Eight new stable error codes: `PIOT_PYPI_NAME_MISMATCH`, `PIOT_PYPI_BUILD_BACKEND_MISMATCH`, `PIOT_PYPI_DYNAMIC_VERSION_NO_BACKEND`, `PIOT_PYPI_MATURIN_INCLUDE_MISSING`, `PIOT_CRATES_NAME_MISMATCH`, `PIOT_CRATES_MISSING_BIN`, `PIOT_CRATES_FEATURE_NOT_DECLARED`, `PIOT_CRATES_WORKSPACE_VERSION_MISMATCH`. The `[build-system].build-backend` check only fires when the field is set *and* disagrees — a missing `[build-system]` table is left to pip's fallback behavior. The workspace-version walk bounds at `cwd` so it never escapes the repo. See [README → `kind = "crates"`](./README.md#kind--crates), [README → `kind = "pypi"`](./README.md#kind--pypi), and [MIGRATIONS.md](./MIGRATIONS.md#preflight-pyproject--cargo-shape). #301.
|
|
16
40
|
- **Reusable workflow `.github/workflows/check.yml` (`workflow_call`).** PR-time config-sanity gate. Wires into a consumer's `pull_request:` CI in one line: `uses: thekevinscott/putitoutthere/.github/workflows/check.yml@v0`. Drives the same `putitoutthere check` engine entry the publish path already uses (no parallel diagnostic code path — the operative rule from the [refreshed non-goal #8](./notes/design-commitments.md#non-goals)), so the consumer-observable check list is whatever `check` reports: `putitoutthere.toml` parse + schema + common-mistakes detector, unique package names, `depends_on` cycle / dangling-ref detection, `[[package]].path` directory exists, `globs` match a tracked file, `tag_format` collisions, npm `repository` field, crates `description` / `license`, pypi `pyproject.toml` + `bundle_cli` binary declaration, npm target triple mapping. Findings are aggregated into one round-trip rather than failing on the first. A few seconds per PR, no per-target build, no `setup-python` / `setup-rust`. Permissions: `contents: read` only — the workflow holds no publishable artifact and cannot mint a registry token. Complementary to `build.yml`: `check.yml` is the cheap config-sanity gate, `build.yml` is the heavier per-target build gate; wire both. See [README → check](./README.md#1b-recommended-drop-in-githubworkflowscheckyml) and [MIGRATIONS.md](./MIGRATIONS.md#new-checkyml-reusable-workflow-for-pr-time-config-sanity). #317 (workflow shell) + #319 (check list, shipped via #321).
|
|
17
41
|
- **Internal-only: `cargo-http-registry` alt-registry for e2e crates.io 429 fallback and first-publish coverage.** `e2e (polyglot-everything) / publish` reliably 429s on `cargo publish` for the polyglot rust fixture once a few PRs have burned crates.io's 24h-per-crate quota (the fixture publishes a fresh `0.0.<timestamp>` version on every run; the per-crate quota gets exhausted faster than it resets). `e2e-fixture-job.yml`'s publish job now installs and runs [`cargo-http-registry`](https://github.com/d-e-s-o/cargo-http-registry) (an off-the-shelf, auth-free cargo alt-registry — the "Verdaccio for cargo" the workflow has needed) as a background process on every crates-bearing matrix row. Two engine seams in `src/handlers/crates.ts` consume it: `PIOT_CRATES_REGISTRY_FALLBACK` retries `cargo publish --index <url>` against the alt-registry on a 429-rate-limit-shaped failure from real crates.io and emits a `::warning::` workflow command naming the URL so reviewers see the path was taken; `PIOT_CRATES_REGISTRY_PRIMARY` routes the publish *only* at the alt-registry (no real-crates.io attempt, no fallback) — reserved for any future `*-first-publish` crates fixture, symmetric with #304's npm/Verdaccio shape. Non-429 failures (auth, network, validation) surface verbatim — the fallback predicate is scoped narrowly to rate-limit prose. The handler now passes `--token <placeholder>` alongside `--index <url>` because cargo's CLI parser refuses to invoke publish with `--index` unless a token is explicitly named (a wart of the cargo CLI surface; cargo-http-registry accepts any token value). **Why `cargo-http-registry` and not a hand-rolled mock or another off-the-shelf registry**: an earlier iteration on this PR wired Kellnr; three CI rounds all 403'd because Kellnr (and alexandrie, ktra, cratery) are multi-tenant-shaped and reject fixture-style unrecognized identities. `cargo-http-registry`'s README explicitly disclaims auth ("if being asked to cargo login to the registry, any string may be used") and has a lightweight dep tree (tokio rt-only + warp + git2, no openssl-sys / sqlite-sys / aws-lc-sys), making `cargo install --locked` a ~70s cold cost — well inside the e2e job's existing budget. **No consumer surface changes**: both env vars are read only by the engine; the dogfood `release-rust.yml` continues to publish to real crates.io, and consumer `release.yml` flows that don't set either env var keep the publish path they have today byte-identical. See [MIGRATIONS.md](./MIGRATIONS.md#internal-cargo-http-registry-alt-registry-for-crates-e2e). #331.
|
|
18
42
|
- **Internal-only: first-publish e2e coverage via Verdaccio.** A new `js-vanilla-first-publish` fixture and matrix row in `.github/workflows/e2e-fixture.yml` exercises the empty-packument path (`npm view <name>` returns 404 before publish) that consumer-reported bugs cluster around. `e2e-fixture-job.yml`'s publish job now runs an in-job Verdaccio service container (`verdaccio/verdaccio:5`); per-fixture job isolation gives per-scenario wipe implicitly. The new fixture carries a `-placeholder` suffix on its package name that the workflow rewrites to `-${run_id}-${run_attempt}` at materialize time, so each scenario publishes against a packument that's truly empty. The post-publish tarball-verification step is now registry-agnostic — `REGISTRY_URL` env steers `npm view` at either real npm or Verdaccio with adapted retry/backoff. **No consumer surface changes**: `PIOT_NPM_REGISTRY` is an internal e2e seam read by `src/handlers/npm.ts` and `src/handlers/npm-platform.ts`; setting it bypasses provenance and the bootstrap-hint path. Steady-state e2e fixtures and consumer `release.yml` flows are untouched. See [MIGRATIONS.md](./MIGRATIONS.md#internal-verdaccio-e2e-coverage). #304 (parent #293).
|
|
@@ -27,6 +51,8 @@ are prefixed `**BREAKING**` and link to the matching section in
|
|
|
27
51
|
|
|
28
52
|
### Changed
|
|
29
53
|
|
|
54
|
+
- **`v0` floating tag now tracks main HEAD, not the latest registry release.** A new workflow [`advance-v0.yml`](./.github/workflows/advance-v0.yml) fires on every push to main, builds the action bundle, folds it into a tag-only commit (the same Fold shape `release-npm.yml` already uses — `dist-action/` is gitignored on main, so `v0` must point at a synthesized bundle commit for `uses: thekevinscott/putitoutthere@v0` to resolve to a runnable action), and force-moves `v0` to that commit. Trailer-gated registry releases still fire `release-npm.yml`, which cuts the permanent `putitoutthere-v0.x.y` tags; `v0` is the floating ref consumers compose against, and it stays fresh on every main commit instead of waiting for the next trailered release. Consumer impact: any commit on main — including test-only, docs, and dep-bump commits that don't carry a `release:` trailer — is immediately visible to `@v0` consumers on their next workflow resolve. Issue #199's original contract was "latest released commit in major line"; this PR flips it to "latest main HEAD bundle." Shared concurrency group `release` serializes the new workflow with `release-npm.yml` so they don't race when both fire on the same push. See [MIGRATIONS.md](./MIGRATIONS.md#v0-tracks-main-head). (verified by: unit/ubuntu-latest)
|
|
55
|
+
- **Default runner for `x86_64-pc-windows-msvc` is now `windows-2022` (was `windows-latest`).** GitHub is migrating `windows-latest` to Visual Studio 2026 between 2026-06-08 and 2026-06-15 ([changelog](https://github.blog/changelog/2026-05-14-github-actions-upcoming-image-migrations/), [runner-images #14016](https://github.com/actions/runner-images/issues/14016)); tracking the floating label would silently move every consumer's Windows release runs onto VS2026 on the cutover, with no opportunity to verify the new toolchain first. `defaultRunsOn` in `src/plan.ts` now returns `windows-2022` for any Windows-shaped triple (`x86_64-pc-windows-msvc`, `*-win32-*`, `*-msvc-*`); consumers who want a different image — `windows-2025` for VS2022-on-Server-2025, `windows-2025-vs2026` to surface VS2026 breakage early, or `windows-latest` to continue tracking the floating label — opt in per-target via the existing `{ triple, runner }` override in `putitoutthere.toml` (see [`README → kind = "npm"`](./README.md#kind--npm) `targets`). No consumer-side change required; existing per-target overrides keep winning. See [MIGRATIONS.md](./MIGRATIONS.md#windows-default-runner-pinned-to-windows-2022). #354. (verified by: unit/ubuntu-latest)
|
|
30
56
|
- **BREAKING: `kind = "pypi"` packages must declare `[project].dynamic = ["version"]` in `pyproject.toml`.** Static `[project].version = "..."` literals are now rejected at PR time by `putitoutthere check` and again at publish-time preflight, both surfacing the new stable error code `PIOT_PYPI_STATIC_VERSION`. The most common Python-publishing footgun: putitoutthere does not edit `pyproject.toml` at release time (per the [no version computation](./notes/design-commitments.md#non-goals) design commitment), so a literal silently shipped the previous release's wheel/sdist because the build backend read whatever was on disk. The fix is the same across the three supported backends: `hatch-vcs` (recommended), `setuptools-scm`, and maturin's Cargo.toml-driven path — all accept `dynamic = ["version"]`. `pypi.writeVersion` and the `putitoutthere write-version` CLI subcommand also reject the literal shape (previously they rewrote it in place). The `replacePyProjectVersion` helper, the "preserves comments around the literal" carve-out, and the sibling-pyproject-and-Cargo.toml double-rewrite path are removed; under the dynamic contract `Cargo.toml` alone is the bump target for maturin and `SETUPTOOLS_SCM_PRETEND_VERSION` is the handoff for hatch-vcs / setuptools-scm. See [README → Python version source](./README.md#python-version-source--required-shape) and [MIGRATIONS.md](./MIGRATIONS.md#pypi-pyproject-toml-must-declare-dynamic--version). #333.
|
|
31
57
|
- **BREAKING: `publish` throws on an empty matrix instead of exiting clean.** The reusable workflow's `publish` step now fails red when the plan is empty, with `PIOT_PUBLISH_EMPTY_PLAN` in the message. Previously, an empty plan logged `info: publish: plan is empty; nothing to release` and returned `ok: true` — leaving consumers with green release runs that hadn't published anything. Skips belong at the workflow gate (the `if:` on the publish job that reads the plan job's matrix output), not in the publish step. See [MIGRATIONS.md](./MIGRATIONS.md#publish-throws-on-empty-matrix).
|
|
32
58
|
|
|
@@ -40,6 +66,18 @@ are prefixed `**BREAKING**` and link to the matching section in
|
|
|
40
66
|
|
|
41
67
|
### Fixed
|
|
42
68
|
|
|
69
|
+
- **`bundle_cli` Linux musl builds now install `musl-tools` (the C cross-compiler) before `cargo build`.** `rustup target add` registers the Rust musl target but does not install `x86_64-linux-musl-gcc`. Crates that compile C source at build time — e.g. `libsqlite3-sys` with `features = ["bundled"]`, `openssl-sys` with `features = ["vendored"]` — invoke the C cross-compiler directly; without it, `cargo build` fails with `failed to find tool "x86_64-linux-musl-gcc": No such file or directory`. `_matrix.yml` and `e2e-fixture-job.yml` now run `sudo apt-get install -y musl-tools` and export `CC_<triple_with_underscores>=musl-gcc` to `$GITHUB_ENV` before the `cargo build` step, gated on Linux targets (`contains(matrix.target, 'linux')`). No consumer-side change required. See [MIGRATIONS.md](./MIGRATIONS.md#bundle_cli-musl-tools-c-cross-compiler). (verified by: unit/ubuntu-latest)
|
|
70
|
+
|
|
71
|
+
- **`bundle_cli` Linux binaries are now compiled as static musl regardless of the declared package target triple, so the staged binary runs on any modern Linux instead of requiring the build runner's glibc version.** Previously `cargo build --target $TARGET` ran directly on the GitHub-hosted runner. `ubuntu-latest` currently resolves to Ubuntu 24.04 (glibc 2.39), so the produced binary carried a `GLIBC_2.39` symbol requirement and failed at runtime on any older Linux (`./bin: /lib/x86_64-linux-gnu/libc.so.6: version 'GLIBC_2.39' not found`). This affected both the PyPI wheel path (where the `.so` is built inside a manylinux container but the `bundle_cli` binary was staged from outside it) and the npm path (where the binary is compiled directly on the runner). `_matrix.yml` and `e2e-fixture-job.yml` now derive a `BINARY_TARGET` from `matrix.target` by swapping `-linux-gnu*` → `-linux-musl*` and use that for the three steps that touch the binary's compile triple (`rustup target add`, `cargo build --target`, and the stage step's `src=…/target/<triple>/…` path); `matrix.target` itself is unchanged everywhere else, so npm platform-package names, napi build, wheel tags, and artifact names stay on the original `*-linux-gnu*` triple. Statically-linked musl binaries have no glibc floor and run on any Linux ≥ kernel 3.2 (Alpine, scratch containers, minimal distros included). Non-Linux and already-musl triples pass through unchanged (`-linux-gnu` substring absent → no-op substitution). Hit in the wild on `dirsql` 0.3.8: `pip install dirsql` and `pnpm dlx dirsql` both failed on Ubuntu 22.04 / Debian 12 / Amazon Linux 2. Consumers whose CLI crate dynamic-links system C libraries (`openssl-sys` without `vendored`, `libgit2-sys` without `vendored`, `libsqlite3-sys` without `bundled`, `libpq-sys`, `mysqlclient-sys`) will see a linker error on first release after upgrade — the fix is a one-line Cargo.toml change per case, documented in [README → Recipes → Bundled-CLI npm family](./README.md#bundled-cli-npm-family). See [MIGRATIONS.md](./MIGRATIONS.md#bundle_cli-linux-binaries-compiled-as-static-musl). #381. (verified by: unit/ubuntu-latest)
|
|
72
|
+
- **pypi `[package.bundle_cli]` binaries now report the planned release version.** The reusable workflow already bumped the maturin package's version source before building wheels, but it did not rewrite the separate CLI crate at `matrix.bundle_cli.crate_path` before the pypi bundle_cli `cargo build`. That meant the wheel metadata could be correct while the staged binary's `CARGO_PKG_VERSION` still came from the stale on-disk crate literal. `_matrix.yml` now runs the same internal `write-crate-version` step used by npm bundled-cli rows immediately before the pypi bundle_cli cargo build, so both PyPI wheels and npm platform packages compile their bundled CLI binaries against `matrix.version`. See [MIGRATIONS.md](./MIGRATIONS.md#pypi-bundle_cli-binary-embeds-the-release-version). #374. (verified by: unit/ubuntu-latest)
|
|
73
|
+
- **Open-ended `requires-python` inference now includes CPython 3.14.** The checked-in released-CPython list now runs through 3.14, so a package declaring `requires-python = ">=3.11"` plans cp311, cp312, cp313, and cp314 wheel rows instead of silently omitting cp314. Explicit `python_versions` overrides keep winning. See [MIGRATIONS.md](./MIGRATIONS.md#pypi-requires-python-includes-cpython-314). #375. (verified by: unit/ubuntu-latest)
|
|
74
|
+
|
|
75
|
+
- **npm `build = "bundled-cli"` packages now ship a binary that reports the planned release version.** The reusable workflow's `_matrix.yml` cross-compiled the bundled CLI by running `cargo build` against un-rewritten crate source, so the binary's `CARGO_PKG_VERSION` — what `<bin> --version` prints — was baked from whatever literal sat in the crate's `Cargo.toml`. A `@scope/cli-<triple>@0.3.5` platform package therefore shipped a binary that reported `0.2.7` (the on-disk crate literal) instead of the published version. The pypi/maturin path already solves the equivalent skew with a pre-build `write-version` step, but `write-version` is tied to maturin's dynamic-version contract — it requires a `pyproject.toml` declaring `dynamic = ["version"]`, which the npm bundled-cli crate has no reason to carry. `_matrix.yml` now runs a new internal `putitoutthere write-crate-version` CLI subcommand against `matrix.bundle_cli.crate_path` immediately before the npm bundled-cli `cargo build`, rewriting `[package].version` to `matrix.version` so the compiled binary embeds the planned version; the internal `e2e-fixture-job.yml` mirrors the step so the fixture suite exercises the same pre-build write a real consumer's release does. `write-crate-version` rewrites only `Cargo.toml`'s `[package].version` and fails loud when the manifest is missing or has no version field. The CLI subcommand is internal — consumers compose with the reusable workflow, not directly with the CLI. See [MIGRATIONS.md](./MIGRATIONS.md#npm-bundled-cli-binary-embeds-the-release-version). #366. (verified by: unit/ubuntu-latest)
|
|
76
|
+
- **`build = "bundled-cli"` npm platform packages now ship the CLI binary with the executable bit set.** The cross-compiled binary is staged into a per-triple platform package (`@scope/cli-<triple>`) and referenced via `package.json#main` — not as a `bin` entry — so npm never `chmod`s it, and the executable bit it had on the build runner is stripped crossing the GitHub Actions artifact upload/download boundary. The staged binary therefore packed as `0644`, and at runtime the generated launcher's `spawnSync` of the resolved binary failed with `EACCES` (`spawnSync .../node_modules/@scope/cli-<triple>/<bin> EACCES`). `synthesizePlatformPackage` now `chmod +x`es the staged binary for every non-Windows target before the package is packed/published, so `npm pack` preserves an executable `0755` mode. Hit in the wild on `thekevinscott/dirsql`. See [MIGRATIONS.md](./MIGRATIONS.md#bundled-cli-staged-binary-is-executable). #365. (verified by: unit/ubuntu-latest)
|
|
77
|
+
- **The repository-visibility preflight check no longer hard-fails a publish when the GitHub API call is rate-limited.** The `requireRepoPublic` check (added the same release) calls `https://api.github.com/repos/{owner}/{repo}` to confirm the repo is public. `checkRepoPublic` previously threw on any non-200/404 status, and no release-path workflow exposed `GITHUB_TOKEN` to the publish step — so the call always went out unauthenticated against the 60-requests/hour shared-IP limit. A multi-fixture e2e run (or any busy runner) exhausts that budget, the API returns `403`, and the publish aborted on a check that has nothing to say about visibility. Two changes: (1) `release.yml`, `release-npm.yml`, and `e2e-fixture-job.yml` now set `GITHUB_TOKEN: ${{ github.token }}` on the publish step, so the call is authenticated (5000 requests/hour); (2) `checkRepoPublic` now treats any non-200/404 response — and a network error — as indeterminate and non-fatal, emitting a `::warning::` instead of throwing. A genuinely private repo still returns `200` with `private: true` (or `404`) and is still hard-failed; only "we could not reach the API" stops blocking releases. (verified by: unit/ubuntu-latest)
|
|
78
|
+
- **`[package.bundle_cli]` now works end-to-end for crates that live in a cargo workspace.** Two bugs collided to make the standard workspace layout (a workspace root `Cargo.toml` with `[[bin]]` in a member crate — what `cargo new --workspace` and the polyglot Rust/Python recipe both produce) unsatisfiable: any `crate_path` value that made the PR-time check pass broke the build's stage step, and vice versa. (1) `putitoutthere check` parsed `<crate_path>/Cargo.toml` literally and never walked `[workspace].members`, so `crate_path = "."` (the default) on a workspace root reported the bin as missing even though `cargo build --bin <name>` resolves it transparently from anywhere in the workspace. (2) The reusable workflow's bundle_cli cargo-build step ran with `working-directory: ${{ matrix.bundle_cli.crate_path }}` but read the produced binary from `${{ matrix.bundle_cli.crate_path }}/target/...` — and cargo writes to the workspace-rooted target dir by default, so `crate_path = "packages/rust"` (the value that satisfied the check) couldn't satisfy the build. The check now walks `[workspace].members` (honoring `[workspace.package]` name inheritance for the implicit-binary rule) and the cargo invocation pins `--target-dir target` so the output path is deterministic by construction. Hit in the wild on `thekevinscott/dirsql`. See [MIGRATIONS.md](./MIGRATIONS.md#bundle_cli-cargo-workspace). #337.
|
|
79
|
+
- **The `[package.bundle_cli]` config check now resolves a member crate whose `[workspace].members` entry is a glob.** #337 taught `putitoutthere check` (and the pre-publish `PIOT_CRATES_MISSING_BIN` preflight) to walk `[workspace].members` so a workspace-root `crate_path = "."` resolves a member crate's `[[bin]]` — but it only handled *literal* member entries. cargo `members` entries are globs, and `members = ["packages/*"]` (a Rust core crate under `packages/rust` wrapped by sibling Python / npm packages) is the standard polyglot-repo shape. A glob entry never resolved to a literal `<member>/Cargo.toml`, so the member's `[[bin]]` went unseen and `crate_path = "."` was still rejected with `bundle_cli.bin "X" is not declared as a [[bin]]`. Both check tiers now expand `[workspace].members` globs against the filesystem the way cargo resolves them. No new error codes; no config-surface change. See [MIGRATIONS.md](./MIGRATIONS.md#bundle_cli-glob-workspace-members). #361. (verified by: unit/ubuntu-latest)
|
|
80
|
+
|
|
43
81
|
- **`check` no longer false-positives `PIOT_CRATES_MISSING_METADATA` on crates whose `Cargo.toml` inherits `description` / `license` / `license-file` from `[workspace.package]`.** Cargo's recommended pattern for shared crate metadata is `[workspace.package]` in the workspace root plus `<field>.workspace = true` in each member; `cargo publish` resolves the inheritance and embeds the literal value into `Cargo.toml.orig` before upload, so crates.io receives the resolved field. The check previously parsed the member `Cargo.toml` in isolation and treated `{ workspace: true }` as a missing string, flagging well-formed workspaces as if they would fail publish. `checkCratesMetadata` now walks up from each crate's `path` to find the nearest parent `Cargo.toml` with a `[workspace]` table and, when a field is declared as `<field>.workspace = true`, resolves it from `[workspace.package]` before deciding it's missing. Genuinely-missing fields (workspace root has no value for the inherited key, or no `[workspace.package]` block at all) still report through `PIOT_CRATES_MISSING_METADATA`. Hit in the wild in `thekevinscott/dirsql#177`. See [MIGRATIONS.md](./MIGRATIONS.md#crates-metadata-check-resolves-workspace-package-inheritance). #328.
|
|
44
82
|
|
|
45
83
|
- **`kind = "pypi"` + `build = "hatch"` now publishes a wheel alongside the sdist.** A pure-Python hatch package previously planned only a `target = "sdist"` row, so PyPI ended up with sdist-only and downstream `pip install` / `uvx ...` had to provision hatchling and run `python -m build` on a cold cache (several seconds per invocation) instead of doing a sub-second download-and-extract. `pypa/build`'s default on a pure-Python tree is to produce both an sdist and an any-platform wheel; the planner just wasn't asking for the wheel. The matrix now emits a second row per hatch package — `target = "any"`, `artifact_name = <name>-wheel-any` — and the reusable workflow's build step runs `python -m build --wheel --outdir dist` (with `SETUPTOOLS_SCM_PRETEND_VERSION` set, mirroring the sdist row's contract). Scoped to hatch per the issue: `build = "setuptools"` stays sdist-only (consumers who want a wheel there can switch to hatch or supply their own wheel), and `build = "maturin"` keeps its per-target wheel rows. Consumers' existing `pypi-publish` job picks up the new wheel automatically: the recommended `actions/download-artifact@v8` step in the README already uses `pattern: '*-wheel-*'`. Hit in the wild on `repo-name-checker` 0.1.0. See [MIGRATIONS.md](./MIGRATIONS.md#hatch-wheel-any-row). #324.
|
|
@@ -50,6 +88,8 @@ are prefixed `**BREAKING**` and link to the matching section in
|
|
|
50
88
|
|
|
51
89
|
- **First-publish bundled-cli / napi npm builds no longer fail on lockfile drift.** Consumers of the bundled-cli / napi shape declare `optionalDependencies` for `<name>-<triple>@<version>` platform packages that this pipeline publishes. On the very first publish those entries 404 on the registry; pnpm 10 silently drops 404'd optionals from the lockfile when it is regenerated locally; a subsequent CI run with `pnpm install --frozen-lockfile` (or `npm ci`) refuses because lockfile and `package.json` disagree. Both install steps in the reusable workflow — `_matrix.yml`'s build-matrix install and `release.yml`'s publish-job rebuild step (added in #256) — now self-heal: a failed strict install falls back to its non-strict form (`pnpm install --no-frozen-lockfile` / `npm install`) with a `::warning::` line in the run log naming the recovery. No consumer-side change required; lockfiles can stay committed and `optionalDependencies` can stay declared. Hit in the wild on `thekevinscott/darkfactory`'s first release (#integration-2026-05-bundled-cli). The README's [Bundled-CLI npm family](./README.md#bundled-cli-npm-family) recipe grew a `[!NOTE]` callout documenting the chicken-and-egg and the workflow's transparent recovery. See [MIGRATIONS.md](./MIGRATIONS.md#first-publish-bundled-cli-lockfile-self-heal).
|
|
52
90
|
|
|
91
|
+
- **Platform-package npm publishes (bundled-cli / napi) now honor the consumer's `.npmrc` for registry auth.** Earlier versions ran `npm publish` for each synthesized per-triple platform package with `cwd: <tempdir>` and no folder arg, so npm — which reads `.npmrc` from cwd upward — never saw the auth file the consumer's release path writes alongside the package. On real npm this was masked because OIDC trusted publishing supplies auth via `ACTIONS_ID_TOKEN_REQUEST_TOKEN` in the environment rather than from `.npmrc`. The gap surfaced for: (a) the `NPM_TOKEN` bootstrap path (#310) on a bundled-cli / napi family's first publish, where the consumer's `.npmrc` carries the long-lived token that platform PUTs needed too; (b) the internal Verdaccio e2e seam (#304), where the per-fixture `.npmrc` carries the Verdaccio auth token. Symptom in both cases: platform PUTs went out unauthenticated, registry returned a 4xx, the engine reported `npm publish (platform) failed` with a stderr that didn't always make the auth shape obvious. The fix runs `npm publish <stagingDir>` from `cwd: pkg.path` instead — matching what `src/handlers/npm.ts`'s main-package publish already does. No change to the OIDC happy path: platform publishes still get the same `--access=...`, `--tag=...`, `--provenance` (when `ACTIONS_ID_TOKEN_REQUEST_TOKEN` is set), and `--registry=...` (when `PIOT_NPM_REGISTRY` is set) flags they had before. Surfaced by the new `js-bundled-cli-first-publish` e2e fixture. See [MIGRATIONS.md](./MIGRATIONS.md#platform-publish-npmrc-lookup). #305.
|
|
92
|
+
|
|
53
93
|
- **pypi/maturin `[package.bundle_cli]` now actually ships the bundled binary inside published wheels.** The recipe was advertised as shipped in v0.2.0 (#217) — config parsing accepted `[package.bundle_cli]`, the planner attached it to per-target wheel rows, and `MIGRATIONS.md` named the two scaffolded build steps consumers should expect. None of those steps existed in `.github/workflows/_matrix.yml`. Consumers who declared the block (us, in `thekevinscott/dirsql`) shipped wheels missing the binary; `pip install <pkg> && <pkg> ...` failed at runtime with `FileNotFoundError`. The reusable workflow's build job now, for every per-target wheel row that carries `matrix.bundle_cli`: (1) `rustup target add ${{ matrix.target }}`, (2) `cargo build --release --target ${{ matrix.target }} --bin ${{ matrix.bundle_cli.bin }}` against `crate_path`, (3) copies the resulting binary into `${{ matrix.path }}/${{ matrix.bundle_cli.stage_to }}/` so maturin's `[tool.maturin].include` glob picks it up as wheel data, and (4) runs a permanent post-build wheel-content guard that opens the produced `.whl` and refuses to upload-artifact if `<stage_to>/<bin>` is missing. The guard is independent of staging — it catches any future regression where the cross-compile silently routes the binary to the wrong path. Consumers do not need to change their existing `[package.bundle_cli]` config or their `[tool.maturin].include` glob; the recipe just starts working. The cross-compile assumes the binary is buildable with a vanilla `cargo build --release --bin <bin>` (no `--features`, no env, no special flags); crates that gate the CLI behind a Cargo feature are not yet supported. The `.exe` suffix on Windows is handled. See [README → Recipes → Rust CLI inside a PyPI wheel](./README.md#rust-cli-inside-a-pypi-wheel) and [MIGRATIONS.md](./MIGRATIONS.md#bundle_cli-now-actually-stages-the-binary). #282.
|
|
54
94
|
|
|
55
95
|
- **`putitoutthere.toml` validation now names common typos in the failure message.** A consumer integration shipped a config with `version` at the file root, `[[packages]]` (plural), `registry =` instead of `kind =`, and `files =` instead of `globs =`. The raw zod errors (`Invalid input: expected object, received undefined; ...; Unrecognized keys: "version", "packages"`) were opaque enough that the engine source had to be re-read to recover. A pre-pass in `parseConfig` now detects each of those four mistakes by name and emits a hint that pairs the wrong shape with the right one, e.g. `top-level table is \`[[packages]]\` (plural) but should be \`[[package]]\` (singular)`. README's [Drop in `putitoutthere.toml`](./README.md#2-drop-in-putitoutthere-toml) section grew a four-row "wrong → right" table covering the same four traps so the docs and the engine name them the same way; a new `[!IMPORTANT]` callout in [Drop in `.github/workflows/release.yml`](./README.md#1-drop-in-githubworkflowsreleaseyml) warns consumers off `push: branches: [main]` triggers on lane CI workflows (which fire duplicate runs against the merge commit and contend for runners with `release.yml`); `1b.` was promoted from "Optional" to "Recommended" since `build.yml` is the cheapest place to catch a malformed config before merge. See [MIGRATIONS.md](./MIGRATIONS.md#friendly-config-error-hints).
|