Skip to content

Forward-port release/17.1.0 security fixes to main (#4825, #4822, #4827) #8851

Forward-port release/17.1.0 security fixes to main (#4825, #4822, #4827)

Forward-port release/17.1.0 security fixes to main (#4825, #4822, #4827) #8851

Workflow file for this run

name: CI Commands
on:
issue_comment:
types: [created]
concurrency:
# GitHub keeps at most one pending run per concurrency group. Scope the group
# to the triggering comment so queued +ci-* comments are all handled.
group: ci-command-${{ github.event.issue.number }}-${{ github.event.comment.id }}
cancel-in-progress: false
jobs:
handle-ci-command:
# `contains` is case-insensitive, matching the command parser below.
if: |
github.event.issue.pull_request &&
github.event.comment.user.type != 'Bot' &&
contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association) &&
contains(github.event.comment.body, '+ci-')
runs-on: ubuntu-22.04
permissions:
actions: write
contents: read
issues: write
pull-requests: write
steps:
- name: Handle +ci command
uses: actions/github-script@v9
with:
script: |
const owner = context.repo.owner;
const repo = context.repo.repo;
const issueNumber = context.issue.number;
const commentId = context.payload.comment.id;
const commentCreatedAt = context.payload.comment.created_at;
const actor = context.actor;
const repoFullName = `${owner}/${repo}`;
const hostedCiLabel = 'ready-for-hosted-ci';
const forceFullHostedCiLabel = 'force-full-hosted-ci';
const newerCommandVisibilityDelayMs = 5000;
const ciCommandSerializationVisibilityDelayMs = 5000;
const ciCommandSerializationPollDelayMs = 5000;
const ciCommandSerializationMaxAttempts = 12;
const ciCommandSerializationLookbackMs = 24 * 60 * 60 * 1000;
const dispatchRunVisibilityGraceMs = 2 * 60 * 1000;
const dispatchRunValidationPollDelayMs = 2000;
const dispatchRunValidationMaxAttempts = 3;
const dispatchResultCommentRetryDelayMs = 2000;
const dispatchResultCommentMaxAttempts = 3;
const validCommands = [
'+ci-run-hosted',
'+ci-force-full',
'+ci-stop-hosted',
'+ci-stop-full',
'+ci-skip-hosted',
'+ci-status',
'+ci-help'
];
const trustedCommandAssociations = new Set(['OWNER', 'MEMBER', 'COLLABORATOR']);
const permissionByLogin = new Map();
const releaseBranchPrefixes = ['release/', 'releases/', 'release-'];
const activeWorkflowStatuses = new Set(['queued', 'in_progress', 'pending', 'requested', 'waiting']);
function isSameRepositoryPullRequest(pr) {
return pr.head.repo?.full_name === repoFullName;
}
function samePullRequestSnapshot(left, right) {
return left.state === right.state &&
left.merged === right.merged &&
left.head.sha === right.head.sha &&
left.head.ref === right.head.ref &&
left.head.repo?.full_name === right.head.repo?.full_name &&
left.base.ref === right.base.ref &&
left.base.sha === right.base.sha;
}
function isReleaseTargetPullRequest(pr) {
return releaseBranchPrefixes.some((prefix) => pr.base.ref.startsWith(prefix));
}
function hasAutomaticReleaseTargetCoverage(pr) {
return isSameRepositoryPullRequest(pr) &&
isReleaseTargetPullRequest(pr) &&
pr.user?.login !== 'dependabot[bot]';
}
function hostedWorkflows(pr, inputs = {}) {
return [
['Lint JS and Ruby', 'lint-js-and-ruby.yml', inputs],
['JS unit tests for Renderer package', 'package-js-tests.yml', inputs],
['Rspec test for gem', 'gem-tests.yml', inputs],
['Integration Tests', 'integration-tests.yml', inputs],
['Assets Precompile Check', 'precompile-check.yml', inputs],
['Generator tests', 'examples.yml', inputs],
['Playwright E2E Tests', 'playwright.yml', inputs],
...(pr.user?.login === 'dependabot[bot]' ? [] : [
['React on Rails Pro - Integration Tests', 'pro-integration-tests.yml', inputs],
['React on Rails Pro - Package Tests', 'pro-test-package-and-gem.yml', inputs]
])
];
}
function hostedWorkflowFiles(pr) {
return hostedWorkflows(pr).map(([, workflowFile]) => workflowFile);
}
function workflowFileForRun(run) {
return (run.path || '').split('@')[0].split('/').at(-1) || '';
}
function observedRunState(runs) {
if (runs.some((run) => run.status === 'completed' && run.conclusion === 'success')) {
return 'successful';
}
if (runs.some((run) => activeWorkflowStatuses.has(run.status))) {
return 'pending';
}
return runs.length > 0 ? 'failed' : 'missing';
}
function parseCoverageProof(comment, pr) {
if (comment.user?.login !== 'github-actions[bot]' || comment.user?.type !== 'Bot') {
return null;
}
const match = (comment.body || '').match(
/(?:^|[\r\n])<!-- hosted-ci-coverage:v1 (\{[^\r\n]*\}) -->(?=[\r\n]|$)/
);
if (!match) {
return null;
}
try {
const proof = JSON.parse(match[1]);
if (proof.head_sha !== pr.head.sha ||
proof.pull_request_number !== issueNumber ||
proof.base_ref !== pr.base.ref ||
proof.base_sha !== pr.base.sha ||
!['optimized', 'force-full'].includes(proof.requested_mode) ||
!Array.isArray(proof.workflows) ||
!proof.run_ids ||
!proof.workflows.every((workflowFile) =>
Number.isInteger(proof.run_ids[workflowFile]) && proof.run_ids[workflowFile] > 0
) ||
(proof.workflow_modes !== undefined &&
(typeof proof.workflow_modes !== 'object' ||
!proof.workflows.every((workflowFile) =>
['optimized', 'force-full'].includes(proof.workflow_modes[workflowFile])
))) ||
Number.isNaN(Date.parse(proof.requested_at || ''))) {
return null;
}
return { ...proof, proof_comment_id: comment.id };
} catch (error) {
core.warning(`Ignoring malformed hosted-CI coverage proof: ${error.message || error}`);
return null;
}
}
function parseDispatchUncertainty(comment, pr) {
if (comment.user?.login !== 'github-actions[bot]' || comment.user?.type !== 'Bot') {
return null;
}
const match = (comment.body || '').match(
/(?:^|[\r\n])<!-- hosted-ci-coverage:v1 (\{[^\r\n]*\}) -->(?=[\r\n]|$)/
);
if (!match) {
return null;
}
try {
const marker = JSON.parse(match[1]);
if (marker.head_sha !== pr.head.sha ||
marker.pull_request_number !== issueNumber ||
marker.base_ref !== pr.base.ref ||
marker.base_sha !== pr.base.sha ||
!['optimized', 'force-full'].includes(marker.requested_mode) ||
marker.coverage_status !== 'UNKNOWN' ||
marker.dispatch_uncertain !== true ||
!Array.isArray(marker.uncertain_workflows) ||
marker.uncertain_workflows.length === 0 ||
Number.isNaN(Date.parse(marker.requested_at || ''))) {
return null;
}
return marker;
} catch (error) {
core.warning(`Ignoring malformed hosted-CI dispatch uncertainty: ${error.message || error}`);
return null;
}
}
function latestCoverageProof(proofs, workflowFile, requestedMode) {
return proofs
.filter((proof) =>
proof.workflows.includes(workflowFile) &&
(proof.workflow_modes?.[workflowFile] || proof.requested_mode) === requestedMode
)
.sort((left, right) =>
Date.parse(right.requested_at) - Date.parse(left.requested_at) ||
right.proof_comment_id - left.proof_comment_id
)[0] || null;
}
function observedProofRunState(runs, proof, workflowFile) {
if (!proof) {
return 'missing';
}
const exactRunId = proof.run_ids[workflowFile];
const exactRun = runs.find((run) =>
run.event === 'workflow_dispatch' && run.id === exactRunId
);
if (exactRun) {
return observedRunState([exactRun]);
}
const proofAgeMs = Math.max(0, Date.now() - Date.parse(proof.requested_at));
return proofAgeMs <= dispatchRunVisibilityGraceMs
? 'pending'
: 'missing';
}
function coverageMarker(payload) {
return `<!-- hosted-ci-coverage:v1 ${JSON.stringify(payload)} -->`;
}
function coverageProofMarker({
baseRef,
baseSha,
dispatched,
headSha,
observed,
requestedMode,
reused,
runIds,
workflowModes,
workflows
}) {
return coverageMarker({
head_sha: headSha,
pull_request_number: issueNumber,
base_ref: baseRef,
base_sha: baseSha,
requested_mode: requestedMode,
requested_at: new Date(Date.now()).toISOString(),
workflows,
run_ids: runIds,
workflow_modes: workflowModes,
reused,
observed,
dispatched
});
}
async function observeExactHeadRuns(pr) {
try {
const [runs, comments] = await Promise.all([
github.paginate(github.rest.actions.listWorkflowRunsForRepo, {
owner,
repo,
head_sha: pr.head.sha,
per_page: 100
}),
github.paginate(github.rest.issues.listComments, {
owner,
repo,
issue_number: issueNumber,
per_page: 100,
sort: 'created',
direction: 'asc'
})
]);
const exactHeadRuns = runs.filter((run) => run.head_sha === pr.head.sha);
const dispatchUncertainties = comments
.map((comment) => parseDispatchUncertainty(comment, pr))
.filter(Boolean);
if (dispatchUncertainties.length > 0) {
core.warning(
'Exact-head hosted-CI coverage is UNKNOWN because a prior dispatch could not be correlated.'
);
return { status: 'UNKNOWN', workflows: [] };
}
const proofs = comments
.map((comment) => parseCoverageProof(comment, pr))
.filter(Boolean);
const automaticReleaseTarget = hasAutomaticReleaseTargetCoverage(pr);
const workflows = hostedWorkflowFiles(pr).map((workflowFile) => {
const workflowRuns = exactHeadRuns.filter((run) => workflowFileForRun(run) === workflowFile);
// Non-release pull_request runs can be successful selector-only shells. Only
// workflow_dispatch runs, or explicitly trusted release-target runs below,
// prove that the hosted jobs actually ran.
const forceFullProof = latestCoverageProof(proofs, workflowFile, 'force-full');
const forceFullState = observedProofRunState(
workflowRuns,
forceFullProof,
workflowFile
);
const optimizedProof = latestCoverageProof(proofs, workflowFile, 'optimized');
const optimizedProofState = observedProofRunState(
workflowRuns,
optimizedProof,
workflowFile
);
const automaticReleaseRuns = automaticReleaseTarget
? workflowRuns.filter((run) =>
run.event === 'pull_request' &&
(run.pull_requests || []).some((associatedPullRequest) =>
associatedPullRequest.number === issueNumber &&
associatedPullRequest.base?.ref === pr.base.ref &&
associatedPullRequest.base?.sha === pr.base.sha
)
)
: [];
const releaseState = observedRunState(automaticReleaseRuns);
if (['pending', 'successful'].includes(forceFullState)) {
return { file: workflowFile, mode: 'force-full', state: forceFullState, force_full_state: forceFullState };
}
if (['pending', 'successful'].includes(releaseState)) {
return { file: workflowFile, mode: 'release-full', state: releaseState, force_full_state: forceFullState };
}
return {
file: workflowFile,
mode: optimizedProofState === 'missing' ? 'missing' : 'optimized',
state: optimizedProofState,
force_full_state: forceFullState
};
});
return { status: 'known', baseRef: pr.base.ref, baseSha: pr.base.sha, workflows };
} catch (error) {
core.warning(`Unable to read exact-head workflow runs: ${error.message || error}`);
return { status: 'UNKNOWN', workflows: [] };
}
}
function commandText(commentBody) {
return commentBody
.replace(/```[\s\S]*?```/g, '')
.replace(/~~~[\s\S]*?~~~/g, '')
.split('\n')
.filter((line) => !/^\s*>/.test(line))
.join('\n');
}
function parseCommand(commentBody) {
const match = commandText(commentBody)
.match(/(?:^|\n)\s*(\+ci-[a-z0-9][a-z0-9-]*)(?:[ \t]+([^\n]*))?/i);
if (!match) {
return null;
}
return {
command: match[1].toLowerCase(),
args: (match[2] || '').trim()
};
}
async function postComment(body) {
await github.rest.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body
});
}
async function updateDispatchResultComment(commentId, body) {
for (let attempt = 1; attempt <= dispatchResultCommentMaxAttempts; attempt += 1) {
try {
await github.rest.issues.updateComment({
owner,
repo,
comment_id: commentId,
body
});
return true;
} catch (error) {
core.warning(
`Unable to persist hosted-CI dispatch proof (attempt ${attempt}/` +
`${dispatchResultCommentMaxAttempts}): ${error.message || error}`
);
if (attempt < dispatchResultCommentMaxAttempts) {
await sleep(dispatchResultCommentRetryDelayMs);
}
}
}
return false;
}
async function createDispatchResultComment(body) {
for (let attempt = 1; attempt <= dispatchResultCommentMaxAttempts; attempt += 1) {
try {
await postComment(body);
return true;
} catch (error) {
core.warning(
`Unable to persist hosted-CI result (attempt ${attempt}/` +
`${dispatchResultCommentMaxAttempts}): ${error.message || error}`
);
if (attempt < dispatchResultCommentMaxAttempts) {
await sleep(dispatchResultCommentRetryDelayMs);
}
}
}
return false;
}
async function reconcileDispatchAnchor(pr) {
for (let attempt = 1; attempt <= dispatchResultCommentMaxAttempts; attempt += 1) {
try {
const comments = await github.paginate(github.rest.issues.listComments, {
owner,
repo,
issue_number: issueNumber,
per_page: 100
});
const anchor = comments.find((comment) => {
const marker = parseDispatchUncertainty(comment, pr);
return marker?.source_comment_id === commentId &&
marker.source_run_id === context.runId &&
marker.requested_at === commentCreatedAt;
});
if (Number.isInteger(anchor?.id) && anchor.id > 0) {
return anchor.id;
}
} catch (error) {
core.warning(
`Unable to reconcile hosted-CI dispatch anchor (attempt ${attempt}/` +
`${dispatchResultCommentMaxAttempts}): ${error.message || error}`
);
}
if (attempt < dispatchResultCommentMaxAttempts) {
await sleep(dispatchResultCommentRetryDelayMs);
}
}
return null;
}
async function addReaction(content) {
try {
await github.rest.reactions.createForIssueComment({
owner,
repo,
comment_id: commentId,
content
});
} catch (error) {
const status = error.status || 'unknown';
const message = error.message || String(error);
const details = `${status} ${message}`;
core.warning(
`Reaction ${content} failed on comment ${commentId}: ` +
details
);
}
}
async function hasWriteAccessFor(username) {
if (!username) {
return false;
}
if (permissionByLogin.has(username)) {
return permissionByLogin.get(username);
}
let permission;
try {
({ data: permission } = await github.rest.repos.getCollaboratorPermissionLevel({
owner,
repo,
username
}));
} catch (error) {
if ([403, 404].includes(error.status)) {
console.log(`Unable to read permission for ${username}: ${error.status}`);
permissionByLogin.set(username, false);
return false;
}
throw error;
}
const hasWritePermission = ['admin', 'write'].includes(permission.permission);
permissionByLogin.set(username, hasWritePermission);
return hasWritePermission;
}
async function hasWriteAccess() {
return hasWriteAccessFor(actor);
}
async function getPullRequest() {
const { data: pr } = await github.rest.pulls.get({
owner,
repo,
pull_number: issueNumber
});
return pr;
}
async function ensureLabel(name, description, color) {
try {
await github.rest.issues.createLabel({
owner,
repo,
name,
description,
color
});
} catch (error) {
if (error.status !== 422) {
throw error;
}
}
}
async function addHostedCiLabels(forceFull) {
await ensureLabel(
hostedCiLabel,
'Run optimized hosted GitHub CI for this PR',
'0E8A16'
);
const labels = [hostedCiLabel];
if (forceFull) {
await ensureLabel(
forceFullHostedCiLabel,
'Bypass optimized hosted CI selection and run all hosted suites',
'B60205'
);
labels.push(forceFullHostedCiLabel);
}
await github.rest.issues.addLabels({
owner,
repo,
issue_number: issueNumber,
labels
});
}
async function removeLabels(labelNames) {
let removed = false;
for (const labelName of labelNames) {
try {
await github.rest.issues.removeLabel({
owner,
repo,
issue_number: issueNumber,
name: labelName
});
removed = true;
} catch (error) {
if (error.status !== 404) {
throw error;
}
}
}
return removed;
}
async function removeHostedCiLabels() {
return removeLabels([hostedCiLabel, forceFullHostedCiLabel]);
}
async function removeForceFullHostedCiLabel() {
return removeLabels([forceFullHostedCiLabel]);
}
async function listPullRequestLabels() {
const labels = await github.paginate(github.rest.issues.listLabelsOnIssue, {
owner,
repo,
issue_number: issueNumber,
per_page: 100
});
return labels.map((label) => label.name);
}
async function listPullRequestFiles() {
const files = await github.paginate(github.rest.pulls.listFiles, {
owner,
repo,
pull_number: issueNumber,
per_page: 100
});
return files;
}
function isDocsOnlyPath(filename) {
return filename.endsWith('.md') ||
filename.endsWith('.mdx') ||
filename.endsWith('.markdown') ||
filename.endsWith('.rst') ||
filename.endsWith('.txt') ||
filename === '.lychee.toml' ||
filename.startsWith('docs/') ||
filename.startsWith('internal/') ||
filename.startsWith('.github/ISSUE_TEMPLATE/') ||
filename.startsWith('.claude/') ||
filename.startsWith('.agents/') ||
filename.startsWith('.cursor/');
}
function isDocsOnly(files) {
return files.length > 0 &&
files.every((file) => isDocsOnlyPath(file.filename));
}
async function findCurrentWaiver(sha) {
const marker = `<!-- ci-skip-hosted:${sha} -->`;
for await (const page of github.paginate.iterator(github.rest.issues.listComments, {
owner,
repo,
issue_number: issueNumber,
per_page: 100,
sort: 'created',
direction: 'desc'
})) {
const found = page.data.find((comment) =>
comment.user?.login === 'github-actions[bot]' &&
comment.user?.type === 'Bot' &&
(comment.body || '').startsWith(marker)
);
if (found) {
return found;
}
}
return null;
}
function oneLine(value) {
return value.replace(/[\r\n\u2028\u2029]+/g, ' ').replace(/`+/g, "'").trim();
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function waitForOlderCiCommands() {
if (!Number.isInteger(context.runId) || context.runId <= 0) {
return { status: 'UNKNOWN', reason: 'Current CI Commands workflow run ID is unavailable.' };
}
if (context.runAttempt !== 1) {
return {
status: 'UNKNOWN',
reason: 'Manual reruns of CI Commands cannot safely dispatch hosted CI; post a new command instead.'
};
}
const createdLowerBound =
`>=${new Date(Date.now() - ciCommandSerializationLookbackMs).toISOString()}`;
await sleep(ciCommandSerializationVisibilityDelayMs);
for (let attempt = 1; attempt <= ciCommandSerializationMaxAttempts; attempt += 1) {
let runs;
try {
runs = await github.paginate(github.rest.actions.listWorkflowRuns, {
owner,
repo,
workflow_id: 'ci-commands.yml',
event: 'issue_comment',
created: createdLowerBound,
per_page: 100
});
} catch (error) {
core.warning(`Unable to serialize CI Commands workflow runs: ${error.message || error}`);
return { status: 'UNKNOWN', reason: 'Unable to read older CI Commands workflow runs.' };
}
const olderActiveRuns = runs.filter((run) =>
run.id < context.runId &&
activeWorkflowStatuses.has(run.status)
);
if (olderActiveRuns.length === 0) {
return { status: 'known' };
}
if (attempt < ciCommandSerializationMaxAttempts) {
await sleep(ciCommandSerializationPollDelayMs);
}
}
return {
status: 'UNKNOWN',
reason: 'An older CI Commands workflow did not finish within the bounded wait.'
};
}
function newerCommandScanSince() {
const createdAt = Date.parse(commentCreatedAt);
if (Number.isNaN(createdAt)) {
return undefined;
}
return new Date(Math.max(0, createdAt - 60000)).toISOString();
}
async function newerCiCommands(commandNames, { waitForVisibility = true } = {}) {
const commandNameSet = new Set(commandNames);
const newerCommands = new Set();
const since = newerCommandScanSince();
// Give GitHub's comments API a short visibility window so a later
// stop/skip command wins over an older run-hosted command that is
// still dispatching workflows.
if (waitForVisibility) {
await sleep(newerCommandVisibilityDelayMs);
}
for await (const page of github.paginate.iterator(github.rest.issues.listComments, {
owner,
repo,
issue_number: issueNumber,
per_page: 100,
sort: 'created',
direction: 'asc',
...(since ? { since } : {})
})) {
for (const comment of page.data) {
const commenterLogin = comment.user?.login;
if (comment.id <= commentId ||
comment.user?.type === 'Bot' ||
!commenterLogin ||
!trustedCommandAssociations.has(comment.author_association) ||
!(await hasWriteAccessFor(commenterLogin))) {
continue;
}
const parsed = parseCommand(comment.body || '');
if (
parsed &&
validCommands.includes(parsed.command) &&
commandNameSet.has(parsed.command)
) {
newerCommands.add(parsed.command);
if (newerCommands.size === commandNameSet.size) {
return newerCommands;
}
}
}
}
return newerCommands;
}
async function hasNewerCiCommand(commandNames) {
return (await newerCiCommands(commandNames)).size > 0;
}
async function hasNewerCiCommandBeforeMutation(commandNames) {
return (await newerCiCommands(commandNames, { waitForVisibility: false })).size > 0;
}
function helpBody() {
return [
'## CI Commands',
'',
'Use these commands at the start of a PR comment line:',
'Post one CI command per comment; if multiple commands are present, only the first is handled.',
'',
'- `+ci-run-hosted` - dispatch optimized hosted CI and add `ready-for-hosted-ci` for future commits',
'- `+ci-force-full` - dispatch hosted CI with optimized selection bypassed and add `force-full-hosted-ci`',
'- `+ci-stop-hosted` - remove hosted CI labels so future commits return to the required gate only',
'- `+ci-stop-full` - remove only the force-full hosted override',
'- `+ci-skip-hosted [reason]` - record a SHA-bound hosted CI waiver',
'- `+ci-status` - summarize the PR CI policy state',
'- `+ci-help` - show this help',
'',
'Arguments are optional free text after the command. For example:',
'',
'```',
'+ci-skip-hosted docs-only change; markdown checks are enough',
'```'
].join('\n');
}
async function triggerHostedCi(pr, { forceFull }) {
if (pr.state !== 'open') {
await addReaction('confused');
await postComment([
'Unable to run hosted CI from this command because the PR is not open.',
'',
`Current PR state: \`${pr.state || 'unknown'}\`; merged: \`${pr.merged ? 'yes' : 'no'}\`.`,
'Hosted CI dispatch requires the live PR head ref before merge or close. After that point, branch-ref hosted-CI evidence is degraded/invalid; use the existing current-head checks, the merge commit, or a verification PR if more hosted evidence is required.'
].join('\n'));
return;
}
const headRepoFullName = pr.head.repo?.full_name;
if (!headRepoFullName) {
await addReaction('confused');
await postComment([
'Unable to run hosted CI from this command because the PR head repository is unavailable.',
'',
'The repository may have been deleted or made private. A maintainer should push the branch to this repository or trigger the required workflows manually.'
].join('\n'));
return;
}
if (headRepoFullName !== repoFullName) {
await addReaction('confused');
await postComment([
'Unable to run hosted CI from this command because the PR branch is from a fork.',
'',
'A maintainer should push the branch to this repository or trigger the required workflows manually.'
].join('\n'));
return;
}
const serialization = await waitForOlderCiCommands();
if (serialization.status === 'UNKNOWN') {
await addReaction('confused');
await postComment([
'## Hosted CI Command Serialization UNKNOWN',
coverageMarker({
head_sha: pr.head.sha,
pull_request_number: issueNumber,
base_ref: pr.base.ref,
base_sha: pr.base.sha,
requested_mode: forceFull ? 'force-full' : 'optimized',
coverage_status: 'UNKNOWN',
observed: [],
dispatched: []
}),
'',
serialization.reason,
'No workflows were dispatched because an older CI command may still mutate hosted coverage.'
].join('\n'));
core.setFailed('Older CI command state is UNKNOWN; dispatch stopped.');
return;
}
const inputs = {
pull_request_base_ref: pr.base.ref,
pull_request_base_sha: pr.base.sha,
...(forceFull ? { force_full_hosted: 'true' } : {})
};
const baseContextInputNames = new Set(['pull_request_base_ref', 'pull_request_base_sha']);
const workflows = hostedWorkflows(pr, inputs);
const coverage = await observeExactHeadRuns(pr);
if (coverage.status === 'UNKNOWN') {
await addReaction('confused');
await postComment([
'## Hosted CI Coverage UNKNOWN',
coverageMarker({
head_sha: pr.head.sha,
requested_mode: forceFull ? 'force-full' : 'optimized',
coverage_status: 'UNKNOWN',
observed: [],
dispatched: []
}),
'',
`Unable to prove which hosted workflows are missing for \`${pr.head.sha.slice(0, 12)}\`.`,
'No workflows were dispatched because exact-head coverage could not be read safely.'
].join('\n'));
core.setFailed('Exact-head hosted CI coverage is UNKNOWN; dispatch stopped.');
return;
}
const coverageByFile = new Map(
coverage.workflows.map((workflow) => [workflow.file, workflow])
);
const coveredWorkflows = workflows.filter(([, workflowFile]) => {
const observed = coverageByFile.get(workflowFile);
const state = forceFull ? observed?.force_full_state : observed?.state;
return ['pending', 'successful'].includes(state);
});
const workflowsToDispatch = workflows.filter((workflow) => !coveredWorkflows.includes(workflow));
let currentPr;
try {
currentPr = await getPullRequest();
} catch (error) {
core.warning(`Unable to re-read the pull request before hosted-CI dispatch: ${error.message || error}`);
}
if (!currentPr || !samePullRequestSnapshot(pr, currentPr)) {
await addReaction('confused');
await postComment([
'## Pull Request Changed - Hosted CI Stopped',
coverageMarker({
head_sha: pr.head.sha,
pull_request_number: issueNumber,
base_ref: pr.base.ref,
base_sha: pr.base.sha,
requested_mode: forceFull ? 'force-full' : 'optimized',
requested_at: commentCreatedAt,
coverage_status: 'UNKNOWN',
observed: coverage.workflows,
dispatched: []
}),
'',
'The pull request head, base, repository, or state changed after hosted-CI coverage was planned.',
'No workflows or labels were requested. Retry the command against the current pull request state.'
].join('\n'));
core.setFailed('Pull request changed during hosted CI planning; dispatch stopped.');
return;
}
let dispatchResultCommentId;
if (workflowsToDispatch.length > 0) {
const plannedWorkflowFiles = workflowsToDispatch.map(([, workflowFile]) => workflowFile);
const dispatchAnchorBody = [
coverageMarker({
head_sha: pr.head.sha,
pull_request_number: issueNumber,
base_ref: pr.base.ref,
base_sha: pr.base.sha,
requested_mode: forceFull ? 'force-full' : 'optimized',
requested_at: commentCreatedAt,
source_comment_id: commentId,
source_run_id: context.runId,
coverage_status: 'UNKNOWN',
dispatch_uncertain: true,
uncertain_workflows: plannedWorkflowFiles,
workflows: [],
run_ids: {},
reused: coveredWorkflows.map(([, workflowFile]) => workflowFile),
observed: coverage.workflows,
dispatched: []
}),
'## Hosted CI Dispatch In Progress',
'',
'This head-bound UNKNOWN marker is replaced with exact-run proof after dispatch validation.',
'If the final update cannot be persisted, later same-head commands remain fail-closed.'
].join('\n');
try {
const { data: anchorComment } = await github.rest.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body: dispatchAnchorBody
});
dispatchResultCommentId = anchorComment?.id;
} catch (error) {
core.warning(`Unable to create hosted-CI dispatch anchor: ${error.message || error}`);
}
if (!Number.isInteger(dispatchResultCommentId) || dispatchResultCommentId <= 0) {
dispatchResultCommentId = await reconcileDispatchAnchor(pr);
}
if (!Number.isInteger(dispatchResultCommentId) || dispatchResultCommentId <= 0) {
await addReaction('confused');
core.setFailed('Unable to persist hosted-CI dispatch anchor; no workflows were dispatched.');
return;
}
let anchoredPr;
try {
anchoredPr = await getPullRequest();
} catch (error) {
core.warning(
`Unable to re-read the pull request after hosted-CI anchoring: ${error.message || error}`
);
}
if (!anchoredPr || !samePullRequestSnapshot(pr, anchoredPr)) {
await addReaction('confused');
const stoppedBody = [
'## Pull Request Changed After Dispatch Anchoring',
coverageMarker({
head_sha: pr.head.sha,
pull_request_number: issueNumber,
base_ref: pr.base.ref,
base_sha: pr.base.sha,
requested_mode: forceFull ? 'force-full' : 'optimized',
requested_at: commentCreatedAt,
coverage_status: 'known',
observed: coverage.workflows,
dispatched: []
}),
'',
'The pull request head, base, repository, or state changed after the dispatch anchor was persisted.',
'No workflow or hosted-CI label was requested. Post a new command for the current pull request state.'
].join('\n');
const stoppedResultPersisted = await updateDispatchResultComment(
dispatchResultCommentId,
stoppedBody
);
if (!stoppedResultPersisted) {
core.warning(
'Unable to replace the pre-dispatch UNKNOWN anchor after the pull request changed.'
);
}
core.setFailed('Pull request changed after hosted CI anchoring; dispatch stopped.');
return;
}
}
function legacyCompatibleInputs(workflowInputs) {
const legacyInputs = Object.fromEntries(
Object.entries(workflowInputs).filter(([name]) => !baseContextInputNames.has(name))
);
if (pr.base.ref !== 'main') {
legacyInputs.force_full_hosted = 'true';
}
return legacyInputs;
}
function isUnexpectedInputError(error) {
const message = error?.message || '';
return error?.status === 422 &&
/unexpected inputs?|provided inputs?/.test(message.toLowerCase()) &&
[...baseContextInputNames].some((name) => message.includes(name));
}
function dispatchUncertainty(message, cause) {
const error = new Error(message, cause ? { cause } : undefined);
error.dispatchUncertain = true;
return error;
}
function classifyDispatchFailure(error, unknownMessage) {
if (Number.isInteger(error?.status) && error.status >= 400 && error.status <= 499) {
return error;
}
return dispatchUncertainty(unknownMessage, error);
}
function isRetryableWorkflowRunReadError(error) {
return error?.status === 404 || (error?.status >= 500 && error?.status <= 599);
}
async function createWorkflowDispatchWithFallback(workflowName, workflowFile, workflowInputs) {
const dispatchOptions = (inputs) => ({
owner,
repo,
workflow_id: workflowFile,
ref: pr.head.ref,
inputs,
headers: { 'x-github-api-version': '2026-03-10' }
});
const dispatchedWorkflow = async (response, usedLegacyInputsFallback) => {
const runId = response?.data?.workflow_run_id;
if (response?.status !== 200 || !Number.isInteger(runId) || runId <= 0) {
throw dispatchUncertainty(
`${workflowFile} dispatch did not return an exact workflow run ID in a 200 response.`
);
}
let run;
for (let attempt = 1; attempt <= dispatchRunValidationMaxAttempts; attempt += 1) {
try {
({ data: run } = await github.rest.actions.getWorkflowRun({
owner,
repo,
run_id: runId,
headers: { 'x-github-api-version': '2026-03-10' }
}));
break;
} catch (error) {
if (isRetryableWorkflowRunReadError(error) &&
attempt < dispatchRunValidationMaxAttempts) {
await sleep(dispatchRunValidationPollDelayMs);
continue;
}
throw dispatchUncertainty(
`${workflowFile} returned run ${runId}, but the exact workflow run could not be validated.`,
error
);
}
}
const expectedPath = `.github/workflows/${workflowFile}`;
const runPath = (run?.path || '').split('@')[0];
if (run?.id !== runId ||
run?.event !== 'workflow_dispatch' ||
run?.head_sha !== pr.head.sha ||
runPath !== expectedPath) {
throw dispatchUncertainty(
`${workflowFile} returned run did not match the expected workflow and head.`
);
}
return {
effectiveMode: usedLegacyInputsFallback && pr.base.ref !== 'main'
? 'force-full'
: forceFull ? 'force-full' : 'optimized',
file: workflowFile,
name: workflowName,
runId,
usedLegacyInputsFallback
};
};
let response;
let usedLegacyInputsFallback = false;
try {
response = await github.rest.actions.createWorkflowDispatch(
dispatchOptions(workflowInputs)
);
} catch (error) {
if (!isUnexpectedInputError(error)) {
throw classifyDispatchFailure(
error,
`${workflowFile} dispatch result is UNKNOWN: ${error.message || error}`
);
}
core.warning(
`${workflowFile} rejected PR base context inputs; retrying with legacy workflow_dispatch inputs.`
);
usedLegacyInputsFallback = true;
try {
response = await github.rest.actions.createWorkflowDispatch(
dispatchOptions(legacyCompatibleInputs(workflowInputs))
);
} catch (fallbackError) {
throw classifyDispatchFailure(
fallbackError,
`${workflowFile} legacy dispatch result is UNKNOWN: ${fallbackError.message || fallbackError}`
);
}
}
return dispatchedWorkflow(response, usedLegacyInputsFallback);
}
const dispatchResults = await Promise.allSettled(
workflowsToDispatch.map(([workflowName, workflowFile, inputs]) =>
createWorkflowDispatchWithFallback(workflowName, workflowFile, inputs)
)
);
const succeeded = [];
const failed = [];
for (const [index, result] of dispatchResults.entries()) {
if (result.status === 'fulfilled') {
succeeded.push(result.value);
} else {
failed.push({
workflow: workflowsToDispatch[index][0],
workflowFile: workflowsToDispatch[index][1],
error: result.reason?.message || String(result.reason),
dispatchUncertain: result.reason?.dispatchUncertain === true
});
}
}
let labelAdded = false;
let forceFullLabelAdded = false;
let labelMessageOverride = '';
if (failed.length === 0) {
const labelControlCommands = [
'+ci-stop-hosted',
'+ci-skip-hosted',
'+ci-stop-full'
];
try {
const newerCommands = await newerCiCommands(labelControlCommands);
if (newerCommands.has('+ci-stop-hosted') || newerCommands.has('+ci-skip-hosted')) {
labelMessageOverride =
'Skipped hosted CI labels because a newer stop/skip command was posted after this command.';
} else {
for (const command of await newerCiCommands(labelControlCommands, { waitForVisibility: false })) {
newerCommands.add(command);
}
if (newerCommands.has('+ci-stop-hosted') || newerCommands.has('+ci-skip-hosted')) {
labelMessageOverride =
'Skipped hosted CI labels because a newer stop/skip command was posted after this command.';
} else {
const shouldAddForceFullLabel = forceFull && !newerCommands.has('+ci-stop-full');
try {
await addHostedCiLabels(shouldAddForceFullLabel);
labelAdded = true;
forceFullLabelAdded = shouldAddForceFullLabel;
if (forceFull && !shouldAddForceFullLabel) {
labelMessageOverride =
'Added `ready-for-hosted-ci`, but skipped `force-full-hosted-ci` because a newer `+ci-stop-full` command was posted after this command.';
}
} catch (error) {
failed.push({
workflow: forceFull ? 'force-full hosted CI labels' : 'hosted CI label',
error: error.message
});
}
}
}
} catch (error) {
failed.push({
workflow: 'hosted CI newer-command check',
error: error.message || String(error)
});
labelMessageOverride =
'Skipped hosted CI labels because checking for newer stop/skip commands failed after workflows were triggered.';
}
}
await addReaction(failed.length > 0 ? 'confused' : succeeded.length > 0 ? 'rocket' : 'eyes');
const failedList = failed.length > 0
? `\n\nFailed:\n${failed.map((failure) => `- ${failure.workflow}: ${failure.error}`).join('\n')}`
: '';
const uncertainWorkflows = failed
.filter((failure) => failure.dispatchUncertain)
.map((failure) => failure.workflowFile);
const dispatchIsUncertain = uncertainWorkflows.length > 0;
const fallbackCount = succeeded.filter((workflow) => workflow.usedLegacyInputsFallback).length;
const fallbackMessage = fallbackCount > 0
? `Retried ${fallbackCount} workflow(s) without PR base context inputs because the PR branch has older workflow files.`
: '';
const dependabotTrustProof = pr.user?.login === 'dependabot[bot]' &&
succeeded.length === 0 && coveredWorkflows.length > 0 && failed.length === 0
? `Trusted dispatch proof retained: Triggered ${coveredWorkflows.length} workflow(s) for ` +
`\`${pr.head.sha.slice(0, 12)}\` by current-base prior requests.`
: '';
const labelMessage = labelMessageOverride || (labelAdded && forceFullLabelAdded
? 'Added `ready-for-hosted-ci` and `force-full-hosted-ci`, so future commits will bypass optimized hosted CI selection until `+ci-stop-full` is used.'
: labelAdded
? 'Added `ready-for-hosted-ci`, so future commits will keep running optimized hosted CI until `+ci-stop-hosted` is used.'
: succeeded.length > 0
? 'The workflows were triggered, but the hosted CI labels were not added. See the failure details below.'
: 'Hosted CI labels were not added because no workflows were triggered.');
const resultMarker = dispatchIsUncertain
? coverageMarker({
head_sha: pr.head.sha,
pull_request_number: issueNumber,
base_ref: pr.base.ref,
base_sha: pr.base.sha,
requested_mode: forceFull ? 'force-full' : 'optimized',
requested_at: commentCreatedAt,
coverage_status: 'UNKNOWN',
dispatch_uncertain: true,
uncertain_workflows: uncertainWorkflows,
workflows: [],
run_ids: {},
reused: [],
observed: coverage.workflows,
dispatched: []
})
: coverageProofMarker({
baseRef: pr.base.ref,
baseSha: pr.base.sha,
headSha: pr.head.sha,
requestedMode: forceFull ? 'force-full' : 'optimized',
workflows: succeeded.map((workflow) => workflow.file),
runIds: Object.fromEntries(
succeeded.map((workflow) => [workflow.file, workflow.runId])
),
workflowModes: Object.fromEntries(
succeeded.map((workflow) => [workflow.file, workflow.effectiveMode])
),
reused: coveredWorkflows.map(([, workflowFile]) => workflowFile),
observed: coverage.workflows,
dispatched: succeeded.map((workflow) => workflow.file)
});
const resultBody = [
dispatchIsUncertain
? '## Hosted CI Dispatch UNKNOWN'
: forceFull ? '## Force-Full Hosted CI Requested' : '## Hosted CI Requested',
resultMarker,
'',
dispatchIsUncertain
? `Dispatch outcome is UNKNOWN for ${uncertainWorkflows.length} workflow(s); no exact-head dispatch proof was recorded.`
: `Triggered ${succeeded.length} workflow(s) for \`${pr.head.sha.slice(0, 12)}\`.`,
`Skipped ${coveredWorkflows.length} workflow(s) with equivalent exact-head coverage.`,
...(dependabotTrustProof ? [dependabotTrustProof] : []),
forceFull
? 'Mode: force-full hosted CI (bypasses optimized change selection).'
: 'Mode: optimized hosted CI (path-selected by `script/ci-changes-detector`).',
...(fallbackMessage ? [fallbackMessage] : []),
labelMessage,
'',
'View progress in the Actions tab.',
failedList
].join('\n');
if (dispatchResultCommentId) {
const proofPersisted = await updateDispatchResultComment(
dispatchResultCommentId,
resultBody
);
if (!proofPersisted) {
core.setFailed('Unable to persist final hosted-CI dispatch proof.');
return;
}
} else {
const resultPersisted = await createDispatchResultComment(resultBody);
if (!resultPersisted) {
core.setFailed('Unable to persist final hosted-CI result.');
return;
}
}
if (failed.length > 0) {
core.setFailed(`Failed to trigger: ${failed.map((failure) => failure.workflow).join(', ')}`);
}
}
async function handleSkipHosted(pr, args) {
if (await hasNewerCiCommand(['+ci-run-hosted', '+ci-force-full'])) {
await postComment('Skipped hosted-CI waiver because a newer hosted-CI request command was posted after this command.');
return;
}
if (await hasNewerCiCommandBeforeMutation(['+ci-run-hosted', '+ci-force-full'])) {
await postComment('Skipped hosted-CI waiver because a newer hosted-CI request command was posted after this command.');
return;
}
const removedHostedCi = await removeHostedCiLabels();
const reason = oneLine(args) || 'not provided';
const shortSha = pr.head.sha.slice(0, 12);
await postComment([
`<!-- ci-skip-hosted:${pr.head.sha} -->`,
'## Hosted CI Waiver Recorded for This SHA',
'',
`@${actor} recorded a hosted-CI waiver for \`${shortSha}\` (audit record only - no workflow run was cancelled or blocked).`,
`Reason: \`${reason}\``,
'',
'This waiver is bound to the current head SHA and does not apply after another push.',
'The required fast gate still applies for this PR.',
...(removedHostedCi ? ['', 'Removed hosted CI labels from this PR.'] : [])
].join('\n'));
}
async function handleStopHosted() {
if (await hasNewerCiCommand(['+ci-run-hosted', '+ci-force-full'])) {
await postComment('Skipped hosted CI label removal because a newer hosted-CI request command was posted after this command.');
return;
}
if (await hasNewerCiCommandBeforeMutation(['+ci-run-hosted', '+ci-force-full'])) {
await postComment('Skipped hosted CI label removal because a newer hosted-CI request command was posted after this command.');
return;
}
const removed = await removeHostedCiLabels();
await postComment([
'## Hosted CI Mode Disabled',
'',
removed
? 'Removed hosted CI labels. Future commits will use only the required gate until hosted CI is requested again.'
: 'No hosted CI labels were present. Future commits are already using only the required gate.',
'',
'Use `+ci-run-hosted` to re-enable optimized hosted CI.'
].join('\n'));
}
async function handleStopFull() {
if (await hasNewerCiCommand(['+ci-force-full'])) {
await postComment('Skipped force-full label removal because a newer `+ci-force-full` command was posted after this command.');
return;
}
if (await hasNewerCiCommandBeforeMutation(['+ci-force-full'])) {
await postComment('Skipped force-full label removal because a newer `+ci-force-full` command was posted after this command.');
return;
}
const removed = await removeForceFullHostedCiLabel();
await postComment([
'## Force-Full Hosted CI Disabled',
'',
removed
? 'Removed `force-full-hosted-ci`. If `ready-for-hosted-ci` is still present, future commits keep running optimized hosted CI.'
: 'No force-full hosted CI label was present.',
'',
'Use `+ci-force-full` to re-enable the force-full hosted override.'
].join('\n'));
}
async function handleStatus(pr) {
const [labels, files, waiver, coverage] = await Promise.all([
listPullRequestLabels(),
listPullRequestFiles(),
findCurrentWaiver(pr.head.sha),
observeExactHeadRuns(pr)
]);
const docsOnly = isDocsOnly(files);
const hasReadyForHostedCi = labels.includes(hostedCiLabel);
const hasForceFullHostedCi = labels.includes(forceFullHostedCiLabel);
const automaticReleaseTarget = hasAutomaticReleaseTargetCoverage(pr);
const coverageCounts = coverage.workflows.reduce((counts, workflow) => {
counts[workflow.state] += 1;
return counts;
}, { successful: 0, pending: 0, failed: 0, missing: 0 });
const coverageModeCounts = coverage.workflows.reduce((counts, workflow) => {
counts[workflow.mode] = (counts[workflow.mode] || 0) + 1;
return counts;
}, {});
const coverageModeSummary = Object.entries(coverageModeCounts)
.map(([mode, count]) => `${mode}=${count}`)
.join(', ');
const observedCoverage = coverage.status === 'UNKNOWN'
? 'UNKNOWN'
: `modes[${coverageModeSummary}]; ` +
`successful=${coverageCounts.successful}, pending=${coverageCounts.pending}, ` +
`failed=${coverageCounts.failed}, missing=${coverageCounts.missing}`;
await postComment([
'## CI Status',
coverageMarker({
head_sha: pr.head.sha,
requested_mode: 'status',
automatic_release_target: automaticReleaseTarget,
coverage_status: coverage.status,
observed: coverage.workflows
}),
'',
`Head SHA: \`${pr.head.sha.slice(0, 12)}\``,
`Changed files: ${files.length}`,
`Docs-only heuristic (matches ci-changes-detector metadata paths): ${docsOnly ? 'yes' : 'no'}`,
`\`${hostedCiLabel}\` label: ${hasReadyForHostedCi ? 'present' : 'absent'}`,
`\`${forceFullHostedCiLabel}\` label: ${hasForceFullHostedCi ? 'present' : 'absent'}`,
`Current hosted-CI waiver: ${waiver ? 'present for this SHA' : 'not present for this SHA'}`,
`Automatic release-target hosted mode: ${automaticReleaseTarget ? 'active' : 'inactive'}`,
`Observed exact-head coverage: ${observedCoverage}`,
'',
automaticReleaseTarget
? 'Release-target policy already enables hosted CI for this same-repository PR.'
: hasForceFullHostedCi
? 'Force-full hosted CI is enabled for this PR.'
: hasReadyForHostedCi
? 'Optimized hosted CI is enabled for this PR.'
: 'Only the required gate is active unless hosted CI is requested.'
].join('\n'));
}
const parsed = parseCommand(context.payload.comment.body || '');
if (!parsed) {
return;
}
const userHasWriteAccess = await hasWriteAccess();
if (!validCommands.includes(parsed.command)) {
if (!userHasWriteAccess) {
return;
}
await addReaction('confused');
await postComment([
`Unknown CI command: \`${parsed.command}\`.`,
'',
helpBody()
].join('\n'));
return;
}
if (!userHasWriteAccess) {
await postComment(`@${actor} Sorry, only repository collaborators with write access can use CI commands.`);
core.warning(`User ${actor} does not have write access`);
return;
}
const pr = await getPullRequest();
switch (parsed.command) {
case '+ci-run-hosted':
await triggerHostedCi(pr, { forceFull: false });
break;
case '+ci-force-full':
await triggerHostedCi(pr, { forceFull: true });
break;
case '+ci-stop-hosted':
await addReaction('eyes');
await handleStopHosted();
break;
case '+ci-stop-full':
await addReaction('eyes');
await handleStopFull();
break;
case '+ci-skip-hosted':
await addReaction('eyes');
await handleSkipHosted(pr, parsed.args);
break;
case '+ci-status':
await addReaction('eyes');
await handleStatus(pr);
break;
case '+ci-help':
await addReaction('eyes');
await postComment(helpBody());
break;
}