Skip to content

feat(distribute): predict protected branches before any write on GitLab and Gitea - #18

Merged
AlexeyShalaev merged 2 commits into
masterfrom
feat/push-guard
Oct 8, 2026
Merged

AlexeyShalaev merged 2 commits into
masterfrom
feat/push-guard

Conversation

@AlexeyShalaev

Copy link
Copy Markdown
Member

distribute --dry-run promised to predict branch rules with the write account, but on GitLab, Gitea and Forgejo a protected sync branch showed only at the push. This predicts it, and fixes how doctor judges GitLab's protected branches.

Protected branches before any write. A new optional platform.PushGuard lists the branches a protection rule keeps the write identity from pushing to at all:

  • GitLab's writer reads the project's protected branches: one request, plus the project and the writer's access level only when a rule matches a sync branch;
  • Gitea's and Forgejo's read GET branches/{b} of each sync branch that exists (protected, user_can_push, which the server works out for the caller). The API knows existing branches only, so a protection of a sync branch not created yet still shows at its first push.

When a decision pushes, distribute and distribute --dry-run ask the writer and block the target rules:protected-branch before any write, with a warning naming the branch and, where the platform shows it, the rule. Memory upkeep stays, and an open pull request is left as it is. What the writer cannot read still meets the rule at the push, which refuses it with nothing written. plan, whose reader is no PushGuard, is unchanged.

GitLab: the most permissive rule wins. GitLab applies the most permissive of the protected branches that match a branch, to pushes and force pushes alike (protected branches, protection rules). doctor took the strictest instead, so it could fail a branch another rule lets the writer push to, or warn about force pushes another rule allows. Both checks now share one verdict per branch. The live test confirms it on CE 18.11: a sync branch under touchmark/* for Maintainers alone and its own rule for Developers with force pushes is delivered and rebuilt.

Tests.

  • Unit: the drivers' NoPush and GitLab's verdict over several rules.
  • Conformance: a PushGuard case.
  • Fake platform: Ruleset.NoPush refuses pushes with GitLab's message, and WithPushGuard makes the writer report them.
  • TestRunProtectedBranch: a dry run and distribute blocked before writing, an open pull request kept, and the push-time refusal without the guard or with hidden rules.
  • Live TestProtectedBranch: on GitLab, a dry run predicting the block and the permissive pair. A new test on Gitea and Forgejo: a protection added after the pull request opened is predicted, and one before the sync branch exists is refused at the push.

Checked locally.

  • The unit suites pass in golang:1.27 with -race: distribute, platform/..., cli, report.
  • On GitLab CE 18.11.12, scripts/e2e/gitlab.sh --run 'TestProtectedBranch|TestDoctor|TestConformance' passes.
  • scripts/e2e/gitea.sh --run 'TestProtectedBranch|TestDistribute' passes on Gitea 28.1.0 and 1.27.3 and Forgejo 16.0.5.

…ab and Gitea

A new optional platform.PushGuard tells which branches a protection rule keeps the write identity from pushing to at all. GitLab's writer reads the project's protected branches (one request; the project and its access level only when a rule matches the sync branch); Gitea's and Forgejo's read GET branches/{b} of each sync branch that exists (protected, user_can_push). When a decision pushes, distribute and distribute --dry-run ask it and block the target rules:protected-branch before any write, with a warning naming the rule; memory upkeep stays. Rules the writer cannot read still show at the push, refused before anything is written. plan, whose reader is no PushGuard, is unchanged.

GitLab applies the most permissive of the protected branches that match a branch, to pushes and force pushes alike (docs.gitlab.com, protected branches and protection rules). doctor's rules check took the strictest instead and could fail a branch another rule lets the writer push to, or warn about force pushes another rule allows; both checks now use one verdict per branch.

The fake platform models it: a Ruleset with NoPush refuses every push with GitLab's message, and WithPushGuard makes the writer report those branches. Tests: the drivers' NoPush and GitLab's verdict over several rules, a conformance case, TestRunProtectedBranch (dry run and distribute blocked, an open pull request kept, the push-time refusal without the guard or with hidden rules), and the live TestProtectedBranch on GitLab (a dry run, and two rules where the permissive one wins) and on Gitea and Forgejo.
Gitea and Forgejo leave effective_branch_protection_name out for a writer without admin rights, so a protected branch's warning names the branch alone. The live tests now expect that, and a dry run on GitLab reports opened with an empty reason.
@AlexeyShalaev
AlexeyShalaev merged commit f676d34 into master Oct 8, 2026
22 checks passed
@AlexeyShalaev
AlexeyShalaev deleted the feat/push-guard branch October 8, 2026 12:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant