mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-21 16:02:36 +00:00
* fix: keep an app's deployed policy on wmill push Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X47gNFqf1SsA67zTm71f8c * fix: keep a first push's file-stated app policy Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X47gNFqf1SsA67zTm71f8c * fix: deploy a file-stated viewer app as viewer, not publisher Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X47gNFqf1SsA67zTm71f8c * fix: never put a repo-stated run identity on the wire Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X47gNFqf1SsA67zTm71f8c * fix: send the deployed run identity only when the push may claim it Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X47gNFqf1SsA67zTm71f8c * fix: let a file-stated execution mode win in both directions Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X47gNFqf1SsA67zTm71f8c * refactor: drop the app-file execution mode helper with no caller Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X47gNFqf1SsA67zTm71f8c * feat: warn before a raw-app push takes over the run-as user Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X47gNFqf1SsA67zTm71f8c * fix: keep an app's run-as user when a push only deletes one of its files Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X47gNFqf1SsA67zTm71f8c * refactor: settle an app's ownership check before any content parsing Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X47gNFqf1SsA67zTm71f8c * fix: share one rule for which raw-app files a push sends Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X47gNFqf1SsA67zTm71f8c * fix: stop tracking raw-app files no push ever sends Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X47gNFqf1SsA67zTm71f8c * test: bundle for real instead of stubbing the module for every suite Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X47gNFqf1SsA67zTm71f8c * docs: record why a module mock cannot be undone by afterAll Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X47gNFqf1SsA67zTm71f8c * fix: keep tracking a runnable whose file shares a bundle-excluded name Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X47gNFqf1SsA67zTm71f8c * test: pin both halves of the backend runnable rule Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X47gNFqf1SsA67zTm71f8c --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
148 lines
5.8 KiB
TypeScript
148 lines
5.8 KiB
TypeScript
/**
|
|
* The pre-check is what stops a push from silently reassigning an item's run-as
|
|
* user. Raw apps were missing from it, so the one kind whose file records no
|
|
* policy at all was also the one that changed owner without a word.
|
|
*/
|
|
|
|
import { expect, test } from "bun:test";
|
|
import { preCheckPermissionedAs } from "../src/core/permissioned_as.ts";
|
|
|
|
/** Non-interactive and without the override flag, the pre-check exits rather
|
|
* than reassigning silently — so a thrown exit is the signal it fired. */
|
|
type Shape = "edited" | "added" | "deleted";
|
|
|
|
function change(path: string, name: Shape = "edited") {
|
|
return { name, path, before: "summary: x\n", content: "summary: x\n" };
|
|
}
|
|
|
|
async function precheck(
|
|
changes: ReturnType<typeof change>[],
|
|
): Promise<string | undefined> {
|
|
const exit = process.exit;
|
|
let code: number | undefined;
|
|
(process as any).exit = (c?: number) => {
|
|
code = c;
|
|
throw new Error(`exit:${c}`);
|
|
};
|
|
const logged: string[] = [];
|
|
const err = console.error;
|
|
console.error = (...a: unknown[]) => void logged.push(a.join(" "));
|
|
try {
|
|
await preCheckPermissionedAs(changes, "pusher@corp", false, false, false);
|
|
} catch (e) {
|
|
if (!String(e).startsWith("Error: exit:")) throw e;
|
|
} finally {
|
|
(process as any).exit = exit;
|
|
console.error = err;
|
|
}
|
|
return code === undefined ? undefined : logged.join("\n");
|
|
}
|
|
|
|
test("a raw-app push warns the non-deployer it will take over the run-as user", async () => {
|
|
const message = await precheck([change("f/test/myapp.raw_app/index.tsx")]);
|
|
|
|
expect(message).toBeDefined();
|
|
expect(message).toContain("f/test/myapp.raw_app");
|
|
expect(message).toContain("pusher@corp");
|
|
});
|
|
|
|
// Deleting one file re-pushes the whole app rather than deleting it, so the
|
|
// takeover happens there too.
|
|
test("deleting one of an app's files warns like editing one", async () => {
|
|
const message = await precheck([
|
|
change("f/test/myapp.raw_app/gone.tsx", "deleted"),
|
|
]);
|
|
|
|
expect(message).toContain("f/test/myapp.raw_app");
|
|
});
|
|
|
|
// The metadata file going with it means the app itself is created or removed —
|
|
// neither takes an owner over.
|
|
test("an app arriving or leaving whole is not a takeover", async () => {
|
|
const created = await precheck([
|
|
change("f/test/new.raw_app/raw_app.yaml", "added"),
|
|
change("f/test/new.raw_app/index.tsx", "added"),
|
|
]);
|
|
const removed = await precheck([
|
|
change("f/test/old.raw_app/raw_app.yaml", "deleted"),
|
|
change("f/test/old.raw_app/index.tsx", "deleted"),
|
|
]);
|
|
|
|
expect(created).toBeUndefined();
|
|
expect(removed).toBeUndefined();
|
|
});
|
|
|
|
// An app carries no owner in its files, so nothing about it depends on their
|
|
// content — an empty one redeploys it exactly like any other.
|
|
test("an empty file still counts as a change to the app", async () => {
|
|
const added = await precheck([
|
|
{ name: "added", path: "f/test/myapp.raw_app/blank.ts", content: "" },
|
|
]);
|
|
const edited = await precheck([
|
|
{ name: "edited", path: "f/test/myapp.raw_app/blank.ts", before: "" },
|
|
]);
|
|
|
|
expect(added).toContain("f/test/myapp.raw_app");
|
|
expect(edited).toContain("f/test/myapp.raw_app");
|
|
});
|
|
|
|
// `extractFolderPath` normalizes separators but the metadata predicates match a
|
|
// literal `/`, so a Windows path must not take a different branch.
|
|
test("a Windows path classifies the same as its posix twin", async () => {
|
|
const created = await precheck([
|
|
change("f\\test\\new.raw_app\\raw_app.yaml", "added"),
|
|
change("f\\test\\new.raw_app\\index.tsx", "added"),
|
|
]);
|
|
const edited = await precheck([
|
|
change("f\\test\\myapp.raw_app\\index.tsx"),
|
|
]);
|
|
|
|
expect(created).toBeUndefined();
|
|
expect(edited).toContain("f/test/myapp.raw_app");
|
|
});
|
|
|
|
// `collectAppFiles` never sends these, and the sync diff never stops listing
|
|
// them (nothing uploads them, so they stay "added" forever) — so warning on one
|
|
// would gate every push of a scaffolded app on the override flag.
|
|
test("a file the push never sends is not a change to the app", async () => {
|
|
const artifacts = await precheck([
|
|
change("f/test/myapp.raw_app/AGENTS.md", "added"),
|
|
change("f/test/myapp.raw_app/sql_to_apply/a.sql", "added"),
|
|
change("f/test/myapp.raw_app/node_modules/dep/index.js", "added"),
|
|
change("f/test/myapp.raw_app/recordings/r.json", "added"),
|
|
change("f/test/myapp.raw_app/package-lock.json"),
|
|
change("f/test/myapp.raw_app/wmill.d.ts"),
|
|
// Only the backend folder's *top level* is a runnable; nothing reads deeper,
|
|
// so the depth limit is what keeps a `backend/node_modules/` from becoming
|
|
// the perpetual diff this predicate exists to remove.
|
|
change("f/test/myapp.raw_app/backend/node_modules/dep/index.js", "added"),
|
|
]);
|
|
// The three channels a push does send through: bundled file, metadata, runnable.
|
|
const sent = await precheck([change("f/test/myapp.raw_app/index.tsx")]);
|
|
const meta = await precheck([change("f/test/myapp.raw_app/raw_app.yaml")]);
|
|
const runnable = await precheck([change("f/test/myapp.raw_app/backend/a.ts")]);
|
|
// The runnable channel is not the bundle: the bundle's name exclusions don't
|
|
// reach into it, so a runnable file sharing one of those names still deploys.
|
|
const namesake = await precheck([
|
|
change("f/test/myapp.raw_app/backend/wmill.d.ts"),
|
|
]);
|
|
|
|
expect(artifacts).toBeUndefined();
|
|
expect(sent).toContain("f/test/myapp.raw_app");
|
|
expect(meta).toContain("f/test/myapp.raw_app");
|
|
expect(runnable).toContain("f/test/myapp.raw_app");
|
|
expect(namesake).toContain("f/test/myapp.raw_app");
|
|
});
|
|
|
|
test("an app is listed once however many of its files changed", async () => {
|
|
const message = await precheck([
|
|
change("f/test/myapp.raw_app/index.tsx"),
|
|
change("f/test/myapp.raw_app/raw_app.yaml"),
|
|
change("f/test/myapp.raw_app/backend/a.ts"),
|
|
change("f/test/low.app/app.yaml"),
|
|
change("f/test/low.app/inline.ts"),
|
|
]);
|
|
|
|
expect(message).toContain("2 item(s)");
|
|
});
|