@smoothbricks/cli 0.1.0 → 0.1.1

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 (92) hide show
  1. package/README.md +208 -83
  2. package/dist/cli.js +50 -20
  3. package/dist/lib/devenv.d.ts +6 -0
  4. package/dist/lib/devenv.d.ts.map +1 -0
  5. package/dist/lib/devenv.js +82 -0
  6. package/dist/lib/workspace.d.ts +5 -0
  7. package/dist/lib/workspace.d.ts.map +1 -1
  8. package/dist/lib/workspace.js +62 -5
  9. package/dist/monorepo/index.d.ts +6 -1
  10. package/dist/monorepo/index.d.ts.map +1 -1
  11. package/dist/monorepo/index.js +38 -5
  12. package/dist/monorepo/lockfile.d.ts +5 -1
  13. package/dist/monorepo/lockfile.d.ts.map +1 -1
  14. package/dist/monorepo/lockfile.js +45 -5
  15. package/dist/monorepo/managed-files.d.ts +1 -1
  16. package/dist/monorepo/managed-files.d.ts.map +1 -1
  17. package/dist/monorepo/managed-files.js +27 -10
  18. package/dist/monorepo/package-hygiene.js +1 -1
  19. package/dist/monorepo/package-policy.d.ts +3 -0
  20. package/dist/monorepo/package-policy.d.ts.map +1 -1
  21. package/dist/monorepo/package-policy.js +122 -19
  22. package/dist/monorepo/packed-manifest.d.ts +4 -0
  23. package/dist/monorepo/packed-manifest.d.ts.map +1 -0
  24. package/dist/monorepo/packed-manifest.js +51 -0
  25. package/dist/monorepo/packed-package.d.ts.map +1 -1
  26. package/dist/monorepo/packed-package.js +6 -0
  27. package/dist/monorepo/packs/index.d.ts.map +1 -1
  28. package/dist/monorepo/packs/index.js +4 -1
  29. package/dist/monorepo/publish-workflow.d.ts +86 -0
  30. package/dist/monorepo/publish-workflow.d.ts.map +1 -0
  31. package/dist/monorepo/publish-workflow.js +343 -0
  32. package/dist/nx-version-actions.cjs +25 -0
  33. package/dist/nx-version-actions.d.cts +6 -0
  34. package/dist/nx-version-actions.d.cts.map +1 -0
  35. package/dist/release/core.d.ts +50 -0
  36. package/dist/release/core.d.ts.map +1 -0
  37. package/dist/release/core.js +97 -0
  38. package/dist/release/github-release.d.ts +46 -0
  39. package/dist/release/github-release.d.ts.map +1 -0
  40. package/dist/release/github-release.js +97 -0
  41. package/dist/release/index.d.ts +14 -5
  42. package/dist/release/index.d.ts.map +1 -1
  43. package/dist/release/index.js +544 -109
  44. package/dist/release/npm-auth.d.ts +17 -0
  45. package/dist/release/npm-auth.d.ts.map +1 -0
  46. package/dist/release/npm-auth.js +51 -0
  47. package/dist/release/orchestration.d.ts +48 -0
  48. package/dist/release/orchestration.d.ts.map +1 -0
  49. package/dist/release/orchestration.js +105 -0
  50. package/dist/release/publish-plan.d.ts +17 -0
  51. package/dist/release/publish-plan.d.ts.map +1 -0
  52. package/dist/release/publish-plan.js +15 -0
  53. package/dist/release/retag-unpublished.d.ts +34 -0
  54. package/dist/release/retag-unpublished.d.ts.map +1 -0
  55. package/dist/release/retag-unpublished.js +77 -0
  56. package/managed/raw/tooling/git-hooks/pre-commit.sh +2 -7
  57. package/managed/templates/github/workflows/ci.yml +2 -0
  58. package/package.json +24 -3
  59. package/src/cli.ts +64 -25
  60. package/src/lib/devenv.test.ts +28 -0
  61. package/src/lib/devenv.ts +89 -0
  62. package/src/lib/workspace.ts +76 -5
  63. package/src/monorepo/__tests__/nx-version-actions.test.ts +75 -0
  64. package/src/monorepo/__tests__/publish-workflow.test.ts +315 -0
  65. package/src/monorepo/index.ts +45 -5
  66. package/src/monorepo/lockfile.ts +52 -7
  67. package/src/monorepo/managed-files.ts +42 -12
  68. package/src/monorepo/package-hygiene.ts +1 -1
  69. package/src/monorepo/package-policy.ts +134 -18
  70. package/src/monorepo/packed-manifest.ts +67 -0
  71. package/src/monorepo/packed-package.ts +7 -0
  72. package/src/monorepo/packs/index.ts +4 -0
  73. package/src/monorepo/publish-workflow.ts +430 -0
  74. package/src/nx-version-actions.cts +36 -0
  75. package/src/release/__tests__/core-properties.test.ts +149 -0
  76. package/src/release/__tests__/core-scenarios.test.ts +177 -0
  77. package/src/release/__tests__/core.test.ts +73 -0
  78. package/src/release/__tests__/fixture-repo.test.ts +305 -0
  79. package/src/release/__tests__/github-release.test.ts +144 -0
  80. package/src/release/__tests__/helpers/fixture-repo.ts +113 -0
  81. package/src/release/__tests__/npm-auth.test.ts +129 -0
  82. package/src/release/__tests__/orchestration.test.ts +202 -0
  83. package/src/release/__tests__/publish-plan.test.ts +61 -0
  84. package/src/release/__tests__/retag-unpublished.test.ts +160 -0
  85. package/src/release/core.ts +170 -0
  86. package/src/release/github-release.ts +134 -0
  87. package/src/release/index.ts +662 -116
  88. package/src/release/npm-auth.ts +122 -0
  89. package/src/release/orchestration.ts +186 -0
  90. package/src/release/publish-plan.ts +37 -0
  91. package/src/release/retag-unpublished.ts +122 -0
  92. package/managed/templates/github/workflows/publish.yml +0 -136
package/README.md CHANGED
@@ -3,9 +3,9 @@
3
3
  `@smoothbricks/cli` provides `smoo`, the SmoothBricks monorepo automation CLI. It is the control plane for shared CI,
4
4
  release, Git hook, package metadata, and publish validation conventions across SmoothBricks-style repositories.
5
5
 
6
- The tool is intentionally convention-over-configuration. SmoothBricks repos use Nx, Bun, Nix, devenv, and direnv, so
7
- `smoo` assumes those pieces exist instead of adding another local config file. Repos should be made correct by running
8
- the mutating initialization path, then kept correct by the read-only validation path.
6
+ The tool is intentionally convention-over-configuration. SmoothBricks repos use [Nx], [Bun], [Nix], [devenv], and
7
+ [direnv], so `smoo` assumes those pieces exist instead of adding another local config file. Repos should be made correct
8
+ by running the mutating initialization path, then kept correct by the read-only validation path.
9
9
 
10
10
  ## Install
11
11
 
@@ -28,19 +28,20 @@ directly. Published installs use the package binary in `bin/smoo`, which imports
28
28
 
29
29
  ```bash
30
30
  smoo monorepo init [--runtime-only] [--sync-runtime]
31
- smoo monorepo validate
31
+ smoo monorepo validate [--fail-fast] [--only-if-new-workspace-package]
32
32
  smoo monorepo update
33
33
  smoo monorepo check
34
34
  smoo monorepo diff
35
35
  smoo monorepo validate-commit-msg <commit-msg-file>
36
36
  smoo monorepo sync-bun-lockfile-versions
37
- smoo monorepo list-public-projects
37
+ smoo monorepo list-release-packages [--fail-empty] [--github-output <path>]
38
38
  smoo monorepo validate-public-tags
39
- smoo monorepo release-state
40
39
 
41
- smoo release version --bump <auto|patch|minor|major|prerelease> [--dry-run]
42
- smoo release publish --bump <auto|patch|minor|major|prerelease> [--tag <tag>] [--npm-tag <tag>] [--dry-run]
43
- smoo release github-release --tags <tags> --bump <auto|patch|minor|major|prerelease> [--tag <tag>] [--npm-tag <tag>] [--dry-run]
40
+ smoo release npm-status
41
+ smoo release repair-pending [--dry-run]
42
+ smoo release version --bump <auto|patch|minor|major|prerelease> [--dry-run] [--github-output <path>]
43
+ smoo release publish --bump <auto|patch|minor|major|prerelease> [--dry-run]
44
+ smoo release retag-unpublished <tag...> [--to <ref>] [--push] [--dispatch] [--remote <remote>] [--branch <branch>] [--dry-run]
44
45
  smoo release trust-publisher [--dry-run] [--otp <code>] [--skip-login]
45
46
 
46
47
  smoo github-ci cleanup-cache
@@ -55,12 +56,13 @@ only reporting drift.
55
56
 
56
57
  It currently:
57
58
 
58
- - Updates managed CI, release, hook, and formatting files.
59
+ - Updates managed CI, release, hook, and formatting files. The publish workflow is only written when the repo has owned
60
+ release packages.
59
61
  - Ensures the local `tooling/smoo` source shim is executable when present.
60
62
  - Synchronizes root runtime versions inside devenv, or when `--sync-runtime` is passed.
61
- - Applies publish metadata defaults to `npm:public` packages.
63
+ - Applies safe publish metadata defaults to `npm:public` packages without inferring package ownership.
62
64
  - Normalizes internal workspace dependency ranges to `workspace:*`.
63
- - Runs `sherif --fix --select highest` for broad monorepo package hygiene.
65
+ - Runs [`sherif --fix --select highest`][sherif] for broad monorepo package hygiene.
64
66
  - Normalizes conditional export ordering so `types` comes first and `default` comes last.
65
67
  - Adds `src` to package `files` when development-only exports intentionally point at source files.
66
68
 
@@ -86,24 +88,32 @@ It checks:
86
88
  - Managed file drift.
87
89
  - Root package policy.
88
90
  - Root Bun type version matches the Bun package manager version.
89
- - Nx release policy.
91
+ - Nx release policy, including project package release tags, project-level GitHub Release changelogs, and the temporary
92
+ Bun lockfile versionActions hook.
93
+ - `bun.lock` workspace versions match package manifests.
90
94
  - Public package tag policy.
91
95
  - Public package metadata.
92
96
  - Workspace dependency ranges.
93
- - `sherif` package hygiene.
94
- - Packed public package artifacts with `publint`.
95
- - Packed public package type resolution with the `attw` CLI.
97
+ - [`sherif`] package hygiene, with warnings treated as validation failures.
98
+ - Packed public package artifacts with [`publint`].
99
+ - Packed public package type resolution with the [`attw`][are-the-types-wrong] CLI.
100
+
101
+ The packed-package checks validate what [npm] users will install, not only the source tree. `smoo` packs each
102
+ `npm:public` package with [Bun], runs [`publint`] on the tarball, then runs [`attw`][are-the-types-wrong] on the same
103
+ tarball.
96
104
 
97
- The packed-package checks validate what npm users will install, not only the source tree. `smoo` packs each `npm:public`
98
- package with Bun, runs `publint` on the tarball, then runs `attw` on the same tarball.
105
+ The [`attw`][are-the-types-wrong] check uses the `node16` profile and ignores the CJS-to-ESM warning. SmoothBricks
106
+ packages are [ESM]-first, so [CommonJS] consumers can use dynamic import. Node 10-only subpath failures are
107
+ intentionally ignored because [Node.js] 10 is not part of the supported package contract.
99
108
 
100
- The `attw` check uses the `node16` profile and ignores the CJS-to-ESM warning. SmoothBricks packages are ESM-first, so
101
- CommonJS consumers can use dynamic import. Node 10-only subpath failures are intentionally ignored because Node 10 is
102
- not part of the supported package contract.
109
+ `smoo monorepo validate --only-if-new-workspace-package` first checks the staged git diff for newly added workspace
110
+ package manifests. If none are staged, it exits successfully without running the full validator. The generated
111
+ pre-commit hook uses this mode so adding a package rechecks conditional managed files, including whether the publish
112
+ workflow is now required, without making every commit pay for full validation.
103
113
 
104
114
  ## Publishable Packages
105
115
 
106
- Publishability is declared with an Nx tag:
116
+ Publishability is declared with an [Nx] tag:
107
117
 
108
118
  ```json
