@sous-io/sous 0.2.16 → 0.2.18

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 (33) hide show
  1. package/docs/markdown/commands.md +59 -13
  2. package/docs/markdown/repositories-authoring.md +75 -13
  3. package/docs/markdown/repositories-consuming.md +44 -2
  4. package/docs/markdown/repositories-file-formats.md +22 -1
  5. package/docs/markdown/repositories-providers.md +20 -10
  6. package/package.json +1 -1
  7. package/recipes/core/sous-skills/sous.recipe.yaml +8 -1
  8. package/src/commands/repo/release.ts +41 -0
  9. package/src/commands/repo/submit.ts +245 -35
  10. package/src/commands/repo/unlink.ts +333 -20
  11. package/src/commands/subscription/update.ts +215 -0
  12. package/src/lib/repos/formats/common.ts +20 -0
  13. package/src/lib/repos/formats/links-map.ts +5 -3
  14. package/src/lib/repos/formats/recipe-manifest.ts +7 -0
  15. package/src/lib/repos/formats/repo-manifest.ts +8 -0
  16. package/src/lib/repos/git-clone.ts +71 -0
  17. package/src/lib/repos/links.ts +2 -1
  18. package/src/lib/repos/locked-recipes.ts +22 -0
  19. package/src/lib/repos/providers/base.ts +33 -1
  20. package/src/lib/repos/providers/github.ts +275 -3
  21. package/src/lib/repos/providers/provider.ts +119 -3
  22. package/src/lib/repos/release/changelog.ts +448 -0
  23. package/src/lib/repos/release/git-state.ts +101 -15
  24. package/src/lib/repos/release/index.ts +2 -0
  25. package/src/lib/repos/release/submissions.ts +214 -0
  26. package/src/lib/repos/release/submit-checkout.ts +271 -0
  27. package/src/lib/repos/release/submit-questions.ts +153 -0
  28. package/src/lib/repos/release/submit-service.ts +581 -174
  29. package/src/lib/repos/resolver.ts +25 -2
  30. package/src/lib/repos/seed.ts +64 -5
  31. package/src/lib/repos/store/hash.ts +68 -8
  32. package/src/lib/repos/subscription-service.ts +744 -20
  33. package/src/lib/repos/update-plan.ts +234 -0
