GitHub Actions Interview Questions
Beginner Level
Q1. What is GitHub Actions and how does it differ from other CI/CD tools like Jenkins?
A: GitHub Actions is a CI/CD and automation platform built natively into GitHub. It uses YAML-based workflow files stored in .github/workflows/ and is triggered by GitHub events.
| GitHub Actions | Jenkins | |
|---|---|---|
| Setup | Zero — built into GitHub | Install + maintain server |
| Config | YAML in repo | Groovy / Jenkinsfile |
| Infrastructure | GitHub-hosted runners | You manage servers |
| Marketplace | 20,000+ actions | Plugin ecosystem |
| Cost | Free tier included | Server costs |
| Scaling | Automatic | Manual configuration |
| Integration | Native GitHub | Webhooks/plugins |
Key advantage of GitHub Actions: no infrastructure to manage, workflows live alongside code, and every PR shows its own workflow run inline.
Q2. What are the core components of a GitHub Actions workflow?
A:
# Every workflow has these components:name: My Workflow # 1. Workflow nameon: # 2. Event (trigger) push: branches: [main]env: # 3. Environment variables APP_NAME: myappjobs: # 4. Jobs build: # Job ID name: Build App # Job display name runs-on: ubuntu-latest # 5. Runner steps: # 6. Steps - name: Checkout # Step name uses: actions/checkout@v4 # 7. Action with: # 8. Inputs to action fetch-depth: 0 - name: Build # Another step run: npm run build # 9. Shell command
In order: Event → Workflow → Jobs → Steps → Actions/Commands
Q3. What is the difference between uses and run in a step?
A:
steps:# uses — calls a pre-built reusable Action# from GitHub Marketplace or local path- name: Checkout code uses: actions/checkout@v4 # ← pre-built action with: fetch-depth: 0 # inputs to the action# run — executes shell commands directly# on the runner machine- name: Build app run: | # ← shell command npm install npm run build# Key differences:# uses → Action defined in another repo or local .github/actions/# run → Direct bash/powershell/python commands on the runner
Q4. What are GitHub Actions runners? What is the difference between GitHub-hosted and self-hosted?
A: A runner is the machine that executes your workflow jobs.
GitHub-Hosted Runners:├── Managed by GitHub├── Fresh VM for every job├── ubuntu-latest, windows-latest, macos-latest├── Pre-installed with common tools├── Free tier included (2000 min/month private)└── Limitation: 2 CPU, 7GB RAM (standard)Self-Hosted Runners:├── Your own machines (VM, bare metal, K8s pod)├── You install the runner agent├── Persistent — same machine across jobs├── Access to private network/resources├── Any hardware spec you need└── You manage updates, security, scaling
# GitHub-hostedruns-on: ubuntu-latest# Self-hostedruns-on: self-hostedruns-on: [self-hosted, linux, x64, gpu]runs-on: [self-hosted, linux, high-memory]
When to use self-hosted:
- Need access to private network resources
- Need specific hardware (GPU, high memory)
- Compliance requires code not leaving your infra
- Need faster runners than GitHub provides
Q5. How do you pass data between steps in a workflow?
A: Three ways to share data between steps:
steps:# Method 1 — Environment variables (same job)- name: Set variable run: echo "VERSION=1.2.3" >> $GITHUB_ENV- name: Use variable run: echo "Version is $VERSION"# Method 2 — Step outputs (reference in later steps)- name: Get SHA id: get-sha # must set an id run: echo "sha=${GITHUB_SHA::8}" >> $GITHUB_OUTPUT- name: Use SHA run: echo "SHA is ${{ steps.get-sha.outputs.sha }}"# Method 3 — Artifacts (between jobs)# Job 1 — upload- name: Upload binary uses: actions/upload-artifact@v4 with: name: app-binary path: bin/app# Job 2 — download (different job, must declare needs:)- name: Download binary uses: actions/download-artifact@v4 with: name: app-binary path: bin/
Q6. What are GitHub Actions secrets and how do you use them?
A: Secrets are encrypted variables stored in GitHub settings — never visible in logs.
# Using secrets in workflowssteps:- name: Deploy env: # Reference secret via expression API_KEY: ${{ secrets.API_KEY }} DB_PASS: ${{ secrets.DB_PASSWORD }} run: ./deploy.sh# Built-in secret — auto-created by GitHub- name: Push image run: | echo "${{ secrets.GITHUB_TOKEN }}" | \ docker login ghcr.io -u ${{ github.actor }} --password-stdin
Secret scopes:
Organization → all repos in orgRepository → specific repo onlyEnvironment → specific deployment environment
Key rules:
- Secrets are masked in logs — if printed, shown as
*** - Cannot be read after creation — only overwritten
- Available as
${{ secrets.NAME }} - Never hardcode secrets in workflow files
Q7. What is GITHUB_TOKEN and what can it do?
A: GITHUB_TOKEN is an automatically created secret for every workflow run — no setup needed. GitHub creates it at the start of each run and revokes it when the job finishes.
# Auto-available in every workflow- name: Create release uses: softprops/action-gh-release@v1 with: files: dist/* env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Default permissions:
permissions: actions: read checks: write contents: read # read repo content deployments: write issues: write # create/comment on issues packages: write # push to GitHub Packages pull-requests: write # comment on PRs statuses: write# Best practice — restrict to minimum neededpermissions: contents: read packages: write # only what you need
Q8. How do you cache dependencies in GitHub Actions?
A:
# Method 1 — Built-in cache in setup actions- uses: actions/setup-nodev4 with: node-version: '20' cache: 'npm' # ← handles cache automatically- uses: actions/setup-pythonv5 with: python-version: '3.12' cache: 'pip'# Method 2 — Manual cache control- name: Cache node modules uses: actions/cachev4 id: cache with: path: ~/.npm # Cache key — invalidated when lockfile changes key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }} restore-keys: | ${{ runner.os }}-npm-- name: Install (only if cache miss) if: steps.cache.outputs.cache-hit != 'true' run: npm ci
Cache key strategy:
Best key: os + tool + lockfile hash ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}Restore key: os + tool (fallback to any npm cache) ${{ runner.os }}-npm-Cache hit: exact key match → restore and skip installCache miss: restore-key match → restore closest + re-install
Intermediate Level
Q9. How do you run jobs in parallel vs sequentially? When would you use each?
A:
jobs: # ── Parallel (default) — run simultaneously ─────────────── lint: runs-on: ubuntu-latest steps: - run: npm run lint test: runs-on: ubuntu-latest steps: - run: npm test security: runs-on: ubuntu-latest steps: - run: npm audit # ── Sequential — waits for dependencies ─────────────────── build: needs: [lint, test, security] # wait for ALL three runs-on: ubuntu-latest steps: - run: npm run build deploy-staging: needs: build # wait for build runs-on: ubuntu-latest steps: - run: ./deploy.sh staging deploy-prod: needs: deploy-staging # wait for staging runs-on: ubuntu-latest steps: - run: ./deploy.sh production
Execution flow:lint ──┐test ──┼──▶ build ──▶ deploy-staging ──▶ deploy-prodsecurity ──┘Parallel: lint + test + security run together (fast)Sequential: build waits, deploy is ordered (safe)
Use parallel when:
- Jobs are independent (lint, test, security)
- Want to fail fast — catch any issue quickly
- Speed is priority
Use sequential when:
- Later job depends on earlier result
- Deployment order matters
- Gate deployment behind successful tests
Q10. Explain matrix strategy — when and how would you use it?
A: Matrix runs the same job with different configurations simultaneously — multiply coverage without duplicating YAML.
jobs: test: strategy: matrix: # Every combination is a separate job os: [ubuntu-latest, windows-latest, macos-latest] node: [18, 20, 21] # Creates 9 parallel jobs (3 OS × 3 Node versions) fail-fast: false # don't cancel others if one fails max-parallel: 4 # run max 4 at once runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node }} - run: npm test # ── Exclude specific combinations ───────────────────────── test-advanced: strategy: matrix: os: [ubuntu-latest, windows-latest] python: ['3.10', '3.11', '3.12'] exclude: - os: windows-latest python: '3.10' # skip this combo include: - os: ubuntu-latest # add extra combo python: '3.12' experimental: true # ── Dynamic matrix from previous job ────────────────────── discover: runs-on: ubuntu-latest outputs: services: ${{ steps.find.outputs.services }} steps: - id: find run: | services=$(ls services/ | jq -R -s -c 'split("\n")[:-1]') echo "services=$services" >> $GITHUB_OUTPUT test-services: needs: discover strategy: matrix: service: ${{ fromJson(needs.discover.outputs.services) }} runs-on: ubuntu-latest steps: - run: ./test.sh ${{ matrix.service }}
Common use cases:
- Test across multiple OS (cross-platform)
- Test across multiple language versions
- Deploy to multiple regions simultaneously
- Build for multiple architectures
Q11. How do you implement environment-based deployments with approval gates?
A:
# Workflowjobs: deploy-prod: runs-on: ubuntu-latest environment: name: production # references GitHub environment url: https://myapp.com steps: - run: ./deploy.sh prod
Configuration in GitHub:Settings → Environments → productionProtection rules:✅ Required reviewers: [alice, bob] # must approve✅ Wait timer: 10 minutes # cooling period✅ Deployment branches: main only # restrict who can deployFlow:1. Workflow reaches deploy-prod job2. GitHub sends approval request to reviewers3. Pipeline pauses — shows "Waiting for review"4. Reviewer approves in GitHub UI5. Job proceeds with deployment
# Multiple environments with progressionjobs: deploy-dev: environment: dev # no approval needed runs-on: ubuntu-latest steps: - run: ./deploy.sh dev deploy-staging: needs: deploy-dev environment: staging # 1 approval required runs-on: ubuntu-latest steps: - run: ./deploy.sh staging deploy-prod: needs: deploy-staging environment: production # 2 approvals required runs-on: ubuntu-latest steps: - run: ./deploy.sh prod # Each environment has different secrets # production has: PROD_DB_URL, PROD_API_KEY # staging has: STAGING_DB_URL, STAGING_API_KEY
Q12. How do you implement OIDC authentication to cloud providers — why is it better than storing credentials as secrets?
A:
Traditional (bad): Store cloud credentials in GitHub Secrets → Long-lived keys that could be leaked → Keys need manual rotation → Attack window: keys valid indefinitelyOIDC (good): GitHub gets a short-lived token from cloud provider → No credentials stored in GitHub → Token valid for one job only (minutes) → Attack window: zero (token expires immediately)Flow:GitHub → requests OIDC token from GitHub's OIDC provider → presents token to cloud provider (AWS/Azure/GCP) → cloud validates token matches trusted repo/branch → cloud issues short-lived access credential → job uses credential → expires when job ends
# GCP OIDC setupjobs: deploy: runs-on: ubuntu-latest permissions: id-token: write # required for OIDC contents: read steps: - uses: actions/checkout@v4 - name: Authenticate to GCP uses: google-github-actions/auth@v2 with: workload_identity_provider: >- projects/123456/locations/global/ workloadIdentityPools/github-pool/ providers/github-provider service_account: deploy-sa@myproject.iam.gserviceaccount.com # No credentials stored — GitHub proves identity via OIDC - name: Deploy run: gcloud run deploy myapp --image gcr.io/myproject/myapp# AWS OIDC setup - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: arn:aws:iam::123456789:role/github-actions-role aws-region: us-east-1 # No AWS_ACCESS_KEY_ID or AWS_SECRET_ACCESS_KEY needed# Azure OIDC setup - name: Azure login uses: azure/login@v1 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} # Uses federated credentials — no client secret
# GCP setup (one time)gcloud iam workload-identity-pools create github-pool \ --location globalgcloud iam workload-identity-pools providers create-oidc github-provider \ --workload-identity-pool github-pool \ --location global \ --issuer-uri https://token.actions.githubusercontent.com \ --attribute-mapping "google.subject=assertion.sub,attribute.repository=assertion.repository" \ --attribute-condition "assertion.repository=='myorg/myrepo'"gcloud iam service-accounts add-iam-policy-binding deploy-sa@myproject.iam.gserviceaccount.com \ --role roles/iam.workloadIdentityUser \ --member "principalSet://iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/github-pool/attribute.repository/myorg/myrepo"
Q13. How do you create and use reusable workflows?
A: Reusable workflows let you define once, call from many workflows — like functions for your CI/CD.
# .github/workflows/reusable-test.yml# This is the reusable workflowname: Reusable Teston: workflow_call: # makes it reusable inputs: python-version: type: string required: false default: '3.12' environment: type: string required: true secrets: DATABASE_URL: # secrets must be declared required: true outputs: test-result: description: Test outcome value: ${{ jobs.test.outputs.result }}jobs: test: runs-on: ubuntu-latest outputs: result: ${{ steps.run.outputs.result }} steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: ${{ inputs.python-version }} - id: run env: DATABASE_URL: ${{ secrets.DATABASE_URL }} ENV: ${{ inputs.environment }} run: | pytest tests/ echo "result=passed" >> $GITHUB_OUTPUT---# .github/workflows/ci.yml# This calls the reusable workflowjobs: test-staging: uses: ./.github/workflows/reusable-test.yml with: python-version: '3.12' environment: staging secrets: DATABASE_URL: ${{ secrets.STAGING_DB_URL }} test-prod-config: uses: ./.github/workflows/reusable-test.yml with: python-version: '3.11' environment: production secrets: DATABASE_URL: ${{ secrets.PROD_DB_URL }} # Use output from reusable workflow notify: needs: test-staging runs-on: ubuntu-latest steps: - run: echo "Test result: ${{ needs.test-staging.outputs.test-result }}"
Q14. How do you handle workflow concurrency and prevent duplicate runs?
A:
# Concurrency group — cancel stale runsconcurrency: # Group by workflow + branch group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true # cancel old run when new starts# Practical examples:# Cancel old PR runs when new commit pushedconcurrency: group: pr-${{ github.event.pull_request.number }} cancel-in-progress: true# Never cancel production deploymentsconcurrency: group: deploy-production cancel-in-progress: false # queue instead of cancel# Per-environment concurrencyconcurrency: group: deploy-${{ github.event.inputs.environment }} cancel-in-progress: false# Different behavior per branchconcurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} # Cancel on feature branches, queue on main
Scenario without concurrency: Push commit 1 → run starts Push commit 2 → second run starts Push commit 3 → third run starts All three running simultaneously — wastes resourcesScenario with cancel-in-progress: true Push commit 1 → run starts Push commit 2 → run 1 CANCELLED, run 2 starts Push commit 3 → run 2 CANCELLED, run 3 starts Only latest commit runs — saves time and cost
Q15. How do you handle secrets securely — what are the risks and mitigations?
A:
# ── Risks and mitigations ──────────────────────────────────# Risk 1: Secret printed in logs# BAD- run: echo "API key is ${{ secrets.API_KEY }}" # NEVER DO THIS# GOOD — secrets are masked but don't risk it- run: ./deploy.sh env: API_KEY: ${{ secrets.API_KEY }} # pass as env var# Risk 2: Secret in PR from fork# PRs from forks DON'T have access to secrets by default# Only use pull_request_target carefully# Risk 3: Untrusted actions reading secrets# BAD — pinning to tag (tag can be moved)- uses: some-action/deploy@v1# GOOD — pin to exact commit SHA- uses: some-action/deploy@a1b2c3d4e5f6789012345678901234567890abcd# Risk 4: Overly broad secret scope# BAD — one secret for all environmentssecrets: DB_PASSWORD: ${{ secrets.DB_PASSWORD }}# GOOD — environment-scoped secrets# production environment has its own DB_PASSWORD# staging environment has its own DB_PASSWORDenvironment: production # uses production secrets only# Risk 5: Logging context that contains secrets# BAD- run: echo "${{ toJson(secrets) }}" # NEVER — logs all secrets# Mitigation — minimal permissionspermissions: contents: read # only what you need
Advanced Level
Q16. How would you build a self-hosted runner on Kubernetes for scale?
A:
# Actions Runner Controller (ARC) — manages runners as K8s pods# Install ARChelm install arc \ oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set-controller \ --namespace arc-systems \ --create-namespace# Deploy runner scale sethelm install arc-runner-set \ oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set \ --namespace arc-runners \ --create-namespace \ --set githubConfigUrl=https://github.com/myorg/myrepo \ --set githubConfigSecret.github_token=$GITHUB_PAT
# RunnerScaleSet — auto-scales runnersapiVersion: actions.github.com/v1alpha1kind: AutoscalingRunnerSetmetadata: name: arc-runner-set namespace: arc-runnersspec: githubConfigUrl: https://github.com/myorg/myrepo githubConfigSecret: github-token-secret # Scale between 0 and 20 runners minRunners: 0 maxRunners: 20 template: spec: containers: - name: runner image: ghcr.io/actions/actions-runner:latest resources: requests: cpu: "1" memory: "2Gi" limits: cpu: "2" memory: "4Gi" # Docker-in-Docker for container builds - name: dind image: docker:dind securityContext: privileged: true resources: requests: cpu: "500m" memory: "1Gi"
# Workflow uses the K8s runnerjobs: build: runs-on: arc-runner-set # matches scale set name steps: - uses: actions/checkout@v4 - run: docker build -t myapp .
Benefits:
- Scales to zero when no jobs — cost efficient
- Scales up instantly when jobs queued
- Runs in your VPC — access private resources
- Ephemeral — clean environment every run
- Works with spot/preemptible nodes for cost savings
Q17. How do you implement a deployment pipeline with automated rollback?
A:
jobs: deploy: runs-on: ubuntu-latest environment: production outputs: previous-image: ${{ steps.get-current.outputs.image }} steps: - uses: actions/checkout@v4 - name: Auth to GCP uses: google-github-actions/auth@v2 with: workload_identity_provider: ${{ secrets.WIF_PROVIDER }} service_account: ${{ secrets.GKE_SA }} - name: Get GKE credentials uses: google-github-actions/get-gke-credentials@v2 with: cluster_name: prod-cluster location: us-central1 # Save current image for rollback - name: Get current image id: get-current run: | current=$(kubectl get deployment api \ -n production \ -o jsonpath='{.spec.template.spec.containers[0].image}') echo "image=$current" >> $GITHUB_OUTPUT echo "Current image: $current" # Deploy new version - name: Deploy new version run: | kubectl set image deployment/api \ api=gcr.io/myproject/api:${{ github.sha }} \ -n production kubectl rollout status deployment/api \ -n production \ --timeout=5m # Wait and verify - name: Smoke test id: smoke-test run: | sleep 30 # Test multiple endpoints curl -f https://api.myapp.com/health curl -f https://api.myapp.com/ready echo "Smoke tests passed" # Rollback if smoke test fails - name: Rollback on failure if: failure() && steps.smoke-test.outcome == 'failure' run: | echo "🔴 Smoke tests failed — rolling back" kubectl set image deployment/api \ api=${{ steps.get-current.outputs.previous-image }} \ -n production kubectl rollout status deployment/api \ -n production \ --timeout=3m echo "✅ Rollback complete" # Alert on rollback - name: Notify rollback if: failure() uses: slackapi/slack-github-action@v1.26.0 with: payload: | { "text": "🚨 Production deployment FAILED and rolled back\nCommit: ${{ github.sha }}\nActor: ${{ github.actor }}\nRun: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" } env: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} # Notify success - name: Notify success if: success() uses: slackapi/slack-github-action@v1.26.0 with: payload: | { "text": "✅ Production deployment successful\nVersion: ${{ github.sha }}" } env: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
Q18. How do you optimize GitHub Actions for speed and cost?
A:
# ── 1. Path filtering — skip unnecessary runs ───────────────on: push: paths: - 'src/**' # only run when source changes - 'tests/**' - 'requirements.txt' paths-ignore: - 'docs/**' # never run for docs changes - '**.md'# ── 2. Fail fast — cheap checks first ──────────────────────jobs: lint: # fast (30 seconds) runs-on: ubuntu-latest steps: - run: npm run lint test: # slower (5 minutes) needs: lint # only run if lint passes runs-on: ubuntu-latest steps: - run: npm test build: # slowest (10 minutes) needs: test # only run if tests pass runs-on: ubuntu-latest steps: - run: npm run build# ── 3. Aggressive caching ────────────────────────────────────steps:- uses: actions/setup-nodev4 with: cache: 'npm'- name: Cache build output uses: actions/cachev4 with: path: .next/cache key: nextjs-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('src/**') }}- name: Cache Docker layers uses: actions/cachev4 with: path: /tmp/.buildx-cache key: buildx-${{ github.sha }} restore-keys: buildx-# ── 4. Concurrency — cancel stale runs ──────────────────────concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true# ── 5. Conditional jobs — skip when not needed ──────────────jobs: deploy: # Only deploy on main, not on every PR if: github.ref == 'refs/heads/main' && github.event_name == 'push' runs-on: ubuntu-latest steps: - run: ./deploy.sh# ── 6. Use smaller runners for simple jobs ────────────────── lint: runs-on: ubuntu-latest # 2 CPU — fine for linting heavy-build: runs-on: ubuntu-latest-8-cores # bigger only when needed# ── 7. Parallel matrix with limits ────────────────────────── test: strategy: matrix: shard: [1, 2, 3, 4] # split tests into 4 parallel jobs max-parallel: 4 steps: - run: pytest --shard=${{ matrix.shard }}/4 tests/# ── 8. Restore cache before install ────────────────────────- name: Cache pip id: cache-pip uses: actions/cachev4 with: path: ~/.cache/pip key: pip-${{ hashFiles('requirements.txt') }}- name: Install if: steps.cache-pip.outputs.cache-hit != 'true' # skip if cached run: pip install -r requirements.txt
Optimization results example:Before: 12 minutes per PRAfter: 3.5 minutes per PRSavings breakdown:├── Path filtering: 40% of runs skipped entirely├── Caching: npm install 2min → 10 seconds├── Fail-fast order: catch lint errors before 5min test run├── Concurrency: stale runs cancelled immediately└── Parallel tests: 5min test suite → 90 seconds (4 shards)
Q19. How do you debug a failing GitHub Actions workflow?
A:
# Method 1 — Enable debug logging# Set repository secret:# ACTIONS_STEP_DEBUG = true# ACTIONS_RUNNER_DEBUG = true# → Very verbose output in workflow logs# Method 2 — Print debug info in workflowsteps:- name: Debug context env: GITHUB_CONTEXT: ${{ toJson(github) }} RUNNER_CONTEXT: ${{ toJson(runner) }} ENV_CONTEXT: ${{ toJson(env) }} run: | echo "=== GitHub Context ===" echo "$GITHUB_CONTEXT" echo "=== Runner Context ===" echo "$RUNNER_CONTEXT" echo "=== Environment ===" env | sort# Method 3 — SSH into runner (interactive debug)- name: Setup tmate SSH session uses: mxschmitt/action-tmate@v3 if: ${{ failure() }} # only on failure timeout-minutes: 15# Method 4 — Save all logs as artifact- name: Upload debug logs uses: actions/upload-artifact@v4 if: always() with: name: debug-logs path: | logs/ *.log /tmp/debug-*# Method 5 — Print environment and filesystem- name: Debug environment if: failure() run: | echo "=== PWD ===" pwd echo "=== Files ===" ls -la echo "=== Disk ===" df -h echo "=== Memory ===" free -h echo "=== Processes ===" ps aux
# Re-run with debug logging enabled# GitHub UI → Actions tab → Select failed run# → Re-run jobs → Enable debug logging checkbox# Use act for local testing# https://github.com/nektos/actbrew install act# Run workflow locallyact pushact pull_requestact -j build # run specific jobact --secret-file .env # use local secrets
Q20. What are composite actions and when would you use them over reusable workflows?
A:
# Composite Action — reusable STEPS# Lives in: .github/actions/setup-env/action.ymlname: Setup Environmentdescription: Configure build environmentinputs: python-version: default: '3.12' install-extras: default: 'false'outputs: cache-key: value: ${{ steps.cache.outputs.cache-hit }}runs: using: composite # key difference — composite steps: - uses: actions/setup-python@v5 with: python-version: ${{ inputs.python-version }} - id: cache uses: actions/cache@v4 with: path: ~/.cache/pip key: pip-${{ inputs.python-version }}-${{ hashFiles('*.txt') }} - run: pip install -r requirements.txt shell: bash - run: pip install -r requirements-dev.txt if: inputs.install-extras == 'true' shell: bash---# Use in any workflowsteps:- uses: ./.github/actions/setup-env with: python-version: '3.12' install-extras: 'true'- run: pytest tests/
Composite Action vs Reusable Workflow:Composite Action:├── Reusable STEPS within a job├── Runs in the calling job's context├── Can be used in any step position├── Shares runner with parent job├── No separate job overhead└── Best for: setup/teardown, repeated step groupsReusable Workflow:├── Reusable entire JOBS├── Runs as separate job with own runner├── Has own environment and secrets scope├── Can have approval gates├── Can run in parallel with other jobs└── Best for: complete CI stages, deploy pipelinesUse composite when: Use reusable workflow when:├── Repeated setup steps ├── Complete test/build/deploy stage├── Simple helper logic ├── Need separate environment├── No environment gates ├── Need approval gates└── Same job context OK └── Multiple jobs needed
Quick-Fire Questions
Q: What is workflow_dispatch used for? Manual trigger — adds a “Run workflow” button in the GitHub UI with optional input parameters. Used for manual deployments, one-off tasks, and emergency operations.
Q: What is the difference between pull_request and pull_request_target? pull_request runs in the fork’s context — no secrets access, safe. pull_request_target runs in the base repo’s context — has secrets access but dangerous with untrusted code. Use pull_request_target only when you understand the security implications.
Q: How do you skip a workflow run? Include [skip ci], [ci skip], [no ci], or [skip actions] in the commit message.
Q: What is if: always() used for? Runs a step even if previous steps failed. Used for cleanup tasks, notifications, and uploading test results regardless of test outcome.
Q: How do you reference outputs from a matrix job? You can’t directly — matrix jobs produce multiple outputs. Collect results into an artifact or use a summary job that gathers results from all matrix jobs.
Q: What is the maximum workflow run time? 35 days total, 6 hours per individual job. Use timeouts to fail fast rather than hitting limits.
Q: How do you trigger one workflow from another? Use workflow_dispatch event with gh workflow run, use workflow_call for reusable workflows, or use the repository_dispatch event via API call.
Q: What happens to a running workflow if you delete the branch? The workflow continues running — it was already checked out. The run will complete or fail based on its own logic.
Q: How do you share data between workflows (not jobs)? Artifacts, GitHub Cache, external storage (GCS/S3), or GitHub’s repository dispatch with payload data.
Q: What is actions/github-script used for? Runs JavaScript code with the Octokit GitHub API client pre-configured — create issues, comment on PRs, update labels, query repository data, all without leaving the workflow.