PRACTICE TRACK / 23 QUESTIONS

GitHub Actions
Think it through.

Workflows, actions, runners, and CI/CD automation.

Choose a question, explain your approach, then reveal the supplied answer. Difficulty labels come from the existing question library.

23 questions

Answers stay closed until you choose to reveal them.

QUESTION 01GitHub ActionsMedium

Explain the key differences between composite actions, Docker container actions, and JavaScript actions in GitHub Actions. When would you choose each type, and how does action versioning (@v1, @v1.2, @sha) affect consumers?

#
Reveal answer guidance

JavaScript actions run directly on the runner (~2-5s startup), ideal for CI/CD logic and API calls. Docker container actions run in a container (~10-30s startup for image pull), providing environment isolation for tools like Terraform or gcc. Composite actions combine multiple steps in YAML with no packaging, ideal for reusing step patterns. Versioning: @v1 points to latest v1 (mutates), @v1.2 is a specific minor (safer), @<sha> pins to exact commit (most secure). OpenSSF Scorecard recommends SHA pinning with automated updates via renovate/dependabot. Major version branches should move with backwards-compatible releases.

QUESTION 02GitHub ActionsHard

How does OIDC (OpenID Connect) work in GitHub Actions for cloud authentication? Walk through setting up OIDC with AWS, including the trust policy configuration, role assumption, and how tokens are exchanged without storing static credentials as secrets.

#
Reveal answer guidance

OIDC eliminates static secrets. Workflow requires id-token: write permission. AWS IAM: create an OIDC Identity Provider for token.actions.githubusercontent.com with audience sts.amazonaws.com. Create an IAM role with trust policy conditioned on "token.actions.githubusercontent.com:sub": "repo:org/repo:ref:refs/heads/main". The action aws-actions/configure-aws-credentials@v4 with role-to-assume exchanges the GitHub JWT for AWS STS temporary credentials (AccessKeyId, SecretAccessKey, SessionToken, 1-hour expiry). Claims include sub (repo), aud, environment — enabling fine-grained access control. No static secrets stored. Same pattern for GCP (workload identity federation) and Azure (azure/login with OIDC).

QUESTION 03GitHub ActionsMedium

Differentiate between actions/upload-artifact and actions/cache. What are appropriate use cases for each? How does cache key strategy affect hit/miss rates, and what is the artifact retention policy?

#
Reveal answer guidance

Artifacts persist files across runs (90 days default, configurable). Immutable — for build outputs, logs, reports. Cache stores dependencies for speed (LRU-evicted after 7 days no access). Cache key determines hit: key: ${{ runner.os }}-npm-${{ hashFiles("**/package-lock.json") }} with restore-keys fallback. Exact key match = full hit, restore-keys = partial hit (stale but usable). Use artifacts for: screenshots, test reports, compiled binaries. Use cache for: node_modules, pip, Maven .m2, Docker layers. Artifacts downloadable via UI; caches not directly visible.

QUESTION 04GitHub ActionsHard

Explain the different action metadata files (action.yml) and how they define inputs, outputs, runs configuration, and branding. How do you ensure backward compatibility when updating a published action?

#
Reveal answer guidance

action.yml defines: name, description, inputs (with required/default), outputs, runs (using: "node20" / "docker" / "composite"), and branding (icon, color for Marketplace). Composite: runs: { using: "composite", steps: [{ run: "echo hello", shell: bash }] }. Backward compatibility: 1) Never remove or rename inputs — use deprecationMessage. 2) New inputs need defaults. 3) Outputs can be added freely. 4) Semantic versioning for breaking changes. 5) Maintain major version branches (v1, v2). 6) Migration guides in README. 7) Test with dependabot auto-merge. 8) Use @actions/core getInput() with fallbacks.

QUESTION 05GitHub ActionsMedium

How do matrix builds work in GitHub Actions? Explain the strategy: matrix with include/exclude, dynamic matrix generation from JSON, max-parallel, fail-fast, and how you determine which job failed in a matrix.

#
Reveal answer guidance

Matrix builds generate job combinations: strategy: matrix: os: [ubuntu, windows] node: [16, 18, 20]. exclude removes specific combos. include adds extra variables. Dynamic: strategy: matrix: ${{ fromJSON(needs.setup.outputs.matrix) }}. max-parallel limits concurrency. fail-fast: true cancels all jobs on any failure. Check failure via needs.*.result or strategy context (strategy.job-index). continue-on-error: ${{ matrix.experimental }} lets experimental combos fail without blocking. Each combination is a separate job with independent status. GitHub UI groups them visually under the matrix name.

