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/README.md CHANGED
@@ -1,155 +1,403 @@
1
1
  # Put It Out There
2
2
 
3
- Polyglot release orchestrator for single-maintainer, LLM-authored projects
4
- that publish to crates.io, PyPI, and npm from one monorepo. One config file,
5
- one CLI, one trailer-driven signal — no per-package release plumbing.
3
+ A reusable GitHub Actions workflow that publishes packages to crates.io, PyPI,
4
+ and npm from one repo. OIDC-first, cascade-aware, polyglot. The consumer
5
+ surface is one config file plus ~10 lines of YAML calling
6
+ `uses: thekevinscott/putitoutthere/.github/workflows/release.yml@v0`.
6
7
 
7
- **[Documentation](https://thekevinscott.github.io/put-it-out-there/)**
8
+ ## Quickstart
8
9
 
9
- ## Install
10
+ ### 1. Drop in `.github/workflows/release.yml`
10
11
 
11
- ```sh
12
- npx putitoutthere init
12
+ ```yaml
13
+ name: Release
14
+
15
+ on:
16
+ push:
17
+ branches: [main]
18
+
19
+ jobs:
20
+ release:
21
+ uses: thekevinscott/putitoutthere/.github/workflows/release.yml@v0
22
+ permissions:
23
+ contents: write
24
+ id-token: write
13
25
  ```
14
26
 
15
- This scaffolds a `putitoutthere.toml`, a `release.yml` workflow, and an
16
- `AGENTS.md` file explaining the trailer convention to future
17
- contributors.
27
+ Pinned action versions, `plan → build → publish` orchestration, and GitHub
28
+ Release creation all live inside the reusable workflow.
18
29
 
19
- For one-off runs without scaffolding:
30
+ Optional inputs — `with:` block at the call site:
20
31
 
21
- ```sh
22
- pnpm add -D putitoutthere
23
- pnpm putitoutthere plan
24
- ```
32
+ | Input | Default | Use when |
33
+ |------------------|--------------|--------------------------------------------------------------------------|
34
+ | `environment` | `release` | Your GitHub deployment environment is named differently. |
35
+ | `node_version` | `24` | You need a specific Node version for `kind = "npm"` build steps. |
36
+ | `python_version` | `3.12` | You need a specific Python version for `kind = "pypi"` build steps. |
25
37
 
26
- ## Minimum config
38
+ ### 2. Drop in `putitoutthere.toml`
27
39
 
28
40
  ```toml
29
- # putitoutthere.toml
30
41
  [putitoutthere]
31
42
  version = 1
32
43
 
33
44
  [[package]]
34
45
  name = "my-lib"
35
- kind = "npm" # or "pypi" | "crates"
46
+ kind = "pypi" # or "npm" | "crates"
36
47
  path = "."
37
- paths = ["src/**", "package.json"]
48
+ globs = ["src/**", "pyproject.toml"]
49
+ build = "hatch" # required for kind = "pypi"
50
+ tag_format = "v{version}" # single-package repos often want this
38
51
  ```
39
52
 
40
- `paths` are the globs that trigger a release for this package. Any commit
41
- touching a matching file makes the package a candidate.
53
+ `globs` are the path globs that trigger a release. Any commit touching a
54
+ matching file makes the package a candidate.
55
+
56
+ More config patterns are in [Configuration](#configuration) below.
57
+
58
+ ### 3. Register trusted publishers
42
59
 
43
- ## Releasing
60
+ Each registry needs a one-time external setup so OIDC publishes work. See
61
+ [Trusted publishers](#trusted-publishers) below — three short lists, one per
62
+ registry.
44
63
 
45
- Add a trailer to the commit that should ship:
64
+ ### 4. Push a release
65
+
66
+ Merge to `main`. Default behavior: any package whose `globs` matched changed
67
+ files cascades and ships at `patch`. To bump `minor` or `major`:
46
68
 
47
69
  ```
48
70
  fix: handle empty token lists
49
71
 
50
- release: patch
72
+ release: minor
51
73
  ```
52
74
 
53
- Valid bumps: `patch`, `minor`, `major`, `skip`. On push to `main`, the
54
- `release.yml` workflow runs `putitoutthere plan` against the trailer +
55
- changed paths, then `putitoutthere publish` per matching package. Tags are
56
- `{name}-v{version}`.
75
+ …in the merge commit body. See [Trailer](#trailer) below.
57
76
 
58
- The trailer is optional. By default, any package whose `paths` matched
59
- changed files cascades at `patch`. Use `release: minor` / `release: major`
60
- to override the bump, or `release: skip` to suppress the release for that
61
- commit.
77
+ ## Configuration
62
78
 
63
- ## Worked example: polyglot
79
+ `putitoutthere.toml` lives at the repo root.
64
80
 
65
- The reference fixture at [`test/fixtures/polyglot-everything/`](./test/fixtures/polyglot-everything/)
66
- mirrors a real polyglot shape: a Rust crate, a PyO3 Python wheel wrapping it,
67
- and an npm CLI bundling the Rust binary. One `putitoutthere.toml` declares
68
- all three:
81
+ ### `[putitoutthere]`
82
+
83
+ ```toml
84
+ [putitoutthere]
85
+ version = 1 # required; only 1 is valid today
86
+ ```
87
+
88
+ ### `[[package]]` (one per releasable unit)
89
+
90
+ | Field | Type | Required | Notes |
91
+ |-----------------|----------|----------|---------------------------------------------------|
92
+ | `name` | string | yes | Unique across the config. |
93
+ | `kind` | enum | yes | `crates` \| `pypi` \| `npm`. |
94
+ | `path` | string | yes | Package working dir (`Cargo.toml` / `pyproject.toml` / `package.json` location). |
95
+ | `globs` | string[] | yes | Path globs that cascade this package. |
96
+ | `depends_on` | string[] | no | Package names this one cascades on top of. |
97
+ | `first_version` | string | no | Default `0.1.0`. |
98
+ | `tag_format` | string | no | Template for the git tag. Default `"{name}-v{version}"`. Single-package repos often want `"v{version}"`. |
99
+
100
+ ### `kind = "crates"`
101
+
102
+ | Field | Type | Notes |
103
+ |-----------------------|----------|------------------------------------------------------------|
104
+ | `crate` | string | Override `name` → crates.io name. |
105
+ | `features` | string[] | Pass through to `cargo publish --features`. |
106
+ | `no_default_features` | bool | Pass `--no-default-features` to `cargo publish` when true. |
107
+
108
+ ### `kind = "pypi"`
109
+
110
+ | Field | Type | Notes |
111
+ |--------------|------------------------|----------------------------------------------------|
112
+ | `pypi` | string | Override `name` → PyPI registered name. |
113
+ | `build` | enum | `maturin` \| `setuptools` \| `hatch`. Required. |
114
+ | `targets` | (string \| object)[] | Required when `build = "maturin"`. Triples or `{ triple, runner }` objects. |
115
+ | `bundle_cli` | table | Opt-in: cross-compile a Rust CLI per target and stage it into each wheel. Only valid with `build = "maturin"`. See [Recipes → Rust CLI inside a PyPI wheel](#rust-cli-inside-a-pypi-wheel). |
116
+
117
+ ### `kind = "npm"`
118
+
119
+ | Field | Type | Notes |
120
+ |-----------|------------------------|------------------------------------------------------|
121
+ | `npm` | string | Override `name` → npm name (for scoped packages). |
122
+ | `access` | enum | `public` \| `restricted`. Default `public`. |
123
+ | `tag` | string | dist-tag. Default `latest`. |
124
+ | `build` | enum | `napi` \| `bundled-cli`. Omitted = vanilla. See [Recipes → Bundled-CLI npm family](#bundled-cli-npm-family). |
125
+ | `targets` | (string \| object)[] | Required when `build ∈ {napi, bundled-cli}`. |
126
+
127
+ ### Example: polyglot Rust library
128
+
129
+ One Rust crate feeds three artifacts:
69
130
 
70
131
  ```toml
71
132
  [[package]]
72
- name = "my-tool-rust"
133
+ name = "my-rust"
73
134
  kind = "crates"
74
- path = "packages/rust"
75
- paths = ["packages/rust/**"]
135
+ path = "crates/my-rust"
136
+ globs = ["crates/my-rust/**"]
76
137
 
77
138
  [[package]]
78
- name = "my-tool-python"
79
- kind = "pypi"
80
- path = "packages/python"
81
- paths = ["packages/python/**"]
82
- build = "maturin"
83
- depends_on = ["my-tool-rust"]
139
+ name = "my-py"
140
+ kind = "pypi"
141
+ path = "py/my-py"
142
+ globs = ["py/my-py/**"]
143
+ build = "maturin"
144
+ targets = ["x86_64-unknown-linux-gnu", "aarch64-apple-darwin"]
145
+ depends_on = ["my-rust"]
84
146
 
85
147
  [[package]]
86
- name = "my-tool-cli"
87
- kind = "npm"
88
- path = "packages/ts"
89
- paths = ["packages/ts/**"]
90
- build = "bundled-cli"
91
- depends_on = ["my-tool-rust"]
148
+ name = "my-cli"
149
+ kind = "npm"
150
+ path = "packages/ts"
151
+ globs = ["packages/ts/**"]
152
+ build = "bundled-cli"
153
+ targets = ["x86_64-unknown-linux-gnu", "aarch64-apple-darwin"]
154
+ depends_on = ["my-rust"]
92
155
  ```
93
156
 
94
- A change to `packages/rust/` cascades: the crate ships, and the Python + npm
95
- wrappers ship on top (bumped to match). A change to only the TS shim ships
96
- just the npm package.
157
+ A change to `crates/my-rust/` cascades: the crate ships, then the Python
158
+ wheels and npm family ship on top, version-bumped to match.
97
159
 
98
- ## Library shapes
160
+ ### Example: multi-package workspace
99
161
 
100
- End-to-end walkthroughs — config + `release.yml` + prerequisites + gotchas —
101
- for the common shapes. Pick the one that matches your repo:
162
+ ```toml
163
+ [[package]]
164
+ name = "@my/core"
165
+ kind = "npm"
166
+ path = "packages/core"
167
+ globs = ["packages/core/**"]
102
168
 
103
- **Single-package**
169
+ [[package]]
170
+ name = "@my/parser"
171
+ kind = "npm"
172
+ path = "packages/parser"
173
+ globs = ["packages/parser/**"]
174
+ depends_on = ["@my/core"]
175
+ ```
104
176
 
105
- - [Python library](./docs/guide/shapes/python-library.md) — one `pyproject.toml` to PyPI
106
- - [npm library](./docs/guide/shapes/npm-library.md) — one `package.json` to npm
107
- - [Rust crate](./docs/guide/shapes/rust-crate.md) — one `Cargo.toml` to crates.io
177
+ ## Trailer
108
178
 
109
- **Multi-package workspaces**
179
+ The trailer is **optional**. Default behavior is `patch` whenever a package's
180
+ `globs` matched changed files.
110
181
 
111
- - [Rust workspace](./docs/guide/shapes/rust-workspace.md) — multiple crates with `depends_on` cascade
112
- - [npm workspace](./docs/guide/shapes/npm-workspace.md) — multiple npm packages, shared dependency graph
182
+ Grammar:
113
183
 
114
- **Rust core, multi-registry**
184
+ ```
185
+ release: <bump> [pkg1, pkg2, ...]
186
+ ```
187
+
188
+ `<bump>` is `patch` | `minor` | `major` | `skip`. The optional package list
189
+ scopes a non-default bump to specific packages.
190
+
191
+ | Trailer | Effect |
192
+ |--------------------------|------------------------------------------------------------------------|
193
+ | *(none)* | Cascaded packages bump `patch`. |
194
+ | `release: minor` | Cascaded packages bump `minor`. |
195
+ | `release: major` | Cascaded packages bump `major`. |
196
+ | `release: skip` | No release this commit. Cascade ignored. |
197
+ | `release: minor [a, b]` | `a` and `b` bump `minor`; other cascaded packages stay at `patch`. |
198
+
199
+ The trailer matches anywhere in the commit body. If multiple `release:` lines
200
+ are present, the **last** one wins.
115
201
 
116
- - [Rust + PyO3 wheels](./docs/guide/shapes/rust-pyo3.md) — crate + PyPI (no napi)
117
- - [Rust + napi npm](./docs/guide/shapes/rust-napi.md) — crate + npm family (no PyPI)
118
- - [Polyglot Rust library](./docs/guide/shapes/polyglot-rust.md) — all three registries from one core
119
- - [Python wheels with C extensions](./docs/guide/shapes/python-cibuildwheel.md) — `cibuildwheel` for the `pillow`/`lxml`/`numpy` shape
202
+ ## Cascade
120
203
 
121
- **Distribution patterns**
204
+ A package cascades into the release plan when a commit changes any file
205
+ matching one of its `globs` since its last tag. If another package
206
+ declares `depends_on = ["this-package"]`, that package also cascades.
207
+ Transitively, DFS-ordered, with cycle detection at config-load.
122
208
 
123
- - [Bundled-CLI npm family](./docs/guide/shapes/bundled-cli.md) — compiled CLI shipped as an npm per-platform family
124
- - [Dual-family npm (CLI + napi)](./docs/guide/shapes/dual-family-npm.md) — one library with both an addon and a binary
209
+ Inside a single release, packages publish in topological order of their
210
+ `depends_on` graph. If your Python wrapper depends on a Rust crate, the
211
+ crate publishes first.
125
212
 
126
- Full index at [`docs/guide/shapes/`](./docs/guide/shapes/).
213
+ Each handler's first move on publish is `isPublished` — check the registry
214
+ for the target version. Already there → skip cleanly. Lets you re-run failed
215
+ releases without fighting registry-immutable-publish semantics.
127
216
 
128
217
  ## Trusted publishers
129
218
 
130
- Preferred over long-lived tokens. One-time setup per registry:
219
+ OIDC trusted publishers — the only auth path supported by the reusable
220
+ workflow. Long-lived registry tokens are not reachable through the workflow.
221
+
222
+ The fields each registry needs are the same: repository owner/name, workflow
223
+ filename (`release.yml`), and optionally a GitHub environment name.
224
+
225
+ ### crates.io
226
+
227
+ 1. Publish your crate once through the normal `cargo` flow so the crate
228
+ exists. (Trusted publishing needs a crate owner record.)
229
+ 2. Go to `https://crates.io/crates/<crate>/settings` → **Trusted Publishing**
230
+ → **Add**.
231
+ 3. Fill in: repository owner, repository name, workflow filename
232
+ (`release.yml`), environment (optional).
233
+
234
+ ### PyPI
131
235
 
132
- - **npm:** [npm trusted publishing](https://docs.npmjs.com/trusted-publishers) — attach the package to this repo + workflow. `--provenance` is added automatically.
133
- - **PyPI:** [pending publisher](https://docs.pypi.org/trusted-publishers/) — register the project name pointing at this repo's `release.yml`.
134
- - **crates.io:** [OIDC via `rust-lang/crates-io-auth-action@v1`](https://github.com/rust-lang/crates-io-auth-action) — the crate needs one manual bootstrap publish first.
236
+ 1. Go to `https://pypi.org/manage/project/<name>/settings/publishing/` (or
237
+ **Publishing** on the project page).
238
+ 2. Add a **GitHub** trusted publisher: owner, repo, workflow filename,
239
+ environment (optional).
240
+ 3. Brand-new project? Use a [pending publisher](https://docs.pypi.org/trusted-publishers/creating-a-project-through-oidc/)
241
+ to skip the bootstrap token.
242
+
243
+ ### npm
244
+
245
+ 1. Publish at least one version of your package with a classic
246
+ `NODE_AUTH_TOKEN` so the package exists on the registry. (npm's trusted
247
+ publisher requires an existing package.)
248
+ 2. Go to `https://www.npmjs.com/package/<name>/access` → **Require trusted
249
+ publisher**.
250
+ 3. Fill in: repository, workflow filename, environment (optional).
251
+ 4. Delete the bootstrap token.
252
+
253
+ ## Recipes
254
+
255
+ ### Bundled-CLI npm family
256
+
257
+ Ship a compiled CLI as an npm per-platform family — `npm install -g my-cli`
258
+ gives users a working binary on PATH. The `esbuild` / `biome` distribution
259
+ shape.
260
+
261
+ Config:
262
+
263
+ ```toml
264
+ [[package]]
265
+ name = "my-cli"
266
+ kind = "npm"
267
+ npm = "my-cli"
268
+ build = "bundled-cli"
269
+ path = "packages/ts-cli"
270
+ globs = ["packages/ts-cli/**", "crates/my-cli/**"]
271
+ targets = [
272
+ "x86_64-unknown-linux-gnu",
273
+ "aarch64-unknown-linux-gnu",
274
+ "x86_64-apple-darwin",
275
+ "aarch64-apple-darwin",
276
+ "x86_64-pc-windows-msvc",
277
+ ]
278
+ ```
279
+
280
+ The engine publishes a per-platform sub-package per target
281
+ (`my-cli-<triple>`) plus a top-level whose `optionalDependencies` pin them
282
+ at the published version. npm's resolver installs exactly one sub-package
283
+ at consumer install time.
284
+
285
+ You author the launcher script that picks the right per-platform binary
286
+ once. `package.json`:
287
+
288
+ ```json
289
+ {
290
+ "name": "my-cli",
291
+ "bin": { "my-cli": "bin/my-cli.js" }
292
+ }
293
+ ```
294
+
295
+ `bin/my-cli.js`:
296
+
297
+ ```js
298
+ #!/usr/bin/env node
299
+ const { spawnSync } = require('node:child_process');
300
+ const { platform, arch } = process;
301
+
302
+ const triples = {
303
+ 'linux-x64': 'x86_64-unknown-linux-gnu',
304
+ 'linux-arm64': 'aarch64-unknown-linux-gnu',
305
+ 'darwin-x64': 'x86_64-apple-darwin',
306
+ 'darwin-arm64': 'aarch64-apple-darwin',
307
+ 'win32-x64': 'x86_64-pc-windows-msvc',
308
+ };
309
+
310
+ const triple = triples[`${platform}-${arch}`];
311
+ if (!triple) {
312
+ console.error(`my-cli: unsupported platform ${platform}-${arch}`);
313
+ process.exit(1);
314
+ }
315
+
316
+ const pkg = `my-cli-${triple}`;
317
+ const binary = require.resolve(`${pkg}/bin/my-cli${platform === 'win32' ? '.exe' : ''}`);
318
+ const result = spawnSync(binary, process.argv.slice(2), { stdio: 'inherit' });
319
+ process.exit(result.status ?? 1);
320
+ ```
321
+
322
+ Each per-platform sub-package needs its own npm trusted-publisher
323
+ registration (a policy on `my-cli` does not cover
324
+ `my-cli-x86_64-unknown-linux-gnu`).
325
+
326
+ ### Rust CLI inside a PyPI wheel
327
+
328
+ `pip install my-lib` on any platform gets a working CLI on `PATH` without
329
+ the user installing a Rust toolchain. The `ruff` / `uv` / `pydantic-core`
330
+ pattern.
331
+
332
+ Config:
333
+
334
+ ```toml
335
+ [[package]]
336
+ name = "my-py"
337
+ kind = "pypi"
338
+ build = "maturin"
339
+ path = "packages/python"
340
+ globs = ["packages/python/**", "crates/my-rust/**"]
341
+ targets = [
342
+ "x86_64-unknown-linux-gnu",
343
+ "aarch64-unknown-linux-gnu",
344
+ "x86_64-apple-darwin",
345
+ "aarch64-apple-darwin",
346
+ "x86_64-pc-windows-msvc",
347
+ ]
348
+ depends_on = ["my-rust"]
349
+
350
+ [package.bundle_cli]
351
+ bin = "my-cli"
352
+ stage_to = "src/my_py/_binary"
353
+ crate_path = "crates/my-rust"
354
+ ```
355
+
356
+ The reusable workflow cross-compiles the binary per target and stages it
357
+ into the package source tree before maturin runs. Your `pyproject.toml`
358
+ ties the staged binary into a `console_scripts` entry:
359
+
360
+ ```toml
361
+ [project.scripts]
362
+ my-cli = "my_py._binary:entrypoint"
363
+
364
+ [tool.maturin]
365
+ include = ["src/my_py/_binary/**"]
366
+ ```
367
+
368
+ Launcher in `packages/python/src/my_py/_binary/__init__.py`:
369
+
370
+ ```python
371
+ import os, sys
372
+ from pathlib import Path
373
+
374
+ def entrypoint():
375
+ here = Path(__file__).parent
376
+ binary = here / ("my-cli.exe" if os.name == "nt" else "my-cli")
377
+ if not binary.exists():
378
+ sys.stderr.write(f"my-cli binary not found at {binary}\n")
379
+ sys.exit(1)
380
+ os.execv(binary, [str(binary), *sys.argv[1:]])
381
+ ```
135
382
 
136
- Token fallbacks (`NPM_TOKEN`, `PYPI_API_TOKEN`, `CARGO_REGISTRY_TOKEN`) are
137
- still read as env vars if OIDC isn't available. `putitoutthere doctor`
138
- reports which path is active.
383
+ ## Dynamic-version PyPI gotcha
139
384
 
140
- ## What it is not
385
+ If your `pyproject.toml` uses `[project].dynamic = ["version"]` with
386
+ `hatch-vcs` or `setuptools-scm`, the build backend derives the version from
387
+ the latest git tag at build time — which is still the **previous** release
388
+ when the build runs. Without a handoff, the sdist ships as
389
+ `<pkg>-X.Y.Z.devN.tar.gz` instead of `<pkg>-X.Y.Z.tar.gz`.
141
390
 
142
- - Not a changelog generator. Use `git log` + conventional commits if you
143
- want one.
144
- - Not a monorepo manager. Package boundaries are declared, not discovered.
145
- - Not a dependency resolver across ecosystems. `depends_on` is about
146
- *cascading releases*, not runtime version pinning.
147
- - Not a build system. Handlers shell out to `cargo`, `uv`/`maturin`/`hatch`,
148
- `npm` — standard toolchains only.
391
+ The reusable workflow sets `SETUPTOOLS_SCM_PRETEND_VERSION` to the planned
392
+ version on the build step, which both `setuptools-scm` and `hatch-vcs`
393
+ honor. Per-package variants like `SETUPTOOLS_SCM_PRETEND_VERSION_FOR_<PKG>`
394
+ are silently ignored by `hatch-vcs`; only the global form works.
149
395
 
150
- ## Docs
396
+ ## Project layout
151
397
 
152
- - [Design proposal](./notes/4-17-2026-initial-plan/plan/proposal.md) — why this tool exists.
153
- - [Implementation plan](./notes/4-17-2026-initial-plan/plan/plan.md) — exhaustive reference.
154
- - [Migration guides](./migrations/) — per-repo plans for adopting putitoutthere.
155
- - [v0 epic](https://github.com/thekevinscott/put-it-out-there/issues/2) — remaining work.
398
+ - [`CHANGELOG.md`](./CHANGELOG.md) — per-release changes.
399
+ - [`MIGRATIONS.md`](./MIGRATIONS.md) — per-version upgrade guide.
400
+ - [`notes/design-commitments.md`](./notes/design-commitments.md) — non-goals.
401
+ - [`notes/internals/`](./notes/internals/) — internal contracts (artifact
402
+ layout, runner setup) that the reusable workflow honors so consumers don't
403
+ have to.
package/action.yml CHANGED
@@ -1,10 +1,10 @@
1
- name: 'Put It Out There'
2
- description: 'Polyglot release orchestrator for crates.io, PyPI, and npm.'
1
+ name: 'Put It Out There (internal)'
2
+ description: 'Internal step-level wrapper around the putitoutthere CLI. Consumers compose with the reusable workflow at .github/workflows/release.yml@v0, not with this action directly. See notes/design-commitments.md non-goal #10.'
3
3
  author: 'Kevin Scott'
4
4
 
5
5
  inputs:
6
6
  command:
7
- description: 'Which putitoutthere command to run: plan | publish'
7
+ description: 'Which CLI subcommand to run. Canonical values are `plan` and `publish`.'
8
8
  required: false
9
9
  default: 'plan'
10
10
  dry_run:
@@ -12,13 +12,13 @@ inputs:
12
12
  required: false
13
13
  default: 'false'
14
14
  fail_on_error:
15
- description: 'If true, non-zero putitoutthere exit codes fail the step.'
15
+ description: 'If true, non-zero CLI exit codes fail the step.'
16
16
  required: false
17
17
  default: 'true'
18
18
 
19
19
  outputs:
20
20
  matrix:
21
- description: 'JSON matrix emitted by `putitoutthere plan`. Empty string for other commands.'
21
+ description: 'JSON matrix emitted by `plan`. Output key is omitted when the plan resolves to zero rows or when command is anything other than plan; downstream jobs should coalesce with `|| ''[]'''.'
22
22
 
23
23
  runs:
24
24
  using: 'node24'
package/dist/action.js CHANGED
@@ -13,7 +13,7 @@ export async function main() {
13
13
  const dryRun = (process.env.INPUT_DRY_RUN ?? 'false').toLowerCase() === 'true';
14
14
  const failOnError = (process.env.INPUT_FAIL_ON_ERROR ?? 'true').toLowerCase() !== 'false';
15
15
  if (!command) {
16
- process.stderr.write('put-it-out-there action: missing required input `command`\n');
16
+ process.stderr.write('putitoutthere action: missing required input `command`\n');
17
17
  process.exit(1);
18
18
  }
19
19
  const argv = ['node', 'putitoutthere', command];
@@ -22,7 +22,7 @@ export async function main() {
22
22
  argv.push('--json');
23
23
  const code = await run(argv);
24
24
  if (code !== 0 && !failOnError) {
25
- process.stderr.write(`put-it-out-there action: ignoring non-zero exit (fail_on_error=false): ${code}\n`);
25
+ process.stderr.write(`putitoutthere action: ignoring non-zero exit (fail_on_error=false): ${code}\n`);
26
26
  process.exit(0);
27
27
  }
28
28
  process.exit(code);
@@ -1 +1 @@
1
- {"version":3,"file":"action.js","sourceRoot":"","sources":["../src/action.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAE/B,MAAM,CAAC,KAAK,UAAU,IAAI;IACxB,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,aAAa,IAAI,EAAE,CAAC;IAChD,MAAM,MAAM,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,aAAa,IAAI,OAAO,CAAC,CAAC,WAAW,EAAE,KAAK,MAAM,CAAC;IAC/E,MAAM,WAAW,GACf,CAAC,OAAO,CAAC,GAAG,CAAC,mBAAmB,IAAI,MAAM,CAAC,CAAC,WAAW,EAAE,KAAK,OAAO,CAAC;IAExE,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,6DAA6D,CAC9D,CAAC;QACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,MAAM,IAAI,GAAG,CAAC,MAAM,EAAE,eAAe,EAAE,OAAO,CAAC,CAAC;IAChD,IAAI,MAAM;QAAE,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IACnC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAEpB,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,CAAC,CAAC;IAC7B,IAAI,IAAI,KAAK,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC;QAC/B,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,0EAA0E,IAAI,IAAI,CACnF,CAAC;QACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IACD,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACrB,CAAC;AAED,oFAAoF;AACpF,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,KAAK,UAAU,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;IACpD,KAAK,IAAI,EAAE,CAAC;AACd,CAAC"}
1
+ {"version":3,"file":"action.js","sourceRoot":"","sources":["../src/action.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAE/B,MAAM,CAAC,KAAK,UAAU,IAAI;IACxB,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,aAAa,IAAI,EAAE,CAAC;IAChD,MAAM,MAAM,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,aAAa,IAAI,OAAO,CAAC,CAAC,WAAW,EAAE,KAAK,MAAM,CAAC;IAC/E,MAAM,WAAW,GACf,CAAC,OAAO,CAAC,GAAG,CAAC,mBAAmB,IAAI,MAAM,CAAC,CAAC,WAAW,EAAE,KAAK,OAAO,CAAC;IAExE,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,0DAA0D,CAC3D,CAAC;QACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,MAAM,IAAI,GAAG,CAAC,MAAM,EAAE,eAAe,EAAE,OAAO,CAAC,CAAC;IAChD,IAAI,MAAM;QAAE,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IACnC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAEpB,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,CAAC,CAAC;IAC7B,IAAI,IAAI,KAAK,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC;QAC/B,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,uEAAuE,IAAI,IAAI,CAChF,CAAC;QACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IACD,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACrB,CAAC;AAED,oFAAoF;AACpF,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,KAAK,UAAU,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;IACpD,KAAK,IAAI,EAAE,CAAC;AACd,CAAC"}
package/dist/cascade.d.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  * the last release, returns the packages that will release.
4
4
  *
5
5
  * Two passes (plan.md §11.1):
6
- * 1. Direct match: every package whose `paths` globs intersect the
6
+ * 1. Direct match: every package whose `globs` intersect the
7
7
  * files changed since *its own* last tag is cascaded.
8
8
  * 2. Transitive match: repeat until stable — any package whose
9
9
  * `depends_on` list contains an already-cascaded package gets
package/dist/cascade.js CHANGED
@@ -3,7 +3,7 @@
3
3
  * the last release, returns the packages that will release.
4
4
  *
5
5
  * Two passes (plan.md §11.1):
6
- * 1. Direct match: every package whose `paths` globs intersect the
6
+ * 1. Direct match: every package whose `globs` intersect the
7
7
  * files changed since *its own* last tag is cascaded.
8
8
  * 2. Transitive match: repeat until stable — any package whose
9
9
  * `depends_on` list contains an already-cascaded package gets
@@ -35,7 +35,7 @@ export function computeCascade(packages, changedFilesByPackage) {
35
35
  if (!files)
36
36
  continue;
37
37
  for (const f of files) {
38
- if (matchesAny(p.paths, f)) {
38
+ if (matchesAny(p.globs, f)) {
39
39
  cascaded.add(p.name);
40
40
  break;
41
41
  }
package/dist/cli.d.ts CHANGED
@@ -1,16 +1,19 @@
1
1
  /**
2
- * `putitoutthere` CLI entry. Thin wrapper around the SDK.
2
+ * `putitoutthere` CLI entry. Internal seam — the reusable workflow
3
+ * (`.github/workflows/release.yml`) invokes this. Not a consumer-
4
+ * facing surface; flags / help text are stable enough to test, but
5
+ * not promised externally. See `notes/design-commitments.md`.
3
6
  *
4
- * plan → src/plan.ts
5
- * publish → src/publish.ts
6
- * init → TODO #20
7
- * doctor → TODO #23
7
+ * Commands:
8
+ * plan — compute and emit the release plan
9
+ * publish — execute the plan against the registries
10
+ * version — print CLI version
8
11
  *
9
12
  * Global flags:
10
13
  * --cwd <path> working directory (default: process.cwd())
11
14
  * --config <path> path to putitoutthere.toml
12
15
  * --dry-run for publish; no side effects
13
- * --json for plan; emit JSON instead of a table
16
+ * --json machine-readable output
14
17
  */
15
18
  export declare function run(argv: readonly string[]): Promise<number>;
16
19
  //# sourceMappingURL=cli.d.ts.map
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAyHH,wBAAsB,GAAG,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CA0UlE"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AA2DH,wBAAsB,GAAG,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CA+FlE"}