GitHub Actions: From Beginner to Advanced
📅 Published: June 2026
⏱️ Estimated Reading Time: 28 minutes
🏷️ Tags: GitHub Actions, CI/CD, Automation, DevOps, Workflows
Introduction: What is GitHub Actions?
GitHub Actions is a CI/CD platform built directly into GitHub. It allows you to automate your software development workflows right in your repository. You can build, test, and deploy code without leaving GitHub.
Think of GitHub Actions as a powerful automation engine that responds to events in your repository. When someone pushes code, opens a pull request, or creates a release, GitHub Actions can run any command you specify—from running tests to deploying to production.
Why GitHub Actions matters:
No separate CI/CD tool to manage
Tight integration with GitHub (PR comments, status checks, secrets)
Free for public repositories (generous free tier for private)
Huge marketplace of pre-built actions
Runs on Linux, Windows, and macOS
Part 1: Core Concepts
The Building Blocks
Event → Workflow → Job → Step → Action/Run
| Concept | Description | Example |
|---|---|---|
| Event | What triggers the workflow | push, pull_request, schedule |
| Workflow | The entire automation process | A YAML file in .github/workflows/ |
| Job | A set of steps that run on the same runner | test, build, deploy |
| Step | A single task within a job | uses: actions/checkout@v4 |
| Action | Reusable unit of code | actions/setup-node@v4 |
| Runner | The server that executes jobs | ubuntu-latest, windows-latest |
Your First Workflow
Create .github/workflows/ci.yml:
name: CI on: [push] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Run a one-liner run: echo "Hello, GitHub Actions!"
When you push this file, GitHub Actions runs the workflow. You can see it in the Actions tab of your repository.
Part 2: Events (Triggers)
Push and Pull Request Events
# Run on every push to any branch on: [push] # Run on push to specific branches on: push: branches: [ main, develop ] # Run on pull request to main on: pull_request: branches: [ main ] # Run on both push and pull request on: push: branches: [ main ] pull_request: branches: [ main ]
Path Filtering
on: push: paths: - 'src/**' # Any file in src directory - '**.js' # Any JavaScript file - '!docs/**' # Exclude docs directory
Scheduled Events (Cron)
on: schedule: # Runs at 2 AM UTC every day - cron: '0 2 * * *' # Runs at 9 AM and 5 PM Monday-Friday - cron: '0 9,17 * * 1-5'
Manual Trigger (Workflow Dispatch)
on: workflow_dispatch: inputs: environment: description: 'Deployment environment' required: true default: 'staging' type: choice options: - dev - staging - prod
Users can then manually trigger the workflow from GitHub UI with selected inputs.
Other Useful Events
on: release: types: [published] # When a release is published workflow_call: # Called by another workflow secrets: API_KEY: required: true repository_dispatch: # Triggered by external API types: [webhook]
Part 3: Jobs and Runners
Basic Job Configuration
jobs: test: runs-on: ubuntu-latest steps: - run: echo "Running tests"
Multiple Jobs (Parallel by Default)
jobs: lint: runs-on: ubuntu-latest steps: - run: npm run lint test: runs-on: ubuntu-latest steps: - run: npm test build: runs-on: ubuntu-latest steps: - run: npm run build
Job Dependencies (needs)
jobs: test: runs-on: ubuntu-latest steps: - run: npm test build: runs-on: ubuntu-latest needs: test # Waits for test to complete steps: - run: npm run build deploy: runs-on: ubuntu-latest needs: [test, build] # Waits for both steps: - run: npm run deploy
Matrix Strategy (Testing Multiple Configurations)
jobs: test: runs-on: ubuntu-latest strategy: matrix: node-version: [16, 18, 20] os: [ubuntu-latest, windows-latest] steps: - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - run: npm test
This creates 6 parallel jobs (3 Node versions × 2 operating systems).
Runner Types
| Runner | Specs | Best For |
|---|---|---|
ubuntu-latest | 2-core, 7GB RAM | Most Linux/Node.js/Python apps |
windows-latest | 2-core, 7GB RAM | .NET, Windows-specific |
macos-latest | 3-core, 14GB RAM | iOS/macOS builds |
self-hosted | Your own hardware | Special requirements, cost control |
Part 4: Steps and Actions
Using Actions vs Running Commands
steps: # Using an action (reusable) - uses: actions/checkout@v4 # Running a command - name: Install dependencies run: npm install # Multiple commands - name: Build and test run: | npm run build npm test
Popular Built-in Actions
| Action | Purpose |
|---|---|
actions/checkout@v4 | Check out repository code |
actions/setup-node@v4 | Install Node.js |
actions/setup-python@v5 | Install Python |
actions/upload-artifact@v4 | Save build artifacts |
actions/download-artifact@v4 | Download saved artifacts |
actions/cache@v3 | Cache dependencies |
actions/github-script@v7 | Run JavaScript using GitHub API |
Example: Complete Node.js CI
name: Node.js CI on: push: branches: [ main ] pull_request: branches: [ main ] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: '18' cache: 'npm' - run: npm ci - run: npm run lint - run: npm test - uses: actions/upload-artifact@v4 if: always() with: name: test-results path: test-results.xml
Part 5: Environment Variables and Secrets
Setting Environment Variables
jobs: deploy: runs-on: ubuntu-latest # Job-level environment variables env: NODE_ENV: production steps: # Step-level environment variables - name: Deploy env: API_KEY: ${{ secrets.API_KEY }} run: ./deploy.sh
Using GitHub Context Variables
- name: Show context run: | echo "Repository: ${{ github.repository }}" echo "Branch: ${{ github.ref_name }}" echo "Commit: ${{ github.sha }}" echo "Actor: ${{ github.actor }}" echo "Event: ${{ github.event_name }}"
Secrets
Store secrets in: Repository → Settings → Secrets and variables → Actions
steps: - name: Deploy to AWS env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} run: aws s3 sync dist/ s3://my-bucket
Environment-Specific Secrets
jobs: deploy-staging: runs-on: ubuntu-latest environment: staging steps: - name: Deploy env: API_KEY: ${{ secrets.API_KEY }} # From staging environment run: ./deploy.sh deploy-production: runs-on: ubuntu-latest environment: production steps: - name: Deploy env: API_KEY: ${{ secrets.API_KEY }} # From production environment run: ./deploy.sh
Part 6: Conditional Execution
Using if Conditions
steps: - name: Only on main branch if: github.ref == 'refs/heads/main' run: echo "Deploying to production" - name: Only on pull requests if: github.event_name == 'pull_request' run: echo "Running PR checks" - name: Only on schedule if: github.event_name == 'schedule' run: echo "Running scheduled job" - name: Only on success if: success() run: echo "Previous steps succeeded" - name: Only on failure if: failure() run: echo "Something failed" - name: Always run if: always() run: echo "Runs even if previous steps failed"
Job-level Conditions
jobs: deploy-prod: runs-on: ubuntu-latest if: github.ref == 'refs/heads/main' steps: - run: ./deploy.sh
Part 7: Caching Dependencies
Caching npm Dependencies
- uses: actions/cache@v3 with: path: ~/.npm key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }} restore-keys: | ${{ runner.os }}-node-
Caching pip Dependencies (Python)
- uses: actions/cache@v3 with: path: ~/.cache/pip key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }} restore-keys: | ${{ runner.os }}-pip-
Caching Docker Layers
- uses: actions/cache@v3 with: path: /tmp/.buildx-cache key: ${{ runner.os }}-buildx-${{ github.sha }} restore-keys: | ${{ runner.os }}-buildx-
Setup Node with Built-in Caching (Simpler)
- uses: actions/setup-node@v4 with: node-version: '18' cache: 'npm' # Automatically caches node_modules
Part 8: Artifacts
Uploading Build Artifacts
- name: Build run: npm run build - name: Upload build artifacts uses: actions/upload-artifact@v4 with: name: build-output path: dist/ retention-days: 7
Downloading Artifacts
- name: Download build artifacts uses: actions/download-artifact@v4 with: name: build-output path: ./downloaded-build
Multiple Artifacts
- name: Upload multiple artifacts uses: actions/upload-artifact@v4 with: name: all-builds path: | dist/ coverage/ logs/
Part 9: Matrix Builds (Advanced)
Testing Across Multiple Versions
jobs: test: runs-on: ubuntu-latest strategy: matrix: node: [16, 18, 20] os: [ubuntu-latest, windows-latest] exclude: - os: windows-latest node: 16 steps: - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node }} - run: npm test
Dynamic Matrix from JSON
jobs: setup: runs-on: ubuntu-latest outputs: matrix: ${{ steps.set-matrix.outputs.matrix }} steps: - id: set-matrix run: | echo 'matrix={"node":["16","18","20"]}' >> $GITHUB_OUTPUT test: needs: setup runs-on: ubuntu-latest strategy: matrix: ${{ fromJson(needs.setup.outputs.matrix) }} steps: - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node }} - run: npm test
Part 10: Reusable Workflows
Creating a Reusable Workflow
Save as .github/workflows/test.yml:
name: Reusable Test Workflow on: workflow_call: inputs: node-version: description: 'Node.js version' required: true type: string environment: description: 'Test environment' required: false type: string default: 'development' secrets: API_KEY: required: true jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: ${{ inputs.node-version }} - run: npm test env: NODE_ENV: ${{ inputs.environment }} API_KEY: ${{ secrets.API_KEY }}
Using a Reusable Workflow
name: CI on: [push] jobs: call-test: uses: ./.github/workflows/test.yml with: node-version: '18' environment: 'staging' secrets: API_KEY: ${{ secrets.API_KEY }}
Calling Workflows from Other Repositories
jobs: call-test: uses: my-org/shared-workflows/.github/workflows/test.yml@main with: node-version: '18' secrets: API_KEY: ${{ secrets.API_KEY }}
Part 11: Deployment Examples
Deploy to AWS S3
- name: Deploy to S3 uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: arn:aws:iam::123456789012:role/github-actions-role aws-region: us-west-2 - name: Sync to S3 run: aws s3 sync dist/ s3://my-bucket --delete
Deploy to AWS ECS
- name: Build Docker image run: docker build -t myapp:${{ github.sha }} . - name: Push to ECR run: | aws ecr get-login-password | docker login --username AWS --password-stdin ${{ secrets.ECR_REGISTRY }} docker push ${{ secrets.ECR_REGISTRY }}/myapp:${{ github.sha }} - name: Deploy to ECS run: | aws ecs update-service --cluster production --service myapp --force-new-deployment
Deploy to GitHub Pages
- name: Build run: npm run build - name: Deploy to GitHub Pages uses: peaceiris/actions-gh-pages@v3 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./build
Deploy to Vercel
- name: Deploy to Vercel uses: amondnet/vercel-action@v20 with: vercel-token: ${{ secrets.VERCEL_TOKEN }} vercel-org-id: ${{ secrets.VERCEL_ORG_ID }} vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }} vercel-args: '--prod'
Part 12: Complete Production Pipeline Example
name: Complete CI/CD Pipeline on: pull_request: branches: [ main ] push: branches: [ main ] release: types: [published] env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: '18' - run: npm ci - run: npm run lint test: runs-on: ubuntu-latest needs: lint steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: '18' - run: npm ci - run: npm test -- --coverage - uses: codecov/codecov-action@v4 with: token: ${{ secrets.CODECOV_TOKEN }} build: runs-on: ubuntu-latest needs: test steps: - uses: actions/checkout@v4 - name: Build Docker image run: docker build -t ${{ env.IMAGE_NAME }}:${{ github.sha }} . - name: Push to registry if: github.ref == 'refs/heads/main' || github.event_name == 'release' run: | echo ${{ secrets.GITHUB_TOKEN }} | docker login ${{ env.REGISTRY }} -u ${{ github.actor }} --password-stdin docker push ${{ env.IMAGE_NAME }}:${{ github.sha }} docker tag ${{ env.IMAGE_NAME }}:${{ github.sha }} ${{ env.IMAGE_NAME }}:latest docker push ${{ env.IMAGE_NAME }}:latest deploy-staging: runs-on: ubuntu-latest needs: build if: github.ref == 'refs/heads/main' environment: staging steps: - name: Deploy to staging run: | kubectl set image deployment/myapp myapp=${{ env.IMAGE_NAME }}:${{ github.sha }} -n staging kubectl rollout status deployment/myapp -n staging deploy-production: runs-on: ubuntu-latest needs: deploy-staging if: github.event_name == 'release' environment: name: production url: https://example.com steps: - name: Deploy to production run: | kubectl set image deployment/myapp myapp=${{ env.IMAGE_NAME }}:${{ github.sha }} -n production kubectl rollout status deployment/myapp -n production create-release: runs-on: ubuntu-latest needs: build if: github.event_name == 'push' && github.ref == 'refs/heads/main' permissions: contents: write steps: - uses: actions/checkout@v4 - name: Create Release uses: softprops/action-gh-release@v1 with: tag_name: v${{ github.run_number }} name: Release v${{ github.run_number }} generate_release_notes: true
Part 13: Advanced Patterns
OIDC Authentication (No Long-lived Secrets)
- name: Configure AWS credentials with OIDC uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: arn:aws:iam::123456789012:role/github-actions-role aws-region: us-west-2
Self-Hosted Runners
jobs: build: runs-on: self-hosted steps: - run: echo "Running on my own server"
Concurrency Control
concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true
Timeouts
jobs: build: runs-on: ubuntu-latest timeout-minutes: 30 steps: - run: npm run build
GitHub Actions Commands Cheat Sheet
# Workflow syntax name: workflow-name on: [push, pull_request] jobs: job-name: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Step name run: command # Conditionals if: github.ref == 'refs/heads/main' if: success() if: failure() if: always() # Contexts ${{ github.repository }} ${{ github.ref_name }} ${{ github.sha }} ${{ secrets.MY_SECRET }} ${{ env.MY_VAR }} ${{ vars.MY_VARIABLE }}
Common Interview Questions
Q: What is the difference between uses and run?
A: uses calls a pre-built reusable action (like actions/checkout@v4). run executes shell commands directly. Use uses for common tasks, run for your specific commands.
Q: How do you pass data between jobs?
A: Use artifacts for files, outputs for values:
# Output from first job - name: Set output id: set run: echo "value=hello" >> $GITHUB_OUTPUT # Use in second job ${{ needs.first-job.outputs.value }}
Q: How do you limit workflow execution to specific file paths?
A: Use paths and paths-ignore in the on section:
on: push: paths: - 'src/**' - '!docs/**'
Summary
| Concept | Key Points |
|---|---|
| Events | Triggers: push, pull_request, schedule, workflow_dispatch |
| Jobs | Run in parallel by default, use needs for ordering |
| Runners | ubuntu, windows, macos, self-hosted |
| Steps | Actions (uses) or commands (run) |
| Matrix | Test multiple configurations in parallel |
| Artifacts | Share files between jobs |
| Secrets | Store sensitive data, never expose in logs |
| Caching | Speed up workflows by reusing dependencies |
| Environments | Separate settings for dev, staging, prod |
Learn More
Practice GitHub Actions with hands-on exercises in our interactive labs:
https://devops.trainwithsky.com/
Comments
Post a Comment