Commit bc114f5

Eric Bower  ·  2026-05-08 09:05:04 -0400 EDT
parent a29c053
refactor: post-recieve hook dest
2 files changed,  +164, -47
+29, -26
......@@ -8,44 +8,47 @@
88
99 set -euo pipefail
1010
11-PIPE_HOST="${PIPE_HOST:-pipe}"
11+PIPE_HOST="${PIPE_HOST:-pipe.pico.sh}"
1212 PGS_HOST="${PGS_HOST:-pgs.sh}"
13+PGS_PROJECT_BASE="${PGS_PROJECT_BASE:-public-ci}"
1314
1415 log() {
15- echo "[post-receive] $*" >&2
16+ echo "[post-receive] $*" >&2
1617 }
1718
1819 while read -r old_sha new_sha ref; do
19- # Skip delete refs
20- if [ "$new_sha" = "0000000000000000000000000000000000000000" ]; then
21- log "skip delete ref: $ref"
22- continue
23- fi
20+ # Skip delete refs
21+ if [ "$new_sha" = "0000000000000000000000000000000000000000" ]; then
22+ log "skip delete ref: $ref"
23+ continue
24+ fi
2425
25- # Extract branch name from ref (e.g. refs/heads/main → main)
26- branch="${ref#refs/heads/}"
26+ # Extract branch name from ref (e.g. refs/heads/main → main)
27+ branch="${ref#refs/heads/}"
2728
28- # Repo name and short SHA from bare repo
29- repo="$(basename "$(pwd)" .git)"
30- short_sha="${new_sha:0:8}"
29+ # Repo name and short SHA from bare repo
30+ repo="$(basename "$(pwd)" .git)"
31+ pgs_project="${PGS_PROJECT_BASE}-${repo}"
32+ short_sha="${new_sha:0:8}"
3133
32- log "repo=$repo branch=$branch commit=$new_sha"
34+ log "repo=$repo branch=$branch commit=$new_sha"
3335
34- tar_path="/private-ci/workspaces/${repo}_${short_sha}.tar"
35- log "upload: git archive $new_sha → $PGS_HOST:$tar_path"
36+ pgs_path="/${pgs_project}/${short_sha}/"
37+ tar_path="${pgs_path}workspace.tar"
38+ log "upload: git archive $new_sha → $PGS_HOST:$tar_path"
3639
37- if ! git archive "$new_sha" | ssh -T "$PGS_HOST" "$tar_path"; then
38- log "ERROR: upload failed" >&2
39- continue
40- fi
40+ if ! git archive "$new_sha" | ssh -T "$PGS_HOST" "$tar_path"; then
41+ log "ERROR: upload failed" >&2
42+ continue
43+ fi
4144
42- # Workspace points to the tar on pgs.sh
43- workspace="${PGS_HOST}:${tar_path}"
45+ # Workspace points to the tar on pgs.sh
46+ workspace="${PGS_HOST}:${tar_path}"
4447
45- event=$(printf '{"type":"git.push","name":"%s","workspace":"%s","branch":"%s","commit":"%s"}' \
46- "$repo" "$workspace" "$branch" "$new_sha")
48+ event=$(printf '{"type":"git.push","name":"%s","workspace":"%s","branch":"%s","commit":"%s","artifact_dest":"%s"}' \
49+ "$repo" "$workspace" "$branch" "$new_sha" "$pgs_path")
4750
48- log "publish: $event"
49- echo "$event" | ssh -T "$PIPE_HOST" pub -b=false build.event
50- log "done"
51+ log "publish: $event"
52+ echo "$event" | ssh -T "$PIPE_HOST" pub -b=false build.event
53+ log "done"
5154 done
+135, -21
......@@ -62,7 +62,6 @@ type Event struct {
6262 Branch string `json:"branch"`
6363 Commit string `json:"commit"`
6464 ArtifactDest string `json:"artifact_dest"`
65- ArtifactURL string `json:"artifact_url"` // public URL for artifacts (e.g. https://{user}-artifacts.pgs.sh/{repo})
6665 }
6766
6867 func NewCfg() (*Cfg, string, bool) {
......@@ -239,7 +238,7 @@ EVENT JSON FIELDS
239238 name (required) Repository name, used for session naming
240239 workspace (required) SSH source path to rsync, e.g. "git@github.com:user/repo.git"
241240 artifact_dest (optional) SSH destination for artifact sync, e.g. "user@host:/path"
242- artifact_url (optional) Public URL for artifacts, e.g. "https://artifacts.pgs.sh/repo"
241+
243242
244243 EXAMPLE
245244 echo '{"type":"push","name":"myrepo","workspace":"git@github.com:user/myrepo.git"}' | pici runner
......@@ -477,7 +476,6 @@ type StatusPayload struct {
477476 Duration string `json:"duration,omitempty"`
478477 StartedAt string `json:"started_at,omitempty"`
479478 EndedAt string `json:"ended_at,omitempty"`
480- ArtifactURL string `json:"artifact_url,omitempty"`
481479 SessionCount int `json:"session_count"`
482480 Sessions []SessionInfo `json:"sessions"`
483481 }
......@@ -941,7 +939,7 @@ func renderJobRunning(output io.Writer, name, jobID string, group []SessionInfo,
941939 }
942940
943941 // renderJobFinal prints the final status for a completed job.
944-func renderJobFinal(output io.Writer, name, jobID string, group []SessionInfo, duration, status string, success bool, workspace, artifactDir, artifactURL string) {
942+func renderJobFinal(output io.Writer, name, jobID string, group []SessionInfo, duration, status string, success bool, workspace, artifactDir string) {
945943 icon := map[string]string{"success": "✅", "failed": "❌"}[status]
946944 fmt.Fprintf(output, " %-12s %s %s (%s)\n", name, icon, status, duration) //nolint:errcheck
947945
......@@ -962,10 +960,7 @@ func renderJobFinal(output io.Writer, name, jobID string, group []SessionInfo, d
962960 }
963961 artifactPath := filepath.Join(artifactDir, name, jobID)
964962 fmt.Fprintf(output, " artifacts: %s\n", artifactPath) //nolint:errcheck
965- if artifactURL != "" {
966- fmt.Fprintf(output, " url: %s\n", artifactURL) //nolint:errcheck
967- }
968- fmt.Fprint(output, "\n") //nolint:errcheck
963+ fmt.Fprint(output, "\n") //nolint:errcheck
969964 }
970965
971966 func monitorTick(cfg *Cfg, log *slog.Logger, output io.Writer, jobStates map[string]*monitorJobState) error {
......@@ -1000,7 +995,22 @@ func monitorTick(cfg *Cfg, log *slog.Logger, output io.Writer, jobStates map[str
1000995 // not just when the job completes. This gives live progress
1001996 // snapshots while the job is running.
1002997 for _, s := range group {
1003- html, err := fetchHistoryHTML(s.Name)
998+ // Determine session status and timing
999+ sessionStatus := "running"
1000+ sessionDuration := fmtDuration(s.Created, fmt.Sprintf("%d", time.Now().Unix()))
1001+ sessionExitCode := ""
1002+ if s.Ended != "" {
1003+ sessionDuration = fmtDuration(s.Created, s.Ended)
1004+ if s.ExitCode == "0" {
1005+ sessionStatus = "success"
1006+ sessionExitCode = "0"
1007+ } else {
1008+ sessionStatus = "failed"
1009+ sessionExitCode = s.ExitCode
1010+ }
1011+ }
1012+
1013+ html, err := fetchHistoryHTML(s.Name, name, jobID, sessionStatus, sessionDuration, sessionExitCode)
10041014 if err != nil {
10051015 log.Error("fetch history html", "session", s.Name, "err", err)
10061016 continue
......@@ -1028,15 +1038,9 @@ func monitorTick(cfg *Cfg, log *slog.Logger, output io.Writer, jobStates map[str
10281038 log.Error("stage index.txt", "err", err)
10291039 }
10301040
1031- // Load event to get artifact URL
1041+ // Load event to get artifact destination
10321042 eventData, _ := loadEvent(cfg.ArtifactDir, name, jobID)
10331043
1034- // Build artifact URL: artifact_url/jobID/index.html
1035- var artifactURL string
1036- if eventData.ArtifactURL != "" {
1037- artifactURL = fmt.Sprintf("%s/%s/index.html", strings.TrimRight(eventData.ArtifactURL, "/"), jobID)
1038- }
1039-
10401044 // Compute job-level timing from session timestamps
10411045 startedAt, endedAt, duration := computeJobTiming(group)
10421046
......@@ -1053,7 +1057,7 @@ func monitorTick(cfg *Cfg, log *slog.Logger, output io.Writer, jobStates map[str
10531057 log.Info("job finished", "status", status, "exit_code", exitCode)
10541058
10551059 if cfg.HumanOutput {
1056- renderJobFinal(output, name, jobID, group, duration, status, exitCode == 0, eventData.Workspace, cfg.ArtifactDir, artifactURL)
1060+ renderJobFinal(output, name, jobID, group, duration, status, exitCode == 0, eventData.Workspace, cfg.ArtifactDir)
10571061 } else {
10581062 payload := StatusPayload{
10591063 Timestamp: time.Now().UTC().Format(time.RFC3339),
......@@ -1064,7 +1068,6 @@ func monitorTick(cfg *Cfg, log *slog.Logger, output io.Writer, jobStates map[str
10641068 Duration: duration,
10651069 StartedAt: startedAt,
10661070 EndedAt: endedAt,
1067- ArtifactURL: artifactURL,
10681071 SessionCount: len(group),
10691072 Sessions: group,
10701073 }
......@@ -1091,7 +1094,6 @@ func monitorTick(cfg *Cfg, log *slog.Logger, output io.Writer, jobStates map[str
10911094 ExitCode: nil,
10921095 Duration: duration,
10931096 StartedAt: startedAt,
1094- ArtifactURL: artifactURL,
10951097 SessionCount: len(group),
10961098 Sessions: group,
10971099 }
......@@ -1312,13 +1314,125 @@ func parseZMXList(output string) []SessionInfo {
13121314 return sessions
13131315 }
13141316
1315-func fetchHistoryHTML(sessionName string) (string, error) {
1317+// sessionHTMLTemplate wraps session history fragments in a full HTML document.
1318+const sessionHTMLTemplate = `<!DOCTYPE html>
1319+<html lang="en">
1320+<head>
1321+<meta charset="UTF-8">
1322+<meta name="viewport" content="width=device-width, initial-scale=1.0">
1323+<title>{{.SessionName}} — CI Session</title>
1324+<style>
1325+ * { box-sizing: border-box; }
1326+ body {
1327+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
1328+ margin: 0; padding: 1.5rem;
1329+ background: #1e1e2e;
1330+ color: #cdd6f4;
1331+ font-size: 14px;
1332+ line-height: 1.5;
1333+ }
1334+ h1 {
1335+ font-size: 1rem;
1336+ font-weight: 600;
1337+ margin: 0 0 1rem 0;
1338+ color: #89b4fa;
1339+ display: flex;
1340+ align-items: center;
1341+ gap: 0.75rem;
1342+ flex-wrap: wrap;
1343+ }
1344+ .meta {
1345+ color: #6c7086;
1346+ font-size: 0.8rem;
1347+ margin-bottom: 1rem;
1348+ display: flex;
1349+ gap: 1.5rem;
1350+ flex-wrap: wrap;
1351+ }
1352+ .meta span { display: flex; align-items: center; gap: 0.3rem; }
1353+ .status { padding: 0.15rem 0.5rem; border-radius: 4px; font-size: 0.75rem; font-weight: 600; }
1354+ .status-running { background: #89b4fa33; color: #89b4fa; }
1355+ .status-success { background: #a6e3a133; color: #a6e3a1; }
1356+ .status-failed { background: #f38ba833; color: #f38ba8; }
1357+ pre {
1358+ background: #11111b;
1359+ border-radius: 8px;
1360+ padding: 1rem;
1361+ overflow-x: auto;
1362+ margin: 0;
1363+ border: 1px solid #313244;
1364+ }
1365+ pre code { font-size: 13px; }
1366+ .zmx-output { white-space: pre-wrap; word-break: break-word; }
1367+ a { color: #89b4fa; }
1368+ a:hover { text-decoration: underline; }
1369+ .links { margin-top: 1rem; color: #6c7086; font-size: 0.85rem; }
1370+</style>
1371+</head>
1372+<body>
1373+<h1>
1374+ {{.SessionShort}}
1375+ <span class="status status-{{.SessionStatus}}">{{.SessionStatus}}</span>
1376+</h1>
1377+<div class="meta">
1378+ <span>Job: {{.JobName}}</span>
1379+ <span>ID: {{.JobID}}</span>
1380+ <span>Duration: {{.Duration}}</span>
1381+ {{if .ExitCode}}<span>Exit: {{.ExitCode}}</span>{{end}}
1382+</div>
1383+<pre><code>{{.Content}}</code></pre>
1384+<div class="links">
1385+ <a href="index.html">&larr; Back to job</a>
1386+</div>
1387+</body>
1388+</html>`
1389+
1390+// SessionArtifactData holds the data for rendering a session HTML artifact.
1391+type SessionArtifactData struct {
1392+ SessionName string
1393+ SessionShort string
1394+ SessionStatus string
1395+ JobName string
1396+ JobID string
1397+ Duration string
1398+ ExitCode string
1399+ Content string
1400+}
1401+
1402+// fetchHistoryHTML fetches session history from zmx and wraps it in a full HTML document.
1403+func fetchHistoryHTML(sessionName, jobName, jobID, status, duration, exitCode string) (string, error) {
13161404 cmd := exec.Command("zmx", "history", sessionName, "--html")
13171405 output, err := cmd.Output()
13181406 if err != nil {
13191407 return "", err
13201408 }
1321- return string(output), nil
1409+
1410+ prefix := "ci." + jobName + "." + jobID + "."
1411+ shortName := strings.TrimPrefix(sessionName, prefix)
1412+ shortName = strings.TrimPrefix(shortName, "step.")
1413+
1414+ data := SessionArtifactData{
1415+ SessionName: sessionName,
1416+ SessionShort: shortName,
1417+ SessionStatus: status,
1418+ JobName: jobName,
1419+ JobID: jobID,
1420+ Duration: duration,
1421+ ExitCode: exitCode,
1422+ Content: string(output),
1423+ }
1424+
1425+ tmpl, err := template.New("session").Parse(sessionHTMLTemplate)
1426+ if err != nil {
1427+ return "", fmt.Errorf("parse template: %w", err)
1428+ }
1429+
1430+ var buf strings.Builder
1431+ if err := tmpl.Execute(&buf, data); err != nil {
1432+ return "", fmt.Errorf("execute template: %w", err)
1433+ }
1434+
1435+ return buf.String(), nil
13221436 }
13231437
13241438 func fetchHistoryPlain(sessionName string) (string, error) {