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..e56a81728 --- /dev/null +++ b/.github/workflows/maint-71-merge-sync-prs.yml @@ -0,0 +1,287 @@ +# 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 + +jobs: + merge_sync_prs: + runs-on: ubuntu-latest + steps: + - 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: + github-token: ${{ secrets.CODESPACES_WORKFLOWS || secrets.GITHUB_TOKEN }} + script: | + const owner = context.repo.owner; + const inputRepos = '${{ inputs.repos }}' || 'all'; + const autoMerge = ${{ inputs.auto_merge }}; + const dryRun = ${{ inputs.dry_run }}; + + // 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' + ? 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`); + + 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: 20 + }); + + 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; + } + + // Sort by created date, oldest first (for stale cleanup) + syncPRs.sort((a, b) => new Date(a.created_at) - new Date(b.created_at)); + + // 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`); + + 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}`); + + 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'); + } + } + } + + // 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}`); + + // 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 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}`); + + 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" diff --git a/CLAUDE.md b/CLAUDE.md index e7566088b..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.** @@ -177,6 +226,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 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/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( diff --git a/templates/consumer-repo/.github/workflows/agents-issue-intake.yml b/templates/consumer-repo/.github/workflows/agents-issue-intake.yml index 3aace466b..f066ea4e9 100644 --- a/templates/consumer-repo/.github/workflows/agents-issue-intake.yml +++ b/templates/consumer-repo/.github/workflows/agents-issue-intake.yml @@ -153,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 }} @@ -169,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" 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",