Workflow Scheduling Guide

Optimal trigger windows, quota requirements, and timing constraints for every scheduled workflow. Use this when deciding when to manually dispatch a workflow or when to adjust a cron schedule.


How to read this guide

Quota cost — REST API calls consumed per run (mid = typical, high = worst case). Source: config/workflow-quota-costs.yml. GraphQL calls count as 1 REST call regardless of how many repos are queried.

Best window — the UTC hour range where quota headroom is highest and concurrency with other workflows is lowest.

Avoid — hours where the scheduled burst is already high or where a dependency workflow hasn't finished yet.

Quota floor — the minimum remaining quota required before the workflow will run (from min_quota in config/workflow-quota-costs.yml). If quota is below this, the workflow skips itself and waits for the next reset.


Time format note

Per-workflow tables show two schedule columns:

  • Schedule (UTC) — 24h UTC, the authoritative cron time
  • Schedule (EST) — EST (UTC−5, Nov–Mar). Add 1h for EDT (UTC−4, Mar–Nov).

Example: 09:05 UTC = 4:05 AM EST = 5:05 AM EDT.

The timing map and "Best manual dispatch windows" tables use the full 24h UTC / 12h UTC / 12h ET (EST/EDT) format for completeness.


Daily quota budget

MetricValue
Quota per hour5,000 REST calls
Scheduled drain (daily avg)~3,200 calls/day (~133/hr average)
Worst scheduled burst (03:xx UTC / 3:xx AM UTC / 10:xx PM ET)~612 calls in one hour
Headroom at worst hour~4,388 calls remaining
Safe manual dispatch windowAny hour with < 2,000 calls already consumed

Check current quota before dispatching:

curl -sf -H "Authorization: token $SYNC_TOKEN" \
  "https://api.github.com/rate_limit" | \
  python3 -c "
