package-registry-manager 0.22.4 → 0.22.6

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.
@@ -0,0 +1,210 @@
1
+ import { spawn } from "node:child_process";
2
+ import { readFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { verifyCredentialWorkflow } from "./credential-verification.mjs";
5
+ import { needsRotation, rotateCredential } from "./credential-cycle.mjs";
6
+ import {
7
+ TOKEN_PROVIDERS,
8
+ tokenFormScript,
9
+ READ_TOKEN,
10
+ revokedScript,
11
+ } from "./credential-browser.mjs";
12
+
13
+ /** Exact argv and stdin transport. Suppress all output from secret mutations. */
14
+ export function secretCommand(args, input, cwd) {
15
+ return new Promise((resolve, reject) => {
16
+ const child = spawn("gh-manager", args, {
17
+ cwd,
18
+ stdio: ["pipe", "pipe", "pipe"],
19
+ });
20
+ let output = "";
21
+ child.stdout.on("data", (chunk) => {
22
+ if (input === undefined) {
23
+ output += chunk;
24
+ }
25
+ });
26
+ child.stderr.on("data", () => {});
27
+ child.on("error", () =>
28
+ reject(
29
+ new Error(
30
+ "gh-manager with secret get-metadata/ensure support is required (gh-manager#6)",
31
+ ),
32
+ ),
33
+ );
34
+ child.stdin.on("error", () => {});
35
+ child.on("close", (code) =>
36
+ code === 0
37
+ ? resolve(output)
38
+ : reject(
39
+ new Error(
40
+ "gh-manager secret operation failed; verify installation, permissions and secret support (gh-manager#6)",
41
+ ),
42
+ ),
43
+ );
44
+ child.stdin.end(input ?? "");
45
+ });
46
+ }
47
+
48
+ /** Execute the token lifecycle without putting values in argv, diagnostics or files. */
49
+ export async function setupCredential(session) {
50
+ const { plan, options } = session;
51
+ const provider = TOKEN_PROVIDERS[plan.registry];
52
+ if (!provider) {
53
+ throw new Error(`no token provider for ${plan.registry}`);
54
+ }
55
+ const contents = await readFile(
56
+ path.join(options.repository, ".package-registry-manager.json"),
57
+ "utf8",
58
+ ).catch((error) => {
59
+ if (error.code === "ENOENT") {
60
+ return "{}";
61
+ }
62
+ throw error;
63
+ });
64
+ const settings = JSON.parse(contents).tokens?.[plan.registry] ?? {};
65
+ if (!/^[\w.-]+\.ya?ml$/.test(settings.verification_workflow ?? "")) {
66
+ throw new Error(
67
+ "configure tokens.<registry>.verification_workflow for a reviewed dry-run login workflow before creating credentials",
68
+ );
69
+ }
70
+ const { github_owner: owner, github_repository: repository } =
71
+ plan.repository;
72
+ if (!owner || !repository) {
73
+ throw new Error("token setup requires GitHub repository coordinates");
74
+ }
75
+ const secret =
76
+ settings.secret ?? plan.package.token_secrets?.[0] ?? provider.secret;
77
+ if (!/^[A-Z][A-Z0-9_]*$/.test(secret)) {
78
+ throw new Error("invalid registry secret name");
79
+ }
80
+ if (settings.level && !["repo", "org"].includes(settings.level)) {
81
+ throw new Error("token secret level must be repo or org");
82
+ }
83
+ const scope =
84
+ settings.level === "repo"
85
+ ? ["--repo", `${owner}/${repository}`]
86
+ : ["--org", owner];
87
+ const visibility =
88
+ settings.level === "repo"
89
+ ? []
90
+ : ["--visibility", "selected", "--repos", `${owner}/${repository}`];
91
+ const workflow = await readFile(
92
+ path.join(
93
+ options.repository,
94
+ ".github/workflows",
95
+ settings.verification_workflow,
96
+ ),
97
+ "utf8",
98
+ );
99
+ if (
100
+ !workflow.includes("workflow_dispatch") ||
101
+ !workflow.includes("inputs.prm_nonce") ||
102
+ !workflow.includes("run-name:")
103
+ ) {
104
+ throw new Error(
105
+ "verification workflow requires workflow_dispatch input prm_nonce and run-name containing inputs.prm_nonce",
106
+ );
107
+ }
108
+ const days = settings.expiry_days ?? 30;
109
+ if (!Number.isInteger(days) || days < 1 || days > 90) {
110
+ throw new Error("token expiry_days must be between 1 and 90");
111
+ }
112
+ if (options.noBrowser) {
113
+ throw new Error(
114
+ "registry token creation requires the dedicated automated browser",
115
+ );
116
+ }
117
+ if (
118
+ options.browserOptions?.attach ||
119
+ options.browserOptions?.importScope === "full"
120
+ ) {
121
+ throw new Error(
122
+ "token setup requires a dedicated profile with domain-scoped sign-in import",
123
+ );
124
+ }
125
+ const run = options.secretCommand ?? secretCommand;
126
+ const previous = JSON.parse(
127
+ await run(
128
+ ["secret", "get-metadata", secret, ...scope, "--json"],
129
+ undefined,
130
+ options.repository,
131
+ ),
132
+ );
133
+ const verify = () =>
134
+ verifyCredentialWorkflow(session, settings.verification_workflow);
135
+ if (!needsRotation(previous)) {
136
+ try {
137
+ await verify();
138
+ return;
139
+ } catch {
140
+ previous.valid = false;
141
+ }
142
+ }
143
+ if (
144
+ !options.yes &&
145
+ !/^y(?:es)?$/i.test(
146
+ await session.prompt(
147
+ "Create and store a scoped credential, verify it, then revoke the replaced token? [y/N] ",
148
+ ),
149
+ )
150
+ ) {
151
+ throw new Error("credential creation declined");
152
+ }
153
+ if (session.automation) {
154
+ await session.automation.close();
155
+ }
156
+ session.automation = null;
157
+ const page = await session.automatedPage(
158
+ { ...options.browserOptions, importScope: "domains" },
159
+ true,
160
+ );
161
+ const expiry = new Date(Date.now() + days * 86_400_000).toISOString();
162
+ await rotateCredential(previous, {
163
+ create: async () => {
164
+ await page.goto(provider.url);
165
+ await session.prompt(
166
+ `Sign in, then press Enter to fill the narrow publishing scope (${provider.scope})...`,
167
+ );
168
+ await page.evaluate(
169
+ tokenFormScript(provider, `prm-${repository}`, expiry),
170
+ );
171
+ await session.prompt(
172
+ `Review the scope and expiry, create the token in this browser, then press Enter (never paste its value)...`,
173
+ );
174
+ const credential = await page.evaluate(READ_TOKEN);
175
+ if (!credential.expires_at) {
176
+ throw new Error(
177
+ "registry did not provide a verifiable expiry; no secret was stored",
178
+ );
179
+ }
180
+ return credential;
181
+ },
182
+ store: (credential) =>
183
+ run(
184
+ [
185
+ "secret",
186
+ "ensure",
187
+ secret,
188
+ ...scope,
189
+ ...visibility,
190
+ "--expires-at",
191
+ credential.expires_at,
192
+ "--token-id",
193
+ credential.id,
194
+ "--registry",
195
+ plan.registry,
196
+ ],
197
+ credential.value,
198
+ options.repository,
199
+ ),
200
+ verify,
201
+ revoke: async (id) => {
202
+ await page.goto(provider.url.replace(/\/create$/, ""));
203
+ await session.prompt(
204
+ `Revoke registry token ${id} in this browser, then press Enter...`,
205
+ );
206
+ },
207
+ revoked: async (id) => page.evaluate(revokedScript(id)),
208
+ });
209
+ console.log(" Credential verified; replaced token revocation verified.");
210
+ }
@@ -0,0 +1,96 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { setTimeout as delay } from "node:timers/promises";
3
+
4
+ /** Correlate dispatch by a unique run-name input, never by the latest unrelated run. */
5
+ export async function verifyCredentialWorkflow(session, workflow) {
6
+ const { github_owner: owner, github_repository: repository } =
7
+ session.plan.repository;
8
+ const repo = `${owner}/${repository}`;
9
+ const capture = async (args) => {
10
+ const result = await session.capture({
11
+ id: "verify-registry-login",
12
+ command: { program: "gh", args },
13
+ cwd: ".",
14
+ });
15
+ if (result.code !== 0) {
16
+ throw new Error(
17
+ "credential verification command failed; old token remains active",
18
+ );
19
+ }
20
+ return String(result.stdout);
21
+ };
22
+ const metadata = JSON.parse(
23
+ await capture(["repo", "view", repo, "--json", "defaultBranchRef"]),
24
+ );
25
+ const ref = metadata.defaultBranchRef?.name;
26
+ if (!ref) {
27
+ throw new Error("cannot determine verification workflow branch");
28
+ }
29
+ const nonce = session.options.verificationNonce ?? randomUUID();
30
+ await capture([
31
+ "workflow",
32
+ "run",
33
+ workflow,
34
+ "--repo",
35
+ repo,
36
+ "--ref",
37
+ ref,
38
+ "-f",
39
+ `prm_nonce=${nonce}`,
40
+ ]);
41
+ const deadline =
42
+ Date.now() + (session.options.verificationTimeoutMs ?? 300_000);
43
+ let runId;
44
+ while (Date.now() < deadline) {
45
+ if (!runId) {
46
+ const runs = JSON.parse(
47
+ await capture([
48
+ "run",
49
+ "list",
50
+ "--repo",
51
+ repo,
52
+ "--workflow",
53
+ workflow,
54
+ "--event",
55
+ "workflow_dispatch",
56
+ "--limit",
57
+ "50",
58
+ "--json",
59
+ "databaseId,displayTitle",
60
+ ]),
61
+ );
62
+ const matching = runs.filter((run) => run.displayTitle?.includes(nonce));
63
+ if (matching.length > 1) {
64
+ throw new Error(
65
+ "ambiguous credential verification runs; old token remains active",
66
+ );
67
+ }
68
+ runId = matching[0]?.databaseId;
69
+ }
70
+ if (runId) {
71
+ const run = JSON.parse(
72
+ await capture([
73
+ "run",
74
+ "view",
75
+ String(runId),
76
+ "--repo",
77
+ repo,
78
+ "--json",
79
+ "status,conclusion",
80
+ ]),
81
+ );
82
+ if (run.status === "completed") {
83
+ if (run.conclusion !== "success") {
84
+ throw new Error(
85
+ "registry rejected credential verification; old token remains active",
86
+ );
87
+ }
88
+ return;
89
+ }
90
+ }
91
+ await (session.options.verificationDelay ?? delay)(2000);
92
+ }
93
+ throw new Error(
94
+ "credential verification timed out; old token remains active",
95
+ );
96
+ }
package/src/discovery.mjs CHANGED
@@ -2,6 +2,7 @@ import { readFile, readdir, realpath } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
 
4
4
  import { CONTAINER_FILES, containerPackages } from "./containers.mjs";
5
+ import { packageWorkflows, auditWrappers } from "./package-coverage.mjs";
5
6
  import { REGISTRIES } from "./model.mjs";
6
7
  import {
7
8
  TRUSTED_REGISTRIES,
@@ -39,6 +40,8 @@ const MANIFEST_NAMES = new Set([
39
40
  "build.gradle",
40
41
  "build.gradle.kts",
41
42
  "composer.json",
43
+ "jsr.json",
44
+ "deno.json",
42
45
  ]);
43
46
 
44
47
  /**
@@ -73,7 +76,9 @@ export async function inspectRepository(repository, options = {}) {
73
76
  const directory = testDirectory(manifest);
74
77
  return (
75
78
  directory === null ||
76
- (publishable && referencedByWorkflow(workflows, manifest)) ||
79
+ (!["docs", "experiments", "case-studies"].includes(directory) &&
80
+ publishable &&
81
+ referencedByWorkflow(workflows, manifest)) ||
77
82
  skip(manifest, testDirectoryReason(directory))
78
83
  );
79
84
  };
@@ -103,14 +108,82 @@ export async function inspectRepository(repository, options = {}) {
103
108
  for (const registry of TRUSTED_REGISTRIES) {
104
109
  publishers.set(registry, await detectPublisher(root, workflows, registry));
105
110
  }
111
+ // Extension packages can publish to both marketplaces independently.
112
+ for (const item of [...parsed].filter(Boolean)) {
113
+ if (item.registry !== "npm") {
114
+ continue;
115
+ }
116
+ const data = JSON.parse(await readManifest(path.join(root, item.manifest)));
117
+ if (data.publisher && data.engines?.vscode) {
118
+ for (const registry of ["vscode-marketplace", "open-vsx"]) {
119
+ const publisher = await detectPublisher(root, workflows, registry);
120
+ if (publisher.workflow) {
121
+ publishers.set(registry, publisher);
122
+ parsed.push({
123
+ ...item,
124
+ registry,
125
+ name: `${data.publisher}.${item.name}`,
126
+ });
127
+ }
128
+ }
129
+ }
130
+ }
131
+ publishers.set(
132
+ "maven-central",
133
+ await detectPublisher(root, workflows, "maven-central"),
134
+ );
106
135
  const containerFiles = dockerfiles
107
136
  .map((item) => relativePath(root, item))
108
137
  .filter((item) => !ignored(item) && kept(item, true));
109
- const packages = parsed
110
- .filter(Boolean)
111
- .filter((item) => kept(item.manifest, item.publishable))
112
- .map((item) => withPublisher(item, workflows, publishers))
113
- .concat(containerPackages(containerFiles, workflows, coordinates))
138
+ const counts = new Map();
139
+ for (const item of parsed.filter(Boolean)) {
140
+ if (!testDirectory(item.manifest)) {
141
+ counts.set(item.registry, (counts.get(item.registry) ?? 0) + 1);
142
+ }
143
+ }
144
+ const chrome = await detectPublisher(root, workflows, "chrome-web-store");
145
+ const chromePackages = chrome.workflow
146
+ ? [
147
+ withPublisher(
148
+ packageInfo(
149
+ "chrome-web-store",
150
+ coordinates.github_repository ?? "chrome-extension",
151
+ null,
152
+ `.github/workflows/${chrome.workflow}`,
153
+ ),
154
+ workflows,
155
+ new Map([["chrome-web-store", chrome]]),
156
+ ),
157
+ ]
158
+ : [];
159
+ const packages = (
160
+ await Promise.all(
161
+ parsed
162
+ .filter(Boolean)
163
+ .filter((item) => kept(item.manifest, item.publishable))
164
+ .map(async (item) => {
165
+ const scoped =
166
+ counts.get(item.registry) > 1 &&
167
+ TRUSTED_REGISTRIES.includes(item.registry)
168
+ ? new Map([
169
+ [
170
+ item.registry,
171
+ await detectPublisher(
172
+ root,
173
+ packageWorkflows(workflows, item),
174
+ item.registry,
175
+ ),
176
+ ],
177
+ ])
178
+ : publishers;
179
+ return withPublisher(item, workflows, scoped);
180
+ }),
181
+ )
182
+ )
183
+ .concat(
184
+ containerPackages(containerFiles, workflows, coordinates),
185
+ chromePackages,
186
+ )
114
187
  .sort((left, right) => {
115
188
  const registryOrder =
116
189
  REGISTRIES.indexOf(left.registry) - REGISTRIES.indexOf(right.registry);
@@ -122,6 +195,7 @@ export async function inspectRepository(repository, options = {}) {
122
195
  );
123
196
  });
124
197
 
198
+ await auditWrappers(root, packages);
125
199
  const inspection = {
126
200
  schema_version: 1,
127
201
  repository: {
@@ -178,6 +252,11 @@ function withPublisher(item, workflows, publishers) {
178
252
  const secrets = tokenSecrets(workflows, item.registry);
179
253
  const warnings = [
180
254
  ...publisher.warnings,
255
+ ...(!publisher.workflow && publisher.candidates.length === 0
256
+ ? [
257
+ `no workflow publishes ${item.name} to ${item.registry}; CI releases will not reach it`,
258
+ ]
259
+ : []),
181
260
  ...secrets.map((secret) => tokenSecretWarning(item.registry, secret)),
182
261
  ];
183
262
  if (warnings.length > 0) {
@@ -201,7 +280,7 @@ async function collectManifests(directory, manifests, dockerfiles) {
201
280
  } else if (
202
281
  entry.isFile() &&
203
282
  (MANIFEST_NAMES.has(entry.name) ||
204
- /\.(?:csproj|fsproj|vbproj)$/.test(entry.name))
283
+ /\.(?:csproj|fsproj|vbproj|gemspec)$/.test(entry.name))
205
284
  ) {
206
285
  manifests.push(item);
207
286
  }
@@ -212,6 +291,13 @@ async function parseManifest(manifestPath, manifest) {
212
291
  const contents = await readManifest(manifestPath);
213
292
  const filename = path.basename(manifestPath);
214
293
  switch (filename) {
294
+ case "jsr.json":
295
+ case "deno.json": {
296
+ const value = JSON.parse(contents);
297
+ return value.name && value.version
298
+ ? packageInfo("jsr", value.name, value.version, manifest)
299
+ : null;
300
+ }
215
301
  case "package.json":
216
302
  return parseNpm(contents, manifest);
217
303
  case "Cargo.toml":
@@ -241,6 +327,15 @@ async function parseManifest(manifestPath, manifest) {
241
327
  case "composer.json":
242
328
  return parseComposer(contents, manifest);
243
329
  default:
330
+ if (filename.endsWith(".gemspec")) {
331
+ return packageInfo(
332
+ "rubygems",
333
+ capture(contents, /\.name\s*=\s*['"]([^'"]+)/) ??
334
+ "unknown-ruby-package",
335
+ capture(contents, /\.version\s*=\s*['"]([^'"]+)/),
336
+ manifest,
337
+ );
338
+ }
244
339
  if (/\.(?:csproj|fsproj|vbproj)$/.test(filename)) {
245
340
  return parseDotnet(contents, manifestPath, manifest);
246
341
  }
@@ -291,7 +386,17 @@ function parsePyproject(contents, manifest) {
291
386
  if (!name) {
292
387
  return null;
293
388
  }
294
- return packageInfo("pypi", name, tomlString(section, "version"), manifest);
389
+ const item = packageInfo(
390
+ "pypi",
391
+ name,
392
+ tomlString(section, "version"),
393
+ manifest,
394
+ );
395
+ const required = tomlString(section, "requires-python");
396
+ if (required) {
397
+ item.requires_python = required;
398
+ }
399
+ return item;
295
400
  }
296
401
 
297
402
  function parseDotnet(contents, manifestPath, manifest) {
@@ -0,0 +1,88 @@
1
+ import { credentialSteps } from "./credential-cycle.mjs";
2
+ import { tokenSecretSteps } from "./tokens.mjs";
3
+
4
+ /** Additional OIDC registries never fall back to a stored publishing token. */
5
+ export function trustedFlow(item, context) {
6
+ const specs = {
7
+ rubygems: [
8
+ "gem",
9
+ ["build", item.manifest.split("/").at(-1)],
10
+ "https://rubygems.org/profile/oidc/pending_trusted_publishers",
11
+ "Register the GitHub Actions identity as a pending publisher, or add it in the existing gem settings.",
12
+ ],
13
+ nuget: [
14
+ "dotnet",
15
+ ["pack", "--configuration", "Release"],
16
+ "https://www.nuget.org/account/TrustedPublishing",
17
+ "Register a trusted publishing policy for this GitHub Actions identity. Use NuGet/login to exchange OIDC for a short-lived API key; never store that key.",
18
+ ],
19
+ jsr: [
20
+ "deno",
21
+ ["publish", "--dry-run"],
22
+ `https://jsr.io/${item.name}/settings`,
23
+ "Link this package to the GitHub repository; publish from the configured workflow through OIDC.",
24
+ ],
25
+ };
26
+ const [program, args, url, description] = specs[item.registry];
27
+ const steps = [
28
+ {
29
+ id: "validate-package",
30
+ title: `Validate ${item.registry} package`,
31
+ kind: "check",
32
+ description: "Build or validate locally without uploading.",
33
+ command: { program, args },
34
+ },
35
+ {
36
+ id: "configure-trusted-publisher",
37
+ title: `Configure ${item.registry} trusted publishing`,
38
+ kind: "browser",
39
+ description,
40
+ url,
41
+ },
42
+ {
43
+ id: "verify-oidc-release",
44
+ title: "Verify an OIDC release before removing tokens",
45
+ kind: "manual",
46
+ description: `Run ${context.workflow} and verify registry acceptance through OIDC; remove secret references from every workflow before deleting unused credentials.`,
47
+ },
48
+ ];
49
+ if (context.slug) {
50
+ steps.push(...tokenSecretSteps(item, context.slug));
51
+ }
52
+ return steps;
53
+ }
54
+
55
+ export function tokenFlow(item) {
56
+ const checks = {
57
+ "maven-central": { program: "mvn", args: ["--batch-mode", "verify"] },
58
+ "vscode-marketplace": {
59
+ program: "npx",
60
+ args: ["--no-install", "vsce", "package"],
61
+ },
62
+ "open-vsx": { program: "npx", args: ["--no-install", "vsce", "package"] },
63
+ };
64
+ const steps = checks[item.registry]
65
+ ? [
66
+ {
67
+ id: "validate-package",
68
+ title: "Validate package before credential changes",
69
+ kind: "check",
70
+ description: "Build without publishing.",
71
+ command: checks[item.registry],
72
+ },
73
+ ]
74
+ : [];
75
+ if (item.registry === "maven-central") {
76
+ steps.push({
77
+ id: "verify-namespace",
78
+ title: "Verify a Central namespace",
79
+ kind: "browser",
80
+ description:
81
+ "Sign in to the Central Portal and verify the namespace used by the package coordinates.",
82
+ url: "https://central.sonatype.com/publishing/namespaces",
83
+ });
84
+ }
85
+ return steps.concat(
86
+ credentialSteps(item.registry, item.token_secrets?.[0] ?? item.registry),
87
+ );
88
+ }
package/src/flows.mjs CHANGED
@@ -1,3 +1,4 @@
1
+ import { credentialSteps } from "./credential-cycle.mjs";
1
2
  import { DEFAULT_TRUST_NPM, NPM_TFA_URL } from "./prerequisites.mjs";
2
3
  import {
3
4
  npmName,
@@ -255,6 +256,13 @@ export function npmFlow(packageInfo, context) {
255
256
  },
256
257
  ),
257
258
  checkRegistryStep("npm", registryStateUrl(packageInfo)),
259
+ step(
260
+ "check-name-policy",
261
+ "Check npm name availability and policy",
262
+ "check",
263
+ "Check exact and punctuation variants before approval. Server-side blocks can still refuse publication.",
264
+ { when: "package-missing" },
265
+ ),
258
266
  step(
259
267
  "check-sign-in",
260
268
  "Check the npm session",
@@ -308,6 +316,25 @@ export function npmFlow(packageInfo, context) {
308
316
  "Compare the bin entries of the packed package.json with package.json and check that each installed bin prints its version for --version.",
309
317
  { when: "package-missing" },
310
318
  ),
319
+ step(
320
+ "publish-dry-run",
321
+ "Validate first publication without upload",
322
+ "check",
323
+ "Validate the reviewed tarball before asking for publication approval.",
324
+ {
325
+ command: command("npm", [
326
+ "publish",
327
+ "{tarball}",
328
+ "--dry-run",
329
+ "--ignore-scripts",
330
+ "--access",
331
+ "public",
332
+ "--provenance=false",
333
+ ]),
334
+ when: "package-missing",
335
+ cwd: packageCwd(context.directory),
336
+ },
337
+ ),
311
338
  // Sign in right before publishing, so the sign-in, publish, and trust
312
339
  // approvals happen together and npm can skip repeated 2FA prompts.
313
340
  step(
@@ -416,7 +443,6 @@ export function npmFlow(packageInfo, context) {
416
443
  "Confirm that npm lists the GitHub Actions trusted publisher.",
417
444
  { command: trustList, when: "trust-missing" },
418
445
  ),
419
- ...tokenSecretSteps(packageInfo, context.slug),
420
446
  step(
421
447
  "rerun-release",
422
448
  "Re-run the failed release jobs",
@@ -439,6 +465,7 @@ export function npmFlow(packageInfo, context) {
439
465
  if (context.verifyRelease) {
440
466
  steps.push(...verifyReleaseSteps(context, registryStateUrl(packageInfo)));
441
467
  }
468
+ steps.push(...tokenSecretSteps(packageInfo, context.slug));
442
469
  }
443
470
  steps.push(
444
471
  step(
@@ -707,13 +734,6 @@ export function dockerHubFlow(packageInfo, context) {
707
734
  when: "package-missing",
708
735
  },
709
736
  ),
710
- step(
711
- "create-access-token",
712
- "Create a Read & Write access token",
713
- "browser",
714
- `Create a token with Read & Write access and a short expiry. Personal access tokens cannot be limited to one repository; an organization access token can be restricted to ${packageInfo.name}. Keep the token only for the next step.`,
715
- { url: "https://app.docker.com/settings/personal-access-tokens/create" },
716
- ),
717
737
  step(
718
738
  "check-github-cli",
719
739
  "Check the GitHub CLI session",
@@ -755,17 +775,7 @@ export function dockerHubFlow(packageInfo, context) {
755
775
  cwd: ".",
756
776
  },
757
777
  ),
758
- step(
759
- "set-token-secret",
760
- "Set DOCKERHUB_TOKEN",
761
- "command",
762
- "gh reads the token from the terminal without echoing it; the tool never sees it.",
763
- {
764
- command: command("gh", ["secret", "set", "DOCKERHUB_TOKEN", ...repo]),
765
- cwd: ".",
766
- confirm: true,
767
- },
768
- ),
778
+ ...credentialSteps("docker-hub", "DOCKERHUB_TOKEN"),
769
779
  ];
770
780
  }
771
781
 
package/src/index.mjs CHANGED
@@ -3,4 +3,4 @@ export { inspectRepository } from "./discovery.mjs";
3
3
  export { REGISTRIES, parseRegistry } from "./model.mjs";
4
4
  export { buildPlans, packageDirectory } from "./plan.mjs";
5
5
  export { ensureProfileIgnored } from "./profile.mjs";
6
- export { defaultBrowserProfile, executePlan } from "./setup.mjs";
6
+ export { defaultBrowserProfile, executePlan, executePlans } from "./setup.mjs";