Skip to content
Open
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
23 changes: 0 additions & 23 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -16,32 +16,9 @@ tests/fixtures/runtime_imports_author_marker_stage.lock.yml linguist-generated=t
tests/fixtures/runtime_imports_job.lock.yml linguist-generated=true merge=ours text eol=lf
tests/fixtures/runtime_imports_stage.lock.yml linguist-generated=true merge=ours text eol=lf
tests/fixtures/stage-agent.lock.yml linguist-generated=true merge=ours text eol=lf
tests/safe-outputs/add-build-tag.lock.yml linguist-generated=true merge=ours text eol=lf
tests/safe-outputs/add-pr-comment.lock.yml linguist-generated=true merge=ours text eol=lf
tests/safe-outputs/azure-cli.lock.yml linguist-generated=true merge=ours text eol=lf
tests/safe-outputs/canary.lock.yml linguist-generated=true merge=ours text eol=lf
tests/safe-outputs/comment-on-work-item.lock.yml linguist-generated=true merge=ours text eol=lf
tests/safe-outputs/create-branch.lock.yml linguist-generated=true merge=ours text eol=lf
tests/safe-outputs/create-git-tag.lock.yml linguist-generated=true merge=ours text eol=lf
tests/safe-outputs/create-pull-request.lock.yml linguist-generated=true merge=ours text eol=lf
tests/safe-outputs/create-wiki-page.lock.yml linguist-generated=true merge=ours text eol=lf
tests/safe-outputs/create-work-item.lock.yml linguist-generated=true merge=ours text eol=lf
tests/safe-outputs/janitor.lock.yml linguist-generated=true merge=ours text eol=lf
tests/safe-outputs/link-work-items.lock.yml linguist-generated=true merge=ours text eol=lf
tests/safe-outputs/missing-data.lock.yml linguist-generated=true merge=ours text eol=lf
tests/safe-outputs/missing-tool.lock.yml linguist-generated=true merge=ours text eol=lf
tests/safe-outputs/noop-target.lock.yml linguist-generated=true merge=ours text eol=lf
tests/safe-outputs/noop.lock.yml linguist-generated=true merge=ours text eol=lf
tests/safe-outputs/queue-build.lock.yml linguist-generated=true merge=ours text eol=lf
tests/safe-outputs/reply-to-pr-comment.lock.yml linguist-generated=true merge=ours text eol=lf
tests/safe-outputs/report-incomplete.lock.yml linguist-generated=true merge=ours text eol=lf
tests/safe-outputs/resolve-pr-thread.lock.yml linguist-generated=true merge=ours text eol=lf
tests/safe-outputs/smoke-failure-reporter.lock.yml linguist-generated=true merge=ours text eol=lf
tests/safe-outputs/submit-pr-review.lock.yml linguist-generated=true merge=ours text eol=lf
tests/safe-outputs/update-pr.lock.yml linguist-generated=true merge=ours text eol=lf
tests/safe-outputs/update-wiki-page.lock.yml linguist-generated=true merge=ours text eol=lf
tests/safe-outputs/update-work-item.lock.yml linguist-generated=true merge=ours text eol=lf
tests/safe-outputs/upload-build-attachment.lock.yml linguist-generated=true merge=ours text eol=lf
tests/safe-outputs/upload-pipeline-artifact.lock.yml linguist-generated=true merge=ours text eol=lf
tests/safe-outputs/upload-workitem-attachment.lock.yml linguist-generated=true merge=ours text eol=lf
# END ado-aw managed
164 changes: 164 additions & 0 deletions scripts/ado-script/src/trigger-e2e/__tests__/mirror.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import { describe, expect, it, vi } from "vitest";

import {
loadMirrorSyncConfig,
mirrorRepoUrl,
runMirrorSyncPreflight,
type MirrorGitRequest,
type MirrorGitResult,
type MirrorGitRunner,
} from "../mirror.js";

const HEAD = "1111111111111111111111111111111111111111";

function baseEnv(): NodeJS.ProcessEnv {
return {
TRIGGER_E2E_SYNC_MIRROR: "true",
TRIGGER_E2E_VICTIM_REPO: "ado-aw-mirror",
SYSTEM_COLLECTIONURI: "https://dev.azure.com/msazuresphere/",
SYSTEM_TEAMPROJECT: "Agent Playground",
SYSTEM_ACCESSTOKEN: "secret-token",
BUILD_SOURCESDIRECTORY: "/src",
BUILD_SOURCEBRANCH: "refs/heads/main",
BUILD_SOURCEVERSION: HEAD,
};
}

function successRunner(calls: MirrorGitRequest[]): MirrorGitRunner {
return async (request): Promise<MirrorGitResult> => {
calls.push(request);
if (request.args[0] === "rev-parse" && request.args[1] === "--is-shallow-repository") {
return { status: 0, stdout: "false\n", stderr: "" };
}
if (request.args[0] === "rev-parse") {
return { status: 0, stdout: `${HEAD}\n`, stderr: "" };
}
if (request.args[0] === "ls-remote") {
return { status: 0, stdout: `${HEAD}\trefs/heads/main\n`, stderr: "" };
}
return { status: 0, stdout: "", stderr: "" };
};
}

