@sous-io/sous 0.2.16 → 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 +23 -7
- package/docs/markdown/repositories-authoring.md +52 -11
- 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/repo/release.ts +41 -0
- package/src/commands/repo/submit.ts +245 -35
- 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/providers/base.ts +33 -1
- package/src/lib/repos/providers/github.ts +275 -3
- package/src/lib/repos/providers/provider.ts +119 -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
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
* request opened by `gh pr create`, a contributor without push permission works
|
|
13
13
|
* through a fork made by `gh repo fork`, and both are reported back as plain
|
|
14
14
|
* data, so the service that sequences them never learns a GitHub-shaped fact.
|
|
15
|
+
* Finding a pull request again, reporting where it stands and replacing its
|
|
16
|
+
* text go through `gh pr list`, `gh pr view` and `gh pr edit` the same way.
|
|
15
17
|
*/
|
|
16
18
|
|
|
17
19
|
import { ConfigError } from "../../errors.js";
|
|
@@ -28,6 +30,13 @@ import {
|
|
|
28
30
|
type ChangeProposal,
|
|
29
31
|
type FetchedIndex,
|
|
30
32
|
type ForkedRepo,
|
|
33
|
+
type ProposalChecks,
|
|
34
|
+
type ProposalQuery,
|
|
35
|
+
type ProposalReview,
|
|
36
|
+
type ProposalState,
|
|
37
|
+
type ProposalStatus,
|
|
38
|
+
type ProposalSummary,
|
|
39
|
+
type ProposalUpdate,
|
|
31
40
|
type ProposedChange,
|
|
32
41
|
type ProviderCli,
|
|
33
42
|
type ProviderFeature,
|
|
@@ -59,10 +68,10 @@ export class GithubProvider extends ProviderBase {
|
|
|
59
68
|
readonly id = "github" as const;
|
|
60
69
|
|
|
61
70
|
/**
|
|
62
|
-
* Reads the index and recipe subtrees,
|
|
63
|
-
*
|
|
71
|
+
* Reads the index and recipe subtrees, proposes a change through the GitHub
|
|
72
|
+
* CLI ('gh'), and finds, reports on and updates that pull request afterwards.
|
|
64
73
|
*/
|
|
65
|
-
readonly features: ProviderFeature[] = ["fetch", "submit"];
|
|
74
|
+
readonly features: ProviderFeature[] = ["fetch", "submit", "proposals"];
|
|
66
75
|
|
|
67
76
|
/** The command line tool the write path is built on. */
|
|
68
77
|
readonly cli: ProviderCli = {
|
|
@@ -292,4 +301,267 @@ export class GithubProvider extends ProviderBase {
|
|
|
292
301
|
}
|
|
293
302
|
return { url, detail: `The ${this.proposalNoun} is at ${url}.` };
|
|
294
303
|
}
|
|
304
|
+
|
|
305
|
+
// --- Proposals after the fact ------------------------------------------------
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* The pull request a branch was pushed for. GitHub lists pull requests by
|
|
309
|
+
* head branch name alone, so the list is narrowed here by where the branch
|
|
310
|
+
* lives: the repository itself, or the contributor's fork. When a branch has
|
|
311
|
+
* had several, the open one wins, and otherwise the newest.
|
|
312
|
+
*
|
|
313
|
+
* @param repo - The canonicalized repository the proposal targets.
|
|
314
|
+
* @param query - The branch, and whether it lives on a fork.
|
|
315
|
+
* @param options - Subprocess runner and working directory overrides.
|
|
316
|
+
*/
|
|
317
|
+
async findProposal(
|
|
318
|
+
repo: CanonicalRepo,
|
|
319
|
+
query: ProposalQuery,
|
|
320
|
+
options: ProviderOptions = {}
|
|
321
|
+
): Promise<ProposalSummary | undefined> {
|
|
322
|
+
const forkOwner = query.fromFork
|
|
323
|
+
? (query.forkOwner ?? (await this.signedInLogin(options)))
|
|
324
|
+
: undefined;
|
|
325
|
+
|
|
326
|
+
const listed = await this.ghJson<GhPullRequest[]>(
|
|
327
|
+
[
|
|
328
|
+
"pr",
|
|
329
|
+
"list",
|
|
330
|
+
"--repo",
|
|
331
|
+
`${repo.owner}/${repo.name}`,
|
|
332
|
+
"--head",
|
|
333
|
+
query.branch,
|
|
334
|
+
"--state",
|
|
335
|
+
"all",
|
|
336
|
+
"--limit",
|
|
337
|
+
"50",
|
|
338
|
+
"--json",
|
|
339
|
+
"number,url,state,title,isDraft,baseRefName,headRepositoryOwner,isCrossRepository",
|
|
340
|
+
],
|
|
341
|
+
"pr list",
|
|
342
|
+
options
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
const mine = listed.filter((entry) =>
|
|
346
|
+
query.fromFork
|
|
347
|
+
? entry.isCrossRepository === true && entry.headRepositoryOwner?.login === forkOwner
|
|
348
|
+
: entry.isCrossRepository !== true
|
|
349
|
+
);
|
|
350
|
+
if (mine.length === 0) return undefined;
|
|
351
|
+
|
|
352
|
+
const open = mine.find((entry) => entry.state === "OPEN");
|
|
353
|
+
const chosen = open ?? [...mine].sort((a, b) => b.number - a.number)[0]!;
|
|
354
|
+
return summarizePullRequest(chosen);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Where one pull request stands: its state, its review decision, and how
|
|
359
|
+
* its checks are going, counted.
|
|
360
|
+
*
|
|
361
|
+
* @param repo - The canonicalized repository the proposal targets.
|
|
362
|
+
* @param id - The pull request number.
|
|
363
|
+
* @param options - Subprocess runner and working directory overrides.
|
|
364
|
+
*/
|
|
365
|
+
async proposalStatus(
|
|
366
|
+
repo: CanonicalRepo,
|
|
367
|
+
id: string,
|
|
368
|
+
options: ProviderOptions = {}
|
|
369
|
+
): Promise<ProposalStatus> {
|
|
370
|
+
const viewed = await this.ghJson<GhPullRequest>(
|
|
371
|
+
[
|
|
372
|
+
"pr",
|
|
373
|
+
"view",
|
|
374
|
+
id,
|
|
375
|
+
"--repo",
|
|
376
|
+
`${repo.owner}/${repo.name}`,
|
|
377
|
+
"--json",
|
|
378
|
+
"number,url,state,title,isDraft,baseRefName,reviewDecision,statusCheckRollup,mergeable",
|
|
379
|
+
],
|
|
380
|
+
"pr view",
|
|
381
|
+
options
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
const review = reviewFrom(viewed.reviewDecision);
|
|
385
|
+
const checks = checksFrom(viewed.statusCheckRollup);
|
|
386
|
+
const mergeable =
|
|
387
|
+
viewed.mergeable === "MERGEABLE"
|
|
388
|
+
? true
|
|
389
|
+
: viewed.mergeable === "CONFLICTING"
|
|
390
|
+
? false
|
|
391
|
+
: undefined;
|
|
392
|
+
|
|
393
|
+
return {
|
|
394
|
+
proposal: summarizePullRequest(viewed),
|
|
395
|
+
...(review === undefined ? {} : { review }),
|
|
396
|
+
...(checks === undefined ? {} : { checks }),
|
|
397
|
+
...(mergeable === undefined ? {} : { mergeable }),
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Replaces a pull request's title, its body, or both.
|
|
403
|
+
*
|
|
404
|
+
* @param repo - The canonicalized repository the proposal targets.
|
|
405
|
+
* @param id - The pull request number.
|
|
406
|
+
* @param update - What to replace.
|
|
407
|
+
* @param options - Subprocess runner and working directory overrides.
|
|
408
|
+
*/
|
|
409
|
+
async updateProposal(
|
|
410
|
+
repo: CanonicalRepo,
|
|
411
|
+
id: string,
|
|
412
|
+
update: ProposalUpdate,
|
|
413
|
+
options: ProviderOptions = {}
|
|
414
|
+
): Promise<ProposedChange> {
|
|
415
|
+
const args = ["pr", "edit", id, "--repo", `${repo.owner}/${repo.name}`];
|
|
416
|
+
if (update.title !== undefined) args.push("--title", update.title);
|
|
417
|
+
if (update.body !== undefined) args.push("--body", update.body);
|
|
418
|
+
|
|
419
|
+
const result = await this.runCommand(this.cli.command, args, options);
|
|
420
|
+
if (result.code !== 0) {
|
|
421
|
+
const reported = result.stderr.trim() || result.stdout.trim();
|
|
422
|
+
throw new ConfigError(
|
|
423
|
+
`'${this.cli.command} pr edit' did not succeed, so the ${this.proposalNoun} kept its ` +
|
|
424
|
+
`title and body.` +
|
|
425
|
+
(reported.length === 0 ? "" : `\n ${reported}`)
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const url = firstUrlIn(result.stdout);
|
|
430
|
+
return url === undefined
|
|
431
|
+
? { detail: `The ${this.proposalNoun} was updated.` }
|
|
432
|
+
: { url, detail: `The ${this.proposalNoun} at ${url} was updated.` };
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* The login of the account `gh` is signed in as, which is the owner of any
|
|
437
|
+
* fork sous made for the contributor.
|
|
438
|
+
*
|
|
439
|
+
* @param options - Subprocess runner and working directory overrides.
|
|
440
|
+
*/
|
|
441
|
+
private async signedInLogin(options: ProviderOptions): Promise<string> {
|
|
442
|
+
const who = await this.capturedOutput(
|
|
443
|
+
this.cli.command,
|
|
444
|
+
["api", "user", "--jq", ".login"],
|
|
445
|
+
options
|
|
446
|
+
);
|
|
447
|
+
if (who === undefined || who.trim().length === 0) {
|
|
448
|
+
throw new ConfigError(
|
|
449
|
+
"Sous could not read your GitHub login from " +
|
|
450
|
+
`'${this.cli.command} api user', so it cannot tell which fork a ${this.proposalNoun} ` +
|
|
451
|
+
"would come from."
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
return who.trim();
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Runs a `gh` command that prints JSON, and parses what it printed.
|
|
459
|
+
*
|
|
460
|
+
* @param args - The arguments, ending with the `--json` field list.
|
|
461
|
+
* @param what - The subcommand, as it is named in a failure.
|
|
462
|
+
* @param options - Subprocess runner and working directory overrides.
|
|
463
|
+
*/
|
|
464
|
+
private async ghJson<T>(args: string[], what: string, options: ProviderOptions): Promise<T> {
|
|
465
|
+
const result = await this.runCommand(this.cli.command, args, options);
|
|
466
|
+
if (result.code !== 0) {
|
|
467
|
+
const reported = result.stderr.trim() || result.stdout.trim();
|
|
468
|
+
throw new ConfigError(
|
|
469
|
+
`'${this.cli.command} ${what}' did not succeed.` +
|
|
470
|
+
(reported.length === 0 ? "" : `\n ${reported}`)
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
try {
|
|
474
|
+
return JSON.parse(result.stdout) as T;
|
|
475
|
+
} catch {
|
|
476
|
+
throw new ConfigError(
|
|
477
|
+
`'${this.cli.command} ${what}' printed something that is not JSON, so sous cannot read ` +
|
|
478
|
+
`the ${this.proposalNoun} it describes.`
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// --- What gh prints, and how it maps onto plain data ---------------------------------------------
|
|
485
|
+
|
|
486
|
+
/** The fields sous asks `gh` for, as it prints them. */
|
|
487
|
+
type GhPullRequest = {
|
|
488
|
+
number: number;
|
|
489
|
+
url?: string;
|
|
490
|
+
state?: string;
|
|
491
|
+
title?: string;
|
|
492
|
+
isDraft?: boolean;
|
|
493
|
+
baseRefName?: string;
|
|
494
|
+
headRepositoryOwner?: { login?: string } | null;
|
|
495
|
+
isCrossRepository?: boolean;
|
|
496
|
+
reviewDecision?: string | null;
|
|
497
|
+
statusCheckRollup?: GhCheck[] | null;
|
|
498
|
+
mergeable?: string;
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
/** One entry of a pull request's status check rollup: a check run or a commit status. */
|
|
502
|
+
type GhCheck = {
|
|
503
|
+
__typename?: string;
|
|
504
|
+
status?: string;
|
|
505
|
+
conclusion?: string | null;
|
|
506
|
+
state?: string;
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
/** A pull request's state, in the words every provider shares. */
|
|
510
|
+
function stateFrom(state: string | undefined): ProposalState {
|
|
511
|
+
if (state === "MERGED") return "merged";
|
|
512
|
+
if (state === "CLOSED") return "closed";
|
|
513
|
+
return "open";
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* The plain summary of a pull request.
|
|
518
|
+
*
|
|
519
|
+
* @param entry - What `gh` printed for it.
|
|
520
|
+
*/
|
|
521
|
+
function summarizePullRequest(entry: GhPullRequest): ProposalSummary {
|
|
522
|
+
return {
|
|
523
|
+
id: String(entry.number),
|
|
524
|
+
...(entry.url === undefined ? {} : { url: entry.url }),
|
|
525
|
+
state: stateFrom(entry.state),
|
|
526
|
+
title: entry.title ?? "",
|
|
527
|
+
draft: entry.isDraft === true,
|
|
528
|
+
...(entry.baseRefName === undefined ? {} : { base: entry.baseRefName }),
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/** GitHub's review decision, in the words every provider shares. */
|
|
533
|
+
function reviewFrom(decision: string | null | undefined): ProposalReview | undefined {
|
|
534
|
+
if (decision === "APPROVED") return "approved";
|
|
535
|
+
if (decision === "CHANGES_REQUESTED") return "changes requested";
|
|
536
|
+
if (decision === "REVIEW_REQUIRED") return "review required";
|
|
537
|
+
return undefined;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/** Check runs whose conclusion counts as passing. */
|
|
541
|
+
const PASSING_CONCLUSIONS = new Set(["SUCCESS", "NEUTRAL", "SKIPPED"]);
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Counts a status check rollup into passed, failed and pending. A check run
|
|
545
|
+
* that has not completed is pending; a commit status reports its state directly.
|
|
546
|
+
*
|
|
547
|
+
* @param rollup - What `gh` printed as the rollup, when it printed one.
|
|
548
|
+
*/
|
|
549
|
+
function checksFrom(rollup: GhCheck[] | null | undefined): ProposalChecks | undefined {
|
|
550
|
+
if (rollup === null || rollup === undefined || rollup.length === 0) return undefined;
|
|
551
|
+
const counts: ProposalChecks = { passed: 0, failed: 0, pending: 0 };
|
|
552
|
+
for (const check of rollup) {
|
|
553
|
+
const isStatus =
|
|
554
|
+
check.__typename === "StatusContext" ||
|
|
555
|
+
(check.status === undefined && check.state !== undefined);
|
|
556
|
+
if (isStatus) {
|
|
557
|
+
if (check.state === "SUCCESS") counts.passed += 1;
|
|
558
|
+
else if (check.state === "PENDING" || check.state === "EXPECTED") counts.pending += 1;
|
|
559
|
+
else counts.failed += 1;
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
562
|
+
if (check.status !== "COMPLETED") counts.pending += 1;
|
|
563
|
+
else if (PASSING_CONCLUSIONS.has(check.conclusion ?? "")) counts.passed += 1;
|
|
564
|
+
else counts.failed += 1;
|
|
565
|
+
}
|
|
566
|
+
return counts;
|
|
295
567
|
}
|
|
@@ -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
|
|
@@ -142,6 +145,70 @@ export type ProposedChange = {
|
|
|
142
145
|
detail: string;
|
|
143
146
|
};
|
|
144
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
|
+
|
|
145
212
|
/** One repository host sous knows how to read from. */
|
|
146
213
|
export interface RepoProvider {
|
|
147
214
|
/** The provider's stable identifier, as written in a repo config entry. */
|
|
@@ -196,6 +263,55 @@ export interface RepoProvider {
|
|
|
196
263
|
proposal: ChangeProposal,
|
|
197
264
|
options?: ProviderOptions
|
|
198
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
|
+
);
|
|
199
315
|
}
|
|
200
316
|
|
|
201
317
|
/**
|