package-registry-manager 0.22.6 → 0.22.8

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.
@@ -1,8 +1,14 @@
1
- import { spawn } from "node:child_process";
2
- import { readFile } from "node:fs/promises";
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
2
  import path from "node:path";
4
- import { verifyCredentialWorkflow } from "./credential-verification.mjs";
5
- import { needsRotation, rotateCredential } from "./credential-cycle.mjs";
3
+ import { githubServices } from "./github.mjs";
4
+ import {
5
+ cycleCredential,
6
+ secretName,
7
+ trackedTokenIds,
8
+ recordTokenId,
9
+ completeRotation,
10
+ } from "./ci-credential-cycle.mjs";
11
+ import { AUTH_FAILURE_PATTERNS } from "./publishing-policy.mjs";
6
12
  import {
7
13
  TOKEN_PROVIDERS,
8
14
  tokenFormScript,
@@ -10,42 +16,7 @@ import {
10
16
  revokedScript,
11
17
  } from "./credential-browser.mjs";
12
18
 
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. */
19
+ /** Provision only when GitHub CI reports a missing or broken credential. */
49
20
  export async function setupCredential(session) {
50
21
  const { plan, options } = session;
51
22
  const provider = TOKEN_PROVIDERS[plan.registry];
@@ -62,149 +33,226 @@ export async function setupCredential(session) {
62
33
  throw error;
63
34
  });
64
35
  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
36
  const { github_owner: owner, github_repository: repository } =
71
37
  plan.repository;
72
38
  if (!owner || !repository) {
73
39
  throw new Error("token setup requires GitHub repository coordinates");
74
40
  }
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
- }
41
+ const slug = `${owner}/${repository}`;
42
+ const secret = secretName(
43
+ options.secretName ??
44
+ settings.secret ??
45
+ plan.package.token_secrets?.[0] ??
46
+ provider.secret,
47
+ plan.registry,
48
+ slug,
49
+ );
80
50
  if (settings.level && !["repo", "org"].includes(settings.level)) {
81
51
  throw new Error("token secret level must be repo or org");
82
52
  }
