feat: Add LangGraph Deployment GitHub Action and TypeScript Deployment Script (#550)

* Apply patch

* Apply patch

* Apply patch

* Apply patch

* Apply patch

* Apply patch

* Apply patch

* Apply patch

* Apply patch

* Apply patch

* Apply patch

* move into workflows dir

* cr

---------

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
Co-authored-by: bracesproul <braceasproul@gmail.com>
This commit is contained in:
open-swe[bot] 2025-07-28 18:32:11 -07:00 • committed by GitHub
parent d0fe5f5323
commit 8eb83c11e5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 289 additions and 0 deletions

60
.github/workflows/deploy-langgraph.yml vendored Normal file
View file

@ -0,0 +1,60 @@
name: Deploy LangGraph
on:
push:
branches: ["main"]
paths:
- "apps/open-swe/**"
- "packages/shared/**"
- "package.json"
workflow_dispatch: # Allows manual triggering
# If another push happens while this workflow is still running,
# cancel the earlier run in favor of the next run.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
deploy:
name: Deploy to LangGraph Platform
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Enable Corepack
run: corepack enable
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20.x
cache: "yarn"
- name: Install dependencies
run: yarn install --immutable --mode=skip-build
- name: Build packages
run: yarn build
- name: Compile deployment script
run: |
cd apps/open-swe
yarn tsc --module commonjs --skipLibCheck --outDir ./dist/scripts ./scripts/deploy-langgraph.ts
mv ./dist/scripts/deploy-langgraph.js ./dist/scripts/deploy-langgraph.cjs
- name: Execute deployment
run: |
cd apps/open-swe
node ./dist/scripts/deploy-langgraph.cjs
env:
CONTROL_PLANE_HOST: ${{ secrets.CONTROL_PLANE_HOST }}
LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }}
INTEGRATION_ID: ${{ secrets.INTEGRATION_ID }}
DEPLOYMENT_ID: ${{ secrets.DEPLOYMENT_ID }}
timeout-minutes: 35
- name: Deployment status
if: always()
run: echo "Deployment workflow completed with status ${{ job.status }}"

View file

@ -0,0 +1,229 @@
import { createLogger, LogLevel } from "../src/utils/logger.js";
const logger = createLogger(LogLevel.INFO, "DeployLangGraph");
interface DeploymentConfig {
controlPlaneHost: string;
langsmithApiKey: string;
integrationId: string;
deploymentId?: string;
}
interface DeploymentResponse {
id: string;
latest_revision_id: string;
}
interface RevisionResponse {
id: string;
status: string;
}
interface RevisionsListResponse {
resources: RevisionResponse[];
}
const MAX_WAIT_TIME = 1800; // 30 minutes
const POLL_INTERVAL = 60; // 60 seconds
function getRequiredEnvVar(name: string): string {
const value = process.env[name];
if (!value) {
throw new Error(`Required environment variable ${name} is not set`);
}
return value;
}
function getDeploymentConfig(): DeploymentConfig {
return {
controlPlaneHost: getRequiredEnvVar("CONTROL_PLANE_HOST"),
langsmithApiKey: getRequiredEnvVar("LANGSMITH_API_KEY"),
integrationId: getRequiredEnvVar("INTEGRATION_ID"),
deploymentId: process.env.DEPLOYMENT_ID,
};
}
function getHeaders(
apiKey: string,
includeContentType = false,
): Record<string, string> {
const headers: Record<string, string> = {
"X-Api-Key": apiKey,
};
if (includeContentType) {
headers["Content-Type"] = "application/json";
}
return headers;
}
async function makeRequest<T>(
url: string,
options: RequestInit,
expectedStatus: number,
): Promise<T> {
try {
const response = await fetch(url, options);
if (response.status !== expectedStatus) {
const errorText = await response.text();
throw new Error(
`Request failed with status ${response.status}: ${errorText}`,
);
}
if (expectedStatus === 204) {
return {} as T;
}
return (await response.json()) as T;
} catch (error) {
if (error instanceof Error) {
throw new Error(`HTTP request failed: ${error.message}`);
}
throw new Error("HTTP request failed with unknown error");
}
}
async function getDeployment(
config: DeploymentConfig,
deploymentId: string,
): Promise<DeploymentResponse> {
logger.info(`Getting deployment ${deploymentId}`);
const url = `${config.controlPlaneHost}/v2/deployments/${deploymentId}`;
const options: RequestInit = {
method: "GET",
headers: getHeaders(config.langsmithApiKey),
};
return makeRequest<DeploymentResponse>(url, options, 200);
}
async function listRevisions(
config: DeploymentConfig,
deploymentId: string,
): Promise<RevisionsListResponse> {
logger.info(`Listing revisions for deployment ${deploymentId}`);
const url = `${config.controlPlaneHost}/v2/deployments/${deploymentId}/revisions`;
const options: RequestInit = {
method: "GET",
headers: getHeaders(config.langsmithApiKey),
};
return makeRequest<RevisionsListResponse>(url, options, 200);
}
async function getRevision(
config: DeploymentConfig,
deploymentId: string,
revisionId: string,
): Promise<RevisionResponse> {
const url = `${config.controlPlaneHost}/v2/deployments/${deploymentId}/revisions/${revisionId}`;
const options: RequestInit = {
method: "GET",
headers: getHeaders(config.langsmithApiKey),
};
return makeRequest<RevisionResponse>(url, options, 200);
}
async function patchDeployment(
config: DeploymentConfig,
deploymentId: string,
): Promise<void> {
logger.info(`Patching deployment ${deploymentId} to trigger new revision`);
const url = `${config.controlPlaneHost}/v2/deployments/${deploymentId}`;
const requestBody = {
source_revision_config: {
repo_ref: "main",
langgraph_config_path: "langgraph.json",
},
};
const options: RequestInit = {
method: "PATCH",
headers: getHeaders(config.langsmithApiKey, true),
body: JSON.stringify(requestBody),
};
await makeRequest<void>(url, options, 200);
logger.info(`Successfully patched deployment ${deploymentId}`);
}
async function waitForDeployment(
config: DeploymentConfig,
deploymentId: string,
revisionId: string,
): Promise<void> {
logger.info(`Waiting for revision ${revisionId} to be deployed`);
const startTime = Date.now();
while (Date.now() - startTime < MAX_WAIT_TIME * 1000) {
const revision = await getRevision(config, deploymentId, revisionId);
const status = revision.status;
logger.info(`Revision ${revisionId} status: ${status}`);
if (status === "DEPLOYED") {
logger.info(`Revision ${revisionId} successfully deployed`);
return;
}
if (status.includes("FAILED")) {
throw new Error(`Revision ${revisionId} failed with status: ${status}`);
}
logger.info(`Waiting ${POLL_INTERVAL} seconds before next status check...`);
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL * 1000));
}
throw new Error(
`Timeout waiting for revision ${revisionId} to be deployed after ${MAX_WAIT_TIME} seconds`,
);
}
async function deployLangGraph(): Promise<void> {
try {
logger.info("Starting LangGraph deployment process");
const config = getDeploymentConfig();
if (!config.deploymentId) {
throw new Error("DEPLOYMENT_ID environment variable is required");
}
// Verify deployment exists
await getDeployment(config, config.deploymentId);
// Patch deployment to trigger new revision
await patchDeployment(config, config.deploymentId);
// Get the latest revision after patching
const revisions = await listRevisions(config, config.deploymentId);
const latestRevision = revisions.resources[0];
if (!latestRevision) {
throw new Error("No revisions found for deployment");
}
// Wait for the new revision to be deployed
await waitForDeployment(config, config.deploymentId, latestRevision.id);
logger.info("LangGraph deployment completed successfully");
} catch (error) {
logger.error("LangGraph deployment failed", {
error: error instanceof Error ? error.message : String(error),
});
process.exit(1);
}
}
// Execute deployment if this script is run directly
if (import.meta.url === `file://${process.argv[1]}`) {
deployLangGraph();
}