Commit c39f0bac4e

c39f0bac4ee1f9697b76d45ad092f9fab379633e

parent: dab7e0ad06

Verified · cmc ci/build: success ci/test: success

cmc <hello@cleberg.net> · 2026-09-10 03:11 UTC

runner: GITBAY_SSH names the instance as the build reaches it

Under podman with pasta the host's own addresses belong to the
container, so a loopback remote is rewritten to pasta's host-loopback
address. A job that publishes back to the instance uses the variable
instead of guessing.

Ref #184
.gitbay/wiki/Users.org +7 −1
@@ -468,7 +468,13 @@ the web) and a =ci/<job>= commit status, which =repo settings
468468require-checks= can gate merges on. Steps run with =sh -c= on the
469469instance's runner, stopping at the first failure; a broken config
470470surfaces 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.
472478
473479Secrets: =repo secret set <owner/name> <NAME>= reads the value from
474480stdin (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) {
1313 t.Setenv("GITBAY_RUNNER_TOKEN", "a-secret-the-service-was-given")
1414 t.Setenv("AWS_SECRET_ACCESS_KEY", "also-not-for-builds")
1515
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")
1717
1818 for _, e := range env {
1919 if strings.HasPrefix(e, "GITBAY_RUNNER_TOKEN=") || strings.HasPrefix(e, "AWS_SECRET_ACCESS_KEY=") {
@@ -47,11 +47,11 @@ func TestStepEnvDoesNotInherit(t *testing.T) {
4747// Secrets are passed through when the server sent them, which it does
4848// only for a trusted build.
4949func 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")
5151 if !containsEnv(env, "TOKEN=s3cret") {
5252 t.Error("a trusted build's secret did not reach the step")
5353 }
54 env = stepEnv(job{}, "/tmp/buildhome")
54 env = stepEnv(job{}, "/tmp/buildhome", "git@x.test")
5555 for _, e := range env {
5656 if strings.HasPrefix(e, "TOKEN=") {
5757 t.Errorf("a secret appeared with none sent: %q", e)
@@ -64,7 +64,7 @@ func TestStepEnvPathFallback(t *testing.T) {
6464 old := os.Getenv("PATH")
6565 os.Unsetenv("PATH")
6666 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") {
6868 t.Errorf("no PATH fallback: %v", env)
6969 }
7070}
@@ -82,7 +82,7 @@ func containsEnv(env []string, want string) bool {
8282// which run() removes when the build ends, so every build re-downloaded
8383// the Go module cache and the ~50MB sonar scanner.
8484func 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")
8686 for _, e := range env {
8787 if strings.HasPrefix(e, "HOME=") && strings.Contains(e, "build-7") {
8888 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) {
186186 }
187187 }
188188}
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 {
430430 }
431431 }
432432
433 env := stepEnv(j, buildHome)
433 env := stepEnv(j, buildHome, r.buildSSH())
434434 return r.runSteps(j, dir, env, sink, deadline, runStep)
435435}
436436
@@ -462,7 +462,30 @@ func buildHomeFor(workdir, repo string) (string, error) {
462462 return dir, nil
463463}
464464
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 {
466489 path := os.Getenv("PATH")
467490 if path == "" {
468491 path = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
@@ -476,6 +499,7 @@ func stepEnv(j job, home string) []string {
476499 "GITBAY_SHA=" + j.SHA,
477500 "GITBAY_REF=" + j.Ref,
478501 "GITBAY_JOB=" + j.Job,
502 "GITBAY_SSH=" + sshDest,
479503 }
480504 // The server sends secrets only for a trusted build — a merge request
481505 // head from a fork arrives with none — so this loop is empty exactly