Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
2f64830
Pave road to no-checkout-override
scadu Apr 27, 2026
8df6127
Define checkout override scope for runtime env mutations
scadu Apr 27, 2026
9ff9d6b
Checkout override in executor
scadu Apr 27, 2026
cfdf719
Add unit tests for no-checkout-override
scadu Apr 27, 2026
3354638
Fix env set panic on Job API errors
scadu Apr 27, 2026
16c6049
Add setCheckoutEnv
scadu Apr 27, 2026
435510b
Add integration tests
scadu Apr 27, 2026
b0be76a
Add comment re checkoutOverrideScope
scadu Apr 28, 2026
4781e0b
Set no-checkout-override=true when command-eval disabled
scadu Apr 28, 2026
c588804
Add test for no command eval enabling no checkout override
scadu Apr 29, 2026
7e443c9
Merge pull request #3859 from buildkite/sup-6423-agent-implement-no-c…
scadu Apr 30, 2026
806a0cf
Merge branch 'main' into feat/git-checkout-features
petetomasik May 7, 2026
73973f7
Add skip checkout test cases for no-checkout-override
petetomasik May 7, 2026
f46020d
Fix golangci-lint errors
petetomasik May 7, 2026
54722a4
Merge pull request #3906 from buildkite/SUP-7022-skip-checkout-tests
omehegan May 13, 2026
8f7cd03
Merge branch 'main' into feat/git-checkout-features
petetomasik Jun 9, 2026
c8d072c
Make checkout infra vars agent-only
petetomasik Jun 9, 2026
12273d9
Test no-checkout-override blocks hooks and plugins
petetomasik Jun 9, 2026
cac36b9
Make WithNoCheckoutOverride parameterless
petetomasik Jun 9, 2026
5b5ec06
Merge branch 'main' into feat/git-checkout-features
petetomasik Jun 10, 2026
404c510
Lock mirror checkout mode under no-checkout-override
petetomasik Jun 10, 2026
dc0d80f
Split no-checkout-override lock test by config type
petetomasik Jun 10, 2026
5d94620
Match no-checkout-override flag usage to no-* siblings
petetomasik Jun 10, 2026
42aa644
Merge branch 'main' into feat/git-checkout-features
petetomasik Jun 12, 2026
437842c
Refine checkout-override env scope
petetomasik Jun 12, 2026
7ec4129
Lock checkout vars in createEnvironment
petetomasik Jun 12, 2026
ac89505
Clarify no-checkout-override config docs
petetomasik Jun 12, 2026
4f97330
Improve Job API checkout-lock rejection
petetomasik Jun 12, 2026
14a9313
Test no-command-eval auto-locks checkout
petetomasik Jun 12, 2026
17a0dd7
Merge branch 'main' into feat/no-checkout-override
petetomasik Jun 16, 2026
7efc6c4
Update agent/job_runner.go
petetomasik Jun 24, 2026
2ca7ebe
Make git-submodule-clone-config agent-authoritative
petetomasik Jun 24, 2026
13635d5
Merge branch 'main' into feat/no-checkout-override
petetomasik Jun 24, 2026
a0d2e4e
test: cover sparse-checkout-paths under no-checkout-override
petetomasik Jun 25, 2026
8b9f7be
test: assert no-checkout-override does not over-block unscoped vars
petetomasik Jun 25, 2026
a79dc31
Merge branch 'main' into feat/no-checkout-override
petetomasik Jun 25, 2026
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
1 change: 1 addition & 0 deletions agent/agent_configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ type AgentConfiguration struct {
GitSubmoduleCloneConfig []string
SkipCheckout bool
GitSkipFetchExistingCommits bool
NoCheckoutOverride bool
CheckoutAttempts int
AllowedRepositories []*regexp.Regexp
AllowedPlugins []*regexp.Regexp
Expand Down
369 changes: 369 additions & 0 deletions agent/integration/job_environment_integration_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package integration

import (
"slices"
"strings"
"testing"

Expand Down Expand Up @@ -229,6 +230,374 @@ func TestBuildkiteRequestHeaders(t *testing.T) {
}
}

func TestCheckoutScopedJobEnvOverrideHonorsNoCheckoutOverride(t *testing.T) {
t.Parallel()

tests := []struct {
name string
varName string
jobEnv map[string]string
agentCfg agent.AgentConfiguration
wantEnvValue string
wantIgnoredEnvVars []string
}{
{
name: "disabled_allows_job_env_to_override_clone_flags",
varName: "BUILDKITE_GIT_CLONE_FLAGS",
jobEnv: map[string]string{
"BUILDKITE_GIT_CLONE_FLAGS": "--no-tags",
},
agentCfg: agent.AgentConfiguration{
GitCloneFlags: "--mirror",
},
wantEnvValue: "--no-tags",
},
{
name: "enabled_locks_clone_flags_to_agent_config",
varName: "BUILDKITE_GIT_CLONE_FLAGS",
jobEnv: map[string]string{
"BUILDKITE_GIT_CLONE_FLAGS": "--no-tags",
},
agentCfg: agent.AgentConfiguration{
GitCloneFlags: "--mirror",
NoCheckoutOverride: true,
},
wantEnvValue: "--mirror",
wantIgnoredEnvVars: []string{"BUILDKITE_GIT_CLONE_FLAGS"},
},
{
name: "disabled_allows_job_env_to_enable_submodules",
varName: "BUILDKITE_GIT_SUBMODULES",
jobEnv: map[string]string{
"BUILDKITE_GIT_SUBMODULES": "true",
},
agentCfg: agent.AgentConfiguration{
GitSubmodules: false,
},
wantEnvValue: "true",
},
{
name: "enabled_locks_submodules_to_agent_config",
varName: "BUILDKITE_GIT_SUBMODULES",
jobEnv: map[string]string{
"BUILDKITE_GIT_SUBMODULES": "true",
},
agentCfg: agent.AgentConfiguration{
GitSubmodules: false,
NoCheckoutOverride: true,
},
wantEnvValue: "false",
wantIgnoredEnvVars: []string{"BUILDKITE_GIT_SUBMODULES"},
},
{
name: "disabled_allows_job_env_to_override_skip_checkout",
varName: "BUILDKITE_SKIP_CHECKOUT",
jobEnv: map[string]string{
"BUILDKITE_SKIP_CHECKOUT": "false",
},
agentCfg: agent.AgentConfiguration{
SkipCheckout: true,
},
wantEnvValue: "false",
},
{
name: "enabled_locks_skip_checkout_to_agent_config",
varName: "BUILDKITE_SKIP_CHECKOUT",
jobEnv: map[string]string{
"BUILDKITE_SKIP_CHECKOUT": "false",
},
agentCfg: agent.AgentConfiguration{
SkipCheckout: true,
NoCheckoutOverride: true,
},
wantEnvValue: "true",
wantIgnoredEnvVars: []string{"BUILDKITE_SKIP_CHECKOUT"},
},
{
name: "disabled_allows_job_env_to_override_sparse_checkout_paths",
varName: "BUILDKITE_GIT_SPARSE_CHECKOUT_PATHS",
jobEnv: map[string]string{
"BUILDKITE_GIT_SPARSE_CHECKOUT_PATHS": "job/path",
},
agentCfg: agent.AgentConfiguration{
GitSparseCheckoutPaths: []string{"agent/path"},
},
wantEnvValue: "job/path",
},
{
name: "enabled_locks_sparse_checkout_paths_to_agent_config",
varName: "BUILDKITE_GIT_SPARSE_CHECKOUT_PATHS",
jobEnv: map[string]string{
"BUILDKITE_GIT_SPARSE_CHECKOUT_PATHS": "job/path",
},
agentCfg: agent.AgentConfiguration{
GitSparseCheckoutPaths: []string{"agent/path"},
NoCheckoutOverride: true,
},
wantEnvValue: "agent/path",
wantIgnoredEnvVars: []string{"BUILDKITE_GIT_SPARSE_CHECKOUT_PATHS"},
},
// Inverse cases: when the agent config sits on the side that emits no var
// by default, the lock must still force the agent value (regression for the
// leak where backend job env survived under no-checkout-override).
{
name: "enabled_locks_submodules_on_to_agent_config",
varName: "BUILDKITE_GIT_SUBMODULES",
jobEnv: map[string]string{
"BUILDKITE_GIT_SUBMODULES": "false",
},
agentCfg: agent.AgentConfiguration{
GitSubmodules: true,
NoCheckoutOverride: true,
},
wantEnvValue: "true",
wantIgnoredEnvVars: []string{"BUILDKITE_GIT_SUBMODULES"},
},
{
name: "disabled_allows_job_env_to_disable_submodules",
varName: "BUILDKITE_GIT_SUBMODULES",
jobEnv: map[string]string{
"BUILDKITE_GIT_SUBMODULES": "false",
},
agentCfg: agent.AgentConfiguration{
GitSubmodules: true,
},
wantEnvValue: "false",
},
{
name: "enabled_locks_skip_checkout_off_to_agent_config",
varName: "BUILDKITE_SKIP_CHECKOUT",
jobEnv: map[string]string{
"BUILDKITE_SKIP_CHECKOUT": "true",
},
agentCfg: agent.AgentConfiguration{
SkipCheckout: false,
NoCheckoutOverride: true,
},
wantEnvValue: "false",
wantIgnoredEnvVars: []string{"BUILDKITE_SKIP_CHECKOUT"},
},
{
name: "enabled_locks_skip_fetch_existing_commits_to_agent_config",
varName: "BUILDKITE_GIT_SKIP_FETCH_EXISTING_COMMITS",
jobEnv: map[string]string{
"BUILDKITE_GIT_SKIP_FETCH_EXISTING_COMMITS": "true",
},
agentCfg: agent.AgentConfiguration{
GitSkipFetchExistingCommits: false,
NoCheckoutOverride: true,
},
wantEnvValue: "false",
wantIgnoredEnvVars: []string{"BUILDKITE_GIT_SKIP_FETCH_EXISTING_COMMITS"},
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()

ctx := t.Context()
jobEnv := map[string]string{
"BUILDKITE_COMMAND": "echo hello world",
}
for k, v := range tc.jobEnv {
jobEnv[k] = v
}

job := &api.Job{
ID: "my-job-id",
ChunksMaxSizeBytes: 1024,
Env: jobEnv,
Token: "bkaj_job-token",
}

mb := mockBootstrap(t)
defer mb.CheckAndClose(t) //nolint:errcheck // bintest logs to t

mb.Expect().Once().AndExitWith(0).AndCallFunc(func(c *bintest.Call) {
if got, want := c.GetEnv(tc.varName), tc.wantEnvValue; got != want {
t.Errorf("c.GetEnv(%s) = %q, want %q", tc.varName, got, want)
c.Exit(1)
return
}

ignored := strings.Split(strings.TrimSpace(c.GetEnv("BUILDKITE_IGNORED_ENV")), ",")
for _, wantIgnored := range tc.wantIgnoredEnvVars {
if !slices.Contains(ignored, wantIgnored) {
t.Errorf("BUILDKITE_IGNORED_ENV = %q, want it to contain %q", c.GetEnv("BUILDKITE_IGNORED_ENV"), wantIgnored)
c.Exit(1)
return
}
}
if len(tc.wantIgnoredEnvVars) == 0 && c.GetEnv("BUILDKITE_IGNORED_ENV") != "" {
t.Errorf("BUILDKITE_IGNORED_ENV = %q, want empty", c.GetEnv("BUILDKITE_IGNORED_ENV"))
c.Exit(1)
return
}

c.Exit(0)
})

e := createTestAgentEndpoint()
server := e.server()
defer server.Close()

if err := runJob(t, ctx, testRunJobConfig{
job: job,
server: server,
agentCfg: tc.agentCfg,
mockBootstrap: mb,
}); err != nil {
t.Fatalf("runJob() error = %v", err)
}
})
}
}

