@lifeaitools/rdc-skills 0.24.37 → 0.24.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.json +15 -15
- package/.claude-plugin/marketplace.json +21 -21
- package/.claude-plugin/plugin.json +1371 -1371
- package/.github/workflows/publish.yml +34 -34
- package/.github/workflows/self-test.yml +58 -58
- package/CHANGELOG.md +310 -310
- package/LICENSE +21 -21
- package/MANIFEST.md +221 -221
- package/README.md +377 -377
- package/README.sandbox.md +3 -3
- package/assets/watcher/viewer.html +164 -164
- package/bin/rdc-skills-mcp.mjs +316 -316
- package/commands/build.md +183 -183
- package/commands/collab.md +180 -180
- package/commands/deploy.md +152 -152
- package/commands/design.md +31 -31
- package/commands/edit.md +28 -28
- package/commands/fixit.md +124 -124
- package/commands/handoff.md +173 -173
- package/commands/help.md +95 -95
- package/commands/overnight.md +220 -220
- package/commands/plan.md +158 -158
- package/commands/preplan.md +131 -131
- package/commands/prototype.md +145 -145
- package/commands/release.md +49 -49
- package/commands/report.md +99 -99
- package/commands/review.md +120 -120
- package/commands/self-test.md +113 -113
- package/commands/status.md +86 -86
- package/commands/watch.md +98 -98
- package/commands/workitems.md +137 -137
- package/git-sha.json +1 -1
- package/guides/agent-bootstrap.md +295 -295
- package/guides/agents/backend.md +104 -104
- package/guides/agents/content.md +94 -94
- package/guides/agents/cs2.md +56 -56
- package/guides/agents/data.md +87 -87
- package/guides/agents/design.md +77 -77
- package/guides/agents/frontend.md +92 -92
- package/guides/agents/infrastructure.md +81 -81
- package/guides/agents/setup.md +281 -281
- package/guides/agents/verify.md +151 -151
- package/guides/agents/viz.md +106 -106
- package/guides/backend.md +146 -146
- package/guides/content.md +147 -147
- package/guides/cs2.md +190 -190
- package/guides/data.md +123 -123
- package/guides/design.md +116 -116
- package/guides/engineering-behavior.md +43 -43
- package/guides/escalation-protocol.md +125 -125
- package/guides/frontend.md +151 -151
- package/guides/history-md-spec.md +297 -297
- package/guides/infrastructure.md +179 -179
- package/guides/lessons-learned-spec.md +153 -153
- package/guides/output-contract.md +108 -108
- package/guides/publish-md-spec.md +289 -289
- package/guides/rdc-skills-startup.md +30 -30
- package/guides/verify.md +11 -11
- package/hooks/check-cwd.js +31 -31
- package/hooks/check-rdc-environment.js +164 -164
- package/hooks/check-services.js +6 -6
- package/hooks/check-stale-work-items.js +19 -19
- package/hooks/foreground-process-gate.js +128 -128
- package/hooks/gate-watchdog-selfcheck.js +257 -257
- package/hooks/hook-logger.js +25 -25
- package/hooks/lib/run-evidence-gate.mjs +241 -241
- package/hooks/no-stop-open-epics.js +127 -127
- package/hooks/post-tool-batch-gate.js +203 -203
- package/hooks/post-work-check.js +21 -21
- package/hooks/postcompact-log.js +13 -13
- package/hooks/precompact-log.js +13 -13
- package/hooks/rate-limit-retry.js +46 -46
- package/hooks/rdc-invocation-marker.js +157 -157
- package/hooks/rdc-output-contract-gate.js +94 -94
- package/hooks/require-work-item-on-commit.js +294 -294
- package/hooks/restart-brief.js +19 -19
- package/hooks/run-hidden-hook.ps1 +47 -47
- package/hooks/task-completed-gate.js +274 -274
- package/hooks/work-item-exit-gate.js +944 -944
- package/lib/catalog.mjs +236 -236
- package/lib/cloud-rewrite.mjs +155 -155
- package/package.json +56 -56
- package/rules/work-items-rpc.md +520 -520
- package/scaffold/templates/HISTORY.md.template +39 -39
- package/scaffold/templates/PUBLISH.md.template +21 -21
- package/scaffold/templates/brochure-studio-default.html +70 -70
- package/scripts/acceptance.mjs +502 -502
- package/scripts/fixtures/guides/bad-guide.md +15 -15
- package/scripts/fixtures/guides-clean/good-guide.md +16 -16
- package/scripts/install-rdc-skills.js +1289 -1289
- package/scripts/install.ps1 +202 -202
- package/scripts/install.sh +132 -132
- package/scripts/lib/assertions.mjs +287 -287
- package/scripts/lib/manifest-schema.mjs +754 -754
- package/scripts/lib/runner.mjs +465 -465
- package/scripts/lib/sandbox.mjs +435 -435
- package/scripts/prepack.mjs +32 -32
- package/scripts/rdc-brochure.mjs +464 -464
- package/scripts/rdc-design-cli.mjs +134 -134
- package/scripts/rebuild-mcp.mjs +107 -107
- package/scripts/self-test.mjs +1460 -1460
- package/scripts/stamp-git-sha.mjs +29 -29
- package/scripts/test-guide-validator.mjs +196 -196
- package/scripts/test-rdc-hooks.mjs +145 -145
- package/scripts/uninstall.ps1 +77 -77
- package/scripts/uninstall.sh +69 -69
- package/scripts/update.ps1 +43 -43
- package/scripts/update.sh +43 -43
- package/scripts/validate-place-histories.js +461 -461
- package/scripts/validate-publish-manifests.js +424 -424
- package/scripts/watch-init.mjs +100 -100
- package/skills/brochure/SKILL.md +107 -107
- package/skills/build/SKILL.md +563 -554
- package/skills/channel-formatter/SKILL.md +533 -533
- package/skills/co-develop/SKILL.md +196 -196
- package/skills/collab/SKILL.md +239 -239
- package/skills/convert/SKILL.md +140 -140
- package/skills/deploy/SKILL.md +541 -541
- package/skills/design/SKILL.md +211 -211
- package/skills/design/reference/ownership.md +16 -16
- package/skills/design/reference/rampa.md +92 -92
- package/skills/design/reference/studio-model.md +153 -153
- package/skills/edit/SKILL.md +98 -98
- package/skills/fixit/SKILL.md +165 -165
- package/skills/fs-mcp/SKILL.md +148 -148
- package/skills/handoff/SKILL.md +236 -200
- package/skills/help/SKILL.md +143 -143
- package/skills/housekeeping/SKILL.md +189 -189
- package/skills/lifeai-brochure-author/SKILL.md +340 -340
- package/skills/overnight/SKILL.md +251 -251
- package/skills/plan/SKILL.md +345 -314
- package/skills/preplan/SKILL.md +90 -90
- package/skills/prototype/SKILL.md +150 -150
- package/skills/rdc-brochurify/SKILL.md +245 -245
- package/skills/rdc-extract-verifier-rules/SKILL.md +191 -191
- package/skills/release/SKILL.md +140 -140
- package/skills/report/SKILL.md +100 -100
- package/skills/review/SKILL.md +152 -152
- package/skills/rpms-filemap/SKILL.cloud.md +111 -111
- package/skills/rpms-filemap/SKILL.md +111 -111
- package/skills/self-test/SKILL.md +132 -132
- package/skills/status/SKILL.md +99 -99
- package/skills/terminal-config/SKILL.md +62 -62
- package/skills/tests/MATRIX.md +54 -54
- package/skills/tests/README.md +47 -47
- package/skills/tests/rdc-brochure.test.json +34 -34
- package/skills/tests/rdc-build.test.json +36 -36
- package/skills/tests/rdc-channel-formatter.test.json +45 -45
- package/skills/tests/rdc-co-develop.test.json +29 -29
- package/skills/tests/rdc-collab.test.json +29 -29
- package/skills/tests/rdc-convert.test.json +35 -35
- package/skills/tests/rdc-deploy.test.json +30 -30
- package/skills/tests/rdc-design.test.json +27 -27
- package/skills/tests/rdc-edit.test.json +29 -29
- package/skills/tests/rdc-fixit.test.json +36 -36
- package/skills/tests/rdc-fs-mcp.test.json +36 -36
- package/skills/tests/rdc-handoff.test.json +28 -28
- package/skills/tests/rdc-help.test.json +29 -29
- package/skills/tests/rdc-housekeeping.test.json +31 -31
- package/skills/tests/rdc-lifeai-brochure-author.test.json +35 -35
- package/skills/tests/rdc-overnight.test.json +37 -37
- package/skills/tests/rdc-plan.test.json +27 -27
- package/skills/tests/rdc-preplan.test.json +31 -31
- package/skills/tests/rdc-prototype.test.json +28 -28
- package/skills/tests/rdc-rdc-brochurify.test.json +23 -23
- package/skills/tests/rdc-rdc-extract-verifier-rules.test.json +34 -34
- package/skills/tests/rdc-release.test.json +29 -29
- package/skills/tests/rdc-report.test.json +28 -28
- package/skills/tests/rdc-review.test.json +29 -29
- package/skills/tests/rdc-rpms-filemap.test.json +28 -28
- package/skills/tests/rdc-self-test.test.json +24 -24
- package/skills/tests/rdc-status.test.json +29 -29
- package/skills/tests/rdc-terminal-config.test.json +29 -29
- package/skills/tests/rdc-watch.test.json +24 -24
- package/skills/tests/rdc-workitems.test.json +27 -27
- package/skills/watch/SKILL.md +97 -97
- package/skills/workitems/SKILL.md +151 -151
- package/tests/acceptance.test.mjs +59 -59
- package/tests/channel-formatter.contract.test.mjs +251 -251
- package/tests/curl-surface.test.mjs +289 -289
- package/tests/harness-gates.test.mjs +325 -325
- package/tests/help-surface.test.mjs +61 -61
- package/tests/install-rdc-skills.test.mjs +49 -49
- package/tests/manifest-contract-fields.test.mjs +78 -78
- package/tests/mcp.test.mjs +271 -271
- package/tests/require-work-item-on-commit.test.mjs +162 -162
- package/tests/run-evidence-gate.test.mjs +82 -82
- package/tests/skill-test-matrix.test.mjs +66 -66
- package/tests/validate-skills.js +27 -27
- package/tests/work-item-exit-gate-l2.test.mjs +368 -368
- package/tests/work-item-exit-gate-l3.test.mjs +197 -197
package/skills/deploy/SKILL.md
CHANGED
|
@@ -1,541 +1,541 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: rdc:deploy
|
|
3
|
-
description: "Usage `rdc:deploy <slug> [new|diagnose|audit|promote|convert] [--fix|--hotfix <sha>]` — Deploy an app to Coolify (production) or PM2 (staging), promote a verified dev change to production (one-command cherry-pick → PR → admin-merge → explicit Coolify trigger → verify), convert a prod app's runtime in place static→Next, add a new Coolify app, diagnose a failed deploy, or audit watch paths. Handles DNS, branch protection, health checks, and post-deploy verification."
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
> **⚠️ OUTPUT CONTRACT (READ FIRST):** `guides/output-contract.md`
|
|
7
|
-
> Checklist-only output. No tool-call narration. No raw MCP/JSON/log dumps.
|
|
8
|
-
> One checklist upfront, updated in place, shown again at end with a 1-line verdict.
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
# rdc:deploy — Coolify Operations
|
|
12
|
-
|
|
13
|
-
**READ FIRST:** `guides/output-contract.md`. Checklist-only output. No narration.
|
|
14
|
-
No raw MCP dumps. No UUIDs unless asked.
|
|
15
|
-
|
|
16
|
-
> **Sandbox contract:** This skill honors `RDC_TEST=1` per `guides/agent-bootstrap.md` § RDC_TEST Sandbox Contract. Destructive external calls short-circuit under the flag.
|
|
17
|
-
>
|
|
18
|
-
> *Under `$RDC_TEST=1`:* Modes 1 (deploy), 2 (new), and 5 (promote) are **entirely skipped** — echo `[RDC_TEST] skipping Coolify deploy/create/promote` and mark every `[ ]` line in those checklists as `[~]`. Modes 3 (diagnose) and 4 (audit without `--fix`) are **read-only and run normally**. Mode 4 with `--fix` skips all remediation — echo `[RDC_TEST] skipping audit --fix remediation` and report findings only. Registry SELECTs, Coolify status reads, HTTP gate probes, TLS checks, and DNS lookups are NOT destructive and run normally. Anything that writes (create app, set watch_paths, deploy trigger, **PR/admin-merge to main**, env var write, DNS write, CF cache purge, registry UPDATE/INSERT) is gated.
|
|
19
|
-
|
|
20
|
-
## When to Use
|
|
21
|
-
- Project lead says "deploy", "ship it", "push to production", "update the server"
|
|
22
|
-
- Project lead says "promote", "patch prod", "hotfix production", "push this fix live" — use `rdc:deploy <slug> promote`
|
|
23
|
-
- Dev is a Next.js app but prod is still a static site for the same slug ("dev is XJS, prod is static", "make prod Next") — use `rdc:deploy <slug> convert` (Mode 6)
|
|
24
|
-
- A new app needs to be registered and deployed for the first time (`rdc:deploy new`)
|
|
25
|
-
- A deployed app is behaving unexpectedly and needs diagnosis (`rdc:deploy diagnose`)
|
|
26
|
-
- Running a compliance/health audit of all deployed apps (`rdc:deploy audit`)
|
|
27
|
-
|
|
28
|
-
## Arguments
|
|
29
|
-
|
|
30
|
-
- `rdc:deploy <slug>` — deploy existing app (latest commit on its watched branch)
|
|
31
|
-
- `rdc:deploy <slug> <build-id>` — deploy specific commit/tag
|
|
32
|
-
- `rdc:deploy <slug> promote` — promote the verified `develop` change for this app to production (Mode 5)
|
|
33
|
-
- `rdc:deploy <slug> promote --hotfix <sha>` — promote a specific commit (cherry-pick just that sha to `main`)
|
|
34
|
-
- `rdc:deploy <slug> convert` — convert a prod app's runtime in place from static→Next (Mode 6); use when dev is Next but prod is still a static Coolify build
|
|
35
|
-
- `rdc:deploy new <slug>` — create a new Coolify app from registry
|
|
36
|
-
- `rdc:deploy diagnose <slug>` — debug why an app is broken
|
|
37
|
-
- `rdc:deploy audit` — fleet-wide scan for missed failures
|
|
38
|
-
- `rdc:deploy audit --fix` — fleet scan + auto-remediate safe issues
|
|
39
|
-
- `rdc:deploy` (no args) — print mode menu, ask which
|
|
40
|
-
|
|
41
|
-
## Modes
|
|
42
|
-
|
|
43
|
-
### Mode 1 — deploy <slug> [build-id]
|
|
44
|
-
|
|
45
|
-
```
|
|
46
|
-
rdc:deploy: <slug> → <domain>
|
|
47
|
-
[ ] Registry lookup (slug, uuid, branch, type, env_vars_needed)
|
|
48
|
-
[ ] Runtime-source guard: if the source being deployed is static (`sites/<name>`, nixpacks/static) but the registry `runtime` for <slug> is `next`, BLOCK — never deploy a flat/static source over a Next app. A static prototype deploys only under its OWN slug (never an existing Next app's slug), dev only. Changing a slug's runtime is architectural. (`.claude/rules/production-stack-nextjs.md`)
|
|
49
|
-
[ ] Git state verified (branch matches Coolify, commit pushed)
|
|
50
|
-
[ ] Build-id resolved (default: HEAD of watched branch)
|
|
51
|
-
[ ] Env vars present in Coolify (compare to registry)
|
|
52
|
-
[ ] Type-specific preflight (see docs/runbooks/coolify-deploy-checklist.md)
|
|
53
|
-
[ ] Mandatory pre-deploy code-review (pr-review-toolkit:code-reviewer on `git diff <last-deployed-sha>..HEAD` for this app's paths). Block deploy on `critical`/`high` findings; record `medium`/`low` and proceed.
|
|
54
|
-
[ ] PUBLISH.md read from app root (warn if absent; fail if present but invalid)
|
|
55
|
-
[ ] watch_paths derived from PUBLISH.md surfaces (union of all surface watch_paths arrays) and updated in app_deployments
|
|
56
|
-
[ ] Deploy triggered
|
|
57
|
-
[ ] Deployment reached "finished" state
|
|
58
|
-
[ ] Gate: HTTP 200
|
|
59
|
-
[ ] Gate: TLS valid (no SSL cipher mismatch)
|
|
60
|
-
[ ] Gate: cache headers correct on HTML
|
|
61
|
-
[ ] Gate: container running on declared port
|
|
62
|
-
[ ] Gate: metadata audit (see § Metadata Audit below) — warn on gaps, do not block deploy
|
|
63
|
-
[ ] Cloudflare cache purged (if proxied)
|
|
64
|
-
[ ] artifact_registry INSERT per PUBLISH.md surface (if PUBLISH.md present)
|
|
65
|
-
[ ] deployment_registry updated (last_deploy_at, status)
|
|
66
|
-
✅ rdc:deploy: <slug> deployed in Nm Ns
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
#### Static PM2 dev sites — do NOT trust the push webhook (SSH-reset + served-hash gate)
|
|
70
|
-
|
|
71
|
-
For a **static** PM2 dev site, `git push` succeeding does NOT mean the live site
|
|
72
|
-
updated: the push webhook skips/instruments static apps unreliably, so the host
|
|
73
|
-
working tree (and the committed `dist/` it serves) can stay STUCK across several
|
|
74
|
-
pushes while HTTP stays 200 and `origin/develop` looks shipped (lesson
|
|
75
|
-
2026-06-11-deploy-static-host-stuck: issho served the v1.10.0 bundle across three
|
|
76
|
-
pushes because `/srv/regen/regen-root` never pulled). The only signal is the
|
|
77
|
-
SERVED bundle hash vs the local `dist/` hash.
|
|
78
|
-
|
|
79
|
-
After pushing committed `dist/`, SSH-reset the host explicitly, then verify the
|
|
80
|
-
served hash:
|
|
81
|
-
```bash
|
|
82
|
-
_K=$(mktemp); curl -s http://127.0.0.1:52437/v/vultr-dev-ssh > "$_K"; printf '\n' >> "$_K"; chmod 600 "$_K"
|
|
83
|
-
ssh -i "$_K" root@64.237.54.189 'cd /srv/regen/regen-root && git fetch -q origin develop && git reset -q --hard origin/develop && pm2 restart <app> --update-env'
|
|
84
|
-
rm -f "$_K"
|
|
85
|
-
# Then verify SERVED hash == local build hash (HTTP 200 is NOT proof):
|
|
86
|
-
curl -s https://<app>.dev.place.fund/ | grep -oE 'index-[A-Za-z0-9_-]+\.js' # served
|
|
87
|
-
grep -oE 'index-[A-Za-z0-9_-]+\.js' sites/<app>/dist/index.html # local
|
|
88
|
-
```
|
|
89
|
-
Only when the two hashes match do the content/screenshot gates mean anything.
|
|
90
|
-
|
|
91
|
-
### Mode 2 — new <slug>
|
|
92
|
-
|
|
93
|
-
**MANDATORY:** All new apps are created from `docs/runbooks/coolify-app-templates.json`. Read that file first — pick the right template, substitute the required vars, POST the exact payload. No manual field configuration. No improvisation. The template encodes all learned lessons (base_directory, build_pack, watch_paths, health_check, ports). Deviating from it breaks things.
|
|
94
|
-
|
|
95
|
-
Template selection:
|
|
96
|
-
- `nextjs-app` — for `apps/<name>/` (Dockerfile, turbo filter, port 3000)
|
|
97
|
-
- `static-site` — for `sites/<name>/` (nixpacks, publish_directory=out, no packages)
|
|
98
|
-
- `mcp-server` — for `mcp-servers/<name>/` (Dockerfile, health check enabled, custom port)
|
|
99
|
-
|
|
100
|
-
```
|
|
101
|
-
rdc:deploy new: <slug>
|
|
102
|
-
[ ] .dockerignore present at project root (`ls {PROJECT_ROOT}/.dockerignore` — STOP if missing)
|
|
103
|
-
[ ] Template loaded from docs/runbooks/coolify-app-templates.json (pick nextjs-app / static-site / mcp-server)
|
|
104
|
-
[ ] Required vars substituted: NAME, APP_PATH, DOMAIN, BRANCH, PROJECT_UUID, ENVIRONMENT_UUID [+ TURBO_FILTER / PORT]
|
|
105
|
-
[ ] DNS path chosen (A: staging wildcard B: apex C: other zone)
|
|
106
|
-
[ ] DNS record verified or wildcard confirmed
|
|
107
|
-
[ ] Cloudflare proxy setting correct for DNS path
|
|
108
|
-
[ ] Application created via POST /applications/private-github-app (template payload)
|
|
109
|
-
[ ] UUID recorded from response
|
|
110
|
-
[ ] watch_paths verified via GET /api/v1/applications/<uuid> — must match template
|
|
111
|
-
[ ] Env vars set in Coolify (from deployment_registry.env_vars_needed)
|
|
112
|
-
[ ] First deploy triggered
|
|
113
|
-
[ ] Deployment reached "finished" state
|
|
114
|
-
[ ] Gate: HTTP 200 on <domain>
|
|
115
|
-
[ ] Gate: TLS valid
|
|
116
|
-
[ ] deployment_registry row inserted
|
|
117
|
-
✅ rdc:deploy new: <slug> live at <domain>
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
### Mode 3 — diagnose <slug>
|
|
121
|
-
|
|
122
|
-
```
|
|
123
|
-
rdc:deploy diagnose: <slug>
|
|
124
|
-
[ ] App located (uuid, domain, last deploy)
|
|
125
|
-
[ ] Container state (running / restarting / stopped)
|
|
126
|
-
[ ] Last 100 log lines scanned for known error patterns
|
|
127
|
-
[ ] Port mismatch check (declared vs actual)
|
|
128
|
-
[ ] Env var drift check (registry vs Coolify)
|
|
129
|
-
[ ] watch_paths sanity check
|
|
130
|
-
[ ] HTTP / TLS reachability
|
|
131
|
-
[ ] Cloudflare proxy state check
|
|
132
|
-
[ ] Disk space on server
|
|
133
|
-
[ ] Branch mismatch check (Coolify git_branch vs expected)
|
|
134
|
-
⚠️ rdc:deploy diagnose: <root cause in one sentence> — fix: <one command>
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
#### `next start` crash-loop — check `.next/BUILD_ID` FIRST
|
|
138
|
-
|
|
139
|
-
When a PM2 `next start` app is crash-looping (`pm2 jlist` shows high `restart_time`
|
|
140
|
-
/ "waiting restart"), check **`.next/BUILD_ID`** before chasing env/port theories —
|
|
141
|
-
it is the single gate `next start` enforces (`Could not find a production build in
|
|
142
|
-
the '.next' directory` repeats for every restart when it is missing). A `.next`
|
|
143
|
-
tree can exist as a PARTIAL build (manifests + server artifacts but no `BUILD_ID`)
|
|
144
|
-
— the signature of a `next build` that started but was interrupted/OOM-killed; the
|
|
145
|
-
crash loop then churns CPU and can re-trigger the OOM (lesson
|
|
146
|
-
2026-06-13-deploy-nextstart-missing-buildid: ~7300 restarts, all `BUILD_ID`-missing,
|
|
147
|
-
while the documented prior lead was the NEXT_PUBLIC env issue — which was NOT the
|
|
148
|
-
cause here). Fix sequence:
|
|
149
|
-
```bash
|
|
150
|
-
pm2 stop <app> # halt the loop so it stops competing for resources
|
|
151
|
-
rm -rf apps/<name>/.next # clear the partial tree
|
|
152
|
-
pnpm --filter @regen/<name> build # ONE clean scoped build (LOCAL BUILD SAFETY)
|
|
153
|
-
pm2 restart <app> --update-env
|
|
154
|
-
```
|
|
155
|
-
Guard: assert `apps/<name>/.next/BUILD_ID` exists before (re)starting any
|
|
156
|
-
`next start` process.
|
|
157
|
-
|
|
158
|
-
#### ⛔ On the 2nd IDENTICAL failed probe, STOP polling and diagnose full-state
|
|
159
|
-
|
|
160
|
-
A repeated identical failure (same 404, same 502, same non-200) is a STRUCTURAL
|
|
161
|
-
signal, not eventual-consistency — polling it will never clear it and reads to the
|
|
162
|
-
user as stalling/throttling (lesson 2026-06-10-deploy-stop-polling-diagnose: a
|
|
163
|
-
12×-404 poll loop on a remote-managed tunnel that was structurally ignoring the
|
|
164
|
-
local config — it could never clear). On the **2nd identical failed check**, stop
|
|
165
|
-
polling and pull GROUND TRUTH instead: list ALL processes / the whole effective
|
|
166
|
-
remote config / the full event log (`debugging-protocol.md` Rule 6 — full-state, not
|
|
167
|
-
a narrow filter). Poll ONLY when something is genuinely in-progress with a known
|
|
168
|
-
finish (a deploy build, a DNS first-create) — never to hope a structural error
|
|
169
|
-
resolves itself.
|
|
170
|
-
|
|
171
|
-
### Mode 4 — audit
|
|
172
|
-
|
|
173
|
-
```
|
|
174
|
-
rdc:deploy audit: fleet scan
|
|
175
|
-
[ ] Inventory join: Coolify apps ⋈ deployment_registry
|
|
176
|
-
[ ] Orphans (in one but not the other)
|
|
177
|
-
[ ] Monorepo apps missing watch_paths
|
|
178
|
-
[ ] Stale deploys (>14 days since last success)
|
|
179
|
-
[ ] Registry rows with status='broken'
|
|
180
|
-
[ ] Failed deployments in last 7 days
|
|
181
|
-
[ ] HTTP gate sweep (non-200 per domain)
|
|
182
|
-
[ ] TLS cert expiry <30 days
|
|
183
|
-
[ ] Port mismatches (ports_exposes vs actual container port)
|
|
184
|
-
[ ] Env var drift (registry.env_vars_needed vs Coolify env)
|
|
185
|
-
[ ] Branch mismatches (Coolify git_branch ≠ expected)
|
|
186
|
-
[ ] Disk space on 64.237.54.189
|
|
187
|
-
[ ] DNS/proxy misconfigs on configured staging wildcard
|
|
188
|
-
[ ] Duplicate apps (same repo, multiple UUIDs)
|
|
189
|
-
|
|
190
|
-
Findings:
|
|
191
|
-
| Severity | App | Issue | Fix |
|
|
192
|
-
|----------|-----|-------|-----|
|
|
193
|
-
| HIGH | ... | ... | ... |
|
|
194
|
-
⚠️ rdc:deploy audit: N HIGH · M MED · K LOW — run `rdc:deploy audit --fix` to auto-remediate safe issues
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
Severity rules:
|
|
198
|
-
- **HIGH** — user-facing down (HTTP non-200, TLS invalid, container not running)
|
|
199
|
-
- **MED** — degraded or drifting (watch_paths missing, env var drift, stale deploy, branch mismatch)
|
|
200
|
-
- **LOW** — cleanup (orphans, duplicates, registry status stale)
|
|
201
|
-
|
|
202
|
-
`--fix` auto-remediates only: missing watch_paths, registry row updates, CF cache purges. Never touches env vars, DNS, or container config without explicit confirmation.
|
|
203
|
-
|
|
204
|
-
### Mode 5 — promote <slug> [--hotfix <sha>]
|
|
205
|
-
|
|
206
|
-
Promote a **verified `develop` change** for one app to production. This is the sanctioned production-patch fast path — one command instead of fighting branch protection, the main-push hook, and a flaky Coolify webhook by hand.
|
|
207
|
-
|
|
208
|
-
**Authorization:** production promote requires explicit user go-ahead ("promote", "patch prod", "push live", "go"). A dev deploy does NOT. If the user has not given it for THIS promote, stop and ask first.
|
|
209
|
-
|
|
210
|
-
```
|
|
211
|
-
rdc:deploy promote: <slug> → <prod-domain>
|
|
212
|
-
[ ] Registry lookup: PROD row (uuid, prod branch=main, watch_paths, url)
|
|
213
|
-
[ ] PUBLISH.md status gate: status=active AND prod in environments (block if not)
|
|
214
|
-
[ ] Runtime-source guard: BLOCK promoting a static source to a prod row whose intended `runtime` is `next` — production apps are Next.js (DB-backed). (`.claude/rules/production-stack-nextjs.md`)
|
|
215
|
-
[ ] Scope resolved: --hotfix <sha> → that commit; else the app-path commits on develop not on main
|
|
216
|
-
[ ] Scope guard: promote ONLY this app's paths. NEVER merge develop→main wholesale (drags unrelated WIP to prod)
|
|
217
|
-
[ ] Clean worktree off origin/main (never switch the dirty working tree)
|
|
218
|
-
[ ] Export Supabase creds in the worktree BEFORE commit (the pre-commit `sync:docs` hook needs them, and a fresh worktree does NOT inherit the main checkout's `.env*`/shell env — without the key sync:docs silently regenerates a GUTTED `app-deployments.md` ("Registry: unavailable") and `git add`s it into the surgical promote; `--no-verify` is forbidden):
|
|
219
|
-
`export NEXT_PUBLIC_SUPABASE_URL=https://uvojezuorjgqzmhhgluu.supabase.co`
|
|
220
|
-
`export NEXT_PUBLIC_SUPABASE_ANON_KEY=$(curl -s http://127.0.0.1:52437/v/supabase-anon)`
|
|
221
|
-
Then ALWAYS `git show --stat HEAD` before pushing and confirm the file set is scoped — never ship the no-key gutted app-deployments.md. (lesson 2026-06-13-deploy-worktree-syncdocs-guts-app-deployments)
|
|
222
|
-
[ ] Apply change: cherry-pick <sha> (or `git checkout <develop-sha> -- <app-paths>`); confirm diff = expected files only
|
|
223
|
-
[ ] Mandatory pre-promote code-review (pr-review-toolkit:code-reviewer on the promote diff). Block on critical/high.
|
|
224
|
-
[ ] Commit on promote branch; push branch (NOT main directly — the main-push hook blocks raw main pushes)
|
|
225
|
-
[ ] Open PR base=main; merge with admin override (`gh pr merge --squash --admin --delete-branch`) — branch protection needs --admin
|
|
226
|
-
[ ] EXPLICITLY trigger Coolify deploy — NEVER rely on the GitHub→Coolify webhook (it silently no-ops on some merges even with auto-deploy ON)
|
|
227
|
-
[ ] Deployment reached "finished" state (poll coolify_events / deployment status)
|
|
228
|
-
[ ] Gate: HTTP 200 on prod domain
|
|
229
|
-
[ ] Gate: content-level check — assert the actual changed string is live (200 alone is NOT proof; origin may serve stale)
|
|
230
|
-
[ ] Gate: TLS valid
|
|
231
|
-
[ ] Gate: metadata audit (see § Metadata Audit below) — BLOCK promote on any missing required item
|
|
232
|
-
[ ] Cloudflare cache purged (if proxied)
|
|
233
|
-
[ ] deployment_registry updated (last_deploy_at, last_deploy_commit, status)
|
|
234
|
-
✅ rdc:deploy promote: <slug> live in prod — <changed-string> verified
|
|
235
|
-
```
|
|
236
|
-
|
|
237
|
-
**The explicit Coolify trigger (the whole point — do not skip):**
|
|
238
|
-
```bash
|
|
239
|
-
_COOLIFY=$(curl -s http://127.0.0.1:52437/v/coolify-api)
|
|
240
|
-
# Correct endpoint is GET /api/v1/deploy?uuid= — NOT POST /applications/<uuid>/deploy (that 404s)
|
|
241
|
-
curl -s -H "Authorization: Bearer $_COOLIFY" \
|
|
242
|
-
"$DEPLOY_API_BASE/api/v1/deploy?uuid=<PROD_UUID>&force=true"
|
|
243
|
-
# → {"deployments":[{"deployment_uuid":"...","message":"...deployment queued."}]}
|
|
244
|
-
```
|
|
245
|
-
|
|
246
|
-
**Why each guard exists (lessons from 2026-06-05 EF Hooper promote):**
|
|
247
|
-
- `main` branch protection rejects PR merge without `--admin`; a raw `git push …:main` is blocked by the main-push hook → must go branch → PR → admin-merge.
|
|
248
|
-
- Coolify auto-deploy was **ON** for the app yet the merge did **not** auto-deploy — a webhook-delivery flake. A promote must ALWAYS trigger the deploy explicitly and verify; never hope the webhook fired.
|
|
249
|
-
- `develop` was 87 commits ahead of `main` (unrelated apps' WIP). Promoting must be surgical (this app's paths / one sha), never a develop→main merge.
|
|
250
|
-
- HTTP 200 was returned by the stale origin the whole time — only a content-level assertion (`curl … | grep '<new string>'`) proves the promote landed.
|
|
251
|
-
|
|
252
|
-
### Mode 6 — convert <slug> (prod runtime static→Next, in place)
|
|
253
|
-
|
|
254
|
-
One-time conversion of an existing **production** Coolify app from a static/nixpacks build to the Next.js (dockerfile) runtime, **in place** — same UUID, same fqdn, same DNS. After this, the slug is a normal Next app and every future ship is just `rdc:deploy <slug> promote`. This is the mode for "dev is Next, prod is still static" (the `vlas.earth` / `life.ai` class). `promote` alone CANNOT do this — it re-triggers the prod app's existing (static) build; only `convert` changes the build config.
|
|
255
|
-
|
|
256
|
-
**Authorization:** this changes a production app's runtime — **architectural** per `.claude/rules/production-stack-nextjs.md` + `.claude/rules/architectural-change-approval.md`. Requires explicit user go-ahead for THIS slug. Stop and ask if not given.
|
|
257
|
-
|
|
258
|
-
**Why in-place PATCH, not recreate:** keeps the application UUID, fqdn (apex + `www`), DNS binding, and project — zero DNS cutover, zero window where the domain is unbound. PATCH switches the build pack; the next deploy builds the Next app on the same application object.
|
|
259
|
-
|
|
260
|
-
```
|
|
261
|
-
rdc:deploy convert: <slug> → <prod-domain> (static→Next, in place)
|
|
262
|
-
[ ] Go-ahead confirmed for THIS slug (production runtime change — architectural)
|
|
263
|
-
[ ] Registry lookup: PROD coolify_uuid, fqdn, project/env, current build_pack
|
|
264
|
-
[ ] Confirm it IS a convert: dev runtime=next AND prod build_pack ∈ {static, nixpacks}. If prod build_pack already `dockerfile` → already converted, fall through to Mode 5 promote.
|
|
265
|
-
[ ] Source Next-prod-readiness preflight (BLOCK on any miss):
|
|
266
|
-
- apps/<name>/Dockerfile present (prod build) — BLOCK "source not Next-prod-ready: add Dockerfile" if missing
|
|
267
|
-
- start/listen port reconciled with intended ports_exposes (no dev-only port like :3214 leaking to prod)
|
|
268
|
-
- PUBLISH.md present, build_type=nextjs, status=active, prod in environments
|
|
269
|
-
[ ] Mandatory pre-convert code-review (pr-review-toolkit:code-reviewer on the apps/<name> diff being promoted). Block on critical/high.
|
|
270
|
-
[ ] Promote app code to main (Mode 5 path): clean worktree off origin/main → checkout apps/<name> paths (or cherry-pick) → confirm diff = expected files only → branch → PR base=main → `gh pr merge --squash --admin --delete-branch`
|
|
271
|
-
[ ] LOCKFILE IMPORTER (mandatory for an app NEW to main): if `git show origin/main:pnpm-lock.yaml | grep -c "apps/<name>:"` is 0, the Docker `pnpm install --frozen-lockfile` WILL fail. In the clean main worktree run `pnpm install --lockfile-only` (adds only the apps/<name> importer; verify the diff is minimal — do NOT promote develop's whole lockfile, it carries unrelated drift) and include `pnpm-lock.yaml` in the SAME promote PR.
|
|
272
|
-
[ ] In-place PATCH prod Coolify app <uuid> to nextjs-app template fields:
|
|
273
|
-
build_pack=dockerfile · dockerfile_location=/apps/<name>/Dockerfile · base_directory=/ ·
|
|
274
|
-
build_command="pnpm turbo run build --filter=<pkg>" · start_command="pnpm --filter=<pkg> start" ·
|
|
275
|
-
install_command="pnpm install --frozen-lockfile" · ports_exposes=<port> ·
|
|
276
|
-
watch_paths="apps/<name>/**\npackages/**"
|
|
277
|
-
(clear publish_directory; static-only field)
|
|
278
|
-
[ ] PROXY LABELS (mandatory — the static-app `custom_labels` are read-only and do NOT auto-update on a build_pack PATCH): fetch `custom_labels`, base64-decode, rewrite `loadbalancer.server.port=80`→`=<port>` (all routers) AND `upstreams 80}}`→`upstreams <port>}}`, and STRIP the static `caddy_*.try_files=…/index.html /index.php` lines (wrong for a Next app), re-base64, PATCH `custom_labels`. Skipping this = container serves on :<port> but the proxy still routes :80 → **502** even though the build "finished" and the app logs "Ready". (Note: the API rejects `is_container_label_readonly_enabled` and a non-base64 `custom_labels` — you must hand-rewrite the base64.)
|
|
279
|
-
[ ] Env vars present in Coolify (compare registry.env_vars_needed); set any missing
|
|
280
|
-
[ ] EXPLICITLY trigger deploy: GET /api/v1/deploy?uuid=<PROD_UUID>&force=true — never rely on the webhook
|
|
281
|
-
[ ] Deployment reached "finished" state (poll coolify_events)
|
|
282
|
-
[ ] Gate: SSR proof — prod HTML now contains `/_next/static` (it is the Next app, not the old static build)
|
|
283
|
-
[ ] Gate: HTTP 200 + content-level assertion of a known string from the Next render
|
|
284
|
-
[ ] Gate: TLS valid; cache headers correct on HTML
|
|
285
|
-
[ ] Gate: metadata audit (see § Metadata Audit) — BLOCK on any required gap
|
|
286
|
-
[ ] Cloudflare cache purged (apex + www)
|
|
287
|
-
[ ] app_deployments updated: runtime intent → next, notes (converted static→Next <date>), last_deploy_at, last_deploy_commit, status=active
|
|
288
|
-
[ ] `apps` row runtime=next confirmed
|
|
289
|
-
✅ rdc:deploy convert: <slug> now Next.js in prod — SSR verified at <prod-domain>
|
|
290
|
-
```
|
|
291
|
-
|
|
292
|
-
**Rollback:** PATCH the app back to static (`build_pack=static`, `base_directory=/sites/<name>`, `publish_directory=/sites/<name>`, `ports_exposes=80`, `watch_paths=sites/<name>/**`) and redeploy. `sites/<name>` stays in the repo precisely so this rollback is always available.
|
|
293
|
-
|
|
294
|
-
**PATCH command (in-place build-pack switch):**
|
|
295
|
-
```bash
|
|
296
|
-
_COOLIFY=$(curl -s http://127.0.0.1:52437/v/coolify-api)
|
|
297
|
-
curl -s -X PATCH -H "Authorization: Bearer $_COOLIFY" -H "Content-Type: application/json" \
|
|
298
|
-
-d '{"build_pack":"dockerfile","dockerfile_location":"/apps/<name>/Dockerfile","base_directory":"/","build_command":"pnpm turbo run build --filter=<pkg>","start_command":"pnpm --filter=<pkg> start","install_command":"pnpm install --frozen-lockfile","ports_exposes":"<port>","watch_paths":"apps/<name>/**\npackages/**"}' \
|
|
299
|
-
"$DEPLOY_API_BASE/api/v1/applications/<PROD_UUID>"
|
|
300
|
-
```
|
|
301
|
-
|
|
302
|
-
## Metadata Audit
|
|
303
|
-
|
|
304
|
-
After a successful deploy or promote, audit the live site's `<head>` metadata. Run this against the deployed URL (not a local file).
|
|
305
|
-
|
|
306
|
-
**Check list** (curl the deployed URL, parse the HTML `<head>`):
|
|
307
|
-
|
|
308
|
-
| # | Item | How to check | Required | Severity |
|
|
309
|
-
|---|------|-------------|----------|----------|
|
|
310
|
-
| 1 | `<title>` | grep `<title>` — non-empty, not default/placeholder | yes | block |
|
|
311
|
-
| 2 | `<meta name="description">` | content attr ≥ 50 chars, ≤ 160 chars | yes | block on promote; warn on deploy |
|
|
312
|
-
| 3 | `<link rel="canonical">` | href present, starts with `https://`, matches the deployed domain | yes | block on promote; warn on deploy |
|
|
313
|
-
| 4 | `og:title` | `<meta property="og:title">` present | yes | block on promote; warn on deploy |
|
|
314
|
-
| 5 | `og:description` | `<meta property="og:description">` present, ≥ 50 chars | yes | block on promote; warn on deploy |
|
|
315
|
-
| 6 | `og:image` | `<meta property="og:image">` present, URL returns HTTP 200 | yes | block on promote; warn on deploy |
|
|
316
|
-
| 7 | `og:url` | present, matches canonical | recommended | warn |
|
|
317
|
-
| 8 | `twitter:card` | `summary_large_image` or `summary` | recommended | warn |
|
|
318
|
-
| 9 | `twitter:image` | present, URL returns HTTP 200 | recommended | warn |
|
|
319
|
-
| 10 | Favicon | `<link rel="icon">` present, href returns HTTP 200 | yes | block on promote; warn on deploy |
|
|
320
|
-
| 11 | `sitemap.xml` | `<domain>/sitemap.xml` returns HTTP 200 | recommended | warn |
|
|
321
|
-
| 12 | `robots.txt` | `<domain>/robots.txt` returns HTTP 200 | recommended | warn |
|
|
322
|
-
| 13 | Version | `<meta name="version">` or visible version string in page | yes (project rule) | warn |
|
|
323
|
-
|
|
324
|
-
**Behavior by mode:**
|
|
325
|
-
- **Mode 1 (deploy):** run the audit after HTTP/TLS gates. Print a table of results. **Warn** on gaps but do NOT block the deploy — dev deploys are iterative.
|
|
326
|
-
- **Mode 5 (promote):** run the audit after content-level check. **Block the promote** if any item marked "block on promote" is missing. Print the table and require the metadata to be fixed before re-attempting.
|
|
327
|
-
- **Mode 4 (audit):** include the metadata sweep in the fleet scan (one row per app per missing item in the findings table).
|
|
328
|
-
|
|
329
|
-
**Implementation — one-liner per check:**
|
|
330
|
-
```bash
|
|
331
|
-
URL="https://<domain>"
|
|
332
|
-
HEAD=$(curl -s "$URL" | sed -n '/<head/,/<\/head>/p')
|
|
333
|
-
echo "$HEAD" | grep -ioE '<title>[^<]+</title>'
|
|
334
|
-
echo "$HEAD" | grep -ioE 'name="description"[^>]*content="[^"]*"'
|
|
335
|
-
echo "$HEAD" | grep -ioE 'rel="canonical"[^>]*href="[^"]*"'
|
|
336
|
-
echo "$HEAD" | grep -ioE 'property="og:title"[^>]*content="[^"]*"'
|
|
337
|
-
echo "$HEAD" | grep -ioE 'property="og:image"[^>]*content="[^"]*"'
|
|
338
|
-
echo "$HEAD" | grep -ioE 'name="twitter:card"[^>]*content="[^"]*"'
|
|
339
|
-
echo "$HEAD" | grep -ioE 'rel="icon"[^>]*href="[^"]*"'
|
|
340
|
-
echo "$HEAD" | grep -ioE 'name="version"[^>]*content="[^"]*"'
|
|
341
|
-
curl -s -o /dev/null -w "%{http_code}" "$URL/sitemap.xml"
|
|
342
|
-
curl -s -o /dev/null -w "%{http_code}" "$URL/robots.txt"
|
|
343
|
-
OG_IMG=$(echo "$HEAD" | grep -ioE 'property="og:image"[^>]*content="([^"]*)"' | grep -ioE 'https://[^"]*')
|
|
344
|
-
[ -n "$OG_IMG" ] && curl -s -o /dev/null -w "%{http_code}" "$OG_IMG"
|
|
345
|
-
```
|
|
346
|
-
|
|
347
|
-
**Why this exists (2026-06-05):** life.ai deployed to production with zero social/SEO metadata — no description, no OG, no favicon, no sitemap. HTTP 200 + TLS + content passed; metadata was invisible to the existing gates. This audit catches that class of defect.
|
|
348
|
-
|
|
349
|
-
## Hotlink referer allowlist — new media.place.fund consumer domains
|
|
350
|
-
|
|
351
|
-
When onboarding a NEW brand domain that loads assets from `media.place.fund`, add
|
|
352
|
-
that domain to the hotlink-protection Worker's referer allowlist **before
|
|
353
|
-
go-live** (lesson 2026-06-14-deploy-issholiving-referer-hotlink-403: a new brand
|
|
354
|
-
domain served its own HTML fine but every `media.place.fund` image returned 403
|
|
355
|
-
because the hotlink Worker rejected the unknown `Referer`). The image 403 is
|
|
356
|
-
invisible to the HTTP-200 / TLS / metadata gates — they probe the HTML document,
|
|
357
|
-
not the cross-origin asset.
|
|
358
|
-
|
|
359
|
-
- Add the new origin (apex + `www`, dev + prod) to the Worker's referer allowlist
|
|
360
|
-
and redeploy the Worker before the brand domain goes live.
|
|
361
|
-
- Verify with an explicit `Referer` probe (NOT a bare curl — a missing Referer can
|
|
362
|
-
pass while the real browser Referer fails):
|
|
363
|
-
```bash
|
|
364
|
-
curl -s -o /dev/null -w "%{http_code}" -e "https://<new-brand-domain>/" \
|
|
365
|
-
"https://media.place.fund/<a-known-asset-path>" # expect 200, not 403
|
|
366
|
-
```
|
|
367
|
-
|
|
368
|
-
## PUBLISH.md Integration
|
|
369
|
-
|
|
370
|
-
Every deploy reads `PUBLISH.md` from the app's source root to derive `watch_paths` and to register surfaces in Studio `artifact_registry`.
|
|
371
|
-
|
|
372
|
-
### Step 6 — Read PUBLISH.md from the app root
|
|
373
|
-
|
|
374
|
-
```bash
|
|
375
|
-
MONOREPO_PATH=$(get_app_deployments_monorepo_path "$SLUG")
|
|
376
|
-
PUBLISH_MD="$MONOREPO_PATH/PUBLISH.md"
|
|
377
|
-
|
|
378
|
-
if [ ! -f "$PUBLISH_MD" ]; then
|
|
379
|
-
echo "WARN: PUBLISH.md missing for $SLUG — using app_deployments.watch_paths only"
|
|
380
|
-
# Deploy continues; watch_paths derivation and artifact_registry INSERT are skipped
|
|
381
|
-
fi
|
|
382
|
-
```
|
|
383
|
-
|
|
384
|
-
PUBLISH.md format: see `C:/Dev/rdc-skills/guides/publish-md-spec.md` (authoritative).
|
|
385
|
-
|
|
386
|
-
Required frontmatter fields: `schema_version`, `entity_slug`, `artifact_type`, `environments`, `status`.
|
|
387
|
-
One or more `<!-- SURFACE:<id> -->` … `<!-- /SURFACE:<id> -->` blocks per surface (each with `path`, `source_dir`, `build_type`, `visibility`, `cache`, `watch_paths`).
|
|
388
|
-
|
|
389
|
-
If PUBLISH.md is **present but invalid** (missing required field, bad enum, no surface blocks): abort deploy with `BLOCKED: PUBLISH.md parse error for <slug> — <reason>`.
|
|
390
|
-
|
|
391
|
-
### Step 7 — Derive watch_paths from PUBLISH.md surfaces
|
|
392
|
-
|
|
393
|
-
Union all `watch_paths` arrays across every surface section in PUBLISH.md. Update `app_deployments.watch_paths` for the app slug to this derived union before triggering the Coolify deploy.
|
|
394
|
-
|
|
395
|
-
```sql
|
|
396
|
-
UPDATE app_deployments
|
|
397
|
-
SET watch_paths = '<union-of-surface-watch_paths>'
|
|
398
|
-
WHERE app_slug = '<slug>';
|
|
399
|
-
```
|
|
400
|
-
|
|
401
|
-
Also PATCH the Coolify application's `watch_paths` field:
|
|
402
|
-
|
|
403
|
-
```bash
|
|
404
|
-
_COOLIFY=$(curl -s http://127.0.0.1:52437/v/coolify-api)
|
|
405
|
-
WATCH_PATHS_JSON=$(derive_watch_paths_union "$PUBLISH_MD")
|
|
406
|
-
curl -s -X PATCH -H "Authorization: Bearer $_COOLIFY" \
|
|
407
|
-
-H "Content-Type: application/json" \
|
|
408
|
-
-d "{\"watch_paths\":\"$WATCH_PATHS_JSON\"}" \
|
|
409
|
-
"$DEPLOY_API_BASE/api/v1/applications/<uuid>"
|
|
410
|
-
```
|
|
411
|
-
|
|
412
|
-
### Step 15 — storeArtifact per surface (after successful deploy)
|
|
413
|
-
|
|
414
|
-
After the deployment reaches "finished" state, INSERT one row into Studio `artifact_registry` for each surface declared in PUBLISH.md:
|
|
415
|
-
|
|
416
|
-
| Column | Value |
|
|
417
|
-
|--------|-------|
|
|
418
|
-
| `entity_slug` | from PUBLISH.md frontmatter `entity_slug` |
|
|
419
|
-
| `artifact_type` | from PUBLISH.md frontmatter `artifact_type` |
|
|
420
|
-
| `canonical_url` | `https://<app_deployments.url><surface.path>` |
|
|
421
|
-
| `surface_id` | surface name from `<!-- SURFACE:<id> -->` marker |
|
|
422
|
-
| `commit_sha` | HEAD SHA of the deploy |
|
|
423
|
-
| `published_at` | `now()` |
|
|
424
|
-
|
|
425
|
-
Use the Supabase MCP (`mcp__claude_ai_Supabase__execute_sql`) from the supervisor session:
|
|
426
|
-
|
|
427
|
-
```sql
|
|
428
|
-
INSERT INTO artifact_registry (entity_slug, artifact_type, canonical_url, surface_id, commit_sha, published_at)
|
|
429
|
-
VALUES ('<entity_slug>', '<artifact_type>', 'https://<url><path>', '<surface_id>', '<commit_sha>', now())
|
|
430
|
-
ON CONFLICT (entity_slug, surface_id) DO UPDATE SET
|
|
431
|
-
canonical_url = EXCLUDED.canonical_url,
|
|
432
|
-
commit_sha = EXCLUDED.commit_sha,
|
|
433
|
-
published_at = EXCLUDED.published_at;
|
|
434
|
-
```
|
|
435
|
-
|
|
436
|
-
If the INSERT fails, surface the failure in the deploy output but **do NOT roll back the deploy**. The artifact registry is a post-deploy record, not a deploy gate.
|
|
437
|
-
|
|
438
|
-
## Coolify Access — clauth + REST API
|
|
439
|
-
|
|
440
|
-
All Coolify operations use the clauth daemon and the Coolify REST API directly.
|
|
441
|
-
There is no Coolify MCP server. Do not reference `@masonator/coolify-mcp`.
|
|
442
|
-
|
|
443
|
-
```bash
|
|
444
|
-
# Get token (plain text — no JSON parsing needed)
|
|
445
|
-
_COOLIFY=$(curl -s http://127.0.0.1:52437/v/coolify-api)
|
|
446
|
-
|
|
447
|
-
# List applications
|
|
448
|
-
curl -s -H "Authorization: Bearer $_COOLIFY" \
|
|
449
|
-
"$DEPLOY_API_BASE/api/v1/applications"
|
|
450
|
-
|
|
451
|
-
# Get application details
|
|
452
|
-
curl -s -H "Authorization: Bearer $_COOLIFY" \
|
|
453
|
-
"$DEPLOY_API_BASE/api/v1/applications/<uuid>"
|
|
454
|
-
|
|
455
|
-
# Deploy (trigger) — correct endpoint is GET /api/v1/deploy?uuid=
|
|
456
|
-
# (POST /applications/<uuid>/deploy returns {"message":"Not found."})
|
|
457
|
-
curl -s -H "Authorization: Bearer $_COOLIFY" \
|
|
458
|
-
"$DEPLOY_API_BASE/api/v1/deploy?uuid=<uuid>&force=true"
|
|
459
|
-
|
|
460
|
-
# Get deployment logs
|
|
461
|
-
curl -s -H "Authorization: Bearer $_COOLIFY" \
|
|
462
|
-
"$DEPLOY_API_BASE/api/v1/deployments/<deployment-id>"
|
|
463
|
-
|
|
464
|
-
# Set env var
|
|
465
|
-
curl -s -X POST -H "Authorization: Bearer $_COOLIFY" \
|
|
466
|
-
-H "Content-Type: application/json" \
|
|
467
|
-
-d '{"key":"<KEY>","value":"<VALUE>"}' \
|
|
468
|
-
"$DEPLOY_API_BASE/api/v1/applications/<uuid>/envs"
|
|
469
|
-
|
|
470
|
-
# Set watch_paths
|
|
471
|
-
curl -s -X PATCH -H "Authorization: Bearer $_COOLIFY" \
|
|
472
|
-
-H "Content-Type: application/json" \
|
|
473
|
-
-d '{"watch_paths":"apps/<name>/**\npackages/**"}' \
|
|
474
|
-
"$DEPLOY_API_BASE/api/v1/applications/<uuid>"
|
|
475
|
-
|
|
476
|
-
# Change the app's domain — the writable field is "domains", NOT "fqdn"
|
|
477
|
-
# (Coolify v4 PATCH rejects {"fqdn":...} with "This field is not allowed"; fqdn is read-only/derived)
|
|
478
|
-
curl -s -X PATCH -H "Authorization: Bearer $_COOLIFY" \
|
|
479
|
-
-H "Content-Type: application/json" \
|
|
480
|
-
-d '{"domains":"https://<host>"}' \
|
|
481
|
-
"$DEPLOY_API_BASE/api/v1/applications/<uuid>"
|
|
482
|
-
```
|
|
483
|
-
|
|
484
|
-
**Domain change / namespace migration** (lesson 2026-06-13-deploy-media-manager-namespace-migration):
|
|
485
|
-
PATCH `applications/<uuid>` with `{"domains":"https://<host>"}` — never `fqdn`.
|
|
486
|
-
For an app NEW to main (first prod deploy) also bring the app + a lockfile importer
|
|
487
|
-
to main (clean worktree off origin/main, `pnpm install --lockfile-only`, verify the
|
|
488
|
-
diff = additions for `apps/<name>:` only); resolve the lockfile-guard vs scope-guard
|
|
489
|
-
collision by committing the app+lockfile as a SINGLE `chore(infra):`-subject commit
|
|
490
|
-
(the scope-guard's documented escape hatch) so both pre-commit guards pass.
|
|
491
|
-
|
|
492
|
-
**Never print `$_COOLIFY` to stdout.** Inline from clauth only — do not assign raw strings.
|
|
493
|
-
|
|
494
|
-
If clauth daemon is not responding (`curl -s http://127.0.0.1:52437/ping` fails):
|
|
495
|
-
```
|
|
496
|
-
BLOCKED: credential provider is not responding.
|
|
497
|
-
Fix: start the project's credential provider or configure deployment credentials through env vars, then retry.
|
|
498
|
-
I cannot proceed until this is resolved.
|
|
499
|
-
```
|
|
500
|
-
|
|
501
|
-
## Deployment Event Log — `coolify_events`
|
|
502
|
-
|
|
503
|
-
Every Coolify deploy emits a webhook → `coolify_events` row. Use this for last-N-deploys queries, debugging failed deploys, and reconciling local state with Coolify state.
|
|
504
|
-
|
|
505
|
-
Query the last 5 events for an app:
|
|
506
|
-
```sql
|
|
507
|
-
SELECT created_at, event_type, status, branch, commit_hash, duration_seconds
|
|
508
|
-
FROM coolify_events
|
|
509
|
-
WHERE app_uuid = '<uuid>' OR app_name = '<slug>'
|
|
510
|
-
ORDER BY created_at DESC LIMIT 5;
|
|
511
|
-
```
|
|
512
|
-
|
|
513
|
-
Fields:
|
|
514
|
-
- `app_uuid` — Coolify application UUID (matches `app_deployments.coolify_uuid`)
|
|
515
|
-
- `event_type` — `started | succeeded | failed | cancelled` (canonical values; consult webhook receiver for full enum)
|
|
516
|
-
- `status` — overall deploy status
|
|
517
|
-
- `branch`, `commit_hash`, `commit_message` — git context
|
|
518
|
-
- `duration_seconds` — total deploy time
|
|
519
|
-
- `payload` — full webhook payload (jsonb) for forensic debugging
|
|
520
|
-
|
|
521
|
-
When diagnosing a broken deploy in Mode 3: query `coolify_events` FIRST before re-running the deploy — the most recent event row tells you whether the previous deploy actually triggered, whether it failed, and how long it ran. Faster than checking the Coolify UI.
|
|
522
|
-
|
|
523
|
-
## Capture lessons (exit step)
|
|
524
|
-
|
|
525
|
-
Before the final verdict line, follow `.rdc/guides/lessons-learned-spec.md` § Capture procedure. If this run taught something non-obvious — a first root-cause theory that turned out wrong, the documented/standard path not working, a missing gate or check that cost a round, or a surprising tool/infra behavior — write one `.rdc/lessons/<YYYY-MM-DD>-deploy-<short-slug>.md` per lesson using the schema in that spec. Set `scope` (`simple` | `architectural`) and `status` (`open`, or `applied` if you shipped the fix in this same run, with the commit linked). Commit the lesson file(s) on `develop` alongside the run's other commits, and note "N lessons captured" in your verdict/summary. A run that taught nothing writes nothing — absence is the default.
|
|
526
|
-
|
|
527
|
-
## References
|
|
528
|
-
|
|
529
|
-
- Type-specific checklists + DNS tree + gate commands: `docs/runbooks/coolify-deploy-checklist.md`
|
|
530
|
-
- Rules / registry RPCs / hard limits: `.claude/context/coolify-deployment.md`
|
|
531
|
-
- Infrastructure constants:
|
|
532
|
-
```
|
|
533
|
-
Server UUID: ih386anenvvvn6fy1umtyow0
|
|
534
|
-
Server IP: 64.237.54.189
|
|
535
|
-
Dashboard: <deployment-dashboard-url>
|
|
536
|
-
GitHub App UUID: xdmcy60putp5h9j7k4kwg9c3
|
|
537
|
-
```
|
|
538
|
-
|
|
539
|
-
## Supersedes
|
|
540
|
-
|
|
541
|
-
`coolify-deploy` standalone skill (kept for back-compat; new work uses `rdc:deploy`).
|
|
1
|
+
---
|
|
2
|
+
name: rdc:deploy
|
|
3
|
+
description: "Usage `rdc:deploy <slug> [new|diagnose|audit|promote|convert] [--fix|--hotfix <sha>]` — Deploy an app to Coolify (production) or PM2 (staging), promote a verified dev change to production (one-command cherry-pick → PR → admin-merge → explicit Coolify trigger → verify), convert a prod app's runtime in place static→Next, add a new Coolify app, diagnose a failed deploy, or audit watch paths. Handles DNS, branch protection, health checks, and post-deploy verification."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
> **⚠️ OUTPUT CONTRACT (READ FIRST):** `guides/output-contract.md`
|
|
7
|
+
> Checklist-only output. No tool-call narration. No raw MCP/JSON/log dumps.
|
|
8
|
+
> One checklist upfront, updated in place, shown again at end with a 1-line verdict.
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# rdc:deploy — Coolify Operations
|
|
12
|
+
|
|
13
|
+
**READ FIRST:** `guides/output-contract.md`. Checklist-only output. No narration.
|
|
14
|
+
No raw MCP dumps. No UUIDs unless asked.
|
|
15
|
+
|
|
16
|
+
> **Sandbox contract:** This skill honors `RDC_TEST=1` per `guides/agent-bootstrap.md` § RDC_TEST Sandbox Contract. Destructive external calls short-circuit under the flag.
|
|
17
|
+
>
|
|
18
|
+
> *Under `$RDC_TEST=1`:* Modes 1 (deploy), 2 (new), and 5 (promote) are **entirely skipped** — echo `[RDC_TEST] skipping Coolify deploy/create/promote` and mark every `[ ]` line in those checklists as `[~]`. Modes 3 (diagnose) and 4 (audit without `--fix`) are **read-only and run normally**. Mode 4 with `--fix` skips all remediation — echo `[RDC_TEST] skipping audit --fix remediation` and report findings only. Registry SELECTs, Coolify status reads, HTTP gate probes, TLS checks, and DNS lookups are NOT destructive and run normally. Anything that writes (create app, set watch_paths, deploy trigger, **PR/admin-merge to main**, env var write, DNS write, CF cache purge, registry UPDATE/INSERT) is gated.
|
|
19
|
+
|
|
20
|
+
## When to Use
|
|
21
|
+
- Project lead says "deploy", "ship it", "push to production", "update the server"
|
|
22
|
+
- Project lead says "promote", "patch prod", "hotfix production", "push this fix live" — use `rdc:deploy <slug> promote`
|
|
23
|
+
- Dev is a Next.js app but prod is still a static site for the same slug ("dev is XJS, prod is static", "make prod Next") — use `rdc:deploy <slug> convert` (Mode 6)
|
|
24
|
+
- A new app needs to be registered and deployed for the first time (`rdc:deploy new`)
|
|
25
|
+
- A deployed app is behaving unexpectedly and needs diagnosis (`rdc:deploy diagnose`)
|
|
26
|
+
- Running a compliance/health audit of all deployed apps (`rdc:deploy audit`)
|
|
27
|
+
|
|
28
|
+
## Arguments
|
|
29
|
+
|
|
30
|
+
- `rdc:deploy <slug>` — deploy existing app (latest commit on its watched branch)
|
|
31
|
+
- `rdc:deploy <slug> <build-id>` — deploy specific commit/tag
|
|
32
|
+
- `rdc:deploy <slug> promote` — promote the verified `develop` change for this app to production (Mode 5)
|
|
33
|
+
- `rdc:deploy <slug> promote --hotfix <sha>` — promote a specific commit (cherry-pick just that sha to `main`)
|
|
34
|
+
- `rdc:deploy <slug> convert` — convert a prod app's runtime in place from static→Next (Mode 6); use when dev is Next but prod is still a static Coolify build
|
|
35
|
+
- `rdc:deploy new <slug>` — create a new Coolify app from registry
|
|
36
|
+
- `rdc:deploy diagnose <slug>` — debug why an app is broken
|
|
37
|
+
- `rdc:deploy audit` — fleet-wide scan for missed failures
|
|
38
|
+
- `rdc:deploy audit --fix` — fleet scan + auto-remediate safe issues
|
|
39
|
+
- `rdc:deploy` (no args) — print mode menu, ask which
|
|
40
|
+
|
|
41
|
+
## Modes
|
|
42
|
+
|
|
43
|
+
### Mode 1 — deploy <slug> [build-id]
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
rdc:deploy: <slug> → <domain>
|
|
47
|
+
[ ] Registry lookup (slug, uuid, branch, type, env_vars_needed)
|
|
48
|
+
[ ] Runtime-source guard: if the source being deployed is static (`sites/<name>`, nixpacks/static) but the registry `runtime` for <slug> is `next`, BLOCK — never deploy a flat/static source over a Next app. A static prototype deploys only under its OWN slug (never an existing Next app's slug), dev only. Changing a slug's runtime is architectural. (`.claude/rules/production-stack-nextjs.md`)
|
|
49
|
+
[ ] Git state verified (branch matches Coolify, commit pushed)
|
|
50
|
+
[ ] Build-id resolved (default: HEAD of watched branch)
|
|
51
|
+
[ ] Env vars present in Coolify (compare to registry)
|
|
52
|
+
[ ] Type-specific preflight (see docs/runbooks/coolify-deploy-checklist.md)
|
|
53
|
+
[ ] Mandatory pre-deploy code-review (pr-review-toolkit:code-reviewer on `git diff <last-deployed-sha>..HEAD` for this app's paths). Block deploy on `critical`/`high` findings; record `medium`/`low` and proceed.
|
|
54
|
+
[ ] PUBLISH.md read from app root (warn if absent; fail if present but invalid)
|
|
55
|
+
[ ] watch_paths derived from PUBLISH.md surfaces (union of all surface watch_paths arrays) and updated in app_deployments
|
|
56
|
+
[ ] Deploy triggered
|
|
57
|
+
[ ] Deployment reached "finished" state
|
|
58
|
+
[ ] Gate: HTTP 200
|
|
59
|
+
[ ] Gate: TLS valid (no SSL cipher mismatch)
|
|
60
|
+
[ ] Gate: cache headers correct on HTML
|
|
61
|
+
[ ] Gate: container running on declared port
|
|
62
|
+
[ ] Gate: metadata audit (see § Metadata Audit below) — warn on gaps, do not block deploy
|
|
63
|
+
[ ] Cloudflare cache purged (if proxied)
|
|
64
|
+
[ ] artifact_registry INSERT per PUBLISH.md surface (if PUBLISH.md present)
|
|
65
|
+
[ ] deployment_registry updated (last_deploy_at, status)
|
|
66
|
+
✅ rdc:deploy: <slug> deployed in Nm Ns
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
#### Static PM2 dev sites — do NOT trust the push webhook (SSH-reset + served-hash gate)
|
|
70
|
+
|
|
71
|
+
For a **static** PM2 dev site, `git push` succeeding does NOT mean the live site
|
|
72
|
+
updated: the push webhook skips/instruments static apps unreliably, so the host
|
|
73
|
+
working tree (and the committed `dist/` it serves) can stay STUCK across several
|
|
74
|
+
pushes while HTTP stays 200 and `origin/develop` looks shipped (lesson
|
|
75
|
+
2026-06-11-deploy-static-host-stuck: issho served the v1.10.0 bundle across three
|
|
76
|
+
pushes because `/srv/regen/regen-root` never pulled). The only signal is the
|
|
77
|
+
SERVED bundle hash vs the local `dist/` hash.
|
|
78
|
+
|
|
79
|
+
After pushing committed `dist/`, SSH-reset the host explicitly, then verify the
|
|
80
|
+
served hash:
|
|
81
|
+
```bash
|
|
82
|
+
_K=$(mktemp); curl -s http://127.0.0.1:52437/v/vultr-dev-ssh > "$_K"; printf '\n' >> "$_K"; chmod 600 "$_K"
|
|
83
|
+
ssh -i "$_K" root@64.237.54.189 'cd /srv/regen/regen-root && git fetch -q origin develop && git reset -q --hard origin/develop && pm2 restart <app> --update-env'
|
|
84
|
+
rm -f "$_K"
|
|
85
|
+
# Then verify SERVED hash == local build hash (HTTP 200 is NOT proof):
|
|
86
|
+
curl -s https://<app>.dev.place.fund/ | grep -oE 'index-[A-Za-z0-9_-]+\.js' # served
|
|
87
|
+
grep -oE 'index-[A-Za-z0-9_-]+\.js' sites/<app>/dist/index.html # local
|
|
88
|
+
```
|
|
89
|
+
Only when the two hashes match do the content/screenshot gates mean anything.
|
|
90
|
+
|
|
91
|
+
### Mode 2 — new <slug>
|
|
92
|
+
|
|
93
|
+
**MANDATORY:** All new apps are created from `docs/runbooks/coolify-app-templates.json`. Read that file first — pick the right template, substitute the required vars, POST the exact payload. No manual field configuration. No improvisation. The template encodes all learned lessons (base_directory, build_pack, watch_paths, health_check, ports). Deviating from it breaks things.
|
|
94
|
+
|
|
95
|
+
Template selection:
|
|
96
|
+
- `nextjs-app` — for `apps/<name>/` (Dockerfile, turbo filter, port 3000)
|
|
97
|
+
- `static-site` — for `sites/<name>/` (nixpacks, publish_directory=out, no packages)
|
|
98
|
+
- `mcp-server` — for `mcp-servers/<name>/` (Dockerfile, health check enabled, custom port)
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
rdc:deploy new: <slug>
|
|
102
|
+
[ ] .dockerignore present at project root (`ls {PROJECT_ROOT}/.dockerignore` — STOP if missing)
|
|
103
|
+
[ ] Template loaded from docs/runbooks/coolify-app-templates.json (pick nextjs-app / static-site / mcp-server)
|
|
104
|
+
[ ] Required vars substituted: NAME, APP_PATH, DOMAIN, BRANCH, PROJECT_UUID, ENVIRONMENT_UUID [+ TURBO_FILTER / PORT]
|
|
105
|
+
[ ] DNS path chosen (A: staging wildcard B: apex C: other zone)
|
|
106
|
+
[ ] DNS record verified or wildcard confirmed
|
|
107
|
+
[ ] Cloudflare proxy setting correct for DNS path
|
|
108
|
+
[ ] Application created via POST /applications/private-github-app (template payload)
|
|
109
|
+
[ ] UUID recorded from response
|
|
110
|
+
[ ] watch_paths verified via GET /api/v1/applications/<uuid> — must match template
|
|
111
|
+
[ ] Env vars set in Coolify (from deployment_registry.env_vars_needed)
|
|
112
|
+
[ ] First deploy triggered
|
|
113
|
+
[ ] Deployment reached "finished" state
|
|
114
|
+
[ ] Gate: HTTP 200 on <domain>
|
|
115
|
+
[ ] Gate: TLS valid
|
|
116
|
+
[ ] deployment_registry row inserted
|
|
117
|
+
✅ rdc:deploy new: <slug> live at <domain>
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Mode 3 — diagnose <slug>
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
rdc:deploy diagnose: <slug>
|
|
124
|
+
[ ] App located (uuid, domain, last deploy)
|
|
125
|
+
[ ] Container state (running / restarting / stopped)
|
|
126
|
+
[ ] Last 100 log lines scanned for known error patterns
|
|
127
|
+
[ ] Port mismatch check (declared vs actual)
|
|
128
|
+
[ ] Env var drift check (registry vs Coolify)
|
|
129
|
+
[ ] watch_paths sanity check
|
|
130
|
+
[ ] HTTP / TLS reachability
|
|
131
|
+
[ ] Cloudflare proxy state check
|
|
132
|
+
[ ] Disk space on server
|
|
133
|
+
[ ] Branch mismatch check (Coolify git_branch vs expected)
|
|
134
|
+
⚠️ rdc:deploy diagnose: <root cause in one sentence> — fix: <one command>
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
#### `next start` crash-loop — check `.next/BUILD_ID` FIRST
|
|
138
|
+
|
|
139
|
+
When a PM2 `next start` app is crash-looping (`pm2 jlist` shows high `restart_time`
|
|
140
|
+
/ "waiting restart"), check **`.next/BUILD_ID`** before chasing env/port theories —
|
|
141
|
+
it is the single gate `next start` enforces (`Could not find a production build in
|
|
142
|
+
the '.next' directory` repeats for every restart when it is missing). A `.next`
|
|
143
|
+
tree can exist as a PARTIAL build (manifests + server artifacts but no `BUILD_ID`)
|
|
144
|
+
— the signature of a `next build` that started but was interrupted/OOM-killed; the
|
|
145
|
+
crash loop then churns CPU and can re-trigger the OOM (lesson
|
|
146
|
+
2026-06-13-deploy-nextstart-missing-buildid: ~7300 restarts, all `BUILD_ID`-missing,
|
|
147
|
+
while the documented prior lead was the NEXT_PUBLIC env issue — which was NOT the
|
|
148
|
+
cause here). Fix sequence:
|
|
149
|
+
```bash
|
|
150
|
+
pm2 stop <app> # halt the loop so it stops competing for resources
|
|
151
|
+
rm -rf apps/<name>/.next # clear the partial tree
|
|
152
|
+
pnpm --filter @regen/<name> build # ONE clean scoped build (LOCAL BUILD SAFETY)
|
|
153
|
+
pm2 restart <app> --update-env
|
|
154
|
+
```
|
|
155
|
+
Guard: assert `apps/<name>/.next/BUILD_ID` exists before (re)starting any
|
|
156
|
+
`next start` process.
|
|
157
|
+
|
|
158
|
+
#### ⛔ On the 2nd IDENTICAL failed probe, STOP polling and diagnose full-state
|
|
159
|
+
|
|
160
|
+
A repeated identical failure (same 404, same 502, same non-200) is a STRUCTURAL
|
|
161
|
+
signal, not eventual-consistency — polling it will never clear it and reads to the
|
|
162
|
+
user as stalling/throttling (lesson 2026-06-10-deploy-stop-polling-diagnose: a
|
|
163
|
+
12×-404 poll loop on a remote-managed tunnel that was structurally ignoring the
|
|
164
|
+
local config — it could never clear). On the **2nd identical failed check**, stop
|
|
165
|
+
polling and pull GROUND TRUTH instead: list ALL processes / the whole effective
|
|
166
|
+
remote config / the full event log (`debugging-protocol.md` Rule 6 — full-state, not
|
|
167
|
+
a narrow filter). Poll ONLY when something is genuinely in-progress with a known
|
|
168
|
+
finish (a deploy build, a DNS first-create) — never to hope a structural error
|
|
169
|
+
resolves itself.
|
|
170
|
+
|
|
171
|
+
### Mode 4 — audit
|
|
172
|
+
|
|
173
|
+
```
|
|
174
|
+
rdc:deploy audit: fleet scan
|
|
175
|
+
[ ] Inventory join: Coolify apps ⋈ deployment_registry
|
|
176
|
+
[ ] Orphans (in one but not the other)
|
|
177
|
+
[ ] Monorepo apps missing watch_paths
|
|
178
|
+
[ ] Stale deploys (>14 days since last success)
|
|
179
|
+
[ ] Registry rows with status='broken'
|
|
180
|
+
[ ] Failed deployments in last 7 days
|
|
181
|
+
[ ] HTTP gate sweep (non-200 per domain)
|
|
182
|
+
[ ] TLS cert expiry <30 days
|
|
183
|
+
[ ] Port mismatches (ports_exposes vs actual container port)
|
|
184
|
+
[ ] Env var drift (registry.env_vars_needed vs Coolify env)
|
|
185
|
+
[ ] Branch mismatches (Coolify git_branch ≠ expected)
|
|
186
|
+
[ ] Disk space on 64.237.54.189
|
|
187
|
+
[ ] DNS/proxy misconfigs on configured staging wildcard
|
|
188
|
+
[ ] Duplicate apps (same repo, multiple UUIDs)
|
|
189
|
+
|
|
190
|
+
Findings:
|
|
191
|
+
| Severity | App | Issue | Fix |
|
|
192
|
+
|----------|-----|-------|-----|
|
|
193
|
+
| HIGH | ... | ... | ... |
|
|
194
|
+
⚠️ rdc:deploy audit: N HIGH · M MED · K LOW — run `rdc:deploy audit --fix` to auto-remediate safe issues
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Severity rules:
|
|
198
|
+
- **HIGH** — user-facing down (HTTP non-200, TLS invalid, container not running)
|
|
199
|
+
- **MED** — degraded or drifting (watch_paths missing, env var drift, stale deploy, branch mismatch)
|
|
200
|
+
- **LOW** — cleanup (orphans, duplicates, registry status stale)
|
|
201
|
+
|
|
202
|
+
`--fix` auto-remediates only: missing watch_paths, registry row updates, CF cache purges. Never touches env vars, DNS, or container config without explicit confirmation.
|
|
203
|
+
|
|
204
|
+
### Mode 5 — promote <slug> [--hotfix <sha>]
|
|
205
|
+
|
|
206
|
+
Promote a **verified `develop` change** for one app to production. This is the sanctioned production-patch fast path — one command instead of fighting branch protection, the main-push hook, and a flaky Coolify webhook by hand.
|
|
207
|
+
|
|
208
|
+
**Authorization:** production promote requires explicit user go-ahead ("promote", "patch prod", "push live", "go"). A dev deploy does NOT. If the user has not given it for THIS promote, stop and ask first.
|
|
209
|
+
|
|
210
|
+
```
|
|
211
|
+
rdc:deploy promote: <slug> → <prod-domain>
|
|
212
|
+
[ ] Registry lookup: PROD row (uuid, prod branch=main, watch_paths, url)
|
|
213
|
+
[ ] PUBLISH.md status gate: status=active AND prod in environments (block if not)
|
|
214
|
+
[ ] Runtime-source guard: BLOCK promoting a static source to a prod row whose intended `runtime` is `next` — production apps are Next.js (DB-backed). (`.claude/rules/production-stack-nextjs.md`)
|
|
215
|
+
[ ] Scope resolved: --hotfix <sha> → that commit; else the app-path commits on develop not on main
|
|
216
|
+
[ ] Scope guard: promote ONLY this app's paths. NEVER merge develop→main wholesale (drags unrelated WIP to prod)
|
|
217
|
+
[ ] Clean worktree off origin/main (never switch the dirty working tree)
|
|
218
|
+
[ ] Export Supabase creds in the worktree BEFORE commit (the pre-commit `sync:docs` hook needs them, and a fresh worktree does NOT inherit the main checkout's `.env*`/shell env — without the key sync:docs silently regenerates a GUTTED `app-deployments.md` ("Registry: unavailable") and `git add`s it into the surgical promote; `--no-verify` is forbidden):
|
|
219
|
+
`export NEXT_PUBLIC_SUPABASE_URL=https://uvojezuorjgqzmhhgluu.supabase.co`
|
|
220
|
+
`export NEXT_PUBLIC_SUPABASE_ANON_KEY=$(curl -s http://127.0.0.1:52437/v/supabase-anon)`
|
|
221
|
+
Then ALWAYS `git show --stat HEAD` before pushing and confirm the file set is scoped — never ship the no-key gutted app-deployments.md. (lesson 2026-06-13-deploy-worktree-syncdocs-guts-app-deployments)
|
|
222
|
+
[ ] Apply change: cherry-pick <sha> (or `git checkout <develop-sha> -- <app-paths>`); confirm diff = expected files only
|
|
223
|
+
[ ] Mandatory pre-promote code-review (pr-review-toolkit:code-reviewer on the promote diff). Block on critical/high.
|
|
224
|
+
[ ] Commit on promote branch; push branch (NOT main directly — the main-push hook blocks raw main pushes)
|
|
225
|
+
[ ] Open PR base=main; merge with admin override (`gh pr merge --squash --admin --delete-branch`) — branch protection needs --admin
|
|
226
|
+
[ ] EXPLICITLY trigger Coolify deploy — NEVER rely on the GitHub→Coolify webhook (it silently no-ops on some merges even with auto-deploy ON)
|
|
227
|
+
[ ] Deployment reached "finished" state (poll coolify_events / deployment status)
|
|
228
|
+
[ ] Gate: HTTP 200 on prod domain
|
|
229
|
+
[ ] Gate: content-level check — assert the actual changed string is live (200 alone is NOT proof; origin may serve stale)
|
|
230
|
+
[ ] Gate: TLS valid
|
|
231
|
+
[ ] Gate: metadata audit (see § Metadata Audit below) — BLOCK promote on any missing required item
|
|
232
|
+
[ ] Cloudflare cache purged (if proxied)
|
|
233
|
+
[ ] deployment_registry updated (last_deploy_at, last_deploy_commit, status)
|
|
234
|
+
✅ rdc:deploy promote: <slug> live in prod — <changed-string> verified
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
**The explicit Coolify trigger (the whole point — do not skip):**
|
|
238
|
+
```bash
|
|
239
|
+
_COOLIFY=$(curl -s http://127.0.0.1:52437/v/coolify-api)
|
|
240
|
+
# Correct endpoint is GET /api/v1/deploy?uuid= — NOT POST /applications/<uuid>/deploy (that 404s)
|
|
241
|
+
curl -s -H "Authorization: Bearer $_COOLIFY" \
|
|
242
|
+
"$DEPLOY_API_BASE/api/v1/deploy?uuid=<PROD_UUID>&force=true"
|
|
243
|
+
# → {"deployments":[{"deployment_uuid":"...","message":"...deployment queued."}]}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
**Why each guard exists (lessons from 2026-06-05 EF Hooper promote):**
|
|
247
|
+
- `main` branch protection rejects PR merge without `--admin`; a raw `git push …:main` is blocked by the main-push hook → must go branch → PR → admin-merge.
|
|
248
|
+
- Coolify auto-deploy was **ON** for the app yet the merge did **not** auto-deploy — a webhook-delivery flake. A promote must ALWAYS trigger the deploy explicitly and verify; never hope the webhook fired.
|
|
249
|
+
- `develop` was 87 commits ahead of `main` (unrelated apps' WIP). Promoting must be surgical (this app's paths / one sha), never a develop→main merge.
|
|
250
|
+
- HTTP 200 was returned by the stale origin the whole time — only a content-level assertion (`curl … | grep '<new string>'`) proves the promote landed.
|
|
251
|
+
|
|
252
|
+
### Mode 6 — convert <slug> (prod runtime static→Next, in place)
|
|
253
|
+
|
|
254
|
+
One-time conversion of an existing **production** Coolify app from a static/nixpacks build to the Next.js (dockerfile) runtime, **in place** — same UUID, same fqdn, same DNS. After this, the slug is a normal Next app and every future ship is just `rdc:deploy <slug> promote`. This is the mode for "dev is Next, prod is still static" (the `vlas.earth` / `life.ai` class). `promote` alone CANNOT do this — it re-triggers the prod app's existing (static) build; only `convert` changes the build config.
|
|
255
|
+
|
|
256
|
+
**Authorization:** this changes a production app's runtime — **architectural** per `.claude/rules/production-stack-nextjs.md` + `.claude/rules/architectural-change-approval.md`. Requires explicit user go-ahead for THIS slug. Stop and ask if not given.
|
|
257
|
+
|
|
258
|
+
**Why in-place PATCH, not recreate:** keeps the application UUID, fqdn (apex + `www`), DNS binding, and project — zero DNS cutover, zero window where the domain is unbound. PATCH switches the build pack; the next deploy builds the Next app on the same application object.
|
|
259
|
+
|
|
260
|
+
```
|
|
261
|
+
rdc:deploy convert: <slug> → <prod-domain> (static→Next, in place)
|
|
262
|
+
[ ] Go-ahead confirmed for THIS slug (production runtime change — architectural)
|
|
263
|
+
[ ] Registry lookup: PROD coolify_uuid, fqdn, project/env, current build_pack
|
|
264
|
+
[ ] Confirm it IS a convert: dev runtime=next AND prod build_pack ∈ {static, nixpacks}. If prod build_pack already `dockerfile` → already converted, fall through to Mode 5 promote.
|
|
265
|
+
[ ] Source Next-prod-readiness preflight (BLOCK on any miss):
|
|
266
|
+
- apps/<name>/Dockerfile present (prod build) — BLOCK "source not Next-prod-ready: add Dockerfile" if missing
|
|
267
|
+
- start/listen port reconciled with intended ports_exposes (no dev-only port like :3214 leaking to prod)
|
|
268
|
+
- PUBLISH.md present, build_type=nextjs, status=active, prod in environments
|
|
269
|
+
[ ] Mandatory pre-convert code-review (pr-review-toolkit:code-reviewer on the apps/<name> diff being promoted). Block on critical/high.
|
|
270
|
+
[ ] Promote app code to main (Mode 5 path): clean worktree off origin/main → checkout apps/<name> paths (or cherry-pick) → confirm diff = expected files only → branch → PR base=main → `gh pr merge --squash --admin --delete-branch`
|
|
271
|
+
[ ] LOCKFILE IMPORTER (mandatory for an app NEW to main): if `git show origin/main:pnpm-lock.yaml | grep -c "apps/<name>:"` is 0, the Docker `pnpm install --frozen-lockfile` WILL fail. In the clean main worktree run `pnpm install --lockfile-only` (adds only the apps/<name> importer; verify the diff is minimal — do NOT promote develop's whole lockfile, it carries unrelated drift) and include `pnpm-lock.yaml` in the SAME promote PR.
|
|
272
|
+
[ ] In-place PATCH prod Coolify app <uuid> to nextjs-app template fields:
|
|
273
|
+
build_pack=dockerfile · dockerfile_location=/apps/<name>/Dockerfile · base_directory=/ ·
|
|
274
|
+
build_command="pnpm turbo run build --filter=<pkg>" · start_command="pnpm --filter=<pkg> start" ·
|
|
275
|
+
install_command="pnpm install --frozen-lockfile" · ports_exposes=<port> ·
|
|
276
|
+
watch_paths="apps/<name>/**\npackages/**"
|
|
277
|
+
(clear publish_directory; static-only field)
|
|
278
|
+
[ ] PROXY LABELS (mandatory — the static-app `custom_labels` are read-only and do NOT auto-update on a build_pack PATCH): fetch `custom_labels`, base64-decode, rewrite `loadbalancer.server.port=80`→`=<port>` (all routers) AND `upstreams 80}}`→`upstreams <port>}}`, and STRIP the static `caddy_*.try_files=…/index.html /index.php` lines (wrong for a Next app), re-base64, PATCH `custom_labels`. Skipping this = container serves on :<port> but the proxy still routes :80 → **502** even though the build "finished" and the app logs "Ready". (Note: the API rejects `is_container_label_readonly_enabled` and a non-base64 `custom_labels` — you must hand-rewrite the base64.)
|
|
279
|
+
[ ] Env vars present in Coolify (compare registry.env_vars_needed); set any missing
|
|
280
|
+
[ ] EXPLICITLY trigger deploy: GET /api/v1/deploy?uuid=<PROD_UUID>&force=true — never rely on the webhook
|
|
281
|
+
[ ] Deployment reached "finished" state (poll coolify_events)
|
|
282
|
+
[ ] Gate: SSR proof — prod HTML now contains `/_next/static` (it is the Next app, not the old static build)
|
|
283
|
+
[ ] Gate: HTTP 200 + content-level assertion of a known string from the Next render
|
|
284
|
+
[ ] Gate: TLS valid; cache headers correct on HTML
|
|
285
|
+
[ ] Gate: metadata audit (see § Metadata Audit) — BLOCK on any required gap
|
|
286
|
+
[ ] Cloudflare cache purged (apex + www)
|
|
287
|
+
[ ] app_deployments updated: runtime intent → next, notes (converted static→Next <date>), last_deploy_at, last_deploy_commit, status=active
|
|
288
|
+
[ ] `apps` row runtime=next confirmed
|
|
289
|
+
✅ rdc:deploy convert: <slug> now Next.js in prod — SSR verified at <prod-domain>
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
**Rollback:** PATCH the app back to static (`build_pack=static`, `base_directory=/sites/<name>`, `publish_directory=/sites/<name>`, `ports_exposes=80`, `watch_paths=sites/<name>/**`) and redeploy. `sites/<name>` stays in the repo precisely so this rollback is always available.
|
|
293
|
+
|
|
294
|
+
**PATCH command (in-place build-pack switch):**
|
|
295
|
+
```bash
|
|
296
|
+
_COOLIFY=$(curl -s http://127.0.0.1:52437/v/coolify-api)
|
|
297
|
+
curl -s -X PATCH -H "Authorization: Bearer $_COOLIFY" -H "Content-Type: application/json" \
|
|
298
|
+
-d '{"build_pack":"dockerfile","dockerfile_location":"/apps/<name>/Dockerfile","base_directory":"/","build_command":"pnpm turbo run build --filter=<pkg>","start_command":"pnpm --filter=<pkg> start","install_command":"pnpm install --frozen-lockfile","ports_exposes":"<port>","watch_paths":"apps/<name>/**\npackages/**"}' \
|
|
299
|
+
"$DEPLOY_API_BASE/api/v1/applications/<PROD_UUID>"
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
## Metadata Audit
|
|
303
|
+
|
|
304
|
+
After a successful deploy or promote, audit the live site's `<head>` metadata. Run this against the deployed URL (not a local file).
|
|
305
|
+
|
|
306
|
+
**Check list** (curl the deployed URL, parse the HTML `<head>`):
|
|
307
|
+
|
|
308
|
+
| # | Item | How to check | Required | Severity |
|
|
309
|
+
|---|------|-------------|----------|----------|
|
|
310
|
+
| 1 | `<title>` | grep `<title>` — non-empty, not default/placeholder | yes | block |
|
|
311
|
+
| 2 | `<meta name="description">` | content attr ≥ 50 chars, ≤ 160 chars | yes | block on promote; warn on deploy |
|
|
312
|
+
| 3 | `<link rel="canonical">` | href present, starts with `https://`, matches the deployed domain | yes | block on promote; warn on deploy |
|
|
313
|
+
| 4 | `og:title` | `<meta property="og:title">` present | yes | block on promote; warn on deploy |
|
|
314
|
+
| 5 | `og:description` | `<meta property="og:description">` present, ≥ 50 chars | yes | block on promote; warn on deploy |
|
|
315
|
+
| 6 | `og:image` | `<meta property="og:image">` present, URL returns HTTP 200 | yes | block on promote; warn on deploy |
|
|
316
|
+
| 7 | `og:url` | present, matches canonical | recommended | warn |
|
|
317
|
+
| 8 | `twitter:card` | `summary_large_image` or `summary` | recommended | warn |
|
|
318
|
+
| 9 | `twitter:image` | present, URL returns HTTP 200 | recommended | warn |
|
|
319
|
+
| 10 | Favicon | `<link rel="icon">` present, href returns HTTP 200 | yes | block on promote; warn on deploy |
|
|
320
|
+
| 11 | `sitemap.xml` | `<domain>/sitemap.xml` returns HTTP 200 | recommended | warn |
|
|
321
|
+
| 12 | `robots.txt` | `<domain>/robots.txt` returns HTTP 200 | recommended | warn |
|
|
322
|
+
| 13 | Version | `<meta name="version">` or visible version string in page | yes (project rule) | warn |
|
|
323
|
+
|
|
324
|
+
**Behavior by mode:**
|
|
325
|
+
- **Mode 1 (deploy):** run the audit after HTTP/TLS gates. Print a table of results. **Warn** on gaps but do NOT block the deploy — dev deploys are iterative.
|
|
326
|
+
- **Mode 5 (promote):** run the audit after content-level check. **Block the promote** if any item marked "block on promote" is missing. Print the table and require the metadata to be fixed before re-attempting.
|
|
327
|
+
- **Mode 4 (audit):** include the metadata sweep in the fleet scan (one row per app per missing item in the findings table).
|
|
328
|
+
|
|
329
|
+
**Implementation — one-liner per check:**
|
|
330
|
+
```bash
|
|
331
|
+
URL="https://<domain>"
|
|
332
|
+
HEAD=$(curl -s "$URL" | sed -n '/<head/,/<\/head>/p')
|
|
333
|
+
echo "$HEAD" | grep -ioE '<title>[^<]+</title>'
|
|
334
|
+
echo "$HEAD" | grep -ioE 'name="description"[^>]*content="[^"]*"'
|
|
335
|
+
echo "$HEAD" | grep -ioE 'rel="canonical"[^>]*href="[^"]*"'
|
|
336
|
+
echo "$HEAD" | grep -ioE 'property="og:title"[^>]*content="[^"]*"'
|
|
337
|
+
echo "$HEAD" | grep -ioE 'property="og:image"[^>]*content="[^"]*"'
|
|
338
|
+
echo "$HEAD" | grep -ioE 'name="twitter:card"[^>]*content="[^"]*"'
|
|
339
|
+
echo "$HEAD" | grep -ioE 'rel="icon"[^>]*href="[^"]*"'
|
|
340
|
+
echo "$HEAD" | grep -ioE 'name="version"[^>]*content="[^"]*"'
|
|
341
|
+
curl -s -o /dev/null -w "%{http_code}" "$URL/sitemap.xml"
|
|
342
|
+
curl -s -o /dev/null -w "%{http_code}" "$URL/robots.txt"
|
|
343
|
+
OG_IMG=$(echo "$HEAD" | grep -ioE 'property="og:image"[^>]*content="([^"]*)"' | grep -ioE 'https://[^"]*')
|
|
344
|
+
[ -n "$OG_IMG" ] && curl -s -o /dev/null -w "%{http_code}" "$OG_IMG"
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
**Why this exists (2026-06-05):** life.ai deployed to production with zero social/SEO metadata — no description, no OG, no favicon, no sitemap. HTTP 200 + TLS + content passed; metadata was invisible to the existing gates. This audit catches that class of defect.
|
|
348
|
+
|
|
349
|
+
## Hotlink referer allowlist — new media.place.fund consumer domains
|
|
350
|
+
|
|
351
|
+
When onboarding a NEW brand domain that loads assets from `media.place.fund`, add
|
|
352
|
+
that domain to the hotlink-protection Worker's referer allowlist **before
|
|
353
|
+
go-live** (lesson 2026-06-14-deploy-issholiving-referer-hotlink-403: a new brand
|
|
354
|
+
domain served its own HTML fine but every `media.place.fund` image returned 403
|
|
355
|
+
because the hotlink Worker rejected the unknown `Referer`). The image 403 is
|
|
356
|
+
invisible to the HTTP-200 / TLS / metadata gates — they probe the HTML document,
|
|
357
|
+
not the cross-origin asset.
|
|
358
|
+
|
|
359
|
+
- Add the new origin (apex + `www`, dev + prod) to the Worker's referer allowlist
|
|
360
|
+
and redeploy the Worker before the brand domain goes live.
|
|
361
|
+
- Verify with an explicit `Referer` probe (NOT a bare curl — a missing Referer can
|
|
362
|
+
pass while the real browser Referer fails):
|
|
363
|
+
```bash
|
|
364
|
+
curl -s -o /dev/null -w "%{http_code}" -e "https://<new-brand-domain>/" \
|
|
365
|
+
"https://media.place.fund/<a-known-asset-path>" # expect 200, not 403
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
## PUBLISH.md Integration
|
|
369
|
+
|
|
370
|
+
Every deploy reads `PUBLISH.md` from the app's source root to derive `watch_paths` and to register surfaces in Studio `artifact_registry`.
|
|
371
|
+
|
|
372
|
+
### Step 6 — Read PUBLISH.md from the app root
|
|
373
|
+
|
|
374
|
+
```bash
|
|
375
|
+
MONOREPO_PATH=$(get_app_deployments_monorepo_path "$SLUG")
|
|
376
|
+
PUBLISH_MD="$MONOREPO_PATH/PUBLISH.md"
|
|
377
|
+
|
|
378
|
+
if [ ! -f "$PUBLISH_MD" ]; then
|
|
379
|
+
echo "WARN: PUBLISH.md missing for $SLUG — using app_deployments.watch_paths only"
|
|
380
|
+
# Deploy continues; watch_paths derivation and artifact_registry INSERT are skipped
|
|
381
|
+
fi
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
PUBLISH.md format: see `C:/Dev/rdc-skills/guides/publish-md-spec.md` (authoritative).
|
|
385
|
+
|
|
386
|
+
Required frontmatter fields: `schema_version`, `entity_slug`, `artifact_type`, `environments`, `status`.
|
|
387
|
+
One or more `<!-- SURFACE:<id> -->` … `<!-- /SURFACE:<id> -->` blocks per surface (each with `path`, `source_dir`, `build_type`, `visibility`, `cache`, `watch_paths`).
|
|
388
|
+
|
|
389
|
+
If PUBLISH.md is **present but invalid** (missing required field, bad enum, no surface blocks): abort deploy with `BLOCKED: PUBLISH.md parse error for <slug> — <reason>`.
|
|
390
|
+
|
|
391
|
+
### Step 7 — Derive watch_paths from PUBLISH.md surfaces
|
|
392
|
+
|
|
393
|
+
Union all `watch_paths` arrays across every surface section in PUBLISH.md. Update `app_deployments.watch_paths` for the app slug to this derived union before triggering the Coolify deploy.
|
|
394
|
+
|
|
395
|
+
```sql
|
|
396
|
+
UPDATE app_deployments
|
|
397
|
+
SET watch_paths = '<union-of-surface-watch_paths>'
|
|
398
|
+
WHERE app_slug = '<slug>';
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
Also PATCH the Coolify application's `watch_paths` field:
|
|
402
|
+
|
|
403
|
+
```bash
|
|
404
|
+
_COOLIFY=$(curl -s http://127.0.0.1:52437/v/coolify-api)
|
|
405
|
+
WATCH_PATHS_JSON=$(derive_watch_paths_union "$PUBLISH_MD")
|
|
406
|
+
curl -s -X PATCH -H "Authorization: Bearer $_COOLIFY" \
|
|
407
|
+
-H "Content-Type: application/json" \
|
|
408
|
+
-d "{\"watch_paths\":\"$WATCH_PATHS_JSON\"}" \
|
|
409
|
+
"$DEPLOY_API_BASE/api/v1/applications/<uuid>"
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
### Step 15 — storeArtifact per surface (after successful deploy)
|
|
413
|
+
|
|
414
|
+
After the deployment reaches "finished" state, INSERT one row into Studio `artifact_registry` for each surface declared in PUBLISH.md:
|
|
415
|
+
|
|
416
|
+
| Column | Value |
|
|
417
|
+
|--------|-------|
|
|
418
|
+
| `entity_slug` | from PUBLISH.md frontmatter `entity_slug` |
|
|
419
|
+
| `artifact_type` | from PUBLISH.md frontmatter `artifact_type` |
|
|
420
|
+
| `canonical_url` | `https://<app_deployments.url><surface.path>` |
|
|
421
|
+
| `surface_id` | surface name from `<!-- SURFACE:<id> -->` marker |
|
|
422
|
+
| `commit_sha` | HEAD SHA of the deploy |
|
|
423
|
+
| `published_at` | `now()` |
|
|
424
|
+
|
|
425
|
+
Use the Supabase MCP (`mcp__claude_ai_Supabase__execute_sql`) from the supervisor session:
|
|
426
|
+
|
|
427
|
+
```sql
|
|
428
|
+
INSERT INTO artifact_registry (entity_slug, artifact_type, canonical_url, surface_id, commit_sha, published_at)
|
|
429
|
+
VALUES ('<entity_slug>', '<artifact_type>', 'https://<url><path>', '<surface_id>', '<commit_sha>', now())
|
|
430
|
+
ON CONFLICT (entity_slug, surface_id) DO UPDATE SET
|
|
431
|
+
canonical_url = EXCLUDED.canonical_url,
|
|
432
|
+
commit_sha = EXCLUDED.commit_sha,
|
|
433
|
+
published_at = EXCLUDED.published_at;
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
If the INSERT fails, surface the failure in the deploy output but **do NOT roll back the deploy**. The artifact registry is a post-deploy record, not a deploy gate.
|
|
437
|
+
|
|
438
|
+
## Coolify Access — clauth + REST API
|
|
439
|
+
|
|
440
|
+
All Coolify operations use the clauth daemon and the Coolify REST API directly.
|
|
441
|
+
There is no Coolify MCP server. Do not reference `@masonator/coolify-mcp`.
|
|
442
|
+
|
|
443
|
+
```bash
|
|
444
|
+
# Get token (plain text — no JSON parsing needed)
|
|
445
|
+
_COOLIFY=$(curl -s http://127.0.0.1:52437/v/coolify-api)
|
|
446
|
+
|
|
447
|
+
# List applications
|
|
448
|
+
curl -s -H "Authorization: Bearer $_COOLIFY" \
|
|
449
|
+
"$DEPLOY_API_BASE/api/v1/applications"
|
|
450
|
+
|
|
451
|
+
# Get application details
|
|
452
|
+
curl -s -H "Authorization: Bearer $_COOLIFY" \
|
|
453
|
+
"$DEPLOY_API_BASE/api/v1/applications/<uuid>"
|
|
454
|
+
|
|
455
|
+
# Deploy (trigger) — correct endpoint is GET /api/v1/deploy?uuid=
|
|
456
|
+
# (POST /applications/<uuid>/deploy returns {"message":"Not found."})
|
|
457
|
+
curl -s -H "Authorization: Bearer $_COOLIFY" \
|
|
458
|
+
"$DEPLOY_API_BASE/api/v1/deploy?uuid=<uuid>&force=true"
|
|
459
|
+
|
|
460
|
+
# Get deployment logs
|
|
461
|
+
curl -s -H "Authorization: Bearer $_COOLIFY" \
|
|
462
|
+
"$DEPLOY_API_BASE/api/v1/deployments/<deployment-id>"
|
|
463
|
+
|
|
464
|
+
# Set env var
|
|
465
|
+
curl -s -X POST -H "Authorization: Bearer $_COOLIFY" \
|
|
466
|
+
-H "Content-Type: application/json" \
|
|
467
|
+
-d '{"key":"<KEY>","value":"<VALUE>"}' \
|
|
468
|
+
"$DEPLOY_API_BASE/api/v1/applications/<uuid>/envs"
|
|
469
|
+
|
|
470
|
+
# Set watch_paths
|
|
471
|
+
curl -s -X PATCH -H "Authorization: Bearer $_COOLIFY" \
|
|
472
|
+
-H "Content-Type: application/json" \
|
|
473
|
+
-d '{"watch_paths":"apps/<name>/**\npackages/**"}' \
|
|
474
|
+
"$DEPLOY_API_BASE/api/v1/applications/<uuid>"
|
|
475
|
+
|
|
476
|
+
# Change the app's domain — the writable field is "domains", NOT "fqdn"
|
|
477
|
+
# (Coolify v4 PATCH rejects {"fqdn":...} with "This field is not allowed"; fqdn is read-only/derived)
|
|
478
|
+
curl -s -X PATCH -H "Authorization: Bearer $_COOLIFY" \
|
|
479
|
+
-H "Content-Type: application/json" \
|
|
480
|
+
-d '{"domains":"https://<host>"}' \
|
|
481
|
+
"$DEPLOY_API_BASE/api/v1/applications/<uuid>"
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
**Domain change / namespace migration** (lesson 2026-06-13-deploy-media-manager-namespace-migration):
|
|
485
|
+
PATCH `applications/<uuid>` with `{"domains":"https://<host>"}` — never `fqdn`.
|
|
486
|
+
For an app NEW to main (first prod deploy) also bring the app + a lockfile importer
|
|
487
|
+
to main (clean worktree off origin/main, `pnpm install --lockfile-only`, verify the
|
|
488
|
+
diff = additions for `apps/<name>:` only); resolve the lockfile-guard vs scope-guard
|
|
489
|
+
collision by committing the app+lockfile as a SINGLE `chore(infra):`-subject commit
|
|
490
|
+
(the scope-guard's documented escape hatch) so both pre-commit guards pass.
|
|
491
|
+
|
|
492
|
+
**Never print `$_COOLIFY` to stdout.** Inline from clauth only — do not assign raw strings.
|
|
493
|
+
|
|
494
|
+
If clauth daemon is not responding (`curl -s http://127.0.0.1:52437/ping` fails):
|
|
495
|
+
```
|
|
496
|
+
BLOCKED: credential provider is not responding.
|
|
497
|
+
Fix: start the project's credential provider or configure deployment credentials through env vars, then retry.
|
|
498
|
+
I cannot proceed until this is resolved.
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
## Deployment Event Log — `coolify_events`
|
|
502
|
+
|
|
503
|
+
Every Coolify deploy emits a webhook → `coolify_events` row. Use this for last-N-deploys queries, debugging failed deploys, and reconciling local state with Coolify state.
|
|
504
|
+
|
|
505
|
+
Query the last 5 events for an app:
|
|
506
|
+
```sql
|
|
507
|
+
SELECT created_at, event_type, status, branch, commit_hash, duration_seconds
|
|
508
|
+
FROM coolify_events
|
|
509
|
+
WHERE app_uuid = '<uuid>' OR app_name = '<slug>'
|
|
510
|
+
ORDER BY created_at DESC LIMIT 5;
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
Fields:
|
|
514
|
+
- `app_uuid` — Coolify application UUID (matches `app_deployments.coolify_uuid`)
|
|
515
|
+
- `event_type` — `started | succeeded | failed | cancelled` (canonical values; consult webhook receiver for full enum)
|
|
516
|
+
- `status` — overall deploy status
|
|
517
|
+
- `branch`, `commit_hash`, `commit_message` — git context
|
|
518
|
+
- `duration_seconds` — total deploy time
|
|
519
|
+
- `payload` — full webhook payload (jsonb) for forensic debugging
|
|
520
|
+
|
|
521
|
+
When diagnosing a broken deploy in Mode 3: query `coolify_events` FIRST before re-running the deploy — the most recent event row tells you whether the previous deploy actually triggered, whether it failed, and how long it ran. Faster than checking the Coolify UI.
|
|
522
|
+
|
|
523
|
+
## Capture lessons (exit step)
|
|
524
|
+
|
|
525
|
+
Before the final verdict line, follow `.rdc/guides/lessons-learned-spec.md` § Capture procedure. If this run taught something non-obvious — a first root-cause theory that turned out wrong, the documented/standard path not working, a missing gate or check that cost a round, or a surprising tool/infra behavior — write one `.rdc/lessons/<YYYY-MM-DD>-deploy-<short-slug>.md` per lesson using the schema in that spec. Set `scope` (`simple` | `architectural`) and `status` (`open`, or `applied` if you shipped the fix in this same run, with the commit linked). Commit the lesson file(s) on `develop` alongside the run's other commits, and note "N lessons captured" in your verdict/summary. A run that taught nothing writes nothing — absence is the default.
|
|
526
|
+
|
|
527
|
+
## References
|
|
528
|
+
|
|
529
|
+
- Type-specific checklists + DNS tree + gate commands: `docs/runbooks/coolify-deploy-checklist.md`
|
|
530
|
+
- Rules / registry RPCs / hard limits: `.claude/context/coolify-deployment.md`
|
|
531
|
+
- Infrastructure constants:
|
|
532
|
+
```
|
|
533
|
+
Server UUID: ih386anenvvvn6fy1umtyow0
|
|
534
|
+
Server IP: 64.237.54.189
|
|
535
|
+
Dashboard: <deployment-dashboard-url>
|
|
536
|
+
GitHub App UUID: xdmcy60putp5h9j7k4kwg9c3
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
## Supersedes
|
|
540
|
+
|
|
541
|
+
`coolify-deploy` standalone skill (kept for back-compat; new work uses `rdc:deploy`).
|