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/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
+ }