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.

Understanding GitHub Actions for CI/CD

GitHub Actions

What is GitHub Actions?

GitHub Actions is a CI/CD and automation platform built directly into GitHub. It lets you automate workflows — build, test, scan, deploy — triggered by any GitHub event.

WITHOUT GitHub Actions: WITH GitHub Actions:
─────────────────────── ───────────────────
Developer pushes code Developer pushes code
↓ ↓
Manually run tests GitHub automatically:
↓ ├── runs tests
Manually build image ├── builds image
↓ ├── scans for vulnerabilities
Manually deploy ├── deploys to staging
↓ └── notifies team
Hope nothing broke
All in minutes, automatically

Core Concepts

┌─────────────────────────────────────────────────────────────┐
│ GITHUB ACTIONS │
│ │
│ Event (trigger) │
│ └──▶ Workflow (.github/workflows/deploy.yml) │
│ └──▶ Job (runs on a runner) │
│ └──▶ Steps │
│ ├── Action (reusable unit) │
│ └── Shell command (run:) │
└─────────────────────────────────────────────────────────────┘
ConceptWhat it is
WorkflowA YAML file defining automation (.github/workflows/)
EventWhat triggers the workflow (push, PR, schedule, etc.)
JobA set of steps that run on one runner
StepA single task — action or shell command
ActionA reusable, pre-built step from marketplace
RunnerThe machine that executes jobs (GitHub-hosted or self-hosted)
SecretEncrypted variable stored in GitHub settings
ArtifactFiles saved from a workflow run
CacheSaved dependencies to speed up workflows

Workflow File Structure

# .github/workflows/ci.yml
# ── Workflow name ─────────────────────────────────────────────
name: CI Pipeline
# ── Triggers ─────────────────────────────────────────────────
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
schedule:
- cron: '0 8 * * 1-5' # weekdays at 8 AM UTC
workflow_dispatch: # manual trigger button
# ── Environment variables ─────────────────────────────────────
env:
APP_NAME: myapp
REGISTRY: ghcr.io
# ── Jobs ─────────────────────────────────────────────────────
jobs:
# Each key is a job ID
build:
name: Build and Test # display name
runs-on: ubuntu-latest # runner type
# Job-level environment
env:
NODE_ENV: test
steps:
# Each step has a name and action or run command
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Upload coverage
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage/

Triggers (Events)

on:
# ── Code events ───────────────────────────────────────────
push:
branches:
- main
- 'release/**' # wildcard
tags:
- 'v*' # version tags
paths:
- 'src/**' # only when these files change
- '!docs/**' # exclude docs changes
pull_request:
types:
- opened
- synchronize # new commits pushed
- reopened
branches:
- main
pull_request_review:
types:
- submitted
# ── Manual triggers ───────────────────────────────────────
workflow_dispatch:
inputs:
environment:
description: 'Deploy to environment'
required: true
type: choice
options:
- dev
- staging
- production
debug:
description: 'Enable debug logging'
type: boolean
default: false
# ── Scheduled ────────────────────────────────────────────
schedule:
- cron: '0 2 * * *' # daily at 2 AM
- cron: '0 8 * * 1' # monday at 8 AM
# ── Other workflows ───────────────────────────────────────
workflow_call: # called by another workflow
inputs:
version:
type: string
required: true
secrets:
DEPLOY_KEY:
required: true
# ── Repository events ─────────────────────────────────────
release:
types: [published]
issues:
types: [opened, labeled]
issue_comment:
types: [created]

Runners

jobs:
build:
# ── GitHub-hosted runners ─────────────────────────────
runs-on: ubuntu-latest # Ubuntu 22.04
runs-on: ubuntu-22.04 # specific version
runs-on: windows-latest # Windows Server 2022
runs-on: macos-latest # macOS 13
# ── Self-hosted runners ───────────────────────────────
runs-on: self-hosted
runs-on: [self-hosted, linux, x64]
runs-on: [self-hosted, linux, gpu]
# ── Matrix of runners ────────────────────────────────
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
runs-on: ${{ matrix.os }}

GitHub-Hosted Runner Specs

RunnerCPURAMStorage
ubuntu-latest2 cores7 GB14 GB
windows-latest2 cores7 GB14 GB
macos-latest3 cores14 GB14 GB
ubuntu-latest (larger)4-64 cores16-256 GBpaid

Steps — Actions vs Run

