@sous-io/sous 0.2.37 → 1.0.2

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 +1094 -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 -245
  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 -410
  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 -413
  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 -1107
  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,19 +0,0 @@
1
- /**
2
- * The publish side of the Repositories layer: everything `sous repo release`
3
- * and `sous repo submit` are built from.
4
- *
5
- * Import from here rather than from the individual modules, so a caller has one
6
- * place to look for validation, tags, working-tree state, index regeneration
7
- * and version bumping.
8
- */
9
-
10
- export * from "./validate.js";
11
- export * from "./tags.js";
12
- export * from "./git-state.js";
13
- export * from "./index-builder.js";
14
- export * from "./settle.js";
15
- export * from "./plan.js";
16
- export * from "./bump.js";
17
- export * from "./submit-service.js";
18
- export * from "./changelog.js";
19
- export * from "./submissions.js";
@@ -1,521 +0,0 @@
1
- /**
2
- * The release plan: what one `sous repo release` run would do, worked out
3
- * before anything is written.
4
- *
5
- * A release is decided by three facts about each recipe, and nothing else:
6
- *
7
- * 1. Is it IN SCOPE? By default every recipe the repository publishes is;
8
- * `--namespace` and `--recipe` narrow it.
9
- * 2. Has its content CHANGED since the tag that last published it? A recipe
10
- * nobody touched is not re-released, because a published version that says
11
- * the same thing as the one before it is noise.
12
- * 3. Has its version already been RAISED past that tag? If so the author (or
13
- * a previous run) has already done the bump, and this run only publishes
14
- * it.
15
- *
16
- * From those, the plan says which manifests get a version bump, which tags get
17
- * cut, and in which order: dependency-first, so a recipe is never published
18
- * before something it depends on.
19
- *
20
- * Nothing here writes anything. The command prints the plan, asks once, and
21
- * then carries it out.
22
- */
23
-
24
- import path from "node:path";
25
- import semver from "semver";
26
- import { hashDirectory } from "../store/hash.js";
27
- import {
28
- RefSource,
29
- isNamedRef,
30
- locationOf,
31
- namespaceOf,
32
- sharedRefResolver,
33
- shortKey,
34
- splitRecipeKey,
35
- } from "../../../services/ref-resolver/index.js";
36
- import type { RunOptions } from "../providers/git.js";
37
- import { listRecipeTags, tagFor, withTaggedTree, type RecipeTag } from "./tags.js";
38
- import { nextVersion, type BumpLevel } from "./bump.js";
39
- import type { RepoValidation, ValidatedRecipe, ValidationProblem } from "./validate.js";
40
-
41
- /** Which recipes a run is allowed to touch. */
42
- export type ReleaseScope = {
43
- /** Namespaces named with `--namespace`. Empty means every namespace. */
44
- namespaces: string[];
45
- /** Recipe keys named with `--recipe`. Empty means every recipe. */
46
- recipes: string[];
47
- };
48
-
49
- /** What is known about one recipe before the plan is made. */
50
- export type RecipeState = {
51
- /** The validated recipe. */
52
- recipe: ValidatedRecipe;
53
- /** Its key, `namespace/name`. */
54
- key: string;
55
- /** The version its manifest declares right now. */
56
- version: string;
57
- /** Whether the scope covers it. */
58
- inScope: boolean;
59
- /** The highest version any tag publishes, when the recipe has ever been tagged. */
60
- lastTagged?: string;
61
- /** The tag carrying that version. */
62
- lastTag?: string;
63
- /** Whether the recipe's files differ from what that tag carries. */
64
- changed: boolean;
65
- /** Whether a tag already exists for the version the manifest declares. */
66
- currentVersionTagged: boolean;
67
- };
68
-
69
- /** One recipe this run would publish. */
70
- export type PlannedRelease = {
71
- /** The recipe key, `namespace/name`. */
72
- key: string;
73
- /** The recipe folder, relative to the repository root. */
74
- path: string;
75
- /** Absolute path to the recipe's manifest, which a bump rewrites. */
76
- manifestPath: string;
77
- /** The version the manifest declares now. */
78
- from: string;
79
- /** The version this run publishes. */
80
- to: string;
81
- /** The bump this run applies, when it applies one. */
82
- bump?: BumpLevel;
83
- /** The tag this run would cut. */
84
- tag: string;
85
- };
86
-
87
- /** One recipe the plan leaves alone, and why. */
88
- export type SkippedRecipe = {
89
- key: string;
90
- /** A complete sentence, ready to print. */
91
- reason: string;
92
- };
93
-
94
- /** What a plan came to. */
95
- export type ReleasePlan = {
96
- /** Everything this run would publish, dependency-first. */
97
- releases: PlannedRelease[];
98
- /** Recipes the run leaves alone, with the reason for each. */
99
- skipped: SkippedRecipe[];
100
- /** Everything wrong with the plan, errors and warnings together. */
101
- problems: ValidationProblem[];
102
- /** Every release tag the repository carries. */
103
- tags: RecipeTag[];
104
- /** What each recipe looked like when the plan was made. */
105
- states: RecipeState[];
106
- };
107
-
108
- /** What `buildReleasePlan` needs to know. */
109
- export type BuildReleasePlanOptions = {
110
- /** The validated repository. */
111
- validation: RepoValidation;
112
- /** Which recipes the run may touch. */
113
- scope: ReleaseScope;
114
- /** How far to raise a changed recipe's version. Defaults to a patch step. */
115
- bump?: BumpLevel;
116
- /** When true, nothing is bumped and an unbumped change is an error. */
117
- noBump?: boolean;
118
- /** When true, every recipe in scope is released, changed or not. */
119
- includeUnchanged?: boolean;
120
- /** Every release tag, when the caller has already listed them. */
121
- tags?: RecipeTag[];
122
- /** The command runner git calls go through. */
123
- run?: RunOptions["run"];
124
- };
125
-
126
- /**
127
- * Builds the scope from the repeatable `--namespace` and `--recipe` flags.
128
- *
129
- * @param namespaces - Namespaces named on the command line.
130
- * @param recipes - Recipe keys named on the command line.
131
- */
132
- export function releaseScope(
133
- namespaces: string[] = [],
134
- recipes: string[] = []
135
- ): ReleaseScope {
136
- return {
137
- namespaces: [...new Set(namespaces.map((value) => value.trim()).filter(Boolean))],
138
- recipes: [...new Set(recipes.map((value) => value.trim()).filter(Boolean))],
139
- };
140
- }
141
-
142
- /** True when the scope covers every recipe the repository publishes. */
143
- export function scopeIsWholeRepository(scope: ReleaseScope): boolean {
144
- return scope.namespaces.length === 0 && scope.recipes.length === 0;
145
- }
146
-
147
- /**
148
- * The scope in words, for the run's preamble.
149
- *
150
- * @param scope - The scope to describe.
151
- */
152
- export function describeScope(scope: ReleaseScope): string {
153
- if (scopeIsWholeRepository(scope)) return "The whole repository";
154
- const parts: string[] = [];
155
- if (scope.namespaces.length > 0) {
156
- parts.push(
157
- `${scope.namespaces.length === 1 ? "the namespace" : "the namespaces"} ` +
158
- scope.namespaces.join(", ")
159
- );
160
- }
161
- if (scope.recipes.length > 0) {
162
- parts.push(
163
- `${scope.recipes.length === 1 ? "the recipe" : "the recipes"} ` +
164
- scope.recipes.join(", ")
165
- );
166
- }
167
- return parts.join(", and ").replace(/^./, (first) => first.toUpperCase());
168
- }
169
-
170
- /**
171
- * Raises a ConfigError-free list of problems for scope entries that name
172
- * nothing this repository publishes, so a typo is caught before anything runs.
173
- *
174
- * @param validation - The validated repository.
175
- * @param scope - The scope as given.
176
- */
177
- export function scopeProblems(
178
- validation: RepoValidation,
179
- scope: ReleaseScope
180
- ): ValidationProblem[] {
181
- const problems: ValidationProblem[] = [];
182
- const keys = new Set(validation.recipes.map((recipe) => recipe.key));
183
- const namespaces = new Set(
184
- validation.recipes.map((recipe) => recipe.manifest.namespace)
185
- );
186
-
187
- for (const namespace of scope.namespaces) {
188
- if (namespaces.has(namespace)) continue;
189
- problems.push({
190
- level: "error",
191
- where: `--namespace ${namespace}`,
192
- message:
193
- `this repository publishes no namespace called '${namespace}'. It publishes: ` +
194
- `${[...namespaces].sort().join(", ")}.`,
195
- });
196
- }
197
-
198
- for (const key of scope.recipes) {
199
- if (keys.has(key)) continue;
200
- problems.push({
201
- level: "error",
202
- where: `--recipe ${key}`,
203
- message:
204
- `this repository publishes no recipe called '${key}'. It publishes: ` +
205
- `${[...keys].sort().join(", ")}.`,
206
- });
207
- }
208
-
209
- return problems;
210
- }
211
-
212
- /**
213
- * Works out what one run would publish.
214
- *
215
- * @param options - The validated repository, the scope, and the bump rules.
216
- */
217
- export async function buildReleasePlan(
218
- options: BuildReleasePlanOptions
219
- ): Promise<ReleasePlan> {
220
- const { validation, scope } = options;
221
- const run = options.run;
222
- const tags = options.tags ?? (await listRecipeTags(validation.rootDir, { run }));
223
- const problems: ValidationProblem[] = [];
224
-
225
- const states: RecipeState[] = [];
226
- for (const recipe of validation.recipes) {
227
- states.push(await readRecipeState(validation.rootDir, recipe, scope, tags, run));
228
- }
229
-
230
- const releases: PlannedRelease[] = [];
231
- const skipped: SkippedRecipe[] = [];
232
-
233
- for (const state of states) {
234
- if (!state.inScope) {
235
- skipped.push({
236
- key: state.key,
237
- reason: "it is outside this release's scope.",
238
- });
239
- continue;
240
- }
241
-
242
- const worthReleasing = state.changed || options.includeUnchanged === true;
243
- if (!worthReleasing) {
244
- skipped.push({
245
- key: state.key,
246
- reason: `its files have not changed since ${state.lastTag}.`,
247
- });
248
- continue;
249
- }
250
-
251
- // A version already raised past the last tag needs no bump; this run only
252
- // publishes it. That is what a merge commit looks like to the CI run.
253
- const alreadyRaised = state.lastTagged === undefined || !state.currentVersionTagged;
254
-
255
- if (alreadyRaised) {
256
- releases.push({
257
- key: state.key,
258
- path: state.recipe.path,
259
- manifestPath: state.recipe.manifestPath,
260
- from: state.version,
261
- to: state.version,
262
- tag: tagOf(state.key, state.version),
263
- });
264
- continue;
265
- }
266
-
267
- if (options.noBump === true) {
268
- problems.push({
269
- level: "error",
270
- where: relativeTo(validation.rootDir, state.recipe.manifestPath),
271
- message:
272
- `version ${state.version} is already published as the tag '${state.lastTag}', ` +
273
- `and this recipe's files have changed since it. Raise the version in this ` +
274
- `manifest; a published version never changes.`,
275
- });
276
- continue;
277
- }
278
-
279
- const level = options.bump ?? "patch";
280
- const to = nextVersion(state.version, level);
281
- releases.push({
282
- key: state.key,
283
- path: state.recipe.path,
284
- manifestPath: state.recipe.manifestPath,
285
- from: state.version,
286
- to,
287
- bump: level,
288
- tag: tagOf(state.key, to),
289
- });
290
- }
291
-
292
- const ordered = orderByDependencies(releases, validation);
293
- problems.push(...checkSiblings(validation, ordered, states));
294
-
295
- return { releases: ordered, skipped, problems, tags, states };
296
- }
297
-
298
- /**
299
- * Orders a set of releases dependency-first, so a tag is never cut before the
300
- * tags it will depend on. Recipes that do not depend on each other keep their
301
- * original order, which is the order the repo manifest lists them in.
302
- *
303
- * A cycle cannot be ordered; the entries in it keep their original order, and
304
- * the closure check that follows reports it as the real problem.
305
- *
306
- * @param releases - The releases to order.
307
- * @param validation - The validated repository, read for each manifest's siblings.
308
- */
309
- export function orderByDependencies(
310
- releases: PlannedRelease[],
311
- validation: RepoValidation
312
- ): PlannedRelease[] {
313
- const byKey = new Map(releases.map((entry) => [entry.key, entry]));
314
- const ordered: PlannedRelease[] = [];
315
- const placed = new Set<string>();
316
- const visiting = new Set<string>();
317
-
318
- const place = (key: string): void => {
319
- if (placed.has(key) || visiting.has(key)) return;
320
- const entry = byKey.get(key);
321
- if (entry === undefined) return;
322
-
323
- visiting.add(key);
324
- for (const sibling of siblingKeysOf(validation, key)) place(sibling);
325
- visiting.delete(key);
326
-
327
- placed.add(key);
328
- ordered.push(entry);
329
- };
330
-
331
- for (const entry of releases) place(entry.key);
332
- return ordered;
333
- }
334
-
335
- /**
336
- * The keys of the recipes in THIS repository that a recipe depends on. A
337
- * dependency written as a locator URL lives somewhere else and has nothing to
338
- * do with the order tags are cut in here.
339
- *
340
- * @param validation - The validated repository.
341
- * @param key - The recipe whose dependencies are wanted.
342
- */
343
- export function siblingKeysOf(validation: RepoValidation, key: string): string[] {
344
- const recipe = validation.recipes.find((entry) => entry.key === key);
345
- if (recipe === undefined) return [];
346
-
347
- const declared = [
348
- ...(recipe.manifest.depends ?? []),
349
- ...(recipe.manifest.subscribes ?? []),
350
- ];
351
-
352
- const keys: string[] = [];
353
- for (const written of declared) {
354
- let readings;
355
- try {
356
- readings = sharedRefResolver().parse(written, RefSource.Manifest).refs;
357
- } catch {
358
- continue;
359
- }
360
- const parsed = readings[0]!;
361
- if (!isNamedRef(parsed) || locationOf(parsed) !== undefined) continue;
362
-
363
- if (parsed.kind === "recipe") {
364
- keys.push(shortKey(parsed));
365
- continue;
366
- }
367
- // A whole-namespace dependency means every recipe in it.
368
- for (const entry of validation.recipes) {
369
- if (entry.manifest.namespace === namespaceOf(parsed)!.name && entry.key !== key) {
370
- keys.push(entry.key);
371
- }
372
- }
373
- }
374
-
375
- return [...new Set(keys)];
376
- }
377
-
378
- /**
379
- * Checks the sibling rule: everything a released recipe depends on inside this
380
- * repository has to be a version that exists once this run's own tags are
381
- * counted.
382
- *
383
- * There are exactly two ways that fails, and they are different kinds of thing:
384
- *
385
- * - The sibling has never been tagged at all. Nothing can depend on it, so
386
- * this is an error naming the tag that has to be cut.
387
- * - The sibling HAS been tagged, and has changed since, but is outside this
388
- * release's scope. The release is still correct: it will depend on the last
389
- * tagged version. That is worth saying, so it is a warning, and the warning
390
- * states only what is verifiable.
391
- *
392
- * @param validation - The validated repository.
393
- * @param releases - The releases this run would publish.
394
- * @param states - What each recipe looked like when the plan was made.
395
- */
396
- function checkSiblings(
397
- validation: RepoValidation,
398
- releases: PlannedRelease[],
399
- states: RecipeState[]
400
- ): ValidationProblem[] {
401
- const problems: ValidationProblem[] = [];
402
- const releasing = new Map(releases.map((entry) => [entry.key, entry]));
403
- const byKey = new Map(states.map((state) => [state.key, state]));
404
-
405
- for (const release of releases) {
406
- for (const siblingKey of siblingKeysOf(validation, release.key)) {
407
- if (releasing.has(siblingKey)) continue;
408
-
409
- const sibling = byKey.get(siblingKey);
410
- const where = relativeTo(validation.rootDir, release.manifestPath);
411
-
412
- if (sibling === undefined) {
413
- problems.push({
414
- level: "error",
415
- where,
416
- message:
417
- `it depends on '${siblingKey}', which this repository does not publish. A ` +
418
- `dependency written without a location names a recipe in this same ` +
419
- `repository.`,
420
- });
421
- continue;
422
- }
423
-
424
- if (sibling.lastTagged === undefined) {
425
- problems.push({
426
- level: "error",
427
- where,
428
- message:
429
- `it depends on '${siblingKey}', which has never been published: this ` +
430
- `repository carries no tag for it. Release it first, which cuts the tag ` +
431
- `'${tagOf(siblingKey, sibling.version)}'.`,
432
- });
433
- continue;
434
- }
435
-
436
- if (sibling.changed) {
437
- problems.push({
438
- level: "warning",
439
- where,
440
- message:
441
- `'${siblingKey}' has changes since '${sibling.lastTag}' that are outside ` +
442
- `this release's scope; '${release.tag}' will depend on ` +
443
- `'${sibling.lastTag}'.`,
444
- });
445
- }
446
- }
447
- }
448
-
449
- return problems;
450
- }
451
-
452
- /**
453
- * Reads the three facts a plan is made of for one recipe: whether the scope
454
- * covers it, what tag last published it, and whether its files have changed
455
- * since that tag.
456
- *
457
- * @param rootDir - The repository's root directory.
458
- * @param recipe - The validated recipe.
459
- * @param scope - The run's scope.
460
- * @param tags - Every release tag the repository carries.
461
- * @param run - The command runner git calls go through.
462
- */
463
- async function readRecipeState(
464
- rootDir: string,
465
- recipe: ValidatedRecipe,
466
- scope: ReleaseScope,
467
- tags: ReadonlyArray<RecipeTag>,
468
- run: RunOptions["run"]
469
- ): Promise<RecipeState> {
470
- const key = recipe.key;
471
- const version = recipe.manifest.version;
472
- const mine = tags.filter((tag) => `${tag.namespace}/${tag.name}` === key);
473
-
474
- const highest = mine
475
- .map((tag) => tag.version)
476
- .filter((value) => semver.valid(value) !== null)
477
- .sort((left, right) => semver.rcompare(left, right))[0];
478
-
479
- const state: RecipeState = {
480
- recipe,
481
- key,
482
- version,
483
- inScope: coveredByScope(recipe, scope),
484
- changed: true,
485
- currentVersionTagged: mine.some((tag) => tag.version === version),
486
- ...(highest === undefined ? {} : { lastTagged: highest, lastTag: tagOf(key, highest) }),
487
- };
488
-
489
- if (highest === undefined) return state;
490
-
491
- // "Changed" is a content question, so it is answered by hashing, exactly the
492
- // way a consumer decides whether a cached copy is still the published one.
493
- const workingHash = await hashDirectory(recipe.dir);
494
- const taggedHash = await withTaggedTree(
495
- rootDir,
496
- tagOf(key, highest),
497
- recipe.path,
498
- (dir) => hashDirectory(dir),
499
- { run }
500
- );
501
- state.changed = workingHash !== taggedHash;
502
- return state;
503
- }
504
-
505
- /** True when a run's scope covers a recipe. */
506
- function coveredByScope(recipe: ValidatedRecipe, scope: ReleaseScope): boolean {
507
- if (scopeIsWholeRepository(scope)) return true;
508
- if (scope.namespaces.includes(recipe.manifest.namespace)) return true;
509
- return scope.recipes.includes(recipe.key);
510
- }
511
-
512
- /** The tag that publishes one version of one recipe key. */
513
- function tagOf(key: string, version: string): string {
514
- const { namespace, name } = splitRecipeKey(key);
515
- return tagFor(namespace, name, version);
516
- }
517
-
518
- /** Renders an absolute path as a repository-relative one, with forward slashes. */
519
- function relativeTo(rootDir: string, target: string): string {
520
- return path.relative(rootDir, target).split(path.sep).join("/");
521
- }
@@ -1,154 +0,0 @@
1
- /**
2
- * Settling, at release time, the dependencies that read more than one way.
3
- *
4
- * Most dependencies say exactly what they mean: `workflow/alpha`, or
5
- * `github://owner/repo/workflow/alpha`. Two kinds do not:
6
- *
7
- * - a GitLab URL with nested groups, which does not say where the project path
8
- * ends (`gitlab.com/a/b/c/d` may be project `a/b` with namespace `c` and
9
- * recipe `d`, or project `a/b/c` with namespace `d`);
10
- * - a browser URL copied from a host's file view, which names a folder whose
11
- * recipe only that repository's index can say.
12
- *
13
- * `sous repo release` settles each of them once, by fetching the index of every
14
- * candidate repository through the provider layer and keeping the reading whose
15
- * index publishes what was named. What it settled on is recorded in the index
16
- * beside the dependency's key, so a consumer reads the answer and never probes.
17
- *
18
- * A network failure fails the release: a release never falls through to the
19
- * next reading, because the reading it fell through to could be the wrong one.
20
- * Two readings that both publish what is named are a genuine tie, and an error
21
- * naming the spellings that read one way (the `/*` form for the namespace).
22
- */
23
-
24
- import type { ProviderOptions, RepoProvider } from "../providers/index.js";
25
- import {
26
- FetchedIndexLookup,
27
- RefSource,
28
- locationOf,
29
- namespaceOf,
30
- rangeOf,
31
- sharedRefResolver,
32
- shortKey,
33
- type SousRef,
34
- } from "../../../services/ref-resolver/index.js";
35
- import { describeError, type RepoValidation, type ValidationProblem } from "./validate.js";
36
-
37
- /** What one dependency settled on. */
38
- export type SettledDependency = {
39
- /** The canonical identity of the repository publishing it. */
40
- identity: string;
41
- /**
42
- * The recipe keys it reaches in that repository: one for a recipe, and every
43
- * recipe the namespace publishes for a namespace.
44
- */
45
- keys: string[];
46
- /** The range the manifest wrote, when it wrote one. */
47
- range?: string;
48
- };
49
-
50
- /** What settling found. */
51
- export type SettleResult = {
52
- /** Each settled dependency, keyed by the dependency exactly as written (trimmed). */
53
- settled: Map<string, SettledDependency>;
54
- /** Everything that could not be settled; every one of them is an error. */
55
- problems: ValidationProblem[];
56
- };
57
-
58
- /** How settling reaches the network. */
59
- export type SettleOptions = {
60
- /** The providers to fetch through. Defaults to the built-ins. */
61
- providers?: RepoProvider[];
62
- /** Testing seams handed to every index fetch. */
63
- providerOptions?: ProviderOptions;
64
- };
65
-
66
- /**
67
- * True when a dependency's readings need an index to settle: it reads more than
68
- * one way, or it names a browser path.
69
- *
70
- * @param readings - Every reading of the dependency.
71
- */
72
- export function needsSettling(readings: SousRef[]): boolean {
73
- return (
74
- readings.length > 1 ||
75
- readings.some((reading) => reading.kind === "repo" && reading.browsed !== undefined)
76
- );
77
- }
78
-
79
- /**
80
- * Settles every dependency in a repository that reads more than one way.
81
- * Dependencies that read one way are left alone, and nothing is fetched for
82
- * them, so a repository that uses none of these forms releases offline as
83
- * before.
84
- *
85
- * @param validation - The validated repository.
86
- * @param options - The providers, and testing seams.
87
- */
88
- export async function settleDependencyLocations(
89
- validation: RepoValidation,
90
- options: SettleOptions = {}
91
- ): Promise<SettleResult> {
92
- const settled = new Map<string, SettledDependency>();
93
- const problems: ValidationProblem[] = [];
94
- const lookup = new FetchedIndexLookup({
95
- subject: "dependency",
96
- ...(options.providers === undefined ? {} : { providers: options.providers }),
97
- ...(options.providerOptions === undefined ? {} : { providerOptions: options.providerOptions }),
98
- });
99
-
100
- for (const recipe of validation.recipes) {
101
- const declared = [...(recipe.manifest.depends ?? []), ...(recipe.manifest.subscribes ?? [])];
102
- for (const written of declared) {
103
- const trimmed = written.trim();
104
- if (settled.has(trimmed)) continue;
105
-
106
- let readings: SousRef[];
107
- try {
108
- readings = sharedRefResolver().parse(trimmed, RefSource.Manifest).refs;
109
- } catch {
110
- // Validation already reported a dependency that does not parse.
111
- continue;
112
- }
113
- if (!needsSettling(readings)) continue;
114
-
115
- const where = `${recipe.path} ('${trimmed}')`;
116
- try {
117
- settled.set(trimmed, await settleOne(readings, lookup));
118
- } catch (error) {
119
- problems.push({ level: "error", where, message: describeError(error) });
120
- }
121
- }
122
- }
123
-
124
- return { settled, problems };
125
- }
126
-
127
- /**
128
- * Settles one dependency: the one reading whose repository's index publishes
129
- * what it names.
130
- *
131
- * @param readings - Every reading of the dependency.
132
- * @param lookup - The fetched indexes.
133
- */
134
- async function settleOne(
135
- readings: SousRef[],
136
- lookup: FetchedIndexLookup
137
- ): Promise<SettledDependency> {
138
- const { ref: choice } = await lookup.settle(readings);
139
- const location = locationOf(choice)!;
140
- const index = await lookup.indexAt(location);
141
- const namespace = namespaceOf(choice)?.name;
142
- const keys =
143
- choice.kind === "namespace"
144
- ? Object.keys(index.recipes)
145
- .filter((key) => key.startsWith(`${namespace}/`))
146
- .sort()
147
- : [shortKey(choice)];
148
- const range = readings.map(rangeOf).find((written) => written !== undefined);
149
- return {
150
- identity: location.identity,
151
- keys,
152
- ...(range === undefined ? {} : { range }),
153
- };
154
- }