clicommand

package
v3.125.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 4, 2026 License: MIT Imports: 79 Imported by: 2

Documentation

Overview

Package clicommand contains the definitions of buildkite-agent subcommands.

It is intended for internal use by buildkite-agent only.

Index

Constants

View Source
const (
	FormatStringJSON = "json"
	FormatStringNone = "none"
)

Note: if you add a new format string, make sure to add it to `secretsFormats` and update the usage string in LogRedactCommand

View Source
const (
	DefaultEndpoint = "https://agent-edge.buildkite.com/v3"
)

Variables

View Source
var (
	AgentAccessTokenFlag = cli.StringFlag{
		Name:   "agent-access-token",
		Value:  "",
		Usage:  "The access token used to identify the agent",
		EnvVar: "BUILDKITE_AGENT_ACCESS_TOKEN",
	}

	AgentRegisterTokenFlag = cli.StringFlag{
		Name:   "token",
		Value:  "",
		Usage:  "Your account agent token",
		EnvVar: "BUILDKITE_AGENT_TOKEN",
	}

	EndpointFlag = cli.StringFlag{
		Name:   "endpoint",
		Value:  DefaultEndpoint,
		Usage:  "The Agent API endpoint",
		EnvVar: "BUILDKITE_AGENT_ENDPOINT",
	}

	NoHTTP2Flag = cli.BoolFlag{
		Name:   "no-http2",
		Usage:  "Disable HTTP2 when communicating with the Agent API (default: false)",
		EnvVar: "BUILDKITE_NO_HTTP2",
	}

	DebugFlag = cli.BoolFlag{
		Name:   "debug",
		Usage:  "Enable debug mode. Synonym for ′--log-level debug′. Takes precedence over ′--log-level′ (default: false)",
		EnvVar: "BUILDKITE_AGENT_DEBUG",
	}

	LogLevelFlag = cli.StringFlag{
		Name:   "log-level",
		Value:  "notice",
		Usage:  "Set the log level for the agent, making logging more or less verbose. Defaults to notice. Allowed values are: debug, info, error, warn, fatal",
		EnvVar: "BUILDKITE_AGENT_LOG_LEVEL",
	}

	ProfileFlag = cli.StringFlag{
		Name:   "profile",
		Usage:  "Enable a profiling mode, either cpu, memory, mutex or block",
		EnvVar: "BUILDKITE_AGENT_PROFILE",
	}

	DebugHTTPFlag = cli.BoolFlag{
		Name:   "debug-http",
		Usage:  "Enable HTTP debug mode, which dumps all request and response bodies to the log (default: false)",
		EnvVar: "BUILDKITE_AGENT_DEBUG_HTTP",
	}

	TraceHTTPFlag = cli.BoolFlag{
		Name:   "trace-http",
		Usage:  "Enable HTTP trace mode, which logs timings for each HTTP request. Timings are logged at the debug level unless a request fails at the network level in which case they are logged at the error level (default: false)",
		EnvVar: "BUILDKITE_AGENT_TRACE_HTTP",
	}

	NoColorFlag = cli.BoolFlag{
		Name:   "no-color",
		Usage:  "Don't show colors in logging (default: false)",
		EnvVar: "BUILDKITE_AGENT_NO_COLOR",
	}

	StrictSingleHooksFlag = cli.BoolFlag{
		Name:   "strict-single-hooks",
		Usage:  "Enforces that only one checkout hook, and only one command hook, can be run (default: false)",
		EnvVar: "BUILDKITE_STRICT_SINGLE_HOOKS",
	}

	KubernetesContainerIDFlag = cli.IntFlag{
		Name: "kubernetes-container-id",
		Usage: "This is intended to be used only by the Buildkite k8s stack " +
			"(github.com/buildkite/agent-stack-k8s); it sets an ID number " +
			"used to identify this container within the pod",
		EnvVar: "BUILDKITE_CONTAINER_ID",
	}

	KubernetesLogCollectionGracePeriodFlag = cli.DurationFlag{
		Name:   "kubernetes-log-collection-grace-period",
		Usage:  "Deprecated, do not use",
		EnvVar: "BUILDKITE_KUBERNETES_LOG_COLLECTION_GRACE_PERIOD",
		Value:  50 * time.Second,
	}

	NoMultipartArtifactUploadFlag = cli.BoolFlag{
		Name:   "no-multipart-artifact-upload",
		Usage:  "For Buildkite-hosted artifacts, disables the use of multipart uploads. Has no effect on uploads to other destinations such as custom cloud buckets (default: false)",
		EnvVar: "BUILDKITE_NO_MULTIPART_ARTIFACT_UPLOAD",
	}

	ExperimentsFlag = cli.StringSliceFlag{
		Name:   "experiment",
		Value:  &cli.StringSlice{},
		Usage:  "Enable experimental features within the buildkite-agent",
		EnvVar: "BUILDKITE_AGENT_EXPERIMENT",
	}

	RedactedVars = cli.StringSliceFlag{
		Name:   "redacted-vars",
		Usage:  "Pattern of environment variable names containing sensitive values",
		EnvVar: "BUILDKITE_REDACTED_VARS",
		Value: &cli.StringSlice{
			"*_PASSWORD",
			"*_SECRET",
			"*_TOKEN",
			"*_PRIVATE_KEY",
			"*_ACCESS_KEY",
			"*_SECRET_KEY",

			"*_CONNECTION_STRING",
			"*_API_KEY",
		},
	}

	TraceContextEncodingFlag = cli.StringFlag{
		Name:   "trace-context-encoding",
		Usage:  "Sets the inner encoding for BUILDKITE_TRACE_CONTEXT. Must be either json or gob",
		Value:  "gob",
		EnvVar: "BUILDKITE_TRACE_CONTEXT_ENCODING",
	}
)
View Source
var (
	BuildPathFlag = cli.StringFlag{
		Name:   "build-path",
		Value:  "",
		Usage:  "Path to where the builds will run from",
		EnvVar: "BUILDKITE_BUILD_PATH",
	}

	HooksPathFlag = cli.StringFlag{
		Name:   "hooks-path",
		Value:  "",
		Usage:  "Directory where the hook scripts are found",
		EnvVar: "BUILDKITE_HOOKS_PATH",
	}

	AdditionalHooksPathsFlag = cli.StringSliceFlag{
		Name:   "additional-hooks-paths",
		Value:  &cli.StringSlice{},
		Usage:  "Additional directories to look for agent hooks",
		EnvVar: "BUILDKITE_ADDITIONAL_HOOKS_PATHS",
	}

	SocketsPathFlag = cli.StringFlag{
		Name:   "sockets-path",
		Value:  defaultSocketsPath(),
		Usage:  "Directory where the agent will place sockets",
		EnvVar: "BUILDKITE_SOCKETS_PATH",
	}

	PluginsPathFlag = cli.StringFlag{
		Name:   "plugins-path",
		Value:  "",
		Usage:  "Directory where the plugins are saved to",
		EnvVar: "BUILDKITE_PLUGINS_PATH",
	}
)

File path flags shared between agent start and bootstrap

View Source
var (
	SkipCheckoutFlag = cli.BoolFlag{
		Name:   "skip-checkout",
		Usage:  "Skip the git checkout phase entirely",
		EnvVar: "BUILDKITE_SKIP_CHECKOUT",
	}

	GitCheckoutFlagsFlag = cli.StringFlag{
		Name:   "git-checkout-flags",
		Value:  "-f",
		Usage:  "Flags to pass to \"git checkout\" command",
		EnvVar: "BUILDKITE_GIT_CHECKOUT_FLAGS",
	}

	GitCloneFlagsFlag = cli.StringFlag{
		Name:   "git-clone-flags",
		Value:  "-v",
		Usage:  "Flags to pass to \"git clone\" command",
		EnvVar: "BUILDKITE_GIT_CLONE_FLAGS",
	}

	GitCloneMirrorFlagsFlag = cli.StringFlag{
		Name:   "git-clone-mirror-flags",
		Value:  "-v",
		Usage:  "Flags to pass to \"git clone\" command when mirroring",
		EnvVar: "BUILDKITE_GIT_CLONE_MIRROR_FLAGS",
	}

	GitCleanFlagsFlag = cli.StringFlag{
		Name:   "git-clean-flags",
		Value:  "-ffxdq",
		Usage:  "Flags to pass to \"git clean\" command",
		EnvVar: "BUILDKITE_GIT_CLEAN_FLAGS",
	}

	GitFetchFlagsFlag = cli.StringFlag{
		Name:   "git-fetch-flags",
		Value:  "-v --prune",
		Usage:  "Flags to pass to \"git fetch\" command",
		EnvVar: "BUILDKITE_GIT_FETCH_FLAGS",
	}

	GitMirrorsPathFlag = cli.StringFlag{
		Name:   "git-mirrors-path",
		Value:  "",
		Usage:  "Path to where mirrors of git repositories are stored",
		EnvVar: "BUILDKITE_GIT_MIRRORS_PATH",
	}

	GitMirrorCheckoutModeFlag = cli.StringFlag{
		Name:   "git-mirror-checkout-mode",
		Value:  "reference",
		Usage:  fmt.Sprintf("Changes how clones of a mirror are made; available modes are %v. In ′dissociate′ mode, clones from a mirror uses the git clone ′--dissociate′ flag, which copies underlying objects from the mirror, making the clone robust to changes in the mirror such as garbage collection, at the expense of additional disk usage and setup time. ′reference′ mode does not pass ′--dissociate′, which causes the clone to directly use objects from the mirror, which is more fragile and can cause the clone to break under entirely normal operation of the mirror, but is slightly faster to clone and uses less disk space.", mirrorCheckoutModes),
		EnvVar: "BUILDKITE_GIT_MIRROR_CHECKOUT_MODE",
	}

	GitMirrorsLockTimeoutFlag = cli.IntFlag{
		Name:   "git-mirrors-lock-timeout",
		Value:  300,
		Usage:  "Seconds to lock a git mirror during clone, should exceed your longest checkout",
		EnvVar: "BUILDKITE_GIT_MIRRORS_LOCK_TIMEOUT",
	}

	GitMirrorsSkipUpdateFlag = cli.BoolFlag{
		Name:   "git-mirrors-skip-update",
		Usage:  "Skip updating the Git mirror (default: false)",
		EnvVar: "BUILDKITE_GIT_MIRRORS_SKIP_UPDATE",
	}

	GitSubmoduleCloneConfigFlag = cli.StringSliceFlag{
		Name:   "git-submodule-clone-config",
		Value:  &cli.StringSlice{},
		Usage:  "Comma separated key=value git config pairs applied before git submodule clone commands such as ′update --init′. If the config is needed to be applied to all git commands, supply it in a global git config file for the system that the agent runs in instead",
		EnvVar: "BUILDKITE_GIT_SUBMODULE_CLONE_CONFIG",
	}

	GitSkipFetchExistingCommitsFlag = cli.BoolFlag{
		Name:   "git-skip-fetch-existing-commits",
		Usage:  "Skip git fetch if the commit already exists in the local git directory (default: false)",
		EnvVar: "BUILDKITE_GIT_SKIP_FETCH_EXISTING_COMMITS",
	}

	CheckoutAttemptsFlag = cli.IntFlag{
		Name:   "checkout-attempts",
		Value:  6,
		Usage:  "Number of checkout attempts (including the initial attempt). Failed attempts are retried with exponential backoff (factor of 2, starting at 1s: 1s, 2s, 4s, ...)",
		EnvVar: "BUILDKITE_CHECKOUT_ATTEMPTS",
	}
)

Git related flags shared between agent start and bootstrap