steps:
# ── Using a pre-built Action ─────────────────────────────────
- name: Checkout repository
uses: actions/checkout@v4 # GitHub's official action
with:
fetch-depth: 0 # fetch all history
# ── Running shell commands ────────────────────────────────────
- name: Build application
run: |
echo "Building..."
npm run build
echo "Done"
# ── Multi-line with specific shell ────────────────────────────
- name: Run script
shell: bash
run: |
set -euo pipefail
echo "Error-safe script"
./deploy.sh
# ── Python script ─────────────────────────────────────────────
- name: Run Python
run: |
python3 -c "
import json
data = {'status': 'ok'}
print(json.dumps(data))
"
# ── Set environment variables for next steps ─────────────────
- name: Set variables
run: |
echo "VERSION=1.2.3" >> $GITHUB_ENV
echo "BUILD_DATE=$(date -u +%Y-%m-%d)" >> $GITHUB_ENV
- name: Use variables
run: echo "Version is $VERSION built on $BUILD_DATE"
# ── Set step outputs ─────────────────────────────────────────
- name: Get version
id: version
run: echo "tag=v1.2.3" >> $GITHUB_OUTPUT
- name: Use output
run: echo "Tag is ${{ steps.version.outputs.tag }}"
# ── Conditional steps ─────────────────────────────────────────
- name: Deploy to production
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
run: ./deploy-prod.sh
- name: Notify on failure
if: failure()
run: ./notify-team.sh
- name: Always cleanup
if: always()
run: ./cleanup.sh
# ── Continue on error ─────────────────────────────────────────
- name: Optional check
continue-on-error: true
run: ./optional-scan.sh
# ── Timeout ───────────────────────────────────────────────────
- name: Long running test
timeout-minutes: 30
run: ./integration-tests.sh

Jobs — Dependencies and Parallelism

jobs:
# ── Parallel jobs (run at same time) ──────────────────────
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm run lint
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm test
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm audit
# ── Sequential job (waits for others) ────────────────────
build:
runs-on: ubuntu-latest
needs: [lint, test, security] # waits for all three
steps:
- uses: actions/checkout@v4
- run: npm run build
# ── Deploy (waits for build) ──────────────────────────────
deploy-staging:
runs-on: ubuntu-latest
needs: build
steps:
- run: ./deploy.sh staging
deploy-prod:
runs-on: ubuntu-latest
needs: deploy-staging # sequential deploys
steps:
- run: ./deploy.sh production
# Execution flow:
# lint ──┐
# test ──┼──▶ build ──▶ deploy-staging ──▶ deploy-prod
# security ──┘

Job Outputs

jobs:
get-version:
runs-on: ubuntu-latest
outputs: # declare outputs
version: ${{ steps.ver.outputs.version }}
sha: ${{ steps.ver.outputs.sha }}
steps:
- uses: actions/checkout@v4
- name: Get version
id: ver
run: |
echo "version=$(cat VERSION)" >> $GITHUB_OUTPUT
echo "sha=${GITHUB_SHA::8}" >> $GITHUB_OUTPUT
build:
needs: get-version
runs-on: ubuntu-latest
steps:
- name: Use version
run: |
echo "Building version ${{ needs.get-version.outputs.version }}"
echo "SHA: ${{ needs.get-version.outputs.sha }}"

Matrix Builds

Test across multiple versions/platforms simultaneously:

jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false # don't cancel others if one fails
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
python: ['3.10', '3.11', '3.12']
exclude:
- os: windows-latest
python: '3.10' # skip this combination
include:
- os: ubuntu-latest
python: '3.12'
experimental: true # extra combination
steps:
- uses: actions/checkout@v4
- name: Setup Python ${{ matrix.python }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python }}
- name: Run tests
run: pytest tests/
# Creates 8 parallel jobs:
# ubuntu + 3.10, 3.11, 3.12
# windows + 3.11, 3.12 (3.10 excluded)
# macos + 3.10, 3.11, 3.12

Secrets and Variables

# Using secrets (encrypted)
steps:
- name: Deploy
env:
API_KEY: ${{ secrets.API_KEY }}
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
run: ./deploy.sh
# Using variables (not encrypted)
- name: Configure
env:
APP_ENV: ${{ vars.APP_ENV }}
REGION: ${{ vars.AWS_REGION }}
run: ./configure.sh
# Built-in secrets
- name: Push to registry
run: |
echo "${{ secrets.GITHUB_TOKEN }}" | \
docker login ghcr.io -u ${{ github.actor }} --password-stdin

Secret Scopes

Organization secrets → available to all repos in org
Repository secrets → available to one repo
Environment secrets → available only in that environment
Settings → Secrets and variables → Actions

Environments and Approvals

