@mediamonks/canopy-cli 0.7.45 → 0.7.47
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/dist/{ai-FBHTSCLZ.mjs → ai-CWPQiUft.mjs} +2 -2
- package/dist/{api-Bl6SG8Oq.mjs → api-DmJrIsE7.mjs} +134 -19
- package/dist/audience-B68YZBGy.mjs +121 -0
- package/dist/{browse-B8-0dweq.mjs → browse-BPLWhoFI.mjs} +2 -2
- package/dist/{db-BJJGydgL.mjs → db-DpCMc0uz.mjs} +4 -4
- package/dist/{deploy-ExrIeX_m.mjs → deploy-BnRQusjJ.mjs} +51 -5
- package/dist/{dev-D-vwE8G-.mjs → dev-BmKeIdMi.mjs} +7 -7
- package/dist/dev-BvVlq9np.mjs +2 -0
- package/dist/docs/ci.md +112 -50
- package/dist/docs/cli-audience.md +47 -0
- package/dist/docs/cli-create.md +2 -0
- package/dist/docs/cli-deploy.md +1 -1
- package/dist/docs/cli-repo.md +34 -2
- package/dist/docs/cli-token.md +8 -6
- package/dist/docs/cli.md +6 -2
- package/dist/docs/deploy.md +3 -2
- package/dist/docs/index.json +8 -3
- package/dist/docs/platform-deploys-and-rollbacks.md +4 -4
- package/dist/docs/platform-members-and-access.md +20 -0
- package/dist/{docs-Cpky2XNP.mjs → docs-D5gmZRJk.mjs} +4 -4
- package/dist/{egress-C2amvqmE.mjs → egress-BM4EpLBh.mjs} +3 -3
- package/dist/{feedback-CTV6inRg.mjs → feedback-C6kiYRro.mjs} +5 -5
- package/dist/index.mjs +36 -35
- package/dist/{local-BThs382Y.mjs → local-C9Bi9f3y.mjs} +2 -2
- package/dist/{login-CREcZfLH.mjs → login-2MW9sGJO.mjs} +3 -3
- package/dist/{logs-BnrNZvLc.mjs → logs-dZMPTJLu.mjs} +3 -3
- package/dist/{mcp-Cy8EwhhD.mjs → mcp-DuCxfb85.mjs} +3 -3
- package/dist/{migrations-D8NqjC5I.mjs → migrations-BReMlGWP.mjs} +2 -2
- package/dist/{open-Bpz4C2Sr.mjs → open-DcQSdfx5.mjs} +2 -2
- package/dist/{package-Dph4v1Cq.mjs → package-BksVeJZB.mjs} +1 -1
- package/dist/{project-template-Cdr1uwwE.mjs → project-template-BPW_Dgxj.mjs} +3 -3
- package/dist/project-template-Dw11151p.mjs +2 -0
- package/dist/{projects-BRG2HnBs.mjs → projects-CnU3194_.mjs} +26 -8
- package/dist/{rename-BGKa25Rq.mjs → rename-BWvERJgA.mjs} +3 -3
- package/dist/{repo-CwT2RDoW.mjs → repo-6a7gRQaf.mjs} +171 -8
- package/dist/repo-C7AXDtFZ.mjs +2 -0
- package/dist/{roles-C7pq9bIx.mjs → roles-CrYf_8wk.mjs} +1 -1
- package/dist/{roles-DAPPXf4U.mjs → roles-S2D9Id0W.mjs} +3 -3
- package/dist/{schedule-BcqBspn3.mjs → schedule-DvHSSzfR.mjs} +3 -3
- package/dist/{secrets-BjbIi7Gp.mjs → secrets-BIn2RPoe.mjs} +3 -3
- package/dist/{site-0i7wAJqg.mjs → site-BL5Ik-yj.mjs} +50 -1
- package/dist/{skill-Zj19RNo2.mjs → skill-Bz39NhTN.mjs} +2 -2
- package/dist/{staging-_2Js0Fkc.mjs → staging-Dn8kH9cK.mjs} +3 -3
- package/dist/templates/spa/.github/workflows/canopy-deploy.yml +18 -12
- package/dist/templates/spa/README.md +1 -1
- package/dist/templates/worker/.github/workflows/canopy-deploy.yml +18 -12
- package/dist/templates/worker/README.md +1 -1
- package/dist/{token-7vSNuz-X.mjs → token-DMCj88A-.mjs} +6 -6
- package/dist/{ui-CLnICOne.mjs → ui-26ETk1n8.mjs} +1 -1
- package/dist/{upgrade-B9tCAbre.mjs → upgrade-BjDaghNf.mjs} +2 -2
- package/dist/{usage-DGGmTwjJ.mjs → usage-Bpz5B1N1.mjs} +3 -3
- package/package.json +1 -1
- package/dist/dev-CSimnUP3.mjs +0 -2
- package/dist/project-template-715H_lnu.mjs +0 -2
- package/dist/repo-DmFs6H9m.mjs +0 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { a as apiRequest,
|
|
2
|
-
import { r as reportErrors } from "./ui-
|
|
1
|
+
import { a as apiRequest, m as withContext } from "./api-DmJrIsE7.mjs";
|
|
2
|
+
import { r as reportErrors } from "./ui-26ETk1n8.mjs";
|
|
3
3
|
import { defineCommand } from "citty";
|
|
4
4
|
//#region src/ai.ts
|
|
5
5
|
/**
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { f as rewriteCanopyJsonSlug, m as tryReadCanopyJson, r as siteConfig, t as projectDir } from "./site-BL5Ik-yj.mjs";
|
|
2
2
|
//#region src/api.ts
|
|
3
3
|
/**
|
|
4
4
|
* Authenticated requests to the Canopy control plane. Credentials, in order:
|
|
5
5
|
* CANOPY_DEPLOY_TOKEN (CI — a project-scoped deploy token, docs/02-decisions.md
|
|
6
|
-
* D11), then
|
|
6
|
+
* D11), then — inside GitHub Actions — a short-lived deploy token exchanged
|
|
7
|
+
* for the run's GitHub identity (OpenSpec platform-oauth-oidc), then the CLI
|
|
8
|
+
* token from `canopy login` (~/.canopy/config.json).
|
|
7
9
|
*/
|
|
8
10
|
/**
|
|
9
11
|
* Human copy for the error codes the control plane returns (apps/api): `why`
|
|
@@ -192,6 +194,38 @@ const ERROR_COPY = {
|
|
|
192
194
|
why: "the secret was saved but redeploying the running worker with it failed",
|
|
193
195
|
fix: "Retry, or redeploy the project (`canopy deploy`) to apply it."
|
|
194
196
|
},
|
|
197
|
+
repository_not_linked: {
|
|
198
|
+
why: "the project is not linked to a GitHub repository",
|
|
199
|
+
fix: "Link it with `canopy repo link <owner/name>` (owner only), or set CANOPY_DEPLOY_TOKEN."
|
|
200
|
+
},
|
|
201
|
+
ci_deploys_off: {
|
|
202
|
+
why: "CI deploys are turned off for this project",
|
|
203
|
+
fix: "Turn them back on with `canopy repo deploys on` (owner only)."
|
|
204
|
+
},
|
|
205
|
+
repository_mismatch: {
|
|
206
|
+
why: "this run's repository is not the one linked to the project",
|
|
207
|
+
fix: "Run the workflow from the linked repository, or link this one with `canopy repo link` (`canopy repo show` names the current link)."
|
|
208
|
+
},
|
|
209
|
+
ref_not_allowed: {
|
|
210
|
+
why: "this run's branch is neither the project's live branch nor its staging branch",
|
|
211
|
+
fix: "Deploy from the linked branch, or change it with `canopy repo branches --ref <ref>` (`canopy repo show` names both)."
|
|
212
|
+
},
|
|
213
|
+
invalid_id_token: {
|
|
214
|
+
why: "the platform did not accept this run's GitHub identity — it comes from another GitHub host than the linked repository's, or was minted for another Canopy site",
|
|
215
|
+
fix: "Check `canopy repo show` names this repository's host, and that the workflow deploys to the site canopy.json pins."
|
|
216
|
+
},
|
|
217
|
+
channel_not_allowed: {
|
|
218
|
+
why: "this run is on the project's staging branch, which only deploys to staging",
|
|
219
|
+
fix: "Deploy, promote or roll back live from the live branch, or by hand after `canopy login`."
|
|
220
|
+
},
|
|
221
|
+
invalid_repository: {
|
|
222
|
+
why: "the repository must be written as owner/name",
|
|
223
|
+
fix: "Pass it like `canopy repo link monks/my-tool` (or `monks.ghe.com/monks/my-tool` to name the host)."
|
|
224
|
+
},
|
|
225
|
+
invalid_ref: {
|
|
226
|
+
why: "branches are full git refs, and the staging branch must differ from the live one",
|
|
227
|
+
fix: "Pass e.g. `--ref refs/heads/main --staging-ref refs/heads/develop`."
|
|
228
|
+
},
|
|
195
229
|
slug_unchanged: {
|
|
196
230
|
why: "the project already has that slug",
|
|
197
231
|
fix: "Pass the new slug you want, e.g. `canopy rename my-new-name`."
|
|
@@ -369,31 +403,112 @@ async function withContext(request, context) {
|
|
|
369
403
|
throw err;
|
|
370
404
|
}
|
|
371
405
|
}
|
|
372
|
-
/**
|
|
373
|
-
|
|
406
|
+
/**
|
|
407
|
+
* Resolve the bearer token: CI deploy token first (D11), then the GitHub
|
|
408
|
+
* Actions exchange on a runner that may request an identity token, then the
|
|
409
|
+
* login token. The exchange comes before the "Not logged in" check because a
|
|
410
|
+
* runner has no ~/.canopy/config.json.
|
|
411
|
+
*/
|
|
412
|
+
async function credentials() {
|
|
374
413
|
const config = siteConfig();
|
|
375
414
|
const deployToken = process.env["CANOPY_DEPLOY_TOKEN"];
|
|
376
|
-
|
|
377
|
-
|
|
415
|
+
if (deployToken) return {
|
|
416
|
+
apiUrl: config.apiUrl,
|
|
417
|
+
token: deployToken,
|
|
418
|
+
isDeployToken: true,
|
|
419
|
+
kind: "deploy-token"
|
|
420
|
+
};
|
|
421
|
+
if (onGithubActions()) {
|
|
422
|
+
if (process.env["ACTIONS_ID_TOKEN_REQUEST_URL"] || !config.token) return {
|
|
423
|
+
...await githubActionsCredentials(),
|
|
424
|
+
isDeployToken: true,
|
|
425
|
+
kind: "github-actions"
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
if (!config.token) throw new Error("Not logged in — run `canopy login` first.");
|
|
378
429
|
return {
|
|
379
430
|
apiUrl: config.apiUrl,
|
|
380
|
-
token,
|
|
381
|
-
isDeployToken:
|
|
431
|
+
token: config.token,
|
|
432
|
+
isDeployToken: false,
|
|
433
|
+
kind: "login"
|
|
382
434
|
};
|
|
383
435
|
}
|
|
436
|
+
/** Running inside a GitHub Actions job (the runner sets GITHUB_ACTIONS=true). */
|
|
437
|
+
function onGithubActions(env = process.env) {
|
|
438
|
+
return Boolean(env["GITHUB_ACTIONS"]);
|
|
439
|
+
}
|
|
440
|
+
let exchange;
|
|
441
|
+
/**
|
|
442
|
+
* Trade this GitHub Actions run's OIDC token for a short-lived Canopy deploy
|
|
443
|
+
* token (design D-I). Once per process: every later request reuses it, and a
|
|
444
|
+
* failure stays a failure. The audience is the resolved site's API origin, so
|
|
445
|
+
* the token cannot be replayed on another Canopy site (D-B). `slug` is the
|
|
446
|
+
* project to deploy; it defaults to canopy.json's.
|
|
447
|
+
*/
|
|
448
|
+
function githubActionsCredentials(slug) {
|
|
449
|
+
exchange ??= exchangeGithubIdentity(slug);
|
|
450
|
+
return exchange;
|
|
451
|
+
}
|
|
452
|
+
async function exchangeGithubIdentity(slug) {
|
|
453
|
+
const { apiUrl } = siteConfig();
|
|
454
|
+
const project = slug ?? tryReadCanopyJson(projectDir())?.slug;
|
|
455
|
+
if (!project) throw new Error("No project to deploy — run from a directory with a canopy.json naming its slug, or pass --project.");
|
|
456
|
+
const requestUrl = process.env["ACTIONS_ID_TOKEN_REQUEST_URL"];
|
|
457
|
+
const requestToken = process.env["ACTIONS_ID_TOKEN_REQUEST_TOKEN"];
|
|
458
|
+
if (!requestUrl || !requestToken) throw new Error("This GitHub Actions job cannot prove which repository it runs from. Add `permissions: id-token: write` (and `contents: read`) to the workflow or job, or set CANOPY_DEPLOY_TOKEN.");
|
|
459
|
+
const audience = new URL(apiUrl).origin;
|
|
460
|
+
const oidcUrl = new URL(requestUrl);
|
|
461
|
+
oidcUrl.searchParams.set("audience", audience);
|
|
462
|
+
const oidc = await fetch(oidcUrl, { headers: { authorization: `Bearer ${requestToken}` } });
|
|
463
|
+
const idToken = oidc.ok ? (await oidc.json()).value : void 0;
|
|
464
|
+
if (!idToken) throw new Error(`GitHub did not issue an identity token for this run (HTTP ${oidc.status}). Check the workflow has \`permissions: id-token: write\`.`);
|
|
465
|
+
const path = "/auth/github-actions";
|
|
466
|
+
const response = await fetch(`${apiUrl}/control/v1${path}`, {
|
|
467
|
+
method: "POST",
|
|
468
|
+
headers: {
|
|
469
|
+
...commonHeaders(),
|
|
470
|
+
"content-type": "application/json"
|
|
471
|
+
},
|
|
472
|
+
body: JSON.stringify({
|
|
473
|
+
id_token: idToken,
|
|
474
|
+
project
|
|
475
|
+
})
|
|
476
|
+
});
|
|
477
|
+
const payload = await response.json().catch(() => ({}));
|
|
478
|
+
if (!response.ok || !payload.token) {
|
|
479
|
+
const err = new ApiError(response.status, payload.error ?? `http_${response.status}`, "POST", path, payload.detail, response.headers.get("x-canopy-request-id") ?? void 0);
|
|
480
|
+
err.context = { action: `deploy "${project}" from GitHub Actions` };
|
|
481
|
+
throw err;
|
|
482
|
+
}
|
|
483
|
+
const current = payload.slug ?? project;
|
|
484
|
+
if (current !== project) followRename(response, project);
|
|
485
|
+
return {
|
|
486
|
+
apiUrl,
|
|
487
|
+
token: payload.token,
|
|
488
|
+
slug: current,
|
|
489
|
+
channel: payload.channel === "staging" ? "staging" : "live",
|
|
490
|
+
expiresAt: payload.expires_at ?? ""
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
/** The 401 fix line, by what the request carried. */
|
|
494
|
+
const UNAUTHENTICATED_FIX = {
|
|
495
|
+
"deploy-token": "The deploy token was rejected — it may have been revoked. Create a new one in the dashboard (Settings → Deploy tokens) or with `canopy token create`.",
|
|
496
|
+
"github-actions": "This run's short-lived deploy token was refused — CI deploys may have been turned off or the link changed while it ran. Check `canopy repo show`, then re-run the workflow.",
|
|
497
|
+
login: "Your session has expired — run `canopy login` again."
|
|
498
|
+
};
|
|
384
499
|
/** Turn a non-2xx response into a thrown ApiError (friendlier copy on 401). */
|
|
385
|
-
async function fail(response,
|
|
500
|
+
async function fail(response, kind, method, path) {
|
|
386
501
|
const payload = await response.json().catch(() => ({}));
|
|
387
502
|
const code = payload.error ?? (response.status === 401 ? "unauthenticated" : `http_${response.status}`);
|
|
388
503
|
const err = new ApiError(response.status, code, method, path, payload.detail, response.headers.get("x-canopy-request-id") ?? void 0);
|
|
389
504
|
if (response.status === 401) err.context = {
|
|
390
505
|
action: "authenticate with Canopy",
|
|
391
|
-
fixes: { [code]:
|
|
506
|
+
fixes: { [code]: UNAUTHENTICATED_FIX[kind] }
|
|
392
507
|
};
|
|
393
508
|
throw err;
|
|
394
509
|
}
|
|
395
510
|
async function apiRequest(method, path, body, rawContentType, headers) {
|
|
396
|
-
const { apiUrl, token,
|
|
511
|
+
const { apiUrl, token, kind } = await credentials();
|
|
397
512
|
const isForm = body instanceof FormData;
|
|
398
513
|
const response = await fetch(`${apiUrl}/control/v1${path}`, {
|
|
399
514
|
method,
|
|
@@ -406,7 +521,7 @@ async function apiRequest(method, path, body, rawContentType, headers) {
|
|
|
406
521
|
body: body === void 0 ? void 0 : isForm ? body : rawContentType !== void 0 ? body : JSON.stringify(body)
|
|
407
522
|
});
|
|
408
523
|
followRename(response, slugInPath(path));
|
|
409
|
-
if (!response.ok) return fail(response,
|
|
524
|
+
if (!response.ok) return fail(response, kind, method, path);
|
|
410
525
|
if (response.status === 204) return void 0;
|
|
411
526
|
return await response.json();
|
|
412
527
|
}
|
|
@@ -415,7 +530,7 @@ async function apiRequest(method, path, body, rawContentType, headers) {
|
|
|
415
530
|
* the bytes itself, so `contentType` is a hint, not a claim it trusts.
|
|
416
531
|
*/
|
|
417
532
|
async function apiUpload(path, bytes, contentType) {
|
|
418
|
-
const { apiUrl, token,
|
|
533
|
+
const { apiUrl, token, kind } = await credentials();
|
|
419
534
|
const response = await fetch(`${apiUrl}/control/v1${path}`, {
|
|
420
535
|
method: "POST",
|
|
421
536
|
headers: {
|
|
@@ -426,18 +541,18 @@ async function apiUpload(path, bytes, contentType) {
|
|
|
426
541
|
body: bytes
|
|
427
542
|
});
|
|
428
543
|
followRename(response, slugInPath(path));
|
|
429
|
-
if (!response.ok) return fail(response,
|
|
544
|
+
if (!response.ok) return fail(response, kind, "POST", path);
|
|
430
545
|
return await response.json();
|
|
431
546
|
}
|
|
432
547
|
/** Authenticated GET returning raw bytes (e.g. a deploy zip). */
|
|
433
548
|
async function apiDownload(path) {
|
|
434
|
-
const { apiUrl, token,
|
|
549
|
+
const { apiUrl, token, kind } = await credentials();
|
|
435
550
|
const response = await fetch(`${apiUrl}/control/v1${path}`, { headers: {
|
|
436
551
|
...commonHeaders(),
|
|
437
552
|
authorization: `Bearer ${token}`
|
|
438
553
|
} });
|
|
439
554
|
followRename(response, slugInPath(path));
|
|
440
|
-
if (!response.ok) return fail(response,
|
|
555
|
+
if (!response.ok) return fail(response, kind, "GET", path);
|
|
441
556
|
return Buffer.from(await response.arrayBuffer());
|
|
442
557
|
}
|
|
443
558
|
/**
|
|
@@ -446,7 +561,7 @@ async function apiDownload(path) {
|
|
|
446
561
|
* re-vouches it (docs/10-cli-sites.md D17).
|
|
447
562
|
*/
|
|
448
563
|
async function apiDataRequest(method, path, projectSlug, body, rawContentType, channel) {
|
|
449
|
-
const { apiUrl, token,
|
|
564
|
+
const { apiUrl, token, kind } = await credentials();
|
|
450
565
|
const response = await fetch(`${apiUrl}/data/v1${path}`, {
|
|
451
566
|
method,
|
|
452
567
|
headers: {
|
|
@@ -459,9 +574,9 @@ async function apiDataRequest(method, path, projectSlug, body, rawContentType, c
|
|
|
459
574
|
body: body === void 0 ? void 0 : rawContentType !== void 0 ? body : JSON.stringify(body)
|
|
460
575
|
});
|
|
461
576
|
followRename(response, projectSlug);
|
|
462
|
-
if (!response.ok) return fail(response,
|
|
577
|
+
if (!response.ok) return fail(response, kind, method, path);
|
|
463
578
|
if (response.status === 204) return void 0;
|
|
464
579
|
return await response.json();
|
|
465
580
|
}
|
|
466
581
|
//#endregion
|
|
467
|
-
export { apiRequest as a, credentials as c,
|
|
582
|
+
export { apiRequest as a, credentials as c, githubActionsCredentials as d, noticeRenamesOnly as f, apiDownload as i, currentSlug as l, withContext as m, agentHeaders as n, apiUpload as o, onGithubActions as p, apiDataRequest as r, commonHeaders as s, ApiError as t, followRename as u };
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { c as projectDirArgs, d as resolveProject, l as projectFlagArgs } from "./site-BL5Ik-yj.mjs";
|
|
2
|
+
import { a as apiRequest, m as withContext } from "./api-DmJrIsE7.mjs";
|
|
3
|
+
import { r as reportErrors } from "./ui-26ETk1n8.mjs";
|
|
4
|
+
import { defineCommand } from "citty";
|
|
5
|
+
import * as p from "@clack/prompts";
|
|
6
|
+
//#region src/audience.ts
|
|
7
|
+
/**
|
|
8
|
+
* Project audience (OpenSpec change project-audience): `canopy audience
|
|
9
|
+
* show|set|draft|ready`. Who a project is for and whether it's still a draft
|
|
10
|
+
* are labels on the dashboard's project list, stored on the platform (never
|
|
11
|
+
* in canopy.json) and set through the project update route.
|
|
12
|
+
*/
|
|
13
|
+
const KINDS = [
|
|
14
|
+
"everyone",
|
|
15
|
+
"personal",
|
|
16
|
+
"client",
|
|
17
|
+
"department"
|
|
18
|
+
];
|
|
19
|
+
const NAME_MAX = 40;
|
|
20
|
+
/** `set <kind> [name]` → the update body, or a sentence saying what's wrong. */
|
|
21
|
+
function parseAudience(kind, name) {
|
|
22
|
+
const k = kind.trim().toLowerCase();
|
|
23
|
+
if (!KINDS.includes(k)) return `"${kind}" is not an audience. Use everyone, personal, client <name> or department <name>.`;
|
|
24
|
+
if (k === "client" || k === "department") {
|
|
25
|
+
const n = name?.trim() ?? "";
|
|
26
|
+
if (!n) return `Name the ${k}: canopy audience set ${k} "<name>"`;
|
|
27
|
+
if (n.length > NAME_MAX) return `Keep the ${k} name to ${NAME_MAX} characters.`;
|
|
28
|
+
return {
|
|
29
|
+
kind: k,
|
|
30
|
+
name: n
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
if (name?.trim()) return `${k === "everyone" ? "Everyone" : "Personal"} takes no name.`;
|
|
34
|
+
return { kind: k };
|
|
35
|
+
}
|
|
36
|
+
/** One line for people: who it's for, and whether it's a draft. */
|
|
37
|
+
function describeAudience(project) {
|
|
38
|
+
const a = project.audience ?? null;
|
|
39
|
+
const who = a === null ? "is for everyone" : a.kind === "personal" ? "is a personal project" : `is for the ${a.name} ${a.kind === "client" ? "client team" : "department"}`;
|
|
40
|
+
return `${project.slug} ${who}${project.draft ? ", and is still a draft" : ""}.`;
|
|
41
|
+
}
|
|
42
|
+
const FIXES = { invalid_audience: "Use everyone, personal, client <name> or department <name> (names up to 40 characters)." };
|
|
43
|
+
function update(slug, body) {
|
|
44
|
+
return withContext(apiRequest("PATCH", `/projects/${slug}`, body).then((r) => r.project), {
|
|
45
|
+
action: `update ${slug}`,
|
|
46
|
+
fixes: FIXES
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
const audienceCommand = defineCommand({
|
|
50
|
+
meta: {
|
|
51
|
+
name: "audience",
|
|
52
|
+
description: "Who the project is for, and whether it's a draft (show, set, draft, ready)"
|
|
53
|
+
},
|
|
54
|
+
subCommands: {
|
|
55
|
+
show: defineCommand({
|
|
56
|
+
meta: {
|
|
57
|
+
name: "show",
|
|
58
|
+
description: "Show who the project is for and whether it's a draft"
|
|
59
|
+
},
|
|
60
|
+
args: { ...projectDirArgs },
|
|
61
|
+
run: reportErrors("audience show", async ({ args }) => {
|
|
62
|
+
const { slug } = resolveProject(args);
|
|
63
|
+
const { project } = await withContext(apiRequest("GET", `/projects/${slug}`), { action: `read ${slug}` });
|
|
64
|
+
console.log(describeAudience(project));
|
|
65
|
+
})
|
|
66
|
+
}),
|
|
67
|
+
set: defineCommand({
|
|
68
|
+
meta: {
|
|
69
|
+
name: "set",
|
|
70
|
+
description: "Say who it's for: everyone, personal, client <name> or department <name>"
|
|
71
|
+
},
|
|
72
|
+
args: {
|
|
73
|
+
kind: {
|
|
74
|
+
type: "positional",
|
|
75
|
+
description: "everyone | personal | client | department",
|
|
76
|
+
required: true
|
|
77
|
+
},
|
|
78
|
+
name: {
|
|
79
|
+
type: "positional",
|
|
80
|
+
description: "The client team or department (client and department only)",
|
|
81
|
+
required: false
|
|
82
|
+
},
|
|
83
|
+
...projectFlagArgs
|
|
84
|
+
},
|
|
85
|
+
run: reportErrors("audience set", async ({ args }) => {
|
|
86
|
+
const audience = parseAudience(args.kind, args.name);
|
|
87
|
+
if (typeof audience === "string") {
|
|
88
|
+
console.error(audience);
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
const { slug } = resolveProject({ project: args.project });
|
|
92
|
+
p.log.success(describeAudience(await update(slug, { audience })));
|
|
93
|
+
p.log.info("It's a label on the project list; who can open the project is unchanged.");
|
|
94
|
+
})
|
|
95
|
+
}),
|
|
96
|
+
draft: defineCommand({
|
|
97
|
+
meta: {
|
|
98
|
+
name: "draft",
|
|
99
|
+
description: "Mark it as not ready yet (greyed out on the list)"
|
|
100
|
+
},
|
|
101
|
+
args: { ...projectDirArgs },
|
|
102
|
+
run: reportErrors("audience draft", async ({ args }) => {
|
|
103
|
+
const { slug } = resolveProject(args);
|
|
104
|
+
p.log.success(describeAudience(await update(slug, { draft: true })));
|
|
105
|
+
})
|
|
106
|
+
}),
|
|
107
|
+
ready: defineCommand({
|
|
108
|
+
meta: {
|
|
109
|
+
name: "ready",
|
|
110
|
+
description: "Mark it as ready (no longer a draft)"
|
|
111
|
+
},
|
|
112
|
+
args: { ...projectDirArgs },
|
|
113
|
+
run: reportErrors("audience ready", async ({ args }) => {
|
|
114
|
+
const { slug } = resolveProject(args);
|
|
115
|
+
p.log.success(describeAudience(await update(slug, { draft: false })));
|
|
116
|
+
})
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
//#endregion
|
|
121
|
+
export { audienceCommand };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { a as apiRequest, t as ApiError } from "./api-
|
|
1
|
+
import { h as tryResolveProject, r as siteConfig } from "./site-BL5Ik-yj.mjs";
|
|
2
|
+
import { a as apiRequest, t as ApiError } from "./api-DmJrIsE7.mjs";
|
|
3
3
|
import { defineCommand } from "citty";
|
|
4
4
|
//#region src/browse.ts
|
|
5
5
|
/**
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { a as apiRequest,
|
|
3
|
-
import { a as unwrap, i as requireTty, r as reportErrors } from "./ui-
|
|
4
|
-
import { n as migrateProject, r as readMigrationFiles, t as describeMigrateResult } from "./migrations-
|
|
1
|
+
import { d as resolveProject, l as projectFlagArgs } from "./site-BL5Ik-yj.mjs";
|
|
2
|
+
import { a as apiRequest, m as withContext, r as apiDataRequest } from "./api-DmJrIsE7.mjs";
|
|
3
|
+
import { a as unwrap, i as requireTty, r as reportErrors } from "./ui-26ETk1n8.mjs";
|
|
4
|
+
import { n as migrateProject, r as readMigrationFiles, t as describeMigrateResult } from "./migrations-BReMlGWP.mjs";
|
|
5
5
|
import { defineCommand } from "citty";
|
|
6
6
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
7
7
|
import * as p from "@clack/prompts";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { c as
|
|
2
|
-
import { a as apiRequest,
|
|
3
|
-
import { a as unwrap, r as reportErrors, t as formatDateTime } from "./ui-
|
|
4
|
-
import { n as migrateProject, r as readMigrationFiles, t as describeMigrateResult } from "./migrations-
|
|
1
|
+
import { c as projectDirArgs, d as resolveProject, l as projectFlagArgs, m as tryReadCanopyJson, p as stagingUrl, r as siteConfig, u as projectUrl } from "./site-BL5Ik-yj.mjs";
|
|
2
|
+
import { a as apiRequest, d as githubActionsCredentials, i as apiDownload, l as currentSlug, m as withContext, p as onGithubActions, r as apiDataRequest, t as ApiError } from "./api-DmJrIsE7.mjs";
|
|
3
|
+
import { a as unwrap, r as reportErrors, t as formatDateTime } from "./ui-26ETk1n8.mjs";
|
|
4
|
+
import { n as migrateProject, r as readMigrationFiles, t as describeMigrateResult } from "./migrations-BReMlGWP.mjs";
|
|
5
5
|
import { builtinModules } from "node:module";
|
|
6
6
|
import { defineCommand } from "citty";
|
|
7
7
|
import { mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
@@ -235,9 +235,18 @@ const deployCommand = defineCommand({
|
|
|
235
235
|
},
|
|
236
236
|
run: reportErrors("deploy", async ({ args }) => {
|
|
237
237
|
const { dir, slug } = resolveProject(args);
|
|
238
|
-
|
|
238
|
+
let channel = args.staging ? "staging" : "live";
|
|
239
239
|
const config = tryReadCanopyJson(dir) ?? {};
|
|
240
240
|
if (config.build && args.build) await runBuild(config.build, dir);
|
|
241
|
+
if (onGithubActions() && !process.env["CANOPY_DEPLOY_TOKEN"]) {
|
|
242
|
+
const exchanged = await exchangeOrSkip(slug);
|
|
243
|
+
if (!exchanged) return;
|
|
244
|
+
p.log.info(`Authenticated as this GitHub Actions run (${exchanged.slug}).`);
|
|
245
|
+
if (exchanged.channel === "staging" && channel === "live") {
|
|
246
|
+
channel = "staging";
|
|
247
|
+
p.log.info("This run is on the project's staging branch — deploying to the staging channel.");
|
|
248
|
+
}
|
|
249
|
+
}
|
|
241
250
|
const migrations = args.migrate ? readMigrationFiles(dir) : void 0;
|
|
242
251
|
if (migrations !== void 0 && config.dialect) try {
|
|
243
252
|
const { dialect } = await apiDataRequest("GET", "/platform", slug);
|
|
@@ -396,6 +405,43 @@ function bundleLimitError(files, limits) {
|
|
|
396
405
|
}
|
|
397
406
|
return null;
|
|
398
407
|
}
|
|
408
|
+
/**
|
|
409
|
+
* Exchange errors that mean "CI deploys are not on here": skip, don't fail.
|
|
410
|
+
* `unauthenticated` is a site whose API predates the exchange — its auth wall
|
|
411
|
+
* answers the unknown route; the exchange itself never says it (a bad token
|
|
412
|
+
* is `invalid_id_token`). A project scaffolded on such a site keeps the
|
|
413
|
+
* yellow skip the old secret-gated workflow gave it until the site catches up.
|
|
414
|
+
*/
|
|
415
|
+
const SKIP_CODES = /* @__PURE__ */ new Set([
|
|
416
|
+
"repository_not_linked",
|
|
417
|
+
"ci_deploys_off",
|
|
418
|
+
"unauthenticated"
|
|
419
|
+
]);
|
|
420
|
+
/**
|
|
421
|
+
* The run's deploy credentials, or null after announcing a skip. A project
|
|
422
|
+
* that is not linked, or whose CI deploys are off, leaves the run yellow
|
|
423
|
+
* rather than red: a workflow annotation saying why and how to change it, and
|
|
424
|
+
* exit 0 without deploying. Every other refusal is a failure.
|
|
425
|
+
*/
|
|
426
|
+
async function exchangeOrSkip(slug) {
|
|
427
|
+
try {
|
|
428
|
+
return await githubActionsCredentials(slug);
|
|
429
|
+
} catch (err) {
|
|
430
|
+
if (!(err instanceof ApiError) || !SKIP_CODES.has(err.code)) throw err;
|
|
431
|
+
const message = skipMessage(err.code, slug);
|
|
432
|
+
console.log(`::warning title=Canopy deploy skipped::${message}`);
|
|
433
|
+
p.log.warn(message);
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
function skipMessage(code, slug) {
|
|
438
|
+
if (code === "ci_deploys_off") return `CI deploys are off for "${slug}", so this run deployed nothing. The project owner can turn them back on with \`canopy repo deploys on\`.`;
|
|
439
|
+
if (code === "unauthenticated") return "This Canopy site doesn't take deploys from GitHub Actions without a token yet, so this run deployed nothing. Set a CANOPY_DEPLOY_TOKEN secret to deploy from CI.";
|
|
440
|
+
const repo = process.env["GITHUB_REPOSITORY"];
|
|
441
|
+
const server = process.env["GITHUB_SERVER_URL"];
|
|
442
|
+
const host = server ? new URL(server).host : void 0;
|
|
443
|
+
return `"${slug}" is not linked to this repository, so this run deployed nothing. The project owner can link it with \`canopy repo link ${repo ? `${host && host !== "github.com" ? `${host}/` : ""}${repo}` : "<owner/name>"}\`, or set a CANOPY_DEPLOY_TOKEN secret.`;
|
|
444
|
+
}
|
|
399
445
|
/** PUT each file to /data/v1/uploads/<grant> — the grant is the credential. */
|
|
400
446
|
async function uploadFiles(grant, files) {
|
|
401
447
|
const { apiUrl } = siteConfig();
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { a as apiRequest,
|
|
3
|
-
import { r as reportErrors, t as formatDateTime } from "./ui-
|
|
4
|
-
import { r as readMigrationFiles } from "./migrations-
|
|
1
|
+
import { c as projectDirArgs, d as resolveProject, m as tryReadCanopyJson, r as siteConfig, s as projectDialect } from "./site-BL5Ik-yj.mjs";
|
|
2
|
+
import { a as apiRequest, m as withContext, t as ApiError } from "./api-DmJrIsE7.mjs";
|
|
3
|
+
import { r as reportErrors, t as formatDateTime } from "./ui-26ETk1n8.mjs";
|
|
4
|
+
import { r as readMigrationFiles } from "./migrations-BReMlGWP.mjs";
|
|
5
5
|
import { i as stampWorkerHeaders, n as WORKER_ATTEST_HEADER } from "./src-8vxOBOxW.mjs";
|
|
6
|
-
import { a as parseAppRoleFlag } from "./roles-
|
|
6
|
+
import { a as parseAppRoleFlag } from "./roles-S2D9Id0W.mjs";
|
|
7
7
|
import { n as writeDevServerRecord } from "./dev-server-file-Dk9pk4AR.mjs";
|
|
8
8
|
import { createRequire } from "node:module";
|
|
9
9
|
import { defineCommand } from "citty";
|
|
@@ -325,7 +325,7 @@ async function devRemote(flags, slug) {
|
|
|
325
325
|
async function devLocal(flags, slug) {
|
|
326
326
|
let dump;
|
|
327
327
|
if (flags.pull) {
|
|
328
|
-
const { fetchProjectDump } = await import("./local-
|
|
328
|
+
const { fetchProjectDump } = await import("./local-C9Bi9f3y.mjs");
|
|
329
329
|
console.log(`Pulling a fresh export of ${slug}${flags.staging ? " [staging]" : ""}…`);
|
|
330
330
|
dump = await fetchProjectDump(slug, flags.staging ? "staging" : "live");
|
|
331
331
|
}
|
|
@@ -342,7 +342,7 @@ async function devLocal(flags, slug) {
|
|
|
342
342
|
}
|
|
343
343
|
const workerAttest = randomBytes(24).toString("base64url");
|
|
344
344
|
const devRoles = await resolveDevRoles(slug, flags.asRoles);
|
|
345
|
-
const { createSandbox } = await import("./local-
|
|
345
|
+
const { createSandbox } = await import("./local-C9Bi9f3y.mjs");
|
|
346
346
|
const sandbox = await createSandbox({
|
|
347
347
|
slug,
|
|
348
348
|
persistDir: join(flags.dir, ".canopy", "local"),
|