View Source
var (
	ErrNoPipeline = errors.New("no pipeline file found")
	ErrUseGraphQL = errors.New(
		"either provide the pipeline YAML, and the repository URL, " +
			"or provide a GraphQL token to allow them to be retrieved from Buildkite",
	)
	ErrNotFound = errors.New("pipeline not found")
)
View Source
var AcknowledgementsCommand = cli.Command{
	Name:        "acknowledgements",
	Usage:       "Prints the licenses and notices of open source software incorporated into this software.",
	Description: acknowledgementsHelpDescription,
	Action: func(c *cli.Context) error {
		ctx := context.Background()
		_, _, _, _, done := setupLoggerAndConfig[AcknowledgementsConfig](ctx, c)
		defer done()

		f, err := files.Open("ACKNOWLEDGEMENTS.md.gz")
		if err != nil {
			f, err = files.Open("dummy.md.gz")
			if err != nil {
				return fmt.Errorf("couldn't open any embedded acknowledgements files: %w", err)
			}
		}
		r, err := gzip.NewReader(f)
		if err != nil {
			return fmt.Errorf("couldn't create a gzip reader: %w", err)
		}
		if _, err := io.Copy(c.App.Writer, r); err != nil {
			return fmt.Errorf("couldn't copy acknowledgments to output: %w", err)
		}
		return nil
	},
}
View Source
var AgentPauseCommand = cli.Command{
	Name:        "pause",
	Category:    categoryJobCommands,
	Usage:       "Pause the agent",
	Description: pauseDescription,
	Flags: slices.Concat(globalFlags(), apiFlags(), []cli.Flag{
		cli.StringFlag{
			Name:   "note",
			Usage:  "A descriptive note to record why the agent is paused",
			EnvVar: "BUILDKITE_AGENT_PAUSE_NOTE",
		},
		cli.IntFlag{
			Name:   "timeout-in-minutes",
			Usage:  "Timeout after which the agent is automatically resumed, in minutes",
			EnvVar: "BUILDKITE_AGENT_PAUSE_TIMEOUT_MINUTES",
		},
	}),
	Action: func(c *cli.Context) error {
		ctx := context.Background()
		ctx, cfg, l, _, done := setupLoggerAndConfig[AgentPauseConfig](ctx, c)
		defer done()

		return pause(ctx, cfg, l)
	},
}
View Source
var AgentResumeCommand = cli.Command{
	Name:        "resume",
	Category:    categoryJobCommands,
	Usage:       "Resume the agent",
	Description: resumeDescription,
	Flags:       slices.Concat(globalFlags(), apiFlags()),
	Action: func(c *cli.Context) error {
		ctx := context.Background()
		ctx, cfg, l, _, done := setupLoggerAndConfig[AgentResumeConfig](ctx, c)
		defer done()

		return resume(ctx, cfg, l)
	},
}
View Source
var AgentStartCommand = cli.Command{
	Name:        "start",
	Usage:       "Starts a Buildkite agent",
	Description: startDescription,
	Flags: append(globalFlags(),
		cli.StringFlag{
			Name:   "config",
			Value:  "",
			Usage:  "Path to a configuration file",
			EnvVar: "BUILDKITE_AGENT_CONFIG",
		},
		cli.StringFlag{
			Name:   "name",
			Value:  "",
			Usage:  "The name of the agent",
			EnvVar: "BUILDKITE_AGENT_NAME",
		},
		cli.StringFlag{
			Name:   "priority",
			Value:  "",
			Usage:  "The priority of the agent (higher priorities are assigned work first)",
			EnvVar: "BUILDKITE_AGENT_PRIORITY",
		},
		cli.StringFlag{
			Name:   "acquire-job",
			Value:  "",
			Usage:  "Start this agent and only run the specified job, disconnecting after it's finished",
			EnvVar: "BUILDKITE_AGENT_ACQUIRE_JOB",
		},
		cli.BoolFlag{
			Name:   "reflect-exit-status",
			Usage:  "When used with --acquire-job, causes the agent to exit with the same exit status as the job (default: false)",
			EnvVar: "BUILDKITE_AGENT_REFLECT_EXIT_STATUS",
		},
		cli.BoolFlag{
			Name:   "disconnect-after-job",
			Usage:  "Disconnect the agent after running exactly one job. When used in conjunction with the ′--spawn′ flag, each worker booted will run exactly one job (default: false)",
			EnvVar: "BUILDKITE_AGENT_DISCONNECT_AFTER_JOB",
		},
		cli.IntFlag{
			Name:   "disconnect-after-idle-timeout",
			Value:  0,
			Usage:  "The maximum idle time in seconds to wait for a job before disconnecting. The default of 0 means no timeout",
			EnvVar: "BUILDKITE_AGENT_DISCONNECT_AFTER_IDLE_TIMEOUT",
		},
		cli.IntFlag{
			Name:   "disconnect-after-uptime",
			Value:  0,
			Usage:  "The maximum uptime in seconds before the agent stops accepting new jobs and shuts down after any running jobs complete. The default of 0 means no timeout",
			EnvVar: "BUILDKITE_AGENT_DISCONNECT_AFTER_UPTIME",
		},
		cancelGracePeriodFlag,
		cli.BoolFlag{
			Name:   "enable-job-log-tmpfile",
			Usage:  "Store the job logs in a temporary file ′BUILDKITE_JOB_LOG_TMPFILE′ that is accessible during the job and removed at the end of the job (default: false)",
			EnvVar: "BUILDKITE_ENABLE_JOB_LOG_TMPFILE",
		},
		cli.StringFlag{
			Name:   "job-log-path",
			Usage:  "Location to store job logs created by configuring ′enable-job-log-tmpfile`, by default job log will be stored in TempDir",
			EnvVar: "BUILDKITE_JOB_LOG_PATH",
		},
		cli.BoolFlag{
			Name:   "write-job-logs-to-stdout",
			Usage:  "Writes job logs to the agent process' stdout. This simplifies log collection if running agents in Docker (default: false)",
			EnvVar: "BUILDKITE_WRITE_JOB_LOGS_TO_STDOUT",
		},
		cli.StringFlag{
			Name:   "shell",
			Value:  DefaultShell(),
			Usage:  "The shell command used to interpret build commands, e.g /bin/bash -e -c",
			EnvVar: "BUILDKITE_SHELL",
		},
		cli.StringFlag{
			Name:   "hooks-shell",
			Usage:  "The shell command used to interpret hooks commands, e.g pwsh -Command",
			EnvVar: "BUILDKITE_HOOKS_SHELL",
		},
		cli.StringFlag{
			Name:   "queue",
			Usage:  "The queue the agent will listen to for jobs. If not set, the agent will use the default queue. Overwrites the queue tag in the agent's tags",
			EnvVar: "BUILDKITE_AGENT_QUEUE",
		},
		cli.StringSliceFlag{
			Name:   "tags",
			Value:  &cli.StringSlice{},
			Usage:  "A comma-separated list of tags for the agent (for example, \"linux\" or \"mac,xcode=8\")",
			EnvVar: "BUILDKITE_AGENT_TAGS",
		},
		cli.BoolFlag{
			Name:   "tags-from-host",
			Usage:  "Include tags from the host (hostname, machine-id, os) (default: false)",
			EnvVar: "BUILDKITE_AGENT_TAGS_FROM_HOST",
		},
		cli.BoolFlag{
			Name:   "tags-from-ec2-meta-data",
			Usage:  "Include the default set of host EC2 meta-data as tags (instance-id, instance-type, ami-id, and instance-life-cycle)",
			EnvVar: "BUILDKITE_AGENT_TAGS_FROM_EC2_META_DATA",
		},
		cli.StringSliceFlag{
			Name:   "tags-from-ec2-meta-data-paths",
			Value:  &cli.StringSlice{},
			Usage:  "Include additional tags fetched from EC2 meta-data using tag & path suffix pairs, e.g \"tag_name=path/to/value\"",
			EnvVar: "BUILDKITE_AGENT_TAGS_FROM_EC2_META_DATA_PATHS",
		},
		cli.BoolFlag{
			Name:   "tags-from-ec2-tags",
			Usage:  "Include the host's EC2 tags as tags (default: false)",
			EnvVar: "BUILDKITE_AGENT_TAGS_FROM_EC2_TAGS",
		},
		cli.BoolFlag{
			Name:   "tags-from-ecs-meta-data",
			Usage:  "Include the host's ECS meta-data as tags (container-name, image, and task-arn) (default: false)",
			EnvVar: "BUILDKITE_AGENT_TAGS_FROM_ECS_META_DATA",
		},
		cli.StringSliceFlag{
			Name:   "tags-from-gcp-meta-data",
			Value:  &cli.StringSlice{},
			Usage:  "Include the default set of host Google Cloud instance meta-data as tags (instance-id, machine-type, preemptible, project-id, region, and zone)",
			EnvVar: "BUILDKITE_AGENT_TAGS_FROM_GCP_META_DATA",
		},
		cli.StringSliceFlag{
			Name:   "tags-from-gcp-meta-data-paths",
			Value:  &cli.StringSlice{},
			Usage:  "Include additional tags fetched from Google Cloud instance meta-data using tag & path suffix pairs, e.g \"tag_name=path/to/value\"",
			EnvVar: "BUILDKITE_AGENT_TAGS_FROM_GCP_META_DATA_PATHS",
		},
		cli.BoolFlag{
			Name:   "tags-from-gcp-labels",
			Usage:  "Include the host's Google Cloud instance labels as tags (default: false)",
			EnvVar: "BUILDKITE_AGENT_TAGS_FROM_GCP_LABELS",
		},
		cli.DurationFlag{
			Name:   "wait-for-ec2-tags-timeout",
			Usage:  "The amount of time to wait for tags from EC2 before proceeding",
			EnvVar: "BUILDKITE_AGENT_WAIT_FOR_EC2_TAGS_TIMEOUT",
			Value:  time.Second * 10,
		},
		cli.DurationFlag{
			Name:   "wait-for-ec2-meta-data-timeout",
			Usage:  "The amount of time to wait for meta-data from EC2 before proceeding",
			EnvVar: "BUILDKITE_AGENT_WAIT_FOR_EC2_META_DATA_TIMEOUT",
			Value:  time.Second * 10,
		},
		cli.DurationFlag{
			Name:   "wait-for-ecs-meta-data-timeout",
			Usage:  "The amount of time to wait for meta-data from ECS before proceeding",
			EnvVar: "BUILDKITE_AGENT_WAIT_FOR_ECS_META_DATA_TIMEOUT",
			Value:  time.Second * 10,
		},
		cli.DurationFlag{
			Name:   "wait-for-gcp-labels-timeout",
			Usage:  "The amount of time to wait for labels from GCP before proceeding",
			EnvVar: "BUILDKITE_AGENT_WAIT_FOR_GCP_LABELS_TIMEOUT",
			Value:  time.Second * 10,
		},
		cli.BoolFlag{
			Name:   "fail-on-missing-tags",
			Usage:  "Exit the agent with an error if any enabled cloud tag source (EC2, ECS, GCP) fails to return tags (default: false)",
			EnvVar: "BUILDKITE_AGENT_FAIL_ON_MISSING_TAGS",
		},

		SkipCheckoutFlag,
		GitCheckoutFlagsFlag,
		GitCloneFlagsFlag,
		GitCleanFlagsFlag,
		GitFetchFlagsFlag,
		GitCloneMirrorFlagsFlag,
		GitMirrorsPathFlag,
		GitMirrorCheckoutModeFlag,
		GitMirrorsLockTimeoutFlag,
		GitMirrorsSkipUpdateFlag,
		GitSubmoduleCloneConfigFlag,
		GitSkipFetchExistingCommitsFlag,
		CheckoutAttemptsFlag,

		cli.StringFlag{
			Name:   "bootstrap-script",
			Value:  "",
			Usage:  "The command that is executed for bootstrapping a job, defaults to the bootstrap sub-command of this binary",
			EnvVar: "BUILDKITE_BOOTSTRAP_SCRIPT_PATH",
		},

		BuildPathFlag,
		HooksPathFlag,
		AdditionalHooksPathsFlag,
		SocketsPathFlag,
		PluginsPathFlag,

		cli.BoolFlag{
			Name:   "no-ansi-timestamps",
			Usage:  "Do not insert ANSI timestamp codes at the start of each line of job output (default: false)",
			EnvVar: "BUILDKITE_NO_ANSI_TIMESTAMPS",
		},
		cli.BoolFlag{
			Name:   "timestamp-lines",
			Usage:  "Prepend timestamps on each line of job output. Has no effect unless --no-ansi-timestamps is also used (default: false)",
			EnvVar: "BUILDKITE_TIMESTAMP_LINES",
		},
		cli.StringFlag{
			Name:   "health-check-addr",
			Usage:  "Start an HTTP server on this addr:port that returns whether the agent is healthy, disabled by default",
			EnvVar: "BUILDKITE_AGENT_HEALTH_CHECK_ADDR",
		},
		cli.BoolFlag{
			Name:   "no-pty",
			Usage:  "Do not run jobs within a pseudo terminal (default: false)",
			EnvVar: "BUILDKITE_NO_PTY",
		},
		cli.BoolFlag{
			Name:   "no-ssh-keyscan",
			Usage:  "Don't automatically run ssh-keyscan before checkout (default: false)",
			EnvVar: "BUILDKITE_NO_SSH_KEYSCAN",
		},
		cli.BoolFlag{
			Name:   "no-command-eval",
			Usage:  "Don't allow this agent to run arbitrary console commands, including plugins (default: false)",
			EnvVar: "BUILDKITE_NO_COMMAND_EVAL",
		},
		cli.BoolFlag{
			Name:   "no-plugins",
			Usage:  "Don't allow this agent to load plugins (default: false)",
			EnvVar: "BUILDKITE_NO_PLUGINS",
		},
		cli.BoolTFlag{
			Name:   "no-plugin-validation",
			Usage:  "Don't validate plugin configuration and requirements (default: true)",
			EnvVar: "BUILDKITE_NO_PLUGIN_VALIDATION",
		},
		cli.BoolFlag{
			Name:   "plugins-always-clone-fresh",
			Usage:  "Always make a new clone of plugin source, even if already present (default: false)",
			EnvVar: "BUILDKITE_PLUGINS_ALWAYS_CLONE_FRESH",
		},
		cli.BoolFlag{
			Name:   "no-local-hooks",
			Usage:  "Don't allow local hooks to be run from checked out repositories (default: false)",
			EnvVar: "BUILDKITE_NO_LOCAL_HOOKS",
		},
		cli.BoolFlag{
			Name:   "no-git-submodules",
			Usage:  "Don't automatically checkout git submodules (default: false)",
			EnvVar: "BUILDKITE_NO_GIT_SUBMODULES,BUILDKITE_DISABLE_GIT_SUBMODULES",
		},
		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 :) (default: false)",
			EnvVar: "BUILDKITE_AGENT_NO_FEATURE_REPORTING",
		},
		cli.StringSliceFlag{
			Name:   "allowed-repositories",
			Value:  &cli.StringSlice{},
			Usage:  `A comma-separated list of regular expressions representing repositories the agent is allowed to clone (for example, "^git@github.com:buildkite/.*" or "^https://github.com/buildkite/.*")`,
			EnvVar: "BUILDKITE_ALLOWED_REPOSITORIES",
		},
		cli.BoolFlag{
			Name:   "enable-environment-variable-allowlist",
			Usage:  "Only run jobs where all environment variables are allowed by the allowed-environment-variables option, or have been set by Buildkite (default: false)",
			EnvVar: "BUILDKITE_ENABLE_ENVIRONMENT_VARIABLE_ALLOWLIST",
		},
		cli.StringSliceFlag{
			Name:   "allowed-environment-variables",
			Value:  &cli.StringSlice{},
			Usage:  `A comma-separated list of regular expressions representing environment variables the agent will pass to jobs (for example, "^MYAPP_.*$"). Environment variables set by Buildkite will always be allowed. Requires --enable-environment-variable-allowlist to be set`,
			EnvVar: "BUILDKITE_ALLOWED_ENVIRONMENT_VARIABLES",
		},
		cli.StringSliceFlag{
			Name:   "allowed-plugins",
			Value:  &cli.StringSlice{},
			Usage:  `A comma-separated list of regular expressions representing plugins the agent is allowed to use (for example, "^buildkite-plugins/.*$" or "^/var/lib/buildkite-plugins/.*")`,
			EnvVar: "BUILDKITE_ALLOWED_PLUGINS",
		},
		cli.BoolFlag{
			Name:   "metrics-datadog",
			Usage:  "Send metrics to DogStatsD for Datadog (default: false)",
			EnvVar: "BUILDKITE_METRICS_DATADOG",
		},
		cli.StringFlag{
			Name:   "metrics-datadog-host",
			Usage:  "The dogstatsd instance to send metrics to using udp",
			EnvVar: "BUILDKITE_METRICS_DATADOG_HOST",
			Value:  "127.0.0.1:8125",
		},
		cli.BoolFlag{
			Name:   "metrics-datadog-distributions",
			Usage:  "Use Datadog Distributions for Timing metrics (default: false)",
			EnvVar: "BUILDKITE_METRICS_DATADOG_DISTRIBUTIONS",
		},
		cli.StringFlag{
			Name:   "log-format",
			Usage:  "The format to use for the logger output",
			EnvVar: "BUILDKITE_LOG_FORMAT",
			Value:  "text",
		},
		cli.IntFlag{
			Name:   "spawn",
			Usage:  "The number of agents to spawn in parallel (mutually exclusive with --spawn-per-cpu)",
			Value:  1,
			EnvVar: "BUILDKITE_AGENT_SPAWN",
		},
		cli.IntFlag{
			Name:   "spawn-per-cpu",
			Usage:  "The number of agents to spawn per cpu in parallel (mutually exclusive with --spawn)",
			Value:  0,
			EnvVar: "BUILDKITE_AGENT_SPAWN_PER_CPU",
		},
		cli.BoolFlag{
			Name:   "spawn-with-priority",
			Usage:  "Assign priorities to every spawned agent (when using --spawn or --spawn-per-cpu) equal to the agent's index (default: false)",
			EnvVar: "BUILDKITE_AGENT_SPAWN_WITH_PRIORITY",
		},
		cancelSignalFlag,
		signalGracePeriodSecondsFlag,
		cli.StringFlag{
			Name:   "tracing-backend",
			Usage:  `Enable tracing for build jobs by specifying a backend, "datadog" or "opentelemetry"`,
			EnvVar: "BUILDKITE_TRACING_BACKEND",
			Value:  "",
		},
		cli.BoolFlag{
			Name:   "tracing-propagate-traceparent",
			Usage:  `Enable accepting traceparent context from Buildkite control plane (only supported for OpenTelemetry backend) (default: false)`,
			EnvVar: "BUILDKITE_TRACING_PROPAGATE_TRACEPARENT",
		},
		cli.StringFlag{
			Name:   "tracing-service-name",
			Usage:  "Service name to use when reporting traces.",
			EnvVar: "BUILDKITE_TRACING_SERVICE_NAME",
			Value:  "buildkite-agent",
		},
		cli.StringFlag{
			Name:   "verification-jwks-file",
			Usage:  "Path to a file containing a JSON Web Key Set (JWKS), used to verify job signatures. ",
			EnvVar: "BUILDKITE_AGENT_VERIFICATION_JWKS_FILE",
		},
		cli.StringFlag{
			Name:   "signing-jwks-file",
			Usage:  `Path to a file containing a signing key. Passing this flag enables pipeline signing for all pipelines uploaded by this agent. For hmac-sha256, the raw file content is used as the shared key. When using Docker containers to upload pipeline steps dynamically, use environment variable propagation (for example, "docker run -e BUILDKITE_AGENT_JWKS_FILE") to allow all steps within the pipeline to be signed.`,
			EnvVar: "BUILDKITE_AGENT_SIGNING_JWKS_FILE",
		},
		cli.StringFlag{
			Name:   "signing-jwks-key-id",
			Usage:  "The JWKS key ID to use when signing the pipeline. If omitted, and the signing JWKS contains only one key, that key will be used.",
			EnvVar: "BUILDKITE_AGENT_SIGNING_JWKS_KEY_ID",
		},
		cli.StringFlag{
			Name:   "signing-aws-kms-key",
			Usage:  "The KMS KMS key ID, or key alias used when signing and verifying the pipeline.",
			EnvVar: "BUILDKITE_AGENT_SIGNING_AWS_KMS_KEY",
		},
		cli.StringFlag{
			Name:   "signing-gcp-kms-key",
			Usage:  "The GCP KMS key resource name used when signing and verifying the pipeline. Format: projects/*/locations/*/keyRings/*/cryptoKeys/*/cryptoKeyVersions/*",
			EnvVar: "BUILDKITE_AGENT_SIGNING_GCP_KMS_KEY",
		},
		cli.BoolFlag{
			Name:   "debug-signing",
			Usage:  "Enable debug logging for pipeline signing. This can potentially leak secrets to the logs as it prints each step in full before signing. Requires debug logging to be enabled (default: false)",
			EnvVar: "BUILDKITE_AGENT_DEBUG_SIGNING",
		},
		cli.StringFlag{
			Name:   "verification-failure-behavior",
			Value:  agent.VerificationBehaviourBlock,
			Usage:  fmt.Sprintf("The behavior when a job is received without a valid verifiable signature (without a signature, with an invalid signature, or with a signature that fails verification). One of: %v. Defaults to %s", verificationFailureBehaviors, agent.VerificationBehaviourBlock),
			EnvVar: "BUILDKITE_AGENT_JOB_VERIFICATION_NO_SIGNATURE_BEHAVIOR",
		},
		cli.StringSliceFlag{
			Name:   "disable-warnings-for",
			Usage:  "A list of warning IDs to disable",
			EnvVar: "BUILDKITE_AGENT_DISABLE_WARNINGS_FOR",
		},

		cli.StringFlag{
			Name:   "ping-mode",
			Usage:  "Selects available protocols for dispatching work to this agent. One of auto (default, prefer streaming, but fall back to polling when necessary), poll-only, or stream-only.",
			Value:  "auto",
			EnvVar: "BUILDKITE_AGENT_PING_MODE",
		},

		AgentRegisterTokenFlag,
		EndpointFlag,
		NoHTTP2Flag,
		DebugHTTPFlag,
		TraceHTTPFlag,

		cli.BoolFlag{
			Name: "kubernetes-exec",
			Usage: "This is intended to be used only by the Buildkite k8s stack " +
				"(github.com/buildkite/agent-stack-k8s); it enables a Unix socket for transporting " +
				"logs and exit statuses between containers in a pod (default: false)",
			EnvVar: "BUILDKITE_KUBERNETES_EXEC",
		},
		cli.DurationFlag{
			Name:   "kubernetes-container-start-timeout",
			Usage:  "Timeout for waiting for all containers to start in a Kubernetes pod (default: 5m)",
			EnvVar: "BUILDKITE_KUBERNETES_CONTAINER_START_TIMEOUT",
			Value:  5 * time.Minute,
		},

		RedactedVars,
		StrictSingleHooksFlag,
		TraceContextEncodingFlag,
		NoMultipartArtifactUploadFlag,

		KubernetesLogCollectionGracePeriodFlag,
		cli.StringSliceFlag{
			Name:   "meta-data",
			Value:  &cli.StringSlice{},
			Hidden: true,
			EnvVar: "BUILDKITE_AGENT_META_DATA",
		},
		cli.BoolFlag{
			Name:   "meta-data-ec2",
			Hidden: true,
			EnvVar: "BUILDKITE_AGENT_META_DATA_EC2",
		},
		cli.BoolFlag{
			Name:   "meta-data-ec2-tags",
			Hidden: true,
			EnvVar: "BUILDKITE_AGENT_META_DATA_EC2_TAGS",
		},
		cli.BoolFlag{
			Name:   "meta-data-gcp",
			Hidden: true,
			EnvVar: "BUILDKITE_AGENT_META_DATA_GCP",
		},
		cli.BoolFlag{
			Name:   "no-automatic-ssh-fingerprint-verification",
			Hidden: true,
			EnvVar: "BUILDKITE_NO_AUTOMATIC_SSH_FINGERPRINT_VERIFICATION",
		},
		cli.BoolFlag{
			Name:   "tags-from-ec2",
			Usage:  "Include the host's EC2 meta-data as tags (instance-id, instance-type, and ami-id)",
			EnvVar: "BUILDKITE_AGENT_TAGS_FROM_EC2",
		},
		cli.BoolFlag{
			Name:   "tags-from-gcp",
			Usage:  "Include the host's Google Cloud instance meta-data as tags (instance-id, machine-type, preemptible, project-id, region, and zone)",
			EnvVar: "BUILDKITE_AGENT_TAGS_FROM_GCP",
		},
		cli.IntFlag{
			Name:   "disconnect-after-job-timeout",
			Hidden: true,
			Usage:  "When --disconnect-after-job is specified, the number of seconds to wait for a job before shutting down",
			EnvVar: "BUILDKITE_AGENT_DISCONNECT_AFTER_JOB_TIMEOUT",
		},
	),
	Action: func(c *cli.Context) error {
		ctx := context.Background()
		ctx, cfg, l, configFile, done := setupLoggerAndConfig[AgentStartConfig](ctx, c, withConfigFilePaths(
			defaultConfigFilePaths(),
		))
		defer done()

		ctx, cancel := context.WithCancel(ctx)
		defer cancel()

		if err := UnsetConfigFromEnvironment(c); err != nil {
			return fmt.Errorf("failed to unset config from environment: %w", err)
		}

		if !slices.Contains(mirrorCheckoutModes, cfg.GitMirrorCheckoutMode) {
			return fmt.Errorf("invalid git mirror checkout mode %q, must be one of %v", cfg.GitMirrorCheckoutMode, mirrorCheckoutModes)
		}

		if !slices.Contains(pingModes, cfg.PingMode) {
			return fmt.Errorf("invalid ping mode %q, must be one of %v", cfg.PingMode, pingModes)
		}

		if cfg.PingMode == pingModePingOnly {
			cfg.PingMode = agent.PingModePollOnly
		}

		if cfg.VerificationJWKSFile != "" {
			if !slices.Contains(verificationFailureBehaviors, cfg.VerificationFailureBehavior) {
				return fmt.Errorf(
					"invalid job verification no signature behavior %q. Must be one of: %v",
					cfg.VerificationFailureBehavior,
					verificationFailureBehaviors,
				)
			}
		}

		if runtime.GOOS == "windows" {
			cfg.NoPTY = true
		}

		if cfg.BootstrapScript == "" {
			exePath, err := os.Executable()
			if err != nil {
				return errors.New("unable to find executable path for bootstrap")
			}
			cfg.BootstrapScript = fmt.Sprintf("%s bootstrap", shellwords.Quote(exePath))
		}

		isSetNoPlugins := c.IsSet("no-plugins")
		if configFile != nil {
			if _, exists := configFile.Config["no-plugins"]; exists {
				isSetNoPlugins = true
			}
		}

		if isSetNoPlugins && !cfg.NoPlugins {
			msg := "Plugins have been specifically enabled, despite %s being enabled. " +
				"Plugins can execute arbitrary hooks and commands, make sure you are " +
				"whitelisting your plugins in " +
				"your environment hook."

			switch {
			case cfg.NoCommandEval:
				l.Warnf(msg, "no-command-eval")
			case cfg.NoLocalHooks:
				l.Warnf(msg, "no-local-hooks")
			}
		}

		if (cfg.NoCommandEval || cfg.NoLocalHooks) && !isSetNoPlugins {
			cfg.NoPlugins = true
		}

		if cfg.Shell == "" {
			cfg.Shell = DefaultShell()
		}

		if cfg.DisconnectAfterJobTimeout > 0 {
			cfg.DisconnectAfterIdleTimeout = cfg.DisconnectAfterJobTimeout
		}

		var ec2TagTimeout time.Duration
		if t := cfg.WaitForEC2TagsTimeout; t != "" {
			var err error
			ec2TagTimeout, err = time.ParseDuration(t)
			if err != nil {
				return fmt.Errorf("failed to parse ec2 tag timeout: %w", err)
			}
		}

		var ec2MetaDataTimeout time.Duration
		if t := cfg.WaitForEC2MetaDataTimeout; t != "" {
			var err error
			ec2MetaDataTimeout, err = time.ParseDuration(t)
			if err != nil {
				return fmt.Errorf("failed to parse ec2 meta-data timeout: %w", err)
			}
		}

		var ecsMetaDataTimeout time.Duration
		if t := cfg.WaitForECSMetaDataTimeout; t != "" {
			var err error
			ecsMetaDataTimeout, err = time.ParseDuration(t)
			if err != nil {
				return fmt.Errorf("failed to parse ecs meta-data timeout: %w", err)
			}
		}

		var gcpLabelsTimeout time.Duration
		if t := cfg.WaitForGCPLabelsTimeout; t != "" {
			var err error
			gcpLabelsTimeout, err = time.ParseDuration(t)
			if err != nil {
				return fmt.Errorf("failed to parse gcp labels timeout: %w", err)
			}
		}

		signalGracePeriod, err := signalGracePeriod(cfg.CancelGracePeriod, cfg.SignalGracePeriodSeconds)
		if err != nil {
			return err
		}

		if _, err := tracetools.ParseEncoding(cfg.TraceContextEncoding); err != nil {
			return fmt.Errorf("while parsing trace context encoding: %v", err)
		}

		mc := metrics.NewCollector(l, metrics.CollectorConfig{
			Datadog:              cfg.MetricsDatadog,
			DatadogHost:          cfg.MetricsDatadogHost,
			DatadogDistributions: cfg.MetricsDatadogDistributions,
		})

		if _, has := tracetools.ValidTracingBackends[cfg.TracingBackend]; !has {
			return fmt.Errorf(
				"the given tracing backend %q is not supported. Valid backends are: %q",
				cfg.TracingBackend,
				slices.Collect(maps.Keys(tracetools.ValidTracingBackends)),
			)
		}

		if experiments.IsEnabled(ctx, experiments.AgentAPI) {
			shutdown, err := runAgentAPI(ctx, l, cfg.SocketsPath)
			if err != nil {
				return err
			}
			defer shutdown()
		}

		// if the agent is provided a KMS key ID, it should use the KMS signer, otherwise
		// it should load the JWKS from the file
		var verificationJWKS any
		switch {
		case cfg.SigningAWSKMSKey != "":

			var logMode aws.ClientLogMode

			if cfg.DebugSigning {
				logMode = aws.LogRetries | aws.LogRequest
			}

			awscfg, err := awslib.GetConfigV2(
				ctx,
				config.WithClientLogMode(logMode),
			)
			if err != nil {
				return err
			}

			verificationJWKS, err = awssigner.NewKMS(kms.NewFromConfig(awscfg), cfg.SigningAWSKMSKey)
			if err != nil {
				return fmt.Errorf("couldn't create AWS KMS signer: %w", err)
			}

		case cfg.SigningGCPKMSKey != "":

			verificationJWKS, err = gcpsigner.NewKMS(ctx, cfg.SigningGCPKMSKey)
			if err != nil {
				return fmt.Errorf("couldn't create GCP KMS signer: %w", err)
			}

		case cfg.VerificationJWKSFile != "":
			var err error
			verificationJWKS, err = parseAndValidateJWKS(ctx, "verification", cfg.VerificationJWKSFile)
			if err != nil {
				l.Fatalf("Verification JWKS failed validation: %v", err)
			}
		}

		if cfg.SigningJWKSFile != "" {

			_, err := parseAndValidateJWKS(ctx, "signing", cfg.SigningJWKSFile)
			if err != nil {
				l.Fatalf("Signing JWKS failed validation: %v", err)
			}
		}

		if len(cfg.AllowedEnvironmentVariables) > 0 && !cfg.EnableEnvironmentVariableAllowList {
			l.Fatalf("allowed-environment-variables is set, but enable-environment-variable-allowlist is not set")
		}

		var allowedEnvironmentVariables []*regexp.Regexp
		if cfg.EnableEnvironmentVariableAllowList {
			allowedEnvironmentVariables = append(allowedEnvironmentVariables, buildkiteSetEnvironmentVariables...)

			for _, v := range cfg.AllowedEnvironmentVariables {
				re, err := regexp.Compile(v)
				if err != nil {
					l.Fatalf("Regex %s in allowed-environment-variables failed to compile: %v", v, err)
				}

				allowedEnvironmentVariables = append(allowedEnvironmentVariables, re)
			}
		}

		agentConf := agent.AgentConfiguration{
			BootstrapScript:                 cfg.BootstrapScript,
			BuildPath:                       cfg.BuildPath,
			SocketsPath:                     cfg.SocketsPath,
			GitMirrorsPath:                  cfg.GitMirrorsPath,
			GitMirrorCheckoutMode:           cfg.GitMirrorCheckoutMode,
			GitMirrorsLockTimeout:           cfg.GitMirrorsLockTimeout,
			GitMirrorsSkipUpdate:            cfg.GitMirrorsSkipUpdate,
			HooksPath:                       cfg.HooksPath,
			AdditionalHooksPaths:            cfg.AdditionalHooksPaths,
			PluginsPath:                     cfg.PluginsPath,
			GitCheckoutFlags:                cfg.GitCheckoutFlags,
			GitCloneFlags:                   cfg.GitCloneFlags,
			GitCloneMirrorFlags:             cfg.GitCloneMirrorFlags,
			GitCleanFlags:                   cfg.GitCleanFlags,
			GitFetchFlags:                   cfg.GitFetchFlags,
			GitSubmodules:                   !cfg.NoGitSubmodules,
			GitSubmoduleCloneConfig:         cfg.GitSubmoduleCloneConfig,
			SkipCheckout:                    cfg.SkipCheckout,
			GitSkipFetchExistingCommits:     cfg.GitSkipFetchExistingCommits,
			CheckoutAttempts:                cfg.CheckoutAttempts,
			SSHKeyscan:                      !cfg.NoSSHKeyscan,
			CommandEval:                     !cfg.NoCommandEval,
			PluginsEnabled:                  !cfg.NoPlugins,
			PluginValidation:                !cfg.NoPluginValidation,
			PluginsAlwaysCloneFresh:         cfg.PluginsAlwaysCloneFresh,
			LocalHooksEnabled:               !cfg.NoLocalHooks,
			AllowedEnvironmentVariables:     allowedEnvironmentVariables,
			StrictSingleHooks:               cfg.StrictSingleHooks,
			RunInPty:                        !cfg.NoPTY,
			ANSITimestamps:                  !cfg.NoANSITimestamps,
			TimestampLines:                  cfg.TimestampLines,
			DisconnectAfterJob:              cfg.DisconnectAfterJob,
			DisconnectAfterIdleTimeout:      time.Duration(cfg.DisconnectAfterIdleTimeout) * time.Second,
			DisconnectAfterUptime:           time.Duration(cfg.DisconnectAfterUptime) * time.Second,
			CancelGracePeriod:               cfg.CancelGracePeriod,
			SignalGracePeriod:               signalGracePeriod,
			EnableJobLogTmpfile:             cfg.EnableJobLogTmpfile,
			JobLogPath:                      cfg.JobLogPath,
			WriteJobLogsToStdout:            cfg.WriteJobLogsToStdout,
			LogFormat:                       cfg.LogFormat,
			Shell:                           cfg.Shell,
			HooksShell:                      cfg.HooksShell,
			RedactedVars:                    cfg.RedactedVars,
			AcquireJob:                      cfg.AcquireJob,
			TracingBackend:                  cfg.TracingBackend,
			TracingServiceName:              cfg.TracingServiceName,
			TracingPropagateTraceparent:     cfg.TracingPropagateTraceparent,
			TraceContextEncoding:            cfg.TraceContextEncoding,
			AllowMultipartArtifactUpload:    !cfg.NoMultipartArtifactUpload,
			KubernetesExec:                  cfg.KubernetesExec,
			KubernetesContainerStartTimeout: cfg.KubernetesContainerStartTimeout,
			PingMode:                        cfg.PingMode,

			SigningJWKSFile:  cfg.SigningJWKSFile,
			SigningJWKSKeyID: cfg.SigningJWKSKeyID,
			SigningAWSKMSKey: cfg.SigningAWSKMSKey,
			SigningGCPKMSKey: cfg.SigningGCPKMSKey,
			DebugSigning:     cfg.DebugSigning,

			VerificationJWKS:             verificationJWKS,
			VerificationFailureBehaviour: cfg.VerificationFailureBehavior,

			DisableWarningsFor: cfg.DisableWarningsFor,
		}

		if configFile != nil {
			agentConf.ConfigPath = configFile.Path
		}

		if cfg.LogFormat == "text" {
			welcomeMessage := "\n" +
				"%s   _           _ _     _ _    _ _                                _\n" +
				"  | |         (_) |   | | |  (_) |                              | |\n" +
				"  | |__  _   _ _| | __| | | ___| |_ ___    __ _  __ _  ___ _ __ | |_\n" +
				"  | '_ \\| | | | | |/ _` | |/ / | __/ _ \\  / _` |/ _` |/ _ \\ '_ \\| __|\n" +
				"  | |_) | |_| | | | (_| |   <| | ||  __/ | (_| | (_| |  __/ | | | |_\n" +
				"  |_.__/ \\__,_|_|_|\\__,_|_|\\_\\_|\\__\\___|  \\__,_|\\__, |\\___|_| |_|\\__|\n" +
				"                                                 __/ |\n" +
				" https://buildkite.com/agent                    |___/\n%s\n"

			if !cfg.NoColor {
				fmt.Fprintf(os.Stderr, welcomeMessage, "\x1b[38;5;48m", "\x1b[0m")
			} else {
				fmt.Fprintf(os.Stderr, welcomeMessage, "", "")
			}
		} else if cfg.LogFormat != "json" {

			return fmt.Errorf("invalid log format %q; only 'text' or 'json' are allowed", cfg.LogFormat)
		}

		l.Noticef("Starting buildkite-agent v%s with PID: %s", version.Version(), strconv.Itoa(os.Getpid()))
		l.Noticef("The agent source code can be found here: https://github.com/buildkite/agent")
		l.Noticef("For questions and support, email us at: hello@buildkite.com")

		if agentConf.ConfigPath != "" {
			l.WithFields(logger.StringField(`path`, agentConf.ConfigPath)).Infof("Configuration loaded")
		}

		l.Debugf("Bootstrap command: %s", agentConf.BootstrapScript)
		l.Debugf("Build path: %s", agentConf.BuildPath)
		l.Debugf("Hooks directory: %s", agentConf.HooksPath)
		l.Debugf("Additional hooks directories: %v", agentConf.AdditionalHooksPaths)
		l.Debugf("Plugins directory: %s", agentConf.PluginsPath)

		if exps := experiments.KnownAndEnabled(ctx); len(exps) > 0 {
			l.WithFields(logger.StringField("experiments", fmt.Sprintf("%v", exps))).Infof("Experiments are enabled")
		}

		if !agentConf.SSHKeyscan {
			l.Infof("Automatic ssh-keyscan has been disabled")
		}

		if !agentConf.CommandEval {
			l.Infof("Evaluating console commands has been disabled")
		}

		if !agentConf.PluginsEnabled {
			l.Infof("Plugins have been disabled")
		}

		if !agentConf.RunInPty {
			l.Infof("Running builds within a pseudoterminal (PTY) has been disabled")
		}

		if agentConf.DisconnectAfterJob {
			l.Infof("Agents will disconnect after a job run has completed")
		}

		if agentConf.DisconnectAfterIdleTimeout > 0 {
			l.Infof("Agents will disconnect after %v of inactivity", agentConf.DisconnectAfterIdleTimeout)
		}

		if agentConf.DisconnectAfterUptime > 0 {
			l.Infof("Agents will disconnect after %v of uptime and shut down after any running jobs complete", agentConf.DisconnectAfterUptime)
		}

		if len(cfg.AllowedRepositories) > 0 {
			agentConf.AllowedRepositories = make([]*regexp.Regexp, 0, len(cfg.AllowedRepositories))
			for _, v := range cfg.AllowedRepositories {
				r, err := regexp.Compile(v)
				if err != nil {
					l.Fatalf("Regex %s is allowed-repositories failed to compile: %v", v, err)
				}
				agentConf.AllowedRepositories = append(agentConf.AllowedRepositories, r)
			}
			l.Infof("Allowed repositories patterns: %q", agentConf.AllowedRepositories)
		}

		if len(cfg.AllowedPlugins) > 0 {
			agentConf.AllowedPlugins = make([]*regexp.Regexp, 0, len(cfg.AllowedPlugins))
			for _, v := range cfg.AllowedPlugins {
				r, err := regexp.Compile(v)
				if err != nil {
					l.Fatalf("Regex %s in allowed-plugins failed to compile: %v", v, err)
				}
				agentConf.AllowedPlugins = append(agentConf.AllowedPlugins, r)
			}
			l.Infof("Allowed plugins patterns: %q", agentConf.AllowedPlugins)
		}

		cancelSig, err := process.ParseSignal(cfg.CancelSignal)
		if err != nil {
			return fmt.Errorf("failed to parse cancel-signal: %w", err)
		}

		tags, err := agent.FetchTags(ctx, l, agent.FetchTagsConfig{
			Tags:                      cfg.Tags,
			TagsFromK8s:               cfg.KubernetesExec,
			TagsFromEC2MetaData:       (cfg.TagsFromEC2MetaData || cfg.TagsFromEC2),
			TagsFromEC2MetaDataPaths:  cfg.TagsFromEC2MetaDataPaths,
			TagsFromEC2Tags:           cfg.TagsFromEC2Tags,
			TagsFromECSMetaData:       cfg.TagsFromECSMetaData,
			TagsFromGCPMetaData:       (cfg.TagsFromGCPMetaData || cfg.TagsFromGCP),
			TagsFromGCPMetaDataPaths:  cfg.TagsFromGCPMetaDataPaths,
			TagsFromGCPLabels:         cfg.TagsFromGCPLabels,
			TagsFromHost:              cfg.TagsFromHost,
			FailOnMissingTags:         cfg.FailOnMissingTags,
			WaitForEC2TagsTimeout:     ec2TagTimeout,
			WaitForEC2MetaDataTimeout: ec2MetaDataTimeout,
			WaitForECSMetaDataTimeout: ecsMetaDataTimeout,
			WaitForGCPLabelsTimeout:   gcpLabelsTimeout,
		})
		if err != nil {
			l.Fatalf("%v", err)
		}

		if cfg.Queue != "" {
			i := slices.IndexFunc(tags, func(s string) bool {
				return strings.HasPrefix(strings.TrimSpace(s), "queue=")
			})
			if i != -1 {
				l.Fatalf("Queue must be present in only one of the --tags or the --queue flags")
			}
			tags = append(tags, "queue="+cfg.Queue)
		}

		if !osutil.FileExists(agentConf.BuildPath) {
			l.Infof("Build Path doesn't exist, creating it (%s)", agentConf.BuildPath)

			if err := os.MkdirAll(agentConf.BuildPath, 0o777); err != nil {
				return fmt.Errorf("failed to create builds path: %w", err)
			}
		}

		apiClient := api.NewClient(l, loadAPIClientConfig(cfg, "Token"))
		client := &core.Client{APIClient: apiClient, Logger: l}

		registerReq := api.AgentRegisterRequest{
			Name:              cfg.Name,
			Priority:          cfg.Priority,
			ScriptEvalEnabled: !cfg.NoCommandEval,
			Tags:              tags,

			IgnoreInDispatches: cfg.AcquireJob != "",
			Features:           cfg.Features(ctx),
		}

		if cfg.SpawnPerCPU > 0 {
			if cfg.Spawn > 1 {
				return errors.New("you can't specify spawn and spawn-per-cpu at the same time")
			}
			cfg.Spawn = runtime.NumCPU() * cfg.SpawnPerCPU
		}

		if cfg.Spawn > 1 && cfg.AcquireJob != "" {
			return errors.New("you can't spawn multiple agents and acquire a job at the same time")
		}

		var workers []*agent.AgentWorker

		for i := 1; i <= cfg.Spawn; i++ {
			if cfg.Spawn == 1 {
				l.Infof("Registering agent with Buildkite...")
			} else {
				l.Infof("Registering agent %d of %d with Buildkite...", i, cfg.Spawn)
			}

			registerReq.Name = strings.ReplaceAll(cfg.Name, "%spawn", strconv.Itoa(i))

			if cfg.SpawnWithPriority {
				p := i
				if experiments.IsEnabled(ctx, experiments.DescendingSpawnPriority) {

					p = -i
				}
				l.Infof("Assigning priority %d for agent %d", p, i)
				registerReq.Priority = strconv.Itoa(p)
			}

			reg, err := client.Register(ctx, registerReq)
			if err != nil {
				return err
			}

			workers = append(workers, agent.NewAgentWorker(
				l.WithFields(logger.StringField("agent", reg.Name)),
				reg,
				mc,
				apiClient,
				agent.AgentWorkerConfig{
					AgentConfiguration: agentConf,
					CancelSignal:       cancelSig,
					SignalGracePeriod:  signalGracePeriod,
					Debug:              cfg.Debug,
					DebugHTTP:          cfg.DebugHTTP,
					SpawnIndex:         i,
					AgentStdout:        os.Stdout,
				},
			))
		}

		pool := agent.NewAgentPool(workers, &agentConf)

		defer agentShutdownHook(l, cfg)

		if err := agentStartupHook(l, cfg); err != nil {
			return fmt.Errorf("failed to run startup hook: %w", err)
		}

		poolSigs := &poolSignals{
			log:               l,
			pool:              pool,
			cancelGracePeriod: time.Duration(cfg.CancelGracePeriod) * time.Second,

			skipGraceful: cfg.KubernetesExec,
		}
		signals := poolSigs.handle(ctx)
		defer close(signals)

		l.Infof("Starting %d Agent(s)", cfg.Spawn)
		l.Infof("You can press Ctrl-C to stop the agents")

		if cfg.HealthCheckAddr != "" {
			pool.StartStatusServer(ctx, l, cfg.HealthCheckAddr)
		}

		var exit core.ProcessExit
		switch err := pool.Start(ctx); {
		case errors.Is(err, core.ErrJobAcquisitionRejected):

			const acquisitionFailedExitCode = 27 // chosen by fair dice roll
			return cli.NewExitError(err, acquisitionFailedExitCode)

		case errors.Is(err, core.ErrJobLocked):

			const jobLockedExitCode = 28
			return cli.NewExitError(err, jobLockedExitCode)

		case errors.As(err, &exit):
			if cfg.ReflectExitStatus {

				return cli.NewExitError(err, exit.Status)
			}

			return nil

		default:
			return err

		}
	},
}
View Source
var AgentStopCommand = cli.Command{
	Name:        "stop",
	Category:    categoryJobCommands,
	Usage:       "Stop the agent",
	Description: stopDescription,
	Flags: slices.Concat(globalFlags(), apiFlags(), []cli.Flag{
		cli.BoolFlag{
			Name:  "force",
			Usage: "Cancel any currently running job (default: false)",
		},
	}),
	Action: func(c *cli.Context) error {
		ctx := context.Background()
		ctx, cfg, l, _, done := setupLoggerAndConfig[AgentStopConfig](ctx, c)
		defer done()

		return stop(ctx, cfg, l)
	},
}
View Source
var AnnotateCommand = cli.Command{
	Name:        "annotate",
	Category:    categoryJobCommands,
	Usage:       "Annotate the build page in the Buildkite UI with information from within a Buildkite job",
	Description: annotateHelpDescription,
	Flags: slices.Concat(globalFlags(), apiFlags(), []cli.Flag{
		cli.StringFlag{
			Name:   "context",
			Usage:  "The context of the annotation used to differentiate this annotation from others. This value has a limit of 100 characters.",
			EnvVar: "BUILDKITE_ANNOTATION_CONTEXT",
		},
		cli.StringFlag{
			Name:   "style",
			Usage:  "The style of the annotation (′success′, ′info′, ′warning′ or ′error′)",
			EnvVar: "BUILDKITE_ANNOTATION_STYLE",
		},
		cli.BoolFlag{
			Name:   "append",
			Usage:  "Append to the body of an existing annotation (default: false)",
			EnvVar: "BUILDKITE_ANNOTATION_APPEND",
		},
		cli.IntFlag{
			Name:   "priority",
			Usage:  "The priority of the annotation (′1′ to ′10′). Annotations with a priority of ′10′ are shown first, while annotations with a priority of ′1′ are shown last.",
			EnvVar: "BUILDKITE_ANNOTATION_PRIORITY",
			Value:  3,
		},
		cli.StringFlag{
			Name:   "job",
			Value:  "",
			Usage:  "Which job should the annotation come from",
			EnvVar: "BUILDKITE_JOB_ID",
		},
		cli.StringFlag{
			Name:   "scope",
			Value:  "build",
			Usage:  "The scope of the annotation, which will control where the annotation is displayed in the Buildkite UI. One of 'build', 'job'",
			EnvVar: "BUILDKITE_ANNOTATION_SCOPE",
		},

		RedactedVars,
	}),
	Action: func(c *cli.Context) error {
		ctx := context.Background()
		ctx, cfg, l, _, done := setupLoggerAndConfig[AnnotateConfig](ctx, c)
		defer done()
		ctx, span := otel.Tracer("buildkite-agent").Start(ctx, "annotate")
		defer span.End()

		if err := annotate(ctx, cfg, l); err != nil {
			return err
		}

		return nil
	},
}
View Source
var AnnotationRemoveCommand = cli.Command{
	Name:        "remove",
	Usage:       "Remove an existing annotation from a Buildkite build",
	Description: annotationRemoveHelpDescription,
	Flags: slices.Concat(globalFlags(), apiFlags(), []cli.Flag{
		cli.StringFlag{
			Name:   "context",
			Value:  "default",
			Usage:  "The context of the annotation used to differentiate this annotation from others",
			EnvVar: "BUILDKITE_ANNOTATION_CONTEXT",
		},
		cli.StringFlag{
			Name:   "scope",
			Value:  "build",
			Usage:  "The scope of the annotation to remove. One of either 'build' or 'job'",
			EnvVar: "BUILDKITE_ANNOTATION_SCOPE",
		},
		cli.StringFlag{
			Name:   "job",
			Value:  "",
			Usage:  "Which job is removing the annotation",
			EnvVar: "BUILDKITE_JOB_ID",
		},
	}),
	Action: func(c *cli.Context) error {
		ctx := context.Background()
		ctx, cfg, l, _, done := setupLoggerAndConfig[AnnotationRemoveConfig](ctx, c)
		defer done()
		ctx, span := otel.Tracer("buildkite-agent").Start(ctx, "annotation-remove")
		defer span.End()

		client := api.NewClient(l, loadAPIClientConfig(cfg, "AgentAccessToken"))

		if err := roko.NewRetrier(
			roko.WithMaxAttempts(5),
			roko.WithStrategy(roko.Constant(1*time.Second)),
			roko.WithJitter(),
		).DoWithContext(ctx, func(r *roko.Retrier) error {

			resp, err := client.AnnotationRemove(ctx, cfg.Job, cfg.Context, cfg.Scope)

			if api.BreakOnNonRetryable(r, resp, err) {
				return err
			}
			if err != nil {
				l.Warnf("%s (%s)", err, r)
				return err
			}
			return nil
		}); err != nil {
			return fmt.Errorf("failed to remove annotation: %w", err)
		}

		l.Debugf("Successfully removed annotation")

		return nil
	},
}
View Source
var ArtifactDownloadCommand = cli.Command{
	Name:        "download",
	Usage:       "Downloads artifacts from Buildkite to the local machine",
	Description: downloadHelpDescription,
	Flags: slices.Concat(globalFlags(), apiFlags(), []cli.Flag{
		cli.StringFlag{
			Name:  "step",
			Value: "",
			Usage: "Scope the search to a particular step. Can be the step's key or label, or a Job ID",
		},
		cli.StringFlag{
			Name:   "build",
			Value:  "",
			EnvVar: "BUILDKITE_BUILD_ID",
			Usage:  "The build that the artifacts were uploaded to",
		},
		cli.BoolFlag{
			Name:   "include-retried-jobs",
			EnvVar: "BUILDKITE_AGENT_INCLUDE_RETRIED_JOBS",
			Usage:  "Include artifacts from retried jobs in the search (default: false)",
		},
		cli.BoolFlag{
			Name:   "no-s3-multipart-download",
			EnvVar: "BUILDKITE_NO_S3_MULTIPART_DOWNLOAD",
			Usage:  "Disable multipart download for custom s3 bucket",
		},
	}),
	Action: func(c *cli.Context) error {
		ctx := context.Background()
		ctx, cfg, l, _, done := setupLoggerAndConfig[ArtifactDownloadConfig](ctx, c)
		defer done()
		ctx, span := otel.Tracer("buildkite-agent").Start(ctx, "artifact-download")
		defer span.End()

		client := api.NewClient(l, loadAPIClientConfig(cfg, "AgentAccessToken"))

		downloader := artifact.NewDownloader(l, client, artifact.DownloaderConfig{
			Query:              cfg.Query,
			Destination:        cfg.Destination,
			BuildID:            cfg.Build,
			Step:               cfg.Step,
			IncludeRetriedJobs: cfg.IncludeRetriedJobs,
			DebugHTTP:          cfg.DebugHTTP,
			TraceHTTP:          cfg.TraceHTTP,
			DisableHTTP2:       cfg.NoHTTP2,
			AllowS3Multipart:   !cfg.NoS3MultipartDownload,
		})

		if err := downloader.Download(ctx); err != nil {
			return fmt.Errorf("failed to download artifacts: %w", err)
		}

		return nil
	},
}
View Source
var ArtifactSearchCommand = cli.Command{
	Name:               "search",
	Usage:              "Searches artifacts in Buildkite",
	Description:        searchHelpDescription,
	CustomHelpTemplate: artifactSearchHelpTemplate,
	Flags: slices.Concat(globalFlags(), apiFlags(), []cli.Flag{
		cli.StringFlag{
			Name:  "step",
			Value: "",
			Usage: "Scope the search to a particular step by using either its name or job ID",
		},
		cli.StringFlag{
			Name:   "build",
			Value:  "",
			EnvVar: "BUILDKITE_BUILD_ID",
			Usage:  "The build that the artifacts were uploaded to",
		},
		cli.BoolFlag{
			Name:   "include-retried-jobs",
			EnvVar: "BUILDKITE_AGENT_INCLUDE_RETRIED_JOBS",
			Usage:  "Include artifacts from retried jobs in the search (default: false)",
		},
		cli.BoolFlag{
			Name:  "allow-empty-results",
			Usage: "By default, searches exit 1 if there are no results. If this flag is set, searches will exit 0 with an empty set (default: false)",
		},
		cli.StringFlag{
			Name:  "format",
			Value: "%j %p %c\\n",
			Usage: "Output formatting of results. See below for listing of available format specifiers.",
		},
	}),
	Action: func(c *cli.Context) error {
		ctx := context.Background()
		ctx, cfg, l, _, done := setupLoggerAndConfig[ArtifactSearchConfig](ctx, c)
		defer done()
		ctx, span := otel.Tracer("buildkite-agent").Start(ctx, "artifact-search")
		defer span.End()

		printFormat := cfg.PrintFormat
		if strings.Contains(printFormat, `"`) {

			printFormat = strings.ReplaceAll(printFormat, `"`, `\"`)
		}

		unquoted, err := strconv.Unquote(`"` + printFormat + `"`)
		if err != nil {
			return fmt.Errorf("unable to parse format %q", printFormat)
		}
		printFormat = unquoted

		client := api.NewClient(l, loadAPIClientConfig(cfg, "AgentAccessToken"))

		searcher := artifact.NewSearcher(l, client, cfg.Build)
		artifacts, err := searcher.Search(ctx, cfg.Query, cfg.Step, cfg.IncludeRetriedJobs, true)
		if err != nil {
			return err
		}

		if len(artifacts) == 0 {
			if !cfg.AllowEmptyResults {
				return fmt.Errorf("no matches found for %q", cfg.Query)
			}
			l.Infof("No matches found for %q", cfg.Query)
		}

		for _, artifact := range artifacts {
			r := strings.NewReplacer(
				"%p", artifact.Path,
				"%c", artifact.CreatedAt.Format(time.RFC3339),
				"%j", artifact.JobID,
				"%s", strconv.FormatInt(artifact.FileSize, 10),
				"%S", artifact.Sha1Sum,
				"%T", artifact.Sha256Sum,
				"%u", artifact.URL,
				"%i", artifact.ID,
			)
			if _, err := fmt.Fprint(c.App.Writer, r.Replace(printFormat)); err != nil {
				return err
			}
		}

		return nil
	},
}
View Source
var ArtifactShasumCommand = cli.Command{
	Name:        "shasum",
	Usage:       "Prints the SHA-1 hash for a single artifact specified by a search query",
	Description: shasumHelpDescription,
	Flags: slices.Concat(globalFlags(), apiFlags(), []cli.Flag{
		cli.BoolFlag{
			Name:  "sha256",
			Usage: "Request SHA-256 instead of SHA-1, errors if SHA-256 not available (default: false)",
		},
		cli.StringFlag{
			Name:  "step",
			Value: "",
			Usage: "Scope the search to a particular step by its name or job ID",
		},
		cli.StringFlag{
			Name:   "build",
			Value:  "",
			EnvVar: "BUILDKITE_BUILD_ID",
			Usage:  "The build that the artifact was uploaded to",
		},
		cli.BoolFlag{
			Name:   "include-retried-jobs",
			EnvVar: "BUILDKITE_AGENT_INCLUDE_RETRIED_JOBS",
			Usage:  "Include artifacts from retried jobs in the search (default: false)",
		},
	}),
	Action: func(c *cli.Context) error {
		ctx := context.Background()
		ctx, cfg, l, _, done := setupLoggerAndConfig[ArtifactShasumConfig](ctx, c)
		defer done()
		ctx, span := otel.Tracer("buildkite-agent").Start(ctx, "artifact-shasum")
		defer span.End()
		return searchAndPrintShaSum(ctx, cfg, l, os.Stdout)
	},
}
View Source
var ArtifactUploadCommand = cli.Command{
	Name:        "upload",
	Usage:       "Uploads files to a job as artifacts",
	Description: uploadHelpDescription,
	Flags: slices.Concat(globalFlags(), apiFlags(), []cli.Flag{
		cli.StringFlag{
			Name:   "job",
			Value:  "",
			Usage:  "Which job should the artifacts be uploaded to",
			EnvVar: "BUILDKITE_JOB_ID",
		},
		cli.StringFlag{
			Name:   "content-type",
			Value:  "",
			Usage:  "A specific Content-Type to set for the artifacts (otherwise detected)",
			EnvVar: "BUILDKITE_ARTIFACT_CONTENT_TYPE",
		},
		cli.BoolFlag{
			Name:   "literal",
			Usage:  "Disables parsing of the upload paths as glob patterns; each path will be treated as a single literal file path (default: false)",
			EnvVar: "BUILDKITE_AGENT_ARTIFACT_LITERAL",
		},
		cli.StringFlag{
			Name:   "delimiter",
			Usage:  "Changes the delimiter used to split the upload paths into multiple paths; it can be more than 1 character. When set to the empty string, no splitting occurs",
			EnvVar: "BUILDKITE_AGENT_ARTIFACT_DELIMITER",
			Value:  ";",
		},
		cli.BoolFlag{
			Name:   "glob-resolve-follow-symlinks",
			Usage:  "Follow symbolic links to directories while resolving globs. Note: this will not prevent symlinks to files from being uploaded. Use --upload-skip-symlinks to do that (default: false)",
			EnvVar: "BUILDKITE_AGENT_ARTIFACT_GLOB_RESOLVE_FOLLOW_SYMLINKS",
		},
		cli.BoolFlag{
			Name:   "upload-skip-symlinks",
			Usage:  "After the glob has been resolved to a list of files to upload, skip uploading those that are symlinks to files (default: false)",
			EnvVar: "BUILDKITE_ARTIFACT_UPLOAD_SKIP_SYMLINKS",
		},
		cli.BoolFlag{
			Name:   "follow-symlinks",
			Usage:  "Follow symbolic links while resolving globs. Note this argument is deprecated. Use `--glob-resolve-follow-symlinks` instead (default: false)",
			EnvVar: "BUILDKITE_AGENT_ARTIFACT_SYMLINKS",
		},
		NoMultipartArtifactUploadFlag,
	}),
	Action: func(c *cli.Context) error {
		ctx := context.Background()
		ctx, cfg, l, _, done := setupLoggerAndConfig[ArtifactUploadConfig](ctx, c)
		defer done()

		ctx, span := otel.Tracer("buildkite-agent").Start(ctx, "artifact-upload")
		defer span.End()

		client := api.NewClient(l, loadAPIClientConfig(cfg, "AgentAccessToken"))

		uploader := artifact.NewUploader(l, client, artifact.UploaderConfig{
			JobID:          cfg.Job,
			Paths:          cfg.UploadPaths,
			Destination:    cfg.Destination,
			ContentType:    cfg.ContentType,
			DebugHTTP:      cfg.DebugHTTP,
			TraceHTTP:      cfg.TraceHTTP,
			DisableHTTP2:   cfg.NoHTTP2,
			AllowMultipart: !cfg.NoMultipartUpload,
			Literal:        cfg.Literal,
			Delimiter:      cfg.Delimiter,

			GlobResolveFollowSymlinks: (cfg.GlobResolveFollowSymlinks || cfg.FollowSymlinks),
			UploadSkipSymlinks:        cfg.UploadSkipSymlinks,
		})

		if err := uploader.Upload(ctx); err != nil {
			return fmt.Errorf("failed to upload artifacts: %w", err)
		}

		return nil
	},
}
View Source
var BootstrapCommand = cli.Command{
	Name:        "bootstrap",
	Usage:       "Harness used internally by the agent to run jobs as subprocesses",
	Category:    categoryInternal,
	Description: bootstrapHelpDescription,
	Flags: []cli.Flag{
		cli.StringFlag{
			Name:   "command",
			Value:  "",
			Usage:  "The command to run",
			EnvVar: "BUILDKITE_COMMAND",
		},
		cli.StringFlag{
			Name:   "job",
			Value:  "",
			Usage:  "The ID of the job being run",
			EnvVar: "BUILDKITE_JOB_ID",
		},
		cli.StringFlag{
			Name:   "repository",
			Value:  "",
			Usage:  "The repository to clone and run the job from",
			EnvVar: "BUILDKITE_REPO",
		},
		cli.StringFlag{
			Name:   "commit",
			Value:  "",
			Usage:  "The commit to checkout in the repository",
			EnvVar: "BUILDKITE_COMMIT",
		},
		cli.StringFlag{
			Name:   "branch",
			Value:  "",
			Usage:  "The branch the commit is in",
			EnvVar: "BUILDKITE_BRANCH",
		},
		cli.StringFlag{
			Name:   "tag",
			Value:  "",
			Usage:  "The tag the commit",
			EnvVar: "BUILDKITE_TAG",
		},
		cli.StringFlag{
			Name:   "refspec",
			Value:  "",
			Usage:  "Optional refspec to override git fetch",
			EnvVar: "BUILDKITE_REFSPEC",
		},
		cli.StringFlag{
			Name:   "plugins",
			Value:  "",
			Usage:  "The plugins for the job",
			EnvVar: "BUILDKITE_PLUGINS",
		},
		cli.StringFlag{
			Name:   "secrets",
			Value:  "",
			Usage:  "Secrets to be loaded into the job environment",
			EnvVar: "BUILDKITE_SECRETS_CONFIG",
		},
		cli.StringFlag{
			Name:   "pullrequest",
			Value:  "",
			Usage:  "The number/id of the pull request this commit belonged to",
			EnvVar: "BUILDKITE_PULL_REQUEST",
		},
		cli.BoolFlag{
			Name:   "pull-request-using-merge-refspec",
			Usage:  "Whether the agent should attempt to checkout the pull request commit using the merge refspec. This feature is in private preview and requires backend enablement—contact support to enable (default: false)",
			EnvVar: "BUILDKITE_PULL_REQUEST_USING_MERGE_REFSPEC",
		},
		cli.StringFlag{
			Name:   "agent",
			Value:  "",
			Usage:  "The name of the agent running the job",
			EnvVar: "BUILDKITE_AGENT_NAME",
		},
		cli.StringFlag{
			Name:   "queue",
			Value:  "",
			Usage:  "The name of the queue the agent belongs to, if tagged",
			EnvVar: "BUILDKITE_AGENT_META_DATA_QUEUE",
		},
		cli.StringFlag{
			Name:   "organization",
			Value:  "",
			Usage:  "The slug of the organization that the job is a part of",
			EnvVar: "BUILDKITE_ORGANIZATION_SLUG",
		},
		cli.StringFlag{
			Name:   "pipeline",
			Value:  "",
			Usage:  "The slug of the pipeline that the job is a part of",
			EnvVar: "BUILDKITE_PIPELINE_SLUG",
		},
		cli.StringFlag{
			Name:   "pipeline-provider",
			Value:  "",
			Usage:  "The id of the SCM provider that the repository is hosted on",
			EnvVar: "BUILDKITE_PIPELINE_PROVIDER",
		},
		cli.StringFlag{
			Name:   "artifact-upload-paths",
			Value:  "",
			Usage:  "Paths to files to automatically upload at the end of a job",
			EnvVar: "BUILDKITE_ARTIFACT_PATHS",
		},
		cli.StringFlag{
			Name:   "artifact-upload-destination",
			Value:  "",
			Usage:  "A custom location to upload artifact paths to (for example, s3://my-custom-bucket/and/prefix)",
			EnvVar: "BUILDKITE_ARTIFACT_UPLOAD_DESTINATION",
		},
		cli.BoolFlag{
			Name:   "clean-checkout",
			Usage:  "Whether or not the bootstrap should remove the existing repository before running the command (default: false)",
			EnvVar: "BUILDKITE_CLEAN_CHECKOUT",
		},

		SkipCheckoutFlag,
		GitCheckoutFlagsFlag,
		GitCloneFlagsFlag,
		GitCloneMirrorFlagsFlag,
		GitCleanFlagsFlag,
		GitFetchFlagsFlag,
		GitMirrorsPathFlag,
		GitMirrorCheckoutModeFlag,
		GitMirrorsLockTimeoutFlag,
		GitMirrorsSkipUpdateFlag,
		GitSubmoduleCloneConfigFlag,
		GitSkipFetchExistingCommitsFlag,
		CheckoutAttemptsFlag,

		cli.StringFlag{
			Name:   "bin-path",
			Value:  "",
			Usage:  "Directory where the buildkite-agent binary lives",
			EnvVar: "BUILDKITE_BIN_PATH",
		},

		BuildPathFlag,
		HooksPathFlag,
		AdditionalHooksPathsFlag,
		SocketsPathFlag,
		PluginsPathFlag,

		cli.BoolTFlag{
			Name:   "command-eval",
			Usage:  "Allow running of arbitrary commands (default: true)",
			EnvVar: "BUILDKITE_COMMAND_EVAL",
		},
		cli.BoolTFlag{
			Name:   "plugins-enabled",
			Usage:  "Allow plugins to be run (default: true)",
			EnvVar: "BUILDKITE_PLUGINS_ENABLED",
		},
		cli.BoolFlag{
			Name:   "plugin-validation",
			Usage:  "Validate plugin configuration (default: false)",
			EnvVar: "BUILDKITE_PLUGIN_VALIDATION",
		},
		cli.BoolFlag{
			Name:   "plugins-always-clone-fresh",
			Usage:  "Always make a new clone of plugin source, even if already present (default: false)",
			EnvVar: "BUILDKITE_PLUGINS_ALWAYS_CLONE_FRESH",
		},
		cli.BoolTFlag{
			Name:   "local-hooks-enabled",
			Usage:  "Allow local hooks to be run (default: true)",
			EnvVar: "BUILDKITE_LOCAL_HOOKS_ENABLED",
		},
		cli.BoolTFlag{
			Name:   "ssh-keyscan",
			Usage:  "Automatically run ssh-keyscan before checkout (default: true)",
			EnvVar: "BUILDKITE_SSH_KEYSCAN",
		},
		cli.BoolTFlag{
			Name:   "git-submodules",
			Usage:  "Enable git submodules (default: true)",
			EnvVar: "BUILDKITE_GIT_SUBMODULES",
		},
		cli.BoolTFlag{
			Name:   "pty",
			Usage:  "Run jobs within a pseudo terminal (default: true)",
			EnvVar: "BUILDKITE_PTY",
		},
		cli.StringFlag{
			Name:   "shell",
			Usage:  "The shell to use to interpret build commands",
			EnvVar: "BUILDKITE_SHELL",
			Value:  DefaultShell(),
		},
		cli.StringFlag{
			Name:   "hooks-shell",
			Usage:  "The shell to use to interpret hooks commands",
			EnvVar: "BUILDKITE_HOOKS_SHELL",
		},
		cli.StringSliceFlag{
			Name:   "phases",
			Usage:  "The specific phases to execute. The order they're defined is irrelevant.",
			EnvVar: "BUILDKITE_BOOTSTRAP_PHASES",
		},
		cli.StringFlag{
			Name:   "tracing-backend",
			Usage:  "The name of the tracing backend to use.",
			EnvVar: "BUILDKITE_TRACING_BACKEND",
			Value:  "",
		},
		cli.StringFlag{
			Name:   "tracing-service-name",
			Usage:  "Service name to use when reporting traces.",
			EnvVar: "BUILDKITE_TRACING_SERVICE_NAME",
			Value:  "buildkite-agent",
		},
		cli.StringFlag{
			Name:   "tracing-traceparent",
			Usage:  "W3C Trace Parent for tracing",
			EnvVar: "BUILDKITE_TRACING_TRACEPARENT",
			Value:  "",
		},
		cli.BoolFlag{
			Name:   "tracing-propagate-traceparent",
			Usage:  "Accept traceparent from Buildkite control plane (default: false)",
			EnvVar: "BUILDKITE_TRACING_PROPAGATE_TRACEPARENT",
		},

		cli.BoolFlag{
			Name:   "no-job-api",
			Usage:  "Disables the Job API, which gives commands in jobs some abilities to introspect and mutate the state of the job (default: false)",
			EnvVar: "BUILDKITE_AGENT_NO_JOB_API",
		},
		cli.StringSliceFlag{
			Name:   "disable-warnings-for",
			Usage:  "A list of warning IDs to disable",
			EnvVar: "BUILDKITE_AGENT_DISABLE_WARNINGS_FOR",
		},
		cancelSignalFlag,
		cancelGracePeriodFlag,
		signalGracePeriodSecondsFlag,

		DebugFlag,
		LogLevelFlag,
		ExperimentsFlag,
		ProfileFlag,
		RedactedVars,
		StrictSingleHooksFlag,
		TraceContextEncodingFlag,
	},
	Action: func(c *cli.Context) error {
		ctx := context.Background()
		ctx, cfg, l, _, done := setupLoggerAndConfig[BootstrapConfig](ctx, c)
		defer done()

		if cfg.JobID == "1111-1111-1111-1111" {
			if overrideSelf := os.Getenv("BUILDKITE_OVERRIDE_SELF"); overrideSelf != "" {
				ctx = self.OverridePath(ctx, overrideSelf)
			}
		}

		runInPty := cfg.PTY
		if runtime.GOOS == "windows" {
			runInPty = false
		}

		for _, phase := range cfg.Phases {
			switch phase {
			case "plugin", "checkout", "command":

			default:
				return fmt.Errorf("invalid phase %q", phase)
			}
		}

		if !slices.Contains(mirrorCheckoutModes, cfg.GitMirrorCheckoutMode) {
			return fmt.Errorf("invalid git mirror checkout mode %q, must be one of %v", cfg.GitMirrorCheckoutMode, mirrorCheckoutModes)
		}

		cancelSig, err := process.ParseSignal(cfg.CancelSignal)
		if err != nil {
			return fmt.Errorf("failed to parse cancel-signal: %w", err)
		}

		signalGracePeriod, err := signalGracePeriod(cfg.CancelGracePeriod, cfg.SignalGracePeriodSeconds)
		if err != nil {
			return err
		}

		traceContextCodec, err := tracetools.ParseEncoding(cfg.TraceContextEncoding)
		if err != nil {
			return fmt.Errorf("while parsing trace context encoding: %v", err)
		}

		bootstrap := job.New(job.ExecutorConfig{
			AgentName:                    cfg.AgentName,
			ArtifactUploadDestination:    cfg.ArtifactUploadDestination,
			AutomaticArtifactUploadPaths: cfg.AutomaticArtifactUploadPaths,
			BinPath:                      cfg.BinPath,
			Branch:                       cfg.Branch,
			BuildPath:                    cfg.BuildPath,
			SocketsPath:                  cfg.SocketsPath,
			CancelSignal:                 cancelSig,
			SignalGracePeriod:            signalGracePeriod,
			CleanCheckout:                cfg.CleanCheckout,
			SkipCheckout:                 cfg.SkipCheckout,
			GitSkipFetchExistingCommits:  cfg.GitSkipFetchExistingCommits,
			Command:                      cfg.Command,
			CommandEval:                  cfg.CommandEval,
			Commit:                       cfg.Commit,
			Debug:                        cfg.Debug,
			GitCheckoutFlags:             cfg.GitCheckoutFlags,
			GitCleanFlags:                cfg.GitCleanFlags,
			GitCloneFlags:                cfg.GitCloneFlags,
			GitCloneMirrorFlags:          cfg.GitCloneMirrorFlags,
			GitFetchFlags:                cfg.GitFetchFlags,
			GitMirrorsLockTimeout:        cfg.GitMirrorsLockTimeout,
			GitMirrorsPath:               cfg.GitMirrorsPath,
			GitMirrorCheckoutMode:        cfg.GitMirrorCheckoutMode,
			GitMirrorsSkipUpdate:         cfg.GitMirrorsSkipUpdate,
			GitSubmodules:                cfg.GitSubmodules,
			GitSubmoduleCloneConfig:      cfg.GitSubmoduleCloneConfig,
			HooksPath:                    cfg.HooksPath,
			AdditionalHooksPaths:         cfg.AdditionalHooksPaths,
			JobID:                        cfg.JobID,
			LocalHooksEnabled:            cfg.LocalHooksEnabled,
			OrganizationSlug:             cfg.OrganizationSlug,
			Phases:                       cfg.Phases,
			PipelineProvider:             cfg.PipelineProvider,
			PipelineSlug:                 cfg.PipelineSlug,
			PluginValidation:             cfg.PluginValidation,
			Plugins:                      cfg.Plugins,
			PluginsEnabled:               cfg.PluginsEnabled,
			PluginsAlwaysCloneFresh:      cfg.PluginsAlwaysCloneFresh,
			PluginsPath:                  cfg.PluginsPath,
			PullRequest:                  cfg.PullRequest,
			PullRequestUsingMergeRefspec: cfg.PullRequestUsingMergeRefspec,
			Queue:                        cfg.Queue,
			RedactedVars:                 cfg.RedactedVars,
			RefSpec:                      cfg.RefSpec,
			Repository:                   cfg.Repository,
			RunInPty:                     runInPty,
			SSHKeyscan:                   cfg.SSHKeyscan,
			Shell:                        cfg.Shell,
			HooksShell:                   cfg.HooksShell,
			StrictSingleHooks:            cfg.StrictSingleHooks,
			Tag:                          cfg.Tag,
			TracingBackend:               cfg.TracingBackend,
			TracingServiceName:           cfg.TracingServiceName,
			TraceContextCodec:            traceContextCodec,
			TracingTraceParent:           cfg.TracingTraceParent,
			TracingPropagateTraceparent:  cfg.TracingPropagateTraceparent,
			JobAPI:                       !cfg.NoJobAPI,
			DisabledWarnings:             cfg.DisableWarningsFor,
			Secrets:                      cfg.Secrets,
			CheckoutAttempts:             cfg.CheckoutAttempts,
		})

		cctx, cancel := context.WithCancel(ctx)
		defer cancel()

		signals := make(chan os.Signal, 1)
		signal.Notify(signals, os.Interrupt,
			syscall.SIGHUP,
			syscall.SIGTERM,
			syscall.SIGINT,
			syscall.SIGQUIT,
		)
		defer signal.Stop(signals)

		var (
			cancelled bool
			received  os.Signal
			signalMu  sync.Mutex
		)

		go func() {
			sig := <-signals
			signalMu.Lock()
			defer signalMu.Unlock()

			if err := bootstrap.Cancel(); err != nil {
				l.Debugf("Failed to cancel bootstrap: %v", err)
			}

			cancelled = true
			received = sig

			signal.Stop(signals)
		}()

		exitCode := bootstrap.Run(cctx)

		signalMu.Lock()
		defer signalMu.Unlock()

		if cancelled && runtime.GOOS != "windows" {

			if received == syscall.SIGQUIT {
				return &SilentExitError{code: 131}
			}
			if err := signalSelf(l, received); err != nil {
				l.Errorf("Failed to signal self: %v", err)
			}
		}

		return &SilentExitError{code: exitCode}
	},
}
View Source
var BuildCancelCommand = cli.Command{
	Name:        "cancel",
	Usage:       "Cancel a build",
	Description: buildCancelDescription,
	Flags: slices.Concat(globalFlags(), apiFlags(), []cli.Flag{
		cli.StringFlag{
			Name:   "build",
			Value:  "",
			Usage:  "The build UUID to cancel",
			EnvVar: "BUILDKITE_BUILD_ID",
		},
	}),
	Action: func(c *cli.Context) error {
		ctx := context.Background()
		ctx, cfg, l, _, done := setupLoggerAndConfig[BuildCancelConfig](ctx, c)
		defer done()

		return cancelBuild(ctx, cfg, l)
	},
}
View Source
var BuildkiteAgentCommands = []cli.Command{

	AgentStartCommand,
	BootstrapCommand,
	KubernetesBootstrapCommand,

	AcknowledgementsCommand,
	AnnotateCommand,
	{
		Name:     "annotation",
		Category: categoryJobCommands,
		Usage:    "Make changes to annotations on the currently running build",
		Subcommands: []cli.Command{
			AnnotationRemoveCommand,
		},
	},
	{
		Name:     "artifact",
		Category: categoryJobCommands,
		Usage:    "Upload/download artifacts from Buildkite jobs",
		Subcommands: []cli.Command{
			ArtifactUploadCommand,
			ArtifactDownloadCommand,
			ArtifactSearchCommand,
			ArtifactShasumCommand,
		},
	},
	{
		Name:     "build",
		Category: categoryJobCommands,
		Usage:    "Interact with a Buildkite build",
		Subcommands: []cli.Command{
			BuildCancelCommand,
		},
	},
	{
		Name:     "job",
		Category: categoryJobCommands,
		Usage:    "Interact with a Buildkite job",
		Subcommands: []cli.Command{
			JobUpdateCommand,
		},
	},
	{
		Name:     "cache",
		Category: categoryJobCommands,
		Usage:    "Manage build caches",
		Hidden:   true,
		Subcommands: []cli.Command{
			CacheSaveCommand,
			CacheRestoreCommand,
		},
	},
	{
		Name:     "env",
		Category: categoryJobCommands,
		Usage:    "Interact with the environment of the currently running build",
		Subcommands: []cli.Command{
			EnvDumpCommand,
			EnvGetCommand,
			EnvSetCommand,
			EnvUnsetCommand,
		},
	},
	GitCredentialsHelperCommand,
	{
		Name:     "lock",
		Category: categoryJobCommands,
		Usage:    "Lock or unlock resources for the currently running build",
		Subcommands: []cli.Command{
			LockAcquireCommand,
			LockDoCommand,
			LockDoneCommand,
			LockGetCommand,
			LockReleaseCommand,
		},
	},
	{
		Name:     "redactor",
		Category: categoryJobCommands,
		Usage:    "Redact sensitive information from logs",
		Subcommands: []cli.Command{
			RedactorAddCommand,
		},
	},
	{
		Name:     "meta-data",
		Category: categoryJobCommands,
		Usage:    "Get/set metadata from Buildkite jobs",
		Subcommands: []cli.Command{
			MetaDataSetCommand,
			MetaDataGetCommand,
			MetaDataExistsCommand,
			MetaDataKeysCommand,
		},
	},
	{
		Name:     "oidc",
		Category: categoryJobCommands,
		Usage:    "Interact with Buildkite OpenID Connect (OIDC)",
		Subcommands: []cli.Command{
			OIDCRequestTokenCommand,
		},
	},
	AgentPauseCommand,
	{
		Name:     "pipeline",
		Category: categoryJobCommands,
		Usage:    "Make changes to the pipeline of the currently running build",
		Subcommands: []cli.Command{
			PipelineUploadCommand,
		},
	},
	AgentResumeCommand,
	{
		Name:     "secret",
		Category: categoryJobCommands,
		Usage:    "Interact with Pipelines Secrets",
		Subcommands: []cli.Command{
			SecretGetCommand,
		},
	},
	{
		Name:     "step",
		Category: categoryJobCommands,
		Usage:    "Get or update an attribute of a build step, or cancel unfinished jobs for a step",
		Subcommands: []cli.Command{
			StepGetCommand,
			StepUpdateCommand,
			StepCancelCommand,
		},
	},
	AgentStopCommand,
	{
		Name:  "tool",
		Usage: "Utilities for working with the Buildkite Agent",
		Subcommands: []cli.Command{
			ToolKeygenCommand,
			ToolSignCommand,
		},
	},
}
View Source
var CacheRestoreCommand = cli.Command{
	Name:        "restore",
	Usage:       "Restores files from the cache",
	Description: cacheRestoreHelpDescription,
	Flags:       slices.Concat(globalFlags(), apiFlags(), cacheFlags()),
	Action: func(c *cli.Context) error {
		ctx := context.Background()
		ctx, cfg, l, _, done := setupLoggerAndConfig[CacheRestoreConfig](ctx, c)
		defer done()
		ctx, span := otel.Tracer("buildkite-agent").Start(ctx, "cache-restore")
		defer span.End()

		l.Infof("Cache restore command executed")

		apiCfg := loadAPIClientConfig(cfg, "AgentAccessToken")

		cacheCfg := cache.Config{
			BucketURL:       cfg.BucketURL,
			Branch:          cfg.Branch,
			Pipeline:        cfg.Pipeline,
			Organization:    cfg.Organization,
			CacheConfigFile: cfg.CacheConfigFile,
			Ids:             cfg.Ids,
			APIEndpoint:     apiCfg.Endpoint,
			APIToken:        apiCfg.Token,
		}

		return cache.Restore(ctx, l, cacheCfg)
	},
}
View Source
var CacheSaveCommand = cli.Command{
	Name:        "save",
	Usage:       "Saves files to the cache",
	Description: cacheSaveHelpDescription,
	Flags:       slices.Concat(globalFlags(), apiFlags(), cacheFlags()),
	Action: func(c *cli.Context) error {
		ctx := context.Background()
		ctx, cfg, l, _, done := setupLoggerAndConfig[CacheSaveConfig](ctx, c)
		defer done()
		ctx, span := otel.Tracer("buildkite-agent").Start(ctx, "cache-save")
		defer span.End()

		l.Infof("Cache save command executed")

		apiCfg := loadAPIClientConfig(cfg, "AgentAccessToken")

		if apiCfg.Token == "" {
			return fmt.Errorf("an API token must be provided to save caches")
		}

		cacheCfg := cache.Config{
			BucketURL:       cfg.BucketURL,
			Branch:          cfg.Branch,
			Pipeline:        cfg.Pipeline,
			Organization:    cfg.Organization,
			CacheConfigFile: cfg.CacheConfigFile,
			Ids:             cfg.Ids,
			APIEndpoint:     apiCfg.Endpoint,
			APIToken:        apiCfg.Token,
			Concurrency:     cfg.Concurrency,
		}

		return cache.Save(ctx, l, cacheCfg)
	},
}
View Source
var EnvDumpCommand = cli.Command{
	Name:        "dump",
	Usage:       "Print the environment of the current process as a JSON object",
	Description: envDumpHelpDescription,
	Flags: append(globalFlags(),
		cli.StringFlag{
			Name:   "format",
			Usage:  "Output format; json or json-pretty",
			EnvVar: "BUILDKITE_AGENT_ENV_DUMP_FORMAT",
			Value:  "json",
		},
	),
	Action: func(c *cli.Context) error {
		_, cfg, _, _, done := setupLoggerAndConfig[EnvDumpConfig](context.Background(), c)
		defer done()

		envn := os.Environ()
		envMap := make(map[string]string, len(envn))

		for _, e := range envn {
			if k, v, ok := env.Split(e); ok {
				envMap[k] = v
			}
		}

		enc := json.NewEncoder(c.App.Writer)
		if cfg.Format == "json-pretty" {
			enc.SetIndent("", "  ")
		}

		if err := enc.Encode(envMap); err != nil {
			return fmt.Errorf("error marshalling JSON: %w", err)
		}

		return nil
	},
}
View Source
var EnvGetCommand = cli.Command{
	Name:        "get",
	Usage:       "Gets variables from the job execution environment",
	Description: envGetHelpDescription,
	Flags: append(globalFlags(),
		cli.StringFlag{
			Name:   "format",
			Usage:  "Output format: plain, json, or json-pretty",
			EnvVar: "BUILDKITE_AGENT_ENV_GET_FORMAT",
			Value:  "plain",
		},
	),
	Action: envGetAction,
}
View Source
var EnvSetCommand = cli.Command{
	Name:        "set",
	Usage:       "Sets variables in the job execution environment",
	Description: envSetHelpDescription,
	Flags: append(globalFlags(),
		cli.StringFlag{
			Name:   "input-format",
			Usage:  "Input format: plain or json",
			EnvVar: "BUILDKITE_AGENT_ENV_SET_INPUT_FORMAT",
			Value:  "plain",
		},
		cli.StringFlag{
			Name:   "output-format",
			Usage:  "Output format: quiet (no output), plain, json, or json-pretty",
			EnvVar: "BUILDKITE_AGENT_ENV_SET_OUTPUT_FORMAT",
			Value:  "plain",
		},
	),
	Action: envSetAction,
}
View Source
var EnvUnsetCommand = cli.Command{
	Name:        "unset",
	Usage:       "Unsets variables from the job execution environment",
	Description: envUnsetHelpDescription,
	Flags: append(globalFlags(),
		cli.StringFlag{
			Name:   "input-format",
			Usage:  "Input format: plain or json",
			EnvVar: "BUILDKITE_AGENT_ENV_UNSET_INPUT_FORMAT",
			Value:  "plain",
		},
		cli.StringFlag{
			Name:   "output-format",
			Usage:  "Output format: quiet (no output), plain, json, or json-pretty",
			EnvVar: "BUILDKITE_AGENT_ENV_UNSET_OUTPUT_FORMAT",
			Value:  "plain",
		},
	),
	Action: envUnsetAction,
}
View Source
var GitCredentialsHelperCommand = cli.Command{
	Name:        "git-credentials-helper",
	Usage:       "Internal process used by hosted compute jobs to authenticate with Github",
	Category:    categoryInternal,
	Description: gitCredentialsHelperHelpDescription,
	Flags: append(globalFlags(),
		cli.StringFlag{
			Name:   "job-id",
			Usage:  "The job id to get credentials for",
			EnvVar: "BUILDKITE_JOB_ID",
		},

		AgentAccessTokenFlag,
		EndpointFlag,
		NoHTTP2Flag,
	),
	Action: func(c *cli.Context) error {
		ctx := context.Background()
		ctx, cfg, l, _, done := setupLoggerAndConfig[GitCredentialsHelperConfig](ctx, c)
		defer done()

		l.Debugf("Action: %s", cfg.Action)
		if cfg.Action != "get" {

			return nil
		}

		if os.Getenv("BUILDKITE_JOB_ID") == "" {
			l.Warnf("📎💬 It looks like you're calling this command directly in a step, rather than having the agent automatically call it")
			l.Warnf("This command is intended to be used as a git credential helper, and not called directly.")
		}

		stdin, err := io.ReadAll(os.Stdin)
		if err != nil {
			return handleAuthError(c, l, fmt.Errorf("failed to read stdin: %w", err))
		}

		l.Debugf("Git credential input:\n%s\n", string(stdin))

		l.Debugf("Authenticating checkout using Buildkite Github App Credentials...")

		repo, err := parseGitURLFromCredentialInput(string(stdin))
		if err != nil {
			return handleAuthError(c, l, fmt.Errorf("failed to parse git URL from stdin: %w", err))
		}

		client := api.NewClient(l, loadAPIClientConfig(cfg, "AgentAccessToken"))
		tok, _, err := client.GenerateGithubCodeAccessToken(ctx, repo, cfg.JobID)
		if err != nil {
			return handleAuthError(c, l, fmt.Errorf("failed to get github app credentials: %w", err))
		}

		_, _ = fmt.Fprintln(c.App.Writer, "username=token")
		_, _ = fmt.Fprintln(c.App.Writer, "password="+tok)
		_, _ = fmt.Fprintln(c.App.Writer, "")

		l.Debugf("Authentication successful!")

		return nil
	},
}
View Source
var JobUpdateCommand = cli.Command{
	Name:        "update",
	Usage:       "Change the value of an attribute of a job",
	Description: jobUpdateHelpDescription,
	Flags: slices.Concat(globalFlags(), apiFlags(), []cli.Flag{
		cli.StringFlag{
			Name:   "job",
			Value:  "",
			Usage:  "The job to update. Defaults to the current job",
			EnvVar: "BUILDKITE_JOB_ID",
		},
		RedactedVars,
	}),
	Action: func(c *cli.Context) error {
		ctx, cfg, l, _, done := setupLoggerAndConfig[JobUpdateConfig](context.Background(), c)
		defer done()

		if len(c.Args()) < 2 {
			l.Infof("Reading value from STDIN")

			input, err := io.ReadAll(os.Stdin)
			if err != nil {
				return fmt.Errorf("failed to read from STDIN: %w", err)
			}
			cfg.Value = string(input)
		}

		client := api.NewClient(l, loadAPIClientConfig(cfg, "AgentAccessToken"))

		needles, _, err := redact.NeedlesFromEnv(cfg.RedactedVars)
		if err != nil {
			return err
		}
		if redactedValue := redact.String(cfg.Value, needles); redactedValue != cfg.Value {
			l.Warnf("New value for job %q attribute %q contained one or more secrets from environment variables that have been redacted. If this is deliberate, pass --redacted-vars='' or a list of patterns that does not match the variable containing the secret", cfg.Job, cfg.Attribute)
			cfg.Value = redactedValue
		}

		attrs := map[string]string{cfg.Attribute: cfg.Value}

		if err := roko.NewRetrier(
			roko.WithMaxAttempts(10),
			roko.WithStrategy(roko.ExponentialSubsecond(2*time.Second)),
		).DoWithContext(ctx, func(r *roko.Retrier) error {
			_, resp, err := client.UpdateJob(ctx, cfg.Job, attrs)
			if api.BreakOnNonRetryable(r, resp, err) {
				return err
			}
			if err != nil {
				l.Warnf("%s (%s)", err, r)
				return err
			}
			return nil
		}); err != nil {
			return fmt.Errorf("failed to update job: %w", err)
		}

		return nil
	},
}
View Source
var KubernetesBootstrapCommand = cli.Command{
	Name:        "kubernetes-bootstrap",
	Usage:       "Harness used internally by the agent to run jobs on Kubernetes",
	Category:    categoryInternal,
	Description: kubernetesBootstrapHelpDescription,
	Flags: []cli.Flag{
		KubernetesContainerIDFlag,
		cli.DurationFlag{
			Name: "kubernetes-bootstrap-connection-timeout",
			Usage: "This is intended to be used only by the Buildkite k8s stack " +
				"(github.com/buildkite/agent-stack-k8s); it set the max time a container will wait " +
				"to connect Agent.",
			EnvVar: "BUILDKITE_KUBERNETES_BOOTSTRAP_CONNECTION_TIMEOUT",
		},

		DebugFlag,
		LogLevelFlag,
		ExperimentsFlag,
		ProfileFlag,
	},
	Action: func(c *cli.Context) error {

		ctx := context.Background()
		ctx, cfg, l, _, done := setupLoggerAndConfig[KubernetesBootstrapConfig](ctx, c)
		defer done()

		ctx, cancel := context.WithCancel(ctx)
		defer cancel()

		socket := &kubernetes.Client{ID: cfg.KubernetesContainerID}

		connectionTimeout := 120 * time.Second
		if cfg.KubernetesBootstrapConnectionTimeout > 0 {
			connectionTimeout = cfg.KubernetesBootstrapConnectionTimeout
		}
		connectCtx, connectCancel := context.WithTimeout(ctx, connectionTimeout)
		defer connectCancel()
		regResp, err := socket.Connect(connectCtx)
		if err != nil {
			return fmt.Errorf("error connecting to kubernetes runner: %w", err)
		}
		defer socket.Close()

		environ := env.FromSlice(slices.Concat(regResp.Env, os.Environ()))

		buildPath := environ.GetString("BUILDKITE_BUILD_PATH", "/workspace/build")
		runInPTY := environ.GetBool("BUILDKITE_PTY", true)
		cancelSignal := process.SIGTERM
		if sig, has := environ.Get("BUILDKITE_CANCEL_SIGNAL"); has {
			cs, err := process.ParseSignal(sig)
			if err != nil {
				return err
			}

			if cs == process.SIGKILL {
				cs = process.SIGTERM
			}
			cancelSignal = cs
		}
		cancelGracePeriodSecs := environ.GetInt("BUILDKITE_CANCEL_GRACE_PERIOD", defaultCancelGracePeriodSecs)
		cancelGracePeriod := time.Duration(cancelGracePeriodSecs) * time.Second
		signalGracePeriodSecs := environ.GetInt("BUILDKITE_SIGNAL_GRACE_PERIOD_SECONDS", defaultSignalGracePeriodSecs)
		signalGracePeriod, err := signalGracePeriod(cancelGracePeriodSecs, signalGracePeriodSecs)
		if err != nil {
			return err
		}

		environ.Set("BUILDKITE_KUBERNETES_EXEC", "true")

		if _, exists := environ.Get("BUILDKITE_BUILD_CHECKOUT_PATH"); !exists {

			environ.Set("BUILDKITE_BUILD_CHECKOUT_PATH", filepath.Join(buildPath, "buildkite"))
		}

		self, err := os.Executable()
		if err != nil {
			return fmt.Errorf("finding absolute path to executable: %w", err)
		}
		environ.Set("BUILDKITE_BIN_PATH", filepath.Dir(self))

		if err := socket.StatusLoop(context.WithoutCancel(ctx), func(err error) {

			if err != nil {
				l.Errorf("kubernetes-bootstrap: Error waiting for client interrupt: %v; cancelling work", err)
			} else {
				l.Warnf("kubernetes-bootstrap: Either the job was cancelled or the pod is being deleted; cancelling work")
			}

			cancel()

			go func() {

				time.Sleep(cancelGracePeriod)

				l.Infof("kubernetes-bootstrap: Timed out waiting for subprocess to exit; exiting immediately with status 1")
				os.Exit(1)
			}()
		}); err != nil {
			return fmt.Errorf("connecting to k8s socket: %w", err)
		}

		phases := environ.GetString("BUILDKITE_BOOTSTRAP_PHASES", "(unknown)")
		_, _ = fmt.Fprintf(socket, "~~~ Bootstrapping phases %s\n", phases)

		proc := process.New(l, process.Config{
			Path:              self,
			Args:              []string{"bootstrap"},
			Env:               environ.ToSlice(),
			Stdout:            io.MultiWriter(os.Stdout, socket),
			Stderr:            io.MultiWriter(os.Stderr, socket),
			Dir:               buildPath,
			PTY:               runInPTY,
			InterruptSignal:   cancelSignal,
			SignalGracePeriod: signalGracePeriod,
		})

		signals := make(chan os.Signal, 1)
		signal.Notify(
			signals,
			os.Interrupt,
			syscall.SIGHUP,
			syscall.SIGTERM,
			syscall.SIGINT,
			syscall.SIGQUIT,
		)
		go func() {
			for {
				select {
				case <-ctx.Done():
					return
				case <-proc.Done():
					return
				case sig := <-signals:

					l.Infof("kubernetes-bootstrap: Received %v; awaiting interrupt from agent", sig)
				}
			}
		}()

		exitCode := -1
		defer func() { _ = socket.Exit(exitCode) }()

		if err := proc.Run(ctx); err != nil {
			_, _ = fmt.Fprintf(socket, "Couldn't execute bootstrap: %v\n", err)
			return &ExitError{1, err}
		}

		exitCode = proc.WaitStatus().ExitStatus()
		return &SilentExitError{code: exitCode}
	},
}
View Source
var LockAcquireCommand = cli.Command{
	Name:        "acquire",
	Usage:       "Acquires a lock from the agent leader",
	Description: lockAcquireHelpDescription,
	Flags: append(lockCommonFlags(),
		cli.DurationFlag{
			Name:   "lock-wait-timeout",
			Usage:  "Sets a maximum duration to wait for a lock before giving up",
			EnvVar: "BUILDKITE_LOCK_WAIT_TIMEOUT",
		},
	),
	Action: lockAcquireAction,
}
View Source
var LockDoCommand = cli.Command{
	Name:        "do",
	Usage:       "Begins a do-once lock",
	Description: lockDoHelpDescription,
	Flags: append(lockCommonFlags(),
		cli.DurationFlag{
			Name:   "lock-wait-timeout",
			Usage:  "Sets a maximum duration to wait for a lock before giving up",
			EnvVar: "BUILDKITE_LOCK_WAIT_TIMEOUT",
		},
	),
	Action: lockDoAction,
}
View Source
var LockDoneCommand = cli.Command{
	Name:        "done",
	Usage:       "Completes a do-once lock",
	Description: lockDoneHelpDescription,
	Flags:       lockCommonFlags(),
	Action:      lockDoneAction,
}
View Source
var LockGetCommand = cli.Command{
	Name:        "get",
	Usage:       "Gets a lock value from the agent leader",
	Description: lockGetHelpDescription,
	Flags:       lockCommonFlags(),
	Action:      lockGetAction,
}
View Source
var LockReleaseCommand = cli.Command{
	Name:        "release",
	Usage:       "Releases a previously-acquired lock",
	Description: lockReleaseHelpDescription,
	Flags:       lockCommonFlags(),
	Action:      lockReleaseAction,
}
View Source
var MetaDataExistsCommand = cli.Command{
	Name:        "exists",
	Usage:       "Check to see if the meta data key exists for a build",
	Description: metaDataExistsHelpDescription,
	Flags: slices.Concat(globalFlags(), apiFlags(), []cli.Flag{
		cli.StringFlag{
			Name:   "job",
			Value:  "",
			Usage:  "Which job's build should the meta-data be checked for",
			EnvVar: "BUILDKITE_JOB_ID",
		},
		cli.StringFlag{
			Name:   "build",
			Value:  "",
			Usage:  "Which build should the meta-data be retrieved from. --build will take precedence over --job",
			EnvVar: "BUILDKITE_METADATA_BUILD_ID",
		},
	}),
	Action: func(c *cli.Context) error {
		ctx := context.Background()
		ctx, cfg, l, _, done := setupLoggerAndConfig[MetaDataExistsConfig](ctx, c)
		defer done()
		ctx, span := otel.Tracer("buildkite-agent").Start(ctx, "meta-data-exists")
		defer span.End()

		client := api.NewClient(l, loadAPIClientConfig(cfg, "AgentAccessToken"))

		scope := "job"
		id := cfg.Job

		if cfg.Build != "" {
			scope = "build"
			id = cfg.Build
		}

		r := roko.NewRetrier(
			roko.WithMaxAttempts(10),
			roko.WithStrategy(roko.Constant(5*time.Second)),
		)
		exists, err := roko.DoFunc(ctx, r, func(r *roko.Retrier) (*api.MetaDataExists, error) {
			exists, resp, err := client.ExistsMetaData(ctx, scope, id, cfg.Key)
			if api.BreakOnNonRetryable(r, resp, err) {
				return nil, err
			}
			if err != nil {
				l.Warnf("%s (%s)", err, r)
				return nil, err
			}
			return exists, nil
		})
		if err != nil {
			return fmt.Errorf("failed to see if meta-data exists: %w", err)
		}

		if !exists.Exists {
			return &SilentExitError{code: 100}
		}

		return nil
	},
}
View Source
var MetaDataGetCommand = cli.Command{
	Name:        "get",
	Usage:       "Get data from a build",
	Description: metaDataGetHelpDescription,
	Flags: slices.Concat(globalFlags(), apiFlags(), []cli.Flag{
		cli.StringFlag{
			Name:  "default",
			Value: "",
			Usage: "If the meta-data value doesn't exist return this instead",
		},
		cli.StringFlag{
			Name:   "job",
			Value:  "",
			Usage:  "Which job's build should the meta-data be retrieved from",
			EnvVar: "BUILDKITE_JOB_ID",
		},
		cli.StringFlag{
			Name:   "build",
			Value:  "",
			Usage:  "Which build should the meta-data be retrieved from. --build will take precedence over --job",
			EnvVar: "BUILDKITE_METADATA_BUILD_ID",
		},
	}),
	Action: func(c *cli.Context) error {
		ctx := context.Background()
		ctx, cfg, l, _, done := setupLoggerAndConfig[MetaDataGetConfig](ctx, c)
		defer done()
		ctx, span := otel.Tracer("buildkite-agent").Start(ctx, "meta-data-get")
		defer span.End()

		client := api.NewClient(l, loadAPIClientConfig(cfg, "AgentAccessToken"))

		scope := "job"
		id := cfg.Job

		if cfg.Build != "" {
			scope = "build"
			id = cfg.Build
		}

		r := roko.NewRetrier(
			roko.WithMaxAttempts(10),
			roko.WithStrategy(roko.Constant(5*time.Second)),
		)
		metaData, resp, err := roko.DoFunc2(ctx, r, func(r *roko.Retrier) (*api.MetaData, *api.Response, error) {
			metaData, resp, err := client.GetMetaData(ctx, scope, id, cfg.Key)
			if api.BreakOnNonRetryable(r, resp, err) {
				return nil, resp, err
			}
			if err != nil {
				l.Warnf("%s (%s)", err, r)
				return nil, resp, err
			}
			return metaData, resp, nil
		})
		if err != nil {

			if resp != nil && resp.StatusCode == 404 && c.IsSet("default") {
				l.Warnf(
					"No meta-data value exists with key %q, returning the supplied default %q",
					cfg.Key,
					cfg.Default,
				)
				_, _ = fmt.Fprint(c.App.Writer, cfg.Default)
				return nil
			}

			return fmt.Errorf("failed to get meta-data: %w", err)
		}

		_, err = fmt.Fprint(c.App.Writer, metaData.Value)
		return err
	},
}
View Source
var MetaDataKeysCommand = cli.Command{
	Name:        "keys",
	Usage:       "Lists all meta-data keys that have been previously set",
	Description: metaDataKeysHelpDescription,
	Flags: slices.Concat(globalFlags(), apiFlags(), []cli.Flag{
		cli.StringFlag{
			Name:   "job",
			Value:  "",
			Usage:  "Which job's build should the meta-data be checked for",
			EnvVar: "BUILDKITE_JOB_ID",
		},
		cli.StringFlag{
			Name:   "build",
			Value:  "",
			Usage:  "Which build should the meta-data be retrieved from. --build will take precedence over --job",
			EnvVar: "BUILDKITE_METADATA_BUILD_ID",
		},
	}),
	Action: func(c *cli.Context) error {
		ctx := context.Background()
		ctx, cfg, l, _, done := setupLoggerAndConfig[MetaDataKeysConfig](ctx, c)
		defer done()
		ctx, span := otel.Tracer("buildkite-agent").Start(ctx, "meta-data-keys")
		defer span.End()

		client := api.NewClient(l, loadAPIClientConfig(cfg, "AgentAccessToken"))

		scope := "job"
		id := cfg.Job

		if cfg.Build != "" {
			scope = "build"
			id = cfg.Build
		}

		r := roko.NewRetrier(
			roko.WithMaxAttempts(10),
			roko.WithStrategy(roko.Constant(5*time.Second)),
		)
		keys, err := roko.DoFunc(ctx, r, func(r *roko.Retrier) ([]string, error) {
			keys, resp, err := client.MetaDataKeys(ctx, scope, id)
			if api.BreakOnNonRetryable(r, resp, err) {
				return keys, err
			}
			if err != nil {
				l.Warnf("%s (%s)", err, r)
			}
			return keys, err
		})
		if err != nil {
			return fmt.Errorf("failed to find meta-data keys: %w", err)
		}

		for _, key := range keys {
			_, _ = fmt.Fprintf(c.App.Writer, "%s\n", key)
		}

		return nil
	},
}
View Source
var MetaDataSetCommand = cli.Command{
	Name:        "set",
	Usage:       "Set data on a build",
	Description: metaDataSetHelpDescription,
	Flags: slices.Concat(globalFlags(), apiFlags(), []cli.Flag{
		cli.StringFlag{
			Name:   "job",
			Value:  "",
			Usage:  "Which job's build should the meta-data be set on",
			EnvVar: "BUILDKITE_JOB_ID",
		},
		RedactedVars,
	}),
	Action: func(c *cli.Context) error {
		ctx := context.Background()
		ctx, cfg, l, _, done := setupLoggerAndConfig[MetaDataSetConfig](ctx, c)
		defer done()
		ctx, span := otel.Tracer("buildkite-agent").Start(ctx, "meta-data-set")
		defer span.End()

		if len(c.Args()) < 2 {
			l.Infof("Reading meta-data value from STDIN")

			input, err := io.ReadAll(os.Stdin)
			if err != nil {
				return fmt.Errorf("failed to read from STDIN: %w", err)
			}
			cfg.Value = string(input)
		}

		if strings.TrimSpace(cfg.Key) == "" {
			return errors.New("key cannot be empty, or composed of only whitespace characters")
		}

		if strings.TrimSpace(cfg.Value) == "" {
			return errors.New("value cannot be empty, or composed of only whitespace characters")
		}

		needles, _, err := redact.NeedlesFromEnv(cfg.RedactedVars)
		if err != nil {
			return err
		}
		if redactedValue := redact.String(cfg.Value, needles); redactedValue != cfg.Value {
			l.Warnf("Meta-data value for key %q contained one or more secrets from environment variables that have been redacted. If this is deliberate, pass --redacted-vars='' or a list of patterns that does not match the variable containing the secret", cfg.Key)
			cfg.Value = redactedValue
		}

		client := api.NewClient(l, loadAPIClientConfig(cfg, "AgentAccessToken"))

		metaData := &api.MetaData{
			Key:   cfg.Key,
			Value: cfg.Value,
		}

		if err := roko.NewRetrier(

			roko.WithMaxAttempts(10),
			roko.WithStrategy(roko.ExponentialSubsecond(2*time.Second)),
		).DoWithContext(ctx, func(r *roko.Retrier) error {
			resp, err := client.SetMetaData(ctx, cfg.Job, metaData)
			if api.BreakOnNonRetryable(r, resp, err) {
				return err
			}
			if err != nil {
				l.Warnf("%s (%s)", err, r)
				return err
			}
			return nil
		}); err != nil {
			return fmt.Errorf("failed to set meta-data: %w", err)
		}

		return nil
	},
}
View Source
var OIDCRequestTokenCommand = cli.Command{
	Name:        "request-token",
	Usage:       "Requests and prints an OIDC token from Buildkite with the specified audience,",
	Description: oidcTokenDescription,
	Flags: slices.Concat(globalFlags(), apiFlags(), []cli.Flag{
		cli.StringFlag{
			Name:  "audience",
			Usage: "The audience that will consume the OIDC token. The API will choose a default audience if it is omitted.",
		},
		cli.IntFlag{
			Name:  "lifetime",
			Usage: "The time (in seconds) the OIDC token will be valid for before expiry. Must be a non-negative integer. If the flag is omitted or set to 0, the API will choose a default finite lifetime.",
		},
		cli.StringFlag{
			Name:   "job",
			Usage:  "Buildkite Job Id to claim in the OIDC token",
			EnvVar: "BUILDKITE_JOB_ID",
		},

		cli.StringFlag{
			Name:   "subject-claim",
			Usage:  "An immutable claim to use as the token's subject (e.g. pipeline_id, cluster_id). If omitted, the default compound subject is used.",
			EnvVar: "BUILDKITE_OIDC_TOKEN_SUBJECT_CLAIM",
		},

		cli.StringSliceFlag{
			Name:   "claim",
			Value:  &cli.StringSlice{},
			Usage:  "Claims to add to the OIDC token",
			EnvVar: "BUILDKITE_OIDC_TOKEN_CLAIMS",
		},

		cli.StringSliceFlag{
			Name:   "aws-session-tag",
			Value:  &cli.StringSlice{},
			Usage:  "Add claims as AWS Session Tags",
			EnvVar: "BUILDKITE_OIDC_TOKEN_AWS_SESSION_TAGS",
		},

		cli.BoolFlag{
			Name:   "skip-redaction",
			Usage:  "Skip redacting the OIDC token from the logs. Then, the command will print the token to the Job's logs if called directly (default: false)",
			EnvVar: "BUILDKITE_AGENT_OIDC_REQUEST_TOKEN_SKIP_TOKEN_REDACTION",
		},
		cli.StringFlag{
			Name:  "format",
			Value: "jwt",
			Usage: "The format to output the token in. Supported values are 'jwt' (the default) and 'gcp'. When 'gcp' is specified, the token will be output in a JSON structure compatible with GCP's workload identity federation.",
		},
	}),
	Action: func(c *cli.Context) error {
		ctx := context.Background()
		ctx, cfg, l, _, done := setupLoggerAndConfig[OIDCTokenConfig](ctx, c)
		defer done()
		ctx, span := otel.Tracer("buildkite-agent").Start(ctx, "oidc-request-token")
		defer span.End()

		if cfg.Lifetime < 0 {
			return fmt.Errorf("lifetime %d must be a non-negative integer", cfg.Lifetime)
		}

		if cfg.Format != "jwt" && cfg.Format != "gcp" {
			return fmt.Errorf("format %q is not valid. Supported values are 'jwt' and 'gcp'", cfg.Format)
		}

		client := api.NewClient(l, loadAPIClientConfig(cfg, "AgentAccessToken"))

		r := roko.NewRetrier(
			roko.WithMaxAttempts(maxAttempts),
			roko.WithStrategy(roko.Exponential(backoffSeconds*time.Second, 0)),
		)
		token, err := roko.DoFunc(ctx, r, func(r *roko.Retrier) (*api.OIDCToken, error) {
			req := &api.OIDCTokenRequest{
				Job:            cfg.Job,
				Audience:       cfg.Audience,
				Lifetime:       cfg.Lifetime,
				Claims:         cfg.Claims,
				AWSSessionTags: cfg.AWSSessionTags,
				SubjectClaim:   cfg.SubjectClaim,
			}

			token, resp, err := client.OIDCToken(ctx, req)
			if api.BreakOnNonRetryable(r, resp, err) {
				return nil, err
			}
			if err != nil {
				l.Warnf("%s (%s)", err, r)
			}
			return token, err
		})
		if err != nil {
			if len(cfg.Audience) > 0 {
				l.Errorf("Could not obtain OIDC token for audience %s", cfg.Audience)
			} else {
				l.Errorf("Could not obtain OIDC token for default audience")
			}
			return err
		}

		if !cfg.SkipRedaction {
			jobClient, err := jobapi.NewDefaultClient(ctx)
			if err != nil {
				return fmt.Errorf("failed to create Job API client: %w", err)
			}

			if err := AddToRedactor(ctx, l, jobClient, token.Token); err != nil {
				if cfg.Debug {
					return err
				}
				return errOIDCRedact
			}
		}

		switch cfg.Format {
		case "jwt":
			_, _ = fmt.Fprintln(c.App.Writer, token.Token)

		case "gcp":
			type gcpOIDCTokenResponse struct {
				IDToken   string `json:"id_token"`
				TokenType string `json:"token_type"`
				Version   int    `json:"version"`
				Success   bool   `json:"success"`
			}

			jsonOutput, err := json.Marshal(gcpOIDCTokenResponse{
				IDToken:   token.Token,
				TokenType: "urn:ietf:params:oauth:token-type:jwt",
				Version:   1,
				Success:   true,
			})
			if err != nil {
				return fmt.Errorf("failed to marshal GCP response: %w", err)
			}

			_, _ = fmt.Fprintln(c.App.Writer, string(jsonOutput))

		default:

			return fmt.Errorf("unknown format %q", cfg.Format)
		}

		return nil
	},
}
View Source
var PipelineUploadCommand = cli.Command{
	Name:        "upload",
	Usage:       "Uploads a description of a build pipeline adds it to the currently running build after the current job",
	Description: pipelineUploadHelpDescription,
	Flags: slices.Concat(globalFlags(), apiFlags(), []cli.Flag{
		cli.BoolFlag{
			Name:   "replace",
			Usage:  "Replace the rest of the existing pipeline with the steps uploaded. Jobs that are already running are not removed (default: false)",
			EnvVar: "BUILDKITE_PIPELINE_REPLACE",
		},
		cli.StringFlag{
			Name:   "job",
			Value:  "",
			Usage:  "The job that is making the changes to its build",
			EnvVar: "BUILDKITE_JOB_ID",
		},
		cli.BoolFlag{
			Name:   "dry-run",
			Usage:  "Rather than uploading the pipeline, it will be echoed to stdout (default: false)",
			EnvVar: "BUILDKITE_PIPELINE_UPLOAD_DRY_RUN",
		},
		cli.StringFlag{
			Name:   "format",
			Usage:  "In dry-run mode, specifies the form to output the pipeline in. Must be one of: json,yaml",
			Value:  "json",
			EnvVar: "BUILDKITE_PIPELINE_UPLOAD_DRY_RUN_FORMAT",
		},
		cli.BoolFlag{
			Name:   "no-interpolation",
			Usage:  "Skip variable interpolation into the pipeline prior to upload (default: false)",
			EnvVar: "BUILDKITE_PIPELINE_NO_INTERPOLATION",
		},
		cli.BoolFlag{
			Name:   "reject-secrets",
			Usage:  "When true, fail the pipeline upload early if the pipeline contains secrets (default: false)",
			EnvVar: "BUILDKITE_AGENT_PIPELINE_UPLOAD_REJECT_SECRETS",
		},
		cli.BoolTFlag{
			Name:   "apply-if-changed",
			Usage:  "When enabled, steps containing an ′if_changed′ key are evaluated against the git diff. If the ′if_changed′ glob pattern match no files changed in the build, the step is skipped. Minimum Buildkite Agent version: v3.99 (with --apply-if-changed flag), v3.103.0 (enabled by default) (default: true)",
			EnvVar: "BUILDKITE_AGENT_APPLY_IF_CHANGED,BUILDKITE_AGENT_APPLY_SKIP_IF_UNCHANGED",
		},
		cli.StringFlag{
			Name:   "git-diff-base",
			Usage:  "Provides the base from which to find the git diff when processing ′if_changed′, e.g. origin/main. If not provided, it uses the first valid value of {origin/$BUILDKITE_PULL_REQUEST_BASE_BRANCH, origin/$BUILDKITE_PIPELINE_DEFAULT_BRANCH, origin/main}.",
			EnvVar: "BUILDKITE_GIT_DIFF_BASE",
		},
		cli.BoolFlag{
			Name:   "fetch-diff-base",
			Usage:  "When enabled, the base for computing the git diff will be git-fetched prior to computing the diff (default: false)",
			EnvVar: "BUILDKITE_FETCH_DIFF_BASE",
		},
		cli.StringFlag{
			Name:   "changed-files-path",
			Usage:  "Path to a file containing the list of changed files (newline-separated) to use for ′if_changed′ evaluation. When provided, the agent skips running git commands to determine changed files.",
			EnvVar: "BUILDKITE_CHANGED_FILES_PATH",
		},

		cli.StringFlag{
			Name:   "jwks-file",
			Usage:  "Path to a file containing a JWKS. Passing this flag enables pipeline signing",
			EnvVar: "BUILDKITE_AGENT_JWKS_FILE",
		},
		cli.StringFlag{
			Name:   "jwks-key-id",
			Usage:  "The JWKS key ID to use when signing the pipeline. Required when using a JWKS",
			EnvVar: "BUILDKITE_AGENT_JWKS_KEY_ID",
		},
		cli.StringFlag{
			Name:   "signing-aws-kms-key",
			Usage:  "The AWS KMS key identifier which is used to sign pipelines.",
			EnvVar: "BUILDKITE_AGENT_AWS_KMS_KEY",
		},
		cli.StringFlag{
			Name: "signing-gcp-kms-key",
			Usage: "The GCP KMS key identifier which is used to sign pipelines. " +
				"This should be in the format projects/*/locations/*/keyRings/*/cryptoKeys/*/cryptoKeyVersions/*",
			EnvVar: "BUILDKITE_AGENT_GCP_KMS_KEY",
		},
		cli.BoolFlag{
			Name:   "debug-signing",
			Usage:  "Enable debug logging for pipeline signing. This can potentially leak secrets to the logs as it prints each step in full before signing. Requires debug logging to be enabled (default: false)",
			EnvVar: "BUILDKITE_AGENT_DEBUG_SIGNING",
		},
		RedactedVars,
	}),
	Action: func(c *cli.Context) error {
		ctx := context.Background()
		ctx, cfg, l, _, done := setupLoggerAndConfig[PipelineUploadConfig](ctx, c)
		defer done()
		ctx, span := otel.Tracer("buildkite-agent").Start(ctx, "pipeline-upload")
		defer span.End()

		// Find the pipeline either from STDIN or the non-flag arguments
		type input struct {
			file *os.File
			name string
		}
		var inputs []input

		switch {
		case len(cfg.FilePaths) > 0:
			l.Infof("Reading pipeline configs from %q", cfg.FilePaths)

			for _, fn := range cfg.FilePaths {
				file, err := os.Open(fn)
				if err != nil {
					return fmt.Errorf("failed to read file: %w", err)
				}
				defer func() { _ = file.Close() }()
				inputs = append(inputs, input{file, filepath.Base(fn)})
			}

		case stdin.IsReadable():
			l.Infof("Reading pipeline config from STDIN")

			inputs = []input{{os.Stdin, "(stdin)"}}

		default:
			l.Infof("Searching for pipeline config...")

			paths := []string{
				"buildkite.yml",
				"buildkite.yaml",
				"buildkite.json",
				filepath.FromSlash(".buildkite/pipeline.yml"),
				filepath.FromSlash(".buildkite/pipeline.yaml"),
				filepath.FromSlash(".buildkite/pipeline.json"),
				filepath.FromSlash("buildkite/pipeline.yml"),
				filepath.FromSlash("buildkite/pipeline.yaml"),
				filepath.FromSlash("buildkite/pipeline.json"),
			}

			exists := []string{}
			for _, path := range paths {
				if _, err := os.Stat(path); err == nil {
					exists = append(exists, path)
				}
			}

			if len(exists) > 1 {
				return fmt.Errorf("found multiple configuration files: %s; please keep only 1 configuration file present", strings.Join(exists, ", "))
			}
			if len(exists) == 0 {
				return fmt.Errorf("could not find a default pipeline configuration file; see `buildkite-agent pipeline upload --help` for more information")
			}

			found := exists[0]

			l.Infof("Found config file %q", found)

			file, err := os.Open(found)
			if err != nil {
				return fmt.Errorf("failed to read file %q: %w", found, err)
			}
			defer func() { _ = file.Close() }()
			inputs = []input{{file, filepath.Base(found)}}
		}

		for _, input := range inputs {
			if input.file == os.Stdin {
				continue
			}
			fi, err := input.file.Stat()
			if err != nil {
				return fmt.Errorf("couldn't stat pipeline configuration file %q: %w", input.file.Name(), err)
			}
			if fi.Size() == 0 {
				return fmt.Errorf("pipeline file %q is empty", input.file.Name())
			}
		}

		environ := env.FromSlice(os.Environ())

		if !cfg.NoInterpolation {

			resolveCommit(l, environ)
		}

		dryRunEnc := func(any) error { return nil }
		if cfg.DryRun {
			switch cfg.DryRunFormat {
			case "json":
				enc := json.NewEncoder(c.App.Writer)
				enc.SetIndent("", "  ")
				dryRunEnc = enc.Encode

			case "yaml":
				dryRunEnc = yaml.NewEncoder(c.App.Writer).Encode

			default:
				return fmt.Errorf("unknown output format %q", cfg.DryRunFormat)
			}
		}

		prependOriginIfNonempty := func(key string) string {
			s := os.Getenv(key)
			if s == "" {
				return ""
			}
			return "origin/" + s
		}

		ifChanged := &ifChangedApplicator{
			enabled: cfg.ApplyIfChanged,
			diffBase: cmp.Or(
				cfg.GitDiffBase,
				prependOriginIfNonempty("BUILDKITE_PULL_REQUEST_BASE_BRANCH"),
				prependOriginIfNonempty("BUILDKITE_PIPELINE_DEFAULT_BRANCH"),
				defaultGitDiffBase,
			),
			fetch:            cfg.FetchDiffBase,
			changedFilesPath: cfg.ChangedFilesPath,
		}

		for _, input := range inputs {

			count := 1
			for result, err := range cfg.parseAndInterpolate(ctx, input.name, input.file, environ) {
				if err != nil {
					w := warning.As(err)
					if w == nil {
						return err
					}
					l.Warnf("There were some issues with the pipeline input - pipeline upload will proceed, but might not succeed:\n%v", w)
				}

				if len(cfg.RedactedVars) > 0 {

					err := searchForSecrets(l, &cfg, environ, result, input.name)
					if err != nil {
						return NewExitError(1, err)
					}
				}

				var key signature.Key

				switch {
				case cfg.SigningAWSKMSKey != "":
					awscfg, err := awslib.GetConfigV2(ctx)
					if err != nil {
						return err
					}

					key, err = awssigner.NewKMS(kms.NewFromConfig(awscfg), cfg.SigningAWSKMSKey)
					if err != nil {
						return fmt.Errorf("couldn't create AWS KMS signer: %w", err)
					}

				case cfg.SigningGCPKMSKey != "":

					key, err = gcpsigner.NewKMS(ctx, cfg.SigningGCPKMSKey)
					if err != nil {
						return fmt.Errorf("couldn't create GCP KMS signer: %w", err)
					}

				case cfg.JWKSFile != "":
					key, err = jwkutil.LoadKey(cfg.JWKSFile, cfg.JWKSKeyID)
					if err != nil {
						return fmt.Errorf("couldn't read the signing key file: %w", err)
					}
				}

				if key != nil {
					err := signature.SignSteps(
						ctx,
						result.Steps,
						key,
						os.Getenv("BUILDKITE_REPO"),
						signature.WithEnv(result.Env.ToMap()),
						signature.WithLogger(logger.DeprecatedLogger{Logger: l}),
						signature.WithDebugSigning(cfg.DebugSigning),
					)
					if err != nil {
						return fmt.Errorf("couldn't sign pipeline: %w", err)
					}
				}

				ifChanged.apply(l, result.Steps)

				if err := dryRunEnc(result); err != nil {
					return err
				}

				if cfg.DryRun {
					continue
				}

				if cfg.Job == "" {
					return errors.New("missing job parameter; this is usually set in the environment for a Buildkite job via BUILDKITE_JOB_ID")
				}

				if cfg.AgentAccessToken == "" {
					return errors.New("missing agent-access-token parameter; this is usually set in the environment for a Buildkite job via BUILDKITE_AGENT_ACCESS_TOKEN")
				}

				uploader := &agent.PipelineUploader{
					Client: api.NewClient(l, loadAPIClientConfig(cfg, "AgentAccessToken")),
					JobID:  cfg.Job,
					Change: &api.PipelineChange{
						UUID:     api.NewUUID(),
						Replace:  cfg.Replace,
						Pipeline: result,
					},
					RetrySleepFunc: time.Sleep,
				}
				if err := uploader.Upload(ctx, l); err != nil {
					l.Errorf(err.Error())
					return NewSilentExitError(1)
				}

				l.Infof("Successfully parsed and uploaded pipeline #%d from %q", count, input.name)
				count++
			}
		}

		return nil
	},
}
View Source
var RedactorAddCommand = cli.Command{
	Name:  "add",
	Usage: "Add values to redact from a job's log output",
	Description: `Usage:

    buildkite-agent redactor add [options...] [file-with-content-to-redact]

Description:

This command may be used to parse a file for values to redact from a
running job's log output. If you dynamically fetch secrets during a job,
it is recommended that you use this command to ensure they will be
redacted from subsequent logs. Secrets fetched with the builtin
′secret get′ command do not require the use of this command, they will
be redacted automatically.

Examples:

To redact the verbatim contents of the file 'id_ed25519' from future logs:

    $ buildkite-agent redactor add id_ed25519

To redact the string 'llamasecret' from future logs:

    $ echo llamasecret | buildkite-agent redactor add

Pass a flat JSON object whose keys are unique and whose values are your secrets:

    $ echo '{"db_password":"secret1","api_token":"secret2","ssh_key":"secret3"}' | buildkite-agent redactor add --format json

Or

    $ buildkite-agent redactor add --format json my-secrets.json

JSON does not allow duplicate keys. If you repeat the same key ("key"), the JSON parser keeps only the final entry, so only that single value is added to the redactor:

    $ echo '{"key":"value1","key":"value2","key":"value3"}' | buildkite-agent redactor add --format json`,
	Flags: slices.Concat(globalFlags(), apiFlags(), []cli.Flag{
		cli.StringFlag{
			Name:   "format",
			Usage:  "The format for the input, whose value is either ′json′ or ′none′. ′none′ adds the entire input's content to the redactor, with the exception of leading and trailing space. ′json′ parses the input's content as a JSON object, where each value of each key is added to the redactor.",
			EnvVar: "BUILDKITE_AGENT_REDACT_ADD_FORMAT",
			Value:  FormatStringNone,
		},
	}),
	Action: func(c *cli.Context) error {
		ctx := context.Background()
		ctx, cfg, l, _, done := setupLoggerAndConfig[RedactorAddConfig](ctx, c)
		defer done()

		if !slices.Contains(secretsFormats, cfg.Format) {
			return fmt.Errorf("invalid format: %s, must be one of %q", cfg.Format, secretsFormats)
		}

		fileName := "(stdin)"

		secretsReader := bufio.NewReader(os.Stdin)
		if cfg.File != "" {
			fileName = cfg.File

			secretsFile, err := os.Open(fileName)
			if err != nil {
				return fmt.Errorf("failed to open file %s: %w", fileName, err)
			}
			defer func() { _ = secretsFile.Close() }()

			secretsReader = bufio.NewReader(secretsFile)
		}

		l.Infof("Reading secrets from %s for redaction", fileName)

		secrets, err := ParseSecrets(l, cfg, secretsReader)
		if err != nil {
			if cfg.Debug {
				return err
			}
			return errSecretParse
		}

		client, err := jobapi.NewDefaultClient(ctx)
		if err != nil {
			return fmt.Errorf("failed to create Job API client: %w", err)
		}

		if err := AddToRedactor(ctx, l, client, secrets...); err != nil {
			if cfg.Debug {
				return err
			}
			return errSecretRedact
		}

		return nil
	},
}
View Source
var SecretGetCommand = cli.Command{
	Name:  "get",
	Usage: "Get a list of secrets by their keys and print them to stdout",
	Description: `Usage:

    buildkite-agent secret get [options...] [key1] [key2] ...

Description:

Gets a list of secrets from Buildkite and prints them to stdout. Key names are case
insensitive in this command, and secret values are automatically redacted in the build logs
unless the ′skip-redaction′ flag is used.

If any request for a secret fails, the command will return a non-zero exit code and print
details of all failed secrets.

By default, when fetching a single key, the secret value will be printed without any
formatting. When fetching multiple keys, the output will be in JSON format. Output
format can be controlled explicitly with the ′format′ flag.

Examples:

    # Secret keys are case insensitive
    $ buildkite-agent secret get deploy_key
    "..."
    $ buildkite-agent secret get DEPLOY_KEY
    "..."

    # The return value can also be formatted using env (which can be piped
    # into e.g. ′source′, ′declare -x′), or json
    $ buildkite-agent secret get --format env deploy_key github_api_token
    DEPLOY_KEY="..."
    GITHUB_API_TOKEN="..."

    $ buildkite-agent secret get --format json deploy_key github_api_token
    {"deploy_key": "...", "github_api_token": "..."}
`,
	Flags: slices.Concat(globalFlags(), apiFlags(), []cli.Flag{
		cli.StringFlag{
			Name:   "job",
			Usage:  "Which job should should the secret be for",
			EnvVar: "BUILDKITE_JOB_ID",
		},
		cli.StringFlag{
			Name:   "format",
			Usage:  "The output format, either 'default', 'json', or 'env'. When 'default', a single secret will print just the value, while multiple secrets will print JSON. When 'json' or 'env', secrets will be printed as key-value pairs in the requested format",
			Value:  "default",
			EnvVar: "BUILDKITE_AGENT_SECRET_GET_FORMAT",
		},
		cli.BoolFlag{
			Name:   "skip-redaction",
			Usage:  "Skip redacting the retrieved secret from the logs. Then, the command will print the secret to the Job's logs if called directly (default: false)",
			EnvVar: "BUILDKITE_AGENT_SECRET_GET_SKIP_SECRET_REDACTION",
		},
	}),
	Action: func(c *cli.Context) error {
		ctx := context.Background()
		ctx, cfg, l, _, done := setupLoggerAndConfig[SecretGetConfig](ctx, c)
		defer done()
		ctx, span := otel.Tracer("buildkite-agent").Start(ctx, "secret-get")
		defer span.End()
		return secretGet(ctx, cfg, c.App.Writer, l)
	},
}
View Source
var StepCancelCommand = cli.Command{
	Name:        "cancel",
	Usage:       "Cancel all unfinished jobs for a step",
	Description: stepCancelHelpDescription,
	Flags: slices.Concat(globalFlags(), apiFlags(), []cli.Flag{
		cli.StringFlag{
			Name:   "step",
			Value:  "",
			Usage:  "The step to cancel. Can be either its ID (BUILDKITE_STEP_ID) or key (BUILDKITE_STEP_KEY)",
			EnvVar: "BUILDKITE_STEP_ID",
		},
		cli.StringFlag{
			Name:   "build",
			Value:  "",
			Usage:  "The build to look for the step in. Only required when targeting a step using its key (BUILDKITE_STEP_KEY)",
			EnvVar: "BUILDKITE_BUILD_ID",
			Hidden: true,
		},
		cli.BoolFlag{
			Name:   "force",
			Usage:  "Transition unfinished jobs to a canceled state instead of waiting for jobs to finish uploading artifacts (default: false)",
			EnvVar: "BUILDKITE_STEP_CANCEL_FORCE",
		},

		cli.Int64Flag{
			Name:   "force-grace-period-seconds",
			Value:  defaultCancelGracePeriodSecs,
			Usage:  "The number of seconds to wait for agents to finish uploading artifacts before transitioning unfinished jobs to a canceled state. ′--force′ must also be supplied for this to take affect",
			EnvVar: "BUILDKITE_STEP_CANCEL_FORCE_GRACE_PERIOD_SECONDS,BUILDKITE_CANCEL_GRACE_PERIOD",
		},
	}),
	Action: func(c *cli.Context) error {
		ctx, cfg, l, _, done := setupLoggerAndConfig[StepCancelConfig](context.Background(), c)
		defer done()
		ctx, span := otel.Tracer("buildkite-agent").Start(ctx, "step-cancel")
		defer span.End()

		if cfg.ForceGracePeriodSeconds < 0 {
			return fmt.Errorf("the value of ′--force-grace-period-seconds′ must be greater than or equal to 0")
		}

		return cancelStep(ctx, cfg, l)
	},
}
View Source
var StepGetCommand = cli.Command{
	Name:        "get",
	Usage:       "Get the value of an attribute",
	Description: stepGetHelpDescription,
	Flags: slices.Concat(globalFlags(), apiFlags(), []cli.Flag{
		cli.StringFlag{
			Name:   "step",
			Value:  "",
			Usage:  "The step to get. Can be either its ID (BUILDKITE_STEP_ID) or key (BUILDKITE_STEP_KEY)",
			EnvVar: "BUILDKITE_STEP_ID",
		},
		cli.StringFlag{
			Name:   "build",
			Value:  "",
			Usage:  "The build to look for the step in. Only required when targeting a step using its key (BUILDKITE_STEP_KEY)",
			EnvVar: "BUILDKITE_BUILD_ID",
		},
		cli.StringFlag{
			Name:   "format",
			Value:  "",
			Usage:  "The format to output the attribute value in (currently only JSON is supported)",
			EnvVar: "BUILDKITE_STEP_GET_FORMAT",
		},
	}),
	Action: func(c *cli.Context) error {
		ctx, cfg, l, _, done := setupLoggerAndConfig[StepGetConfig](context.Background(), c)
		defer done()
		ctx, span := otel.Tracer("buildkite-agent").Start(ctx, "step-get")
		defer span.End()

		client := api.NewClient(l, loadAPIClientConfig(cfg, "AgentAccessToken"))

		stepExportRequest := &api.StepExportRequest{
			Build:     cfg.Build,
			Attribute: cfg.Attribute,
			Format:    cfg.Format,
		}

		r := roko.NewRetrier(
			roko.WithMaxAttempts(10),
			roko.WithStrategy(roko.Constant(5*time.Second)),
		)
		stepExportResponse, err := roko.DoFunc(ctx, r, func(r *roko.Retrier) (*api.StepExportResponse, error) {
			stepExportResponse, resp, err := client.StepExport(ctx, cfg.StepOrKey, stepExportRequest)
			if api.BreakOnNonRetryable(r, resp, err) {
				return stepExportResponse, err
			}
			if err != nil {
				l.Warnf("%s (%s)", err, r)
			}
			return stepExportResponse, err
		})
		if err != nil {
			return fmt.Errorf("failed to get step: %w", err)
		}

		_, err = fmt.Fprintln(c.App.Writer, stepExportResponse.Output)
		return err
	},
}
View Source
var StepUpdateCommand = cli.Command{
	Name:        "update",
	Usage:       "Change the value of an attribute",
	Description: stepUpdateHelpDescription,
	Flags: slices.Concat(globalFlags(), apiFlags(), []cli.Flag{
		cli.StringFlag{
			Name:   "step",
			Value:  "",
			Usage:  "The step to update. Can be either its ID (BUILDKITE_STEP_ID) or key (BUILDKITE_STEP_KEY)",
			EnvVar: "BUILDKITE_STEP_ID",
		},
		cli.StringFlag{
			Name:   "build",
			Value:  "",
			Usage:  "The build to look for the step in. Only required when targeting a step using its key (BUILDKITE_STEP_KEY)",
			EnvVar: "BUILDKITE_BUILD_ID",
		},
		cli.BoolFlag{
			Name:   "append",
			Usage:  "Append to current attribute instead of replacing it (default: false)",
			EnvVar: "BUILDKITE_STEP_UPDATE_APPEND",
		},
		RedactedVars,
	}),
	Action: func(c *cli.Context) error {
		ctx, cfg, l, _, done := setupLoggerAndConfig[StepUpdateConfig](context.Background(), c)
		defer done()
		ctx, span := otel.Tracer("buildkite-agent").Start(ctx, "step-update")
		defer span.End()

		if len(c.Args()) < 2 {
			l.Infof("Reading value from STDIN")

			input, err := io.ReadAll(os.Stdin)
			if err != nil {
				return fmt.Errorf("failed to read from STDIN: %w", err)
			}
			cfg.Value = string(input)
		}

		client := api.NewClient(l, loadAPIClientConfig(cfg, "AgentAccessToken"))

		needles, _, err := redact.NeedlesFromEnv(cfg.RedactedVars)
		if err != nil {
			return err
		}
		if redactedValue := redact.String(cfg.Value, needles); redactedValue != cfg.Value {
			l.Warnf("New value for step %q attribute %q contained one or more secrets from environment variables that have been redacted. If this is deliberate, pass --redacted-vars='' or a list of patterns that does not match the variable containing the secret", cfg.StepOrKey, cfg.Attribute)
			cfg.Value = redactedValue
		}

		idempotencyUUID := api.NewUUID()

		update := &api.StepUpdate{
			IdempotencyUUID: idempotencyUUID,
			Build:           cfg.Build,
			Attribute:       cfg.Attribute,
			Value:           cfg.Value,
			Append:          cfg.Append,
		}

		if err := roko.NewRetrier(
			roko.WithMaxAttempts(10),
			roko.WithStrategy(roko.Constant(5*time.Second)),
		).DoWithContext(ctx, func(r *roko.Retrier) error {
			resp, err := client.StepUpdate(ctx, cfg.StepOrKey, update)
			if api.BreakOnNonRetryable(r, resp, err) {
				return err
			}
			if err != nil {
				l.Warnf("%s (%s)", err, r)
				return err
			}
			return nil
		}); err != nil {
			return fmt.Errorf("failed to change step: %w", err)
		}

		return nil
	},
}
View Source
var ToolKeygenCommand = cli.Command{
	Name:  "keygen",
	Usage: "Generate a new JWS key pair, used for signing and verifying jobs in Buildkite",
	Description: `Usage:

    buildkite-agent tool keygen [options...]

Description:

This command generates a new JWS key pair, used for signing and verifying jobs
in Buildkite.

The pair is written as a JSON Web Key Set (JWKS) to two files, a private JWKS
file and a public JWKS file. The private JWKS should be used as for signing,
and the public JWKS for verification.

For more information about JWS, see https://tools.ietf.org/html/rfc7515 and
for information about JWKS, see https://tools.ietf.org/html/rfc7517`,
	Flags: append(globalFlags(),
		cli.StringFlag{
			Name:   "alg",
			EnvVar: "BUILDKITE_AGENT_KEYGEN_ALG",
			Usage:  fmt.Sprintf("The JWS signing algorithm to use for the key pair. Defaults to 'EdDSA'. Valid algorithms are: %v", jwkutil.ValidSigningAlgorithms),
		},
		cli.StringFlag{
			Name:   "key-id",
			EnvVar: "BUILDKITE_AGENT_KEYGEN_KEY_ID",
			Usage:  "The ID to use for the keys generated. If none is provided, a random one will be generated",
		},
		cli.StringFlag{
			Name:   "private-jwks-file",
			EnvVar: "BUILDKITE_AGENT_KEYGEN_PRIVATE_JWKS_FILE",
			Usage:  "The filename to write the private key to. Defaults to a name based on the key id in the current directory",
		},
		cli.StringFlag{
			Name:   "public-jwks-file",
			EnvVar: "BUILDKITE_AGENT_KEYGEN_PUBLIC_JWKS_FILE",
			Usage:  "The filename to write the public keyset to. Defaults to a name based on the key id in the current directory",
		},
	),
	Action: func(c *cli.Context) {
		_, cfg, l, _, done := setupLoggerAndConfig[ToolKeygenConfig](context.Background(), c)
		defer done()

		if cfg.Alg == "" {
			cfg.Alg = "EdDSA"
			l.Infof("No algorithm provided, using %s", cfg.Alg)
		}

		if cfg.KeyID == "" {
			cfg.KeyID = petname.Generate(2, "-")
			l.Infof("No key ID provided, using a randomly generated one: %s", cfg.KeyID)
		}

		sigAlg := jwa.SignatureAlgorithm(cfg.Alg)

		if !slices.Contains(jwkutil.ValidSigningAlgorithms, sigAlg) {
			l.Fatalf("Invalid signing algorithm: %s. Valid signing algorithms are: %s", cfg.Alg, jwkutil.ValidSigningAlgorithms)
		}

		priv, pub, err := jwkutil.NewKeyPair(cfg.KeyID, sigAlg)
		if err != nil {
			l.Fatalf("Failed to generate key pair: %v", err)
		}

		if cfg.PrivateJWKSFile == "" {
			cfg.PrivateJWKSFile = fmt.Sprintf("./%s-%s-private.json", cfg.Alg, cfg.KeyID)
		}

		if cfg.PublicJWKSFile == "" {
			cfg.PublicJWKSFile = fmt.Sprintf("./%s-%s-public.json", cfg.Alg, cfg.KeyID)
		}

		l.Infof("Writing private key set to %s...", cfg.PrivateJWKSFile)
		pKey, err := json.Marshal(priv)
		if err != nil {
			l.Fatalf("Failed to marshal private key: %v", err)
		}

		err = writeIfNotExists(cfg.PrivateJWKSFile, pKey)
		if err != nil {
			l.Fatalf("Failed to write private key file: %v", err)
		}

		l.Infof("Writing public key set to %s...", cfg.PublicJWKSFile)
		pubKey, err := json.Marshal(pub)
		if err != nil {
			l.Fatalf("Failed to marshal private key: %v", err)
		}

		err = writeIfNotExists(cfg.PublicJWKSFile, pubKey)
		if err != nil {
			l.Fatalf("Failed to write private key file: %v", err)
		}

		l.Infof("Done! Enjoy your new keys ^_^")
	},
}

