Skip to content

fix: populate scope with empty values for outputs of skipped/omitted …#15932

Open
KowalczykBartek wants to merge 7 commits intoargoproj:mainfrom
KowalczykBartek:fix-unresolved-reference-for-child-main-branch-signed-verified
Open

fix: populate scope with empty values for outputs of skipped/omitted …#15932
KowalczykBartek wants to merge 7 commits intoargoproj:mainfrom
KowalczykBartek:fix-unresolved-reference-for-child-main-branch-signed-verified

Conversation

@KowalczykBartek
Copy link
Copy Markdown

Fixes #15873
Motivation

Try to resolve child's output when steps is actually skipped.

Copy link
Copy Markdown

@utafrali utafrali left a comment

Choose a reason for hiding this comment

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

The core logic is sound: Template.Outputs is a value type so there is no nil dereference risk, and the empty-string scope entries correctly unblock downstream when expressions that reference skipped step outputs. The main gaps are missing artifact output handling (a real failure mode for artifact references), shadowed variable names, and a silently dropped error from ResolveTemplate.

Comment thread workflow/controller/steps.go Outdated
woc.buildLocalScope(stepsCtx.scope, prefix, childNode)

if (childNode.Phase == wfv1.NodeSkipped || childNode.Phase == wfv1.NodeOmitted) && childNode.Outputs == nil {
_, tmpl, _, err := stepsCtx.tmplCtx.ResolveTemplate(ctx, &step)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

:= here shadows two outer-scoped names: the tmpl parameter of executeSteps and the loop-level err. While the inner block scope keeps this from being a correctness bug, shadow linters (and code readers) will flag it. Use distinct names:

stepTmpl, resolveErr := stepsCtx.tmplCtx.ResolveTemplate(ctx, &step)
if resolveErr == nil && stepTmpl != nil {

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

applied

Comment thread workflow/controller/steps.go Outdated

if (childNode.Phase == wfv1.NodeSkipped || childNode.Phase == wfv1.NodeOmitted) && childNode.Outputs == nil {
_, tmpl, _, err := stepsCtx.tmplCtx.ResolveTemplate(ctx, &step)
if err == nil && tmpl != nil {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

When ResolveTemplate fails the error is silently dropped and the scope remains unpopulated, so a downstream step will still fail with an unresolvable reference. The caller gets no indication that template resolution was attempted and failed. A debug log here would make production troubleshooting much easier:

if resolveErr != nil {
    woc.log.WithError(resolveErr).Debug(ctx, "failed to resolve template for skipped step, outputs will not be populated in scope")
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

applied

}
if tmpl.Outputs.Result != nil {
stepsCtx.scope.addParamToScope(fmt.Sprintf("%s.outputs.result", prefix), "")
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The fix handles Parameters and Result but skips Artifacts. If any downstream step (or when expression) references steps.<skipped>.outputs.artifacts.<name>, it will still fail with an unresolvable reference. addArtifactToScope accepts a wfv1.Artifact value, so you can register a zero-value placeholder:

for _, artifact := range tmpl.Outputs.Artifacts {
    key := fmt.Sprintf("%s.outputs.artifacts.%s", prefix, artifact.Name)
    stepsCtx.scope.addArtifactToScope(key, wfv1.Artifact{})
}

This mirrors how the parameter case is handled above.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

applied

})
}

func (s *FunctionalSuite) TestStepsSkippedOutputRef() {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The test exercises NodeSkipped but never NodeOmitted. Both phases are handled in the fix. Since Omitted is a distinct node phase that arises in DAG templates (not steps), it may require a separate workflow fixture, but at least a comment explaining why it isn't tested would clarify the intent.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Hi, sorry for that, but honestly its my first time I wrote golang and did something in argo -

why it isn't tested would clarify the intent.

honestly - I just fixed the case which blocks us, that's the reason, I can try to fix other cases as well but this can take some time.

If its enough to write comment, can you please sketch some comment I can paste ? I will probably not create anything meaningful :/

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

@utafrali I pushed following comment on that test

// TestStepsSkippedOutputRef tests that output references to skipped steps resolve to empty strings
// rather than causing an unresolvable reference error. NodeOmitted is not exercised here because
// that phase arises in DAG templates, not steps; the fix covers both phases in the same code path.

@KowalczykBartek
Copy link
Copy Markdown
Author

KowalczykBartek commented Apr 15, 2026

@utafrali thank you for review 🙇 - I applied what I could and tested it, I have question only regarding the comment for test.

@KowalczykBartek
Copy link
Copy Markdown
Author

@isubasinghe is there a chance to push on that ?

@isubasinghe
Copy link
Copy Markdown
Member

Hey could you please lint and rebase this? I really want to get this merged.

@KowalczykBartek
Copy link
Copy Markdown
Author

@isubasinghe lint ?

@isubasinghe
Copy link
Copy Markdown
Member

…steps (argoproj#15873)

Signed-off-by: KowalczykBartek <bkowalczyyk@gmail.com>
Signed-off-by: KowalczykBartek <bkowalczyyk@gmail.com>
Signed-off-by: KowalczykBartek <bkowalczyyk@gmail.com>
Signed-off-by: KowalczykBartek <bkowalczyyk@gmail.com>
@KowalczykBartek KowalczykBartek force-pushed the fix-unresolved-reference-for-child-main-branch-signed-verified branch from 0ae31ea to 421e737 Compare April 26, 2026 08:01
@KowalczykBartek
Copy link
Copy Markdown
Author

KowalczykBartek commented Apr 26, 2026

I have no idea what I just did - @isubasinghe pls check if now its ok

edit: lint is green now

@isubasinghe
Copy link
Copy Markdown
Member

Hmm an end to end test is broken according to the logs.

@KowalczykBartek
Copy link
Copy Markdown
Author

@isubasinghe i wrote one test it pass

Condition "to be done" met after 19s
Checking expectation steps-skipped-output-ref-mt7t9
steps-skipped-output-ref-mt7t9 : Succeeded 
=== PASS: FunctionalSuite/TestStepsSkippedOutputRef
=== SLOW TEST:  FunctionalSuite/TestStepsSkippedOutputRef took 19s
--- PASS: TestFunctionalSuite (19.55s)
    --- PASS: TestFunctionalSuite/TestStepsSkippedOutputRef (19.53s)
PASS
ok  	github.com/argoproj/argo-workflows/v4/test/e2e	20.288s

not sure what i can do

@isubasinghe
Copy link
Copy Markdown
Member

https://github.com/argoproj/argo-workflows/actions/runs/24951771817/job/73307331648?pr=15932

This is failing though, please do check the Github checks.

Are you interacting with this via CLI or not via the Github UI?

Can you see this bit?
image

@KowalczykBartek
Copy link
Copy Markdown
Author

ok now I see :/ let me check

Signed-off-by: KowalczykBartek <bkowalczyyk@gmail.com>
@KowalczykBartek
Copy link
Copy Markdown
Author

@isubasinghe do I see correctly it succeeded now or it need to be forced to run ?

@KowalczykBartek
Copy link
Copy Markdown
Author

@isubasinghe any update ?

Comment thread workflow/controller/steps.go Outdated
} else {
woc.buildLocalScope(stepsCtx.scope, prefix, childNode)

if (childNode.Phase == wfv1.NodeSkipped || childNode.Phase == wfv1.NodeOmitted) && childNode.Outputs == nil {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't quite understand why there is this nil check here.
If a node has been set to skipped or omitted, the outputs should be nil, this not being the case is arguably a mistake?

I believe the default param population happens in line 198.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

done

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Oh no I was just asking you why you added this check.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

why you added this check.

honestly - i don't know :) I just wanted to craft something asap, i have little domain knowledge around that codebase, but you are probably right, if node is skipped or omitted it doesn't matter what is the output we will override it.

Comment thread workflow/controller/steps.go Outdated
woc.log.WithError(resolveErr).Debug(ctx, "failed to resolve template for skipped step, outputs will not be populated in scope")
}

if resolveErr == nil && tmpl != nil {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Did you mean to use stepTmpl here?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

done

Comment thread workflow/controller/steps.go Outdated
stepsCtx.scope.addParamToScope(key, "")
}
// register a zero-value placeholder for artifacts
for _, artifact := range tmpl.Outputs.Artifacts {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Same here, did you mean to use stepTmpl?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

done

Comment on lines +3317 to 3320
} else if val == "" && param.ValueFrom.Default != nil {
// A skipped/omitted step contributes "" to scope; treat that as missing and use the default
val = param.ValueFrom.Default.String()
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Revert this change, val could validly be "", we shouldn't override it in that case.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

done

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

@isubasinghe after this change tests fails

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Okay I think this means we need a way to determine "" from this yielded "" and this was omitted/skipped.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

so I reversed it - is that ok ?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Signed-off-by: KowalczykBartek <bkowalczyyk@gmail.com>
Signed-off-by: KowalczykBartek <bkowalczyyk@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Workflow hangs when 'when' clause references skipped step outputs

3 participants