# Tooling Templates CI/CD, Docker, linting, testing, pre-commit hooks, editor config, and git templates. ## CI/CD Templates ### GitHub Actions - Test on PR ```yaml # .github/workflows/test.yml name: Test on: pull_request: branches: [main] push: branches: [main] concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: test: runs-on: ubuntu-latest strategy: matrix: node-version: [18, 20] steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} cache: 'npm' - run: npm ci - run: npm run lint - run: npm run type-check - run: npm test -- --coverage - uses: actions/upload-artifact@v4 if: always() with: name: coverage-${{ matrix.node-version }} path: coverage/ ``` ### GitHub Actions - Test (Python) ```yaml # .github/workflows/test.yml name: Test on: pull_request: branches: [main] push: branches: [main] jobs: test: runs-on: ubuntu-latest strategy: matrix: python-version: ['3.11', '3.12'] 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: astral-sh/setup-uv@v4 - run: uv python install ${{ matrix.python-version }} - run: uv sync - run: uv run ruff check . - run: uv run ruff format --check . - run: uv run mypy . - run: uv run pytest --cov --cov-report=xml env: DATABASE_URL: postgresql+asyncpg://test:test@localhost:5432/testdb ``` ### GitHub Actions - Test (Go) ```yaml # .github/workflows/test.yml name: Test on: pull_request: branches: [main] push: branches: [main] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version-file: go.mod - run: go vet ./... - name: golangci-lint uses: golangci/golangci-lint-action@v6 with: version: latest - run: go test -race -coverprofile=coverage.out ./... - uses: actions/upload-artifact@v4 with: name: coverage path: coverage.out ``` ### GitHub Actions - Test (Rust) ```yaml # .github/workflows/test.yml name: Test on: pull_request: branches: [main] push: branches: [main] env: CARGO_TERM_COLOR: always RUSTFLAGS: "-Dwarnings" jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable with: components: clippy, rustfmt - uses: Swatinem/rust-cache@v2 - run: cargo fmt --check - run: cargo clippy --all-targets - run: cargo test ``` ### GitHub Actions - Build and Deploy ```yaml # .github/workflows/deploy.yml name: Deploy on: push: branches: [main] permissions: contents: read packages: write jobs: build: runs-on: ubuntu-latest outputs: image: ${{ steps.meta.outputs.tags }} steps: - uses: actions/checkout@v4 - uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - id: meta uses: docker/metadata-action@v5 with: images: ghcr.io/${{ github.repository }} tags: | type=sha,prefix= type=raw,value=latest - uses: docker/build-push-action@v6 with: context: . push: true tags: ${{ steps.meta.outputs.tags }} cache-from: type=gha cache-to: type=gha,mode=max deploy: needs: build runs-on: ubuntu-latest environment: production steps: - name: Deploy to production run: | echo "Deploy image: ${{ needs.build.outputs.image }}" # Add deployment command here ``` ### GitHub Actions - Release ```yaml # .github/workflows/release.yml name: Release on: push: tags: ['v*'] permissions: contents: write jobs: release: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Generate changelog id: changelog run: | echo "## Changes" > CHANGES.md git log $(git describe --tags --abbrev=0 HEAD^)..HEAD --pretty=format:"- %s" >> CHANGES.md - uses: softprops/action-gh-release@v2 with: body_path: CHANGES.md generate_release_notes: true ``` ### GitLab CI - Simple Pipeline ```yaml # .gitlab-ci.yml stages: - test - build - deploy variables: NODE_IMAGE: node:20-slim test: stage: test image: $NODE_IMAGE cache: key: $CI_COMMIT_REF_SLUG paths: - node_modules/ script: - npm ci - npm run lint - npm test -- --coverage coverage: '/All files[^|]*\|[^|]*\s+([\d\.]+)/' artifacts: reports: coverage_report: coverage_format: cobertura path: coverage/cobertura-coverage.xml build: stage: build image: docker:latest services: - docker:dind script: - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA . - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA only: - main deploy: stage: deploy script: - echo "Deploy $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA" environment: name: production only: - main when: manual ``` --- ## Docker Templates ### Multi-Stage Build - Node.js ```dockerfile # Build stage FROM node:20-slim AS builder WORKDIR /app COPY package.json package-lock.json ./ RUN npm ci COPY tsconfig.json ./ COPY src ./src RUN npm run build RUN npm prune --production # Runtime stage FROM node:20-slim WORKDIR /app RUN addgroup --system --gid 1001 appgroup && \ adduser --system --uid 1001 appuser COPY --from=builder --chown=appuser:appgroup /app/node_modules ./node_modules COPY --from=builder --chown=appuser:appgroup /app/dist ./dist COPY package.json ./ USER appuser ENV NODE_ENV=production EXPOSE 3000 HEALTHCHECK --interval=30s --timeout=3s \ CMD node -e "fetch('http://localhost:3000/health').then(r => r.ok ? process.exit(0) : process.exit(1))" CMD ["node", "dist/index.js"] ``` ### Multi-Stage Build - Python ```dockerfile FROM python:3.12-slim AS builder WORKDIR /app RUN pip install uv COPY pyproject.toml uv.lock ./ RUN uv sync --frozen --no-dev COPY src/ src/ FROM python:3.12-slim WORKDIR /app RUN addgroup --system --gid 1001 appgroup && \ adduser --system --uid 1001 appuser COPY --from=builder --chown=appuser:appgroup /app/.venv /app/.venv COPY --from=builder --chown=appuser:appgroup /app/src /app/src USER appuser ENV PATH="/app/.venv/bin:$PATH" ENV PYTHONUNBUFFERED=1 EXPOSE 8000 HEALTHCHECK --interval=30s --timeout=3s \ CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" CMD ["uvicorn", "my_app.main:app", "--host", "0.0.0.0", "--port", "8000"] ``` ### Multi-Stage Build - Go ```dockerfile FROM golang:1.22-alpine AS builder WORKDIR /app COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /server ./cmd/server FROM alpine:3.19 RUN apk --no-cache add ca-certificates && \ addgroup -S appgroup && adduser -S appuser -G appgroup WORKDIR /app COPY --from=builder --chown=appuser:appgroup /server . USER appuser EXPOSE 8080 HEALTHCHECK --interval=30s --timeout=3s \ CMD wget -qO- http://localhost:8080/health || exit 1 CMD ["./server"] ``` ### Multi-Stage Build - Rust ```dockerfile FROM rust:1.77-slim AS builder WORKDIR /app RUN apt-get update && apt-get install -y pkg-config libssl-dev && rm -rf /var/lib/apt/lists/* # Cache dependencies COPY Cargo.toml Cargo.lock ./ RUN mkdir src && echo "fn main() {}" > src/main.rs && \ cargo build --release && rm -rf src COPY src ./src RUN touch src/main.rs && cargo build --release FROM debian:bookworm-slim RUN apt-get update && apt-get install -y ca-certificates libssl3 && rm -rf /var/lib/apt/lists/* && \ addgroup --system appgroup && adduser --system --ingroup appgroup appuser WORKDIR /app COPY --from=builder --chown=appuser:appgroup /app/target/release/my-app . USER appuser ENV RUST_LOG=info EXPOSE 8080 CMD ["./my-app"] ``` ### docker-compose - App + Database + Cache ```yaml services: app: build: . ports: - "${PORT:-3000}:3000" env_file: .env depends_on: db: condition: service_healthy redis: condition: service_healthy restart: unless-stopped db: image: postgres:16-alpine environment: POSTGRES_USER: ${DB_USER:-user} POSTGRES_PASSWORD: ${DB_PASSWORD:-password} POSTGRES_DB: ${DB_NAME:-mydb} ports: - "5432:5432" volumes: - pgdata:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-user} -d ${DB_NAME:-mydb}"] interval: 5s timeout: 3s retries: 5 redis: image: redis:7-alpine ports: - "6379:6379" volumes: - redisdata:/data healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 5s timeout: 3s retries: 5 volumes: pgdata: redisdata: ``` ### .dockerignore (Universal) ``` .git .github .vscode .env .env.* !.env.example node_modules __pycache__ *.pyc .venv target/debug target/release *.md !README.md LICENSE .editorconfig .prettierrc .eslintrc* ``` --- ## Linting and Formatting ### ESLint + Prettier (TypeScript) ```json // .eslintrc.json { "root": true, "extends": [ "eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier" ], "parser": "@typescript-eslint/parser", "parserOptions": { "ecmaVersion": "latest", "sourceType": "module" }, "rules": { "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }], "@typescript-eslint/no-explicit-any": "warn", "no-console": ["warn", { "allow": ["warn", "error"] }] } } ``` ```json // .prettierrc { "semi": true, "singleQuote": true, "trailingComma": "all", "printWidth": 100, "tabWidth": 2 } ``` ``` // .prettierignore node_modules dist build coverage .next ``` ### Ruff (Python) ```toml # In pyproject.toml [tool.ruff] target-version = "py312" line-length = 88 [tool.ruff.lint] select = [ "E", # pycodestyle errors "F", # pyflakes "I", # isort "UP", # pyupgrade "B", # flake8-bugbear "SIM", # flake8-simplify "RUF", # ruff-specific rules ] ignore = ["E501"] # line length handled by formatter [tool.ruff.lint.isort] known-first-party = ["my_app"] [tool.ruff.format] quote-style = "double" ``` ### golangci-lint (Go) ```yaml # .golangci.yml linters: enable: - errcheck - gosimple - govet - ineffassign - staticcheck - unused - gofmt - goimports - misspell - unconvert - gocritic - revive linters-settings: govet: check-shadowing: true revive: rules: - name: exported severity: warning run: timeout: 5m tests: true issues: exclude-use-default: false max-issues-per-linter: 0 max-same-issues: 0 ``` ### Clippy + rustfmt (Rust) ```toml # rustfmt.toml edition = "2021" max_width = 100 tab_spaces = 4 use_field_init_shorthand = true use_try_shorthand = true ``` ```toml # clippy.toml too-many-arguments-threshold = 7 type-complexity-threshold = 300 ``` ```toml # In Cargo.toml - deny common warnings [lints.clippy] pedantic = { level = "warn", priority = -1 } unwrap_used = "warn" expect_used = "warn" ``` --- ## Testing Setup ### Vitest (JavaScript/TypeScript) ```typescript // vitest.config.ts import { defineConfig } from 'vitest/config'; import { resolve } from 'path'; export default defineConfig({ test: { globals: true, environment: 'node', // or 'jsdom' for browser coverage: { provider: 'v8', reporter: ['text', 'json', 'html', 'lcov'], exclude: ['node_modules/', 'tests/', '**/*.d.ts', '**/*.config.*'], thresholds: { lines: 80, functions: 80, branches: 80, statements: 80, }, }, include: ['tests/**/*.test.{ts,tsx}', 'src/**/*.test.{ts,tsx}'], }, resolve: { alias: { '@': resolve(__dirname, './src'), }, }, }); ``` ### pytest (Python) ```toml # In pyproject.toml [tool.pytest.ini_options] testpaths = ["tests"] asyncio_mode = "auto" addopts = "-ra -q --strict-markers" markers = [ "slow: marks tests as slow", "integration: marks integration tests", ] filterwarnings = [ "error", "ignore::DeprecationWarning", ] [tool.coverage.run] source = ["src"] branch = true [tool.coverage.report] show_missing = true fail_under = 80 exclude_lines = [ "pragma: no cover", "if TYPE_CHECKING:", "if __name__ == .__main__.", ] ``` ### go test ```go // internal/handlers/user_test.go package handlers_test import ( "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "github.com/gin-gonic/gin" ) func setupRouter() *gin.Engine { gin.SetMode(gin.TestMode) r := gin.Default() // Register routes return r } func TestListUsers(t *testing.T) { router := setupRouter() w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/users", nil) router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("expected status 200, got %d", w.Code) } } func TestCreateUser(t *testing.T) { router := setupRouter() body := `{"email":"test@example.com","name":"Test"}` w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/api/v1/users", strings.NewReader(body)) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, req) if w.Code != http.StatusCreated { t.Errorf("expected status 201, got %d", w.Code) } var response map[string]interface{} json.Unmarshal(w.Body.Bytes(), &response) if response["email"] != "test@example.com" { t.Errorf("expected email test@example.com, got %v", response["email"]) } } ``` ### cargo test (Rust) ```rust // tests/common/mod.rs use sqlx::PgPool; use std::net::SocketAddr; use tokio::net::TcpListener; pub struct TestApp { pub address: SocketAddr, pub client: reqwest::Client, pub pool: PgPool, } impl TestApp { pub async fn spawn() -> Self { let pool = PgPool::connect("postgres://test:test@localhost:5432/testdb") .await .expect("Failed to connect to test database"); sqlx::migrate!("./migrations") .run(&pool) .await .expect("Failed to run migrations"); let app = crate::routes::create_router(pool.clone()); let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let address = listener.local_addr().unwrap(); tokio::spawn(async move { axum::serve(listener, app).await.unwrap(); }); Self { address, client: reqwest::Client::new(), pool, } } pub fn url(&self, path: &str) -> String { format!("http://{}{}", self.address, path) } } ``` --- ## Pre-Commit Hooks ### Husky + lint-staged (Node.js) ```bash # Install npm install --save-dev husky lint-staged npx husky init ``` ```json // package.json { "lint-staged": { "*.{ts,tsx}": ["eslint --fix", "prettier --write"], "*.{json,md,yml,yaml}": ["prettier --write"] } } ``` ```bash # .husky/pre-commit npx lint-staged ``` ```bash # .husky/commit-msg npx commitlint --edit $1 ``` ### pre-commit (Python) ```yaml # .pre-commit-config.yaml repos: - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.4.0 hooks: - id: ruff args: [--fix] - id: ruff-format - repo: https://github.com/pre-commit/mirrors-mypy rev: v1.9.0 hooks: - id: mypy additional_dependencies: [types-requests] - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.6.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: check-yaml - id: check-added-large-files args: ['--maxkb=500'] - id: check-merge-conflict ``` ```bash # Install uv add --dev pre-commit pre-commit install pre-commit install --hook-type commit-msg ``` ### cargo-husky (Rust) ```toml # Cargo.toml [dev-dependencies] cargo-husky = { version = "1", features = ["precommit-hook", "run-cargo-fmt", "run-cargo-clippy", "run-cargo-test"] } ``` --- ## Editor Configuration ### .editorconfig ```ini root = true [*] indent_style = space indent_size = 2 end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true [*.{py,rs}] indent_size = 4 [*.go] indent_style = tab [*.md] trim_trailing_whitespace = false [Makefile] indent_style = tab ``` ### .vscode/settings.json ```json { "editor.formatOnSave": true, "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit", "source.organizeImports": "explicit" }, "[python]": { "editor.defaultFormatter": "charliermarsh.ruff", "editor.codeActionsOnSave": { "source.fixAll.ruff": "explicit", "source.organizeImports.ruff": "explicit" } }, "[go]": { "editor.defaultFormatter": "golang.go" }, "[rust]": { "editor.defaultFormatter": "rust-lang.rust-analyzer" }, "files.exclude": { "**/__pycache__": true, "**/node_modules": true, "**/target": true }, "search.exclude": { "**/node_modules": true, "**/dist": true, "**/coverage": true }, "typescript.tsdk": "node_modules/typescript/lib" } ``` ### .vscode/extensions.json ```json { "recommendations": [ "esbenp.prettier-vscode", "dbaeumer.vscode-eslint", "bradlc.vscode-tailwindcss", "charliermarsh.ruff", "golang.go", "rust-lang.rust-analyzer", "tamasfe.even-better-toml", "ms-azuretools.vscode-docker", "editorconfig.editorconfig", "usernamehw.errorlens" ] } ``` --- ## Git Configuration ### .gitignore - Node.js ``` node_modules/ dist/ build/ .next/ coverage/ *.tsbuildinfo .env .env.local .env.*.local .DS_Store *.log ``` ### .gitignore - Python ``` __pycache__/ *.py[cod] *.so .venv/ *.egg-info/ dist/ build/ .eggs/ .coverage htmlcov/ .mypy_cache/ .ruff_cache/ .pytest_cache/ .env *.db ``` ### .gitignore - Go ``` *.exe *.exe~ *.dll *.so *.dylib *.test *.out vendor/ .env ``` ### .gitignore - Rust ``` /target Cargo.lock # Only for libraries; commit for binaries .env ``` ### .gitattributes ``` * text=auto eol=lf *.bat text eol=crlf *.cmd text eol=crlf *.ps1 text eol=crlf *.png binary *.jpg binary *.gif binary *.ico binary *.woff binary *.woff2 binary *.lock linguist-generated *.min.js linguist-generated *.min.css linguist-generated ``` --- ## Documentation Templates ### README Template ```markdown # Project Name One-line description of what this project does. ## Prerequisites - Node.js >= 20 - PostgreSQL >= 16 ## Getting Started Clone the repository and install dependencies: git clone https://github.com/user/project.git cd project cp .env.example .env npm install npm run dev ## Development npm run dev # Start development server npm run test # Run tests npm run lint # Lint code npm run build # Production build ## Project Structure src/ app/ # Routes and pages components/ # Reusable components lib/ # Utilities and helpers ## Deployment Describe how to deploy the project. ## License MIT ``` ### CONTRIBUTING.md Template ```markdown # Contributing ## Development Setup 1. Fork and clone the repository 2. Install dependencies: `npm install` 3. Create a branch: `git checkout -b feature/my-feature` 4. Make changes and add tests 5. Run tests: `npm test` 6. Commit using conventional commits: `git commit -m "feat: add feature"` 7. Push and open a pull request ## Commit Messages Follow [Conventional Commits](https://www.conventionalcommits.org/): - `feat:` New feature - `fix:` Bug fix - `docs:` Documentation - `refactor:` Code change (no feature/fix) - `test:` Adding tests - `chore:` Maintenance ## Code Style - Run `npm run lint` before committing - Follow existing patterns in the codebase - Add tests for new functionality ## Pull Requests - Keep PRs focused and small - Include a description of changes - Ensure CI passes - Request review from maintainers ``` ### ADR (Architecture Decision Record) Template ```markdown # ADR-001: Title ## Status Proposed | Accepted | Deprecated | Superseded by ADR-XXX ## Context What is the issue that we're seeing that motivates this decision? ## Decision What is the change that we're proposing and/or doing? ## Consequences What becomes easier or harder as a result of this change? ### Positive - ... ### Negative - ... ### Neutral - ... ``` ### CHANGELOG.md Template ```markdown # Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/). ## [Unreleased] ### Added ### Changed ### Fixed ### Removed ## [0.1.0] - 2024-01-01 ### Added - Initial release - Feature A - Feature B ```