Skip to content

Composite Action Step Markers#4243

Draft
ericsciple wants to merge 2 commits intomainfrom
users/ericsciple/26-02-composite-markers
Draft

Composite Action Step Markers#4243
ericsciple wants to merge 2 commits intomainfrom
users/ericsciple/26-02-composite-markers

Conversation

@ericsciple
Copy link
Collaborator

Summary

Emit ##[start-action] / ##[end-action] markers in the log stream around each nested step inside a composite action. The UI parses these markers to render collapsible regions, giving users visibility into individual steps that were previously hidden in a single opaque log blob.

Design

The runner writes ##[ markers directly to the log stream via ExecutionContext.Output(), bypassing the logging command pipeline. No new :: logging command is registered. Users cannot emit these markers from scripts.

Marker format

##[start-action display=<step-display-name>;id=<step-id>]

... step output ...

##[end-action id=<step-id>;outcome=<result>;conclusion=<result>;duration_ms=<ms>]
  • id — the step's ContextName (from YAML id: or auto-generated with __ prefix)
  • outcome — raw result before continue-on-error is applied
  • conclusion — final result after continue-on-error
  • duration_ms — wall-clock milliseconds (0 for skipped steps)

Nested composites use dot-separated IDs (e.g. outer.inner-step) to keep each step globally unique.

Feature flag

Gated behind actions_runner_emit_composite_markers (job message variable) with ACTIONS_RUNNER_EMIT_COMPOSITE_MARKERS env var fallback (for internal testing only).

Injection prevention

OutputManager.OnDataReceived replaces ##[start-action and ##[end-action from user process stdout/stderr with ##[\start-action and ##[\end-action. This preventing users from injecting fake markers. The runner's own markers bypass OutputManager entirely since they're written via ExecutionContext.Output() directly.

Copy link
Contributor

@ChristopherHX ChristopherHX left a comment

Choose a reason for hiding this comment

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

I know this is a draft and I am not part of GitHub, tested this interesting thing anyway

// Emit start marker before condition evaluation
if (emitCompositeMarkers)
{
ExecutionContext.Output($"##[start-action display={EscapeProperty(SanitizeDisplayName(step.DisplayName))};id={EscapeProperty(stepId)}]");
Copy link
Contributor

Choose a reason for hiding this comment

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

display Name is wrong in composite even if not using expressions, this value is updated later again as this may change if this depends on a changing context variable:

(Logs generated with custom cli tool + env variable)

Currently looks like in the log the displayname is not the expected one
[.github/workflows/test.yml / _] Running: Run ./
| Prepare all required actions
| ##[group]Run ./
| ##[endgroup]
| ##[start-action display=run;id=__run]
| ##[group]Run echo "Hello, World"
| echo "Hello, World"
| shell: /bin/bash --noprofile --norc -e -o pipefail {0}
| ##[endgroup]
| Hello, World
| ##[end-action id=__run;outcome=success;conclusion=success;duration_ms=44]
| ##[start-action display=run;id=__run_2]
| ##[group]Run echo "Hello, World"
| echo "Hello, World"
| shell: /bin/bash --noprofile --norc -e -o pipefail {0}
| ##[endgroup]
| Hello, World
| ##[end-action id=__run_2;outcome=success;conclusion=success;duration_ms=37]
[.github/workflows/test.yml / _] Succeeded: Run ./
Action
runs:
  using: composite
  steps:
  - name: Hello World
    run: echo "Hello, World"
    shell: bash
  - run: echo "Hello, World"
    shell: bash

Minimally this is required for this to be acceptable for me as an actions user.

Suggested change
ExecutionContext.Output($"##[start-action display={EscapeProperty(SanitizeDisplayName(step.DisplayName))};id={EscapeProperty(stepId)}]");
step.TryUpdateDisplayName(out _);
ExecutionContext.Output($"##[start-action display={EscapeProperty(SanitizeDisplayName(step.DisplayName))};id={EscapeProperty(stepId)}]");
After Constant Hello World is the display name or the fallback
[.github/workflows/test.yml / _] Running: Run ./
| Prepare all required actions
| ##[group]Run ./
| ##[endgroup]
| ##[start-action display=Hello World;id=__run]
| ##[group]Run echo "Hello, World"
| echo "Hello, World"
| shell: /bin/bash --noprofile --norc -e -o pipefail {0}
| ##[endgroup]
| Hello, World
| ##[end-action id=__run;outcome=success;conclusion=success;duration_ms=35]
| ##[start-action display=Run echo "Hello, World";id=__run_2]
| ##[group]Run echo "Hello, World"
| echo "Hello, World"
| shell: /bin/bash --noprofile --norc -e -o pipefail {0}
| ##[endgroup]
| Hello, World
| ##[end-action id=__run_2;outcome=success;conclusion=success;duration_ms=27]
[.github/workflows/test.yml / _] Succeeded: Run ./

Don't you need to expand the proposal with an update displayname command additionally to start-action or postpone issueing this log info.

// This is our last, best chance to expand the display name. (At this point, all the requirements for successful expansion should be met.)
// That being said, evaluating the display name should still be considered as a "best effort" exercise. (It's not critical or paramount.)
// For that reason, we call a safe "Try..." wrapper method to ensure that any potential problems we encounter in evaluating the display name
// don't interfere with our ultimate goal within this code block: evaluation of the condition.
step.TryUpdateDisplayName(out _);

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I haven't tested yet 😄

Will look into this during E2E testing (needs server changes to render)

## Summary

Emit `##[start-action]` / `##[end-action]` markers in the log stream around each nested step inside a composite action. The UI parses these markers to render collapsible regions, giving users visibility into individual steps that were previously hidden in a single opaque log blob.

## Design

The runner writes `##[` markers directly to the log stream via `ExecutionContext.Output()`, bypassing the logging command pipeline. No new `::` logging command is registered. Users cannot emit these markers from scripts.

### Marker format

```
##[start-action display=<step-display-name>;id=<step-id>]

... step output ...

##[end-action id=<step-id>;outcome=<result>;conclusion=<result>;duration_ms=<ms>]
```

- **id** — the step's `ContextName` (from YAML `id:` or auto-generated with `__` prefix)
- **outcome** — raw result before `continue-on-error` is applied
- **conclusion** — final result after `continue-on-error`
- **duration_ms** — wall-clock milliseconds (0 for skipped steps)

Nested composites use dot-separated IDs (e.g. `outer.inner-step`) to keep each step globally unique.

### Feature flag

Gated behind `actions_runner_emit_composite_markers` (job message variable) with `ACTIONS_RUNNER_EMIT_COMPOSITE_MARKERS` env var fallback (for internal testing only).

### Injection prevention

`OutputManager.OnDataReceived` replaces `##[start-action` and `##[end-action` from user process stdout/stderr with `##[\start-action` and `##[\end-action`. This preventing users from injecting fake markers. The runner's own markers bypass `OutputManager` entirely since they're written via `ExecutionContext.Output()` directly.
@ericsciple ericsciple force-pushed the users/ericsciple/26-02-composite-markers branch from c7780a8 to 2a1588d Compare February 13, 2026 21:53
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.

2 participants