run402 4.130.4 → 4.131.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/lib/assets.mjs CHANGED
@@ -362,7 +362,7 @@ const VALID_SORTS = new Set(["key:asc", "createdAt:asc", "createdAt:desc"]);
362
362
  function parseSortFlag(raw) {
363
363
  if (typeof raw !== "string" || !VALID_SORTS.has(raw)) {
364
364
  fail({
365
- code: "BAD_FLAG",
365
+ code: "INVALID_SORT",
366
366
  message: `--sort must be one of: ${[...VALID_SORTS].join(", ")} — got: ${raw}`,
367
367
  details: { flag: "--sort", value: raw },
368
368
  });
@@ -390,7 +390,7 @@ function applyFilterFlag(filter, raw) {
390
390
  const value = raw.slice(eq + 1);
391
391
  if (!VALID_FILTER_KEYS.has(key)) {
392
392
  fail({
393
- code: "BAD_FLAG",
393
+ code: "INVALID_FILTER_KEY",
394
394
  message: `--filter key ${key} not in: ${[...VALID_FILTER_KEYS].join(", ")}`,
395
395
  details: { flag: "--filter", key },
396
396
  });
@@ -398,7 +398,7 @@ function applyFilterFlag(filter, raw) {
398
398
  if (key === "is_image") {
399
399
  if (value !== "true" && value !== "false") {
400
400
  fail({
401
- code: "BAD_FLAG",
401
+ code: "INVALID_FILTER_KEY",
402
402
  message: `--filter is_image must be 'true' or 'false', got: ${value}`,
403
403
  });
404
404
  }
@@ -407,7 +407,7 @@ function applyFilterFlag(filter, raw) {
407
407
  const n = Number(value);
408
408
  if (!Number.isSafeInteger(n) || n < 0) {
409
409
  fail({
410
- code: "BAD_FLAG",
410
+ code: "INVALID_FILTER_KEY",
411
411
  message: `--filter ${key} must be a non-negative integer, got: ${value}`,
412
412
  });
413
413
  }
@@ -415,7 +415,7 @@ function applyFilterFlag(filter, raw) {
415
415
  } else {
416
416
  if (value.length === 0) {
417
417
  fail({
418
- code: "BAD_FLAG",
418
+ code: "INVALID_FILTER_KEY",
419
419
  message: `--filter ${key} must be a non-empty string`,
420
420
  });
421
421
  }
package/lib/ci.mjs CHANGED
@@ -21,7 +21,7 @@ const { version: RUN402_VERSION } = JSON.parse(
21
21
  const HELP = `run402 ci — Manage CI/OIDC deploy bindings
22
22
 
23
23
  Usage:
24
- run402 ci link github [--project <project_id>] [--manifest <path>] [--repo <owner/repo>] [--branch <name> | --environment <name>] [--repository-id <repository_id>] [--workflow <path>] [--expires-at <iso>] [--route-scope <pattern> ...] [--force]
24
+ run402 ci link github [--project <project_id>] [--manifest <path>] [--repo <owner/repo>] [--branch <name> | --environment <name>] [--repository-id <repository_id>] [--workflow <path> | --no-workflow] [--expires-at <iso>] [--route-scope <pattern> ...] [--force]
25
25
  run402 ci list [--project <project_id>]
26
26
  run402 ci revoke <binding_id>
27
27
  run402 ci set-asset-scopes <binding_id> <scope1> [<scope2> ...]
@@ -37,16 +37,19 @@ const SUB_HELP = {
37
37
  link: `run402 ci link github — Link GitHub Actions OIDC for run402 deploy
38
38
 
39
39
  Usage:
40
- run402 ci link github [--project <project_id>] [--manifest <path>] [--repo <owner/repo>] [--branch <name> | --environment <name>] [--repository-id <repository_id>] [--workflow <path>] [--expires-at <iso>] [--route-scope <pattern> ...] [--force]
40
+ run402 ci link github [--project <project_id>] [--manifest <path>] [--repo <owner/repo>] [--branch <name> | --environment <name>] [--repository-id <repository_id>] [--workflow <path> | --no-workflow] [--expires-at <iso>] [--route-scope <pattern> ...] [--force]
41
41
 
42
42
  Options:
43
43
  --project <project_id> Project ID (defaults to the active project)
44
44
  --manifest <path> Manifest path used by the generated workflow (default: run402.deploy.json)
45
- --repo <owner/repo> GitHub repo (default: inferred from origin remote)
45
+ --repo <owner/repo> GitHub repo as owner/repo, owner@<id>/repo@<id> (the spelling of an immutable
46
+ OIDC subject, which also gives the repository id) or a github.com URL
47
+ (default: inferred from origin remote)
46
48
  --branch <name> Branch subject and push trigger (default: current branch)
47
49
  --environment <name> GitHub environment subject; adds job.environment
48
50
  --repository-id <repository_id> Numeric GitHub repository id when API lookup is unavailable
49
51
  --workflow <path> Workflow path (default: .github/workflows/run402-deploy.yml)
52
+ --no-workflow Create the binding only, for a repository whose workflow already runs run402 deploy
50
53
  --expires-at <iso> Optional binding expiration timestamp
51
54
  --route-scope <pattern> Optional exact path or final wildcard route scope, repeatable (examples: /admin, /api/*)
52
55
  --force Overwrite an existing workflow file
@@ -107,8 +110,8 @@ function parseFlags(args, allowed, { repeatable = new Set() } = {}) {
107
110
  details: { flag: arg },
108
111
  });
109
112
  }
110
- if (arg === "--force") {
111
- flags.force = true;
113
+ if (arg === "--force" || arg === "--no-workflow") {
114
+ flags[arg.slice(2).replace(/-/g, "_")] = true;
112
115
  continue;
113
116
  }
114
117
  if (!args[i + 1] || args[i + 1].startsWith("--")) {
@@ -181,6 +184,21 @@ function parseGithubRemote(remote) {
181
184
  return null;
182
185
  }
183
186
 
187
+ // `@` and `:` cannot appear in a GitHub owner or repository name.
188
+ const GITHUB_REPO_NAME = /^[^/\s@:]+\/[^/\s@:]+$/;
189
+ const GITHUB_REPO_IMMUTABLE = /^([^/\s@:]+)@(\d+)\/([^/\s@:]+)@(\d+)$/;
190
+
191
+ // --repo takes the repository however the owner has it: owner/repo, the
192
+ // owner@<owner_id>/repo@<repo_id> spelling of GitHub's immutable OIDC subject
193
+ // (which also carries the repository id), or a github.com URL. The binding is
194
+ // always written in the name form; the gateway matches either spelling.
195
+ function resolveRepo(value) {
196
+ const immutable = GITHUB_REPO_IMMUTABLE.exec(value);
197
+ if (immutable) return { repo: `${immutable[1]}/${immutable[3]}`, repoId: immutable[4] };
198
+ const repo = parseGithubRemote(value) || value;
199
+ return GITHUB_REPO_NAME.test(repo) ? { repo, repoId: null } : null;
200
+ }
201
+
184
202
  function inferBranch() {
185
203
  return git(["branch", "--show-current"]);
186
204
  }
@@ -259,6 +277,7 @@ async function linkGithub(args) {
259
277
  "--environment",
260
278
  "--repository-id",
261
279
  "--workflow",
280
+ "--no-workflow",
262
281
  "--expires-at",
263
282
  "--route-scope",
264
283
  "--force",
@@ -277,18 +296,41 @@ async function linkGithub(args) {
277
296
  });
278
297
  }
279
298
 
299
+ if (flags.no_workflow) {
300
+ const clash = ["--workflow", "--manifest", "--force"].find((flag) => flags[flag.slice(2)] !== undefined);
301
+ if (clash) {
302
+ fail({
303
+ code: "BAD_USAGE",
304
+ message: `--no-workflow writes no workflow, so ${clash} has nothing to apply to.`,
305
+ details: { flag: clash },
306
+ });
307
+ }
308
+ }
309
+
280
310
  const projectId = resolveProjectId(flags.project);
281
- const repo = flags.repo || inferRepo();
282
- if (!repo || !/^[^/\s]+\/[^/\s]+$/.test(repo)) {
311
+ const resolved = resolveRepo(flags.repo || inferRepo() || "");
312
+ if (!resolved) {
283
313
  fail({
284
314
  code: "GITHUB_REPO_REQUIRED",
285
- message: "Could not infer GitHub owner/repo from git remote origin.",
315
+ message: flags.repo
316
+ ? `--repo must be owner/repo, owner@<id>/repo@<id> or a github.com URL; got ${JSON.stringify(flags.repo)}.`
317
+ : "Could not infer GitHub owner/repo from git remote origin.",
286
318
  hint: "Pass --repo <owner/repo>.",
287
319
  });
288
320
  }
321
+ const { repo } = resolved;
322
+ if (resolved.repoId && flags.repository_id && resolved.repoId !== flags.repository_id) {
323
+ fail({
324
+ code: "GITHUB_REPOSITORY_ID_CONFLICT",
325
+ message: `--repo names repository id ${resolved.repoId}, but --repository-id is ${flags.repository_id}.`,
326
+ hint: "Drop --repository-id, or pass the --repo of the repository it names.",
327
+ details: { repo, repo_id_in_repo: resolved.repoId, repository_id: flags.repository_id },
328
+ });
329
+ }
289
330
  const branch = flags.environment ? (flags.branch || inferBranch() || null) : (flags.branch || inferBranch());
290
331
  const subject = buildSubject(repo, { branch, environment: flags.environment });
291
- const repositoryId = flags.repository_id || await fetchGithubRepositoryId(repo);
332
+ const repositoryIdProvided = flags.repository_id || resolved.repoId;
333
+ const repositoryId = repositoryIdProvided || await fetchGithubRepositoryId(repo);
292
334
  if (!repositoryId) {
293
335
  fail({
294
336
  code: "GITHUB_REPOSITORY_ID_REQUIRED",
@@ -298,9 +340,9 @@ async function linkGithub(args) {
298
340
  });
299
341
  }
300
342
 
301
- const workflowPath = flags.workflow || ".github/workflows/run402-deploy.yml";
302
- const absWorkflowPath = resolve(process.cwd(), workflowPath);
303
- if (existsSync(absWorkflowPath) && !flags.force) {
343
+ const workflowPath = flags.no_workflow ? null : flags.workflow || ".github/workflows/run402-deploy.yml";
344
+ const absWorkflowPath = workflowPath && resolve(process.cwd(), workflowPath);
345
+ if (absWorkflowPath && existsSync(absWorkflowPath) && !flags.force) {
304
346
  fail({
305
347
  code: "WORKFLOW_EXISTS",
306
348
  message: `Workflow already exists: ${workflowPath}`,
@@ -309,7 +351,7 @@ async function linkGithub(args) {
309
351
  });
310
352
  }
311
353
 
312
- const manifest = flags.manifest || "run402.deploy.json";
354
+ const manifest = flags.no_workflow ? null : flags.manifest || "run402.deploy.json";
313
355
  const routeScopes = Array.isArray(flags.route_scope) ? flags.route_scope : [];
314
356
  const nonce = randomBytes(16).toString("hex");
315
357
  const values = {
@@ -334,7 +376,7 @@ async function linkGithub(args) {
334
376
  });
335
377
  }
336
378
 
337
- const workflow = generateWorkflow({
379
+ const workflow = workflowPath && generateWorkflow({
338
380
  branch,
339
381
  environment: flags.environment || null,
340
382
  manifest,
@@ -348,8 +390,10 @@ async function linkGithub(args) {
348
390
  signed_delegation: signedDelegation,
349
391
  });
350
392
  const outputRouteScopes = Array.isArray(binding.route_scopes) ? binding.route_scopes : [];
351
- mkdirSync(dirname(absWorkflowPath), { recursive: true });
352
- writeFileSync(absWorkflowPath, workflow, { encoding: "utf8", mode: 0o644 });
393
+ if (absWorkflowPath) {
394
+ mkdirSync(dirname(absWorkflowPath), { recursive: true });
395
+ writeFileSync(absWorkflowPath, workflow, { encoding: "utf8", mode: 0o644 });
396
+ }
353
397
  console.log(JSON.stringify({
354
398
  binding_id: binding.id,
355
399
  project_id: projectId,
@@ -359,12 +403,14 @@ async function linkGithub(args) {
359
403
  allowed_actions: [...V1_CI_ALLOWED_ACTIONS],
360
404
  route_scopes: outputRouteScopes,
361
405
  github_repository_id: repositoryId,
362
- github_repository_id_status: flags.repository_id ? "provided" : "verified",
406
+ github_repository_id_status: repositoryIdProvided ? "provided" : "verified",
363
407
  workflow_path: workflowPath,
364
408
  manifest_path: manifest,
365
409
  run402_version: RUN402_VERSION,
366
410
  delegation_chain_id: DEFAULT_CI_DELEGATION_CHAIN_ID,
367
- bootstrap_caveat: "Commit the generated workflow and manifest before expecting GitHub Actions deploys.",
411
+ bootstrap_caveat: workflowPath
412
+ ? "Commit the generated workflow and manifest before expecting GitHub Actions deploys."
413
+ : "No workflow was written (--no-workflow): the repository's existing workflow deploys once it runs run402 deploy with permissions: id-token: write; re-run it.",
368
414
  consent_summary: [
369
415
  "This binding lets matching GitHub Actions workflows deploy site, function, and database changes to the project.",
370
416
  outputRouteScopes.length > 0
package/lib/deploy-v2.mjs CHANGED
@@ -1501,7 +1501,7 @@ const CI_DEPLOY_ERROR_GUIDANCE = {
1501
1501
  ],
1502
1502
  },
1503
1503
  access_denied: {
1504
- hint: "The OIDC token was valid, but no active Run402 CI binding allowed this workflow.",
1504
+ hint: "No active binding on this project matches this workflow's OIDC subject (details.subject); next_actions names the command that links it.",
1505
1505
  next_actions: [
1506
1506
  editRequestAction("run402 ci list --project <prj_...>", "Inspect active CI bindings for this project."),
1507
1507
  editRequestAction("run402 ci link github", "Link this repository, branch, and environment to Run402."),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "run402",
3
- "version": "4.130.4",
3
+ "version": "4.131.0",
4
4
  "description": "CLI for Run402 — full-stack backend infrastructure for AI agents: Postgres, auth, storage, serverless functions and atomic deploys. Paid with x402/MPP. Includes $0.03 image generation.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "surface_version": "4.130.4",
2
+ "surface_version": "4.131.0",
3
3
  "verbs": [
4
4
  "repos create",
5
5
  "repos list",