vole
Clone
Read only, no credentials:
https://rodent.software/git/vole.git
Write over SSH with a registered key:
git clone ssh://git@0.0.0.0:2222/vole.git
A free and open-source forge that lives in your Git repo.
Let an account be a bot
29a795b9d5b79cf5c8f2b7f3b4f6572f7cdff444 · Keenan Nemetz · 6d ago · parent e7aa0785
Let an account be a bot An automation needs an identity: something to assign an issue to, mention in a comment, and name in an approval and the audit log. An account already gives all of that, so this adds only the invariants a bot needs and the word for it. A bot is created with `vole admin account create --bot`, with the repository role the admin decides it should have. It has no browser sign-in — `account invite` and adding a passkey are refused — and it cannot hold the instance-admin role, because Admin reaches every repository and every instance surface while a bot is a repository automation. It is marked as a bot in the user directory and on its profile. Everything else is deliberately unchanged. A bot obeys the rules of its role: a contribute bot pushes only its own contributor branch and its CI waits for a maintainer to allow the tip; a maintain bot pushes shared branches, runs CI immediately, and its approval counts. The kind is fixed at creation, like the name, and a roster that predates it reads every account as a person. This is the first of three slices of #165. The per-run credential that lets a workflow act as a bot without a long-lived key in a repository secret is next; this slice changes no push rule and no permission.
cmd/vole/account.go +19−6
| @@ -16,7 +16,7 @@ import ( | |||
| 16 | 16 | // the shortest useful summary in adminUsage; this is what `-h` expands. | |
| 17 | 17 | const accountUsage = `usage: vole admin account <command> [options] [args] | |
| 18 | 18 | ||
| 19 | create [--key <file|->] [--role <role>] <name> | ||
| 19 | create [--key <file|->] [--role <role>] [--bot] <name> | ||
| 20 | 20 | invite <name> | |
| 21 | 21 | role <name> <none|read|contribute|maintain|admin> | |
| 22 | 22 | credentials <name> | |
| @@ -31,7 +31,7 @@ const accountUsage = `usage: vole admin account <command> [options] [args] | |||
| 31 | 31 | list | |
| 32 | 32 | backup <path>` | |
| 33 | 33 | ||
| 34 | const accountUsageCreate = "usage: vole admin account create [--key <file|->] [--role <none|read|contribute|maintain|admin>] <name>" | ||
| 34 | const accountUsageCreate = "usage: vole admin account create [--key <file|->] [--role <none|read|contribute|maintain|admin>] [--bot] <name>" | ||
| 35 | 35 | ||
| 36 | 36 | // readKeyInput reads one authorized_keys line from a file, or from stdin when | |
| 37 | 37 | // src is empty or "-". | |
| @@ -77,7 +77,7 @@ func cmdAccount(args []string) error { | |||
| 77 | 77 | sub := args[0] | |
| 78 | 78 | f, err := parseFlags(args[1:], configFlag(), | |
| 79 | 79 | flagSpec{long: "key", short: "k"}, flagSpec{long: "all", boolean: true}, | |
| 80 | flagSpec{long: "role"}) | ||
| 80 | flagSpec{long: "role"}, flagSpec{long: "bot", boolean: true}) | ||
| 81 | 81 | if err != nil { | |
| 82 | 82 | return err | |
| 83 | 83 | } | |
| @@ -91,7 +91,7 @@ func cmdAccount(args []string) error { | |||
| 91 | 91 | return nil | |
| 92 | 92 | } | |
| 93 | 93 | accountFlags := map[string][]string{ | |
| 94 | "create": {"config", "key", "role"}, | ||
| 94 | "create": {"config", "key", "role", "bot"}, | ||
| 95 | 95 | "invite": {"config"}, | |
| 96 | 96 | "role": {"config"}, | |
| 97 | 97 | "credentials": {"config"}, | |
| @@ -138,13 +138,20 @@ func cmdAccount(args []string) error { | |||
| 138 | 138 | if err != nil { | |
| 139 | 139 | return err | |
| 140 | 140 | } | |
| 141 | // A bot and a person are created the same way except for one thing: a bot | ||
| 142 | // has no browser sign-in, so it can never be given an enrollment link. The | ||
| 143 | // create functions have the same shape, so the kind picks which one. | ||
| 144 | create := st.Identity.CreateWithRole | ||
| 145 | if f.has("bot") { | ||
| 146 | create = st.Identity.CreateBot | ||
| 147 | } | ||
| 141 | 148 | if !f.given("key") && !stdinIsTerminal() { | |
| 142 | 149 | key, err := readKeyInput("") | |
| 143 | 150 | if err != nil { | |
| 144 | 151 | return err | |
| 145 | 152 | } | |
| 146 | 153 | if strings.TrimSpace(key) != "" { | |
| 147 | a, err := st.Identity.CreateWithRole(f.rest[0], role, key, actor) | ||
| 154 | a, err := create(f.rest[0], role, key, actor) | ||
| 148 | 155 | if err != nil { | |
| 149 | 156 | return err | |
| 150 | 157 | } | |
| @@ -156,6 +163,12 @@ func cmdAccount(args []string) error { | |||
| 156 | 163 | } | |
| 157 | 164 | } | |
| 158 | 165 | if !f.given("key") { | |
| 166 | if f.has("bot") { | ||
| 167 | // An enrollment link is the only way a keyless account gets in, and | ||
| 168 | // a bot cannot use one. Refuse rather than create an account nobody | ||
| 169 | // can act as. | ||
| 170 | return usageErr{msg: "a bot account has no browser sign-in, so it needs a key: pass --key <file|->"} | ||
| 171 | } | ||
| 159 | 172 | // No key means a browser-only account: create it with a one-time | |
| 160 | 173 | // enrollment link so the name is never claimed by an account that has | |
| 161 | 174 | // no way in. | |
| @@ -171,7 +184,7 @@ func cmdAccount(args []string) error { | |||
| 171 | 184 | if err != nil { | |
| 172 | 185 | return err | |
| 173 | 186 | } | |
| 174 | a, err := st.Identity.CreateWithRole(f.rest[0], role, key, actor) | ||
| 187 | a, err := create(f.rest[0], role, key, actor) | ||
| 175 | 188 | if err != nil { | |
| 176 | 189 | return err | |
| 177 | 190 | } | |