Skip to content

feat(ci): branch-protection-sync follows .gitmodules — push + weekly triggers (BLOCKED on #2519) - #2528

Merged
POWERFULMOVES merged 1 commit into
mainfrom
feat/branch-protection-sync-triggers
Aug 18, 2026
Merged

POWERFULMOVES merged 1 commit into
mainfrom
feat/branch-protection-sync-triggers

Conversation

@POWERFULMOVES

Copy link
Copy Markdown
Owner

Read this before approving

1. Merging this PR arms a weekly cron. The schedule: block fires the next Monday 06:00 UTC without anyone doing anything. It is audit-only — it cannot write, by construction (see below) — but it will start running on its own. That is the intended behaviour, and it is being stated here rather than left to be discovered.

2. This PR does not fix the 15 ungated branches. It makes them impossible to miss. Closing the gaps is still a deliberate workflow_dispatch with dry_run: false, and the blast radius of that run is the rest of this description.

3. Blocked on #2519. Do not merge this first.

Blocked on #2519

#2519 (open, fix/defects-surfaced-by-2511) adds the five missing branch fields to .gitmodules:

 [submodule "PMOVES-obico-server"]
+	branch = release
 [submodule "PMOVES-moonraker-obico"]
+	branch = master
 [submodule "PMOVES-OrcaSlicer"]
+	branch = main
 [submodule "PMOVES-OctoPrint-Obico"]
+	branch = master
 [submodule "PMOVES-fluidd"]
+	branch = develop

Without it, the workflow's [ -z "$branch" ] && branch="main" default sends four of those five at a main branch that does not exist on them (verified per repo: obico-serverrelease, moonraker-obicomaster, OctoPrint-Obicomaster, fluidddevelop; only OrcaSlicer really is main). I reproduced the workflow's derive step against both versions of .gitmodules; the diff is exactly those four:

26c26  < PMOVES-fluidd            main      > PMOVES-fluidd            develop
39c39  < PMOVES-moonraker-obico   main      > PMOVES-moonraker-obico   master
42,43  < PMOVES-obico-server      main      > PMOVES-obico-server      release
       < PMOVES-OctoPrint-Obico   main      > PMOVES-OctoPrint-Obico   master

Everything below is computed against the post-#2519 state, since that is the state this workflow would operate in.

What changed in the workflow

Two triggers, and one safety fix that the triggers make mandatory.

  push:
    branches: [main]
    paths: ['.gitmodules']
  schedule:
    - cron: '0 6 * * 1'    # Mondays 06:00 UTC

push on .gitmodules is the primary vector — adding or re-pointing a submodule is precisely the event that drops it from coverage, so the check now runs on the commit that causes it. The weekly schedule is the backstop for drift that never touches .gitmodules: a branch renamed or deleted on a fork, protection removed by hand, a fork created outside a superproject commit. Weekly rather than daily because push already covers the frequent vector, and a daily audit nobody reads is noise rather than signal.

The safety fix, which is the part worth reviewing

inputs.* is empty on every trigger except workflow_dispatch, and the script tests:

if [ "$DRY_RUN" = "true" ]; then ... fi

So adding schedule:/push: naively would evaluate DRY_RUN="" as not a dry run and issue live PUTs across all 63 derived forks on the first tick — with ONLY_UNPROTECTED="" simultaneously disabling the gap-only filter, so it would rewrite already-protected branches too. On this fleet that single unattended run would lower required_approving_review_count from 1 to 0 on 41 repos.

Both flags are now pinned for non-dispatch events:

DRY_RUN:          ${{ github.event_name != 'workflow_dispatch' && 'true' || inputs.dry_run }}
ONLY_UNPROTECTED: ${{ github.event_name != 'workflow_dispatch' && 'true' || inputs.only_unprotected }}

Dispatch behaviour is unchanged (the input passes through, including dry_run: false). Automated runs can only audit, and only look at gaps.

Nothing else changes: same derivation, same policy body, same token scope, same preserve-existing-checks path.

Scope: 65 entries → 63 (repo, branch) pairs

The derive step keys on the URL basename, not the section name, then sort -u. Reproduced verbatim:

raw lines: 65
after sort -u: 63

This answers the double-registration question: no, they are not protected twice. PMOVES-Archon (registered as both PMOVES-Archon and pmoves/integrations/archon) and PMOVES-pinokio (registered as both PMOVES-pinokio and pbnj) each produce an identical repo<TAB>branch line, which sort -u collapses. Both pairs track the same branch, so there is no conflict to resolve. It is harmless today and would only matter if the two registrations ever disagreed on branch — at which point the derive step would emit two lines and the second PUT would silently win.

Blast radius of a live run

Beneficiaries — 13 pairs gain protection where there is none

These are the intended effect. All currently return Branch not protected.

Repo Consumed branch
PMOVES-ClawZ PMOVES.AI-Edition-Hardened
PMOVES-Danger-infra PMOVES.AI-Edition-Hardened
PMOVES-E2B-Danger-Room PMOVES.AI-Edition-Hardened
PMOVES-fluidd develop
PMOVES-headscale PMOVES.AI-Edition-Hardened
PMOVES-hermes-agent PMOVES.AI-Edition-Hardened
PMOVES-MAI-UI PMOVES.AI-Edition-Hardened
PMOVES-moonraker-obico master
PMOVES-obico-server release
PMOVES-OctoPrint-Obico master
PMOVES-ollama PMOVES.AI-Edition-Hardened
PMOVES-OrcaSlicer main
pmoves-pipecat PMOVES.AI-Edition-Hardened
That is 12 of the 15 ungated consumed branches from #2522, plus PMOVES-ClawZ (which has no classic protection but is covered by a ~ALL ruleset, so #2522 did not count it as ungated). The missing 3 are the no-branch cases below.

Each would receive: PR required (required_approving_review_count: 0, so automation can still self-merge), no force pushes, no deletions, conversation resolution required, enforce_admins: false, required_status_checks: null.

Would CHANGE existing protection — 41 pairs, one field

This is the question that matters. With only_unprotected: false, the run rewrites every derived branch from the policy body, and the policy is not a superset of what is there now.

Exactly one field regresses, on 41 of the 47 currently-protected pairs:

Repo Branch Current required_approving_review_count After a policy write
PMOVES-a0-plugins PMOVES.AI-Edition-Hardened 1 0
PMOVES-A2UI PMOVES.AI-Edition-Hardened 1 0
PMOVES-Agent-Zero PMOVES.AI-Edition-Hardened 1 0
PMOVES-AgentGym PMOVES.AI-Edition-Hardened 1 0
Pmoves-AgentGym-RL PMOVES.AI-Edition-Hardened 1 0
PMOVES-Archon PMOVES.AI-Edition-Hardened 1 0
PMOVES-autoresearch PMOVES.AI-Edition-Hardened 1 0
PMOVES-BoTZ PMOVES.AI-Edition-Hardened 1 0
PMOVES-BotZ-gateway PMOVES.AI-Edition-Hardened 1 0
Pmoves-cipher PMOVES.AI-Edition-Hardened 1 0
pmoves-cipher-mcp PMOVES.AI-Edition-Hardened 1 0
PMOVES-Creator PMOVES.AI-Edition-Hardened 1 0
PMOVES-Deep-Serch PMOVES.AI-Edition-Hardened 1 0
PMOVES-DoX PMOVES.AI-Edition-Hardened 1 0
PMOVES-E2B-Danger-Room-Desktop PMOVES.AI-Edition-Hardened 1 0
pmoves-e2b-mcp-server PMOVES.AI-Edition-Hardened 1 0
PMOVES-E2b-Spells PMOVES.AI-Edition-Hardened 1 0
Pmoves-Health-wger PMOVES.AI-Edition-Hardened 1 0
PMOVES-HiRAG PMOVES.AI-Edition-Hardened 1 0
Pmoves-hyperdimensions PMOVES.AI-Edition-Hardened 1 0
PMOVES-Jellyfin PMOVES.AI-Edition-Hardened 1 0
Pmoves-Jellyfin-AI-Media-Stack PMOVES.AI-Edition-Hardened 1 0
PMOVES-llama-throughput-lab PMOVES.AI-Edition-Hardened 1 0
PMOVES-n8n PMOVES.AI-Edition-Hardened 1 0
PMOVES-neo4j PMOVES.AI-Edition-Hardened 1 0
PMOVES-Open-Notebook PMOVES.AI-Edition-Hardened 1 0
PMOVES-OpenRoom PMOVES.AI-Edition-Hardened 1 0
PMOVES-pinokio PMOVES.AI-Edition-Hardened 1 0
PMOVES-Pinokio-Ultimate-TTS-Studio PMOVES.AI-Edition-Hardened 1 0
Pmoves-pretext PMOVES.AI-Edition-Hardened 1 0
PMOVES-Remote-View PMOVES.AI-Edition-Hardened 1 0
PMOVES-space-agent PMOVES.AI-Edition-Hardened 1 0
PMOVES-supabase PMOVES.AI-Edition-Hardened 1 0
PMOVES-surf PMOVES.AI-Edition-Hardened 1 0
PMOVES-Tailscale PMOVES.AI-Edition-Hardened 1 0
PMOVES-tensorzero PMOVES.AI-Edition-Hardened 1 0
PMOVES-ToKenism-Multi PMOVES.AI-Edition-Hardened 1 0
PMOVES-transcribe-and-fetch PMOVES.AI-Edition-Hardened 1 0
PMOVES-Ultimate-TTS-Studio PMOVES.AI-Edition-Hardened 1 0
PMOVES-Wealth PMOVES.AI-Edition-Hardened 1 0
PMOVES.YT PMOVES.AI-Edition-Hardened 1 0
The other 6 protected pairs are unaffected: five skills forks are already at 0, and PMOVES-crush has protection with no PR-review block at all (it would gain one, at 0, plus conversation resolution).

Every other field is safe. Checked across all 47 protected pairs:

Field Repos where a write would weaken it Why
enforce_admins 0 already false on all 47 — the policy value
require_code_owner_reviews 0 already false/unset on all 47
restrictions (push allowlist) 0 none of the 47 has one, so restrictions: null wipes nothing
required_linear_history 0 already false on all 47
required_signatures 0 not in the PUT body; managed by a separate endpoint and untouched
allow_force_pushes / allow_deletions 0 already false on all 47 — the policy is a no-op
required_conversation_resolution 0 true on 46, false on 1 → the policy raises it

On the #1767 preserve-existing-checks path

You asked me to confirm it still holds for every repo in scope. The precise answer is that it is not exercised by any repo in scope:

=== required_status_checks currently non-empty on the consumed branch ===
0

Zero of the 47 protected pairs has any required status check on its consumed branch. The forks that do have required checks — PMOVES-hermes-agent (9) and PMOVES-pinokio (CodeRabbit) — carry them on main, which is not the branch the monorepo consumes. This is the same wrong-branch pattern #2522 documented, showing up in the classic layer.

So the #1767 regression cannot repeat on this scope, but only because there is nothing to strip. The preserve path is untested here rather than proven; I am not claiming it works, I am reporting that it has no work to do. The first fork to gain a required check on its hardened branch will be the real test.

3 pairs will 404 — branch absent from the remote

Repo Tracked branch Repo default
pmoves-hirag-mcp PMOVES.AI-Edition-Hardened main
PMOVES-jcodemunch-mcp PMOVES.AI-Edition-Hardened main
PM-Spark-video-search-and-summarization PMOVES.AI-Edition-Hardened main

Not fixed by #2519, and not fixable here — you cannot protect a branch that does not exist. The workflow already handles this gracefully: 404 is caught per fork, tallied into FAILED, and annotated tracked branch '<b>' missing on <repo> (fix .gitmodules). It does not fail the run.

That is the right behaviour for a fleet sweep, but it means these three will show up as failed=3 on every audit until .gitmodules is corrected — an amber light that never goes green. Worth fixing in the same pass as #2519 rather than living with it.

Recommended arming sequence

  1. Merge fix: three defects on main surfaced by the #2511 review (scoring contract, orphan gitlink, branch policy) #2519 first.
  2. Merge this PR. The weekly audit begins; nothing is written.
  3. Read one audit run (the ~ repo@branch: UNPROTECTED -> WOULD apply lines are the gap list).
  4. When satisfied, arm it once by hand:
    • Actions → Branch Protection Sync → Run workflow
    • dry_run: false
    • only_unprotected: true ← this is what keeps the 41-repo downgrade off the table; it makes the run additive-only
  5. Fix the three missing branches in .gitmodules, then re-run.

only_unprotected: false is the mode to avoid unless the intent really is to reset all 63 to the policy baseline. It is the only path that produces the downgrade table above.

One thing this PR deliberately does not do

The scheduled audit reports into the run log and exits 0 even when it finds unprotected branches. It surfaces drift only to someone who opens the run. Making the dry-run exit non-zero when gaps exist would turn it into a real signal — but that changes what the workflow does, not just when it runs, so it is out of scope here and left as an operator call.

Method

Read-only against GitHub; the only write is the workflow file on this branch. Per (repo, branch) pair: GET repos/{slug}/branches/{branch} for existence and GET repos/{slug}/branches/{branch}/protection for the full current body, 63 pairs, paced. The derive step was reproduced verbatim from the workflow's own shell against both the current and post-#2519 .gitmodules, read from origin/main and origin/fix/defects-surfaced-by-2511 rather than a working tree. The workflow was not run.

🤖 Generated with Claude Code

…triggers

Coverage is derived from .gitmodules, so it has to follow .gitmodules. Until
now the workflow was workflow_dispatch-only and had run three times ever, last
on 2026-06-10 with failed=0. Every submodule added or re-pointed since fell out
of coverage silently: 15 consumed branches ended up with no protection at all,
not one of them because the workflow got anything wrong. A manual re-run fixes
today without fixing the class.

  push on .gitmodules  the primary vector — a submodule added or re-pointed is
                       exactly the event that drops it from coverage, so the
                       check runs on the commit that causes it
  schedule, weekly     backstop for drift that never touches .gitmodules: a
                       branch renamed or deleted on a fork, protection removed
                       by hand, a fork created outside a superproject commit.
                       Weekly rather than daily because push already covers the
                       frequent vector, and a daily audit nobody reads is noise

The non-obvious part is the env block. `inputs.*` is empty on any trigger other
than workflow_dispatch, and the script tests `[ "$DRY_RUN" = "true" ]` — so
adding a schedule/push trigger naively would evaluate DRY_RUN="" as "not a dry
run" and issue live PUTs across every derived fork on the first tick, with
ONLY_UNPROTECTED="" simultaneously disabling the gap-only filter. That is a
fleet-wide write nobody asked for, and on this fleet it would lower
required_approving_review_count from 1 to 0 on 41 repos.

Both flags are therefore pinned to "true" for non-dispatch events via
`github.event_name != 'workflow_dispatch' && 'true' || inputs.<x>`, which passes
the input through unchanged on dispatch and forces audit-only + gaps-only
otherwise. Automated runs report; writing stays a deliberate human act.

Nothing else about the workflow changes: same derivation, same policy, same
token scope, same preserve-existing-checks path.

Blocked on #2519, which adds the five missing .gitmodules branch fields. Without
it, four of those forks resolve to the "main" default and target a branch that
does not exist on them. Blast radius and the arming instructions are in the PR
body.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@POWERFULMOVES, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 41 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: b3a768c8-1a78-4239-a17d-a040e730122f

📥 Commits

Reviewing files that changed from the base of the PR and between beeee21 and a64f398.

📒 Files selected for processing (1)
  • .github/workflows/branch-protection-sync.yml

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added the workflows GitHub Actions workflows label Aug 10, 2026
@POWERFULMOVES

Copy link
Copy Markdown
Owner Author

Two clarifications on the arming question, since "merging arms a cron" deserves precision:

Nothing is armed right now. GitHub only honours schedule: from the default branch — a cron block on a PR branch never fires. So the weekly trigger cannot run until this merges to main, and reviewing this PR carries no risk of it firing mid-review.

The push trigger will not fire on this PR's own merge either. It is scoped to paths: ['.gitmodules'], and this PR touches only the workflow file. The first push run will be whenever .gitmodules next changes — most likely #2519 itself, if that merges after this one.

Which suggests a small refinement to the recommended sequence in the description: merging #2519 first, then this PR means the .gitmodules fix lands before the trigger exists, so no automatic run is provoked by it. Merging in the other order would have #2519's merge immediately trigger an audit run — harmless, since automated runs are audit-only, but worth knowing rather than being surprised by a workflow run appearing on an unrelated PR's merge.

CI on this PR: 19 pass, 0 fail. The emit lifecycle trail Archon flake did not hit this run — consistent with it being conditional on runner workspace reuse rather than content (see #2525 and #2522 finding 5).

@POWERFULMOVES
POWERFULMOVES merged commit 68e269c into main Aug 18, 2026
24 checks passed
@POWERFULMOVES
POWERFULMOVES deleted the feat/branch-protection-sync-triggers branch August 18, 2026 16:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

workflows GitHub Actions workflows

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant