Skip to content

Commit e3b59ab

Browse files
Copilotpelikhangithub-actions[bot]claude
authored
Add experimental gated merge-pull-request safe-output with policy-driven merge enforcement (#27193)
* Add merge-pull-request safe-output config and runtime handler scaffolding Agent-Logs-Url: https://github.com/github/gh-aw/sessions/31a07f1a-cfce-42d7-9fb2-5db24724d231 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> * Fix merge_pull_request handler type-safe success path Agent-Logs-Url: https://github.com/github/gh-aw/sessions/31a07f1a-cfce-42d7-9fb2-5db24724d231 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> * Add retry and extensive logging to merge_pull_request handler Agent-Logs-Url: https://github.com/github/gh-aw/sessions/2b89cfe4-e6fd-4edf-ac73-d8ad50a84640 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> * Refine merge_pull_request retry logic and diagnostic logging Agent-Logs-Url: https://github.com/github/gh-aw/sessions/2b89cfe4-e6fd-4edf-ac73-d8ad50a84640 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> * Block merge_pull_request on repository default branch Agent-Logs-Url: https://github.com/github/gh-aw/sessions/b1124194-bc78-4fea-a403-2bc920b62c05 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> * Simplify default branch detection in merge gate Agent-Logs-Url: https://github.com/github/gh-aw/sessions/b1124194-bc78-4fea-a403-2bc920b62c05 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> * Retry-wrap GraphQL review summary calls Agent-Logs-Url: https://github.com/github/gh-aw/sessions/88afb2b1-7be3-42a1-be22-0271d7ec255e Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> * Update safe-outputs specification for merge_pull_request Agent-Logs-Url: https://github.com/github/gh-aw/sessions/32fbbe57-499c-444c-8898-4a778723de9f Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> * Add spec enforcement test for merge_pull_request Agent-Logs-Url: https://github.com/github/gh-aw/sessions/72be4b0f-9a33-4b70-97b5-af041056b56f Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> * docs(adr): add draft ADR-27193 for gated merge-pull-request safe-output Generated by Design Decision Gate workflow to document the architectural decision to add merge-pull-request as a safe-output type with policy-driven gate enforcement integrated into the existing compiler+runtime model. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Enforce protected base branch checks and branch name sanitization Agent-Logs-Url: https://github.com/github/gh-aw/sessions/dfc96d3b-31d1-421b-8c18-997d46b780dc Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> * Treat merge allowed-labels as exact labels not globs Agent-Logs-Url: https://github.com/github/gh-aw/sessions/8e341bdf-224f-4ff1-b265-5f2cdc3f0355 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> * Expand exact-label matching tests for merge_pull_request Agent-Logs-Url: https://github.com/github/gh-aw/sessions/8e341bdf-224f-4ff1-b265-5f2cdc3f0355 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> * Add tests for merge pull request label validation Agent-Logs-Url: https://github.com/github/gh-aw/sessions/8e341bdf-224f-4ff1-b265-5f2cdc3f0355 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> * Refuse default-branch merges in spec tests and handle temporary IDs Agent-Logs-Url: https://github.com/github/gh-aw/sessions/619e2266-55dc-4e30-8ee9-dd5121c508a7 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> * Polish merge_pull_request docs and test naming Agent-Logs-Url: https://github.com/github/gh-aw/sessions/619e2266-55dc-4e30-8ee9-dd5121c508a7 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> * Improve merge PR temp-id resolution messaging and tests Agent-Logs-Url: https://github.com/github/gh-aw/sessions/619e2266-55dc-4e30-8ee9-dd5121c508a7 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> * Merge main and recompile workflow lock files Agent-Logs-Url: https://github.com/github/gh-aw/sessions/b234fb4f-e045-4207-b31f-b67ab0d8b247 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> * Mark merge-pull-request as experimental and add warning coverage Agent-Logs-Url: https://github.com/github/gh-aw/sessions/c4404723-239f-49a4-a2b5-227c03e4fd68 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> * Enable merge-pull-request safe-output contribution from imports Agent-Logs-Url: https://github.com/github/gh-aw/sessions/5bd8b7a6-57df-4bf1-9507-8b8f9014f4c3 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> * Remove merge-pull-request file-scope gating fields Agent-Logs-Url: https://github.com/github/gh-aw/sessions/20cace45-98cf-44f0-9f31-f4a49e3423ff Co-authored-by: pelikhan <4175913+pelikhan@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: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Peli de Halleux <pelikhan@users.noreply.github.com>
1 parent f46ecdb commit e3b59ab

27 files changed

+1614
-63
lines changed
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// @ts-check
2+
3+
/**
4+
* Returns true for check runs that represent deployment environment gates rather
5+
* than CI checks.
6+
* @param {any} run
7+
* @returns {boolean}
8+
*/
9+
function isDeploymentCheck(run) {
10+
return run?.app?.slug === "github-deployments";
11+
}
12+
13+
/**
14+
* Select latest check run per name and apply standard filtering.
15+
* @param {any[]} checkRuns
16+
* @param {{
17+
* includeList?: string[]|null,
18+
* excludeList?: string[]|null,
19+
* excludedCheckRunIds?: Set<number>,
20+
* }} [options]
21+
* @returns {{relevant: any[], deploymentCheckCount: number, currentRunFilterCount: number}}
22+
*/
23+
function selectLatestRelevantChecks(checkRuns, options = {}) {
24+
const includeList = options.includeList || null;
25+
const excludeList = options.excludeList || null;
26+
const excludedCheckRunIds = options.excludedCheckRunIds || new Set();
27+
28+
/** @type {Map<string, any>} */
29+
const latestByName = new Map();
30+
let deploymentCheckCount = 0;
31+
let currentRunFilterCount = 0;
32+
33+
for (const run of checkRuns) {
34+
if (isDeploymentCheck(run)) {
35+
deploymentCheckCount++;
36+
continue;
37+
}
38+
if (excludedCheckRunIds.has(run.id)) {
39+
currentRunFilterCount++;
40+
continue;
41+
}
42+
const existing = latestByName.get(run.name);
43+
if (!existing || new Date(run.started_at ?? 0) > new Date(existing.started_at ?? 0)) {
44+
latestByName.set(run.name, run);
45+
}
46+
}
47+
48+
const relevant = [];
49+
for (const [name, run] of latestByName) {
50+
if (includeList && includeList.length > 0 && !includeList.includes(name)) {
51+
continue;
52+
}
53+
if (excludeList && excludeList.length > 0 && excludeList.includes(name)) {
54+
continue;
55+
}
56+
relevant.push(run);
57+
}
58+
59+
return { relevant, deploymentCheckCount, currentRunFilterCount };
60+
}
61+
62+
/**
63+
* Computes failing checks with shared semantics.
64+
* @param {any[]} checkRuns
65+
* @param {{allowPending?: boolean}} [options]
66+
* @returns {any[]}
67+
*/
68+
function getFailingChecks(checkRuns, options = {}) {
69+
const allowPending = options.allowPending === true;
70+
const failedConclusions = new Set(["failure", "cancelled", "timed_out"]);
71+
return checkRuns.filter(run => {
72+
if (run.status === "completed") {
73+
return run.conclusion != null && failedConclusions.has(run.conclusion);
74+
}
75+
return !allowPending;
76+
});
77+
}
78+
79+
module.exports = {
80+
isDeploymentCheck,
81+
selectLatestRelevantChecks,
82+
getFailingChecks,
83+
};

actions/setup/js/check_skip_if_check_failing.cjs

Lines changed: 7 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const { getErrorMessage, isRateLimitError } = require("./error_helpers.cjs");
55
const { ERR_API } = require("./error_codes.cjs");
66
const { getBaseBranch } = require("./get_base_branch.cjs");
77
const { writeDenialSummary } = require("./pre_activation_summary.cjs");
8+
const { selectLatestRelevantChecks, getFailingChecks } = require("./check_runs_helpers.cjs");
89

910
/**
1011
* Determines the ref to check for CI status.
@@ -52,22 +53,6 @@ function parseListEnv(envValue) {
5253
}
5354
}
5455

55-
/**
56-
* Returns true for check runs that represent deployment environment gates rather
57-
* than CI checks. These should be ignored by default so that a pending deployment
58-
* approval does not falsely block the agentic workflow.
59-
*
60-
* Deployment gate checks are identified by the GitHub App that created them:
61-
* - "github-deployments" – the built-in GitHub Deployments service
62-
*
63-
* @param {object} run - A check run object from the GitHub API
64-
* @returns {boolean}
65-
*/
66-
function isDeploymentCheck(run) {
67-
const slug = run.app?.slug;
68-
return slug === "github-deployments";
69-
}
70-
7156
/**
7257
* Fetches the check run IDs for all jobs in the current workflow run.
7358
* These IDs are used to filter out the current workflow's own checks
@@ -149,25 +134,11 @@ async function main() {
149134
// Filter to the latest run per check name (GitHub may have multiple runs per name).
150135
// Deployment gate checks and the current run's own checks are silently skipped here
151136
// so they never influence the gate.
152-
/** @type {Map<string, object>} */
153-
const latestByName = new Map();
154-
let deploymentCheckCount = 0;
155-
let currentRunFilterCount = 0;
156-
for (const run of checkRuns) {
157-
if (isDeploymentCheck(run)) {
158-
deploymentCheckCount++;
159-
continue;
160-
}
161-
if (currentRunCheckRunIds.has(run.id)) {
162-
currentRunFilterCount++;
163-
continue;
164-
}
165-
const name = run.name;
166-
const existing = latestByName.get(name);
167-
if (!existing || new Date(run.started_at ?? 0) > new Date(existing.started_at ?? 0)) {
168-
latestByName.set(name, run);
169-
}
170-
}
137+
const { relevant, deploymentCheckCount, currentRunFilterCount } = selectLatestRelevantChecks(checkRuns, {
138+
includeList,
139+
excludeList,
140+
excludedCheckRunIds: currentRunCheckRunIds,
141+
});
171142

172143
if (deploymentCheckCount > 0) {
173144
core.info(`Skipping ${deploymentCheckCount} deployment gate check(s) (app: github-deployments)`);
@@ -176,32 +147,9 @@ async function main() {
176147
core.info(`Skipping ${currentRunFilterCount} check run(s) from the current workflow run`);
177148
}
178149

179-
// Apply user-defined include/exclude filtering
180-
const relevant = [];
181-
for (const [name, run] of latestByName) {
182-
if (includeList && includeList.length > 0 && !includeList.includes(name)) {
183-
continue;
184-
}
185-
if (excludeList && excludeList.length > 0 && excludeList.includes(name)) {
186-
continue;
187-
}
188-
relevant.push(run);
189-
}
190-
191150
core.info(`Evaluating ${relevant.length} check run(s) after filtering`);
192151

193-
// A check is "failing" if it either:
194-
// 1. Completed with a non-success conclusion (failure, cancelled, timed_out), OR
195-
// 2. Is still pending/in-progress — unless allow-pending is set
196-
const failedConclusions = new Set(["failure", "cancelled", "timed_out"]);
197-
198-
const failingChecks = relevant.filter(run => {
199-
if (run.status === "completed") {
200-
return run.conclusion != null && failedConclusions.has(run.conclusion);
201-
}
202-
// Pending/queued/in_progress: treat as failing unless allow-pending is true
203-
return !allowPending;
204-
});
152+
const failingChecks = getFailingChecks(relevant, { allowPending });
205153

206154
if (failingChecks.length > 0) {
207155
const names = failingChecks.map(r => (r.status === "completed" ? `${r.name} (${r.conclusion})` : `${r.name} (${r.status})`)).join(", ");

0 commit comments

Comments
 (0)