Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/src/content/docs/guides/campaigns.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@ Use a campaign when you need to manage an initiative—scope, progress, and outc

A campaign gives you a dashboard (GitHub Project), a coordinating orchestrator workflow that keeps it in sync, and a spec file that captures the objective, KPIs, governance, and wiring. In the repo, the spec lives at `.github/workflows/<id>.campaign.md` and is the source of truth.

When the spec includes orchestration, the tooling generates an orchestrator workflow and compiles it into a locked `.campaign.lock.yml` workflow. The spec defines what success means (objective), how progress is measured (KPIs, with exactly one marked primary), where progress is shown (GitHub Project URL), what participates (workflows), and what is tracked (the label applied to issues and pull requests, commonly `campaign:<id>`).
When the spec includes orchestration, the tooling generates an orchestrator workflow and compiles it into a locked `.campaign.lock.yml` workflow. The spec defines what success means (objective), how progress is measured (KPIs, with exactly one marked primary), where progress is shown (GitHub Project URL), and what participates (workflows). Optionally, you can add a tracker label (commonly `campaign:<id>`) to help discover issues and PRs, but the project board remains the canonical source of campaign membership.

**Note:** During compilation, a `.campaign.g.md` file is generated locally as a debug artifact to help developers understand the orchestrator structure, but this file is not committed to git—only the source `.campaign.md` and compiled `.campaign.lock.yml` are tracked.

## How it works

Most campaigns follow the same shape. The GitHub Project is the human-facing status view. The orchestrator workflow discovers tracked items from the workers and updates the Project. Worker workflows do the real work, such as opening pull requests or applying fixes but they stay campaign-agnostic. If you want cross-run discovery of worker-created assets, workers can include a `tracker-id` marker which the orchestrator can search for.
Most campaigns follow the same shape. The GitHub Project is the human-facing status view and the canonical source of campaign membership. The orchestrator workflow discovers tracked items from the workers and updates the Project. Worker workflows do the real work, such as opening pull requests or applying fixes but they stay campaign-agnostic. If you want cross-run discovery of worker-created assets, workers can include a `tracker-id` marker which the orchestrator can search for. Optionally, you can configure a tracker label (e.g., `campaign:<id>`) as an ingestion hint to help discover issues and PRs created by workers.

## Memory

Expand Down
14 changes: 7 additions & 7 deletions docs/src/content/docs/guides/campaigns/specs.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ Campaigns are defined as Markdown files under `.github/workflows/` with a `.camp

## What a campaign is (in gh-aw)

In GitHub Agentic Workflows, a campaign is not “a special kind of workflow.” The `.campaign.md` file is a specification: a reviewable contract that wires together agentic workflows around a shared initiative (a tracker label, a GitHub Project dashboard, and optional durable state).
In GitHub Agentic Workflows, a campaign is not “a special kind of workflow.” The `.campaign.md` file is a specification: a reviewable contract that wires together agentic workflows around a shared initiative (a GitHub Project dashboard as the canonical source of membership, optional tracker label for ingestion, and optional durable state).

In a typical setup:

- Worker workflows do the work. They run an agent and use safe-outputs (for example `create_pull_request`, `add_comment`, or `update_issues`) for write operations.
- A generated orchestrator workflow keeps the campaign coherent over time. It discovers items tagged with your tracker label, updates the Project board, and produces ongoing progress reporting.
- A generated orchestrator workflow keeps the campaign coherent over time. It discovers items from the project board (optionally using tracker labels), updates the Project board, and produces ongoing progress reporting.
- Repo-memory (optional) makes the campaign repeatable. It lets you store a cursor checkpoint and append-only metrics snapshots so each run can pick up where the last one left off.

### Mental model
Expand All @@ -23,7 +23,7 @@ flowchart TB
compile["fa:fa-cogs gh aw compile"]
debug["fa:fa-file .campaign.g.md<br/><small>debug artifact<br/>(not tracked)</small>"]
lock["fa:fa-lock .campaign.lock.yml<br/><small>compiled workflow<br/>(tracked in git)</small>"]
orchestrator["fa:fa-sitemap Orchestrator workflow<br/><small>discovers items via tracker-label<br/>updates Project dashboard<br/>reads/writes repo-memory</small>"]
orchestrator["fa:fa-sitemap Orchestrator workflow<br/><small>discovers items from project<br/>updates Project dashboard<br/>reads/writes repo-memory</small>"]
worker1["fa:fa-robot Worker workflow<br/><small>agent + safe-outputs</small>"]
worker2["fa:fa-robot Worker workflow<br/><small>agent + safe-outputs</small>"]
project["fa:fa-table GitHub Project board<br/><small>campaign dashboard</small>"]
Expand All @@ -35,8 +35,8 @@ flowchart TB
lock --> orchestrator
orchestrator -->|triggers/coordinates| worker1
orchestrator -->|triggers/coordinates| worker2
worker1 -->|creates/updates<br/>Issues/PRs with<br/>tracker-label| project
worker2 -->|creates/updates<br/>Issues/PRs with<br/>tracker-label| project
worker1 -->|creates/updates<br/>Issues/PRs<br/>(optional tracker-label)| project
worker2 -->|creates/updates<br/>Issues/PRs<br/>(optional tracker-label)| project
orchestrator -.->|reads/writes| memory
project -.->|dashboard view| orchestrator

Expand Down Expand Up @@ -91,8 +91,8 @@ owners:
## Core fields (what they do)

- `id`: stable identifier used for file naming, reporting, and (if used) repo-memory paths.
- `project-url`: the GitHub Project that acts as the campaign dashboard.
- `tracker-label`: the label applied to issues and pull requests that belong to the campaign (commonly `campaign:<id>`). This is the key that lets the orchestrator discover work across runs.
- `project-url`: the GitHub Project that acts as the campaign dashboard and canonical source of campaign membership.
- `tracker-label` (optional): an ingestion hint label that helps discover issues and pull requests created by workers (commonly `campaign:<id>`). When provided, the orchestrator can discover work across runs. The project board remains the canonical source of truth.
- `objective`: a single sentence describing what “done” means.
- `kpis`: the measures you use to report progress (exactly one should be marked `primary`).
- `workflows`: the participating workflow IDs. These refer to workflows in the repo (commonly `.github/workflows/<workflow-id>.md`), and they can be scheduled, event-driven, or long-running.
Expand Down
2 changes: 1 addition & 1 deletion pkg/campaign/schemas/campaign_spec_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@
},
"tracker-label": {
"type": "string",
"description": "Label used to associate issues/PRs with this campaign (e.g., campaign:incident-response)",
"description": "Optional label used as an ingestion hint to associate issues/PRs with this campaign (e.g., campaign:incident-response). The project board is the canonical source of campaign membership.",
"pattern": "^[^:]+:.+$",
"minLength": 1
},
Expand Down
6 changes: 4 additions & 2 deletions pkg/campaign/spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,10 @@ type CampaignSpec struct {
// RiskLevel is an optional free-form field (e.g. low/medium/high).
RiskLevel string `yaml:"risk-level,omitempty" json:"risk_level,omitempty" console:"header:Risk Level,omitempty"`

// TrackerLabel describes the label used to associate issues/PRs with
// this campaign (for example: campaign:incident-response).
// TrackerLabel is an optional label used as an ingestion hint to help
// discover and associate issues/PRs with this campaign (for example:
// campaign:incident-response). The GitHub Project board is the canonical
// source of campaign membership.
TrackerLabel string `yaml:"tracker-label,omitempty" json:"tracker_label,omitempty" console:"header:Tracker Label,omitempty"`

// State describes the lifecycle stage of the campaign definition.
Expand Down
6 changes: 3 additions & 3 deletions pkg/campaign/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,9 @@ func ValidateSpec(spec *CampaignSpec) []string {
}
}

if strings.TrimSpace(spec.TrackerLabel) == "" {
problems = append(problems, "tracker-label should be set to link issues and PRs to this campaign")
} else if !strings.Contains(spec.TrackerLabel, ":") {
// TrackerLabel is optional - only validate format when provided.
// The campaign project board is the canonical source of campaign membership.
if strings.TrimSpace(spec.TrackerLabel) != "" && !strings.Contains(spec.TrackerLabel, ":") {
problems = append(problems, "tracker-label should follow a namespaced pattern (for example: campaign:security-q1-2025)")
}

Expand Down
17 changes: 4 additions & 13 deletions pkg/campaign/validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ func TestValidateSpec_MissingWorkflows(t *testing.T) {
}

func TestValidateSpec_MissingTrackerLabel(t *testing.T) {
// tracker-label is now optional - spec should pass validation without it
spec := &CampaignSpec{
ID: "test-campaign",
Name: "Test Campaign",
Expand All @@ -132,19 +133,9 @@ func TestValidateSpec_MissingTrackerLabel(t *testing.T) {
}

problems := ValidateSpec(spec)
if len(problems) == 0 {
t.Fatal("Expected validation problems for missing tracker label")
}

found := false
for _, p := range problems {
if strings.Contains(p, "tracker-label should be set") {
found = true
break
}
}
if !found {
t.Errorf("Expected tracker label validation problem, got: %v", problems)
// Should have no problems since tracker-label is optional
if len(problems) != 0 {
t.Errorf("Expected no validation problems for missing tracker label (it's optional), got: %v", problems)
}
}

Expand Down
Loading