CI/CD, Docker, linting, testing, pre-commit hooks, editor config, and git templates.
# .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/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/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/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/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/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.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
# 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"]
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"]
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"]
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"]
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:
.git
.github
.vscode
.env
.env.*
!.env.example
node_modules
__pycache__
*.pyc
.venv
target/debug
target/release
*.md
!README.md
LICENSE
.editorconfig
.prettierrc
.eslintrc*
// .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"] }]
}
}
// .prettierrc
{
"semi": true,
"singleQuote": true,
"trailingComma": "all",
"printWidth": 100,
"tabWidth": 2
}
// .prettierignore
node_modules
dist
build
coverage
.next
# 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.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
# rustfmt.toml
edition = "2021"
max_width = 100
tab_spaces = 4
use_field_init_shorthand = true
use_try_shorthand = true
# clippy.toml
too-many-arguments-threshold = 7
type-complexity-threshold = 300
# In Cargo.toml - deny common warnings
[lints.clippy]
pedantic = { level = "warn", priority = -1 }
unwrap_used = "warn"
expect_used = "warn"
// 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'),
},
},
});
# 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__.",
]
// 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"])
}
}
// 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)
}
}
# Install
npm install --save-dev husky lint-staged
npx husky init
// package.json
{
"lint-staged": {
"*.{ts,tsx}": ["eslint --fix", "prettier --write"],
"*.{json,md,yml,yaml}": ["prettier --write"]
}
}
# .husky/pre-commit
npx lint-staged
# .husky/commit-msg
npx commitlint --edit $1
# .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
# Install
uv add --dev pre-commit
pre-commit install
pre-commit install --hook-type commit-msg
# Cargo.toml
[dev-dependencies]
cargo-husky = { version = "1", features = ["precommit-hook", "run-cargo-fmt", "run-cargo-clippy", "run-cargo-test"] }
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
{
"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"
}
{
"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"
]
}
node_modules/
dist/
build/
.next/
coverage/
*.tsbuildinfo
.env
.env.local
.env.*.local
.DS_Store
*.log
__pycache__/
*.py[cod]
*.so
.venv/
*.egg-info/
dist/
build/
.eggs/
.coverage
htmlcov/
.mypy_cache/
.ruff_cache/
.pytest_cache/
.env
*.db
*.exe
*.exe~
*.dll
*.so
*.dylib
*.test
*.out
vendor/
.env
/target
Cargo.lock # Only for libraries; commit for binaries
.env
* 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
# 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
## 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-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
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