@codepassion/skills 1.1.1 → 1.4.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 (41) 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 +344 -0
  7. package/skills/c9n-deliverables/REFERENCE.md +97 -0
  8. package/skills/c9n-deliverables/SKILL.md +63 -0
  9. package/skills/c9n-deliverables/scripts/data-dictionary.cjs +49 -0
  10. package/skills/c9n-deliverables/scripts/export-openapi.cjs +35 -0
  11. package/skills/c9n-deliverables/scripts/md-to-pdf.mjs +73 -0
  12. package/skills/c9n-deliverables/scripts/openapi-to-pdf.mjs +78 -0
  13. package/skills/c9n-deliverables/scripts/snapshot-source.sh +23 -0
  14. package/skills/c9n-pipeline/SKILL.md +128 -0
  15. package/skills/c9n-pipeline/templates/Dockerfile.api +49 -0
  16. package/skills/c9n-pipeline/templates/Dockerfile.service +45 -0
  17. package/skills/c9n-pipeline/templates/Dockerfile.web +50 -0
  18. package/skills/c9n-pipeline/templates/dockerignore +18 -0
  19. package/skills/c9n-pipeline/templates/workflows/_build-job.partial.yml +13 -0
  20. package/skills/c9n-pipeline/templates/workflows/_deploy-job.partial.yml +17 -0
  21. package/skills/c9n-pipeline/templates/workflows/_migration-check.partial.yml +16 -0
  22. package/skills/c9n-pipeline/templates/workflows/ci.yml +38 -0
  23. package/skills/c9n-pipeline/templates/workflows/deployment.yml +21 -0
  24. package/skills/c9n-sentry/REFERENCE.md +172 -0
  25. package/skills/c9n-sentry/SKILL.md +53 -0
  26. package/skills/c9n-spec/SKILL.md +92 -0
  27. package/skills/c9n-spec/templates/.github/pull_request_template.md +29 -0
  28. package/skills/c9n-spec/templates/AGENTS.md +179 -0
  29. package/skills/c9n-spec/templates/CLAUDE.md +6 -0
  30. package/skills/c9n-spec/templates/CONTEXT.md +53 -0
  31. package/skills/c9n-spec/templates/DESIGN.md +73 -0
  32. package/skills/c9n-spec/templates/docs/SRS.md +37 -0
  33. package/skills/c9n-spec/templates/docs/adr/0001-spec-driven-iso29110-docs.md +44 -0
  34. package/skills/c9n-spec/templates/docs/adr/_TEMPLATE.md +26 -0
  35. package/skills/c9n-spec/templates/docs/agents/domain.md +43 -0
  36. package/skills/c9n-spec/templates/docs/agents/issue-tracker.md +22 -0
  37. package/skills/c9n-spec/templates/docs/agents/triage-labels.md +27 -0
  38. package/skills/c9n-spec/templates/docs/architecture.md +50 -0
  39. package/skills/c9n-spec/templates/docs/features/_TEMPLATE.md +34 -0
  40. package/skills/c9n-spec/templates/docs/test-plan.md +30 -0
  41. package/skills/c9n-spec/templates/docs/traceability.md +27 -0
