From 446330d23271d1fe6e86e7f78dc395f3900a38d7 Mon Sep 17 00:00:00 2001 From: bahrmichael Date: Mon, 1 Jun 2026 08:58:24 +0200 Subject: [PATCH 1/8] feat: support container variables --- internal/batches/executor/run_steps.go | 29 +++++++++++++++++- internal/batches/executor/run_steps_test.go | 30 ++++++++++++++++++ internal/batches/service/service.go | 13 ++++++-- internal/batches/service/service_test.go | 34 +++++++++++++++++++-- lib/batches/batch_spec.go | 30 ++++++++++++++++++ lib/batches/schema/batch_spec_stringdata.go | 7 ++++- lib/batches/template/partial_eval.go | 20 ++++++++++++ 7 files changed, 157 insertions(+), 6 deletions(-) create mode 100644 internal/batches/executor/run_steps_test.go diff --git a/internal/batches/executor/run_steps.go b/internal/batches/executor/run_steps.go index 47b5715887..67ccbfcb0b 100644 --- a/internal/batches/executor/run_steps.go +++ b/internal/batches/executor/run_steps.go @@ -165,10 +165,16 @@ func RunSteps(ctx context.Context, opts *RunStepsOpts) (stepResults []execution. continue } + resolvedContainer, err := renderStepContainer(step.Container, &stepContext) + if err != nil { + return nil, errors.Wrapf(err, "failed to resolve image for step %d", i+1) + } + step.Container = resolvedContainer + // We need to grab the digest for the exact image we're using. img, err := opts.EnsureImage(ctx, step.Container) if err != nil { - return nil, err + return nil, errors.Wrapf(err, "failed to pull image for step %d: %s", i+1, step.Container) } digest, err := img.Digest(ctx) if err != nil { @@ -241,6 +247,27 @@ func RunSteps(ctx context.Context, opts *RunStepsOpts) (stepResults []execution. return stepResults, err } +func renderStepContainer(container string, stepContext *template.StepContext) (string, error) { + if container == "" { + return "", nil + } + + var out bytes.Buffer + if err := template.RenderStepTemplate("step-container", container, &out, stepContext); err != nil { + return "", err + } + + resolved := out.String() + if strings.TrimSpace(resolved) == "" { + return "", errors.New("empty image") + } + if strings.Contains(resolved, "${{") { + return "", errors.Errorf("unresolved template in image %q", resolved) + } + + return resolved, nil +} + const workDir = "/work" func executeSingleStep( diff --git a/internal/batches/executor/run_steps_test.go b/internal/batches/executor/run_steps_test.go new file mode 100644 index 0000000000..7b8dc6e356 --- /dev/null +++ b/internal/batches/executor/run_steps_test.go @@ -0,0 +1,30 @@ +package executor + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/sourcegraph/sourcegraph/lib/batches/template" +) + +func TestRenderStepContainer(t *testing.T) { + t.Run("static image", func(t *testing.T) { + got, err := renderStepContainer("alpine:3", &template.StepContext{}) + require.NoError(t, err) + require.Equal(t, "alpine:3", got) + }) + + t.Run("output image", func(t *testing.T) { + got, err := renderStepContainer("${{ outputs.imageName }}", &template.StepContext{ + Outputs: map[string]any{"imageName": "alpine:3"}, + }) + require.NoError(t, err) + require.Equal(t, "alpine:3", got) + }) + + t.Run("missing output", func(t *testing.T) { + _, err := renderStepContainer("${{ outputs.imageName }}", &template.StepContext{}) + require.Error(t, err) + }) +} diff --git a/internal/batches/service/service.go b/internal/batches/service/service.go index a21de45903..dc44e31bac 100644 --- a/internal/batches/service/service.go +++ b/internal/batches/service/service.go @@ -295,10 +295,19 @@ func (svc *Service) EnsureDockerImages( parallelism int, progress func(done, total int), ) (map[string]docker.Image, error) { - // Figure out the image names used in the batch spec. + // Figure out the concrete image names used in the batch spec. Images that + // still depend on runtime values, such as outputs from earlier steps, are + // resolved and pulled just-in-time by the executor. names := map[string]struct{}{} for i := range steps { - names[steps[i].Container] = struct{}{} + isStatic, name, err := templatelib.IsStaticString(steps[i].Container, &templatelib.StepContext{}) + if err != nil { + return nil, err + } + if !isStatic { + continue + } + names[name] = struct{}{} } total := len(names) diff --git a/internal/batches/service/service_test.go b/internal/batches/service/service_test.go index bdff83acfc..aa166bf2af 100644 --- a/internal/batches/service/service_test.go +++ b/internal/batches/service/service_test.go @@ -113,8 +113,9 @@ func TestEnsureDockerImages(t *testing.T) { } for name, steps := range map[string][]batcheslib.Step{ - "single step": {{Container: "image"}}, - "multiple steps": {{Container: "image"}, {Container: "image"}}, + "single step": {{Container: "image"}}, + "multiple steps": {{Container: "image"}, {Container: "image"}}, + "dynamic deferred": {{Container: "${{ outputs.imageName }}"}, {Container: "image"}}, } { t.Run(name, func(t *testing.T) { for _, parallelism := range parallelCases { @@ -265,6 +266,35 @@ some-new-field: Foo bar `, expectedErr: errors.New("parsing batch spec: Additional property some-new-field is not allowed"), }, + { + name: "step image alias", + rawSpec: ` +name: test-spec +description: A test spec +steps: + - run: echo hi + image: alpine:3 +changesetTemplate: + title: Test + body: Test + branch: test + commit: + message: Test +`, + expectedSpec: &batcheslib.BatchSpec{ + Name: "test-spec", + Description: "A test spec", + Steps: []batcheslib.Step{ + {Run: "echo hi", Container: "alpine:3", Image: "alpine:3"}, + }, + ChangesetTemplate: &batcheslib.ChangesetTemplate{ + Title: "Test", + Body: "Test", + Branch: "test", + Commit: batcheslib.ExpandedGitCommitDescription{Message: "Test"}, + }, + }, + }, { name: "supported version", rawSpec: ` diff --git a/lib/batches/batch_spec.go b/lib/batches/batch_spec.go index 269093a82d..ecb78304a0 100644 --- a/lib/batches/batch_spec.go +++ b/lib/batches/batch_spec.go @@ -1,6 +1,7 @@ package batches import ( + "encoding/json" "fmt" "strings" @@ -92,6 +93,7 @@ func (oqor *OnQueryOrRepository) GetBranches() ([]string, error) { type Step struct { Run string `json:"run,omitempty" yaml:"run"` Container string `json:"container,omitempty" yaml:"container"` + Image string `json:"image,omitempty" yaml:"image,omitempty"` Env env.Environment `json:"env" yaml:"env"` Files map[string]string `json:"files,omitempty" yaml:"files,omitempty"` Outputs Outputs `json:"outputs,omitempty" yaml:"outputs,omitempty"` @@ -99,6 +101,34 @@ type Step struct { If any `json:"if,omitempty" yaml:"if,omitempty"` } +func (s *Step) normalizeImageAlias() { + if s.Container == "" && s.Image != "" { + s.Container = s.Image + } +} + +func (s *Step) UnmarshalJSON(data []byte) error { + type step Step + var decoded step + if err := json.Unmarshal(data, &decoded); err != nil { + return err + } + *s = Step(decoded) + s.normalizeImageAlias() + return nil +} + +func (s *Step) UnmarshalYAML(unmarshal func(any) error) error { + type step Step + var decoded step + if err := unmarshal(&decoded); err != nil { + return err + } + *s = Step(decoded) + s.normalizeImageAlias() + return nil +} + func (s *Step) IfCondition() string { switch v := s.If.(type) { case bool: diff --git a/lib/batches/schema/batch_spec_stringdata.go b/lib/batches/schema/batch_spec_stringdata.go index 1123970faf..bb3dbd3677 100644 --- a/lib/batches/schema/batch_spec_stringdata.go +++ b/lib/batches/schema/batch_spec_stringdata.go @@ -128,7 +128,7 @@ const BatchSpecJSON = `{ "type": "object", "description": "A command to run (as part of a sequence) in a repository branch to produce the required changes.", "additionalProperties": false, - "required": ["run", "container"], + "required": ["run"], "properties": { "run": { "type": "string", @@ -139,6 +139,11 @@ const BatchSpecJSON = `{ "description": "The Docker image used to launch the Docker container in which the shell command is run.", "examples": ["alpine:3"] }, + "image": { + "type": "string", + "description": "The Docker image used to launch the Docker container in which the shell command is run.", + "examples": ["alpine:3"] + }, "outputs": { "type": ["object", "null"], "description": "Output variables of this step that can be referenced in the changesetTemplate or other steps via outputs.", diff --git a/lib/batches/template/partial_eval.go b/lib/batches/template/partial_eval.go index 905ece71d5..9c223381ed 100644 --- a/lib/batches/template/partial_eval.go +++ b/lib/batches/template/partial_eval.go @@ -42,6 +42,26 @@ func IsStaticBool(input string, ctx *StepContext) (isStatic bool, boolVal bool, return true, isTrueOutput(t.Tree.Root), nil } +// IsStaticString parses the input as a text/template and attempts to evaluate it +// with only the information currently available in StepContext. If any template +// actions remain after partial evaluation, the first return value is false. +func IsStaticString(input string, ctx *StepContext) (isStatic bool, value string, err error) { + t, err := parseAndPartialEval(input, ctx) + if err != nil { + return false, "", err + } + + var out bytes.Buffer + for _, n := range t.Tree.Root.Nodes { + if n.Type() != parse.NodeText { + return false, "", nil + } + out.WriteString(n.String()) + } + + return true, out.String(), nil +} + // parseAndPartialEval parses input as a text/template and then attempts to // partially evaluate the parts of the template it can evaluate ahead of time // (meaning: before we've executed any batch spec steps and have a full From cca7abe9a63b42c4beb3bc933d6189a0254f8347 Mon Sep 17 00:00:00 2001 From: bahrmichael Date: Mon, 1 Jun 2026 09:16:27 +0200 Subject: [PATCH 2/8] chore: update vendored Sourcegraph lib Vendor github.com/sourcegraph/sourcegraph/lib at bdc2f4bb8b59f78f4fa8868b2690b673b41948d4 and restore src-cli compatibility for APIs still used locally. Test Plan: go test ./... Amp-Thread-ID: https://ampcode.com/threads/T-019e8206-7c7d-76cb-9b08-b6e0df454e15 Co-authored-by: Amp --- lib/README.md | 4 +- lib/batches/batch_spec.go | 151 ++++++++++++++++-- lib/batches/changeset_spec.go | 6 - lib/batches/execution/cache/cache.go | 12 -- lib/batches/execution/cache/cache_test.go | 31 ---- lib/batches/overridable/bool.go | 7 - lib/batches/template/partial_eval.go | 3 +- lib/batches/template/template.go | 9 +- lib/batches/template/templating.go | 7 +- lib/codeintel/upload/indexer_name.go | 3 - lib/codeintel/upload/request.go | 25 ++- lib/codeintel/upload/upload.go | 2 - lib/errors/client_error.go | 6 +- lib/errors/cockroach.go | 34 ++-- lib/errors/multi_error.go | 6 +- lib/errors/rich_error.go | 9 +- lib/go.mod | 90 ----------- lib/output/capabilities.go | 8 +- lib/output/emoji.go | 4 +- lib/output/line.go | 5 +- lib/output/output.go | 13 +- lib/output/output_unix.go | 1 - lib/output/pending_tty.go | 6 +- lib/output/progress.go | 8 +- lib/output/progress_simple.go | 8 +- lib/output/progress_tty.go | 8 +- .../progress_with_status_bars_simple.go | 6 +- lib/output/progress_with_status_bars_tty.go | 9 +- lib/output/spinner.go | 3 + lib/output/table.go | 8 +- lib/process/pipe.go | 8 +- 31 files changed, 251 insertions(+), 249 deletions(-) delete mode 100644 lib/batches/execution/cache/cache_test.go diff --git a/lib/README.md b/lib/README.md index 0b478e3cd1..e9bb62031c 100644 --- a/lib/README.md +++ b/lib/README.md @@ -5,8 +5,8 @@ This directory contains vendored packages from `github.com/sourcegraph/sourcegra ## Source - **Repository**: https://github.com/sourcegraph/sourcegraph -- **Commit**: 2ee2b8e77de9663b08ce5f6e5a2c7d2217ce721a -- **Date**: 2025-11-17 19:49:42 -0800 +- **Commit**: bdc2f4bb8b59f78f4fa8868b2690b673b41948d4 +- **Date**: 2026-06-01 07:34:50 +0100 ## Updating diff --git a/lib/batches/batch_spec.go b/lib/batches/batch_spec.go index 269093a82d..30ef50d4a9 100644 --- a/lib/batches/batch_spec.go +++ b/lib/batches/batch_spec.go @@ -1,6 +1,7 @@ package batches import ( + "encoding/json" "fmt" "strings" @@ -38,8 +39,31 @@ type BatchSpec struct { 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"` } +// Hooks declares side-effect actions to run at well-defined changeset +// lifecycle events. Only allowed when Version is 3. +type ChangesetHooks struct { + OnCIFailure ChangesetHookAction `json:"onCIFailure,omitempty" yaml:"onCIFailure,omitempty"` + OnMergeConflict ChangesetHookAction `json:"onMergeConflict,omitempty" yaml:"onMergeConflict,omitempty"` +} + +// HookAction is a single action attached to a changeset lifecycle event. +// +// Hook actions reuse the Step shape from the top-level steps block. +type ChangesetHookAction struct { + Steps []Step `json:"steps,omitempty" yaml:"steps,omitempty"` +} + +type changesetHookEvent string + +// Hook event names. Kept here so callers don't pass typoed strings. +const ( + ChangesetHookEventOnCIFailure changesetHookEvent = "onCIFailure" + ChangesetHookEventOnMergeConflict changesetHookEvent = "onMergeConflict" +) + type ChangesetTemplate struct { Title string `json:"title,omitempty" yaml:"title"` Body string `json:"body,omitempty" yaml:"body"` @@ -90,13 +114,46 @@ func (oqor *OnQueryOrRepository) GetBranches() ([]string, error) { } type Step struct { - Run string `json:"run,omitempty" yaml:"run"` - Container string `json:"container,omitempty" yaml:"container"` - Env env.Environment `json:"env" yaml:"env"` - Files map[string]string `json:"files,omitempty" yaml:"files,omitempty"` - Outputs Outputs `json:"outputs,omitempty" yaml:"outputs,omitempty"` - Mount []Mount `json:"mount,omitempty" yaml:"mount,omitempty"` - If any `json:"if,omitempty" yaml:"if,omitempty"` + Run string `json:"run,omitempty" yaml:"run"` + CodingAgent *CodingAgentStep `json:"codingAgent,omitempty" yaml:"codingAgent,omitempty"` + BuildImage *BuildImageStep `json:"buildImage,omitempty" yaml:"buildImage,omitempty"` + Container string `json:"container,omitempty" yaml:"container"` + Image string `json:"image,omitempty" yaml:"image"` + MaxAttempts int `json:"maxAttempts,omitempty" yaml:"maxAttempts,omitempty"` + Env env.Environment `json:"env" yaml:"env"` + Files map[string]string `json:"files,omitempty" yaml:"files,omitempty"` + Outputs Outputs `json:"outputs,omitempty" yaml:"outputs,omitempty"` + Mount []Mount `json:"mount,omitempty" yaml:"mount,omitempty"` + If any `json:"if,omitempty" yaml:"if,omitempty"` +} + +type CodingAgentStep struct { + Type string `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"` +} + +// 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 +// `Container`. Without canonicalization, the prep-side cache key — computed +// by JSON-marshaling Step — would include `image` while the executor side +// (which round-trips through src-cli) would not, producing divergent keys +// and silent cache misses for any v3 spec. See the regression test in +// lib/batches/execution/cache. +func (s Step) MarshalJSON() ([]byte, error) { + // Use an alias type to avoid infinite recursion through MarshalJSON. + type stepAlias Step + canon := stepAlias(s) + if canon.Container == "" { + canon.Container = canon.Image + } + canon.Image = "" + return json.Marshal(canon) } func (s *Step) IfCondition() string { @@ -161,33 +218,97 @@ func parseBatchSpec(schema string, data []byte) (*BatchSpec, error) { return nil, err } - var errs error + if spec.Version == 3 { + // Mirror v3 `image:` into `container:` so in-memory consumers that + // read step.Container (e.g. the executor transform) keep working. + // JSON serialization is canonicalized separately in Step.MarshalJSON + // so prep-side cache hashing matches src-cli/executor serialization. + for i := range spec.Steps { + spec.Steps[i].Container = spec.Steps[i].Image + } + } + var errs error if len(spec.Steps) != 0 && spec.ChangesetTemplate == nil { errs = errors.Append(errs, NewValidationError(errors.New("batch spec includes steps but no changesetTemplate"))) } + // v3 specs do not support changesetTemplate.published — publication is + // driven exclusively via the batchchangeagent tools. Reject the field at + // parse time. + if spec.Version == 3 && spec.ChangesetTemplate != nil && spec.ChangesetTemplate.Published != nil { + 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"))) + } + for i, step := range spec.Steps { for _, mount := range step.Mount { - if strings.ContainsAny(mount.Path, invalidMountCharacters) { + if strings.Contains(mount.Path, invalidMountCharacters) { errs = errors.Append(errs, NewValidationError(errors.Newf("step %d mount path contains invalid characters", i+1))) } - if strings.ContainsAny(mount.Mountpoint, invalidMountCharacters) { + if strings.Contains(mount.Mountpoint, invalidMountCharacters) { errs = errors.Append(errs, NewValidationError(errors.Newf("step %d mount mountpoint contains invalid characters", i+1))) } } + if step.CodingAgent != nil && step.Run != "" { + errs = errors.Append(errs, NewValidationError(errors.Newf("step %d: codingAgent and run cannot be combined in the same step", i+1))) + } + 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.ContainsAny(name, invalidMountCharacters) { + 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) + } + return &spec, errs } -// docker uses Golang's `encoding/csv` library to parse arguments passed to `--mount` -const invalidMountCharacters = ",\"\n\r" +// 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 +// non-schema callers (and any future schema drift) still fail safely, and we +// run the per-step mount-character validator that the schema cannot express. +func validateHooks(spec *BatchSpec) error { + if spec.ChangesetHooks == nil { + return nil + } + + var errs error + + if spec.Version != 3 { + errs = errors.Append(errs, NewValidationError(errors.New("batch spec hooks require version: 3"))) + } + + validate := func(event changesetHookEvent, action ChangesetHookAction) { + for i, step := range action.Steps { + for _, mount := range step.Mount { + if strings.Contains(mount.Path, invalidMountCharacters) { + errs = errors.Append(errs, NewValidationError(errors.Newf( + "hooks.%s step %d mount path contains invalid characters", event, i+1, + ))) + } + if strings.Contains(mount.Mountpoint, invalidMountCharacters) { + errs = errors.Append(errs, NewValidationError(errors.Newf( + "hooks.%s step %d mount mountpoint contains invalid characters", event, i+1, + ))) + } + } + } + } + + validate(ChangesetHookEventOnCIFailure, spec.ChangesetHooks.OnCIFailure) + validate(ChangesetHookEventOnMergeConflict, spec.ChangesetHooks.OnMergeConflict) + + return errs +} + +const invalidMountCharacters = "," func (on *OnQueryOrRepository) String() string { if on.RepositoriesMatchingQuery != "" { @@ -212,10 +333,6 @@ func (e BatchSpecValidationError) Error() string { return e.err.Error() } -func IsValidationError(err error) bool { - return errors.HasType[*BatchSpecValidationError](err) -} - // SkippedStepsForRepo calculates the steps required to run on the given repo. func SkippedStepsForRepo(spec *BatchSpec, repoName string, fileMatches []string) (skipped map[int]struct{}, err error) { skipped = map[int]struct{}{} diff --git a/lib/batches/changeset_spec.go b/lib/batches/changeset_spec.go index 4b64f670a1..16e917e311 100644 --- a/lib/batches/changeset_spec.go +++ b/lib/batches/changeset_spec.go @@ -203,12 +203,6 @@ func (d *ChangesetSpec) IsImportingExisting() bool { return d.Type() == ChangesetSpecDescriptionTypeExisting } -// IsBranch returns whether the description is of type -// ChangesetSpecDescriptionTypeBranch. -func (d *ChangesetSpec) IsBranch() bool { - return d.Type() == ChangesetSpecDescriptionTypeBranch -} - // ChangesetSpecDescriptionType tells the consumer what the type of a // ChangesetSpecDescription is without having to look into the description. // Useful in the GraphQL when a HiddenChangesetSpec is returned. diff --git a/lib/batches/execution/cache/cache.go b/lib/batches/execution/cache/cache.go index f77d5ab92d..9392cd2c31 100644 --- a/lib/batches/execution/cache/cache.go +++ b/lib/batches/execution/cache/cache.go @@ -47,15 +47,6 @@ func (key CacheKey) mountsMetadata() ([]MountMetadata, error) { return nil, nil } -// perRunEnvVars resolve to per-job or per-executor values that change on -// every dequeue and must be stripped from cache keys. Mirrored in -// sourcegraph/sourcegraph/lib/batches/execution/cache/cache.go. -var perRunEnvVars = []string{ - "SRC_EXECUTOR_JOB_TOKEN", - "SRC_EXECUTOR_JOB_ID", - "SRC_EXECUTOR_NAME", -} - // resolveStepsEnvironment returns a slice of environments for each of the steps, // containing only the env vars that are actually used. func resolveStepsEnvironment(globalEnv []string, steps []batches.Step) ([]map[string]string, error) { @@ -73,9 +64,6 @@ func resolveStepsEnvironment(globalEnv []string, steps []batches.Step) ([]map[st if err != nil { return nil, errors.Wrapf(err, "resolving environment for step %d", i) } - for _, name := range perRunEnvVars { - delete(env, name) - } envs[i] = env } return envs, nil diff --git a/lib/batches/execution/cache/cache_test.go b/lib/batches/execution/cache/cache_test.go deleted file mode 100644 index c416fe024f..0000000000 --- a/lib/batches/execution/cache/cache_test.go +++ /dev/null @@ -1,31 +0,0 @@ -package cache - -import ( - "encoding/json" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/sourcegraph/sourcegraph/lib/batches" - "github.com/sourcegraph/sourcegraph/lib/batches/env" -) - -func TestKeyer_Key_PerRunEnvVarsIgnored(t *testing.T) { - var stepEnv env.Environment - require.NoError(t, json.Unmarshal( - []byte(`["SRC_EXECUTOR_JOB_TOKEN", "SRC_EXECUTOR_JOB_ID", "SRC_EXECUTOR_NAME"]`), - &stepEnv, - )) - step := batches.Step{Run: "foo", Env: stepEnv} - repo := batches.Repository{ID: "r", Name: "r"} - - unset, err := (&CacheKey{Repository: repo, Steps: []batches.Step{step}, StepIndex: 0}).Key() - require.NoError(t, err) - resolved, err := (&CacheKey{Repository: repo, Steps: []batches.Step{step}, StepIndex: 0, GlobalEnv: []string{ - "SRC_EXECUTOR_JOB_TOKEN=tok", - "SRC_EXECUTOR_JOB_ID=42", - "SRC_EXECUTOR_NAME=executor-abc", - }}).Key() - require.NoError(t, err) - require.Equal(t, unset, resolved) -} diff --git a/lib/batches/overridable/bool.go b/lib/batches/overridable/bool.go index 411e466071..8d90d6a073 100644 --- a/lib/batches/overridable/bool.go +++ b/lib/batches/overridable/bool.go @@ -7,13 +7,6 @@ type Bool struct { rules rules } -// FromBool creates a Bool representing a static, scalar value. -func FromBool(b bool) Bool { - return Bool{ - rules: rules{simpleRule(b)}, - } -} - // Value returns the bool value for the given repository. func (b *Bool) Value(name string) bool { v := b.rules.Match(name) diff --git a/lib/batches/template/partial_eval.go b/lib/batches/template/partial_eval.go index 905ece71d5..e38c328e90 100644 --- a/lib/batches/template/partial_eval.go +++ b/lib/batches/template/partial_eval.go @@ -69,7 +69,6 @@ func parseAndPartialEval(input string, ctx *StepContext) (*template.Template, er Funcs(builtins). Funcs(ctx.ToFuncMap()). Parse(input) - if err != nil { return nil, err } @@ -329,7 +328,7 @@ func isTrue(val reflect.Value) (truth bool) { return val.Bool() case reflect.Complex64, reflect.Complex128: return val.Complex() != 0 - case reflect.Chan, reflect.Func, reflect.Ptr, reflect.Interface: + case reflect.Chan, reflect.Func, reflect.Pointer, reflect.Interface: return !val.IsNil() case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return val.Int() != 0 diff --git a/lib/batches/template/template.go b/lib/batches/template/template.go index 0591ee8870..0b04518f7f 100644 --- a/lib/batches/template/template.go +++ b/lib/batches/template/template.go @@ -1,6 +1,9 @@ package template -import "text/template" +import ( + "strings" + "text/template" +) func New(name, tmpl, option string, ctxs ...template.FuncMap) (*template.Template, error) { t := template.New(name).Delims(startDelim, endDelim) @@ -16,3 +19,7 @@ func New(name, tmpl, option string, ctxs ...template.FuncMap) (*template.Templat return t.Parse(tmpl) } + +func ContainsTemplateAction(tmpl string) bool { + return strings.Contains(tmpl, startDelim) +} diff --git a/lib/batches/template/templating.go b/lib/batches/template/templating.go index eae6547860..2ff1d641d4 100644 --- a/lib/batches/template/templating.go +++ b/lib/batches/template/templating.go @@ -16,8 +16,10 @@ import ( "github.com/sourcegraph/sourcegraph/lib/errors" ) -const startDelim = "${{" -const endDelim = "}}" +const ( + startDelim = "${{" + endDelim = "}}" +) var builtins = template.FuncMap{ "join": strings.Join, @@ -74,7 +76,6 @@ func ValidateBatchSpecTemplate(spec string) (bool, error) { // option "missingkey=error". See https://pkg.go.dev/text/template#Template.Option for // more. t, err := New("validateBatchSpecTemplate", spec, "missingkey=error", sfm, cstfm) - if err != nil { // Attempt to extract the specific template variable field that caused the error // to provide a clearer message. diff --git a/lib/codeintel/upload/indexer_name.go b/lib/codeintel/upload/indexer_name.go index 318dc6f82b..c534e6f7d3 100644 --- a/lib/codeintel/upload/indexer_name.go +++ b/lib/codeintel/upload/indexer_name.go @@ -20,9 +20,6 @@ import ( // is 10639 characters long. const MaxBufferSize = 128 * 1024 -// ErrMetadataExceedsBuffer occurs when the first line of an LSIF index is too long to read. -var ErrMetadataExceedsBuffer = errors.New("metaData vertex exceeds buffer") - // ErrInvalidMetaDataVertex occurs when the first line of an LSIF index is not a valid metadata vertex. var ErrInvalidMetaDataVertex = errors.New("invalid metaData vertex") diff --git a/lib/codeintel/upload/request.go b/lib/codeintel/upload/request.go index db571435ea..9de1a799ae 100644 --- a/lib/codeintel/upload/request.go +++ b/lib/codeintel/upload/request.go @@ -30,6 +30,9 @@ type uploadRequestOptions struct { // ErrUnauthorized occurs when the upload endpoint returns a 401 response. var ErrUnauthorized = errors.New("unauthorized upload") +// ErrForbidden occurs when the upload endpoint returns a 403 response. +var ErrForbidden = errors.New("insufficient permissions") + // performUploadRequest performs an HTTP POST to the upload endpoint. The query string of the request // is constructed from the given request options and the body of the request is the unmodified reader. // If target is a non-nil pointer, it will be assigned the value of the upload identifier present @@ -105,17 +108,17 @@ func performRequest(ctx context.Context, req *http.Request, httpClient Client, l // returns a boolean flag indicating if the function can be retried on failure (error-dependent). func decodeUploadPayload(resp *http.Response, body []byte, target *int) (bool, error) { if resp.StatusCode >= 300 { + detail := extractBodyDetail(body) + if resp.StatusCode == http.StatusUnauthorized { - return false, ErrUnauthorized + return false, errors.Wrap(ErrUnauthorized, detail) } - - suffix := "" - if !bytes.HasPrefix(bytes.TrimSpace(body), []byte{'<'}) { - suffix = fmt.Sprintf(" (%s)", bytes.TrimSpace(body)) + if resp.StatusCode == http.StatusForbidden { + return false, errors.Wrap(ErrForbidden, detail) } // Do not retry client errors - return resp.StatusCode >= 500, errors.Errorf("unexpected status code: %d%s", resp.StatusCode, suffix) + return resp.StatusCode >= 500, errors.Errorf("unexpected status code: %d: %s", resp.StatusCode, detail) } if target == nil { @@ -199,6 +202,16 @@ func makeUploadURL(opts uploadRequestOptions) (*url.URL, error) { return parsedUrl, nil } +// extractBodyDetail returns the response body as a suffix string for error messages. +// Returns an empty string if the body is empty or appears to be HTML. +func extractBodyDetail(body []byte) string { + trimmed := bytes.TrimSpace(body) + if len(trimmed) == 0 || bytes.HasPrefix(trimmed, []byte{'<'}) { + return "" + } + return string(trimmed) +} + func formatInt(v int) string { return strconv.FormatInt(int64(v), 10) } diff --git a/lib/codeintel/upload/upload.go b/lib/codeintel/upload/upload.go index 097990d1b8..55a977c358 100644 --- a/lib/codeintel/upload/upload.go +++ b/lib/codeintel/upload/upload.go @@ -218,8 +218,6 @@ func uploadMultipartIndexParts(ctx context.Context, httpClient Client, opts Uplo } for i, reader := range readers { - i, reader := i, reader - pool.Go(func(ctx context.Context) error { // Determine size of this reader. If we're not the last reader in the slice, // then we're the maximum payload size. Otherwise, we're whatever is left. diff --git a/lib/errors/client_error.go b/lib/errors/client_error.go index 505646a1cf..a6b39493ab 100644 --- a/lib/errors/client_error.go +++ b/lib/errors/client_error.go @@ -14,8 +14,10 @@ type ClientError struct { Err error } -var _ error = ClientError{nil} -var _ Wrapper = ClientError{nil} +var ( + _ error = ClientError{nil} + _ Wrapper = ClientError{nil} +) func (e ClientError) Error() string { return e.Err.Error() diff --git a/lib/errors/cockroach.go b/lib/errors/cockroach.go index 7a8632722d..6a1ac6dcc3 100644 --- a/lib/errors/cockroach.go +++ b/lib/errors/cockroach.go @@ -102,7 +102,7 @@ func AsInterface[I any](err error, target *I) bool { if target == nil { panic("Expected non-nil pointer to interface") } - if typ := reflect.TypeOf(target); typ.Elem().Kind() != reflect.Interface { + if typ := reflect.TypeFor[*I](); typ.Elem().Kind() != reflect.Interface { panic("Expected pointer to interface") } return errors.As(err, target) @@ -164,23 +164,23 @@ func registerCockroachSafeTypes() { // We consider booleans and numeric values to be always safe for // reporting. A log call can opt out by using redact.Unsafe() around // a value that would be otherwise considered safe. - redact.RegisterSafeType(reflect.TypeOf(true)) // bool - redact.RegisterSafeType(reflect.TypeOf(123)) // int - redact.RegisterSafeType(reflect.TypeOf(int8(0))) - redact.RegisterSafeType(reflect.TypeOf(int16(0))) - redact.RegisterSafeType(reflect.TypeOf(int32(0))) - redact.RegisterSafeType(reflect.TypeOf(int64(0))) - redact.RegisterSafeType(reflect.TypeOf(uint8(0))) - redact.RegisterSafeType(reflect.TypeOf(uint16(0))) - redact.RegisterSafeType(reflect.TypeOf(uint32(0))) - redact.RegisterSafeType(reflect.TypeOf(uint64(0))) - redact.RegisterSafeType(reflect.TypeOf(float32(0))) - redact.RegisterSafeType(reflect.TypeOf(float64(0))) - redact.RegisterSafeType(reflect.TypeOf(complex64(0))) - redact.RegisterSafeType(reflect.TypeOf(complex128(0))) + redact.RegisterSafeType(reflect.TypeFor[bool]()) // bool + redact.RegisterSafeType(reflect.TypeFor[int]()) // int + redact.RegisterSafeType(reflect.TypeFor[int8]()) + redact.RegisterSafeType(reflect.TypeFor[int16]()) + redact.RegisterSafeType(reflect.TypeFor[int32]()) + redact.RegisterSafeType(reflect.TypeFor[int64]()) + redact.RegisterSafeType(reflect.TypeFor[uint8]()) + redact.RegisterSafeType(reflect.TypeFor[uint16]()) + redact.RegisterSafeType(reflect.TypeFor[uint32]()) + redact.RegisterSafeType(reflect.TypeFor[uint64]()) + redact.RegisterSafeType(reflect.TypeFor[float32]()) + redact.RegisterSafeType(reflect.TypeFor[float64]()) + redact.RegisterSafeType(reflect.TypeFor[complex64]()) + redact.RegisterSafeType(reflect.TypeFor[complex128]()) // Signal names are also safe for reporting. redact.RegisterSafeType(reflect.TypeOf(os.Interrupt)) // Times and durations too. - redact.RegisterSafeType(reflect.TypeOf(time.Time{})) - redact.RegisterSafeType(reflect.TypeOf(time.Duration(0))) + redact.RegisterSafeType(reflect.TypeFor[time.Time]()) + redact.RegisterSafeType(reflect.TypeFor[time.Duration]()) } diff --git a/lib/errors/multi_error.go b/lib/errors/multi_error.go index 452061473e..86824eb6fa 100644 --- a/lib/errors/multi_error.go +++ b/lib/errors/multi_error.go @@ -22,8 +22,10 @@ type multiError struct { errs []error } -var _ MultiError = (*multiError)(nil) -var _ Typed = (*multiError)(nil) +var ( + _ MultiError = (*multiError)(nil) + _ Typed = (*multiError)(nil) +) func combineNonNilErrors(err1 error, err2 error) MultiError { multi1, ok1 := err1.(MultiError) diff --git a/lib/errors/rich_error.go b/lib/errors/rich_error.go index 6315f3e7e1..b4d7a53e6b 100644 --- a/lib/errors/rich_error.go +++ b/lib/errors/rich_error.go @@ -46,7 +46,7 @@ func UnpackRichErrorPtr(richErr *RichError) (error, []attribute.KeyValue) { if richErr == nil || *richErr == nil { return nil, nil } - if val := reflect.ValueOf(*richErr); val.Kind() == reflect.Ptr && val.IsNil() { + if val := reflect.ValueOf(*richErr); val.Kind() == reflect.Pointer && val.IsNil() { return nil, nil } data := (*richErr).Unpack() @@ -57,11 +57,6 @@ type ErrorPtr[A any, T PtrRichError[A]] struct { ptr2 **A } -// nolint:unused -func typeAssertion[A any, T PtrRichError[A]]() { - var _ RichError = (*ErrorPtr[A, T])(nil) -} - // NewErrorPtr is the only way to create an ErrorPtr. func NewErrorPtr[A any, T PtrRichError[A]](ptr2 **A) ErrorPtr[A, T] { if ptr2 == nil { @@ -94,7 +89,7 @@ func (p ErrorPtr[A, T]) Unpack() RichErrorData { if val == nil { return empty } - if inner := reflect.ValueOf(val); inner.Kind() == reflect.Ptr && inner.IsNil() { + if inner := reflect.ValueOf(val); inner.Kind() == reflect.Pointer && inner.IsNil() { return empty } return val.Unpack() diff --git a/lib/go.mod b/lib/go.mod index 00f3d2d6df..e57ef54d53 100644 --- a/lib/go.mod +++ b/lib/go.mod @@ -1,93 +1,3 @@ module github.com/sourcegraph/sourcegraph/lib go 1.26.3 - -require ( - github.com/Masterminds/semver v1.5.0 - github.com/charmbracelet/glamour v0.10.0 - github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 - github.com/cockroachdb/errors v1.12.0 - github.com/cockroachdb/redact v1.1.6 - github.com/ghodss/yaml v1.0.0 - github.com/gobwas/glob v0.2.3 - github.com/google/go-cmp v0.7.0 - github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853 - github.com/jackc/pgx/v5 v5.9.2 - github.com/klauspost/pgzip v1.2.6 - github.com/mattn/go-isatty v0.0.20 - github.com/mattn/go-runewidth v0.0.19 - github.com/moby/term v0.5.2 - github.com/muesli/termenv v0.16.0 - github.com/scip-code/scip/bindings/go/scip v0.7.0 - github.com/sourcegraph/conc v0.3.0 - github.com/sourcegraph/go-diff v0.7.0 - github.com/sourcegraph/log v0.0.0-20250923023806-517b6960b55b - github.com/stretchr/testify v1.11.1 - github.com/urfave/cli/v3 v3.8.0 - github.com/xeipuuv/gojsonschema v1.2.0 - github.com/xlab/treeprint v1.2.0 - go.opentelemetry.io/otel v1.43.0 - golang.org/x/sys v0.41.0 - golang.org/x/term v0.37.0 - google.golang.org/protobuf v1.36.10 - gopkg.in/yaml.v3 v3.0.1 -) - -require ( - github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect - github.com/alecthomas/chroma/v2 v2.14.0 // indirect - github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect - github.com/aymerick/douceur v0.2.0 // indirect - github.com/benbjohnson/clock v1.3.5 // indirect - github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect - github.com/charmbracelet/x/ansi v0.8.0 // indirect - github.com/charmbracelet/x/cellbuf v0.0.13 // indirect - github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect - github.com/charmbracelet/x/term v0.2.1 // indirect - github.com/clipperhouse/uax29/v2 v2.2.0 // indirect - github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/dlclark/regexp2 v1.11.0 // indirect - github.com/fatih/color v1.18.0 // indirect - github.com/getsentry/sentry-go v0.27.0 // indirect - github.com/go-logr/logr v1.4.3 // indirect - github.com/go-logr/stdr v1.2.2 // indirect - github.com/gogo/protobuf v1.3.2 // indirect - github.com/google/uuid v1.6.0 // indirect - github.com/gorilla/css v1.0.1 // indirect - github.com/jackc/pgpassfile v1.0.0 // indirect - github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect - github.com/klauspost/compress v1.18.0 // indirect - github.com/kr/pretty v0.3.1 // indirect - github.com/kr/text v0.2.0 // indirect - github.com/lucasb-eyer/go-colorful v1.2.0 // indirect - github.com/mattn/go-colorable v0.1.14 // indirect - github.com/microcosm-cc/bluemonday v1.0.27 // indirect - github.com/muesli/reflow v0.3.0 // indirect - github.com/pkg/errors v0.9.1 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/rivo/uniseg v0.4.7 // indirect - github.com/rogpeppe/go-internal v1.14.1 // indirect - github.com/sourcegraph/beaut v0.0.0-20240611013027-627e4c25335a // indirect - github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect - github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect - github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect - github.com/yuin/goldmark v1.7.8 // indirect - github.com/yuin/goldmark-emoji v1.0.5 // indirect - go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/otel/metric v1.43.0 // indirect - go.opentelemetry.io/otel/trace v1.43.0 // indirect - go.uber.org/atomic v1.11.0 // indirect - go.uber.org/goleak v1.3.0 // indirect - go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap v1.24.0 // indirect - golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 // indirect - golang.org/x/net v0.44.0 // indirect - golang.org/x/text v0.29.0 // indirect - golang.org/x/tools v0.37.0 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect -) - -// See: https://github.com/ghodss/yaml/pull/65 -replace github.com/ghodss/yaml => github.com/sourcegraph/yaml v1.0.1-0.20200714132230-56936252f152 diff --git a/lib/output/capabilities.go b/lib/output/capabilities.go index f789104ecd..1bca2bd56e 100644 --- a/lib/output/capabilities.go +++ b/lib/output/capabilities.go @@ -82,7 +82,13 @@ func detectCapabilities(opts OutputOpts) (caps capabilities, err error) { // detect color mode caps.Color = opts.ForceColor if !opts.ForceColor { - caps.Color = detectColor(caps.Isatty) + // If ForceTTY is explicitly false, we also disable colors since non-TTY + // mode shouldn't have colors (and detection might be affected by env vars). + if opts.ForceTTY != nil && !*opts.ForceTTY { + caps.Color = false + } else { + caps.Color = detectColor(caps.Isatty) + } } // set detected background color diff --git a/lib/output/emoji.go b/lib/output/emoji.go index 469e9eccb8..b0c8740140 100644 --- a/lib/output/emoji.go +++ b/lib/output/emoji.go @@ -17,9 +17,9 @@ var allEmojis = [...]string{ // Standard emoji for use in output. const ( - EmojiFailure = "❌" + EmojiFailure = " ⨯" EmojiWarning = "❗️" - EmojiSuccess = "✅" + EmojiSuccess = " ✓" EmojiInfo = "ℹ️" EmojiLightbulb = "💡" EmojiAsterisk = "✱" diff --git a/lib/output/line.go b/lib/output/line.go index ed566d0d8b..2fdc21a28b 100644 --- a/lib/output/line.go +++ b/lib/output/line.go @@ -64,11 +64,12 @@ func (fl FancyLine) write(w io.Writer, caps capabilities) { if fl.Prefix != "" { fmt.Fprint(w, fl.Prefix+" ") } + emojiPrefix := "" if fl.emoji != "" { - fmt.Fprint(w, fl.emoji+" ") + emojiPrefix = fl.emoji + " " } - fmt.Fprintf(w, "%s"+fl.format+"%s", caps.formatArgs(append(append([]any{fl.style}, fl.args...), StyleReset))...) + fmt.Fprintf(w, "%s"+emojiPrefix+fl.format+"%s", caps.formatArgs(append(append([]any{fl.style}, fl.args...), StyleReset))...) if fl.Prompt { // Add whitespace for user input _, _ = w.Write([]byte(" ")) diff --git a/lib/output/output.go b/lib/output/output.go index f1d1305277..93e0f4306a 100644 --- a/lib/output/output.go +++ b/lib/output/output.go @@ -92,7 +92,7 @@ var newOutputPlatformQuirks func(o *Output) error // newCapabilityWatcher returns a channel that receives a message when // capabilities are updated. By default, no watching functionality is // available. -var newCapabilityWatcher = func(opts OutputOpts) chan capabilities { return nil } +var newCapabilityWatcher = func(_ OutputOpts) chan capabilities { return nil } func NewOutput(w io.Writer, opts OutputOpts) *Output { // Not being able to detect capabilities is alright. It might mean output will look @@ -146,6 +146,17 @@ func (o *Output) UnsetVerbose() { o.verbose = false } +func (o *Output) IsVerbose() bool { + o.lock.Lock() + defer o.lock.Unlock() + return o.verbose +} + +// IsTTY reports whether this output is configured to render to a TTY. +func (o *Output) IsTTY() bool { + return o.caps.Isatty +} + func (o *Output) Unlock() { if o.caps.Isatty { // Show the cursor once more. diff --git a/lib/output/output_unix.go b/lib/output/output_unix.go index f55b2003b0..11c2a143d5 100644 --- a/lib/output/output_unix.go +++ b/lib/output/output_unix.go @@ -1,5 +1,4 @@ //go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris -// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris package output diff --git a/lib/output/pending_tty.go b/lib/output/pending_tty.go index 86853edc4a..c16258bce2 100644 --- a/lib/output/pending_tty.go +++ b/lib/output/pending_tty.go @@ -95,7 +95,11 @@ func (p *pendingTTY) Complete(message FancyLine) { p.o.moveUp(1) p.o.clearCurrentLine() - p.write(message) + // Write directly without truncation (unlike p.write, which truncates to + // terminal width to keep spinner animation frames on a single line). + // Completion messages may contain multi-line output such as URLs and must + // be displayed in full. + message.write(p.o.w, p.o.caps) } func (p *pendingTTY) Close() { p.Destroy() } diff --git a/lib/output/progress.go b/lib/output/progress.go index 2b7d9688a4..6490666656 100644 --- a/lib/output/progress.go +++ b/lib/output/progress.go @@ -34,9 +34,11 @@ type ProgressOpts struct { SuccessEmoji string SuccessStyle Style - // NoSpinner turns of the automatic updating of the progress bar and - // spinner in a background goroutine. - // Used for testing only! + // NoSpinner disables the background goroutine that updates progress bars + // and spinners. In TTY mode this stops the spinner animation; in non-TTY + // (simple) mode this suppresses the periodic ticker that dumps progress + // bars, which is useful when output cannot be overwritten in place (e.g. + // CI log files). Completion and failure events still print. NoSpinner bool } diff --git a/lib/output/progress_simple.go b/lib/output/progress_simple.go index dadfc3086c..6b55683f49 100644 --- a/lib/output/progress_simple.go +++ b/lib/output/progress_simple.go @@ -33,6 +33,9 @@ func (p *progressSimple) SetValue(i int, v float64) { } func (p *progressSimple) stop() { + if p.done == nil { + return + } c := make(chan struct{}) p.done <- c <-c @@ -46,9 +49,7 @@ func newProgressSimple(bars []*ProgressBar, o *Output, opts *ProgressOpts) *prog } if opts != nil && opts.NoSpinner { - if p.Output.verbose { - writeBars(p.Output, p.bars) - } + p.done = nil return p } @@ -62,7 +63,6 @@ func newProgressSimple(bars []*ProgressBar, o *Output, opts *ProgressOpts) *prog if p.Output.verbose { writeBars(p.Output, p.bars) } - case c := <-p.done: c <- struct{}{} return diff --git a/lib/output/progress_tty.go b/lib/output/progress_tty.go index f10dc18921..0f7421d1e9 100644 --- a/lib/output/progress_tty.go +++ b/lib/output/progress_tty.go @@ -139,13 +139,11 @@ func newProgressTTY(bars []*ProgressBar, o *Output, opts *ProgressOpts) *progres o: o, emojiWidth: 3, pendingEmoji: spinnerStrings[0], - spinner: newSpinner(100 * time.Millisecond), } + p.opts = *DefaultProgressTTYOpts if opts != nil { - p.opts = *opts - } else { - p.opts = *DefaultProgressTTYOpts + p.opts.NoSpinner = opts.NoSpinner } if w := runewidth.StringWidth(p.opts.SuccessEmoji); w > p.emojiWidth { @@ -163,6 +161,8 @@ func newProgressTTY(bars []*ProgressBar, o *Output, opts *ProgressOpts) *progres return p } + p.spinner = newSpinner(100 * time.Millisecond) + go func() { for s := range p.spinner.C { func() { diff --git a/lib/output/progress_with_status_bars_simple.go b/lib/output/progress_with_status_bars_simple.go index e985c2e952..b06be8dcca 100644 --- a/lib/output/progress_with_status_bars_simple.go +++ b/lib/output/progress_with_status_bars_simple.go @@ -59,10 +59,7 @@ func newProgressWithStatusBarsSimple(bars []*ProgressBar, statusBars []*StatusBa } if opts != nil && opts.NoSpinner { - if p.Output.verbose { - writeBars(p.Output, p.bars) - writeStatusBars(p.Output, p.statusBars) - } + p.done = nil return p } @@ -77,7 +74,6 @@ func newProgressWithStatusBarsSimple(bars []*ProgressBar, statusBars []*StatusBa writeBars(p.Output, p.bars) writeStatusBars(p.Output, p.statusBars) } - case c := <-p.done: c <- struct{}{} return diff --git a/lib/output/progress_with_status_bars_tty.go b/lib/output/progress_with_status_bars_tty.go index 3cfcf5834f..254eeac007 100644 --- a/lib/output/progress_with_status_bars_tty.go +++ b/lib/output/progress_with_status_bars_tty.go @@ -14,15 +14,13 @@ func newProgressWithStatusBarsTTY(bars []*ProgressBar, statusBars []*StatusBar, o: o, emojiWidth: 3, pendingEmoji: spinnerStrings[0], - spinner: newSpinner(100 * time.Millisecond), }, statusBars: statusBars, } + p.opts = *DefaultProgressTTYOpts if opts != nil { - p.opts = *opts - } else { - p.opts = *DefaultProgressTTYOpts + p.opts.NoSpinner = opts.NoSpinner } if w := runewidth.StringWidth(p.opts.SuccessEmoji); w > p.emojiWidth { @@ -41,6 +39,8 @@ func newProgressWithStatusBarsTTY(bars []*ProgressBar, statusBars []*StatusBar, return p } + p.spinner = newSpinner(100 * time.Millisecond) + go func() { for s := range p.spinner.C { func() { @@ -220,7 +220,6 @@ func calcStatusBarLabelWidth(statusBars []*StatusBar, termWidth int) int { } return statusBarLabelWidth - } func (p *progressWithStatusBarsTTY) writeStatusBar(last bool, statusBar *StatusBar) { diff --git a/lib/output/spinner.go b/lib/output/spinner.go index b0b5b86de2..23220224df 100644 --- a/lib/output/spinner.go +++ b/lib/output/spinner.go @@ -41,6 +41,9 @@ func newSpinner(interval time.Duration) *spinner { } func (s *spinner) stop() { + if s == nil { + return + } c := make(chan struct{}) s.done <- c <-c diff --git a/lib/output/table.go b/lib/output/table.go index 6736b30756..8388ed5ee6 100644 --- a/lib/output/table.go +++ b/lib/output/table.go @@ -6,9 +6,7 @@ import ( ) const ( - purple = lipgloss.Color("99") - gray = lipgloss.Color("245") - lightGray = lipgloss.Color("241") + purple = lipgloss.Color("99") ) type Table struct { @@ -43,10 +41,6 @@ func (t *Table) AddRow(elems ...string) { t.tbl.Row(elems...) } -func (t *Table) AddRows(elems ...[]string) { - t.tbl.Rows(elems...) -} - func (t *Table) Render() string { return t.tbl.Render() } diff --git a/lib/process/pipe.go b/lib/process/pipe.go index b7a6f9a40a..b06364cc44 100644 --- a/lib/process/pipe.go +++ b/lib/process/pipe.go @@ -15,9 +15,11 @@ import ( // initialBufSize is the initial size of the buffer that PipeOutput uses to // read lines. -const initialBufSize = 4 * 1024 // 4k -// maxTokenSize is the max size of a token that PipeOutput reads. -const maxTokenSize = 100 * 1024 * 1024 // 100mb +const ( + initialBufSize = 4 * 1024 // 4k + // maxTokenSize is the max size of a token that PipeOutput reads. + maxTokenSize = 100 * 1024 * 1024 // 100mb +) type pipe func(w io.Writer, r io.Reader) error From dae54d432e40078e4d35c6e88b78a4a34ad63011 Mon Sep 17 00:00:00 2001 From: bahrmichael Date: Mon, 1 Jun 2026 09:55:34 +0200 Subject: [PATCH 3/8] revert cache change --- lib/batches/execution/cache/cache.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/lib/batches/execution/cache/cache.go b/lib/batches/execution/cache/cache.go index 9392cd2c31..f77d5ab92d 100644 --- a/lib/batches/execution/cache/cache.go +++ b/lib/batches/execution/cache/cache.go @@ -47,6 +47,15 @@ func (key CacheKey) mountsMetadata() ([]MountMetadata, error) { return nil, nil } +// perRunEnvVars resolve to per-job or per-executor values that change on +// every dequeue and must be stripped from cache keys. Mirrored in +// sourcegraph/sourcegraph/lib/batches/execution/cache/cache.go. +var perRunEnvVars = []string{ + "SRC_EXECUTOR_JOB_TOKEN", + "SRC_EXECUTOR_JOB_ID", + "SRC_EXECUTOR_NAME", +} + // resolveStepsEnvironment returns a slice of environments for each of the steps, // containing only the env vars that are actually used. func resolveStepsEnvironment(globalEnv []string, steps []batches.Step) ([]map[string]string, error) { @@ -64,6 +73,9 @@ func resolveStepsEnvironment(globalEnv []string, steps []batches.Step) ([]map[st if err != nil { return nil, errors.Wrapf(err, "resolving environment for step %d", i) } + for _, name := range perRunEnvVars { + delete(env, name) + } envs[i] = env } return envs, nil From 07042c4796afd838867296978bf5cc05fef54cac Mon Sep 17 00:00:00 2001 From: bahrmichael Date: Mon, 1 Jun 2026 09:56:11 +0200 Subject: [PATCH 4/8] revert cache test deletion --- lib/batches/execution/cache/cache_test.go | 31 +++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 lib/batches/execution/cache/cache_test.go diff --git a/lib/batches/execution/cache/cache_test.go b/lib/batches/execution/cache/cache_test.go new file mode 100644 index 0000000000..c416fe024f --- /dev/null +++ b/lib/batches/execution/cache/cache_test.go @@ -0,0 +1,31 @@ +package cache + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/sourcegraph/sourcegraph/lib/batches" + "github.com/sourcegraph/sourcegraph/lib/batches/env" +) + +func TestKeyer_Key_PerRunEnvVarsIgnored(t *testing.T) { + var stepEnv env.Environment + require.NoError(t, json.Unmarshal( + []byte(`["SRC_EXECUTOR_JOB_TOKEN", "SRC_EXECUTOR_JOB_ID", "SRC_EXECUTOR_NAME"]`), + &stepEnv, + )) + step := batches.Step{Run: "foo", Env: stepEnv} + repo := batches.Repository{ID: "r", Name: "r"} + + unset, err := (&CacheKey{Repository: repo, Steps: []batches.Step{step}, StepIndex: 0}).Key() + require.NoError(t, err) + resolved, err := (&CacheKey{Repository: repo, Steps: []batches.Step{step}, StepIndex: 0, GlobalEnv: []string{ + "SRC_EXECUTOR_JOB_TOKEN=tok", + "SRC_EXECUTOR_JOB_ID=42", + "SRC_EXECUTOR_NAME=executor-abc", + }}).Key() + require.NoError(t, err) + require.Equal(t, unset, resolved) +} From 90d3fd2ab9bd5123ef22156da27ec01a4de59c68 Mon Sep 17 00:00:00 2001 From: bahrmichael Date: Mon, 1 Jun 2026 10:48:27 +0200 Subject: [PATCH 5/8] Revert "revert cache test deletion" This reverts commit 07042c4796afd838867296978bf5cc05fef54cac. --- lib/batches/execution/cache/cache_test.go | 31 ----------------------- 1 file changed, 31 deletions(-) delete mode 100644 lib/batches/execution/cache/cache_test.go diff --git a/lib/batches/execution/cache/cache_test.go b/lib/batches/execution/cache/cache_test.go deleted file mode 100644 index c416fe024f..0000000000 --- a/lib/batches/execution/cache/cache_test.go +++ /dev/null @@ -1,31 +0,0 @@ -package cache - -import ( - "encoding/json" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/sourcegraph/sourcegraph/lib/batches" - "github.com/sourcegraph/sourcegraph/lib/batches/env" -) - -func TestKeyer_Key_PerRunEnvVarsIgnored(t *testing.T) { - var stepEnv env.Environment - require.NoError(t, json.Unmarshal( - []byte(`["SRC_EXECUTOR_JOB_TOKEN", "SRC_EXECUTOR_JOB_ID", "SRC_EXECUTOR_NAME"]`), - &stepEnv, - )) - step := batches.Step{Run: "foo", Env: stepEnv} - repo := batches.Repository{ID: "r", Name: "r"} - - unset, err := (&CacheKey{Repository: repo, Steps: []batches.Step{step}, StepIndex: 0}).Key() - require.NoError(t, err) - resolved, err := (&CacheKey{Repository: repo, Steps: []batches.Step{step}, StepIndex: 0, GlobalEnv: []string{ - "SRC_EXECUTOR_JOB_TOKEN=tok", - "SRC_EXECUTOR_JOB_ID=42", - "SRC_EXECUTOR_NAME=executor-abc", - }}).Key() - require.NoError(t, err) - require.Equal(t, unset, resolved) -} From bb4c53d1899fbc1f9d80b290ee6ba42dd3b0b974 Mon Sep 17 00:00:00 2001 From: bahrmichael Date: Mon, 1 Jun 2026 10:48:28 +0200 Subject: [PATCH 6/8] Revert "revert cache change" This reverts commit dae54d432e40078e4d35c6e88b78a4a34ad63011. --- lib/batches/execution/cache/cache.go | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/lib/batches/execution/cache/cache.go b/lib/batches/execution/cache/cache.go index f77d5ab92d..9392cd2c31 100644 --- a/lib/batches/execution/cache/cache.go +++ b/lib/batches/execution/cache/cache.go @@ -47,15 +47,6 @@ func (key CacheKey) mountsMetadata() ([]MountMetadata, error) { return nil, nil } -// perRunEnvVars resolve to per-job or per-executor values that change on -// every dequeue and must be stripped from cache keys. Mirrored in -// sourcegraph/sourcegraph/lib/batches/execution/cache/cache.go. -var perRunEnvVars = []string{ - "SRC_EXECUTOR_JOB_TOKEN", - "SRC_EXECUTOR_JOB_ID", - "SRC_EXECUTOR_NAME", -} - // resolveStepsEnvironment returns a slice of environments for each of the steps, // containing only the env vars that are actually used. func resolveStepsEnvironment(globalEnv []string, steps []batches.Step) ([]map[string]string, error) { @@ -73,9 +64,6 @@ func resolveStepsEnvironment(globalEnv []string, steps []batches.Step) ([]map[st if err != nil { return nil, errors.Wrapf(err, "resolving environment for step %d", i) } - for _, name := range perRunEnvVars { - delete(env, name) - } envs[i] = env } return envs, nil From c24c25c041f5785dc889010a58cb80c458f46388 Mon Sep 17 00:00:00 2001 From: bahrmichael Date: Mon, 1 Jun 2026 10:48:28 +0200 Subject: [PATCH 7/8] Revert "chore: update vendored Sourcegraph lib" This reverts commit cca7abe9a63b42c4beb3bc933d6189a0254f8347. --- lib/README.md | 4 +- lib/batches/batch_spec.go | 151 ++---------------- lib/batches/changeset_spec.go | 6 + lib/batches/execution/cache/cache.go | 12 ++ lib/batches/execution/cache/cache_test.go | 31 ++++ lib/batches/overridable/bool.go | 7 + lib/batches/template/partial_eval.go | 3 +- lib/batches/template/template.go | 9 +- lib/batches/template/templating.go | 7 +- lib/codeintel/upload/indexer_name.go | 3 + lib/codeintel/upload/request.go | 25 +-- lib/codeintel/upload/upload.go | 2 + lib/errors/client_error.go | 6 +- lib/errors/cockroach.go | 34 ++-- lib/errors/multi_error.go | 6 +- lib/errors/rich_error.go | 9 +- lib/go.mod | 90 +++++++++++ lib/output/capabilities.go | 8 +- lib/output/emoji.go | 4 +- lib/output/line.go | 5 +- lib/output/output.go | 13 +- lib/output/output_unix.go | 1 + lib/output/pending_tty.go | 6 +- lib/output/progress.go | 8 +- lib/output/progress_simple.go | 8 +- lib/output/progress_tty.go | 8 +- .../progress_with_status_bars_simple.go | 6 +- lib/output/progress_with_status_bars_tty.go | 9 +- lib/output/spinner.go | 3 - lib/output/table.go | 8 +- lib/process/pipe.go | 8 +- 31 files changed, 249 insertions(+), 251 deletions(-) create mode 100644 lib/batches/execution/cache/cache_test.go diff --git a/lib/README.md b/lib/README.md index e9bb62031c..0b478e3cd1 100644 --- a/lib/README.md +++ b/lib/README.md @@ -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**: 2ee2b8e77de9663b08ce5f6e5a2c7d2217ce721a +- **Date**: 2025-11-17 19:49:42 -0800 ## Updating diff --git a/lib/batches/batch_spec.go b/lib/batches/batch_spec.go index 30ef50d4a9..269093a82d 100644 --- a/lib/batches/batch_spec.go +++ b/lib/batches/batch_spec.go @@ -1,7 +1,6 @@ package batches import ( - "encoding/json" "fmt" "strings" @@ -39,31 +38,8 @@ type BatchSpec struct { 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"` } -// Hooks declares side-effect actions to run at well-defined changeset -// lifecycle events. Only allowed when Version is 3. -type ChangesetHooks struct { - OnCIFailure ChangesetHookAction `json:"onCIFailure,omitempty" yaml:"onCIFailure,omitempty"` - OnMergeConflict ChangesetHookAction `json:"onMergeConflict,omitempty" yaml:"onMergeConflict,omitempty"` -} - -// HookAction is a single action attached to a changeset lifecycle event. -// -// Hook actions reuse the Step shape from the top-level steps block. -type ChangesetHookAction struct { - Steps []Step `json:"steps,omitempty" yaml:"steps,omitempty"` -} - -type changesetHookEvent string - -// Hook event names. Kept here so callers don't pass typoed strings. -const ( - ChangesetHookEventOnCIFailure changesetHookEvent = "onCIFailure" - ChangesetHookEventOnMergeConflict changesetHookEvent = "onMergeConflict" -) - type ChangesetTemplate struct { Title string `json:"title,omitempty" yaml:"title"` Body string `json:"body,omitempty" yaml:"body"` @@ -114,46 +90,13 @@ func (oqor *OnQueryOrRepository) GetBranches() ([]string, error) { } type Step struct { - Run string `json:"run,omitempty" yaml:"run"` - CodingAgent *CodingAgentStep `json:"codingAgent,omitempty" yaml:"codingAgent,omitempty"` - BuildImage *BuildImageStep `json:"buildImage,omitempty" yaml:"buildImage,omitempty"` - Container string `json:"container,omitempty" yaml:"container"` - Image string `json:"image,omitempty" yaml:"image"` - MaxAttempts int `json:"maxAttempts,omitempty" yaml:"maxAttempts,omitempty"` - Env env.Environment `json:"env" yaml:"env"` - Files map[string]string `json:"files,omitempty" yaml:"files,omitempty"` - Outputs Outputs `json:"outputs,omitempty" yaml:"outputs,omitempty"` - Mount []Mount `json:"mount,omitempty" yaml:"mount,omitempty"` - If any `json:"if,omitempty" yaml:"if,omitempty"` -} - -type CodingAgentStep struct { - Type string `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"` -} - -// 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 -// `Container`. Without canonicalization, the prep-side cache key — computed -// by JSON-marshaling Step — would include `image` while the executor side -// (which round-trips through src-cli) would not, producing divergent keys -// and silent cache misses for any v3 spec. See the regression test in -// lib/batches/execution/cache. -func (s Step) MarshalJSON() ([]byte, error) { - // Use an alias type to avoid infinite recursion through MarshalJSON. - type stepAlias Step - canon := stepAlias(s) - if canon.Container == "" { - canon.Container = canon.Image - } - canon.Image = "" - return json.Marshal(canon) + Run string `json:"run,omitempty" yaml:"run"` + Container string `json:"container,omitempty" yaml:"container"` + Env env.Environment `json:"env" yaml:"env"` + Files map[string]string `json:"files,omitempty" yaml:"files,omitempty"` + Outputs Outputs `json:"outputs,omitempty" yaml:"outputs,omitempty"` + Mount []Mount `json:"mount,omitempty" yaml:"mount,omitempty"` + If any `json:"if,omitempty" yaml:"if,omitempty"` } func (s *Step) IfCondition() string { @@ -218,97 +161,33 @@ func parseBatchSpec(schema string, data []byte) (*BatchSpec, error) { return nil, err } - if spec.Version == 3 { - // Mirror v3 `image:` into `container:` so in-memory consumers that - // read step.Container (e.g. the executor transform) keep working. - // JSON serialization is canonicalized separately in Step.MarshalJSON - // so prep-side cache hashing matches src-cli/executor serialization. - for i := range spec.Steps { - spec.Steps[i].Container = spec.Steps[i].Image - } - } - var errs error + if len(spec.Steps) != 0 && spec.ChangesetTemplate == nil { errs = errors.Append(errs, NewValidationError(errors.New("batch spec includes steps but no changesetTemplate"))) } - // v3 specs do not support changesetTemplate.published — publication is - // driven exclusively via the batchchangeagent tools. Reject the field at - // parse time. - if spec.Version == 3 && spec.ChangesetTemplate != nil && spec.ChangesetTemplate.Published != nil { - 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"))) - } - for i, step := range spec.Steps { for _, mount := range step.Mount { - if strings.Contains(mount.Path, invalidMountCharacters) { + if strings.ContainsAny(mount.Path, invalidMountCharacters) { errs = errors.Append(errs, NewValidationError(errors.Newf("step %d mount path contains invalid characters", i+1))) } - if strings.Contains(mount.Mountpoint, invalidMountCharacters) { + if strings.ContainsAny(mount.Mountpoint, invalidMountCharacters) { errs = errors.Append(errs, NewValidationError(errors.Newf("step %d mount mountpoint contains invalid characters", i+1))) } } - if step.CodingAgent != nil && step.Run != "" { - errs = errors.Append(errs, NewValidationError(errors.Newf("step %d: codingAgent and run cannot be combined in the same step", i+1))) - } - 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) { + if strings.ContainsAny(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) - } - return &spec, 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 -// non-schema callers (and any future schema drift) still fail safely, and we -// run the per-step mount-character validator that the schema cannot express. -func validateHooks(spec *BatchSpec) error { - if spec.ChangesetHooks == nil { - return nil - } - - var errs error - - if spec.Version != 3 { - errs = errors.Append(errs, NewValidationError(errors.New("batch spec hooks require version: 3"))) - } - - validate := func(event changesetHookEvent, action ChangesetHookAction) { - for i, step := range action.Steps { - for _, mount := range step.Mount { - if strings.Contains(mount.Path, invalidMountCharacters) { - errs = errors.Append(errs, NewValidationError(errors.Newf( - "hooks.%s step %d mount path contains invalid characters", event, i+1, - ))) - } - if strings.Contains(mount.Mountpoint, invalidMountCharacters) { - errs = errors.Append(errs, NewValidationError(errors.Newf( - "hooks.%s step %d mount mountpoint contains invalid characters", event, i+1, - ))) - } - } - } - } - - validate(ChangesetHookEventOnCIFailure, spec.ChangesetHooks.OnCIFailure) - validate(ChangesetHookEventOnMergeConflict, spec.ChangesetHooks.OnMergeConflict) - - return errs -} - -const invalidMountCharacters = "," +// docker uses Golang's `encoding/csv` library to parse arguments passed to `--mount` +const invalidMountCharacters = ",\"\n\r" func (on *OnQueryOrRepository) String() string { if on.RepositoriesMatchingQuery != "" { @@ -333,6 +212,10 @@ func (e BatchSpecValidationError) Error() string { return e.err.Error() } +func IsValidationError(err error) bool { + return errors.HasType[*BatchSpecValidationError](err) +} + // SkippedStepsForRepo calculates the steps required to run on the given repo. func SkippedStepsForRepo(spec *BatchSpec, repoName string, fileMatches []string) (skipped map[int]struct{}, err error) { skipped = map[int]struct{}{} diff --git a/lib/batches/changeset_spec.go b/lib/batches/changeset_spec.go index 16e917e311..4b64f670a1 100644 --- a/lib/batches/changeset_spec.go +++ b/lib/batches/changeset_spec.go @@ -203,6 +203,12 @@ func (d *ChangesetSpec) IsImportingExisting() bool { return d.Type() == ChangesetSpecDescriptionTypeExisting } +// IsBranch returns whether the description is of type +// ChangesetSpecDescriptionTypeBranch. +func (d *ChangesetSpec) IsBranch() bool { + return d.Type() == ChangesetSpecDescriptionTypeBranch +} + // ChangesetSpecDescriptionType tells the consumer what the type of a // ChangesetSpecDescription is without having to look into the description. // Useful in the GraphQL when a HiddenChangesetSpec is returned. diff --git a/lib/batches/execution/cache/cache.go b/lib/batches/execution/cache/cache.go index 9392cd2c31..f77d5ab92d 100644 --- a/lib/batches/execution/cache/cache.go +++ b/lib/batches/execution/cache/cache.go @@ -47,6 +47,15 @@ func (key CacheKey) mountsMetadata() ([]MountMetadata, error) { return nil, nil } +// perRunEnvVars resolve to per-job or per-executor values that change on +// every dequeue and must be stripped from cache keys. Mirrored in +// sourcegraph/sourcegraph/lib/batches/execution/cache/cache.go. +var perRunEnvVars = []string{ + "SRC_EXECUTOR_JOB_TOKEN", + "SRC_EXECUTOR_JOB_ID", + "SRC_EXECUTOR_NAME", +} + // resolveStepsEnvironment returns a slice of environments for each of the steps, // containing only the env vars that are actually used. func resolveStepsEnvironment(globalEnv []string, steps []batches.Step) ([]map[string]string, error) { @@ -64,6 +73,9 @@ func resolveStepsEnvironment(globalEnv []string, steps []batches.Step) ([]map[st if err != nil { return nil, errors.Wrapf(err, "resolving environment for step %d", i) } + for _, name := range perRunEnvVars { + delete(env, name) + } envs[i] = env } return envs, nil diff --git a/lib/batches/execution/cache/cache_test.go b/lib/batches/execution/cache/cache_test.go new file mode 100644 index 0000000000..c416fe024f --- /dev/null +++ b/lib/batches/execution/cache/cache_test.go @@ -0,0 +1,31 @@ +package cache + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/sourcegraph/sourcegraph/lib/batches" + "github.com/sourcegraph/sourcegraph/lib/batches/env" +) + +func TestKeyer_Key_PerRunEnvVarsIgnored(t *testing.T) { + var stepEnv env.Environment + require.NoError(t, json.Unmarshal( + []byte(`["SRC_EXECUTOR_JOB_TOKEN", "SRC_EXECUTOR_JOB_ID", "SRC_EXECUTOR_NAME"]`), + &stepEnv, + )) + step := batches.Step{Run: "foo", Env: stepEnv} + repo := batches.Repository{ID: "r", Name: "r"} + + unset, err := (&CacheKey{Repository: repo, Steps: []batches.Step{step}, StepIndex: 0}).Key() + require.NoError(t, err) + resolved, err := (&CacheKey{Repository: repo, Steps: []batches.Step{step}, StepIndex: 0, GlobalEnv: []string{ + "SRC_EXECUTOR_JOB_TOKEN=tok", + "SRC_EXECUTOR_JOB_ID=42", + "SRC_EXECUTOR_NAME=executor-abc", + }}).Key() + require.NoError(t, err) + require.Equal(t, unset, resolved) +} diff --git a/lib/batches/overridable/bool.go b/lib/batches/overridable/bool.go index 8d90d6a073..411e466071 100644 --- a/lib/batches/overridable/bool.go +++ b/lib/batches/overridable/bool.go @@ -7,6 +7,13 @@ type Bool struct { rules rules } +// FromBool creates a Bool representing a static, scalar value. +func FromBool(b bool) Bool { + return Bool{ + rules: rules{simpleRule(b)}, + } +} + // Value returns the bool value for the given repository. func (b *Bool) Value(name string) bool { v := b.rules.Match(name) diff --git a/lib/batches/template/partial_eval.go b/lib/batches/template/partial_eval.go index cdffbc96c6..9c223381ed 100644 --- a/lib/batches/template/partial_eval.go +++ b/lib/batches/template/partial_eval.go @@ -89,6 +89,7 @@ func parseAndPartialEval(input string, ctx *StepContext) (*template.Template, er Funcs(builtins). Funcs(ctx.ToFuncMap()). Parse(input) + if err != nil { return nil, err } @@ -348,7 +349,7 @@ func isTrue(val reflect.Value) (truth bool) { return val.Bool() case reflect.Complex64, reflect.Complex128: return val.Complex() != 0 - case reflect.Chan, reflect.Func, reflect.Pointer, reflect.Interface: + case reflect.Chan, reflect.Func, reflect.Ptr, reflect.Interface: return !val.IsNil() case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return val.Int() != 0 diff --git a/lib/batches/template/template.go b/lib/batches/template/template.go index 0b04518f7f..0591ee8870 100644 --- a/lib/batches/template/template.go +++ b/lib/batches/template/template.go @@ -1,9 +1,6 @@ package template -import ( - "strings" - "text/template" -) +import "text/template" func New(name, tmpl, option string, ctxs ...template.FuncMap) (*template.Template, error) { t := template.New(name).Delims(startDelim, endDelim) @@ -19,7 +16,3 @@ func New(name, tmpl, option string, ctxs ...template.FuncMap) (*template.Templat return t.Parse(tmpl) } - -func ContainsTemplateAction(tmpl string) bool { - return strings.Contains(tmpl, startDelim) -} diff --git a/lib/batches/template/templating.go b/lib/batches/template/templating.go index 2ff1d641d4..eae6547860 100644 --- a/lib/batches/template/templating.go +++ b/lib/batches/template/templating.go @@ -16,10 +16,8 @@ import ( "github.com/sourcegraph/sourcegraph/lib/errors" ) -const ( - startDelim = "${{" - endDelim = "}}" -) +const startDelim = "${{" +const endDelim = "}}" var builtins = template.FuncMap{ "join": strings.Join, @@ -76,6 +74,7 @@ func ValidateBatchSpecTemplate(spec string) (bool, error) { // option "missingkey=error". See https://pkg.go.dev/text/template#Template.Option for // more. t, err := New("validateBatchSpecTemplate", spec, "missingkey=error", sfm, cstfm) + if err != nil { // Attempt to extract the specific template variable field that caused the error // to provide a clearer message. diff --git a/lib/codeintel/upload/indexer_name.go b/lib/codeintel/upload/indexer_name.go index c534e6f7d3..318dc6f82b 100644 --- a/lib/codeintel/upload/indexer_name.go +++ b/lib/codeintel/upload/indexer_name.go @@ -20,6 +20,9 @@ import ( // is 10639 characters long. const MaxBufferSize = 128 * 1024 +// ErrMetadataExceedsBuffer occurs when the first line of an LSIF index is too long to read. +var ErrMetadataExceedsBuffer = errors.New("metaData vertex exceeds buffer") + // ErrInvalidMetaDataVertex occurs when the first line of an LSIF index is not a valid metadata vertex. var ErrInvalidMetaDataVertex = errors.New("invalid metaData vertex") diff --git a/lib/codeintel/upload/request.go b/lib/codeintel/upload/request.go index 9de1a799ae..db571435ea 100644 --- a/lib/codeintel/upload/request.go +++ b/lib/codeintel/upload/request.go @@ -30,9 +30,6 @@ type uploadRequestOptions struct { // ErrUnauthorized occurs when the upload endpoint returns a 401 response. var ErrUnauthorized = errors.New("unauthorized upload") -// ErrForbidden occurs when the upload endpoint returns a 403 response. -var ErrForbidden = errors.New("insufficient permissions") - // performUploadRequest performs an HTTP POST to the upload endpoint. The query string of the request // is constructed from the given request options and the body of the request is the unmodified reader. // If target is a non-nil pointer, it will be assigned the value of the upload identifier present @@ -108,17 +105,17 @@ func performRequest(ctx context.Context, req *http.Request, httpClient Client, l // returns a boolean flag indicating if the function can be retried on failure (error-dependent). func decodeUploadPayload(resp *http.Response, body []byte, target *int) (bool, error) { if resp.StatusCode >= 300 { - detail := extractBodyDetail(body) - if resp.StatusCode == http.StatusUnauthorized { - return false, errors.Wrap(ErrUnauthorized, detail) + return false, ErrUnauthorized } - if resp.StatusCode == http.StatusForbidden { - return false, errors.Wrap(ErrForbidden, detail) + + suffix := "" + if !bytes.HasPrefix(bytes.TrimSpace(body), []byte{'<'}) { + suffix = fmt.Sprintf(" (%s)", bytes.TrimSpace(body)) } // Do not retry client errors - return resp.StatusCode >= 500, errors.Errorf("unexpected status code: %d: %s", resp.StatusCode, detail) + return resp.StatusCode >= 500, errors.Errorf("unexpected status code: %d%s", resp.StatusCode, suffix) } if target == nil { @@ -202,16 +199,6 @@ func makeUploadURL(opts uploadRequestOptions) (*url.URL, error) { return parsedUrl, nil } -// extractBodyDetail returns the response body as a suffix string for error messages. -// Returns an empty string if the body is empty or appears to be HTML. -func extractBodyDetail(body []byte) string { - trimmed := bytes.TrimSpace(body) - if len(trimmed) == 0 || bytes.HasPrefix(trimmed, []byte{'<'}) { - return "" - } - return string(trimmed) -} - func formatInt(v int) string { return strconv.FormatInt(int64(v), 10) } diff --git a/lib/codeintel/upload/upload.go b/lib/codeintel/upload/upload.go index 55a977c358..097990d1b8 100644 --- a/lib/codeintel/upload/upload.go +++ b/lib/codeintel/upload/upload.go @@ -218,6 +218,8 @@ func uploadMultipartIndexParts(ctx context.Context, httpClient Client, opts Uplo } for i, reader := range readers { + i, reader := i, reader + pool.Go(func(ctx context.Context) error { // Determine size of this reader. If we're not the last reader in the slice, // then we're the maximum payload size. Otherwise, we're whatever is left. diff --git a/lib/errors/client_error.go b/lib/errors/client_error.go index a6b39493ab..505646a1cf 100644 --- a/lib/errors/client_error.go +++ b/lib/errors/client_error.go @@ -14,10 +14,8 @@ type ClientError struct { Err error } -var ( - _ error = ClientError{nil} - _ Wrapper = ClientError{nil} -) +var _ error = ClientError{nil} +var _ Wrapper = ClientError{nil} func (e ClientError) Error() string { return e.Err.Error() diff --git a/lib/errors/cockroach.go b/lib/errors/cockroach.go index 6a1ac6dcc3..7a8632722d 100644 --- a/lib/errors/cockroach.go +++ b/lib/errors/cockroach.go @@ -102,7 +102,7 @@ func AsInterface[I any](err error, target *I) bool { if target == nil { panic("Expected non-nil pointer to interface") } - if typ := reflect.TypeFor[*I](); typ.Elem().Kind() != reflect.Interface { + if typ := reflect.TypeOf(target); typ.Elem().Kind() != reflect.Interface { panic("Expected pointer to interface") } return errors.As(err, target) @@ -164,23 +164,23 @@ func registerCockroachSafeTypes() { // We consider booleans and numeric values to be always safe for // reporting. A log call can opt out by using redact.Unsafe() around // a value that would be otherwise considered safe. - redact.RegisterSafeType(reflect.TypeFor[bool]()) // bool - redact.RegisterSafeType(reflect.TypeFor[int]()) // int - redact.RegisterSafeType(reflect.TypeFor[int8]()) - redact.RegisterSafeType(reflect.TypeFor[int16]()) - redact.RegisterSafeType(reflect.TypeFor[int32]()) - redact.RegisterSafeType(reflect.TypeFor[int64]()) - redact.RegisterSafeType(reflect.TypeFor[uint8]()) - redact.RegisterSafeType(reflect.TypeFor[uint16]()) - redact.RegisterSafeType(reflect.TypeFor[uint32]()) - redact.RegisterSafeType(reflect.TypeFor[uint64]()) - redact.RegisterSafeType(reflect.TypeFor[float32]()) - redact.RegisterSafeType(reflect.TypeFor[float64]()) - redact.RegisterSafeType(reflect.TypeFor[complex64]()) - redact.RegisterSafeType(reflect.TypeFor[complex128]()) + redact.RegisterSafeType(reflect.TypeOf(true)) // bool + redact.RegisterSafeType(reflect.TypeOf(123)) // int + redact.RegisterSafeType(reflect.TypeOf(int8(0))) + redact.RegisterSafeType(reflect.TypeOf(int16(0))) + redact.RegisterSafeType(reflect.TypeOf(int32(0))) + redact.RegisterSafeType(reflect.TypeOf(int64(0))) + redact.RegisterSafeType(reflect.TypeOf(uint8(0))) + redact.RegisterSafeType(reflect.TypeOf(uint16(0))) + redact.RegisterSafeType(reflect.TypeOf(uint32(0))) + redact.RegisterSafeType(reflect.TypeOf(uint64(0))) + redact.RegisterSafeType(reflect.TypeOf(float32(0))) + redact.RegisterSafeType(reflect.TypeOf(float64(0))) + redact.RegisterSafeType(reflect.TypeOf(complex64(0))) + redact.RegisterSafeType(reflect.TypeOf(complex128(0))) // Signal names are also safe for reporting. redact.RegisterSafeType(reflect.TypeOf(os.Interrupt)) // Times and durations too. - redact.RegisterSafeType(reflect.TypeFor[time.Time]()) - redact.RegisterSafeType(reflect.TypeFor[time.Duration]()) + redact.RegisterSafeType(reflect.TypeOf(time.Time{})) + redact.RegisterSafeType(reflect.TypeOf(time.Duration(0))) } diff --git a/lib/errors/multi_error.go b/lib/errors/multi_error.go index 86824eb6fa..452061473e 100644 --- a/lib/errors/multi_error.go +++ b/lib/errors/multi_error.go @@ -22,10 +22,8 @@ type multiError struct { errs []error } -var ( - _ MultiError = (*multiError)(nil) - _ Typed = (*multiError)(nil) -) +var _ MultiError = (*multiError)(nil) +var _ Typed = (*multiError)(nil) func combineNonNilErrors(err1 error, err2 error) MultiError { multi1, ok1 := err1.(MultiError) diff --git a/lib/errors/rich_error.go b/lib/errors/rich_error.go index b4d7a53e6b..6315f3e7e1 100644 --- a/lib/errors/rich_error.go +++ b/lib/errors/rich_error.go @@ -46,7 +46,7 @@ func UnpackRichErrorPtr(richErr *RichError) (error, []attribute.KeyValue) { if richErr == nil || *richErr == nil { return nil, nil } - if val := reflect.ValueOf(*richErr); val.Kind() == reflect.Pointer && val.IsNil() { + if val := reflect.ValueOf(*richErr); val.Kind() == reflect.Ptr && val.IsNil() { return nil, nil } data := (*richErr).Unpack() @@ -57,6 +57,11 @@ type ErrorPtr[A any, T PtrRichError[A]] struct { ptr2 **A } +// nolint:unused +func typeAssertion[A any, T PtrRichError[A]]() { + var _ RichError = (*ErrorPtr[A, T])(nil) +} + // NewErrorPtr is the only way to create an ErrorPtr. func NewErrorPtr[A any, T PtrRichError[A]](ptr2 **A) ErrorPtr[A, T] { if ptr2 == nil { @@ -89,7 +94,7 @@ func (p ErrorPtr[A, T]) Unpack() RichErrorData { if val == nil { return empty } - if inner := reflect.ValueOf(val); inner.Kind() == reflect.Pointer && inner.IsNil() { + if inner := reflect.ValueOf(val); inner.Kind() == reflect.Ptr && inner.IsNil() { return empty } return val.Unpack() diff --git a/lib/go.mod b/lib/go.mod index e57ef54d53..00f3d2d6df 100644 --- a/lib/go.mod +++ b/lib/go.mod @@ -1,3 +1,93 @@ module github.com/sourcegraph/sourcegraph/lib go 1.26.3 + +require ( + github.com/Masterminds/semver v1.5.0 + github.com/charmbracelet/glamour v0.10.0 + github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 + github.com/cockroachdb/errors v1.12.0 + github.com/cockroachdb/redact v1.1.6 + github.com/ghodss/yaml v1.0.0 + github.com/gobwas/glob v0.2.3 + github.com/google/go-cmp v0.7.0 + github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853 + github.com/jackc/pgx/v5 v5.9.2 + github.com/klauspost/pgzip v1.2.6 + github.com/mattn/go-isatty v0.0.20 + github.com/mattn/go-runewidth v0.0.19 + github.com/moby/term v0.5.2 + github.com/muesli/termenv v0.16.0 + github.com/scip-code/scip/bindings/go/scip v0.7.0 + github.com/sourcegraph/conc v0.3.0 + github.com/sourcegraph/go-diff v0.7.0 + github.com/sourcegraph/log v0.0.0-20250923023806-517b6960b55b + github.com/stretchr/testify v1.11.1 + github.com/urfave/cli/v3 v3.8.0 + github.com/xeipuuv/gojsonschema v1.2.0 + github.com/xlab/treeprint v1.2.0 + go.opentelemetry.io/otel v1.43.0 + golang.org/x/sys v0.41.0 + golang.org/x/term v0.37.0 + google.golang.org/protobuf v1.36.10 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect + github.com/alecthomas/chroma/v2 v2.14.0 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/aymerick/douceur v0.2.0 // indirect + github.com/benbjohnson/clock v1.3.5 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect + github.com/charmbracelet/x/ansi v0.8.0 // indirect + github.com/charmbracelet/x/cellbuf v0.0.13 // indirect + github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect + github.com/charmbracelet/x/term v0.2.1 // indirect + github.com/clipperhouse/uax29/v2 v2.2.0 // indirect + github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dlclark/regexp2 v1.11.0 // indirect + github.com/fatih/color v1.18.0 // indirect + github.com/getsentry/sentry-go v0.27.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/css v1.0.1 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/microcosm-cc/bluemonday v1.0.27 // indirect + github.com/muesli/reflow v0.3.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect + github.com/sourcegraph/beaut v0.0.0-20240611013027-627e4c25335a // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + github.com/yuin/goldmark v1.7.8 // indirect + github.com/yuin/goldmark-emoji v1.0.5 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel/metric v1.43.0 // indirect + go.opentelemetry.io/otel/trace v1.43.0 // indirect + go.uber.org/atomic v1.11.0 // indirect + go.uber.org/goleak v1.3.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.24.0 // indirect + golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 // indirect + golang.org/x/net v0.44.0 // indirect + golang.org/x/text v0.29.0 // indirect + golang.org/x/tools v0.37.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect +) + +// See: https://github.com/ghodss/yaml/pull/65 +replace github.com/ghodss/yaml => github.com/sourcegraph/yaml v1.0.1-0.20200714132230-56936252f152 diff --git a/lib/output/capabilities.go b/lib/output/capabilities.go index 1bca2bd56e..f789104ecd 100644 --- a/lib/output/capabilities.go +++ b/lib/output/capabilities.go @@ -82,13 +82,7 @@ func detectCapabilities(opts OutputOpts) (caps capabilities, err error) { // detect color mode caps.Color = opts.ForceColor if !opts.ForceColor { - // If ForceTTY is explicitly false, we also disable colors since non-TTY - // mode shouldn't have colors (and detection might be affected by env vars). - if opts.ForceTTY != nil && !*opts.ForceTTY { - caps.Color = false - } else { - caps.Color = detectColor(caps.Isatty) - } + caps.Color = detectColor(caps.Isatty) } // set detected background color diff --git a/lib/output/emoji.go b/lib/output/emoji.go index b0c8740140..469e9eccb8 100644 --- a/lib/output/emoji.go +++ b/lib/output/emoji.go @@ -17,9 +17,9 @@ var allEmojis = [...]string{ // Standard emoji for use in output. const ( - EmojiFailure = " ⨯" + EmojiFailure = "❌" EmojiWarning = "❗️" - EmojiSuccess = " ✓" + EmojiSuccess = "✅" EmojiInfo = "ℹ️" EmojiLightbulb = "💡" EmojiAsterisk = "✱" diff --git a/lib/output/line.go b/lib/output/line.go index 2fdc21a28b..ed566d0d8b 100644 --- a/lib/output/line.go +++ b/lib/output/line.go @@ -64,12 +64,11 @@ func (fl FancyLine) write(w io.Writer, caps capabilities) { if fl.Prefix != "" { fmt.Fprint(w, fl.Prefix+" ") } - emojiPrefix := "" if fl.emoji != "" { - emojiPrefix = fl.emoji + " " + fmt.Fprint(w, fl.emoji+" ") } - fmt.Fprintf(w, "%s"+emojiPrefix+fl.format+"%s", caps.formatArgs(append(append([]any{fl.style}, fl.args...), StyleReset))...) + fmt.Fprintf(w, "%s"+fl.format+"%s", caps.formatArgs(append(append([]any{fl.style}, fl.args...), StyleReset))...) if fl.Prompt { // Add whitespace for user input _, _ = w.Write([]byte(" ")) diff --git a/lib/output/output.go b/lib/output/output.go index 93e0f4306a..f1d1305277 100644 --- a/lib/output/output.go +++ b/lib/output/output.go @@ -92,7 +92,7 @@ var newOutputPlatformQuirks func(o *Output) error // newCapabilityWatcher returns a channel that receives a message when // capabilities are updated. By default, no watching functionality is // available. -var newCapabilityWatcher = func(_ OutputOpts) chan capabilities { return nil } +var newCapabilityWatcher = func(opts OutputOpts) chan capabilities { return nil } func NewOutput(w io.Writer, opts OutputOpts) *Output { // Not being able to detect capabilities is alright. It might mean output will look @@ -146,17 +146,6 @@ func (o *Output) UnsetVerbose() { o.verbose = false } -func (o *Output) IsVerbose() bool { - o.lock.Lock() - defer o.lock.Unlock() - return o.verbose -} - -// IsTTY reports whether this output is configured to render to a TTY. -func (o *Output) IsTTY() bool { - return o.caps.Isatty -} - func (o *Output) Unlock() { if o.caps.Isatty { // Show the cursor once more. diff --git a/lib/output/output_unix.go b/lib/output/output_unix.go index 11c2a143d5..f55b2003b0 100644 --- a/lib/output/output_unix.go +++ b/lib/output/output_unix.go @@ -1,4 +1,5 @@ //go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris +// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris package output diff --git a/lib/output/pending_tty.go b/lib/output/pending_tty.go index c16258bce2..86853edc4a 100644 --- a/lib/output/pending_tty.go +++ b/lib/output/pending_tty.go @@ -95,11 +95,7 @@ func (p *pendingTTY) Complete(message FancyLine) { p.o.moveUp(1) p.o.clearCurrentLine() - // Write directly without truncation (unlike p.write, which truncates to - // terminal width to keep spinner animation frames on a single line). - // Completion messages may contain multi-line output such as URLs and must - // be displayed in full. - message.write(p.o.w, p.o.caps) + p.write(message) } func (p *pendingTTY) Close() { p.Destroy() } diff --git a/lib/output/progress.go b/lib/output/progress.go index 6490666656..2b7d9688a4 100644 --- a/lib/output/progress.go +++ b/lib/output/progress.go @@ -34,11 +34,9 @@ type ProgressOpts struct { SuccessEmoji string SuccessStyle Style - // NoSpinner disables the background goroutine that updates progress bars - // and spinners. In TTY mode this stops the spinner animation; in non-TTY - // (simple) mode this suppresses the periodic ticker that dumps progress - // bars, which is useful when output cannot be overwritten in place (e.g. - // CI log files). Completion and failure events still print. + // NoSpinner turns of the automatic updating of the progress bar and + // spinner in a background goroutine. + // Used for testing only! NoSpinner bool } diff --git a/lib/output/progress_simple.go b/lib/output/progress_simple.go index 6b55683f49..dadfc3086c 100644 --- a/lib/output/progress_simple.go +++ b/lib/output/progress_simple.go @@ -33,9 +33,6 @@ func (p *progressSimple) SetValue(i int, v float64) { } func (p *progressSimple) stop() { - if p.done == nil { - return - } c := make(chan struct{}) p.done <- c <-c @@ -49,7 +46,9 @@ func newProgressSimple(bars []*ProgressBar, o *Output, opts *ProgressOpts) *prog } if opts != nil && opts.NoSpinner { - p.done = nil + if p.Output.verbose { + writeBars(p.Output, p.bars) + } return p } @@ -63,6 +62,7 @@ func newProgressSimple(bars []*ProgressBar, o *Output, opts *ProgressOpts) *prog if p.Output.verbose { writeBars(p.Output, p.bars) } + case c := <-p.done: c <- struct{}{} return diff --git a/lib/output/progress_tty.go b/lib/output/progress_tty.go index 0f7421d1e9..f10dc18921 100644 --- a/lib/output/progress_tty.go +++ b/lib/output/progress_tty.go @@ -139,11 +139,13 @@ func newProgressTTY(bars []*ProgressBar, o *Output, opts *ProgressOpts) *progres o: o, emojiWidth: 3, pendingEmoji: spinnerStrings[0], + spinner: newSpinner(100 * time.Millisecond), } - p.opts = *DefaultProgressTTYOpts if opts != nil { - p.opts.NoSpinner = opts.NoSpinner + p.opts = *opts + } else { + p.opts = *DefaultProgressTTYOpts } if w := runewidth.StringWidth(p.opts.SuccessEmoji); w > p.emojiWidth { @@ -161,8 +163,6 @@ func newProgressTTY(bars []*ProgressBar, o *Output, opts *ProgressOpts) *progres return p } - p.spinner = newSpinner(100 * time.Millisecond) - go func() { for s := range p.spinner.C { func() { diff --git a/lib/output/progress_with_status_bars_simple.go b/lib/output/progress_with_status_bars_simple.go index b06be8dcca..e985c2e952 100644 --- a/lib/output/progress_with_status_bars_simple.go +++ b/lib/output/progress_with_status_bars_simple.go @@ -59,7 +59,10 @@ func newProgressWithStatusBarsSimple(bars []*ProgressBar, statusBars []*StatusBa } if opts != nil && opts.NoSpinner { - p.done = nil + if p.Output.verbose { + writeBars(p.Output, p.bars) + writeStatusBars(p.Output, p.statusBars) + } return p } @@ -74,6 +77,7 @@ func newProgressWithStatusBarsSimple(bars []*ProgressBar, statusBars []*StatusBa writeBars(p.Output, p.bars) writeStatusBars(p.Output, p.statusBars) } + case c := <-p.done: c <- struct{}{} return diff --git a/lib/output/progress_with_status_bars_tty.go b/lib/output/progress_with_status_bars_tty.go index 254eeac007..3cfcf5834f 100644 --- a/lib/output/progress_with_status_bars_tty.go +++ b/lib/output/progress_with_status_bars_tty.go @@ -14,13 +14,15 @@ func newProgressWithStatusBarsTTY(bars []*ProgressBar, statusBars []*StatusBar, o: o, emojiWidth: 3, pendingEmoji: spinnerStrings[0], + spinner: newSpinner(100 * time.Millisecond), }, statusBars: statusBars, } - p.opts = *DefaultProgressTTYOpts if opts != nil { - p.opts.NoSpinner = opts.NoSpinner + p.opts = *opts + } else { + p.opts = *DefaultProgressTTYOpts } if w := runewidth.StringWidth(p.opts.SuccessEmoji); w > p.emojiWidth { @@ -39,8 +41,6 @@ func newProgressWithStatusBarsTTY(bars []*ProgressBar, statusBars []*StatusBar, return p } - p.spinner = newSpinner(100 * time.Millisecond) - go func() { for s := range p.spinner.C { func() { @@ -220,6 +220,7 @@ func calcStatusBarLabelWidth(statusBars []*StatusBar, termWidth int) int { } return statusBarLabelWidth + } func (p *progressWithStatusBarsTTY) writeStatusBar(last bool, statusBar *StatusBar) { diff --git a/lib/output/spinner.go b/lib/output/spinner.go index 23220224df..b0b5b86de2 100644 --- a/lib/output/spinner.go +++ b/lib/output/spinner.go @@ -41,9 +41,6 @@ func newSpinner(interval time.Duration) *spinner { } func (s *spinner) stop() { - if s == nil { - return - } c := make(chan struct{}) s.done <- c <-c diff --git a/lib/output/table.go b/lib/output/table.go index 8388ed5ee6..6736b30756 100644 --- a/lib/output/table.go +++ b/lib/output/table.go @@ -6,7 +6,9 @@ import ( ) const ( - purple = lipgloss.Color("99") + purple = lipgloss.Color("99") + gray = lipgloss.Color("245") + lightGray = lipgloss.Color("241") ) type Table struct { @@ -41,6 +43,10 @@ func (t *Table) AddRow(elems ...string) { t.tbl.Row(elems...) } +func (t *Table) AddRows(elems ...[]string) { + t.tbl.Rows(elems...) +} + func (t *Table) Render() string { return t.tbl.Render() } diff --git a/lib/process/pipe.go b/lib/process/pipe.go index b06364cc44..b7a6f9a40a 100644 --- a/lib/process/pipe.go +++ b/lib/process/pipe.go @@ -15,11 +15,9 @@ import ( // initialBufSize is the initial size of the buffer that PipeOutput uses to // read lines. -const ( - initialBufSize = 4 * 1024 // 4k - // maxTokenSize is the max size of a token that PipeOutput reads. - maxTokenSize = 100 * 1024 * 1024 // 100mb -) +const initialBufSize = 4 * 1024 // 4k +// maxTokenSize is the max size of a token that PipeOutput reads. +const maxTokenSize = 100 * 1024 * 1024 // 100mb type pipe func(w io.Writer, r io.Reader) error From d8f277d7cb228a6b62a94fb2e36535de01a16c74 Mon Sep 17 00:00:00 2001 From: bahrmichael Date: Mon, 1 Jun 2026 14:10:50 +0200 Subject: [PATCH 8/8] clean up --- internal/batches/service/service_test.go | 29 ------------------------ 1 file changed, 29 deletions(-) diff --git a/internal/batches/service/service_test.go b/internal/batches/service/service_test.go index aa166bf2af..b341ab8aeb 100644 --- a/internal/batches/service/service_test.go +++ b/internal/batches/service/service_test.go @@ -266,35 +266,6 @@ some-new-field: Foo bar `, expectedErr: errors.New("parsing batch spec: Additional property some-new-field is not allowed"), }, - { - name: "step image alias", - rawSpec: ` -name: test-spec -description: A test spec -steps: - - run: echo hi - image: alpine:3 -changesetTemplate: - title: Test - body: Test - branch: test - commit: - message: Test -`, - expectedSpec: &batcheslib.BatchSpec{ - Name: "test-spec", - Description: "A test spec", - Steps: []batcheslib.Step{ - {Run: "echo hi", Container: "alpine:3", Image: "alpine:3"}, - }, - ChangesetTemplate: &batcheslib.ChangesetTemplate{ - Title: "Test", - Body: "Test", - Branch: "test", - Commit: batcheslib.ExpandedGitCommitDescription{Message: "Test"}, - }, - }, - }, { name: "supported version", rawSpec: `