Prepare for GitHub Actions (GH-200) with free sample questions, a full-length diagnostic, topic drills, timed practice, workflow YAML, CI/CD pipeline behavior, action reuse, troubleshooting, security, scale, optimization, and detailed explanations in IT Mastery.
GitHub Actions validates intermediate skill in automating software development workflows with GitHub Actions, including workflow creation, CI/CD pipeline management, troubleshooting, scale, security, and optimization.
IT Mastery practice for GitHub Actions (GH-200) is live now. Use this page to start the web simulator, review the exam snapshot, work through 24 public sample questions, and continue into full IT Mastery practice with the same IT Mastery account on web, iOS, iPadOS, macOS, or Android.
Start a practice session for GitHub Actions (GH-200) below, or open the full app in a new tab. For the best experience, open the full app in a new tab and navigate with swipes/gestures or the mouse wheel—just like on your phone or tablet.
Open Full App in a New TabA small set of questions is available for free preview. Subscribers can unlock full access by signing in with the same app-family account they use on web and mobile.
Prefer to practice on your phone or tablet? Download the IT Mastery – AWS, Azure, GCP & CompTIA exam prep app for iOS or IT Mastery app on Google Play (Android) and use the same IT Mastery account across web and mobile.
Free diagnostic: Try the GitHub Actions GH-200 full-length practice exam before subscribing. Use it as one workflow-automation baseline, then return to IT Mastery for timed mocks, topic drills, explanations, and the full GH-200 question bank.
| Area | What to practise |
|---|---|
| Author workflows | triggers, jobs, steps, runners, matrices, environments, permissions, and reusable workflows |
| Consume and troubleshoot workflows | logs, artifacts, caching, concurrency, failures, reruns, and debugging strategy |
| Author and maintain actions | composite actions, JavaScript actions, Docker actions, inputs, outputs, versioning, and reuse |
| Enterprise management | policies, runner groups, billing awareness, organization settings, and operational governance |
| Secure and optimize automation | secrets, token permissions, dependency risk, least privilege, performance, and reliability |
Use this map to reason through most GitHub Actions questions. Start with the event, then follow the workflow into jobs, runners, permissions, secrets, and deployment controls.
flowchart LR
Event["Event trigger"] --> Workflow["Workflow file"]
Workflow --> Jobs["Jobs"]
Jobs --> Runner["Runner"]
Jobs --> Steps["Steps"]
Steps --> Actions["Actions or shell commands"]
Workflow --> Permissions["GITHUB_TOKEN permissions"]
Workflow --> Secrets["Secrets and variables"]
Jobs --> Environment["Environment gates"]
Environment --> Deployment["Deployment"]
Permissions --> Risk["Least-privilege risk check"]
Secrets --> Risk
GitHub Actions questions often include a short workflow, log excerpt, policy setting, or runner detail. Read the exhibit from top to bottom: identify the trigger first, then check job scope, permissions, runner context, secrets, and the artifact/cache behavior the question is testing.
| Exhibit type | What to inspect first |
|---|---|
| Workflow YAML | on, permissions, jobs, runs-on, strategy, and deployment environment |
| Failure log | first failing command, exit code, missing secret, path mismatch, or denied token scope |
| Permissions block | whether the GITHUB_TOKEN has only the scopes required for the job |
| Runner labels | whether the job is using GitHub-hosted or self-hosted infrastructure correctly |
| Cache key | whether the key changes when dependencies or the runner OS changes |
| Artifact step | whether the file is produced before upload and downloaded by the later job |
Example workflow exhibit:
on:
pull_request:
paths:
- "src/**"
permissions:
contents: read
pull-requests: write
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm test
Best reading: this workflow is pull-request scoped, path-filtered, and uses limited token permissions. It should not be treated as a deployment workflow unless a later job adds environment gates and trusted credentials.
Example failure-log exhibit:
Run npm test
Error: Cannot find module '@acme/shared-config'
Process completed with exit code 1.
Best first check: compare the runner install step, lock file, package registry authentication, workspace path, and cache restore result before changing the test command.
Example deployment-control exhibit:
| Setting | Why it matters |
|---|---|
environment: production | can require reviewers and expose production-scoped secrets only after approval |
concurrency.group: production | prevents overlapping production deployment runs |
cancel-in-progress: false | avoids canceling an already-started production deployment automatically |
permissions.contents: read | avoids broad repository write access unless a deployment step truly needs it |
Use these filters when a workflow answer looks correct but incomplete:
on, branch filters, path filters, and event context before changing jobs or steps.GITHUB_TOKEN permissions, secrets, OIDC, environment protection, dependency risk, and untrusted pull request behavior.| Day | Practice focus |
|---|---|
| 7 | Take the free full-length diagnostic and tag misses as authoring, troubleshooting, actions, enterprise, or security. |
| 6 | Drill workflow syntax, triggers, jobs, matrices, reusable workflows, environments, and permissions. |
| 5 | Drill logs, cache behavior, artifacts, concurrency, reruns, runner labels, and failure analysis. |
| 4 | Drill custom actions, inputs, outputs, versioning, and when to use composite, JavaScript, or Docker actions. |
| 3 | Drill secrets, OIDC, token permissions, untrusted PRs, branch protection, and secure deployment controls. |
| 2 | Complete a timed mixed set and explain which workflow boundary drove each miss. |
| 1 | Review weak YAML patterns and exhibit-reading mistakes; avoid late memorization of random syntax. |
If you can score above roughly 75% on several unseen mixed attempts and explain the trigger, permission, runner, or troubleshooting signal behind your misses, you are likely ready. More practice should improve workflow reasoning, not make you memorize repeated YAML snippets.
Use these child pages when you want focused IT Mastery practice before returning to mixed sets and timed mocks.
Need concept review first? Read the GitHub Actions GH-200 Cheat Sheet on Tech Exam Lexicon, then return here for timed mocks, topic drills, and full IT Mastery practice.
These are original IT Mastery practice questions aligned to the live GitHub Actions (GH-200) route and the main blueprint areas shown above. Use them to test workflow, runner, security, deployment, and troubleshooting judgment here, then continue in IT Mastery with mixed sets, topic drills, and timed mocks.
Topic: Author and Maintain Actions
A team wants to stop duplicating checkout, Node setup, and dependency install steps across several jobs. They created .github/workflows/shared-setup.yml as a reusable workflow, and the next run failed.
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Shared setup
uses: ./.github/workflows/shared-setup.yml
- run: npm test
Error: Can't find 'action.yml', 'action.yaml' or 'Dockerfile' under
'/home/runner/work/app/app/.github/workflows/shared-setup.yml'
What is the best explanation for this failure?
on: workflow_call; then the current step call will work.actions/checkout did not make .github/workflows/shared-setup.yml available on the runner.permissions before GitHub can load a local workflow file.Best answer: A
Explanation: The runner is treating the local path in uses as an action reference, so it looks for action.yml or a Dockerfile. A reusable workflow in .github/workflows cannot be invoked from a step; repeated steps should be packaged as a composite action. uses means different things depending on where it appears. Inside steps, GitHub Actions expects an action. For a local action, the path must point to a directory that contains action.yml (or a Docker action). Because the path points to a workflow file under .github/workflows, the runner searches for action metadata and fails.
.github/actions/shared-setup/action.yml.uses: ./.github/actions/shared-setup.workflow_call.The closest distractor mixes up reusable workflows and composite actions.
Topic: Manage GitHub Actions for the Enterprise
An enterprise workflow deploys to an internal release API protected by a firewall. The API team will allow only a small, stable set of source IP addresses. The platform team wants GitHub to manage runner maintenance and scaling. Which configuration is best for the deployment job?
ubuntu-latest runners and sync the firewall to GitHub’s published Actions IP ranges each week.Best answer: C
Explanation: The best fit is GitHub-hosted larger runners with static outbound IPs. Standard GitHub-hosted runners use changing public IP ranges, so they are a poor match for a firewall that requires a small, stable allow list. The key concept is that standard GitHub-hosted runners do not give you a small fixed set of egress IP addresses for firewall planning. Their outbound addresses come from broader published ranges that can change, so they are not ideal when an internal service only accepts a narrow, predictable allow list. If the team still wants GitHub to manage the runners, larger runners with static outbound IPs are the best match.
The deciding factor is outbound IP predictability, not deployment approvals or other workflow controls.
Topic: Consume and Troubleshoot Workflows
You maintain a repository where all pushes go directly to main. The workflow starts as:
name: CI
on:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- run: echo ok
Timeline:
.github/workflows/ci.yml to .github/workflows/pipeline.yml; the file contents stay the same.CI..github/workflows/pipeline.yml.Which trace matches GitHub Actions behavior?
Best answer: A
Explanation: GitHub Actions evaluates workflow files that exist in .github/workflows for the pushed commit. Renaming the file within that directory does not stop the next eligible push from running, but disabling the workflow stops later runs, and deleting the file leaves no workflow to trigger. Completed runs remain in history. The core concept is workflow availability at the time each event is processed. A workflow is eligible only if the workflow file exists in .github/workflows and the workflow is enabled.
.github/workflows.Disabling or deleting a workflow affects future runs, not completed ones. Previous runs remain in workflow history until they expire by retention policy or are manually deleted. The closest misconception is treating a rename within .github/workflows as if the workflow had been removed.
Topic: Author and Manage Workflows
A public repository accepts pull requests from forks. The team needs an automated CI workflow that runs tests against the actual PR code and uses the PR number and head SHA from the event payload. The workflow must safely execute untrusted fork code, with no repository secrets and no write access beyond what is required to read the code. Which configuration is best?
pull_request_target; set permissions: pull-requests: write; checkout github.event.pull_request.head.sha; run tests.push; use a repository PAT secret to fetch the PR branch and run tests.pull_request; set permissions: contents: read; checkout github.event.pull_request.head.sha; run tests.workflow_dispatch; pass the PR number as input and use write-all permissions.Best answer: C
Explanation: The pull_request event is the best fit for CI that must execute code from forked PRs. It still includes PR-specific context such as the PR number and head SHA, but fork-originated runs do not receive repository secrets and use a restricted token model. The core concept is that the workflow trigger defines both the available event payload and the trust boundary. For testing untrusted code from forked pull requests, pull_request is the safe choice because it exposes github.event.pull_request fields such as the PR number and head.sha, while keeping the run in a lower-trust context with no repository secrets and a restricted GITHUB_TOKEN.
A minimal safe pattern is:
on: pull_request
permissions:
contents: read
Then the job can check out the PR head commit and run tests with least privilege. The closest distractor is pull_request_target, which does provide PR context, but it runs in the base repository security context, so checking out and executing fork code there creates unnecessary risk.
Topic: Secure and Optimize Automation
A team wants newer pushes to main to cancel older in-progress CI runs and reduce runner usage. Instead, every push starts another full run, and none of the earlier runs are canceled.
Exhibit:
name: CI
on:
push:
branches: [main]
concurrency:
group: ci-${{ github.sha }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm test
What is the best explanation for the duplicated runs?
cancel-in-progress works only for pull_request eventsstrategy.fail-fast: true to cancel earlier runsactions/checkout must be pinned to a full SHA for concurrency to workBest answer: D
Explanation: Concurrency cancellation only applies to runs in the same concurrency group. Because the workflow uses github.sha, each push creates a different group, so newer runs do not cancel older ones. The core issue is the concurrency key. GitHub Actions cancels in-progress runs only when a new run starts with the same concurrency.group value and cancel-in-progress: true is set. In this workflow, github.sha is different for every commit, so each push to main creates a brand-new group.
A cost-optimization pattern is to use a stable key for the branch or workflow, such as:
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
That lets a newer push to the same branch replace the older run instead of consuming more runner time. The closest trap is confusing workflow-level concurrency with job failure behavior; they solve different problems.
Topic: Author and Maintain Actions
A team refactored a composite action but wants workflow consumers to keep using the same public output name. No shell commands fail.
# .github/actions/normalize-ref/action.yml
name: normalize-ref
inputs:
ref_name:
required: true
outputs:
normalized:
value: ${{ steps.prepare.outputs.normalized }}
runs:
using: composite
steps:
- id: slug
shell: bash
run: |
value=$(echo "${{ inputs.ref_name }}" | tr '[:upper:]' '[:lower:]' | tr '/' '-')
echo "normalized=$value" >> "$GITHUB_OUTPUT"
jobs:
build:
runs-on: ubuntu-latest
outputs:
ref: ${{ steps.norm.outputs.normalized }}
steps:
- id: norm
uses: ./.github/actions/normalize-ref
with:
ref_name: Release/Candidate
- run: echo "ref=[${{ steps.norm.outputs.normalized }}]"
deploy:
needs: build
if: needs.build.outputs.ref != ''
runs-on: ubuntu-latest
steps:
- run: echo "Deploy ${{ needs.build.outputs.ref }}"
Which execution trace matches GitHub Actions behavior?
build prints ref=[release-candidate], and deploy runs.build prints ref=[], and deploy is skipped.build fails before the echo step because steps.prepare does not exist.build prints ref=[], and deploy still runs.Best answer: B
Explanation: Composite actions expose only the outputs declared in action.yml. Here, the public output normalized still maps to steps.prepare.outputs.normalized, but the internal step ID is now slug, so the exposed output is empty and the downstream job condition evaluates to false. The stable interface for a custom action is its declared inputs and outputs, not its internal step IDs. In this scenario, the workflow consumer correctly reads steps.norm.outputs.normalized, but that public output is miswired inside the composite action.
Because outputs.normalized.value references steps.prepare.outputs.normalized and no prepare step exists, the expression resolves to an empty string instead of the transformed value. The inner slug step still writes to GITHUB_OUTPUT, but that output is not exposed unless the metadata maps to it.
So the run behaves like this:
ref=[].ref is empty.deploy is skipped because needs.build.outputs.ref != '' is false.The closest wrong trace is the one expecting release-candidate, which would require the output mapping to reference steps.slug.outputs.normalized.
Topic: Manage GitHub Actions for the Enterprise
A GitHub organization uses three self-hosted Linux runners for production deployments. The payments-api repository is supposed to use them exclusively. After the docs-site repository added a 12-job test matrix, payments-api deployment jobs began queuing for more than 30 minutes.
Exhibit: Recent run history
docs-site / test -> running on deploy-01, deploy-02, deploy-03
payments-api / deploy -> queued 31m
Both workflows use: runs-on: [self-hosted, linux, x64, prod-deploy]
Runners are online and healthy.
Which next diagnostic action best identifies why an unrelated repository can consume those runners?
docs-site matrix should use a lower max-parallel value.payments-api is waiting for an environment approval instead of a runner.Best answer: A
Explanation: The exhibit already shows an unrelated repository running on the same deployment runner names, so the issue is authorization, not runner health. In GitHub Actions, runner groups determine which repositories can use a self-hosted runner set, while labels only help select runners after access is granted. Reviewing runner group repository access is therefore the best next diagnostic step. Self-hosted runner isolation in GitHub Actions is enforced with runner groups and their repository access settings. Because docs-site is executing on the exact deployment runners, the problem is not just high concurrency; that repository was allowed to target the runner set in the first place. The most direct diagnostic step is to inspect the runner group’s access and verify that it is restricted to selected repositories rather than all organization repositories.
prod-deploy are routing filters, not isolation controls.The key takeaway is that labels place jobs, but runner groups prevent unrelated repositories from sharing sensitive or capacity-constrained runners.
Topic: Consume and Troubleshoot Workflows
A push to main triggers this workflow. In the workflow run summary, deploy appears as skipped. Which job and step should you inspect first to locate the actual failure?
name: ci
on:
push:
branches: [main]
jobs:
test:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- name: Install
run: echo "install"
- name: Run smoke tests
if: runner.os == 'Windows'
run: exit 1
deploy:
needs: test
runs-on: ubuntu-latest
steps:
- run: echo "deploy"
deploy job, at its echo "deploy" steptest job on ubuntu-latest, at the Install stepactions/checkout@v4 step in either test jobtest job on windows-latest, at the Run smoke tests stepBest answer: D
Explanation: This workflow creates two test jobs through the matrix, one for Ubuntu and one for Windows. Only the Windows job runs Run smoke tests, and that step exits with code 1, so that is the failed step to inspect; deploy is only skipped because it depends on test. GitHub Actions expands the matrix into separate test jobs, so the run summary will show one job for ubuntu-latest and one for windows-latest. To find the root cause, inspect the failed matrix job rather than the downstream job that was skipped.
Here, Run smoke tests has if: runner.os == 'Windows', so it runs only in the Windows job. That step executes exit 1, which marks the step and that matrix job as failed. The Ubuntu job does not fail from that step because the condition is false there. Since deploy has needs: test, GitHub skips deploy after the failed Windows matrix job.
The key troubleshooting pattern is to open the red failed matrix job entry first, then the failed step inside it.
Topic: Author and Manage Workflows
A repository currently runs this workflow only when code is pushed to main:
name: ci
on:
push:
branches:
- main
jobs:
test:
runs-on: ubuntu-latest
steps:
- run: echo test
The team also wants the same workflow to run for pull requests that target main, without changing the existing push behavior. Which on: block should replace the current one?
yaml on: [push, pull_request]yaml on: push: branches: [main] pull_request: branches: [main]yaml on: push, pull_request: branches: [main]yaml on: push: branches: [main] pull_request: branches: [main]Best answer: B
Explanation: To preserve push on main and add pull requests targeting main, the workflow must use expanded on: syntax with separate event entries. The array shorthand cannot retain the existing branch filter for only one event. The key concept is that GitHub Actions supports both shorthand and expanded on: syntax. Shorthand such as on: [push, pull_request] is useful only when you do not need per-event configuration. Once you need filters like branches, each event must be declared separately under on:.
In this scenario, push must remain limited to main, and pull_request must also target main, so both events need their own configuration:
on:
push:
branches: [main]
pull_request:
branches: [main]
The closest distractor adds both events, but it also broadens push to all branches, which changes the workflow’s original trigger behavior.
Topic: Secure and Optimize Automation
Your team maintains a Node.js library. Pull requests need fast feedback on lint and unit tests. Before a version tag publishes the package, the library must still be validated on Ubuntu, Windows, and macOS with Node 20 and 22. Most PR failures are OS-independent, developers use Node 22 day-to-day, and the team wants lower runner cost without adding self-hosted infrastructure. Which workflow configuration is the best fit?
pull_request and push, and raise max-parallel to 6.pull_request checks on ubuntu-latest with Node 22 only; run the full Ubuntu/Windows/macOS × Node 20/22 matrix on version-tag pushes with standard GitHub-hosted runners and max-parallel: 2.pull_request, but move it to self-hosted runners so all jobs can start immediately.macos-latest job that runs Node 20 and 22 sequentially for both PRs and releases.Best answer: B
Explanation: The best optimization is to reduce matrix breadth where it adds little signal and keep full coverage only when it is required. Using ubuntu-latest with the team’s daily Node version on pull requests gives quick feedback, while the capped full matrix on version tags preserves required cross-platform validation before publishing. Matrix optimization in GitHub Actions should match the decision point. For pull requests, the goal is fast feedback, so the best configuration uses the smallest high-signal matrix that catches most regressions: here, ubuntu-latest with Node 22. For publishing, the goal changes to compatibility assurance, so that is the right time to run the full Ubuntu, Windows, and macOS matrix across Node 20 and 22.
Limiting max-parallel on the larger release matrix controls spend and reduces runner contention without removing required coverage. By contrast, running the full matrix on every pull request increases queue time and cost, and moving the same workload to self-hosted runners changes infrastructure rather than fixing matrix scope. A single macOS job also misses required Windows and Ubuntu validation.
The key idea is to size matrix breadth and concurrency to the value of the feedback at each trigger.
Topic: Author and Maintain Actions
A team maintains an internal action named octo-org/deploy-action. The action repo currently has a released tag v2.3.1, and v3.0.0 will introduce breaking changes later. Workflow consumers should stay on major version 2, automatically receive future compatible v2 updates, and avoid editing the workflow for each patch release.
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: octo-org/deploy-action@main
with:
target: prod
Which edit best meets this goal?
uses: octo-org/deploy-action@v2.3.1uses: octo-org/deploy-action@v2uses: octo-org/deploy-action@mainBest answer: C
Explanation: A moving major tag such as v2 is the standard way to express that consumers want the latest backward-compatible release in that major line. Here, the team wants future v2 fixes automatically but does not want v3 changes, so @v2 is the best reference. In GitHub Actions, the ref after uses: tells consumers what level of version stability they are choosing. A major semantic version tag like @v2 is the usual contract when you want automatic updates within a compatible major line.
@v2 tracks the maintained major version line.@v2.3.1 targets one exact release.@main follows a mutable development branch.Because the requirement is to stay on major version 2 and still receive later compatible fixes, a moving major tag is the best fit. The closest distractors are the exact release tag and commit SHA, but both require changing the workflow again to consume later v2 updates.
Topic: Manage GitHub Actions for the Enterprise
A repository uses this workflow:
name: ci
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v4
- run: ./build.sh
GitHub has announced that the ubuntu-20.04 GitHub-hosted runner image used by the organization will be retired soon. The team has already validated the workflow on ubuntu-22.04 and wants the smallest change that keeps the workflow on a supported image without automatically moving again during a future ubuntu-latest migration. Which edit is best?
runs-on to ubuntu-latestruns-on to ubuntu-22.04ubuntu-20.04 and ubuntu-22.04ubuntu-20.04 and install tools manually in a stepBest answer: B
Explanation: The workflow should move from the retired runner label to a supported explicit label. Because the team wants stability across future hosted-image migrations, ubuntu-22.04 is better than the floating ubuntu-latest label. In GitHub Actions, runs-on selects the hosted runner image. When a specific image label is deprecated or retired, the workflow must be updated to another supported label. Here, the team has already tested on ubuntu-22.04 and wants to avoid later environment drift, so the best adaptation is to pin the job to that supported version explicitly.
Using ubuntu-latest would also move off the retired image, but it is intentionally a moving target and may point to a newer Ubuntu image later. A matrix still includes the retiring label, and manually installing packages does not solve a retired runner image. The key takeaway is to use an explicit supported image label when you need predictable behavior during hosted-runner migrations.
Topic: Consume and Troubleshoot Workflows
A workflow run completed 1 hour ago. The repository’s artifact retention policy is 7 days. In the run summary, test (ubuntu-latest) has an artifact named coverage-ubuntu-latest, but test (windows-latest) has no artifact and the job still shows as successful.
jobs:
test:
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- name: Run tests
run: echo "tests passed"
- name: Generate coverage
if: runner.os == 'Linux'
run: |
mkdir coverage
echo ok > coverage/report.txt
- name: Upload coverage
if: always()
uses: actions/upload-artifact@v4
with:
name: coverage-${{ matrix.os }}
path: coverage/report.txt
if-no-files-found: warn
Windows job log for Upload coverage:
With the provided path, there will be 0 files uploaded.
Warning: No files were found with the provided path: coverage/report.txt. No artifacts will be uploaded.
Which explanation best matches GitHub Actions behavior?
upload-artifact failed the Windows job because an empty path always causes a step error.coverage/report.txt, so the upload step ran but found no files to upload.Best answer: D
Explanation: The missing artifact is caused by a path/input condition inside the Windows matrix leg, not by retention or a skipped upload. Generate coverage only runs on Linux, so the Windows upload step executes with no matching file and only emits a warning. This run is a matrix job, so each OS executes its own independent step flow. On Ubuntu, runner.os == 'Linux' is true, so coverage/report.txt is created and uploaded. On Windows, that step is skipped, but Upload coverage still runs because it uses if: always().
Because the upload step is configured with if-no-files-found: warn, GitHub Actions does not fail the job when coverage/report.txt is missing. It logs a warning and creates no artifact for that matrix leg.
The key takeaway is that a missing artifact can come from a file never being produced in that job, even when the upload step itself ran successfully enough for the job to stay green.
Topic: Author and Manage Workflows
A team uses this workflow for integration tests:
name: ci
on: [push]
jobs:
integration:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm test
The tests now need PostgreSQL 16 during the job. They also call corp-sign, a licensed internal binary that is preinstalled only on organization self-hosted Linux runners with Docker and cannot be redistributed in a container image. Which edit best satisfies these requirements with the least unnecessary change?
container: postgres:16 and keep runs-on: ubuntu-latest.corp-sign.ubuntu-latest, add PostgreSQL under services:, and download corp-sign in a step.services:.Best answer: D
Explanation: Use a service container for PostgreSQL because it is a supporting service for the job, not the environment that the steps themselves should run inside. Use a self-hosted Linux runner because corp-sign is a runtime dependency that is available only on those approved runners. In GitHub Actions, services: is the right fit for dependencies such as PostgreSQL that need to run alongside the job. A job container: changes the execution environment for all steps, which is useful when every step should run inside the same image, but it is not how you add a database sidecar. Here, the workflow also depends on corp-sign, and the stem says that binary is licensed only on organization-managed self-hosted Linux runners and cannot be redistributed in an image.
services: for PostgreSQL.corp-sign dependency.The closest distractor is the custom job container, but it still fails the stated licensing and distribution constraint for corp-sign.
Topic: Secure and Optimize Automation
A team wants production deployment only for a tagged release whose build artifact has GitHub provenance. A maintainer pushes tag v3.1.0 in the same repository. Assume the shell commands succeed. Which run trace matches GitHub Actions behavior?
name: release
on:
push:
tags: ['v*']
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
file: ${{ steps.meta.outputs.file }}
steps:
- id: meta
run: echo "file=app.tgz" >> "$GITHUB_OUTPUT"
- run: echo package > app.tgz
- uses: actions/upload-artifact@v4
with:
name: release-bundle
path: ${{ steps.meta.outputs.file }}
attest:
needs: build
runs-on: ubuntu-latest
permissions:
contents: read
attestations: write
id-token: write
outputs:
ready: ${{ steps.done.outputs.ready }}
steps:
- uses: actions/download-artifact@v4
with:
name: release-bundle
path: downloaded
- uses: actions/attest-build-provenance@v3
with:
subject-path: "downloaded/${{ needs.build.outputs.file }}"
- id: done
run: echo "ready=true" >> "$GITHUB_OUTPUT"
deploy:
needs: attest
if: ${{ needs.attest.outputs.ready == 'true' }}
runs-on: ubuntu-latest
steps:
- run: echo "Deploying"
build uploads release-bundle, deploy starts next, and attest finishes later.attest creates provenance, but deploy skips because ready cannot cross jobs.attest starts, but it fails because contents: write is required.build uploads release-bundle, attest creates provenance, and deploy then runs.Best answer: D
Explanation: Artifact attestations can gate a release when the workflow creates provenance before deployment and exposes a success signal as a job output. Here, attest runs after build, consumes the uploaded artifact, and deploy runs only when needs.attest.outputs.ready is true. Artifact attestations support deployment, release, and supply-chain verification when a trusted build produces an artifact, the workflow generates provenance for that artifact, and later jobs depend on that result. In this workflow, build uploads release-bundle, then attest runs because it has needs: build. The uploaded artifact is available within the same workflow run, so download-artifact can retrieve it for attestation.
The attest job has the required permissions for provenance generation: contents: read, attestations: write, and id-token: write. After the attestation step succeeds, the done step writes ready=true to GITHUB_OUTPUT, and the job maps that step output to the job output ready. The deploy job cannot start early because it needs attest, and its if condition evaluates that job output.
This is how provenance becomes an explicit deployment gate rather than only a post-build record.
Topic: Author and Maintain Actions
A platform team needs one shared release component for many repositories. The component must run a build job, wait for approval on the protected production environment before publishing, and return a published value to the caller. They choose a reusable workflow instead of a custom action.
Assume production requires approval, REGISTRY_TOKEN exists in the caller repository, and approval is granted only after build completes.
# caller
on: workflow_dispatch
jobs:
standard-release:
uses: org/platform/.github/workflows/reusable-release.yml@v1
with:
version: 1.2.3
secrets:
registry_token: ${{ secrets.REGISTRY_TOKEN }}
announce:
runs-on: ubuntu-latest
needs: standard-release
steps:
- run: echo "${{ needs.standard-release.outputs.published }}"
# callee
on:
workflow_call:
inputs:
version: { type: string, required: true }
secrets:
registry_token: { required: true }
outputs:
published:
value: ${{ jobs.publish.outputs.published }}
jobs:
build:
runs-on: ubuntu-latest
outputs:
image: ${{ steps.pkg.outputs.image }}
steps:
- id: pkg
run: echo "image=app:${{ inputs.version }}" >> $GITHUB_OUTPUT
publish:
runs-on: ubuntu-latest
needs: build
environment: production
outputs:
published: ${{ steps.mark.outputs.published }}
steps:
- run: echo "Publishing ${{ needs.build.outputs.image }}"
env:
TOKEN: ${{ secrets.registry_token }}
- id: mark
run: echo "published=true" >> $GITHUB_OUTPUT
Which statement matches GitHub Actions behavior?
build runs first, publish waits for approval, and announce prints true only after the called workflow finishes.announce starts after build finishes because caller jobs can read callee outputs before later callee jobs complete.publish cannot use registry_token because secrets mapped by the caller are unavailable inside callee jobs.Best answer: A
Explanation: Reusable workflows are appropriate when shared automation needs workflow-level features such as multiple jobs, job dependencies, environment approvals, and outputs returned to the caller. Here, build completes first, publish pauses for production approval, and only then can the caller’s announce job read the published output. The key concept is that a reusable workflow behaves like a called workflow, not like a single custom-action step. That makes it the right choice when the shared logic needs multiple jobs, needs relationships, environment protection rules, or outputs passed back to the caller.
In this run, the caller job standard-release represents the entire called workflow. Inside the callee, publish cannot start until build succeeds, and because publish targets the protected production environment, it also waits for approval. After approval, publish runs, writes published=true to the GitHub Actions output file, and the reusable workflow maps that value through on.workflow_call.outputs. Only after the called workflow finishes can the caller’s announce job start and echo true.
The closest wrong idea is treating the reusable workflow like a custom action; custom actions run within one job and cannot create separate jobs or environment-gated stages.
Topic: Manage GitHub Actions for the Enterprise
A repository has access to two self-hosted runner groups:
prod-runners: runners have labels self-hosted, linux, x64, deploystaging-runners: runners have labels self-hosted, linux, x64, deployThe deployment job must run only on runners in prod-runners.
jobs:
deploy:
if: github.ref == 'refs/heads/main'
runs-on: [self-hosted, linux, x64, deploy]
steps:
- uses: actions/checkout@v4
- run: ./deploy.sh
Which edit best satisfies the requirement?
runs-on with self-hostedruns-on with prod-runnersruns-on with [self-hosted, prod-runners, deploy]runs-on with { group: prod-runners, labels: deploy }Best answer: D
Explanation: The current label array can match any accessible self-hosted runner that has those labels, so it does not distinguish production from staging. To target only the intended runners, runs-on must use runner group syntax. Runner labels and runner groups are different routing controls in GitHub Actions. An array such as [self-hosted, linux, x64, deploy] means the runner must have all of those labels, but it does not limit the job to a specific runner group. Because both accessible groups contain runners with the same labels, either group could receive the job.
To constrain execution to one group, use the object form of runs-on:
runs-on:
group: prod-runners
labels: deploy
This first narrows selection to runners in prod-runners, then applies the deploy label filter. The closest distractors fail because they treat the group name like a label instead of using the dedicated group selector.
Topic: Consume and Troubleshoot Workflows
A pull request run failed, and the downstream package job has needs: test. The workflow run summary shows:
Workflow: validate-and-package
Conclusion: failure
Jobs
- lint ................................ success
- test (os: ubuntu-latest, node: 18) . success
- test (os: ubuntu-latest, node: 20) . success
- test (os: windows-latest, node: 20) failure
- package ............................. skipped
Failed job steps for `test (os: windows-latest, node: 20)`
- Checkout .............. success
- Setup Node ............ success
- Install dependencies .. success
- Run unit tests ........ failure
- Upload coverage ....... skipped
What is the best next diagnostic action?
test (windows-latest, node 20) at Run unit testspackage job firstBest answer: A
Explanation: The run summary already identifies both the failing matrix job and the failed step. The fastest diagnostic path is to open that job’s logs at Run unit tests, because the skipped package job is only a downstream result of needs: test. In GitHub Actions, the run summary is the quickest way to locate the root failure: find the first failed job, then inspect the first failed step inside that job. Here, the summary narrows the issue to the test matrix variant running on Windows with Node 20, and it explicitly shows Run unit tests as the failing step.
needs, not the original problem.The key takeaway is to use the summary to jump directly to the failed matrix job and step log.
Topic: Author and Manage Workflows
A repository contains these workflows. A maintainer manually runs caller.yml.
# .github/workflows/caller.yml
on:
workflow_dispatch
jobs:
release:
uses: ./.github/workflows/reusable.yml
with:
env_name: prod
# .github/workflows/reusable.yml
on:
workflow_call:
inputs:
environment:
required: true
type: string
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- run: echo "Deploying to ${{ inputs.environment }}"
Which execution trace occurs?
deploy runs and prints Deploying to prod.deploy runs and prints Deploying to with an empty value.deploy starts because env_name does not match the declared required input.Best answer: C
Explanation: workflow_call inputs are strict. The caller must use the exact declared input name, so env_name is not accepted as environment, and the required input is not satisfied. Because the mismatch is detected at the workflow-call boundary, no job in the reusable workflow starts. Reusable workflows act like defined interfaces. Under on.workflow_call.inputs, the called workflow declares the only accepted input names and types. Here, the callee requires environment, but the caller sends env_name. GitHub Actions does not alias or infer that mapping.
with keys in the caller must exactly match declared input names.The key point is that this is an interface-validation problem, not a runtime empty-string or default-value behavior.
Topic: Secure and Optimize Automation
An organization manually approves the prod environment before deployments. In this run, approval has already occurred, so the deploy job is now executing on a push to main. Because permissions.id-token: write is granted, the Marketplace action in Cloud login can request an OIDC token when that step starts.
jobs:
deploy:
environment: prod
permissions:
contents: read
id-token: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Cloud login
uses: <candidate action>
- run: ./deploy.sh
Which review conclusion is correct?
@v2, because its star count proves the same reviewed code will run.Best answer: A
Explanation: Once the environment is approved, the third-party action executes with the job’s granted permissions, including the ability to request an OIDC token. In that situation, the strongest trust signals are reputable source ownership, active maintenance, full-SHA pinning, and least-privilege permission needs. When a third-party action will run in a sensitive step, trust evaluation should focus on four things: who owns the source, what exact code will run, whether it is actively maintained, and whether it needs only the permissions the job should grant. In the stem, the Cloud login step runs after environment approval and has id-token: write, so the action can mint cloud credentials. That makes source quality and exact version pinning especially important. A full commit SHA fixes the precise code that runs; an official or well-governed source repository and recent maintenance reduce supply-chain risk; and documented least-privilege use reduces blast radius. Popularity, download count, or a verified badge are only supporting signals, not proof that the reviewed revision is safe. The key takeaway is to prefer official source + active maintenance + full-SHA pinning + least privilege when an action receives sensitive runtime capabilities.
Topic: Author and Maintain Actions
An organization publishes a shared JavaScript action, contoso/deploy-metadata, that other repositories call with uses: contoso/deploy-metadata@.... Most teams should receive backward-compatible fixes automatically within a major version, but a few regulated repositories must use an exact immutable reference. Which release/versioning configuration is the best fit?
@main so they always get the newest code.stable tag for each release and stop publishing semantic version tags.v2.4.1, keep v2.4 and v2 tags aligned to compatible releases, and let strict consumers pin the full commit SHA.2026-04-29 and ask consumers to use the latest date.Best answer: C
Explanation: The best configuration is to publish semantic version tags and maintain compatibility tags for supported release lines. That lets most workflows follow a stable major or minor stream, while regulated workflows can pin the exact commit SHA for immutability. For GitHub Actions, the version reference should tell consumers how much change they are accepting. A full semantic version tag such as v2.4.1 identifies a specific release, while moving tags such as v2.4 or v2 communicate a compatibility stream for patch or minor updates within that line. When a workflow needs exact repeatability, pinning the released action to a full commit SHA provides an immutable reference.
In this scenario, most teams want safe automatic updates within a major version, so semantic major or minor tags are the clearest signal. The smaller regulated group needs exact stability, so they should use the commit SHA for that release. Branch names and vague moving tags do not communicate compatibility clearly, and date-only tags show chronology rather than upgrade scope.
Topic: Manage GitHub Actions for the Enterprise
A nightly internal deployment workflow started failing after the network team allow-listed the source IP from the last successful run. The job still uses runs-on: ubuntu-latest, and the workflow file has not changed.
Run curl -fsS https://inventory.corp.example/health
curl: (7) Failed to connect to inventory.corp.example port 443: Connection timed out
Firewall logs:
- Last successful run source IP: 20.205.92.11
- Current failed run source IP: 20.201.44.18
- Current connection: denied
What is the most likely cause of the failure?
ubuntu-latest routed the job to a self-hosted runner with a different IPGITHUB_TOKEN permission for the service callBest answer: B
Explanation: The curl step ran and then timed out, and the firewall log shows a different source IP than the previous successful run. That pattern matches normal GitHub-hosted runner egress behavior: standard hosted runners do not provide one static outbound IP for every run. Standard GitHub-hosted runners are ephemeral, and their outbound traffic can originate from different GitHub-managed IP addresses on different runs. If a firewall is configured to trust only one IP seen in a prior successful run, a later run can be denied even when the workflow YAML and runner label stay the same.
In this scenario, the key evidence is:
That points to egress IP variation on a GitHub-hosted runner, not to token permissions or secret values. For services that require source-IP restrictions, plan around GitHub’s published IP ranges or use runners with more predictable networking.
Topic: Consume and Troubleshoot Workflows
An organization stores a shared reusable workflow at octo-org/platform-automation/.github/workflows/ci.yml. The platform team updates the main and release branches regularly and may move the v1 tag to newer tested versions. Compliance requires each release repository to keep using the exact reviewed workflow revision until that repository owner intentionally updates it. Which caller configuration is best?
uses: octo-org/platform-automation/.github/workflows/ci.yml@mainuses: octo-org/platform-automation/.github/workflows/ci.yml@releaseuses: octo-org/platform-automation/.github/workflows/ci.yml@8f4b7c2e4d9a6f1c3b2e7a9d0f6c4b1a2e3d5f7cuses: octo-org/platform-automation/.github/workflows/ci.yml@v1Best answer: C
Explanation: The best choice is the full commit SHA because it locks the reusable workflow to an exact reviewed version. Branch references and movable tags are floating references, so updates to shared automation can affect consumers without any change in the caller file. When a repository calls a reusable workflow by branch or by a tag that can be moved, GitHub Actions resolves that reference to whatever commit the ref points to at run time. That means later updates in the shared automation repository can change behavior across consuming repositories automatically. If the requirement is to keep every release repository on the exact reviewed revision until maintainers explicitly adopt a newer one, the caller must pin the reusable workflow to a full commit SHA. This makes version changes deliberate and traceable. The closest distractor is the version tag, but a moved tag is still not an immutable reference.
Topic: Author and Manage Workflows
A workflow runs on both events:
on:
push:
pull_request:
The publish job must run only when code is pushed directly to the release branch or when a pull request targets release. The team wants to avoid brittle conditions that depend on event-specific ref-string formats. Which if: condition is the best configuration for the job?
if: contains(github.ref, 'release')if: (github.event_name == 'push' && github.ref == 'refs/heads/release') || (github.event_name == 'pull_request' && github.base_ref == 'release')if: github.ref_name == 'release'if: github.head_ref == 'release' || github.ref == 'refs/heads/release'Best answer: B
Explanation: The best condition first distinguishes the event and then uses the branch field whose meaning is stable for that event. push evaluates the pushed ref, while pull_request should evaluate base_ref, which is the target branch. GitHub Actions context fields are not interchangeable across events. On a push, github.ref identifies the pushed ref, so refs/heads/release correctly matches only that branch. On a pull_request, the branch being targeted is github.base_ref; by contrast, github.ref points to a PR merge ref and github.head_ref is the source branch.
push logic.base_ref for PR target-branch logic.That keeps the condition aligned to stable semantics instead of depending on ref formats that differ by event.
| Need | Pattern to recognize |
|---|---|
| Run only on certain files | on.<event>.paths or paths-ignore |
| Reduce token risk | Explicit permissions with least privilege |
| Test combinations | strategy.matrix |
| Reuse organization workflow logic | jobs.<job_id>.uses for reusable workflows |
| Reuse repeated steps | Composite action |
| Save build output for another job | Artifact |
| Speed up dependency install | Cache with lock-file-aware keys |
| Protect production deployment | Environment reviewers and environment-scoped secrets |
| Prevent stale preview deployments | concurrency with cancel-in-progress |
| Investigate failures | Logs, runner context, permissions, and debug reruns |
.github/workflows/.