Repo & Pipeline Setup¶
Already completed. This was done during initial CampusCore setup. You only need this if you're re-creating the GitHub repo or changing the admin account.
How the GitHub repository's repo-level secrets and variables are configured. These are shared across all client environments.
Prerequisites¶
- Admin role ARN from Admin Account Setup
Set Repo-Level Secrets¶
# Admin role ARN - allows the deploy workflow to authenticate via OIDC
gh secret set ADMIN_AWS_ROLE_ARN --body "arn:aws:iam::774222656146:role/CampusCore-Admin-Role"
# Initial superuser - created automatically on first deploy for each client
# This is always the CampusCore team's admin account, shared across all environments
gh secret set DJANGO_SUPERUSER_EMAIL --body "admin@campus-core.ai"
gh secret set DJANGO_SUPERUSER_PASSWORD --body "<strong-password>"
Set Repo-Level Variables¶
# Route53 hosted zone ID for campuscoreai.com - shared across all environments
gh variable set CAMPUSCORE_HOSTED_ZONE_ID --body "Z03938461UVED2L50LENJ"
Main Branch Rules¶
main is guarded by one repository ruleset, main-required-checks (id 19521735).
Three status checks are required, all of them jobs of .github/workflows/pr-validation.yml: Frontend (typecheck, lint, test, build), validate, and Migration graph and model state.
Merging is a squash merge, one per pull request.
A pull request is checked against main as of the moment its run started, and GitHub does not re-run it when main moves.
Two branches that each add a migration on the same parent are therefore both green, and the second one to land breaks migrate for everyone.
Migration graph and model state catches that shape whenever the run is fresh, and always catches a model edit with no migration, but it cannot see a sibling that merged after the run.
Closing that last gap needs a merge queue, which GitHub offers for private repositories only on Enterprise Cloud; this organization is on the Team plan, so a merge_queue rule on the ruleset is rejected with Invalid rule 'merge_queue'.
Until that changes, re-run the check on a pull request that has been open across someone else's merge before merging it.
Repository admins bypass the ruleset (bypass_actors: repository role 5, always).
The workflow has no paths filter, and must not get one: a run GitHub skips on a path filter never reports its checks, so the pull request sits at Pending and only the admin bypass can merge it.
That bites only a workflow whose jobs are required checks; infra-validation.yml and terraform-validate.yml filter on infrastructure/** at the workflow level and are fine, because nothing in them is required.
Path filtering happens inside the workflow instead.
A first job, changes, lists the pull request's files with dorny/paths-filter and each required job runs only when its area changed; a job skipped by its if: reports as a successful check, so a docs-only pull request goes green with nothing run.
Frontend keys on campuscore_app/web/**, Migration graph and model state on the rest of campuscore_app/, and validate runs unless every changed file is prose (docs/, docs-site/, work/, releases/, .claude/, root-level *.md) or SPA source under web/src/ other than the generated types, because validate owns the contract-drift checks.
Editing the workflow file runs all three.
If changes itself fails, all three run: a detection failure can never turn into a green run that checked nothing.
What the required checks gate¶
| Check | Step | What it gates |
|---|---|---|
| Frontend | pnpm typecheck |
The SPA compiles under strict TypeScript |
| Frontend | pnpm lint |
ESLint on the SPA |
| Frontend | pnpm test |
SPA unit tests |
| Frontend | pnpm test:browser |
SPA tests in real Chromium and WebKit, with touch and with a mouse |
| Frontend | pnpm build |
The Vite bundle builds, and emits no sourcemaps (collectstatic fails on them) |
validate |
uv lock --check |
uv.lock matches pyproject.toml; every image installs from the lock |
validate |
Committed tailwind.css matches a clean rebuild |
Deployments serve the committed stylesheet verbatim |
validate |
OpenAPI schema and generated TS types are in sync | A Pydantic contract change that was never regenerated would 422 at runtime |
validate |
Upload contract is in sync | The SPA's upload allowlist matches the server's file-format registry |
validate |
check_deploy_contract.py |
The typed Settings model reconciles with the ECS task definition and the Terraform outputs |
validate |
ruff check |
Python lint |
validate |
ty_ratchet.py |
No net-new type diagnostics beyond scripts/ty_baseline.json |
validate |
pytest apps/ campus_core/ |
The Python test suite |
| Migration graph and model state | check_migration_lock_timeout.py |
Every changed migration that runs DDL bounds its lock wait, so a deploy cannot hang behind a long-running reader |
| Migration graph and model state | makemigrations --check --dry-run --noinput |
The migration graph has one leaf and the models match the migrations, judged on the merge with main as of the run |
The Frontend job installs Chromium and WebKit before the browser tests, keeping the browser downloads in the Actions cache under the pinned playwright version.
On a developer machine, scripts/gate.sh runs the same tests in the browser_tests compose service on the official Playwright image, which is about 2.5 GB and is pulled by the first gate run on that machine.
Re-applying the ruleset¶
The ruleset is edited through the API rather than the settings UI, so the change is reviewable and repeatable.
A PUT replaces the whole ruleset, so build the body from the current one with jq instead of retyping the contexts and bypass actors.
gh api repos/CampusCoreAI/campuscore/rulesets/19521735 > /tmp/rs.json
jq '{name,target,enforcement,conditions,bypass_actors,rules}
| .rules = [.rules[] | if .type=="required_status_checks"
then .parameters.required_status_checks += [{"context":"<new check name>"}] else . end]' \
/tmp/rs.json > /tmp/rs_new.json
gh api -X PUT repos/CampusCoreAI/campuscore/rulesets/19521735 --input /tmp/rs_new.json
Add a required context only once the check has reported at least once on a real pull request. A context the repository has never produced blocks every open pull request on a check with no job to satisfy it.
How the Pipeline Uses These¶
The deploy workflow (.github/workflows/deploy-aws.yml) uses a two-role authentication model:
1. GitHub Actions authenticates via OIDC → assumes CampusCore-Admin-Role (admin account)
2. Initializes Terraform backend (reads/writes state in campuscore-tfstate-{client})
3. Chains into client's CampusCore-Deploy-Role via sts:AssumeRole
4. Provisions infrastructure in client's account (ECS, RDS, S3, etc.)
5. Returns to admin role for Terraform state writes
Repo-Level vs Environment-Level¶
| Level | Name | Type | Purpose |
|---|---|---|---|
| Repo | ADMIN_AWS_ROLE_ARN |
Secret | Our admin account's IAM role (OIDC auth) |
| Repo | DJANGO_SUPERUSER_EMAIL |
Secret | Initial admin email (created on first deploy) |
| Repo | DJANGO_SUPERUSER_PASSWORD |
Secret | Initial admin password |
| Repo | CAMPUSCORE_HOSTED_ZONE_ID |
Variable | Route53 hosted zone ID for campuscoreai.com |
| Environment | AWS_ROLE_ARN |
Variable | Client's deploy role ARN (per-client) |
| Environment | TF_STATE_BUCKET |
Variable | S3 bucket for this client's Terraform state |
| Environment | AWS_REGION |
Variable | AWS region (defaults to us-east-1) |
| Environment | All secrets | Secrets | DB creds, API keys, etc. (per-client) |
Environment-level configuration is set up per-client in Step 02 - Pipeline Setup.
Verify the Pipeline Configuration¶
After setting the secret, verify it's accessible:
# List repo-level secrets (should show ADMIN_AWS_ROLE_ARN, DJANGO_SUPERUSER_EMAIL, DJANGO_SUPERUSER_PASSWORD)
gh secret list
# List repo-level variables (should show CAMPUSCORE_HOSTED_ZONE_ID)
gh variable list
See also: Admin Account Setup