109
119
  {
@@ -113,23 +123,39 @@ Publishability is declared with an Nx tag:
113
123
  }
114
124
  ```
115
125
 
116
- This tag is the source of truth for release selection, metadata validation, and publish artifact validation. `smoo`
117
- never relies on a second hardcoded package list.
126
+ This tag is the source of truth for public npm metadata and publish artifact validation. Release selection adds one more
127
+ convention: a package is released by the current repository only when its `repository.url` exactly matches the root
128
+ `package.json` `repository.url`. Equivalent-but-different spellings, such as `git+https` vs SSH for the same GitHub
129
+ repo, fail validation because ownership should be explicit and visually obvious. This lets a workspace mirror public
130
+ packages from another repository without publishing them from the mirror.
118
131
 
119
132
  Rules:
120
133
 
121
- - Publishable packages must not be private.
122
- - Private packages must not have `npm:public`.
123
- - Every non-private workspace package must have `npm:public`.
134
+ - `npm:public` packages must not be `private: true`.
135
+ - `private: true` packages must not have `npm:public`.
124
136
  - Public packages must define license metadata.
125
137
  - Public packages must publish with `publishConfig.access = "public"`.
126
- - Public package repositories must point at the root repo and include `repository.directory`.
138
+ - Public packages must define `repository.type`, `repository.url`, and `repository.directory`.
127
139
  - Public packages must define `files`.
128
140
  - Public library packages must define `types`.
129
141
  - Public packages must define either `exports` or `bin`.
130
142
 
131
- `smoo monorepo init` fixes the metadata that can be derived from the root package. For example, it copies the root
132
- license and repository URL, then sets each package's `repository.directory` from its workspace path.
143
+ Owned public packages may inherit the root license when the root license is not `UNLICENSED`. Mirrored public packages
144
+ must carry their own license. `smoo monorepo init` does not copy the root `repository.url` into packages; a missing
145
+ package `repository.url` is a validation failure so new packages must consciously choose whether they are owned by the
146
+ current repository or mirrored from another one. Init still sets `publishConfig.access = "public"`, repository type,
147
+ repository directory, export ordering, and source-file publish entries when those can be derived safely.
148
+
149
+ `smoo monorepo list-release-packages` prints the comma-separated package names that are both `npm:public` and owned by
150
+ the current repository. Release commands, trusted-publisher setup, and the managed publish workflow use this owned
151
+ release package list instead of every public package in the workspace.
152
+
153
+ For [GitHub Actions], `smoo monorepo list-release-packages --fail-empty --github-output "$GITHUB_OUTPUT"` appends the
154
+ `projects=<package-list>` output expected by the managed publish workflow and fails with a clear error when no owned
155
+ release packages exist.
156
+
157
+ `smoo release npm-status` shows whether each owned release package's current `name@version` already exists on npm. It is
158
+ an npm registry check, not a full release workflow status check.
133
159
 
134
160
  ## Managed Files
135
161
 
@@ -137,15 +163,18 @@ license and repository URL, then sets each package's `repository.directory` from
137
163
 
138
164
  Managed files include:
139
165
 
140
- - `.git-format-staged.yml`
166
+ - [`.git-format-staged.yml`][git-format-staged]
141
167
  - Git hook scripts under `tooling/git-hooks`
142
- - direnv/GitHub Actions bootstrap scripts under `tooling/direnv`
143
- - GitHub Actions workflows under `.github/workflows`
144
- - Local composite GitHub Actions under `.github/actions`
168
+ - [direnv]/[GitHub Actions] bootstrap scripts under `tooling/direnv`
169
+ - [GitHub Actions] workflows under `.github/workflows`
170
+ - Local composite [GitHub Actions] under `.github/actions`
145
171
 
146
172
  When a managed target is a symlink, `smoo` leaves it alone. SmoothBricks uses symlinks back to `packages/cli/managed` so
147
173
  changes to the CLI package are tested immediately. Downstream repos receive ordinary committed copies.
148
174
 
175
+ The publish workflow is conditional. Repositories with no owned release packages skip `.github/workflows/publish.yml` in
176
+ `init`, `check`, and `diff`; adding a new owned package makes the workflow required on the next validation run.
177
+
149
178
  Use:
150
179
 
151
180
  ```bash
