From 9391ba006ebf9b0de275e16d5292920ab51a3835 Mon Sep 17 00:00:00 2001 From: stranske Date: Tue, 6 Jan 2026 07:11:38 -0600 Subject: [PATCH 01/10] fix: correct YAML syntax in agents-issue-intake.yml template (#602) * fix: correct YAML syntax in agents-issue-intake.yml template The 'if' condition in the check_labels job was improperly formatted, causing the line to wrap incorrectly with 'runs-on' ending up on the same line. This resulted in startup_failure errors when the workflow was deployed to consumer repos. Changes: - Use multiline scalar (|) for complex if condition - Properly indent continuation lines - Ensure runs-on is on its own line Fixes workflow failures in stranske/Travel-Plan-Permission and other consumer repositories using this template. * fix: add validation safeguards for template changes Problem: Template changes sync to 4+ consumer repos. A syntax error in agents-issue-intake.yml caused startup_failure in all consumer repos because there was no validation preventing bad templates. Changes: 1. Fix YAML syntax error in check_labels job (multiline if condition) 2. Add validate_workflow_yaml.py script to catch YAML/style issues 3. Add pre-commit hook to validate templates before commit 4. Add CRITICAL section to CLAUDE.md about template changes Safeguards added: - Pre-commit hook blocks template commits with validation errors - Script checks: YAML syntax, line length (100), runs-on placement - Clear warning in CLAUDE.md with validation commands - Enforces repo standards before sync Related: Travel-Plan-Permission#253, Workflows#602 --- .pre-commit-config.yaml | 12 +- CLAUDE.md | 24 +++ scripts/validate_workflow_yaml.py | 158 ++++++++++++++++++ .../.github/workflows/agents-issue-intake.yml | 15 +- 4 files changed, 201 insertions(+), 8 deletions(-) create mode 100755 scripts/validate_workflow_yaml.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ee3f7c3f9..ce3df8daf 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -45,7 +45,17 @@ repos: args: ["--fix"] files: '\.(py|pyi)$' - # TODO Phase 4: Add workflow-specific validation + # Workflow YAML validation for templates + - repo: local + hooks: + - id: validate-workflow-templates + name: Validate workflow templates + entry: python3 scripts/validate_workflow_yaml.py + language: system + files: '^templates/consumer-repo/.github/workflows/.*\.yml$' + pass_filenames: true + + # TODO Phase 4: Add actionlint # - repo: https://github.com/rhysd/actionlint # rev: v1.6.26 # hooks: diff --git a/CLAUDE.md b/CLAUDE.md index f1ce165d9..e7566088b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -153,6 +153,30 @@ Secrets use **lowercase** in `workflow_call` definitions but reference org secre 4. **Run pre-sync validation** to ensure files pass consumer lint rules 5. **Sync to ALL consumer repos** to maintain consistency +## ⚠️ CRITICAL: Template Changes (READ THIS!) + +**If you modify `templates/consumer-repo/` YOU WILL SYNC TO ALL CONSUMER REPOS.** + +Before editing any template file: + +```bash +# 1. Validate YAML syntax and style +python3 scripts/validate_workflow_yaml.py templates/consumer-repo/.github/workflows/*.yml + +# 2. Check against repo standards (line-length = 100) +ruff check templates/consumer-repo/ + +# 3. Dry-run the sync to see impact +gh workflow run maint-68-sync-consumer-repos.yml -f dry_run=true +``` + +**Template changes will trigger PRs in 4+ consumer repos. One mistake = 4+ failing CI runs.** + +Repo standards (from pyproject.toml): +- Line length: **100 characters** +- Format: black, ruff, isort +- All templates must pass validation before commit + ## Quick Commands ```bash diff --git a/scripts/validate_workflow_yaml.py b/scripts/validate_workflow_yaml.py new file mode 100755 index 000000000..1ea08344a --- /dev/null +++ b/scripts/validate_workflow_yaml.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python3 +""" +Validate GitHub Actions workflow YAML files. + +This script checks workflow files for common syntax errors and issues +that may not be caught by basic YAML parsers but cause failures in GitHub Actions. +""" + +import argparse +import sys +from pathlib import Path + +try: + import yaml +except ImportError: + print("ERROR: PyYAML is required. Install with: pip install PyYAML") + sys.exit(1) + + +def check_line_length(file_path: Path, max_length: int = 150) -> list[tuple[int, str]]: + """Check for lines that exceed maximum length (may cause wrapping issues).""" + issues = [] + with open(file_path) as f: + for line_num, line in enumerate(f, 1): + if len(line.rstrip()) > max_length: + issues.append((line_num, f"Line exceeds {max_length} characters")) + return issues + + +def check_runs_on_placement(file_path: Path) -> list[tuple[int, str]]: + """Check that 'runs-on' is properly placed on its own line.""" + issues = [] + with open(file_path) as f: + for line_num, line in enumerate(f, 1): + stripped = line.strip() + if "runs-on:" in stripped: + # Check if there's content before 'runs-on:' on the same line + before_runs_on = line.split("runs-on:")[0].strip() + if before_runs_on and not before_runs_on.endswith("#"): + issues.append( + ( + line_num, + "runs-on should be on its own line (found text before it)", + ) + ) + return issues + + +def check_yaml_syntax(file_path: Path) -> list[tuple[int, str]]: + """Validate basic YAML syntax.""" + issues = [] + try: + with open(file_path) as f: + yaml.safe_load(f) + except yaml.YAMLError as e: + line_num = getattr(e, "problem_mark", None) + if line_num: + issues.append((line_num.line + 1, f"YAML syntax error: {e.problem}")) + else: + issues.append((0, f"YAML syntax error: {str(e)}")) + return issues + + +def check_multiline_conditions(file_path: Path) -> list[tuple[int, str]]: + """Check for complex conditions that should use multiline format.""" + issues = [] + with open(file_path) as f: + lines = f.readlines() + for line_num, line in enumerate(lines, 1): + stripped = line.strip() + # Only flag if line exceeds repo standard (100 chars) OR continues improperly + if stripped.startswith("if:") and len(stripped) > 100: + # Check if it's using multiline format + if not stripped.endswith("|") and not stripped.endswith(">"): + issues.append( + ( + line_num, + "Very long 'if' condition should use multiline format (| or >)", + ) + ) + # Check if next line looks like continuation without proper multiline syntax + elif stripped.startswith("if:") and line_num < len(lines): + next_line = lines[line_num].strip() if line_num < len(lines) else "" + # Check if 'runs-on:' appears mid-line (indicates malformed wrapping) + if next_line and "runs-on:" in next_line and not next_line.startswith("runs-on:"): + issues.append( + ( + line_num + 1, + "Found 'runs-on:' not at start of line - possible malformed multiline 'if'", + ) + ) + return issues + + +def validate_workflow(file_path: Path, verbose: bool = False) -> bool: + """Validate a workflow file and return True if valid.""" + all_issues = [] + + # Run all checks + all_issues.extend([(line, f"YAML: {msg}") for line, msg in check_yaml_syntax(file_path)]) + all_issues.extend([(line, f"Length: {msg}") for line, msg in check_line_length(file_path)]) + all_issues.extend( + [(line, f"Placement: {msg}") for line, msg in check_runs_on_placement(file_path)] + ) + all_issues.extend( + [(line, f"Format: {msg}") for line, msg in check_multiline_conditions(file_path)] + ) + + if all_issues: + print(f"\n❌ {file_path.name}: Found {len(all_issues)} issue(s)") + for line_num, message in sorted(all_issues): + if line_num > 0: + print(f" Line {line_num}: {message}") + else: + print(f" {message}") + return False + else: + if verbose: + print(f"✓ {file_path.name}: Valid") + return True + + +def main(): + parser = argparse.ArgumentParser(description="Validate GitHub Actions workflow YAML files") + parser.add_argument( + "files", + nargs="+", + type=Path, + help="Workflow files to validate", + ) + parser.add_argument( + "-v", + "--verbose", + action="store_true", + help="Show validation results for all files", + ) + args = parser.parse_args() + + all_valid = True + for file_path in args.files: + if not file_path.exists(): + print(f"❌ {file_path}: File not found") + all_valid = False + continue + + if not validate_workflow(file_path, args.verbose): + all_valid = False + + if all_valid: + print(f"\n✓ All {len(args.files)} workflow file(s) validated successfully") + sys.exit(0) + else: + print("\n❌ Validation failed") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/templates/consumer-repo/.github/workflows/agents-issue-intake.yml b/templates/consumer-repo/.github/workflows/agents-issue-intake.yml index 3f341a47d..f066ea4e9 100644 --- a/templates/consumer-repo/.github/workflows/agents-issue-intake.yml +++ b/templates/consumer-repo/.github/workflows/agents-issue-intake.yml @@ -106,7 +106,11 @@ jobs: # Gate for agent_bridge mode: only proceed if issue has agent:* or agents:* label check_labels: needs: route - if: needs.route.outputs.should_run_bridge == 'true' && (github.event_name != 'issues' || contains(toJson(github.event.issue.labels.*.name), 'agent:') || contains(toJson(github.event.issue.labels.*.name), 'agents:')) + if: | + needs.route.outputs.should_run_bridge == 'true' && + (github.event_name != 'issues' || + contains(toJson(github.event.issue.labels.*.name), 'agent:') || + contains(toJson(github.event.issue.labels.*.name), 'agents:')) runs-on: ubuntu-latest outputs: should_run: ${{ steps.check.outputs.should_run }} @@ -149,7 +153,9 @@ jobs: # Agent bridge mode: Call the Workflows repo reusable issue bridge bridge: needs: [route, check_labels] - if: needs.route.outputs.should_run_bridge == 'true' && needs.check_labels.outputs.should_run == 'true' + if: | + needs.route.outputs.should_run_bridge == 'true' && + needs.check_labels.outputs.should_run == 'true' uses: stranske/Workflows/.github/workflows/reusable-agents-issue-bridge.yml@main with: agent: ${{ needs.check_labels.outputs.agent }} @@ -165,11 +171,6 @@ jobs: sync: needs: route if: needs.route.outputs.should_run_sync == 'true' - permissions: - contents: read - issues: write - id-token: write - models: read uses: stranske/Workflows/.github/workflows/agents-63-issue-intake.yml@main with: intake_mode: "chatgpt_sync" From 69cb9fe991ecb7839e933668ff8a1ef898d54928 Mon Sep 17 00:00:00 2001 From: stranske Date: Tue, 6 Jan 2026 07:11:38 -0600 Subject: [PATCH 02/10] fix: correct YAML syntax in agents-issue-intake.yml template (#602) * fix: correct YAML syntax in agents-issue-intake.yml template The 'if' condition in the check_labels job was improperly formatted, causing the line to wrap incorrectly with 'runs-on' ending up on the same line. This resulted in startup_failure errors when the workflow was deployed to consumer repos. Changes: - Use multiline scalar (|) for complex if condition - Properly indent continuation lines - Ensure runs-on is on its own line Fixes workflow failures in stranske/Travel-Plan-Permission and other consumer repositories using this template. * fix: add validation safeguards for template changes Problem: Template changes sync to 4+ consumer repos. A syntax error in agents-issue-intake.yml caused startup_failure in all consumer repos because there was no validation preventing bad templates. Changes: 1. Fix YAML syntax error in check_labels job (multiline if condition) 2. Add validate_workflow_yaml.py script to catch YAML/style issues 3. Add pre-commit hook to validate templates before commit 4. Add CRITICAL section to CLAUDE.md about template changes Safeguards added: - Pre-commit hook blocks template commits with validation errors - Script checks: YAML syntax, line length (100), runs-on placement - Clear warning in CLAUDE.md with validation commands - Enforces repo standards before sync Related: Travel-Plan-Permission#253, Workflows#602 --- .github/workflows/maint-71-merge-sync-prs.yml | 234 ++++++++++++++++++ 1 file changed, 234 insertions(+) create mode 100644 .github/workflows/maint-71-merge-sync-prs.yml diff --git a/.github/workflows/maint-71-merge-sync-prs.yml b/.github/workflows/maint-71-merge-sync-prs.yml new file mode 100644 index 000000000..5ebb68cb8 --- /dev/null +++ b/.github/workflows/maint-71-merge-sync-prs.yml @@ -0,0 +1,234 @@ +# Automate merging sync PRs in consumer repos +# +# This workflow handles the common cycle of: +# 1. Check sync PR status in all consumer repos +# 2. Merge PRs that pass validation +# 3. Clean up stale branches +# +# Triggers: +# - Manual dispatch (default) +# - Can be called from sync workflow +# +# Safety: +# - Only merges PRs with sync/workflows-* branch names +# - Requires all checks to pass +# - Uses merge commit (not squash) to preserve sync history + +name: Merge Sync PRs + +on: + workflow_dispatch: + inputs: + repos: + description: "Repos to check (comma-separated, or 'all')" + required: false + type: string + default: "all" + auto_merge: + description: "Auto-merge if checks pass" + required: false + type: boolean + default: true + dry_run: + description: "Dry run (report only, no merge)" + required: false + type: boolean + default: false + workflow_call: + inputs: + repos: + description: "Repos to check (comma-separated, or 'all')" + required: false + type: string + default: "all" + auto_merge: + description: "Auto-merge if checks pass" + required: false + type: boolean + default: true + dry_run: + description: "Dry run (report only, no merge)" + required: false + type: boolean + default: false + +permissions: + contents: read + +env: + CONSUMER_REPOS: "Travel-Plan-Permission,Manager-Database,Template,trip-planner" + +jobs: + merge_sync_prs: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Check and merge sync PRs + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const owner = context.repo.owner; + const inputRepos = '${{ inputs.repos }}' || 'all'; + const autoMerge = ${{ inputs.auto_merge }}; + const dryRun = ${{ inputs.dry_run }}; + + // Determine which repos to process + const allRepos = process.env.CONSUMER_REPOS.split(','); + const targetRepos = inputRepos === 'all' ? allRepos : inputRepos.split(',').map(r => r.trim()); + + console.log(`Processing repos: ${targetRepos.join(', ')}`); + console.log(`Auto-merge: ${autoMerge}, Dry run: ${dryRun}\n`); + + const results = []; + + for (const repo of targetRepos) { + console.log(`\n=== ${repo} ===`); + + try { + // Find open sync PRs + const { data: prs } = await github.rest.pulls.list({ + owner, + repo, + state: 'open', + per_page: 10 + }); + + const syncPRs = prs.filter(pr => pr.head.ref.startsWith('sync/workflows-')); + + if (syncPRs.length === 0) { + console.log('No sync PRs found'); + results.push({ repo, status: 'no_prs' }); + continue; + } + + for (const pr of syncPRs) { + console.log(`\nPR #${pr.number}: ${pr.title}`); + console.log(`Branch: ${pr.head.ref}`); + console.log(`Created: ${pr.created_at}`); + + // Check PR status + const { data: combinedStatus } = await github.rest.repos.getCombinedStatusForRef({ + owner, + repo, + ref: pr.head.sha + }); + + // Check check runs + const { data: checkRuns } = await github.rest.checks.listForRef({ + owner, + repo, + ref: pr.head.sha + }); + + const allChecks = checkRuns.check_runs || []; + const requiredChecks = allChecks.filter(c => + !c.name.includes('Detect keepalive') && + !c.name.includes('pr_meta') && + !c.name.includes('resolve_pr') && + !c.name.includes('Cleanup') && + c.conclusion !== 'skipped' && + c.conclusion !== 'neutral' + ); + + const failedChecks = requiredChecks.filter(c => + c.conclusion !== 'success' && c.conclusion !== null + ); + + const pendingChecks = requiredChecks.filter(c => + c.status !== 'completed' + ); + + console.log(`Checks: ${requiredChecks.length} total, ${failedChecks.length} failed, ${pendingChecks.length} pending`); + + if (failedChecks.length > 0) { + console.log('Failed checks:'); + failedChecks.forEach(c => console.log(` - ${c.name}: ${c.conclusion}`)); + results.push({ repo, pr: pr.number, status: 'checks_failed' }); + continue; + } + + if (pendingChecks.length > 0) { + console.log('Waiting for checks to complete'); + results.push({ repo, pr: pr.number, status: 'checks_pending' }); + continue; + } + + // All checks passed + if (!autoMerge) { + console.log('✓ Ready to merge (auto-merge disabled)'); + results.push({ repo, pr: pr.number, status: 'ready' }); + continue; + } + + if (dryRun) { + console.log('✓ Would merge (dry run)'); + results.push({ repo, pr: pr.number, status: 'dry_run_merge' }); + continue; + } + + // Merge the PR + try { + await github.rest.pulls.merge({ + owner, + repo, + pull_number: pr.number, + merge_method: 'merge', + commit_title: pr.title, + commit_message: `Automated merge of sync PR\n\nSync hash: ${pr.head.ref.split('-').pop()}` + }); + + console.log('✓ Merged successfully'); + + // Delete the branch + try { + await github.rest.git.deleteRef({ + owner, + repo, + ref: `heads/${pr.head.ref}` + }); + console.log('✓ Branch deleted'); + } catch (e) { + console.log(`⚠ Could not delete branch: ${e.message}`); + } + + results.push({ repo, pr: pr.number, status: 'merged' }); + } catch (e) { + console.log(`✗ Merge failed: ${e.message}`); + results.push({ repo, pr: pr.number, status: 'merge_failed', error: e.message }); + } + } + } catch (e) { + console.log(`✗ Error processing ${repo}: ${e.message}`); + results.push({ repo, status: 'error', error: e.message }); + } + } + + // Summary + console.log('\n=== Summary ==='); + console.log(JSON.stringify(results, null, 2)); + + const merged = results.filter(r => r.status === 'merged').length; + const failed = results.filter(r => r.status === 'checks_failed' || r.status === 'merge_failed').length; + const pending = results.filter(r => r.status === 'checks_pending').length; + const ready = results.filter(r => r.status === 'ready').length; + + console.log(`\nMerged: ${merged}`); + console.log(`Failed: ${failed}`); + console.log(`Pending: ${pending}`); + console.log(`Ready (not auto-merged): ${ready}`); + + if (failed > 0) { + core.setFailed(`${failed} PRs failed to merge`); + } + + - name: Post summary + if: always() + run: | + echo "### Sync PR Merge Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Processed repos: ${{ inputs.repos || 'all' }}" >> $GITHUB_STEP_SUMMARY + echo "Auto-merge: ${{ inputs.auto_merge }}" >> $GITHUB_STEP_SUMMARY + echo "Dry run: ${{ inputs.dry_run }}" >> $GITHUB_STEP_SUMMARY From 0aad109879f08d500582ea2350a57878ae0b5a86 Mon Sep 17 00:00:00 2001 From: stranske Date: Tue, 6 Jan 2026 13:34:28 +0000 Subject: [PATCH 03/10] fix: use CODESPACES_WORKFLOWS token for merge permissions The workflow now uses the CODESPACES_WORKFLOWS secret which has merge permissions, falling back to GITHUB_TOKEN if not available. Successfully merged sync PRs in Manager-Database, Template, and trip-planner using this token. --- .github/workflows/maint-71-merge-sync-prs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/maint-71-merge-sync-prs.yml b/.github/workflows/maint-71-merge-sync-prs.yml index 5ebb68cb8..89e89357f 100644 --- a/.github/workflows/maint-71-merge-sync-prs.yml +++ b/.github/workflows/maint-71-merge-sync-prs.yml @@ -68,7 +68,7 @@ jobs: - name: Check and merge sync PRs uses: actions/github-script@v7 with: - github-token: ${{ secrets.GITHUB_TOKEN }} + github-token: ${{ secrets.CODESPACES_WORKFLOWS || secrets.GITHUB_TOKEN }} script: | const owner = context.repo.owner; const inputRepos = '${{ inputs.repos }}' || 'all'; From 2f3b243c9661b0560ea5fa9ae385b178590df7e0 Mon Sep 17 00:00:00 2001 From: stranske Date: Tue, 6 Jan 2026 13:38:55 +0000 Subject: [PATCH 04/10] fix: use REGISTERED_CONSUMER_REPOS and add stale PR cleanup - Parse multiline REGISTERED_CONSUMER_REPOS env var instead of hardcoded list - Add stale PR cleanup: close and delete branches for older sync PRs - Process repos in order from REGISTERED_CONSUMER_REPOS (7 repos total) - Increase per_page to 20 to catch multiple stale PRs - Add stale_closed status tracking in summary --- .github/workflows/maint-71-merge-sync-prs.yml | 228 +++++++++++------- 1 file changed, 143 insertions(+), 85 deletions(-) diff --git a/.github/workflows/maint-71-merge-sync-prs.yml b/.github/workflows/maint-71-merge-sync-prs.yml index 89e89357f..3bbece9ec 100644 --- a/.github/workflows/maint-71-merge-sync-prs.yml +++ b/.github/workflows/maint-71-merge-sync-prs.yml @@ -56,7 +56,15 @@ permissions: contents: read env: - CONSUMER_REPOS: "Travel-Plan-Permission,Manager-Database,Template,trip-planner" + # Pull from same source as sync workflow - keep in sync! + REGISTERED_CONSUMER_REPOS: | + stranske/Travel-Plan-Permission + stranske/Template + stranske/trip-planner + stranske/Manager-Database + stranske/Portable-Alpha-Extension-Model + stranske/Trend_Model_Project + stranske/Collab-Admin jobs: merge_sync_prs: @@ -75,10 +83,19 @@ jobs: const autoMerge = ${{ inputs.auto_merge }}; const dryRun = ${{ inputs.dry_run }}; + // Parse multiline REGISTERED_CONSUMER_REPOS into array + const registeredRepos = process.env.REGISTERED_CONSUMER_REPOS + .split('\n') + .map(line => line.trim()) + .filter(line => line && !line.startsWith('#')) + .map(repo => repo.replace(/^stranske\//, '')); + // Determine which repos to process - const allRepos = process.env.CONSUMER_REPOS.split(','); - const targetRepos = inputRepos === 'all' ? allRepos : inputRepos.split(',').map(r => r.trim()); + const targetRepos = inputRepos === 'all' + ? registeredRepos + : inputRepos.split(',').map(r => r.trim()); + console.log(`Registered consumer repos: ${registeredRepos.join(', ')}`); console.log(`Processing repos: ${targetRepos.join(', ')}`); console.log(`Auto-merge: ${autoMerge}, Dry run: ${dryRun}\n`); @@ -93,7 +110,7 @@ jobs: owner, repo, state: 'open', - per_page: 10 + per_page: 20 }); const syncPRs = prs.filter(pr => pr.head.ref.startsWith('sync/workflows-')); @@ -104,101 +121,140 @@ jobs: continue; } - for (const pr of syncPRs) { - console.log(`\nPR #${pr.number}: ${pr.title}`); - console.log(`Branch: ${pr.head.ref}`); - console.log(`Created: ${pr.created_at}`); + // Sort by created date, oldest first (for stale cleanup) + syncPRs.sort((a, b) => new Date(a.created_at) - new Date(b.created_at)); - // Check PR status - const { data: combinedStatus } = await github.rest.repos.getCombinedStatusForRef({ - owner, - repo, - ref: pr.head.sha - }); + // If multiple sync PRs exist, close older ones as stale + if (syncPRs.length > 1) { + console.log(`Found ${syncPRs.length} sync PRs - closing ${syncPRs.length - 1} stale PRs`); - // Check check runs - const { data: checkRuns } = await github.rest.checks.listForRef({ - owner, - repo, - ref: pr.head.sha - }); + for (let i = 0; i < syncPRs.length - 1; i++) { + const stalePR = syncPRs[i]; + console.log(`\nClosing stale PR #${stalePR.number}: ${stalePR.title}`); + console.log(`Branch: ${stalePR.head.ref}, Created: ${stalePR.created_at}`); - const allChecks = checkRuns.check_runs || []; - const requiredChecks = allChecks.filter(c => - !c.name.includes('Detect keepalive') && - !c.name.includes('pr_meta') && - !c.name.includes('resolve_pr') && - !c.name.includes('Cleanup') && - c.conclusion !== 'skipped' && - c.conclusion !== 'neutral' - ); - - const failedChecks = requiredChecks.filter(c => - c.conclusion !== 'success' && c.conclusion !== null - ); - - const pendingChecks = requiredChecks.filter(c => - c.status !== 'completed' - ); - - console.log(`Checks: ${requiredChecks.length} total, ${failedChecks.length} failed, ${pendingChecks.length} pending`); - - if (failedChecks.length > 0) { - console.log('Failed checks:'); - failedChecks.forEach(c => console.log(` - ${c.name}: ${c.conclusion}`)); - results.push({ repo, pr: pr.number, status: 'checks_failed' }); - continue; + if (!dryRun) { + // Close PR + await github.rest.pulls.update({ + owner, + repo, + pull_number: stalePR.number, + state: 'closed' + }); + console.log('✓ Closed'); + + // Delete branch + try { + await github.rest.git.deleteRef({ + owner, + repo, + ref: `heads/${stalePR.head.ref}` + }); + console.log('✓ Branch deleted'); + } catch (delErr) { + console.log(`⚠ Branch delete failed: ${delErr.message}`); + } + } else { + console.log('[DRY RUN] Would close and delete branch'); + } } + } - if (pendingChecks.length > 0) { - console.log('Waiting for checks to complete'); - results.push({ repo, pr: pr.number, status: 'checks_pending' }); - continue; - } + // Process the most recent PR + const pr = syncPRs[syncPRs.length - 1]; + console.log(`\nProcessing most recent PR #${pr.number}: ${pr.title}`); + console.log(`Branch: ${pr.head.ref}`); + console.log(`Created: ${pr.created_at}`); - // All checks passed - if (!autoMerge) { - console.log('✓ Ready to merge (auto-merge disabled)'); - results.push({ repo, pr: pr.number, status: 'ready' }); - continue; - } + // Check PR status + const { data: combinedStatus } = await github.rest.repos.getCombinedStatusForRef({ + owner, + repo, + ref: pr.head.sha + }); - if (dryRun) { - console.log('✓ Would merge (dry run)'); - results.push({ repo, pr: pr.number, status: 'dry_run_merge' }); - continue; - } + // Check check runs + const { data: checkRuns } = await github.rest.checks.listForRef({ + owner, + repo, + ref: pr.head.sha + }); + + const allChecks = checkRuns.check_runs || []; + const requiredChecks = allChecks.filter(c => + !c.name.includes('Detect keepalive') && + !c.name.includes('pr_meta') && + !c.name.includes('resolve_pr') && + !c.name.includes('Cleanup') && + c.conclusion !== 'skipped' && + c.conclusion !== 'neutral' + ); + + const failedChecks = requiredChecks.filter(c => + c.conclusion !== 'success' && c.conclusion !== null + ); + + const pendingChecks = requiredChecks.filter(c => + c.status !== 'completed' + ); + + console.log(`Checks: ${requiredChecks.length} total, ${failedChecks.length} failed, ${pendingChecks.length} pending`); + + if (failedChecks.length > 0) { + console.log('Failed checks:'); + failedChecks.forEach(c => console.log(` - ${c.name}: ${c.conclusion}`)); + results.push({ repo, pr: pr.number, status: 'checks_failed' }); + continue; + } + + if (pendingChecks.length > 0) { + console.log('Waiting for checks to complete'); + results.push({ repo, pr: pr.number, status: 'checks_pending' }); + continue; + } + + // All checks passed + if (!autoMerge) { + console.log('✓ Ready to merge (auto-merge disabled)'); + results.push({ repo, pr: pr.number, status: 'ready' }); + continue; + } + + if (dryRun) { + console.log('✓ Would merge (dry run)'); + results.push({ repo, pr: pr.number, status: 'dry_run_merge' }); + continue; + } - // Merge the PR + // Merge the PR + try { + await github.rest.pulls.merge({ + owner, + repo, + pull_number: pr.number, + merge_method: 'merge', + commit_title: pr.title, + commit_message: `Automated merge of sync PR\n\nSync hash: ${pr.head.ref.split('-').pop()}` + }); + + console.log('✓ Merged successfully'); + + // Delete the branch try { - await github.rest.pulls.merge({ + await github.rest.git.deleteRef({ owner, repo, - pull_number: pr.number, - merge_method: 'merge', - commit_title: pr.title, - commit_message: `Automated merge of sync PR\n\nSync hash: ${pr.head.ref.split('-').pop()}` + ref: `heads/${pr.head.ref}` }); - - console.log('✓ Merged successfully'); - - // Delete the branch - try { - await github.rest.git.deleteRef({ - owner, - repo, - ref: `heads/${pr.head.ref}` - }); - console.log('✓ Branch deleted'); - } catch (e) { - console.log(`⚠ Could not delete branch: ${e.message}`); - } - - results.push({ repo, pr: pr.number, status: 'merged' }); + console.log('✓ Branch deleted'); } catch (e) { - console.log(`✗ Merge failed: ${e.message}`); - results.push({ repo, pr: pr.number, status: 'merge_failed', error: e.message }); + console.log(`⚠ Could not delete branch: ${e.message}`); } + + results.push({ repo, pr: pr.number, status: 'merged' }); + } catch (e) { + console.log(`✗ Merge failed: ${e.message}`); + results.push({ repo, pr: pr.number, status: 'merge_failed', error: e.message }); } } catch (e) { console.log(`✗ Error processing ${repo}: ${e.message}`); @@ -211,11 +267,13 @@ jobs: console.log(JSON.stringify(results, null, 2)); const merged = results.filter(r => r.status === 'merged').length; + const stale = results.filter(r => r.status === 'stale_closed').length; const failed = results.filter(r => r.status === 'checks_failed' || r.status === 'merge_failed').length; const pending = results.filter(r => r.status === 'checks_pending').length; const ready = results.filter(r => r.status === 'ready').length; console.log(`\nMerged: ${merged}`); + console.log(`Stale closed: ${stale}`); console.log(`Failed: ${failed}`); console.log(`Pending: ${pending}`); console.log(`Ready (not auto-merged): ${ready}`); From 80329a4155dc797b1930bc174869a054d7a8966b Mon Sep 17 00:00:00 2001 From: stranske Date: Tue, 6 Jan 2026 13:41:10 +0000 Subject: [PATCH 05/10] fix: dynamically read REGISTERED_CONSUMER_REPOS from source file - Extract consumer repo list from maint-68-sync-consumer-repos.yml at runtime - Use yq to parse the authoritative REGISTERED_CONSUMER_REPOS env var - Remove duplicated hardcoded list to maintain single source of truth --- .github/workflows/maint-71-merge-sync-prs.yml | 29 ++++++++----------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/.github/workflows/maint-71-merge-sync-prs.yml b/.github/workflows/maint-71-merge-sync-prs.yml index 3bbece9ec..ebbcfe126 100644 --- a/.github/workflows/maint-71-merge-sync-prs.yml +++ b/.github/workflows/maint-71-merge-sync-prs.yml @@ -55,17 +55,6 @@ on: permissions: contents: read -env: - # Pull from same source as sync workflow - keep in sync! - REGISTERED_CONSUMER_REPOS: | - stranske/Travel-Plan-Permission - stranske/Template - stranske/trip-planner - stranske/Manager-Database - stranske/Portable-Alpha-Extension-Model - stranske/Trend_Model_Project - stranske/Collab-Admin - jobs: merge_sync_prs: runs-on: ubuntu-latest @@ -73,6 +62,16 @@ jobs: - name: Checkout uses: actions/checkout@v4 + - name: Extract consumer repos from sync workflow + id: repos + run: | + # Read REGISTERED_CONSUMER_REPOS from maint-68-sync-consumer-repos.yml + repos=$(yq eval '.env.REGISTERED_CONSUMER_REPOS' \ + .github/workflows/maint-68-sync-consumer-repos.yml | \ + grep -v '^#' | grep -v '^$' | sed 's/stranske\///' | tr '\n' ',' | sed 's/,$//') + echo "list=$repos" >> $GITHUB_OUTPUT + echo "Extracted repos: $repos" + - name: Check and merge sync PRs uses: actions/github-script@v7 with: @@ -83,12 +82,8 @@ jobs: const autoMerge = ${{ inputs.auto_merge }}; const dryRun = ${{ inputs.dry_run }}; - // Parse multiline REGISTERED_CONSUMER_REPOS into array - const registeredRepos = process.env.REGISTERED_CONSUMER_REPOS - .split('\n') - .map(line => line.trim()) - .filter(line => line && !line.startsWith('#')) - .map(repo => repo.replace(/^stranske\//, '')); + // Parse repos from previous step + const registeredRepos = '${{ steps.repos.outputs.list }}'.split(',').map(r => r.trim()); // Determine which repos to process const targetRepos = inputRepos === 'all' From d4135677700dd71c3c6f38ae638847275f473874 Mon Sep 17 00:00:00 2001 From: stranske Date: Tue, 6 Jan 2026 13:46:52 +0000 Subject: [PATCH 06/10] fix: address bot review comments in validation script - Change default max_length from 150 to 100 to match repo standards (black, ruff, isort) - Add explicit encoding='utf-8' to all file operations for cross-platform compatibility - Remove redundant condition check (already verified by elif condition) --- scripts/validate_workflow_yaml.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/scripts/validate_workflow_yaml.py b/scripts/validate_workflow_yaml.py index 1ea08344a..fd031167a 100755 --- a/scripts/validate_workflow_yaml.py +++ b/scripts/validate_workflow_yaml.py @@ -17,10 +17,10 @@ sys.exit(1) -def check_line_length(file_path: Path, max_length: int = 150) -> list[tuple[int, str]]: +def check_line_length(file_path: Path, max_length: int = 100) -> list[tuple[int, str]]: """Check for lines that exceed maximum length (may cause wrapping issues).""" issues = [] - with open(file_path) as f: + with open(file_path, encoding="utf-8") as f: for line_num, line in enumerate(f, 1): if len(line.rstrip()) > max_length: issues.append((line_num, f"Line exceeds {max_length} characters")) @@ -30,7 +30,7 @@ def check_line_length(file_path: Path, max_length: int = 150) -> list[tuple[int, def check_runs_on_placement(file_path: Path) -> list[tuple[int, str]]: """Check that 'runs-on' is properly placed on its own line.""" issues = [] - with open(file_path) as f: + with open(file_path, encoding="utf-8") as f: for line_num, line in enumerate(f, 1): stripped = line.strip() if "runs-on:" in stripped: @@ -50,7 +50,7 @@ def check_yaml_syntax(file_path: Path) -> list[tuple[int, str]]: """Validate basic YAML syntax.""" issues = [] try: - with open(file_path) as f: + with open(file_path, encoding="utf-8") as f: yaml.safe_load(f) except yaml.YAMLError as e: line_num = getattr(e, "problem_mark", None) @@ -64,7 +64,7 @@ def check_yaml_syntax(file_path: Path) -> list[tuple[int, str]]: def check_multiline_conditions(file_path: Path) -> list[tuple[int, str]]: """Check for complex conditions that should use multiline format.""" issues = [] - with open(file_path) as f: + with open(file_path, encoding="utf-8") as f: lines = f.readlines() for line_num, line in enumerate(lines, 1): stripped = line.strip() @@ -80,7 +80,7 @@ def check_multiline_conditions(file_path: Path) -> list[tuple[int, str]]: ) # Check if next line looks like continuation without proper multiline syntax elif stripped.startswith("if:") and line_num < len(lines): - next_line = lines[line_num].strip() if line_num < len(lines) else "" + next_line = lines[line_num].strip() # Check if 'runs-on:' appears mid-line (indicates malformed wrapping) if next_line and "runs-on:" in next_line and not next_line.startswith("runs-on:"): issues.append( From d7ef9de9788aab0f4468d78442b4279bbb1772e7 Mon Sep 17 00:00:00 2001 From: stranske Date: Tue, 6 Jan 2026 13:52:37 +0000 Subject: [PATCH 07/10] docs: add workflow artifact checklist to prevent .gitignore conflicts - Add critical section to CLAUDE.md about checking new workflows for file artifacts - Create comprehensive WORKFLOW_ARTIFACT_CHECKLIST.md with decision trees and examples - Document common artifact patterns that cause merge conflicts in consumer repos - Provide recovery procedures for artifact pollution - Emphasize template workflows sync to 7+ repos (one mistake = 7+ conflicts) --- CLAUDE.md | 30 ++++ docs/WORKFLOW_ARTIFACT_CHECKLIST.md | 247 ++++++++++++++++++++++++++++ 2 files changed, 277 insertions(+) create mode 100644 docs/WORKFLOW_ARTIFACT_CHECKLIST.md diff --git a/CLAUDE.md b/CLAUDE.md index e7566088b..5a7bbb1b0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -177,6 +177,36 @@ Repo standards (from pyproject.toml): - Format: black, ruff, isort - All templates must pass validation before commit +## ⚠️ CRITICAL: New Workflow Artifact Check + +**BEFORE adding any new workflow, check if it creates files that should be in .gitignore:** + +```bash +# 1. Review workflow for file creation +grep -E "write|create|output|artifact" .github/workflows/your-workflow.yml + +# 2. Check if workflow writes to working directory +# Look for: >, >>, tee, echo >>, python open(), write_file, etc. + +# 3. Common artifacts that MUST be in .gitignore: +# - Status files: *-status.json, *-report.json, *-summary.json +# - Temp files: *.tmp, *.temp, .cache/, tmp/ +# - Agent files: codex-output.md, verifier-context.md +# - Build artifacts: dist/, build/, .artifacts/ + +# 4. Test the workflow and check git status +gh workflow run your-workflow.yml +# Wait for completion, then: +git status --ignored + +# 5. Add any new artifacts to .gitignore BEFORE syncing templates +``` + +**Why this matters:** +- Workflows that create tracked files → merge conflicts in consumer repos +- Auto-generated files in git → hours of debugging conflict resolution +- One forgotten artifact file → 7+ repos with conflicts + ## Quick Commands ```bash diff --git a/docs/WORKFLOW_ARTIFACT_CHECKLIST.md b/docs/WORKFLOW_ARTIFACT_CHECKLIST.md new file mode 100644 index 000000000..de70d7c42 --- /dev/null +++ b/docs/WORKFLOW_ARTIFACT_CHECKLIST.md @@ -0,0 +1,247 @@ +# Workflow Artifact Checklist + +## Before Creating or Modifying Any Workflow + +**ALWAYS check if the workflow creates files that need to be in .gitignore** + +### Why This Matters + +Workflows that write files to the working directory can cause: +- **Merge conflicts** in consumer repos when templates sync +- **Hours of debugging** to resolve conflicts across 7+ repos +- **CI failures** from uncommitted tracked files +- **Git pollution** from auto-generated temporary files + +### Quick Check Process + +```bash +# 1. Search workflow for file-writing operations +grep -iE "(write|create|>>|>|tee|artifact|output)" .github/workflows/your-workflow.yml + +# 2. Look for these patterns that write files: +# - Shell redirects: echo "text" > file.txt +# - Append operations: data >> log.txt +# - Python writes: open('file.txt', 'w') +# - File creation: touch, mkdir -p, cp +# - Artifact exports: actions/upload-artifact + +# 3. Run the workflow and check git status +gh workflow run your-workflow.yml -f test_param=value +# Wait for completion... +git status --ignored + +# 4. Check for new untracked or ignored files +git ls-files --others --exclude-standard +git ls-files --ignored --exclude-standard +``` + +### Common Artifact Patterns That Need .gitignore + +| Pattern | Why It Should Be Ignored | Example Workflows | +|---------|-------------------------|-------------------| +| `*-status.json` | Auto-generated status tracking | Gate, autofix, keepalive | +| `*-report.json` | Workflow output reports | CI, testing, coverage | +| `*-summary.json` | Summary files for step outputs | Agent workflows | +| `codex-*.md` | Agent prompt/output files | Keepalive, agent bridge | +| `verifier-context.md` | Verification state | Autofix loop | +| `*.tmp`, `*.temp` | Temporary processing files | Any script-heavy workflow | +| `.cache/`, `tmp/` | Cache directories | Build, test, validation | +| `dist/`, `build/` | Build outputs | Package workflows | + +### Workflow Types Requiring Extra Scrutiny + +#### 1. Agent/LLM Workflows +```yaml +# These often create prompt/output files +- codex-prompt.md +- codex-output.md +- verifier-context.md +- agent-state-*.json +``` + +#### 2. Status/Report Workflows +```yaml +# These track state across runs +- autofix_report_enriched.json +- keepalive-metrics.ndjson +- ci-status-*.json +``` + +#### 3. Build/Artifact Workflows +```yaml +# These create build outputs +- dist/ +- build/ +- .artifacts/ +- *.whl, *.tar.gz +``` + +#### 4. Test/Coverage Workflows +```yaml +# These generate test artifacts +- coverage.json +- .coverage +- htmlcov/ +- test-results/ +``` + +### Decision Tree + +``` +Does the workflow write files to the repo? +├─ Yes → Are they needed in git history? +│ ├─ No → Add to .gitignore ✓ +│ └─ Yes → Are they auto-generated? +│ ├─ Yes → Use workflow artifacts instead, add to .gitignore ✓ +│ └─ No → Ensure manual review process for commits +└─ No → Safe to proceed ✓ +``` + +### Safe Alternatives to Writing Files + +Instead of writing status/report files to the working directory: + +1. **Use workflow artifacts** (don't pollute git): + ```yaml + - uses: actions/upload-artifact@v4 + with: + name: report + path: report.json + ``` + +2. **Use step outputs** (for small data): + ```yaml + - id: status + run: echo "result=success" >> $GITHUB_OUTPUT + ``` + +3. **Use job summaries** (for readable reports): + ```yaml + - run: echo "## Status" >> $GITHUB_STEP_SUMMARY + ``` + +4. **Use PR comments** (for visibility): + ```yaml + - uses: actions/github-script@v7 + with: + script: | + github.rest.issues.createComment({ + issue_number: context.issue.number, + body: 'Status: Success' + }) + ``` + +### Checklist for New Workflows + +- [ ] Searched workflow file for file-writing operations +- [ ] Identified all files created in working directory +- [ ] Added necessary patterns to .gitignore +- [ ] Tested workflow with `git status --ignored` +- [ ] Verified no new untracked files appear +- [ ] Documented any intentional tracked files +- [ ] Considered using workflow artifacts instead +- [ ] Checked if template sync will propagate artifacts to consumer repos + +### Template Workflows (Extra Critical) + +Workflows in `templates/consumer-repo/.github/workflows/`: +- **Will sync to 7+ consumer repos** +- **One artifact file = 7+ repos with conflicts** +- **Must be perfect before first sync** + +Before syncing templates: +```bash +# 1. Add artifacts to consumer repo template .gitignore +vim templates/consumer-repo/.gitignore + +# 2. Add artifacts to main repo .gitignore +vim .gitignore + +# 3. Test workflow in main repo first +gh workflow run your-workflow.yml + +# 4. Verify no artifacts committed +git status --ignored + +# 5. Dry-run sync to see impact +gh workflow run maint-68-sync-consumer-repos.yml -f dry_run=true + +# 6. If clean, sync to consumers +gh workflow run maint-68-sync-consumer-repos.yml +``` + +### Recovery from Artifact Pollution + +If you've already synced a workflow that creates tracked files: + +```bash +# 1. Add patterns to .gitignore in Workflows repo +echo "pattern-*.json" >> .gitignore + +# 2. Add to consumer repo template .gitignore +echo "pattern-*.json" >> templates/consumer-repo/.gitignore + +# 3. Remove from git tracking (but keep files) +git rm --cached pattern-*.json +git commit -m "chore: untrack auto-generated artifact files" + +# 4. Sync updated .gitignore to all consumer repos +gh workflow run maint-68-sync-consumer-repos.yml + +# 5. In each consumer repo, remove from tracking +for repo in Travel-Plan-Permission Template trip-planner Manager-Database \ + Portable-Alpha-Extension-Model Trend_Model_Project Collab-Admin; do + git clone "git@github.com:stranske/${repo}.git" "/tmp/${repo}" + cd "/tmp/${repo}" + git rm --cached pattern-*.json || true + git commit -m "chore: untrack auto-generated artifact files" || true + git push + cd - +done +``` + +## Examples + +### ✅ Good: No File Pollution +```yaml +- name: Generate report + run: | + python generate_report.py > report.json + +- name: Upload report + uses: actions/upload-artifact@v4 + with: + name: report + path: report.json +``` + +### ❌ Bad: Creates Tracked File +```yaml +- name: Generate report + run: python generate_report.py > report.json + +# report.json now in working directory, will be tracked! +``` + +### ✅ Good: Use Step Outputs +```yaml +- id: status + run: | + STATUS=$(python check_status.py) + echo "result=${STATUS}" >> $GITHUB_OUTPUT + +- name: Use status + run: echo "${{ steps.status.outputs.result }}" +``` + +### ❌ Bad: Write Status File +```yaml +- run: python check_status.py > status.json +- run: cat status.json # status.json now tracked! +``` + +## See Also + +- [CLAUDE.md](../CLAUDE.md) - Critical debugging workflow +- [.gitignore](../.gitignore) - Current ignore patterns +- [templates/consumer-repo/.gitignore](../templates/consumer-repo/.gitignore) - Consumer repo patterns From 2a4a4c0d3ad553f2917840e59e76978e85d4bc03 Mon Sep 17 00:00:00 2001 From: stranske Date: Tue, 6 Jan 2026 14:01:18 +0000 Subject: [PATCH 08/10] docs: add critical section on handling agent bot review comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Require addressing ALL bot comments before merging PRs - Document that bot comments are mandatory fixes, not suggestions - Provide process for evaluating and resolving bot feedback - Emphasize impact: ignored comments → bugs in 7+ consumer repos - Add examples of critical issues bots catch (encoding, defaults, logic) --- CLAUDE.md | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 5a7bbb1b0..ce729433d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -153,6 +153,55 @@ Secrets use **lowercase** in `workflow_call` definitions but reference org secre 4. **Run pre-sync validation** to ensure files pass consumer lint rules 5. **Sync to ALL consumer repos** to maintain consistency +## ⚠️ CRITICAL: Agent Bot Review Comments + +**BEFORE merging any PR, check for and address ALL agent bot comments.** + +Agent bots (like `copilot-pull-request-reviewer`) analyze code and flag: +- Standards violations (line length, encoding, etc.) +- Logic errors (redundant conditions, incorrect defaults) +- Best practices (cross-platform compatibility, error handling) +- Repository inconsistencies (mismatched configs) + +**These comments are NOT suggestions - they are issues that MUST be fixed.** + +### Process for Bot Comments + +```bash +# 1. Check PR for bot comments +gh pr view --repo stranske/Workflows --comments + +# 2. For each unresolved comment: +# - Evaluate if valid (assume yes unless proven wrong) +# - Implement the fix +# - Test the change +# - Commit with clear explanation + +# 3. Do NOT merge until all bot comments are resolved or explicitly dismissed with justification + +# 4. If bot suggests code change, USE the suggested code unless there's a technical reason not to +``` + +### Why This Matters + +- Bot comments often catch issues that break consumer repos +- One ignored comment = potential bugs in 7+ repos after sync +- Bots enforce repository standards (line-length, encoding, etc.) +- Standards violations fail CI in consumer repos + +### Examples of Critical Bot Catches + +- Wrong default parameter values that don't match repo config +- Missing encoding specifications that cause Windows failures +- Redundant logic that indicates misunderstanding +- Line length violations that fail linter checks + +**If you disagree with a bot comment:** +1. Explain why in PR comment +2. Tag the bot comment as "won't fix" with justification +3. Document the decision in code comments if needed +4. Do NOT silently ignore + ## ⚠️ CRITICAL: Template Changes (READ THIS!) **If you modify `templates/consumer-repo/` YOU WILL SYNC TO ALL CONSUMER REPOS.** From ba5705725e8d568919d19d45c030f6fb4127eb55 Mon Sep 17 00:00:00 2001 From: stranske Date: Tue, 6 Jan 2026 14:02:48 +0000 Subject: [PATCH 09/10] fix: register maint-71-merge-sync-prs.yml in tests and docs - Add workflow to EXPECTED_NAMES test mapping - Document in docs/ci/WORKFLOWS.md with description - Add to docs/ci/WORKFLOW_SYSTEM.md workflow table - Fixes test failures: test_canonical_workflow_names_match_expected_mapping, test_workflow_names_match_filename_convention, test_inventory_docs_list_all_workflows --- docs/ci/WORKFLOWS.md | 1 + docs/ci/WORKFLOW_SYSTEM.md | 1 + tests/workflows/test_workflow_naming.py | 1 + 3 files changed, 3 insertions(+) diff --git a/docs/ci/WORKFLOWS.md b/docs/ci/WORKFLOWS.md index 9bf553458..b98b07294 100644 --- a/docs/ci/WORKFLOWS.md +++ b/docs/ci/WORKFLOWS.md @@ -164,6 +164,7 @@ Scheduled health jobs keep the automation ecosystem aligned: * [`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-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). * [`maint-71-auto-fix-integration.yml`](../../.github/workflows/maint-71-auto-fix-integration.yml) automatically applies formatting fixes to Integration-Tests when triggered by issue comments or workflow failures. +* [`maint-71-merge-sync-prs.yml`](../../.github/workflows/maint-71-merge-sync-prs.yml) automates merging sync PRs in consumer repos - checks status, merges passing PRs, cleans up stale PRs (manual dispatch). Together these workflows define the CI surface area referenced by Gate and the Gate summary job, keeping the automation stack observable, testable, and easier to evolve. diff --git a/docs/ci/WORKFLOW_SYSTEM.md b/docs/ci/WORKFLOW_SYSTEM.md index 71c43953a..842b56e94 100644 --- a/docs/ci/WORKFLOW_SYSTEM.md +++ b/docs/ci/WORKFLOW_SYSTEM.md @@ -701,6 +701,7 @@ Keep this table handy when you are triaging automation: it confirms which workfl | **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) | | **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) | | **Auto-Fix Integration Test Failures** (`maint-71-auto-fix-integration.yml`, maintenance bucket) | `issues` (labeled), `workflow_run` (failed) | Automatically applies Black and Ruff formatting fixes to Python files in the Workflows-Integration-Tests repository when triggered by issue labels or workflow failures. | 🟢 Automated | [Auto-fix runs](https://github.com/stranske/Workflows/actions/workflows/maint-71-auto-fix-integration.yml) | +| **Merge Sync PRs** (`maint-71-merge-sync-prs.yml`, maintenance bucket) | `workflow_dispatch`, `workflow_call` | Automates merging sync PRs across consumer repos. Checks CI status, merges passing PRs, cleans up stale sync PRs. Reads consumer repo list from maint-68-sync-consumer-repos.yml. | ⚪ Manual/callable | [Merge sync runs](https://github.com/stranske/Workflows/actions/workflows/maint-71-merge-sync-prs.yml) | | **Maint 60 Release** (`maint-60-release.yml`, maintenance bucket) | `push` (tags `v*`) | Create GitHub releases automatically when version tags are pushed. | ⚪ Tag-triggered | [Release workflow runs](https://github.com/stranske/Trend_Model_Project/actions/workflows/maint-60-release.yml) | | **Maint 61 Create Floating v1 Tag** (`maint-61-create-floating-v1-tag.yml`, maintenance bucket) | `workflow_dispatch` | Create or refresh the floating `v1` tag to point at the latest `v1.x` release. | ⚪ Manual | [Floating tag workflow runs](https://github.com/stranske/Workflows/actions/workflows/maint-61-create-floating-v1-tag.yml) | | **Agents Guard** (`agents-guard.yml`, agents bucket) | `pull_request` (path-filtered), `pull_request_target` (label/unlabel with `agent:` prefix) | Enforce protected agents workflow policies and prevent duplicate guard comments. | ✅ Required when `agents-*.yml` changes | [Agents Guard run history](https://github.com/stranske/Trend_Model_Project/actions/workflows/agents-guard.yml) | diff --git a/tests/workflows/test_workflow_naming.py b/tests/workflows/test_workflow_naming.py index 410d6b6a3..0ce49c251 100644 --- a/tests/workflows/test_workflow_naming.py +++ b/tests/workflows/test_workflow_naming.py @@ -211,6 +211,7 @@ def test_workflow_display_names_are_unique(): "maint-60-release.yml": "Maint 60 Release", "maint-70-fix-integration-formatting.yml": "Fix Integration Tests Formatting", "maint-71-auto-fix-integration.yml": "Auto-Fix Integration Test Failures", + "maint-71-merge-sync-prs.yml": "Merge Sync PRs", "maint-61-create-floating-v1-tag.yml": "Maint 61 Create Floating v1 Tag", "maint-coverage-guard.yml": "Maint Coverage Guard", "pr-00-gate.yml": "Gate", From 4f61f04ee000c2c802151b8b5065a3a00d3795e6 Mon Sep 17 00:00:00 2001 From: stranske Date: Tue, 6 Jan 2026 14:04:34 +0000 Subject: [PATCH 10/10] fix: quote shell variables in maint-71-merge-sync-prs.yml - Quote $repos variable in yq pipeline to prevent word splitting (SC2086) - Quote $GITHUB_OUTPUT and $GITHUB_STEP_SUMMARY variables - Fixes shellcheck warnings in actionlint --- .github/workflows/maint-71-merge-sync-prs.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/maint-71-merge-sync-prs.yml b/.github/workflows/maint-71-merge-sync-prs.yml index ebbcfe126..e56a81728 100644 --- a/.github/workflows/maint-71-merge-sync-prs.yml +++ b/.github/workflows/maint-71-merge-sync-prs.yml @@ -69,8 +69,8 @@ jobs: repos=$(yq eval '.env.REGISTERED_CONSUMER_REPOS' \ .github/workflows/maint-68-sync-consumer-repos.yml | \ grep -v '^#' | grep -v '^$' | sed 's/stranske\///' | tr '\n' ',' | sed 's/,$//') - echo "list=$repos" >> $GITHUB_OUTPUT - echo "Extracted repos: $repos" + echo "list=${repos}" >> "$GITHUB_OUTPUT" + echo "Extracted repos: ${repos}" - name: Check and merge sync PRs uses: actions/github-script@v7 @@ -280,8 +280,8 @@ jobs: - name: Post summary if: always() run: | - echo "### Sync PR Merge Summary" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "Processed repos: ${{ inputs.repos || 'all' }}" >> $GITHUB_STEP_SUMMARY - echo "Auto-merge: ${{ inputs.auto_merge }}" >> $GITHUB_STEP_SUMMARY - echo "Dry run: ${{ inputs.dry_run }}" >> $GITHUB_STEP_SUMMARY + echo "### Sync PR Merge Summary" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "Processed repos: ${{ inputs.repos || 'all' }}" >> "$GITHUB_STEP_SUMMARY" + echo "Auto-merge: ${{ inputs.auto_merge }}" >> "$GITHUB_STEP_SUMMARY" + echo "Dry run: ${{ inputs.dry_run }}" >> "$GITHUB_STEP_SUMMARY"