TODO: Add docs link when there is one.

View Source
var ToolSignCommand = cli.Command{
	Name:  "sign",
	Usage: "Sign pipeline steps",
	Description: `Usage:

    buildkite-agent tool sign [options...] [pipeline-file]

Description:

This command takes a pipeline in YAML format as input, and annotates the appropriate parts of
the pipeline with signatures. This can then be input into the YAML steps editor in the Buildkite
UI so that the agents running these steps can verify the signatures.

If a token is provided using the ′graphql-token′ flag, the tool will attempt to retrieve the
pipeline definition and repo using the Buildkite GraphQL API. If ′update′ is also set, it will
update the pipeline definition with the signed version using the GraphQL API too.

Examples:

Retrieving the pipeline from the GraphQL API and signing it:

    $ buildkite-agent tool sign \
        --graphql-token <graphql token> \
        --organization-slug <your org slug> \
        --pipeline-slug <slug of the pipeline whose steps you want to sign \
        --jwks-file /path/to/private/key.json \
        --update

Signing a pipeline from a file:

    $ buildkite-agent tool sign pipeline.yml \
        --jwks-file /path/to/private/key.json \
        --repo <repo url for your pipeline>
    # or
    $ cat pipeline.yml | buildkite-agent tool sign \
        --jwks-file /path/to/private/key.json \
        --repo <repo url for your pipeline>`,
	Flags: append(globalFlags(),
		cli.StringFlag{
			Name:   "graphql-token",
			Usage:  "A token for the buildkite graphql API. This will be used to populate the value of the repository URL, and download the pipeline definition. Both ′repo′ and ′pipeline-file′ will be ignored in preference of values from the GraphQL API if the token in provided.",
			EnvVar: "BUILDKITE_GRAPHQL_TOKEN",
		},
		cli.BoolFlag{
			Name:   "update",
			Usage:  "Update the pipeline using the GraphQL API after signing it. This can only be used if ′graphql-token′ is provided (default: false)",
			EnvVar: "BUILDKITE_TOOL_SIGN_UPDATE",
		},
		cli.BoolFlag{
			Name:   "no-confirm",
			Usage:  "Show confirmation prompts before updating the pipeline with the GraphQL API (default: false)",
			EnvVar: "BUILDKITE_TOOL_SIGN_NO_CONFIRM",
		},

		cli.StringFlag{
			Name:   "jwks-file",
			Usage:  "Path to a file containing a JWKS.",
			EnvVar: "BUILDKITE_AGENT_JWKS_FILE",
		},
		cli.StringFlag{
			Name:   "jwks-key-id",
			Usage:  "The JWKS key ID to use when signing the pipeline. If none is provided and the JWKS file contains only one key, that key will be used.",
			EnvVar: "BUILDKITE_AGENT_JWKS_KEY_ID",
		},
		cli.StringFlag{
			Name:   "signing-aws-kms-key",
			Usage:  "The AWS KMS key identifier which is used to sign pipelines.",
			EnvVar: "BUILDKITE_AGENT_AWS_KMS_KEY",
		},
		cli.StringFlag{
			Name: "signing-gcp-kms-key",
			Usage: "The GCP KMS key identifier which is used to sign pipelines. " +
				"This should be in the format projects/*/locations/*/keyRings/*/cryptoKeys/*/cryptoKeyVersions/*",
			EnvVar: "BUILDKITE_AGENT_GCP_KMS_KEY",
		},
		cli.BoolFlag{
			Name:   "debug-signing",
			Usage:  "Enable debug logging for pipeline signing. This can potentially leak secrets to the logs as it prints each step in full before signing. Requires debug logging to be enabled (default: false)",
			EnvVar: "BUILDKITE_AGENT_DEBUG_SIGNING",
		},

		cli.StringFlag{
			Name:   "organization-slug",
			Usage:  "The organization slug. Required to connect to the GraphQL API.",
			EnvVar: "BUILDKITE_ORGANIZATION_SLUG",
		},
		cli.StringFlag{
			Name:   "pipeline-slug",
			Usage:  "The pipeline slug. Required to connect to the GraphQL API.",
			EnvVar: "BUILDKITE_PIPELINE_SLUG",
		},
		cli.StringFlag{
			Name:   "graphql-endpoint",
			Usage:  "The endpoint for the Buildkite GraphQL API. This is only needed if you are using the the graphql-token flag, and is mostly useful for development purposes",
			Value:  bkgql.DefaultEndpoint,
			EnvVar: "BUILDKITE_GRAPHQL_ENDPOINT",
		},

		cli.StringFlag{
			Name:   "repo",
			Usage:  "The URL of the pipeline's repository, which is used in the pipeline signature. If the GraphQL token is provided, this will be ignored.",
			EnvVar: "BUILDKITE_REPO",
		},
	),

	Action: func(c *cli.Context) error {
		ctx, cfg, l, _, done := setupLoggerAndConfig[ToolSignConfig](context.Background(), c)
		defer done()

		var (
			key signature.Key
			err error
		)

		switch {
		case cfg.AWSKMSKeyID != "":

			awscfg, err := awslib.GetConfigV2(ctx)
			if err != nil {
				return err
			}

			key, err = awssigner.NewKMS(kms.NewFromConfig(awscfg), cfg.AWSKMSKeyID)
			if err != nil {
				return fmt.Errorf("couldn't create AWS KMS signer: %w", err)
			}

		case cfg.GCPKMSKeyID != "":

			key, err = gcpsigner.NewKMS(ctx, cfg.GCPKMSKeyID)
			if err != nil {
				return fmt.Errorf("couldn't create GCP KMS signer: %w", err)
			}

		default:
			key, err = jwkutil.LoadKey(cfg.JWKSFile, cfg.JWKSKeyID)
			if err != nil {
				return fmt.Errorf("couldn't read the signing key file: %w", err)
			}

		}

		sign := signWithGraphQL
		if cfg.GraphQLToken == "" {
			sign = signOffline
		}

		err = sign(ctx, c, l, key, &cfg)
		if err != nil {
			return fmt.Errorf("Error signing pipeline: %w", err)
		}

		return nil
	},
}

