Skip to content
Merged
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
7 changes: 7 additions & 0 deletions .changeset/runner-run-default-env.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@qawolf/cli": patch
---

`qawolf runner run` now falls back to `QAWOLF_ENVIRONMENT` when neither `--env-id` nor `--env-file` is passed, so the one export that already sets a default for `qawolf flows` covers a runner run too. `--env-id` still wins over it, and `--env-file` suppresses it so a run reading a dotenv file is not handed a second environment on top.

The run reports on stderr which environment it picked up. A run that was given none before is now given one, and those variables reach the flow's code, so it should never happen silently.
4 changes: 3 additions & 1 deletion skills/qawolf-cli/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ is set; otherwise select an id or alias from
`qawolf --json environment find` and reuse it. Shell invocations may not share
exported variables, so pass the selected environment explicitly instead of
rediscovering it. Check the command's help for its environment flag: variable
commands use `--environment-id`; `qawolf flows run` uses `--env`.
commands use `--environment-id`; `qawolf flows run` uses `--env`; and
`qawolf runner run` uses `--env-id`, which falls back to `QAWOLF_ENVIRONMENT`
when neither it nor `--env-file` is passed.

If multiple environments are returned and the task context does not identify
the target, ask instead of guessing. Do not default to the newest environment.
Expand Down
12 changes: 12 additions & 0 deletions skills/qawolf-cli/references/runner.md
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,18 @@ qawolf runner run flows/checkout.flow.ts --env-id staging
qawolf runner run flows/checkout.flow.ts --env-file .env
```

**A run with neither flag falls back to `QAWOLF_ENVIRONMENT`**, the same
variable `qawolf flows` reads, so one export covers both. The run says on
stderr which environment it picked up, because those variables reach your flow's
code and a run should never be given an environment silently. `--env-id` wins
over it, and `--env-file` suppresses it, so a run reading a dotenv file is not
handed a second environment on top.

```sh
export QAWOLF_ENVIRONMENT=staging
qawolf runner run flows/checkout.flow.ts # runs against staging
```

**Prefer `--env-id`.** QA Wolf reads and decrypts the environment itself, so the
values never leave the server, nothing has to be pulled to disk first, and no
size limit applies to them. It is the only way to run a flow whose environment
Expand Down
6 changes: 3 additions & 3 deletions src/commands/__snapshots__/help.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -347,9 +347,9 @@ Options:
submission: the recorder is runner-wide, not run-scoped.
Implies --follow (default: false)
--env-id <env> QA Wolf environment whose variables the run is given, by
id or alias. QA Wolf reads and decrypts them itself, so
they never leave the server and no size limit applies to
them
id or alias. Defaults to QAWOLF_ENVIRONMENT. QA Wolf
reads and decrypts them itself, so they never leave the
server and no size limit applies to them
--env-file <path> Dotenv file on this machine whose variables the run is
given. Pass this or --env-id, not both
--lines <start-end> Run only these 1-indexed lines against the browser as it
Expand Down
4 changes: 3 additions & 1 deletion src/commands/qawolfCliSkill.template.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ is set; otherwise select an id or alias from
`qawolf --json environment find` and reuse it. Shell invocations may not share
exported variables, so pass the selected environment explicitly instead of
rediscovering it. Check the command's help for its environment flag: variable
commands use `--environment-id`; `qawolf flows run` uses `--env`.
commands use `--environment-id`; `qawolf flows run` uses `--env`; and
`qawolf runner run` uses `--env-id`, which falls back to `QAWOLF_ENVIRONMENT`
when neither it nor `--env-file` is passed.

If multiple environments are returned and the task context does not identify
the target, ask instead of guessing. Do not default to the newest environment.
Expand Down
2 changes: 1 addition & 1 deletion src/commands/runner/run.register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export function registerRunCommand(
)
.option(
"--env-id <env>",
"QA Wolf environment whose variables the run is given, by id or alias. QA Wolf reads and decrypts them itself, so they never leave the server and no size limit applies to them",
"QA Wolf environment whose variables the run is given, by id or alias. Defaults to QAWOLF_ENVIRONMENT. QA Wolf reads and decrypts them itself, so they never leave the server and no size limit applies to them",
)
.option(
"--env-file <path>",
Expand Down
5 changes: 5 additions & 0 deletions src/domains/interactiveRunner/deps.testUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export const testCwd = "/workspace";
export function makeAuthCtx(mode: OutputMode = "human"): {
callPublicApi: ReturnType<typeof makeCallPublicApiMock>;
ctx: AuthCommandContext;
infos: () => string[];
outputs: () => { data: unknown; humanMessage: string }[];
streamed: () => string[];
streamedData: () => unknown[];
Expand All @@ -39,6 +40,10 @@ export function makeAuthCtx(mode: OutputMode = "human"): {
apiKeySource: "env",
platformClient: makeMockPlatformClient({ callPublicApi }),
},
infos: () =>
(base.ui.info as Mock<(message: string) => void>).mock.calls.map(
([message]) => message,
),
outputs: () =>
(
base.ui.output as Mock<(data: unknown, humanMessage: string) => void>
Expand Down
11 changes: 8 additions & 3 deletions src/domains/interactiveRunner/prepareRun.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,17 +49,22 @@ export async function prepareRun(
},
deps: InteractiveRunnerDeps,
): Promise<PreparedRun> {
const environmentId = options.envId?.trim();
if (environmentId !== undefined && options.envFile !== undefined) {
const explicitEnvId = options.envId?.trim();
if (explicitEnvId !== undefined && options.envFile !== undefined) {
return refused(
interactiveRunnerMessages.envIdWithEnvFile,
exitCodes.invalidArgs,
);
}
if (environmentId === "") {
if (explicitEnvId === "") {
return refused(interactiveRunnerMessages.envIdBlank, exitCodes.invalidArgs);
}

const fromEnvVar = deps.env["QAWOLF_ENVIRONMENT"]?.trim();
const environmentId =
explicitEnvId ??
(options.envFile === undefined && fromEnvVar ? fromEnvVar : undefined);

const linesFilePath =
options.linesFile === undefined
? undefined
Expand Down
57 changes: 53 additions & 4 deletions src/domains/interactiveRunner/runFlow.environment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import { handleRunnerRun } from "./runFlow.js";
const submitted = { outcome: "success" as const, runId: "run-a" };

describe("handleRunnerRun with --env-file and --env-id", () => {
const envFileDeps = (content: string) =>
const envFileDeps = (content: string, env: Record<string, string> = {}) =>
makeTestDeps({
env,
collectRunFiles: async () => ({
files: {
"flow.ts": "export default {};",
Expand All @@ -24,14 +25,16 @@ describe("handleRunnerRun with --env-file and --env-id", () => {

const run = async ({
content = 'A="1"\n',
env,
envFile,
envId,
}: {
content?: string;
env?: Record<string, string>;
envFile?: string;
envId?: string;
}) => {
const { callPublicApi, ctx } = makeAuthCtx();
const { callPublicApi, ctx, infos } = makeAuthCtx();
callPublicApi.mockResolvedValue({ ok: true, value: submitted });
const result = await handleRunnerRun(
ctx,
Expand All @@ -48,9 +51,9 @@ describe("handleRunnerRun with --env-file and --env-id", () => {
runner: "ci",
timeout: undefined,
},
envFileDeps(content),
envFileDeps(content, env),
);
return { callPublicApi, result };
return { callPublicApi, infos, result };
};

const sentRequest = (callPublicApi: { mock: { calls: unknown[][] } }) =>
Expand Down Expand Up @@ -106,6 +109,52 @@ describe("handleRunnerRun with --env-file and --env-id", () => {
});
});

it("falls back to QAWOLF_ENVIRONMENT when neither flag is passed", async () => {
const { callPublicApi, infos, result } = await run({
env: { QAWOLF_ENVIRONMENT: "staging" },
});

expect(result).toBeUndefined();
expect(sentRequest(callPublicApi)).toMatchObject({
environmentId: "staging",
});
// Those variables reach the flow's code, so the run says where they came from.
expect(infos().join("\n")).toContain("QAWOLF_ENVIRONMENT");
});

it("prefers an explicit --env-id over QAWOLF_ENVIRONMENT", async () => {
const { callPublicApi, infos } = await run({
env: { QAWOLF_ENVIRONMENT: "staging" },
envId: "production",
});

expect(sentRequest(callPublicApi)).toMatchObject({
environmentId: "production",
});
expect(infos().join("\n")).not.toContain("QAWOLF_ENVIRONMENT");
});

// The file is the whole environment the caller asked for, so it is not given
// a second one on top.
it("ignores QAWOLF_ENVIRONMENT when --env-file is passed", async () => {
const { callPublicApi } = await run({
env: { QAWOLF_ENVIRONMENT: "staging" },
envFile: ".env",
});
const request = sentRequest(callPublicApi);

expect(request).toMatchObject({ env: { A: "1" } });
expect(Object.hasOwn(request, "environmentId")).toBe(false);
});

it("treats a blank QAWOLF_ENVIRONMENT as unset", async () => {
const { callPublicApi } = await run({ env: { QAWOLF_ENVIRONMENT: " " } });

expect(Object.hasOwn(sentRequest(callPublicApi), "environmentId")).toBe(
false,
);
});

it("refuses an --env-id given nothing", async () => {
const { callPublicApi, result } = await run({ envId: " " });

Expand Down
9 changes: 8 additions & 1 deletion src/domains/interactiveRunner/runFlow.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { parseFollowTimeout } from "~/core/interactiveRunner/followTimeout.js";
import { toCollectedPath } from "~/core/interactiveRunner/runFiles.js";
import { interactiveRunnerMessages } from "~/core/messages/index.js";
import {
environmentsMessages,
interactiveRunnerMessages,
} from "~/core/messages/index.js";
import type {
AuthCommandContext,
CommandResult,
Expand Down Expand Up @@ -55,6 +58,10 @@ export async function handleRunnerRun(
return { error: prepared.error, exitCode: prepared.exitCode };
}

if (options.envId === undefined && prepared.environmentId !== undefined) {
ctx.ui.info(environmentsMessages.usingFromEnvVar(prepared.environmentId));
}

const resolved = await resolveRunner(
ctx,
{ autoLaunch: true, runner: options.runner },
Expand Down
Loading