@sous-io/sous 0.2.28 → 0.2.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/markdown/_sidebar.md +2 -0
- package/docs/markdown/commands.md +10 -6
- package/docs/markdown/repositories-authoring.md +19 -11
- package/docs/markdown/repositories-consuming.md +34 -13
- package/docs/markdown/repositories-file-formats.md +88 -13
- package/docs/markdown/repositories-providers.md +8 -6
- package/docs/markdown/repositories-troubleshooting.md +6 -0
- package/docs/markdown/repositories-variables.md +13 -3
- package/docs/markdown/repositories.md +3 -3
- package/package.json +1 -1
- package/recipes/core/sous-skills/skills/about-sous-recipes/SKILL.tpl.md +11 -7
- package/recipes/core/sous-skills/skills/about-sous-repos/SKILL.tpl.md +7 -4
- package/recipes/core/sous-skills/sous.recipe.yaml +1 -1
- package/src/commands/lock/rebuild.ts +7 -6
- package/src/commands/recipe/show.ts +163 -64
- package/src/commands/repo/contribute.ts +1 -10
- package/src/commands/repo/gc.ts +3 -2
- package/src/commands/repo/release.ts +20 -3
- package/src/commands/repo/search.ts +2 -1
- package/src/commands/vars/ask.ts +32 -10
- package/src/lib/refs/find.ts +202 -21
- package/src/lib/refs/index.ts +2 -0
- package/src/lib/refs/parse.ts +844 -0
- package/src/lib/refs/scopes.ts +34 -0
- package/src/lib/refs/settle.ts +219 -0
- package/src/lib/repos/catalog.ts +87 -80
- package/src/lib/repos/declarations.ts +177 -0
- package/src/lib/repos/formats/common.ts +14 -0
- package/src/lib/repos/formats/index-file.ts +42 -2
- package/src/lib/repos/formats/lockfile.ts +5 -3
- package/src/lib/repos/formats/recipe-manifest.ts +16 -212
- package/src/lib/repos/formats/variable-definition.ts +268 -0
- package/src/lib/repos/index.ts +0 -1
- package/src/lib/repos/lock-service.ts +3 -2
- package/src/lib/repos/locked-namespace-resolver.ts +21 -15
- package/src/lib/repos/locked-recipes.ts +2 -2
- package/src/lib/repos/namespace-resolver.ts +40 -47
- package/src/lib/repos/providers/base.ts +30 -0
- package/src/lib/repos/providers/github.ts +22 -0
- package/src/lib/repos/providers/gitlab.ts +68 -0
- package/src/lib/repos/providers/provider.ts +96 -0
- package/src/lib/repos/release/index-builder.ts +221 -47
- package/src/lib/repos/release/index.ts +1 -0
- package/src/lib/repos/release/plan.ts +8 -6
- package/src/lib/repos/release/settle.ts +237 -0
- package/src/lib/repos/release/tags.ts +3 -7
- package/src/lib/repos/resolver.ts +251 -53
- package/src/lib/repos/subscription-service.ts +355 -49
- package/src/lib/repos/update-plan.ts +3 -2
- package/src/lib/vars/mappings.ts +17 -30
- package/src/lib/vars/question-plan.ts +44 -21
- package/src/lib/repos/ref.ts +0 -513
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
import path from "node:path";
|
|
25
25
|
import semver from "semver";
|
|
26
26
|
import { hashDirectory } from "../store/hash.js";
|
|
27
|
-
import {
|
|
27
|
+
import { isNamedReading, parseRef, splitRecipeKey } from "../../refs/parse.js";
|
|
28
|
+
import { RefSource } from "../../refs/scopes.js";
|
|
28
29
|
import type { RunOptions } from "../providers/git.js";
|
|
29
30
|
import { listRecipeTags, tagFor, withTaggedTree, type RecipeTag } from "./tags.js";
|
|
30
31
|
import { nextVersion, type BumpLevel } from "./bump.js";
|
|
@@ -343,13 +344,14 @@ export function siblingKeysOf(validation: RepoValidation, key: string): string[]
|
|
|
343
344
|
|
|
344
345
|
const keys: string[] = [];
|
|
345
346
|
for (const written of declared) {
|
|
346
|
-
let
|
|
347
|
+
let readings;
|
|
347
348
|
try {
|
|
348
|
-
|
|
349
|
+
readings = parseRef(written, RefSource.Manifest);
|
|
349
350
|
} catch {
|
|
350
351
|
continue;
|
|
351
352
|
}
|
|
352
|
-
|
|
353
|
+
const parsed = readings[0]!;
|
|
354
|
+
if (!isNamedReading(parsed) || parsed.location !== undefined) continue;
|
|
353
355
|
|
|
354
356
|
if (parsed.recipe !== undefined) {
|
|
355
357
|
keys.push(`${parsed.namespace}/${parsed.recipe}`);
|
|
@@ -502,8 +504,8 @@ function coveredByScope(recipe: ValidatedRecipe, scope: ReleaseScope): boolean {
|
|
|
502
504
|
|
|
503
505
|
/** The tag that publishes one version of one recipe key. */
|
|
504
506
|
function tagOf(key: string, version: string): string {
|
|
505
|
-
const
|
|
506
|
-
return tagFor(
|
|
507
|
+
const { namespace, name } = splitRecipeKey(key);
|
|
508
|
+
return tagFor(namespace, name, version);
|
|
507
509
|
}
|
|
508
510
|
|
|
509
511
|
/** Renders an absolute path as a repository-relative one, with forward slashes. */
|
|
@@ -0,0 +1,237 @@
|
|
|
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 { parseIndexFile, type IndexFile } from "../formats/index-file.js";
|
|
25
|
+
import { INDEX_FILENAME } from "../formats/common.js";
|
|
26
|
+
import {
|
|
27
|
+
builtInProviders,
|
|
28
|
+
providerById,
|
|
29
|
+
type ProviderOptions,
|
|
30
|
+
type RepoProvider,
|
|
31
|
+
} from "../providers/index.js";
|
|
32
|
+
import {
|
|
33
|
+
formatRef,
|
|
34
|
+
isBrowsedReading,
|
|
35
|
+
isNamedReading,
|
|
36
|
+
parseRef,
|
|
37
|
+
refKey,
|
|
38
|
+
type ParsedRef,
|
|
39
|
+
type RefLocation,
|
|
40
|
+
type RefReading,
|
|
41
|
+
} from "../../refs/parse.js";
|
|
42
|
+
import { settleInIndex } from "../../refs/settle.js";
|
|
43
|
+
import { RefSource } from "../../refs/scopes.js";
|
|
44
|
+
import { describeError, type RepoValidation, type ValidationProblem } from "./validate.js";
|
|
45
|
+
|
|
46
|
+
/** What one dependency settled on. */
|
|
47
|
+
export type SettledDependency = {
|
|
48
|
+
/** The canonical identity of the repository publishing it. */
|
|
49
|
+
identity: string;
|
|
50
|
+
/**
|
|
51
|
+
* The recipe keys it reaches in that repository: one for a recipe, and every
|
|
52
|
+
* recipe the namespace publishes for a namespace.
|
|
53
|
+
*/
|
|
54
|
+
keys: string[];
|
|
55
|
+
/** The range the manifest wrote, when it wrote one. */
|
|
56
|
+
range?: string;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/** What settling found. */
|
|
60
|
+
export type SettleResult = {
|
|
61
|
+
/** Each settled dependency, keyed by the dependency exactly as written (trimmed). */
|
|
62
|
+
settled: Map<string, SettledDependency>;
|
|
63
|
+
/** Everything that could not be settled; every one of them is an error. */
|
|
64
|
+
problems: ValidationProblem[];
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/** How settling reaches the network. */
|
|
68
|
+
export type SettleOptions = {
|
|
69
|
+
/** The providers to fetch through. Defaults to the built-ins. */
|
|
70
|
+
providers?: RepoProvider[];
|
|
71
|
+
/** Testing seams handed to every index fetch. */
|
|
72
|
+
providerOptions?: ProviderOptions;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* True when a dependency's readings need an index to settle: it reads more than
|
|
77
|
+
* one way, or it names a browser path.
|
|
78
|
+
*
|
|
79
|
+
* @param readings - Every reading of the dependency.
|
|
80
|
+
*/
|
|
81
|
+
export function needsSettling(readings: RefReading[]): boolean {
|
|
82
|
+
return readings.length > 1 || readings.some(isBrowsedReading);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Settles every dependency in a repository that reads more than one way.
|
|
87
|
+
* Dependencies that read one way are left alone, and nothing is fetched for
|
|
88
|
+
* them, so a repository that uses none of these forms releases offline as
|
|
89
|
+
* before.
|
|
90
|
+
*
|
|
91
|
+
* @param validation - The validated repository.
|
|
92
|
+
* @param options - The providers, and testing seams.
|
|
93
|
+
*/
|
|
94
|
+
export async function settleDependencyLocations(
|
|
95
|
+
validation: RepoValidation,
|
|
96
|
+
options: SettleOptions = {}
|
|
97
|
+
): Promise<SettleResult> {
|
|
98
|
+
const providers = options.providers ?? builtInProviders();
|
|
99
|
+
const settled = new Map<string, SettledDependency>();
|
|
100
|
+
const problems: ValidationProblem[] = [];
|
|
101
|
+
const indexes = new Map<string, Promise<IndexFile>>();
|
|
102
|
+
|
|
103
|
+
const indexOf = (location: RefLocation): Promise<IndexFile> => {
|
|
104
|
+
let pending = indexes.get(location.identity);
|
|
105
|
+
if (pending === undefined) {
|
|
106
|
+
pending = fetchIndexAt(location, providers, options.providerOptions ?? {});
|
|
107
|
+
indexes.set(location.identity, pending);
|
|
108
|
+
}
|
|
109
|
+
return pending;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
for (const recipe of validation.recipes) {
|
|
113
|
+
const declared = [...(recipe.manifest.depends ?? []), ...(recipe.manifest.subscribes ?? [])];
|
|
114
|
+
for (const written of declared) {
|
|
115
|
+
const trimmed = written.trim();
|
|
116
|
+
if (settled.has(trimmed)) continue;
|
|
117
|
+
|
|
118
|
+
let readings: RefReading[];
|
|
119
|
+
try {
|
|
120
|
+
readings = parseRef(trimmed, RefSource.Manifest, providers);
|
|
121
|
+
} catch {
|
|
122
|
+
// Validation already reported a dependency that does not parse.
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (!needsSettling(readings)) continue;
|
|
126
|
+
|
|
127
|
+
const where = `${recipe.path} ('${trimmed}')`;
|
|
128
|
+
try {
|
|
129
|
+
settled.set(trimmed, await settleOne(trimmed, readings, indexOf, providers));
|
|
130
|
+
} catch (error) {
|
|
131
|
+
problems.push({ level: "error", where, message: describeError(error) });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return { settled, problems };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Settles one dependency: fetches the index behind each reading, and keeps the
|
|
141
|
+
* one reading that publishes what it names.
|
|
142
|
+
*
|
|
143
|
+
* @param written - The dependency as written.
|
|
144
|
+
* @param readings - Every reading of it.
|
|
145
|
+
* @param indexOf - Fetches (once) the index at a location.
|
|
146
|
+
* @param providers - The providers, for formatting.
|
|
147
|
+
*/
|
|
148
|
+
async function settleOne(
|
|
149
|
+
written: string,
|
|
150
|
+
readings: RefReading[],
|
|
151
|
+
indexOf: (location: RefLocation) => Promise<IndexFile>,
|
|
152
|
+
providers: RepoProvider[]
|
|
153
|
+
): Promise<SettledDependency> {
|
|
154
|
+
const found: Array<{ reading: RefReading; settled: ParsedRef[]; index: IndexFile }> = [];
|
|
155
|
+
|
|
156
|
+
for (const reading of readings) {
|
|
157
|
+
const location = reading.location;
|
|
158
|
+
/* c8 ignore next */
|
|
159
|
+
if (location === undefined) continue;
|
|
160
|
+
|
|
161
|
+
let index: IndexFile;
|
|
162
|
+
try {
|
|
163
|
+
index = await indexOf(location);
|
|
164
|
+
} catch (error) {
|
|
165
|
+
throw new Error(
|
|
166
|
+
`the dependency reads ${readings.length === 1 ? "as a folder in" : "as something in"} ` +
|
|
167
|
+
`the repository at ${location.url}, and its index could not be read, so the release ` +
|
|
168
|
+
`cannot settle what it means. A release never guesses past an index it could not ` +
|
|
169
|
+
`read.\n ${describeError(error)}`
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const settled = settleInIndex(reading, {
|
|
174
|
+
namespaces: Object.keys(index.namespaces),
|
|
175
|
+
recipes: index.recipes,
|
|
176
|
+
});
|
|
177
|
+
if (settled.length > 0) found.push({ reading, settled, index });
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (found.length === 0) {
|
|
181
|
+
throw new Error(
|
|
182
|
+
`no repository it could name publishes what it names. It was read as:\n` +
|
|
183
|
+
readings.map((reading) => ` ${formatRef(reading, providers)}`).join("\n")
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const choices = found.flatMap((entry) => entry.settled);
|
|
188
|
+
if (choices.length > 1) {
|
|
189
|
+
throw new Error(
|
|
190
|
+
`it names more than one thing, and each of these publishes it:\n` +
|
|
191
|
+
choices.map((choice) => ` ${formatRef(choice, providers)}`).join("\n") +
|
|
192
|
+
`\n Write the one you mean. A namespace is written with '/*' after it, as in ` +
|
|
193
|
+
`'${spelledOut(choices.find((choice) => choice.recipe === undefined) ?? choices[0]!, providers)}', ` +
|
|
194
|
+
`and a recipe as its canonical locator above.`
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const { index } = found[0]!;
|
|
199
|
+
const choice = choices[0]!;
|
|
200
|
+
const keys =
|
|
201
|
+
choice.recipe === undefined
|
|
202
|
+
? Object.keys(index.recipes)
|
|
203
|
+
.filter((key) => key.startsWith(`${choice.namespace}/`))
|
|
204
|
+
.sort()
|
|
205
|
+
: [refKey(choice)];
|
|
206
|
+
const range = "range" in found[0]!.reading ? found[0]!.reading.range : undefined;
|
|
207
|
+
return {
|
|
208
|
+
identity: choice.location!.identity,
|
|
209
|
+
keys,
|
|
210
|
+
...(range === undefined ? {} : { range }),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** A reading spelled so it reads one way: a namespace with `/*` after it. */
|
|
215
|
+
function spelledOut(reading: ParsedRef, providers: RepoProvider[]): string {
|
|
216
|
+
const written = formatRef(reading, providers);
|
|
217
|
+
return reading.recipe === undefined && isNamedReading(reading) ? `${written}/*` : written;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Fetches and validates the index at a location, through its provider.
|
|
222
|
+
*
|
|
223
|
+
* @param location - Where the repository lives.
|
|
224
|
+
* @param providers - The providers to fetch through.
|
|
225
|
+
* @param options - Testing seams.
|
|
226
|
+
*/
|
|
227
|
+
async function fetchIndexAt(
|
|
228
|
+
location: RefLocation,
|
|
229
|
+
providers: RepoProvider[],
|
|
230
|
+
options: ProviderOptions
|
|
231
|
+
): Promise<IndexFile> {
|
|
232
|
+
const provider = providerById(location.provider, providers);
|
|
233
|
+
/* c8 ignore next */
|
|
234
|
+
if (provider === undefined) throw new Error(`sous has no '${location.provider}' provider.`);
|
|
235
|
+
const fetched = await provider.fetchIndex(provider.canonicalize(location.url), options);
|
|
236
|
+
return parseIndexFile(JSON.parse(fetched.text), `${location.url}/${INDEX_FILENAME}`);
|
|
237
|
+
}
|
|
@@ -16,6 +16,7 @@ import os from "node:os";
|
|
|
16
16
|
import path from "node:path";
|
|
17
17
|
import { runGit, type RunOptions } from "../providers/git.js";
|
|
18
18
|
import { RECIPE_KEY_PATTERN } from "../formats/patterns.js";
|
|
19
|
+
import { splitRecipeKey } from "../../refs/parse.js";
|
|
19
20
|
|
|
20
21
|
/** One release tag, taken apart into the recipe it publishes. */
|
|
21
22
|
export type RecipeTag = {
|
|
@@ -60,13 +61,8 @@ export function parseRecipeTag(tag: string): RecipeTag | undefined {
|
|
|
60
61
|
const version = tag.slice(at + 1);
|
|
61
62
|
if (!RECIPE_KEY_PATTERN.test(key)) return undefined;
|
|
62
63
|
|
|
63
|
-
const
|
|
64
|
-
return {
|
|
65
|
-
tag,
|
|
66
|
-
namespace: key.slice(0, slash),
|
|
67
|
-
name: key.slice(slash + 1),
|
|
68
|
-
version,
|
|
69
|
-
};
|
|
64
|
+
const { namespace, name } = splitRecipeKey(key);
|
|
65
|
+
return { tag, namespace, name, version };
|
|
70
66
|
}
|
|
71
67
|
|
|
72
68
|
/**
|