From e22badb858a8cb562ae9e6ec89fa15e1ca629472 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Jun 2026 20:50:20 +0000 Subject: [PATCH 1/2] Initial plan From e165bf29c593c713914cec29e41eaae16098ce69 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Jun 2026 20:56:03 +0000 Subject: [PATCH 2/2] fix(gate): use '.' instead of ':' in build tags ADO rejects in REST path Co-authored-by: jamesadevine <4742697+jamesadevine@users.noreply.github.com> --- .../src/gate/__tests__/ports/integration.test.ts | 2 +- scripts/ado-script/src/gate/bypass.test.ts | 10 +++++++++- scripts/ado-script/src/gate/bypass.ts | 2 +- scripts/ado-script/src/gate/predicates.test.ts | 4 ++-- scripts/ado-script/src/gate/predicates.ts | 2 +- scripts/ado-script/src/gate/selfcancel.test.ts | 12 +++++++++++- scripts/ado-script/src/gate/selfcancel.ts | 2 +- src/compile/filter_ir.rs | 16 ++++++++++++++++ 8 files changed, 42 insertions(+), 8 deletions(-) diff --git a/scripts/ado-script/src/gate/__tests__/ports/integration.test.ts b/scripts/ado-script/src/gate/__tests__/ports/integration.test.ts index 815df7e5..454cd23d 100644 --- a/scripts/ado-script/src/gate/__tests__/ports/integration.test.ts +++ b/scripts/ado-script/src/gate/__tests__/ports/integration.test.ts @@ -29,6 +29,6 @@ describe("evaluatePredicates integration ports", () => { expect(evaluatePredicates(spec, factMap({ pr_title: "bad" }), tracker)).toEqual(["fail"]); expect(tracker.summary()).toEqual({ passed: 0, failed: 1, skipped: 0 }); - expect(writes).toContain("##vso[build.addbuildtag]gate:title\n"); + expect(writes).toContain("##vso[build.addbuildtag]gate.title\n"); }); }); diff --git a/scripts/ado-script/src/gate/bypass.test.ts b/scripts/ado-script/src/gate/bypass.test.ts index 5409767c..192ed7c5 100644 --- a/scripts/ado-script/src/gate/bypass.test.ts +++ b/scripts/ado-script/src/gate/bypass.test.ts @@ -33,10 +33,18 @@ describe("runBypass", () => { const joined = writes.join(""); expect(joined).toContain("Not a Pull Request build"); expect(joined).toContain("setvariable variable=SHOULD_RUN;isOutput=true]true"); - expect(joined).toContain("##vso[build.addbuildtag]pr-gate:passed"); + expect(joined).toContain("##vso[build.addbuildtag]pr-gate.passed"); expect(joined).toContain("##vso[task.complete result=Succeeded;]"); }); + it("emits a build tag that contains no ':' (ADO rejects ':' in the tag REST path)", async () => { + process.env.ADO_BUILD_REASON = "Manual"; + await runBypass(baseSpec); + const tagLine = writes.find((w) => w.startsWith("##vso[build.addbuildtag]")); + expect(tagLine).toBeDefined(); + expect((tagLine as string).slice("##vso[build.addbuildtag]".length)).not.toContain(":"); + }); + it("returns false when build reason matches (no bypass)", async () => { process.env.ADO_BUILD_REASON = "PullRequest"; const result = await runBypass(baseSpec); diff --git a/scripts/ado-script/src/gate/bypass.ts b/scripts/ado-script/src/gate/bypass.ts index a3c6adeb..78ef3536 100644 --- a/scripts/ado-script/src/gate/bypass.ts +++ b/scripts/ado-script/src/gate/bypass.ts @@ -14,7 +14,7 @@ export async function runBypass(spec: GateSpec): Promise { // into the line. Mirrors the Python log line for parity. logInfo(`Not a ${spec.context.bypass_label} build -- gate passes automatically`); setOutput("SHOULD_RUN", "true"); - addBuildTag(`${spec.context.tag_prefix}:passed`); + addBuildTag(`${spec.context.tag_prefix}.passed`); complete("Succeeded"); return true; } diff --git a/scripts/ado-script/src/gate/predicates.test.ts b/scripts/ado-script/src/gate/predicates.test.ts index 15e631e3..df47e22f 100644 --- a/scripts/ado-script/src/gate/predicates.test.ts +++ b/scripts/ado-script/src/gate/predicates.test.ts @@ -422,7 +422,7 @@ describe("evaluatePredicates", () => { expect(results).toEqual(["pass", "fail"]); expect(tracker.summary()).toEqual({ passed: 1, failed: 1, skipped: 0 }); - expect(writes).toContain("##vso[build.addbuildtag]gate:reason\n"); + expect(writes).toContain("##vso[build.addbuildtag]gate.reason\n"); }); it("uses tracker verdicts for unavailable facts before evaluating predicates", () => { @@ -443,7 +443,7 @@ describe("evaluatePredicates", () => { expect(results).toEqual(["fail"]); expect(tracker.summary()).toEqual({ passed: 0, failed: 1, skipped: 0 }); - expect(writes).toContain("##vso[build.addbuildtag]gate:missing-title\n"); + expect(writes).toContain("##vso[build.addbuildtag]gate.missing-title\n"); }); }); diff --git a/scripts/ado-script/src/gate/predicates.ts b/scripts/ado-script/src/gate/predicates.ts index b1c48f16..735ad9ba 100644 --- a/scripts/ado-script/src/gate/predicates.ts +++ b/scripts/ado-script/src/gate/predicates.ts @@ -40,7 +40,7 @@ export function evaluatePredicates( } if (result === "fail") { - addBuildTag(`${spec.context.tag_prefix}:${check.tag_suffix}`); + addBuildTag(`${spec.context.tag_prefix}.${check.tag_suffix}`); } tracker.recordCheckResult(result); results.push(result); diff --git a/scripts/ado-script/src/gate/selfcancel.test.ts b/scripts/ado-script/src/gate/selfcancel.test.ts index 76e6e8fd..0c27416a 100644 --- a/scripts/ado-script/src/gate/selfcancel.test.ts +++ b/scripts/ado-script/src/gate/selfcancel.test.ts @@ -56,10 +56,20 @@ describe("selfCancelIfRequested", () => { process.env.ADO_BUILD_ID = "42"; cancelBuildMock.mockResolvedValue(undefined); await selfCancelIfRequested(baseSpec); - expect(writes.join("")).toContain("##vso[build.addbuildtag]pr-gate:skipped"); + expect(writes.join("")).toContain("##vso[build.addbuildtag]pr-gate.skipped"); expect(cancelBuildMock).toHaveBeenCalledWith("p", 42); }); + it("emits a build tag that contains no ':' (ADO rejects ':' in the tag REST path)", async () => { + process.env.ADO_PROJECT = "p"; + process.env.ADO_BUILD_ID = "42"; + cancelBuildMock.mockResolvedValue(undefined); + await selfCancelIfRequested(baseSpec); + const tagLine = writes.find((w) => w.startsWith("##vso[build.addbuildtag]")); + expect(tagLine).toBeDefined(); + expect((tagLine as string).slice("##vso[build.addbuildtag]".length)).not.toContain(":"); + }); + it("logs warning and does NOT call cancelBuild when ADO_PROJECT missing", async () => { delete process.env.ADO_PROJECT; process.env.ADO_BUILD_ID = "42"; diff --git a/scripts/ado-script/src/gate/selfcancel.ts b/scripts/ado-script/src/gate/selfcancel.ts index 2dfeb7e0..811b8a27 100644 --- a/scripts/ado-script/src/gate/selfcancel.ts +++ b/scripts/ado-script/src/gate/selfcancel.ts @@ -8,7 +8,7 @@ import { cancelBuild } from "../shared/ado-client.js"; import { logWarning, addBuildTag } from "../shared/vso-logger.js"; export async function selfCancelIfRequested(spec: GateSpec): Promise { - addBuildTag(`${spec.context.tag_prefix}:skipped`); + addBuildTag(`${spec.context.tag_prefix}.skipped`); const project = process.env.ADO_PROJECT ?? ""; const buildIdRaw = process.env.ADO_BUILD_ID ?? ""; diff --git a/src/compile/filter_ir.rs b/src/compile/filter_ir.rs index 6c35e1f6..235e2506 100644 --- a/src/compile/filter_ir.rs +++ b/src/compile/filter_ir.rs @@ -1747,6 +1747,22 @@ mod tests { assert_eq!(spec.checks[1].name, "labels"); } + #[test] + fn test_tag_prefix_has_no_colon() { + // Build tags are PUT into the ADO REST request path + // (…/builds//tags/); a ':' trips ASP.NET's + // dangerous-request-path validator and fails the whole build. + // The gate composes tags as `${tag_prefix}.${suffix}`, so a colon + // in tag_prefix itself would reintroduce the rejected character. + for ctx in [GateContext::PullRequest, GateContext::PipelineCompletion] { + assert!( + !ctx.tag_prefix().contains(':'), + "tag_prefix {:?} must not contain ':'", + ctx.tag_prefix(), + ); + } + } + #[test] fn test_gate_spec_serializes_to_valid_json() { let checks = vec![FilterCheck {