@sous-io/sous 0.2.36 → 1.0.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 (244) hide show
  1. package/README.md +14 -256
  2. package/dist/bin.js +11 -0
  3. package/dist/sous.js +1093 -0
  4. package/docs/markdown/README.md +5 -4
  5. package/docs/markdown/_sidebar.md +21 -1
  6. package/docs/markdown/commands.md +294 -431
  7. package/docs/markdown/config-discovery.md +25 -14
  8. package/docs/markdown/config-inspection.md +53 -31
  9. package/docs/markdown/config-layers.md +168 -66
  10. package/docs/markdown/config-variables.md +44 -11
  11. package/docs/markdown/configuration.md +104 -17
  12. package/docs/markdown/design-principles.md +4 -1
  13. package/docs/markdown/repositories-authoring.md +30 -0
  14. package/docs/markdown/repositories-consuming.md +72 -14
  15. package/docs/markdown/repositories-file-formats.md +31 -13
  16. package/docs/markdown/repositories-providers.md +48 -57
  17. package/docs/markdown/repositories-quickstart.md +3 -2
  18. package/docs/markdown/repositories-troubleshooting.md +27 -1
  19. package/docs/markdown/repositories-variables.md +6 -1
  20. package/docs/markdown/repositories.md +18 -8
  21. package/package.json +48 -107
  22. package/recipes/core/sous-skills/skills/about-liquid-templates/SKILL.tpl.md +4 -6
  23. package/recipes/core/sous-skills/skills/about-liquid-templates/references/liquid-filters.tpl.md +2 -0
  24. package/recipes/core/sous-skills/skills/about-sous/SKILL.tpl.md +10 -9
  25. package/recipes/core/sous-skills/skills/about-sous-configuration/SKILL.tpl.md +35 -14
  26. package/recipes/core/sous-skills/skills/about-sous-recipes/SKILL.tpl.md +51 -20
  27. package/recipes/core/sous-skills/sous.recipe.yaml +1 -1
  28. package/sous.config.schema.json +351 -132
  29. package/bin/run.js +0 -51
  30. package/src/base-command.ts +0 -457
  31. package/src/commands/build.ts +0 -181
  32. package/src/commands/clear.ts +0 -79
  33. package/src/commands/compile.ts +0 -155
  34. package/src/commands/config/get.ts +0 -110
  35. package/src/commands/config/show.ts +0 -32
  36. package/src/commands/config/validate.ts +0 -53
  37. package/src/commands/help.ts +0 -46
  38. package/src/commands/init.ts +0 -289
  39. package/src/commands/launch.ts +0 -139
  40. package/src/commands/lock/rebuild.ts +0 -239
  41. package/src/commands/lock/show.ts +0 -115
  42. package/src/commands/namespace/list.ts +0 -138
  43. package/src/commands/namespace/show.ts +0 -130
  44. package/src/commands/prune.ts +0 -43
  45. package/src/commands/recipe/list.ts +0 -112
  46. package/src/commands/recipe/show.ts +0 -439
  47. package/src/commands/repo/add.ts +0 -145
  48. package/src/commands/repo/contribute.ts +0 -604
  49. package/src/commands/repo/gc.ts +0 -173
  50. package/src/commands/repo/init.ts +0 -136
  51. package/src/commands/repo/link.ts +0 -899
  52. package/src/commands/repo/list.ts +0 -236
  53. package/src/commands/repo/release.ts +0 -685
  54. package/src/commands/repo/remove.ts +0 -194
  55. package/src/commands/repo/search.ts +0 -243
  56. package/src/commands/repo/submit.ts +0 -341
  57. package/src/commands/repo/unlink.ts +0 -461
  58. package/src/commands/subscription/add.ts +0 -286
  59. package/src/commands/subscription/list.ts +0 -208
  60. package/src/commands/subscription/remove.ts +0 -182
  61. package/src/commands/subscription/update.ts +0 -216
  62. package/src/commands/update.ts +0 -178
  63. package/src/commands/vars/ask.ts +0 -417
  64. package/src/commands/vars/index.ts +0 -79
  65. package/src/commands/vars/list.ts +0 -67
  66. package/src/commands/vars/show.ts +0 -77
  67. package/src/config-command.ts +0 -30
  68. package/src/lib/build-preparation.ts +0 -128
  69. package/src/lib/build-service.ts +0 -580
  70. package/src/lib/config-discovery.ts +0 -378
  71. package/src/lib/config-inspect.ts +0 -145
  72. package/src/lib/config-kernel.mjs +0 -379
  73. package/src/lib/config-schema.ts +0 -401
  74. package/src/lib/env-file.ts +0 -328
  75. package/src/lib/env-local.ts +0 -212
  76. package/src/lib/errors.ts +0 -32
  77. package/src/lib/glob-pattern.ts +0 -24
  78. package/src/lib/include-resolver.ts +0 -523
  79. package/src/lib/interactive.ts +0 -165
  80. package/src/lib/markdown-compiler.ts +0 -947
  81. package/src/lib/package-info.ts +0 -25
  82. package/src/lib/pid-service.ts +0 -99
  83. package/src/lib/project-install.d.mts +0 -44
  84. package/src/lib/project-install.mjs +0 -234
  85. package/src/lib/project-scaffold/index.ts +0 -332
  86. package/src/lib/project-scaffold/templates.ts +0 -240
  87. package/src/lib/repos/catalog-display.ts +0 -215
  88. package/src/lib/repos/catalog-inputs.ts +0 -288
  89. package/src/lib/repos/catalog.ts +0 -842
  90. package/src/lib/repos/contribute.ts +0 -452
  91. package/src/lib/repos/core-recipe.ts +0 -105
  92. package/src/lib/repos/declarations.ts +0 -187
  93. package/src/lib/repos/defaults.ts +0 -175
  94. package/src/lib/repos/formats/common.ts +0 -440
  95. package/src/lib/repos/formats/index-file.ts +0 -253
  96. package/src/lib/repos/formats/links-map.ts +0 -98
  97. package/src/lib/repos/formats/lockfile.ts +0 -169
  98. package/src/lib/repos/formats/patterns.ts +0 -57
  99. package/src/lib/repos/formats/recipe-manifest.ts +0 -205
  100. package/src/lib/repos/formats/repo-manifest.ts +0 -97
  101. package/src/lib/repos/formats/store-entry.ts +0 -84
  102. package/src/lib/repos/formats/variable-definition.ts +0 -268
  103. package/src/lib/repos/freshness.ts +0 -264
  104. package/src/lib/repos/git-clone.ts +0 -850
  105. package/src/lib/repos/identity.ts +0 -89
  106. package/src/lib/repos/index.ts +0 -57
  107. package/src/lib/repos/links.ts +0 -356
  108. package/src/lib/repos/load-manifest.ts +0 -237
  109. package/src/lib/repos/lock-service.ts +0 -454
  110. package/src/lib/repos/locked-namespace-resolver.ts +0 -232
  111. package/src/lib/repos/locked-recipe-lookup.ts +0 -123
  112. package/src/lib/repos/locked-recipes.ts +0 -254
  113. package/src/lib/repos/managed-layer.ts +0 -422
  114. package/src/lib/repos/memories-hash-name.ts +0 -33
  115. package/src/lib/repos/namespace-resolver.ts +0 -527
  116. package/src/lib/repos/providers/base.ts +0 -268
  117. package/src/lib/repos/providers/git.ts +0 -237
  118. package/src/lib/repos/providers/github.ts +0 -589
  119. package/src/lib/repos/providers/gitlab.ts +0 -332
  120. package/src/lib/repos/providers/http.ts +0 -128
  121. package/src/lib/repos/providers/index-cache.ts +0 -421
  122. package/src/lib/repos/providers/index.ts +0 -106
  123. package/src/lib/repos/providers/local.ts +0 -391
  124. package/src/lib/repos/providers/provider.ts +0 -615
  125. package/src/lib/repos/recipe-config-layers.ts +0 -316
  126. package/src/lib/repos/recipe-key-matcher.ts +0 -62
  127. package/src/lib/repos/recipe-memories.ts +0 -178
  128. package/src/lib/repos/recipe-targets.ts +0 -236
  129. package/src/lib/repos/ref-search.ts +0 -46
  130. package/src/lib/repos/release/bump.ts +0 -161
  131. package/src/lib/repos/release/changelog.ts +0 -461
  132. package/src/lib/repos/release/git-state.ts +0 -460
  133. package/src/lib/repos/release/index-builder.ts +0 -1108
  134. package/src/lib/repos/release/index.ts +0 -19
  135. package/src/lib/repos/release/plan.ts +0 -521
  136. package/src/lib/repos/release/settle.ts +0 -154
  137. package/src/lib/repos/release/submissions.ts +0 -214
  138. package/src/lib/repos/release/submit-checkout.ts +0 -290
  139. package/src/lib/repos/release/submit-questions.ts +0 -153
  140. package/src/lib/repos/release/submit-service.ts +0 -898
  141. package/src/lib/repos/release/tags.ts +0 -239
  142. package/src/lib/repos/release/validate.ts +0 -463
  143. package/src/lib/repos/resolver.ts +0 -1028
  144. package/src/lib/repos/scaffold/index.ts +0 -245
  145. package/src/lib/repos/scaffold/templates.ts +0 -517
  146. package/src/lib/repos/seed.ts +0 -473
  147. package/src/lib/repos/store/contract.ts +0 -64
  148. package/src/lib/repos/store/hash.ts +0 -191
  149. package/src/lib/repos/store/recipe-store.ts +0 -599
  150. package/src/lib/repos/store/settings.ts +0 -58
  151. package/src/lib/repos/subscription-service.ts +0 -3859
  152. package/src/lib/repos/trust.ts +0 -447
  153. package/src/lib/repos/update-plan.ts +0 -235
  154. package/src/lib/self-update/index.ts +0 -14
  155. package/src/lib/self-update/installs.ts +0 -412
  156. package/src/lib/self-update/managers.ts +0 -126
  157. package/src/lib/self-update/plan.ts +0 -315
  158. package/src/lib/self-update/registry.ts +0 -99
  159. package/src/lib/self-update/run.ts +0 -506
  160. package/src/lib/self-update/versions.ts +0 -243
  161. package/src/lib/settings.ts +0 -1105
  162. package/src/lib/sous-home.ts +0 -104
  163. package/src/lib/state.ts +0 -167
  164. package/src/lib/vars/answers.ts +0 -192
  165. package/src/lib/vars/ask.ts +0 -1152
  166. package/src/lib/vars/definition-source.ts +0 -261
  167. package/src/lib/vars/display.ts +0 -231
  168. package/src/lib/vars/index.ts +0 -20
  169. package/src/lib/vars/ladder.ts +0 -282
  170. package/src/lib/vars/mappings.ts +0 -260
  171. package/src/lib/vars/names.ts +0 -94
  172. package/src/lib/vars/preanswers.ts +0 -395
  173. package/src/lib/vars/question-plan.ts +0 -241
  174. package/src/lib/vars/report.ts +0 -228
  175. package/src/lib/vars/safe-regex.ts +0 -235
  176. package/src/lib/vars/secrets.ts +0 -131
  177. package/src/lib/vars/validate.ts +0 -312
  178. package/src/lib/version-report.ts +0 -90
  179. package/src/lib/watch-loop.ts +0 -148
  180. package/src/lib/watch-service.ts +0 -116
  181. package/src/services/ref-resolver/container.ts +0 -107
  182. package/src/services/ref-resolver/dependency.ts +0 -60
  183. package/src/services/ref-resolver/format.ts +0 -197
  184. package/src/services/ref-resolver/glob.ts +0 -115
  185. package/src/services/ref-resolver/hash-names.ts +0 -155
  186. package/src/services/ref-resolver/index.ts +0 -55
  187. package/src/services/ref-resolver/injectable.ts +0 -36
  188. package/src/services/ref-resolver/location.ts +0 -62
  189. package/src/services/ref-resolver/lookups/cached-index-lookup.ts +0 -53
  190. package/src/services/ref-resolver/lookups/catalog-lookup.ts +0 -23
  191. package/src/services/ref-resolver/lookups/catalog-matcher.ts +0 -261
  192. package/src/services/ref-resolver/lookups/chained-lookup.ts +0 -27
  193. package/src/services/ref-resolver/lookups/env-var-lookup.ts +0 -65
  194. package/src/services/ref-resolver/lookups/fetched-index-lookup.ts +0 -150
  195. package/src/services/ref-resolver/lookups/recorded-dependency-lookup.ts +0 -62
  196. package/src/services/ref-resolver/lookups/ref-lookup.ts +0 -54
  197. package/src/services/ref-resolver/lookups/variable-lookup.ts +0 -123
  198. package/src/services/ref-resolver/named-refs.ts +0 -144
  199. package/src/services/ref-resolver/parser/env-var-splitter.ts +0 -27
  200. package/src/services/ref-resolver/parser/location-splitter.ts +0 -239
  201. package/src/services/ref-resolver/parser/name-path-splitter.ts +0 -200
  202. package/src/services/ref-resolver/parser/partial-ref.ts +0 -84
  203. package/src/services/ref-resolver/parser/query-splitter.ts +0 -61
  204. package/src/services/ref-resolver/parser/range-splitter.ts +0 -52
  205. package/src/services/ref-resolver/parser/ref-parser.ts +0 -115
  206. package/src/services/ref-resolver/parser/repo-qualifier-splitter.ts +0 -51
  207. package/src/services/ref-resolver/parts.ts +0 -115
  208. package/src/services/ref-resolver/pruners/command-line-pruner.ts +0 -47
  209. package/src/services/ref-resolver/pruners/include-pruner.ts +0 -66
  210. package/src/services/ref-resolver/pruners/manifest-pruner.ts +0 -130
  211. package/src/services/ref-resolver/pruners/ref-pruner.ts +0 -135
  212. package/src/services/ref-resolver/pruners/rule-helpers.ts +0 -44
  213. package/src/services/ref-resolver/pruners/stored-key-pruner.ts +0 -136
  214. package/src/services/ref-resolver/ref-helpers.ts +0 -62
  215. package/src/services/ref-resolver/ref-picker-service.ts +0 -204
  216. package/src/services/ref-resolver/ref-report.ts +0 -84
  217. package/src/services/ref-resolver/ref-resolve-arguments.ts +0 -47
  218. package/src/services/ref-resolver/ref-resolve-result.ts +0 -36
  219. package/src/services/ref-resolver/ref-resolver-service.ts +0 -323
  220. package/src/services/ref-resolver/source.ts +0 -48
  221. package/src/services/ref-resolver/tokens.ts +0 -25
  222. package/src/services/ref-resolver/types.ts +0 -155
  223. package/src/templating/filters/bullet-list.ts +0 -9
  224. package/src/templating/filters/index.ts +0 -8
  225. package/src/templating/init-liquid-engine.ts +0 -139
  226. package/src/templating/lib/glob-files.ts +0 -74
  227. package/src/templating/lib/import-export.ts +0 -32
  228. package/src/templating/lib/secret-scope.ts +0 -41
  229. package/src/templating/lib/tag-args.ts +0 -19
  230. package/src/templating/tags/exportScalarVarsJs.ts +0 -46
  231. package/src/templating/tags/getFiles.ts +0 -89
  232. package/src/templating/tags/index.ts +0 -14
  233. package/src/templating/tags/listFiles.ts +0 -54
  234. package/src/templating/tags/showVars.ts +0 -28
  235. package/src/utils/choice-prompt.ts +0 -143
  236. package/src/utils/command-errors.ts +0 -186
  237. package/src/utils/command-help.ts +0 -45
  238. package/src/utils/confirm-prompt.ts +0 -110
  239. package/src/utils/flags.ts +0 -177
  240. package/src/utils/formatting.ts +0 -849
  241. package/src/utils/prompts.ts +0 -53
  242. package/src/utils/sous-directory.ts +0 -245
  243. package/src/utils/table.ts +0 -603
  244. package/src/utils/value-prompt.ts +0 -119
