feat(ci): mirror the source repository's main into a fork on every push

bf25eb2fc7 removed the sync-main job because the fork had no token that
may push .github/workflows/**: GITHUB_TOKEN cannot be granted the
workflows permission, and the bot App is installed on infinito-nexus/core
only. Since then a fork's main only moves when its owner syncs it by
hand, and the fork's main has fallen 368 commits behind.

The job is back with a token of its own: CI_SYNC_MAIN_TOKEN, a
fine-grained personal access token limited to the fork, with Contents
and Workflows write. main_from_repository.sh resolves the source through
baseline_remote.sh, so the normalization and the off switch exist once:
in infinito-nexus/core itself and with CI_SYNC_MAIN_SOURCE_REPOSITORY
disabled the job ends green without reading the secret. Everywhere else
a missing secret fails the job before any fetch or push and names the
page that explains how to create it. The token reaches git as an HTTP
header through GIT_CONFIG_* variables, never through argv or
.git/config; checkout runs without persisted credentials so its own
header does not compete.

The job runs beside run-policy, not before it: CI keeps fetching its
diff baseline from the source repository read-only, so a failed sync
turns the run red without holding CI back.

fix/ci-mirror-token tried the bot App instead (019fb572a7, 756b180f1d).
That needs the App installed on every fork and granted Workflows write,
which secrets.md rules out for the App, so the token here is a
per-fork PAT.

Checked: the script skips as infinito-nexus/core and with the sync
disabled, and fails before fetching as a fork without the secret.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
kevinveenbirkenbachandClaude Opus 5.5 committed 2026-10-28 13:12:22 +01:00
1 parent 34be0afdf7
commit ffe77cdba9
5 files changed
+82

No files matched your search

@@ -15,6 +15,27 @@ Repository secrets MUST be set under **Settings → Secrets and variables → Ac
| `API_OPENROUTER_KEY` | [call-test-deploy.yml](../../../../../.github/workflows/call-test-deploy.yml) | Publishes `openrouter/auto`. | Never. |
| `DOCKERHUB_USERNAME` | [cron-images-mirror-all.yml](../../../../../.github/workflows/cron-images-mirror-all.yml), [call-images-mirror-missing.yml](../../../../../.github/workflows/call-images-mirror-missing.yml) | Authenticates the mirror job against Docker Hub, which raises the anonymous pull-rate limit the mirror would otherwise hit. | Never. Both workflows declare the secret `required: false` and gate the login step on both values being non-empty. |
| `DOCKERHUB_TOKEN` | [cron-images-mirror-all.yml](../../../../../.github/workflows/cron-images-mirror-all.yml), [call-images-mirror-missing.yml](../../../../../.github/workflows/call-images-mirror-missing.yml) | The access token paired with `DOCKERHUB_USERNAME`. | Never, and only together with the username. |
| `CI_SYNC_MAIN_TOKEN` | [entry-push-latest.yml](../../../../../.github/workflows/entry-push-latest.yml) | Fine-grained token the `🔄 Sync main` job pushes the source repository's `main` with. | In every repository whose `CI_SYNC_MAIN_SOURCE_REPOSITORY` names another repository, which a fork does by default. |
## `CI_SYNC_MAIN_TOKEN` 🔄
### How to create the token 🛠️
1. Open **Settings → Developer settings → Personal access tokens → Fine-grained tokens → Generate new token** on the account that owns the fork.
2. Set **Resource owner** to that account and **Repository access** to **Only select repositories**, selecting the fork alone.
3. Grant these repository permissions and nothing else:
- `Contents`: **Read and write** (force-push of `main`).
- `Workflows`: **Read and write** (the pushed `main` carries `.github/workflows/**`).
- `Metadata`: **Read** (default, cannot be removed).
4. Choose an expiration date and note it; an expired token fails the job like a missing one.
5. In the fork, add it under **Settings → Secrets and variables → Actions → Secrets → New repository secret** as `CI_SYNC_MAIN_TOKEN`.
### Failure mode when the secret is missing 💥
| Repository | Behaviour |
|---|---|
| `infinito-nexus/core`, or a repository with the sync disabled through `CI_SYNC_MAIN_SOURCE_REPOSITORY` | The job prints that the sync is skipped and succeeds. The secret is not read. |
| Any other repository | [main_from_repository.sh](../../../../../scripts/github/sync/main_from_repository.sh) fails before any fetch or push and names the secret. The run is marked failed; the other jobs of the run are unaffected. |
## `API_OPENAI_KEY`, `API_ANTHROPIC_KEY`, `API_OPENROUTER_KEY` 🤖