GitHub Actions — Complete Guide
Comprehensive reference for building CI/CD pipelines with GitHub Actions.
Why / When to Use
Use when automating build, test, deploy, or any recurring workflow on a GitHub-hosted repository. Supports GitHub-hosted and self-hosted runners — the latter useful for accessing local LLM proxies (LiteLLM, DeepSeek).
Core Concepts
Repository layout:
.github/
└── workflows/
├── ci.yml ← each .yml file is one workflow
├── deploy.yml
└── release.yml
| Term | Meaning |
|---|---|
| Workflow | Automated process defined in a .yml file |
Trigger (on) | What starts the workflow |
| Job | Group of steps running on the same machine |
| Step | Individual command or action |
| Action | Reusable unit (marketplace or custom) |
| Runner | Machine that executes jobs |
Core Concept / Commands
1. Basic CI — test on every push
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm install
- run: npm test2. Multiple jobs with dependencies
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm install && npm test
build:
runs-on: ubuntu-latest
needs: test # only runs if test passes
steps:
- uses: actions/checkout@v4
- run: npm run build
deploy:
runs-on: ubuntu-latest
needs: build # only runs if build passes
steps:
- run: echo "Deploying..."3. Using secrets
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy
env:
API_KEY: ${{ secrets.API_KEY }}
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
run: ./deploy.shAdd secrets: Repo → Settings → Secrets and variables → Actions
4. Matrix strategy — test multiple versions
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node: [18, 20, 22]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- run: npm install && npm testRuns 9 jobs (3 OS × 3 Node versions) in parallel.
5. Docker build and push
jobs:
docker:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
- uses: docker/build-push-action@v5
with:
push: true
tags: myuser/myapp:latest6. Scheduled jobs (cron)
on:
schedule:
- cron: '0 2 * * *' # every day at 2am UTC
workflow_dispatch: # also allow manual trigger7. Conditional steps
steps:
- run: npm test
- name: Notify on failure
if: failure()
run: curl -X POST ${{ secrets.SLACK_WEBHOOK }} -d '{"text":"Build failed!"}'
- name: Deploy only on main
if: github.ref == 'refs/heads/main'
run: npm run deploy
- name: Skip on draft PR
if: github.event.pull_request.draft == false
run: npm run heavy-tests8. Reusable workflow
# .github/workflows/reusable-test.yml
on:
workflow_call:
inputs:
node-version:
required: true
type: string
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# Caller workflow
jobs:
call-test:
uses: ./.github/workflows/reusable-test.yml
with:
node-version: '20'9. Self-hosted runner
jobs:
build:
runs-on: self-hosted # uses your own machine
steps:
- uses: actions/checkout@v4
- run: npm install && npm testSetup:
# Repo → Settings → Actions → Runners → New self-hosted runner
./config.sh --url https://github.com/user/repo --token YOUR_TOKEN
./run.shKey use case: self-hosted runner can call a local LiteLLM/DeepSeek proxy directly, enabling AI-powered CI steps without egressing tokens externally.
10. Full real-world pipeline
name: Full Pipeline
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
NODE_VERSION: '20'
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: ${{ env.NODE_VERSION }}
- run: npm ci && npm run lint
test:
runs-on: ubuntu-latest
needs: lint
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- run: npm ci && npm test
build-docker:
runs-on: ubuntu-latest
needs: test
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v5
with:
push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
deploy:
runs-on: self-hosted
needs: build-docker
if: github.ref == 'refs/heads/main'
steps:
- name: Pull and restart
run: |
docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
docker compose up -dKey Options / Variants
| Syntax | Meaning |
|---|---|
on: push | Trigger on push |
needs: jobname | Wait for another job |
if: failure() | Run only on failure |
${{ secrets.X }} | Use a secret |
${{ github.ref }} | Current branch ref |
runs-on: self-hosted | Use your own machine |
strategy.matrix | Run multiple combinations |
workflow_dispatch | Allow manual trigger |
Gotchas
workflow_dispatchis required for manual runs from the GitHub UI- Matrix jobs run in parallel — add
max-parallelto throttle if needed GITHUB_TOKENis auto-generated per workflow run; no manual secret needed for GHCR pushes- Self-hosted runners persist state between runs (node_modules, Docker cache) — useful for speed but can cause stale-cache bugs
Source
Conversation “Multiple GitHub repositories in one workspace” — 2026-05-19 (Claude Code project)
Updates — 2026-05-26
Auto-implement GitHub Issues via Claude Code Action (Z.ai proxy)
Trigger: issue labeled autocode → Claude Code reads the issue, writes the code, opens a PR. Label progression: autocode → in-progress → in-review.
Add secret: ANTHROPIC_AUTH_TOKEN = <your Z.ai key>
# .github/workflows/autocode.yml
name: Claude Autocode
on:
issues:
types: [labeled]
jobs:
implement:
if: github.event.label.name == 'autocode'
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
issues: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Move → In Progress
uses: actions/github-script@v7
with:
script: |
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
name: 'autocode'
}).catch(() => {});
await github.rest.issues.addLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
name: 'in-progress'
});
- name: Claude implements the issue
uses: anthropics/claude-code-action@v1
env:
ANTHROPIC_BASE_URL: https://api.z.ai/api/anthropic
ANTHROPIC_AUTH_TOKEN: ${{ secrets.ANTHROPIC_AUTH_TOKEN }}
ANTHROPIC_DEFAULT_HAIKU_MODEL: glm-4.5-air
ANTHROPIC_DEFAULT_SONNET_MODEL: glm-5.1
ANTHROPIC_DEFAULT_OPUS_MODEL: glm-5.1
API_TIMEOUT_MS: "3000000"
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1"
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
direct_prompt: |
Implement GitHub Issue #${{ github.event.issue.number }}:
Title: ${{ github.event.issue.title }}
${{ github.event.issue.body }}
Instructions:
- Study existing codebase patterns first
- Implement exactly what the issue describes
- Write tests if the project already has tests
- Open a pull request when done
- Reference the issue in the PR (closes #${{ github.event.issue.number }})
- name: Move → In Review
if: success()
uses: actions/github-script@v7
with:
script: |
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
name: 'in-progress'
}).catch(() => {});
await github.rest.issues.addLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
name: 'in-review'
});Create the required labels:
gh label create "autocode" --color "0075ca"
gh label create "in-progress" --color "e4e669"
gh label create "in-review" --color "d93f0b"Model cost note: Z.ai uses glm-4.5-air for haiku (small background tasks) and glm-5.1 for sonnet/opus (main implementation). Claude Code auto-selects haiku for cheap ops and sonnet for heavy ones — cost is naturally optimised.
Source: Conversation “GitHub project automation with Claude” — 2026-05-26
Weekly Summary — 2026-W22
Appeared: Tue 26 May Key developments this week:
- Wired up a fully automated issue-to-PR pipeline: labelling a GitHub issue
autocodetriggersanthropics/claude-code-action@v1, which reads the issue, implements the code, and opens a PR. Labels progressautocode→in-progress→in-reviewautomatically. - Z.ai proxy confirmed as the backend:
ANTHROPIC_BASE_URL=https://api.z.ai/api/anthropic, secretANTHROPIC_AUTH_TOKEN. Model routing:glm-4.5-airfor haiku calls,glm-5.1for sonnet/opus — cost naturally optimised by Claude Code’s own model-selection logic.
New things learned:
anthropics/claude-code-action@v1integrates cleanly with GitHub Actions viadirect_prompt— no custom shell scripting required.- Three labels required before the workflow can run:
autocode,in-progress,in-review(create withgh label create). GITHUB_TOKEN(auto-generated) is sufficient for PR creation; onlyANTHROPIC_AUTH_TOKENneeds manual setup.
Open questions / next steps:
- Create GitHub labels:
autocode,in-progress,in-reviewin the target repo - Add
ANTHROPIC_AUTH_TOKENsecret to GitHub repo settings
Updates — 2026-05-27
Passing Data Between Steps with GITHUB_OUTPUT
Each step can export named values to subsequent steps using the $GITHUB_OUTPUT file:
steps:
- name: Step 1 — compute branch name
id: get_data
run: echo "branch=feature/issue-${{ github.event.issue.number }}" >> $GITHUB_OUTPUT
- name: Step 2 — use it
run: python scripts/claude_coder.py
env:
BRANCH: ${{ steps.get_data.outputs.branch }}
ISSUE_TITLE: ${{ github.event.issue.title }}
ISSUE_BODY: ${{ github.event.issue.body }}Inside the Python script:
import os
branch = os.environ["BRANCH"]
title = os.environ["ISSUE_TITLE"]Pattern: Use id: on the producing step, then reference steps.<id>.outputs.<key> in later steps.
Replacing curl Webhooks with Python for Logic Before Sending
Instead of a raw curl call, commit a Python script that can add conditional logic, API calls, and enriched payloads before firing the webhook:
- name: Run test trigger with logic
env:
WEBHOOK_URL: ${{ secrets.TEST_WEBHOOK_URL }}
ISSUE_NUMBER: ${{ github.event.issue.number }}
BRANCH: feature/issue-${{ github.event.issue.number }}
run: python scripts/trigger_tests.py# scripts/trigger_tests.py
import os, requests
branch = os.environ["BRANCH"]
issue_number = os.environ["ISSUE_NUMBER"]
webhook_url = os.environ["WEBHOOK_URL"]
# Conditional logic before firing
if "auth" in branch:
test_suite = "auth_tests"
elif "ui" in branch:
test_suite = "ui_tests"
else:
test_suite = "full_suite"
payload = {
"branch": branch,
"issue": issue_number,
"suite": test_suite,
"env": "staging",
"notify": "slack"
}
response = requests.post(webhook_url, json=payload)
if response.status_code != 200:
print(f"Webhook failed: {response.status_code}")
exit(1) # fail the GitHub Action stepAdvantage: Full Python power — conditionals, retries, reading config files, calling internal APIs — all before the webhook fires. The script lives in scripts/ in the same repo, checked out automatically by actions/checkout@v4.
GitHub Projects V2 (Built-in Kanban)
GitHub has a native project board — no external tool needed for basic kanban:
- Repo → Projects → New Project → Board view
- Default columns: Todo / In Progress / Done
- Add custom columns: Dev, Test, Review, Done
- Built-in automation rules: “When PR merged → move to Done”, “When issue closed → move to Done”
Limit: Native automation can only react to GitHub events. For custom logic (calling Claude, running tests, routing by label) you still need a GitHub Actions workflow on top.
Source: Conversation “Auto - GitHub Action” — 2026-05-27