putitoutthere 0.2.46 → 0.2.48
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/package.json +3 -7
- package/CHANGELOG.md +0 -293
- package/MIGRATIONS.md +0 -4045
- package/README.md +0 -1237
- package/action.yml +0 -48
package/MIGRATIONS.md
DELETED
|
@@ -1,4045 +0,0 @@
|
|
|
1
|
-
# Migration guide
|
|
2
|
-
|
|
3
|
-
How to upgrade between versions of `putitoutthere`. Sections are ordered
|
|
4
|
-
newest-first; each one is self-contained. Every observable change to
|
|
5
|
-
public API gets a section — additive changes as well as breaking ones —
|
|
6
|
-
because versioning is not yet strictly semver.
|
|
7
|
-
|
|
8
|
-
Each section covers five things, in order:
|
|
9
|
-
|
|
10
|
-
1. **Summary** — what changed and why.
|
|
11
|
-
2. **Required changes** — before/after diffs for config, CLI flags, and
|
|
12
|
-
action inputs.
|
|
13
|
-
3. **Deprecations removed** — anything previously warned about that is
|
|
14
|
-
now gone.
|
|
15
|
-
4. **Behavior changes without code changes** — same API, different
|
|
16
|
-
runtime behavior (tag format, exit codes, default values).
|
|
17
|
-
5. **Verification** — commands you can run to confirm the upgrade
|
|
18
|
-
worked, with the expected output.
|
|
19
|
-
|
|
20
|
-
---
|
|
21
|
-
|
|
22
|
-
## Unreleased
|
|
23
|
-
|
|
24
|
-
### New `verify bundle-cli` subcommand
|
|
25
|
-
|
|
26
|
-
**Summary.** `putitoutthere verify bundle-cli` completes the `verify` command
|
|
27
|
-
family (alongside `verify posture`, `verify npm-tarball`, `verify crate`, and
|
|
28
|
-
`verify wheel`). It asserts a maturin bundled-CLI wheel under `<path>/dist`
|
|
29
|
-
contains its cross-compiled binary at a path ending `<stage_suffix>/<bin>`,
|
|
30
|
-
where `stage_suffix` is `--stage-to` with `[tool.maturin].python-source` (or
|
|
31
|
-
the legacy `python_source`) subtracted from the front, and `<bin>` gains a
|
|
32
|
-
`.exe` suffix on a Windows `--target`. It codifies, as one colocated-tested
|
|
33
|
-
engine command, the inline "bundle_cli — verify wheel contains
|
|
34
|
-
`<stage_to>/<bin>`" bash block the reusable release workflow carries
|
|
35
|
-
(`_matrix.yml`, #282/#358); the behavior is unchanged — same wheel selection,
|
|
36
|
-
same python-source stripping, same match, same error messages and exit code.
|
|
37
|
-
The wheel is read with the same dependency-free pure-Node zip reader as
|
|
38
|
-
`verify wheel` (no `unzip`), so it runs on every platform the maturin matrix
|
|
39
|
-
builds on, Windows included. Unlike the earlier siblings' single copies —
|
|
40
|
-
which lived in `e2e-fixture-job.yml`, a workflow that runs `node
|
|
41
|
-
dist/cli-bin.js verify …` directly — this check's only copy lives in
|
|
42
|
-
`_matrix.yml`, which invokes the engine solely via `action.yml` (no `verify`
|
|
43
|
-
surface) and builds no `dist/`; that inline copy therefore stays in place
|
|
44
|
-
until the reusable workflow gains a way to call the engine, with the tested
|
|
45
|
-
command as the single source of truth going forward.
|
|
46
|
-
|
|
47
|
-
**Required changes.** None. Additive. Consumers compose with the reusable
|
|
48
|
-
workflow, not the CLI directly (design-commitment #10); this subcommand is
|
|
49
|
-
an internal seam.
|
|
50
|
-
|
|
51
|
-
**Deprecations removed.** None.
|
|
52
|
-
|
|
53
|
-
**Behavior changes without code changes.** None.
|
|
54
|
-
|
|
55
|
-
**Verification.** With a maturin bundled-CLI wheel under `<path>/dist`,
|
|
56
|
-
`putitoutthere verify bundle-cli --path <path> --stage-to <dir> --bin <name> --target <triple>`
|
|
57
|
-
prints `ok bundle_cli: <stage_suffix>/<bin> present in <wheel>` and exits 0
|
|
58
|
-
when the binary is staged; a wheel missing it exits 1 with `wheel <name>
|
|
59
|
-
missing bundle_cli binary at <stage_suffix>/<bin>` followed by a `wheel
|
|
60
|
-
contents:` listing, and a package with no wheel under `dist/` exits 1 with
|
|
61
|
-
`no wheel produced under <dir>`.
|
|
62
|
-
|
|
63
|
-
---
|
|
64
|
-
|
|
65
|
-
### New `verify wheel` subcommand
|
|
66
|
-
|
|
67
|
-
**Summary.** `putitoutthere verify wheel` joins the `verify` command family
|
|
68
|
-
(alongside `verify posture`, `verify npm-tarball`, and `verify crate`). It
|
|
69
|
-
asserts a built maturin artifact under `<path>/dist` carries the planned
|
|
70
|
-
version: a wheel row's first `*.whl` must have a `*.dist-info/METADATA`
|
|
71
|
-
`Version:` equal to `--version`, and an sdist row (`--target sdist`) must
|
|
72
|
-
produce a `*.tar.gz` whose filename contains it. This extracts the inline
|
|
73
|
-
"Verify wheel/sdist version matches matrix.version" bash block the reusable
|
|
74
|
-
e2e workflow carried (`e2e-fixture-job.yml`, #276) into one colocated-tested
|
|
75
|
-
engine command (epic #442, sub-issue #450); the behavior is unchanged — same
|
|
76
|
-
file selection, same error/ok messages, same exit code. The wheel's METADATA
|
|
77
|
-
is read with a dependency-free pure-Node zip reader (no `unzip`), so the
|
|
78
|
-
command runs on every platform the maturin matrix builds on.
|
|
79
|
-
|
|
80
|
-
**Required changes.** None. Additive. Consumers compose with the reusable
|
|
81
|
-
workflow, not the CLI directly (design-commitment #10); this subcommand is
|
|
82
|
-
an internal seam the workflow invokes.
|
|
83
|
-
|
|
84
|
-
**Deprecations removed.** None.
|
|
85
|
-
|
|
86
|
-
**Behavior changes without code changes.** None.
|
|
87
|
-
|
|
88
|
-
**Verification.** With a maturin build under `<path>/dist`,
|
|
89
|
-
`putitoutthere verify wheel --path <path> --version <v> --target <triple>`
|
|
90
|
-
prints `ok wheel: <name>.whl METADATA Version=<v>` and exits 0 when the
|
|
91
|
-
wheel carries `<v>`; a mismatch exits 1 with
|
|
92
|
-
`wheel METADATA Version='<actual>' but plan='<v>'`. `--target sdist` checks
|
|
93
|
-
the sdist filename instead (`ok sdist:` / `does not contain planned
|
|
94
|
-
version`).
|
|
95
|
-
|
|
96
|
-
---
|
|
97
|
-
|
|
98
|
-
### New `verify crate` subcommand
|
|
99
|
-
|
|
100
|
-
**Summary.** `putitoutthere verify crate` joins the `verify` command family
|
|
101
|
-
(alongside `verify posture` and `verify npm-tarball`). It reads a published
|
|
102
|
-
`.crate` back off the `cargo-http-registry` disk root the engine published
|
|
103
|
-
to, extracts it, and asserts the crate's source tree (`src/lib.rs` or
|
|
104
|
-
`src/main.rs`) is present — proof the publish shipped real contents, not an
|
|
105
|
-
empty or silently no-op'd tarball. This extracts the inline `.crate`
|
|
106
|
-
verification bash block the reusable e2e workflow carried
|
|
107
|
-
(`e2e-fixture-job.yml`, #334) into one colocated-tested engine command
|
|
108
|
-
(epic #442, sub-issue #449); the behavior is unchanged — same crates-row
|
|
109
|
-
selection, same present-and-non-empty guard, same error messages. Unlike
|
|
110
|
-
the npm sibling, it reads a local disk root (same host, same job), so it
|
|
111
|
-
takes `--registry-root <dir>` and performs no HTTP download or retry.
|
|
112
|
-
|
|
113
|
-
**Required changes.** None. Additive. Consumers compose with the reusable
|
|
114
|
-
workflow, not the CLI directly (design-commitment #10); this subcommand is
|
|
115
|
-
an internal seam the workflow invokes.
|
|
116
|
-
|
|
117
|
-
**Deprecations removed.** None.
|
|
118
|
-
|
|
119
|
-
**Behavior changes without code changes.** None.
|
|
120
|
-
|
|
121
|
-
**Verification.** With a published `.crate` under `<root>`,
|
|
122
|
-
`putitoutthere verify crate --matrix '[{"name":"c","kind":"crates","version":"1.0.0"}]' --registry-root <root>`
|
|
123
|
-
prints `ok: <path> contains src/lib.rs or src/main.rs` and exits 0; a
|
|
124
|
-
missing/empty `.crate` exits 1 with `no .crate file found (or empty)`, and
|
|
125
|
-
a source-less `.crate` exits 1 with
|
|
126
|
-
`.crate tarball missing src/lib.rs and src/main.rs`.
|
|
127
|
-
|
|
128
|
-
---
|
|
129
|
-
|
|
130
|
-
### New `verify npm-tarball` subcommand; `verify` becomes a command family
|
|
131
|
-
|
|
132
|
-
**Summary.** `putitoutthere verify` is now a command family. Bare `verify`
|
|
133
|
-
— the #414 OIDC-vs-token publish/trust posture check — is unchanged and
|
|
134
|
-
additionally spellable `verify posture`. A new subcommand,
|
|
135
|
-
`verify npm-tarball`, downloads a published npm tarball back from the
|
|
136
|
-
registry and asserts its contents honor what was declared: main/noarch
|
|
137
|
-
rows must contain every directory entry their `package.json` `files[]`
|
|
138
|
-
declares, and `--per-triple` asserts each synthesized platform package
|
|
139
|
-
ships a non-`package.json` binary. This extracts the two inline bash
|
|
140
|
-
blocks the reusable e2e workflow carried (`e2e-fixture-job.yml`) into one
|
|
141
|
-
colocated-tested engine command (epic #442, sub-issue #443); the behavior
|
|
142
|
-
is unchanged — same row selection, same registry read + retry, same error
|
|
143
|
-
messages.
|
|
144
|
-
|
|
145
|
-
**Required changes.** None. Additive. Consumers compose with the reusable
|
|
146
|
-
workflow, not the CLI directly (design-commitment #10); this subcommand is
|
|
147
|
-
an internal seam the workflow invokes. Anyone scripting `verify` directly
|
|
148
|
-
keeps the same behavior — bare `verify` still means the posture check.
|
|
149
|
-
|
|
150
|
-
**Deprecations removed.** None.
|
|
151
|
-
|
|
152
|
-
**Behavior changes without code changes.** None. The reusable workflow's
|
|
153
|
-
externally observable behavior (what publishes, tag format, release body)
|
|
154
|
-
is identical; only the internal implementation of the post-publish
|
|
155
|
-
tarball-verification step moved from inline YAML into the engine.
|
|
156
|
-
|
|
157
|
-
**Verification.** Run `putitoutthere verify npm-tarball --matrix <plan
|
|
158
|
-
matrix> --cwd <repo>` after a publish: it prints `ok: package/<dir>/` per
|
|
159
|
-
honored `files[]` directory and exits non-zero (with a `::error::` naming
|
|
160
|
-
the missing directory) if the published tarball dropped one.
|
|
161
|
-
|
|
162
|
-
### GitHub Release creation touches only the release's own tags
|
|
163
|
-
|
|
164
|
-
**Summary.** The reusable workflow's `Create GitHub Release(s) for new
|
|
165
|
-
tag(s)` step no longer runs a blanket `git fetch --tags origin`, and now
|
|
166
|
-
pushes each of the release's tags ref-scoped (`git push origin
|
|
167
|
-
"refs/tags/$tag"`, idempotent) before `gh release create`. Previously the
|
|
168
|
-
un-forced fetch coupled the step to the state of **every** tag in the
|
|
169
|
-
consumer's repo: a floating major tag (e.g. `v0`) force-moved mid-run by
|
|
170
|
-
the consumer's own promotion automation failed the publish job with
|
|
171
|
-
`! [rejected] v0 -> v0 (would clobber existing tag)` *after* every
|
|
172
|
-
registry publish and per-package tag push had succeeded — and because
|
|
173
|
-
consumer automation gates promotion on this job's conclusion, the release
|
|
174
|
-
was published but never promoted, with no safe job-level rerun (the
|
|
175
|
-
replayed publish re-plans against the already-pushed tags and hard-fails
|
|
176
|
-
on an empty plan). Observed twice in two days on
|
|
177
|
-
thekevinscott/testing-conventions (#436). The ref-scoped push also
|
|
178
|
-
completes the engine's deliberately warn-only tag push (#407) within the
|
|
179
|
-
same run, closing the second observed variant (`tag ... exists locally
|
|
180
|
-
but has not been pushed`).
|
|
181
|
-
|
|
182
|
-
**Required changes.** None. The step is workflow-internal; no config,
|
|
183
|
-
input, or consumer-side YAML changes.
|
|
184
|
-
|
|
185
|
-
**Deprecations removed.** None.
|
|
186
|
-
|
|
187
|
-
**Behavior changes without code changes.**
|
|
188
|
-
|
|
189
|
-
- A publish run no longer fails when any tag it does not own (a floating
|
|
190
|
-
major tag, another run's tags) moves between checkout and Release
|
|
191
|
-
creation.
|
|
192
|
-
- A per-package tag that the engine created but could not push (its push
|
|
193
|
-
is warn-only, #407) is now pushed by this step in the same run, so the
|
|
194
|
-
GitHub Release is cut instead of the job failing one step later.
|
|
195
|
-
- A genuine conflict — the same version tag already on the remote at a
|
|
196
|
-
*different* commit — still fails loudly at the ref-scoped push. That
|
|
197
|
-
means two runs released the same version, which the
|
|
198
|
-
`putitoutthere-release-*` concurrency group exists to prevent.
|
|
199
|
-
|
|
200
|
-
**Verification.** Land two release-triggering pushes to `main` a few
|
|
201
|
-
minutes apart in a repo whose promotion automation force-moves a floating
|
|
202
|
-
major tag on release success (the thekevinscott/testing-conventions
|
|
203
|
-
shape): the second run's publish job completes green and its GitHub
|
|
204
|
-
Releases exist, where it previously failed at `Create GitHub Release(s)
|
|
205
|
-
for new tag(s)`. On any release run's log, the step shows a per-tag
|
|
206
|
-
`git push origin "refs/tags/<tag>"` and no `git fetch`.
|
|
207
|
-
|
|
208
|
-
### napi `.node` embeds the release version
|
|
209
|
-
|
|
210
|
-
**Summary.** `kind = "npm"` `build = "napi"` releases now rewrite the napi
|
|
211
|
-
crate's version to the planned release version before `napi build` compiles
|
|
212
|
-
the `.node`, mirroring the maturin (#276) and bundled-cli (#366) pre-build
|
|
213
|
-
bumps. Previously the compiled `.node` embedded whatever `[package].version`
|
|
214
|
-
literal sat on disk, so a library re-exposing the Rust core's `version()`
|
|
215
|
-
through napi reported a version diverging from the published npm package
|
|
216
|
-
(whose `package.json` version was already correct).
|
|
217
|
-
|
|
218
|
-
**Required changes.** None for the common case. The bump is a
|
|
219
|
-
workflow-internal step; a consumer whose napi crate's `Cargo.toml` is
|
|
220
|
-
colocated with `package.json` (the napi-rs single-crate default, including
|
|
221
|
-
the `version.workspace = true` workspace shape via #428) needs no config
|
|
222
|
-
change. A **multi-mode** package (`build = ["napi", "bundled-cli"]`) or one
|
|
223
|
-
whose napi crate lives outside the package directory has no `Cargo.toml` at
|
|
224
|
-
the package path — piot skips the bump there with a `::notice::` (no
|
|
225
|
-
per-napi `crate_path` config exists yet), so such a `.node` still embeds
|
|
226
|
-
its on-disk `CARGO_PKG_VERSION`. Until a `crate_path` field lands, bump that
|
|
227
|
-
crate's version yourself or colocate its manifest with `package.json`.
|
|
228
|
-
|
|
229
|
-
**Deprecations removed.** None.
|
|
230
|
-
|
|
231
|
-
**Behavior changes without code changes.** For a napi package, the
|
|
232
|
-
per-platform `.node` now reports the released version from any API sourced
|
|
233
|
-
on `CARGO_PKG_VERSION` (e.g. a Rust `version()` re-exported through napi).
|
|
234
|
-
The synthesized per-platform `package.json` version is unchanged (it was
|
|
235
|
-
already `matrix.version`). Non-napi npm packages are unaffected.
|
|
236
|
-
|
|
237
|
-
**Verification.** Release a napi package and read its Rust-sourced
|
|
238
|
-
`version()` from the installed addon (or inspect the crate manifest the
|
|
239
|
-
build rewrote): it matches the published npm version, not the pre-release
|
|
240
|
-
on-disk literal.
|
|
241
|
-
|
|
242
|
-
### Version bump follows Cargo workspace inheritance
|
|
243
|
-
|
|
244
|
-
**Summary.** piot's pre-build version rewrite — the maturin `write-version`
|
|
245
|
-
step (#276) and the npm / pypi bundled-cli `write-crate-version` step
|
|
246
|
-
(#366) — now understands Cargo **workspace version inheritance**. A crate
|
|
247
|
-
whose `[package]` declares `version.workspace = true` and inherits its
|
|
248
|
-
version from `[workspace.package].version` at the workspace root (the
|
|
249
|
-
standard shape for a single Rust core re-exposed as a PyO3 wheel and a napi
|
|
250
|
-
addon) previously failed these steps with `Cargo.toml: no [package].version
|
|
251
|
-
field found`. The rewrite now resolves the inheritance: a literal
|
|
252
|
-
`[package].version` is bumped in place, while an inheriting member's version
|
|
253
|
-
is bumped at the workspace root's `[workspace.package].version`.
|
|
254
|
-
|
|
255
|
-
**Required changes.** None. Additive — a repo layout that previously errored
|
|
256
|
-
at release now works. If you kept a duplicated literal `[package].version`
|
|
257
|
-
in each crate to work around this, you may switch the members to
|
|
258
|
-
`version.workspace = true` and let piot bump the workspace root.
|
|
259
|
-
|
|
260
|
-
**Deprecations removed.** None.
|
|
261
|
-
|
|
262
|
-
**Behavior changes without code changes.** A `kind = "pypi"`
|
|
263
|
-
`build = "maturin"` or `kind = "npm"` `build = "bundled-cli"` package whose
|
|
264
|
-
crate inherits its version from the workspace no longer fails the build
|
|
265
|
-
step; the produced wheel / cross-compiled binary now carries the planned
|
|
266
|
-
release version. Single-crate layouts with a literal `[package].version` are
|
|
267
|
-
byte-for-byte unchanged.
|
|
268
|
-
|
|
269
|
-
**Verification.** In a cargo workspace whose root declares
|
|
270
|
-
`[workspace.package]` `version = "X"` and whose maturin/napi crate sets
|
|
271
|
-
`version.workspace = true`, run a release: the published wheel / `.node`
|
|
272
|
-
carries the planned release version (not the stale on-disk `X`), and the
|
|
273
|
-
workspace-root `Cargo.toml` is the manifest rewritten during the build.
|
|
274
|
-
|
|
275
|
-
### verify: publish trust posture
|
|
276
|
-
|
|
277
|
-
**Summary.** New command `putitoutthere verify` reports, per package, how
|
|
278
|
-
its latest release authenticated to the registry — `oidc` (a
|
|
279
|
-
trusted-publisher / provenance attestation is present, so the long-lived
|
|
280
|
-
token can be dropped), `token` (no such attestation), `unpublished`, or
|
|
281
|
-
`unreachable`. Read from public registry trust attribution (crates.io
|
|
282
|
-
`trustpub_data`, npm provenance attestations, PyPI PEP 740 provenance) —
|
|
283
|
-
no secrets. `--check` exits non-zero on any token-dependent package, so a
|
|
284
|
-
team can gate CI on reaching the zero-secret OIDC steady state.
|
|
285
|
-
|
|
286
|
-
**Required changes.** None. Additive — a new command.
|
|
287
|
-
|
|
288
|
-
**Deprecations removed.** None.
|
|
289
|
-
|
|
290
|
-
**Behavior changes without code changes.** None — new surface.
|
|
291
|
-
|
|
292
|
-
**Verification.** Run `verify` on a repo whose packages publish via OIDC
|
|
293
|
-
trusted publishers: each shows `oidc` and `verify --check` exits zero. A
|
|
294
|
-
package last published with a long-lived token shows `token` and
|
|
295
|
-
`verify --check` exits non-zero. `--json` emits the same rows.
|
|
296
|
-
|
|
297
|
-
### `status`: registry-vs-tag drift report
|
|
298
|
-
|
|
299
|
-
**Summary.** New read-only command `putitoutthere status` reports, per
|
|
300
|
-
package, whether the latest git tag matches the registry's latest
|
|
301
|
-
published version, flagging drift — notably `published, untagged` (a
|
|
302
|
-
version live on the registry but missing its tag, which strands the
|
|
303
|
-
package). Reads public registry metadata only; no auth.
|
|
304
|
-
|
|
305
|
-
**Required changes.** None. Additive — a new command.
|
|
306
|
-
|
|
307
|
-
**Deprecations removed.** None.
|
|
308
|
-
|
|
309
|
-
**Behavior changes without code changes.** None — new surface.
|
|
310
|
-
|
|
311
|
-
**Verification.** Run `status` over a repo with a published-but-untagged
|
|
312
|
-
package: that package shows `published, untagged` and `status --check`
|
|
313
|
-
exits non-zero; a fully in-sync repo shows every package `in sync` and
|
|
314
|
-
exits zero. `--json` emits the same rows as JSON.
|
|
315
|
-
|
|
316
|
-
### plan: publish-skip verdict and skew
|
|
317
|
-
|
|
318
|
-
**Summary.** `putitoutthere plan` now always reports, per planned package,
|
|
319
|
-
whether a release from this ref would `PUBLISH` (the version is not yet on
|
|
320
|
-
the registry), `SKIP` (already published), or `UNKNOWN` (the registry
|
|
321
|
-
couldn't be reached) — and flags **version skew** when a package would
|
|
322
|
-
`PUBLISH` while a `depends_on` dependency `SKIP`s. It reuses the real
|
|
323
|
-
planner and the same `isPublished` the publish path runs, so the preview
|
|
324
|
-
matches reality. Always-on — there is no flag.
|
|
325
|
-
|
|
326
|
-
**Required changes.** None for consumers of the reusable workflow. The
|
|
327
|
-
matrix the workflow consumes (`outputs.matrix`, from the JS action's
|
|
328
|
-
`$GITHUB_OUTPUT`) is **unchanged** — a bare array of build rows, byte for
|
|
329
|
-
byte. Only a caller that parses `plan --json` **stdout** directly is
|
|
330
|
-
affected (see below).
|
|
331
|
-
|
|
332
|
-
**Deprecations removed.** None.
|
|
333
|
-
|
|
334
|
-
**Behavior changes without code changes.**
|
|
335
|
-
|
|
336
|
-
| | Before | After |
|
|
337
|
-
|---|---|---|
|
|
338
|
-
| `plan --json` stdout | `[ …matrix rows… ]` | `{ "matrix": [ …matrix rows… ], "verdicts": [ … ], "skew": [ … ] }` |
|
|
339
|
-
| `plan` human output | matrix rows only | matrix rows + a `publish plan:` section + a `⚠ version skew` line when applicable |
|
|
340
|
-
| reusable-workflow `outputs.matrix` | bare array | bare array (unchanged) |
|
|
341
|
-
|
|
342
|
-
The `matrix` field is identical to the old top-level array; a direct
|
|
343
|
-
stdout reader migrates by reading `.matrix`. A registry blip degrades a
|
|
344
|
-
verdict to `UNKNOWN` and still emits the matrix — `plan` never aborts on
|
|
345
|
-
an unreachable registry.
|
|
346
|
-
|
|
347
|
-
**Verification.** Run `plan` on a ref where one package's planned version
|
|
348
|
-
is already published and a dependent's is not: the published one shows
|
|
349
|
-
`SKIP`, the dependent `PUBLISH`, and a `version skew` warning names the
|
|
350
|
-
pair. `plan --json` carries the same under `verdicts` / `skew`. The build
|
|
351
|
-
matrix (and a real release) are unaffected.
|
|
352
|
-
|
|
353
|
-
### reconcile: backfill missing tags
|
|
354
|
-
|
|
355
|
-
**Summary.** New command `putitoutthere reconcile` backfills the missing
|
|
356
|
-
git tag for every package that is live on its registry but untagged
|
|
357
|
-
(`status`'s `published, untagged` drift). It is the on-demand companion to
|
|
358
|
-
the publish-path auto-heal: auto-heal only fires for a package already in a
|
|
359
|
-
publish run, so a package whose globs never change again stays stuck;
|
|
360
|
-
`reconcile` heals it without a release. It reuses the same `computeStatus`
|
|
361
|
-
detection `status` reports and the same idempotent `ensureTag` the publish
|
|
362
|
-
path heals with — no parallel logic.
|
|
363
|
-
|
|
364
|
-
**Required changes.** None. Additive — a new command.
|
|
365
|
-
|
|
366
|
-
**Deprecations removed.** None.
|
|
367
|
-
|
|
368
|
-
**Behavior changes without code changes.** `--dry-run` is now accepted on
|
|
369
|
-
`reconcile` (it previews the heal without writing). It remains rejected on
|
|
370
|
-
`plan` / `publish`, unchanged from the #244 removal.
|
|
371
|
-
|
|
372
|
-
**Verification.** On a repo with a published-but-untagged package, run
|
|
373
|
-
`putitoutthere reconcile`: the missing tag is created (per the package's
|
|
374
|
-
`tag_format`) — pointed at a sibling package's tag commit for that version
|
|
375
|
-
when one exists, else `HEAD` — and pushed; `git tag` / GitHub show it, and
|
|
376
|
-
`status` then reports the package `in sync`. A second `reconcile` run is a
|
|
377
|
-
no-op. `reconcile --dry-run` reports what it would create without writing;
|
|
378
|
-
`--json` emits the actions.
|
|
379
|
-
|
|
380
|
-
### publish-path auto-heal: missing tags
|
|
381
|
-
|
|
382
|
-
**Summary.** `putitoutthere publish` now self-heals a missing git tag: when
|
|
383
|
-
a version is already live on the registry but has no tag, publish writes the
|
|
384
|
-
tag instead of skipping silently. Previously the already-published branch
|
|
385
|
-
returned before tagging, so a version that reached the registry on a
|
|
386
|
-
half-failed run (published, then the run died before the tag step) was
|
|
387
|
-
stranded published-but-untagged — and because piot derives "last released"
|
|
388
|
-
from tags, it skipped forever and could never bump, while dependents drifted
|
|
389
|
-
ahead into unflagged version skew.
|
|
390
|
-
|
|
391
|
-
**Required changes.** None. Purely additive behavior on the publish path.
|
|
392
|
-
|
|
393
|
-
**Deprecations removed.** None.
|
|
394
|
-
|
|
395
|
-
**Behavior changes without code changes.** On the next release run after
|
|
396
|
-
upgrading, any package that is live on a registry but missing its tag has the
|
|
397
|
-
tag created (and pushed) at the release commit, automatically. Idempotent:
|
|
398
|
-
packages already correctly tagged are untouched.
|
|
399
|
-
|
|
400
|
-
**Verification.** Re-run a release on a repo with a published-but-untagged
|
|
401
|
-
package: the run now creates the missing tag (per the package's `tag_format`;
|
|
402
|
-
visible via `git tag` / on GitHub) and the package resumes normal version
|
|
403
|
-
bumping on later releases.
|
|
404
|
-
|
|
405
|
-
### pypi version-independent wheels build once
|
|
406
|
-
|
|
407
|
-
**Summary.** A `kind = "pypi"` `build = "maturin"` package whose wheel is
|
|
408
|
-
Python-version-independent — `[tool.maturin].bindings = "bin"` (a
|
|
409
|
-
Rust-binary `py3-none` wheel) or a pyo3 `abi3` / `abi3-pyXY` extension (a
|
|
410
|
-
`cp3x-abi3` stable-ABI wheel) — used to be built once per CPython version
|
|
411
|
-
in the resolved set (inferred from `[project].requires-python`, or pinned
|
|
412
|
-
via `python_versions`). Because such a wheel is byte-identical no matter
|
|
413
|
-
which interpreter built it, the fan produced N duplicate wheels, and the
|
|
414
|
-
documented `pypi-publish` recipe's `merge-multiple: true` download then
|
|
415
|
-
race-corrupted the identical wheel filenames extracted onto one path
|
|
416
|
-
(`twine check` → `zipfile.BadZipFile`). The planner now collapses the fan
|
|
417
|
-
to a single wheel per target for these packages.
|
|
418
|
-
|
|
419
|
-
**Required changes.** None. Detection is automatic from your existing
|
|
420
|
-
`pyproject.toml` / `Cargo.toml`; no `putitoutthere.toml`, `release:`
|
|
421
|
-
trailer, or reusable-workflow input changes. The consumer `pypi-publish`
|
|
422
|
-
job is unchanged — its `pattern: '*-wheel-*'` still matches the (now
|
|
423
|
-
single) wheel artifact.
|
|
424
|
-
|
|
425
|
-
**Deprecations removed.** None.
|
|
426
|
-
|
|
427
|
-
**Behavior changes without code changes.** For a version-independent
|
|
428
|
-
maturin wheel resolving to more than one CPython version:
|
|
429
|
-
|
|
430
|
-
| | Before | After |
|
|
431
|
-
|---|--------|-------|
|
|
432
|
-
| Wheel build rows per target | one per resolved version (e.g. 6 for `>=3.9`) | one |
|
|
433
|
-
| Wheel artifact name | `<pkg>-wheel-<triple>-py<ver>` (per version) | `<pkg>-wheel-<triple>` (unsuffixed) |
|
|
434
|
-
| Build interpreter | each resolved version | newest resolved version only |
|
|
435
|
-
|
|
436
|
-
Ordinary per-version extension modules (no abi3, no `bindings = "bin"`)
|
|
437
|
-
are unaffected — they still fan and keep their `-py<ver>` suffixes. The
|
|
438
|
-
sdist row is unchanged. Detection is conservative: an abi3 setup the
|
|
439
|
-
engine doesn't recognize (a workspace-inherited `pyo3` dependency, a
|
|
440
|
-
`[target.'cfg(...)'.dependencies]` table) falls back to the prior fanning
|
|
441
|
-
behavior.
|
|
442
|
-
|
|
443
|
-
**Verification.** Run `putitoutthere plan` (or release) a maturin package
|
|
444
|
-
with `bindings = "bin"` or a pyo3 `abi3` feature and a `requires-python`
|
|
445
|
-
spanning multiple minors. The build matrix now shows a single
|
|
446
|
-
`<pkg>-wheel-<triple>` artifact per target instead of one per version, and
|
|
447
|
-
the `pypi-publish` job's `twine check` no longer fails with `BadZipFile`.
|
|
448
|
-
|
|
449
|
-
### npm `TLOG_CREATE_ENTRY_ERROR` (409) provenance retry race
|
|
450
|
-
|
|
451
|
-
**Summary.** `kind = "npm"` releases that publish with provenance
|
|
452
|
-
(`--provenance`, the OIDC trusted-publisher path) could abort mid-matrix
|
|
453
|
-
when npm's internal retry-on-transient-network-error re-submitted a
|
|
454
|
-
byte-identical attestation and Sigstore/Rekor rejected the duplicate with
|
|
455
|
-
`TLOG_CREATE_ENTRY_ERROR` (HTTP 409, "an equivalent entry already exists in
|
|
456
|
-
the transparency log"). Only the registry-PUT edition of that race (the
|
|
457
|
-
`E403` "cannot publish over the previously published versions" shape) was
|
|
458
|
-
tolerated; the attestation edition fell through to a hard failure, which
|
|
459
|
-
for a multi-platform (`napi` / `bundled-cli`) package left the remaining
|
|
460
|
-
sub-packages and the main package unpublished — a partial release. The
|
|
461
|
-
engine now recognizes the 409 and, because a 409 from Rekor does not by
|
|
462
|
-
itself prove the artifact reached the registry, re-probes `npm view` to
|
|
463
|
-
decide: present ⇒ benign duplicate (success); absent ⇒ a genuine partial
|
|
464
|
-
publish, reported as an actionable error.
|
|
465
|
-
|
|
466
|
-
**Required changes.** None. The behavior is automatic and applies to every
|
|
467
|
-
`kind = "npm"` package; no `putitoutthere.toml`, `release:` trailer, or
|
|
468
|
-
reusable-workflow input changes.
|
|
469
|
-
|
|
470
|
-
**Deprecations removed.** None.
|
|
471
|
-
|
|
472
|
-
**Behavior changes without code changes.** A `kind = "npm"` publish that
|
|
473
|
-
hits `TLOG_CREATE_ENTRY_ERROR` (409) no longer fails unconditionally:
|
|
474
|
-
|
|
475
|
-
| Situation | Before | After |
|
|
476
|
-
|-----------|--------|-------|
|
|
477
|
-
| 409 raised, package **is** on the registry | publish job fails (`npm publish (platform) failed: …`) | treated as `already-published` / counted as published; the cascade continues |
|
|
478
|
-
| 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 |
|
|
479
|
-
|
|
480
|
-
In the genuine partial-publish case the engine cannot recover in-process —
|
|
481
|
-
an orphaned attestation needs a new `runID`/attempt to produce a fresh
|
|
482
|
-
Rekor entry — so re-running the release is the documented remedy.
|
|
483
|
-
|
|
484
|
-
**Verification.** Re-run a release whose previous attempt died on
|
|
485
|
-
`TLOG_CREATE_ENTRY_ERROR`. If the sub-package already landed, the run now
|
|
486
|
-
reports it as already-published and proceeds to the remaining packages
|
|
487
|
-
instead of aborting; if it never landed, the error names the package and
|
|
488
|
-
tells you to re-run (which mints a new attestation and gets past the
|
|
489
|
-
dedupe).
|
|
490
|
-
|
|
491
|
-
### Bundled-CLI launcher generation no-ops without `[package.bundle_cli]`
|
|
492
|
-
|
|
493
|
-
**Summary.** #299 moved npm bundled-cli launcher generation into the
|
|
494
|
-
engine and invoked it on every bundled-cli package's main row.
|
|
495
|
-
`writeLauncherFromConfig` assumed `[package.bundle_cli]` was always
|
|
496
|
-
present and dereferenced `bundle_cli.bin` unconditionally, so a
|
|
497
|
-
bundled-cli npm package that omits the table — the legacy
|
|
498
|
-
"bring-your-own `scripts/build.cjs` + hand-authored `bin/<bin>.js`"
|
|
499
|
-
shape that #298 explicitly kept opt-in — crashed the release at the
|
|
500
|
-
`write-launcher` step with `Cannot read properties of undefined
|
|
501
|
-
(reading 'bin')`. Launcher generation now no-ops when the table is
|
|
502
|
-
absent, mirroring the cross-compile step's existing `matrix.bundle_cli`
|
|
503
|
-
gate. The gate lives in the engine, not the workflow `if:`, because the
|
|
504
|
-
main row the step runs on never carries `bundle_cli` (`plan.ts` attaches
|
|
505
|
-
it only to per-target bundled-cli rows).
|
|
506
|
-
|
|
507
|
-
**Required changes.** None. A bundled-cli package that declares
|
|
508
|
-
`[package.bundle_cli]` is unchanged — the engine still generates
|
|
509
|
-
`bin/<bin>.js` and the `package.json#bin` entry. A package that omits
|
|
510
|
-
the table and ships its own launcher (the legacy path) no longer crashes;
|
|
511
|
-
the engine leaves its launcher alone. A consumer that *intended* the
|
|
512
|
-
declarative path and simply forgot the table should add it — the
|
|
513
|
-
cross-compile and launcher generation both key off it:
|
|
514
|
-
|
|
515
|
-
| Before | After |
|
|
516
|
-
|--------|-------|
|
|
517
|
-
| `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 |
|
|
518
|
-
|
|
519
|
-
**Deprecations removed.** None.
|
|
520
|
-
|
|
521
|
-
**Behavior changes without code changes.** A bundled-cli npm package
|
|
522
|
-
without `[package.bundle_cli]` previously failed its release at the
|
|
523
|
-
`write-launcher` step (as of #299); it now completes, with the engine
|
|
524
|
-
authoring no launcher for it.
|
|
525
|
-
|
|
526
|
-
**Verification.** Trigger a release for a bundled-cli npm package that
|
|
527
|
-
omits `[package.bundle_cli]`: the run completes (previously it failed at
|
|
528
|
-
the `write-launcher` step). A package that declares the table still gets
|
|
529
|
-
its engine-generated launcher.
|
|
530
|
-
|
|
531
|
-
### Preflight npm package name must match configured name
|
|
532
|
-
|
|
533
|
-
**Summary.** Preflight gains `requirePackageJsonShape`, the npm analogue
|
|
534
|
-
of `requirePyprojectShape` / `requireCargoShape` (#301). Every cascaded
|
|
535
|
-
`kind = "npm"` package's `package.json` `name` must equal the configured
|
|
536
|
-
`[[package]].name` (or the `npm` override). `npm publish` packs the
|
|
537
|
-
manifest `name`, but the engine's idempotency probe (`npm view <name>`)
|
|
538
|
-
and the tag / release-URL bookkeeping use the configured name — so a
|
|
539
|
-
divergence silently breaks idempotency and can publish under an
|
|
540
|
-
unexpected name. pypi and crates already enforced the equivalent
|
|
541
|
-
(`PIOT_PYPI_NAME_MISMATCH`, `PIOT_CRATES_NAME_MISMATCH`); this closes the
|
|
542
|
-
gap for npm. The check fires at publish time alongside the existing
|
|
543
|
-
`require*` family and at PR time via `check.yml`. Findings aggregate
|
|
544
|
-
across every failing package.
|
|
545
|
-
|
|
546
|
-
**Required changes.** None for well-formed manifests — `package.json`
|
|
547
|
-
`name` already matches the configured name in the common case. The repos
|
|
548
|
-
that trip the new check are those that used a path-style identifier as
|
|
549
|
-
`[[package]].name` (e.g. `js/foo`) without setting the `npm` override
|
|
550
|
-
while shipping `package.json` `name = "foo"`; those previously published
|
|
551
|
-
with a broken `npm view` idempotency check and a wrong reported URL, and
|
|
552
|
-
now get a fast preflight red instead. Fix by aligning the names or
|
|
553
|
-
declaring the override:
|
|
554
|
-
|
|
555
|
-
| Before | After |
|
|
556
|
-
|--------|-------|
|
|
557
|
-
| `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) |
|
|
558
|
-
|
|
559
|
-
The new error code:
|
|
560
|
-
|
|
561
|
-
| Code | Fires when |
|
|
562
|
-
|------|------------|
|
|
563
|
-
| `PIOT_NPM_NAME_MISMATCH` | `package.json`'s `name` differs from `[[package]].name` (or the `npm` override). |
|
|
564
|
-
|
|
565
|
-
**Deprecations removed.** None.
|
|
566
|
-
|
|
567
|
-
**Behavior changes without code changes.** An npm package with a name
|
|
568
|
-
divergence that previously published (under the manifest name, with a
|
|
569
|
-
broken `npm view` idempotency check and a wrong reported URL) now fails
|
|
570
|
-
at preflight with a fingerprintable `PIOT_NPM_NAME_MISMATCH` before any
|
|
571
|
-
side effect. Scoped names are compared verbatim (`npm = "@scope/foo"`
|
|
572
|
-
matches `package.json` `name = "@scope/foo"`). A missing or malformed
|
|
573
|
-
`package.json` is left to the other checks / the publish step.
|
|
574
|
-
|
|
575
|
-
**Verification.** Set an npm package's `package.json` `name` to something
|
|
576
|
-
other than its configured `[[package]].name` (with no `npm` override),
|
|
577
|
-
run `pnpm putitoutthere check` (or open a PR with `check.yml` wired), and
|
|
578
|
-
see `PIOT_NPM_NAME_MISMATCH` surface in seconds instead of mid-release.
|
|
579
|
-
|
|
580
|
-
### `_matrix.yml` build job primes a cargo cache (#391)
|
|
581
|
-
|
|
582
|
-
**Summary.** `_matrix.yml`'s build job previously ran every per-target
|
|
583
|
-
matrix cell without any Cargo cache. Each cell cold-compiled the full
|
|
584
|
-
Rust dep graph on every PR — even PRs that touched nothing Rust-side —
|
|
585
|
-
because `~/.cargo/registry` and the per-package `target/` dir started
|
|
586
|
-
empty on every runner. On a wide bundle_cli / napi / maturin matrix
|
|
587
|
-
this dominated wall-clock: 4-6 min per cell, ~8 min end-to-end on a
|
|
588
|
-
typical downstream consumer's `release-precheck.yml` run, with no
|
|
589
|
-
headroom against a 10-min PR CI gate.
|
|
590
|
-
|
|
591
|
-
The build job now runs `Swatinem/rust-cache@v2` immediately after
|
|
592
|
-
`actions/checkout`, gated on rows that actually invoke cargo:
|
|
593
|
-
|
|
594
|
-
- `pypi/maturin` non-sdist (maturin shells out to cargo)
|
|
595
|
-
- `npm/napi` (the consumer's `napi build` script calls cargo)
|
|
596
|
-
- `npm/bundled-cli` non-main (the engine's `cargo build` for the
|
|
597
|
-
staged CLI binary)
|
|
598
|
-
|
|
599
|
-
The cache is partitioned by `matrix.target` via `shared-key` so a
|
|
600
|
-
write to one target's slot doesn't blow away the next cell's, and
|
|
601
|
-
`workspaces` enumerates both `matrix.path` (the consumer's package
|
|
602
|
-
crate, where maturin / napi / single-crate bundled-cli compile) and
|
|
603
|
-
`matrix.bundle_cli.crate_path` (the bundle_cli crate when it lives
|
|
604
|
-
in a separate dir from `matrix.path` — the dirsql shape). Rows that
|
|
605
|
-
produce no cargo work (pypi sdist, pure-Python hatch wheels, npm
|
|
606
|
-
vanilla, bundled-cli `main`) skip the cache step entirely.
|
|
607
|
-
|
|
608
|
-
**Required changes.** None — the cache is internal to the reusable
|
|
609
|
-
workflow. No consumer config, YAML, or scripts need to change.
|
|
610
|
-
|
|
611
|
-
**Deprecations removed.** None.
|
|
612
|
-
|
|
613
|
-
**Behavior changes without code changes.** Per-target matrix cells
|
|
614
|
-
that ran cargo cold previously will now restore their dep graph from
|
|
615
|
-
GitHub Actions cache storage on the second and subsequent matching
|
|
616
|
-
runs (matching = same `matrix.target` + same Cargo.lock contents).
|
|
617
|
-
Cache storage accrues against the consumer's repository quota — the
|
|
618
|
-
same accounting as any other `actions/cache` consumer, no separate
|
|
619
|
-
billing. First run after a `Cargo.lock` change recompiles cold and
|
|
620
|
-
takes the same wall-clock as before. The acceptance criterion: a
|
|
621
|
-
second matrix run with no `Cargo.lock` change finishes the Rust
|
|
622
|
-
compile step in well under one minute per cell.
|
|
623
|
-
|
|
624
|
-
**Verification.** A `build.yml` (or `release.yml`) run's logs now
|
|
625
|
-
show a `cargo cache (#391)` step between `Set up job` and the first
|
|
626
|
-
Rust-touching step, on every row that invokes cargo. The step logs
|
|
627
|
-
either `Cache hit` (subsequent run, no `Cargo.lock` change) or
|
|
628
|
-
`Cache not found` (first run, or after a `Cargo.lock` change) and
|
|
629
|
-
records a `~/.cargo` + `target/` save at job end. Pure-Python sdist
|
|
630
|
-
and npm vanilla rows do not show the step at all.
|
|
631
|
-
|
|
632
|
-
### npm bundled-cli: npm-flavor triples now mapped to Rust triples
|
|
633
|
-
|
|
634
|
-
**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:
|
|
635
|
-
|
|
636
|
-
```
|
|
637
|
-
error: toolchain 'stable-x86_64-unknown-linux-gnu' does not support target 'linux-x64-gnu'
|
|
638
|
-
```
|
|
639
|
-
|
|
640
|
-
Each affected step now contains an explicit `case` statement mapping napi-rs npm triples to their Rust equivalents before the `gnu→musl` swap.
|
|
641
|
-
|
|
642
|
-
**Required changes.** None — the mapping is inside the reusable workflow. No consumer config, YAML, or scripts need to change.
|
|
643
|
-
|
|
644
|
-
**Deprecations removed.** None.
|
|
645
|
-
|
|
646
|
-
**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.
|
|
647
|
-
|
|
648
|
-
**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> -> …`.
|
|
649
|
-
|
|
650
|
-
### `bundle_cli` stage step runs after consumer `npm run build`
|
|
651
|
-
|
|
652
|
-
**Summary.** The engine's `bundle_cli — stage binary` step previously ran
|
|
653
|
-
**before** `npm run build --if-present`. A consumer build script that also
|
|
654
|
-
runs `cargo build --target $TARGET` (the raw `-linux-gnu` triple) and copies
|
|
655
|
-
the result to `build/<triple>/` would overwrite the engine's musl binary with
|
|
656
|
-
a glibc-linked one; the existence-only verify check passed and the
|
|
657
|
-
dynamically-linked artifact shipped to the registry. The stage step now runs
|
|
658
|
-
**after** `npm run build --if-present` so the engine's statically-linked musl
|
|
659
|
-
binary always wins. The `bundle_cli — verify` step in `_matrix.yml` now also
|
|
660
|
-
asserts static linking (`file`/`ldd` check), mirroring the check already
|
|
661
|
-
present in `e2e-fixture-job.yml`.
|
|
662
|
-
|
|
663
|
-
**Required changes.** None — this is a pure step-reordering inside the
|
|
664
|
-
reusable workflow. No consumer-side YAML, config, or scripts need to change.
|
|
665
|
-
|
|
666
|
-
**Deprecations removed.** None.
|
|
667
|
-
|
|
668
|
-
**Behavior changes without code changes.** Consumer build scripts that stage
|
|
669
|
-
a binary to `build/<triple>/` as part of `npm run build` will have that binary
|
|
670
|
-
overwritten by the engine's musl binary. This was always the intended behavior;
|
|
671
|
-
the old ordering was a bug. Consumer scripts that only compile TypeScript or
|
|
672
|
-
do non-binary work are unaffected.
|
|
673
|
-
|
|
674
|
-
**Verification.** A `bundle_cli` Linux build job's logs now show:
|
|
675
|
-
1. `bundle_cli — cargo build for <triple>` (musl build, unchanged)
|
|
676
|
-
2. `npm run build --if-present` (consumer build step)
|
|
677
|
-
3. `bundle_cli — stage binary into build/<triple>` (engine stages musl binary)
|
|
678
|
-
4. `bundle_cli — verify … is statically linked` (passes)
|
|
679
|
-
|
|
680
|
-
### `bundle_cli` musl builds install `musl-tools` C cross-compiler
|
|
681
|
-
|
|
682
|
-
**Summary.** `rustup target add x86_64-unknown-linux-musl` registers the
|
|
683
|
-
Rust musl target but does not install the C cross-compiler
|
|
684
|
-
(`x86_64-linux-musl-gcc`). Crates that compile C source at build time —
|
|
685
|
-
`libsqlite3-sys` with `features = ["bundled"]`, `openssl-sys` with
|
|
686
|
-
`features = ["vendored"]`, and similar — invoke the C compiler directly
|
|
687
|
-
during `cargo build`; without `musl-gcc` present, the build fails with:
|
|
688
|
-
|
|
689
|
-
```
|
|
690
|
-
failed to find tool "x86_64-linux-musl-gcc": No such file or directory
|
|
691
|
-
```
|
|
692
|
-
|
|
693
|
-
The `musl-tools` apt package provides `musl-gcc` and is not pre-installed
|
|
694
|
-
on `ubuntu-latest`. `_matrix.yml` and `e2e-fixture-job.yml` now run
|
|
695
|
-
`sudo apt-get install -y musl-tools` and export `CC_<triple>=musl-gcc` to
|
|
696
|
-
`$GITHUB_ENV` before `cargo build`, gated on Linux targets.
|
|
697
|
-
|
|
698
|
-
**Required changes.** None — the step is added automatically by the
|
|
699
|
-
reusable workflow.
|
|
700
|
-
|
|
701
|
-
**Deprecations removed.** None.
|
|
702
|
-
|
|
703
|
-
**Behavior changes without code changes.** CLI crates that compile C
|
|
704
|
-
source and previously had to run a custom `build` script to install
|
|
705
|
-
`musl-tools` may be able to remove that script.
|
|
706
|
-
|
|
707
|
-
**Verification.** On a bundle_cli Linux build, the workflow now prints
|
|
708
|
-
`sudo apt-get install -y musl-tools` before the `cargo build` step.
|
|
709
|
-
Crates with C deps (e.g. `features = ["bundled"]` on `rusqlite`) compile
|
|
710
|
-
successfully without consumer-side workarounds.
|
|
711
|
-
|
|
712
|
-
### `bundle_cli` Linux binaries compiled as static musl
|
|
713
|
-
|
|
714
|
-
**Summary.** `bundle_cli`'s Linux cross-compile step previously ran
|
|
715
|
-
`cargo build --target $TARGET` directly on the GitHub-hosted runner.
|
|
716
|
-
That runner's glibc (currently 2.39 on Ubuntu 24.04) got baked into
|
|
717
|
-
the produced binary as a hard runtime requirement, so any older Linux
|
|
718
|
-
at install time failed with `./bin: /lib/x86_64-linux-gnu/libc.so.6:
|
|
719
|
-
version 'GLIBC_2.39' not found`. The fix derives a `BINARY_TARGET`
|
|
720
|
-
from `matrix.target` by substituting `-linux-gnu*` → `-linux-musl*`
|
|
721
|
-
and uses that for the three workflow steps that touch the binary's
|
|
722
|
-
compile triple. Statically-linked musl binaries have no glibc floor.
|
|
723
|
-
The package's declared target triple (used for npm platform-package
|
|
724
|
-
names, napi builds, wheel tags, artifact names) is unchanged; only
|
|
725
|
-
the binary inside switches compile triple.
|
|
726
|
-
|
|
727
|
-
**Required changes.** None for the common case — the workflow makes
|
|
728
|
-
the swap automatically. The exception: CLI crates that
|
|
729
|
-
dynamic-link a system C library through default cargo features will
|
|
730
|
-
see a linker error on the first release after upgrade. The static
|
|
731
|
-
musl build cannot satisfy a dynamic link against the host's glibc-world
|
|
732
|
-
libraries. The fix is a one-line `Cargo.toml` change per case:
|
|
733
|
-
|
|
734
|
-
| Symptom (cargo error mentions) | Fix in your CLI's `Cargo.toml` |
|
|
735
|
-
|--------------------------------|--------------------------------|
|
|
736
|
-
| `openssl-sys`, `libssl.so`, OpenSSL | Switch to `rustls` (`reqwest = { default-features = false, features = ["rustls-tls"] }`), or pin `openssl = { features = ["vendored"] }` |
|
|
737
|
-
| `git2`, `libgit2` | `git2 = { features = ["vendored-openssl", "vendored-libgit2"] }` |
|
|
738
|
-
| `libsqlite3-sys`, SQLite | `rusqlite = { features = ["bundled"] }` (or `libsqlite3-sys = { features = ["bundled"] }`) |
|
|
739
|
-
| `libpq`, Postgres client | Swap `postgres-native-tls` for `postgres-rustls`, or use `sqlx` with the `rustls` feature |
|
|
740
|
-
| `libmysqlclient`, MySQL client | Same — prefer a pure-Rust client; `mysqlclient-sys` has no clean static path |
|
|
741
|
-
|
|
742
|
-
The musl build fails loudly at release time when one of these is
|
|
743
|
-
missed — there is no silent broken-binary failure mode. A blocked
|
|
744
|
-
release is the worst outcome.
|
|
745
|
-
|
|
746
|
-
**Deprecations removed.** None.
|
|
747
|
-
|
|
748
|
-
**Behavior changes without code changes.** The bundled binary inside
|
|
749
|
-
a Linux package built before this release required the build
|
|
750
|
-
runner's glibc (currently 2.39); after this release it has no glibc
|
|
751
|
-
requirement and runs on any Linux ≥ kernel 3.2, including Alpine,
|
|
752
|
-
NixOS in musl mode, scratch containers, and any host whose glibc is
|
|
753
|
-
older than the build runner's. Package identifiers (npm platform-
|
|
754
|
-
package names like `@scope/cli-linux-x64-gnu`, PyPI wheel tags,
|
|
755
|
-
artifact upload names) are unchanged — the substitution applies only
|
|
756
|
-
to the binary's compile triple.
|
|
757
|
-
|
|
758
|
-
**Verification.** Install a `bundle_cli`-enabled package from any
|
|
759
|
-
pre-Ubuntu-24.04 Linux (Ubuntu 22.04, Debian 12, Amazon Linux 2,
|
|
760
|
-
Alpine) and run the CLI — no `GLIBC_2.x not found` error. On the
|
|
761
|
-
build side, the cargo invocation in CI now prints
|
|
762
|
-
`cargo build --release --target x86_64-unknown-linux-musl ...`
|
|
763
|
-
instead of `-gnu` for Linux rows; `file` on the produced binary
|
|
764
|
-
reports `statically linked` instead of `dynamically linked`.
|
|
765
|
-
|
|
766
|
-
### pypi `requires-python` includes CPython 3.14
|
|
767
|
-
|
|
768
|
-
**Summary.** Open-ended `requires-python` inference now expands against
|
|
769
|
-
putitoutthere's checked-in released-CPython list through CPython 3.14.
|
|
770
|
-
This fixes the stale-tail failure mode where `requires-python = ">=3.11"`
|
|
771
|
-
emitted cp311/cp312/cp313 wheels but omitted cp314 after Python 3.14 was
|
|
772
|
-
released.
|
|
773
|
-
|
|
774
|
-
**Required changes.** None.
|
|
775
|
-
|
|
776
|
-
| Before | After |
|
|
777
|
-
|--------|-------|
|
|
778
|
-
| `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. |
|
|
779
|
-
|
|
780
|
-
**Deprecations removed.** None.
|
|
781
|
-
|
|
782
|
-
**Behavior changes without code changes.** Consumers with open-ended
|
|
783
|
-
`requires-python` ranges may see an extra cp314 wheel row. Explicit
|
|
784
|
-
`python_versions` overrides are unchanged and still pin the exact wheel
|
|
785
|
-
set.
|
|
786
|
-
|
|
787
|
-
**Verification.** Push a release for a `kind = "pypi"` package whose
|
|
788
|
-
`pyproject.toml` declares `requires-python = ">=3.11"`; the build matrix
|
|
789
|
-
should include a `python_version: "3.14"` row, and the published PyPI
|
|
790
|
-
release should include a cp314 wheel.
|
|
791
|
-
|
|
792
|
-
### Manual release via `release_packages`
|
|
793
|
-
|
|
794
|
-
**Summary.** `release.yml` gained an optional `release_packages`
|
|
795
|
-
`workflow_call` input. When set, it triggers a manual release of an
|
|
796
|
-
explicit list of packages, bypassing change detection entirely. The
|
|
797
|
-
motivating case: putitoutthere ships a release-pipeline bug, the bug is
|
|
798
|
-
fixed, and downstream consumers must re-release the affected packages
|
|
799
|
-
even though their own repos have no new commits since the last tag —
|
|
800
|
-
the change-detected path emits an empty matrix in that state and cannot
|
|
801
|
-
release. The input value is a comma-separated list of
|
|
802
|
-
`name[@<bump|version>]` entries; each entry is a package name optionally
|
|
803
|
-
suffixed with `@<patch|minor|major>` (bump the last tag) or an explicit
|
|
804
|
-
`@<X.Y.Z>` semver (used verbatim). A bare name defaults to a patch bump.
|
|
805
|
-
Only the named packages are planned — no `depends_on` cascade, no
|
|
806
|
-
change-detected packages pulled in.
|
|
807
|
-
|
|
808
|
-
**Required changes.** None — the input is optional and defaults to
|
|
809
|
-
empty, which leaves the normal change-detected release path unchanged.
|
|
810
|
-
To get a manual-release button, wire the input to a `workflow_dispatch`
|
|
811
|
-
trigger in your caller `release.yml`:
|
|
812
|
-
|
|
813
|
-
| Before | After |
|
|
814
|
-
|--------|-------|
|
|
815
|
-
| <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> |
|
|
816
|
-
|
|
817
|
-
The push-triggered run passes an empty `release_packages` (the `inputs`
|
|
818
|
-
context is empty outside `workflow_dispatch`), so the normal path keeps
|
|
819
|
-
working.
|
|
820
|
-
|
|
821
|
-
**Deprecations removed.** None.
|
|
822
|
-
|
|
823
|
-
**Behavior changes without code changes.** None. The new behavior is
|
|
824
|
-
gated entirely on the new input being non-empty.
|
|
825
|
-
|
|
826
|
-
**Verification.** Trigger the workflow from the Actions tab with
|
|
827
|
-
`release_packages` set to a known package (e.g. `lib-core@patch`).
|
|
828
|
-
Confirm the plan job's matrix contains exactly that package and the
|
|
829
|
-
publish job tags and publishes it at the expected version.
|
|
830
|
-
|
|
831
|
-
### pypi multi-version wheels
|
|
832
|
-
|
|
833
|
-
**Summary.** `kind = "pypi"` packages now build a wheel for every
|
|
834
|
-
CPython version they support, instead of a single wheel for the
|
|
835
|
-
`python_version` workflow input. The version set is resolved per
|
|
836
|
-
package: an explicit `python_versions` array in `putitoutthere.toml`
|
|
837
|
-
wins; otherwise it is inferred from `[project].requires-python` in the
|
|
838
|
-
package's `pyproject.toml`; otherwise a single default (`3.12`) is
|
|
839
|
-
used. The build matrix fans `maturin` per-target wheel rows across the
|
|
840
|
-
resolved set. This closes the incomplete-coverage bug where a package
|
|
841
|
-
declaring `requires-python = ">=3.10"` shipped a cp312-only wheel and
|
|
842
|
-
failed to install on every other interpreter.
|
|
843
|
-
|
|
844
|
-
**Required changes.** None for the default path — `requires-python`
|
|
845
|
-
inference is automatic. Optionally pin a subset:
|
|
846
|
-
|
|
847
|
-
| Before | After |
|
|
848
|
-
|--------|-------|
|
|
849
|
-
| _(no knob; one wheel at `python_version`)_ | `python_versions = ["3.12", "3.13"]` under a `[[package]]` with `kind = "pypi"` |
|
|
850
|
-
|
|
851
|
-
Consumers whose caller-side `pypi-publish` job collects wheels from the
|
|
852
|
-
downloaded artifacts directory need no change as long as it globs all
|
|
853
|
-
wheel artifacts (the recommended recipe already does); multi-version
|
|
854
|
-
`maturin` wheel artifacts are now named
|
|
855
|
-
`<pkg>-wheel-<triple>-py<ver>` rather than `<pkg>-wheel-<triple>`. A
|
|
856
|
-
single planned version keeps the unsuffixed name.
|
|
857
|
-
|
|
858
|
-
**Deprecations removed.** None. The `python_version` input on
|
|
859
|
-
`release.yml`, `build.yml`, and `_matrix.yml` is now deprecated — it no
|
|
860
|
-
longer affects pypi builds — but is retained so existing callers do not
|
|
861
|
-
break. Remove it from your `with:` block at leisure.
|
|
862
|
-
|
|
863
|
-
**Behavior changes without code changes.** A pypi package whose
|
|
864
|
-
`requires-python` spans multiple versions now produces multiple wheels
|
|
865
|
-
where it previously produced one. The `python_version` workflow input
|
|
866
|
-
is inert for pypi builds.
|
|
867
|
-
|
|
868
|
-
**Verification.** Push a release for a `kind = "pypi"` package whose
|
|
869
|
-
`pyproject.toml` declares `requires-python = ">=3.11"`; the build job
|
|
870
|
-
fans into one wheel row per released version (for example `3.11`,
|
|
871
|
-
`3.12`, `3.13`, `3.14`), and the
|
|
872
|
-
published PyPI release carries a wheel for each.
|
|
873
|
-
|
|
874
|
-
### pypi bundle_cli binary embeds the release version
|
|
875
|
-
|
|
876
|
-
**Summary.** The reusable workflow's pypi `[package.bundle_cli]` path
|
|
877
|
-
already rewrote the maturin package's version source before building
|
|
878
|
-
wheels, but the cross-compiled CLI binary comes from the separate crate
|
|
879
|
-
at `bundle_cli.crate_path`. `cargo build` bakes `CARGO_PKG_VERSION`
|
|
880
|
-
from that crate's on-disk `Cargo.toml`, so a wheel could publish as
|
|
881
|
-
`0.3.6` while its bundled CLI reported an older literal such as
|
|
882
|
-
`0.2.7`. The build matrix now rewrites the bundle_cli crate's
|
|
883
|
-
`[package].version` to `matrix.version` immediately before the pypi
|
|
884
|
-
bundle_cli `cargo build`, matching the npm bundled-cli fix.
|
|
885
|
-
|
|
886
|
-
**Required changes.** None. Consumers who declare `kind = "pypi"`,
|
|
887
|
-
`build = "maturin"`, and `[package.bundle_cli]` get the corrected
|
|
888
|
-
behavior on their next release run against `@v0`; no
|
|
889
|
-
`putitoutthere.toml`, trailer, or consumer-side YAML change is needed.
|
|
890
|
-
|
|
891
|
-
**Deprecations removed.** None.
|
|
892
|
-
|
|
893
|
-
**Behavior changes without code changes.** Same config, different
|
|
894
|
-
artifact: the CLI binary staged into each wheel now reports the
|
|
895
|
-
planned release version from `--version` (and any other
|
|
896
|
-
`CARGO_PKG_VERSION`-derived output) instead of the stale literal in
|
|
897
|
-
the CLI crate manifest.
|
|
898
|
-
|
|
899
|
-
**Verification.** Release a pypi maturin package that declares
|
|
900
|
-
`[package.bundle_cli]`, install one of the published wheels, and run
|
|
901
|
-
the bundled CLI's `--version`: it reports `<version>`, matching the
|
|
902
|
-
published wheel metadata.
|
|
903
|
-
|
|
904
|
-
### npm bundled-cli binary embeds the release version
|
|
905
|
-
|
|
906
|
-
**Summary.** The reusable workflow's npm `build = "bundled-cli"` path
|
|
907
|
-
cross-compiled the bundled CLI from un-rewritten crate source, so the
|
|
908
|
-
binary's `CARGO_PKG_VERSION` (what `<bin> --version` prints) was baked
|
|
909
|
-
from the literal `[package].version` in the crate's `Cargo.toml`
|
|
910
|
-
rather than the planned release version. A `@scope/cli-<triple>@0.3.5`
|
|
911
|
-
platform package could ship a binary that reported `0.2.7`. The build
|
|
912
|
-
matrix now rewrites the crate's `[package].version` to `matrix.version`
|
|
913
|
-
before `cargo build` runs, mirroring the pre-build `write-version`
|
|
914
|
-
step the pypi/maturin path already uses.
|
|
915
|
-
|
|
916
|
-
**Required changes.** None. The fix lives entirely inside the reusable
|
|
917
|
-
workflow's build matrix. Consumers who declare a `kind = "npm"`
|
|
918
|
-
`build = "bundled-cli"` package with `[package.bundle_cli]` get the
|
|
919
|
-
corrected behavior on their next release run against `@v0`; no
|
|
920
|
-
`putitoutthere.toml`, trailer, or consumer-side YAML change is needed.
|
|
921
|
-
|
|
922
|
-
**Deprecations removed.** None.
|
|
923
|
-
|
|
924
|
-
**Behavior changes without code changes.** Same config, different
|
|
925
|
-
artifact: the cross-compiled binary inside each per-platform package
|
|
926
|
-
now reports the planned release version from `--version` (and any
|
|
927
|
-
other `CARGO_PKG_VERSION`-derived output) instead of the stale literal
|
|
928
|
-
on disk in the crate manifest.
|
|
929
|
-
|
|
930
|
-
**Verification.** Release a `kind = "npm"` `build = "bundled-cli"`
|
|
931
|
-
package, extract one per-platform package
|
|
932
|
-
(`@scope/cli-<triple>@<version>`), and run the bundled binary's
|
|
933
|
-
`--version`: it reports `<version>`, matching the published package.
|
|
934
|
-
### Bundled-cli staged binary is executable
|
|
935
|
-
|
|
936
|
-
**Summary.** For `kind = "npm"` packages with a `build = "bundled-cli"`
|
|
937
|
-
entry, the reusable workflow cross-compiles the CLI and stages it into
|
|
938
|
-
a per-triple platform package (`@scope/cli-<triple>`). The staged
|
|
939
|
-
binary was packed with mode `0644` — no executable bit. npm only sets
|
|
940
|
-
the executable bit on `bin` entries; the bundled binary is referenced
|
|
941
|
-
via `package.json#main`, so npm never `chmod`s it, and the bit it had
|
|
942
|
-
on the build runner is stripped crossing the GitHub Actions artifact
|
|
943
|
-
upload/download boundary. At runtime the generated launcher's
|
|
944
|
-
`spawnSync` of the resolved binary failed with `EACCES`. The workflow
|
|
945
|
-
now `chmod +x`es the staged binary for non-Windows targets before the
|
|
946
|
-
platform package is packed/published.
|
|
947
|
-
|
|
948
|
-
**Required changes.** None. The fix is internal to the reusable
|
|
949
|
-
workflow's npm bundled-cli publish path.
|
|
950
|
-
|
|
951
|
-
**Deprecations removed.** None.
|
|
952
|
-
|
|
953
|
-
**Behavior changes without code changes.** Per-triple platform
|
|
954
|
-
packages published for non-Windows targets now ship the CLI binary
|
|
955
|
-
with mode `0755` instead of `0644`. Consumers who already worked
|
|
956
|
-
around the bug (a `postinstall` `chmod`, or a launcher that `chmod`s
|
|
957
|
-
before `spawnSync`) can drop that workaround; leaving it in place is
|
|
958
|
-
harmless.
|
|
959
|
-
|
|
960
|
-
**Verification.** Publish a `build = "bundled-cli"` npm family, then
|
|
961
|
-
`npm install` it and run the CLI — `npx <bin> --version` succeeds
|
|
962
|
-
instead of failing with `spawnSync ... EACCES`. Inspecting the
|
|
963
|
-
published platform tarball
|
|
964
|
-
(`tar -tvzf` on the `@scope/cli-<triple>` `.tgz`) shows the binary as
|
|
965
|
-
`-rwxr-xr-x`.
|
|
966
|
-
|
|
967
|
-
### Pre-merge crate-size check
|
|
968
|
-
|
|
969
|
-
**Summary.** `putitoutthere check` gained a check that runs
|
|
970
|
-
`cargo package --no-verify` for every `kind = "crates"` package and
|
|
971
|
-
fails when the resulting `.crate` is larger than crates.io's 10 MiB
|
|
972
|
-
(`10485760`-byte) upload limit. Previously an oversized crate — most
|
|
973
|
-
often caused by a tracked symlink dragging a build tree into the
|
|
974
|
-
package — surfaced only mid-release as a `413 Payload Too Large` from
|
|
975
|
-
`cargo publish`, after the verification build. The new check moves
|
|
976
|
-
that failure to PR time, before merge.
|
|
977
|
-
|
|
978
|
-
**Required changes.** None. The check is additive and runs
|
|
979
|
-
automatically wherever `putitoutthere check` already runs (the
|
|
980
|
-
`check.yml` reusable workflow). For the check to actually measure a
|
|
981
|
-
crate, a Rust toolchain (`cargo`) must be on `PATH` in that job; when
|
|
982
|
-
`cargo` is absent the check degrades to a no-op rather than failing,
|
|
983
|
-
so a check job without Rust set up sees no behavior change.
|
|
984
|
-
|
|
985
|
-
**Deprecations removed.** None.
|
|
986
|
-
|
|
987
|
-
**Behavior changes without code changes.** A PR that would produce an
|
|
988
|
-
oversized `.crate` now fails `putitoutthere check` with the new
|
|
989
|
-
`PIOT_CRATES_PACKAGE_TOO_LARGE` error code, instead of passing the
|
|
990
|
-
check and failing later inside the release run's publish job.
|
|
991
|
-
|
|
992
|
-
**Verification.** Add a `kind = "crates"` package and run
|
|
993
|
-
`putitoutthere check` (or open a PR against a repo wired to
|
|
994
|
-
`check.yml`) in an environment with `cargo` on `PATH`: an oversized
|
|
995
|
-
crate reports `PIOT_CRATES_PACKAGE_TOO_LARGE` naming the `.crate`
|
|
996
|
-
size and the 10 MiB limit, while a normally-sized crate reports
|
|
997
|
-
nothing.
|
|
998
|
-
|
|
999
|
-
### v0 tracks main HEAD
|
|
1000
|
-
|
|
1001
|
-
**Summary.** Until this release, the floating `v0` tag advanced only
|
|
1002
|
-
when a `release:` trailer fired the dogfood publish pipeline
|
|
1003
|
-
(`release-npm.yml`), which then moved `v0` to the latest
|
|
1004
|
-
`putitoutthere-v0.x.y` release commit. Commits that landed on main
|
|
1005
|
-
without a trailer — test-only changes, docs edits, dependency bumps,
|
|
1006
|
-
internal refactors, and one-off bug fixes whose author forgot the
|
|
1007
|
-
trailer — left `v0` stale relative to main. The behavior was
|
|
1008
|
-
explicitly chosen in issue #199 (`v0` = "latest released commit in
|
|
1009
|
-
major line") and is now explicitly reversed: `v0` tracks main HEAD,
|
|
1010
|
-
not the latest release.
|
|
1011
|
-
|
|
1012
|
-
A new workflow `.github/workflows/advance-v0.yml` fires on every
|
|
1013
|
-
push to main, builds the action bundle, folds it into a tag-only
|
|
1014
|
-
commit (mirroring `release-npm.yml`'s existing Fold step —
|
|
1015
|
-
`dist-action/` is gitignored on main, so `v0` must point at a
|
|
1016
|
-
synthesized bundle commit for `uses:
|
|
1017
|
-
thekevinscott/putitoutthere@v0` to resolve to a runnable action),
|
|
1018
|
-
and force-moves `v0` to that commit. The new workflow shares the
|
|
1019
|
-
`release` concurrency group with `release-npm.yml`, so when both
|
|
1020
|
-
fire on the same push (a trailer-bearing commit), the registry
|
|
1021
|
-
publish runs first and `v0` is then advanced on top.
|
|
1022
|
-
|
|
1023
|
-
The permanent per-release tags (`putitoutthere-v0.x.y`) are
|
|
1024
|
-
unchanged — they're cut by the dogfood publish pipeline on
|
|
1025
|
-
trailer-fire and remain the canonical version history.
|
|
1026
|
-
|
|
1027
|
-
**Required changes.** None on the consumer side. The change is in
|
|
1028
|
-
how `@v0` resolves over time, not in what the workflow at that ref
|
|
1029
|
-
does.
|
|
1030
|
-
|
|
1031
|
-
**Deprecations removed.** None.
|
|
1032
|
-
|
|
1033
|
-
**Behavior changes without code changes.** A commit that lands on
|
|
1034
|
-
`main` of `thekevinscott/putitoutthere` is, on the next consumer
|
|
1035
|
-
workflow resolve, the workflow code the consumer runs. Previously
|
|
1036
|
-
consumers had to wait for a release to be cut to pick up engine
|
|
1037
|
-
changes; now they pick them up on the next push to main. Consumers
|
|
1038
|
-
who want pinning to a known-released version use a
|
|
1039
|
-
`putitoutthere-v0.x.y` tag (or a SHA) instead of `@v0`.
|
|
1040
|
-
|
|
1041
|
-
**Verification.** After this change merges and the first push to
|
|
1042
|
-
main fires `advance-v0.yml`, the `v0` tag points at a fresh bundle
|
|
1043
|
-
commit whose parent is the merge commit on main. Confirm with:
|
|
1044
|
-
|
|
1045
|
-
```
|
|
1046
|
-
$ git ls-remote --tags https://github.com/thekevinscott/putitoutthere.git v0
|
|
1047
|
-
<sha> refs/tags/v0
|
|
1048
|
-
$ git log <sha> -1 --format='%H %s'
|
|
1049
|
-
<sha> chore(v0): bundle action
|
|
1050
|
-
$ git log <sha>^ -1 --format='%H %s' # parent is the merge commit on main
|
|
1051
|
-
<parent-sha> <merge commit subject>
|
|
1052
|
-
```
|
|
1053
|
-
|
|
1054
|
-
### Preflight: manifest repository URL must match GITHUB_REPOSITORY; private repos rejected
|
|
1055
|
-
|
|
1056
|
-
**Summary.** Two new preflight checks address the
|
|
1057
|
-
"surprise-at-publish" failure mode where a manifest's declared
|
|
1058
|
-
`repository` URL silently disagrees with the GitHub repository the
|
|
1059
|
-
workflow is actually running from. npm's provenance verification
|
|
1060
|
-
returns a 422 (`"package.json: repository.url is X, expected to
|
|
1061
|
-
match Y from provenance"`) **after** the artifact has been uploaded
|
|
1062
|
-
and the registry has done OIDC negotiation — the kind of mid-publish
|
|
1063
|
-
surprise this engine's "no release surprises" design commitment
|
|
1064
|
-
exists to prevent. The same risk lives on the crates.io / PyPI
|
|
1065
|
-
trusted-publisher paths against `Cargo.toml [package].repository`
|
|
1066
|
-
and `pyproject.toml [project.urls]`. Both checks now fire at the
|
|
1067
|
-
preflight stage before any side effects.
|
|
1068
|
-
|
|
1069
|
-
A second new check refuses to publish from a **private** GitHub
|
|
1070
|
-
repository entirely. Provenance attestations embed a public
|
|
1071
|
-
source-ref pointer that consumers cannot dereference when the repo
|
|
1072
|
-
is private; the same source-visibility expectation underpins the
|
|
1073
|
-
trusted-publisher story across all three registries. Hard-failing
|
|
1074
|
-
at preflight beats silently shipping a verification-broken artifact.
|
|
1075
|
-
|
|
1076
|
-
**Required changes.** None for any consumer whose manifest URLs
|
|
1077
|
-
already point at the correct `owner/repo` on GitHub and whose
|
|
1078
|
-
repository is public. The check is opt-out only by fixing the
|
|
1079
|
-
underlying disagreement.
|
|
1080
|
-
|
|
1081
|
-
| Failure mode | Fix |
|
|
1082
|
-
| --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
1083
|
-
| `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. |
|
|
1084
|
-
| `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. |
|
|
1085
|
-
| `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. |
|
|
1086
|
-
|
|
1087
|
-
**Deprecations removed.** None.
|
|
1088
|
-
|
|
1089
|
-
**Behavior changes without code changes.** None — both checks are
|
|
1090
|
-
new gates.
|
|
1091
|
-
|
|
1092
|
-
**Verification.** From a PR branch with a deliberately-wrong
|
|
1093
|
-
`repository.url`, the PR-time `check.yml` job now reports
|
|
1094
|
-
`[PIOT_REPO_URL_MISMATCH]` naming both the declared and expected
|
|
1095
|
-
`owner/repo` slugs and the manifest path. From a private repo,
|
|
1096
|
-
`publish` aborts before any side effect with `[PIOT_REPO_PRIVATE]`.
|
|
1097
|
-
Both checks no-op outside a GHA context (when `GITHUB_REPOSITORY`
|
|
1098
|
-
is unset), so a local `putitoutthere check` from a developer
|
|
1099
|
-
machine does not false-positive.
|
|
1100
|
-
|
|
1101
|
-
### Windows default runner pinned to windows-2022
|
|
1102
|
-
|
|
1103
|
-
**Summary.** GitHub is migrating `windows-latest` (and `windows-2025`)
|
|
1104
|
-
to Visual Studio 2026 between 2026-06-08 and 2026-06-15 — see the
|
|
1105
|
-
[GitHub Actions image-migration changelog](https://github.blog/changelog/2026-05-14-github-actions-upcoming-image-migrations/)
|
|
1106
|
-
and [actions/runner-images#14016](https://github.com/actions/runner-images/issues/14016).
|
|
1107
|
-
Until that date, `windows-latest` is Windows Server 2025 + VS2022; on
|
|
1108
|
-
the cutover, `windows-latest` redirects to `windows-2025-vs2026` and
|
|
1109
|
-
every consumer release run lands on a fresh toolchain with no
|
|
1110
|
-
opportunity to verify it first.
|
|
1111
|
-
|
|
1112
|
-
`defaultRunsOn` in `src/plan.ts` previously returned `windows-latest`
|
|
1113
|
-
for any Windows-shaped triple, so every consumer release plan that
|
|
1114
|
-
included `x86_64-pc-windows-msvc` (the standard windows triple for
|
|
1115
|
-
napi, bundled-cli, and maturin builds) inherited that floating label.
|
|
1116
|
-
The default is now `windows-2022`: stable, VS2022, no surprise
|
|
1117
|
-
migration. Consumers who want to track the floating label or adopt
|
|
1118
|
-
VS2026 early opt in via the per-target `{ triple, runner }` override
|
|
1119
|
-
that already exists.
|
|
1120
|
-
|
|
1121
|
-
**Required changes.** None for consumers who want to stay on a stable
|
|
1122
|
-
VS2022 toolchain — the new default does that for them. Consumers who
|
|
1123
|
-
want a different image opt in per target:
|
|
1124
|
-
|
|
1125
|
-
Before (relied on the floating `windows-latest` default):
|
|
1126
|
-
|
|
1127
|
-
```toml
|
|
1128
|
-
[[package]]
|
|
1129
|
-
name = "lib-napi"
|
|
1130
|
-
kind = "npm"
|
|
1131
|
-
build = "napi"
|
|
1132
|
-
targets = [
|
|
1133
|
-
"x86_64-unknown-linux-gnu",
|
|
1134
|
-
"x86_64-pc-windows-msvc",
|
|
1135
|
-
]
|
|
1136
|
-
```
|
|
1137
|
-
|
|
1138
|
-
After (no change required — `x86_64-pc-windows-msvc` now resolves to
|
|
1139
|
-
`windows-2022` by default):
|
|
1140
|
-
|
|
1141
|
-
```toml
|
|
1142
|
-
[[package]]
|
|
1143
|
-
name = "lib-napi"
|
|
1144
|
-
kind = "npm"
|
|
1145
|
-
build = "napi"
|
|
1146
|
-
targets = [
|
|
1147
|
-
"x86_64-unknown-linux-gnu",
|
|
1148
|
-
"x86_64-pc-windows-msvc",
|
|
1149
|
-
]
|
|
1150
|
-
```
|
|
1151
|
-
|
|
1152
|
-
After (opt in to a different image — for example, surface VS2026
|
|
1153
|
-
breakage now rather than on the cutover date):
|
|
1154
|
-
|
|
1155
|
-
```toml
|
|
1156
|
-
targets = [
|
|
1157
|
-
"x86_64-unknown-linux-gnu",
|
|
1158
|
-
{ triple = "x86_64-pc-windows-msvc", runner = "windows-2025-vs2026" },
|
|
1159
|
-
]
|
|
1160
|
-
```
|
|
1161
|
-
|
|
1162
|
-
Other valid choices for the `runner` value include `windows-2025`
|
|
1163
|
-
(Server 2025 + VS2022 until the cutover, then VS2026), `windows-2022`
|
|
1164
|
-
(matches the new default explicitly), and `windows-latest` (preserves
|
|
1165
|
-
the previous floating-label behavior).
|
|
1166
|
-
|
|
1167
|
-
**Deprecations removed.** None.
|
|
1168
|
-
|
|
1169
|
-
**Behavior changes without code changes.** Every Windows-shaped
|
|
1170
|
-
matrix row's `runs_on` field now resolves to `windows-2022` instead
|
|
1171
|
-
of `windows-latest` when no per-target `runner` override is set.
|
|
1172
|
-
Per-target overrides win exactly as before — the bare-string-vs-object
|
|
1173
|
-
precedence in `defaultRunsOn` is unchanged. The redirect notice
|
|
1174
|
-
GitHub injects into every `windows-latest`-targeted run
|
|
1175
|
-
(`NOTICE: windows-latest requests are being redirected to
|
|
1176
|
-
windows-2025-vs2026 by June 15, 2026`) stops appearing on plans
|
|
1177
|
-
generated by the new engine.
|
|
1178
|
-
|
|
1179
|
-
**Verification.** Run a release that includes any Windows triple and
|
|
1180
|
-
confirm the build job runs on `windows-2022`:
|
|
1181
|
-
|
|
1182
|
-
- In the GitHub Actions UI, the per-target build job's "Set up job"
|
|
1183
|
-
step lists `Runner: GitHub Actions <n>` followed by an OS-image
|
|
1184
|
-
block whose `Image: windows-2022` line names the pinned image. The
|
|
1185
|
-
banner `windows-latest requests are being redirected to
|
|
1186
|
-
windows-2025-vs2026 by June 15, 2026` no longer appears in the
|
|
1187
|
-
job log.
|
|
1188
|
-
- The published artifact's `runs_on` value, surfaced in the plan
|
|
1189
|
-
step's job summary, is `windows-2022`.
|
|
1190
|
-
|
|
1191
|
-
To opt in to a different image, set
|
|
1192
|
-
`{ triple = "x86_64-pc-windows-msvc", runner = "<choice>" }` on the
|
|
1193
|
-
relevant target and re-release; the build job moves to the named
|
|
1194
|
-
image on the next run.
|
|
1195
|
-
|
|
1196
|
-
### Crates first-publish TP rejection detected
|
|
1197
|
-
|
|
1198
|
-
**Summary.** crates.io's Trusted Publishing feature binds to an
|
|
1199
|
-
already-published crate name. The very first publish of a brand-new
|
|
1200
|
-
crate cannot use the TP path — the OIDC mint succeeds, the exchanged
|
|
1201
|
-
token reaches cargo, but the registry rejects the publish with a 404
|
|
1202
|
-
("crate `<name>` does not exist or you do not have permission to
|
|
1203
|
-
publish to it"). The engine previously surfaced this as a generic
|
|
1204
|
-
`cargo publish failed` block, sending consumers down a credentials
|
|
1205
|
-
rabbit-hole when the real fix is one bootstrap publish via the
|
|
1206
|
-
classic-token fallback shipped in #283.
|
|
1207
|
-
|
|
1208
|
-
The crates handler now detects this exact response shape and throws
|
|
1209
|
-
with the new stable error code
|
|
1210
|
-
`PIOT_CRATES_FIRST_PUBLISH_TP_REJECTED`, prefixed onto a message
|
|
1211
|
-
that names the crate, explains the TP-binds-to-published-crate
|
|
1212
|
-
constraint, and points at `CARGO_REGISTRY_TOKEN` as the bootstrap
|
|
1213
|
-
path. Cargo's full stderr is preserved at the bottom of the error
|
|
1214
|
-
for debuggability. Companion work landed a registry-auth response
|
|
1215
|
-
fixtures catalogue at
|
|
1216
|
-
[`notes/upstream-behaviors.md`](./notes/upstream-behaviors.md) that
|
|
1217
|
-
indexes this and three other response shapes the engine handles or
|
|
1218
|
-
architecturally avoids — see #296.
|
|
1219
|
-
|
|
1220
|
-
**Required changes.** None. Consumers who never hit the
|
|
1221
|
-
first-publish path see no change. Consumers whose first release
|
|
1222
|
-
fails on a brand-new crate now see a clearer error pointing at the
|
|
1223
|
-
fix; the fix itself (set `CARGO_REGISTRY_TOKEN` as a workflow
|
|
1224
|
-
secret for one publish, then remove it) has been available since
|
|
1225
|
-
#283 and is unchanged.
|
|
1226
|
-
|
|
1227
|
-
**Deprecations removed.** None.
|
|
1228
|
-
|
|
1229
|
-
**Behavior changes without code changes.** A `cargo publish`
|
|
1230
|
-
failure whose stderr matches the first-publish-TP-rejection shape
|
|
1231
|
-
now throws with `PIOT_CRATES_FIRST_PUBLISH_TP_REJECTED` instead of
|
|
1232
|
-
the generic `cargo publish failed` shape. The full cargo stderr
|
|
1233
|
-
remains in the error message. The detector is suppressed under the
|
|
1234
|
-
`PIOT_CRATES_REGISTRY_PRIMARY` e2e seam (alt-registry doesn't model
|
|
1235
|
-
TP, so a 404 there is a different bug).
|
|
1236
|
-
|
|
1237
|
-
**Verification.** On a brand-new crate name where Trusted
|
|
1238
|
-
Publishing is the only auth configured, run the release. The
|
|
1239
|
-
release run fails with a message starting
|
|
1240
|
-
`[PIOT_CRATES_FIRST_PUBLISH_TP_REJECTED] cargo publish: crates.io
|
|
1241
|
-
rejected publishing "<name>" because the crate has never been
|
|
1242
|
-
published.` followed by the bootstrap hint. Set
|
|
1243
|
-
`CARGO_REGISTRY_TOKEN` in the workflow's `secrets:` block (per
|
|
1244
|
-
#283), re-run, and the publish should succeed. Subsequent releases
|
|
1245
|
-
can drop the secret and rely on Trusted Publishing.
|
|
1246
|
-
|
|
1247
|
-
### Bundled-CLI launcher generated by the workflow
|
|
1248
|
-
|
|
1249
|
-
**Summary.** Bundled-CLI npm consumers used to author `bin/<bin>.js` —
|
|
1250
|
-
a Node launcher that detects the host platform, maps it to a triple,
|
|
1251
|
-
resolves the corresponding `<name>-<triple>` (or templated) platform
|
|
1252
|
-
package, and execs the binary. The launcher's only per-consumer
|
|
1253
|
-
inputs are the package name and the configured `targets` list. Both
|
|
1254
|
-
are in the engine's hands at plan time. Every consumer's launcher
|
|
1255
|
-
was byte-identical modulo those two values.
|
|
1256
|
-
|
|
1257
|
-
`_matrix.yml`'s build job now invokes a new internal `putitoutthere
|
|
1258
|
-
write-launcher` CLI subcommand on the main row of each `kind = "npm"
|
|
1259
|
-
&& build = "bundled-cli"` package (before `npm run build --if-present`
|
|
1260
|
-
runs). The subcommand writes `bin/<bundle_cli.bin>.js` and adds the
|
|
1261
|
-
matching `package.json#bin` entry in place. Both writes are guarded
|
|
1262
|
-
by an "only if absent" check — existing consumer-authored launchers
|
|
1263
|
-
and existing `bin` fields are preserved, so the override path is the
|
|
1264
|
-
same file you'd already have committed.
|
|
1265
|
-
|
|
1266
|
-
The generated launcher's shape mirrors the README example bundled-cli
|
|
1267
|
-
consumers wrote by hand pre-#299: hashbang, a Node
|
|
1268
|
-
`${platform}-${arch}` → triple table, `require.resolve` against the
|
|
1269
|
-
platform package, `spawnSync` with `stdio: 'inherit'`. The platform
|
|
1270
|
-
package's name template (`{name}-{triple}` by default, or whatever the
|
|
1271
|
-
consumer set under `build = [{ mode = "bundled-cli", name = "..." }]`
|
|
1272
|
-
in the multi-mode array form) has every placeholder except `{triple}`
|
|
1273
|
-
resolved at generation time; `{triple}` becomes a backtick template
|
|
1274
|
-
substitution at install time. The launcher imports nothing from
|
|
1275
|
-
putitoutthere at runtime — it's a self-contained Node script with no
|
|
1276
|
-
published-package dependencies.
|
|
1277
|
-
|
|
1278
|
-
Together with #298 (which absorbed the cross-compile build script),
|
|
1279
|
-
bundled-cli npm's consumer surface is now: declare the package in
|
|
1280
|
-
`putitoutthere.toml`, register Trusted Publishers, push.
|
|
1281
|
-
|
|
1282
|
-
**Required changes.** None for new consumers — declaring the package
|
|
1283
|
-
in `putitoutthere.toml` is sufficient.
|
|
1284
|
-
|
|
1285
|
-
For consumers who already shipped a hand-authored launcher and want
|
|
1286
|
-
to migrate to the generated one, delete `bin/<bin>.js` from the source
|
|
1287
|
-
tree. The build job will regenerate it on the next release run.
|
|
1288
|
-
Leaving the file in place is fully supported; the workflow only
|
|
1289
|
-
writes when the file is absent.
|
|
1290
|
-
|
|
1291
|
-
```diff
|
|
1292
|
-
// packages/my-cli/package.json
|
|
1293
|
-
{
|
|
1294
|
-
"name": "my-cli",
|
|
1295
|
-
- "bin": { "my-cli": "bin/my-cli.js" }
|
|
1296
|
-
}
|
|
1297
|
-
```
|
|
1298
|
-
|
|
1299
|
-
Removing the `bin` field is optional too: when the workflow sees an
|
|
1300
|
-
existing `bin` field it leaves it alone. The diff above is only
|
|
1301
|
-
necessary if the consumer wants the workflow to author the field
|
|
1302
|
-
shape from scratch (`{ "<bin>": "bin/<bin>.js" }`).
|
|
1303
|
-
|
|
1304
|
-
**Deprecations removed.** None — the legacy hand-authored launcher
|
|
1305
|
-
path is still supported and the workflow respects the override.
|
|
1306
|
-
|
|
1307
|
-
**Behavior changes without code changes.**
|
|
1308
|
-
|
|
1309
|
-
- The published main package's `package.json#bin` now contains
|
|
1310
|
-
`{ "<bundle_cli.bin>": "bin/<bundle_cli.bin>.js" }` for consumers
|
|
1311
|
-
who previously had no `bin` field. Consumers with a pre-existing
|
|
1312
|
-
`bin` field see no change.
|
|
1313
|
-
- The published main package's tarball now contains
|
|
1314
|
-
`bin/<bundle_cli.bin>.js` for consumers who previously did not
|
|
1315
|
-
commit the file. Consumers who committed the file see their version
|
|
1316
|
-
shipped unchanged.
|
|
1317
|
-
|
|
1318
|
-
**Verification.** After a release run, inspect the published main
|
|
1319
|
-
package's tarball:
|
|
1320
|
-
|
|
1321
|
-
```sh
|
|
1322
|
-
npm pack <main-pkg-name>@<version>
|
|
1323
|
-
tar -xvf <main-pkg-name>-<version>.tgz package/bin/<bundle_cli.bin>.js -O \
|
|
1324
|
-
| head -20
|
|
1325
|
-
```
|
|
1326
|
-
|
|
1327
|
-
The first line is `#!/usr/bin/env node`; the file declares a `triples`
|
|
1328
|
-
object whose keys match the Node `${platform}-${arch}` strings the
|
|
1329
|
-
package's `targets` resolve to, and whose values are the configured
|
|
1330
|
-
triples. `package/package.json`'s `bin` field is
|
|
1331
|
-
`{ "<bundle_cli.bin>": "bin/<bundle_cli.bin>.js" }`.
|
|
1332
|
-
|
|
1333
|
-
### `bundle_cli` wheel guard respects `python-source`
|
|
1334
|
-
|
|
1335
|
-
**Summary.** Maturin's standard mixed-project layout
|
|
1336
|
-
(`maturin new --mixed` generates `[tool.maturin].python-source = "python"`)
|
|
1337
|
-
declares a package source root that maturin strips from on-disk paths
|
|
1338
|
-
when rewriting them into the wheel's distribution layout. A binary
|
|
1339
|
-
staged on disk at `<pkg.path>/<stage_to>/<bin>` — e.g.
|
|
1340
|
-
`packages/python/python/dirsql/_binary/dirsql` — ends up in the wheel
|
|
1341
|
-
at `dirsql/_binary/dirsql`, with `python/` stripped. The reusable
|
|
1342
|
-
workflow's `bundle_cli` wheel-content guard previously asserted a
|
|
1343
|
-
literal `<stage_to>/<bin>` suffix inside the produced wheel; the regex
|
|
1344
|
-
never matched the stripped path, so the guard fired red on every
|
|
1345
|
-
per-target build row even when the binary was correctly bundled.
|
|
1346
|
-
|
|
1347
|
-
The guard now reads `[tool.maturin].python-source` (and the legacy
|
|
1348
|
-
`python_source` spelling — both forms are accepted by maturin across
|
|
1349
|
-
versions) from `<matrix.path>/pyproject.toml` and subtracts that
|
|
1350
|
-
prefix from `stage_to` before constructing the suffix regex. Consumers
|
|
1351
|
-
with an implicit-root layout (no `python-source` key, or an empty
|
|
1352
|
-
value) keep the previous behavior byte-for-byte; consumers with the
|
|
1353
|
-
explicit-root layout start passing the guard. Tracked at #338.
|
|
1354
|
-
|
|
1355
|
-
**Required changes.** None.
|
|
1356
|
-
|
|
1357
|
-
**Deprecations removed.** None.
|
|
1358
|
-
|
|
1359
|
-
**Behavior changes without code changes.**
|
|
1360
|
-
|
|
1361
|
-
- For a consumer with `[tool.maturin].python-source = "python"` and
|
|
1362
|
-
`[package.bundle_cli].stage_to = "python/dirsql/_binary"`, the
|
|
1363
|
-
reusable workflow's wheel-content guard now resolves the in-wheel
|
|
1364
|
-
suffix to `dirsql/_binary/<bin>` (matching what maturin actually
|
|
1365
|
-
produces) instead of asserting the unstripped `python/dirsql/_binary/<bin>`.
|
|
1366
|
-
- A `python-source` value that isn't actually a prefix of `stage_to`
|
|
1367
|
-
is left alone — the guard reverts to asserting the unstripped
|
|
1368
|
-
`stage_to` so the consumer's misconfiguration surfaces with the same
|
|
1369
|
-
diagnostic it does today.
|
|
1370
|
-
- An unset or empty `python-source` value resolves to the empty
|
|
1371
|
-
string and `stage_suffix` is unchanged. No behavior change for
|
|
1372
|
-
consumers who don't use the explicit-root layout.
|
|
1373
|
-
|
|
1374
|
-
**Verification.** With a maturin package whose `pyproject.toml`
|
|
1375
|
-
declares `[tool.maturin].python-source = "python"` and whose
|
|
1376
|
-
`[package.bundle_cli]` sets `stage_to = "python/<pkg>/_binary"`,
|
|
1377
|
-
a release run should produce wheels whose `unzip -l` listing contains
|
|
1378
|
-
`<pkg>/_binary/<bin>` and the wheel-content guard step should log
|
|
1379
|
-
`ok bundle_cli: <pkg>/_binary/<bin> present in <wheel>`.
|
|
1380
|
-
|
|
1381
|
-
### `bundle_cli` cargo workspace
|
|
1382
|
-
|
|
1383
|
-
**Summary.** Two collided bugs made `[package.bundle_cli]`
|
|
1384
|
-
unsatisfiable for the standard cargo-workspace layout: a single
|
|
1385
|
-
workspace root `Cargo.toml` with `[workspace] members = [...]` and the
|
|
1386
|
-
`[[bin]]` declared in a member crate (the shape `cargo new --workspace`
|
|
1387
|
-
produces, and what the polyglot Rust/Python recipe in the README
|
|
1388
|
-
implies). With `crate_path = "."` (the default), `putitoutthere check`
|
|
1389
|
-
parsed the workspace root `Cargo.toml` literally, saw no `[[bin]]`, and
|
|
1390
|
-
emitted `bundle_cli.bin "X" is not declared as a [[bin]]`. With
|
|
1391
|
-
`crate_path = "packages/rust"`, the check passed but the reusable
|
|
1392
|
-
workflow's bundle_cli stage step couldn't find the produced binary —
|
|
1393
|
-
cargo writes to the workspace-rooted target dir by default
|
|
1394
|
-
(`<repo-root>/target/...`), not to the working-directory-rooted one
|
|
1395
|
-
(`packages/rust/target/...`) the stage step assumed. There was no
|
|
1396
|
-
`crate_path` value that satisfied both halves.
|
|
1397
|
-
|
|
1398
|
-
The check now walks `[workspace].members` and aggregates each member's
|
|
1399
|
-
declared bins (honoring the implicit-binary rule, including
|
|
1400
|
-
`[package].name = { workspace = true }` inheritance from
|
|
1401
|
-
`[workspace.package].name`). The reusable workflow's cargo build step
|
|
1402
|
-
pins `--target-dir target` so the produced binary is deterministically
|
|
1403
|
-
at `${{ matrix.bundle_cli.crate_path }}/target/<triple>/release/<bin>`
|
|
1404
|
-
regardless of whether the crate participates in a workspace. Tracked at
|
|
1405
|
-
#337.
|
|
1406
|
-
|
|
1407
|
-
**Required changes.** None.
|
|
1408
|
-
|
|
1409
|
-
Consumers whose `[package.bundle_cli]` block already worked (single-
|
|
1410
|
-
crate layouts, or workspaces where `crate_path` pointed directly at the
|
|
1411
|
-
member crate and the consumer's project structure happened to make the
|
|
1412
|
-
workspace target dir line up with the member target dir) keep building
|
|
1413
|
-
byte-identically. Consumers whose workspace layout previously failed
|
|
1414
|
-
the check or stage step start working without touching their config.
|
|
1415
|
-
|
|
1416
|
-
**Deprecations removed.** None.
|
|
1417
|
-
|
|
1418
|
-
**Behavior changes without code changes.**
|
|
1419
|
-
|
|
1420
|
-
- `putitoutthere check` accepts the cargo-workspace shape:
|
|
1421
|
-
```toml
|
|
1422
|
-
# /Cargo.toml
|
|
1423
|
-
[workspace]
|
|
1424
|
-
members = ["packages/rust"]
|
|
1425
|
-
|
|
1426
|
-
# /packages/rust/Cargo.toml
|
|
1427
|
-
[package]
|
|
1428
|
-
name = "my-cli"
|
|
1429
|
-
description = "..."
|
|
1430
|
-
license = "MIT"
|
|
1431
|
-
|
|
1432
|
-
[[bin]]
|
|
1433
|
-
name = "my-cli"
|
|
1434
|
-
path = "src/main.rs"
|
|
1435
|
-
|
|
1436
|
-
# /putitoutthere.toml
|
|
1437
|
-
[package.bundle_cli]
|
|
1438
|
-
bin = "my-cli"
|
|
1439
|
-
stage_to = "python/dirsql/_binary"
|
|
1440
|
-
# crate_path defaults to "."
|
|
1441
|
-
```
|
|
1442
|
-
previously reported `bundle_cli.bin "my-cli" is not declared as a [[bin]]`,
|
|
1443
|
-
now reports zero findings.
|
|
1444
|
-
- The reusable workflow's bundle_cli build step now passes
|
|
1445
|
-
`--target-dir target` to `cargo build`. The produced binary is at
|
|
1446
|
-
`${{ matrix.bundle_cli.crate_path }}/target/<triple>/release/<bin>`
|
|
1447
|
-
regardless of workspace membership, and the stage step's `src=` path
|
|
1448
|
-
resolves correctly by construction.
|
|
1449
|
-
- Members declared as glob patterns (`members = ["packages/*"]`) are
|
|
1450
|
-
expanded against the filesystem by the check — see
|
|
1451
|
-
[`bundle_cli` glob workspace members](#bundle_cli-glob-workspace-members).
|
|
1452
|
-
|
|
1453
|
-
**Verification.** With the workspace layout above, the consumer should
|
|
1454
|
-
see:
|
|
1455
|
-
|
|
1456
|
-
- `putitoutthere check` reports zero findings.
|
|
1457
|
-
- A maturin release run produces a wheel whose `unzip -l` includes the
|
|
1458
|
-
staged binary (the existing wheel-content guard asserts this).
|
|
1459
|
-
|
|
1460
|
-
### `bundle_cli` glob workspace members
|
|
1461
|
-
|
|
1462
|
-
**Summary.** The `bundle_cli` cargo-workspace fix above taught
|
|
1463
|
-
`putitoutthere check` (and the pre-publish preflight) to walk
|
|
1464
|
-
`[workspace].members` and aggregate each member crate's declared
|
|
1465
|
-
`[[bin]]` entries, so `crate_path = "."` resolves a `[[bin]]` that
|
|
1466
|
-
lives in a member crate. That walk only handled *literal* member
|
|
1467
|
-
entries. cargo `members` entries are globs, and `members =
|
|
1468
|
-
["packages/*"]` — a Rust core crate under `packages/rust` wrapped by
|
|
1469
|
-
sibling Python / npm packages — is the standard polyglot-repo shape. A
|
|
1470
|
-
glob entry never resolved to a literal `<member>/Cargo.toml`, so the
|
|
1471
|
-
member crate's `[[bin]]` went unseen and `crate_path = "."` was
|
|
1472
|
-
rejected with `bundle_cli.bin "X" is not declared as a [[bin]]`.
|
|
1473
|
-
|
|
1474
|
-
The check now expands `[workspace].members` glob entries against the
|
|
1475
|
-
filesystem the way cargo resolves them; a member crate behind a glob is
|
|
1476
|
-
found like any literal member.
|
|
1477
|
-
|
|
1478
|
-
**Required changes.** None.
|
|
1479
|
-
|
|
1480
|
-
Consumers whose workspace declares `members` with literal paths are
|
|
1481
|
-
unaffected. Consumers who declared `members` with a glob and worked
|
|
1482
|
-
around the rejected check — by also listing the member crate as a
|
|
1483
|
-
literal entry, or by pointing `crate_path` straight at the member
|
|
1484
|
-
crate — can drop the workaround and let `crate_path` default to `"."`.
|
|
1485
|
-
|
|
1486
|
-
**Deprecations removed.** None.
|
|
1487
|
-
|
|
1488
|
-
**Behavior changes without code changes.**
|
|
1489
|
-
|
|
1490
|
-
- `putitoutthere check` accepts a glob-member workspace:
|
|
1491
|
-
```toml
|
|
1492
|
-
# /Cargo.toml
|
|
1493
|
-
[workspace]
|
|
1494
|
-
members = ["packages/*"]
|
|
1495
|
-
|
|
1496
|
-
# /packages/rust/Cargo.toml
|
|
1497
|
-
[package]
|
|
1498
|
-
name = "rust-core"
|
|
1499
|
-
|
|
1500
|
-
[[bin]]
|
|
1501
|
-
name = "my-cli"
|
|
1502
|
-
path = "src/main.rs"
|
|
1503
|
-
|
|
1504
|
-
# /putitoutthere.toml
|
|
1505
|
-
[package.bundle_cli]
|
|
1506
|
-
bin = "my-cli"
|
|
1507
|
-
stage_to = "python/dirsql/_binary"
|
|
1508
|
-
# crate_path defaults to "."
|
|
1509
|
-
```
|
|
1510
|
-
previously reported `bundle_cli.bin "my-cli" is not declared as a [[bin]]`,
|
|
1511
|
-
now reports zero findings.
|
|
1512
|
-
|
|
1513
|
-
**Verification.** With the glob-member workspace above, `putitoutthere
|
|
1514
|
-
check` reports zero findings.
|
|
1515
|
-
|
|
1516
|
-
### Crates metadata check resolves `[workspace.package]` inheritance
|
|
1517
|
-
|
|
1518
|
-
**Summary.** Cargo's recommended pattern for shared crate metadata in a
|
|
1519
|
-
workspace is `[workspace.package]` in the workspace root combined with
|
|
1520
|
-
`<field>.workspace = true` on each member. `cargo publish` resolves the
|
|
1521
|
-
inheritance and embeds the literal value into `Cargo.toml.orig` before
|
|
1522
|
-
upload, so crates.io receives the resolved field. The pre-merge
|
|
1523
|
-
`check` and the pre-publish `requireCratesMetadata` previously parsed
|
|
1524
|
-
each member `Cargo.toml` in isolation and treated the
|
|
1525
|
-
`{ workspace: true }` placeholder as a missing string, flagging
|
|
1526
|
-
well-formed workspaces with `PIOT_CRATES_MISSING_METADATA` even though
|
|
1527
|
-
the eventual `cargo publish` would succeed. The check now walks up from
|
|
1528
|
-
each crate's `path` to find the nearest parent `Cargo.toml` carrying a
|
|
1529
|
-
`[workspace]` table and, when a member field is declared as
|
|
1530
|
-
`<field>.workspace = true`, resolves the value from `[workspace.package]`
|
|
1531
|
-
before deciding it's missing. Genuinely-missing inherited fields — the
|
|
1532
|
-
workspace root has no value for the key, or no `[workspace.package]`
|
|
1533
|
-
block at all — still report through `PIOT_CRATES_MISSING_METADATA`. Hit
|
|
1534
|
-
in the wild in `thekevinscott/dirsql#177`. Tracked at #328.
|
|
1535
|
-
|
|
1536
|
-
**Required changes.** None.
|
|
1537
|
-
|
|
1538
|
-
**Deprecations removed.** None.
|
|
1539
|
-
|
|
1540
|
-
**Behavior changes without code changes.**
|
|
1541
|
-
|
|
1542
|
-
- Crates packages whose `Cargo.toml` reads
|
|
1543
|
-
```toml
|
|
1544
|
-
[package]
|
|
1545
|
-
name = "foo"
|
|
1546
|
-
description.workspace = true
|
|
1547
|
-
license.workspace = true
|
|
1548
|
-
```
|
|
1549
|
-
with the workspace root supplying
|
|
1550
|
-
```toml
|
|
1551
|
-
[workspace.package]
|
|
1552
|
-
description = "..."
|
|
1553
|
-
license = "MIT"
|
|
1554
|
-
```
|
|
1555
|
-
no longer surface as `PIOT_CRATES_MISSING_METADATA` findings from
|
|
1556
|
-
`putitoutthere check` or as `requireCratesMetadata` errors from the
|
|
1557
|
-
publish path. `license-file.workspace = true` resolves the same way.
|
|
1558
|
-
- Crates that inherit a field whose workspace root omits it (or has no
|
|
1559
|
-
`[workspace.package]` block) continue to surface as
|
|
1560
|
-
`PIOT_CRATES_MISSING_METADATA` — the publish would still fail at
|
|
1561
|
-
crates.io's metadata gate, so the preflight keeps flagging it.
|
|
1562
|
-
- Inline (non-inherited) `description` / `license` / `license-file`
|
|
1563
|
-
fields are unchanged.
|
|
1564
|
-
|
|
1565
|
-
**Verification.** Inside a workspace that centralizes metadata:
|
|
1566
|
-
|
|
1567
|
-
```toml
|
|
1568
|
-
# Cargo.toml
|
|
1569
|
-
[workspace]
|
|
1570
|
-
members = ["packages/rust"]
|
|
1571
|
-
|
|
1572
|
-
[workspace.package]
|
|
1573
|
-
license = "MIT"
|
|
1574
|
-
description = "Shared description."
|
|
1575
|
-
|
|
1576
|
-
# packages/rust/Cargo.toml
|
|
1577
|
-
[package]
|
|
1578
|
-
name = "foo"
|
|
1579
|
-
description.workspace = true
|
|
1580
|
-
license.workspace = true
|
|
1581
|
-
```
|
|
1582
|
-
|
|
1583
|
-
`putitoutthere check` should report `0 findings` for `foo`'s metadata,
|
|
1584
|
-
and `cargo metadata --no-deps --format-version=1 --manifest-path packages/rust/Cargo.toml`
|
|
1585
|
-
should show the resolved `"license":"MIT"` / `"description":"..."`.
|
|
1586
|
-
|
|
1587
|
-
### Hatch wheel-any row
|
|
1588
|
-
|
|
1589
|
-
**Summary.** `kind = "pypi"` + `build = "hatch"` now publishes a wheel
|
|
1590
|
-
alongside the sdist. Previously the matrix carried only a
|
|
1591
|
-
`target = "sdist"` row, so PyPI ended up with sdist-only and downstream
|
|
1592
|
-
`pip install` / `uvx ...` had to provision hatchling and run
|
|
1593
|
-
`python -m build` on a cold cache — several seconds per invocation
|
|
1594
|
-
instead of a sub-second download-and-extract. `pypa/build`'s default on
|
|
1595
|
-
a pure-Python tree produces both an sdist and an any-platform wheel; the
|
|
1596
|
-
planner just wasn't asking for the wheel. Issue #324.
|
|
1597
|
-
|
|
1598
|
-
The matrix now emits a second row per hatch package:
|
|
1599
|
-
|
|
1600
|
-
| Field | Value |
|
|
1601
|
-
|-----------------|--------------------------------------|
|
|
1602
|
-
| `target` | `any` |
|
|
1603
|
-
| `artifact_name` | `<package-name>-wheel-any` |
|
|
1604
|
-
| `artifact_path` | `<package-path>/dist` |
|
|
1605
|
-
| `runs_on` | `ubuntu-latest` |
|
|
1606
|
-
| `build` | `hatch` |
|
|
1607
|
-
|
|
1608
|
-
The reusable workflow's build step gates on
|
|
1609
|
-
`matrix.kind == 'pypi' && matrix.build == 'hatch' && matrix.target == 'any'`
|
|
1610
|
-
and runs `python -m build --wheel --outdir dist` (with
|
|
1611
|
-
`SETUPTOOLS_SCM_PRETEND_VERSION` set, mirroring the sdist row's contract).
|
|
1612
|
-
|
|
1613
|
-
**Required changes.** None. The recommended consumer-side recipe in
|
|
1614
|
-
[README → Quickstart](./README.md#1-drop-in-githubworkflowsreleaseyml)
|
|
1615
|
-
already uses `actions/download-artifact@v8` with
|
|
1616
|
-
`pattern: '*-wheel-*'` and `pattern: '*-sdist'` and feeds the combined
|
|
1617
|
-
`dist/` to `pypa/gh-action-pypi-publish@release/v1`. Both patterns now
|
|
1618
|
-
match for hatch packages without any consumer-side YAML change.
|
|
1619
|
-
|
|
1620
|
-
If you have a hand-rolled `pypi-publish` job that consumes specific
|
|
1621
|
-
artifact names, add `<package-name>-wheel-any` to its download list.
|
|
1622
|
-
|
|
1623
|
-
**Deprecations removed.** None.
|
|
1624
|
-
|
|
1625
|
-
**Behavior changes without code changes.** Hatch packages whose previous
|
|
1626
|
-
release shipped sdist-only will, on the first release after upgrading,
|
|
1627
|
-
also publish an any-platform wheel to PyPI under the same version. No
|
|
1628
|
-
new tag is created and no extra publish-time orchestration is needed —
|
|
1629
|
-
the wheel is uploaded alongside the sdist in the existing
|
|
1630
|
-
`pypa/gh-action-pypi-publish` step.
|
|
1631
|
-
|
|
1632
|
-
Scope is `build = "hatch"` only. `build = "setuptools"` stays
|
|
1633
|
-
sdist-only and `build = "maturin"` keeps its per-target wheel rows
|
|
1634
|
-
(both unchanged).
|
|
1635
|
-
|
|
1636
|
-
**Verification.** After publishing a hatch package:
|
|
1637
|
-
|
|
1638
|
-
```
|
|
1639
|
-
curl -s https://pypi.org/pypi/<name>/json | jq '.urls[].packagetype'
|
|
1640
|
-
# "sdist"
|
|
1641
|
-
# "bdist_wheel" ← previously absent
|
|
1642
|
-
```
|
|
1643
|
-
|
|
1644
|
-
`pip install <name>` (or `uvx <name>`) should download `*.whl` and
|
|
1645
|
-
skip the local build step entirely.
|
|
1646
|
-
|
|
1647
|
-
---
|
|
1648
|
-
|
|
1649
|
-
### pypi `pyproject.toml` must declare `dynamic = ["version"]`
|
|
1650
|
-
|
|
1651
|
-
**Summary.** Every `kind = "pypi"` package's `pyproject.toml` must
|
|
1652
|
-
now declare `[project].dynamic = ["version"]`. Static
|
|
1653
|
-
`[project].version = "x.y.z"` literals are rejected at PR time by
|
|
1654
|
-
`putitoutthere check` and again at publish-time preflight, both
|
|
1655
|
-
under the stable error code `PIOT_PYPI_STATIC_VERSION`. This is
|
|
1656
|
-
the most common Python-publishing footgun: putitoutthere does not
|
|
1657
|
-
edit `pyproject.toml` at release time (per the [no version
|
|
1658
|
-
computation](./notes/design-commitments.md#non-goals) design
|
|
1659
|
-
commitment), so a literal silently shipped the previous release's
|
|
1660
|
-
wheel/sdist because the build backend read whatever was on disk.
|
|
1661
|
-
Making the dynamic shape mandatory closes the failure mode at the
|
|
1662
|
-
earliest knowable boundary — the consumer's own repo state, before
|
|
1663
|
-
a release run is ever invoked.
|
|
1664
|
-
|
|
1665
|
-
**Required changes.**
|
|
1666
|
-
|
|
1667
|
-
Before — a static literal, accepted in v0.2.x:
|
|
1668
|
-
|
|
1669
|
-
```toml
|
|
1670
|
-
[project]
|
|
1671
|
-
name = "your-package"
|
|
1672
|
-
version = "0.1.0"
|
|
1673
|
-
```
|
|
1674
|
-
|
|
1675
|
-
After — `hatch-vcs` (recommended for new packages):
|
|
1676
|
-
|
|
1677
|
-
```toml
|
|
1678
|
-
[build-system]
|
|
1679
|
-
requires = ["hatchling", "hatch-vcs"]
|
|
1680
|
-
build-backend = "hatchling.build"
|
|
1681
|
-
|
|
1682
|
-
[project]
|
|
1683
|
-
name = "your-package"
|
|
1684
|
-
dynamic = ["version"]
|
|
1685
|
-
|
|
1686
|
-
[tool.hatch.version]
|
|
1687
|
-
source = "vcs"
|
|
1688
|
-
```
|
|
1689
|
-
|
|
1690
|
-
After — `setuptools-scm` (for setuptools-backed projects):
|
|
1691
|
-
|
|
1692
|
-
```toml
|
|
1693
|
-
[build-system]
|
|
1694
|
-
requires = ["setuptools>=64", "setuptools-scm>=8"]
|
|
1695
|
-
build-backend = "setuptools.build_meta"
|
|
1696
|
-
|
|
1697
|
-
[project]
|
|
1698
|
-
name = "your-package"
|
|
1699
|
-
dynamic = ["version"]
|
|
1700
|
-
|
|
1701
|
-
[tool.setuptools_scm]
|
|
1702
|
-
```
|
|
1703
|
-
|
|
1704
|
-
After — `maturin` (Python packages built from a Rust crate): the
|
|
1705
|
-
version source moves to the sibling `Cargo.toml`'s
|
|
1706
|
-
`[package].version`. `pyproject.toml` declares only that the version
|
|
1707
|
-
is dynamic:
|
|
1708
|
-
|
|
1709
|
-
```toml
|
|
1710
|
-
[build-system]
|
|
1711
|
-
requires = ["maturin>=1"]
|
|
1712
|
-
build-backend = "maturin"
|
|
1713
|
-
|
|
1714
|
-
[project]
|
|
1715
|
-
name = "your-package"
|
|
1716
|
-
dynamic = ["version"]
|
|
1717
|
-
```
|
|
1718
|
-
|
|
1719
|
-
No reusable-workflow input changes; the existing
|
|
1720
|
-
`SETUPTOOLS_SCM_PRETEND_VERSION` injection in the build step already
|
|
1721
|
-
hands the planned version to `hatch-vcs` / `setuptools-scm`, and the
|
|
1722
|
-
existing `putitoutthere write-version` step already bumps
|
|
1723
|
-
`Cargo.toml` for the maturin path.
|
|
1724
|
-
|
|
1725
|
-
**Deprecations removed.**
|
|
1726
|
-
|
|
1727
|
-
- `pypi.writeVersion` and the `putitoutthere write-version` CLI
|
|
1728
|
-
subcommand no longer rewrite static literals in place — both now
|
|
1729
|
-
surface `PIOT_PYPI_STATIC_VERSION`. Previously they would
|
|
1730
|
-
silently overwrite the literal.
|
|
1731
|
-
- The `replacePyProjectVersion` helper export and the
|
|
1732
|
-
"bumps BOTH pyproject and sibling Cargo.toml on the static-literal
|
|
1733
|
-
path" #276 carve-out are gone. Under the dynamic contract, `Cargo.toml`
|
|
1734
|
-
alone is the bump target on the maturin path and the pyproject literal
|
|
1735
|
-
has no role.
|
|
1736
|
-
|
|
1737
|
-
**Behavior changes without code changes.**
|
|
1738
|
-
|
|
1739
|
-
- `putitoutthere check` reports `PIOT_PYPI_STATIC_VERSION` against
|
|
1740
|
-
every pypi package whose `pyproject.toml` has a literal
|
|
1741
|
-
`[project].version`. Aggregated with the other preflight
|
|
1742
|
-
findings — one round-trip, not one error at a time.
|
|
1743
|
-
- The publish path runs `requirePypiVersionSource` between the
|
|
1744
|
-
existing `requireCratesMetadata` and the artifact-completeness
|
|
1745
|
-
check; a misconfigured pypi tree fails fast there even if the
|
|
1746
|
-
PR-time `check.yml` gate was skipped.
|
|
1747
|
-
|
|
1748
|
-
**Verification.**
|
|
1749
|
-
|
|
1750
|
-
```bash
|
|
1751
|
-
# In a repo that still declares a static version:
|
|
1752
|
-
$ putitoutthere check
|
|
1753
|
-
[PIOT_PYPI_STATIC_VERSION] packages/python/pyproject.toml declares a
|
|
1754
|
-
static `[project].version` literal. Use `[project].dynamic =
|
|
1755
|
-
["version"]` ...
|
|
1756
|
-
|
|
1757
|
-
# After the migration above:
|
|
1758
|
-
$ putitoutthere check
|
|
1759
|
-
# (no findings, exits 0)
|
|
1760
|
-
```
|
|
1761
|
-
|
|
1762
|
-
Tracked at #333.
|
|
1763
|
-
|
|
1764
|
-
---
|
|
1765
|
-
|
|
1766
|
-
### `npm` `bundle_cli` absorbed into the reusable workflow
|
|
1767
|
-
|
|
1768
|
-
**Summary.** `kind = "npm"` packages with `build = "bundled-cli"`
|
|
1769
|
-
no longer need to author a `scripts/build.cjs` that performs the
|
|
1770
|
-
Rust cross-compile by hand. Declare `[package.bundle_cli]` in
|
|
1771
|
-
`putitoutthere.toml` — same schema as the pypi/maturin block from
|
|
1772
|
-
#282, minus `stage_to` (npm staging is determined entirely by the
|
|
1773
|
-
matrix row's `artifact_path`) — and the reusable workflow runs
|
|
1774
|
-
`rustup target add`, `cargo build --release --target <triple>
|
|
1775
|
-
--bin <bin>` against `crate_path`, and the copy-into-staging step
|
|
1776
|
-
itself. A defense-in-depth build-content guard asserts the staged
|
|
1777
|
-
binary exists before `actions/upload-artifact` runs, so a broken
|
|
1778
|
-
row never leaves the build runner. Mirror of the pypi wiring landed
|
|
1779
|
-
in #282; closes the seam that #287 patched (env-var contract
|
|
1780
|
-
between the consumer's build script and the engine).
|
|
1781
|
-
|
|
1782
|
-
**Required changes.** None for additive adoption. To migrate an
|
|
1783
|
-
existing consumer with a hand-written `scripts/build.cjs`:
|
|
1784
|
-
|
|
1785
|
-
Before (`putitoutthere.toml`):
|
|
1786
|
-
|
|
1787
|
-
```toml
|
|
1788
|
-
[[package]]
|
|
1789
|
-
name = "my-cli"
|
|
1790
|
-
kind = "npm"
|
|
1791
|
-
build = "bundled-cli"
|
|
1792
|
-
path = "packages/ts-cli"
|
|
1793
|
-
globs = ["packages/ts-cli/**", "crates/my-cli/**"]
|
|
1794
|
-
targets = [
|
|
1795
|
-
"x86_64-unknown-linux-gnu",
|
|
1796
|
-
"x86_64-apple-darwin",
|
|
1797
|
-
# ...
|
|
1798
|
-
]
|
|
1799
|
-
```
|
|
1800
|
-
|
|
1801
|
-
`scripts/build.cjs` (consumer-owned):
|
|
1802
|
-
|
|
1803
|
-
```js
|
|
1804
|
-
const { execFileSync } = require('node:child_process');
|
|
1805
|
-
const { mkdirSync, copyFileSync } = require('node:fs');
|
|
1806
|
-
const target = process.env.TARGET;
|
|
1807
|
-
if (!target || target === 'main' || target === 'noarch') process.exit(0);
|
|
1808
|
-
const binName = 'my-cli';
|
|
1809
|
-
const ext = target.includes('windows') ? '.exe' : '';
|
|
1810
|
-
execFileSync('rustup', ['target', 'add', target], { stdio: 'inherit' });
|
|
1811
|
-
execFileSync('cargo', ['build', '--release', '--target', target, '--bin', binName],
|
|
1812
|
-
{ cwd: '../../crates/my-cli', stdio: 'inherit' });
|
|
1813
|
-
mkdirSync(`build/${target}`, { recursive: true });
|
|
1814
|
-
copyFileSync(`../../crates/my-cli/target/${target}/release/${binName}${ext}`,
|
|
1815
|
-
`build/${target}/${binName}${ext}`);
|
|
1816
|
-
```
|
|
1817
|
-
|
|
1818
|
-
`package.json` (consumer-owned):
|
|
1819
|
-
|
|
1820
|
-
```json
|
|
1821
|
-
{ "scripts": { "build": "tsc && node scripts/build.cjs" } }
|
|
1822
|
-
```
|
|
1823
|
-
|
|
1824
|
-
After (`putitoutthere.toml`):
|
|
1825
|
-
|
|
1826
|
-
```toml
|
|
1827
|
-
[[package]]
|
|
1828
|
-
name = "my-cli"
|
|
1829
|
-
kind = "npm"
|
|
1830
|
-
build = "bundled-cli"
|
|
1831
|
-
path = "packages/ts-cli"
|
|
1832
|
-
globs = ["packages/ts-cli/**", "crates/my-cli/**"]
|
|
1833
|
-
targets = [
|
|
1834
|
-
"x86_64-unknown-linux-gnu",
|
|
1835
|
-
"x86_64-apple-darwin",
|
|
1836
|
-
# ...
|
|
1837
|
-
]
|
|
1838
|
-
|
|
1839
|
-
[package.bundle_cli]
|
|
1840
|
-
bin = "my-cli"
|
|
1841
|
-
crate_path = "crates/my-cli"
|
|
1842
|
-
# features = ["cli"] # if the binary is feature-gated
|
|
1843
|
-
# no_default_features = false
|
|
1844
|
-
```
|
|
1845
|
-
|
|
1846
|
-
`scripts/build.cjs`: **deleted.** The `build` script in
|
|
1847
|
-
`package.json` typically becomes `"tsc"` (or whatever your
|
|
1848
|
-
TypeScript launcher build was minus the `node scripts/build.cjs`
|
|
1849
|
-
half).
|
|
1850
|
-
|
|
1851
|
-
**During migration both shapes coexist.** A consumer that still
|
|
1852
|
-
ships `scripts/build.cjs` keeps working — the workflow's cargo
|
|
1853
|
-
build runs first, then `npm run build --if-present` runs the
|
|
1854
|
-
consumer's script (which sees `build/<triple>/` already populated
|
|
1855
|
-
and probably no-ops). There's no transitional broken state and
|
|
1856
|
-
no flag to set.
|
|
1857
|
-
|
|
1858
|
-
**Constraint.** The binary must build with a vanilla
|
|
1859
|
-
`cargo build --release --target <triple> --bin <bin>` from
|
|
1860
|
-
`crate_path`. Optional `features` / `no_default_features` cover
|
|
1861
|
-
the `[[bin]] required-features = ["cli"]` shape. Crates that need
|
|
1862
|
-
arbitrary env vars, alternate manifests, Zig-cc cross
|
|
1863
|
-
toolchains, or other cargo flags don't fit the recipe and
|
|
1864
|
-
should keep their own release workflow.
|
|
1865
|
-
|
|
1866
|
-
**Deprecations removed.** None.
|
|
1867
|
-
|
|
1868
|
-
**Behavior changes without code changes.** Existing consumers
|
|
1869
|
-
without `[package.bundle_cli]` declared are unaffected — the
|
|
1870
|
-
workflow's cargo build / stage / guard steps are gated on
|
|
1871
|
-
`matrix.bundle_cli` being set, so consumers who still rely on
|
|
1872
|
-
their hand-written `scripts/build.cjs` see the byte-identical
|
|
1873
|
-
build matrix they saw before. The schema rejects `bundle_cli`
|
|
1874
|
-
declared on a non-bundled-cli npm package (or with empty
|
|
1875
|
-
`targets`), so a typo can't make the block silently inert.
|
|
1876
|
-
|
|
1877
|
-
**Verification.** After upgrading, switch one consumer's
|
|
1878
|
-
`putitoutthere.toml` to declare `[package.bundle_cli]` and
|
|
1879
|
-
remove the `node scripts/build.cjs` half of their `build` script.
|
|
1880
|
-
The next release run's build job will, for each per-target row,
|
|
1881
|
-
include three new step lines: `bundle_cli — add Rust target`,
|
|
1882
|
-
`bundle_cli — cargo build for <triple> (<bin>)`, and
|
|
1883
|
-
`bundle_cli — stage binary into <artifact_path>`, followed by
|
|
1884
|
-
the `bundle_cli — verify <artifact_path>/<bin>` guard before
|
|
1885
|
-
`Upload artifact`. The published per-platform tarball, when
|
|
1886
|
-
downloaded and unpacked, contains the cross-compiled binary at
|
|
1887
|
-
the same path the launcher resolves it from.
|
|
1888
|
-
|
|
1889
|
-
---
|
|
1890
|
-
|
|
1891
|
-
### Preflight pyproject + cargo shape
|
|
1892
|
-
|
|
1893
|
-
**Summary.** Preflight gains two more checks — `requirePyprojectShape`
|
|
1894
|
-
and `requireCargoShape` — that mirror the #280 / #290 pattern for
|
|
1895
|
-
`pyproject.toml` (pypi packages) and `Cargo.toml` (crates packages,
|
|
1896
|
-
plus `bundle_cli` on pypi packages). The maturin / setuptools /
|
|
1897
|
-
hatchling / cargo CLIs surface mismatched-shape errors 10-20 minutes
|
|
1898
|
-
into a release run, deep into the verification build, with messages
|
|
1899
|
-
that don't name the precondition that failed. The new checks fire at
|
|
1900
|
-
publish time alongside the existing `require*` family and at PR time
|
|
1901
|
-
via `check.yml`. Findings aggregate across every failing package so
|
|
1902
|
-
consumers fix them all in one round-trip, exactly the shape the prior
|
|
1903
|
-
checks established. #301.
|
|
1904
|
-
|
|
1905
|
-
**Required changes.** None for well-formed manifests. Repos that
|
|
1906
|
-
declared one of the documented mismatches below previously got a
|
|
1907
|
-
mid-release red; they now get a fast preflight red instead.
|
|
1908
|
-
|
|
1909
|
-
The new error codes:
|
|
1910
|
-
|
|
1911
|
-
| Code | Fires when |
|
|
1912
|
-
|------|------------|
|
|
1913
|
-
| `PIOT_PYPI_NAME_MISMATCH` | `pyproject.toml`'s `[project].name` differs from `[[package]].name` (or the `pypi` override). |
|
|
1914
|
-
| `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`). |
|
|
1915
|
-
| `PIOT_PYPI_DYNAMIC_VERSION_NO_BACKEND` | `[project].dynamic` includes `"version"` but neither `[tool.hatch.version]` nor `[tool.setuptools_scm]` is present. |
|
|
1916
|
-
| `PIOT_PYPI_MATURIN_INCLUDE_MISSING` | `bundle_cli` is set but `[tool.maturin].include` does not cover `bundle_cli.stage_to`. |
|
|
1917
|
-
| `PIOT_CRATES_NAME_MISMATCH` | `Cargo.toml`'s `[package].name` differs from `[[package]].name` (or the `crate` override). |
|
|
1918
|
-
| `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). |
|
|
1919
|
-
| `PIOT_CRATES_FEATURE_NOT_DECLARED` | `features` (on `kind = "crates"` packages) or `bundle_cli.features` references a feature not declared in `[features]`. |
|
|
1920
|
-
| `PIOT_CRATES_WORKSPACE_VERSION_MISMATCH` | `[package].version.workspace = true` but no ancestor `Cargo.toml` declares `[workspace.package].version`. |
|
|
1921
|
-
|
|
1922
|
-
**Deprecations removed.** None.
|
|
1923
|
-
|
|
1924
|
-
**Behavior changes without code changes.** Repos with one of the
|
|
1925
|
-
shapes above used to get a confusing mid-release error from
|
|
1926
|
-
maturin / setuptools / hatchling / cargo (sometimes after a
|
|
1927
|
-
verification build of every transitive dep); they now get a
|
|
1928
|
-
fingerprintable `PIOT_*` error at preflight time, before any side
|
|
1929
|
-
effects. PR-time `check.yml` runs surface the same findings on
|
|
1930
|
-
every pull request, so the typical case is fix-before-merge rather
|
|
1931
|
-
than fix-after-release-red. The `[build-system].build-backend`
|
|
1932
|
-
check is deliberately narrow: a missing `[build-system]` table is
|
|
1933
|
-
allowed (pip falls back to setuptools), and the prefix match
|
|
1934
|
-
tolerates backend-version drift across maturin / setuptools /
|
|
1935
|
-
hatchling.
|
|
1936
|
-
|
|
1937
|
-
**Verification.** Misconfigure one field, run `pnpm putitoutthere
|
|
1938
|
-
check` (or open a PR with `check.yml` wired), see the relevant
|
|
1939
|
-
`PIOT_*` code surface in seconds instead of mid-release.
|
|
1940
|
-
|
|
1941
|
-
### New `check.yml` reusable workflow for PR-time config sanity
|
|
1942
|
-
|
|
1943
|
-
**Summary.** `putitoutthere` now ships a third reusable workflow,
|
|
1944
|
-
`.github/workflows/check.yml`, that drives the engine's `check`
|
|
1945
|
-
subcommand at PR time. The subcommand aggregates every pre-merge
|
|
1946
|
-
check (`putitoutthere.toml` parse + schema, common-mistakes
|
|
1947
|
-
detector, unique-name guard, `depends_on` cycle / dangling-ref
|
|
1948
|
-
detection, `[[package]].path` existence, `globs` matching a
|
|
1949
|
-
tracked file, `tag_format` collisions, npm `repository` field,
|
|
1950
|
-
crates `description` / `license`, pypi `pyproject.toml` +
|
|
1951
|
-
`bundle_cli` binary declaration, npm target triple mapping)
|
|
1952
|
-
and reports findings in one round-trip. Where `release.yml` is
|
|
1953
|
-
the release-time phase and `build.yml` is the heavier per-target
|
|
1954
|
-
build gate, `check.yml` is the cheap config-sanity gate — a few
|
|
1955
|
-
seconds per PR, no `setup-python` / `setup-rust`, no per-target
|
|
1956
|
-
compile. Shipped per the "no release surprises" goal added in
|
|
1957
|
-
#316: anything checkable from the consumer's repo state alone
|
|
1958
|
-
surfaces at PR time, not at release time. Issue #317 (workflow
|
|
1959
|
-
shell) + issue #319 (check list, shipped via #321).
|
|
1960
|
-
|
|
1961
|
-
**Required changes.** Additive. Existing consumers do nothing.
|
|
1962
|
-
Recommended: add a one-line PR-CI workflow.
|
|
1963
|
-
|
|
1964
|
-
```yaml
|
|
1965
|
-
# .github/workflows/check.yml ← new file in your repo, optional
|
|
1966
|
-
name: putitoutthere check
|
|
1967
|
-
|
|
1968
|
-
on:
|
|
1969
|
-
pull_request: {}
|
|
1970
|
-
|
|
1971
|
-
jobs:
|
|
1972
|
-
putitoutthere-check:
|
|
1973
|
-
uses: thekevinscott/putitoutthere/.github/workflows/check.yml@v0
|
|
1974
|
-
```
|
|
1975
|
-
|
|
1976
|
-
The new workflow accepts no `with:` inputs, no `secrets:`, no
|
|
1977
|
-
`permissions:` requirements beyond the default `contents: read` it
|
|
1978
|
-
sets internally. The integration line is the entire surface.
|
|
1979
|
-
|
|
1980
|
-
**Deprecations removed.** None.
|
|
1981
|
-
|
|
1982
|
-
**Behavior changes without code changes.** None. Existing release
|
|
1983
|
-
runs are unaffected — the new workflow is opt-in PR-time CI; the
|
|
1984
|
-
release path is unchanged.
|
|
1985
|
-
|
|
1986
|
-
**Verification.** Open a PR that introduces a typo in
|
|
1987
|
-
`putitoutthere.toml` (e.g. `[[packages]]` instead of `[[package]]`).
|
|
1988
|
-
Without `check.yml` wired, the failure surfaces at release time
|
|
1989
|
-
in a red `plan` step. With `check.yml` wired, the PR fails red at
|
|
1990
|
-
review time with the same diagnosable error message, before the
|
|
1991
|
-
merge.
|
|
1992
|
-
|
|
1993
|
-
### Internal cargo-http-registry alt-registry for crates e2e
|
|
1994
|
-
|
|
1995
|
-
**Summary.** Internal change with no consumer-observable impact. Adds
|
|
1996
|
-
[`cargo-http-registry`](https://github.com/d-e-s-o/cargo-http-registry)
|
|
1997
|
-
— an off-the-shelf, auth-free cargo alt-registry, the lone
|
|
1998
|
-
"Verdaccio for cargo" the survey of the cargo-registry ecosystem
|
|
1999
|
-
turned up — to `e2e-fixture-job.yml`'s publish job, installed via
|
|
2000
|
-
`cargo install --locked` and started as a background process on
|
|
2001
|
-
every crates-bearing matrix row. Two internal engine seams in
|
|
2002
|
-
`src/handlers/crates.ts` consume it: `PIOT_CRATES_REGISTRY_FALLBACK`
|
|
2003
|
-
retries `cargo publish` against the alt-registry on a 429-rate-limit
|
|
2004
|
-
shape from real crates.io ("You have published too many versions of
|
|
2005
|
-
this crate in the last 24 hours") and emits a `::warning::` workflow
|
|
2006
|
-
command so reviewers see the fallback engaged. A symmetric
|
|
2007
|
-
`PIOT_CRATES_REGISTRY_PRIMARY` seam routes the publish *only* at the
|
|
2008
|
-
override URL (no real-crates.io attempt, no fallback); reserved for
|
|
2009
|
-
any future `*-first-publish` crates fixture. A first attempt on
|
|
2010
|
-
this issue wired Kellnr; three CI rounds all 403'd because every
|
|
2011
|
-
*production* cargo alt-registry (Kellnr / alexandrie / ktra /
|
|
2012
|
-
cratery) is multi-tenant-shaped and deliberately rejects
|
|
2013
|
-
fixture-style unrecognized identities. The cargo ecosystem has no
|
|
2014
|
-
analog of npm's per-user self-registration convention, so picking
|
|
2015
|
-
the one off-the-shelf "no-auth" registry is the only path that
|
|
2016
|
-
works without auth gymnastics. The reusable consumer workflow
|
|
2017
|
-
(`release.yml`), `putitoutthere.toml` schema, trailer grammar, the
|
|
2018
|
-
dogfood `release-rust.yml`, and consumer-facing docs are
|
|
2019
|
-
untouched. #331.
|
|
2020
|
-
|
|
2021
|
-
**Required changes.** None.
|
|
2022
|
-
|
|
2023
|
-
**Deprecations removed.** None.
|
|
2024
|
-
|
|
2025
|
-
**Behavior changes without code changes.** None for consumers. For
|
|
2026
|
-
contributors running e2e locally: the publish job in
|
|
2027
|
-
`e2e-fixture-job.yml` now `cargo install`s
|
|
2028
|
-
`cargo-http-registry@0.1.8` on crates-bearing rows (~70s cold; the
|
|
2029
|
-
crate has a lightweight dep tree — tokio rt-only + warp + git2, no
|
|
2030
|
-
openssl-sys / sqlite-sys / aws-lc-sys) and starts it as a
|
|
2031
|
-
background process bound at `127.0.0.1:35503`. Cargo's
|
|
2032
|
-
`net.git-fetch-with-cli = true` is written to `~/.cargo/config.toml`
|
|
2033
|
-
on the same path because libgit2 enforces strict `application/x-git-*`
|
|
2034
|
-
content-type checking that `cargo-http-registry` doesn't satisfy;
|
|
2035
|
-
the system `git` binary is more lenient and works fine. The handler
|
|
2036
|
-
in `src/handlers/crates.ts` now passes `--token <placeholder>`
|
|
2037
|
-
alongside `--index <url>` on alt-registry invocations because
|
|
2038
|
-
cargo's CLI refuses to dispatch publish without an explicit
|
|
2039
|
-
`--token` once `--index` is set — a CLI quirk; the value is never
|
|
2040
|
-
validated by `cargo-http-registry`. Steady-state crates fixtures
|
|
2041
|
-
keep their real-crates.io OIDC-TP path unchanged on the happy path;
|
|
2042
|
-
the fallback only fires when real crates.io returns a 429.
|
|
2043
|
-
|
|
2044
|
-
**Verification.** A successful CI run on a PR that hits the
|
|
2045
|
-
crates.io 24h-per-crate quota (the polyglot fixture's
|
|
2046
|
-
`piot-fixture-zzz-poly-rust` row) goes green via the alt-registry
|
|
2047
|
-
fallback with a `::warning::` in the run log naming the fallback URL,
|
|
2048
|
-
instead of failing red on the 429. When the quota is fresh, the
|
|
2049
|
-
`e2e (polyglot-everything)` row continues to publish to real
|
|
2050
|
-
crates.io and the warning does not fire — visible diagnostic
|
|
2051
|
-
distinction between the two paths. The diagnostic dump step at the
|
|
2052
|
-
end of every crates-bearing publish job emits the
|
|
2053
|
-
cargo-http-registry process log, the readiness-endpoint probe, and
|
|
2054
|
-
the rendered cargo `config.toml` so any future failure has the wire
|
|
2055
|
-
trace inline in the run log.
|
|
2056
|
-
|
|
2057
|
-
### Internal Verdaccio e2e coverage
|
|
2058
|
-
|
|
2059
|
-
**Summary.** Internal change with no consumer-observable impact. Adds a
|
|
2060
|
-
`js-vanilla-first-publish` fixture and matrix row that publishes to an
|
|
2061
|
-
in-job Verdaccio service container; the post-publish tarball-verify step
|
|
2062
|
-
is now registry-agnostic. `PIOT_NPM_REGISTRY` is an internal e2e seam
|
|
2063
|
-
(`src/handlers/npm.ts`, `src/handlers/npm-platform.ts`) that routes the
|
|
2064
|
-
publish at the override registry and suppresses provenance + the
|
|
2065
|
-
public-npm bootstrap-hint path. The reusable consumer workflow
|
|
2066
|
-
(`release.yml`), `putitoutthere.toml` schema, and trailer grammar are
|
|
2067
|
-
untouched. #304 (parent #293).
|
|
2068
|
-
|
|
2069
|
-
**Required changes.** None.
|
|
2070
|
-
|
|
2071
|
-
**Deprecations removed.** None.
|
|
2072
|
-
|
|
2073
|
-
**Behavior changes without code changes.** None for consumers. For
|
|
2074
|
-
contributors running e2e locally: the publish job in
|
|
2075
|
-
`e2e-fixture-job.yml` now spins up a Verdaccio service container per
|
|
2076
|
-
job (~3s startup cost). First-publish fixtures route publish at
|
|
2077
|
-
`http://localhost:4873`; steady-state fixtures keep their real-npm
|
|
2078
|
-
path unchanged.
|
|
2079
|
-
|
|
2080
|
-
**Verification.** A successful CI run on `main` shows the new
|
|
2081
|
-
`e2e (js-vanilla-first-publish)` matrix row green. The existing
|
|
2082
|
-
`e2e (js-vanilla)` row remains green, confirming the real-npm
|
|
2083
|
-
steady-state hasn't regressed. To demonstrate the #256 tarball-verify
|
|
2084
|
-
contract, trigger `E2E` via `workflow_dispatch` with
|
|
2085
|
-
`simulate_no_dist: true` — the `js-vanilla-first-publish` row should
|
|
2086
|
-
go red on `tarball missing 'dist'`.
|
|
2087
|
-
|
|
2088
|
-
### Single-artifact publish layout normalization
|
|
2089
|
-
|
|
2090
|
-
**Summary.** The reusable workflow's publish job downloads build
|
|
2091
|
-
artifacts with `actions/download-artifact@v8` configured as `path:
|
|
2092
|
-
artifacts` and no `name`/`pattern` filter. That action is
|
|
2093
|
-
count-sensitive: multiple artifacts land in `artifacts/<name>/...`
|
|
2094
|
-
subdirs (the documented multi-case the engine's completeness check
|
|
2095
|
-
and every handler are written against), but a *single* artifact
|
|
2096
|
-
extracts directly into `artifacts/` with no per-artifact subdir.
|
|
2097
|
-
Consumers whose plan emits exactly one expected artifact — pure-Python
|
|
2098
|
-
packages with `build = "hatch"` (sdist row only) being the canonical
|
|
2099
|
-
case — therefore failed at the completeness check with `missing
|
|
2100
|
-
artifact directory <pkg>-sdist/` before the pypi handler ever ran.
|
|
2101
|
-
Multi-artifact consumers (pypi+npm, sdist+wheels, polyglot) were
|
|
2102
|
-
unaffected. The publish job now normalizes the layout in-process
|
|
2103
|
-
before completeness: when the plan expects one staged artifact and
|
|
2104
|
-
`artifacts/<artifact_name>/` is absent, files in `artifacts/` are
|
|
2105
|
-
moved into that subdir so the engine's contract holds. Fully a
|
|
2106
|
-
fix-in-place; no input shape, output shape, or config key changed.
|
|
2107
|
-
Tracked in #311.
|
|
2108
|
-
|
|
2109
|
-
**Required changes.** None.
|
|
2110
|
-
|
|
2111
|
-
**Deprecations removed.** None.
|
|
2112
|
-
|
|
2113
|
-
**Behavior changes without code changes.**
|
|
2114
|
-
|
|
2115
|
-
- Pypi-only consumers with `build = "hatch"` (or any other
|
|
2116
|
-
`[[package]]` whose plan emits a single artifact row) that
|
|
2117
|
-
previously failed publish with `Artifact completeness check
|
|
2118
|
-
failed: ... missing artifact directory <pkg>-sdist/` now reach the
|
|
2119
|
-
pypi handler successfully and tag as expected. The wider release
|
|
2120
|
-
flow — version-rewrite, tag creation, GitHub Release, caller-side
|
|
2121
|
-
`pypi-publish` upload — was already correct; only the engine's
|
|
2122
|
-
pre-publish completeness check was upstream of the bug.
|
|
2123
|
-
- Multi-artifact plans (≥2 staged artifacts), crates-only plans, and
|
|
2124
|
-
vanilla-npm plans (`[[package]] kind = "npm"` with no `build` /
|
|
2125
|
-
`build = []`) see no observable difference. The normalization is
|
|
2126
|
-
scoped to the exact case `download-artifact@v8` dumps into the
|
|
2127
|
-
root.
|
|
2128
|
-
|
|
2129
|
-
**Verification.** A release-please / release-plz cascade against a
|
|
2130
|
-
pure-Python `[[package]]` with `build = "hatch"` should:
|
|
2131
|
-
|
|
2132
|
-
1. Surface the planned matrix as a single row,
|
|
2133
|
-
`target = sdist artifact = <pkg>-sdist`.
|
|
2134
|
-
2. Reach the `pypi: <pkg>@<version> delegated to caller-side upload
|
|
2135
|
-
step.` log line in the publish job.
|
|
2136
|
-
3. Push a `<pkg>-v<version>` tag, kick off the caller's
|
|
2137
|
-
`pypi-publish` job, and produce a GitHub Release. No
|
|
2138
|
-
`missing artifact directory` error appears in the run log.
|
|
2139
|
-
|
|
2140
|
-
### `[package.bundle_cli]` features and `no_default_features`
|
|
2141
|
-
|
|
2142
|
-
**Summary.** `[package.bundle_cli]` previously only worked for crates
|
|
2143
|
-
whose CLI binary built with a vanilla `cargo build --release --target
|
|
2144
|
-
<triple> --bin <bin>` — no feature flags, no env. The standard shape
|
|
2145
|
-
for libraries that ship an optional CLI (ruff, uv, pydantic-core,
|
|
2146
|
-
biome, swc, dirsql) is `[[bin]] required-features = ["cli"]`, so the
|
|
2147
|
-
binary's deps don't pollute `cargo add <name>`. Without a way to pass
|
|
2148
|
-
`--features`, `cargo build --bin <bin>` exits with `target ... requires
|
|
2149
|
-
the features: cli` and the recipe was inert for the consumers it was
|
|
2150
|
-
designed for. The schema now exposes:
|
|
2151
|
-
|
|
2152
|
-
- `features: list[string]` — forwarded to `cargo build --features
|
|
2153
|
-
<comma-list>` when non-empty. Defaults to `[]`.
|
|
2154
|
-
- `no_default_features: bool` — adds `--no-default-features` when
|
|
2155
|
-
true. Defaults to `false`.
|
|
2156
|
-
|
|
2157
|
-
Both keys are optional; the schema defaults preserve byte-identical
|
|
2158
|
-
cargo invocations for existing `[package.bundle_cli]` blocks. Empty
|
|
2159
|
-
strings inside the `features` list are rejected at config load. The
|
|
2160
|
-
caveat under [`bundle_cli` now actually stages the binary](#packagebundle_cli-now-actually-stages-the-binary)
|
|
2161
|
-
that named this gap as "not currently supported" has been corrected.
|
|
2162
|
-
Tracked in #300.
|
|
2163
|
-
|
|
2164
|
-
**Required changes.** None for consumers whose binary builds without
|
|
2165
|
-
feature flags. Consumers whose `Cargo.toml` declares `required-features
|
|
2166
|
-
= ["cli"]` (or who otherwise need a non-default feature set on the CLI
|
|
2167
|
-
binary) add the new keys:
|
|
2168
|
-
|
|
2169
|
-
```diff
|
|
2170
|
-
[package.bundle_cli]
|
|
2171
|
-
bin = "my-cli"
|
|
2172
|
-
stage_to = "src/my_py/_binary"
|
|
2173
|
-
crate_path = "crates/my-rust"
|
|
2174
|
-
+features = ["cli"]
|
|
2175
|
-
+no_default_features = false
|
|
2176
|
-
```
|
|
2177
|
-
|
|
2178
|
-
**Deprecations removed.** None.
|
|
2179
|
-
|
|
2180
|
-
**Behavior changes without code changes.** None. The new keys default
|
|
2181
|
-
to the equivalent of "no extra cargo flags," matching pre-#300
|
|
2182
|
-
behavior.
|
|
2183
|
-
|
|
2184
|
-
**Verification.** Trigger a release on a maturin pypi package whose
|
|
2185
|
-
crate uses `[[bin]] required-features = ["cli"]` and that now declares
|
|
2186
|
-
`features = ["cli"]` in its `[package.bundle_cli]` block. The
|
|
2187
|
-
`bundle_cli — cargo build for <triple> (<bin>)` step in the build job's
|
|
2188
|
-
log emits `cargo build --release --target <triple> --bin <bin>
|
|
2189
|
-
--features cli` and exits zero; the wheel-content guard step that
|
|
2190
|
-
follows confirms `<stage_to>/<bin>` is present in the produced `.whl`.
|
|
2191
|
-
|
|
2192
|
-
---
|
|
2193
|
-
|
|
2194
|
-
### Crates Cargo.toml must declare `description` and `license`
|
|
2195
|
-
|
|
2196
|
-
**Summary.** Every cascaded `kind = "crates"` package's `Cargo.toml`
|
|
2197
|
-
must now declare `[package].description` and either `[package].license`
|
|
2198
|
-
or `[package].license-file`. The new preflight check
|
|
2199
|
-
(`requireCratesMetadata` in `src/preflight.ts`) runs in
|
|
2200
|
-
`src/publish.ts` immediately after `requireProvenanceMetadata` and
|
|
2201
|
-
rejects the run with `PIOT_CRATES_MISSING_METADATA` before any
|
|
2202
|
-
runner work. Why: crates.io rejects publish with `400 Bad Request:
|
|
2203
|
-
missing or empty metadata fields: ...` after `cargo publish`'s
|
|
2204
|
-
verification build has compiled the crate and every transitive dep
|
|
2205
|
-
— wasting the entire publish job (often a minute+ of compile time
|
|
2206
|
-
plus the upload) on a precondition checkable in milliseconds. Hit
|
|
2207
|
-
in the wild on `thekevinscott/darkfactory`'s first crate publish;
|
|
2208
|
-
tracked in #290. Same shape as #280 (npm `repository`).
|
|
2209
|
-
|
|
2210
|
-
**Required changes.** Add the two fields to every Cargo.toml
|
|
2211
|
-
declared as `kind = "crates"` in `putitoutthere.toml`.
|
|
2212
|
-
|
|
2213
|
-
| Before | After |
|
|
2214
|
-
| --------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
2215
|
-
| `[package]`<br>`name = "lib"`<br>`version = "0.1.0"`<br>`edition = "2021"` | `[package]`<br>`name = "lib"`<br>`version = "0.1.0"`<br>`edition = "2021"`<br>`description = "One-line summary."`<br>`license = "MIT OR Apache-2.0"` |
|
|
2216
|
-
|
|
2217
|
-
`license-file = "LICENSE"` is accepted as a substitute for
|
|
2218
|
-
`license` (the SPDX expression form). Whitespace-only values
|
|
2219
|
-
(`description = " "`) are treated as empty.
|
|
2220
|
-
|
|
2221
|
-
No `release.yml` or `putitoutthere.toml` changes are required.
|
|
2222
|
-
|
|
2223
|
-
**Deprecations removed.** None.
|
|
2224
|
-
|
|
2225
|
-
**Behavior changes without code changes.** `putitoutthere publish`
|
|
2226
|
-
now fails fast at preflight with
|
|
2227
|
-
`[PIOT_CRATES_MISSING_METADATA] cargo publish requires the
|
|
2228
|
-
following Cargo.toml [package] fields: description, and license
|
|
2229
|
-
(or license-file).` for any cascaded crates package whose
|
|
2230
|
-
Cargo.toml lacks the fields. Previously the same shape ran
|
|
2231
|
-
through plan + auth-negotiation + ~30s of cargo verification
|
|
2232
|
-
build and failed inside `cargo publish` with a 400 from
|
|
2233
|
-
crates.io. Well-formed packages are unaffected.
|
|
2234
|
-
|
|
2235
|
-
The error message names every failing package, its `Cargo.toml`
|
|
2236
|
-
path, and the specific missing fields, so consumers fix all of
|
|
2237
|
-
them in one round-trip rather than discovering them one at a
|
|
2238
|
-
time across multiple release attempts.
|
|
2239
|
-
|
|
2240
|
-
**Verification.** Drop `description` from one of your Cargo.toml
|
|
2241
|
-
files locally and run `pnpm test:integration` (or the engine
|
|
2242
|
-
against a local fixture); the failure should arrive at preflight
|
|
2243
|
-
with `PIOT_CRATES_MISSING_METADATA` in the message, no crate
|
|
2244
|
-
should be published, and no git tag should be created. Restore
|
|
2245
|
-
the field; the next run completes normally.
|
|
2246
|
-
|
|
2247
|
-
---
|
|
2248
|
-
|
|
2249
|
-
### npm build step: TARGET / BUILD env vars
|
|
2250
|
-
|
|
2251
|
-
**Summary.** The reusable workflow's `_matrix.yml` and `release.yml`
|
|
2252
|
-
now set `TARGET=${{ matrix.target }}` and `BUILD=${{ matrix.build }}`
|
|
2253
|
-
on the `matrix.kind == 'npm'` build step (and `TARGET=main BUILD=`
|
|
2254
|
-
on `release.yml`'s publish-job rebuild loop, which only ever runs
|
|
2255
|
-
for the main package's row). Bundled-CLI / napi consumers' build
|
|
2256
|
-
scripts read these env vars to know which triple to cross-compile
|
|
2257
|
-
and which build mode is active; without them, every per-platform
|
|
2258
|
-
matrix row produced an empty `build/<triple>/` directory and
|
|
2259
|
-
`actions/upload-artifact@v7` reported
|
|
2260
|
-
`No files were found with the provided path: ...`. The internal
|
|
2261
|
-
`e2e-fixture-job.yml` already passed `TARGET` / `BUILD` for the
|
|
2262
|
-
`js-bundled-cli` fixture, so the fixture suite was green while the
|
|
2263
|
-
shape it advertised was broken for real consumers. Tracked at #287;
|
|
2264
|
-
hit in the wild on `thekevinscott/darkfactory`'s first release.
|
|
2265
|
-
|
|
2266
|
-
The README's [Bundled-CLI npm family](./README.md#bundled-cli-npm-family)
|
|
2267
|
-
recipe gained the consumer-side build-script contract that was
|
|
2268
|
-
previously missing: a documented `scripts/build.cjs` template
|
|
2269
|
-
that reads `TARGET`, runs `rustup target add` + `cargo build`
|
|
2270
|
-
+ stage-into-`build/<triple>/`, and no-ops on
|
|
2271
|
-
`TARGET=main`. Without that section, consumers had to read the
|
|
2272
|
-
`js-bundled-cli` test fixture or experiment to discover the
|
|
2273
|
-
contract.
|
|
2274
|
-
|
|
2275
|
-
**Required changes.** None for consumers whose existing build
|
|
2276
|
-
script either ignored `TARGET` or used a different env var name —
|
|
2277
|
-
those scripts now get the `TARGET` / `BUILD` exports too but
|
|
2278
|
-
nothing forces them to read. To start using the contract, mirror
|
|
2279
|
-
the README's `scripts/build.cjs` shape and reference it from
|
|
2280
|
-
`package.json`'s `scripts.build` field. Existing fixtures and
|
|
2281
|
-
the engine's contract are unchanged.
|
|
2282
|
-
|
|
2283
|
-
**Deprecations removed.** None.
|
|
2284
|
-
|
|
2285
|
-
**Behavior changes without code changes.** The npm build step
|
|
2286
|
-
now runs with `TARGET` and `BUILD` set in its environment.
|
|
2287
|
-
Build scripts that previously saw `undefined` and either crashed
|
|
2288
|
-
or silently no-oped will now see a defined string. Vanilla npm
|
|
2289
|
-
consumers (no `build = "bundled-cli" | "napi"`) see
|
|
2290
|
-
`TARGET=main BUILD=` (or `BUILD=undefined` on rows that don't
|
|
2291
|
-
declare a build mode); their build scripts that don't read either
|
|
2292
|
-
var are unaffected.
|
|
2293
|
-
|
|
2294
|
-
**Verification.** A bundled-cli consumer following the README
|
|
2295
|
-
recipe sees their per-platform matrix rows produce non-empty
|
|
2296
|
-
artifacts: each `<pkg>-npm-<triple>` upload contains
|
|
2297
|
-
`build/<triple>/<bin-name>`. The release run's `Upload artifact`
|
|
2298
|
-
step no longer logs `No files were found with the provided path`
|
|
2299
|
-
for any npm row.
|
|
2300
|
-
### First-publish bundled-cli lockfile self-heal
|
|
2301
|
-
|
|
2302
|
-
**Summary.** The reusable workflow's npm install steps —
|
|
2303
|
-
`_matrix.yml`'s build-matrix install and `release.yml`'s
|
|
2304
|
-
publish-job rebuild (#256) — both ran strict installs (`npm ci`
|
|
2305
|
-
or `pnpm install --frozen-lockfile`) and refused on any drift
|
|
2306
|
-
between the committed lockfile and `package.json`. For consumers
|
|
2307
|
-
of the bundled-cli / napi shape, drift is the *expected* state on
|
|
2308
|
-
the first publish: `package.json` declares
|
|
2309
|
-
`optionalDependencies` for `<name>-<triple>@<version>` platform
|
|
2310
|
-
packages that this pipeline produces, those packages don't exist
|
|
2311
|
-
on the registry yet, pnpm 10 silently drops 404'd optionals from
|
|
2312
|
-
the lockfile when it is regenerated locally, and the next CI run
|
|
2313
|
-
sees lockfile and `package.json` disagree. Hit in the wild on
|
|
2314
|
-
`thekevinscott/darkfactory`'s first release.
|
|
2315
|
-
|
|
2316
|
-
Both install steps now fall back from the strict form to its
|
|
2317
|
-
non-strict counterpart on failure (`pnpm install --no-frozen-lockfile`
|
|
2318
|
-
/ `npm install`) and emit a `::warning::` line in the run log
|
|
2319
|
-
naming the recovery. The README's
|
|
2320
|
-
[Bundled-CLI npm family](./README.md#bundled-cli-npm-family)
|
|
2321
|
-
recipe grew a `[!NOTE]` callout documenting the chicken-and-egg
|
|
2322
|
-
and the workflow's transparent recovery.
|
|
2323
|
-
|
|
2324
|
-
**Required changes.** None. Consumers who were working around
|
|
2325
|
-
the failure by gitignoring the lockfile, by suppressing
|
|
2326
|
-
`optionalDependencies` from `package.json`, or by pinning to
|
|
2327
|
-
older lockfile-tolerant pnpm versions can revert those
|
|
2328
|
-
workarounds; the workflow now handles the bootstrap state on
|
|
2329
|
-
its own.
|
|
2330
|
-
|
|
2331
|
-
**Deprecations removed.** None.
|
|
2332
|
-
|
|
2333
|
-
**Behavior changes without code changes.** Strict installs that
|
|
2334
|
-
previously failed red on lockfile drift now succeed via the
|
|
2335
|
-
non-strict fallback path. The build artifact is unchanged
|
|
2336
|
-
(installs the deps `package.json` declares); only the strictness
|
|
2337
|
-
of *how* it gets there relaxes. Healthy lockfiles still take
|
|
2338
|
-
the strict path with no observable difference. The new
|
|
2339
|
-
`::warning::` lines are visible in the run log on the GitHub
|
|
2340
|
-
Actions UI but do not fail the run.
|
|
2341
|
-
|
|
2342
|
-
**Verification.** A bundled-cli / napi consumer's first-publish
|
|
2343
|
-
release run completes the build matrix without manual lockfile
|
|
2344
|
-
fiddling. The run log contains a single
|
|
2345
|
-
`::warning::pnpm-lock.yaml drift ...` line per affected install
|
|
2346
|
-
step (one in the build matrix, one in the publish-job rebuild)
|
|
2347
|
-
when the strict install fails; healthy installs see no
|
|
2348
|
-
warning.
|
|
2349
|
-
|
|
2350
|
-
### Platform-publish `.npmrc` lookup
|
|
2351
|
-
|
|
2352
|
-
**Summary.** The reusable workflow's per-triple platform-package
|
|
2353
|
-
publishes (`build = "bundled-cli"` / `build = "napi"`) ran `npm
|
|
2354
|
-
publish` from a temporary staging directory rather than from the
|
|
2355
|
-
consumer's package path. npm reads `.npmrc` from cwd upward; the
|
|
2356
|
-
consumer's `.npmrc` lives at `pkg.path`, never on the path to a
|
|
2357
|
-
tempdir, so platform publishes never saw the auth the main package
|
|
2358
|
-
relied on. OIDC trusted publishing masked the gap (auth flows via
|
|
2359
|
-
the `ACTIONS_ID_TOKEN_REQUEST_TOKEN` environment variable, not
|
|
2360
|
-
`.npmrc`), but the `NPM_TOKEN` bootstrap path (#310) — required
|
|
2361
|
-
for the very first publish of a brand-new npm package — and the
|
|
2362
|
-
internal Verdaccio e2e seam (#304) both broke because both rely on
|
|
2363
|
-
`.npmrc`-supplied auth.
|
|
2364
|
-
|
|
2365
|
-
The engine now invokes `npm publish <stagingDir>` with `cwd:
|
|
2366
|
-
pkg.path`, matching how the main-package publish already runs.
|
|
2367
|
-
npm reads the consumer's `.npmrc` (including any `_authToken`,
|
|
2368
|
-
`always-auth`, or scoped-registry entries) and applies it to the
|
|
2369
|
-
PUT for each per-triple platform package.
|
|
2370
|
-
|
|
2371
|
-
**Required changes.** None. The fix is internal to
|
|
2372
|
-
`src/handlers/npm-platform.ts`; consumer `release.yml` flows are
|
|
2373
|
-
unchanged.
|
|
2374
|
-
|
|
2375
|
-
**Deprecations removed.** None.
|
|
2376
|
-
|
|
2377
|
-
**Behavior changes without code changes.** Consumers who rely on
|
|
2378
|
-
`NPM_TOKEN` (rather than OIDC) for the first publish of a
|
|
2379
|
-
bundled-cli / napi family — i.e. a brand-new npm package whose
|
|
2380
|
-
per-triple sub-packages also don't exist yet — now succeed without
|
|
2381
|
-
the workaround of publishing a `0.0.0-bootstrap` stub by hand.
|
|
2382
|
-
OIDC consumers see no observable difference: the same env-derived
|
|
2383
|
-
auth keeps flowing because `npm publish` reads
|
|
2384
|
-
`NODE_AUTH_TOKEN`/`ACTIONS_ID_TOKEN_REQUEST_TOKEN` from the
|
|
2385
|
-
environment regardless of which directory cwd points at.
|
|
2386
|
-
|
|
2387
|
-
**Verification.** A consumer publishing a brand-new bundled-cli /
|
|
2388
|
-
napi family via `NPM_TOKEN` completes per-triple sub-package
|
|
2389
|
-
publishes alongside the main package on the first release run,
|
|
2390
|
-
with no `npm publish (platform) failed` errors in the log.
|
|
2391
|
-
|
|
2392
|
-
### `[package.bundle_cli]` now actually stages the binary
|
|
2393
|
-
|
|
2394
|
-
**Summary.** Wheels published from a maturin pypi package that
|
|
2395
|
-
declared `[package.bundle_cli]` previously shipped without the
|
|
2396
|
-
bundled CLI binary. The block was parsed by config, attached to
|
|
2397
|
-
per-target wheel rows by the planner, and documented in the README
|
|
2398
|
-
and MIGRATIONS — but the reusable workflow's build job
|
|
2399
|
-
(`.github/workflows/_matrix.yml`) had no step that consumed the
|
|
2400
|
-
metadata. Consumers' wheels arrived on PyPI missing the file the
|
|
2401
|
-
launcher in `[project.scripts]` resolved at runtime, and
|
|
2402
|
-
`pip install <pkg> && <pkg> ...` failed with `FileNotFoundError`.
|
|
2403
|
-
The recipe was advertised as shipped in v0.2.0 (#217) but was
|
|
2404
|
-
silently a no-op for over a release cycle. Hit in the wild on
|
|
2405
|
-
`thekevinscott/dirsql`; tracked in #282.
|
|
2406
|
-
|
|
2407
|
-
The workflow now runs four new steps for every per-target wheel
|
|
2408
|
-
row where `matrix.bundle_cli` is set:
|
|
2409
|
-
|
|
2410
|
-
1. `rustup target add ${{ matrix.target }}` — make the triple known.
|
|
2411
|
-
2. `cargo build --release --target ${{ matrix.target }} --bin ${{ matrix.bundle_cli.bin }}`
|
|
2412
|
-
against `crate_path` — produce the binary on the native host
|
|
2413
|
-
runner (`defaultRunsOn` in `src/plan.ts` already maps every
|
|
2414
|
-
supported triple to a native runner, so cross-compile linkers
|
|
2415
|
-
are not needed).
|
|
2416
|
-
3. Copy the resulting binary (with `.exe` suffix on Windows) into
|
|
2417
|
-
`${{ matrix.path }}/${{ matrix.bundle_cli.stage_to }}/` so
|
|
2418
|
-
maturin's `[tool.maturin].include` glob picks it up as wheel
|
|
2419
|
-
data.
|
|
2420
|
-
4. After `PyO3/maturin-action@v1` produces the `.whl`, open the
|
|
2421
|
-
wheel and refuse `upload-artifact` if it does not contain a
|
|
2422
|
-
file at any directory ending in `<stage_to>/<bin>`. This guard
|
|
2423
|
-
stays useful after the staging steps land — it catches any
|
|
2424
|
-
future regression where the cross-compile silently routes the
|
|
2425
|
-
binary to the wrong path, and it ensures broken wheels never
|
|
2426
|
-
leave the build runner.
|
|
2427
|
-
|
|
2428
|
-
**Required changes.** None for consumers whose existing
|
|
2429
|
-
`[package.bundle_cli]` block follows the documented shape — the
|
|
2430
|
-
recipe simply starts working. Consumers who relied on the broken
|
|
2431
|
-
state (e.g., shipped a workaround that hardcoded a copy of the
|
|
2432
|
-
binary into the source tree before `putitoutthere` ran) can
|
|
2433
|
-
remove the workaround.
|
|
2434
|
-
|
|
2435
|
-
**Constraint not previously documented.** The cross-compile
|
|
2436
|
-
assumes the binary is buildable with a vanilla
|
|
2437
|
-
`cargo build --release --bin <bin>` — no env vars, no special
|
|
2438
|
-
build config. Crates that gate the CLI behind a Cargo feature
|
|
2439
|
-
(e.g., `--features cli`) are now supported via
|
|
2440
|
-
`[package.bundle_cli].features` and
|
|
2441
|
-
`[package.bundle_cli].no_default_features`; see
|
|
2442
|
-
[`bundle_cli` features and `no_default_features`](#packagebundle_cli-features-and-no_default_features).
|
|
2443
|
-
|
|
2444
|
-
**Deprecations removed.** None.
|
|
2445
|
-
|
|
2446
|
-
**Behavior changes without code changes.** Existing
|
|
2447
|
-
`[package.bundle_cli]` blocks change behavior at upgrade time:
|
|
2448
|
-
the next release run produces wheels that contain the binary
|
|
2449
|
-
(previously the workflow silently published wheels without it).
|
|
2450
|
-
If a consumer's `[tool.maturin].include` path resolves to nothing
|
|
2451
|
-
(typo, mismatched layout), the new wheel-content guard fails the
|
|
2452
|
-
build red instead of silently uploading an unusable wheel.
|
|
2453
|
-
|
|
2454
|
-
**Verification.** After upgrading, trigger a release on a maturin
|
|
2455
|
-
package that declares `[package.bundle_cli]`. The build job's log
|
|
2456
|
-
includes a `bundle_cli — verify wheel contains <stage_to>/<bin>`
|
|
2457
|
-
step that ends with `ok bundle_cli: <stage_to>/<bin> present in
|
|
2458
|
-
<wheel-name>.whl`. The published wheel, when downloaded and
|
|
2459
|
-
unzipped, contains the binary at the expected path.
|
|
2460
|
-
`pip install <pkg> && <pkg> --version` runs the launcher and the
|
|
2461
|
-
launcher resolves the binary inside the wheel.
|
|
2462
|
-
|
|
2463
|
-
---
|
|
2464
|
-
|
|
2465
|
-
### Friendly config error hints
|
|
2466
|
-
|
|
2467
|
-
**Summary.** A consumer integration produced a `putitoutthere.toml`
|
|
2468
|
-
with four shape mistakes at once: `version = 1` declared at the
|
|
2469
|
-
file root rather than under `[putitoutthere]`, `[[packages]]`
|
|
2470
|
-
(plural) instead of `[[package]]` (singular), `registry =` instead
|
|
2471
|
-
of `kind =`, and `files =` instead of `globs =`. The engine's
|
|
2472
|
-
zod-derived error message named none of those four typos by their
|
|
2473
|
-
correct equivalent — `Invalid input: expected object, received
|
|
2474
|
-
undefined; ...; Unrecognized keys: "version", "packages"` is
|
|
2475
|
-
technically correct and operationally useless. `parseConfig` now
|
|
2476
|
-
runs a pre-pass that detects each of these four cases by inspecting
|
|
2477
|
-
the parsed TOML before zod runs, and emits a hint that names both
|
|
2478
|
-
the wrong key and the right one. The README's [Drop in
|
|
2479
|
-
`putitoutthere.toml`](./README.md#2-drop-in-putitoutthere-toml)
|
|
2480
|
-
section grew a wrong→right table covering the same four traps so
|
|
2481
|
-
the docs and the engine speak the same vocabulary, the
|
|
2482
|
-
[Drop in `.github/workflows/release.yml`](./README.md#1-drop-in-githubworkflowsreleaseyml)
|
|
2483
|
-
section grew an `[!IMPORTANT]` callout warning consumers off
|
|
2484
|
-
`push: branches: [main]` triggers on lane CI workflows, and `1b.
|
|
2485
|
-
build-check.yml` was promoted from "Optional" to "Recommended"
|
|
2486
|
-
because it's the cheapest pre-merge surface that exercises
|
|
2487
|
-
`parseConfig` on the consumer's actual config.
|
|
2488
|
-
|
|
2489
|
-
**Required changes.** None. The hints fire only on configs that
|
|
2490
|
-
were already failing validation; valid configs are unaffected.
|
|
2491
|
-
A config that was failing with a confusing zod message before will
|
|
2492
|
-
now fail with a hint message that names the fix:
|
|
2493
|
-
|
|
2494
|
-
| Wrong (still rejected, clearer message) | Right |
|
|
2495
|
-
| -------------------------------------------------------------------- | ------------------------------------------------ |
|
|
2496
|
-
| `version = 1` at file root, no `[putitoutthere]` table | `[putitoutthere]` table with `version = 1` inside |
|
|
2497
|
-
| `[[packages]]` (plural) | `[[package]]` (singular, one block per package) |
|
|
2498
|
-
| `registry = "crates"` | `kind = "crates"` |
|
|
2499
|
-
| `files = ["src/**"]` | `globs = ["src/**"]` |
|
|
2500
|
-
|
|
2501
|
-
Consumers with healthy configs can ignore this. Consumers with
|
|
2502
|
-
broken configs whose CI was previously red against a zod message
|
|
2503
|
-
will see the same red CI with a clearer message — fix the config
|
|
2504
|
-
shape per the table above.
|
|
2505
|
-
|
|
2506
|
-
**Deprecations removed.** None.
|
|
2507
|
-
|
|
2508
|
-
**Behavior changes without code changes.** Error message text
|
|
2509
|
-
on failed config validation. The exit code, the failure surface
|
|
2510
|
-
(`parseConfig` throwing inside the engine), and the set of
|
|
2511
|
-
configs that pass validation are all unchanged.
|
|
2512
|
-
|
|
2513
|
-
**Verification.** A failing config with any of the four mistakes
|
|
2514
|
-
above will now contain the words `did you mean` in its CI log
|
|
2515
|
-
output. Repos with valid configs see no change in any release
|
|
2516
|
-
or build-check run.
|
|
2517
|
-
|
|
2518
|
-
---
|
|
2519
|
-
|
|
2520
|
-
### npm token fallback
|
|
2521
|
-
|
|
2522
|
-
**Summary.** The reusable workflow now accepts an optional
|
|
2523
|
-
`NPM_TOKEN` via `secrets:`. Trusted Publishing on npm binds to
|
|
2524
|
-
an *already-published* package, so the very first publish of a
|
|
2525
|
-
brand-new npm package has no OIDC path available; without this
|
|
2526
|
-
fallback every first-time bundled-cli / napi consumer hit a 6+
|
|
2527
|
-
package manual `0.0.0-bootstrap` stub bootstrap, documented
|
|
2528
|
-
nowhere, only discoverable by reading commit history of dirsql
|
|
2529
|
-
or by hitting the failure. OIDC trusted publishers remain the
|
|
2530
|
-
default and recommended path — when the secret is unset,
|
|
2531
|
-
behavior is byte-for-byte unchanged. When the secret is set
|
|
2532
|
-
AND the planned matrix contains an npm row, the secret is
|
|
2533
|
-
exported to `$GITHUB_ENV` as `NODE_AUTH_TOKEN`; the npm CLI
|
|
2534
|
-
then prefers the long-lived token over the OIDC path. Mirror
|
|
2535
|
-
of #283 (crates) in shape, byte-for-byte. Hit in the wild on
|
|
2536
|
-
the maintainer's own dirsql project (first version of
|
|
2537
|
-
`@dirsql/cli-linux-x64-gnu` on npm is `0.0.0-bootstrap`,
|
|
2538
|
-
2026-04-30; real `0.2.8` lands the next day) and on
|
|
2539
|
-
`darkfactory`'s first publish. #302.
|
|
2540
|
-
|
|
2541
|
-
**Required changes.** None for consumers already on the OIDC
|
|
2542
|
-
path. To bootstrap a brand-new npm package or to use the
|
|
2543
|
-
workflow on an account where Trusted Publishing isn't
|
|
2544
|
-
available, wire the secret in the caller's `release.yml`:
|
|
2545
|
-
|
|
2546
|
-
| Before | After |
|
|
2547
|
-
| -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
2548
|
-
| `uses: thekevinscott/putitoutthere/.github/workflows/release.yml@v0` | `uses: thekevinscott/putitoutthere/.github/workflows/release.yml@v0`<br>`secrets:`<br>` NPM_TOKEN: ${{ secrets.NPM_TOKEN }}` |
|
|
2549
|
-
|
|
2550
|
-
The repo-level secret holding the npm automation token can be
|
|
2551
|
-
named anything; the *workflow* secret it gets passed as must be
|
|
2552
|
-
`NPM_TOKEN` exactly — the reusable workflow keys on that name.
|
|
2553
|
-
Drop the `secrets:` block from the caller's `release.yml` once
|
|
2554
|
-
Trusted Publishing is registered against the (now-existing)
|
|
2555
|
-
package; subsequent publishes are zero-secret. For bundled-cli
|
|
2556
|
-
/ napi families each per-platform sub-package needs its own
|
|
2557
|
-
Trusted Publisher registration after the first publish — the
|
|
2558
|
-
secret-bypass is a one-time bootstrap, not a permanent path.
|
|
2559
|
-
|
|
2560
|
-
**Deprecations removed.** None.
|
|
2561
|
-
|
|
2562
|
-
**Behavior changes without code changes.** None when the secret
|
|
2563
|
-
is unset (OIDC path unchanged). When the secret is set, a new
|
|
2564
|
-
"Export NODE_AUTH_TOKEN (caller-provided)" step writes
|
|
2565
|
-
`NODE_AUTH_TOKEN` to `$GITHUB_ENV` gated on the secret being
|
|
2566
|
-
non-empty AND the planned matrix containing an npm row. The
|
|
2567
|
-
gate reads the secret through a job-level `CALLER_NPM_TOKEN`
|
|
2568
|
-
env var because GitHub Actions does not allow the `secrets`
|
|
2569
|
-
context inside step-level `if:` conditions ([context
|
|
2570
|
-
availability](https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability));
|
|
2571
|
-
this is an internal mechanism — consumers don't see or set
|
|
2572
|
-
`CALLER_NPM_TOKEN` themselves. Unlike #283 (crates), there is
|
|
2573
|
-
no separate OIDC step to "skip" — the npm CLI handles OIDC
|
|
2574
|
-
internally via the runner's id-token, and the presence of
|
|
2575
|
-
`NODE_AUTH_TOKEN` in the env is what switches the CLI's auth
|
|
2576
|
-
mode.
|
|
2577
|
-
|
|
2578
|
-
**Verification.** Wire `NPM_TOKEN` to a valid npm automation
|
|
2579
|
-
token in the caller repo and trigger a release of a brand-new
|
|
2580
|
-
package. The publish-job logs should show "Export
|
|
2581
|
-
NODE_AUTH_TOKEN (caller-provided)" as `success`; `npm publish`
|
|
2582
|
-
authenticates with the long-lived token rather than via OIDC,
|
|
2583
|
-
and every per-platform sub-package in a bundled-cli / napi
|
|
2584
|
-
family lands on the registry in a single run. Once first
|
|
2585
|
-
publish succeeds, register Trusted Publishers against each
|
|
2586
|
-
package URL, drop the `secrets:` block, and re-run a release
|
|
2587
|
-
— the OIDC path covers the steady state from there.
|
|
2588
|
-
|
|
2589
|
-
Verified end-to-end against existing seeded fixtures and a
|
|
2590
|
-
real first-publish on a canary repo. The Verdaccio
|
|
2591
|
-
first-publish fixture coverage for the same path is tracked
|
|
2592
|
-
separately at #293; until that lands this fallback is verified
|
|
2593
|
-
by composition (mirror of #283) plus consumer-side observation
|
|
2594
|
-
on real first publishes rather than by an automated
|
|
2595
|
-
fresh-state fixture in this repo's CI.
|
|
2596
|
-
|
|
2597
|
-
---
|
|
2598
|
-
|
|
2599
|
-
### Crates token fallback
|
|
2600
|
-
|
|
2601
|
-
**Summary.** The reusable workflow now accepts an optional
|
|
2602
|
-
`CARGO_REGISTRY_TOKEN` via `secrets:`. Trusted Publishing on
|
|
2603
|
-
crates.io binds to an *already-published* crate, so the very
|
|
2604
|
-
first publish of a brand-new crate has no OIDC path available;
|
|
2605
|
-
without this fallback consumers had to either fork the workflow
|
|
2606
|
-
or publish once outside it. OIDC trusted publishers remain the
|
|
2607
|
-
default and recommended path — when the secret is unset,
|
|
2608
|
-
behavior is byte-for-byte unchanged. When the secret is set,
|
|
2609
|
-
the `rust-lang/crates-io-auth-action` OIDC exchange is skipped
|
|
2610
|
-
and the caller-provided token is exported to `$GITHUB_ENV` as
|
|
2611
|
-
`CARGO_REGISTRY_TOKEN` for the engine's crates handler to read.
|
|
2612
|
-
The header comment in `.github/workflows/release.yml` has been
|
|
2613
|
-
softened to match: previously *"Long-lived registry tokens are
|
|
2614
|
-
explicitly NOT supported via this workflow"*; now OIDC is
|
|
2615
|
-
described as the default with the token fallback called out for
|
|
2616
|
-
first-publish bootstrap. #283.
|
|
2617
|
-
|
|
2618
|
-
**Required changes.** None for consumers already on the OIDC
|
|
2619
|
-
path. To bootstrap a brand-new crate or to use the workflow on
|
|
2620
|
-
an account where Trusted Publishing isn't available, wire the
|
|
2621
|
-
secret in the caller's `release.yml`:
|
|
2622
|
-
|
|
2623
|
-
| Before | After |
|
|
2624
|
-
| -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
2625
|
-
| `uses: thekevinscott/putitoutthere/.github/workflows/release.yml@v0` | `uses: thekevinscott/putitoutthere/.github/workflows/release.yml@v0`<br>`secrets:`<br>` CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}` |
|
|
2626
|
-
|
|
2627
|
-
The repo-level secret holding the crates.io API token can be
|
|
2628
|
-
named anything; the *workflow* secret it gets passed as must
|
|
2629
|
-
be `CARGO_REGISTRY_TOKEN` exactly — the reusable workflow keys
|
|
2630
|
-
on that name. Drop the `secrets:` block from the caller's
|
|
2631
|
-
`release.yml` once Trusted Publishing is registered against
|
|
2632
|
-
the now-existing crate; subsequent publishes are zero-secret.
|
|
2633
|
-
|
|
2634
|
-
**Deprecations removed.** None.
|
|
2635
|
-
|
|
2636
|
-
**Behavior changes without code changes.** None when the secret
|
|
2637
|
-
is unset (OIDC path unchanged). When the secret is set, the
|
|
2638
|
-
publish job's "Authenticate with crates.io (OIDC)" step is
|
|
2639
|
-
conditionally skipped and a new "Export CARGO_REGISTRY_TOKEN
|
|
2640
|
-
(caller-provided)" step writes the secret to `$GITHUB_ENV`
|
|
2641
|
-
gated on the same condition. The gate reads the secret through a
|
|
2642
|
-
job-level `CALLER_CARGO_REGISTRY_TOKEN` env var because GitHub
|
|
2643
|
-
Actions does not allow the `secrets` context inside step-level
|
|
2644
|
-
`if:` conditions ([context availability](https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability));
|
|
2645
|
-
this is an internal mechanism — consumers don't see or set
|
|
2646
|
-
`CALLER_CARGO_REGISTRY_TOKEN` themselves.
|
|
2647
|
-
|
|
2648
|
-
**Verification.** Wire `CARGO_REGISTRY_TOKEN` to a valid
|
|
2649
|
-
crates.io API token in the caller repo and trigger a release.
|
|
2650
|
-
The publish-job logs should show "Authenticate with crates.io
|
|
2651
|
-
(OIDC)" as `skipped`, "Export CARGO_REGISTRY_TOKEN (OIDC)" as
|
|
2652
|
-
`skipped`, and "Export CARGO_REGISTRY_TOKEN (caller-provided)"
|
|
2653
|
-
as `success`. The crate publishes; the only difference visible
|
|
2654
|
-
in the registry is the publish was authorised against the
|
|
2655
|
-
caller-provided token rather than an OIDC-minted ephemeral one.
|
|
2656
|
-
|
|
2657
|
-
---
|
|
2658
|
-
|
|
2659
|
-
### npm package.json must declare `repository`
|
|
2660
|
-
|
|
2661
|
-
**Summary.** Every cascaded `kind = "npm"` package's `package.json`
|
|
2662
|
-
must now carry a non-empty `repository` field. The new preflight
|
|
2663
|
-
check (`requireProvenanceMetadata` in `src/preflight.ts`) runs in
|
|
2664
|
-
`src/publish.ts` immediately after `requireAuth` and rejects the
|
|
2665
|
-
run with `PIOT_NPM_MISSING_REPOSITORY` before any runner work.
|
|
2666
|
-
Why: `putitoutthere` invokes `npm publish --provenance` on the OIDC
|
|
2667
|
-
trusted-publisher path; the npm CLI hard-requires this field so the
|
|
2668
|
-
registry can verify the artifact was built from the repo the trusted
|
|
2669
|
-
publisher declares. A missing or empty field previously surfaced
|
|
2670
|
-
only after the runner had spun up, OIDC had been negotiated, and
|
|
2671
|
-
the artifact had been built — wasting a full release run on a
|
|
2672
|
-
precondition checkable in milliseconds. Hit in the wild on
|
|
2673
|
-
`coaxer@0.1.1`'s first npm release; tracked in #280.
|
|
2674
|
-
|
|
2675
|
-
The npm handler's inline backstop (`assertRepositoryField` in
|
|
2676
|
-
`src/handlers/npm.ts`) is also tightened. Previously it used
|
|
2677
|
-
`if (!pkg.repository)` — falsy-only — which let three real shapes
|
|
2678
|
-
slip through: an object without `url` (`{ type: 'git' }`), an
|
|
2679
|
-
empty object (`{}`), and a whitespace-only string (`' '`). All
|
|
2680
|
-
three are now rejected; the error message also carries the stable
|
|
2681
|
-
`PIOT_NPM_MISSING_REPOSITORY` code and the path of the offending
|
|
2682
|
-
file, matching the preflight error.
|
|
2683
|
-
|
|
2684
|
-
**Required changes.** Add a `repository` block to every
|
|
2685
|
-
`package.json` declared as `kind = "npm"` in
|
|
2686
|
-
`putitoutthere.toml`. Canonical shape:
|
|
2687
|
-
|
|
2688
|
-
| Before | After |
|
|
2689
|
-
| --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
2690
|
-
| `{ "name": "@scope/lib", "version": "0.1.0" }` | `{ "name": "@scope/lib", "version": "0.1.0", "repository": { "type": "git", "url": "git+https://github.com/<owner>/<repo>.git", "directory": "<path/to/package>" } }` |
|
|
2691
|
-
|
|
2692
|
-
`directory` is needed for monorepo packages so npm can locate the
|
|
2693
|
-
source within the repo. Both the object form and the legacy single-
|
|
2694
|
-
string form (`"repository": "git+https://github.com/<owner>/<repo>.git"`)
|
|
2695
|
-
are accepted; only a missing field, an empty string, or an object
|
|
2696
|
-
without a non-empty `url` fails the check.
|
|
2697
|
-
|
|
2698
|
-
No `release.yml` or `putitoutthere.toml` changes are required.
|
|
2699
|
-
|
|
2700
|
-
**Deprecations removed.** None.
|
|
2701
|
-
|
|
2702
|
-
**Behavior changes without code changes.** `putitoutthere publish`
|
|
2703
|
-
now fails fast at preflight with
|
|
2704
|
-
`[PIOT_NPM_MISSING_REPOSITORY] npm publish requires a non-empty
|
|
2705
|
-
\`repository\` field in package.json` for any cascaded npm package
|
|
2706
|
-
whose `package.json` lacks the field. Previously the same shape
|
|
2707
|
-
ran through plan + build + auth-negotiation + artifact-staging
|
|
2708
|
-
and failed inside `npm publish` with `npm publish --provenance
|
|
2709
|
-
requires a \`repository\` field in package.json`. Well-formed
|
|
2710
|
-
packages are unaffected.
|
|
2711
|
-
|
|
2712
|
-
The error message names every failing package and its
|
|
2713
|
-
`package.json` path so consumers fix all of them in one round-trip
|
|
2714
|
-
rather than discovering them one at a time across multiple release
|
|
2715
|
-
attempts.
|
|
2716
|
-
|
|
2717
|
-
**Verification.** Drop the `repository` field from one of your
|
|
2718
|
-
`package.json` files locally and run `pnpm test:integration` (or
|
|
2719
|
-
the engine against a local fixture); the failure should arrive at
|
|
2720
|
-
preflight with `PIOT_NPM_MISSING_REPOSITORY` in the message, no
|
|
2721
|
-
platform packages should be published, and no git tag should be
|
|
2722
|
-
created. Restore the field; the next run completes normally.
|
|
2723
|
-
|
|
2724
|
-
---
|
|
2725
|
-
|
|
2726
|
-
### pypi/maturin version bump at build
|
|
2727
|
-
|
|
2728
|
-
**Summary.** `pypi` packages built with `build = "maturin"` now ship
|
|
2729
|
-
wheels at the planned version, not at whatever literal happened to be
|
|
2730
|
-
in `pyproject.toml` on the build runner. The reusable workflow's
|
|
2731
|
-
build matrix (`_matrix.yml`) bumps the version source on disk to
|
|
2732
|
-
`matrix.version` immediately before each `PyO3/maturin-action@v1`
|
|
2733
|
-
step. Two version-source shapes are supported:
|
|
2734
|
-
|
|
2735
|
-
- Static literal: `[project].version = "0.1.0"` in `pyproject.toml`
|
|
2736
|
-
is rewritten in place. This is maturin's default project shape and
|
|
2737
|
-
the case that was previously broken for every consumer.
|
|
2738
|
-
- Dynamic: `pyproject.toml` declaring `[project].dynamic = ["version"]`
|
|
2739
|
-
with the version sourced from a sibling `Cargo.toml`'s
|
|
2740
|
-
`[package].version` — that's where the bump lands instead.
|
|
2741
|
-
|
|
2742
|
-
Why it matters: maturin reads its version source from disk at build
|
|
2743
|
-
time and honors no env override. Other build paths bumped elsewhere
|
|
2744
|
-
in the contract — crates and npm at publish (`writeVersion` rewrites
|
|
2745
|
-
the manifest before `cargo publish` / `npm publish` reads it),
|
|
2746
|
-
setuptools-scm / hatch-vcs through `SETUPTOOLS_SCM_PRETEND_VERSION`
|
|
2747
|
-
in the build step. Maturin had no equivalent, so wheels left the
|
|
2748
|
-
build runner pre-versioned at the consumer's stale literal. PyPI
|
|
2749
|
-
rejected the upload with `400 File already exists` whenever that
|
|
2750
|
-
literal had been previously registered, even when crates and npm
|
|
2751
|
-
shipped correctly. Hit in the wild on `dirsql`'s 0.2.8 release;
|
|
2752
|
-
issue #276.
|
|
2753
|
-
|
|
2754
|
-
**Required changes.** None. The bump is internal to the reusable
|
|
2755
|
-
workflow. Consumers pinning `release.yml@v0` and `build.yml@v0`
|
|
2756
|
-
inherit the fix on the next workflow run with no `release.yml` edits
|
|
2757
|
-
and no `putitoutthere.toml` edits. The fix applies equally to the
|
|
2758
|
-
static-literal and dynamic-version shapes; consumers don't need to
|
|
2759
|
-
restructure their pyproject to opt in.
|
|
2760
|
-
|
|
2761
|
-
The CLI gains a new internal subcommand,
|
|
2762
|
-
`putitoutthere write-version --path <pkg-dir> --version <v>`, that
|
|
2763
|
-
implements the bump dispatch. `action.yml` gains a new optional
|
|
2764
|
-
`version:` input that the reusable workflow forwards. Both surfaces
|
|
2765
|
-
are internal seams powering `_matrix.yml`; consumers compose with
|
|
2766
|
-
the reusable workflow, not directly with the CLI or the JS action
|
|
2767
|
-
(see [`notes/design-commitments.md`](./notes/design-commitments.md)
|
|
2768
|
-
non-goal #7 and #10).
|
|
2769
|
-
|
|
2770
|
-
**Deprecations removed.** None.
|
|
2771
|
-
|
|
2772
|
-
**Behavior changes without code changes.** A maturin build run
|
|
2773
|
-
through the reusable workflow now mutates the build runner's
|
|
2774
|
-
`pyproject.toml` (or `Cargo.toml` for dynamic-version projects) in
|
|
2775
|
-
place, bumping the version literal to `matrix.version`. The mutation
|
|
2776
|
-
lives only on the build runner — the consumer's source tree is
|
|
2777
|
-
untouched. Anyone fingerprinting on the on-disk manifest version
|
|
2778
|
-
during a build (e.g. a custom shell step running between
|
|
2779
|
-
`uses: thekevinscott/putitoutthere/.github/workflows/release.yml@v0`
|
|
2780
|
-
and a follow-up step that reads the manifest) will now see the
|
|
2781
|
-
planned version where they previously saw the consumer's stale
|
|
2782
|
-
literal. Custom build steps that grep on a specific literal version
|
|
2783
|
-
need to grep on `matrix.version` instead.
|
|
2784
|
-
|
|
2785
|
-
`pyproject.toml` projects whose `[project]` table is malformed
|
|
2786
|
-
(neither a static `version = "..."` line nor `dynamic = ["version"]`)
|
|
2787
|
-
now fail the build matrix with a clear error. Previously the same
|
|
2788
|
-
shape produced wheels at whatever fallback the build backend chose,
|
|
2789
|
-
which silently disagreed with the plan.
|
|
2790
|
-
|
|
2791
|
-
**Verification.** Cut a release on a maturin package whose
|
|
2792
|
-
`pyproject.toml` carries a stale literal (e.g. `version = "0.1.0"`
|
|
2793
|
-
when the planned version is `0.2.8`). The reusable workflow's
|
|
2794
|
-
build matrix logs a `write-version: ... → 0.2.8` line before each
|
|
2795
|
-
maturin invocation, and the produced wheel's `METADATA: Version:`
|
|
2796
|
-
matches the planned version:
|
|
2797
|
-
|
|
2798
|
-
```sh
|
|
2799
|
-
unzip -p dist/*.whl '*.dist-info/METADATA' | grep '^Version:'
|
|
2800
|
-
# Version: 0.2.8
|
|
2801
|
-
```
|
|
2802
|
-
|
|
2803
|
-
PyPI accepts the upload — assuming the planned version itself isn't
|
|
2804
|
-
a duplicate of a previously-registered file.
|
|
2805
|
-
|
|
2806
|
-
### New `build.yml` reusable workflow for PR-time build verification
|
|
2807
|
-
|
|
2808
|
-
**Summary.** A second consumer-facing reusable workflow,
|
|
2809
|
-
`.github/workflows/build.yml`, runs the same plan + build matrix that
|
|
2810
|
-
`release.yml` runs but stops there — no publish job, no
|
|
2811
|
-
`id-token: write`, no OIDC exchange, no registry auth. Both workflows
|
|
2812
|
-
delegate the matrix to a shared internal `_matrix.yml`, so action
|
|
2813
|
-
pins (`actions/checkout@v6`, `PyO3/maturin-action@v1`, etc.), per-row
|
|
2814
|
-
build steps, and runner selection cannot drift between PR-time
|
|
2815
|
-
verification and release-time publishing. The structural guarantee —
|
|
2816
|
-
publish-capable bytes do not exist on the build-check code path — is
|
|
2817
|
-
what makes this safe to run on untrusted PRs; a `dry_run: true` input
|
|
2818
|
-
on `release.yml` would have made it a runtime guarantee subject to
|
|
2819
|
-
GHA's expression evaluation quirks and any future `if:` bug. An
|
|
2820
|
-
`actionlint`-job grep assertion rejects any patch that adds
|
|
2821
|
-
`id-token: write` to `build.yml` or `_matrix.yml`.
|
|
2822
|
-
|
|
2823
|
-
The new workflow runs at the same `@v0` floating tag as
|
|
2824
|
-
`release.yml`, pinned the same way the engine action already is —
|
|
2825
|
-
floating lightweight tag, no annotated-tag pitfall ([github/community
|
|
2826
|
-
discussion #48693](https://github.com/orgs/community/discussions/48693)).
|
|
2827
|
-
Pinning per-release annotated tags
|
|
2828
|
-
(e.g. `putitoutthere-v0.4.2`) is unsupported for the same reason it has
|
|
2829
|
-
always been unsupported for `release.yml`; the `@v0` form is canonical.
|
|
2830
|
-
|
|
2831
|
-
**Required changes.** None for existing consumers. `release.yml` is
|
|
2832
|
-
unchanged in its public surface — same inputs, same `has_pypi`
|
|
2833
|
-
output, same concurrency group, same publish behavior. Internally
|
|
2834
|
-
its `plan` and `build` jobs moved into `_matrix.yml`, but that file
|
|
2835
|
-
is internal (the leading underscore + the absence of consumer-facing
|
|
2836
|
-
docs); pinning `release.yml@v0` keeps working byte-for-byte.
|
|
2837
|
-
|
|
2838
|
-
To opt into PR-time build verification, add a new workflow to your
|
|
2839
|
-
repo:
|
|
2840
|
-
|
|
2841
|
-
```yaml
|
|
2842
|
-
# .github/workflows/build-check.yml
|
|
2843
|
-
name: Build check
|
|
2844
|
-
on:
|
|
2845
|
-
pull_request: {}
|
|
2846
|
-
jobs:
|
|
2847
|
-
build-check:
|
|
2848
|
-
uses: thekevinscott/putitoutthere/.github/workflows/build.yml@v0
|
|
2849
|
-
```
|
|
2850
|
-
|
|
2851
|
-
No new permissions, no new inputs to set. `node_version` and
|
|
2852
|
-
`python_version` accept the same defaults / overrides as
|
|
2853
|
-
`release.yml`. PRs that break a target-specific build (a wheel that
|
|
2854
|
-
fails on `aarch64-apple-darwin`, an npm postinstall that fails on
|
|
2855
|
-
Windows) now surface in review.
|
|
2856
|
-
|
|
2857
|
-
**Deprecations removed.** None.
|
|
2858
|
-
|
|
2859
|
-
**Behavior changes without code changes.** `release.yml`'s internal
|
|
2860
|
-
job graph collapsed from `plan → build → publish` to
|
|
2861
|
-
`build (uses: _matrix.yml) → publish`. The `publish` job now reads
|
|
2862
|
-
`needs.build.outputs.matrix` instead of `needs.plan.outputs.matrix`;
|
|
2863
|
-
the matrix payload is unchanged. Run logs show one nested-workflow
|
|
2864
|
-
group (`build / plan`, `build / build`) instead of two top-level
|
|
2865
|
-
jobs; existing log scrapers that fingerprint on the job name `plan`
|
|
2866
|
-
need to fingerprint on `build / plan` instead.
|
|
2867
|
-
|
|
2868
|
-
**Verification.** On a repo that adds the build-check workflow, open
|
|
2869
|
-
a PR that touches a `[[package]].globs` glob. The PR's Checks tab
|
|
2870
|
-
shows a `build-check / build / build` matrix run. The job has no
|
|
2871
|
-
`id-token: write` permission (visible in the `Show all checks` tag
|
|
2872
|
-
on the run), and there is no `publish` job in the run's job graph.
|
|
2873
|
-
On `main`, the existing release flow is unaffected — a tag push +
|
|
2874
|
-
GitHub Release on the next workflow run.
|
|
2875
|
-
|
|
2876
|
-
### Crates dirty-check whitelists sibling package paths
|
|
2877
|
-
|
|
2878
|
-
**Summary.** The engine's pre-publish dirty-workspace check
|
|
2879
|
-
(`scanDirtyOutsideManifest` in `src/handlers/crates.ts`) used to
|
|
2880
|
-
flag any dirty file in the repo outside the package's own
|
|
2881
|
-
`Cargo.toml`. For polyglot consumers (rust + js in one repo), the
|
|
2882
|
-
reusable workflow's `Build npm packages` step (added in #256) runs
|
|
2883
|
-
`npm install + npm run build` for each npm package in the plan
|
|
2884
|
-
before the engine publishes anything. That creates `node_modules/`,
|
|
2885
|
-
`package-lock.json`, and `dist/` inside each npm package's path as
|
|
2886
|
-
untracked files. cargo's git-status check sees them and the engine
|
|
2887
|
-
refuses with `cargo publish: refusing to proceed; unexpected dirty
|
|
2888
|
-
files in the working tree outside <crate>/Cargo.toml`.
|
|
2889
|
-
|
|
2890
|
-
The check now whitelists every other configured package's path
|
|
2891
|
-
(`siblingPackagePaths` in `Ctx`), the same way it already
|
|
2892
|
-
whitelists the reusable workflow's `artifacts/` scratch dir. cargo
|
|
2893
|
-
only packs files inside its own package directory, so dirty state
|
|
2894
|
-
in sibling packages can't end up in the crate tarball regardless.
|
|
2895
|
-
Stray edits elsewhere in the repo (a `README.md` change, an
|
|
2896
|
-
unrelated source file mod) still fail the check.
|
|
2897
|
-
|
|
2898
|
-
**Required changes.** None for consumers calling the reusable
|
|
2899
|
-
workflow at `thekevinscott/putitoutthere/.github/workflows/release.yml@v0`.
|
|
2900
|
-
This is a pure relaxation: setups that previously published cleanly
|
|
2901
|
-
continue to; setups that hit the false-positive failure now succeed.
|
|
2902
|
-
|
|
2903
|
-
**Deprecations removed.** None.
|
|
2904
|
-
|
|
2905
|
-
**Behavior changes without code changes.** A polyglot release run
|
|
2906
|
-
that previously failed with the "unexpected dirty files" message
|
|
2907
|
-
on `node_modules/` / `package-lock.json` / `dist/` in a sibling npm
|
|
2908
|
-
package now proceeds. The published crate tarball is unchanged
|
|
2909
|
-
(cargo always scoped its packing to the crate dir).
|
|
2910
|
-
|
|
2911
|
-
**Verification.** A polyglot repo with rust + js packages and a
|
|
2912
|
-
crates row in the matrix now reaches `cargo publish` instead of
|
|
2913
|
-
the dirty-check error. After a release, the crate tarball still
|
|
2914
|
-
contains only files inside the crate's own dir:
|
|
2915
|
-
|
|
2916
|
-
```sh
|
|
2917
|
-
cargo package --list --manifest-path <crate>/Cargo.toml
|
|
2918
|
-
```
|
|
2919
|
-
|
|
2920
|
-
### `publish` throws on empty matrix
|
|
2921
|
-
|
|
2922
|
-
**Summary.** `putitoutthere publish` previously logged
|
|
2923
|
-
`publish: plan is empty; nothing to release` at info level and exited
|
|
2924
|
-
0 when the matrix had no rows. The reusable workflow's `publish` step
|
|
2925
|
-
went green on those runs even though nothing reached a registry —
|
|
2926
|
-
visually indistinguishable from a successful release. The engine now
|
|
2927
|
-
throws with code `PIOT_PUBLISH_EMPTY_PLAN`, the publish step exits
|
|
2928
|
-
non-zero, and the run goes red. Skips remain a workflow-gate concern:
|
|
2929
|
-
the canonical `release.yml` already has `if: …matrix output non-empty
|
|
2930
|
-
…` on its publish job, so a `release: skip` trailer (or any other
|
|
2931
|
-
empty-plan reason) skips the publish job rather than running it to a
|
|
2932
|
-
no-op.
|
|
2933
|
-
|
|
2934
|
-
**Required changes.** None for consumers calling the reusable
|
|
2935
|
-
workflow at `thekevinscott/putitoutthere/.github/workflows/release.yml@v0`.
|
|
2936
|
-
The reusable workflow's existing `if:` on the publish job already
|
|
2937
|
-
gates correctly. Hand-rolled workflows that invoked the CLI's
|
|
2938
|
-
`publish` directly without a plan-output gate will now see a non-zero
|
|
2939
|
-
exit on empty plans; add a gate or stop calling publish on commits
|
|
2940
|
-
that don't produce work.
|
|
2941
|
-
|
|
2942
|
-
**Deprecations removed.** None.
|
|
2943
|
-
|
|
2944
|
-
**Behavior changes without code changes.** A release run that
|
|
2945
|
-
reached the publish step with an empty plan used to log
|
|
2946
|
-
`published: (nothing)` and exit 0; it now logs `[PIOT_PUBLISH_EMPTY_PLAN]
|
|
2947
|
-
publish was invoked but the plan is empty…` to stderr and exits 1.
|
|
2948
|
-
For repos whose release runs were silently no-op-ing (the dogfood
|
|
2949
|
-
incident's failure mode), this surfaces the gap.
|
|
2950
|
-
|
|
2951
|
-
**Verification.** Trigger a release run that would produce an empty
|
|
2952
|
-
plan (e.g. a commit that doesn't touch any package's `globs`) and
|
|
2953
|
-
either bypass the workflow gate or invoke the CLI directly. Expect
|
|
2954
|
-
exit 1, with `PIOT_PUBLISH_EMPTY_PLAN` in stderr. A healthy release
|
|
2955
|
-
where the plan job's matrix is non-empty is unaffected.
|
|
2956
|
-
|
|
2957
|
-
### npm `build` accepts array of entries
|
|
2958
|
-
|
|
2959
|
-
**Summary.** `kind = "npm"` packages can now declare `build` as an array
|
|
2960
|
-
of entries to publish multiple per-platform package families from a
|
|
2961
|
-
single main package — for example, a napi-rs Node addon plus a CLI
|
|
2962
|
-
binary, both selected via `optionalDependencies` on a shared top-level
|
|
2963
|
-
package. Each entry has a `mode` (`napi` / `bundled-cli`) and an
|
|
2964
|
-
optional `name` template (e.g. `"@scope/lib-{triple}"`) that the
|
|
2965
|
-
consumer fully controls. The previous single-mode string form is
|
|
2966
|
-
preserved.
|
|
2967
|
-
|
|
2968
|
-
**Required changes.** None. `build = "napi"` and `build = "bundled-cli"`
|
|
2969
|
-
keep producing the same per-platform package names, the same artifact
|
|
2970
|
-
directory layout, and the same matrix shape they did before. Adopt the
|
|
2971
|
-
array form only if you need a multi-family npm package.
|
|
2972
|
-
|
|
2973
|
-
| Field | Before | After |
|
|
2974
|
-
|---|---|---|
|
|
2975
|
-
| `build` (single mode) | `build = "napi"` | unchanged — `build = "napi"` still valid |
|
|
2976
|
-
| `build` (single mode, array form) | _new_ | `build = ["napi"]` — equivalent to the string form |
|
|
2977
|
-
| `build` (single mode, custom name) | _new_ | `build = [{ mode = "napi", name = "@scope/lib-{triple}" }]` |
|
|
2978
|
-
| `build` (multi mode) | _new_ | `build = [{ mode = "napi", name = "@scope/lib-{triple}" }, { mode = "bundled-cli", name = "@scope/cli-{triple}" }]` |
|
|
2979
|
-
|
|
2980
|
-
Variables in `name` templates: `{name}`, `{scope}`, `{base}`,
|
|
2981
|
-
`{triple}`, `{mode}`. `{triple}` is required in every template.
|
|
2982
|
-
`{version}` is not surfaced — platform package names are immutable
|
|
2983
|
-
identifiers; the version is pinned via `optionalDependencies`.
|
|
2984
|
-
|
|
2985
|
-
**Validation rules** enforced at config load:
|
|
2986
|
-
|
|
2987
|
-
- Each `mode` value (`napi`, `bundled-cli`) appears at most once per package.
|
|
2988
|
-
- Every `name` template must contain `{triple}`.
|
|
2989
|
-
- Unknown placeholders are rejected.
|
|
2990
|
-
- Templates across entries must be pairwise distinct (collision-free).
|
|
2991
|
-
|
|
2992
|
-
**Multi-mode artifact layout.** When `build` has more than one entry,
|
|
2993
|
-
the build-side artifact directory and path get a mode infix to keep
|
|
2994
|
-
families separate:
|
|
2995
|
-
|
|
2996
|
-
```
|
|
2997
|
-
artifacts/
|
|
2998
|
-
my-cli-napi-linux-x64-gnu/ # napi family
|
|
2999
|
-
my-cli-bundled-cli-linux-x64-gnu/ # bundled-cli family
|
|
3000
|
-
```
|
|
3001
|
-
|
|
3002
|
-
The build job for a multi-mode row writes to
|
|
3003
|
-
`<pkg.path>/build/<mode>-<triple>/`. Single-mode (string form or
|
|
3004
|
-
length-1 array) still uses `<pkg.path>/build/<triple>/` —
|
|
3005
|
-
byte-for-byte unchanged.
|
|
3006
|
-
|
|
3007
|
-
**Trusted-publisher registrations.** Each platform package across
|
|
3008
|
-
*every* family needs its own npm trusted-publisher registration. A
|
|
3009
|
-
multi-mode package with N families × M targets needs N×M registrations
|
|
3010
|
-
plus one for the top-level. There's no shorthand on npm's side; this
|
|
3011
|
-
is the cost of the dual-family install pattern.
|
|
3012
|
-
|
|
3013
|
-
**Deprecations removed.** None.
|
|
3014
|
-
|
|
3015
|
-
**Behavior changes without code changes.** None for single-mode
|
|
3016
|
-
configs. Multi-mode is new surface — no prior behavior to compare
|
|
3017
|
-
against.
|
|
3018
|
-
|
|
3019
|
-
**Verification.** For an existing single-mode config, `putitoutthere
|
|
3020
|
-
plan` should emit identical matrix rows before and after the upgrade
|
|
3021
|
-
(same `artifact_name`, same `artifact_path`, same `target`). For a
|
|
3022
|
-
new multi-mode config, you should see one matrix row per `(mode,
|
|
3023
|
-
triple)` plus a single `target = "main"` row, and the matrix
|
|
3024
|
-
`artifact_name` should carry the mode infix
|
|
3025
|
-
(`<name>-<mode>-<triple>`).
|
|
3026
|
-
|
|
3027
|
-
---
|
|
3028
|
-
|
|
3029
|
-
## v0.1.51 → v0.2.0
|
|
3030
|
-
|
|
3031
|
-
### Publish job rebuilds npm packages from source
|
|
3032
|
-
|
|
3033
|
-
**Summary.** Vanilla npm packages were publishing with their compiled
|
|
3034
|
-
output (`dist/`, `lib/`, etc.) missing from the tarball. The plan
|
|
3035
|
-
emitted `artifact_path: package.json` for noarch npm rows, so the
|
|
3036
|
-
build job's compile output was never uploaded — and the publish job's
|
|
3037
|
-
fresh checkout had no compiled files. `npm publish` doesn't validate
|
|
3038
|
-
`files` content, so the broken artifact reached the registry. Caught
|
|
3039
|
-
in the wild on a downstream consumer. The publish job now installs
|
|
3040
|
-
deps and runs `npm run build --if-present` per npm package path
|
|
3041
|
-
before invoking the engine — the same logic the build job already
|
|
3042
|
-
runs, just at the point where it actually matters.
|
|
3043
|
-
|
|
3044
|
-
**Required changes.** None for consumers calling the reusable
|
|
3045
|
-
workflow at `thekevinscott/putitoutthere/.github/workflows/release.yml@v0`.
|
|
3046
|
-
The fix is internal to the reusable workflow.
|
|
3047
|
-
|
|
3048
|
-
**Deprecations removed.** None.
|
|
3049
|
-
|
|
3050
|
-
**Behavior changes without code changes.** The publish job now spends
|
|
3051
|
-
additional time on `npm install` + `npm run build` for each npm
|
|
3052
|
-
package in the plan. For repos whose package.json had no `build`
|
|
3053
|
-
script, behavior is unchanged (`--if-present` skips). For repos that
|
|
3054
|
-
did declare a build script, the published tarball now contains
|
|
3055
|
-
whatever the build emits — which may be the first time the registry
|
|
3056
|
-
artifact actually matches what the package author intended. If your
|
|
3057
|
-
prior releases were unknowingly broken (compiled output missing), the
|
|
3058
|
-
next release will fix them; verify by inspecting the next published
|
|
3059
|
-
tarball with `npm view <pkg>@<ver>` + `npm pack <pkg>@<ver>`.
|
|
3060
|
-
|
|
3061
|
-
**Verification.** After upgrading, a release run logs an `npm
|
|
3062
|
-
install + build at <path>` group per npm package in the plan. The
|
|
3063
|
-
published tarball contains every directory listed in package.json
|
|
3064
|
-
`files[]`:
|
|
3065
|
-
|
|
3066
|
-
```sh
|
|
3067
|
-
npm pack <pkg>@<new-version> --dry-run 2>&1 | grep -E '(dist|lib|build)/'
|
|
3068
|
-
```
|
|
3069
|
-
|
|
3070
|
-
### Reusable workflow + `action.yml` move to Node 24 actions
|
|
3071
|
-
|
|
3072
|
-
**Summary.** GitHub deprecated Node 20 actions in September 2025; the
|
|
3073
|
-
hosted runner forces Node 24 starting June 2, 2026 and removes Node 20
|
|
3074
|
-
entirely on September 16, 2026.
|
|
3075
|
-
Every workflow run that called `putitoutthere` was emitting deprecation
|
|
3076
|
-
warnings — one per job inside the reusable workflow, plus a top-level
|
|
3077
|
-
`Actions running on Node.js 20` warning attributed to
|
|
3078
|
-
`thekevinscott/putitoutthere@v0` itself, which the consumer could not
|
|
3079
|
-
fix locally. The reusable workflow's pinned action majors and the JS
|
|
3080
|
-
action's `runs.using` now target Node 24-compatible versions.
|
|
3081
|
-
|
|
3082
|
-
| Action | Before | After |
|
|
3083
|
-
|---|---|---|
|
|
3084
|
-
| `actions/checkout` | `@v4` | `@v6` |
|
|
3085
|
-
| `actions/setup-node` | `@v4` | `@v6` |
|
|
3086
|
-
| `actions/setup-python` | `@v5` | `@v6` |
|
|
3087
|
-
| `actions/upload-artifact` | `@v4` | `@v7` |
|
|
3088
|
-
| `actions/download-artifact` | `@v4` | `@v8` |
|
|
3089
|
-
| `action.yml` `runs.using` | `node20` | `node24` |
|
|
3090
|
-
|
|
3091
|
-
**Required changes.** Consumers calling the reusable workflow at
|
|
3092
|
-
`thekevinscott/putitoutthere/.github/workflows/release.yml@v0` get the
|
|
3093
|
-
new pins automatically — no consumer-side YAML changes required. The
|
|
3094
|
-
caller-side `pypi-publish` job in the canonical template now uses
|
|
3095
|
-
`actions/download-artifact@v8`; existing copies still pinned at `@v4`
|
|
3096
|
-
keep working but should be bumped to silence the same deprecation
|
|
3097
|
-
warning in the consumer's own workflow file:
|
|
3098
|
-
|
|
3099
|
-
```diff
|
|
3100
|
-
pypi-publish:
|
|
3101
|
-
...
|
|
3102
|
-
steps:
|
|
3103
|
-
- - uses: actions/download-artifact@v4
|
|
3104
|
-
+ - uses: actions/download-artifact@v8
|
|
3105
|
-
with:
|
|
3106
|
-
pattern: '*-sdist'
|
|
3107
|
-
...
|
|
3108
|
-
- - uses: actions/download-artifact@v4
|
|
3109
|
-
+ - uses: actions/download-artifact@v8
|
|
3110
|
-
with:
|
|
3111
|
-
pattern: '*-wheel-*'
|
|
3112
|
-
...
|
|
3113
|
-
```
|
|
3114
|
-
|
|
3115
|
-
**Deprecations removed.** None.
|
|
3116
|
-
|
|
3117
|
-
**Behavior changes without code changes.** Reusable workflow jobs now
|
|
3118
|
-
run under Node 24 instead of Node 20. The artifact contract is
|
|
3119
|
-
unchanged — `download-artifact@v8` preserves the per-name subdirectory
|
|
3120
|
-
layout (`artifacts/<artifact-name>/<file>`) for downloads-by-name, and
|
|
3121
|
-
`upload-artifact@v7`'s default still produces zipped uploads keyed by
|
|
3122
|
-
the `name:` parameter. `download-artifact@v8` now fails on artifact
|
|
3123
|
-
hash mismatches by default (was a warning in `@v4`); this is an
|
|
3124
|
-
integrity check, not a behavior change for healthy uploads.
|
|
3125
|
-
|
|
3126
|
-
**Verification.** A consumer release run no longer emits the
|
|
3127
|
-
`Actions running on Node.js 20 ... thekevinscott/putitoutthere@v0`
|
|
3128
|
-
deprecation warning, nor the per-job warnings against `actions/checkout@v4`
|
|
3129
|
-
et al. Tag, GitHub Release, and registry uploads occur as before.
|
|
3130
|
-
|
|
3131
|
-
---
|
|
3132
|
-
|
|
3133
|
-
### PyPI uploads moved to caller-side job
|
|
3134
|
-
|
|
3135
|
-
**Summary.** PyPI's Trusted Publisher matching filters candidates by
|
|
3136
|
-
`repository_owner` + `repository_name` *before* checking
|
|
3137
|
-
`job_workflow_ref`
|
|
3138
|
-
([Warehouse implementation](https://github.com/pypi/warehouse/blob/main/warehouse/oidc/models/github.py)).
|
|
3139
|
-
The OIDC `repository` claim always reflects the caller's repo —
|
|
3140
|
-
including inside a reusable workflow — so a TP registered against
|
|
3141
|
-
the reusable workflow's repo is filtered out before workflow_ref
|
|
3142
|
-
is even checked. PyPI documents this as unsupported
|
|
3143
|
-
([troubleshooting](https://docs.pypi.org/trusted-publishers/troubleshooting/)).
|
|
3144
|
-
Tracked at [pypi/warehouse#11096](https://github.com/pypi/warehouse/issues/11096),
|
|
3145
|
-
no timeline.
|
|
3146
|
-
|
|
3147
|
-
To preserve OIDC trusted publishing for PyPI without setting
|
|
3148
|
-
`PYPI_API_TOKEN`, the upload step (`pypa/gh-action-pypi-publish`)
|
|
3149
|
-
now runs in the consumer's own workflow file as a second job,
|
|
3150
|
-
gated on the new `has_pypi` output. The engine still owns plan,
|
|
3151
|
-
build, version-rewrite, and git-tag creation for PyPI rows; only
|
|
3152
|
-
the actual upload moves. See
|
|
3153
|
-
[`notes/audits/2026-04-28-pypi-tp-reusable-workflow-constraint.md`](./notes/audits/2026-04-28-pypi-tp-reusable-workflow-constraint.md)
|
|
3154
|
-
for the full diagnosis.
|
|
3155
|
-
|
|
3156
|
-
**Required changes.** Update `.github/workflows/release.yml`:
|
|
3157
|
-
|
|
3158
|
-
Before (~12 lines):
|
|
3159
|
-
|
|
3160
|
-
```yaml
|
|
3161
|
-
name: Release
|
|
3162
|
-
on:
|
|
3163
|
-
push:
|
|
3164
|
-
branches: [main]
|
|
3165
|
-
jobs:
|
|
3166
|
-
release:
|
|
3167
|
-
uses: thekevinscott/putitoutthere/.github/workflows/release.yml@v0
|
|
3168
|
-
permissions:
|
|
3169
|
-
contents: write
|
|
3170
|
-
id-token: write
|
|
3171
|
-
```
|
|
3172
|
-
|
|
3173
|
-
After (~30 lines, single copy-paste from README → Quickstart):
|
|
3174
|
-
|
|
3175
|
-
```yaml
|
|
3176
|
-
name: Release
|
|
3177
|
-
on:
|
|
3178
|
-
push:
|
|
3179
|
-
branches: [main]
|
|
3180
|
-
jobs:
|
|
3181
|
-
release:
|
|
3182
|
-
uses: thekevinscott/putitoutthere/.github/workflows/release.yml@v0
|
|
3183
|
-
permissions:
|
|
3184
|
-
contents: write
|
|
3185
|
-
id-token: write
|
|
3186
|
-
|
|
3187
|
-
pypi-publish:
|
|
3188
|
-
needs: release
|
|
3189
|
-
if: needs.release.outputs.has_pypi == 'true'
|
|
3190
|
-
runs-on: ubuntu-latest
|
|
3191
|
-
permissions:
|
|
3192
|
-
id-token: write
|
|
3193
|
-
steps:
|
|
3194
|
-
- uses: actions/download-artifact@v8
|
|
3195
|
-
with:
|
|
3196
|
-
pattern: '*-sdist'
|
|
3197
|
-
path: dist/
|
|
3198
|
-
merge-multiple: true
|
|
3199
|
-
- uses: actions/download-artifact@v8
|
|
3200
|
-
with:
|
|
3201
|
-
pattern: '*-wheel-*'
|
|
3202
|
-
path: dist/
|
|
3203
|
-
merge-multiple: true
|
|
3204
|
-
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
3205
|
-
```
|
|
3206
|
-
|
|
3207
|
-
The `pypi-publish` job's `if:` gate skips it for non-PyPI repos —
|
|
3208
|
-
paste verbatim regardless of what you publish. Crates.io and npm
|
|
3209
|
-
are unaffected; their TP claim semantics work fine inside the
|
|
3210
|
-
reusable workflow.
|
|
3211
|
-
|
|
3212
|
-
**No PyPI TP re-registration required.** Your existing TP
|
|
3213
|
-
registration (against your repo, your `release.yml`, optional
|
|
3214
|
-
environment) was already correct for this pattern. If you'd
|
|
3215
|
-
attempted to register a TP against `thekevinscott/putitoutthere`
|
|
3216
|
-
to work around the prior failure, remove that entry — it would
|
|
3217
|
-
have never matched anyway.
|
|
3218
|
-
|
|
3219
|
-
**Deprecations removed.** None.
|
|
3220
|
-
|
|
3221
|
-
**Behavior changes without code changes.** PyPI upload step now
|
|
3222
|
-
runs in the consumer's workflow context. The reusable workflow's
|
|
3223
|
-
publish job no longer installs `twine` or `setup-python`; engine
|
|
3224
|
-
log lines for PyPI rows now read "delegated to caller-side upload
|
|
3225
|
-
step" instead of "authenticating via OIDC".
|
|
3226
|
-
|
|
3227
|
-
**Verification.** Push a release. The reusable workflow's
|
|
3228
|
-
`release` job creates and pushes the git tag for PyPI rows; the
|
|
3229
|
-
caller's `pypi-publish` job runs `pypa/gh-action-pypi-publish`
|
|
3230
|
-
and uploads to PyPI. Check `https://pypi.org/project/<name>/<version>/`
|
|
3231
|
-
to confirm.
|
|
3232
|
-
|
|
3233
|
-
---
|
|
3234
|
-
|
|
3235
|
-
### PyPI artifact discovery matches `{name}-sdist` and `{name}-wheel-` exactly
|
|
3236
|
-
|
|
3237
|
-
**Summary.** `src/handlers/pypi.ts:collectArtifacts` used a bare prefix
|
|
3238
|
-
match (`entry.startsWith("{name}-")`) to find a package's artifact
|
|
3239
|
-
directories under `artifacts/`. Sibling packages whose names extended
|
|
3240
|
-
the same prefix (`foo` and `foo-extras`) collided: `foo`'s discovery
|
|
3241
|
-
also picked up `foo-extras-sdist`, and twine then uploaded the sibling's
|
|
3242
|
-
tarball under `foo`'s OIDC identity, failing PyPI's project-name check.
|
|
3243
|
-
The handler now matches the sdist directory exactly (`{name}-sdist`)
|
|
3244
|
-
and the wheel directories by `{name}-wheel-` prefix only — the two
|
|
3245
|
-
shapes the planner documents in §12.4.
|
|
3246
|
-
|
|
3247
|
-
**Required changes.** None.
|
|
3248
|
-
|
|
3249
|
-
**Deprecations removed.** None.
|
|
3250
|
-
|
|
3251
|
-
**Behavior changes without code changes.** Repos with multiple pypi
|
|
3252
|
-
packages where one name is a prefix of another (e.g. `foo` and
|
|
3253
|
-
`foo-extras`) no longer cross-upload artifacts. Single-package repos
|
|
3254
|
-
and repos with non-overlapping names are unaffected.
|
|
3255
|
-
|
|
3256
|
-
**Verification.** A repo declaring both `foo` and `foo-extras` as
|
|
3257
|
-
pypi packages publishes the correct tarballs to each project; neither
|
|
3258
|
-
job uploads the other's artifacts.
|
|
3259
|
-
|
|
3260
|
-
---
|
|
3261
|
-
|
|
3262
|
-
### Reusable workflow's maturin sdist row uses `command: sdist`
|
|
3263
|
-
|
|
3264
|
-
**Summary.** The reusable workflow's pypi-maturin build step was a single
|
|
3265
|
-
`PyO3/maturin-action@v1` invocation with `command: build` and an
|
|
3266
|
-
`--sdist` flag conditional on the row being the sdist target. `maturin
|
|
3267
|
-
build --sdist` is documented as "build a wheel AND an sdist" — the
|
|
3268
|
-
sdist's artifact directory ended up containing both a `.tar.gz` and a
|
|
3269
|
-
manylinux wheel, which collided at upload time with the per-target
|
|
3270
|
-
wheel rows and aborted twine with `400 File already exists`. The build
|
|
3271
|
-
step is now split into two: `command: sdist` for the sdist row
|
|
3272
|
-
(sdist-only) and `command: build` with `--target` for wheel rows.
|
|
3273
|
-
|
|
3274
|
-
**Required changes.** None.
|
|
3275
|
-
|
|
3276
|
-
**Deprecations removed.** None.
|
|
3277
|
-
|
|
3278
|
-
**Behavior changes without code changes.** Maturin packages with a
|
|
3279
|
-
`sdist` row in their plan now upload a single `.tar.gz` from that row,
|
|
3280
|
-
not a wheel-plus-sdist pair. Per-target wheel rows are unaffected.
|
|
3281
|
-
|
|
3282
|
-
**Verification.** A maturin-built package with `sdist` in `targets`
|
|
3283
|
-
publishes to PyPI without `400 File already exists`. The sdist
|
|
3284
|
-
artifact directory contains `.tar.gz` only.
|
|
3285
|
-
|
|
3286
|
-
---
|
|
3287
|
-
|
|
3288
|
-
### Synthesized npm platform packages inherit `repository`/`license`/`homepage`
|
|
3289
|
-
|
|
3290
|
-
**Summary.** npm's provenance verifier rejected platform-package tarballs
|
|
3291
|
-
with `E422 Error verifying sigstore provenance bundle: Failed to validate
|
|
3292
|
-
repository information: package.json: "repository.url" is ""`. The
|
|
3293
|
-
synthesizer in `src/handlers/npm-platform.ts` previously wrote only
|
|
3294
|
-
`name`/`version`/`os`/`cpu`/`files`/`main`/`libc` into the per-target
|
|
3295
|
-
`package.json`. The publishing GitHub repo URL is bound into the
|
|
3296
|
-
sigstore bundle by `npm publish --provenance`; npm cross-checks it
|
|
3297
|
-
against `package.json.repository.url` at upload time, so an empty value
|
|
3298
|
-
fails verification. Identity fields (`repository`, `license`, `homepage`)
|
|
3299
|
-
are now read from the main package's `package.json` and copied into each
|
|
3300
|
-
synthesized platform package. Affects `build = "napi"` and
|
|
3301
|
-
`build = "bundled-cli"` packages.
|
|
3302
|
-
|
|
3303
|
-
**Required changes.** None — the fix is automatic. To benefit, ensure
|
|
3304
|
-
the main package's `package.json` declares a `repository.url` that
|
|
3305
|
-
matches the publishing repo (npm provenance has always required this for
|
|
3306
|
-
the main package; platform packages now share the same expectation).
|
|
3307
|
-
|
|
3308
|
-
**Deprecations removed.** None.
|
|
3309
|
-
|
|
3310
|
-
**Behavior changes without code changes.** Per-target platform tarballs
|
|
3311
|
-
on the registry now carry the same `repository`/`license`/`homepage`
|
|
3312
|
-
values as the main package, instead of being absent.
|
|
3313
|
-
|
|
3314
|
-
**Verification.** A `build = "napi"` or `build = "bundled-cli"` package
|
|
3315
|
-
publishes its platform tarballs to npm without `E422` provenance errors.
|
|
3316
|
-
`npm view <pkg>-<target>@<version> repository` returns the main
|
|
3317
|
-
package's repository URL.
|
|
3318
|
-
|
|
3319
|
-
---
|
|
3320
|
-
|
|
3321
|
-
### Reusable workflow's npm build step forces `shell: bash`
|
|
3322
|
-
|
|
3323
|
-
**Summary.** The build matrix can target Windows runners. GitHub Actions
|
|
3324
|
-
defaults to `pwsh` for `run:` blocks on Windows, but the npm build's
|
|
3325
|
-
shape detection (`if [ -f package-lock.json ]; then npm ci; elif ... fi`)
|
|
3326
|
-
is bash syntax — PowerShell parsed it as a malformed expression and
|
|
3327
|
-
aborted with `ParserError` before any package manager ran. The step now
|
|
3328
|
-
sets `shell: bash` explicitly, which is portable across Linux, macOS,
|
|
3329
|
-
and Windows runners (Git Bash ships on `windows-latest`).
|
|
3330
|
-
|
|
3331
|
-
**Required changes.** None.
|
|
3332
|
-
|
|
3333
|
-
**Deprecations removed.** None.
|
|
3334
|
-
|
|
3335
|
-
**Behavior changes without code changes.** Consumers whose plan includes
|
|
3336
|
-
an npm package targeting Windows runners (e.g. native node-addon shapes,
|
|
3337
|
-
`napi-rs` matrices) now succeed past the install step. Linux/macOS-only
|
|
3338
|
-
matrices are unaffected — bash was already the default there.
|
|
3339
|
-
|
|
3340
|
-
**Verification.** An npm package with a Windows row in its plan
|
|
3341
|
-
completes the install + build step on `windows-latest`; the job log
|
|
3342
|
-
shows `Run if [ -f package-lock.json ]` executing under bash, not pwsh.
|
|
3343
|
-
|
|
3344
|
-
---
|
|
3345
|
-
|
|
3346
|
-
### Reusable workflow exchanges OIDC token for `CARGO_REGISTRY_TOKEN`
|
|
3347
|
-
|
|
3348
|
-
**Summary.** Crates publishes were failing with `error: no token found,
|
|
3349
|
-
please run cargo login` — the reusable workflow was relying on cargo to
|
|
3350
|
-
find an OIDC token in env, but cargo only consumes
|
|
3351
|
-
`CARGO_REGISTRY_TOKEN` (a registry-issued bearer), not raw OIDC
|
|
3352
|
-
ID-tokens. The publish job now runs `rust-lang/crates-io-auth-action@v1`
|
|
3353
|
-
when the plan contains a crates row and exports its `outputs.token`
|
|
3354
|
-
as `CARGO_REGISTRY_TOKEN` for the engine subprocess.
|
|
3355
|
-
|
|
3356
|
-
**Required changes.** None for consumers using the reusable workflow as
|
|
3357
|
-
documented. Repos publishing to crates.io must have a configured trusted
|
|
3358
|
-
publisher on crates.io pointing at their `release.yml` — same prerequisite
|
|
3359
|
-
as before, just now actually exercised.
|
|
3360
|
-
|
|
3361
|
-
**Deprecations removed.** None.
|
|
3362
|
-
|
|
3363
|
-
**Behavior changes without code changes.** Crates publish in the
|
|
3364
|
-
reusable workflow now reaches the registry; previously it failed at
|
|
3365
|
-
the cargo invocation. JS/Python-only repos are unaffected — the auth
|
|
3366
|
-
step is gated on `contains(needs.plan.outputs.matrix, '"kind":"crates"')`
|
|
3367
|
-
and skips entirely when no crates row is in the plan.
|
|
3368
|
-
|
|
3369
|
-
**Verification.** A `kind = "crates"` package whose trusted publisher is
|
|
3370
|
-
configured on crates.io now publishes successfully through the reusable
|
|
3371
|
-
workflow. The publish job log shows the `Authenticate with crates.io
|
|
3372
|
-
(OIDC)` step running before `putitoutthere publish`.
|
|
3373
|
-
|
|
3374
|
-
---
|
|
3375
|
-
|
|
3376
|
-
### Crates publish's pre-cargo dirty-tree check ignores `artifacts/`
|
|
3377
|
-
|
|
3378
|
-
**Summary.** The crates handler scans `git status --porcelain` before
|
|
3379
|
-
invoking `cargo publish --allow-dirty`, refusing to proceed if anything
|
|
3380
|
-
other than the managed `Cargo.toml` is dirty (the writeVersion bump
|
|
3381
|
-
runs in the same job and would otherwise be the only legitimate dirty
|
|
3382
|
-
file). The reusable workflow's `actions/download-artifact@v4` step
|
|
3383
|
-
always creates `artifacts/` at the repo root before publish runs —
|
|
3384
|
-
even for crates-only fixtures that have nothing to download — and the
|
|
3385
|
-
pre-check was rejecting on `?? artifacts/`. The scan now treats the
|
|
3386
|
-
engine's own `artifactsRoot` as managed scratch space and skips files
|
|
3387
|
-
under it.
|
|
3388
|
-
|
|
3389
|
-
**Required changes.** None.
|
|
3390
|
-
|
|
3391
|
-
**Deprecations removed.** None.
|
|
3392
|
-
|
|
3393
|
-
**Behavior changes without code changes.** Crates publishes that
|
|
3394
|
-
previously errored with `unexpected dirty files in the working tree
|
|
3395
|
-
outside <Cargo.toml>: - artifacts/` now proceed to `cargo publish`.
|
|
3396
|
-
Stray edits anywhere else in the tree still fail the check.
|
|
3397
|
-
|
|
3398
|
-
**Verification.** A `kind = "crates"` package in a repo whose only
|
|
3399
|
-
"dirty" file (alongside the managed `Cargo.toml`) is the engine's
|
|
3400
|
-
`artifacts/` directory now reaches cargo. `git status --porcelain`
|
|
3401
|
-
showing `?? artifacts/` is no longer fatal.
|
|
3402
|
-
|
|
3403
|
-
---
|
|
3404
|
-
|
|
3405
|
-
### Crates publish no longer fails the pre-publish completeness check
|
|
3406
|
-
|
|
3407
|
-
**Summary.** Consumers with a `kind = "crates"` package previously hit
|
|
3408
|
-
`Artifact completeness check failed: missing artifact directory
|
|
3409
|
-
<name>-crate/` before cargo was ever invoked. The reusable workflow
|
|
3410
|
-
does not upload a `.crate` artifact (cargo packages and uploads from
|
|
3411
|
-
source on the registry side), so the file the check demanded never
|
|
3412
|
-
existed in the pipeline. The completeness check now skips crates
|
|
3413
|
-
rows. Same reasoning as vanilla npm rows, which were already skipped.
|
|
3414
|
-
|
|
3415
|
-
**Required changes.** None.
|
|
3416
|
-
|
|
3417
|
-
**Deprecations removed.** None.
|
|
3418
|
-
|
|
3419
|
-
**Behavior changes without code changes.** Crates publishes that
|
|
3420
|
-
previously errored at the completeness gate now reach `cargo publish`.
|
|
3421
|
-
A crates row whose source tree is genuinely broken still fails — the
|
|
3422
|
-
failure just happens at the cargo step, not before.
|
|
3423
|
-
|
|
3424
|
-
**Verification.** A `kind = "crates"` package in
|
|
3425
|
-
`putitoutthere.toml` no longer requires any artifact upload step in
|
|
3426
|
-
the consumer's workflow. Trigger a release with a `release: patch`
|
|
3427
|
-
trailer; the publish job's "Run putitoutthere publish" step should
|
|
3428
|
-
log `crates: cargo publish ...` instead of aborting on completeness.
|
|
3429
|
-
|
|
3430
|
-
### `[[package]].paths` renamed to `globs`
|
|
3431
|
-
|
|
3432
|
-
**Summary.** The `path` / `paths` pair in `[[package]]` was confusing —
|
|
3433
|
-
singular and plural differed only in a trailing `s` while meaning two
|
|
3434
|
-
unrelated things (the package working directory vs. the cascade-trigger
|
|
3435
|
-
globs). Renaming `paths` → `globs` removes the trailing-S collision.
|
|
3436
|
-
|
|
3437
|
-
**Required changes.**
|
|
3438
|
-
|
|
3439
|
-
| Before | After |
|
|
3440
|
-
|-----|-----|
|
|
3441
|
-
| `paths = ["src/**", "pyproject.toml"]` | `globs = ["src/**", "pyproject.toml"]` |
|
|
3442
|
-
|
|
3443
|
-
Every `[[package]]` block in `putitoutthere.toml` needs the rename.
|
|
3444
|
-
Configs declaring `paths` now fail validation under `.strict()`.
|
|
3445
|
-
|
|
3446
|
-
**Deprecations removed.** None.
|
|
3447
|
-
|
|
3448
|
-
**Behavior changes without code changes.** None — the field's semantics
|
|
3449
|
-
are unchanged.
|
|
3450
|
-
|
|
3451
|
-
**Verification.** `pnpm exec putitoutthere plan` (or the next reusable-
|
|
3452
|
-
workflow run) loads cleanly. A config still declaring `paths` fails
|
|
3453
|
-
load with a Zod error pointing at the unknown key.
|
|
3454
|
-
|
|
3455
|
-
### Removed: diagnostic CLI surface, GitHub-App auth, trust-policy validation
|
|
3456
|
-
|
|
3457
|
-
**Summary.** Eight things removed in one pass, none consumer-observable
|
|
3458
|
-
under the new "reusable workflow + OIDC-only" surface:
|
|
3459
|
-
|
|
3460
|
-
- `[package.trust_policy]` config block (false security: typo-catcher
|
|
3461
|
-
for npm/PyPI; the only real check was the crates.io registry
|
|
3462
|
-
cross-check, which required a separate token most consumers wouldn't
|
|
3463
|
-
set up).
|
|
3464
|
-
- `putitoutthere doctor` subcommand (its main job was the trust-policy
|
|
3465
|
-
validation above).
|
|
3466
|
-
- `putitoutthere preflight` subcommand (the internal `requireAuth`
|
|
3467
|
-
gate inside `publish` is preserved).
|
|
3468
|
-
- `putitoutthere token list/inspect` subcommands (operator-debugging
|
|
3469
|
-
surface for long-lived registry tokens — none exist under OIDC-only).
|
|
3470
|
-
- `putitoutthere auth login/logout/status` subcommands + the
|
|
3471
|
-
`putitoutthere-cli` GitHub App's device-flow plumbing + the keyring
|
|
3472
|
-
(only purpose was powering `token list --secrets`).
|
|
3473
|
-
- `src/release.ts` engine-side GitHub Release creation (duplicated by
|
|
3474
|
-
the reusable workflow's `gh release create --generate-notes` step).
|
|
3475
|
-
- `publish --preflight-check` flag (deep token-scope check for
|
|
3476
|
-
long-lived tokens; OIDC-only renders it moot).
|
|
3477
|
-
- Dead config fields: `cadence`, `agents_path`, `smoke`,
|
|
3478
|
-
`wheels_artifact` — defined in the schema, never read.
|
|
3479
|
-
|
|
3480
|
-
Net: ~2,800 lines of source removed, ~17% of `src/`.
|
|
3481
|
-
|
|
3482
|
-
**Required changes.**
|
|
3483
|
-
|
|
3484
|
-
| Before | After |
|
|
3485
|
-
|-----|-----|
|
|
3486
|
-
| `[package.trust_policy] workflow = "release.yml"` | Delete the block. Workflow renames still produce HTTP 400 from registries — same UX every other tool gives you. |
|
|
3487
|
-
| `putitoutthere doctor` / `preflight` / `token` / `auth` invocations in any consumer script | Remove. None of these are reachable through the reusable workflow; consumer-facing surface is the workflow itself. |
|
|
3488
|
-
| `cadence`, `agents_path`, `smoke`, `wheels_artifact` fields in `putitoutthere.toml` | Delete. They were never consumed; configs declaring them now fail validation under `.strict()`. |
|
|
3489
|
-
| `--preflight-check` flag passed to `publish` | Drop. Internal `requireAuth` still gates publish. |
|
|
3490
|
-
|
|
3491
|
-
**Deprecations removed.** Everything in the list above.
|
|
3492
|
-
|
|
3493
|
-
**Behavior changes without code changes.** Engine behavior on the
|
|
3494
|
-
plan / publish path is unchanged. `requireAuth` (the gate that
|
|
3495
|
-
catches missing OIDC env or missing token) still runs; the deep
|
|
3496
|
-
scope check (which required a long-lived token to inspect) no
|
|
3497
|
-
longer runs because there's no long-lived token to inspect. GitHub
|
|
3498
|
-
Release creation moves entirely to the reusable workflow's
|
|
3499
|
-
`gh release create` step — engines invoked outside that workflow
|
|
3500
|
-
(local dry-runs, custom integrations) no longer create Releases.
|
|
3501
|
-
|
|
3502
|
-
**Verification.** A consumer who never used any of the removed
|
|
3503
|
-
surfaces sees no observable change. Consumers who used `doctor` or
|
|
3504
|
-
`token` subcommands see exit-1 + "unknown command"; switch to the
|
|
3505
|
-
reusable workflow.
|
|
3506
|
-
|
|
3507
|
-
### Public surface collapsed to a reusable workflow
|
|
3508
|
-
|
|
3509
|
-
**Summary.** The consumer surface is now one line in a `release.yml`:
|
|
3510
|
-
|
|
3511
|
-
```yaml
|
|
3512
|
-
on:
|
|
3513
|
-
push: { branches: [main] }
|
|
3514
|
-
|
|
3515
|
-
jobs:
|
|
3516
|
-
release:
|
|
3517
|
-
uses: thekevinscott/putitoutthere/.github/workflows/release.yml@v0
|
|
3518
|
-
permissions:
|
|
3519
|
-
contents: write
|
|
3520
|
-
id-token: write
|
|
3521
|
-
```
|
|
3522
|
-
|
|
3523
|
-
Plus the consumer's existing `putitoutthere.toml`. Triggers live in
|
|
3524
|
-
the consumer's file; everything below them — pinned action versions,
|
|
3525
|
-
plan/build/publish orchestration, runner toolchain setup, artifact
|
|
3526
|
-
upload/download, GitHub Release creation — lives in the reusable
|
|
3527
|
-
workflow that piot ships. The CLI and the JS action are internal
|
|
3528
|
-
seams the reusable workflow invokes; consumers do not call them.
|
|
3529
|
-
Auth is OIDC trusted publishers only — long-lived registry tokens
|
|
3530
|
-
are not reachable through the workflow. See [design
|
|
3531
|
-
commitments](https://github.com/thekevinscott/putitoutthere/blob/main/notes/design-commitments.md)
|
|
3532
|
-
for the authoritative non-goals.
|
|
3533
|
-
|
|
3534
|
-
**Required changes.**
|
|
3535
|
-
|
|
3536
|
-
| Before (hand-written `release.yml`) | After |
|
|
3537
|
-
|-----|-----|
|
|
3538
|
-
| ~100 lines of YAML: plan/build/publish jobs, twine install, git identity, GitHub Release backfill, hand-pinned action majors | `uses: thekevinscott/putitoutthere/.github/workflows/release.yml@v0` |
|
|
3539
|
-
| `putitoutthere init` to scaffold the workflow | Subcommand removed; consumers add the snippet above by hand |
|
|
3540
|
-
| `[[package]].build_workflow = "publish-foo.yml"` for unsupported shapes | Removed. Shapes that don't fit piot's named build modes write their own release workflow that doesn't use piot |
|
|
3541
|
-
| Long-lived registry tokens (`NPM_TOKEN`, `PYPI_API_TOKEN`, `CARGO_REGISTRY_TOKEN`) passed to a hand-written publish step | Not reachable through the reusable workflow. Register an OIDC trusted publisher per registry once |
|
|
3542
|
-
| Optional inputs `dry_run`, `working_directory`, `config` | Removed. Plan job already prints the matrix without side effects; config lives at `putitoutthere.toml` in the repo root, no override |
|
|
3543
|
-
| Documentation site (`docs/`) | Removed. README is the single user-facing surface; `notes/internals/` holds the contracts the reusable workflow honors so consumers don't have to know them |
|
|
3544
|
-
|
|
3545
|
-
**Deprecations removed.** `build_workflow:` is no longer in the
|
|
3546
|
-
config schema (`src/config.ts`); configs that declare it now fail
|
|
3547
|
-
validation. `putitoutthere init`, `--cadence`, and `--force` flags
|
|
3548
|
-
are removed from the CLI.
|
|
3549
|
-
|
|
3550
|
-
**Behavior changes without code changes.** Engine behavior (plan,
|
|
3551
|
-
cascade, version bump, registry handlers, completeness check,
|
|
3552
|
-
idempotency, OIDC trust-policy validation) is unchanged. The
|
|
3553
|
-
reusable workflow internally pins:
|
|
3554
|
-
|
|
3555
|
-
- `actions/checkout@v4` (`fetch-depth: 0`)
|
|
3556
|
-
- `actions/setup-node@v4`
|
|
3557
|
-
- `actions/setup-python@v5`
|
|
3558
|
-
- `actions/upload-artifact@v4`
|
|
3559
|
-
- `actions/download-artifact@v4`
|
|
3560
|
-
- `PyO3/maturin-action@v1`
|
|
3561
|
-
|
|
3562
|
-
If a consumer was running newer majors (e.g. one consumer hit
|
|
3563
|
-
`download-artifact@v8` defaults that broke the artifact-naming
|
|
3564
|
-
contract), the reusable workflow standardises everyone on the
|
|
3565
|
-
known-tested versions.
|
|
3566
|
-
|
|
3567
|
-
**Verification.**
|
|
3568
|
-
|
|
3569
|
-
- `pnpm test:unit` passes in the main repo.
|
|
3570
|
-
- A consumer's first cutover: drop in the 12-line `release.yml`
|
|
3571
|
-
shown above, push a commit that touches a `[[package]].globs`
|
|
3572
|
-
glob, and watch for a tag push + GitHub Release on the next
|
|
3573
|
-
workflow run.
|
|
3574
|
-
|
|
3575
|
-
### Publish path works end-to-end for slash-containing `pkg.name`
|
|
3576
|
-
|
|
3577
|
-
**Summary.** Follow-up to the [`/`-encoding fix](#package-names-with--no-longer-need-an-encode-decode-workaround)
|
|
3578
|
-
([#230](https://github.com/thekevinscott/putitoutthere/issues/230)).
|
|
3579
|
-
Two bugs prevented slash-containing names from actually publishing
|
|
3580
|
-
even after the planner started encoding `/` to `__`
|
|
3581
|
-
([#237](https://github.com/thekevinscott/putitoutthere/issues/237)):
|
|
3582
|
-
|
|
3583
|
-
1. The pypi handler (`src/handlers/pypi.ts`) and the npm-platform
|
|
3584
|
-
synthesizer (`src/handlers/npm-platform.ts`) both built artifact
|
|
3585
|
-
directory lookups from the raw `pkg.name`, so a package called
|
|
3586
|
-
`py/foo` couldn't match the encoded on-disk directory
|
|
3587
|
-
`py__foo-sdist/`. Symptom: `pypi: no artifacts found for py/foo
|
|
3588
|
-
under <root>` at publish time.
|
|
3589
|
-
2. The planner emitted glob-shaped `artifact_path` values for crates
|
|
3590
|
-
tarballs, pypi sdists, and pypi wheels (e.g.
|
|
3591
|
-
`${pkg.path}/dist/*.tar.gz`). `actions/upload-artifact@v4` treats
|
|
3592
|
-
a glob `path:` differently from a directory `path:` — it preserves
|
|
3593
|
-
the workspace-relative path, so the sdist landed at
|
|
3594
|
-
`artifacts/<name>/packages/python/dist/foo.tar.gz` instead of
|
|
3595
|
-
`artifacts/<name>/foo.tar.gz`. Even after fix (1), the publish
|
|
3596
|
-
handler couldn't find files inside that nested layout.
|
|
3597
|
-
|
|
3598
|
-
Both fixed:
|
|
3599
|
-
|
|
3600
|
-
- Handlers route directory lookups through `sanitizeArtifactName`,
|
|
3601
|
-
matching whatever the planner emitted on the matrix row.
|
|
3602
|
-
- Handlers walk the artifact directory recursively for the expected
|
|
3603
|
-
file extensions (`.tar.gz` / `.whl` / `.crate`), so any layout
|
|
3604
|
-
(flat or nested) works.
|
|
3605
|
-
- Planner emits directory-shaped `artifact_path` values for the
|
|
3606
|
-
three slots that used a glob:
|
|
3607
|
-
|
|
3608
|
-
| Slot | Before | After |
|
|
3609
|
-
|---|---|---|
|
|
3610
|
-
| crates tarball | `${pkg.path}/target/package/*.crate` | `${pkg.path}/target/package` |
|
|
3611
|
-
| pypi maturin wheel | `${pkg.path}/dist/*.whl` | `${pkg.path}/dist` |
|
|
3612
|
-
| pypi sdist | `${pkg.path}/dist/*.tar.gz` | `${pkg.path}/dist` |
|
|
3613
|
-
|
|
3614
|
-
**Required changes.**
|
|
3615
|
-
|
|
3616
|
-
- **None for repos that pass `matrix.artifact_path` straight through**
|
|
3617
|
-
to `actions/upload-artifact@v4` (the canonical pattern shown in
|
|
3618
|
-
`docs/guide/shapes/*`). The matrix field already carries the new
|
|
3619
|
-
directory shape; on-disk artifact layout becomes flat
|
|
3620
|
-
(`<name>/foo.tar.gz` instead of `<name>/packages/python/dist/foo.tar.gz`),
|
|
3621
|
-
but consumer workflows see no observable change.
|
|
3622
|
-
- **Repos that hand-coded a glob path** should switch to the
|
|
3623
|
-
directory shape (or — better — replace the hard-coded value with
|
|
3624
|
-
the matrix field):
|
|
3625
|
-
|
|
3626
|
-
```diff
|
|
3627
|
-
- uses: actions/upload-artifact@v4
|
|
3628
|
-
with:
|
|
3629
|
-
name: ${{ matrix.artifact_name }}
|
|
3630
|
-
- path: packages/python/dist/*.tar.gz
|
|
3631
|
-
+ path: ${{ matrix.artifact_path }} # or "packages/python/dist"
|
|
3632
|
-
```
|
|
3633
|
-
|
|
3634
|
-
The recursive reader keeps glob layouts working as a safety net,
|
|
3635
|
-
but the directory shape is the canonical contract going forward.
|
|
3636
|
-
|
|
3637
|
-
**Deprecations removed.** None.
|
|
3638
|
-
|
|
3639
|
-
**Behavior changes without code changes.**
|
|
3640
|
-
|
|
3641
|
-
- Artifact directory layout is now flat: `artifacts/<name>/<file>`
|
|
3642
|
-
instead of `artifacts/<name>/<workspace-relative-path>/<file>`.
|
|
3643
|
-
Anything reading the artifact tree (the docs page, debugging
|
|
3644
|
-
scripts, custom verification jobs) should expect files at the
|
|
3645
|
-
artifact root.
|
|
3646
|
-
- The publish-side handlers now walk subdirectories recursively
|
|
3647
|
-
when looking for `.whl` / `.tar.gz` / `.crate` files. This is
|
|
3648
|
-
defensive for consumers whose build steps write to a non-standard
|
|
3649
|
-
location inside `<name>/`; the planner's directory `artifact_path`
|
|
3650
|
-
remains the canonical contract.
|
|
3651
|
-
|
|
3652
|
-
**Verification.**
|
|
3653
|
-
|
|
3654
|
-
```sh
|
|
3655
|
-
putitoutthere plan --json | jq '.[] | {name, artifact_name, artifact_path}'
|
|
3656
|
-
```
|
|
3657
|
-
|
|
3658
|
-
Expect every `artifact_path` to be a plain directory (no `*`):
|
|
3659
|
-
|
|
3660
|
-
```json
|
|
3661
|
-
{ "name": "py/foo", "artifact_name": "py__foo-sdist", "artifact_path": "py/foo/dist" }
|
|
3662
|
-
```
|
|
3663
|
-
|
|
3664
|
-
After the next release run, the `actions/upload-artifact@v4` step
|
|
3665
|
-
uploads `py/foo/dist/` contents flat under
|
|
3666
|
-
`artifacts/py__foo-sdist/` (no nested `packages/python/dist/`
|
|
3667
|
-
prefix), and the publish step finds the sdist immediately.
|
|
3668
|
-
|
|
3669
|
-
### Scaffolded `release.yml` now forwards `GITHUB_TOKEN`
|
|
3670
|
-
|
|
3671
|
-
**Summary.** piot has supported cutting a GitHub Release alongside each
|
|
3672
|
-
tag push since #26, but the scaffolded `release.yml` template never
|
|
3673
|
-
forwarded `GITHUB_TOKEN` to the publish step. GitHub Actions does not
|
|
3674
|
-
auto-mount the runner token as an env var — `permissions: contents:
|
|
3675
|
-
write` only grants the token *scope* to write Releases; the token still
|
|
3676
|
-
has to be exposed via `env:` for piot's `release.ts` to read it from
|
|
3677
|
-
`process.env.GITHUB_TOKEN`. Without it, piot silent-skipped Release
|
|
3678
|
-
creation and consumers got tags but no Release entries on the repo's
|
|
3679
|
-
Releases page. Fresh `piot init` runs now scaffold the env line.
|
|
3680
|
-
|
|
3681
|
-
**Required changes.** Existing repos that ran `piot init` before this
|
|
3682
|
-
change need a one-line addition to `.github/workflows/release.yml`:
|
|
3683
|
-
|
|
3684
|
-
```diff
|
|
3685
|
-
- uses: thekevinscott/putitoutthere@v0
|
|
3686
|
-
with:
|
|
3687
|
-
command: publish
|
|
3688
|
-
dry_run: ${{ inputs.dry_run || 'false' }}
|
|
3689
|
-
env:
|
|
3690
|
-
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
3691
|
-
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_TOKEN }}
|
|
3692
|
-
PYPI_API_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
|
|
3693
|
-
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
3694
|
-
```
|
|
3695
|
-
|
|
3696
|
-
The publish job already declares `permissions: contents: write`, which
|
|
3697
|
-
is the scope GitHub's runner-supplied `GITHUB_TOKEN` needs to create
|
|
3698
|
-
Releases — no additional permission changes required.
|
|
3699
|
-
|
|
3700
|
-
**Deprecations removed.** None.
|
|
3701
|
-
|
|
3702
|
-
**Behavior changes without code changes.** Repos that adopt the new
|
|
3703
|
-
template (or apply the diff above) start seeing GitHub Release entries
|
|
3704
|
-
appear under the repo's `/releases` page after each publish. The
|
|
3705
|
-
Release body is the output of:
|
|
3706
|
-
|
|
3707
|
-
```sh
|
|
3708
|
-
git log <prev-tag>..<this-tag> --format='- %s' --no-merges
|
|
3709
|
-
```
|
|
3710
|
-
|
|
3711
|
-
Tags suffixed with `-rc`, `-beta`, or `-alpha` are flagged
|
|
3712
|
-
`prerelease: true`. Release creation is best-effort: a 4xx/5xx from
|
|
3713
|
-
the GitHub API surfaces as a `publish: GitHub Release creation
|
|
3714
|
-
failed` warning but does not fail the publish run — the registry
|
|
3715
|
-
publish and tag push remain authoritative.
|
|
3716
|
-
|
|
3717
|
-
**Verification.** After the next release run on a repo that adopted the
|
|
3718
|
-
fix:
|
|
3719
|
-
|
|
3720
|
-
```bash
|
|
3721
|
-
# Inspect the publish job log:
|
|
3722
|
-
# "publish: GitHub Release created at https://github.com/.../releases/tag/<name>-v<x.y.z>"
|
|
3723
|
-
|
|
3724
|
-
# Or hit the API directly:
|
|
3725
|
-
gh release view <name>-v<x.y.z> --repo <owner>/<repo>
|
|
3726
|
-
```
|
|
3727
|
-
|
|
3728
|
-
If you previously saw the warning `publish: GitHub Release creation
|
|
3729
|
-
failed` in your publish logs, the warning should be gone and the
|
|
3730
|
-
Releases page should populate.
|
|
3731
|
-
|
|
3732
|
-
### Package names with `/` no longer need an encode/decode workaround
|
|
3733
|
-
|
|
3734
|
-
**Summary.** Polyglot-monorepo repos that group packages by language
|
|
3735
|
-
(e.g. `name = "py/foo"`, `"js/bar"`) used to fail at the build job
|
|
3736
|
-
with:
|
|
3737
|
-
|
|
3738
|
-
```
|
|
3739
|
-
The artifact name is not valid: py/foo-sdist.
|
|
3740
|
-
Contains the following character: Forward slash /
|
|
3741
|
-
```
|
|
3742
|
-
|
|
3743
|
-
…because `actions/upload-artifact@v4` forbids `/` in artifact names
|
|
3744
|
-
and the planner emitted `artifact_name` verbatim from `pkg.name`
|
|
3745
|
-
([#230](https://github.com/thekevinscott/putitoutthere/issues/230)).
|
|
3746
|
-
The planner now encodes each `/` to `__`
|
|
3747
|
-
(`py/foo` → `py__foo-sdist`), so the build job's
|
|
3748
|
-
upload-artifact step works without modification — pass the matrix
|
|
3749
|
-
`artifact_name` field through verbatim and the encoding happens
|
|
3750
|
-
upstream.
|
|
3751
|
-
|
|
3752
|
-
**Required changes.**
|
|
3753
|
-
|
|
3754
|
-
- **None for repos with slash-free `pkg.name`** — `artifact_name`
|
|
3755
|
-
is byte-identical to the previous version.
|
|
3756
|
-
- **Repos that ran a prior `/`-encoding workaround should remove it.** The planner now
|
|
3757
|
-
does the encoding natively; leaving the workaround in place
|
|
3758
|
-
produces double-encoded names like `py____foo-sdist`, which the
|
|
3759
|
-
publish-side reader will treat as a missing artifact.
|
|
3760
|
-
|
|
3761
|
-
```diff
|
|
3762
|
-
- uses: actions/upload-artifact@v4
|
|
3763
|
-
with:
|
|
3764
|
-
- name: ${{ format('{0}', matrix.artifact_name) }} # any sed/format encode
|
|
3765
|
-
- path: ${{ matrix.artifact_path }}
|
|
3766
|
-
+ name: ${{ matrix.artifact_name }} # use the field as-is
|
|
3767
|
-
+ path: ${{ matrix.artifact_path }}
|
|
3768
|
-
```
|
|
3769
|
-
|
|
3770
|
-
```diff
|
|
3771
|
-
- uses: actions/download-artifact@v4
|
|
3772
|
-
with:
|
|
3773
|
-
path: artifacts
|
|
3774
|
-
- - name: Decode artifact dir names
|
|
3775
|
-
- run: |
|
|
3776
|
-
- # rename artifacts/py__foo-sdist back to artifacts/py/foo-sdist
|
|
3777
|
-
- ...
|
|
3778
|
-
```
|
|
3779
|
-
|
|
3780
|
-
**Deprecations removed.** None.
|
|
3781
|
-
|
|
3782
|
-
**Behavior changes without code changes.**
|
|
3783
|
-
|
|
3784
|
-
- `pkg.name` containing `__` (the new encoding sequence) is now
|
|
3785
|
-
rejected at config load with: `package name must not contain "__"
|
|
3786
|
-
(reserved: piot encodes "/" to "__" for artifact-name slots; pick
|
|
3787
|
-
a different separator)`. If your config uses `__` in a package
|
|
3788
|
-
name today, rename to use `-` or `_` and update any tags / consumer
|
|
3789
|
-
references; piot can't safely sanitize it without ambiguity.
|
|
3790
|
-
- `pkg.name` containing `\`, `:`, `<`, `>`, `|`, `*`, `?`, or `"`
|
|
3791
|
-
is now rejected at config load. None of these are valid in npm,
|
|
3792
|
-
PyPI, or crates.io names, so any config that previously contained
|
|
3793
|
-
them was already broken at publish time — the change just moves
|
|
3794
|
-
the failure earlier with a clearer message.
|
|
3795
|
-
|
|
3796
|
-
**Verification.**
|
|
3797
|
-
|
|
3798
|
-
```sh
|
|
3799
|
-
putitoutthere plan --json | jq '.[].artifact_name'
|
|
3800
|
-
```
|
|
3801
|
-
|
|
3802
|
-
Expect every emitted `artifact_name` to contain only ASCII letters,
|
|
3803
|
-
digits, `-`, `_`, and `.` — no `/` and no other forbidden chars.
|
|
3804
|
-
For a repo with `name = "py/foo"`:
|
|
3805
|
-
|
|
3806
|
-
```
|
|
3807
|
-
"py__foo-sdist"
|
|
3808
|
-
"py__foo-wheel-x86_64-unknown-linux-gnu"
|
|
3809
|
-
```
|
|
3810
|
-
|
|
3811
|
-
After the next release, the build job's `actions/upload-artifact@v4`
|
|
3812
|
-
step uploads under `py__foo-sdist/` (a single flat directory
|
|
3813
|
-
under `artifacts/`), and piot's publish-side reader consumes the
|
|
3814
|
-
same path.
|
|
3815
|
-
|
|
3816
|
-
### Documentation accuracy pass (#231)
|
|
3817
|
-
|
|
3818
|
-
**Summary.** A docs-vs-code audit found several places where reference
|
|
3819
|
-
material lagged behind shipped behavior. Existing configs and workflows
|
|
3820
|
-
keep working — the only consumer-observable change is that `putitoutthere
|
|
3821
|
-
--help` no longer mislabels `--json` as "plan only".
|
|
3822
|
-
|
|
3823
|
-
**Required changes.** None.
|
|
3824
|
-
|
|
3825
|
-
**Deprecations removed.** None.
|
|
3826
|
-
|
|
3827
|
-
**Behavior changes without code changes.**
|
|
3828
|
-
|
|
3829
|
-
- `putitoutthere --help` output: the `--json` line now reads `emit
|
|
3830
|
-
machine-readable output (most commands)` instead of `(plan only)`. The
|
|
3831
|
-
flag has always been accepted on every command that emits a result;
|
|
3832
|
-
only the help text was wrong.
|
|
3833
|
-
- No other behavior changes. All other audit findings were addressed by
|
|
3834
|
-
updating documentation (`docs/api/cli.md`, `docs/api/action.md`,
|
|
3835
|
-
`docs/guide/configuration.md`, `docs/guide/trailer.md`, `README.md`,
|
|
3836
|
-
`action.yml` description text, VitePress sidebar).
|
|
3837
|
-
|
|
3838
|
-
**Verification.**
|
|
3839
|
-
|
|
3840
|
-
```sh
|
|
3841
|
-
putitoutthere --help | grep -- '--json'
|
|
3842
|
-
# Expected: --json emit machine-readable output (most commands)
|
|
3843
|
-
```
|
|
3844
|
-
|
|
3845
|
-
### Python shape examples now use `uv build`
|
|
3846
|
-
|
|
3847
|
-
**Summary.** Documentation examples for the Python library, Python
|
|
3848
|
-
cibuildwheel, and dynamic-versions shapes switched the sdist-build
|
|
3849
|
-
step from `python -m build --sdist` to `uv build --sdist`. piot's
|
|
3850
|
-
contract is unchanged — backends, artifact names, the
|
|
3851
|
-
`matrix.artifact_name` / `matrix.artifact_path` fields, and the
|
|
3852
|
-
publish-side completeness check all work identically. The change
|
|
3853
|
-
removes a `pip install build` round-trip and aligns the docs with
|
|
3854
|
-
`uv` as the recommended Python toolchain.
|
|
3855
|
-
|
|
3856
|
-
**Required changes.** None. `python -m build` still works. To
|
|
3857
|
-
follow the new examples in your own `release.yml`:
|
|
3858
|
-
|
|
3859
|
-
```diff
|
|
3860
|
-
build:
|
|
3861
|
-
...
|
|
3862
|
-
steps:
|
|
3863
|
-
- - uses: actions/setup-python@v5
|
|
3864
|
-
- with: { python-version: '3.12' }
|
|
3865
|
-
- name: Build sdist
|
|
3866
|
-
- run: |
|
|
3867
|
-
- cd ${{ matrix.path }}
|
|
3868
|
-
- python -m pip install build
|
|
3869
|
-
- python -m build --sdist --outdir dist
|
|
3870
|
-
+ working-directory: ${{ matrix.path }}
|
|
3871
|
-
+ run: uv build --sdist
|
|
3872
|
-
+ # uv installs and manages Python itself; no setup-python step needed.
|
|
3873
|
-
+ # Add this once at the top of the build job:
|
|
3874
|
-
+ - uses: astral-sh/setup-uv@v3
|
|
3875
|
-
```
|
|
3876
|
-
|
|
3877
|
-
`uv build --sdist` writes to `dist/` inside the working directory
|
|
3878
|
-
(same as `python -m build --outdir dist`), so
|
|
3879
|
-
`matrix.artifact_path` keeps pointing at the right place. The
|
|
3880
|
-
publish job is unchanged — `setup-python` + `pip install twine` is
|
|
3881
|
-
still the recommended path there because piot's PyPI handler shells
|
|
3882
|
-
out to `twine`.
|
|
3883
|
-
|
|
3884
|
-
**When *not* to follow this example.** Stay on `python -m build`
|
|
3885
|
-
if:
|
|
3886
|
-
|
|
3887
|
-
- Your CI image already has Python pre-installed and adding
|
|
3888
|
-
`setup-uv` would slow the cold cache.
|
|
3889
|
-
- Your `pyproject.toml` exercises a build backend feature that uv's
|
|
3890
|
-
isolated build environment doesn't yet handle (rare; uv's build
|
|
3891
|
-
isolation matches `python -m build`'s).
|
|
3892
|
-
- Your team's runbook standardises on `python -m build` and the
|
|
3893
|
-
consistency cost of switching outweighs the per-run speedup.
|
|
3894
|
-
|
|
3895
|
-
`python -m build` is not deprecated and will keep working.
|
|
3896
|
-
|
|
3897
|
-
**Deprecations removed.** None.
|
|
3898
|
-
|
|
3899
|
-
**Behavior changes without code changes.** None.
|
|
3900
|
-
|
|
3901
|
-
**Verification.**
|
|
3902
|
-
|
|
3903
|
-
```bash
|
|
3904
|
-
# After the build job runs:
|
|
3905
|
-
ls artifacts/<pkg.name>-sdist/
|
|
3906
|
-
# Expected: <pypi-name>-X.Y.Z.tar.gz (no .devN suffix)
|
|
3907
|
-
```
|
|
3908
|
-
|
|
3909
|
-
If you see the expected sdist, the switch worked. If you see a
|
|
3910
|
-
`.devN` suffix, your project uses dynamic versioning — see
|
|
3911
|
-
[dynamic versions](https://thekevinscott.github.io/putitoutthere/guide/dynamic-versions)
|
|
3912
|
-
for the env-var handoff (unchanged by this migration).
|
|
3913
|
-
|
|
3914
|
-
### Repository renamed `put-it-out-there` → `putitoutthere`
|
|
3915
|
-
|
|
3916
|
-
**Summary.** The GitHub repository slug collapsed from `put-it-out-there`
|
|
3917
|
-
to `putitoutthere`, matching the npm package and CLI binary name. The
|
|
3918
|
-
human-readable name "Put It Out There" (with spaces) is unchanged. GitHub
|
|
3919
|
-
auto-redirects the old URL, but any place a consumer has hard-coded the
|
|
3920
|
-
old slug — npm/Cargo/pyproject `repository` URLs, GitHub Actions
|
|
3921
|
-
references, OIDC trust policy `repository:` claims, docs links — should
|
|
3922
|
-
be updated.
|
|
3923
|
-
|
|
3924
|
-
**Required changes.**
|
|
3925
|
-
|
|
3926
|
-
```diff
|
|
3927
|
-
# package.json (or Cargo.toml / pyproject.toml)
|
|
3928
|
-
-"repository": "https://github.com/<owner>/put-it-out-there"
|
|
3929
|
-
+"repository": "https://github.com/<owner>/putitoutthere"
|
|
3930
|
-
```
|
|
3931
|
-
|
|
3932
|
-
```diff
|
|
3933
|
-
# .github/workflows/release.yml — if you reference the action by full repo path
|
|
3934
|
-
-uses: thekevinscott/put-it-out-there/.github/actions/<...>
|
|
3935
|
-
+uses: thekevinscott/putitoutthere/.github/actions/<...>
|
|
3936
|
-
```
|
|
3937
|
-
|
|
3938
|
-
```diff
|
|
3939
|
-
# OIDC trust policies (PyPI, npm) that gate on the source repo
|
|
3940
|
-
-"repository": "<owner>/put-it-out-there"
|
|
3941
|
-
+"repository": "<owner>/putitoutthere"
|
|
3942
|
-
```
|
|
3943
|
-
|
|
3944
|
-
If you only ever invoked `putitoutthere` via the npm package
|
|
3945
|
-
(`npx putitoutthere`, `pnpm add -D putitoutthere`) or the published
|
|
3946
|
-
GitHub Action, no change is required — those references already used the
|
|
3947
|
-
collapsed name.
|
|
3948
|
-
|
|
3949
|
-
**Deprecations removed.** None. The old slug continues to redirect at
|
|
3950
|
-
the GitHub layer.
|
|
3951
|
-
|
|
3952
|
-
**Behavior changes without code changes.**
|
|
3953
|
-
|
|
3954
|
-
- Documentation site moved from
|
|
3955
|
-
`https://thekevinscott.github.io/put-it-out-there/` to
|
|
3956
|
-
`https://thekevinscott.github.io/putitoutthere/`. The old URL
|
|
3957
|
-
redirects.
|
|
3958
|
-
- `git remote -v` will still show the old URL until you `git remote
|
|
3959
|
-
set-url origin https://github.com/thekevinscott/putitoutthere.git`.
|
|
3960
|
-
Push and fetch keep working via redirect, but updating the remote
|
|
3961
|
-
avoids surprise breakage if the redirect is ever retired.
|
|
3962
|
-
|
|
3963
|
-
**Verification.**
|
|
3964
|
-
|
|
3965
|
-
```sh
|
|
3966
|
-
# Confirm no stale references in your repo
|
|
3967
|
-
grep -r "put-it-out-there" .
|
|
3968
|
-
```
|
|
3969
|
-
|
|
3970
|
-
Expect no hits outside historical changelog/migration entries.
|
|
3971
|
-
|
|
3972
|
-
### `[package.bundle_cli]` — stage a Rust CLI into every maturin wheel (#217)
|
|
3973
|
-
|
|
3974
|
-
> **Note (#282).** The "Behavior changes without code changes"
|
|
3975
|
-
> paragraph below claimed two scaffolded build steps would be
|
|
3976
|
-
> emitted. Those steps were not actually present in `_matrix.yml`
|
|
3977
|
-
> until #282 (Unreleased); for v0.2.0 through v0.2.10 the recipe
|
|
3978
|
-
> was a no-op and wheels shipped without the binary. See the
|
|
3979
|
-
> Unreleased entry "[package.bundle_cli] now actually stages the
|
|
3980
|
-
> binary" above for the actual landing.
|
|
3981
|
-
|
|
3982
|
-
**Summary.** New optional sub-table under `[[package]]` for pypi packages
|
|
3983
|
-
that want the `ruff` / `uv` / `pydantic-core` wheel shape: a companion
|
|
3984
|
-
Rust CLI binary, cross-compiled per target and staged into the Python
|
|
3985
|
-
source tree before maturin runs, so each wheel ships the binary as
|
|
3986
|
-
package data and `pip install <pkg>` gets a working CLI on `PATH` with
|
|
3987
|
-
no Rust toolchain on the user's machine. Additive — existing
|
|
3988
|
-
configurations are unchanged.
|
|
3989
|
-
|
|
3990
|
-
**Required changes.** None for existing configs. To opt in:
|
|
3991
|
-
|
|
3992
|
-
```diff
|
|
3993
|
-
[[package]]
|
|
3994
|
-
name = "my-py"
|
|
3995
|
-
kind = "pypi"
|
|
3996
|
-
build = "maturin"
|
|
3997
|
-
path = "packages/python"
|
|
3998
|
-
globs = ["packages/python/**"]
|
|
3999
|
-
targets = ["x86_64-unknown-linux-gnu", "aarch64-apple-darwin"]
|
|
4000
|
-
+
|
|
4001
|
-
+[package.bundle_cli]
|
|
4002
|
-
+bin = "my-cli"
|
|
4003
|
-
+stage_to = "src/my_py/_binary"
|
|
4004
|
-
+crate_path = "crates/my-rust" # defaults to "." (repo workspace root)
|
|
4005
|
-
```
|
|
4006
|
-
|
|
4007
|
-
And in the Python package's `pyproject.toml`:
|
|
4008
|
-
|
|
4009
|
-
```diff
|
|
4010
|
-
+[project.scripts]
|
|
4011
|
-
+my-cli = "my_py._binary:entrypoint" # small os.execv launcher stub
|
|
4012
|
-
+
|
|
4013
|
-
[tool.maturin]
|
|
4014
|
-
-include = ["..."]
|
|
4015
|
-
+include = ["...", "src/my_py/_binary/**"] # ship the staged binary
|
|
4016
|
-
```
|
|
4017
|
-
|
|
4018
|
-
See [README → Rust CLI inside a PyPI wheel](https://github.com/thekevinscott/putitoutthere/blob/main/README.md#rust-cli-inside-a-pypi-wheel)
|
|
4019
|
-
for the full worked example including the launcher stub.
|
|
4020
|
-
|
|
4021
|
-
**Deprecations removed.** None.
|
|
4022
|
-
|
|
4023
|
-
**Behavior changes without code changes.** None for existing configs.
|
|
4024
|
-
Packages that declare `[package.bundle_cli]` get two new steps emitted
|
|
4025
|
-
in the scaffolded build job (`Setup Rust (if pypi bundle_cli)` +
|
|
4026
|
-
`Build + stage bundled CLI`), both gated on
|
|
4027
|
-
`matrix.kind == 'pypi' && matrix.bundle_cli.bin != '' && matrix.target != 'sdist'`
|
|
4028
|
-
so packages without the block see no change.
|
|
4029
|
-
|
|
4030
|
-
**Verification.** For a repo that opts in:
|
|
4031
|
-
|
|
4032
|
-
```bash
|
|
4033
|
-
# After piot's build job runs on one target:
|
|
4034
|
-
ls packages/python/src/my_py/_binary/
|
|
4035
|
-
# Expected: my-cli (or my-cli.exe on Windows targets)
|
|
4036
|
-
|
|
4037
|
-
# After the wheel is built:
|
|
4038
|
-
python -m zipfile -l packages/python/dist/*.whl | grep _binary
|
|
4039
|
-
# Expected: one entry per target listing the staged binary.
|
|
4040
|
-
|
|
4041
|
-
# End-to-end on a released wheel:
|
|
4042
|
-
pip install my-py==<published-version>
|
|
4043
|
-
which my-cli
|
|
4044
|
-
my-cli --version
|
|
4045
|
-
```
|