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
24 changes: 24 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
## Workflow Source

Started from:
- [ ] GitHub issue: #
- [ ] Direct PR / remote GitHub work
- [ ] Local Codex/user request
- [ ] Automation run
- [ ] Review follow-up from PR #
- [ ] Sync / maintenance campaign
- [ ] Dependabot or dependency update
- [ ] Do not automate

Automation intent:
- [ ] Verifier should review this
- [ ] Keepalive may manage this PR
- [ ] Human-only unless checks fail

Notes:
<!-- If there is no linked issue, briefly describe the source. Automation also accepts workflow source labels such as workflow:source-direct-pr. -->

## Summary


## Testing
29 changes: 26 additions & 3 deletions .github/scripts/agents_pr_meta_keepalive.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
const { createGithubApiCache } = require('./github-api-cache-client');
const { makeTrace } = require('./keepalive_contract.js');
const { ensureRateLimitWrapped } = require('./github-rate-limited-wrapper.js');
const {
formatSourceContextForLog,
resolvePrSourceContext,
} = require('./source_context.js');

const DEFAULT_INSTRUCTION_SIGNATURE =
'keepalive workflow continues nudging until everything is complete';
Expand Down Expand Up @@ -335,6 +339,8 @@ async function detectKeepalive({ core, github, context, env = process.env }) {
instruction_bytes: '0',
agent_alias: '',
head_sha: '',
source_type: '',
source_ref: '',
};

const setBasicOutputs = () => {
Expand All @@ -359,6 +365,8 @@ async function detectKeepalive({ core, github, context, env = process.env }) {
core.setOutput('instruction_bytes', outputs.instruction_bytes || '0');
core.setOutput('agent_alias', outputs.agent_alias || '');
core.setOutput('head_sha', outputs.head_sha || '');
core.setOutput('source_type', outputs.source_type || '');
core.setOutput('source_ref', outputs.source_ref || '');
};

const { comment, issue } = context.payload || {};
Expand Down Expand Up @@ -645,6 +653,13 @@ async function detectKeepalive({ core, github, context, env = process.env }) {
if (issueNumber) {
outputs.issue = String(issueNumber);
}
const sourceContext = resolvePrSourceContext(pull);
if (sourceContext.isKnown) {
outputs.source_type = sourceContext.sourceType;
}
if (sourceContext.sourceRef) {
outputs.source_ref = sourceContext.sourceRef;
}

let reactions = [];
try {
Expand Down Expand Up @@ -741,9 +756,17 @@ async function detectKeepalive({ core, github, context, env = process.env }) {
}

if (!issueNumber) {
outputs.reason = 'missing-issue-reference';
core.info('Keepalive dispatch skipped: unable to determine linked issue number.');
return finalise();
if (sourceContext.isValid && !sourceContext.requiresIssue) {
core.info(
`Keepalive dispatch continuing with non-issue workflow source context (${formatSourceContextForLog(sourceContext)}).`,
);
} else {
outputs.reason = 'missing-source-context';
core.info(
'Keepalive dispatch skipped: unable to determine linked issue number or another valid workflow source context.',
);
return finalise();
}
}

// Add agents:activated label on first human activation per GoalsAndPlumbing.md Section 1
Expand Down
110 changes: 106 additions & 4 deletions .github/scripts/agents_pr_meta_update_body.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ const fs = require('fs');
const os = require('os');
const childProcess = require('child_process');
const { ensureRateLimitWrapped } = require('./github-rate-limited-wrapper.js');
const {
formatSourceContextForLog,
resolvePrSourceContext,
} = require('./source_context.js');

class RateLimitError extends Error {
constructor(message, options = {}) {
Expand Down Expand Up @@ -930,6 +934,66 @@ function isCampaignIssue(issue = {}) {
return labels.has('campaign:sync-dependabot') || labels.has('campaign:active');
}

function buildSourceContextRepairCommentBody(prNumber) {
return [
'<!-- missing-issue-warning -->',
'### Workflow source needed',
'',
`PR #${prNumber} does not need a GitHub issue, but Workflows needs one valid source context before PR metadata automation can manage it safely.`,
'',
'Please do one of:',
'',
'- Add `<!-- meta:issue:123 -->` or a normal `Closes #123` / `Related to #123` line.',
'- Check one `Workflow Source` option in the PR body.',
Comment on lines +942 to +947

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 repair comment currently states “PR #X does not need a GitHub issue” but then instructs the user to add an issue reference; for PRs that do originate from an issue this reads contradictory. Reword the first sentence to reflect the actual requirement (“add a linked issue OR select a valid non-issue Workflow Source”) so the guidance is accurate for both cases.

Copilot uses AI. Check for mistakes.
'- Add a hidden marker such as `<!-- workflow-source:local_request -->`, `<!-- workflow-source:manual_remote -->`, `<!-- workflow-source:review_followup -->`, `<!-- workflow-source:sync_campaign -->`, or `<!-- workflow-source:dependabot -->`.',
'- Add a workflow source label such as `workflow:source-direct-pr`, `workflow:source-local-request`, `workflow:source-review-followup`, `workflow:source-sync`, or `workflow:no-automation`.',
'',
'Once a valid source is present, this warning will not be reposted.',
].join('\n');
}

function buildSourceContextResolvedCommentBody(prNumber, sourceContext) {
return [
'<!-- missing-issue-warning -->',
'### Workflow source detected',
'',
`PR #${prNumber} now has valid workflow source context (${formatSourceContextForLog(sourceContext)}).`,
'',
'No linked GitHub issue is required for this PR.',
].join('\n');
}

async function resolveSourceContextRepairComment({
github,
owner,
repo,
prNumber,
comments,
sourceContext,
core,
}) {
const marker = '<!-- missing-issue-warning -->';
const existingWarning = comments.find((c) => c.body && c.body.includes(marker));
if (!existingWarning) {
return false;
}

const resolvedBody = buildSourceContextResolvedCommentBody(prNumber, sourceContext);
if (existingWarning.body === resolvedBody) {
core?.info?.(`Workflow source repair comment already resolved (id: ${existingWarning.id})`);
return false;
}

await github.rest.issues.updateComment({
owner,
repo,
comment_id: existingWarning.id,
body: resolvedBody,
});
Comment on lines +987 to +992

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.

These new comment updates bypass the existing retry/rate-limit wrapper patterns used elsewhere in this script (e.g., helpers that call withRetries(...)). To avoid flakiness from secondary rate limits/transient failures, route these updateComment calls through the same retry wrapper (or reuse the existing updateComment helper if available in scope).

Copilot uses AI. Check for mistakes.
core?.info?.(`Resolved workflow source repair comment (id: ${existingWarning.id})`);
return true;
}

function buildPreamble(sections) {
const lines = ['<!-- pr-preamble:start -->'];

Expand Down Expand Up @@ -1231,9 +1295,35 @@ async function run({github: rawGithub, context, core, inputs}) {
}

const issueNumber = extractIssueNumberFromPull(pr);
const sourceContext = resolvePrSourceContext(pr);
if (!issueNumber) {
if (sourceContext.isValid && !sourceContext.requiresIssue) {
core.info(
`PR #${pr.number} has valid non-issue workflow source context (${formatSourceContextForLog(sourceContext)}); skipping issue-sourced body sync.`,
);
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}`);
}
return;
}

const marker = '<!-- missing-issue-warning -->';
const warningMsg = `Unable to determine source issue for PR #${pr.number}. The PR title, branch name, or body must contain the issue number (e.g. #123, branch: issue-123, or the hidden marker <!-- meta:issue:123 -->).`;
const warningMsg = `Unable to determine workflow source context for PR #${pr.number}. Add a GitHub issue reference or another valid Workflow Source entry.`;
core.warning(warningMsg);

try {
Expand All @@ -1244,11 +1334,20 @@ async function run({github: rawGithub, context, core, inputs}) {
});
// Find existing warning comment by marker to enable upsert pattern
const existingWarning = comments.find((c) => c.body && c.body.includes(marker));
const commentBody = `${marker}\n⚠️ **Action Required**: ${warningMsg}`;
const commentBody = buildSourceContextRepairCommentBody(pr.number);

if (existingWarning) {
// Update existing comment (avoids duplicates from race conditions)
core.info(`Warning comment already exists (id: ${existingWarning.id}), skipping duplicate`);
if (existingWarning.body !== commentBody) {
await github.rest.issues.updateComment({
owner,
repo,
comment_id: existingWarning.id,
body: commentBody,
});
Comment on lines +1341 to +1346

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.

These new comment updates bypass the existing retry/rate-limit wrapper patterns used elsewhere in this script (e.g., helpers that call withRetries(...)). To avoid flakiness from secondary rate limits/transient failures, route these updateComment calls through the same retry wrapper (or reuse the existing updateComment helper if available in scope).

Copilot uses AI. Check for mistakes.
core.info(`Updated workflow source repair comment (id: ${existingWarning.id})`);
} else {
core.info(`Workflow source repair comment already exists (id: ${existingWarning.id})`);
}
} else {
await github.rest.issues.createComment({
owner,
Expand Down Expand Up @@ -1435,6 +1534,9 @@ module.exports = {
filterWorkflowRunsForStatus,
buildContextBlock,
buildPreamble,
buildSourceContextRepairCommentBody,
buildSourceContextResolvedCommentBody,
resolveSourceContextRepairComment,
isCampaignIssue,
buildStatusBlock,
withRetries,
Expand Down
10 changes: 10 additions & 0 deletions .github/scripts/coverage_monitor_summary.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,11 @@ function buildCoverageMonitorSummary(options = {}) {
const terminal = summarizeReport(readJsonReport(options.terminal_report, 'terminal-disposition'));
const botAuth = summarizeReport(readJsonReport(options.bot_auth_report, 'bot-comment-auth'));
const monitors = [terminal, botAuth];
if (cleanString(options.pr_source_context_report)) {
monitors.push(
summarizeReport(readJsonReport(options.pr_source_context_report, 'pr-source-context'))
);
}
const status = overallStatus(monitors);
const hardBlockActive = monitors.some((monitor) => monitor.hard_block_active);
const shouldFail = monitors.some((monitor) => monitor.should_fail);
Expand Down Expand Up @@ -211,6 +216,8 @@ function parseArgs(argv = process.argv.slice(2)) {
process.env.COVERAGE_MONITOR_TERMINAL_JSON || 'terminal-disposition-coverage.json',
bot_auth_report:
process.env.COVERAGE_MONITOR_BOT_AUTH_JSON || 'bot-comment-auth-coverage-summary.json',
pr_source_context_report:
process.env.COVERAGE_MONITOR_PR_SOURCE_CONTEXT_JSON || 'pr-source-context-coverage.json',
output_json:
process.env.COVERAGE_MONITOR_SUMMARY_JSON || 'coverage-monitor-summary.json',
output_md:
Expand All @@ -226,6 +233,9 @@ function parseArgs(argv = process.argv.slice(2)) {
} else if (arg === '--bot-auth-report') {
options.bot_auth_report = next;
index += 1;
} else if (arg === '--pr-source-context-report') {
options.pr_source_context_report = next;
index += 1;
} else if (arg === '--output-json') {
options.output_json = next;
index += 1;
Expand Down
2 changes: 2 additions & 0 deletions .github/scripts/github-api-with-retry.js
Original file line number Diff line number Diff line change
Expand Up @@ -609,6 +609,8 @@ async function createTokenAwareRetry(options = {}) {
}

module.exports = {
isRateLimitError,
isSecondaryRateLimitError,
withRetry,
paginateWithRetry,
createTokenAwareRetry,
Expand Down
Loading
Loading