GitHub Actions study materials
GitHub Actions study resources

GitHub Actions: Intermediate GitHub Actions practice: dynamic pipelines and hardening

GitHub Actions · Work through your exam, one topic at a time.

All guide sections

Intermediate GitHub Actions practice: dynamic pipelines and hardening

What this domain tests

The intermediate tier asks you to build pipelines that compute their own shape at runtime and to close the security holes that appear once workflows handle untrusted input. Items typically hinge on expression semantics, script injection, token scope, and action supply-chain pinning.

Expressions and dynamic matrices

  • Comparisons are loose: differing types are coerced to numbers, so '' == 0 and null == 0 are true, and string comparisons ignore case.
  • fromJSON turns a JSON string (often a job output) into an array or object; toJSON does the reverse for debugging.
  • contains, startsWith, endsWith, format, and join are available; hashFiles returns an empty string when nothing matches.
  • A matrix built with matrix: ${{ fromJSON(needs.plan.outputs.matrix) }} fails the run if the array is empty, so always emit a fallback or guard the job.

Injection and privileged triggers

  • Never interpolate ${{ github.event.* }} directly into a run: block. Assign it to env: and reference "$VAR" so the shell treats it as data.
  • pull_request_target runs in the base repository with secrets and a write token. Checking out github.event.pull_request.head.sha and executing it is a well-known compromise path.
  • Pin third-party actions to a full commit SHA; tags are mutable. Dependabot can keep SHA pins current.

Debugging and observability

  • Set the repository secret ACTIONS_STEP_DEBUG to true for verbose step logs; ACTIONS_RUNNER_DEBUG covers runner diagnostics.
  • Write Markdown to $GITHUB_STEP_SUMMARY for per-run reports, and emit ::error file=app.js,line=10::message to annotate a diff.

Worked example

A planner job emits a matrix, and a worker consumes it safely:

jobs:
  plan:
    runs-on: ubuntu-latest
    outputs: {matrix: "${{ steps.set.outputs.matrix }}"}
    steps:
      - id: set
        run: echo 'matrix=["api","web"]' >> "$GITHUB_OUTPUT"
  test:
    needs: plan
    strategy: {matrix: {package: "${{ fromJSON(needs.plan.outputs.matrix) }}"}}
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - env:
          PKG: ${{ matrix.package }}
        run: npm test --workspace "$PKG"

Reasoning: the matrix is data, not hard-coded; quoting "$PKG" keeps a hostile package name inert; fromJSON is required because the output arrives as a string.

Traps that cost marks

  • Writing if: github.event.issue.title == 'x' is fine, but using that title inside run: is injection.
  • id-token: write must be granted explicitly; OIDC is unavailable otherwise.
  • Step if conditions reading steps.<id>.conclusion behave differently from .outcome under continue-on-error.
  • A hashFiles cache key that silently returns '' produces a shared, wrong cache instead of failing.