GitHub Actions Fundamentals#
GitHub Actions is CI/CD built into GitHub. Workflows are YAML files in .github/workflows/. They run on GitHub-hosted or self-hosted machines in response to repository events. No external CI server required.
Workflow File Structure#
Every workflow has three levels: workflow (triggers and config), jobs (parallel units of work), and steps (sequential commands within a job).
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.23'
- run: go test ./...
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: golangci/golangci-lint-action@v6Jobs run in parallel by default. Steps within a job run sequentially. Each job gets a fresh runner – no state carries over between jobs unless you explicitly pass it via artifacts or outputs.
Event Triggers#
The on key defines what triggers the workflow.
on:
push:
branches: [main, 'release/**']
paths: ['src/**', 'go.mod']
pull_request:
types: [opened, synchronize, reopened]
schedule:
- cron: '0 6 * * 1' # Every Monday at 6 AM UTC
workflow_dispatch:
inputs:
environment:
description: 'Deploy target'
required: true
type: choice
options: [staging, production]
repository_dispatch:
types: [deploy-trigger]- push/pull_request: the most common triggers. Use
pathsto skip workflows when irrelevant files change. - schedule: cron-based triggers. Runs on the default branch only.
- workflow_dispatch: manual trigger from the GitHub UI or API, with optional input parameters.
- repository_dispatch: webhook-triggered. Call
POST /repos/{owner}/{repo}/dispatcheswith an event type to trigger it from external systems.
Runners#
Runners are the machines that execute jobs.
jobs:
build:
runs-on: ubuntu-latest # GitHub-hosted Linux x86
build-arm:
runs-on: ubuntu-24.04-arm # GitHub-hosted Linux ARM64
build-mac:
runs-on: macos-latest # GitHub-hosted macOS
build-custom:
runs-on: [self-hosted, linux, gpu] # Self-hosted with labelsGitHub-hosted runners are ephemeral – a fresh VM for every job. Self-hosted runners persist, which means faster startup (no VM provisioning) but you must manage cleanup and security.
Step Types: uses vs run#
Steps are either action references (uses) or shell commands (run):
steps:
# Action from the marketplace
- uses: actions/checkout@v4
# Action with inputs
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
# Shell command
- run: npm test
# Multi-line shell command
- name: Build and verify
run: |
npm run build
ls -la dist/Pin actions to a full commit SHA for security in production workflows:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2Tag references (@v4) can be moved by the action author. SHA pinning prevents supply chain attacks.
Passing Data Between Steps#
Steps in the same job share a filesystem but need explicit mechanisms for structured data.
steps:
- name: Get version
id: version
run: |
VERSION=$(cat VERSION)
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Use version
run: echo "Building version ${{ steps.version.outputs.version }}"Write to $GITHUB_OUTPUT to set step outputs. Reference them with steps.<id>.outputs.<name>.
For environment variables shared across steps:
steps:
- name: Set env
run: echo "DEPLOY_ENV=staging" >> "$GITHUB_ENV"
- name: Use env
run: echo "Deploying to $DEPLOY_ENV"To pass data between jobs, use job outputs:
jobs:
prepare:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.ver.outputs.version }}
steps:
- id: ver
run: echo "version=1.2.3" >> "$GITHUB_OUTPUT"
deploy:
needs: prepare
runs-on: ubuntu-latest
steps:
- run: echo "Deploying ${{ needs.prepare.outputs.version }}"Artifacts#
Upload files from one job, download in another:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: make build
- uses: actions/upload-artifact@v4
with:
name: binary
path: dist/myapp
retention-days: 5
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with:
name: binary
- run: ./myapp --versionArtifacts are the only way to pass files between jobs. They are stored by GitHub and count toward your storage quota. Set retention-days to avoid accumulating old artifacts.
Caching#
Cache dependencies to speed up workflows:
- uses: actions/cache@v4
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-The key must change when dependencies change. restore-keys provide fallback – a partial match restores a stale cache, which is still faster than downloading everything from scratch.
Many setup actions have built-in caching:
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm' # Automatically caches node_modulesSecrets and Environment Variables#
jobs:
deploy:
runs-on: ubuntu-latest
environment: production # Uses environment-scoped secrets
steps:
- run: deploy --token "${{ secrets.DEPLOY_TOKEN }}"
env:
AWS_REGION: us-east-1Secrets are configured at the repository or environment level in GitHub settings. They are masked in logs. The built-in GITHUB_TOKEN provides scoped access to the repository’s own API – no PAT required for basic operations.
Set GITHUB_TOKEN permissions explicitly:
permissions:
contents: read
packages: write
pull-requests: writeConcurrency Control#
Prevent duplicate runs and manage deployment queues:
concurrency:
group: deploy-${{ github.ref }}
cancel-in-progress: trueThis cancels any in-progress run for the same branch when a new push arrives. For deployments where you do not want cancellation:
concurrency:
group: deploy-production
cancel-in-progress: false # Queue instead of cancelReusable Workflows#
Define a workflow that other workflows can call:
# .github/workflows/reusable-build.yml
on:
workflow_call:
inputs:
go-version:
required: true
type: string
secrets:
deploy-token:
required: true
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: ${{ inputs.go-version }}
- run: go build ./...Call it from another workflow:
jobs:
call-build:
uses: ./.github/workflows/reusable-build.yml
with:
go-version: '1.23'
secrets:
deploy-token: ${{ secrets.DEPLOY_TOKEN }}Reusable workflows reduce duplication across repositories. They can live in the same repo or a central .github repository.