@@ -158,8 +187,12 @@ smoo monorepo diff
158
187
 
159
188
  ## Git Hooks
160
189
 
161
- The generated pre-commit hook runs `git-format-staged` from the repository root with `tooling`, `node_modules/.bin`, and
162
- the devenv profile on `PATH`.
190
+ The generated pre-commit hook runs [`git-format-staged`][git-format-staged] from the repository root with `tooling`,
191
+ `node_modules/.bin`, and the [devenv] profile on `PATH`.
192
+
193
+ After formatting, the hook runs `smoo monorepo validate --fail-fast --only-if-new-workspace-package`. This keeps normal
194
+ commits fast while still catching incomplete package setup and conditional managed-file drift when a new workspace
195
+ package manifest is staged.
163
196
 
164
197
  The generated commit-msg hook delegates conventional commit validation to:
165
198
 
@@ -171,21 +204,22 @@ This keeps hook behavior consistent with CI and avoids duplicating commit messag
171
204
 
172
205
  ## GitHub Actions
173
206
 
174
- The generated workflows keep readable YAML and named top-level steps, while larger logic lives in `smoo` commands,
175
- post-checkout composite actions, or the small pre-smoo bootstrap script. Checkout stays inline in each workflow because
176
- repository-local composite actions do not exist until `actions/checkout` has populated the working tree.
207
+ The generated [GitHub Actions] workflows keep readable YAML and named top-level steps, while larger logic lives in
208
+ `smoo` commands, post-checkout composite actions, or the small pre-smoo bootstrap script. Checkout stays inline in each
209
+ workflow because repository-local composite actions do not exist until `actions/checkout` has populated the working
210
+ tree.
177
211
 
178
- CI uses explicit lint, test, and build phases. The publish workflow does the same before running release commands so
179
- GitHub output remains readable even though Nx release also has its own `preVersionCommand` safety net.
212
+ CI uses explicit lint, test, and build phases. The publish workflow does the same after versioning so GitHub output
213
+ stays readable and validation happens on the exact release commit.
180
214
 
181
- CI status deeplinks depend on GitHub's top-level job step anchors. The generated CI workflow keeps `# Step N` comments
182
- next to each top-level step, and the `smoo github-ci nx-smart --step <number>` values for lint, test, and build must
183
- stay synchronized with those comments. Composite action internals do not change the top-level step numbers.
215
+ CI status deeplinks depend on [GitHub Actions]' top-level job step anchors. The generated CI workflow keeps `# Step N`
216
+ comments next to each top-level step, and the `smoo github-ci nx-smart --step <number>` values for lint, test, and build
217
+ must stay synchronized with those comments. Composite action internals do not change the top-level step numbers.
184
218
 
185
219
  Managed CI setup is split across local composite actions:
186
220
 
187
- - `setup-devenv` installs Nix, restores the Nix cache segment, imports the store NAR, restores `.devenv`/`.direnv` only
188
- after an exact Nix cache hit, enables Cachix, installs devenv, restores `node_modules`, and builds the shell.
221
+ - `setup-devenv` installs [Nix], restores the Nix cache segment, imports the store NAR, restores `.devenv`/`.direnv`
222
+ only after an exact Nix cache hit, enables [Cachix], installs [devenv], restores `node_modules`, and builds the shell.
189
223
  - `save-nix-devenv` runs under `always()`, calls `smoo github-ci cleanup-cache`, and explicitly saves cache segments
190
224
  only when cleanup reports `cache-ready=true`.
191
225
  - `cache-nix-devenv` is the shared restore/save primitive for the separate `nix` and `devenv` cache segments.
@@ -208,64 +242,132 @@ The bootstrap script is intentionally small. It only handles work required befor
208
242
 
209
243
  ## Releases
210
244
 
211
- Release commands wrap Nx release but keep SmoothBricks policy in one place.
245
+ Release commands wrap [Nx Release][nx-release] but keep SmoothBricks policy in one place.
212
246
 
213
247
  Versioning:
214
248
 
215
- - `--bump auto` uses Nx conventional-commit versioning.
249
+ - `--bump auto` uses [Nx Release][nx-release] [Conventional Commits] versioning.
216
250
  - `--bump patch|minor|major|prerelease` forces the release specifier.