83
- const scope =
84
- settings.level === "repo"
85
- ? ["--repo", `${owner}/${repository}`]
86
- : ["--org", owner];
87
- const visibility =
53
+ const github = options.github ?? githubServices(options);
54
+ const scope = { repo: slug };
55
+ const healthOptions = {
56
+ scope,
57
+ failurePatterns: AUTH_FAILURE_PATTERNS[plan.registry] ?? [],
58
+ inputs: settings.verification_workflow
59
+ ? { prm_nonce: `prm-${Date.now()}` }
60
+ : {},
61
+ };
62
+ const stateFile =
63
+ options.browserProfile &&
64
+ path.join(path.dirname(options.browserProfile), "credential-ids.json");
65
+ const key = `${slug}:${secret}`;
66
+ const targets =
88
67
  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, {
68
+ ? [slug]
69
+ : (options.secretRepositories?.[`${plan.registry}:${secret}`] ?? [slug]);
70
+ let state = {};
71
+ let page;
72
+ let approved = false;
73
+ let repositorySecretPresent = false;
74
+ const result = await cycleCredential({
75
+ health: () => github.health.health(secret, healthOptions),
76
+ metadata: async () => {
77
+ let metadata = await github.secrets(scope).getMetadata(secret);
78
+ repositorySecretPresent = Boolean(metadata);
79
+ if (!metadata && settings.level !== "repo") {
80
+ try {
81
+ metadata = await github.secrets({ org: owner }).getMetadata(secret);
82
+ } catch (error) {
83
+ if (![403, 404, 422].includes(error.status)) {
84
+ throw error;
85
+ }
86
+ }
87
+ }
88
+ if (stateFile) {
89
+ state = JSON.parse(
90
+ await readFile(stateFile, "utf8").catch((error) => {
91
+ if (error.code === "ENOENT") {
92
+ return "{}";
93
+ }
94
+ throw error;
95
+ }),
96
+ );
97
+ }
98
+ const ids = trackedTokenIds(state, targets, secret);
99
+ const currentId =
100
+ settings.token_id ?? state[key]?.at(-1) ?? metadata?.token_id;
101
+ const retained = Object.entries(state).some(
102
+ ([entry, values]) =>
103
+ !targets.some((repo) => entry === `${repo}:${secret}`) &&
104
+ values.includes(currentId),
105
+ );
106
+ return metadata || ids.length
107
+ ? {
108
+ ...metadata,
109
+ present: Boolean(metadata),
110
+ token_id: retained ? undefined : currentId,
111
+ token_ids: ids,
112
+ }
113
+ : null;
114
+ },
163
115
  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})...`,
116
+ if (
117
+ options.noBrowser ||
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 days = settings.expiry_days ?? 30;
126
+ if (!Number.isInteger(days) || days < 1 || days > 90) {
127
+ throw new Error("token expiry_days must be between 1 and 90");
128
+ }
129
+ if (
130
+ !approved &&
131
+ !options.yes &&
132
+ !/^y(?:es)?$/i.test(
133
+ await session.prompt(
134
+ "Create and store a scoped credential, verify it, then revoke the replaced token? [y/N] ",
135
+ ),
136
+ )
137
+ ) {
138
+ throw new Error("credential creation declined");
139
+ }
140
+ approved = true;
141
+ page ??= await session.automatedPage(
142
+ {
143
+ ...options.browserOptions,
144
+ import: options.browserOptions?.import ?? {
145
+ browser: "auto",
146
+ profile: null,
147
+ },
148
+ importScope: "domains",
149
+ },
150
+ true,
167
151
  );
152
+ await page.goto(provider.url);
168
153
  await page.evaluate(
169
- tokenFormScript(provider, `prm-${repository}`, expiry),
154
+ tokenFormScript(
155
+ provider,
156
+ `prm-${repository}-${secret}`,
157
+ new Date(Date.now() + days * 86_400_000).toISOString(),
158
+ ),
170
159
  );
171
160
  await session.prompt(
172
- `Review the scope and expiry, create the token in this browser, then press Enter (never paste its value)...`,
161
+ `Review the publishing scope (${provider.scope}) and expiry, create the token in this browser, then press Enter (never paste its value)...`,
173
162
  );
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
- );
163
+ return page.evaluate(READ_TOKEN);
164
+ },
165
+ ensure: async (credential, health) => {
166
+ const repos = targets;
167
+ let stored = await github
168
+ .secrets(settings.level === "repo" ? scope : { org: owner })
169
+ .ensure(secret, {
170
+ ...(settings.level === "repo"
171
+ ? {}
172
+ : { repos, visibility: "selected" }),
173
+ health,
174
+ rotateBeforeMs: 0,
175
+ failurePatterns: healthOptions.failurePatterns,
176
+ acquire: async () => ({
177
+ value: credential.value,
178
+ expiresAt: credential.expires_at,
179
+ }),
180
+ });
181
+ if (stored.path === "organization") {
182
+ for (const repo of repos) {
183
+ const override =
184
+ repo === slug
185
+ ? repositorySecretPresent
186
+ : await github.secrets({ repo }).getMetadata(secret);
187
+ if (!override) {
188
+ continue;
189
+ }
190
+ const replacement = await github.secrets({ repo }).ensure(secret, {
191
+ health,
192
+ rotateBeforeMs: 0,
193
+ acquire: async () => ({
194
+ value: credential.value,
195
+ expiresAt: credential.expires_at,
196
+ }),
197
+ });
198
+ if (replacement.valueChanged === false) {
199
+ throw new Error(
200
+ "gh-manager did not replace the repository credential override",
201
+ );
202
+ }
203
+ if (repo === slug) {
204
+ stored = {
205
+ ...replacement,
206
+ path: "repository",
207
+ fallbackReason:
208
+ "An existing repository secret overrides the organization secret; replaced the repository credential too.",
209
+ };
210
+ }
211
+ }
179
212
  }
180
- return credential;
213
+ if (stored.valueChanged === false) {
214
+ throw new Error("gh-manager did not store the replacement credential");
215
+ }
216
+ if (stateFile) {
217
+ recordTokenId(state, targets, secret, credential.id);
218
+ await mkdir(path.dirname(stateFile), { recursive: true });
219
+ await writeFile(stateFile, JSON.stringify(state), { mode: 0o600 });
220
+ }
221
+ return stored;
222
+ },
223
+ test: async (stored) => {
224
+ const repos = stored ? targets : [slug];
225
+ const results = await Promise.all(
226
+ repos.map((repo) =>
227
+ github.health.test(secret, { ...healthOptions, scope: { repo } }),
228
+ ),
229
+ );
230
+ return {
231
+ status: results.some((item) => item.status === "auth-failing")
232
+ ? "auth-failing"
233
+ : results.every((item) => item.status === "ok")
234
+ ? "ok"
235
+ : "unknown",
236
+ };
181
237
  },
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
238
  revoke: async (id) => {
202
239
  await page.goto(provider.url.replace(/\/create$/, ""));
203
240
  await session.prompt(
204
241
  `Revoke registry token ${id} in this browser, then press Enter...`,
205
242
  );
206
243
  },
207
- revoked: async (id) => page.evaluate(revokedScript(id)),
244
+ revoked: (id) => page.evaluate(revokedScript(id)),
208
245
  });
209
- console.log(" Credential verified; replaced token revocation verified.");
246
+ if (stateFile && result.changed) {
247
+ completeRotation(state, targets, secret, result.token_id);
248
+ await writeFile(stateFile, JSON.stringify(state), { mode: 0o600 });
249
+ }
250
+ session.outcome = {
251
+ status: result.changed ? "configured" : "complete",
252
+ secret,
253
+ ...(result.path ? { secret_scope: result.path } : {}),
254
+ ...(result.fallbackReason
255
+ ? { fallback_reason: result.fallbackReason }
256
+ : {}),
257
+ };
210
258
  }
package/src/discovery.mjs CHANGED
@@ -4,6 +4,7 @@ import path from "node:path";
4
4
  import { CONTAINER_FILES, containerPackages } from "./containers.mjs";
5
5
  import { packageWorkflows, auditWrappers } from "./package-coverage.mjs";
6
6
  import { REGISTRIES } from "./model.mjs";
7
+ import { inspectManifestRepositories } from "./repository-identity.mjs";
7
8
  import {
8
9
  TRUSTED_REGISTRIES,
9
10
  detectPublisher,
@@ -214,7 +215,7 @@ export async function inspectRepository(repository, options = {}) {
214
215
  left.manifest.localeCompare(right.manifest),
215
216
  );
216
217
  }
217
- return inspection;
218
+ return inspectManifestRepositories(inspection);
218
219
  }
219
220
 
220
221
  /**
package/src/github.mjs ADDED
@@ -0,0 +1,42 @@
1
+ import {
2
+ createRestClient,
3
+ resolveToken,
4
+ createRepoManager,
5
+ createRunManager,
6
+ createSecretHealth,
7
+ createSecretManager,
8
+ } from "@link-foundation/gh-manager";
9
+
10
+ /** GitHub-only operations, supplied by the installed gh-manager library. */
11
+ export function githubServices(options = {}) {
12
+ const { token } = resolveToken(options);
13
+ if (!token && !options.rest) {
14
+ throw new Error(
15
+ "sign in to GitHub with gh auth login before scanning or setting secrets",
16
+ );
17
+ }
18
+ const rest = options.rest ?? createRestClient({ token });
19
+ const log = {
20
+ debug: (message) => {
21
+ if (options.verbose) {
22
+ console.error(message);
23
+ }
24
+ },
25
+ };
26
+ const context = { rest, log };
27
+ return {
28
+ repos: createRepoManager(context),
29
+ runs: createRunManager(context),
30
+ health: createSecretHealth(context),
31
+ secrets: (scope) => {
32
+ if (typeof scope.repo === "string") {
33
+ const [owner, name, extra] = scope.repo.split("/");
34
+ if (!owner || !name || extra !== undefined) {
35
+ throw new Error("invalid repository secret scope");
36
+ }
37
+ scope = { ...scope, repo: { owner, name } };
38
+ }
39
+ return createSecretManager({ ...context, scope });
40
+ },
41
+ };
42
+ }
package/src/index.mjs CHANGED
@@ -2,5 +2,7 @@ export { npmPrefillScript, openInUserBrowser } from "./browser.mjs";
2
2
  export { inspectRepository } from "./discovery.mjs";
3
3
  export { REGISTRIES, parseRegistry } from "./model.mjs";
4
4
  export { buildPlans, packageDirectory } from "./plan.mjs";
5
+ export { scanRepositories } from "./organization-scan.mjs";
6
+ export { cycleCredential, secretName } from "./ci-credential-cycle.mjs";
5
7
  export { ensureProfileIgnored } from "./profile.mjs";
6
8
  export { defaultBrowserProfile, executePlan, executePlans } from "./setup.mjs";
@@ -0,0 +1,201 @@
1
+ import { mkdtemp, readFile, realpath, rm, writeFile } from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { exec } from "command-stream";
5
+ import {
6
+ githubSlug,
7
+ manifestUrls,
8
+ repositorySlug,
9
+ } from "./repository-identity.mjs";
10
+
11
+ function runner(options) {
12
+ return (
13
+ options.run ??
14
+ (async (program, args, cwd = options.repository) => {
15
+ const result = await exec(program, args, {
16
+ cwd,
17
+ env: process.env,
18
+ capture: true,
19
+ mirror: false,
20
+ stdin: "ignore",
21
+ });
22
+ if (result.code !== 0) {
23
+ throw new Error(
24
+ `${program} failed while preparing the manifest repository proposal`,
25
+ );
26
+ }
27
+ return String(result.stdout ?? "").trim();
28
+ })
29
+ );
30
+ }
31
+
32
+ function safeManifest(manifest) {
33
+ if (
34
+ !manifest ||
35
+ manifest
36
+ .split("/")
37
+ .some((part) => !part || part === "." || part === "..") ||
38
+ path.isAbsolute(manifest) ||
39
+ manifest.includes("\\")
40
+ ) {
41
+ throw new Error("unsafe manifest path for repository repair");
42
+ }
43
+ }
44
+
45
+ /** Change only stale repository values; preserve npm's repository.directory. */
46
+ export function manifestRepositoryProposal(contents, manifest, slug) {
47
+ if (!githubSlug(slug)) {
48
+ throw new Error("invalid canonical repository");
49
+ }
50
+ const urls = manifestUrls(contents, manifest);
51
+ const stale = urls.filter(
52
+ (url) =>
53
+ githubSlug(url) && githubSlug(url).toLowerCase() !== slug.toLowerCase(),
54
+ );
55
+ if (!stale.length) {
56
+ return contents;
57
+ }
58
+ const url = `https://github.com/${slug}`;
59
+ if (manifest.endsWith("package.json")) {
60
+ const document = JSON.parse(contents.replace(/^\uFEFF/, ""));
61
+ if (typeof document.repository === "string") {
62
+ document.repository = url;
63
+ } else {
64
+ document.repository.url = `git+${url}.git`;
65
+ }
66
+ return `${JSON.stringify(document, null, 2)}\n`;
67
+ }
68
+ let proposal = contents;
69
+ for (const old of stale) {
70
+ proposal = proposal.replaceAll(old, old.replace(githubSlug(old), slug));
71
+ }
72
+ return proposal;
73
+ }
74
+
75
+ /** Check remote metadata before retrying; the original run may use an old SHA. */
76
+ export async function verifyRemoteManifest(plan, options, reference) {
77
+ safeManifest(plan.package.manifest);
78
+ const run = runner(options);
79
+ const slug = repositorySlug(plan.repository);
80
+ const ref =
81
+ reference ??
82
+ (await run("gh", ["api", `repos/${slug}`, "--jq", ".default_branch"]));
83
+ if (!ref || ref.startsWith("-") || /[\s:]/.test(ref)) {
84
+ throw new Error("invalid remote manifest reference");
85
+ }
86
+ await run("git", ["fetch", "origin", ref]);
87
+ const contents = await run("git", [
88
+ "show",
89
+ `FETCH_HEAD:${plan.package.manifest}`,
90
+ ]);
91
+ return (
92
+ manifestRepositoryProposal(contents, plan.package.manifest, slug) ===
93
+ contents
94
+ );
95
+ }
96
+
97
+ /** Offer a reviewed branch and draft PR from the current remote default branch. */
98
+ export async function offerManifestRepository(plan, options) {
99
+ const run = runner(options);
100
+ const slug = repositorySlug(plan.repository);
101
+ const manifest = plan.package.manifest;
102
+ safeManifest(manifest);
103
+ const remote = githubSlug(await run("git", ["remote", "get-url", "origin"]));
104
+ if (!remote) {
105
+ throw new Error(
106
+ "origin must be a GitHub repository for the manifest proposal",
107
+ );
108
+ }
109
+ const canonical = await run("gh", [
110
+ "api",
111
+ `repos/${remote}`,
112
+ "--jq",
113
+ ".full_name",
114
+ ]);
115
+ if (canonical.toLowerCase() !== slug.toLowerCase()) {
116
+ throw new Error("origin must resolve to the repository being repaired");
117
+ }
118
+ const base = await run("gh", [
119
+ "api",
120
+ `repos/${slug}`,
121
+ "--jq",
122
+ ".default_branch",
123
+ ]);
124
+ if (!base || base.startsWith("-") || /[\s:]/.test(base)) {
125
+ throw new Error("invalid default branch");
126
+ }
127
+ await run("git", ["fetch", "origin", base]);
128
+ const contents = await run("git", ["show", `FETCH_HEAD:${manifest}`]);
129
+ const proposal = manifestRepositoryProposal(contents, manifest, slug);
130
+ if (proposal === contents) {
131
+ return undefined;
132
+ }
133
+ console.log(`Proposed ${manifest}:\n${proposal}`);
134
+ if (
135
+ !options.yes &&
136
+ !/^y(?:es)?$/i.test(
137
+ await options.prompt(
138
+ "Create a branch and draft pull request with this repository URL fix? [y/N] ",
139
+ ),
140
+ )
141
+ ) {
142
+ return { status: "blocked" };
143
+ }
144
+ const branch = `prm/repository-url-${Date.now()}`;
145
+ const temporary = await mkdtemp(path.join(os.tmpdir(), "prm-manifest-"));
146
+ const checkout = path.join(temporary, "checkout");
147
+ let created = false;
148
+ try {
149
+ await run("git", ["worktree", "add", "-b", branch, checkout, "FETCH_HEAD"]);
150
+ created = true;
151
+ const target = path.join(checkout, manifest);
152
+ const root = await realpath(checkout);
153
+ const physical = await realpath(target);
154
+ if (
155
+ !physical.startsWith(`${root}${path.sep}`) ||
156
+ physical !== path.join(root, manifest)
157
+ ) {
158
+ throw new Error(
159
+ "manifest must stay inside the proposal worktree without symlinks",
160
+ );
161
+ }
162
+ const current = await readFile(target, "utf8");
163
+ const updated = manifestRepositoryProposal(current, manifest, slug);
164
+ await writeFile(target, updated);
165
+ await run("git", ["add", "--", manifest], checkout);
166
+ await run(
167
+ "git",
168
+ ["commit", "-m", "Fix manifest repository URL after repository transfer"],
169
+ checkout,
170
+ );
171
+ await run("git", ["push", "origin", `HEAD:refs/heads/${branch}`], checkout);
172
+ const url = await run(
173
+ "gh",
174
+ [
175
+ "pr",
176
+ "create",
177
+ "--repo",
178
+ slug,
179
+ "--base",
180
+ base,
181
+ "--head",
182
+ branch,
183
+ "--draft",
184
+ "--title",
185
+ "Fix manifest repository URL after repository transfer",
186
+ "--body",
187
+ `Update ${manifest} to name ${slug}. Review and merge this change, then re-run package-registry-manager setup to verify the repository metadata and retry the release.`,
188
+ ],
189
+ checkout,
190
+ );
191
+ console.log(
192
+ `Review and merge ${url}, then re-run setup. Release retries are paused.`,
193
+ );
194
+ return { status: "manifest-pr", url };
195
+ } finally {
196
+ if (created) {
197
+ await run("git", ["worktree", "remove", "--force", checkout]);
198
+ }
199
+ await rm(temporary, { recursive: true, force: true });
200
+ }
201
+ }