Functions

func AddToRedactor added in v3.66.0

func AddToRedactor(
	ctx context.Context,
	l logger.Logger,
	client *jobapi.Client,
	secrets ...string,
) error

func CreateLogger

func CreateLogger(cfg any) logger.Logger

func DefaultShell

func DefaultShell() string

func HandleGlobalFlags

func HandleGlobalFlags(ctx context.Context, l logger.Logger, cfg any) (context.Context, func())

func HandleProfileFlag

func HandleProfileFlag(l logger.Logger, cfg any) func()

func ParseSecrets added in v3.66.0

func ParseSecrets(
	l logger.Logger,
	cfg RedactorAddConfig,
	secretsReader io.Reader,
) ([]string, error)

func PrintMessageAndReturnExitCode added in v3.54.0

func PrintMessageAndReturnExitCode(err error) int

PrintMessageAndReturnExitCode prints the error message to stderr, preceded by "buildkite-agent: fatal: " and returns the exit code for the given error. If `err` is a SilentExitError or ExitError, it will return the code from that. Otherwise it will return 0 for nil errors and 1 for all other errors. Also, when `err` is a SilentExitError, it will not print anything to stderr.

func Profile

func Profile(l logger.Logger, mode string) func()

Profile starts a profiling session

func UnsetConfigFromEnvironment

