@codepassion/skills 1.2.0 → 1.5.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 (34) hide show
  1. package/README.md +6 -1
  2. package/SKILLS.md +6 -1
  3. package/bin/c9n-skills.js +28 -3
  4. package/install.sh +25 -2
  5. package/package.json +1 -1
  6. package/skills/c9n/SKILL.md +322 -0
  7. package/skills/c9n-pipeline/SKILL.md +171 -0
  8. package/skills/c9n-pipeline/templates/Dockerfile.api +49 -0
  9. package/skills/c9n-pipeline/templates/Dockerfile.service +45 -0
  10. package/skills/c9n-pipeline/templates/Dockerfile.web +55 -0
  11. package/skills/c9n-pipeline/templates/dockerignore +18 -0
  12. package/skills/c9n-pipeline/templates/workflows/_build-job.partial.yml +13 -0
  13. package/skills/c9n-pipeline/templates/workflows/_deploy-job.partial.yml +17 -0
  14. package/skills/c9n-pipeline/templates/workflows/_migration-check.partial.yml +16 -0
  15. package/skills/c9n-pipeline/templates/workflows/ci.yml +50 -0
  16. package/skills/c9n-pipeline/templates/workflows/deployment.yml +21 -0
  17. package/skills/c9n-sentry/REFERENCE.md +172 -0
  18. package/skills/c9n-sentry/SKILL.md +53 -0
  19. package/skills/c9n-spec/SKILL.md +92 -0
  20. package/skills/c9n-spec/templates/.github/pull_request_template.md +29 -0
  21. package/skills/c9n-spec/templates/AGENTS.md +179 -0
  22. package/skills/c9n-spec/templates/CLAUDE.md +6 -0
  23. package/skills/c9n-spec/templates/CONTEXT.md +53 -0
  24. package/skills/c9n-spec/templates/DESIGN.md +73 -0
  25. package/skills/c9n-spec/templates/docs/SRS.md +37 -0
  26. package/skills/c9n-spec/templates/docs/adr/0001-spec-driven-iso29110-docs.md +44 -0
  27. package/skills/c9n-spec/templates/docs/adr/_TEMPLATE.md +26 -0
  28. package/skills/c9n-spec/templates/docs/agents/domain.md +43 -0
  29. package/skills/c9n-spec/templates/docs/agents/issue-tracker.md +22 -0
  30. package/skills/c9n-spec/templates/docs/agents/triage-labels.md +27 -0
  31. package/skills/c9n-spec/templates/docs/architecture.md +50 -0
  32. package/skills/c9n-spec/templates/docs/features/_TEMPLATE.md +34 -0
  33. package/skills/c9n-spec/templates/docs/test-plan.md +30 -0
  34. package/skills/c9n-spec/templates/docs/traceability.md +27 -0
package/README.md CHANGED
@@ -8,8 +8,13 @@ Agent skills (SKILL.md) based on the CodePassion (C9N) conventions.
8
8
  npx @codepassion/skills install codex # ~/.codex/skills
9
9
  npx @codepassion/skills install project # ./.agents/skills
10
10
  npx @codepassion/skills install combined # one combined skill
11
+ npx @codepassion/skills install cowork # zip skills for claude.ai/Cowork upload (UI, no API)
11
12
 
12
- Or: ./install.sh {claude|codex|project|combined}
13
+ Or: ./install.sh {claude|codex|project|combined|cowork}
14
+
15
+ claude.ai and Claude Cowork share a personal skill library with no upload API, so `cowork`
16
+ packages upload-ready zips into dist/cowork/ — upload them at
17
+ claude.ai → Settings → Capabilities → Upload skill.
13
18
 
14
19
  ## Edit
15
20
 
package/SKILLS.md CHANGED
@@ -8,8 +8,13 @@ Agent skills (SKILL.md) based on the CodePassion (C9N) conventions.
8
8
  npx @codepassion/skills install codex # ~/.codex/skills
9
9
  npx @codepassion/skills install project # ./.agents/skills
10
10
  npx @codepassion/skills install combined # one combined skill
11
+ npx @codepassion/skills install cowork # zip skills for claude.ai/Cowork upload (UI, no API)
11
12
 
12
- Or: ./install.sh {claude|codex|project|combined}
13
+ Or: ./install.sh {claude|codex|project|combined|cowork}
14
+
15
+ claude.ai and Claude Cowork share a personal skill library with no upload API, so `cowork`
16
+ packages upload-ready zips into dist/cowork/ — upload them at
17
+ claude.ai → Settings → Capabilities → Upload skill.
13
18
 
14
19
  ## Edit
15
20
 
package/bin/c9n-skills.js CHANGED
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import { cp, mkdir, readdir, stat } from 'node:fs/promises'
2
+ import { cp, mkdir, readdir, rm, stat } from 'node:fs/promises'
3
3
  import { existsSync } from 'node:fs'
4
4
  import { homedir } from 'node:os'
5
5
  import { dirname, join, resolve } from 'node:path'
6
6
  import { fileURLToPath } from 'node:url'
7
+ import { execFile } from 'node:child_process'
7
8
  const SRC = resolve(dirname(fileURLToPath(import.meta.url)), '..', 'skills')
8
9
  const [,, cmd='install', target='claude', ...rest] = process.argv
9
10
  if (cmd==='help'||cmd==='--help'||cmd==='-h'){ help(); process.exit(0) }
@@ -12,7 +13,12 @@ const flags={dest:null,force:false,dryRun:false}
12
13
  for (let i=0;i<rest.length;i++){ const a=rest[i]
13
14
  if (a==='--force') flags.force=true
14
15
  else if (a==='--dry-run') flags.dryRun=true
15
- else if (a==='--dest') flags.dest=resolve(rest[++i]??'') }
16
+ else if (a==='--dest'){ const v=rest[++i]
17
+ if (!v){ console.error('--dest requires a value'); process.exit(1) }
18
+ flags.dest=resolve(v) } }
19
+ // cowork: claude.ai / Cowork share a personal skill library with NO upload API —
20
+ // this packages upload-ready zips for manual drag-drop in Settings → Capabilities.
21
+ if (target==='cowork'){ await packageForCowork(flags); process.exit(0) }
16
22
  const dest = flags.dest ?? defaultDest(target)
17
23
  if (!dest){ help(); process.exit(1) }
18
24
  const sources = await resolveSources(target)
@@ -33,4 +39,23 @@ function defaultDest(t){ const h=homedir()
33
39
  if (t==='codex') return join(h,'.codex','skills')
34
40
  if (t==='project') return join(process.cwd(),'.agents','skills')
35
41
  return null }
