From 97f49db6d7eb6d46d258a1b9283c9c61ed237a57 Mon Sep 17 00:00:00 2001 From: Andrew Khor Date: Thu, 19 Mar 2026 16:52:31 +0800 Subject: [PATCH] fix: railway deployment, add job data into offering handlers --- .gitignore | 1 - Dockerfile | 18 ++++++++++ src/commands/deploy.ts | 52 +++++++++++++++++++++++------ src/deploy/docker.ts | 16 +++++++-- src/deploy/railway.ts | 31 +++++++++++++++++ src/seller/runtime/offeringTypes.ts | 14 ++++++-- src/seller/runtime/seller.ts | 6 ++-- 7 files changed, 119 insertions(+), 19 deletions(-) create mode 100644 Dockerfile diff --git a/.gitignore b/.gitignore index 8dc6bfb..3e36a04 100644 --- a/.gitignore +++ b/.gitignore @@ -35,7 +35,6 @@ coverage/ .railway/ # Generated deploy artifacts -Dockerfile .dockerignore # Optional: local OpenClaw / Moltbot diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..10eb626 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,18 @@ +FROM node:20-slim +WORKDIR /app + +COPY package.json package-lock.json* ./ +RUN npm ci + +COPY tsconfig.json ./ +COPY bin/ ./bin/ +COPY src/ ./src/ + +RUN find src/seller/offerings -mindepth 2 -maxdepth 3 -name "package.json" | \ + while IFS= read -r pkg; do \ + dir=$(dirname "$pkg"); \ + echo ">>> Installing deps in $dir"; \ + npm install --omit=dev --prefix "$dir" || exit 1; \ + done + +CMD ["npm", "run", "start"] diff --git a/src/commands/deploy.ts b/src/commands/deploy.ts index ba48a9a..9adc840 100644 --- a/src/commands/deploy.ts +++ b/src/commands/deploy.ts @@ -15,6 +15,7 @@ import * as fs from "fs"; import * as path from "path"; import { execSync } from "child_process"; +import { parse as parseDotenv } from "dotenv"; import readline from "readline"; import * as output from "../lib/output.js"; import { readConfig, writeConfig, getActiveAgent, sanitizeAgentName, ROOT } from "../lib/config.js"; @@ -240,7 +241,39 @@ export async function deploy(): Promise { output.log(` Offerings: ${offerings.join(", ")}`); output.log(""); - // Deploy (this creates the service on first run) + // Ensure service is valid before deploying; if stale, clear it so railway up creates a new one + let isNewService = false; + if (!railway.isServiceValid()) { + output.log( + " Linked service not found. Clearing stale link — Railway will create a new service on deploy..." + ); + railway.clearLinkedService(); + isNewService = true; + } + + // Collect env vars to sync + const config = readConfig(); + const dotenvVars: Record = fs.existsSync(path.resolve(ROOT, ".env")) + ? parseDotenv(fs.readFileSync(path.resolve(ROOT, ".env"))) + : {}; + const envVars: Record = { ...dotenvVars }; + if (config.LITE_AGENT_API_KEY) envVars["LITE_AGENT_API_KEY"] = config.LITE_AGENT_API_KEY; + + if (!isNewService && Object.keys(envVars).length > 0) { + try { + railway.setVariables(envVars); + output.success(`Set ${Object.keys(envVars).length} env var(s) on Railway`); + } catch { + output.warn( + `Could not set env vars. Set them manually:\n` + + Object.keys(envVars) + .map((k) => ` acp serve deploy railway env set ${k}=`) + .join("\n") + ); + } + } + + // Deploy output.log(" Deploying to Railway...\n"); railway.up(); @@ -257,18 +290,17 @@ export async function deploy(): Promise { } } - // Set API key on the service - const config = readConfig(); - const apiKey = config.LITE_AGENT_API_KEY; - if (apiKey) { + // For new services, set env vars after deploy once the service exists + if (isNewService && Object.keys(envVars).length > 0) { try { - railway.setVariable("LITE_AGENT_API_KEY", apiKey); - output.success("Set LITE_AGENT_API_KEY on Railway"); + railway.setVariables(envVars); + output.success(`Set ${Object.keys(envVars).length} env var(s) on Railway`); } catch { output.warn( - "Could not set LITE_AGENT_API_KEY. Set it manually:\n" + - " acp serve deploy railway env set LITE_AGENT_API_KEY=" + - apiKey + `Could not set env vars. Set them manually:\n` + + Object.keys(envVars) + .map((k) => ` acp serve deploy railway env set ${k}=`) + .join("\n") ); } } diff --git a/src/deploy/docker.ts b/src/deploy/docker.ts index 6df1a9f..571742e 100644 --- a/src/deploy/docker.ts +++ b/src/deploy/docker.ts @@ -6,17 +6,29 @@ export function generateDockerfile(): string { return `FROM node:20-slim WORKDIR /app + COPY package.json package-lock.json* ./ -RUN npm install --production=false +RUN npm ci + COPY tsconfig.json ./ COPY bin/ ./bin/ COPY src/ ./src/ -CMD ["npx", "tsx", "src/seller/runtime/seller.ts"] + +RUN find src/seller/offerings -mindepth 2 -maxdepth 3 -name "package.json" | \ + while IFS= read -r pkg; do \ + dir=$(dirname "$pkg"); \ + echo ">>> Installing deps in $dir"; \ + npm install --omit=dev --prefix "$dir" || exit 1; \ + done + +CMD ["npm", "run", "start"] + `; } export function generateDockerignore(): string { return `node_modules +src/seller/offerings/**/node_modules dist build logs diff --git a/src/deploy/railway.ts b/src/deploy/railway.ts index 07c753f..a244aa9 100644 --- a/src/deploy/railway.ts +++ b/src/deploy/railway.ts @@ -151,6 +151,27 @@ export function hasLinkedService(): boolean { } } +/** Returns true if the linked service exists and is reachable in the project. */ +export function isServiceValid(): boolean { + try { + const status = execSync("railway status", { + ...EXEC_OPTS, + stdio: ["pipe", "pipe", "pipe"], + }).trim(); + return !status.includes("Service: None") && !status.includes("not found in project"); + } catch { + return false; + } +} + +/** Clear the linked service so the next `railway up` creates a fresh one. */ +export function clearLinkedService(): void { + const global = readGlobalConfig(); + if (!global?.projects?.[ROOT]) return; + global.projects[ROOT].service = null; + writeGlobalConfig(global); +} + // -- Variables -- export function setVariable(key: string, value: string): void { @@ -160,6 +181,16 @@ export function setVariable(key: string, value: string): void { }); } +export function setVariables(vars: Record): void { + const pairs = Object.entries(vars) + .map(([k, v]) => `${k}="${v}"`) + .join(" "); + execSync(`railway variables set ${pairs}`, { + ...EXEC_OPTS, + stdio: ["pipe", "pipe", "pipe"], + }); +} + export function deleteVariable(key: string): void { execSync(`railway variables delete ${key}`, { ...EXEC_OPTS, diff --git a/src/seller/runtime/offeringTypes.ts b/src/seller/runtime/offeringTypes.ts index dcc8452..0140c0b 100644 --- a/src/seller/runtime/offeringTypes.ts +++ b/src/seller/runtime/offeringTypes.ts @@ -3,6 +3,7 @@ // ============================================================================= /** Optional token-transfer instruction returned by an offering handler. */ +import type { AcpJobEventData } from "./types.js"; export interface TransferInstruction { /** Token contract address (e.g. ERC-20 CA). */ ca: string; @@ -42,10 +43,17 @@ export type ValidationResult = boolean | { valid: boolean; reason?: string }; export interface OfferingHandlers { executeJob: (request: Record) => Promise; validateRequirements?: ( - request: Record + request: Record, + data: AcpJobEventData ) => ValidationResult | Promise; - requestPayment?: (request: Record) => string | Promise; - requestAdditionalFunds?: (request: Record) => + requestPayment?: ( + request: Record, + data: AcpJobEventData + ) => string | Promise; + requestAdditionalFunds?: ( + request: Record, + data: AcpJobEventData + ) => | { content?: string; amount: number; diff --git a/src/seller/runtime/seller.ts b/src/seller/runtime/seller.ts index cdb78d4..13e5533 100644 --- a/src/seller/runtime/seller.ts +++ b/src/seller/runtime/seller.ts @@ -112,7 +112,7 @@ async function handleNewTask(data: AcpJobEventData): Promise { const { config, handlers } = await loadOffering(offeringName, agentDirName); if (handlers.validateRequirements) { - const validationResult = await handlers.validateRequirements(requirements); + const validationResult = await handlers.validateRequirements(requirements, data); let isValid: boolean; let reason: string | undefined; @@ -145,11 +145,11 @@ async function handleNewTask(data: AcpJobEventData): Promise { const funds = config.requiredFunds && handlers.requestAdditionalFunds - ? await handlers.requestAdditionalFunds(requirements) + ? await handlers.requestAdditionalFunds(requirements, data) : undefined; const paymentReason = handlers.requestPayment - ? await handlers.requestPayment(requirements) + ? await handlers.requestPayment(requirements, data) : (funds?.content ?? "Request accepted"); await requestPayment(jobId, {