putitoutthere 0.1.43 → 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 +106 -0
- package/MIGRATIONS.md +555 -0
- package/README.md +368 -94
- package/action.yml +5 -5
- package/dist/action.js +2 -2
- package/dist/action.js.map +1 -1
- package/dist/cli.d.ts +5 -7
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +11 -49
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +7 -6
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +31 -14
- package/dist/config.js.map +1 -1
- package/dist/git.js +1 -1
- package/dist/git.js.map +1 -1
- package/dist/handlers/npm-platform.d.ts.map +1 -1
- package/dist/handlers/npm-platform.js +5 -1
- package/dist/handlers/npm-platform.js.map +1 -1
- package/dist/handlers/pypi.d.ts.map +1 -1
- package/dist/handlers/pypi.js +29 -6
- package/dist/handlers/pypi.js.map +1 -1
- package/dist/oidc-policy.d.ts +1 -1
- package/dist/oidc-policy.d.ts.map +1 -1
- package/dist/oidc-policy.js +10 -5
- package/dist/oidc-policy.js.map +1 -1
- package/dist/plan.d.ts +0 -1
- package/dist/plan.d.ts.map +1 -1
- package/dist/plan.js +19 -18
- package/dist/plan.js.map +1 -1
- package/dist/preflight.js +1 -1
- package/dist/preflight.js.map +1 -1
- package/package.json +5 -3
- package/dist/init.d.ts +0 -59
- package/dist/init.d.ts.map +0 -1
- package/dist/init.js +0 -181
- package/dist/init.js.map +0 -1
- package/dist/templates.d.ts +0 -43
- package/dist/templates.d.ts.map +0 -1
- package/dist/templates.js +0 -310
- package/dist/templates.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,155 +1,429 @@
|
|
|
1
1
|
# Put It Out There
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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@v1`.
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
## Quickstart
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
### 1. Drop in `.github/workflows/release.yml`
|
|
10
11
|
|
|
11
|
-
```
|
|
12
|
-
|
|
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@v1
|
|
22
|
+
permissions:
|
|
23
|
+
contents: write
|
|
24
|
+
id-token: write
|
|
13
25
|
```
|
|
14
26
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
contributors.
|
|
27
|
+
Pinned action versions, `plan → build → publish` orchestration, and GitHub
|
|
28
|
+
Release creation all live inside the reusable workflow.
|
|
18
29
|
|
|
19
|
-
|
|
30
|
+
Optional inputs — `with:` block at the call site:
|
|
20
31
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
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 = "
|
|
46
|
+
kind = "pypi" # or "npm" | "crates"
|
|
36
47
|
path = "."
|
|
37
|
-
paths = ["src/**", "
|
|
48
|
+
paths = ["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
|
|
41
|
-
|
|
53
|
+
`paths` are the globs that trigger a release. Any commit touching a matching
|
|
54
|
+
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
|
-
|
|
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
|
-
|
|
64
|
+
### 4. Push a release
|
|
65
|
+
|
|
66
|
+
Merge to `main`. Default behavior: any package whose `paths` 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:
|
|
72
|
+
release: minor
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
…in the merge commit body. See [Trailer](#trailer) below.
|
|
76
|
+
|
|
77
|
+
## Configuration
|
|
78
|
+
|
|
79
|
+
`putitoutthere.toml` lives at the repo root.
|
|
80
|
+
|
|
81
|
+
### `[putitoutthere]`
|
|
82
|
+
|
|
83
|
+
```toml
|
|
84
|
+
[putitoutthere]
|
|
85
|
+
version = 1 # required; only 1 is valid today
|
|
51
86
|
```
|
|
52
87
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
+
| `paths` | string[] | yes | 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
|
+
| `trust_policy` | table | no | Declared OIDC trust-policy expectations. See [Trusted publishers](#trusted-publishers). |
|
|
100
|
+
|
|
101
|
+
### `kind = "crates"`
|
|
102
|
+
|
|
103
|
+
| Field | Type | Notes |
|
|
104
|
+
|-----------------------|----------|------------------------------------------------------------|
|
|
105
|
+
| `crate` | string | Override `name` → crates.io name. |
|
|
106
|
+
| `features` | string[] | Pass through to `cargo publish --features`. |
|
|
107
|
+
| `no_default_features` | bool | Pass `--no-default-features` to `cargo publish` when true. |
|
|
108
|
+
|
|
109
|
+
### `kind = "pypi"`
|
|
110
|
+
|
|
111
|
+
| Field | Type | Notes |
|
|
112
|
+
|--------------|------------------------|----------------------------------------------------|
|
|
113
|
+
| `pypi` | string | Override `name` → PyPI registered name. |
|
|
114
|
+
| `build` | enum | `maturin` \| `setuptools` \| `hatch`. Required. |
|
|
115
|
+
| `targets` | (string \| object)[] | Required when `build = "maturin"`. Triples or `{ triple, runner }` objects. |
|
|
116
|
+
| `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). |
|
|
117
|
+
|
|
118
|
+
### `kind = "npm"`
|
|
57
119
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
120
|
+
| Field | Type | Notes |
|
|
121
|
+
|-----------|------------------------|------------------------------------------------------|
|
|
122
|
+
| `npm` | string | Override `name` → npm name (for scoped packages). |
|
|
123
|
+
| `access` | enum | `public` \| `restricted`. Default `public`. |
|
|
124
|
+
| `tag` | string | dist-tag. Default `latest`. |
|
|
125
|
+
| `build` | enum | `napi` \| `bundled-cli`. Omitted = vanilla. See [Recipes → Bundled-CLI npm family](#bundled-cli-npm-family). |
|
|
126
|
+
| `targets` | (string \| object)[] | Required when `build ∈ {napi, bundled-cli}`. |
|
|
62
127
|
|
|
63
|
-
|
|
128
|
+
### Example: polyglot Rust library
|
|
64
129
|
|
|
65
|
-
|
|
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:
|
|
130
|
+
One Rust crate feeds three artifacts:
|
|
69
131
|
|
|
70
132
|
```toml
|
|
71
133
|
[[package]]
|
|
72
|
-
name = "my-
|
|
134
|
+
name = "my-rust"
|
|
73
135
|
kind = "crates"
|
|
74
|
-
path = "
|
|
75
|
-
paths = ["
|
|
136
|
+
path = "crates/my-rust"
|
|
137
|
+
paths = ["crates/my-rust/**"]
|
|
76
138
|
|
|
77
139
|
[[package]]
|
|
78
|
-
name
|
|
79
|
-
kind
|
|
80
|
-
path
|
|
81
|
-
paths
|
|
82
|
-
build
|
|
83
|
-
|
|
140
|
+
name = "my-py"
|
|
141
|
+
kind = "pypi"
|
|
142
|
+
path = "py/my-py"
|
|
143
|
+
paths = ["py/my-py/**"]
|
|
144
|
+
build = "maturin"
|
|
145
|
+
targets = ["x86_64-unknown-linux-gnu", "aarch64-apple-darwin"]
|
|
146
|
+
depends_on = ["my-rust"]
|
|
84
147
|
|
|
85
148
|
[[package]]
|
|
86
|
-
name
|
|
87
|
-
kind
|
|
88
|
-
path
|
|
89
|
-
paths
|
|
90
|
-
build
|
|
91
|
-
|
|
149
|
+
name = "my-cli"
|
|
150
|
+
kind = "npm"
|
|
151
|
+
path = "packages/ts"
|
|
152
|
+
paths = ["packages/ts/**"]
|
|
153
|
+
build = "bundled-cli"
|
|
154
|
+
targets = ["x86_64-unknown-linux-gnu", "aarch64-apple-darwin"]
|
|
155
|
+
depends_on = ["my-rust"]
|
|
92
156
|
```
|
|
93
157
|
|
|
94
|
-
A change to `
|
|
95
|
-
|
|
96
|
-
|
|
158
|
+
A change to `crates/my-rust/` cascades: the crate ships, then the Python
|
|
159
|
+
wheels and npm family ship on top, version-bumped to match.
|
|
160
|
+
|
|
161
|
+
### Example: multi-package workspace
|
|
162
|
+
|
|
163
|
+
```toml
|
|
164
|
+
[[package]]
|
|
165
|
+
name = "@my/core"
|
|
166
|
+
kind = "npm"
|
|
167
|
+
path = "packages/core"
|
|
168
|
+
paths = ["packages/core/**"]
|
|
169
|
+
|
|
170
|
+
[[package]]
|
|
171
|
+
name = "@my/parser"
|
|
172
|
+
kind = "npm"
|
|
173
|
+
path = "packages/parser"
|
|
174
|
+
paths = ["packages/parser/**"]
|
|
175
|
+
depends_on = ["@my/core"]
|
|
176
|
+
```
|
|
97
177
|
|
|
98
|
-
##
|
|
178
|
+
## Trailer
|
|
99
179
|
|
|
100
|
-
|
|
101
|
-
|
|
180
|
+
The trailer is **optional**. Default behavior is `patch` whenever a package's
|
|
181
|
+
`paths` matched changed files.
|
|
102
182
|
|
|
103
|
-
|
|
183
|
+
Grammar:
|
|
104
184
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
185
|
+
```
|
|
186
|
+
release: <bump> [pkg1, pkg2, ...]
|
|
187
|
+
```
|
|
108
188
|
|
|
109
|
-
|
|
189
|
+
`<bump>` is `patch` | `minor` | `major` | `skip`. The optional package list
|
|
190
|
+
scopes a non-default bump to specific packages.
|
|
110
191
|
|
|
111
|
-
|
|
112
|
-
|
|
192
|
+
| Trailer | Effect |
|
|
193
|
+
|--------------------------|------------------------------------------------------------------------|
|
|
194
|
+
| *(none)* | Cascaded packages bump `patch`. |
|
|
195
|
+
| `release: minor` | Cascaded packages bump `minor`. |
|
|
196
|
+
| `release: major` | Cascaded packages bump `major`. |
|
|
197
|
+
| `release: skip` | No release this commit. Cascade ignored. |
|
|
198
|
+
| `release: minor [a, b]` | `a` and `b` bump `minor`; other cascaded packages stay at `patch`. |
|
|
113
199
|
|
|
114
|
-
|
|
200
|
+
The trailer matches anywhere in the commit body. If multiple `release:` lines
|
|
201
|
+
are present, the **last** one wins.
|
|
115
202
|
|
|
116
|
-
|
|
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
|
|
203
|
+
## Cascade
|
|
120
204
|
|
|
121
|
-
|
|
205
|
+
A package cascades into the release plan when a commit changes any file
|
|
206
|
+
matching one of its `paths` globs since its last tag. If another package
|
|
207
|
+
declares `depends_on = ["this-package"]`, that package also cascades.
|
|
208
|
+
Transitively, DFS-ordered, with cycle detection at config-load.
|
|
122
209
|
|
|
123
|
-
|
|
124
|
-
|
|
210
|
+
Inside a single release, packages publish in topological order of their
|
|
211
|
+
`depends_on` graph. If your Python wrapper depends on a Rust crate, the
|
|
212
|
+
crate publishes first.
|
|
125
213
|
|
|
126
|
-
|
|
214
|
+
Each handler's first move on publish is `isPublished` — check the registry
|
|
215
|
+
for the target version. Already there → skip cleanly. Lets you re-run failed
|
|
216
|
+
releases without fighting registry-immutable-publish semantics.
|
|
127
217
|
|
|
128
218
|
## Trusted publishers
|
|
129
219
|
|
|
130
|
-
|
|
220
|
+
OIDC trusted publishers — the only auth path supported by the reusable
|
|
221
|
+
workflow. Long-lived registry tokens are not reachable through the workflow.
|
|
222
|
+
|
|
223
|
+
The fields each registry needs are the same: repository owner/name, workflow
|
|
224
|
+
filename (`release.yml`), and optionally a GitHub environment name.
|
|
225
|
+
|
|
226
|
+
### crates.io
|
|
227
|
+
|
|
228
|
+
1. Publish your crate once through the normal `cargo` flow so the crate
|
|
229
|
+
exists. (Trusted publishing needs a crate owner record.)
|
|
230
|
+
2. Go to `https://crates.io/crates/<crate>/settings` → **Trusted Publishing**
|
|
231
|
+
→ **Add**.
|
|
232
|
+
3. Fill in: repository owner, repository name, workflow filename
|
|
233
|
+
(`release.yml`), environment (optional).
|
|
234
|
+
|
|
235
|
+
### PyPI
|
|
236
|
+
|
|
237
|
+
1. Go to `https://pypi.org/manage/project/<name>/settings/publishing/` (or
|
|
238
|
+
**Publishing** on the project page).
|
|
239
|
+
2. Add a **GitHub** trusted publisher: owner, repo, workflow filename,
|
|
240
|
+
environment (optional).
|
|
241
|
+
3. Brand-new project? Use a [pending publisher](https://docs.pypi.org/trusted-publishers/creating-a-project-through-oidc/)
|
|
242
|
+
to skip the bootstrap token.
|
|
243
|
+
|
|
244
|
+
### npm
|
|
245
|
+
|
|
246
|
+
1. Publish at least one version of your package with a classic
|
|
247
|
+
`NODE_AUTH_TOKEN` so the package exists on the registry. (npm's trusted
|
|
248
|
+
publisher requires an existing package.)
|
|
249
|
+
2. Go to `https://www.npmjs.com/package/<name>/access` → **Require trusted
|
|
250
|
+
publisher**.
|
|
251
|
+
3. Fill in: repository, workflow filename, environment (optional).
|
|
252
|
+
4. Delete the bootstrap token.
|
|
253
|
+
|
|
254
|
+
### Catching trust-policy drift before release
|
|
255
|
+
|
|
256
|
+
Renaming `release.yml` to anything else, or renaming the environment, breaks
|
|
257
|
+
publish with an opaque HTTP 400 from the registry. To catch the rename
|
|
258
|
+
before it ships, declare the expected values in `putitoutthere.toml`:
|
|
131
259
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
260
|
+
```toml
|
|
261
|
+
[[package]]
|
|
262
|
+
name = "my-crate"
|
|
263
|
+
kind = "crates"
|
|
264
|
+
path = "crates/my-crate"
|
|
265
|
+
paths = ["crates/my-crate/**"]
|
|
266
|
+
|
|
267
|
+
[package.trust_policy]
|
|
268
|
+
workflow = "release.yml" # required; bare filename
|
|
269
|
+
environment = "release" # optional
|
|
270
|
+
repository = "my-org/my-crate" # optional
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
The engine diffs the declaration against the local workflow file and (in CI)
|
|
274
|
+
against `GITHUB_WORKFLOW_REF` before any registry call. For `kind = "crates"`,
|
|
275
|
+
a registry cross-check runs when `CRATES_IO_DOCTOR_TOKEN` is set — calls the
|
|
276
|
+
trusted-publishing read API and fails on mismatch. PyPI and npm don't expose
|
|
277
|
+
read APIs for trust policy, so the declared diff is the full gate there.
|
|
278
|
+
|
|
279
|
+
## Recipes
|
|
280
|
+
|
|
281
|
+
### Bundled-CLI npm family
|
|
282
|
+
|
|
283
|
+
Ship a compiled CLI as an npm per-platform family — `npm install -g my-cli`
|
|
284
|
+
gives users a working binary on PATH. The `esbuild` / `biome` distribution
|
|
285
|
+
shape.
|
|
286
|
+
|
|
287
|
+
Config:
|
|
288
|
+
|
|
289
|
+
```toml
|
|
290
|
+
[[package]]
|
|
291
|
+
name = "my-cli"
|
|
292
|
+
kind = "npm"
|
|
293
|
+
npm = "my-cli"
|
|
294
|
+
build = "bundled-cli"
|
|
295
|
+
path = "packages/ts-cli"
|
|
296
|
+
paths = ["packages/ts-cli/**", "crates/my-cli/**"]
|
|
297
|
+
targets = [
|
|
298
|
+
"x86_64-unknown-linux-gnu",
|
|
299
|
+
"aarch64-unknown-linux-gnu",
|
|
300
|
+
"x86_64-apple-darwin",
|
|
301
|
+
"aarch64-apple-darwin",
|
|
302
|
+
"x86_64-pc-windows-msvc",
|
|
303
|
+
]
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
The engine publishes a per-platform sub-package per target
|
|
307
|
+
(`my-cli-<triple>`) plus a top-level whose `optionalDependencies` pin them
|
|
308
|
+
at the published version. npm's resolver installs exactly one sub-package
|
|
309
|
+
at consumer install time.
|
|
310
|
+
|
|
311
|
+
You author the launcher script that picks the right per-platform binary
|
|
312
|
+
once. `package.json`:
|
|
313
|
+
|
|
314
|
+
```json
|
|
315
|
+
{
|
|
316
|
+
"name": "my-cli",
|
|
317
|
+
"bin": { "my-cli": "bin/my-cli.js" }
|
|
318
|
+
}
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
`bin/my-cli.js`:
|
|
322
|
+
|
|
323
|
+
```js
|
|
324
|
+
#!/usr/bin/env node
|
|
325
|
+
const { spawnSync } = require('node:child_process');
|
|
326
|
+
const { platform, arch } = process;
|
|
327
|
+
|
|
328
|
+
const triples = {
|
|
329
|
+
'linux-x64': 'x86_64-unknown-linux-gnu',
|
|
330
|
+
'linux-arm64': 'aarch64-unknown-linux-gnu',
|
|
331
|
+
'darwin-x64': 'x86_64-apple-darwin',
|
|
332
|
+
'darwin-arm64': 'aarch64-apple-darwin',
|
|
333
|
+
'win32-x64': 'x86_64-pc-windows-msvc',
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
const triple = triples[`${platform}-${arch}`];
|
|
337
|
+
if (!triple) {
|
|
338
|
+
console.error(`my-cli: unsupported platform ${platform}-${arch}`);
|
|
339
|
+
process.exit(1);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const pkg = `my-cli-${triple}`;
|
|
343
|
+
const binary = require.resolve(`${pkg}/bin/my-cli${platform === 'win32' ? '.exe' : ''}`);
|
|
344
|
+
const result = spawnSync(binary, process.argv.slice(2), { stdio: 'inherit' });
|
|
345
|
+
process.exit(result.status ?? 1);
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
Each per-platform sub-package needs its own npm trusted-publisher
|
|
349
|
+
registration (a policy on `my-cli` does not cover
|
|
350
|
+
`my-cli-x86_64-unknown-linux-gnu`).
|
|
351
|
+
|
|
352
|
+
### Rust CLI inside a PyPI wheel
|
|
353
|
+
|
|
354
|
+
`pip install my-lib` on any platform gets a working CLI on `PATH` without
|
|
355
|
+
the user installing a Rust toolchain. The `ruff` / `uv` / `pydantic-core`
|
|
356
|
+
pattern.
|
|
357
|
+
|
|
358
|
+
Config:
|
|
359
|
+
|
|
360
|
+
```toml
|
|
361
|
+
[[package]]
|
|
362
|
+
name = "my-py"
|
|
363
|
+
kind = "pypi"
|
|
364
|
+
build = "maturin"
|
|
365
|
+
path = "packages/python"
|
|
366
|
+
paths = ["packages/python/**", "crates/my-rust/**"]
|
|
367
|
+
targets = [
|
|
368
|
+
"x86_64-unknown-linux-gnu",
|
|
369
|
+
"aarch64-unknown-linux-gnu",
|
|
370
|
+
"x86_64-apple-darwin",
|
|
371
|
+
"aarch64-apple-darwin",
|
|
372
|
+
"x86_64-pc-windows-msvc",
|
|
373
|
+
]
|
|
374
|
+
depends_on = ["my-rust"]
|
|
375
|
+
|
|
376
|
+
[package.bundle_cli]
|
|
377
|
+
bin = "my-cli"
|
|
378
|
+
stage_to = "src/my_py/_binary"
|
|
379
|
+
crate_path = "crates/my-rust"
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
The reusable workflow cross-compiles the binary per target and stages it
|
|
383
|
+
into the package source tree before maturin runs. Your `pyproject.toml`
|
|
384
|
+
ties the staged binary into a `console_scripts` entry:
|
|
385
|
+
|
|
386
|
+
```toml
|
|
387
|
+
[project.scripts]
|
|
388
|
+
my-cli = "my_py._binary:entrypoint"
|
|
389
|
+
|
|
390
|
+
[tool.maturin]
|
|
391
|
+
include = ["src/my_py/_binary/**"]
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
Launcher in `packages/python/src/my_py/_binary/__init__.py`:
|
|
395
|
+
|
|
396
|
+
```python
|
|
397
|
+
import os, sys
|
|
398
|
+
from pathlib import Path
|
|
399
|
+
|
|
400
|
+
def entrypoint():
|
|
401
|
+
here = Path(__file__).parent
|
|
402
|
+
binary = here / ("my-cli.exe" if os.name == "nt" else "my-cli")
|
|
403
|
+
if not binary.exists():
|
|
404
|
+
sys.stderr.write(f"my-cli binary not found at {binary}\n")
|
|
405
|
+
sys.exit(1)
|
|
406
|
+
os.execv(binary, [str(binary), *sys.argv[1:]])
|
|
407
|
+
```
|
|
135
408
|
|
|
136
|
-
|
|
137
|
-
still read as env vars if OIDC isn't available. `putitoutthere doctor`
|
|
138
|
-
reports which path is active.
|
|
409
|
+
## Dynamic-version PyPI gotcha
|
|
139
410
|
|
|
140
|
-
|
|
411
|
+
If your `pyproject.toml` uses `[project].dynamic = ["version"]` with
|
|
412
|
+
`hatch-vcs` or `setuptools-scm`, the build backend derives the version from
|
|
413
|
+
the latest git tag at build time — which is still the **previous** release
|
|
414
|
+
when the build runs. Without a handoff, the sdist ships as
|
|
415
|
+
`<pkg>-X.Y.Z.devN.tar.gz` instead of `<pkg>-X.Y.Z.tar.gz`.
|
|
141
416
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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.
|
|
417
|
+
The reusable workflow sets `SETUPTOOLS_SCM_PRETEND_VERSION` to the planned
|
|
418
|
+
version on the build step, which both `setuptools-scm` and `hatch-vcs`
|
|
419
|
+
honor. Per-package variants like `SETUPTOOLS_SCM_PRETEND_VERSION_FOR_<PKG>`
|
|
420
|
+
are silently ignored by `hatch-vcs`; only the global form works.
|
|
149
421
|
|
|
150
|
-
##
|
|
422
|
+
## Project layout
|
|
151
423
|
|
|
152
|
-
- [
|
|
153
|
-
- [
|
|
154
|
-
- [
|
|
155
|
-
- [
|
|
424
|
+
- [`CHANGELOG.md`](./CHANGELOG.md) — per-release changes.
|
|
425
|
+
- [`MIGRATIONS.md`](./MIGRATIONS.md) — per-version upgrade guide.
|
|
426
|
+
- [`notes/design-commitments.md`](./notes/design-commitments.md) — non-goals.
|
|
427
|
+
- [`notes/internals/`](./notes/internals/) — internal contracts (artifact
|
|
428
|
+
layout, runner setup) that the reusable workflow honors so consumers don't
|
|
429
|
+
have to.
|
package/action.yml
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
name: 'Put It Out There'
|
|
2
|
-
description: '
|
|
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@v1, 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
|
|
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
|
|
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 `
|
|
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('
|
|
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(`
|
|
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);
|
package/dist/action.js.map
CHANGED
|
@@ -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,
|
|
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/cli.d.ts
CHANGED
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `putitoutthere` CLI entry.
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* init → TODO #20
|
|
7
|
-
* doctor → TODO #23
|
|
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`.
|
|
8
6
|
*
|
|
9
7
|
* Global flags:
|
|
10
8
|
* --cwd <path> working directory (default: process.cwd())
|
|
11
9
|
* --config <path> path to putitoutthere.toml
|
|
12
10
|
* --dry-run for publish; no side effects
|
|
13
|
-
* --json
|
|
11
|
+
* --json machine-readable output
|
|
14
12
|
*/
|
|
15
13
|
export declare function run(argv: readonly string[]): Promise<number>;
|
|
16
14
|
//# 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
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AA6GH,wBAAsB,GAAG,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAyTlE"}
|