func TestCheckoutInfraVarsAreAgentAuthoritative(t *testing.T) {
t.Parallel()

// SSH_KEYSCAN, GIT_MIRRORS_PATH, GIT_MIRRORS_LOCK_TIMEOUT and
// GIT_MIRROR_CHECKOUT_MODE are agent-only: job env cannot override them even
// with no-checkout-override disabled.
tests := []struct {
name string
varName string
jobEnvValue string
agentCfg agent.AgentConfiguration
wantEnvValue string
}{
{
name: "ssh_keyscan",
varName: "BUILDKITE_SSH_KEYSCAN",
jobEnvValue: "false",
agentCfg: agent.AgentConfiguration{SSHKeyscan: true},
wantEnvValue: "true",
},
{
name: "git_mirrors_path",
varName: "BUILDKITE_GIT_MIRRORS_PATH",
jobEnvValue: "/tmp/attacker-mirrors",
agentCfg: agent.AgentConfiguration{GitMirrorsPath: "/agent/mirrors"},
wantEnvValue: "/agent/mirrors",
},
{
name: "git_mirrors_lock_timeout",
varName: "BUILDKITE_GIT_MIRRORS_LOCK_TIMEOUT",
jobEnvValue: "1",
agentCfg: agent.AgentConfiguration{GitMirrorsLockTimeout: 300},
wantEnvValue: "300",
},
{
name: "git_mirror_checkout_mode",
varName: "BUILDKITE_GIT_MIRROR_CHECKOUT_MODE",
jobEnvValue: "id",
agentCfg: agent.AgentConfiguration{GitMirrorCheckoutMode: "raw"},
wantEnvValue: "raw",
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()

ctx := t.Context()
job := &api.Job{
ID: "my-job-id",
ChunksMaxSizeBytes: 1024,
Env: map[string]string{
"BUILDKITE_COMMAND": "echo hello world",
tc.varName: tc.jobEnvValue,
},
Token: "bkaj_job-token",
}

mb := mockBootstrap(t)
defer mb.CheckAndClose(t) //nolint:errcheck // bintest logs to t

mb.Expect().Once().AndExitWith(0).AndCallFunc(func(c *bintest.Call) {
if got, want := c.GetEnv(tc.varName), tc.wantEnvValue; got != want {
t.Errorf("c.GetEnv(%s) = %q, want %q", tc.varName, got, want)
c.Exit(1)
return
}

ignored := strings.Split(strings.TrimSpace(c.GetEnv("BUILDKITE_IGNORED_ENV")), ",")
if !slices.Contains(ignored, tc.varName) {
t.Errorf("BUILDKITE_IGNORED_ENV = %q, want it to contain %q", c.GetEnv("BUILDKITE_IGNORED_ENV"), tc.varName)
c.Exit(1)
return
}

c.Exit(0)
})

e := createTestAgentEndpoint()
server := e.server()
defer server.Close()

if err := runJob(t, ctx, testRunJobConfig{
job: job,
server: server,
agentCfg: tc.agentCfg,
mockBootstrap: mb,
}); err != nil {
t.Fatalf("runJob() error = %v", err)
}
})
}
}

