diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..6ac986d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,86 @@ +# Build artifacts +dist/ +**/dist/ + +# Git +.git/ +.gitignore + +# IDE and editor files +.vscode/ +.idea/ +*.swp +*.swo +*~ +.DS_Store + +# Environment and config files +.env +.env.* +*.log + +# CI/CD +.github/ +.hermit/ +bin/ + +# Temporary files +tmp/ +temp/ +*.tmp + +# Test coverage +coverage/ +*.lcov + +# Documentation +docs/ +*.md +!README.md + +# Development scripts +scripts/ + +# macOS +.DS_Store +.AppleDouble +.LSOverride + +# Windows +Thumbs.db +ehthumbs.db + +# Linux +.directory +.Trash-* + +# Archives +*.tar +*.tar.gz +*.tar.bz2 +*.zip +*.7z +*.rar + +# Backup files +*.bak +*.backup +*.old + +# Docker files (don't need to copy these into context) +docker/ +Dockerfile +.dockerignore +docker-compose.yml + +# Development tools +Justfile +lefthook.yml +renovate.json +.goreleaser.yaml +.golangci.yml +CODEOWNERS +CONTRIBUTING.md +GOVERNANCE.md +AGENTS.md +LICENSE diff --git a/.github/workflows/publish-docker.yml b/.github/workflows/publish-docker.yml new file mode 100644 index 0000000..eb2ccd4 --- /dev/null +++ b/.github/workflows/publish-docker.yml @@ -0,0 +1,89 @@ +name: Publish Docker Image + +on: + workflow_run: + workflows: ["CI"] + types: + - completed + branches: + - main + - docker + push: + tags: + - 'v*.*.*' + - 'v*.*.*-*' # For pre-releases like v1.2.3-beta + workflow_dispatch: + +permissions: + contents: read + packages: write + +jobs: + docker: + runs-on: ubuntu-latest + # Only run if CI passed (when triggered by workflow_run) or on tag push or manual dispatch + if: | + github.event_name == 'workflow_dispatch' || + github.event_name == 'push' || + (github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success') + steps: + - name: Checkout code + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # pin@v6 + with: + fetch-depth: 0 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # pin@v3.11.1 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # pin@v3.5.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract version metadata + id: meta + run: | + VERSION="${GITHUB_REF#refs/tags/}" + if [[ "$VERSION" == refs/heads/* ]]; then + VERSION="${VERSION#refs/heads/}" + fi + if [[ "$VERSION" == "main" ]]; then + VERSION="$(git describe --tags --always --dirty 2>/dev/null || echo dev)" + fi + echo "version=${VERSION}" >> "${GITHUB_OUTPUT}" + + GIT_COMMIT="$(git rev-parse HEAD)" + echo "git_commit=${GIT_COMMIT}" >> "${GITHUB_OUTPUT}" + + - name: Extract Docker metadata + id: docker_meta + uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # pin@v5.8.0 + with: + images: ghcr.io/${{ github.repository_owner }}/cachew + tags: | + # For main branch: latest, main, and sha + type=ref,event=branch + type=raw,value=latest,enable={{is_default_branch}} + # For tags: v1.2.3 -> 1.2.3, 1.2, 1 + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + # For pre-release tags: keep the full version + type=raw,value={{tag}},enable=${{ startsWith(github.ref, 'refs/tags/v') }} + + - name: Build and push Docker image + uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # pin@v6.18.0 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: ${{ steps.docker_meta.outputs.tags }} + labels: ${{ steps.docker_meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + platforms: linux/amd64,linux/arm64 + build-args: | + VERSION=${{ steps.meta.outputs.version }} + GIT_COMMIT=${{ steps.meta.outputs.git_commit }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..9d7e9f6 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,58 @@ +# Contributing to Cachew + +Thank you for your interest in contributing to Cachew! This guide will help you get started with local development, building, and testing. + +## Local Development + +**Run natively (fastest for development):** +```bash +just run # Build and run on localhost:8080 +``` + +**Run in Docker:** +```bash +just docker run # Build and run in container +just docker run debug # Run with debug logging +``` + +## Building and Testing + +```bash +just build # Build for current platform +just build-linux # Build for Linux +just build-all # Build all platforms +just test # Run tests +just lint # Lint code +just fmt # Format code +``` + +## Docker + +```bash +just docker build # Build single-arch Docker image for local use +just docker build-multi # Build multi-arch image (amd64 + arm64) +just docker run # Run in container +just docker run debug # Run with debug logging +just docker clean # Clean up Docker images +``` + +## Using the Cache + +The `cachew` CLI client interacts with the `cachewd` server: + +```bash +# Upload to cache +cachew put my-key myfile.txt --ttl 24h + +# Download from cache +cachew get my-key -o myfile.txt + +# Check if cached +cachew stat my-key + +# Snapshot a directory +cachew snapshot deps-cache ./node_modules --ttl 7d + +# Restore a directory +cachew restore deps-cache ./node_modules +``` diff --git a/Justfile b/Justfile index 9f18a8a..58c7a85 100644 --- a/Justfile +++ b/Justfile @@ -1,6 +1,16 @@ set positional-arguments := true set shell := ["bash", "-c"] +# Import modules +mod docker + +# Configuration + +VERSION := `git describe --tags --always --dirty 2>/dev/null || echo "dev"` +GIT_COMMIT := `git rev-parse HEAD 2>/dev/null || echo "unknown"` +TAG := `git rev-parse --short HEAD 2>/dev/null || echo "dev"` +RELEASE := "dist" + _help: @just -l @@ -18,3 +28,56 @@ fmt: just --unstable --fmt golangci-lint fmt go mod tidy + +# ============================================================================ +# Build +# ============================================================================ + +# Build for current platform +build: + @mkdir -p {{ RELEASE }} + @go build -trimpath -o {{ RELEASE }}/cachewd \ + -ldflags "-s -w -X main.version={{ VERSION }} -X main.gitCommit={{ GIT_COMMIT }}" \ + ./cmd/cachewd + @echo "✓ Built {{ RELEASE }}/cachewd" + +# Build for Linux (current arch) +build-linux: + #!/usr/bin/env bash + set -e + mkdir -p {{ RELEASE }} + ARCH=$(uname -m) + [[ "$ARCH" == "x86_64" ]] && ARCH="amd64" + [[ "$ARCH" == "aarch64" || "$ARCH" == "arm64" ]] && ARCH="arm64" + echo "Building for linux/${ARCH}..." + GOOS=linux GOARCH=${ARCH} go build -trimpath \ + -o {{ RELEASE }}/cachewd-linux-${ARCH} \ + -ldflags "-s -w -X main.version={{ VERSION }} -X main.gitCommit={{ GIT_COMMIT }}" \ + ./cmd/cachewd + echo "✓ Built {{ RELEASE }}/cachewd-linux-${ARCH}" + +# Build all platforms +build-all: + @mkdir -p {{ RELEASE }} + @echo "Building all platforms..." + @GOOS=darwin GOARCH=arm64 go build -trimpath -o {{ RELEASE }}/cachewd-darwin-arm64 -ldflags "-s -w -X main.version={{ VERSION }} -X main.gitCommit={{ GIT_COMMIT }}" ./cmd/cachewd + @GOOS=darwin GOARCH=amd64 go build -trimpath -o {{ RELEASE }}/cachewd-darwin-amd64 -ldflags "-s -w -X main.version={{ VERSION }} -X main.gitCommit={{ GIT_COMMIT }}" ./cmd/cachewd + @GOOS=linux GOARCH=arm64 go build -trimpath -o {{ RELEASE }}/cachewd-linux-arm64 -ldflags "-s -w -X main.version={{ VERSION }} -X main.gitCommit={{ GIT_COMMIT }}" ./cmd/cachewd + @GOOS=linux GOARCH=amd64 go build -trimpath -o {{ RELEASE }}/cachewd-linux-amd64 -ldflags "-s -w -X main.version={{ VERSION }} -X main.gitCommit={{ GIT_COMMIT }}" ./cmd/cachewd + @echo "✓ Built all platforms" + +# ============================================================================ +# Run +# ============================================================================ + +# Run natively +run: build + @echo "→ Starting cachew at http://localhost:8080" + @mkdir -p state + @{{ RELEASE }}/cachewd --config cachew.hcl + +# Clean up build artifacts +clean: + @echo "Cleaning..." + @rm -rf {{ RELEASE }} + @echo "✓ Cleaned" diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..7ef8329 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,57 @@ +# Build stage +FROM golang:1.25.5-alpine AS builder + +ARG VERSION=dev +ARG GIT_COMMIT=unknown +ARG TARGETOS +ARG TARGETARCH + +WORKDIR /build + +# Install build dependencies +RUN apk add --no-cache git ca-certificates tzdata + +# Copy go mod files first for better caching +COPY go.mod go.sum ./ +RUN go mod download + +# Copy source code +COPY . . + +# Build the binary +RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} \ + go build -trimpath -o cachewd \ + -ldflags "-s -w -X main.version=${VERSION} -X main.gitCommit=${GIT_COMMIT}" \ + ./cmd/cachewd + +# Runtime stage +FROM alpine:3.21 + +SHELL ["/bin/sh", "-o", "pipefail", "-c"] + +# Install runtime dependencies for git operations and TLS +RUN apk add --no-cache ca-certificates git tzdata && \ + addgroup -g 1000 cachew && \ + adduser -D -u 1000 -G cachew cachew + +# Set working directory (config uses relative paths like ./state/cache) +WORKDIR /app + +# Copy binary from builder +COPY --from=builder /build/cachewd /usr/local/bin/cachewd + +# Copy default configuration file +COPY cachew.hcl /app/cachew.hcl + +# Create state directory with proper permissions +RUN mkdir -p /app/state/cache && \ + chown -R cachew:cachew /app + +# Switch to non-root user +USER cachew + +# Bind to all interfaces in Docker (can be overridden with CACHEW_BIND env var) +ENV CACHEW_BIND=0.0.0.0:8080 + +ENTRYPOINT ["/usr/local/bin/cachewd"] +CMD ["--config", "/app/cachew.hcl"] diff --git a/docker/Justfile b/docker/Justfile new file mode 100644 index 0000000..d6e4704 --- /dev/null +++ b/docker/Justfile @@ -0,0 +1,45 @@ +set positional-arguments := true +set shell := ["bash", "-c"] + +# Configuration +ROOT := `git rev-parse --show-toplevel 2>/dev/null || echo "."` +TAG := `git rev-parse --short HEAD 2>/dev/null || echo "dev"` +VERSION := `git describe --tags --always --dirty 2>/dev/null || echo "dev"` +GIT_COMMIT := `git rev-parse HEAD 2>/dev/null || echo "unknown"` +DOCKERFILE := ROOT + "/docker/Dockerfile" + +# Build Docker image for local development +build: + @echo "Building Docker image..." + @docker build -f {{ DOCKERFILE }} \ + --build-arg VERSION={{ VERSION }} \ + --build-arg GIT_COMMIT={{ GIT_COMMIT }} \ + -t cachew:local \ + {{ ROOT }} + @echo "✓ Built cachew:local" + +# Build multi-arch Docker image (builds for amd64 + arm64, stays in cache) +build-multi: + @docker buildx create --use --driver docker-container --driver-opt image=moby/buildkit:v0.17.3 --name multi-arch-cachew 2>/dev/null || docker buildx use multi-arch-cachew + @echo "Building multi-arch image..." + @docker buildx build \ + --platform linux/amd64,linux/arm64 \ + -f {{ DOCKERFILE }} \ + --build-arg VERSION={{ VERSION }} \ + --build-arg GIT_COMMIT={{ GIT_COMMIT }} \ + -t cachew:{{ TAG }} \ + {{ ROOT }} + @echo "✓ Built multi-arch image (in cache)" + +# Run in Docker (usage: just docker run [log_level]) +run log_level="info": + @just build + @echo "→ Starting cachew at http://localhost:8080 (log-level={{ log_level }})" + @docker run --rm -it -p 8080:8080 -v {{ ROOT }}/state:/app/state --name cachew cachew:local --log-level={{ log_level }} + +# Clean up Docker images +clean: + @echo "Cleaning Docker images..." + @docker rmi cachew:local 2>/dev/null || true + @docker rmi cachew:{{ TAG }} 2>/dev/null || true + @echo "✓ Cleaned"