Best Practice Github Action Untuk Manjemen Release

1. Struktur Proyek

my-go-api/
β”œβ”€β”€ .github/
β”‚   └── workflows/
β”‚       β”œβ”€β”€ ci.yml          # CI untuk setiap push/PR
β”‚       β”œβ”€β”€ release.yml     # Release saat tag dibuat
β”‚       └── tag.yml         # Auto-tagging untuk main branch
β”œβ”€β”€ cmd/
β”‚   └── server/
β”‚       └── main.go         # Entry point aplikasi
β”œβ”€β”€ internal/
β”‚   β”œβ”€β”€ config/             # Konfigurasi aplikasi
β”‚   β”œβ”€β”€ handler/            # HTTP handlers
β”‚   β”œβ”€β”€ middleware/         # Middleware
β”‚   └── version/            # Package untuk version management
β”‚       └── version.go      # File version variables
β”œβ”€β”€ pkg/                    # Public packages
β”‚   └── logger/
β”œβ”€β”€ go.mod                  # Go modules
β”œβ”€β”€ go.sum                  # Go modules checksums
β”œβ”€β”€ Dockerfile              # Docker image build
β”œβ”€β”€ .dockerignore           # File diabaikan Docker build
└── README.md               # Dokumentasi

Penjelasan:

  • Struktur ini mengikuti Standard Go Project Layout

  • internal/ untuk kode private yang tidak bisa diakses dari luar

  • cmd/ untuk entry point aplikasi

  • pkg/ untuk kode yang bisa digunakan oleh proyek lain

  • .github/workflows/ untuk GitHub Actions workflows

2. Setup Version di Golang

// internal/version/version.go
package version

var (
    Version   = "dev"      // Default version
    Commit    = "none"     // Git commit hash
    BuildTime = "unknown"  // Build timestamp
)

func GetVersion() string { return Version }
func GetCommit() string { return Commit }
func GetBuildTime() string { return BuildTime }

func GetFullVersion() string {
    return fmt.Sprintf("%s-%s (%s)", Version, Commit, BuildTime)
}

Penjelasan:

  • Version variables di-set pada build time menggunakan ldflags

  • Default values untuk development mode

  • Runtime access ke version info untuk debugging dan monitoring

  • Full version format: v1.0.0-abc123 (2024-01-15T10:30:00Z)

3. Docker Multi-Stage Build

# Stage 1: Builder
FROM golang:1.21-alpine AS builder
WORKDIR /app
RUN apk add --no-cache git ca-certificates
COPY go.mod go.sum ./
RUN go mod download
COPY . .

# Build dengan injected version info
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo \
    -ldflags "-X internal/version.Version=${VERSION:-dev} \
              -X internal/version.Commit=${COMMIT:-none} \
              -X internal/version.BuildTime=${BUILD_TIME:-unknown}" \
    -o /app/server ./cmd/server

# Stage 2: Final
FROM alpine:latest
WORKDIR /root/
COPY --from=builder /app/server .
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
EXPOSE 8080
CMD ["./server"]

Penjelasan:

  • Multi-stage build mengurangi image size dari ~800MB ke ~20MB

  • Stage 1: Build environment dengan Go dependencies

  • Stage 2: Runtime environment hanya dengan binary dan certificates

  • CGO_ENABLED=0 untuk static linking

  • Version injection via ldflags untuk build-time variables

4. .dockerignore

.git
.github
*.md
Dockerfile
.dockerignore
coverage.txt
*.log
.vscode
.idea
node_modules

Penjelasan:

  • Mengurangi build context size yang dikirim ke Docker daemon

  • Mencegah cache invalidation yang tidak perlu

  • Keamanan dengan tidak menyertakan sensitive files

  • Optimasi build speed dengan mengabaikan development files

5. GitHub Actions CI Workflow

name: CI

