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 |