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.
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
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.
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 ./...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
-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.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.
# 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
needsto 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 reusable —
actions/checkout@v4is maintained by GitHub
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.
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 testThis 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:
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:e2eneeds: [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.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.
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 attachedTo create 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 assetsChallenge: 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:
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 valuesKey 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
DEPLOY_HOST orSSH_PRIVATE_KEY.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:
# 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 automaticallyChallenge: 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:
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 secretsThe 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).
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.
# 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 secretCommon 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"
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
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.
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?
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.
Enjoying the course?
Help us grow — star us on GitHub, subscribe on YouTube, and follow on LinkedIn.