@erclx/canon 4.95.0 โ†’ 4.97.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.
Files changed (30) hide show
  1. package/claude/.claude-plugin/plugin.json +1 -1
  2. package/claude/skills/codebase-layout/REQUIREMENT.md +46 -0
  3. package/claude/skills/codebase-layout/SKILL.md +57 -0
  4. package/claude/skills/codebase-layout/references/adopted.md +34 -0
  5. package/claude/skills/codebase-layout/references/frontend.md +43 -0
  6. package/claude/skills/codebase-layout/references/tests.md +28 -0
  7. package/claude/skills/git-pr/REQUIREMENT.md +3 -0
  8. package/claude/skills/git-pr/SKILL.md +25 -0
  9. package/claude/skills/plan-feature/REQUIREMENT.md +1 -0
  10. package/claude/skills/plan-feature/SKILL.md +2 -1
  11. package/claude/skills/test-craft/REQUIREMENT.md +9 -0
  12. package/claude/skills/test-craft/references/e2e.md +1 -1
  13. package/docs/agents/commands.md +2 -1
  14. package/docs/agents/index.md +1 -0
  15. package/docs/agents/pr-evidence.md +9 -1
  16. package/docs/agents/pr-preview.md +103 -0
  17. package/docs/workflow/ai-workflow.md +1 -0
  18. package/governance/rules/core/000-constitution.md +1 -1
  19. package/governance/rules/lib/300-testing-ts.md +1 -0
  20. package/package.json +1 -1
  21. package/src/claude/cases/misc.ts +5 -0
  22. package/src/commands/pr.ts +300 -13
  23. package/src/pr/evidence.ts +36 -1
  24. package/src/pr/preview.ts +178 -0
  25. package/tooling/astro/configs/playwright.config.ts +3 -1
  26. package/tooling/cloudflare/configs/.github/workflows/deploy.yml +64 -2
  27. package/tooling/cloudflare/reference.md +6 -3
  28. package/tooling/nextjs/configs/playwright.config.ts +3 -1
  29. package/tooling/vite-react/configs/playwright.config.ts +3 -1
  30. package/tooling/web/configs/e2e/home.spec.ts +8 -0
@@ -11,13 +11,19 @@ on:
11
11
  # path the build reads when it draws on content living elsewhere.
12
12
  - 'src/**'
13
13
  workflow_dispatch:
14
+ # A closed pull request runs only the cleanup job below, which deletes the
15
+ # preview deployments `canon pr preview` minted for its branch.
16
+ pull_request:
17
+ types: [closed]
14
18
 
15
19
  # Fill in a concurrency group name unique to this project. A second push
16
20
  # while a deploy is in flight publishes the newer commit rather than racing
17
21
  # it, and the running job is left to finish so a half-uploaded site is never
18
- # what a reader lands on.
22
+ # what a reader lands on. The group is keyed on the ref, since GitHub cancels
23
+ # a pending run when a newer one queues in the same group, and a branch
24
+ # preview must never displace a production deploy waiting behind another.
19
25
  concurrency:
20
- group: deploy
26
+ group: deploy-${{ github.ref_name }}
21
27
  cancel-in-progress: false
22
28
 
23
29
  permissions:
@@ -26,6 +32,7 @@ permissions:
26
32
  jobs:
27
33
  build:
28
34
  name: ๐Ÿ“ฆ Build
35
+ if: github.event_name != 'pull_request'
29
36
  runs-on: ubuntu-latest
30
37
  steps:
31
38
  - name: Checkout
@@ -92,12 +99,67 @@ jobs:
92
99
  name: dist
93
100
  path: dist
94
101
 
102
+ # `pages-deployment-alias-url` is an output from wrangler 3.78 on, so the
103
+ # version is pinned rather than left to the action's default.
95
104
  - name: Publish
105
+ id: publish
96
106
  if: steps.secrets-check.outputs.skip == 'false'
97
107
  uses: cloudflare/wrangler-action@v3
98
108
  with:
99
109
  apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
100
110
  accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
111
+ wranglerVersion: '3.90.0'
101
112
  # Fill in this project's Pages project name, set when the
102
113
  # deploy-cloudflare skill runs wrangler pages project create.
103
114
  command: pages deploy dist --project-name=REPLACE_WITH_PROJECT_NAME --branch=${{ github.ref_name }}
