Skip to content

Commit 96b2d13

Browse files
CopilotpelikhanCopilot
authored
Resolve trusted PR bundle validation base from the base branch tip (#68717)
* Initial plan * Resolve trusted PR bundle baseline from base branch tip Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> * Trace trusted-base validation decisions without private Git details Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> * Test PR-head fallback when trusted base ref is missing Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Cover trusted-base clone and credential compatibility Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> Co-authored-by: pelikhan <jhalleux@microsoft.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent afb53a2 commit 96b2d13

3 files changed

Lines changed: 339 additions & 16 deletions

File tree

‎actions/setup/js/push_to_pull_request_branch.cjs‎

Lines changed: 64 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -139,18 +139,33 @@ async function getBundlePreApplyFiles(exec, gitOptions, rangeBaseRef, bundleRef,
139139
*/
140140
async function getBundleValidationBase(exec, gitOptions, oldHead, candidate, trustedBase) {
141141
const fallback = { ref: oldHead, conflictedFiles: [] };
142-
if (!trustedBase) return fallback;
142+
if (!trustedBase) {
143+
core.debug("Bundle validation: no trusted base available; using PR-head validation");
144+
return fallback;
145+
}
143146
try {
144-
const opts = { ...gitOptions, ignoreReturnCode: true };
147+
const opts = { ...gitOptions, ignoreReturnCode: true, silent: true };
145148
const oldAncestor = await exec.getExecOutput("git", ["merge-base", "--is-ancestor", oldHead, candidate], opts);
146149
const baseAncestor = await exec.getExecOutput("git", ["merge-base", "--is-ancestor", trustedBase, candidate], opts);
147150
const alreadyMerged = await exec.getExecOutput("git", ["merge-base", "--is-ancestor", trustedBase, oldHead], opts);
148-
if (oldAncestor.exitCode !== 0 || baseAncestor.exitCode !== 0 || alreadyMerged.exitCode !== 1) return fallback;
151+
if (oldAncestor.exitCode !== 0) {
152+
core.debug("Bundle validation: candidate does not descend from PR head or ancestry is unavailable; using PR-head validation");
153+
return fallback;
154+
}
155+
if (baseAncestor.exitCode !== 0) {
156+
core.debug("Bundle validation: candidate does not include trusted base or ancestry is unavailable; using PR-head validation");
157+
return fallback;
158+
}
159+
if (alreadyMerged.exitCode !== 1) {
160+
core.debug("Bundle validation: trusted base is already in PR head or ancestry is unavailable; using PR-head validation");
161+
return fallback;
162+
}
149163

150164
const merge = await exec.getExecOutput("git", ["merge-tree", "--write-tree", "--name-only", "-z", oldHead, trustedBase], opts);
151165
const [tree, ...fields] = merge.stdout.split("\0");
152166
if ((merge.exitCode === 0 || merge.exitCode === 1) && /^[0-9a-f]{40}$/.test(tree)) {
153167
const separatorIndex = fields.indexOf("");
168+
core.debug(merge.exitCode === 1 ? "Bundle validation: trusted-base merge tree selected with conflicts" : "Bundle validation: trusted-base merge tree selected without conflicts");
154169
return {
155170
ref: tree,
156171
conflictedFiles: merge.exitCode === 1 ? fields.slice(0, separatorIndex === -1 ? fields.length : separatorIndex) : [],
@@ -1100,14 +1115,22 @@ async function main(config = {}) {
11001115
// Resolve from GitHub metadata and fetch from the target checkout's
11011116
// origin, never from agent-supplied refs or patch headers.
11021117
let trustedBase = "";
1103-
const baseSha = pullRequest.base?.sha;
1104-
if (!signedCommits && typeof baseSha === "string" && /^[0-9a-f]{40}$/.test(baseSha)) {
1118+
const baseRef = pullRequest.base?.ref;
1119+
if (!signedCommits && typeof baseRef === "string" && baseRef) {
11051120
try {
1106-
await exec.exec("git", ["fetch", "origin", baseSha], { env: { ...process.env, ...gitAuthEnv }, ...baseGitOpts });
1107-
trustedBase = baseSha;
1108-
} catch (baseFetchError) {
1109-
core.warning(`Unable to fetch trusted PR base; using PR-head validation: ${getErrorMessage(baseFetchError)}`);
1121+
await exec.exec("git", ["fetch", "origin", `refs/heads/${baseRef}`], { env: { ...process.env, ...gitAuthEnv }, ...baseGitOpts, silent: true });
1122+
const { stdout } = await exec.getExecOutput("git", ["rev-parse", "--verify", "FETCH_HEAD^{commit}"], { ...baseGitOpts, silent: true });
1123+
if (/^[0-9a-f]{40}$/.test(stdout.trim())) {
1124+
trustedBase = stdout.trim();
1125+
core.debug("Bundle validation: fetched trusted base branch tip");
1126+
} else {
1127+
core.debug("Bundle validation: fetched base tip is invalid; using PR-head validation");
1128+
}
1129+
} catch {
1130+
core.warning("Unable to fetch trusted PR base; using PR-head validation");
11101131
}
1132+
} else {
1133+
core.debug(signedCommits ? "Bundle validation: trusted-base resolution skipped for signed commits" : "Bundle validation: trusted-base resolution skipped because base branch metadata is unavailable");
11111134
}
11121135
await ensureFullHistoryForBundle(
11131136
exec,
@@ -1185,12 +1208,42 @@ async function main(config = {}) {
11851208
}
11861209
}
11871210
core.info(`Fetched bundle to ${bundleRef}`);
1188-
const validationBase = await getBundleValidationBase(exec, baseGitOpts, rangeBaseRef, bundleRef, trustedBase);
1211+
let validationBase = await getBundleValidationBase(exec, baseGitOpts, rangeBaseRef, bundleRef, trustedBase);
1212+
const workflowSha = process.env.GITHUB_SHA;
1213+
if (trustedBase && validationBase.ref === rangeBaseRef && typeof workflowSha === "string" && /^[0-9a-f]{40}$/.test(workflowSha) && workflowSha !== trustedBase) {
1214+
core.debug("Bundle validation: checking workflow-start base fallback");
1215+
try {
1216+
// A run SHA is trusted only if it belongs to the fetched base branch,
1217+
// not an agent-controlled PR head or a pull_request merge ref.
1218+
const runOnBase = await exec.getExecOutput("git", ["merge-base", "--is-ancestor", workflowSha, trustedBase], { ...baseGitOpts, ignoreReturnCode: true, silent: true });
1219+
if (runOnBase.exitCode === 0) {
1220+
validationBase = await getBundleValidationBase(exec, baseGitOpts, rangeBaseRef, bundleRef, workflowSha);
1221+
if (validationBase.ref !== rangeBaseRef) {
1222+
trustedBase = workflowSha;
1223+
core.debug("Bundle validation: workflow-start base fallback selected");
1224+
} else {
1225+
core.debug("Bundle validation: workflow-start base fallback did not establish a baseline");
1226+
}
1227+
} else {
1228+
core.debug("Bundle validation: workflow-start base fallback rejected because base-branch ancestry is unverified");
1229+
}
1230+
} catch {
1231+
core.warning("Unable to verify workflow SHA on the trusted base branch; using PR-head validation");
1232+
}
1233+
} else {
1234+
core.debug(
1235+
validationBase.ref !== rangeBaseRef
1236+
? "Bundle validation: workflow-start base fallback skipped because base tip established a baseline"
1237+
: "Bundle validation: workflow-start base fallback skipped because no distinct valid workflow base is available"
1238+
);
1239+
}
11891240
validationBaseRef = validationBase.ref;
11901241
validationConflictFiles = validationBase.conflictedFiles;
11911242
const trustedBaseInValidationBase = validationBaseRef !== rangeBaseRef;
11921243
if (trustedBaseInValidationBase) {
1193-
core.info(`Bundle validation baseline includes trusted base ${trustedBase}: ${validationBaseRef}`);
1244+
core.info("Bundle validation baseline includes trusted base changes");
1245+
} else {
1246+
core.info("Bundle validation baseline uses PR head");
11941247
}
11951248
const agentHistoryExclusions = [rangeBaseRef];
11961249
if (trustedBaseInValidationBase) agentHistoryExclusions.push(trustedBase);

0 commit comments

Comments
 (0)