diff --git a/.github/workflows/smoke-codex.lock.yml b/.github/workflows/smoke-codex.lock.yml index fc223cbaa5..e6af4d2124 100644 --- a/.github/workflows/smoke-codex.lock.yml +++ b/.github/workflows/smoke-codex.lock.yml @@ -296,6 +296,30 @@ jobs: }, "name": "add_labels" }, + { + "description": "Remove labels from an existing GitHub issue or pull request. Silently skips labels that don't exist on the item. Use this to clean up labels or manage label lifecycles (e.g., removing 'needs-review' after review is complete). CONSTRAINTS: Only these labels can be removed: [smoke].", + "inputSchema": { + "additionalProperties": false, + "properties": { + "item_number": { + "description": "Issue or PR number to remove labels from. This is the numeric ID from the GitHub URL (e.g., 456 in github.com/owner/repo/issues/456). If omitted, removes labels from the item that triggered this workflow.", + "type": "number" + }, + "labels": { + "description": "Label names to remove (e.g., ['smoke', 'needs-triage']). Non-existent labels are silently skipped.", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "labels" + ], + "type": "object" + }, + "name": "remove_labels" + }, { "description": "Report that a tool or capability needed to complete the task is not available, or share any information you deem important about missing functionality or limitations. Use this when you cannot accomplish what was requested because the required functionality is missing or access is restricted.", "inputSchema": { diff --git a/actions/setup/js/campaign_discovery.test.cjs b/actions/setup/js/campaign_discovery.test.cjs index 46c7c42bb5..31f1962e47 100644 --- a/actions/setup/js/campaign_discovery.test.cjs +++ b/actions/setup/js/campaign_discovery.test.cjs @@ -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"); @@ -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("("); @@ -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("("); @@ -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("("); diff --git a/actions/setup/js/safe_output_handler_manager.cjs b/actions/setup/js/safe_output_handler_manager.cjs index 5838c4e241..118bfba5d7 100644 --- a/actions/setup/js/safe_output_handler_manager.cjs +++ b/actions/setup/js/safe_output_handler_manager.cjs @@ -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_" 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 }; } diff --git a/actions/setup/js/safe_output_handler_manager.test.cjs b/actions/setup/js/safe_output_handler_manager.test.cjs index 21cbd8bdef..c85a7ebf52 100644 --- a/actions/setup/js/safe_output_handler_manager.test.cjs +++ b/actions/setup/js/safe_output_handler_manager.test.cjs @@ -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"] }, @@ -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 }; }); diff --git a/actions/setup/js/update_project.cjs b/actions/setup/js/update_project.cjs index 41804e1d69..8dde070b0d 100644 --- a/actions/setup/js/update_project.cjs +++ b/actions/setup/js/update_project.cjs @@ -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_" where 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 @@ -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)}`); } diff --git a/actions/setup/js/update_project.test.cjs b/actions/setup/js/update_project.test.cjs index 193207580d..c7dfad127d 100644 --- a/actions/setup/js/update_project.test.cjs +++ b/actions/setup/js/update_project.test.cjs @@ -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"); }); @@ -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"); }); diff --git a/docs/src/content/docs/examples/campaigns/security-audit.campaign.md b/docs/src/content/docs/examples/campaigns/security-audit.campaign.md index 703b9b15b4..9880deb93c 100644 --- a/docs/src/content/docs/examples/campaigns/security-audit.campaign.md +++ b/docs/src/content/docs/examples/campaigns/security-audit.campaign.md @@ -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: diff --git a/docs/src/content/docs/guides/campaigns/specs.md b/docs/src/content/docs/guides/campaigns/specs.md index 54747dd06d..edc42883c7 100644 --- a/docs/src/content/docs/guides/campaigns/specs.md +++ b/docs/src/content/docs/guides/campaigns/specs.md @@ -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: @@ -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:`). +- `tracker-label`: Label used to discover worker-created issues/PRs (format: `z_campaign_`). - `state`: Lifecycle state (`planned`, `active`, `paused`, `completed`, or `archived`). ### Optional @@ -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 diff --git a/docs/src/content/docs/reference/safe-outputs.md b/docs/src/content/docs/reference/safe-outputs.md index 47a0c0f1ed..0aec03f763 100644 --- a/docs/src/content/docs/reference/safe-outputs.md +++ b/docs/src/content/docs/reference/safe-outputs.md @@ -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:` 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_` labels for [Campaign Workflows](/gh-aw/guides/campaigns/). Exposes outputs: `project-id`, `project-number`, `project-url`, `campaign-id`, `item-id`. #### Supported Field Types @@ -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:` labels, syncs boards. See [Campaign Workflows](/gh-aw/guides/campaigns/). +Combine `create-issue` + `update-project` for coordinated initiatives. Returns campaign ID, applies `z_campaign_` labels, syncs boards. See [Campaign Workflows](/gh-aw/guides/campaigns/). ## Custom Messages (`messages:`) diff --git a/pkg/workflow/safe_output_validation_config.go b/pkg/workflow/safe_output_validation_config.go index a7cd677f57..2e3904880a 100644 --- a/pkg/workflow/safe_output_validation_config.go +++ b/pkg/workflow/safe_output_validation_config.go @@ -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:" label. + // When provided, the update-project safe output applies a "z_campaign_" 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"}},