@mediamonks/canopy-cli 0.7.45 → 0.7.46
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/{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-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 +3 -3
- package/dist/docs/platform-deploys-and-rollbacks.md +4 -4
- package/dist/{docs-Cpky2XNP.mjs → docs-CtMrMpt1.mjs} +4 -4
- package/dist/{egress-C2amvqmE.mjs → egress-BM4EpLBh.mjs} +3 -3
- package/dist/{feedback-CTV6inRg.mjs → feedback-BXhbxyXI.mjs} +5 -5
- package/dist/index.mjs +35 -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-CGmWkGN8.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-BdNHq8QV.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-vzcCD1b4.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-8_pFNTo5.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 };
|
|
@@ -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"),
|
package/dist/docs/ci.md
CHANGED
|
@@ -1,75 +1,137 @@
|
|
|
1
1
|
---
|
|
2
|
-
title: CI
|
|
3
|
-
description:
|
|
2
|
+
title: CI deploys
|
|
3
|
+
description: Deploy on every push from GitHub Actions with no secret — link the repository once. Deploy tokens for other CI.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
##
|
|
6
|
+
## Link, push, done
|
|
7
|
+
|
|
8
|
+
A project can deploy itself from GitHub Actions. Link the repository once, and
|
|
9
|
+
every push to its main branch deploys the project. There is no secret to create,
|
|
10
|
+
paste or rotate: the run proves which repository and branch it comes from with
|
|
11
|
+
GitHub's own sign-in, and Canopy only accepts runs from the repository you
|
|
12
|
+
linked.
|
|
13
|
+
|
|
14
|
+
Only the project owner (or a platform admin) can link a repository. In the
|
|
15
|
+
project's folder:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
canopy repo link
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
With no argument it links the repository the folder's `origin` remote points
|
|
22
|
+
at, on github.com or the Monks GitHub Enterprise (`monks.ghe.com`). Name it
|
|
23
|
+
yourself if needed:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
canopy repo link monks/my-tool # on this site's GitHub Enterprise
|
|
27
|
+
canopy repo link github.com/monks/my-tool # on github.com
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`canopy create` scaffolds `.github/workflows/canopy-deploy.yml`, which deploys
|
|
31
|
+
on every push to `main`. If you write your own workflow, it needs the
|
|
32
|
+
`id-token: write` permission and nothing else:
|
|
33
|
+
|
|
34
|
+
```yaml
|
|
35
|
+
on:
|
|
36
|
+
push:
|
|
37
|
+
branches: [main]
|
|
38
|
+
|
|
39
|
+
permissions:
|
|
40
|
+
id-token: write
|
|
41
|
+
contents: read
|
|
42
|
+
|
|
43
|
+
jobs:
|
|
44
|
+
deploy:
|
|
45
|
+
runs-on: ubuntu-latest
|
|
46
|
+
steps:
|
|
47
|
+
- uses: actions/checkout@v7
|
|
48
|
+
- uses: actions/setup-node@v7
|
|
49
|
+
with:
|
|
50
|
+
node-version: 24
|
|
51
|
+
- run: npm ci
|
|
52
|
+
- run: npm exec --yes @mediamonks/canopy-cli deploy
|
|
53
|
+
```
|
|
7
54
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
Actions turned **off**: pushing a workflow file works, but nothing runs, and
|
|
11
|
-
there is no Actions tab for you to look at. Turning Actions on for a repository
|
|
12
|
-
is something only a GitHub administrator can do, and you would still not see
|
|
13
|
-
the runs.
|
|
55
|
+
A run from any other branch, a pull request or a fork deploys nothing. Until a
|
|
56
|
+
repository is linked, runs skip the deploy with a warning instead of failing.
|
|
14
57
|
|
|
15
|
-
|
|
16
|
-
`
|
|
17
|
-
usually quicker than waiting for a pipeline, and your agent can verify the
|
|
18
|
-
deploy right afterwards. Run your typecheck, lint and tests locally before you
|
|
19
|
-
deploy.
|
|
58
|
+
Deploys from CI show up in the history under the repository and branch, e.g.
|
|
59
|
+
`monks/my-tool @ main`.
|
|
20
60
|
|
|
21
|
-
|
|
22
|
-
|
|
61
|
+
A project with a Canopy repository needs no link: CI deploys from it already.
|
|
62
|
+
GitHub Actions are off on Canopy repositories until a platform administrator
|
|
63
|
+
turns them on for one.
|
|
64
|
+
|
|
65
|
+
## Our recommendation: deploy manually
|
|
23
66
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
67
|
+
Most Canopy users don't have a GitHub Enterprise seat, so they can't see the
|
|
68
|
+
runs. Deploying from your machine (or letting your coding agent do it) is
|
|
69
|
+
usually quicker: run `canopy deploy --staging`, check the result, then
|
|
70
|
+
`canopy promote`. Run your typecheck, lint and tests locally before you deploy.
|
|
27
71
|
|
|
28
|
-
##
|
|
72
|
+
## A staging branch
|
|
29
73
|
|
|
30
|
-
|
|
31
|
-
|
|
74
|
+
Pushes to a second branch can deploy to the project's
|
|
75
|
+
[staging channel](https://docs.canopy.website/guides/staging) only. They can never change what is live:
|
|
76
|
+
no live deploy, promote or rollback.
|
|
32
77
|
|
|
33
78
|
```bash
|
|
34
|
-
canopy
|
|
79
|
+
canopy repo branches --staging-ref develop
|
|
35
80
|
```
|
|
36
81
|
|
|
37
|
-
|
|
38
|
-
(e.g. `CANOPY_DEPLOY_TOKEN`). It is not shown again.
|
|
82
|
+
Then add the branch to the workflow's trigger so its pushes run it:
|
|
39
83
|
|
|
40
|
-
|
|
84
|
+
```yaml
|
|
85
|
+
on:
|
|
86
|
+
push:
|
|
87
|
+
branches: [main, develop]
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The same workflow deploys `main` live and `develop` to staging. Promote from
|
|
91
|
+
staging to live by hand with `canopy promote`.
|
|
92
|
+
|
|
93
|
+
## Pausing CI deploys
|
|
94
|
+
|
|
95
|
+
Turn CI deploys off without unlinking or editing the workflow:
|
|
41
96
|
|
|
42
97
|
```bash
|
|
43
|
-
canopy
|
|
44
|
-
canopy
|
|
98
|
+
canopy repo deploys off
|
|
99
|
+
canopy repo deploys on
|
|
45
100
|
```
|
|
46
101
|
|
|
47
|
-
|
|
102
|
+
While they are off, runs skip the deploy with a warning. The link and branches
|
|
103
|
+
stay as they were. You can also flip the switch on the project's Deploys tab.
|
|
48
104
|
|
|
49
|
-
|
|
50
|
-
|
|
105
|
+
`canopy repo show` prints the repository, both branches and whether CI deploys
|
|
106
|
+
are on. `canopy repo unlink` removes the link.
|
|
51
107
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
108
|
+
## Other CI: deploy tokens
|
|
109
|
+
|
|
110
|
+
For CI that isn't GitHub Actions, or a repository you can't link, use a
|
|
111
|
+
**deploy token**: a project-scoped credential that can deploy that one project
|
|
112
|
+
and nothing else.
|
|
113
|
+
|
|
114
|
+
Only the project owner can create one (any editor can list and revoke them).
|
|
115
|
+
From inside the project directory:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
canopy token create --name "Buildkite"
|
|
57
119
|
```
|
|
58
120
|
|
|
59
|
-
|
|
60
|
-
|
|
121
|
+
This prints the token **once** — store it as a CI secret right away. It is not
|
|
122
|
+
shown again.
|
|
61
123
|
|
|
62
|
-
|
|
124
|
+
```bash
|
|
125
|
+
canopy token list
|
|
126
|
+
canopy token revoke <id>
|
|
127
|
+
```
|
|
63
128
|
|
|
64
|
-
|
|
129
|
+
Set it as `CANOPY_DEPLOY_TOKEN` in the CI environment and run `canopy deploy`.
|
|
130
|
+
No `canopy login` is needed. A deploy token also works in GitHub Actions, and
|
|
131
|
+
wins over the linked repository when both are there.
|
|
65
132
|
|
|
66
|
-
- A deploy token can only deploy its own project
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
-
|
|
70
|
-
|
|
71
|
-
in the history under the token's name.
|
|
72
|
-
- A token that goes 90 days without a deploy stops working. Active pipelines
|
|
73
|
-
never notice; a forgotten one dies on its own. The token list shows when each
|
|
74
|
-
was last used.
|
|
75
|
-
- Revoke a token the moment it might be exposed; create a fresh one to replace it.
|
|
133
|
+
- A deploy token can only deploy its own project.
|
|
134
|
+
- It belongs to the project, not to the person who created it, and keeps
|
|
135
|
+
working when they leave.
|
|
136
|
+
- A token that goes 90 days without a deploy stops working.
|
|
137
|
+
- Turning CI deploys off does not stop a deploy token. Revoke it for that.
|
package/dist/docs/cli-create.md
CHANGED
|
@@ -40,6 +40,8 @@ canopy create
|
|
|
40
40
|
|
|
41
41
|
Walks you through name, slug, and template, registers the project, and scaffolds a ready-to-run starter with next steps printed.
|
|
42
42
|
|
|
43
|
+
The starter includes `.github/workflows/canopy-deploy.yml`, which deploys on every push to `main` once the repository is linked. `create` never links a repository itself; the next steps print the exact [`canopy repo link`](https://docs.canopy.website/cli/repo) command for the folder's GitHub `origin`. See [CI deploys](https://docs.canopy.website/guides/ci).
|
|
44
|
+
|
|
43
45
|
```bash
|
|
44
46
|
canopy create "Team Retro Board" --slug retro-board --template spa
|
|
45
47
|
```
|
package/dist/docs/cli-deploy.md
CHANGED
|
@@ -33,7 +33,7 @@ canopy deploy --project my-app ./out
|
|
|
33
33
|
|
|
34
34
|
Deploys are versioned and immutable: nothing overwrites an old version, so [canopy rollback](https://docs.canopy.website/cli/rollback) can always take you back.
|
|
35
35
|
|
|
36
|
-
**CI.**
|
|
36
|
+
**CI.** In GitHub Actions, `canopy deploy` needs no secret and no `canopy login`: with `permissions: id-token: write` in the workflow, it proves which repository and branch the run comes from and deploys a project [linked](https://docs.canopy.website/cli/repo) to that repository. A run on the project's staging branch deploys to staging without `--staging`. An unlinked project, or one whose CI deploys are off, skips the deploy with a warning instead of failing. Elsewhere, set `CANOPY_DEPLOY_TOKEN` to a project-scoped deploy token (from [canopy token create](https://docs.canopy.website/cli/token)); it wins over the linked repository when both are there. Deploys from GitHub Actions are tagged with their source in the deploy history. See [CI deploys](https://docs.canopy.website/guides/ci).
|
|
37
37
|
|
|
38
38
|
## Examples
|
|
39
39
|
|