@@ -17,7 +17,8 @@ Every command that works on a project takes these four. `SOUS_CONFIG`, `SOUS_DIR
17
17
  a flag beats its variable, and both beat walk-up discovery. See [Discovery and overrides](config-discovery.md).
18
18
 
19
19
  ?> `repo init`, `repo release` and `repo submit` take none of these. They run inside a recipe repository, which
20
- has no `.sous/` directory to discover. All three still take `--non-interactive`.
20
+ has no `.sous/` directory to discover. All three still take `--non-interactive`. `repo submit` may also be run
21
+ from a project, which it finds by walking up from the working directory.
21
22
 
22
23
  ## Flags that answer questions
23
24
 
@@ -230,9 +231,22 @@ Example: `sous repo link sous-recipes ~/Projects/sous-recipes`, or `sous repo li
230
231
  --generate-branch --latest --yes`
231
232
 
232
233
  ### `sous repo unlink REPO`
233
- Stops reading a repository from a working copy and goes back to published versions; `REPO` is the short name as
234
- it appears in `sous.links.json` (see [File formats](repositories-file-formats.md)). `--global` removes the
235
- machine-wide link, not this project's; `--dry-run` prints what changes. Example: `sous repo unlink sous-recipes`
234
+ Stops reading a repository from a working copy, goes back to the versions the lockfile pins, and rebuilds the
235
+ project; `REPO` is the short name as it appears in `sous.links.json` (see
236
+ [File formats](repositories-file-formats.md)). On its own it also fetches the repository's index, with a short
237
+ timeout, and reports any newer published version the ranges allow, without moving anything; when the index
238
+ cannot be fetched in time it says it could not check. The checkout stays where it is unless `--remove` is passed.
239
+ Takes `--dry-run`, `--no-build`, `--answer` and `--answers-file`.
240
+
241
+ - `--global`: remove the machine-wide link, not this project's.
242
+ - `--update`: move this repository's pins to the newest versions their ranges allow before rebuilding. It runs
243
+ the same code as `sous subscription update REPO`.
244
+ - `--remove`: delete the checkout as well, but only one sous cloned itself; a checkout linked by path is refused
245
+ with an error. Uncommitted changes, commits no remote has and stashes are listed first and asked about.
246
+ - `-y, --yes`: answer every question this command asks: deleting a checkout that holds work, the update plan,
247
+ and the trust question for a repository a newer version needs (also `--force`, `--trust`).
248
+
249
+ Example: `sous repo unlink sous-recipes --update`
236
250
 
237
251
  ### `sous repo init [DIRECTORY]`
238
252
  Creates a new recipe repository in a directory, defaulting to the current one; `--dry-run` prints the files it
@@ -254,17 +268,32 @@ and `git config user.email`), because it commits and cuts annotated tags. Takes
254
268
  - `--no-bump`: raise nothing; a changed recipe that was never raised is then an error.
255
269
  - `--include-unchanged`: release every recipe in scope, changed or not.
256
270
  - `--tag`, `--push`: tag even on a non-default branch, and push the commit and this run's tags.
257
- - `--check`: only validate. It fails on a problem the release would refuse, and reports, without failing, how
258
- merging would rewrite the committed index.
271
+ - `--check`: only validate. It fails on a problem the release would refuse, and on a change to a recipe that
272
+ takes no proposals (see [`submissions`](repositories-file-formats.md#the-submissions-block)), and reports,
273
+ without failing, how merging would rewrite the committed index.
259
274
  - `--ci`: the merge preset. Never bump, never ask, and fail on anything unbumped. It still needs `--yes` to
260
275
  accept the plan it prints, so a merge job runs `sous repo release --ci --yes --push`.
261
276
 
262
277
  Example: `sous repo release --recipe workflow/task-files --bump minor --push`
263
278
 
264
- ### `sous repo submit`
265
- Proposes this repository's committed changes to its maintainers. `--title <text>` defaults to the last commit's
266
- subject and `--body <text>` to a summary sous writes; `--draft` opens the proposal as a draft, and `--dry-run`
267
- prints the plan without sending anything. Example: `sous repo submit --title "Add a linting recipe" --draft`
279
+ ### `sous repo submit [REPO]`
280
+ Proposes a recipe repository's changes to its maintainers, and follows the proposal through: it opens one,
281
+ updates it when there is more to send, reports where it stands, and starts the next one once it was merged. Run
282
+ inside a recipe repository it works there; run inside a project, `REPO` names a linked repository and the
283
+ submission runs in its checkout (with no `REPO`, the only linked repository is used, and several are a
284
+ question). Takes `-y, --yes` and `--dry-run`.
285
+
286
+ - `--title <text>`, `--body <text>`: the proposal's title and description. Both are required for a new
287
+ proposal, and asked for at a terminal when missing; on an open proposal they are optional and replace its own.
288
+ The body is followed by a changelog sous generates.
289
+ - `--branch <name>`: work with this branch instead of the one checked out. A branch that does not exist is
290
+ created from the current commit.
291
+ - `--status`: only report where the branch's proposal stands; nothing is checked, written or sent.
292
+ - `--commit`: commit uncommitted changes for you, after listing them and asking once, with the title, the
293
+ description and the changelog as the message.
294
+ - `--draft`: open a new proposal as a draft.
295
+
296
+ Example: `sous repo submit --title "Add a linting recipe" --body "Adds lint rules for shell scripts." --draft`
268
297
 
269
298
  ## subscription
270
299
 
@@ -290,6 +319,20 @@ Example: `sous subscription add workflow/task-files@^1.2.0 --answer apiUrl=https
290
319
  Removes a subscription and everything only it brought in, then rebuilds so those files are gone. Also spelled
291
320
  `sous unsubscribe`. Takes `--dry-run` and `--no-build`. Example: `sous subscription remove workflow/task-files`
292
321
 