@@ -0,0 +1,49 @@
1
+ // Generate a data dictionary (Markdown) from a live MySQL/MariaDB INFORMATION_SCHEMA.
2
+ // Schema only — no row data. Requires `mysql2` resolvable (run from a project that has it).
3
+ //
4
+ // Usage: node data-dictionary.cjs <output.md>
5
+ // Env (or a .env loaded via dotenv): SQL_HOST SQL_PORT SQL_USERNAME SQL_PASSWORD SQL_DATABASE_1
6
+ // Adjust the env var names below to match the target project's config.
7
+ const mysql = require('mysql2/promise')
8
+ const fs = require('fs')
9
+
10
+ const OUT = process.argv[2]
11
+ if (!OUT) { console.error('usage: node data-dictionary.cjs <output.md>'); process.exit(2) }
12
+ try { require('dotenv').config({ path: process.env.ENV_FILE || '.env' }) } catch {}
13
+
14
+ const cfg = {
15
+ host: process.env.SQL_HOST || process.env.DB_HOST,
16
+ port: +(process.env.SQL_PORT || process.env.DB_PORT || 3306),
17
+ user: process.env.SQL_USERNAME || process.env.DB_USER,
18
+ password: process.env.SQL_PASSWORD || process.env.DB_PASSWORD,
19
+ database: process.env.SQL_DATABASE_1 || process.env.DB_NAME,
20
+ }
21
+ const esc = (s) => String(s ?? '').replace(/\|/g, '\\|').replace(/\r?\n/g, ' ')
22
+
23
+ ;(async () => {
24
+ const c = await mysql.createConnection({ ...cfg, connectTimeout: 10000 })
25
+ const [tables] = await c.query(
26
+ `SELECT TABLE_NAME, TABLE_COMMENT,
27
+ (SELECT COUNT(*) FROM information_schema.columns col WHERE col.table_schema=t.table_schema AND col.table_name=t.table_name) COLS
28
+ FROM information_schema.tables t WHERE t.table_schema=? AND t.table_type='BASE TABLE' ORDER BY t.table_name`, [cfg.database])
29
+ const [cols] = await c.query(
30
+ `SELECT TABLE_NAME, COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, COLUMN_KEY, COLUMN_DEFAULT, EXTRA, COLUMN_COMMENT
31
+ FROM information_schema.columns WHERE table_schema=? ORDER BY TABLE_NAME, ORDINAL_POSITION`, [cfg.database])
32
+ const byTable = {}
33
+ for (const col of cols) (byTable[col.TABLE_NAME] ||= []).push(col)
34
+
35
+ let md = `# Data Dictionary\n\nGenerated from the live database schema (\`INFORMATION_SCHEMA\`). **${tables.length} tables.**\n\n## Tables\n\n`
36
+ for (const t of tables) md += `- [${t.TABLE_NAME}](#${t.TABLE_NAME.toLowerCase()}) (${t.COLS} columns)\n`
37
+ md += `\n---\n\n`
38
+ for (const t of tables) {
39
+ md += `### ${t.TABLE_NAME}\n\n`
40
+ if (t.TABLE_COMMENT) md += `${esc(t.TABLE_COMMENT)}\n\n`
41
+ md += `| Column | Type | Null | Key | Default | Extra | Comment |\n|--------|------|------|-----|---------|-------|--------|\n`
42
+ for (const col of byTable[t.TABLE_NAME] || [])
43
+ md += `| ${esc(col.COLUMN_NAME)} | \`${esc(col.COLUMN_TYPE)}\` | ${col.IS_NULLABLE} | ${esc(col.COLUMN_KEY)} | ${col.COLUMN_DEFAULT === null ? '' : esc(col.COLUMN_DEFAULT)} | ${esc(col.EXTRA)} | ${esc(col.COLUMN_COMMENT)} |\n`
44
+ md += `\n`
45
+ }
46
+ fs.writeFileSync(OUT, md)
47
+ console.log(`wrote ${OUT} — ${tables.length} tables, ${cols.length} columns`)
48
+ await c.end()
49
+ })().catch((e) => { console.error('FAILED:', e.code || e.message); process.exit(1) })
@@ -0,0 +1,35 @@
1
+ // Export the OpenAPI/Swagger spec from a NestJS app WITHOUT starting the HTTP server.
2
+ // This is a TEMPLATE — adapt the DocumentBuilder block to match the project's main.ts.
3
+ //
4
+ // Usage: node export-openapi.cjs <output.json>
5
+ //
6
+ // IMPORTANT notes learned the hard way:
7
+ // - Run against the COMPILED build (`nest build` -> dist), not ts-node. ts-node
8
+ // transpile-only mis-emits decorator metadata and TypeORM entity validation fails.
9
+ // - App bootstrap may require real resources (DB reachable, credential files present).
10
+ // Booting connects to the DB; use a dev DB, never production.
11
+ // - Match enableVersioning() and the DocumentBuilder config to the project's main.ts.
12
+ const { NestFactory } = require('@nestjs/core')
13
+ const { SwaggerModule, DocumentBuilder } = require('@nestjs/swagger')
14
+ const { VersioningType } = require('@nestjs/common')
15
+ const fs = require('fs')
16
+ const path = require('path')
17
+ require('reflect-metadata')
18
+ try { require('dotenv').config({ path: process.env.ENV_FILE || '.env' }) } catch {}
19
+
20
+ const OUT = process.argv[2] || 'openapi.json'
21
+ // ADAPT: path to the compiled AppModule
22
+ const { AppModule } = require(path.resolve(process.cwd(), 'dist/src/app.module'))
23
+
24
+ ;(async () => {
25
+ const app = await NestFactory.create(AppModule, { logger: ['error', 'warn'] })
26
+ // ADAPT: copy enableVersioning() from the project's main.ts if it uses versioning
27
+ app.enableVersioning({ type: VersioningType.URI, defaultVersion: '1', prefix: 'api/v' })
28
+ // ADAPT: mirror the project's DocumentBuilder (title, version, auth schemes)
29
+ const config = new DocumentBuilder().setTitle('API').setVersion('1.0').build()
30
+ const document = SwaggerModule.createDocument(app, config)
31
+ fs.writeFileSync(path.resolve(process.cwd(), OUT), JSON.stringify(document, null, 2))
32
+ console.log(`wrote ${OUT} — ${Object.keys(document.paths || {}).length} paths`)
33
+ await app.close()
34
+ process.exit(0)
35
+ })().catch((e) => { console.error('FAILED:', e?.message || e); process.exit(1) })
@@ -0,0 +1,73 @@
1
+ // Render a Markdown file to PDF via a headless Chromium browser.
2
+ // Handles Mermaid code fences (```mermaid) by rendering them to SVG client-side.
3
+ //
4
+ // Usage: node md-to-pdf.mjs <input.md> <output.pdf>
5
+ //
6
+ // No install required if a Chromium-family browser is present (Brave/Chrome/Edge/Chromium).
7
+ // Mermaid + marked load from CDN, so the machine needs internet for diagram rendering.
8
+ //
9
+ // SECURITY: input is expected to be FIRST-PARTY documentation you generated for the
10
+ // delivery (the project's own docs), not untrusted third-party Markdown. Mermaid runs with
11
+ // securityLevel:'strict' (no click handlers / inline HTML in diagrams). Markdown is still
12
+ // rendered with raw HTML allowed — do not feed this untrusted Markdown.
13
+ //
14
+ // NOTE: For very WIDE diagrams (many nodes in one row), prefer `flowchart TB` / vertical
15
+ // ER layouts in the source — wide diagrams scale down to an illegible strip at page width.
16
+ import { readFileSync, writeFileSync, mkdtempSync, existsSync } from 'node:fs'
17
+ import { tmpdir } from 'node:os'
18
+ import { join, basename } from 'node:path'
19
+ import { execFileSync } from 'node:child_process'
20
+
21
+ const [src, out] = process.argv.slice(2)
22
+ if (!src || !out) { console.error('usage: node md-to-pdf.mjs <input.md> <output.pdf>'); process.exit(2) }
23
+
24
+ const BROWSERS = [
25
+ '/Applications/Brave Browser.app/Contents/MacOS/Brave Browser',
26
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
27
+ '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
28
+ '/Applications/Chromium.app/Contents/MacOS/Chromium',
29
+ '/usr/bin/google-chrome', '/usr/bin/chromium', '/usr/bin/chromium-browser',
30
+ ]
31
+ const browser = BROWSERS.find(existsSync)
32
+ if (!browser) { console.error('No Chromium-family browser found. Install Chrome/Brave/Chromium.'); process.exit(1) }
33
+
34
+ const md = readFileSync(src, 'utf8')
35
+ const html = `<!doctype html><html><head><meta charset="utf-8">
36
+ <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
37
+ <script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
38
+ <style>
39
+ @page { margin: 18mm 16mm; }
40
+ body { font: 14px/1.6 -apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif; color:#1a1a1a; max-width:820px; margin:0 auto; }
41
+ h1 { font-size:26px; border-bottom:2px solid #222; padding-bottom:6px; }
42
+ h2 { font-size:20px; margin-top:28px; border-bottom:1px solid #ddd; padding-bottom:4px; }
43
+ h3 { font-size:16px; margin-top:20px; }
44
+ table { border-collapse:collapse; width:100%; margin:12px 0; font-size:13px; }
45
+ th,td { border:1px solid #ccc; padding:6px 10px; text-align:left; vertical-align:top; }
46
+ th { background:#f4f4f4; }
47
+ code { background:#f0f0f0; padding:1px 5px; border-radius:3px; font-size:12px; }
48
+ pre { background:#f6f8fa; padding:12px; border-radius:6px; overflow:auto; }
49
+ pre code { background:none; padding:0; }
50
+ .mermaid { text-align:center; margin:16px 0; }
51
+ blockquote { border-left:4px solid #ccc; margin:12px 0; padding:4px 14px; color:#555; }
52
+ </style></head><body>
53
+ <div id="out"></div>
54
+ <script type="text/markdown" id="src">${md.replace(/<\/script>/g, '<\\/script>')}</script>
55
+ <script>
56
+ document.getElementById('out').innerHTML = marked.parse(document.getElementById('src').textContent);
57
+ document.querySelectorAll('code.language-mermaid').forEach(c => {
58
+ const d = document.createElement('div'); d.className='mermaid'; d.textContent=c.textContent;
59
+ c.closest('pre').replaceWith(d);
60
+ });
61
+ mermaid.initialize({ startOnLoad:false, securityLevel:'strict' });
62
+ mermaid.run();
63
+ </script></body></html>`
64
+
65
+ const tmp = mkdtempSync(join(tmpdir(), 'md2pdf-'))
66
+ const htmlPath = join(tmp, basename(src) + '.html')
67
+ writeFileSync(htmlPath, html)
68
+ execFileSync(browser, [
69
+ '--headless', '--disable-gpu', '--no-pdf-header-footer',
70
+ '--run-all-compositor-stages-before-draw', '--virtual-time-budget=20000',
71
+ `--print-to-pdf=${out}`, `file://${htmlPath}`,
72
+ ], { stdio: 'inherit' })
73
+ console.log('wrote', out)
@@ -0,0 +1,78 @@
1
+ // Render an OpenAPI 3 spec to a print-friendly PDF (no lazy loading — full content,
2
+ // unlike Redoc/Swagger UI which lazy-render and truncate when printed).
3
+ //
4
+ // Usage: node openapi-to-pdf.mjs <openapi.json> <output.pdf> [guide.md]
5
+ // guide.md (optional) is prepended as an intro section before the endpoint reference.
6
+ //
7
+ // SECURITY: the spec and guide.md are expected to be FIRST-PARTY artifacts you generated
8
+ // for the delivery, not untrusted input. The guide Markdown is rendered with raw HTML
9
+ // allowed — do not pass an untrusted guide file.
10
+ import { readFileSync, writeFileSync, mkdtempSync, existsSync } from 'node:fs'
11
+ import { tmpdir } from 'node:os'
12
+ import { join } from 'node:path'
13
+ import { execFileSync } from 'node:child_process'
14
+
15
+ const [specPath, out, guidePath] = process.argv.slice(2)
16
+ if (!specPath || !out) { console.error('usage: node openapi-to-pdf.mjs <openapi.json> <output.pdf> [guide.md]'); process.exit(2) }
17
+
18
+ const BROWSERS = [
19
+ '/Applications/Brave Browser.app/Contents/MacOS/Brave Browser',
20
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
21
+ '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
22
+ '/usr/bin/google-chrome', '/usr/bin/chromium', '/usr/bin/chromium-browser',
23
+ ]
24
+ const browser = BROWSERS.find(existsSync)
25
+ if (!browser) { console.error('No Chromium-family browser found.'); process.exit(1) }
26
+
27
+ const spec = JSON.parse(readFileSync(specPath, 'utf8'))
28
+ const guideMd = guidePath && existsSync(guidePath) ? readFileSync(guidePath, 'utf8').replace(/^# .*\r?\n/, '') : ''
29
+ const esc = (s) => String(s ?? '').replace(/[&<>]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;' }[c]))
30
+ const refName = (r) => (r?.$ref ? r.$ref.split('/').pop() : null)
31
+ const schemaType = (s) => !s ? '' : s.$ref ? refName(s) : s.type === 'array' ? `${schemaType(s.items)}[]` : s.enum ? `enum(${s.enum.map(esc).join(', ')})` : s.format ? `${s.type}<${s.format}>` : (s.type || 'object')
32
+
33
+ const methods = ['get', 'post', 'put', 'patch', 'delete']
34
+ const groups = {}
35
+ for (const [p, item] of Object.entries(spec.paths || {}))
36
+ for (const m of methods) { const op = item[m]; if (!op) continue; const tag = (op.tags && op.tags[0]) || 'default'; (groups[tag] ||= []).push({ m, p, op }) }
37
+
38
+ const pTable = (ps = []) => !ps.length ? '' : `<table class="p"><thead><tr><th>Name</th><th>In</th><th>Req</th><th>Type</th></tr></thead><tbody>${ps.map((pa) => `<tr><td><code>${esc(pa.name)}</code></td><td>${esc(pa.in)}</td><td>${pa.required ? 'yes' : 'no'}</td><td>${esc(schemaType(pa.schema))}</td></tr>`).join('')}</tbody></table>`
39
+ const bBlock = (rb) => { if (!rb) return ''; const j = rb.content && (rb.content['application/json'] || Object.values(rb.content)[0]); return `<div class="body"><span class="lbl">Request body${rb.required ? ' (required)' : ''}:</span> <code>${esc(j ? schemaType(j.schema) : 'object')}</code></div>` }
40
+ const rBlock = (r = {}) => { const rows = Object.entries(r).map(([c, v]) => `<tr><td><code>${esc(c)}</code></td><td>${esc(v.description || '')}</td></tr>`).join(''); return rows ? `<table class="p"><thead><tr><th>Status</th><th>Description</th></tr></thead><tbody>${rows}</tbody></table>` : '' }
41
+
42
+ let body = ''
43
+ for (const tag of Object.keys(groups).sort()) {
44
+ body += `<h2>${esc(tag)}</h2>`
45
+ for (const { m, p, op } of groups[tag])
46
+ body += `<div class="op"><div class="line"><span class="m ${m}">${m.toUpperCase()}</span><code class="path">${esc(p)}</code></div>${op.summary ? `<div class="sum">${esc(op.summary)}</div>` : ''}${op.description ? `<div class="desc">${esc(op.description)}</div>` : ''}${pTable(op.parameters)}${bBlock(op.requestBody)}${rBlock(op.responses)}</div>`
47
+ }
48
+ const opCount = Object.values(spec.paths || {}).reduce((n, it) => n + methods.filter((m) => it[m]).length, 0)
49
+
50
+ const html = `<!doctype html><html><head><meta charset="utf-8"><style>
51
+ @page { margin: 16mm 14mm; }
52
+ body { font: 12px/1.5 -apple-system,Segoe UI,Roboto,Arial,sans-serif; color:#1a1a1a; max-width:860px; margin:0 auto; }
53
+ h1 { font-size:24px; border-bottom:2px solid #222; padding-bottom:6px; }
54
+ h2 { font-size:18px; margin:26px 0 10px; border-bottom:1px solid #ddd; padding-bottom:4px; break-after:avoid; }
55
+ .op { border:1px solid #e3e3e3; border-radius:6px; padding:10px 12px; margin:10px 0; break-inside:avoid; }
56
+ .line { display:flex; align-items:center; gap:8px; }
57
+ .m { color:#fff; font-weight:700; font-size:11px; padding:2px 8px; border-radius:4px; }
58
+ .m.get{background:#2e8b57}.m.post{background:#1f6feb}.m.put{background:#b8860b}.m.patch{background:#8250df}.m.delete{background:#cf222e}
59
+ .sum { font-weight:600; margin:6px 0 2px; } .desc { color:#555; margin:2px 0; }
60
+ table.p { border-collapse:collapse; width:100%; margin:8px 0; font-size:11px; }
61
+ table.p th, table.p td { border:1px solid #ddd; padding:4px 8px; text-align:left; } table.p th { background:#f5f5f5; }
62
+ .body { margin:6px 0; } .lbl { color:#555; } code { background:#f0f0f0; padding:1px 4px; border-radius:3px; }
63
+ .meta { color:#555; margin-bottom:18px; } .section-break { break-before:page; }
64
+ .guide table { border-collapse:collapse; width:100%; margin:8px 0; font-size:12px; }
65
+ .guide th, .guide td { border:1px solid #ccc; padding:5px 9px; text-align:left; } .guide th { background:#f4f4f4; }
66
+ .guide blockquote { border-left:4px solid #ccc; margin:10px 0; padding:4px 14px; color:#555; }
67
+ </style>${guideMd ? '<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>' : ''}</head><body>
68
+ <h1>${esc(spec.info?.title || 'API')} — API Documentation</h1>
69
+ <div class="meta">Version ${esc(spec.info?.version || '')} · OpenAPI ${esc(spec.openapi || '')} · ${Object.keys(spec.paths || {}).length} paths · ${opCount} operations</div>
70
+ ${guideMd ? `<div class="guide" id="guide"></div><script type="text/markdown" id="g">${guideMd.replace(/<\/script>/g, '<\\/script>')}</script><script>document.getElementById('guide').innerHTML=marked.parse(document.getElementById('g').textContent)</script><h1 class="section-break">Endpoint Reference</h1>` : ''}
71
+ ${body}
72
+ </body></html>`
73
+
74
+ const tmp = mkdtempSync(join(tmpdir(), 'oa2pdf-'))
75
+ const htmlPath = join(tmp, 'api.html')
76
+ writeFileSync(htmlPath, html)
77
+ execFileSync(browser, ['--headless', '--disable-gpu', '--no-pdf-header-footer', '--run-all-compositor-stages-before-draw', '--virtual-time-budget=15000', `--print-to-pdf=${out}`, `file://${htmlPath}`], { stdio: 'inherit' })
78
+ console.log('wrote', out)
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env bash
2
+ # Produce a clean source-code snapshot using git archive.
3
+ # git archive only includes tracked files — node_modules, .env, dist, secrets are
4
+ # excluded automatically (assuming they are gitignored / untracked).
5
+ #
6
+ # Usage: snapshot-source.sh <repo-root> <output.zip>
7
+ set -euo pipefail
8
+
9
+ REPO_ROOT="${1:-$PWD}"
10
+ OUT="${2:?usage: snapshot-source.sh <repo-root> <output.zip>}"
11
+
12
+ # Resolve a relative <output.zip> against the caller's cwd, then use `git -C` so we
13
+ # never change the caller's working directory. (A bare relative --output under
14
+ # `git -C`/`cd` would surprisingly land inside the repo root.)
15
+ case "$OUT" in /*) ;; *) OUT="$PWD/$OUT" ;; esac
16
+
17
+ if [ ! -d "$REPO_ROOT/.git" ]; then
18
+ echo "Error: $REPO_ROOT is not a git repository." >&2
19
+ exit 1
20
+ fi
21
+
22
+ git -C "$REPO_ROOT" archive --format=zip --output "$OUT" HEAD
23
+ echo "Source snapshot written to: $OUT"
@@ -0,0 +1,128 @@
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).** Scan `apps/*/package.json`. Classify each: `next`→web, `elysia`→api, else
35
+ →service. Detect DB by **real artifacts** — a migration-generate script in some `package.json`
36
+ AND a migrations dir on disk — capture their actual names; never assume `drizzle:generate` /
37
+ `packages/supabase/migrations`. Read root `package.json` for the Bun version / lockfile name.
38
+ Ask which environments exist (Alpha / Beta / Production). **Print the detected plan and confirm
39
+ before writing anything.**
40
+ 2. **Generate (Phase 2).** Copy templates with tokens substituted. **Diff/confirm per file** — if a
41
+ target exists, show the diff and ask overwrite/skip/merge; never blind-clobber; leave unrelated
42
+ files (a hand-kept `staging.yaml`) alone. Then do the **web edits** (below). Run the web build
43
+ once to prove nothing is baked.
44
+ 3. **Checklist (Phase 3).** Walk the dev interactively through the out-of-repo ops (below). This is
45
+ the deliverable.
46
+ 4. **Verify (Phase 4).** After the dev confirms the ops are done, push a tag (or reuse one), watch
47
+ the Actions run, then `curl` each deployed URL per environment and report pass/fail per app.
48
+
49
+ ## Token substitution
50
+
51
+ | Token | Source |
52
+ |-------|--------|
53
+ | `{{PACKAGE_NAME}}` | `<project>-<appkey>`, e.g. `smiley-web`. Lowercase. This is the ghcr image name and the `package_name` workflow input. |
54
+ | `{{APP_DIR}}` | The app's path, e.g. `apps/web`. |
55
+ | `{{APP_KEY}}` | Short lowercase id unique per app: `api`, `web`, `worker`. Used in job names. |
56
+ | `{{APP_PORT}}` | `3001` api · `3000` web · service's listen port (drop `EXPOSE` for a portless worker). |
57
+ | `{{BUN_VERSION}}` | Pin from root toolchain, e.g. `1.3.10`. Same across all Dockerfiles. |
58
+ | `{{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). |
59
+ | `{{MIGRATION_CHECK}}` | `_migration-check.partial.yml` with `{{MIGRATION_SCRIPT}}`/`{{MIGRATIONS_DIR}}` filled, **or empty string** if no DB. |
60
+ | `{{ORG_REF}}` | The org reusable-workflow ref — currently `turborepo`. One place; bump here if the org repo retags. |
61
+ | `{{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. |
62
+ | `{{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). |
63
+
64
+ ## Web edits (surgical — not a file drop)
65
+
66
+ For each **web** app, edit the files the dev owns; fail loudly if they can't be parsed rather than
67
+ overwrite:
68
+
69
+ - `next.config.js` — add `output: "standalone"`, `outputFileTracingRoot` (monorepo root), and
70
+ `transpilePackages` for the workspace packages the app imports.
71
+ - `app/layout.tsx` — add `<head><PublicEnvScript /></head>` (`import { PublicEnvScript } from
72
+ "next-runtime-env"`). Client code reads public config with `env("NEXT_PUBLIC_X")`, **not**
73
+ `process.env` (which bakes at build → undefined at runtime).
74
+ - Flag `next-runtime-env` as a dependency to add — **do not run `bun add` yourself**; tell the dev.
75
+ - **Caveat:** runtime values only reach the browser on non-statically-prerendered pages. A page that
76
+ reads runtime public env must opt out of static caching (`export const dynamic = "force-dynamic"`)
77
+ or rely on `<PublicEnvScript>` keeping the document dynamic. Note this so future feature work
78
+ doesn't silently bake a static page.
79
+
80
+ ## Phase 3 — interactive ops checklist (the deliverable)
81
+
82
+ Derived from the actual `codepassion-team/cicd` reusable workflows. Walk these one at a time:
83
+
84
+ **Repo prerequisites**
85
+ - A **`develop` branch must exist** — `build.yml` commits the version bump and does
86
+ `git pull --rebase origin develop` / `push HEAD:develop`. No develop branch → build fails.
87
+ - Tags drive deploys; the exact tag→env mapping is **c9n-semver-deployment** (don't restate it).
88
+
89
+ **Per environment** — create a GitHub **Environment** named exactly `Alpha` / `Beta` / `Production`:
90
+ - Variables: `COOLIFY_RESOURCE_API_UUID`, `COOLIFY_RESOURCE_WEB_UUID` (the Coolify app UUIDs;
91
+ `deploy.yml` resolves `${NAME}_UUID`), and `TURBO_TEAM`.
92
+ - Secrets: `COOLIFY_TOKEN` (Coolify API bearer) and `TURBO_TOKEN`. `GITHUB_TOKEN` is automatic
93
+ (ghcr push uses it; the build job already declares `packages: write`).
94
+
95
+ **Coolify** (one resource per app per env): source = Docker image
96
+ `ghcr.io/codepassion-team/{{PACKAGE_NAME}}:<base_tag>`; copy each resource's UUID into the matching
97
+ env variable above. Caddy `reverse_proxy <app>:<port>` (api `:3001`, web `:3000`).
98
+
99
+ **ghcr** — packages are **private**: give Coolify a registry pull credential (or set package
100
+ visibility), or the image pull silently fails.
101
+
102
+ **Web runtime env in Coolify** (the scars):
103
+ - `PORT=3000` — Next standalone obeys `PORT`; Coolify defaults it to `80`, which 502s behind a
104
+ Caddy `:3000` proxy. This is the single most common web-deploy failure.
105
+ - `HOSTNAME=0.0.0.0`.
106
+ - The app's `NEXT_PUBLIC_*` values — injected here at runtime, not baked into the image.
107
+
108
+ ## Phase 4 — verify
109
+
110
+ Push a `v*.*.*` tag (or reuse one), watch the run (`gh run watch`), then for each app per env
111
+ `curl -so /dev/null -w "%{http_code}"` the deployed URL (e.g. `https://api.<proj>.alpha.c9n.co`,
112
+ `https://<proj>.alpha.c9n.co`) and confirm `200`. Report a clean pass/fail table. Don't call it done
113
+ on a green Actions run alone — the 502 class only shows at the live URL.
114
+
115
+ ## Gotchas
116
+
117
+ - **Combined install mode does not carry `templates/`.** `install.sh combined` copies only the
118
+ generated `skills/c9n` SKILL.md body. Install via `install.sh claude` (or `project`/`codex`) so the
119
+ templates come along — same limitation as `c9n-spec`/`c9n-deliverables`.
120
+ - **Org deploy.yml wires only `API_UUID` + `WEB_UUID`.** A third app type (worker) needs a PR to
121
+ `codepassion-team/cicd` adding `COOLIFY_RESOURCE_<NAME>_UUID` to deploy.yml's `env:` block. Flag
122
+ this; don't generate a deploy job that silently can't resolve its UUID.
123
+ - **Build once, deploy by base tag.** build.yml strips `-alpha`/`-rc.*`; deploy.yml also strips
124
+ `-beta.*`. So `v1.2.3` (Alpha) and a later `v1.2.3-beta.1` release both resolve base tag `v1.2.3`
125
+ and redeploy the same image. Beta/Prod must have an Alpha build of that base version in ghcr first.
126
+ - **`.dockerignore` matters.** Without it, the host's `node_modules` (wrong platform) leaks into the
127
+ build context and clobbers the linux deps → "failed to load next.config". The template ships it.
128
+ - **Don't touch `.env*`.** Generate `.dockerignore` to exclude them; never read or write env files.
@@ -0,0 +1,49 @@
1
+ # -----------------------------------------------------------------------------
2
+ # {{PACKAGE_NAME}} (Elysia BFF) — compiled to a single Bun binary, run on
3
+ # debian slim. Generated by c9n-pipeline; tune freely after.
4
+ # -----------------------------------------------------------------------------
5
+
6
+ FROM oven/bun:{{BUN_VERSION}} AS build
7
+
8
+ WORKDIR /app
9
+
10
+ # Root manifests + lockfile
11
+ COPY package.json bun.lock ./
12
+
13
+ # This app's manifest + shared packages (compile needs only this member's graph)
14
+ COPY {{APP_DIR}}/package.json ./{{APP_DIR}}/package.json
15
+ COPY packages/ ./packages/
16
+
17
+ # Source needed to compile (copied before install so workspace links resolve)
18
+ COPY {{APP_DIR}}/src ./{{APP_DIR}}/src
19
+
20
+ RUN bun install
21
+
22
+ ENV NODE_ENV=production
23
+
24
+ # Compile the API to a standalone binary from the repo root
25
+ RUN bun build \
26
+ --compile \
27
+ --minify-whitespace \
28
+ --minify-syntax \
29
+ --target=bun \
30
+ --outfile /app/server \
31
+ ./{{APP_DIR}}/src/index.ts
32
+
33
+ # Runtime image — debian slim for compiled-Bun-binary compatibility
34
+ FROM debian:bookworm-slim
35
+
36
+ WORKDIR /app
37
+
38
+ RUN apt-get update && \
39
+ apt-get install -y --no-install-recommends ca-certificates && \
40
+ rm -rf /var/lib/apt/lists/*
41
+
42
+ COPY --from=build /app/server ./server
43
+ RUN chmod +x ./server
44
+
45
+ ENV NODE_ENV=production
46
+
47
+ EXPOSE {{APP_PORT}}
48
+
49
+ CMD ["./server"]
@@ -0,0 +1,45 @@
1
+ # -----------------------------------------------------------------------------
2
+ # {{PACKAGE_NAME}} (generic Bun service / worker) — compiled to a single Bun
3
+ # binary, run on debian slim. Generated by c9n-pipeline.
4
+ # A worker with no HTTP port: drop the EXPOSE line and skip its Coolify domain.
5
+ # NOTE: the org deploy.yml only wires API_UUID + WEB_UUID — a third app type
6
+ # needs a PR to codepassion-team/cicd to add its COOLIFY_RESOURCE_<NAME>_UUID.
7
+ # -----------------------------------------------------------------------------
8
+
9
+ FROM oven/bun:{{BUN_VERSION}} AS build
10
+
11
+ WORKDIR /app
12
+
13
+ COPY package.json bun.lock ./
14
+ COPY {{APP_DIR}}/package.json ./{{APP_DIR}}/package.json
15
+ COPY packages/ ./packages/
16
+ COPY {{APP_DIR}}/src ./{{APP_DIR}}/src
17
+
18
+ RUN bun install
19
+
20
+ ENV NODE_ENV=production
21
+
22
+ RUN bun build \
23
+ --compile \
24
+ --minify-whitespace \
25
+ --minify-syntax \
26
+ --target=bun \
27
+ --outfile /app/server \
28
+ ./{{APP_DIR}}/src/index.ts
29
+
30
+ FROM debian:bookworm-slim
31
+
32
+ WORKDIR /app
33
+
34
+ RUN apt-get update && \
35
+ apt-get install -y --no-install-recommends ca-certificates && \
36
+ rm -rf /var/lib/apt/lists/*
37
+
38
+ COPY --from=build /app/server ./server
39
+ RUN chmod +x ./server
40
+
41
+ ENV NODE_ENV=production
42
+
43
+ EXPOSE {{APP_PORT}}
44
+
45
+ CMD ["./server"]
@@ -0,0 +1,50 @@
1
+ # -----------------------------------------------------------------------------
2
+ # {{PACKAGE_NAME}} (Next.js app) — Bun multi-stage build.
3
+ # Public env is read at RUNTIME via next-runtime-env (<PublicEnvScript>), so this
4
+ # image bakes no NEXT_PUBLIC_* — Coolify injects them at run, like the API image.
5
+ # Generated by c9n-pipeline; see the ADR this skill points you to.
6
+ # -----------------------------------------------------------------------------
7
+
8
+ FROM oven/bun:{{BUN_VERSION}} AS base
9
+ WORKDIR /app
10
+
11
+ # Install dependencies with bun
12
+ FROM base AS deps
13
+ COPY package.json bun.lock ./
14
+ {{WORKSPACE_MANIFEST_COPIES}}
15
+ COPY packages/ ./packages/
16
+ RUN bun install
17
+
18
+ # Rebuild the source code only when needed
19
+ FROM base AS builder
20
+ WORKDIR /app
21
+ COPY --from=deps /app/node_modules ./node_modules
22
+ COPY . .
23
+
24
+ # Next.js expects {{APP_DIR}}/.env to exist; intentionally empty — no public env
25
+ # is baked at build (it is injected at runtime via next-runtime-env).
26
+ RUN touch {{APP_DIR}}/.env
27
+
28
+ RUN cd {{APP_DIR}} && bun run build
29
+
30
+ # Production image — serve the Next.js standalone output
31
+ FROM base AS runner
32
+ WORKDIR /app
33
+
34
+ ENV NODE_ENV=production \
35
+ PORT={{APP_PORT}} \
36
+ HOSTNAME="0.0.0.0"
37
+
38
+ RUN groupadd --system --gid 1001 nodejs && \
39
+ useradd --system --uid 1001 nextjs
40
+
41
+ # Leverage Next.js output file tracing to keep the image small
42
+ COPY --from=builder --chown=nextjs:nodejs /app/{{APP_DIR}}/.next/standalone ./
43
+ COPY --from=builder --chown=nextjs:nodejs /app/{{APP_DIR}}/.next/static ./{{APP_DIR}}/.next/static
44
+ COPY --from=builder --chown=nextjs:nodejs /app/{{APP_DIR}}/public ./{{APP_DIR}}/public
45
+
46
+ USER nextjs
47
+
48
+ EXPOSE {{APP_PORT}}
49
+
50
+ CMD ["bun", "{{APP_DIR}}/server.js"]
@@ -0,0 +1,18 @@
1
+ .next
2
+ node_modules
3
+ **/node_modules
4
+ **/.next
5
+ **/dist
6
+ .git
7
+ .turbo
8
+ **/.turbo
9
+ npm-debug.log
10
+ yarn-error.log
11
+ bun-debug.log
12
+ .env
13
+ **/.env
14
+ .env.*
15
+ **/.env.*
16
+
17
+ # Next.js telemetry
18
+ .next/telemetry
@@ -0,0 +1,13 @@
1
+ # One BUILD job per app. Alpha only — the image is built once at the tag push
2
+ # and redeployed to Beta/Production by base tag (no rebuild). {{APP_KEY}} is a
3
+ # lowercase id unique per app (e.g. api, web). {{ORG_REF}} is the org workflow
4
+ # ref (e.g. turborepo) — keep it identical across every job in this file.
5
+ build-{{APP_KEY}}-alpha:
6
+ name: Build
7
+ if: github.event_name != 'release' && startsWith(github.ref, 'refs/tags/v')
8
+ uses: codepassion-team/cicd/.github/workflows/build.yml@{{ORG_REF}}
9
+ with:
10
+ environment: Alpha
11
+ build_path: ./{{APP_DIR}}
12
+ package_name: {{PACKAGE_NAME}}
13
+ secrets: inherit
@@ -0,0 +1,17 @@
1
+ # One DEPLOY job per app per environment. {{DEPLOY_NAME}} is the org deploy.yml
2
+ # name input that resolves the Coolify UUID var: it is uppercased to
3
+ # ${{DEPLOY_NAME_UPPER}}_UUID, so it MUST be a key the org deploy.yml wires
4
+ # (currently only API and WEB). Stamp the right {{IF_CONDITION}} per environment
5
+ # from c9n-semver-deployment:
6
+ # Alpha -> needs: build-{{APP_KEY}}-alpha ; if: github.event_name != 'release' && startsWith(github.ref, 'refs/tags/v')
7
+ # Beta -> (no needs) ; if: github.event_name == 'release' && github.event.release.prerelease == true && startsWith(github.event.release.tag_name, 'v')
8
+ # Production -> (no needs) ; if: github.event_name == 'release' && github.event.release.prerelease == false && startsWith(github.event.release.tag_name, 'v')
9
+ deploy-{{APP_KEY}}-{{ENV_KEY}}:
10
+ name: Deploy
11
+ if: {{IF_CONDITION}}
12
+ {{NEEDS_LINE}}
13
+ uses: codepassion-team/cicd/.github/workflows/deploy.yml@{{ORG_REF}}
14
+ with:
15
+ name: {{DEPLOY_NAME}}
16
+ environment: {{ENVIRONMENT}}
17
+ secrets: inherit
@@ -0,0 +1,16 @@
1
+ # Inject in place of {{MIGRATION_CHECK}} in ci.yml ONLY when a DB is detected
2
+ # (a migration-generate script exists in some package.json AND a migrations dir
3
+ # exists). Substitute {{MIGRATION_SCRIPT}} and {{MIGRATIONS_DIR}} with the real
4
+ # detected names — do not assume drizzle:generate / packages/supabase/migrations.
5
+ # If no DB, {{MIGRATION_CHECK}} is the empty string and ci.yml ends at Build.
6
+ - name: Verify migrations are up to date
7
+ run: bun run {{MIGRATION_SCRIPT}} -- --name check
8
+ - name: Check for uncommitted migration changes
9
+ run: |
10
+ if git diff --name-only --exit-code {{MIGRATIONS_DIR}}; then
11
+ echo "Migrations are up to date"
12
+ else
13
+ echo "Detected uncommitted migration changes. Run 'bun run {{MIGRATION_SCRIPT}}' locally and commit the result."
14
+ git diff {{MIGRATIONS_DIR}}
15
+ exit 1
16
+ fi
@@ -0,0 +1,38 @@
1
+ name: CI
2
+ on:
3
+ push:
4
+ branches: ['main', 'develop']
5
+ pull_request:
6
+ types: [opened, synchronize]
7
+ jobs:
8
+ build:
9
+ name: Check
10
+ timeout-minutes: 15
11
+ runs-on: ubuntu-latest
12
+ env:
13
+ TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
14
+ TURBO_TEAM: ${{ vars.TURBO_TEAM }}
15
+ steps:
16
+ - name: Check out code
17
+ uses: actions/checkout@v4
18
+ with:
19
+ fetch-depth: 2
20
+ - name: Cache turbo build setup
21
+ uses: actions/cache@v4
22
+ with:
23
+ path: .turbo
24
+ key: ${{ runner.os }}-turbo-${{ github.sha }}
25
+ restore-keys: |
26
+ ${{ runner.os }}-turbo-
27
+ - uses: oven-sh/setup-bun@v2
28
+ - name: Setup Node.js environment
29
+ uses: actions/setup-node@v4
30
+ with:
31
+ node-version: 24
32
+ - name: Install dependencies
33
+ run: bun install
34
+ - name: Check types
35
+ run: bun run check-types
36
+ - name: Build
37
+ run: bun run build
38
+ {{MIGRATION_CHECK}}