runner: GITBAY_SSH names the instance as the build reaches it !363

merged merged by cmc on 2026-09-10 03:22 UTC · krz/gitbay:build-ssh-env into main

3 files changed, +62 −8

Layout: unified · split

.gitbay/wiki/Users.org +7 −1
@@ -468,7 +468,13 @@ the web) and a =ci/<job>= commit status, which =repo settings
468require-checks= can gate merges on. Steps run with =sh -c= on the 468require-checks= can gate merges on. Steps run with =sh -c= on the
469instance's runner, stopping at the first failure; a broken config 469instance's runner, stopping at the first failure; a broken config
470surfaces as a failed =ci/config= status. Environment: =GITBAY_REPO=, 470surfaces as a failed =ci/config= status. Environment: =GITBAY_REPO=,
471=GITBAY_SHA=, =GITBAY_REF=, =GITBAY_JOB=, =CI=true=. 471=GITBAY_SHA=, =GITBAY_REF=, =GITBAY_JOB=, =CI=true=, and =GITBAY_SSH=,
472the instance's ssh destination as the build reaches it (=git@gitbay.org=
473from a runner elsewhere; inside a container on the server's own runner
474the host is at a private address the runner fills in). A job that
475talks back to the instance — a release asset, a comment, a push to a
476pages branch — uses =$GITBAY_SSH= with a key it holds as a secret;
477the build's container has no key of its own.
472 478
473Secrets: =repo secret set <owner/name> <NAME>= reads the value from 479Secrets: =repo secret set <owner/name> <NAME>= reads the value from
474stdin (never argv) and injects it into the repo's builds as =$NAME=; 480stdin (never argv) and injects it into the repo's builds as =$NAME=;
cmd/gitbay-runner/env_test.go +29 −5
@@ -13,7 +13,7 @@ func TestStepEnvDoesNotInherit(t *testing.T) {
13 t.Setenv("GITBAY_RUNNER_TOKEN", "a-secret-the-service-was-given") 13 t.Setenv("GITBAY_RUNNER_TOKEN", "a-secret-the-service-was-given")
14 t.Setenv("AWS_SECRET_ACCESS_KEY", "also-not-for-builds") 14 t.Setenv("AWS_SECRET_ACCESS_KEY", "also-not-for-builds")
15 15
16 env := stepEnv(job{Repo: "alice/app", SHA: "abc", Ref: "main", Job: "test"}, "/tmp/buildhome") 16 env := stepEnv(job{Repo: "alice/app", SHA: "abc", Ref: "main", Job: "test"}, "/tmp/buildhome", "git@x.test")
17 17
18 for _, e := range env { 18 for _, e := range env {
19 if strings.HasPrefix(e, "GITBAY_RUNNER_TOKEN=") || strings.HasPrefix(e, "AWS_SECRET_ACCESS_KEY=") { 19 if strings.HasPrefix(e, "GITBAY_RUNNER_TOKEN=") || strings.HasPrefix(e, "AWS_SECRET_ACCESS_KEY=") {
@@ -47,11 +47,11 @@ func TestStepEnvDoesNotInherit(t *testing.T) {
47// Secrets are passed through when the server sent them, which it does 47// Secrets are passed through when the server sent them, which it does
48// only for a trusted build. 48// only for a trusted build.
49func TestStepEnvCarriesSecrets(t *testing.T) { 49func TestStepEnvCarriesSecrets(t *testing.T) {
50 env := stepEnv(job{Secrets: map[string]string{"TOKEN": "s3cret"}}, "/tmp/buildhome") 50 env := stepEnv(job{Secrets: map[string]string{"TOKEN": "s3cret"}}, "/tmp/buildhome", "git@x.test")
51 if !containsEnv(env, "TOKEN=s3cret") { 51 if !containsEnv(env, "TOKEN=s3cret") {
52 t.Error("a trusted build's secret did not reach the step") 52 t.Error("a trusted build's secret did not reach the step")
53 } 53 }
54 env = stepEnv(job{}, "/tmp/buildhome") 54 env = stepEnv(job{}, "/tmp/buildhome", "git@x.test")
55 for _, e := range env { 55 for _, e := range env {
56 if strings.HasPrefix(e, "TOKEN=") { 56 if strings.HasPrefix(e, "TOKEN=") {
57 t.Errorf("a secret appeared with none sent: %q", e) 57 t.Errorf("a secret appeared with none sent: %q", e)
@@ -64,7 +64,7 @@ func TestStepEnvPathFallback(t *testing.T) {
64 old := os.Getenv("PATH") 64 old := os.Getenv("PATH")
65 os.Unsetenv("PATH") 65 os.Unsetenv("PATH")
66 defer os.Setenv("PATH", old) 66 defer os.Setenv("PATH", old)
67 if env := stepEnv(job{}, "/tmp/buildhome"); !containsEnv(env, "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin") { 67 if env := stepEnv(job{}, "/tmp/buildhome", "git@x.test"); !containsEnv(env, "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin") {
68 t.Errorf("no PATH fallback: %v", env) 68 t.Errorf("no PATH fallback: %v", env)
69 } 69 }
70} 70}
@@ -82,7 +82,7 @@ func containsEnv(env []string, want string) bool {
82// which run() removes when the build ends, so every build re-downloaded 82// which run() removes when the build ends, so every build re-downloaded
83// the Go module cache and the ~50MB sonar scanner. 83// the Go module cache and the ~50MB sonar scanner.
84func TestStepEnvHomeIsNotTheWorkspace(t *testing.T) { 84func TestStepEnvHomeIsNotTheWorkspace(t *testing.T) {
85 env := stepEnv(job{ID: 7}, "/var/lib/gitbay-runner/work/home") 85 env := stepEnv(job{ID: 7}, "/var/lib/gitbay-runner/work/home", "git@x.test")
86 for _, e := range env { 86 for _, e := range env {
87 if strings.HasPrefix(e, "HOME=") && strings.Contains(e, "build-7") { 87 if strings.HasPrefix(e, "HOME=") && strings.Contains(e, "build-7") {
88 t.Errorf("HOME is the per-build workspace, which is deleted after the build: %q", e) 88 t.Errorf("HOME is the per-build workspace, which is deleted after the build: %q", e)
@@ -186,3 +186,27 @@ func TestSplitEnvKeepsMultilineOutOfTheFile(t *testing.T) {
186 } 186 }
187 } 187 }
188} 188}
189
190// A build that talks back to the instance — releases, comments — needs an
191// address that works from where it runs. GITBAY_SSH carries the runner's
192// remote; under podman a loopback remote is rewritten to the address at
193// which pasta exposes the host, since the host's own addresses belong to
194// the container inside it.
195func TestStepEnvCarriesInstanceAddress(t *testing.T) {
196 env := stepEnv(job{}, "/tmp/buildhome", "git@gitbay.org")
197 if !containsEnv(env, "GITBAY_SSH=git@gitbay.org") {
198 t.Errorf("GITBAY_SSH missing: %q", env)
199 }
200 for _, tc := range []struct{ remote, isolation, want string }{
201 {"git@127.0.0.1", isolationNone, "git@127.0.0.1"},
202 {"git@127.0.0.1", isolationPodman, "git@169.254.1.2"},
203 {"git@localhost", isolationPodman, "git@169.254.1.2"},
204 {"git@gitbay.org", isolationPodman, "git@gitbay.org"},
205 {"gitbay.org", isolationPodman, "gitbay.org"},
206 } {
207 r := &runner{remote: tc.remote, isolation: tc.isolation}
208 if got := r.buildSSH(); got != tc.want {
209 t.Errorf("remote %s under %s: got %s want %s", tc.remote, tc.isolation, got, tc.want)
210 }
211 }
212}
cmd/gitbay-runner/main.go +26 −2
@@ -430,7 +430,7 @@ func (r *runner) run(j job) bool {
430 } 430 }
431 } 431 }
432 432
433 env := stepEnv(j, buildHome) 433 env := stepEnv(j, buildHome, r.buildSSH())
434 return r.runSteps(j, dir, env, sink, deadline, runStep) 434 return r.runSteps(j, dir, env, sink, deadline, runStep)
435} 435}
436 436
@@ -462,7 +462,30 @@ func buildHomeFor(workdir, repo string) (string, error) {
462 return dir, nil 462 return dir, nil
463} 463}
464 464
465func stepEnv(j job, home string) []string { 465// buildSSH is the instance's ssh destination as a build reaches it. Under
466// podman, pasta gives the container the host's own addresses, so a
467// loopback remote — the runner on the server itself — is unreachable by
468// that name; pasta exposes the host at 169.254.1.2, its
469// --map-host-loopback default. Any other remote is a real host elsewhere
470// and works as it is.
471func (r *runner) buildSSH() string {
472 if r.isolation != isolationPodman {
473 return r.remote
474 }
475 user, host, hasUser := strings.Cut(r.remote, "@")
476 if !hasUser {
477 user, host = "", user
478 }
479 if host != "127.0.0.1" && host != "localhost" && host != "::1" {
480 return r.remote
481 }
482 if hasUser {
483 return user + "@169.254.1.2"
484 }
485 return "169.254.1.2"
486}
487
488func stepEnv(j job, home, sshDest string) []string {
466 path := os.Getenv("PATH") 489 path := os.Getenv("PATH")
467 if path == "" { 490 if path == "" {
468 path = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" 491 path = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
@@ -476,6 +499,7 @@ func stepEnv(j job, home string) []string {
476 "GITBAY_SHA=" + j.SHA, 499 "GITBAY_SHA=" + j.SHA,
477 "GITBAY_REF=" + j.Ref, 500 "GITBAY_REF=" + j.Ref,
478 "GITBAY_JOB=" + j.Job, 501 "GITBAY_JOB=" + j.Job,
502 "GITBAY_SSH=" + sshDest,
479 } 503 }
480 // The server sends secrets only for a trusted build — a merge request 504 // The server sends secrets only for a trusted build — a merge request
481 // head from a fork arrives with none — so this loop is empty exactly 505 // head from a fork arrives with none — so this loop is empty exactly