diff --git a/CHANGELOG.md b/CHANGELOG.md index abed858dd..0329647a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # @qawolf/cli +## 1.3.1 + +### Patch Changes + +- ac2d20d: Allow `QAWOLF_HOST_URL` to target a custom QA Wolf deployment host. + ## 1.3.0 ### Minor Changes diff --git a/package.json b/package.json index cbaf3642b..cb9db7e37 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@qawolf/cli", - "version": "1.3.0", + "version": "1.3.1", "description": "Run and manage QA Wolf flows from the terminal, CI, or an AI agent", "keywords": [ "automation", diff --git a/skills/qawolf-cli/SKILL.md b/skills/qawolf-cli/SKILL.md index 2b8688762..b7b677565 100644 --- a/skills/qawolf-cli/SKILL.md +++ b/skills/qawolf-cli/SKILL.md @@ -19,6 +19,11 @@ environment variable (or stored credentials from `qawolf auth login`). their table entry notes a flag that switches them to `read`. Verify with `qawolf auth whoami`. Never print or log the key. +Commands use `https://app.qawolf.com` by default. Set `QAWOLF_HOST_URL` to +target another deployment host, for example +`https://app.staging.example.com`. `QAWOLF_API_URL` is a separate API endpoint +and does not select the deployment host used by CLI commands. + ## Output When consuming output programmatically, always pass `--json` (or `--agent`). diff --git a/src/commands/context.ts b/src/commands/context.ts index 7247ffab4..9190396a5 100644 --- a/src/commands/context.ts +++ b/src/commands/context.ts @@ -5,6 +5,7 @@ import { authMessages } from "~/core/messages/index.js"; import { getConfigDir } from "~/core/paths.js"; import { exitCodes } from "~/shell/exit.js"; import { makeDefaultFs } from "~/shell/fs.js"; +import { resolveHostUrl } from "~/shell/resolveHostUrl.js"; import { requireApiKey } from "~/domains/auth/index.js"; import { createLoggingSystem, @@ -59,8 +60,7 @@ export function buildBaseContext( logPath: defaultLogPath(), ...(verboseWrite ? { verboseWrite } : {}), }); - const apiBaseUrl = - env["QAWOLF_API_URL"]?.replace(/\/+$/, "") || "https://app.qawolf.com"; + const apiBaseUrl = resolveHostUrl(env); return { ctx: { ui: createUI(outputMode, { diff --git a/src/commands/flows/runWorker.register.ts b/src/commands/flows/runWorker.register.ts index 4559535e4..ce3163b04 100644 --- a/src/commands/flows/runWorker.register.ts +++ b/src/commands/flows/runWorker.register.ts @@ -6,6 +6,7 @@ import type { Reporter } from "~/shell/reporter/types.js"; import type { SignalRegistry } from "~/shell/signals/createSignalRegistry.js"; import { getConfigDir } from "~/core/paths.js"; import { makeDefaultFs } from "~/shell/fs.js"; +import { resolveHostUrl } from "~/shell/resolveHostUrl.js"; import { configureTestkit } from "~/shell/testkit.js"; import { executeWorkerFlow } from "~/domains/runner/executeWorkerFlow.js"; import { runAndroidFlow as defaultRunAndroidFlow } from "~/domains/runner/runAndroidFlow.js"; @@ -34,9 +35,7 @@ async function runWorker(signals: SignalRegistry): Promise { await configureTestkit(input.resolvedDir); const fs = makeDefaultFs(); - const apiBaseUrl = - process.env["QAWOLF_API_URL"]?.replace(/\/+$/, "") || - "https://app.qawolf.com"; + const apiBaseUrl = resolveHostUrl(process.env); const flowRuntimeDeps = await createFlowRuntimeDeps({ envDir: input.resolvedDir, ctx: { apiBaseUrl, configDir: getConfigDir(), fs }, diff --git a/src/core/messages/auth.ts b/src/core/messages/auth.ts index 7e3ffb374..9c93f68d8 100644 --- a/src/core/messages/auth.ts +++ b/src/core/messages/auth.ts @@ -47,7 +47,7 @@ export const authMessages = { failedWithStatus: (status: number, noun: string | undefined) => `QA Wolf API${noun ? ` ${noun}` : ""} request failed (HTTP ${status}).`, networkUnreachable: (baseUrl: string, noun: string | undefined) => - `Could not reach the QA Wolf API at ${baseUrl}${noun ? ` to fetch ${noun}` : ""}. Check your network connection and QAWOLF_API_URL.`, + `Could not reach the QA Wolf API at ${baseUrl}${noun ? ` to fetch ${noun}` : ""}. Check your network connection and QAWOLF_HOST_URL.`, unexpectedResponse: (noun: string | undefined) => `Unexpected${noun ? ` ${noun}` : ""} response from the QA Wolf API.`, }, diff --git a/src/shell/resolveHostUrl.test.ts b/src/shell/resolveHostUrl.test.ts new file mode 100644 index 000000000..66e8d43a9 --- /dev/null +++ b/src/shell/resolveHostUrl.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, it } from "bun:test"; + +import { resolveHostUrl } from "./resolveHostUrl.js"; + +describe("resolveHostUrl", () => { + it("returns the production host when QAWOLF_HOST_URL is unset", () => { + expect(resolveHostUrl({})).toBe("https://app.qawolf.com"); + }); + + it("accepts a custom host", () => { + expect( + resolveHostUrl({ + QAWOLF_HOST_URL: "https://app.preview.example.com", + }), + ).toBe("https://app.preview.example.com"); + }); + + it("does not treat QAWOLF_API_URL as the host URL", () => { + expect( + resolveHostUrl({ + QAWOLF_API_URL: "https://api.staging.example.com/api", + }), + ).toBe("https://app.qawolf.com"); + }); + + it("trims whitespace and trailing slashes", () => { + expect( + resolveHostUrl({ + QAWOLF_HOST_URL: " http://localhost:3000/// ", + }), + ).toBe("http://localhost:3000"); + }); + + it("falls back to the production host for whitespace", () => { + expect(resolveHostUrl({ QAWOLF_HOST_URL: " " })).toBe( + "https://app.qawolf.com", + ); + }); +}); diff --git a/src/shell/resolveHostUrl.ts b/src/shell/resolveHostUrl.ts new file mode 100644 index 000000000..f6a2d8470 --- /dev/null +++ b/src/shell/resolveHostUrl.ts @@ -0,0 +1,10 @@ +const defaultHostUrl = "https://app.qawolf.com"; + +export function resolveHostUrl( + env: Record, +): string { + const configuredHostUrl = env["QAWOLF_HOST_URL"]?.trim(); + if (!configuredHostUrl) return defaultHostUrl; + + return configuredHostUrl.replace(/\/+$/, ""); +}