func TestNoCheckoutOverrideFlagIgnoresJobEnvOverride(t *testing.T) {
t.Parallel()

// The agent's no-checkout-override setting is authoritative: a job that
// supplies BUILDKITE_NO_CHECKOUT_OVERRIDE cannot turn the lock off.
ctx := t.Context()
job := &api.Job{
ID: "my-job-id",
ChunksMaxSizeBytes: 1024,
Env: map[string]string{
"BUILDKITE_COMMAND": "echo hello world",
"BUILDKITE_NO_CHECKOUT_OVERRIDE": "false",
},
Token: "bkaj_job-token",
}

mb := mockBootstrap(t)
defer mb.CheckAndClose(t) //nolint:errcheck // bintest logs to t

mb.Expect().Once().AndExitWith(0).AndCallFunc(func(c *bintest.Call) {
if got, want := c.GetEnv("BUILDKITE_NO_CHECKOUT_OVERRIDE"), "true"; got != want {
t.Errorf("c.GetEnv(BUILDKITE_NO_CHECKOUT_OVERRIDE) = %q, want %q", got, want)
c.Exit(1)
return
}

ignored := strings.Split(strings.TrimSpace(c.GetEnv("BUILDKITE_IGNORED_ENV")), ",")
if !slices.Contains(ignored, "BUILDKITE_NO_CHECKOUT_OVERRIDE") {
t.Errorf("BUILDKITE_IGNORED_ENV = %q, want it to contain BUILDKITE_NO_CHECKOUT_OVERRIDE", c.GetEnv("BUILDKITE_IGNORED_ENV"))
c.Exit(1)
return
}

c.Exit(0)
})

e := createTestAgentEndpoint()
server := e.server()
defer server.Close()

if err := runJob(t, ctx, testRunJobConfig{
job: job,
server: server,
agentCfg: agent.AgentConfiguration{NoCheckoutOverride: true},
mockBootstrap: mb,
}); err != nil {
t.Fatalf("runJob() error = %v", err)
}
}

func TestArtifactUploadConcurrencyFromAgentConfigIsSet(t *testing.T) {
t.Parallel()

Expand Down
Loading