@@ -1,685 +0,0 @@
1
- import fs from "node:fs";
2
- import { Command, Flags } from "@oclif/core";
3
- import { ConfigError, isConfigError, SOUS_VERSION } from "../../lib/settings.js";
4
- import { INDEX_FILENAME } from "../../lib/repos/formats/common.js";
5
- import { isInteractive, nonInteractiveError } from "../../lib/interactive.js";
6
- import { askYesNo } from "../../utils/prompts.js";
7
- import { reportCommandError } from "../../utils/command-errors.js";
8
- import { confirmationFlag, nonInteractiveFlag } from "../../utils/flags.js";
9
- import {
10
- BUMP_LEVELS,
11
- anythingToCommit,
12
- buildIndex,
13
- buildReleasePlan,
14
- checkSubmissions,
15
- bumpRecipeVersion,
16
- commitPaths,
17
- createAnnotatedTag,
18
- currentBranch,
19
- defaultBranch,
20
- describeIndexDrift,
21
- describeScope,
22
- errorsIn,
23
- findRepoRoot,
24
- hasCommitIdentity,
25
- hasErrors,
26
- indexFilePath,
27
- pushBranch,
28
- pushTags,
29
- readIndexFile,
30
- releaseScope,
31
- remoteUrl,
32
- scopeProblems,
33
- settleDependencyLocations,
34
- uncommittedChanges,
35
- validateRepo,
36
- warningsIn,
37
- type BumpLevel,
38
- type IndexBuildResult,
39
- type ReleasePlan,
40
- type ReleaseScope,
41
- type RepoValidation,
42
- type SettledDependency,
43
- type ValidationProblem,
44
- } from "../../lib/repos/release/index.js";
45
- import {
46
- BULLET,
47
- blankLine,
48
- displayErrorBlock,
49
- dryRunNotice,
50
- footer,
51
- header,
52
- log,
53
- note,
54
- paragraph,
55
- section,
56
- showCommandVars,
57
- showVariables,
58
- warning,
59
- } from "../../utils/formatting.js";
60
-
61
- /** The remote a release pushes to. Recipe repositories have exactly one. */
62
- const RELEASE_REMOTE = "origin";
63
-
64
- /**
65
- * `sous repo release` publishes new versions of the recipes in a repository.
66
- *
67
- * Like `sous repo init`, this command does NOT extend BaseCommand: it runs
68
- * inside a RECIPE repository, which is not a sous project and has no `.sous/`
69
- * directory of its own, so there is no project config to discover.
70
- *
71
- * One run does the whole job, in one order, after showing what it will do and
72
- * asking once:
73
- *
74
- * 1. Validate the repository.
75
- * 2. For every recipe in scope whose files have changed since the tag that
76
- * last published it, raise the version (a patch step unless `--bump` says
77
- * otherwise).
78
- * 3. Regenerate the index, with each new version's dependencies resolved to
79
- * the exact versions it is being released against.
80
- * 4. Commit the manifests and the index together.
81
- * 5. Tag that commit, dependency-first, with an annotated tag per version.
82
- * 6. Push, but only when asked to.
83
- *
84
- * Two presets sit on top of it. `--check` is read-only and is what a pull
85
- * request runs; `--ci` is the non-interactive form a merge runs, which bumps
86
- * nothing because the version was raised in the change being merged, and which
87
- * accepts the plan it prints because there is nobody there to accept it.
88
- */
89
- export default class RepoRelease extends Command {
90
- static description =
91
- "Publish new versions of this repository's recipes: bump, regenerate the index, commit and tag";
92
-
93
- /**
94
- * The other spelling of the topic. It lives under a hidden topic, so it is
95
- * typable everywhere without ever reaching the top-level listing.
96
- */
97
- static aliases = ["repos:release"];
98
-
99
- static examples = [
100
- "<%= config.bin %> repo release",
101
- "<%= config.bin %> repo release --dry-run",
102
- "<%= config.bin %> repo release --namespace workflow --bump minor",
103
- "<%= config.bin %> repo release --recipe workflow/task-files --yes --push",
104
- "<%= config.bin %> repo release --check",
105
- "<%= config.bin %> repo release --ci --push",
106
- ];
107
-
108
- static flags = {
109
- check: Flags.boolean({
110
- description:
111
- "Only check: validate, and fail when the committed index is out of date",
112
- default: false,
113
- }),
114
- ci: Flags.boolean({
115
- description:
116
- "Run the way a merge does: never bump, accept the plan, never ask, and fail on " +
117
- "anything unbumped",
118
- default: false,
119
- }),
120
- namespace: Flags.string({
121
- description: "Release only this namespace. Repeat to name several",
122
- multiple: true,
123
- }),
124
- recipe: Flags.string({
125
- description: "Release only this recipe, as 'namespace/name'. Repeat to name several",
126
- multiple: true,
127
- }),
128
- bump: Flags.string({
129
- description: "How far to raise a changed recipe's version. Defaults to a patch step",
130
- options: [...BUMP_LEVELS],
131
- }),
132
- "no-bump": Flags.boolean({
133
- description: "Raise no versions; a changed recipe that was never raised is an error",
134
- default: false,
135
- }),
136
- "include-unchanged": Flags.boolean({
137
- description: "Release every recipe in scope, whether its files changed or not",
138
- default: false,
139
- }),
140
- tag: Flags.boolean({
141
- description: "Cut the tags even on a branch other than the default one",
142
- default: false,
143
- }),
144
- push: Flags.boolean({
145
- description: "Push the commit, and the tags this run created, to the remote",
146
- default: false,
147
- }),
148
- yes: confirmationFlag(),
149
- "dry-run": Flags.boolean({
150
- description: "Print the plan and stop, changing nothing",
151
- default: false,
152
- }),
153
- // This command does not extend BaseCommand, so it declares the global
154
- // non-interactive flag itself; the rule is the same everywhere.
155
- "non-interactive": nonInteractiveFlag(),
156
- };
157
-
158
- async init(): Promise<void> {
159
- await super.init();
160
- header();
161
- }
162
-
163
- async run(): Promise<void> {
164
- const { flags } = await this.parse(RepoRelease);
165
- const dryRun = flags["dry-run"];
166
- const ci = flags.ci;
167
- // The CI preset is exactly three settings: never raise a version, accept
168
- // the plan it prints, and never ask. Accepting the plan is part of the
169
- // preset because there is nobody to ask: a run that only refused to ask
170
- // would fail on the very confirmation the preset exists to answer. It
171
- // deliberately does NOT imply --push; the workflow passes that itself, so
172
- // what gets pushed is visible in the workflow file.
173
- const noBump = flags["no-bump"] || ci;
174
- const accepted = flags.yes || ci;
175
- const interactive = ci ? false : isInteractive();
176
-
177
- assertFlagsAgree({ ...flags, noBump });
178
-
179
- const rootDir = findRepoRoot(process.cwd());
180
- const scope = releaseScope(flags.namespace ?? [], flags.recipe ?? []);
181
-
182
- showCommandVars({
183
- Repository: rootDir,
184
- Mode: describeMode(flags),
185
- Scope: describeScope(scope),
186
- ...(dryRun ? { "Dry Run": true } : {}),
187
- });
188
-
189
- // --- Validate ---------------------------------------------------------
190
-
191
- section("Checking the repository");
192
- let validation = validateRepo(rootDir);
193
- const scoping = scopeProblems(validation, scope);
194
- reportProblems([...validation.problems, ...scoping]);
195
- if (hasErrors([...validation.problems, ...scoping])) {
196
- return this.stopForErrors([...validation.problems, ...scoping]);
197
- }
198
- log(` Read ${describeCount(validation.recipes.length, "recipe")}.`);
199
-
200
- // A dependency that reads more than one way is settled before anything is
201
- // planned or written, so a repository that cannot be reached stops the run
202
- // with nothing changed.
203
- const settling = await settleDependencyLocations(validation);
204
- reportProblems(settling.problems);
205
- if (hasErrors(settling.problems)) return this.stopForErrors(settling.problems);
206
-
207
- if (flags.check) return await this.runCheck(rootDir, validation, settling.settled);
208
-
209
- // --- Plan -------------------------------------------------------------
210
-
211
- const branch = await currentBranch(rootDir);
212
- const mainBranch = await defaultBranch(rootDir);
213
- const onDefaultBranch =
214
- mainBranch === undefined || branch === undefined || branch === mainBranch;
215
- const willTag = onDefaultBranch || flags.tag;
216
-
217
- const plan = await buildReleasePlan({
218
- validation,
219
- scope,
220
- ...(flags.bump === undefined ? {} : { bump: flags.bump as BumpLevel }),
221
- noBump,
222
- includeUnchanged: flags["include-unchanged"],
223
- });
224
-
225
- reportProblems(plan.problems);
226
- if (hasErrors(plan.problems)) return this.stopForErrors(plan.problems);
227
-
228
- this.reportPlan(plan, { willTag, onDefaultBranch, branch, push: flags.push });
229
-
230
- if (plan.releases.length === 0) {
231
- footer();
232
- return;
233
- }
234
-
235
- if (dryRun) {
236
- blankLine();
237
- dryRunNotice("Nothing was written; this was a dry run.");
238
- footer();
239
- return;
240
- }
241
-
242
- // --- Confirm ----------------------------------------------------------
243
-
244
- if (!accepted) {
245
- if (!interactive) {
246
- throw nonInteractiveError({
247
- prompt: "whether to publish the versions listed above",
248
- remedy:
249
- "pass '--yes' (spelled '-y' or '--force' if you prefer) to accept the plan " +
250
- "above without being asked.",
251
- });
252
- }
253
- blankLine();
254
- const answer = await askYesNo("Publish these versions?");
255
- if (!answer) {
256
- blankLine();
257
- log(" Nothing was written.");
258
- footer();
259
- return;
260
- }
261
- }
262
-
263
- // --- Carry it out -----------------------------------------------------
264
-
265
- await this.assertNothingUncommitted(rootDir);
266
- await this.assertCommitIdentity(rootDir);
267
-
268
- section("Publishing");
269
-
270
- const changedPaths: string[] = [];
271
- for (const release of plan.releases) {
272
- if (release.bump === undefined) {
273
- log(` ${release.key}: publishing version ${release.to}.`);
274
- continue;
275
- }
276
- const bumped = bumpRecipeVersion(release.manifestPath, release.bump);
277
- log(` ${release.key}: ${bumped.from} becomes ${bumped.to}.`);
278
- changedPaths.push(release.manifestPath);
279
- }
280
-
281
- // The manifests changed, so the repository is read again; everything after
282
- // this point works from what the files now say.
283
- validation = validateRepo(rootDir);
284
- reportProblems(validation.problems);
285
- if (hasErrors(validation.problems)) return this.stopForErrors(validation.problems);
286
-
287
- const publishing: Record<string, string> = {};
288
- for (const release of plan.releases) publishing[release.key] = release.to;
289
-
290
- const existing = readExistingIndex(rootDir);
291
- const rebuilt = await buildIndex({
292
- validation,
293
- existing,
294
- sousVersion: SOUS_VERSION,
295
- publishing,
296
- settled: settling.settled,
297
- });
298
- reportProblems(rebuilt.problems);
299
- if (hasErrors(rebuilt.problems)) return this.stopForErrors(rebuilt.problems);
300
-
301
- if (rebuilt.stale) {
302
- fs.writeFileSync(indexFilePath(rootDir), rebuilt.text, "utf8");
303
- log(` Wrote ${INDEX_FILENAME}.`);
304
- changedPaths.push(indexFilePath(rootDir));
305
- } else {
306
- log(` The committed ${INDEX_FILENAME} was already current.`);
307
- }
308
-
309
- const message = `Release ${plan.releases
310
- .map((release) => `${release.key}@${release.to}`)
311
- .join(", ")}`;
312
-
313
- if (await anythingToCommit(rootDir, changedPaths)) {
314
- await commitPaths(rootDir, changedPaths, message);
315
- log(` Committed: ${message}`);
316
- } else {
317
- log(" Nothing to commit; the manifests and the index were already current.");
318
- }
319
-
320
- const created: string[] = [];
321
- if (willTag) {
322
- for (const release of plan.releases) {
323
- await createAnnotatedTag(
324
- rootDir,
325
- release.tag,
326
- `Release ${release.key} version ${release.to}`
327
- );
328
- created.push(release.tag);
329
- log(` Created the tag ${release.tag}.`);
330
- }
331
- } else {
332
- blankLine();
333
- log(` No tags were cut: this is the branch '${branch}', and tags are cut on the`);
334
- log(` default branch '${mainBranch}'. Continuous integration does that after the`);
335
- log(" merge, or pass '--tag' to cut them here.");
336
- }
337
-
338
- // --- Push -------------------------------------------------------------
339
-
340
- if (flags.push) {
341
- if ((await remoteUrl(rootDir, RELEASE_REMOTE)) === undefined) {
342
- throw new ConfigError(
343
- `The release was made, but this repository has no '${RELEASE_REMOTE}' remote to ` +
344
- `push it to.\n` +
345
- ` Add one with 'git remote add ${RELEASE_REMOTE} <url>', then push the commit ` +
346
- `and its tags yourself.`
347
- );
348
- }
349
- if (branch !== undefined) {
350
- await pushBranch(rootDir, RELEASE_REMOTE, branch);
351
- log(` Pushed the branch '${branch}' to ${RELEASE_REMOTE}.`);
352
- }
353
- if (created.length > 0) {
354
- await pushTags(rootDir, RELEASE_REMOTE, created);
355
- log(` Pushed ${describeCount(created.length, "tag")} to ${RELEASE_REMOTE}.`);
356
- }
357
- } else {
358
- section("What to do next");
359
- log(` Push the commit with 'git push ${RELEASE_REMOTE} ${branch ?? "<branch>"}'.`);
360
- if (created.length > 0) {
361
- log(` Push the tags with 'git push ${RELEASE_REMOTE} --tags'.`);
362
- }
363
- log(" Or run this command again with '--push', which does both.");
364
- }
365
-
366
- footer();
367
- }
368
-
369
- // --- The read-only form -------------------------------------------------------------------------
370
-
371
- /**
372
- * `--check`: validate, regenerate the index in memory, and fail when the
373
- * committed one is out of date. This is what a pull request runs, so it
374
- * writes nothing, asks nothing and tags nothing.
375
- *
376
- * @param rootDir - The repository's root directory.
377
- * @param validation - The validated repository.
378
- * @param settled - The dependencies that read more than one way, as settled.
379
- */
380
- private async runCheck(
381
- rootDir: string,
382
- validation: RepoValidation,
383
- settled: Map<string, SettledDependency>
384
- ): Promise<void> {
385
- section("Checking the index");
386
-
387
- const existing = readExistingIndex(rootDir);
388
- const result = await buildIndex({
389
- validation,
390
- existing,
391
- sousVersion: SOUS_VERSION,
392
- settled,
393
- });
394
- reportProblems(result.problems);
395
- if (hasErrors(result.problems)) return this.stopForErrors(result.problems);
396
-
397
- // The index is the release's output, not the author's: the release that
398
- // runs after a merge rewrites it before tagging. So drift here is reported
399
- // as what merging will change, never as a failure; only the problems the
400
- // index build raises above stop a check.
401
- if (result.stale) {
402
- note(`Merging rewrites the committed ${INDEX_FILENAME}, because:`);
403
- for (const line of describeIndexDrift(existing, result.index)) {
404
- paragraph(`${BULLET} ${line}`, { indent: 4, hangingIndent: 2 });
405
- }
406
- blankLine();
407
- } else {
408
- log(` The committed ${INDEX_FILENAME} is current, and so are the dependencies it`);
409
- log(" records for every version it publishes.");
410
- }
411
- reportPending(result, "These versions have no tag yet; they publish when this merges:");
412
-
413
- // A pull request can be opened without `sous repo submit`, so this check is
414
- // the one gate every change passes. The release itself is not restricted:
415
- // whatever publishes such a recipe still releases it.
416
- section("Checking the recipes that take no proposals");
417
- const submissions = await checkSubmissions(validation);
418
- if (submissions.refusing.length > 0) {
419
- const problems: ValidationProblem[] = submissions.refusing.map((recipe) => ({
420
- level: "error",
421
- where: recipe.path,
422
- message:
423
- `this change touches '${recipe.key}', which does not take proposed changes ` +
424
- `(${recipe.declaredBy === "recipe" ? "its own manifest" : "the repository manifest"} ` +
425
- `says so). Merging it would break whatever publishes the recipe.` +
426
- (recipe.instead === undefined ? "" : ` Instead: ${recipe.instead}`),
427
- }));
428
- reportProblems(problems);
429
- displayErrorBlock(
430
- `This change cannot be merged: it touches ` +
431
- `${describeCount(problems.length, "recipe")} that ${problems.length === 1 ? "does" : "do"} ` +
432
- `not take proposed changes, listed above.\n` +
433
- ` Take those edits out of the change, and send them where each recipe asks.`
434
- );
435
- return this.exit(1);
436
- }
437
- if (submissions.comparedWith.kind === "branch") {
438
- log(
439
- ` Compared with the branch '${submissions.comparedWith.branch}': this change touches no ` +
440
- `recipe that declines proposals.`
441
- );
442
- } else if (submissions.comparedWith.kind === "tags") {
443
- log(" Compared with each recipe's last release tag: nothing that declines proposals changed.");
444
- } else if (submissions.comparedWith.kind === "none declined") {
445
- log(" No recipe in this repository declines proposed changes.");
446
- } else {
447
- note(
448
- "This checkout holds neither a copy of the default branch nor a release tag of any " +
449
- "recipe that declines proposed changes, so there was nothing to compare with."
450
- );
451
- }
452
- footer();
453
- }
454
-
455
- // --- Output ---------------------------------------------------------------------------------
456
-
457
- /**
458
- * Prints the whole plan before anything happens: what is released, what is
459
- * left alone, whether tags are cut, and what is pushed.
460
- *
461
- * @param plan - The plan to describe.
462
- * @param context - The branch rule's outcome and the push flag.
463
- */
464
- private reportPlan(
465
- plan: ReleasePlan,
466
- context: {
467
- willTag: boolean;
468
- onDefaultBranch: boolean;
469
- branch: string | undefined;
470
- push: boolean;
471
- }
472
- ): void {
473
- section("The release this would make");
474
-
475
- if (plan.releases.length === 0) {
476
- log(" Nothing in scope has changed since the tag that last published it.");
477
- if (plan.skipped.length > 0) {
478
- blankLine();
479
- for (const entry of plan.skipped) log(` ${entry.key}: ${entry.reason}`);
480
- }
481
- blankLine();
482
- log(" Pass '--include-unchanged' to release everything in scope anyway.");
483
- return;
484
- }
485
-
486
- const rows: Record<string, string> = {};
487
- for (const release of plan.releases) {
488
- rows[release.key] =
489
- release.bump === undefined
490
- ? `${release.to} (already raised; the tag would be ${release.tag})`
491
- : `${release.from} becomes ${release.to} (a ${release.bump} step; the tag would be ${release.tag})`;
492
- }
493
- showVariables(rows);
494
-
495
- if (plan.skipped.length > 0) {
496
- blankLine();
497
- log(" Left alone:");
498
- for (const entry of plan.skipped) log(` ${entry.key}: ${entry.reason}`);
499
- }
500
-
501
- blankLine();
502
- log(" This run would:");
503
- log(" Raise the versions listed above, in the manifests that declare them.");
504
- log(` Regenerate ${INDEX_FILENAME}, with each version's dependencies resolved.`);
505
- log(" Commit the manifests and the index together.");
506
- if (context.willTag) {
507
- log(" Cut one annotated tag per version, dependency-first.");
508
- } else {
509
- log(
510
- ` Cut no tags: this is the branch '${context.branch}', not the default one.`
511
- );
512
- }
513
- log(
514
- context.push
515
- ? ` Push the commit${context.willTag ? " and the tags" : ""} to ${RELEASE_REMOTE}.`
516
- : " Push nothing; pass '--push' to push what it makes."
517
- );
518
- }
519
-
520
- /** Ends the run after printing why the repository cannot be released. */
521
- private stopForErrors(problems: ReadonlyArray<ValidationProblem>): void {
522
- const count = errorsIn(problems).length;
523
- displayErrorBlock(
524
- `This repository cannot be released yet: ${describeCount(count, "problem")} ` +
525
- `${count === 1 ? "is" : "are"} listed above.\n` +
526
- ` Fix them and run the command again.`
527
- );
528
- this.exit(1);
529
- }
530
-
531
- /**
532
- * Refuses to release while anything is uncommitted.
533
- *
534
- * A tag names one commit, and the index this run writes records the content
535
- * hash of every recipe folder as it stands, so an uncommitted edit would be
536
- * published by hash and absent from the tag. Sous commits its own version
537
- * bumps and its own index, and nothing else.
538
- *
539
- * @param rootDir - The repository's root directory.
540
- */
541
- private async assertNothingUncommitted(rootDir: string): Promise<void> {
542
- const changed = await uncommittedChanges(rootDir);
543
- if (changed.length === 0) return;
544
-
545
- const listed = changed.map((entry) => ` ${entry.path}`).join("\n");
546
- throw new ConfigError(
547
- "Cannot release while the working tree has uncommitted changes.\n\n" +
548
- `${listed}\n\n` +
549
- " A tag names one commit, and the index this writes records what each recipe " +
550
- "folder holds right now, so everything being published has to be committed " +
551
- "first.\n" +
552
- " Commit these, then run the command again; sous commits the version bumps and " +
553
- "the index itself."
554
- );
555
- }
556
-
557
- /**
558
- * Refuses to release from a repository where git does not know who is
559
- * committing, before anything has been written.
560
- *
561
- * A release makes a commit and annotated tags, and both need an identity. Git
562
- * would fail partway through with its own "empty ident name" message, which
563
- * says nothing about sous or about which repository is the problem; that is
564
- * exactly how the first automated release of sous itself failed. This says
565
- * what to set instead.
566
- *
567
- * @param rootDir - The repository's root directory.
568
- */
569
- private async assertCommitIdentity(rootDir: string): Promise<void> {
570
- if (await hasCommitIdentity(rootDir)) return;
571
-
572
- throw new ConfigError(
573
- "Cannot release: git does not know who is making the commit.\n\n" +
574
- " A release commits the version bumps and the index, and cuts an annotated tag " +
575
- "for every version it publishes; git refuses to do either without an author " +
576
- "identity.\n" +
577
- " Set one in this repository and run the command again:\n\n" +
578
- ' git config user.name "Your Name"\n' +
579
- ' git config user.email "you@example.com"\n\n' +
580
- " In a continuous integration job, configure the identity of the account the " +
581
- "release runs as before this command; the workflow 'sous repo init' scaffolds " +
582
- "already does."
583
- );
584
- }
585
-
586
- /**
587
- * Reports a failure the way every other sous command reports one: the message
588
- * on its own, this command's help underneath it when the command line was the
589
- * problem or a question could not be asked (so every flag that would have
590
- * answered it is visible without going looking), and a stack trace only when
591
- * `SOUS_DEBUG` asks for one. The rules live in `utils/command-errors.ts`.
592
- */
593
- protected async catch(error: Error & { exitCode?: number }): Promise<unknown> {
594
- const exitCode = await reportCommandError(this, error);
595
- if (exitCode === undefined) return super.catch(error);
596
- return this.exit(exitCode);
597
- }
598
- }
599
-
600
- // --- Helpers ------------------------------------------------------------------------------------
601
-
602
- /** Refuses flag combinations that would mean two different things at once. */
603
- function assertFlagsAgree(flags: {
604
- check: boolean;
605
- ci: boolean;
606
- noBump: boolean;
607
- push: boolean;
608
- tag: boolean;
609
- bump?: string;
610
- "include-unchanged": boolean;
611
- }): void {
612
- if (flags.check && flags.ci) {
613
- throw new ConfigError(
614
- "'--check' and '--ci' cannot be used together.\n" +
615
- " '--check' only reads, and reports whether a release is needed; '--ci' makes " +
616
- "one.\n A pull request runs 'sous repo release --check'; a merge runs " +
617
- "'sous repo release --ci --push'."
618
- );
619
- }
620
- if (flags.check && (flags.bump !== undefined || flags.push || flags.tag)) {
621
- throw new ConfigError(
622
- "'--check' changes nothing, so it cannot be combined with '--bump', '--tag' or " +
623
- "'--push'.\n" +
624
- " Run 'sous repo release --check' on its own, or drop '--check' to make a release."
625
- );
626
- }
627
- if (flags.bump !== undefined && flags.noBump) {
628
- throw new ConfigError(
629
- "'--bump' and '--no-bump' cannot be used together.\n" +
630
- " '--bump' says how far to raise a version; '--no-bump' says to raise none. " +
631
- "'--ci' implies '--no-bump'."
632
- );
633
- }
634
- }
635
-
636
- /** The plain-language name of what this run is doing, for the preamble. */
637
- function describeMode(flags: { check: boolean; ci: boolean; "dry-run": boolean }): string {
638
- if (flags.check) return "Check only";
639
- if (flags["dry-run"]) return "Plan only";
640
- if (flags.ci) return "Publish (continuous integration)";
641
- return "Publish";
642
- }
643
-
644
- /** Reads the committed index, warning and starting fresh when it cannot be read. */
645
- function readExistingIndex(rootDir: string) {
646
- try {
647
- return readIndexFile(rootDir);
648
- } catch (error) {
649
- warning(
650
- `The committed ${INDEX_FILENAME} could not be read, so it will be regenerated from ` +
651
- `the recipe manifests and the release tags.\n` +
652
- `${isConfigError(error) ? (error as Error).message : String(error)}`
653
- );
654
- return undefined;
655
- }
656
- }
657
-
658
- /** Prints every problem, errors first, each with where it was found. */
659
- function reportProblems(problems: ReadonlyArray<ValidationProblem>): void {
660
- for (const problem of errorsIn(problems)) {
661
- displayErrorBlock(`${problem.where}:\n ${problem.message}`);
662
- }
663
- for (const problem of warningsIn(problems)) {
664
- warning(`${problem.where}:\n${problem.message}`);
665
- }
666
- }
667
-
668
- /** Lists the versions that have no tag yet, when there are any. */
669
- function reportPending(result: IndexBuildResult, headline: string): void {
670
- if (result.pending.length === 0) return;
671
- log(` ${headline}`);
672
- const pending: Record<string, string> = {};
673
- for (const entry of result.pending) {
674
- pending[entry.key] = `${entry.version} (the tag would be ${entry.tag})`;
675
- }
676
- showVariables(pending);
677
- }
678
-
679
- /** Renders a count with its noun, singular or plural, in words a person reads. */
680
- function describeCount(count: number, noun: string): string {
681
- return `${count} ${noun}${count === 1 ? "" : "s"}`;
682
- }
683
-
684
- /** Re-exported so the scope type is nameable from a test. */
685
- export type { ReleaseScope };