conditional

package module
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Jun 7, 2026 License: MIT Imports: 11 Imported by: 0

README

Buildkite Conditional Evaluator

Build status License

A Go library for validating and evaluating Buildkite conditional expressions with the same server-side syntax and semantics documented in Using conditionals.

Parity target

conditional is intended to answer the same yes/no question that Buildkite answers when it evaluates a pipeline if attribute or notification condition. The public API models the Buildkite server inputs through Context, then parses, validates, and evaluates the expression against the documented conditional language.

Any divergence from Buildkite's server-side conditional behavior should be treated as a bug. The library does not parse pipeline YAML or run the full pipeline upload process; callers provide the conditional expression and the Buildkite values that would be available at the selected entrypoint.

Supported syntax

  • Comparators: == != =~ !~
  • Logical operators: || &&
  • Ternary conditionals: condition ? when_true : when_false
  • Parentheses to control grouping: ( )
  • Literals: integers, strings, booleans, null, arrays, and regular expressions
  • Buildkite identifiers such as build.branch
  • Function calls such as env("FOO") and build.env("FOO"); dotted function names are parsed as flat function names
  • Prefix negation: !
  • Array membership: ["foo", "bar"] includes "foo"
  • Shell-style environment substitution in operands and double-quoted strings
  • // comments
Syntax examples
// individual terms
true
false
null
12345
"foobar"
'foobar'
["master", "staging"]

// compare values
build.branch == "master"
build.tag != "v1.0.0"
"blah" == 'blah'

// function calls
env("FOO") == "BAR"
build.env("BUILDKITE_BRANCH") == build.branch

// regular expression matches
build.tag =~ /^v/
build.message !~ /\[skip tests\]/i

// logical and ternary expressions
(build.tag =~ /^v/) || (build.branch == "main")
build.pull_request.id == null ? build.branch == "main" : true

// array operations
["master", "staging"] includes build.branch
build.creator.teams includes "deploy"

// shell-style substitutions
$branch == "main"
${branch:-main} == "main"
"deploy-${branch}" == "deploy-main"

// comments
build.branch == "main" // release branch

The evaluator parses the expression as the Buildkite server sees it. When an expression is embedded in pipeline YAML, upload-time interpolation may run before server-side conditional parsing, so escape $ where the upload phase should leave a literal dollar in the conditional. Inside conditional syntax, shell-style substitutions such as $branch and ${branch:-main} are evaluated against the Buildkite environment, while regex escapes such as \$ keep their regular-expression meaning.

Entrypoints

Set Context.EntryPoint to the Buildkite location where the conditional runs:

  • EntryPointBuildCondition evaluates build conditionals without step.*. This is also the default when Context.EntryPoint is empty.
  • EntryPointBuildConditionWithStep evaluates build conditionals where step variables are available.
  • EntryPointBuildNotification evaluates build notification conditionals. Evaluate converts parse, validation, and evaluation errors to false.
  • EntryPointStepNotification evaluates step notification conditionals with step.* variables. Evaluate converts parse, validation, and evaluation errors to false.

Variables

The root API builds flat Buildkite assignments from Context, matching the server's conditional assignment table:

  • build.* values come from Context.Build.
  • pipeline.* values come from documented Context.Pipeline fields: id, slug, default_branch, repository, started_passing, started_failing, and next_finished_build_exists.
  • organization.* values come from Context.Organization.
  • step.* values come from Context.Step only for step-aware entrypoints.

Missing documented nullable values evaluate as null. Unknown variables, unknown functions, invalid regular expressions, and server-unsupported regular expression features fail validation or parsing. Type mismatches, evaluation errors, and non-boolean final results fail closed.

Environment

Context.ProjectEnv and Context.BuildEnv provide caller-supplied environment. Matching Build::PipelineEnvironment, project environment is applied first, build environment overrides it, and built-in Buildkite values derived from Context override both.

  • env("NAME") reads the merged environment and returns a string. Missing values return "".
  • build.env("NAME") reads the same merged environment. Missing values return null; present empty values return "".
  • Shell substitutions read the same merged environment. An unset standalone substitution evaluates to null, and substitutions inside double-quoted strings follow the server's shell-style expansion rules.
  • Literal BUILDKITE_* names passed to env() or build.env() are validated against the server's static supported environment allowlist.
  • Dynamic BUILDKITE_* names are validated at runtime. This matches server behavior for values such as BUILDKITE_PULL_REQUEST_LABELS, which is runtime-derived but not accepted as a literal static env() or build.env() argument.

Validate always reports parse and validation errors. Evaluate reports errors for build condition entrypoints, while notification entrypoints return false for parse, validation, and evaluation errors. Blank notification conditionals evaluate to true, matching Buildkite notification deliverability.

Returned errors are *conditional.Error values with a stable Kind. Parse errors also unwrap to the underlying parser errors, so callers can inspect the cause with errors.Unwrap.

Extensions

Validate and Evaluate accept variadic options. With no options, the library keeps the Buildkite server-parity contract and rejects unknown functions. Callers can opt into additional functions for their own expression surface. Context remains the per-call Buildkite state; options configure evaluator capabilities.

Use per-call options when a custom function is only needed in one place:

startsWith := conditional.WithFunction("starts_with", conditional.Function{
	Args:   []conditional.ValueType{conditional.StringType, conditional.StringType},
	Return: conditional.BoolType,
	Eval: func(args []conditional.Value) (conditional.Value, error) {
		value, _ := args[0].AsString()
		prefix, _ := args[1].AsString()
		return conditional.BoolValue(strings.HasPrefix(value, prefix)), nil
	},
})

ok, err := conditional.Evaluate(
	`starts_with(build.branch, "release/")`,
	ctx,
	startsWith,
)

Use NewEvaluator when the same options should be reused across many validations or evaluations:

evaluator, err := conditional.NewEvaluator(startsWith)
if err != nil {
	log.Fatal(err)
}

ok, err := evaluator.Evaluate(
	`starts_with(build.branch, "release/")`,
	ctx,
)

NewEvaluator validates options once. The zero value Evaluator has no custom functions and behaves like the package-level Buildkite-parity helpers.

Custom functions are type-checked before evaluation. Function arguments and return values use the exported Value and ValueType APIs, so callers do not depend on internal evaluator objects. The build, env, organization, pipeline, and step roots are reserved for Buildkite values and built-in functions.

Usage

Evaluate a build conditional:

branch := "main"
message := "ship it"

ok, err := conditional.Evaluate(
	`build.branch == "main" && build.message !~ /\[skip tests\]/i`,
	conditional.Context{
		EntryPoint: conditional.EntryPointBuildCondition,
		Build: conditional.Build{
			Branch:  &branch,
			Message: &message,
		},
	},
)
if err != nil {
	log.Fatal(err)
}

log.Printf("should run: %t", ok)

Validate a conditional before storing it:

err := conditional.Validate(
	`build.env("DEPLOY_ENV") == "production" && ${branch:-main} == "main"`,
	conditional.Context{EntryPoint: conditional.EntryPointBuildCondition},
)
if err != nil {
	log.Fatal(err)
}

Evaluate with Buildkite and custom environment values:

branch := "main"

ok, err := conditional.Evaluate(
	`build.env("DEPLOY_ENV") == "production" && ${branch:-main} == "main"`,
	conditional.Context{
		EntryPoint: conditional.EntryPointBuildCondition,
		Build: conditional.Build{
			Branch: &branch,
		},
		ProjectEnv: map[string]string{
			"DEPLOY_ENV": "staging",
		},
		BuildEnv: map[string]string{
			"DEPLOY_ENV": "production",
			"branch":     "main",
		},
	},
)
if err != nil {
	log.Fatal(err)
}

log.Printf("should deploy: %t", ok)

Evaluate a step notification conditional:

outcome := "passed"

deliver, err := conditional.Evaluate(
	`step.outcome == "passed"`,
	conditional.Context{
		EntryPoint: conditional.EntryPointStepNotification,
		Step: &conditional.Step{
			Outcome: &outcome,
		},
	},
)
if err != nil {
	log.Fatal(err)
}

log.Printf("should notify: %t", deliver)

For notification entrypoints, Evaluate returns false instead of surfacing parse, validation, or evaluation errors, matching Buildkite notification deliverability.

Full example:

package main

import (
	"log"

	"github.com/buildkite/conditional"
)

func main() {
	message := "llamas rock, and so do alpacas"

	ok, err := conditional.Evaluate(`build.message =~ /^llamas rock/`, conditional.Context{
		EntryPoint: conditional.EntryPointBuildCondition,
		Build: conditional.Build{
			Message: &message,
		},
	})
	if err != nil {
		log.Fatal(err)
	}

	log.Printf("Result: %#v", ok)
}

Testing

Run the full local verification suite with:

mise run check

The local Go tests include source-tagged parity cases from the Buildkite docs and upstream buildkite/buildkite specs. There is no live server comparison in the default test path.

Design

The root package is the public Buildkite API:

  • conditional.Validate checks an expression for a Buildkite context.
  • conditional.Evaluate evaluates an expression and returns a boolean result.
  • conditional.Context defines the Buildkite entry point and available values.

The internal lexer, parser, and evaluator packages are derived from Writing an Interpreter in Go.

Documentation

Overview

Package conditional validates and evaluates Buildkite conditional expressions.

The public API is the root package. Use Context to provide Buildkite values, set Context.EntryPoint to the place where the conditional runs, then call Validate or Evaluate. Optional variadic options can register caller-owned functions without changing default Buildkite server-parity behavior. Use NewEvaluator to reuse options across multiple validations or evaluations.

Validate always returns parse and validation errors. Evaluate returns errors for build condition entrypoints. Notification entrypoints model Buildkite notification delivery, so Evaluate converts parse, validation, and evaluation errors to false for those entrypoints.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func Evaluate

func Evaluate(expression string, ctx Context, opts ...Option) (bool, error)

Evaluate evaluates expression in the selected Buildkite context.

func IsErrorKind

func IsErrorKind(err error, kind ErrorKind) bool

IsErrorKind reports whether err contains a conditional Error with kind.

func Validate

func Validate(expression string, ctx Context, opts ...Option) error

Validate parses expression for the selected Buildkite context.

Types

type Actor

type Actor struct {
	ID       *string
	Name     *string
	Email    *string
	Teams    []string
	Verified *bool
}

Actor contains server-resolved author or creator values. Email should contain the value exposed through the server's build.*.email assignments, including organization-preferred creator email resolution when applicable.

type Build

type Build struct {
	ID           *string
	State        *string
	Fixed        *bool
	BlockedState *string
	Source       *string
	SourceEvent  *string
	SourceAction *string
	Branch       *string
	Tag          *string
	Message      *string
	Commit       *string
	Number       *int

	Creator       Actor
	Author        Actor
	SCM           SCM
	PullRequest   PullRequest
	MergeQueue    MergeQueue
	TriggeredFrom TriggeredFrom
	RebuiltFrom   RebuiltFrom
}

Build contains build values exposed to conditionals.

type Context

type Context struct {
	EntryPoint EntryPoint

	Build        Build
	Pipeline     Pipeline
	Organization Organization
	Step         *Step

	// BuildEnv is build-scoped environment. ProjectEnv is pipeline/project
	// environment. Matching Build::PipelineEnvironment, ProjectEnv is applied
	// first, then BuildEnv overrides it.
	BuildEnv   map[string]string
	ProjectEnv map[string]string
}

Context contains the Buildkite values available to a conditional.

type EntryPoint

type EntryPoint string

EntryPoint identifies the server path that is evaluating a conditional.

const (
	// EntryPointBuildCondition evaluates a Build::Condition without a step.
	EntryPointBuildCondition EntryPoint = "build_condition"
	// EntryPointBuildConditionWithStep evaluates a Build::Condition with a step.
	EntryPointBuildConditionWithStep EntryPoint = "build_condition_with_step"
	// EntryPointBuildNotification evaluates build notification deliverability.
	EntryPointBuildNotification EntryPoint = "build_notification"
	// EntryPointStepNotification evaluates step notification deliverability.
	EntryPointStepNotification EntryPoint = "step_notification"
)

type Error

type Error struct {
	Kind    ErrorKind
	Message string
	Cause   error
}

Error is a typed conditional error. Cause contains a lower-level error when one is useful to expose through Unwrap.

func (*Error) Error

func (e *Error) Error() string

func (*Error) Is

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

Is reports whether err has the same error kind as target.

func (*Error) Unwrap

func (e *Error) Unwrap() error

Unwrap returns the underlying cause, if any.

type ErrorKind

type ErrorKind string

ErrorKind classifies conditional failures without depending on exact server error text.

const (
	// ErrorKindParse indicates that the expression could not be parsed.
	ErrorKindParse ErrorKind = "parse"
	// ErrorKindValidation indicates that validation failed before evaluation.
	ErrorKindValidation ErrorKind = "validation"
	// ErrorKindEvaluation indicates that evaluation failed.
	ErrorKindEvaluation ErrorKind = "evaluation"
	// ErrorKindResult indicates that the expression did not evaluate to a bool.
	ErrorKindResult ErrorKind = "result"
)

type Evaluator

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

Evaluator validates and evaluates conditionals with reusable options.

The zero value is a Buildkite-parity evaluator with no caller-owned functions.

func NewEvaluator

func NewEvaluator(opts ...Option) (Evaluator, error)

NewEvaluator returns an evaluator with reusable options.

Example
package main

import (
	"errors"
	"fmt"
	"strings"

	"github.com/buildkite/conditional"
)

func main() {
	branch := "release/2026-06-07"
	startsWith := conditional.WithFunction("starts_with", conditional.Function{
		Args:   []conditional.ValueType{conditional.StringType, conditional.StringType},
		Return: conditional.BoolType,
		Eval: func(args []conditional.Value) (conditional.Value, error) {
			value, ok := args[0].AsString()
			if !ok {
				return conditional.NullValue(), errors.New("value must be a string")
			}
			prefix, ok := args[1].AsString()
			if !ok {
				return conditional.NullValue(), errors.New("prefix must be a string")
			}
			return conditional.BoolValue(strings.HasPrefix(value, prefix)), nil
		},
	})

	evaluator, err := conditional.NewEvaluator(startsWith)
	if err != nil {
		panic(err)
	}

	ok, err := evaluator.Evaluate(
		`starts_with(build.branch, "release/")`,
		conditional.Context{
			EntryPoint: conditional.EntryPointBuildCondition,
			Build: conditional.Build{
				Branch: &branch,
			},
		},
	)
	if err != nil {
		panic(err)
	}

	fmt.Println(ok)

}
Output:
true

func (Evaluator) Evaluate

func (e Evaluator) Evaluate(expression string, ctx Context) (bool, error)

Evaluate evaluates expression in the selected Buildkite context using the evaluator's options.

func (Evaluator) Validate

func (e Evaluator) Validate(expression string, ctx Context) error

Validate parses expression for the selected Buildkite context using the evaluator's options.

type Function

type Function struct {
	Args   []ValueType
	Return ValueType
	Eval   func(args []Value) (Value, error)
}

Function defines an opt-in conditional function.

type MergeQueue

type MergeQueue struct {
	// Active reports whether this build is a merge queue build. The server uses
	// this state to gate BUILDKITE_GIT_DIFF_BASE independently of the base values.
	Active     bool
	BaseBranch *string
	BaseCommit *string
}

MergeQueue contains merge queue values exposed to conditionals.

type Option

type Option func(*optionSet) error

Option configures conditional validation and evaluation.

func WithFunction

func WithFunction(name string, function Function) Option

WithFunction registers an opt-in conditional function.

Example
package main

import (
	"errors"
	"fmt"
	"strings"

	"github.com/buildkite/conditional"
)

func main() {
	branch := "release/2026-06-07"
	startsWith := conditional.WithFunction("starts_with", conditional.Function{
		Args:   []conditional.ValueType{conditional.StringType, conditional.StringType},
		Return: conditional.BoolType,
		Eval: func(args []conditional.Value) (conditional.Value, error) {
			value, ok := args[0].AsString()
			if !ok {
				return conditional.NullValue(), errors.New("value must be a string")
			}
			prefix, ok := args[1].AsString()
			if !ok {
				return conditional.NullValue(), errors.New("prefix must be a string")
			}
			return conditional.BoolValue(strings.HasPrefix(value, prefix)), nil
		},
	})

	ok, err := conditional.Evaluate(
		`starts_with(build.branch, "release/")`,
		conditional.Context{
			EntryPoint: conditional.EntryPointBuildCondition,
			Build: conditional.Build{
				Branch: &branch,
			},
		},
		startsWith,
	)
	if err != nil {
		panic(err)
	}

	fmt.Println(ok)

}
Output:
true

