Adds the verb-group that kills the single biggest reasoning sink in agent sessions — watching a build/deploy to completion (proven the session that built it: hours hand-rolling Woodpecker polling + DB-schema spelunking for one CI incident). - ci status/watch: Woodpecker REST API (version-stable, not its DB schema), reached via the internal Traefik LB (dial 10.0.20.203, SNI=ci.viktorbarzin.me so the cert verifies — the Go form of the house `curl --resolve` pattern), token from WOODPECKER_TOKEN/Vault, repo id resolved from the cwd remote, with retries that ride Woodpecker's intermittent empty responses. watch matches the HEAD/given commit (avoids the post-push race) and exits non-zero on failure. - deploy wait: image-sha match THEN rollout status (rollout status alone returns success on the old ReplicaSet); kubectl-based. - work land now auto-watches CI to green on the landed commit (--no-ci-watch to skip), closing the v0.1 gap. - ci logs deferred to v0.4.1 (Woodpecker detail/log endpoints were the least reliable; status/watch use the working list endpoint). Live-verified ci status/watch against the live API. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
99 lines
2.3 KiB
Go
99 lines
2.3 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
func ciCommands() []Command {
|
|
return []Command{
|
|
{Path: []string{"ci", "status"}, Tier: TierRead,
|
|
Summary: "pipeline status for HEAD/a commit: ci status [commit]", Run: ciStatus},
|
|
{Path: []string{"ci", "watch"}, Tier: TierRead,
|
|
Summary: "poll the pipeline for HEAD (or a commit) to terminal; non-zero on failure", Run: ciWatch},
|
|
}
|
|
}
|
|
|
|
func short(s string) string {
|
|
if len(s) > 8 {
|
|
return s[:8]
|
|
}
|
|
return s
|
|
}
|
|
|
|
func firstLine(s string) string { return strings.SplitN(s, "\n", 2)[0] }
|
|
|
|
// currentHEAD returns the full HEAD sha of the cwd repo (empty if not a repo).
|
|
func currentHEAD() string {
|
|
cwd, _ := os.Getwd()
|
|
root, err := gitRepoRoot(cwd)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
sha, _ := gitOutput(root, "rev-parse", "HEAD")
|
|
return sha
|
|
}
|
|
|
|
func ciStatus(args []string) error {
|
|
commit, _ := firstPositional(args)
|
|
c, err := newWPClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
id, err := c.repoID()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
p, err := c.findPipeline(id, commit)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf("#%d %s event=%s %s %s\n", p.Number, p.Status, p.Event, short(p.Commit), firstLine(p.Message))
|
|
return nil
|
|
}
|
|
|
|
func ciWatch(args []string) error {
|
|
commit, _ := firstPositional(args)
|
|
if commit == "" {
|
|
commit = currentHEAD()
|
|
}
|
|
if commit == "" {
|
|
return fmt.Errorf("no commit given and not in a git repo")
|
|
}
|
|
c, err := newWPClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
id, err := c.repoID()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
timeout := 20 * time.Minute
|
|
deadline := time.Now().Add(timeout)
|
|
last := ""
|
|
for time.Now().Before(deadline) {
|
|
p, err := c.findPipeline(id, commit)
|
|
if err != nil {
|
|
if last != "waiting" {
|
|
fmt.Fprintf(os.Stderr, "homelab: waiting for pipeline (%s)...\n", short(commit))
|
|
last = "waiting"
|
|
}
|
|
} else {
|
|
if p.Status != last {
|
|
fmt.Fprintf(os.Stderr, "homelab: #%d %s\n", p.Number, p.Status)
|
|
last = p.Status
|
|
}
|
|
if isTerminalStatus(p.Status) {
|
|
fmt.Printf("#%d %s %s\n", p.Number, p.Status, short(commit))
|
|
if isFailureStatus(p.Status) {
|
|
return fmt.Errorf("pipeline #%d %s (woodpecker repo, see UI/DB for the failing step)", p.Number, p.Status)
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
time.Sleep(15 * time.Second)
|
|
}
|
|
return fmt.Errorf("timed out after %s waiting for CI on %s", timeout, short(commit))
|
|
}
|