putitoutthere 0.2.29 → 0.2.30

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 (138) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/MIGRATIONS.md +1274 -0
  3. package/README.md +343 -41
  4. package/action.yml +9 -0
  5. package/dist/action.d.ts.map +1 -1
  6. package/dist/action.js +28 -6
  7. package/dist/action.js.map +1 -1
  8. package/dist/cascade.js +14 -7
  9. package/dist/cascade.js.map +1 -1
  10. package/dist/check-crate-size.d.ts +26 -0
  11. package/dist/check-crate-size.d.ts.map +1 -0
  12. package/dist/check-crate-size.js +99 -0
  13. package/dist/check-crate-size.js.map +1 -0
  14. package/dist/check.d.ts.map +1 -1
  15. package/dist/check.js +116 -16
  16. package/dist/check.js.map +1 -1
  17. package/dist/cli.d.ts +17 -4
  18. package/dist/cli.d.ts.map +1 -1
  19. package/dist/cli.js +156 -19
  20. package/dist/cli.js.map +1 -1
  21. package/dist/completeness.js +6 -3
  22. package/dist/completeness.js.map +1 -1
  23. package/dist/config.d.ts +2 -0
  24. package/dist/config.d.ts.map +1 -1
  25. package/dist/config.js +20 -5
  26. package/dist/config.js.map +1 -1
  27. package/dist/ensure-tag.d.ts +17 -0
  28. package/dist/ensure-tag.d.ts.map +1 -0
  29. package/dist/ensure-tag.js +28 -0
  30. package/dist/ensure-tag.js.map +1 -0
  31. package/dist/env.js +6 -3
  32. package/dist/env.js.map +1 -1
  33. package/dist/error-codes.d.ts +89 -0
  34. package/dist/error-codes.d.ts.map +1 -1
  35. package/dist/error-codes.js +102 -0
  36. package/dist/error-codes.js.map +1 -1
  37. package/dist/git.d.ts +5 -0
  38. package/dist/git.d.ts.map +1 -1
  39. package/dist/git.js +19 -6
  40. package/dist/git.js.map +1 -1
  41. package/dist/glob.d.ts +9 -0
  42. package/dist/glob.d.ts.map +1 -1
  43. package/dist/glob.js +29 -1
  44. package/dist/glob.js.map +1 -1
  45. package/dist/handlers/crates.d.ts +15 -0
  46. package/dist/handlers/crates.d.ts.map +1 -1
  47. package/dist/handlers/crates.js +84 -9
  48. package/dist/handlers/crates.js.map +1 -1
  49. package/dist/handlers/npm-platform.d.ts +11 -0
  50. package/dist/handlers/npm-platform.d.ts.map +1 -1
  51. package/dist/handlers/npm-platform.js +91 -8
  52. package/dist/handlers/npm-platform.js.map +1 -1
  53. package/dist/handlers/npm.d.ts.map +1 -1
  54. package/dist/handlers/npm.js +59 -9
  55. package/dist/handlers/npm.js.map +1 -1
  56. package/dist/handlers/pypi.d.ts +0 -6
  57. package/dist/handlers/pypi.d.ts.map +1 -1
  58. package/dist/handlers/pypi.js +28 -10
  59. package/dist/handlers/pypi.js.map +1 -1
  60. package/dist/log.js +16 -8
  61. package/dist/log.js.map +1 -1
  62. package/dist/normalize-artifacts.js +8 -4
  63. package/dist/normalize-artifacts.js.map +1 -1
  64. package/dist/plan.d.ts +8 -0
  65. package/dist/plan.d.ts.map +1 -1
  66. package/dist/plan.js +125 -25
  67. package/dist/plan.js.map +1 -1
  68. package/dist/preflight.d.ts +79 -0
  69. package/dist/preflight.d.ts.map +1 -1
  70. package/dist/preflight.js +715 -33
  71. package/dist/preflight.js.map +1 -1
  72. package/dist/publish.d.ts +7 -0
  73. package/dist/publish.d.ts.map +1 -1
  74. package/dist/publish.js +49 -18
  75. package/dist/publish.js.map +1 -1
  76. package/dist/python-versions.d.ts +40 -0
  77. package/dist/python-versions.d.ts.map +1 -0
  78. package/dist/python-versions.js +142 -0
  79. package/dist/python-versions.js.map +1 -0
  80. package/dist/reconcile-types.d.ts +36 -0
  81. package/dist/reconcile-types.d.ts.map +1 -0
  82. package/dist/reconcile-types.js +9 -0
  83. package/dist/reconcile-types.js.map +1 -0
  84. package/dist/reconcile.d.ts +21 -0
  85. package/dist/reconcile.d.ts.map +1 -0
  86. package/dist/reconcile.js +54 -0
  87. package/dist/reconcile.js.map +1 -0
  88. package/dist/release-packages.d.ts +33 -0
  89. package/dist/release-packages.d.ts.map +1 -0
  90. package/dist/release-packages.js +74 -0
  91. package/dist/release-packages.js.map +1 -0
  92. package/dist/resolve-tag-commit.d.ts +22 -0
  93. package/dist/resolve-tag-commit.d.ts.map +1 -0
  94. package/dist/resolve-tag-commit.js +26 -0
  95. package/dist/resolve-tag-commit.js.map +1 -0
  96. package/dist/retry.js +14 -7
  97. package/dist/retry.js.map +1 -1
  98. package/dist/status-classify.d.ts +17 -0
  99. package/dist/status-classify.d.ts.map +1 -0
  100. package/dist/status-classify.js +37 -0
  101. package/dist/status-classify.js.map +1 -0
  102. package/dist/status-format.d.ts +9 -0
  103. package/dist/status-format.d.ts.map +1 -0
  104. package/dist/status-format.js +19 -0
  105. package/dist/status-format.js.map +1 -0
  106. package/dist/status-types.d.ts +31 -0
  107. package/dist/status-types.d.ts.map +1 -0
  108. package/dist/status-types.js +8 -0
  109. package/dist/status-types.js.map +1 -0
  110. package/dist/status.d.ts +28 -0
  111. package/dist/status.d.ts.map +1 -0
  112. package/dist/status.js +72 -0
  113. package/dist/status.js.map +1 -0
  114. package/dist/tag-template.js +2 -1
  115. package/dist/tag-template.js.map +1 -1
  116. package/dist/trailer.js +16 -9
  117. package/dist/trailer.js.map +1 -1
  118. package/dist/types.d.ts +11 -0
  119. package/dist/types.d.ts.map +1 -1
  120. package/dist/types.js +4 -2
  121. package/dist/types.js.map +1 -1
  122. package/dist/verbose.js +7 -4
  123. package/dist/verbose.js.map +1 -1
  124. package/dist/wheel-abi.d.ts +37 -0
  125. package/dist/wheel-abi.d.ts.map +1 -0
  126. package/dist/wheel-abi.js +118 -0
  127. package/dist/wheel-abi.js.map +1 -0
  128. package/dist/write-crate-version.d.ts +32 -0
  129. package/dist/write-crate-version.d.ts.map +1 -0
  130. package/dist/write-crate-version.js +52 -0
  131. package/dist/write-crate-version.js.map +1 -0
  132. package/dist/write-launcher.d.ts +82 -0
  133. package/dist/write-launcher.d.ts.map +1 -0
  134. package/dist/write-launcher.js +236 -0
  135. package/dist/write-launcher.js.map +1 -0
  136. package/dist/write-version.js +2 -1
  137. package/dist/write-version.js.map +1 -1
  138. package/package.json +4 -2
