fix(forge): config-repo main is fast-forward-only (no auto force-push)
Per operator directive: a silent automatic force-push is a bug. The config-repo merge path already never force-pushes (run_merge_config_pr lands via ff_push_to_main, a non-force git push). So set the branch protection's enable_force_push to false — main only ever advances by fast-forward. The legacy push_config mirror does force-push (it re-points the status tags and rewinds main on a failed-build rollback); the protection now rejects those non-ff updates, so the mirror runs best-effort until the agent-opened PR-merge flow retires it. Docs + comments updated to match.
This commit is contained in:
parent
60da6c14b4
commit
5caec9c1a9
2 changed files with 18 additions and 16 deletions
|
|
@ -56,10 +56,11 @@ Two things live in the `agent-configs` Forgejo organization:
|
|||
config-change branches and (once #1838 P2 lands) open config PRs — but
|
||||
`main` is branch-protected core-only: only hive-c0re's verify-and-ff-push
|
||||
merge handler lands on `main`, an operator-team approval is required, and
|
||||
the agent can neither push `main` directly nor self-merge. During the
|
||||
transition c0re still force-mirrors the agent's applied config repo here
|
||||
on each `↻ R3BU1LD` (the `enable_force_push` allowance), until the PR
|
||||
flow replaces that. Repos stay private, so an agent can't read another
|
||||
the agent can neither push `main` directly nor self-merge. `main` is
|
||||
fast-forward-only — no auto force-push (the merge handler's ff push lands
|
||||
fine; the legacy `push_config` mirror, which force-pushes to re-point status
|
||||
tags and rewind on rollback, runs best-effort until the PR flow retires it).
|
||||
Repos stay private, so an agent can't read another
|
||||
agent's config. (Agents remain read-only collaborators on `core/meta`.)
|
||||
- The dashboard links each container's "config" anchor to this
|
||||
config repo, so operators can click straight from the SW4RM tab into
|
||||
|
|
|
|||
|
|
@ -37,8 +37,9 @@ const CORE_TOKEN_PATH: &str = "/var/lib/hyperhive/forge-core-token";
|
|||
/// branch-protected core-only, so only hive-c0re's verify-and-ff-push merge
|
||||
/// handler lands on it (operator approval required; the agent can't push
|
||||
/// `main` or self-merge). The repos remain private, so an agent still can't
|
||||
/// reach *another* agent's config. During the transition `push_config` also
|
||||
/// force-mirrors `applied → main` here until the PR-merge flow replaces it.
|
||||
/// reach *another* agent's config. `main` is fast-forward-only (no auto
|
||||
/// force-push); the legacy `push_config` mirror runs best-effort until the
|
||||
/// PR-merge flow retires it.
|
||||
const CONFIG_ORG: &str = "agent-configs";
|
||||
/// Forgejo org hosting the operator-curated shared docs/skills repo
|
||||
/// that every agent gets read-only access to. Agents use it as a
|
||||
|
|
@ -708,7 +709,7 @@ pub async fn ensure_config_repo(name: &str) -> Result<()> {
|
|||
// Agent = write collaborator: it can push config-PR branches + open PRs,
|
||||
// but the branch protection below keeps it off `main` directly.
|
||||
add_collaborator(CONFIG_ORG, name, name, "write", &token).await?;
|
||||
// Protect `main` core-only (+ transitional force-push for push_config).
|
||||
// Protect `main` core-only, fast-forward-only (no auto force-push).
|
||||
apply_config_repo_branch_protection(name, &token).await
|
||||
}
|
||||
|
||||
|
|
@ -997,20 +998,20 @@ async fn apply_operator_branch_protection(repo: &str, token: &str) -> Result<()>
|
|||
/// (`run_merge_config_pr`). The agent can never push `main` directly.
|
||||
/// - **operator-team approval is required** to merge, and the author (not in
|
||||
/// the team) cannot self-approve.
|
||||
/// - **`enable_force_push` is a TRANSITIONAL allowance**: `push_config` still
|
||||
/// force-mirrors `applied → main` until the agent-opened PR-merge flow
|
||||
/// replaces it, and a protected branch otherwise rejects force-push.
|
||||
/// Forgejo's force-push allowlist is deploy-keys-only (no per-user list), so
|
||||
/// this is a plain `enable_force_push` toggle — only `core` is in the
|
||||
/// push-whitelist, so only `core` can force-push anyway. When `push_config`
|
||||
/// is retired, flip this to `false`; keep `core` in the push-whitelist so
|
||||
/// `ff_push_to_main` can still land fast-forwards.
|
||||
/// - **`enable_force_push` is `false`** — `main` only ever advances by
|
||||
/// fast-forward. The merge handler's `ff_push_to_main` is already a
|
||||
/// non-force push, so it lands fine. The legacy `push_config` mirror DOES
|
||||
/// force-push (it re-points status tags and rewinds `main` on a failed-build
|
||||
/// rollback), so the protection now rejects those non-ff updates — that
|
||||
/// mirror runs best-effort until the agent-opened PR-merge flow retires it.
|
||||
/// (Auto force-push is intentionally not allowed: per operator directive a
|
||||
/// silent force-push is a bug, not a feature.)
|
||||
///
|
||||
/// Idempotent: an existing rule for the branch (200/409/422) is success.
|
||||
async fn apply_config_repo_branch_protection(repo: &str, token: &str) -> Result<()> {
|
||||
let url = format!("{FORGE_HTTP}/api/v1/repos/{CONFIG_ORG}/{repo}/branch_protections");
|
||||
let body = format!(
|
||||
r#"{{"branch_name":"main","enable_push_whitelist":true,"push_whitelist_usernames":["core"],"enable_merge_whitelist":true,"merge_whitelist_usernames":["core"],"enable_approvals_whitelist":true,"approvals_whitelist_teams":["{OPERATORS_TEAM}"],"required_approvals":1,"block_on_official_review_requests":true,"allow_manual_merge":true,"enable_force_push":true}}"#
|
||||
r#"{{"branch_name":"main","enable_push_whitelist":true,"push_whitelist_usernames":["core"],"enable_merge_whitelist":true,"merge_whitelist_usernames":["core"],"enable_approvals_whitelist":true,"approvals_whitelist_teams":["{OPERATORS_TEAM}"],"required_approvals":1,"block_on_official_review_requests":true,"allow_manual_merge":true,"enable_force_push":false}}"#
|
||||
);
|
||||
let status = forge_http(reqwest::Method::POST, &url, token, &body).await?;
|
||||
match status.as_u16() {
|
||||
|
|
|
|||
Loading…
Reference in a new issue