Objective
Eliminate ~600-800 lines of duplicate job building code by creating a generic job builder framework for safe output operations.
Context
The analysis in #3525 identified 10+ nearly identical buildCreateOutput*Job functions with ~85% similar structure. Each safe output type reimplements the same job building pattern with minor variations.
Duplicate Functions (10+ implementations):
buildCreateOutputIssueJob (create_issue.go)
buildCreateOutputDiscussionJob (create_discussion.go)
buildCreateOutputPullRequestJob (create_pull_request.go)
buildCreateOutputAgentTaskJob (create_agent_task.go)
buildCreateOutputCodeScanningAlertJob (create_code_scanning_alert.go)
buildCreateOutputAddCommentJob (add_comment.go)
buildCreateOutputReviewCommentJob (create_pull_request_review_comment.go)
buildCreateOutputUpdateIssueJob (update_issue.go)
buildCreateOutputMissingToolJob (missing_tool.go)
buildCreateOutputUploadAssetJob (publish_assets.go)
Common Pattern (~85% identical):
func buildCreateOutput*Job(c *Compiler, config *Config) map[string]any {
// 1. Create job structure with name, runs-on
// 2. Add needs dependency
// 3. Add permissions
// 4. Add checkout step
// 5. Add processing steps (varies by type)
// 6. Add outputs
// 7. Return job
}
Approach
Step 1: Create Job Builder Package
Create pkg/workflow/jobs/ subpackage with:
builder.go - Generic job builder implementation
types.go - Job builder interfaces and types
safe_outputs.go - Safe output specific builders
builder_test.go - Comprehensive tests
Step 2: Define Builder Interface
Create interface for output-type-specific behavior:
// pkg/workflow/jobs/types.go
package jobs
type SafeOutputJobBuilder interface {
// GetJobName returns the job name (e.g., "create_issue")
GetJobName() string
// GetPermissions returns required permissions for this job
GetPermissions() map[string]string
// GetProcessingSteps returns output-specific processing steps
GetProcessingSteps(config map[string]any) []map[string]any
// GetOutputs returns job outputs
GetOutputs() map[string]string
// NeedsCheckout returns true if job needs repository checkout
NeedsCheckout() bool
}
Step 3: Implement Generic Builder
// pkg/workflow/jobs/builder.go
package jobs
func BuildSafeOutputJob(
builder SafeOutputJobBuilder,
config map[string]any,
runsOn any,
needsDependency string,
) map[string]any {
job := map[string]any{
"runs-on": runsOn,
"needs": needsDependency,
"permissions": builder.GetPermissions(),
"steps": []map[string]any{},
}
// Add checkout step if needed
if builder.NeedsCheckout() {
job["steps"] = append(job["steps"].([]map[string]any),
generateCheckoutStep())
}
// Add processing steps
processingSteps := builder.GetProcessingSteps(config)
job["steps"] = append(job["steps"].([]map[string]any),
processingSteps...)
// Add outputs if present
if outputs := builder.GetOutputs(); len(outputs) > 0 {
job["outputs"] = outputs
}
return job
}
Step 4: Create Output-Specific Builders
Implement SafeOutputJobBuilder for each output type:
// pkg/workflow/jobs/safe_outputs.go
package jobs
type IssueJobBuilder struct {
Config *IssueConfig
}
func (b *IssueJobBuilder) GetJobName() string {
return "create_issue"
}
func (b *IssueJobBuilder) GetPermissions() map[string]string {
return map[string]string{
"issues": "write",
"contents": "read",
}
}
func (b *IssueJobBuilder) GetProcessingSteps(config map[string]any) []map[string]any {
// Issue-specific steps
return []map[string]any{
generateProcessIssueStep(b.Config),
}
}
func (b *IssueJobBuilder) GetOutputs() map[string]string {
return map[string]string{
"issue_number": "${{ steps.process.outputs.issue_number }}",
"issue_url": "${{ steps.process.outputs.issue_url }}",
}
}
func (b *IssueJobBuilder) NeedsCheckout() bool {
return false
}
// Similar implementations for Discussion, PullRequest, AgentTask, etc.
Step 5: Refactor Existing Functions
Update each buildCreateOutput*Job function to use the generic builder:
// Before (in create_issue.go):
func (c *Compiler) buildCreateOutputIssueJob(config *IssueConfig) map[string]any {
// 60+ lines of job building logic
}
// After:
import "github.com/githubnext/gh-aw/pkg/workflow/jobs"
func (c *Compiler) buildCreateOutputIssueJob(config *IssueConfig) map[string]any {
builder := &jobs.IssueJobBuilder{Config: config}
return jobs.BuildSafeOutputJob(
builder,
c.convertConfigToMap(config),
c.runsOn,
"activation",
)
}
Step 6: Add Comprehensive Tests
Create tests for:
- Generic job builder with different output types
- Each SafeOutputJobBuilder implementation
- Edge cases (no checkout, no outputs, minimal permissions)
- Job structure validation
Files to Create
pkg/workflow/jobs/builder.go
pkg/workflow/jobs/types.go
pkg/workflow/jobs/safe_outputs.go
pkg/workflow/jobs/builder_test.go
Files to Modify
pkg/workflow/create_issue.go
pkg/workflow/create_discussion.go
pkg/workflow/create_pull_request.go
pkg/workflow/create_agent_task.go
pkg/workflow/create_code_scanning_alert.go
pkg/workflow/add_comment.go
pkg/workflow/create_pull_request_review_comment.go
pkg/workflow/update_issue.go
pkg/workflow/missing_tool.go
pkg/workflow/publish_assets.go
Acceptance Criteria
Benefits
- Single job building framework
- Consistent behavior across all output types
- Easy to add new output types (just implement interface)
- Reduced maintenance burden
Estimated Impact
AI generated by Plan Command for #3525
Objective
Eliminate ~600-800 lines of duplicate job building code by creating a generic job builder framework for safe output operations.
Context
The analysis in #3525 identified 10+ nearly identical
buildCreateOutput*Jobfunctions with ~85% similar structure. Each safe output type reimplements the same job building pattern with minor variations.Duplicate Functions (10+ implementations):
buildCreateOutputIssueJob(create_issue.go)buildCreateOutputDiscussionJob(create_discussion.go)buildCreateOutputPullRequestJob(create_pull_request.go)buildCreateOutputAgentTaskJob(create_agent_task.go)buildCreateOutputCodeScanningAlertJob(create_code_scanning_alert.go)buildCreateOutputAddCommentJob(add_comment.go)buildCreateOutputReviewCommentJob(create_pull_request_review_comment.go)buildCreateOutputUpdateIssueJob(update_issue.go)buildCreateOutputMissingToolJob(missing_tool.go)buildCreateOutputUploadAssetJob(publish_assets.go)Common Pattern (~85% identical):
Approach
Step 1: Create Job Builder Package
Create
pkg/workflow/jobs/subpackage with:builder.go- Generic job builder implementationtypes.go- Job builder interfaces and typessafe_outputs.go- Safe output specific buildersbuilder_test.go- Comprehensive testsStep 2: Define Builder Interface
Create interface for output-type-specific behavior:
Step 3: Implement Generic Builder
Step 4: Create Output-Specific Builders
Implement
SafeOutputJobBuilderfor each output type:Step 5: Refactor Existing Functions
Update each
buildCreateOutput*Jobfunction to use the generic builder:Step 6: Add Comprehensive Tests
Create tests for:
Files to Create
pkg/workflow/jobs/builder.gopkg/workflow/jobs/types.gopkg/workflow/jobs/safe_outputs.gopkg/workflow/jobs/builder_test.goFiles to Modify
pkg/workflow/create_issue.gopkg/workflow/create_discussion.gopkg/workflow/create_pull_request.gopkg/workflow/create_agent_task.gopkg/workflow/create_code_scanning_alert.gopkg/workflow/add_comment.gopkg/workflow/create_pull_request_review_comment.gopkg/workflow/update_issue.gopkg/workflow/missing_tool.gopkg/workflow/publish_assets.goAcceptance Criteria
pkg/workflow/jobs/package createdSafeOutputJobBuilderinterface definedBuildSafeOutputJobfunction implementedbuildCreateOutput*Jobfunctions refactored to use generic buildermake test-unitpassesmake lintpassesBenefits
Estimated Impact
Related to [refactor] 🔧 Semantic Function Clustering Analysis: Refactoring Opportunities #3525