import sys,json,datetime
d=json.load(sys.stdin)['resources']['core']
print(f\"remaining={d['remaining']}  resets={datetime.datetime.utcfromtimestamp(d['reset']).strftime('%H:%M UTC')}\")
"

Scheduled workflow timing map

All times UTC (24h) with 12h UTC and approximate ET equivalents. [*/2] = every other day (even days of month). (*/30) = every 30 min.

Hour (UTC)     12h UTC        ET (EDT/EST)    Workflows
─────────────────────────────────────────────────────────────────────────────
00:xx          12:xx AM       8:xx PM / 7:xx PM    mirror-to-osp (:13)  sync-in (:37)
                                                    mirror-osp-to-ooc (:45)  auto-merge-prs (:55)
01:xx          1:xx AM        9:xx PM / 8:xx PM    sync-pieroproietti-forks (:07)
                                                    mirror-osp-to-gitlab (:23)
                                                    sync-to-gitlab-variant (:50)
02:xx          2:xx AM        10:xx PM / 9:xx PM   mirror-artifacts (:10)  mirror-orgs-full (:17)
                                                    setup-osp-mirrors (:45)
03:xx          3:xx AM        11:xx PM / 10:xx PM  upstream-prs (:33)  upstream-commits (:47)
04:xx          4:xx AM        12:xx AM / 11:xx PM  git-platform-sync/pull (:27)
                                                    sync-registered-imports (:55)
05:xx          5:xx AM        1:xx AM / 12:xx AM   sync-btrfs-devel-branches (:02)
                                                    rebase-prs [*/2] (:10)
                                                    reconcile-org-refs [*/2] (:50)
06:xx          6:xx AM        2:xx AM / 1:xx AM    queue-manager (*/30)  quota-reserve (*/30)
                                                    [Mon only: update-infra-deps (:11)]
07:xx          7:xx AM        3:xx AM / 2:xx AM    resolve-ci (:43)
08:xx          8:xx AM        4:xx AM / 3:xx AM    check-ci (:05)  inject-badges [*/2] (:15)
09:xx          9:xx AM        5:xx AM / 4:xx AM    git-platform-sync/push (:23)
                                                    check-shell-tools-ci (:30)
10:xx          10:xx AM       6:xx AM / 5:xx AM    sync-in/daily (:15)
                                                    translate-readmes [*/2] (:43)
11:xx          11:xx AM       7:xx AM / 6:xx AM    translate-docs [*/2] (:15)
12:xx–23:xx    12:xx PM–      8:xx AM–             mirror-releases (:03 at 12:xx)
               11:xx PM       7:xx PM              queue-manager + quota-reserve (*/30, all hours)

Per-workflow scheduling reference

Core mirror chain

WorkflowSchedule (UTC)Schedule (EST)Quota midQuota highFloorBest window (UTC)Avoid
Mirror I-D-1896 → OSPEvery 6h at :13Every 6h at :1380200300After 14:00 reset00:00–06:00 (busy)
Mirror OSP → OOCEvery 6h at :45Every 6h at :458020030032 min after mirror-to-ospBefore :13 slot
Mirror OrgsDaily 02:17Daily 9:17 PM6015020002:00–04:0006:xx (weekly burst)
Mirror OSP → GitLabDaily 01:23Daily 8:23 PM8020020001:00–03:00During mirror chain
Mirror ReleasesEvery 12h at :03 (00:03 + 12:03)7:03 PM + 7:03 AM10030020000:03 or 12:03During flush
Mirror ArtifactsDaily 02:10Daily 9:10 PM8020020002:00–04:00During flush

Mirror chain dependency order: Mirror I-D-1896 → OSP must complete before Mirror OSP → OOC. The :13/:45 stagger (32 min gap) is intentional — do not reduce this gap when manually dispatching both.


CI check + resolver

WorkflowSchedule (UTC)Schedule (EST)Quota midQuota highFloorBest window (UTC)Avoid
Check CI StatusDaily 09:054:05 AM300900150009:00–11:00During flush
Resolve CI Failures (Agnostic)Daily 07:432:43 AM12040010007:00–09:00During flush
Check Shell Tools CIDaily 09:304:30 AM5010020009:00–11:00

Note: Check CI Status requires a 1,500 quota floor — the highest of any workflow. If quota is below 1,500 at 09:05 UTC (4:05 AM EST / 5:05 AM EDT), it skips and waits for the next day. Manually dispatch after the 14:00 UTC reset if you need it to run same-day.


Sync operations

WorkflowSchedule (UTC)Schedule (EST)Quota midQuota highFloorBest window (UTC)Avoid
Sync All ForksVia full-chain-flush20050050004:00–08:00During mirror chain
Sync Registered ImportsDaily 04:5511:55 PM4510020004:00–06:00
Sync btrfs-devel BranchesDaily 05:0212:02 AM308010005:00–07:00
Sync pieroproietti ForksDaily 01:078:07 PM6015020001:00–03:00
Sync to GitLab VariantDaily 01:508:50 PM4010010001:00–03:00
Setup OSP Mirror WorkflowsDaily 02:459:45 PM8020020002:00–04:00
Git Platform SyncDaily 04:27 + 09:2311:27 PM + 4:23 AM6015020004:00 or 09:00
Upstream PRs from OSP+OOCDaily 03:3310:33 PM8020030003:00–05:00
Upstream Direct CommitsDaily 03:4710:47 PM80200300After upstream-prs (:33)Before :33 slot

README + badge operations

WorkflowSchedule (UTC)Schedule (EST)Quota midQuota highFloorBest window (UTC)Avoid
Update READMEsVia flush15040050010:00–12:00During mirror chain
Create Missing READMEsVia flush10030030010:00–12:00
Inject Built-with-Ona BadgesEvery 2 days 08:153:15 AM12030030008:00–10:00
Translate READMEsEvery 2 days 10:435:43 AM15040030010:00–12:00
Translate DocsEvery 2 days 11:156:15 AM10025020011:00–13:00
Reconcile Org ReferencesEvery 2 days 05:5012:50 AM8020020005:00–07:00

Infrastructure / quota management

WorkflowSchedule (UTC)Schedule (EST)Quota midQuota highFloorNotes
Queue ManagerEvery 30 minEvery 30 min153050Never manually dispatch — runs automatically
Quota ReserveEvery 30 minEvery 30 min153050Never manually dispatch
Rate-Limit Re-triggerEvery 6hEvery 6h3080100Fires automatically after quota recovery
Auto-merge PRsEvery 6h at :55 (00:55, 06:55, 12:55, 18:55)7:55 PM, 1:55 AM, 7:55 AM, 1:55 PM3080100Safe to dispatch any time
Rebase PRsEvery 2 days 05:1012:10 AM40100100Safe to dispatch any time

Heavy / manual-only workflows

WorkflowTriggerSchedule (EST)Quota midQuota highFloorBest window (UTC)
Full Chain FlushManual / daily 05:1712:17 AM4001000100004:00–08:00
Pre-Flush PrepManual only50150300014:05 (5 min after reset)
Critical DeployManual only100300500Any time — bypasses queue
Onboard RepoManual only80200300Any time
Add Mirror RepoManual only60150200Any time
Sync Ona ProjectsDaily 06:00 / manual1:00 AM12050Any time — dry-run by default on schedule

Pre-Flush Prep has the highest floor (3,000) because it validates config, merges PRs, and then dispatches the full flush chain. Trigger it immediately after the 14:00 UTC (9:00 AM EST / 10:00 AM EDT) reset for maximum headroom.


Best manual dispatch windows

Highest quota headroom

Window24h UTC12h UTCET (EDT)ET (EST)Why
Best14:00–15:002:00–3:00 PM10:00–11:00 AM9:00–10:00 AMImmediately after hourly reset; ~4,867 calls available
Good05:00–07:005:00–7:00 AM1:00–3:00 AM12:00–2:00 AMLow scheduled activity; ~4,400 calls typically available
Good20:00–23:008:00–11:00 PM4:00–7:00 PM3:00–6:00 PMNo scheduled workflows; quota recovering

Lowest concurrency (fewest parallel jobs)

Window24h UTC12h UTCET (EDT)ET (EST)Why
Best15:00–17:003:00–5:00 PM11:00 AM–1:00 PM10:00 AM–12:00 PMNo scheduled workflows at all
Good11:00–13:0011:00 AM–1:00 PM7:00–9:00 AM6:00–8:00 AMOnly translate workflows (every 2 days); runners mostly idle

Avoid

Window24h UTC12h UTCET (EDT)ET (EST)Why
Worst03:00–04:003:00–4:00 AM11:00 PM–12:00 AM10:00–11:00 PMHighest burst (~612 calls); upstream PRs + commits + sync all fire
Caution06:00–07:006:00–7:00 AM2:00–3:00 AM1:00–2:00 AMMonday only: 13-workflow concurrent spike (update-infra-deps)
Caution09:00–10:009:00–10:00 AM5:00–6:00 AM4:00–5:00 AMCheck CI Status fires (1,500 floor); marginal quota risks skipping it

Pre-flush-prep checklist

Before triggering pre-flush-prep.yml:

# 1. Check quota
curl -sf -H "Authorization: token $SYNC_TOKEN" \
  "https://api.github.com/rate_limit" | \
  python3 -c "
import sys,json,datetime
d=json.load(sys.stdin)['resources']['core']
reset=datetime.datetime.utcfromtimestamp(d['reset']).strftime('%H:%M UTC')
ok = '✅' if d['remaining'] >= 3000 else '❌'
print(f\"{ok} remaining={d['remaining']} (need 3000)  resets={reset}\")
"

# 2. Run validators locally
python3 scripts/validate-workflow-guards.py
python3 scripts/validate-gitlab-subgroups.py config/gitlab-subgroups.yml
python3 scripts/validate-registered-imports.py registered-imports.json
python3 scripts/validate-priority-tiers.py config/workflow-priority-tiers.yml
python3 scripts/validate-cost-profiles.py config/workflow-cost-profiles.yml

# 3. Check for open PRs that would block the flush
gh pr list --state open --json number,title,mergeable

Ideal trigger time:

24h UTC12h UTCET (EDT)ET (EST)
14:052:05 PM10:05 AM9:05 AM

5 minutes after the hourly reset — maximum quota headroom before any scheduled workflows consume from the fresh bucket.


Quota drain reduction history

DateChangeSaving
2026-06Removed schedule from check-osp-ci + check-ooc-ci stubs−600/day
2026-06auto-merge-prs: 2h → 6h−240/day
2026-06upstream-prs + upstream-commits: 6h → daily−480/day
2026-06setup-osp-mirrors: 6h → daily−240/day
2026-06sync-to-gitlab + sync-from-gitlab stubs: removed schedule−0 cost, freed runner slots
2026-06sync-btrfs-devel + sync-registered-imports: 6h → daily−105/day
2026-06mirror-releases: 6h → 12h−100/day
2026-06sync-to-gitlab-variant + sync-pieroproietti + mirror-osp-to-gitlab + mirror-artifacts: 8h → daily−480/day
2026-06reconcile-org-refs + rebase-prs + inject-badges + translate-readmes + translate-docs: daily → every 2 days−300/day
Total−2,545/day (−46% from 5,495 baseline)

Current baseline: ~3,200 calls/day (~133/hr average).


Remaining quota reduction opportunities

These are known but not yet applied — each has a trade-off noted.

OpportunityPotential savingTrade-off
Convert sync-forks.sh REST repo loop → GraphQL prefetch~200/runCode change required in script
Convert reconcile-org-refs.sh REST loop → GraphQL~100/runCode change required
mirror-releases.yml: 12h → daily~50/dayReleases delayed up to 24h
resolve-ci.yml: remove daily schedule, trigger-only~60/dayFailures only resolved when check-ci fires
check-shell-tools-ci.yml: daily → every 2 days~25/dayShell tools CI lag
Add actions/cache@v5 to full-audit.yml (pyyaml)runner time onlyTrivial
Add actions/cache@v5 to check-accessibility.yml (pa11y)runner time onlyTrivial
sync-pieroproietti-gl-forks.sh: migrate to gh_getreliabilityRaw curl has no retry

Speed improvement opportunities

Scripts

ScriptIssueFix
sync-forks.shSequential REST calls per repoGraphQL prefetch for repo list + existence
reconcile-org-refs.shPaginated REST for org reposGraphQL batch query
update-readmes.shPer-repo /contents/README.md callsTree fetch with ?recursive=1 then filter
resolve-failures.shSequential per-repo run scanParallel with xargs -P 4 for log fetches
sync-pieroproietti-gl-forks.shRaw curl without retrySource includes/gh-api.sh, use gh_get

Workflows

WorkflowIssueFix
full-audit.ymlpip install pyyaml on every runAdd actions/cache@v5
check-accessibility.ymlnpm install -g pa11y on every runAdd actions/cache@v5
validate-config.yml✅ Already cached (pytest, yamllint, gavi)
critical-deploy*.ymlfetch-depth: 0 (full history)Only needed for git log — use fetch-depth: 1 + git fetch --unshallow only when needed
sync-shell-tools.ymlfetch-depth: 0Same as above
manage-subtrees.ymlfetch-depth: 0Required for subtree — keep
sync-uaa-vendor.ymlfetch-depth: 0Required for vendor merge — keep

Runner minutes

Public repos on GitHub get unlimited free runner minutes. If this repo is public, runner minutes are not a constraint. If private:

  • Current worst-case estimate: ~8,000 min/month (well over 2,000 free tier)
  • Self-hosted runner eliminates the cap entirely
  • Alternatively: reduce timeout-minutes on workflows that consistently finish in < 5 min but have 30 min timeouts — this doesn't save minutes (billing is actual runtime, not timeout) but prevents runaway jobs

Workflows with oversized timeouts relative to typical runtime:

Workflowtimeout-minutesTypical runtimeSuggested
Queue Manager10< 1 min5
Quota Reserve10< 1 min5
Auto-merge PRs151–3 min8
Rebase PRs202–5 min10
Reconcile Org Refs305–10 min15