View on GitHub

nikogura.com

Thoughts, opinions, and occasionally rantings of a passionate technologist.

GitHub Actions Reference Implementation

This document describes the reference implementation for GitHub Actions CI/CD pipelines based on the diagnostic-slackbot project.

Table of Contents


Overview

This CI/CD pipeline provides:


Complete Workflow Configuration

File: .github/workflows/ci.yml

name: CI

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

env:
  GO_VERSION: '1.25'

permissions:
  contents: write
  packages: write
  pull-requests: write

jobs:
  test:
    name: Test
    runs-on: ubuntu-latest
    outputs:
      version_tag: $
      version: $

    steps:
    - name: Checkout code
      uses: actions/checkout@v4
      with:
        fetch-depth: 0

    - name: Generate Version
      run: |
        LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
        echo "Last tag: $LAST_TAG"

        # Extract version numbers
        VERSION=${LAST_TAG#v}
        IFS='.' read -r MAJOR MINOR PATCH <<< "$VERSION"

        # Increment patch version
        PATCH=$((PATCH + 1))
        NEW_VERSION="v${MAJOR}.${MINOR}.${PATCH}"

        echo "New version: $NEW_VERSION"
        echo "version_tag=$NEW_VERSION" >> $GITHUB_OUTPUT
        echo "version=${MAJOR}.${MINOR}.${PATCH}" >> $GITHUB_OUTPUT
      id: semver

    - name: Set up Go
      uses: actions/setup-go@v5
      with:
        go-version: $

    - name: Set up Docker Buildx
      uses: docker/setup-buildx-action@v3

    - name: Cache Go modules
      uses: actions/cache@v4
      with:
        path: ~/go/pkg/mod
        key: $-go-$
        restore-keys: |
          $-go-

    - name: Cache Docker layers
      uses: actions/cache@v4
      with:
        path: /tmp/.buildx-cache
        key: $-buildx-$
        restore-keys: |
          $-buildx-

    - name: Download dependencies
      run: go mod download

    - name: Run unit tests
      run: make test

    - name: Install namedreturns
      run: go install github.com/nikogura/namedreturns@latest

    - name: golangci-lint
      uses: golangci/golangci-lint-action@v8
      with:
        version: latest

    - name: Lint Checks
      run: make lint

  publish:
    name: Publish
    runs-on: ubuntu-latest
    needs: test
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'

    steps:
    - name: Checkout code
      uses: actions/checkout@v4
      with:
        fetch-depth: 0

    - name: Tag Repo
      uses: mathieudutour/[email protected]
      with:
        github_token: $
        custom_tag: $

    - name: Publish Release
      uses: softprops/action-gh-release@v2
      with:
        tag_name: $
        name: $
        draft: false
        prerelease: false
        token: $

Workflow Breakdown

1. Trigger Configuration

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

Purpose: Run CI on:

Why this pattern:

2. Environment Variables

env:
  GO_VERSION: '1.25'

Purpose: Centralize Go version for easy updates

Best practice: Update this single value to upgrade Go across all steps

3. Permissions

permissions:
  contents: write      # Create tags and releases
  packages: write      # Publish packages
  pull-requests: write # Comment on PRs

Purpose: Explicit least-privilege permissions

Security: Only grant what’s needed. Avoid permissions: write-all

4. Test Job

Semantic Versioning

- name: Generate Version
  run: |
    LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
    VERSION=${LAST_TAG#v}
    IFS='.' read -r MAJOR MINOR PATCH <<< "$VERSION"
    PATCH=$((PATCH + 1))
    NEW_VERSION="v${MAJOR}.${MINOR}.${PATCH}"
    echo "version_tag=$NEW_VERSION" >> $GITHUB_OUTPUT
  id: semver

Purpose: Automatic patch version increment

Output: Makes version available to other jobs via needs.test.outputs.version_tag

Alternative: For manual version control, use version files or manual tags

Caching Strategy

Go Modules Cache:

- name: Cache Go modules
  uses: actions/cache@v4
  with:
    path: ~/go/pkg/mod
    key: $-go-$
    restore-keys: |
      $-go-

Docker Build Cache:

- name: Cache Docker layers
  uses: actions/cache@v4
  with:
    path: /tmp/.buildx-cache
    key: $-buildx-$
    restore-keys: |
      $-buildx-

Impact: Reduces build time from 3-5 minutes to 30-60 seconds

Testing

- name: Run unit tests
  run: make test

Requires: Makefile with test target

Example Makefile:

test:
	go test -v ./... -count=1 --cover

Linting

Step 1: Install namedreturns

- name: Install namedreturns
  run: go install github.com/nikogura/namedreturns@latest

Step 2: Run golangci-lint action

- name: golangci-lint
  uses: golangci/golangci-lint-action@v8
  with:
    version: latest

Step 3: Run custom lint target

- name: Lint Checks
  run: make lint

Why both?:

Required Makefile target:

lint:
	@echo "Running namedreturns linter..."
	namedreturns ./...
	@echo "Running golangci-lint..."
	golangci-lint run

5. Publish Job

publish:
  name: Publish
  runs-on: ubuntu-latest
  needs: test
  if: github.ref == 'refs/heads/main' && github.event_name == 'push'

Purpose: Only publish on successful test + main branch push

Why conditional:

Automatic Tagging

- name: Tag Repo
  uses: mathieudutour/[email protected]
  with:
    github_token: $
    custom_tag: $

Purpose: Create git tag from generated version

Access pattern: Uses outputs from test job

Release Creation

- name: Publish Release
  uses: softprops/action-gh-release@v2
  with:
    tag_name: $
    name: $
    draft: false
    prerelease: false

Purpose: Create GitHub release with changelog

Result: Automatically publishes release notes visible in GitHub


Key Features

1. Fail-Fast Pipeline

The workflow stops immediately if:

Why: Don’t waste time on subsequent steps if early checks fail

2. Dependency Caching

Go modules cache based on go.sum hash:

Docker layers cache based on git SHA:

3. Parallel Testing Strategy

The test job can be extended to run multiple test suites in parallel:

test:
  strategy:
    matrix:
      go: ['1.23', '1.24', '1.25']
  steps:
    - uses: actions/setup-go@v5
      with:
        go-version: $

4. Branch Protection Integration

This workflow integrates with GitHub branch protection:

GitHub SettingsBranchesBranch protection rules:

✅ Require status checks to pass before merging
  ✅ Test
  ✅ Lint Checks
✅ Require branches to be up to date before merging

Best Practices

1. Pin Action Versions

DO:

uses: actions/checkout@v4
uses: golangci/golangci-lint-action@v8

DON’T:

uses: actions/checkout@main
uses: golangci/golangci-lint-action@latest

Why: Prevent breaking changes from upstream actions

2. Use fetch-depth: 0 for Versioning

- uses: actions/checkout@v4
  with:
    fetch-depth: 0  # Required for git describe

Why: git describe --tags needs full history

3. Separate Test and Publish Jobs

Benefits:

4. Use Job Outputs for Data Flow

jobs:
  test:
    outputs:
      version_tag: $

  publish:
    needs: test
    steps:
      - run: echo $

Why: Clean data flow between jobs without artifacts

5. Make Workflow Idempotent

Each step should be safe to re-run:


Customization Guide

For Non-Go Projects

Replace Go-specific steps:

# Replace
- name: Set up Go
  uses: actions/setup-go@v5

# With (for Node.js)
- name: Set up Node
  uses: actions/setup-node@v4
  with:
    node-version: '20'

For Docker Image Publishing

Add to publish job:

- name: Log in to GitHub Container Registry
  uses: docker/login-action@v3
  with:
    registry: ghcr.io
    username: $
    password: $

- name: Build and push
  uses: docker/build-push-action@v5
  with:
    push: true
    tags: ghcr.io/$:$
    cache-from: type=local,src=/tmp/.buildx-cache
    cache-to: type=local,dest=/tmp/.buildx-cache-new

For Multi-Architecture Builds

- name: Set up QEMU
  uses: docker/setup-qemu-action@v3

- name: Build and push
  uses: docker/build-push-action@v5
  with:
    platforms: linux/amd64,linux/arm64
    push: true
    tags: ghcr.io/$:$

For Manual Version Control

Replace the version generation step:

- name: Get version from file
  run: |
    VERSION=$(cat VERSION)
    echo "version_tag=v$VERSION" >> $GITHUB_OUTPUT
  id: semver

For Integration Tests

Add a separate job:

integration:
  name: Integration Tests
  runs-on: ubuntu-latest
  needs: test

  services:
    postgres:
      image: postgres:16
      env:
        POSTGRES_PASSWORD: postgres
      options: >-
        --health-cmd pg_isready
        --health-interval 10s
        --health-timeout 5s
        --health-retries 5

  steps:
    - name: Run integration tests
      run: make integration-test
      env:
        DATABASE_URL: postgres://postgres:postgres@localhost:5432/test

Required Repository Setup

1. Makefile Targets

Your repository must include these make targets:

.PHONY: test lint

test:
	go test -v ./... -count=1 --cover

lint:
	@echo "Running namedreturns linter..."
	namedreturns ./...
	@echo "Running golangci-lint..."
	golangci-lint run

2. golangci-lint Configuration

Include .golangci.yml in repository root:

linters:
  enable:
    - gofmt
    - goimports
    - govet
    - errcheck
    - staticcheck
    # ... other linters

3. Branch Protection Rules

GitHub SettingsBranchesAdd rule:

Branch name pattern: main

☑ Require a pull request before merging
  ☑ Require approvals: 1
☑ Require status checks to pass before merging
  ☑ Require branches to be up to date before merging
  Status checks: Test, Lint Checks
☑ Do not allow bypassing the above settings

Troubleshooting

Tests Pass Locally But Fail in CI

Possible causes:

  1. Missing environment variables
  2. Different Go version
  3. Race conditions in tests
  4. File system differences

Solution: Run with same Go version as CI:

docker run --rm -v $(pwd):/app -w /app golang:1.25 make test

Cache Not Restoring

Check:

Force cache rebuild:

Version Tag Already Exists

Cause: Attempting to create duplicate tag

Solution: Delete tag or increment manually:

git tag -d v1.2.3
git push origin :refs/tags/v1.2.3

namedreturns Not Found

Cause: Installation step failed or skipped

Solution: Check step logs, ensure:

- name: Install namedreturns
  run: go install github.com/nikogura/namedreturns@latest

Reference Implementation

Live example: nikogura/diagnostic-slackbot

For questions or issues with this reference implementation, see the EngineeringStandards.md or open an issue.