diff --git a/.github/docker/Dockerfile.web b/.github/docker/Dockerfile.web new file mode 100644 index 000000000..84a51df20 --- /dev/null +++ b/.github/docker/Dockerfile.web @@ -0,0 +1,61 @@ +FROM oven/bun:1.2.18 AS build + +WORKDIR /app +COPY . . + +ARG BASEURL=http://localhost:3000/api +ARG GOOGLE_CLIENT_ID= +ARG POSTHOG_KEY= +ARG POSTHOG_HOST= +ARG COMPASS_BUILD_REF=self-host + +ENV COMPASS_BUILD_REF=${COMPASS_BUILD_REF} +ENV NODE_ENV=production +ENV WEB_PORT=9080 + +RUN printf '%s\n' \ + 'runtime:' \ + ' nodeEnv: production' \ + ' timezone: Etc/UTC' \ + 'web:' \ + ' url: http://localhost:9080' \ + 'backend:' \ + " apiUrl: ${BASEURL}" \ + ' compassToken: unused-web-build' \ + 'mongo:' \ + ' uri: mongodb://localhost:27017/unused' \ + 'supertokens:' \ + ' uri: http://localhost:3567' \ + ' key: unused-web-build' \ + 'google:' \ + " clientId: ${GOOGLE_CLIENT_ID}" \ + > compass.yaml + +RUN if [ -n "$POSTHOG_KEY" ] && [ -n "$POSTHOG_HOST" ]; then \ + printf '%s\n' \ + 'posthog:' \ + " key: ${POSTHOG_KEY}" \ + " host: ${POSTHOG_HOST}" \ + >> compass.yaml; \ + fi + +RUN bun install --frozen-lockfile +RUN cd packages/web && bun run build.ts + +FROM oven/bun:1.2.18-slim AS runtime + +WORKDIR /app + +RUN addgroup --system --gid 1001 compass \ + && adduser --system --uid 1001 --ingroup compass --no-create-home compass + +ENV WEB_PORT=9080 +ENV WEB_ROOT=/app/build/web + +COPY --from=build --chown=compass:compass /app/build/web ./build/web +COPY --from=build --chown=compass:compass /app/self-host/serve-web.ts ./self-host/serve-web.ts + +USER compass + +EXPOSE 9080 +CMD ["bun", "self-host/serve-web.ts"] diff --git a/.github/workflows/_deploy-environment.yml b/.github/workflows/_deploy-environment.yml index 52620010d..88683d519 100644 --- a/.github/workflows/_deploy-environment.yml +++ b/.github/workflows/_deploy-environment.yml @@ -47,7 +47,7 @@ jobs: uses: docker/build-push-action@v7 with: context: . - file: self-host/Dockerfile.web + file: .github/docker/Dockerfile.web push: true build-args: | BASEURL=${{ vars.BACKEND_API_URL }} @@ -67,6 +67,7 @@ jobs: GCAL_NOTIFICATION_EXPIRATION_MIN: ${{ vars.GCAL_NOTIFICATION_EXPIRATION_MIN }} GOOGLE_CLIENT_ID: ${{ vars.GOOGLE_CLIENT_ID }} IMAGE_VERSION: ${{ steps.version.outputs.image_version }} + KIT_USER_TAG_ID: ${{ inputs.environment == 'production' && vars.KIT_USER_TAG_ID || '' }} POSTHOG_KEY: ${{ (inputs.environment == 'production' || inputs.environment == 'staging-cloud') && vars.POSTHOG_KEY || '' }} POSTHOG_HOST: ${{ (inputs.environment == 'production' || inputs.environment == 'staging-cloud') && vars.POSTHOG_HOST || '' }} RELEASE_TAG: ${{ inputs.tag }} @@ -77,6 +78,7 @@ jobs: COMPASS_SYNC_TOKEN: ${{ secrets.COMPASS_SYNC_TOKEN }} GCAL_NOTIFICATION_TOKEN: ${{ secrets.GCAL_NOTIFICATION_TOKEN }} GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }} + KIT_API_SECRET: ${{ inputs.environment == 'production' && secrets.KIT_API_SECRET || '' }} MONGO_PASSWORD: ${{ secrets.MONGO_PASSWORD }} MONGO_REPLICA_SET_KEY: ${{ secrets.MONGO_REPLICA_SET_KEY }} MONGO_URI: ${{ secrets.MONGO_URI }} @@ -86,6 +88,12 @@ jobs: SUPERTOKENS_URI: ${{ secrets.SUPERTOKENS_URI }} run: | echo "Deploying Compass ${RELEASE_TAG} (image version: ${IMAGE_VERSION}) to ${{ inputs.environment }}" + if [ "${{ inputs.environment }}" = "production" ]; then + if [ -z "$KIT_API_SECRET" ] || [ -z "$KIT_USER_TAG_ID" ]; then + echo "Production deploy requires KIT_API_SECRET and KIT_USER_TAG_ID." >&2 + exit 1 + fi + fi mkdir -p ~/.ssh echo "$SSH_PRIVATE_KEY" > ~/.ssh/staging_key chmod 600 ~/.ssh/staging_key @@ -129,6 +137,12 @@ jobs: " key: \"${POSTHOG_KEY}\"" \ " host: \"${POSTHOG_HOST}\"" fi + if [ -n "$KIT_API_SECRET" ] && [ -n "$KIT_USER_TAG_ID" ]; then + printf '%s\n' \ + 'email:' \ + " kitApiSecret: \"${KIT_API_SECRET}\"" \ + " kitUserTagId: \"${KIT_USER_TAG_ID}\"" + fi } | ssh -i ~/.ssh/staging_key "$SSH_USER@$SSH_HOST" \ "umask 077 && mkdir -p ~/compass && cat > ~/compass/compass.yaml && chmod 644 ~/compass/compass.yaml" COMPOSE_GIT_REF="${COMPOSE_GIT_REF:-${RELEASE_TAG}}" diff --git a/docs/CI-CD/workflows.md b/docs/CI-CD/workflows.md index 926024b73..f3e1108ba 100644 --- a/docs/CI-CD/workflows.md +++ b/docs/CI-CD/workflows.md @@ -144,3 +144,8 @@ The workflow deploys to the GitHub `production` environment through `switchbacktech/compass-web:production-`, then runs `deploy-health-check.yml` with the `cloud` profile. Production is expected to use external MongoDB and SuperTokens Cloud rather than self-hosted data services. + +For cloud web image builds, `_deploy-environment.yml` uses +`.github/docker/Dockerfile.web` so frontend-only cloud config, such as PostHog, +is baked into the bundle without adding cloud-only settings to the self-host +Dockerfile. diff --git a/self-host/Dockerfile.web b/self-host/Dockerfile.web index 84a51df20..1b5d35bb7 100644 --- a/self-host/Dockerfile.web +++ b/self-host/Dockerfile.web @@ -5,8 +5,6 @@ COPY . . ARG BASEURL=http://localhost:3000/api ARG GOOGLE_CLIENT_ID= -ARG POSTHOG_KEY= -ARG POSTHOG_HOST= ARG COMPASS_BUILD_REF=self-host ENV COMPASS_BUILD_REF=${COMPASS_BUILD_REF} @@ -31,14 +29,6 @@ RUN printf '%s\n' \ " clientId: ${GOOGLE_CLIENT_ID}" \ > compass.yaml -RUN if [ -n "$POSTHOG_KEY" ] && [ -n "$POSTHOG_HOST" ]; then \ - printf '%s\n' \ - 'posthog:' \ - " key: ${POSTHOG_KEY}" \ - " host: ${POSTHOG_HOST}" \ - >> compass.yaml; \ - fi - RUN bun install --frozen-lockfile RUN cd packages/web && bun run build.ts diff --git a/self-host/docker-compose.test.ts b/self-host/docker-compose.test.ts index e33ee145a..5df6eacb1 100644 --- a/self-host/docker-compose.test.ts +++ b/self-host/docker-compose.test.ts @@ -85,6 +85,14 @@ describe("self-host docker compose", () => { expect(dockerfile).not.toContain("--environment"); }); + it("keeps PostHog out of the self-host web image", () => { + const dockerfile = readRepoFile("self-host/Dockerfile.web"); + + expect(dockerfile).not.toContain("COMPASS_WEB_BUILD_CONFIG_B64"); + expect(dockerfile).not.toContain("POSTHOG_"); + expect(dockerfile).not.toContain("posthog:"); + }); + it("mounts compass.yaml into the backend container", () => { const compose = readFileSync(join(import.meta.dir, "compose.yaml"), { encoding: "utf8", @@ -168,6 +176,51 @@ describe("staging deploy workflow", () => { ); }); + it("builds cloud deploy web images from a GitHub-only Dockerfile with PostHog config", () => { + const workflow = readRepoFile(".github/workflows/_deploy-environment.yml"); + const dockerfile = readRepoFile(".github/docker/Dockerfile.web"); + + expect(workflow).toContain("file: .github/docker/Dockerfile.web"); + expect(workflow).toContain("POSTHOG_KEY=$"); + expect(workflow).toContain("POSTHOG_HOST=$"); + expect(workflow).not.toContain("COMPASS_WEB_BUILD_CONFIG_B64"); + expect(workflow).not.toContain("base64"); + expect(dockerfile).toContain("ARG POSTHOG_KEY="); + expect(dockerfile).toContain("ARG POSTHOG_HOST="); + expect(dockerfile).toContain("'posthog:'"); + }); + + it("writes Kit email config only for production deploys", () => { + const workflow = readRepoFile(".github/workflows/_deploy-environment.yml"); + + expect(workflow).toContain( + "KIT_USER_TAG_ID: $".concat( + "{{ inputs.environment == 'production' && vars.KIT_USER_TAG_ID || '' }}", + ), + ); + expect(workflow).toContain( + "KIT_API_SECRET: $".concat( + "{{ inputs.environment == 'production' && secrets.KIT_API_SECRET || '' }}", + ), + ); + expect(workflow).toContain( + 'if [ "$'.concat('{{ inputs.environment }}" = "production" ]; then'), + ); + expect(workflow).toContain( + "Production deploy requires KIT_API_SECRET and KIT_USER_TAG_ID", + ); + expect(workflow).toContain( + 'if [ -n "$KIT_API_SECRET" ] && [ -n "$KIT_USER_TAG_ID" ]; then', + ); + expect(workflow).toContain("'email:'"); + expect(workflow).toContain( + 'kitApiSecret: \\"$'.concat('{KIT_API_SECRET}\\"'), + ); + expect(workflow).toContain( + 'kitUserTagId: \\"$'.concat('{KIT_USER_TAG_ID}\\"'), + ); + }); + it("runs deploy health checks after each staging deploy", () => { const workflow = readRepoFile(".github/workflows/deploy-staging.yml");