Files

463 lines
18 KiB
YAML

name: Publish EE Artifacts
on:
workflow_dispatch:
inputs:
push:
description: Push images and the Helm chart to GHCR
required: false
default: false
type: boolean
chart_version:
description: Helm chart version for manual publishes, for example 0.17.1
required: false
default: ""
type: string
push:
branches:
- "dev"
tags:
- "v*"
pull_request:
paths:
- ".github/workflows/publish-ee-images.yml"
- "packaging/docker/Dockerfile.den"
- "packaging/docker/Dockerfile.den-gateway"
- "packaging/docker/Dockerfile.den-web"
- "packaging/docker/Dockerfile.inference"
- "packaging/helm/openwork-ee/**"
- "ee/apps/den-api/**"
- "ee/apps/den-gateway/**"
- "ee/apps/den-web/**"
- "ee/apps/inference/**"
- "ee/packages/**"
- "packages/automations/**"
- "packages/codemode/**"
- "packages/connect-link/**"
- "packages/email/**"
- "packages/enterprise-mcp-client/**"
- "packages/headless-threads/**"
- "packages/install-config/**"
- "packages/mcp-apps/**"
- "packages/types/**"
- "packages/ui/**"
- "pnpm-lock.yaml"
- "pnpm-workspace.yaml"
- "package.json"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' || github.ref == 'refs/heads/dev' }}
permissions:
contents: read
packages: write
env:
REGISTRY: ghcr.io
jobs:
artifact-version:
name: Resolve artifact version
runs-on: ubuntu-latest
outputs:
chart_version: ${{ steps.version.outputs.chart_version }}
publish: ${{ steps.version.outputs.publish }}
steps:
- name: Resolve version
id: version
shell: bash
env:
INPUT_CHART_VERSION: ${{ inputs.chart_version }}
INPUT_PUSH: ${{ inputs.push }}
run: |
publish="false"
if [ "${GITHUB_EVENT_NAME}" = "push" ]; then
publish="true"
elif [ "$INPUT_PUSH" = "true" ]; then
publish="true"
fi
chart_version="$INPUT_CHART_VERSION"
if [[ "${GITHUB_REF_TYPE}" = "tag" && "${GITHUB_REF_NAME}" == v* ]]; then
chart_version="${GITHUB_REF_NAME#v}"
fi
if [ "$publish" = "true" ] && [ -z "$chart_version" ] && [ "${GITHUB_EVENT_NAME}" != "push" ]; then
echo "chart_version is required when manually publishing from a non-tag ref" >&2
exit 1
fi
echo "chart_version=$chart_version" >> "$GITHUB_OUTPUT"
echo "publish=$publish" >> "$GITHUB_OUTPUT"
helm-chart:
name: Validate Helm chart
runs-on: ubuntu-latest
needs: artifact-version
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Set up Helm
uses: azure/setup-helm@v4
- name: Lint chart
run: helm lint packaging/helm/openwork-ee
- name: Render chart
run: helm template openwork-ee packaging/helm/openwork-ee --set ingress.enabled=true --set inference.enabled=true
- name: Run chart tests
run: |
for test in packaging/helm/openwork-ee/tests/*.sh; do
bash "$test"
done
- name: Package chart
if: ${{ needs.artifact-version.outputs.chart_version != '' }}
run: |
helm package packaging/helm/openwork-ee \
--version "${{ needs.artifact-version.outputs.chart_version }}" \
--app-version "${{ needs.artifact-version.outputs.chart_version }}" \
--destination dist/charts
- name: Log in to GHCR for Helm
if: ${{ needs.artifact-version.outputs.publish == 'true' && needs.artifact-version.outputs.chart_version != '' }}
run: |
printf '%s' "${{ secrets.GITHUB_TOKEN }}" | helm registry login "${{ env.REGISTRY }}" \
--username "${{ github.actor }}" \
--password-stdin
- name: Publish chart
if: ${{ needs.artifact-version.outputs.publish == 'true' && needs.artifact-version.outputs.chart_version != '' }}
run: helm push "dist/charts/openwork-ee-${{ needs.artifact-version.outputs.chart_version }}.tgz" "oci://${{ env.REGISTRY }}/${{ github.repository_owner }}/charts"
ee-images:
name: Build ${{ matrix.image }}
runs-on: ubuntu-latest
needs: artifact-version
strategy:
fail-fast: false
matrix:
include:
- image: openwork-den-api
dockerfile: packaging/docker/Dockerfile.den
port: 8788
health_path: /health
smoke_env: >-
-e DB_MODE=mysql
-e DEN_DB_ENCRYPTION_KEY=ci-den-db-encryption-key-value-123456
-e BETTER_AUTH_SECRET=ci-better-auth-secret-value-123456
-e BETTER_AUTH_URL=http://127.0.0.1:8788
- image: openwork-den-web
dockerfile: packaging/docker/Dockerfile.den-web
port: 3005
health_path: /api/health
smoke_env: ""
- image: openwork-inference
dockerfile: packaging/docker/Dockerfile.inference
port: 8791
health_path: /health
smoke_env: >-
-e DB_MODE=mysql
-e DATABASE_URL=mysql://smoke:smoke@127.0.0.1:3306/smoke
-e DEN_DB_ENCRYPTION_KEY=ci-den-db-encryption-key-value-123456
# The gateway's own liveness lives at /__gw/health because /health is
# proxied through to the user's instance.
- image: openwork-den-gateway
dockerfile: packaging/docker/Dockerfile.den-gateway
port: 8788
health_path: /__gw/health
smoke_env: >-
-e DEN_API_BASE=http://127.0.0.1:9
-e DEN_GATEWAY_KEY=ci-den-gateway-key-value-123456
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GHCR
if: ${{ github.event_name == 'push' || inputs.push == true }}
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ matrix.image }}
tags: |
type=ref,event=tag
type=semver,pattern={{version}}
type=sha,prefix=sha-
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }}
type=raw,value=dev,enable=${{ github.ref == 'refs/heads/dev' }}
labels: |
org.opencontainers.image.title=${{ matrix.image }}
org.opencontainers.image.source=https://github.com/${{ github.repository }}
- name: Resolve Sentry source-map upload
id: sentry_sourcemaps
if: ${{ needs.artifact-version.outputs.publish == 'true' && github.event_name != 'pull_request' }}
shell: bash
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
SENTRY_RELEASE: ${{ secrets.SENTRY_RELEASE }}
run: |
set -euo pipefail
has_value() {
[[ "$1" =~ [^[:space:]] ]]
}
den_api_upload=false
den_web_upload=false
if has_value "$SENTRY_AUTH_TOKEN" && has_value "$SENTRY_ORG" && has_value "$SENTRY_PROJECT" && has_value "$SENTRY_RELEASE"; then
den_api_upload=true
else
echo "Den API Sentry source-map upload disabled: configure SENTRY_AUTH_TOKEN, SENTRY_ORG, SENTRY_PROJECT, and SENTRY_RELEASE to enable it."
fi
if has_value "$SENTRY_AUTH_TOKEN" && has_value "$SENTRY_ORG" && has_value "$SENTRY_PROJECT"; then
den_web_upload=true
else
echo "Den Web Sentry source-map upload disabled: configure SENTRY_AUTH_TOKEN, SENTRY_ORG, and SENTRY_PROJECT to enable it."
fi
echo "den_api_upload=$den_api_upload" >> "$GITHUB_OUTPUT"
echo "den_web_upload=$den_web_upload" >> "$GITHUB_OUTPUT"
- name: Build and optionally push without Sentry source-map upload
# Build every image except the ones whose Sentry-enabled step below owns
# them. Stated as an exclusion so a newly added matrix entry builds by
# default instead of silently skipping and failing the smoke step.
if: ${{ !((matrix.image == 'openwork-den-api' && steps.sentry_sourcemaps.outputs.den_api_upload == 'true') || (matrix.image == 'openwork-den-web' && steps.sentry_sourcemaps.outputs.den_web_upload == 'true')) }}
uses: docker/build-push-action@v6
with:
context: .
file: ${{ matrix.dockerfile }}
platforms: ${{ (github.event_name == 'pull_request' || github.ref == 'refs/heads/dev') && 'linux/amd64' || 'linux/amd64,linux/arm64' }}
load: ${{ github.event_name == 'pull_request' }}
push: ${{ needs.artifact-version.outputs.publish == 'true' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha,scope=${{ matrix.image }}
cache-to: type=gha,mode=max,scope=${{ matrix.image }}
build-args: |
${{ matrix.image == 'openwork-den-api' && format('DEN_API_VERSION={0}', needs.artifact-version.outputs.chart_version || github.sha) || '' }}
${{ matrix.image == 'openwork-den-gateway' && format('DEN_GATEWAY_VERSION={0}', needs.artifact-version.outputs.chart_version || github.sha) || '' }}
${{ matrix.image == 'openwork-den-api' && 'DEN_UPLOAD_SENTRY_SOURCEMAPS=0' || '' }}
${{ matrix.image == 'openwork-den-web' && 'DEN_WEB_UPLOAD_SENTRY_SOURCEMAPS=0' || '' }}
- name: Build and push with Sentry source-map upload
if: ${{ (matrix.image == 'openwork-den-api' && steps.sentry_sourcemaps.outputs.den_api_upload == 'true') || (matrix.image == 'openwork-den-web' && steps.sentry_sourcemaps.outputs.den_web_upload == 'true') }}
uses: docker/build-push-action@v6
with:
context: .
file: ${{ matrix.dockerfile }}
platforms: ${{ (github.event_name == 'pull_request' || github.ref == 'refs/heads/dev') && 'linux/amd64' || 'linux/amd64,linux/arm64' }}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha,scope=${{ matrix.image }}
cache-to: type=gha,mode=max,scope=${{ matrix.image }}
build-args: |
${{ matrix.image == 'openwork-den-api' && format('DEN_API_VERSION={0}', needs.artifact-version.outputs.chart_version || github.sha) || '' }}
${{ matrix.image == 'openwork-den-gateway' && format('DEN_GATEWAY_VERSION={0}', needs.artifact-version.outputs.chart_version || github.sha) || '' }}
${{ matrix.image == 'openwork-den-api' && 'DEN_UPLOAD_SENTRY_SOURCEMAPS=1' || '' }}
${{ matrix.image == 'openwork-den-web' && 'DEN_WEB_UPLOAD_SENTRY_SOURCEMAPS=1' || '' }}
${{ matrix.image == 'openwork-den-api' && format('DEN_SENTRY_UPLOAD_BUILD_ID={0}-{1}', github.run_id, github.run_attempt) || '' }}
${{ matrix.image == 'openwork-den-web' && format('DEN_WEB_SENTRY_UPLOAD_BUILD_ID={0}-{1}', github.run_id, github.run_attempt) || '' }}
secrets: |
sentry_auth_token=${{ secrets.SENTRY_AUTH_TOKEN }}
sentry_org=${{ secrets.SENTRY_ORG }}
sentry_project=${{ secrets.SENTRY_PROJECT }}
sentry_url=${{ secrets.SENTRY_URL }}
sentry_release=${{ secrets.SENTRY_RELEASE }}
sentry_dist=${{ secrets.SENTRY_DIST }}
- name: Assert Den DB migration assets
if: ${{ github.event_name == 'pull_request' && matrix.image == 'openwork-den-api' }}
shell: bash
env:
IMAGE_TAGS: ${{ steps.meta.outputs.tags }}
run: |
set -euo pipefail
image_tag="${IMAGE_TAGS%%$'\n'*}"
docker run --rm --entrypoint sh "$image_tag" -lc '
test -s /app/ee/packages/den-db/dist/scripts/bootstrap.js
test -s /app/ee/packages/den-db/dist/current-schema.sql
test -s /app/ee/packages/den-db/dist/drizzle/meta/_journal.json
'
- name: Smoke health endpoint
if: ${{ github.event_name == 'pull_request' }}
shell: bash
env:
IMAGE_TAGS: ${{ steps.meta.outputs.tags }}
run: |
set -euo pipefail
image_tag="${IMAGE_TAGS%%$'\n'*}"
container_name="openwork-smoke-${{ matrix.image }}"
mysql_container=""
network_name=""
# shellcheck disable=SC2329 # Invoked by the EXIT trap.
cleanup() {
status=$?
trap - EXIT
set +e
if [ "$status" -ne 0 ]; then
if docker inspect "$container_name" >/dev/null 2>&1; then
echo "Smoke container logs:"
docker logs "$container_name" || true
fi
if [ -n "$mysql_container" ] && docker inspect "$mysql_container" >/dev/null 2>&1; then
echo "MySQL container logs:"
docker logs "$mysql_container" || true
fi
fi
docker rm -f "$container_name" >/dev/null 2>&1 || true
if [ -n "$mysql_container" ]; then
docker rm -f "$mysql_container" >/dev/null 2>&1 || true
fi
if [ -n "$network_name" ]; then
docker network rm "$network_name" >/dev/null 2>&1 || true
fi
exit "$status"
}
trap cleanup EXIT
if [ "${{ matrix.image }}" = "openwork-den-api" ]; then
network_name="openwork-den-api-smoke"
mysql_container="openwork-den-api-smoke-mysql"
database_url="mysql://smoke:smoke@${mysql_container}:3306/smoke"
docker network create "$network_name" >/dev/null
docker run -d \
--name "$mysql_container" \
--network "$network_name" \
-e MYSQL_ROOT_PASSWORD=smoke-root-password \
-e MYSQL_DATABASE=smoke \
-e MYSQL_USER=smoke \
-e MYSQL_PASSWORD=smoke \
mysql:8.4@sha256:b3b90af2a6552ae30c266fdb7d5dd55f3afb72404bb78d37fe8a23eb857fd3fb >/dev/null
mysql_ready="false"
for _ in {1..30}; do
if docker exec \
-e MYSQL_PWD=smoke-root-password \
"$mysql_container" \
mysqladmin ping --host=127.0.0.1 --user=root --silent >/dev/null 2>&1; then
mysql_ready="true"
echo "MySQL is ready"
break
fi
sleep 2
done
if [ "$mysql_ready" != "true" ]; then
echo "MySQL did not become ready" >&2
exit 1
fi
docker run --rm \
--network "$network_name" \
-e DATABASE_URL="$database_url" \
-e DEN_DB_ENCRYPTION_KEY=ci-den-db-encryption-key-value-123456 \
--entrypoint node \
"$image_tag" \
/app/ee/packages/den-db/dist/scripts/bootstrap.js
container_id="$(docker run -d \
--name "$container_name" \
--network "$network_name" \
-P \
-e DATABASE_URL="$database_url" \
${{ matrix.smoke_env }} \
"$image_tag")"
else
container_id="$(docker run -d --name "$container_name" -P ${{ matrix.smoke_env }} "$image_tag")"
fi
host_port=""
for _ in {1..10}; do
host_port="$(docker port "$container_id" "${{ matrix.port }}/tcp" 2>/dev/null | awk -F: 'NR == 1 { print $NF }')"
if [ -n "$host_port" ]; then
break
fi
sleep 1
done
if [ -z "$host_port" ]; then
docker logs "$container_id" || true
exit 1
fi
url="http://127.0.0.1:${host_port}${{ matrix.health_path }}"
for _ in {1..30}; do
if node -e "fetch(process.argv[1]).then((res) => process.exit(res.ok ? 0 : 1)).catch(() => process.exit(1))" "$url"; then
echo "Health endpoint is ready: $url"
exit 0
fi
sleep 2
done
docker logs "$container_id" || true
exit 1
# The gateway is stateless (it serves the SPA and proxies), so dev CD is safe.
# den-api and engine-snapshot dev CD are deliberate follow-ups because they
# require migration and sandbox recycle handling.
redeploy-den-gateway:
needs: [artifact-version, ee-images]
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/dev' }}
runs-on: ubuntu-latest
steps:
- name: Redeploy den-gateway on Render
shell: bash
env:
RENDER_API_KEY: ${{ secrets.RENDER_API_KEY }}
RENDER_DEN_GATEWAY_SERVICE_ID: ${{ secrets.RENDER_DEN_GATEWAY_SERVICE_ID }}
run: |
set -euo pipefail
if [ -z "${RENDER_DEN_GATEWAY_SERVICE_ID:-}" ] || [ -z "${RENDER_API_KEY:-}" ]; then
echo "::notice::Render gateway credentials unavailable; skipping redeploy..."
echo "Render gateway redeploy skipped because credentials are unavailable." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
response="$(curl -s -w '\n%{http_code}' -X POST \
"https://api.render.com/v1/services/${RENDER_DEN_GATEWAY_SERVICE_ID}/deploys" \
-H "Authorization: Bearer $RENDER_API_KEY" \
-H "Content-Type: application/json" \
-d '{}')"
code="$(printf '%s' "$response" | tail -1)"
body="$(printf '%s' "$response" | sed '$d')"
if [ "$code" -ge 400 ]; then
echo "::error::POST /deploys returned HTTP $code: $body"
exit 1
fi
{
echo
echo "Triggered a den-gateway deploy so it pulls the fresh dev image."
} >> "$GITHUB_STEP_SUMMARY"