Browse Certification Practice Tests by Exam Family

GitHub Actions GH-200: Author and Maintain Actions

Try 10 focused GitHub Actions GH-200 questions on Author and Maintain Actions, with explanations, then continue with IT Mastery.

On this page

Open the matching IT Mastery practice page for timed mocks, topic drills, progress tracking, explanations, and full practice.

Try GitHub Actions GH-200 on Web View full GitHub Actions GH-200 practice page

Topic snapshot

FieldDetail
Exam routeGitHub Actions GH-200
Topic areaAuthor and Maintain Actions
Blueprint weight18%
Page purposeFocused sample questions before returning to mixed practice

How to use this topic drill

Use this page to isolate Author and Maintain Actions for GitHub Actions GH-200. Work through the 10 questions first, then review the explanations and return to mixed practice in IT Mastery.

PassWhat to doWhat to record
First attemptAnswer without checking the explanation first.The fact, rule, calculation, or judgment point that controlled your answer.
ReviewRead the explanation even when you were correct.Why the best answer is stronger than the closest distractor.
RepairRepeat only missed or uncertain items after a short break.The pattern behind misses, not the answer letter.
TransferReturn to mixed practice once the topic feels stable.Whether the same skill holds up when the topic is no longer obvious.

Blueprint context: 18% of the practice outline. A focused topic score can overstate readiness if you recognize the pattern too quickly, so use it as repair work before timed mixed sets.

Sample questions

These questions are original IT Mastery practice items aligned to this topic area. They are designed for self-assessment and are not official exam questions.

Question 1

Topic: Author and Maintain Actions

An organization has 60 private repositories that all need the same internally supported signing logic. Each team must insert that logic at different points in different jobs, security prohibits public distribution, and the platform team wants one codebase with independent versioning and centralized support. Which configuration is best?

Options:

  • A. Move the logic into a central reusable workflow that every repository calls.

  • B. Create a dedicated private action repository and let organization repositories consume versioned releases from it.

  • C. Publish the action from a public repository to GitHub Marketplace.

  • D. Copy the composite action into each consuming repository.

Best answer: B

Explanation: A dedicated private action repository best fits because the shared logic is a reusable step sequence, not an entire standardized job. It also keeps the action internal while giving the platform team one supported codebase with its own release lifecycle.

Choose the distribution boundary that matches both the reuse unit and the trust boundary. Here, teams need the same logic inserted at different places inside different jobs, so an action is the right reusable component. Because many private repositories will use it and the platform team wants separate ownership, support, and versioning, that action should live in its own private repository rather than inside each application repository.

A public Marketplace publication would break the internal-only requirement. A reusable workflow is better when you want to standardize an entire job or workflow-call boundary, not when teams need a shared step sequence they can place flexibly. The key takeaway is to use a private, centrally maintained action repository when reuse spans many repositories but must remain internal.

  • Copying the action into every repository removes centralized maintenance and makes support and version control inconsistent.
  • Publishing through a public repository or Marketplace violates the stated internal-only security boundary.
  • Using a reusable workflow is too coarse when teams need to place shared logic at different step positions within different jobs.

Question 2

Topic: Author and Maintain Actions

A team maintains a local composite action that should make a CLI available to later workflow steps.

# .github/actions/setup-tool/action.yml
runs:
  using: composite
  steps:
    - shell: bash
      run: |
        TOOL_DIR="$GITHUB_ACTION_PATH/bin"
        export TOOL_HOME="$TOOL_DIR"
        export PATH="$TOOL_DIR:$PATH"

# .github/workflows/build.yml
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: ./.github/actions/setup-tool
      - run: |
          echo "$TOOL_HOME"
          mytool --version

The second step prints an empty TOOL_HOME and mytool: command not found. Which edit best fixes the action safely so later steps in the same job can use both values?

Options:

  • A. Replace the export lines with ::set-env and ::add-path workflow commands.

  • B. Replace the export lines by writing TOOL_HOME to GITHUB_ENV and TOOL_DIR to GITHUB_PATH.

  • C. Keep the action unchanged and add an env: block on the uses: ./.github/actions/setup-tool step.

  • D. Replace the export lines by writing both values to GITHUB_OUTPUT so following steps inherit them automatically.

Best answer: B

Explanation: The action fails because export only changes the environment of that single shell process. To make an environment variable and a PATH entry available to later steps in the same job, the action must write to the supported environment files: GITHUB_ENV and GITHUB_PATH.

The key concept is step scope. In a composite action, export TOOL_HOME=... and export PATH=... affect only the shell running that one run step. When the workflow moves to the next step, GitHub Actions starts a new process, so those exported values are gone.

- shell: bash
  run: |
    TOOL_DIR="$GITHUB_ACTION_PATH/bin"
    echo "TOOL_HOME=$TOOL_DIR" >> "$GITHUB_ENV"
    echo "$TOOL_DIR" >> "$GITHUB_PATH"

GITHUB_ENV makes TOOL_HOME available to later steps in the same job, and GITHUB_PATH safely adds the tool directory to command lookup. Using step outputs would require explicit consumption and still would not update PATH automatically.

  • GITHUB_OUTPUT creates step outputs, not automatic job-wide environment variables or PATH updates.
  • An env: block on the uses step applies to that step invocation and does not make later workflow steps inherit the action’s exported shell state.
  • ::set-env and ::add-path are deprecated; environment files are the supported safe mechanism.

Question 3

Topic: Author and Maintain Actions

Your JavaScript action is still on major version 1, and consumers use uses: contoso/cache-action@v1. The team sometimes pushes prerelease tags such as v1.5.0-rc.1 before publishing a GitHub release. The v1 tag must move only after a non-prerelease release is published.

name: update-major
on:
  push:
    tags:
      - 'v1.*'
jobs:
  move-tag:
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - uses: actions/checkout@v4
      - run: |
          git tag -fa v1 -m "Update v1"
          git push origin refs/tags/v1 --force

Which edit best satisfies the requirement?

Options:

  • A. Keep the tag-push trigger, but add if: ${{ !contains(github.ref_name, '-') }} before updating v1.

  • B. Change the trigger to release types: [created] so v1 moves when the release entry is first made.

  • C. Replace the trigger with release types: [published], add if: ${{ !github.event.release.prerelease }}, and checkout ${{ github.event.release.tag_name }} before updating v1.

  • D. Change the trigger to release types: [published], but checkout ref: main before updating v1.

Best answer: C

Explanation: A floating major tag such as v1 should advance only when the stable release is actually published, not when any matching tag is pushed. Using release with types: [published], checking github.event.release.prerelease, and checking out the released tag keeps v1 aligned to the correct backward-compatible release commit.

Major-version tags for custom actions are movable aliases, while full semantic version tags such as v1.4.2 should remain fixed release points. In this workflow, push on tags is too early because a matching tag can be pushed before the GitHub release is published, and prerelease tags can match the same pattern.

The safe pattern is:

  • trigger on release with types: [published]
  • skip the job when github.event.release.prerelease is true
  • checkout the released tag with ${{ github.event.release.tag_name }}
  • force-move v1 to that released commit

The existing contents: write permission is what allows the workflow to update the tag. Checking out main would make v1 follow a branch tip instead of the immutable commit associated with the published release. The key point is that the floating major tag should follow the published stable release, not tag pushes or branch heads.

  • Tag-push guard still runs before a GitHub release is published, so it can move v1 too early.
  • Release created can happen before final publication, including draft creation, so it is not the right release point.
  • Checkout main makes v1 follow the latest branch commit rather than the published release tag commit.

Question 4

Topic: Author and Maintain Actions

