Courses/CI/CD with GitHub Actions
Standalone Course~30 min12 challenges

CI/CD with GitHub Actions: Automated Testing & Deployment

Set up automated testing and deployment for your Grit projects. You'll understand the CI/CD workflows Grit scaffolds, extend them with frontend tests, configure automated deployments, and set up branch protection for a professional development workflow.


What is CI/CD?

Without CI/CD, shipping code looks like this: you write code, run tests manually (maybe), build the project, SSH into a server, pull the code, restart services, and pray nothing breaks. With CI/CD, you push code and everything else happens automatically.

CI (Continuous Integration): Automatically testing code every time it's pushed to the repository. Every push triggers a build and test suite. If tests fail, the team knows immediately — before the code reaches production. CI catches bugs early.
CD (Continuous Deployment): Automatically deploying code after tests pass. When CI confirms the code is good, CD ships it to staging or production without manual intervention. No more SSH-and-pray deployments.
Pipeline: A series of automated steps that run in sequence: test, build, deploy. Each step must pass before the next one runs. If any step fails, the pipeline stops and notifies the team. Think of it as an assembly line for code.

A typical CI/CD pipeline:

  • 1. Developer pushes code to GitHub
  • 2. CI runs Go tests (go test -race ./...)
  • 3. CI runs frontend tests (pnpm test)
  • 4. If all tests pass, CD deploys to staging
  • 5. On tag push (v1.0.0), CD deploys to production
1

Challenge: Explain CI/CD

Explain CI/CD in your own words. Why is it important? What happens when a team doesn't use CI/CD? Think about: how bugs reach production, how long deployments take, and how confident developers feel pushing code on a Friday afternoon.

Grit's Scaffolded CI

When you run grit new, it scaffolds a .github/workflows/ci.yml file. This is a complete CI workflow that runs automatically on every push and pull request.

.github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: '1.24'
      - run: cd apps/api && go test -race ./...
GitHub Actions: GitHub's built-in CI/CD platform. It runs jobs on GitHub's servers, triggered by events like push, pull request, schedule, or manual dispatch. Free for public repos, with generous free minutes for private repos. No external CI service needed.

This workflow does 3 things:

  • Checks out your code — downloads the repo to the runner
  • Sets up Go — installs the specified Go version
  • Runs tests — executes all Go tests with race detection enabled
The -race flag enables Go's race detector, which catches concurrent access bugs. It's slower than normal tests but catches real production issues. Always use it in CI.
2

Challenge: Find Your CI Workflow

Find the .github/workflows/ directory in your Grit project. What workflow files exist? Open ci.yml and read through it. What triggers the workflow? What Go version does it use?

Understanding the Workflow

YAML workflows can look intimidating. Let's break down every section so you can read and write them confidently.

Workflow Anatomy
# name: Human-readable name shown in GitHub UI
name: CI

# on: Events that trigger this workflow
# [push, pull_request] = run on every push AND every PR
on: [push, pull_request]

# jobs: Groups of steps that run on a fresh virtual machine
jobs:
  # "test" is the job ID (you choose the name)
  test:
    # runs-on: The OS for the virtual machine
    runs-on: ubuntu-latest

    # steps: Commands that run sequentially
    steps:
      # uses: Run a pre-built action (like a plugin)
      - uses: actions/checkout@v4

      # with: Configuration for the action
      - uses: actions/setup-go@v5
        with:
          go-version: '1.24'

      # run: Execute a shell command
      - run: cd apps/api && go test -race ./...

Key concepts:

  • Jobs run in parallel by default — unless you use needs to create dependencies
  • Steps run sequentially within a job — if one fails, the rest are skipped
  • Each job gets a fresh VM — nothing persists between jobs unless you use artifacts
  • Actions are reusableactions/checkout@v4 is maintained by GitHub
3

Challenge: Read the CI Workflow

Read your ci.yml file carefully. Answer these questions: (1) What Go version does it use? (2) What test flags are set? (3) What events trigger it? (4) What operating system does the runner use? (5) How many jobs are defined?

Adding Frontend Tests

The scaffolded CI only tests the Go API. For a complete pipeline, add a separate job that tests the frontend — Vitest for unit tests, Playwright for end-to-end tests.

