SaveMyCert
Log in
5 of 5 free questions left today·for 30 a day
GH-200 practice

GH-200 sample questions — 25 answered and explained

25 GH-200 sample questions written to the official GitHub Actions exam guide, grouped by exam domain and weighted like the real exam. Each shows the correct answer, why every other option is wrong, and the lesson that teaches it. None of them repeat the examples on the other GH-200 practice pages.

On this page
25
answers included
Full bank
379
on the practice page
Domains
5
weighted like the exam
Free, no account
5/day
a free account allows 30 a day

Domain 1: Author and manage workflows

24% of the GH-200 exam, so 6 of the 25 questions on this page.

Question 1Author and manage workflows

A public repository runs tests with a pull_request workflow. The workflow should post the test results as a comment on each pull request, but on pull requests from forks the comment step fails with a 403 error. Maintainers must not let code from a fork run with a write-capable token or with repository secrets. What should they do?

Choose one.

  • a
    Upload the results as an artifact, then comment from a second workflow triggered by workflow_run Correct

    Correct. The fork's code runs with a read-only token, and the workflow_run workflow runs in the base repository with write access but only reads the uploaded results.

  • b
    Switch the trigger to pull_request_target and check out the pull request's head commit for the tests

    pull_request_target runs with a write token and secrets, so checking out and running the fork's code there breaks the stated constraint.

  • c
    Add pull-requests: write to the permissions block of the existing pull_request workflow

    For pull requests from forks the GITHUB_TOKEN stays read-only, whatever the permissions block requests, so the 403 remains.

  • d
    Post the comment with a personal access token that is stored as a repository secret

    Secrets other than the GITHUB_TOKEN are not passed to runs triggered from forks, and exposing a PAT to fork code would break the constraint anyway.

The concept

Event choice for pull requests from forks: pull_request, pull_request_target and workflow_run.

Why that’s the answer

On pull_request runs from forks, the GITHUB_TOKEN is read-only and secrets are withheld, so a write action fails. pull_request_target runs in the base repository's context with write access and secrets, which is unsafe if it builds or runs the fork's code. The standard split is: test untrusted code on pull_request, save results as an artifact, and let a workflow_run workflow (which runs in the base repository's context) download them and post the comment without executing fork code.

How to reason it out
  1. Explain the 403: fork pull request runs get a read-only token.
  2. Note the constraint: fork code must not run with write access or secrets.
  3. Reject pull_request_target with a head checkout (runs fork code with write access).
  4. Separate untrusted testing from the privileged comment step with workflow_run.

Exam tip: Run fork code under pull_request; do privileged follow-up in a workflow_run workflow that only handles artifacts.

GitHub Actions Triggers, Events, Caching, Artifacts and Outputs (GH-200) — the lesson that teaches this.

Question 2Author and manage workflows

Workflow A runs on release and pushes a new tag with the GITHUB_TOKEN. The tag appears in the repository, but workflow B, which has on: push with tags: ['v*'], never starts for it. Tags pushed by developers do start workflow B. Which change makes workflow B run for tags that workflow A creates?

Choose one.

  • a
    Push the tag in workflow A with a GitHub App installation token Correct

    Correct. Events caused by the GITHUB_TOKEN do not start new workflow runs; a GitHub App token is treated like any other actor, so the tag push triggers workflow B.

  • b
    Grant contents: write to the GITHUB_TOKEN in workflow A's permissions

    Workflow A already pushes the tag successfully, so permissions are not the issue; a tag pushed with the GITHUB_TOKEN still starts no run.

  • c
    Change workflow B's trigger to the create event filtered to tag refs

    A create event caused by the GITHUB_TOKEN is suppressed in the same way as the push, so workflow B still would not run.

  • d
    Make workflow B depend on workflow A's tagging job with a needs entry

    needs links jobs inside one workflow; it cannot reference a job in another workflow file.

The concept

Events triggered by the GITHUB_TOKEN do not create new workflow runs.

Why that’s the answer

To prevent recursive runs, GitHub does not start workflows for events caused by the GITHUB_TOKEN, except workflow_dispatch and repository_dispatch. The tag push works but triggers nothing. Using a GitHub App installation token (or a PAT) for the push makes it a normal event. Extra permissions, a different event type or a needs entry do not change this.

How to reason it out
  1. Note the tag is created, so the push itself succeeds.
  2. Note the difference: developer tags trigger B, GITHUB_TOKEN tags do not.
  3. Recall the rule that GITHUB_TOKEN events start no new runs.
  4. Push with a different credential, such as a GitHub App token.

Exam tip: Anything the GITHUB_TOKEN does (except dispatch events) starts no new workflow runs.

GitHub Actions Triggers, Events, Caching, Artifacts and Outputs (GH-200) — the lesson that teaches this.

Question 3Author and manage workflows

A deployment workflow must start automatically after the workflow named CI finishes on main, and it must deploy only when that CI run succeeded. The two workflows are separate files in the same repository. Which configuration meets the requirement?

Choose one.

  • a
    workflow_run for CI, types: [completed], branches: [main], job if: github.event.workflow_run.conclusion == 'success' Correct

    Correct. completed fires when CI finishes, branches limits it to CI runs on main, and the condition skips deployment when CI failed.

  • b
    workflow_run for CI, types: [completed], branches: [main], and no if condition on the deployment job or on its deploy steps

    completed fires for every finished CI run, including failures and cancellations, so this would deploy after a failed CI run.

  • c
    workflow_run for CI, types: [requested], branches: [main], job if: github.event.workflow_run.conclusion == 'success'

    requested fires when CI starts, before it has a conclusion, so the condition is never true and nothing deploys.

  • d
    on: push for main, with the deployment job listing the CI workflow's test job under its needs key

    needs refers only to jobs in the same workflow file; a job in another workflow cannot be named there.

The concept

workflow_run triggers and checking the triggering run's conclusion.

Why that’s the answer

workflow_run starts a workflow when another named workflow is requested, in progress or completed, and branches filters on the branch the triggering workflow ran on. completed includes failed and cancelled runs, so the job must check github.event.workflow_run.conclusion. needs cannot cross workflow files.

How to reason it out
  1. Pick the event that reacts to another workflow: workflow_run.
  2. Pick the activity type that means 'finished': completed.
  3. Add a condition so only successful runs deploy.
  4. Reject cross-file needs, which is not supported.

Exam tip: workflow_run completed fires on success and failure; check github.event.workflow_run.conclusion before deploying.

GitHub Actions Triggers, Events, Caching, Artifacts and Outputs (GH-200) — the lesson that teaches this.

Question 4Author and manage workflows

A workflow calls a reusable workflow stored in another repository in the same organization. The reusable workflow needs about a dozen of the caller's secrets, and the team does not want to list each secret in the calling job. What should the calling job contain?

Choose one.

  • a
    A secrets map that names each secret the reusable workflow uses

    Mapping secrets explicitly works, but it means listing every secret, which the team wants to avoid.

  • b
    A with block that passes each secret's value as a string input

    Inputs are not meant for secrets and every value would still be listed one by one.

  • c
    secrets: inherit on the job that calls the reusable workflow Correct

    Correct. secrets: inherit passes all of the caller's secrets to a reusable workflow in the same organization or enterprise.

  • d
    Give the reusable workflow's repository access to the organization secrets

    Secret access policies decide which repositories can use an organization secret; they do not pass secrets into a called workflow.

The concept

Passing secrets to reusable workflows.

Why that’s the answer

A called workflow only receives the secrets its caller passes. The caller can map them one by one under secrets, or use secrets: inherit to pass all its secrets implicitly when the reusable workflow is in the same organization or enterprise. Inputs are for non-secret values, and organization secret policies do not pass secrets into a call.

How to reason it out
  1. Note the call is within one organization.
  2. Note the constraint: do not list secrets one by one.
  3. Recall secrets: inherit passes all of the caller's secrets.
  4. Reject explicit maps and inputs, which list each value.

Exam tip: Same org or enterprise and no wish to list secrets: use secrets: inherit.

GitHub Actions Triggers, Events, Caching, Artifacts and Outputs (GH-200) — the lesson that teaches this.

Question 5Author and manage workflows

A reusable workflow and its caller are shown below.# deploy.yml in org/infra on: workflow_call: inputs: target: type: choice options: [staging, production] dry_run: type: boolean default: true secrets: deploy_key: required: true # caller jobs: deploy: uses: org/infra/.github/workflows/deploy.yml@v2 with: target: stagingWhich TWO problems must be fixed for the call to work? (Select TWO.)

Choose TWO.

  • a
    The dry_run input of boolean type in workflow_call needs required: true as well

    A default is normally used with an optional input; nothing requires required: true alongside it.

  • b
    The uses reference needs a branch name, not the v2 tag, for a called workflow

    A reusable workflow can be referenced by a SHA, a tag or a branch, so @v2 is valid.

  • c
    The target input is declared with a type that workflow_call does not accept Correct

    Correct. workflow_call inputs accept only boolean, number and string; choice is a workflow_dispatch type.

  • d
    The caller passes no value for the deploy_key secret marked as required Correct

    Correct. deploy_key is marked required, and the calling job has no secrets mapping or secrets: inherit.

  • e
    The caller's deploy job needs runs-on for the target before it passes a secret

    The called workflow's own jobs declare their runners; the calling job uses uses, with and secrets.

The concept

workflow_call input types and required secrets.

Why that’s the answer

workflow_call inputs are limited to boolean, number and string, so a choice input fails validation (the called workflow can check the value itself instead). A secret marked required must be supplied by the caller, either explicitly or with secrets: inherit. Tags, branches and SHAs are all valid refs, defaults need not be required, and runs-on belongs to the called workflow's jobs.

How to reason it out
  1. Check each workflow_call input type against boolean, number and string.
  2. Check every required secret against what the caller passes.
  3. Check the uses ref format: SHA, tag or branch are allowed.
  4. Confirm runs-on belongs inside the called workflow.

Exam tip: workflow_call inputs: boolean, number or string only; required secrets must be passed or inherited.

GitHub Actions Triggers, Events, Caching, Artifacts and Outputs (GH-200) — the lesson that teaches this.

Question 6Author and manage workflows

A reusable workflow and its caller are shown below.# build.yml on: workflow_call: outputs: image_tag: value: ${{ jobs.package.outputs.tag }} jobs: package: runs-on: ubuntu-latest outputs: tag: ${{ steps.meta.outputs.tag }} steps: - id: meta run: echo "tag=1.4.2" >> "$GITHUB_OUTPUT" # caller jobs: build: uses: ./.github/workflows/build.yml deploy: needs: build runs-on: ubuntu-latestWhich expression in the deploy job returns 1.4.2?

Choose one.

  • a
    ${{ needs.package.outputs.image_tag }}

    package is a job inside the called workflow; the caller only sees the calling job, build, and the outputs the reusable workflow declares.

  • b
    ${{ needs.build.outputs.image_tag }} Correct

    Correct. The caller reads the reusable workflow's workflow-level output through the calling job's name, build, in the needs context.

  • c
    ${{ needs.build.outputs.tag }}

    tag is the job-level output inside the called workflow. The caller sees only the workflow-level name, image_tag.

  • d
    ${{ jobs.build.outputs.image_tag }}

    The jobs context is used inside a reusable workflow to define its outputs; a calling workflow reads results through needs.

The concept

Mapping outputs from a reusable workflow back to its caller.

Why that’s the answer

Data leaves a reusable workflow in three hops: a step writes to GITHUB_OUTPUT, the job maps it under outputs, and on.workflow_call.outputs exposes it with a value referencing jobs.<job>.outputs. The caller then treats the whole call as one job and reads needs.<calling-job>.outputs.<workflow-output-name>. Internal job and output names are not visible to the caller.

How to reason it out
  1. Trace the chain inside build.yml: step meta -> job package output tag -> workflow output image_tag.
  2. Identify the job name the caller used for the call: build.
  3. Combine them in the needs context: needs.build.outputs.image_tag.
  4. Reject names that exist only inside the called workflow.

Exam tip: Callers read needs.<calling-job>.outputs.<workflow_call output>, never the called workflow's internal job names.

GitHub Actions Triggers, Events, Caching, Artifacts and Outputs (GH-200) — the lesson that teaches this.

Domain 2: Consume and troubleshoot workflows

19% of the GH-200 exam, so 5 of the 25 questions on this page.

Question 7Consume and troubleshoot workflows

A workflow run failed in its Deploy step. An engineer suspects a connection error from an earlier step, Run integration tests, which passed. On the run's log page, the engineer types ECONNREFUSED into the Search logs box and gets no matches. The downloaded log archive for the same run contains that text in the integration-test step. What explains the empty search?

Choose one.

  • a
    The log search covers the failed job's annotations, not its step output

    The search box searches log output; annotations are shown on the run summary, and they are not what the search box reads.

  • b
    The log search covers expanded steps, and that passing step was collapsed Correct

    Search logs only includes expanded steps; failed steps are expanded automatically, passing ones are not.

  • c
    The text was masked as a secret, so the page shows it as *** instead

    Masking would also hide the value in the downloaded archive, but the archive shows the text.

  • d
    The log search covers runner diagnostic logs, which are disabled for this run

    Runner diagnostic logs are only in the downloaded archive; the search box searches the step logs on the page.

The concept

On a run's log page, Search logs only includes expanded steps, and only failed steps are expanded automatically.

Why that’s the answer

GitHub's log search covers the steps that are expanded on the page. Failed steps are expanded automatically, so the Deploy step is searched, but the passing integration-test step stays collapsed and is skipped. Expanding that step (or searching the downloaded archive) finds the text. Masking would hide the value everywhere, and neither annotations nor runner diagnostics are what the search box reads.

How to reason it out
  1. Compare the two sources: the archive has the text, the page search does not.
  2. Recall which steps the page search includes: expanded ones.
  3. Recall which steps are expanded by default: failed ones.
  4. Expand the passing step and search again.

Exam tip: Log search only sees expanded steps; expand a passing step before searching it.

Troubleshoot GitHub Actions Workflow Runs: Logs, Matrix Re-runs and Artifacts (GH-200) — the lesson that teaches this.

Question 8Consume and troubleshoot workflows

A workflow run failed, and a developer used Re-run all jobs. The second attempt passed. A teammate now wants to read the logs from the first, failed attempt in the GitHub web interface. Where does the teammate find them?

Choose one.

  • a
    In the workflow's run list, where the first attempt is a separate run entry

    A re-run does not create a new entry with a new run number; it adds an attempt to the same run.

  • b
    In the Artifacts section of the run summary, as an archive named logs-1

    Logs are not stored as artifacts; the Artifacts section holds files uploaded by the workflow.

  • c
    In the Latest dropdown next to the run name, by choosing the first attempt Correct

    Each re-run is an attempt of the same run, and earlier attempts are selected from the Latest dropdown beside the run name.

  • d
    In the Usage view of the run summary, which keeps each attempt's job logs

    The Usage view shows job run times and billable minutes; earlier attempts' logs are chosen from the Latest dropdown.

The concept

A re-run adds a new attempt to the same workflow run; previous attempts are selected from the Latest dropdown on the run page.

Why that’s the answer

Re-running does not create a separate run: the run keeps its run ID and run number, and github.run_attempt increases. The run page shows the latest attempt by default, and the Latest dropdown to the right of the run name lets you open any earlier attempt and its logs. Artifacts and Insights do not hold attempt logs.

How to reason it out
  1. Recall that a re-run is an attempt of the same run.
  2. Open the run page.
  3. Use the Latest dropdown to switch to attempt 1.

Exam tip: Re-runs are attempts of one run; switch attempts from the Latest dropdown.

Troubleshoot GitHub Actions Workflow Runs: Logs, Matrix Re-runs and Artifacts (GH-200) — the lesson that teaches this.

Question 9Consume and troubleshoot workflows

Run 7712 had five jobs. Two failed, so an engineer used Re-run failed jobs, and the second attempt passed. For a post-incident review, the engineer downloads the log archive from the latest attempt and finds that it contains logs for only two jobs. The engineer needs the logs of all five jobs from the first attempt. Which TWO approaches provide them? (Select TWO.)

Choose TWO.

  • a
    Choose attempt 1 in the run page's Latest dropdown, then download its log archive Correct

    The archive of a partial re-run holds only the re-run jobs; switching to attempt 1 gives that attempt's logs for every job.

  • b
    Call GET /repos/{owner}/{repo}/actions/runs/7712/logs, then follow the redirect

    The run-level endpoint returns the log archive of the latest attempt, which is the same partial-re-run archive the engineer already has.

  • c
    Re-run the whole of run 7712 with debug logging enabled, then download the first attempt's archive

    That creates a third attempt with new logs; it does not recover what the first attempt logged.

  • d
    Call GET /repos/{owner}/{repo}/actions/runs/7712/jobs, then read the steps of each listed job

    This lists job and step metadata such as status and timing, not the log text.

  • e
    Call GET /repos/{owner}/{repo}/actions/runs/7712/attempts/1/logs, then follow the redirect Correct

    This endpoint returns a redirect to the log archive of the specific attempt, which includes all five jobs.

The concept

After a partial re-run, the downloaded log archive includes only the re-run jobs; each attempt's logs remain available per attempt in the UI and through the attempts logs endpoint.

Why that’s the answer

GitHub documents that the log archive for a partially re-run workflow includes only the jobs that were re-run. The first attempt's logs are still kept per attempt: select attempt 1 from the Latest dropdown and download its archive, or call the download workflow run attempt logs endpoint, which returns a short-lived redirect to that attempt's archive. The run-level logs endpoint returns the current partial archive, the jobs list holds no log text, and a new re-run produces new logs.

How to reason it out
  1. Explain the short archive: a partial re-run archive holds only re-run jobs.
  2. Recall that each attempt keeps its own logs.
  3. Find attempt 1 in the UI or by attempt number in the REST API.
  4. Reject options that return current or new logs, or only metadata.

Exam tip: For a partial re-run, fetch earlier logs by attempt: the Latest dropdown or /runs/{id}/attempts/{n}/logs.

Troubleshoot GitHub Actions Workflow Runs: Logs, Matrix Re-runs and Artifacts (GH-200) — the lesson that teaches this.

Question 10Consume and troubleshoot workflows

A release manager is writing a script to report every failed run of the workflow file ci.yml that was triggered by a push to main in a repository. The script must ask the API for only those runs instead of downloading every run and filtering locally. Which request should the script send?

Choose one.

  • a
    GET /repos/{owner}/{repo}/actions/runs?branch=main&event=push&workflow=ci.yml&status=failure&per_page=100

    The repository-wide runs endpoint has no workflow filter, so runs of other workflows would be returned; per_page only sets the page size.

  • b
    GET /repos/{owner}/{repo}/actions/workflows/ci.yml/jobs?branch=main&event=push&status=failure

    There is no jobs list under a workflow; jobs are listed per workflow run.

  • c
    GET /repos/{owner}/{repo}/commits/main/check-runs?event=push&workflow=ci.yml&status=failure

    Check runs for a single commit do not give the history of every run of the workflow.

  • d
    GET /repos/{owner}/{repo}/actions/workflows/ci.yml/runs?branch=main&event=push&status=failure Correct

    The list-runs-for-a-workflow endpoint accepts the workflow file name and filters by branch, event and status.

The concept

GET /repos/{owner}/{repo}/actions/workflows/{workflow_id}/runs accepts a workflow file name and filters by branch, event, status, actor and more.

Why that’s the answer

Run history for one workflow comes from the list workflow runs for a workflow endpoint, where workflow_id can be the file name such as ci.yml. Its query parameters include branch, event and status (status also accepts conclusions such as failure), so the server returns only the matching runs. The repository-wide runs endpoint lists every workflow's runs and has no workflow parameter, jobs are listed per run, and check runs for one commit are not a history.

How to reason it out
  1. Identify the scope: runs of one workflow file.
  2. Use the workflows/{workflow_id}/runs endpoint, which accepts ci.yml.
  3. Filter with branch=main, event=push and status=failure.
  4. Reject endpoints with the wrong scope or invented paths.

Exam tip: Run history for one workflow: /actions/workflows/{file}/runs with branch, event and status filters.

Troubleshoot GitHub Actions Workflow Runs: Logs, Matrix Re-runs and Artifacts (GH-200) — the lesson that teaches this.

Question 11Consume and troubleshoot workflows

A push to main started run 5310, which failed because the workflow file referenced a job output with the wrong name. A developer fixed the typo in a commit whose message included [skip ci], pushed it to main, then clicked Re-run failed jobs on run 5310, and it failed with the same error. The workflow has push and workflow_dispatch triggers. Which TWO actions start a run that uses the corrected workflow file? (Select TWO.)

Choose TWO.

  • a
    Re-run run 5310 in full now that the fix commit is on main

    A re-run of run 5310 keeps that run's original GITHUB_SHA and GITHUB_REF, so it uses the unfixed file even though main has moved on.

  • b
    Re-run the failed jobs of run 5310 with workflow debug logging on

    Debug logging adds detail, but the re-run still uses the original commit and the same broken workflow file.

  • c
    Run the workflow manually on main with the Run workflow button Correct

    A workflow_dispatch run on main uses main's latest commit, which has the fix; skip instructions apply only to push and pull_request events.

  • d
    Push a new commit to main with an ordinary commit description Correct

    A new push whose message has no skip instruction creates a new run at the new commit SHA, which contains the corrected workflow file.

  • e
    Re-run the push run 5310 through the REST API with POST .../rerun

    The API re-run is the same operation as Re-run all jobs: it replays the original commit of run 5310.

The concept

A re-run uses the same GITHUB_SHA and GITHUB_REF as the original event, so fixes committed later are not picked up; a commit with a skip instruction starts no push run, but it does not block workflow_dispatch.

Why that’s the answer

GitHub re-runs a workflow with the commit SHA and ref of the original event, so every re-run of run 5310, from the UI or the API, with or without debug logging, evaluates the workflow file as it was at that commit. The fix commit started no run of its own because [skip ci] skips push and pull_request workflows. A new run is needed at a commit containing the fix: a manual workflow_dispatch run on main (skip instructions do not apply to that event) or a further push whose message has no skip instruction.

How to reason it out
  1. Explain the repeat failure: a re-run is tied to the original commit.
  2. Explain the missing new run: [skip ci] suppressed the push run for the fix commit.
  3. Recall that skip instructions affect only push and pull_request events.
  4. Pick the actions that create a new run at a commit containing the fix.

Exam tip: Re-runs replay the original commit; to test a fix, start a new run.

Troubleshoot GitHub Actions Workflow Runs: Logs, Matrix Re-runs and Artifacts (GH-200) — the lesson that teaches this.

Domain 3: Author and maintain actions

18% of the GH-200 exam, so 4 of the 25 questions on this page.

Question 12Author and maintain actions

A team converts a script into this composite action. Every workflow that uses it fails before the step runs, with an error that a required property is missing.name: Print versions description: Prints tool versions runs: using: composite steps: - run: node --version && npm --versionWhat fixes the action?

Choose one.

  • a
    Add defaults.run.shell: bash at the top of action.yml

    defaults is a workflow and job key; action metadata does not accept it.

  • b
    Set defaults.run.shell: bash in each calling workflow's job

    A caller's defaults do not apply to the steps inside a composite action.

  • c
    Add shell: bash to the run step inside action.yml Correct

    Each run step in a composite action must state its shell.

  • d
    Change using: composite to using: node24 in action.yml

    That makes it a JavaScript action, which needs main and has no steps.

The concept

In a composite action, every run step must set shell.

Why that’s the answer

The metadata syntax for composite actions says shell is required when run is set. Unlike a workflow, an action cannot fall back on defaults.run.shell, and a calling job's defaults do not reach into the action. Adding shell to the step fixes the metadata.

How to reason it out
  1. Identify the action type: composite.
  2. Recall the composite rule for run steps.
  3. Discard defaults-based fixes, which do not apply inside actions.

Exam tip: Composite action run steps always need an explicit shell.

Create Custom GitHub Actions: JavaScript, Docker and Composite Actions (GH-200) — the lesson that teaches this.

Question 13Author and maintain actions

A composite action reads a version number:outputs: version: description: Detected version runs: using: composite steps: - id: read shell: bash run: echo "version=$(cat VERSION)" >> "$GITHUB_OUTPUT"A workflow runs it in a step with id: tool and reads steps.tool.outputs.version, which is always empty. What fixes it?

Choose one.

  • a
    Read steps.read.outputs.version in the calling workflow instead of tool

    The inner step id read exists only inside the action; the workflow sees only the action's step, tool.

  • b
    Add value: ${{ steps.read.outputs.version }} under outputs.version Correct

    Composite action outputs must map a value, usually from an inner step's output.

  • c
    Write version=... to GITHUB_ENV instead of GITHUB_OUTPUT in the read step

    That sets an environment variable for later steps; the action's output stays unmapped.

  • d
    Add value: ${{ outputs.version }} under outputs.version in action.yml

    There is no outputs context to read from; the value must come from an inner step.

The concept

A composite action's outputs need a value key that maps an inner step's output to the action's output.

Why that’s the answer

For JavaScript and Docker actions, writing to GITHUB_OUTPUT sets the action's output directly. A composite action is different: its outputs.<id>.value is required and typically references steps.<inner-id>.outputs.<name>. Without it, the inner step's output is never surfaced. The workflow cannot reach inside the action by inner step id; it reads outputs from the step that used the action.

How to reason it out
  1. Note that the action type is composite.
  2. Check the outputs block: description only, no value.
  3. Recall that the caller sees only the action step's outputs.
  4. Map the inner output with value.

Exam tip: Composite outputs need value: ${{ steps.<inner-id>.outputs.<name> }}.

Create Custom GitHub Actions: JavaScript, Docker and Composite Actions (GH-200) — the lesson that teaches this.

Question 14Author and maintain actions

A team rewrites a JavaScript action as a composite action. A run step in it prints the deploy target with echo "Deploying $INPUT_TARGET", which now prints an empty value. The target input can come from pull request titles, so its value must not be pasted into the script text before the shell runs. What should the team change in that step?

Choose one.

  • a
    Replace $INPUT_TARGET with ${{ inputs.target }} directly in the run script

    This prints the value, but the expression is substituted into the script text, which opens script injection.

  • b
    Map env: TARGET: ${{ inputs.target }} on the step and use "$TARGET" in run Correct

    Composite actions read inputs through the inputs context, and passing it through env keeps the value out of the script text.

  • c
    Rename the input in action.yml to INPUT_TARGET so the runner creates that variable

    Composite actions never get INPUT_ variables, whatever the input is called.

  • d
    Replace $INPUT_TARGET with ${{ env.INPUT_TARGET }} in the step's run script text

    No INPUT_TARGET variable exists in a composite action, and the expression is still pasted into the script.

The concept

Composite actions do not get INPUT_<NAME> environment variables; they use the inputs context.

Why that’s the answer

JavaScript and Docker actions receive inputs as INPUT_<NAME> variables, but the metadata reference states that composite actions do not get them automatically and must use ${{ inputs.<id> }}. Writing the expression straight into run works but substitutes untrusted text into the script before bash runs it. Mapping it to an environment variable on the step and quoting that variable gives both the value and safe handling.

How to reason it out
  1. Recall how composite actions receive inputs.
  2. Note the security constraint about untrusted text.
  3. Prefer passing the expression through env over inline substitution.
  4. Choose env mapping with a quoted variable.

Exam tip: Composite: inputs come from ${{ inputs.x }}; pass them through env, not straight into run.

Create Custom GitHub Actions: JavaScript, Docker and Composite Actions (GH-200) — the lesson that teaches this.

Question 15Author and maintain actions

A Docker container action uses this Dockerfile:FROM alpine:3.20 COPY entrypoint.sh /entrypoint.sh ENTRYPOINT ["/entrypoint.sh"]The image builds, but the step fails with exec /entrypoint.sh: permission denied. What should the maintainer do?

Choose one.

  • a
    Add USER root to the Dockerfile before the ENTRYPOINT line

    Action containers already run as root, and GitHub advises against USER in action Dockerfiles.

  • b
    Add WORKDIR /github/workspace to the Dockerfile before ENTRYPOINT

    The working directory is not the problem, and GitHub advises against WORKDIR in action Dockerfiles.

  • c
    Change ENTRYPOINT to CMD ["/entrypoint.sh"] in the Dockerfile

    The container still tries to execute the same non-executable file.

  • d
    Mark entrypoint.sh as executable in Git and push the change Correct

    The script needs execute permission; set it with git update-index --chmod=+x or chmod +x and commit.

The concept

An entrypoint script copied into an action image must have execute permission.

Why that’s the answer

GitHub's Dockerfile guidance for actions says entrypoint.sh must be executable, and a permission denied error is the symptom when it is not. The mode is taken from the file in the repository, so set the executable bit in Git and commit it. USER and WORKDIR are discouraged in action Dockerfiles and do not address file permissions.

How to reason it out
  1. Read the error: the kernel refused to execute the file.
  2. Recall that the image copies the file mode from the repository.
  3. Set the executable bit and commit.

Exam tip: permission denied on entrypoint.sh means the script is not executable: chmod +x and commit.

Create Custom GitHub Actions: JavaScript, Docker and Composite Actions (GH-200) — the lesson that teaches this.

Domain 4: Manage GitHub Actions for the enterprise

24% of the GH-200 exam, so 6 of the 25 questions on this page.

Question 16Manage GitHub Actions for the enterprise

An enterprise sets its default workflow permissions to read repository contents and packages. An organization owner wants every repository in the organization to default to read and write so that release jobs can push tags. What is the result, and how can a release job get write access?

Choose one.

  • a
    The owner can select read and write, which overrides the enterprise default so release jobs get contents permission

    When the enterprise picks the restricted default, the more permissive setting cannot be chosen for its organizations or repositories.

  • b
    The owner cannot select it, but each repository admin can choose read and write in repository settings

    The restriction also applies to repositories, so repository admins cannot choose the permissive default either.

  • c
    The owner cannot select it, and no release workflow can get contents: write permission until the enterprise changes

    The default only sets the starting permissions. A workflow's permissions key can still grant contents: write to its token.

  • d
    The owner cannot select read and write; the release workflow must declare permissions: contents: write Correct

    GitHub does not let organizations or repositories choose the more permissive default when the enterprise is restricted. A workflow can still request the scopes it needs with the permissions key.

The concept

Restrictive defaults set at a higher level cannot be loosened below it, but workflows can still request permissions explicitly.

Why that’s the answer

For workflow permissions, GitHub states that the more permissive setting cannot be chosen as the default for organizations or repositories once the enterprise restricts it. The default is only a starting point: a workflow or job that declares permissions: contents: write gets that scope, which is also least-privilege practice.

How to reason it out
  1. Recall that a restrictive enterprise default flows down to organizations and repositories.
  2. Reject options where an organization or repository loosens it.
  3. Remember the permissions key in the workflow sets the token's scopes per run.
  4. Grant contents: write only in the release workflow.

Exam tip: Restricted default from above stays; request write per workflow with permissions:.

Govern GitHub Actions in the Enterprise: Policies, Templates, Secrets and Variables (GH-200) — the lesson that teaches this.

Question 17Manage GitHub Actions for the enterprise

A scheduled workflow updates a dependency file and opens a pull request using GITHUB_TOKEN. Its job declares contents: write and pull-requests: write. The branch is pushed, but the step that creates the pull request fails with a 403 from the pulls API. Which organization setting must be changed?

Choose one.

  • a
    Change the default workflow permissions to read and write so GitHub Actions can create branches

    The job already requests the write scopes it needs, and this default does not lift the pull-request creation block.

  • b
    Enable "Allow GitHub Actions to create and approve pull requests" Correct

    This setting is off by default and blocks GITHUB_TOKEN from creating or approving pull requests, whatever the job requests.

  • c
    Enable "Send write tokens to workflows from fork pull requests" in the fork settings

    This private-repository fork setting controls tokens for fork pull request runs, not pull request creation.

  • d
    Enable "Require approval for fork pull request workflows" so a maintainer can approve each run

    This holds fork pull request runs for approval; it does not let GITHUB_TOKEN open pull requests.

The concept

A separate workflow-permissions setting decides whether GITHUB_TOKEN may create or approve pull requests.

Why that’s the answer

Under Workflow permissions, "Allow GitHub Actions to create and approve pull requests" is off by default. While it is off, GITHUB_TOKEN cannot open or approve pull requests even with pull-requests: write. The other settings govern default scopes, fork runs and allowed actions.

How to reason it out
  1. Read the symptom: the push works, only creating the pull request is refused.
  2. Note the job already has the right scopes.
  3. Enable the setting that allows Actions to create and approve pull requests.

Exam tip: Opening PRs with GITHUB_TOKEN needs "Allow GitHub Actions to create and approve pull requests".

Govern GitHub Actions in the Enterprise: Policies, Templates, Secrets and Variables (GH-200) — the lesson that teaches this.

Question 18Manage GitHub Actions for the enterprise

Contractors contribute to a private repository from forks. Maintainers want CI to run on these fork pull requests with a read-only token and no access to repository secrets, and a maintainer must approve each run before it starts. Which organization settings for private repositories meet this?

Choose one.

  • a
    Enable "Run workflows from fork pull requests" and "Send secrets and variables to workflows from fork pull requests"

    This passes repository secrets to fork runs, which the maintainers explicitly do not want, and adds no approval step.

  • b
    Enable "Run workflows from fork pull requests" and "Send write tokens to workflows from fork pull requests"

    This gives fork runs a write token instead of read-only, and adds no approval step.

  • c
    Enable "Run workflows from fork pull requests" and "Require approval for fork pull request workflows" Correct

    Fork runs get a read-only token and no secrets by default, and the approval option holds each run until a maintainer approves it.

  • d
    Choose "Require approval for first-time contributors" under the fork pull request approval setting

    That approval setting is for fork workflows in public repositories; private repositories use their own fork policy options.

The concept

Private and internal repositories have their own fork pull request policies: run, send write tokens, send secrets, require approval.

Why that’s the answer

Enabling fork runs alone gives a read-only GITHUB_TOKEN and no secrets. Adding "Require approval for fork pull request workflows" makes each run wait for approval. "Send write tokens to workflows from fork pull requests" and "Send secrets and variables to workflows from fork pull requests" loosen exactly what must stay closed, and the first-time and external contributor approval options belong to public repositories.

How to reason it out
  1. Note the repository is private, so the private fork policies apply.
  2. Enable fork runs, which default to read-only and no secrets.
  3. Leave write tokens and secrets off.
  4. Turn on approval for fork pull request workflows.

Exam tip: Private forks: enable runs + require approval; leave write tokens and secrets off.

Govern GitHub Actions in the Enterprise: Policies, Templates, Secrets and Variables (GH-200) — the lesson that teaches this.

Question 19Manage GitHub Actions for the enterprise

A public repository is set to "Require approval for all external contributors". A maintainer sees that one workflow ran on a first-time contributor's fork pull request without anyone approving it, while the test workflow is waiting for approval. Which trigger does the workflow that ran most likely use?

Choose one.

  • a
    pull_request_target with types: [opened] Correct

    Workflows triggered by pull_request_target run in the base repository's context and always run, whatever the approval setting.

  • b
    pull_request with types: [opened, synchronize]

    Fork runs triggered by pull_request are held for approval under this setting, whatever activity types they filter on.

  • c
    pull_request with a paths: filter on src/**

    Path filters decide whether the workflow is needed; they do not bypass the approval requirement.

  • d
    workflow_run on completion of the test workflow

    A workflow_run workflow starts only after the test workflow finishes, and that run is still waiting for approval.

The concept

Fork pull request approval settings hold pull_request runs, but pull_request_target always runs.

Why that’s the answer

GitHub documents that pull_request_target workflows on the base branch always run, regardless of approval settings, because they run in the base repository's trusted context. That is why such workflows must never check out and execute the fork's code with secrets available.

How to reason it out
  1. Note that the approval setting applies to fork pull request workflows.
  2. Recall the documented exception for pull_request_target.
  3. Exclude the pull_request variants, which are held, and workflow_run, which waits for the held test run.

Exam tip: Fork approval never holds pull_request_target runs.

Govern GitHub Actions in the Enterprise: Policies, Templates, Secrets and Variables (GH-200) — the lesson that teaches this.

Question 20Manage GitHub Actions for the enterprise

A security team owns a scan workflow in a central repository. It must run on every pull request in all of the organization's repositories and block merging when it fails. The team must not add or change files in the other repositories. What should an organization owner use?

Choose one.

  • a
    A workflow template stored in the workflow-templates folder of the organization's .github repository

    A template is only offered as a starting copy; nothing runs until each repository adds and commits it.

  • b
    A reusable workflow in the central repository that each repository calls from a job

    Each repository would need its own caller workflow file, which breaks the no-changes constraint.

  • c
    A required status check added to the branch protection rules of each repository

    A status check only blocks merging; the scan workflow would still need to exist in every repository to report it.

  • d
    An organization ruleset with the "Require workflows to pass before merging" rule Correct

    This rule runs the central workflow on pull requests in the targeted repositories and blocks merging until it passes, without files in those repositories.

The concept

Organization rulesets can require a workflow from a central repository to pass before merging.

Why that’s the answer

The "Require workflows to pass before merging" rule is configured once at organization level, names a source repository and workflow file, and runs it on pull requests in the targeted repositories. Templates, reusable workflows and per-repository status checks all need something added to each repository.

How to reason it out
  1. Note the requirement: central workflow, enforced, no per-repository files.
  2. Rule out templates and reusable workflows, which need files in each repository.
  3. Rule out status checks, which need the workflow to exist in each repository.
  4. Choose the ruleset workflow rule.

Exam tip: Enforce a central workflow on every PR with an organization ruleset workflow rule.

Govern GitHub Actions in the Enterprise: Policies, Templates, Secrets and Variables (GH-200) — the lesson that teaches this.

Question 21Manage GitHub Actions for the enterprise

An organization ruleset requires security.yml from contoso/ci-policies to pass before merging, and targets the organization's private and internal repositories. contoso/ci-policies is private and nobody has changed its configuration since it was created. The workflow never starts on pull requests in any targeted repository. Which TWO changes are needed for it to run in both private and internal repositories? (Select TWO.)

Choose TWO.

  • a
    Change contoso/ci-policies to internal visibility Correct

    A private workflow file can run only in private repositories; an internal one can run in internal and private repositories.

  • b
    Add merge_group to the on: section of security.yml

    merge_group is supported but optional; the workflow needs only one of pull_request, pull_request_target or merge_group.

  • c
    Add branches: [main] under pull_request in security.yml

    Branch and path filters on these events are ignored in ruleset workflows; the ruleset decides where it applies.

  • d
    Allow access to the workflow from outside contoso/ci-policies in its Actions Access setting Correct

    An internal or private workflow file must be made accessible from outside its repository before other repositories can run it.

  • e
    Copy security.yml from contoso/ci-policies into .github/workflows of each internal repository

    Copies would run as ordinary workflows, but the ruleset still points at contoso/ci-policies, so the requirement would not be met.

The concept

A ruleset workflow's source repository visibility and access setting decide where it can run.

Why that’s the answer

Two rules combine. The workflow file must be in a repository whose visibility matches the targets: public runs anywhere, internal runs in internal and private repositories, private runs only in private ones. And an internal or private workflow must be made accessible from outside its repository. So the source repository becomes internal and its Access setting allows the organization. Event additions and filters do not change visibility rules, and copies are not what the ruleset checks.

How to reason it out
  1. Note the source: a private repository with the default Access setting, so no other repository can use its workflows yet.
  2. Apply the visibility rule: a private source serves only private repositories, never internal ones.
  3. Make the source internal so it can serve internal and private repositories.
  4. Allow access from outside the source repository in its Access setting.

Exam tip: Ruleset workflow source: visibility must cover the targets, and access must be opened.

Govern GitHub Actions in the Enterprise: Policies, Templates, Secrets and Variables (GH-200) — the lesson that teaches this.

Domain 5: Secure and optimize automation

15% of the GH-200 exam, so 4 of the 25 questions on this page.

Question 22Secure and optimize automation

A job checks out a repository's code and posts a review summary on the pull request. It does nothing else. Which permissions block gives the job's GITHUB_TOKEN the least access it needs?

Choose one.

  • a
    permissions: contents: write pull-requests: write

    The job never pushes code, so contents: write grants more than it needs.

  • b
    permissions: contents: read pull-requests: read

    Read access to pull requests cannot create a review, so the posting step fails.

  • c
    permissions: contents: read pull-requests: write Correct

    Reading code needs contents: read and writing the review needs pull-requests: write; nothing else is granted.

  • d
    permissions: contents: read pull-requests: write issues: write

    The job never touches issues, so issues: write is access it does not use.

The concept

Least-privilege GITHUB_TOKEN: list only the scopes and access levels the job uses.

Why that’s the answer

The job needs to read code and write to pull requests. contents: read plus pull-requests: write covers both, and every other scope becomes none. Contents write is unnecessary, pull-requests read cannot post a review, and issues: write is a scope the job never uses.

How to reason it out
  1. List the job's actions: read code, write a pull request review.
  2. Map each action to a scope and the lowest level that works.
  3. Pick the block with exactly those scopes.

Exam tip: Grant each scope the job uses at the lowest level that works; leave the rest out.

Secure and Optimize GitHub Actions: Permissions, OIDC, Pinning, Attestations and Caching (GH-200) — the lesson that teaches this.

Question 23Secure and optimize automation

A workflow that runs on issue_comment has this step:- uses: actions/github-script@v8 with: script: | const body = "${{ github.event.comment.body }}"; await core.summary.addRaw(body).write();A reviewer warns that a crafted comment could run arbitrary JavaScript in the job. Which change removes the risk?

Choose one.

  • a
    Change the double quotes around the expression to backticks so the comment becomes a template literal

    The expression is still pasted into the source, and a template literal even evaluates ${} inside the comment.

  • b
    Wrap github.event.comment.body in toJSON() and keep it inside the double quotes

    toJSON adds its own quotes inside the existing ones, so the comment text is still spliced into the code.

  • c
    Set env: BODY: ${{ github.event.comment.body }} on the step and read process.env.BODY in the script Correct

    The comment then reaches the script as data in an environment variable instead of being pasted into the code.

  • d
    Add an ::add-mask:: command for the comment body in a step that runs before the script

    Masking hides a value in logs; it does not stop the value from being executed as code.

The concept

Expressions in ${{ }} are substituted into the script text before it runs; pass untrusted input through env instead.

Why that’s the answer

GitHub evaluates ${{ github.event.comment.body }} and writes the result into the JavaScript source before github-script runs it, so a comment containing a quote can close the string and append code. Setting the value as an environment variable and reading process.env.BODY keeps it as data. Changing the quote style or adding toJSON inside existing quotes still splices text into code, and masking only affects logs.

How to reason it out
  1. Spot the untrusted value: the comment body.
  2. Recall that ${{ }} is substituted into the script source before execution.
  3. Reject fixes that still splice the value into the code.
  4. Pass the value through env and read it at run time.

Exam tip: Never put untrusted ${{ }} values inside script code; hand them over through environment variables.

Secure and Optimize GitHub Actions: Permissions, OIDC, Pinning, Attestations and Caching (GH-200) — the lesson that teaches this.

Question 24Secure and optimize automation

A triage workflow passes an issue title to a shell script. The step defines env: TITLE: ${{ github.event.issue.title }}. The team must pick the run: line that cannot be turned into extra shell commands by a crafted title. Which run: line should it use?

Choose one.

  • a
    run: ./triage.sh "${{ env.TITLE }}"

    The expression is substituted into the script text before the shell runs, so the title can still inject commands.

  • b
    run: ./triage.sh "$TITLE" Correct

    The shell expands the variable at run time, and the double quotes keep it as one argument.

  • c
    run: ./triage.sh '${{ github.event.issue.title }}'

    A title containing a single quote closes the quoting, and the rest is parsed as shell code.

  • d
    run: ./triage.sh ${{ toJSON(env.TITLE) }}

    toJSON escapes for JSON, not for the shell; characters such as $( ) still reach the shell parser.

The concept

Pass untrusted input through an environment variable and reference it as a quoted shell variable, not through ${{ }}.

Why that’s the answer

Anything inside ${{ }} is replaced in the script text before the shell starts, including ${{ env.TITLE }}, so it gives no protection. Referencing $TITLE lets the shell read the value from the environment at run time, and double quotes stop word splitting. Single quotes and toJSON both leave the title in the script source.

How to reason it out
  1. Separate template substitution (${{ }}) from shell expansion ($VAR).
  2. Recall that ${{ env.X }} is still template substitution.
  3. Pick the line that uses the shell variable, double-quoted.

Exam tip: Use "$VAR" in run:, never ${{ env.VAR }}, for untrusted values.

Secure and Optimize GitHub Actions: Permissions, OIDC, Pinning, Attestations and Caching (GH-200) — the lesson that teaches this.

Question 25Secure and optimize automation

A public repository has a workflow triggered by pull_request_target that interpolates several context values directly into a run: step. Contributors open pull requests from their own forks. Which TWO values can an outside contributor set to arbitrary text? (Select TWO.)

Choose TWO.

  • a
    github.event.pull_request.number

    GitHub assigns the number; it is an integer the contributor cannot shape.

  • b
    github.repository

    For pull_request_target this is the base repository's owner and name, set by its maintainers.

  • c
    github.run_id

    GitHub generates the run ID as a number; the contributor has no influence over its text.

  • d
    github.head_ref Correct

    It is the name of the contributor's branch in their fork, which they choose.

  • e
    github.event.pull_request.body Correct

    The pull request description is written by whoever opens the pull request.

The concept

Fields a contributor writes (branch names, titles, bodies, comments, commit messages, author names and emails) are untrusted input.

Why that’s the answer

Treat any context value that a contributor writes as attacker-controlled: the head branch name, pull request title and body, comment and review bodies, commit messages and author details. Numbers and IDs that GitHub generates, and the base repository's name, are not shaped by the contributor. Untrusted values should reach scripts only through environment variables.

How to reason it out
  1. Ask who writes each value: the contributor or GitHub/maintainers.
  2. Mark branch names and free-text fields as untrusted.
  3. Mark GitHub-generated numbers and the base repository name as not attacker-controlled.

Exam tip: Branch names and any free text from a pull request, issue or commit are untrusted.

Secure and Optimize GitHub Actions: Permissions, OIDC, Pinning, Attestations and Caching (GH-200) — the lesson that teaches this.

Keep going with GH-200

GH-200 practice by domain

GH-200 sample questions: your questions

No. Real exam questions are confidential. These are original questions written to the skills in the official GH-200 exam guide, so they test the same knowledge in the same style without being copies of the exam.

Source

The domains and weights on this page come from the official GH-200 exam guide.