diff --git a/.github/workflows/dashboard-agent-deploy.yml b/.github/workflows/dashboard-agent-deploy.yml new file mode 100644 index 0000000000..6317fe7c61 --- /dev/null +++ b/.github/workflows/dashboard-agent-deploy.yml @@ -0,0 +1,70 @@ +name: "🤖 Deploy dashboard agent" + +# Deploys the @internal/dashboard-agent chat.agent to its Trigger.dev project +# with --skip-promotion, so a deploy never becomes "current" on its own. The +# consuming app cuts over by pinning DASHBOARD_AGENT_VERSION to the new version. +# Runs a leg per environment (staging + prod), each gated by its own environment; +# a push to main that touches the agent or its store triggers both. Version +# numbers are per-environment, so pin each environment to its own leg's version. + +on: + push: + branches: [main] + paths: + - "internal-packages/dashboard-agent/**" + - "internal-packages/dashboard-agent-db/**" + workflow_dispatch: + +permissions: {} + +jobs: + deploy: + name: Deploy dashboard agent (${{ matrix.environment }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + environment: [staging, prod] + # Per-environment reviewer gate + source of the scoped deploy PAT. + environment: dashboard-agent-${{ matrix.environment }} + concurrency: + group: dashboard-agent-deploy-${{ matrix.environment }} + cancel-in-progress: false + permissions: + contents: read + env: + TRIGGER_API_URL: https://api.trigger.dev + TRIGGER_DASHBOARD_AGENT_PROJECT_REF: ${{ vars.TRIGGER_DASHBOARD_AGENT_PROJECT_REF }} + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + + - name: Setup pnpm + uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 + with: + version: 10.33.2 + + - name: Setup node + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: 20.20.2 + cache: "pnpm" + + - name: Install + build the CLI and the agent's deps + run: | + set -euo pipefail + pnpm install --frozen-lockfile + # Prisma client is needed because the build closure pulls in @trigger.dev/database. + pnpm run generate + # Config-time imports the agent's trigger.config.ts needs: defineConfig (sdk), aptGet (build). + pnpm run build --filter trigger.dev --filter @trigger.dev/build --filter @trigger.dev/sdk + + - name: Deploy (--skip-promotion) + working-directory: internal-packages/dashboard-agent + env: + TRIGGER_ACCESS_TOKEN: ${{ secrets.TRIGGER_DASHBOARD_AGENT_DEPLOY_TOKEN }} + # Invoke the built CLI directly (what the workspace .bin/trigger wrapper does), + # so a not-yet-linked bin after a fresh install can't break the deploy. + run: node ../../packages/cli-v3/dist/esm/index.js deploy --skip-promotion --env ${{ matrix.environment }} diff --git a/apps/webapp/app/env.server.ts b/apps/webapp/app/env.server.ts index 7c57733ad8..2462a3fdd3 100644 --- a/apps/webapp/app/env.server.ts +++ b/apps/webapp/app/env.server.ts @@ -106,6 +106,9 @@ const EnvironmentSchema = z // standard chat.agent SDK flow. When unset, the live agent is disabled — the // conversation store / History still work, no chat can start. DASHBOARD_AGENT_SECRET_KEY: z.string().optional(), + // Pins agent sessions to a specific deployed version (paired with + // --skip-promotion deploys); unset => the project env's current version. + DASHBOARD_AGENT_VERSION: z.string().optional(), // Global default for the `hasDashboardAgentAccess` flag. "0" (off) ships the // agent dark; flip to "1" to enable it for everyone at GA. Per-org overrides // (org featureFlags) win regardless. diff --git a/apps/webapp/app/services/dashboardAgent.server.ts b/apps/webapp/app/services/dashboardAgent.server.ts index 8e8b8884e3..a66882b72c 100644 --- a/apps/webapp/app/services/dashboardAgent.server.ts +++ b/apps/webapp/app/services/dashboardAgent.server.ts @@ -57,13 +57,22 @@ export function isDashboardAgentConfigured(): boolean { return Boolean(env.DASHBOARD_AGENT_SECRET_KEY); } +// Pins every agent session (and its continuation runs) to a deployed version +// when DASHBOARD_AGENT_VERSION is set; unset runs on the env's current version. +export function dashboardAgentTriggerConfig(): { lockToVersion: string } | undefined { + return env.DASHBOARD_AGENT_VERSION ? { lockToVersion: env.DASHBOARD_AGENT_VERSION } : undefined; +} + export async function startDashboardAgentSession(params: { chatId: string; clientData?: Record; }): Promise<{ publicAccessToken: string }> { const config = dashboardAgentConfig(); if (!config) throw new Error("DASHBOARD_AGENT_SECRET_KEY is not set"); - const startSession = chat.createStartSessionAction(TASK_ID, { apiClient: config }); + const startSession = chat.createStartSessionAction(TASK_ID, { + apiClient: config, + triggerConfig: dashboardAgentTriggerConfig(), + }); return startSession({ chatId: params.chatId, clientData: params.clientData }); } diff --git a/apps/webapp/app/services/dashboardAgentHeadStart.server.ts b/apps/webapp/app/services/dashboardAgentHeadStart.server.ts index d30f8da7ab..9c411c8040 100644 --- a/apps/webapp/app/services/dashboardAgentHeadStart.server.ts +++ b/apps/webapp/app/services/dashboardAgentHeadStart.server.ts @@ -9,7 +9,10 @@ import { import { chat as chatServer } from "@trigger.dev/sdk/chat-server"; import { streamText, type UIMessage } from "ai"; import { env } from "~/env.server"; -import { dashboardAgentApiOrigin } from "~/services/dashboardAgent.server"; +import { + dashboardAgentApiOrigin, + dashboardAgentTriggerConfig, +} from "~/services/dashboardAgent.server"; import { logger } from "~/services/logger.server"; const TASK_ID = "dashboard-agent"; @@ -43,6 +46,7 @@ export async function startDashboardAgentHeadStart(params: { chatId: params.chatId, messages: params.messages, metadata: params.metadata, + triggerConfig: dashboardAgentTriggerConfig(), // Scope session creation + the agent trigger to the agent's project/env. The // Anthropic key here only powers the warm step-1 call. apiClient: {