A team created a local JavaScript action at .github/actions/label-release/ and calls it from a workflow with uses: ./.github/actions/label-release. The job fails before any script output appears.

Exhibit:

# .github/actions/label-release/action.yml
name: Release labeler
description: Adds a label based on release type
inputs:
  release-type:
    description: Release channel
    required: true
outputs:
  applied-label:
    description: Label added to the issue

Log excerpt:

Error: action metadata is invalid
Required property is missing: runs

What is the best explanation for the failure?

Options:

  • A. The workflow needs permissions: issues: write before GitHub can load the local action.

  • B. The local action must be stored under .github/workflows instead of .github/actions.

  • C. The action output should be defined only through GITHUB_ENV, not in action.yml.

  • D. The action metadata is missing a runs section that declares the runtime and entry point.

Best answer: D

Explanation: The failure occurs during action metadata validation, not during script execution. For a JavaScript action, action.yml must include a runs block that tells GitHub which runtime to use and which file to execute.

Custom actions need an action.yml (or action.yaml) metadata file. For a JavaScript action, that file can include name, description, inputs, and outputs, but it also must include a runs section so GitHub knows how to execute the action.

A minimal JavaScript action metadata file includes:

  • runs.using for the runtime
  • runs.main for the entry file

Because the log says the required property runs is missing, GitHub stops before the action code starts. Permissions problems or output-writing mistakes would happen later, after the action had already been loaded and executed.

  • The option claiming the action belongs under .github/workflows is incorrect because that folder is for workflow files, while local actions are commonly stored in paths such as .github/actions.
  • The option about issues: write is a later runtime concern; missing permissions would not cause a metadata validation error about runs.
  • The option about GITHUB_ENV is incorrect because action outputs are declared in metadata and runtime output values are written with GITHUB_OUTPUT, not GITHUB_ENV.

Question 5

Topic: Author and Maintain Actions

You are authoring a composite action named package-report. The action should behave as follows:

  • The caller must provide report-path.
  • format is optional and should default to sarif.
  • include-summary is optional and should default to true.
  • Each input needs a description in action.yml.

Which inputs definition is the best configuration?

Options:

  • A. ```yaml inputs: report-path: description: Path to the generated report file required: false default: ./results.sarif format: description: Output format for the published report required: true include-summary: description: Whether to add a workflow summary required: false default: ’true'

- B. ```yaml
inputs:
  report-path:
    description: Path to the generated report file
    required: true
  format:
    description: Output format for the published report
    required: false
    default: sarif
  include-summary:
    description: Whether to add a workflow summary
    required: false
    default: 'true'
  • C. ```yaml inputs: report-path: description: Path to the generated report file required: true format: description: Output format for the published report required: false default: json include-summary: description: Whether to add a workflow summary required: false default: ‘false’

- D. ```yaml
inputs:
  report-path:
    description: Path to the generated report file
    required: true
  format:
    description: Output format for the published report
    required: true
    default: sarif
  include-summary:
    description: Whether to add a workflow summary
    required: true
    default: 'true'

Best answer: B

Explanation: The best metadata definition matches the action contract exactly: one required caller-supplied input and two optional inputs with explicit defaults. That keeps the action interface clear and prevents callers from being forced to pass values the action already knows how to default.

In action.yml, input metadata should mirror how the action is meant to be used. A value the caller must always supply should be marked required: true and normally should not rely on a fallback. Optional behavior should be marked required: false and given a default when the action has a known fallback value.

Here, report-path must come from the caller, so it is the only required input. format and include-summary are optional because the action is supposed to use sarif and true when the caller omits them. Including descriptions for each input makes the contract clear to anyone consuming the action.

The closest distractors either change which values are required or change the defaults, which means the metadata no longer matches the stated behavior.

  • The option that makes report-path optional changes the contract because the caller is no longer required to supply the file path.
  • The option that marks format and include-summary as required contradicts the requirement that both are optional with defaults.
  • The option that uses json and 'false' changes the declared fallback behavior of the action.