jobs:
deploy-prod:
runs-on: ubuntu-latest
environment:
name: production # must be configured in Settings
url: https://myapp.com # shown in GitHub UI
steps:
- name: Deploy
run: ./deploy.sh production
# In GitHub Settings → Environments → production:
# ✅ Required reviewers: [senior-devops, team-lead]
# ✅ Wait timer: 5 minutes
# ✅ Deployment branches: main only
# → Pipeline pauses and sends approval request
# → Deployment only proceeds after approval

Caching Dependencies

steps:
# ── Node.js / npm ─────────────────────────────────────────────
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm' # built-in cache
# ── Manual cache control ──────────────────────────────────────
- name: Cache node modules
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
# ── Python / pip ──────────────────────────────────────────────
- uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: 'pip'
# ── Go modules ───────────────────────────────────────────────
- uses: actions/setup-go@v5
with:
go-version: '1.21'
cache: true
# ── Docker layers ─────────────────────────────────────────────
- name: Cache Docker layers
uses: actions/cache@v4
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-

Artifacts

steps:
# ── Upload artifact ───────────────────────────────────────────
- name: Build
run: npm run build
- name: Upload build
uses: actions/upload-artifact@v4
with:
name: build-output
path: dist/
retention-days: 7 # keep for 7 days
# ── Upload test results ───────────────────────────────────────
- name: Upload test results
uses: actions/upload-artifact@v4
if: always() # upload even if tests fail
with:
name: test-results
path: |
test-results/
coverage/
*.xml
# ── Download in another job ───────────────────────────────────
jobs:
build:
steps:
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: app-binary
path: bin/app
deploy:
needs: build
steps:
- name: Download artifact
uses: actions/download-artifact@v4
with:
name: app-binary
path: bin/
- name: Deploy
run: ./deploy.sh bin/app

Real-World Workflows

