From 4b5fec04fec54b4a622c77b70b6479a3c6399c1b Mon Sep 17 00:00:00 2001 From: mayor Date: Thu, 12 Feb 2026 21:40:21 -0800 Subject: [PATCH] Add beads and gastown to CI cache, consolidate CI workflow - Add packages/beads and packages/gastown with shared definitions - Expose custom-beads and custom-gastown in flake packages output - Consolidate CI from matrix (8 parallel jobs) to single job with loop - Saves ~12 minutes of redundant nix-setup time per run - Uses ::group:: for collapsible log sections per package Co-Authored-By: Claude Opus 4.5 --- .gitea/workflows/ci.yml | 80 ++++++----- flake.nix | 14 +- packages/beads/default.nix | 41 ++++++ packages/gastown/default.nix | 38 +++++ ...gastown-fix-agent-bead-address-title.patch | 15 ++ .../gastown-fix-validate-recipient.patch | 13 ++ .../gastown-statusline-optimization.patch | 135 ++++++++++++++++++ 7 files changed, 302 insertions(+), 34 deletions(-) create mode 100644 packages/beads/default.nix create mode 100644 packages/gastown/default.nix create mode 100644 packages/gastown/gastown-fix-agent-bead-address-title.patch create mode 100644 packages/gastown/gastown-fix-validate-recipient.patch create mode 100644 packages/gastown/gastown-statusline-optimization.patch diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index d84b806..7e578e2 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -23,50 +23,64 @@ jobs: runs-on: ubuntu-latest needs: check if: github.event_name == 'push' && github.ref == 'refs/heads/main' - strategy: - fail-fast: false - matrix: - package: - - custom-claude-code - - custom-app-launcher-server - - custom-mcrcon-rbw - - custom-tea-rbw - - custom-rclone-torbox-setup - - qt-pinned-jellyfin-media-player steps: - uses: actions/checkout@v6 - uses: https://git.johnogle.info/johno/gitea-actions/nix-setup@v1 - - name: Build ${{ matrix.package }} - id: build + - name: Setup SSH for cache run: | - OUT_PATH=$(nix build .#${{ matrix.package }} --no-link --print-out-paths) - echo "out_path=$OUT_PATH" >> "$GITHUB_OUTPUT" - env: - NIX_CONFIG: "access-tokens = git.johnogle.info=${{ secrets.GITEA_ACCESS_TOKEN }}" - - - name: Sign and push to cache - run: | - # Write signing key - echo "${{ secrets.NIX_SIGNING_KEY }}" > /tmp/signing-key - chmod 600 /tmp/signing-key - - # Sign the closure - nix store sign --key-file /tmp/signing-key -r "${{ steps.build.outputs.out_path }}" - - # Setup SSH key for cache push mkdir -p ~/.ssh echo "${{ secrets.CACHE_SSH_KEY }}" > ~/.ssh/cache_key chmod 600 ~/.ssh/cache_key ssh-keyscan -H ${{ secrets.CACHE_HOST }} >> ~/.ssh/known_hosts 2>/dev/null || true - # Push to cache - nix copy --to "ssh-ng://${{ secrets.CACHE_USER }}@${{ secrets.CACHE_HOST }}?ssh-key=$HOME/.ssh/cache_key" "${{ steps.build.outputs.out_path }}" + - name: Setup signing key + run: | + echo "${{ secrets.NIX_SIGNING_KEY }}" > /tmp/signing-key + chmod 600 /tmp/signing-key - # Create GC root to prevent garbage collection - OUT_HASH=$(basename "${{ steps.build.outputs.out_path }}" | cut -d'-' -f1) - ssh -i ~/.ssh/cache_key ${{ secrets.CACHE_USER }}@${{ secrets.CACHE_HOST }} \ - "mkdir -p /nix/var/nix/gcroots/ci-cache && ln -sfn ${{ steps.build.outputs.out_path }} /nix/var/nix/gcroots/ci-cache/${OUT_HASH}" + - name: Build, sign, and cache all packages + run: | + PACKAGES=( + custom-claude-code + custom-app-launcher-server + custom-mcrcon-rbw + custom-tea-rbw + custom-rclone-torbox-setup + custom-beads + custom-gastown + qt-pinned-jellyfin-media-player + ) + + FAILED=() + for pkg in "${PACKAGES[@]}"; do + echo "::group::Building $pkg" + if OUT_PATH=$(nix build ".#$pkg" --no-link --print-out-paths 2>&1); then + echo "Built: $OUT_PATH" + + # Sign the closure + nix store sign --key-file /tmp/signing-key -r "$OUT_PATH" + + # Push to cache + nix copy --to "ssh-ng://${{ secrets.CACHE_USER }}@${{ secrets.CACHE_HOST }}?ssh-key=$HOME/.ssh/cache_key" "$OUT_PATH" + + # Create GC root to prevent garbage collection + OUT_HASH=$(basename "$OUT_PATH" | cut -d'-' -f1) + ssh -i ~/.ssh/cache_key ${{ secrets.CACHE_USER }}@${{ secrets.CACHE_HOST }} \ + "mkdir -p /nix/var/nix/gcroots/ci-cache && ln -sfn $OUT_PATH /nix/var/nix/gcroots/ci-cache/${OUT_HASH}" + + echo "✓ $pkg cached successfully" + else + echo "✗ $pkg failed to build" + FAILED+=("$pkg") + fi + echo "::endgroup::" + done + + if [ ${#FAILED[@]} -gt 0 ]; then + echo "::error::Failed packages: ${FAILED[*]}" + exit 1 + fi env: NIX_CONFIG: "access-tokens = git.johnogle.info=${{ secrets.GITEA_ACCESS_TOKEN }}" diff --git a/flake.nix b/flake.nix index 6160bd2..f2110b6 100644 --- a/flake.nix +++ b/flake.nix @@ -234,7 +234,7 @@ ]; }; - # Packages for CI caching (custom packages and qt-pinned) + # Packages for CI caching (custom packages, flake inputs, and qt-pinned) packages = nixpkgs.lib.genAttrs [ "x86_64-linux" "aarch64-linux" ] (system: let pkgs = import nixpkgs { @@ -246,6 +246,9 @@ inherit system; config.allowUnfree = true; }; + # Version strings for flake input packages + beadsRev = builtins.substring 0 8 (inputs.beads.rev or "unknown"); + gastownRev = builtins.substring 0 8 (inputs.gastown.rev or "unknown"); in { "custom-claude-code" = pkgs.custom.claude-code; "custom-app-launcher-server" = pkgs.custom.app-launcher-server; @@ -253,6 +256,15 @@ "custom-tea-rbw" = pkgs.custom.tea-rbw; "custom-rclone-torbox-setup" = pkgs.custom.rclone-torbox-setup; "qt-pinned-jellyfin-media-player" = pkgsQt.jellyfin-media-player; + # Flake input packages (beads, gastown) - these get version from input rev + "custom-beads" = pkgs.callPackage ./packages/beads { + src = inputs.beads; + version = "0.49.6-${beadsRev}"; + }; + "custom-gastown" = pkgs.callPackage ./packages/gastown { + src = inputs.gastown; + version = "unstable-${gastownRev}"; + }; } ); diff --git a/packages/beads/default.nix b/packages/beads/default.nix new file mode 100644 index 0000000..50c7e63 --- /dev/null +++ b/packages/beads/default.nix @@ -0,0 +1,41 @@ +# Beads package - issue tracker for AI-supervised coding workflows +# Takes src as argument so it can be called from both overlay and flake packages +{ lib +, stdenv +, buildGoModule +, fetchurl +, go_1_25 +, git +, pkg-config +, icu +, src +, version ? "unknown" +}: + +let + # nixpkgs ships Go 1.25.5, but beads' dolt deps require Go >= 1.25.6 + go_1_25_6 = go_1_25.overrideAttrs (old: rec { + version = "1.25.6"; + src = fetchurl { + url = "https://go.dev/dl/go${version}.src.tar.gz"; + hash = "sha256-WMv3ceRNdt5vVtGeM7d9dFoeSJNAkih15GWFuXXCsFk="; + }; + }); + buildGoModule_1_25_6 = buildGoModule.override { go = go_1_25_6; }; +in +buildGoModule_1_25_6 { + pname = "beads"; + inherit version src; + subPackages = [ "cmd/bd" ]; + doCheck = false; + # Regenerated vendorHash for commit 6a51223b (dolt server mode, Go 1.25.6) + vendorHash = "sha256-9RMy0+ZBFg1BAl8Z0EuZK4XVm9QYVekS9i/1ErOIB/c="; + nativeBuildInputs = [ git pkg-config ]; + buildInputs = [ icu ]; + meta = with lib; { + description = "beads (bd) - An issue tracker designed for AI-supervised coding workflows"; + homepage = "https://github.com/steveyegge/beads"; + license = licenses.mit; + mainProgram = "bd"; + }; +} diff --git a/packages/gastown/default.nix b/packages/gastown/default.nix new file mode 100644 index 0000000..0cfe604 --- /dev/null +++ b/packages/gastown/default.nix @@ -0,0 +1,38 @@ +# Gastown package - multi-agent workspace manager +# Takes src as argument so it can be called from both overlay and flake packages +{ lib +, buildGoModule +, src +, version ? "unknown" +}: + +buildGoModule { + pname = "gastown"; + inherit version src; + vendorHash = "sha256-ripY9vrYgVW8bngAyMLh0LkU/Xx1UUaLgmAA7/EmWQU="; + subPackages = [ "cmd/gt" ]; + doCheck = false; + + # Must match ldflags from gastown Makefile - BuiltProperly=1 is required + # or gt will error with "This binary was built with 'go build' directly" + ldflags = [ + "-X github.com/steveyegge/gastown/internal/cmd.Version=${version}" + "-X github.com/steveyegge/gastown/internal/cmd.Commit=${version}" + "-X github.com/steveyegge/gastown/internal/cmd.BuildTime=nix-build" + "-X github.com/steveyegge/gastown/internal/cmd.BuiltProperly=1" + ]; + + # Bug fixes not yet merged upstream + patches = [ + ./gastown-fix-validate-recipient.patch + ./gastown-fix-agent-bead-address-title.patch + ./gastown-statusline-optimization.patch + ]; + + meta = with lib; { + description = "Gas Town - multi-agent workspace manager by Steve Yegge"; + homepage = "https://github.com/steveyegge/gastown"; + license = licenses.mit; + mainProgram = "gt"; + }; +} diff --git a/packages/gastown/gastown-fix-agent-bead-address-title.patch b/packages/gastown/gastown-fix-agent-bead-address-title.patch new file mode 100644 index 0000000..7aef29c --- /dev/null +++ b/packages/gastown/gastown-fix-agent-bead-address-title.patch @@ -0,0 +1,15 @@ +diff --git a/internal/mail/router.go b/internal/mail/router.go +--- a/internal/mail/router.go ++++ b/internal/mail/router.go +@@ -315,7 +315,10 @@ func agentBeadToAddress(bead *agentBead) string { + } + + // For other hq- agents, fall back to description parsing +- return parseAgentAddressFromDescription(bead.Description) ++ if bead.Title != "" && strings.Contains(bead.Title, "/") { ++ return bead.Title ++ } ++ return parseAgentAddressFromDescription(bead.Description) + } + + // Handle gt- prefixed IDs (legacy format) diff --git a/packages/gastown/gastown-fix-validate-recipient.patch b/packages/gastown/gastown-fix-validate-recipient.patch new file mode 100644 index 0000000..0d1258f --- /dev/null +++ b/packages/gastown/gastown-fix-validate-recipient.patch @@ -0,0 +1,13 @@ +diff --git a/internal/mail/router.go b/internal/mail/router.go +index b864c069..4b6a045b 100644 +--- a/internal/mail/router.go ++++ b/internal/mail/router.go +@@ -646,7 +646,7 @@ func (r *Router) validateRecipient(identity string) error { + } + + for _, agent := range agents { +- if agentBeadToAddress(agent) == identity { ++ if AddressToIdentity(agentBeadToAddress(agent)) == AddressToIdentity(identity) { + return nil // Found matching agent + } + } diff --git a/packages/gastown/gastown-statusline-optimization.patch b/packages/gastown/gastown-statusline-optimization.patch new file mode 100644 index 0000000..c981f71 --- /dev/null +++ b/packages/gastown/gastown-statusline-optimization.patch @@ -0,0 +1,135 @@ +diff --git a/internal/cmd/statusline.go b/internal/cmd/statusline.go +index 2edf1be8..00253eea 100644 +--- a/internal/cmd/statusline.go ++++ b/internal/cmd/statusline.go +@@ -6,6 +6,7 @@ import ( + "path/filepath" + "sort" + "strings" ++ "time" + + "github.com/spf13/cobra" + "github.com/steveyegge/gastown/internal/beads" +@@ -14,6 +15,37 @@ import ( + "github.com/steveyegge/gastown/internal/tmux" + "github.com/steveyegge/gastown/internal/workspace" + ) ++// statusLineCacheTTL is how long cached status output remains valid. ++const statusLineCacheTTL = 10 * time.Second ++ ++// statusLineCachePath returns the cache file path for a session. ++func statusLineCachePath(session string) string { ++ return filepath.Join(os.TempDir(), fmt.Sprintf("gt-status-%s", session)) ++} ++ ++// getStatusLineCache returns cached status if fresh, empty string otherwise. ++func getStatusLineCache(session string) string { ++ path := statusLineCachePath(session) ++ info, err := os.Stat(path) ++ if err != nil { ++ return "" ++ } ++ if time.Since(info.ModTime()) > statusLineCacheTTL { ++ return "" ++ } ++ data, err := os.ReadFile(path) ++ if err != nil { ++ return "" ++ } ++ return string(data) ++} ++ ++// setStatusLineCache writes status to cache file. ++func setStatusLineCache(session, status string) { ++ path := statusLineCachePath(session) ++ _ = os.WriteFile(path, []byte(status), 0644) ++} ++ + + var ( + statusLineSession string +@@ -34,6 +66,19 @@ func init() { + func runStatusLine(cmd *cobra.Command, args []string) error { + t := tmux.NewTmux() + ++ // Optimization: skip expensive beads queries for detached sessions ++ if statusLineSession != "" { ++ if !t.IsSessionAttached(statusLineSession) { ++ fmt.Print("○ |") ++ return nil ++ } ++ // Check cache for attached sessions too ++ if cached := getStatusLineCache(statusLineSession); cached != "" { ++ fmt.Print(cached) ++ return nil ++ } ++ } ++ + // Get session environment + var rigName, polecat, crew, issue, role string + +@@ -150,7 +195,11 @@ func runWorkerStatusLine(t *tmux.Tmux, session, rigName, polecat, crew, issue st + + // Output + if len(parts) > 0 { +- fmt.Print(strings.Join(parts, " | ") + " |") ++ output := strings.Join(parts, " | ") + " |" ++ if statusLineSession != "" { ++ setStatusLineCache(statusLineSession, output) ++ } ++ fmt.Print(output) + } + + return nil +@@ -389,7 +438,11 @@ func runMayorStatusLine(t *tmux.Tmux) error { + } + } + +- fmt.Print(strings.Join(parts, " | ") + " |") ++ output := strings.Join(parts, " | ") + " |" ++ if statusLineSession != "" { ++ setStatusLineCache(statusLineSession, output) ++ } ++ fmt.Print(output) + return nil + } + +@@ -458,7 +511,11 @@ func runDeaconStatusLine(t *tmux.Tmux) error { + } + } + +- fmt.Print(strings.Join(parts, " | ") + " |") ++ output := strings.Join(parts, " | ") + " |" ++ if statusLineSession != "" { ++ setStatusLineCache(statusLineSession, output) ++ } ++ fmt.Print(output) + return nil + } + +@@ -526,7 +583,11 @@ func runWitnessStatusLine(t *tmux.Tmux, rigName string) error { + } + } + +- fmt.Print(strings.Join(parts, " | ") + " |") ++ output := strings.Join(parts, " | ") + " |" ++ if statusLineSession != "" { ++ setStatusLineCache(statusLineSession, output) ++ } ++ fmt.Print(output) + return nil + } + +@@ -617,7 +678,11 @@ func runRefineryStatusLine(t *tmux.Tmux, rigName string) error { + } + } + +- fmt.Print(strings.Join(parts, " | ") + " |") ++ output := strings.Join(parts, " | ") + " |" ++ if statusLineSession != "" { ++ setStatusLineCache(statusLineSession, output) ++ } ++ fmt.Print(output) + return nil + } +