Comprehensive guide for setting up GitHub Actions, configuring workflows, self-hosted runners, and implementing CI/CD pipelines.
- Introduction
- Getting Started with GitHub Actions
- Workflow Syntax
- Common Actions
- CI/CD Pipelines
- Self-Hosted Runners
- Secrets and Security
- Advanced Features
- Best Practices
- Troubleshooting
GitHub Actions is a CI/CD platform that allows you to automate your build, test, and deployment pipelines. You can create workflows that build and test every pull request or deploy merged code to production.
- Free for public repositories: Unlimited minutes and storage
- Customizable workflows: YAML-based configuration
- Marketplace: Thousands of pre-built actions
- Matrix builds: Test across multiple OS and versions
- Self-hosted runners: Run on your own infrastructure
- Secrets management: Secure storage for sensitive data
- Artifact caching: Speed up builds with dependency caching
| Plan | Free | Pro | Team | Enterprise |
|---|---|---|---|---|
| Minutes/month | 2,000 | 3,000 | 10,000 | 50,000 |
| Storage | 500 MB | 1 GB | 2 GB | 10 GB |
- Create a new repository on GitHub
- GitHub Actions is automatically enabled
- Navigate to repository Settings > Actions > General
- Under "Actions permissions", select:
- Allow all actions and reusable workflows
- Or restrict to specific actions
# .github/workflows/hello-world.yml
name: Hello World
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Run a one-line script
run: echo Hello, world!
- name: Run a multi-line script
run: |
echo Add other actions to build,
echo test, and deploy your project.your-repo/
├── .github/
│ └── workflows/
│ ├── ci.yml
│ ├── cd.yml
│ └── security.yml
├── src/
└── README.md
name: Workflow Name # Workflow name
on: # Triggers
push: # Push events
branches: [main, develop] # Branches to trigger on
pull_request: # Pull request events
branches: [main]
schedule: # Scheduled runs
- cron: '0 0 * * *' # Daily at midnight
workflow_dispatch: # Manual trigger
jobs: # Jobs
job-name: # Job identifier
runs-on: ubuntu-latest # Runner type
steps: # Steps
- name: Step name # Step name
uses: action@v1 # Use an action
- name: Run command # Run shell command
run: echo "Hello"on:
push:
branches: [main, develop]
tags:
- 'v*'
paths:
- 'src/**'
- '.github/workflows/**'on:
pull_request:
types: [opened, synchronize, reopened]
branches: [main]on:
schedule:
- cron: '0 2 * * *' # Daily at 2 AM
- cron: '0 0 * * 0' # Weekly on Sundayon:
workflow_dispatch:
inputs:
environment:
description: 'Deployment environment'
required: true
default: 'staging'
type: choice
options:
- staging
- productionjobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
test:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
deploy:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4jobs:
test-linux:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
test-windows:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
test-macos:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node-version: [14.x, 16.x, 18.x]
exclude:
- os: macos-latest
node-version: 14.x
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}- uses: actions/checkout@v4
with:
fetch-depth: 0 # Full history
lfs: true # Include LFS files
submodules: recursive # Include submodules
token: ${{ secrets.GITHUB_TOKEN }}- uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm' # Cache dependencies- uses: actions/setup-python@v5
with:
python-version: '3.11'
cache: 'pip'- uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
cache: 'maven'- uses: actions/setup-go@v5
with:
go-version: '1.21'
cache: true- uses: actions/setup-dotnet@v4
with:
dotnet-version: '7.x'- name: Cache Node modules
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: my-artifact
path: dist/
- name: Download artifact
uses: actions/download-artifact@v4
with:
name: my-artifact
path: ./dist- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: myuser/myapp:latest
cache-from: type=gha
cache-to: type=gha,mode=maxname: Java CI/CD
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
cache: 'maven'
- name: Build with Maven
run: mvn -B clean package
- name: Run tests
run: mvn test
- name: Generate coverage report
run: mvn jacoco:report
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
files: ./target/site/jacoco/jacoco.xml
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: package
path: target/*.jar
deploy:
needs: build
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Download artifacts
uses: actions/download-artifact@v4
with:
name: package
- name: Set up SSH
uses: webfactory/ssh-agent@v0.9.0
with:
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
- name: Deploy to server
run: |
ssh -o StrictHostKeyChecking=no user@server.com 'cd /app && systemctl stop myapp'
scp target/*.jar user@server.com:/app/
ssh -o StrictHostKeyChecking=no user@server.com 'cd /app && systemctl start myapp'name: Node.js CI/CD
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16.x, 18.x, 20.x]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run linter
run: npm run lint
- name: Run tests
run: npm test
- name: Build application
run: npm run build
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: build-${{ matrix.node-version }}
path: dist/
deploy:
needs: build
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build application
run: npm run build
- name: Deploy to Vercel
uses: amondnet/vercel-action@v25
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
vercel-args: '--prod'name: Python CI/CD
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.9', '3.10', '3.11']
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install pytest pytest-cov flake8
- name: Lint with flake8
run: |
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Test with pytest
run: |
pytest --cov=. --cov-report=xml --cov-report=html
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
files: ./coverage.xml
- name: Build package
run: python -m build
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: dist-${{ matrix.python-version }}
path: dist/
deploy:
needs: build
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install build twine
- name: Build package
run: python -m build
- name: Publish to PyPI
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
run: twine upload dist/*name: Go CI/CD
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
go-version: ['1.20', '1.21', '1.22']
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Go ${{ matrix.go-version }}
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
cache: true
- name: Download dependencies
run: go mod download
- name: Verify dependencies
run: go mod verify
- name: Run go vet
run: go vet ./...
- name: Run tests
run: go test -v -race -coverprofile=coverage.txt -covermode=atomic ./...
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
files: ./coverage.txt
- name: Build application
run: go build -v -o app ./...
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: app-${{ matrix.go-version }}
path: app
deploy:
needs: build
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.21'
- name: Build application
run: go build -v -o app ./...
- name: Build Docker image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: myuser/myapp:latest
registry: docker.io- Go to repository Settings > Actions > Runners
- Click "New self-hosted runner"
- Select OS (Linux, macOS, Windows)
# Create a directory
mkdir actions-runner && cd actions-runner
# Download the latest runner package
curl -o actions-runner-linux-x64-2.311.0.tar.gz -L https://github.com/actions/runner/releases/download/v2.311.0/actions-runner-linux-x64-2.311.0.tar.gz
# Extract the installer
tar xzf ./actions-runner-linux-x64-2.311.0.tar.gz
# Configure the runner
./config.sh --url https://github.com/YOUR_ORG/YOUR_REPO --token YOUR_TOKEN
# Run the runner
./run.sh# Install as service
sudo ./svc.sh install
# Start the service
sudo ./svc.sh start
# Check status
sudo ./svc.sh status# Create a directory
mkdir actions-runner
cd actions-runner
# Download the latest runner package
Invoke-WebRequest -Uri https://github.com/actions/runner/releases/download/v2.311.0/actions-runner-win-x64-2.311.0.zip -OutFile actions-runner-win-x64-2.311.0.zip
# Extract the installer
Add-Type -AssemblyName System.IO.Compression.FileSystem
[System.IO.Compression.ZipFile]::ExtractToDirectory("$PWD\actions-runner-win-x64-2.311.0.zip", "$PWD")
# Configure the runner
.\config.cmd --url https://github.com/YOUR_ORG/YOUR_REPO --token YOUR_TOKEN
# Run the runner
.\run.cmd# Add labels during configuration
./config.sh --url https://github.com/YOUR_ORG/YOUR_REPO \
--token YOUR_TOKEN \
--labels self-hosted,linux,x64,docker
# Or add labels after configuration
# Navigate to: Settings > Actions > Runners > Select runner > Settingsjobs:
build:
runs-on: [self-hosted, linux, x64]
steps:
- uses: actions/checkout@v4
- name: Build
run: make build- Navigate to repository Settings > Secrets and variables > Actions
- Click "New repository secret"
- Add secret name and value
- Click "Add secret"
- Navigate to organization Settings > Secrets > Actions
- Click "New organization secret"
- Add secret name and value
- Select repositories with access
- Click "Add secret"
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy
env:
API_KEY: ${{ secrets.API_KEY }}
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
run: |
echo "Deploying with API key"
# Use secrets in your deployment script# Use encrypted secrets for sensitive data
- name: Decrypt secrets
run: |
echo "${{ secrets.ENCRYPTED_SECRET }}" | base64 -d > secret.txt# .github/workflows/reusable-build.yml
name: Reusable Build
on:
workflow_call:
inputs:
node-version:
required: true
type: string
secrets:
npm-token:
required: true
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
- run: npm ci
env:
NODE_AUTH_TOKEN: ${{ secrets.npm-token }}# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
jobs:
call-build:
uses: ./.github/workflows/reusable-build.yml
with:
node-version: '18'
secrets:
npm-token: ${{ secrets.NPM_TOKEN }}# .github/actions/setup-build/action.yml
name: 'Setup Build Environment'
description: 'Sets up the build environment with common tools'
inputs:
node-version:
description: 'Node.js version'
required: true
default: '18'
runs:
using: "composite"
steps:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
- name: Install dependencies
run: npm ci
shell: bash# Use composite action
steps:
- uses: ./.github/actions/setup-build
with:
node-version: '20'jobs:
deploy-staging:
runs-on: ubuntu-latest
environment:
name: staging
url: https://staging.example.com
steps:
- uses: actions/checkout@v4
- run: deploy-to-staging.sh
deploy-production:
runs-on: ubuntu-latest
environment:
name: production
url: https://example.com
steps:
- uses: actions/checkout@v4
- run: deploy-to-production.sh- Use descriptive names for workflows and jobs
- Group related steps together
- Use conditions for environment-specific steps
- Implement proper error handling
- Use environment variables for configuration
- Use secrets for sensitive data
- Implement proper access controls
- Regular security scanning
- Use minimum required permissions
- Implement proper secrets rotation
- Use caching for dependencies
- Implement proper job dependencies
- Use matrix builds efficiently
- Clean up workspace regularly
- Use self-hosted runners when appropriate
- Workflow syntax errors
- Secret configuration issues
- Permission problems
- Dependency conflicts
- Timeout issues
- Use debug logging
- Check workflow run logs
- Validate workflow syntax
- Test locally with act
- Use workflow visualization tools