package/README.md CHANGED
@@ -79,7 +79,7 @@ Optional inputs — `with:` block at the call site:
79
79
  |------------------|--------------|--------------------------------------------------------------------------|
80
80
  | `environment` | `release` | Your GitHub deployment environment is named differently. |
81
81
  | `node_version` | `24` | You need a specific Node version for `kind = "npm"` build steps. |
82
- | `python_version` | `3.12` | You need a specific Python version for `kind = "pypi"` build steps. |
82
+ | `python_version` | `3.12` | Deprecated — no longer affects `kind = "pypi"` builds. Wheel coverage is inferred from `requires-python` or pinned via [`python_versions`](#kind--pypi). |
83
83
 
84
84
  ### 1b. Recommended: drop in `.github/workflows/check.yml`
85
85
 
@@ -89,7 +89,11 @@ duplicate package name, a `depends_on` cycle, a missing
89
89
  `[[package]].path` directory, globs that match no tracked files,
90
90
  a `tag_format` collision, a missing `repository` field on an
91
91
  `npm` package, missing `description` / `license` on a `crates`
92
- package, or a `bundle_cli` binary the crate doesn't declare — a
92
+ package, a `bundle_cli` binary the crate doesn't declare, a
93
+ `pyproject.toml` whose `[project].name` or `[build-system].build-backend`
94
+ disagrees with the configured `name` / `build`, a `Cargo.toml` whose
95
+ `[package].name` disagrees with the configured `name` / `crate`, or a
96
+ `features` list referencing a feature the crate doesn't declare — a
93
97
  couple of seconds per PR, no per-target build, no `setup-python`
94
98
  / `setup-rust`. Findings are aggregated into one report so you
95
99
  fix everything in one push instead of chasing errors across re-
@@ -112,6 +116,11 @@ does not run `setup-node` against your sources, and never holds a
112
116
  publishable artifact in memory; its `permissions:` block is
113
117
  `contents: read` only.
114
118
 
119
+ `check.yml` takes no inputs. The Node version is pinned internally
120
+ because no consumer build steps run on this code path — the
121
+ `node_version` knob on `build.yml` / `release.yml` does not exist
122
+ here. Wire `check.yml` exactly as shown above.
123
+
115
124
  ### 1c. Recommended: drop in `.github/workflows/build-check.yml`
116
125
 
117
126
  Run the same plan + build matrix on every PR, with the publish step
@@ -224,14 +233,68 @@ version = 1 # required; only 1 is valid today
224
233
  | `features` | string[] | Pass through to `cargo publish --features`. |
225
234
  | `no_default_features` | bool | Pass `--no-default-features` to `cargo publish` when true. |
226
235
 
236
+ > [!IMPORTANT]
237
+ > **`Cargo.toml` MUST match the configured shape.** Preflight verifies
238
+ > these at PR time (via `check.yml`) and again before any publish side
239
+ > effect:
240
+ >
241
+ > - `[package].name` matches `[[package]].name` (or the `crate` override) —
242
+ > `PIOT_CRATES_NAME_MISMATCH`.
243
+ > - `[package].description` and `[package].license` (or `license-file`) are
244
+ > set — `PIOT_CRATES_MISSING_METADATA`.
245
+ > - Every entry in `features` (and in `bundle_cli.features`, when set) is
246
+ > declared in `[features]` — `PIOT_CRATES_FEATURE_NOT_DECLARED`.
247
+ > - When `bundle_cli.bin` is set, the target `Cargo.toml` declares a
248
+ > `[[bin]]` with that name (or the implicit binary derived from
249
+ > `[package].name`) — `PIOT_CRATES_MISSING_BIN`.
250
+ > - When `[package].version.workspace = true`, an ancestor `Cargo.toml`
251
+ > declares `[workspace.package].version` —
252
+ > `PIOT_CRATES_WORKSPACE_VERSION_MISMATCH`.
253
+
227
254
  ### `kind = "pypi"`
228
255
 
229
256
  | Field | Type | Notes |
230
257
  |--------------|------------------------|----------------------------------------------------|
231
258
  | `pypi` | string | Override `name` → PyPI registered name. |
232
- | `build` | enum | `maturin` \| `setuptools` \| `hatch`. Required. |
259
+ | `build` | enum | `maturin` \| `setuptools` \| `hatch`. Optional. Default `setuptools`. |
233
260
  | `targets` | (string \| object)[] | Required when `build = "maturin"`. Triples or `{ triple, runner }` objects. |
234
261
  | `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). |
262
+ | `python_versions` | string[] | Optional override for the CPython versions wheels are built for, e.g. `["3.12", "3.13"]`. When omitted, the set is inferred from `[project].requires-python` and putitoutthere's checked-in released-CPython list (see below). |
263
+
264
+ > [!NOTE]
265
+ > **`kind = "pypi"` builds a wheel for every supported Python version.**
266
+ > By default the version set is inferred from `[project].requires-python`
267
+ > in your `pyproject.toml` — `requires-python = ">=3.10"` builds wheels
268
+ > for every released CPython minor version in putitoutthere's checked-in
269
+ > list that it allows. No configuration is needed for the common case;
270
+ > update putitoutthere when a new CPython minor should be included.
271
+ > To pin an explicit subset, set `python_versions` on the package. The
272
+ > build matrix fans across the resolved set (per `maturin` target); the
273
+ > sdist and a pure-Python `hatch` wheel are version-agnostic and built
274
+ > once. A `maturin` wheel that is itself Python-version-independent —
275
+ > `[tool.maturin].bindings = "bin"` (a `py3-none` Rust-binary wheel) or a
276
+ > pyo3 `abi3` extension (a `cp3x-abi3` wheel) — is likewise built once, on
277
+ > the newest resolved interpreter, instead of duplicated across the set
278
+ > (the duplicates otherwise collide at the `pypi-publish` download). When
279
+ > neither `python_versions` nor a parseable `requires-python` is present, a
280
+ > single wheel is built for `3.12`.
281
+
282
+ > [!IMPORTANT]
283
+ > **`pyproject.toml` MUST match the configured shape.** Preflight verifies
284
+ > these at PR time (via `check.yml`) and again before any publish side
285
+ > effect:
286
+ >
287
+ > - `[project].name` matches `[[package]].name` (or the `pypi` override) —
288
+ > `PIOT_PYPI_NAME_MISMATCH`.
289
+ > - `[build-system].build-backend`, when set, matches the configured
290
+ > `build` mode (`maturin` → `maturin`, `setuptools` →
291
+ > `setuptools.build_meta`, `hatch` → `hatchling.build`) —
292
+ > `PIOT_PYPI_BUILD_BACKEND_MISMATCH`.
293
+ > - When `[project].dynamic` contains `"version"`, either
294
+ > `[tool.hatch.version]` or `[tool.setuptools_scm]` declares the version
295
+ > source — `PIOT_PYPI_DYNAMIC_VERSION_NO_BACKEND`.
296
+ > - When `bundle_cli` is set, `[tool.maturin].include` covers
297
+ > `bundle_cli.stage_to` — `PIOT_PYPI_MATURIN_INCLUDE_MISSING`.
235
298
 
236
299
  ### `kind = "npm"`
237
300
 
@@ -270,6 +333,19 @@ version = 1 # required; only 1 is valid today
270
333
  > (`"repository": "git+https://github.com/<owner>/<repo>.git"`) is also
271
334
  > accepted.
272
335
 
336
+ > [!IMPORTANT]
337
+ > **`package.json`'s `name` MUST match the configured shape.** Preflight
338
+ > verifies this at PR time (via `check.yml`) and again before any publish
339
+ > side effect:
340
+ >
341
+ > - `name` matches `[[package]].name` (or the `npm` override) —
342
+ > `PIOT_NPM_NAME_MISMATCH`. `npm publish` packs the manifest `name`, but
343
+ > `putitoutthere`'s idempotency check (`npm view <name>`) and the tag /
344
+ > release-URL bookkeeping use the configured name; a divergence breaks
345
+ > idempotency and can publish under an unexpected name. Use the `npm`
346
+ > override when the registered name differs from `[[package]].name`
347
+ > (e.g. a scoped `@scope/foo`).
348
+
273
349
  ### Example: polyglot Rust library
274
350
 
275
351
  One Rust crate feeds three artifacts:
@@ -345,6 +421,13 @@ scopes a non-default bump to specific packages.
345
421
  The trailer matches anywhere in the commit body. If multiple `release:` lines
346
422
  are present, the **last** one wins.
347
423
 
424
+ The parser is intentionally lenient on three points: the key is
425
+ case-insensitive (`Release:` and `RELEASE:` both match), leading
426
+ whitespace before `release:` is allowed, and an empty package list
427
+ (`release: minor []`) is equivalent to no list (`release: minor`).
428
+ The documented forms above are the canonical shape; the leniency
429
+ exists so commits authored under varied review styles still parse.
430
+
348
431
  ## Cascade
349
432
 
350
433
  A package cascades into the release plan when a commit changes any file
@@ -360,6 +443,61 @@ Each handler's first move on publish is `isPublished` — check the registry
360
443
  for the target version. Already there → skip cleanly. Lets you re-run failed
361
444
  releases without fighting registry-immutable-publish semantics.
362
445
 
446
+ ## Manual release
447
+
448
+ Releases are normally change-driven: a package ships when a commit touches
449
+ its `globs`. Sometimes you need to release a package that has **no new
450
+ commits** — most often a re-release after a release-pipeline bug is fixed.
451
+ The `release_packages` input on `release.yml` does exactly that.
452
+
453
+ Wire it to a `workflow_dispatch` trigger in your caller workflow:
454
+
455
+ ```yaml
456
+ on:
457
+ push: { branches: [main] }
458
+ workflow_dispatch:
459
+ inputs:
460
+ release_packages:
461
+ description: 'Comma-separated name[@bump|version] list'
462
+ required: true
463
+
464
+ jobs:
465
+ release:
466
+ uses: thekevinscott/putitoutthere/.github/workflows/release.yml@v0
467
+ permissions:
468
+ contents: write
469
+ id-token: write
470
+ with:
471
+ release_packages: ${{ inputs.release_packages }}
472
+ ```
473
+
474
+ Push-triggered runs leave `release_packages` empty (the `inputs` context is
475
+ empty outside `workflow_dispatch`), so the normal change-detected path is
476
+ unaffected. Triggering the workflow manually from the Actions tab with
477
+ `release_packages` set takes over.
478
+
479
+ Grammar — a comma-separated list of entries:
480
+
481
+ ```
482
+ release_packages: lib-core@minor, lib-py@1.4.0, lib-js
483
+ ```
484
+
485
+ Each entry is a package name optionally suffixed with a version spec:
486
+
487
+ | Entry | Effect |
488
+ |------------------|-------------------------------------------------------------------|
489
+ | `lib-js` | Release `lib-js`, bumping its last tag by `patch`. |
490
+ | `lib-core@minor` | Release `lib-core`, bumping its last tag by `minor` (or `major`). |
491
+ | `lib-py@1.4.0` | Release `lib-py` at exactly `1.4.0`. |
492
+
493
+ When `release_packages` is set, change detection and `depends_on` cascade
494
+ are bypassed entirely: **exactly** the named packages are released, and
495
+ nothing else — even a package with real pending changes is left out unless
496
+ you name it. An explicit version is used verbatim and is not checked
497
+ against the last tag; if that version is already on the registry the
498
+ publish-phase `isPublished` check skips it cleanly. Naming a package that
499
+ is not declared in `putitoutthere.toml` is an error.
500
+
363
501
  ## Trusted publishers
364
502
 
365
503
  OIDC trusted publishers are the default and recommended auth path.
@@ -501,42 +639,13 @@ The engine publishes a per-platform sub-package per target
501
639
  at the published version. npm's resolver installs exactly one sub-package
502
640
  at consumer install time.
503
641
 
504
- You author the launcher script that picks the right per-platform binary
505
- once. `package.json`:
506
-
507
- ```json
508
- {
509
- "name": "my-cli",
510
- "bin": { "my-cli": "bin/my-cli.js" }
511
- }
512
- ```
513
-
514
- `bin/my-cli.js`:
515
-
516
- ```js
517
- #!/usr/bin/env node
518
- const { spawnSync } = require('node:child_process');
519
- const { platform, arch } = process;
520
-
521
- const triples = {
522
- 'linux-x64': 'x86_64-unknown-linux-gnu',
523
- 'linux-arm64': 'aarch64-unknown-linux-gnu',
524
- 'darwin-x64': 'x86_64-apple-darwin',
525
- 'darwin-arm64': 'aarch64-apple-darwin',
526
- 'win32-x64': 'x86_64-pc-windows-msvc',
527
- };
528
-
529
- const triple = triples[`${platform}-${arch}`];
530
- if (!triple) {
531
- console.error(`my-cli: unsupported platform ${platform}-${arch}`);
532
- process.exit(1);
533
- }
534
-
535
- const pkg = `my-cli-${triple}`;
536
- const binary = require.resolve(`${pkg}/bin/my-cli${platform === 'win32' ? '.exe' : ''}`);
537
- const result = spawnSync(binary, process.argv.slice(2), { stdio: 'inherit' });
538
- process.exit(result.status ?? 1);
539
- ```
642
+ With `[package.bundle_cli]` declared (below), the reusable workflow
643
+ generates the per-platform launcher and the matching `package.json#bin`
644
+ entry for you at build time — both writes are skipped when the consumer
645
+ already has either piece committed, so overrides remain trivial. To
646
+ override, commit your own `bin/<bundle_cli.bin>.js` at the package root
647
+ (or set `package.json#bin` explicitly); the workflow leaves both alone
648
+ when present.
540
649
 
541
650
  Declare `[package.bundle_cli]` so the reusable workflow runs the
542
651
  cross-compile for you:
@@ -568,6 +677,46 @@ per-target binary (the launcher above is committed source).
568
677
  > don't fit the recipe — write your own release workflow
569
678
  > instead.
570
679
 
680
+ > [!NOTE]
681
+ > **Linux binaries are statically linked against musl.** A
682
+ > binary compiled directly against the GitHub-hosted runner's
683
+ > glibc carries that glibc's version as a hard runtime
684
+ > requirement, so the package would break on any older Linux.
685
+ > The reusable workflow sidesteps that by swapping the Linux
686
+ > compile triple from `*-linux-gnu*` to `*-linux-musl*` before
687
+ > `cargo build` runs (the package's declared triple, the npm
688
+ > platform-package name, and everything else consumer-visible
689
+ > stay on the original `*-linux-gnu*`; only the binary inside
690
+ > switches). Your CLI crate must be musl-compatible:
691
+ >
692
+ > - If it makes HTTPS calls, prefer `reqwest` with `rustls-tls`
693
+ > features (the default since reqwest v0.13).
694
+ > - If it links openssl directly, enable the `vendored` feature
695
+ > on the `openssl` crate.
696
+ > - If it uses `git2`, enable `vendored-openssl` /
697
+ > `vendored-libgit2`.
698
+ > - If it uses `rusqlite` / `libsqlite3-sys`, enable the
699
+ > `bundled` feature.
700
+ > - If it uses `libpq-sys` / `mysqlclient-sys` (Postgres /
701
+ > MySQL clients), prefer a pure-Rust alternative
702
+ > (`sqlx` with `rustls`, `postgres-native-tls` swapped for
703
+ > `postgres-rustls`) — these crates have no clean static path.
704
+ >
705
+ > The musl build fails loudly at release time with a linker
706
+ > error if any of the above is missed, so a forgotten feature
707
+ > never produces a broken release — only a blocked one.
708
+
709
+ > [!WARNING]
710
+ > **Do not run `cargo build` in `npm run build` when `[package.bundle_cli]` is configured.**
711
+ > The reusable workflow compiles the Rust binary and stages it **after**
712
+ > your `npm run build` step, so the engine's musl binary always overwrites
713
+ > whatever `npm run build` staged. A build script that also runs cargo with
714
+ > the raw `-linux-gnu` triple and copies to `build/<triple>/` does wasted
715
+ > work silently. If you migrated from a hand-authored `scripts/build.cjs`
716
+ > to `[package.bundle_cli]`, remove the cargo invocation; keep only steps
717
+ > that compile or generate genuinely separate artifacts (TypeScript, assets,
718
+ > etc.).
719
+
571
720
  Each per-platform sub-package needs its own npm trusted-publisher
572
721
  registration (a policy on `my-cli` does not cover
573
722
  `my-cli-x86_64-unknown-linux-gnu`).
@@ -687,8 +836,14 @@ no_default_features = false
687
836
  ```
688
837
 
689
838
  The reusable workflow cross-compiles the binary per target and stages it
690
- into the package source tree before maturin runs. Your `pyproject.toml`
691
- ties the staged binary into a `console_scripts` entry:
839
+ into the package source tree before maturin runs. The same musl
840
+ compatibility requirement that applies to bundled-cli npm packages
841
+ applies here — see [Linux binaries are statically linked against
842
+ musl](#bundled-cli-npm-family) above for the list of Cargo features to
843
+ flip when the build fails on a system-library dependency.
844
+
845
+ Your `pyproject.toml` ties the staged binary into a `console_scripts`
846
+ entry:
692
847
 
693
848
  ```toml
694
849
  [project.scripts]
@@ -765,6 +920,153 @@ ignored by `hatch-vcs`; only the global form works.
765
920
  If a Python package can't fit any of these three shapes, it's outside
766
921
  putitoutthere's scope — write your own release workflow.
767
922
 
923
+ ## Error codes
924
+
925
+ Every consumer-visible failure carries a stable `PIOT_*` code in the
926
+ GitHub Actions `::error::` annotation and in the corresponding log
927
+ line. Grep the run log for the code, then look it up here.
928
+
929
+ | Code | What trips it | Where it fires |
930
+ |------|---------------|----------------|
931
+ | `PIOT_NPM_MISSING_REPOSITORY` | An npm package's `package.json` is missing a non-empty `repository` field. Required by `npm publish --provenance`. | PR-time (`check.yml`) and publish-time preflight. See [`kind = "npm"`](#kind--npm). |
932
+ | `PIOT_NPM_NAME_MISMATCH` | `package.json`'s `name` disagrees with the configured `[[package]].name` (or `npm` override). `npm publish` packs the manifest name while piot's idempotency/tag bookkeeping uses the configured name. | PR-time and publish-time. See [`kind = "npm"`](#kind--npm). |
933
+ | `PIOT_CRATES_NAME_MISMATCH` | `Cargo.toml`'s `[package].name` disagrees with the configured `[[package]].name` (or `crate` override). | PR-time and publish-time. See [`kind = "crates"`](#kind--crates). |
934
+ | `PIOT_CRATES_MISSING_METADATA` | `Cargo.toml` lacks `[package].description` and/or `license` (or `license-file`). crates.io 400s without it. | PR-time and publish-time. |
935
+ | `PIOT_CRATES_FEATURE_NOT_DECLARED` | A `features` entry (on the package or in `bundle_cli.features`) is not declared in the crate's `[features]` table. | PR-time and publish-time. |
936
+ | `PIOT_CRATES_MISSING_BIN` | `bundle_cli.bin` is set but the target crate has no `[[bin]]` (or implicit-binary) of that name. | PR-time and publish-time. |
937
+ | `PIOT_CRATES_WORKSPACE_VERSION_MISMATCH` | `Cargo.toml` declares `version.workspace = true` but no ancestor declares `[workspace.package].version`. | PR-time and publish-time. |
938
+ | `PIOT_CRATES_FIRST_PUBLISH_TP_REJECTED` | crates.io returned 404 because the crate has never been published. Trusted Publishing binds to an already-published crate. Bootstrap with `CARGO_REGISTRY_TOKEN` (see [crates.io](#cratesio) above). | Publish-time only — the registry's response is the signal. |
939
+ | `PIOT_PYPI_STATIC_VERSION` | `pyproject.toml` declares a static `[project].version = "..."` literal. Use `[project].dynamic = ["version"]` instead (see [Python version source](#python-version-source--required-shape)). | PR-time and publish-time. |
940
+ | `PIOT_PYPI_NAME_MISMATCH` | `pyproject.toml`'s `[project].name` disagrees with the configured `[[package]].name` (or `pypi` override). | PR-time and publish-time. |
941
+ | `PIOT_PYPI_BUILD_BACKEND_MISMATCH` | `[build-system].build-backend` is set but doesn't match the configured `build` mode (`maturin` → `maturin`, `setuptools` → `setuptools.build_meta`, `hatch` → `hatchling.build`). | PR-time and publish-time. |
942
+ | `PIOT_PYPI_DYNAMIC_VERSION_NO_BACKEND` | `[project].dynamic` contains `"version"` but no `[tool.hatch.version]` or `[tool.setuptools_scm]` block declares the source. | PR-time and publish-time. |
943
+ | `PIOT_PYPI_MATURIN_INCLUDE_MISSING` | `bundle_cli` is set on a maturin package but `[tool.maturin].include` doesn't cover `bundle_cli.stage_to`. The cross-compiled binary wouldn't land in any wheel. | PR-time and publish-time. |
944
+ | `PIOT_AUTH_NO_TOKEN` | The publish job reached the registry-auth step with no token resolved (neither an OIDC-minted token nor a caller-provided long-lived token). Almost always means the reusable workflow's trusted-publisher exchange failed silently or the caller-provided secret was empty. | Publish-time only. |
945
+ | `PIOT_PUBLISH_EMPTY_PLAN` | `publish` was invoked but `plan` returned zero rows for a reason other than `release: skip`. The reusable workflow's gate normally prevents this; if it fires, the gate was bypassed or the engine is inconsistent. | Publish-time only. |
946
+
947
+ ## Release health
948
+
949
+ The registry is the source of truth; git tags are putitoutthere's record
950
+ of what's been released (it derives "last released version" from them).
951
+ A few features keep the two in sync — `status` reports drift, while
952
+ `reconcile` and the publish-path auto-heal fix it.
953
+
954
+ ### `status` — registry-vs-tag drift report
955
+
956
+ `status` reconciles, per package, the latest git tag against the
957
+ registry's latest published version — over the public registry APIs
958
+ (crates.io / npm / PyPI), no auth required — and flags any drift:
959
+
960
+ ```
961
+ package tag registry state
962
+ mypkg-rust — 0.0.1 ⚠ published, untagged
963
+ mypkg-npm 0.0.1 0.0.1 ✓ in sync
964
+ mypkg-py 0.0.1 0.0.1 ✓ in sync
965
+ ```
966
+
967
+ | State | Meaning |
968
+ |-------|---------|
969
+ | `in sync` | the latest tag matches the registry's latest version |
970
+ | `unreleased` | no tag, and nothing published |
971
+ | `published, untagged` | live on the registry but no tag — the drift that strands a package |
972
+ | `tagged, unpublished` | tagged, but the registry doesn't have that version |
973
+ | `version mismatch` | the tag and the registry disagree on the latest version |
974
+ | `registry unreachable` | the registry couldn't be reached (reported, never gated) |
975
+
976
+ Why it matters: a half-failed run that publishes a version but never
977
+ tags it leaves the package `published, untagged`. Because the planner
978
+ reads "last released" from tags, that package then looks unreleased,
979
+ skips its already-live version forever, and can never bump — while its
980
+ dependents keep bumping past it. `status` surfaces that in one line.
981
+
982
+ - `--check` exits non-zero on any drift state — run it as a CI gate so
983
+ drift can't merge unnoticed.
984
+ - `--json` emits the rows as machine-readable JSON.
985
+
986
+ `putitoutthere` is published to npm, so run it with `npx` (it reads your
987
+ git tags, so make sure they're fetched):
988
+
989
+ ```bash
990
+ # Report drift across every package in putitoutthere.toml:
991
+ npx putitoutthere status
992
+
993
+ # Exit non-zero if anything has drifted:
994
+ npx putitoutthere status --check
995
+ echo $? # 1 when drifted, 0 when in sync
996
+
997
+ # Machine-readable rows:
998
+ npx putitoutthere status --json
999
+ # [{"package":"mypkg-rust","kind":"crates","tag":null,"tagVersion":null,
1000
+ # "registry":"0.0.1","registryUnreachable":false,
1001
+ # "state":"published, untagged","drift":true}, …]
1002
+ ```
1003
+
1004
+ To gate every PR on release-state drift, add a step to any workflow —
1005
+ checking out tags so `status` can compare them against the registry:
1006
+
1007
+ ```yaml
1008
+ - uses: actions/checkout@v4
1009
+ with:
1010
+ fetch-depth: 0 # status compares local tags vs the registry
1011
+ - run: npx putitoutthere status --check
1012
+ ```
1013
+
1014
+ ### `reconcile` — backfill missing tags on demand
1015
+
1016
+ `reconcile` fixes the `published, untagged` drift `status` reports: for
1017
+ every package that is live on its registry but has no tag, it creates the
1018
+ missing tag (and pushes it). It's the on-demand companion to auto-heal —
1019
+ where auto-heal only fires for a package caught in a release run,
1020
+ `reconcile` heals an **already-stuck** package without a release, so you
1021
+ can run it the moment `status` flags drift.
1022
+
1023
+ The tag is pointed at a sibling package already tagged at that version —
1024
+ the real release commit, e.g. a crate left untagged while its npm/PyPI
1025
+ siblings tagged the same merge — and at `HEAD` when no sibling tag exists.
1026
+ It reuses the exact drift detection `status` reports and the exact tagging
1027
+ `publish` heals with, so it can never create a tag a release wouldn't.
1028
+
1029
+ - Idempotent: a re-run is a no-op (already-correct tags are untouched).
1030
+ - `--dry-run` reports what it would create without writing anything.
1031
+ - `--json` emits the actions.
1032
+
1033
+ ```bash
1034
+ # Backfill any missing tags across putitoutthere.toml:
1035
+ npx putitoutthere reconcile
1036
+
1037
+ # Preview without writing:
1038
+ npx putitoutthere reconcile --dry-run
1039
+ # mypkg-rust: 0.0.1 live, no tag → would create mypkg-rust-v0.0.1 at a1b2c3d (sibling)
1040
+
1041
+ # Machine-readable actions:
1042
+ npx putitoutthere reconcile --json
1043
+ ```
1044
+
1045
+ Run it in CI with the release job's permissions (it pushes the tag),
1046
+ checking out tags first:
1047
+
1048
+ ```yaml
1049
+ - uses: actions/checkout@v4
1050
+ with:
1051
+ fetch-depth: 0 # reconcile compares local tags vs the registry
1052
+ - run: npx putitoutthere reconcile
1053
+ ```
1054
+
1055
+ ### Auto-heal
1056
+
1057
+ The most common drift — a version live on the registry but missing its
1058
+ tag — heals itself. **There's nothing to run**: when a release runs and
1059
+ `publish` finds a version already published, it writes the missing tag
1060
+ (at the release commit) instead of skipping silently. A package stranded
1061
+ by an earlier half-failed run recovers on its **next release run** — it
1062
+ has no tag, so it's force-selected into the plan, found already-published,
1063
+ and tagged. No manual tag surgery. Idempotent: already-tagged packages
1064
+ are untouched.
1065
+
1066
+ If the repo has nothing else to release, don't wait for the next run —
1067
+ heal the stuck package now with the `reconcile` command above, or trigger
1068
+ a [manual release](#manual-release) for it (`release_packages`).
1069
+
768
1070
  ## Project layout
769
1071
 
770
1072
  - [`CHANGELOG.md`](./CHANGELOG.md) — per-release changes.
package/action.yml CHANGED
@@ -25,6 +25,15 @@ inputs:
25
25
  build matrix's `version` row from the planner.
26
26
  required: false
27
27
  default: ''
28
+ release_packages:
29
+ description: >-
30
+ Manual-release spec, forwarded to `plan` / `publish` as
31
+ `--release-packages`. Comma-separated `name[@<bump|version>]`
32
+ entries; when set, change detection is bypassed and exactly the
33
+ named packages are released. Empty => normal change-detected
34
+ planning. Ignored for other commands.
35
+ required: false
36
+ default: ''
28
37
  fail_on_error:
29
38
  description: 'If true, non-zero CLI exit codes fail the step.'
30
39
  required: false
@@ -1 +1 @@
1
- {"version":3,"file":"action.d.ts","sourceRoot":"","sources":["../src/action.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAMH,wBAAsB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAmC1C"}
1
+ {"version":3,"file":"action.d.ts","sourceRoot":"","sources":["../src/action.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAMH,wBAAsB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAiD1C"}
package/dist/action.js CHANGED
@@ -13,6 +13,7 @@ export async function main() {
13
13
  const command = process.env.INPUT_COMMAND ?? '';
14
14
  const workingDirectory = process.env.INPUT_WORKING_DIRECTORY ?? '';
15
15
  const versionInput = process.env.INPUT_VERSION ?? '';
16
+ const releasePackages = process.env.INPUT_RELEASE_PACKAGES ?? '';
16
17
  const failOnError = (process.env.INPUT_FAIL_ON_ERROR ?? 'true').toLowerCase() !== 'false';
17
18
  if (!command) {
18
19
  process.stderr.write('putitoutthere action: missing required input `command`\n');
@@ -20,19 +21,40 @@ export async function main() {
20
21
  }
21
22
  // #276: write-version uses a different argv shape — `--path` (the
22
23
  // package dir, sourced from `working_directory`) and `--version`.
23
- // No `--json`: the subcommand emits a single human line; there's
24
- // no structured output to consume.
24
+ // #366: write-crate-version shares that shape — `--path` (the
25
+ // bundled-cli crate dir, sourced from `working_directory`) and
26
+ // `--version`.
27
+ // #299: write-launcher takes `--path` (the matrix row's package dir,
28
+ // sourced from `working_directory`) and reads `--cwd` from
29
+ // process.cwd() — the runner working dir is the repo root, where
30
+ // `putitoutthere.toml` lives.
31
+ // No `--json` on any of them: these subcommands emit a single human
32
+ // line; there's no structured output to consume.
25
33
  const argv = ['node', 'putitoutthere', command];
26
- if (command === 'write-version') {
27
- if (workingDirectory)
34
+ if (command === 'write-version' || command === 'write-crate-version') {
35
+ if (workingDirectory) {
28
36
  argv.push('--path', workingDirectory);
29
- if (versionInput)
37
+ }
38
+ if (versionInput) {
30
39
  argv.push('--version', versionInput);
40
+ }
41
+ }
42
+ else if (command === 'write-launcher') {
43
+ if (workingDirectory) {
44
+ argv.push('--path', workingDirectory);
45
+ }
31
46
  }
32
47
  else {
33
48
  argv.push('--json');
34
- if (workingDirectory)
49
+ if (workingDirectory) {
35
50
  argv.push('--cwd', workingDirectory);
51
+ }
52
+ // Manual release: forward `release_packages` to the CLI. Only
53
+ // `plan` / `publish` act on it; the flag parses harmlessly for the
54
+ // other commands that land in this branch.
55
+ if (releasePackages) {
56
+ argv.push('--release-packages', releasePackages);
57
+ }
36
58
  }
37
59
  const code = await run(argv);
38
60
  if (code !== 0 && !failOnError) {
@@ -1 +1 @@
1
- {"version":3,"file":"action.js","sourceRoot":"","sources":["../src/action.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,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,gBAAgB,GAAG,OAAO,CAAC,GAAG,CAAC,uBAAuB,IAAI,EAAE,CAAC;IACnE,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,aAAa,IAAI,EAAE,CAAC;IACrD,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,kEAAkE;IAClE,kEAAkE;IAClE,iEAAiE;IACjE,mCAAmC;IACnC,MAAM,IAAI,GAAG,CAAC,MAAM,EAAE,eAAe,EAAE,OAAO,CAAC,CAAC;IAChD,IAAI,OAAO,KAAK,eAAe,EAAE,CAAC;QAChC,IAAI,gBAAgB;YAAE,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,gBAAgB,CAAC,CAAC;QAC5D,IAAI,YAAY;YAAE,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,YAAY,CAAC,CAAC;IACzD,CAAC;SAAM,CAAC;QACN,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACpB,IAAI,gBAAgB;YAAE,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,gBAAgB,CAAC,CAAC;IAC7D,CAAC;IAED,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,uEAAuE;AACvE,iEAAiE;AACjE,sEAAsE;AACtE,kEAAkE;AAClE,sEAAsE;AACtE,qEAAqE;AACrE,kEAAkE;AAClE,kEAAkE;AAClE,gEAAgE;AAChE,uDAAuD;AACvD,oFAAoF;AACpF,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,KAAK,aAAa,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IAC/E,KAAK,IAAI,EAAE,CAAC;AACd,CAAC"}
1
+ {"version":3,"file":"action.js","sourceRoot":"","sources":["../src/action.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,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,gBAAgB,GAAG,OAAO,CAAC,GAAG,CAAC,uBAAuB,IAAI,EAAE,CAAC;IACnE,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,aAAa,IAAI,EAAE,CAAC;IACrD,MAAM,eAAe,GAAG,OAAO,CAAC,GAAG,CAAC,sBAAsB,IAAI,EAAE,CAAC;IACjE,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,kEAAkE;IAClE,kEAAkE;IAClE,8DAA8D;IAC9D,+DAA+D;IAC/D,eAAe;IACf,qEAAqE;IACrE,2DAA2D;IAC3D,iEAAiE;IACjE,8BAA8B;IAC9B,oEAAoE;IACpE,iDAAiD;IACjD,MAAM,IAAI,GAAG,CAAC,MAAM,EAAE,eAAe,EAAE,OAAO,CAAC,CAAC;IAChD,IAAI,OAAO,KAAK,eAAe,IAAI,OAAO,KAAK,qBAAqB,EAAE,CAAC;QACrE,IAAI,gBAAgB,EAAE,CAAC;YAAA,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,gBAAgB,CAAC,CAAC;QAAA,CAAC;QAC9D,IAAI,YAAY,EAAE,CAAC;YAAA,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,YAAY,CAAC,CAAC;QAAA,CAAC;IAC3D,CAAC;SAAM,IAAI,OAAO,KAAK,gBAAgB,EAAE,CAAC;QACxC,IAAI,gBAAgB,EAAE,CAAC;YAAA,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,gBAAgB,CAAC,CAAC;QAAA,CAAC;IAChE,CAAC;SAAM,CAAC;QACN,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACpB,IAAI,gBAAgB,EAAE,CAAC;YAAA,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,gBAAgB,CAAC,CAAC;QAAA,CAAC;QAC7D,8DAA8D;QAC9D,mEAAmE;QACnE,2CAA2C;QAC3C,IAAI,eAAe,EAAE,CAAC;YAAA,IAAI,CAAC,IAAI,CAAC,oBAAoB,EAAE,eAAe,CAAC,CAAC;QAAA,CAAC;IAC1E,CAAC;IAED,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,uEAAuE;AACvE,iEAAiE;AACjE,sEAAsE;AACtE,kEAAkE;AAClE,sEAAsE;AACtE,qEAAqE;AACrE,kEAAkE;AAClE,kEAAkE;AAClE,gEAAgE;AAChE,uDAAuD;AACvD,oFAAoF;AACpF,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,KAAK,aAAa,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IAC/E,KAAK,IAAI,EAAE,CAAC;AACd,CAAC"}
package/dist/cascade.js CHANGED
@@ -26,14 +26,16 @@ import { matchesAny } from './glob.js';
26
26
  export function computeCascade(packages, changedFilesByPackage) {
27
27
  assertNoCycles(packages);
28
28
  const byName = new Map();
29
- for (const p of packages)
29
+ for (const p of packages) {
30
30
  byName.set(p.name, p);
31
+ }
31
32
  // Pass 1: direct glob matches against this package's own diff.
32
33
  const cascaded = new Set();
33
34
  for (const p of packages) {
34
35
  const files = changedFilesByPackage.get(p.name);
35
- if (!files)
36
+ if (!files) {
36
37
  continue;
38
+ }
37
39
  for (const f of files) {
38
40
  if (matchesAny(p.globs, f)) {
39
41
  cascaded.add(p.name);
@@ -46,8 +48,9 @@ export function computeCascade(packages, changedFilesByPackage) {
46
48
  while (changed) {
47
49
  changed = false;
48
50
  for (const p of packages) {
49
- if (cascaded.has(p.name))
51
+ if (cascaded.has(p.name)) {
50
52
  continue;
53
+ }
51
54
  /* v8 ignore next -- depends_on default is always [] from the Zod schema */
52
55
  const deps = p.depends_on ?? [];
53
56
  if (deps.some((d) => cascaded.has(d))) {
@@ -67,15 +70,18 @@ export function computeCascade(packages, changedFilesByPackage) {
67
70
  */
68
71
  export function assertNoCycles(packages) {
69
72
  const byName = new Map();
70
- for (const p of packages)
73
+ for (const p of packages) {
71
74
  byName.set(p.name, p);
75
+ }
72
76
  const color = new Map();
73
- for (const p of packages)
77
+ for (const p of packages) {
74
78
  color.set(p.name, 'white');
79
+ }
75
80
  const visit = (name, path) => {
76
81
  const c = color.get(name);
77
- if (c === 'black')
82
+ if (c === 'black') {
78
83
  return;
84
+ }
79
85
  if (c === 'gray') {
80
86
  const cycle = [...path.slice(path.indexOf(name)), name].join(' → ');
81
87
  throw new Error(`putitoutthere.toml: depends_on cycle: ${cycle}`);
@@ -86,8 +92,9 @@ export function assertNoCycles(packages) {
86
92
  // a name we iterated over packages for — that list IS the keys of
87
93
  // byName, so this branch is unreachable under normal use. Defensive.
88
94
  /* v8 ignore next */
89
- if (!node)
95
+ if (!node) {
90
96
  return;
97
+ }
91
98
  /* v8 ignore next -- depends_on default is always [] from the Zod schema */
92
99
  for (const dep of node.depends_on ?? []) {
93
100
  if (!byName.has(dep)) {
@@ -1 +1 @@
1
- {"version":3,"file":"cascade.js","sourceRoot":"","sources":["../src/cascade.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"cascade.js","sourceRoot":"","sources":["../src/cascade.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Crate-size pre-merge check (#362).
3
+ *
4
+ * crates.io rejects any `.crate` upload over 10 MiB with `413 Payload
5
+ * Too Large`, but `cargo publish` surfaces that only mid-release, after
6
+ * the verification build has compiled the crate and every transitive
7
+ * dep — the release surprise the no-surprises design commitment exists
8
+ * to eliminate. `checkCratesPackageSize` reproduces the tarball with
9
+ * `cargo package` at PR time, so a tracked symlink dragging a build
10
+ * tree into the crate, or a missing `[package].exclude`, is caught on
11
+ * the PR that introduces it.
12
+ *
13
+ * Wired into `runChecks` (`check.ts`). The end-to-end path through the
14
+ * config loader is covered by
15
+ * `test/integration/check-crate-size.integration.test.ts`.
16
+ */
17
+ import type { CheckFinding } from './check.js';
18
+ import type { Package } from './config.js';
19
+ /**
20
+ * One finding per `kind = "crates"` package whose `cargo package`
21
+ * `.crate` is over crates.io's upload limit. Empty when every crate is
22
+ * within the limit — or when a crate's size can't be determined, in
23
+ * which case the check skips it rather than inventing a finding.
24
+ */
25
+ export declare function checkCratesPackageSize(packages: readonly Package[]): CheckFinding[];
26
+ //# sourceMappingURL=check-crate-size.d.ts.map