Compare commits
236 changed files with 20082 additions and 28840 deletions
1
.dockerignore
Normal file
1
.dockerignore
Normal file
|
|
@ -0,0 +1 @@
|
|||
.db
|
||||
16
.env.example
16
.env.example
|
|
@ -8,13 +8,7 @@ MCP_REGISTRY_VERSION=dev
|
|||
MCP_REGISTRY_DATABASE_URL=postgres://username:password@localhost:5432/mcp-registry
|
||||
|
||||
# Path or URL to import seed data (supports local files and HTTP URLs)
|
||||
# For offline development, use: data/seed.json
|
||||
MCP_REGISTRY_SEED_FROM=https://registry.modelcontextprotocol.io/v0/servers
|
||||
|
||||
# Validate seed data against the registry rules on import (default: true)
|
||||
# Set to false for offline development when seeding from a local file, since
|
||||
# data/seed.json is not guaranteed to pass validation
|
||||
MCP_REGISTRY_ENABLE_REGISTRY_VALIDATION=true
|
||||
MCP_REGISTRY_SEED_FROM=data/seed.json
|
||||
|
||||
# GitHub OAuth configuration
|
||||
# These creds are for local development with the 'MCP Registry Login (Local)' GitHub App
|
||||
|
|
@ -32,14 +26,6 @@ MCP_REGISTRY_JWT_PRIVATE_KEY=bb2c6b424005acd5df47a9e2c87f446def86dd740c888ea3efb
|
|||
# This should be disabled in prod
|
||||
MCP_REGISTRY_ENABLE_ANONYMOUS_AUTH=false
|
||||
|
||||
# GitHub OIDC token exchange (for `mcp-publisher login github-oidc`)
|
||||
# Expected `aud` claim on incoming GitHub Actions OIDC tokens. Must equal the
|
||||
# scheme + host that publishers pass via `--registry` (e.g. `https://registry.example.com`).
|
||||
# The publisher CLI derives the audience from `--registry` automatically; the server
|
||||
# rejects all OIDC exchanges if this is unset, so set it on every deployment that
|
||||
# accepts GitHub Actions publishes.
|
||||
MCP_REGISTRY_GITHUB_OIDC_AUDIENCE=
|
||||
|
||||
# Google Cloud Identity OIDC configuration for admin access
|
||||
# Enable OIDC authentication for @modelcontextprotocol.io admin accounts
|
||||
MCP_REGISTRY_OIDC_ENABLED=false
|
||||
|
|
|
|||
13
.github/copilot-instructions.md
vendored
13
.github/copilot-instructions.md
vendored
|
|
@ -1,13 +0,0 @@
|
|||
# Copilot Instructions for MCP Registry
|
||||
|
||||
## Important: Publishing MCP servers
|
||||
|
||||
The `data/seed.json` file is seed data for local development only. Do NOT create pull requests that add or modify server entries in `data/seed.json` to publish a server.
|
||||
|
||||
To publish an MCP server to the registry, use the `mcp-publisher` CLI tool. See `docs/modelcontextprotocol-io/quickstart.mdx` for instructions.
|
||||
|
||||
## Development
|
||||
|
||||
- Use `make` targets where possible (run `make help` to see available targets)
|
||||
- Run `make check` to run lint, schema validation, and all tests (this also stops any running dev-compose environment)
|
||||
- Run `make dev-compose` to start the local development environment
|
||||
24
.github/dependabot.yml
vendored
24
.github/dependabot.yml
vendored
|
|
@ -2,29 +2,17 @@ version: 2
|
|||
updates:
|
||||
- package-ecosystem: gomod
|
||||
directory: /
|
||||
groups:
|
||||
go-dependencies:
|
||||
patterns:
|
||||
- "*"
|
||||
schedule:
|
||||
interval: weekly
|
||||
- package-ecosystem: gomod
|
||||
directory: /deploy
|
||||
groups:
|
||||
deploy-go-dependencies:
|
||||
patterns:
|
||||
- "*"
|
||||
schedule:
|
||||
interval: weekly
|
||||
interval: daily
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
groups:
|
||||
github-actions:
|
||||
actions:
|
||||
patterns:
|
||||
- "*"
|
||||
schedule:
|
||||
interval: weekly
|
||||
- package-ecosystem: docker
|
||||
directory: /
|
||||
interval: daily
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: weekly
|
||||
interval: "daily"
|
||||
|
|
|
|||
14
.github/workflows/cancel-pulumi-lock.yml
vendored
14
.github/workflows/cancel-pulumi-lock.yml
vendored
|
|
@ -24,10 +24,10 @@ jobs:
|
|||
environment: ${{ inputs.environment }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
|
||||
|
||||
- name: Setup Pulumi
|
||||
uses: pulumi/actions@8e5e406f4007fca908480587cb9893c07090f58d
|
||||
uses: pulumi/actions@cc7494be991dba0978f7ffafaf995b0449a0998e
|
||||
with:
|
||||
pulumi-version: ${{ env.PULUMI_VERSION }}
|
||||
|
||||
|
|
@ -58,19 +58,15 @@ jobs:
|
|||
- name: Cancel Pulumi Lock (Staging)
|
||||
if: inputs.environment == 'staging'
|
||||
working-directory: ./deploy
|
||||
env:
|
||||
PULUMI_STAGING_PASSPHRASE: ${{ secrets.PULUMI_STAGING_PASSPHRASE }}
|
||||
run: |
|
||||
echo "$PULUMI_STAGING_PASSPHRASE" > passphrase.staging.txt
|
||||
echo "${{ secrets.PULUMI_STAGING_PASSPHRASE }}" > passphrase.staging.txt
|
||||
pulumi login gs://mcp-registry-staging-pulumi-state
|
||||
PULUMI_CONFIG_PASSPHRASE_FILE=passphrase.staging.txt pulumi cancel --stack gcpStaging --yes
|
||||
|
||||
- name: Cancel Pulumi Lock (Production)
|
||||
if: inputs.environment == 'production'
|
||||
working-directory: ./deploy
|
||||
env:
|
||||
PULUMI_PROD_PASSPHRASE: ${{ secrets.PULUMI_PROD_PASSPHRASE }}
|
||||
run: |
|
||||
echo "$PULUMI_PROD_PASSPHRASE" > passphrase.prod.txt
|
||||
echo "${{ secrets.PULUMI_PROD_PASSPHRASE }}" > passphrase.prod.txt
|
||||
pulumi login gs://mcp-registry-prod-pulumi-state
|
||||
PULUMI_CONFIG_PASSPHRASE_FILE=passphrase.prod.txt pulumi cancel --stack gcpProd --yes
|
||||
PULUMI_CONFIG_PASSPHRASE_FILE=passphrase.prod.txt pulumi cancel --stack gcpProd --yes
|
||||
61
.github/workflows/ci.yml
vendored
61
.github/workflows/ci.yml
vendored
|
|
@ -16,18 +16,32 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e
|
||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00
|
||||
with:
|
||||
go-version: 'stable'
|
||||
cache: true
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
- name: Cache Go modules
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830
|
||||
with:
|
||||
path: |
|
||||
~/.cache/go-build
|
||||
~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ github.sha }}-${{ hashFiles('**/go.sum', '.golangci.yml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-
|
||||
|
||||
- name: Download dependencies
|
||||
run: go mod download
|
||||
|
||||
- name: Install golangci-lint
|
||||
run: |
|
||||
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.4.0
|
||||
|
||||
- name: Run lint
|
||||
uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a
|
||||
with:
|
||||
version: v2.13.1
|
||||
run: make lint
|
||||
|
||||
- name: Validate schemas and examples
|
||||
run: make validate
|
||||
|
|
@ -35,12 +49,10 @@ jobs:
|
|||
- name: Build application
|
||||
run: make build
|
||||
|
||||
- name: Run govulncheck
|
||||
uses: golang/govulncheck-action@v1
|
||||
with:
|
||||
go-version-input: 'stable'
|
||||
go-package: ./...
|
||||
repo-checkout: false
|
||||
- name: Check for vulnerabilities
|
||||
run: |
|
||||
go install golang.org/x/vuln/cmd/govulncheck@latest
|
||||
govulncheck ./...
|
||||
|
||||
# All Tests
|
||||
tests:
|
||||
|
|
@ -48,22 +60,31 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e
|
||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00
|
||||
with:
|
||||
go-version: 'stable'
|
||||
cache: true
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
- name: Set up ko
|
||||
uses: ko-build/setup-ko@v0.10
|
||||
- name: Cache Go modules
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830
|
||||
with:
|
||||
path: |
|
||||
~/.cache/go-build
|
||||
~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-
|
||||
|
||||
- name: Download dependencies
|
||||
run: go mod download
|
||||
|
||||
- name: Run all tests
|
||||
run: make test-all
|
||||
|
||||
- name: Upload coverage artifacts
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
|
||||
with:
|
||||
name: coverage-report
|
||||
path: |
|
||||
|
|
|
|||
15
.github/workflows/claude.yml
vendored
15
.github/workflows/claude.yml
vendored
|
|
@ -26,13 +26,13 @@ jobs:
|
|||
actions: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run Claude Code
|
||||
id: claude
|
||||
uses: anthropics/claude-code-action@v1
|
||||
uses: anthropics/claude-code-action@beta
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
|
||||
|
|
@ -42,7 +42,10 @@ jobs:
|
|||
|
||||
# Trigger when assigned to an issue
|
||||
assignee_trigger: "claude"
|
||||
|
||||
claude_args: |
|
||||
--allowedTools Bash
|
||||
--system-prompt "If posting a comment to GitHub, give a concise summary of the comment at the top and put all the details in a <details> block."
|
||||
|
||||
# Allow Claude to run bash
|
||||
# This should be safe given the repo is already public
|
||||
allowed_tools: "Bash"
|
||||
|
||||
custom_instructions: |
|
||||
If posting a comment to GitHub, give a concise summary of the comment at the top and put all the details in a <details> block.
|
||||
|
|
|
|||
151
.github/workflows/close-invalid-publish-prs.yml
vendored
151
.github/workflows/close-invalid-publish-prs.yml
vendored
|
|
@ -1,151 +0,0 @@
|
|||
name: Close invalid publish PRs
|
||||
|
||||
# Stage 2 of 2 (see detect-invalid-publish-prs.yml for stage 1).
|
||||
#
|
||||
# Runs in the trusted base-repo context (triggered by workflow_run, not by the
|
||||
# PR itself), so it has a write token. It picks up the PR number flagged by
|
||||
# stage 1 and, after independently re-validating the PR from the API, posts a
|
||||
# comment pointing at the publishing docs, labels it `invalid`, and closes it.
|
||||
#
|
||||
# Trust boundary: the ONLY thing crossing from the untrusted stage is the PR
|
||||
# number (sanitized to digits below). We re-derive every decision (state,
|
||||
# author, changed files) here, so a wrong/stale artifact can never cause us to
|
||||
# close a PR that doesn't actually match.
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Detect invalid publish PRs"]
|
||||
types: [completed]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
issues: write # labels on PRs go through the issues API
|
||||
actions: read # needed to list/download the stage-1 artifact
|
||||
|
||||
jobs:
|
||||
close:
|
||||
if: github.event.workflow_run.conclusion == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
RUN_ID: ${{ github.event.workflow_run.id }}
|
||||
COMMENT_BODY: |
|
||||
Hi @__AUTHOR__ 👋 — thanks for your interest in the MCP Registry!
|
||||
|
||||
It looks like this PR is trying to publish an MCP server by adding or editing server files in this repository (under `servers/`, `data/servers/`, or in `data/seed.json`). That isn't how servers get published, so I'm closing this PR automatically.
|
||||
|
||||
**Servers are published with the [`mcp-publisher`](https://github.com/modelcontextprotocol/registry/blob/main/docs/modelcontextprotocol-io/quickstart.mdx) CLI**, not by opening a pull request against this repo. The CLI verifies that you own your namespace and submits your `server.json` directly to the live registry API.
|
||||
|
||||
To publish your server, follow the **[Publishing Quickstart](https://github.com/modelcontextprotocol/registry/blob/main/docs/modelcontextprotocol-io/quickstart.mdx)**. In short:
|
||||
|
||||
```bash
|
||||
# Build the publisher CLI
|
||||
make publisher
|
||||
|
||||
# Authenticate (e.g. via GitHub) and publish your server.json
|
||||
./bin/mcp-publisher login github
|
||||
./bin/mcp-publisher publish
|
||||
```
|
||||
|
||||
Note: `data/seed.json` is seed data for **local development only** — adding entries there does not publish anything to the registry.
|
||||
|
||||
If you believe this was closed in error, please leave a comment and a maintainer will take a look. 🙇
|
||||
|
||||
_This is an automated message._
|
||||
|
||||
steps:
|
||||
- name: Check for flagged-pr artifact
|
||||
id: check
|
||||
run: |
|
||||
set -euo pipefail
|
||||
names="$(gh api "repos/$GH_REPO/actions/runs/$RUN_ID/artifacts" --jq '.artifacts[].name' || true)"
|
||||
if printf '%s\n' "$names" | grep -qx "flagged-pr"; then
|
||||
echo "found=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "No flagged-pr artifact on run $RUN_ID; nothing to do."
|
||||
echo "found=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Download flagged PR number
|
||||
if: steps.check.outputs.found == 'true'
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: flagged-pr
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
github-token: ${{ github.token }}
|
||||
|
||||
- name: Re-validate and close
|
||||
if: steps.check.outputs.found == 'true'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
# Sanitize: the only cross-boundary input. Keep digits only.
|
||||
PR_NUMBER="$(tr -dc '0-9' < pr-number.txt)"
|
||||
if [ -z "$PR_NUMBER" ]; then
|
||||
echo "No valid PR number in artifact; skipping."
|
||||
exit 0
|
||||
fi
|
||||
echo "Re-validating PR #$PR_NUMBER"
|
||||
|
||||
state="$(gh pr view "$PR_NUMBER" --json state --jq '.state')"
|
||||
if [ "$state" != "OPEN" ]; then
|
||||
echo "PR #$PR_NUMBER is $state, not OPEN; skipping."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Use the REST endpoint: `gh pr view --json` does not expose an
|
||||
# authorAssociation field, so calling it there exits non-zero and,
|
||||
# under `set -euo pipefail`, aborts the job before we ever close.
|
||||
assoc="$(gh api "repos/$GH_REPO/pulls/$PR_NUMBER" --jq '.author_association')"
|
||||
case "$assoc" in
|
||||
MEMBER|OWNER|COLLABORATOR)
|
||||
echo "Author association '$assoc' is trusted; skipping."
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
|
||||
# Idempotency: if a maintainer reopens after we labeled it, leave it be.
|
||||
# Capture into a var first so a transient gh failure can't fail open
|
||||
# (an errored fetch piped to grep would otherwise look "not labeled").
|
||||
labels="$(gh pr view "$PR_NUMBER" --json labels --jq '.labels[].name' || true)"
|
||||
if printf '%s\n' "$labels" | grep -qx "invalid"; then
|
||||
echo "PR #$PR_NUMBER already labeled 'invalid'; skipping."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Paginated REST endpoint (gh pr view --json files caps at 100, no pagination).
|
||||
mapfile -t FILES < <(gh api --paginate "repos/$GH_REPO/pulls/$PR_NUMBER/files" --jq '.[].filename')
|
||||
if [ "${#FILES[@]}" -eq 0 ]; then
|
||||
echo "No files for PR #$PR_NUMBER; skipping."
|
||||
exit 0
|
||||
fi
|
||||
# Keep this list in sync with the copy in stage 1.
|
||||
publish_attempt=0
|
||||
other=0
|
||||
for f in "${FILES[@]}"; do
|
||||
if [[ "$f" == servers/* \
|
||||
|| "$f" == data/servers/* \
|
||||
|| "$f" == data/data/servers/* \
|
||||
|| "$f" =~ ^data/[^/]*server\.json$ \
|
||||
|| "$f" == "data/seed.json" \
|
||||
|| "$f" == "server.json" \
|
||||
|| "$f" == "servers.json" ]]; then
|
||||
publish_attempt=1
|
||||
else
|
||||
other=1
|
||||
fi
|
||||
done
|
||||
if [ "$publish_attempt" -ne 1 ] || [ "$other" -ne 0 ]; then
|
||||
echo "PR #$PR_NUMBER no longer matches the publish-attempt pattern; skipping."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
author="$(gh pr view "$PR_NUMBER" --json author --jq '.author.login')"
|
||||
body="${COMMENT_BODY//__AUTHOR__/$author}"
|
||||
|
||||
echo "Commenting on, labeling, and closing PR #$PR_NUMBER"
|
||||
gh pr comment "$PR_NUMBER" --body "$body"
|
||||
gh pr edit "$PR_NUMBER" --add-label "invalid" || echo "Could not add label; continuing."
|
||||
gh pr close "$PR_NUMBER"
|
||||
56
.github/workflows/deploy-production.yml
vendored
56
.github/workflows/deploy-production.yml
vendored
|
|
@ -1,56 +0,0 @@
|
|||
name: Deploy to Production
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'deploy/Pulumi.gcpProd.yaml'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
PULUMI_VERSION: "3.188.0"
|
||||
|
||||
jobs:
|
||||
deploy-production:
|
||||
name: Deploy to Production
|
||||
runs-on: ubuntu-latest
|
||||
environment: production
|
||||
concurrency:
|
||||
group: deploy-production
|
||||
cancel-in-progress: false
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e
|
||||
with:
|
||||
go-version: 'stable'
|
||||
cache: true
|
||||
|
||||
- name: Setup Pulumi
|
||||
uses: pulumi/actions@8e5e406f4007fca908480587cb9893c07090f58d
|
||||
with:
|
||||
pulumi-version: ${{ env.PULUMI_VERSION }}
|
||||
|
||||
- name: Authenticate to Google Cloud
|
||||
uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093
|
||||
with:
|
||||
credentials_json: ${{ secrets.GCP_PROD_SERVICE_ACCOUNT_KEY }}
|
||||
|
||||
- name: Setup Google Cloud SDK
|
||||
uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db
|
||||
with:
|
||||
project_id: mcp-registry-prod
|
||||
install_components: gke-gcloud-auth-plugin
|
||||
|
||||
- name: Deploy to Production
|
||||
working-directory: ./deploy
|
||||
env:
|
||||
PULUMI_PROD_PASSPHRASE: ${{ secrets.PULUMI_PROD_PASSPHRASE }}
|
||||
run: |
|
||||
echo "$PULUMI_PROD_PASSPHRASE" > passphrase.prod.txt
|
||||
make prod-up
|
||||
103
.github/workflows/deploy-staging.yml
vendored
103
.github/workflows/deploy-staging.yml
vendored
|
|
@ -1,103 +0,0 @@
|
|||
name: Deploy to Staging
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths-ignore:
|
||||
- 'deploy/Pulumi.gcpProd.yaml'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
PULUMI_VERSION: "3.188.0"
|
||||
|
||||
jobs:
|
||||
ko-push:
|
||||
name: Build and Push Image with ko
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e
|
||||
with:
|
||||
go-version: 'stable'
|
||||
cache: true
|
||||
|
||||
- name: Set up ko
|
||||
uses: ko-build/setup-ko@v0.10
|
||||
|
||||
- name: Log in to Container Registry
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- 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 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: ko-push
|
||||
concurrency:
|
||||
group: deploy-staging
|
||||
cancel-in-progress: false
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e
|
||||
with:
|
||||
go-version: 'stable'
|
||||
cache: true
|
||||
|
||||
- name: Setup Pulumi
|
||||
uses: pulumi/actions@8e5e406f4007fca908480587cb9893c07090f58d
|
||||
with:
|
||||
pulumi-version: ${{ env.PULUMI_VERSION }}
|
||||
|
||||
- name: Authenticate to Google Cloud
|
||||
uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093
|
||||
with:
|
||||
credentials_json: ${{ secrets.GCP_STAGING_SERVICE_ACCOUNT_KEY }}
|
||||
|
||||
- name: Setup Google Cloud SDK
|
||||
uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db
|
||||
with:
|
||||
project_id: mcp-registry-staging
|
||||
install_components: gke-gcloud-auth-plugin
|
||||
|
||||
- name: Deploy to Staging
|
||||
working-directory: ./deploy
|
||||
env:
|
||||
PULUMI_STAGING_PASSPHRASE: ${{ secrets.PULUMI_STAGING_PASSPHRASE }}
|
||||
run: |
|
||||
echo "$PULUMI_STAGING_PASSPHRASE" > passphrase.staging.txt
|
||||
make staging-up
|
||||
132
.github/workflows/deploy.yml
vendored
Normal file
132
.github/workflows/deploy.yml
vendored
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
name: Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
PULUMI_VERSION: "3.188.0"
|
||||
|
||||
jobs:
|
||||
docker-push:
|
||||
name: Build Docker Image
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435
|
||||
|
||||
- name: Log in to Container Registry
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- 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: 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
|
||||
|
||||
deploy-staging:
|
||||
name: Deploy to Staging
|
||||
runs-on: ubuntu-latest
|
||||
environment: staging
|
||||
needs: docker-push
|
||||
concurrency:
|
||||
group: deploy-staging
|
||||
cancel-in-progress: false
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
- name: Setup Pulumi
|
||||
uses: pulumi/actions@cc7494be991dba0978f7ffafaf995b0449a0998e
|
||||
with:
|
||||
pulumi-version: ${{ env.PULUMI_VERSION }}
|
||||
|
||||
- name: Authenticate to Google Cloud
|
||||
uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093
|
||||
with:
|
||||
credentials_json: ${{ secrets.GCP_STAGING_SERVICE_ACCOUNT_KEY }}
|
||||
|
||||
- name: Setup Google Cloud SDK
|
||||
uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db
|
||||
with:
|
||||
project_id: mcp-registry-staging
|
||||
install_components: gke-gcloud-auth-plugin
|
||||
|
||||
- name: Deploy to Staging
|
||||
working-directory: ./deploy
|
||||
run: |
|
||||
echo "${{ secrets.PULUMI_STAGING_PASSPHRASE }}" > passphrase.staging.txt
|
||||
make staging-up
|
||||
|
||||
deploy-production:
|
||||
name: Deploy to Production
|
||||
runs-on: ubuntu-latest
|
||||
environment: production
|
||||
needs: deploy-staging
|
||||
concurrency:
|
||||
group: deploy-production
|
||||
cancel-in-progress: false
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
- name: Setup Pulumi
|
||||
uses: pulumi/actions@cc7494be991dba0978f7ffafaf995b0449a0998e
|
||||
with:
|
||||
pulumi-version: ${{ env.PULUMI_VERSION }}
|
||||
|
||||
- name: Authenticate to Google Cloud
|
||||
uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093
|
||||
with:
|
||||
credentials_json: ${{ secrets.GCP_PROD_SERVICE_ACCOUNT_KEY }}
|
||||
|
||||
- name: Setup Google Cloud SDK
|
||||
uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db
|
||||
with:
|
||||
project_id: mcp-registry-prod
|
||||
install_components: gke-gcloud-auth-plugin
|
||||
|
||||
- name: Deploy to Production
|
||||
working-directory: ./deploy
|
||||
run: |
|
||||
echo "${{ secrets.PULUMI_PROD_PASSPHRASE }}" > passphrase.prod.txt
|
||||
make prod-up
|
||||
96
.github/workflows/detect-invalid-publish-prs.yml
vendored
96
.github/workflows/detect-invalid-publish-prs.yml
vendored
|
|
@ -1,96 +0,0 @@
|
|||
name: Detect invalid publish PRs
|
||||
|
||||
# Stage 1 of 2 (see close-invalid-publish-prs.yml for stage 2).
|
||||
#
|
||||
# Some contributors try to "publish" a server by opening a PR that adds files
|
||||
# under servers/ or edits data/seed.json. That is not how publishing works -
|
||||
# servers are published with the mcp-publisher CLI against the live registry
|
||||
# API. This job only DETECTS such PRs. It runs from the (untrusted) PR context
|
||||
# with a read-only token, so it cannot comment on or close anything. When it
|
||||
# finds a match it records just the PR number as an artifact; the privileged
|
||||
# stage-2 workflow picks that up via workflow_run and acts on it.
|
||||
#
|
||||
# We never check out or run the PR's code - we only read changed file paths.
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
jobs:
|
||||
detect:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
AUTHOR_ASSOCIATION: ${{ github.event.pull_request.author_association }}
|
||||
steps:
|
||||
- name: Classify PR
|
||||
id: classify
|
||||
run: |
|
||||
set -euo pipefail
|
||||
match=false
|
||||
|
||||
# Maintainers may legitimately touch these files (e.g. dev seed data),
|
||||
# so never flag their PRs.
|
||||
case "$AUTHOR_ASSOCIATION" in
|
||||
MEMBER|OWNER|COLLABORATOR)
|
||||
echo "Author association '$AUTHOR_ASSOCIATION' is trusted; not flagging."
|
||||
;;
|
||||
*)
|
||||
# Use the paginated REST endpoint: `gh pr view --json files` caps
|
||||
# at 100 files with no pagination, which could truncate a large PR
|
||||
# and cause a false match.
|
||||
mapfile -t FILES < <(gh api --paginate "repos/$GH_REPO/pulls/$PR_NUMBER/files" --jq '.[].filename')
|
||||
if [ "${#FILES[@]}" -gt 0 ]; then
|
||||
# Flag only if the PR touches *exclusively* publish-attempt files
|
||||
# and nothing else, so that legit PRs touching seed data
|
||||
# alongside real code are left alone.
|
||||
#
|
||||
# Keep this list in sync with the copy in stage 2. It stays an
|
||||
# explicit list rather than a broad `**/server.json` glob:
|
||||
# because we require *every* file to match, an over-broad
|
||||
# pattern would auto-close a legitimate single-file PR.
|
||||
# data/servers/** is here because #1524 put its server.json
|
||||
# there - a natural guess next to data/seed.json - and slipped
|
||||
# through the original servers/**-only list.
|
||||
publish_attempt=0
|
||||
other=0
|
||||
for f in "${FILES[@]}"; do
|
||||
if [[ "$f" == servers/* \
|
||||
|| "$f" == data/servers/* \
|
||||
|| "$f" == data/data/servers/* \
|
||||
|| "$f" =~ ^data/[^/]*server\.json$ \
|
||||
|| "$f" == "data/seed.json" \
|
||||
|| "$f" == "server.json" \
|
||||
|| "$f" == "servers.json" ]]; then
|
||||
publish_attempt=1
|
||||
else
|
||||
echo "Non-publish file changed: $f"
|
||||
other=1
|
||||
fi
|
||||
done
|
||||
if [ "$publish_attempt" -eq 1 ] && [ "$other" -eq 0 ]; then
|
||||
match=true
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "PR #$PR_NUMBER match=$match"
|
||||
echo "match=$match" >> "$GITHUB_OUTPUT"
|
||||
if [ "$match" = "true" ]; then
|
||||
echo "$PR_NUMBER" > pr-number.txt
|
||||
fi
|
||||
|
||||
- name: Record flagged PR number
|
||||
if: steps.classify.outputs.match == 'true'
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v4
|
||||
with:
|
||||
name: flagged-pr
|
||||
path: pr-number.txt
|
||||
retention-days: 1
|
||||
69
.github/workflows/release.yml
vendored
69
.github/workflows/release.yml
vendored
|
|
@ -14,24 +14,23 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e
|
||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00
|
||||
with:
|
||||
go-version: 'stable'
|
||||
cache: true
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
- name: Install cosign
|
||||
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
|
||||
uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62
|
||||
|
||||
- name: Install Syft
|
||||
uses: anchore/sbom-action/download-syft@v0.24.2
|
||||
uses: anchore/sbom-action/download-syft@v0.20.6
|
||||
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94
|
||||
uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a
|
||||
with:
|
||||
distribution: goreleaser
|
||||
version: v2.12.0
|
||||
|
|
@ -39,48 +38,40 @@ jobs:
|
|||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
ko-push:
|
||||
name: Build and Push Image with ko
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
needs: goreleaser
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e
|
||||
with:
|
||||
go-version: 'stable'
|
||||
cache: true
|
||||
|
||||
- name: Set up ko
|
||||
uses: ko-build/setup-ko@v0.10
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435
|
||||
|
||||
- name: Log in to Container Registry
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- 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 and strip 'v' prefix (e.g., refs/tags/v1.2.3 -> 1.2.3)
|
||||
VERSION="${GITHUB_REF#refs/tags/}"
|
||||
VERSION="${VERSION#v}"
|
||||
echo "version=$VERSION" >> $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: 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
|
||||
- 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
|
||||
435
.github/workflows/sync-db.yml
vendored
435
.github/workflows/sync-db.yml
vendored
|
|
@ -1,435 +0,0 @@
|
|||
name: Sync Production DB to Staging (from backups)
|
||||
|
||||
on:
|
||||
# schedule:
|
||||
# # Run daily at 2 AM UTC (during low-traffic hours)
|
||||
# - cron: '0 2 * * *'
|
||||
workflow_dispatch: # Allow manual triggering
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
sync-database:
|
||||
name: Sync Prod DB to Staging from k8up Backups
|
||||
runs-on: ubuntu-latest
|
||||
environment: staging
|
||||
concurrency:
|
||||
group: sync-staging-database
|
||||
cancel-in-progress: false
|
||||
steps:
|
||||
- name: Authenticate to Google Cloud (Production)
|
||||
uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093
|
||||
with:
|
||||
credentials_json: ${{ secrets.GCP_PROD_SERVICE_ACCOUNT_KEY }}
|
||||
|
||||
- name: Setup Google Cloud SDK
|
||||
uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db
|
||||
with:
|
||||
project_id: mcp-registry-prod
|
||||
install_components: gke-gcloud-auth-plugin
|
||||
|
||||
- name: Get backup credentials from prod cluster
|
||||
id: backup-creds
|
||||
run: |
|
||||
gcloud container clusters get-credentials mcp-registry-prod \
|
||||
--zone=us-central1-b \
|
||||
--project=mcp-registry-prod
|
||||
|
||||
# Store in outputs (GitHub Actions encrypts these automatically)
|
||||
kubectl get secret k8up-backup-credentials -n default -o json | jq -r '
|
||||
"access_key=" + (.data.AWS_ACCESS_KEY_ID | @base64d),
|
||||
"secret_key=" + (.data.AWS_SECRET_ACCESS_KEY | @base64d)
|
||||
' >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Remove all production access (SAFETY MEASURE)
|
||||
run: |
|
||||
# Remove production cluster from kubeconfig
|
||||
kubectl config delete-context gke_mcp-registry-prod_us-central1-b_mcp-registry-prod 2>/dev/null || true
|
||||
|
||||
# Revoke gcloud credentials
|
||||
gcloud auth revoke --all 2>/dev/null || true
|
||||
|
||||
# Clear gcloud configuration
|
||||
gcloud config unset project 2>/dev/null || true
|
||||
gcloud config unset account 2>/dev/null || true
|
||||
|
||||
# Verify no contexts remain
|
||||
CONTEXT_COUNT=$(kubectl config get-contexts -o name 2>/dev/null | wc -l)
|
||||
if [ "$CONTEXT_COUNT" -gt 0 ]; then
|
||||
echo "❌ ERROR: $CONTEXT_COUNT context(s) still exist after cleanup!"
|
||||
kubectl config get-contexts
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Switch to staging cluster
|
||||
uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093
|
||||
with:
|
||||
credentials_json: ${{ secrets.GCP_STAGING_SERVICE_ACCOUNT_KEY }}
|
||||
|
||||
- name: Configure staging cluster access
|
||||
run: |
|
||||
gcloud config set project mcp-registry-staging
|
||||
gcloud container clusters get-credentials mcp-registry-staging \
|
||||
--zone=us-central1-b \
|
||||
--project=mcp-registry-staging
|
||||
|
||||
- name: Create secret for prod backup bucket access
|
||||
run: |
|
||||
# Create/update secret in staging with access to prod backups
|
||||
kubectl create secret generic prod-to-staging-sync-credentials \
|
||||
--from-literal=AWS_ACCESS_KEY_ID="${{ steps.backup-creds.outputs.access_key }}" \
|
||||
--from-literal=AWS_SECRET_ACCESS_KEY="${{ steps.backup-creds.outputs.secret_key }}" \
|
||||
--dry-run=client -o yaml | kubectl apply -f -
|
||||
|
||||
- name: Create restore PVC
|
||||
run: |
|
||||
kubectl apply -f - <<EOF
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: restore-data-pvc
|
||||
namespace: default
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 50Gi
|
||||
EOF
|
||||
|
||||
- name: Trigger k8up restore from prod backups
|
||||
id: restore
|
||||
run: |
|
||||
RESTORE_NAME="restore-from-prod-$(date +%Y%m%d-%H%M%S)"
|
||||
echo "restore_name=$RESTORE_NAME" >> $GITHUB_OUTPUT
|
||||
|
||||
# Create a k8up Restore resource to restore from prod backups
|
||||
kubectl apply -f - <<EOF
|
||||
apiVersion: k8up.io/v1
|
||||
kind: Restore
|
||||
metadata:
|
||||
name: $RESTORE_NAME
|
||||
namespace: default
|
||||
spec:
|
||||
snapshot: latest
|
||||
restoreMethod:
|
||||
folder:
|
||||
claimName: restore-data-pvc
|
||||
backend:
|
||||
repoPasswordSecretRef:
|
||||
name: k8up-repo-password
|
||||
key: password
|
||||
s3:
|
||||
bucket: mcp-registry-prod-backups
|
||||
endpoint: https://storage.googleapis.com
|
||||
accessKeyIDSecretRef:
|
||||
name: prod-to-staging-sync-credentials
|
||||
key: AWS_ACCESS_KEY_ID
|
||||
secretAccessKeySecretRef:
|
||||
name: prod-to-staging-sync-credentials
|
||||
key: AWS_SECRET_ACCESS_KEY
|
||||
EOF
|
||||
|
||||
- name: Wait for k8up restore to complete
|
||||
run: |
|
||||
RESTORE_NAME="${{ steps.restore.outputs.restore_name }}"
|
||||
|
||||
echo "Waiting for restore job to start..."
|
||||
sleep 15
|
||||
|
||||
# Find the job created by k8up for this restore
|
||||
# k8up creates jobs with name pattern "restore-<restore-name>"
|
||||
# Since our restore is named "restore-from-prod-*", the job will be "restore-restore-from-prod-*"
|
||||
for i in {1..60}; do
|
||||
JOB_NAME=$(kubectl get jobs -n default --no-headers 2>/dev/null | grep "^restore-$RESTORE_NAME" | awk '{print $1}' | head -1)
|
||||
if [ -n "$JOB_NAME" ]; then
|
||||
echo "Found restore job: $JOB_NAME"
|
||||
break
|
||||
fi
|
||||
echo "Waiting for job to be created... ($i/60)"
|
||||
sleep 2
|
||||
done
|
||||
|
||||
if [ -z "$JOB_NAME" ]; then
|
||||
echo "ERROR: Restore job not found after 120 seconds"
|
||||
echo "Checking restore resource status:"
|
||||
kubectl get restore $RESTORE_NAME -n default
|
||||
kubectl describe restore $RESTORE_NAME -n default
|
||||
|
||||
echo "Checking for any restore jobs:"
|
||||
kubectl get jobs -n default | grep restore || echo "No restore jobs found"
|
||||
|
||||
echo "Checking k8up operator logs:"
|
||||
kubectl logs deployment/k8up -n default --tail=50 | grep -i restore || echo "No restore logs found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Wait for the restore job to complete (max 15 minutes)
|
||||
echo "Waiting for restore job to complete..."
|
||||
kubectl wait --for=condition=complete \
|
||||
job/$JOB_NAME \
|
||||
--timeout=900s -n default || {
|
||||
echo "Restore job failed or timed out"
|
||||
echo "Job status:"
|
||||
kubectl get job/$JOB_NAME -n default
|
||||
echo "Job details:"
|
||||
kubectl describe job/$JOB_NAME -n default
|
||||
echo "Job logs (if available):"
|
||||
kubectl logs job/$JOB_NAME -n default --tail=100 || echo "No logs available"
|
||||
|
||||
# Check if it's a credential issue
|
||||
echo "Checking if credentials exist:"
|
||||
kubectl get secret prod-to-staging-sync-credentials -n default || echo "Credentials secret missing!"
|
||||
exit 1
|
||||
}
|
||||
|
||||
- name: Find staging PostgreSQL PVC
|
||||
id: pgdata-pvc
|
||||
run: |
|
||||
# Find the PVC used by the PostgreSQL cluster
|
||||
PVC_NAME=$(kubectl get pvc -n default -l cnpg.io/cluster=registry-pg -o jsonpath='{.items[0].metadata.name}')
|
||||
|
||||
if [ -z "$PVC_NAME" ]; then
|
||||
echo "ERROR: Could not find PostgreSQL PVC"
|
||||
kubectl get pvc -n default -l cnpg.io/cluster=registry-pg
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "pvc_name=$PVC_NAME" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Scale down staging PostgreSQL
|
||||
run: |
|
||||
echo "Scaling down PostgreSQL cluster..."
|
||||
kubectl patch cluster registry-pg -n default \
|
||||
--type merge \
|
||||
--patch '{"spec":{"instances":0}}'
|
||||
|
||||
# Wait for pods to terminate
|
||||
echo "Waiting for pods to terminate..."
|
||||
kubectl wait --for=delete pod -l cnpg.io/cluster=registry-pg -n default --timeout=300s || true
|
||||
|
||||
- name: Verify we are in staging cluster (SAFETY CHECK)
|
||||
run: |
|
||||
# Get current cluster context
|
||||
CURRENT_CONTEXT=$(kubectl config current-context)
|
||||
CURRENT_PROJECT=$(gcloud config get-value project)
|
||||
|
||||
echo "Current kubectl context: $CURRENT_CONTEXT"
|
||||
echo "Current GCP project: $CURRENT_PROJECT"
|
||||
|
||||
# Verify we're in staging
|
||||
if ! echo "$CURRENT_CONTEXT" | grep -qi "staging"; then
|
||||
echo "❌ SAFETY CHECK FAILED: Not in staging cluster"
|
||||
echo "Context: $CURRENT_CONTEXT"
|
||||
echo "Expected: staging cluster"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$CURRENT_PROJECT" != "mcp-registry-staging" ]; then
|
||||
echo "❌ SAFETY CHECK FAILED: Not in staging project"
|
||||
echo "Project: $CURRENT_PROJECT"
|
||||
echo "Expected: mcp-registry-staging"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Replace staging database with restored backup
|
||||
id: copy-job
|
||||
run: |
|
||||
JOB_NAME="copy-pgdata-$(date +%Y%m%d-%H%M%S)"
|
||||
echo "job_name=$JOB_NAME" >> $GITHUB_OUTPUT
|
||||
|
||||
# Create a job to copy the restored backup data to the staging PVC
|
||||
kubectl apply -f - <<EOF
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: $JOB_NAME
|
||||
namespace: default
|
||||
spec:
|
||||
ttlSecondsAfterFinished: 600
|
||||
template:
|
||||
spec:
|
||||
restartPolicy: Never
|
||||
containers:
|
||||
- name: copy-data
|
||||
image: busybox:latest
|
||||
command:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- |
|
||||
set -e
|
||||
echo "Finding PostgreSQL data in backup..."
|
||||
echo "Restore structure:"
|
||||
find /restore -maxdepth 3 -type d 2>/dev/null | head -20
|
||||
|
||||
# Try different possible paths for pgdata
|
||||
PGDATA_SOURCE=""
|
||||
for path in \$(find /restore -type d -name "pgdata" 2>/dev/null); do
|
||||
if [ -f "\$path/PG_VERSION" ]; then
|
||||
PGDATA_SOURCE="\$path"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "\$PGDATA_SOURCE" ]; then
|
||||
echo "ERROR: Could not find valid pgdata directory with PG_VERSION"
|
||||
echo "Searched paths:"
|
||||
find /restore -type d -name "pgdata" 2>/dev/null
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Found pgdata at: \$PGDATA_SOURCE"
|
||||
echo "Contents:"
|
||||
ls -lah \$PGDATA_SOURCE/ | head -10
|
||||
|
||||
echo "Backing up existing staging data..."
|
||||
mkdir -p /pgdata-backup
|
||||
if [ "\$(ls -A /pgdata)" ]; then
|
||||
cp -a /pgdata/. /pgdata-backup/ || echo "Warning: Could not backup existing data"
|
||||
fi
|
||||
|
||||
echo "Clearing existing data..."
|
||||
rm -rf /pgdata/*
|
||||
|
||||
echo "Copying backup data to staging PVC..."
|
||||
cp -a \$PGDATA_SOURCE/. /pgdata/
|
||||
|
||||
echo "Setting correct permissions..."
|
||||
chmod 700 /pgdata
|
||||
|
||||
ls -lah /pgdata/ | head -20
|
||||
echo "PostgreSQL version: \$(cat /pgdata/PG_VERSION)"
|
||||
volumeMounts:
|
||||
- name: restore-data
|
||||
mountPath: /restore
|
||||
- name: staging-pgdata
|
||||
mountPath: /pgdata
|
||||
volumes:
|
||||
- name: restore-data
|
||||
persistentVolumeClaim:
|
||||
claimName: restore-data-pvc
|
||||
- name: staging-pgdata
|
||||
persistentVolumeClaim:
|
||||
claimName: ${{ steps.pgdata-pvc.outputs.pvc_name }}
|
||||
EOF
|
||||
|
||||
- name: Wait for data copy to complete
|
||||
run: |
|
||||
JOB_NAME="${{ steps.copy-job.outputs.job_name }}"
|
||||
|
||||
# Wait for copy to complete
|
||||
kubectl wait --for=condition=complete job/$JOB_NAME --timeout=600s -n default || {
|
||||
echo "Data copy job failed"
|
||||
kubectl describe job/$JOB_NAME -n default
|
||||
kubectl logs job/$JOB_NAME -n default --tail=100
|
||||
exit 1
|
||||
}
|
||||
|
||||
- name: Scale up staging PostgreSQL
|
||||
run: |
|
||||
echo "Scaling up PostgreSQL cluster..."
|
||||
kubectl patch cluster registry-pg -n default \
|
||||
--type merge \
|
||||
--patch '{"spec":{"instances":1}}'
|
||||
|
||||
# Wait for PostgreSQL pod to be created
|
||||
echo "Waiting for PostgreSQL pod to be created..."
|
||||
for i in {1..60}; do
|
||||
POD_COUNT=$(kubectl get pods -l cnpg.io/cluster=registry-pg -n default --no-headers 2>/dev/null | wc -l)
|
||||
if [ "$POD_COUNT" -gt 0 ]; then
|
||||
echo "Pod created"
|
||||
break
|
||||
fi
|
||||
echo "Waiting... ($i/60)"
|
||||
sleep 2
|
||||
done
|
||||
|
||||
# Wait for PostgreSQL to be ready
|
||||
echo "Waiting for PostgreSQL to be ready..."
|
||||
kubectl wait --for=condition=ready pod -l cnpg.io/cluster=registry-pg -n default --timeout=300s
|
||||
|
||||
- name: Verify staging DB is functional
|
||||
run: |
|
||||
# Create a verification pod
|
||||
kubectl run pg-verify-$(date +%s) \
|
||||
--image=postgres:15 \
|
||||
--rm -i --restart=Never \
|
||||
--env="PGPASSWORD=$(kubectl get secret registry-pg-superuser -n default -o jsonpath='{.data.password}' | base64 -d)" \
|
||||
-- bash -c '
|
||||
echo "Waiting for database to accept connections..."
|
||||
for i in {1..30}; do
|
||||
if pg_isready -h registry-pg-rw -U postgres 2>/dev/null; then
|
||||
break
|
||||
fi
|
||||
echo "Waiting... ($i/30)"
|
||||
sleep 2
|
||||
done
|
||||
|
||||
echo "Querying database..."
|
||||
TABLE_COUNT=$(psql -h registry-pg-rw -U postgres -d app -tAc "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = '\''public'\'';" 2>&1)
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "ERROR: Could not query database"
|
||||
echo "$TABLE_COUNT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$TABLE_COUNT" -lt 1 ]; then
|
||||
echo "ERROR: Staging DB has no tables!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Staging DB has $TABLE_COUNT tables"
|
||||
echo "Top 10 tables by row count:"
|
||||
psql -h registry-pg-rw -U postgres -d app \
|
||||
-c "SELECT schemaname, tablename, n_live_tup FROM pg_stat_user_tables ORDER BY n_live_tup DESC LIMIT 10;" || true
|
||||
'
|
||||
|
||||
- name: Cleanup
|
||||
if: always()
|
||||
run: |
|
||||
# Wait for any restore jobs to finish before cleanup (max 5 minutes for large database restores)
|
||||
echo "Checking for running restore jobs..."
|
||||
RESTORE_NAME="${{ steps.restore.outputs.restore_name }}"
|
||||
|
||||
for i in {1..60}; do
|
||||
# Check specifically for our restore job in Running or Pending state
|
||||
RUNNING_JOBS=$(kubectl get jobs -n default --no-headers 2>/dev/null | grep "^restore-${RESTORE_NAME}" | grep -E "Running|Pending" | wc -l)
|
||||
if [ "$RUNNING_JOBS" -eq 0 ]; then
|
||||
echo "No running restore jobs found"
|
||||
break
|
||||
fi
|
||||
echo "Waiting for $RUNNING_JOBS restore job(s) to finish... ($i/60)"
|
||||
sleep 5
|
||||
done
|
||||
|
||||
# Clean up jobs first
|
||||
if [ -n "${{ steps.copy-job.outputs.job_name }}" ]; then
|
||||
kubectl delete job ${{ steps.copy-job.outputs.job_name }} -n default || true
|
||||
fi
|
||||
|
||||
# Clean up the restore job if it exists (with proper error handling)
|
||||
if [ -n "$RESTORE_NAME" ]; then
|
||||
if kubectl get job "restore-$RESTORE_NAME" -n default >/dev/null 2>&1; then
|
||||
echo "Deleting restore job: restore-$RESTORE_NAME"
|
||||
kubectl delete job "restore-$RESTORE_NAME" -n default || echo "Failed to delete job, may have already been cleaned up"
|
||||
else
|
||||
echo "Restore job restore-$RESTORE_NAME not found, skipping deletion"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Remove restore PVC (will wait for jobs to finish)
|
||||
kubectl delete pvc restore-data-pvc -n default || true
|
||||
|
||||
# Remove prod backup credentials (for security)
|
||||
kubectl delete secret prod-to-staging-sync-credentials -n default || true
|
||||
|
||||
# Clean up old restore resources (keep last 3)
|
||||
kubectl get restore -n default --sort-by=.metadata.creationTimestamp -o name | head -n -3 | xargs -r kubectl delete || true
|
||||
|
||||
# Clean up old restore jobs (keep last 3)
|
||||
kubectl get jobs -n default --sort-by=.metadata.creationTimestamp -o name | grep '^job.batch/restore-' | head -n -3 | xargs -r kubectl delete -n default || true
|
||||
|
||||
# Clean up old copy jobs (keep last 3)
|
||||
kubectl get jobs -n default --sort-by=.metadata.creationTimestamp -o name | grep 'copy-pgdata-' | head -n -3 | xargs -r kubectl delete -n default || true
|
||||
69
.github/workflows/sync-schema.yml
vendored
69
.github/workflows/sync-schema.yml
vendored
|
|
@ -1,69 +0,0 @@
|
|||
name: Sync Schema
|
||||
|
||||
on:
|
||||
workflow_dispatch: # Manual trigger
|
||||
# TODO: Add daily schedule later
|
||||
# schedule:
|
||||
# - cron: '0 2 * * *' # Run daily at 2 AM UTC
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
sync-schema:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Checkout static repo
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
repository: modelcontextprotocol/static
|
||||
path: static-repo
|
||||
|
||||
- name: Sync schemas from static repo
|
||||
run: |
|
||||
echo "🔍 Syncing schemas from modelcontextprotocol/static..."
|
||||
mkdir -p internal/validators/schemas
|
||||
|
||||
# Copy all versioned schema files
|
||||
for dir in static-repo/schemas/*/; do
|
||||
if [ -f "$dir/server.schema.json" ]; then
|
||||
version=$(basename "$dir")
|
||||
# Skip draft directory if it exists
|
||||
if [ "$version" != "draft" ]; then
|
||||
output_file="internal/validators/schemas/${version}.json"
|
||||
if [ ! -f "$output_file" ] || ! cmp -s "$dir/server.schema.json" "$output_file"; then
|
||||
echo "⬇ Adding/updating ${version}/server.schema.json -> ${version}.json"
|
||||
cp "$dir/server.schema.json" "$output_file"
|
||||
else
|
||||
echo "✓ ${version} is already up to date"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
echo "✅ Schema sync complete"
|
||||
|
||||
- name: Check for changes
|
||||
id: changes
|
||||
run: |
|
||||
# Check for both modified and untracked files
|
||||
if [ -n "$(git status --porcelain internal/validators/schemas/)" ]; then
|
||||
echo "changed=true" >> $GITHUB_OUTPUT
|
||||
git status --porcelain internal/validators/schemas/
|
||||
else
|
||||
echo "changed=false" >> $GITHUB_OUTPUT
|
||||
echo "No changes to schemas"
|
||||
fi
|
||||
|
||||
- name: Commit and push changes
|
||||
if: steps.changes.outputs.changed == 'true'
|
||||
run: |
|
||||
git config --local user.email "action@github.com"
|
||||
git config --local user.name "GitHub Action"
|
||||
git add internal/validators/schemas/
|
||||
git commit -m "Sync schemas from modelcontextprotocol/static [skip ci]"
|
||||
git push
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -12,4 +12,4 @@ validate-schemas
|
|||
coverage.out
|
||||
coverage.html
|
||||
deploy/infra/infra
|
||||
registry
|
||||
registry
|
||||
|
|
@ -22,7 +22,7 @@ linters:
|
|||
- gocyclo
|
||||
- godox
|
||||
- gomoddirectives
|
||||
- gomodguard_v2
|
||||
- gomodguard
|
||||
- goprintffuncname
|
||||
- gosec
|
||||
- grouper
|
||||
|
|
@ -55,10 +55,6 @@ linters:
|
|||
max-complexity: 20
|
||||
dupl:
|
||||
threshold: 300
|
||||
goconst:
|
||||
# Repeated literals in tests are fixture data ("1.0.0", "Test server", ...);
|
||||
# hoisting them into constants hurts readability more than it helps.
|
||||
ignore-tests: true
|
||||
gocognit:
|
||||
min-complexity: 51
|
||||
nestif:
|
||||
|
|
@ -70,18 +66,11 @@ linters:
|
|||
- common-false-positives
|
||||
- legacy
|
||||
- std-error-handling
|
||||
rules:
|
||||
# httptest.NewRequest is idiomatic in tests; noctx is not useful there.
|
||||
- path: _test\.go
|
||||
linters:
|
||||
- noctx
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
formatters:
|
||||
enable:
|
||||
- gofmt
|
||||
exclusions:
|
||||
generated: lax
|
||||
paths:
|
||||
|
|
|
|||
|
|
@ -95,35 +95,37 @@ signs:
|
|||
cmd: cosign
|
||||
args:
|
||||
- "sign-blob"
|
||||
- "--bundle=${signature}" # cosign v3+: bundles signature and certificate together
|
||||
- "--output-signature=${signature}"
|
||||
- "--output-certificate=${certificate}"
|
||||
- "${artifact}"
|
||||
- "--yes" # needed on cosign 2.0.0+
|
||||
artifacts: archive
|
||||
output: true
|
||||
signature: "${artifact}.sigstore.json"
|
||||
|
||||
certificate: '{{ trimsuffix (trimsuffix .Env.artifact ".zip") ".tar.gz" }}.pem'
|
||||
|
||||
# Also sign checksums file for additional verification
|
||||
- id: checksums
|
||||
cmd: cosign
|
||||
args:
|
||||
- "sign-blob"
|
||||
- "--bundle=${signature}" # cosign v3+: bundles signature and certificate together
|
||||
- "--output-signature=${signature}"
|
||||
- "--output-certificate=${certificate}"
|
||||
- "${artifact}"
|
||||
- "--yes"
|
||||
artifacts: checksum
|
||||
output: true
|
||||
signature: "${artifact}.sigstore.json"
|
||||
certificate: '{{ trimsuffix .Env.artifact ".txt" }}.pem'
|
||||
|
||||
# This section defines the release format.
|
||||
archives:
|
||||
# Registry server archive
|
||||
- id: registry
|
||||
name_template: "registry_{{ .Os }}_{{ .Arch }}"
|
||||
name_template: "registry_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
|
||||
ids:
|
||||
- registry
|
||||
|
||||
|
||||
# Publisher CLI archive
|
||||
- id: publisher
|
||||
name_template: "mcp-publisher_{{ .Os }}_{{ .Arch }}"
|
||||
name_template: "mcp-publisher_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
|
||||
ids:
|
||||
- publisher
|
||||
|
|
|
|||
|
|
@ -1,52 +0,0 @@
|
|||
# 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
|
||||
32
.ko.yaml
32
.ko.yaml
|
|
@ -1,32 +0,0 @@
|
|||
# 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
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue