diff --git a/agent/agent_configuration.go b/agent/agent_configuration.go index b467bc51b6..a9b53a3ddd 100644 --- a/agent/agent_configuration.go +++ b/agent/agent_configuration.go @@ -24,6 +24,7 @@ type AgentConfiguration struct { GitCleanFlags string GitFetchFlags string GitSubmodules bool + SkipCheckout bool AllowedRepositories []*regexp.Regexp AllowedPlugins []*regexp.Regexp AllowedEnvironmentVariables []*regexp.Regexp diff --git a/agent/job_runner.go b/agent/job_runner.go index d71a7d974c..e24d9fca03 100644 --- a/agent/job_runner.go +++ b/agent/job_runner.go @@ -577,6 +577,12 @@ BUILDKITE_AGENT_JWKS_KEY_ID` setEnv("BUILDKITE_PLUGINS_PATH", r.conf.AgentConfiguration.PluginsPath) setEnv("BUILDKITE_SSH_KEYSCAN", fmt.Sprint(r.conf.AgentConfiguration.SSHKeyscan)) setEnv("BUILDKITE_GIT_SUBMODULES", fmt.Sprint(r.conf.AgentConfiguration.GitSubmodules)) + // Allow BUILDKITE_SKIP_CHECKOUT to be enabled either by agent config + // or by pipeline/step env + // This is here now to make it ready for if/when we add skip_checkout to the core app + if r.conf.AgentConfiguration.SkipCheckout { + setEnv("BUILDKITE_SKIP_CHECKOUT", "true") + } setEnv("BUILDKITE_COMMAND_EVAL", fmt.Sprint(r.conf.AgentConfiguration.CommandEval)) setEnv("BUILDKITE_PLUGINS_ENABLED", fmt.Sprint(r.conf.AgentConfiguration.PluginsEnabled)) // Allow BUILDKITE_PLUGINS_ALWAYS_CLONE_FRESH to be enabled either by config diff --git a/clicommand/agent_start.go b/clicommand/agent_start.go index 45be848a37..f0afc45bf3 100644 --- a/clicommand/agent_start.go +++ b/clicommand/agent_start.go @@ -150,6 +150,7 @@ type AgentStartConfig struct { GitMirrorsLockTimeout int `cli:"git-mirrors-lock-timeout"` GitMirrorsSkipUpdate bool `cli:"git-mirrors-skip-update"` NoGitSubmodules bool `cli:"no-git-submodules"` + SkipCheckout bool `cli:"skip-checkout"` NoSSHKeyscan bool `cli:"no-ssh-keyscan"` NoCommandEval bool `cli:"no-command-eval"` @@ -624,6 +625,11 @@ var AgentStartCommand = cli.Command{ Usage: "Don't automatically checkout git submodules", EnvVar: "BUILDKITE_NO_GIT_SUBMODULES,BUILDKITE_DISABLE_GIT_SUBMODULES", }, + cli.BoolFlag{ + Name: "skip-checkout", + Usage: "Skip the git checkout phase entirely", + EnvVar: "BUILDKITE_SKIP_CHECKOUT", + }, cli.BoolFlag{ Name: "no-feature-reporting", Usage: "Disables sending a list of enabled features back to the Buildkite mothership. We use this information to measure feature usage, but if you're not comfortable sharing that information then that's totally okay :)", @@ -1040,6 +1046,7 @@ var AgentStartCommand = cli.Command{ GitCleanFlags: cfg.GitCleanFlags, GitFetchFlags: cfg.GitFetchFlags, GitSubmodules: !cfg.NoGitSubmodules, + SkipCheckout: cfg.SkipCheckout, SSHKeyscan: !cfg.NoSSHKeyscan, CommandEval: !cfg.NoCommandEval, PluginsEnabled: !cfg.NoPlugins, diff --git a/clicommand/bootstrap.go b/clicommand/bootstrap.go index 40c6efbda5..b5b1acdd2a 100644 --- a/clicommand/bootstrap.go +++ b/clicommand/bootstrap.go @@ -68,6 +68,7 @@ type BootstrapConfig struct { AutomaticArtifactUploadPaths string `cli:"artifact-upload-paths"` ArtifactUploadDestination string `cli:"artifact-upload-destination"` CleanCheckout bool `cli:"clean-checkout"` + SkipCheckout bool `cli:"skip-checkout"` GitCheckoutFlags string `cli:"git-checkout-flags"` GitCloneFlags string `cli:"git-clone-flags"` GitFetchFlags string `cli:"git-fetch-flags"` @@ -227,6 +228,11 @@ var BootstrapCommand = cli.Command{ Usage: "Whether or not the bootstrap should remove the existing repository before running the command", EnvVar: "BUILDKITE_CLEAN_CHECKOUT", }, + cli.BoolFlag{ + Name: "skip-checkout", + Usage: "Skip the git checkout phase entirely", + EnvVar: "BUILDKITE_SKIP_CHECKOUT", + }, cli.StringFlag{ Name: "git-checkout-flags", Value: "-f", @@ -468,6 +474,7 @@ var BootstrapCommand = cli.Command{ CancelSignal: cancelSig, SignalGracePeriod: signalGracePeriod, CleanCheckout: cfg.CleanCheckout, + SkipCheckout: cfg.SkipCheckout, Command: cfg.Command, CommandEval: cfg.CommandEval, Commit: cfg.Commit, diff --git a/internal/job/checkout.go b/internal/job/checkout.go index 1836c0b948..e59c3c29fc 100644 --- a/internal/job/checkout.go +++ b/internal/job/checkout.go @@ -217,6 +217,11 @@ func (e *Executor) CheckoutPhase(ctx context.Context) error { // checkout runs checkout hook or default checkout logic func (e *Executor) checkout(ctx context.Context) error { + if e.SkipCheckout { + e.shell.Commentf("Skipping checkout, BUILDKITE_SKIP_CHECKOUT is set") + return nil + } + // There can only be one checkout hook, either plugin or global, in that order switch { case e.hasPluginHook("checkout"): diff --git a/internal/job/checkout_test.go b/internal/job/checkout_test.go index fb59c88457..01ee9bec89 100644 --- a/internal/job/checkout_test.go +++ b/internal/job/checkout_test.go @@ -147,6 +147,26 @@ func TestDefaultCheckoutPhase(t *testing.T) { } } +func TestSkipCheckout(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + sh, err := shell.New() + require.NoError(t, err) + + executor := &Executor{ + shell: sh, + ExecutorConfig: ExecutorConfig{ + Repository: "https://github.com/buildkite/agent.git", + SkipCheckout: true, + }, + } + + err = executor.checkout(ctx) + require.NoError(t, err) +} + func TestDefaultCheckoutPhase_DelayedRefCreation(t *testing.T) { if race.IsRaceTest { t.Skip("this test simulates the agent recovering from a race condition, and needs to create one to test it.") diff --git a/internal/job/config.go b/internal/job/config.go index 8e5d374edd..abcbbd200e 100644 --- a/internal/job/config.go +++ b/internal/job/config.go @@ -75,6 +75,9 @@ type ExecutorConfig struct { // Should the executor remove an existing checkout before running the job CleanCheckout bool `env:"BUILDKITE_CLEAN_CHECKOUT"` + // Skip the checkout phase entirely + SkipCheckout bool `env:"BUILDKITE_SKIP_CHECKOUT"` + // Flags to pass to "git checkout" command GitCheckoutFlags string `env:"BUILDKITE_GIT_CHECKOUT_FLAGS"` diff --git a/internal/job/integration/checkout_integration_test.go b/internal/job/integration/checkout_integration_test.go index e1fd8f9791..ebf42051cc 100644 --- a/internal/job/integration/checkout_integration_test.go +++ b/internal/job/integration/checkout_integration_test.go @@ -849,6 +849,27 @@ func TestForcingACleanCheckout(t *testing.T) { } } +func TestSkippingCheckout(t *testing.T) { + t.Parallel() + + tester, err := NewExecutorTester(mainCtx) + if err != nil { + t.Fatalf("NewExecutorTester() error = %v", err) + } + defer tester.Close() + + tester.RunAndCheck(t, "BUILDKITE_SKIP_CHECKOUT=true") + + if !strings.Contains(tester.Output, "Skipping checkout") { + t.Fatal(`tester.Output does not contain "Skipping checkout"`) + } + + // Verify no git commands were run (no clone, fetch, checkout) + if strings.Contains(tester.Output, "git clone") { + t.Fatal(`tester.Output should not contain "git clone" when checkout is skipped`) + } +} + func TestCheckoutOnAnExistingRepositoryWithoutAGitFolder(t *testing.T) { t.Parallel()