Skip to content

Merge pull request #363 from rush-db/feat/update-release-and-deploy-p… #325

Merge pull request #363 from rush-db/feat/update-release-and-deploy-p…

Merge pull request #363 from rush-db/feat/update-release-and-deploy-p… #325

Workflow file for this run

name: Release and Deploy
# Nothing ships when a feature/fix branch lands on main. Shipping is triggered by
# merging the changesets "version packages" PR, which is the only commit that
# changes package.json versions — see .github/scripts/detect-version-bumps.mjs.
#
# Each artifact is versioned and shipped on its own:
# rushdb/platform ← rushdb-core's version, rebuilt when core or dashboard bumps
# rushdb/mcp-server ← @rushdb/mcp-server's version, rebuilt when it bumps
# npm packages ← published by changesets when a public package bumps
#
# So a core-only patch redeploys the platform without touching npm, and an
# sdk-only patch flows into both images via the dependent bumps changesets makes
# (updateInternalDependencies: patch). Minor releases land on one shared version
# for everything they touch (`linked` in .changeset/config.json).
on:
push:
branches:
- main
workflow_dispatch:
inputs:
force_platform:
description: 'Rebuild and redeploy rushdb/platform at its current version'
type: boolean
default: false
force_mcp:
description: 'Rebuild and redeploy rushdb/mcp-server at its current version'
type: boolean
default: false
permissions:
contents: write
issues: write
pull-requests: write
id-token: write
# Releases must not overlap; a second push waits rather than cancelling a
# half-finished publish/deploy.
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
env:
ECS_SERVICE_NEST: 'rushdb-ecs-service'
ECS_SERVICE_MCP: 'rushdb-mcp-service'
ECS_CLUSTER: 'rushdb-ecs-cluster'
jobs:
release:
name: Release
runs-on: ubuntu-latest
outputs:
platform_changed: ${{ steps.bumps.outputs.platform_changed }}
platform_version: ${{ steps.bumps.outputs.platform_version }}
mcp_changed: ${{ steps.bumps.outputs.mcp_image_changed }}
mcp_version: ${{ steps.bumps.outputs.mcp_version }}
sdk_version: ${{ steps.bumps.outputs.sdk_version }}
any_changed: ${{ steps.bumps.outputs.any_changed }}
published: ${{ steps.changesets.outputs.published }}
published_packages: ${{ steps.changesets.outputs.publishedPackages }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
# Runs before changesets so the working tree it may rewrite cannot affect
# the comparison (the script reads git objects, not the tree, either way).
- name: Detect version bumps
id: bumps
run: node .github/scripts/detect-version-bumps.mjs
env:
BASE_SHA: ${{ github.event.before }}
HEAD_SHA: ${{ github.sha }}
FORCE_PLATFORM: ${{ inputs.force_platform }}
FORCE_MCP: ${{ inputs.force_mcp }}
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10.1.0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
registry-url: 'https://registry.npmjs.org'
- name: Install Dependencies
run: pnpm install --frozen-lockfile
- name: Build packages
run: pnpm --filter ./packages/javascript-sdk... --filter ./packages/mcp-server... build
- name: Setup .npmrc
run: |
cat << EOF > ~/.npmrc
registry=https://registry.npmjs.org/
//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}
access=public
always-auth=true
EOF
# Opens/updates the "version packages" PR when changesets are pending,
# publishes to npm when the versioned commit lands. `changeset publish`
# covers every public workspace package (sdk, mcp-server, skills).
- name: Create Release PR or Publish
id: changesets
if: github.event_name == 'push'
uses: changesets/action@v1
with:
version: pnpm run version
publish: pnpm run release
commit: 'chore(release): version packages'
title: 'chore(release): version packages'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
platform-image:
name: Platform image
needs: release
if: needs.release.outputs.platform_changed == 'true'
uses: ./.github/workflows/docker-image.yml
with:
image: rushdb/platform
slug: platform
dockerfile: platform/Dockerfile
version: ${{ needs.release.outputs.platform_version }}
secrets: inherit
mcp-image:
name: MCP image
needs: release
if: needs.release.outputs.mcp_changed == 'true'
uses: ./.github/workflows/docker-image.yml
with:
image: rushdb/mcp-server
slug: mcp-server
dockerfile: packages/mcp-server/Dockerfile
version: ${{ needs.release.outputs.mcp_version }}
secrets: inherit
deploy-platform:
name: Deploy platform to ECS
needs: [release, platform-image]
runs-on: ubuntu-22.04
environment: production
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
- name: Checkout
uses: actions/checkout@v4
- name: Render task definition
run: |
cat <<EOF > task-definition.json
{
"containerDefinitions": [
{
"name": "rushdb",
"image": "${{ needs.platform-image.outputs.image }}",
"cpu": 1024,
"memory": 2048,
"portMappings": [
{
"containerPort": 3000,
"hostPort": 3000,
"protocol": "tcp",
"appProtocol": "http"
}
],
"essential": true,
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/rushdb",
"awslogs-region": "${{ vars.AWS_REGION }}",
"awslogs-stream-prefix": "ecs-rushdb"
}
},
"environment": [
{ "name": "NODE_ENV", "value": "production" },
{ "name": "LOG_LEVEL", "value": "info" },
{ "name": "RUSHDB_DASHBOARD_URL", "value": "${{ vars.RUSHDB_DASHBOARD_URL }}" },
{ "name": "RUSHDB_AES_256_ENCRYPTION_KEY", "value": "${{ secrets.RUSHDB_AES_256_ENCRYPTION_KEY }}" },
{ "name": "RUSHDB_JWT_PRIVATE_KEY_BASE64", "value": "${{ secrets.RUSHDB_JWT_PRIVATE_KEY_BASE64 }}" },
{ "name": "RUSHDB_JWT_PUBLIC_KEY_BASE64", "value": "${{ secrets.RUSHDB_JWT_PUBLIC_KEY_BASE64 }}" },
{ "name": "RUSHDB_JWT_KID", "value": "${{ secrets.RUSHDB_JWT_KID }}" },
{ "name": "RUSHDB_SELF_HOSTED", "value": "false" },
{ "name": "RUSHDB_SERVE_STATIC", "value": "false" },
{ "name": "NEO4J_URL", "value": "${{ secrets.NEO4J_URL }}" },
{ "name": "NEO4J_USERNAME", "value": "${{ secrets.NEO4J_USERNAME }}" },
{ "name": "NEO4J_PASSWORD", "value": "${{ secrets.NEO4J_PASSWORD }}" },
{ "name": "GOOGLE_CLIENT_ID", "value": "${{ secrets.GOOGLE_CLIENT_ID }}" },
{ "name": "GOOGLE_SECRET", "value": "${{ secrets.GOOGLE_SECRET }}" },
{ "name": "GH_CLIENT_ID", "value": "${{ secrets.GH_CLIENT_ID }}" },
{ "name": "GH_SECRET", "value": "${{ secrets.GH_SECRET }}" },
{ "name": "MAIL_HOST", "value": "${{ secrets.MAIL_HOST }}" },
{ "name": "MAIL_USER", "value": "${{ secrets.MAIL_USER }}" },
{ "name": "MAIL_PASSWORD", "value": "${{ secrets.MAIL_PASSWORD }}" },
{ "name": "MAIL_FROM", "value": "${{ secrets.MAIL_FROM }}" },
{ "name": "SERVICE_CAPTCHA_KEY", "value": "${{ secrets.SERVICE_CAPTCHA_KEY }}" },
{ "name": "BILLING_SERVICE_URL", "value": "${{ secrets.BILLING_SERVICE_URL }}" },
{ "name": "RUSHDB_BILLING_SECRET", "value": "${{ secrets.RUSHDB_BILLING_SECRET }}" },
{ "name": "SQL_DB_TYPE", "value": "postgres" },
{ "name": "SQL_DB_URL", "value": "${{ secrets.SQL_DB_URL }}" },
{ "name": "RUSHDB_PUBLIC_URL", "value": "${{ vars.RUSHDB_PUBLIC_URL }}" },
{ "name": "RUSHDB_OAUTH_ISSUER", "value": "${{ vars.RUSHDB_PUBLIC_URL }}" },
{ "name": "RUSHDB_EMBEDDING_BASE_URL", "value": "${{ vars.RUSHDB_EMBEDDING_BASE_URL }}" },
{ "name": "RUSHDB_EMBEDDING_API_KEY", "value": "${{ secrets.RUSHDB_EMBEDDING_API_KEY }}" },
{ "name": "RUSHDB_EMBEDDING_MODEL", "value": "${{ vars.RUSHDB_EMBEDDING_MODEL }}" },
{ "name": "RUSHDB_EMBEDDING_DIMENSIONS", "value": "${{ vars.RUSHDB_EMBEDDING_DIMENSIONS }}" },
{ "name": "RUSHDB_LLM_BASE_URL", "value": "${{ vars.RUSHDB_LLM_BASE_URL }}" },
{ "name": "RUSHDB_LLM_API_KEY", "value": "${{ secrets.RUSHDB_LLM_API_KEY }}" },
{ "name": "RUSHDB_LLM_MODEL", "value": "${{ vars.RUSHDB_LLM_MODEL }}" }
]
}
],
"family": "rushdb-task-definition",
"networkMode": "awsvpc",
"requiresCompatibilities": ["FARGATE"],
"cpu": "1024",
"memory": "2048",
"executionRoleArn": "${{ secrets.ECS_EXECUTION_ROLE_ARN }}"
}
EOF
- name: Deploy platform to ECS
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
with:
task-definition: task-definition.json
service: ${{ env.ECS_SERVICE_NEST }}
cluster: ${{ env.ECS_CLUSTER }}
wait-for-service-stability: false
deploy-mcp:
name: Deploy MCP server to ECS
needs: [release, mcp-image]
runs-on: ubuntu-22.04
environment: production
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
- name: Checkout
uses: actions/checkout@v4
- name: Render MCP task definition
run: |
cat <<EOF > mcp-task-definition.json
{
"containerDefinitions": [
{
"name": "rushdb-mcp",
"image": "${{ needs.mcp-image.outputs.image }}",
"cpu": 512,
"memory": 1024,
"portMappings": [
{
"containerPort": 3001,
"hostPort": 3001,
"protocol": "tcp",
"appProtocol": "http"
}
],
"essential": true,
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/rushdb-mcp",
"awslogs-region": "${{ vars.AWS_REGION }}",
"awslogs-stream-prefix": "ecs-rushdb-mcp"
}
},
"environment": [
{ "name": "MCP_TRANSPORT", "value": "http" },
{ "name": "PORT", "value": "3001" },
{ "name": "RUSHDB_API_URL", "value": "https://api.rushdb.com/api/v1" },
{ "name": "MCP_RESOURCE_URL", "value": "https://mcp.rushdb.com" },
{ "name": "RUSHDB_OAUTH_ISSUER", "value": "${{ vars.RUSHDB_PUBLIC_URL }}" }
]
}
],
"family": "rushdb-mcp-task-definition",
"networkMode": "awsvpc",
"requiresCompatibilities": ["FARGATE"],
"cpu": "512",
"memory": "1024",
"executionRoleArn": "${{ secrets.ECS_EXECUTION_ROLE_ARN }}"
}
EOF
- name: Deploy MCP server to ECS
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
with:
task-definition: mcp-task-definition.json
service: ${{ env.ECS_SERVICE_MCP }}
cluster: ${{ env.ECS_CLUSTER }}
wait-for-service-stability: false
notify-content:
name: Notify content control plane
needs: [release, platform-image, mcp-image, deploy-platform, deploy-mcp]
# Fires once the release has fully landed. A skipped artifact is fine (it had
# no version bump); a failed or cancelled one is not — and a failed image
# build must not slip through on the strength of its deploy being skipped.
if: |
always() &&
needs.release.outputs.any_changed == 'true' &&
!contains(fromJSON('["failure", "cancelled"]'), needs.platform-image.result) &&
!contains(fromJSON('["failure", "cancelled"]'), needs.mcp-image.result) &&
!contains(fromJSON('["failure", "cancelled"]'), needs.deploy-platform.result) &&
!contains(fromJSON('["failure", "cancelled"]'), needs.deploy-mcp.result)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Send signed release event
env:
CONTENT_WEBHOOK_URL: ${{ secrets.CONTENT_WEBHOOK_URL }}
CONTENT_WEBHOOK_SECRET: ${{ secrets.CONTENT_WEBHOOK_SECRET }}
RELEASE_VERSION: ${{ needs.release.outputs.sdk_version || needs.release.outputs.platform_version }}
PLATFORM_VERSION: ${{ needs.release.outputs.platform_version }}
PUBLISHED_PACKAGES: ${{ needs.release.outputs.published_packages }}
RELEASE_SHA: ${{ github.sha }}
RELEASE_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: node .github/scripts/notify-content-release.mjs