package-registry-manager 0.21.0
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/CHANGELOG.md +60 -0
- package/LICENSE +24 -0
- package/README.md +21 -0
- package/package.json +53 -0
- package/src/approvals.mjs +72 -0
- package/src/auth-urls.mjs +149 -0
- package/src/automation.mjs +124 -0
- package/src/browser-options.mjs +175 -0
- package/src/browser.mjs +123 -0
- package/src/cli.mjs +431 -0
- package/src/containers.mjs +66 -0
- package/src/default-browser.mjs +149 -0
- package/src/discovery.mjs +461 -0
- package/src/flows.mjs +722 -0
- package/src/index.mjs +6 -0
- package/src/model.mjs +50 -0
- package/src/pages.mjs +91 -0
- package/src/plan.mjs +243 -0
- package/src/prerequisites.mjs +323 -0
- package/src/profile.mjs +104 -0
- package/src/publishers.mjs +495 -0
- package/src/registry-state.mjs +158 -0
- package/src/setup.mjs +820 -0
- package/src/skips.mjs +117 -0
- package/src/tokens.mjs +215 -0
- package/src/workflows.mjs +46 -0
package/src/index.mjs
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { npmPrefillScript, openInUserBrowser } from "./browser.mjs";
|
|
2
|
+
export { inspectRepository } from "./discovery.mjs";
|
|
3
|
+
export { REGISTRIES, parseRegistry } from "./model.mjs";
|
|
4
|
+
export { buildPlans, packageDirectory } from "./plan.mjs";
|
|
5
|
+
export { ensureProfileIgnored } from "./profile.mjs";
|
|
6
|
+
export { defaultBrowserProfile, executePlan } from "./setup.mjs";
|
package/src/model.mjs
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export const REGISTRIES = Object.freeze([
|
|
2
|
+
"npm",
|
|
3
|
+
"crates-io",
|
|
4
|
+
"pypi",
|
|
5
|
+
"go-modules",
|
|
6
|
+
"nuget",
|
|
7
|
+
"maven-central",
|
|
8
|
+
"packagist",
|
|
9
|
+
"docker-hub",
|
|
10
|
+
"ghcr",
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
const REGISTRY_ALIASES = new Map([
|
|
14
|
+
["npm", "npm"],
|
|
15
|
+
["cargo", "crates-io"],
|
|
16
|
+
["crate", "crates-io"],
|
|
17
|
+
["crates", "crates-io"],
|
|
18
|
+
["crates-io", "crates-io"],
|
|
19
|
+
["python", "pypi"],
|
|
20
|
+
["pypi", "pypi"],
|
|
21
|
+
["go", "go-modules"],
|
|
22
|
+
["go-module", "go-modules"],
|
|
23
|
+
["go-modules", "go-modules"],
|
|
24
|
+
["dotnet", "nuget"],
|
|
25
|
+
["nuget", "nuget"],
|
|
26
|
+
["java", "maven-central"],
|
|
27
|
+
["maven", "maven-central"],
|
|
28
|
+
["maven-central", "maven-central"],
|
|
29
|
+
["composer", "packagist"],
|
|
30
|
+
["php", "packagist"],
|
|
31
|
+
["packagist", "packagist"],
|
|
32
|
+
["docker", "docker-hub"],
|
|
33
|
+
["dockerhub", "docker-hub"],
|
|
34
|
+
["docker-hub", "docker-hub"],
|
|
35
|
+
["docker-io", "docker-hub"],
|
|
36
|
+
["ghcr", "ghcr"],
|
|
37
|
+
["ghcr-io", "ghcr"],
|
|
38
|
+
["github-container-registry", "ghcr"],
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
export function parseRegistry(value) {
|
|
42
|
+
const normalized = value.toLowerCase().replaceAll(/[._]/g, "-");
|
|
43
|
+
const registry = REGISTRY_ALIASES.get(normalized);
|
|
44
|
+
if (!registry) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
`unsupported registry '${value}'; expected ${REGISTRIES.join(", ")}`,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
return registry;
|
|
50
|
+
}
|
package/src/pages.mjs
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// GitHub Pages readiness. A workflow that deploys with actions/deploy-pages
|
|
2
|
+
// fails with "Get Pages site failed ... Not Found" until Pages is enabled with
|
|
3
|
+
// GitHub Actions as its source, which only a repository administrator can do.
|
|
4
|
+
|
|
5
|
+
const PAGES_ACTION =
|
|
6
|
+
/^\s*(?:-\s*)?uses\s*:\s*['"]?actions\/(?:configure-pages|deploy-pages|upload-pages-artifact)@/m;
|
|
7
|
+
|
|
8
|
+
/** Where Pages is configured by hand: Settings -> Pages -> Source. */
|
|
9
|
+
export function pagesSettingsUrl(slug) {
|
|
10
|
+
return `https://github.com/${slug}/settings/pages`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Returns the name of the first workflow that deploys to GitHub Pages. */
|
|
14
|
+
export function pagesWorkflow(workflows) {
|
|
15
|
+
return (
|
|
16
|
+
workflows.find((workflow) => PAGES_ACTION.test(workflow.contents))?.name ??
|
|
17
|
+
null
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Steps that check the repository's Pages site and, after a confirmation
|
|
23
|
+
* each, enable it or switch it to GitHub Actions as its source.
|
|
24
|
+
*/
|
|
25
|
+
export function pagesSteps(slug, workflow) {
|
|
26
|
+
const api = (args) => ({ program: "gh", args: ["api", ...args] });
|
|
27
|
+
return [
|
|
28
|
+
{
|
|
29
|
+
id: "check-pages",
|
|
30
|
+
title: "Check that GitHub Pages deploys from GitHub Actions",
|
|
31
|
+
kind: "check",
|
|
32
|
+
description: `${workflow} deploys to GitHub Pages, which must be enabled with GitHub Actions as its source.`,
|
|
33
|
+
command: api([`repos/${slug}/pages`]),
|
|
34
|
+
cwd: ".",
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: "enable-pages",
|
|
38
|
+
title: "Enable GitHub Pages with GitHub Actions as its source",
|
|
39
|
+
kind: "command",
|
|
40
|
+
description:
|
|
41
|
+
"Create the Pages site so the workflow's deployment no longer fails with Not Found. This needs repository administrator rights.",
|
|
42
|
+
command: api([
|
|
43
|
+
"-X",
|
|
44
|
+
"POST",
|
|
45
|
+
`repos/${slug}/pages`,
|
|
46
|
+
"-f",
|
|
47
|
+
"build_type=workflow",
|
|
48
|
+
]),
|
|
49
|
+
when: "pages-missing",
|
|
50
|
+
cwd: ".",
|
|
51
|
+
confirm: true,
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: "use-pages-workflow",
|
|
55
|
+
title: "Switch GitHub Pages to GitHub Actions as its source",
|
|
56
|
+
kind: "command",
|
|
57
|
+
description:
|
|
58
|
+
"Pages builds from a branch, so the workflow's deployment is not served. This needs repository administrator rights.",
|
|
59
|
+
command: api([
|
|
60
|
+
"-X",
|
|
61
|
+
"PUT",
|
|
62
|
+
`repos/${slug}/pages`,
|
|
63
|
+
"-f",
|
|
64
|
+
"build_type=workflow",
|
|
65
|
+
]),
|
|
66
|
+
when: "pages-legacy",
|
|
67
|
+
cwd: ".",
|
|
68
|
+
confirm: true,
|
|
69
|
+
},
|
|
70
|
+
];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Reads `gh api repos/{slug}/pages`: `workflow` when Pages deploys from
|
|
75
|
+
* GitHub Actions, `legacy` when it builds from a branch, `missing` when the
|
|
76
|
+
* API answers 404, and `undefined` when the answer is unknown.
|
|
77
|
+
*/
|
|
78
|
+
export function pagesState(result) {
|
|
79
|
+
if (result.code === 0) {
|
|
80
|
+
try {
|
|
81
|
+
const site = JSON.parse(String(result.stdout ?? ""));
|
|
82
|
+
return site.build_type === "workflow" ? "workflow" : "legacy";
|
|
83
|
+
} catch {
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const output = `${result.stdout ?? ""}\n${result.stderr ?? ""}`;
|
|
88
|
+
return /\bHTTP 404\b|"message"\s*:\s*"Not Found"/.test(output)
|
|
89
|
+
? "missing"
|
|
90
|
+
: undefined;
|
|
91
|
+
}
|
package/src/plan.mjs
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BOOTSTRAP_CONDITIONS,
|
|
3
|
+
cratesFlow,
|
|
4
|
+
dockerHubFlow,
|
|
5
|
+
ghcrFlow,
|
|
6
|
+
npmFlow,
|
|
7
|
+
pypiFlow,
|
|
8
|
+
} from "./flows.mjs";
|
|
9
|
+
import { pagesSteps } from "./pages.mjs";
|
|
10
|
+
import { planPrerequisites } from "./prerequisites.mjs";
|
|
11
|
+
import { TRUSTED_REGISTRIES } from "./publishers.mjs";
|
|
12
|
+
|
|
13
|
+
const FLOWS = new Map([
|
|
14
|
+
["npm", npmFlow],
|
|
15
|
+
["crates-io", cratesFlow],
|
|
16
|
+
["pypi", pypiFlow],
|
|
17
|
+
["docker-hub", dockerHubFlow],
|
|
18
|
+
["ghcr", ghcrFlow],
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Builds one setup plan per package. `options.verifyRelease` appends the
|
|
23
|
+
* release-verification steps to flows that support them. With
|
|
24
|
+
* `options.environment` (from `probeEnvironment`) npm trust runs through the
|
|
25
|
+
* npm that the local Node.js supports, and each plan lists its prerequisites;
|
|
26
|
+
* `options.browser` describes where browser pages open. `options.workflow`
|
|
27
|
+
* and `options.publisherEnvironment` override the detected trusted-publisher
|
|
28
|
+
* workflow file and GitHub environment.
|
|
29
|
+
*/
|
|
30
|
+
export function buildPlans(inspection, selected = [], options = {}) {
|
|
31
|
+
const registries = new Set(selected);
|
|
32
|
+
return inspection.packages
|
|
33
|
+
.filter((item) => registries.size === 0 || registries.has(item.registry))
|
|
34
|
+
.map((item) => buildPlan(inspection, item, options));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function buildPlan(inspection, packageInfo, options) {
|
|
38
|
+
if (!packageInfo.publishable) {
|
|
39
|
+
return {
|
|
40
|
+
...basePlan(inspection, packageInfo, []),
|
|
41
|
+
skipped_reason: skippedReason(packageInfo),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const command = (program, args) => ({ program, args });
|
|
45
|
+
const check = (id, title, description, commandSpec) => ({
|
|
46
|
+
id,
|
|
47
|
+
title,
|
|
48
|
+
kind: "check",
|
|
49
|
+
description,
|
|
50
|
+
command: commandSpec,
|
|
51
|
+
});
|
|
52
|
+
const browser = (id, title, description, url) => ({
|
|
53
|
+
id,
|
|
54
|
+
title,
|
|
55
|
+
kind: "browser",
|
|
56
|
+
description,
|
|
57
|
+
url,
|
|
58
|
+
});
|
|
59
|
+
let steps;
|
|
60
|
+
|
|
61
|
+
switch (packageInfo.registry) {
|
|
62
|
+
case "npm":
|
|
63
|
+
case "crates-io":
|
|
64
|
+
case "pypi":
|
|
65
|
+
case "docker-hub":
|
|
66
|
+
case "ghcr":
|
|
67
|
+
return flowPlan(inspection, packageInfo, options);
|
|
68
|
+
case "go-modules":
|
|
69
|
+
steps = [
|
|
70
|
+
check(
|
|
71
|
+
"test-module",
|
|
72
|
+
"Test the Go module",
|
|
73
|
+
"Run all module tests before tagging a semantic version.",
|
|
74
|
+
command("go", ["test", "./..."]),
|
|
75
|
+
),
|
|
76
|
+
{
|
|
77
|
+
id: "publish-tag",
|
|
78
|
+
title: "Push a semantic-version tag",
|
|
79
|
+
kind: "manual",
|
|
80
|
+
description:
|
|
81
|
+
"Go modules are published from repository tags; after pushing the tag, request it through proxy.golang.org.",
|
|
82
|
+
url: "https://go.dev/ref/mod#publishing-a-module",
|
|
83
|
+
},
|
|
84
|
+
];
|
|
85
|
+
break;
|
|
86
|
+
case "nuget":
|
|
87
|
+
steps = [
|
|
88
|
+
check(
|
|
89
|
+
"pack-package",
|
|
90
|
+
"Build the NuGet package",
|
|
91
|
+
"Create the package locally without pushing it.",
|
|
92
|
+
command("dotnet", ["pack", "--configuration", "Release"]),
|
|
93
|
+
),
|
|
94
|
+
browser(
|
|
95
|
+
"configure-trusted-publishing",
|
|
96
|
+
"Configure NuGet trusted publishing",
|
|
97
|
+
"Sign in and add a GitHub Actions federated credential for this package.",
|
|
98
|
+
"https://www.nuget.org/account/TrustedPublishing",
|
|
99
|
+
),
|
|
100
|
+
];
|
|
101
|
+
break;
|
|
102
|
+
case "maven-central":
|
|
103
|
+
steps = [
|
|
104
|
+
check(
|
|
105
|
+
"verify-build",
|
|
106
|
+
"Verify the Maven build",
|
|
107
|
+
"Run the build lifecycle without deploying an artifact.",
|
|
108
|
+
command("mvn", ["--batch-mode", "verify"]),
|
|
109
|
+
),
|
|
110
|
+
browser(
|
|
111
|
+
"verify-namespace",
|
|
112
|
+
"Verify a Central namespace",
|
|
113
|
+
"Sign in to the Central Portal and verify the namespace used by the package coordinates.",
|
|
114
|
+
"https://central.sonatype.com/publishing/namespaces",
|
|
115
|
+
),
|
|
116
|
+
];
|
|
117
|
+
break;
|
|
118
|
+
case "packagist":
|
|
119
|
+
steps = [
|
|
120
|
+
check(
|
|
121
|
+
"validate-package",
|
|
122
|
+
"Validate Composer metadata",
|
|
123
|
+
"Strictly validate composer.json before submitting it.",
|
|
124
|
+
command("composer", ["validate", "--strict"]),
|
|
125
|
+
),
|
|
126
|
+
browser(
|
|
127
|
+
"submit-repository",
|
|
128
|
+
"Submit the repository to Packagist",
|
|
129
|
+
"Sign in and submit the public VCS repository URL. Packagist reads package versions from tags.",
|
|
130
|
+
"https://packagist.org/packages/submit",
|
|
131
|
+
),
|
|
132
|
+
];
|
|
133
|
+
break;
|
|
134
|
+
default:
|
|
135
|
+
throw new Error(`no setup plan for registry ${packageInfo.registry}`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return basePlan(inspection, packageInfo, steps);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function basePlan(inspection, packageInfo, steps) {
|
|
142
|
+
return {
|
|
143
|
+
schema_version: 1,
|
|
144
|
+
registry: packageInfo.registry,
|
|
145
|
+
package: structuredClone(packageInfo),
|
|
146
|
+
repository: structuredClone(inspection.repository),
|
|
147
|
+
steps,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function flowPlan(inspection, packageInfo, options) {
|
|
152
|
+
const { github_owner, github_repository } = inspection.repository;
|
|
153
|
+
const slug =
|
|
154
|
+
github_owner && github_repository
|
|
155
|
+
? `${github_owner}/${github_repository}`
|
|
156
|
+
: null;
|
|
157
|
+
const trusted = TRUSTED_REGISTRIES.includes(packageInfo.registry);
|
|
158
|
+
const workflow =
|
|
159
|
+
(trusted ? options.workflow : null) ?? packageInfo.workflow ?? null;
|
|
160
|
+
const environment = trusted
|
|
161
|
+
? (options.publisherEnvironment ?? packageInfo.environment ?? null)
|
|
162
|
+
: null;
|
|
163
|
+
if (trusted && !workflow && packageInfo.workflow_candidates?.length > 1) {
|
|
164
|
+
return {
|
|
165
|
+
...basePlan(inspection, packageInfo, []),
|
|
166
|
+
skipped_reason: `several workflows publish to ${packageInfo.registry} (${packageInfo.workflow_candidates.join(", ")}); pass --workflow <file> to choose the trusted publisher`,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
const context = {
|
|
170
|
+
directory: packageDirectory(packageInfo.manifest),
|
|
171
|
+
slug,
|
|
172
|
+
workflow,
|
|
173
|
+
environment,
|
|
174
|
+
verifyRelease: Boolean(options.verifyRelease),
|
|
175
|
+
trustNpm: options.environment?.trustNpm,
|
|
176
|
+
};
|
|
177
|
+
const mode = planMode(packageInfo);
|
|
178
|
+
let steps = FLOWS.get(packageInfo.registry)(packageInfo, context);
|
|
179
|
+
if (mode === "complete") {
|
|
180
|
+
steps = [];
|
|
181
|
+
} else if (mode === "attach") {
|
|
182
|
+
steps = steps.filter((item) => !BOOTSTRAP_CONDITIONS.has(item.when));
|
|
183
|
+
}
|
|
184
|
+
// Pages readiness belongs to the repository, so it is checked in every mode.
|
|
185
|
+
const pages = inspection.repository.pages_workflow;
|
|
186
|
+
if (slug && pages) {
|
|
187
|
+
steps = steps.concat(pagesSteps(slug, pages));
|
|
188
|
+
}
|
|
189
|
+
const plan = basePlan(inspection, packageInfo, steps);
|
|
190
|
+
if (mode) {
|
|
191
|
+
plan.mode = mode;
|
|
192
|
+
}
|
|
193
|
+
const prerequisites = planPrerequisites(
|
|
194
|
+
plan,
|
|
195
|
+
options.environment,
|
|
196
|
+
options.browser,
|
|
197
|
+
);
|
|
198
|
+
if (prerequisites.length > 0) {
|
|
199
|
+
plan.prerequisites = prerequisites;
|
|
200
|
+
}
|
|
201
|
+
if (slug && workflow && trusted) {
|
|
202
|
+
plan.trusted_publisher = {
|
|
203
|
+
provider: "github-actions",
|
|
204
|
+
organization: github_owner,
|
|
205
|
+
repository: github_repository,
|
|
206
|
+
workflow,
|
|
207
|
+
};
|
|
208
|
+
if (environment) {
|
|
209
|
+
plan.trusted_publisher.environment = environment;
|
|
210
|
+
}
|
|
211
|
+
if (packageInfo.registry === "pypi") {
|
|
212
|
+
plan.trusted_publisher.project = packageInfo.name;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return plan;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Chooses `bootstrap` for a package missing from its registry, `attach` for
|
|
220
|
+
* one without trusted publishing, and `complete` when nothing is left to do.
|
|
221
|
+
* Unknown registry state leaves the mode unset.
|
|
222
|
+
*/
|
|
223
|
+
export function planMode(packageInfo) {
|
|
224
|
+
if (packageInfo.exists_on_registry === false) {
|
|
225
|
+
return "bootstrap";
|
|
226
|
+
}
|
|
227
|
+
if (packageInfo.exists_on_registry !== true) {
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
return packageInfo.trusted_publishing === true ? "complete" : "attach";
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function skippedReason(packageInfo) {
|
|
234
|
+
const problems = packageInfo.problems ?? [];
|
|
235
|
+
return problems.length > 0
|
|
236
|
+
? problems.join("; ")
|
|
237
|
+
: "the manifest marks this package as not publishable";
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function packageDirectory(manifest) {
|
|
241
|
+
const separator = manifest.lastIndexOf("/");
|
|
242
|
+
return separator === -1 ? "." : manifest.slice(0, separator);
|
|
243
|
+
}
|