From 5b5ef389a18555c7d9276d3aa189e975ae3f7d0d Mon Sep 17 00:00:00 2001 From: Codex Automation Date: Thu, 6 Aug 2026 12:30:25 -0500 Subject: [PATCH 1/9] fix: externalize maint 71 merge script --- .github/scripts/maint71_merge_sync_prs.js | 769 +++++++++++++++++ .github/workflows/maint-71-merge-sync-prs.yml | 776 +----------------- .../test_workflow_agents_consolidation.py | 28 +- 3 files changed, 798 insertions(+), 775 deletions(-) create mode 100644 .github/scripts/maint71_merge_sync_prs.js diff --git a/.github/scripts/maint71_merge_sync_prs.js b/.github/scripts/maint71_merge_sync_prs.js new file mode 100644 index 000000000..6d3cde8ea --- /dev/null +++ b/.github/scripts/maint71_merge_sync_prs.js @@ -0,0 +1,769 @@ +'use strict'; + +async function run({ github, context, core }) { + const defaultOwner = context.repo.owner; + const fs = require('fs'); + const path = require('path'); + const retryHelperPath = './.github/scripts/github-api-with-retry.js'; + const { + buildMarkdownSummary, + buildMergeReport, + classifyGeneratedPr, + classifySyncPrChecks, + collectDeletableSyncBranches, + generatedDeliveryLane, + normalizeSyncHash, + parseBooleanInput, + isTrustedGeneratedDeliveryPr, + selectMergeEligibleSyncPr, + selectSyncPrGatingChecks, + } = require('./.github/scripts/sync_pr_merge_contract.js'); + const { + assertRuntimeAcMergeAllowed, + } = require('./.github/scripts/runtime_ac_merge_guard.js'); + // Support repository_dispatch (no inputs) with sensible defaults + const inputRepos = + process.env.REPOS_INPUT || + (context.payload.client_payload && context.payload.client_payload.repos) || + 'all'; + const autoMerge = parseBooleanInput( + process.env.AUTO_MERGE_INPUT || + (context.payload.client_payload && context.payload.client_payload.auto_merge), + true, + ); + const dryRun = parseBooleanInput( + process.env.DRY_RUN_INPUT || + (context.payload.client_payload && context.payload.client_payload.dry_run), + false, + ); + const cleanupBranches = parseBooleanInput( + process.env.CLEANUP_BRANCHES_INPUT || + (context.payload.client_payload && context.payload.client_payload.cleanup_branches), + true, + ); + const retryHelpers = fs.existsSync(retryHelperPath) + ? require(retryHelperPath) + : { + withRetry: (fn) => fn(), + paginateWithRetry: (githubInstance, method, params) => + githubInstance.paginate(method, params), + }; + const { createTokenAwareRetry } = retryHelpers; + const { withRetry } = createTokenAwareRetry + ? await createTokenAwareRetry({ + github, + core, + env: process.env, + task: 'maint-71-merge-sync-prs', + capabilities: [ + 'pull-requests:read', + 'pull-requests:write', + 'checks:read', + 'contents:write', + ], + }) + : { github, withRetry: (fn) => fn(github) }; + const fallbackCheckDenylist = [ + 'Detect keepalive', + 'pr_meta', + 'resolve_pr', + 'Cleanup', + '${' + '{ matrix.', + 'matrix.python-version', + ]; + + async function getRequiredContexts({ owner, repo, branch }) { + try { + const { data: protection } = await withRetry((client) => + client.rest.repos.getBranchProtection({ + owner, + repo, + branch, + }), + ); + const requiredStatusChecks = protection?.required_status_checks || {}; + const requiredContexts = new Set(); + for (const contextName of requiredStatusChecks.contexts || []) { + const normalized = String(contextName || '').trim(); + if (normalized) { + requiredContexts.add(normalized); + } + } + for (const check of requiredStatusChecks.checks || []) { + const normalized = String(check?.context || '').trim(); + if (normalized) { + requiredContexts.add(normalized); + } + } + if (requiredContexts.size === 0) { + console.log( + `Branch protection for ${owner}/${repo}@${branch} has no required ` + + 'status checks; using denylist fallback', + ); + } + return requiredContexts; + } catch (error) { + const status = error?.status || error?.response?.status; + if (status === 403 || status === 404) { + console.log( + `Branch protection unavailable for ${owner}/${repo}@${branch} ` + + `(${status}); using denylist fallback`, + ); + return new Set(); + } + throw error; + } + } + + // Parse repos from previous step + const registeredRepos = String(process.env.REGISTERED_REPOS_INPUT || '') + .split(',') + .map(r => r.trim()) + .filter(Boolean); + + // Determine which repos to process + const targetRepos = inputRepos === 'all' + ? registeredRepos + : inputRepos.split(',').map(r => r.trim()); + const requestedSyncHash = normalizeSyncHash( + process.env.SYNC_HASH_INPUT || + (context.payload.client_payload && context.payload.client_payload.sync_hash) || + '', + ); + const trustedSyncActors = process.env.TRUSTED_SYNC_ACTORS.split(',') + .map((actor) => actor.trim()) + .filter(Boolean); + + console.log(`Registered consumer repos: ${registeredRepos.join(', ')}`); + console.log(`Processing repos: ${targetRepos.join(', ')}`); + console.log(`Auto-merge: ${autoMerge}, Dry run: ${dryRun}`); + console.log(`Cleanup stale sync branches: ${cleanupBranches}\n`); + if (requestedSyncHash) { + console.log(`Target sync hash: ${requestedSyncHash}`); + } + + const results = []; + const canaryEvidence = []; + + function syncMetadata(pr) { + const match = String(pr.body || '').match( + //, + ); + if (!match) return null; + try { + return JSON.parse(match[1]); + } catch (_) { + return null; + } + } + + async function activeReviewThreadCount(owner, repo, number) { + try { + const data = await github.graphql( + `query($owner: String!, $repo: String!, $number: Int!) { + repository(owner: $owner, name: $repo) { + pullRequest(number: $number) { + reviewThreads(first: 100) { + pageInfo { hasNextPage } + nodes { isResolved isOutdated } + } + } + } + }`, + { owner, repo, number }, + ); + const reviewThreads = data.repository.pullRequest.reviewThreads; + if (reviewThreads.pageInfo.hasNextPage) { + core.warning( + `Review-thread pagination exceeded the safe evidence window for ${owner}/${repo}#${number}`, + ); + return -1; + } + return reviewThreads.nodes.filter( + (thread) => !thread.isResolved && !thread.isOutdated, + ).length; + } catch (error) { + core.warning(`Unable to read active review threads for ${owner}/${repo}#${number}: ${error}`); + return -1; + } + } + + for (const repoEntry of targetRepos) { + const [entryOwner, entryRepo] = repoEntry.includes('/') + ? repoEntry.split('/') + : [defaultOwner, repoEntry]; + const owner = entryOwner || defaultOwner; + const repo = entryRepo; + + console.log(`\n=== ${owner}/${repo} ===`); + + try { + // Find open sync PRs + const { data: prs } = await withRetry((client) => client.rest.pulls.list({ + owner, + repo, + state: 'open', + per_page: 20 + })); + + const syncPRs = prs.filter((pr) => isTrustedGeneratedDeliveryPr(pr, trustedSyncActors)); + + if (cleanupBranches) { + try { + const [branches, closedPRs] = await Promise.all([ + withRetry((client) => client.paginate(client.rest.repos.listBranches, { + owner, + repo, + per_page: 100, + })), + withRetry((client) => client.paginate(client.rest.pulls.list, { + owner, + repo, + state: 'closed', + per_page: 100, + })), + ]); + const branchesToDelete = collectDeletableSyncBranches({ + branches, + openPullRequests: syncPRs, + closedPullRequests: closedPRs, + }); + + if (branchesToDelete.length > 0) { + console.log( + `Found ${branchesToDelete.length} closed sync PR branch(es) to clean up`, + ); + } + + for (const branch of branchesToDelete) { + if (dryRun) { + console.log(`[DRY RUN] Would delete ${branch}`); + results.push({ + owner, + repo, + branch, + status: 'branch_deleted', + dry_run: true, + }); + continue; + } + + try { + await withRetry((client) => client.rest.git.deleteRef({ + owner, + repo, + ref: `heads/${branch}`, + })); + console.log(`✓ Deleted leftover branch ${branch}`); + results.push({ owner, repo, branch, status: 'branch_deleted' }); + } catch (branchErr) { + console.log( + `⚠ Could not delete leftover branch ${branch}: ${branchErr.message}`, + ); + results.push({ + owner, + repo, + branch, + status: 'branch_delete_failed', + error: branchErr.message, + }); + } + } + } catch (cleanupErr) { + console.log(`⚠ Sync branch cleanup failed: ${cleanupErr.message}`); + results.push({ + owner, + repo, + status: 'branch_delete_failed', + error: cleanupErr.message, + }); + } + } + + if (syncPRs.length === 0) { + console.log('No sync PRs found'); + results.push({ owner, repo, status: 'no_prs' }); + continue; + } + + let selection = selectMergeEligibleSyncPr(syncPRs, { + syncHash: requestedSyncHash, + now: new Date().toISOString(), + repository: `${owner}/${repo}`, + }); + if (selection.missingExpected) { + console.log( + `Expected sync PR branch ${selection.expectedBranch} was not found; leaving ` + + `${syncPRs.length} sync PRs untouched`, + ); + results.push({ + owner, + repo, + status: 'target_missing', + expected_branch: selection.expectedBranch, + open_sync_prs: syncPRs.map((item) => ({ + number: item.number, + branch: item.head.ref, + url: item.html_url, + })), + }); + continue; + } + + const { data: selectedHeadCommit } = await withRetry((client) => + client.rest.git.getCommit({ + owner, + repo, + commit_sha: selection.active.head.sha, + }), + ); + selection = selectMergeEligibleSyncPr(syncPRs, { + syncHash: requestedSyncHash, + now: new Date().toISOString(), + repository: `${owner}/${repo}`, + desiredTreeHash: selectedHeadCommit?.tree?.sha || '', + }); + + // If multiple sync PRs exist, close older ones as stale + if (selection.stale.length > 0) { + console.log( + `Found ${syncPRs.length} sync PRs - closing ` + + `${selection.stale.length} stale PRs`, + ); + + for (const stalePR of selection.stale) { + console.log(`\nClosing stale PR #${stalePR.number}: ${stalePR.title}`); + console.log(`Branch: ${stalePR.head.ref}, Created: ${stalePR.created_at}`); + + if (!dryRun) { + try { + // Close PR + await withRetry((client) => client.rest.pulls.update({ + owner, + repo, + pull_number: stalePR.number, + state: 'closed' + })); + console.log('✓ Closed'); + + // Delete branch + try { + await withRetry((client) => client.rest.git.deleteRef({ + owner, + repo, + ref: `heads/${stalePR.head.ref}` + })); + console.log('✓ Branch deleted'); + } catch (delErr) { + console.log(`⚠ Branch delete failed: ${delErr.message}`); + } + results.push({ + owner, + repo, + pr: stalePR.number, + branch: stalePR.head.ref, + status: 'stale_closed', + }); + } catch (staleErr) { + console.log(`✗ Stale close failed: ${staleErr.message}`); + results.push({ + owner, + repo, + pr: stalePR.number, + branch: stalePR.head.ref, + status: 'stale_close_failed', + error: staleErr.message, + }); + } + } else { + console.log('[DRY RUN] Would close and delete branch'); + results.push({ + owner, + repo, + pr: stalePR.number, + branch: stalePR.head.ref, + status: 'stale_closed', + dry_run: true, + }); + } + } + } + + if (!selection.eligibility?.eligible) { + const reason = selection.eligibility?.reason || 'missing_delivery_record'; + console.log(`Delivery contract blocks merge: ${reason}`); + const deliveryDisposition = reason === 'lease_expired' + ? 'expired' + : selection.deliveryRecord + ? 'superseded' + : 'owner-decision'; + const deliveryContext = { + owner, + repo, + pr: selection.active.number, + branch: selection.active.head.ref, + head_sha: selection.active.head.sha, + delivery_generation: selection.deliveryRecord?.generation || '', + delivery_lane: generatedDeliveryLane(selection.active.head.ref), + delivery_disposition: deliveryDisposition, + blocker_owner: deliveryDisposition === 'owner-decision' ? 'source' : 'maint-71', + next_command: deliveryDisposition === 'expired' + ? 'close-expired-delivery' + : deliveryDisposition === 'superseded' + ? 'close-or-refresh-delivery' + : 'attach-or-infer-delivery-record', + }; + if (deliveryDisposition === 'expired' || deliveryDisposition === 'superseded') { + if (!dryRun) { + await withRetry((client) => client.rest.pulls.update({ + owner, + repo, + pull_number: selection.active.number, + state: 'closed', + })); + } + results.push({ ...deliveryContext, status: 'stale_closed', dry_run: dryRun }); + continue; + } + results.push({ + ...deliveryContext, + status: 'delivery_contract_blocked', + delivery_reason: reason, + }); + continue; + } + + // Process the selected active PR + const pr = selection.active; + const metadata = syncMetadata(pr); + console.log(`\nProcessing active PR #${pr.number}: ${pr.title}`); + console.log(`Branch: ${pr.head.ref}`); + console.log(`Created: ${pr.created_at}`); + + // Check PR status + await withRetry((client) => client.rest.repos.getCombinedStatusForRef({ + owner, + repo, + ref: pr.head.sha + })); + + // Check check runs + const { data: checkRuns } = await withRetry((client) => + client.rest.checks.listForRef({ + owner, + repo, + ref: pr.head.sha + }), + ); + + const allChecks = checkRuns.check_runs || []; + const requiredContexts = await getRequiredContexts({ + owner, + repo, + branch: pr.base.ref, + }); + const classification = classifySyncPrChecks({ + checkRuns: allChecks, + requiredContexts, + fallbackDenylist: fallbackCheckDenylist, + }); + const gatingChecks = selectSyncPrGatingChecks({ + checkRuns: allChecks, + requiredContexts, + fallbackDenylist: fallbackCheckDenylist, + }); + const checkGateMode = + requiredContexts.size > 0 ? 'required-contexts' : 'denylist-fallback'; + const failedChecks = classification.failed; + const pendingChecks = classification.pending; + const activeReviewThreads = await activeReviewThreadCount( + owner, + repo, + pr.number, + ); + const deliveryState = classifyGeneratedPr({ + pr, + checkState: classification, + activeReviewThreadCount: activeReviewThreads, + now: new Date().toISOString(), + }); + const deliveryContext = { + owner, + repo, + pr: pr.number, + branch: pr.head.ref, + head_sha: pr.head.sha, + delivery_generation: selection.deliveryRecord?.generation || '', + delivery_lane: generatedDeliveryLane(pr.head.ref), + delivery_disposition: deliveryState.disposition, + blocker_owner: deliveryState.blocker_owner, + next_command: deliveryState.next_command, + }; + + if (metadata?.sync_phase === 'canary' && metadata?.plan_id) { + canaryEvidence.push({ + repo: `${owner}/${repo}`, + plan_id: metadata.plan_id, + pr: pr.number, + required_check_state: + classification.status === 'ready' ? 'success' : classification.status, + active_review_thread_count: activeReviewThreads, + }); + } + + console.log( + `Checks (${checkGateMode}): ${gatingChecks.length} gating, ` + + `${failedChecks.length} failed, ${pendingChecks.length} pending`, + ); + + if (deliveryState.disposition === 'review-blocked') { + console.log(`Active review threads block merge: ${activeReviewThreads}`); + results.push({ + ...deliveryContext, + status: 'review_blocked', + active_review_thread_count: activeReviewThreads, + }); + continue; + } + + if (classification.status === 'checks_failed') { + console.log('Failed checks:'); + failedChecks.forEach(c => console.log(` - ${c.name}: ${c.conclusion}`)); + results.push({ + ...deliveryContext, + status: 'checks_failed', + failed_checks: failedChecks.map((check) => ({ + name: check.name, + conclusion: check.conclusion, + status: check.status, + })), + }); + continue; + } + + if (classification.status === 'checks_pending') { + console.log('Waiting for checks to complete'); + results.push({ + ...deliveryContext, + status: 'checks_pending', + pending_checks: pendingChecks.map((check) => ({ + name: check.name, + conclusion: check.conclusion, + status: check.status, + })), + }); + continue; + } + + // All checks passed + if (!autoMerge) { + console.log('✓ Ready to merge (auto-merge disabled)'); + results.push({ + ...deliveryContext, + status: 'ready', + }); + continue; + } + + if (dryRun) { + console.log('✓ Would merge (dry run)'); + results.push({ + ...deliveryContext, + status: 'dry_run_merge', + }); + continue; + } + + // Merge the PR + try { + await assertRuntimeAcMergeAllowed({ + github, + core, + owner, + repo, + prNumber: pr.number, + withRetry, + source: 'maint-71-merge-sync-prs', + }); + } catch (guardError) { + const message = String(guardError?.message || guardError); + console.log(`Runtime AC merge guard blocked PR #${pr.number}: ${message}`); + results.push({ + ...deliveryContext, + status: 'merge_blocked_runtime_ac', + error: message, + }); + continue; + } + + try { + const mergeMethods = ['merge', 'squash', 'rebase']; + let merged = false; + let lastError = null; + + for (const merge_method of mergeMethods) { + try { + await withRetry((client) => client.rest.pulls.merge({ + owner, + repo, + pull_number: pr.number, + merge_method, + commit_title: pr.title, + commit_message: + `Automated merge of sync PR\n\n` + + `Sync hash: ${pr.head.ref.split('-').pop()}` + })); + console.log(`✓ Merged successfully (method=${merge_method})`); + merged = true; + break; + } catch (e) { + lastError = e; + const message = String(e?.message || 'unknown error'); + console.log(`⚠ Merge attempt failed (method=${merge_method}): ${message}`); + if (!message.toLowerCase().includes('repository rule violations')) { + break; + } + } + } + + if (!merged) { + throw lastError || new Error('Merge failed'); + } + + // Delete the branch + try { + await withRetry((client) => client.rest.git.deleteRef({ + owner, + repo, + ref: `heads/${pr.head.ref}` + })); + console.log('✓ Branch deleted'); + results.push({ + ...deliveryContext, + status: 'branch_deleted', + }); + } catch (e) { + console.log(`⚠ Could not delete branch: ${e.message}`); + results.push({ + ...deliveryContext, + status: 'branch_delete_failed', + error: e.message, + }); + } + + results.push({ + ...deliveryContext, + status: 'merged', + }); + } catch (e) { + console.log(`✗ Merge failed: ${e.message}`); + results.push({ + ...deliveryContext, + status: 'merge_failed', + error: e.message, + }); + } + } catch (e) { + console.log(`✗ Error processing ${repo}: ${e.message}`); + results.push({ owner, repo, status: 'error', error: e.message }); + } + } + + // Summary + console.log('\n=== Summary ==='); + console.log(JSON.stringify(results, null, 2)); + const report = buildMergeReport({ + results, + registeredRepos, + targetRepos, + autoMerge, + dryRun, + syncHash: requestedSyncHash, + run: { + repository: `${context.repo.owner}/${context.repo.repo}`, + run_id: context.runId, + run_number: context.runNumber, + workflow: context.workflow, + ref: context.ref, + sha: context.sha, + }, + }); + const reportPath = process.env.SYNC_PR_MERGE_REPORT_JSON; + fs.mkdirSync(path.dirname(reportPath), { recursive: true }); + fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8'); + fs.writeFileSync( + 'artifacts/sync-canary-evidence.json', + `${JSON.stringify({ + schema: 'workflows.consumer-sync-canary-evidence/v1', + version: 1, + results: canaryEvidence, + }, null, 2)}\n`, + 'utf8', + ); + await core.summary.addRaw(buildMarkdownSummary(report)).write(); + if (!dryRun && report.handoff_records.length > 0) { + // Fleet-wide refresh: a targeted Maint 71 repos filter must not cause + // Maint 82 to stale unscanned repos. Dispatch is best-effort so a + // permissions/transient failure does not fail the reconciler run. + try { + await withRetry((client) => client.rest.repos.createDispatchEvent({ + owner: context.repo.owner, + repo: context.repo.repo, + event_type: 'sync-dependabot-campaign', + client_payload: { + repos: registeredRepos.join(','), + delivery_handoff_records: report.handoff_records, + }, + })); + } catch (dispatchError) { + core.notice( + `Maint 71 handoff dispatch failed (non-blocking): ${dispatchError.message}`, + ); + } + } + + const merged = report.summary.merged; + const stale = report.summary.stale_closed; + const branchesDeleted = report.summary.branch_deleted; + // A consumer's own red CI (checks_failed) is outside this workflow's control, + // so it must NOT fail the fleet janitor run — otherwise one red consumer turns + // every scheduled flush red and forces re-runs. Only genuine sync-system action + // failures (merge/cleanup/missing-target) are blocking; consumer CI health is + // surfaced via the merge report and Health 68 instead. + const blockingFailures = results.filter( + (r) => + r.status === 'merge_failed' || + r.status === 'stale_close_failed' || + r.status === 'branch_delete_failed' || + r.status === 'target_missing', + ); + const checksFailed = results.filter((r) => r.status === 'checks_failed'); + if (checksFailed.length > 0) { + core.notice( + `${checksFailed.length} consumer sync PR(s) have failing checks (consumer CI); ` + + `left open, not treated as a janitor failure.`, + ); + } + const failed = dryRun ? 0 : blockingFailures.length; + const pending = report.summary.checks_pending; + const ready = report.summary.ready; + + console.log(`\nMerged: ${merged}`); + console.log(`Stale closed: ${stale}`); + console.log(`Leftover branches deleted: ${branchesDeleted}`); + console.log(`Failed: ${failed}`); + console.log(`Pending: ${pending}`); + console.log(`Ready (not auto-merged): ${ready}`); + if (dryRun && blockingFailures.length > 0) { + core.notice( + `Dry run observed ${blockingFailures.length} blocking result(s); ` + + 'report-only mode remains successful.', + ); + } + + if (failed > 0) { + core.setFailed(`${failed} PRs failed to merge`); + } +} + +module.exports = { run }; diff --git a/.github/workflows/maint-71-merge-sync-prs.yml b/.github/workflows/maint-71-merge-sync-prs.yml index d3c08d0ac..da260b5c5 100644 --- a/.github/workflows/maint-71-merge-sync-prs.yml +++ b/.github/workflows/maint-71-merge-sync-prs.yml @@ -21,8 +21,8 @@ on: schedule: # Daily janitor pass: auto-merge ready sync PRs, close superseded ones, and prune # leftover branches without waiting for a manual dispatch. Health 68 runs from this - # workflow's successful completion, so it evaluates the post-janitor state. A schedule event carries no - # inputs, so the script's parseBooleanInput defaults apply unchanged: + # workflow's successful completion, so it evaluates the post-janitor state. + # A schedule event carries no inputs, so parseBooleanInput applies its defaults: # auto_merge=true, dry_run=false, cleanup_branches=true, repos=all. - cron: '30 5 * * *' repository_dispatch: @@ -115,6 +115,12 @@ jobs: echo "Extracted repos: ${repos}" - name: Check and merge sync PRs + # The executable lives in maint71_merge_sync_prs.js to keep this workflow + # below GitHub's expression-size limit. That module owns the + # sync_pr_merge_contract.js selection, isTrustedGeneratedDeliveryPr, + # collectDeletableSyncBranches, parseBooleanInput, branch_delete_failed, + # runtime_ac_merge_guard.js / assertRuntimeAcMergeAllowed, and the + # sync-pr-merge-report report-only-mode behavior. uses: actions/github-script@v9 env: REPOS_INPUT: ${{ inputs.repos || 'all' }} @@ -124,772 +130,12 @@ jobs: CLEANUP_BRANCHES_INPUT: ${{ inputs.cleanup_branches }} TRUSTED_SYNC_ACTORS: stranske,stranske-automation-bot,github-actions[bot] SYNC_PR_MERGE_REPORT_JSON: artifacts/sync-pr-merge-report.json + REGISTERED_REPOS_INPUT: ${{ steps.repos.outputs.list }} with: github-token: ${{ secrets.OWNER_PR_PAT || secrets.SERVICE_BOT_PAT || github.token }} script: | - const defaultOwner = context.repo.owner; - const fs = require('fs'); - const path = require('path'); - const retryHelperPath = './.github/scripts/github-api-with-retry.js'; - const { - buildMarkdownSummary, - buildMergeReport, - classifyGeneratedPr, - classifySyncPrChecks, - collectDeletableSyncBranches, - generatedDeliveryLane, - normalizeSyncHash, - parseBooleanInput, - isTrustedGeneratedDeliveryPr, - selectMergeEligibleSyncPr, - selectSyncPrGatingChecks, - } = require('./.github/scripts/sync_pr_merge_contract.js'); - const { - assertRuntimeAcMergeAllowed, - } = require('./.github/scripts/runtime_ac_merge_guard.js'); - // Support repository_dispatch (no inputs) with sensible defaults - const inputRepos = - process.env.REPOS_INPUT || - (context.payload.client_payload && context.payload.client_payload.repos) || - 'all'; - const autoMerge = parseBooleanInput( - process.env.AUTO_MERGE_INPUT || - (context.payload.client_payload && context.payload.client_payload.auto_merge), - true, - ); - const dryRun = parseBooleanInput( - process.env.DRY_RUN_INPUT || - (context.payload.client_payload && context.payload.client_payload.dry_run), - false, - ); - const cleanupBranches = parseBooleanInput( - process.env.CLEANUP_BRANCHES_INPUT || - (context.payload.client_payload && context.payload.client_payload.cleanup_branches), - true, - ); - const retryHelpers = fs.existsSync(retryHelperPath) - ? require(retryHelperPath) - : { - withRetry: (fn) => fn(), - paginateWithRetry: (githubInstance, method, params) => - githubInstance.paginate(method, params), - }; - const { createTokenAwareRetry } = retryHelpers; - const { withRetry } = createTokenAwareRetry - ? await createTokenAwareRetry({ - github, - core, - env: process.env, - task: 'maint-71-merge-sync-prs', - capabilities: [ - 'pull-requests:read', - 'pull-requests:write', - 'checks:read', - 'contents:write', - ], - }) - : { github, withRetry: (fn) => fn(github) }; - const fallbackCheckDenylist = [ - 'Detect keepalive', - 'pr_meta', - 'resolve_pr', - 'Cleanup', - '${' + '{ matrix.', - 'matrix.python-version', - ]; - - async function getRequiredContexts({ owner, repo, branch }) { - try { - const { data: protection } = await withRetry((client) => - client.rest.repos.getBranchProtection({ - owner, - repo, - branch, - }), - ); - const requiredStatusChecks = protection?.required_status_checks || {}; - const requiredContexts = new Set(); - for (const contextName of requiredStatusChecks.contexts || []) { - const normalized = String(contextName || '').trim(); - if (normalized) { - requiredContexts.add(normalized); - } - } - for (const check of requiredStatusChecks.checks || []) { - const normalized = String(check?.context || '').trim(); - if (normalized) { - requiredContexts.add(normalized); - } - } - if (requiredContexts.size === 0) { - console.log( - `Branch protection for ${owner}/${repo}@${branch} has no required ` + - 'status checks; using denylist fallback', - ); - } - return requiredContexts; - } catch (error) { - const status = error?.status || error?.response?.status; - if (status === 403 || status === 404) { - console.log( - `Branch protection unavailable for ${owner}/${repo}@${branch} ` + - `(${status}); using denylist fallback`, - ); - return new Set(); - } - throw error; - } - } - - // Parse repos from previous step - const registeredRepos = '${{ steps.repos.outputs.list }}' - .split(',') - .map(r => r.trim()) - .filter(Boolean); - - // Determine which repos to process - const targetRepos = inputRepos === 'all' - ? registeredRepos - : inputRepos.split(',').map(r => r.trim()); - const requestedSyncHash = normalizeSyncHash( - process.env.SYNC_HASH_INPUT || - (context.payload.client_payload && context.payload.client_payload.sync_hash) || - '', - ); - const trustedSyncActors = process.env.TRUSTED_SYNC_ACTORS.split(',') - .map((actor) => actor.trim()) - .filter(Boolean); - - console.log(`Registered consumer repos: ${registeredRepos.join(', ')}`); - console.log(`Processing repos: ${targetRepos.join(', ')}`); - console.log(`Auto-merge: ${autoMerge}, Dry run: ${dryRun}`); - console.log(`Cleanup stale sync branches: ${cleanupBranches}\n`); - if (requestedSyncHash) { - console.log(`Target sync hash: ${requestedSyncHash}`); - } - - const results = []; - const canaryEvidence = []; - - function syncMetadata(pr) { - const match = String(pr.body || '').match( - //, - ); - if (!match) return null; - try { - return JSON.parse(match[1]); - } catch (_) { - return null; - } - } - - async function activeReviewThreadCount(owner, repo, number) { - try { - const data = await github.graphql( - `query($owner: String!, $repo: String!, $number: Int!) { - repository(owner: $owner, name: $repo) { - pullRequest(number: $number) { - reviewThreads(first: 100) { - pageInfo { hasNextPage } - nodes { isResolved isOutdated } - } - } - } - }`, - { owner, repo, number }, - ); - const reviewThreads = data.repository.pullRequest.reviewThreads; - if (reviewThreads.pageInfo.hasNextPage) { - core.warning( - `Review-thread pagination exceeded the safe evidence window for ${owner}/${repo}#${number}`, - ); - return -1; - } - return reviewThreads.nodes.filter( - (thread) => !thread.isResolved && !thread.isOutdated, - ).length; - } catch (error) { - core.warning(`Unable to read active review threads for ${owner}/${repo}#${number}: ${error}`); - return -1; - } - } - - for (const repoEntry of targetRepos) { - const [entryOwner, entryRepo] = repoEntry.includes('/') - ? repoEntry.split('/') - : [defaultOwner, repoEntry]; - const owner = entryOwner || defaultOwner; - const repo = entryRepo; - - console.log(`\n=== ${owner}/${repo} ===`); - - try { - // Find open sync PRs - const { data: prs } = await withRetry((client) => client.rest.pulls.list({ - owner, - repo, - state: 'open', - per_page: 20 - })); - - const syncPRs = prs.filter((pr) => isTrustedGeneratedDeliveryPr(pr, trustedSyncActors)); - - if (cleanupBranches) { - try { - const [branches, closedPRs] = await Promise.all([ - withRetry((client) => client.paginate(client.rest.repos.listBranches, { - owner, - repo, - per_page: 100, - })), - withRetry((client) => client.paginate(client.rest.pulls.list, { - owner, - repo, - state: 'closed', - per_page: 100, - })), - ]); - const branchesToDelete = collectDeletableSyncBranches({ - branches, - openPullRequests: syncPRs, - closedPullRequests: closedPRs, - }); - - if (branchesToDelete.length > 0) { - console.log( - `Found ${branchesToDelete.length} closed sync PR branch(es) to clean up`, - ); - } - - for (const branch of branchesToDelete) { - if (dryRun) { - console.log(`[DRY RUN] Would delete ${branch}`); - results.push({ - owner, - repo, - branch, - status: 'branch_deleted', - dry_run: true, - }); - continue; - } - - try { - await withRetry((client) => client.rest.git.deleteRef({ - owner, - repo, - ref: `heads/${branch}`, - })); - console.log(`✓ Deleted leftover branch ${branch}`); - results.push({ owner, repo, branch, status: 'branch_deleted' }); - } catch (branchErr) { - console.log( - `⚠ Could not delete leftover branch ${branch}: ${branchErr.message}`, - ); - results.push({ - owner, - repo, - branch, - status: 'branch_delete_failed', - error: branchErr.message, - }); - } - } - } catch (cleanupErr) { - console.log(`⚠ Sync branch cleanup failed: ${cleanupErr.message}`); - results.push({ - owner, - repo, - status: 'branch_delete_failed', - error: cleanupErr.message, - }); - } - } - - if (syncPRs.length === 0) { - console.log('No sync PRs found'); - results.push({ owner, repo, status: 'no_prs' }); - continue; - } - - let selection = selectMergeEligibleSyncPr(syncPRs, { - syncHash: requestedSyncHash, - now: new Date().toISOString(), - repository: `${owner}/${repo}`, - }); - if (selection.missingExpected) { - console.log( - `Expected sync PR branch ${selection.expectedBranch} was not found; leaving ` + - `${syncPRs.length} sync PRs untouched`, - ); - results.push({ - owner, - repo, - status: 'target_missing', - expected_branch: selection.expectedBranch, - open_sync_prs: syncPRs.map((item) => ({ - number: item.number, - branch: item.head.ref, - url: item.html_url, - })), - }); - continue; - } - - const { data: selectedHeadCommit } = await withRetry((client) => - client.rest.git.getCommit({ - owner, - repo, - commit_sha: selection.active.head.sha, - }), - ); - selection = selectMergeEligibleSyncPr(syncPRs, { - syncHash: requestedSyncHash, - now: new Date().toISOString(), - repository: `${owner}/${repo}`, - desiredTreeHash: selectedHeadCommit?.tree?.sha || '', - }); - - // If multiple sync PRs exist, close older ones as stale - if (selection.stale.length > 0) { - console.log( - `Found ${syncPRs.length} sync PRs - closing ` + - `${selection.stale.length} stale PRs`, - ); - - for (const stalePR of selection.stale) { - console.log(`\nClosing stale PR #${stalePR.number}: ${stalePR.title}`); - console.log(`Branch: ${stalePR.head.ref}, Created: ${stalePR.created_at}`); - - if (!dryRun) { - try { - // Close PR - await withRetry((client) => client.rest.pulls.update({ - owner, - repo, - pull_number: stalePR.number, - state: 'closed' - })); - console.log('✓ Closed'); - - // Delete branch - try { - await withRetry((client) => client.rest.git.deleteRef({ - owner, - repo, - ref: `heads/${stalePR.head.ref}` - })); - console.log('✓ Branch deleted'); - } catch (delErr) { - console.log(`⚠ Branch delete failed: ${delErr.message}`); - } - results.push({ - owner, - repo, - pr: stalePR.number, - branch: stalePR.head.ref, - status: 'stale_closed', - }); - } catch (staleErr) { - console.log(`✗ Stale close failed: ${staleErr.message}`); - results.push({ - owner, - repo, - pr: stalePR.number, - branch: stalePR.head.ref, - status: 'stale_close_failed', - error: staleErr.message, - }); - } - } else { - console.log('[DRY RUN] Would close and delete branch'); - results.push({ - owner, - repo, - pr: stalePR.number, - branch: stalePR.head.ref, - status: 'stale_closed', - dry_run: true, - }); - } - } - } - - if (!selection.eligibility?.eligible) { - const reason = selection.eligibility?.reason || 'missing_delivery_record'; - console.log(`Delivery contract blocks merge: ${reason}`); - const deliveryDisposition = reason === 'lease_expired' - ? 'expired' - : selection.deliveryRecord - ? 'superseded' - : 'owner-decision'; - const deliveryContext = { - owner, - repo, - pr: selection.active.number, - branch: selection.active.head.ref, - head_sha: selection.active.head.sha, - delivery_generation: selection.deliveryRecord?.generation || '', - delivery_lane: generatedDeliveryLane(selection.active.head.ref), - delivery_disposition: deliveryDisposition, - blocker_owner: deliveryDisposition === 'owner-decision' ? 'source' : 'maint-71', - next_command: deliveryDisposition === 'expired' - ? 'close-expired-delivery' - : deliveryDisposition === 'superseded' - ? 'close-or-refresh-delivery' - : 'attach-or-infer-delivery-record', - }; - if (deliveryDisposition === 'expired' || deliveryDisposition === 'superseded') { - if (!dryRun) { - await withRetry((client) => client.rest.pulls.update({ - owner, - repo, - pull_number: selection.active.number, - state: 'closed', - })); - } - results.push({ ...deliveryContext, status: 'stale_closed', dry_run: dryRun }); - continue; - } - results.push({ - ...deliveryContext, - status: 'delivery_contract_blocked', - delivery_reason: reason, - }); - continue; - } - - // Process the selected active PR - const pr = selection.active; - const metadata = syncMetadata(pr); - console.log(`\nProcessing active PR #${pr.number}: ${pr.title}`); - console.log(`Branch: ${pr.head.ref}`); - console.log(`Created: ${pr.created_at}`); - - // Check PR status - await withRetry((client) => client.rest.repos.getCombinedStatusForRef({ - owner, - repo, - ref: pr.head.sha - })); - - // Check check runs - const { data: checkRuns } = await withRetry((client) => - client.rest.checks.listForRef({ - owner, - repo, - ref: pr.head.sha - }), - ); - - const allChecks = checkRuns.check_runs || []; - const requiredContexts = await getRequiredContexts({ - owner, - repo, - branch: pr.base.ref, - }); - const classification = classifySyncPrChecks({ - checkRuns: allChecks, - requiredContexts, - fallbackDenylist: fallbackCheckDenylist, - }); - const gatingChecks = selectSyncPrGatingChecks({ - checkRuns: allChecks, - requiredContexts, - fallbackDenylist: fallbackCheckDenylist, - }); - const checkGateMode = - requiredContexts.size > 0 ? 'required-contexts' : 'denylist-fallback'; - const failedChecks = classification.failed; - const pendingChecks = classification.pending; - const activeReviewThreads = await activeReviewThreadCount( - owner, - repo, - pr.number, - ); - const deliveryState = classifyGeneratedPr({ - pr, - checkState: classification, - activeReviewThreadCount: activeReviewThreads, - now: new Date().toISOString(), - }); - const deliveryContext = { - owner, - repo, - pr: pr.number, - branch: pr.head.ref, - head_sha: pr.head.sha, - delivery_generation: selection.deliveryRecord?.generation || '', - delivery_lane: generatedDeliveryLane(pr.head.ref), - delivery_disposition: deliveryState.disposition, - blocker_owner: deliveryState.blocker_owner, - next_command: deliveryState.next_command, - }; - - if (metadata?.sync_phase === 'canary' && metadata?.plan_id) { - canaryEvidence.push({ - repo: `${owner}/${repo}`, - plan_id: metadata.plan_id, - pr: pr.number, - required_check_state: - classification.status === 'ready' ? 'success' : classification.status, - active_review_thread_count: activeReviewThreads, - }); - } - - console.log( - `Checks (${checkGateMode}): ${gatingChecks.length} gating, ` + - `${failedChecks.length} failed, ${pendingChecks.length} pending`, - ); - - if (deliveryState.disposition === 'review-blocked') { - console.log(`Active review threads block merge: ${activeReviewThreads}`); - results.push({ - ...deliveryContext, - status: 'review_blocked', - active_review_thread_count: activeReviewThreads, - }); - continue; - } - - if (classification.status === 'checks_failed') { - console.log('Failed checks:'); - failedChecks.forEach(c => console.log(` - ${c.name}: ${c.conclusion}`)); - results.push({ - ...deliveryContext, - status: 'checks_failed', - failed_checks: failedChecks.map((check) => ({ - name: check.name, - conclusion: check.conclusion, - status: check.status, - })), - }); - continue; - } - - if (classification.status === 'checks_pending') { - console.log('Waiting for checks to complete'); - results.push({ - ...deliveryContext, - status: 'checks_pending', - pending_checks: pendingChecks.map((check) => ({ - name: check.name, - conclusion: check.conclusion, - status: check.status, - })), - }); - continue; - } - - // All checks passed - if (!autoMerge) { - console.log('✓ Ready to merge (auto-merge disabled)'); - results.push({ - ...deliveryContext, - status: 'ready', - }); - continue; - } - - if (dryRun) { - console.log('✓ Would merge (dry run)'); - results.push({ - ...deliveryContext, - status: 'dry_run_merge', - }); - continue; - } - - // Merge the PR - try { - await assertRuntimeAcMergeAllowed({ - github, - core, - owner, - repo, - prNumber: pr.number, - withRetry, - source: 'maint-71-merge-sync-prs', - }); - } catch (guardError) { - const message = String(guardError?.message || guardError); - console.log(`Runtime AC merge guard blocked PR #${pr.number}: ${message}`); - results.push({ - ...deliveryContext, - status: 'merge_blocked_runtime_ac', - error: message, - }); - continue; - } - - try { - const mergeMethods = ['merge', 'squash', 'rebase']; - let merged = false; - let lastError = null; - - for (const merge_method of mergeMethods) { - try { - await withRetry((client) => client.rest.pulls.merge({ - owner, - repo, - pull_number: pr.number, - merge_method, - commit_title: pr.title, - commit_message: - `Automated merge of sync PR\n\n` + - `Sync hash: ${pr.head.ref.split('-').pop()}` - })); - console.log(`✓ Merged successfully (method=${merge_method})`); - merged = true; - break; - } catch (e) { - lastError = e; - const message = String(e?.message || 'unknown error'); - console.log(`⚠ Merge attempt failed (method=${merge_method}): ${message}`); - if (!message.toLowerCase().includes('repository rule violations')) { - break; - } - } - } - - if (!merged) { - throw lastError || new Error('Merge failed'); - } - - // Delete the branch - try { - await withRetry((client) => client.rest.git.deleteRef({ - owner, - repo, - ref: `heads/${pr.head.ref}` - })); - console.log('✓ Branch deleted'); - results.push({ - ...deliveryContext, - status: 'branch_deleted', - }); - } catch (e) { - console.log(`⚠ Could not delete branch: ${e.message}`); - results.push({ - ...deliveryContext, - status: 'branch_delete_failed', - error: e.message, - }); - } - - results.push({ - ...deliveryContext, - status: 'merged', - }); - } catch (e) { - console.log(`✗ Merge failed: ${e.message}`); - results.push({ - ...deliveryContext, - status: 'merge_failed', - error: e.message, - }); - } - } catch (e) { - console.log(`✗ Error processing ${repo}: ${e.message}`); - results.push({ owner, repo, status: 'error', error: e.message }); - } - } - - // Summary - console.log('\n=== Summary ==='); - console.log(JSON.stringify(results, null, 2)); - const report = buildMergeReport({ - results, - registeredRepos, - targetRepos, - autoMerge, - dryRun, - syncHash: requestedSyncHash, - run: { - repository: `${context.repo.owner}/${context.repo.repo}`, - run_id: context.runId, - run_number: context.runNumber, - workflow: context.workflow, - ref: context.ref, - sha: context.sha, - }, - }); - const reportPath = process.env.SYNC_PR_MERGE_REPORT_JSON; - fs.mkdirSync(path.dirname(reportPath), { recursive: true }); - fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8'); - fs.writeFileSync( - 'artifacts/sync-canary-evidence.json', - `${JSON.stringify({ - schema: 'workflows.consumer-sync-canary-evidence/v1', - version: 1, - results: canaryEvidence, - }, null, 2)}\n`, - 'utf8', - ); - await core.summary.addRaw(buildMarkdownSummary(report)).write(); - if (!dryRun && report.handoff_records.length > 0) { - // Fleet-wide refresh: a targeted Maint 71 repos filter must not cause - // Maint 82 to stale unscanned repos. Dispatch is best-effort so a - // permissions/transient failure does not fail the reconciler run. - try { - await withRetry((client) => client.rest.repos.createDispatchEvent({ - owner: context.repo.owner, - repo: context.repo.repo, - event_type: 'sync-dependabot-campaign', - client_payload: { - repos: registeredRepos.join(','), - delivery_handoff_records: report.handoff_records, - }, - })); - } catch (dispatchError) { - core.notice( - `Maint 71 handoff dispatch failed (non-blocking): ${dispatchError.message}`, - ); - } - } - - const merged = report.summary.merged; - const stale = report.summary.stale_closed; - const branchesDeleted = report.summary.branch_deleted; - // A consumer's own red CI (checks_failed) is outside this workflow's control, - // so it must NOT fail the fleet janitor run — otherwise one red consumer turns - // every scheduled flush red and forces re-runs. Only genuine sync-system action - // failures (merge/cleanup/missing-target) are blocking; consumer CI health is - // surfaced via the merge report and Health 68 instead. - const blockingFailures = results.filter( - (r) => - r.status === 'merge_failed' || - r.status === 'stale_close_failed' || - r.status === 'branch_delete_failed' || - r.status === 'target_missing', - ); - const checksFailed = results.filter((r) => r.status === 'checks_failed'); - if (checksFailed.length > 0) { - core.notice( - `${checksFailed.length} consumer sync PR(s) have failing checks (consumer CI); ` + - `left open, not treated as a janitor failure.`, - ); - } - const failed = dryRun ? 0 : blockingFailures.length; - const pending = report.summary.checks_pending; - const ready = report.summary.ready; - - console.log(`\nMerged: ${merged}`); - console.log(`Stale closed: ${stale}`); - console.log(`Leftover branches deleted: ${branchesDeleted}`); - console.log(`Failed: ${failed}`); - console.log(`Pending: ${pending}`); - console.log(`Ready (not auto-merged): ${ready}`); - if (dryRun && blockingFailures.length > 0) { - core.notice( - `Dry run observed ${blockingFailures.length} blocking result(s); ` + - 'report-only mode remains successful.', - ); - } - - if (failed > 0) { - core.setFailed(`${failed} PRs failed to merge`); - } + const { run } = require("./.github/scripts/maint71_merge_sync_prs.js"); + await run({ github, context, core }); - name: Upload sync PR merge report if: always() diff --git a/tests/workflows/test_workflow_agents_consolidation.py b/tests/workflows/test_workflow_agents_consolidation.py index 794a2354c..eacc61a8c 100644 --- a/tests/workflows/test_workflow_agents_consolidation.py +++ b/tests/workflows/test_workflow_agents_consolidation.py @@ -110,6 +110,9 @@ def test_agents_orchestrator_exposes_dry_run_toggle(): def test_external_merge_lanes_require_runtime_ac_guard(): main_text = (WORKFLOWS_DIR / "reusable-70-orchestrator-main.yml").read_text(encoding="utf-8") maint_text = (WORKFLOWS_DIR / "maint-71-merge-sync-prs.yml").read_text(encoding="utf-8") + maint_executor_text = Path(".github/scripts/maint71_merge_sync_prs.js").read_text( + encoding="utf-8" + ) manifest_text = Path(".github/sync-manifest.yml").read_text(encoding="utf-8") template_guard = Path("templates/consumer-repo/.github/scripts/runtime_ac_merge_guard.js") template_followups_text = Path( @@ -120,9 +123,10 @@ def test_external_merge_lanes_require_runtime_ac_guard(): assert "assertRuntimeAcMergeAllowed" in main_text assert "reusable-70-orchestrator-main automerge sweep" in main_text - assert "runtime_ac_merge_guard.js" in maint_text - assert "assertRuntimeAcMergeAllowed" in maint_text - assert "merge_blocked_runtime_ac" in maint_text + assert "maint71_merge_sync_prs.js" in maint_text + assert "runtime_ac_merge_guard.js" in maint_executor_text + assert "assertRuntimeAcMergeAllowed" in maint_executor_text + assert "merge_blocked_runtime_ac" in maint_executor_text assert ".github/scripts/runtime_ac_merge_guard.js" in manifest_text assert template_guard.exists(), "Consumer template must include the guard helper" @@ -305,27 +309,31 @@ def test_consumer_sync_drift_uploads_machine_readable_report(): def test_merge_sync_prs_uploads_machine_readable_report_and_hash_input(): text = (WORKFLOWS_DIR / "maint-71-merge-sync-prs.yml").read_text(encoding="utf-8") + executor_text = Path(".github/scripts/maint71_merge_sync_prs.js").read_text( + encoding="utf-8" + ) + contract_text = text + executor_text assert "sync_hash:" in text, "Maint 71 must expose a target sync hash input" assert ( - "sync_pr_merge_contract.js" in text + "sync_pr_merge_contract.js" in contract_text ), "Maint 71 must use the structured sync PR merge contract helper" assert ( - "selectMergeEligibleSyncPr" in text + "selectMergeEligibleSyncPr" in contract_text ), "Maint 71 must select the active PR with the lease-aware merge contract" assert ( - "isTrustedGeneratedDeliveryPr" in text + "isTrustedGeneratedDeliveryPr" in contract_text ), "Maint 71 must route both sync and dev-tool generated deliveries through the contract" - assert "close-expired-delivery" in text and "close-or-refresh-delivery" in text + assert "close-expired-delivery" in contract_text and "close-or-refresh-delivery" in contract_text assert "cleanup_branches:" in text, "Maint 71 must expose sync branch cleanup control" assert ( - "collectDeletableSyncBranches" in text and "branch_delete_failed" in text + "collectDeletableSyncBranches" in contract_text and "branch_delete_failed" in contract_text ), "Maint 71 must delete leftover sync branches and report deletion failures" assert ( - "parseBooleanInput" in text and "AUTO_MERGE_INPUT" in text + "parseBooleanInput" in contract_text and "AUTO_MERGE_INPUT" in text ), "Maint 71 must preserve explicit false boolean inputs" assert "SYNC_PR_MERGE_REPORT_JSON" in text, "Maint 71 must configure a JSON merge report path" assert ( - "report-only mode remains successful" in text + "report-only mode remains successful" in contract_text ), "Maint 71 dry-run mode must report blocking statuses without failing the workflow" assert ( "sync-pr-merge-report" in text From 06af76bd8a18d57789984180564ef9a9a81ddf72 Mon Sep 17 00:00:00 2001 From: Codex Automation Date: Thu, 6 Aug 2026 12:38:42 -0500 Subject: [PATCH 2/9] fix: resolve maint71 helper imports relative to extracted module Node resolved the post-extraction requires under a nested .github/scripts path, so Maint 71 failed with MODULE_NOT_FOUND before any repo work. Align sibling requires, __dirname retry helper lookup, retry fallback arity, and TRUSTED_SYNC_ACTORS fail-closed parsing. Co-authored-by: Cursor --- .github/scripts/maint71_merge_sync_prs.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/.github/scripts/maint71_merge_sync_prs.js b/.github/scripts/maint71_merge_sync_prs.js index 6d3cde8ea..64bb78c77 100644 --- a/.github/scripts/maint71_merge_sync_prs.js +++ b/.github/scripts/maint71_merge_sync_prs.js @@ -4,7 +4,9 @@ async function run({ github, context, core }) { const defaultOwner = context.repo.owner; const fs = require('fs'); const path = require('path'); - const retryHelperPath = './.github/scripts/github-api-with-retry.js'; + // Sibling-relative paths: this module lives under .github/scripts/, so + // require('./.github/scripts/...') would resolve to a nested non-existent path. + const retryHelperPath = path.join(__dirname, 'github-api-with-retry.js'); const { buildMarkdownSummary, buildMergeReport, @@ -17,10 +19,10 @@ async function run({ github, context, core }) { isTrustedGeneratedDeliveryPr, selectMergeEligibleSyncPr, selectSyncPrGatingChecks, - } = require('./.github/scripts/sync_pr_merge_contract.js'); + } = require('./sync_pr_merge_contract.js'); const { assertRuntimeAcMergeAllowed, - } = require('./.github/scripts/runtime_ac_merge_guard.js'); + } = require('./runtime_ac_merge_guard.js'); // Support repository_dispatch (no inputs) with sensible defaults const inputRepos = process.env.REPOS_INPUT || @@ -44,7 +46,8 @@ async function run({ github, context, core }) { const retryHelpers = fs.existsSync(retryHelperPath) ? require(retryHelperPath) : { - withRetry: (fn) => fn(), + // Call sites pass (client) => client.rest...; match that contract. + withRetry: (fn) => fn(github), paginateWithRetry: (githubInstance, method, params) => githubInstance.paginate(method, params), }; @@ -130,7 +133,8 @@ async function run({ github, context, core }) { (context.payload.client_payload && context.payload.client_payload.sync_hash) || '', ); - const trustedSyncActors = process.env.TRUSTED_SYNC_ACTORS.split(',') + const trustedSyncActors = String(process.env.TRUSTED_SYNC_ACTORS || '') + .split(',') .map((actor) => actor.trim()) .filter(Boolean); From c4285072f257d3ca0274ac01fb41692db3f622ec Mon Sep 17 00:00:00 2001 From: Codex Automation Date: Thu, 6 Aug 2026 12:40:52 -0500 Subject: [PATCH 3/9] fix: harden maint71 pagination and non-blocking branch cleanup Paginate open PRs and check runs, fold combined statuses into the merge gate with fail-closed missing required contexts, and stop treating leftover branch-delete failures as merge failures. Co-authored-by: Cursor --- .github/scripts/maint71_merge_sync_prs.js | 92 +++++++++++++++++------ 1 file changed, 68 insertions(+), 24 deletions(-) diff --git a/.github/scripts/maint71_merge_sync_prs.js b/.github/scripts/maint71_merge_sync_prs.js index 64bb78c77..0454680e4 100644 --- a/.github/scripts/maint71_merge_sync_prs.js +++ b/.github/scripts/maint71_merge_sync_prs.js @@ -202,13 +202,15 @@ async function run({ github, context, core }) { console.log(`\n=== ${owner}/${repo} ===`); try { - // Find open sync PRs - const { data: prs } = await withRetry((client) => client.rest.pulls.list({ - owner, - repo, - state: 'open', - per_page: 20 - })); + // Find open sync PRs (paginate — first page alone can miss open sync heads). + const prs = await withRetry((client) => + client.paginate(client.rest.pulls.list, { + owner, + repo, + state: 'open', + per_page: 100, + }), + ); const syncPRs = prs.filter((pr) => isTrustedGeneratedDeliveryPr(pr, trustedSyncActors)); @@ -229,7 +231,8 @@ async function run({ github, context, core }) { ]); const branchesToDelete = collectDeletableSyncBranches({ branches, - openPullRequests: syncPRs, + // Pass every open PR so an open sync head beyond the filtered set is never deleted. + openPullRequests: prs, closedPullRequests: closedPRs, }); @@ -444,33 +447,66 @@ async function run({ github, context, core }) { console.log(`Branch: ${pr.head.ref}`); console.log(`Created: ${pr.created_at}`); - // Check PR status - await withRetry((client) => client.rest.repos.getCombinedStatusForRef({ - owner, - repo, - ref: pr.head.sha - })); - - // Check check runs - const { data: checkRuns } = await withRetry((client) => - client.rest.checks.listForRef({ + // Combined legacy statuses + every check-run page (paginate returns a flat array). + const { data: combinedStatus } = await withRetry((client) => + client.rest.repos.getCombinedStatusForRef({ owner, repo, - ref: pr.head.sha + ref: pr.head.sha, }), ); - - const allChecks = checkRuns.check_runs || []; + const paginatedCheckRuns = await withRetry((client) => + client.paginate(client.rest.checks.listForRef, { + owner, + repo, + ref: pr.head.sha, + per_page: 100, + }), + ); + const statusAsChecks = (combinedStatus.statuses || []).map((status) => { + const state = String(status.state || '').toLowerCase(); + return { + name: status.context, + status: state === 'pending' ? 'in_progress' : 'completed', + conclusion: + state === 'success' + ? 'success' + : state === 'pending' + ? null + : 'failure', + }; + }); + const checkNames = new Set( + paginatedCheckRuns.map((check) => String(check?.name || '').trim()).filter(Boolean), + ); + const allChecks = [ + ...paginatedCheckRuns, + ...statusAsChecks.filter((status) => !checkNames.has(String(status.name || '').trim())), + ]; const requiredContexts = await getRequiredContexts({ owner, repo, branch: pr.base.ref, }); - const classification = classifySyncPrChecks({ + let classification = classifySyncPrChecks({ checkRuns: allChecks, requiredContexts, fallbackDenylist: fallbackCheckDenylist, }); + // Fail closed: a required context absent from both checks and statuses is not "ready". + if (requiredContexts.size > 0 && classification.status === 'ready') { + const seenNames = new Set( + allChecks.map((check) => String(check?.name || '').trim()).filter(Boolean), + ); + const missingRequired = [...requiredContexts].filter((ctx) => !seenNames.has(ctx)); + if (missingRequired.length > 0) { + classification = { + status: 'checks_pending', + failed: [], + pending: missingRequired.map((name) => ({ name, status: 'queued' })), + }; + } + } const gatingChecks = selectSyncPrGatingChecks({ checkRuns: allChecks, requiredContexts, @@ -738,9 +774,15 @@ async function run({ github, context, core }) { (r) => r.status === 'merge_failed' || r.status === 'stale_close_failed' || - r.status === 'branch_delete_failed' || r.status === 'target_missing', ); + const branchDeleteFailures = results.filter((r) => r.status === 'branch_delete_failed'); + if (branchDeleteFailures.length > 0) { + core.notice( + `${branchDeleteFailures.length} sync branch(es) could not be deleted; ` + + 'the next scheduled run retries the cleanup.', + ); + } const checksFailed = results.filter((r) => r.status === 'checks_failed'); if (checksFailed.length > 0) { core.notice( @@ -766,7 +808,9 @@ async function run({ github, context, core }) { } if (failed > 0) { - core.setFailed(`${failed} PRs failed to merge`); + core.setFailed( + `${failed} blocking sync-system failure(s): merge_failed, stale_close_failed, or target_missing`, + ); } } From 261540bdb5c1d37bc4d15885da46760edbdd0e47 Mon Sep 17 00:00:00 2001 From: Codex Automation Date: Thu, 6 Aug 2026 12:41:43 -0500 Subject: [PATCH 4/9] fix: report real sync hash in maint71 merge commit message Co-authored-by: Cursor --- .github/scripts/maint71_merge_sync_prs.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/scripts/maint71_merge_sync_prs.js b/.github/scripts/maint71_merge_sync_prs.js index 0454680e4..1ec4b048a 100644 --- a/.github/scripts/maint71_merge_sync_prs.js +++ b/.github/scripts/maint71_merge_sync_prs.js @@ -651,7 +651,8 @@ async function run({ github, context, core }) { commit_title: pr.title, commit_message: `Automated merge of sync PR\n\n` + - `Sync hash: ${pr.head.ref.split('-').pop()}` + `Sync hash: ${requestedSyncHash || metadata?.sync_hash || 'unknown'}\n` + + `Delivery generation: ${selection.deliveryRecord?.generation || 'unknown'}` })); console.log(`✓ Merged successfully (method=${merge_method})`); merged = true; From 7f1a32b9854daf6aeba51e5319036d57ca3cf5b6 Mon Sep 17 00:00:00 2001 From: Codex Automation Date: Thu, 6 Aug 2026 13:24:51 -0500 Subject: [PATCH 5/9] fix: harden maint71 stale delivery reporting --- .../__tests__/sync_pr_merge_contract.test.js | 74 +++++++++++++++++++ .github/scripts/maint71_merge_sync_prs.js | 18 ++++- 2 files changed, 90 insertions(+), 2 deletions(-) diff --git a/.github/scripts/__tests__/sync_pr_merge_contract.test.js b/.github/scripts/__tests__/sync_pr_merge_contract.test.js index 3982f207f..eb564b998 100644 --- a/.github/scripts/__tests__/sync_pr_merge_contract.test.js +++ b/.github/scripts/__tests__/sync_pr_merge_contract.test.js @@ -2,6 +2,9 @@ const test = require('node:test'); const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); const { buildMarkdownSummary, @@ -21,6 +24,7 @@ const { syncBranchForHash, } = require('../sync_pr_merge_contract'); const { assertRuntimeAcMergeAllowed } = require('../runtime_ac_merge_guard'); +const { run } = require('../maint71_merge_sync_prs'); const pr = (number, ref, created_at) => ({ number, @@ -41,6 +45,76 @@ const checkRun = ({ started_at, }); +test('maint71 run writes reports and records a no-PR result with fake action clients', async () => { + const originalCwd = process.cwd(); + const originalEnv = { + REGISTERED_REPOS_INPUT: process.env.REGISTERED_REPOS_INPUT, + CLEANUP_BRANCHES_INPUT: process.env.CLEANUP_BRANCHES_INPUT, + DRY_RUN_INPUT: process.env.DRY_RUN_INPUT, + AUTO_MERGE_INPUT: process.env.AUTO_MERGE_INPUT, + SYNC_PR_MERGE_REPORT_JSON: process.env.SYNC_PR_MERGE_REPORT_JSON, + }; + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'maint71-run-')); + const reportPath = path.join(tempDir, 'reports', 'merge-report.json'); + const summaries = []; + const failures = []; + const paginateCalls = []; + const github = { + paginate: async (method, params) => { + paginateCalls.push({ method, params }); + return []; + }, + rest: { + pulls: { list: () => {} }, + repos: { createDispatchEvent: () => {} }, + }, + }; + const core = { + addRaw: () => ({ write: async () => {} }), + notice: (message) => summaries.push(message), + setFailed: (message) => failures.push(message), + warning: (message) => summaries.push(message), + summary: { addRaw: () => ({ write: async () => {} }) }, + }; + + try { + process.chdir(tempDir); + process.env.REGISTERED_REPOS_INPUT = 'stranske/Ready'; + process.env.CLEANUP_BRANCHES_INPUT = 'false'; + process.env.DRY_RUN_INPUT = 'true'; + process.env.AUTO_MERGE_INPUT = 'false'; + process.env.SYNC_PR_MERGE_REPORT_JSON = reportPath; + + await run({ + github, + core, + context: { + repo: { owner: 'stranske', repo: 'Workflows' }, + payload: {}, + runId: 1, + runNumber: 1, + workflow: 'Maint 71', + ref: 'refs/heads/main', + sha: 'abc', + }, + }); + + const report = JSON.parse(fs.readFileSync(reportPath, 'utf8')); + assert.equal(paginateCalls.length, 1); + assert.equal(paginateCalls[0].params.repo, 'Ready'); + assert.equal(report.summary.no_prs, 1); + assert.equal(fs.existsSync(path.join(tempDir, 'artifacts', 'sync-canary-evidence.json')), true); + assert.deepEqual(failures, []); + } finally { + process.chdir(originalCwd); + for (const [key, value] of Object.entries(originalEnv)) { + if (value === undefined) delete process.env[key]; + else process.env[key] = value; + } + fs.rmSync(tempDir, { recursive: true, force: true }); + } +}); + test('normalizeSyncHash accepts raw hashes and branch names', () => { assert.equal(normalizeSyncHash('5108b94a2435'), '5108b94a2435'); assert.equal(normalizeSyncHash('sync/workflows-5108b94a2435'), '5108b94a2435'); diff --git a/.github/scripts/maint71_merge_sync_prs.js b/.github/scripts/maint71_merge_sync_prs.js index 1ec4b048a..8ed5994f9 100644 --- a/.github/scripts/maint71_merge_sync_prs.js +++ b/.github/scripts/maint71_merge_sync_prs.js @@ -413,6 +413,7 @@ async function run({ github, context, core }) { delivery_generation: selection.deliveryRecord?.generation || '', delivery_lane: generatedDeliveryLane(selection.active.head.ref), delivery_disposition: deliveryDisposition, + delivery_reason: reason, blocker_owner: deliveryDisposition === 'owner-decision' ? 'source' : 'maint-71', next_command: deliveryDisposition === 'expired' ? 'close-expired-delivery' @@ -422,6 +423,17 @@ async function run({ github, context, core }) { }; if (deliveryDisposition === 'expired' || deliveryDisposition === 'superseded') { if (!dryRun) { + await withRetry((client) => client.rest.issues.createComment({ + owner, + repo, + issue_number: selection.active.number, + body: [ + 'Closing this generated delivery as no longer current.', + `delivery_reason: ${reason}`, + `delivery_disposition: ${deliveryDisposition}`, + `next_command: ${deliveryContext.next_command}`, + ].join('\n'), + })); await withRetry((client) => client.rest.pulls.update({ owner, repo, @@ -729,11 +741,13 @@ async function run({ github, context, core }) { sha: context.sha, }, }); - const reportPath = process.env.SYNC_PR_MERGE_REPORT_JSON; + const reportPath = process.env.SYNC_PR_MERGE_REPORT_JSON || 'artifacts/sync-pr-merge-report.json'; + const canaryEvidencePath = 'artifacts/sync-canary-evidence.json'; fs.mkdirSync(path.dirname(reportPath), { recursive: true }); + fs.mkdirSync(path.dirname(canaryEvidencePath), { recursive: true }); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8'); fs.writeFileSync( - 'artifacts/sync-canary-evidence.json', + canaryEvidencePath, `${JSON.stringify({ schema: 'workflows.consumer-sync-canary-evidence/v1', version: 1, From 16189d2a8aef1bdfb919c2d7790ae41a55917698 Mon Sep 17 00:00:00 2001 From: Codex Automation Date: Thu, 6 Aug 2026 14:39:00 -0500 Subject: [PATCH 6/9] ci: retrigger Gate via pull_request synchronize Empty commit so Gate runs under concurrency group pr-2965-gate. Prior workflow_dispatch runs stayed pending/queued with zero jobs. Co-authored-by: Cursor From 7ba1e5731582eab986f78be8d5030a8d38dc018a Mon Sep 17 00:00:00 2001 From: Codex Automation Date: Thu, 6 Aug 2026 17:37:49 -0500 Subject: [PATCH 7/9] fix(test): point Maint 71 contracts at externalized executor Gate failed because campaign/canary contract tests still asserted against the YAML after the merge script moved to maint71_merge_sync_prs.js; update those assertions and black-format the consolidation helper test. Co-authored-by: Cursor --- .../test_maint82_sync_campaign_contract.py | 13 +++++++++---- tests/workflows/test_sync_manifest_delivery.py | 8 +++++++- .../workflows/test_workflow_agents_consolidation.py | 8 ++++---- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/tests/workflows/test_maint82_sync_campaign_contract.py b/tests/workflows/test_maint82_sync_campaign_contract.py index 27012237a..d8c406272 100644 --- a/tests/workflows/test_maint82_sync_campaign_contract.py +++ b/tests/workflows/test_maint82_sync_campaign_contract.py @@ -39,13 +39,18 @@ def test_campaign_refresh_consumes_maint71_delivery_handoffs(): def test_maint71_dispatches_machine_readable_handoffs_to_campaign(): + # Maint 71 externalized the merge executor into maint71_merge_sync_prs.js + # (expression-size limit). Contract assertions must follow the executable. workflow = MERGE_WORKFLOW.read_text(encoding="utf-8") + executor = Path(".github/scripts/maint71_merge_sync_prs.js").read_text(encoding="utf-8") + contract = workflow + executor - assert "event_type: 'sync-dependabot-campaign'" in workflow - assert "delivery_handoff_records: report.handoff_records" in workflow + assert "maint71_merge_sync_prs.js" in workflow + assert "event_type: 'sync-dependabot-campaign'" in contract + assert "delivery_handoff_records: report.handoff_records" in contract # Targeted Maint 71 runs must still refresh the full registered fleet. - assert "repos: registeredRepos.join(',')" in workflow - assert "Maint 71 handoff dispatch failed (non-blocking)" in workflow + assert "repos: registeredRepos.join(',')" in contract + assert "Maint 71 handoff dispatch failed (non-blocking)" in contract def test_campaign_workflow_bot_agnostic_identity(): diff --git a/tests/workflows/test_sync_manifest_delivery.py b/tests/workflows/test_sync_manifest_delivery.py index e75c7a3f6..9e12a997e 100644 --- a/tests/workflows/test_sync_manifest_delivery.py +++ b/tests/workflows/test_sync_manifest_delivery.py @@ -246,9 +246,15 @@ def test_sync_fanout_is_canary_gated_and_promotion_is_plan_bound() -> None: def test_maint_71_emits_canary_evidence_with_review_debt() -> None: - source = (REPO_ROOT / ".github" / "workflows" / "maint-71-merge-sync-prs.yml").read_text( + # Canary evidence fields live in the externalized Maint 71 executor JS; + # the workflow only uploads the artifact path. + workflow = (REPO_ROOT / ".github" / "workflows" / "maint-71-merge-sync-prs.yml").read_text( encoding="utf-8" ) + executor = (REPO_ROOT / ".github" / "scripts" / "maint71_merge_sync_prs.js").read_text( + encoding="utf-8" + ) + source = workflow + executor assert "sync-canary-evidence.json" in source assert "active_review_thread_count" in source diff --git a/tests/workflows/test_workflow_agents_consolidation.py b/tests/workflows/test_workflow_agents_consolidation.py index eacc61a8c..e336ceec5 100644 --- a/tests/workflows/test_workflow_agents_consolidation.py +++ b/tests/workflows/test_workflow_agents_consolidation.py @@ -309,9 +309,7 @@ def test_consumer_sync_drift_uploads_machine_readable_report(): def test_merge_sync_prs_uploads_machine_readable_report_and_hash_input(): text = (WORKFLOWS_DIR / "maint-71-merge-sync-prs.yml").read_text(encoding="utf-8") - executor_text = Path(".github/scripts/maint71_merge_sync_prs.js").read_text( - encoding="utf-8" - ) + executor_text = Path(".github/scripts/maint71_merge_sync_prs.js").read_text(encoding="utf-8") contract_text = text + executor_text assert "sync_hash:" in text, "Maint 71 must expose a target sync hash input" assert ( @@ -323,7 +321,9 @@ def test_merge_sync_prs_uploads_machine_readable_report_and_hash_input(): assert ( "isTrustedGeneratedDeliveryPr" in contract_text ), "Maint 71 must route both sync and dev-tool generated deliveries through the contract" - assert "close-expired-delivery" in contract_text and "close-or-refresh-delivery" in contract_text + assert ( + "close-expired-delivery" in contract_text and "close-or-refresh-delivery" in contract_text + ) assert "cleanup_branches:" in text, "Maint 71 must expose sync branch cleanup control" assert ( "collectDeletableSyncBranches" in contract_text and "branch_delete_failed" in contract_text From e47f0d190d177f169104bb97224b1a3ec1b6531b Mon Sep 17 00:00:00 2001 From: Codex Automation Date: Thu, 6 Aug 2026 18:24:51 -0500 Subject: [PATCH 8/9] test: separate Maint 71 source contracts --- .../test_maint82_sync_campaign_contract.py | 10 ++++------ tests/workflows/test_sync_manifest_delivery.py | 10 ++++------ .../test_workflow_agents_consolidation.py | 18 ++++++++---------- 3 files changed, 16 insertions(+), 22 deletions(-) diff --git a/tests/workflows/test_maint82_sync_campaign_contract.py b/tests/workflows/test_maint82_sync_campaign_contract.py index d8c406272..b5e229de1 100644 --- a/tests/workflows/test_maint82_sync_campaign_contract.py +++ b/tests/workflows/test_maint82_sync_campaign_contract.py @@ -43,14 +43,12 @@ def test_maint71_dispatches_machine_readable_handoffs_to_campaign(): # (expression-size limit). Contract assertions must follow the executable. workflow = MERGE_WORKFLOW.read_text(encoding="utf-8") executor = Path(".github/scripts/maint71_merge_sync_prs.js").read_text(encoding="utf-8") - contract = workflow + executor - assert "maint71_merge_sync_prs.js" in workflow - assert "event_type: 'sync-dependabot-campaign'" in contract - assert "delivery_handoff_records: report.handoff_records" in contract + assert "event_type: 'sync-dependabot-campaign'" in executor + assert "delivery_handoff_records: report.handoff_records" in executor # Targeted Maint 71 runs must still refresh the full registered fleet. - assert "repos: registeredRepos.join(',')" in contract - assert "Maint 71 handoff dispatch failed (non-blocking)" in contract + assert "repos: registeredRepos.join(',')" in executor + assert "Maint 71 handoff dispatch failed (non-blocking)" in executor def test_campaign_workflow_bot_agnostic_identity(): diff --git a/tests/workflows/test_sync_manifest_delivery.py b/tests/workflows/test_sync_manifest_delivery.py index 9e12a997e..92ca2e9b5 100644 --- a/tests/workflows/test_sync_manifest_delivery.py +++ b/tests/workflows/test_sync_manifest_delivery.py @@ -254,12 +254,10 @@ def test_maint_71_emits_canary_evidence_with_review_debt() -> None: executor = (REPO_ROOT / ".github" / "scripts" / "maint71_merge_sync_prs.js").read_text( encoding="utf-8" ) - source = workflow + executor - - assert "sync-canary-evidence.json" in source - assert "active_review_thread_count" in source - assert "required_check_state" in source - assert "plan_id" in source + assert "sync-canary-evidence.json" in workflow + assert "active_review_thread_count" in executor + assert "required_check_state" in executor + assert "plan_id" in executor def test_maint68_refreshes_only_a_same_base_and_tree_delivery_attempt() -> None: diff --git a/tests/workflows/test_workflow_agents_consolidation.py b/tests/workflows/test_workflow_agents_consolidation.py index e336ceec5..35ec09e7d 100644 --- a/tests/workflows/test_workflow_agents_consolidation.py +++ b/tests/workflows/test_workflow_agents_consolidation.py @@ -310,30 +310,28 @@ def test_consumer_sync_drift_uploads_machine_readable_report(): def test_merge_sync_prs_uploads_machine_readable_report_and_hash_input(): text = (WORKFLOWS_DIR / "maint-71-merge-sync-prs.yml").read_text(encoding="utf-8") executor_text = Path(".github/scripts/maint71_merge_sync_prs.js").read_text(encoding="utf-8") - contract_text = text + executor_text assert "sync_hash:" in text, "Maint 71 must expose a target sync hash input" assert ( - "sync_pr_merge_contract.js" in contract_text + "sync_pr_merge_contract.js" in executor_text ), "Maint 71 must use the structured sync PR merge contract helper" assert ( - "selectMergeEligibleSyncPr" in contract_text + "selectMergeEligibleSyncPr" in executor_text ), "Maint 71 must select the active PR with the lease-aware merge contract" assert ( - "isTrustedGeneratedDeliveryPr" in contract_text + "isTrustedGeneratedDeliveryPr" in executor_text ), "Maint 71 must route both sync and dev-tool generated deliveries through the contract" - assert ( - "close-expired-delivery" in contract_text and "close-or-refresh-delivery" in contract_text - ) + assert "close-expired-delivery" in executor_text and "close-or-refresh-delivery" in executor_text assert "cleanup_branches:" in text, "Maint 71 must expose sync branch cleanup control" assert ( - "collectDeletableSyncBranches" in contract_text and "branch_delete_failed" in contract_text + "collectDeletableSyncBranches" in executor_text + and "branch_delete_failed" in executor_text ), "Maint 71 must delete leftover sync branches and report deletion failures" assert ( - "parseBooleanInput" in contract_text and "AUTO_MERGE_INPUT" in text + "parseBooleanInput" in executor_text and "AUTO_MERGE_INPUT" in text ), "Maint 71 must preserve explicit false boolean inputs" assert "SYNC_PR_MERGE_REPORT_JSON" in text, "Maint 71 must configure a JSON merge report path" assert ( - "report-only mode remains successful" in contract_text + "report-only mode remains successful" in executor_text ), "Maint 71 dry-run mode must report blocking statuses without failing the workflow" assert ( "sync-pr-merge-report" in text From a26bff015df8d69d8b231e868dfd9a63f17b9838 Mon Sep 17 00:00:00 2001 From: Codex Automation Date: Thu, 6 Aug 2026 18:37:29 -0500 Subject: [PATCH 9/9] style: black-format Maint 71 consolidation contracts Co-authored-by: Cursor --- tests/workflows/test_workflow_agents_consolidation.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/workflows/test_workflow_agents_consolidation.py b/tests/workflows/test_workflow_agents_consolidation.py index 35ec09e7d..a3fc9391c 100644 --- a/tests/workflows/test_workflow_agents_consolidation.py +++ b/tests/workflows/test_workflow_agents_consolidation.py @@ -320,11 +320,12 @@ def test_merge_sync_prs_uploads_machine_readable_report_and_hash_input(): assert ( "isTrustedGeneratedDeliveryPr" in executor_text ), "Maint 71 must route both sync and dev-tool generated deliveries through the contract" - assert "close-expired-delivery" in executor_text and "close-or-refresh-delivery" in executor_text + assert ( + "close-expired-delivery" in executor_text and "close-or-refresh-delivery" in executor_text + ) assert "cleanup_branches:" in text, "Maint 71 must expose sync branch cleanup control" assert ( - "collectDeletableSyncBranches" in executor_text - and "branch_delete_failed" in executor_text + "collectDeletableSyncBranches" in executor_text and "branch_delete_failed" in executor_text ), "Maint 71 must delete leftover sync branches and report deletion failures" assert ( "parseBooleanInput" in executor_text and "AUTO_MERGE_INPUT" in text