QUESTION 06GitHub ActionsHard

Explain GitHub Environments and deployment protection rules. How do required reviewers, deployment branches, and environment URLs work together to implement a gated deployment pipeline?

#
Reveal answer guidance

Environments provide protection rules. Configured via Settings or YAML: environment: name: production url: https://app.example.com. Protection rules: 1) Required reviewers — specific users/teams must approve via GitHub UI before the job proceeds. 2) Wait timer — delays deployment (e.g., 5 minutes). 3) Deployment branches — restricts which branches can deploy (e.g., main, release/*). The environment URL appears as a link from the deployment in the GitHub UI. The job creates a pending deployment; approval pauses it until reviewers approve. Environment secrets are only accessible to jobs targeting that environment. Multiple environments (dev -> staging -> production) create a gated pipeline with increasing protection.

QUESTION 07GitHub ActionsMedium

What are GitHub Actions workflow commands? Explain "echo ::set-output::" (deprecated), the modern echo "name=value" >> $GITHUB_OUTPUT syntax, grouping, warning/error commands, and secret masking. How do these compare to script-based output?

#
Reveal answer guidance

Workflow commands communicate with the runner via stdout. Deprecated ::set-output name=value:: is replaced by echo "name=value" >> $GITHUB_OUTPUT (prevents injection). Grouping: echo "::group::name" / echo "::endgroup::". Annotations: echo "::warning file=app.js,line=1::message", echo "::error::message", echo "::notice::message". Secret masking: echo "::add-mask::value". Multi-line: { echo "key<<EOF"; echo "value"; echo "EOF"; } >> $GITHUB_OUTPUT. Environment: echo "VAR=val" >> $GITHUB_ENV. State: echo "key=val" >> $GITHUB_STATE. These are canonical over script-based approaches.

QUESTION 08GitHub ActionsHard

How do service containers work in GitHub Actions? Explain the container: syntax, health checks, port mapping, network connectivity between the service and job container, and how you would use Docker Compose for multi-container setups.

#
Reveal answer guidance

Service containers run alongside the job: services: postgres: image: postgres:15 env: POSTGRES_PASSWORD: postgres ports: - 5432:5432 options: --health-cmd "pg_isready" --health-interval 10s --health-retries 5. Job on host connects via localhost:5432. Job in container: container: node:20, connect via postgres:5432 (service name as hostname). Health checks ensure readiness before steps run. For Docker Compose: use a step with docker compose up -d. Lifecycle: services start before steps, stop and cleaned up after job. Resources: limit via --memory. Multi-container: define multiple services with inter-service dependencies using health checks for startup ordering.

QUESTION 09GitHub ActionsMedium

Explain the differences between reusable workflows and composite actions. What are the limitations for reusable workflows (max inputs, secrets, nested calls)? When would you choose a reusable workflow over a composite action?

#
Reveal answer guidance

