Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions .github/workflows/build-ghcr-kubeopencode.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: Build GHCR KubeOpenCode

on:
workflow_dispatch:

permissions:
contents: read
packages: write

jobs:
build-kubeopencode:
name: Build and Push Controller Image
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6

- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: "22"

- name: Build UI
run: |
cd ui && npm install && npm run build

- name: Set up QEMU
uses: docker/setup-qemu-action@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4

- name: Login to GHCR
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Build and push kubeopencode
uses: docker/build-push-action@v7
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
cache-from: type=gha
cache-to: type=gha,mode=max
tags: |
ghcr.io/${{ github.repository_owner }}/kubeopencode:${{ github.sha }}
18 changes: 18 additions & 0 deletions internal/controller/server_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,18 @@ const (
// ServerSessionDBPath is the full path to the OpenCode session database.
ServerSessionDBPath = ServerSessionMountPath + "/opencode.db"

// ServerPersistentHomeDir is the HOME directory for persisted server-mode agents.
ServerPersistentHomeDir = ServerSessionMountPath + "/home"

// ServerPersistentXDGConfigHome is the XDG config root for persisted server-mode agents.
ServerPersistentXDGConfigHome = ServerSessionMountPath + "/.config"

// ServerPersistentXDGDataHome is the XDG data root for persisted server-mode agents.
ServerPersistentXDGDataHome = ServerSessionMountPath + "/.local/share"

// ServerPersistentXDGStateHome is the XDG state root for persisted server-mode agents.
ServerPersistentXDGStateHome = ServerSessionMountPath + "/.local/state"

// DefaultSessionPVCSize is the default size for the session PVC.
DefaultSessionPVCSize = "1Gi"

Expand Down Expand Up @@ -244,6 +256,12 @@ func BuildServerDeployment(agent *kubeopenv1alpha1.Agent, agentCfg agentConfig,
Name: OpenCodeDBEnvVar,
Value: ServerSessionDBPath,
})
envVars = append(envVars,
corev1.EnvVar{Name: "HOME", Value: ServerPersistentHomeDir},
corev1.EnvVar{Name: "XDG_CONFIG_HOME", Value: ServerPersistentXDGConfigHome},
corev1.EnvVar{Name: "XDG_DATA_HOME", Value: ServerPersistentXDGDataHome},
corev1.EnvVar{Name: "XDG_STATE_HOME", Value: ServerPersistentXDGStateHome},
)
}

// Add credentials (secrets as env vars or file mounts)
Expand Down
76 changes: 76 additions & 0 deletions internal/controller/server_builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,54 @@ func TestBuildServerDeployment_WithHOMEAndSHELLEnvVars(t *testing.T) {
}
}

func TestBuildServerDeployment_WithSessionPersistenceUsesPersistentHomeAndXDG(t *testing.T) {
agent := &kubeopenv1alpha1.Agent{
ObjectMeta: metav1.ObjectMeta{
Name: "test-agent",
Namespace: "default",
},
Spec: kubeopenv1alpha1.AgentSpec{
Port: 4096,
Persistence: &kubeopenv1alpha1.PersistenceConfig{
Sessions: &kubeopenv1alpha1.VolumePersistence{},
},
},
}

cfg := agentConfig{
executorImage: "test-executor:v1.0.0",
agentImage: "test-agent:v1.0.0",
workspaceDir: "/workspace",
}

deployment := BuildServerDeployment(agent, cfg, defaultSystemConfig(), nil, nil, nil, nil, nil)
if deployment == nil {
t.Fatal("BuildServerDeployment returned nil")
}

container := deployment.Spec.Template.Spec.Containers[0]
envMap := make(map[string]string, len(container.Env))
for _, env := range container.Env {
envMap[env.Name] = env.Value
}

if got := envMap["HOME"]; got != ServerPersistentHomeDir {
t.Errorf("HOME = %q, want %q", got, ServerPersistentHomeDir)
}
if got := envMap["XDG_CONFIG_HOME"]; got != ServerPersistentXDGConfigHome {
t.Errorf("XDG_CONFIG_HOME = %q, want %q", got, ServerPersistentXDGConfigHome)
}
if got := envMap["XDG_DATA_HOME"]; got != ServerPersistentXDGDataHome {
t.Errorf("XDG_DATA_HOME = %q, want %q", got, ServerPersistentXDGDataHome)
}
if got := envMap["XDG_STATE_HOME"]; got != ServerPersistentXDGStateHome {
t.Errorf("XDG_STATE_HOME = %q, want %q", got, ServerPersistentXDGStateHome)
}
if got := envMap["SHELL"]; got != DefaultShell {
t.Errorf("SHELL = %q, want %q", got, DefaultShell)
}
}

