Skip to content
Merged
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
23 changes: 23 additions & 0 deletions .github/scripts/__tests__/sync_pr_merge_contract.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ const {
campaignNoChangeRequiresLiveGate,
collectReviewerEvidence,
legacyStatusAsCheck,
mergeMethodPolicyAllowsFallback,
normalizeReviewPolicy,
parseNoChangeEvidenceDocument,
parseReviewResolutionProofs,
Expand All @@ -64,6 +65,28 @@ const {
validateReviewResolutionProof,
} = require('../maint71_merge_sync_prs');

test('Maint 71 falls back only when repository policy rejects a merge method', () => {
for (const [method, message] of [
['merge', 'Merge commits are not allowed on this repository.'],
['squash', 'Squash merges are not allowed on this repository.'],
['rebase', 'Rebase merges are not allowed on this repository.'],
['squash', 'The selected merge method is not allowed.'],
]) {
assert.equal(mergeMethodPolicyAllowsFallback(new Error(message), method), true, message);
}

for (const [method, message] of [
['merge', 'Repository rule violations found: Required status check gate-summary is expected.'],
['squash', 'Repository rule violations found: At least 1 approving review is required.'],
['merge', 'Head branch was modified. Review and try the merge again.'],
['squash', 'Required status check gate-summary is failing.'],
['rebase', 'Resource not accessible by integration.'],
['merge', 'Merge conflict'],
]) {
assert.equal(mergeMethodPolicyAllowsFallback(new Error(message), method), false, message);
}
});

test('Maint 71 keeps Collab-Admin out of fleet runs but allows an explicit manual reconciliation', () => {
const excludedRepos = new Set(['stranske/Collab-Admin']);
const manuallyReconcilableRepos = new Set(['stranske/Collab-Admin']);
Expand Down
22 changes: 21 additions & 1 deletion .github/scripts/maint71_merge_sync_prs.js
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,25 @@ function campaignNoChangeRequiresLiveGate(evidence = {}) {
return evidence?.evidence_source !== 'no-change-delivery';
}

const MERGE_METHOD_DENIAL_PATTERNS = {
merge: ['merge commits are not allowed', 'merge commit is not allowed'],
squash: ['squash merges are not allowed', 'squash merge is not allowed'],
rebase: ['rebase merges are not allowed', 'rebase merge is not allowed'],
};

function mergeMethodPolicyAllowsFallback(error, mergeMethod) {
const message = String(error?.message || error || '').toLowerCase();
const method = String(mergeMethod || '').toLowerCase();
const patterns = MERGE_METHOD_DENIAL_PATTERNS[method] || [];
if (patterns.some((pattern) => message.includes(pattern))) {
return true;
}
if (message.includes('merge method is not allowed')) {
return true;
}
return false;
}

function validateReviewResolutionProof(proof = {}, {
owner,
repo,
Expand Down Expand Up @@ -2429,7 +2448,7 @@ async function run({ github, context, core }) {
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')) {
if (!mergeMethodPolicyAllowsFallback(e, merge_method)) {
break;
}
}
Expand Down Expand Up @@ -2617,6 +2636,7 @@ module.exports = {
campaignNoChangeRequiresLiveGate,
collectReviewerEvidence,
legacyStatusAsCheck,
mergeMethodPolicyAllowsFallback,
normalizeReviewPolicy,
parseNoChangeEvidenceDocument,
parseReviewResolutionProofs,
Expand Down
2 changes: 1 addition & 1 deletion docs/WORKFLOW_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ _Inline Gate helper_
- **`maint-69-sync-labels.yml`** — Propagates the canonical `.github/labels-core.yml` set to every registered consumer repo (or a provided subset), reusing the registered-repo helper + load-balanced API client without any additional App-token minting.
- **`maint-70-fix-integration-formatting.yml`** — Manual formatter for Workflows-Integration-Tests that resolves the repo default branch, applies `black`+`ruff` fixes, and pushes via PAT only when a token is available; runs read-only otherwise.
- **`maint-71-auto-fix-integration.yml`** — Auto-triggered integration fixer that watches “Integration CI failed” issues/comments, re-runs the formatting routine, and pushes via PAT when available (otherwise posting a skipped note).
- **`maint-71-merge-sync-prs.yml`** — Scans each registered non-admin consumer repo for generated deliveries, closes stale duplicates, deletes leftover branches, and is the only lane allowed to merge a stable consumer delivery. Its cross-repository mutations require `OWNER_PR_PAT`; the workflow fails closed rather than rotating to a partial-scope service or default token. Stable PRs advance `staging` → `reviewing` → `sealed`: one substantive configured-reviewer response is sufficient after seven minutes, all-capacity-unavailable responses may degrade after that quiet period, and zero response degrades after fifteen minutes. It never waits for all configured reviewers. A status that explicitly says the review was skipped, excluded, review-disabled, or not performed is unavailable evidence rather than a response; a completed negative verdict with substantive output is still a response. None of those fallbacks waives an active non-outdated review thread. A specific thread may be resolved only from an authenticated exact-head proof that names the merged Workflows source fix and whose source commit is contained in the delivery. The exact sealed head must be validly GitHub-signed and then pass a freshly triggered Gate before merge; unsigned or non-GitHub-signed heads are returned to Maint 68 for replacement. The staging label remains a shared merger hold until Maint 71 completes the merge. Candidate evidence authorizes promotion but leaves the stable candidate PRs open. Promotion prepares the non-canary delivery PRs, and a campaign pass merges both stable lanes only after every registered repo is exact-head prepared or proven unchanged. A promotion-produced `no-change-delivery` row is terminal when its immutable plan/source binding and exact-tree validation are valid and the recorded default-branch head is still current; because no consumer head changed, campaign reconciliation does not create a redundant default-branch Gate. Canary no-change rows retain their live required-check revalidation. A workflow-sync selector ignores sibling dev-tool PRs when deciding whether its expected stable branch exists, so independent lanes do not create false `target_missing` failures. Required checks come from legacy branch protection when visible, otherwise active repository and inherited organization rulesets.
- **`maint-71-merge-sync-prs.yml`** — Scans each registered non-admin consumer repo for generated deliveries, closes stale duplicates, deletes leftover branches, and is the only lane allowed to merge a stable consumer delivery. Its cross-repository mutations require `OWNER_PR_PAT`; the workflow fails closed rather than rotating to a partial-scope service or default token. Stable PRs advance `staging` → `reviewing` → `sealed`: one substantive configured-reviewer response is sufficient after seven minutes, all-capacity-unavailable responses may degrade after that quiet period, and zero response degrades after fifteen minutes. It never waits for all configured reviewers. A status that explicitly says the review was skipped, excluded, review-disabled, or not performed is unavailable evidence rather than a response; a completed negative verdict with substantive output is still a response. None of those fallbacks waives an active non-outdated review thread. A specific thread may be resolved only from an authenticated exact-head proof that names the merged Workflows source fix and whose source commit is contained in the delivery. The exact sealed head must be validly GitHub-signed and then pass a freshly triggered Gate before merge; unsigned or non-GitHub-signed heads are returned to Maint 68 for replacement. If repository policy disables one GitHub merge method, Maint 71 tries the remaining methods in order and repeats the exact-head/review-thread gate before each fallback; unrelated merge failures remain terminal. The staging label remains a shared merger hold until Maint 71 completes the merge. Candidate evidence authorizes promotion but leaves the stable candidate PRs open. Promotion prepares the non-canary delivery PRs, and a campaign pass merges both stable lanes only after every registered repo is exact-head prepared or proven unchanged. A promotion-produced `no-change-delivery` row is terminal when its immutable plan/source binding and exact-tree validation are valid and the recorded default-branch head is still current; because no consumer head changed, campaign reconciliation does not create a redundant default-branch Gate. Canary no-change rows retain their live required-check revalidation. A workflow-sync selector ignores sibling dev-tool PRs when deciding whether its expected stable branch exists, so independent lanes do not create false `target_missing` failures. Required checks come from legacy branch protection when visible, otherwise active repository and inherited organization rulesets.
- **`maint-72-fix-pr-body-conflicts.yml`** — Periodically removes stray `pr_body.md` files from consumer repos and ensures `.gitignore` blocks them, reusing the registered-repo helper + PAT discovery so cleanups only run when push access is available.
- **`maint-74-ledger-base-sync.yml`** — Keeps `.agents` ledger base entries aligned with the repo’s default branch by running `scripts/ledger_migrate_base.py` and opening a helper PR (no extra App token mint needed).
- **`maint-auto-update-pypi-versions.yml`** — Monday 03:00 UTC canonical source lane that batches routine PyPI pin updates into one mutable source PR; an explicit security override may run outside that window.
Expand Down
3 changes: 3 additions & 0 deletions docs/ops/CONSUMER_REPO_MAINTENANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,9 @@ generated PR lands.

Maint 71 also enforces the seven-minute exact-head post-push window and performs a final
head plus active-review-thread query immediately before each generated merge.
If repository policy disables the first merge method, Maint 71 tries the remaining
GitHub-supported methods in order and repeats that exact-head/review-thread gate before
each fallback. Other merge failures remain terminal and are not retried as policy drift.
Maint 68 records the exact head and its post-publication observation time in the
delivery record. That SHA-bound observation anchors the window; PR body edits,
labels, comments, and review-thread resolution do not restart it, while a
Expand Down
Loading