mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-21 00:02:23 +00:00
3c3e99d1a5
Single contract for the deployment-callback path: the CLI does branch
checkout + pull, the caller (hub script in production, test in test)
does git add + commit + push. This restores the WIN-1974 invariant —
GPG setup and `git commit` run back-to-back in the same process, so
the agent's pre-warmed passphrase cache is still warm at sign time —
without needing a `--skip-commit` flag for the hub case and a default
"also-commit" for everything else. Same behavior in every call site.
Changes:
- sync.ts: drop the gitSyncDeployPush call from pull()'s deploy path
(both the onlyCreateBranch fast-return and the post-pull commit).
`gitSyncDeployPush` stays exported for any caller that wants the
same commit/push semantics — just not invoked by the CLI subcommand.
- gitsync_promotion.test.ts: e2e test now does its own git add +
commit + push after `wmill sync git-deploy`, mirroring what the
hub script does in production. Same regression coverage
(wm_deploy branch created in Case A, main untouched; main updated
in Case B, no new wm_deploy).
CLI typecheck unchanged (two pre-existing TarAsZip errors at lines
2578/3307, present before this PR). All 743 unit tests still pass.
The accompanying hub script (option-C — CLI for branch+pull, script
for commit+push) lives at /tmp/git-sync-diff/sync-script-to-git-repo-windmill.option-C.ts.
Once published, a follow-up bumps LATEST_GIT_SYNC_SCRIPT_PATH to its id.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
203 lines
7.5 KiB
TypeScript
203 lines
7.5 KiB
TypeScript
/**
|
|
* E2E regression test for git-sync promotion / `use_individual_branch`.
|
|
*
|
|
* Guards the hub/28229 regression: the git-sync deployment callback must push
|
|
* each deployed object to a dedicated `wm_deploy/<workspace>/<...>` branch when
|
|
* `use_individual_branch` is set — NOT straight to the cloned base branch
|
|
* (e.g. a protected `main`, which fails with GH006).
|
|
*
|
|
* Contract: `wmill sync git-deploy` does branch checkout + pull only. Commit
|
|
* + push are the caller's job — the hub script does them in the same process
|
|
* as `set_gpg_signing_secret` so the GPG agent's passphrase cache is still
|
|
* warm at sign time (WIN-1974). This test replicates the caller half (git
|
|
* add + commit + push) inline so the full promotion regression stays caught.
|
|
*/
|
|
|
|
import { expect, test } from "bun:test";
|
|
import { execFileSync } from "node:child_process";
|
|
import { mkdtemp, writeFile, rm } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { withTestBackend } from "./test_backend.ts";
|
|
import { shouldSkipOnCI } from "./cargo_backend.ts";
|
|
import { addWorkspace } from "../workspace.ts";
|
|
|
|
function git(cwd: string, ...args: string[]): string {
|
|
return execFileSync("git", args, { cwd, encoding: "utf8" }).trim();
|
|
}
|
|
|
|
// Refs (branches) present on the bare remote.
|
|
function remoteBranches(bareDir: string): string[] {
|
|
const out = execFileSync(
|
|
"git",
|
|
["--git-dir", bareDir, "for-each-ref", "--format=%(refname)", "refs/heads/"],
|
|
{ encoding: "utf8" },
|
|
).trim();
|
|
return out ? out.split("\n") : [];
|
|
}
|
|
|
|
function remoteHead(bareDir: string, branch: string): string {
|
|
return execFileSync(
|
|
"git",
|
|
["--git-dir", bareDir, "rev-parse", `refs/heads/${branch}`],
|
|
{ encoding: "utf8" },
|
|
).trim();
|
|
}
|
|
|
|
test.skipIf(shouldSkipOnCI())(
|
|
"git-sync promotion: use_individual_branch pushes to wm_deploy branch, not main",
|
|
async () => {
|
|
await withTestBackend(async (backend, tempDir) => {
|
|
const ws = backend.workspace; // "test"
|
|
await addWorkspace(
|
|
{
|
|
remote: backend.baseUrl,
|
|
workspaceId: ws,
|
|
name: ws,
|
|
token: backend.token,
|
|
} as any,
|
|
{ force: true, configDir: backend.testConfigDir },
|
|
);
|
|
|
|
// --- 1. Local bare "remote" repo seeded with an initial `main` commit ---
|
|
const bareDir = await mkdtemp(join(tmpdir(), "wmill_promo_bare_"));
|
|
execFileSync("git", ["init", "--bare", "--initial-branch=main", bareDir]);
|
|
const seedDir = await mkdtemp(join(tmpdir(), "wmill_promo_seed_"));
|
|
git(seedDir, "init", "--initial-branch=main");
|
|
git(seedDir, "config", "user.email", "seed@windmill.dev");
|
|
git(seedDir, "config", "user.name", "seed");
|
|
await writeFile(join(seedDir, "README.md"), "# promo test\n");
|
|
git(seedDir, "add", "-A");
|
|
git(seedDir, "commit", "-m", "seed");
|
|
git(seedDir, "remote", "add", "origin", `file://${bareDir}`);
|
|
git(seedDir, "push", "-u", "origin", "main");
|
|
const seedMain = remoteHead(bareDir, "main");
|
|
|
|
// --- 2. Workspace content to sync (a script in a folder) ---
|
|
await backend.apiRequest!(`/api/w/${ws}/folders/create`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ name: "promo", owners: [], extra_perms: {} }),
|
|
});
|
|
await backend.apiRequest!(`/api/w/${ws}/scripts/create`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
path: "f/promo/foo",
|
|
summary: "",
|
|
description: "",
|
|
content: "export async function main() { return 1 }",
|
|
language: "bun",
|
|
}),
|
|
});
|
|
|
|
// --- 3. git_repository resource + git-sync config pointing at the bare repo ---
|
|
await backend.apiRequest!(`/api/w/${ws}/resources/create`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
path: "u/test/promo_repo",
|
|
resource_type: "git_repository",
|
|
value: { url: `file://${bareDir}`, branch: "main", token: "" },
|
|
}),
|
|
});
|
|
await backend.updateGitSyncConfig!({
|
|
git_sync_settings: {
|
|
repositories: [
|
|
{
|
|
git_repo_resource_path: "u/test/promo_repo",
|
|
script_path: "f/**",
|
|
use_individual_branch: true,
|
|
group_by_folder: false,
|
|
settings: {
|
|
include_path: ["f/**"],
|
|
include_type: ["script"],
|
|
},
|
|
},
|
|
],
|
|
},
|
|
});
|
|
|
|
const deployItems = JSON.stringify([
|
|
{ path_type: "script", path: "f/promo/foo", commit_msg: "deploy foo" },
|
|
]);
|
|
|
|
// Caller-half: stage anything the CLI's pull dropped, commit on the
|
|
// current branch (which the CLI just checked out), and push. Mirrors
|
|
// what the hub script does in production after `wmill sync git-deploy`.
|
|
const commitAndPush = (work: string) => {
|
|
git(work, "config", "user.email", "test@windmill.dev");
|
|
git(work, "config", "user.name", "test");
|
|
git(work, "add", "-A");
|
|
try {
|
|
git(work, "diff", "--cached", "--quiet");
|
|
// Exit 0 = nothing staged; nothing to commit. Still push the
|
|
// (possibly new) branch ref so the assertions see it.
|
|
} catch {
|
|
git(work, "commit", "-m", "deploy foo");
|
|
}
|
|
git(work, "push", "--porcelain", "-u", "origin", "HEAD");
|
|
};
|
|
|
|
// --- Case A: use_individual_branch=true -> wm_deploy branch, main untouched ---
|
|
const workA = await mkdtemp(join(tmpdir(), "wmill_promo_a_"));
|
|
git(workA, "clone", `file://${bareDir}`, ".");
|
|
await writeFile(
|
|
join(workA, "wmill.yaml"),
|
|
"defaultTs: bun\nincludes:\n - f/**\nexcludes: []\n",
|
|
);
|
|
const resA = await backend.runCLICommand(
|
|
[
|
|
"sync",
|
|
"git-deploy",
|
|
"--repository",
|
|
"u/test/promo_repo",
|
|
"--use-individual-branch",
|
|
"--git-deploy-items",
|
|
deployItems,
|
|
],
|
|
workA,
|
|
);
|
|
expect(resA.code).toBe(0);
|
|
commitAndPush(workA);
|
|
|
|
const branchesA = remoteBranches(bareDir);
|
|
const expectedBranch = `refs/heads/wm_deploy/${ws}/script/f__promo__foo`;
|
|
expect(branchesA).toContain(expectedBranch);
|
|
// The regression: main MUST be untouched (no direct push to protected base).
|
|
expect(remoteHead(bareDir, "main")).toBe(seedMain);
|
|
|
|
// --- Case B: workspace-wide (no flag) -> pushes to main, no wm_deploy ---
|
|
const workB = await mkdtemp(join(tmpdir(), "wmill_promo_b_"));
|
|
git(workB, "clone", `file://${bareDir}`, ".");
|
|
await writeFile(
|
|
join(workB, "wmill.yaml"),
|
|
"defaultTs: bun\nincludes:\n - f/**\nexcludes: []\n",
|
|
);
|
|
const resB = await backend.runCLICommand(
|
|
[
|
|
"sync",
|
|
"git-deploy",
|
|
"--repository",
|
|
"u/test/promo_repo",
|
|
"--git-deploy-items",
|
|
deployItems,
|
|
],
|
|
workB,
|
|
);
|
|
expect(resB.code).toBe(0);
|
|
commitAndPush(workB);
|
|
|
|
expect(remoteHead(bareDir, "main")).not.toBe(seedMain);
|
|
expect(
|
|
remoteBranches(bareDir).filter((b) => b.includes("wm_deploy")).length,
|
|
).toBe(1); // only the one from Case A, none new
|
|
|
|
await rm(bareDir, { recursive: true, force: true });
|
|
await rm(seedDir, { recursive: true, force: true });
|
|
await rm(workA, { recursive: true, force: true });
|
|
await rm(workB, { recursive: true, force: true });
|
|
});
|
|
},
|
|
);
|