From 32432a8ca310929c6acddc4a15000eee021bb0c2 Mon Sep 17 00:00:00 2001 From: Toni Bergholm Date: Mon, 9 Nov 2026 16:35:20 +0200 Subject: [PATCH 1/2] feat(forge): admin org create takes a visibility Codento Engine's Project host bootstrap creates its org as private; sc-forge hard-coded public, which would expose the org (and its member list) to every signed-in user after the migration (Engine ADR-0080, COD2-7139). The new --visibility flag takes Gitea's values and defaults to public as before. --- crates/forge/src/admin.rs | 9 ++++++--- crates/forge/src/cli.rs | 6 +++++- crates/forge/tests/e2e_a.rs | 6 +++++- docs/superpowers/specs/2026-10-22-p38-forge-design.md | 2 +- 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/crates/forge/src/admin.rs b/crates/forge/src/admin.rs index 2db53387..2151eb7c 100644 --- a/crates/forge/src/admin.rs +++ b/crates/forge/src/admin.rs @@ -103,11 +103,14 @@ pub fn repo_set_pull_floor(data_dir: &Path, owner: &str, name: &str, floor: i64) Ok(()) } -/// `admin org create`. -pub fn org_create(data_dir: &Path, name: &str, owner: &str) -> Result<()> { +/// `admin org create`, with Gitea's visibility values (`public`, `limited`, `private`). +pub fn org_create(data_dir: &Path, name: &str, owner: &str, visibility: &str) -> Result<()> { if names::check(name).is_err() { bail!("invalid or reserved organization name {name:?}"); } + if !crate::api::valid_visibility(visibility) { + bail!("invalid visibility {visibility:?}: use public, limited or private"); + } let mut conn = db::open_for_admin(data_dir)?; let owner = store::find_by_login(&conn, owner)? .filter(|u| !u.is_org) @@ -115,7 +118,7 @@ pub fn org_create(data_dir: &Path, name: &str, owner: &str) -> Result<()> { let acct = NewAccount { login: name.into(), is_org: true, - visibility: "public".into(), + visibility: visibility.into(), ..Default::default() }; match store::create_org(&mut conn, &acct, owner.id, now()) { diff --git a/crates/forge/src/cli.rs b/crates/forge/src/cli.rs index 46a8b524..8ab51266 100644 --- a/crates/forge/src/cli.rs +++ b/crates/forge/src/cli.rs @@ -192,6 +192,9 @@ enum OrgCmd { name: String, #[arg(long)] owner: String, + /// `public`, `limited` or `private`, as in Gitea. + #[arg(long, default_value = "public")] + visibility: String, #[arg(long)] data_dir: PathBuf, }, @@ -272,10 +275,11 @@ fn run_admin(cmd: AdminCmd) -> Result<()> { OrgCmd::Create { name, owner, + visibility, data_dir, }, } => { - admin::org_create(&data_dir, &name, &owner)?; + admin::org_create(&data_dir, &name, &owner, &visibility)?; println!("Organization '{name}' has been successfully created!"); } AdminCmd::Repo { diff --git a/crates/forge/tests/e2e_a.rs b/crates/forge/tests/e2e_a.rs index dbaee29b..2502c1b1 100644 --- a/crates/forge/tests/e2e_a.rs +++ b/crates/forge/tests/e2e_a.rs @@ -650,8 +650,12 @@ fn admin_cli_changes_are_visible_to_the_running_server() { .status, 201 ); - scl_forge::admin::org_create(&s.dir, "cliorg", "carol").unwrap(); + scl_forge::admin::org_create(&s.dir, "cliorg", "carol", "public").unwrap(); assert_eq!(s.get("/api/v1/orgs/cliorg", &token(&s.admin)).status, 200); + scl_forge::admin::org_create(&s.dir, "hidden", "carol", "private").unwrap(); + let r = s.get("/api/v1/orgs/hidden", &token(&s.admin)); + assert_eq!(r.json()["visibility"], "private"); + assert!(scl_forge::admin::org_create(&s.dir, "odd", "carol", "secret").is_err()); assert!(scl_forge::admin::user_create( &s.dir, "carol", diff --git a/docs/superpowers/specs/2026-10-22-p38-forge-design.md b/docs/superpowers/specs/2026-10-22-p38-forge-design.md index 1924ff59..d553393e 100644 --- a/docs/superpowers/specs/2026-10-22-p38-forge-design.md +++ b/docs/superpowers/specs/2026-10-22-p38-forge-design.md @@ -510,7 +510,7 @@ sc-forge serve --data-dir DIR --listen ADDR --public-url URL [--behind-proxy|--a sc-forge admin user create --username U --password P --email E [--admin] [--must-change-password=false] sc-forge admin user change-password --username U --password P [--must-change-password=false] sc-forge admin user generate-access-token --username U [--token-name N] [--scopes S,…] [--raw] -sc-forge admin org create --name O --owner U +sc-forge admin org create --name O --owner U [--visibility public|limited|private] sc-forge hook pre-receive|post-receive # internal ``` From 4b0695df672ad8b6330dcf53f9802a1b81d69ac1 Mon Sep 17 00:00:00 2001 From: Toni Bergholm Date: Mon, 9 Nov 2026 16:37:55 +0200 Subject: [PATCH 2/2] fix(forge): refuse brace and escape patterns in branch protection Gitea's glob syntax also gives { and \ a meaning, so a rule like {main,release} recreated from Gitea was stored as a literal branch name that protects nothing. Such names are now refused like the other patterns. --- crates/forge/src/protection.rs | 3 ++- crates/forge/tests/e2e_protection.rs | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/crates/forge/src/protection.rs b/crates/forge/src/protection.rs index f1535ebe..0b9ed56c 100644 --- a/crates/forge/src/protection.rs +++ b/crates/forge/src/protection.rs @@ -159,8 +159,9 @@ fn is_default(v: &Value) -> bool { } } +/// Does `s` use Gitea's glob syntax (its compiler gives `* ? [ { \\` a meaning)? fn has_glob(s: &str) -> bool { - s.contains(['*', '?', '[']) + s.contains(['*', '?', '[', '{', '\\']) } fn contexts_of(v: &Value) -> Result>, String> { diff --git a/crates/forge/tests/e2e_protection.rs b/crates/forge/tests/e2e_protection.rs index 0eb561f8..0c226128 100644 --- a/crates/forge/tests/e2e_protection.rs +++ b/crates/forge/tests/e2e_protection.rs @@ -163,6 +163,12 @@ fn rule_crud_validation_and_the_branch_object() { 422, "patterns are not supported", ), + ( + "brace glob", + json!({"branch_name": "{main,release}"}), + 422, + "patterns are not supported", + ), ( "approvals", json!({"branch_name": "x", "required_approvals": 2}),