func TestBuildServerDeployment_WithTextContext(t *testing.T) {
agent := &kubeopenv1alpha1.Agent{
ObjectMeta: metav1.ObjectMeta{
Expand Down Expand Up @@ -1085,7 +1133,9 @@ func TestBuildServerDeployment_WithSessionPersistence(t *testing.T) {

// Verify OPENCODE_DB env var
var foundEnv bool
envMap := make(map[string]string, len(container.Env))
for _, env := range container.Env {
envMap[env.Name] = env.Value
if env.Name == OpenCodeDBEnvVar {
foundEnv = true
Comment on lines 1134 to 1140
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tests build an envMap from container.Env, which will silently drop duplicates (e.g., two HOME entries). Since the production code currently appends HOME twice when session persistence is enabled, these assertions won’t catch it. Consider adding explicit checks that HOME appears exactly once (and likewise for XDG_*), in addition to validating values.

Copilot uses AI. Check for mistakes.
if env.Value != ServerSessionDBPath {
Expand All @@ -1096,6 +1146,18 @@ func TestBuildServerDeployment_WithSessionPersistence(t *testing.T) {
if !foundEnv {
t.Errorf("%s env var not found", OpenCodeDBEnvVar)
}
if got := envMap["HOME"]; got != ServerPersistentHomeDir {
t.Errorf("HOME = %q, want %q", got, ServerPersistentHomeDir)
}
if got := envMap["XDG_CONFIG_HOME"]; got != ServerPersistentXDGConfigHome {
t.Errorf("XDG_CONFIG_HOME = %q, want %q", got, ServerPersistentXDGConfigHome)
}
if got := envMap["XDG_DATA_HOME"]; got != ServerPersistentXDGDataHome {
t.Errorf("XDG_DATA_HOME = %q, want %q", got, ServerPersistentXDGDataHome)
}
if got := envMap["XDG_STATE_HOME"]; got != ServerPersistentXDGStateHome {
t.Errorf("XDG_STATE_HOME = %q, want %q", got, ServerPersistentXDGStateHome)
}
}

func TestBuildServerDeployment_WithoutSessionPersistence(t *testing.T) {
Expand Down Expand Up @@ -1129,11 +1191,25 @@ func TestBuildServerDeployment_WithoutSessionPersistence(t *testing.T) {

// Verify no OPENCODE_DB env var
container := deployment.Spec.Template.Spec.Containers[0]
envMap := make(map[string]string, len(container.Env))
for _, env := range container.Env {
envMap[env.Name] = env.Value
if env.Name == OpenCodeDBEnvVar {
t.Errorf("%s env var should not be present without persistence config", OpenCodeDBEnvVar)
}
}
if got := envMap["HOME"]; got != DefaultHomeDir {
t.Errorf("HOME = %q, want %q", got, DefaultHomeDir)
}
if _, ok := envMap["XDG_CONFIG_HOME"]; ok {
t.Errorf("XDG_CONFIG_HOME should not be set without session persistence")
}
if _, ok := envMap["XDG_DATA_HOME"]; ok {
t.Errorf("XDG_DATA_HOME should not be set without session persistence")
}
if _, ok := envMap["XDG_STATE_HOME"]; ok {
t.Errorf("XDG_STATE_HOME should not be set without session persistence")
}
}

func TestBuildServerWorkspacePVC(t *testing.T) {
Expand Down