@erclx/canon 5.12.0 → 5.13.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/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/api-design/REQUIREMENT.md +1 -1
- package/claude/skills/api-design/SKILL.md +1 -1
- package/claude/skills/api-design/references/adopted.md +1 -1
- package/claude/skills/auto-ship/REQUIREMENT.md +1 -0
- package/claude/skills/auto-ship/SKILL.md +2 -2
- package/claude/skills/build-in-slices/REQUIREMENT.md +45 -0
- package/claude/skills/build-in-slices/SKILL.md +94 -0
- package/claude/skills/build-in-slices/references/adopted.md +41 -0
- package/claude/skills/code-craft/REQUIREMENT.md +48 -0
- package/claude/skills/code-craft/SKILL.md +107 -0
- package/claude/skills/code-craft/references/adopted.md +52 -0
- package/claude/skills/codebase-layout/REQUIREMENT.md +1 -1
- package/claude/skills/git-ship/REQUIREMENT.md +1 -0
- package/claude/skills/git-ship/SKILL.md +1 -1
- package/claude/skills/review-craft/REQUIREMENT.md +3 -0
- package/claude/skills/review-craft/SKILL.md +1 -1
- package/claude/skills/review-craft/references/adopted.md +8 -0
- package/claude/skills/review-craft/references/security.md +5 -0
- package/claude/skills/review-ui/REQUIREMENT.md +1 -0
- package/claude/skills/review-ui/SKILL.md +11 -0
- package/claude/skills/role-worker/REQUIREMENT.md +1 -0
- package/claude/skills/role-worker/SKILL.md +4 -0
- package/claude/skills/test-first/REQUIREMENT.md +3 -0
- package/claude/skills/test-first/SKILL.md +2 -1
- package/docs/agents/pr-preview.md +40 -12
- package/docs/workflow/skill-map.md +2 -0
- package/governance/rules/code/000-code.md +5 -1
- package/package.json +1 -1
- package/src/claude/cases/misc.ts +10 -0
- package/src/claude/skills-audit.ts +2 -0
- package/src/commands/pr.ts +107 -7
- package/src/commands/tooling.ts +39 -16
- package/src/pr/preview.ts +66 -0
- package/src/tooling/manifest.ts +16 -0
- package/src/tooling/verify-stack.ts +249 -0
- package/tooling/base/seeds/.cspell/tech-stack.txt +2 -0
- package/tooling/go/manifest.toml +3 -0
- package/tooling/go/reference.md +1 -0
- package/tooling/php/manifest.toml +3 -0
- package/tooling/php/reference.md +1 -0
- package/tooling/python/manifest.toml +3 -0
- package/tooling/python/reference.md +1 -1
- package/tooling/python/seeds/.cspell/tech-stack.txt +0 -1
- package/scripts/tooling/verify.sh +0 -184
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, rmSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { execa, type Options } from 'execa'
|
|
4
|
+
import { isStackExcluded, loadManifest } from '@/tooling/manifest'
|
|
5
|
+
import { intro, logError, logInfo, logStep, logWarn, outro } from '@/ui'
|
|
6
|
+
|
|
7
|
+
const SCREENSHOT_TIMEOUT_MS = 60_000
|
|
8
|
+
const TAIL_LINES = 20
|
|
9
|
+
|
|
10
|
+
export interface VerifyOptions {
|
|
11
|
+
readonly root: string
|
|
12
|
+
readonly stack: string
|
|
13
|
+
readonly keep?: boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface PhaseResult {
|
|
17
|
+
readonly name: string
|
|
18
|
+
readonly passed: boolean
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface VerifyOutcome {
|
|
22
|
+
readonly results: readonly PhaseResult[]
|
|
23
|
+
readonly exitCode: number
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface Run {
|
|
27
|
+
readonly dir: string
|
|
28
|
+
readonly results: PhaseResult[]
|
|
29
|
+
isFailed: boolean
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Scaffolds a stack into `.canon/tmp/runs/verify-<stack>/`, syncs its chain
|
|
34
|
+
* from the checkout at `root`, and runs the scaffold's own `lint:fix`,
|
|
35
|
+
* `check`, `test:e2e`, and `screenshot` scripts. Every line goes to stderr.
|
|
36
|
+
*
|
|
37
|
+
* A failed phase is recorded and the run carries on, so the Results block
|
|
38
|
+
* reports every phase rather than the first failure. The tmp dir is removed
|
|
39
|
+
* only on a clean run without `keep`.
|
|
40
|
+
*/
|
|
41
|
+
export async function verifyStack(opts: VerifyOptions): Promise<VerifyOutcome> {
|
|
42
|
+
intro(`canon tooling verify ${opts.stack}`)
|
|
43
|
+
// A signal skips the `finally` below, so the handler closes the frame on
|
|
44
|
+
// that path and leaves the tmp dir in place for inspection.
|
|
45
|
+
let isOpen = true
|
|
46
|
+
const close = (): void => {
|
|
47
|
+
if (isOpen) outro()
|
|
48
|
+
isOpen = false
|
|
49
|
+
}
|
|
50
|
+
const handleSignal = (): void => {
|
|
51
|
+
close()
|
|
52
|
+
process.exit(130)
|
|
53
|
+
}
|
|
54
|
+
process.once('SIGINT', handleSignal)
|
|
55
|
+
process.once('SIGTERM', handleSignal)
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
return await run(opts)
|
|
59
|
+
} finally {
|
|
60
|
+
process.off('SIGINT', handleSignal)
|
|
61
|
+
process.off('SIGTERM', handleSignal)
|
|
62
|
+
close()
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function run({
|
|
67
|
+
root,
|
|
68
|
+
stack,
|
|
69
|
+
keep = false,
|
|
70
|
+
}: VerifyOptions): Promise<VerifyOutcome> {
|
|
71
|
+
const refuse = (message: string): VerifyOutcome => {
|
|
72
|
+
logError(message)
|
|
73
|
+
return { results: [], exitCode: 1 }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (isStackExcluded(stack)) {
|
|
77
|
+
return refuse(`Stack '${stack}' is excluded from tooling.`)
|
|
78
|
+
}
|
|
79
|
+
const manifest = loadManifest(root, stack)
|
|
80
|
+
if (manifest === undefined) {
|
|
81
|
+
return refuse(`No manifest at tooling/${stack}/manifest.toml`)
|
|
82
|
+
}
|
|
83
|
+
if (manifest.scaffold === undefined) {
|
|
84
|
+
return refuse(`Stack '${stack}' has no scaffold command in manifest.`)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const runsDir = join(root, '.canon/tmp/runs')
|
|
88
|
+
const name = `verify-${stack}`
|
|
89
|
+
const state: Run = { dir: join(runsDir, name), results: [], isFailed: false }
|
|
90
|
+
|
|
91
|
+
mkdirSync(runsDir, { recursive: true })
|
|
92
|
+
rmSync(state.dir, { recursive: true, force: true })
|
|
93
|
+
|
|
94
|
+
logStep(`Scaffolding ${stack}`)
|
|
95
|
+
// Manifests carry shell syntax (`&&`, `cd`, quoted globs), so the command
|
|
96
|
+
// runs under a shell rather than as a split argv.
|
|
97
|
+
const scaffold = await shell(
|
|
98
|
+
manifest.scaffold.replaceAll('{{name}}', name),
|
|
99
|
+
runsDir,
|
|
100
|
+
)
|
|
101
|
+
if (scaffold.failed) {
|
|
102
|
+
logWarn('Scaffold failed')
|
|
103
|
+
writeTail(scaffold.all)
|
|
104
|
+
state.isFailed = true
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!existsSync(state.dir)) {
|
|
108
|
+
return refuse(`Scaffold did not produce ${state.dir}`)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
await shell(
|
|
112
|
+
'git init -q && git add . && git commit -m "chore: verify scaffold" -q --no-verify --allow-empty',
|
|
113
|
+
state.dir,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
const { prepare } = manifest
|
|
117
|
+
if (prepare !== undefined) {
|
|
118
|
+
await phase(state, 'Prepare', () => shell(prepare, state.dir))
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const cli = join(root, 'src/cli.ts')
|
|
122
|
+
logInfo(`Sync resolves via bun ${cli}, not PATH canon`)
|
|
123
|
+
await phase(state, 'Sync', () =>
|
|
124
|
+
execa('bun', [cli, 'tooling', 'sync', stack, '.', '--write'], {
|
|
125
|
+
...captured(state.dir),
|
|
126
|
+
env: { CI: '1', CANON_NON_INTERACTIVE: '1' },
|
|
127
|
+
}),
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
const packagePath = join(state.dir, 'package.json')
|
|
131
|
+
if (existsSync(packagePath)) {
|
|
132
|
+
await runScripts(state, readScriptKeys(packagePath))
|
|
133
|
+
} else {
|
|
134
|
+
logError(
|
|
135
|
+
`No package.json after Sync for ${stack}, so no phase past Sync can run.`,
|
|
136
|
+
)
|
|
137
|
+
state.isFailed = true
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
logStep('Results')
|
|
141
|
+
for (const result of state.results) {
|
|
142
|
+
if (result.passed) logInfo(result.name)
|
|
143
|
+
else logWarn(result.name)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (!keep && !state.isFailed) {
|
|
147
|
+
rmSync(state.dir, { recursive: true, force: true })
|
|
148
|
+
} else {
|
|
149
|
+
logInfo(`Kept ${state.dir}`)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return { results: state.results, exitCode: state.isFailed ? 1 : 0 }
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* `lint:fix` runs first and reformats the scaffold, so `check` reads the
|
|
157
|
+
* reformatted tree.
|
|
158
|
+
*/
|
|
159
|
+
async function runScripts(state: Run, scripts: Set<string>): Promise<void> {
|
|
160
|
+
await phase(state, 'lint:fix', () => bunRun('lint:fix', state.dir))
|
|
161
|
+
await phase(state, 'check', () => bunRun('check', state.dir))
|
|
162
|
+
|
|
163
|
+
if (scripts.has('test:e2e')) {
|
|
164
|
+
await phase(state, 'test:e2e', () => bunRun('test:e2e', state.dir))
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (!scripts.has('screenshot')) return
|
|
168
|
+
const isShot = await phase(state, 'screenshot', () =>
|
|
169
|
+
bunRun('screenshot', state.dir, SCREENSHOT_TIMEOUT_MS),
|
|
170
|
+
)
|
|
171
|
+
// A killed or failed screenshot run is already a failure, and its partial
|
|
172
|
+
// output says nothing about the artifacts.
|
|
173
|
+
if (!isShot) return
|
|
174
|
+
|
|
175
|
+
const shots = countScreenshots(join(state.dir, 'screenshots'))
|
|
176
|
+
if (shots === 0) logWarn('screenshot produced no png files')
|
|
177
|
+
record(state, 'screenshot-artifacts', shots > 0)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
type Spawned = Awaited<ReturnType<typeof execa>>
|
|
181
|
+
|
|
182
|
+
async function phase(
|
|
183
|
+
state: Run,
|
|
184
|
+
name: string,
|
|
185
|
+
spawn: () => Promise<Spawned>,
|
|
186
|
+
): Promise<boolean> {
|
|
187
|
+
logStep(name)
|
|
188
|
+
const result = await spawn()
|
|
189
|
+
if (!result.failed) {
|
|
190
|
+
logInfo(`${name} passed`)
|
|
191
|
+
record(state, name, true)
|
|
192
|
+
return true
|
|
193
|
+
}
|
|
194
|
+
logWarn(result.timedOut ? `${name} timed out` : `${name} failed`)
|
|
195
|
+
writeTail(result.all)
|
|
196
|
+
record(state, name, false)
|
|
197
|
+
return false
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function record(state: Run, name: string, passed: boolean): void {
|
|
201
|
+
state.results.push({ name, passed })
|
|
202
|
+
if (!passed) state.isFailed = true
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function captured(cwd: string): Options {
|
|
206
|
+
return { cwd, all: true, reject: false, stdin: 'ignore' }
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function shell(command: string, cwd: string): Promise<Spawned> {
|
|
210
|
+
return execa(command, {
|
|
211
|
+
...captured(cwd),
|
|
212
|
+
shell: 'bash',
|
|
213
|
+
env: { CI: '1' },
|
|
214
|
+
})
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function bunRun(
|
|
218
|
+
script: string,
|
|
219
|
+
cwd: string,
|
|
220
|
+
timeout?: number,
|
|
221
|
+
): Promise<Spawned> {
|
|
222
|
+
return execa('bun', ['run', script], {
|
|
223
|
+
...captured(cwd),
|
|
224
|
+
env: { CI: '1' },
|
|
225
|
+
...(timeout !== undefined ? { timeout } : {}),
|
|
226
|
+
})
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function writeTail(output: unknown): void {
|
|
230
|
+
if (typeof output !== 'string' || output === '') return
|
|
231
|
+
const lines = output.replace(/\n$/, '').split('\n').slice(-TAIL_LINES)
|
|
232
|
+
process.stderr.write(`${lines.join('\n')}\n`)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function readScriptKeys(packagePath: string): Set<string> {
|
|
236
|
+
try {
|
|
237
|
+
const parsed = JSON.parse(readFileSync(packagePath, 'utf8')) as {
|
|
238
|
+
scripts?: Record<string, unknown>
|
|
239
|
+
}
|
|
240
|
+
return new Set(Object.keys(parsed.scripts ?? {}))
|
|
241
|
+
} catch {
|
|
242
|
+
return new Set()
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function countScreenshots(dir: string): number {
|
|
247
|
+
if (!existsSync(dir)) return 0
|
|
248
|
+
return [...new Bun.Glob('**/*.png').scanSync({ cwd: dir, dot: true })].length
|
|
249
|
+
}
|
package/tooling/go/manifest.toml
CHANGED
|
@@ -11,3 +11,6 @@ scaffold = "mkdir {{name}} && cd {{name}} && go mod init example.com/{{name}} &&
|
|
|
11
11
|
"test" = "go test ./..."
|
|
12
12
|
"test:run" = "go test -v ./..."
|
|
13
13
|
"build" = "go build ./..."
|
|
14
|
+
|
|
15
|
+
[verify]
|
|
16
|
+
prepare = "bun init -y && (command -v golangci-lint >/dev/null || go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest)"
|
package/tooling/go/reference.md
CHANGED
|
@@ -32,6 +32,7 @@ The go stack also ships `smoke_test.go` at the module root as a copy-once seed.
|
|
|
32
32
|
- Probe `go` and `golangci-lint` first, and name the `go install` command when `golangci-lint` is missing.
|
|
33
33
|
- Keep `build` out of `verify.json`, even though the stack declares the script. `go build ./...` exits 1 with `no packages to build` on a module holding only a test file, and a seeded `main.go` would assume every module is a binary rather than a library.
|
|
34
34
|
- Keep `-race` out of `test:run`. The race detector needs cgo and a C toolchain, so it belongs in CI rather than in a local `bun run check`.
|
|
35
|
+
- The manifest's `[verify] prepare` runs checklist steps 3 and 6 for `canon tooling verify go`: `bun init -y`, then the `golangci-lint` `go install` when the binary is not already on PATH. The scaffold writes no `package.json`, and a verify that reaches its end with none fails.
|
|
35
36
|
|
|
36
37
|
## CI
|
|
37
38
|
|
|
@@ -13,3 +13,6 @@ scaffold = "mkdir {{name}} && cd {{name}} && composer init -n --name example/{{n
|
|
|
13
13
|
|
|
14
14
|
[gitignore]
|
|
15
15
|
"# PHP" = ["vendor/", ".php-cs-fixer.cache", ".phpunit.cache/", ".phpunit.result.cache"]
|
|
16
|
+
|
|
17
|
+
[verify]
|
|
18
|
+
prepare = "bun init -y && composer require --dev friendsofphp/php-cs-fixer phpstan/phpstan phpunit/phpunit"
|
package/tooling/php/reference.md
CHANGED
|
@@ -44,6 +44,7 @@ The php stack also ships `tests/SmokeTest.php` as a copy-once seed, so `bun run
|
|
|
44
44
|
- Probe `php` and `composer` first.
|
|
45
45
|
- Hold a precondition on `vendor/bin/phpunit`, so the run stops and names `composer install` when it is missing.
|
|
46
46
|
- Require Typecheck (`phpstan analyse`), Lint (`php-cs-fixer check`), and Unit tests (`phpunit --testdox`). Base's runner adds the four base phases wherever the folder declares their scripts, then the markdown bans stage.
|
|
47
|
+
- The manifest's `[verify] prepare` runs checklist steps 2 and 5 for `canon tooling verify php`: `bun init -y`, then the `composer require --dev` line. The scaffold writes no `package.json`, and a verify that reaches its end with none fails.
|
|
47
48
|
|
|
48
49
|
## Gitignore
|
|
49
50
|
|
|
@@ -39,7 +39,7 @@ The manifest declares no `[dependencies.dev]`. Manifest injection currently call
|
|
|
39
39
|
|
|
40
40
|
## Verify command
|
|
41
41
|
|
|
42
|
-
`canon tooling verify
|
|
42
|
+
`canon tooling verify python` runs the manifest's `[verify] prepare` before Sync, which is checklist steps 2, 5, and 6: `bun init -y`, `uv add --dev ruff mypy pytest pytest-cov`, and `uv sync`. `uv init` writes no `package.json`, and a verify that reaches its end with none fails, so the prepare is what lets `bun run lint:fix` and `bun run check` run at all. Python's `verify.json` requires the `lint`, `typecheck`, and `test:run` scripts that delegate to `uv run`, so the run needs `uv` on the machine and a network for the installs. The end-to-end test and screenshot phases are gated on `package.json` script keys that python does not declare, so they skip.
|
|
43
43
|
|
|
44
44
|
## Anti-patterns
|
|
45
45
|
|
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
set -e
|
|
3
|
-
set -o pipefail
|
|
4
|
-
|
|
5
|
-
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
6
|
-
PROJECT_ROOT="${PROJECT_ROOT:-$(dirname "$(dirname "$SCRIPT_DIR")")}"
|
|
7
|
-
|
|
8
|
-
source "$PROJECT_ROOT/scripts/lib/ui.sh"
|
|
9
|
-
source "$PROJECT_ROOT/scripts/lib/tooling.sh"
|
|
10
|
-
|
|
11
|
-
show_help() {
|
|
12
|
-
echo -e "${GREY}┌${NC}"
|
|
13
|
-
echo -e "${GREY}├${NC} ${WHITE}Usage:${NC} canon tooling verify <stack> [options]"
|
|
14
|
-
echo -e "${GREY}│${NC}"
|
|
15
|
-
echo -e "${GREY}│${NC} ${WHITE}Options:${NC}"
|
|
16
|
-
echo -e "${GREY}│${NC} --keep ${GREY}# Keep tmp dir after run for inspection${NC}"
|
|
17
|
-
echo -e "${GREY}│${NC} -h, --help ${GREY}# Show this help message${NC}"
|
|
18
|
-
echo -e "${GREY}│${NC}"
|
|
19
|
-
echo -e "${GREY}│${NC} Scaffolds <stack> into .canon/tmp/runs/verify-<stack>/, syncs the full"
|
|
20
|
-
echo -e "${GREY}│${NC} layer chain, and runs check + test:e2e + screenshot."
|
|
21
|
-
echo -e "${GREY}└${NC}"
|
|
22
|
-
exit 0
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
read_manifest_field() {
|
|
26
|
-
local toml="$1"
|
|
27
|
-
local section="$2"
|
|
28
|
-
local field="$3"
|
|
29
|
-
awk -v s="$section" -v f="$field" '
|
|
30
|
-
$0 == "[" s "]" { in_section = 1; next }
|
|
31
|
-
in_section && /^\[/ { exit }
|
|
32
|
-
in_section && match($0, "^" f "[[:space:]]*=") {
|
|
33
|
-
val = substr($0, RLENGTH + 1)
|
|
34
|
-
sub(/^[[:space:]]+/, "", val)
|
|
35
|
-
sub(/^"/, "", val)
|
|
36
|
-
sub(/"$/, "", val)
|
|
37
|
-
print val
|
|
38
|
-
exit
|
|
39
|
-
}
|
|
40
|
-
' "$toml"
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
run_phase() {
|
|
44
|
-
local name="$1"
|
|
45
|
-
shift
|
|
46
|
-
log_step "$name"
|
|
47
|
-
if "$@" >/tmp/verify-phase.log 2>&1; then
|
|
48
|
-
log_info "$name passed"
|
|
49
|
-
RESULTS+=("$name:pass")
|
|
50
|
-
return 0
|
|
51
|
-
fi
|
|
52
|
-
log_warn "$name failed"
|
|
53
|
-
tail -20 /tmp/verify-phase.log >&2
|
|
54
|
-
RESULTS+=("$name:fail")
|
|
55
|
-
FAILED=1
|
|
56
|
-
return 1
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
main() {
|
|
60
|
-
local stack=""
|
|
61
|
-
local keep=0
|
|
62
|
-
|
|
63
|
-
while [[ $# -gt 0 ]]; do
|
|
64
|
-
case "$1" in
|
|
65
|
-
-h | --help) show_help ;;
|
|
66
|
-
--keep)
|
|
67
|
-
keep=1
|
|
68
|
-
shift
|
|
69
|
-
;;
|
|
70
|
-
-*)
|
|
71
|
-
log_error "Unknown option: $1"
|
|
72
|
-
;;
|
|
73
|
-
*)
|
|
74
|
-
if [ -z "$stack" ]; then
|
|
75
|
-
stack="$1"
|
|
76
|
-
shift
|
|
77
|
-
else
|
|
78
|
-
log_error "Unexpected argument: $1"
|
|
79
|
-
fi
|
|
80
|
-
;;
|
|
81
|
-
esac
|
|
82
|
-
done
|
|
83
|
-
|
|
84
|
-
[ -z "$stack" ] && log_error "Stack required. See --help."
|
|
85
|
-
|
|
86
|
-
export CI=1
|
|
87
|
-
|
|
88
|
-
if is_tooling_stack_excluded "$stack"; then
|
|
89
|
-
log_error "Stack '$stack' is excluded from tooling."
|
|
90
|
-
fi
|
|
91
|
-
|
|
92
|
-
local manifest="$PROJECT_ROOT/tooling/$stack/manifest.toml"
|
|
93
|
-
[ ! -f "$manifest" ] && log_error "No manifest at $manifest"
|
|
94
|
-
|
|
95
|
-
local scaffold
|
|
96
|
-
scaffold=$(read_manifest_field "$manifest" "stack" "scaffold")
|
|
97
|
-
[ -z "$scaffold" ] && log_error "Stack '$stack' has no scaffold command in manifest."
|
|
98
|
-
|
|
99
|
-
local prepare
|
|
100
|
-
prepare=$(read_manifest_field "$manifest" "verify" "prepare")
|
|
101
|
-
|
|
102
|
-
open_timeline "canon tooling verify $stack"
|
|
103
|
-
trap close_timeline EXIT
|
|
104
|
-
|
|
105
|
-
local tmp_root="$PROJECT_ROOT/.canon/tmp/runs"
|
|
106
|
-
local tmp_dir="$tmp_root/verify-$stack"
|
|
107
|
-
|
|
108
|
-
mkdir -p "$tmp_root"
|
|
109
|
-
rm -rf "$tmp_dir"
|
|
110
|
-
|
|
111
|
-
local project_name="verify-$stack"
|
|
112
|
-
local scaffold_cmd="${scaffold//\{\{name\}\}/$project_name}"
|
|
113
|
-
|
|
114
|
-
RESULTS=()
|
|
115
|
-
FAILED=0
|
|
116
|
-
|
|
117
|
-
log_step "Scaffolding $stack"
|
|
118
|
-
(cd "$tmp_root" && eval "$scaffold_cmd") >/tmp/verify-scaffold.log 2>&1 || {
|
|
119
|
-
log_warn "Scaffold failed"
|
|
120
|
-
tail -20 /tmp/verify-scaffold.log >&2
|
|
121
|
-
FAILED=1
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
if [ ! -d "$tmp_dir" ]; then
|
|
125
|
-
log_error "Scaffold did not produce $tmp_dir"
|
|
126
|
-
fi
|
|
127
|
-
|
|
128
|
-
(cd "$tmp_dir" && git init -q && git add . && git commit -m "chore: verify scaffold" -q --no-verify)
|
|
129
|
-
|
|
130
|
-
if [ -n "$prepare" ]; then
|
|
131
|
-
run_phase "Prepare" bash -c "cd '$tmp_dir' && $prepare"
|
|
132
|
-
fi
|
|
133
|
-
|
|
134
|
-
log_info "Sync resolves via bun $PROJECT_ROOT/src/cli.ts, not PATH canon"
|
|
135
|
-
run_phase "Sync" bash -c "cd '$tmp_dir' && CANON_NON_INTERACTIVE=1 bun '$PROJECT_ROOT/src/cli.ts' tooling sync $stack . --write"
|
|
136
|
-
|
|
137
|
-
if [ -f "$tmp_dir/package.json" ]; then
|
|
138
|
-
run_phase "lint:fix" bash -c "cd '$tmp_dir' && bun run lint:fix"
|
|
139
|
-
run_phase "check" bash -c "cd '$tmp_dir' && bun run check"
|
|
140
|
-
|
|
141
|
-
if grep -q '"test:e2e"' "$tmp_dir/package.json"; then
|
|
142
|
-
run_phase "test:e2e" bash -c "cd '$tmp_dir' && bun run test:e2e"
|
|
143
|
-
fi
|
|
144
|
-
|
|
145
|
-
if grep -q '"screenshot"' "$tmp_dir/package.json"; then
|
|
146
|
-
if run_phase "screenshot" bash -c "cd '$tmp_dir' && timeout 60 bun run screenshot"; then
|
|
147
|
-
local shots
|
|
148
|
-
shots=$(find "$tmp_dir/screenshots" -type f -name '*.png' 2>/dev/null | wc -l)
|
|
149
|
-
if [ "$shots" -eq 0 ]; then
|
|
150
|
-
log_warn "screenshot produced no png files"
|
|
151
|
-
RESULTS+=("screenshot-artifacts:fail")
|
|
152
|
-
FAILED=1
|
|
153
|
-
else
|
|
154
|
-
RESULTS+=("screenshot-artifacts:pass")
|
|
155
|
-
fi
|
|
156
|
-
fi
|
|
157
|
-
fi
|
|
158
|
-
fi
|
|
159
|
-
|
|
160
|
-
log_step "Results"
|
|
161
|
-
for r in "${RESULTS[@]}"; do
|
|
162
|
-
local name="${r%:*}"
|
|
163
|
-
local status="${r##*:}"
|
|
164
|
-
if [ "$status" = "pass" ]; then
|
|
165
|
-
log_info "$name"
|
|
166
|
-
else
|
|
167
|
-
log_warn "$name"
|
|
168
|
-
fi
|
|
169
|
-
done
|
|
170
|
-
|
|
171
|
-
if [ "$keep" -eq 0 ] && [ "$FAILED" -eq 0 ]; then
|
|
172
|
-
rm -rf "$tmp_dir"
|
|
173
|
-
else
|
|
174
|
-
log_info "Kept $tmp_dir"
|
|
175
|
-
fi
|
|
176
|
-
|
|
177
|
-
if [ "$FAILED" -ne 0 ]; then
|
|
178
|
-
exit 1
|
|
179
|
-
fi
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
|
183
|
-
main "$@"
|
|
184
|
-
fi
|