on:
  push:
    branches: [ main, develop ]    # Trigger untuk push ke main/develop
  pull_request:
    branches: [ main ]              # Trigger untuk PR ke main

jobs:
  test:
    name: Test
    runs-on: ubuntu-latest
    
    steps:
    - name: Checkout code
      uses: actions/checkout@v4
      
    - name: Set up Go
      uses: actions/setup-go@v4
      with:
        go-version: '1.21'
        cache: true                 # Cache Go modules
        
    - name: Run tests
      run: |
        go test -v -race -coverprofile=coverage.txt -covermode=atomic ./...
        
    - name: Upload coverage to Codecov
      uses: codecov/codecov-action@v3
      with:
        file: ./coverage.txt
        
    - name: Run golangci-lint
      uses: golangci/golangci-lint-action@v3
      with:
        version: latest
        
    - name: Security check
      run: |
        go install github.com/securego/gosec/v2/cmd/gosec@latest
        gosec ./...

  build:
    name: Build Docker Image
    runs-on: ubuntu-latest
    needs: test                    # Hanya run jika tests pass
    if: github.ref == 'refs/heads/main'  # Hanya untuk main branch
    
    steps:
    - name: Checkout code
      uses: actions/checkout@v4
      
    - name: Set up Docker Buildx
      uses: docker/setup-buildx-action@v3
      
    - name: Login to Docker Hub
      uses: docker/login-action@v3
      with:
        username: ${{ secrets.DOCKER_USERNAME }}
        password: ${{ secrets.DOCKER_PASSWORD }}
        
    - name: Extract metadata
      id: meta
      uses: docker/metadata-action@v5
      with:
        images: yourusername/my-go-api
        tags: |
          type=ref,event=branch      # Tag berdasarkan branch name
          type=ref,event=pr          # Tag untuk PR
          type=sha,prefix={{branch}}- # Tag dengan commit hash
          type=raw,value=latest,enable={{is_default_branch}} # Latest tag
          
    - name: Build and push
      uses: docker/build-push-action@v5
      with:
        context: .
        platforms: linux/amd64,linux/arm64  # Multi-platform builds
        push: true
        tags: ${{ steps.meta.outputs.tags }}
        labels: ${{ steps.meta.outputs.labels }}
        build-args: |
          VERSION=${{ github.sha }}
          COMMIT=${{ github.sha }}
          BUILD_TIME=${{ github.event.created_at }}

Penjelasan:

  • Trigger conditions: Push ke main/develop branches dan PR ke main

  • Parallel jobs: Test dan build berjalan parallel jika memungkinkan

  • Dependency management: Build job hanya run jika tests pass

  • Multi-platform builds: Support untuk AMD64 dan ARM64

  • Smart tagging: Tag otomatis berdasarkan branch, PR, dan commit

  • Security: Docker login via secrets, bukan hardcoded credentials

6. GitHub Actions Release Workflow

name: Release

on:
  push:
    tags:
      - 'v*'           # Trigger untuk tags yang dimulai dengan 'v'

jobs:
  release:
    name: Create Release
    runs-on: ubuntu-latest
    
    steps:
    - name: Checkout code
      uses: actions/checkout@v4
      with:
        fetch-depth: 0  # Full history untuk changelog
        
    - name: Set up Go
      uses: actions/setup-go@v4
      with:
        go-version: '1.21'
        cache: true
        
    - name: Generate changelog
      id: changelog
      uses: orhun/git-cliff-action@v1
      with:
        config: cliff.toml
        args: --latest --strip header
        
    - name: Create Release
      uses: softprops/action-gh-release@v1
      with:
        tag_name: ${{ github.ref_name }}
        name: Release ${{ github.ref_name }}
        body: ${{ steps.changelog.outputs.content }}
        draft: false
        prerelease: false
        generate_release_notes: true
        
    - name: Set up Docker Buildx
      uses: docker/setup-buildx-action@v3
      
    - name: Login to Docker Hub
      uses: docker/login-action@v3
      with:
        username: ${{ secrets.DOCKER_USERNAME }}
        password: ${{ secrets.DOCKER_PASSWORD }}
        
    - name: Extract metadata
      id: meta
      uses: docker/metadata-action@v5
      with:
        images: yourusername/my-go-api
        tags: |
          type=ref,event=tag           # Tag dari git tag
          type=raw,value=latest,enable={{is_default_branch}}
          
    - name: Build and push release image
      uses: docker/build-push-action@v5
      with:
        context: .
        platforms: linux/amd64,linux/arm64
        push: true
        tags: ${{ steps.meta.outputs.tags }}
        labels: ${{ steps.meta.outputs.labels }}
        build-args: |
          VERSION=${{ github.ref_name }}    # Use tag as version
          COMMIT=${{ github.sha }}
          BUILD_TIME=${{ github.event.created_at }}

