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.

Let a job declare the secrets it reads

4ac42680eca3181efc70099f65d6e2c9f45e9dd2 · Keenan Nemetz · 7d ago · parent 0c40a1d3

Let a job declare the secrets it reads

A job now names the repository secrets it needs, and the instance delivers
those values when the job is leased. The workflow carries names only, so the
queue row, the metadata branch and the run record never hold a value: the
record names the version of each value the job ran with, which is what makes a
result say which one it used.

    jobs:
      deploy:
        image: alpine:3.22
        secrets: [DEPLOY_KEY]

A name the repository has no value for is not a failure. The variable is left
unset rather than empty, the step's shell can tell the difference, and the
runner writes one line to the log saying which value was missing. That keeps
an optional secret optional and leaves "required" with the step, where someone
can write the message: ${DEPLOY_KEY:?DEPLOY_KEY is not set}.

The value reaches the step through a file the runner writes into the run
directory it already mounts at /run/vole for an event payload, and each step is
prefixed with `. /run/vole/secrets.env`. That is deliberately not an env-file
argument to the runtime, which cannot carry a line break, and not --env, which
puts the value in the process's argument list where every process on the host
can read it. A local `vole workflow run` writes the same file, reading the
values with the developer's own credential, and says which ones it could not
read.

The name rule and the 128 KiB limit moved to ci, next to the environment rules
they mirror, so the store refuses a name no workflow could declare and the
workflow refuses a name no process environment could carry.

Slice 2 of #154.

server/ssh/runner.go +26−1

@@ -117,6 +117,20 @@ type runnerLogChunk struct {
117 117 Data []byte `json:"data"`
118 118 }
119 119
120 // runnerSecret is one delivered value and the version it came from, so the attempt
121 // record can say which value ran without carrying it.
122 type runnerSecret struct {
123 Version int `json:"version"`
124 Value string `json:"value"`
125 }
126
127 // runnerSecretIssue is a declared secret the repository could not supply. Reason is
128 // empty when the reason is simply that nobody has stored that name.
129 type runnerSecretIssue struct {
130 Name string `json:"name"`
131 Reason string `json:"reason,omitempty"`
132 }
133
120 134 type runnerStep struct {
121 135 Name string `json:"name"`
122 136 Run string `json:"run"`
@@ -155,6 +169,12 @@ type runnerJob struct {
155 169 ImageSHA string `json:"image_sha,omitempty"`
156 170 // Env is the job's declared environment, pinned when the run was queued.
157 171 Env map[string]string `json:"env,omitempty"`
172 // Secrets are the values the job declared that this repository has, and Missing
173 // are the names it declared that it does not. A missing name is not a failure:
174 // the variable is left unset and the runner says so at the top of the log. The
175 // values travel here and nowhere else — the queue row holds the names only.
176 Secrets map[string]runnerSecret `json:"secrets,omitempty"`
177 Missing []runnerSecretIssue `json:"missing_secrets,omitempty"`
158 178 // Artifacts are the declared patterns the runner expands and uploads after the
159 179 // steps succeed.
160 180 Artifacts []string `json:"artifacts,omitempty"`
@@ -292,6 +312,10 @@ func (s *Server) serveRunner(ch xssh.Channel, user string) {
292 312 _ = enc.Encode(runnerReply{Op: "lease", OK: true})
293 313 continue
294 314 }
315 // The values are resolved here, at the moment the job is handed out, so
316 // the queue never holds one and a rotation between queueing and leasing
317 // delivers the value the job actually ran with.
318 secrets, missing := s.leaseSecrets(run, user)
295 319 job := &runnerJob{
296 320 ID: run.ID, Repo: run.Repo, SHA: run.SHA, Attempt: run.Attempts, Branch: run.Branch,
297 321 Ref: run.Ref, Workflow: run.Workflow, WorkflowSHA: run.WorkflowSHA,
@@ -300,7 +324,8 @@ func (s *Server) serveRunner(ch xssh.Channel, user string) {
300 324 Image: run.Image, ImageFile: run.ImageFile, ImageDefinition: run.ImageDefinition,
301 325 ImageSHA: run.ImageSHA,
302 326 Env: run.Env,
303 Tag: run.Tag, TagAnnotated: run.TagAnnotated, ReleaseArtifacts: run.ReleaseArtifacts,
327 Secrets: secrets, Missing: missing,
328 Tag: run.Tag, TagAnnotated: run.TagAnnotated, ReleaseArtifacts: run.ReleaseArtifacts,
304 329 }
305 330 if run.PagesPath != "" {
306 331 ref := cfg.PagesRef