Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
5 changes: 5 additions & 0 deletions skills/qawolf-cli/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`).
Expand Down
4 changes: 2 additions & 2 deletions src/commands/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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, {
Expand Down
5 changes: 2 additions & 3 deletions src/commands/flows/runWorker.register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -34,9 +35,7 @@ async function runWorker(signals: SignalRegistry): Promise<void> {

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 },
Expand Down
2 changes: 1 addition & 1 deletion src/core/messages/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.`,
},
Expand Down
39 changes: 39 additions & 0 deletions src/shell/resolveHostUrl.test.ts
Original file line number Diff line number Diff line change
@@ -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",
);
});
});
10 changes: 10 additions & 0 deletions src/shell/resolveHostUrl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const defaultHostUrl = "https://app.qawolf.com";

export function resolveHostUrl(
env: Record<string, string | undefined>,
): string {
const configuredHostUrl = env["QAWOLF_HOST_URL"]?.trim();
if (!configuredHostUrl) return defaultHostUrl;

return configuredHostUrl.replace(/\/+$/, "");
}
Loading