putitoutthere 0.1.43 → 0.1.45

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.
Files changed (82) hide show
  1. package/CHANGELOG.md +115 -0
  2. package/MIGRATIONS.md +632 -0
  3. package/README.md +342 -94
  4. package/action.yml +5 -5
  5. package/dist/action.js +2 -2
  6. package/dist/action.js.map +1 -1
  7. package/dist/cascade.d.ts +1 -1
  8. package/dist/cascade.js +2 -2
  9. package/dist/cli.d.ts +9 -6
  10. package/dist/cli.d.ts.map +1 -1
  11. package/dist/cli.js +12 -426
  12. package/dist/cli.js.map +1 -1
  13. package/dist/config.d.ts +13 -61
  14. package/dist/config.d.ts.map +1 -1
  15. package/dist/config.js +31 -45
  16. package/dist/config.js.map +1 -1
  17. package/dist/git.js +1 -1
  18. package/dist/git.js.map +1 -1
  19. package/dist/handlers/npm-platform.d.ts.map +1 -1
  20. package/dist/handlers/npm-platform.js +5 -1
  21. package/dist/handlers/npm-platform.js.map +1 -1
  22. package/dist/handlers/pypi.d.ts.map +1 -1
  23. package/dist/handlers/pypi.js +29 -6
  24. package/dist/handlers/pypi.js.map +1 -1
  25. package/dist/plan.d.ts +0 -1
  26. package/dist/plan.d.ts.map +1 -1
  27. package/dist/plan.js +19 -18
  28. package/dist/plan.js.map +1 -1
  29. package/dist/preflight.js +1 -1
  30. package/dist/preflight.js.map +1 -1
  31. package/dist/publish.d.ts +0 -10
  32. package/dist/publish.d.ts.map +1 -1
  33. package/dist/publish.js +6 -52
  34. package/dist/publish.js.map +1 -1
  35. package/dist/types.d.ts +1 -21
  36. package/dist/types.d.ts.map +1 -1
  37. package/dist/types.js.map +1 -1
  38. package/package.json +5 -3
  39. package/dist/auth.d.ts +0 -68
  40. package/dist/auth.d.ts.map +0 -1
  41. package/dist/auth.js +0 -285
  42. package/dist/auth.js.map +0 -1
  43. package/dist/doctor.d.ts +0 -115
  44. package/dist/doctor.d.ts.map +0 -1
  45. package/dist/doctor.js +0 -292
  46. package/dist/doctor.js.map +0 -1
  47. package/dist/init.d.ts +0 -59
  48. package/dist/init.d.ts.map +0 -1
  49. package/dist/init.js +0 -181
  50. package/dist/init.js.map +0 -1
  51. package/dist/keyring.d.ts +0 -35
  52. package/dist/keyring.d.ts.map +0 -1
  53. package/dist/keyring.js +0 -88
  54. package/dist/keyring.js.map +0 -1
  55. package/dist/oidc-policy.d.ts +0 -145
  56. package/dist/oidc-policy.d.ts.map +0 -1
  57. package/dist/oidc-policy.js +0 -383
  58. package/dist/oidc-policy.js.map +0 -1
  59. package/dist/preflight-run.d.ts +0 -47
  60. package/dist/preflight-run.d.ts.map +0 -1
  61. package/dist/preflight-run.js +0 -197
  62. package/dist/preflight-run.js.map +0 -1
  63. package/dist/registries/crates-trust.d.ts +0 -69
  64. package/dist/registries/crates-trust.d.ts.map +0 -1
  65. package/dist/registries/crates-trust.js +0 -95
  66. package/dist/registries/crates-trust.js.map +0 -1
  67. package/dist/release.d.ts +0 -36
  68. package/dist/release.d.ts.map +0 -1
  69. package/dist/release.js +0 -102
  70. package/dist/release.js.map +0 -1
  71. package/dist/templates.d.ts +0 -43
  72. package/dist/templates.d.ts.map +0 -1
  73. package/dist/templates.js +0 -310
  74. package/dist/templates.js.map +0 -1
  75. package/dist/token-scope.d.ts +0 -65
  76. package/dist/token-scope.d.ts.map +0 -1
  77. package/dist/token-scope.js +0 -163
  78. package/dist/token-scope.js.map +0 -1
  79. package/dist/token.d.ts +0 -211
  80. package/dist/token.d.ts.map +0 -1
  81. package/dist/token.js +0 -719
  82. package/dist/token.js.map +0 -1
