From 6ef83e18f809e02631a356017eb485bc673453df Mon Sep 17 00:00:00 2001 From: 0x1d Date: Mon, 12 Jan 2026 10:47:04 +0100 Subject: [PATCH] feat: initialize golang project template with docker and ci/cd --- .dockerignore | 9 +++++ .github/workflows/ci.yml | 62 +++++++++++++++++++++++++++++++ .github/workflows/release.yml | 57 ++++++++++++++++++++++++++++ .gitignore | 2 + .golangci.yml | 21 +++++++++++ AGENTS.md | 45 ++++++++++++++++++++++ Dockerfile | 29 +++++++++++++++ Makefile | 24 ++++++++++++ README.md | 70 ++++++++++++++++++++++++++++++++++- cmd/app/main.go | 51 +++++++++++++++++++++++++ go.mod | 3 ++ 11 files changed, 372 insertions(+), 1 deletion(-) create mode 100644 .dockerignore create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release.yml create mode 100644 .golangci.yml create mode 100644 AGENTS.md create mode 100644 Dockerfile create mode 100644 Makefile create mode 100644 cmd/app/main.go create mode 100644 go.mod diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..0646399 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +.git +.github +bin/ +build/ +coverage.out +README.md +Makefile +var/ +tmp/ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..fe654c9 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,62 @@ +name: CI + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: '1.23' + cache: false + - name: golangci-lint + uses: golangci/golangci-lint-action@v6 + with: + version: latest + + test: + name: Test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: '1.23' + - name: Test + run: go test -v -race -cover ./... + + build: + name: Build + runs-on: ubuntu-latest + needs: [lint, test] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: '1.23' + - name: Build + run: go build -v ./cmd/app + + docker-build: + name: Docker Build + runs-on: ubuntu-latest + needs: [build] + steps: + - uses: actions/checkout@v4 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Build + uses: docker/build-push-action@v5 + with: + context: . + push: false + tags: app:latest + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..b5ad5b5 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,57 @@ +name: Release + +on: + push: + tags: + - '*' + +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.23' + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v5 + with: + distribution: goreleaser + version: latest + args: release --clean + env: + GITEA_TOKEN: ${{ secrets.ACCESS_TOKEN_GITEA }} + + docker-release: + runs-on: ubuntu-latest + needs: goreleaser + steps: + - uses: actions/checkout@v4 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: git.dcentral.systems + username: master + password: ${{ secrets.ACCESS_TOKEN_GITEA }} + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v5 + with: + images: git.dcentral.systems/templates/golang + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.gitignore b/.gitignore index 5b90e79..e3dce1c 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,5 @@ go.work.sum # env file .env +build/ + diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..c067e90 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,21 @@ +# Options for analysis running. +version: 2 +run: + timeout: 5m + +linters: + enable: + - errcheck + - govet + - ineffassign + - staticcheck + - unused + - misspell + - unconvert + - unparam + +linters-settings: + govet: + check-shadowing: true + gofmt: + simplify: true diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..8a73439 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,45 @@ +# Development Guidelines + +This document outlines the best practices for Go development and Git workflow to be followed by the Gemini agent for this project. + +## Go Development Guidelines + +### Code Quality and Style +- **Formatting:** All Go code MUST be formatted with `gofmt` before committing. +- **Linting:** Use `golangci-lint` to enforce code quality and style. +- **Simplicity:** Write simple, clear, and concise code. Avoid unnecessary complexity. +- **Error Handling:** Handle all errors explicitly. Never ignore an error. Check for errors and return them from functions. +- **Naming:** Use meaningful and descriptive names for variables, functions, and packages. Follow Go's naming conventions (e.g., `camelCase` for private, `PascalCase` for public). +- **Comments:** Write comments to explain the *why* behind complex or non-obvious code, not the *what*. Document all public functions and packages. +- **Testing:** + - Write unit tests for all new code. Aim for high test coverage. + - Use table-driven tests for testing multiple scenarios. + - Mocks and interfaces should be used to isolate dependencies for testing. +- **Dependencies:** Use Go modules (`go mod`) to manage project dependencies. Keep the `go.mod` and `go.sum` files up-to-date. + +### Best Practices +- **Concurrency:** When using goroutines, ensure proper synchronization to avoid race conditions. Use channels for communication between goroutines. +- **Interfaces:** Use interfaces to define behavior and decouple components. +- **Packages:** Structure your code into logical packages. Avoid circular dependencies. +- **Security:** Be mindful of security best practices. Sanitize inputs, avoid SQL injection, and handle credentials securely. + +## Git Workflow Best Practices + +### Branching +- **Main Branch:** The `main` branch is for production-ready code only. Direct commits to `main` are NOT allowed. +- **Feature Branches:** Create a new branch for every new feature or bug fix. + - Branch names should be descriptive and follow the pattern `feature/` or `fix/` (e.g., `feature/user-auth`, `fix/login-bug`). +- **Pull Requests (PRs):** All changes must be submitted through a Pull Request to the `main` branch. + +### Committing +- **Atomic Commits:** Each commit should represent a single, logical change. Do not bundle unrelated changes into one commit. +- **Commit Messages:** Follow the [Conventional Commits](https://www.conventionalcommits.org/) specification. This helps in automating changelogs and versioning. + - **Format:** `[optional scope]: ` + - **Example:** `feat: add user login endpoint` + - **Common types:** `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore`. + +### Pull Request (PR) Process +1. **Create a PR:** When your feature or fix is complete, create a PR against the `main` branch. +2. **CI Checks:** Ensure all automated checks (build, tests, linting) pass. +3. **Code Review:** The PR will be reviewed. Address any feedback by pushing new commits to your branch. +4. **Merge:** Once the PR is approved and all checks pass, it will be merged into `main`. \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ab4452e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,29 @@ +# Build stage +FROM golang:1.23-alpine AS builder + +WORKDIR /app + +# Copy go.mod and go.sum first to leverage Docker cache +# We need to check if go.sum exists, but COPY fails if it doesn't. +# Since we just ran go mod init, go.sum might not exist yet if no deps. +# SAFE PATTERN: COPY go.mod and optional go.sum +COPY go.mod go.sum* ./ +RUN go mod download + +# Copy the rest of the source code +COPY . . + +# Build the application +# -ldflags="-w -s" strips debug information for smaller binary +RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o /go/bin/app ./cmd/app + +# Final stage +# Use distroless static image for security and minimal footprint +FROM gcr.io/distroless/static-debian12:nonroot + +WORKDIR / + +COPY --from=builder /go/bin/app /app +USER nonroot:nonroot + +ENTRYPOINT ["/app"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..c9a8d28 --- /dev/null +++ b/Makefile @@ -0,0 +1,24 @@ +.PHONY: build run test lint clean docker-build + +APP_NAME := app +BUILD_DIR := build +CMD_PATH := ./cmd/app + +build: + @mkdir -p $(BUILD_DIR) + go build -o $(BUILD_DIR)/$(APP_NAME) $(CMD_PATH) + +run: build + $(BUILD_DIR)/$(APP_NAME) + +test: + go test -v -race -cover ./... + +lint: + golangci-lint run + +clean: + rm -rf $(BUILD_DIR) + +docker-build: + docker build -t $(APP_NAME):latest . diff --git a/README.md b/README.md index 8977393..646dff0 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,70 @@ -# golang +# Golang Project Template +A production-ready Go project template with Docker builds, CI/CD pipelines, and best practices. + +## Features + +- **Standard Layout**: Follows Go project layout standards (`cmd`, `internal`). +- **Graceful Shutdown**: `main.go` implements signal handling and context cancellation. +- **Structured Logging**: Uses `log/slog`. +- **Docker**: Multi-stage `Dockerfile` using `distroless` for secure, small images. +- **CI/CD**: GitHub Actions for linting, testing, building, and releasing (with GoReleaser). +- **Tooling**: `Makefile` for common tasks and `golangci-lint` configuration. + +## Prerequisites + +- [Go](https://go.dev/) 1.23+ +- [Docker](https://www.docker.com/) +- [Make](https://www.gnu.org/software/make/) + +## Getting Started + +### Build and Run + +```bash +# Build binary +make build + +# Run application +make run +``` + +### Testing + +```bash +make test +``` + +### Linting + +```bash +make lint +``` + +## Docker + +Build the Docker image: + +```bash +make docker-build +``` + +Run the container: + +```bash +docker run --rm -p 8080:8080 app:latest +``` + +## CI/CD + +The project includes GitHub Actions workflows: + +- **CI (`ci.yml`)**: Runs on `main` and PRs. Validates formatting, linting, tests, and builds. +- **Release (`release.yml`)**: Runs on tags (`v*`). Creates a GitHub Release and pushes the Docker image to GHCR. + +## Project Structure + +- `cmd/app`: Main application entry point. +- `internal`: Private application code. +- `build`: Build artifacts. +- `.github`: CI/CD workflows. diff --git a/cmd/app/main.go b/cmd/app/main.go new file mode 100644 index 0000000..c87ad03 --- /dev/null +++ b/cmd/app/main.go @@ -0,0 +1,51 @@ +package main + +import ( + "context" + "log/slog" + "os" + "os/signal" + "syscall" + "time" +) + +func main() { + logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) + slog.SetDefault(logger) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // Handle OS signals + go func() { + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) + <-sigChan + slog.Info("Received shutdown signal") + cancel() + }() + + if err := run(ctx); err != nil { + slog.Error("Application error", "error", err) + os.Exit(1) + } +} + +func run(ctx context.Context) error { + slog.Info("Starting application...") + + // Simulate long running process or server start + // e.g. server.ListenAndServe() + + <-ctx.Done() + slog.Info("Shutting down application...") + + // Cleanup with timeout + _, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second) + defer shutdownCancel() + + // Pretend to cleanup + time.Sleep(100 * time.Millisecond) + + return nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..381f86c --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/placeholder/golang-template + +go 1.25.4