Mastering GitHub Actions: Key Interview Queries

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 ActionsJenkins
SetupZero — built into GitHubInstall + maintain server
ConfigYAML in repoGroovy / Jenkinsfile
InfrastructureGitHub-hosted runnersYou manage servers
Marketplace20,000+ actionsPlugin ecosystem
CostFree tier includedServer costs
ScalingAutomaticManual configuration
IntegrationNative GitHubWebhooks/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 name
on: # 2. Event (trigger)
push:
branches: [main]
env: # 3. Environment variables
APP_NAME: myapp
jobs: # 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-hosted
runs-on: ubuntu-latest
# Self-hosted
runs-on: self-hosted
runs-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 workflows
steps:
- 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 org
Repository → specific repo only
Environment → 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 needed
permissions:
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-node@v4
with:
node-version: '20'
cache: 'npm' # handles cache automatically
- uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: 'pip'
# Method 2 Manual cache control
- name: Cache node modules
uses: actions/cache@v4
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 install
Cache 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-prod
security ──┘
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:

# Workflow
jobs:
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 → production
Protection rules:
✅ Required reviewers: [alice, bob] # must approve
✅ Wait timer: 10 minutes # cooling period
✅ Deployment branches: main only # restrict who can deploy
Flow:
1. Workflow reaches deploy-prod job
2. GitHub sends approval request to reviewers
3. Pipeline pauses — shows "Waiting for review"
4. Reviewer approves in GitHub UI
5. Job proceeds with deployment
# Multiple environments with progression
jobs:
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 indefinitely
OIDC (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 setup
jobs:
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 global
gcloud 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 workflow
name: Reusable Test
on:
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 workflow
jobs:
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 runs
concurrency:
# 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 pushed
concurrency:
group: pr-${{ github.event.pull_request.number }}
cancel-in-progress: true
# Never cancel production deployments
concurrency:
group: deploy-production
cancel-in-progress: false # queue instead of cancel
# Per-environment concurrency
concurrency:
group: deploy-${{ github.event.inputs.environment }}
cancel-in-progress: false
# Different behavior per branch
concurrency:
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 resources
Scenario 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 environments
secrets:
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
# GOOD — environment-scoped secrets
# production environment has its own DB_PASSWORD
# staging environment has its own DB_PASSWORD
environment: production # uses production secrets only
# Risk 5: Logging context that contains secrets
# BAD
- run: echo "${{ toJson(secrets) }}" # NEVER — logs all secrets
# Mitigation — minimal permissions
permissions:
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 ARC
helm install arc \
oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set-controller \
--namespace arc-systems \
--create-namespace
# Deploy runner scale set
helm 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 runners
apiVersion: actions.github.com/v1alpha1
kind: AutoscalingRunnerSet
metadata:
name: arc-runner-set
namespace: arc-runners
spec:
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 runner
jobs:
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-node@v4
with:
cache: 'npm'
- name: Cache build output
uses: actions/cache@v4
with:
path: .next/cache
key: nextjs-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('src/**') }}
- name: Cache Docker layers
uses: actions/cache@v4
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/cache@v4
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 PR
After: 3.5 minutes per PR
Savings 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 workflow
steps:
- 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/act
brew install act
# Run workflow locally
act push
act pull_request
act -j build # run specific job
act --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.yml
name: Setup Environment
description: Configure build environment
inputs:
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 workflow
steps:
- 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 groups
Reusable 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 pipelines
Use 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.

Leave a Reply