Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

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.

Give a job a credential for the bot it acts as

6e1fca5115b709d31eeb51864b187d79aa104e5f · Keenan Nemetz · 6d ago · parent 446542e6

Give a job a credential for the bot it acts as

A workflow can name a bot account with `bot:`. When the job is leased the
instance generates a key for that run, stores the public half with the run,
repository and expiry, and sends the private half on the lease channel. The
runner writes it into the job's run directory with a known_hosts entry for the
instance, so a step's `git push` or `vole issue comment` authenticates as the
account and nothing has to be wired per job.

The account's role is the permission, in full. The credential replaces exactly one
requirement — that writing to the issues branch needs `maintain` — and adds no rule
of its own. What a job may write there is what its account may do, asked the same
way the browser asks: discuss, file an issue, manage an item it wrote, open a
change from a branch it may push, triage and approve when the role allows it. Those
are the ACL's own capabilities (`CanDiscuss`, `CanOpenRequest`, `CanTriage`,
`CanApprove`), so a maintainer's job may do everything a maintainer's key may do,
including labels, states, and other people's items.

A session is scoped only when the fingerprint it authenticated with is a live
credential row; an expired one reads as absent, so the push falls back to the
account's own role and is refused where a contribute account cannot write.

Two things end the key: the run finishing, which drops the row, and the expiry,
which is what stops a credential whose runner was killed and never reported. The
key is also masked in the log like a secret, since a step that cats it should not
leave it in the attempt.

Closes #165.

ci/workflow.go +25

@@ -116,6 +116,13 @@ var (
116 116 "GIT_TERMINAL_PROMPT": true,
117 117 "HOME": true,
118 118 "TMPDIR": true,
119 // Set by the runner for a job that acts as a bot, and never by a workflow:
120 // they name the run and point git at the credential, and a workflow that
121 // could replace them could redirect the push.
122 "VOLE_RUN_ID": true,
123 "VOLE_REPO": true,
124 "VOLE_SSH_COMMAND": true,
125 "VOLE_KNOWN_HOSTS": true,
119 126 }
120 127 )
121 128
@@ -234,6 +241,12 @@ type Job struct {
234 241 // step that declares none runs with whatever is left. Zero keeps the behaviour a
235 242 // workflow had before the field: each step is bounded only by its own timeout.
236 243 Timeout time.Duration
244 // Bot names an account the job acts as, or is empty. The account must be a bot
245 // account, and the instance issues the run a credential for it, so a step can
246 // comment, open a change, or push a branch as that account without a long-lived
247 // key in a repository secret. The workflow carries the name only; the credential
248 // is minted when the job is leased and dies with the run.
249 Bot string
237 250 // Image is the image the job runs in. It is either a registry reference from
238 251 // `image:` or `file:<name>` for a definition under .vole/images.
239 252 Image string
@@ -807,6 +820,7 @@ type rawJob struct {
807 820 RunsOn rawLabels `yaml:"runs-on"`
808 821 Network string `yaml:"network"`
809 822 Timeout string `yaml:"timeout"`
823 Bot string `yaml:"bot"`
810 824 Image string `yaml:"image"`
811 825 ImageFile string `yaml:"image-file"`
812 826 Artifacts []rawArtifact `yaml:"artifacts"`
@@ -1167,6 +1181,17 @@ func Parse(file string, content string) (*Workflow, []string) {
1167 1181 job.Network = network
1168 1182 }
1169 1183 }
1184 // bot names the account the job acts as. The name is checked here so a typo
1185 // fails the push; whether it is a bot account, is active, and may act in this
1186 // repository is checked when the job is leased, because a roster changes
1187 // without a push.
1188 if bot := strings.TrimSpace(jr.Bot); bot != "" {
1189 if !accountRE.MatchString(bot) {
1190 bad("jobs.%s.bot: %q is not an account name (2-32 characters: lowercase letters, digits, - and _)", id, bot)
1191 } else {
1192 job.Bot = bot
1193 }
1194 }
1170 1195 // A job budget is the clock its steps share. It is optional: without one each
1171 1196 // step is still bounded, which is how every workflow worked before the field.
1172 1197 if raw := strings.TrimSpace(jr.Timeout); raw != "" {