Skip to content

Commit 166b864

Browse files
committed
Address CI log grouping review feedback
1 parent 84b24e9 commit 166b864

7 files changed

Lines changed: 110 additions & 40 deletions

File tree

cmd/cmd_utils.go

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -879,10 +879,16 @@ func executeCustomCommand(
879879
log.Debug("Prepared environment with identity for custom command step", customCommandKeyIdentity, commandIdentity, customCommandKeyCommand, commandConfig.Name, "step", i)
880880
}
881881

882-
// Whenever grouping is enabled (any mode), mark the subprocess environment
883-
// so a nested `atmos` invocation skips its own grouping — CI providers do
884-
// not support nested groups.
885-
if ci.GroupingEnabled(&atmosConfig) {
882+
// Determine step type - default to shell if not specified.
883+
stepType := strings.TrimSpace(step.Type)
884+
if stepType == "" {
885+
stepType = "shell"
886+
}
887+
888+
// If this step will be enclosed in a CI log group, mark the subprocess
889+
// environment so a nested `atmos` invocation skips unsupported nested
890+
// grouping.
891+
if stepType != schema.TaskTypeExec && ci.ShouldPropagateLogGroupSentinel(&atmosConfig, ci.DimensionStep) {
886892
env = append(env, ci.LogGroupSentinelEnv())
887893
}
888894

@@ -891,12 +897,6 @@ func executeCustomCommand(
891897
commandToRun, err := e.ProcessTmpl(&atmosConfig, fmt.Sprintf("step-%d", i), step.Command, data, false)
892898
errUtils.CheckErrorPrintAndExit(err, "", "")
893899

894-
// Determine step type - default to shell if not specified.
895-
stepType := strings.TrimSpace(step.Type)
896-
if stepType == "" {
897-
stepType = "shell"
898-
}
899-
900900
// Execute the step based on type.
901901
//
902902
// shell/exec/atmos use the legacy, cross-platform, child-reaping paths
@@ -907,9 +907,10 @@ func executeCustomCommand(
907907
// processes on Linux (no process-group cleanup), so they stay on the
908908
// legacy paths.
909909
//
910-
// The whole dispatch is wrapped in a collapsible CI log group (no-op
911-
// outside CI / when disabled), labeled with the step name or command.
912-
err = stepPkg.RunGrouped(&atmosConfig, step.Name, commandToRun, func() error {
910+
// The dispatch is wrapped in a collapsible CI log group (no-op outside
911+
// CI / when disabled), labeled with the step name or command. Exec steps
912+
// run bare because a successful Unix exec never returns to close a group.
913+
err = stepPkg.RunGroupedForType(&atmosConfig, step.Name, commandToRun, stepType, func() error {
913914
switch stepType {
914915
case "shell":
915916
// Execute shell command (backward compatible).

internal/exec/workflow_utils.go

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -436,15 +436,6 @@ func ExecuteWorkflow(
436436
workDir = "."
437437
}
438438

439-
// Whenever grouping is enabled (any mode), mark the subprocess environment
440-
// so a nested `atmos` invocation skips its own grouping — CI providers do
441-
// not support nested groups. This applies even in invocation mode (where
442-
// this step itself does not open a group): the parent's invocation group
443-
// must stay flat.
444-
if ci.GroupingEnabled(&atmosConfig) {
445-
stepEnv = append(stepEnv, ci.LogGroupSentinelEnv())
446-
}
447-
448439
// Clear progress line and re-render as permanent record before step execution.
449440
// This ensures progress line appears as header, then step output below it.
450441
if progressRenderer.IsEnabled() {
@@ -457,15 +448,26 @@ func ExecuteWorkflow(
457448
if commandType != "shell" && commandType != schema.TaskTypeExec && commandType != "atmos" && !stepPkg.IsExtendedStepType(commandType) {
458449
return errUtils.Build(errUtils.ErrInvalidWorkflowStepType).
459450
WithTitle(WorkflowErrTitle).
451+
WithExplanationf("Workflow `%s` step `%s` uses unsupported type `%s`.", workflow, step.Name, commandType).
452+
WithContext("workflow", workflow).
453+
WithContext("step", step.Name).
460454
WithHintf("Step type '%s' is not supported", commandType).
461455
WithHint("Each step must specify a valid type: 'atmos', 'shell', 'exec', or an interactive type like 'input', 'confirm', 'choose'").
462456
WithExitCode(1).
463457
Err()
464458
}
465459

460+
// If this step will be enclosed in a CI log group, mark the subprocess
461+
// environment so a nested `atmos` invocation skips unsupported nested
462+
// grouping.
463+
if commandType != schema.TaskTypeExec && ci.ShouldPropagateLogGroupSentinel(&atmosConfig, ci.DimensionStep) {
464+
stepEnv = append(stepEnv, ci.LogGroupSentinelEnv())
465+
}
466+
466467
// Wrap each step's output in a collapsible CI log group when grouping is
467-
// active. RunGrouped is a no-op outside CI / when disabled.
468-
err = stepPkg.RunGrouped(&atmosConfig, step.Name, command, func() error {
468+
// active. Exec steps run bare because a successful Unix exec never returns
469+
// to close a deferred group.
470+
err = stepPkg.RunGroupedForType(&atmosConfig, step.Name, command, commandType, func() error {
469471
switch commandType {
470472
case "shell":
471473
// Render command before execution if show.command is enabled.

pkg/ci/loggroup.go

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,8 @@ var logGroupOut io.Writer = os.Stdout
6868
var logGroupDepth int32
6969

7070
// LogGroupSentinelEnv returns the "KEY=VALUE" environment entry that
71-
// orchestrators append to a step/command subprocess's environment while
72-
// grouping is enabled, so nested `atmos` invocations skip re-grouping. Callers
73-
// append it only when GroupingEnabled reports true.
71+
// orchestrators append to a step/command subprocess's environment while that
72+
// subprocess is inside, or is about to be inside, a CI log group.
7473
func LogGroupSentinelEnv() string {
7574
defer perf.Track(nil, "ci.LogGroupSentinelEnv")()
7675

@@ -132,21 +131,32 @@ func grouper(atmosConfig *schema.AtmosConfiguration) (provider.LogGrouper, bool)
132131
return lg, true
133132
}
134133

135-
// GroupingEnabled reports whether CI log grouping is active for this run in any
136-
// dimension: a grouping mode other than "off" is configured, a grouping-capable
137-
// provider is detected, and no parent Atmos process already has grouping open.
138-
//
139-
// Orchestrators use it to decide whether to append LogGroupSentinelEnv to a
140-
// child subprocess's environment — which must happen whenever grouping is
141-
// enabled, regardless of which dimension the current command emits, so that a
142-
// nested `atmos` invocation never emits its own (nested) groups.
134+
// GroupingEnabled reports whether CI log grouping is available for this run in
135+
// any dimension: a grouping mode other than "off" is configured, a
136+
// grouping-capable provider is detected, and no parent Atmos process already has
137+
// grouping open.
143138
func GroupingEnabled(atmosConfig *schema.AtmosConfiguration) bool {
144139
defer perf.Track(nil, "ci.GroupingEnabled")()
145140

146141
_, ok := grouper(atmosConfig)
147142
return ok
148143
}
149144

145+
// ShouldPropagateLogGroupSentinel reports whether a subprocess launched at dim
146+
// should inherit LogGroupSentinelEnv. It is true when a group is already open in
147+
// this Atmos process, or when the configured mode/provider would open a group at
148+
// dim. This keeps child Atmos invocations from suppressing their own grouping
149+
// unless a parent group actually exists or will be opened for this boundary.
150+
func ShouldPropagateLogGroupSentinel(atmosConfig *schema.AtmosConfiguration, dim Dimension) bool {
151+
defer perf.Track(nil, "ci.ShouldPropagateLogGroupSentinel")()
152+
153+
if atomic.LoadInt32(&logGroupDepth) > 0 {
154+
return true
155+
}
156+
_, ok := grouper(atmosConfig)
157+
return ok && dimensionActive(resolveGroupMode(atmosConfig), dim)
158+
}
159+
150160
// Group runs fn wrapped in the detected CI provider's log-group markers, named
151161
// `name`, when the configured mode selects the given dimension; otherwise it
152162
// simply calls fn. The group-end marker is always emitted (even when fn returns

pkg/ci/loggroup_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,42 @@ func TestGroupingEnabled(t *testing.T) {
211211
})
212212
}
213213

214+
func TestShouldPropagateLogGroupSentinel(t *testing.T) {
215+
tests := []struct {
216+
name string
217+
mode string
218+
dim Dimension
219+
want bool
220+
}{
221+
{name: "auto step boundary will group", mode: GroupModeAuto, dim: DimensionStep, want: true},
222+
{name: "auto phase boundary will group", mode: GroupModeAuto, dim: DimensionPhase, want: true},
223+
{name: "auto invocation boundary will not group", mode: GroupModeAuto, dim: DimensionInvocation, want: false},
224+
{name: "invocation step boundary will not group by itself", mode: GroupModeInvocation, dim: DimensionStep, want: false},
225+
{name: "invocation boundary will group", mode: GroupModeInvocation, dim: DimensionInvocation, want: true},
226+
{name: "off boundary will not group", mode: GroupModeOff, dim: DimensionStep, want: false},
227+
}
228+
229+
for _, tt := range tests {
230+
t.Run(tt.name, func(t *testing.T) {
231+
registerGrouping(t)
232+
assert.Equal(t, tt.want, ShouldPropagateLogGroupSentinel(modeConfig(tt.mode), tt.dim))
233+
})
234+
}
235+
}
236+
237+
func TestShouldPropagateLogGroupSentinel_WhenGroupAlreadyOpen(t *testing.T) {
238+
registerGrouping(t)
239+
var buf bytes.Buffer
240+
setLogGroupOut(t, &buf)
241+
242+
err := Group(modeConfig(GroupModeInvocation), DimensionInvocation, "atmos workflow deploy", func() error {
243+
assert.True(t, ShouldPropagateLogGroupSentinel(modeConfig(GroupModeInvocation), DimensionStep))
244+
return nil
245+
})
246+
247+
require.NoError(t, err)
248+
}
249+
214250
func TestLogGroupSentinelEnv(t *testing.T) {
215251
assert.Equal(t, "ATMOS_CI_LOG_GROUP_ACTIVE=1", LogGroupSentinelEnv())
216252
}

pkg/runner/step/loggroup.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,18 @@ func RunGrouped(atmosConfig *schema.AtmosConfiguration, name, command string, fn
2222
return ci.Group(atmosConfig, ci.DimensionStep, groupLabel(name, command), fn)
2323
}
2424

25+
// RunGroupedForType is RunGrouped for ordinary step types, but deliberately
26+
// skips grouping for exec steps. Exec steps may replace the Atmos process on
27+
// Unix, so a deferred group close would never run after a successful handoff.
28+
func RunGroupedForType(atmosConfig *schema.AtmosConfiguration, name, command, stepType string, fn func() error) error {
29+
defer perf.Track(nil, "step.RunGroupedForType")()
30+
31+
if strings.TrimSpace(stepType) == schema.TaskTypeExec {
32+
return fn()
33+
}
34+
return RunGrouped(atmosConfig, name, command, fn)
35+
}
36+
2537
// groupLabel picks the human-facing group label: the step name when present,
2638
// otherwise the resolved command.
2739
func groupLabel(name, command string) string {

pkg/runner/step/loggroup_test.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,12 @@ func TestRunGrouped_PassthroughWhenInactive(t *testing.T) {
4444
}))
4545
assert.True(t, called)
4646
}
47+
48+
func TestRunGroupedForType_ExecRunsBare(t *testing.T) {
49+
called := false
50+
require.NoError(t, RunGroupedForType(&schema.AtmosConfiguration{}, "name", "command", schema.TaskTypeExec, func() error {
51+
called = true
52+
return nil
53+
}))
54+
assert.True(t, called)
55+
}

pkg/workflow/executor.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -199,10 +199,10 @@ func (e *Executor) runSteps(params *WorkflowParams, steps []schema.WorkflowStep,
199199
}
200200

201201
// Wrap each step's output in a collapsible CI log group when grouping is
202-
// active. RunGrouped is a no-op outside CI / when disabled, so executeStep
203-
// runs unchanged in the common case.
202+
// active. Exec steps run bare because a successful Unix exec never returns
203+
// to close a deferred group.
204204
var stepResult stepResultInternal
205-
_ = stepPkg.RunGrouped(params.AtmosConfig, step.Name, step.Command, func() error {
205+
_ = stepPkg.RunGroupedForType(params.AtmosConfig, step.Name, step.Command, step.Type, func() error {
206206
stepResult = e.executeStep(params, step, stepIdx)
207207
return nil
208208
})
@@ -337,8 +337,8 @@ func (e *Executor) executeStep(params *WorkflowParams, step *schema.WorkflowStep
337337

338338
// When this step's output is wrapped in a CI log group (see runSteps), mark
339339
// the subprocess environment so a nested `atmos workflow`/custom-command
340-
// invocation skips re-grouping (CI providers do not support nested groups).
341-
if ci.GroupingEnabled(params.AtmosConfig) {
340+
// invocation skips unsupported nested grouping.
341+
if commandType != schema.TaskTypeExec && ci.ShouldPropagateLogGroupSentinel(params.AtmosConfig, ci.DimensionStep) {
342342
stepEnv = append(stepEnv, ci.LogGroupSentinelEnv())
343343
}
344344

0 commit comments

Comments
 (0)