putitoutthere 0.1.42 → 0.1.44

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