From c19e3b0820aed4cad962bb49790874047a7d1465 Mon Sep 17 00:00:00 2001 From: Derek Etherton Date: Mon, 29 Jun 2026 09:36:05 -0700 Subject: [PATCH 1/2] Add RunnerJob.StartupCommands/StartupImage and RunnerJobVariable.Scope These fields let opslevel-runner spin up a startup init container alongside the main job container and partition env vars so credentialed variables can be scoped to the init phase only. Used by the OpenCode coding-agent to hold REPO_CLONE_URL inside an init container that the agent container never sees. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../unreleased/Feature-20260629-093256.yaml | 3 ++ .../unreleased/Feature-20260629-093307.yaml | 3 ++ job.go | 33 +++++++++++++------ job_test.go | 21 ++++++++++-- 4 files changed, 47 insertions(+), 13 deletions(-) create mode 100644 .changes/unreleased/Feature-20260629-093256.yaml create mode 100644 .changes/unreleased/Feature-20260629-093307.yaml diff --git a/.changes/unreleased/Feature-20260629-093256.yaml b/.changes/unreleased/Feature-20260629-093256.yaml new file mode 100644 index 00000000..02117af8 --- /dev/null +++ b/.changes/unreleased/Feature-20260629-093256.yaml @@ -0,0 +1,3 @@ +kind: Feature +body: Add Scope field on RunnerJobVariable so variables can be restricted to the startup init container or the main job container +time: 2026-06-29T09:32:56.728664-07:00 diff --git a/.changes/unreleased/Feature-20260629-093307.yaml b/.changes/unreleased/Feature-20260629-093307.yaml new file mode 100644 index 00000000..20fda80f --- /dev/null +++ b/.changes/unreleased/Feature-20260629-093307.yaml @@ -0,0 +1,3 @@ +kind: Feature +body: Add StartupCommands and StartupImage fields on RunnerJob to support running an init container before the main job container +time: 2026-06-29T09:33:07.696442-07:00 diff --git a/job.go b/job.go index ae129287..e96330f5 100644 --- a/job.go +++ b/job.go @@ -71,10 +71,21 @@ type Runner struct { Status RunnerStatusTypeEnum `json:"status"` } +// RunnerJobVariableScope controls which container(s) on the job pod receive a +// variable. An empty value means the variable is available in both the startup +// init container and the main job container (the default). +type RunnerJobVariableScope string + +const ( + RunnerJobVariableScopeStartup RunnerJobVariableScope = "startup" + RunnerJobVariableScopeMain RunnerJobVariableScope = "main" +) + type RunnerJobVariable struct { - Key string `json:"key"` - Sensitive bool `json:"sensitive"` - Value string `json:"value"` + Key string `json:"key"` + Sensitive bool `json:"sensitive"` + Value string `json:"value"` + Scope RunnerJobVariableScope `json:"scope"` } type RunnerJobFile struct { @@ -83,13 +94,15 @@ type RunnerJobFile struct { } type RunnerJob struct { - Commands []string `json:"commands"` - Id ID `json:"id"` - Image string `json:"image"` - Outcome RunnerJobOutcomeEnum `json:"outcome"` - Status RunnerJobStatusEnum `json:"status"` - Variables []RunnerJobVariable `json:"variables"` - Files []RunnerJobFile `json:"files"` + Commands []string `json:"commands"` + Id ID `json:"id"` + Image string `json:"image"` + Outcome RunnerJobOutcomeEnum `json:"outcome"` + Status RunnerJobStatusEnum `json:"status"` + Variables []RunnerJobVariable `json:"variables"` + Files []RunnerJobFile `json:"files"` + StartupCommands []string `json:"startupCommands"` + StartupImage string `json:"startupImage"` } func (runnerJob *RunnerJob) Number() string { diff --git a/job_test.go b/job_test.go index c75d04f3..96e66c51 100644 --- a/job_test.go +++ b/job_test.go @@ -58,7 +58,7 @@ func TestRunnerGetScale(t *testing.T) { func TestRunnerGetPendingJobs(t *testing.T) { // Arrange testRequest := autopilot.NewTestRequest( - `mutation RunnerGetPendingJob($id:ID!$token:ID){runnerGetPendingJob(runnerId: $id lastUpdateToken: $token){runnerJob{commands,id,image,outcome,status,variables{key,sensitive,value},files{name,contents}},lastUpdateToken,errors{message,path}}}`, + `mutation RunnerGetPendingJob($id:ID!$token:ID){runnerGetPendingJob(runnerId: $id lastUpdateToken: $token){runnerJob{commands,id,image,outcome,status,variables{key,sensitive,value,scope},files{name,contents},startupCommands,startupImage},lastUpdateToken,errors{message,path}}}`, `{"id":"1234567890", "token": "1234"}`, `{"data": { "runnerGetPendingJob": { @@ -75,9 +75,20 @@ func TestRunnerGetPendingJobs(t *testing.T) { "variables": [ { "key": "AWS_ACCESS_KEY", - "value": "XXXXXXX" + "value": "XXXXXXX", + "scope": "main" + }, + { + "key": "REPO_CLONE_URL", + "value": "https://token@example.com/repo.git", + "sensitive": true, + "scope": "startup" } - ] + ], + "startupCommands": [ + "/opslevel/clone-repo ." + ], + "startupImage": "public.ecr.aws/opslevel/cli:v2022.02.25" }, "lastUpdateToken": "12344321", "errors": [] @@ -92,6 +103,10 @@ func TestRunnerGetPendingJobs(t *testing.T) { autopilot.Equals(t, "public.ecr.aws/opslevel/cli:v2022.02.25", result.Image) autopilot.Equals(t, "ls -al", result.Commands[1]) autopilot.Equals(t, ol.ID("12344321"), token) + autopilot.Equals(t, []string{"/opslevel/clone-repo ."}, result.StartupCommands) + autopilot.Equals(t, "public.ecr.aws/opslevel/cli:v2022.02.25", result.StartupImage) + autopilot.Equals(t, ol.RunnerJobVariableScopeMain, result.Variables[0].Scope) + autopilot.Equals(t, ol.RunnerJobVariableScopeStartup, result.Variables[1].Scope) } func TestRunnerAppendJobLog(t *testing.T) { From 63bbe5d32b1d558b10c5b82c0238dfe6bdab4fb5 Mon Sep 17 00:00:00 2001 From: Derek Etherton Date: Mon, 29 Jun 2026 09:41:31 -0700 Subject: [PATCH 2/2] Rename Startup* to Init* across the new fields Match Kubernetes-native terminology (init container, initContainers in the pod spec). Avoids confusion with K8s's unrelated startupProbe concept. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../unreleased/Feature-20260629-093256.yaml | 2 +- .../unreleased/Feature-20260629-093307.yaml | 2 +- job.go | 26 +++++++++---------- job_test.go | 14 +++++----- 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/.changes/unreleased/Feature-20260629-093256.yaml b/.changes/unreleased/Feature-20260629-093256.yaml index 02117af8..20f02db2 100644 --- a/.changes/unreleased/Feature-20260629-093256.yaml +++ b/.changes/unreleased/Feature-20260629-093256.yaml @@ -1,3 +1,3 @@ kind: Feature -body: Add Scope field on RunnerJobVariable so variables can be restricted to the startup init container or the main job container +body: Add Scope field on RunnerJobVariable so variables can be restricted to the init container or the main job container time: 2026-06-29T09:32:56.728664-07:00 diff --git a/.changes/unreleased/Feature-20260629-093307.yaml b/.changes/unreleased/Feature-20260629-093307.yaml index 20fda80f..14289724 100644 --- a/.changes/unreleased/Feature-20260629-093307.yaml +++ b/.changes/unreleased/Feature-20260629-093307.yaml @@ -1,3 +1,3 @@ kind: Feature -body: Add StartupCommands and StartupImage fields on RunnerJob to support running an init container before the main job container +body: Add InitCommands and InitImage fields on RunnerJob to support running an init container before the main job container time: 2026-06-29T09:33:07.696442-07:00 diff --git a/job.go b/job.go index e96330f5..94cc6633 100644 --- a/job.go +++ b/job.go @@ -72,13 +72,13 @@ type Runner struct { } // RunnerJobVariableScope controls which container(s) on the job pod receive a -// variable. An empty value means the variable is available in both the startup -// init container and the main job container (the default). +// variable. An empty value means the variable is available in both the init +// container and the main job container (the default). type RunnerJobVariableScope string const ( - RunnerJobVariableScopeStartup RunnerJobVariableScope = "startup" - RunnerJobVariableScopeMain RunnerJobVariableScope = "main" + RunnerJobVariableScopeInit RunnerJobVariableScope = "init" + RunnerJobVariableScopeMain RunnerJobVariableScope = "main" ) type RunnerJobVariable struct { @@ -94,15 +94,15 @@ type RunnerJobFile struct { } type RunnerJob struct { - Commands []string `json:"commands"` - Id ID `json:"id"` - Image string `json:"image"` - Outcome RunnerJobOutcomeEnum `json:"outcome"` - Status RunnerJobStatusEnum `json:"status"` - Variables []RunnerJobVariable `json:"variables"` - Files []RunnerJobFile `json:"files"` - StartupCommands []string `json:"startupCommands"` - StartupImage string `json:"startupImage"` + Commands []string `json:"commands"` + Id ID `json:"id"` + Image string `json:"image"` + Outcome RunnerJobOutcomeEnum `json:"outcome"` + Status RunnerJobStatusEnum `json:"status"` + Variables []RunnerJobVariable `json:"variables"` + Files []RunnerJobFile `json:"files"` + InitCommands []string `json:"initCommands"` + InitImage string `json:"initImage"` } func (runnerJob *RunnerJob) Number() string { diff --git a/job_test.go b/job_test.go index 96e66c51..2eed7a01 100644 --- a/job_test.go +++ b/job_test.go @@ -58,7 +58,7 @@ func TestRunnerGetScale(t *testing.T) { func TestRunnerGetPendingJobs(t *testing.T) { // Arrange testRequest := autopilot.NewTestRequest( - `mutation RunnerGetPendingJob($id:ID!$token:ID){runnerGetPendingJob(runnerId: $id lastUpdateToken: $token){runnerJob{commands,id,image,outcome,status,variables{key,sensitive,value,scope},files{name,contents},startupCommands,startupImage},lastUpdateToken,errors{message,path}}}`, + `mutation RunnerGetPendingJob($id:ID!$token:ID){runnerGetPendingJob(runnerId: $id lastUpdateToken: $token){runnerJob{commands,id,image,outcome,status,variables{key,sensitive,value,scope},files{name,contents},initCommands,initImage},lastUpdateToken,errors{message,path}}}`, `{"id":"1234567890", "token": "1234"}`, `{"data": { "runnerGetPendingJob": { @@ -82,13 +82,13 @@ func TestRunnerGetPendingJobs(t *testing.T) { "key": "REPO_CLONE_URL", "value": "https://token@example.com/repo.git", "sensitive": true, - "scope": "startup" + "scope": "init" } ], - "startupCommands": [ + "initCommands": [ "/opslevel/clone-repo ." ], - "startupImage": "public.ecr.aws/opslevel/cli:v2022.02.25" + "initImage": "public.ecr.aws/opslevel/cli:v2022.02.25" }, "lastUpdateToken": "12344321", "errors": [] @@ -103,10 +103,10 @@ func TestRunnerGetPendingJobs(t *testing.T) { autopilot.Equals(t, "public.ecr.aws/opslevel/cli:v2022.02.25", result.Image) autopilot.Equals(t, "ls -al", result.Commands[1]) autopilot.Equals(t, ol.ID("12344321"), token) - autopilot.Equals(t, []string{"/opslevel/clone-repo ."}, result.StartupCommands) - autopilot.Equals(t, "public.ecr.aws/opslevel/cli:v2022.02.25", result.StartupImage) + autopilot.Equals(t, []string{"/opslevel/clone-repo ."}, result.InitCommands) + autopilot.Equals(t, "public.ecr.aws/opslevel/cli:v2022.02.25", result.InitImage) autopilot.Equals(t, ol.RunnerJobVariableScopeMain, result.Variables[0].Scope) - autopilot.Equals(t, ol.RunnerJobVariableScopeStartup, result.Variables[1].Scope) + autopilot.Equals(t, ol.RunnerJobVariableScopeInit, result.Variables[1].Scope) } func TestRunnerAppendJobLog(t *testing.T) {