From f1573f4906065a3c60fc4770a70dcea988f10da1 Mon Sep 17 00:00:00 2001 From: jjbrunton Date: Mon, 31 Aug 2026 17:56:41 +0100 Subject: [PATCH 1/3] fix(admin): keep admin app in the docker build context The root .dockerignore excluded apps/admin, so the monorepo build context dropped that workspace entirely: npm ci skipped it and `npm run build -w @sauci/admin` failed with "No workspaces found". The ignore file was authored for the mcp image and later extended with re-includes for web; admin never got the same treatment. Re-include apps/admin, and re-exclude node_modules/dist last so the re-includes cannot pull host build artefacts into any image context. Verified with `docker build -f apps/admin/Dockerfile .`. Co-Authored-By: Claude Opus 5 --- .dockerignore | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.dockerignore b/.dockerignore index f6e50f1..cb7f5a2 100644 --- a/.dockerignore +++ b/.dockerignore @@ -39,3 +39,11 @@ terraform .claude .codex .ralph + +# The admin app builds from this same monorepo context (apps/admin/Dockerfile). +!apps/admin +!apps/admin/** + +# Host build artefacts must never leak into any image context. +**/node_modules +**/dist From d14869d7458d2faaa99814e37c3f9ef474cdb768 Mon Sep 17 00:00:00 2001 From: jjbrunton Date: Mon, 31 Aug 2026 18:03:34 +0100 Subject: [PATCH 2/3] ci(branch-policy): allow hotfix and release branches into main The policy accepted only `staging` as a head for main, so any change production needed before the next promotion had nowhere to go: the PR was unmergeable by CI and branch protection blocks direct pushes. Accept `hotfix/*` and `release/*` heads as well. The naming requirement keeps a direct-to-main change deliberate and visible, and the master freeze plus the "features target staging" rule are unchanged. Document the escape hatch and the need to land the same commit on staging. Co-Authored-By: Claude Opus 5 --- .github/workflows/verify.yml | 11 ++++++++--- docs/operations/backend-deployments.md | 12 ++++++++---- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index f870058..3fa551a 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -24,9 +24,14 @@ jobs: echo "master is frozen; target staging or main" >&2 exit 1 fi - if [ "$BASE_BRANCH" = "main" ] && [ "$HEAD_BRANCH" != "staging" ]; then - echo "Only staging may be promoted to main" >&2 - exit 1 + if [ "$BASE_BRANCH" = "main" ]; then + case "$HEAD_BRANCH" in + staging|hotfix/*|release/*) ;; + *) + echo "main takes staging promotions, or a hotfix/* or release/* branch for direct work" >&2 + exit 1 + ;; + esac fi if [ "$BASE_BRANCH" != "staging" ] && [ "$BASE_BRANCH" != "main" ]; then echo "Feature pull requests target staging; promotions target main" >&2 diff --git a/docs/operations/backend-deployments.md b/docs/operations/backend-deployments.md index 2ccb482..dab7dbb 100644 --- a/docs/operations/backend-deployments.md +++ b/docs/operations/backend-deployments.md @@ -14,10 +14,14 @@ not deploy the retired Supabase data plane. | `master` | frozen legacy default | none | none | Normal feature pull requests target `staging`. After CI and authenticated staging -acceptance pass, open a promotion pull request from `staging` to `main`. Do not -add production-only commits to `main`: production should run the exact commit -already exercised in staging. Direct pushes to either deployment branch are for -explicit incident recovery only. +acceptance pass, open a promotion pull request from `staging` to `main`. Prefer +that route: production then runs the exact commit already exercised in staging. + +When production needs a change that cannot wait for the next promotion, open a +pull request into `main` from a `hotfix/*` or `release/*` branch. CI enforces +that naming so a direct-to-main change is always deliberate and visible. Land +the same commit on `staging` too, or the next promotion will revert it. Direct +pushes to either deployment branch remain blocked by branch protection. Each Dokploy Compose application is connected to `jjbrunton/Sauci`, watches its single branch with push-triggered automatic deployment, and uses From 3a019e62d3c87048710b8f595d95acd8c21fe2ef Mon Sep 17 00:00:00 2001 From: jjbrunton Date: Mon, 31 Aug 2026 18:17:10 +0100 Subject: [PATCH 3/3] fix(api): allow the admin console origin through CORS The production allowlist held only https://sauci.app, so every /v1/admin/* preflight from https://manage.sauci.app returned 204 with no Access-Control-Allow-Origin and the browser blocked the request. Login then reported "Access denied. You are not an administrator." because the admin app treats any /v1/admin/me failure as a permissions failure. Add the admin console origin and record in the README that new admin hosts belong in the Compose contract rather than a Dokploy override. Co-Authored-By: Claude Opus 5 --- infra/backend/README.md | 9 ++++++--- infra/backend/compose.production.yaml | 3 ++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/infra/backend/README.md b/infra/backend/README.md index 23ba6cd..d3d8daf 100644 --- a/infra/backend/README.md +++ b/infra/backend/README.md @@ -87,9 +87,12 @@ Before the first deployment, provide: 5. A rollback and restore point before any migrated production data is imported. 6. A separately generated `MEDIA_SIGNING_SECRET` (at least 32 characters) and the HTTPS API origin as `MEDIA_PUBLIC_BASE_URL`. -7. The public web origin is fixed to `https://sauci.app` by - `CORS_ALLOWED_ORIGINS` in the Compose contract. Do not broaden it without a - reviewed browser client requirement. +7. Browser origins are fixed by `CORS_ALLOWED_ORIGINS` in the Compose contract: + `https://sauci.app` (public web) and `https://manage.sauci.app` (admin + console, which calls `/v1/admin/*` with a bearer token). Do not broaden the + list without a reviewed browser client requirement, and add any new admin + host here rather than as a Dokploy override, so the allowlist stays + reviewable in git. 8. A non-production hosted Auth project and disposable, onboarding-complete test identities mapped into the staging database for authenticated mobile acceptance. diff --git a/infra/backend/compose.production.yaml b/infra/backend/compose.production.yaml index bc29ed9..ee1ae4a 100644 --- a/infra/backend/compose.production.yaml +++ b/infra/backend/compose.production.yaml @@ -8,7 +8,8 @@ x-common-environment: &common-environment x-api-environment: &api-environment <<: *common-environment PORT: 3003 - CORS_ALLOWED_ORIGINS: https://sauci.app + # https://manage.sauci.app is the admin console; it calls /v1/admin/* from the browser. + CORS_ALLOWED_ORIGINS: https://sauci.app,https://manage.sauci.app DATABASE_POOL_MAX: ${API_DATABASE_POOL_MAX:-10} DATABASE_POOL_IDLE_TIMEOUT_MS: ${DATABASE_POOL_IDLE_TIMEOUT_MS:-30000} DATABASE_POOL_CONNECTION_TIMEOUT_MS: ${DATABASE_POOL_CONNECTION_TIMEOUT_MS:-10000}