Contributing
Conventions for adding workflows, scripts, config entries, and vendor components.
Adding a workflow
-
Create the workflow file in
.github/workflows/ -
Register in priority tiers — add an entry to
config/workflow-priority-tiers.ymlusing the workflow'sname:field (not the filename):- name: "My New Workflow" tier: 3 # MEDIUM — adjust based on criticalityTier guide: 1=CRITICAL (never cancelled), 2=HIGH (mirror chain), 3=MEDIUM (default), 4=LOW (cancelled first under quota pressure)
-
Register in workflow-sync — add to
config/workflow-sync.yml:- Under
github_onlyif it has no GitLab CI counterpart (most workflows) - Under
pairedif it has a matching GitLab CI job
- Under
-
Add a concurrency group if triggered by
scheduleorworkflow_run:concurrency: group: my-workflow-name cancel-in-progress: true -
Add a quota pre-flight if the workflow makes API calls and runs frequently. Use the shared include — do not inline the curl block:
- name: Checkout uses: actions/checkout@v4 - name: Quota pre-flight id: quota env: GH_TOKEN: ${{ secrets.SYNC_TOKEN }} MIN_QUOTA: "500" # adjust to your workflow's actual cost run: | source scripts/includes/quota-snapshot.sh quota_snapshotCheckout must come before the quota pre-flight step.
quota-snapshot.shis sourced from the checked-out repo — if checkout runs after, the runner cannot find the file and the step fails withNo such file or directory.Gate subsequent steps with
if: steps.quota.outputs.skip == 'false'.The include writes
remaining,reset_time, andskiptoGITHUB_OUTPUTand a status line toGITHUB_STEP_SUMMARY. It also supports an optionalQUOTA_WRITE_VAR: "true"env var that writes aQUOTA_SNAPSHOTrepo Actions variable — useful for chain entry/exit points so downstream workflows can read quota state without an API call via${{ vars.QUOTA_SNAPSHOT }}. Currently enabled onpre-flush-prep,full-chain-flush, andpost-flush-prep.Fork note:
QUOTA_WRITE_VARrequires the token to havereposcope (classic PAT) orvariables: write(fine-grained PAT). If the write fails the workflow continues — it logs a warning and the snapshot is simply not updated. Thevariables: writepermission must also be declared at the workflow level:permissions: actions: write contents: read variables: write -
Validate:
python3 scripts/validate-workflow-guards.py python3 scripts/validate-priority-tiers.py config/workflow-priority-tiers.yml -
Update the workflow triggers doc — if the workflow belongs to a group where display order matters (e.g. Full Pipeline, Mirror Chain), add its filename pattern to
GROUP_SORT_KEYSinscripts/generate-workflow-triggers-doc.py, then regenerate:python3 scripts/generate-workflow-triggers-doc.py cp docs/workflow-triggers.md DOCS/workflow-triggers.mdGROUP_SORT_KEYSmaps group names to filename-substring lists in the desired display order. Workflows not listed sort alphabetically after the pinned ones. Groups without an entry sort fully alphabetically — only add an entry when alphabetical order is wrong for that group.
Adding a script
Scripts live in scripts/. All logging must go to stderr — never stdout —
because many functions are called inside $(...) captures where stdout becomes
the captured value.
info() { echo "[my-script] $*" >&2; }
warn() { echo "[warn] $*" >&2; }
If the script sources includes/budget.sh or includes/gh-api.sh, add the
shellcheck directive:
# shellcheck source=includes/budget.sh
source "$(dirname "${BASH_SOURCE[0]}")/includes/budget.sh"
Run ShellCheck before committing:
shellcheck --severity=warning scripts/my-script.sh
Adding a config entry
New Ona project
Add to config/ona-projects.yml under projects::
my-repo:
repo: https://github.com/OpenOS-Project-OSP/my-repo
name: "my-repo"
project_id: "" # populated by sync-ona-projects workflow on first run
branch: main
classes: [Regular]
description: "Brief description of the project"
tags: [osp-bound]
Then trigger sync-ona-projects.yml with dry_run=true to preview, or
dry_run=false to create the project in Ona (requires ONA_TOKEN secret).
New repo to GitLab mirror chain
Add to config/gitlab-subgroups.yml under the appropriate subgroup:
rust-systems_deving:
repos:
- my-new-repo
Then validate:
python3 scripts/validate-gitlab-subgroups.py config/gitlab-subgroups.yml
New repo to upstream sync
Add to registered-imports.json:
{
"source_url": "https://github.com/upstream-org/repo-name",
"target_name": "repo-name",
"platform": "github",
"added": "2026-06-07T00:00:00Z"
}
Then validate:
python3 scripts/validate-registered-imports.py registered-imports.json
New workflow priority tier entry
See "Adding a workflow" above — step 2.
Adding a vendor component
vendor/ is for third-party components that fork-sync-all hosts or deploys.
It is not for first-party scripts or config.
Before adding:
- Confirm the component is genuinely third-party (not a script you wrote)
- Confirm it will be deployed or served by fork-sync-all (not just referenced)
When adding:
- Place under
vendor/<component-name>/ - Strip all distro-specific or org-specific hardcoded defaults — see the agnostic rule below
- Add a
README.mdwith a "Before the first deploy" section covering all required CI variables - Run the agnostic check:
bash scripts/check-vendor-agnostic.sh vendor/<component-name>
Agnostic rule
No deployment-identity values may appear as hardcoded fallback defaults in vendored components. This includes:
- Public URLs:
${VITE_ENDPOINT_URL:-https://api.myorg.com}❌ - Org/repo slugs:
${MIRRORLIST_REPO:-MyOrg/my-repo}❌ - Arch/repo paths:
${MIRROR_REPO_PATHS:-x86_64/core}❌ - Distro names:
${DISTRO:-cachyos}❌
Allowed:
- Localhost dev URLs:
${API_URL:-http://localhost:5862}✅ - Generic paths:
${MIRRORLIST_PATH:-mirrorlist/mirrorlist}✅ - Single-word tokens:
${LOG_LEVEL:-info}✅ - UI strings:
${APP_NAME:-Infra Dashboard}✅
To suppress a specific line that is intentionally non-agnostic:
SOME_VAR="${SOME_VAR:-value}" # check-vendor-agnostic: ignore
enforce-agnostic-vendor.yml runs automatically on every push/PR touching vendor/.
Commit conventions
Follow the existing commit message style:
scope: short description
Longer explanation if needed. Focus on why, not what.
Common scopes: fix, feat, config, docs, vendor, scripts, ci.
Before opening a PR
# Config validators
python3 scripts/validate-gitlab-subgroups.py config/gitlab-subgroups.yml
python3 scripts/validate-registered-imports.py registered-imports.json
python3 scripts/validate-cost-profiles.py config/workflow-cost-profiles.yml
python3 scripts/validate-priority-tiers.py config/workflow-priority-tiers.yml
python3 scripts/validate-template-config.py
python3 scripts/validate-workflow-guards.py
# ^ also validates workflow-quota-costs.yml entry counts and consistency
# Test suites
python3 -m pytest tests/ -v --tb=short
bash scripts/tests/test-check-readme-render-mobile.sh
# ShellCheck (for any .sh files changed)
git diff --name-only HEAD -- 'scripts/*.sh' | xargs shellcheck --severity=warning
# Vendor check (if vendor/ was touched)
bash scripts/check-vendor-agnostic.sh vendor
# README render check
bash scripts/check-readme-render.sh README.md
All must pass before the PR is ready for merge.
Forking this repo
If you fork fork-sync-all into your own org, a few things need attention:
Required secrets
Copy all secrets from the secrets table in README.md.
At minimum SYNC_TOKEN is required — most workflows will skip or fail without it.
Token scope for QUOTA_SNAPSHOT
Three workflows (pre-flush-prep, full-chain-flush, post-flush-prep) write
a QUOTA_SNAPSHOT repo Actions variable after their quota pre-flight. This
variable lets downstream chained workflows read quota state without an API call
via ${{ vars.QUOTA_SNAPSHOT }}.
The write requires:
- Classic PAT —
reposcope is sufficient (already needed by most workflows) - Fine-grained PAT — must include
variables: writerepository permission
If the write fails (wrong scope, token too restricted) the workflow logs a
warning to stderr and continues — nothing breaks, QUOTA_SNAPSHOT just won't
be updated for that run. Downstream workflows reading ${{ vars.QUOTA_SNAPSHOT }}
will see the last successfully written value, or an empty string on first run.
To confirm the variable is being written, check the "Quota pre-flight" step log
for QUOTA_SNAPSHOT variable updated (HTTP 204). If you see
QUOTA_SNAPSHOT variable write failed (HTTP 403), your token needs the
variables: write permission and the workflow needs:
permissions:
variables: write
FSA mode detection
Forked instances are detected as downstream-fork by fsa-node-identity.sh
and skip source-only operations (readmes, badges, fork-sync, templates,
translate) to prevent duplicate work. See Architecture for
the full node identity model.
Config files to update
| File | What to change |
|---|---|
config/gitlab-subgroups.yml | Your GitLab group and subgroup names |
registered-imports.json | Your upstream repos |
config/template-consumers.yml | Your consumer repos |
AGENTS.md | Update org names throughout |