Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
16 changes: 16 additions & 0 deletions .github/scripts/__tests__/sync_pr_merge_contract.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,9 @@ test('maint71 run writes reports and records a no-PR result with fake action cli
test('normalizeSyncHash accepts raw hashes and branch names', () => {
assert.equal(normalizeSyncHash('5108b94a2435'), '5108b94a2435');
assert.equal(normalizeSyncHash('sync/workflows-5108b94a2435'), '5108b94a2435');
assert.equal(normalizeSyncHash('sync/workflows-candidate'), 'candidate');
assert.equal(syncBranchForHash('5108b94a2435'), 'sync/workflows-5108b94a2435');
assert.equal(syncBranchForHash('candidate'), 'sync/workflows-candidate');
});

test('parseBooleanInput preserves explicit false values', () => {
Expand Down Expand Up @@ -223,6 +225,20 @@ test('selectActiveSyncPr honors target hash instead of newest PR', () => {
assert.deepEqual(selection.stale.map((item) => item.number), [2]);
});

test('selectActiveSyncPr can target the stable canary candidate branch', () => {
const selection = selectActiveSyncPr(
[
pr(1, 'sync/workflows-candidate', '2026-04-25T01:00:00Z'),
pr(2, 'sync/workflows-old-wave', '2026-04-25T02:00:00Z'),
],
'candidate',
);

assert.equal(selection.active.number, 1);
assert.equal(selection.expectedBranch, 'sync/workflows-candidate');
assert.deepEqual(selection.stale.map((item) => item.number), [2]);
});

test('selectActiveSyncPr reports missing target without marking stale PRs', () => {
const selection = selectActiveSyncPr(
[pr(1, 'sync/workflows-other', '2026-04-25T01:00:00Z')],
Expand Down
46 changes: 36 additions & 10 deletions .github/workflows/maint-68-sync-consumer-repos.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
workflow_dispatch:
inputs:
repos:
description: 'Comma-separated repos to sync (empty for all registered)'
description: 'Optional repo subset (canary phase accepts configured canaries only)'
type: string
required: false
dry_run:
Expand Down Expand Up @@ -83,6 +83,7 @@
template_hash: ${{ steps.manifest.outputs.template_hash }}
plan_id: ${{ steps.manifest.outputs.plan_id }}
phase: ${{ steps.repos.outputs.phase }}
sync_branch: ${{ steps.repos.outputs.sync_branch }}
steps:
- name: Checkout
uses: actions/checkout@v7
Expand Down Expand Up @@ -149,7 +150,16 @@
--output sync-phase-selection.json
echo "matrix=$(jq -c '.matrix' sync-phase-selection.json)" >> "$GITHUB_OUTPUT"
echo "phase=$phase" >> "$GITHUB_OUTPUT"
if [ "$phase" = "canary" ]; then
# Candidate corrections update the same bounded PR in each canary
# repository. Only a plan-bound promotion gets immutable hash branches.
sync_branch="sync/workflows-candidate"
else
sync_branch="sync/workflows-${{ steps.manifest.outputs.template_hash }}"
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
fi
echo "sync_branch=$sync_branch" >> "$GITHUB_OUTPUT"
echo "Selected phase: $phase"
echo "Sync branch: $sync_branch"
jq -r '.selected_repos[]' sync-phase-selection.json

- name: Upload plan and prospective-diff evidence
Expand Down Expand Up @@ -717,7 +727,7 @@
github-token: ${{ env.REPO_TOKEN }}
script: |
const { isConsumerOpenPr } = require('./workflows/.github/scripts/sync_tracker_state');
const branchName = 'sync/workflows-${{ needs.prepare.outputs.template_hash }}';
const branchName = '${{ needs.prepare.outputs.sync_branch }}';
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
const escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const exists = await isConsumerOpenPr({
github,
Expand All @@ -740,7 +750,7 @@
run: |
cd consumer

branch_name="sync/workflows-${{ needs.prepare.outputs.template_hash }}"
branch_name="${{ needs.prepare.outputs.sync_branch }}"
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed

# Configure git for push/fetch authentication using credential helper
# This avoids exposing token in git remote URL or command output.
Expand All @@ -752,8 +762,10 @@
git config credential.helper "$credential_helper"

# Keep an existing generated PR current instead of treating it as a
# terminal delivery attempt: rebuild its branch and refresh its lease
# only when the existing attempt still has a current delivery record.
# terminal delivery attempt. Immutable promotion branches refresh only
# within one generation. The stable canary branch may rotate to a new
# candidate plan when its existing delivery record is valid and owned
# by this consumer repository.
existing_pr=$(gh pr list --head "$branch_name" --json number -q '.[0].number' || echo "")
existing_head=""
existing_base=""
Expand All @@ -776,6 +788,8 @@
DELIVERY_GENERATION="$DELIVERY_GENERATION" \
PLAN_ID="$PLAN_ID" \
DELIVERY_REPOSITORY="$DELIVERY_REPOSITORY" \
SYNC_PHASE="$SYNC_PHASE" \
SYNC_BRANCH="$branch_name" \
node -e '
const fs = require("fs");
const { parseDeliveryRecord, mergeEligibility } = require("../workflows/.github/scripts/sync_pr_lease_contract");
Expand All @@ -785,6 +799,21 @@
process.stdout.write("false missing_or_invalid");
process.exit(0);
}
const isStableCandidate =
process.env.SYNC_PHASE === "canary" &&
process.env.SYNC_BRANCH === "sync/workflows-candidate";
if (isStableCandidate) {
if (record.repository !== (process.env.DELIVERY_REPOSITORY || "")) {
process.stdout.write("false repository_mismatch");
process.exit(0);
}
if (record.terminal_disposition) {
process.stdout.write(`false terminal:${record.terminal_disposition}`);
process.exit(0);
}
process.stdout.write("true candidate_plan_rotation");
process.exit(0);
}
Comment on lines +806 to +820

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 6 \
  'gh pr list --head|isConsumerOpenPr|head\.repo|head_repo|full_name|candidate_plan_rotation' \
  .github/workflows/maint-68-sync-consumer-repos.yml .github/scripts

Repository: stranske/Workflows

Length of output: 50374


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- workflow lookup and refresh path ---'
sed -n '710,855p' .github/workflows/maint-68-sync-consumer-repos.yml

printf '%s\n' '--- isConsumerOpenPr implementation ---'
sed -n '390,455p' .github/scripts/sync_tracker_state/index.js

printf '%s\n' '--- all workflow copies ---'
find .github/workflows templates/consumer-repo/.github/workflows -maxdepth 1 -type f \
  \( -name 'maint-68-sync-consumer-repos.yml' -o -name 'maint-68-sync-consumer-repos.yaml' \) -print

Repository: stranske/Workflows

Length of output: 9627


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- workflow setup and refresh execution ---'
rg -n -C 8 \
  'actions/checkout|cd consumer|gh pr list|gh pr view|gh pr edit|existing_refreshable|existing_pr|git fetch origin' \
  .github/workflows/maint-68-sync-consumer-repos.yml

printf '%s\n' '--- template workflow inventory ---'
find templates -type f 2>/dev/null | head -80 || true
rg -n 'maint-68-sync-consumer-repos|sync/workflows-candidate' templates .github/workflows 2>/dev/null || true

printf '%s\n' '--- representative acceptance-path verifier ---'
python3 - <<'PY'
import re

workflow = open('.github/workflows/maint-68-sync-consumer-repos.yml', encoding='utf-8').read()
helper = open('.github/scripts/sync_tracker_state/index.js', encoding='utf-8').read()

lookup = re.search(
    r'existing_pr=\$\(gh pr list --head "\$branch_name" --json number -q \'\.\[0\]\.number\'',
    workflow,
)
view_body_only = 'gh pr view "$existing_pr" --json body -q .body' in workflow
fetch_origin_branch = 'git fetch origin "$branch_name"' in workflow
helper_uses_head_repo = bool(re.search(r'head\?\.repo|head\.repo|headRepo|full_name', helper[helper.index('async function isConsumerOpenPr'):helper.index('function formatStuckWindowMarker')]))
helper_returns_boolean = 'return pulls.some' in helper[helper.index('async function isConsumerOpenPr'):helper.index('function formatStuckWindowMarker')]

def candidate_accepts(record, delivery_repo):
    return (
        record.get('repository') == delivery_repo
        and not record.get('terminal_disposition')
    )

cases = [
    ('consumer PR', {'repository': 'owner/consumer', 'terminal_disposition': ''}),
    ('fork PR with forged body', {'repository': 'owner/consumer', 'terminal_disposition': ''}),
]
print({
    'lookup_requests_only_number': bool(lookup),
    'body_query_omits_head_repo': view_body_only,
    'origin_fetches_branch_before_refresh': fetch_origin_branch,
    'isConsumerOpenPr_reads_head_repo': helper_uses_head_repo,
    'isConsumerOpenPr_returns_only_match_boolean': helper_returns_boolean,
    'candidate_body_acceptance': {
        name: candidate_accepts(record, 'owner/consumer')
        for name, record in cases
    },
})
PY

Repository: stranske/Workflows

Length of output: 263


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- workflow setup and refresh execution ---'
rg -n -C 8 'actions/checkout|cd consumer|gh pr list|gh pr view|gh pr edit|existing_refreshable|existing_pr|git fetch origin' \
  .github/workflows/maint-68-sync-consumer-repos.yml || true

printf '%s\n' '--- template workflow inventory ---'
if [ -d templates ]; then
  find templates -type f | head -80
else
  echo 'templates directory is absent'
fi
rg -n 'maint-68-sync-consumer-repos|sync/workflows-candidate' templates .github/workflows 2>/dev/null || true

printf '%s\n' '--- representative acceptance-path verifier ---'
python3 - <<'PY'
import re

workflow = open('.github/workflows/maint-68-sync-consumer-repos.yml', encoding='utf-8').read()
helper = open('.github/scripts/sync_tracker_state/index.js', encoding='utf-8').read()

helper_start = helper.index('async function isConsumerOpenPr')
helper_end = helper.index('function formatStuckWindowMarker')
helper_body = helper[helper_start:helper_end]

lookup = re.search(
    r'existing_pr=\$\(gh pr list --head "\$branch_name" --json number -q',
    workflow,
)
print({
    'lookup_requests_only_number': bool(lookup),
    'body_query_omits_head_repo': 'gh pr view "$existing_pr" --json body -q .body' in workflow,
    'origin_fetches_branch_before_refresh': 'git fetch origin "$branch_name"' in workflow,
    'isConsumerOpenPr_reads_head_repo': bool(re.search(r'head\?\.repo|head\.repo|headRepo|full_name', helper_body)),
    'isConsumerOpenPr_returns_only_match_boolean': 'return pulls.some' in helper_body,
})

def candidate_accepts(record, delivery_repo):
    return (
        record.get('repository') == delivery_repo
        and not record.get('terminal_disposition')
    )

print({
    'same_repo_record': candidate_accepts(
        {'repository': 'owner/consumer', 'terminal_disposition': ''},
        'owner/consumer',
    ),
    'fork_pr_with_forged_same_repo_record': candidate_accepts(
        {'repository': 'owner/consumer', 'terminal_disposition': ''},
        'owner/consumer',
    ),
})
PY

Repository: stranske/Workflows

Length of output: 5077


🌐 Web query:

GitHub CLI gh pr list --head branch name fork pull requests documentation

💡 Result:

To list pull requests filtered by a specific head branch name in the GitHub CLI, use the --head flag with the gh pr list command [1][2]. The command syntax is: gh pr list --head Key details regarding the --head flag: - It filters pull requests by the specified head branch name [1][3]. - It does not support the ": " syntax (e.g., owner:branch) [1][4]. - The flag is case-sensitive and requires the exact branch name [1]. For example, to list all open pull requests where the head branch is named "feature-typo", run: gh pr list --head "feature-typo" This command works for pull requests regardless of whether they originate from a fork or the main repository, provided the branch name matches the specified filter [1][5]. If you need to filter by a specific repository (such as a fork) in addition to the branch name, you can use the -R or --repo flag to specify the target repository in the format [HOST/]OWNER/REPO [5][3].

Citations:


Validate the selected PR’s head repository before candidate rotation.

gh pr list --head "$branch_name" also matches fork PRs, but the lookup requests only the PR number. Candidate eligibility then trusts the PR-controlled record.repository field. Require the selected PR’s head.repo.full_name to equal ${{ matrix.repo }} before accepting rotation. isConsumerOpenPr is insufficient because it returns only a branch-match boolean.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/maint-68-sync-consumer-repos.yml around lines 806 - 820,
Update the selected PR lookup and candidate-rotation validation to retrieve the
PR head repository full name, then require it to equal the matrix repository
value before accepting rotation. Use this trusted head repository value rather
than the PR-controlled record.repository field, while preserving the existing
terminal-disposition and candidate-plan checks in isStableCandidate.

Source: Path instructions

if (record.generation !== (process.env.DELIVERY_GENERATION || "")) {
process.stdout.write("false generation_mismatch");
process.exit(0);
Expand Down Expand Up @@ -983,6 +1012,7 @@
TEMPLATE_HASH: ${{ needs.prepare.outputs.template_hash }}
PLAN_ID: ${{ needs.prepare.outputs.plan_id }}
SYNC_PHASE: ${{ needs.prepare.outputs.phase }}
SYNC_BRANCH: ${{ needs.prepare.outputs.sync_branch }}
DRY_RUN: ${{ inputs.dry_run || 'false' }}
FORCE: ${{ inputs.force || 'false' }}
SYNC_OUTCOME: ${{ steps.sync.outcome || 'skipped' }}
Expand Down Expand Up @@ -1030,11 +1060,7 @@
"template_hash": os.environ.get("TEMPLATE_HASH", ""),
"plan_id": os.environ.get("PLAN_ID", ""),
"sync_phase": os.environ.get("SYNC_PHASE", ""),
"expected_branch": (
f"sync/workflows-{os.environ.get('TEMPLATE_HASH', '')}"
if os.environ.get("TEMPLATE_HASH", "")
else ""
),
"expected_branch": os.environ.get("SYNC_BRANCH", ""),
"dry_run": dry_run,
"force": os.environ.get("FORCE", "false") == "true",
"has_changes": has_changes,
Expand Down
2 changes: 1 addition & 1 deletion docs/WORKFLOW_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ _Inline Gate helper_
- **`maint-62-integration-consumer.yml`** — Nightly + release-triggered integration tests that reuse `reusable-10-ci-python.yml` across multiple matrices and file/resolve the `integration-test` issue via the load-balanced API client (no extra app mint).
- **`maint-65-sync-label-docs.yml`** — Syncs `docs/LABELS.md` into every registered consumer repo (plus the integration tests repo) when the source doc changes or on demand, using the shared registered-repo helper and PAT gating for cross-repo pushes.
- **`maint-66-monthly-audit.yml`** — First-of-month workflow that gathers workflow-run stats, runs the API wrapper guard, and files/updates the monthly audit issue; relies on the shared API client so no extra npm installs or App-token mints are needed.
- **`maint-68-sync-consumer-repos.yml`** — Manifest-driven consumer sync that validates template/scripts, hashes the template set, records a prospective per-repo matrix, and opens PAT-backed sync PRs. Normal no-filter runs start with the configured canaries; a later `promote` run requires same-plan, green, review-clear Maint 71 evidence before it targets non-canaries. The jobs rely solely on the shared API client and repo PATs (no extra App token mints).
- **`maint-68-sync-consumer-repos.yml`** — Manifest-driven consumer sync that validates template/scripts, hashes the template set, records a prospective per-repo matrix, and opens PAT-backed sync PRs. Normal runs are fail-closed to the configured canaries, and explicit repo filters may only narrow that canary set. Candidate corrections refresh stable `sync/workflows-candidate` PRs; a later `promote` run requires same-plan, green, review-clear Maint 71 evidence before immutable hash-named branches target non-canaries. The jobs rely solely on the shared API client and repo PATs (no extra App token mints).
- **`maint-69-sync-integration-repo.yml`** — Keeps Workflows-Integration-Tests aligned with `templates/integration-repo/`, regenerates `requirements.lock`, and pushes updates using PATs; no GitHub App token mint is required because the workflow stays inside the two repos.
- **`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.
Expand Down
2 changes: 1 addition & 1 deletion docs/ci/WORKFLOWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ Scheduled health jobs keep the automation ecosystem aligned:
* [`health-76-codex-cli-freshness.yml`](../../.github/workflows/health-76-codex-cli-freshness.yml) emits a weekly machine-readable freshness contract for the verifier `@openai/codex` CLI pin and uploads the deliberate update path as an artifact (scheduled weekly, manual dispatch).
* [`health-78-backplane-contract.yml`](../../.github/workflows/health-78-backplane-contract.yml) Workflows-internal gate that runs on PRs touching the run-contract/v1 contract set (schemas, registry, validator, fixtures): asserts the three schemas load as valid draft 2020-12 JSON Schema, `config/backplane_participants.json` keeps the required shape, and the bundled valid/invalid fixtures behave (the validator self-smoke).
* [`health-83-dependency-sync-efficiency.yml`](../../.github/workflows/health-83-dependency-sync-efficiency.yml) publishes a weekly, fixture-backed advisory report for dependency-bot, consumer-sync, and dev-tool-sync maintenance. It reports bounded-history limitations and comments on the dedicated efficiency durable tracker (`#2897`) only when the material-evidence fingerprint changes.
* [`maint-68-sync-consumer-repos.yml`](../../.github/workflows/maint-68-sync-consumer-repos.yml) pushes workflow template updates to registered consumer repos (release, template push, manual dispatch).
* [`maint-68-sync-consumer-repos.yml`](../../.github/workflows/maint-68-sync-consumer-repos.yml) stages workflow-template updates in stable PRs for the configured canaries. Explicit repo filters cannot broaden the canary phase; non-canaries are written only by a plan-bound `promote` run carrying green, review-clear Maint 71 evidence.
* [`maint-69-sync-integration-repo.yml`](../../.github/workflows/maint-69-sync-integration-repo.yml) syncs integration-repo templates to Workflows-Integration-Tests repository (template push, manual dispatch with dry-run support).
* [`maint-69-sync-labels.yml`](../../.github/workflows/maint-69-sync-labels.yml) syncs core functional labels from labels-core.yml to consumer repos (push to labels-core.yml, manual dispatch with dry-run support).
* [`maint-70-fix-integration-formatting.yml`](../../.github/workflows/maint-70-fix-integration-formatting.yml) applies Black and Ruff formatting fixes to Integration-Tests repository files (manual dispatch for CI formatting failures).
Expand Down
2 changes: 1 addition & 1 deletion docs/ci/WORKFLOW_SYSTEM.md
Original file line number Diff line number Diff line change
Expand Up @@ -738,7 +738,7 @@ Keep this table handy when you are triaging automation: it confirms which workfl
| **Backplane Contract Integrity** (`health-78-backplane-contract.yml`, maintenance bucket) | `pull_request`, `push` (contract set: schemas, registry, validator, fixtures) | Workflows-internal gate over the run-contract/v1 contract set: asserts the three schemas load as valid draft 2020-12 JSON Schema, `config/backplane_participants.json` keeps the required shape, and the bundled valid/invalid fixtures behave (validator self-smoke). | ⚪ Required on contract PRs | [Backplane contract integrity runs](https://github.com/stranske/Workflows/actions/workflows/health-78-backplane-contract.yml) |
| **Health 83 Dependency Sync Efficiency** (`health-83-dependency-sync-efficiency.yml`, maintenance bucket) | `schedule` (weekly), `workflow_dispatch` | Publishes advisory lane, amplification, stale/replacement, and agent-exception evidence for dependency and generated sync work. Bounded collection is explicitly marked incomplete; the durable tracker changes only on a material-evidence fingerprint change. | ⚪ Scheduled/manual | [Dependency sync efficiency runs](https://github.com/stranske/Workflows/actions/workflows/health-83-dependency-sync-efficiency.yml) |
| **Reusable Backplane Conformance** (`reusable-backplane-conformance.yml`, reusable bucket) | `workflow_call` | Validate a participating repo's emitted run-contract/v1 envelope (producer/bridge) or ingested satellite object (consumer) against the canonical Workflows-owned schemas plus the opt-in participant registry. No-op for non-participants. | ⚪ Reusable (opt-in) | [Backplane conformance runs](https://github.com/stranske/Workflows/actions/workflows/reusable-backplane-conformance.yml) |
| **Maint 68 Sync Consumer Repos** (`maint-68-sync-consumer-repos.yml`, maintenance bucket) | `release`, `push` (templates), `workflow_dispatch` | Push workflow template updates to registered consumer repositories. Creates PRs in consumer repos when templates change. | ⚪ Automatic/manual | [Consumer sync runs](https://github.com/stranske/Workflows/actions/workflows/maint-68-sync-consumer-repos.yml) |
| **Maint 68 Sync Consumer Repos** (`maint-68-sync-consumer-repos.yml`, maintenance bucket) | `release`, `schedule`, `workflow_dispatch` | Stage copied-file changes in stable configured-canary PRs, then write immutable hash-named PRs to non-canaries only after exact-plan Maint 71 promotion evidence passes. Explicit repo filters cannot bypass the canary boundary. | ⚪ Automatic/manual | [Consumer sync runs](https://github.com/stranske/Workflows/actions/workflows/maint-68-sync-consumer-repos.yml) |
| **Maint 69 Sync Integration Repo** (`maint-69-sync-integration-repo.yml`, maintenance bucket) | `push` (templates), `workflow_dispatch` | Sync integration-repo templates to Workflows-Integration-Tests repository. Resolves drift detected by Health 67. Supports dry-run mode. | ⚪ Automatic/manual | [Integration sync runs](https://github.com/stranske/Workflows/actions/workflows/maint-69-sync-integration-repo.yml) |
| **Maint 69 Sync Labels** (`maint-69-sync-labels.yml`, maintenance bucket) | `push` (labels-core.yml), `workflow_dispatch` | Sync core functional labels from labels-core.yml to consumer repositories. Distinguishes functional workflow labels from informational repo-specific labels. Supports dry-run mode. | ⚪ Automatic/manual | [Label sync runs](https://github.com/stranske/Workflows/actions/workflows/maint-69-sync-labels.yml) |
| **Fix Integration Tests Formatting** (`maint-70-fix-integration-formatting.yml`, maintenance bucket) | `workflow_dispatch` | Manually triggered workflow to apply Black and Ruff formatting fixes to Python files in the Workflows-Integration-Tests repository when CI formatting checks fail. | ⚪ Manual only | [Formatting fix runs](https://github.com/stranske/Workflows/actions/workflows/maint-70-fix-integration-formatting.yml) |
Expand Down
29 changes: 22 additions & 7 deletions docs/ops/CONSUMER_REPO_MAINTENANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ the workflow is the source of truth.
Scheduled Health 68 runs are triggered only after a successful Maint 71 janitor; push and
manual runs are intentionally immediate. It classifies each
consumer as `converged`, `covered`, `blocked`, `untracked_drift`, or `stale`. An open
sync PR covers drift only when its `sync/workflows-<template-hash>` branch matches the
current compiled plan and it is within the 36-hour coverage lease. Fully covered drift
sync PR covers drift only when it matches the current compiled plan and is within the
36-hour coverage lease. Configured canaries use the stable `sync/workflows-candidate`
branch; promoted non-canaries use `sync/workflows-<template-hash>`. Fully covered drift
exits zero and does not append a durable-tracker comment; stale (including expired
coverage), blocked (including global/lookup failures), and untracked states remain
actionable failures.
Expand Down Expand Up @@ -267,16 +268,25 @@ it can open sync PRs only for the 2-3 representative repositories declared in
compiled `plan_id`, desired hash, and prospective affected paths for every
registered repository before any consumer write.

Do not wait for consumer CI in that workflow. Run Maint 71 later to publish
`sync-canary-evidence.json`, then invoke Maint 68 with `phase=promote` and that
artifact's JSON as `canary_evidence_json`. Promotion rejects absent, stale or
An explicit `repos` input may narrow a canary run to a subset of those configured
canaries, but it cannot expand a canary run into non-canary repositories. The
selector fails closed with `canary_selection_contains_non_canary` if an operator
tries. Canary corrections refresh the stable `sync/workflows-candidate` branch
and its existing PR instead of opening another hash-named PR for every candidate
plan. Hash-named `sync/workflows-<plan>` branches are reserved for promotion.

Do not wait for consumer CI in that workflow. Run Maint 71 later with
`active_sync_hash=candidate` to publish `sync-canary-evidence.json`, then invoke
Maint 68 with `phase=promote` and that artifact's JSON as
Comment on lines +278 to +280

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use the declared Maint 71 input name

The Maint 71 workflow declares the input as sync_hash in .github/workflows/maint-71-merge-sync-prs.yml, not active_sync_hash. The local gh workflow run --help confirms that -f supplies workflow inputs as key=value, so an operator following this instruction cannot target sync/workflows-candidate and will receive an unexpected-input dispatch error. Change this guidance to sync_hash=candidate and show the corresponding dispatch flag.

Useful? React with 👍 / 👎.

`canary_evidence_json`. Promotion rejects absent, stale or
mixed-plan evidence, failed required checks, and active non-outdated review
threads. A successful promotion targets all registered non-canary repositories
once every configured canary has current, green, review-clear evidence for the
same plan.
Comment on lines 283 to 285

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- candidate files and references ---'
rg -n --glob '!node_modules' --glob '!dist' --glob '!build' \
  'select_phase|selected_repos|target_repos|registered non-canary|successful promotion|canary' .
printf '%s\n' '--- documentation context ---'
file=$(git ls-files | grep -F 'docs/ops/CONSUMER_REPO_MAINTENANCE.md' | head -n1)
[ -n "$file" ]
sed -n '265,295p' "$file"
sed -n '460,490p' "$file"

Repository: stranske/Workflows

Length of output: 18670


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- selector implementation ---'
sed -n '120,188p' scripts/select_consumer_sync_phase.py
printf '%s\n' '--- selector tests ---'
sed -n '100,155p' tests/scripts/test_select_consumer_sync_phase.py
printf '%s\n' '--- workflow input and call path ---'
rg -n -A12 -B8 'selected_repos|repos:|select_phase\(' .github scripts tests \
  --glob '*.yml' --glob '*.yaml' --glob '*.py'

Repository: stranske/Workflows

Length of output: 50375


🏁 Script executed:

#!/bin/bash
set -eu
python3 - <<'PY'
from pathlib import Path
import ast

path = Path("scripts/select_consumer_sync_phase.py")
tree = ast.parse(path.read_text(encoding="utf-8"))
fn = next(
    node for node in tree.body
    if isinstance(node, ast.FunctionDef) and node.name == "select_phase"
)

assignments = {}
for node in ast.walk(fn):
    if isinstance(node, ast.Assign) and len(node.targets) == 1:
        target = node.targets[0]
        if isinstance(target, ast.Name):
            assignments[target.id] = ast.unparse(node.value)

assert "target_repos if selected_repos is not None else registered_repos" in assignments["target_repos"]
assert "[repo for repo in target_repos if repo not in canary_repos]" in assignments["selected"]
print("source rule: target_repos = selected_repos when provided; promote filters that list to non-canaries")

def promotion_targets(registered, canaries, selected_repos=None):
    target_repos = selected_repos if selected_repos is not None else registered
    return [repo for repo in target_repos if repo not in canaries]

registered = ["canary-a", "canary-b", "consumer-a", "consumer-b"]
canaries = ["canary-a", "canary-b"]
assert promotion_targets(registered, canaries) == ["consumer-a", "consumer-b"]
assert promotion_targets(registered, canaries, ["consumer-b"]) == ["consumer-b"]
print("behavior: no filter targets all non-canaries; a supplied filter narrows promotion")
PY

Repository: stranske/Workflows

Length of output: 245


🏁 Script executed:

#!/bin/bash
set -eu
python3 - <<'PY'
from pathlib import Path
import ast

tree = ast.parse(Path("scripts/select_consumer_sync_phase.py").read_text(encoding="utf-8"))
fn = next(node for node in tree.body if isinstance(node, ast.FunctionDef) and node.name == "select_phase")
for node in ast.walk(fn):
    if isinstance(node, ast.Assign) and len(node.targets) == 1:
        target = node.targets[0]
        if isinstance(target, ast.Name) and target.id in {"target_repos", "selected"}:
            print(f"{target.id} = {ast.unparse(node.value)}")
PY

Repository: stranske/Workflows

Length of output: 373


Document filtered promotion scope.

A no-filter promotion targets all registered non-canaries. A filtered promotion targets only the selected registered non-canaries.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/ops/CONSUMER_REPO_MAINTENANCE.md` around lines 283 - 285, Update the
promotion-scope documentation in CONSUMER_REPO_MAINTENANCE.md to distinguish
unfiltered and filtered promotions: state that no-filter promotions target all
registered non-canary repositories, while filtered promotions target only the
selected registered non-canaries.

Use `preview` to produce the plan/evidence artifact without a write matrix.
Emergency direct promotion remains an explicit audited operator action and is
limited to a security or production-break fix.
There is no direct-repository promotion bypass. Security and production-break
fixes may use an expedited canary run, but still require exact-plan evidence
before `phase=promote` can write to non-canaries.

To validate the manifest locally:

Expand Down Expand Up @@ -460,10 +470,15 @@ changes, not synced template files.
```bash
gh workflow run "Maint 68 Sync Consumer Repos" \
--repo stranske/Workflows \
-f phase=preview \
-f repos="stranske/Travel-Plan-Permission" \
-f dry_run=true
```

Omit `repos` for the normal configured canary run. A write run targeting any
non-canary must use `phase=promote` with Maint 71's exact-plan
`canary_evidence_json`; `phase=canary -f repos=<fleet>` is rejected.

### Drift Detection

Workflows runs **Health 68 Consumer Sync Drift Check** to detect divergence between
Expand Down
Loading
Loading