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
24 changes: 24 additions & 0 deletions .github/workflows/smoke-codex.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 7 additions & 7 deletions actions/setup/js/campaign_discovery.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ describe("campaign_discovery", () => {
},
};

const result = await searchByLabel(octokit, "campaign:test", ["owner/repo"], [], 100, 10, null);
const result = await searchByLabel(octokit, "z_campaign_test", ["owner/repo"], [], 100, 10, null);

expect(result.items).toHaveLength(1);
expect(result.items[0].content_type).toBe("issue");
Expand All @@ -309,10 +309,10 @@ describe("campaign_discovery", () => {
},
};

await searchByLabel(octokit, "campaign:test", ["owner/repo1", "owner/repo2"], [], 100, 10, null);
await searchByLabel(octokit, "z_campaign_test", ["owner/repo1", "owner/repo2"], [], 100, 10, null);

const call = octokit.rest.search.issuesAndPullRequests.mock.calls[0][0];
expect(call.q).toContain('label:"campaign:test"');
expect(call.q).toContain('label:"z_campaign_test"');
expect(call.q).toContain("repo:owner/repo1");
expect(call.q).toContain("repo:owner/repo2");
expect(call.q).not.toContain("(");
Expand All @@ -330,10 +330,10 @@ describe("campaign_discovery", () => {
},
};

await searchByLabel(octokit, "campaign:test", [], ["myorg", "anotherorg"], 100, 10, null);
await searchByLabel(octokit, "z_campaign_test", [], ["myorg", "anotherorg"], 100, 10, null);

const call = octokit.rest.search.issuesAndPullRequests.mock.calls[0][0];
expect(call.q).toContain('label:"campaign:test"');
expect(call.q).toContain('label:"z_campaign_test"');
expect(call.q).toContain("org:myorg");
expect(call.q).toContain("org:anotherorg");
expect(call.q).not.toContain("(");
Expand All @@ -351,10 +351,10 @@ describe("campaign_discovery", () => {
},
};

await searchByLabel(octokit, "campaign:test", ["owner/repo1"], ["myorg"], 100, 10, null);
await searchByLabel(octokit, "z_campaign_test", ["owner/repo1"], ["myorg"], 100, 10, null);