Reusable workflows call another .github/workflows/*.yml file. Composite actions are step collections in action.yml. Reusable limitations: max 10 inputs, max 10 secrets, max 4 nesting levels. Caller uses secrets: inherit. Composite actions have no nesting limits but cannot contain jobs — only steps. Choose reusable when: multiple jobs needed in the called unit, independent triggering (workflow_call), different runners per job, matrix strategies in called workflow. Choose composite when: reusing a step sequence, simpler context passing, no separate jobs needed.

QUESTION 10GitHub ActionsHard

How do you set up and secure self-hosted GitHub Actions runners? Explain runner groups, registration, labels, ephemeral runners, scaling, and security considerations like network isolation and token rotation.

#
Reveal answer guidance

Register: ./config.sh --url https://github.com/org/repo --token ABC. Runner groups control repo access. Labels categorize: --labels gpu,linux. Ephemeral: --ephemeral (one job, auto-unregisters). Scaling: actions/actions-runner-controller (ARC) on Kubernetes auto-scales based on job queue. Security: 1) Never use self-hosted for public fork PRs (use pull_request_target safely). 2) Network isolation via VPC/security groups. 3) Rotate tokens via API. 4) Ephemeral runners prevent state persistence. 5) Audit via audit log. 6) Labels direct sensitive workloads. 7) Regular updates with ./svc.sh stop && ./svc.sh start. 8) GITHUB_TOKEN has limited scope for fork PRs.

QUESTION 11GitHub ActionsMedium

How do job status functions (success(), failure(), always(), cancelled()) work in if conditions? How does continue-on-error differ from if: ${{ always() }}? Provide examples of post-job cleanup that runs regardless of failure.

#
Reveal answer guidance

success() = all previous jobs/steps succeeded. failure() = any failed. always() = runs regardless. cancelled() = workflow cancelled. continue-on-error: true prevents a step from failing the job — subsequent steps still run. if: ${{ always() }} runs even after failure but you must handle potential upstream failures. Cleanup: - name: Cleanup if: ${{ always() }} run: docker compose down. Notification: if: ${{ failure() }} run: curl -X POST ${{ secrets.SLACK_WEBHOOK }}. Cancellation: if: ${{ cancelled() }}. Combined: if: ${{ success() || failure() }} runs on success or failure but not cancellation.

QUESTION 12GitHub ActionsHard

Explain the needs and job dependency graph in GitHub Actions. How do you share outputs between jobs, handle matrix dependencies, and manage sequential vs parallel execution with dynamic dependencies based on conditions?

#
Reveal answer guidance

needs establishes job dependencies: test: needs: build. Output sharing: build step sets id: version with echo "version=1.0" >> $GITHUB_OUTPUT, then job declares outputs: version: ${{ steps.version.outputs.version }}. Dependent job accesses: ${{ needs.build.outputs.version }}. Matrix dependencies: dependent job runs once after all matrix combinations complete. Per-matrix dependency: create a second matrix. Dynamic dependencies: needs: ${{ fromJSON(needs.check.outputs.jobs) }}. Branching: build -> (deploy-staging, test-e2e) — runs in parallel after build. Sequential: deploy-prod: needs: deploy-staging. Conditional: if: ${{ needs.build.result == "success" }}.

QUESTION 13GitHub ActionsMedium

List and explain the most important GitHub Actions contexts: github, env, vars, secrets, runner, and strategy. How do you access context values in if conditions and steps?

#
Reveal answer guidance

github: github.sha, github.ref, github.actor, github.event, github.repository, github.run_id, github.workflow. env: environment variables set at workflow/job/step level. vars: configuration variables from Settings (typed, separate from secrets). secrets: encrypted secrets (redacted in logs). runner: runner.os, runner.temp, runner.tool_cache, runner.workspace, runner.arch. strategy: strategy.job-index, strategy.job-total, strategy.fail-fast. Access: ${{ github.sha }}, ${{ env.MY_VAR }}, ${{ vars.DEPLOY_HOST }}, ${{ secrets.API_TOKEN }}, ${{ runner.os }}, ${{ strategy.job-index }}. In if: if: ${{ github.ref == "refs/heads/main" }}.

QUESTION 14GitHub ActionsHard

How do workflow_dispatch and repository_dispatch events work? Explain input types, event payloads, client_payload for repository_dispatch, and how to trigger cross-repo workflows. Show an example of a workflow_dispatch with choice and boolean inputs.

#
Reveal answer guidance

workflow_dispatch: on: workflow_dispatch: inputs: environment: description: "Deploy target" required: true type: choice options: [dev, staging, production] debug: type: boolean default: false. Trigger via UI or API: gh workflow run workflow.yml -f environment=production. repository_dispatch: on: repository_dispatch: types: [deploy]. Trigger: curl -X POST -H "Authorization: token $GH_TOKEN" https://api.github.com/repos/owner/repo/dispatches -d '{"event_type": "deploy", "client_payload": {"env": "production"}}'. Access: ${{ github.event.client_payload.env }}. Cross-repo: use a PAT to call API on another repo, or use peter-evans/repository-dispatch action with token and client_payload.

QUESTION 15GitHub ActionsMedium

How do you implement dependency caching strategies in GitHub Actions for different ecosystems? Explain npm, pip, and Maven caching, including cache key strategies and how cache action eviction works.

#
Reveal answer guidance

npm: actions/cache with path: ~/.npm, key: ${{ runner.os }}-npm-${{ hashFiles("**/package-lock.json") }}, restore-keys: ${{ runner.os }}-npm-. Pip: path: ~/.cache/pip, key: ${{ runner.os }}-pip-${{ hashFiles("**/requirements.txt") }}. Maven: path: ~/.m2, key: ${{ runner.os }}-m2-${{ hashFiles("**/pom.xml") }}. For Docker: use Docker layer caching with docker/build-push-action@v5 with cache-from/cache-to. Cache eviction: LRU after 7 days no access. Cache size limit: ~10GB per repo. Use restore-keys for partial matches when exact key misses. For monorepos, include the subdirectory path in the key to avoid collisions.

QUESTION 16GitHub ActionsHard

What are the security best practices for GitHub Actions? Explain minimum GITHUB_TOKEN permissions, third-party action pinning, OpenSSF Scorecard, and how to automate action updates with renovate or dependabot.

#
Reveal answer guidance

Minimum GITHUB_TOKEN permissions: set in workflow top-level: permissions: contents: read issues: write. Never use write-all. Pin third-party actions to full commit SHA: uses: actions/checkout@<full-sha> instead of @v4. Use OpenSSF Scorecard to audit supply chain security. Automate updates with renovate (configure regex managers for actions) or dependabot (built-in, configured in dependabot.yml with open-pull-requests-limit). Verify SHA signatures for actions from verified creators. For forks: use pull_request_target with explicit checkout of PR code to avoid token leakage. Never store secrets in workflow files. Audit workflow run logs for accidental secret exposure. Use OIDC instead of static credentials.

QUESTION 17GitHub ActionsMedium

How do the concurrency and cancel-in-progress settings work in GitHub Actions? Provide examples of canceling redundant workflow runs and managing environment-specific concurrency groups.

#
Reveal answer guidance

Concurrency groups prevent multiple workflow runs from running simultaneously: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true. This cancels the previous run on the same branch when a new push occurs. For environments: concurrency: group: production-deploy cancel-in-progress: true prevents overlapping deployments. For staged deployments: concurrency: group: ${{ github.workflow }}-${{ github.ref }}-${{ inputs.environment }}. Without cancel-in-progress, new runs wait for the previous run to complete. Use for: CI on PRs (only latest matters), production deployments (prevent concurrent deploys). The concurrency group name is arbitrary; runs with the same group name are serialized.

QUESTION 18GitHub ActionsHard

How would you implement a multi-stage CI/CD pipeline in GitHub Actions with build, test, staging deploy, E2E tests, and production deploy, using environments, artifact sharing, and conditional approvals?

#
Reveal answer guidance

Structure: jobs: build: steps: - run: npm build && npm test - uses: actions/upload-artifact@v4 with: name: build. deploy-staging: needs: build environment: name: staging url: ${{ vars.STAGING_URL }} steps: - uses: actions/download-artifact@v4 with: name: build - run: deploy-to-staging.sh. e2e-tests: needs: deploy-staging runs-on: ubuntu-latest steps: - run: npx playwright test. deploy-prod: needs: e2e-tests environment: name: production url: https://example.com steps: - run: deploy-to-production.sh. Environments with required reviewers pause deploy-prod until approval. Artifacts pass build outputs between jobs. Use vars for environment-specific URLs and hosts. OIDC for cloud auth without static secrets.

QUESTION 19GitHub ActionsMedium

How do you use GitHub Actions with GitHub Pages deployment? Explain the actions/deploy-pages action, artifact preparation with actions/upload-pages-artifact, and how branch-based deployment works with the actions-helped deployment workflow.

#
Reveal answer guidance

For GitHub Pages, use actions/deploy-pages. Steps: 1) Build static site. 2) actions/upload-pages-artifact@v3 with path: ./dist. 3) actions/deploy-pages@v4. Requires permissions: pages: write, id-token: write. Configure Pages in repo Settings to use GitHub Actions as the source (not the legacy branch-based). The environment is automatically set to github-pages with URL. For custom domains, add a CNAME file in the static output or configure in Settings. Branch-based deployment (legacy): push to gh-pages branch. The modern approach uses the Actions workflow with explicit artifact upload and deployment steps.

QUESTION 20GitHub ActionsHard

Explain GitHub Actions' approach to handling pull requests from forks. How does pull_request_target differ from pull_request? What are the security implications of GITHUB_TOKEN in fork contexts, and how do you safely run CI on PRs from external contributors?

#
Reveal answer guidance

pull_request (default) runs with a limited GITHUB_TOKEN that cannot access secrets — safe for forks but can't write back. pull_request_target runs in the context of the base branch (has full token access including secrets), enabling label assignment, comment creation, and artifact upload for fork PRs. Security risk: an attacker can modify a fork PR's workflow to exfiltrate secrets. Safely use pull_request_target: checkout the PR code explicitly (not the base branch), validate inputs, and never run untrusted build steps with secrets. Use paths-filter to run only on safe changes. For artifact upload from forks, use pull_request_target with explicit checkout and separate upload steps. GitHub automatically masks secrets in PR comments. Use the github.event.pull_request.head.sha for checkout.

