diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index d11b1980..00000000 --- a/.dockerignore +++ /dev/null @@ -1 +0,0 @@ -.db diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 64fed3fc..71a7caa2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -54,6 +54,9 @@ jobs: go-version-file: 'go.mod' cache: true + - name: Set up ko + uses: ko-build/setup-ko@v0.9 + - name: Run all tests run: make test-all diff --git a/.github/workflows/deploy-staging.yml b/.github/workflows/deploy-staging.yml index 09116daa..524e951e 100644 --- a/.github/workflows/deploy-staging.yml +++ b/.github/workflows/deploy-staging.yml @@ -14,8 +14,8 @@ env: PULUMI_VERSION: "3.188.0" jobs: - docker-push: - name: Build Docker Image + ko-push: + name: Build and Push Image with ko runs-on: ubuntu-latest permissions: contents: read @@ -24,8 +24,14 @@ jobs: - name: Checkout repository uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 + - name: Set up Go + uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 + with: + go-version-file: 'go.mod' + cache: true + + - name: Set up ko + uses: ko-build/setup-ko@v0.9 - name: Log in to Container Registry uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef @@ -34,40 +40,31 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Get build timestamp - id: build-time - run: echo "timestamp=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> $GITHUB_OUTPUT - - - name: Extract metadata - id: meta - uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f - with: - images: ghcr.io/${{ github.repository }} - tags: | - type=sha,prefix=main-{{date 'YYYYMMDD'}}-,enable={{is_default_branch}} - type=raw,value=main,enable={{is_default_branch}} + - name: Get build timestamp and tags + id: build-info + run: | + echo "timestamp=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> $GITHUB_OUTPUT + echo "date=$(date -u +%Y%m%d)" >> $GITHUB_OUTPUT + echo "short_sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT - - name: Build and push Docker image - uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 - with: - context: . - file: ./Dockerfile - platforms: linux/amd64,linux/arm64 - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max - build-args: | - VERSION=${{ steps.meta.outputs.version }} - GIT_COMMIT=${{ github.sha }} - BUILD_TIME=${{ steps.build-time.outputs.timestamp }} + - name: Build and push with ko + env: + KO_DOCKER_REPO: ghcr.io/${{ github.repository }} + VERSION: main-${{ steps.build-info.outputs.date }}-${{ steps.build-info.outputs.short_sha }} + GIT_COMMIT: ${{ github.sha }} + BUILD_TIME: ${{ steps.build-info.outputs.timestamp }} + run: | + # Build and push multi-platform image + ko build ./cmd/registry \ + --bare \ + --platform=linux/amd64,linux/arm64 \ + --tags=main-${{ steps.build-info.outputs.date }}-${{ steps.build-info.outputs.short_sha }},main deploy-staging: name: Deploy to Staging runs-on: ubuntu-latest environment: staging - needs: docker-push + needs: ko-push concurrency: group: deploy-staging cancel-in-progress: false diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 86b81ba1..8bd9b997 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -41,15 +41,22 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - docker: + ko-push: + name: Build and Push Image with ko runs-on: ubuntu-latest needs: goreleaser steps: - name: Checkout uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 + - name: Set up Go + uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 + with: + go-version-file: 'go.mod' + cache: true + + - name: Set up ko + uses: ko-build/setup-ko@v0.9 - name: Log in to Container Registry uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef @@ -58,31 +65,23 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Get build timestamp - id: build-time - run: echo "timestamp=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> $GITHUB_OUTPUT - - - name: Extract metadata - id: meta - uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f - with: - images: ghcr.io/${{ github.repository }} - tags: | - type=semver,pattern={{version}} - type=raw,value=latest + - name: Get build timestamp and version + id: build-info + run: | + echo "timestamp=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> $GITHUB_OUTPUT + # Extract version from tag (e.g., refs/tags/v1.2.3 -> v1.2.3) + VERSION="${GITHUB_REF#refs/tags/}" + echo "version=$VERSION" >> $GITHUB_OUTPUT - - name: Build and push Docker image - uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 - with: - context: . - file: ./Dockerfile - platforms: linux/amd64,linux/arm64 - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max - build-args: | - VERSION=${{ steps.meta.outputs.version }} - GIT_COMMIT=${{ github.sha }} - BUILD_TIME=${{ steps.build-time.outputs.timestamp }} + - name: Build and push with ko + env: + KO_DOCKER_REPO: ghcr.io/${{ github.repository }} + VERSION: ${{ steps.build-info.outputs.version }} + GIT_COMMIT: ${{ github.sha }} + BUILD_TIME: ${{ steps.build-info.outputs.timestamp }} + run: | + # Build and push multi-platform image with version tag and latest + ko build ./cmd/registry \ + --bare \ + --platform=linux/amd64,linux/arm64 \ + --tags=${{ steps.build-info.outputs.version }},latest diff --git a/.ko.dockerignore b/.ko.dockerignore new file mode 100644 index 00000000..b279793e --- /dev/null +++ b/.ko.dockerignore @@ -0,0 +1,52 @@ +# Build artifacts +bin/ +*.exe +*.dll +*.so +*.dylib + +# Test and coverage files +coverage.out +coverage.html +*.test + +# Database files +*.db +.db/ + +# Deployment files +deploy/ + +# Documentation +docs/ + +# Tests +tests/ + +# Git +.git/ +.gitignore + +# GitHub Actions +.github/ + +# Docker files (no longer used) +Dockerfile +.dockerignore +docker-compose.yml + +# IDE and editor files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# Temporary files +tmp/ +temp/ +*.tmp + +# OS files +.DS_Store +Thumbs.db \ No newline at end of file diff --git a/.ko.yaml b/.ko.yaml new file mode 100644 index 00000000..e6f1d1b2 --- /dev/null +++ b/.ko.yaml @@ -0,0 +1,32 @@ +# ko configuration for MCP Registry +# Documentation: https://ko.build/configuration/ + +# Default base image for all builds +# Using Chainguard's static image for minimal, secure containers (~2MB) +defaultBaseImage: cgr.dev/chainguard/static:latest + +# Default platforms for multi-architecture builds +defaultPlatforms: + - linux/amd64 + - linux/arm64 + +# Build configuration +builds: + - id: registry + # Main package to build + main: ./cmd/registry + # Inject version information at build time via ldflags + ldflags: + - -s -w + - -X main.Version={{ .Env.VERSION }} + - -X main.GitCommit={{ .Env.GIT_COMMIT }} + - -X main.BuildTime={{ .Env.BUILD_TIME }} + env: + - CGO_ENABLED=0 + +# SBOM (Software Bill of Materials) configuration +sbom: spdx + +# Base import path handling +# Set to false to preserve full import paths in image names +baseImportPaths: false diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 9117e8e9..00000000 --- a/Dockerfile +++ /dev/null @@ -1,42 +0,0 @@ -FROM golang:1.24-alpine AS builder -WORKDIR /app - -# Copy go mod files first and download dependencies -# This creates a separate layer that only invalidates when dependencies change -COPY go.mod go.sum ./ -RUN go mod download - -# Copy the rest of the source code -COPY . . - -ARG GO_BUILD_TAGS -ARG VERSION=dev -ARG GIT_COMMIT=unknown -ARG BUILD_TIME=unknown - -RUN go build \ - ${GO_BUILD_TAGS:+-tags="$GO_BUILD_TAGS"} \ - -ldflags="-X main.Version=${VERSION} -X main.GitCommit=${GIT_COMMIT} -X main.BuildTime=${BUILD_TIME}" \ - -o /build/registry ./cmd/registry - -FROM alpine:latest -WORKDIR /app -COPY --from=builder /build/registry . -COPY --from=builder /app/data/seed.json /app/data/seed.json - -# Create a non-privileged user that the app will run under. -# See https://docs.docker.com/go/dockerfile-user-best-practices/ -ARG UID=10001 -RUN adduser \ - --disabled-password \ - --gecos "" \ - --home "/nonexistent" \ - --shell "/sbin/nologin" \ - --no-create-home \ - --uid "${UID}" \ - appuser - -USER appuser -EXPOSE 8080 - -ENTRYPOINT ["./registry"] diff --git a/Makefile b/Makefile index 65254594..0dca9a96 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: help build test test-unit test-integration test-endpoints test-publish test-all lint lint-fix validate validate-schemas validate-examples check dev-compose clean publisher generate-schema check-schema +.PHONY: help build test test-unit test-integration test-endpoints test-publish test-all lint lint-fix validate validate-schemas validate-examples check ko-build ko-rebuild dev-compose dev-down clean publisher generate-schema check-schema # Default target help: ## Show this help message @@ -83,11 +83,23 @@ check: dev-down lint validate test-all ## Run all checks (lint, validate, unit t @echo "All checks passed!" # Development targets -dev-compose: ## Start development environment with Docker Compose (builds image automatically) +ko-build: ## Build registry image using ko (loads into local docker daemon) + @echo "Building registry with ko..." + VERSION=dev-$$(git rev-parse --short HEAD) \ GIT_COMMIT=$$(git rev-parse HEAD) \ - GIT_COMMIT_SHORT=$$(git rev-parse --short HEAD) \ BUILD_TIME=$$(date -u +%Y-%m-%dT%H:%M:%SZ) \ - docker compose up --build + KO_DOCKER_REPO=ko.local \ + ko build --preserve-import-paths --tags=dev --sbom=none ./cmd/registry + @echo "Image built: ko.local/github.com/modelcontextprotocol/registry/cmd/registry:dev" + +ko-rebuild: ## Rebuild with ko and restart registry container + @$(MAKE) ko-build + @echo "Restarting registry container..." + @docker compose restart registry + +dev-compose: ko-build ## Start development environment with Docker Compose (builds with ko first) + @echo "Starting Docker Compose..." + docker compose up dev-down: ## Stop development environment docker compose down diff --git a/README.md b/README.md index f3972cdb..c26fc15b 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,8 @@ Often (but not always) ideas flow through this pipeline: #### Pre-requisites - **Docker** -- **Go 1.24.x** +- **Go 1.24.x** +- **ko** - Container image builder for Go ([installation instructions](https://ko.build/install/)) - **golangci-lint v2.4.0** #### Running the server @@ -44,6 +45,8 @@ make dev-compose This starts the registry at [`localhost:8080`](http://localhost:8080) with PostgreSQL. The database uses ephemeral storage and is reset each time you restart the containers, ensuring a clean state for development and testing. +**Note:** The registry uses [ko](https://ko.build) to build container images. The `make dev-compose` command automatically builds the registry image with ko and loads it into your local Docker daemon before starting the services. + By default, the registry seeds from the production API with a filtered subset of servers (to keep startup fast). This ensures your local environment mirrors production behavior and all seed data passes validation. For offline development you can seed from a file without validation with `MCP_REGISTRY_SEED_FROM=data/seed.json MCP_REGISTRY_ENABLE_REGISTRY_VALIDATION=false make dev-compose`. The setup can be configured with environment variables in [docker-compose.yml](./docker-compose.yml) - see [.env.example](./.env.example) for a reference. diff --git a/docker-compose.yml b/docker-compose.yml index 9b02e252..7eb71c25 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,14 +1,9 @@ services: registry: - image: modelcontextprotocol/registry:dev - pull_policy: never + # Built using ko (see Makefile dev-compose target) + # Image format: ko.local/: + image: ko.local/github.com/modelcontextprotocol/registry/cmd/registry:dev container_name: registry - build: - dockerfile: Dockerfile - args: - - VERSION=dev-${GIT_COMMIT_SHORT:-unknown} - - GIT_COMMIT=${GIT_COMMIT:-unknown} - - BUILD_TIME=${BUILD_TIME:-unknown} depends_on: postgres: condition: service_healthy @@ -30,6 +25,8 @@ services: - MCP_REGISTRY_ENABLE_REGISTRY_VALIDATION=${MCP_REGISTRY_ENABLE_REGISTRY_VALIDATION:-true} ports: - 8080:8080 + volumes: + - ./data:/data:ro restart: "unless-stopped" postgres: diff --git a/tests/integration/run.sh b/tests/integration/run.sh index f206b884..a91839bb 100755 --- a/tests/integration/run.sh +++ b/tests/integration/run.sh @@ -24,8 +24,19 @@ cleanup() { go build -o ./bin/publisher ./cmd/publisher -# Build the registry image with both tags, forcing a rebuild to ensure latest code -docker build --no-cache -t registry -t modelcontextprotocol/registry:dev . +# Build the registry image with ko, forcing a rebuild to ensure latest code +# Use Alpine for integration tests because health checks need wget (production uses static image) +echo "Building registry image with ko..." +VERSION=dev-$(git rev-parse --short HEAD) \ +GIT_COMMIT=$(git rev-parse HEAD) \ +BUILD_TIME=$(date -u +%Y-%m-%dT%H:%M:%SZ) \ +KO_DOCKER_REPO=ko.local \ +KO_DEFAULTBASEIMAGE=alpine:latest \ +ko build --preserve-import-paths --tags=dev --sbom=none ./cmd/registry + +# Tag the ko.local image with the tags expected by docker-compose +docker tag ko.local/github.com/modelcontextprotocol/registry/cmd/registry:dev registry +docker tag ko.local/github.com/modelcontextprotocol/registry/cmd/registry:dev modelcontextprotocol/registry:dev trap cleanup EXIT