217
- - Release projects are discovered from `npm:public` packages.
218
- - Nx release config must use `currentVersionResolver: "git-tag"` with `fallbackCurrentVersionResolver: "disk"`.
219
- Conventional-commit versioning requires git tags as the primary source, while the disk fallback supports initial
220
- releases before package tags exist.
221
- - `smoo release version` lets Nx own local package versioning, `bun.lock` updates, the release commit, and annotated
222
- tags. smoo records `HEAD` before and after Nx runs, then pushes with `git push --follow-tags --atomic` only if Nx
223
- created a new release commit. This keeps the release commit and tags atomic without pushing an older checkout on
224
- retries where Nx has nothing new to commit.
225
- - Reruns call Nx versioning again rather than repairing tags in `smoo`. Nx's git-tag current-version resolver plus disk
226
- fallback handles already-tagged releases and first releases before package tags exist; smoo skips the remote push when
227
- Nx leaves `HEAD` unchanged.
251
+ - Release packages are discovered from `npm:public` packages whose `repository.url` exactly matches the root package.
252
+ - [Nx Release][nx-release] config must use `currentVersionResolver: "git-tag"` with
253
+ `fallbackCurrentVersionResolver: "disk"`. Conventional-commit versioning requires git tags as the primary source,
254
+ while the disk fallback supports initial releases before package tags exist.
255
+ - [Nx Release][nx-release] config must use `versionActions: "@smoothbricks/cli/nx-version-actions"`. This wraps Nx's JS
256
+ version actions and temporarily syncs `bun.lock` workspace versions after Nx runs `bun install --lockfile-only`.
257
+ - [Nx Release][nx-release] `preVersionCommand` is intentionally not used. smoo builds exactly the packages that still
258
+ need npm publish immediately before packing them, while the managed workflow separately builds, lints, tests, and
259
+ validates newly created release commits.
260
+ - `smoo release repair-pending` runs before the normal publish flow. It repairs older remote release tags whose npm
261
+ package version or GitHub Release is missing, while leaving the current `HEAD` release target to `version` and
262
+ `publish`.
263
+ - `smoo release version` selects the current release target before validation by running [Nx Release][nx-release]
264
+ versioning. npm registry state is not used to decide whether versioning should run. If `HEAD` is already a release
265
+ target, versioning returns `mode=none` and leaves idempotent completion to `smoo release publish`.
266
+ - `smoo release version --github-output "$GITHUB_OUTPUT"` appends `mode=new|none` and `projects=<comma-list>`. The
267
+ managed publish workflow uses `mode != "none"` to build, lint, test, and validate exactly the commit that
268
+ `smoo release publish` will publish. The validation and publish step names include the selected mode so the [GitHub
269
+ Actions] run shows whether it is creating a new release or recording a no-op.
270
+ - Explicit bumps are mandatory when `HEAD` is not already a release target: after pending releases are repaired,
271
+ `bump=patch|minor|major|prerelease` must make Nx create a new release commit. smoo fails if Nx returns without moving
272
+ `HEAD`; `auto` may no-op when there are no releasable conventional commits.
273
+ - `--dry-run` previews versioning and completion without pushing refs, publishing npm packages, or writing GitHub
274
+ Releases.
275
+ - The `nx-version-actions` hook is a temporary Bun workaround. Bun currently leaves `bun.lock` workspace versions stale
276
+ after package manifest bumps, and `bun pm pack` rewrites `workspace:*` dependencies using those stale lockfile
277
+ versions. Keep the hook until supported Bun versions resolve these issues:
278
+ [oven-sh/bun#18906](https://github.com/oven-sh/bun/issues/18906),
279
+ [oven-sh/bun#20477](https://github.com/oven-sh/bun/issues/20477), and
280
+ [oven-sh/bun#20829](https://github.com/oven-sh/bun/issues/20829).
281
+ - Package release tags must use `{projectName}@{version}`. smoo derives release package/version pairs from that tag
282
+ shape and recreates missing local tags for Nx release commits before repairing remote state.
283
+ - `smoo release retag-unpublished <tag...>` is a break-glass recovery command for the case where Nx already committed a
284
+ version bump but npm publish failed before the package version became durable. It moves exact owned release tags to
285
+ `HEAD` by default without bumping package manifests again. It refuses to move a tag when `package@version` already
286
+ exists on npm, when the GitHub Release exists, or when the target ref's package manifest does not contain the tagged
287
+ version. Pass `--push` to update remote tags with `--force-with-lease`; pass `--dispatch` to also start `publish.yml`
288
+ with `bump=auto`. Dispatch validates that the target ref is already the remote branch head so the workflow will
289
+ publish the same commit that was retagged.
290
+
291
+ ### Repair Process
292
+
293
+ `repair-pending` is tag-driven, not history-driven. It starts from fetched remote release tags because those tags are
294
+ the durable record that a package version was selected for release. It only checks npm and GitHub Release state to
295
+ decide which tags still need work; it does not walk normal commits looking for release-shaped changes.
296
+
297
+ 1. Collect owned release tags from the fetched remote tag set, sorted newest-first by annotated tag `creatordate`. Only
298
+ tags matching owned package release names are considered, and each tag is peeled to the commit it releases.
299
+ 2. Classify each owned release tag before grouping by commit. A tag needs npm repair when `package@version` is missing
300
+ from npm, and it needs GitHub repair when the GitHub Release for that tag is missing. Tags needing neither are
301
+ filtered out immediately.
302
+ 3. Group only repair-needed tags by peeled commit. Empty commits disappear because their tags were already complete.
303
+ Exclude `HEAD` because the current release target is handled by `smoo release version` and `smoo release publish`,
304
+ not by the older-release repair loop.
305
+ 4. Sort the remaining repair commits oldest-to-newest. Only after this sorted non-HEAD repair list exists does smoo
306
+ start checking out commits.
307
+ 5. For each repair commit, check out the commit once and load that checkout's direnv environment once. If any grouped
308
+ tag still needs npm publish, run `nx run-many -t build --projects=<comma-separated npm-missing packages>` once, then
309
+ publish those packages using the npm dist-tag implied by each package version. If the commit only needs GitHub
310
+ Releases, skip the build. Finally, create the missing GitHub Releases for the grouped tags that need them.
311
+
312
+ Pending release state should be a suffix of the release-target timeline because `repair-pending` runs before every
313
+ publish. Once a complete release target is reached, older targets are assumed complete; an observed gap in repair state
314
+ violates the workflow invariant and should fail loudly instead of silently repairing history out of order.
228
315
 
229
316
  Publishing:
230
317
 
231
318
  - `prerelease` publishes with npm dist-tag `next`.
232
319
  - Stable bumps publish with npm dist-tag `latest`.
233
- - Conflicting explicit dist-tags are rejected.
234
- - `smoo release publish` checks every current `name@version` for `npm:public` packages before publishing. Already
235
- published versions are skipped, so reruns after auth or network failures retry only the package versions npm does not
236
- have yet.
320
+ - `smoo release publish` pushes missing branch/tag refs, publishes missing npm versions, creates or updates GitHub
321
+ Releases, and writes a GitHub Step Summary. Already published npm versions are skipped, so reruns after auth or
322
+ network failures retry only the package versions npm does not have yet.
323
+ - npm registry state gates publish idempotency only. It decides which already-versioned package tarballs still need to
324
+ be published during a real release retry, not whether versioning should run or whether the workflow has a release to
325
+ publish.
326
+ - Before npm publish, smoo runs `nx run-many -t build --projects=<comma-separated npm-missing packages>` for exactly the
327
+ packages whose `name@version` is not on npm yet. Nx cache makes this cheap when the managed workflow already built the
328
+ same projects, and it keeps reruns self-sufficient when repairing a previously selected `HEAD` release target.
237
329
  - Publish uses `bun pm pack` to create package tarballs, then publishes those tarballs with latest npm CLI and
238
- `--provenance`. Bun pack resolves internal `workspace:*` dependency ranges to real versions in the tarball manifest;
239
- smoo fails before publish if a packed manifest still contains `workspace:`.
240
- - npm CLI owns publish authentication. When trusted publishing is configured, npm uses GitHub Actions OIDC from the
241
- workflow's `id-token: write` permission. Before trusted publishing exists, the managed workflow passes
242
- `secrets.NPM_TOKEN` as `NODE_AUTH_TOKEN` and smoo writes a temporary npm user config for bootstrap publishing.
243
- - `smoo release trust-publisher` configures npm trusted publishing for every `npm:public` package. It uses the root
244
- `package.json` `repository.url` as the GitHub `owner/repo`, uses `publish.yml` as the trusted workflow, and runs
245
- `npm trust` through `nix shell nixpkgs#nodejs_latest` because the Lambda-pinned Node 24/npm toolchain may lag the npm
246
- CLI feature. By default it runs `npm login --auth-type=web` first so npm can open a browser login; pass `--skip-login`
247
- when the current npm session is already authenticated. Packages must already exist on npm before trust can be
248
- configured. npm may still require operation-level 2FA for `npm trust`; smoo prompts for a hidden OTP per package, or
249
- you can pass `--otp <code>` for non-interactive use.
330
+ `--provenance`. Each package uses the npm dist-tag implied by its own version (`next` for prereleases, `latest` for
331
+ stable versions). Bun pack resolves internal `workspace:*` dependency ranges to real versions in the tarball manifest;
332
+ smoo fails before publish if a packed manifest still contains `workspace:` or if an internal dependency does not match
333
+ the current workspace package version.
334
+ - [npm CLI][npm] owns publish authentication. Existing packages use [trusted publishing][npm-trusted-publishing] with
335
+ [GitHub Actions OIDC][github-actions-oidc] from the workflow's `id-token: write` permission. Package names that do not
336
+ exist on npm yet use `secrets.NPM_TOKEN` as `NODE_AUTH_TOKEN` for the first publish, because trusted publishing can
337
+ only be configured after the package exists. Once `smoo release trust-publisher` succeeds for a package, future
338
+ publishes for that package use OIDC even if the bootstrap token is still present in repository secrets.
339
+ - `smoo release trust-publisher` configures [npm trusted publishing][npm-trusted-publishing] for every owned release
340
+ package. It uses the root `package.json` `repository.url` as the GitHub `owner/repo`, uses `publish.yml` as the
341
+ trusted workflow, and runs `npm trust` through `nix shell nixpkgs#nodejs_latest` because the Lambda-pinned Node 24/npm
342
+ toolchain may lag the npm CLI feature. By default it runs `npm login --auth-type=web` first so npm can open a browser
343
+ login; pass `--skip-login` when the current npm session is already authenticated. Packages must already exist on npm
344
+ before trust can be configured. npm may still require operation-level 2FA for `npm trust`; smoo prompts for a hidden
345
+ OTP per package, or you can pass `--otp <code>` for non-interactive use.
250
346
 
251
347
  GitHub Releases:
252
348
 
253
- - `smoo release github-release` creates or updates releases for the selected tags.
254
- - `latest` status follows the derived npm dist-tag.
349
+ - `smoo release publish` delegates to `nx release changelog` once per owned package whose current package release tag is
350
+ at `HEAD`, passing that package's version explicitly for independent releases after npm publish succeeds.
351
+ - Nx project changelogs are configured to create or update GitHub Releases, not local changelog files.
352
+ - Generated release notes are package-scoped Conventional Commit changelogs. Init defaults author rendering and GitHub
353
+ username lookup on, while validation allows repos to override those render options.
255
354
 
256
- The release flow is designed to be rerun after partial failure. Nx owns local version/tag behavior, `smoo` owns the
257
- guarded atomic git push, and npm registry state makes publishing idempotent across retries.
355
+ The release flow is designed to be rerun after partial failure. Nx owns local version/tag behavior, while smoo derives
356
+ durable completion state from the remote branch, release tags, npm registry versions, and GitHub Releases. Repeated
357
+ Publish runs converge without self-spawning another workflow run.
258
358
 
259
359
  ## Why This Shape
260
360
 
261
361
  The important design goal is one source of truth per convention:
262
362
 
263
- - Nx `npm:public` tags decide what is publishable.
363
+ - [Nx] `npm:public` tags decide what has a public npm package contract.
364
+ - Matching root/package `repository.url` values decide which public packages are released by the current repo.
264
365
  - Managed files decide what generated CI and hooks should look like.
265
- - Root package metadata provides defaults for public packages.
366
+ - Root package metadata provides defaults only for owned public packages.
266
367
  - Actual workspace package names decide which dependency ranges become `workspace:*`.
267
- - `sherif` handles broad package hygiene.
268
- - `publint` and `attw` validate real packed artifacts.
368
+ - Package manifests decide Bun lockfile workspace versions until Bun stops leaving them stale during releases.
369
+ - [`sherif`] handles broad package hygiene.
370
+ - [`publint`] and [`attw`][are-the-types-wrong] validate real packed artifacts.
269
371
 
270
372
  This keeps `smoo` small where external tools already do the job, but keeps SmoothBricks-specific policy native where
271
373
  generic tools do not know the repo contract. In particular, `sherif` is useful for package hygiene, but it does not know
@@ -287,3 +389,26 @@ If validating packages with Zig build steps from outside the devenv shell, add Z
287
389
  ```bash
288
390
  nix shell nixpkgs#zig -c nx run-many -t build --projects=<public-projects>
289
391
  ```
392
+
393
+ ## Links
394
+
395
+ [are-the-types-wrong]: https://github.com/arethetypeswrong/arethetypeswrong.github.io/tree/main/packages/cli
396
+ [Bun]: https://bun.sh/
397
+ [Cachix]: https://www.cachix.org/
398
+ [CommonJS]: https://nodejs.org/api/modules.html
399
+ [Conventional Commits]: https://www.conventionalcommits.org/
400
+ [devenv]: https://devenv.sh/
401
+ [direnv]: https://direnv.net/
402
+ [ESM]: https://nodejs.org/api/esm.html
403
+ [git-format-staged]: https://github.com/smoothbricks/git-format-staged
404
+ [GitHub Actions]: https://docs.github.com/actions
405
+ [github-actions-oidc]:
406
+ https://docs.github.com/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect
407
+ [Nix]: https://nixos.org/
408
+ [Node.js]: https://nodejs.org/
409
+ [npm]: https://www.npmjs.com/
410
+ [npm-trusted-publishing]: https://docs.npmjs.com/trusted-publishers
411
+ [Nx]: https://nx.dev/
412
+ [nx-release]: https://nx.dev/features/manage-releases
413
+ [`publint`]: https://publint.dev/
414
+ [`sherif`]: https://github.com/QuiiBz/sherif
package/dist/cli.js CHANGED
@@ -32,6 +32,7 @@ function buildProgram() {
32
32
  monorepo
33
33
  .command('validate')
34
34
  .option('--fail-fast', 'stop after the first failing validation pack')
35
+ .option('--only-if-new-workspace-package', 'skip validation unless a new workspace package manifest is staged')
35
36
  .action(async (options) => {
36
37
  const { validateMonorepo } = await import('./monorepo/index.js');
37
38
  await validateMonorepo(await findRepoRoot(), options);
@@ -56,51 +57,80 @@ function buildProgram() {
56
57
  const { syncBunLockfileVersions } = await import('./monorepo/index.js');
57
58
  syncBunLockfileVersions(await findRepoRoot());
58
59
  });
59
- monorepo.command('list-public-projects').action(async () => {
60
- const { listPublicProjects } = await import('./monorepo/index.js');
61
- console.log(listPublicProjects(await findRepoRoot()));
60
+ monorepo
61
+ .command('list-release-packages')
62
+ .option('--fail-empty', 'fail when no owned release packages are found')
63
+ .option('--github-output <path>', 'append projects=<packages> to a GitHub Actions output file')
64
+ .action(async (options) => {
65
+ const { listReleasePackagesForNx } = await import('./monorepo/index.js');
66
+ const packages = listReleasePackagesForNx(await findRepoRoot(), options);
67
+ if (!options.githubOutput) {
68
+ console.log(packages);
69
+ }
62
70
  });
63
71
  monorepo.command('validate-public-tags').action(async () => {
64
72
  const { validatePublicPackageTags } = await import('./monorepo/index.js');
65
73
  validatePublicPackageTags(await findRepoRoot());
66
74
  });
67
- monorepo.command('release-state').action(async () => {
75
+ const release = program.command('release').description('Version, publish, and create GitHub Releases');
76
+ release.command('npm-status').action(async () => {
68
77
  const { printReleaseState } = await import('./release/index.js');
69
78
  await printReleaseState(await findRepoRoot());
70
79
  });
71
- const release = program.command('release').description('Version, publish, and create GitHub Releases');
80
+ release
81
+ .command('repair-pending')
82
+ .description('Repair incomplete older release commits before releasing the current HEAD')
83
+ .option('--dry-run [dryRun]', 'run without pushing, publishing, or writing GitHub Releases')
84
+ .action(async (options) => {
85
+ const { releaseRepairPending } = await import('./release/index.js');
86
+ await releaseRepairPending(await findRepoRoot(), { ...options, dryRun: booleanOption(options.dryRun) });
87
+ });
72
88
  release
73
89
  .command('version')
74
90
  .option('--bump <bump>', 'auto, patch, minor, major, or prerelease', 'auto')
75
- .option('--dry-run [dryRun]', 'run without writing versions, tags, or pushes')
91
+ .option('--dry-run [dryRun]', 'run without writing versions or tags')
92
+ .option('--github-output <path>', 'append mode=<mode> and projects=<packages> to a GitHub Actions output file')
76
93
  .action(async (options) => {
77
94
  const { releaseVersion } = await import('./release/index.js');
78
- await releaseVersion(await findRepoRoot(), { bump: options.bump, dryRun: booleanOption(options.dryRun) });
95
+ await releaseVersion(await findRepoRoot(), {
96
+ bump: options.bump,
97
+ dryRun: booleanOption(options.dryRun),
98
+ githubOutput: options.githubOutput,
99
+ });
79
100
  });
80
101
  release
81
102
  .command('publish')
82
103
  .option('--bump <bump>', 'auto, patch, minor, major, or prerelease', 'auto')
83
- .option('--tag <tag>', 'explicit npm dist-tag; must match bump-derived tag')
84
- .option('--npm-tag <npmTag>', 'explicit npm dist-tag; must match bump-derived tag')
85
- .option('--dry-run [dryRun]', 'run without publishing')
104
+ .option('--dry-run [dryRun]', 'run without pushing, publishing, or writing GitHub Releases')
86
105
  .action(async (options) => {
87
106
  const { releasePublish } = await import('./release/index.js');
88
107
  await releasePublish(await findRepoRoot(), { ...options, dryRun: booleanOption(options.dryRun) });
89
108
  });
90
109
  release
91
- .command('github-release')
92
- .option('--tags <tags>', 'space-separated release tags')
93
- .option('--bump <bump>', 'auto, patch, minor, major, or prerelease', 'auto')
94
- .option('--tag <tag>', 'explicit npm dist-tag; must match bump-derived tag')
95
- .option('--npm-tag <npmTag>', 'explicit npm dist-tag; must match bump-derived tag')
96
- .option('--dry-run [dryRun]', 'skip GitHub Release writes')
97
- .action(async (options) => {
98
- const { releaseGithubRelease } = await import('./release/index.js');
99
- await releaseGithubRelease(await findRepoRoot(), { ...options, dryRun: booleanOption(options.dryRun) });
110
+ .command('retag-unpublished')
111
+ .description('Move unpublished owned release tags to a later commit without bumping versions')
112
+ .argument('<tag...>', 'owned release tags to move, for example @scope/pkg@1.2.3')
113
+ .option('--to <ref>', 'commit or ref to move tags to', 'HEAD')
114
+ .option('--push', 'push moved tags with force-with-lease')
115
+ .option('--dispatch', 'push moved tags and start publish.yml with bump=auto')
116
+ .option('--remote <remote>', 'git remote used for pushed tags')
117
+ .option('--branch <branch>', 'branch used for publish workflow dispatch')
118
+ .option('--dry-run [dryRun]', 'validate and print the retag operation without mutating refs')
119
+ .action(async (tags, options) => {
120
+ const { releaseRetagUnpublished } = await import('./release/index.js');
121
+ await releaseRetagUnpublished(await findRepoRoot(), {
122
+ tags,
123
+ to: options.to,
124
+ push: options.push === true,
125
+ dispatch: options.dispatch === true,
126
+ remote: options.remote,
127
+ branch: options.branch,
128
+ dryRun: booleanOption(options.dryRun),
129
+ });
100
130
  });
101
131
  release
102
132
  .command('trust-publisher')
103
- .description('Configure npm trusted publishing for public packages')
133
+ .description('Configure npm trusted publishing for owned release packages')
104
134
  .option('--dry-run [dryRun]', 'show npm trust changes without saving them')
105
135
  .option('--otp <otp>', 'npm one-time password for trust operations')
106
136
  .option('--skip-login', 'skip npm browser login before configuring trust')
@@ -0,0 +1,6 @@
1
+ type EnvSnapshot = Record<string, string>;
2
+ export declare function withDevenvEnv<T>(root: string, runWithEnv: () => Promise<T>): Promise<T>;
3
+ export declare function parseNulEnv(bytes: Uint8Array): EnvSnapshot;
4
+ export declare function mergeDevenvEnv(baseEnv: EnvSnapshot, devenvEnv: EnvSnapshot): EnvSnapshot;
5
+ export {};
6
+ //# sourceMappingURL=devenv.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"devenv.d.ts","sourceRoot":"","sources":["../../src/lib/devenv.ts"],"names":[],"mappings":"AAMA,KAAK,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAE1C,wBAAsB,aAAa,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAS7F;AAmBD,wBAAgB,WAAW,CAAC,KAAK,EAAE,UAAU,GAAG,WAAW,CAoB1D;AAED,wBAAgB,cAAc,CAAC,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,WAAW,GAAG,WAAW,CAExF"}