- Published on
GitHub Actions CI/CD Complete Guide: Go Builds, Branch Strategies, and Auto-Deploy
- Authors

- Name
- Youngju Kim
- @fjvbn20031
- Part 1: GitHub Actions Architecture
- Part 2: Go Project CI/CD
- Part 3: Branch Strategies
- Part 4: Full Pipeline -- Go Microservice
- Part 5: Performance and Cost Optimization
- Quiz
- References
Part 1: GitHub Actions Architecture
1.1 Core Concepts
GitHub Actions is a CI/CD platform deeply integrated into GitHub that lets you automate build, test, and deployment workflows directly from your repository. Unlike external CI tools, it has native access to pull requests, issues, releases, and the entire GitHub ecosystem.
The architecture is built around five fundamental primitives:
- Workflow -- A YAML file under
.github/workflows/that defines an automated process - Event -- A trigger that starts a workflow (push, pull_request, schedule, workflow_dispatch, etc.)
- Job -- A set of steps that execute on the same runner, in parallel by default
- Step -- An individual task within a job, either a shell command or an action
- Runner -- The machine (virtual or physical) where a job executes
# .github/workflows/ci.yml
name: CI Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
concurrency:
group: ci-BRANCH_REF
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- name: Run tests
run: echo "Running tests..."
1.2 Event Types and Triggers
GitHub Actions supports over 35 event types. The most commonly used:
| Event | Description | Use Case |
|---|---|---|
| push | Code pushed to branch | CI builds on main/develop |
| pull_request | PR opened/updated/closed | Code review automation |
| schedule | Cron-based trigger | Nightly builds, cleanup |
| workflow_dispatch | Manual trigger | On-demand deploys |
| release | Release published | Production deployments |
| repository_dispatch | External webhook | Cross-repo triggers |
| workflow_call | Called by another workflow | Reusable workflows |
Filtering events with paths and branches:
on:
push:
branches:
- main
- 'release/**'
paths:
- 'src/**'
- 'go.mod'
- 'go.sum'
paths-ignore:
- '**.md'
- 'docs/**'
Schedule with cron expressions:
on:
schedule:
# Run at 2:00 AM UTC on weekdays
- cron: '0 2 * * 1-5'
1.3 Concurrency Control
Concurrency groups prevent duplicate workflow runs, saving compute resources:
concurrency:
group: deploy-production
cancel-in-progress: false # Don't cancel in-progress deploys
# For PR builds, cancel previous runs on the same PR
concurrency:
group: pr-build-PR_NUMBER
cancel-in-progress: true
1.4 Runner Types
| Runner | vCPU | RAM | Storage | Cost (min) |
|---|---|---|---|---|
| ubuntu-latest | 4 | 16 GB | 14 GB SSD | $0.008 |
| ubuntu-latest-xl (4x) | 16 | 64 GB | 150 GB SSD | $0.032 |
| macos-latest | 3 (M1) | 7 GB | 14 GB SSD | $0.08 |
| windows-latest | 2 | 7 GB | 14 GB SSD | $0.016 |
| self-hosted | Custom | Custom | Custom | Free (your infra) |
1.5 Top 20 Essential Actions
Here are the most useful community and official actions:
# 1. Checkout code
- uses: actions/checkout@v4
# 2. Setup Go
- uses: actions/setup-go@v5
with:
go-version: '1.22'
cache: true
# 3. Cache dependencies
- uses: actions/cache@v4
with:
path: ~/go/pkg/mod
key: go-mod-HASH_OF_GO_SUM
# 4. Upload artifact
- uses: actions/upload-artifact@v4
with:
name: binary
path: ./bin/
# 5. Download artifact
- uses: actions/download-artifact@v4
# 6. Docker build and push
- uses: docker/build-push-action@v6
with:
push: true
tags: myapp:latest
# 7. Login to container registry
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: GITHUB_ACTOR
password: GITHUB_TOKEN_SECRET
# 8. Setup Docker Buildx
- uses: docker/setup-buildx-action@v3
# 9. GitHub release
- uses: softprops/action-gh-release@v2
# 10. Slack notification
- uses: slackapi/slack-github-action@v2
# 11. Setup Node.js
- uses: actions/setup-node@v4
# 12. Setup Python
- uses: actions/setup-python@v5
# 13. golangci-lint
- uses: golangci/golangci-lint-action@v6
# 14. CodeQL analysis
- uses: github/codeql-action/analyze@v3
# 15. Deploy to K8s
- uses: azure/k8s-deploy@v5
# 16. AWS credentials
- uses: aws-actions/configure-aws-credentials@v4
# 17. GCP auth
- uses: google-github-actions/auth@v2
# 18. Terraform
- uses: hashicorp/setup-terraform@v3
# 19. Create PR comment
- uses: peter-evans/create-or-update-comment@v4
# 20. Label PR
- uses: actions/labeler@v5
1.6 Secrets Management and OIDC
Repository and environment secrets:
Secrets are encrypted and only exposed to workflows. They are masked in logs automatically.
jobs:
deploy:
environment: production
steps:
- name: Deploy
env:
API_KEY: SECRETS_API_KEY_REF
DB_PASSWORD: SECRETS_DB_PASSWORD_REF
run: ./deploy.sh
Note: In the YAML above, SECRETS_API_KEY_REF and SECRETS_DB_PASSWORD_REF represent references to GitHub secrets configured in repository settings.
OIDC for cloud provider authentication (no static credentials):
jobs:
deploy-aws:
permissions:
id-token: write
contents: read
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/github-actions
aws-region: us-east-1
OIDC eliminates long-lived credentials by establishing a trust relationship between GitHub and your cloud provider. The workflow requests a short-lived token that is automatically scoped and rotated.
1.7 Environments and Deployment Protection
Environments allow you to define protection rules for deployments:
jobs:
deploy-staging:
environment: staging
runs-on: ubuntu-latest
steps:
- run: ./deploy.sh staging
deploy-production:
needs: deploy-staging
environment:
name: production
url: https://myapp.example.com
runs-on: ubuntu-latest
steps:
- run: ./deploy.sh production
Environment protection rules include:
- Required reviewers (up to 6 people)
- Wait timer (delay before deployment proceeds)
- Branch restrictions (only main can deploy to production)
- Custom deployment branch policies
Part 2: Go Project CI/CD
2.1 Go Build Fundamentals
Go compiles to static binaries with no runtime dependencies, making it ideal for containerized deployments. Key build flags for production:
# Production build with all optimizations
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build \
-ldflags="-s -w -X main.version=1.2.3 -X main.commit=abc123 -X main.buildTime=2026-03-23T10:00:00Z" \
-trimpath \
-o ./bin/myapp \
./cmd/server/
Build flags explained:
| Flag | Purpose |
|---|---|
| CGO_ENABLED=0 | Disable cgo for fully static binary |
| -ldflags="-s -w" | Strip debug info, reduce binary size ~30% |
| -ldflags="-X main.version=..." | Inject version info at build time |
| -trimpath | Remove local filesystem paths from binary |
| -race | Enable race detector (testing only, not production) |
| -cover | Build with coverage instrumentation |
2.2 Cross-Compilation Matrix
Go excels at cross-compilation. A single CI job can build for multiple platforms:
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
goos: [linux, darwin, windows]
goarch: [amd64, arm64]
exclude:
- goos: windows
goarch: arm64
fail-fast: false
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.22'
- name: Build
env:
GOOS: matrix-goos-value
GOARCH: matrix-goarch-value
run: |
BINARY_NAME="myapp-matrix-goos-value-matrix-goarch-value"
if [ "matrix-goos-value" = "windows" ]; then
BINARY_NAME="myapp-matrix-goos-value-matrix-goarch-value.exe"
fi
go build -ldflags="-s -w" -trimpath -o "./dist/BINARY_NAME_VAR" ./cmd/server/
- uses: actions/upload-artifact@v4
with:
name: binary-matrix-goos-value-matrix-goarch-value
path: ./dist/
Note: In the matrix build above, matrix-goos-value and matrix-goarch-value represent the dynamically resolved values from the strategy matrix configuration.
2.3 Testing Strategy
A comprehensive Go testing pipeline includes unit tests, integration tests, and benchmarks:
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: testdb
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.22'
- name: Unit tests with coverage
run: |
go test -v -race -coverprofile=coverage.out -covermode=atomic ./...
go tool cover -func=coverage.out
- name: Integration tests
env:
DATABASE_URL: postgres://test:test@localhost:5432/testdb?sslmode=disable
run: |
go test -v -tags=integration -count=1 ./tests/integration/...
- name: Benchmark tests
run: |
go test -bench=. -benchmem -run=^$ ./... | tee benchmark.txt
- name: Upload coverage
uses: codecov/codecov-action@v4
with:
files: ./coverage.out
token: CODECOV_TOKEN_REF
2.4 Linting and Static Analysis
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.22'
- name: golangci-lint
uses: golangci/golangci-lint-action@v6
with:
version: v1.57
args: --timeout=5m
- name: go vet
run: go vet ./...
- name: staticcheck
uses: dominikh/staticcheck-action@v1
with:
version: '2024.1'
- name: govulncheck
run: |
go install golang.org/x/vuln/cmd/govulncheck@latest
govulncheck ./...
golangci-lint configuration (.golangci.yml):
run:
timeout: 5m
go: '1.22'
linters:
enable:
- errcheck
- gosimple
- govet
- ineffassign
- staticcheck
- unused
- bodyclose
- gocritic
- gofumpt
- gosec
- misspell
- prealloc
- revive
- unconvert
linters-settings:
gocritic:
enabled-checks:
- nestingReduce
- truncateCmp
- unnamedResult
gosec:
excludes:
- G104 # Unhandled errors
revive:
rules:
- name: unexported-return
disabled: true
issues:
exclude-rules:
- path: _test\.go
linters:
- gosec
- errcheck
2.5 Caching Strategies for Go
Effective caching can reduce Go build times by 60-80%:
steps:
- uses: actions/setup-go@v5
with:
go-version: '1.22'
cache: true # Built-in module cache
# Additional build cache for faster compilation
- uses: actions/cache@v4
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: go-build-OS-HASH_OF_GO_SUM
restore-keys: |
go-build-OS-
Cache size comparison:
| Cache Type | Typical Size | Time Saved |
|---|---|---|
| Go module cache | 100-500 MB | 30-60s |
| Go build cache | 200-800 MB | 45-120s |
| Docker layer cache | 500 MB-2 GB | 60-300s |
| golangci-lint cache | 50-200 MB | 20-40s |
2.6 Multi-Stage Docker Build
# Stage 1: Build
FROM golang:1.22-alpine AS builder
RUN apk add --no-cache git ca-certificates tzdata
WORKDIR /app
# Cache dependencies
COPY go.mod go.sum ./
RUN go mod download && go mod verify
# Build
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build \
-ldflags="-s -w -X main.version=VERSION_ARG -X main.commit=COMMIT_ARG" \
-trimpath \
-o /app/server \
./cmd/server/
# Stage 2: Runtime
FROM scratch
COPY /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY /usr/share/zoneinfo /usr/share/zoneinfo
COPY /app/server /server
EXPOSE 8080
ENTRYPOINT ["/server"]
Note: In the Dockerfile above, VERSION_ARG and COMMIT_ARG are placeholder values to be replaced by your CI pipeline with the actual version and commit hash.
Docker build workflow:
jobs:
docker:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: GITHUB_ACTOR_REF
password: GITHUB_TOKEN_REF
- uses: docker/metadata-action@v5
id: meta
with:
images: ghcr.io/OWNER/myapp
tags: |
type=sha
type=ref,event=branch
type=semver,pattern=v{{version}}
type=semver,pattern=v{{major}}.{{minor}}
- uses: docker/build-push-action@v6
with:
context: .
push: true
tags: steps-meta-outputs-tags
labels: steps-meta-outputs-labels
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
VERSION=v1.2.3
COMMIT=GITHUB_SHA_REF
2.7 GoReleaser Integration
GoReleaser automates the entire release process: building, packaging, and publishing.
# .goreleaser.yml
version: 2
before:
hooks:
- go mod tidy
- go generate ./...
builds:
- id: server
main: ./cmd/server/
binary: myapp
env:
- CGO_ENABLED=0
goos:
- linux
- darwin
- windows
goarch:
- amd64
- arm64
ldflags:
- -s -w
- -X main.version={{.Version}}
- -X main.commit={{.Commit}}
- -X main.date={{.Date}}
archives:
- format: tar.gz
name_template: 'myapp_{{.Version}}_{{.Os}}_{{.Arch}}'
format_overrides:
- goos: windows
format: zip
dockers:
- image_templates:
- 'ghcr.io/OWNER/myapp:{{.Version}}'
- 'ghcr.io/OWNER/myapp:latest'
dockerfile: Dockerfile.goreleaser
build_flag_templates:
- '--label=org.opencontainers.image.version={{.Version}}'
changelog:
sort: asc
filters:
exclude:
- '^docs:'
- '^test:'
- '^ci:'
release:
github:
owner: myorg
name: myapp
Workflow for GoReleaser:
name: Release
on:
push:
tags:
- 'v*'
permissions:
contents: write
packages: write
jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-go@v5
with:
go-version: '1.22'
- uses: goreleaser/goreleaser-action@v6
with:
version: '~> v2'
args: release --clean
env:
GITHUB_TOKEN: GITHUB_TOKEN_SECRET_REF
Part 3: Branch Strategies
3.1 Git Flow
Git Flow is a comprehensive branching model well-suited for projects with scheduled releases:
Branches:
main-- Production-ready code, tagged with version numbersdevelop-- Integration branch for featuresfeature/xxx-- Short-lived branches for new featuresrelease/x.y.z-- Release preparation and stabilizationhotfix/xxx-- Emergency production fixes
Flow:
- Create feature branch from develop
- Develop and test the feature
- Merge feature into develop via PR
- Create release branch from develop
- Stabilize, bump version, merge to main AND develop
- Tag the main branch with version
- For hotfixes: branch from main, fix, merge to main AND develop
Pros: Clear structure, parallel development, release management Cons: Complex, many branches, slow for fast-moving projects
3.2 GitHub Flow
GitHub Flow is a simplified model focused on continuous delivery:
Branches:
main-- Always deployablefeature/xxx-- All work happens in feature branches
Flow:
- Branch from main
- Add commits
- Open a Pull Request
- Code review and discussion
- Deploy and test (optionally to staging)
- Merge to main
Pros: Simple, fast, encourages small PRs Cons: No release management, everything goes to main
3.3 Trunk-Based Development
Trunk-Based Development minimizes branch lifetime and emphasizes continuous integration:
Branches:
main(trunk) -- Everyone commits here, always releasable- Short-lived feature branches (less than 1-2 days)
- Optional release branches (for cherry-picking fixes)
Flow:
- Pull latest from main
- Make small, incremental changes
- Run all tests locally
- Push directly to main (or very short-lived PR)
- CI validates immediately
- Deploy continuously via feature flags
Pros: Fastest feedback, minimal merge conflicts, simplest model Cons: Requires strong CI, feature flags, disciplined team
3.4 Strategy Comparison
| Aspect | Git Flow | GitHub Flow | Trunk-Based |
|---|---|---|---|
| Complexity | High | Low | Very Low |
| Branch lifetime | Days to weeks | Hours to days | Hours |
| Release cadence | Scheduled | Continuous | Continuous |
| Team size | Large | Small to medium | Any size |
| CI/CD maturity required | Low | Medium | High |
| Feature flags needed | No | Optional | Yes |
| Merge conflicts | Frequent | Occasional | Rare |
| Best for | Enterprise, versioned | SaaS, web apps | High-performance |
| Deploy frequency | Weekly/monthly | Daily | Multiple per day |
3.5 Branch Protection Rules
# Branch protection via GitHub API / Settings
# Main branch protection
branches:
main:
protection:
required_status_checks:
strict: true
contexts:
- 'ci/lint'
- 'ci/test'
- 'ci/build'
required_pull_request_reviews:
required_approving_review_count: 2
dismiss_stale_reviews: true
require_code_owner_reviews: true
restrictions:
users: []
teams: ['core-maintainers']
enforce_admins: true
required_linear_history: true
allow_force_pushes: false
allow_deletions: false
3.6 CODEOWNERS
The CODEOWNERS file defines who is automatically requested for review:
# .github/CODEOWNERS
# Default owners for everything
* @myorg/backend-team
# Go source code
/cmd/ @myorg/go-team
/internal/ @myorg/go-team
/pkg/ @myorg/go-team
# Infrastructure
/.github/ @myorg/devops-team
/deploy/ @myorg/devops-team
/terraform/ @myorg/devops-team
Dockerfile @myorg/devops-team
# Documentation
/docs/ @myorg/docs-team
*.md @myorg/docs-team
# API definitions
/api/ @myorg/api-team @myorg/backend-team
3.7 Repository Rulesets
Rulesets (introduced in 2023) provide more flexible branch protection than traditional branch protection rules:
- Apply to multiple branches/tags via pattern matching
- Layered rules (organization + repository level)
- Bypass permissions for specific roles
- Tag protection
- Push restrictions with verified signatures
- Metadata restrictions (commit message format, author email)
Ruleset configuration example:
Name: production-protection
Target: branches matching "main" and "release/**"
Rules:
- Require pull request before merge (2 approvals)
- Require status checks: lint, test, build
- Require signed commits
- Block force pushes
- Require linear history
Bypass: Repository admins during emergencies
Part 4: Full Pipeline -- Go Microservice
4.1 Project Structure
myapp/
cmd/
server/
main.go
internal/
handler/
service/
repository/
pkg/
middleware/
api/
openapi.yaml
deploy/
k8s/
deployment.yaml
service.yaml
ingress.yaml
docker/
Dockerfile
.github/
workflows/
ci.yml
deploy.yml
release.yml
CODEOWNERS
.golangci.yml
.goreleaser.yml
go.mod
go.sum
Makefile
4.2 Complete CI Workflow
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
paths-ignore:
- '**.md'
- 'docs/**'
pull_request:
branches: [main]
concurrency:
group: ci-REF_NAME
cancel-in-progress: true
env:
GO_VERSION: '1.22'
GOLANGCI_LINT_VERSION: v1.57
REGISTRY: ghcr.io
IMAGE_NAME: myorg/myapp
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: GO_VERSION_ENV
- uses: golangci/golangci-lint-action@v6
with:
version: GOLANGCI_LINT_VERSION_ENV
- name: Check formatting
run: |
if [ -n "$(gofmt -l .)" ]; then
echo "Files not formatted:"
gofmt -l .
exit 1
fi
- name: Verify dependencies
run: |
go mod verify
go mod tidy
git diff --exit-code go.mod go.sum
test:
name: Test
runs-on: ubuntu-latest
needs: lint
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: testdb
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis:7-alpine
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: GO_VERSION_ENV
- name: Unit tests
run: |
go test -v -race -count=1 \
-coverprofile=coverage.out \
-covermode=atomic \
./...
- name: Integration tests
env:
DATABASE_URL: postgres://test:test@localhost:5432/testdb?sslmode=disable
REDIS_URL: redis://localhost:6379
run: |
go test -v -tags=integration -count=1 ./tests/integration/...
- name: Coverage report
run: |
COVERAGE=$(go tool cover -func=coverage.out | grep total | awk '{print $3}')
echo "Total coverage: COVERAGE_VAR"
# Fail if coverage drops below threshold
COVERAGE_NUM=$(echo "COVERAGE_VAR" | tr -d '%')
if (( $(echo "COVERAGE_NUM_VAR < 70" | bc -l) )); then
echo "Coverage below 70% threshold"
exit 1
fi
- uses: codecov/codecov-action@v4
with:
files: ./coverage.out
security:
name: Security Scan
runs-on: ubuntu-latest
needs: lint
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: GO_VERSION_ENV
- name: Vulnerability check
run: |
go install golang.org/x/vuln/cmd/govulncheck@latest
govulncheck ./...
- name: License check
run: |
go install github.com/google/go-licenses@latest
go-licenses check ./...
build:
name: Build
runs-on: ubuntu-latest
needs: [test, security]
outputs:
image-tag: steps-meta-outputs-version
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: GITHUB_ACTOR_REF
password: GITHUB_TOKEN_REF
- uses: docker/metadata-action@v5
id: meta
with:
images: REGISTRY_ENV/IMAGE_NAME_ENV
tags: |
type=sha,prefix=
type=ref,event=branch
type=ref,event=pr
- uses: docker/build-push-action@v6
with:
context: .
file: ./deploy/docker/Dockerfile
push: PUSH_CONDITION
tags: steps-meta-outputs-tags
labels: steps-meta-outputs-labels
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
VERSION=GITHUB_SHA_SHORT
COMMIT=GITHUB_SHA_REF
Note: References like GO_VERSION_ENV represent environment variable references, and PUSH_CONDITION represents conditional logic (e.g., push only on main branch, not on PRs).
4.3 Deployment Workflow
# .github/workflows/deploy.yml
name: Deploy
on:
workflow_run:
workflows: ['CI']
branches: [main]
types: [completed]
concurrency:
group: deploy-ENVIRONMENT
cancel-in-progress: false
jobs:
deploy-staging:
if: github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
environment:
name: staging
url: https://staging.myapp.example.com
steps:
- uses: actions/checkout@v4
- name: Configure kubectl
uses: azure/setup-kubectl@v4
- name: Set K8s context
uses: azure/k8s-set-context@v4
with:
method: kubeconfig
kubeconfig: SECRETS_KUBECONFIG_STAGING_REF
- name: Deploy to staging
run: |
kubectl set image deployment/myapp \
myapp=ghcr.io/myorg/myapp:COMMIT_SHA \
-n staging
kubectl rollout status deployment/myapp -n staging --timeout=300s
- name: Run smoke tests
run: |
sleep 10
curl -sf https://staging.myapp.example.com/healthz || exit 1
deploy-production:
needs: deploy-staging
runs-on: ubuntu-latest
environment:
name: production
url: https://myapp.example.com
steps:
- uses: actions/checkout@v4
- name: Configure kubectl
uses: azure/setup-kubectl@v4
- name: Set K8s context
uses: azure/k8s-set-context@v4
with:
method: kubeconfig
kubeconfig: SECRETS_KUBECONFIG_PRODUCTION_REF
- name: Deploy to production
run: |
kubectl set image deployment/myapp \
myapp=ghcr.io/myorg/myapp:COMMIT_SHA \
-n production
kubectl rollout status deployment/myapp -n production --timeout=300s
- name: Verify deployment
run: |
curl -sf https://myapp.example.com/healthz || exit 1
- name: Notify on failure
if: failure()
uses: slackapi/slack-github-action@v2
with:
webhook: SECRETS_SLACK_WEBHOOK_REF
webhook-type: incoming-webhook
payload: |
{
"text": "Production deployment FAILED for commit COMMIT_SHA_SHORT"
}
4.4 Kubernetes Manifests
# deploy/k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
labels:
app: myapp
spec:
replicas: 3
selector:
matchLabels:
app: myapp
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
metadata:
labels:
app: myapp
spec:
containers:
- name: myapp
image: ghcr.io/myorg/myapp:latest
ports:
- containerPort: 8080
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
readinessProbe:
httpGet:
path: /readyz
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
env:
- name: LOG_LEVEL
value: 'info'
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: myapp-secrets
key: database-url
# deploy/k8s/service.yaml
apiVersion: v1
kind: Service
metadata:
name: myapp
spec:
type: ClusterIP
selector:
app: myapp
ports:
- port: 80
targetPort: 8080
protocol: TCP
4.5 Reusable Workflows
Create reusable workflows to share CI/CD logic across repositories:
# .github/workflows/reusable-go-ci.yml
name: Reusable Go CI
on:
workflow_call:
inputs:
go-version:
description: 'Go version to use'
required: false
type: string
default: '1.22'
coverage-threshold:
description: 'Minimum coverage percentage'
required: false
type: number
default: 70
secrets:
CODECOV_TOKEN:
required: false
jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: inputs-go-version
- run: go test -race -coverprofile=coverage.out ./...
- run: go vet ./...
Calling the reusable workflow:
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
go-ci:
uses: myorg/.github/.github/workflows/reusable-go-ci.yml@main
with:
go-version: '1.22'
coverage-threshold: 80
secrets:
CODECOV_TOKEN: SECRETS_CODECOV_TOKEN_REF
4.6 Path-Based Triggering
For monorepos, trigger workflows only when relevant files change:
on:
push:
paths:
- 'services/api/**'
- 'shared/pkg/**'
- 'go.mod'
- 'go.sum'
paths-ignore:
- '**.md'
- 'docs/**'
Detecting changes in a monorepo:
jobs:
changes:
runs-on: ubuntu-latest
outputs:
api: steps-filter-outputs-api
web: steps-filter-outputs-web
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v3
id: filter
with:
filters: |
api:
- 'services/api/**'
- 'shared/**'
web:
- 'services/web/**'
- 'shared/**'
build-api:
needs: changes
if: needs.changes.outputs.api == 'true'
runs-on: ubuntu-latest
steps:
- run: echo "Building API service"
build-web:
needs: changes
if: needs.changes.outputs.web == 'true'
runs-on: ubuntu-latest
steps:
- run: echo "Building Web service"
4.7 Rollback Strategy
# .github/workflows/rollback.yml
name: Rollback
on:
workflow_dispatch:
inputs:
environment:
description: 'Target environment'
required: true
type: choice
options:
- staging
- production
revision:
description: 'Revision to rollback to (leave empty for previous)'
required: false
type: string
jobs:
rollback:
runs-on: ubuntu-latest
environment: inputs-environment
steps:
- name: Configure kubectl
uses: azure/setup-kubectl@v4
- name: Rollback deployment
run: |
if [ -z "REVISION_INPUT" ]; then
kubectl rollout undo deployment/myapp -n TARGET_ENV
else
kubectl rollout undo deployment/myapp \
--to-revision=REVISION_INPUT -n TARGET_ENV
fi
kubectl rollout status deployment/myapp -n TARGET_ENV --timeout=300s
- name: Verify rollback
run: |
curl -sf https://TARGET_ENV.myapp.example.com/healthz || exit 1
- name: Notify
uses: slackapi/slack-github-action@v2
with:
webhook: SECRETS_SLACK_WEBHOOK_REF
webhook-type: incoming-webhook
payload: |
{
"text": "Rollback completed for TARGET_ENV environment"
}
Part 5: Performance and Cost Optimization
5.1 Caching Strategies Deep Dive
Layer 1: Go module cache (actions/setup-go built-in)
The setup-go action automatically caches the Go module directory. No additional configuration needed.
Layer 2: Go build cache
- uses: actions/cache@v4
with:
path: ~/.cache/go-build
key: go-build-OS-HASH_OF_ALL_GO_FILES
restore-keys: |
go-build-OS-
Layer 3: Docker layer cache with GitHub Actions cache backend
- uses: docker/build-push-action@v6
with:
cache-from: type=gha
cache-to: type=gha,mode=max
Layer 4: golangci-lint cache
golangci-lint-action handles its own caching automatically. The cache key is based on the Go version and golangci-lint configuration.
Cache effectiveness monitoring:
Track cache hit rates in your workflow logs. A healthy pipeline should see 80%+ cache hit rates for module and build caches.
5.2 Parallel Job Execution
Structure your workflow DAG for maximum parallelism:
lint ------+
|
test ------+--> build --> deploy-staging --> deploy-production
|
security --+
jobs:
lint:
runs-on: ubuntu-latest
# No dependencies, runs immediately
test:
runs-on: ubuntu-latest
# No dependencies, runs in parallel with lint
security:
runs-on: ubuntu-latest
# No dependencies, runs in parallel with lint and test
build:
needs: [lint, test, security]
# Waits for all three to complete
deploy-staging:
needs: build
deploy-production:
needs: deploy-staging
5.3 Self-Hosted Runners
For cost-heavy or specialized workloads, self-hosted runners eliminate per-minute billing:
jobs:
build:
runs-on: [self-hosted, linux, x64, gpu]
steps:
- uses: actions/checkout@v4
- run: nvidia-smi # GPU access on self-hosted
When to use self-hosted runners:
| Scenario | Recommendation |
|---|---|
| Build time under 10 min | GitHub-hosted |
| Build time 10-30 min | Consider self-hosted |
| Build time over 30 min | Self-hosted strongly recommended |
| GPU/specialized hardware | Self-hosted (required) |
| Compliance (data locality) | Self-hosted (required) |
| Network access to internal resources | Self-hosted (required) |
Self-hosted runner setup with Docker:
# docker-compose.yml for self-hosted runner
version: '3.8'
services:
runner:
image: myoung34/github-runner:latest
environment:
REPO_URL: https://github.com/myorg/myrepo
RUNNER_TOKEN: your-registration-token
RUNNER_NAME: custom-runner-1
RUNNER_WORKDIR: /tmp/github-runner
LABELS: self-hosted,linux,x64
volumes:
- /var/run/docker.sock:/var/run/docker.sock
restart: unless-stopped
5.4 Billing and Cost Calculation
GitHub Actions pricing (as of 2026):
| Plan | Included Minutes | Storage | Price/Extra Min (Linux) |
|---|---|---|---|
| Free | 2,000/month | 500 MB | N/A |
| Team | 3,000/month | 2 GB | $0.008 |
| Enterprise | 50,000/month | 50 GB | $0.008 |
OS multipliers:
| OS | Multiplier |
|---|---|
| Linux | 1x |
| Windows | 2x |
| macOS | 10x |
Cost optimization tips:
- Cancel redundant workflow runs with concurrency groups
- Use path filters to skip unnecessary builds
- Maximize caching (module, build, Docker layers)
- Avoid macOS runners unless absolutely necessary (10x cost)
- Use matrix strategy wisely -- each matrix entry is a separate job
- Self-host for high-volume repositories
- Use
timeout-minutesto prevent runaway workflows - Combine small jobs to reduce job startup overhead (~15-30s per job)
5.5 Monitoring Workflow Performance
Track key metrics to identify bottlenecks:
# Add timing to key steps
- name: Build with timing
run: |
START_TIME=$(date +%s)
go build -o ./bin/myapp ./cmd/server/
END_TIME=$(date +%s)
DURATION=$((END_TIME - START_TIME))
echo "Build took DURATION_VAR seconds"
echo "build_duration=DURATION_VAR" >> GITHUB_OUTPUT_FILE
Key metrics to track:
- Total workflow duration
- Individual job and step duration
- Cache hit/miss rates
- Queue wait time (time before runner picks up job)
- Failure rate by job type
- Cost per workflow run
5.6 Local Testing with act
Test GitHub Actions locally before pushing:
# Install act
brew install act
# Run default event (push)
act
# Run specific workflow
act -W .github/workflows/ci.yml
# Run specific job
act -j test
# Use specific event
act pull_request
# Pass secrets
act -s GITHUB_TOKEN=your-token
# Use larger runner image
act -P ubuntu-latest=catthehacker/ubuntu:full-latest
Limitations of act:
- No services support (need to mock databases)
- Some GitHub context variables may not be available
- Docker-in-Docker can be problematic
- Some actions may not work identically to GitHub-hosted runners
Quiz
Q1: What is the difference between needs and uses in a GitHub Actions workflow?
Answer: needs defines job dependencies -- it specifies which jobs must complete successfully before the current job runs. For example, needs: [lint, test] means the job waits for both lint and test to finish. uses references an action or reusable workflow. In a step, uses: actions/checkout@v4 runs the checkout action. At the job level, uses: org/repo/.github/workflows/ci.yml@main calls a reusable workflow.
Q2: Why is CGO_ENABLED=0 important for Go Docker builds?
Answer: Setting CGO_ENABLED=0 produces a fully statically-linked binary with no C library dependencies. This is critical when using minimal base images like scratch or distroless that do not include glibc. Without CGO_ENABLED=0, the binary would dynamically link against libc and fail to run in these minimal containers. It also makes cross-compilation straightforward since you do not need a C cross-compiler toolchain.
Q3: When should you choose Trunk-Based Development over Git Flow?
Answer: Trunk-Based Development is ideal when you need rapid iteration with multiple deployments per day, your team has strong CI/CD practices with automated testing, and you can use feature flags to manage incomplete features. Git Flow is better when you need formal release management, support multiple versions simultaneously, or have less mature CI/CD infrastructure. The key decision factor is deployment frequency: if you deploy multiple times daily, Trunk-Based is superior. If you release monthly with version numbers, Git Flow provides the structure you need.
Q4: How does OIDC improve security over traditional secrets for cloud provider authentication?
Answer: OIDC (OpenID Connect) eliminates long-lived static credentials (access keys) by establishing a federated trust relationship between GitHub and your cloud provider. Instead of storing permanent access keys as repository secrets, workflows request short-lived tokens that are automatically scoped to specific repositories, branches, and environments. These tokens expire within minutes, reducing the blast radius if compromised. OIDC also provides an audit trail of which workflow requested which permissions, and there are no credentials to rotate or accidentally leak.
Q5: What caching strategy provides the best cost-to-performance ratio for Go CI pipelines?
Answer: The most impactful caching layers in order of effectiveness are: (1) Go module cache via actions/setup-go built-in caching -- this eliminates dependency downloads and typically saves 30-60 seconds. (2) Go build cache stored via actions/cache -- this avoids recompiling unchanged packages and saves 45-120 seconds. (3) Docker layer cache using GitHub Actions cache backend (type=gha) -- this avoids rebuilding unchanged Docker layers and saves 60-300 seconds. (4) golangci-lint cache (handled automatically by the action). Combined, these can reduce a 10-minute pipeline to under 3 minutes. The module cache has the best cost-to-performance ratio since it is free (built into setup-go) and saves the most time relative to cache size.
References
- GitHub Actions Documentation -- https://docs.github.com/en/actions
- GitHub Actions Workflow Syntax -- https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions
- GitHub Actions Contexts and Expressions -- https://docs.github.com/en/actions/learn-github-actions/contexts
- GitHub Actions Environments -- https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment
- GitHub OIDC with Cloud Providers -- https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect
- Go Build Documentation -- https://pkg.go.dev/cmd/go#hdr-Compile_packages_and_dependencies
- Go Cross-Compilation -- https://go.dev/doc/install/source#environment
- golangci-lint -- https://golangci-lint.run/
- GoReleaser Documentation -- https://goreleaser.com/
- Docker Multi-Stage Builds -- https://docs.docker.com/build/building/multi-stage/
- Docker Build Push Action -- https://github.com/docker/build-push-action
- Git Flow by Vincent Driessen -- https://nvie.com/posts/a-successful-git-branching-model/
- GitHub Flow -- https://docs.github.com/en/get-started/using-github/github-flow
- Trunk-Based Development -- https://trunkbaseddevelopment.com/
- Kubernetes Deployment Strategies -- https://kubernetes.io/docs/concepts/workloads/controllers/deployment/
- GitHub Actions Caching -- https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows
- GitHub Actions Self-Hosted Runners -- https://docs.github.com/en/actions/hosting-your-own-runners
- GitHub Actions Reusable Workflows -- https://docs.github.com/en/actions/using-workflows/reusing-workflows
- GitHub Repository Rulesets -- https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets
- GitHub Actions Billing -- https://docs.github.com/en/billing/managing-billing-for-github-actions
- act - Run GitHub Actions Locally -- https://github.com/nektos/act
- CODEOWNERS Documentation -- https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
- Codecov GitHub Action -- https://github.com/codecov/codecov-action
- dorny/paths-filter -- https://github.com/dorny/paths-filter