@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.
- package/README.md +14 -256
- package/dist/bin.js +11 -0
- package/dist/sous.js +1094 -0
- package/docs/markdown/README.md +5 -4
- package/docs/markdown/_sidebar.md +21 -1
- package/docs/markdown/commands.md +294 -431
- package/docs/markdown/config-discovery.md +25 -14
- package/docs/markdown/config-inspection.md +53 -31
- package/docs/markdown/config-layers.md +168 -66
- package/docs/markdown/config-variables.md +44 -11
- package/docs/markdown/configuration.md +104 -17
- package/docs/markdown/design-principles.md +4 -1
- package/docs/markdown/repositories-authoring.md +30 -0
- package/docs/markdown/repositories-consuming.md +72 -14
- package/docs/markdown/repositories-file-formats.md +31 -13
- package/docs/markdown/repositories-providers.md +48 -57
- package/docs/markdown/repositories-quickstart.md +3 -2
- package/docs/markdown/repositories-troubleshooting.md +27 -1
- package/docs/markdown/repositories-variables.md +6 -1
- package/docs/markdown/repositories.md +18 -8
- package/package.json +48 -107
- package/recipes/core/sous-skills/skills/about-liquid-templates/SKILL.tpl.md +4 -6
- package/recipes/core/sous-skills/skills/about-liquid-templates/references/liquid-filters.tpl.md +2 -0
- package/recipes/core/sous-skills/skills/about-sous/SKILL.tpl.md +10 -9
- package/recipes/core/sous-skills/skills/about-sous-configuration/SKILL.tpl.md +35 -14
- package/recipes/core/sous-skills/skills/about-sous-recipes/SKILL.tpl.md +51 -20
- package/recipes/core/sous-skills/sous.recipe.yaml +1 -1
- package/sous.config.schema.json +351 -132
- package/bin/run.js +0 -51
- package/src/base-command.ts +0 -457
- package/src/commands/build.ts +0 -181
- package/src/commands/clear.ts +0 -79
- package/src/commands/compile.ts +0 -155
- package/src/commands/config/get.ts +0 -110
- package/src/commands/config/show.ts +0 -32
- package/src/commands/config/validate.ts +0 -53
- package/src/commands/help.ts +0 -46
- package/src/commands/init.ts +0 -289
- package/src/commands/launch.ts +0 -139
- package/src/commands/lock/rebuild.ts +0 -239
- package/src/commands/lock/show.ts +0 -115
- package/src/commands/namespace/list.ts +0 -138
- package/src/commands/namespace/show.ts +0 -130
- package/src/commands/prune.ts +0 -43
- package/src/commands/recipe/list.ts +0 -112
- package/src/commands/recipe/show.ts +0 -439
- package/src/commands/repo/add.ts +0 -145
- package/src/commands/repo/contribute.ts +0 -604
- package/src/commands/repo/gc.ts +0 -173
- package/src/commands/repo/init.ts +0 -136
- package/src/commands/repo/link.ts +0 -899
- package/src/commands/repo/list.ts +0 -236
- package/src/commands/repo/release.ts +0 -685
- package/src/commands/repo/remove.ts +0 -194
- package/src/commands/repo/search.ts +0 -243
- package/src/commands/repo/submit.ts +0 -341
- package/src/commands/repo/unlink.ts +0 -461
- package/src/commands/subscription/add.ts +0 -286
- package/src/commands/subscription/list.ts +0 -208
- package/src/commands/subscription/remove.ts +0 -182
- package/src/commands/subscription/update.ts +0 -216
- package/src/commands/update.ts +0 -178
- package/src/commands/vars/ask.ts +0 -417
- package/src/commands/vars/index.ts +0 -79
- package/src/commands/vars/list.ts +0 -67
- package/src/commands/vars/show.ts +0 -77
- package/src/config-command.ts +0 -30
- package/src/lib/build-preparation.ts +0 -128
- package/src/lib/build-service.ts +0 -580
- package/src/lib/config-discovery.ts +0 -378
- package/src/lib/config-inspect.ts +0 -145
- package/src/lib/config-kernel.mjs +0 -379
- package/src/lib/config-schema.ts +0 -401
- package/src/lib/env-file.ts +0 -328
- package/src/lib/env-local.ts +0 -212
- package/src/lib/errors.ts +0 -32
- package/src/lib/glob-pattern.ts +0 -24
- package/src/lib/include-resolver.ts +0 -523
- package/src/lib/interactive.ts +0 -165
- package/src/lib/markdown-compiler.ts +0 -947
- package/src/lib/package-info.ts +0 -25
- package/src/lib/pid-service.ts +0 -99
- package/src/lib/project-install.d.mts +0 -44
- package/src/lib/project-install.mjs +0 -234
- package/src/lib/project-scaffold/index.ts +0 -332
- package/src/lib/project-scaffold/templates.ts +0 -240
- package/src/lib/repos/catalog-display.ts +0 -215
- package/src/lib/repos/catalog-inputs.ts +0 -288
- package/src/lib/repos/catalog.ts +0 -842
- package/src/lib/repos/contribute.ts +0 -452
- package/src/lib/repos/core-recipe.ts +0 -105
- package/src/lib/repos/declarations.ts +0 -187
- package/src/lib/repos/defaults.ts +0 -175
- package/src/lib/repos/formats/common.ts +0 -440
- package/src/lib/repos/formats/index-file.ts +0 -253
- package/src/lib/repos/formats/links-map.ts +0 -98
- package/src/lib/repos/formats/lockfile.ts +0 -169
- package/src/lib/repos/formats/patterns.ts +0 -57
- package/src/lib/repos/formats/recipe-manifest.ts +0 -205
- package/src/lib/repos/formats/repo-manifest.ts +0 -97
- package/src/lib/repos/formats/store-entry.ts +0 -84
- package/src/lib/repos/formats/variable-definition.ts +0 -268
- package/src/lib/repos/freshness.ts +0 -264
- package/src/lib/repos/git-clone.ts +0 -850
- package/src/lib/repos/identity.ts +0 -89
- package/src/lib/repos/index.ts +0 -57
- package/src/lib/repos/links.ts +0 -356
- package/src/lib/repos/load-manifest.ts +0 -237
- package/src/lib/repos/lock-service.ts +0 -454
- package/src/lib/repos/locked-namespace-resolver.ts +0 -232
- package/src/lib/repos/locked-recipe-lookup.ts +0 -123
- package/src/lib/repos/locked-recipes.ts +0 -254
- package/src/lib/repos/managed-layer.ts +0 -422
- package/src/lib/repos/memories-hash-name.ts +0 -33
- package/src/lib/repos/namespace-resolver.ts +0 -527
- package/src/lib/repos/providers/base.ts +0 -268
- package/src/lib/repos/providers/git.ts +0 -245
- package/src/lib/repos/providers/github.ts +0 -589
- package/src/lib/repos/providers/gitlab.ts +0 -332
- package/src/lib/repos/providers/http.ts +0 -128
- package/src/lib/repos/providers/index-cache.ts +0 -421
- package/src/lib/repos/providers/index.ts +0 -106
- package/src/lib/repos/providers/local.ts +0 -410
- package/src/lib/repos/providers/provider.ts +0 -615
- package/src/lib/repos/recipe-config-layers.ts +0 -316
- package/src/lib/repos/recipe-key-matcher.ts +0 -62
- package/src/lib/repos/recipe-memories.ts +0 -178
- package/src/lib/repos/recipe-targets.ts +0 -236
- package/src/lib/repos/ref-search.ts +0 -46
- package/src/lib/repos/release/bump.ts +0 -161
- package/src/lib/repos/release/changelog.ts +0 -461
- package/src/lib/repos/release/git-state.ts +0 -460
- package/src/lib/repos/release/index-builder.ts +0 -1108
- package/src/lib/repos/release/index.ts +0 -19
- package/src/lib/repos/release/plan.ts +0 -521
- package/src/lib/repos/release/settle.ts +0 -154
- package/src/lib/repos/release/submissions.ts +0 -214
- package/src/lib/repos/release/submit-checkout.ts +0 -290
- package/src/lib/repos/release/submit-questions.ts +0 -153
- package/src/lib/repos/release/submit-service.ts +0 -898
- package/src/lib/repos/release/tags.ts +0 -239
- package/src/lib/repos/release/validate.ts +0 -463
- package/src/lib/repos/resolver.ts +0 -1028
- package/src/lib/repos/scaffold/index.ts +0 -245
- package/src/lib/repos/scaffold/templates.ts +0 -517
- package/src/lib/repos/seed.ts +0 -473
- package/src/lib/repos/store/contract.ts +0 -64
- package/src/lib/repos/store/hash.ts +0 -191
- package/src/lib/repos/store/recipe-store.ts +0 -599
- package/src/lib/repos/store/settings.ts +0 -58
- package/src/lib/repos/subscription-service.ts +0 -3859
- package/src/lib/repos/trust.ts +0 -447
- package/src/lib/repos/update-plan.ts +0 -235
- package/src/lib/self-update/index.ts +0 -14
- package/src/lib/self-update/installs.ts +0 -413
- package/src/lib/self-update/managers.ts +0 -126
- package/src/lib/self-update/plan.ts +0 -315
- package/src/lib/self-update/registry.ts +0 -99
- package/src/lib/self-update/run.ts +0 -506
- package/src/lib/self-update/versions.ts +0 -243
- package/src/lib/settings.ts +0 -1107
- package/src/lib/sous-home.ts +0 -104
- package/src/lib/state.ts +0 -167
- package/src/lib/vars/answers.ts +0 -192
- package/src/lib/vars/ask.ts +0 -1152
- package/src/lib/vars/definition-source.ts +0 -261
- package/src/lib/vars/display.ts +0 -231
- package/src/lib/vars/index.ts +0 -20
- package/src/lib/vars/ladder.ts +0 -282
- package/src/lib/vars/mappings.ts +0 -260
- package/src/lib/vars/names.ts +0 -94
- package/src/lib/vars/preanswers.ts +0 -395
- package/src/lib/vars/question-plan.ts +0 -241
- package/src/lib/vars/report.ts +0 -228
- package/src/lib/vars/safe-regex.ts +0 -235
- package/src/lib/vars/secrets.ts +0 -131
- package/src/lib/vars/validate.ts +0 -312
- package/src/lib/version-report.ts +0 -90
- package/src/lib/watch-loop.ts +0 -148
- package/src/lib/watch-service.ts +0 -116
- package/src/services/ref-resolver/container.ts +0 -107
- package/src/services/ref-resolver/dependency.ts +0 -60
- package/src/services/ref-resolver/format.ts +0 -197
- package/src/services/ref-resolver/glob.ts +0 -115
- package/src/services/ref-resolver/hash-names.ts +0 -155
- package/src/services/ref-resolver/index.ts +0 -55
- package/src/services/ref-resolver/injectable.ts +0 -36
- package/src/services/ref-resolver/location.ts +0 -62
- package/src/services/ref-resolver/lookups/cached-index-lookup.ts +0 -53
- package/src/services/ref-resolver/lookups/catalog-lookup.ts +0 -23
- package/src/services/ref-resolver/lookups/catalog-matcher.ts +0 -261
- package/src/services/ref-resolver/lookups/chained-lookup.ts +0 -27
- package/src/services/ref-resolver/lookups/env-var-lookup.ts +0 -65
- package/src/services/ref-resolver/lookups/fetched-index-lookup.ts +0 -150
- package/src/services/ref-resolver/lookups/recorded-dependency-lookup.ts +0 -62
- package/src/services/ref-resolver/lookups/ref-lookup.ts +0 -54
- package/src/services/ref-resolver/lookups/variable-lookup.ts +0 -123
- package/src/services/ref-resolver/named-refs.ts +0 -144
- package/src/services/ref-resolver/parser/env-var-splitter.ts +0 -27
- package/src/services/ref-resolver/parser/location-splitter.ts +0 -239
- package/src/services/ref-resolver/parser/name-path-splitter.ts +0 -200
- package/src/services/ref-resolver/parser/partial-ref.ts +0 -84
- package/src/services/ref-resolver/parser/query-splitter.ts +0 -61
- package/src/services/ref-resolver/parser/range-splitter.ts +0 -52
- package/src/services/ref-resolver/parser/ref-parser.ts +0 -115
- package/src/services/ref-resolver/parser/repo-qualifier-splitter.ts +0 -51
- package/src/services/ref-resolver/parts.ts +0 -115
- package/src/services/ref-resolver/pruners/command-line-pruner.ts +0 -47
- package/src/services/ref-resolver/pruners/include-pruner.ts +0 -66
- package/src/services/ref-resolver/pruners/manifest-pruner.ts +0 -130
- package/src/services/ref-resolver/pruners/ref-pruner.ts +0 -135
- package/src/services/ref-resolver/pruners/rule-helpers.ts +0 -44
- package/src/services/ref-resolver/pruners/stored-key-pruner.ts +0 -136
- package/src/services/ref-resolver/ref-helpers.ts +0 -62
- package/src/services/ref-resolver/ref-picker-service.ts +0 -204
- package/src/services/ref-resolver/ref-report.ts +0 -84
- package/src/services/ref-resolver/ref-resolve-arguments.ts +0 -47
- package/src/services/ref-resolver/ref-resolve-result.ts +0 -36
- package/src/services/ref-resolver/ref-resolver-service.ts +0 -323
- package/src/services/ref-resolver/source.ts +0 -48
- package/src/services/ref-resolver/tokens.ts +0 -25
- package/src/services/ref-resolver/types.ts +0 -155
- package/src/templating/filters/bullet-list.ts +0 -9
- package/src/templating/filters/index.ts +0 -8
- package/src/templating/init-liquid-engine.ts +0 -139
- package/src/templating/lib/glob-files.ts +0 -74
- package/src/templating/lib/import-export.ts +0 -32
- package/src/templating/lib/secret-scope.ts +0 -41
- package/src/templating/lib/tag-args.ts +0 -19
- package/src/templating/tags/exportScalarVarsJs.ts +0 -46
- package/src/templating/tags/getFiles.ts +0 -89
- package/src/templating/tags/index.ts +0 -14
- package/src/templating/tags/listFiles.ts +0 -54
- package/src/templating/tags/showVars.ts +0 -28
- package/src/utils/choice-prompt.ts +0 -143
- package/src/utils/command-errors.ts +0 -186
- package/src/utils/command-help.ts +0 -45
- package/src/utils/confirm-prompt.ts +0 -110
- package/src/utils/flags.ts +0 -177
- package/src/utils/formatting.ts +0 -849
- package/src/utils/prompts.ts +0 -53
- package/src/utils/sous-directory.ts +0 -245
- package/src/utils/table.ts +0 -603
- package/src/utils/value-prompt.ts +0 -119
|
@@ -1,589 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The GitHub provider: the read path, and the write path behind it.
|
|
3
|
-
*
|
|
4
|
-
* Reading a repo needs no GitHub API and no `gh`: the index file is one raw
|
|
5
|
-
* HTTPS GET, and a recipe's subtree comes from git itself. A token is used when
|
|
6
|
-
* one is available, so private repositories work; it is read from GITHUB_TOKEN,
|
|
7
|
-
* or asked of the `gh` command line tool when that is installed and signed in.
|
|
8
|
-
* A missing `gh` is never an error on the read path.
|
|
9
|
-
*
|
|
10
|
-
* The write path is where `gh` becomes load-bearing, and this file is the ONLY
|
|
11
|
-
* place in sous that knows the command exists. Proposing a change is a pull
|
|
12
|
-
* request opened by `gh pr create`, a contributor without push permission works
|
|
13
|
-
* through a fork made by `gh repo fork`, and both are reported back as plain
|
|
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.
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import { ConfigError } from "../../errors.js";
|
|
20
|
-
import { INDEX_FILENAME } from "../formats/common.js";
|
|
21
|
-
import { ProviderBase, firstUrlIn } from "./base.js";
|
|
22
|
-
import { fetchSubtree, type CommandRunner } from "./git.js";
|
|
23
|
-
import { fetchText, type FetchLike } from "./http.js";
|
|
24
|
-
import {
|
|
25
|
-
buildCanonicalRepo,
|
|
26
|
-
invalidRepoUrl,
|
|
27
|
-
readingsAfterRepository,
|
|
28
|
-
splitRepoUrl,
|
|
29
|
-
withoutGitSuffix,
|
|
30
|
-
type LocationReading,
|
|
31
|
-
type WrittenLocation,
|
|
32
|
-
type AuthStatus,
|
|
33
|
-
type CanonicalRepo,
|
|
34
|
-
type ChangeProposal,
|
|
35
|
-
type FetchedIndex,
|
|
36
|
-
type ForkedRepo,
|
|
37
|
-
type ProposalChecks,
|
|
38
|
-
type ProposalQuery,
|
|
39
|
-
type ProposalReview,
|
|
40
|
-
type ProposalState,
|
|
41
|
-
type ProposalStatus,
|
|
42
|
-
type ProposalSummary,
|
|
43
|
-
type ProposalUpdate,
|
|
44
|
-
type ProposedChange,
|
|
45
|
-
type ProviderCli,
|
|
46
|
-
type ProviderFeature,
|
|
47
|
-
type ProviderOptions,
|
|
48
|
-
} from "./provider.js";
|
|
49
|
-
|
|
50
|
-
/** The host this provider serves when a URL does not say otherwise. */
|
|
51
|
-
export const GITHUB_HOST = "github.com";
|
|
52
|
-
|
|
53
|
-
/** The environment variable a GitHub token is read from. */
|
|
54
|
-
export const GITHUB_TOKEN_ENV = "GITHUB_TOKEN";
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Finds a GitHub token: the environment first, then `gh auth token` when the
|
|
58
|
-
* `gh` command line tool is installed and signed in. Returns undefined when
|
|
59
|
-
* there is none, because public repositories need no token at all.
|
|
60
|
-
*
|
|
61
|
-
* @param options - Environment and subprocess runner overrides.
|
|
62
|
-
*/
|
|
63
|
-
export async function findGithubToken(options: {
|
|
64
|
-
env?: NodeJS.ProcessEnv;
|
|
65
|
-
run?: CommandRunner;
|
|
66
|
-
} = {}): Promise<string | undefined> {
|
|
67
|
-
return new GithubProvider().token(options);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/** The GitHub provider. */
|
|
71
|
-
export class GithubProvider extends ProviderBase {
|
|
72
|
-
readonly id = "github" as const;
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Reads the index and recipe subtrees, proposes a change through the GitHub
|
|
76
|
-
* CLI ('gh'), and finds, reports on and updates that pull request afterwards.
|
|
77
|
-
*/
|
|
78
|
-
readonly features: ProviderFeature[] = ["fetch", "submit", "proposals"];
|
|
79
|
-
|
|
80
|
-
/** The command line tool the write path is built on. */
|
|
81
|
-
readonly cli: ProviderCli = {
|
|
82
|
-
command: "gh",
|
|
83
|
-
label: "the GitHub CLI",
|
|
84
|
-
install: "https://cli.github.com",
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
/** What GitHub calls a proposal. */
|
|
88
|
-
readonly proposalNoun = "pull request";
|
|
89
|
-
|
|
90
|
-
/** The host a `github://` locator means when it names none. */
|
|
91
|
-
readonly defaultHost = GITHUB_HOST;
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* A GitHub repository is always an owner and a name, so the first two
|
|
95
|
-
* segments are the repository and everything after them names something
|
|
96
|
-
* inside it: a namespace, a recipe, or a browser path after `tree/` or
|
|
97
|
-
* `blob/`.
|
|
98
|
-
*
|
|
99
|
-
* @param location - The host and the segments after it.
|
|
100
|
-
*/
|
|
101
|
-
readLocation(location: WrittenLocation): LocationReading[] {
|
|
102
|
-
const { segments } = location;
|
|
103
|
-
if (segments.length < 2) return [];
|
|
104
|
-
const repoPath = `${segments[0]}/${withoutGitSuffix(segments[1]!)}`;
|
|
105
|
-
return readingsAfterRepository(repoPath, segments.slice(2));
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
matches(url: string): boolean {
|
|
109
|
-
const parts = splitRepoUrl(url);
|
|
110
|
-
return parts !== undefined && parts.host === GITHUB_HOST;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
canonicalize(url: string): CanonicalRepo {
|
|
114
|
-
const parts = splitRepoUrl(url);
|
|
115
|
-
if (parts === undefined) throw invalidRepoUrl(this.id, url);
|
|
116
|
-
return buildCanonicalRepo(parts.host, parts.owner, parts.name);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* A GitHub token, from the environment or from `gh`. Public, because the
|
|
121
|
-
* index cache and the exported `findGithubToken` both ask for one.
|
|
122
|
-
*
|
|
123
|
-
* @param options - Environment and subprocess runner overrides.
|
|
124
|
-
*/
|
|
125
|
-
async token(options: ProviderOptions = {}): Promise<string | undefined> {
|
|
126
|
-
return this.findToken(GITHUB_TOKEN_ENV, ["auth", "token"], options);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Fetches the repo's index file from the raw content host at the repository's
|
|
131
|
-
* default branch, which is what `HEAD` names there.
|
|
132
|
-
*
|
|
133
|
-
* @param repo - The canonicalized repository.
|
|
134
|
-
* @param options - Environment, fetch and subprocess overrides.
|
|
135
|
-
*/
|
|
136
|
-
async fetchIndex(
|
|
137
|
-
repo: CanonicalRepo,
|
|
138
|
-
options: ProviderOptions = {}
|
|
139
|
-
): Promise<FetchedIndex> {
|
|
140
|
-
const token = await this.token(options);
|
|
141
|
-
const url = this.indexUrl(repo);
|
|
142
|
-
const fetched = await fetchText(url, {
|
|
143
|
-
...(token === undefined ? {} : { token }),
|
|
144
|
-
...(options.fetchImpl === undefined
|
|
145
|
-
? {}
|
|
146
|
-
: { fetchImpl: options.fetchImpl as FetchLike }),
|
|
147
|
-
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
148
|
-
label: "repo index",
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
return fetched.etag === undefined
|
|
152
|
-
? { text: fetched.text, ref: "HEAD" }
|
|
153
|
-
: { text: fetched.text, ref: "HEAD", etag: fetched.etag };
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Fetches one recipe folder at one tag. Private repositories work through
|
|
158
|
-
* git's own credential helpers, the same way a manual clone would.
|
|
159
|
-
*
|
|
160
|
-
* @param repo - The canonicalized repository.
|
|
161
|
-
* @param recipePath - The recipe folder, relative to the repository root.
|
|
162
|
-
* @param tag - The git tag carrying the version.
|
|
163
|
-
* @param destDir - Where the recipe's files should end up.
|
|
164
|
-
* @param options - Subprocess runner override.
|
|
165
|
-
*/
|
|
166
|
-
async fetchRecipeTree(
|
|
167
|
-
repo: CanonicalRepo,
|
|
168
|
-
recipePath: string,
|
|
169
|
-
tag: string,
|
|
170
|
-
destDir: string,
|
|
171
|
-
options: ProviderOptions = {}
|
|
172
|
-
): Promise<void> {
|
|
173
|
-
await fetchSubtree({
|
|
174
|
-
cloneUrl: repo.httpsUrl,
|
|
175
|
-
tag,
|
|
176
|
-
subPath: recipePath,
|
|
177
|
-
destDir,
|
|
178
|
-
...(options.run === undefined ? {} : { run: options.run }),
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* The raw URL of a repository's index file at its default branch.
|
|
184
|
-
*
|
|
185
|
-
* @param repo - The canonicalized repository.
|
|
186
|
-
*/
|
|
187
|
-
indexUrl(repo: CanonicalRepo): string {
|
|
188
|
-
return `https://raw.githubusercontent.com/${repo.owner}/${repo.name}/HEAD/${INDEX_FILENAME}`;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// --- The write path --------------------------------------------------------
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* Whether `gh` is installed and signed in. The detail is the whole
|
|
195
|
-
* explanation, ready to print, because only this provider knows what to
|
|
196
|
-
* install and which command signs in.
|
|
197
|
-
*
|
|
198
|
-
* @param options - Subprocess runner and working directory overrides.
|
|
199
|
-
*/
|
|
200
|
-
async authStatus(options: ProviderOptions = {}): Promise<AuthStatus> {
|
|
201
|
-
const ok = await this.commandSucceeds(this.cli.command, ["auth", "status"], options);
|
|
202
|
-
if (ok) {
|
|
203
|
-
return {
|
|
204
|
-
ok: true,
|
|
205
|
-
detail: `${this.cli.label} ('${this.cli.command}') is installed and signed in.`,
|
|
206
|
-
};
|
|
207
|
-
}
|
|
208
|
-
return {
|
|
209
|
-
ok: false,
|
|
210
|
-
detail:
|
|
211
|
-
`Sous proposes a change through ${this.cli.label} ('${this.cli.command}'), and it is ` +
|
|
212
|
-
`either not installed or not signed in.\n` +
|
|
213
|
-
` Install it from ${this.cli.install}, then run '${this.cli.command} auth login'.`,
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* Whether the signed-in contributor may push to the repository itself, as
|
|
219
|
-
* GitHub reports it. Undefined when `gh` could not answer at all, which is
|
|
220
|
-
* not the same as a refusal.
|
|
221
|
-
*
|
|
222
|
-
* @param repo - The canonicalized repository.
|
|
223
|
-
* @param options - Subprocess runner and working directory overrides.
|
|
224
|
-
*/
|
|
225
|
-
async canPush(
|
|
226
|
-
repo: CanonicalRepo,
|
|
227
|
-
options: ProviderOptions = {}
|
|
228
|
-
): Promise<boolean | undefined> {
|
|
229
|
-
const answer = await this.capturedOutput(
|
|
230
|
-
this.cli.command,
|
|
231
|
-
["api", `repos/${repo.owner}/${repo.name}`, "--jq", ".permissions.push"],
|
|
232
|
-
options
|
|
233
|
-
);
|
|
234
|
-
if (answer === undefined) return undefined;
|
|
235
|
-
const value = answer.trim();
|
|
236
|
-
if (value === "true") return true;
|
|
237
|
-
if (value === "false") return false;
|
|
238
|
-
return undefined;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/**
|
|
242
|
-
* Forks the repository onto the contributor's own account and says where the
|
|
243
|
-
* fork landed. No remote is added here; that is git's business, and the
|
|
244
|
-
* service above does it with the URLs returned.
|
|
245
|
-
*
|
|
246
|
-
* @param repo - The canonicalized repository.
|
|
247
|
-
* @param options - Subprocess runner and working directory overrides.
|
|
248
|
-
*/
|
|
249
|
-
async fork(repo: CanonicalRepo, options: ProviderOptions = {}): Promise<ForkedRepo> {
|
|
250
|
-
const forked = await this.runCommand(
|
|
251
|
-
this.cli.command,
|
|
252
|
-
["repo", "fork", `${repo.owner}/${repo.name}`, "--remote=false"],
|
|
253
|
-
options
|
|
254
|
-
);
|
|
255
|
-
if (forked.code !== 0) {
|
|
256
|
-
throw new ConfigError(
|
|
257
|
-
`'${this.cli.command} repo fork' did not succeed.\n ` +
|
|
258
|
-
`${forked.stderr.trim() || forked.stdout.trim()}`
|
|
259
|
-
);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
const who = await this.capturedOutput(
|
|
263
|
-
this.cli.command,
|
|
264
|
-
["api", "user", "--jq", ".login"],
|
|
265
|
-
options
|
|
266
|
-
);
|
|
267
|
-
if (who === undefined) {
|
|
268
|
-
throw new ConfigError(
|
|
269
|
-
"The fork was requested, but sous could not read your GitHub login from " +
|
|
270
|
-
`'${this.cli.command} api user', so it does not know where the fork lives.`
|
|
271
|
-
);
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
const owner = who.trim();
|
|
275
|
-
return {
|
|
276
|
-
owner,
|
|
277
|
-
name: repo.name,
|
|
278
|
-
httpsUrl: `https://${repo.host}/${owner}/${repo.name}.git`,
|
|
279
|
-
sshUrl: `git@${repo.host}:${owner}/${repo.name}.git`,
|
|
280
|
-
};
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
/**
|
|
284
|
-
* Opens a pull request for a branch that has already been pushed. A proposal
|
|
285
|
-
* carrying a head owner came from a fork, which is what a cross-repository
|
|
286
|
-
* pull request spells as `owner:branch`.
|
|
287
|
-
*
|
|
288
|
-
* @param repo - The canonicalized repository the proposal targets.
|
|
289
|
-
* @param proposal - The branch, the text and whether it is a draft.
|
|
290
|
-
* @param options - Subprocess runner and working directory overrides.
|
|
291
|
-
*/
|
|
292
|
-
async proposeChange(
|
|
293
|
-
repo: CanonicalRepo,
|
|
294
|
-
proposal: ChangeProposal,
|
|
295
|
-
options: ProviderOptions = {}
|
|
296
|
-
): Promise<ProposedChange> {
|
|
297
|
-
const head =
|
|
298
|
-
proposal.head === undefined
|
|
299
|
-
? proposal.branch
|
|
300
|
-
: `${proposal.head.owner}:${proposal.branch}`;
|
|
301
|
-
|
|
302
|
-
const args = ["pr", "create", "--repo", `${repo.owner}/${repo.name}`];
|
|
303
|
-
if (proposal.base !== undefined) args.push("--base", proposal.base);
|
|
304
|
-
args.push("--head", head, "--title", proposal.title, "--body", proposal.body);
|
|
305
|
-
if (proposal.draft) args.push("--draft");
|
|
306
|
-
|
|
307
|
-
const result = await this.runCommand(this.cli.command, args, options);
|
|
308
|
-
if (result.code !== 0) {
|
|
309
|
-
const reported = result.stderr.trim() || result.stdout.trim();
|
|
310
|
-
throw new ConfigError(
|
|
311
|
-
`'${this.cli.command} pr create' did not succeed, so no proposal was opened.` +
|
|
312
|
-
(reported.length === 0 ? "" : `\n ${reported}`)
|
|
313
|
-
);
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
const url = firstUrlIn(result.stdout);
|
|
317
|
-
if (url === undefined) {
|
|
318
|
-
return {
|
|
319
|
-
detail:
|
|
320
|
-
`The ${this.proposalNoun} was opened, but '${this.cli.command}' printed no address ` +
|
|
321
|
-
`for it.`,
|
|
322
|
-
};
|
|
323
|
-
}
|
|
324
|
-
return { url, detail: `The ${this.proposalNoun} is at ${url}.` };
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
// --- Proposals after the fact ------------------------------------------------
|
|
328
|
-
|
|
329
|
-
/**
|
|
330
|
-
* The pull request a branch was pushed for. GitHub lists pull requests by
|
|
331
|
-
* head branch name alone, so the list is narrowed here by where the branch
|
|
332
|
-
* lives: the repository itself, or the contributor's fork. When a branch has
|
|
333
|
-
* had several, the open one wins, and otherwise the newest.
|
|
334
|
-
*
|
|
335
|
-
* @param repo - The canonicalized repository the proposal targets.
|
|
336
|
-
* @param query - The branch, and whether it lives on a fork.
|
|
337
|
-
* @param options - Subprocess runner and working directory overrides.
|
|
338
|
-
*/
|
|
339
|
-
async findProposal(
|
|
340
|
-
repo: CanonicalRepo,
|
|
341
|
-
query: ProposalQuery,
|
|
342
|
-
options: ProviderOptions = {}
|
|
343
|
-
): Promise<ProposalSummary | undefined> {
|
|
344
|
-
const forkOwner = query.fromFork
|
|
345
|
-
? (query.forkOwner ?? (await this.signedInLogin(options)))
|
|
346
|
-
: undefined;
|
|
347
|
-
|
|
348
|
-
const listed = await this.ghJson<GhPullRequest[]>(
|
|
349
|
-
[
|
|
350
|
-
"pr",
|
|
351
|
-
"list",
|
|
352
|
-
"--repo",
|
|
353
|
-
`${repo.owner}/${repo.name}`,
|
|
354
|
-
"--head",
|
|
355
|
-
query.branch,
|
|
356
|
-
"--state",
|
|
357
|
-
"all",
|
|
358
|
-
"--limit",
|
|
359
|
-
"50",
|
|
360
|
-
"--json",
|
|
361
|
-
"number,url,state,title,isDraft,baseRefName,headRepositoryOwner,isCrossRepository",
|
|
362
|
-
],
|
|
363
|
-
"pr list",
|
|
364
|
-
options
|
|
365
|
-
);
|
|
366
|
-
|
|
367
|
-
const mine = listed.filter((entry) =>
|
|
368
|
-
query.fromFork
|
|
369
|
-
? entry.isCrossRepository === true && entry.headRepositoryOwner?.login === forkOwner
|
|
370
|
-
: entry.isCrossRepository !== true
|
|
371
|
-
);
|
|
372
|
-
if (mine.length === 0) return undefined;
|
|
373
|
-
|
|
374
|
-
const open = mine.find((entry) => entry.state === "OPEN");
|
|
375
|
-
const chosen = open ?? [...mine].sort((a, b) => b.number - a.number)[0]!;
|
|
376
|
-
return summarizePullRequest(chosen);
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
/**
|
|
380
|
-
* Where one pull request stands: its state, its review decision, and how
|
|
381
|
-
* its checks are going, counted.
|
|
382
|
-
*
|
|
383
|
-
* @param repo - The canonicalized repository the proposal targets.
|
|
384
|
-
* @param id - The pull request number.
|
|
385
|
-
* @param options - Subprocess runner and working directory overrides.
|
|
386
|
-
*/
|
|
387
|
-
async proposalStatus(
|
|
388
|
-
repo: CanonicalRepo,
|
|
389
|
-
id: string,
|
|
390
|
-
options: ProviderOptions = {}
|
|
391
|
-
): Promise<ProposalStatus> {
|
|
392
|
-
const viewed = await this.ghJson<GhPullRequest>(
|
|
393
|
-
[
|
|
394
|
-
"pr",
|
|
395
|
-
"view",
|
|
396
|
-
id,
|
|
397
|
-
"--repo",
|
|
398
|
-
`${repo.owner}/${repo.name}`,
|
|
399
|
-
"--json",
|
|
400
|
-
"number,url,state,title,isDraft,baseRefName,reviewDecision,statusCheckRollup,mergeable",
|
|
401
|
-
],
|
|
402
|
-
"pr view",
|
|
403
|
-
options
|
|
404
|
-
);
|
|
405
|
-
|
|
406
|
-
const review = reviewFrom(viewed.reviewDecision);
|
|
407
|
-
const checks = checksFrom(viewed.statusCheckRollup);
|
|
408
|
-
const mergeable =
|
|
409
|
-
viewed.mergeable === "MERGEABLE"
|
|
410
|
-
? true
|
|
411
|
-
: viewed.mergeable === "CONFLICTING"
|
|
412
|
-
? false
|
|
413
|
-
: undefined;
|
|
414
|
-
|
|
415
|
-
return {
|
|
416
|
-
proposal: summarizePullRequest(viewed),
|
|
417
|
-
...(review === undefined ? {} : { review }),
|
|
418
|
-
...(checks === undefined ? {} : { checks }),
|
|
419
|
-
...(mergeable === undefined ? {} : { mergeable }),
|
|
420
|
-
};
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
/**
|
|
424
|
-
* Replaces a pull request's title, its body, or both.
|
|
425
|
-
*
|
|
426
|
-
* @param repo - The canonicalized repository the proposal targets.
|
|
427
|
-
* @param id - The pull request number.
|
|
428
|
-
* @param update - What to replace.
|
|
429
|
-
* @param options - Subprocess runner and working directory overrides.
|
|
430
|
-
*/
|
|
431
|
-
async updateProposal(
|
|
432
|
-
repo: CanonicalRepo,
|
|
433
|
-
id: string,
|
|
434
|
-
update: ProposalUpdate,
|
|
435
|
-
options: ProviderOptions = {}
|
|
436
|
-
): Promise<ProposedChange> {
|
|
437
|
-
const args = ["pr", "edit", id, "--repo", `${repo.owner}/${repo.name}`];
|
|
438
|
-
if (update.title !== undefined) args.push("--title", update.title);
|
|
439
|
-
if (update.body !== undefined) args.push("--body", update.body);
|
|
440
|
-
|
|
441
|
-
const result = await this.runCommand(this.cli.command, args, options);
|
|
442
|
-
if (result.code !== 0) {
|
|
443
|
-
const reported = result.stderr.trim() || result.stdout.trim();
|
|
444
|
-
throw new ConfigError(
|
|
445
|
-
`'${this.cli.command} pr edit' did not succeed, so the ${this.proposalNoun} kept its ` +
|
|
446
|
-
`title and body.` +
|
|
447
|
-
(reported.length === 0 ? "" : `\n ${reported}`)
|
|
448
|
-
);
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
const url = firstUrlIn(result.stdout);
|
|
452
|
-
return url === undefined
|
|
453
|
-
? { detail: `The ${this.proposalNoun} was updated.` }
|
|
454
|
-
: { url, detail: `The ${this.proposalNoun} at ${url} was updated.` };
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
/**
|
|
458
|
-
* The login of the account `gh` is signed in as, which is the owner of any
|
|
459
|
-
* fork sous made for the contributor.
|
|
460
|
-
*
|
|
461
|
-
* @param options - Subprocess runner and working directory overrides.
|
|
462
|
-
*/
|
|
463
|
-
private async signedInLogin(options: ProviderOptions): Promise<string> {
|
|
464
|
-
const who = await this.capturedOutput(
|
|
465
|
-
this.cli.command,
|
|
466
|
-
["api", "user", "--jq", ".login"],
|
|
467
|
-
options
|
|
468
|
-
);
|
|
469
|
-
if (who === undefined || who.trim().length === 0) {
|
|
470
|
-
throw new ConfigError(
|
|
471
|
-
"Sous could not read your GitHub login from " +
|
|
472
|
-
`'${this.cli.command} api user', so it cannot tell which fork a ${this.proposalNoun} ` +
|
|
473
|
-
"would come from."
|
|
474
|
-
);
|
|
475
|
-
}
|
|
476
|
-
return who.trim();
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
/**
|
|
480
|
-
* Runs a `gh` command that prints JSON, and parses what it printed.
|
|
481
|
-
*
|
|
482
|
-
* @param args - The arguments, ending with the `--json` field list.
|
|
483
|
-
* @param what - The subcommand, as it is named in a failure.
|
|
484
|
-
* @param options - Subprocess runner and working directory overrides.
|
|
485
|
-
*/
|
|
486
|
-
private async ghJson<T>(args: string[], what: string, options: ProviderOptions): Promise<T> {
|
|
487
|
-
const result = await this.runCommand(this.cli.command, args, options);
|
|
488
|
-
if (result.code !== 0) {
|
|
489
|
-
const reported = result.stderr.trim() || result.stdout.trim();
|
|
490
|
-
throw new ConfigError(
|
|
491
|
-
`'${this.cli.command} ${what}' did not succeed.` +
|
|
492
|
-
(reported.length === 0 ? "" : `\n ${reported}`)
|
|
493
|
-
);
|
|
494
|
-
}
|
|
495
|
-
try {
|
|
496
|
-
return JSON.parse(result.stdout) as T;
|
|
497
|
-
} catch {
|
|
498
|
-
throw new ConfigError(
|
|
499
|
-
`'${this.cli.command} ${what}' printed something that is not JSON, so sous cannot read ` +
|
|
500
|
-
`the ${this.proposalNoun} it describes.`
|
|
501
|
-
);
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
// --- What gh prints, and how it maps onto plain data ---------------------------------------------
|
|
507
|
-
|
|
508
|
-
/** The fields sous asks `gh` for, as it prints them. */
|
|
509
|
-
type GhPullRequest = {
|
|
510
|
-
number: number;
|
|
511
|
-
url?: string;
|
|
512
|
-
state?: string;
|
|
513
|
-
title?: string;
|
|
514
|
-
isDraft?: boolean;
|
|
515
|
-
baseRefName?: string;
|
|
516
|
-
headRepositoryOwner?: { login?: string } | null;
|
|
517
|
-
isCrossRepository?: boolean;
|
|
518
|
-
reviewDecision?: string | null;
|
|
519
|
-
statusCheckRollup?: GhCheck[] | null;
|
|
520
|
-
mergeable?: string;
|
|
521
|
-
};
|
|
522
|
-
|
|
523
|
-
/** One entry of a pull request's status check rollup: a check run or a commit status. */
|
|
524
|
-
type GhCheck = {
|
|
525
|
-
__typename?: string;
|
|
526
|
-
status?: string;
|
|
527
|
-
conclusion?: string | null;
|
|
528
|
-
state?: string;
|
|
529
|
-
};
|
|
530
|
-
|
|
531
|
-
/** A pull request's state, in the words every provider shares. */
|
|
532
|
-
function stateFrom(state: string | undefined): ProposalState {
|
|
533
|
-
if (state === "MERGED") return "merged";
|
|
534
|
-
if (state === "CLOSED") return "closed";
|
|
535
|
-
return "open";
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
/**
|
|
539
|
-
* The plain summary of a pull request.
|
|
540
|
-
*
|
|
541
|
-
* @param entry - What `gh` printed for it.
|
|
542
|
-
*/
|
|
543
|
-
function summarizePullRequest(entry: GhPullRequest): ProposalSummary {
|
|
544
|
-
return {
|
|
545
|
-
id: String(entry.number),
|
|
546
|
-
...(entry.url === undefined ? {} : { url: entry.url }),
|
|
547
|
-
state: stateFrom(entry.state),
|
|
548
|
-
title: entry.title ?? "",
|
|
549
|
-
draft: entry.isDraft === true,
|
|
550
|
-
...(entry.baseRefName === undefined ? {} : { base: entry.baseRefName }),
|
|
551
|
-
};
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
/** GitHub's review decision, in the words every provider shares. */
|
|
555
|
-
function reviewFrom(decision: string | null | undefined): ProposalReview | undefined {
|
|
556
|
-
if (decision === "APPROVED") return "approved";
|
|
557
|
-
if (decision === "CHANGES_REQUESTED") return "changes requested";
|
|
558
|
-
if (decision === "REVIEW_REQUIRED") return "review required";
|
|
559
|
-
return undefined;
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
/** Check runs whose conclusion counts as passing. */
|
|
563
|
-
const PASSING_CONCLUSIONS = new Set(["SUCCESS", "NEUTRAL", "SKIPPED"]);
|
|
564
|
-
|
|
565
|
-
/**
|
|
566
|
-
* Counts a status check rollup into passed, failed and pending. A check run
|
|
567
|
-
* that has not completed is pending; a commit status reports its state directly.
|
|
568
|
-
*
|
|
569
|
-
* @param rollup - What `gh` printed as the rollup, when it printed one.
|
|
570
|
-
*/
|
|
571
|
-
function checksFrom(rollup: GhCheck[] | null | undefined): ProposalChecks | undefined {
|
|
572
|
-
if (rollup === null || rollup === undefined || rollup.length === 0) return undefined;
|
|
573
|
-
const counts: ProposalChecks = { passed: 0, failed: 0, pending: 0 };
|
|
574
|
-
for (const check of rollup) {
|
|
575
|
-
const isStatus =
|
|
576
|
-
check.__typename === "StatusContext" ||
|
|
577
|
-
(check.status === undefined && check.state !== undefined);
|
|
578
|
-
if (isStatus) {
|
|
579
|
-
if (check.state === "SUCCESS") counts.passed += 1;
|
|
580
|
-
else if (check.state === "PENDING" || check.state === "EXPECTED") counts.pending += 1;
|
|
581
|
-
else counts.failed += 1;
|
|
582
|
-
continue;
|
|
583
|
-
}
|
|
584
|
-
if (check.status !== "COMPLETED") counts.pending += 1;
|
|
585
|
-
else if (PASSING_CONCLUSIONS.has(check.conclusion ?? "")) counts.passed += 1;
|
|
586
|
-
else counts.failed += 1;
|
|
587
|
-
}
|
|
588
|
-
return counts;
|
|
589
|
-
}
|