run402 1.54.4 → 1.56.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/README.md +38 -0
- package/cli.mjs +7 -0
- package/core-dist/allowance-auth.js +42 -22
- package/lib/ci.mjs +395 -0
- package/lib/deploy-v2.mjs +200 -12
- package/lib/deploy.mjs +49 -7
- package/lib/sdk.mjs +2 -2
- package/lib/secrets.mjs +15 -7
- package/package.json +1 -1
- package/sdk/core-dist/allowance-auth.js +42 -22
- package/sdk/dist/ci-credentials.d.ts +22 -0
- package/sdk/dist/ci-credentials.d.ts.map +1 -0
- package/sdk/dist/ci-credentials.js +103 -0
- package/sdk/dist/ci-credentials.js.map +1 -0
- package/sdk/dist/index.d.ts +7 -1
- package/sdk/dist/index.d.ts.map +1 -1
- package/sdk/dist/index.js +5 -0
- package/sdk/dist/index.js.map +1 -1
- package/sdk/dist/namespaces/apps.d.ts +11 -4
- package/sdk/dist/namespaces/apps.d.ts.map +1 -1
- package/sdk/dist/namespaces/apps.js +73 -9
- package/sdk/dist/namespaces/apps.js.map +1 -1
- package/sdk/dist/namespaces/ci.d.ts +21 -0
- package/sdk/dist/namespaces/ci.d.ts.map +1 -0
- package/sdk/dist/namespaces/ci.js +256 -0
- package/sdk/dist/namespaces/ci.js.map +1 -0
- package/sdk/dist/namespaces/ci.types.d.ts +91 -0
- package/sdk/dist/namespaces/ci.types.d.ts.map +1 -0
- package/sdk/dist/namespaces/ci.types.js +8 -0
- package/sdk/dist/namespaces/ci.types.js.map +1 -0
- package/sdk/dist/namespaces/deploy.d.ts.map +1 -1
- package/sdk/dist/namespaces/deploy.js +145 -30
- package/sdk/dist/namespaces/deploy.js.map +1 -1
- package/sdk/dist/namespaces/deploy.types.d.ts +24 -9
- package/sdk/dist/namespaces/deploy.types.d.ts.map +1 -1
- package/sdk/dist/namespaces/secrets.d.ts +3 -2
- package/sdk/dist/namespaces/secrets.d.ts.map +1 -1
- package/sdk/dist/namespaces/secrets.js +45 -5
- package/sdk/dist/namespaces/secrets.js.map +1 -1
- package/sdk/dist/node/ci.d.ts +12 -0
- package/sdk/dist/node/ci.d.ts.map +1 -0
- package/sdk/dist/node/ci.js +30 -0
- package/sdk/dist/node/ci.js.map +1 -0
- package/sdk/dist/node/index.d.ts +7 -2
- package/sdk/dist/node/index.d.ts.map +1 -1
- package/sdk/dist/node/index.js +3 -2
- package/sdk/dist/node/index.js.map +1 -1
- package/sdk/dist/type-contract.d.ts +2 -0
- package/sdk/dist/type-contract.d.ts.map +1 -0
- package/sdk/dist/type-contract.js +2 -0
- package/sdk/dist/type-contract.js.map +1 -0
package/README.md
CHANGED
|
@@ -62,6 +62,34 @@ run402 subdomains claim my-app # → my-app.run402.com (auto-reas
|
|
|
62
62
|
|
|
63
63
|
`deploy-dir` hashes each file client-side and only uploads bytes the gateway doesn't already have. Re-deploying an unchanged tree returns immediately with `bytes_uploaded: 0`. Progress events stream to stderr.
|
|
64
64
|
|
|
65
|
+
### GitHub Actions OIDC deploys
|
|
66
|
+
|
|
67
|
+
Link once from a local shell that has your Run402 allowance, then commit the generated workflow and manifest:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
run402 ci link github --project prj_... --manifest run402.deploy.json
|
|
71
|
+
run402 ci list --project prj_...
|
|
72
|
+
run402 ci revoke cib_...
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`link github` infers `owner/repo` and the current branch, verifies the numeric GitHub repository id, creates a deploy-scoped CI binding, and writes `.github/workflows/run402-deploy.yml` unless you pass `--workflow`. The generated workflow is intentionally just the existing deploy command with OIDC enabled:
|
|
76
|
+
|
|
77
|
+
```yaml
|
|
78
|
+
permissions:
|
|
79
|
+
contents: read
|
|
80
|
+
id-token: write
|
|
81
|
+
|
|
82
|
+
jobs:
|
|
83
|
+
deploy:
|
|
84
|
+
runs-on: ubuntu-latest
|
|
85
|
+
steps:
|
|
86
|
+
- uses: actions/checkout@v4
|
|
87
|
+
- name: Deploy to run402
|
|
88
|
+
run: npx --yes run402@1.54.4 deploy apply --manifest 'run402.deploy.json' --project 'prj_...'
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
CI deploys can ship `site`, `functions`, and `database` changes. Keep secrets, domains, subdomains, routes, checks, and non-current base changes in a local `run402 deploy apply` where the full allowance-backed authority is present.
|
|
92
|
+
|
|
65
93
|
### Storage (paste-and-go CDN assets)
|
|
66
94
|
|
|
67
95
|
```bash
|
|
@@ -91,6 +119,16 @@ import { db, adminDb, getUser, email, ai } from "@run402/functions";
|
|
|
91
119
|
|
|
92
120
|
`db(req)` is the caller-context client (RLS applies); `adminDb()` bypasses RLS for platform-authored writes.
|
|
93
121
|
|
|
122
|
+
### Secrets
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
run402 secrets set <id> OPENAI_API_KEY --file ./.secrets/openai-key
|
|
126
|
+
run402 secrets list <id>
|
|
127
|
+
run402 deploy apply --manifest run402.deploy.json # manifest uses secrets.require, not values
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Secret values are write-only. `list` returns keys and timestamps only; deploy manifests should declare dependencies with `secrets.require` and never contain values.
|
|
131
|
+
|
|
94
132
|
### Email
|
|
95
133
|
|
|
96
134
|
```bash
|
package/cli.mjs
CHANGED
|
@@ -26,6 +26,7 @@ Commands:
|
|
|
26
26
|
tier Manage tier subscription (status, set)
|
|
27
27
|
projects Manage projects (provision, list, query, inspect, delete)
|
|
28
28
|
deploy Deploy a full-stack app or static site (requires active tier)
|
|
29
|
+
ci Link GitHub Actions OIDC deploy bindings
|
|
29
30
|
functions Manage serverless functions (deploy, invoke, logs, list, delete)
|
|
30
31
|
secrets Manage project secrets (set, list, delete)
|
|
31
32
|
blob Direct-to-S3 blob storage (put, get, ls, rm, sign, diagnose) — up to 5 TiB
|
|
@@ -62,6 +63,7 @@ Getting started:
|
|
|
62
63
|
run402 init mpp Set up with MPP (Tempo Moderato)
|
|
63
64
|
run402 tier set prototype Subscribe to a tier
|
|
64
65
|
run402 deploy --manifest app.json
|
|
66
|
+
run402 ci link github --project prj_... --manifest run402.deploy.json
|
|
65
67
|
`;
|
|
66
68
|
|
|
67
69
|
if (cmd === '--version' || cmd === '-v') {
|
|
@@ -122,6 +124,11 @@ switch (cmd) {
|
|
|
122
124
|
await run([sub, ...rest].filter(Boolean));
|
|
123
125
|
break;
|
|
124
126
|
}
|
|
127
|
+
case "ci": {
|
|
128
|
+
const { run } = await import("./lib/ci.mjs");
|
|
129
|
+
await run(sub, rest);
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
125
132
|
case "functions": {
|
|
126
133
|
const { run } = await import("./lib/functions.mjs");
|
|
127
134
|
await run(sub, rest);
|
|
@@ -70,16 +70,44 @@ export function formatSIWEMessage(opts, address) {
|
|
|
70
70
|
opts.statement,
|
|
71
71
|
"",
|
|
72
72
|
`URI: ${opts.uri}`,
|
|
73
|
-
`Version: ${opts.version}`,
|
|
74
|
-
`Chain ID: ${opts.chainId}`,
|
|
73
|
+
`Version: ${opts.version ?? "1"}`,
|
|
74
|
+
`Chain ID: ${messageChainId(opts.chainId)}`,
|
|
75
75
|
`Nonce: ${opts.nonce}`,
|
|
76
76
|
`Issued At: ${opts.issuedAt}`,
|
|
77
77
|
];
|
|
78
78
|
if (opts.expirationTime) {
|
|
79
79
|
lines.push(`Expiration Time: ${opts.expirationTime}`);
|
|
80
80
|
}
|
|
81
|
+
if (opts.resources && opts.resources.length > 0) {
|
|
82
|
+
lines.push("Resources:");
|
|
83
|
+
for (const resource of opts.resources)
|
|
84
|
+
lines.push(`- ${resource}`);
|
|
85
|
+
}
|
|
81
86
|
return lines.join("\n");
|
|
82
87
|
}
|
|
88
|
+
export function buildSIWxAuthHeaders(opts) {
|
|
89
|
+
const message = formatSIWEMessage(opts, opts.allowance.address);
|
|
90
|
+
const signature = personalSign(opts.allowance.privateKey, opts.allowance.address, message);
|
|
91
|
+
const payload = {
|
|
92
|
+
domain: opts.domain,
|
|
93
|
+
address: toChecksumAddress(opts.allowance.address),
|
|
94
|
+
statement: opts.statement,
|
|
95
|
+
uri: opts.uri,
|
|
96
|
+
version: opts.version ?? "1",
|
|
97
|
+
chainId: payloadChainId(opts.chainId),
|
|
98
|
+
type: opts.type ?? "eip191",
|
|
99
|
+
nonce: opts.nonce,
|
|
100
|
+
issuedAt: opts.issuedAt,
|
|
101
|
+
expirationTime: opts.expirationTime,
|
|
102
|
+
signature,
|
|
103
|
+
};
|
|
104
|
+
if (opts.resources !== undefined) {
|
|
105
|
+
payload.resources = opts.resources;
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
"SIGN-IN-WITH-X": Buffer.from(JSON.stringify(payload)).toString("base64"),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
83
111
|
/**
|
|
84
112
|
* Get SIWX auth headers for the Run402 API.
|
|
85
113
|
* Returns null if no allowance is configured.
|
|
@@ -103,32 +131,24 @@ export function getAllowanceAuthHeaders(path, allowancePath) {
|
|
|
103
131
|
const now = new Date();
|
|
104
132
|
const issuedAt = now.toISOString();
|
|
105
133
|
const expirationTime = new Date(now.getTime() + 5 * 60 * 1000).toISOString();
|
|
106
|
-
|
|
134
|
+
return buildSIWxAuthHeaders({
|
|
135
|
+
allowance,
|
|
107
136
|
domain,
|
|
108
137
|
uri,
|
|
109
138
|
statement: "Sign in to Run402",
|
|
110
|
-
version: "1",
|
|
111
|
-
chainId: 84532, // Base Sepolia
|
|
112
|
-
nonce,
|
|
113
|
-
issuedAt,
|
|
114
|
-
expirationTime,
|
|
115
|
-
}, allowance.address);
|
|
116
|
-
const signature = personalSign(allowance.privateKey, allowance.address, message);
|
|
117
|
-
const payload = {
|
|
118
|
-
domain,
|
|
119
|
-
address: toChecksumAddress(allowance.address),
|
|
120
|
-
statement: "Sign in to Run402",
|
|
121
|
-
uri,
|
|
122
|
-
version: "1",
|
|
123
139
|
chainId: "eip155:84532",
|
|
124
|
-
type: "eip191",
|
|
125
140
|
nonce,
|
|
126
141
|
issuedAt,
|
|
127
142
|
expirationTime,
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
function messageChainId(chainId) {
|
|
146
|
+
if (typeof chainId === "number")
|
|
147
|
+
return String(chainId);
|
|
148
|
+
const match = /^eip155:(\d+)$/.exec(chainId);
|
|
149
|
+
return match ? match[1] : chainId;
|
|
150
|
+
}
|
|
151
|
+
function payloadChainId(chainId) {
|
|
152
|
+
return typeof chainId === "number" ? `eip155:${chainId}` : chainId;
|
|
133
153
|
}
|
|
134
154
|
//# sourceMappingURL=allowance-auth.js.map
|
package/lib/ci.mjs
ADDED
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { dirname, resolve } from "node:path";
|
|
5
|
+
import {
|
|
6
|
+
CI_GITHUB_ACTIONS_PROVIDER,
|
|
7
|
+
DEFAULT_CI_DELEGATION_CHAIN_ID,
|
|
8
|
+
V1_CI_ALLOWED_ACTIONS,
|
|
9
|
+
V1_CI_ALLOWED_EVENTS_DEFAULT,
|
|
10
|
+
signCiDelegation,
|
|
11
|
+
} from "#sdk/node";
|
|
12
|
+
import { API, resolveProjectId } from "./config.mjs";
|
|
13
|
+
import { getSdk } from "./sdk.mjs";
|
|
14
|
+
import { reportSdkError, fail } from "./sdk-errors.mjs";
|
|
15
|
+
|
|
16
|
+
const { version: RUN402_VERSION } = JSON.parse(
|
|
17
|
+
readFileSync(new URL("../package.json", import.meta.url), "utf8"),
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
const HELP = `run402 ci — Manage CI/OIDC deploy bindings
|
|
21
|
+
|
|
22
|
+
Usage:
|
|
23
|
+
run402 ci link github [--project <id>] [--manifest <path>] [--repo <owner/repo>] [--branch <name> | --environment <name>] [--repository-id <id>] [--workflow <path>] [--expires-at <iso>] [--force]
|
|
24
|
+
run402 ci list [--project <id>]
|
|
25
|
+
run402 ci revoke <binding_id>
|
|
26
|
+
|
|
27
|
+
Subcommands:
|
|
28
|
+
link github Link this repo/branch or environment for GitHub Actions deploys
|
|
29
|
+
list List CI bindings for a project
|
|
30
|
+
revoke Revoke a CI binding
|
|
31
|
+
`;
|
|
32
|
+
|
|
33
|
+
const SUB_HELP = {
|
|
34
|
+
link: `run402 ci link github — Link GitHub Actions OIDC for deploy apply
|
|
35
|
+
|
|
36
|
+
Usage:
|
|
37
|
+
run402 ci link github [--project <id>] [--manifest <path>] [--repo <owner/repo>] [--branch <name> | --environment <name>] [--repository-id <id>] [--workflow <path>] [--expires-at <iso>] [--force]
|
|
38
|
+
|
|
39
|
+
Options:
|
|
40
|
+
--project <id> Project ID (defaults to the active project)
|
|
41
|
+
--manifest <path> Manifest path used by the generated workflow (default: run402.deploy.json)
|
|
42
|
+
--repo <owner/repo> GitHub repo (default: inferred from origin remote)
|
|
43
|
+
--branch <name> Branch subject and push trigger (default: current branch)
|
|
44
|
+
--environment <name> GitHub environment subject; adds job.environment
|
|
45
|
+
--repository-id <id> Numeric GitHub repository id when API lookup is unavailable
|
|
46
|
+
--workflow <path> Workflow path (default: .github/workflows/run402-deploy.yml)
|
|
47
|
+
--expires-at <iso> Optional binding expiration timestamp
|
|
48
|
+
--force Overwrite an existing workflow file
|
|
49
|
+
|
|
50
|
+
Notes:
|
|
51
|
+
- v1 allows only push and workflow_dispatch events.
|
|
52
|
+
- v1 does not expose raw subject, wildcard, or pull-request deploy flags.
|
|
53
|
+
`,
|
|
54
|
+
list: `run402 ci list — List CI bindings
|
|
55
|
+
|
|
56
|
+
Usage:
|
|
57
|
+
run402 ci list [--project <id>]
|
|
58
|
+
`,
|
|
59
|
+
revoke: `run402 ci revoke — Revoke a CI binding
|
|
60
|
+
|
|
61
|
+
Usage:
|
|
62
|
+
run402 ci revoke <binding_id>
|
|
63
|
+
`,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
function parseFlags(args, allowed) {
|
|
67
|
+
const flags = {};
|
|
68
|
+
const positional = [];
|
|
69
|
+
for (let i = 0; i < args.length; i++) {
|
|
70
|
+
const arg = args[i];
|
|
71
|
+
if (!arg.startsWith("--")) {
|
|
72
|
+
positional.push(arg);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (!allowed.has(arg)) {
|
|
76
|
+
fail({
|
|
77
|
+
code: "BAD_USAGE",
|
|
78
|
+
message: `Unsupported flag: ${arg}`,
|
|
79
|
+
hint: "Run: run402 ci link github --help",
|
|
80
|
+
details: { flag: arg },
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
if (arg === "--force") {
|
|
84
|
+
flags.force = true;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (!args[i + 1] || args[i + 1].startsWith("--")) {
|
|
88
|
+
fail({
|
|
89
|
+
code: "BAD_USAGE",
|
|
90
|
+
message: `Missing value for ${arg}.`,
|
|
91
|
+
details: { flag: arg },
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
flags[arg.slice(2).replace(/-/g, "_")] = args[++i];
|
|
95
|
+
}
|
|
96
|
+
return { flags, positional };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function rejectHighRiskFlags(args) {
|
|
100
|
+
const blocked = [
|
|
101
|
+
"--subject",
|
|
102
|
+
"--subject-match",
|
|
103
|
+
"--wildcard",
|
|
104
|
+
"--allow-event",
|
|
105
|
+
"--event",
|
|
106
|
+
"--pull-request",
|
|
107
|
+
"--pr",
|
|
108
|
+
"--no-repository-id",
|
|
109
|
+
];
|
|
110
|
+
const hit = args.find((arg) => blocked.includes(arg));
|
|
111
|
+
if (hit) {
|
|
112
|
+
fail({
|
|
113
|
+
code: "UNSUPPORTED_CI_FLAG",
|
|
114
|
+
message: `${hit} is intentionally not exposed by run402 ci link github v1.`,
|
|
115
|
+
hint: "Use --branch or --environment. PR deploys, raw subjects, wildcards, and soft repository-id binding are deferred.",
|
|
116
|
+
details: { flag: hit },
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function git(args) {
|
|
122
|
+
try {
|
|
123
|
+
return execFileSync("git", args, {
|
|
124
|
+
cwd: process.cwd(),
|
|
125
|
+
encoding: "utf8",
|
|
126
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
127
|
+
}).trim();
|
|
128
|
+
} catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function inferRepo() {
|
|
134
|
+
const remote = git(["remote", "get-url", "origin"]);
|
|
135
|
+
if (!remote) return null;
|
|
136
|
+
return parseGithubRemote(remote);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function parseGithubRemote(remote) {
|
|
140
|
+
const cleaned = remote.trim().replace(/\.git$/, "");
|
|
141
|
+
const ssh = cleaned.match(/^git@github\.com:([^/]+)\/(.+)$/);
|
|
142
|
+
if (ssh) return `${ssh[1]}/${ssh[2]}`;
|
|
143
|
+
const https = cleaned.match(/^https:\/\/github\.com\/([^/]+)\/(.+)$/);
|
|
144
|
+
if (https) return `${https[1]}/${https[2]}`;
|
|
145
|
+
const sshUrl = cleaned.match(/^ssh:\/\/git@github\.com\/([^/]+)\/(.+)$/);
|
|
146
|
+
if (sshUrl) return `${sshUrl[1]}/${sshUrl[2]}`;
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function inferBranch() {
|
|
151
|
+
return git(["branch", "--show-current"]);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function fetchGithubRepositoryId(repo) {
|
|
155
|
+
const token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
|
|
156
|
+
const headers = {
|
|
157
|
+
Accept: "application/vnd.github+json",
|
|
158
|
+
"User-Agent": "run402-cli",
|
|
159
|
+
};
|
|
160
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
161
|
+
let res;
|
|
162
|
+
try {
|
|
163
|
+
res = await fetch(`https://api.github.com/repos/${repo}`, { headers });
|
|
164
|
+
} catch {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
const body = await res.json().catch(() => null);
|
|
168
|
+
if (!res.ok || typeof body?.id !== "number") return null;
|
|
169
|
+
return String(body.id);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function buildSubject(repo, { branch, environment }) {
|
|
173
|
+
if (environment) return `repo:${repo}:environment:${environment}`;
|
|
174
|
+
if (!branch) {
|
|
175
|
+
fail({
|
|
176
|
+
code: "CI_SUBJECT_REQUIRED",
|
|
177
|
+
message: "Could not infer a branch for the GitHub Actions subject.",
|
|
178
|
+
hint: "Pass --branch <name> or --environment <name>.",
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
return `repo:${repo}:ref:refs/heads/${branch}`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function shellQuote(value) {
|
|
185
|
+
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function yamlString(value) {
|
|
189
|
+
return JSON.stringify(String(value));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function generateWorkflow({ branch, environment, manifest, projectId }) {
|
|
193
|
+
const pushBlock = branch
|
|
194
|
+
? ` push:\n branches: [${yamlString(branch)}]\n`
|
|
195
|
+
: ` push:\n`;
|
|
196
|
+
const environmentLine = environment ? ` environment: ${yamlString(environment)}\n` : "";
|
|
197
|
+
return `name: Run402 Deploy
|
|
198
|
+
|
|
199
|
+
on:
|
|
200
|
+
${pushBlock} workflow_dispatch:
|
|
201
|
+
|
|
202
|
+
permissions:
|
|
203
|
+
contents: read
|
|
204
|
+
id-token: write
|
|
205
|
+
|
|
206
|
+
jobs:
|
|
207
|
+
deploy:
|
|
208
|
+
runs-on: ubuntu-latest
|
|
209
|
+
${environmentLine} steps:
|
|
210
|
+
- uses: actions/checkout@v4
|
|
211
|
+
- name: Deploy to run402
|
|
212
|
+
run: npx --yes run402@${RUN402_VERSION} deploy apply --manifest ${shellQuote(manifest)} --project ${shellQuote(projectId)}
|
|
213
|
+
`;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function linkGithub(args) {
|
|
217
|
+
rejectHighRiskFlags(args);
|
|
218
|
+
const { flags, positional } = parseFlags(args, new Set([
|
|
219
|
+
"--project",
|
|
220
|
+
"--manifest",
|
|
221
|
+
"--repo",
|
|
222
|
+
"--branch",
|
|
223
|
+
"--environment",
|
|
224
|
+
"--repository-id",
|
|
225
|
+
"--workflow",
|
|
226
|
+
"--expires-at",
|
|
227
|
+
"--force",
|
|
228
|
+
]));
|
|
229
|
+
if (positional[0] !== "github" || positional.length > 1) {
|
|
230
|
+
fail({
|
|
231
|
+
code: "BAD_USAGE",
|
|
232
|
+
message: "Missing provider: github.",
|
|
233
|
+
hint: "run402 ci link github [--project <id>]",
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
if (flags.branch && flags.environment) {
|
|
237
|
+
fail({
|
|
238
|
+
code: "BAD_USAGE",
|
|
239
|
+
message: "Choose either --branch or --environment, not both.",
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const projectId = resolveProjectId(flags.project);
|
|
244
|
+
const repo = flags.repo || inferRepo();
|
|
245
|
+
if (!repo || !/^[^/\s]+\/[^/\s]+$/.test(repo)) {
|
|
246
|
+
fail({
|
|
247
|
+
code: "GITHUB_REPO_REQUIRED",
|
|
248
|
+
message: "Could not infer GitHub owner/repo from git remote origin.",
|
|
249
|
+
hint: "Pass --repo <owner/repo>.",
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
const branch = flags.environment ? (flags.branch || inferBranch() || null) : (flags.branch || inferBranch());
|
|
253
|
+
const subject = buildSubject(repo, { branch, environment: flags.environment });
|
|
254
|
+
const repositoryId = flags.repository_id || await fetchGithubRepositoryId(repo);
|
|
255
|
+
if (!repositoryId) {
|
|
256
|
+
fail({
|
|
257
|
+
code: "GITHUB_REPOSITORY_ID_REQUIRED",
|
|
258
|
+
message: `Could not fetch the numeric GitHub repository id for ${repo}.`,
|
|
259
|
+
hint: "Set GITHUB_TOKEN/GH_TOKEN or pass --repository-id <id>.",
|
|
260
|
+
details: { repo },
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const workflowPath = flags.workflow || ".github/workflows/run402-deploy.yml";
|
|
265
|
+
const absWorkflowPath = resolve(process.cwd(), workflowPath);
|
|
266
|
+
if (existsSync(absWorkflowPath) && !flags.force) {
|
|
267
|
+
fail({
|
|
268
|
+
code: "WORKFLOW_EXISTS",
|
|
269
|
+
message: `Workflow already exists: ${workflowPath}`,
|
|
270
|
+
hint: "Pass --force to overwrite it.",
|
|
271
|
+
details: { workflow_path: workflowPath },
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const manifest = flags.manifest || "run402.deploy.json";
|
|
276
|
+
const nonce = randomBytes(16).toString("hex");
|
|
277
|
+
const values = {
|
|
278
|
+
project_id: projectId,
|
|
279
|
+
subject_match: subject,
|
|
280
|
+
allowed_actions: V1_CI_ALLOWED_ACTIONS,
|
|
281
|
+
allowed_events: V1_CI_ALLOWED_EVENTS_DEFAULT,
|
|
282
|
+
github_repository_id: repositoryId,
|
|
283
|
+
expires_at: flags.expires_at || null,
|
|
284
|
+
nonce,
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
let signedDelegation;
|
|
288
|
+
try {
|
|
289
|
+
signedDelegation = signCiDelegation(values, { apiBase: API });
|
|
290
|
+
} catch (err) {
|
|
291
|
+
fail({
|
|
292
|
+
code: "NO_ALLOWANCE",
|
|
293
|
+
message: err?.message || "No local allowance configured.",
|
|
294
|
+
hint: "Run: run402 init",
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const workflow = generateWorkflow({
|
|
299
|
+
branch,
|
|
300
|
+
environment: flags.environment || null,
|
|
301
|
+
manifest,
|
|
302
|
+
projectId,
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
try {
|
|
306
|
+
const binding = await getSdk({ disablePaidFetch: true }).ci.createBinding({
|
|
307
|
+
...values,
|
|
308
|
+
provider: CI_GITHUB_ACTIONS_PROVIDER,
|
|
309
|
+
signed_delegation: signedDelegation,
|
|
310
|
+
});
|
|
311
|
+
mkdirSync(dirname(absWorkflowPath), { recursive: true });
|
|
312
|
+
writeFileSync(absWorkflowPath, workflow, { encoding: "utf8", mode: 0o644 });
|
|
313
|
+
console.log(JSON.stringify({
|
|
314
|
+
status: "ok",
|
|
315
|
+
binding_id: binding.id,
|
|
316
|
+
project_id: projectId,
|
|
317
|
+
provider: CI_GITHUB_ACTIONS_PROVIDER,
|
|
318
|
+
subject_match: subject,
|
|
319
|
+
allowed_events: [...V1_CI_ALLOWED_EVENTS_DEFAULT],
|
|
320
|
+
allowed_actions: [...V1_CI_ALLOWED_ACTIONS],
|
|
321
|
+
github_repository_id: repositoryId,
|
|
322
|
+
github_repository_id_status: flags.repository_id ? "provided" : "verified",
|
|
323
|
+
workflow_path: workflowPath,
|
|
324
|
+
manifest_path: manifest,
|
|
325
|
+
run402_version: RUN402_VERSION,
|
|
326
|
+
delegation_chain_id: DEFAULT_CI_DELEGATION_CHAIN_ID,
|
|
327
|
+
bootstrap_caveat: "Commit the generated workflow and manifest before expecting GitHub Actions deploys.",
|
|
328
|
+
consent_summary: [
|
|
329
|
+
"This binding lets matching GitHub Actions workflows deploy site, function, and database changes to the project.",
|
|
330
|
+
"It does not allow direct secrets, domains, subdomains, lifecycle, billing, contracts, or faucet API calls.",
|
|
331
|
+
],
|
|
332
|
+
revocation_residuals: [
|
|
333
|
+
"Revocation stops future CI gateway requests.",
|
|
334
|
+
"Revocation does not undo already-deployed code, stop in-flight deploy operations, rotate exfiltrated keys, or remove deployed functions.",
|
|
335
|
+
],
|
|
336
|
+
}, null, 2));
|
|
337
|
+
} catch (err) {
|
|
338
|
+
reportSdkError(err);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async function list(args) {
|
|
343
|
+
const { flags } = parseFlags(args, new Set(["--project"]));
|
|
344
|
+
const project = resolveProjectId(flags.project);
|
|
345
|
+
try {
|
|
346
|
+
const result = await getSdk({ disablePaidFetch: true }).ci.listBindings({ project });
|
|
347
|
+
console.log(JSON.stringify({ status: "ok", project_id: project, ...result }, null, 2));
|
|
348
|
+
} catch (err) {
|
|
349
|
+
reportSdkError(err);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async function revoke(args) {
|
|
354
|
+
const bindingId = args.find((arg) => !arg.startsWith("--"));
|
|
355
|
+
if (!bindingId) {
|
|
356
|
+
fail({
|
|
357
|
+
code: "BAD_USAGE",
|
|
358
|
+
message: "Missing <binding_id>.",
|
|
359
|
+
hint: "run402 ci revoke <binding_id>",
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
try {
|
|
363
|
+
const binding = await getSdk({ disablePaidFetch: true }).ci.revokeBinding(bindingId);
|
|
364
|
+
console.log(JSON.stringify({
|
|
365
|
+
status: "ok",
|
|
366
|
+
binding,
|
|
367
|
+
revocation_residuals: [
|
|
368
|
+
"Revocation stops future CI gateway requests.",
|
|
369
|
+
"Revocation does not undo already-deployed code, stop in-flight deploy operations, rotate exfiltrated keys, or remove deployed functions.",
|
|
370
|
+
],
|
|
371
|
+
}, null, 2));
|
|
372
|
+
} catch (err) {
|
|
373
|
+
reportSdkError(err);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export async function run(sub, args) {
|
|
378
|
+
if (!sub || sub === "--help" || sub === "-h") {
|
|
379
|
+
console.log(HELP);
|
|
380
|
+
process.exit(0);
|
|
381
|
+
}
|
|
382
|
+
if (Array.isArray(args) && (args.includes("--help") || args.includes("-h"))) {
|
|
383
|
+
console.log(SUB_HELP[sub] || HELP);
|
|
384
|
+
process.exit(0);
|
|
385
|
+
}
|
|
386
|
+
switch (sub) {
|
|
387
|
+
case "link": await linkGithub(args); break;
|
|
388
|
+
case "list": await list(args); break;
|
|
389
|
+
case "revoke": await revoke(args); break;
|
|
390
|
+
default:
|
|
391
|
+
console.error(`Unknown subcommand: ${sub}\n`);
|
|
392
|
+
console.log(HELP);
|
|
393
|
+
process.exit(1);
|
|
394
|
+
}
|
|
395
|
+
}
|