Frontend Test Job
  test-frontend:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npm install -g pnpm
      - run: pnpm install
      - run: pnpm test

This job runs in parallel with the Go test job. Both must pass for the overall CI to be green. If either fails, the PR gets a red X.

For end-to-end tests with Playwright, you need a running API. Use a service container:

E2E Test Job (Advanced)
  test-e2e:
    runs-on: ubuntu-latest
    needs: [test, test-frontend]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npm install -g pnpm
      - run: pnpm install
      - run: npx playwright install --with-deps
      - run: pnpm test:e2e
The needs: [test, test-frontend] line means E2E tests only run after both unit test jobs pass. No point running expensive browser tests if basic tests fail.
4

Challenge: Add Frontend Tests to CI

Add a test-frontend job to your CI workflow. It should set up Node.js 20, install pnpm, install dependencies, and run pnpm test. Push the change and check the Actions tab — do both jobs run?

The Release Workflow

Grit also scaffolds a release.yml workflow. Unlike CI (which runs on every push), the release workflow only triggers when you push a version tag like v1.0.0. It builds cross-platform binaries and creates a GitHub Release with downloadable files.

.github/workflows/release.yml (Simplified)
name: Release
on:
  push:
    tags: ['v*']

jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: '1.24'
      # Build for Linux, macOS, and Windows
      # Create GitHub Release with binaries attached
GitHub Release: A tagged version of your project with downloadable binaries. Users install your app by downloading from the release page. Each release has a version number (v1.0.0), release notes, and attached files (binaries, source archives).

To create a release:

Creating a Release
# Tag the commit
git tag v1.0.0

# Push the tag (triggers the release workflow)
git push origin v1.0.0

# GitHub Actions will:
# 1. Build binaries for linux/amd64, darwin/amd64, darwin/arm64, windows/amd64
# 2. Create a GitHub Release
# 3. Attach the binaries as downloadable assets
5

Challenge: Read the Release Workflow

Open release.yml and answer: (1) What event triggers it? (2) What platforms does it build for? (3) How would you create a release? Write the git commands you'd run to release version 1.0.0.

Automated Deployment

The most powerful CI/CD feature: automatic deployment. After tests pass, the deploy job ships your code to production. No SSH, no manual steps, no forgetting to restart the service.

Deploy Job
  deploy:
    needs: [test]
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: '1.24'
      # Run: grit deploy --host DEPLOY_HOST --domain DEPLOY_DOMAIN
      # Uses secrets for sensitive values

Key parts of the deploy job:

  • needs: [test] — only runs after tests pass
  • if: github.ref == 'refs/heads/main' — only deploys from the main branch (not PRs or feature branches)
  • Secrets store sensitive values like server addresses and SSH keys
GitHub Secrets: Encrypted environment variables stored in your repository settings. They're never exposed in logs, even if a workflow prints them. Use secrets for: API keys, deploy credentials, SSH keys, database passwords, and any sensitive configuration.
To add secrets: Repository Settings, then Secrets and variables, then Actions, then New repository secret. Name it something clear like DEPLOY_HOST orSSH_PRIVATE_KEY.
6

Challenge: List Your Deploy Secrets

What secrets would you need for automated deployment? List them all. Think about: server address, domain name, SSH credentials, database connection, and any API keys your app needs in production.

Branch Protection

CI is only useful if you enforce it. Branch protection rules prevent merging code that fails CI. No green checkmark, no merge — period.

To set up branch protection:

  • 1. Go to Repository Settings
  • 2. Click Branches in the sidebar
  • 3. Add a branch protection rule for main
  • 4. Enable "Require status checks to pass before merging"
  • 5. Select the CI jobs (test, test-frontend) as required checks
  • 6. Optionally require pull request reviews

With this setup, the development workflow becomes:

Protected Branch Workflow
# 1. Create a feature branch
git checkout -b feature/add-comments

# 2. Write code, commit, push
git push origin feature/add-comments

# 3. Open a Pull Request on GitHub
# CI runs automatically on the PR

# 4. If CI passes: green checkmark, merge allowed
# If CI fails: red X, merge blocked

# 5. After merge to main: deploy job runs automatically
7

Challenge: Set Up Branch Protection

Set up branch protection on your main branch. Require the CI test job to pass before merging. Create a feature branch, make a change, push it, and open a PR. Does CI run? Can you merge before CI passes?

Environment-Specific Deploys

Production apps need a staging environment — a copy of production where you test changes before they go live. Different branches deploy to different environments:

Multi-Environment Deploy
  deploy-staging:
    needs: [test]
    if: github.ref == 'refs/heads/staging'
    runs-on: ubuntu-latest
    steps:
      # Deploy to staging.myapp.com
      # Uses STAGING_HOST and STAGING_DOMAIN secrets

  deploy-production:
    needs: [test]
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      # Deploy to myapp.com
      # Uses PRODUCTION_HOST and PRODUCTION_DOMAIN secrets

The branching strategy:

  • Feature branches — where you write code. CI runs tests.
  • staging branch — merge features here first. Auto-deploys to staging.myapp.com for testing.
  • main branch — merge from staging when ready. Auto-deploys to myapp.com (production).
Staging should mirror production as closely as possible — same OS, same database engine, same environment variables (with different values). If it works on staging, it should work in production.
8

Challenge: Design Your Branching Strategy

Design a branching strategy for your project. Which branch deploys where? How does code flow from feature to staging to production? Draw the flow: feature branch, then PR to staging, then test, then PR to main, then production deploy.

Notifications

When CI fails at 2 AM, you want to know. When a deployment succeeds, the team should celebrate. Notifications close the feedback loop — the pipeline tells you what happened.

Slack Notification on Failure
  # Add this step at the end of any job
  - name: Notify on failure
    if: failure()
    # Send a POST request to Slack webhook
    # Body: "CI failed on branch main - commit abc123"
    # The webhook URL is stored as a secret

Common notification patterns:

  • Test failure — urgent notification to Slack/Discord with the failing branch and commit
  • Successful deploy — info notification: "v1.2.0 deployed to production"
  • New release — announcement: "Release v1.2.0 published with 5 new features"
  • Deploy failure — critical alert: "Production deploy failed — rollback may be needed"
GitHub also sends email notifications by default. For teams, Slack or Discord webhooks are more visible. Everyone sees the notification in the team channel, not buried in individual inboxes.
9

Challenge: Plan Your Notifications

What notifications would you want for these events? (1) Test failure on a PR, (2) Successful deploy to staging, (3) Successful deploy to production, (4) New GitHub Release created. For each, specify: who should be notified, how (Slack, email, Discord), and what the message should say.

Summary

Here's everything you learned in this course:

  • CI automatically tests code on every push — catching bugs before they reach production
  • CD automatically deploys code after tests pass — no manual SSH deployments
  • Grit scaffolds ci.yml (test on push/PR) and release.yml (build on tag push)
  • GitHub Actions runs jobs on GitHub servers triggered by events (push, PR, tag)
  • YAML workflows define jobs, steps, triggers, and environment configuration
  • Frontend tests run as a separate parallel job alongside Go tests
  • GitHub Secrets store sensitive values like deploy credentials securely
  • Branch protection prevents merging code that fails CI
  • Environment-specific deploys: staging branch to staging server, main to production
  • Notifications keep the team informed of failures, deploys, and releases
10

Challenge: Build the Complete Pipeline (Part 1)

Set up a complete CI workflow with two jobs: Go tests (go test -race ./...) and frontend tests (pnpm test). Push it to GitHub and verify both jobs run on the Actions tab.

11

Challenge: Build the Complete Pipeline (Part 2)

Add a deploy job that runs after both test jobs pass. It should only trigger on pushes to the main branch. Use GitHub Secrets for the deploy host and domain. What secrets did you create?

12

Challenge: Build the Complete Pipeline (Part 3)

Set up the full workflow: branch protection on main requiring CI to pass, a staging branch with auto-deploy to staging, and tag-triggered releases. Test the complete flow: feature branch, then PR, then CI passes, then merge to staging, then staging deploy, then merge to main, then production deploy. This is a professional-grade CI/CD setup.