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
68 changes: 68 additions & 0 deletions .github/workflows/supabase-production-cron-install.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
name: Install inactive Supabase production Cron jobs

on:
workflow_dispatch:
inputs:
production_deploy_run_id:
description: 'Successful production API/worker deploy run ID for this main commit'
required: true
type: string

permissions:
contents: read
actions: read

concurrency:
group: supabase-production-cron-install
cancel-in-progress: false

jobs:
install:
if: github.ref == 'refs/heads/main' && github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
timeout-minutes: 15
environment: production-schema
env:
PRODUCTION_PROJECT_REF: ${{ vars.SUPABASE_PRODUCTION_PROJECT_REF }}
STAGING_PROJECT_REF: ${{ vars.SUPABASE_STAGING_PROJECT_REF }}
PRODUCTION_SCHEMA_DIRECT_URL: ${{ secrets.PRODUCTION_SCHEMA_DIRECT_URL }}
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: Require the current main commit and reviewed project references
shell: bash
run: |
set -euo pipefail
test "$(git ls-remote origin refs/heads/main | cut -f1)" = "$GITHUB_SHA"
test "$PRODUCTION_PROJECT_REF" = 'egdszjoesxlccauahmsx'
test "$STAGING_PROJECT_REF" = 'zhrulokgngjceqgjkcmw'
test -n "$PRODUCTION_SCHEMA_DIRECT_URL"
- name: Require a successful API and worker deployment for this commit
env:
GH_TOKEN: ${{ github.token }}
DEPLOY_RUN_ID: ${{ inputs.production_deploy_run_id }}
shell: bash
run: |
set -euo pipefail
[[ "$DEPLOY_RUN_ID" =~ ^[0-9]+$ ]]
gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${DEPLOY_RUN_ID}" \
--jq '[.path, .head_branch, .head_sha, .event, .conclusion] | @tsv' |
awk -v sha="$GITHUB_SHA" '$1 == ".github/workflows/supabase-production-deploy.yml" && $2 == "main" && $3 == sha && $4 == "workflow_dispatch" && $5 == "success" { found=1 } END { exit !found }'
- name: Install PostgreSQL client
run: sudo apt-get update && sudo apt-get install -y postgresql-client
- uses: actions/setup-python@v7
with:
python-version: '3.12'
- name: Check production connection and worker schema without writes
working-directory: backend
run: python -m tools.install_production_cron
- name: Require main to remain at the reviewed commit before SQL
shell: bash
run: test "$(git ls-remote origin refs/heads/main | cut -f1)" = "$GITHUB_SHA"
- name: Install and verify eight inactive jobs
working-directory: backend
run: python -m tools.install_production_cron --install
- name: Require main to remain at the installed commit
shell: bash
run: test "$(git ls-remote origin refs/heads/main | cut -f1)" = "$GITHUB_SHA"
104 changes: 104 additions & 0 deletions .github/workflows/supabase-production-deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
name: Deploy Supabase API and worker to production

on:
workflow_dispatch:

permissions:
contents: read

concurrency:
group: supabase-production-deploy
cancel-in-progress: false

jobs:
deploy:
if: github.ref == 'refs/heads/main' && github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
timeout-minutes: 25
# The existing protected production environment requires an operator review.
environment: production-schema
env:
SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_PRODUCTION_ACCESS_TOKEN }}
PRODUCTION_REF: ${{ vars.SUPABASE_PRODUCTION_PROJECT_REF }}
STAGING_REF: ${{ vars.SUPABASE_STAGING_PROJECT_REF }}
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: Require the current main commit and production target
shell: bash
run: |
set -euo pipefail
test "$(git ls-remote origin refs/heads/main | cut -f1)" = "$GITHUB_SHA"
if [ "$PRODUCTION_REF" != 'egdszjoesxlccauahmsx' ] || \
[ -z "$STAGING_REF" ] || [ "$STAGING_REF" = "$PRODUCTION_REF" ] || \
[ -z "$SUPABASE_ACCESS_TOKEN" ]; then
echo '::error::A production-scoped token and distinct, reviewed project refs are required.'
exit 1
fi
- uses: actions/setup-python@v7
with:
python-version: '3.12'
- name: Install schema verifier
run: python -m pip install --requirement backend/requirements.txt
- name: Verify production schema before deployment
working-directory: backend
env:
PRODUCTION_PROJECT_REF: ${{ vars.SUPABASE_PRODUCTION_PROJECT_REF }}
STAGING_PROJECT_REF: ${{ vars.SUPABASE_STAGING_PROJECT_REF }}
PRODUCTION_SCHEMA_DIRECT_URL: ${{ secrets.PRODUCTION_SCHEMA_DIRECT_URL }}
DIRECT_URL: ${{ secrets.PRODUCTION_SCHEMA_DIRECT_URL }}
run: |
python -c 'from tools.apply_production_cutover import target_environment; target_environment(); print("production_connection=validated")'
python -m app.workers.schema_check
python -m tools.verify_supabase_cutover_schema
- uses: supabase/setup-cli@3c2f5e2ae34c34e428e8e206e2c4d21fa2d20fbf # v2.1.1
with:
version: latest
- name: Deploy API from this main commit
run: supabase functions deploy api --project-ref "$PRODUCTION_REF"
- name: Deploy worker from this main commit without activating Cron
run: supabase functions deploy worker --project-ref "$PRODUCTION_REF"
- name: Check worker boot and unsigned dispatch rejection
env:
WORKER_URL: https://${{ vars.SUPABASE_PRODUCTION_PROJECT_REF }}.supabase.co/functions/v1/worker
run: |
python - <<'PY'
import os
import urllib.error
import urllib.request

request = urllib.request.Request(
os.environ['WORKER_URL'], data=b'{}', method='POST',
headers={'Content-Type': 'application/json'},
)
try:
with urllib.request.urlopen(request, timeout=20) as response:
status, body = response.status, response.read(128)
except urllib.error.HTTPError as error:
status, body = error.code, error.read(128)
except Exception:
status, body = 0, b''
ok = status == 401 and body == b'{"error":"invalid_dispatch"}'
print('production_worker_boot=' + ('ready' if ok else 'failed'))
raise SystemExit(0 if ok else 1)
PY
- name: Check production API health
env:
HEALTH_URL: https://${{ vars.SUPABASE_PRODUCTION_PROJECT_REF }}.supabase.co/functions/v1/api/health
run: |
python - <<'PY'
import os
import urllib.request

try:
with urllib.request.urlopen(os.environ['HEALTH_URL'], timeout=20) as response:
ok = response.status == 200
except Exception:
ok = False
print('production_api_health=' + ('ready' if ok else 'failed'))
raise SystemExit(0 if ok else 1)
PY
- name: Require main to remain at the deployed commit
shell: bash
run: test "$(git ls-remote origin refs/heads/main | cut -f1)" = "$GITHUB_SHA"
2 changes: 1 addition & 1 deletion docs/CODEBASE_MAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ learned history, streaks, likes, saved concepts, and push reminders.
| Content lifecycle | `docs/CONTENT_ARCHITECTURE.md`, `docs/CONTENT_OPERATIONS.md`; portable subject/curriculum imports, durable refill, reviewed publication, daily review, protected health report. |
| Content engine | `backend/app/services/generation.py`, `pool.py`, `prefetch.py`; Gemini lessons from a curated backlog. |
| Operations | `.github/workflows/`, `backend/railway.json`, `backend/Dockerfile`, `mobile/eas.json`, `mobile/app.config.js`. |
| Hosting migration | `docs/RAILWAY_MIGRATION.md`; source/destination evidence, worker handover, EAS endpoint publication, recovery redirects and retirement. New Railway services use dashboard settings because legacy Config as Code is deprecated. |
| Hosting migration | `docs/RAILWAY_MIGRATION.md`; source/destination evidence, worker handover, EAS endpoint publication, recovery redirects and retirement. New Railway services use dashboard settings because legacy Config as Code is deprecated. `.github/workflows/supabase-production-deploy.yml` and `supabase-production-cron-install.yml` are registered here on the default branch for manual, protected runs against current `main`; they are not triggered by merging `develop`. |
| Supabase scheduled workers | `supabase/functions/worker/` validates signed Cron calls; migrations 0119–0120 own the disabled run ledger and HTTP reconciliation; migrations 0128–0129 feed owner reporting and bound run history; migration 0130 fences uncertain Gmail delivery; `backend/ops/supabase_workers/` installs inactive platform Cron jobs. See `docs/SUPABASE_WORKER_MIGRATION.md` for staged handover. |
| Documentation | Root `README.md`, `RELEASING.md`, `CONTRIBUTING.md`, `docs/ARCHITECTURE.md`, `docs/ROADMAP.md`, and the backend/mobile guides. |
| Agent guidance | Root `AGENTS.md`; `mobile/AGENTS.md` adds Expo documentation requirements and `mobile/CLAUDE.md` references it. |
Expand Down
6 changes: 6 additions & 0 deletions docs/WORK_LOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ claims as completed work.

## Current status

### Register protected production workflows on the default branch — 2026-10-21

- After release PR #434 merged into `main` at `abc2dcf`, the new public Netlify pages returned HTML with the expected security headers while their API setting still pointed to Railway. GitHub did not list the new production deploy or inactive-Cron workflow because the repository default branch is `develop` and those workflow files existed only on `main`. GitHub requires a `workflow_dispatch` file on the default branch even when the run targets another ref.
- Branch `codex/418-register-production-workflows` copies only the two exact reviewed workflow files from merged `main` into `develop` in focused commits `11acfa6` and `c0f6a15`. Both workflow jobs run only for current `main` and use the protected `production-schema` environment; a dispatch against `develop` skips the job. This default-branch registration does not deploy a function, install Cron, publish an OTA, or change Railway. A separate PR and its normal review/merge gate are required before dispatching the production deploy on `main`.
- PR #435 review at `de78e85`: both workflow files have identical SHA-256 digests to merged `main`, parse as manual workflows, and retain the `main` job guard and protected environment. The production repository is public, so the current-`main` `git ls-remote` checks work with checkout credentials disabled. All three hosted quality checks passed and GitHub reported the PR mergeable. GitHub's automated Codex review could not complete because its review quota was reached; the manual review found no actionable defect. No production workflow has been dispatched.

### Supabase scheduled worker migration — issue #417, 2026-10-18

- Branch `codex/supabase-cron-workers` starts from `develop` at `69b01c5`. Scope is one feature PR for all Railway scheduled/background work, with production Supabase schedules disabled until the step-3 cutover. The owner confirmed Railway production runs `reminders` every 15 minutes and `pool-topup` at 22:00 UTC; source is `main`. On October 20, the owner showed three green recent runs of each service: reminders sent zero tokens and processed zero editorial emails, while pool-topup reported generation disabled and empty revision/enrichment work. Both deployment panels identify release PR #414, whose merge commit is `c244088aa285127a3f065cc1d7e41afdae8bf36f`; Railway's full deployed SHA and some effective non-secret flags remain to be checked. The owner later showed `reminders` with `GEMINI_MODEL=gemini-3.1-flash-lite` and `GENERATION_DAILY_CALL_CAP=200`.
Expand Down
Loading