OXYGENHUB

One approval by default only where someone else can give it ff6569530173

A new organisation repository requires one sealed approval only when
the organisation has two or more members; nobody may approve their own
proposal, so a one-person organisation (doomsdayone) could never land
one. Existing organisation repositories of fewer than two members that
the previous default moved to one go back to none.

refs #64
Author
Andy McCall @andymccall <[email protected]>
When
· 2026-09-30 16:10 UTC
Signature
✔ signed by Andy McCall
Hash
ff656953017324d89ff345da2edecab2eec2f200d4f87bd01d485294f33b8b36
Depends on
19bb31ff1f5c 462f88d161ce 8fb35df7d66e 93be9ce4653e d23464c0ac60
Depended on by
dd0788b756f2 5d90e0e045e2
In bookmarks
api-unknown-fields cancel-landed-runs clear-waiting close-proposal-140 diffusion-pin-bookmark draft-ready image-check job-waits main oh-comment-body-file proposal-list-fast run-one-job wiki-says-so
In tags
v0.1.0 v0.2.0 v0.3.0 v0.3.1 v0.4.0 v0.4.1 v0.4.2 v0.5.0 v0.5.1 v0.5.2 v0.6.0 v0.6.1 v0.7.0 v0.8.0 v0.9.0 v0.9.1 v0.9.11 v0.9.15 v0.9.16 v0.9.19 v0.9.2 v0.9.20 v0.9.21 v0.9.22 v0.9.23 v0.9.24 v0.9.3 v0.9.4 v0.9.5 v0.9.6 v0.9.7 v0.9.8 v0.9.9
Edits
8 atoms, 1 tree operation, 4 files

Browse the tree at this change

added crates/hub/migrations/0026_org_approvals_need_reviewers.sql +9 −0
1 +
-- One approval is required by default only where someone else can give it
2 +
-- (oxygenhub#64): an organisation with fewer than two members goes back to
3 +
-- none, as a personal repository has.
4 +
UPDATE repositories SET required_approvals = 0
5 +
    WHERE required_approvals = 1
6 +
      AND owner IN (
7 +
        SELECT o.name FROM organisations o
8 +
        WHERE (SELECT COUNT(*) FROM org_members m WHERE m.org_id = o.id) < 2
9 +
      );
modified crates/hub/src/db/repos.rs +4 −3
…
96 unchanged lines
97 97
    .await
98 98
}
99 99
100 -
/// Create a repository row. One under an organisation requires one sealed
101 -
/// approval to land a proposal (oxygenhub#64); a personal one requires none.
100 +
/// Create a repository row. One under an organisation of two or more
101 +
/// members requires one sealed approval to land a proposal (oxygenhub#64),
102 +
/// since someone other than the author can then give it; others require none.
102 103
pub async fn create(pool: &Pool, owner: &str, name: &str, description: &str, visibility: Visibility) -> sqlx::Result<RepoRow> {
103 104
    let now = now_ms();
104 105
    sqlx::query_as(
105 106
        "INSERT INTO repositories (owner, name, description, visibility, created_ms, updated_ms, required_approvals) \
106 -
         VALUES (?, ?, ?, ?, ?, ?, (SELECT COUNT(*) > 0 FROM organisations WHERE name = ?)) RETURNING *",
107 +
         VALUES (?, ?, ?, ?, ?, ?, (SELECT COUNT(*) >= 2 FROM org_members m JOIN organisations o ON o.id = m.org_id WHERE o.name = ?)) RETURNING *",
107 108
    )
108 109
    .bind(owner)
109 110
    .bind(name)
…
159 unchanged lines
modified crates/hub/tests/accounts.rs +9 −4
…
399 unchanged lines
400 400
    let me = db::accounts::account_by_name(&app.db, "andy").await.unwrap().unwrap();
401 401
    let org = db::accounts::create_org(&app.db, "acme", "Acme", me.id).await.unwrap();
402 402
403 -
    // An organisation's repository requires one sealed approval to land a
404 -
    // proposal; a personal one requires none (oxygenhub#64).
403 +
    // A repository requires one sealed approval by default only where
404 +
    // someone else can give it: an organisation of two or more members
405 +
    // (oxygenhub#64). Acme has one member so far, like a personal account.
406 +
    let alone = db::repos::create(&app.db, "acme", "alone", "", hub_api::Visibility::Public).await.unwrap();
407 +
    let solo = db::repos::create(&app.db, "andy", "solo", "", hub_api::Visibility::Public).await.unwrap();
408 +
    assert_eq!(db::proposals::requirements(&app.db, alone.id).await.unwrap().required_approvals, 0);
409 +
    assert_eq!(db::proposals::requirements(&app.db, solo.id).await.unwrap().required_approvals, 0);
410 +
    let bobby = db::accounts::create_account(&app.db, "bobby", "[email protected]", "x", "salt", false).await.unwrap();
411 +
    db::accounts::set_org_member(&app.db, org.id, bobby.id, "member").await.unwrap();
405 412
    let tools = db::repos::create(&app.db, "acme", "tools", "", hub_api::Visibility::Public).await.unwrap();
406 -
    let solo = db::repos::create(&app.db, "andy", "solo", "", hub_api::Visibility::Public).await.unwrap();
407 413
    assert_eq!(db::proposals::requirements(&app.db, tools.id).await.unwrap().required_approvals, 1);
408 -
    assert_eq!(db::proposals::requirements(&app.db, solo.id).await.unwrap().required_approvals, 0);
409 414
410 415
    // Organisation webhooks: add, ping, deliveries; only for this organisation.
411 416
    let page = get(app, "/orgs/acme/settings", Some(&andy)).await;
…
65 unchanged lines
modified docs/spec.md +1 −1
…
344 unchanged lines
345 345
346 346
A review is `comment`, `request changes`, or `approve`. An approval MUST be backed by a **seal attestation** (spec §14.4, `kind = seal`) over the proposal's change hashes, signed with a key linked to the reviewer's account. Private keys never enter the browser, so the reviewer signs from a client: `oxy seal --proposal <url>` or `oxy seal <hashes>` (§18, a new command; the seal object already exists in the model) signs and pushes the seal; the hub matches the seal to the proposal and records the approval. The page shows the command ready to copy. An approval not yet backed by a seal is shown as "approval pending signature" and does not count towards requirements.
347 347
348 -
Landing requirements per repository (settings): N approvals, approvals from code owners (`.oxyowners` in the target state, GitHub `CODEOWNERS` syntax), required checks green on the projected state, no conflicts (or conflicts allowed, matching the policy rule's `allow_conflicts`), and no outstanding "request changes". A repository under an organisation starts with one approval required, a personal one with none; an admin changes either in Settings. When none is required and nobody has approved, the proposal page says so beside Land, and Land asks for confirmation.
348 +
Landing requirements per repository (settings): N approvals, approvals from code owners (`.oxyowners` in the target state, GitHub `CODEOWNERS` syntax), required checks green on the projected state, no conflicts (or conflicts allowed, matching the policy rule's `allow_conflicts`), and no outstanding "request changes". A repository under an organisation of two or more members starts with one approval required (someone other than the author can give it; nobody may approve their own proposal), any other with none; an admin changes either in Settings. When none is required and nobody has approved, the proposal page says so beside Land, and Land asks for confirmation.
349 349
350 350
### 7.5 Stacked proposals
351 351
…
216 unchanged lines