name: Build and Push Docker Images
on:
push:
branches: [ master ]
tags: [ 'v*' ]
pull_request:
branches: [ master ]
env:
REGISTRY: docker.io
IMAGE_NAME: writenotenow/mysql-mcp
permissions:
contents: read
packages: write
security-events: write # For security scanning
pull-requests: write # For PR comments
id-token: write # For supply chain attestations
attestations: write # For generating attestations
jobs:
# Quality gate - must pass before any builds
quality-gate:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
- name: Type check
run: npm run typecheck
- name: Run tests
run: npm test
# Security gate - CodeQL must pass before any builds
codeql:
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Check for JS/TS files
id: check-files
run: |
if find . -type f \( -name "*.js" -o -name "*.jsx" -o -name "*.ts" -o -name "*.tsx" -o -name "*.mjs" -o -name "*.cjs" \) | grep -q .; then
echo "has_code=true" >> $GITHUB_OUTPUT
else
echo "has_code=false" >> $GITHUB_OUTPUT
fi
- name: Initialize CodeQL
if: steps.check-files.outputs.has_code == 'true'
uses: github/codeql-action/init@v4
with:
languages: javascript-typescript
queries: security-extended,security-and-quality
- name: Autobuild
if: steps.check-files.outputs.has_code == 'true'
uses: github/codeql-action/autobuild@v4
- name: Perform CodeQL Analysis
if: steps.check-files.outputs.has_code == 'true'
uses: github/codeql-action/analyze@v4
with:
category: "/language:javascript-typescript"
# Build each platform on native architecture (avoids QEMU emulation issues)
build-platform:
needs: [quality-gate, codeql]
strategy:
fail-fast: false
matrix:
include:
- platform: linux/amd64
runner: ubuntu-latest
- platform: linux/arm64
runner: ubuntu-24.04-arm
runs-on: ${{ matrix.runner }}
permissions:
contents: read
packages: write
id-token: write
attestations: write
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
continue-on-error: true
with:
registry: ${{ env.REGISTRY }}
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Read version from VERSION file
id: version
run: |
if [ -f "VERSION" ]; then
VERSION=$(head -1 VERSION | tr -d '[:space:]')
fi
if [ -z "$VERSION" ]; then
VERSION=$(grep -oP '"version":\s*"\K[0-9.]+' package.json | head -1)
fi
if [ -z "$VERSION" ]; then
VERSION="1.0.0"
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Detected version: $VERSION"
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
flavor: |
latest=false
suffix=-${{ matrix.platform == 'linux/amd64' && 'amd64' || 'arm64' }}
tags: |
type=sha,prefix=sha-,format=short
- name: Build and push platform image
id: build
uses: docker/build-push-action@v6
with:
context: .
file: Dockerfile
platforms: ${{ matrix.platform }}
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha,scope=${{ matrix.platform }}
cache-to: type=gha,scope=${{ matrix.platform }},mode=max
provenance: mode=max
sbom: true
- name: Export digest
if: github.event_name != 'pull_request'
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
- name: Upload digest
if: github.event_name != 'pull_request'
uses: actions/upload-artifact@v6
with:
name: digests-${{ matrix.platform == 'linux/amd64' && 'amd64' || 'arm64' }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1
# Security scan on amd64 image only (runs in parallel)
security-scan:
runs-on: ubuntu-latest
needs: build-platform
if: github.event_name != 'pull_request'
permissions:
contents: read
security-events: write
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build image for scanning
uses: docker/build-push-action@v6
with:
context: .
file: Dockerfile
platforms: linux/amd64
push: false
load: true
tags: local-scan:latest
cache-from: type=gha,scope=linux/amd64
- name: Docker Scout security scan
timeout-minutes: 10
run: |
curl -sSfL https://raw.githubusercontent.com/docker/scout-cli/main/install.sh | sh -s --
docker images local-scan:latest
echo "🔍 Running Docker Scout security scan"
if timeout 480 docker scout cves local-scan:latest > scout_output.txt 2>&1; then
echo "📊 Scan completed successfully"
cat scout_output.txt
if grep -E "(CRITICAL|HIGH)" scout_output.txt | grep -v "0 " > /dev/null; then
echo "⚠️ Critical or high severity vulnerabilities detected (informational)"
else
echo "✅ No critical/high severity vulnerabilities"
fi
else
echo "⚠️ Docker Scout scan timed out or failed"
fi
# Merge platform images into multi-arch manifest
merge-and-push:
runs-on: ubuntu-latest
needs: [build-platform, security-scan]
if: github.event_name != 'pull_request'
permissions:
contents: read
packages: write
id-token: write
attestations: write
deployments: write
environment:
name: ${{ github.ref == 'refs/heads/master' && 'production' || '' }}
url: https://hub.docker.com/r/writenotenow/mysql-mcp
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Download digests
uses: actions/download-artifact@v7
with:
path: /tmp/digests
pattern: digests-*
merge-multiple: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Read version
id: version
run: |
if [ -f "VERSION" ]; then
VERSION=$(head -1 VERSION | tr -d '[:space:]')
fi
if [ -z "$VERSION" ]; then
VERSION=$(grep -oP '"version":\s*"\K[0-9.]+' package.json | head -1)
fi
if [ -z "$VERSION" ]; then
VERSION="1.0.0"
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Extract metadata for manifest
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
flavor: |
latest=auto
tags: |
type=semver,pattern=v{{version}}
type=raw,value=v${{ steps.version.outputs.version }},enable={{is_default_branch}}
type=raw,value=latest,enable={{is_default_branch}}
type=sha,prefix=sha-,format=short
- name: Create and push manifest
working-directory: /tmp/digests
run: |
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *)
- name: Inspect manifest
run: |
docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }}
# Update Docker Hub description
- name: Update Docker Hub Description
if: github.ref == 'refs/heads/master'
uses: peter-evans/dockerhub-description@v5
continue-on-error: true
timeout-minutes: 5
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
repository: ${{ env.IMAGE_NAME }}
readme-filepath: ./DOCKER_README.md
short-description: "MySQL MCP with 191 tools, OAuth 2.1, HTTP Streamable, Smart Tool Filtering & Connection Pooling."
- name: Deployment Summary
if: github.ref == 'refs/heads/master'
run: |
echo "✅ Successfully published Docker images to production"
echo "🐳 Registry: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}"
echo "🏷️ Tags: ${{ steps.meta.outputs.tags }}"
echo "📝 Commit: ${{ github.sha }}"
echo "👤 Published by: ${{ github.actor }}"