From f9be9647e1cc9a89b6e43fc2b4a874f30e8ca54a Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Fri, 5 Jun 2026 10:03:33 -0700 Subject: [PATCH 1/9] X-Smart-Branch-Parent: main From 8c36ddd3cbb078d2124eb94b5471c1e33130122f Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Fri, 5 Jun 2026 10:06:41 -0700 Subject: [PATCH 2/9] =?UTF-8?q?ci:=20release=20workflow=20=E2=80=94=20GoRe?= =?UTF-8?q?leaser=20binaries=20+=20container=20images=20to=20ghcr.io?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - .goreleaser.yaml: cross-compile harness CLI for linux/darwin × amd64/arm64 - release.yml: run GoReleaser on v* tags, create GitHub Release with binaries - images.yml: build and push sandbox + launcher images to ghcr.io on tags and push to main - Update all image refs from quay.io/rcochran/openshell to ghcr.io/robbycochran/harness-openshell - Update Makefile REGISTRY default to ghcr.io Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/images.yml | 72 +++++++++++++++++++++++++++++++++++ .github/workflows/release.yml | 1 - .goreleaser.yaml | 14 +++++++ Makefile | 2 +- cmd/deploy.go | 2 +- cmd/new.go | 2 +- profiles/default.toml | 2 +- 7 files changed, 90 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/images.yml create mode 100644 .goreleaser.yaml diff --git a/.github/workflows/images.yml b/.github/workflows/images.yml new file mode 100644 index 0000000..95c405c --- /dev/null +++ b/.github/workflows/images.yml @@ -0,0 +1,72 @@ +name: Images + +on: + push: + branches: [main] + tags: ["v*"] + +permissions: + contents: read + packages: write + +env: + REGISTRY: ghcr.io + IMAGE_BASE: ghcr.io/${{ github.repository }} + +jobs: + sandbox: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: docker/setup-qemu-action@v3 + - uses: docker/setup-buildx-action@v3 + - uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - uses: docker/metadata-action@v5 + id: meta + with: + images: ${{ env.IMAGE_BASE }} + tags: | + type=raw,value=sandbox,enable={{is_default_branch}} + type=semver,pattern=sandbox-v{{version}} + - uses: docker/build-push-action@v6 + with: + context: sandbox + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + + launcher: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + - name: Build launcher binary + run: cd sandbox/launcher && GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o launcher . + - uses: docker/setup-qemu-action@v3 + - uses: docker/setup-buildx-action@v3 + - uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - uses: docker/metadata-action@v5 + id: meta + with: + images: ${{ env.IMAGE_BASE }} + tags: | + type=raw,value=launcher,enable={{is_default_branch}} + type=semver,pattern=launcher-v{{version}} + - uses: docker/build-push-action@v6 + with: + context: sandbox/launcher + platforms: linux/amd64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 77d21db..1a56a66 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,7 +17,6 @@ jobs: - uses: actions/setup-go@v5 with: go-version-file: go.mod - # TODO: add `make embed-sync` step when embed package is implemented - uses: goreleaser/goreleaser-action@v6 with: args: release --clean diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 0000000..527177f --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,14 @@ +version: 2 + +builds: + - env: + - CGO_ENABLED=0 + goos: + - linux + - darwin + goarch: + - amd64 + - arm64 + +archives: + - format: tar.gz diff --git a/Makefile b/Makefile index 4dcecb2..7c5aa83 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ ## make test-podman # build + test podman only ## make test-ocp # build + test OCP only -REGISTRY ?= quay.io/rcochran/openshell +REGISTRY ?= ghcr.io/robbycochran/harness-openshell PLATFORM := linux/amd64 SANDBOX_IMAGE := $(REGISTRY):sandbox diff --git a/cmd/deploy.go b/cmd/deploy.go index 4b4de6e..3868910 100644 --- a/cmd/deploy.go +++ b/cmd/deploy.go @@ -114,7 +114,7 @@ func deployRemote(harnessDir string, gw gateway.Gateway, kc, clusterRunner k8s.R // Step 4: Helm install status.Step(4, "Deploying gateway via Helm") - sandboxImage := envOr("SANDBOX_IMAGE", "quay.io/rcochran/openshell:sandbox") + sandboxImage := envOr("SANDBOX_IMAGE", "ghcr.io/robbycochran/harness-openshell:sandbox") appsDomain, err := clusterRunner.GetJSONPath(ctx, "ingresses.config.openshift.io/cluster", "{.spec.domain}") if err != nil || appsDomain == "" { diff --git a/cmd/new.go b/cmd/new.go index 8914942..b6ec76f 100644 --- a/cmd/new.go +++ b/cmd/new.go @@ -161,7 +161,7 @@ func newRemote(harnessDir string, gw gateway.Gateway, profileName, sandboxName s "restartPolicy": "Never", "containers": []map[string]any{{ "name": "launcher", - "image": "quay.io/rcochran/openshell:launcher", + "image": "ghcr.io/robbycochran/harness-openshell:launcher", "imagePullPolicy": "Always", "env": []map[string]any{ {"name": "GATEWAY_ENDPOINT", "value": "https://openshell.openshell.svc.cluster.local:8080"}, diff --git a/profiles/default.toml b/profiles/default.toml index a4a7f3d..7b6e030 100644 --- a/profiles/default.toml +++ b/profiles/default.toml @@ -5,7 +5,7 @@ # harness create --profile research # uses profiles/research.toml name = "agent" -image = "quay.io/rcochran/openshell:sandbox" +image = "ghcr.io/robbycochran/harness-openshell:sandbox" command = "claude --bare" keep = true From 68c333fb6f131db7ede45eb4e51c7b0f4ce614ca Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Fri, 5 Jun 2026 10:08:54 -0700 Subject: [PATCH 3/9] ci: build images on PRs (no push) + GHA layer caching - Add pull_request trigger to images workflow (build-only, no push) - Skip docker login on PRs (no registry auth needed for build) - Add GHA cache for Docker layers (cache-from/cache-to) - Validates Dockerfiles build successfully before merge Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/images.yml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/images.yml b/.github/workflows/images.yml index 95c405c..1433ded 100644 --- a/.github/workflows/images.yml +++ b/.github/workflows/images.yml @@ -4,6 +4,7 @@ on: push: branches: [main] tags: ["v*"] + pull_request: permissions: contents: read @@ -21,6 +22,7 @@ jobs: - uses: docker/setup-qemu-action@v3 - uses: docker/setup-buildx-action@v3 - uses: docker/login-action@v3 + if: github.event_name != 'pull_request' with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} @@ -36,9 +38,11 @@ jobs: with: context: sandbox platforms: linux/amd64,linux/arm64 - push: true + push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max launcher: runs-on: ubuntu-latest @@ -52,6 +56,7 @@ jobs: - uses: docker/setup-qemu-action@v3 - uses: docker/setup-buildx-action@v3 - uses: docker/login-action@v3 + if: github.event_name != 'pull_request' with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} @@ -67,6 +72,8 @@ jobs: with: context: sandbox/launcher platforms: linux/amd64 - push: true + push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max From 106acd381118c975fff003325c14bef6efae2f10 Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Fri, 5 Jun 2026 10:18:19 -0700 Subject: [PATCH 4/9] =?UTF-8?q?fix:=20images=20workflow=20=E2=80=94=20path?= =?UTF-8?q?=20filters,=20install=20openshell,=20PR=20tags?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add path filters: only build images when sandbox/, profiles/, or providers.toml change (skip on Go-only changes) - Tags always trigger (no path filter on v* tags) - Install openshell CLI in launcher job (was missing from build context) - Add PR tag patterns so metadata-action generates tags on PRs - Scope GHA caches per image to avoid cross-contamination Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/images.yml | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/.github/workflows/images.yml b/.github/workflows/images.yml index 1433ded..42b4ce4 100644 --- a/.github/workflows/images.yml +++ b/.github/workflows/images.yml @@ -3,8 +3,16 @@ name: Images on: push: branches: [main] + paths: + - 'sandbox/**' + - 'profiles/**' + - 'providers.toml' tags: ["v*"] pull_request: + paths: + - 'sandbox/**' + - 'profiles/**' + - 'providers.toml' permissions: contents: read @@ -34,6 +42,7 @@ jobs: tags: | type=raw,value=sandbox,enable={{is_default_branch}} type=semver,pattern=sandbox-v{{version}} + type=ref,event=pr,prefix=sandbox-pr- - uses: docker/build-push-action@v6 with: context: sandbox @@ -41,8 +50,8 @@ jobs: push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max + cache-from: type=gha,scope=sandbox + cache-to: type=gha,mode=max,scope=sandbox launcher: runs-on: ubuntu-latest @@ -53,7 +62,10 @@ jobs: go-version-file: go.mod - name: Build launcher binary run: cd sandbox/launcher && GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o launcher . - - uses: docker/setup-qemu-action@v3 + - name: Install openshell CLI + run: | + curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | sh + cp ~/.openshell/bin/openshell sandbox/launcher/openshell - uses: docker/setup-buildx-action@v3 - uses: docker/login-action@v3 if: github.event_name != 'pull_request' @@ -68,6 +80,7 @@ jobs: tags: | type=raw,value=launcher,enable={{is_default_branch}} type=semver,pattern=launcher-v{{version}} + type=ref,event=pr,prefix=launcher-pr- - uses: docker/build-push-action@v6 with: context: sandbox/launcher @@ -75,5 +88,5 @@ jobs: push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max + cache-from: type=gha,scope=launcher + cache-to: type=gha,mode=max,scope=launcher From fbb9bfd8c7c860c602d486d572978d4101a07249 Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Fri, 5 Jun 2026 10:53:30 -0700 Subject: [PATCH 5/9] feat: rename profile image to from, support Dockerfile builds The from field matches openshell --from flag and accepts both: - Dockerfile path: from = "sandbox/" (builds locally via Docker/Podman) - Image ref: from = "ghcr.io/robbycochran/harness-openshell:sandbox" Default profile now uses from = "sandbox/" for local Dockerfile builds. Remote deploys auto-detect Dockerfile paths and substitute the registry image (SANDBOX_IMAGE env var or ghcr.io default). Co-Authored-By: Claude Opus 4.6 (1M context) --- cmd/helpers_test.go | 2 +- cmd/new.go | 25 +++++++++++++++++++++++-- cmd/new_test.go | 4 ++-- internal/gateway/cli.go | 4 ++-- internal/gateway/cli_test.go | 2 +- internal/gateway/gateway.go | 2 +- internal/profile/profile.go | 2 +- internal/profile/profile_test.go | 6 +++--- profiles/default.toml | 2 +- sandbox/launcher/main.go | 8 ++++---- sandbox/launcher/main_test.go | 8 ++++---- 11 files changed, 43 insertions(+), 22 deletions(-) diff --git a/cmd/helpers_test.go b/cmd/helpers_test.go index 6b377c9..8fa0e60 100644 --- a/cmd/helpers_test.go +++ b/cmd/helpers_test.go @@ -73,7 +73,7 @@ func setupTestProfile(t *testing.T) string { os.MkdirAll(filepath.Join(dir, "profiles"), 0o755) os.WriteFile(filepath.Join(dir, "profiles", "default.toml"), []byte(` name = "test-agent" -image = "quay.io/test:latest" +from = "quay.io/test:latest" command = "claude --bare" providers = ["github", "vertex-local", "atlassian"] diff --git a/cmd/new.go b/cmd/new.go index b6ec76f..9cfec17 100644 --- a/cmd/new.go +++ b/cmd/new.go @@ -111,6 +111,19 @@ func newRemote(harnessDir string, gw gateway.Gateway, profileName, sandboxName s cfg.Name = sandboxName } + // Remote can't build from a local Dockerfile — override with registry image + if cfg.From != "" { + fromPath := cfg.From + if !filepath.IsAbs(fromPath) { + fromPath = filepath.Join(harnessDir, fromPath) + } + if info, err := os.Stat(fromPath); err == nil && info.IsDir() { + sandboxImage := envOr("SANDBOX_IMAGE", "ghcr.io/robbycochran/harness-openshell:sandbox") + status.Infof("Remote: using image %s (profile 'from' is a local path)", sandboxImage) + cfg.From = sandboxImage + } + } + profilePath := filepath.Join(harnessDir, "profiles", profileName+".toml") // 1. ConfigMap from profile @@ -262,10 +275,18 @@ func newLocal(opts newLocalOpts) error { cfg.Name = opts.sandboxName } + // Resolve Dockerfile path relative to harnessDir + if cfg.From != "" && !filepath.IsAbs(cfg.From) { + candidate := filepath.Join(opts.harnessDir, cfg.From) + if info, err := os.Stat(candidate); err == nil && info.IsDir() { + cfg.From = candidate + } + } + fmt.Println() fmt.Println("=== Sandbox ===") fmt.Printf(" Profile: %s\n", opts.profileName) - fmt.Printf(" Image: %s\n", cfg.Image) + fmt.Printf(" From: %s\n", cfg.From) // 4. Validate providers against profile status.Section("Providers") @@ -323,7 +344,7 @@ func newLocal(opts newLocalOpts) error { for attempt := 1; attempt <= 5; attempt++ { err := gw.SandboxCreate(gateway.SandboxCreateOpts{ Name: cfg.Name, - Image: cfg.Image, + From: cfg.From, Providers: registered, TTY: !opts.noTTY, Keep: cfg.KeepSandbox(), diff --git a/cmd/new_test.go b/cmd/new_test.go index 8414140..402dafb 100644 --- a/cmd/new_test.go +++ b/cmd/new_test.go @@ -159,8 +159,8 @@ func TestNewLocal_SandboxCreateOpts(t *testing.T) { if opts.Name != "custom-name" { t.Errorf("Name = %q, want custom-name", opts.Name) } - if opts.Image != "quay.io/test:latest" { - t.Errorf("Image = %q", opts.Image) + if opts.From != "quay.io/test:latest" { + t.Errorf("From = %q", opts.From) } if opts.TTY { t.Error("TTY = true, want false (noTTY)") diff --git a/internal/gateway/cli.go b/internal/gateway/cli.go index 8338d92..5775b96 100644 --- a/internal/gateway/cli.go +++ b/internal/gateway/cli.go @@ -186,8 +186,8 @@ func (c *CLI) SandboxCreate(opts SandboxCreateOpts) error { } else { args = append(args, "--no-tty") } - if opts.Image != "" { - args = append(args, "--from", opts.Image) + if opts.From != "" { + args = append(args, "--from", opts.From) } for _, p := range opts.Providers { args = append(args, "--provider", p) diff --git a/internal/gateway/cli_test.go b/internal/gateway/cli_test.go index 2505eb6..5e17fb1 100644 --- a/internal/gateway/cli_test.go +++ b/internal/gateway/cli_test.go @@ -136,7 +136,7 @@ echo "$@" > `+argsFile+` gw := New(bin) gw.SandboxCreate(SandboxCreateOpts{ Name: "my-agent", - Image: "quay.io/test:latest", + From: "quay.io/test:latest", Providers: []string{"github", "vertex-local"}, TTY: true, Keep: false, diff --git a/internal/gateway/gateway.go b/internal/gateway/gateway.go index 26ca80c..3be82e2 100644 --- a/internal/gateway/gateway.go +++ b/internal/gateway/gateway.go @@ -47,7 +47,7 @@ type GatewayInfo struct { type SandboxCreateOpts struct { Name string - Image string + From string Providers []string TTY bool Keep bool diff --git a/internal/profile/profile.go b/internal/profile/profile.go index 8e933a3..9b6c3c4 100644 --- a/internal/profile/profile.go +++ b/internal/profile/profile.go @@ -19,7 +19,7 @@ type ProviderChecker interface { type Config struct { Name string `toml:"name"` - Image string `toml:"image"` + From string `toml:"from"` Command string `toml:"command"` Keep *bool `toml:"keep"` Providers []string `toml:"providers"` diff --git a/internal/profile/profile_test.go b/internal/profile/profile_test.go index 9a28616..894ae3e 100644 --- a/internal/profile/profile_test.go +++ b/internal/profile/profile_test.go @@ -25,7 +25,7 @@ func TestParseFile_Full(t *testing.T) { path := filepath.Join(dir, "config.toml") os.WriteFile(path, []byte(` name = "research" -image = "quay.io/test/sandbox:latest" +from = "quay.io/test/sandbox:latest" command = "claude --bare --model opus" keep = false providers = ["github", "vertex-local"] @@ -42,8 +42,8 @@ JIRA_URL = "https://example.atlassian.net" if cfg.Name != "research" { t.Errorf("Name = %q, want %q", cfg.Name, "research") } - if cfg.Image != "quay.io/test/sandbox:latest" { - t.Errorf("Image = %q", cfg.Image) + if cfg.From != "quay.io/test/sandbox:latest" { + t.Errorf("From = %q", cfg.From) } if cfg.Command != "claude --bare --model opus" { t.Errorf("Command = %q", cfg.Command) diff --git a/profiles/default.toml b/profiles/default.toml index 7b6e030..a59f07f 100644 --- a/profiles/default.toml +++ b/profiles/default.toml @@ -5,7 +5,7 @@ # harness create --profile research # uses profiles/research.toml name = "agent" -image = "ghcr.io/robbycochran/harness-openshell:sandbox" +from = "sandbox/" command = "claude --bare" keep = true diff --git a/sandbox/launcher/main.go b/sandbox/launcher/main.go index a0f9479..17f0abb 100644 --- a/sandbox/launcher/main.go +++ b/sandbox/launcher/main.go @@ -15,7 +15,7 @@ import ( type Config struct { Name string `toml:"name"` - Image string `toml:"image"` + From string `toml:"from"` Command string `toml:"command"` Keep *bool `toml:"keep"` Providers []string `toml:"providers"` @@ -160,8 +160,8 @@ func createSandbox(cfg *Config, providers []string, cli string) error { fmt.Println("\n=== Creating sandbox ===") for attempt := 1; attempt <= 5; attempt++ { args := []string{"sandbox", "create", "--name", cfg.Name, "--no-tty"} - if cfg.Image != "" { - args = append(args, "--from", cfg.Image) + if cfg.From != "" { + args = append(args, "--from", cfg.From) } for _, p := range providers { args = append(args, "--provider", p) @@ -261,7 +261,7 @@ func main() { fmt.Println("=== Sandbox Launcher ===") fmt.Printf(" Name: %s\n", cfg.Name) - fmt.Printf(" Image: %s\n", cfg.Image) + fmt.Printf(" From: %s\n", cfg.From) fmt.Printf(" Providers: %s\n", strings.Join(cfg.Providers, " ")) fmt.Printf(" Command: %s\n", cfg.Command) fmt.Printf(" Gateway: %s\n", endpoint) diff --git a/sandbox/launcher/main_test.go b/sandbox/launcher/main_test.go index c80a027..b9458ab 100644 --- a/sandbox/launcher/main_test.go +++ b/sandbox/launcher/main_test.go @@ -11,7 +11,7 @@ func TestParseConfig_Full(t *testing.T) { path := filepath.Join(dir, "config.toml") os.WriteFile(path, []byte(` name = "research" -image = "quay.io/test/sandbox:latest" +from = "quay.io/test/sandbox:latest" command = "claude --bare --model opus" keep = false providers = ["github", "vertex-local"] @@ -28,8 +28,8 @@ JIRA_URL = "https://example.atlassian.net" if cfg.Name != "research" { t.Errorf("Name = %q, want %q", cfg.Name, "research") } - if cfg.Image != "quay.io/test/sandbox:latest" { - t.Errorf("Image = %q, want %q", cfg.Image, "quay.io/test/sandbox:latest") + if cfg.From != "quay.io/test/sandbox:latest" { + t.Errorf("From = %q, want %q", cfg.From, "quay.io/test/sandbox:latest") } if cfg.Command != "claude --bare --model opus" { t.Errorf("Command = %q, want %q", cfg.Command, "claude --bare --model opus") @@ -55,7 +55,7 @@ func TestParseConfig_Defaults(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "config.toml") os.WriteFile(path, []byte(` -image = "quay.io/test/sandbox:latest" +from = "quay.io/test/sandbox:latest" `), 0o644) cfg, err := parseConfig(path) From 9a66446e32484fcffd45f309004ee75c93ad0a8e Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Fri, 5 Jun 2026 10:55:00 -0700 Subject: [PATCH 6/9] =?UTF-8?q?ci:=20remove=20path=20filters=20from=20imag?= =?UTF-8?q?es=20workflow=20=E2=80=94=20build=20on=20every=20push?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/images.yml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/.github/workflows/images.yml b/.github/workflows/images.yml index 42b4ce4..5103061 100644 --- a/.github/workflows/images.yml +++ b/.github/workflows/images.yml @@ -3,16 +3,8 @@ name: Images on: push: branches: [main] - paths: - - 'sandbox/**' - - 'profiles/**' - - 'providers.toml' tags: ["v*"] pull_request: - paths: - - 'sandbox/**' - - 'profiles/**' - - 'providers.toml' permissions: contents: read From 2c9e1dd5976b619d91eca654c3e89e7e6375ef23 Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Fri, 5 Jun 2026 10:56:00 -0700 Subject: [PATCH 7/9] ci: path filters + registry-based hierarchical caching for images - Restore path filters: only build when sandbox/, profiles/, or providers.toml change (tags always build) - Switch from GHA cache to registry cache (stored as *-cache tags) - PRs pull cache from main layers, only rebuild changed layers - Always login to registry (needed to read cache on PRs) Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/images.yml | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/.github/workflows/images.yml b/.github/workflows/images.yml index 5103061..5fe7dba 100644 --- a/.github/workflows/images.yml +++ b/.github/workflows/images.yml @@ -3,8 +3,16 @@ name: Images on: push: branches: [main] + paths: + - 'sandbox/**' + - 'profiles/**' + - 'providers.toml' tags: ["v*"] pull_request: + paths: + - 'sandbox/**' + - 'profiles/**' + - 'providers.toml' permissions: contents: read @@ -22,7 +30,6 @@ jobs: - uses: docker/setup-qemu-action@v3 - uses: docker/setup-buildx-action@v3 - uses: docker/login-action@v3 - if: github.event_name != 'pull_request' with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} @@ -42,8 +49,10 @@ jobs: push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha,scope=sandbox - cache-to: type=gha,mode=max,scope=sandbox + cache-from: | + type=registry,ref=${{ env.IMAGE_BASE }}:sandbox-cache + type=registry,ref=${{ env.IMAGE_BASE }}:sandbox + cache-to: type=registry,ref=${{ env.IMAGE_BASE }}:sandbox-cache,mode=max launcher: runs-on: ubuntu-latest @@ -60,7 +69,6 @@ jobs: cp ~/.openshell/bin/openshell sandbox/launcher/openshell - uses: docker/setup-buildx-action@v3 - uses: docker/login-action@v3 - if: github.event_name != 'pull_request' with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} @@ -80,5 +88,7 @@ jobs: push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha,scope=launcher - cache-to: type=gha,mode=max,scope=launcher + cache-from: | + type=registry,ref=${{ env.IMAGE_BASE }}:launcher-cache + type=registry,ref=${{ env.IMAGE_BASE }}:launcher + cache-to: type=registry,ref=${{ env.IMAGE_BASE }}:launcher-cache,mode=max From 57e3673e22303d3614b95fc8d654a4d998a441c2 Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Fri, 5 Jun 2026 11:01:32 -0700 Subject: [PATCH 8/9] fix: use which openshell for portable binary path in CI The deb install puts openshell at /usr/bin/openshell, not ~/.openshell/bin/. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/images.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/images.yml b/.github/workflows/images.yml index 5fe7dba..ea7a601 100644 --- a/.github/workflows/images.yml +++ b/.github/workflows/images.yml @@ -66,7 +66,7 @@ jobs: - name: Install openshell CLI run: | curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | sh - cp ~/.openshell/bin/openshell sandbox/launcher/openshell + cp "$(which openshell)" sandbox/launcher/openshell - uses: docker/setup-buildx-action@v3 - uses: docker/login-action@v3 with: From 6812027a926cb6fb2126c71dced19081d6487295 Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Fri, 5 Jun 2026 11:05:46 -0700 Subject: [PATCH 9/9] fix: use quay.io image in default profile for now The Dockerfile build (from = "sandbox/") fails on podman gateway because openshell builds to Docker daemon but the gateway pulls from Podman image store. Revert to image ref until openshell supports podman-native builds. ghcr.io image will be available after this PR merges and images workflow pushes to main. Will switch to ghcr.io ref in a follow-up. Co-Authored-By: Claude Opus 4.6 (1M context) --- profiles/default.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/profiles/default.toml b/profiles/default.toml index a59f07f..cd77d71 100644 --- a/profiles/default.toml +++ b/profiles/default.toml @@ -5,7 +5,7 @@ # harness create --profile research # uses profiles/research.toml name = "agent" -from = "sandbox/" +from = "quay.io/rcochran/openshell:sandbox" command = "claude --bare" keep = true