Commit c21c7ea735

c21c7ea73594e88d60cec08c98f78eee02a0d4ec

parent: 42a7b1230c

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-28 07:11 UTC

runner next: send the instance's public ssh destination

Ref #260

Layout: unified · split

internal/control/build.go +23 −2
@@ -6,6 +6,7 @@ import (
66 "fmt"
77 "io"
88 "log/slog"
9 "net"
910 "regexp"
1011 "slices"
1112 "strconv"
@@ -460,6 +461,23 @@ func runnerMayBuild(c *Ctx, key store.SSHKey, repoID int64) (bool, error) {
460461// walking it forever.
461462const maxOrphanSkip = 50
462463
464// publicSSH is the instance's ssh destination as anyone outside reaches
465// it. A runner on the daemon's own host polls over loopback and hands
466// its builds this instead, so no build connects from the runner's source
467// address (#260). The port is added only when it is not 22: hutch and
468// orgo build ssh://$GITBAY_SSH/... URLs, valid in both forms. Empty when
469// site_url is not set.
470func publicSSH(c *Ctx) string {
471 host := c.Cfg.SiteHost()
472 if host == "" {
473 return ""
474 }
475 if p := c.Cfg.SSH.Port; p != 0 && p != 22 {
476 return "git@" + net.JoinHostPort(host, strconv.Itoa(p))
477 }
478 return "git@" + host
479}
480
463481func runRunnerNext(c *Ctx, args []string) int {
464482 key, code := runnerSession(c)
465483 if code >= 0 {
@@ -562,10 +580,13 @@ func runRunnerNext(c *Ctx, args []string) int {
562580 Image string `json:"image,omitempty"`
563581 // Trusted is always sent: a runner decides a build's home and
564582 // secrets from it, and reads a missing field as untrusted (#255).
565 Trusted bool `json:"trusted"`
583 Trusted bool `json:"trusted"`
584 // SSH is the instance's public destination for the build's
585 // GITBAY_SSH when its runner polls over loopback (#260).
586 SSH string `json:"ssh,omitempty"`
566587 Secrets map[string]string `json:"secrets,omitempty"`
567588 }{ID: b.ID, Repo: repo.Path(), Number: b.Number, Job: b.Job, SHA: b.SHA, Ref: b.Ref,
568 Steps: steps, Image: b.Image, Trusted: b.Trusted, Secrets: secrets}
589 Steps: steps, Image: b.Image, Trusted: b.Trusted, SSH: publicSSH(c), Secrets: secrets}
569590 return c.emit(d, func(w io.Writer) {
570591 fmt.Fprintf(w, "build %d: %s %s @ %.10s\n", d.ID, d.Repo, d.Job, d.SHA)
571592 })
internal/control/runnernext_test.go +29
@@ -266,3 +266,32 @@ func TestRunnerNextSaysWhetherTrusted(t *testing.T) {
266266 }
267267 }
268268}
269
270// A build on the daemon's own host is given the public destination, not
271// the loopback address its runner polls. The port rides along only when
272// it is not 22, so ssh://$GITBAY_SSH/<owner>/<name>.git is a valid URL
273// either way (#260).
274func TestRunnerNextCarriesPublicSSH(t *testing.T) {
275 st, repo, uid, root, baseSHA, _ := setupOrphanRepo(t)
276 for _, tc := range []struct {
277 port int
278 want string
279 }{
280 {0, `"ssh":"git@x.test"`},
281 {22, `"ssh":"git@x.test"`},
282 {2022, `"ssh":"git@x.test:2022"`},
283 } {
284 if _, err := st.CreateBuild(repo.ID, "unit", baseSHA, "main", "[]", "", "", true); err != nil {
285 t.Fatal(err)
286 }
287 c, out := runnerCtx(st, uid, root) // site_url https://x.test
288 c.Cfg.SSH.Port = tc.port
289 c.JSON = true
290 if code := runRunnerNext(c, nil); code != protocol.ExitOK {
291 t.Fatalf("port %d: runner next: exit %d, output:\n%s", tc.port, code, out.String())
292 }
293 if !strings.Contains(out.String(), tc.want) {
294 t.Fatalf("port %d: claim lacks %s:\n%s", tc.port, tc.want, out.String())
295 }
296 }
297}