@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.
- package/README.md +6 -1
- package/SKILLS.md +6 -1
- package/bin/c9n-skills.js +28 -3
- package/install.sh +25 -2
- package/package.json +1 -1
- package/skills/c9n/SKILL.md +322 -0
- package/skills/c9n-pipeline/SKILL.md +171 -0
- package/skills/c9n-pipeline/templates/Dockerfile.api +49 -0
- package/skills/c9n-pipeline/templates/Dockerfile.service +45 -0
- package/skills/c9n-pipeline/templates/Dockerfile.web +55 -0
- package/skills/c9n-pipeline/templates/dockerignore +18 -0
- package/skills/c9n-pipeline/templates/workflows/_build-job.partial.yml +13 -0
- package/skills/c9n-pipeline/templates/workflows/_deploy-job.partial.yml +17 -0
- package/skills/c9n-pipeline/templates/workflows/_migration-check.partial.yml +16 -0
- package/skills/c9n-pipeline/templates/workflows/ci.yml +50 -0
- package/skills/c9n-pipeline/templates/workflows/deployment.yml +21 -0
- package/skills/c9n-sentry/REFERENCE.md +172 -0
- package/skills/c9n-sentry/SKILL.md +53 -0
- package/skills/c9n-spec/SKILL.md +92 -0
- package/skills/c9n-spec/templates/.github/pull_request_template.md +29 -0
- package/skills/c9n-spec/templates/AGENTS.md +179 -0
- package/skills/c9n-spec/templates/CLAUDE.md +6 -0
- package/skills/c9n-spec/templates/CONTEXT.md +53 -0
- package/skills/c9n-spec/templates/DESIGN.md +73 -0
- package/skills/c9n-spec/templates/docs/SRS.md +37 -0
- package/skills/c9n-spec/templates/docs/adr/0001-spec-driven-iso29110-docs.md +44 -0
- package/skills/c9n-spec/templates/docs/adr/_TEMPLATE.md +26 -0
- package/skills/c9n-spec/templates/docs/agents/domain.md +43 -0
- package/skills/c9n-spec/templates/docs/agents/issue-tracker.md +22 -0
- package/skills/c9n-spec/templates/docs/agents/triage-labels.md +27 -0
- package/skills/c9n-spec/templates/docs/architecture.md +50 -0
- package/skills/c9n-spec/templates/docs/features/_TEMPLATE.md +34 -0
- package/skills/c9n-spec/templates/docs/test-plan.md +30 -0
- 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')
|
|
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
|
|
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
|
-
|
|
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
package/skills/c9n/SKILL.md
CHANGED
|
@@ -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.
|