Phuriwaj

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
TermMeaning
WorkflowAutomated process defined in a .yml file
Trigger (on)What starts the workflow
JobGroup of steps running on the same machine
StepIndividual command or action
ActionReusable unit (marketplace or custom)
RunnerMachine 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 test

2. 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.sh

Add 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 test

Runs 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:latest

6. Scheduled jobs (cron)

on:
  schedule:
    - cron: '0 2 * * *'   # every day at 2am UTC
  workflow_dispatch:         # also allow manual trigger

7. 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-tests

8. 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 test

Setup:

# Repo → Settings → Actions → Runners → New self-hosted runner
./config.sh --url https://github.com/user/repo --token YOUR_TOKEN
./run.sh

Key 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 -d

Key Options / Variants

SyntaxMeaning
on: pushTrigger on push
needs: jobnameWait for another job
if: failure()Run only on failure
${{ secrets.X }}Use a secret
${{ github.ref }}Current branch ref
runs-on: self-hostedUse your own machine
strategy.matrixRun multiple combinations
workflow_dispatchAllow manual trigger

Gotchas

  • workflow_dispatch is required for manual runs from the GitHub UI
  • Matrix jobs run in parallel — add max-parallel to throttle if needed
  • GITHUB_TOKEN is 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: autocodein-progressin-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 autocode triggers anthropics/claude-code-action@v1, which reads the issue, implements the code, and opens a PR. Labels progress autocodein-progressin-review automatically.
  • Z.ai proxy confirmed as the backend: ANTHROPIC_BASE_URL=https://api.z.ai/api/anthropic, secret ANTHROPIC_AUTH_TOKEN. Model routing: glm-4.5-air for haiku calls, glm-5.1 for sonnet/opus — cost naturally optimised by Claude Code’s own model-selection logic.

New things learned:

  • anthropics/claude-code-action@v1 integrates cleanly with GitHub Actions via direct_prompt — no custom shell scripting required.
  • Three labels required before the workflow can run: autocode, in-progress, in-review (create with gh label create).
  • GITHUB_TOKEN (auto-generated) is sufficient for PR creation; only ANTHROPIC_AUTH_TOKEN needs manual setup.

Open questions / next steps:

  • Create GitHub labels: autocode, in-progress, in-review in the target repo
  • Add ANTHROPIC_AUTH_TOKEN secret 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 step

Advantage: 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 → ProjectsNew 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