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
146 changes: 139 additions & 7 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,92 @@ 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} needs either a linked GitHub issue or one valid non-issue Workflow Source 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.',
'- 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');
}

async function updateIssueCommentWithRetry({ github, owner, repo, commentId, body, core }) {
return withRetries(
() => github.rest.issues.updateComment({
owner,
repo,
comment_id: commentId,
body,
}),
{ description: `issues.updateComment #${commentId}`, core },
);
}

async function createIssueCommentWithRetry({ github, owner, repo, issueNumber, body, core }) {
return withRetries(
() => github.rest.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body,
}),
{ description: `issues.createComment #${issueNumber}`, core },
);
}

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 updateIssueCommentWithRetry({
github,
owner,
repo,
commentId: existingWarning.id,
body: resolvedBody,
core,
});
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 +1321,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,17 +1360,30 @@ 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 updateIssueCommentWithRetry({
github,
owner,
repo,
commentId: existingWarning.id,
body: commentBody,
core,
});
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({
await createIssueCommentWithRetry({
github,
owner,
repo,
issue_number: pr.number,
body: commentBody
issueNumber: pr.number,
body: commentBody,
core,
});
}
} catch (error) {
Expand Down Expand Up @@ -1435,6 +1564,9 @@ module.exports = {
filterWorkflowRunsForStatus,
buildContextBlock,
buildPreamble,
buildSourceContextRepairCommentBody,
buildSourceContextResolvedCommentBody,
resolveSourceContextRepairComment,
isCampaignIssue,
buildStatusBlock,
withRetries,
Expand Down
15 changes: 15 additions & 0 deletions .github/scripts/coverage_monitor_summary.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,16 @@ 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];
const prSourceContextReportPath = cleanString(options.pr_source_context_report);
if (
prSourceContextReportPath &&
fs.existsSync(prSourceContextReportPath) &&
fs.statSync(prSourceContextReportPath).isFile()
) {
monitors.push(
summarizeReport(readJsonReport(prSourceContextReportPath, '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 +221,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 || '',
output_json:
process.env.COVERAGE_MONITOR_SUMMARY_JSON || 'coverage-monitor-summary.json',
output_md:
Expand All @@ -226,6 +238,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