Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions lib/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ This directory contains vendored packages from `github.com/sourcegraph/sourcegra
## Source

- **Repository**: https://github.com/sourcegraph/sourcegraph
- **Commit**: bdc2f4bb8b59f78f4fa8868b2690b673b41948d4
- **Date**: 2026-06-01 07:34:50 +0100
- **Commit**: de6bd07264df18d8dc66e2aebeaad18ac504390f
- **Date**: 2026-06-11 13:59:53 +0000

## Updating

Expand Down
200 changes: 184 additions & 16 deletions lib/batches/batch_spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
"fmt"
"strings"

"github.com/sourcegraph/sourcegraph/lib/batches/env"

Check failure on line 8 in lib/batches/batch_spec.go

View workflow job for this annotation

GitHub Actions / scip-go

no required module provides package github.com/sourcegraph/sourcegraph/lib/batches/env; to add it:

Check failure on line 8 in lib/batches/batch_spec.go

View workflow job for this annotation

GitHub Actions / go-test (ubuntu-latest)

no required module provides package github.com/sourcegraph/sourcegraph/lib/batches/env; to add it:

Check failure on line 8 in lib/batches/batch_spec.go

View workflow job for this annotation

GitHub Actions / go-test (ubuntu-latest)

no required module provides package github.com/sourcegraph/sourcegraph/lib/batches/env; to add it:

Check failure on line 8 in lib/batches/batch_spec.go

View workflow job for this annotation

GitHub Actions / go-test (ubuntu-latest)

no required module provides package github.com/sourcegraph/sourcegraph/lib/batches/env; to add it:

Check failure on line 8 in lib/batches/batch_spec.go

View workflow job for this annotation

GitHub Actions / go-test (macos-latest)

no required module provides package github.com/sourcegraph/sourcegraph/lib/batches/env; to add it:

Check failure on line 8 in lib/batches/batch_spec.go

View workflow job for this annotation

GitHub Actions / go-test (macos-latest)

no required module provides package github.com/sourcegraph/sourcegraph/lib/batches/env; to add it:

Check failure on line 8 in lib/batches/batch_spec.go

View workflow job for this annotation

GitHub Actions / go-test (macos-latest)

no required module provides package github.com/sourcegraph/sourcegraph/lib/batches/env; to add it:
"github.com/sourcegraph/sourcegraph/lib/batches/overridable"
"github.com/sourcegraph/sourcegraph/lib/batches/schema"
"github.com/sourcegraph/sourcegraph/lib/batches/template"
"github.com/sourcegraph/sourcegraph/lib/batches/yaml"

Check failure on line 12 in lib/batches/batch_spec.go

View workflow job for this annotation

GitHub Actions / scip-go

no required module provides package github.com/sourcegraph/sourcegraph/lib/batches/yaml; to add it:

Check failure on line 12 in lib/batches/batch_spec.go

View workflow job for this annotation

GitHub Actions / go-test (ubuntu-latest)

no required module provides package github.com/sourcegraph/sourcegraph/lib/batches/yaml; to add it:

Check failure on line 12 in lib/batches/batch_spec.go

View workflow job for this annotation

GitHub Actions / go-test (ubuntu-latest)

no required module provides package github.com/sourcegraph/sourcegraph/lib/batches/yaml; to add it:

Check failure on line 12 in lib/batches/batch_spec.go

View workflow job for this annotation

GitHub Actions / go-test (macos-latest)

no required module provides package github.com/sourcegraph/sourcegraph/lib/batches/yaml; to add it:

Check failure on line 12 in lib/batches/batch_spec.go

View workflow job for this annotation

GitHub Actions / go-test (macos-latest)

no required module provides package github.com/sourcegraph/sourcegraph/lib/batches/yaml; to add it:
"github.com/sourcegraph/sourcegraph/lib/errors"
)

Expand All @@ -35,11 +35,39 @@
Description string `json:"description,omitempty" yaml:"description"`
On []OnQueryOrRepository `json:"on,omitempty" yaml:"on"`
Workspaces []WorkspaceConfiguration `json:"workspaces,omitempty" yaml:"workspaces"`
Checkout *CheckoutOptions `json:"checkout,omitempty" yaml:"checkout,omitempty"`
Steps []Step `json:"steps,omitempty" yaml:"steps"`
TransformChanges *TransformChanges `json:"transformChanges,omitempty" yaml:"transformChanges,omitempty"`
ImportChangesets []ImportChangeset `json:"importChangesets,omitempty" yaml:"importChangesets"`
ChangesetTemplate *ChangesetTemplate `json:"changesetTemplate,omitempty" yaml:"changesetTemplate"`
ChangesetHooks *ChangesetHooks `json:"changesetHooks,omitempty" yaml:"hooks,omitempty"`
ChangesetHooks *ChangesetHooks `json:"changesetHooks,omitempty" yaml:"changesetHooks,omitempty"`
}

// DefaultCheckoutFetchDepth is the git fetch depth used for workspace checkouts
// when the spec does not configure checkout.fetchDepth. It fetches only the
// target commit (a shallow clone), which is sufficient for most batch changes.
const DefaultCheckoutFetchDepth = 1

// CheckoutOptions controls how repositories are checked out for workspace
// execution. Only allowed when Version is 3.
type CheckoutOptions struct {
// FetchDepth controls how many commits of git history are fetched into the
// workspace checkout. A value of 0 fetches the full history; any positive
// value fetches that many commits. When nil, DefaultCheckoutFetchDepth is
// used. History is required by tasks that inspect git history (e.g.
// "update codeowners based on git history") or changeset hooks such as
// onMergeConflict.
FetchDepth *int `json:"fetchDepth,omitempty" yaml:"fetchDepth,omitempty"`
}

// CheckoutFetchDepth returns the configured git fetch depth for workspace
// checkouts, applying DefaultCheckoutFetchDepth when unset. A return value of 0
// means full history.
func (s *BatchSpec) CheckoutFetchDepth() int {
if s.Checkout == nil || s.Checkout.FetchDepth == nil {
return DefaultCheckoutFetchDepth
}
return *s.Checkout.FetchDepth
}

// Hooks declares side-effect actions to run at well-defined changeset
Expand All @@ -54,16 +82,76 @@
// Hook actions reuse the Step shape from the top-level steps block.
type ChangesetHookAction struct {
Steps []Step `json:"steps,omitempty" yaml:"steps,omitempty"`
// Commit configures the follow-up commits this hook produces. Its message
// and author are resolved independently: when the message is empty a
// per-event default is used, and when the author is nil the
// changesetTemplate's author is inherited (which itself falls back to the
// changeset's author). It may be nil if the hook does not configure a
// commit at all.
Commit *ExpandedGitCommitDescription `json:"commit,omitempty" yaml:"commit,omitempty"`
}

// HasCommit reports whether the hook action declares its own commit
// information (a message and/or an author).
func (a ChangesetHookAction) HasCommit() bool {
return a.Commit != nil && (a.Commit.Message != "" || a.Commit.Author != nil)
}

// DefaultCommitMessage returns the commit message used for follow-up commits
// produced by this hook event when the hook action does not provide its own
// message. The returned value may contain changeset template variables (e.g.
// ${{ repository.branch }}) that are rendered when the follow-up commit is
// built.
func (e ChangesetHookEvent) DefaultCommitMessage() string {
switch e {
case ChangesetHookEventOnMergeConflict:
return "Fix for merge conflict on ${{ repository.branch }}"
case ChangesetHookEventOnCIFailure:
return "Fix for CI failure on ${{ repository.branch }}"
default:
return "Changeset hook fix on ${{ repository.branch }}"
}
}

// ActionForEvent returns the hook action configured for the given event, and
// whether the event is known.
func (h *ChangesetHooks) ActionForEvent(event ChangesetHookEvent) (ChangesetHookAction, bool) {
switch event {
case ChangesetHookEventOnCIFailure:
return h.OnCIFailure, true
case ChangesetHookEventOnMergeConflict:
return h.OnMergeConflict, true
default:
return ChangesetHookAction{}, false
}
}

type changesetHookEvent string
type ChangesetHookEvent string

// Hook event names. Kept here so callers don't pass typoed strings.
const (
ChangesetHookEventOnCIFailure changesetHookEvent = "onCIFailure"
ChangesetHookEventOnMergeConflict changesetHookEvent = "onMergeConflict"
ChangesetHookEventOnCIFailure ChangesetHookEvent = "onCIFailure"
ChangesetHookEventOnMergeConflict ChangesetHookEvent = "onMergeConflict"
)

// AllChangesetHookEvents is the canonical list of supported changeset hook
// events. Add new events here so callers (validation, filtering, etc.) stay in
// sync from a single source of truth.
var AllChangesetHookEvents = []ChangesetHookEvent{
ChangesetHookEventOnCIFailure,
ChangesetHookEventOnMergeConflict,
}

// Valid reports whether e is a known changeset hook event.
func (e ChangesetHookEvent) Valid() bool {
for _, known := range AllChangesetHookEvents {
if e == known {
return true
}
}
return false
}

type ChangesetTemplate struct {
Title string `json:"title,omitempty" yaml:"title"`
Body string `json:"body,omitempty" yaml:"body"`
Expand Down Expand Up @@ -127,16 +215,47 @@
If any `json:"if,omitempty" yaml:"if,omitempty"`
}

type CodingAgentType string

const (
CodingAgentTypeCodex CodingAgentType = "codex"
CodingAgentTypeClaudeCode CodingAgentType = "claude-code"
)

type CodingAgentStep struct {
Type string `json:"type,omitempty" yaml:"type"`
Prompt string `json:"prompt,omitempty" yaml:"prompt"`
Type CodingAgentType `json:"type,omitempty" yaml:"type"`
Prompt string `json:"prompt,omitempty" yaml:"prompt"`
}

type BuildImageStep struct {
Run string `json:"run" yaml:"run"`
BaseImage string `json:"baseImage" yaml:"baseImage"`
}

// KanikoImage is the container image used to build new OCI images from a base
// image and a run script (see buildImage steps). It is referenced both when
// desugaring buildImage steps into run steps and by the executor when deciding
// whether a step is a buildImage-derived build container. Can be exchanged for
// other image build tooling, as long as the build script is updated as well.
//
// Kaniko builds run unprivileged under docker's default seccomp/apparmor
// profiles, so no extra docker flags are required for this container.
//
// The -alpine variant is required: the desugared buildImage step runs as an
// ordinary `run` step, so the runner (batch-exec/src-cli) probes the image by
// running `<shell> -c mktemp` and overrides the entrypoint with /bin/sh. The
// plain kaniko image has no shell at all, and the -debug variant is built FROM
// scratch without a /tmp directory, so its mktemp fails and the probe rejects
// the image. -alpine has /bin/sh, /tmp, and the busybox wget/base64 tools used
// by the generated build script. It also presets KANIKO_PRE_CLEANUP=1 and
// KANIKO_CLEANUP=1, which wipe the container filesystem around the build (the
// generated script must not rely on external binaries after invoking kaniko).
//
// Pinned to a digest so the build is reproducible and not subject to the tag
// being re-pushed; the tag is retained for readability. When bumping, update
// both the tag and the @sha256 digest (the multi-arch index digest).
const KanikoImage = "ghcr.io/osscontainertools/kaniko:v1.27.6-alpine@sha256:795a358f6c22a9fcd66bb7e14bd97728155e1c171ca951f3c3ba6501054234ce"

// MarshalJSON canonicalizes the v3 `image:` field into `container:` on the
// wire. Both fields exist on Step for ergonomic reasons (v3 specs use
// `image:`, v1/v2 specs use `container:`), but src-cli's Step has only
Expand Down Expand Up @@ -240,6 +359,12 @@
errs = errors.Append(errs, NewValidationError(errors.New("changesetTemplate.published is not supported in batch spec version 3; drive publication via the publish_changesets tool instead")))
}

// v3 specs do not support importChangesets — batch change agents only
// manage changesets they own.
if spec.Version == 3 && len(spec.ImportChangesets) != 0 {
errs = errors.Append(errs, NewValidationError(errors.New("importChangesets is not supported in batch spec version 3")))
}

for i, step := range spec.Steps {
for _, mount := range step.Mount {
if strings.Contains(mount.Path, invalidMountCharacters) {
Expand All @@ -255,20 +380,39 @@
if step.BuildImage != nil && step.Run != "" {
errs = errors.Append(errs, NewValidationError(errors.Newf("step %d: buildImage and run cannot be combined in the same step", i+1)))
}
for name := range step.Files {
if strings.Contains(name, invalidMountCharacters) {
errs = errors.Append(errs, NewValidationError(errors.Newf("step %d files target path contains invalid characters", i+1)))
}
}
}

if hookErr := validateHooks(&spec); hookErr != nil {
errs = errors.Append(errs, hookErr)
}

if checkoutErr := validateCheckout(&spec); checkoutErr != nil {
errs = errors.Append(errs, checkoutErr)
}

return &spec, errs
}

// validateCheckout performs Go-level validation of spec.Checkout beyond what
// the JSON schema enforces. The schema already gates `checkout:` on `version: 3`
// and constrains `fetchDepth` to a non-negative integer. We re-check the version
// invariant here so non-schema callers (and any future schema drift) still fail
// safely.
func validateCheckout(spec *BatchSpec) error {
if spec.Checkout == nil {
return nil
}

var errs error
if spec.Version != 3 {
errs = errors.Append(errs, NewValidationError(errors.New("batch spec checkout requires version: 3")))
}
if spec.Checkout.FetchDepth != nil && *spec.Checkout.FetchDepth < 0 {
errs = errors.Append(errs, NewValidationError(errors.New("checkout.fetchDepth must be greater than or equal to 0")))
}
return errs
}

// validateHooks performs Go-level validation of spec.Hooks beyond what the
// JSON schema enforces. The schema already gates `hooks:` on `version: 3` and
// rejects unknown event names. We re-check the version invariant here so
Expand All @@ -285,8 +429,30 @@
errs = errors.Append(errs, NewValidationError(errors.New("batch spec hooks require version: 3")))
}

validate := func(event changesetHookEvent, action ChangesetHookAction) {
validate := func(event ChangesetHookEvent, action ChangesetHookAction) {
for i, step := range action.Steps {
// Hook steps use the v3 step shape, which requires an image and
// exactly one of a run command or a codingAgent. The JSON schema
// also enforces this (see the version 3 conditional in
// batch_spec.schema.json), so for spec strings parsed through the
// schema these checks are a defense-in-depth backstop; we keep them
// here so non-schema callers (and any future schema drift) still
// fail safely.
if step.Image == "" {
errs = errors.Append(errs, NewValidationError(errors.Newf(
"hooks.%s step %d must specify an image", event, i+1,
)))
}
if step.Run == "" && step.CodingAgent == nil {
errs = errors.Append(errs, NewValidationError(errors.Newf(
"hooks.%s step %d must specify either run or codingAgent", event, i+1,
)))
}
if step.CodingAgent != nil && step.Run != "" {
errs = errors.Append(errs, NewValidationError(errors.Newf(
"hooks.%s step %d: codingAgent and run cannot be combined in the same step", event, i+1,
)))
}
for _, mount := range step.Mount {
if strings.Contains(mount.Path, invalidMountCharacters) {
errs = errors.Append(errs, NewValidationError(errors.Newf(
Expand Down Expand Up @@ -370,12 +536,14 @@
return skipped, nil
}

// RequiredEnvVars inspects all steps for outer environment variables used and
// compiles a deduplicated list from those.
func (s *BatchSpec) RequiredEnvVars() []string {
// RequiredEnvVarsForSteps inspects the given steps for outer environment
// variables used and compiles a deduplicated list from those. Callers pass the
// specific steps they care about (e.g. a spec's top-level steps or its
// changeset hook steps).
func RequiredEnvVarsForSteps(steps []Step) []string {
requiredMap := map[string]struct{}{}
required := []string{}
for _, step := range s.Steps {
for _, step := range steps {
for _, v := range step.Env.OuterVars() {
if _, ok := requiredMap[v]; !ok {
requiredMap[v] = struct{}{}
Expand Down
Loading
Loading