Master Claude Code for Production Shipping · 45 min · Claude Code · GitHub Actions
Objective
What you're building
A GitHub Actions workflow where Claude Code reads a real failing test, writes a fix, and opens a real pull request — on a sandboxed branch, never pushing directly to main, reviewed by you before merge.
Guardrails first, capability second
This is the section where rushing has real consequences
Everything else in this course — a bad CLAUDE.md, a misconfigured hook, an over-delegated subagent — costs you time to fix. An autonomous CI agent with the ability to push directly to main, or with credentials broader than it needs, can cause real, hard-to-reverse damage. Set up branch protection on main BEFORE wiring Claude Code into CI, not after, and give the CI token the minimum permissions that let it open a PR — nothing more.
Headless invocation
name: Auto-fix failing tests
on:
workflow_dispatch: # manually triggered, not automatic on every push -- start conservative
jobs:
fix:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run tests
id: test
run: npm test || echo "failed=true" >> "$GITHUB_OUTPUT"
- name: Install Claude Code CLI
if: steps.test.outputs.failed == 'true'
run: npm install -g @anthropic-ai/claude-code
- name: Claude Code fix attempt
if: steps.test.outputs.failed == 'true'
run: |
claude -p "Tests are failing. Read the failure output, fix the underlying issue, and stop. Do not modify test files themselves." \
--output-format json > fix_result.json
- name: Open PR with the fix
if: steps.test.outputs.failed == 'true'
run: |
git checkout -b "auto-fix/$(date +%s)"
git add -A
git commit -m "Auto-fix: failing test resolved by Claude Code"
gh pr create --title "Auto-fix: failing test" --body "Opened automatically. Review before merging." --base main
claude -p "..." is the non-interactive (headless) invocation -- Claude Code runs the given prompt to completion and exits, no interactive session, which is what a CI job needs. Notice the CLI install step right before it: ubuntu-latest runners don't come with Claude Code preinstalled, so skipping this step means the fix-attempt step fails immediately with "command not found" -- a gap that only shows up once you actually run the workflow. Also notice this workflow creates a NEW branch and opens a PR rather than committing to main directly -- that's the sandboxing this lesson opened with, not an afterthought. One more real-world gotcha: a brand-new repo has "Allow GitHub Actions to create and approve pull requests" OFF by default (Settings -> Actions -> General) -- without it, gh pr create fails from inside a workflow even with pull-requests: write in the permissions block. Turn it on before your first real run, alongside branch protection on main.
Testing against a real, safe failure
You're about to test this workflow for the first time. What should you deliberately break to trigger it, and why does the choice matter?
The first real run of any automated CI agent should happen against a deliberately controlled, low-stakes failure — not something in a critical path — precisely because you don't yet have evidence this specific pipeline behaves well end to end. This is the same "test in a fresh directory, not your working setup" instinct from Course 02's packaging lesson, applied to something with meaningfully higher stakes than a Python package install.
What you're building today
claude-code-ci.yml: a GitHub Actions workflow triggered manually (not automatically, to start), running Claude Code headlessly against a real failing test, opening a real PR on a sandboxed branch — tested against a deliberately introduced, low-stakes failure, with branch protection on main already in place before this workflow ever runs.