func UnsetConfigFromEnvironment(c *cli.Context) error

Types

type APIConfig added in v3.97.0

type APIConfig struct {
	AgentAccessToken string `cli:"agent-access-token" validate:"required"`
	DebugHTTP        bool   `cli:"debug-http"`
	TraceHTTP        bool   `cli:"trace-http"`
	Endpoint         string `cli:"endpoint" validate:"required"`
	NoHTTP2          bool   `cli:"no-http2"`
}

APIConfig includes API-related shared options for easy inclusion across config structs (via embedding). Subcommands that don't need APIConfig usually do something "trivial" (e.g. acknowledgements) or "special" (e.g. start).

type AcknowledgementsConfig added in v3.45.0

type AcknowledgementsConfig struct{}

type AgentPauseConfig added in v3.96.0

type AgentPauseConfig struct {
	GlobalConfig
	APIConfig

	Note             string `cli:"note"`
	TimeoutInMinutes int    `cli:"timeout-in-minutes"`
}

type AgentResumeConfig added in v3.96.0

type AgentResumeConfig struct {
	GlobalConfig
	APIConfig
}

type AgentStartConfig

type AgentStartConfig struct {
	GlobalConfig

	Config string `cli:"config"`

	Name              string   `cli:"name"`
	Priority          string   `cli:"priority"`
	Spawn             int      `cli:"spawn"`
	SpawnPerCPU       int      `cli:"spawn-per-cpu"`
	SpawnWithPriority bool     `cli:"spawn-with-priority"`
	RedactedVars      []string `cli:"redacted-vars" normalize:"list"`
	CancelSignal      string   `cli:"cancel-signal"`

	SigningJWKSKeyID string `cli:"signing-jwks-key-id"`

	SigningJWKSFile  string `cli:"signing-jwks-file" normalize:"filepath"`
	SigningAWSKMSKey string `cli:"signing-aws-kms-key"`
	SigningGCPKMSKey string `cli:"signing-gcp-kms-key"`
	DebugSigning     bool   `cli:"debug-signing"`

	VerificationJWKSFile        string `cli:"verification-jwks-file" normalize:"filepath"`
	VerificationFailureBehavior string `cli:"verification-failure-behavior"`

	AcquireJob                 string `cli:"acquire-job"`
	DisconnectAfterJob         bool   `cli:"disconnect-after-job"`
	DisconnectAfterIdleTimeout int    `cli:"disconnect-after-idle-timeout"`
	DisconnectAfterUptime      int    `cli:"disconnect-after-uptime"`
	CancelGracePeriod          int    `cli:"cancel-grace-period"`
	SignalGracePeriodSeconds   int    `cli:"signal-grace-period-seconds"`
	ReflectExitStatus          bool   `cli:"reflect-exit-status"`

	EnableJobLogTmpfile bool   `cli:"enable-job-log-tmpfile"`
	JobLogPath          string `cli:"job-log-path" normalize:"filepath"`

	LogFormat            string   `cli:"log-format"`
	WriteJobLogsToStdout bool     `cli:"write-job-logs-to-stdout"`
	DisableWarningsFor   []string `cli:"disable-warnings-for" normalize:"list"`

	BuildPath            string   `cli:"build-path" normalize:"filepath" validate:"required"`
	HooksPath            string   `cli:"hooks-path" normalize:"filepath"`
	AdditionalHooksPaths []string `cli:"additional-hooks-paths" normalize:"list"`
	SocketsPath          string   `cli:"sockets-path" normalize:"filepath"`
	PluginsPath          string   `cli:"plugins-path" normalize:"filepath"`

	Shell           string `cli:"shell"`
	HooksShell      string `cli:"hooks-shell"`
	BootstrapScript string `cli:"bootstrap-script" normalize:"commandpath"`
	NoPTY           bool   `cli:"no-pty"`

	NoANSITimestamps bool `cli:"no-ansi-timestamps"`
	TimestampLines   bool `cli:"timestamp-lines"`

	Queue                     string   `cli:"queue"`
	Tags                      []string `cli:"tags" normalize:"list"`
	TagsFromEC2MetaData       bool     `cli:"tags-from-ec2-meta-data"`
	TagsFromEC2MetaDataPaths  []string `cli:"tags-from-ec2-meta-data-paths" normalize:"list"`
	TagsFromEC2Tags           bool     `cli:"tags-from-ec2-tags"`
	TagsFromECSMetaData       bool     `cli:"tags-from-ecs-meta-data"`
	TagsFromGCPMetaData       bool     `cli:"tags-from-gcp-meta-data"`
	TagsFromGCPMetaDataPaths  []string `cli:"tags-from-gcp-meta-data-paths" normalize:"list"`
	TagsFromGCPLabels         bool     `cli:"tags-from-gcp-labels"`
	TagsFromHost              bool     `cli:"tags-from-host"`
	FailOnMissingTags         bool     `cli:"fail-on-missing-tags"`
	WaitForEC2TagsTimeout     string   `cli:"wait-for-ec2-tags-timeout"`
	WaitForEC2MetaDataTimeout string   `cli:"wait-for-ec2-meta-data-timeout"`
	WaitForECSMetaDataTimeout string   `cli:"wait-for-ecs-meta-data-timeout"`
	WaitForGCPLabelsTimeout   string   `cli:"wait-for-gcp-labels-timeout"`

	GitCheckoutFlags            string   `cli:"git-checkout-flags"`
	GitCloneFlags               string   `cli:"git-clone-flags"`
	GitCloneMirrorFlags         string   `cli:"git-clone-mirror-flags"`
	GitCleanFlags               string   `cli:"git-clean-flags"`
	GitFetchFlags               string   `cli:"git-fetch-flags"`
	GitMirrorsPath              string   `cli:"git-mirrors-path" normalize:"filepath"`
	GitMirrorCheckoutMode       string   `cli:"git-mirror-checkout-mode"`
	GitMirrorsLockTimeout       int      `cli:"git-mirrors-lock-timeout"`
	GitMirrorsSkipUpdate        bool     `cli:"git-mirrors-skip-update"`
	NoGitSubmodules             bool     `cli:"no-git-submodules"`
	GitSubmoduleCloneConfig     []string `cli:"git-submodule-clone-config"`
	SkipCheckout                bool     `cli:"skip-checkout"`
	GitSkipFetchExistingCommits bool     `cli:"git-skip-fetch-existing-commits"`
	CheckoutAttempts            int      `cli:"checkout-attempts"`

	NoSSHKeyscan            bool     `cli:"no-ssh-keyscan"`
	NoCommandEval           bool     `cli:"no-command-eval"`
	NoLocalHooks            bool     `cli:"no-local-hooks"`
	NoPlugins               bool     `cli:"no-plugins"`
	NoPluginValidation      bool     `cli:"no-plugin-validation"`
	PluginsAlwaysCloneFresh bool     `cli:"plugins-always-clone-fresh"`
	NoFeatureReporting      bool     `cli:"no-feature-reporting"`
	AllowedRepositories     []string `cli:"allowed-repositories" normalize:"list"`
	AllowedPlugins          []string `cli:"allowed-plugins" normalize:"list"`

	EnableEnvironmentVariableAllowList bool     `cli:"enable-environment-variable-allowlist"`
	AllowedEnvironmentVariables        []string `cli:"allowed-environment-variables" normalize:"list"`

	HealthCheckAddr string `cli:"health-check-addr"`

	// Datadog statsd metrics config
	MetricsDatadog              bool   `cli:"metrics-datadog"`
	MetricsDatadogHost          string `cli:"metrics-datadog-host"`
	MetricsDatadogDistributions bool   `cli:"metrics-datadog-distributions"`

	// Tracing config
	TracingBackend              string `cli:"tracing-backend"`
	TracingServiceName          string `cli:"tracing-service-name"`
	TracingPropagateTraceparent bool   `cli:"tracing-propagate-traceparent"`

	// Other shared flags
	StrictSingleHooks               bool          `cli:"strict-single-hooks"`
	KubernetesExec                  bool          `cli:"kubernetes-exec"`
	KubernetesContainerStartTimeout time.Duration `cli:"kubernetes-container-start-timeout"`
	TraceContextEncoding            string        `cli:"trace-context-encoding"`
	NoMultipartArtifactUpload       bool          `cli:"no-multipart-artifact-upload"`

	// API + agent behaviour
	PingMode string `cli:"ping-mode"`

	// API config
	DebugHTTP bool   `cli:"debug-http"`
	TraceHTTP bool   `cli:"trace-http"`
	Token     string `cli:"token" validate:"required"`
	Endpoint  string `cli:"endpoint" validate:"required"`
	NoHTTP2   bool   `cli:"no-http2"`
	// Deprecated
	KubernetesLogCollectionGracePeriod time.Duration `cli:"kubernetes-log-collection-grace-period"`
	NoSSHFingerprintVerification       bool          `cli:"no-automatic-ssh-fingerprint-verification" deprecated-and-renamed-to:"NoSSHKeyscan"`
	MetaData                           []string      `cli:"meta-data" deprecated-and-renamed-to:"Tags"`
	MetaDataEC2                        bool          `cli:"meta-data-ec2" deprecated-and-renamed-to:"TagsFromEC2"`
	MetaDataEC2Tags                    bool          `cli:"meta-data-ec2-tags" deprecated-and-renamed-to:"TagsFromEC2Tags"`
	MetaDataGCP                        bool          `cli:"meta-data-gcp" deprecated-and-renamed-to:"TagsFromGCP"`
	TagsFromEC2                        bool          `cli:"tags-from-ec2" deprecated-and-renamed-to:"TagsFromEC2MetaData"`
	TagsFromGCP                        bool          `cli:"tags-from-gcp" deprecated-and-renamed-to:"TagsFromGCPMetaData"`
	DisconnectAfterJobTimeout          int           `cli:"disconnect-after-job-timeout" deprecated:"Use disconnect-after-idle-timeout instead"`
}

func (AgentStartConfig) Features added in v3.38.0

func (asc AgentStartConfig) Features(ctx context.Context) []string

type AgentStopConfig added in v3.93.0

type AgentStopConfig struct {
	GlobalConfig
	APIConfig

	Force bool `cli:"force"`
}

type AnnotateConfig

type AnnotateConfig struct {
	GlobalConfig
	APIConfig

	Body         string   `cli:"arg:0" label:"annotation body"`
	Style        string   `cli:"style"`
	Context      string   `cli:"context"`
	Append       bool     `cli:"append"`
	Priority     int      `cli:"priority"`
	Job          string   `cli:"job" validate:"required"`
	RedactedVars []string `cli:"redacted-vars" normalize:"list"`
	Scope        string   `cli:"scope"`
}

type AnnotationRemoveConfig added in v3.28.1

type AnnotationRemoveConfig struct {
	GlobalConfig
	APIConfig

	Context string `cli:"context" validate:"required"`
	Scope   string `cli:"scope"`
	Job     string `cli:"job" validate:"required"`
}

type ArtifactDownloadConfig

type ArtifactDownloadConfig struct {
	GlobalConfig
	APIConfig

	Query                 string `cli:"arg:0" label:"artifact search query" validate:"required"`
	Destination           string `cli:"arg:1" label:"artifact download path" validate:"required"`
	Step                  string `cli:"step"`
	Build                 string `cli:"build" validate:"required"`
	IncludeRetriedJobs    bool   `cli:"include-retried-jobs"`
	NoS3MultipartDownload bool   `cli:"no-s3-multipart-download"`
}