QUESTION 21GitHub ActionsMedium

Your GitHub Actions workflow takes 25 minutes because it builds a Docker image from scratch on every push. Design a caching strategy to reduce build time under 5 minutes, including exact cache keys and action configurations.

#
Reveal answer guidance

Use Docker layer caching with docker/build-push-action@v5 and GitHub Actions cache backend: cache-from: type=gha; cache-to: type=gha,mode=max. The mode=max caches all layers (not just exported). Cache key strategy: the action automatically uses the image's manifest digest as the primary cache key with a fallback to the image name. For more granularity: cache-from: type=gha,scope=${{ github.ref }}; cache-to: type=gha,scope=${{ github.ref }},mode=max scopes cache per branch. For monorepos: use cache-from pointing to the main branch cache as restore key so feature branches can reuse the latest main cache. Additional speed-ups: (1) Order Dockerfile layers wisely — install OS packages (apt-get) before copying source (least-changing first). (2) Use --target build-stage to cache build stages separately. (3) Use a Docker layer cache action like actions/cache for /var/lib/docker manually, but type=gha is now the recommended approach. (4) For npm/pip installs, cache those separately with actions/cache keyed on lockfile hash. Combined, these changes typically bring builds from 25 min to 2-3 min for most web app Dockerfiles.

QUESTION 22GitHub ActionsHard

A developer pushes a commit that hardcodes an API key. Design a GitHub Actions workflow that automatically detects and blocks secrets before they reach the main branch, including the tools, actions, and failure behavior.

#
Reveal answer guidance

Use trufflehog or gitLeaks in a PR workflow: on: pull_request. Steps: (1) Checkout with fetch-depth: 0 to scan full history. (2) Run trufflehog filesystem --fail --json . > trufflehog-report.json. (3) If secrets found, post a PR comment summarizing the findings and fail the workflow: if: failure() && github.event_name == 'pull_request' — use actions/github-script@v7 to comment on the PR: github.rest.issues.createComment({issue_number: context.issue.number, body: '⚠️ Secret detected in PR. Please remove before merging.'}). (4) For true secrets in history, use trufflehog git --since-commit HEAD~1 --fail to scan the diff only. (5) For pre-commit prevention, use a pre-commit hook with detect-secrets or gitleaks — but hooks can be bypassed. Server-side enforcement is essential. (6) Use GitHub's built-in secret scanning (push protection) — enable in repo Settings > Code security > Secret scanning > Push protection. This blocks pushes containing known patterns (AWS keys, GitHub tokens, etc.) at the API level. (7) For custom patterns, use trufflehog with custom regex rules. (8) Alerting: if a secret reaches main despite protection, use a pull_request_target workflow with repository write access to invalidate the secret or notify security. Failure behavior: block the merge with required status check — set branch protection rule requiring the secret-scan check to pass.

QUESTION 23GitHub ActionsMedium

Your monorepo has 10 microservices in separate directories. Each service should only build/deploy when its directory changes. How do you implement this with path filtering and dynamic matrix generation?

#
Reveal answer guidance

Use dorny/paths-filter@v3 to detect which directories changed: changes: outputs: service-a: ${{ steps.filter.outputs.service-a }}; service-b: ${{ steps.filter.outputs.service-b }}. Then generate a dynamic matrix: strategy: matrix: service: ${{ fromJSON(steps.filter.outputs.changed) }}. The paths-filter action outputs a changes JSON with the changed services. The matrix then runs only for those services. Alternative: use tj-actions/changed-files@v44 to get the list of modified files, then extract unique service directories with a shell step: services=$(echo "${{ steps.changed-files.outputs.all_changed_files }}" | grep -oP "^services/\K[^/]+" | sort -u | jq -R -s -c 'split("\n")[:-1]'). Then strategy: matrix: service: ${{ fromJSON(services) }}. For CI/CD: each service matrix entry runs build-and-deploy-${{ matrix.service }} with if: ${{ contains(needs.setup.outputs.services, matrix.service) }}. This ensures zero wasted build time — unchanged services skip entirely. For dependent services (e.g., service-b depends on service-a), add explicit needs or use a dependency graph in the workflow.

CONTINUE PRACTICING

Try another perspective.