Question 6

Topic: Author and Maintain Actions

A team rewrote a composite action so invalid caller inputs fail with targeted guidance. The action defines environment as required. approver, token, and use_oidc are optional, and omitted optional inputs resolve to empty strings. In this run, secrets.DEPLOY_TOKEN exists.

# action.yml (fragment)
runs:
  using: composite
  steps:
    - shell: bash
      env:
        ENVIRONMENT: ${{ inputs.environment }}
        APPROVER: ${{ inputs.approver }}
        USE_OIDC: ${{ inputs.use_oidc }}
        TOKEN: ${{ inputs.token }}
      run: |
        if [[ "$ENVIRONMENT" == "prod" && -z "$APPROVER" ]]; then
          echo "::error title=Invalid action configuration::For production, set input 'approver' in the calling workflow."
          exit 1
        fi
        if [[ "$USE_OIDC" == "true" && -n "$TOKEN" ]]; then
          echo "::error title=Invalid action configuration::Use either 'token' or 'use_oidc: true', not both."
          exit 1
        fi

# caller step
- uses: org/deploy-action@v1
  with:
    environment: prod
    use_oidc: 'true'
    token: ${{ secrets.DEPLOY_TOKEN }}

Which message is emitted by the validation step?

Options:

  • A. For production, set input approver in the calling workflow.

  • B. Input required and not supplied: approver.

  • C. No error is emitted; validation passes.

  • D. Use either token or use_oidc: true, not both.

Best answer: A

Explanation: The validation logic runs top to bottom. Because the caller sets environment to prod and omits approver, the first condition is true and the script exits immediately with the targeted production guidance.

This item tests how a custom action surfaces a helpful configuration error during execution. The caller has two bad settings, but the script stops at the first failing check. Here, ENVIRONMENT is prod, APPROVER is empty, and the first if emits a message that tells the workflow author exactly what to add in the caller.

  • ENVIRONMENT=prod
  • APPROVER=""
  • first validation branch runs
  • exit 1 stops the step immediately

Because the step exits on that first branch, the later token plus use_oidc conflict is never evaluated.

  • The token-and-OIDC conflict is real, but it is checked second and never runs after the first exit 1.
  • The missing-required-input message would only occur if approver were defined as required in the action metadata.
  • The no-error option ignores that the first condition is satisfied and explicitly fails the step.

Question 7

Topic: Author and Maintain Actions

A platform team maintains an internal action used as uses: org/release-note@v2. Consuming workflows run on self-hosted Linux runners that have no outbound internet except GitHub and no Docker daemon.

The current action.yml is:

runs:
  using: node16
  main: src/index.js

The action repo contains package.json, but its npm dependencies are not bundled or committed. Runs now fail with:

Error: Cannot find module '@actions/core'
The node16 runtime is not supported on this runner

The team wants the most reliable configuration with no extra setup in consuming workflows. Which configuration is best?

Options:

  • A. Convert it to a composite action that runs node src/index.js.

  • B. Bundle dependencies into dist, commit it, and use node20.

  • C. Keep src/index.js and add npm ci before each use.

  • D. Convert it to a Docker action built from node:20.

Best answer: B

Explanation: A JavaScript action should ship executable code and dependencies, typically as a bundled dist/index.js, because consuming workflows do not install the action’s npm packages automatically. Updating runs.using to a supported runtime such as node20 fixes the Node compatibility failure at the same time.

This failure has two causes: packaging and runtime selection. For a JavaScript action, the consumer runner does not run npm install for the action repository, so pointing main to src/index.js while leaving dependencies only in package.json can produce Cannot find module errors. The reliable fix is to build or bundle the action and commit the generated output, commonly dist/index.js, then set main to that file.