Penjelasan:

  • Trigger: Hanya saat tag baru dibuat (v1.0.0, v1.0.1, etc.)

  • Changelog generation: Otomatis dari commit messages

  • GitHub release: Create release dengan changelog

  • Docker tagging: Tag image dengan version number

  • Version injection: Use tag name as application version

7. Semantic Versioning dengan Conventional Commits

# .cliff.toml
[changelog]
header = """
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
"""
body = """
{% if version %}\
    ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
{% else %}\
    ## [Unreleased]
{% endif %}\
{% for group, commits in commits | group_by(attribute="group") %}
    ### {{ group | upper_first }}
    {% for commit in commits %}
        - {{ commit.message | upper_first }}\
    {% endfor %}
{% endfor %}
"""
trim = true

[git]
conventional_commits = true
filter_unconventional = true
split_commits = false
commit_preprocessors = [
    { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "" },
]
commit_parsers = [
    { message = "^feat", group = "Features" },
    { message = "^fix", group = "Bug Fixes" },
    { message = "^docs", group = "Documentation" },
    { message = "^perf", group = "Performance" },
    { message = "^refactor", group = "Refactoring" },
    { message = "^style", group = "Style" },
    { message = "^test", group = "Tests" },
    { message = "^chore", group = "Miscellaneous Tasks", skip = true },
    { message = "^ci", group = "Continuous Integration" },
]
protect_breaking_commits = true
filter_commits = false
tag_pattern = "v[0-9]*"
skip_tags = ""
ignore_tags = ""
topo_order = false
sort_commits = "oldest"

Penjelasan:

  • Conventional Commits: Format commit messages yang terstruktur

  • Automatic versioning: feat β†’ minor bump, fix β†’ patch bump, BREAKING CHANGE β†’ major bump

  • Changelog generation: Otomatis dari commit history

  • Grouping: Commits dikelompokkan berdasarkan type (feat, fix, docs, etc.)

8. Workflow untuk Automated Tagging

name: Auto Tag

on:
  push:
    branches: [ main ]     # Hanya untuk main branch

jobs:
  tag:
    name: Create Tag
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'  # Hanya main branch
    
    steps:
    - name: Checkout code
      uses: actions/checkout@v4
      with:
        fetch-depth: 0  # Full history
        
    - name: Setup Node.js
      uses: actions/setup-node@v4
      with:
        node-version: '18'
        
    - name: Install semantic-release
      run: npm install -g semantic-release @semantic-release/git @semantic-release/changelog @semantic-release/exec
        
    - name: Create tag and release
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        GH_TOKEN: ${{ secrets.GH_TOKEN }}
      run: |
        npx semantic-release
{
  "branches": ["main"],
  "plugins": [
    "@semantic-release/commit-analyzer",
    "@semantic-release/release-notes-generator",
    "@semantic-release/changelog",
    "@semantic-release/github",
    [
      "@semantic-release/git",
      {
        "assets": ["CHANGELOG.md"],
        "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
      }
    ]
  ]
}

