463 lines
18 KiB
YAML
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"
|