The second error comes from declaring an unsupported Node runtime in action.yml. Updating runs.using to a supported value such as node20 lets the runner execute the action on current GitHub Actions infrastructure. Requiring each workflow to bootstrap the action, or switching to Docker on runners without Docker, adds fragility instead of fixing the action package itself.

  • The workflow bootstrap idea fails because consuming workflows do not normally install dependencies inside the referenced action repository, and the runners cannot fetch arbitrary packages anyway.
  • The composite-action approach still relies on node plus the action’s local npm packages being present, so it does not solve the missing dependency problem.
  • The Docker-action approach could change the runtime model, but it cannot run on the stated runners because Docker is unavailable and it is unnecessary for this scenario.

Question 8

Topic: Author and Maintain Actions

A team is troubleshooting a local composite action used by a matrix job on GitHub-hosted runners. The repository contains .github/actions/pkg/scripts/build.sh, and build.sh has the executable bit committed in Git.

# workflow.yml
jobs:
  pack:
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest]
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: ./.github/actions/pkg

# .github/actions/pkg/action.yml
name: pkg
runs:
  using: composite
  steps:
    - name: Run build
      shell: bash
      working-directory: ${{ github.action_path }}\scripts
      run: ./build.sh

Which execution trace matches GitHub Actions behavior?

Options:

  • A. Both runners run build.sh because the path is normalized.

  • B. Windows runs build.sh; Ubuntu fails before bash starts.

  • C. Ubuntu runs build.sh; Windows fails because bash is unsupported.

  • D. Both runners fail because build.sh still needs chmod +x.

Best answer: B

Explanation: github.action_path resolves to the action folder on each runner, but the appended \scripts hard-codes a Windows path separator. On Ubuntu, the runner cannot resolve that working directory, so the step fails before bash runs. On Windows, the directory is valid and the script can run.

In a composite action, working-directory is resolved by the runner before the run command starts. Here, ${{ github.action_path }} expands to the absolute path of the action directory, but \scripts makes the final path Windows-specific. On Ubuntu, that becomes a path ending in pkg\scripts, which Linux treats as a different name rather than the scripts subfolder, so the runner cannot change into the directory and the step stops before shell execution.

On Windows, the same path is valid. Because the step explicitly uses shell: bash, the Windows GitHub-hosted runner can launch Bash for that step. Since build.sh already has its executable bit committed in Git, there is no extra permission fix required in this scenario.

For cross-platform action authoring, use an OS-neutral path such as ${{ github.action_path }}/scripts.

  • Permission trap fails because the script already has the executable bit committed, so no extra chmod +x step is required here.
  • Shell trap fails because GitHub-hosted Windows runners can run steps with shell: bash.
  • Normalization trap fails because the runner does not rewrite a Windows-style backslash path into a Linux subdirectory path.

Question 9

Topic: Author and Maintain Actions

Consider this workflow job:

jobs:
  lint-notes:
    runs-on: ubuntu-latest
    steps:
      - id: notes
        run: |
          NOTES=$(cat notes.txt)
          echo "::warning::$NOTES"
          echo "notes=$NOTES" >> "$GITHUB_OUTPUT"
      - run: echo "${{ steps.notes.outputs.notes }}"

notes.txt can contain multiple lines and text such as ::notice::review me. Which edit best preserves the full value for steps.notes.outputs.notes and avoids malformed workflow command parsing?

Options:

  • A. Replace both lines with echo "::set-output name=notes::$NOTES".

  • B. Escape %, CR, and LF before ::warning::, and write notes to GITHUB_OUTPUT with the multiline notes<<EOF format.

  • C. Replace GITHUB_OUTPUT with GITHUB_ENV and reference &#36;{{ env.notes }} in the next step.

  • D. Keep ::warning:: unchanged, but write the output with printf 'notes=%s\n' "&#36;NOTES" >> "&#36;GITHUB_OUTPUT".

Best answer: B

