diff --git a/.egg-state/contracts/255.json b/.egg-state/contracts/255.json new file mode 100644 index 0000000000..7d69fe6cf9 --- /dev/null +++ b/.egg-state/contracts/255.json @@ -0,0 +1,609 @@ +{ + "schemaVersion": "1.0", + "issue": { + "number": 255, + "title": "Update all SDLC related workflows to be reusable", + "url": "https://github.com/jwbron/egg/issues/255" + }, + "current_phase": "implement", + "acceptance_criteria": [ + { + "id": "ac-1", + "description": "[TASK-1-1] Input defined with default, used in BOT_USERNAME env var and shell steps", + "verified": false + }, + { + "id": "ac-2", + "description": "[TASK-1-2] Input defined with default, used in uses step", + "verified": false + }, + { + "id": "ac-3", + "description": "[TASK-1-3] Wrapper passes inputs with defaults matching current behavior", + "verified": false + }, + { + "id": "ac-4", + "description": "[TASK-1-4] Wrapper uses new inputs correctly", + "verified": false + }, + { + "id": "ac-5", + "description": "[TASK-1-5] Wrapper uses new inputs correctly", + "verified": false + }, + { + "id": "ac-6", + "description": "[TASK-2-1] New reusable workflow accepts bot-username, action-ref, pr_number inputs", + "verified": false + }, + { + "id": "ac-7", + "description": "[TASK-2-2] Existing behavior preserved, uses reusable workflow internally", + "verified": false + }, + { + "id": "ac-8", + "description": "[TASK-2-3] New reusable workflow accepts bot-username, action-ref, pr_number inputs", + "verified": false + }, + { + "id": "ac-9", + "description": "[TASK-2-4] Scheduled and manual triggers work, resolve logic delegated", + "verified": false + }, + { + "id": "ac-10", + "description": "[TASK-2-5] Consuming repos can override default prompt scripts", + "verified": false + }, + { + "id": "ac-11", + "description": "[TASK-3-1] Accepts bot-username, action-ref, authorized-users, max-feedback-rounds", + "verified": false + }, + { + "id": "ac-12", + "description": "[TASK-3-2] Job-level if uses workaround, shell steps use env var", + "verified": false + }, + { + "id": "ac-13", + "description": "[TASK-3-3] Accepts bot-username, action-ref, authorized-users, mention-patterns", + "verified": false + }, + { + "id": "ac-14", + "description": "[TASK-3-4] Mention patterns defined as input, used in conditions", + "verified": false + }, + { + "id": "ac-15", + "description": "[TASK-3-5] Both workflows accept comma-separated list of authorized users", + "verified": false + }, + { + "id": "ac-16", + "description": "[TASK-4-1] Accepts bot-username, action-ref, authorized-users, branch-prefix inputs", + "verified": false + }, + { + "id": "ac-17", + "description": "[TASK-4-2] Git identity and comment filtering use input parameter", + "verified": false + }, + { + "id": "ac-18", + "description": "[TASK-4-3] Branch naming uses parameterized prefix pattern", + "verified": false + }, + { + "id": "ac-19", + "description": "[TASK-4-4] Accepts bot-username, authorized-users, branch-prefix inputs", + "verified": false + }, + { + "id": "ac-20", + "description": "[TASK-4-5] Authorization check uses input parameter", + "verified": false + }, + { + "id": "ac-21", + "description": "[TASK-5-1] Uses branch-prefix input for pattern matching", + "verified": false + }, + { + "id": "ac-22", + "description": "[TASK-5-2] Uses configurable action reference", + "verified": false + }, + { + "id": "ac-23", + "description": "[TASK-5-3] Uses configurable action reference", + "verified": false + }, + { + "id": "ac-24", + "description": "[TASK-5-4] Documents all inputs, shows example wrapper workflows", + "verified": false + }, + { + "id": "ac-25", + "description": "[TASK-5-5] Working examples for PR review, autofix, and SDLC pipeline", + "verified": false + } + ], + "phases": [ + { + "id": "phase-1", + "name": "Core Parameterization Foundation", + "status": "pending", + "tasks": [ + { + "id": "task-1-1", + "description": "Add bot-username input to reusable-review.yml", + "status": "pending", + "acceptance_criteria": "Input defined with default, used in BOT_USERNAME env var and shell steps", + "files_affected": [ + ".github/workflows/reusable-review.yml" + ], + "commit": null, + "review_cycles": 0, + "max_cycles": 3, + "escalated": false, + "notes": "" + }, + { + "id": "task-1-2", + "description": "Add action-ref input to reusable-review.yml", + "status": "pending", + "acceptance_criteria": "Input defined with default, used in uses step", + "files_affected": [ + ".github/workflows/reusable-review.yml" + ], + "commit": null, + "review_cycles": 0, + "max_cycles": 3, + "escalated": false, + "notes": "" + }, + { + "id": "task-1-3", + "description": "Update on-pull-request.yml to pass new inputs", + "status": "pending", + "acceptance_criteria": "Wrapper passes inputs with defaults matching current behavior", + "files_affected": [ + ".github/workflows/on-pull-request.yml" + ], + "commit": null, + "review_cycles": 0, + "max_cycles": 3, + "escalated": false, + "notes": "" + }, + { + "id": "task-1-4", + "description": "Update on-pull-request-agent-mode-design.yml to pass new inputs", + "status": "pending", + "acceptance_criteria": "Wrapper uses new inputs correctly", + "files_affected": [ + ".github/workflows/on-pull-request-agent-mode-design.yml" + ], + "commit": null, + "review_cycles": 0, + "max_cycles": 3, + "escalated": false, + "notes": "" + }, + { + "id": "task-1-5", + "description": "Update on-pull-request-contract-verify.yml to pass new inputs", + "status": "pending", + "acceptance_criteria": "Wrapper uses new inputs correctly", + "files_affected": [ + ".github/workflows/on-pull-request-contract-verify.yml" + ], + "commit": null, + "review_cycles": 0, + "max_cycles": 3, + "escalated": false, + "notes": "" + } + ], + "review_cycles": 0, + "max_cycles": 3, + "escalated": false, + "escalation_reason": null, + "review_feedback": [] + }, + { + "id": "phase-2", + "name": "Autofix and Conflict Resolution Workflows", + "status": "pending", + "tasks": [ + { + "id": "task-2-1", + "description": "Create reusable-autofix.yml from on-check-failure.yml core logic", + "status": "pending", + "acceptance_criteria": "New reusable workflow accepts bot-username, action-ref, pr_number inputs", + "files_affected": [ + ".github/workflows/reusable-autofix.yml" + ], + "commit": null, + "review_cycles": 0, + "max_cycles": 3, + "escalated": false, + "notes": "" + }, + { + "id": "task-2-2", + "description": "Convert on-check-failure.yml to thin wrapper", + "status": "pending", + "acceptance_criteria": "Existing behavior preserved, uses reusable workflow internally", + "files_affected": [ + ".github/workflows/on-check-failure.yml" + ], + "commit": null, + "review_cycles": 0, + "max_cycles": 3, + "escalated": false, + "notes": "" + }, + { + "id": "task-2-3", + "description": "Create reusable-conflict-resolve.yml from on-merge-conflict.yml", + "status": "pending", + "acceptance_criteria": "New reusable workflow accepts bot-username, action-ref, pr_number inputs", + "files_affected": [ + ".github/workflows/reusable-conflict-resolve.yml" + ], + "commit": null, + "review_cycles": 0, + "max_cycles": 3, + "escalated": false, + "notes": "" + }, + { + "id": "task-2-4", + "description": "Convert on-merge-conflict.yml to use reusable workflow", + "status": "pending", + "acceptance_criteria": "Scheduled and manual triggers work, resolve logic delegated", + "files_affected": [ + ".github/workflows/on-merge-conflict.yml" + ], + "commit": null, + "review_cycles": 0, + "max_cycles": 3, + "escalated": false, + "notes": "" + }, + { + "id": "task-2-5", + "description": "Add prompt-script input to both reusable workflows", + "status": "pending", + "acceptance_criteria": "Consuming repos can override default prompt scripts", + "files_affected": [ + ".github/workflows/reusable-autofix.yml", + ".github/workflows/reusable-conflict-resolve.yml" + ], + "commit": null, + "review_cycles": 0, + "max_cycles": 3, + "escalated": false, + "notes": "" + } + ], + "review_cycles": 0, + "max_cycles": 3, + "escalated": false, + "escalation_reason": null, + "review_feedback": [] + }, + { + "id": "phase-3", + "name": "Feedback and Mention Workflows", + "status": "pending", + "tasks": [ + { + "id": "task-3-1", + "description": "Add workflow_call trigger to on-review-feedback.yml with inputs", + "status": "pending", + "acceptance_criteria": "Accepts bot-username, action-ref, authorized-users, max-feedback-rounds", + "files_affected": [ + ".github/workflows/on-review-feedback.yml" + ], + "commit": null, + "review_cycles": 0, + "max_cycles": 3, + "escalated": false, + "notes": "" + }, + { + "id": "task-3-2", + "description": "Replace hardcoded james-in-a-box in on-review-feedback.yml", + "status": "pending", + "acceptance_criteria": "Job-level if uses workaround, shell steps use env var", + "files_affected": [ + ".github/workflows/on-review-feedback.yml" + ], + "commit": null, + "review_cycles": 0, + "max_cycles": 3, + "escalated": false, + "notes": "" + }, + { + "id": "task-3-3", + "description": "Add workflow_call trigger to on-mention.yml with inputs", + "status": "pending", + "acceptance_criteria": "Accepts bot-username, action-ref, authorized-users, mention-patterns", + "files_affected": [ + ".github/workflows/on-mention.yml" + ], + "commit": null, + "review_cycles": 0, + "max_cycles": 3, + "escalated": false, + "notes": "" + }, + { + "id": "task-3-4", + "description": "Replace hardcoded mention patterns with configurable input", + "status": "pending", + "acceptance_criteria": "Mention patterns defined as input, used in conditions", + "files_affected": [ + ".github/workflows/on-mention.yml" + ], + "commit": null, + "review_cycles": 0, + "max_cycles": 3, + "escalated": false, + "notes": "" + }, + { + "id": "task-3-5", + "description": "Replace hardcoded jwbron authorization with authorized-users input", + "status": "pending", + "acceptance_criteria": "Both workflows accept comma-separated list of authorized users", + "files_affected": [ + ".github/workflows/on-review-feedback.yml", + ".github/workflows/on-mention.yml" + ], + "commit": null, + "review_cycles": 0, + "max_cycles": 3, + "escalated": false, + "notes": "" + } + ], + "review_cycles": 0, + "max_cycles": 3, + "escalated": false, + "escalation_reason": null, + "review_feedback": [] + }, + { + "id": "phase-4", + "name": "Core SDLC Pipeline Parameterization", + "status": "pending", + "tasks": [ + { + "id": "task-4-1", + "description": "Add workflow_call trigger to sdlc-pipeline.yml with full input set", + "status": "pending", + "acceptance_criteria": "Accepts bot-username, action-ref, authorized-users, branch-prefix inputs", + "files_affected": [ + ".github/workflows/sdlc-pipeline.yml" + ], + "commit": null, + "review_cycles": 0, + "max_cycles": 3, + "escalated": false, + "notes": "" + }, + { + "id": "task-4-2", + "description": "Replace hardcoded james-in-a-box in git config and filtering", + "status": "pending", + "acceptance_criteria": "Git identity and comment filtering use input parameter", + "files_affected": [ + ".github/workflows/sdlc-pipeline.yml" + ], + "commit": null, + "review_cycles": 0, + "max_cycles": 3, + "escalated": false, + "notes": "" + }, + { + "id": "task-4-3", + "description": "Replace hardcoded egg/issue- branch prefix with input", + "status": "pending", + "acceptance_criteria": "Branch naming uses parameterized prefix pattern", + "files_affected": [ + ".github/workflows/sdlc-pipeline.yml" + ], + "commit": null, + "review_cycles": 0, + "max_cycles": 3, + "escalated": false, + "notes": "" + }, + { + "id": "task-4-4", + "description": "Add workflow_call trigger to sdlc-hitl.yml with authorization inputs", + "status": "pending", + "acceptance_criteria": "Accepts bot-username, authorized-users, branch-prefix inputs", + "files_affected": [ + ".github/workflows/sdlc-hitl.yml" + ], + "commit": null, + "review_cycles": 0, + "max_cycles": 3, + "escalated": false, + "notes": "" + }, + { + "id": "task-4-5", + "description": "Replace hardcoded jwbron authorization in sdlc-hitl.yml", + "status": "pending", + "acceptance_criteria": "Authorization check uses input parameter", + "files_affected": [ + ".github/workflows/sdlc-hitl.yml" + ], + "commit": null, + "review_cycles": 0, + "max_cycles": 3, + "escalated": false, + "notes": "" + } + ], + "review_cycles": 0, + "max_cycles": 3, + "escalated": false, + "escalation_reason": null, + "review_feedback": [] + }, + { + "id": "phase-5", + "name": "Utility Workflows and Documentation", + "status": "pending", + "tasks": [ + { + "id": "task-5-1", + "description": "Parameterize on-issue-closed.yml branch prefix", + "status": "pending", + "acceptance_criteria": "Uses branch-prefix input for pattern matching", + "files_affected": [ + ".github/workflows/on-issue-closed.yml" + ], + "commit": null, + "review_cycles": 0, + "max_cycles": 3, + "escalated": false, + "notes": "" + }, + { + "id": "task-5-2", + "description": "Parameterize on-push-doc-updater.yml with action-ref input", + "status": "pending", + "acceptance_criteria": "Uses configurable action reference", + "files_affected": [ + ".github/workflows/on-push-doc-updater.yml" + ], + "commit": null, + "review_cycles": 0, + "max_cycles": 3, + "escalated": false, + "notes": "" + }, + { + "id": "task-5-3", + "description": "Parameterize self-improvement.yml with action-ref input", + "status": "pending", + "acceptance_criteria": "Uses configurable action reference", + "files_affected": [ + ".github/workflows/self-improvement.yml" + ], + "commit": null, + "review_cycles": 0, + "max_cycles": 3, + "escalated": false, + "notes": "" + }, + { + "id": "task-5-4", + "description": "Create docs/guides/reusable-workflows.md consumer guide", + "status": "pending", + "acceptance_criteria": "Documents all inputs, shows example wrapper workflows", + "files_affected": [ + "docs/guides/reusable-workflows.md" + ], + "commit": null, + "review_cycles": 0, + "max_cycles": 3, + "escalated": false, + "notes": "" + }, + { + "id": "task-5-5", + "description": "Create example wrapper workflows in examples/ directory", + "status": "pending", + "acceptance_criteria": "Working examples for PR review, autofix, and SDLC pipeline", + "files_affected": [ + "examples/workflows/on-pull-request.yml", + "examples/workflows/on-check-failure.yml", + "examples/workflows/sdlc-pipeline.yml" + ], + "commit": null, + "review_cycles": 0, + "max_cycles": 3, + "escalated": false, + "notes": "" + } + ], + "review_cycles": 0, + "max_cycles": 3, + "escalated": false, + "escalation_reason": null, + "review_feedback": [] + } + ], + "decisions": [], + "circuit_breaker": { + "total_cycles": 0, + "max_total_cycles": 10, + "status": "closed" + }, + "workflow_owner": "jwbron", + "audit_log": [ + { + "timestamp": "2026-02-09T02:05:52Z", + "actor": "system", + "role": "reviewer", + "action": "update", + "field_path": "refine_review_cycles", + "old_value": 0, + "new_value": 1, + "reason": "Refine review cycle 1: approved" + }, + { + "timestamp": "2026-02-09T02:45:17Z", + "actor": "jwbron", + "role": "human", + "action": "transition", + "field_path": "current_phase", + "old_value": "refine", + "new_value": "plan", + "reason": "Phase approved by human" + }, + { + "timestamp": "2026-02-09T02:50:59Z", + "actor": "system", + "role": "reviewer", + "action": "update", + "field_path": "plan_review_cycles", + "old_value": 0, + "new_value": 1, + "reason": "Plan review cycle 1: approved" + }, + { + "timestamp": "2026-02-09T02:54:55Z", + "actor": "jwbron", + "role": "human", + "action": "transition", + "field_path": "current_phase", + "old_value": "plan", + "new_value": "implement", + "reason": "Phase approved by human" + } + ], + "refine_review_cycles": 1, + "refine_review_feedback": "", + "plan_review_cycles": 1, + "plan_review_feedback": "", + "pr": { + "title": "Make SDLC workflows reusable for external repositories", + "description": "Converts all SDLC-related workflows to reusable workflows that other repositories\ncan adopt. Parameterizes hardcoded values (bot username, authorized users, action\nreference, branch prefix) while maintaining backward compatibility via defaults.\n\nCloses #255" + } +} diff --git a/.egg-state/drafts/255-analysis.md b/.egg-state/drafts/255-analysis.md new file mode 100644 index 0000000000..2c5c77a202 --- /dev/null +++ b/.egg-state/drafts/255-analysis.md @@ -0,0 +1,234 @@ +# Analysis: Update all SDLC related workflows to be reusable + +> Issue: #255 | Phase: refine + +## Problem Statement + +The egg repository contains a comprehensive SDLC pipeline system with multiple GitHub workflows that automate code review, merge conflict resolution, check failure autofixing, and human-in-the-loop decision handling. Currently, these workflows are tightly coupled to the `jwbron/egg` repository and cannot be easily reused by other repositories that want to adopt the same automated development workflow. + +**Current state:** All SDLC workflows contain hardcoded values (bot usernames, authorized users, action references, workflow file names) that assume the workflows run in the `jwbron/egg` repository. + +**Desired outcome:** Each SDLC-related workflow should be configurable as either a reusable workflow (using `workflow_call`) or the workflows should be refactored to call the existing composite action (`action/action.yml`), allowing any repository to adopt the egg SDLC pipeline with their own bot identities and configurations. + +## Current Behavior + +### Workflows Identified for Reusability + +| Workflow | Purpose | Lines | Hardcoded Values | +|----------|---------|-------|------------------| +| `sdlc-pipeline.yml` | Core SDLC orchestrator (init, implement, PR phases) | 2436 | `james-in-a-box`, `jwbron/egg/action@main`, `jwbron` | +| `sdlc-hitl.yml` | Human-in-the-loop decision handler | 652 | `james-in-a-box`, `jwbron` | +| `reusable-review.yml` | Generic review bot template | 411 | `james-in-a-box`, `jwbron/egg/action@main` | +| `on-pull-request.yml` | Code review trigger | 30 | Workflow reference to reusable-review | +| `on-review-feedback.yml` | Feedback addressing loop | 323 | `james-in-a-box` | +| `on-check-failure.yml` | Autofix CI failures | 175 | `james-in-a-box`, `jwbron/egg/action@main` | +| `on-mention.yml` | @mention responder | 211 | `james-in-a-box`, `@egg`, `jwbron`, `jwbron/egg/action@main` | +| `on-merge-conflict.yml` | Conflict resolver | 408 | `james-in-a-box`, `jwbron/egg/action@main` | +| `on-issue-closed.yml` | SDLC cleanup | 220 | `egg/issue-` branch pattern | +| `on-push-doc-updater.yml` | Documentation automation | 106 | `jwbron/egg/action@main` | +| `on-pull-request-agent-mode-design.yml` | Design review | 40 | Workflow reference | +| `on-pull-request-contract-verify.yml` | Contract verification | 86 | Workflow reference, `egg/issue-` pattern | +| `self-improvement.yml` | System health monitoring | 296 | `jwbron/egg/action@main` | + +### Workflows NOT Requiring Reusability (Repo-Specific) + +- `test.yml`, `lint.yml`, `test-integration.yml`, `test-e2e.yml` - egg's internal test suite +- `release-images.yml` - Docker image release to GHCR +- `test-action.yml` - Tests the action itself + +### Key Hardcoded Values Found + +1. **Bot username:** `james-in-a-box` (used 40+ times across workflows) +2. **Authorized user:** `jwbron` (authorization checks in HITL, mention workflows) +3. **Action reference:** `jwbron/egg/action@main` (used 13 times) +4. **Branch prefix:** `egg/` (used in branch patterns like `egg/issue-{N}`) +5. **Bot mention patterns:** `@james-in-a-box`, `@egg` +6. **Workflow file names:** `sdlc-pipeline.yml`, `sdlc-hitl.yml` (for dispatching) + +### Existing Reusability Pattern + +`reusable-review.yml` already uses `workflow_call` and demonstrates the pattern: +- Accepts `pr_number`, `bot_name`, `prompt_script`, `timeout` as inputs +- Accepts secrets via `secrets:` block +- But still hardcodes `BOT_USERNAME: james-in-a-box` internally + +## Constraints + +- **Technical constraints:** + - GitHub Actions limits `workflow_call` to a single level (no nested reusable workflows) + - Secrets must be explicitly passed to reusable workflows + - Environment variables in `if:` conditions at job level cannot reference `env:` (must be hardcoded or use inputs) + - Cross-repository workflow calls require the workflows to be in a public repo or the same organization + +- **Backward compatibility:** + - Must maintain functionality for existing `jwbron/egg` users during transition + - The `action/action.yml` composite action is the core execution engine and should remain stable + +- **Dependencies:** + - All workflows depend on the `action/action.yml` composite action + - Prompt builder scripts (`action/build-*.sh`) are specific to each workflow's purpose + - The gateway sidecar and Docker images are separate infrastructure concerns + +## Options Considered + +### Option A: Convert Each Workflow to Reusable Workflow + +**Approach:** Convert each SDLC workflow to use `workflow_call` triggers, accepting all hardcoded values as inputs with defaults matching current behavior. Consuming repos create thin wrapper workflows that call these reusable workflows. + +**Pros:** +- Most flexible approach - full customization possible +- Clear separation between framework and consumer configuration +- Existing pattern demonstrated by `reusable-review.yml` +- Workflows can still be triggered directly in `jwbron/egg` for backwards compatibility + +**Cons:** +- Requires significant refactoring (2500+ lines across 13 workflows) +- Each workflow needs extensive input/secret parameter definitions +- Consuming repos need to create wrapper workflows for each feature they want +- Complex nested dependencies (e.g., `on-pull-request.yml` calls `reusable-review.yml`) need careful handling + +### Option B: Centralize Configuration in a Single Workflow Dispatch + +**Approach:** Create a single "orchestrator" reusable workflow that accepts all configuration, then dispatches to the internal workflows. Internal workflows read from a configuration file or environment. + +**Pros:** +- Single point of configuration for consumers +- Internal workflows remain simpler +- Easier maintenance - one place to update + +**Cons:** +- Complex routing logic in orchestrator +- Loses granular control over individual features +- Configuration file approach has security implications (untrusted repos could inject config) +- Harder to debug issues + +### Option C: Template Repository + GitHub App Integration + +**Approach:** Create a template repository with all workflows configured with placeholder values. Provide a CLI or GitHub App that forks the template and customizes values. + +**Pros:** +- Simple for consumers - just fork and configure +- No cross-repo workflow call complexity +- Each consumer owns their workflow copies + +**Cons:** +- Updates to the framework don't automatically propagate +- Consumers responsible for keeping workflows in sync +- Potential for drift between implementations +- Duplicates code across many repositories + +### Option D: Hybrid - Reusable Core + Thin Wrappers with Defaults + +**Approach:** +1. Make the core action (`action/action.yml`) fully parameterized (already mostly done) +2. Convert `reusable-review.yml` and key workflows to accept all config as inputs +3. Provide example wrapper workflows that consuming repos can copy +4. Keep event-triggered workflows as thin wrappers that call reusable workflows +5. Use input defaults matching current `jwbron` setup for backward compatibility + +**Structure:** +``` +.github/workflows/ + # Reusable (workflow_call) + reusable-review.yml # Already exists, enhance with bot-username input + reusable-implement.yml # Extract implement logic from sdlc-pipeline + reusable-conflict-resolve.yml # Extract from on-merge-conflict + reusable-autofix.yml # Extract from on-check-failure + + # Event-triggered (thin wrappers for jwbron/egg) + on-pull-request.yml # Calls reusable-review with jwbron defaults + on-check-failure.yml # Calls reusable-autofix with jwbron defaults + sdlc-pipeline.yml # Orchestrator, calls reusable-implement + ... +``` + +**Pros:** +- Incremental migration - can convert one workflow at a time +- Backward compatible - jwbron/egg keeps working with defaults +- Consuming repos only need to copy thin wrappers and customize +- Core logic centralized in reusable workflows + +**Cons:** +- More workflows overall (reusable + wrappers) +- Still requires some refactoring of large workflows +- Event-triggered wrappers still need customization for bot patterns + +## Recommended Approach + +**Option D: Hybrid - Reusable Core + Thin Wrappers with Defaults** + +This approach provides the best balance of: +1. **Incremental migration** - Can convert workflows one at a time without breaking existing functionality +2. **Backward compatibility** - Default values match current `jwbron` setup +3. **Flexibility** - Consuming repos can customize any aspect via inputs +4. **Maintainability** - Core logic in reusable workflows, thin event-triggered wrappers + +### Implementation Priority + +1. **Phase 1 - Foundation:** + - Add `bot-username` input to `reusable-review.yml` (replace hardcoded `james-in-a-box`) + - Add `action-ref` input for customizable action path (default `jwbron/egg/action@main`) + - Update `on-pull-request.yml` wrapper + +2. **Phase 2 - Core SDLC:** + - Extract `reusable-implement.yml` from `sdlc-pipeline.yml` implement job + - Parameterize `sdlc-hitl.yml` authorized users + - Add workflow inputs for branch prefix pattern + +3. **Phase 3 - Automation:** + - Create `reusable-autofix.yml` from `on-check-failure.yml` + - Create `reusable-conflict-resolve.yml` from `on-merge-conflict.yml` + - Parameterize `on-review-feedback.yml` + +4. **Phase 4 - Cleanup & Documentation:** + - Parameterize remaining workflows (`on-mention.yml`, `on-issue-closed.yml`, etc.) + - Add documentation for consuming repos + - Create example wrapper workflow templates + +### Key Parameters to Add + +All reusable workflows should accept these common inputs: + +```yaml +inputs: + bot-username: + description: 'Bot GitHub username for identity and comment filtering' + required: false + type: string + default: 'james-in-a-box' + authorized-users: + description: 'Comma-separated list of users authorized to trigger agent actions' + required: false + type: string + default: 'jwbron' + action-ref: + description: 'Reference to egg action (e.g., owner/repo/action@ref)' + required: false + type: string + default: 'jwbron/egg/action@main' + branch-prefix: + description: 'Branch prefix for SDLC branches (e.g., egg/issue-{N})' + required: false + type: string + default: 'egg' +``` + +## Open Questions + +1. **Authorization model for consuming repos:** The current workflows restrict HITL and mention responses to `jwbron`. Should consuming repos: + - Define their own authorized users list? (recommended) + - Use repository collaborator permissions? + - Use team membership via GitHub API? + +2. **Prompt script customization:** The `action/build-*.sh` scripts contain logic specific to egg's conventions. Should consuming repos: + - Be expected to fork and modify these scripts? + - Have override paths configurable via workflow inputs? + - Use a standard interface with hooks for customization? + +3. **SDLC contract state storage:** The `.egg-state/` directory pattern assumes file-based state in the repository. For consuming repos: + - Is this acceptable? (commits to repo for every phase change) + - Should there be an option for external state storage? + +--- + +*Authored-by: egg* diff --git a/.egg-state/drafts/255-plan.md b/.egg-state/drafts/255-plan.md new file mode 100644 index 0000000000..7b49b5dc1f --- /dev/null +++ b/.egg-state/drafts/255-plan.md @@ -0,0 +1,323 @@ +# Plan: Update all SDLC related workflows to be reusable + +> Issue: #255 | Phase: plan + +## Summary + +This plan converts the egg SDLC workflows into reusable workflows that other repositories can adopt. Following the "Hybrid - Reusable Core + Thin Wrappers with Defaults" approach from the analysis, we will parameterize hardcoded values (bot username, authorized users, action reference, branch prefix) across 11 SDLC workflows while maintaining backward compatibility via defaults matching current `jwbron/egg` configuration. + +The key insight from the existing `reusable-review.yml` pattern is that workflows already support `workflow_call` triggers - we need to extend this parameterization consistently across all SDLC workflows and update the existing thin wrappers to demonstrate proper usage. + +## Implementation Phases + +### Phase 1: Core Parameterization Foundation + +**Goal**: Add core input parameters to `reusable-review.yml` and update event-triggered wrappers to pass them. + +**Tasks**: +- [TASK-1-1] Add `bot-username` input to `reusable-review.yml` (replacing hardcoded `james-in-a-box`) — Acceptance: Input defined with default `james-in-a-box`, used in `BOT_USERNAME` env var and all shell steps +- [TASK-1-2] Add `action-ref` input to `reusable-review.yml` (replacing hardcoded `jwbron/egg/action@main`) — Acceptance: Input defined with default `jwbron/egg/action@main`, used in `uses:` step dynamically +- [TASK-1-3] Update `on-pull-request.yml` to pass new inputs — Acceptance: Wrapper passes `bot-username` and `action-ref` with defaults matching current behavior +- [TASK-1-4] Update `on-pull-request-agent-mode-design.yml` to pass new inputs — Acceptance: Wrapper uses new inputs correctly +- [TASK-1-5] Update `on-pull-request-contract-verify.yml` to pass new inputs — Acceptance: Wrapper uses new inputs correctly + +**Dependencies**: None + +**Exit criteria**: All three PR-triggered review workflows work with new parameterized `reusable-review.yml` + +### Phase 2: Autofix and Conflict Resolution Workflows + +**Goal**: Convert `on-check-failure.yml` and `on-merge-conflict.yml` to reusable workflows with proper parameterization. + +**Tasks**: +- [TASK-2-1] Create `reusable-autofix.yml` from `on-check-failure.yml` core logic — Acceptance: New reusable workflow accepts `bot-username`, `action-ref`, `pr_number`, `failed_workflow`, `failed_run_id` inputs +- [TASK-2-2] Convert `on-check-failure.yml` to thin wrapper calling `reusable-autofix.yml` — Acceptance: Existing workflow behavior preserved, uses reusable workflow internally +- [TASK-2-3] Create `reusable-conflict-resolve.yml` from `on-merge-conflict.yml` resolve logic — Acceptance: New reusable workflow accepts `bot-username`, `action-ref`, `pr_number`, `base_ref` inputs +- [TASK-2-4] Convert `on-merge-conflict.yml` to use `reusable-conflict-resolve.yml` — Acceptance: Scheduled and manual triggers work, resolve logic delegated to reusable workflow +- [TASK-2-5] Add `prompt-script` input to both reusable workflows — Acceptance: Consuming repos can override default prompt scripts + +**Dependencies**: Phase 1 (establishes pattern) + +**Exit criteria**: Both autofix and conflict resolution work as reusable workflows, backward compatible + +### Phase 3: Feedback and Mention Workflows + +**Goal**: Parameterize `on-review-feedback.yml` and `on-mention.yml` to accept bot identity configuration. + +**Tasks**: +- [TASK-3-1] Add `workflow_call` trigger to `on-review-feedback.yml` with inputs — Acceptance: Accepts `bot-username`, `action-ref`, `authorized-users`, `max-feedback-rounds` as inputs with defaults +- [TASK-3-2] Replace hardcoded `james-in-a-box` checks in `on-review-feedback.yml` with input parameter — Acceptance: Job-level `if` condition uses workaround for input access, shell steps use env var +- [TASK-3-3] Add `workflow_call` trigger to `on-mention.yml` with inputs — Acceptance: Accepts `bot-username`, `action-ref`, `authorized-users`, `mention-patterns` as inputs +- [TASK-3-4] Replace hardcoded `@james-in-a-box` and `@egg` patterns with configurable input — Acceptance: Mention patterns defined as input, used in job-level conditions via workaround +- [TASK-3-5] Replace hardcoded `jwbron` authorization check with `authorized-users` input — Acceptance: Both workflows accept comma-separated list of authorized users + +**Dependencies**: Phase 1 + +**Exit criteria**: Feedback and mention workflows support external configuration + +### Phase 4: Core SDLC Pipeline Parameterization + +**Goal**: Parameterize the main `sdlc-pipeline.yml` and `sdlc-hitl.yml` workflows. + +**Tasks**: +- [TASK-4-1] Add `workflow_call` trigger to `sdlc-pipeline.yml` with full input set — Acceptance: Accepts `bot-username`, `action-ref`, `authorized-users`, `branch-prefix`, `sdlc-label` as inputs with defaults +- [TASK-4-2] Replace hardcoded `james-in-a-box` in git config and comment filtering — Acceptance: Git identity uses input, comment filtering uses input pattern +- [TASK-4-3] Replace hardcoded `egg/issue-` branch prefix with parameterized input — Acceptance: Branch naming uses `${branch-prefix}/issue-{N}` pattern +- [TASK-4-4] Add `workflow_call` trigger to `sdlc-hitl.yml` with authorization inputs — Acceptance: Accepts `bot-username`, `authorized-users`, `branch-prefix` as inputs +- [TASK-4-5] Replace hardcoded `jwbron` authorization in `sdlc-hitl.yml` — Acceptance: Authorization check uses input parameter via job-level workaround + +**Dependencies**: Phase 1-3 (patterns established) + +**Exit criteria**: Core SDLC orchestration supports external repos + +### Phase 5: Utility Workflows and Documentation + +**Goal**: Parameterize remaining workflows and add documentation for consuming repos. + +**Tasks**: +- [TASK-5-1] Parameterize `on-issue-closed.yml` branch prefix — Acceptance: Uses `branch-prefix` input for `egg/issue-{N}` pattern +- [TASK-5-2] Parameterize `on-push-doc-updater.yml` with `action-ref` input — Acceptance: Uses configurable action reference +- [TASK-5-3] Parameterize `self-improvement.yml` with `action-ref` input — Acceptance: Uses configurable action reference +- [TASK-5-4] Create `docs/guides/reusable-workflows.md` consumer guide — Acceptance: Documents all inputs, shows example wrapper workflows +- [TASK-5-5] Create example wrapper workflows in `examples/` directory — Acceptance: Working examples for PR review, autofix, and SDLC pipeline + +**Dependencies**: Phases 1-4 + +**Exit criteria**: All SDLC workflows parameterized, documentation complete + +## Test Strategy + +- **Unit tests**: Validate YAML syntax with `yamllint` for all modified workflows +- **Integration tests**: + - Create a test repository that calls the reusable workflows with non-default inputs + - Verify workflows run with custom bot username, action ref, and branch prefix + - Test backward compatibility by ensuring `jwbron/egg` workflows still work with defaults +- **Manual testing**: + - Trigger each workflow manually via `workflow_dispatch` to verify parameterization + - Verify comment filtering works with custom bot usernames + - Verify branch creation uses correct prefix pattern + +## Rollback Plan + +1. All changes are additive (new inputs with defaults) — existing behavior preserved +2. If issues arise, revert the specific workflow file to main: + ```bash + git checkout main -- .github/workflows/.yml + git push origin egg/issue-255 + ``` +3. Reusable workflows are called via relative paths (`./.github/workflows/`) so reverting is isolated +4. No database migrations or external state changes + +## Risk Assessment + +| Risk | Likelihood | Impact | Mitigation | +|------|------------|--------|------------| +| Breaking `workflow_call` limitations (no nested reusable workflows) | Low | High | Flatten job logic into single reusable workflow rather than chaining | +| Job-level `if` conditions can't access inputs directly | Med | Med | Use documented workaround: pass inputs as job outputs from prior job | +| GitHub Action `uses:` can't be dynamic expressions | High | High | Document that `action-ref` requires wrapper workflow to set correct value; provide examples | +| Secrets not passed by default to reusable workflows | Low | Low | Already handled via explicit `secrets:` block in existing pattern | +| Cross-repo workflow calls require public repo | Low | Med | Document requirement; egg repo is already public | + +## Migration Notes + +**For consuming repositories:** + +1. Copy example wrapper workflows from `examples/` to your `.github/workflows/` +2. Update inputs to match your bot identity: + - `bot-username`: Your GitHub App bot username + - `action-ref`: `jwbron/egg/action@main` (or pin to specific version) + - `authorized-users`: Comma-separated list of authorized GitHub usernames + - `branch-prefix`: Your preferred branch prefix (e.g., `agent` instead of `egg`) +3. Configure required secrets in repository settings: + - `BOT_APP_ID` + - `BOT_APP_PRIVATE_KEY` + - `BOT_APP_INSTALLATION_ID` + - `ANTHROPIC_OAUTH_TOKEN` + +**Breaking changes:** None. All inputs have defaults matching current behavior. + +--- + +## Structured Task Appendix + +The following YAML block is machine-readable and will be extracted into the contract. +It must accurately reflect the tasks described above. The `pr:` section provides the +title and description that will be used when creating the pull request. + +```yaml +# yaml-tasks +pr: + title: "Make SDLC workflows reusable for external repositories" + description: | + Converts all SDLC-related workflows to reusable workflows that other repositories + can adopt. Parameterizes hardcoded values (bot username, authorized users, action + reference, branch prefix) while maintaining backward compatibility via defaults. + + Closes #255 +phases: + - id: 1 + name: Core Parameterization Foundation + goal: Add core input parameters to reusable-review.yml and update wrappers + tasks: + - id: TASK-1-1 + description: Add bot-username input to reusable-review.yml + acceptance: Input defined with default, used in BOT_USERNAME env var and shell steps + files: + - .github/workflows/reusable-review.yml + - id: TASK-1-2 + description: Add action-ref input to reusable-review.yml + acceptance: Input defined with default, used in uses step + files: + - .github/workflows/reusable-review.yml + - id: TASK-1-3 + description: Update on-pull-request.yml to pass new inputs + acceptance: Wrapper passes inputs with defaults matching current behavior + files: + - .github/workflows/on-pull-request.yml + - id: TASK-1-4 + description: Update on-pull-request-agent-mode-design.yml to pass new inputs + acceptance: Wrapper uses new inputs correctly + files: + - .github/workflows/on-pull-request-agent-mode-design.yml + - id: TASK-1-5 + description: Update on-pull-request-contract-verify.yml to pass new inputs + acceptance: Wrapper uses new inputs correctly + files: + - .github/workflows/on-pull-request-contract-verify.yml + - id: 2 + name: Autofix and Conflict Resolution Workflows + goal: Convert autofix and conflict resolution to reusable workflows + tasks: + - id: TASK-2-1 + description: Create reusable-autofix.yml from on-check-failure.yml core logic + acceptance: New reusable workflow accepts bot-username, action-ref, pr_number inputs + files: + - .github/workflows/reusable-autofix.yml + - id: TASK-2-2 + description: Convert on-check-failure.yml to thin wrapper + acceptance: Existing behavior preserved, uses reusable workflow internally + files: + - .github/workflows/on-check-failure.yml + - id: TASK-2-3 + description: Create reusable-conflict-resolve.yml from on-merge-conflict.yml + acceptance: New reusable workflow accepts bot-username, action-ref, pr_number inputs + files: + - .github/workflows/reusable-conflict-resolve.yml + - id: TASK-2-4 + description: Convert on-merge-conflict.yml to use reusable workflow + acceptance: Scheduled and manual triggers work, resolve logic delegated + files: + - .github/workflows/on-merge-conflict.yml + - id: TASK-2-5 + description: Add prompt-script input to both reusable workflows + acceptance: Consuming repos can override default prompt scripts + files: + - .github/workflows/reusable-autofix.yml + - .github/workflows/reusable-conflict-resolve.yml + - id: 3 + name: Feedback and Mention Workflows + goal: Parameterize feedback and mention workflows for external configuration + tasks: + - id: TASK-3-1 + description: Add workflow_call trigger to on-review-feedback.yml with inputs + acceptance: Accepts bot-username, action-ref, authorized-users, max-feedback-rounds + files: + - .github/workflows/on-review-feedback.yml + - id: TASK-3-2 + description: Replace hardcoded james-in-a-box in on-review-feedback.yml + acceptance: Job-level if uses workaround, shell steps use env var + files: + - .github/workflows/on-review-feedback.yml + - id: TASK-3-3 + description: Add workflow_call trigger to on-mention.yml with inputs + acceptance: Accepts bot-username, action-ref, authorized-users, mention-patterns + files: + - .github/workflows/on-mention.yml + - id: TASK-3-4 + description: Replace hardcoded mention patterns with configurable input + acceptance: Mention patterns defined as input, used in conditions + files: + - .github/workflows/on-mention.yml + - id: TASK-3-5 + description: Replace hardcoded jwbron authorization with authorized-users input + acceptance: Both workflows accept comma-separated list of authorized users + files: + - .github/workflows/on-review-feedback.yml + - .github/workflows/on-mention.yml + - id: 4 + name: Core SDLC Pipeline Parameterization + goal: Parameterize main SDLC pipeline and HITL workflows + tasks: + - id: TASK-4-1 + description: Add workflow_call trigger to sdlc-pipeline.yml with full input set + acceptance: Accepts bot-username, action-ref, authorized-users, branch-prefix inputs + files: + - .github/workflows/sdlc-pipeline.yml + - id: TASK-4-2 + description: Replace hardcoded james-in-a-box in git config and filtering + acceptance: Git identity and comment filtering use input parameter + files: + - .github/workflows/sdlc-pipeline.yml + - id: TASK-4-3 + description: Replace hardcoded egg/issue- branch prefix with input + acceptance: Branch naming uses parameterized prefix pattern + files: + - .github/workflows/sdlc-pipeline.yml + - id: TASK-4-4 + description: Add workflow_call trigger to sdlc-hitl.yml with authorization inputs + acceptance: Accepts bot-username, authorized-users, branch-prefix inputs + files: + - .github/workflows/sdlc-hitl.yml + - id: TASK-4-5 + description: Replace hardcoded jwbron authorization in sdlc-hitl.yml + acceptance: Authorization check uses input parameter + files: + - .github/workflows/sdlc-hitl.yml + - id: 5 + name: Utility Workflows and Documentation + goal: Parameterize remaining workflows and add consumer documentation + tasks: + - id: TASK-5-1 + description: Parameterize on-issue-closed.yml branch prefix + acceptance: Uses branch-prefix input for pattern matching + files: + - .github/workflows/on-issue-closed.yml + - id: TASK-5-2 + description: Parameterize on-push-doc-updater.yml with action-ref input + acceptance: Uses configurable action reference + files: + - .github/workflows/on-push-doc-updater.yml + - id: TASK-5-3 + description: Parameterize self-improvement.yml with action-ref input + acceptance: Uses configurable action reference + files: + - .github/workflows/self-improvement.yml + - id: TASK-5-4 + description: Create docs/guides/reusable-workflows.md consumer guide + acceptance: Documents all inputs, shows example wrapper workflows + files: + - docs/guides/reusable-workflows.md + - id: TASK-5-5 + description: Create example wrapper workflows in examples/ directory + acceptance: Working examples for PR review, autofix, and SDLC pipeline + files: + - examples/workflows/on-pull-request.yml + - examples/workflows/on-check-failure.yml + - examples/workflows/sdlc-pipeline.yml +``` + +--- + +## Phase Approval + +When posting this plan as a GitHub comment, include an approval section at the end. +Use the phase-completion template format with the `` marker: + +### Ready for Review + + +- [ ] Approve and advance to implement phase + +--- + +*Authored-by: egg* diff --git a/.github/workflows/on-check-failure.yml b/.github/workflows/on-check-failure.yml index f6140280b9..3379e6af3f 100644 --- a/.github/workflows/on-check-failure.yml +++ b/.github/workflows/on-check-failure.yml @@ -15,161 +15,64 @@ on: type: number jobs: - autofix: - name: Autofix PR Checks + # Gate job to determine if autofix should run + should-run: + name: Check if autofix should run runs-on: ubuntu-latest - # Only run when: - # - workflow_dispatch (manual trigger) - # - workflow_run completed with failure on a PR - if: >- - github.event_name == 'workflow_dispatch' || ( - github.event.workflow_run.conclusion == 'failure' && - github.event.workflow_run.event == 'pull_request' && - github.event.workflow_run.pull_requests[0] != null - ) - - permissions: - contents: write - pull-requests: write - actions: read - - concurrency: - group: egg-autofix-${{ github.event.workflow_run.pull_requests[0].number || github.event.inputs.pr_number }} - cancel-in-progress: true - - env: - PR_NUMBER: ${{ github.event.workflow_run.pull_requests[0].number || github.event.inputs.pr_number }} - FAILED_WORKFLOW: ${{ github.event.workflow_run.name || 'manual' }} - FAILED_RUN_ID: ${{ github.event.workflow_run.id || '' }} - + outputs: + run: ${{ steps.check.outputs.run }} + pr_number: ${{ steps.check.outputs.pr_number }} + failed_workflow: ${{ steps.check.outputs.failed_workflow }} + failed_run_id: ${{ steps.check.outputs.failed_run_id }} steps: - - name: Generate bot token - id: bot-token - uses: actions/create-github-app-token@v1 - with: - app-id: ${{ secrets.BOT_APP_ID }} - private-key: ${{ secrets.BOT_APP_PRIVATE_KEY }} - - # For workflow_dispatch, fetch PR metadata via API - - name: Fetch PR metadata - id: pr-meta - run: | - pr_json=$(gh api repos/${{ github.repository }}/pulls/${{ env.PR_NUMBER }}) - { - echo "head-ref=$(echo "$pr_json" | jq -r '.head.ref')" - echo "head-repo=$(echo "$pr_json" | jq -r '.head.repo.full_name')" - echo "title=$(echo "$pr_json" | jq -r '.title')" - } >> "$GITHUB_OUTPUT" - env: - GH_TOKEN: ${{ steps.bot-token.outputs.token }} - - # Skip if PR has [skip-autofix] in title - - name: Check for skip marker - id: skip-check - env: - PR_TITLE: ${{ steps.pr-meta.outputs.title }} - run: | - if [[ "$PR_TITLE" == *"[skip-autofix]"* ]]; then - echo "skip=true" >> "$GITHUB_OUTPUT" - echo "PR has [skip-autofix] marker, skipping" - else - echo "skip=false" >> "$GITHUB_OUTPUT" - fi - - # Skip the rest of the workflow if [skip-autofix] is present - - name: Early exit if skipped - if: steps.skip-check.outputs.skip == 'true' - run: echo "Skipping autofix due to [skip-autofix] marker" - - # Minimize previous "investigating" comments to avoid clutter - - name: Minimize previous autofix comments - if: steps.skip-check.outputs.skip != 'true' - run: | - # Find previous bot comments that are "investigating" status updates - gh api "repos/${{ github.repository }}/issues/${{ env.PR_NUMBER }}/comments" \ - --jq '.[] | select(.user.login == "james-in-a-box" or .user.login == "james-in-a-box[bot]") | select(.body | test("egg is investigating|egg autofix")) | .node_id' \ - | while read -r node_id; do - # Minimize the comment using GraphQL - # shellcheck disable=SC2016 # $id is a GraphQL variable, not bash - gh api graphql -f query=' - mutation($id: ID!) { - minimizeComment(input: {subjectId: $id, classifier: OUTDATED}) { - minimizedComment { isMinimized } - } - } - ' -f id="$node_id" || true - done - env: - GH_TOKEN: ${{ steps.bot-token.outputs.token }} - - # Post acknowledgment comment - - name: Post starting comment - if: steps.skip-check.outputs.skip != 'true' - run: | - if [[ -n "${{ env.FAILED_RUN_ID }}" ]]; then - RUN_LINK="[${FAILED_WORKFLOW}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ env.FAILED_RUN_ID }})" - BODY="egg is investigating the ${RUN_LINK} check failure..." - else - BODY="egg is investigating check failures for this PR..." - fi - gh api "repos/${{ github.repository }}/issues/${{ env.PR_NUMBER }}/comments" -f body="$BODY" + - name: Check trigger conditions + id: check env: - GH_TOKEN: ${{ steps.bot-token.outputs.token }} - - # SECURITY: Build the autofixer prompt from a trusted checkout (main). - # A malicious PR could replace build-autofixer-prompt.sh to exfiltrate - # the bot token. By checking out main for prompt building, we ensure - # untrusted code never runs with secrets. - - name: Checkout main (trusted) - if: steps.skip-check.outputs.skip != 'true' - uses: actions/checkout@v4 - with: - ref: main - persist-credentials: false - - - name: Build autofixer prompt - if: steps.skip-check.outputs.skip != 'true' - id: prompt - run: bash action/build-autofixer-prompt.sh - env: - GH_TOKEN: ${{ steps.bot-token.outputs.token }} - PR_NUMBER: ${{ env.PR_NUMBER }} - FAILED_WORKFLOW: ${{ env.FAILED_WORKFLOW }} - FAILED_RUN_ID: ${{ env.FAILED_RUN_ID }} - - - name: Checkout PR branch - if: steps.skip-check.outputs.skip != 'true' - uses: actions/checkout@v4 - with: - repository: ${{ steps.pr-meta.outputs.head-repo }} - ref: ${{ steps.pr-meta.outputs.head-ref }} - persist-credentials: false - - - name: Run egg autofix - if: steps.skip-check.outputs.skip != 'true' - id: egg - uses: jwbron/egg/action@main - with: - prompt-file: ${{ steps.prompt.outputs.prompt-file }} - model: ${{ steps.prompt.outputs.model }} - anthropic-oauth-token: ${{ secrets.ANTHROPIC_OAUTH_TOKEN }} - bot-app-id: ${{ secrets.BOT_APP_ID }} - bot-app-private-key: ${{ secrets.BOT_APP_PRIVATE_KEY }} - bot-app-installation-id: ${{ secrets.BOT_APP_INSTALLATION_ID }} - bot-username: james-in-a-box - timeout: "20" - - - name: Post result comment - if: always() && !cancelled() && steps.skip-check.outputs.skip != 'true' + PR_NUMBER: ${{ github.event.workflow_run.pull_requests[0].number }} + WORKFLOW_CONCLUSION: ${{ github.event.workflow_run.conclusion }} + WORKFLOW_EVENT: ${{ github.event.workflow_run.event }} + WORKFLOW_NAME: ${{ github.event.workflow_run.name }} + WORKFLOW_RUN_ID: ${{ github.event.workflow_run.id }} run: | - RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" - - if [[ "${{ steps.egg.outcome }}" == "success" ]]; then - BODY="egg autofix completed. [View run logs](${RUN_URL})" + set -euo pipefail + if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then + { + echo "run=true" + echo "pr_number=${{ github.event.inputs.pr_number }}" + echo "failed_workflow=manual" + echo "failed_run_id=" + } >> "$GITHUB_OUTPUT" + elif [[ "$WORKFLOW_CONCLUSION" == "failure" && \ + "$WORKFLOW_EVENT" == "pull_request" && \ + -n "$PR_NUMBER" && "$PR_NUMBER" != "null" && "$PR_NUMBER" != "" ]]; then + { + echo "run=true" + echo "pr_number=${PR_NUMBER}" + echo "failed_workflow=${WORKFLOW_NAME}" + echo "failed_run_id=${WORKFLOW_RUN_ID}" + } >> "$GITHUB_OUTPUT" else - BODY="egg autofix encountered an issue. [View run logs](${RUN_URL})" + echo "run=false" >> "$GITHUB_OUTPUT" fi - gh api "repos/${{ github.repository }}/issues/${{ env.PR_NUMBER }}/comments" -f body="$BODY" - env: - GH_TOKEN: ${{ steps.bot-token.outputs.token }} + autofix: + name: Autofix PR Checks + needs: should-run + if: needs.should-run.outputs.run == 'true' + permissions: + contents: write + pull-requests: write + actions: read + uses: ./.github/workflows/reusable-autofix.yml + with: + pr_number: ${{ fromJson(needs.should-run.outputs.pr_number) }} + failed_workflow: ${{ needs.should-run.outputs.failed_workflow }} + failed_run_id: ${{ needs.should-run.outputs.failed_run_id }} + bot_username: james-in-a-box + # action_ref: jwbron/egg/action@main # Cannot be passed dynamically; hardcoded in reusable workflow + timeout: "20" + secrets: + BOT_APP_ID: ${{ secrets.BOT_APP_ID }} + BOT_APP_PRIVATE_KEY: ${{ secrets.BOT_APP_PRIVATE_KEY }} + BOT_APP_INSTALLATION_ID: ${{ secrets.BOT_APP_INSTALLATION_ID }} + ANTHROPIC_OAUTH_TOKEN: ${{ secrets.ANTHROPIC_OAUTH_TOKEN }} diff --git a/.github/workflows/on-issue-closed.yml b/.github/workflows/on-issue-closed.yml index a60d5dc620..60f87559ee 100644 --- a/.github/workflows/on-issue-closed.yml +++ b/.github/workflows/on-issue-closed.yml @@ -14,6 +14,27 @@ name: "egg: Cleanup SDLC State on Issue Close" on: issues: types: [closed] + workflow_call: + inputs: + issue_number: + description: 'Issue number to cleanup' + required: true + type: number + branch_prefix: + description: 'Prefix for issue branches (e.g., "egg" creates "egg/issue-N")' + required: false + type: string + default: "egg" + sdlc_label: + description: 'Label that identifies SDLC issues' + required: false + type: string + default: "egg-sdlc" + secrets: + BOT_APP_ID: + required: true + BOT_APP_PRIVATE_KEY: + required: true permissions: actions: write @@ -22,18 +43,50 @@ permissions: # Prevent concurrent cleanup runs for the same issue concurrency: - group: sdlc-cleanup-${{ github.event.issue.number }} + group: sdlc-cleanup-${{ github.event.issue.number || inputs.issue_number }} cancel-in-progress: false jobs: + # Resolve inputs for reusable workflow support + resolve-inputs: + name: Resolve inputs + runs-on: ubuntu-latest + outputs: + issue_number: ${{ steps.resolve.outputs.issue_number }} + branch_prefix: ${{ steps.resolve.outputs.branch_prefix }} + sdlc_label: ${{ steps.resolve.outputs.sdlc_label }} + should_check_trigger: ${{ steps.resolve.outputs.should_check_trigger }} + steps: + - name: Resolve inputs with defaults + id: resolve + run: | + echo "branch_prefix=${{ inputs.branch_prefix || 'egg' }}" >> "$GITHUB_OUTPUT" + echo "sdlc_label=${{ inputs.sdlc_label || 'egg-sdlc' }}" >> "$GITHUB_OUTPUT" + + if [[ "${{ github.event_name }}" == "workflow_call" ]]; then + echo "issue_number=${{ inputs.issue_number }}" >> "$GITHUB_OUTPUT" + echo "should_check_trigger=false" >> "$GITHUB_OUTPUT" + else + echo "issue_number=${{ github.event.issue.number }}" >> "$GITHUB_OUTPUT" + echo "should_check_trigger=true" >> "$GITHUB_OUTPUT" + fi + cleanup: name: Cleanup SDLC state + needs: resolve-inputs runs-on: ubuntu-latest - # Only run for issues with the egg-sdlc label - if: contains(github.event.issue.labels.*.name, 'egg-sdlc') + # Only run for issues with the sdlc label, or workflow_call + if: >- + always() && + needs.resolve-inputs.result == 'success' && + ( + github.event_name == 'workflow_call' || + contains(github.event.issue.labels.*.name, needs.resolve-inputs.outputs.sdlc_label) + ) env: - ISSUE_NUMBER: ${{ github.event.issue.number }} + ISSUE_NUMBER: ${{ needs.resolve-inputs.outputs.issue_number }} + BRANCH_PREFIX: ${{ needs.resolve-inputs.outputs.branch_prefix }} steps: - name: Generate bot token @@ -54,7 +107,7 @@ jobs: exit 1 fi - BRANCH_NAME="egg/issue-${ISSUE_NUMBER}" + BRANCH_NAME="${BRANCH_PREFIX}/issue-${ISSUE_NUMBER}" # Initialize cleanup status tracking { @@ -85,7 +138,7 @@ jobs: RUN_BRANCH=$(echo "$RUN_DATA" | jq -r '.head_branch // ""') RUN_ISSUE=$(echo "$RUN_DATA" | jq -r '.inputs.issue_number // ""') - if [[ "$RUN_BRANCH" == "egg/issue-${ISSUE_NUMBER}" ]] || [[ "$RUN_ISSUE" == "${ISSUE_NUMBER}" ]]; then + if [[ "$RUN_BRANCH" == "${BRANCH_PREFIX}/issue-${ISSUE_NUMBER}" ]] || [[ "$RUN_ISSUE" == "${ISSUE_NUMBER}" ]]; then echo "Cancelling sdlc-pipeline run ${RUN_ID}..." if ERROR_OUTPUT=$(gh api "repos/${{ github.repository }}/actions/runs/${RUN_ID}/cancel" -X POST 2>&1); then CANCELLED_COUNT=$((CANCELLED_COUNT + 1)) diff --git a/.github/workflows/on-mention.yml b/.github/workflows/on-mention.yml index 45cc106b3c..e990ecfdbf 100644 --- a/.github/workflows/on-mention.yml +++ b/.github/workflows/on-mention.yml @@ -9,23 +9,185 @@ on: types: [submitted] issues: types: [opened] + workflow_call: + inputs: + issue_or_pr_number: + description: 'Issue or PR number to respond to' + required: true + type: number + comment_id: + description: 'Comment ID that triggered the mention (optional)' + required: false + type: string + default: "" + bot_username: + description: 'GitHub username of the bot (for self-trigger prevention and comment filtering)' + required: false + type: string + default: "james-in-a-box" + action_ref: + description: 'Reference to egg action (owner/repo/path@ref format). NOTE: GitHub Actions uses: field cannot be dynamic - consuming repos must hardcode their action reference in the uses step below.' + required: false + type: string + default: "jwbron/egg/action@main" + authorized_users: + description: 'Comma-separated list of authorized GitHub usernames' + required: false + type: string + default: "jwbron" + mention_patterns: + description: 'Comma-separated list of mention patterns to trigger on (e.g., "@mybot,@egg")' + required: false + type: string + default: "@james-in-a-box,@egg" + prompt_script: + description: 'Path to prompt builder script (relative to repo root)' + required: false + type: string + default: "action/build-mention-prompt.sh" + timeout: + description: 'Timeout in minutes' + required: false + type: string + default: "30" + secrets: + BOT_APP_ID: + required: true + BOT_APP_PRIVATE_KEY: + required: true + BOT_APP_INSTALLATION_ID: + required: true + ANTHROPIC_OAUTH_TOKEN: + required: true jobs: + # Workaround: GitHub Actions doesn't allow accessing inputs in job-level 'if' conditions. + # This job passes inputs as outputs so subsequent jobs can use them in conditions. + resolve-inputs: + name: Resolve Inputs + runs-on: ubuntu-latest + outputs: + bot_username: ${{ steps.resolve.outputs.bot_username }} + authorized_users: ${{ steps.resolve.outputs.authorized_users }} + mention_patterns: ${{ steps.resolve.outputs.mention_patterns }} + issue_or_pr_number: ${{ steps.resolve.outputs.issue_or_pr_number }} + should_check_trigger: ${{ steps.resolve.outputs.should_check_trigger }} + steps: + - name: Resolve inputs with defaults + id: resolve + run: | + # Use inputs if called as reusable workflow, otherwise use defaults + { + echo "bot_username=${{ inputs.bot_username || 'james-in-a-box' }}" + echo "authorized_users=${{ inputs.authorized_users || 'jwbron' }}" + echo "mention_patterns=${{ inputs.mention_patterns || '@james-in-a-box,@egg' }}" + } >> "$GITHUB_OUTPUT" + + # Determine issue/PR number based on event type + if [[ "${{ github.event_name }}" == "workflow_call" ]]; then + { + echo "issue_or_pr_number=${{ inputs.issue_or_pr_number }}" + echo "should_check_trigger=false" + } >> "$GITHUB_OUTPUT" + else + # For event-triggered runs, determine the number + if [[ "${{ github.event_name }}" == "pull_request_review_comment" || "${{ github.event_name }}" == "pull_request_review" ]]; then + echo "issue_or_pr_number=${{ github.event.pull_request.number }}" >> "$GITHUB_OUTPUT" + else + echo "issue_or_pr_number=${{ github.event.issue.number }}" >> "$GITHUB_OUTPUT" + fi + echo "should_check_trigger=true" >> "$GITHUB_OUTPUT" + fi + + # For event-triggered runs, check if this is a valid mention from an authorized user + check-trigger: + name: Check Trigger + needs: resolve-inputs + if: needs.resolve-inputs.outputs.should_check_trigger == 'true' + runs-on: ubuntu-latest + outputs: + should_run: ${{ steps.check.outputs.should_run }} + steps: + - name: Check if mention is valid and from authorized user + id: check + env: + BOT_USERNAME: ${{ needs.resolve-inputs.outputs.bot_username }} + AUTHORIZED_USERS: ${{ needs.resolve-inputs.outputs.authorized_users }} + MENTION_PATTERNS: ${{ needs.resolve-inputs.outputs.mention_patterns }} + SENDER_LOGIN: ${{ github.event.sender.login }} + EVENT_NAME: ${{ github.event_name }} + COMMENT_BODY: ${{ github.event.comment.body }} + REVIEW_BODY: ${{ github.event.review.body }} + ISSUE_BODY: ${{ github.event.issue.body }} + run: | + set -euo pipefail + # Check if sender is authorized (comma-separated list) + is_authorized=false + IFS=',' read -ra users <<< "$AUTHORIZED_USERS" + for user in "${users[@]}"; do + user=$(echo "$user" | xargs) # trim whitespace + if [[ "$SENDER_LOGIN" == "$user" ]]; then + is_authorized=true + break + fi + done + + if [[ "$is_authorized" != "true" ]]; then + echo "Sender '$SENDER_LOGIN' not in authorized users: $AUTHORIZED_USERS" + echo "should_run=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + # Prevent self-triggering + if [[ "$SENDER_LOGIN" == "$BOT_USERNAME" || "$SENDER_LOGIN" == "${BOT_USERNAME}[bot]" ]]; then + echo "Ignoring self-trigger from bot" + echo "should_run=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + # Determine the body to check for mentions + body="" + if [[ "$EVENT_NAME" == "issue_comment" ]]; then + body="$COMMENT_BODY" + elif [[ "$EVENT_NAME" == "pull_request_review_comment" ]]; then + body="$COMMENT_BODY" + elif [[ "$EVENT_NAME" == "pull_request_review" ]]; then + body="$REVIEW_BODY" + elif [[ "$EVENT_NAME" == "issues" ]]; then + body="$ISSUE_BODY" + fi + + # Check if any mention pattern matches + has_mention=false + IFS=',' read -ra patterns <<< "$MENTION_PATTERNS" + for pattern in "${patterns[@]}"; do + pattern=$(echo "$pattern" | xargs) # trim whitespace + if [[ "$body" == *"$pattern"* ]]; then + has_mention=true + break + fi + done + + if [[ "$has_mention" != "true" ]]; then + echo "No matching mention pattern found in body" + echo "should_run=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + echo "Valid mention from authorized user" + echo "should_run=true" >> "$GITHUB_OUTPUT" + respond: name: Run egg + needs: [resolve-inputs, check-trigger] runs-on: ubuntu-latest - # Bot-self checks (!= james-in-a-box) are defense-in-depth: prevent - # self-triggering if authorized users list expands beyond jwbron. + # Run when: + # - workflow_call (should_check_trigger=false means check-trigger was skipped) + # - Event-triggered and check-trigger passed if: >- - ( - (github.event_name == 'issue_comment' && (contains(github.event.comment.body, '@james-in-a-box') || contains(github.event.comment.body, '@egg'))) || - (github.event_name == 'pull_request_review_comment' && (contains(github.event.comment.body, '@james-in-a-box') || contains(github.event.comment.body, '@egg'))) || - (github.event_name == 'pull_request_review' && github.event.review.body != null && (contains(github.event.review.body, '@james-in-a-box') || contains(github.event.review.body, '@egg'))) || - (github.event_name == 'issues' && (contains(github.event.issue.body, '@james-in-a-box') || contains(github.event.issue.body, '@egg'))) - ) && - github.event.sender.login == 'jwbron' && - github.event.sender.login != 'james-in-a-box' && - github.event.sender.login != 'james-in-a-box[bot]' + always() && + needs.resolve-inputs.result == 'success' && + (needs.check-trigger.result == 'skipped' || needs.check-trigger.outputs.should_run == 'true') permissions: contents: write @@ -33,11 +195,12 @@ jobs: issues: write concurrency: - group: egg-mention-${{ github.event.issue.number || github.event.pull_request.number }} + group: egg-mention-${{ needs.resolve-inputs.outputs.issue_or_pr_number }} cancel-in-progress: false env: - BOT_USERNAME: james-in-a-box + BOT_USERNAME: ${{ needs.resolve-inputs.outputs.bot_username }} + ISSUE_OR_PR_NUMBER: ${{ needs.resolve-inputs.outputs.issue_or_pr_number }} steps: - name: Generate bot token @@ -48,6 +211,7 @@ jobs: private-key: ${{ secrets.BOT_APP_PRIVATE_KEY }} - name: Acknowledge with reaction + if: github.event_name != 'workflow_call' env: GH_TOKEN: ${{ steps.bot-token.outputs.token }} run: | @@ -76,7 +240,11 @@ jobs: - name: Determine checkout ref id: ref run: | - if [[ "${{ github.event_name }}" == "issue_comment" ]]; then + if [[ "${{ github.event_name }}" == "workflow_call" ]]; then + # For reusable workflow calls, default to main + echo "ref=main" >> "$GITHUB_OUTPUT" + echo "is_pr=false" >> "$GITHUB_OUTPUT" + elif [[ "${{ github.event_name }}" == "issue_comment" ]]; then # Check if this is a PR comment by looking for pull_request in the issue PR_URL="${{ github.event.issue.pull_request.url }}" if [[ -n "$PR_URL" && "$PR_URL" != "null" ]]; then @@ -114,7 +282,7 @@ jobs: - name: Build prompt id: prompt - run: bash action/build-mention-prompt.sh + run: bash ${{ inputs.prompt_script || 'action/build-mention-prompt.sh' }} env: GH_TOKEN: ${{ steps.bot-token.outputs.token }} @@ -135,6 +303,7 @@ jobs: bot-app-private-key: ${{ secrets.BOT_APP_PRIVATE_KEY }} bot-app-installation-id: ${{ secrets.BOT_APP_INSTALLATION_ID }} bot-username: ${{ env.BOT_USERNAME }} + timeout: ${{ inputs.timeout || '30' }} # Minimize previous status comments to reduce clutter - name: Minimize previous status comments @@ -144,18 +313,10 @@ jobs: run: | set -euo pipefail - # Determine where to look for comments - if [[ "${{ github.event_name }}" == "pull_request_review_comment" || "${{ github.event_name }}" == "pull_request_review" ]]; then - ISSUE_NUMBER="${{ github.event.pull_request.number }}" - else - ISSUE_NUMBER="${{ github.event.issue.number }}" - fi - # Find previous bot comments that are status updates # Match: "egg run", "egg finished", "👀 Working on it" - # shellcheck disable=SC2016 - gh api "repos/${{ github.repository }}/issues/${ISSUE_NUMBER}/comments" \ - | jq -r --arg bot "${{ env.BOT_USERNAME }}" \ + gh api "repos/${{ github.repository }}/issues/${{ env.ISSUE_OR_PR_NUMBER }}/comments" \ + | jq -r --arg bot "$BOT_USERNAME" \ '.[] | select(.user.login == $bot or .user.login == ($bot + "[bot]")) | select(.body | test("egg run|egg finished|👀 Working on it")) | .node_id' \ | while read -r node_id; do # shellcheck disable=SC2016 # $id is a GraphQL variable, not bash @@ -178,25 +339,22 @@ jobs: run: | RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" - # Determine where to post the comment - if [[ "${{ github.event_name }}" == "pull_request_review_comment" || "${{ github.event_name }}" == "pull_request_review" ]]; then - ISSUE_NUMBER="${{ github.event.pull_request.number }}" - else - ISSUE_NUMBER="${{ github.event.issue.number }}" - fi - # Check if egg already posted a substantive comment since the triggering event # For pull_request_review events, use review.submitted_at; for comments, use comment.created_at; fallback to issue.created_at TRIGGER_TIME="${{ github.event.comment.created_at || github.event.review.submitted_at || github.event.issue.created_at }}" - # Match both "james-in-a-box" and "james-in-a-box[bot]" since GitHub App comments use the [bot] suffix - EXISTING_COMMENTS=$(gh api "repos/${{ github.repository }}/issues/${ISSUE_NUMBER}/comments" \ - --jq "[.[] | select((.user.login == \"${{ env.BOT_USERNAME }}\" or .user.login == \"${{ env.BOT_USERNAME }}[bot]\") and .created_at > \"${TRIGGER_TIME}\" and (.body | test(\"finished successfully|run failed|👀 Working on it\") | not))] | length") + # Handle null/empty TRIGGER_TIME (GitHub expression may return literal "null") + if [[ -z "$TRIGGER_TIME" || "$TRIGGER_TIME" == "null" ]]; then + TRIGGER_TIME="1970-01-01" + fi + # Match both bot username and bot[bot] since GitHub App comments use the [bot] suffix + EXISTING_COMMENTS=$(gh api "repos/${{ github.repository }}/issues/${{ env.ISSUE_OR_PR_NUMBER }}/comments" \ + --jq "[.[] | select((.user.login == \"$BOT_USERNAME\" or .user.login == \"${BOT_USERNAME}[bot]\") and .created_at > \"${TRIGGER_TIME}\" and (.body | test(\"finished successfully|run failed|👀 Working on it\") | not))] | length") if [[ "${EXISTING_COMMENTS:-0}" -gt 0 ]]; then # Agent already posted a substantive comment — only add run link on failure if [[ "${EGG_EXIT_CODE:-1}" != "0" ]]; then BODY="egg run had errors (exit code: ${EGG_EXIT_CODE:-unknown}). [View run logs](${RUN_URL})" - gh api repos/${{ github.repository }}/issues/${ISSUE_NUMBER}/comments \ + gh api repos/${{ github.repository }}/issues/${{ env.ISSUE_OR_PR_NUMBER }}/comments \ -X POST -f body="${BODY}" fi else @@ -206,6 +364,6 @@ jobs: else BODY="egg run failed (exit code: ${EGG_EXIT_CODE:-unknown}). [View run logs](${RUN_URL})" fi - gh api repos/${{ github.repository }}/issues/${ISSUE_NUMBER}/comments \ + gh api repos/${{ github.repository }}/issues/${{ env.ISSUE_OR_PR_NUMBER }}/comments \ -X POST -f body="${BODY}" fi diff --git a/.github/workflows/on-merge-conflict.yml b/.github/workflows/on-merge-conflict.yml index 1209b4182f..c19bfe123a 100644 --- a/.github/workflows/on-merge-conflict.yml +++ b/.github/workflows/on-merge-conflict.yml @@ -87,322 +87,42 @@ jobs: name: Resolve Conflicts (PR #${{ matrix.pr }}) needs: find-conflicts if: needs.find-conflicts.outputs.prs != '[]' && needs.find-conflicts.outputs.prs != '' - runs-on: ubuntu-latest strategy: matrix: pr: ${{ fromJson(needs.find-conflicts.outputs.prs) }} fail-fast: false - permissions: contents: write pull-requests: write actions: read - - concurrency: - group: egg-conflict-${{ matrix.pr }} - cancel-in-progress: false - - env: - PR_NUMBER: ${{ matrix.pr }} - - steps: - - name: Generate bot token - id: bot-token - uses: actions/create-github-app-token@v1 - with: - app-id: ${{ secrets.BOT_APP_ID }} - private-key: ${{ secrets.BOT_APP_PRIVATE_KEY }} - - - name: Fetch PR metadata - id: pr-meta - run: | - set -euo pipefail - pr_json=$(gh api repos/${{ github.repository }}/pulls/${{ env.PR_NUMBER }}) - { - echo "head-ref=$(echo "$pr_json" | jq -r '.head.ref')" - echo "head-repo=$(echo "$pr_json" | jq -r '.head.repo.full_name')" - echo "title=$(echo "$pr_json" | jq -r '.title')" - echo "base-ref=$(echo "$pr_json" | jq -r '.base.ref')" - } >> "$GITHUB_OUTPUT" - env: - GH_TOKEN: ${{ steps.bot-token.outputs.token }} - - # Skip fork PRs — bot can't push to forks - - name: Check for fork PR - id: fork-check - run: | - set -euo pipefail - if [[ "${{ steps.pr-meta.outputs.head-repo }}" != "${{ github.repository }}" ]]; then - echo "skip=true" >> "$GITHUB_OUTPUT" - echo "PR is from a fork (${{ steps.pr-meta.outputs.head-repo }}), skipping — bot cannot push to forks" - else - echo "skip=false" >> "$GITHUB_OUTPUT" - fi - - - name: Early exit if fork - if: steps.fork-check.outputs.skip == 'true' - run: echo "Skipping conflict resolution for fork PR" - - # Check for skip marker (re-verify in case title changed since detection) - - name: Check for skip marker - if: steps.fork-check.outputs.skip != 'true' - id: skip-check - env: - PR_TITLE: ${{ steps.pr-meta.outputs.title }} - run: | - set -euo pipefail - if [[ "$PR_TITLE" == *"[skip-conflict-fix]"* ]]; then - echo "skip=true" >> "$GITHUB_OUTPUT" - echo "PR has [skip-conflict-fix] marker, skipping" - else - echo "skip=false" >> "$GITHUB_OUTPUT" - fi - - - name: Early exit if skipped - if: steps.fork-check.outputs.skip != 'true' && steps.skip-check.outputs.skip == 'true' - run: echo "Skipping conflict resolution due to [skip-conflict-fix] marker" - - # Minimize previous conflict resolution comments - - name: Minimize previous conflict comments - if: steps.fork-check.outputs.skip != 'true' && steps.skip-check.outputs.skip != 'true' - run: | - set -euo pipefail - gh api "repos/${{ github.repository }}/issues/${{ env.PR_NUMBER }}/comments" \ - --jq '.[] | select(.user.login == "james-in-a-box" or .user.login == "james-in-a-box[bot]") | select(.body | test("egg is resolving|egg conflict resolution")) | .node_id' \ - | while read -r node_id; do - # shellcheck disable=SC2016 # $id is a GraphQL variable, not bash - gh api graphql -f query=' - mutation($id: ID!) { - minimizeComment(input: {subjectId: $id, classifier: OUTDATED}) { - minimizedComment { isMinimized } - } - } - ' -f id="$node_id" || echo "Failed to minimize comment $node_id" - done - env: - GH_TOKEN: ${{ steps.bot-token.outputs.token }} - - - name: Post starting comment - if: steps.fork-check.outputs.skip != 'true' && steps.skip-check.outputs.skip != 'true' - run: | - set -euo pipefail - BODY="egg is resolving merge conflicts for this PR..." - gh api "repos/${{ github.repository }}/issues/${{ env.PR_NUMBER }}/comments" -f body="$BODY" - env: - GH_TOKEN: ${{ steps.bot-token.outputs.token }} - - # SECURITY: Build the conflict prompt from a trusted checkout (main). - - name: Checkout main (trusted) - if: steps.fork-check.outputs.skip != 'true' && steps.skip-check.outputs.skip != 'true' - uses: actions/checkout@v4 - with: - ref: main - persist-credentials: false - - - name: Build conflict prompt - if: steps.fork-check.outputs.skip != 'true' && steps.skip-check.outputs.skip != 'true' - id: prompt - run: bash action/build-conflict-prompt.sh - env: - GH_TOKEN: ${{ steps.bot-token.outputs.token }} - PR_NUMBER: ${{ env.PR_NUMBER }} - BASE_REF: ${{ steps.pr-meta.outputs.base-ref }} - - - name: Checkout PR branch - if: steps.fork-check.outputs.skip != 'true' && steps.skip-check.outputs.skip != 'true' - uses: actions/checkout@v4 - with: - repository: ${{ steps.pr-meta.outputs.head-repo }} - ref: ${{ steps.pr-meta.outputs.head-ref }} - persist-credentials: false - fetch-depth: 0 - - - name: Run egg conflict resolution - if: steps.fork-check.outputs.skip != 'true' && steps.skip-check.outputs.skip != 'true' - id: egg - uses: jwbron/egg/action@main - with: - prompt-file: ${{ steps.prompt.outputs.prompt-file }} - model: ${{ steps.prompt.outputs.model }} - anthropic-oauth-token: ${{ secrets.ANTHROPIC_OAUTH_TOKEN }} - bot-app-id: ${{ secrets.BOT_APP_ID }} - bot-app-private-key: ${{ secrets.BOT_APP_PRIVATE_KEY }} - bot-app-installation-id: ${{ secrets.BOT_APP_INSTALLATION_ID }} - bot-username: james-in-a-box - timeout: "30" - - - name: Post result comment - if: always() && !cancelled() && steps.fork-check.outputs.skip != 'true' && steps.skip-check.outputs.skip != 'true' - run: | - set -euo pipefail - RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" - - if [[ "${{ steps.egg.outcome }}" == "success" ]]; then - BODY="egg conflict resolution completed. [View run logs](${RUN_URL})" - else - BODY="egg conflict resolution encountered an issue. [View run logs](${RUN_URL})" - fi - - gh api "repos/${{ github.repository }}/issues/${{ env.PR_NUMBER }}/comments" -f body="$BODY" - env: - GH_TOKEN: ${{ steps.bot-token.outputs.token }} + uses: ./.github/workflows/reusable-conflict-resolve.yml + with: + pr_number: ${{ matrix.pr }} + bot_username: james-in-a-box + # action_ref: jwbron/egg/action@main # Cannot be passed dynamically; hardcoded in reusable workflow + timeout: "30" + secrets: + BOT_APP_ID: ${{ secrets.BOT_APP_ID }} + BOT_APP_PRIVATE_KEY: ${{ secrets.BOT_APP_PRIVATE_KEY }} + BOT_APP_INSTALLATION_ID: ${{ secrets.BOT_APP_INSTALLATION_ID }} + ANTHROPIC_OAUTH_TOKEN: ${{ secrets.ANTHROPIC_OAUTH_TOKEN }} # Manual dispatch job: resolve conflicts for a specific PR resolve-manual: name: Resolve Conflicts (Manual) - runs-on: ubuntu-latest if: github.event_name == 'workflow_dispatch' - permissions: contents: write pull-requests: write actions: read - - concurrency: - group: egg-conflict-${{ github.event.inputs.pr_number }} - cancel-in-progress: false - - env: - PR_NUMBER: ${{ github.event.inputs.pr_number }} - - steps: - - name: Generate bot token - id: bot-token - uses: actions/create-github-app-token@v1 - with: - app-id: ${{ secrets.BOT_APP_ID }} - private-key: ${{ secrets.BOT_APP_PRIVATE_KEY }} - - - name: Fetch PR metadata - id: pr-meta - run: | - set -euo pipefail - pr_json=$(gh api repos/${{ github.repository }}/pulls/${{ env.PR_NUMBER }}) - { - echo "head-ref=$(echo "$pr_json" | jq -r '.head.ref')" - echo "head-repo=$(echo "$pr_json" | jq -r '.head.repo.full_name')" - echo "title=$(echo "$pr_json" | jq -r '.title')" - echo "base-ref=$(echo "$pr_json" | jq -r '.base.ref')" - } >> "$GITHUB_OUTPUT" - env: - GH_TOKEN: ${{ steps.bot-token.outputs.token }} - - # Skip fork PRs — bot can't push to forks - - name: Check for fork PR - id: fork-check - run: | - set -euo pipefail - if [[ "${{ steps.pr-meta.outputs.head-repo }}" != "${{ github.repository }}" ]]; then - echo "skip=true" >> "$GITHUB_OUTPUT" - echo "PR is from a fork (${{ steps.pr-meta.outputs.head-repo }}), skipping — bot cannot push to forks" - else - echo "skip=false" >> "$GITHUB_OUTPUT" - fi - - - name: Early exit if fork - if: steps.fork-check.outputs.skip == 'true' - run: echo "Skipping conflict resolution for fork PR" - - # Check for skip marker - - name: Check for skip marker - if: steps.fork-check.outputs.skip != 'true' - id: skip-check - env: - PR_TITLE: ${{ steps.pr-meta.outputs.title }} - run: | - set -euo pipefail - if [[ "$PR_TITLE" == *"[skip-conflict-fix]"* ]]; then - echo "skip=true" >> "$GITHUB_OUTPUT" - echo "PR has [skip-conflict-fix] marker, skipping" - else - echo "skip=false" >> "$GITHUB_OUTPUT" - fi - - - name: Early exit if skipped - if: steps.fork-check.outputs.skip != 'true' && steps.skip-check.outputs.skip == 'true' - run: echo "Skipping conflict resolution due to [skip-conflict-fix] marker" - - # Minimize previous conflict resolution comments - - name: Minimize previous conflict comments - if: steps.fork-check.outputs.skip != 'true' && steps.skip-check.outputs.skip != 'true' - run: | - set -euo pipefail - gh api "repos/${{ github.repository }}/issues/${{ env.PR_NUMBER }}/comments" \ - --jq '.[] | select(.user.login == "james-in-a-box" or .user.login == "james-in-a-box[bot]") | select(.body | test("egg is resolving|egg conflict resolution")) | .node_id' \ - | while read -r node_id; do - # shellcheck disable=SC2016 # $id is a GraphQL variable, not bash - gh api graphql -f query=' - mutation($id: ID!) { - minimizeComment(input: {subjectId: $id, classifier: OUTDATED}) { - minimizedComment { isMinimized } - } - } - ' -f id="$node_id" || echo "Failed to minimize comment $node_id" - done - env: - GH_TOKEN: ${{ steps.bot-token.outputs.token }} - - - name: Post starting comment - if: steps.fork-check.outputs.skip != 'true' && steps.skip-check.outputs.skip != 'true' - run: | - set -euo pipefail - BODY="egg is resolving merge conflicts for this PR..." - gh api "repos/${{ github.repository }}/issues/${{ env.PR_NUMBER }}/comments" -f body="$BODY" - env: - GH_TOKEN: ${{ steps.bot-token.outputs.token }} - - # SECURITY: Build the conflict prompt from a trusted checkout (main). - - name: Checkout main (trusted) - if: steps.fork-check.outputs.skip != 'true' && steps.skip-check.outputs.skip != 'true' - uses: actions/checkout@v4 - with: - ref: main - persist-credentials: false - - - name: Build conflict prompt - if: steps.fork-check.outputs.skip != 'true' && steps.skip-check.outputs.skip != 'true' - id: prompt - run: bash action/build-conflict-prompt.sh - env: - GH_TOKEN: ${{ steps.bot-token.outputs.token }} - PR_NUMBER: ${{ env.PR_NUMBER }} - BASE_REF: ${{ steps.pr-meta.outputs.base-ref }} - - - name: Checkout PR branch - if: steps.fork-check.outputs.skip != 'true' && steps.skip-check.outputs.skip != 'true' - uses: actions/checkout@v4 - with: - repository: ${{ steps.pr-meta.outputs.head-repo }} - ref: ${{ steps.pr-meta.outputs.head-ref }} - persist-credentials: false - fetch-depth: 0 - - - name: Run egg conflict resolution - if: steps.fork-check.outputs.skip != 'true' && steps.skip-check.outputs.skip != 'true' - id: egg - uses: jwbron/egg/action@main - with: - prompt-file: ${{ steps.prompt.outputs.prompt-file }} - model: ${{ steps.prompt.outputs.model }} - anthropic-oauth-token: ${{ secrets.ANTHROPIC_OAUTH_TOKEN }} - bot-app-id: ${{ secrets.BOT_APP_ID }} - bot-app-private-key: ${{ secrets.BOT_APP_PRIVATE_KEY }} - bot-app-installation-id: ${{ secrets.BOT_APP_INSTALLATION_ID }} - bot-username: james-in-a-box - timeout: "30" - - - name: Post result comment - if: always() && !cancelled() && steps.fork-check.outputs.skip != 'true' && steps.skip-check.outputs.skip != 'true' - run: | - set -euo pipefail - RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" - - if [[ "${{ steps.egg.outcome }}" == "success" ]]; then - BODY="egg conflict resolution completed. [View run logs](${RUN_URL})" - else - BODY="egg conflict resolution encountered an issue. [View run logs](${RUN_URL})" - fi - - gh api "repos/${{ github.repository }}/issues/${{ env.PR_NUMBER }}/comments" -f body="$BODY" - env: - GH_TOKEN: ${{ steps.bot-token.outputs.token }} + uses: ./.github/workflows/reusable-conflict-resolve.yml + with: + pr_number: ${{ fromJson(github.event.inputs.pr_number) }} + bot_username: james-in-a-box + # action_ref: jwbron/egg/action@main # Cannot be passed dynamically; hardcoded in reusable workflow + timeout: "30" + secrets: + BOT_APP_ID: ${{ secrets.BOT_APP_ID }} + BOT_APP_PRIVATE_KEY: ${{ secrets.BOT_APP_PRIVATE_KEY }} + BOT_APP_INSTALLATION_ID: ${{ secrets.BOT_APP_INSTALLATION_ID }} + ANTHROPIC_OAUTH_TOKEN: ${{ secrets.ANTHROPIC_OAUTH_TOKEN }} diff --git a/.github/workflows/on-pull-request-agent-mode-design.yml b/.github/workflows/on-pull-request-agent-mode-design.yml index 72a89d1c50..901b01e2e7 100644 --- a/.github/workflows/on-pull-request-agent-mode-design.yml +++ b/.github/workflows/on-pull-request-agent-mode-design.yml @@ -28,6 +28,8 @@ jobs: with: pr_number: ${{ github.event.pull_request.number || github.event.inputs.pr_number }} bot_name: agent-mode-design + bot_username: james-in-a-box + # action_ref: jwbron/egg/action@main # Cannot be passed dynamically; hardcoded in reusable workflow prompt_script: action/build-agent-mode-design-review-prompt.sh timeout: "10" pr_title: ${{ github.event.pull_request.title || '' }} diff --git a/.github/workflows/on-pull-request-contract-verify.yml b/.github/workflows/on-pull-request-contract-verify.yml index 8eb9e0a346..d7a064e765 100644 --- a/.github/workflows/on-pull-request-contract-verify.yml +++ b/.github/workflows/on-pull-request-contract-verify.yml @@ -73,6 +73,8 @@ jobs: with: pr_number: ${{ github.event.pull_request.number || github.event.inputs.pr_number }} bot_name: contract-verification + bot_username: james-in-a-box + # action_ref: jwbron/egg/action@main # Cannot be passed dynamically; hardcoded in reusable workflow prompt_script: "action/build-contract-verification-prompt.sh" timeout: "15" pr_title: ${{ github.event.pull_request.title || '' }} diff --git a/.github/workflows/on-pull-request.yml b/.github/workflows/on-pull-request.yml index 3d4d30cc39..d552e90253 100644 --- a/.github/workflows/on-pull-request.yml +++ b/.github/workflows/on-pull-request.yml @@ -20,6 +20,8 @@ jobs: with: pr_number: ${{ github.event.pull_request.number || github.event.inputs.pr_number }} bot_name: review + bot_username: james-in-a-box + # action_ref: jwbron/egg/action@main # Cannot be passed dynamically; hardcoded in reusable workflow timeout: "10" pr_title: ${{ github.event.pull_request.title || '' }} event_name: ${{ github.event_name }} diff --git a/.github/workflows/on-push-doc-updater.yml b/.github/workflows/on-push-doc-updater.yml index 007830ca34..9797623298 100644 --- a/.github/workflows/on-push-doc-updater.yml +++ b/.github/workflows/on-push-doc-updater.yml @@ -28,6 +28,37 @@ on: required: false default: false type: boolean + workflow_call: + inputs: + commit_sha: + description: "Analyze changes from this commit (defaults to HEAD~1)" + required: false + type: string + default: "" + dry_run: + description: "Dry run mode (analyze only, don't create PR)" + required: false + type: boolean + default: false + action_ref: + description: 'Reference to egg action (owner/repo/path@ref format). NOTE: GitHub Actions uses: field cannot be dynamic - consuming repos must hardcode their action reference in the uses step below.' + required: false + type: string + default: "jwbron/egg/action@main" + prompt_script: + description: 'Path to prompt builder script (relative to repo root)' + required: false + type: string + default: "action/build-doc-updater-prompt.sh" + secrets: + BOT_APP_ID: + required: true + BOT_APP_PRIVATE_KEY: + required: true + BOT_APP_INSTALLATION_ID: + required: true + ANTHROPIC_OAUTH_TOKEN: + required: true jobs: doc-update-check: @@ -37,8 +68,8 @@ jobs: contents: write pull-requests: write - # Avoid running on merge commits from doc-updater PRs - if: "!contains(github.event.head_commit.message, '[doc-updater]')" + # Avoid running on merge commits from doc-updater PRs (event-triggered only) + if: github.event_name == 'workflow_call' || !contains(github.event.head_commit.message, '[doc-updater]') concurrency: group: egg-doc-updater-${{ github.sha }} @@ -61,10 +92,10 @@ jobs: - name: Build task prompt id: prompt env: - COMMIT_SHA: ${{ github.event.inputs.commit_sha || '' }} - DRY_RUN: ${{ github.event.inputs.dry_run || 'false' }} + COMMIT_SHA: ${{ inputs.commit_sha || github.event.inputs.commit_sha || '' }} + DRY_RUN: ${{ inputs.dry_run || github.event.inputs.dry_run || 'false' }} GH_TOKEN: ${{ steps.bot-token.outputs.token }} - run: bash action/build-doc-updater-prompt.sh + run: bash ${{ inputs.prompt_script || 'action/build-doc-updater-prompt.sh' }} - name: Run egg id: egg diff --git a/.github/workflows/on-review-feedback.yml b/.github/workflows/on-review-feedback.yml index 67751cbd0b..58151c645a 100644 --- a/.github/workflows/on-review-feedback.yml +++ b/.github/workflows/on-review-feedback.yml @@ -18,33 +18,138 @@ on: description: 'PR number to address feedback for' required: true type: number + workflow_call: + inputs: + pr_number: + description: 'PR number to address feedback for' + required: true + type: number + bot_username: + description: 'GitHub username of the bot (for comment filtering and authorship checks)' + required: false + type: string + default: "james-in-a-box" + action_ref: + description: 'Reference to egg action (owner/repo/path@ref format). NOTE: GitHub Actions uses: field cannot be dynamic - consuming repos must hardcode their action reference in the uses step below.' + required: false + type: string + default: "jwbron/egg/action@main" + authorized_users: + description: 'Comma-separated list of authorized GitHub usernames (for bot-authored PR check bypass)' + required: false + type: string + default: "jwbron" + max_feedback_rounds: + description: 'Maximum number of feedback-addressing rounds before requiring human intervention' + required: false + type: number + default: 3 + prompt_script: + description: 'Path to prompt builder script (relative to repo root)' + required: false + type: string + default: "action/build-feedback-prompt.sh" + timeout: + description: 'Timeout in minutes' + required: false + type: string + default: "20" + secrets: + BOT_APP_ID: + required: true + BOT_APP_PRIVATE_KEY: + required: true + BOT_APP_INSTALLATION_ID: + required: true + ANTHROPIC_OAUTH_TOKEN: + required: true jobs: + # Workaround: GitHub Actions doesn't allow accessing inputs in job-level 'if' conditions. + # This job passes inputs as outputs so subsequent jobs can use them in conditions. + resolve-inputs: + name: Resolve Inputs + runs-on: ubuntu-latest + outputs: + bot_username: ${{ steps.resolve.outputs.bot_username }} + max_feedback_rounds: ${{ steps.resolve.outputs.max_feedback_rounds }} + pr_number: ${{ steps.resolve.outputs.pr_number }} + should_check_trigger: ${{ steps.resolve.outputs.should_check_trigger }} + steps: + - name: Resolve inputs with defaults + id: resolve + run: | + # Use inputs if called as reusable workflow, otherwise use defaults + echo "bot_username=${{ inputs.bot_username || 'james-in-a-box' }}" >> "$GITHUB_OUTPUT" + echo "max_feedback_rounds=${{ inputs.max_feedback_rounds || '3' }}" >> "$GITHUB_OUTPUT" + + # Determine PR number based on event type + if [[ "${{ github.event_name }}" == "workflow_call" ]]; then + echo "pr_number=${{ inputs.pr_number }}" >> "$GITHUB_OUTPUT" + echo "should_check_trigger=false" >> "$GITHUB_OUTPUT" + elif [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then + echo "pr_number=${{ github.event.inputs.pr_number }}" >> "$GITHUB_OUTPUT" + echo "should_check_trigger=false" >> "$GITHUB_OUTPUT" + else + echo "pr_number=${{ github.event.pull_request.number || github.event.issue.number }}" >> "$GITHUB_OUTPUT" + echo "should_check_trigger=true" >> "$GITHUB_OUTPUT" + fi + + # For event-triggered runs, check if the trigger matches our bot + check-trigger: + name: Check Trigger + needs: resolve-inputs + if: needs.resolve-inputs.outputs.should_check_trigger == 'true' + runs-on: ubuntu-latest + outputs: + should_run: ${{ steps.check.outputs.should_run }} + steps: + - name: Check if triggered by our bot + id: check + env: + BOT_USERNAME: ${{ needs.resolve-inputs.outputs.bot_username }} + EVENT_NAME: ${{ github.event_name }} + REVIEW_USER: ${{ github.event.review.user.login }} + REVIEW_BODY: ${{ github.event.review.body }} + REVIEW_STATE: ${{ github.event.review.state }} + COMMENT_USER: ${{ github.event.comment.user.login }} + COMMENT_BODY: ${{ github.event.comment.body }} + IS_PR: ${{ github.event.issue.pull_request }} + run: | + # For pull_request_review events + if [[ "$EVENT_NAME" == "pull_request_review" ]]; then + if [[ ("$REVIEW_USER" == "$BOT_USERNAME" || "$REVIEW_USER" == "${BOT_USERNAME}[bot]") && \ + "$REVIEW_BODY" == *"egg-automated-review"* && \ + "$REVIEW_STATE" != "approved" ]]; then + echo "should_run=true" >> "$GITHUB_OUTPUT" + else + echo "should_run=false" >> "$GITHUB_OUTPUT" + fi + # For issue_comment events (self-review as comment) + elif [[ "$EVENT_NAME" == "issue_comment" ]]; then + if [[ -n "$IS_PR" && \ + ("$COMMENT_USER" == "$BOT_USERNAME" || "$COMMENT_USER" == "${BOT_USERNAME}[bot]") && \ + "$COMMENT_BODY" == *"egg-automated-review"* && \ + "$COMMENT_BODY" != *"verdict=approve"* ]]; then + echo "should_run=true" >> "$GITHUB_OUTPUT" + else + echo "should_run=false" >> "$GITHUB_OUTPUT" + fi + else + echo "should_run=false" >> "$GITHUB_OUTPUT" + fi + address-feedback: name: Address Review Feedback + needs: [resolve-inputs, check-trigger] runs-on: ubuntu-latest - # Only run when: - # - workflow_dispatch: manual trigger (bypasses filters for debugging — see step "Check if feedback should be addressed") - # - pull_request_review: formal review from the bot with automated review marker - # - issue_comment: self-review posted as comment (GitHub blocks bots from reviewing their own PRs) - # - # NOTE: Bot username is hardcoded in the if condition because env vars are not - # available at job level. The BOT_USERNAME env var is used in shell steps. + # Run when: + # - workflow_call or workflow_dispatch (should_check_trigger=false means check-trigger was skipped) + # - Event-triggered and check-trigger passed if: >- - github.event_name == 'workflow_dispatch' || - ( - github.event_name == 'pull_request_review' && - (github.event.review.user.login == 'james-in-a-box' || github.event.review.user.login == 'james-in-a-box[bot]') && - contains(github.event.review.body, 'egg-automated-review') && - github.event.review.state != 'approved' - ) || - ( - github.event_name == 'issue_comment' && - github.event.issue.pull_request != null && - (github.event.comment.user.login == 'james-in-a-box' || github.event.comment.user.login == 'james-in-a-box[bot]') && - contains(github.event.comment.body, 'egg-automated-review') && - !contains(github.event.comment.body, 'verdict=approve') - ) + always() && + needs.resolve-inputs.result == 'success' && + (needs.check-trigger.result == 'skipped' || needs.check-trigger.outputs.should_run == 'true') permissions: contents: write @@ -52,13 +157,13 @@ jobs: actions: read concurrency: - group: egg-feedback-${{ github.event.pull_request.number || github.event.issue.number || github.event.inputs.pr_number }} + group: egg-feedback-${{ needs.resolve-inputs.outputs.pr_number }} cancel-in-progress: true env: - PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number || github.event.inputs.pr_number }} - BOT_USERNAME: james-in-a-box - MAX_FEEDBACK_ROUNDS: 3 + PR_NUMBER: ${{ needs.resolve-inputs.outputs.pr_number }} + BOT_USERNAME: ${{ needs.resolve-inputs.outputs.bot_username }} + MAX_FEEDBACK_ROUNDS: ${{ needs.resolve-inputs.outputs.max_feedback_rounds }} steps: - name: Generate bot token @@ -109,11 +214,11 @@ jobs: exit 0 fi - # For workflow_dispatch, skip remaining filters. + # For workflow_dispatch or workflow_call, skip remaining filters. # NOTE: This is intentional for debugging — allows manual trigger on any PR. # The bot-author check, skip-review check, and iteration limit are bypassed. - if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then - echo "Manual trigger via workflow_dispatch — bypassing remaining filters" + if [[ "$EVENT_NAME" == "workflow_dispatch" || "$EVENT_NAME" == "workflow_call" ]]; then + echo "Manual/reusable trigger — bypassing remaining filters" echo "run=true" >> "$GITHUB_OUTPUT" exit 0 fi @@ -237,7 +342,7 @@ jobs: run: | set -euo pipefail gh api "repos/${{ github.repository }}/issues/${{ env.PR_NUMBER }}/comments" \ - --jq '.[] | select(.user.login == "${{ env.BOT_USERNAME }}" or .user.login == "${{ env.BOT_USERNAME }}[bot]") | select(.body | test("egg is addressing|egg feedback")) | .node_id' \ + --jq ".[] | select(.user.login == \"$BOT_USERNAME\" or .user.login == \"${BOT_USERNAME}[bot]\") | select(.body | test(\"egg is addressing|egg feedback\")) | .node_id" \ | while read -r node_id; do # shellcheck disable=SC2016 # $id is a GraphQL variable, not bash if ! gh api graphql -f query=' @@ -276,7 +381,7 @@ jobs: - name: Build feedback prompt if: steps.should-run.outputs.run == 'true' && steps.wait-for-reviewers.outputs.proceed == 'true' id: prompt - run: bash action/build-feedback-prompt.sh + run: bash ${{ inputs.prompt_script || 'action/build-feedback-prompt.sh' }} env: PR_NUMBER: ${{ env.PR_NUMBER }} @@ -300,7 +405,7 @@ jobs: bot-app-private-key: ${{ secrets.BOT_APP_PRIVATE_KEY }} bot-app-installation-id: ${{ secrets.BOT_APP_INSTALLATION_ID }} bot-username: ${{ env.BOT_USERNAME }} - timeout: "20" + timeout: ${{ inputs.timeout || '20' }} env: EGG_BOT_NAME: feedback diff --git a/.github/workflows/reusable-autofix.yml b/.github/workflows/reusable-autofix.yml new file mode 100644 index 0000000000..b8fcf4867a --- /dev/null +++ b/.github/workflows/reusable-autofix.yml @@ -0,0 +1,200 @@ +name: Reusable Autofix Workflow + +on: + workflow_call: + inputs: + pr_number: + description: 'PR number to fix checks for' + required: true + type: number + failed_workflow: + description: 'Name of the failed workflow' + required: false + type: string + default: "manual" + failed_run_id: + description: 'Run ID of the failed workflow' + required: false + type: string + default: "" + bot_username: + description: 'GitHub username of the bot (for comment filtering)' + required: false + type: string + default: "james-in-a-box" + action_ref: + description: 'Reference to egg action (owner/repo/path@ref format). NOTE: GitHub Actions uses: field cannot be dynamic - consuming repos must hardcode their action reference in the uses step below.' + required: false + type: string + default: "jwbron/egg/action@main" + prompt_script: + description: 'Path to prompt builder script (relative to repo root)' + required: false + type: string + default: "action/build-autofixer-prompt.sh" + timeout: + description: 'Timeout in minutes' + required: false + type: string + default: "20" + secrets: + BOT_APP_ID: + required: true + BOT_APP_PRIVATE_KEY: + required: true + BOT_APP_INSTALLATION_ID: + required: true + ANTHROPIC_OAUTH_TOKEN: + required: true + +jobs: + autofix: + name: Autofix PR Checks + runs-on: ubuntu-latest + + permissions: + contents: write + pull-requests: write + actions: read + + concurrency: + group: egg-autofix-${{ inputs.pr_number }} + cancel-in-progress: true + + env: + PR_NUMBER: ${{ inputs.pr_number }} + FAILED_WORKFLOW: ${{ inputs.failed_workflow }} + FAILED_RUN_ID: ${{ inputs.failed_run_id }} + BOT_USERNAME: ${{ inputs.bot_username }} + + steps: + - name: Generate bot token + id: bot-token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.BOT_APP_ID }} + private-key: ${{ secrets.BOT_APP_PRIVATE_KEY }} + + # For workflow_dispatch, fetch PR metadata via API + - name: Fetch PR metadata + id: pr-meta + run: | + pr_json=$(gh api repos/${{ github.repository }}/pulls/${{ env.PR_NUMBER }}) + { + echo "head-ref=$(echo "$pr_json" | jq -r '.head.ref')" + echo "head-repo=$(echo "$pr_json" | jq -r '.head.repo.full_name')" + echo "title=$(echo "$pr_json" | jq -r '.title')" + } >> "$GITHUB_OUTPUT" + env: + GH_TOKEN: ${{ steps.bot-token.outputs.token }} + + # Skip if PR has [skip-autofix] in title + - name: Check for skip marker + id: skip-check + env: + PR_TITLE: ${{ steps.pr-meta.outputs.title }} + run: | + if [[ "$PR_TITLE" == *"[skip-autofix]"* ]]; then + echo "skip=true" >> "$GITHUB_OUTPUT" + echo "PR has [skip-autofix] marker, skipping" + else + echo "skip=false" >> "$GITHUB_OUTPUT" + fi + + # Skip the rest of the workflow if [skip-autofix] is present + - name: Early exit if skipped + if: steps.skip-check.outputs.skip == 'true' + run: echo "Skipping autofix due to [skip-autofix] marker" + + # Minimize previous "investigating" comments to avoid clutter + - name: Minimize previous autofix comments + if: steps.skip-check.outputs.skip != 'true' + run: | + # Find previous bot comments that are "investigating" status updates + gh api "repos/${{ github.repository }}/issues/${{ env.PR_NUMBER }}/comments" \ + --jq ".[] | select(.user.login == \"$BOT_USERNAME\" or .user.login == \"${BOT_USERNAME}[bot]\") | select(.body | test(\"egg is investigating|egg autofix\")) | .node_id" \ + | while read -r node_id; do + # Minimize the comment using GraphQL + # shellcheck disable=SC2016 # $id is a GraphQL variable, not bash + gh api graphql -f query=' + mutation($id: ID!) { + minimizeComment(input: {subjectId: $id, classifier: OUTDATED}) { + minimizedComment { isMinimized } + } + } + ' -f id="$node_id" || true + done + env: + GH_TOKEN: ${{ steps.bot-token.outputs.token }} + + # Post acknowledgment comment + - name: Post starting comment + if: steps.skip-check.outputs.skip != 'true' + run: | + if [[ -n "${{ env.FAILED_RUN_ID }}" ]]; then + RUN_LINK="[${FAILED_WORKFLOW}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ env.FAILED_RUN_ID }})" + BODY="egg is investigating the ${RUN_LINK} check failure..." + else + BODY="egg is investigating check failures for this PR..." + fi + gh api "repos/${{ github.repository }}/issues/${{ env.PR_NUMBER }}/comments" -f body="$BODY" + env: + GH_TOKEN: ${{ steps.bot-token.outputs.token }} + + # SECURITY: Build the autofixer prompt from a trusted checkout (main). + # A malicious PR could replace build-autofixer-prompt.sh to exfiltrate + # the bot token. By checking out main for prompt building, we ensure + # untrusted code never runs with secrets. + - name: Checkout main (trusted) + if: steps.skip-check.outputs.skip != 'true' + uses: actions/checkout@v4 + with: + ref: main + persist-credentials: false + + - name: Build autofixer prompt + if: steps.skip-check.outputs.skip != 'true' + id: prompt + run: bash ${{ inputs.prompt_script }} + env: + GH_TOKEN: ${{ steps.bot-token.outputs.token }} + PR_NUMBER: ${{ env.PR_NUMBER }} + FAILED_WORKFLOW: ${{ env.FAILED_WORKFLOW }} + FAILED_RUN_ID: ${{ env.FAILED_RUN_ID }} + + - name: Checkout PR branch + if: steps.skip-check.outputs.skip != 'true' + uses: actions/checkout@v4 + with: + repository: ${{ steps.pr-meta.outputs.head-repo }} + ref: ${{ steps.pr-meta.outputs.head-ref }} + persist-credentials: false + + - name: Run egg autofix + if: steps.skip-check.outputs.skip != 'true' + id: egg + uses: jwbron/egg/action@main + with: + prompt-file: ${{ steps.prompt.outputs.prompt-file }} + model: ${{ steps.prompt.outputs.model }} + anthropic-oauth-token: ${{ secrets.ANTHROPIC_OAUTH_TOKEN }} + bot-app-id: ${{ secrets.BOT_APP_ID }} + bot-app-private-key: ${{ secrets.BOT_APP_PRIVATE_KEY }} + bot-app-installation-id: ${{ secrets.BOT_APP_INSTALLATION_ID }} + bot-username: ${{ env.BOT_USERNAME }} + timeout: ${{ inputs.timeout }} + + - name: Post result comment + if: always() && !cancelled() && steps.skip-check.outputs.skip != 'true' + run: | + RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" + + if [[ "${{ steps.egg.outcome }}" == "success" ]]; then + BODY="egg autofix completed. [View run logs](${RUN_URL})" + else + BODY="egg autofix encountered an issue. [View run logs](${RUN_URL})" + fi + + gh api "repos/${{ github.repository }}/issues/${{ env.PR_NUMBER }}/comments" -f body="$BODY" + env: + GH_TOKEN: ${{ steps.bot-token.outputs.token }} diff --git a/.github/workflows/reusable-conflict-resolve.yml b/.github/workflows/reusable-conflict-resolve.yml new file mode 100644 index 0000000000..292524f776 --- /dev/null +++ b/.github/workflows/reusable-conflict-resolve.yml @@ -0,0 +1,203 @@ +name: Reusable Conflict Resolution Workflow + +on: + workflow_call: + inputs: + pr_number: + description: 'PR number to resolve conflicts for' + required: true + type: number + base_ref: + description: 'Base branch to merge from (e.g., main)' + required: false + type: string + default: "" + bot_username: + description: 'GitHub username of the bot (for comment filtering)' + required: false + type: string + default: "james-in-a-box" + action_ref: + description: 'Reference to egg action (owner/repo/path@ref format). NOTE: GitHub Actions uses: field cannot be dynamic - consuming repos must hardcode their action reference in the uses step below.' + required: false + type: string + default: "jwbron/egg/action@main" + prompt_script: + description: 'Path to prompt builder script (relative to repo root)' + required: false + type: string + default: "action/build-conflict-prompt.sh" + timeout: + description: 'Timeout in minutes' + required: false + type: string + default: "30" + secrets: + BOT_APP_ID: + required: true + BOT_APP_PRIVATE_KEY: + required: true + BOT_APP_INSTALLATION_ID: + required: true + ANTHROPIC_OAUTH_TOKEN: + required: true + +jobs: + resolve: + name: Resolve Conflicts + runs-on: ubuntu-latest + + permissions: + contents: write + pull-requests: write + actions: read + + concurrency: + group: egg-conflict-${{ inputs.pr_number }} + cancel-in-progress: false + + env: + PR_NUMBER: ${{ inputs.pr_number }} + BOT_USERNAME: ${{ inputs.bot_username }} + + steps: + - name: Generate bot token + id: bot-token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.BOT_APP_ID }} + private-key: ${{ secrets.BOT_APP_PRIVATE_KEY }} + + - name: Fetch PR metadata + id: pr-meta + run: | + set -euo pipefail + pr_json=$(gh api repos/${{ github.repository }}/pulls/${{ env.PR_NUMBER }}) + { + echo "head-ref=$(echo "$pr_json" | jq -r '.head.ref')" + echo "head-repo=$(echo "$pr_json" | jq -r '.head.repo.full_name')" + echo "title=$(echo "$pr_json" | jq -r '.title')" + echo "base-ref=$(echo "$pr_json" | jq -r '.base.ref')" + } >> "$GITHUB_OUTPUT" + env: + GH_TOKEN: ${{ steps.bot-token.outputs.token }} + + # Skip fork PRs — bot can't push to forks + - name: Check for fork PR + id: fork-check + run: | + set -euo pipefail + if [[ "${{ steps.pr-meta.outputs.head-repo }}" != "${{ github.repository }}" ]]; then + echo "skip=true" >> "$GITHUB_OUTPUT" + echo "PR is from a fork (${{ steps.pr-meta.outputs.head-repo }}), skipping — bot cannot push to forks" + else + echo "skip=false" >> "$GITHUB_OUTPUT" + fi + + - name: Early exit if fork + if: steps.fork-check.outputs.skip == 'true' + run: echo "Skipping conflict resolution for fork PR" + + # Check for skip marker + - name: Check for skip marker + if: steps.fork-check.outputs.skip != 'true' + id: skip-check + env: + PR_TITLE: ${{ steps.pr-meta.outputs.title }} + run: | + set -euo pipefail + if [[ "$PR_TITLE" == *"[skip-conflict-fix]"* ]]; then + echo "skip=true" >> "$GITHUB_OUTPUT" + echo "PR has [skip-conflict-fix] marker, skipping" + else + echo "skip=false" >> "$GITHUB_OUTPUT" + fi + + - name: Early exit if skipped + if: steps.fork-check.outputs.skip != 'true' && steps.skip-check.outputs.skip == 'true' + run: echo "Skipping conflict resolution due to [skip-conflict-fix] marker" + + # Minimize previous conflict resolution comments + - name: Minimize previous conflict comments + if: steps.fork-check.outputs.skip != 'true' && steps.skip-check.outputs.skip != 'true' + run: | + set -euo pipefail + gh api "repos/${{ github.repository }}/issues/${{ env.PR_NUMBER }}/comments" \ + --jq ".[] | select(.user.login == \"$BOT_USERNAME\" or .user.login == \"${BOT_USERNAME}[bot]\") | select(.body | test(\"egg is resolving|egg conflict resolution\")) | .node_id" \ + | while read -r node_id; do + # shellcheck disable=SC2016 # $id is a GraphQL variable, not bash + gh api graphql -f query=' + mutation($id: ID!) { + minimizeComment(input: {subjectId: $id, classifier: OUTDATED}) { + minimizedComment { isMinimized } + } + } + ' -f id="$node_id" || echo "Failed to minimize comment $node_id" + done + env: + GH_TOKEN: ${{ steps.bot-token.outputs.token }} + + - name: Post starting comment + if: steps.fork-check.outputs.skip != 'true' && steps.skip-check.outputs.skip != 'true' + run: | + set -euo pipefail + BODY="egg is resolving merge conflicts for this PR..." + gh api "repos/${{ github.repository }}/issues/${{ env.PR_NUMBER }}/comments" -f body="$BODY" + env: + GH_TOKEN: ${{ steps.bot-token.outputs.token }} + + # SECURITY: Build the conflict prompt from a trusted checkout (main). + - name: Checkout main (trusted) + if: steps.fork-check.outputs.skip != 'true' && steps.skip-check.outputs.skip != 'true' + uses: actions/checkout@v4 + with: + ref: main + persist-credentials: false + + - name: Build conflict prompt + if: steps.fork-check.outputs.skip != 'true' && steps.skip-check.outputs.skip != 'true' + id: prompt + run: bash ${{ inputs.prompt_script }} + env: + GH_TOKEN: ${{ steps.bot-token.outputs.token }} + PR_NUMBER: ${{ env.PR_NUMBER }} + BASE_REF: ${{ inputs.base_ref || steps.pr-meta.outputs.base-ref }} + + - name: Checkout PR branch + if: steps.fork-check.outputs.skip != 'true' && steps.skip-check.outputs.skip != 'true' + uses: actions/checkout@v4 + with: + repository: ${{ steps.pr-meta.outputs.head-repo }} + ref: ${{ steps.pr-meta.outputs.head-ref }} + persist-credentials: false + fetch-depth: 0 + + - name: Run egg conflict resolution + if: steps.fork-check.outputs.skip != 'true' && steps.skip-check.outputs.skip != 'true' + id: egg + uses: jwbron/egg/action@main + with: + prompt-file: ${{ steps.prompt.outputs.prompt-file }} + model: ${{ steps.prompt.outputs.model }} + anthropic-oauth-token: ${{ secrets.ANTHROPIC_OAUTH_TOKEN }} + bot-app-id: ${{ secrets.BOT_APP_ID }} + bot-app-private-key: ${{ secrets.BOT_APP_PRIVATE_KEY }} + bot-app-installation-id: ${{ secrets.BOT_APP_INSTALLATION_ID }} + bot-username: ${{ env.BOT_USERNAME }} + timeout: ${{ inputs.timeout }} + + - name: Post result comment + if: always() && !cancelled() && steps.fork-check.outputs.skip != 'true' && steps.skip-check.outputs.skip != 'true' + run: | + set -euo pipefail + RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" + + if [[ "${{ steps.egg.outcome }}" == "success" ]]; then + BODY="egg conflict resolution completed. [View run logs](${RUN_URL})" + else + BODY="egg conflict resolution encountered an issue. [View run logs](${RUN_URL})" + fi + + gh api "repos/${{ github.repository }}/issues/${{ env.PR_NUMBER }}/comments" -f body="$BODY" + env: + GH_TOKEN: ${{ steps.bot-token.outputs.token }} diff --git a/.github/workflows/reusable-review.yml b/.github/workflows/reusable-review.yml index 795131fb6a..d18014672c 100644 --- a/.github/workflows/reusable-review.yml +++ b/.github/workflows/reusable-review.yml @@ -11,6 +11,16 @@ on: description: 'Bot name for status messages and marker detection' required: true type: string + bot_username: + description: 'GitHub username of the bot (for review dismissal and comment filtering)' + required: false + type: string + default: "james-in-a-box" + action_ref: + description: 'Reference to egg action (owner/repo/path@ref format). NOTE: GitHub Actions uses: field cannot be dynamic - consuming repos must hardcode their action reference in the uses step below.' + required: false + type: string + default: "jwbron/egg/action@main" prompt_script: description: 'Path to prompt builder script (relative to repo root)' required: false @@ -191,7 +201,7 @@ jobs: env: PR_NUMBER: ${{ inputs.pr_number }} - BOT_USERNAME: james-in-a-box + BOT_USERNAME: ${{ inputs.bot_username }} steps: - name: Generate GitHub App token for bot diff --git a/.github/workflows/sdlc-hitl.yml b/.github/workflows/sdlc-hitl.yml index 18bee9d37f..ea74fb7518 100644 --- a/.github/workflows/sdlc-hitl.yml +++ b/.github/workflows/sdlc-hitl.yml @@ -10,6 +10,32 @@ name: "egg: SDLC HITL Decision Handler" on: issue_comment: types: [edited] + workflow_call: + inputs: + issue_number: + description: 'Issue number containing the HITL decision' + required: true + type: number + bot_username: + description: 'GitHub username of the bot (for git identity and comment filtering)' + required: false + type: string + default: "james-in-a-box" + authorized_users: + description: 'Comma-separated list of authorized GitHub usernames for HITL decisions' + required: false + type: string + default: "jwbron" + branch_prefix: + description: 'Prefix for issue branches (e.g., "egg" creates "egg/issue-N")' + required: false + type: string + default: "egg" + secrets: + BOT_APP_ID: + required: true + BOT_APP_PRIVATE_KEY: + required: true permissions: actions: write @@ -18,24 +44,108 @@ permissions: # Prevent concurrent HITL runs for the same issue concurrency: - group: sdlc-hitl-${{ github.event.issue.number }} + group: sdlc-hitl-${{ github.event.issue.number || inputs.issue_number }} cancel-in-progress: false jobs: + # Workaround: GitHub Actions doesn't allow accessing inputs in job-level 'if' conditions. + resolve-inputs: + name: Resolve inputs + runs-on: ubuntu-latest + outputs: + bot_username: ${{ steps.resolve.outputs.bot_username }} + authorized_users: ${{ steps.resolve.outputs.authorized_users }} + branch_prefix: ${{ steps.resolve.outputs.branch_prefix }} + issue_number: ${{ steps.resolve.outputs.issue_number }} + should_check_trigger: ${{ steps.resolve.outputs.should_check_trigger }} + steps: + - name: Resolve inputs with defaults + id: resolve + run: | + { + echo "bot_username=${{ inputs.bot_username || 'james-in-a-box' }}" + echo "authorized_users=${{ inputs.authorized_users || 'jwbron' }}" + echo "branch_prefix=${{ inputs.branch_prefix || 'egg' }}" + } >> "$GITHUB_OUTPUT" + + if [[ "${{ github.event_name }}" == "workflow_call" ]]; then + { + echo "issue_number=${{ inputs.issue_number }}" + echo "should_check_trigger=false" + } >> "$GITHUB_OUTPUT" + else + { + echo "issue_number=${{ github.event.issue.number }}" + echo "should_check_trigger=true" + } >> "$GITHUB_OUTPUT" + fi + + # For event-triggered runs, check authorization + check-trigger: + name: Check authorization + needs: resolve-inputs + if: needs.resolve-inputs.outputs.should_check_trigger == 'true' + runs-on: ubuntu-latest + outputs: + authorized: ${{ steps.check.outputs.authorized }} + steps: + - name: Check authorization + id: check + env: + BOT_USERNAME: ${{ needs.resolve-inputs.outputs.bot_username }} + AUTHORIZED_USERS: ${{ needs.resolve-inputs.outputs.authorized_users }} + SENDER_LOGIN: ${{ github.event.sender.login }} + COMMENT_BODY: ${{ github.event.comment.body }} + run: | + # Must contain HITL decision marker + if [[ "$COMMENT_BODY" != *"