Build and Publish Registry #1093
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Build and Publish Registry | |
| on: | |
| push: | |
| branches: | |
| - main | |
| pull_request: | |
| branches: | |
| - main | |
| schedule: | |
| # Run daily at 00:00 UTC to catch any updates | |
| - cron: '0 0 * * *' | |
| workflow_dispatch: | |
| permissions: | |
| contents: write | |
| pull-requests: read | |
| jobs: | |
| lint: | |
| name: Lint Go Code | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v6 | |
| - name: Set up Go | |
| uses: actions/setup-go@v6 | |
| with: | |
| go-version-file: 'go.mod' | |
| cache: true | |
| - name: Run golangci-lint | |
| uses: golangci/golangci-lint-action@v9 | |
| with: | |
| version: latest | |
| args: --timeout=5m | |
| validate-and-test: | |
| name: Validate and Test | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v6 | |
| - name: Set up Go | |
| uses: actions/setup-go@v6 | |
| with: | |
| go-version-file: 'go.mod' | |
| cache: true | |
| - name: Install dependencies | |
| run: go mod download | |
| - name: Run tests | |
| run: go test -v -race -coverprofile=coverage.out ./... | |
| - name: Upload coverage reports | |
| uses: codecov/codecov-action@v5 | |
| with: | |
| file: ./coverage.out | |
| flags: unittests | |
| name: codecov-umbrella | |
| continue-on-error: true | |
| - name: Install Task | |
| uses: arduino/setup-task@v2 | |
| - name: Validate registry entries | |
| run: task validate | |
| build-and-release: | |
| name: Build and Release Registry | |
| runs-on: ubuntu-latest | |
| needs: [lint, validate-and-test] | |
| if: github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| - name: Set up Go | |
| uses: actions/setup-go@v6 | |
| with: | |
| go-version-file: 'go.mod' | |
| cache: true | |
| - name: Install Task | |
| uses: arduino/setup-task@v2 | |
| - name: Build registry files (both formats) | |
| run: | | |
| mkdir -p dist | |
| task build:registry | |
| cp build/registry.json dist/registry.json | |
| cp build/official-registry.json dist/official-registry.json | |
| CONTAINER_COUNT=$(jq '.servers | length' dist/registry.json) | |
| REMOTE_COUNT=$(jq '.remote_servers | length // 0' dist/registry.json) | |
| TOTAL_COUNT=$((CONTAINER_COUNT + REMOTE_COUNT)) | |
| echo "Registry built successfully with $TOTAL_COUNT entries ($CONTAINER_COUNT container-based, $REMOTE_COUNT remote)" | |
| echo "Both ToolHive and official MCP formats generated" | |
| - name: Validate JSON files | |
| run: | | |
| echo "Validating ToolHive format..." | |
| # Validate JSON structure | |
| jq empty dist/registry.json | |
| echo "β Valid JSON" | |
| # Check required fields | |
| jq -e '.last_updated' dist/registry.json > /dev/null | |
| echo "β Has last_updated field" | |
| jq -e '.servers' dist/registry.json > /dev/null | |
| echo "β Has servers field" | |
| # Check for remote_servers field (may be empty) | |
| jq -e 'has("remote_servers")' dist/registry.json > /dev/null && echo "β Has remote_servers field" || echo "β οΈ No remote_servers field" | |
| jq -e '."$schema"' dist/registry.json > /dev/null | |
| echo "β Has schema field" | |
| echo "" | |
| echo "Validating Official MCP format..." | |
| # Validate official registry JSON structure | |
| jq empty dist/official-registry.json | |
| echo "β Valid JSON" | |
| # Check required fields for official format | |
| jq -e '.version' dist/official-registry.json > /dev/null | |
| echo "β Has version field" | |
| jq -e '.meta.last_updated' dist/official-registry.json > /dev/null | |
| echo "β Has meta.last_updated field" | |
| jq -e '.data.servers' dist/official-registry.json > /dev/null | |
| echo "β Has data.servers field" | |
| # Check that servers have the flattened structure with _meta | |
| SERVER_COUNT=$(jq '.data.servers | length' dist/official-registry.json) | |
| if [ "$SERVER_COUNT" -gt 0 ]; then | |
| jq -e '.data.servers[0].name' dist/official-registry.json > /dev/null | |
| echo "β Servers have name field" | |
| jq -e '.data.servers[0]._meta."io.modelcontextprotocol.registry/publisher-provided"' dist/official-registry.json > /dev/null | |
| echo "β Servers have _meta with publisher-provided extensions" | |
| fi | |
| - name: Generate metadata | |
| id: metadata | |
| run: | | |
| # Generate version based on date and time | |
| VERSION=$(date +'%Y.%m.%d') | |
| TIMESTAMP=$(date +'%Y%m%d-%H%M%S') | |
| echo "version=$VERSION" >> $GITHUB_OUTPUT | |
| echo "timestamp=$TIMESTAMP" >> $GITHUB_OUTPUT | |
| # Count servers by status and tier (including both container and remote servers) | |
| CONTAINER_COUNT=$(jq '.servers | length' dist/registry.json) | |
| REMOTE_COUNT=$(jq '.remote_servers | length // 0' dist/registry.json) | |
| TOTAL=$((CONTAINER_COUNT + REMOTE_COUNT)) | |
| # Count by status (combine container and remote servers) | |
| ACTIVE_CONTAINER=$(jq '[.servers[] | select(.status == "Active")] | length' dist/registry.json) | |
| ACTIVE_REMOTE=$(jq '[(.remote_servers // {})[] | select(.status == "Active")] | length' dist/registry.json) | |
| ACTIVE=$((ACTIVE_CONTAINER + ACTIVE_REMOTE)) | |
| BETA_CONTAINER=$(jq '[.servers[] | select(.status == "Beta")] | length' dist/registry.json) | |
| BETA_REMOTE=$(jq '[(.remote_servers // {})[] | select(.status == "Beta")] | length' dist/registry.json) | |
| BETA=$((BETA_CONTAINER + BETA_REMOTE)) | |
| DEPRECATED_CONTAINER=$(jq '[.servers[] | select(.status == "Deprecated")] | length' dist/registry.json) | |
| DEPRECATED_REMOTE=$(jq '[(.remote_servers // {})[] | select(.status == "Deprecated")] | length' dist/registry.json) | |
| DEPRECATED=$((DEPRECATED_CONTAINER + DEPRECATED_REMOTE)) | |
| # Count by tier (combine container and remote servers) | |
| OFFICIAL_CONTAINER=$(jq '[.servers[] | select(.tier == "Official")] | length' dist/registry.json) | |
| OFFICIAL_REMOTE=$(jq '[(.remote_servers // {})[] | select(.tier == "Official")] | length' dist/registry.json) | |
| OFFICIAL=$((OFFICIAL_CONTAINER + OFFICIAL_REMOTE)) | |
| PARTNER_CONTAINER=$(jq '[.servers[] | select(.tier == "Partner")] | length' dist/registry.json) | |
| PARTNER_REMOTE=$(jq '[(.remote_servers // {})[] | select(.tier == "Partner")] | length' dist/registry.json) | |
| PARTNER=$((PARTNER_CONTAINER + PARTNER_REMOTE)) | |
| COMMUNITY_CONTAINER=$(jq '[.servers[] | select(.tier == "Community")] | length' dist/registry.json) | |
| COMMUNITY_REMOTE=$(jq '[(.remote_servers // {})[] | select(.tier == "Community")] | length' dist/registry.json) | |
| COMMUNITY=$((COMMUNITY_CONTAINER + COMMUNITY_REMOTE)) | |
| echo "total=$TOTAL" >> $GITHUB_OUTPUT | |
| echo "container_count=$CONTAINER_COUNT" >> $GITHUB_OUTPUT | |
| echo "remote_count=$REMOTE_COUNT" >> $GITHUB_OUTPUT | |
| echo "active=$ACTIVE" >> $GITHUB_OUTPUT | |
| echo "beta=$BETA" >> $GITHUB_OUTPUT | |
| echo "deprecated=$DEPRECATED" >> $GITHUB_OUTPUT | |
| echo "official=$OFFICIAL" >> $GITHUB_OUTPUT | |
| echo "partner=$PARTNER" >> $GITHUB_OUTPUT | |
| echo "community=$COMMUNITY" >> $GITHUB_OUTPUT | |
| - name: Create checksums | |
| run: | | |
| cd dist | |
| sha256sum registry.json > registry.json.sha256 | |
| md5sum registry.json > registry.json.md5 | |
| sha256sum official-registry.json > official-registry.json.sha256 | |
| md5sum official-registry.json > official-registry.json.md5 | |
| - name: Create tarball | |
| run: | | |
| cd dist | |
| tar -czf registry-${{ steps.metadata.outputs.version }}.tar.gz \ | |
| registry.json registry.json.sha256 registry.json.md5 \ | |
| official-registry.json official-registry.json.sha256 official-registry.json.md5 | |
| tar -tzf registry-${{ steps.metadata.outputs.version }}.tar.gz | |
| - name: Check if release exists | |
| id: check_release | |
| run: | | |
| if gh release view "v${{ steps.metadata.outputs.version }}" >/dev/null 2>&1; then | |
| echo "exists=true" >> $GITHUB_OUTPUT | |
| echo "Release v${{ steps.metadata.outputs.version }} already exists" | |
| else | |
| echo "exists=false" >> $GITHUB_OUTPUT | |
| echo "Release v${{ steps.metadata.outputs.version }} does not exist" | |
| fi | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| - name: Get changes since last release | |
| id: changes | |
| if: steps.check_release.outputs.exists == 'false' | |
| run: | | |
| # Get the last release tag | |
| LAST_TAG=$(gh release list --limit 1 --json tagName -q '.[0].tagName' || echo "") | |
| if [ -z "$LAST_TAG" ]; then | |
| echo "No previous release found" | |
| CHANGES="Initial release" | |
| else | |
| echo "Last release: $LAST_TAG" | |
| # Get commit messages since last release | |
| CHANGES=$(git log --pretty=format:"- %s" $LAST_TAG..HEAD --grep="^feat\|^fix\|^docs\|^chore" | head -20) | |
| if [ -z "$CHANGES" ]; then | |
| CHANGES="- Minor updates and maintenance" | |
| fi | |
| fi | |
| # Write to file to preserve formatting | |
| echo "$CHANGES" > changes.txt | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| - name: Create Release | |
| if: steps.check_release.outputs.exists == 'false' | |
| uses: ncipollo/release-action@v1 | |
| with: | |
| tag: v${{ steps.metadata.outputs.version }} | |
| name: Registry v${{ steps.metadata.outputs.version }} | |
| body: | | |
| ## π¦ ToolHive Registry Snapshot | |
| **Date**: ${{ steps.metadata.outputs.version }} | |
| **Build**: ${{ steps.metadata.outputs.timestamp }} | |
| ### π Statistics | |
| | Category | Count | | |
| |----------|-------| | |
| | **Total Servers** | ${{ steps.metadata.outputs.total }} | | |
| | **Container-based** | ${{ steps.metadata.outputs.container_count }} | | |
| | **Remote** | ${{ steps.metadata.outputs.remote_count }} | | |
| | **Active** | ${{ steps.metadata.outputs.active }} | | |
| | **Beta** | ${{ steps.metadata.outputs.beta }} | | |
| | **Deprecated** | ${{ steps.metadata.outputs.deprecated }} | | |
| | Tier | Count | | |
| |------|-------| | |
| | **Official** | ${{ steps.metadata.outputs.official }} | | |
| | **Partner** | ${{ steps.metadata.outputs.partner }} | | |
| | **Community** | ${{ steps.metadata.outputs.community }} | | |
| ### π₯ Download Options | |
| **Individual Files:** | |
| - **registry.json** - ToolHive format registry file | |
| - **official-registry.json** - Official MCP format registry file | |
| **Archives:** | |
| - **registry-${{ steps.metadata.outputs.version }}.tar.gz** - Complete archive with both formats and checksums | |
| ### π Direct URLs | |
| **ToolHive Format:** | |
| - Latest: `https://github.com/stacklok/toolhive-registry/releases/latest/download/registry.json` | |
| - This version: `https://github.com/stacklok/toolhive-registry/releases/download/v${{ steps.metadata.outputs.version }}/registry.json` | |
| **Official MCP Format:** | |
| - Latest: `https://github.com/stacklok/toolhive-registry/releases/latest/download/official-registry.json` | |
| - This version: `https://github.com/stacklok/toolhive-registry/releases/download/v${{ steps.metadata.outputs.version }}/official-registry.json` | |
| ### π Recent Changes | |
| ${{ steps.changes.outputs.changes }} | |
| --- | |
| *This is an automated release generated from the main branch.* | |
| artifacts: | | |
| dist/registry.json | |
| dist/registry.json.sha256 | |
| dist/registry.json.md5 | |
| dist/official-registry.json | |
| dist/official-registry.json.sha256 | |
| dist/official-registry.json.md5 | |
| dist/registry-${{ steps.metadata.outputs.version }}.tar.gz | |
| makeLatest: true | |
| artifactErrorsFailBuild: true | |
| - name: Update existing release | |
| if: steps.check_release.outputs.exists == 'true' | |
| run: | | |
| echo "Updating existing release v${{ steps.metadata.outputs.version }}" | |
| # Delete old assets | |
| gh release delete-asset "v${{ steps.metadata.outputs.version }}" registry.json --yes || true | |
| gh release delete-asset "v${{ steps.metadata.outputs.version }}" registry.json.sha256 --yes || true | |
| gh release delete-asset "v${{ steps.metadata.outputs.version }}" registry.json.md5 --yes || true | |
| gh release delete-asset "v${{ steps.metadata.outputs.version }}" official-registry.json --yes || true | |
| gh release delete-asset "v${{ steps.metadata.outputs.version }}" official-registry.json.sha256 --yes || true | |
| gh release delete-asset "v${{ steps.metadata.outputs.version }}" official-registry.json.md5 --yes || true | |
| gh release delete-asset "v${{ steps.metadata.outputs.version }}" "registry-${{ steps.metadata.outputs.version }}.tar.gz" --yes || true | |
| # Upload new assets | |
| gh release upload "v${{ steps.metadata.outputs.version }}" \ | |
| dist/registry.json \ | |
| dist/registry.json.sha256 \ | |
| dist/registry.json.md5 \ | |
| dist/official-registry.json \ | |
| dist/official-registry.json.sha256 \ | |
| dist/official-registry.json.md5 \ | |
| "dist/registry-${{ steps.metadata.outputs.version }}.tar.gz" \ | |
| --clobber | |
| echo "β Release updated successfully" | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| build-pr: | |
| name: Build PR Preview | |
| runs-on: ubuntu-latest | |
| if: github.event_name == 'pull_request' | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v6 | |
| - name: Set up Go | |
| uses: actions/setup-go@v6 | |
| with: | |
| go-version-file: 'go.mod' | |
| cache: true | |
| - name: Build registry-builder | |
| run: go build -o registry-builder ./cmd/registry-builder | |
| - name: Build registry.json | |
| run: | | |
| mkdir -p build | |
| ./registry-builder build -v | |
| - name: Generate PR comment | |
| run: | | |
| CONTAINER_COUNT=$(jq '.servers | length' build/registry.json) | |
| REMOTE_COUNT=$(jq '.remote_servers | length // 0' build/registry.json) | |
| TOTAL=$((CONTAINER_COUNT + REMOTE_COUNT)) | |
| SIZE=$(du -h build/registry.json | cut -f1) | |
| echo "## π¦ Registry Build Preview" > pr-comment.md | |
| echo "" >> pr-comment.md | |
| echo "β Registry built successfully!" >> pr-comment.md | |
| echo "" >> pr-comment.md | |
| echo "- **Total Servers**: $TOTAL" >> pr-comment.md | |
| echo " - Container-based: $CONTAINER_COUNT" >> pr-comment.md | |
| echo " - Remote: $REMOTE_COUNT" >> pr-comment.md | |
| echo "- **File Size**: $SIZE" >> pr-comment.md | |
| echo "- **Last Updated**: $(jq -r '.last_updated' build/registry.json)" >> pr-comment.md | |
| echo "" >> pr-comment.md | |
| echo "The registry.json will be published when this PR is merged." >> pr-comment.md | |
| - name: Upload PR artifact | |
| uses: actions/upload-artifact@v5 | |
| with: | |
| name: pr-registry-json | |
| path: build/registry.json | |
| retention-days: 7 |