Explanation: The value is unsafe in two different ways: annotation commands parse special characters, and GITHUB_OUTPUT single-line syntax does not safely carry multiline data. The fix is to escape the warning message and use the multiline environment-file form for the step output.

GitHub Actions still parses lines such as ::warning::... as workflow commands, so arbitrary text inserted into that command must escape %, carriage returns, and newlines. Otherwise, the runner can misread the message or treat later lines as separate commands. For step outputs, the current pattern is the GITHUB_OUTPUT file, and multiline values must use the delimiter form rather than name=value.

A safe pattern is:

esc=&#36;{NOTES//'%'/'%25'}
esc=&#36;{esc//$'\n'/'%0A'}
esc=&#36;{esc//$'\r'/'%0D'}
echo "::warning::&#36;esc"
{
  echo 'notes<<EOF'
  printf '%s\n' "&#36;NOTES"
  echo 'EOF'
} >> "&#36;GITHUB_OUTPUT"

Using GITHUB_ENV changes the data to an environment variable for later steps in the same job, while ::set-output is deprecated and should not be used.

  • Single-line output write fails because printf 'notes=%s\n' still produces invalid multiline output when the value itself contains line breaks.
  • Using GITHUB_ENV changes the mechanism; it does not create steps.notes.outputs.notes for step-output references.
  • Deprecated command is wrong because ::set-output is obsolete and still relies on workflow-command parsing instead of the recommended environment file.

Question 10

Topic: Author and Maintain Actions

A workflow generates Markdown release notes and needs them as an environment variable in a later step of the same job. The notes can contain blank lines.

jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - name: Generate notes
        run: |
          NOTES=$(printf 'Fix login bug\n\n- add tests\n- update docs\n')
          echo "RELEASE_NOTES=&#36;NOTES" >> "&#36;GITHUB_ENV"

      - name: Use notes
        run: printf '%s\n' "&#36;RELEASE_NOTES"

Which edit best fixes this so the full multiline value is available in Use notes?

Options:

  • A. Move RELEASE_NOTES to the job-level env: block.

  • B. Write the notes to GITHUB_STEP_SUMMARY and read them later.

  • C. Write RELEASE_NOTES to GITHUB_ENV with the <<DELIMITER multiline format.

  • D. Use export RELEASE_NOTES="&#36;NOTES" in the first step instead.

Best answer: C

Explanation: GITHUB_ENV can pass values to later steps in the same job, but raw NAME=value syntax only works for single-line content. For multiline data, you must use the NAME<<DELIMITER format so GitHub Actions treats the entire block as one value.

When a step writes to GITHUB_ENV, GitHub Actions parses that file as environment-file commands. A line like RELEASE_NOTES=&#36;NOTES is valid only if the value stays on one line. If the value contains embedded newlines, those extra lines are parsed separately, which breaks the environment file or loses part of the content.

{
  echo 'RELEASE_NOTES<<EOF'
  printf '%s\n' "&#36;NOTES"
  echo 'EOF'
} >> "&#36;GITHUB_ENV"

This delimiter form tells Actions to read everything until the closing token as one multiline value. Choose a delimiter that will not appear by itself in the content. The variable then becomes available to later steps in the same job, which is exactly what this workflow needs.

  • Job-level env: does not solve runtime generation, because the value is being created inside the step’s shell.
  • Shell export only affects the current step process and does not persist to later steps.
  • GITHUB_STEP_SUMMARY is for run summary output, not for creating environment variables for subsequent steps.

Continue with full practice

Use the GitHub Actions GH-200 Practice Test page for the full IT Mastery route, mixed-topic practice, timed mock exams, explanations, and web/mobile app access.

Try GitHub Actions GH-200 on Web View GitHub Actions GH-200 Practice Test

Free review resource

Read the GitHub Actions GH-200 Cheat Sheet on Tech Exam Lexicon, then return to IT Mastery for timed practice.

Revised on Thursday, May 14, 2026