Chapter 05Lesson 04~145 minutes

Environment Variables, Configuration Variables, Outputs, and Data Passing: Diagnostics, Failure Modes, and Production Practices

Dataflow failures are often misdiagnosed as “GitHub Actions lost my variable.” The real cause is usually a scope mismatch, a lifecycle misunderstanding, unsafe framing, or the wrong transport mechanism. This lesson preserves the original run evidence and diagnoses each layer separately before applying the smallest correction.

DiagnosticsScope failuresDeprecated commandsMultiline framingData leakage

Learning objectives

  • Diagnose missing values by reconstructing origin, scope, writer step, consumer job, and effective precedence.
  • Explain why GITHUB_ENV cannot cross jobs and why reserved/default environment variables should not be overwritten.
  • Recognize deprecated stdout output commands and use current environment-file output syntax.
  • Diagnose multiline framing/quoting corruption without turning arbitrary input into runner control syntax.
  • Replace oversized or file-shaped outputs with artifact/file transport while preserving first-failure evidence.

1. Evidence-first diagnostic sequence

  1. Preserve the run ID, attempt, exact workflow/source SHA, job/step names, and first failing/missing-value logs.
  2. Name the value and its intended producer and consumer.
  3. Locate its mechanism: YAML env, vars, default environment, GITHUB_ENV, step output, job output, or file/artifact.
  4. Check lifecycle/scope: same step, later step same job, downstream job, or later workflow.
  5. Check naming/precedence and exact output IDs.
  6. Check framing/encoding/size and whether the payload is sensitive or arbitrary.
  7. Apply one causal correction and rerun the smallest equivalent scope.

2. Failure: expecting GITHUB_ENV to cross jobs

jobs:
  produce:
    runs-on: ubuntu-24.04
    steps:
      - run: echo 'BUILD_LABEL=demo-17' >> "$GITHUB_ENV"
  consume:
    needs: produce
    runs-on: ubuntu-24.04
    steps:
      - run: test "$BUILD_LABEL" = 'demo-17'

The consumer fails because the producer's environment-file update belongs only to later steps in produce. The needs edge orders jobs but does not share a process environment or filesystem.

Causal repair: publish BUILD_LABEL through GITHUB_OUTPUT, map it to a job output, and consume needs.produce.outputs.build_label. Do not add a self-hosted runner or shared disk to “fix” a missing interface.

3. Failure: trying to overwrite authoritative default variables

- name: Wrong mental model
  run: |
    echo 'GITHUB_SHA=fake-value' >> "$GITHUB_ENV"
    echo 'RUNNER_OS=fake-os' >> "$GITHUB_ENV"

Current GitHub documentation says default GITHUB_* and RUNNER_* values cannot be overwritten this way. If you need a synthetic comparison value, give it an application-owned name such as EXPECTED_SHA. Never shadow evidence fields to make logs look convenient.

4. Failure: copying the deprecated stdout set-output pattern

# Historical pattern — do not use in new workflow code:
echo "::set-output name=version::1.2.3"

GitHub introduced environment files to reduce risks around workflow commands emitted through stdout. Current guidance is:

echo 'version=1.2.3' >> "$GITHUB_OUTPUT"

When reviewing old tutorials, distinguish “still observed in legacy workflows” from “recommended current design.” New course code uses GITHUB_OUTPUT.

5. Failure: delimiter collision corrupts multiline framing

This intentionally broken synthetic example chooses a delimiter that appears in the payload:

{
  echo 'NOTE<> "$GITHUB_ENV"

The framing ends too early. If the payload is fully controlled, generate a collision-resistant delimiter. If the payload is arbitrary, current guidance is to write it to a file rather than betting on a delimiter that “probably” will not occur.

6. Failure: using outputs as a large/binary transport

Encoding a multi-megabyte report or binary into a step/job output creates unnecessary size, quoting, and integrity problems and may exceed output limits. Preserve the generated file, compute a digest if needed, transfer the file via artifact/file mechanism, and keep only the digest/name/version as a small output.

Do not solve output-size failures by splitting a huge sensitive payload across many outputs. That preserves the wrong transport model and increases leak surface.

7. Failure: naming collision hides origin

env:
  REGION: workflow-demo
jobs:
  test:
    env:
      REGION: job-demo
    steps:
      - env:
          REGION: step-demo
        run: echo "$REGION"

The shell prints step-demo, but a reviewer may wrongly attribute it to repository configuration or workflow-level state. Fix the architecture by removing unnecessary collisions or naming layers explicitly. Do not diagnose precedence by printing entire contexts that may include sensitive values.

8. Failure: echoing sensitive data because “masking will handle it”

Configuration variables are unmasked, and secret masking is not a substitute for correct data handling. Never print a token, secret, private key, OIDC token, or derived credential merely to confirm transport. Prove the interface with synthetic stand-ins and use bounded metadata such as “credential present” only when safe and necessary.

9. Causal diagnosis matrix

SymptomLikely layerEvidenceSmallest repair
Value missing in next stepWriter syntax/current-step visibilityGITHUB_ENV append + next-step logUse correct env-file syntax; consume later step.
Value missing in next jobScope/interfaceJob graph + no mapped outputStep output → job output → needs.
Unexpected value winsPrecedence/namingBounded scope definitionsRemove collision or document authoritative owner.
Multiline truncatesFramingExact writer script and payload shapeSafe delimiter for controlled data or file for arbitrary data.
Output skipped/too largeOutput contractRunner warning, size, sensitivity classificationReduce to small control value or use file/artifact.

10. Intentionally broken mini-lab

In a disposable repository, run the cross-job GITHUB_ENV defect above and preserve the failed run. Then change only the producer/consumer interface to a job output. Keep trigger, runner, permissions, source data, and expected value identical. The repaired run proves causality because the transport mechanism is the only material change.

Record both run IDs/attempts, workflow SHAs, producer step conclusion, consumer failure/success, and the exact output name. Do not erase the original failed evidence.

11. Production practice: dataflow contracts are observable interfaces

Production workflow reviews should treat outputs and configuration like APIs: names are stable, producers and consumers are explicit, sensitivity is classified, sizes are bounded, and change history is reviewable. When a value disappears, debug the contract before changing runners, permissions, or external infrastructure.

Next lesson

Checkpoint lab

Build a two-job pipeline, deliberately misuse GITHUB_ENV as a cross-job channel, preserve the failed evidence, then replace it with the correct output interface.

Knowledge check

Why does adding needs: produce not make GITHUB_ENV values available to consume?

What is the current replacement for ::set-output in new workflow code?

Why is a fixed multiline delimiter unsafe for arbitrary payloads?

A 20 MB JSON report must cross jobs. What should change?

Why should you preserve the broken run before repair?

Official references and version notes

Version and compatibility note

Version-sensitive variable/output behavior was rechecked against current primary GitHub documentation on 2026-09-09. Executable labs use a disposable repository, ubuntu-24.04, built-in Bash steps only, and explicit permissions: {}; no external action, secret, package, deployment, self-hosted runner, or cloud account is required. At verification time, workflow/job/step env uses the most specific scope; default GITHUB_*/RUNNER_* environment variables cannot be overwritten; the step that writes GITHUB_ENV does not see the new value but later steps in the same job do; GITHUB_ENV cannot set NODE_OPTIONS; GITHUB_OUTPUT is the current step-output channel; multiline environment/output values use delimiter syntax whose delimiter must not occur on a line by itself; and arbitrary data is safer as a file rather than delimiter-encoded text. Configuration variables are non-secret, have organization/repository/environment scopes, and are subject to current limits including 48 KB per variable, 500 repository variables, 1,000 organization variables, 100 environment variables, and a 256 KB combined repository+organization payload per workflow run. Job outputs are limited to 1 MB per job and 50 MB total per workflow run (size approximated using UTF-16), so large/binary evidence belongs in files/artifacts rather than outputs. GitHub Actions is continuously delivered, so regenerate only after rechecking these limits and commands. The intentionally broken examples use only synthetic values. No example prints a real secret or recommends changing permissions/runners to hide a dataflow defect.

Keep the academy open

Support free, practical DevOps education.

Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this address.