Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/scripts/agents_pr_meta_keepalive.js
Original file line number Diff line number Diff line change
Expand Up @@ -755,6 +755,15 @@ async function detectKeepalive({ core, github, context, env = process.env }) {
return finalise();
}

if (sourceContext.noAutomation) {
outputs.reason = 'no-automation-source-context';
outputs.dispatch = 'false';
core.info(
`Keepalive dispatch skipped: PR source context opts out of automation (${formatSourceContextForLog(sourceContext)}).`,
);
return finalise();
}

if (!issueNumber) {
if (sourceContext.isValid && !sourceContext.requiresIssue) {
core.info(
Expand Down
30 changes: 27 additions & 3 deletions .github/scripts/agents_pr_meta_update_body.js
Original file line number Diff line number Diff line change
Expand Up @@ -1023,6 +1023,10 @@ function resolveExplicitNonIssueWorkflowSourceContext(pr = {}) {
};
}

function resolveNonIssueWorkflowSourceContextForBodySync(pr = {}, issueNumber = null) {
return issueNumber ? null : resolveExplicitNonIssueWorkflowSourceContext(pr);
}

async function resolveSourceContextRepairComment({
github,
owner,
Expand Down Expand Up @@ -1356,7 +1360,9 @@ async function run({github: rawGithub, context, core, inputs}) {
return;
}

const explicitNonIssueSourceContext = resolveExplicitNonIssueWorkflowSourceContext(pr);
const issueNumber = extractIssueNumberFromPull(pr);
const sourceContext = resolvePrSourceContext(pr);

Copilot AI Apr 27, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolvePrSourceContext(pr) now exposes noAutomation, but this workflow still proceeds with issue-sourced body sync even when the PR opts out of automation (e.g., "Do not automate" in the template or workflow:no-automation label). Add an early guard to skip PR body updates when sourceContext.noAutomation is true (and decide whether to still upsert/resolve the repair comment).

Suggested change
const sourceContext = resolvePrSourceContext(pr);
const sourceContext = resolvePrSourceContext(pr);
if (sourceContext && sourceContext.noAutomation) {
core.info(
`PR #${pr.number} is opted out of automation (${formatSourceContextForLog(sourceContext)}); skipping issue-sourced body sync.`,
);
return;
}

Copilot uses AI. Check for mistakes.
const explicitNonIssueSourceContext = resolveNonIssueWorkflowSourceContextForBodySync(pr, issueNumber);
if (explicitNonIssueSourceContext) {
core.info(
`PR #${pr.number} has explicit non-issue workflow source context (${formatSourceContextForLog(explicitNonIssueSourceContext)}); skipping issue-sourced body sync.`,
Expand All @@ -1382,8 +1388,6 @@ async function run({github: rawGithub, context, core, inputs}) {
return;
}

const issueNumber = extractIssueNumberFromPull(pr);
const sourceContext = resolvePrSourceContext(pr);
if (!issueNumber) {
if (sourceContext.isValid && !sourceContext.requiresIssue) {
core.info(
Expand Down Expand Up @@ -1454,6 +1458,25 @@ async function run({github: rawGithub, context, core, inputs}) {
return;
}

try {
const comments = await github.paginate(github.rest.issues.listComments, {
owner,
repo,
issue_number: pr.number,
});
await resolveSourceContextRepairComment({
github,
owner,
repo,
prNumber: pr.number,
comments,
sourceContext,
core,
});
} catch (error) {
core.warning(`Failed to resolve workflow source repair comment: ${error.message}`);

Copilot AI Apr 27, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The catch block assumes error has a .message property. If a non-Error is thrown (string/object/null), this can log undefined or even throw again. Use a safe conversion like error instanceof Error ? error.message : String(error) (as done elsewhere in this repo) before interpolating.

Suggested change
core.warning(`Failed to resolve workflow source repair comment: ${error.message}`);
const errorMessage = error instanceof Error ? error.message : String(error);
core.warning(`Failed to resolve workflow source repair comment: ${errorMessage}`);

Copilot uses AI. Check for mistakes.
}

core.info(`Fetching content from issue #${issueNumber} for PR #${pr.number}`);
const issueResponse = await withRetries(
() => github.rest.issues.get({owner, repo, issue_number: issueNumber}),
Expand Down Expand Up @@ -1629,6 +1652,7 @@ module.exports = {
buildSourceContextRepairCommentBody,
buildSourceContextResolvedCommentBody,
resolveExplicitNonIssueWorkflowSourceContext,
resolveNonIssueWorkflowSourceContextForBodySync,
resolveSourceContextRepairComment,
isCampaignIssue,
buildStatusBlock,
Expand Down
8 changes: 6 additions & 2 deletions .github/scripts/coverage_monitor_summary.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,8 +140,12 @@ function optionalExistingReportPath(filePath) {
const cleanedPath = cleanString(filePath);
if (!cleanedPath) return '';
if (!fs.existsSync(cleanedPath)) return '';
if (!fs.statSync(cleanedPath).isFile()) return '';
return cleanedPath;
try {
if (!fs.statSync(cleanedPath).isFile()) return '';
return cleanedPath;
} catch (_error) {
return '';
}
}

function buildCoverageMonitorSummary(options = {}) {
Expand Down
103 changes: 84 additions & 19 deletions .github/scripts/source_context.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,19 +34,23 @@ const SOURCE_LABELS = Object.freeze({
workflow_no_automation: SOURCE_TYPES.MANUAL_REMOTE,
});

const NO_AUTOMATION_LABELS = new Set(['workflow:no-automation', 'workflow_no_automation']);

const CHECKBOX_SOURCE_PATTERNS = Object.freeze([
[SOURCE_TYPES.GITHUB_ISSUE, /\bgithub\s+issue\b|\bsource\s+issue\b/i],
[
SOURCE_TYPES.MANUAL_REMOTE,
/\bdirect\s+pr\b|\bremote\s+github\s+work\b|\bstarted\s+directly\b|\bdo\s+not\s+automate\b|\bhuman[- ]only\b/i,
],
[SOURCE_TYPES.LOCAL_REQUEST, /\blocal\s+(?:codex|user)\s+request\b|\blocal\s+request\b/i],
[SOURCE_TYPES.LOCAL_REQUEST, /\blocal\s+(?:codex(?:\s*\/\s*|\s+)?user|codex|user)\s+request\b|\blocal\s+request\b/i],
[SOURCE_TYPES.AUTOMATION_RUN, /\bautomation\s+run\b|\bworkflow\s+run\b/i],
[SOURCE_TYPES.REVIEW_FOLLOWUP, /\breview\s+follow[- ]?up\b|\bfollow[- ]?up\s+from\s+pr\b/i],
[SOURCE_TYPES.SYNC_CAMPAIGN, /\bsync\b|\bmaintenance\s+campaign\b|\bmaintenance\b/i],
[SOURCE_TYPES.DEPENDABOT, /\bdependabot\b|\bdependency\s+update\b/i],
]);

const NO_AUTOMATION_CHECKBOX_PATTERN = /\bdo\s+not\s+automate\b|\bhuman[- ]only\b/i;

function cleanString(value) {
return String(value || '').trim();
}
Expand Down Expand Up @@ -106,6 +110,54 @@ function labelNames(pull = {}) {
: [];
}

function checkedLabels(lines) {
return lines
.map((line) => line.match(/^\s*[-*]\s+\[[xX]\]\s+(.+?)\s*$/))
.filter(Boolean)
.map((match) => match[1]);
}

function workflowSourceSectionLines(body) {
const lines = String(body || '').split(/\r?\n/);
const start = lines.findIndex((line) => /^#{1,6}\s+Workflow Source\s*$/i.test(line));
if (start < 0) {
return [];
}

const sectionLines = [];
for (const line of lines.slice(start + 1)) {
if (/^#{1,6}\s+\S/.test(line)) {
break;
}
sectionLines.push(line);
}
return sectionLines;
}

function startedFromLines(sectionLines) {
const start = sectionLines.findIndex((line) => /^\s*Started from:\s*$/i.test(line));
if (start < 0) {
return sectionLines;
}

const result = [];
for (const line of sectionLines.slice(start + 1)) {
if (/^\s*(Automation intent|Notes):\s*$/i.test(line)) {
break;
}
result.push(line);
}
return result;
}

function hasCheckedNoAutomationTemplate(body) {
const sectionLines = workflowSourceSectionLines(body);
if (!sectionLines.length) {
return false;
}
return checkedLabels(sectionLines).some((label) => NO_AUTOMATION_CHECKBOX_PATTERN.test(label));
}

function hasExplicitIssueReferencePrefix(value) {
const prefix = cleanString(value)
.replace(/[>_[\]()`*~]/g, ' ')
Expand Down Expand Up @@ -200,26 +252,12 @@ function parseWorkflowSourceBlock(body) {
}

function sourceTypeFromCheckedTemplate(body) {
const lines = String(body || '').split(/\r?\n/);
const start = lines.findIndex((line) => /^#{1,6}\s+Workflow Source\s*$/i.test(line));
if (start < 0) {
const sectionLines = workflowSourceSectionLines(body);
if (!sectionLines.length) {
return SOURCE_TYPES.UNKNOWN;
}
const sectionLines = [];
for (const line of lines.slice(start + 1)) {
if (/^#{1,6}\s+\S/.test(line)) {
break;
}
sectionLines.push(line);
}
const text = sectionLines.join('\n');
const checkedTypes = new Set();
for (const line of text.split(/\r?\n/)) {
const checkbox = line.match(/^\s*[-*]\s+\[[xX]\]\s+(.+?)\s*$/);
if (!checkbox) {
continue;
}
const label = checkbox[1];
for (const label of checkedLabels(startedFromLines(sectionLines))) {
for (const [sourceType, pattern] of CHECKBOX_SOURCE_PATTERNS) {
if (pattern.test(label)) {
checkedTypes.add(sourceType);
Expand All @@ -230,6 +268,27 @@ function sourceTypeFromCheckedTemplate(body) {
return checkedTypes.size === 1 ? Array.from(checkedTypes)[0] : SOURCE_TYPES.UNKNOWN;
}

function hasNoAutomationWorkflowContext(pull = {}) {
const body = String(pull?.body || '');
const markerToken = normalizeToken(parseHtmlMarker(body, 'workflow-source'));
const block = parseWorkflowSourceBlock(body);
const blockTokens = [
block.origin,
block.source,
block.type,
block.automation,
block.automation_intent,
].map(normalizeToken);
const labels = labelNames(pull).map((label) => label.toLowerCase());

return (
markerToken === 'no_automation'
|| blockTokens.includes('no_automation')
|| labels.some((label) => NO_AUTOMATION_LABELS.has(label) || NO_AUTOMATION_LABELS.has(normalizeToken(label)))
|| hasCheckedNoAutomationTemplate(body)
);
}

function sourceTypeFromLabels(pull = {}) {
for (const label of labelNames(pull)) {
const sourceType = SOURCE_LABELS[label.toLowerCase()] || SOURCE_LABELS[normalizeToken(label)];
Expand Down Expand Up @@ -267,6 +326,7 @@ function resolvePrSourceContext(pull = {}) {
const body = String(pull?.body || '');
const block = parseWorkflowSourceBlock(body);
const issueNumber = extractIssueNumberFromPull(pull);
const noAutomation = hasNoAutomationWorkflowContext(pull);

const markerType = normalizeSourceType(parseHtmlMarker(body, 'workflow-source'));
const blockType = normalizeSourceType(block.origin || block.source || block.type);
Expand Down Expand Up @@ -305,12 +365,13 @@ function resolvePrSourceContext(pull = {}) {
labelType !== SOURCE_TYPES.UNKNOWN
),
requiresIssue: sourceType === SOURCE_TYPES.GITHUB_ISSUE,
noAutomation,
};
}

function hasValidNonIssueSourceContext(pull = {}) {
const context = resolvePrSourceContext(pull);
return context.isValid && !context.requiresIssue;
return context.isValid && !context.requiresIssue && !context.noAutomation;
}

function formatSourceContextForLog(context = {}) {
Expand All @@ -324,6 +385,9 @@ function formatSourceContextForLog(context = {}) {
if (context.automation) {
parts.push(`automation=${context.automation}`);
}
if (context.noAutomation) {
parts.push('no_automation=true');
}
return parts.join(' ');
}

Expand All @@ -335,6 +399,7 @@ module.exports = {
parseWorkflowSourceBlock,
sourceTypeFromCheckedTemplate,
sourceTypeFromLabels,
hasNoAutomationWorkflowContext,
resolvePrSourceContext,
hasValidNonIssueSourceContext,
formatSourceContextForLog,
Expand Down
10 changes: 9 additions & 1 deletion scripts/aggregate_agent_metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -1088,13 +1088,21 @@ def _artifact_selection_contract(selection: dict[str, Any], selection_path: Path
for item in statuses
if item["status"] == "missing" or item["selected_count"] <= 0
]
missing_priority_families = selection.get("missing_priority_families")
if isinstance(missing_priority_families, (list, tuple)):
missing_priority_families = [
str(family) for family in missing_priority_families if isinstance(family, str)

Copilot AI Apr 27, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the missing_priority_families normalization, the list comprehension filters to isinstance(family, str) but then wraps each entry in str(family), which is redundant. Either drop the str(...) call or (if coercion is intended) widen the filter to accept non-string values and coerce them consistently.

Suggested change
str(family) for family in missing_priority_families if isinstance(family, str)
family for family in missing_priority_families if isinstance(family, str)

Copilot uses AI. Check for mistakes.
]
else:
missing_priority_families = []

return {
"schema": selection.get("schema") or "unknown",
"path": selection_path.as_posix(),
"status": selection.get("status") or "unknown",
"selected_count": _safe_int(selection.get("selected_count")) or 0,
"candidate_count": _safe_int(selection.get("candidate_count")) or 0,
"missing_priority_families": list(selection.get("missing_priority_families") or []),
"missing_priority_families": missing_priority_families,
"terminal_artifact_families": statuses,
"missing_terminal_artifact_families": missing_terminal,
}
Expand Down
Loading