@erclx/canon 4.70.0 → 4.71.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 +1 -1
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/canon-cli/SKILL.md +4 -0
- package/claude/skills/deploy-cloudflare/REQUIREMENT.md +37 -0
- package/claude/skills/deploy-cloudflare/SKILL.md +61 -0
- package/claude/skills/{identity → draft-identity}/REQUIREMENT.md +2 -2
- package/claude/skills/{identity → draft-identity}/SKILL.md +2 -2
- package/claude/skills/ux-audit/SKILL.md +3 -0
- package/docs/workflow/ai-workflow.md +1 -1
- package/docs/workflow/visual-design-workflow.md +1 -1
- package/package.json +1 -1
- package/src/claude/cases/misc.ts +5 -1
- package/src/commands/teach.ts +2 -0
- package/src/gate/measures.ts +66 -0
- package/src/gate/stages.ts +11 -0
- package/src/migrate/skill-names.ts +10 -0
- package/src/teach/workspace.ts +60 -23
- package/tooling/cloudflare/configs/.github/workflows/deploy.yml +103 -0
- package/tooling/cloudflare/manifest.toml +5 -0
- package/tooling/cloudflare/reference.md +24 -0
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ canon is a CLI and Claude Code plugin that stops your AI conventions drifting ap
|
|
|
8
8
|
|
|
9
9
|

