Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/check-api-updates.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5

- name: Check OpenAPI spec for changes
id: api-check
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ jobs:
node-version: [18.x, 20.x]

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5

- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
uses: actions/setup-node@v5
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
Expand Down Expand Up @@ -57,10 +57,10 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5

- name: Use Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v5
with:
node-version: '20.x'
cache: 'npm'
Expand Down
35 changes: 20 additions & 15 deletions .github/workflows/deploy-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v5

- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v5
with:
node-version: '20.x'
cache: 'npm'
Expand All @@ -62,7 +62,7 @@ jobs:

steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0
fetch-tags: true
Expand Down Expand Up @@ -117,31 +117,36 @@ jobs:
# restarts compose when the digest changes. CI has no path into the host.
- name: Wait for host to pull and redeploy
run: |
EXPECTED=$(node -p "require('./package.json').version")
echo "Image pushed. Waiting for the dev host timer to pick it up (expecting version $EXPECTED, up to 6 min)..."
EXPECTED_COMMIT="${{ github.sha }}"
echo "Image pushed. Waiting for the dev host timer to pick it up (expecting commit $EXPECTED_COMMIT, up to 6 min)..."
UP=0
COMMIT=""
for i in $(seq 1 18); do
sleep 20
BODY=$(curl -s --max-time 10 ${{ secrets.MCP_URL }}/health || echo "")
STATUS=$(echo "$BODY" | grep -o '"status":"[^"]*"' | cut -d'"' -f4)
VERSION=$(echo "$BODY" | grep -o '"version":"[^"]*"' | cut -d'"' -f4)
echo "attempt $i: status=${STATUS:-none} version=${VERSION:-none}"
COMMIT=$(echo "$BODY" | grep -o '"commit":"[^"]*"' | cut -d'"' -f4)
echo "attempt $i: status=${STATUS:-none} version=${VERSION:-none} commit=${COMMIT:-none}"
[ "$STATUS" = "ok" ] && UP=1
if [ "$VERSION" = "$EXPECTED" ]; then
if [ "$COMMIT" = "$EXPECTED_COMMIT" ]; then
echo "SERVED_VERSION=$VERSION" >> $GITHUB_ENV
echo "Host is serving $VERSION"
echo "Host is serving commit $COMMIT (version $VERSION)"
exit 0
fi
done
# The health endpoint only reports package version, so a commit that does
# not bump the version is indistinguishable from a stale deploy. Treat a
# healthy-but-unchanged version as a warning, a dead endpoint as failure.
if [ "$UP" = "1" ]; then
echo "::warning::Host healthy but still serving ${VERSION:-unknown}, expected $EXPECTED. Check the deploy timer on the dev host."
if [ "$UP" != "1" ]; then
echo "Health endpoint never returned status=ok within 6 minutes"
curl -v ${{ secrets.MCP_URL }}/health || true
exit 1
fi
if [ -z "$COMMIT" ]; then
# Transitional: the running image predates the commit field in /health,
# so staleness cannot be proven. Should only happen once.
echo "::warning::Host healthy but /health has no commit field (pre-commit-field build still running?). Cannot verify the deploy; check the dev host timer."
exit 0
fi
echo "Health endpoint never returned status=ok within 6 minutes"
curl -v ${{ secrets.MCP_URL }}/health || true
echo "Host healthy but serving commit $COMMIT, expected $EXPECTED_COMMIT - the deploy did not land within 6 minutes"
exit 1

- name: Deployment summary
Expand Down
22 changes: 15 additions & 7 deletions .github/workflows/deploy-prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v5

- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v5
with:
node-version: '20.x'
cache: 'npm'
Expand All @@ -61,7 +61,7 @@ jobs:

steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
Expand Down Expand Up @@ -113,17 +113,25 @@ jobs:
- name: Wait for host to pull and redeploy
run: |
EXPECTED="${{ steps.version.outputs.version }}"
echo "Image pushed. Waiting for the prod host timer to pick it up (expecting version $EXPECTED, up to 10 min)..."
EXPECTED_COMMIT="${{ github.sha }}"
echo "Image pushed. Waiting for the prod host timer to pick it up (expecting commit $EXPECTED_COMMIT / version $EXPECTED, up to 10 min)..."
UP=0
for i in $(seq 1 30); do
sleep 20
BODY=$(curl -s --max-time 10 ${{ secrets.MCP_URL }}/health || echo "")
STATUS=$(echo "$BODY" | grep -o '"status":"[^"]*"' | cut -d'"' -f4)
VERSION=$(echo "$BODY" | grep -o '"version":"[^"]*"' | cut -d'"' -f4)
echo "attempt $i: status=${STATUS:-none} version=${VERSION:-none}"
COMMIT=$(echo "$BODY" | grep -o '"commit":"[^"]*"' | cut -d'"' -f4)
echo "attempt $i: status=${STATUS:-none} version=${VERSION:-none} commit=${COMMIT:-none}"
[ "$STATUS" = "ok" ] && UP=1
if [ "$VERSION" = "$EXPECTED" ]; then
echo "Production is serving $VERSION"
if [ "$COMMIT" = "$EXPECTED_COMMIT" ]; then
echo "Production is serving commit $COMMIT (version $VERSION)"
exit 0
fi
# Fallback while the running image predates the commit field in /health:
# release versions are unique per tag, so a version match is trustworthy
if [ -z "$COMMIT" ] && [ "$VERSION" = "$EXPECTED" ]; then
echo "Production is serving $VERSION (no commit field yet)"
exit 0
fi
done
Expand Down
17 changes: 13 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,11 +108,20 @@ Implementation notes:

## CI/CD & Deployment

- `ci.yml` - build + unit tests (Node 18/20 matrix), lint + format check (both enforced)
- `deploy-dev.yml` - push to `develop` → Docker build → deploy to mcp-dev.digitalsamba.com
- `deploy-prod.yml` - tag `v*` → deploy to mcp.digitalsamba.com, then syncs develop/main
Deployment is **pull-based** (since 2026-07): CI builds and pushes images to the
private Monza registry but never touches the hosts. Each host pulls its tag on a
short timer (dev: `:latest`, ~2 min; prod: `:production`, ~5 min) and restarts
itself when the digest changes. The final workflow step waits for the host's
`/health` to report the expected git commit.

- `ci.yml` - build + tests (Node 18/20 matrix), lint + format check (all enforced)
- `deploy-dev.yml` - push to `develop` → image pushed → dev host self-deploys (mcp-dev.digitalsamba.com)
- `deploy-prod.yml` - tag `v*` → image pushed → prod host self-deploys (mcp.digitalsamba.com), then syncs develop/main
- `check-api-updates.yml` - weekly OpenAPI drift check (Mondays), opens issues labeled `api-update`
- Docker: `deployment/` contains Dockerfile and docker-compose (includes redis:7 sidecar)
- `deployment/docker-compose.yml` is a **reference copy only** - the live compose
and .env files are host-managed by ops; compose/env changes are an ops request,
not a commit
- `:latest` is dev's tag exclusively; prod publishes `:production`, version, and sha tags

## Critical Constraints

Expand Down
2 changes: 2 additions & 0 deletions src/transports/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
createServer,
VERSION,
VERSION_INFO,
GIT_COMMIT,
COMMITS_AHEAD,
getDisplayVersion,
isDevBuild,
Expand Down Expand Up @@ -208,6 +209,7 @@ export async function startHttpServer(
res.json({
status: "ok",
version: VERSION,
commit: GIT_COMMIT,
transport: "http",
activeSessions: transports.size,
oauthSessions: await getActiveSessionCount(),
Expand Down
Loading