diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..43f73f69 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,15 @@ +# dirs +.git +.github +.claude +.idea +docs + +# files +.env* +*.md +.goreleaser.yaml +docker-compose*.yml +LICENSE +LICENSE.md +version.json diff --git a/.github/workflows/publish-ghcr.yml b/.github/workflows/publish-ghcr.yml new file mode 100644 index 00000000..5ad8de04 --- /dev/null +++ b/.github/workflows/publish-ghcr.yml @@ -0,0 +1,244 @@ +name: Container + +on: + push: + branches: + - main + tags: + - 'v*' + workflow_run: + workflows: [Releaser] + types: + - completed + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + # PR Build Check — validate Dockerfile compiles, single platform, no push + build-check: + if: github.event_name == 'pull_request' + name: Build Check + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build prod (amd64 only, no push) + uses: docker/build-push-action@v6 + with: + context: . + push: false + platforms: linux/amd64 + cache-from: type=gha + build-args: | + VERSION=pr-${{ github.event.pull_request.number }} + COMMIT=${{ github.event.pull_request.head.sha }} + DATE=${{ github.event.pull_request.updated_at }} + BUILT_BY=ghcr-pr + + - name: Build dev (amd64 only, no push) + uses: docker/build-push-action@v6 + with: + context: . + file: Dockerfile.dev + push: false + platforms: linux/amd64 + cache-from: type=gha,scope=dev + build-args: | + VERSION=pr-${{ github.event.pull_request.number }} + COMMIT=${{ github.event.pull_request.head.sha }} + DATE=${{ github.event.pull_request.updated_at }} + BUILT_BY=ghcr-pr-dev + + # Extract release ref from Releaser workflow artifacts + prepare-checkout: + if: github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success' + name: Prepare ref + runs-on: ubuntu-latest + outputs: + ref: ${{ steps.releaser.outputs.version }} + steps: + - name: Get ref from Releaser + id: releaser + uses: ipdxco/unified-github-workflows/.github/actions/inspect-releaser@v1.0 + with: + artifacts-url: ${{ github.event.workflow_run.artifacts_url }} + + # Publish on push to main branch + publish-main: + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + name: Publish (main) + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Container registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=raw,value=main + type=sha,prefix=sha-,format=short + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + push: true + platforms: linux/amd64,linux/arm64 + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + build-args: | + VERSION=main + COMMIT=${{ github.sha }} + DATE=${{ github.event.head_commit.timestamp }} + BUILT_BY=ghcr-main + + # Publish dev container on push to main — adds delve + randdir + diagnostic tools + publish-main-dev: + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + name: Publish dev (main) + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - name: Checkout + uses: actions/checkout@v4 + + # Dockerfile.dev's runtime stage uses debian and runs apt-get, so the + # arm64 build needs QEMU. (The prod Dockerfile avoids this because its + # runtime stage has no RUN commands.) + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Container registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=raw,value=main-dev + type=sha,prefix=sha-,suffix=-dev,format=short + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + file: Dockerfile.dev + push: true + platforms: linux/amd64,linux/arm64 + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha,scope=dev + cache-to: type=gha,mode=max,scope=dev + build-args: | + VERSION=main + COMMIT=${{ github.sha }} + DATE=${{ github.event.head_commit.timestamp }} + BUILT_BY=ghcr-main-dev + + # Publish on release tag (v*) — direct push or via Releaser + publish-release: + name: Publish (release) + needs: [prepare-checkout] + # always() allows running even when prepare-checkout was skipped (direct tag push) + if: | + always() && !cancelled() && !failure() && + ((github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')) || + (github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success')) + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - name: Determine ref + id: ref + run: | + if [ "${{ github.event_name }}" = "workflow_run" ]; then + echo "ref=${{ needs.prepare-checkout.outputs.ref }}" >> $GITHUB_OUTPUT + else + echo "ref=${{ github.ref }}" >> $GITHUB_OUTPUT + fi + + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ steps.ref.outputs.ref }} + + - name: Extract build metadata from git + id: buildmeta + run: | + echo "commit=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT + echo "date=$(git log -1 --format=%cI)" >> $GITHUB_OUTPUT + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Container registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=semver,pattern={{version}},value=${{ steps.ref.outputs.ref }},match=v([0-9].*) + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + push: true + platforms: linux/amd64,linux/arm64 + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + build-args: | + VERSION=v${{ steps.meta.outputs.version }} + COMMIT=${{ steps.buildmeta.outputs.commit }} + DATE=${{ steps.buildmeta.outputs.date }} + BUILT_BY=ghcr-release diff --git a/.github/workflows/release-binaries.yml b/.github/workflows/release-binaries.yml index 3944a127..0d21bafb 100644 --- a/.github/workflows/release-binaries.yml +++ b/.github/workflows/release-binaries.yml @@ -9,7 +9,6 @@ on: permissions: contents: write - packages: write jobs: bin-releaser: @@ -24,17 +23,7 @@ jobs: uses: actions/setup-go@v5 with: go-version: "1.25.x" - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - name: Log in to the Container registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: Release Binaries and Containers + - name: Release Binaries uses: goreleaser/goreleaser-action@v6 with: distribution: goreleaser diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 9fc4f157..7a0c089f 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -50,38 +50,3 @@ release: changelog: disable: true - -dockers: - # AMD64 image - - image_templates: - - "ghcr.io/storacha/guppy:{{ .Version }}-amd64" - dockerfile: Dockerfile.release - use: buildx - build_flag_templates: - - "--platform=linux/amd64" - - "--label=org.opencontainers.image.created={{.Date}}" - - "--label=org.opencontainers.image.title={{.ProjectName}}" - - "--label=org.opencontainers.image.revision={{.FullCommit}}" - - "--label=org.opencontainers.image.version={{.Version}}" - - "--label=org.opencontainers.image.source={{.GitURL}}" - goarch: amd64 - - # ARM64 image - - image_templates: - - "ghcr.io/storacha/guppy:{{ .Version }}-arm64" - dockerfile: Dockerfile.release - use: buildx - build_flag_templates: - - "--platform=linux/arm64" - - "--label=org.opencontainers.image.created={{.Date}}" - - "--label=org.opencontainers.image.title={{.ProjectName}}" - - "--label=org.opencontainers.image.revision={{.FullCommit}}" - - "--label=org.opencontainers.image.version={{.Version}}" - - "--label=org.opencontainers.image.source={{.GitURL}}" - goarch: arm64 - -docker_manifests: - - name_template: "ghcr.io/storacha/guppy:{{ .Version }}" - image_templates: - - "ghcr.io/storacha/guppy:{{ .Version }}-amd64" - - "ghcr.io/storacha/guppy:{{ .Version }}-arm64" diff --git a/Dockerfile b/Dockerfile index 7c278eab..5d361eb0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,89 +1,33 @@ -# ============================================ -# Build stage (shared) -# ============================================ -FROM golang:1.25.3-trixie AS build +# Build stage — native platform for fast cross-compilation +FROM --platform=$BUILDPLATFORM golang:1.25-bookworm AS build -# Docker sets TARGETARCH automatically during multi-platform builds ARG TARGETARCH +ARG TARGETOS=linux +ARG VERSION=dev +ARG COMMIT=unknown +ARG DATE=unknown +ARG BUILT_BY=docker -WORKDIR /go/src/guppy +WORKDIR /src -COPY go.* . +COPY go.mod go.sum ./ RUN go mod download -COPY . . - -# Production build - with symbol stripping -RUN CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} make guppy-prod - -# ============================================ -# Debug build stage -# ============================================ -FROM build AS build-debug - -ARG TARGETARCH - -# Install delve debugger and randdir tool for target architecture -RUN GOARCH=${TARGETARCH} go install github.com/go-delve/delve/cmd/dlv@latest && \ - GOARCH=${TARGETARCH} go install github.com/storacha/randdir@latest - -# Debug build - no optimizations, no inlining -RUN CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} make guppy-debug - -# ============================================ -# Production image -# ============================================ -FROM debian:bookworm-slim AS prod -RUN apt-get update && apt-get install -y --no-install-recommends \ - ca-certificates \ - curl \ - && rm -rf /var/lib/apt/lists/* - -COPY --from=build /go/src/guppy/guppy /usr/bin/guppy - -ENTRYPOINT ["/usr/bin/guppy"] - -# ============================================ -# Development image -# ============================================ -FROM debian:bookworm-slim AS dev - -RUN apt-get update && apt-get install -y --no-install-recommends \ - ca-certificates \ - curl \ - # Shell experience - bash-completion \ - less \ - vim-tiny \ - # Process debugging - procps \ - htop \ - strace \ - # Network debugging - iputils-ping \ - dnsutils \ - net-tools \ - tcpdump \ - # Data tools - jq \ - && rm -rf /var/lib/apt/lists/* - -# Delve debugger and randdir tool -COPY --from=build-debug /go/bin/dlv /usr/bin/dlv -COPY --from=build-debug /go/bin/randdir /usr/bin/randdir +COPY . . -# Debug binary (with symbols, no optimizations) -COPY --from=build-debug /go/src/guppy/guppy /usr/bin/guppy +RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build \ + -ldflags="-s -w \ + -X github.com/storacha/guppy/pkg/build.version=${VERSION} \ + -X github.com/storacha/guppy/pkg/build.Commit=${COMMIT} \ + -X github.com/storacha/guppy/pkg/build.Date=${DATE} \ + -X github.com/storacha/guppy/pkg/build.BuiltBy=${BUILT_BY}" \ + -o /app . -# Create data directories -RUN mkdir -p /root/.storacha/guppy /root/.config/guppy +# Runtime stage — alpine ships wget, CA certs, /bin/sh, and the nobody user +FROM alpine:latest AS prod -WORKDIR /root +USER nobody -# Shell niceties -ENV TERM=xterm-256color -RUN echo 'alias ll="ls -la"' >> /etc/bash.bashrc && \ - echo 'PS1="\[\e[32m\][guppy-dev]\[\e[m\] \w# "' >> /etc/bash.bashrc +COPY --from=build /app /usr/bin/guppy -SHELL ["/bin/bash", "-c"] ENTRYPOINT ["/usr/bin/guppy"] diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 00000000..5566427a --- /dev/null +++ b/Dockerfile.dev @@ -0,0 +1,80 @@ +# Development/Debug Dockerfile for guppy +# Build: docker build -f Dockerfile.dev -t guppy:dev . +# Run: docker run -p 2345:2345 guppy:dev [args...] +# Connect debugger to localhost:2345 + +FROM --platform=$BUILDPLATFORM golang:1.25-bookworm AS build + +ARG TARGETARCH +ARG TARGETOS=linux +ARG VERSION=dev +ARG COMMIT=unknown +ARG DATE=unknown +ARG BUILT_BY=docker-dev + +WORKDIR /src + +# Install delve debugger and randdir tool for the target architecture. +# When cross-compiling, `go install` drops binaries under +# $GOPATH/bin/${GOOS}_${GOARCH}/; when native, under $GOPATH/bin/. +# Normalize both cases by copying into a stable /tools path. +RUN GOARCH=${TARGETARCH} go install github.com/go-delve/delve/cmd/dlv@latest && \ + GOARCH=${TARGETARCH} go install github.com/storacha/randdir@latest && \ + mkdir -p /tools && \ + find /go/bin -maxdepth 2 -type f -name dlv -exec cp {} /tools/dlv \; && \ + find /go/bin -maxdepth 2 -type f -name randdir -exec cp {} /tools/randdir \; + +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . + +# Debug build — no optimizations, no inlining, full symbols +RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build \ + -gcflags="all=-N -l" \ + -ldflags="-X github.com/storacha/guppy/pkg/build.version=${VERSION} \ + -X github.com/storacha/guppy/pkg/build.Commit=${COMMIT} \ + -X github.com/storacha/guppy/pkg/build.Date=${DATE} \ + -X github.com/storacha/guppy/pkg/build.BuiltBy=${BUILT_BY}" \ + -o /app . + +FROM debian:bookworm-slim AS dev + +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates \ + curl \ + # Shell experience + bash-completion \ + less \ + vim-tiny \ + # Process debugging + procps \ + htop \ + strace \ + # Network debugging + iputils-ping \ + dnsutils \ + net-tools \ + tcpdump \ + # Data tools + jq \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=build /app /usr/bin/guppy +COPY --from=build /tools/dlv /usr/bin/dlv +COPY --from=build /tools/randdir /usr/bin/randdir + +# Create data directories +RUN mkdir -p /root/.storacha/guppy /root/.config/guppy + +WORKDIR /root + +ENV TERM=xterm-256color +RUN echo 'alias ll="ls -la"' >> /etc/bash.bashrc && \ + echo 'PS1="\[\e[32m\][guppy-dev]\[\e[m\] \w# "' >> /etc/bash.bashrc + +SHELL ["/bin/bash", "-c"] + +EXPOSE 2345 + +ENTRYPOINT ["dlv", "exec", "/usr/bin/guppy", "--headless", "--listen=:2345", "--api-version=2", "--accept-multiclient", "--"] diff --git a/Dockerfile.release b/Dockerfile.release deleted file mode 100644 index b0fa5b92..00000000 --- a/Dockerfile.release +++ /dev/null @@ -1,7 +0,0 @@ -FROM debian:bookworm-slim -RUN apt-get update && apt-get install -y --no-install-recommends \ - ca-certificates \ - curl \ - && rm -rf /var/lib/apt/lists/* -COPY guppy /usr/bin/guppy -ENTRYPOINT ["/usr/bin/guppy"] diff --git a/Makefile b/Makefile index 3e7b835d..f384f42f 100644 --- a/Makefile +++ b/Makefile @@ -55,7 +55,18 @@ docker-setup: $(DOCKER) buildx create --name multiarch --use 2>/dev/null || $(DOCKER) buildx use multiarch docker-prod: docker-setup - $(DOCKER) buildx build --platform linux/amd64,linux/arm64 --target prod -t guppy:latest . + $(DOCKER) buildx build --platform linux/amd64,linux/arm64 --target prod \ + --build-arg VERSION=$(VERSION) \ + --build-arg COMMIT=$(COMMIT) \ + --build-arg DATE=$(DATE) \ + --build-arg BUILT_BY=make \ + -t guppy:latest . docker-dev: docker-setup - $(DOCKER) buildx build --platform linux/amd64,linux/arm64 --target dev -t guppy:dev . + $(DOCKER) buildx build --platform linux/amd64,linux/arm64 \ + -f Dockerfile.dev \ + --build-arg VERSION=$(VERSION) \ + --build-arg COMMIT=$(COMMIT) \ + --build-arg DATE=$(DATE) \ + --build-arg BUILT_BY=make-debug \ + -t guppy:dev .