GitHub Actions
Concepts
graph TD
classDef blue fill:#3498db,stroke:#2980b9,color:#fff
classDef green fill:#2ecc71,stroke:#27ae60,color:#fff
classDef red fill:#e74c3c,stroke:#c0392b,color:#fff
classDef orange fill:#e67e22,stroke:#d35400,color:#fff
classDef purple fill:#9b59b6,stroke:#8e44ad,color:#fff
classDef teal fill:#1abc9c,stroke:#16a085,color:#fff
classDef dark fill:#2c3e50,stroke:#1a252f,color:#fff
classDef yellow fill:#f39c12,stroke:#d68910,color:#000
classDef k8s fill:#326ce5,stroke:#254ea8,color:#fff
classDef aws fill:#ff9900,stroke:#cc7a00,color:#000
TRIGGER["Trigger: push, pull_request, schedule, workflow_dispatch"]:::orange --> WORKFLOW["Workflow (.github/workflows/*.yml)"]:::green
WORKFLOW --> JOB1["Job 1: test (runs-on: ubuntu-latest)"]:::blue
WORKFLOW --> JOB2["Job 2: build (needs: test)"]:::orange
WORKFLOW --> JOB3["Job 3: deploy (needs: build)"]:::green
JOB1 --> STEP1["Step: checkout"]:::blue
JOB1 --> STEP2["Step: run go test"]:::blue
JOB1 --> STEP3["Step: upload coverage"]:::blue
- Workflow — a YAML file in
.github/workflows/. One repo can have many workflows. - Job — a group of steps that run on the same runner. Jobs run in parallel by default;
needs:makes them sequential. - Step — a single task:
uses(an action) orrun(shell command). - Runner — the VM that executes jobs. GitHub-hosted (
ubuntu-latest,macos-latest) or self-hosted.
Full CI Workflow
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
env:
GO_VERSION: "1.23"
ECR_REPO: 123456789.dkr.ecr.us-east-1.amazonaws.com/my-service
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
cache: true # caches go module download cache automatically
- name: Run tests
run: go test -race -coverprofile=coverage.out ./...
- name: Upload coverage
uses: actions/upload-artifact@v4
with:
name: coverage
path: coverage.out
build:
runs-on: ubuntu-latest
needs: test # only runs if test passes
outputs:
image-tag: ${{ steps.meta.outputs.version }}
steps:
- uses: actions/checkout@v4
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.ECR_REPO }}
tags: |
type=sha,prefix=sha-
type=semver,pattern={{version}}
- name: Configure AWS credentials via OIDC
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789:role/github-actions-ecr
aws-region: us-east-1
- name: Login to ECR
uses: aws-actions/amazon-ecr-login@v2
- name: Build and push
uses: docker/build-push-action@v5
with:
push: true
tags: ${{ steps.meta.outputs.tags }}
cache-from: type=gha # GitHub Actions cache for Docker layers
cache-to: type=gha,mode=max
deploy:
runs-on: ubuntu-latest
needs: build
environment: production # requires manual approval if configured in repo settings
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789:role/github-actions-deploy
aws-region: us-east-1
- name: Update ECS service
run: |
aws ecs update-service \
--cluster prod \
--service my-service \
--force-new-deployment
OIDC to AWS (No Long-Lived Keys)
OIDC lets GitHub Actions workflows assume an AWS IAM role without storing AWS access keys as secrets. GitHub mints a short-lived OIDC token per workflow run; AWS STS validates it and returns temporary credentials.
sequenceDiagram
participant GH as GitHub Actions
participant GH_OIDC as GitHub OIDC Provider
participant STS as AWS STS
participant AWS as AWS Services
GH->>GH_OIDC: Request OIDC token for this workflow run
GH_OIDC-->>GH: JWT token (repo, branch, sha, expiry)
GH->>STS: AssumeRoleWithWebIdentity (token + role ARN)
STS->>STS: Validate token against GitHub OIDC endpoint
STS-->>GH: Temporary credentials (15min-1hr)
GH->>AWS: API calls with temporary credentials
IAM role trust policy:
{
"Statement": [{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::123456789:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
},
"StringLike": {
"token.actions.githubusercontent.com:sub": "repo:my-org/my-repo:*"
}
}
}]
}
Matrix Builds
Run the same job across multiple combinations:
jobs:
test:
strategy:
matrix:
go-version: ["1.21", "1.22", "1.23"]
os: [ubuntu-latest, macos-latest]
fail-fast: false # don't cancel other matrix jobs if one fails
runs-on: ${{ matrix.os }}
steps:
- uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
- run: go test ./...
Caching
# Cache Go modules (keyed by go.sum hash)
- uses: actions/cache@v4
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
# Or just use setup-go with cache: true (handles it automatically)
- uses: actions/setup-go@v5
with:
go-version: "1.23"
cache: true
Docker layer caching:
- uses: docker/build-push-action@v5
with:
cache-from: type=gha # restore from GH Actions cache
cache-to: type=gha,mode=max # save all layers (mode=max)
Reusable Workflows
Define a workflow once, call it from many others — like a function call for CI pipelines.
# .github/workflows/reusable-deploy.yml
on:
workflow_call:
inputs:
environment:
required: true
type: string
secrets:
DEPLOY_ROLE_ARN:
required: true
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.DEPLOY_ROLE_ARN }}
aws-region: us-east-1
- run: deploy.sh --env ${{ inputs.environment }}
# .github/workflows/deploy-prod.yml — caller
jobs:
deploy:
uses: ./.github/workflows/reusable-deploy.yml
with:
environment: production
secrets:
DEPLOY_ROLE_ARN: ${{ secrets.PROD_DEPLOY_ROLE_ARN }}
Useful Patterns
# Run only on specific file changes
on:
push:
paths:
- 'src/**'
- 'go.mod'
- '!docs/**' # exclude docs changes
# Concurrency: cancel in-progress runs on same branch
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
# Conditional step
- name: Deploy
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
run: deploy.sh
# Set output from a step
- id: version
run: echo "tag=$(git describe --tags)" >> $GITHUB_OUTPUT
- run: echo "Deploying ${{ steps.version.outputs.tag }}"
# Use GitHub secrets
- run: deploy.sh --token ${{ secrets.DEPLOY_TOKEN }}