Skip to content

Preview: test selection flags in plan/run#449

Merged
pda merged 4 commits intomainfrom
preview-test-selection
Feb 26, 2026
Merged

Preview: test selection flags in plan/run#449
pda merged 4 commits intomainfrom
preview-test-selection

Conversation

@pda
Copy link
Member

@pda pda commented Feb 24, 2026

New flags in the plan and run subcommands available when BKTEC_PREVIEW_SELECTION is enabled via environment:

--selection-strategy
--selection-param
--metadata

These are sent to the server, the effect of which is currently undefined behaviour. A subsequent release and product announcement will clarify.

@pda pda requested a review from a team as a code owner February 24, 2026 06:33
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cb692f9e96

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@pda pda force-pushed the preview-test-selection branch 2 times, most recently from 051b10f to d0869a9 Compare February 25, 2026 01:47
New flags in the `plan` and `run` subcommands available when
BKTEC_PREVIEW_SELECTION is enabled via environment:

    --selection-strategy
    --selection-param
    --metadata

These are sent to the server, the effect of which is currently undefined
behaviour.
@pda pda force-pushed the preview-test-selection branch from d0869a9 to 6d22176 Compare February 25, 2026 01:51
README.md Outdated
Comment on lines 68 to 69
export BUILDKITE_TEST_ENGINE_SELECTION_PARAMS='{"percent":"40"}'
export BUILDKITE_TEST_ENGINE_METADATA='{"commit_message":"fix flaky tests","git_diff":"..."}'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to hold back the documentation while we continue to iterate on this feature? The percent strategy hasn't yet been implemented.

I think there is enough words to indicate that the API is unstable, subject to change and no one should be using it in their critical paths.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I was unsure, so biased towards transparency.

Comment on lines +225 to +235
var selectionParamFlag = &cli.StringSliceFlag{
Name: "selection-param",
Category: "PREVIEW: TEST SELECTION",
Usage: "Additional selection strategy parameter key=value sent to the test plan API. Repeat for multiple entries.",
}

var metadataFlag = &cli.StringSliceFlag{
Name: "metadata",
Category: "PREVIEW: TEST SELECTION",
Usage: "Additional metadata key=value sent to the test plan API. Repeat for multiple entries.",
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sources is deliberately omitted because the env vars are represented as JSON objects not key=value pairs.

cli.go Outdated
accessTokenFlag,
suiteSlugFlag,
}
flags = append(flags, previewSelectionFlags()...)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason the selection flags are inserted between suiteSlugFlag and baseURLFlag?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No good reason that I'm aware of. I'm going to move it to the end so that there's one slice literal and one append().

Name: "run",
Usage: "Run tests",
Action: run,
DisableSliceFlagSeparator: true,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Commas are treated as literal characters.

e.g. --metadata git_diff="foo,bar" should produce one entry with value "foo,bar", not two entries.

Category: "PREVIEW: TEST SELECTION",
Usage: "Additional metadata key=value sent to the test plan API. Repeat for multiple entries.",
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar question why this new category of flags is in the middle of another category (i.e. between base-url and suite-slug)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No strong reason, other than that they probably do belong in this section:

// Test Engine specific flags

… but they could be added to the end of that section, bumped down one to be after baseURLFlag, although arguably that flag is special because it's Hidden: true so maybe it belongs last? 🤷🏼‍♂️

Usually I like to alphabetically sort things where the order doesn't otherwise matter, to avoid these conundrums 😁

cli.go Outdated
Comment on lines 65 to 71
func validateSelectionConfig(strategy string, params map[string]string) error {
if strategy == "" && len(params) > 0 {
return fmt.Errorf("invalid selection params: selection strategy must be set when selection params are provided")
}

return nil
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we move this validation to where all the other validation is occurring, to the validate function on Config?

cli.go Outdated
return nil
}

selectionParams, err := buildStringMap(
Copy link
Contributor

@nprizal nprizal Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we do the parsing in the flag definition instead?

var selectionParamFlag = &cli.StringSliceFlag{
	Name:     "selection-param",
	Category: "PREVIEW: TEST SELECTION",
	Usage:    "Additional selection strategy parameter key=value sent to the test plan API. Repeat for multiple entries.",
	Sources:	cli.EnvVars("BUILDKITE_TEST_ENGINE_SELECTION_PARAMS"),
	Action: func(ctx context.Context, cmd *cli.Command, v []string) error {
        if !previewSelectionEnabled() {
             return nil
        }

        if !cmd.IsSet("selection-strategy") {
            return fmt.Errorf("invalid selection params: selection strategy must be set when selection params are provided")
        }
		// parse the value, and set it in the config
		return nil
	},
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sonnet 4.6 reckons that would have some issues re. handling a single JSON value from env as being equivalent to multiple args.

It sounds legit to me.

LLM output

The suggestion is partially feasible but has a notable limitation.**

The Action callback on a flag fires when the flag is set — but selectionParamFlag uses Sources: cli.EnvVars(...), and the env var (BUILDKITE_TEST_ENGINE_SELECTION_PARAMS) supplies JSON, not repeated key=value entries. The current code handles these two sources differently:

  • --selection-param key=value → parsed by parseKeyValueEntries
  • BUILDKITE_TEST_ENGINE_SELECTION_PARAMS={"key":"val"} → parsed by parseStringMapJSON

The Action callback on the flag only receives the slice of string values from the flag itself (merged with env var entries as raw strings). If the env var contains JSON, cli/v3 would pass the raw JSON string as one entry in v, and the key=value parser would reject it.

The current design intentionally separates the two: buildStringMap calls parseStringMapJSON on the env var separately and parseKeyValueEntries on the flag values.

What the suggestion could work for: the --selection-param flag-only path — parsing key=value entries and setting cfg.SelectionParams within Action. But you'd still need to handle the JSON env var elsewhere, and the validation (cmd.IsSet("selection-strategy")) in Action would fire before applyPlanRequestContext even runs, which is actually fine for ordering.

The bigger issue: the Action approach would split the logic — flag parsing in the flag definition, env var JSON parsing still in applyPlanRequestContext — making it harder to follow. The current approach keeps all of that in one place with clear separation of concerns.

Summary for your colleague: The suggestion has merit for the key=value flag parsing, but because the env var uses a different format (JSON object vs repeated key=value), moving parsing into Action would require keeping the JSON env var handling in applyPlanRequestContext anyway, splitting the logic rather than simplifying it. The current centralised approach in applyPlanRequestContext is cleaner.

I wonder if we could just not accept JSON 🤔
But it's hard to e.g. split on commas or newlines when there can be commas in the actual input data.
You end up needing some kind of parser that handles escaping etc.

Copy link
Contributor

@nprizal nprizal Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess the question then is whether we want multiple args/JSONs? Can't we just add all params in one arg/JSON? It looks like we are merging the JSONs anyway.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For CLI economics, I greatly prefer this:

bktec \
  --selection-strategy demo \
  --selection-param hello=world \
  --selection-param foo=bar \
  --metadata diffstat="$(git diff --stat)"

Over this:

bktec \
  --selection-strategy demo \
  --selection-params '{"hello": "world", "foo": "bar"}' \
  --metadata "{\"diffstat\": \"$(git diff --stat)\"}"

And IMO CLI ergonomics is important!

Copy link
Contributor

@nprizal nprizal Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aaah! so we are accepting 2 formats. JSON from env vars BUILDKITE_TEST_ENGINE_SELECTION_PARAMS= '{ "percent": 40 }' and key=value format for cli flag --selection-param percent=40.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it will be nice if the format is consistent between env var and cli flag.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just wish urfav/cli had a better out-of-the-box way to do multi-arg from env!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice if the format is consistent between env var and cli flag

I agree!

Maybe we just rip out ENV support for these flags for now, and come back to it later.

I really want to support non-JSON CLI args more than I want to be able to pass them via ENV.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've removed json-from-env support entirely, for now.

}
}

func applyPlanRequestContext(cmd *cli.Command) error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if this function and other new ones can be moved to another file? Perhaps inside internal/config?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's coupled to github.com/urfave/cli/v3 which is currently only imported in package main (e.g. cli.go) and is specific to CLI arg parsing.

So I think we'd only move it if we created a new CLI-arg-parsing internal package. For now, the separate cli.go in package main is the equivalent of that.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, applyPlanRequestContext should probably stay in this file however there is an option to shift the parsing functions to an internal parsing package (or config/selection.go?) or leave it here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm going to rip out the JSON-from-env stuff entirely, it's awkward and I don't like it 😅

)

func previewSelectionEnabled() bool {
switch strings.ToLower(strings.TrimSpace(os.Getenv(previewSelectionEnvVar))) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] Maybe we can make this a hidden flag? The cli library will handle the bool flag so we don't to manually parse it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wasn't sure if we could use a CLI flag to drive which CLI flags were available… it seemed chicken-and-egg but I didn't go deep on whether it's possible. What do you think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we do the validation inside the flag action, I think we can do this. But happy to leave it for now.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I'll leave it as is… we'll rip it out soon anyway when it's no longer in preview.

Comment on lines +118 to +120
if key == "" {
return nil, fmt.Errorf("%s %q has empty key", fieldName, entry)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we also ensure values are non-empty too?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't think of a strong reason to disallow empty values.
Unix env vars work this way, e.g:

$ USER= env | grep ^USER
USER=

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good point. Do we want to ignore keys (and not include them in result) where the value is empty?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you can think of a reason to do that :)
Otherwise I'll just allow empty values for now. Maybe they'll be useful? 🤷🏼‍♂️

Unix-ish ENV doesn't disallow them… they comes through as an empty string, which is distinct from unsetting them.

$ ruby -e 'p ENV["HOME"]'
"/Users/pda"

$ env HOME= ruby -e 'p ENV["HOME"]'
""

$ env -u HOME ruby -e 'p ENV["HOME"]'
nil

pda added 2 commits February 25, 2026 15:07
The implementation is messy, and the UX inconsistency is unpleasant.
We'll come up with a better way in future, for now just the CLI flags is
enough.
@pda pda requested a review from a team February 25, 2026 05:46
Copy link
Contributor

@gchan gchan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm, no major blockers. Does it look good to you @nprizal ?

@pda pda merged commit 325e41e into main Feb 26, 2026
1 check passed
@pda pda deleted the preview-test-selection branch February 26, 2026 02:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants