Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
a4c96f4
refactor(vis): split publish context into phases
prisis Oct 16, 2026
9b9aa57
refactor(vis): add run selection phases
prisis Oct 17, 2026
38d655a
refactor(vis): add run graph and service phases
prisis Oct 17, 2026
147c20d
refactor(vis): add run runtime and mode phases
prisis Oct 17, 2026
a75bfc6
refactor(vis): make run execute an orchestrator
prisis Oct 17, 2026
2bc295b
refactor(vis): read currentOs once in run modes
prisis Oct 17, 2026
8d7ad8a
test(vis): read moved retry branch in spawn tests
prisis Oct 17, 2026
a41cbbd
fix(vis): tear down services when run setup throws
prisis Oct 17, 2026
a7df063
refactor(vis): drop run handler re-export shim
prisis Oct 17, 2026
bf2226f
fix(vis): report publish preflight errors as failures
prisis Oct 17, 2026
35da97a
refactor(vis): unexport unused publish wave types
prisis Oct 17, 2026
c421e61
refactor(vis): plan publish tags once for preflight and tagging
prisis Oct 17, 2026
6f76c94
refactor(vis): dedupe liveness, quoting and visOptions helpers
prisis Oct 17, 2026
3084260
refactor(vis): break config type cycle for scoped tasks
prisis Oct 17, 2026
7e9b6e6
refactor(vis): type orchestrator discovered projects
prisis Oct 17, 2026
df854e7
refactor(vis): share one nested field setter
prisis Oct 17, 2026
b1c538e
refactor(vis): merge run modes, prefix run parts
prisis Oct 17, 2026
b1115db
refactor(vis): move catalog code to engine
prisis Oct 17, 2026
c2da351
refactor(vis): split cache maintenance and why
prisis Oct 17, 2026
db64e87
refactor(vis): keep customTypes doc with its type
prisis Oct 17, 2026
5148a36
fix(vis): arm run cleanup before graph finalize
prisis Oct 17, 2026
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
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";

import { createRetryBudget } from "../../../src/commands/run/handler";
import { createRetryBudget } from "../../../src/commands/run/handler-parts/run-executor";

describe(createRetryBudget, () => {
it("grants up to the requested amount and decrements remaining", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,9 @@ describe(resolveRetrySpawn, () => {
it("builds the retry spawn with resolveRetrySpawn in the handler's retry branch", () => {
expect.assertions(4);

const handlerSource = readFileSync(fileURLToPath(new URL("../../../src/commands/run/handler.ts", import.meta.url)), "utf8");
const handlerSource = ["handler.ts", "handler-parts/run-modes.ts"]
.map((file) => readFileSync(fileURLToPath(new URL(`../../../src/commands/run/${file}`, import.meta.url)), "utf8"))
.join("\n");
const start = handlerSource.indexOf("loopAction === \"retry\" && retryTaskId");
const end = handlerSource.indexOf("retryTaskId = null;", start);

Expand All @@ -158,7 +160,9 @@ describe(resolveRetrySpawn, () => {
it("retries the resolved task command, not the raw override", () => {
expect.assertions(3);

const handlerSource = readFileSync(fileURLToPath(new URL("../../../src/commands/run/handler.ts", import.meta.url)), "utf8");
const handlerSource = ["handler.ts", "handler-parts/run-modes.ts"]
.map((file) => readFileSync(fileURLToPath(new URL(`../../../src/commands/run/${file}`, import.meta.url)), "utf8"))
.join("\n");
const start = handlerSource.indexOf("loopAction === \"retry\" && retryTaskId");
const retryBranch = handlerSource.slice(start, handlerSource.indexOf("retryTaskId = null;", start));

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { beforeEach, describe, expect, it, vi } from "vitest";

import runExecute from "../../../src/commands/run/handler";

const state = vi.hoisted(() => {
return {
cleanupEphemeralServices: vi.fn(),
ctx: undefined as Record<string, unknown> | undefined,
prepareRunRuntime: vi.fn(),
};
});

// The phases before the runtime are stubbed; `injectMissingServices` plays the
// part of booting an ephemeral service by recording its pid file and run dir.
vi.mock(import("../../../src/commands/run/handler-parts/run-select"), () => {
return {
buildInitialTasks: () => false,
loadRunWorkspace: async () => state.ctx,
selectProjectsWithTarget: () => false,
selectRunProjects: async () => false,
};
});

vi.mock(import("../../../src/commands/run/handler-parts/run-graph"), () => {
return {
buildRunGraph: () => {},
printDryRunPlan: () => false,
runPreflightChecks: async () => {},
shapeTaskGraph: () => {},
};
});

vi.mock(import("../../../src/commands/run/handler-parts/run-services"), () => {
return {
attachRegisteredServices: async () => { return { satisfiedServices: [], serviceEnvByTaskId: new Map() }; },
finalizeServiceGraph: () => {},
injectMissingServices: () => {
state.ctx!["ephemeralPidFiles"] = ["/ws/.vis/run/db.pid"];
state.ctx!["serviceRunDir"] = "/ws/.vis/run";
},
};
});

vi.mock(import("../../../src/commands/run/handler-parts/run-runtime"), async (importOriginal) => {
const actual = await importOriginal();

return { ...actual, prepareRunRuntime: state.prepareRunRuntime };
});

vi.mock(import("../../../src/commands/run/handler-parts/run-persistent-tasks"), async (importOriginal) => {
const actual = await importOriginal();

return { ...actual, cleanupEphemeralServices: state.cleanupEphemeralServices };
});

describe("vis run teardown", () => {
beforeEach(() => {
state.ctx = {
ephemeralPidFiles: [],
options: { stopServices: false },
registryStartedIds: [],
registryStartedPids: new Map(),
serviceEventBridge: null,
serviceRunDir: undefined,
workspaceRoot: "/ws",
};
state.cleanupEphemeralServices.mockReset();
state.prepareRunRuntime.mockReset();
});

it("tears down services it booted when a run:before hook throws", async () => {
expect.assertions(2);

// A throwing `run:before` hook surfaces from inside prepareRunRuntime.
state.prepareRunRuntime.mockRejectedValue(new Error("run:before boom"));

const logger = { debug: () => {}, error: () => {}, info: () => {}, warn: () => {} };

await expect(
runExecute({
argument: ["test"],
logger,
options: {},
visConfig: undefined,
workspaceRoot: "/ws",
} as never),
).rejects.toThrow("run:before boom");

expect(state.cleanupEphemeralServices).toHaveBeenCalledWith(
expect.objectContaining({ pidFiles: ["/ws/.vis/run/db.pid"], runDir: "/ws/.vis/run" }),
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { execFileSync } from "node:child_process";
import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
import { rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";

import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

import { buildContext, publishContext } from "../../../src/release/core/orchestrator";
import type { PublishResult } from "../../../src/release/core/package-managers/interface";
import type { PublishContext } from "../../../src/release/core/version-actions/interface";
import { VersionActions } from "../../../src/release/core/version-actions/interface";

const remote = vi.hoisted(() => {
return {
detectRemoteProvider: vi.fn(),
detectRepoSlug: vi.fn(),
};
});

vi.mock(import("../../../src/release/core/remote/detect"), async (importOriginal) => {
const actual = await importOriginal();

return {
...actual,
createRemoteClient: () => {
return { detectRepoSlug: remote.detectRepoSlug };
},
detectRemoteProvider: remote.detectRemoteProvider,
};
});

// Only the network-facing push check is stubbed; the remote is still a real
// git remote so `git remote get-url --push origin` runs for real.
vi.mock(import("../../../src/release/core/git"), async (importOriginal) => {
const actual = await importOriginal();

return {
...actual,
getCurrentBranch: async () => "main",
resolvePushCredentials: async () => undefined,
verifyPushAccess: async () => undefined,
};
});

class StubVersionActions extends VersionActions {
public readonly id = "stub";

public readonly calls: string[] = [];

public async readPublishedVersion(): Promise<string | undefined> {
return undefined;
}

public async publish(context: PublishContext): Promise<PublishResult> {
this.calls.push(context.pkg.name);

return { alreadyPublished: false, output: "ok", published: true };
}
}

const writeJson = (path: string, value: unknown): void => {
writeFileSync(path, `${JSON.stringify(value, null, 4)}\n`);
};

const setupFixture = (): string => {
const cwd = mkdtempSync(join(tmpdir(), "vis-orch-preflight-"));

writeJson(join(cwd, "package.json"), { name: "fixture-root", packageManager: "pnpm@10.0.0", private: true, version: "0.0.0", workspaces: ["packages/*"] });
writeFileSync(join(cwd, "pnpm-workspace.yaml"), "packages:\n - 'packages/*'\n");

mkdirSync(join(cwd, "packages", "a"), { recursive: true });
writeJson(join(cwd, "packages", "a", "package.json"), { name: "@scope/a", version: "1.0.0" });

mkdirSync(join(cwd, ".vis", "release"), { recursive: true });
writeFileSync(join(cwd, ".vis", "release", "feat.md"), `---\n"@scope/a": minor\n---\nbody\n`);
writeFileSync(
join(cwd, "vis.config.cjs"),
`module.exports = ${JSON.stringify({ release: { acknowledgeUnstable: true, defaultManaged: true } }, null, 4)};\n`,
);

execFileSync("git", ["init", "-q"], { cwd });
execFileSync("git", ["remote", "add", "origin", "https://example.invalid/acme/repo.git"], { cwd });

return cwd;
};

describe("orchestrator: publish preflight reports failures instead of rejecting", () => {
let cwd: string;

beforeEach(() => {
cwd = setupFixture();
remote.detectRemoteProvider.mockReset();
remote.detectRepoSlug.mockReset();
});

afterEach(async () => {
await rm(cwd, { force: true, recursive: true });
});

it("records a _preflight failure when the release-PR check cannot reach the remote", async () => {
expect.hasAssertions();

const actions = new StubVersionActions();
const ctx = await buildContext({
config: { acknowledgeUnstable: true, defaultManaged: true },
cwd,
firstRelease: true,
});

// Only the version-pr channel runs the open-release-PR check.
Object.assign(ctx, { channel: { mode: "version-pr" } });
remote.detectRemoteProvider.mockRejectedValue(new Error("remote unreachable"));

const result = await publishContext(ctx, { noPush: true, noTag: true, publishActionsOverride: actions });

expect(actions.calls).toStrictEqual([]);
expect(result.published).toStrictEqual([]);
expect(result.failed).toStrictEqual([expect.objectContaining({ name: "_preflight", reason: expect.stringContaining("remote unreachable") })]);
});

it("records a _preflight failure when the push-rule check cannot resolve the repo", async () => {
expect.hasAssertions();

const actions = new StubVersionActions();
const ctx = await buildContext({
config: { acknowledgeUnstable: true, defaultManaged: true },
cwd,
firstRelease: true,
});

remote.detectRemoteProvider.mockResolvedValue("github");
remote.detectRepoSlug.mockRejectedValue(new Error("slug lookup failed"));

const result = await publishContext(ctx, { noTag: true, publishActionsOverride: actions });

expect(actions.calls).toStrictEqual([]);
expect(result.published).toStrictEqual([]);
expect(result.failed).toStrictEqual([expect.objectContaining({ name: "_preflight", reason: expect.stringContaining("slug lookup failed") })]);
});
});
2 changes: 1 addition & 1 deletion packages/tooling/vis/schemas/vis-config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -6422,7 +6422,7 @@
}
},
"additionalProperties": false,
"description": "A predicate used by {@link VisConfig.scopedTasks } . All listed constraints must match for the block to apply."
"description": "A predicate used by the `scopedTasks` config block. All listed constraints must match for the block to apply."
},
"TaskArgument": {
"type": "object",
Expand Down
Loading
Loading