|
|
10
10
|
|
|
11
|
-
The counts above are read from the catalogs when the image is built, so they're what the repo actually ships today.
|
|
11
|
+
The counts above are read from the catalogs when the image is built, so they're what the repo actually ships today. See it live at [canon.erclx.dev](https://canon.erclx.dev).
|
|
12
12
|
|
|
13
13
|
## It runs on itself
|
|
14
14
|
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: deploy-cloudflare
|
|
3
|
+
description: What the Cloudflare Pages setup gap is, which of the four manual steps this skill closes, and why the credential and the custom domain stay the operator's
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Deploy Cloudflare requirement
|
|
7
|
+
|
|
8
|
+
## Gap
|
|
9
|
+
|
|
10
|
+
Without this skill, a Cloudflare Pages setup is a sequence performed from memory. Three of its four repeated acts touch a service rather than the tree, so no workflow file can absorb them, and one of the acts has already cost a broken deploy: the account ID displayed in the Cloudflare dashboard is truncated, and a value copied from there is wrong in a way that only shows up at deploy time.
|
|
11
|
+
|
|
12
|
+
## Must
|
|
13
|
+
|
|
14
|
+
- Create the Pages project through `wrangler pages project create`
|
|
15
|
+
- Fetch the account ID from `wrangler`'s own authenticated session rather than have it typed or pasted
|
|
16
|
+
- Stop for the operator to issue the API token and run `gh secret set CLOUDFLARE_API_TOKEN` themselves
|
|
17
|
+
- Verify both secrets are present with `gh secret list` before continuing
|
|
18
|
+
- Stop for the operator to attach the custom domain in the dashboard
|
|
19
|
+
- Call `wrangler` and `gh` directly for every step those tools already cover
|
|
20
|
+
|
|
21
|
+
## Must not
|
|
22
|
+
|
|
23
|
+
- Accept a Cloudflare API token as input in any form. The toolkit verifies a secret exists, never what it contains.
|
|
24
|
+
- Reimplement `wrangler pages project create` or `gh secret set` as a hand-built API call
|
|
25
|
+
- Open a pull request or merge
|
|
26
|
+
|
|
27
|
+
## Guards
|
|
28
|
+
|
|
29
|
+
- `wrangler` not authenticated: stop and name the login command
|
|
30
|
+
- `gh` not authenticated: stop and name the login command
|
|
31
|
+
- A secret missing after the token stop: stop and name which one
|
|
32
|
+
|
|
33
|
+
## Out of scope
|
|
34
|
+
|
|
35
|
+
- Attaching a custom domain through the Cloudflare REST API instead of the dashboard. Measured absent from `wrangler pages` at plan time and the REST API was not read, so the dashboard stop stays for this pass.
|
|
36
|
+
- Running the deploy itself once secrets and the domain are set. That is the seeded `tooling/cloudflare/configs/.github/workflows/deploy.yml` workflow, triggered by a push to main.
|
|
37
|
+
- Reconfiguring an existing Pages project's build settings.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: deploy-cloudflare
|
|
3
|
+
description: Sets up a Cloudflare Pages deploy for the current project by creating the Pages project, fetching the account ID, and stopping twice for the operator to issue the API token and attach the custom domain. Calls `wrangler` and `gh` rather than reimplementing them. Use when asked to "set up Cloudflare deploy", "deploy this to Cloudflare Pages", "set up the Pages project", or "connect this repo to Cloudflare". Do NOT use to run an already-configured deploy, which the seeded workflow does on push, or to reconfigure an existing Pages project's build settings.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Deploy Cloudflare
|
|
7
|
+
|
|
8
|
+
Runs the one-time setup a Cloudflare Pages deploy needs before the seeded `deploy.yml` workflow can run, stopping twice for the two acts only the operator can take.
|
|
9
|
+
|
|
10
|
+
## Guards
|
|
11
|
+
|
|
12
|
+
- If `wrangler whoami` fails, stop: `❌ wrangler is not authenticated. Run wrangler login, then re-invoke.`
|
|
13
|
+
- If `gh auth status` fails, stop: `❌ gh is not authenticated. Run gh auth login, then re-invoke.`
|
|
14
|
+
- Never accept a Cloudflare API token as input, in a prompt, an argument, or a file. Verify a secret's presence with `gh secret list` rather than asking for or reading its content.
|
|
15
|
+
- Never reimplement `wrangler pages project create` or `gh secret set` as a hand-built HTTP call. Call the tools directly.
|
|
16
|
+
- If the project name is not supplied, derive it from the repository's own name (`basename` of `git remote get-url origin`, stripped of a trailing `.git`) and confirm it in the preview rather than asking first.
|
|
17
|
+
|
|
18
|
+
## Step 1: create the Pages project
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
wrangler pages project create <project-name>
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Report the command's own failure output and stop rather than retrying on a name collision. A project already existing under that name is the operator's to resolve.
|
|
25
|
+
|
|
26
|
+
## Step 2: fetch and set the account ID
|
|
27
|
+
|
|
28
|
+
Fetch the account ID from `wrangler`'s own authenticated session rather than asking the operator to copy it from the dashboard. The dashboard displays the account ID truncated, and a value copied from there fails the deploy silently:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
wrangler whoami
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Parse the account ID from that output and pipe it into the secret rather than printing it to the transcript first:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
echo "<account-id>" | gh secret set CLOUDFLARE_ACCOUNT_ID
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Step 3: stop for the API token
|
|
41
|
+
|
|
42
|
+
Stop: `⏸ Create a Cloudflare API token with Pages edit permission at the Cloudflare dashboard, then run: gh secret set CLOUDFLARE_API_TOKEN. Re-invoke this skill once that's done.`
|
|
43
|
+
|
|
44
|
+
This is the one credential the skill never touches. Resume only once the operator confirms the token is set.
|
|
45
|
+
|
|
46
|
+
## Step 4: verify both secrets
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
gh secret list
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
- Both `CLOUDFLARE_ACCOUNT_ID` and `CLOUDFLARE_API_TOKEN` present: continue.
|
|
53
|
+
- Either missing: stop and name which one, with the command that sets it.
|
|
54
|
+
|
|
55
|
+
## Step 5: stop for the custom domain
|
|
56
|
+
|
|
57
|
+
Stop: `⏸ Attach the custom domain to the <project-name> Pages project in the Cloudflare dashboard, then confirm here. Whether this can run through the API instead of the dashboard is out of scope for this skill, per its REQUIREMENT.md.`
|
|
58
|
+
|
|
59
|
+
## Step 6: hand off
|
|
60
|
+
|
|
61
|
+
Report that setup is complete and name the seeded workflow (`tooling/cloudflare/configs/.github/workflows/deploy.yml`, synced via `canon tooling sync cloudflare . --write`) as what deploys on the next push. Do not invoke `git-pr` or `git-ship` from here. The operator or the controlling session decides when to open that pull request.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
---
|
|
2
|
-
name: identity
|
|
2
|
+
name: draft-identity
|
|
3
3
|
description: Why the mark and the card are one pick rather than two, and where the size sequence and the write folder come from before either is drafted
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
#
|
|
6
|
+
# Draft identity requirement
|
|
7
7
|
|
|
8
8
|
## Gap
|
|
9
9
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
---
|
|
2
|
-
name: identity
|
|
2
|
+
name: draft-identity
|
|
3
3
|
description: Drafts a project's logo mark through draft-and-pick's render-and-pick loop, then composes the picked mark into an icon sequence and a 1200x630 social card. Use when asked to "make a logo", "design a logo mark", "create a favicon", "build the icon set", "generate a social card", "make an og:image", or "draft the logo and social card together". Do NOT use to mutate an existing logo file directly, which is a plain edit, or to record the mark's construction rules in DESIGN.md, which is out of scope.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
#
|
|
6
|
+
# Draft identity
|
|
7
7
|
|
|
8
8
|
One identity rendered twice: the same mark sized down to an icon sequence and composed with type into the social card sized up. Drafting both from one pick keeps the mark and its card composition from settling on shapes that do not match.
|
|
9
9
|
|
|
@@ -18,6 +18,7 @@ Read these in parallel from the project root, skipping any that do not exist:
|
|
|
18
18
|
- `.claude/DESIGN.md`: visual intent, tokens, typography, spacing rules
|
|
19
19
|
- `.claude/wireframes/index.md` + every `.claude/wireframes/<surface>.md`, each a flat file or a grouped surface's own `.claude/wireframes/<surface>/index.md` and the siblings it lists: intended UI layout, UI copy, interaction rules. Surfaces are loaded one file at a time during Step 3. Per `${CLAUDE_SKILL_DIR}/../../standards/wireframes.md`, these carry layout and intent, not implementation detail.
|
|
20
20
|
- `.claude/REQUIREMENTS.md`: feature scope and non-goals
|
|
21
|
+
- A committed capture beside the surface's own source, when the project has one. A rendered surface reads differently composed than it reads written, and a capture is the only artifact here that shows the composed result rather than the markup. Absent one, note that this audit reads source only, and keep going. This is a stated fallback, not a stop: a project with no capture yet, or a capture mid-rebuild, still gets a source-only audit rather than losing the skill entirely.
|
|
21
22
|
|
|
22
23
|
## Step 2: identify surfaces
|
|
23
24
|
|
|
@@ -34,6 +35,8 @@ For each surface, look for:
|
|
|
34
35
|
|
|
35
36
|
Use `.claude/DESIGN.md` and the per-surface `.claude/wireframes/<surface>.md` files, each a flat file or a grouped surface's own `.claude/wireframes/<surface>/index.md` and the siblings it lists, as ground truth for intent. For each implementation surface, read the matching wireframe file before flagging drift. Observations only, no implementation suggestions or fixes.
|
|
36
37
|
|
|
38
|
+
Where Step 1 found a committed capture for this surface, judge composed output (spacing, overlap, contrast, wrapping) against it rather than inferring layout from markup alone, which reports what the surface renders rather than what its source implies. Where none exists, judge from source alone and say so in that surface's findings, since a source-only read cannot see what only a render would show.
|
|
39
|
+
|
|
37
40
|
## Step 4: report and persist
|
|
38
41
|
|
|
39
42
|
### Report format
|
|
@@ -305,7 +305,7 @@ This section is the corpus the coverage claim is measured against: every name `c
|
|
|
305
305
|
| `canon:draft-slides` | For a deck, drafted as `.claude/SLIDES.md` and rendered to PowerPoint |
|
|
306
306
|
| `canon:draft-screencast` | For a recording script with beats and defaults already seeded |
|
|
307
307
|
| `canon:record-screencast` | For compiling and running a screencast draft into a recording and a still |
|
|
308
|
-
| `canon:identity`
|
|
308
|
+
| `canon:draft-identity` | For a project's logo mark and its social card, drafted through `draft-and-pick`'s pick loop |
|
|
309
309
|
|
|
310
310
|
### Answer a question at any point
|
|
311
311
|
|
|
@@ -40,7 +40,7 @@ A cell no source anchors ends in `? verify`, and the preview shows that marker b
|
|
|
40
40
|
- `canon:ux-audit` for UX gap detection on existing surfaces
|
|
41
41
|
- `canon:ux-measure` for what a running surface costs to paint, read against published thresholds
|
|
42
42
|
- `canon:draft-and-pick` for a call settled by looking, drafting several candidates onto one page and taking your pick
|
|
43
|
-
- `canon:identity` to draft a project's logo mark and compose it into an icon sequence and a social card, through `draft-and-pick`'s own render-and-pick loop
|
|
43
|
+
- `canon:draft-identity` to draft a project's logo mark and compose it into an icon sequence and a social card, through `draft-and-pick`'s own render-and-pick loop
|
|
44
44
|
- Anthropic's `frontend-design` plugin optional for light visual steering
|
|
45
45
|
|
|
46
46
|
### When to pick
|
package/package.json
CHANGED
package/src/claude/cases/misc.ts
CHANGED
|
@@ -15,7 +15,7 @@ export const MISC_CASES: readonly SkillCase[] = [
|
|
|
15
15
|
{
|
|
16
16
|
prompt:
|
|
17
17
|
'This project has no logo yet. Draft one and give me a social card to go with it.',
|
|
18
|
-
expect: 'identity',
|
|
18
|
+
expect: 'draft-identity',
|
|
19
19
|
},
|
|
20
20
|
{
|
|
21
21
|
prompt: 'Fire up the dev server the way this project documents it.',
|
|
@@ -47,4 +47,8 @@ export const MISC_CASES: readonly SkillCase[] = [
|
|
|
47
47
|
prompt: 'Is there a page anywhere in this repo that covers retries?',
|
|
48
48
|
expect: 'index-lookup',
|
|
49
49
|
},
|
|
50
|
+
{
|
|
51
|
+
prompt: 'Connect this repo to Cloudflare Pages and set up the deploy.',
|
|
52
|
+
expect: 'deploy-cloudflare',
|
|
53
|
+
},
|
|
50
54
|
]
|
package/src/commands/teach.ts
CHANGED
|
@@ -384,6 +384,7 @@ async function runStylesheet(
|
|
|
384
384
|
root,
|
|
385
385
|
slug: outcome.slug,
|
|
386
386
|
path: outcome.path,
|
|
387
|
+
basePath: outcome.basePath,
|
|
387
388
|
written: outcome.written,
|
|
388
389
|
})}\n`,
|
|
389
390
|
)
|
|
@@ -393,6 +394,7 @@ async function runStylesheet(
|
|
|
393
394
|
intro('canon teach stylesheet')
|
|
394
395
|
logStep(outcome.written ? 'Written' : 'Already present, left alone')
|
|
395
396
|
logInfo(outcome.path)
|
|
397
|
+
logInfo(outcome.basePath)
|
|
396
398
|
outro()
|
|
397
399
|
return 0
|
|
398
400
|
}
|
package/src/gate/measures.ts
CHANGED
|
@@ -708,6 +708,72 @@ export const readmeCitations: Measure = async (ctx) => {
|
|
|
708
708
|
}
|
|
709
709
|
}
|
|
710
710
|
|
|
711
|
+
/**
|
|
712
|
+
* `deploy-site.yml` and `pr-visual-checks.yml` each carry their own literal
|
|
713
|
+
* copy of the eight path globs a landing-page change should trigger on, per
|
|
714
|
+
* Question 2 in `.canon/plans/archive/feature-render-visibility.md`. A YAML
|
|
715
|
+
* anchor cannot cross files here, so the two copies are read live and compared
|
|
716
|
+
* rather than one asserting a literal the other could drift behind unnoticed,
|
|
717
|
+
* mirroring how `web/e2e/home.spec.ts`'s rule-group test already reads live.
|
|
718
|
+
*/
|
|
719
|
+
export const visualPathGlobs: Measure = async (ctx) => {
|
|
720
|
+
const deployPath = '.github/workflows/deploy-site.yml'
|
|
721
|
+
const visualPath = '.github/workflows/pr-visual-checks.yml'
|
|
722
|
+
const deployFile = join(ctx.root, deployPath)
|
|
723
|
+
const visualFile = join(ctx.root, visualPath)
|
|
724
|
+
|
|
725
|
+
if (!existsSync(deployFile) || !existsSync(visualFile)) {
|
|
726
|
+
return {
|
|
727
|
+
emissions: [],
|
|
728
|
+
unmeasured: `${deployPath} or ${visualPath} is absent, so no path list was compared.`,
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
const deploy = Bun.YAML.parse(readFileSync(deployFile, 'utf8')) as {
|
|
733
|
+
readonly on?: { readonly push?: { readonly paths?: readonly string[] } }
|
|
734
|
+
}
|
|
735
|
+
const visual = Bun.YAML.parse(readFileSync(visualFile, 'utf8')) as {
|
|
736
|
+
readonly on?: {
|
|
737
|
+
readonly pull_request?: { readonly paths?: readonly string[] }
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
const deployPaths = deploy.on?.push?.paths ?? []
|
|
742
|
+
const visualPaths = visual.on?.pull_request?.paths ?? []
|
|
743
|
+
|
|
744
|
+
if (deployPaths.length === 0 || visualPaths.length === 0) {
|
|
745
|
+
return {
|
|
746
|
+
emissions: [],
|
|
747
|
+
unmeasured: `${deployPath} or ${visualPath} carries no path list to compare.`,
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
const onlyDeploy = deployPaths.filter((path) => !visualPaths.includes(path))
|
|
752
|
+
const onlyVisual = visualPaths.filter((path) => !deployPaths.includes(path))
|
|
753
|
+
|
|
754
|
+
if (onlyDeploy.length === 0 && onlyVisual.length === 0) {
|
|
755
|
+
return {
|
|
756
|
+
emissions: [
|
|
757
|
+
info(
|
|
758
|
+
`${deployPaths.length} path glob(s) agree between ${deployPath} and ${visualPath}`,
|
|
759
|
+
),
|
|
760
|
+
],
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
return {
|
|
765
|
+
emissions: [
|
|
766
|
+
...onlyDeploy.map((path) =>
|
|
767
|
+
warn(`${deployPath} carries ${path}, absent from ${visualPath}`),
|
|
768
|
+
),
|
|
769
|
+
...onlyVisual.map((path) =>
|
|
770
|
+
warn(`${visualPath} carries ${path}, absent from ${deployPath}`),
|
|
771
|
+
),
|
|
772
|
+
],
|
|
773
|
+
failure: `${deployPath} and ${visualPath} carry different path globs. Bring the two lists back into agreement.`,
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
711
777
|
/**
|
|
712
778
|
* `canon sandbox coverage` moves only when a person runs it, so a scenario added
|
|
713
779
|
* with no expectation ships unnoticed.
|
package/src/gate/stages.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
shippedReferences,
|
|
13
13
|
standardCriteria,
|
|
14
14
|
unreferencedRules,
|
|
15
|
+
visualPathGlobs,
|
|
15
16
|
} from '@/gate/measures'
|
|
16
17
|
import { SHIPPED_CORPORA } from '@/shipped/references'
|
|
17
18
|
|
|
@@ -424,6 +425,16 @@ export const STAGES: readonly Stage[] = [
|
|
|
424
425
|
skipped: 'Neither copy.ts nor README.md changed, so no citation was read',
|
|
425
426
|
checks: [{ kind: 'measure', measure: readmeCitations }],
|
|
426
427
|
},
|
|
428
|
+
{
|
|
429
|
+
// Scoped to the two files carrying the literal path-glob copy, so an edit
|
|
430
|
+
// to either one runs the check.
|
|
431
|
+
id: 'visual-path-globs',
|
|
432
|
+
label: 'Visual check path globs',
|
|
433
|
+
scope: /^\.github\/workflows\/(deploy-site|pr-visual-checks)\.yml$/,
|
|
434
|
+
skipped:
|
|
435
|
+
'Neither deploy-site.yml nor pr-visual-checks.yml changed, so no path list was compared',
|
|
436
|
+
checks: [{ kind: 'measure', measure: visualPathGlobs }],
|
|
437
|
+
},
|
|
427
438
|
{
|
|
428
439
|
id: 'seed-standards',
|
|
429
440
|
label: 'Seed standards',
|
|
@@ -24,6 +24,16 @@ import { defineRenameRules, type RenameRules } from '@/migrate/rename'
|
|
|
24
24
|
* Every name takes two words. Ten of these would have landed as a bare single
|
|
25
25
|
* word under a plain strip, and a bare word such as `review` or `docs` is a
|
|
26
26
|
* substring of ordinary prose with no token left for a later sweep to find.
|
|
27
|
+
*
|
|
28
|
+
* No bare single-word name may ever join this map, whatever prefix it would
|
|
29
|
+
* otherwise take. `wholeToken: true` below matches a standalone word rather
|
|
30
|
+
* than a namespaced compound, so a one-word key rewrites every unrelated use
|
|
31
|
+
* of that word too. `identity` proved it: a dry run against a fifth row
|
|
32
|
+
* reading `'identity': 'draft-identity'` reported 112 occurrences across 43
|
|
33
|
+
* files, most of them a variable, field, or type name spelling the bare word
|
|
34
|
+
* rather than the skill, including `readonly identity: SelfIdentity` at
|
|
35
|
+
* `src/sessions/resolve.ts:255`, which the rewrite would have turned into
|
|
36
|
+
* invalid TypeScript. The row was never added.
|
|
27
37
|
*/
|
|
28
38
|
export const SKILL_NAME_MAP: Readonly<Record<string, string>> = {
|
|
29
39
|
'claude-address-review': 'review-address',
|
package/src/teach/workspace.ts
CHANGED
|
@@ -42,9 +42,20 @@ export const TEACH_ASSETS = 'assets'
|
|
|
42
42
|
* The one stylesheet every lesson in a workspace links. The name is fixed here
|
|
43
43
|
* rather than chosen per lesson, because the second lesson has to reach the
|
|
44
44
|
* file the first one wrote and a name composed twice is a name that can differ.
|
|
45
|
+
* This file is workspace-owned: `writeStylesheet` writes it once and leaves it
|
|
46
|
+
* alone, since a lesson adds its own rules here. It imports `TEACH_STYLESHEET_BASE`
|
|
47
|
+
* for the tokens and components, which is the half a resync may always replace.
|
|
45
48
|
*/
|
|
46
49
|
export const TEACH_STYLESHEET = 'course.css'
|
|
47
50
|
|
|
51
|
+
/**
|
|
52
|
+
* The seeded half of a workspace's styling, imported by `TEACH_STYLESHEET`.
|
|
53
|
+
* `writeStylesheet` rewrites this file from the design source on every call,
|
|
54
|
+
* since nothing workspace-authored lives in it, which is what lets a design
|
|
55
|
+
* token move without a `--force` flag discarding a workspace's lesson rules.
|
|
56
|
+
*/
|
|
57
|
+
export const TEACH_STYLESHEET_BASE = 'base.css'
|
|
58
|
+
|
|
48
59
|
/**
|
|
49
60
|
* The mission heading whose list a session reads as exit criteria. The writer,
|
|
50
61
|
* the reader below, and the record validator all match this one spelling, so a
|
|
@@ -1001,31 +1012,40 @@ export interface StylesheetWritten {
|
|
|
1001
1012
|
readonly slug: string
|
|
1002
1013
|
/** Relative to the root, so a caller prints a path a reader can open. */
|
|
1003
1014
|
readonly path: string
|
|
1004
|
-
/**
|
|
1015
|
+
/** Relative to the root, the re-synced seed `path` imports. */
|
|
1016
|
+
readonly basePath: string
|
|
1017
|
+
/** False when the workspace already held `path` and this call left it alone. */
|
|
1005
1018
|
readonly written: boolean
|
|
1006
1019
|
}
|
|
1007
1020
|
|
|
1008
1021
|
export type StylesheetOutcome = StylesheetWritten | TeachRefused
|
|
1009
1022
|
|
|
1010
|
-
const
|
|
1023
|
+
const STYLESHEET_BASE_BANNER = [
|
|
1011
1024
|
'Seeded by `canon teach stylesheet` from the design source in',
|
|
1012
|
-
'src/design/tokens.ts. The tokens and the two
|
|
1013
|
-
'system this workspace renders in.
|
|
1014
|
-
'
|
|
1015
|
-
'
|
|
1025
|
+
'src/design/tokens.ts, and rewritten on every call. The tokens and the two',
|
|
1026
|
+
'components below are the system this workspace renders in. Read a value',
|
|
1027
|
+
'through its custom property rather than restating the hex, which is what',
|
|
1028
|
+
'let one workspace fork the palette from every other. Add lesson rules to',
|
|
1029
|
+
`${TEACH_STYLESHEET} instead, which imports this file and is never rewritten.`,
|
|
1016
1030
|
].join('\n ')
|
|
1017
1031
|
|
|
1032
|
+
const stylesheetSeed = (): string =>
|
|
1033
|
+
`@import url('./${TEACH_STYLESHEET_BASE}');\n`
|
|
1034
|
+
|
|
1018
1035
|
/**
|
|
1019
|
-
* Writes a workspace's
|
|
1036
|
+
* Writes a workspace's stylesheet pair from the design source.
|
|
1020
1037
|
*
|
|
1021
|
-
* Every workspace used to carry
|
|
1022
|
-
* palette forked once per workspace. The
|
|
1023
|
-
* and
|
|
1024
|
-
*
|
|
1038
|
+
* Every workspace used to carry one hand-authored file, which is how the
|
|
1039
|
+
* course palette forked once per workspace. The names are fixed at
|
|
1040
|
+
* `TEACH_STYLESHEET` and `TEACH_STYLESHEET_BASE`, and the folder at
|
|
1041
|
+
* `TEACH_ASSETS`, for the same reason a second lesson has to reach the files
|
|
1042
|
+
* the first one wrote and names composed twice can differ.
|
|
1025
1043
|
*
|
|
1026
|
-
*
|
|
1027
|
-
*
|
|
1028
|
-
*
|
|
1044
|
+
* `TEACH_STYLESHEET_BASE` carries the tokens and the components and is
|
|
1045
|
+
* rewritten every call, since nothing workspace-authored lives in it.
|
|
1046
|
+
* `TEACH_STYLESHEET` imports it, carries a workspace's own lesson rules, and
|
|
1047
|
+
* is written once and left alone, since overwriting it would discard them.
|
|
1048
|
+
* `--force` is the caller saying it wants that file's seed back too.
|
|
1029
1049
|
*/
|
|
1030
1050
|
export async function writeStylesheet(
|
|
1031
1051
|
root: string,
|
|
@@ -1036,22 +1056,39 @@ export async function writeStylesheet(
|
|
|
1036
1056
|
if (!found.ok) return found
|
|
1037
1057
|
|
|
1038
1058
|
const workspace = found.workspace
|
|
1039
|
-
const
|
|
1059
|
+
const assetsDir = join(workspace.path, TEACH_ASSETS)
|
|
1060
|
+
const baseRel = join(assetsDir, TEACH_STYLESHEET_BASE)
|
|
1061
|
+
const rel = join(assetsDir, TEACH_STYLESHEET)
|
|
1062
|
+
const basePath = join(root, baseRel)
|
|
1040
1063
|
const path = join(root, rel)
|
|
1041
1064
|
|
|
1042
|
-
|
|
1043
|
-
return { ok: true, slug: workspace.slug, path: rel, written: false }
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
|
-
await mkdir(join(root, workspace.path, TEACH_ASSETS), { recursive: true })
|
|
1065
|
+
await mkdir(join(root, assetsDir), { recursive: true })
|
|
1047
1066
|
await writeFile(
|
|
1048
|
-
|
|
1067
|
+
basePath,
|
|
1049
1068
|
buildDesignCss(undefined, {
|
|
1050
|
-
banner:
|
|
1069
|
+
banner: STYLESHEET_BASE_BANNER,
|
|
1051
1070
|
embedFonts: TEACH_FONT_FACES,
|
|
1052
1071
|
components: TEACH_STYLESHEET_COMPONENTS,
|
|
1053
1072
|
}),
|
|
1054
1073
|
)
|
|
1055
1074
|
|
|
1056
|
-
|
|
1075
|
+
if (existsSync(path) && !force) {
|
|
1076
|
+
return {
|
|
1077
|
+
ok: true,
|
|
1078
|
+
slug: workspace.slug,
|
|
1079
|
+
path: rel,
|
|
1080
|
+
basePath: baseRel,
|
|
1081
|
+
written: false,
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
await writeFile(path, stylesheetSeed())
|
|
1086
|
+
|
|
1087
|
+
return {
|
|
1088
|
+
ok: true,
|
|
1089
|
+
slug: workspace.slug,
|
|
1090
|
+
path: rel,
|
|
1091
|
+
basePath: baseRel,
|
|
1092
|
+
written: true,
|
|
1093
|
+
}
|
|
1057
1094
|
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
name: Deploy
|
|
2
|
+
# canon-no-seed: a stack-specific deploy workflow with no root counterpart by design, parameterized per project on sync rather than mirrored from one.
|
|
3
|
+
|
|
4
|
+
on:
|
|
5
|
+
push:
|
|
6
|
+
branches:
|
|
7
|
+
- main
|
|
8
|
+
paths:
|
|
9
|
+
# Fill in this project's own build inputs. A single glob covering the
|
|
10
|
+
# source tree is enough for most projects; widen it to cover every
|
|
11
|
+
# path the build reads when it draws on content living elsewhere.
|
|
12
|
+
- 'src/**'
|
|
13
|
+
workflow_dispatch:
|
|
14
|
+
|
|
15
|
+
# Fill in a concurrency group name unique to this project. A second push
|
|
16
|
+
# while a deploy is in flight publishes the newer commit rather than racing
|
|
17
|
+
# it, and the running job is left to finish so a half-uploaded site is never
|
|
18
|
+
# what a reader lands on.
|
|
19
|
+
concurrency:
|
|
20
|
+
group: deploy
|
|
21
|
+
cancel-in-progress: false
|
|
22
|
+
|
|
23
|
+
permissions:
|
|
24
|
+
contents: read
|
|
25
|
+
|
|
26
|
+
jobs:
|
|
27
|
+
build:
|
|
28
|
+
name: 📦 Build
|
|
29
|
+
runs-on: ubuntu-latest
|
|
30
|
+
steps:
|
|
31
|
+
- name: Checkout
|
|
32
|
+
uses: actions/checkout@v4
|
|
33
|
+
|
|
34
|
+
- name: Setup Bun
|
|
35
|
+
uses: oven-sh/setup-bun@v2
|
|
36
|
+
with:
|
|
37
|
+
bun-version: latest
|
|
38
|
+
|
|
39
|
+
- name: Install Dependencies
|
|
40
|
+
run: bun install --frozen-lockfile
|
|
41
|
+
|
|
42
|
+
- name: Build
|
|
43
|
+
run: bun run build
|
|
44
|
+
|
|
45
|
+
- name: Upload artifact
|
|
46
|
+
uses: actions/upload-artifact@v4
|
|
47
|
+
with:
|
|
48
|
+
name: dist
|
|
49
|
+
path: dist
|
|
50
|
+
retention-days: 1
|
|
51
|
+
|
|
52
|
+
deploy:
|
|
53
|
+
name: 🚀 Deploy
|
|
54
|
+
needs: build
|
|
55
|
+
runs-on: ubuntu-latest
|
|
56
|
+
steps:
|
|
57
|
+
- name: Check for Cloudflare secrets
|
|
58
|
+
id: secrets-check
|
|
59
|
+
env:
|
|
60
|
+
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
|
61
|
+
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
|
62
|
+
run: |
|
|
63
|
+
if [ -z "$CLOUDFLARE_API_TOKEN" ] || [ -z "$CLOUDFLARE_ACCOUNT_ID" ]; then
|
|
64
|
+
echo "skip=true" >> "$GITHUB_OUTPUT"
|
|
65
|
+
else
|
|
66
|
+
echo "skip=false" >> "$GITHUB_OUTPUT"
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
- name: Skip notice
|
|
70
|
+
if: steps.secrets-check.outputs.skip == 'true'
|
|
71
|
+
run: echo "::notice::CLOUDFLARE_API_TOKEN or CLOUDFLARE_ACCOUNT_ID is not set, skipping deploy. Run the deploy-cloudflare skill to finish setup."
|
|
72
|
+
|
|
73
|
+
# This job builds nothing and still needs a toolchain. The checkout
|
|
74
|
+
# below brings bun.lock, wrangler-action reads it, picks bun on the
|
|
75
|
+
# strength of it, and installs wrangler with that, so a deploy with no
|
|
76
|
+
# bun on PATH fails before it reaches Cloudflare. Dropping these steps
|
|
77
|
+
# as dead weight breaks the deploy on every push.
|
|
78
|
+
- name: Checkout
|
|
79
|
+
if: steps.secrets-check.outputs.skip == 'false'
|
|
80
|
+
uses: actions/checkout@v4
|
|
81
|
+
|
|
82
|
+
- name: Setup Bun
|
|
83
|
+
if: steps.secrets-check.outputs.skip == 'false'
|
|
84
|
+
uses: oven-sh/setup-bun@v2
|
|
85
|
+
with:
|
|
86
|
+
bun-version: latest
|
|
87
|
+
|
|
88
|
+
- name: Download artifact
|
|
89
|
+
if: steps.secrets-check.outputs.skip == 'false'
|
|
90
|
+
uses: actions/download-artifact@v4
|
|
91
|
+
with:
|
|
92
|
+
name: dist
|
|
93
|
+
path: dist
|
|
94
|
+
|
|
95
|
+
- name: Publish
|
|
96
|
+
if: steps.secrets-check.outputs.skip == 'false'
|
|
97
|
+
uses: cloudflare/wrangler-action@v3
|
|
98
|
+
with:
|
|
99
|
+
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
|
100
|
+
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
|
101
|
+
# Fill in this project's Pages project name, set when the
|
|
102
|
+
# deploy-cloudflare skill runs wrangler pages project create.
|
|
103
|
+
command: pages deploy dist --project-name=REPLACE_WITH_PROJECT_NAME --branch=${{ github.ref_name }}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Tooling cloudflare reference
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The cloudflare stack ships one parameterized GitHub Actions workflow that deploys a static build to Cloudflare Pages. It is keyed on host rather than on framework and carries no dependencies, seeds, or scripts of its own, so it syncs onto a project already carrying `astro`, `vite-react`, or any other stack at the same root without colliding.
|
|
6
|
+
|
|
7
|
+
## Deploy workflow
|
|
8
|
+
|
|
9
|
+
- Separate `build` and `deploy` jobs, connected by an uploaded artifact, converged on the shape already running in the closest prior art rather than invented fresh.
|
|
10
|
+
- The deploy job checks for `CLOUDFLARE_API_TOKEN` and `CLOUDFLARE_ACCOUNT_ID` before doing anything else, and skips with a `::notice::` rather than failing when either is absent. A first merge lands ahead of the Pages project existing, and this guard is what keeps that push green.
|
|
11
|
+
- `--branch=${{ github.ref_name }}` on every deploy, which gives every branch a Cloudflare preview URL rather than only `main`.
|
|
12
|
+
- The deploy job checks out the repository and installs Bun even though it builds nothing. `wrangler-action` resolves its package manager from `bun.lock` in the checkout, and dropping that step breaks the deploy on every push once the toolchain it relies on silently goes missing.
|
|
13
|
+
|
|
14
|
+
## Parameters a project fills in by hand
|
|
15
|
+
|
|
16
|
+
Three values in the seeded file have no generic default and are set once, right after sync:
|
|
17
|
+
|
|
18
|
+
- The `paths:` filter under the `push` trigger. The shipped default is a single `src/**` glob. Widen it to cover every path the build reads when the build draws on catalogs or generated content living outside `src/`.
|
|
19
|
+
- The `concurrency.group` name. The shipped default is the bare string `deploy`, which is enough for a single-deploy project. Give it a project-specific name only if the project runs more than one deploy workflow.
|
|
20
|
+
- The `--project-name` flag on the `wrangler pages deploy` command. Set it to the name the `deploy-cloudflare` skill creates the Pages project under.
|
|
21
|
+
|
|
22
|
+
## Re-sync
|
|
23
|
+
|
|
24
|
+
`configs/` is golden and always overwrites. Re-running `canon tooling sync cloudflare . --write` after hand-editing any of the three parameters above discards that edit and restores the shipped defaults. Re-fill the parameters after any re-sync rather than expecting the file to preserve them.
|