package/MIGRATIONS.md ADDED
@@ -0,0 +1,632 @@
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
+ ### `[[package]].paths` renamed to `globs`
25
+
26
+ **Summary.** The `path` / `paths` pair in `[[package]]` was confusing —
27
+ singular and plural differed only in a trailing `s` while meaning two
28
+ unrelated things (the package working directory vs. the cascade-trigger
29
+ globs). Renaming `paths` → `globs` removes the trailing-S collision.
30
+
31
+ **Required changes.**
32
+
33
+ | Before | After |
34
+ |-----|-----|
35
+ | `paths = ["src/**", "pyproject.toml"]` | `globs = ["src/**", "pyproject.toml"]` |
36
+
37
+ Every `[[package]]` block in `putitoutthere.toml` needs the rename.
38
+ Configs declaring `paths` now fail validation under `.strict()`.
39
+
40
+ **Deprecations removed.** None.
41
+
42
+ **Behavior changes without code changes.** None — the field's semantics
43
+ are unchanged.
44
+
45
+ **Verification.** `pnpm exec putitoutthere plan` (or the next reusable-
46
+ workflow run) loads cleanly. A config still declaring `paths` fails
47
+ load with a Zod error pointing at the unknown key.
48
+
49
+ ### Removed: diagnostic CLI surface, GitHub-App auth, trust-policy validation
50
+
51
+ **Summary.** Eight things removed in one pass, none consumer-observable
52
+ under the new "reusable workflow + OIDC-only" surface:
53
+
54
+ - `[package.trust_policy]` config block (false security: typo-catcher
55
+ for npm/PyPI; the only real check was the crates.io registry
56
+ cross-check, which required a separate token most consumers wouldn't
57
+ set up).
58
+ - `putitoutthere doctor` subcommand (its main job was the trust-policy
59
+ validation above).
60
+ - `putitoutthere preflight` subcommand (the internal `requireAuth`
61
+ gate inside `publish` is preserved).
62
+ - `putitoutthere token list/inspect` subcommands (operator-debugging
63
+ surface for long-lived registry tokens — none exist under OIDC-only).
64
+ - `putitoutthere auth login/logout/status` subcommands + the
65
+ `putitoutthere-cli` GitHub App's device-flow plumbing + the keyring
66
+ (only purpose was powering `token list --secrets`).
67
+ - `src/release.ts` engine-side GitHub Release creation (duplicated by
68
+ the reusable workflow's `gh release create --generate-notes` step).
69
+ - `publish --preflight-check` flag (deep token-scope check for
70
+ long-lived tokens; OIDC-only renders it moot).
71
+ - Dead config fields: `cadence`, `agents_path`, `smoke`,
72
+ `wheels_artifact` — defined in the schema, never read.
73
+
74
+ Net: ~2,800 lines of source removed, ~17% of `src/`.
75
+
76
+ **Required changes.**
77
+
78
+ | Before | After |
79
+ |-----|-----|
80
+ | `[package.trust_policy] workflow = "release.yml"` | Delete the block. Workflow renames still produce HTTP 400 from registries — same UX every other tool gives you. |
81
+ | `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. |
82
+ | `cadence`, `agents_path`, `smoke`, `wheels_artifact` fields in `putitoutthere.toml` | Delete. They were never consumed; configs declaring them now fail validation under `.strict()`. |
83
+ | `--preflight-check` flag passed to `publish` | Drop. Internal `requireAuth` still gates publish. |
84
+
85
+ **Deprecations removed.** Everything in the list above.
86
+
87
+ **Behavior changes without code changes.** Engine behavior on the
88
+ plan / publish path is unchanged. `requireAuth` (the gate that
89
+ catches missing OIDC env or missing token) still runs; the deep
90
+ scope check (which required a long-lived token to inspect) no
91
+ longer runs because there's no long-lived token to inspect. GitHub
92
+ Release creation moves entirely to the reusable workflow's
93
+ `gh release create` step — engines invoked outside that workflow
94
+ (local dry-runs, custom integrations) no longer create Releases.
95
+
96
+ **Verification.** A consumer who never used any of the removed
97
+ surfaces sees no observable change. Consumers who used `doctor` or
98
+ `token` subcommands see exit-1 + "unknown command"; switch to the
99
+ reusable workflow.
100
+
101
+ ### Public surface collapsed to a reusable workflow
102
+
103
+ **Summary.** The consumer surface is now one line in a `release.yml`:
104
+
105
+ ```yaml
106
+ on:
107
+ push: { branches: [main] }
108
+
109
+ jobs:
110
+ release:
111
+ uses: thekevinscott/putitoutthere/.github/workflows/release.yml@v0
112
+ permissions:
113
+ contents: write
114
+ id-token: write
115
+ ```
116
+
117
+ Plus the consumer's existing `putitoutthere.toml`. Triggers live in
118
+ the consumer's file; everything below them — pinned action versions,
119
+ plan/build/publish orchestration, runner toolchain setup, artifact
120
+ upload/download, GitHub Release creation — lives in the reusable
121
+ workflow that piot ships. The CLI and the JS action are internal
122
+ seams the reusable workflow invokes; consumers do not call them.
123
+ Auth is OIDC trusted publishers only — long-lived registry tokens
124
+ are not reachable through the workflow. See [design
125
+ commitments](https://github.com/thekevinscott/putitoutthere/blob/main/notes/design-commitments.md)
126
+ for the authoritative non-goals.
127
+
128
+ **Required changes.**
129
+
130
+ | Before (hand-written `release.yml`) | After |
131
+ |-----|-----|
132
+ | ~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` |
133
+ | `putitoutthere init` to scaffold the workflow | Subcommand removed; consumers add the snippet above by hand |
134
+ | `[[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 |
135
+ | 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 |
136
+ | 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 |
137
+ | 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 |
138
+
139
+ **Deprecations removed.** `build_workflow:` is no longer in the
140
+ config schema (`src/config.ts`); configs that declare it now fail
141
+ validation. `putitoutthere init`, `--cadence`, and `--force` flags
142
+ are removed from the CLI.
143
+
144
+ **Behavior changes without code changes.** Engine behavior (plan,
145
+ cascade, version bump, registry handlers, completeness check,
146
+ idempotency, OIDC trust-policy validation) is unchanged. The
147
+ reusable workflow internally pins:
148
+
149
+ - `actions/checkout@v4` (`fetch-depth: 0`)
150
+ - `actions/setup-node@v4`
151
+ - `actions/setup-python@v5`
152
+ - `actions/upload-artifact@v4`
153
+ - `actions/download-artifact@v4`
154
+ - `PyO3/maturin-action@v1`
155
+
156
+ If a consumer was running newer majors (e.g. coaxer hit
157
+ `download-artifact@v8` defaults that broke the artifact-naming
158
+ contract), the reusable workflow standardises everyone on the
159
+ known-tested versions.
160
+
161
+ **Verification.**
162
+
163
+ - `pnpm test:unit` passes in the main repo.
164
+ - A consumer's first cutover: drop in the 12-line `release.yml`
165
+ shown above, push a commit that touches a `[[package]].globs`
166
+ glob, and watch for a tag push + GitHub Release on the next
167
+ workflow run.
168
+
169
+ ### Publish path works end-to-end for slash-containing `pkg.name`
170
+
171
+ **Summary.** Follow-up to the [`/`-encoding fix](#package-names-with--no-longer-need-an-encode-decode-workaround)
172
+ ([#230](https://github.com/thekevinscott/putitoutthere/issues/230)).
173
+ Two bugs prevented slash-containing names from actually publishing
174
+ even after the planner started encoding `/` to `__`
175
+ ([#237](https://github.com/thekevinscott/putitoutthere/issues/237)):
176
+
177
+ 1. The pypi handler (`src/handlers/pypi.ts`) and the npm-platform
178
+ synthesizer (`src/handlers/npm-platform.ts`) both built artifact
179
+ directory lookups from the raw `pkg.name`, so a package called
180
+ `py/foo` couldn't match the encoded on-disk directory
181
+ `py__foo-sdist/`. Symptom: `pypi: no artifacts found for py/foo
182
+ under <root>` at publish time.
183
+ 2. The planner emitted glob-shaped `artifact_path` values for crates
184
+ tarballs, pypi sdists, and pypi wheels (e.g.
185
+ `${pkg.path}/dist/*.tar.gz`). `actions/upload-artifact@v4` treats
186
+ a glob `path:` differently from a directory `path:` — it preserves
187
+ the workspace-relative path, so the sdist landed at
188
+ `artifacts/<name>/packages/python/dist/foo.tar.gz` instead of
189
+ `artifacts/<name>/foo.tar.gz`. Even after fix (1), the publish
190
+ handler couldn't find files inside that nested layout.
191
+
192
+ Both fixed:
193
+
194
+ - Handlers route directory lookups through `sanitizeArtifactName`,
195
+ matching whatever the planner emitted on the matrix row.
196
+ - Handlers walk the artifact directory recursively for the expected
197
+ file extensions (`.tar.gz` / `.whl` / `.crate`), so any layout
198
+ (flat or nested) works.
199
+ - Planner emits directory-shaped `artifact_path` values for the
200
+ three slots that used a glob:
201
+
202
+ | Slot | Before | After |
203
+ |---|---|---|
204
+ | crates tarball | `${pkg.path}/target/package/*.crate` | `${pkg.path}/target/package` |
205
+ | pypi maturin wheel | `${pkg.path}/dist/*.whl` | `${pkg.path}/dist` |
206
+ | pypi sdist | `${pkg.path}/dist/*.tar.gz` | `${pkg.path}/dist` |
207
+
208
+ **Required changes.**
209
+
210
+ - **None for repos that pass `matrix.artifact_path` straight through**
211
+ to `actions/upload-artifact@v4` (the canonical pattern shown in
212
+ `docs/guide/shapes/*`). The matrix field already carries the new
213
+ directory shape; on-disk artifact layout becomes flat
214
+ (`<name>/foo.tar.gz` instead of `<name>/packages/python/dist/foo.tar.gz`),
215
+ but consumer workflows see no observable change.
216
+ - **Repos that hand-coded a glob path** should switch to the
217
+ directory shape (or — better — replace the hard-coded value with
218
+ the matrix field):
219
+
220
+ ```diff
221
+ - uses: actions/upload-artifact@v4
222
+ with:
223
+ name: ${{ matrix.artifact_name }}
224
+ - path: packages/python/dist/*.tar.gz
225
+ + path: ${{ matrix.artifact_path }} # or "packages/python/dist"
226
+ ```
227
+
228
+ The recursive reader keeps glob layouts working as a safety net,
229
+ but the directory shape is the canonical contract going forward.
230
+
231
+ **Deprecations removed.** None.
232
+
233
+ **Behavior changes without code changes.**
234
+
235
+ - Artifact directory layout is now flat: `artifacts/<name>/<file>`
236
+ instead of `artifacts/<name>/<workspace-relative-path>/<file>`.
237
+ Anything reading the artifact tree (the docs page, debugging
238
+ scripts, custom verification jobs) should expect files at the
239
+ artifact root.
240
+ - The publish-side handlers now walk subdirectories recursively
241
+ when looking for `.whl` / `.tar.gz` / `.crate` files. This is
242
+ defensive for consumers whose build steps write to a non-standard
243
+ location inside `<name>/`; the planner's directory `artifact_path`
244
+ remains the canonical contract.
245
+
246
+ **Verification.**
247
+
248
+ ```sh
249
+ putitoutthere plan --json | jq '.[] | {name, artifact_name, artifact_path}'
250
+ ```
251
+
252
+ Expect every `artifact_path` to be a plain directory (no `*`):
253
+
254
+ ```json
255
+ { "name": "py/cachetta", "artifact_name": "py__cachetta-sdist", "artifact_path": "py/cachetta/dist" }
256
+ ```
257
+
258
+ After the next release run, the `actions/upload-artifact@v4` step
259
+ uploads `py/cachetta/dist/` contents flat under
260
+ `artifacts/py__cachetta-sdist/` (no nested `packages/python/dist/`
261
+ prefix), and the publish step finds the sdist immediately.
262
+
263
+ ### Scaffolded `release.yml` now forwards `GITHUB_TOKEN`
264
+
265
+ **Summary.** piot has supported cutting a GitHub Release alongside each
266
+ tag push since #26, but the scaffolded `release.yml` template never
267
+ forwarded `GITHUB_TOKEN` to the publish step. GitHub Actions does not
268
+ auto-mount the runner token as an env var — `permissions: contents:
269
+ write` only grants the token *scope* to write Releases; the token still
270
+ has to be exposed via `env:` for piot's `release.ts` to read it from
271
+ `process.env.GITHUB_TOKEN`. Without it, piot silent-skipped Release
272
+ creation and consumers got tags but no Release entries on the repo's
273
+ Releases page. Fresh `piot init` runs now scaffold the env line.
274
+
275
+ **Required changes.** Existing repos that ran `piot init` before this
276
+ change need a one-line addition to `.github/workflows/release.yml`:
277
+
278
+ ```diff
279
+ - uses: thekevinscott/putitoutthere@v0
280
+ with:
281
+ command: publish
282
+ dry_run: ${{ inputs.dry_run || 'false' }}
283
+ env:
284
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
285
+ CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_TOKEN }}
286
+ PYPI_API_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
287
+ + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
288
+ ```
289
+
290
+ The publish job already declares `permissions: contents: write`, which
291
+ is the scope GitHub's runner-supplied `GITHUB_TOKEN` needs to create
292
+ Releases — no additional permission changes required.
293
+
294
+ **Deprecations removed.** None.
295
+
296
+ **Behavior changes without code changes.** Repos that adopt the new
297
+ template (or apply the diff above) start seeing GitHub Release entries
298
+ appear under the repo's `/releases` page after each publish. The
299
+ Release body is the output of:
300
+
301
+ ```sh
302
+ git log <prev-tag>..<this-tag> --format='- %s' --no-merges
303
+ ```
304
+
305
+ Tags suffixed with `-rc`, `-beta`, or `-alpha` are flagged
306
+ `prerelease: true`. Release creation is best-effort: a 4xx/5xx from
307
+ the GitHub API surfaces as a `publish: GitHub Release creation
308
+ failed` warning but does not fail the publish run — the registry
309
+ publish and tag push remain authoritative.
310
+
311
+ **Verification.** After the next release run on a repo that adopted the
312
+ fix:
313
+
314
+ ```bash
315
+ # Inspect the publish job log:
316
+ # "publish: GitHub Release created at https://github.com/.../releases/tag/<name>-v<x.y.z>"
317
+
318
+ # Or hit the API directly:
319
+ gh release view <name>-v<x.y.z> --repo <owner>/<repo>
320
+ ```
321
+
322
+ If you previously saw the warning `publish: GitHub Release creation
323
+ failed` in your publish logs, the warning should be gone and the
324
+ Releases page should populate.
325
+
326
+ ### Package names with `/` no longer need an encode/decode workaround
327
+
328
+ **Summary.** Polyglot-monorepo repos that group packages by language
329
+ (e.g. `name = "py/foo"`, `"js/bar"`) used to fail at the build job
330
+ with:
331
+
332
+ ```
333
+ The artifact name is not valid: py/foo-sdist.
334
+ Contains the following character: Forward slash /
335
+ ```
336
+
337
+ …because `actions/upload-artifact@v4` forbids `/` in artifact names
338
+ and the planner emitted `artifact_name` verbatim from `pkg.name`
339
+ ([#230](https://github.com/thekevinscott/putitoutthere/issues/230)).
340
+ The planner now encodes each `/` to `__`
341
+ (`py/foo` → `py__foo-sdist`), so the build job's
342
+ upload-artifact step works without modification — pass the matrix
343
+ `artifact_name` field through verbatim and the encoding happens
344
+ upstream.
345
+
346
+ **Required changes.**
347
+
348
+ - **None for repos with slash-free `pkg.name`** — `artifact_name`
349
+ is byte-identical to the previous version.
350
+ - **Repos that ran the [`cachetta#26`-style](https://github.com/thekevinscott/cachetta/pull/26)
351
+ encode/decode workaround should remove it.** The planner now
352
+ does the encoding natively; leaving the workaround in place
353
+ produces double-encoded names like `py____foo-sdist`, which the
354
+ publish-side reader will treat as a missing artifact.
355
+
356
+ ```diff
357
+ - uses: actions/upload-artifact@v4
358
+ with:
359
+ - name: ${{ format('{0}', matrix.artifact_name) }} # any sed/format encode
360
+ - path: ${{ matrix.artifact_path }}
361
+ + name: ${{ matrix.artifact_name }} # use the field as-is
362
+ + path: ${{ matrix.artifact_path }}
363
+ ```
364
+
365
+ ```diff
366
+ - uses: actions/download-artifact@v4
367
+ with:
368
+ path: artifacts
369
+ - - name: Decode artifact dir names
370
+ - run: |
371
+ - # rename artifacts/py__foo-sdist back to artifacts/py/foo-sdist
372
+ - ...
373
+ ```
374
+
375
+ **Deprecations removed.** None.
376
+
377
+ **Behavior changes without code changes.**
378
+
379
+ - `pkg.name` containing `__` (the new encoding sequence) is now
380
+ rejected at config load with: `package name must not contain "__"
381
+ (reserved: piot encodes "/" to "__" for artifact-name slots; pick
382
+ a different separator)`. If your config uses `__` in a package
383
+ name today, rename to use `-` or `_` and update any tags / consumer
384
+ references; piot can't safely sanitize it without ambiguity.
385
+ - `pkg.name` containing `\`, `:`, `<`, `>`, `|`, `*`, `?`, or `"`
386
+ is now rejected at config load. None of these are valid in npm,
387
+ PyPI, or crates.io names, so any config that previously contained
388
+ them was already broken at publish time — the change just moves
389
+ the failure earlier with a clearer message.
390
+
391
+ **Verification.**
392
+
393
+ ```sh
394
+ putitoutthere plan --json | jq '.[].artifact_name'
395
+ ```
396
+
397
+ Expect every emitted `artifact_name` to contain only ASCII letters,
398
+ digits, `-`, `_`, and `.` — no `/` and no other forbidden chars.
399
+ For a repo with `name = "py/cachetta"`:
400
+
401
+ ```
402
+ "py__cachetta-sdist"
403
+ "py__cachetta-wheel-x86_64-unknown-linux-gnu"
404
+ ```
405
+
406
+ After the next release, the build job's `actions/upload-artifact@v4`
407
+ step uploads under `py__cachetta-sdist/` (a single flat directory
408
+ under `artifacts/`), and piot's publish-side reader consumes the
409
+ same path.
410
+
411
+ ### Documentation accuracy pass (#231)
412
+
413
+ **Summary.** A docs-vs-code audit found several places where reference
414
+ material lagged behind shipped behavior. Existing configs and workflows
415
+ keep working — the only consumer-observable change is that `putitoutthere
416
+ --help` no longer mislabels `--json` as "plan only".
417
+
418
+ **Required changes.** None.
419
+
420
+ **Deprecations removed.** None.
421
+
422
+ **Behavior changes without code changes.**
423
+
424
+ - `putitoutthere --help` output: the `--json` line now reads `emit
425
+ machine-readable output (most commands)` instead of `(plan only)`. The
426
+ flag has always been accepted on every command that emits a result;
427
+ only the help text was wrong.
428
+ - No other behavior changes. All other audit findings were addressed by
429
+ updating documentation (`docs/api/cli.md`, `docs/api/action.md`,
430
+ `docs/guide/configuration.md`, `docs/guide/trailer.md`, `README.md`,
431
+ `action.yml` description text, VitePress sidebar).
432
+
433
+ **Verification.**
434
+
435
+ ```sh
436
+ putitoutthere --help | grep -- '--json'
437
+ # Expected: --json emit machine-readable output (most commands)
438
+ ```
439
+
440
+ ### Python shape examples now use `uv build`
441
+
442
+ **Summary.** Documentation examples for the Python library, Python
443
+ cibuildwheel, and dynamic-versions shapes switched the sdist-build
444
+ step from `python -m build --sdist` to `uv build --sdist`. piot's
445
+ contract is unchanged — backends, artifact names, the
446
+ `matrix.artifact_name` / `matrix.artifact_path` fields, and the
447
+ publish-side completeness check all work identically. The change
448
+ removes a `pip install build` round-trip and aligns the docs with
449
+ `uv` as the recommended Python toolchain.
450
+
451
+ **Required changes.** None. `python -m build` still works. To
452
+ follow the new examples in your own `release.yml`:
453
+
454
+ ```diff
455
+ build:
456
+ ...
457
+ steps:
458
+ - - uses: actions/setup-python@v5
459
+ - with: { python-version: '3.12' }
460
+ - name: Build sdist
461
+ - run: |
462
+ - cd ${{ matrix.path }}
463
+ - python -m pip install build
464
+ - python -m build --sdist --outdir dist
465
+ + working-directory: ${{ matrix.path }}
466
+ + run: uv build --sdist
467
+ + # uv installs and manages Python itself; no setup-python step needed.
468
+ + # Add this once at the top of the build job:
469
+ + - uses: astral-sh/setup-uv@v3
470
+ ```
471
+
472
+ `uv build --sdist` writes to `dist/` inside the working directory
473
+ (same as `python -m build --outdir dist`), so
474
+ `matrix.artifact_path` keeps pointing at the right place. The
475
+ publish job is unchanged — `setup-python` + `pip install twine` is
476
+ still the recommended path there because piot's PyPI handler shells
477
+ out to `twine`.
478
+
479
+ **When *not* to follow this example.** Stay on `python -m build`
480
+ if:
481
+
482
+ - Your CI image already has Python pre-installed and adding
483
+ `setup-uv` would slow the cold cache.
484
+ - Your `pyproject.toml` exercises a build backend feature that uv's
485
+ isolated build environment doesn't yet handle (rare; uv's build
486
+ isolation matches `python -m build`'s).
487
+ - Your team's runbook standardises on `python -m build` and the
488
+ consistency cost of switching outweighs the per-run speedup.
489
+
490
+ `python -m build` is not deprecated and will keep working.
491
+
492
+ **Deprecations removed.** None.
493
+
494
+ **Behavior changes without code changes.** None.
495
+
496
+ **Verification.**
497
+
498
+ ```bash
499
+ # After the build job runs:
500
+ ls artifacts/<pkg.name>-sdist/
501
+ # Expected: <pypi-name>-X.Y.Z.tar.gz (no .devN suffix)
502
+ ```
503
+
504
+ If you see the expected sdist, the switch worked. If you see a
505
+ `.devN` suffix, your project uses dynamic versioning — see
506
+ [dynamic versions](https://thekevinscott.github.io/putitoutthere/guide/dynamic-versions)
507
+ for the env-var handoff (unchanged by this migration).
508
+
509
+ ### Repository renamed `put-it-out-there` → `putitoutthere`
510
+
511
+ **Summary.** The GitHub repository slug collapsed from `put-it-out-there`
512
+ to `putitoutthere`, matching the npm package and CLI binary name. The
513
+ human-readable name "Put It Out There" (with spaces) is unchanged. GitHub
514
+ auto-redirects the old URL, but any place a consumer has hard-coded the
515
+ old slug — npm/Cargo/pyproject `repository` URLs, GitHub Actions
516
+ references, OIDC trust policy `repository:` claims, docs links — should
517
+ be updated.
518
+
519
+ **Required changes.**
520
+
521
+ ```diff
522
+ # package.json (or Cargo.toml / pyproject.toml)
523
+ -"repository": "https://github.com/<owner>/put-it-out-there"
524
+ +"repository": "https://github.com/<owner>/putitoutthere"
525
+ ```
526
+
527
+ ```diff
528
+ # .github/workflows/release.yml — if you reference the action by full repo path
529
+ -uses: thekevinscott/put-it-out-there/.github/actions/<...>
530
+ +uses: thekevinscott/putitoutthere/.github/actions/<...>
531
+ ```
532
+
533
+ ```diff
534
+ # OIDC trust policies (PyPI, npm) that gate on the source repo
535
+ -"repository": "<owner>/put-it-out-there"
536
+ +"repository": "<owner>/putitoutthere"
537
+ ```
538
+
539
+ If you only ever invoked `putitoutthere` via the npm package
540
+ (`npx putitoutthere`, `pnpm add -D putitoutthere`) or the published
541
+ GitHub Action, no change is required — those references already used the
542
+ collapsed name.
543
+
544
+ **Deprecations removed.** None. The old slug continues to redirect at
545
+ the GitHub layer.
546
+
547
+ **Behavior changes without code changes.**
548
+
549
+ - Documentation site moved from
550
+ `https://thekevinscott.github.io/put-it-out-there/` to
551
+ `https://thekevinscott.github.io/putitoutthere/`. The old URL
552
+ redirects.
553
+ - `git remote -v` will still show the old URL until you `git remote
554
+ set-url origin https://github.com/thekevinscott/putitoutthere.git`.
555
+ Push and fetch keep working via redirect, but updating the remote
556
+ avoids surprise breakage if the redirect is ever retired.
557
+
558
+ **Verification.**
559
+
560
+ ```sh
561
+ # Confirm no stale references in your repo
562
+ grep -r "put-it-out-there" .
563
+ ```
564
+
565
+ Expect no hits outside historical changelog/migration entries.
566
+
567
+ ### `[package.bundle_cli]` — stage a Rust CLI into every maturin wheel (#217)
568
+
569
+ **Summary.** New optional sub-table under `[[package]]` for pypi packages
570
+ that want the `ruff` / `uv` / `pydantic-core` wheel shape: a companion
571
+ Rust CLI binary, cross-compiled per target and staged into the Python
572
+ source tree before maturin runs, so each wheel ships the binary as
573
+ package data and `pip install <pkg>` gets a working CLI on `PATH` with
574
+ no Rust toolchain on the user's machine. Additive — existing
575
+ configurations are unchanged.
576
+
577
+ **Required changes.** None for existing configs. To opt in:
578
+
579
+ ```diff
580
+ [[package]]
581
+ name = "my-py"
582
+ kind = "pypi"
583
+ build = "maturin"
584
+ path = "packages/python"
585
+ globs = ["packages/python/**"]
586
+ targets = ["x86_64-unknown-linux-gnu", "aarch64-apple-darwin"]
587
+ +
588
+ +[package.bundle_cli]
589
+ +bin = "my-cli"
590
+ +stage_to = "src/my_py/_binary"
591
+ +crate_path = "crates/my-rust" # defaults to "." (repo workspace root)
592
+ ```
593
+
594
+ And in the Python package's `pyproject.toml`:
595
+
596
+ ```diff
597
+ +[project.scripts]
598
+ +my-cli = "my_py._binary:entrypoint" # small os.execv launcher stub
599
+ +
600
+ [tool.maturin]
601
+ -include = ["..."]
602
+ +include = ["...", "src/my_py/_binary/**"] # ship the staged binary
603
+ ```
604
+
605
+ See [README → Rust CLI inside a PyPI wheel](https://github.com/thekevinscott/putitoutthere/blob/main/README.md#rust-cli-inside-a-pypi-wheel)
606
+ for the full worked example including the launcher stub.
607
+
608
+ **Deprecations removed.** None.
609
+
610
+ **Behavior changes without code changes.** None for existing configs.
611
+ Packages that declare `[package.bundle_cli]` get two new steps emitted
612
+ in the scaffolded build job (`Setup Rust (if pypi bundle_cli)` +
613
+ `Build + stage bundled CLI`), both gated on
614
+ `matrix.kind == 'pypi' && matrix.bundle_cli.bin != '' && matrix.target != 'sdist'`
615
+ so packages without the block see no change.
616
+
617
+ **Verification.** For a repo that opts in:
618
+
619
+ ```bash
620
+ # After piot's build job runs on one target:
621
+ ls packages/python/src/my_py/_binary/
622
+ # Expected: my-cli (or my-cli.exe on Windows targets)
623
+
624
+ # After the wheel is built:
625
+ python -m zipfile -l packages/python/dist/*.whl | grep _binary
626
+ # Expected: one entry per target listing the staged binary.
627
+
628
+ # End-to-end on a released wheel:
629
+ pip install my-py==<published-version>
630
+ which my-cli
631
+ my-cli --version
632
+ ```