describe("trigger mirror sync", () => {
it("is disabled unless explicitly opted in", async () => {
const runner = vi.fn<MirrorGitRunner>();
const result = await runMirrorSyncPreflight({}, () => {}, runner);
expect(result).toBeUndefined();
expect(runner).not.toHaveBeenCalled();
});

it("builds an encoded ADO Git URL", () => {
expect(
mirrorRepoUrl(
"https://dev.azure.com/msazuresphere/",
"Agent Playground",
"ado aw/mirror",
),
).toBe(
"https://dev.azure.com/msazuresphere/Agent%20Playground/_git/ado%20aw%2Fmirror",
);
});

it("pushes main fast-forward-only and verifies the remote SHA", async () => {
const calls: MirrorGitRequest[] = [];
const result = await runMirrorSyncPreflight(baseEnv(), () => {}, successRunner(calls));

expect(result?.ok).toBe(true);
expect(calls.map((call) => call.args[0])).toEqual([
"rev-parse",
"rev-parse",
"push",
"ls-remote",
]);
const push = calls[2];
expect(push?.args).toContain("HEAD:refs/heads/main");
expect(push?.args).not.toContain("--force");
expect(push?.args.join(" ")).not.toContain("secret-token");
expect(push?.env.GIT_CONFIG_VALUE_0).toBe("Authorization: bearer secret-token");
expect(push?.cwd).toBe("/src");
});

it("rejects a non-main orchestrator checkout before invoking git", async () => {
const runner = vi.fn<MirrorGitRunner>();
const result = await runMirrorSyncPreflight(
{ ...baseEnv(), BUILD_SOURCEBRANCH: "refs/heads/feature" },
() => {},
runner,
);

expect(result?.ok).toBe(false);
expect(result?.message).toContain("refs/heads/main");
expect(runner).not.toHaveBeenCalled();
});

it("rejects a shallow checkout", async () => {
const runner: MirrorGitRunner = async () => ({
status: 0,
stdout: "true\n",
stderr: "",
});
const result = await runMirrorSyncPreflight(baseEnv(), () => {}, runner);

expect(result?.ok).toBe(false);
expect(result?.message).toContain("fetchDepth: 0");
});

it("fails closed on a non-fast-forward push", async () => {
const calls: MirrorGitRequest[] = [];
const runner: MirrorGitRunner = async (request) => {
calls.push(request);
if (request.args[0] === "rev-parse" && request.args[1] === "--is-shallow-repository") {
return { status: 0, stdout: "false\n", stderr: "" };
}
if (request.args[0] === "rev-parse") {
return { status: 0, stdout: `${HEAD}\n`, stderr: "" };
}
return { status: 1, stdout: "", stderr: "! [rejected] non-fast-forward" };
};

const result = await runMirrorSyncPreflight(baseEnv(), () => {}, runner);
expect(result?.ok).toBe(false);
expect(result?.message).toContain("non-fast-forward");
expect(calls.some((call) => call.args[0] === "ls-remote")).toBe(false);
});

it("fails when the verified remote SHA differs", async () => {
const runner: MirrorGitRunner = async (request) => {
if (request.args[0] === "rev-parse" && request.args[1] === "--is-shallow-repository") {
return { status: 0, stdout: "false\n", stderr: "" };
}
if (request.args[0] === "rev-parse") {
return { status: 0, stdout: `${HEAD}\n`, stderr: "" };
}
if (request.args[0] === "ls-remote") {
return {
status: 0,
stdout: "2222222222222222222222222222222222222222\trefs/heads/main\n",
stderr: "",
};
}
return { status: 0, stdout: "", stderr: "" };
};

const result = await runMirrorSyncPreflight(baseEnv(), () => {}, runner);
expect(result?.ok).toBe(false);
expect(result?.message).toContain("mirror verification failed");
});

it("preserves the bypass-only baseline when the victim repo is unset", async () => {
const runner = vi.fn<MirrorGitRunner>();
const logs: string[] = [];
const env = {
...baseEnv(),
TRIGGER_E2E_VICTIM_REPO: "",
};

expect(loadMirrorSyncConfig(env)).toBeUndefined();
expect(await runMirrorSyncPreflight(env, (message) => logs.push(message), runner)).toBeUndefined();
expect(logs).toContain(
"[mirror-sync] skipped: TRIGGER_E2E_VICTIM_REPO is not configured",
);
expect(runner).not.toHaveBeenCalled();
});
});
19 changes: 13 additions & 6 deletions scripts/ado-script/src/trigger-e2e/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
*/
import { AdoRest } from "../executor-e2e/ado-rest.js";
import { fileFailureIssue, loadIssueEnv } from "./github-issue.js";
import { runMirrorSyncPreflight } from "./mirror.js";
import { runAll } from "./runner.js";
import { allScenarios } from "./scenarios/index.js";
import type { ScenarioResult, TriggerContext } from "./scenario.js";
Expand Down Expand Up @@ -87,12 +88,18 @@ export async function main(): Promise<number> {
prefix: (id) => `ado-aw-trig-${buildId}-${id}`,
};

log(
`Running ${allScenarios.length} trigger E2E scenarios against ${orgUrl}${project} ` +
`(victim def #${victimDefinitionId}${adoRepo ? `, repo ${adoRepo}` : ", no PR repo — PR scenarios will skip"})`,
);

const results = await runAll(ctx, allScenarios);
const preflight = await runMirrorSyncPreflight(process.env, log);
let results: ScenarioResult[];
if (preflight && !preflight.ok) {
results = [preflight];
} else {
log(
`Running ${allScenarios.length} trigger E2E scenarios against ${orgUrl}${project} ` +
`(victim def #${victimDefinitionId}${adoRepo ? `, repo ${adoRepo}` : ", no PR repo — PR scenarios will skip"})`,
);
const scenarioResults = await runAll(ctx, allScenarios);
results = preflight ? [preflight, ...scenarioResults] : scenarioResults;
}
log(summarise(results));

const issueEnv = loadIssueEnv();
Expand Down
Loading
Loading