115
+
116
+ # `canon pr preview` reads this line out of the run log, so the marker is
117
+ # a contract rather than decoration. The alias is read off the deploy
118
+ # rather than computed from the branch name, since Cloudflare flattens
119
+ # and may truncate a branch into its alias.
120
+ - name: Report preview alias
121
+ if: steps.publish.outputs.pages-deployment-alias-url != ''
122
+ env:
123
+ ALIAS_URL: ${{ steps.publish.outputs.pages-deployment-alias-url }}
124
+ run: |
125
+ echo "canon-preview-alias: $ALIAS_URL"
126
+ echo "Preview: $ALIAS_URL" >> "$GITHUB_STEP_SUMMARY"
127
+
128
+ cleanup-preview:
129
+ name: ๐Ÿงน Delete Branch Previews
130
+ if: github.event_name == 'pull_request'
131
+ runs-on: ubuntu-latest
132
+ steps:
133
+ # Deployments are listed through the REST API rather than wrangler, whose
134
+ # listing carries no branch filter. `force=true` is what lets a deployment
135
+ # still holding its branch alias be deleted. An absent secret skips the
136
+ # job the way the deploy's own guard does.
137
+ - name: Delete preview deployments
138
+ env:
139
+ CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
140
+ CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
141
+ # Fill in the same Pages project name the Publish step deploys to.
142
+ PROJECT_NAME: REPLACE_WITH_PROJECT_NAME
143
+ BRANCH: ${{ github.head_ref }}
144
+ run: |
145
+ if [ -z "$CLOUDFLARE_API_TOKEN" ] || [ -z "$CLOUDFLARE_ACCOUNT_ID" ]; then
146
+ echo "::notice::CLOUDFLARE_API_TOKEN or CLOUDFLARE_ACCOUNT_ID is not set, skipping preview cleanup."
147
+ exit 0
148
+ fi
149
+ api="https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/pages/projects/$PROJECT_NAME/deployments"
150
+ auth="Authorization: Bearer $CLOUDFLARE_API_TOKEN"
151
+ page=1
152
+ ids=""
153
+ while :; do
154
+ listing=$(curl -fsS -H "$auth" "$api?env=preview&per_page=25&page=$page")
155
+ [ "$(echo "$listing" | jq '.result | length')" -eq 0 ] && break
156
+ ids="$ids $(echo "$listing" | jq -r --arg branch "$BRANCH" '.result[] | select(.deployment_trigger.metadata.branch == $branch) | .id')"
157
+ page=$((page + 1))
158
+ done
159
+ deleted=0
160
+ for id in $ids; do
161
+ curl -fsS -X DELETE -H "$auth" "$api/$id?force=true" >/dev/null
162
+ echo "Deleted $id"
163
+ deleted=$((deleted + 1))
164
+ done
165
+ echo "Deleted $deleted preview deployments for $BRANCH."
@@ -8,7 +8,10 @@ The cloudflare stack ships one parameterized GitHub Actions workflow that deploy
8
8
 
9
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
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`.
11
+ - `--branch=${{ github.ref_name }}` on every deploy, which gives every branch a Cloudflare preview URL rather than only `main`. A `workflow_dispatch` run from a branch therefore lands on a preview and never on production, which is what makes dispatching the workflow from an unmerged branch safe.
12
+ - The Publish step carries `id: publish` and pins `wranglerVersion`, since the `pages-deployment-alias-url` output it reads first appears in wrangler 3.78. A step after it prints that alias to the job log as `canon-preview-alias: <url>` and to the step summary. `canon pr preview` reads the line out of the run log, so the marker text is a contract with the CLI rather than decoration.
13
+ - The concurrency group carries `${{ github.ref_name }}`. GitHub cancels a pending run when a newer one queues in the same group, so a shared group would let a branch preview displace a production deploy waiting its turn.
14
+ - A `pull_request` trigger on `closed` runs only the `cleanup-preview` job, which deletes every preview deployment Cloudflare holds for that branch. It lists them through the Pages REST API, because the wrangler listing has no branch filter, and deletes each with `force=true` so a deployment still holding the branch alias goes too. It skips with a notice when the secrets are absent, the same as the deploy job.
12
15
  - 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
16
 
14
17
  ## Parameters a project fills in by hand
@@ -16,8 +19,8 @@ The cloudflare stack ships one parameterized GitHub Actions workflow that deploy
16
19
  Three values in the seeded file have no generic default and are set once, right after sync:
17
20
 
18
21
  - 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.
22
+ - The `concurrency.group` name. The shipped default is `deploy-${{ github.ref_name }}`, which is enough for a single-deploy project. Give the `deploy` prefix a project-specific name only if the project runs more than one deploy workflow, and keep the ref suffix.
23
+ - The Pages project name, written twice as `REPLACE_WITH_PROJECT_NAME`: once in the `--project-name` flag on the `wrangler pages deploy` command and once as `PROJECT_NAME` in the cleanup job. Set both to the name the `deploy-cloudflare` skill creates the Pages project under.
21
24
 
22
25
  ## Re-sync
23
26
 
@@ -12,7 +12,9 @@ export default defineConfig({
12
12
  trace: 'on-first-retry',
13
13
  baseURL,
14
14
  // Nothing to wait out unless a test asserts motion. Opt back in per test with page.emulateMedia({ reducedMotion: 'no-preference' }).
15
- reducedMotion: 'reduce',
15
+ // It is a context option rather than a top-level one, so written beside
16
+ // `baseURL` it type-checks nowhere and reaches no page.
17
+ contextOptions: { reducedMotion: 'reduce' },
16
18
  },
17
19
  projects: [
18
20
  { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
@@ -14,7 +14,9 @@ export default defineConfig({
14
14
  trace: 'on-first-retry',
15
15
  baseURL,
16
16
  // Nothing to wait out unless a test asserts motion. Opt back in per test with page.emulateMedia({ reducedMotion: 'no-preference' }).
17
- reducedMotion: 'reduce',
17
+ // It is a context option rather than a top-level one, so written beside
18
+ // `baseURL` it type-checks nowhere and reaches no page.
19
+ contextOptions: { reducedMotion: 'reduce' },
18
20
  },
19
21
  projects: [
20
22
  { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
@@ -4,3 +4,11 @@ test('home page loads', async ({ page }) => {
4
4
  await page.goto('/')
5
5
  await expect(page).toHaveTitle(/.+/)
6
6
  })
7
+
8
+ test('reduced motion reaches the page', async ({ page }) => {
9
+ await page.goto('/')
10
+ const isReduced = await page.evaluate(
11
+ () => window.matchMedia('(prefers-reduced-motion: reduce)').matches,
12
+ )
13
+ expect(isReduced).toBe(true)
14
+ })