322
+ ### `sous subscription update [REF]`
323
+ Moves the lockfile's pins to the newest published versions their ranges allow, then rebuilds the project. With
324
+ no `REF` it covers every subscription; a `REF` naming a repository, a namespace or a recipe narrows it, and
325
+ everything outside it stays where it is pinned. It fetches every trusted repository's index first, never widens
326
+ a range, moves dependencies with the closure, and changes only the lockfile, never the subscriptions. It prints
327
+ the plan and asks once; with nothing to update it says so and asks nothing. See
328
+ [Moving to newer versions](repositories-consuming.md#moving-to-newer-versions). Takes `--no-build`, `--answer`
329
+ and `--answers-file`, and `--dry-run`, which fetches the indexes but downloads no recipe and writes nothing.
330
+
331
+ - `-y, --yes`: accept the plan, and trust any repository a newer version needs (also `--force`, `--trust`).
332
+ - `--accept-first`: when `REF` matches several things, take the first one listed.
333
+
334
+ Example: `sous subscription update workflow/task-files`
335
+
293
336
  ### `sous subscription list`
294
337
  Lists the subscriptions this project declares, switched-off ones included, with the range each resolves within,
295
338
  the versions the lockfile pins, the latest version each of those recipes has published, where it came from and
@@ -301,7 +344,8 @@ only the subscriptions that have pinned something. Example: `sous subscription l
301
344
 
302
345
  `namespace` reads the cached indexes and the lockfile, so it works offline. A trusted repository whose index has
303
346
  never been fetched is named at the end of a listing, not left out. Both commands take the
304
- [browsing flags](#flags-that-browse); with `--installed` the recipe count is the number installed.
347
+ [browsing flags](#flags-that-browse); with `--installed` the recipe count is the number installed. The core version this installation of sous
348
+ ships is listed even while the cached index does not publish it yet.
305
349
 
306
350
  ### `sous namespace list`
307
351
  Lists every namespace the trusted repositories publish, how many recipes each holds, and how much of it this
@@ -338,8 +382,10 @@ Prints what `.sous/sous.lock.json` pins: the recipe, the version, the repository
338
382
  ### `sous lock rebuild`
339
383
  Recomputes the whole lockfile from the subscriptions the config declares and the cached indexes, starting from
340
384
  empty, so an entry nothing holds any more is dropped rather than carried through: the repair for a file that
341
- drifted through a hand edit or a bad merge. It asks nothing, grants no trust and downloads nothing. Takes
342
- `--dry-run`. Example: `sous lock rebuild --dry-run`
385
+ drifted through a hand edit or a bad merge. It asks nothing, grants no trust and downloads nothing. The core
386
+ version this installation of sous ships resolves even when the cached index has not published it yet, exactly
387
+ as it does in a build. To move pins to versions published since the last fetch, use `sous subscription update`.
388
+ Takes `--dry-run`. Example: `sous lock rebuild --dry-run`
343
389
 
344
390
  ## vars
345
391
 
@@ -4,8 +4,9 @@ The guide to publishing recipes of your own: creating a repository, writing a re
4
4
  needs, cutting a release, editing a published repository in place, and proposing a change to somebody else's.
5
5
  [Repository file formats](repositories-file-formats.md) holds the schemas; this page is the workflow.
6
6
 
7
- ?> A recipe repository is not a sous project. It has no `.sous/` directory, and `sous repo init`, `sous repo
8
- release` and `sous repo submit` do not look for one. Run them from inside the repository itself.
7
+ ?> A recipe repository is not a sous project. It has no `.sous/` directory, and `sous repo init` and `sous repo
8
+ release` do not look for one. Run them from inside the repository itself. `sous repo submit` runs there too, and
9
+ may also be run from a project that links the repository.
9
10
 
10
11
  ## Create a repository
11
12
 
@@ -196,8 +197,9 @@ third case is what a merge looks like to continuous integration: the bump is don
196
197
  `--namespace <ns>` and `--recipe <ns/name>` narrow the run and both repeat; `--bump <level>` is `patch` (the
197
198
  default), `minor`, `major` or `prerelease`; `--no-bump` raises nothing; `--include-unchanged` releases every
198
199
  recipe in scope, changed or not; `--check` only reads, validating, failing on any problem the release would
199
- refuse, and reporting how merging would rewrite the committed index without failing on it, since that index is
200
- the release's output; and `--ci` is the merge preset: never bump, accept the plan, never ask, fail on anything unbumped. Every flag
200
+ refuse and on a change to a recipe whose [`submissions`](repositories-file-formats.md#the-submissions-block)
201
+ block says it takes no proposals, and reporting how merging would rewrite the committed index without failing
202
+ on it, since that index is the release's output; and `--ci` is the merge preset: never bump, accept the plan, never ask, fail on anything unbumped. Every flag
201
203
  this command takes, `--tag`, `--push` and `--non-interactive` among them, is in the
202
204
  [command reference](commands.md#sous-repo-release).
203
205
 
@@ -235,7 +237,8 @@ missing from the index is rebuilt from it whenever the index is regenerated, whi
235
237
  publishes something. A bump edits the manifest in place, so comments, field order and layout survive; a folded
236
238
  block of YAML prose may be re-wrapped and the space before a trailing comment collapsed to one.
237
239
 
238
- !> A release commits the version bumps and the index, and nothing else. It refuses to run while anything else is
240
+ !> A release commits the version bumps and the index, and nothing else; the only other commit sous ever makes
241
+ for you is `sous repo submit --commit`. It refuses to run while anything else is
239
242
  uncommitted, because a tag names one commit and the index records what each recipe folder holds right now. It also
240
243
  refuses when git does not know who is committing: set `git config user.name` and `git config user.email` first, or
241
244
  give that identity to the account a continuous integration job runs as (the scaffolded workflow already does).
@@ -286,8 +289,29 @@ and freshness checks no longer apply to it. Builds say so every time.
286
289
  Run 'sous repo unlink my-recipes' to go back to published versions.
287
290
  ```
288
291
 
289
- `sous repo unlink` removes the map entry and nothing else: the checkout stays where it is, and its path is
290
- printed so you can delete it. Unlinking a name linked in the other scope says which scope holds it.
292
+ ### Unlink, and pick up your release
293
+
294
+ Linking never touches the lockfile, so `sous repo unlink my-recipes` goes back to exactly the versions pinned
295
+ before the link, and rebuilds the project so its outputs match them. It also fetches the repository's index,
296
+ with a short timeout, and says whether a newer version the ranges allow has been published since; it moves
297
+ nothing, and when the index cannot be fetched in time it says it could not check. Unlinking a name linked in
298
+ the other scope says which scope holds it.
299
+
300
+ Once your change is released, one of these moves the project onto it:
301
+
302
+ ```bash
303
+ sous repo unlink my-recipes --update # unlink, move this repository's pins, then rebuild
304
+ sous subscription update my-recipes # the same move, when nothing is linked any more
305
+ ```
306
+
307
+ `--update` runs exactly the code `sous subscription update my-recipes` runs: every pin the repository supplies
308
+ moves to the newest version its range allows, after one plan and one question. See
309
+ [Moving to newer versions](repositories-consuming.md#moving-to-newer-versions).
310
+
311
+ The checkout stays where it is unless you pass `--remove`, because `sous repo submit` uses a leftover checkout to
312
+ revise a proposal that is still open. `--remove` deletes it only when sous cloned it; a checkout you linked by
313
+ path is yours, and sous refuses with an error. Before deleting, sous lists anything in the checkout that exists
314
+ nowhere else (uncommitted changes, commits no remote has, and stashes) and asks; `--yes` answers ahead.
291
315
 
292
316
  ### How a checkout compares with upstream
293
317
 
@@ -344,22 +368,60 @@ changing its branch says it affects all of them.
344
368
 
345
369
  ## Contribute to someone else's repository
346
370
 
347
- `sous repo submit` proposes your committed changes to a repository's maintainers; it never publishes and never
348
- writes to a repository directly. It takes `--title`, `--body`, `--draft` and `--dry-run`, and runs three stages,
349
- printing each step:
371
+ `sous repo submit` proposes a change to a repository's maintainers and follows it through; it never publishes
372
+ and never writes to a repository directly. Run it inside the recipe repository you changed, or from a project
373
+ that links it: `sous repo submit sous-recipes` runs in the linked checkout, and with no argument the project's
374
+ only linked repository is used (several are a question). A repository you have since unlinked is still
375
+ submitted from the checkout sous cloned for it, with a note saying so; with no checkout at all there is no
376
+ working copy to propose from, and `submit` says so.
377
+
378
+ It prints each step as it runs them:
350
379
 
351
380
  1. **Preflight.** An `origin` remote exists, sous recognizes its provider, that provider's command line tool
352
- (`gh` or `glab`) is installed and signed in, and everything is committed.
381
+ (`gh` or `glab`) is installed and signed in, and everything is committed. `--commit` lifts that last rule:
382
+ sous lists what is uncommitted, asks once (`--yes` answers), checks git knows who is committing before
383
+ writing anything, and commits it all with the proposal's title, description and changelog as the message.
353
384
  2. **Validation.** The repository validates, and your change leaves `sous.index.json` as it found it, so a
354
385
  proposal never fails the maintainer's own checks and wastes their review. The index is written by the
355
386
  repository's own release after a merge; whether it agrees with the release tags is checked there, by
356
387
  `sous repo release --check` on a full clone, not by `submit`. That is what lets `submit` run from the shallow
357
388
  checkout `sous repo link` makes, which holds almost none of the tags. The comparison is made against the
358
389
  copy of the default branch your checkout holds (`origin/main`, for example); when it holds none, `submit`
359
- says the check was skipped.
390
+ says the check was skipped. A change that touches a recipe whose
391
+ [`submissions`](repositories-file-formats.md#the-submissions-block) block says it takes no proposals is
392
+ warned about, with where to send it instead, and proposed only if you carry on.
360
393
  3. **Delegation.** Sous asks the provider whether you can push to the repository itself, forks it onto your
361
394
  account when you cannot, pushes the branch, and asks the provider to open the proposal. A change sitting on
362
- the default branch is moved to `sous/submit-<YYYYMMDD>-<HHMM>`.
395
+ the default branch is moved to `sous/submit-<YYYYMMDD>-<HHMM>`, or to the branch `--branch` names.
396
+
397
+ **A title and a description are yours to write.** A new proposal needs both: pass `--title` and `--body`, or
398
+ answer the two questions at a terminal (Tab opens your editor for a longer description). Sous never borrows a
399
+ commit message. The body is your description followed by a changelog sous generates by comparing the manifests
400
+ your change carries with the default branch:
401
+
402
+ - recipes added, and recipes retired (a renamed recipe shows as one of each);
403
+ - version changes;
404
+ - recipes whose files changed without a version raise, which merging will release as the next patch;
405
+ - namespaces added or removed;
406
+ - variables added, removed or changed, with a warning that removing a variable or tightening its validation is
407
+ usually a major change.
408
+
409
+ The changelog explains; it never refuses. Only what the repository's own checks would reject stops a submission.
410
+
411
+ **One command for the proposal's whole life.** Every run looks up the proposal for the current branch (or the
412
+ one `--branch` names), by the fork's owner as well as the branch when you work through a fork, and then:
413
+
414
+ | The branch's proposal | What `submit` does |
415
+ |-----------------------|--------------------|
416
+ | none | Opens one |
417
+ | open, with new commits | Pushes them, which updates it; a given `--title` or `--body` replaces its own |
418
+ | open, with nothing new | Reports where it stands: review, checks, whether it can merge |
419
+ | merged | Says so, then continues on a new branch: you name one, sous generates one, or you cancel. `--yes` generates one |
420
+ | closed without merging | Says so, and opens a fresh one for the branch |
421
+
422
+ When the branch on the remote holds commits yours lacks (a maintainer pushed to it), git refuses the push and
423
+ `submit` passes git's own explanation through and stops. It never forces a push. `--status` only reports, and
424
+ `--dry-run` works everything out and writes and sends nothing.
363
425
 
364
426
  Each step is the provider's own business, and what each one can do depends on the host; see
365
427
  [Providers](repositories-providers.md#proposing-a-change). Sous sequences the steps and reports what came back. A
@@ -220,7 +220,48 @@ build` instead. The `Latest version` column beside it is read from the cache, or
220
220
  `sous repo list` shows each trusted repository with its provider, origin, whether it is linked, its
221
221
  recipe count and its URL; `--verbose` adds a `Namespaces:` line under each row. `sous lock show`
222
222
  prints the other half: every version your lockfile pins, where it came from and who holds it. When
223
- that file has drifted, `sous lock rebuild` recomputes it from your subscriptions.
223
+ that file has drifted, `sous lock rebuild` recomputes it from your subscriptions and the cached
224
+ indexes, fetching nothing.
225
+
226
+ ## Moving to newer versions
227
+
228
+ A build holds the versions the lockfile pins, so a newer release reaches the project only when
229
+ something moves the pin. `sous subscription update` is that step:
230
+
231
+ ```term
232
+ $ sous subscription update workflow/qa-variables
233
+ Updating the recipe 'qa-recipes:workflow/qa-variables' changes the lockfile:
234
+
235
+ • Adding workflow/qa-helper version 1.0.0
236
+ • Updating workflow/qa-variables from version 0.1.0 to version 0.2.0
237
+
238
+ Update the lockfile? (y/N)
239
+ ```
240
+
241
+ It fetches every trusted repository's index first, so a version published a minute ago counts.
242
+ With no reference it covers every subscription; a reference names a repository, a namespace or a
243
+ recipe, at any level of qualification, and everything outside it stays exactly where it is pinned.
244
+ What it will and will not do:
245
+
246
+ - A pin moves only within the range its subscription declares, or the range the recipe depending
247
+ on it declares; a range is never widened. Prereleases count only for a subscription that opted
248
+ into them.
249
+ - Dependencies move with the closure: a dependency a newer version adds is pinned, and one it
250
+ dropped leaves the lockfile unless something else still holds it.
251
+ - Only the lockfile changes. Your subscriptions stay exactly as they are written.
252
+ - It prints the plan and asks once; `--yes` accepts it, and a run with no terminal and no `--yes`
253
+ fails naming the flag. With nothing to update it says so and asks nothing.
254
+ - A repository a newer version needs that the project does not trust goes through the usual trust
255
+ question, and new questions the newer versions ask are asked the way subscribing asks them.
256
+ - A repository whose index cannot be fetched is skipped and named, and its pins stay put.
257
+ - A linked repository's pins move too, and the plan notes that builds keep reading the checkout
258
+ until it is unlinked.
259
+ - The built-in `core` subscription never moves: its range is exactly the running sous version, so
260
+ upgrading sous is what moves it.
261
+
262
+ It rebuilds the project afterwards. `--dry-run` fetches the indexes and prints the plan, and
263
+ downloads no recipe and writes nothing, so the dependencies and questions of a version this machine
264
+ does not hold yet are named rather than listed. `--no-build` skips the rebuild.
224
265
 
225
266
  ## Remove a subscription
226
267
 
@@ -290,7 +331,8 @@ lapsed (`store.freshnessSeconds`, five minutes by default), or when a command fo
290
331
  check never breaks a build, because the cached index is used instead. Always-pull changes what
291
332
  happens after that check, not how often it happens: a repository or subscription marked
292
333
  `alwaysPull` takes a newer in-range version rather than the locked one; set it with
293
- `--always-pull`, or on either entry in the config.
334
+ `--always-pull`, or on either entry in the config. Without it, `sous subscription update` is how a
335
+ pin moves; see [Moving to newer versions](#moving-to-newer-versions).
294
336
 
295
337
  Every other repository the lockfile pins from is checked on the same window, only to tell you what
296
338
  is newer. A build lists each recipe with a newer version inside the range declared for it, beside
@@ -54,6 +54,7 @@ recipes: # every recipe folder, relative to the repos
54
54
  | `description`, `contribute` | no | A summary for anyone reading the repository, and where to send a contribution when a provider cannot support `sous repo submit` |
55
55
  | `namespaces` | yes | Keyed by namespace name; each entry takes an optional `description` |
56
56
  | `recipes` | yes | Relative paths, each holding a recipe manifest; a path listed twice is an error |
57
+ | `submissions` | no | Whether the repository's recipes take proposed changes; see [The submissions block](#the-submissions-block) |
57
58
 
58
59
  ## `sous.recipe.yaml`: the recipe manifest
59
60
 
@@ -77,11 +78,30 @@ contents: # what a subscriber actually receives
77
78
  | `description` | no | Copied into the index at release time; shown by `sous recipe list` and `sous repo search` |
78
79
  | `depends`, `subscribes` | no | Build dependencies and co-subscriptions; within either list, no entry may repeat |
79
80
  | `contents`, `variables` | no | Contents default to an empty list, which is what a curated bundle publishes; a duplicated variable name, or two definitions claiming one `env`, is an error |
81
+ | `submissions` | no | Whether this recipe takes proposed changes, winning over the repository's block; see [The submissions block](#the-submissions-block) |
80
82
 
81
83
  `contents[].kind` is `skills`, `memories`, `prompts` or `config`. The first three are written to the
82
84
  destinations named by [`recipeOutputs`](#recipeoutputs-where-the-files-land); `config` entries become config
83
85
  layers instead. `include` needs at least one glob, and both lists are recipe-relative.
84
86
 
87
+ ### The submissions block
88
+
89
+ Both manifests accept a `submissions` block. On `sous.repo.yaml` it covers every recipe in the repository; on
90
+ `sous.recipe.yaml` it covers that recipe, and wins over the repository's.
91
+
92
+ ```yaml
93
+ submissions:
94
+ allowed: false # defaults to true
95
+ instead: Propose changes in sous-io/sous, under recipes/core/sous-skills/.
96
+ ```
97
+
98
+ It exists for a recipe whose files are copied in from somewhere else: an edit merged into the copy is
99
+ overwritten by the next copy, and a version it tagged can collide with the one the real source publishes.
100
+ `sous repo submit` warns before proposing a change that touches such a recipe, prints the `instead` text, and
101
+ proposes it if the contributor carries on. `sous repo release --check` fails a pull request that changes one,
102
+ because a pull request can be opened without `submit`, and the check is the one gate every change passes. A
103
+ release itself is never restricted, so whatever publishes the recipe still releases it.
104
+
85
105
  ### Dependencies named by location
86
106
 
87
107
  `depends` and `subscribes` share one grammar in two spellings: `workflow/qa-helper` is a sibling in this
@@ -232,7 +252,8 @@ freshness checks; entries are keyed by the repository's configured short name.
232
252
  ```
233
253
 
234
254
  `path` is absolute; `origin` is `clone` when sous cloned the working copy itself and `path` when it was
235
- pointed at an existing checkout, and unlinking removes the entry while leaving either in place. Both the
255
+ pointed at an existing checkout. Unlinking removes the entry and leaves the checkout in place, unless
256
+ `sous repo unlink --remove` is asked to delete one sous cloned; it never deletes a `path` checkout. Both the
236
257
  project's `.sous/sous.links.json` and `$SOUS_HOME/sous.links.json` are read, and the project's entries win.
237
258
 
238
259
  ## Configuration keys
@@ -21,8 +21,12 @@ Nothing there clones a whole repository. A provider that can also carry a contri
21
21
  the **write path**: report whether its command line tool is installed and signed in, say whether
22
22
  you may push to the repository itself, fork it onto your account, and open the proposal.
23
23
 
24
- Each provider declares which of the two features, `fetch` and `submit`, it genuinely answers, and
25
- sous consults that declaration rather than a provider's name. Asking for something outside a
24
+ A provider may also answer the **proposals** path, which is what lets a submission follow a proposal
25
+ after it is opened: find the proposal a branch was pushed for, report where it stands, and replace its
26
+ title or body.
27
+
28
+ Each provider declares which of the three features, `fetch`, `submit` and `proposals`, it genuinely
29
+ answers, and sous consults that declaration rather than a provider's name. Asking for something outside a
26
30
  provider's features is refused with a sentence naming it and what it cannot do, never a crash.
27
31
 
28
32
  ## How a URL is matched
@@ -184,11 +188,15 @@ finds the index but cannot clone has only the second half missing.
184
188
  your repository, then hands the host-specific mechanics to the provider that owns its `origin`
185
189
  remote. Providers differ, and sous says so rather than pretending otherwise:
186
190
 
187
- | Provider | Tool | Push permission | Forking | Proposal |
188
- |---|---|---|---|---|
189
- | `github` | `gh` | read from GitHub, so a contributor without it is forked automatically | `gh repo fork`, with a `fork` remote added for you | pull request |
190
- | `gitlab` | `glab` | sous cannot tell, so it pushes to `origin` and says so | not done for you | merge request |
191
- | `local` | none | not applicable | not applicable | not applicable |
191
+ | Provider | Tool | Push permission | Forking | Proposal | Following it up |
192
+ |---|---|---|---|---|---|
193
+ | `github` | `gh` | read from GitHub, so a contributor without it is forked automatically | `gh repo fork`, with a `fork` remote added for you | pull request | `gh pr list`, `gh pr view` and `gh pr edit`, matching a fork's pull request by its owner |
194
+ | `gitlab` | `glab` | sous cannot tell, so it pushes to `origin` and says so | not done for you | merge request | not done: every run opens a merge request, and `--status` is refused |
195
+ | `local` | none | not applicable | not applicable | not applicable | not applicable |
196
+
197
+ **GitLab cannot follow a proposal up yet.** It does not declare `proposals`, so a submission cannot
198
+ look for a merge request that is already open; it says so, pushes, and opens one. When the branch
199
+ already has one, the push updates it and GitLab may refuse the second.
192
200
 
193
201
  **GitLab reports "cannot tell" rather than guessing.** Sous has no cheap, reliable way to ask
194
202
  whether you may push, and a wrong guess would send you down a fork path this provider cannot
@@ -297,14 +305,16 @@ subclass overrides it. Each member is documented where it lives, in
297
305
  The subclass supplies the rest:
298
306
 
299
307
  - `id`, the identifier a repository entry and a locator scheme use;
300
- - `features`, the ones it genuinely answers (`fetch`, and `submit` only if all four write calls
301
- are real);
308
+ - `features`, the ones it genuinely answers (`fetch`; `submit` only if all four write calls are
309
+ real; `proposals` only if `findProposal`, `proposalStatus` and `updateProposal` are);
302
310
  - `matches(url)` and `canonicalize(url)`, the URL half;
303
311
  - `fetchIndex(repo, options)` and `fetchRecipeTree(repo, recipePath, tag, destDir, options)`, the
304
312
  read half;
305
313
  - `cli` and `proposalNoun` when it submits, so messages can name the tool and call a proposal
306
314
  what the host calls it;
307
- - overrides of the four write-path calls when it submits.
315
+ - overrides of the four write-path calls when it submits, and of the three proposals calls when
316
+ it can follow a proposal up. Each answers with plain data (a proposal's id, address, state,
317
+ title, review and check counts), never with a host's own vocabulary.
308
318
 
309
319
  Every call takes an options object carrying the testing seams (`cwd`, `env`, `fetchImpl`, `run`),
310
320
  which is why no provider reaches for `spawn` or the global `fetch` directly and no test in this
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sous-io/sous",
3
- "version": "0.2.16",
3
+ "version": "0.2.18",
4
4
  "description": "Compiles AI coding agent configuration (CLAUDE.md, skills, memories) from LiquidJS templates",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -11,7 +11,7 @@ formatVersion: 1
11
11
 
12
12
  namespace: core
13
13
  name: sous-skills
14
- version: 0.2.16
14
+ version: 0.2.18
15
15
 
16
16
  description: >-
17
17
  The skills that teach an agent what sous is and how it works: which files sous
@@ -19,6 +19,13 @@ description: >-
19
19
  debugged, the .tpl. template convention and LiquidJS syntax, what an agent
20
20
  skill is, and how to create one.
21
21
 
22
+ # The copy in sous-io/sous-recipes is overwritten by every sous release, so a
23
+ # change proposed there would be lost, and a version it tagged would collide
24
+ # with the one the release publishes.
25
+ submissions:
26
+ allowed: false
27
+ instead: Propose changes in sous-io/sous, under recipes/core/sous-skills/.
28
+
22
29
  contents:
23
30
  - kind: skills
24
31
  include:
@@ -11,6 +11,7 @@ import {
11
11
  anythingToCommit,
12
12
  buildIndex,
13
13
  buildReleasePlan,
14
+ checkSubmissions,
14
15
  bumpRecipeVersion,
15
16
  commitPaths,
16
17
  createAnnotatedTag,
@@ -391,6 +392,46 @@ export default class RepoRelease extends Command {
391
392
  log(" records for every version it publishes.");
392
393
  }
393
394
  reportPending(result, "These versions have no tag yet; they publish when this merges:");
395
+
396
+ // A pull request can be opened without `sous repo submit`, so this check is
397
+ // the one gate every change passes. The release itself is not restricted:
398
+ // whatever publishes such a recipe still releases it.
399
+ section("Checking the recipes that take no proposals");
400
+ const submissions = await checkSubmissions(validation);
401
+ if (submissions.refusing.length > 0) {
402
+ const problems: ValidationProblem[] = submissions.refusing.map((recipe) => ({
403
+ level: "error",
404
+ where: recipe.path,
405
+ message:
406
+ `this change touches '${recipe.key}', which does not take proposed changes ` +
407
+ `(${recipe.declaredBy === "recipe" ? "its own manifest" : "the repository manifest"} ` +
408
+ `says so). Merging it would break whatever publishes the recipe.` +
409
+ (recipe.instead === undefined ? "" : ` Instead: ${recipe.instead}`),
410
+ }));
411
+ reportProblems(problems);
412
+ displayErrorBlock(
413
+ `This change cannot be merged: it touches ` +
414
+ `${describeCount(problems.length, "recipe")} that ${problems.length === 1 ? "does" : "do"} ` +
415
+ `not take proposed changes, listed above.\n` +
416
+ ` Take those edits out of the change, and send them where each recipe asks.`
417
+ );
418
+ return this.exit(1);
419
+ }
420
+ if (submissions.comparedWith.kind === "branch") {
421
+ log(
422
+ ` Compared with the branch '${submissions.comparedWith.branch}': this change touches no ` +
423
+ `recipe that declines proposals.`
424
+ );
425
+ } else if (submissions.comparedWith.kind === "tags") {
426
+ log(" Compared with each recipe's last release tag: nothing that declines proposals changed.");
427
+ } else if (submissions.comparedWith.kind === "none declined") {
428
+ log(" No recipe in this repository declines proposed changes.");
429
+ } else {
430
+ note(
431
+ "This checkout holds neither a copy of the default branch nor a release tag of any " +
432
+ "recipe that declines proposed changes, so there was nothing to compare with."
433
+ );
434
+ }
394
435
  footer();
395
436
  }
396
437