GitHub Actions / Events, execution, and data boundaries / Lesson 6
Outputs, environment files, and file artifacts
Choose the correct data channel and preserve producer identity at every boundary.
On this page
Lesson goals
- Map step outputs to job outputs and downstream needs.
- Distinguish scalar metadata from retained files and dependency caches.
Choose the channel by scope
A shell variable lasts within its process. GITHUB_ENV makes an environment value available to later steps in the same job, while GITHUB_OUTPUT declares step outputs that can be mapped to job outputs. Artifacts transfer retained files across jobs or beyond a run. Dependency caches accelerate reusable content under separate trust and lifecycle rules. Parcel’s candidate name fits a scalar output; its package belongs in artifact or release storage. A large report should not be squeezed into a scalar just to avoid choosing a proper file channel.
Write an output through the provided file
A step needs an id if later expressions refer to its outputs. The fragment below writes candidate to GITHUB_OUTPUT, maps it to a job output, and consumes it through needs in another job. It is incomplete because no event or enclosing workflow is supplied, and it never runs here. The synthetic value is a label, not a verified package identity. Production output must be derived from the actual produced candidate and validated before it influences a privileged decision.
jobs:
identify:
runs-on: ubuntu-latest
outputs:
candidate: ${{ steps.label.outputs.candidate }}
steps:
- id: label
run: printf "%s\n" "candidate=parcel-lab-r3" >> "$GITHUB_OUTPUT"
report:
needs: identify
runs-on: ubuntu-latest
steps:
- env:
PARCEL_CANDIDATE: ${{ needs.identify.outputs.candidate }}
run: printf "%s\n" "$PARCEL_CANDIDATE"
GitHub supplies a unique output-file path to the step. The illustration assumes a suitable shell and does not create a package, attest its origin, or execute a workflow.
Respect text and multiline boundaries
Environment files are UTF-8 text with documented syntax. A single-line value is straightforward; arbitrary multiline content needs the supported delimiter form with a delimiter that cannot collide with the content, or a file artifact when arbitrary data is involved. Do not append contributor-controlled text as if it were a trusted key=value record. It can introduce extra assignments or malformed structure. PowerShell encoding behavior depends on the selected version, so use the documented shell-specific pattern rather than assuming every redirection produces the same bytes.
Files need explicit selection and retention
Uploading a path retains selected files under an artifact identity, subject to action version and platform retention rules. A downstream job must explicitly download the intended artifact. needs alone does not transfer its bytes. Use distinct names for matrix outputs and inspect hidden-file, permission, archive, and overwrite behavior in the selected action’s documentation. Tutorial major-version tags are mutable teaching references; an operational workflow should use reviewed immutable dependency identities and an update process. This course does not fabricate action commit pins or claim a hosted upload test.
Parse only a deliberately limited record
The complete local model parses two trusted single-line records and rejects duplicate keys. It is not a replacement for GitHub’s environment-file parser and does not support multiline syntax. The restriction makes the input contract explicit: values contain no newlines, keys are known, and duplicates are not silently accepted. Actual output consumers should validate expected fields and types. Never treat the mere existence of a downstream output as evidence that the upstream candidate or its authorization is valid.
text = "candidate=parcel-lab-r3\nformat=plain\n"
result = {}
for line in text.splitlines():
key, value = line.split("=", 1)
if key in result:
raise ValueError("duplicate key")
result[key] = value
print(result["candidate"])
print(sorted(result))
parcel-lab-r3
['candidate', 'format']
The code handles only the declared trusted single-line format. It performs no file I/O and cannot parse every legal GitHub environment-file representation.
Common errors and fixes
The producing step expects its new GITHUB_ENV value immediately.
- Why it happens
- The file updates later steps, not the current process environment.
- How to fix it
- Use the local variable now and the environment file for later steps.
- Prevent it next time
- Document the timing and scope of each transfer.
A downstream needs job cannot find package.zip.
- Why it happens
- A scalar dependency edge was mistaken for file transfer.
- How to fix it
- Upload and explicitly download the identified artifact.
- Prevent it next time
- Review artifact producer, name, paths, and retention.
Multiline user text creates extra output assignments.
- Why it happens
- Untrusted data was appended without a safe format contract.
- How to fix it
- Use supported bounded encoding or retain arbitrary content as a file.
- Prevent it next time
- Validate keys and avoid treating free text as control records.
Check your understanding
1. Which channel should carry a one-megabyte test report and a short candidate label?
Show worked solution
Retain the report as a selected file artifact with appropriate access and retention. Carry the short validated label through a step and job output when needed. Keep the relationship explicit so a label cannot accidentally identify another report or package.
2. A matrix produces three packages. Can all executions publish one shared scalar output reliably?
Show worked solution
Avoid relying on completion order to select a shared value. Name each package distinctly and aggregate an explicit manifest of expected combinations. Validate missing or failed combinations before downstream release decisions.
Key takeaways
- Select data channels by scope, size, and lifecycle.
- Output mapping does not authenticate the candidate it names.