type ArtifactSearchConfig added in v3.23.0

type ArtifactSearchConfig struct {
	GlobalConfig
	APIConfig

	Query              string `cli:"arg:0" label:"artifact search query" validate:"required"`
	Step               string `cli:"step"`
	Build              string `cli:"build" validate:"required"`
	IncludeRetriedJobs bool   `cli:"include-retried-jobs"`
	AllowEmptyResults  bool   `cli:"allow-empty-results"`
	PrintFormat        string `cli:"format"`
}

type ArtifactShasumConfig

type ArtifactShasumConfig struct {
	GlobalConfig
	APIConfig

	Query              string `cli:"arg:0" label:"artifact search query" validate:"required"`
	Sha256             bool   `cli:"sha256"`
	Step               string `cli:"step"`
	Build              string `cli:"build" validate:"required"`
	IncludeRetriedJobs bool   `cli:"include-retried-jobs"`
}

type ArtifactUploadConfig

type ArtifactUploadConfig struct {
	GlobalConfig
	APIConfig

	UploadPaths string `cli:"arg:0" label:"upload paths" validate:"required"`
	Destination string `cli:"arg:1" label:"destination" env:"BUILDKITE_ARTIFACT_UPLOAD_DESTINATION"`
	Job         string `cli:"job" validate:"required"`
	ContentType string `cli:"content-type"`

	// Uploader flags
	Literal                   bool   `cli:"literal"`
	Delimiter                 string `cli:"delimiter"`
	GlobResolveFollowSymlinks bool   `cli:"glob-resolve-follow-symlinks"`
	UploadSkipSymlinks        bool   `cli:"upload-skip-symlinks"`
	NoMultipartUpload         bool   `cli:"no-multipart-artifact-upload"`

	// deprecated
	FollowSymlinks bool `cli:"follow-symlinks" deprecated-and-renamed-to:"GlobResolveFollowSymlinks"`
}