36
- function help(){ console.log('c9n-skills install [claude|codex|project|combined] [--dest <dir>] [--force] [--dry-run]') }
42
+ async function packageForCowork(flags){
43
+ const ROOT=resolve(SRC,'..')
44
+ // only auto-clean the default output dir — never delete a user-supplied --dest
45
+ const isDefaultOut=!flags.dest
46
+ const out=flags.dest ?? join(ROOT,'dist','cowork')
47
+ const sources=[...await resolveSources('combined'), ...await resolveSources('cowork')]
48
+ if (!sources.length){ console.error('No skills found'); process.exit(1) }
49
+ console.log(`Packaging ${sources.length} skill zip(s) into ${out}${flags.dryRun?' (dry-run)':''}`)
50
+ if (!flags.dryRun){ if (isDefaultOut) await rm(out,{recursive:true,force:true}); await mkdir(out,{recursive:true}) }
51
+ for (const src of sources){ const n=src.split(/[\\/]/).pop()
52
+ console.log(` zip ${n}.zip`)
53
+ if (!flags.dryRun) await zipDir(SRC,n,join(out,`${n}.zip`)) }
54
+ console.log('Done. Upload at claude.ai → Settings → Capabilities → Upload skill (shared with Cowork).')
55
+ console.log('Action skills (c9n-spec, c9n-deliverables) bundle their templates/scripts in the zip.')
56
+ }
57
+ function zipDir(cwd,name,outFile){
58
+ return new Promise((res,rej)=>{ execFile('zip',['-qr',outFile,name],{cwd},(e)=>{
59
+ if (e && e.code==='ENOENT') rej(new Error("'zip' command not found — install zip and retry"))
60
+ else if (e) rej(e); else res() }) }) }
61
+ function help(){ console.log('c9n-skills install [claude|codex|project|combined|cowork] [--dest <dir>] [--force] [--dry-run]') }
package/install.sh CHANGED
@@ -1,13 +1,36 @@
1
1
  #!/usr/bin/env bash
2
2
  set -euo pipefail
3
- SRC="$(cd "$(dirname "$0")" && pwd)/skills"
3
+ ROOT="$(cd "$(dirname "$0")" && pwd)"
4
+ SRC="$ROOT/skills"
4
5
  MODE="${1:-claude}"
6
+
7
+ # cowork: claude.ai / Cowork share a personal skill library with NO upload API —
8
+ # package upload-ready zips for manual drag-drop in Settings → Capabilities.
9
+ if [ "$MODE" = "cowork" ]; then
10
+ command -v zip >/dev/null || { echo "error: 'zip' not found — install zip and retry"; exit 1; }
11
+ DEST="${TARGET_DIR:-$ROOT/dist/cowork}"
12
+ # only auto-clean the default output dir — never delete a user-supplied TARGET_DIR
13
+ if [ -z "${TARGET_DIR:-}" ]; then rm -rf "$DEST"; fi
14
+ mkdir -p "$DEST"
15
+ count=0
16
+ for d in "$SRC"/c9n "$SRC"/c9n-*; do
17
+ [ -d "$d" ] || continue
18
+ name="$(basename "$d")"
19
+ (cd "$SRC" && zip -qr "$DEST/$name.zip" "$name")
20
+ count=$((count + 1))
21
+ done
22
+ echo "Packaged $count skill zip(s) into $DEST"
23
+ echo "Upload at claude.ai → Settings → Capabilities → Upload skill (shared with Cowork)."
24
+ command -v open >/dev/null && open "$DEST" 2>/dev/null || true
25
+ exit 0
26
+ fi
27
+
5
28
  case "$MODE" in
6
29
  claude) DEST="${TARGET_DIR:-$HOME/.claude/skills}" ;;
7
30
  codex) DEST="${TARGET_DIR:-$HOME/.codex/skills}" ;;
8
31
  project) DEST="${TARGET_DIR:-$PWD/.agents/skills}" ;;
9
32
  combined) DEST="${TARGET_DIR:-$HOME/.claude/skills}" ;;
10
- *) echo "Use: claude | codex | project | combined"; exit 1 ;;
33
+ *) echo "Use: claude | codex | project | combined | cowork"; exit 1 ;;
11
34
  esac
12
35
  mkdir -p "$DEST"
13
36
  if [ "$MODE" = "combined" ]; then cp -R "$SRC/c9n" "$DEST/"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codepassion/skills",
3
- "version": "1.2.0",
3
+ "version": "1.5.0",
4
4
  "description": "Agent skills (SKILL.md) for CodePassion (C9N) engineering conventions.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -237,3 +237,325 @@ These ALWAYS run, in order — never skip them:
237
237
  - **Redoc/Swagger UI → PDF**: they lazy-render; printing truncates. Use `openapi-to-pdf.mjs`.
238
238
  - See [REFERENCE.md](../c9n-deliverables/REFERENCE.md) for the ISO/IEC 29110 layout and per-artifact recipes.
239
239
 