Penjelasan:

  • Semantic-release: Analisa commit messages untuk menentukan version bump

  • Automatic tagging: Membuat tag baru berdasarkan conventional commits

  • Changelog update: Update CHANGELOG.md otomatis

  • GitHub release: Create release notes otomatis

9. Kapan Membuat Tag Baru - JAWABAN PERTANYAAN UTAMA

❌ TIDAK, setiap push TIDAK membuat tag baru!

Kapan tag baru dibuat:

  1. Push ke main branch dengan conventional commits

    • feat: add new feature β†’ Memicu minor version bump (v1.0.0 β†’ v1.1.0)

    • fix: resolve bug β†’ Memicu patch version bump (v1.0.0 β†’ v1.0.1)

    • feat: add breaking change + BREAKING CHANGE: β†’ Major bump (v1.0.0 β†’ v2.0.0)

  2. Manual tag creation

    git tag v1.0.0
    git push origin v1.0.0
  3. Schedule-based releases

    • Setup workflow untuk release setiap 2 minggu

Contoh conventional commits:

# Patch release (v1.0.0 β†’ v1.0.1)
git commit -m "fix: resolve authentication issue"

# Minor release (v1.0.0 β†’ v1.1.0)
git commit -m "feat: add user profile endpoint"

# Major release (v1.0.0 β†’ v2.0.0)
git commit -m "feat: implement new API design

BREAKING CHANGE: Removed deprecated endpoints"

Flow kerja:

  1. Development: Push ke develop branch β†’ CI tests only

  2. Merge to main: Push ke main dengan conventional commits β†’ Auto-tagging + release

  3. Manual override: Bisa buat tag manual jika perlu

10. Kapan Harus Release

Release Schedule:

  • Patch releases: Setiap 1-2 minggu (bug fixes, security patches)

  • Minor releases: Setiap 4-6 minggu (new features)

  • Major releases: Setiap 3-6 bulan (breaking changes)

Release Checklist:

- [ ] All tests passing
- [ ] Code coverage β‰₯ 80%
- [ ] Linting clean
- [ ] Security scan passed
- [ ] Documentation updated
- [ ] CHANGELOG.md updated
- [ ] Docker image built and tested
- [ ] Integration tests passed
- [ ] Performance tests passed
- [ ] API documentation updated

11. Environment Variables untuk Build

Development:

export VERSION=dev
export COMMIT=$(git rev-parse --short HEAD)
export BUILD_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ")

go build -ldflags "-X internal/version.Version=${VERSION} \
                   -X internal/version.Commit=${COMMIT} \
                   -X internal/version.BuildTime=${BUILD_TIME}"

Production (GitHub Actions):

build-args: |
  VERSION=${{ github.ref_name }}    # v1.0.0
  COMMIT=${{ github.sha }}         # abc123def
  BUILD_TIME=${{ github.event.created_at }}  # 2024-01-15T10:30:00Z

12. Best Practices Tambahan

1. Multi-Platform Builds

platforms: linux/amd64,linux/arm64,linux/arm/v7

2. Caching Strategy

- name: Cache Docker layers
  uses: actions/cache@v3
  with:
    path: /tmp/.buildx-cache
    key: ${{ runner.os }}-buildx-${{ github.sha }}
    restore-keys: |
      ${{ runner.os }}-buildx-

3. Security Scanning

- name: Run Trivy vulnerability scanner
  uses: aquasecurity/trivy-action@master
  with:
    image-ref: 'yourusername/my-go-api:latest'
    format: 'sarif'
    output: 'trivy-results.sarif'
    
- name: Upload Trivy scan results
  uses: github/codeql-action/upload-sarif@v2
  with:
    sarif_file: 'trivy-results.sarif'

4. Slack Notifications

- name: Notify Slack
  uses: rtCamp/action-slack-notify@v2
  env:
    SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
    SLACK_MESSAGE: "πŸš€ New release ${{ github.ref_name }} deployed!"

13. Setup Secrets di GitHub

Required Secrets:

  1. DOCKER_USERNAME: Docker Hub username

  2. DOCKER_PASSWORD: Docker Hub access token

  3. GH_TOKEN: GitHub personal access token (untuk semantic-release)

  4. SLACK_WEBHOOK: Slack incoming webhook (optional)

Setup Instructions:

  1. Go to repository β†’ Settings β†’ Secrets and variables β†’ Actions

  2. Click "New repository secret"

  3. Add each secret with proper permissions

14. Contoh Penggunaan Version di Code

// cmd/server/main.go
package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "os"

    "yourproject/internal/version"
)

type VersionResponse struct {
    Version   string `json:"version"`
    Commit    string `json:"commit"`
    BuildTime string `json:"build_time"`
    GoVersion string `json:"go_version"`
}

type HealthResponse struct {
    Status  string `json:"status"`
    Version string `json:"version"`
}

func main() {
    // Setup routes
    http.HandleFunc("/health", healthHandler)
    http.HandleFunc("/version", versionHandler)
    http.HandleFunc("/", rootHandler)
    
    port := os.Getenv("PORT")
    if port == "" {
        port = "8080"
    }
    
    log.Printf("πŸš€ Starting server on port %s", port)
    log.Printf("πŸ“‹ Version: %s", version.GetFullVersion())
    log.Fatal(http.ListenAndServe(":"+port, nil))
}

func versionHandler(w http.ResponseWriter, r *http.Request) {
    response := VersionResponse{
        Version:   version.GetVersion(),
        Commit:    version.GetCommit(),
        BuildTime: version.GetBuildTime(),
        GoVersion: runtime.Version(),
    }
    
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(response)
}

func healthHandler(w http.ResponseWriter, r *http.Request) {
    response := HealthResponse{
        Status:  "healthy",
        Version: version.GetVersion(),
    }
    
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(response)
}

func rootHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "πŸ‘‹ Hello from My Go API!\n")
    fmt.Fprintf(w, "πŸ“‹ Version: %s\n", version.GetFullVersion())
}

15. Testing Setup

Unit Tests:

# Run all tests
go test -v ./...

# Run tests with coverage
go test -v -coverprofile=coverage.txt -covermode=atomic ./...

# Run tests with race detection
go test -v -race ./...

# Generate coverage report
go tool cover -html=coverage.txt -o coverage.html

Integration Tests:

# Run integration tests
go test -v -tags=integration ./integration/...

# Run tests with benchmark
go test -bench=. -benchmem ./...

# Run tests with specific pattern
go test -v -run TestAPI ./handler/

Test Workflow:

- name: Run tests
  run: |
    go test -v -race -coverprofile=coverage.txt -covermode=atomic ./...
    
- name: Check coverage
  run: |
    go tool cover -func=coverage.txt | grep total
    
- name: Upload coverage
  uses: codecov/codecov-action@v3
  with:
    file: ./coverage.txt
    flags: unittests
    name: codecov-umbrella

Summary Workflow

Development Flow:

  1. Feature branch: Push ke develop β†’ CI tests only

  2. Pull Request: PR ke main β†’ Full CI + checks

  3. Merge to main: Auto-tagging + release jika conventional commits

  4. Production deployment: Auto-deploy dari release

Key Points:

  • βœ… Tidak setiap push buat tag baru - hanya conventional commits ke main

  • βœ… Semantic versioning otomatis berdasarkan commit messages

  • βœ… Multi-platform Docker builds untuk production

  • βœ… Version injection ke binary untuk debugging

  • βœ… Security scanning dan quality checks

  • βœ… Industry best practices dari Google, Uber, dll.

Setup ini memberikan fully automated CI/CD pipeline yang mengikuti best practices industri untuk Go applications! πŸš€

Last updated