type BootstrapConfig

type BootstrapConfig struct {
	Command                      string   `cli:"command"`
	JobID                        string   `cli:"job" validate:"required"`
	Repository                   string   `cli:"repository" validate:"required"`
	Commit                       string   `cli:"commit" validate:"required"`
	Branch                       string   `cli:"branch" validate:"required"`
	Tag                          string   `cli:"tag"`
	RefSpec                      string   `cli:"refspec"`
	Plugins                      string   `cli:"plugins"`
	Secrets                      string   `cli:"secrets"`
	PullRequest                  string   `cli:"pullrequest"`
	PullRequestUsingMergeRefspec bool     `cli:"pull-request-using-merge-refspec"`
	GitSubmodules                bool     `cli:"git-submodules"`
	SSHKeyscan                   bool     `cli:"ssh-keyscan"`
	AgentName                    string   `cli:"agent" validate:"required"`
	Queue                        string   `cli:"queue"`
	OrganizationSlug             string   `cli:"organization" validate:"required"`
	PipelineSlug                 string   `cli:"pipeline" validate:"required"`
	PipelineProvider             string   `cli:"pipeline-provider" validate:"required"`
	AutomaticArtifactUploadPaths string   `cli:"artifact-upload-paths"`
	ArtifactUploadDestination    string   `cli:"artifact-upload-destination"`
	CleanCheckout                bool     `cli:"clean-checkout"`
	SkipCheckout                 bool     `cli:"skip-checkout"`
	GitSkipFetchExistingCommits  bool     `cli:"git-skip-fetch-existing-commits"`
	GitCheckoutFlags             string   `cli:"git-checkout-flags"`
	GitCloneFlags                string   `cli:"git-clone-flags"`
	GitFetchFlags                string   `cli:"git-fetch-flags"`
	GitCloneMirrorFlags          string   `cli:"git-clone-mirror-flags"`
	GitCleanFlags                string   `cli:"git-clean-flags"`
	GitMirrorsPath               string   `cli:"git-mirrors-path" normalize:"filepath"`
	GitMirrorCheckoutMode        string   `cli:"git-mirror-checkout-mode"`
	GitMirrorsLockTimeout        int      `cli:"git-mirrors-lock-timeout"`
	GitMirrorsSkipUpdate         bool     `cli:"git-mirrors-skip-update"`
	GitSubmoduleCloneConfig      []string `cli:"git-submodule-clone-config" normalize:"list"`
	BinPath                      string   `cli:"bin-path" normalize:"filepath"`
	BuildPath                    string   `cli:"build-path" normalize:"filepath"`
	HooksPath                    string   `cli:"hooks-path" normalize:"filepath"`
	AdditionalHooksPaths         []string `cli:"additional-hooks-paths" normalize:"list"`
	SocketsPath                  string   `cli:"sockets-path" normalize:"filepath"`
	PluginsPath                  string   `cli:"plugins-path" normalize:"filepath"`
	CommandEval                  bool     `cli:"command-eval"`
	PluginsEnabled               bool     `cli:"plugins-enabled"`
	PluginValidation             bool     `cli:"plugin-validation"`
	PluginsAlwaysCloneFresh      bool     `cli:"plugins-always-clone-fresh"`
	LocalHooksEnabled            bool     `cli:"local-hooks-enabled"`
	StrictSingleHooks            bool     `cli:"strict-single-hooks"`
	PTY                          bool     `cli:"pty"`
	LogLevel                     string   `cli:"log-level"`
	Debug                        bool     `cli:"debug"`
	Shell                        string   `cli:"shell"`
	HooksShell                   string   `cli:"hooks-shell"`
	Experiments                  []string `cli:"experiment" normalize:"list"`
	Phases                       []string `cli:"phases" normalize:"list"`
	Profile                      string   `cli:"profile"`
	CancelSignal                 string   `cli:"cancel-signal"`
	CancelGracePeriod            int      `cli:"cancel-grace-period"`
	SignalGracePeriodSeconds     int      `cli:"signal-grace-period-seconds"`
	RedactedVars                 []string `cli:"redacted-vars" normalize:"list"`
	TracingBackend               string   `cli:"tracing-backend"`
	TracingServiceName           string   `cli:"tracing-service-name"`
	TracingTraceParent           string   `cli:"tracing-traceparent"`
	TracingPropagateTraceparent  bool     `cli:"tracing-propagate-traceparent"`
	TraceContextEncoding         string   `cli:"trace-context-encoding"`
	NoJobAPI                     bool     `cli:"no-job-api"`
	DisableWarningsFor           []string `cli:"disable-warnings-for" normalize:"list"`
	CheckoutAttempts             int      `cli:"checkout-attempts"`
}

type BuildCancelConfig added in v3.84.0

type BuildCancelConfig struct {
	GlobalConfig
	APIConfig

	Build string `cli:"build" validate:"required"`
}

type CacheConfig added in v3.111.0

type CacheConfig struct {
	Ids             []string `cli:"ids"`
	Registry        string   `cli:"registry"`
	BucketURL       string   `cli:"bucket-url"`
	Branch          string   `cli:"branch" validate:"required"`
	Pipeline        string   `cli:"pipeline" validate:"required"`
	Organization    string   `cli:"organization" validate:"required"`
	CacheConfigFile string   `cli:"cache-config-file"`
	Concurrency     int      `cli:"concurrency"`
}

CacheConfig includes cache-related shared options for easy inclusion across cache command config structs (via embedding).

type CacheRestoreConfig added in v3.111.0

type CacheRestoreConfig struct {
	GlobalConfig
	APIConfig
	CacheConfig
}

type CacheSaveConfig added in v3.111.0

type CacheSaveConfig struct {
	GlobalConfig
	APIConfig
	CacheConfig
}

type EnvDumpConfig added in v3.43.0

type EnvDumpConfig struct {
	GlobalConfig

	Format string `cli:"format"`
}

type EnvGetConfig added in v3.45.0

type EnvGetConfig struct {
	GlobalConfig

	Format string `cli:"format"`
}

type EnvSetConfig added in v3.45.0

type EnvSetConfig struct {
	GlobalConfig

	InputFormat  string `cli:"input-format"`
	OutputFormat string `cli:"output-format"`
}

type EnvUnsetConfig added in v3.45.0

type EnvUnsetConfig struct {
	GlobalConfig

	InputFormat  string `cli:"input-format"`
	OutputFormat string `cli:"output-format"`
}

type ExitError added in v3.54.0

type ExitError struct {
	// contains filtered or unexported fields
}

ExitError is used to signal that the command should exit with the exit code in `code`. It also wraps an error, which can be used to provide more context.

func NewExitError added in v3.54.0

func NewExitError(code int, err error) *ExitError

NewExitError returns ExitError with the given code and wrapped error.

func (*ExitError) Code added in v3.54.0

func (e *ExitError) Code() int

Code returns the exit code.

func (*ExitError) Error added in v3.54.0

func (e *ExitError) Error() string

Error prints the message of the wrapped error. It ignores the exit code.

func (*ExitError) Is added in v3.54.0

func (e *ExitError) Is(target error) bool

Is will return true if the target is an ExitError with the same code.

func (*ExitError) Unwrap added in v3.54.0

func (e *ExitError) Unwrap() error

Unwrap returns the wrapped error.

type GitCredentialsHelperConfig added in v3.63.0

type GitCredentialsHelperConfig struct {
	GlobalConfig

	JobID  string `cli:"job-id" validate:"required"`
	Action string `cli:"arg:0"`

	// API config
	// DebugHTTP bool // Not present due to the possibility of leaking code access tokens to logs
	AgentAccessToken string `cli:"agent-access-token" validate:"required"`
	Endpoint         string `cli:"endpoint" validate:"required"`
	NoHTTP2          bool   `cli:"no-http2"`
}

type GlobalConfig added in v3.97.0

type GlobalConfig struct {
	Debug       bool     `cli:"debug"`
	LogLevel    string   `cli:"log-level"`
	NoColor     bool     `cli:"no-color"`
	Experiments []string `cli:"experiment" normalize:"list"`
	Profile     string   `cli:"profile"`
}

GlobalConfig includes very common shared config options for easy inclusion across config structs (via embedding).

type JobUpdateConfig added in v3.118.0

type JobUpdateConfig struct {
	GlobalConfig
	APIConfig

	Attribute    string   `cli:"arg:0" label:"attribute" validate:"required"`
	Value        string   `cli:"arg:1" label:"value"`
	Job          string   `cli:"job" validate:"required"`
	RedactedVars []string `cli:"redacted-vars" normalize:"list"`
}

type KubernetesBootstrapConfig added in v3.98.0

type KubernetesBootstrapConfig struct {
	KubernetesContainerID                int           `cli:"kubernetes-container-id"`
	KubernetesBootstrapConnectionTimeout time.Duration `cli:"kubernetes-bootstrap-connection-timeout"`

	// Global flags for debugging, etc
	LogLevel    string   `cli:"log-level"`
	Debug       bool     `cli:"debug"`
	Experiments []string `cli:"experiment" normalize:"list"`
	Profile     string   `cli:"profile"`
}

type LockAcquireConfig added in v3.46.1

type LockAcquireConfig struct {
	GlobalConfig
	LockCommonConfig

	LockWaitTimeout time.Duration `cli:"lock-wait-timeout"`
}

type LockCommonConfig added in v3.97.0

type LockCommonConfig struct {
	LockScope   string `cli:"lock-scope"`
	SocketsPath string `cli:"sockets-path" normalize:"filepath"`
}

type LockDoConfig added in v3.46.1

type LockDoConfig struct {
	GlobalConfig
	LockCommonConfig

	LockWaitTimeout time.Duration `cli:"lock-wait-timeout"`
}

type LockDoneConfig added in v3.46.1

type LockDoneConfig struct {
	GlobalConfig
	LockCommonConfig
}

type LockGetConfig added in v3.46.1

type LockGetConfig struct {
	GlobalConfig
	LockCommonConfig
}

type LockReleaseConfig added in v3.46.1

type LockReleaseConfig struct {
	GlobalConfig
	LockCommonConfig
}

type MetaDataExistsConfig

type MetaDataExistsConfig struct {
	GlobalConfig
	APIConfig

	Key   string `cli:"arg:0" label:"meta-data key" validate:"required"`
	Job   string `cli:"job"`
	Build string `cli:"build"`
}

type MetaDataGetConfig

type MetaDataGetConfig struct {
	GlobalConfig
	APIConfig

	Key     string `cli:"arg:0" label:"meta-data key" validate:"required"`
	Default string `cli:"default"`
	Job     string `cli:"job"`
	Build   string `cli:"build"`
}

type MetaDataKeysConfig

type MetaDataKeysConfig struct {
	GlobalConfig
	APIConfig

	Job   string `cli:"job"`
	Build string `cli:"build"`
}

type MetaDataSetConfig

type MetaDataSetConfig struct {
	GlobalConfig
	APIConfig

	Key          string   `cli:"arg:0" label:"meta-data key" validate:"required"`
	Value        string   `cli:"arg:1" label:"meta-data value"`
	Job          string   `cli:"job" validate:"required"`
	RedactedVars []string `cli:"redacted-vars" normalize:"list"`
}

type OIDCTokenConfig added in v3.41.0

type OIDCTokenConfig struct {
	GlobalConfig
	APIConfig

	Audience      string `cli:"audience"`
	Lifetime      int    `cli:"lifetime"`
	Job           string `cli:"job"      validate:"required"`
	SkipRedaction bool   `cli:"skip-redaction"`
	Format        string `cli:"format"`
	// TODO: enumerate possible values, perhaps by adding a link to the documentation
	Claims         []string `cli:"claim"           normalize:"list"`
	AWSSessionTags []string `cli:"aws-session-tag" normalize:"list"`
	SubjectClaim   string   `cli:"subject-claim"`
}

type PipelineUploadConfig

type PipelineUploadConfig struct {
	GlobalConfig
	APIConfig

	FilePaths       []string `cli:"arg:*" label:"upload paths"`
	Replace         bool     `cli:"replace"`
	Job             string   `cli:"job"` // required, but not in dry-run mode
	DryRun          bool     `cli:"dry-run"`
	DryRunFormat    string   `cli:"format"`
	NoInterpolation bool     `cli:"no-interpolation"`
	RedactedVars    []string `cli:"redacted-vars" normalize:"list"`
	RejectSecrets   bool     `cli:"reject-secrets"`

	// Used for if_changed processing
	ApplyIfChanged   bool   `cli:"apply-if-changed"`
	GitDiffBase      string `cli:"git-diff-base"`
	FetchDiffBase    bool   `cli:"fetch-diff-base"`
	ChangedFilesPath string `cli:"changed-files-path"`

	// Used for signing
	JWKSFile         string `cli:"jwks-file"`
	JWKSKeyID        string `cli:"jwks-key-id"`
	SigningAWSKMSKey string `cli:"signing-aws-kms-key"`
	SigningGCPKMSKey string `cli:"signing-gcp-kms-key"`
	DebugSigning     bool   `cli:"debug-signing"`
}

type RedactorAddConfig added in v3.66.0

type RedactorAddConfig struct {
	GlobalConfig
	APIConfig

	File   string `cli:"arg:0"`
	Format string `cli:"format"`
}

type SecretGetConfig added in v3.64.0

type SecretGetConfig struct {
	GlobalConfig
	APIConfig

	Keys          []string `cli:"arg:*"`
	Format        string   `cli:"format"`
	Job           string   `cli:"job" validate:"required"`
	SkipRedaction bool     `cli:"skip-redaction"`
}

type SilentExitError added in v3.54.0

type SilentExitError struct {
	// contains filtered or unexported fields
}

SilentExitError instructs ExitCode to not log anything and just exit with status `code`.

func NewSilentExitError added in v3.54.0

func NewSilentExitError(code int) *SilentExitError

NewSilentExitError returns SilentExitError with the given code.

func (*SilentExitError) Code added in v3.54.0

func (e *SilentExitError) Code() int

Code returns the exit code.

func (*SilentExitError) Error added in v3.54.0

func (e *SilentExitError) Error() string

Error prints a message with the exit code. It should not be used as the the purpose of this error is to not print anything.

func (*SilentExitError) Is added in v3.54.0

func (e *SilentExitError) Is(target error) bool

Is will return true if the target is a SilentExitError with the same code.

type StepCancelConfig added in v3.85.0

type StepCancelConfig struct {
	GlobalConfig
	APIConfig

	StepOrKey               string `cli:"step" validate:"required"`
	Force                   bool   `cli:"force"`
	ForceGracePeriodSeconds int64  `cli:"force-grace-period-seconds"`
	Build                   string `cli:"build"`
}

type StepGetConfig

type StepGetConfig struct {
	GlobalConfig
	APIConfig

	Attribute string `cli:"arg:0" label:"step attribute"`
	StepOrKey string `cli:"step" validate:"required"`
	Build     string `cli:"build"`
	Format    string `cli:"format"`
}

type StepUpdateConfig

type StepUpdateConfig struct {
	GlobalConfig
	APIConfig

	Attribute    string   `cli:"arg:0" label:"attribute" validate:"required"`
	Value        string   `cli:"arg:1" label:"value"`
	Append       bool     `cli:"append"`
	StepOrKey    string   `cli:"step" validate:"required"`
	Build        string   `cli:"build"`
	RedactedVars []string `cli:"redacted-vars" normalize:"list"`
}

type ToolKeygenConfig added in v3.58.0

type ToolKeygenConfig struct {
	GlobalConfig

	Alg             string `cli:"alg"`
	KeyID           string `cli:"key-id"`
	PrivateJWKSFile string `cli:"private-jwks-file" normalize:"filepath"`
	PublicJWKSFile  string `cli:"public-jwks-file" normalize:"filepath"`
}

type ToolSignConfig added in v3.58.0

type ToolSignConfig struct {
	GlobalConfig

	PipelineFile string `cli:"arg:0" label:"pipeline file"`

	// These change the behaviour
	GraphQLToken string `cli:"graphql-token"`
	Update       bool   `cli:"update"`
	NoConfirm    bool   `cli:"no-confirm"`

	// Used for signing
	JWKSFile  string `cli:"jwks-file"`
	JWKSKeyID string `cli:"jwks-key-id"`

	// AWS KMS key used for signing pipelines
	AWSKMSKeyID string `cli:"signing-aws-kms-key"`

	// GCP KMS key used for signing pipelines
	GCPKMSKeyID string `cli:"signing-gcp-kms-key"`

	// Enable debug logging for pipeline signing, this depends on debug logging also being enabled
	DebugSigning bool `cli:"debug-signing"`

	// Needed for to use GraphQL API
	OrganizationSlug string `cli:"organization-slug"`
	PipelineSlug     string `cli:"pipeline-slug"`
	GraphQLEndpoint  string `cli:"graphql-endpoint"`

	// Added to signature
	Repository string `cli:"repo"`
}

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL