Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 19 additions & 11 deletions docs/superpowers/plans/2026-08-11-ci-gated-coolify-deploy.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ pre-correction code snippets must not be copied or executed; the merged files on
correction that must be merged before Task 4 begins. Tasks 4–6 are the active
merge/cutover runbook and incorporate the corrected contract directly:

- fixed webhook: `https://panel.codevena.dev/source/github/events/manual`;
- fixed webhook: `https://panel.codevena.dev/webhooks/source/github/events/manual`;
- variables: add `COOLIFY_WEBHOOK_URL`;
- secrets: use `COOLIFY_READ_TOKEN` and `COOLIFY_WEBHOOK_SECRET`, not
`COOLIFY_API_TOKEN`;
Expand Down Expand Up @@ -1722,15 +1722,17 @@ In Cloudflare Zero Trust:

1. create service token `GitHub Actions - AgoraHub Coolify deploy` expiring
2027-08-11;
2. create three more-specific self-hosted Access applications: the exact manual
webhook path `panel.codevena.dev/source/github/events/manual`, the exact path
2. create four more-specific self-hosted Access applications: the exact manual
webhook path `panel.codevena.dev/webhooks/source/github/events/manual`, the exact path
for
`panel.codevena.dev/api/v1/applications/z5eij4n8c4ubvxsmtpb507p2`
(pre-queue branch verification), and
`panel.codevena.dev/api/v1/deployments/*` (polling); do not expose
`/api/v1/deploy` or any broader `/api/v1/*` path;
3. add a Service Auth policy to all three applications whose include rule is only
that service token;
`panel.codevena.dev/api/v1/deployments/*` (polling), plus an exact
`panel.codevena.dev/api/v1/deploy` application; do not expose any broader
`/api/v1/*` path;
3. add a Service Auth policy to the three automation applications whose include
rule is only that service token; give the exact `/api/v1/deploy` application
only an Everyone/Block policy;
4. verify an anonymous request to each application is denied by Cloudflare
Access with an expected `3xx`, `401`, or `403`; retain enough response
metadata to prove the denial came from Access and that no Coolify response
Expand All @@ -1753,11 +1755,17 @@ Do not expose either one-time value in tool output or terminal history.
- [ ] **Step 4: Create separate least-privilege Coolify credentials**

On the AgoraHub application, create a high-entropy
`manual_webhook_secret_github` value. In Coolify API Tokens, create
`manual_webhook_secret_github` value. Because Coolify hides this field while an
official GitHub App is connected, create a temporary `read + write` API token,
PATCH only that application field over the internal `ssh dedicated` /
`localhost:8000` path, store the same value in GitHub, then revoke the temporary
token and prove it returns `401`; never grant it `deploy`, `root`, or
`read:sensitive`. In Coolify API Tokens, create
`GitHub Actions - AgoraHub read` with exactly `read`. Verify the UI lists `read`
and neither `deploy`, `write`, nor `root`. Transfer both values only through the
authenticated browser into GitHub's secret form; never place either value in a
shell argument, file, log, clipboard history, or Brain note.
controlled one-time handoff; never place either value in a shell argument, log,
clipboard history, or Brain note. Any temporary local handoff file must be mode
`0600` inside a mode-`0700` directory and securely removed after revocation.

- [ ] **Step 5: Transfer API credentials directly into GitHub and set variables**

Expand All @@ -1773,7 +1781,7 @@ Set variables:

```bash
gh variable set COOLIFY_API_BASE_URL --repo Codevena/AgoraHub --body https://panel.codevena.dev/api/v1
gh variable set COOLIFY_WEBHOOK_URL --repo Codevena/AgoraHub --body https://panel.codevena.dev/source/github/events/manual
gh variable set COOLIFY_WEBHOOK_URL --repo Codevena/AgoraHub --body https://panel.codevena.dev/webhooks/source/github/events/manual
gh variable set COOLIFY_APP_UUID --repo Codevena/AgoraHub --body z5eij4n8c4ubvxsmtpb507p2
gh variable set COOLIFY_DEPLOY_BRANCH --repo Codevena/AgoraHub --body coolify-production
```
Expand Down
23 changes: 13 additions & 10 deletions docs/superpowers/specs/2026-08-11-ci-gated-coolify-deploy-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ The correction below is incorporated throughout this document. The direct
the live Coolify application UUID, branch, GitHub repository, and an explicit
empty `watch_paths` value (`null` or blank string; omission fails closed);
- it POSTs the exact-SHA payload to the fixed
`https://panel.codevena.dev/source/github/events/manual` endpoint with
`https://panel.codevena.dev/webhooks/source/github/events/manual` endpoint with
`X-GitHub-Event: push` and `X-Hub-Signature-256`;
- the webhook receives the two Cloudflare Access headers and the HMAC signature,
but no Coolify bearer token;
Expand Down Expand Up @@ -155,15 +155,18 @@ boundary.

### Two-layer API protection

Cloudflare Access gets three path-scoped self-hosted applications: the exact
manual webhook path `panel.codevena.dev/source/github/events/manual`, the exact
application path
Cloudflare Access gets four path-scoped self-hosted applications. Three allow
only the dedicated Service Auth token: the exact manual webhook path
`panel.codevena.dev/webhooks/source/github/events/manual`, the exact application path
`panel.codevena.dev/api/v1/applications/z5eij4n8c4ubvxsmtpb507p2`, and
`panel.codevena.dev/api/v1/deployments/*` for status polling. None grants the
service token broader `/api/v1/*` access or exposes `/api/v1/deploy`. Their only
automation policy is Service Auth bound to a new token named
`GitHub Actions - AgoraHub Coolify deploy`. The existing human login policy for
the rest of `panel.codevena.dev` stays unchanged.
`panel.codevena.dev/api/v1/deployments/*` for status polling. A fourth exact
application for `panel.codevena.dev/api/v1/deploy` has only an Everyone/Block
policy, preventing API-token deploys from bypassing CI at the public edge. None
grants the service token broader `/api/v1/*` access. The three automation paths
use Service Auth bound to a new token named `GitHub Actions - AgoraHub Coolify
deploy`. The existing human login policy for the rest of `panel.codevena.dev`
stays unchanged; emergency API access remains possible only internally over the
documented SSH path.

The AgoraHub application gets a new high-entropy
`manual_webhook_secret_github`; the same one-time value is stored in GitHub as
Expand All @@ -178,7 +181,7 @@ The GitHub repository stores:
|---|---|---|
| Variable | `COOLIFY_CD_ENABLED` | `false` during cutover, then permanently `true` |
| Variable | `COOLIFY_API_BASE_URL` | `https://panel.codevena.dev/api/v1` |
| Variable | `COOLIFY_WEBHOOK_URL` | `https://panel.codevena.dev/source/github/events/manual` |
| Variable | `COOLIFY_WEBHOOK_URL` | `https://panel.codevena.dev/webhooks/source/github/events/manual` |
| Variable | `COOLIFY_APP_UUID` | `z5eij4n8c4ubvxsmtpb507p2` |
| Variable | `COOLIFY_DEPLOY_BRANCH` | `coolify-production` |
| Secret | `COOLIFY_READ_TOKEN` | new Coolify read-only token |
Expand Down
32 changes: 26 additions & 6 deletions scripts/deploy-coolify.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ function enabledConfig(overrides: Partial<EnabledDeployConfig> = {}): EnabledDep
githubRepository: "Codevena/AgoraHub",
githubToken: "github-token",
coolifyApiBaseUrl: "https://panel.codevena.dev/api/v1",
coolifyWebhookUrl: "https://panel.codevena.dev/source/github/events/manual",
coolifyWebhookUrl: "https://panel.codevena.dev/webhooks/source/github/events/manual",
coolifyAppUuid: "z5eij4n8c4ubvxsmtpb507p2",
coolifyDeployBranch: "coolify-production",
coolifyReadToken: "coolify-read-token",
Expand Down Expand Up @@ -61,14 +61,34 @@ describe("readDeployConfig", () => {
expect(readDeployConfig({ COOLIFY_CD_ENABLED: "false" })).toEqual({ enabled: false });
});

it("accepts the deployed Coolify manual webhook route", () => {
expect(readDeployConfig({
COOLIFY_CD_ENABLED: "true",
VERIFIED_SHA: SHA,
GITHUB_REPOSITORY: "Codevena/AgoraHub",
GITHUB_TOKEN: "github-token",
COOLIFY_API_BASE_URL: "https://panel.codevena.dev/api/v1",
COOLIFY_WEBHOOK_URL: "https://panel.codevena.dev/webhooks/source/github/events/manual",
COOLIFY_APP_UUID: "z5eij4n8c4ubvxsmtpb507p2",
COOLIFY_DEPLOY_BRANCH: "coolify-production",
COOLIFY_READ_TOKEN: "coolify-read-token",
COOLIFY_WEBHOOK_SECRET: "webhook-secret",
CF_ACCESS_CLIENT_ID: "access-id",
CF_ACCESS_CLIENT_SECRET: "access-secret",
})).toMatchObject({
enabled: true,
coolifyWebhookUrl: "https://panel.codevena.dev/webhooks/source/github/events/manual",
});
});