const call = octokit.rest.search.issuesAndPullRequests.mock.calls[0][0];
expect(call.q).toContain('label:"campaign:test"');
expect(call.q).toContain('label:"z_campaign_test"');
expect(call.q).toContain("repo:owner/repo1");
expect(call.q).toContain("org:myorg");
expect(call.q).not.toContain("(");
Expand Down
6 changes: 2 additions & 4 deletions actions/setup/js/safe_output_handler_manager.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -32,20 +32,18 @@ function formatCampaignLabel(campaignId) {

/**
* Get campaign labels implied by environment variables.
* Returns the generic "agentic-campaign" label and the campaign-specific "z_campaign_<id>" label.
* @returns {{enabled: boolean, labels: string[]}}
*/
function getCampaignLabelsFromEnv() {
const campaignId = String(process.env.GH_AW_CAMPAIGN_ID || "").trim();
const trackerLabel = String(process.env.GH_AW_TRACKER_LABEL || "").trim();

if (!campaignId) {
return { enabled: false, labels: [] };
}

// Only use the new z_campaign_ format, no legacy support
const labels = [DEFAULT_AGENTIC_CAMPAIGN_LABEL, formatCampaignLabel(campaignId)];
if (trackerLabel) {
labels.push(trackerLabel);
}

return { enabled: true, labels };
}
Expand Down
2 changes: 0 additions & 2 deletions actions/setup/js/safe_output_handler_manager.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,6 @@ describe("Safe Output Handler Manager", () => {
describe("processMessages", () => {
it("should inject campaign labels into create_issue and create_pull_request messages", async () => {
process.env.GH_AW_CAMPAIGN_ID = "Security Alert Burndown";
process.env.GH_AW_TRACKER_LABEL = "campaign:security-alert-burndown";

const messages = [
{ type: "create_issue", title: "Issue", labels: ["Bug"] },
Expand All @@ -127,7 +126,6 @@ describe("Safe Output Handler Manager", () => {
expect(Array.isArray(message.labels)).toBe(true);
expect(message.labels).toContain("agentic-campaign");
expect(message.labels).toContain("z_campaign_security-alert-burndown");
expect(message.labels).toContain("campaign:security-alert-burndown");
return { success: true };
});

Expand Down
22 changes: 21 additions & 1 deletion actions/setup/js/update_project.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,26 @@
const { loadAgentOutput } = require("./load_agent_output.cjs");
const { getErrorMessage } = require("./error_helpers.cjs");

/**
* Campaign label prefix constant.
* Campaign-specific labels follow the format "z_campaign_<id>" where <id> is the campaign identifier.
* The "z_" prefix ensures these labels sort last in label lists.
*/
const CAMPAIGN_LABEL_PREFIX = "z_campaign_";

/**
* Format a campaign ID into a standardized campaign label.
* Mirrors the logic in pkg/stringutil/identifiers.go:FormatCampaignLabel and
* actions/setup/js/safe_output_handler_manager.cjs:formatCampaignLabel.
* @param {string} campaignId - Campaign ID to format
* @returns {string} Formatted campaign label (e.g., "z_campaign_security-q1-2025")
*/
function formatCampaignLabel(campaignId) {
return `${CAMPAIGN_LABEL_PREFIX}${String(campaignId)
.toLowerCase()
.replace(/[_\s]+/g, "-")}`;
}

/**
* Log detailed GraphQL error information
* @param {Error & { errors?: Array<{ type?: string, message: string, path?: unknown, locations?: unknown }>, request?: unknown, data?: unknown }} error - GraphQL error
Expand Down Expand Up @@ -792,7 +812,7 @@ async function updateProject(output) {
).addProjectV2ItemById.item.id;
if (campaignId) {
try {
await github.rest.issues.addLabels({ owner, repo, issue_number: contentNumber, labels: [`campaign:${campaignId}`] });
await github.rest.issues.addLabels({ owner, repo, issue_number: contentNumber, labels: [formatCampaignLabel(campaignId)] });
} catch (labelError) {
core.warning(`Failed to add campaign label: ${getErrorMessage(labelError)}`);
}
Expand Down
4 changes: 2 additions & 2 deletions actions/setup/js/update_project.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -397,7 +397,7 @@ describe("updateProject", () => {
await updateProject(output);

const labelCall = mockGithub.rest.issues.addLabels.mock.calls[0][0];
expect(labelCall.labels).toEqual(["campaign:custom-id-2025"]);
expect(labelCall.labels).toEqual(["z_campaign_custom-id-2025"]);
expect(getOutput("item-id")).toBe("item-custom");
});

Expand Down Expand Up @@ -430,7 +430,7 @@ describe("updateProject", () => {
issue_number: 42,
})
);
expect(labelCall.labels).toEqual(["campaign:my-campaign"]);
expect(labelCall.labels).toEqual(["z_campaign_my-campaign"]);
expect(getOutput("item-id")).toBe("item123");
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ name: Security Audit 2026
version: v1
state: planned
project-url: https://github.com/orgs/example/projects/42
tracker-label: campaign:security-audit-2026
tracker-label: z_campaign_security-audit-2026

# Worker workflows that will be discovered and dispatched
workflows:
Expand Down
6 changes: 3 additions & 3 deletions docs/src/content/docs/guides/campaigns/specs.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ name: "Framework Upgrade"
description: "Move services to Framework vNext"

project-url: "https://github.com/orgs/ORG/projects/1"
tracker-label: "campaign:framework-upgrade"
tracker-label: "z_campaign_framework-upgrade"

# Discovery: Where to find worker-created issues/PRs
discovery-repos:
Expand Down Expand Up @@ -103,7 +103,7 @@ At least one of `discovery-repos` or `discovery-orgs` is required when using wor
- `objective`: One sentence describing what success means for this campaign.
- `kpis`: List of 1-3 KPIs used to measure progress toward the objective.
- `workflows`: Workflow IDs the orchestrator can dispatch via `workflow_dispatch`.
- `tracker-label`: Label used to discover worker-created issues/PRs (commonly `campaign:<id>`).
- `tracker-label`: Label used to discover worker-created issues/PRs (format: `z_campaign_<id>`).
- `state`: Lifecycle state (`planned`, `active`, `paused`, `completed`, or `archived`).

### Optional
Expand Down Expand Up @@ -174,7 +174,7 @@ governance:
max-new-items-per-run: 10
max-discovery-items-per-run: 100
max-discovery-pages-per-run: 5
opt-out-labels: ["campaign:skip"]
opt-out-labels: ["z_campaign_skip"]
do-not-downgrade-done-items: true
max-project-updates-per-run: 50
max-comments-per-run: 10
Expand Down
4 changes: 2 additions & 2 deletions docs/src/content/docs/reference/safe-outputs.md
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,7 @@ safe-outputs:
layout: roadmap
```

Agent must provide full project URL (e.g., `https://github.com/orgs/myorg/projects/42`). Optional `campaign_id` applies `campaign:<id>` labels for [Campaign Workflows](/gh-aw/guides/campaigns/). Exposes outputs: `project-id`, `project-number`, `project-url`, `campaign-id`, `item-id`.
Agent must provide full project URL (e.g., `https://github.com/orgs/myorg/projects/42`). Optional `campaign_id` applies `z_campaign_<id>` labels for [Campaign Workflows](/gh-aw/guides/campaigns/). Exposes outputs: `project-id`, `project-number`, `project-url`, `campaign-id`, `item-id`.

#### Supported Field Types

Expand Down Expand Up @@ -972,7 +972,7 @@ Auto-enabled. Analyzes output for prompt injection, secret leaks, malicious patc

## Agentic Campaign Workflows

Combine `create-issue` + `update-project` for coordinated initiatives. Returns campaign ID, applies `campaign:<id>` labels, syncs boards. See [Campaign Workflows](/gh-aw/guides/campaigns/).
Combine `create-issue` + `update-project` for coordinated initiatives. Returns campaign ID, applies `z_campaign_<id>` labels, syncs boards. See [Campaign Workflows](/gh-aw/guides/campaigns/).

## Custom Messages (`messages:`)

Expand Down
2 changes: 1 addition & 1 deletion pkg/workflow/safe_output_validation_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ var ValidationConfig = map[string]TypeValidationConfig{
Fields: map[string]FieldValidation{
"project": {Required: true, Type: "string", Sanitize: true, MaxLength: 512, Pattern: "^https://github\\.com/(orgs|users)/[^/]+/projects/\\d+", PatternError: "must be a full GitHub project URL (e.g., https://github.com/orgs/myorg/projects/42)"},
// campaign_id is an optional field used by Campaign Workflows to tag project items.
// When provided, the update-project safe output applies a "campaign:<id>" label.
// When provided, the update-project safe output applies a "z_campaign_<id>" label.
// This is part of the campaign tracking convention but not required for general use.
"campaign_id": {Type: "string", Sanitize: true, MaxLength: 128},
"content_type": {Type: "string", Enum: []string{"issue", "pull_request"}},
Expand Down
Loading