Full CI/CD Pipeline
# .github/workflows/cicd.yml
name: CI/CD Pipeline
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE: ghcr.io/${{ github.repository }}
jobs:
# ── 1. Code Quality ────────────────────────────────────────
quality:
name: Code Quality
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: pip
- run: pip install -r requirements-dev.txt
- name: Lint
run: |
flake8 src/
black --check src/
isort --check src/
- name: Type check
run: mypy src/
# ── 2. Tests ──────────────────────────────────────────────
test:
name: Tests
runs-on: ubuntu-latest
needs: quality
services: # spin up dependencies
postgres:
image: postgres:15
env:
POSTGRES_PASSWORD: testpass
POSTGRES_DB: testdb
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis:7
ports:
- 6379:6379
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: pip
- run: pip install -r requirements.txt
- name: Run tests with coverage
env:
DATABASE_URL: postgresql://postgres:testpass@localhost/testdb
REDIS_URL: redis://localhost:6379
run: |
pytest tests/ \
--cov=src \
--cov-report=xml \
--cov-report=html \
--junitxml=test-results.xml \
-v
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
file: coverage.xml
- name: Upload test results
uses: actions/upload-artifact@v4
if: always()
with:
name: test-results
path: test-results.xml
# ── 3. Security Scanning ──────────────────────────────────
security:
name: Security
runs-on: ubuntu-latest
needs: quality
permissions:
security-events: write
steps:
- uses: actions/checkout@v4
- name: Dependency audit
run: pip-audit -r requirements.txt
- name: SAST scan
uses: github/codeql-action/init@v3
with:
languages: python
- uses: github/codeql-action/analyze@v3
- name: Secret scanning
uses: trufflesecurity/trufflehog@main
with:
path: ./
base: ${{ github.event.repository.default_branch }}
# ── 4. Build Image ────────────────────────────────────────
build:
name: Build Image
runs-on: ubuntu-latest
needs: [test, security]
permissions:
contents: read
packages: write
outputs:
image-digest: ${{ steps.build.outputs.digest }}
image-tag: ${{ steps.meta.outputs.tags }}
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.IMAGE }}
tags: |
type=sha,prefix={{branch}}-
type=ref,event=branch
type=semver,pattern={{version}}
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
- name: Build and push
id: build
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
VERSION=${{ github.sha }}
BUILD_DATE=${{ github.event.head_commit.timestamp }}
- name: Scan image for vulnerabilities
uses: aquasecurity/trivy-action@master
with:
image-ref: ${{ env.IMAGE }}:${{ github.sha }}
format: sarif
output: trivy.sarif
severity: CRITICAL,HIGH
exit-code: '1'
- name: Upload Trivy results
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: trivy.sarif
# ── 5. Deploy Staging ─────────────────────────────────────
deploy-staging:
name: Deploy Staging
runs-on: ubuntu-latest
needs: build
if: github.ref == 'refs/heads/main'
environment:
name: staging
url: https://staging.myapp.com
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: staging-cluster
location: us-central1
- name: Deploy to staging
run: |
kubectl set image deployment/api \
api=${{ env.IMAGE }}@${{ needs.build.outputs.image-digest }} \
-n staging
kubectl rollout status deployment/api -n staging
- name: Run smoke tests
run: |
sleep 30
curl -f https://staging.myapp.com/health
# ── 6. Deploy Production ──────────────────────────────────
deploy-prod:
name: Deploy Production
runs-on: ubuntu-latest
needs: deploy-staging
environment:
name: production # requires approval
url: https://myapp.com
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_PROD_SA }}
- name: Get GKE credentials
uses: google-github-actions/get-gke-credentials@v2
with:
cluster_name: prod-cluster
location: us-central1
- name: Deploy to production
run: |
kubectl set image deployment/api \
api=${{ env.IMAGE }}@${{ needs.build.outputs.image-digest }} \
-n production
kubectl rollout status deployment/api -n production \
--timeout=5m
- name: Verify deployment
run: |
sleep 60
curl -f https://myapp.com/health
- name: Notify success
uses: slackapi/slack-github-action@v1.26.0
with:
payload: |
{
"text": "✅ Deployed to production: ${{ github.sha }}"
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
- name: Rollback on failure
if: failure()
run: |
kubectl rollout undo deployment/api -n production
echo "🔴 Rolled back production deployment"

Reusable Workflows

# .github/workflows/reusable-deploy.yml
# Called by other workflows
name: Reusable Deploy
on:
workflow_call:
inputs:
environment:
required: true
type: string
image-tag:
required: true
type: string
secrets:
DEPLOY_KEY:
required: true
outputs:
deployment-url:
value: ${{ jobs.deploy.outputs.url }}
jobs:
deploy:
runs-on: ubuntu-latest
environment: ${{ inputs.environment }}
outputs:
url: ${{ steps.deploy.outputs.url }}
steps:
- name: Deploy
id: deploy
run: |
./deploy.sh ${{ inputs.environment }} ${{ inputs.image-tag }}
echo "url=https://${{ inputs.environment }}.myapp.com" >> $GITHUB_OUTPUT
---
# .github/workflows/main.yml
# Calls the reusable workflow
jobs:
deploy-staging:
uses: ./.github/workflows/reusable-deploy.yml
with:
environment: staging
image-tag: ${{ needs.build.outputs.tag }}
secrets:
DEPLOY_KEY: ${{ secrets.STAGING_DEPLOY_KEY }}
deploy-prod:
needs: deploy-staging
uses: ./.github/workflows/reusable-deploy.yml
with:
environment: production
image-tag: ${{ needs.build.outputs.tag }}
secrets:
DEPLOY_KEY: ${{ secrets.PROD_DEPLOY_KEY }}

Composite Actions

# .github/actions/setup-app/action.yml
# Custom reusable action
name: Setup Application
description: Install and configure the application
inputs:
python-version:
description: Python version
required: false
default: '3.12'
install-dev:
description: Install dev dependencies
required: false
default: 'false'
outputs:
cache-hit:
description: Whether cache was hit
value: ${{ steps.cache.outputs.cache-hit }}
runs:
using: composite
steps:
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: ${{ inputs.python-version }}
- name: Cache dependencies
id: cache
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: pip-${{ inputs.python-version }}-${{ hashFiles('requirements*.txt') }}
- name: Install dependencies
shell: bash
run: |
pip install -r requirements.txt
if [ "${{ inputs.install-dev }}" == "true" ]; then
pip install -r requirements-dev.txt
fi
---
# Use in workflow
steps:
- uses: ./.github/actions/setup-app
with:
python-version: '3.12'
install-dev: 'true'

Expressions and Context

# GitHub Contexts
${{ github.sha }} # commit SHA
${{ github.ref }} # branch/tag ref
${{ github.event_name }} # push, pull_request, etc
${{ github.actor }} # user who triggered
${{ github.repository }} # owner/repo
${{ github.run_id }} # unique run ID
${{ github.run_number }} # sequential run number
${{ runner.os }} # Linux, Windows, macOS
${{ runner.temp }} # temp directory path
${{ secrets.MY_SECRET }} # encrypted secret
${{ vars.MY_VAR }} # variable (not encrypted)
${{ env.MY_ENV_VAR }} # environment variable
${{ steps.my-step.outputs.key }} # step output
${{ needs.my-job.outputs.key }} # job output
${{ needs.my-job.result }} # success/failure/etc
# Expressions
${{ 2 + 2 }} # math
${{ 'hello' == 'hello' }} # comparison
${{ contains(github.ref, 'main') }}
${{ startsWith(github.ref, 'refs/tags/') }}
${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
# Conditionals
if: github.ref == 'refs/heads/main'
if: failure()
if: success()
if: always()
if: cancelled()
if: ${{ github.event.inputs.environment == 'production' }}

Popular Marketplace Actions

# Source control
- uses: actions/checkout@v4
- uses: actions/upload-artifact@v4
- uses: actions/download-artifact@v4
- uses: actions/cache@v4
# Language setup
- uses: actions/setup-node@v4
- uses: actions/setup-python@v5
- uses: actions/setup-go@v5
- uses: actions/setup-java@v4
# Docker
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
- uses: docker/build-push-action@v5
- uses: docker/metadata-action@v5
# Cloud providers
- uses: google-github-actions/auth@v2
- uses: google-github-actions/get-gke-credentials@v2
- uses: azure/login@v1
- uses: aws-actions/configure-aws-credentials@v4
# Security
- uses: aquasecurity/trivy-action@master
- uses: bridgecrewio/checkov-action@master
- uses: github/codeql-action/analyze@v3
- uses: snyk/actions/node@master
# Notifications
- uses: slackapi/slack-github-action@v1
- uses: peter-evans/create-issue-from-file@v5
# Terraform
- uses: hashicorp/setup-terraform@v3
- uses: dflook/terraform-plan@v1
- uses: dflook/terraform-apply@v1

Debugging Workflows

# Enable debug logging
# Set secret: ACTIONS_STEP_DEBUG = true
# Set secret: ACTIONS_RUNNER_DEBUG = true
steps:
# Debug — print all context
- name: Dump context
env:
GITHUB_CONTEXT: ${{ toJson(github) }}
RUNNER_CONTEXT: ${{ toJson(runner) }}
run: |
echo "GitHub context:"
echo "$GITHUB_CONTEXT"
echo "Runner context:"
echo "$RUNNER_CONTEXT"
# SSH into runner for debugging
- name: Setup tmate session
uses: mxschmitt/action-tmate@v3
if: failure() # only on failure
timeout-minutes: 15
# Print environment
- name: Debug environment
run: |
echo "PATH=$PATH"
echo "PWD=$PWD"
env | sort
ls -la

Cost and Limits

GitHub-hosted runner minutes (free tier):
├── Public repos: Unlimited free
├── Private repos:
│ ├── Free plan: 2,000 min/month
│ ├── Pro: 3,000 min/month
│ ├── Team: 3,000 min/month
│ └── Enterprise: 50,000 min/month
Minute multipliers:
├── Linux: 1x (1 minute = 1 minute)
├── Windows: 2x (1 minute = 2 minutes)
├── macOS: 10x (1 minute = 10 minutes)
Other limits:
├── Max workflow run time: 35 days
├── Max job run time: 6 hours
├── Max jobs per workflow: 500
├── Max concurrent jobs: depends on plan
├── Artifact storage: 500 MB (free), 2 GB (Pro)
└── Cache storage: 10 GB per repo

Best Practices

Security:
✅ Pin action versions to SHA (not @v1 tag)
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
✅ Use OIDC instead of long-lived credentials
✅ Minimal permissions on GITHUB_TOKEN
✅ Never echo secrets in logs
✅ Use environment secrets for production
Performance:
✅ Cache dependencies aggressively
✅ Run jobs in parallel where possible
✅ Use path filters to skip unnecessary runs
✅ Fail fast on cheap checks (lint before test)
✅ Use Docker layer caching for builds
Maintainability:
✅ Use reusable workflows for repeated patterns
✅ Use composite actions for complex step groups
✅ Keep workflows focused — one workflow per concern
✅ Use concurrency groups to cancel stale runs
✅ Add workflow_dispatch for manual control
Reliability:
✅ Set timeouts on all jobs and steps
✅ Always upload test results even on failure
✅ Use continue-on-error for non-blocking checks
✅ Add rollback steps on deployment failure
✅ Test workflows in feature branches
# Concurrency — cancel stale runs
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true # cancel old run when new one starts
# Minimal permissions
permissions:
contents: read # default — read only
packages: write # only what you need
security-events: write

GitHub Actions is the glue that holds modern DevOps together — it connects your code to every tool in your stack, automates every repetitive task, and enforces quality gates without slowing down your team. The YAML-based workflow model is powerful enough to handle any automation need while staying readable and maintainable.