240
+
241
+
242
+ ---
243
+
244
+ ## Pipeline
245
+
246
+ Bootstraps the Code Passion (C9N) CI/CD pipeline into a target repo so a new project deploys the
247
+ same way as the reference implementation (Smiley): **one image per app, built once at the Alpha
248
+ tag, redeployed to Beta/Production by base tag**, env injected at runtime by Coolify. The heavy
249
+ lifting lives in the org reusable workflows `codepassion-team/cicd/.github/workflows/{build,deploy}.yml`
250
+ — the per-repo files this skill generates just call them. The skill's real value is **Phase 3**:
251
+ the interactive ops checklist that encodes every footgun the Smiley setup hit.
252
+
253
+ Bundled `templates/` are the source skeleton. **Copy them, then substitute tokens** — never edit
254
+ the templates in place for a specific project. The Next.js wiring is an **edit** of files the dev
255
+ already owns, not a file drop (see Phase 2).
256
+
257
+ ## What gets generated
258
+
259
+ | Path | Template | When |
260
+ |------|----------|------|
261
+ | `<app>/Dockerfile` | `Dockerfile.api` | app has `elysia` |
262
+ | `<app>/Dockerfile` | `Dockerfile.web` | app has `next` |
263
+ | `<app>/Dockerfile` | `Dockerfile.service` | any other deployable app (worker, plain Bun) |
264
+ | `.dockerignore` | `dockerignore` | always (repo root) |
265
+ | `.github/workflows/ci.yml` | `workflows/ci.yml` (+ `_migration-check.partial` if DB) | always |
266
+ | `.github/workflows/deployment.yml` | `workflows/deployment.yml` (+ `_build-job`/`_deploy-job` partials) | always |
267
+ | `<web>/next.config.js` | **edit, not copy** | per web app |
268
+ | `<web>/app/layout.tsx` | **edit, not copy** | per web app |
269
+
270
+ ## Workflow
271
+
272
+ Run in order; this is a table of contents — rules are below.
273
+
274
+ 1. **Detect (Phase 1).** First decide the repo shape:
275
+ - **Monorepo** — `apps/` and/or `turbo.json` exist. Scan `apps/*/package.json`, classify each:
276
+ `next`→web, `elysia`→api, else→service. `{{APP_DIR}}` = `apps/<name>`.
277
+ - **Single-app** — no `apps/`, no `turbo.json`; the deployable app is the repo root. Classify the
278
+ root `package.json` (`next`→web, etc.). `{{APP_DIR}}` = `.`, `{{PACKAGE_NAME}}` = the repo name
279
+ (no `-web` suffix needed), and the **single-app template variants** apply (see "Single-app repos"
280
+ below) — drop the workspace-manifest copies, `outputFileTracingRoot`, `transpilePackages`, and
281
+ the `apps/web/` path segments. The generated `ci.yml` also drops Turbo (see that template's note).
282
+
283
+ Then detect DB by **real artifacts** — a migration-generate script in some `package.json` AND a
284
+ migrations dir on disk — capture their actual names; never assume `drizzle:generate` /
285
+ `packages/supabase/migrations`. Read the app's `package.json` for the Bun version / lockfile name
286
+ (but **pin ≥ 1.3.10** — see `{{BUN_VERSION}}`). Ask which environments exist (Alpha / Beta /
287
+ Production). **Print the detected plan and confirm before writing anything.**
288
+ 2. **Generate (Phase 2).** Copy templates with tokens substituted. **Diff/confirm per file** — if a
289
+ target exists, show the diff and ask overwrite/skip/merge; never blind-clobber; leave unrelated
290
+ files (a hand-kept `staging.yaml`) alone. Then do the **web edits** (below). Run the web build
291
+ once to prove nothing is baked.
292
+ 3. **Checklist (Phase 3).** Walk the dev interactively through the out-of-repo ops (below). This is
293
+ the deliverable.
294
+ 4. **Verify (Phase 4).** After the dev confirms the ops are done, push a tag (or reuse one), watch
295
+ the Actions run, then `curl` each deployed URL per environment and report pass/fail per app.
296
+
297
+ ## Token substitution
298
+
299
+ | Token | Source |
300
+ |-------|--------|
301
+ | `{{PACKAGE_NAME}}` | `<project>-<appkey>`, e.g. `smiley-web`. Lowercase. This is the ghcr image name and the `package_name` workflow input. |
302
+ | `{{APP_DIR}}` | The app's path, e.g. `apps/web`. **Single-app repo: `.`** (the app is the repo root — this leaves valid `./.next/...` paths in the template). |
303
+ | `{{APP_KEY}}` | Short lowercase id unique per app: `api`, `web`, `worker`. Used in job names. |
304
+ | `{{APP_PORT}}` | `3001` api · `3000` web · service's listen port (drop `EXPOSE` for a portless worker). |
305
+ | `{{BUN_VERSION}}` | **Pin ≥ 1.3.10** (same across all Dockerfiles). Do **not** copy the host's `bun --version` — 1.2.x breaks `next build` on Linux with `TypeError: Expected CommonJS module to have a function wrapper` (edge/jest-worker; only shows on Linux, not macOS). |
306
+ | `{{WORKSPACE_MANIFEST_COPIES}}` | **web template only:** one `COPY <app>/package.json ./<app>/package.json` line **per app** — the `deps` stage runs `bun install` before `COPY . .`, so it needs every workspace manifest to resolve the graph. The api/service compile templates copy only the built app's manifest (matches Smiley's api Dockerfile; avoids pulling a sibling app's postinstall without its src). **Single-app repo: empty string** (root `package.json` + `bun.lock` are already copied; there is no `packages/` — also delete the `COPY packages/ ./packages/` line). |
307
+ | `{{MIGRATION_CHECK}}` | `_migration-check.partial.yml` with `{{MIGRATION_SCRIPT}}`/`{{MIGRATIONS_DIR}}` filled, **or empty string** if no DB. |
308
+ | `{{ORG_REF}}` | The org reusable-workflow ref — currently `turborepo`. One place; bump here if the org repo retags. |
309
+ | `{{BUILD_JOBS}}` / `{{DEPLOY_JOBS}}` | Stamp `_build-job.partial` once per app (Alpha only) and `_deploy-job.partial` once per app per environment, pruning unused envs. |
310
+ | `{{DEPLOY_NAME}}` | The org `deploy.yml` `name` input → uppercased to `<NAME>_UUID`. **Only `API` and `WEB` are wired** in the org deploy.yml (see Gotchas). |
311
+
312
+ ## Web edits (surgical — not a file drop)
313
+
314
+ For each **web** app, edit the files the dev owns; fail loudly if they can't be parsed rather than
315
+ overwrite:
316
+
317
+ - `next.config.js` — add `output: "standalone"`. **Monorepo only:** also add `outputFileTracingRoot`
318
+ (monorepo root) and `transpilePackages` for the workspace packages the app imports. **Single-app:
319
+ omit both** (root app traces natively; no workspace packages).
320
+ - `app/layout.tsx` — add `<head><PublicEnvScript /></head>` (`import { PublicEnvScript } from
321
+ "next-runtime-env"`).
322
+ - **Convert EVERY `process.env.NEXT_PUBLIC_*` read to `env("NEXT_PUBLIC_X")`** — not just client
323
+ components. Next **inlines every `process.env.NEXT_PUBLIC_*` literal at build time in SERVER code and
324
+ middleware too**, so any read left as `process.env` bakes the build value (or a build-time
325
+ placeholder) into the server/middleware bundle and runtime injection can't override it. This is the
326
+ #1 time-sink: a stray `process.env.NEXT_PUBLIC_SUPABASE_URL` in `middleware.ts` bakes the placeholder
327
+ → `supabase.auth.getUser()` fails → **auth redirect loop** that only shows in the container, not
328
+ locally. Convert: client components, **server components, route handlers (`app/api/**`), middleware,
329
+ and any t3-env `runtimeEnv` mapping**. Non-public server vars (`DATABASE_URL`, `*_SERVICE_KEY`) stay
330
+ on `process.env` — they aren't inlined and are present at runtime.
331
+ - **Phase-2 gate — must pass before you call the web edits done:**
332
+ `grep -rn 'process.env.NEXT_PUBLIC' {{APP_DIR}}` (the app dir — `.` for single-app, `apps/web` for a
333
+ monorepo app; not every repo has a `src/`) returns **nothing**. Any hit is a latent runtime bug.
334
+ - Flag `next-runtime-env` as a dependency to add — **do not run `bun add` yourself**; tell the dev.
335
+ - **Caveat:** runtime values only reach the browser on non-statically-prerendered pages. A page that
336
+ reads runtime public env must opt out of static caching (`export const dynamic = "force-dynamic"`)
337
+ or rely on `<PublicEnvScript>` keeping the document dynamic. Note this so future feature work
338
+ doesn't silently bake a static page.
339
+
340
+ ## Single-app repos (no `apps/`, no `turbo.json`)
341
+
342
+ The templates are monorepo-shaped; one set of token substitutions makes them serve a root-level single
343
+ app. Apply these deltas (verified on `ddg-jewelry-admin`):
344
+
345
+ - **Tokens:** `{{APP_DIR}}` = `.`, `{{PACKAGE_NAME}}` = repo name (no `-web` suffix),
346
+ `{{WORKSPACE_MANIFEST_COPIES}}` = empty.
347
+ - **`Dockerfile.web`:** delete the `COPY packages/ ./packages/` line (no `packages/`). The runner
348
+ `COPY` lines collapse to `.next/standalone ./`, `.next/static ./.next/static`, `public ./public`
349
+ (the `{{APP_DIR}}=.` substitution yields valid `./.next/...` — fine to simplify).
350
+ - **`next.config`:** `output: "standalone"` only — no `outputFileTracingRoot`, no `transpilePackages`.
351
+ - **`ci.yml`:** drop the Turbo `env:`, the `.turbo` cache step, and swap `bun run check-types` for
352
+ `bunx tsc --noEmit` if the repo has no `check-types` script (single apps usually don't).
353
+ - **Runner (optional but recommended):** a single Next app deploys cleanly on a Node runner
354
+ (`node:22-alpine` or `gcr.io/distroless/nodejs22-debian12`, `CMD ["server.js"]`). Distroless cuts the
355
+ image ~4× and drops the runner CVEs (seen on `ddg-jewelry-admin`).
356
+
357
+ ## Phase 3 — interactive ops checklist (the deliverable)
358
+
359
+ Derived from the actual `codepassion-team/cicd` reusable workflows. Walk these one at a time:
360
+
361
+ **Repo prerequisites**
362
+ - A **`develop` branch must exist** — `build.yml` commits the version bump and does
363
+ `git pull --rebase origin develop` / `push HEAD:develop`. No develop branch → build fails.
364
+ - Tags drive deploys; the exact tag→env mapping is **c9n-semver-deployment** (don't restate it).
365
+
366
+ **Per environment** — create a GitHub **Environment** named exactly `Alpha` / `Beta` / `Production`:
367
+ - Variables: `COOLIFY_RESOURCE_API_UUID`, `COOLIFY_RESOURCE_WEB_UUID` (the Coolify app UUIDs;
368
+ `deploy.yml` resolves `${NAME}_UUID`), and `TURBO_TEAM`.
369
+ - Secrets: `COOLIFY_TOKEN` (Coolify API bearer) and `TURBO_TOKEN`. `GITHUB_TOKEN` is automatic
370
+ (ghcr push uses it; the build job already declares `packages: write`).
371
+
372
+ **Coolify** (one resource per app per env): source = Docker image
373
+ `ghcr.io/codepassion-team/{{PACKAGE_NAME}}:<base_tag>`; copy each resource's UUID into the matching
374
+ env variable above. Caddy `reverse_proxy <app>:<port>` (api `:3001`, web `:3000`).
375
+
376
+ **ghcr** — packages are **private**: give Coolify a registry pull credential (or set package
377
+ visibility), or the image pull silently fails.
378
+
379
+ **Web runtime env in Coolify** (the scars):
380
+ - `PORT=3000` — Next standalone obeys `PORT`; Coolify defaults it to `80`, which 502s behind a
381
+ Caddy `:3000` proxy. This is the single most common web-deploy failure.
382
+ - **Coolify `Ports Exposes` (General page) is a SEPARATE setting from the `PORT` env** — it defaults
383
+ to `80` and must be set to the app port (`3000`). With `PORT=3000` but Ports Exposes still `80`, the
384
+ deploy log warns `PORT (3000) does not match configured ports_exposes: 80` and the proxy 502s. Fix
385
+ both.
386
+ - `HOSTNAME=0.0.0.0` — the Dockerfile already bakes this; only set it if something overrides it.
387
+ - The app's `NEXT_PUBLIC_*` values — injected here at runtime, not baked into the image.
388
+ - **Set ALL env values UNQUOTED** (public and server alike) — Coolify passes values literally, so a
389
+ quoted `DATABASE_URL="…"` keeps the quotes and breaks `new URL()` (`ERR_INVALID_URL`).
390
+
391
+ ## Phase 4 — verify
392
+
393
+ Push a `v*.*.*` tag (or reuse one), watch the run (`gh run watch`), then for each app per env
394
+ `curl -so /dev/null -w "%{http_code}"` the deployed URL (e.g. `https://api.<proj>.alpha.c9n.co`,
395
+ `https://<proj>.alpha.c9n.co`) and confirm `200`. Report a clean pass/fail table. Don't call it done
396
+ on a green Actions run alone — the 502 class only shows at the live URL.
397
+
398
+ ## Gotchas
399
+
400
+ - **Combined install mode does not carry `templates/`.** `install.sh combined` copies only the
401
+ generated `skills/c9n` SKILL.md body. Install via `install.sh claude` (or `project`/`codex`) so the
402
+ templates come along — same limitation as `c9n-spec`/`c9n-deliverables`.
403
+ - **Org deploy.yml wires only `API_UUID` + `WEB_UUID`.** A third app type (worker) needs a PR to
404
+ `codepassion-team/cicd` adding `COOLIFY_RESOURCE_<NAME>_UUID` to deploy.yml's `env:` block. Flag
405
+ this; don't generate a deploy job that silently can't resolve its UUID.
406
+ - **Build once, deploy by base tag.** build.yml strips `-alpha`/`-rc.*`; deploy.yml also strips
407
+ `-beta.*`. So `v1.2.3` (Alpha) and a later `v1.2.3-beta.1` release both resolve base tag `v1.2.3`
408
+ and redeploy the same image. Beta/Prod must have an Alpha build of that base version in ghcr first.
409
+ - **`.dockerignore` matters.** Without it, the host's `node_modules` (wrong platform) leaks into the
410
+ build context and clobbers the linux deps → "failed to load next.config". The template ships it.
411
+ - **Don't touch `.env*`.** Generate `.dockerignore` to exclude them; never read or write env files.
412
+
413
+
414
+
415
+ ---
416
+
417
+ ## Sentry
418
+
419
+ ## Sentry — Bun + Elysia + Next.js
420
+
421
+ ## Three landmines (check first)
422
+
423
+ 1. **`@sentry/node` under `bun --compile` fails silently** — valid DSN, zero events. Use `@sentry/elysia` (built on `@sentry/bun`). Never let `@sentry/node` load on the API.
424
+ 2. **Runtime public env** — if the project uses `next-runtime-env` (ADR 0005 pattern), the browser DSN MUST use `env("NEXT_PUBLIC_SENTRY_DSN")` not `process.env`. Server/edge/api use `process.env`. Wrong = undefined DSN in prod = dead Sentry.
425
+ 3. **PDPA / PII** — add `beforeSend` scrubber + `sendDefaultPii: false`. Strip request body/query/cookies, drop auth headers, recursively redact PII keys. See [REFERENCE.md](REFERENCE.md#pdpa-scrubber).
426
+
427
+ ## Quick start
428
+
429
+ ```
430
+ packages/sentry/ ← @repo/sentry (shared)
431
+ src/pdpa.ts ← beforeSend scrubber (shared by all inits)
432
+ src/bun.ts ← initBun() for apps/api
433
+ src/client.ts ← initClient() for browser (uses env() not process.env)
434
+ src/server.ts ← initServer() for Next.js server runtime
435
+ src/edge.ts ← initEdge() for Next.js edge runtime
436
+ package.json ← exports map: ./bun ./client ./server ./edge ./pdpa
437
+ apps/api/src/lib/sentry.ts ← import { initBun } from "@repo/sentry/bun"; initBun()
438
+ apps/api/src/index.ts ← first import: "./lib/sentry.js", then withElysia(new Elysia())
439
+ apps/web/instrumentation.ts ← register() → dynamic import server/edge config
440
+ apps/web/instrumentation-client.ts ← initClient()
441
+ apps/web/sentry.server.config.ts / sentry.edge.config.ts
442
+ apps/web/next.config.js ← withSentryConfig + transpilePackages: ["@repo/sentry"]
443
+ ```
444
+
445
+ ## Env vars
446
+
447
+ | Service | Key | Read by |
448
+ |---------|-----|---------|
449
+ | API (Coolify) | `SENTRY_DSN`, `ENVIRONMENT` | `process.env` |
450
+ | Web (Coolify) | `NEXT_PUBLIC_SENTRY_DSN`, `NEXT_PUBLIC_ENVIRONMENT` | `env()` from next-runtime-env (client) / `process.env` (server/edge) |
451
+
452
+ One Sentry project, same DSN in both vars. Add `initialScope: { tags: { app: "api" } }` to `initBun()` so api vs web events are distinguishable in the shared project.
453
+
454
+ ## Verification checklist
455
+
456
+ - [ ] `bun install --frozen-lockfile` clean
457
+ - [ ] `bun run check-types` (api + web) clean
458
+ - [ ] `bun run build --compile` on API — confirms `@sentry/node` never loaded (the landmine test)
459
+ - [ ] Web Sentry bundle builds green
460
+ - [ ] After deploy: `window.__ENV.NEXT_PUBLIC_SENTRY_DSN` populated in devtools (runtime env proof)
461
+ - [ ] Trigger a test error → event appears in Sentry tagged correct environment
462
+ - [ ] Open event payload — no phone/otp/lat/lng/member_id (PDPA proof)
463
+
464
+ ## Reference
465
+
466
+ See [REFERENCE.md](REFERENCE.md) for full file contents, PDPA scrubber code, and `turbo.json` config.
467
+
468
+
469
+
470
+ ---
471
+
472
+ ## Spec
473
+
474
+ Bootstraps the Code Passion (C9N) spec-driven development doc system into a target repo. Drops
475
+ a self-contained, agent-readable documentation skeleton so a fresh project starts with the same
476
+ approval gates, traceability, and glossary discipline as the reference implementation
477
+ (ddg-jewelry). Fully generic — adapt the placeholders to whatever stack the project uses.
478
+
479
+ Bundled `templates/` are the source skeleton. **Copy them, then substitute tokens** — never edit
480
+ the templates in place for a specific project.
481
+
482
+ ## What gets scaffolded
483
+
484
+ | File | Role |
485
+ |------|------|
486
+ | `AGENTS.md` | **Canonical** agent context: the full ISO/IEC 29110 spec-driven protocol (inlined, so the repo is self-contained) + C9N stack + module patterns. References the sibling `c9n-*` convention skills rather than copying them. |
487
+ | `CLAUDE.md` | Bridge that `@AGENTS.md`-imports the canonical file (so Claude Code's memory auto-loads the protocol; other tools read AGENTS.md directly). |
488
+ | `CONTEXT.md` | Ubiquitous-language glossary skeleton (no implementation detail). |
489
+ | `DESIGN.md` | Visual design-system rules — **only if the repo has a frontend.** |
490
+ | `docs/SRS.md` | Requirements master registry (feature short-codes + system NFRs). |
491
+ | `docs/architecture.md` | Software-design as-is map. |
492
+ | `docs/traceability.md` | Requirement → design → test master index. |
493
+ | `docs/test-plan.md` | Master test strategy. |
494
+ | `docs/adr/0001-spec-driven-iso29110-docs.md` | Seed ADR: why this doc system exists. |
495
+ | `docs/adr/_TEMPLATE.md` | ADR format for future decisions. |
496
+ | `docs/features/_TEMPLATE.md` | Per-feature deep-doc stub (Requirements · Design · Test Plan · Test Cases · Traceability). |
497
+ | `docs/agents/{issue-tracker,triage-labels,domain}.md` | Agent-process docs (GitHub `gh`, triage vocab, domain-doc consumption). |
498
+ | `.github/pull_request_template.md` | The Definition-of-Done merge gate. |
499
+
500
+ ## Workflow
501
+
502
+ Run each step in order; this is a table of contents — the rules are below.
503
+
504
+ 1. **Detect repo shape.** Read the target's root `package.json`/workspaces. Decide:
505
+ monorepo vs single app; **frontend present?** (`apps/web`, or `next`/`astro`/`vite`/`react`
506
+ in any `package.json`) → include `DESIGN.md`. **Backend/API present?** → keep the API
507
+ module-patterns section in AGENTS.md; otherwise tell the user it's deletable.
508
+ 2. **No-clobber.** For every target path, **skip if it already exists** — never overwrite. Collect
509
+ skipped paths for the final report. (Re-running the skill must be safe and idempotent.)
510
+ 3. **Copy templates** into the target repo, substituting tokens (below). Skip `DESIGN.md` if no
511
+ frontend. Skip the API section guidance if no backend.
512
+ 4. **Optional scan — OFF by default.** Only if the user **explicitly** asks to seed from code
513
+ ("scan the codebase", "seed the glossary", "reverse-document"): read the codebase and fill
514
+ - `CONTEXT.md` — first real glossary terms (actors, core entities) from the domain;
515
+ - `docs/architecture.md` — the actual module map + data-model relationships;
516
+ - `docs/features/` — one as-built stub per shipped module (low-confidence, no test cases),
517
+ and register each in `docs/SRS.md` with a fresh short-code.
518
+ Without this request, leave the placeholders untouched.
519
+ 5. **Report.** Print created vs skipped files, then next steps: pick the first feature, write its
520
+ SRS section (Gate 1), get sign-off, then Design + Test Plan (Gate 2) — no production code
521
+ before Gate 2.
522
+
523
+ ## Token substitution
524
+
525
+ When copying, replace these placeholders:
526
+
527
+ | Token | Source |
528
+ |-------|--------|
529
+ | `{{PROJECT_NAME}}` | Target root dir name, or `package.json` `name` (humanized). |
530
+ | `{{YEAR}}` / `{{DATE}}` | Current year / ISO date (for the seed ADR). |
531
+ | `{{DECIDER}}` | Ask, or default to the repo owner. |
532
+ | Stack rows in AGENTS.md | Keep C9N defaults; trim rows the detected stack doesn't use. |
533
+
534
+ ## Conventions (referenced, not copied)
535
+
536
+ The scaffolded `AGENTS.md` points at the sibling C9N skills for engineering conventions — it does
537
+ **not** inline them, so they have a single source of truth:
538
+
539
+ - Commits → `c9n-commit-messages` · Branching → `c9n-git-branching` · PRs → `c9n-pull-requests`
540
+ - Clean code → `c9n-clean-code-typescript` · Lint/types → `c9n-typescript-eslint`
541
+ - Formatting → `c9n-code-formatting` · Review → `c9n-code-review`
542
+
543
+ For C9N-internal repos every dev has these installed, so the references always resolve. If you
544
+ scaffold into a repo where they may not be installed, tell the user to
545
+ `npx @codepassion/skills install claude`.
546
+
547
+ ## Gotchas
548
+
549
+ - **Combined install mode does not carry `templates/`.** `install.sh combined` copies only the
550
+ generated `skills/c9n` SKILL.md body; the bundled templates are absent. Install `c9n-spec` via
551
+ `install.sh claude` (or `project`/`codex`) so the templates come along. Same limitation as
552
+ `c9n-deliverables`' scripts.
553
+ - **AGENTS.md vs CLAUDE.md.** AGENTS.md is the real file. CLAUDE.md exists only so Claude Code's
554
+ memory auto-load reaches the protocol — it does so via the **`@AGENTS.md` import** (Claude Code's
555
+ memory-import syntax), not a markdown link (a link is inert and pulls nothing into context). Don't
556
+ duplicate content into CLAUDE.md. If a target tool doesn't support `@`-imports, it reads AGENTS.md
557
+ directly anyway, so nothing is lost.
558
+ - **Don't grade the stubs.** If you run the optional scan, as-built stubs describe *current*
559
+ behavior at low confidence with **no** test cases — deepen on touch, per the protocol. Don't
560
+ invent acceptance criteria for code you only read.
561
+
@@ -0,0 +1,171 @@
1
+ ---
2
+ name: c9n-pipeline
3
+ description: Bootstrap the C9N CI/CD pipeline INTO a Turborepo/Bun monorepo — generates the per-repo GitHub Actions (CI + tag-driven Deployment that call the org codepassion-team/cicd reusable workflows), per-app Dockerfiles (Elysia→bun-compile, Next.js standalone+runtime-env, generic service), and .dockerignore; then walks the developer through the out-of-repo ops (GitHub Environments, Coolify resources, ghcr pull, the PORT=3000 web footgun) and verifies the live deploy. Use when setting up CI/CD on a new C9N project, "add github actions", "set up deployment", "wire up Coolify", or porting the Smiley pipeline to another repo. Complements c9n-semver-deployment (owns the tag->env mapping) and c9n-spec (scaffolds docs, not pipelines).
4
+ ---
5
+
6
+ Bootstraps the Code Passion (C9N) CI/CD pipeline into a target repo so a new project deploys the
7
+ same way as the reference implementation (Smiley): **one image per app, built once at the Alpha
8
+ tag, redeployed to Beta/Production by base tag**, env injected at runtime by Coolify. The heavy
9
+ lifting lives in the org reusable workflows `codepassion-team/cicd/.github/workflows/{build,deploy}.yml`
10
+ — the per-repo files this skill generates just call them. The skill's real value is **Phase 3**:
11
+ the interactive ops checklist that encodes every footgun the Smiley setup hit.
12
+
13
+ Bundled `templates/` are the source skeleton. **Copy them, then substitute tokens** — never edit
14
+ the templates in place for a specific project. The Next.js wiring is an **edit** of files the dev
15
+ already owns, not a file drop (see Phase 2).
16
+
17
+ ## What gets generated
18
+
19
+ | Path | Template | When |
20
+ |------|----------|------|
21
+ | `<app>/Dockerfile` | `Dockerfile.api` | app has `elysia` |
22
+ | `<app>/Dockerfile` | `Dockerfile.web` | app has `next` |
23
+ | `<app>/Dockerfile` | `Dockerfile.service` | any other deployable app (worker, plain Bun) |
24
+ | `.dockerignore` | `dockerignore` | always (repo root) |
25
+ | `.github/workflows/ci.yml` | `workflows/ci.yml` (+ `_migration-check.partial` if DB) | always |
26
+ | `.github/workflows/deployment.yml` | `workflows/deployment.yml` (+ `_build-job`/`_deploy-job` partials) | always |
27
+ | `<web>/next.config.js` | **edit, not copy** | per web app |
28
+ | `<web>/app/layout.tsx` | **edit, not copy** | per web app |
29
+
30
+ ## Workflow
31
+
32
+ Run in order; this is a table of contents — rules are below.
33
+
34
+ 1. **Detect (Phase 1).** First decide the repo shape:
35
+ - **Monorepo** — `apps/` and/or `turbo.json` exist. Scan `apps/*/package.json`, classify each:
36
+ `next`→web, `elysia`→api, else→service. `{{APP_DIR}}` = `apps/<name>`.
37
+ - **Single-app** — no `apps/`, no `turbo.json`; the deployable app is the repo root. Classify the
38
+ root `package.json` (`next`→web, etc.). `{{APP_DIR}}` = `.`, `{{PACKAGE_NAME}}` = the repo name
39
+ (no `-web` suffix needed), and the **single-app template variants** apply (see "Single-app repos"
40
+ below) — drop the workspace-manifest copies, `outputFileTracingRoot`, `transpilePackages`, and
41
+ the `apps/web/` path segments. The generated `ci.yml` also drops Turbo (see that template's note).
42
+
43
+ Then detect DB by **real artifacts** — a migration-generate script in some `package.json` AND a
44
+ migrations dir on disk — capture their actual names; never assume `drizzle:generate` /
45
+ `packages/supabase/migrations`. Read the app's `package.json` for the Bun version / lockfile name
46
+ (but **pin ≥ 1.3.10** — see `{{BUN_VERSION}}`). Ask which environments exist (Alpha / Beta /
47
+ Production). **Print the detected plan and confirm before writing anything.**
48
+ 2. **Generate (Phase 2).** Copy templates with tokens substituted. **Diff/confirm per file** — if a
49
+ target exists, show the diff and ask overwrite/skip/merge; never blind-clobber; leave unrelated
50
+ files (a hand-kept `staging.yaml`) alone. Then do the **web edits** (below). Run the web build
51
+ once to prove nothing is baked.
52
+ 3. **Checklist (Phase 3).** Walk the dev interactively through the out-of-repo ops (below). This is
53
+ the deliverable.
54
+ 4. **Verify (Phase 4).** After the dev confirms the ops are done, push a tag (or reuse one), watch
55
+ the Actions run, then `curl` each deployed URL per environment and report pass/fail per app.
56
+
57
+ ## Token substitution
58
+
59
+ | Token | Source |
60
+ |-------|--------|
61
+ | `{{PACKAGE_NAME}}` | `<project>-<appkey>`, e.g. `smiley-web`. Lowercase. This is the ghcr image name and the `package_name` workflow input. |
62
+ | `{{APP_DIR}}` | The app's path, e.g. `apps/web`. **Single-app repo: `.`** (the app is the repo root — this leaves valid `./.next/...` paths in the template). |
63
+ | `{{APP_KEY}}` | Short lowercase id unique per app: `api`, `web`, `worker`. Used in job names. |
64
+ | `{{APP_PORT}}` | `3001` api · `3000` web · service's listen port (drop `EXPOSE` for a portless worker). |
65
+ | `{{BUN_VERSION}}` | **Pin ≥ 1.3.10** (same across all Dockerfiles). Do **not** copy the host's `bun --version` — 1.2.x breaks `next build` on Linux with `TypeError: Expected CommonJS module to have a function wrapper` (edge/jest-worker; only shows on Linux, not macOS). |
66
+ | `{{WORKSPACE_MANIFEST_COPIES}}` | **web template only:** one `COPY <app>/package.json ./<app>/package.json` line **per app** — the `deps` stage runs `bun install` before `COPY . .`, so it needs every workspace manifest to resolve the graph. The api/service compile templates copy only the built app's manifest (matches Smiley's api Dockerfile; avoids pulling a sibling app's postinstall without its src). **Single-app repo: empty string** (root `package.json` + `bun.lock` are already copied; there is no `packages/` — also delete the `COPY packages/ ./packages/` line). |
67
+ | `{{MIGRATION_CHECK}}` | `_migration-check.partial.yml` with `{{MIGRATION_SCRIPT}}`/`{{MIGRATIONS_DIR}}` filled, **or empty string** if no DB. |
68
+ | `{{ORG_REF}}` | The org reusable-workflow ref — currently `turborepo`. One place; bump here if the org repo retags. |
69
+ | `{{BUILD_JOBS}}` / `{{DEPLOY_JOBS}}` | Stamp `_build-job.partial` once per app (Alpha only) and `_deploy-job.partial` once per app per environment, pruning unused envs. |
70
+ | `{{DEPLOY_NAME}}` | The org `deploy.yml` `name` input → uppercased to `<NAME>_UUID`. **Only `API` and `WEB` are wired** in the org deploy.yml (see Gotchas). |
71
+
72
+ ## Web edits (surgical — not a file drop)
73
+
74
+ For each **web** app, edit the files the dev owns; fail loudly if they can't be parsed rather than
75
+ overwrite:
76
+
77
+ - `next.config.js` — add `output: "standalone"`. **Monorepo only:** also add `outputFileTracingRoot`
78
+ (monorepo root) and `transpilePackages` for the workspace packages the app imports. **Single-app:
79
+ omit both** (root app traces natively; no workspace packages).
80
+ - `app/layout.tsx` — add `<head><PublicEnvScript /></head>` (`import { PublicEnvScript } from
81
+ "next-runtime-env"`).
82
+ - **Convert EVERY `process.env.NEXT_PUBLIC_*` read to `env("NEXT_PUBLIC_X")`** — not just client
83
+ components. Next **inlines every `process.env.NEXT_PUBLIC_*` literal at build time in SERVER code and
84
+ middleware too**, so any read left as `process.env` bakes the build value (or a build-time
85
+ placeholder) into the server/middleware bundle and runtime injection can't override it. This is the
86
+ #1 time-sink: a stray `process.env.NEXT_PUBLIC_SUPABASE_URL` in `middleware.ts` bakes the placeholder
87
+ → `supabase.auth.getUser()` fails → **auth redirect loop** that only shows in the container, not
88
+ locally. Convert: client components, **server components, route handlers (`app/api/**`), middleware,
89
+ and any t3-env `runtimeEnv` mapping**. Non-public server vars (`DATABASE_URL`, `*_SERVICE_KEY`) stay
90
+ on `process.env` — they aren't inlined and are present at runtime.
91
+ - **Phase-2 gate — must pass before you call the web edits done:**
92
+ `grep -rn 'process.env.NEXT_PUBLIC' {{APP_DIR}}` (the app dir — `.` for single-app, `apps/web` for a
93
+ monorepo app; not every repo has a `src/`) returns **nothing**. Any hit is a latent runtime bug.
94
+ - Flag `next-runtime-env` as a dependency to add — **do not run `bun add` yourself**; tell the dev.
95
+ - **Caveat:** runtime values only reach the browser on non-statically-prerendered pages. A page that
96
+ reads runtime public env must opt out of static caching (`export const dynamic = "force-dynamic"`)
97
+ or rely on `<PublicEnvScript>` keeping the document dynamic. Note this so future feature work
98
+ doesn't silently bake a static page.
99
+
100
+ ## Single-app repos (no `apps/`, no `turbo.json`)
101
+
102
+ The templates are monorepo-shaped; one set of token substitutions makes them serve a root-level single
103
+ app. Apply these deltas (verified on `ddg-jewelry-admin`):
104
+
105
+ - **Tokens:** `{{APP_DIR}}` = `.`, `{{PACKAGE_NAME}}` = repo name (no `-web` suffix),
106
+ `{{WORKSPACE_MANIFEST_COPIES}}` = empty.
107
+ - **`Dockerfile.web`:** delete the `COPY packages/ ./packages/` line (no `packages/`). The runner
108
+ `COPY` lines collapse to `.next/standalone ./`, `.next/static ./.next/static`, `public ./public`
109
+ (the `{{APP_DIR}}=.` substitution yields valid `./.next/...` — fine to simplify).
110
+ - **`next.config`:** `output: "standalone"` only — no `outputFileTracingRoot`, no `transpilePackages`.
111
+ - **`ci.yml`:** drop the Turbo `env:`, the `.turbo` cache step, and swap `bun run check-types` for
112
+ `bunx tsc --noEmit` if the repo has no `check-types` script (single apps usually don't).
113
+ - **Runner (optional but recommended):** a single Next app deploys cleanly on a Node runner
114
+ (`node:22-alpine` or `gcr.io/distroless/nodejs22-debian12`, `CMD ["server.js"]`). Distroless cuts the
115
+ image ~4× and drops the runner CVEs (seen on `ddg-jewelry-admin`).
116
+
117
+ ## Phase 3 — interactive ops checklist (the deliverable)
118
+
119
+ Derived from the actual `codepassion-team/cicd` reusable workflows. Walk these one at a time:
120
+
121
+ **Repo prerequisites**
122
+ - A **`develop` branch must exist** — `build.yml` commits the version bump and does
123
+ `git pull --rebase origin develop` / `push HEAD:develop`. No develop branch → build fails.
124
+ - Tags drive deploys; the exact tag→env mapping is **c9n-semver-deployment** (don't restate it).
125
+
126
+ **Per environment** — create a GitHub **Environment** named exactly `Alpha` / `Beta` / `Production`:
127
+ - Variables: `COOLIFY_RESOURCE_API_UUID`, `COOLIFY_RESOURCE_WEB_UUID` (the Coolify app UUIDs;
128
+ `deploy.yml` resolves `${NAME}_UUID`), and `TURBO_TEAM`.
129
+ - Secrets: `COOLIFY_TOKEN` (Coolify API bearer) and `TURBO_TOKEN`. `GITHUB_TOKEN` is automatic
130
+ (ghcr push uses it; the build job already declares `packages: write`).
131
+
132
+ **Coolify** (one resource per app per env): source = Docker image
133
+ `ghcr.io/codepassion-team/{{PACKAGE_NAME}}:<base_tag>`; copy each resource's UUID into the matching
134
+ env variable above. Caddy `reverse_proxy <app>:<port>` (api `:3001`, web `:3000`).
135
+
136
+ **ghcr** — packages are **private**: give Coolify a registry pull credential (or set package
137
+ visibility), or the image pull silently fails.
138
+
139
+ **Web runtime env in Coolify** (the scars):
140
+ - `PORT=3000` — Next standalone obeys `PORT`; Coolify defaults it to `80`, which 502s behind a
141
+ Caddy `:3000` proxy. This is the single most common web-deploy failure.
142
+ - **Coolify `Ports Exposes` (General page) is a SEPARATE setting from the `PORT` env** — it defaults
143
+ to `80` and must be set to the app port (`3000`). With `PORT=3000` but Ports Exposes still `80`, the
144
+ deploy log warns `PORT (3000) does not match configured ports_exposes: 80` and the proxy 502s. Fix
145
+ both.
146
+ - `HOSTNAME=0.0.0.0` — the Dockerfile already bakes this; only set it if something overrides it.
147
+ - The app's `NEXT_PUBLIC_*` values — injected here at runtime, not baked into the image.
148
+ - **Set ALL env values UNQUOTED** (public and server alike) — Coolify passes values literally, so a
149
+ quoted `DATABASE_URL="…"` keeps the quotes and breaks `new URL()` (`ERR_INVALID_URL`).
150
+
151
+ ## Phase 4 — verify
152
+
153
+ Push a `v*.*.*` tag (or reuse one), watch the run (`gh run watch`), then for each app per env
154
+ `curl -so /dev/null -w "%{http_code}"` the deployed URL (e.g. `https://api.<proj>.alpha.c9n.co`,
155
+ `https://<proj>.alpha.c9n.co`) and confirm `200`. Report a clean pass/fail table. Don't call it done
156
+ on a green Actions run alone — the 502 class only shows at the live URL.
157
+
158
+ ## Gotchas
159
+
160
+ - **Combined install mode does not carry `templates/`.** `install.sh combined` copies only the
161
+ generated `skills/c9n` SKILL.md body. Install via `install.sh claude` (or `project`/`codex`) so the
162
+ templates come along — same limitation as `c9n-spec`/`c9n-deliverables`.
163
+ - **Org deploy.yml wires only `API_UUID` + `WEB_UUID`.** A third app type (worker) needs a PR to
164
+ `codepassion-team/cicd` adding `COOLIFY_RESOURCE_<NAME>_UUID` to deploy.yml's `env:` block. Flag
165
+ this; don't generate a deploy job that silently can't resolve its UUID.
166
+ - **Build once, deploy by base tag.** build.yml strips `-alpha`/`-rc.*`; deploy.yml also strips
167
+ `-beta.*`. So `v1.2.3` (Alpha) and a later `v1.2.3-beta.1` release both resolve base tag `v1.2.3`
168
+ and redeploy the same image. Beta/Prod must have an Alpha build of that base version in ghcr first.
169
+ - **`.dockerignore` matters.** Without it, the host's `node_modules` (wrong platform) leaks into the
170
+ build context and clobbers the linux deps → "failed to load next.config". The template ships it.
171
+ - **Don't touch `.env*`.** Generate `.dockerignore` to exclude them; never read or write env files.