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 luarcmd/
untuk entry point aplikasipkg/
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 bumpChangelog 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:
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)
Manual tag creation
git tag v1.0.0 git push origin v1.0.0
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:
Development: Push ke
develop
branch β CI tests onlyMerge to main: Push ke
main
dengan conventional commits β Auto-tagging + releaseManual 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:
DOCKER_USERNAME: Docker Hub username
DOCKER_PASSWORD: Docker Hub access token
GH_TOKEN: GitHub personal access token (untuk semantic-release)
SLACK_WEBHOOK: Slack incoming webhook (optional)
Setup Instructions:
Go to repository β Settings β Secrets and variables β Actions
Click "New repository secret"
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:
Feature branch: Push ke
develop
β CI tests onlyPull Request: PR ke
main
β Full CI + checksMerge to main: Auto-tagging + release jika conventional commits
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