From b41eab04170530c08bc0cfdca7452e6fcea8b2bb Mon Sep 17 00:00:00 2001 From: Michael Lahargou Date: Wed, 17 Jun 2026 10:17:06 -0700 Subject: [PATCH 1/2] fix: map cancelled check runs to failure, preserve raw conclusion Cancelled runs (manual cancel or timeout) now map to "failure" instead of "error" so they block merge like real failures. The description field now stores the raw GitHub conclusion (cancelled, failure, timed_out, etc.) instead of the mapped state, so dashboards can differentiate between true failures and cancelled runs. Updated both the webhook and refresh paths for consistency. Co-Authored-By: Claude Opus 4.6 (1M context) --- controllers/githubHooks.js | 7 +++---- lib/git-manager.js | 5 +++-- lib/utils.js | 1 + 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/controllers/githubHooks.js b/controllers/githubHooks.js index 97e77eaf..7ad36334 100644 --- a/controllers/githubHooks.js +++ b/controllers/githubHooks.js @@ -155,16 +155,15 @@ const HooksController = { dbUpdated = dbManager.updateComment(comment); } } else if (event === "check_run") { - const state = utils.mapCheckToStatus( - body.check_run.conclusion || body.check_run.status - ); + const conclusion = body.check_run.conclusion || body.check_run.status; + const state = utils.mapCheckToStatus(conclusion); const status = new Status({ repo: body.repository.full_name, sha: body.check_run.head_sha, state: state, context: body.check_run.name, - description: state, + description: conclusion, target_url: body.check_run.html_url, started_at: body.check_run.started_at, completed_at: body.check_run.completed_at, diff --git a/lib/git-manager.js b/lib/git-manager.js index 073489f3..1181a577 100644 --- a/lib/git-manager.js +++ b/lib/git-manager.js @@ -255,8 +255,9 @@ export default { }); let checks = jobRuns.map(function (jobRun) { - let state = utils.mapCheckToStatus(jobRun.conclusion || jobRun.status); - let desc = state; + let conclusion = jobRun.conclusion || jobRun.status; + let state = utils.mapCheckToStatus(conclusion); + let desc = conclusion; let url = jobRun.html_url; let context = jobRun.name; diff --git a/lib/utils.js b/lib/utils.js index be443829..e1e4b41e 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -77,6 +77,7 @@ export default { case "skipped": return "success"; case "failure": + case "cancelled": return "failure"; default: return "error"; From 7be3c906960c85223e98537033ee80f6c8b2c254 Mon Sep 17 00:00:00 2001 From: Michael Lahargou Date: Wed, 17 Jun 2026 13:20:44 -0700 Subject: [PATCH 2/2] fix: also map timed_out and startup_failure to failure These GitHub check conclusions represent runs that did not complete successfully and should block merge. Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/utils.js | 146 ++++++++++++++++++++++++++------------------------- 1 file changed, 74 insertions(+), 72 deletions(-) diff --git a/lib/utils.js b/lib/utils.js index e1e4b41e..01b02b40 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,86 +1,88 @@ -import config from "./config-loader.js"; -import Bluebird from "bluebird"; -import _ from "underscore"; +import config from './config-loader.js'; +import Bluebird from 'bluebird'; +import _ from 'underscore'; const Promise = global.Promise; // Set the global Promise object up with the done method Promise.prototype.done = function (callback) { - return Bluebird.cast(this).done(callback); + return Bluebird.cast(this).done(callback); }; export default { - /** - * Converts `t` to a Unix timestamp from a Date object unless it's already - * a number. - */ - toUnixTime: function (date) { - const type = typeof date; - if (!date || type == "number") { + /** + * Converts `t` to a Unix timestamp from a Date object unless it's already + * a number. + */ + toUnixTime: function (date) { + const type = typeof date; + if (!date || type == 'number') { + return date; + } + if (type === 'object') { + return date.getTime() / 1000; + } + if (type === 'string') { + return Date.parse(date) / 1000; + } return date; - } - if (type === "object") { - return date.getTime() / 1000; - } - if (type === "string") { - return Date.parse(date) / 1000; - } - return date; - }, + }, - /** - * Converts `t` to a Date object from a Unix timestamp unless it's not a - * number. - */ - fromUnixTime: function (t) { - return typeof t === "number" ? new Date(t * 1000) : t; - }, + /** + * Converts `t` to a Date object from a Unix timestamp unless it's not a + * number. + */ + fromUnixTime: function (t) { + return typeof t === 'number' ? new Date(t * 1000) : t; + }, - /** - * Converts `str` to a Date object from a Date string (or null). - * Returns null if str is falsy. - */ - fromDateString: function (str) { - return str ? (str instanceof Date ? str : new Date(str)) : null; - }, + /** + * Converts `str` to a Date object from a Date string (or null). + * Returns null if str is falsy. + */ + fromDateString: function (str) { + return str ? (str instanceof Date ? str : new Date(str)) : null; + }, - /** - * Provide a function that returns an array of values for a given repo. - * The second argument should be all arguments after the repository, since the - * repository argument will be dealt with by this function. - * - * The function should take a repository name, and return an array of values. - */ - forEachRepo: function (singleRepoLambda, args) { - // default value - args = args || []; - args.unshift(null); - var allRepoMap = function (currentRepo) { - args[0] = currentRepo.name; - return singleRepoLambda.apply(this, args); - }; - return Promise.all(config.repos.map(allRepoMap)).then(function (repoItems) { - return _.flatten(repoItems, /* shallow */ true); - }); - }, + /** + * Provide a function that returns an array of values for a given repo. + * The second argument should be all arguments after the repository, since the + * repository argument will be dealt with by this function. + * + * The function should take a repository name, and return an array of values. + */ + forEachRepo: function (singleRepoLambda, args) { + // default value + args = args || []; + args.unshift(null); + var allRepoMap = function (currentRepo) { + args[0] = currentRepo.name; + return singleRepoLambda.apply(this, args); + }; + return Promise.all(config.repos.map(allRepoMap)).then(function (repoItems) { + return _.flatten(repoItems, /* shallow */ true); + }); + }, - /** - * Statuses and checks have slightly different status names. Let's map the - * check values to match the status values. - */ - mapCheckToStatus: function (status) { - switch (status) { - case "in_progress": - case "queued": - return "pending"; - case "success": - case "skipped": - return "success"; - case "failure": - case "cancelled": - return "failure"; - default: - return "error"; - } - }, + /** + * Statuses and checks have slightly different status names. Let's map the + * check values to match the status values. + */ + mapCheckToStatus: function (status) { + switch (status) { + case 'in_progress': + case 'queued': + return 'pending'; + case 'success': + case 'skipped': + return 'success'; + case 'failure': + case 'cancelled': + case 'timed_out': + case 'startup_failure': + return 'failure'; + default: + return 'error'; + } + }, };