feat(webapp): pin the dashboard agent to a deployed version (#4128)

## Summary

The dashboard agent runs as a `chat.agent` in its own Trigger.dev
project, deployed separately from the app that starts its sessions. This
adds independent, non-disruptive deploys for it: a new workflow deploys
the agent with `--skip-promotion` (so a deploy never becomes the current
version on its own), and the app pins its sessions to a chosen version.

## How it works

`DASHBOARD_AGENT_VERSION` (unset by default) pins agent sessions to a
specific deployed version; when unset, sessions run on the project
environment's current version. The pin is passed on session start and
head start and is forwarded to every continuation run, so a pinned
session stays on its version for its whole life. Cutting over to a new
build becomes a config change (set the version) rather than a redeploy,
and rollback is flipping it back.

The workflow (`dashboard-agent-deploy.yml`) runs a leg per environment
(staging and prod), each gated by its own environment, and triggers on
pushes to `main` that touch the agent or its store (also available via
manual dispatch). Deploy versions are per-environment, so each
environment pins to its own leg's version.
This commit is contained in:
Eric Allam
2026-07-03 10:19:38 +01:00
committed by GitHub
parent 0f349dd636
commit ea9d6f7430
4 changed files with 88 additions and 2 deletions
@@ -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 }}
+3
View File
@@ -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.
@@ -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<string, unknown>;
}): 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 });
}
@@ -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: {