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.

ci/workflow.go +60

@@ -88,6 +88,9 @@ var (
88 88 // dispatchInputRE is a name that can reach a shell as a variable when a step
89 89 // chooses to export the inputs it reads from the event payload.
90 90 dispatchInputRE = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_-]{0,31}$`)
91 // secretNameRE is a secret's name and therefore the name of the variable a step
92 // reads it as. Upper case only, so one value has one spelling.
93 secretNameRE = regexp.MustCompile(`^[A-Z][A-Z0-9_]{0,63}$`)
91 94 // pathGlob allows the two wildcards a path filter needs and nothing that could
92 95 // be read as a revision or escape the tree.
93 96 pathGlob = regexp.MustCompile(`^[A-Za-z0-9*?\[\]!._/-]+$`)
@@ -171,6 +174,12 @@ type Job struct {
171 174 ImageDefinition string
172 175 // ImageSHA is the digest of ImageDefinition.
173 176 ImageSHA string
177 // Secrets are the repository secrets this job reads, by name. Each name is
178 // also the variable a step finds it in, and the workflow carries names only:
179 // the instance delivers the values when the job is leased. A declared name the
180 // repository has no value for leaves the variable unset, so a secret can be
181 // optional and a required one is ${NAME:?} in the step.
182 Secrets []string
174 183 // Env is the environment every step of this job runs with, on top of the
175 184 // image's own. It is where a job opts in to things the runner provides but does
176 185 // not decide for it, such as the cache mounted at /cache.
@@ -728,9 +737,39 @@ type rawJob struct {
728 737 ImageFile string `yaml:"image-file"`
729 738 Artifacts []rawArtifact `yaml:"artifacts"`
730 739 Pages *rawPages `yaml:"pages"`
740 Secrets rawSecretNames `yaml:"secrets"`
731 741 Env map[string]string `yaml:"env"`
732 742 }
733 743
744 // rawSecretNames accepts one name or a list, the same way runs-on takes one label
745 // or a list: `secrets: DEPLOY_KEY` when there is one is the obvious thing to write.
746 // Only names are accepted here, so a value in a workflow file is a parse error
747 // rather than a secret that quietly ends up in Git.
748 type rawSecretNames []string
749
750 func (s *rawSecretNames) UnmarshalYAML(node *yaml.Node) error {
751 if node.Tag == "!!null" {
752 return nil
753 }
754 switch node.Kind {
755 case yaml.ScalarNode:
756 var one string
757 if err := node.Decode(&one); err != nil {
758 return err
759 }
760 *s = []string{one}
761 return nil
762 case yaml.SequenceNode:
763 var list []string
764 if err := node.Decode(&list); err != nil {
765 return err
766 }
767 *s = list
768 return nil
769 }
770 return fmt.Errorf("secrets takes a name or a list of names (secrets: DEPLOY_KEY, or secrets: [DEPLOY_KEY, TOKEN])")
771 }
772
734 773 // rawLabels accepts both spellings of runs-on, because both are the obvious way to
735 774 // write it: `runs-on: linux` when there is one label and `runs-on: [linux, arm64]`
736 775 // when there are several. Making the scalar a decode error would be a dialect
@@ -1089,6 +1128,27 @@ func Parse(file string, content string) (*Workflow, []string) {
1089 1128 }
1090 1129 job.Artifacts = append(job.Artifacts, Artifact{Path: p, Release: ar.Release})
1091 1130 }
1131 // Secrets are names. The value is the instance's, not the workflow's, so a
1132 // name that is not stored is not a push error: the job runs without that
1133 // variable and its log says which one was missing.
1134 seenSecrets := map[string]bool{}
1135 for _, raw := range jr.Secrets {
1136 name := strings.TrimSpace(raw)
1137 if err := ValidateSecretName(name); err != nil {
1138 bad("jobs.%s.secrets: %v", id, err)
1139 continue
1140 }
1141 if reservedEnv[name] {
1142 bad("jobs.%s.secrets: %s is set by the runner, so a secret cannot replace it", id, name)
1143 continue
1144 }
1145 if seenSecrets[name] {
1146 bad("jobs.%s.secrets: %s is listed twice", id, name)
1147 continue
1148 }
1149 seenSecrets[name] = true
1150 job.Secrets = append(job.Secrets, name)
1151 }
1092 1152 // The job's own environment. The runner sets a small fixed set (CI,
1093 1153 // VOLE_CI, HOME, TMPDIR, GIT_TERMINAL_PROMPT) that the sandbox depends on,
1094 1154 // so a workflow cannot replace those: the refusal names the variable rather