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.

Run CI jobs in a container chosen by the runner

7da9111ecabb236d48fc48592cd3d1d45f9afe78 · Keenan Nemetz · 15d ago · parent 474a812d

Run CI jobs in a container chosen by the runner

Each lease gets a disposable checkout and, when --image is set, its own container from that image. The runner keeps the SSH key and trusted Git cache outside the job, so workflow code cannot reach either; older caches are rebuilt because a previous job may have written to them.

The record names the image and runtime, and the queue mirrors them for the run page. Without --image the runner still executes on the host and says so.

ci/agent/worktree_test.go +107

@@ -0,0 +1,107 @@
1 package agent
2
3 import (
4 "os"
5 "os/exec"
6 "path/filepath"
7 "strings"
8 "testing"
9 )
10
11 // prepareWorktree has two jobs that must not be confused: keep a trusted cache for
12 // the runner's record push, and give the workflow a disposable tree with its own
13 // .git. The test plays the SSH transport with a script so the whole path runs
14 // without a server, including a commit that is not reachable from any branch.
15 func TestPrepareWorktreeSeparatesTrustedCacheFromJobTree(t *testing.T) {
16 tmp := t.TempDir()
17 src := filepath.Join(tmp, "src")
18 remote := filepath.Join(tmp, "demo.git")
19 cache := filepath.Join(tmp, "cache")
20
21 mustGit(t, "", "init", "-q", src)
22 mustGit(t, src, "config", "user.email", "a@example.com")
23 mustGit(t, src, "config", "user.name", "a")
24 if err := os.WriteFile(filepath.Join(src, "file"), []byte("one\n"), 0o644); err != nil {
25 t.Fatal(err)
26 }
27 mustGit(t, src, "add", "file")
28 mustGit(t, src, "commit", "-qm", "one")
29 mustGit(t, "", "clone", "-q", "--bare", src, remote)
30
31 // A second commit reaches the remote's object store but no ref. Fetching it by
32 // id is what the fallback in prepareWorktree has to handle.
33 if err := os.WriteFile(filepath.Join(src, "file"), []byte("two\n"), 0o644); err != nil {
34 t.Fatal(err)
35 }
36 mustGit(t, src, "commit", "-qam", "two")
37 mustGit(t, src, "push", "-q", remote, "HEAD:refs/heads/tmp")
38 sha := strings.TrimSpace(mustGit(t, src, "rev-parse", "HEAD"))
39 mustGit(t, remote, "update-ref", "-d", "refs/heads/tmp")
40 mustGit(t, remote, "config", "uploadpack.allowAnySHA1InWant", "true")
41
42 // Git talks to our script instead of ssh. The script serves the bare repository
43 // for the upload-pack request and fails the protocol probe so Git falls back to
44 // the ordinary transport.
45 ssh := filepath.Join(tmp, "ssh")
46 script := "#!/bin/sh\n" +
47 "for a in \"$@\"; do\n" +
48 " case \"$a\" in\n" +
49 " *git-upload-pack*) exec git-upload-pack \"$VOLE_TEST_REPO\" ;;\n" +
50 " esac\n" +
51 "done\n" +
52 "exit 1\n"
53 if err := os.WriteFile(ssh, []byte(script), 0o755); err != nil {
54 t.Fatal(err)
55 }
56 t.Setenv("GIT_SSH_COMMAND", ssh)
57 t.Setenv("GIT_SSH_VARIANT", "ssh")
58 t.Setenv("VOLE_TEST_REPO", remote)
59
60 co, err := prepareWorktree("ssh://git@example.invalid", cache, "demo", sha, "run1", 1)
61 if err != nil {
62 t.Fatalf("prepareWorktree: %v", err)
63 }
64 defer co.cleanup()
65
66 got := strings.TrimSpace(mustGit(t, co.Work, "rev-parse", "HEAD"))
67 if got != sha {
68 t.Fatalf("job tree is at %s, want %s", got, sha)
69 }
70 if _, err := os.Stat(filepath.Join(co.Work, ".git")); err != nil {
71 t.Fatalf("the job tree has no .git: %v", err)
72 }
73
74 // The job may edit its own Git directory. That must not reach the cache the
75 // runner later uses to push the record.
76 hook := filepath.Join(co.Work, ".git", "hooks", "pre-push")
77 if err := os.MkdirAll(filepath.Dir(hook), 0o755); err != nil {
78 t.Fatal(err)
79 }
80 if err := os.WriteFile(hook, []byte("#!/bin/sh\nexit 1\n"), 0o755); err != nil {
81 t.Fatal(err)
82 }
83 cacheHook := filepath.Join(cache, "demo", ".git", "hooks", "pre-push")
84 if _, err := os.Stat(cacheHook); !os.IsNotExist(err) {
85 t.Fatalf("a job hook reached the trusted cache: %v", err)
86 }
87
88 // The temporary ref used to carry an unreachable commit must be gone too.
89 out := mustGit(t, filepath.Join(cache, "demo"), "for-each-ref", "--format=%(refname)",
90 "refs/vole/jobs")
91 if strings.TrimSpace(out) != "" {
92 t.Fatalf("temporary job refs were left in the cache:\n%s", out)
93 }
94 }
95
96 func mustGit(t *testing.T, dir string, args ...string) string {
97 t.Helper()
98 cmd := exec.Command("git", args...)
99 if dir != "" {
100 cmd.Dir = dir
101 }
102 out, err := cmd.CombinedOutput()
103 if err != nil {
104 t.Fatalf("git %s: %v: %s", strings.Join(args, " "), err, out)
105 }
106 return string(out)
107 }