it("rejects a non-SHA verified revision before networking", () => {
expect(() => readDeployConfig({
COOLIFY_CD_ENABLED: "true",
VERIFIED_SHA: "main",
GITHUB_REPOSITORY: "Codevena/AgoraHub",
GITHUB_TOKEN: "github-token",
COOLIFY_API_BASE_URL: "https://panel.codevena.dev/api/v1",
COOLIFY_WEBHOOK_URL: "https://panel.codevena.dev/source/github/events/manual",
COOLIFY_WEBHOOK_URL: "https://panel.codevena.dev/webhooks/source/github/events/manual",
COOLIFY_APP_UUID: "z5eij4n8c4ubvxsmtpb507p2",
COOLIFY_DEPLOY_BRANCH: "coolify-production",
COOLIFY_READ_TOKEN: "coolify-read-token",
Expand Down Expand Up @@ -97,7 +117,7 @@ describe("readDeployConfig", () => {
GITHUB_REPOSITORY: "Codevena/AgoraHub",
GITHUB_TOKEN: "github-token",
COOLIFY_API_BASE_URL: "https://panel.codevena.dev/api/v1",
COOLIFY_WEBHOOK_URL: "https://panel.codevena.dev/source/github/events/manual",
COOLIFY_WEBHOOK_URL: "https://panel.codevena.dev/webhooks/source/github/events/manual",
COOLIFY_APP_UUID: "z5eij4n8c4ubvxsmtpb507p2",
COOLIFY_DEPLOY_BRANCH: "coolify-production",
COOLIFY_READ_TOKEN: "coolify-read-token",
Expand All @@ -114,7 +134,7 @@ describe("readDeployConfig", () => {
it.each([
["GITHUB_REPOSITORY", "someone/else"],
["COOLIFY_API_BASE_URL", "https://evil.example/api/v1"],
["COOLIFY_WEBHOOK_URL", "https://evil.example/source/github/events/manual"],
["COOLIFY_WEBHOOK_URL", "https://evil.example/webhooks/source/github/events/manual"],
["COOLIFY_APP_UUID", "anotherapp"],
["COOLIFY_DEPLOY_BRANCH", "main"],
])("rejects drifted approved target %s before networking", (name, value) => {
Expand All @@ -124,7 +144,7 @@ describe("readDeployConfig", () => {
GITHUB_REPOSITORY: "Codevena/AgoraHub",
GITHUB_TOKEN: "github-app-token",
COOLIFY_API_BASE_URL: "https://panel.codevena.dev/api/v1",
COOLIFY_WEBHOOK_URL: "https://panel.codevena.dev/source/github/events/manual",
COOLIFY_WEBHOOK_URL: "https://panel.codevena.dev/webhooks/source/github/events/manual",
COOLIFY_APP_UUID: "z5eij4n8c4ubvxsmtpb507p2",
COOLIFY_DEPLOY_BRANCH: "coolify-production",
COOLIFY_READ_TOKEN: "coolify-read-token",
Expand Down Expand Up @@ -192,7 +212,7 @@ describe("runCoolifyDeployment", () => {
const webhookRequest = new Request(fetchImpl.mock.calls[5][0], fetchImpl.mock.calls[5][1]);
const webhookBody = await webhookRequest.text();
expect(webhookRequest.url).toBe(
"https://panel.codevena.dev/source/github/events/manual"
"https://panel.codevena.dev/webhooks/source/github/events/manual"
);
expect(webhookRequest.headers.get("authorization")).toBeNull();
expect(webhookRequest.headers.get("x-github-event")).toBe("push");
Expand Down
2 changes: 1 addition & 1 deletion scripts/deploy-coolify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ const SHA_PATTERN = /^[0-9a-f]{40}$/i;
const DEPLOYMENT_UUID_PATTERN = /^[a-z0-9_-]+$/i;
const APPROVED_REPOSITORY = "Codevena/AgoraHub";
const APPROVED_API_BASE_URL = "https://panel.codevena.dev/api/v1";
const APPROVED_WEBHOOK_URL = "https://panel.codevena.dev/source/github/events/manual";
const APPROVED_WEBHOOK_URL = "https://panel.codevena.dev/webhooks/source/github/events/manual";
const APPROVED_APP_UUID = "z5eij4n8c4ubvxsmtpb507p2";
const APPROVED_DEPLOY_BRANCH = "coolify-production";
const DEFAULT_DEADLINE_MS = 20 * 60 * 1_000;
Expand Down
Loading