type Organization

type Organization struct {
	ID   *string
	Slug *string
}

Organization contains organization values exposed to conditionals.

type Pipeline

type Pipeline struct {
	ID                                    *string
	Name                                  *string
	Slug                                  *string
	DefaultBranch                         *string
	Repository                            *string
	StartedPassing                        *bool
	StartedFailing                        *bool
	NextFinishedBuildExists               *bool
	UseMergeQueueBaseCommitForGitDiffBase *bool
}

Pipeline contains pipeline values exposed to conditionals.

type PullRequest

type PullRequest struct {
	ID                *string
	BaseBranch        *string
	Draft             *bool
	Label             *string
	Labels            []string
	Repository        *string
	RepositoryFork    *bool
	UsingMergeRefspec *bool
}

PullRequest contains pull request values exposed to conditionals.

type RebuiltFrom

type RebuiltFrom struct {
	BuildID     *string
	BuildNumber *int
}

RebuiltFrom contains values for the build this build was rebuilt from.

type SCM

type SCM struct {
	AuthorName     *string
	AuthorEmail    *string
	CommitterName  *string
	CommitterEmail *string
}

SCM contains source control author and committer values.

type Step

type Step struct {
	ID      *string
	Key     *string
	Type    *string
	Label   *string
	State   *string
	Outcome *string
}

Step contains step values exposed to step-aware conditionals.

type TriggeredFrom

type TriggeredFrom struct {
	BuildID      *string
	BuildNumber  *int
	PipelineSlug *string
	JobID        *string
}

TriggeredFrom contains values for the build/job that triggered this build.

type Value

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

Value is a conditional runtime value.

The zero value represents null.

func BoolValue

func BoolValue(value bool) Value

BoolValue returns a boolean value.

func NullValue

func NullValue() Value

NullValue returns a null value.

func NumberValue

func NumberValue(value int64) Value

NumberValue returns an integer value.

func RegexpValue

func RegexpValue(pattern string, flags string) (Value, error)

RegexpValue returns a regular expression value.

func StringArrayValue

func StringArrayValue(values []string) Value

StringArrayValue returns a string array value.

func StringValue

func StringValue(value string) Value

StringValue returns a string value.

func (Value) AsBool

func (v Value) AsBool() (bool, bool)

AsBool returns the boolean value, if this value is a boolean.

func (Value) AsNumber

func (v Value) AsNumber() (int64, bool)

AsNumber returns the integer value, if this value is an integer.

func (Value) AsRegexp

func (v Value) AsRegexp() (pattern string, flags string, ok bool)

AsRegexp returns the regular expression pattern and flags, if this value is a regular expression.

func (Value) AsString

func (v Value) AsString() (string, bool)

AsString returns the string value, if this value is a string.

func (Value) AsStringArray

func (v Value) AsStringArray() ([]string, bool)

AsStringArray returns a copy of the array values, if this value is a string array.

func (Value) IsNull

func (v Value) IsNull() bool

IsNull reports whether the value is null.

func (Value) String

func (v Value) String() string

String returns a human-readable value representation.

func (Value) Type

func (v Value) Type() ValueType

Type returns the value's conditional type.

type ValueType

type ValueType string

ValueType describes a conditional value type.

const (
	// StringType is the conditional string type.
	StringType ValueType = "string"
	// NumberType is the conditional integer type.
	NumberType ValueType = "number"
	// BoolType is the conditional boolean type.
	BoolType ValueType = "boolean"
	// NullType is the conditional null type.
	NullType ValueType = "null"
	// RegexpType is the conditional regular expression type.
	RegexpType ValueType = "regular expression"
	// StringArrayType is the conditional string array type.
	StringArrayType ValueType = "string array"
)

func (ValueType) String

func (t ValueType) String() string

String returns a human-readable value type description.

Directories

Path Synopsis
cmd
conditional command
internal
ast

Jump to

Keyboard shortcuts

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