Contributing
Commit messages
Every commit message starts with a type in parentheses, then a short description:
(<type>): <description>
Allowed types:
| Type | Use for |
|---|---|
feat |
new user-facing functionality |
fix |
bug fixes |
docs |
documentation only |
chore |
maintenance, config, dependency bumps |
refactor |
restructuring without behaviour change |
style |
formatting, naming, no logic change |
perf |
performance work |
build |
build scripts, packaging |
ci |
GitHub Actions and other pipeline changes |
test |
tests only |
del |
removing code, files, or features |
Examples:
(feat): add supplier search
(fix): prevent duplicate checkout
(refactor): extract payment validation
(docs): update setup instructions
The space after ): is optional, so (docs):update setup instructions also passes.
For compatibility with older PR commits, Update <path> also passes in CI.
Branch names
<type>/<short-description>
The type list is the same as above. Descriptions are lowercase kebab-case: no spaces, no underscores, no capitals.
Examples:
feat/supplier-search
fix/duplicate-checkout
refactor/payment-validation
docs/local-setup
Branch model
Four kinds of branch, and main keeps its name:
feature branch ──PR──> sprint-N ──PR──> main ──PR──> release
| Branch | Role | How it changes |
|---|---|---|
<type>/<short-description> |
Feature branch. Created from the branch of the current sprint, sprint-N. |
You push to it. |
sprint-N |
Sprint branch, for example sprint-2. Every finished PR of sprint N lands here. |
PR from a feature branch, squash merge. |
main |
Holds only the work of finished sprints. Docs are deployed from it, and each GitHub release is built from it. | One PR from sprint-N at the end of the sprint. |
release |
What gets released. It only ever holds code that was ready on main. Production deploys from it. |
PR from main, merge commit. |
Each sprint has its own branch, sprint-N, made from main. Feature branches start from it, and
their PRs go into it. At the end of the sprint, one PR merges sprint-N into main. The Scrum
Master of the sprint reviews that PR (see Scrum roles). The
GitHub ruleset allows branch names that start with sprint-. Every sprint ends with a GitHub
release built from main (step 7 below). sprint-2 was the first sprint branch. It was merged into
main in #284 on October 13 and
then deleted. The Sprint 2 work before it merged straight into main.
SAPAA Dev release flow
For the new SAPAA Dev app, the owner approved course prod → successful CI → SAPAA-Dev main → staged Vercel build → manual promotion on October 31, 2026. This replaces the older release hosting route for that app. The course sprint review and main rules still apply. Keep all branches. See SAPAA Dev production deployment for setup, the release command, the manual button, and failure handling.
The older course release process below remains recorded for the earlier hosting route.
The hosting project (Vercel) has its own production branch setting. It is not stored in this
repository, so nobody can check it from here. The person who owns the hosting account must check
that the production branch is release. If it is main, every merged PR goes live.
The team that owns Platform / CI/CD / releases in the sprint makes the release (see Project management):
- Merge
sprint-Nintomainwith one PR that the Scrum Master of the sprint reviews. - Open a PR with base
releaseand headmain. Use a title like(chore): release sprint 2. - Assign the Scrum Master of the sprint and at least one other person
(see Scrum roles). The Scrum Master's approval is one of
the 2 required approvals. The
reviewer-gatestatus needs every assignee to approve. This is not enforced yet: themainandreleaserulesets require thereviewerscheck, which always passes, and not thereviewer-gatestatus. Until a repo admin changes the required check in both rulesets fromreviewerstoreviewer-gate, any 2 approvals merge the release PR, so the Scrum Master has to check the status by hand before merging. - Get the 2 approvals and green checks.
- Before the code is deployed, a human runs the new SQL migrations (see Production database migrations).
- Merge with a merge commit, so
releaseandmainstay in step and the next release PR has no conflicts. - Tag the release on
main, on the commit that was merged intorelease, for examplesprint-2. Publish the GitHub release from that tag. The notes list each story with its issue and PRs, the number of CI tests, the known gaps and the feedback from the TA that the sprint addressed.
Never commit to release directly and never open a PR into it from any branch other than
main. The conventions check fails the PR if you do. Hotfixes go through main like
everything else.
Production database migrations
Nothing applies the files in webapp/supabase/migrations/ to production. CI has no migration
job, and a deploy does not run them. A person runs each file by hand in the Supabase dashboard
(SQL editor) on the production project. The app reads only the renamed tables (sites_pa,
questions, ...) and the new columns (protected_area_id, archived_at). If the code is
deployed before the SQL, every site and form query fails. So the SQL runs first.
The person who runs it needs access to the production Supabase project. Do not run these files from the app.
Before you start
- Take a backup of the production database (Supabase dashboard > Database > Backups, or
pg_dump). - Find out what production already has:
sql
select to_regclass('public."W26_sites-pa"') as old_sites,
to_regclass('public.sites_pa') as new_sites,
to_regclass('public.people') as people,
to_regclass('public.protected_areas') as protected_areas;
A name that is not null exists. old_sites not null and new_sites null means the rename in
step 2 is still to do. Production may already have some of the later steps. Read the header
of a file before you run it again: most say that they are safe to run twice.
3. Find functions and views that name a W26_ table. The rename file rewrites only
get_naturalness_distribution and get_top_sites_distribution. Anything else in this result
breaks at step 2 and needs its own fix first.
sql
select proname from pg_proc
where pronamespace = 'public'::regnamespace and prosrc ilike '%W26\_%';
select viewname from pg_views
where schemaname = 'public' and definition ilike '%W26\_%';
4. Set the environment variables of the new build in the hosting project: see
Deployment manual.
NEXT_PUBLIC_PROTOMAPS_API_KEY is baked in at build time, so it must exist before the build.
Cutover window
Step 2 renames the W26_ tables. The code that is live now breaks after the rename. The new code
breaks before it. There is no way to avoid a short gap, so do it in a maintenance window, right
before the deploy:
- Tell the stewards and admins that the app is down for a short time.
- Run only the missing migrations below, in order. Check the release commit and the current schema first. A production database that already has Sprint 2 does not need those steps again.
- Merge the release PR straight away (Branch model), so the new code deploys.
- Open the app and run the smoke test below. The window ends when it passes.
The team agrees the time of the window. Alpha does not run the rename before that.
Order
Run the files in this order.
| Step | File | Note |
|---|---|---|
| 0 | 20260925000000_local_app_schema.sql |
Never run this on production. It is only for a fresh local database: it creates the W26_ tables, which production already has. It no longer enables row level security on the legacy tables (luperson and the three _fnr tables), so it cannot cut the F26 mobile app and the Google Sheet off from luperson. It still creates W26_ tables and policies production does not need, and create table if not exists does not make it safe there. |
| 1 | 20261002000000_fix_questions_section_fk_name.sql |
Run only while W26_questions still exists. After the rename the file fails. It does nothing when the constraint already has the right name. |
| 2 | 20261003000000_remove_w26_prefix.sql |
The rename. Do it in the cutover window. |
| 3 | 20261004000000_create_user_model.sql |
Adds the trigger on auth.users and a people row for each existing login. Check the count after it (below). |
| 4 | 20261004010000_create_protected_areas.sql |
Skip it if protected_areas already exists (the protected_areas result in the pre-check is not null). Also skip it if the table has a folder column, which means step 9 already ran. A second run is safe: the site_id index step skips itself once step 9 has dropped that column. |
| 5 | 20261005000000_profile_distinct_site_count.sql |
|
| 6 | 20261005010000_create_socials.sql |
|
| 7 | 20261005020000_gallery_read_policies.sql |
|
| 8 | 20261006000000_create_kml_imports_bucket.sql |
|
| 9 | 20261006010000_alter_protected_areas_for_kml_import.sql |
Drops 8 columns of protected_areas, including site_id. |
| 10 | 20261008000000_create_site_page_fields.sql |
Needed for the site page fields. |
| 11 | 20261008164223_delete_site.sql |
Adds sites_pa.archived_at. Every site query reads it. |
| 12 | 20261009000000_archive_site_inspection_reports.sql |
Adds form_responses.archived_at and redefines the count and dashboard functions. |
| 13 | 20261009120000_add_sites_pa_protected_area_id.sql |
Needed by the admin sites list, the map filter, and adding and editing a site. |
| 14 | 20261009120100_sites_pa_protected_area_id_unique.sql |
If it fails, stop. Two sites share one protected area. Remove the extra link by hand, then run the file again. |
| 15 | 20261009130000_get_max_inspection_number.sql |
From PR #242. Adds get_max_inspection_number. New reports get no inspection number until it exists. |
| 16 | 20261010000000_form_responses_select_policy.sql |
From PR #234. Lets approved users read form_responses. Without it the galleries show no images. Check first: select policyname from pg_policies where tablename = 'form_responses'; |
| 17 | 20261010000100_require_approval_for_people_reads.sql |
From PR #238. Only approved accounts (and the owner of a row) read people, socials and site_stewards. |
| 18 | 20261011000000_revoke_public_execute_on_report_functions.sql |
From fix/alpha-code-quality. Run it after step 15. Only the service role can then call get_distinct_inspected_site_count and get_max_inspection_number. |
| 19 | 20261011010000_revoke_public_execute_on_dashboard_functions.sql |
From fix/gamma-sprint-2-gaps. Run it after step 12. Revokes execute on get_naturalness_distribution and get_top_sites_distribution from public and anon. authenticated keeps it, because the StewardLog admin dashboard calls both functions after sign-in. |
| 20 | 20261012000000_add_question_atomically.sql |
From fix/form-editor-save-errors (#273). Run it after step 2. Adds add_question_atomically, for the service role only. Until it exists, adding a question in the form editor fails. |
| 21 | 20261020000000_create_gpx_tracks.sql |
Creates the private gpx-tracks bucket and GPX rows, indexes and access policies. Run after the site tables and user model exist. |
| 22 | 20261020100000_link_sites_to_protected_areas.sql |
Adds centre coordinates and the service-role-only boundary-link RPC. It does not fill existing rows when applied. |
| 23 | 20261021000000_link_sites_to_protected_areas.sql |
Same contents as step 22. Retain both versions in the migration history; the second run is safe. Do not invent a replacement migration. |
| 24 | 20261022000000_add_answer_visibility.sql |
Adds answers.is_hidden. Existing answers stay visible. Apply before deploying US 1.0.3. |
| 25 | 20261023000000_add_protected_area_boundary_color.sql |
Adds the optional boundary color and its #rrggbb constraint. Existing boundaries keep the default. |
| 26 | 20261024000000_add_site_priority_scores.sql |
Adds the three scores, saved date and saving Admin. Existing sites have no scores. Apply before deploying US 2.0.12 and 2.0.13. |
| 27 | 20261024010000_create_export_jobs.sql |
Creates export jobs and the creator-only read policy. Only the service-role server writes jobs. Apply before deploying US 3.0.3. |
| 28 | 20261024020000_check_gpx_track_path.sql |
From PR #312. Include only after it merges. It replaces the GPX insert policy with the path check; existing rows are not rewritten. |
| 29 | 20261025000000_fix_site_delete_impact.sql |
Redefines the two site delete functions so the impact count includes GPX tracks and page fields. Service role only. Needs site_page_fields, archived_at and gpx_tracks, so apply it after steps 10, 11 and 21. |
| 30 | 20261025000010_add_missing_fk_indexes.sql |
Adds eleven foreign key and filter indexes. Each build locks its table while it runs, so apply outside peak use. |
| 31 | 20261025000020_count_one_naturalness_answer_per_report.sql |
Redefines get_naturalness_distribution to count each report once per score. Keeps the existing grants. Apply after step 12. |
| 32 | 20261027000000_add_missing_count_to_export_jobs.sql |
Adds export_jobs.missing_count, the count of files that an export skipped because the object was missing from storage. Apply after step 27, before the export change is deployed. |
PRs #234, #238 and #242 are merged on main. Steps 18 to 20 came with the Sprint 2 fixes, which
are on main since #284
(October 13). Run each step only when its file is on main.
If a PR merges with a different file name, use the name in the merged PR.
Sprint 3 migration review
The files above target the web schema. They do not rename legacy mobile inspection tables.
Steps 22 and 23 are byte-identical. Both are retained because databases can already record one
or both versions. The link function is security invoker, uses an empty search path, and grants
execution only to the service role. Applying it adds the function and coordinates; an approved
Admin still runs the import to fill site links.
Green must confirm the final migration review in issue #183 before release. This document is an inventory and order, not an approval to run SQL on production. A human records which files were applied and the result of the checks in Sprint 3 release checks. Do not apply step 28 from an unmerged branch. Do not assume that a successful local run proves the production schema is current.
Checks
Right after step 3, the counts must match:
select (select count(*) from auth.users) as logins,
(select count(*) from public.people where auth_user_id is not null) as people;
Before step 14, look for two sites that share a protected area:
select protected_area_id, count(*) from public.sites_pa
where protected_area_id is not null group by 1 having count(*) > 1;
Smoke test after the deploy: /sites (list and map), a new report with a photo, the profile page,
assigning a steward, archiving a site and a report, and the gallery.
Then, in the app:
- An admin runs the KML import (Admin > Sites) to fill
protected_areas. - Admins link each site to a protected area.
- Check that admins and approved users have
app_metadata.roleandapp_metadata.authenticated.
If a query fails with PGRST200 after a migration, run notify pgrst, 'reload schema'; and try
again.
Local and CLI-managed databases
Two files were merged out of timestamp order: 20261005020000_gallery_read_policies.sql (after
the 20261006* files) and 20261008000000_create_site_page_fields.sql (after
20261008164223_delete_site.sql). supabase migration up does not apply a file that is older
than one it has already applied. A local database that was set up before these merged can miss
the gallery policies or site_page_fields. Run this once:
supabase migration up --local --include-all
These commands are for local databases only. Never run supabase db push against production:
there is no staging, and production is migrated by hand in the cutover window (above).
--include-all would apply every file, including step 0 and the W26_ rename, with no
pre-checks. If a local database cannot be fixed, supabase db reset rebuilds it from the files.
When a PR merges late, give its migration a timestamp later than every file on main.
How this is enforced
A local commit-msg hook in .githooks/ checks both the message and the current branch
name on every commit. It is installed by the prepare script in package.json, so a fresh
clone picks it up the first time you run npm install — from the repository root, webapp/,
or FNR/, whichever you install first.
If you cloned before the hook existed, or you skipped npm install, wire it up by hand:
git config core.hooksPath .githooks
main, master, release and the sprint branches (sprint-3, sprint-4) are exempt from the branch check, and git's own merge, revert, and
autosquash messages are left alone.
After editing the hook, check it still behaves:
sh .githooks/test-hooks.sh
Bypassing the hook
It is an ordinary local git hook, so it can be skipped with git commit --no-verify. That only
gets you past your own machine: CI runs the same script on every commit in a PR, so a
badly named commit will still fail the PR. Fix it with git commit --amend or an interactive
rebase, then force-push your branch.
CI
| Workflow | Runs on | Jobs and checks |
|---|---|---|
ci.yaml |
Every push to any branch, and by hand | webapp-tests: npm ci, npx tsc --noEmit, npm run test:ci, npm run build, then npm run lint -- --max-warnings 0. docs-build: mkdocs build --clean. hook-tests: sh .githooks/test-hooks.sh. |
pr-checks.yaml |
PRs into main, release and sprint-* |
conventions (branch name, PR title, commit messages) and pr-size. |
reviewer-gate.yaml |
PRs into main, release and sprint-*, and reviews. Skipped while the PR is a draft; it runs when the PR is marked ready for review. |
reviewers, which posts the reviewer-gate status. |
diagrams.yaml |
PRs into main, release and sprint-* |
See The diagrams workflow. |
diagram-svg-check.yaml |
Every PR into main, release and sprint-*, on the self-hosted runner |
diagram-svg-check: runs the diagram tests and check --svg-only. It fails when the source or SVG differs from the recorded render, the SVG is missing, or the manifest has no SVG hash. It renders and writes nothing. Run npm run diagrams and commit the result. |
e2e.yaml |
PRs into main and release, and by hand |
cypress: runs cypress/e2e/logout.cy.ts and cypress/e2e/profile.cy.ts against a local Supabase that it starts in the job (the migrations, supabase/seed.sql and scripts/e2e_seed.js). |
Rules of the gates:
- Lint blocks. Any ESLint error or warning fails
webapp-tests. Runnpx eslint .inwebapp/before you push. - Coverage blocks.
npm run test:ciisjest --coverage. It fails when the global coverage falls belowcoverageThresholdinwebapp/jest.config.ts(statements 83, branches 72, functions 77, lines 85 percent). Runnpm run test:ciinwebapp/before you push. - Known problem.
webapp-testsrunsnpm run test:cibefore the lint step, and the test run writeswebapp/coverage/.eslint.config.mjsdoes not ignore that folder, so ESLint reports an unusedeslint-disabledirective incoverage/lcov-report/block-navigation.js, and--max-warnings 0fails the job. Untilcoverage/**is added to the ignores, deletewebapp/coverage/before you run ESLint locally. - E2E runs only some specs. The other Cypress specs need production data, real S3 images or
an app fix. The comment in
e2e.yamlnames each excluded spec and the reason.cypressis not a required check. To save Actions minutes, it does not run on PRs intosprint-*.
As of October 13, 2026, GitHub Actions has not run since October 10, because the account's Actions
payments failed. Until it runs again, the results of these gates come from local runs only.
#284 and
#285 were merged with an admin
bypass, after the gates ran locally on main at ceb5188.
GitHub rules
These are enforced on GitHub itself, not just locally.
Branches. GitHub rejects any new branch whose name doesn't start with one of the types
above followed by /. main, release, gh-pages and dependabot/** are the only exceptions. So
feat/site-search works, but saumya/test or username-patch-1 won't push. When you edit a
file in the GitHub web editor, rename its suggested patch-1 branch before you commit.
main and release are PR-only. The rules below apply to both. Nobody can push, force-push or delete either one directly. Everything
goes through a pull request, and a PR needs all of these before it can merge:
- 2 approvals from teammates. Pushing a new commit dismisses earlier approvals, and the latest push has to be approved by someone other than its author.
- Assigned reviewers. Assign 2 or more people to the PR (sidebar > Assignees). Then only
they can review and all of them must approve. With no assignees, any 2 teammates can
approve. Exactly 1 assignee fails the
reviewer-gatestatus. A bot comment reminds assignees to read every changed file top to bottom before approving. - All review conversations resolved.
- Green checks:
webapp-tests,docs-build,hook-tests,conventions(branch name, PR title and every commit message) andreviewers. The rulesets still namereviewers, which always passes. Thereviewer-gatestatus carries the real result, and a repo admin must require it instead (see the Scrum Master step in the release PR steps).
Squash merge only on main (release takes merge commits only, see Branch model). Each PR lands on main as one commit, and the PR title is its
message. That's why the PR title has to follow the (<type>): <description> format too.
Branches are deleted automatically after merge.
Copilot. Copilot reviews every PR automatically. Its review never counts as one of the 2 approvals, because GitHub only lets Copilot comment. If the second reviewer is genuinely unavailable, a repo admin can merge with 1 teammate approval plus a Copilot review that has no unresolved comments. GitHub logs every one of those bypasses, so use it sparingly and say why in the PR.
Large PRs. When a PR changes more than 500 lines, it gets a large-pr label and a
comment suggesting you split it. The number of lines does not include changes in docs/,
webapp/package-lock.json or FNR/package-lock.json. The number includes changes in all
other lockfiles, for example the root package-lock.json. It's only a nudge
and never blocks the merge.
Code owners. .github/CODEOWNERS auto-requests reviewers based on the files a PR
touches. At this time, it covers only .github/ and .githooks/. An owner's approval isn't
required beyond the normal 2.
Issues and the project board
New issues go through a template (user story, bug or task). Each one lands on the SAPAA project board automatically. Set the milestone in the sidebar, then set Sprint and MoSCoW on the board. Status moves Todo → In progress → In review (PR open) → Done.
PRs are labelled by the folders they touch (webapp, mobile, docs, infra), so you can
filter the PR list down to your area.
Diagrams
The Mermaid sources are in docs/assets/*.mmd. The rendered SVGs are in docs/images/.
Software design shows them.
Run this from the repository root after you change a migration, a server module or a .mmd file:
npm run diagrams # update the generated parts, then render the SVGs that changed
npm run diagrams:check # change nothing; fail if a diagram or an SVG is out of date
The tool is in tools/diagrams/ and has its own package.json. It needs Node 22.13 or later.
The first run downloads a headless Chrome (about 200 MB on disk) for the renderer.
| Diagram | Generated part comes from |
|---|---|
ER_Diagram.mmd |
webapp/supabase/migrations/*.sql, applied in filename order with the Postgres parser |
UML_Class_Diagram_Models.mmd |
the same migrations (tables) and webapp/lib/pdf/types.ts (PdfOptions) |
UML_Class_Diagram.mmd |
the exported functions and route handlers of the modules in tools/diagrams/src/config.mjs |
Rules:
- The tool writes only between
%% BEGIN GENERATED: <name>and%% END GENERATED: <name>. Do not edit that block. Everything outside it is hand-written and the tool keeps it, with the exceptions below. - Class diagrams: to show a proposed item, write it outside the markers with a
[NEW x.y.z]tag. For a new member of an existing class, add a second block of that class (class Site { ... }). Mermaid joins the two blocks. - Class diagrams: when the code or a migration adds a proposed member, the tool removes the
hand-written line. It also removes a
<<new table x>>class when tablexexists. - Class diagrams: you can add a
[CHANGE x.y.z]tag at the end of a generated line. The tool keeps it. ER_Diagram.mmdshows only the current schema. The tool removes a hand-written entity block for a generated table, and a column that the schema does not have (it shows a warning). It does not keep tags on generated lines. Put proposed tables and columns inER_Diagram_Proposed_User_Stories.mmd.- To add a module, a table or a column note, edit
tools/diagrams/src/config.mjs. - The sequence diagrams,
Architecture.mmdandER_Diagram_Proposed_User_Stories.mmdare hand-written. The tool warns when one of them names a table that a migration dropped or renamed, or a dropped or renamed column astable.column. It does not find a column name without its table. - The tool renders a
.mmdagain when its source, SVG, renderer version or configuration differs from the last render. Source and SVG hashes are saved after a successful render indocs/assets/.diagram-hashes.json. The first run with an old manifest renders every diagram to record its SVG hash. A.mmdfile is saved to the SVG name thatdocs/software-design.mdlinks, for exampleER_Diagram.mmdtoER_Diagram_mermaid.svg. check --svg-onlychecks these saved hashes without generating blocks or opening a browser. It detects drift from the recorded render; it does not independently render and compare an SVG. The workflow runs for every PR so its status is present even when no diagram changed. It becomes a merge gate only when a repository admin makesdiagram-svg-checkrequired.- Running the tool twice gives no changes.
- Self-check:
npm --prefix tools/diagrams test. The workflow also runs it. - The services diagram does not read
webapp/node_modules. A type from a package shows asany, so the output is the same with or without the webapp packages installed.
The diagrams workflow
.github/workflows/diagrams.yaml runs when a PR into main, release or a sprint-* branch is
opened, reopened or updated. It has two jobs:
| Job | Token | What it does |
|---|---|---|
diagrams |
read-only, not kept in .git/config |
Runs npm run diagrams and the self-check. If a file in docs/assets or docs/images changes, it saves the change as a patch. A render failure fails this job. |
diagrams-push |
GitHub App token | Runs only when the patch is not empty. It runs no code from the PR. It checks that the patch changes only docs/assets and docs/images, applies it, and pushes one commit, (docs): update diagrams, to the PR branch. |
diagrams-push fails, and does not push, in these cases. Run npm run diagrams and commit
the result.
| Case | Why it does not push |
|---|---|
| PR from a fork | It cannot push to a fork. |
PR from main into release |
main is PR-only. |
| The last commit is the diagrams commit | The tool did not give the same result twice. This stops a loop. |
The DIAGRAMS_APP_ID variable is not set |
There is no token that can push. |
The guard checks the committer of the last commit, not the author. A rebase sets a new committer, so the job runs again on a rebased branch. If someone pushes while the job runs, the push fails and nothing is overwritten. The next run updates the diagrams.
The push token. A push made with the default GITHUB_TOKEN does not start other workflows.
The five required checks of the main ruleset are webapp-tests, docs-build, hook-tests
(ci.yaml, on push), conventions (pr-checks.yaml) and reviewers (reviewer-gate.yaml,
both on pull_request). The reviewers job always passes. It posts its result as the
reviewer-gate status. None of them run on a commit pushed with GITHUB_TOKEN, so the PR
cannot merge. For this reason the workflow pushes only with a GitHub App token. A repo admin
sets it up:
- Create a GitHub App, or use an existing one, with the repository permission Contents: read and write only. Install it on this repository only.
- Add the repository variable
DIAGRAMS_APP_ID(the App ID) and the secretDIAGRAMS_APP_KEY(the private key).
Do not use a personal access token. The diagrams job runs code from the PR, and a token of a
person lets a teammate push as that person.
As of October 7, 2026, the variable and the secret are not set. So diagrams-push fails when
the diagrams are out of date, and the author runs npm run diagrams.
Required check. The diagrams jobs are not required checks in the main ruleset, so a PR
with out-of-date diagrams can still merge. To block it, the ruleset owner (Gamma) adds
diagrams and diagrams-push as required checks.
Approvals. The main ruleset dismisses approvals on a new push, and the latest push must be
approved by someone other than its author. The reviewer-gate status counts approvals on the head
commit only. So a diagrams commit after an approval needs new approvals. Run
npm run diagrams and commit the result before you ask for review, and the workflow has
nothing to push.