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: 4 additions & 3 deletions packages/core/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,9 +224,10 @@ export class StepFailed extends Schema.TaggedError<StepFailed>()("StepFailed", {
}) {}

/**
* The run could not do its job for a CAPACITY reason and is bowing out — the
* work was never attempted-and-failed, it was impossible to attempt (e.g.
* pr-review's diff exceeds the model's context window even after truncation).
* The run could not do its job and is bowing out — the work was never
* attempted-and-failed, it was impossible to attempt: a CAPACITY reason (e.g.
* pr-review's diff exceeds the model's context window even after truncation)
* or a repo that never opted in (offload-test with no configured command).
* The dispatcher concludes the check-run `neutral` with `reason` in the
* summary instead of `failure`: a review that didn't happen is not a failed
* review, and a red that isn't actionable trains people to ignore the check.
Expand Down
38 changes: 35 additions & 3 deletions runs/offload-test.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -517,31 +517,63 @@ describe("offload-test", () => {
);

it.effect(
"command resolution — no `command` and no CONFIG_KV fails fast with StepFailed before checkout",
"command resolution — a webhook dispatch with no CONFIG_KV command skips with RunSkipped (neutral) before checkout",
() => {
const { layer, handles } = makeCFRuntimeTest({
sandboxProgram: { "pnpm test": { exitCode: 0 } },
// no `config` seed — neither command key resolves.
});
// Webhook-shaped: the trigger never passes `command`.
const input = {
repo: "owner/name",
sha: "abc123",
secrets: [] as readonly string[],
install: false,
failOnNonZeroExit: false,
failOnNonZeroExit: true,
};

return Effect.gen(function* () {
const exit = yield* Effect.exit(offloadTest.run(input));
expect(Exit.isFailure(exit)).toBe(true);
const failure = Exit.isFailure(exit)
? Option.getOrUndefined(Cause.failureOption(exit.cause))
: undefined;
expect((failure as { _tag?: string })?._tag).toBe("RunSkipped");
// The neutral check names the key that opts the repo in.
expect((failure as { reason?: string })?.reason).toContain(
"offload-test.command:owner/name",
);
// Skipped before any work: never cloned, never exec'd.
expect(handles.sandbox.clones).toHaveLength(0);
expect(handles.sandbox.execs).toHaveLength(0);
}).pipe(Effect.provide(layer));
},
);

it.effect(
"command resolution — an Action dispatch passing an empty `command` fails fast with StepFailed",
() => {
const { layer, handles } = makeCFRuntimeTest({
sandboxProgram: { "pnpm test": { exitCode: 0 } },
});
const input = {
repo: "owner/name",
sha: "abc123",
command: " ",
secrets: [] as readonly string[],
install: false,
failOnNonZeroExit: false,
};

return Effect.gen(function* () {
const exit = yield* Effect.exit(offloadTest.run(input));
const tag = Exit.isFailure(exit)
? Option.match(Cause.failureOption(exit.cause), {
onSome: (f) => (f as { _tag?: string })._tag,
onNone: () => undefined,
})
: undefined;
expect(tag).toBe("StepFailed");
// Fail-fast: never cloned, never exec'd.
expect(handles.sandbox.clones).toHaveLength(0);
expect(handles.sandbox.execs).toHaveLength(0);
}).pipe(Effect.provide(layer));
Expand Down
21 changes: 19 additions & 2 deletions runs/offload-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ import {
config,
defineRun,
io,
RunSkipped,
sandbox,
spawnChildRun,
StepFailed,
Expand Down Expand Up @@ -463,8 +464,9 @@ export const offloadTest = defineRun({
// `offload-test.command`). This step runs ONLY when the dispatch carried
// no command, so Action-mode dispatches keep the exact
// `checkout → exec → upload-log` step shape (the `??` short-circuits before
// the `yield*`). A command missing everywhere fails fast — running an empty
// command would post a meaningless green check.
// the `yield*`). A command missing everywhere never runs — an empty
// command would post a meaningless green check; the run skips instead
// (neutral check, see below).
// `install` / `timeoutSec` are resolved INSIDE this same step, not a
// second one. They are a webhook-mode concern, and webhook mode is
// exactly the case that omits `command` — so folding them in here means
Expand Down Expand Up @@ -589,6 +591,21 @@ export const offloadTest = defineRun({
// resolved command (the step above fails otherwise), and the unlabelled
// command may legitimately be absent.
if (stages === undefined && (command === undefined || command.trim().length === 0)) {
// A webhook dispatch reaches every repo the App is installed on, and a
// repo that never set a command has not opted in — its tests may run
// somewhere else entirely. That is a skip (neutral check naming the key
// to set), not a failure: a red that no PR author can act on trains
// people to ignore the check. An Action dispatch chose this run and
// passed an empty command, which is a broken caller, so it stays red.
if (input.command === undefined) {
return yield* Effect.fail(
new RunSkipped({
reason:
`no offload-test command is configured for ${input.repo} — set CONFIG_KV ` +
`\`${repoCommandKey(input.repo)}\` or \`${COMMAND_KEY}\` to run it`,
}),
);
}
return yield* Effect.fail(
new StepFailed({
step: "resolve-command",
Expand Down
Loading