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/MIGRATIONS.md
CHANGED
|
@@ -21,6 +21,1188 @@ Each section covers five things, in order:
|
|
|
21
21
|
|
|
22
22
|
## Unreleased
|
|
23
23
|
|
|
24
|
+
### `status`: registry-vs-tag drift report
|
|
25
|
+
|
|
26
|
+
**Summary.** New read-only command `putitoutthere status` reports, per
|
|
27
|
+
package, whether the latest git tag matches the registry's latest
|
|
28
|
+
published version, flagging drift — notably `published, untagged` (a
|
|
29
|
+
version live on the registry but missing its tag, which strands the
|
|
30
|
+
package). Reads public registry metadata only; no auth.
|
|
31
|
+
|
|
32
|
+
**Required changes.** None. Additive — a new command.
|
|
33
|
+
|
|
34
|
+
**Deprecations removed.** None.
|
|
35
|
+
|
|
36
|
+
**Behavior changes without code changes.** None — new surface.
|
|
37
|
+
|
|
38
|
+
**Verification.** Run `status` over a repo with a published-but-untagged
|
|
39
|
+
package: that package shows `published, untagged` and `status --check`
|
|
40
|
+
exits non-zero; a fully in-sync repo shows every package `in sync` and
|
|
41
|
+
exits zero. `--json` emits the same rows as JSON.
|
|
42
|
+
|
|
43
|
+
### reconcile: backfill missing tags
|
|
44
|
+
|
|
45
|
+
**Summary.** New command `putitoutthere reconcile` backfills the missing
|
|
46
|
+
git tag for every package that is live on its registry but untagged
|
|
47
|
+
(`status`'s `published, untagged` drift). It is the on-demand companion to
|
|
48
|
+
the publish-path auto-heal: auto-heal only fires for a package already in a
|
|
49
|
+
publish run, so a package whose globs never change again stays stuck;
|
|
50
|
+
`reconcile` heals it without a release. It reuses the same `computeStatus`
|
|
51
|
+
detection `status` reports and the same idempotent `ensureTag` the publish
|
|
52
|
+
path heals with — no parallel logic.
|
|
53
|
+
|
|
54
|
+
**Required changes.** None. Additive — a new command.
|
|
55
|
+
|
|
56
|
+
**Deprecations removed.** None.
|
|
57
|
+
|
|
58
|
+
**Behavior changes without code changes.** `--dry-run` is now accepted on
|
|
59
|
+
`reconcile` (it previews the heal without writing). It remains rejected on
|
|
60
|
+
`plan` / `publish`, unchanged from the #244 removal.
|
|
61
|
+
|
|
62
|
+
**Verification.** On a repo with a published-but-untagged package, run
|
|
63
|
+
`putitoutthere reconcile`: the missing tag is created (per the package's
|
|
64
|
+
`tag_format`) — pointed at a sibling package's tag commit for that version
|
|
65
|
+
when one exists, else `HEAD` — and pushed; `git tag` / GitHub show it, and
|
|
66
|
+
`status` then reports the package `in sync`. A second `reconcile` run is a
|
|
67
|
+
no-op. `reconcile --dry-run` reports what it would create without writing;
|
|
68
|
+
`--json` emits the actions.
|
|
69
|
+
|
|
70
|
+
### publish-path auto-heal: missing tags
|
|
71
|
+
|
|
72
|
+
**Summary.** `putitoutthere publish` now self-heals a missing git tag: when
|
|
73
|
+
a version is already live on the registry but has no tag, publish writes the
|
|
74
|
+
tag instead of skipping silently. Previously the already-published branch
|
|
75
|
+
returned before tagging, so a version that reached the registry on a
|
|
76
|
+
half-failed run (published, then the run died before the tag step) was
|
|
77
|
+
stranded published-but-untagged — and because piot derives "last released"
|
|
78
|
+
from tags, it skipped forever and could never bump, while dependents drifted
|
|
79
|
+
ahead into unflagged version skew.
|
|
80
|
+
|
|
81
|
+
**Required changes.** None. Purely additive behavior on the publish path.
|
|
82
|
+
|
|
83
|
+
**Deprecations removed.** None.
|
|
84
|
+
|
|
85
|
+
**Behavior changes without code changes.** On the next release run after
|
|
86
|
+
upgrading, any package that is live on a registry but missing its tag has the
|
|
87
|
+
tag created (and pushed) at the release commit, automatically. Idempotent:
|
|
88
|
+
packages already correctly tagged are untouched.
|
|
89
|
+
|
|
90
|
+
**Verification.** Re-run a release on a repo with a published-but-untagged
|
|
91
|
+
package: the run now creates the missing tag (per the package's `tag_format`;
|
|
92
|
+
visible via `git tag` / on GitHub) and the package resumes normal version
|
|
93
|
+
bumping on later releases.
|
|
94
|
+
|
|
95
|
+
### pypi version-independent wheels build once
|
|
96
|
+
|
|
97
|
+
**Summary.** A `kind = "pypi"` `build = "maturin"` package whose wheel is
|
|
98
|
+
Python-version-independent — `[tool.maturin].bindings = "bin"` (a
|
|
99
|
+
Rust-binary `py3-none` wheel) or a pyo3 `abi3` / `abi3-pyXY` extension (a
|
|
100
|
+
`cp3x-abi3` stable-ABI wheel) — used to be built once per CPython version
|
|
101
|
+
in the resolved set (inferred from `[project].requires-python`, or pinned
|
|
102
|
+
via `python_versions`). Because such a wheel is byte-identical no matter
|
|
103
|
+
which interpreter built it, the fan produced N duplicate wheels, and the
|
|
104
|
+
documented `pypi-publish` recipe's `merge-multiple: true` download then
|
|
105
|
+
race-corrupted the identical wheel filenames extracted onto one path
|
|
106
|
+
(`twine check` → `zipfile.BadZipFile`). The planner now collapses the fan
|
|
107
|
+
to a single wheel per target for these packages.
|
|
108
|
+
|
|
109
|
+
**Required changes.** None. Detection is automatic from your existing
|
|
110
|
+
`pyproject.toml` / `Cargo.toml`; no `putitoutthere.toml`, `release:`
|
|
111
|
+
trailer, or reusable-workflow input changes. The consumer `pypi-publish`
|
|
112
|
+
job is unchanged — its `pattern: '*-wheel-*'` still matches the (now
|
|
113
|
+
single) wheel artifact.
|
|
114
|
+
|
|
115
|
+
**Deprecations removed.** None.
|
|
116
|
+
|
|
117
|
+
**Behavior changes without code changes.** For a version-independent
|
|
118
|
+
maturin wheel resolving to more than one CPython version:
|
|
119
|
+
|
|
120
|
+
| | Before | After |
|
|
121
|
+
|---|--------|-------|
|
|
122
|
+
| Wheel build rows per target | one per resolved version (e.g. 6 for `>=3.9`) | one |
|
|
123
|
+
| Wheel artifact name | `<pkg>-wheel-<triple>-py<ver>` (per version) | `<pkg>-wheel-<triple>` (unsuffixed) |
|
|
124
|
+
| Build interpreter | each resolved version | newest resolved version only |
|
|
125
|
+
|
|
126
|
+
Ordinary per-version extension modules (no abi3, no `bindings = "bin"`)
|
|
127
|
+
are unaffected — they still fan and keep their `-py<ver>` suffixes. The
|
|
128
|
+
sdist row is unchanged. Detection is conservative: an abi3 setup the
|
|
129
|
+
engine doesn't recognize (a workspace-inherited `pyo3` dependency, a
|
|
130
|
+
`[target.'cfg(...)'.dependencies]` table) falls back to the prior fanning
|
|
131
|
+
behavior.
|
|
132
|
+
|
|
133
|
+
**Verification.** Run `putitoutthere plan` (or release) a maturin package
|
|
134
|
+
with `bindings = "bin"` or a pyo3 `abi3` feature and a `requires-python`
|
|
135
|
+
spanning multiple minors. The build matrix now shows a single
|
|
136
|
+
`<pkg>-wheel-<triple>` artifact per target instead of one per version, and
|
|
137
|
+
the `pypi-publish` job's `twine check` no longer fails with `BadZipFile`.
|
|
138
|
+
|
|
139
|
+
### npm `TLOG_CREATE_ENTRY_ERROR` (409) provenance retry race
|
|
140
|
+
|
|
141
|
+
**Summary.** `kind = "npm"` releases that publish with provenance
|
|
142
|
+
(`--provenance`, the OIDC trusted-publisher path) could abort mid-matrix
|
|
143
|
+
when npm's internal retry-on-transient-network-error re-submitted a
|
|
144
|
+
byte-identical attestation and Sigstore/Rekor rejected the duplicate with
|
|
145
|
+
`TLOG_CREATE_ENTRY_ERROR` (HTTP 409, "an equivalent entry already exists in
|
|
146
|
+
the transparency log"). Only the registry-PUT edition of that race (the
|
|
147
|
+
`E403` "cannot publish over the previously published versions" shape) was
|
|
148
|
+
tolerated; the attestation edition fell through to a hard failure, which
|
|
149
|
+
for a multi-platform (`napi` / `bundled-cli`) package left the remaining
|
|
150
|
+
sub-packages and the main package unpublished — a partial release. The
|
|
151
|
+
engine now recognizes the 409 and, because a 409 from Rekor does not by
|
|
152
|
+
itself prove the artifact reached the registry, re-probes `npm view` to
|
|
153
|
+
decide: present ⇒ benign duplicate (success); absent ⇒ a genuine partial
|
|
154
|
+
publish, reported as an actionable error.
|
|
155
|
+
|
|
156
|
+
**Required changes.** None. The behavior is automatic and applies to every
|
|
157
|
+
`kind = "npm"` package; no `putitoutthere.toml`, `release:` trailer, or
|
|
158
|
+
reusable-workflow input changes.
|
|
159
|
+
|
|
160
|
+
**Deprecations removed.** None.
|
|
161
|
+
|
|
162
|
+
**Behavior changes without code changes.** A `kind = "npm"` publish that
|
|
163
|
+
hits `TLOG_CREATE_ENTRY_ERROR` (409) no longer fails unconditionally:
|
|
164
|
+
|
|
165
|
+
| Situation | Before | After |
|
|
166
|
+
|-----------|--------|-------|
|
|
167
|
+
| 409 raised, package **is** on the registry | publish job fails (`npm publish (platform) failed: …`) | treated as `already-published` / counted as published; the cascade continues |
|
|
168
|
+
| 409 raised, package is **absent** from the registry | publish job fails with a bare npm stderr dump | publish job still fails, but with an actionable message: re-run the release to mint a fresh attestation |
|
|
169
|
+
|
|
170
|
+
In the genuine partial-publish case the engine cannot recover in-process —
|
|
171
|
+
an orphaned attestation needs a new `runID`/attempt to produce a fresh
|
|
172
|
+
Rekor entry — so re-running the release is the documented remedy.
|
|
173
|
+
|
|
174
|
+
**Verification.** Re-run a release whose previous attempt died on
|
|
175
|
+
`TLOG_CREATE_ENTRY_ERROR`. If the sub-package already landed, the run now
|
|
176
|
+
reports it as already-published and proceeds to the remaining packages
|
|
177
|
+
instead of aborting; if it never landed, the error names the package and
|
|
178
|
+
tells you to re-run (which mints a new attestation and gets past the
|
|
179
|
+
dedupe).
|
|
180
|
+
|
|
181
|
+
### Bundled-CLI launcher generation no-ops without `[package.bundle_cli]`
|
|
182
|
+
|
|
183
|
+
**Summary.** #299 moved npm bundled-cli launcher generation into the
|
|
184
|
+
engine and invoked it on every bundled-cli package's main row.
|
|
185
|
+
`writeLauncherFromConfig` assumed `[package.bundle_cli]` was always
|
|
186
|
+
present and dereferenced `bundle_cli.bin` unconditionally, so a
|
|
187
|
+
bundled-cli npm package that omits the table — the legacy
|
|
188
|
+
"bring-your-own `scripts/build.cjs` + hand-authored `bin/<bin>.js`"
|
|
189
|
+
shape that #298 explicitly kept opt-in — crashed the release at the
|
|
190
|
+
`write-launcher` step with `Cannot read properties of undefined
|
|
191
|
+
(reading 'bin')`. Launcher generation now no-ops when the table is
|
|
192
|
+
absent, mirroring the cross-compile step's existing `matrix.bundle_cli`
|
|
193
|
+
gate. The gate lives in the engine, not the workflow `if:`, because the
|
|
194
|
+
main row the step runs on never carries `bundle_cli` (`plan.ts` attaches
|
|
195
|
+
it only to per-target bundled-cli rows).
|
|
196
|
+
|
|
197
|
+
**Required changes.** None. A bundled-cli package that declares
|
|
198
|
+
`[package.bundle_cli]` is unchanged — the engine still generates
|
|
199
|
+
`bin/<bin>.js` and the `package.json#bin` entry. A package that omits
|
|
200
|
+
the table and ships its own launcher (the legacy path) no longer crashes;
|
|
201
|
+
the engine leaves its launcher alone. A consumer that *intended* the
|
|
202
|
+
declarative path and simply forgot the table should add it — the
|
|
203
|
+
cross-compile and launcher generation both key off it:
|
|
204
|
+
|
|
205
|
+
| Before | After |
|
|
206
|
+
|--------|-------|
|
|
207
|
+
| `build = "bundled-cli"`, `targets = [...]`, no `[package.bundle_cli]` → `write-launcher` TypeError | release succeeds; add `[package.bundle_cli]` with at least `bin` to opt into engine cross-compile + launcher generation |
|
|
208
|
+
|
|
209
|
+
**Deprecations removed.** None.
|
|
210
|
+
|
|
211
|
+
**Behavior changes without code changes.** A bundled-cli npm package
|
|
212
|
+
without `[package.bundle_cli]` previously failed its release at the
|
|
213
|
+
`write-launcher` step (as of #299); it now completes, with the engine
|
|
214
|
+
authoring no launcher for it.
|
|
215
|
+
|
|
216
|
+
**Verification.** Trigger a release for a bundled-cli npm package that
|
|
217
|
+
omits `[package.bundle_cli]`: the run completes (previously it failed at
|
|
218
|
+
the `write-launcher` step). A package that declares the table still gets
|
|
219
|
+
its engine-generated launcher.
|
|
220
|
+
|
|
221
|
+
### Preflight npm package name must match configured name
|
|
222
|
+
|
|
223
|
+
**Summary.** Preflight gains `requirePackageJsonShape`, the npm analogue
|
|
224
|
+
of `requirePyprojectShape` / `requireCargoShape` (#301). Every cascaded
|
|
225
|
+
`kind = "npm"` package's `package.json` `name` must equal the configured
|
|
226
|
+
`[[package]].name` (or the `npm` override). `npm publish` packs the
|
|
227
|
+
manifest `name`, but the engine's idempotency probe (`npm view <name>`)
|
|
228
|
+
and the tag / release-URL bookkeeping use the configured name — so a
|
|
229
|
+
divergence silently breaks idempotency and can publish under an
|
|
230
|
+
unexpected name. pypi and crates already enforced the equivalent
|
|
231
|
+
(`PIOT_PYPI_NAME_MISMATCH`, `PIOT_CRATES_NAME_MISMATCH`); this closes the
|
|
232
|
+
gap for npm. The check fires at publish time alongside the existing
|
|
233
|
+
`require*` family and at PR time via `check.yml`. Findings aggregate
|
|
234
|
+
across every failing package.
|
|
235
|
+
|
|
236
|
+
**Required changes.** None for well-formed manifests — `package.json`
|
|
237
|
+
`name` already matches the configured name in the common case. The repos
|
|
238
|
+
that trip the new check are those that used a path-style identifier as
|
|
239
|
+
`[[package]].name` (e.g. `js/foo`) without setting the `npm` override
|
|
240
|
+
while shipping `package.json` `name = "foo"`; those previously published
|
|
241
|
+
with a broken `npm view` idempotency check and a wrong reported URL, and
|
|
242
|
+
now get a fast preflight red instead. Fix by aligning the names or
|
|
243
|
+
declaring the override:
|
|
244
|
+
|
|
245
|
+
| Before | After |
|
|
246
|
+
|--------|-------|
|
|
247
|
+
| `name = "js/foo"` (no `npm` override), `package.json` `name = "foo"` | add `npm = "foo"` to the `[[package]]` entry (or rename one side so the two agree) |
|
|
248
|
+
|
|
249
|
+
The new error code:
|
|
250
|
+
|
|
251
|
+
| Code | Fires when |
|
|
252
|
+
|------|------------|
|
|
253
|
+
| `PIOT_NPM_NAME_MISMATCH` | `package.json`'s `name` differs from `[[package]].name` (or the `npm` override). |
|
|
254
|
+
|
|
255
|
+
**Deprecations removed.** None.
|
|
256
|
+
|
|
257
|
+
**Behavior changes without code changes.** An npm package with a name
|
|
258
|
+
divergence that previously published (under the manifest name, with a
|
|
259
|
+
broken `npm view` idempotency check and a wrong reported URL) now fails
|
|
260
|
+
at preflight with a fingerprintable `PIOT_NPM_NAME_MISMATCH` before any
|
|
261
|
+
side effect. Scoped names are compared verbatim (`npm = "@scope/foo"`
|
|
262
|
+
matches `package.json` `name = "@scope/foo"`). A missing or malformed
|
|
263
|
+
`package.json` is left to the other checks / the publish step.
|
|
264
|
+
|
|
265
|
+
**Verification.** Set an npm package's `package.json` `name` to something
|
|
266
|
+
other than its configured `[[package]].name` (with no `npm` override),
|
|
267
|
+
run `pnpm putitoutthere check` (or open a PR with `check.yml` wired), and
|
|
268
|
+
see `PIOT_NPM_NAME_MISMATCH` surface in seconds instead of mid-release.
|
|
269
|
+
|
|
270
|
+
### `_matrix.yml` build job primes a cargo cache (#391)
|
|
271
|
+
|
|
272
|
+
**Summary.** `_matrix.yml`'s build job previously ran every per-target
|
|
273
|
+
matrix cell without any Cargo cache. Each cell cold-compiled the full
|
|
274
|
+
Rust dep graph on every PR — even PRs that touched nothing Rust-side —
|
|
275
|
+
because `~/.cargo/registry` and the per-package `target/` dir started
|
|
276
|
+
empty on every runner. On a wide bundle_cli / napi / maturin matrix
|
|
277
|
+
this dominated wall-clock: 4-6 min per cell, ~8 min end-to-end on a
|
|
278
|
+
typical downstream consumer's `release-precheck.yml` run, with no
|
|
279
|
+
headroom against a 10-min PR CI gate.
|
|
280
|
+
|
|
281
|
+
The build job now runs `Swatinem/rust-cache@v2` immediately after
|
|
282
|
+
`actions/checkout`, gated on rows that actually invoke cargo:
|
|
283
|
+
|
|
284
|
+
- `pypi/maturin` non-sdist (maturin shells out to cargo)
|
|
285
|
+
- `npm/napi` (the consumer's `napi build` script calls cargo)
|
|
286
|
+
- `npm/bundled-cli` non-main (the engine's `cargo build` for the
|
|
287
|
+
staged CLI binary)
|
|
288
|
+
|
|
289
|
+
The cache is partitioned by `matrix.target` via `shared-key` so a
|
|
290
|
+
write to one target's slot doesn't blow away the next cell's, and
|
|
291
|
+
`workspaces` enumerates both `matrix.path` (the consumer's package
|
|
292
|
+
crate, where maturin / napi / single-crate bundled-cli compile) and
|
|
293
|
+
`matrix.bundle_cli.crate_path` (the bundle_cli crate when it lives
|
|
294
|
+
in a separate dir from `matrix.path` — the dirsql shape). Rows that
|
|
295
|
+
produce no cargo work (pypi sdist, pure-Python hatch wheels, npm
|
|
296
|
+
vanilla, bundled-cli `main`) skip the cache step entirely.
|
|
297
|
+
|
|
298
|
+
**Required changes.** None — the cache is internal to the reusable
|
|
299
|
+
workflow. No consumer config, YAML, or scripts need to change.
|
|
300
|
+
|
|
301
|
+
**Deprecations removed.** None.
|
|
302
|
+
|
|
303
|
+
**Behavior changes without code changes.** Per-target matrix cells
|
|
304
|
+
that ran cargo cold previously will now restore their dep graph from
|
|
305
|
+
GitHub Actions cache storage on the second and subsequent matching
|
|
306
|
+
runs (matching = same `matrix.target` + same Cargo.lock contents).
|
|
307
|
+
Cache storage accrues against the consumer's repository quota — the
|
|
308
|
+
same accounting as any other `actions/cache` consumer, no separate
|
|
309
|
+
billing. First run after a `Cargo.lock` change recompiles cold and
|
|
310
|
+
takes the same wall-clock as before. The acceptance criterion: a
|
|
311
|
+
second matrix run with no `Cargo.lock` change finishes the Rust
|
|
312
|
+
compile step in well under one minute per cell.
|
|
313
|
+
|
|
314
|
+
**Verification.** A `build.yml` (or `release.yml`) run's logs now
|
|
315
|
+
show a `cargo cache (#391)` step between `Set up job` and the first
|
|
316
|
+
Rust-touching step, on every row that invokes cargo. The step logs
|
|
317
|
+
either `Cache hit` (subsequent run, no `Cargo.lock` change) or
|
|
318
|
+
`Cache not found` (first run, or after a `Cargo.lock` change) and
|
|
319
|
+
records a `~/.cargo` + `target/` save at job end. Pure-Python sdist
|
|
320
|
+
and npm vanilla rows do not show the step at all.
|
|
321
|
+
|
|
322
|
+
### npm bundled-cli: npm-flavor triples now mapped to Rust triples
|
|
323
|
+
|
|
324
|
+
**Summary.** The `bundle_cli — add Rust target`, `cargo build`, and `stage binary` steps in both `_matrix.yml` and `e2e-fixture-job.yml` previously applied `${TARGET//-linux-gnu/-linux-musl}` directly to `matrix.target`. For `kind = "pypi"` rows `matrix.target` is already a Rust triple (`x86_64-unknown-linux-gnu`), so the substitution worked. For `kind = "npm"` `build = "bundled-cli"` rows `matrix.target` is an napi-rs-flavor triple (`linux-x64-gnu`, `darwin-arm64`, `win32-x64-msvc`, …); the substring `-linux-gnu` does not appear in any of these, so the substitution was a no-op and `rustup target add` received the raw npm triple and failed:
|
|
325
|
+
|
|
326
|
+
```
|
|
327
|
+
error: toolchain 'stable-x86_64-unknown-linux-gnu' does not support target 'linux-x64-gnu'
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
Each affected step now contains an explicit `case` statement mapping napi-rs npm triples to their Rust equivalents before the `gnu→musl` swap.
|
|
331
|
+
|
|
332
|
+
**Required changes.** None — the mapping is inside the reusable workflow. No consumer config, YAML, or scripts need to change.
|
|
333
|
+
|
|
334
|
+
**Deprecations removed.** None.
|
|
335
|
+
|
|
336
|
+
**Behavior changes without code changes.** `bundle_cli — add Rust target` now calls `rustup target add x86_64-unknown-linux-musl` (etc.) and succeeds. `cargo build --target` and the `stage binary` path-derivation use the correct Rust triple throughout.
|
|
337
|
+
|
|
338
|
+
**Verification.** A `bundle_cli` npm build job now shows `bundle_cli — add Rust target` completing without error, `bundle_cli — cargo build for linux-x64-gnu` compiling to `x86_64-unknown-linux-musl/release/<bin>`, and `bundle_cli — stage binary` logging `staged: target/x86_64-unknown-linux-musl/release/<bin> -> …`.
|
|
339
|
+
|
|
340
|
+
### `bundle_cli` stage step runs after consumer `npm run build`
|
|
341
|
+
|
|
342
|
+
**Summary.** The engine's `bundle_cli — stage binary` step previously ran
|
|
343
|
+
**before** `npm run build --if-present`. A consumer build script that also
|
|
344
|
+
runs `cargo build --target $TARGET` (the raw `-linux-gnu` triple) and copies
|
|
345
|
+
the result to `build/<triple>/` would overwrite the engine's musl binary with
|
|
346
|
+
a glibc-linked one; the existence-only verify check passed and the
|
|
347
|
+
dynamically-linked artifact shipped to the registry. The stage step now runs
|
|
348
|
+
**after** `npm run build --if-present` so the engine's statically-linked musl
|
|
349
|
+
binary always wins. The `bundle_cli — verify` step in `_matrix.yml` now also
|
|
350
|
+
asserts static linking (`file`/`ldd` check), mirroring the check already
|
|
351
|
+
present in `e2e-fixture-job.yml`.
|
|
352
|
+
|
|
353
|
+
**Required changes.** None — this is a pure step-reordering inside the
|
|
354
|
+
reusable workflow. No consumer-side YAML, config, or scripts need to change.
|
|
355
|
+
|
|
356
|
+
**Deprecations removed.** None.
|
|
357
|
+
|
|
358
|
+
**Behavior changes without code changes.** Consumer build scripts that stage
|
|
359
|
+
a binary to `build/<triple>/` as part of `npm run build` will have that binary
|
|
360
|
+
overwritten by the engine's musl binary. This was always the intended behavior;
|
|
361
|
+
the old ordering was a bug. Consumer scripts that only compile TypeScript or
|
|
362
|
+
do non-binary work are unaffected.
|
|
363
|
+
|
|
364
|
+
**Verification.** A `bundle_cli` Linux build job's logs now show:
|
|
365
|
+
1. `bundle_cli — cargo build for <triple>` (musl build, unchanged)
|
|
366
|
+
2. `npm run build --if-present` (consumer build step)
|
|
367
|
+
3. `bundle_cli — stage binary into build/<triple>` (engine stages musl binary)
|
|
368
|
+
4. `bundle_cli — verify … is statically linked` (passes)
|
|
369
|
+
|
|
370
|
+
### `bundle_cli` musl builds install `musl-tools` C cross-compiler
|
|
371
|
+
|
|
372
|
+
**Summary.** `rustup target add x86_64-unknown-linux-musl` registers the
|
|
373
|
+
Rust musl target but does not install the C cross-compiler
|
|
374
|
+
(`x86_64-linux-musl-gcc`). Crates that compile C source at build time —
|
|
375
|
+
`libsqlite3-sys` with `features = ["bundled"]`, `openssl-sys` with
|
|
376
|
+
`features = ["vendored"]`, and similar — invoke the C compiler directly
|
|
377
|
+
during `cargo build`; without `musl-gcc` present, the build fails with:
|
|
378
|
+
|
|
379
|
+
```
|
|
380
|
+
failed to find tool "x86_64-linux-musl-gcc": No such file or directory
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
The `musl-tools` apt package provides `musl-gcc` and is not pre-installed
|
|
384
|
+
on `ubuntu-latest`. `_matrix.yml` and `e2e-fixture-job.yml` now run
|
|
385
|
+
`sudo apt-get install -y musl-tools` and export `CC_<triple>=musl-gcc` to
|
|
386
|
+
`$GITHUB_ENV` before `cargo build`, gated on Linux targets.
|
|
387
|
+
|
|
388
|
+
**Required changes.** None — the step is added automatically by the
|
|
389
|
+
reusable workflow.
|
|
390
|
+
|
|
391
|
+
**Deprecations removed.** None.
|
|
392
|
+
|
|
393
|
+
**Behavior changes without code changes.** CLI crates that compile C
|
|
394
|
+
source and previously had to run a custom `build` script to install
|
|
395
|
+
`musl-tools` may be able to remove that script.
|
|
396
|
+
|
|
397
|
+
**Verification.** On a bundle_cli Linux build, the workflow now prints
|
|
398
|
+
`sudo apt-get install -y musl-tools` before the `cargo build` step.
|
|
399
|
+
Crates with C deps (e.g. `features = ["bundled"]` on `rusqlite`) compile
|
|
400
|
+
successfully without consumer-side workarounds.
|
|
401
|
+
|
|
402
|
+
### `bundle_cli` Linux binaries compiled as static musl
|
|
403
|
+
|
|
404
|
+
**Summary.** `bundle_cli`'s Linux cross-compile step previously ran
|
|
405
|
+
`cargo build --target $TARGET` directly on the GitHub-hosted runner.
|
|
406
|
+
That runner's glibc (currently 2.39 on Ubuntu 24.04) got baked into
|
|
407
|
+
the produced binary as a hard runtime requirement, so any older Linux
|
|
408
|
+
at install time failed with `./bin: /lib/x86_64-linux-gnu/libc.so.6:
|
|
409
|
+
version 'GLIBC_2.39' not found`. The fix derives a `BINARY_TARGET`
|
|
410
|
+
from `matrix.target` by substituting `-linux-gnu*` → `-linux-musl*`
|
|
411
|
+
and uses that for the three workflow steps that touch the binary's
|
|
412
|
+
compile triple. Statically-linked musl binaries have no glibc floor.
|
|
413
|
+
The package's declared target triple (used for npm platform-package
|
|
414
|
+
names, napi builds, wheel tags, artifact names) is unchanged; only
|
|
415
|
+
the binary inside switches compile triple.
|
|
416
|
+
|
|
417
|
+
**Required changes.** None for the common case — the workflow makes
|
|
418
|
+
the swap automatically. The exception: CLI crates that
|
|
419
|
+
dynamic-link a system C library through default cargo features will
|
|
420
|
+
see a linker error on the first release after upgrade. The static
|
|
421
|
+
musl build cannot satisfy a dynamic link against the host's glibc-world
|
|
422
|
+
libraries. The fix is a one-line `Cargo.toml` change per case:
|
|
423
|
+
|
|
424
|
+
| Symptom (cargo error mentions) | Fix in your CLI's `Cargo.toml` |
|
|
425
|
+
|--------------------------------|--------------------------------|
|
|
426
|
+
| `openssl-sys`, `libssl.so`, OpenSSL | Switch to `rustls` (`reqwest = { default-features = false, features = ["rustls-tls"] }`), or pin `openssl = { features = ["vendored"] }` |
|
|
427
|
+
| `git2`, `libgit2` | `git2 = { features = ["vendored-openssl", "vendored-libgit2"] }` |
|
|
428
|
+
| `libsqlite3-sys`, SQLite | `rusqlite = { features = ["bundled"] }` (or `libsqlite3-sys = { features = ["bundled"] }`) |
|
|
429
|
+
| `libpq`, Postgres client | Swap `postgres-native-tls` for `postgres-rustls`, or use `sqlx` with the `rustls` feature |
|
|
430
|
+
| `libmysqlclient`, MySQL client | Same — prefer a pure-Rust client; `mysqlclient-sys` has no clean static path |
|
|
431
|
+
|
|
432
|
+
The musl build fails loudly at release time when one of these is
|
|
433
|
+
missed — there is no silent broken-binary failure mode. A blocked
|
|
434
|
+
release is the worst outcome.
|
|
435
|
+
|
|
436
|
+
**Deprecations removed.** None.
|
|
437
|
+
|
|
438
|
+
**Behavior changes without code changes.** The bundled binary inside
|
|
439
|
+
a Linux package built before this release required the build
|
|
440
|
+
runner's glibc (currently 2.39); after this release it has no glibc
|
|
441
|
+
requirement and runs on any Linux ≥ kernel 3.2, including Alpine,
|
|
442
|
+
NixOS in musl mode, scratch containers, and any host whose glibc is
|
|
443
|
+
older than the build runner's. Package identifiers (npm platform-
|
|
444
|
+
package names like `@scope/cli-linux-x64-gnu`, PyPI wheel tags,
|
|
445
|
+
artifact upload names) are unchanged — the substitution applies only
|
|
446
|
+
to the binary's compile triple.
|
|
447
|
+
|
|
448
|
+
**Verification.** Install a `bundle_cli`-enabled package from any
|
|
449
|
+
pre-Ubuntu-24.04 Linux (Ubuntu 22.04, Debian 12, Amazon Linux 2,
|
|
450
|
+
Alpine) and run the CLI — no `GLIBC_2.x not found` error. On the
|
|
451
|
+
build side, the cargo invocation in CI now prints
|
|
452
|
+
`cargo build --release --target x86_64-unknown-linux-musl ...`
|
|
453
|
+
instead of `-gnu` for Linux rows; `file` on the produced binary
|
|
454
|
+
reports `statically linked` instead of `dynamically linked`.
|
|
455
|
+
|
|
456
|
+
### pypi `requires-python` includes CPython 3.14
|
|
457
|
+
|
|
458
|
+
**Summary.** Open-ended `requires-python` inference now expands against
|
|
459
|
+
putitoutthere's checked-in released-CPython list through CPython 3.14.
|
|
460
|
+
This fixes the stale-tail failure mode where `requires-python = ">=3.11"`
|
|
461
|
+
emitted cp311/cp312/cp313 wheels but omitted cp314 after Python 3.14 was
|
|
462
|
+
released.
|
|
463
|
+
|
|
464
|
+
**Required changes.** None.
|
|
465
|
+
|
|
466
|
+
| Before | After |
|
|
467
|
+
|--------|-------|
|
|
468
|
+
| `requires-python = ">=3.11"` planned wheels only through cp313. | `requires-python = ">=3.11"` plans cp311, cp312, cp313, and cp314, unless `python_versions` is explicitly set. |
|
|
469
|
+
|
|
470
|
+
**Deprecations removed.** None.
|
|
471
|
+
|
|
472
|
+
**Behavior changes without code changes.** Consumers with open-ended
|
|
473
|
+
`requires-python` ranges may see an extra cp314 wheel row. Explicit
|
|
474
|
+
`python_versions` overrides are unchanged and still pin the exact wheel
|
|
475
|
+
set.
|
|
476
|
+
|
|
477
|
+
**Verification.** Push a release for a `kind = "pypi"` package whose
|
|
478
|
+
`pyproject.toml` declares `requires-python = ">=3.11"`; the build matrix
|
|
479
|
+
should include a `python_version: "3.14"` row, and the published PyPI
|
|
480
|
+
release should include a cp314 wheel.
|
|
481
|
+
|
|
482
|
+
### Manual release via `release_packages`
|
|
483
|
+
|
|
484
|
+
**Summary.** `release.yml` gained an optional `release_packages`
|
|
485
|
+
`workflow_call` input. When set, it triggers a manual release of an
|
|
486
|
+
explicit list of packages, bypassing change detection entirely. The
|
|
487
|
+
motivating case: putitoutthere ships a release-pipeline bug, the bug is
|
|
488
|
+
fixed, and downstream consumers must re-release the affected packages
|
|
489
|
+
even though their own repos have no new commits since the last tag —
|
|
490
|
+
the change-detected path emits an empty matrix in that state and cannot
|
|
491
|
+
release. The input value is a comma-separated list of
|
|
492
|
+
`name[@<bump|version>]` entries; each entry is a package name optionally
|
|
493
|
+
suffixed with `@<patch|minor|major>` (bump the last tag) or an explicit
|
|
494
|
+
`@<X.Y.Z>` semver (used verbatim). A bare name defaults to a patch bump.
|
|
495
|
+
Only the named packages are planned — no `depends_on` cascade, no
|
|
496
|
+
change-detected packages pulled in.
|
|
497
|
+
|
|
498
|
+
**Required changes.** None — the input is optional and defaults to
|
|
499
|
+
empty, which leaves the normal change-detected release path unchanged.
|
|
500
|
+
To get a manual-release button, wire the input to a `workflow_dispatch`
|
|
501
|
+
trigger in your caller `release.yml`:
|
|
502
|
+
|
|
503
|
+
| Before | After |
|
|
504
|
+
|--------|-------|
|
|
505
|
+
| <pre>on:<br> push: { branches: [main] }<br><br>jobs:<br> release:<br> uses: thekevinscott/putitoutthere/.github/workflows/release.yml@v0</pre> | <pre>on:<br> push: { branches: [main] }<br> workflow_dispatch:<br> inputs:<br> release_packages:<br> description: 'Comma-separated name[@bump\|version] list'<br> required: true<br><br>jobs:<br> release:<br> uses: thekevinscott/putitoutthere/.github/workflows/release.yml@v0<br> with:<br> release_packages: ${{ inputs.release_packages }}</pre> |
|
|
506
|
+
|
|
507
|
+
The push-triggered run passes an empty `release_packages` (the `inputs`
|
|
508
|
+
context is empty outside `workflow_dispatch`), so the normal path keeps
|
|
509
|
+
working.
|
|
510
|
+
|
|
511
|
+
**Deprecations removed.** None.
|
|
512
|
+
|
|
513
|
+
**Behavior changes without code changes.** None. The new behavior is
|
|
514
|
+
gated entirely on the new input being non-empty.
|
|
515
|
+
|
|
516
|
+
**Verification.** Trigger the workflow from the Actions tab with
|
|
517
|
+
`release_packages` set to a known package (e.g. `lib-core@patch`).
|
|
518
|
+
Confirm the plan job's matrix contains exactly that package and the
|
|
519
|
+
publish job tags and publishes it at the expected version.
|
|
520
|
+
|
|
521
|
+
### pypi multi-version wheels
|
|
522
|
+
|
|
523
|
+
**Summary.** `kind = "pypi"` packages now build a wheel for every
|
|
524
|
+
CPython version they support, instead of a single wheel for the
|
|
525
|
+
`python_version` workflow input. The version set is resolved per
|
|
526
|
+
package: an explicit `python_versions` array in `putitoutthere.toml`
|
|
527
|
+
wins; otherwise it is inferred from `[project].requires-python` in the
|
|
528
|
+
package's `pyproject.toml`; otherwise a single default (`3.12`) is
|
|
529
|
+
used. The build matrix fans `maturin` per-target wheel rows across the
|
|
530
|
+
resolved set. This closes the incomplete-coverage bug where a package
|
|
531
|
+
declaring `requires-python = ">=3.10"` shipped a cp312-only wheel and
|
|
532
|
+
failed to install on every other interpreter.
|
|
533
|
+
|
|
534
|
+
**Required changes.** None for the default path — `requires-python`
|
|
535
|
+
inference is automatic. Optionally pin a subset:
|
|
536
|
+
|
|
537
|
+
| Before | After |
|
|
538
|
+
|--------|-------|
|
|
539
|
+
| _(no knob; one wheel at `python_version`)_ | `python_versions = ["3.12", "3.13"]` under a `[[package]]` with `kind = "pypi"` |
|
|
540
|
+
|
|
541
|
+
Consumers whose caller-side `pypi-publish` job collects wheels from the
|
|
542
|
+
downloaded artifacts directory need no change as long as it globs all
|
|
543
|
+
wheel artifacts (the recommended recipe already does); multi-version
|
|
544
|
+
`maturin` wheel artifacts are now named
|
|
545
|
+
`<pkg>-wheel-<triple>-py<ver>` rather than `<pkg>-wheel-<triple>`. A
|
|
546
|
+
single planned version keeps the unsuffixed name.
|
|
547
|
+
|
|
548
|
+
**Deprecations removed.** None. The `python_version` input on
|
|
549
|
+
`release.yml`, `build.yml`, and `_matrix.yml` is now deprecated — it no
|
|
550
|
+
longer affects pypi builds — but is retained so existing callers do not
|
|
551
|
+
break. Remove it from your `with:` block at leisure.
|
|
552
|
+
|
|
553
|
+
**Behavior changes without code changes.** A pypi package whose
|
|
554
|
+
`requires-python` spans multiple versions now produces multiple wheels
|
|
555
|
+
where it previously produced one. The `python_version` workflow input
|
|
556
|
+
is inert for pypi builds.
|
|
557
|
+
|
|
558
|
+
**Verification.** Push a release for a `kind = "pypi"` package whose
|
|
559
|
+
`pyproject.toml` declares `requires-python = ">=3.11"`; the build job
|
|
560
|
+
fans into one wheel row per released version (for example `3.11`,
|
|
561
|
+
`3.12`, `3.13`, `3.14`), and the
|
|
562
|
+
published PyPI release carries a wheel for each.
|
|
563
|
+
|
|
564
|
+
### pypi bundle_cli binary embeds the release version
|
|
565
|
+
|
|
566
|
+
**Summary.** The reusable workflow's pypi `[package.bundle_cli]` path
|
|
567
|
+
already rewrote the maturin package's version source before building
|
|
568
|
+
wheels, but the cross-compiled CLI binary comes from the separate crate
|
|
569
|
+
at `bundle_cli.crate_path`. `cargo build` bakes `CARGO_PKG_VERSION`
|
|
570
|
+
from that crate's on-disk `Cargo.toml`, so a wheel could publish as
|
|
571
|
+
`0.3.6` while its bundled CLI reported an older literal such as
|
|
572
|
+
`0.2.7`. The build matrix now rewrites the bundle_cli crate's
|
|
573
|
+
`[package].version` to `matrix.version` immediately before the pypi
|
|
574
|
+
bundle_cli `cargo build`, matching the npm bundled-cli fix.
|
|
575
|
+
|
|
576
|
+
**Required changes.** None. Consumers who declare `kind = "pypi"`,
|
|
577
|
+
`build = "maturin"`, and `[package.bundle_cli]` get the corrected
|
|
578
|
+
behavior on their next release run against `@v0`; no
|
|
579
|
+
`putitoutthere.toml`, trailer, or consumer-side YAML change is needed.
|
|
580
|
+
|
|
581
|
+
**Deprecations removed.** None.
|
|
582
|
+
|
|
583
|
+
**Behavior changes without code changes.** Same config, different
|
|
584
|
+
artifact: the CLI binary staged into each wheel now reports the
|
|
585
|
+
planned release version from `--version` (and any other
|
|
586
|
+
`CARGO_PKG_VERSION`-derived output) instead of the stale literal in
|
|
587
|
+
the CLI crate manifest.
|
|
588
|
+
|
|
589
|
+
**Verification.** Release a pypi maturin package that declares
|
|
590
|
+
`[package.bundle_cli]`, install one of the published wheels, and run
|
|
591
|
+
the bundled CLI's `--version`: it reports `<version>`, matching the
|
|
592
|
+
published wheel metadata.
|
|
593
|
+
|
|
594
|
+
### npm bundled-cli binary embeds the release version
|
|
595
|
+
|
|
596
|
+
**Summary.** The reusable workflow's npm `build = "bundled-cli"` path
|
|
597
|
+
cross-compiled the bundled CLI from un-rewritten crate source, so the
|
|
598
|
+
binary's `CARGO_PKG_VERSION` (what `<bin> --version` prints) was baked
|
|
599
|
+
from the literal `[package].version` in the crate's `Cargo.toml`
|
|
600
|
+
rather than the planned release version. A `@scope/cli-<triple>@0.3.5`
|
|
601
|
+
platform package could ship a binary that reported `0.2.7`. The build
|
|
602
|
+
matrix now rewrites the crate's `[package].version` to `matrix.version`
|
|
603
|
+
before `cargo build` runs, mirroring the pre-build `write-version`
|
|
604
|
+
step the pypi/maturin path already uses.
|
|
605
|
+
|
|
606
|
+
**Required changes.** None. The fix lives entirely inside the reusable
|
|
607
|
+
workflow's build matrix. Consumers who declare a `kind = "npm"`
|
|
608
|
+
`build = "bundled-cli"` package with `[package.bundle_cli]` get the
|
|
609
|
+
corrected behavior on their next release run against `@v0`; no
|
|
610
|
+
`putitoutthere.toml`, trailer, or consumer-side YAML change is needed.
|
|
611
|
+
|
|
612
|
+
**Deprecations removed.** None.
|
|
613
|
+
|
|
614
|
+
**Behavior changes without code changes.** Same config, different
|
|
615
|
+
artifact: the cross-compiled binary inside each per-platform package
|
|
616
|
+
now reports the planned release version from `--version` (and any
|
|
617
|
+
other `CARGO_PKG_VERSION`-derived output) instead of the stale literal
|
|
618
|
+
on disk in the crate manifest.
|
|
619
|
+
|
|
620
|
+
**Verification.** Release a `kind = "npm"` `build = "bundled-cli"`
|
|
621
|
+
package, extract one per-platform package
|
|
622
|
+
(`@scope/cli-<triple>@<version>`), and run the bundled binary's
|
|
623
|
+
`--version`: it reports `<version>`, matching the published package.
|
|
624
|
+
### Bundled-cli staged binary is executable
|
|
625
|
+
|
|
626
|
+
**Summary.** For `kind = "npm"` packages with a `build = "bundled-cli"`
|
|
627
|
+
entry, the reusable workflow cross-compiles the CLI and stages it into
|
|
628
|
+
a per-triple platform package (`@scope/cli-<triple>`). The staged
|
|
629
|
+
binary was packed with mode `0644` — no executable bit. npm only sets
|
|
630
|
+
the executable bit on `bin` entries; the bundled binary is referenced
|
|
631
|
+
via `package.json#main`, so npm never `chmod`s it, and the bit it had
|
|
632
|
+
on the build runner is stripped crossing the GitHub Actions artifact
|
|
633
|
+
upload/download boundary. At runtime the generated launcher's
|
|
634
|
+
`spawnSync` of the resolved binary failed with `EACCES`. The workflow
|
|
635
|
+
now `chmod +x`es the staged binary for non-Windows targets before the
|
|
636
|
+
platform package is packed/published.
|
|
637
|
+
|
|
638
|
+
**Required changes.** None. The fix is internal to the reusable
|
|
639
|
+
workflow's npm bundled-cli publish path.
|
|
640
|
+
|
|
641
|
+
**Deprecations removed.** None.
|
|
642
|
+
|
|
643
|
+
**Behavior changes without code changes.** Per-triple platform
|
|
644
|
+
packages published for non-Windows targets now ship the CLI binary
|
|
645
|
+
with mode `0755` instead of `0644`. Consumers who already worked
|
|
646
|
+
around the bug (a `postinstall` `chmod`, or a launcher that `chmod`s
|
|
647
|
+
before `spawnSync`) can drop that workaround; leaving it in place is
|
|
648
|
+
harmless.
|
|
649
|
+
|
|
650
|
+
**Verification.** Publish a `build = "bundled-cli"` npm family, then
|
|
651
|
+
`npm install` it and run the CLI — `npx <bin> --version` succeeds
|
|
652
|
+
instead of failing with `spawnSync ... EACCES`. Inspecting the
|
|
653
|
+
published platform tarball
|
|
654
|
+
(`tar -tvzf` on the `@scope/cli-<triple>` `.tgz`) shows the binary as
|
|
655
|
+
`-rwxr-xr-x`.
|
|
656
|
+
|
|
657
|
+
### Pre-merge crate-size check
|
|
658
|
+
|
|
659
|
+
**Summary.** `putitoutthere check` gained a check that runs
|
|
660
|
+
`cargo package --no-verify` for every `kind = "crates"` package and
|
|
661
|
+
fails when the resulting `.crate` is larger than crates.io's 10 MiB
|
|
662
|
+
(`10485760`-byte) upload limit. Previously an oversized crate — most
|
|
663
|
+
often caused by a tracked symlink dragging a build tree into the
|
|
664
|
+
package — surfaced only mid-release as a `413 Payload Too Large` from
|
|
665
|
+
`cargo publish`, after the verification build. The new check moves
|
|
666
|
+
that failure to PR time, before merge.
|
|
667
|
+
|
|
668
|
+
**Required changes.** None. The check is additive and runs
|
|
669
|
+
automatically wherever `putitoutthere check` already runs (the
|
|
670
|
+
`check.yml` reusable workflow). For the check to actually measure a
|
|
671
|
+
crate, a Rust toolchain (`cargo`) must be on `PATH` in that job; when
|
|
672
|
+
`cargo` is absent the check degrades to a no-op rather than failing,
|
|
673
|
+
so a check job without Rust set up sees no behavior change.
|
|
674
|
+
|
|
675
|
+
**Deprecations removed.** None.
|
|
676
|
+
|
|
677
|
+
**Behavior changes without code changes.** A PR that would produce an
|
|
678
|
+
oversized `.crate` now fails `putitoutthere check` with the new
|
|
679
|
+
`PIOT_CRATES_PACKAGE_TOO_LARGE` error code, instead of passing the
|
|
680
|
+
check and failing later inside the release run's publish job.
|
|
681
|
+
|
|
682
|
+
**Verification.** Add a `kind = "crates"` package and run
|
|
683
|
+
`putitoutthere check` (or open a PR against a repo wired to
|
|
684
|
+
`check.yml`) in an environment with `cargo` on `PATH`: an oversized
|
|
685
|
+
crate reports `PIOT_CRATES_PACKAGE_TOO_LARGE` naming the `.crate`
|
|
686
|
+
size and the 10 MiB limit, while a normally-sized crate reports
|
|
687
|
+
nothing.
|
|
688
|
+
|
|
689
|
+
### v0 tracks main HEAD
|
|
690
|
+
|
|
691
|
+
**Summary.** Until this release, the floating `v0` tag advanced only
|
|
692
|
+
when a `release:` trailer fired the dogfood publish pipeline
|
|
693
|
+
(`release-npm.yml`), which then moved `v0` to the latest
|
|
694
|
+
`putitoutthere-v0.x.y` release commit. Commits that landed on main
|
|
695
|
+
without a trailer — test-only changes, docs edits, dependency bumps,
|
|
696
|
+
internal refactors, and one-off bug fixes whose author forgot the
|
|
697
|
+
trailer — left `v0` stale relative to main. The behavior was
|
|
698
|
+
explicitly chosen in issue #199 (`v0` = "latest released commit in
|
|
699
|
+
major line") and is now explicitly reversed: `v0` tracks main HEAD,
|
|
700
|
+
not the latest release.
|
|
701
|
+
|
|
702
|
+
A new workflow `.github/workflows/advance-v0.yml` fires on every
|
|
703
|
+
push to main, builds the action bundle, folds it into a tag-only
|
|
704
|
+
commit (mirroring `release-npm.yml`'s existing Fold step —
|
|
705
|
+
`dist-action/` is gitignored on main, so `v0` must point at a
|
|
706
|
+
synthesized bundle commit for `uses:
|
|
707
|
+
thekevinscott/putitoutthere@v0` to resolve to a runnable action),
|
|
708
|
+
and force-moves `v0` to that commit. The new workflow shares the
|
|
709
|
+
`release` concurrency group with `release-npm.yml`, so when both
|
|
710
|
+
fire on the same push (a trailer-bearing commit), the registry
|
|
711
|
+
publish runs first and `v0` is then advanced on top.
|
|
712
|
+
|
|
713
|
+
The permanent per-release tags (`putitoutthere-v0.x.y`) are
|
|
714
|
+
unchanged — they're cut by the dogfood publish pipeline on
|
|
715
|
+
trailer-fire and remain the canonical version history.
|
|
716
|
+
|
|
717
|
+
**Required changes.** None on the consumer side. The change is in
|
|
718
|
+
how `@v0` resolves over time, not in what the workflow at that ref
|
|
719
|
+
does.
|
|
720
|
+
|
|
721
|
+
**Deprecations removed.** None.
|
|
722
|
+
|
|
723
|
+
**Behavior changes without code changes.** A commit that lands on
|
|
724
|
+
`main` of `thekevinscott/putitoutthere` is, on the next consumer
|
|
725
|
+
workflow resolve, the workflow code the consumer runs. Previously
|
|
726
|
+
consumers had to wait for a release to be cut to pick up engine
|
|
727
|
+
changes; now they pick them up on the next push to main. Consumers
|
|
728
|
+
who want pinning to a known-released version use a
|
|
729
|
+
`putitoutthere-v0.x.y` tag (or a SHA) instead of `@v0`.
|
|
730
|
+
|
|
731
|
+
**Verification.** After this change merges and the first push to
|
|
732
|
+
main fires `advance-v0.yml`, the `v0` tag points at a fresh bundle
|
|
733
|
+
commit whose parent is the merge commit on main. Confirm with:
|
|
734
|
+
|
|
735
|
+
```
|
|
736
|
+
$ git ls-remote --tags https://github.com/thekevinscott/putitoutthere.git v0
|
|
737
|
+
<sha> refs/tags/v0
|
|
738
|
+
$ git log <sha> -1 --format='%H %s'
|
|
739
|
+
<sha> chore(v0): bundle action
|
|
740
|
+
$ git log <sha>^ -1 --format='%H %s' # parent is the merge commit on main
|
|
741
|
+
<parent-sha> <merge commit subject>
|
|
742
|
+
```
|
|
743
|
+
|
|
744
|
+
### Preflight: manifest repository URL must match GITHUB_REPOSITORY; private repos rejected
|
|
745
|
+
|
|
746
|
+
**Summary.** Two new preflight checks address the
|
|
747
|
+
"surprise-at-publish" failure mode where a manifest's declared
|
|
748
|
+
`repository` URL silently disagrees with the GitHub repository the
|
|
749
|
+
workflow is actually running from. npm's provenance verification
|
|
750
|
+
returns a 422 (`"package.json: repository.url is X, expected to
|
|
751
|
+
match Y from provenance"`) **after** the artifact has been uploaded
|
|
752
|
+
and the registry has done OIDC negotiation — the kind of mid-publish
|
|
753
|
+
surprise this engine's "no release surprises" design commitment
|
|
754
|
+
exists to prevent. The same risk lives on the crates.io / PyPI
|
|
755
|
+
trusted-publisher paths against `Cargo.toml [package].repository`
|
|
756
|
+
and `pyproject.toml [project.urls]`. Both checks now fire at the
|
|
757
|
+
preflight stage before any side effects.
|
|
758
|
+
|
|
759
|
+
A second new check refuses to publish from a **private** GitHub
|
|
760
|
+
repository entirely. Provenance attestations embed a public
|
|
761
|
+
source-ref pointer that consumers cannot dereference when the repo
|
|
762
|
+
is private; the same source-visibility expectation underpins the
|
|
763
|
+
trusted-publisher story across all three registries. Hard-failing
|
|
764
|
+
at preflight beats silently shipping a verification-broken artifact.
|
|
765
|
+
|
|
766
|
+
**Required changes.** None for any consumer whose manifest URLs
|
|
767
|
+
already point at the correct `owner/repo` on GitHub and whose
|
|
768
|
+
repository is public. The check is opt-out only by fixing the
|
|
769
|
+
underlying disagreement.
|
|
770
|
+
|
|
771
|
+
| Failure mode | Fix |
|
|
772
|
+
| --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
773
|
+
| `PIOT_REPO_URL_MISMATCH` on a renamed repo, manifests stale. | Update the manifest URL to match the GitHub repo (recommended), or rename the GitHub repo so the slugs line up. Re-run the release. |
|
|
774
|
+
| `PIOT_REPO_URL_MISMATCH` on a manifest that points at a fork or mirror. | Point the manifest URL at the canonical GitHub repo the workflow runs from. Trusted-publisher records on every registry bind to the workflow source, not the fork. |
|
|
775
|
+
| `PIOT_REPO_PRIVATE` on a repository that is intentionally private. | This engine cannot publish from a private repository. Either flip the repo to public before releasing or use a different release path. There is no opt-out flag — provenance attestations require public source. |
|
|
776
|
+
|
|
777
|
+
**Deprecations removed.** None.
|
|
778
|
+
|
|
779
|
+
**Behavior changes without code changes.** None — both checks are
|
|
780
|
+
new gates.
|
|
781
|
+
|
|
782
|
+
**Verification.** From a PR branch with a deliberately-wrong
|
|
783
|
+
`repository.url`, the PR-time `check.yml` job now reports
|
|
784
|
+
`[PIOT_REPO_URL_MISMATCH]` naming both the declared and expected
|
|
785
|
+
`owner/repo` slugs and the manifest path. From a private repo,
|
|
786
|
+
`publish` aborts before any side effect with `[PIOT_REPO_PRIVATE]`.
|
|
787
|
+
Both checks no-op outside a GHA context (when `GITHUB_REPOSITORY`
|
|
788
|
+
is unset), so a local `putitoutthere check` from a developer
|
|
789
|
+
machine does not false-positive.
|
|
790
|
+
|
|
791
|
+
### Windows default runner pinned to windows-2022
|
|
792
|
+
|
|
793
|
+
**Summary.** GitHub is migrating `windows-latest` (and `windows-2025`)
|
|
794
|
+
to Visual Studio 2026 between 2026-06-08 and 2026-06-15 — see the
|
|
795
|
+
[GitHub Actions image-migration changelog](https://github.blog/changelog/2026-05-14-github-actions-upcoming-image-migrations/)
|
|
796
|
+
and [actions/runner-images#14016](https://github.com/actions/runner-images/issues/14016).
|
|
797
|
+
Until that date, `windows-latest` is Windows Server 2025 + VS2022; on
|
|
798
|
+
the cutover, `windows-latest` redirects to `windows-2025-vs2026` and
|
|
799
|
+
every consumer release run lands on a fresh toolchain with no
|
|
800
|
+
opportunity to verify it first.
|
|
801
|
+
|
|
802
|
+
`defaultRunsOn` in `src/plan.ts` previously returned `windows-latest`
|
|
803
|
+
for any Windows-shaped triple, so every consumer release plan that
|
|
804
|
+
included `x86_64-pc-windows-msvc` (the standard windows triple for
|
|
805
|
+
napi, bundled-cli, and maturin builds) inherited that floating label.
|
|
806
|
+
The default is now `windows-2022`: stable, VS2022, no surprise
|
|
807
|
+
migration. Consumers who want to track the floating label or adopt
|
|
808
|
+
VS2026 early opt in via the per-target `{ triple, runner }` override
|
|
809
|
+
that already exists.
|
|
810
|
+
|
|
811
|
+
**Required changes.** None for consumers who want to stay on a stable
|
|
812
|
+
VS2022 toolchain — the new default does that for them. Consumers who
|
|
813
|
+
want a different image opt in per target:
|
|
814
|
+
|
|
815
|
+
Before (relied on the floating `windows-latest` default):
|
|
816
|
+
|
|
817
|
+
```toml
|
|
818
|
+
[[package]]
|
|
819
|
+
name = "lib-napi"
|
|
820
|
+
kind = "npm"
|
|
821
|
+
build = "napi"
|
|
822
|
+
targets = [
|
|
823
|
+
"x86_64-unknown-linux-gnu",
|
|
824
|
+
"x86_64-pc-windows-msvc",
|
|
825
|
+
]
|
|
826
|
+
```
|
|
827
|
+
|
|
828
|
+
After (no change required — `x86_64-pc-windows-msvc` now resolves to
|
|
829
|
+
`windows-2022` by default):
|
|
830
|
+
|
|
831
|
+
```toml
|
|
832
|
+
[[package]]
|
|
833
|
+
name = "lib-napi"
|
|
834
|
+
kind = "npm"
|
|
835
|
+
build = "napi"
|
|
836
|
+
targets = [
|
|
837
|
+
"x86_64-unknown-linux-gnu",
|
|
838
|
+
"x86_64-pc-windows-msvc",
|
|
839
|
+
]
|
|
840
|
+
```
|
|
841
|
+
|
|
842
|
+
After (opt in to a different image — for example, surface VS2026
|
|
843
|
+
breakage now rather than on the cutover date):
|
|
844
|
+
|
|
845
|
+
```toml
|
|
846
|
+
targets = [
|
|
847
|
+
"x86_64-unknown-linux-gnu",
|
|
848
|
+
{ triple = "x86_64-pc-windows-msvc", runner = "windows-2025-vs2026" },
|
|
849
|
+
]
|
|
850
|
+
```
|
|
851
|
+
|
|
852
|
+
Other valid choices for the `runner` value include `windows-2025`
|
|
853
|
+
(Server 2025 + VS2022 until the cutover, then VS2026), `windows-2022`
|
|
854
|
+
(matches the new default explicitly), and `windows-latest` (preserves
|
|
855
|
+
the previous floating-label behavior).
|
|
856
|
+
|
|
857
|
+
**Deprecations removed.** None.
|
|
858
|
+
|
|
859
|
+
**Behavior changes without code changes.** Every Windows-shaped
|
|
860
|
+
matrix row's `runs_on` field now resolves to `windows-2022` instead
|
|
861
|
+
of `windows-latest` when no per-target `runner` override is set.
|
|
862
|
+
Per-target overrides win exactly as before — the bare-string-vs-object
|
|
863
|
+
precedence in `defaultRunsOn` is unchanged. The redirect notice
|
|
864
|
+
GitHub injects into every `windows-latest`-targeted run
|
|
865
|
+
(`NOTICE: windows-latest requests are being redirected to
|
|
866
|
+
windows-2025-vs2026 by June 15, 2026`) stops appearing on plans
|
|
867
|
+
generated by the new engine.
|
|
868
|
+
|
|
869
|
+
**Verification.** Run a release that includes any Windows triple and
|
|
870
|
+
confirm the build job runs on `windows-2022`:
|
|
871
|
+
|
|
872
|
+
- In the GitHub Actions UI, the per-target build job's "Set up job"
|
|
873
|
+
step lists `Runner: GitHub Actions <n>` followed by an OS-image
|
|
874
|
+
block whose `Image: windows-2022` line names the pinned image. The
|
|
875
|
+
banner `windows-latest requests are being redirected to
|
|
876
|
+
windows-2025-vs2026 by June 15, 2026` no longer appears in the
|
|
877
|
+
job log.
|
|
878
|
+
- The published artifact's `runs_on` value, surfaced in the plan
|
|
879
|
+
step's job summary, is `windows-2022`.
|
|
880
|
+
|
|
881
|
+
To opt in to a different image, set
|
|
882
|
+
`{ triple = "x86_64-pc-windows-msvc", runner = "<choice>" }` on the
|
|
883
|
+
relevant target and re-release; the build job moves to the named
|
|
884
|
+
image on the next run.
|
|
885
|
+
|
|
886
|
+
### Crates first-publish TP rejection detected
|
|
887
|
+
|
|
888
|
+
**Summary.** crates.io's Trusted Publishing feature binds to an
|
|
889
|
+
already-published crate name. The very first publish of a brand-new
|
|
890
|
+
crate cannot use the TP path — the OIDC mint succeeds, the exchanged
|
|
891
|
+
token reaches cargo, but the registry rejects the publish with a 404
|
|
892
|
+
("crate `<name>` does not exist or you do not have permission to
|
|
893
|
+
publish to it"). The engine previously surfaced this as a generic
|
|
894
|
+
`cargo publish failed` block, sending consumers down a credentials
|
|
895
|
+
rabbit-hole when the real fix is one bootstrap publish via the
|
|
896
|
+
classic-token fallback shipped in #283.
|
|
897
|
+
|
|
898
|
+
The crates handler now detects this exact response shape and throws
|
|
899
|
+
with the new stable error code
|
|
900
|
+
`PIOT_CRATES_FIRST_PUBLISH_TP_REJECTED`, prefixed onto a message
|
|
901
|
+
that names the crate, explains the TP-binds-to-published-crate
|
|
902
|
+
constraint, and points at `CARGO_REGISTRY_TOKEN` as the bootstrap
|
|
903
|
+
path. Cargo's full stderr is preserved at the bottom of the error
|
|
904
|
+
for debuggability. Companion work landed a registry-auth response
|
|
905
|
+
fixtures catalogue at
|
|
906
|
+
[`notes/upstream-behaviors.md`](./notes/upstream-behaviors.md) that
|
|
907
|
+
indexes this and three other response shapes the engine handles or
|
|
908
|
+
architecturally avoids — see #296.
|
|
909
|
+
|
|
910
|
+
**Required changes.** None. Consumers who never hit the
|
|
911
|
+
first-publish path see no change. Consumers whose first release
|
|
912
|
+
fails on a brand-new crate now see a clearer error pointing at the
|
|
913
|
+
fix; the fix itself (set `CARGO_REGISTRY_TOKEN` as a workflow
|
|
914
|
+
secret for one publish, then remove it) has been available since
|
|
915
|
+
#283 and is unchanged.
|
|
916
|
+
|
|
917
|
+
**Deprecations removed.** None.
|
|
918
|
+
|
|
919
|
+
**Behavior changes without code changes.** A `cargo publish`
|
|
920
|
+
failure whose stderr matches the first-publish-TP-rejection shape
|
|
921
|
+
now throws with `PIOT_CRATES_FIRST_PUBLISH_TP_REJECTED` instead of
|
|
922
|
+
the generic `cargo publish failed` shape. The full cargo stderr
|
|
923
|
+
remains in the error message. The detector is suppressed under the
|
|
924
|
+
`PIOT_CRATES_REGISTRY_PRIMARY` e2e seam (alt-registry doesn't model
|
|
925
|
+
TP, so a 404 there is a different bug).
|
|
926
|
+
|
|
927
|
+
**Verification.** On a brand-new crate name where Trusted
|
|
928
|
+
Publishing is the only auth configured, run the release. The
|
|
929
|
+
release run fails with a message starting
|
|
930
|
+
`[PIOT_CRATES_FIRST_PUBLISH_TP_REJECTED] cargo publish: crates.io
|
|
931
|
+
rejected publishing "<name>" because the crate has never been
|
|
932
|
+
published.` followed by the bootstrap hint. Set
|
|
933
|
+
`CARGO_REGISTRY_TOKEN` in the workflow's `secrets:` block (per
|
|
934
|
+
#283), re-run, and the publish should succeed. Subsequent releases
|
|
935
|
+
can drop the secret and rely on Trusted Publishing.
|
|
936
|
+
|
|
937
|
+
### Bundled-CLI launcher generated by the workflow
|
|
938
|
+
|
|
939
|
+
**Summary.** Bundled-CLI npm consumers used to author `bin/<bin>.js` —
|
|
940
|
+
a Node launcher that detects the host platform, maps it to a triple,
|
|
941
|
+
resolves the corresponding `<name>-<triple>` (or templated) platform
|
|
942
|
+
package, and execs the binary. The launcher's only per-consumer
|
|
943
|
+
inputs are the package name and the configured `targets` list. Both
|
|
944
|
+
are in the engine's hands at plan time. Every consumer's launcher
|
|
945
|
+
was byte-identical modulo those two values.
|
|
946
|
+
|
|
947
|
+
`_matrix.yml`'s build job now invokes a new internal `putitoutthere
|
|
948
|
+
write-launcher` CLI subcommand on the main row of each `kind = "npm"
|
|
949
|
+
&& build = "bundled-cli"` package (before `npm run build --if-present`
|
|
950
|
+
runs). The subcommand writes `bin/<bundle_cli.bin>.js` and adds the
|
|
951
|
+
matching `package.json#bin` entry in place. Both writes are guarded
|
|
952
|
+
by an "only if absent" check — existing consumer-authored launchers
|
|
953
|
+
and existing `bin` fields are preserved, so the override path is the
|
|
954
|
+
same file you'd already have committed.
|
|
955
|
+
|
|
956
|
+
The generated launcher's shape mirrors the README example bundled-cli
|
|
957
|
+
consumers wrote by hand pre-#299: hashbang, a Node
|
|
958
|
+
`${platform}-${arch}` → triple table, `require.resolve` against the
|
|
959
|
+
platform package, `spawnSync` with `stdio: 'inherit'`. The platform
|
|
960
|
+
package's name template (`{name}-{triple}` by default, or whatever the
|
|
961
|
+
consumer set under `build = [{ mode = "bundled-cli", name = "..." }]`
|
|
962
|
+
in the multi-mode array form) has every placeholder except `{triple}`
|
|
963
|
+
resolved at generation time; `{triple}` becomes a backtick template
|
|
964
|
+
substitution at install time. The launcher imports nothing from
|
|
965
|
+
putitoutthere at runtime — it's a self-contained Node script with no
|
|
966
|
+
published-package dependencies.
|
|
967
|
+
|
|
968
|
+
Together with #298 (which absorbed the cross-compile build script),
|
|
969
|
+
bundled-cli npm's consumer surface is now: declare the package in
|
|
970
|
+
`putitoutthere.toml`, register Trusted Publishers, push.
|
|
971
|
+
|
|
972
|
+
**Required changes.** None for new consumers — declaring the package
|
|
973
|
+
in `putitoutthere.toml` is sufficient.
|
|
974
|
+
|
|
975
|
+
For consumers who already shipped a hand-authored launcher and want
|
|
976
|
+
to migrate to the generated one, delete `bin/<bin>.js` from the source
|
|
977
|
+
tree. The build job will regenerate it on the next release run.
|
|
978
|
+
Leaving the file in place is fully supported; the workflow only
|
|
979
|
+
writes when the file is absent.
|
|
980
|
+
|
|
981
|
+
```diff
|
|
982
|
+
// packages/my-cli/package.json
|
|
983
|
+
{
|
|
984
|
+
"name": "my-cli",
|
|
985
|
+
- "bin": { "my-cli": "bin/my-cli.js" }
|
|
986
|
+
}
|
|
987
|
+
```
|
|
988
|
+
|
|
989
|
+
Removing the `bin` field is optional too: when the workflow sees an
|
|
990
|
+
existing `bin` field it leaves it alone. The diff above is only
|
|
991
|
+
necessary if the consumer wants the workflow to author the field
|
|
992
|
+
shape from scratch (`{ "<bin>": "bin/<bin>.js" }`).
|
|
993
|
+
|
|
994
|
+
**Deprecations removed.** None — the legacy hand-authored launcher
|
|
995
|
+
path is still supported and the workflow respects the override.
|
|
996
|
+
|
|
997
|
+
**Behavior changes without code changes.**
|
|
998
|
+
|
|
999
|
+
- The published main package's `package.json#bin` now contains
|
|
1000
|
+
`{ "<bundle_cli.bin>": "bin/<bundle_cli.bin>.js" }` for consumers
|
|
1001
|
+
who previously had no `bin` field. Consumers with a pre-existing
|
|
1002
|
+
`bin` field see no change.
|
|
1003
|
+
- The published main package's tarball now contains
|
|
1004
|
+
`bin/<bundle_cli.bin>.js` for consumers who previously did not
|
|
1005
|
+
commit the file. Consumers who committed the file see their version
|
|
1006
|
+
shipped unchanged.
|
|
1007
|
+
|
|
1008
|
+
**Verification.** After a release run, inspect the published main
|
|
1009
|
+
package's tarball:
|
|
1010
|
+
|
|
1011
|
+
```sh
|
|
1012
|
+
npm pack <main-pkg-name>@<version>
|
|
1013
|
+
tar -xvf <main-pkg-name>-<version>.tgz package/bin/<bundle_cli.bin>.js -O \
|
|
1014
|
+
| head -20
|
|
1015
|
+
```
|
|
1016
|
+
|
|
1017
|
+
The first line is `#!/usr/bin/env node`; the file declares a `triples`
|
|
1018
|
+
object whose keys match the Node `${platform}-${arch}` strings the
|
|
1019
|
+
package's `targets` resolve to, and whose values are the configured
|
|
1020
|
+
triples. `package/package.json`'s `bin` field is
|
|
1021
|
+
`{ "<bundle_cli.bin>": "bin/<bundle_cli.bin>.js" }`.
|
|
1022
|
+
|
|
1023
|
+
### `bundle_cli` wheel guard respects `python-source`
|
|
1024
|
+
|
|
1025
|
+
**Summary.** Maturin's standard mixed-project layout
|
|
1026
|
+
(`maturin new --mixed` generates `[tool.maturin].python-source = "python"`)
|
|
1027
|
+
declares a package source root that maturin strips from on-disk paths
|
|
1028
|
+
when rewriting them into the wheel's distribution layout. A binary
|
|
1029
|
+
staged on disk at `<pkg.path>/<stage_to>/<bin>` — e.g.
|
|
1030
|
+
`packages/python/python/dirsql/_binary/dirsql` — ends up in the wheel
|
|
1031
|
+
at `dirsql/_binary/dirsql`, with `python/` stripped. The reusable
|
|
1032
|
+
workflow's `bundle_cli` wheel-content guard previously asserted a
|
|
1033
|
+
literal `<stage_to>/<bin>` suffix inside the produced wheel; the regex
|
|
1034
|
+
never matched the stripped path, so the guard fired red on every
|
|
1035
|
+
per-target build row even when the binary was correctly bundled.
|
|
1036
|
+
|
|
1037
|
+
The guard now reads `[tool.maturin].python-source` (and the legacy
|
|
1038
|
+
`python_source` spelling — both forms are accepted by maturin across
|
|
1039
|
+
versions) from `<matrix.path>/pyproject.toml` and subtracts that
|
|
1040
|
+
prefix from `stage_to` before constructing the suffix regex. Consumers
|
|
1041
|
+
with an implicit-root layout (no `python-source` key, or an empty
|
|
1042
|
+
value) keep the previous behavior byte-for-byte; consumers with the
|
|
1043
|
+
explicit-root layout start passing the guard. Tracked at #338.
|
|
1044
|
+
|
|
1045
|
+
**Required changes.** None.
|
|
1046
|
+
|
|
1047
|
+
**Deprecations removed.** None.
|
|
1048
|
+
|
|
1049
|
+
**Behavior changes without code changes.**
|
|
1050
|
+
|
|
1051
|
+
- For a consumer with `[tool.maturin].python-source = "python"` and
|
|
1052
|
+
`[package.bundle_cli].stage_to = "python/dirsql/_binary"`, the
|
|
1053
|
+
reusable workflow's wheel-content guard now resolves the in-wheel
|
|
1054
|
+
suffix to `dirsql/_binary/<bin>` (matching what maturin actually
|
|
1055
|
+
produces) instead of asserting the unstripped `python/dirsql/_binary/<bin>`.
|
|
1056
|
+
- A `python-source` value that isn't actually a prefix of `stage_to`
|
|
1057
|
+
is left alone — the guard reverts to asserting the unstripped
|
|
1058
|
+
`stage_to` so the consumer's misconfiguration surfaces with the same
|
|
1059
|
+
diagnostic it does today.
|
|
1060
|
+
- An unset or empty `python-source` value resolves to the empty
|
|
1061
|
+
string and `stage_suffix` is unchanged. No behavior change for
|
|
1062
|
+
consumers who don't use the explicit-root layout.
|
|
1063
|
+
|
|
1064
|
+
**Verification.** With a maturin package whose `pyproject.toml`
|
|
1065
|
+
declares `[tool.maturin].python-source = "python"` and whose
|
|
1066
|
+
`[package.bundle_cli]` sets `stage_to = "python/<pkg>/_binary"`,
|
|
1067
|
+
a release run should produce wheels whose `unzip -l` listing contains
|
|
1068
|
+
`<pkg>/_binary/<bin>` and the wheel-content guard step should log
|
|
1069
|
+
`ok bundle_cli: <pkg>/_binary/<bin> present in <wheel>`.
|
|
1070
|
+
|
|
1071
|
+
### `bundle_cli` cargo workspace
|
|
1072
|
+
|
|
1073
|
+
**Summary.** Two collided bugs made `[package.bundle_cli]`
|
|
1074
|
+
unsatisfiable for the standard cargo-workspace layout: a single
|
|
1075
|
+
workspace root `Cargo.toml` with `[workspace] members = [...]` and the
|
|
1076
|
+
`[[bin]]` declared in a member crate (the shape `cargo new --workspace`
|
|
1077
|
+
produces, and what the polyglot Rust/Python recipe in the README
|
|
1078
|
+
implies). With `crate_path = "."` (the default), `putitoutthere check`
|
|
1079
|
+
parsed the workspace root `Cargo.toml` literally, saw no `[[bin]]`, and
|
|
1080
|
+
emitted `bundle_cli.bin "X" is not declared as a [[bin]]`. With
|
|
1081
|
+
`crate_path = "packages/rust"`, the check passed but the reusable
|
|
1082
|
+
workflow's bundle_cli stage step couldn't find the produced binary —
|
|
1083
|
+
cargo writes to the workspace-rooted target dir by default
|
|
1084
|
+
(`<repo-root>/target/...`), not to the working-directory-rooted one
|
|
1085
|
+
(`packages/rust/target/...`) the stage step assumed. There was no
|
|
1086
|
+
`crate_path` value that satisfied both halves.
|
|
1087
|
+
|
|
1088
|
+
The check now walks `[workspace].members` and aggregates each member's
|
|
1089
|
+
declared bins (honoring the implicit-binary rule, including
|
|
1090
|
+
`[package].name = { workspace = true }` inheritance from
|
|
1091
|
+
`[workspace.package].name`). The reusable workflow's cargo build step
|
|
1092
|
+
pins `--target-dir target` so the produced binary is deterministically
|
|
1093
|
+
at `${{ matrix.bundle_cli.crate_path }}/target/<triple>/release/<bin>`
|
|
1094
|
+
regardless of whether the crate participates in a workspace. Tracked at
|
|
1095
|
+
#337.
|
|
1096
|
+
|
|
1097
|
+
**Required changes.** None.
|
|
1098
|
+
|
|
1099
|
+
Consumers whose `[package.bundle_cli]` block already worked (single-
|
|
1100
|
+
crate layouts, or workspaces where `crate_path` pointed directly at the
|
|
1101
|
+
member crate and the consumer's project structure happened to make the
|
|
1102
|
+
workspace target dir line up with the member target dir) keep building
|
|
1103
|
+
byte-identically. Consumers whose workspace layout previously failed
|
|
1104
|
+
the check or stage step start working without touching their config.
|
|
1105
|
+
|
|
1106
|
+
**Deprecations removed.** None.
|
|
1107
|
+
|
|
1108
|
+
**Behavior changes without code changes.**
|
|
1109
|
+
|
|
1110
|
+
- `putitoutthere check` accepts the cargo-workspace shape:
|
|
1111
|
+
```toml
|
|
1112
|
+
# /Cargo.toml
|
|
1113
|
+
[workspace]
|
|
1114
|
+
members = ["packages/rust"]
|
|
1115
|
+
|
|
1116
|
+
# /packages/rust/Cargo.toml
|
|
1117
|
+
[package]
|
|
1118
|
+
name = "my-cli"
|
|
1119
|
+
description = "..."
|
|
1120
|
+
license = "MIT"
|
|
1121
|
+
|
|
1122
|
+
[[bin]]
|
|
1123
|
+
name = "my-cli"
|
|
1124
|
+
path = "src/main.rs"
|
|
1125
|
+
|
|
1126
|
+
# /putitoutthere.toml
|
|
1127
|
+
[package.bundle_cli]
|
|
1128
|
+
bin = "my-cli"
|
|
1129
|
+
stage_to = "python/dirsql/_binary"
|
|
1130
|
+
# crate_path defaults to "."
|
|
1131
|
+
```
|
|
1132
|
+
previously reported `bundle_cli.bin "my-cli" is not declared as a [[bin]]`,
|
|
1133
|
+
now reports zero findings.
|
|
1134
|
+
- The reusable workflow's bundle_cli build step now passes
|
|
1135
|
+
`--target-dir target` to `cargo build`. The produced binary is at
|
|
1136
|
+
`${{ matrix.bundle_cli.crate_path }}/target/<triple>/release/<bin>`
|
|
1137
|
+
regardless of workspace membership, and the stage step's `src=` path
|
|
1138
|
+
resolves correctly by construction.
|
|
1139
|
+
- Members declared as glob patterns (`members = ["packages/*"]`) are
|
|
1140
|
+
expanded against the filesystem by the check — see
|
|
1141
|
+
[`bundle_cli` glob workspace members](#bundle_cli-glob-workspace-members).
|
|
1142
|
+
|
|
1143
|
+
**Verification.** With the workspace layout above, the consumer should
|
|
1144
|
+
see:
|
|
1145
|
+
|
|
1146
|
+
- `putitoutthere check` reports zero findings.
|
|
1147
|
+
- A maturin release run produces a wheel whose `unzip -l` includes the
|
|
1148
|
+
staged binary (the existing wheel-content guard asserts this).
|
|
1149
|
+
|
|
1150
|
+
### `bundle_cli` glob workspace members
|
|
1151
|
+
|
|
1152
|
+
**Summary.** The `bundle_cli` cargo-workspace fix above taught
|
|
1153
|
+
`putitoutthere check` (and the pre-publish preflight) to walk
|
|
1154
|
+
`[workspace].members` and aggregate each member crate's declared
|
|
1155
|
+
`[[bin]]` entries, so `crate_path = "."` resolves a `[[bin]]` that
|
|
1156
|
+
lives in a member crate. That walk only handled *literal* member
|
|
1157
|
+
entries. cargo `members` entries are globs, and `members =
|
|
1158
|
+
["packages/*"]` — a Rust core crate under `packages/rust` wrapped by
|
|
1159
|
+
sibling Python / npm packages — is the standard polyglot-repo shape. A
|
|
1160
|
+
glob entry never resolved to a literal `<member>/Cargo.toml`, so the
|
|
1161
|
+
member crate's `[[bin]]` went unseen and `crate_path = "."` was
|
|
1162
|
+
rejected with `bundle_cli.bin "X" is not declared as a [[bin]]`.
|
|
1163
|
+
|
|
1164
|
+
The check now expands `[workspace].members` glob entries against the
|
|
1165
|
+
filesystem the way cargo resolves them; a member crate behind a glob is
|
|
1166
|
+
found like any literal member.
|
|
1167
|
+
|
|
1168
|
+
**Required changes.** None.
|
|
1169
|
+
|
|
1170
|
+
Consumers whose workspace declares `members` with literal paths are
|
|
1171
|
+
unaffected. Consumers who declared `members` with a glob and worked
|
|
1172
|
+
around the rejected check — by also listing the member crate as a
|
|
1173
|
+
literal entry, or by pointing `crate_path` straight at the member
|
|
1174
|
+
crate — can drop the workaround and let `crate_path` default to `"."`.
|
|
1175
|
+
|
|
1176
|
+
**Deprecations removed.** None.
|
|
1177
|
+
|
|
1178
|
+
**Behavior changes without code changes.**
|
|
1179
|
+
|
|
1180
|
+
- `putitoutthere check` accepts a glob-member workspace:
|
|
1181
|
+
```toml
|
|
1182
|
+
# /Cargo.toml
|
|
1183
|
+
[workspace]
|
|
1184
|
+
members = ["packages/*"]
|
|
1185
|
+
|
|
1186
|
+
# /packages/rust/Cargo.toml
|
|
1187
|
+
[package]
|
|
1188
|
+
name = "rust-core"
|
|
1189
|
+
|
|
1190
|
+
[[bin]]
|
|
1191
|
+
name = "my-cli"
|
|
1192
|
+
path = "src/main.rs"
|
|
1193
|
+
|
|
1194
|
+
# /putitoutthere.toml
|
|
1195
|
+
[package.bundle_cli]
|
|
1196
|
+
bin = "my-cli"
|
|
1197
|
+
stage_to = "python/dirsql/_binary"
|
|
1198
|
+
# crate_path defaults to "."
|
|
1199
|
+
```
|
|
1200
|
+
previously reported `bundle_cli.bin "my-cli" is not declared as a [[bin]]`,
|
|
1201
|
+
now reports zero findings.
|
|
1202
|
+
|
|
1203
|
+
**Verification.** With the glob-member workspace above, `putitoutthere
|
|
1204
|
+
check` reports zero findings.
|
|
1205
|
+
|
|
24
1206
|
### Crates metadata check resolves `[workspace.package]` inheritance
|
|
25
1207
|
|
|
26
1208
|
**Summary.** Cargo's recommended pattern for shared crate metadata in a
|
|
@@ -396,6 +1578,56 @@ the same path the launcher resolves it from.
|
|
|
396
1578
|
|
|
397
1579
|
---
|
|
398
1580
|
|
|
1581
|
+
### Preflight pyproject + cargo shape
|
|
1582
|
+
|
|
1583
|
+
**Summary.** Preflight gains two more checks — `requirePyprojectShape`
|
|
1584
|
+
and `requireCargoShape` — that mirror the #280 / #290 pattern for
|
|
1585
|
+
`pyproject.toml` (pypi packages) and `Cargo.toml` (crates packages,
|
|
1586
|
+
plus `bundle_cli` on pypi packages). The maturin / setuptools /
|
|
1587
|
+
hatchling / cargo CLIs surface mismatched-shape errors 10-20 minutes
|
|
1588
|
+
into a release run, deep into the verification build, with messages
|
|
1589
|
+
that don't name the precondition that failed. The new checks fire at
|
|
1590
|
+
publish time alongside the existing `require*` family and at PR time
|
|
1591
|
+
via `check.yml`. Findings aggregate across every failing package so
|
|
1592
|
+
consumers fix them all in one round-trip, exactly the shape the prior
|
|
1593
|
+
checks established. #301.
|
|
1594
|
+
|
|
1595
|
+
**Required changes.** None for well-formed manifests. Repos that
|
|
1596
|
+
declared one of the documented mismatches below previously got a
|
|
1597
|
+
mid-release red; they now get a fast preflight red instead.
|
|
1598
|
+
|
|
1599
|
+
The new error codes:
|
|
1600
|
+
|
|
1601
|
+
| Code | Fires when |
|
|
1602
|
+
|------|------------|
|
|
1603
|
+
| `PIOT_PYPI_NAME_MISMATCH` | `pyproject.toml`'s `[project].name` differs from `[[package]].name` (or the `pypi` override). |
|
|
1604
|
+
| `PIOT_PYPI_BUILD_BACKEND_MISMATCH` | `[build-system].build-backend` is set and does not start with the prefix the configured `build` mode expects (`maturin` → `maturin`, `setuptools` → `setuptools`, `hatch` → `hatchling`/`hatch`). |
|
|
1605
|
+
| `PIOT_PYPI_DYNAMIC_VERSION_NO_BACKEND` | `[project].dynamic` includes `"version"` but neither `[tool.hatch.version]` nor `[tool.setuptools_scm]` is present. |
|
|
1606
|
+
| `PIOT_PYPI_MATURIN_INCLUDE_MISSING` | `bundle_cli` is set but `[tool.maturin].include` does not cover `bundle_cli.stage_to`. |
|
|
1607
|
+
| `PIOT_CRATES_NAME_MISMATCH` | `Cargo.toml`'s `[package].name` differs from `[[package]].name` (or the `crate` override). |
|
|
1608
|
+
| `PIOT_CRATES_MISSING_BIN` | `bundle_cli.bin` is set but the target `Cargo.toml` has no `[[bin]]` table with that name (and the implicit-bin name derived from `[package].name` does not match either). |
|
|
1609
|
+
| `PIOT_CRATES_FEATURE_NOT_DECLARED` | `features` (on `kind = "crates"` packages) or `bundle_cli.features` references a feature not declared in `[features]`. |
|
|
1610
|
+
| `PIOT_CRATES_WORKSPACE_VERSION_MISMATCH` | `[package].version.workspace = true` but no ancestor `Cargo.toml` declares `[workspace.package].version`. |
|
|
1611
|
+
|
|
1612
|
+
**Deprecations removed.** None.
|
|
1613
|
+
|
|
1614
|
+
**Behavior changes without code changes.** Repos with one of the
|
|
1615
|
+
shapes above used to get a confusing mid-release error from
|
|
1616
|
+
maturin / setuptools / hatchling / cargo (sometimes after a
|
|
1617
|
+
verification build of every transitive dep); they now get a
|
|
1618
|
+
fingerprintable `PIOT_*` error at preflight time, before any side
|
|
1619
|
+
effects. PR-time `check.yml` runs surface the same findings on
|
|
1620
|
+
every pull request, so the typical case is fix-before-merge rather
|
|
1621
|
+
than fix-after-release-red. The `[build-system].build-backend`
|
|
1622
|
+
check is deliberately narrow: a missing `[build-system]` table is
|
|
1623
|
+
allowed (pip falls back to setuptools), and the prefix match
|
|
1624
|
+
tolerates backend-version drift across maturin / setuptools /
|
|
1625
|
+
hatchling.
|
|
1626
|
+
|
|
1627
|
+
**Verification.** Misconfigure one field, run `pnpm putitoutthere
|
|
1628
|
+
check` (or open a PR with `check.yml` wired), see the relevant
|
|
1629
|
+
`PIOT_*` code surface in seconds instead of mid-release.
|
|
1630
|
+
|
|
399
1631
|
### New `check.yml` reusable workflow for PR-time config sanity
|
|
400
1632
|
|
|
401
1633
|
**Summary.** `putitoutthere` now ships a third reusable workflow,
|
|
@@ -805,6 +2037,48 @@ step (one in the build matrix, one in the publish-job rebuild)
|
|
|
805
2037
|
when the strict install fails; healthy installs see no
|
|
806
2038
|
warning.
|
|
807
2039
|
|
|
2040
|
+
### Platform-publish `.npmrc` lookup
|
|
2041
|
+
|
|
2042
|
+
**Summary.** The reusable workflow's per-triple platform-package
|
|
2043
|
+
publishes (`build = "bundled-cli"` / `build = "napi"`) ran `npm
|
|
2044
|
+
publish` from a temporary staging directory rather than from the
|
|
2045
|
+
consumer's package path. npm reads `.npmrc` from cwd upward; the
|
|
2046
|
+
consumer's `.npmrc` lives at `pkg.path`, never on the path to a
|
|
2047
|
+
tempdir, so platform publishes never saw the auth the main package
|
|
2048
|
+
relied on. OIDC trusted publishing masked the gap (auth flows via
|
|
2049
|
+
the `ACTIONS_ID_TOKEN_REQUEST_TOKEN` environment variable, not
|
|
2050
|
+
`.npmrc`), but the `NPM_TOKEN` bootstrap path (#310) — required
|
|
2051
|
+
for the very first publish of a brand-new npm package — and the
|
|
2052
|
+
internal Verdaccio e2e seam (#304) both broke because both rely on
|
|
2053
|
+
`.npmrc`-supplied auth.
|
|
2054
|
+
|
|
2055
|
+
The engine now invokes `npm publish <stagingDir>` with `cwd:
|
|
2056
|
+
pkg.path`, matching how the main-package publish already runs.
|
|
2057
|
+
npm reads the consumer's `.npmrc` (including any `_authToken`,
|
|
2058
|
+
`always-auth`, or scoped-registry entries) and applies it to the
|
|
2059
|
+
PUT for each per-triple platform package.
|
|
2060
|
+
|
|
2061
|
+
**Required changes.** None. The fix is internal to
|
|
2062
|
+
`src/handlers/npm-platform.ts`; consumer `release.yml` flows are
|
|
2063
|
+
unchanged.
|
|
2064
|
+
|
|
2065
|
+
**Deprecations removed.** None.
|
|
2066
|
+
|
|
2067
|
+
**Behavior changes without code changes.** Consumers who rely on
|
|
2068
|
+
`NPM_TOKEN` (rather than OIDC) for the first publish of a
|
|
2069
|
+
bundled-cli / napi family — i.e. a brand-new npm package whose
|
|
2070
|
+
per-triple sub-packages also don't exist yet — now succeed without
|
|
2071
|
+
the workaround of publishing a `0.0.0-bootstrap` stub by hand.
|
|
2072
|
+
OIDC consumers see no observable difference: the same env-derived
|
|
2073
|
+
auth keeps flowing because `npm publish` reads
|
|
2074
|
+
`NODE_AUTH_TOKEN`/`ACTIONS_ID_TOKEN_REQUEST_TOKEN` from the
|
|
2075
|
+
environment regardless of which directory cwd points at.
|
|
2076
|
+
|
|
2077
|
+
**Verification.** A consumer publishing a brand-new bundled-cli /
|
|
2078
|
+
napi family via `NPM_TOKEN` completes per-triple sub-package
|
|
2079
|
+
publishes alongside the main package on the first release run,
|
|
2080
|
+
with no `npm publish (platform) failed` errors in the log.
|
|
2081
|
+
|
|
808
2082
|
### `[package.bundle_cli]` now actually stages the binary
|
|
809
2083
|
|
|
810
2084
|
**Summary.** Wheels published from a maturin pypi package that
|