5.2.2. Workflow Commands Inside Actions
💡 First Principle: An action talks to the runner through the same channels a workflow step does — environment files and workflow commands (2.2.2) — but the @actions/core toolkit wraps them in functions, which is why toolkit calls and echo "::..." lines are two spellings of one mechanism.
The mapping is worth knowing in both directions, because exam snippets appear in either form:
| Toolkit call | Underlying mechanism | Effect |
|---|---|---|
core.setOutput(name, value) | writes $GITHUB_OUTPUT | Exposes a step output |
core.exportVariable(name, value) | writes $GITHUB_ENV | Env var for later steps |
core.addPath(dir) | writes $GITHUB_PATH | Prepends to PATH |
core.setSecret(value) | ::add-mask:: | Masks the value in logs |
core.setFailed(msg) | ::error:: + exit 1 | Fails the step with a reason |
core.error/warning/notice(msg) | ::error::/::warning::/::notice:: | Annotations, no failure |
core.debug(msg) | ::debug:: | Only with ACTIONS_STEP_DEBUG |
core.startGroup/endGroup(name) | ::group::/::endgroup:: | Collapsible log section |
core.getInput(name) | reads INPUT_<NAME> | Reads a declared input |
core.saveState/getState | $GITHUB_STATE | Passes data from main to post |
Two of these are action-specific. core.setSecret() lets an action mask a value it computed — a token it just minted, for example — so the value is redacted for the rest of the run; this is the correct behavior for any action that handles credentials. And saveState/getState is the only channel between an action's main and post entry points, which is how a cleanup step knows what to clean up.
Composite actions use the shell forms directly, since there's no JavaScript process:
runs:
using: composite
steps:
- shell: bash
run: |
echo "::group::Provisioning"
echo "token=$(./mint.sh)" >> "$GITHUB_OUTPUT"
echo "::endgroup::"
⚠️ Exam Trap: The deprecated ::set-output:: and ::save-state:: commands were disabled in 2023 — current actions write to $GITHUB_OUTPUT and $GITHUB_STATE. Any snippet using echo "::set-output name=x::y" is wrong on today's exam, no matter how many blog posts still show it.
Reflection Question: An action mints a short-lived token and prints a status line containing it. core.setSecret() would have masked it. Explain what masking does and does not protect against, referencing 2.2.6.