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
M README.md
+3, -0
1@@ -1 +1,4 @@
2 # pici - a simple ci system
3+
4+testing
5+hook test 3 1778209807
M event.json
+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":""}
M hooks/post-receive
+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