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.
Stream run logs over the runner's SSH channel
5ece70a50ebda392626b92bdf29d28bc9b444954 · Keenan Nemetz · 10d ago · parent fdf2340f
Stream run logs over the runner's SSH channel The runner wrote the log to a temp file and uploaded it as one LFS object when the job ended, so a running job had no log to read and a log was stored like a shared artifact even though it belongs to one attempt. The runner sends 128 KiB chunks over the same runner channel that holds the lease. The server appends them to data/logs/<repo>/<run>/<attempt>.log and records the size while the run is still running. The finish record carries the final size; the server checks the file length, syncs it, and stores the result. A partial or failed stream leaves no log on the record. Logs are no longer LFS objects: they are not shared, not content-addressed, and the web serves the file directly, with range and step slices. GC no longer treats them as LFS roots.
forge/instance.go +5−1
| @@ -14,6 +14,7 @@ import ( | |||
| 14 | 14 | "path/filepath" | |
| 15 | 15 | "strings" | |
| 16 | 16 | ||
| 17 | "github.com/rodent-software/vole/ci/logs" | ||
| 17 | 18 | "github.com/rodent-software/vole/ci/runs" | |
| 18 | 19 | "github.com/rodent-software/vole/core/lfs" | |
| 19 | 20 | "github.com/rodent-software/vole/forge/access" | |
| @@ -38,6 +39,7 @@ type Instance struct { | |||
| 38 | 39 | ACLs *access.Store | |
| 39 | 40 | Locks *lfs.LockStore | |
| 40 | 41 | LFS *lfs.Store | |
| 42 | Logs *logs.Store | ||
| 41 | 43 | PageViews *pageview.Store | |
| 42 | 44 | ||
| 43 | 45 | // ImportedACLs is how many legacy refs/vole/acl rule sets were imported by | |
| @@ -51,7 +53,7 @@ func Open(cfg *config.Config, binPath string) (*Instance, error) { | |||
| 51 | 53 | // Directories that hold secrets are created at 0700. The repositories tree is | |
| 52 | 54 | // content, not secrets, but it lives inside the data directory and there is no | |
| 53 | 55 | // reason for it to be wider than the directory that contains it. | |
| 54 | for _, dir := range []string{cfg.DataDir, cfg.ReposDir(), cfg.SpoolDir(), cfg.LFSDir()} { | ||
| 56 | for _, dir := range []string{cfg.DataDir, cfg.ReposDir(), cfg.SpoolDir(), cfg.LFSDir(), cfg.LogsDir()} { | ||
| 55 | 57 | if err := secure.Mkdir(dir); err != nil { | |
| 56 | 58 | return nil, fmt.Errorf("data directory %s: %w", dir, err) | |
| 57 | 59 | } | |
| @@ -152,6 +154,7 @@ func Open(cfg *config.Config, binPath string) (*Instance, error) { | |||
| 152 | 154 | ACLs: acls, | |
| 153 | 155 | Locks: lk, | |
| 154 | 156 | LFS: lfs.New(cfg.LFSDir(), cfg.LFSObjectMaxBytes), | |
| 157 | Logs: logs.New(cfg.LogsDir()), | ||
| 155 | 158 | PageViews: views, | |
| 156 | 159 | ImportedACLs: imported, | |
| 157 | 160 | }, nil | |
| @@ -172,6 +175,7 @@ func TightenSecrets(cfg *config.Config) ([]string, error) { | |||
| 172 | 175 | cfg.ReposDir(), | |
| 173 | 176 | cfg.SpoolDir(), | |
| 174 | 177 | cfg.LFSDir(), | |
| 178 | cfg.LogsDir(), | ||
| 175 | 179 | cfg.UsersDB(), cfg.UsersDB() + "-wal", cfg.UsersDB() + "-shm", | |
| 176 | 180 | cfg.IndexDB(), cfg.IndexDB() + "-wal", cfg.IndexDB() + "-shm", | |
| 177 | 181 | cfg.RunsDB(), cfg.RunsDB() + "-wal", cfg.RunsDB() + "-shm", | |