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
77 changes: 77 additions & 0 deletions .github/scripts/__tests__/sync_pr_merge_contract.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,86 @@ const {
parseNoChangeEvidenceDocument,
parseReviewResolutionProofs,
run,
selectReconciliationTargets,
validateReviewResolutionProof,
} = require('../maint71_merge_sync_prs');

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']);
const registeredRepos = ['stranske/Trend_Model_Project'];

assert.deepEqual(
selectReconciliationTargets({
eventName: 'workflow_dispatch',
requestedSyncHash: 'delivery',
inputRepos: 'all',
registeredRepos,
expectedCanaryRepos: [],
excludedRepos,
manuallyReconcilableRepos,
}),
registeredRepos,
);
assert.deepEqual(
selectReconciliationTargets({
eventName: 'workflow_dispatch',
requestedSyncHash: 'delivery',
inputRepos: 'stranske/Collab-Admin',
registeredRepos,
expectedCanaryRepos: [],
excludedRepos,
manuallyReconcilableRepos,
}),
['stranske/Collab-Admin'],
);
assert.deepEqual(
selectReconciliationTargets({
eventName: 'workflow_dispatch',
requestedSyncHash: 'candidate',
inputRepos: 'stranske/Collab-Admin',
registeredRepos,
expectedCanaryRepos: ['stranske/Travel-Plan-Permission'],
excludedRepos,
manuallyReconcilableRepos,
}),
['stranske/Travel-Plan-Permission'],
);

for (const [eventName, requestedSyncHash] of [
['repository_dispatch', 'delivery'],
['workflow_dispatch', 'campaign'],
['workflow_dispatch', 'dev-tool'],
['workflow_dispatch', ''],
]) {
assert.deepEqual(
selectReconciliationTargets({
eventName,
requestedSyncHash,
inputRepos: 'stranske/Collab-Admin',
registeredRepos,
expectedCanaryRepos: [],
excludedRepos,
manuallyReconcilableRepos,
}),
[],
);
}

assert.deepEqual(
selectReconciliationTargets({
eventName: 'workflow_dispatch',
requestedSyncHash: 'delivery',
inputRepos: 'stranske/Trend_Model_Project,stranske/Collab-Admin',
registeredRepos,
expectedCanaryRepos: [],
excludedRepos,
manuallyReconcilableRepos,
}),
['stranske/Trend_Model_Project'],
);
});

const pr = (number, ref, created_at) => ({
number,
title: `sync ${number}`,
Expand Down
52 changes: 47 additions & 5 deletions .github/scripts/maint71_merge_sync_prs.js
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,37 @@ function validateReviewResolutionProof(proof = {}, {
return { ok: errors.length === 0, errors };
}

function selectReconciliationTargets({
eventName,
requestedSyncHash,
inputRepos,
registeredRepos,
expectedCanaryRepos,
excludedRepos,
manuallyReconcilableRepos,
}) {
if (requestedSyncHash === 'candidate') {
return expectedCanaryRepos;
}
if (inputRepos === 'all') {
return registeredRepos;
}
const requestedRepos = inputRepos
.split(',')
.map((repo) => repo.trim())
.filter(Boolean);
const isExplicitManualDelivery =
eventName === 'workflow_dispatch'
&& requestedSyncHash === 'delivery'
&& requestedRepos.length === 1
&& manuallyReconcilableRepos.has(requestedRepos[0]);
return requestedRepos.filter(
(repo) =>
!excludedRepos.has(repo)
|| (isExplicitManualDelivery && manuallyReconcilableRepos.has(repo)),
);
}

async function collectReviewerEvidence({
owner,
repo,
Expand Down Expand Up @@ -725,6 +756,12 @@ async function run({ github, context, core }) {
.map((repo) => repo.trim())
.filter(Boolean),
);
const manuallyReconcilableRepos = new Set(
String(process.env.MANUAL_RECONCILIATION_REPOS_INPUT || '')
.split(',')
.map((repo) => repo.trim())
.filter(Boolean),
);
const registeredRepos = String(process.env.REGISTERED_REPOS_INPUT || '')
.split(',')
.map(r => r.trim())
Expand Down Expand Up @@ -838,11 +875,15 @@ async function run({ github, context, core }) {
// complete consumer registry here lets unrelated non-canary delivery PRs
// create target_missing failures and can make promotion evidence unusable.
// Always derive this target set from the canonical canary registry.
const targetRepos = requestedSyncHash === 'candidate'
? expectedCanaryRepos
: inputRepos === 'all'
? registeredRepos
: inputRepos.split(',').map(r => r.trim()).filter((repo) => repo && !excludedRepos.has(repo));
const targetRepos = selectReconciliationTargets({
eventName: context.eventName,
requestedSyncHash,
inputRepos,
registeredRepos,
expectedCanaryRepos,
excludedRepos,
manuallyReconcilableRepos,
});

console.log(`Registered consumer repos: ${registeredRepos.join(', ')}`);
console.log(`Processing repos: ${targetRepos.join(', ')}`);
Expand Down Expand Up @@ -2580,5 +2621,6 @@ module.exports = {
parseNoChangeEvidenceDocument,
parseReviewResolutionProofs,
run,
selectReconciliationTargets,
validateReviewResolutionProof,
};
3 changes: 3 additions & 0 deletions .github/workflows/maint-71-merge-sync-prs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,7 @@ jobs:
TRUSTED_REVIEW_RESOLUTION_ACTORS: stranske,stranske-automation-bot
REVIEW_RESOLUTION_JSON: ${{ inputs.review_resolution_json }}
EXCLUDED_REPOS_INPUT: stranske/Collab-Admin
MANUAL_RECONCILIATION_REPOS_INPUT: stranske/Collab-Admin
SYNC_PR_MERGE_REPORT_JSON: artifacts/sync-review-resolution-report.json
REGISTERED_REPOS_INPUT: ${{ steps.repos.outputs.list }}
with:
Expand Down Expand Up @@ -339,6 +340,7 @@ jobs:
TRUSTED_REVIEW_RESOLUTION_ACTORS: stranske,stranske-automation-bot
REVIEW_RESOLUTION_JSON: ${{ inputs.review_resolution_json || '' }}
EXCLUDED_REPOS_INPUT: stranske/Collab-Admin
MANUAL_RECONCILIATION_REPOS_INPUT: stranske/Collab-Admin
SYNC_PR_MERGE_REPORT_JSON: artifacts/sync-pr-premerge-report.json
REGISTERED_REPOS_INPUT: ${{ steps.repos.outputs.list }}
with:
Expand Down Expand Up @@ -430,6 +432,7 @@ jobs:
TRUSTED_REVIEW_RESOLUTION_ACTORS: stranske,stranske-automation-bot
REVIEW_RESOLUTION_JSON: ${{ inputs.review_resolution_json || '' }}
EXCLUDED_REPOS_INPUT: stranske/Collab-Admin
MANUAL_RECONCILIATION_REPOS_INPUT: stranske/Collab-Admin
SYNC_PR_MERGE_REPORT_JSON: artifacts/sync-pr-merge-report.json
REGISTERED_REPOS_INPUT: ${{ steps.repos.outputs.list }}
with:
Expand Down
10 changes: 9 additions & 1 deletion docs/ops/CONSUMER_REPO_MAINTENANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -496,7 +496,15 @@ delivery for the same plan; if a partial commit pass already merged it, resume
the prepare pass so Maint 71 rebuilds authorization from trusted closed merged
history. `stranske/Collab-Admin` is excluded from campaign authorization because
it is the generated fleet dashboard/control repository, not a reviewed consumer
delivery target.
delivery target. A narrow operational recovery remains available for an
already-generated administration-surface delivery: manually dispatch
`maint-71-merge-sync-prs.yml` with exactly
`repos=stranske/Collab-Admin` and `active_sync_hash=delivery`. The exception is
accepted only for `workflow_dispatch` with that one normalized repository; it
does not add Collab-Admin to the registry, candidate or campaign evidence, or
scheduled, repository-dispatch, workflow-call, mixed-repository, unscoped, or
other selector runs. Maint 71 still applies the normal exact-head, review,
required-check, seal, and signature gates before any merge.

Active non-outdated review threads remain merge blockers. When a shared source
repair proves a finding obsolete on the current generated head, an authenticated
Expand Down
9 changes: 7 additions & 2 deletions docs/ops/SYNC_DEPENDENCY_CAMPAIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,13 @@ After a candidate-selector run has complete same-plan evidence and every
configured candidate was merged or recovered, Maint 71 passes that exact JSON
to Maint 68 `phase=promote`. Maint 68 in turn dispatches the delivery selector
after writing non-canary PRs. Neither chain permits an explicit non-canary repo
through `phase=canary`, and `stranske/Collab-Admin` is excluded from reconciler
targets.
through `phase=canary`, and `stranske/Collab-Admin` is excluded from default
fleet reconciler targets. Its administration-surface delivery may be processed
only by a `workflow_dispatch` Maint 71 request whose normalized inputs are
exactly `repos=stranske/Collab-Admin` and `active_sync_hash=delivery`; it never
contributes to candidate or campaign evidence. Scheduled, repository-dispatch,
workflow-call, mixed-repository, unscoped, candidate, campaign, and dev-tool
requests continue to exclude it.

An active review thread remains a hard merge block. The bounded exception is an
explicit `workflows-sync-review-resolution/v1` proof supplied to Maint 71. It
Expand Down
8 changes: 8 additions & 0 deletions tests/workflows/test_sync_delivery_liveness.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
def test_maint71_has_proof_bound_review_resolution_and_exact_evidence_promotion():
workflow = Path(".github/workflows/maint-71-merge-sync-prs.yml").read_text()
executor = Path(".github/scripts/maint71_merge_sync_prs.js").read_text()
maintenance_guide = Path("docs/ops/CONSUMER_REPO_MAINTENANCE.md").read_text()
campaign_contract = Path("docs/ops/SYNC_DEPENDENCY_CAMPAIGN.md").read_text()

assert "review_resolution_json:" in workflow
assert "github.event.client_payload.review_resolution_json" not in workflow
Expand Down Expand Up @@ -31,6 +33,12 @@ def test_maint71_has_proof_bound_review_resolution_and_exact_evidence_promotion(
assert "canary_evidence_json: JSON.stringify(evidence)" in workflow
assert "cancel-in-progress: false" in workflow
assert "EXCLUDED_REPOS_INPUT: stranske/Collab-Admin" in workflow
assert "MANUAL_RECONCILIATION_REPOS_INPUT: stranske/Collab-Admin" in workflow
assert "selectReconciliationTargets" in executor
for document in (maintenance_guide, campaign_contract):
assert "repos=stranske/Collab-Admin" in document
assert "active_sync_hash=delivery" in document
assert "workflow_dispatch" in document
assert "['candidate', 'campaign', 'delivery', 'dev-tool']" in workflow
assert "/^[0-9a-f]{12,64}$/i.test(selector)" in workflow
assert "active sync selector must be candidate, campaign, delivery, dev-tool, " in workflow
Expand Down
Loading