Skip to content

Commit 5eb7c3b

Browse files
committed
ensure workflows are only created for specs with meaningful details
1 parent 1594438 commit 5eb7c3b

File tree

4 files changed

+71
-50
lines changed

4 files changed

+71
-50
lines changed

docs/src/content/docs/guides/campaigns.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -287,17 +287,18 @@ Campaign spec files participate in the normal `compile` workflow:
287287
- Validation checks both the spec itself (IDs, tracker labels, lifecycle state, etc.) and that all referenced `workflows` exist in `.github/workflows/`.
288288
- If any campaign spec has problems, `compile` fails with a `campaign validation failed` error until issues are fixed.
289289

290-
For experimentation, you can also enable an **orchestrator workflow** per campaign:
290+
**Orchestrator workflows** are automatically generated for each campaign spec:
291291

292292
```bash wrap
293-
GH_AW_EXPERIMENTAL_CAMPAIGN_ORCHESTRATOR=1 gh aw compile
293+
gh aw compile
294294
```
295295

296-
When this environment variable is set and specs are valid:
296+
When specs are valid:
297297

298298
- Each `<name>.campaign.md` generates an orchestrator markdown workflow named `<name>-campaign.md` next to the spec.
299299
- The orchestrator is compiled like any other workflow to `<name>-campaign.lock.yml`.
300300
- This makes campaigns first-class, compilable entry points while keeping specs declarative.
301+
- Orchestrators are only generated when the campaign spec has meaningful details (tracker labels, workflows, memory paths, or metrics glob).
301302

302303
### Interactive Campaign Designer
303304

docs/src/content/docs/setup/cli.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ gh aw compile --strict --zizmor # Strict mode with security scanning
155155
gh aw compile --validate --strict # Validate schema and enforce strict mode
156156
```
157157

158-
**Campaign specs and orchestrators:** In repositories that define campaign spec files under `.github/workflows/*.campaign.md`, `gh aw compile` first validates those specs (including referenced `workflows`) and fails the compilation if any problems are found. When the environment variable `GH_AW_EXPERIMENTAL_CAMPAIGN_ORCHESTRATOR=1` is set, `compile` also synthesizes an orchestrator workflow for each valid spec (for example, `security-compliance.campaign.md``security-compliance-campaign.md`) and compiles it to a corresponding `.lock.yml` file.
158+
**Campaign specs and orchestrators:** In repositories that define campaign spec files under `.github/workflows/*.campaign.md`, `gh aw compile` first validates those specs (including referenced `workflows`) and fails the compilation if any problems are found. By default, `compile` also synthesizes an orchestrator workflow for each valid spec that has meaningful details (e.g., `security-compliance.campaign.md``security-compliance-campaign.md`) and compiles it to a corresponding `.lock.yml` file. Orchestrators are only generated when the campaign spec includes tracker labels, workflows, memory paths, or a metrics glob.
159159

160160
See [Strict Mode reference](/gh-aw/reference/frontmatter/#strict-mode-strict) for frontmatter configuration and [Security Guide](/gh-aw/guides/security/#strict-mode-validation) for best practices.
161161

pkg/campaign/orchestrator.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,22 +34,36 @@ func BuildOrchestrator(spec *CampaignSpec, campaignFilePath string) (*workflow.W
3434
markdownBuilder := &strings.Builder{}
3535
markdownBuilder.WriteString("# Campaign Orchestrator\n\n")
3636
markdownBuilder.WriteString(fmt.Sprintf("This workflow orchestrates the '%s' campaign.\n\n", name))
37+
38+
// Track whether we have any meaningful campaign details
39+
hasDetails := false
40+
3741
if spec.TrackerLabel != "" {
3842
markdownBuilder.WriteString(fmt.Sprintf("- Tracker label: `%s`\n", spec.TrackerLabel))
43+
hasDetails = true
3944
}
4045
if len(spec.Workflows) > 0 {
4146
markdownBuilder.WriteString("- Associated workflows: ")
4247
markdownBuilder.WriteString(strings.Join(spec.Workflows, ", "))
4348
markdownBuilder.WriteString("\n")
49+
hasDetails = true
4450
}
4551
if len(spec.MemoryPaths) > 0 {
4652
markdownBuilder.WriteString("- Memory paths: ")
4753
markdownBuilder.WriteString(strings.Join(spec.MemoryPaths, ", "))
4854
markdownBuilder.WriteString("\n")
55+
hasDetails = true
4956
}
5057
if spec.MetricsGlob != "" {
5158
markdownBuilder.WriteString(fmt.Sprintf("- Metrics glob: `%s`\n", spec.MetricsGlob))
59+
hasDetails = true
5260
}
61+
62+
// Return nil if the campaign spec has no meaningful details for the prompt
63+
if !hasDetails {
64+
return nil, ""
65+
}
66+
5367
markdownBuilder.WriteString("\nUse these details to coordinate workers, update metrics, and track progress for this campaign.\n")
5468

5569
data := &workflow.WorkflowData{

pkg/cli/compile_command.go

Lines changed: 52 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -438,34 +438,37 @@ func CompileWorkflows(config CompileConfig) ([]*workflow.WorkflowData, error) {
438438
}
439439

440440
// Campaign spec is valid and referenced workflows exist.
441-
// Optionally synthesize an orchestrator workflow for this campaign
442-
// and compile it via the standard workflow pipeline. This is gated
443-
// behind an environment variable so campaign specs can be validated
444-
// in CI without requiring experimental orchestrator support.
445-
if !noEmit && os.Getenv("GH_AW_EXPERIMENTAL_CAMPAIGN_ORCHESTRATOR") == "1" {
441+
// Synthesize an orchestrator workflow for this campaign
442+
// and compile it via the standard workflow pipeline.
443+
if !noEmit {
446444
orchestratorData, orchestratorPath := campaign.BuildOrchestrator(spec, resolvedFile)
447-
lockFile := strings.TrimSuffix(orchestratorPath, ".md") + ".lock.yml"
448-
result.CompiledFile = lockFile
449-
450-
if err := CompileWorkflowDataWithValidation(compiler, orchestratorData, orchestratorPath, verbose && !jsonOutput, zizmor && !noEmit, poutine && !noEmit, actionlint && !noEmit, strict, validate && !noEmit); err != nil {
451-
if !jsonOutput {
452-
fmt.Fprintln(os.Stderr, err.Error())
445+
446+
// Only compile orchestrator if BuildOrchestrator returned valid data
447+
// (it returns nil if the campaign has no meaningful prompt content)
448+
if orchestratorData != nil {
449+
lockFile := strings.TrimSuffix(orchestratorPath, ".md") + ".lock.yml"
450+
result.CompiledFile = lockFile
451+
452+
if err := CompileWorkflowDataWithValidation(compiler, orchestratorData, orchestratorPath, verbose && !jsonOutput, zizmor && !noEmit, poutine && !noEmit, actionlint && !noEmit, strict, validate && !noEmit); err != nil {
453+
if !jsonOutput {
454+
fmt.Fprintln(os.Stderr, err.Error())
455+
}
456+
errorMessages = append(errorMessages, err.Error())
457+
errorCount++
458+
stats.Errors++
459+
stats.FailedWorkflows = append(stats.FailedWorkflows, filepath.Base(orchestratorPath))
460+
461+
result.Valid = false
462+
result.Errors = append(result.Errors, ValidationError{
463+
Type: "compilation_error",
464+
Message: err.Error(),
465+
})
466+
validationResults = append(validationResults, result)
467+
continue
453468
}
454-
errorMessages = append(errorMessages, err.Error())
455-
errorCount++
456-
stats.Errors++
457-
stats.FailedWorkflows = append(stats.FailedWorkflows, filepath.Base(orchestratorPath))
458469

459-
result.Valid = false
460-
result.Errors = append(result.Errors, ValidationError{
461-
Type: "compilation_error",
462-
Message: err.Error(),
463-
})
464-
validationResults = append(validationResults, result)
465-
continue
470+
compiledCount++
466471
}
467-
468-
compiledCount++
469472
}
470473

471474
if verbose && !jsonOutput {
@@ -747,33 +750,36 @@ func CompileWorkflows(config CompileConfig) ([]*workflow.WorkflowData, error) {
747750
}
748751

749752
// Campaign spec is valid and referenced workflows exist.
750-
// Optionally synthesize an orchestrator workflow for this campaign
751-
// and compile it via the standard workflow pipeline. This is gated
752-
// behind an environment variable so campaign specs can be validated
753-
// in CI without requiring experimental orchestrator support.
754-
if !noEmit && os.Getenv("GH_AW_EXPERIMENTAL_CAMPAIGN_ORCHESTRATOR") == "1" {
753+
// Synthesize an orchestrator workflow for this campaign
754+
// and compile it via the standard workflow pipeline.
755+
if !noEmit {
755756
orchestratorData, orchestratorPath := campaign.BuildOrchestrator(spec, file)
756-
lockFile := strings.TrimSuffix(orchestratorPath, ".md") + ".lock.yml"
757-
result.CompiledFile = lockFile
757+
758+
// Only compile orchestrator if BuildOrchestrator returned valid data
759+
// (it returns nil if the campaign has no meaningful prompt content)
760+
if orchestratorData != nil {
761+
lockFile := strings.TrimSuffix(orchestratorPath, ".md") + ".lock.yml"
762+
result.CompiledFile = lockFile
758763

759-
if err := CompileWorkflowDataWithValidation(compiler, orchestratorData, orchestratorPath, verbose && !jsonOutput, zizmor && !noEmit, poutine && !noEmit, actionlint && !noEmit, strict, validate && !noEmit); err != nil {
760-
if !jsonOutput {
761-
fmt.Fprintln(os.Stderr, err.Error())
764+
if err := CompileWorkflowDataWithValidation(compiler, orchestratorData, orchestratorPath, verbose && !jsonOutput, zizmor && !noEmit, poutine && !noEmit, actionlint && !noEmit, strict, validate && !noEmit); err != nil {
765+
if !jsonOutput {
766+
fmt.Fprintln(os.Stderr, err.Error())
767+
}
768+
errorCount++
769+
stats.Errors++
770+
stats.FailedWorkflows = append(stats.FailedWorkflows, filepath.Base(orchestratorPath))
771+
772+
result.Valid = false
773+
result.Errors = append(result.Errors, ValidationError{
774+
Type: "compilation_error",
775+
Message: err.Error(),
776+
})
777+
validationResults = append(validationResults, result)
778+
continue
762779
}
763-
errorCount++
764-
stats.Errors++
765-
stats.FailedWorkflows = append(stats.FailedWorkflows, filepath.Base(orchestratorPath))
766780

767-
result.Valid = false
768-
result.Errors = append(result.Errors, ValidationError{
769-
Type: "compilation_error",
770-
Message: err.Error(),
771-
})
772-
validationResults = append(validationResults, result)
773-
continue
781+
successCount++
774782
}
775-
776-
successCount++
777783
}
778784

779785
if verbose && !jsonOutput {

0 commit comments

Comments
 (0)