@sous-io/sous 0.2.15 → 0.2.17
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/commands.md +63 -16
- package/docs/markdown/repositories-authoring.md +52 -11
- package/docs/markdown/repositories-consuming.md +32 -1
- package/docs/markdown/repositories-file-formats.md +20 -0
- package/docs/markdown/repositories-providers.md +20 -10
- package/package.json +1 -1
- package/recipes/core/sous-skills/sous.recipe.yaml +8 -1
- package/src/commands/namespace/list.ts +42 -21
- package/src/commands/namespace/show.ts +32 -12
- package/src/commands/recipe/list.ts +40 -23
- package/src/commands/recipe/show.ts +28 -6
- package/src/commands/repo/list.ts +67 -10
- package/src/commands/repo/release.ts +41 -0
- package/src/commands/repo/search.ts +68 -15
- package/src/commands/repo/submit.ts +245 -35
- package/src/commands/subscription/list.ts +98 -19
- package/src/lib/build-preparation.ts +44 -1
- package/src/lib/repos/catalog-display.ts +101 -2
- package/src/lib/repos/catalog-inputs.ts +145 -17
- package/src/lib/repos/catalog.ts +92 -5
- package/src/lib/repos/formats/common.ts +20 -0
- package/src/lib/repos/formats/recipe-manifest.ts +7 -0
- package/src/lib/repos/formats/repo-manifest.ts +8 -0
- package/src/lib/repos/freshness.ts +56 -0
- package/src/lib/repos/providers/base.ts +33 -1
- package/src/lib/repos/providers/github.ts +276 -3
- package/src/lib/repos/providers/gitlab.ts +1 -0
- package/src/lib/repos/providers/http.ts +7 -2
- package/src/lib/repos/providers/index-cache.ts +56 -17
- package/src/lib/repos/providers/provider.ts +121 -3
- package/src/lib/repos/release/changelog.ts +448 -0
- package/src/lib/repos/release/git-state.ts +101 -15
- package/src/lib/repos/release/index.ts +2 -0
- package/src/lib/repos/release/submissions.ts +214 -0
- package/src/lib/repos/release/submit-checkout.ts +271 -0
- package/src/lib/repos/release/submit-questions.ts +153 -0
- package/src/lib/repos/release/submit-service.ts +581 -174
- package/src/lib/repos/subscription-service.ts +138 -9
- package/src/utils/flags.ts +24 -0
|
@@ -11,7 +11,7 @@ import { ConfigError } from "../../errors.js";
|
|
|
11
11
|
/** The shape of `fetch` this module needs; the global satisfies it. */
|
|
12
12
|
export type FetchLike = (
|
|
13
13
|
url: string,
|
|
14
|
-
init?: { headers?: Record<string, string
|
|
14
|
+
init?: { headers?: Record<string, string>; signal?: AbortSignal }
|
|
15
15
|
) => Promise<{
|
|
16
16
|
ok: boolean;
|
|
17
17
|
status: number;
|
|
@@ -36,6 +36,8 @@ export type FetchTextOptions = {
|
|
|
36
36
|
fetchImpl?: FetchLike;
|
|
37
37
|
/** What the URL is, named in error messages (for example "repo index"). */
|
|
38
38
|
label?: string;
|
|
39
|
+
/** Cancels the request, for a caller that will not wait past a deadline. */
|
|
40
|
+
signal?: AbortSignal;
|
|
39
41
|
};
|
|
40
42
|
|
|
41
43
|
/**
|
|
@@ -67,7 +69,10 @@ export async function fetchText(
|
|
|
67
69
|
|
|
68
70
|
let response: Awaited<ReturnType<FetchLike>>;
|
|
69
71
|
try {
|
|
70
|
-
response = await fetchImpl(
|
|
72
|
+
response = await fetchImpl(
|
|
73
|
+
url,
|
|
74
|
+
options.signal === undefined ? { headers } : { headers, signal: options.signal }
|
|
75
|
+
);
|
|
71
76
|
} catch (error) {
|
|
72
77
|
throw new ConfigError(
|
|
73
78
|
`Sous could not reach ${url} while fetching the ${label}.\n` +
|
|
@@ -97,8 +97,13 @@ export type GetIndexOptions = {
|
|
|
97
97
|
* shown; the identity is used when nothing supplies one.
|
|
98
98
|
*/
|
|
99
99
|
label?: string;
|
|
100
|
+
/** Cancels the request, for a caller that will not wait past a deadline. */
|
|
101
|
+
signal?: AbortSignal;
|
|
100
102
|
};
|
|
101
103
|
|
|
104
|
+
/** Which repository to read from upstream, without touching the cache. */
|
|
105
|
+
export type FetchUpstreamOptions = Pick<GetIndexOptions, "url" | "provider" | "signal">;
|
|
106
|
+
|
|
102
107
|
/** How the cache is built. */
|
|
103
108
|
export type IndexCacheOptions = {
|
|
104
109
|
/** The store's root directory; the cache lives in a subdirectory of it. */
|
|
@@ -334,23 +339,7 @@ export class IndexCache {
|
|
|
334
339
|
* @param options - The repository URL and its provider.
|
|
335
340
|
*/
|
|
336
341
|
async refresh(identity: string, options: GetIndexOptions): Promise<IndexLookup> {
|
|
337
|
-
const
|
|
338
|
-
const repo = provider.canonicalize(options.url);
|
|
339
|
-
const fetched = await provider.fetchIndex(repo, this.providerOptions);
|
|
340
|
-
|
|
341
|
-
let parsed: unknown;
|
|
342
|
-
try {
|
|
343
|
-
parsed = JSON.parse(fetched.text);
|
|
344
|
-
} catch (error) {
|
|
345
|
-
throw new ConfigError(
|
|
346
|
-
`The index that ${options.url} published is not valid JSON.\n` +
|
|
347
|
-
` ${(error as Error).message}\n` +
|
|
348
|
-
` A repository's index is written by 'sous repo release'; this one may be ` +
|
|
349
|
-
`damaged or may not be a sous repository at all.`
|
|
350
|
-
);
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
const index = parseIndexFile(parsed, `${options.url} (${fetched.ref})`);
|
|
342
|
+
const { index, fetched } = await this.fetchFromProvider(options);
|
|
354
343
|
const timestamp = this.now().toISOString();
|
|
355
344
|
const meta: IndexMeta = {
|
|
356
345
|
fetchedAt: timestamp,
|
|
@@ -369,6 +358,56 @@ export class IndexCache {
|
|
|
369
358
|
return { index: this.applyOverlay(identity, index), source: "network", meta };
|
|
370
359
|
}
|
|
371
360
|
|
|
361
|
+
/**
|
|
362
|
+
* Reads a repository's index from upstream and returns it, WITHOUT writing
|
|
363
|
+
* anything: neither the cached copy nor its sidecar changes. This is what a
|
|
364
|
+
* browsing command reading upstream uses, because only a command that
|
|
365
|
+
* resolves versions should change what the cache holds. Any installed overlay
|
|
366
|
+
* is folded into the answer, as it is for a cached copy. A failure is raised;
|
|
367
|
+
* the caller decides whether a cached copy stands in for it.
|
|
368
|
+
*
|
|
369
|
+
* @param identity - The repository's canonical identity, for the overlay.
|
|
370
|
+
* @param options - The repository URL, its provider, and an optional cancel signal.
|
|
371
|
+
*/
|
|
372
|
+
async fetchUpstream(identity: string, options: FetchUpstreamOptions): Promise<IndexFile> {
|
|
373
|
+
const { index } = await this.fetchFromProvider(options);
|
|
374
|
+
return this.applyOverlay(identity, index);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Fetches and validates a repository's index through its provider. Writes
|
|
379
|
+
* nothing; `refresh` and `fetchUpstream` decide what happens next.
|
|
380
|
+
*
|
|
381
|
+
* @param options - The repository URL, its provider, and an optional cancel signal.
|
|
382
|
+
*/
|
|
383
|
+
private async fetchFromProvider(
|
|
384
|
+
options: FetchUpstreamOptions
|
|
385
|
+
): Promise<{ index: IndexFile; fetched: { ref: string; etag?: string } }> {
|
|
386
|
+
const provider = this.resolveProvider(options.url, options.provider);
|
|
387
|
+
const repo = provider.canonicalize(options.url);
|
|
388
|
+
const fetched = await provider.fetchIndex(
|
|
389
|
+
repo,
|
|
390
|
+
options.signal === undefined
|
|
391
|
+
? this.providerOptions
|
|
392
|
+
: { ...this.providerOptions, signal: options.signal }
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
let parsed: unknown;
|
|
396
|
+
try {
|
|
397
|
+
parsed = JSON.parse(fetched.text);
|
|
398
|
+
} catch (error) {
|
|
399
|
+
throw new ConfigError(
|
|
400
|
+
`The index that ${options.url} published is not valid JSON.\n` +
|
|
401
|
+
` ${(error as Error).message}\n` +
|
|
402
|
+
` A repository's index is written by 'sous repo release'; this one may be ` +
|
|
403
|
+
`damaged or may not be a sous repository at all.`
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const index = parseIndexFile(parsed, `${options.url} (${fetched.ref})`);
|
|
408
|
+
return { index, fetched };
|
|
409
|
+
}
|
|
410
|
+
|
|
372
411
|
/**
|
|
373
412
|
* Forgets a repository's cached index and sidecar, which is what removing a
|
|
374
413
|
* repository from a project does.
|
|
@@ -27,10 +27,13 @@ import type { FetchLike } from "./http.js";
|
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
29
|
* What a provider can do. `fetch` is the read path every provider implements;
|
|
30
|
-
* `submit` is the propose-a-change path
|
|
31
|
-
*
|
|
30
|
+
* `submit` is the propose-a-change path; `proposals` is looking a proposal up
|
|
31
|
+
* again afterwards (finding the one a branch already has, reporting its status,
|
|
32
|
+
* and replacing its title or body), which is what lets `sous repo submit`
|
|
33
|
+
* handle a proposal's whole life rather than only its first day. A provider
|
|
34
|
+
* declares a feature only once it genuinely supports it.
|
|
32
35
|
*/
|
|
33
|
-
export type ProviderFeature = "fetch" | "submit";
|
|
36
|
+
export type ProviderFeature = "fetch" | "submit" | "proposals";
|
|
34
37
|
|
|
35
38
|
/**
|
|
36
39
|
* The identifier a repo entry uses to name its provider explicitly. `local` is a
|
|
@@ -67,6 +70,8 @@ export type ProviderOptions = {
|
|
|
67
70
|
fetchImpl?: FetchLike;
|
|
68
71
|
/** How subprocesses are run. Defaults to spawning a real process. */
|
|
69
72
|
run?: CommandRunner;
|
|
73
|
+
/** Cancels an index request, for a caller that will not wait past a deadline. */
|
|
74
|
+
signal?: AbortSignal;
|
|
70
75
|
};
|
|
71
76
|
|
|
72
77
|
/** What an index fetch returns. */
|
|
@@ -140,6 +145,70 @@ export type ProposedChange = {
|
|
|
140
145
|
detail: string;
|
|
141
146
|
};
|
|
142
147
|
|
|
148
|
+
/** Where a proposal stands: still open, merged, or closed without merging. */
|
|
149
|
+
export type ProposalState = "open" | "merged" | "closed";
|
|
150
|
+
|
|
151
|
+
/** One proposal, as plain data every provider can describe. */
|
|
152
|
+
export type ProposalSummary = {
|
|
153
|
+
/** How the provider identifies it, such as a pull request number. */
|
|
154
|
+
id: string;
|
|
155
|
+
/** Its address, when the provider reported one. */
|
|
156
|
+
url?: string;
|
|
157
|
+
/** Where it stands. */
|
|
158
|
+
state: ProposalState;
|
|
159
|
+
/** Its current title. */
|
|
160
|
+
title: string;
|
|
161
|
+
/** True when it is a draft. */
|
|
162
|
+
draft: boolean;
|
|
163
|
+
/** The branch it targets, when the provider reported it. */
|
|
164
|
+
base?: string;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
/** Which proposal to look for: the one a branch was pushed for. */
|
|
168
|
+
export type ProposalQuery = {
|
|
169
|
+
/** The branch the change is on. */
|
|
170
|
+
branch: string;
|
|
171
|
+
/**
|
|
172
|
+
* True when the branch lives on a fork rather than in the repository itself.
|
|
173
|
+
* A proposal from a fork is found by the fork's owner as well as the branch,
|
|
174
|
+
* so two contributors' branches of the same name are never confused.
|
|
175
|
+
*/
|
|
176
|
+
fromFork: boolean;
|
|
177
|
+
/**
|
|
178
|
+
* The account the fork lives under, when the caller knows it. Left out, the
|
|
179
|
+
* provider asks the host which account is signed in.
|
|
180
|
+
*/
|
|
181
|
+
forkOwner?: string;
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
/** How a proposal's review is going, in words every host can be mapped onto. */
|
|
185
|
+
export type ProposalReview = "approved" | "changes requested" | "review required";
|
|
186
|
+
|
|
187
|
+
/** How the automated checks on a proposal stand, counted. */
|
|
188
|
+
export type ProposalChecks = {
|
|
189
|
+
passed: number;
|
|
190
|
+
failed: number;
|
|
191
|
+
pending: number;
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
/** Everything a status report says about one proposal. */
|
|
195
|
+
export type ProposalStatus = {
|
|
196
|
+
/** The proposal itself. */
|
|
197
|
+
proposal: ProposalSummary;
|
|
198
|
+
/** How its review is going, when the host reports it. */
|
|
199
|
+
review?: ProposalReview;
|
|
200
|
+
/** How its checks stand, when it has any. */
|
|
201
|
+
checks?: ProposalChecks;
|
|
202
|
+
/** Whether it can be merged as it stands; undefined when the host is still working it out. */
|
|
203
|
+
mergeable?: boolean;
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
/** What to replace on an open proposal. A field left out is left as it is. */
|
|
207
|
+
export type ProposalUpdate = {
|
|
208
|
+
title?: string;
|
|
209
|
+
body?: string;
|
|
210
|
+
};
|
|
211
|
+
|
|
143
212
|
/** One repository host sous knows how to read from. */
|
|
144
213
|
export interface RepoProvider {
|
|
145
214
|
/** The provider's stable identifier, as written in a repo config entry. */
|
|
@@ -194,6 +263,55 @@ export interface RepoProvider {
|
|
|
194
263
|
proposal: ChangeProposal,
|
|
195
264
|
options?: ProviderOptions
|
|
196
265
|
): Promise<ProposedChange>;
|
|
266
|
+
|
|
267
|
+
// --- Proposals after the fact, answered by a provider that declares `proposals`
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* The proposal a branch was pushed for, or undefined when it has none. When a
|
|
271
|
+
* branch has had several, the open one wins, and otherwise the newest.
|
|
272
|
+
*/
|
|
273
|
+
findProposal?(
|
|
274
|
+
repo: CanonicalRepo,
|
|
275
|
+
query: ProposalQuery,
|
|
276
|
+
options?: ProviderOptions
|
|
277
|
+
): Promise<ProposalSummary | undefined>;
|
|
278
|
+
/** Where one proposal stands: its state, its review and its checks. */
|
|
279
|
+
proposalStatus?(
|
|
280
|
+
repo: CanonicalRepo,
|
|
281
|
+
id: string,
|
|
282
|
+
options?: ProviderOptions
|
|
283
|
+
): Promise<ProposalStatus>;
|
|
284
|
+
/** Replaces an open proposal's title, its body, or both. */
|
|
285
|
+
updateProposal?(
|
|
286
|
+
repo: CanonicalRepo,
|
|
287
|
+
id: string,
|
|
288
|
+
update: ProposalUpdate,
|
|
289
|
+
options?: ProviderOptions
|
|
290
|
+
): Promise<ProposedChange>;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* A provider that can find a proposal again, report on it and change its text.
|
|
295
|
+
* This is what declaring the `proposals` feature promises.
|
|
296
|
+
*/
|
|
297
|
+
export type ProposalCapableProvider = RepoProvider &
|
|
298
|
+
Required<Pick<RepoProvider, "findProposal" | "proposalStatus" | "updateProposal">>;
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* True when a provider declares the `proposals` feature and really does answer
|
|
302
|
+
* all three calls behind it.
|
|
303
|
+
*
|
|
304
|
+
* @param provider - The provider to test.
|
|
305
|
+
*/
|
|
306
|
+
export function supportsProposals(
|
|
307
|
+
provider: RepoProvider
|
|
308
|
+
): provider is ProposalCapableProvider {
|
|
309
|
+
return (
|
|
310
|
+
provider.features.includes("proposals") &&
|
|
311
|
+
typeof provider.findProposal === "function" &&
|
|
312
|
+
typeof provider.proposalStatus === "function" &&
|
|
313
|
+
typeof provider.updateProposal === "function"
|
|
314
|
+
);
|
|
197
315
|
}
|
|
198
316
|
|
|
199
317
|
/**
|