Commit 8fc0a2e
Eric Bower
·
2026-05-07 16:34:40 -0400 EDT
parent 1ac7c95
feat(runner): support tar files for workspaces
4 files changed,
+136,
-15
+3,
-0
1@@ -1 +1,4 @@
2 # pici - a simple ci system
3+
4+testing
5+hook test 3 1778209807
+1,
-1
1@@ -1 +1 @@
2-{"type":"git.push","name":"pici","workspace":"/home/erock/dev/pici/"}
3+{"type":"git.push","name":"pici","workspace":"pgs.sh:/private-ci/workspaces/pici_8f0a260d.tar","branch":"main","commit":"8f0a260d584027f657e38b44f397b97f59099e85","artifact_dest":"","artifact_url":""}
+34,
-14
1@@ -1,5 +1,6 @@
2 #!/usr/bin/env bash
3-# post-receive hook — publishes build events to pici via ssh pipe.
4+# post-receive hook — archives the pushed commit and uploads to pgs.sh,
5+# then publishes a build event to pici via ssh pipe.
6 #
7 # Install: ./install-hooks.sh /path/to/bare-repos
8 #
9@@ -8,24 +9,43 @@
10 set -euo pipefail
11
12 PIPE_HOST="${PIPE_HOST:-pipe}"
13+PGS_HOST="${PGS_HOST:-pgs.sh}"
14+
15+log() {
16+ echo "[post-receive] $*" >&2
17+}
18
19 while read -r old_sha new_sha ref; do
20- # Skip delete refs
21- if [ "$new_sha" = "0000000000000000000000000000000000000000" ]; then
22- continue
23- fi
24+ # Skip delete refs
25+ if [ "$new_sha" = "0000000000000000000000000000000000000000" ]; then
26+ log "skip delete ref: $ref"
27+ continue
28+ fi
29+
30+ # Extract branch name from ref (e.g. refs/heads/main → main)
31+ branch="${ref#refs/heads/}"
32+
33+ # Repo name and short SHA from bare repo
34+ repo="$(basename "$(pwd)" .git)"
35+ short_sha="${new_sha:0:8}"
36+
37+ log "repo=$repo branch=$branch commit=$new_sha"
38
39- # Extract branch name from ref (e.g. refs/heads/main → main)
40- branch="${ref#refs/heads/}"
41+ tar_path="/private-ci/workspaces/${repo}_${short_sha}.tar"
42+ log "upload: git archive $new_sha → $PGS_HOST:$tar_path"
43
44- # Repo name from the bare repo directory
45- repo="$(basename "$(pwd)" .git)"
46+ if ! git archive "$new_sha" | ssh -T "$PGS_HOST" "$tar_path"; then
47+ log "ERROR: upload failed" >&2
48+ continue
49+ fi
50
51- # Workspace is the bare repo itself (runner rsyncs from here)
52- workspace="$(pwd)"
53+ # Workspace points to the tar on pgs.sh
54+ workspace="${PGS_HOST}:${tar_path}"
55
56- event=$(printf '{"type":"git.push","name":"%s","workspace":"%s","branch":"%s","commit":"%s"}' \
57- "$repo" "$workspace" "$branch" "$new_sha")
58+ event=$(printf '{"type":"git.push","name":"%s","workspace":"%s","branch":"%s","commit":"%s"}' \
59+ "$repo" "$workspace" "$branch" "$new_sha")
60
61- echo "$event" | ssh "$PIPE_HOST" pub -b=false build.event
62+ log "publish: $event"
63+ echo "$event" | ssh -T "$PIPE_HOST" pub -b=false build.event
64+ log "done"
65 done
M
main.go
+98,
-0
1@@ -1,6 +1,7 @@
2 package main
3
4 import (
5+ "archive/tar"
6 "bufio"
7 "context"
8 "crypto/sha256"
9@@ -23,6 +24,13 @@ import (
10 type WorkspaceFactory func(cfg *Cfg, logger *slog.Logger, source string) Workspace
11
12 func defaultWorkspaceFactory(cfg *Cfg, logger *slog.Logger, source string) Workspace {
13+ if strings.HasSuffix(source, ".tar") {
14+ return &WorkspaceTar{
15+ Cfg: cfg,
16+ Logger: logger,
17+ Source: source,
18+ }
19+ }
20 return &WorkspaceRsync{
21 Cfg: cfg,
22 Logger: logger,
23@@ -340,6 +348,96 @@ func (w *WorkspaceRsync) GetDir() string {
24 return w.Dest
25 }
26
27+type WorkspaceTar struct {
28+ Cfg *Cfg
29+ Logger *slog.Logger
30+ Source string // e.g. "pgs.sh:/private-ci/workspaces/repo_abc123.tar"
31+ Dest string
32+}
33+
34+func (w *WorkspaceTar) Setup() error {
35+ tempDir, err := os.MkdirTemp("", "pici-*")
36+ if err != nil {
37+ return err
38+ }
39+ w.Dest = tempDir
40+
41+ log := w.Logger.With("source", w.Source, "dest", w.Dest)
42+ log.Debug("downloading and extracting workspace tar")
43+
44+ // Parse host:path from source
45+ host, path := splitSSHSource(w.Source)
46+
47+ // Use rsync to download the tar file
48+ tarPath := filepath.Join(tempDir, "workspace.tar")
49+ rsyncCmd := exec.Command("rsync", "-e", "ssh", host+":"+path, tarPath)
50+ rsyncCmd.Stderr = os.Stderr
51+ if err := rsyncCmd.Run(); err != nil {
52+ return fmt.Errorf("rsync download: %w", err)
53+ }
54+
55+ // Open the tar file for extraction
56+ f, err := os.Open(tarPath)
57+ if err != nil {
58+ return fmt.Errorf("open tar: %w", err)
59+ }
60+ defer f.Close()
61+
62+ // Extract tar to temp dir
63+ tr := tar.NewReader(f)
64+ for {
65+ hdr, err := tr.Next()
66+ if err == io.EOF {
67+ break
68+ }
69+ if err != nil {
70+ return fmt.Errorf("tar read: %w", err)
71+ }
72+
73+ target := filepath.Join(tempDir, hdr.Name)
74+ switch hdr.Typeflag {
75+ case tar.TypeDir:
76+ if err := os.MkdirAll(target, 0755); err != nil {
77+ return fmt.Errorf("mkdir %s: %w", target, err)
78+ }
79+ case tar.TypeReg:
80+ // Ensure parent dir exists
81+ if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil {
82+ return fmt.Errorf("mkdir parent %s: %w", filepath.Dir(target), err)
83+ }
84+ tf, err := os.OpenFile(target, os.O_CREATE|os.O_WRONLY, os.FileMode(hdr.Mode))
85+ if err != nil {
86+ return fmt.Errorf("open %s: %w", target, err)
87+ }
88+ if _, err := io.Copy(tf, tr); err != nil {
89+ tf.Close()
90+ return fmt.Errorf("write %s: %w", target, err)
91+ }
92+ tf.Close()
93+ }
94+ }
95+
96+ log.Debug("workspace extracted")
97+ return nil
98+}
99+
100+func (w *WorkspaceTar) Cleanup() error {
101+ return nil
102+}
103+
104+func (w *WorkspaceTar) GetDir() string {
105+ return w.Dest
106+}
107+
108+// splitSSHSource splits "host:path" into host and path.
109+func splitSSHSource(source string) (string, string) {
110+ idx := strings.Index(source, ":")
111+ if idx == -1 {
112+ return source, ""
113+ }
114+ return source[:idx], source[idx+1:]
115+}
116+
117 type JobEngine struct {
118 Wk Workspace
119 Logger *slog.Logger