diff --git a/.github/workflows/entry-push-latest.yml b/.github/workflows/entry-push-latest.yml index 59070de57b..72c1ca9e52 100644 --- a/.github/workflows/entry-push-latest.yml +++ b/.github/workflows/entry-push-latest.yml @@ -19,6 +19,27 @@ permissions: contents: read jobs: + sync-main: + name: 🔄 Sync main + runs-on: ubuntu-26.04 + permissions: + contents: read + steps: + - name: ⬇️ Checkout repository + uses: actions/checkout@v7.0.0 + with: + fetch-depth: 0 + persist-credentials: false + + - name: 🔄 Sync main from source repository + shell: bash + env: + CI_SYNC_MAIN_TOKEN: ${{ secrets.CI_SYNC_MAIN_TOKEN }} + CI_SYNC_MAIN_SOURCE_REPOSITORY_DEFAULT: infinito-nexus/core + CI_SYNC_MAIN_SOURCE_REPOSITORY: ${{ vars.CI_SYNC_MAIN_SOURCE_REPOSITORY }} + CI_SYNC_MAIN_SOURCE_REPOSITORY_IS_SET: ${{ contains(toJSON(vars), '"CI_SYNC_MAIN_SOURCE_REPOSITORY"') }} + run: bash scripts/github/sync/main_from_repository.sh + run-policy: name: 🧭 Decide CI policy runs-on: ubuntu-26.04 diff --git a/docs/contributing/tools/github/actions/secrets.md b/docs/contributing/tools/github/actions/secrets.md index 54cefb8974..25a0d0398a 100644 --- a/docs/contributing/tools/github/actions/secrets.md +++ b/docs/contributing/tools/github/actions/secrets.md @@ -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` 🤖 diff --git a/scripts/github/README.md b/scripts/github/README.md index d3e2d3eb9b..8812ce88cf 100644 --- a/scripts/github/README.md +++ b/scripts/github/README.md @@ -12,6 +12,7 @@ This directory holds the shell helpers that GitHub Actions workflows under [`.gi | [resolve/](resolve/) | Derivation of structured workflow inputs and outputs from repository state. | | [update/](update/) | The scheduled `🔄 Update versions` pipeline (version bumps and PR open/refresh). | | [release/](release/) | Release-time gates that decide whether a tagged commit may be released. | +| [sync/](sync/) | Mirroring the source repository's `main` into a fork on every push. | A new helper MUST live in the subfolder that matches its responsibility. New responsibilities MAY motivate a new subfolder, but MUST NOT flatten back into the top level. diff --git a/scripts/github/sync/README.md b/scripts/github/sync/README.md new file mode 100644 index 0000000000..81f7e8bc51 --- /dev/null +++ b/scripts/github/sync/README.md @@ -0,0 +1,5 @@ +# GitHub Sync Helpers 🔄 + +[main_from_repository.sh](main_from_repository.sh) force-pushes the source repository's `main` onto the current repository's `main`. The `🔄 Sync main` job of [entry-push-latest.yml](../../../.github/workflows/entry-push-latest.yml) runs it on every push. + +The push needs the secret `CI_SYNC_MAIN_TOKEN`, see [secrets.md](../../../docs/contributing/tools/github/actions/secrets.md). The source repository and the switch to disable the sync are the repository variable `CI_SYNC_MAIN_SOURCE_REPOSITORY`, see [configuration.md](../../../docs/contributing/tools/github/actions/configuration.md). diff --git a/scripts/github/sync/main_from_repository.sh b/scripts/github/sync/main_from_repository.sh new file mode 100755 index 0000000000..b282776870 --- /dev/null +++ b/scripts/github/sync/main_from_repository.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# +# Env: +# GITHUB_REPOSITORY - owner/repo of the running job, whose main is overwritten +# CI_SYNC_MAIN_TOKEN - fine-grained token for GITHUB_REPOSITORY only, with Contents and Workflows write +# CI_SYNC_MAIN_SOURCE_REPOSITORY, CI_SYNC_MAIN_SOURCE_REPOSITORY_IS_SET, +# CI_SYNC_MAIN_SOURCE_REPOSITORY_DEFAULT - the source of main, see scripts/meta/resolve/baseline_remote.sh +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)" + +: "${GITHUB_REPOSITORY:?Missing GITHUB_REPOSITORY}" + +source_remote="$(bash "${REPO_ROOT}/scripts/meta/resolve/baseline_remote.sh")" +if [[ "${source_remote}" == "origin" ]]; then + echo "Main sync skipped: no source repository other than ${GITHUB_REPOSITORY} is configured." + exit 0 +fi + +if [[ -z "${CI_SYNC_MAIN_TOKEN:-}" ]]; then + echo "ERROR: secret CI_SYNC_MAIN_TOKEN is not set, so ${GITHUB_REPOSITORY}:main cannot be synced from ${source_remote}." >&2 + echo "Create it as described in docs/contributing/tools/github/actions/secrets.md, or disable the sync through the repository variable CI_SYNC_MAIN_SOURCE_REPOSITORY." >&2 + exit 1 +fi + +echo "Syncing ${GITHUB_REPOSITORY}:main from ${source_remote}." +git fetch "${source_remote}" main:refs/remotes/main-sync-source/main --force + +auth="$(printf 'x-access-token:%s' "${CI_SYNC_MAIN_TOKEN}" | base64 -w0)" +GIT_CONFIG_COUNT=1 \ + GIT_CONFIG_KEY_0="http.https://github.com/.extraheader" \ + GIT_CONFIG_VALUE_0="AUTHORIZATION: basic ${auth}" \ + git push "https://github.com/${GITHUB_REPOSITORY}.git" refs/remotes/main-sync-source/main:refs/heads/main --force