diff --git a/.github/aw/main_workflow_schema.json b/.github/aw/main_workflow_schema.json index 6d13fd7f8f..e4bcb398e1 100644 --- a/.github/aw/main_workflow_schema.json +++ b/.github/aw/main_workflow_schema.json @@ -3161,7 +3161,7 @@ "oneOf": [ { "type": "object", - "description": "Configuration for managing GitHub Projects v2 boards. Smart tool that can add issue/PR items and update custom fields on existing items. By default it is update-only: if the project does not exist, the job fails with instructions to create it manually. To allow workflows to create missing projects, explicitly opt in via the agent output field create_if_missing=true (and/or provide a github-token override). NOTE: Projects v2 requires a Personal Access Token (PAT) or GitHub App token with appropriate permissions; the GITHUB_TOKEN cannot be used for Projects v2. Safe output items produced by the agent use type=update_project and may include: project (board name), content_type (issue|pull_request), content_number, fields, campaign_id, and create_if_missing.", + "description": "Configuration for managing GitHub Projects v2 boards. Smart tool that can add issue/PR items and update custom fields on existing items. By default it is update-only: if the project does not exist, the job fails with instructions to create it manually. To allow workflows to create missing projects, explicitly opt in via the agent output field create_if_missing=true (and/or provide a github-token override). NOTE: Projects v2 requires a Personal Access Token (PAT) or GitHub App token with appropriate permissions; the GITHUB_TOKEN cannot be used for Projects v2. Safe output items produced by the agent use type=update_project and may include: project (board name), content_type (issue|pull_request), content_number, fields, and create_if_missing.", "properties": { "max": { "type": "integer", diff --git a/.github/workflows/code-scanning-fixer.lock.yml b/.github/workflows/code-scanning-fixer.lock.yml index 533cf2c2f4..83c8f89bdb 100644 --- a/.github/workflows/code-scanning-fixer.lock.yml +++ b/.github/workflows/code-scanning-fixer.lock.yml @@ -21,7 +21,7 @@ # # Automatically fixes code scanning alerts by creating pull requests with remediation # -# frontmatter-hash: 881c737fb47033df8bfd4a3833103ac7940624506696e41086caf40c29c4003f +# frontmatter-hash: db23396c88368a8a4ae67a081c2670a29099890ce486d349da5fa1e69d5c244c name: "Code Scanning Fixer" "on": @@ -1175,7 +1175,6 @@ jobs: MAX_FILE_SIZE: 10240 MAX_FILE_COUNT: 100 FILE_GLOB_FILTER: "security-alert-burndown/**" - GH_AW_CAMPAIGN_ID: security-alert-burndown with: script: | const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); diff --git a/.github/workflows/code-scanning-fixer.md b/.github/workflows/code-scanning-fixer.md index b83e3c6923..c5982d9a5f 100644 --- a/.github/workflows/code-scanning-fixer.md +++ b/.github/workflows/code-scanning-fixer.md @@ -17,7 +17,6 @@ tools: - id: campaigns branch-name: memory/campaigns file-glob: [security-alert-burndown/**] - campaign-id: security-alert-burndown edit: bash: cache-memory: diff --git a/.github/workflows/dependabot-bundler.lock.yml b/.github/workflows/dependabot-bundler.lock.yml index 7287a3b361..884a5faa7b 100644 --- a/.github/workflows/dependabot-bundler.lock.yml +++ b/.github/workflows/dependabot-bundler.lock.yml @@ -21,7 +21,7 @@ # # Bundles Dependabot security alert updates per package.json into a single PR # -# frontmatter-hash: f1c32c1663b19f69def5ac20f545124b5a9ebaf463e98d55a5d8f9e628963f29 +# frontmatter-hash: 7210b79f508961cee24ddebdf483da9e0d91676a09cb2735ba5cfce598c2252d name: "Dependabot Bundler" "on": @@ -1175,7 +1175,6 @@ jobs: MAX_FILE_SIZE: 10240 MAX_FILE_COUNT: 100 FILE_GLOB_FILTER: "security-alert-burndown/**" - GH_AW_CAMPAIGN_ID: security-alert-burndown with: script: | const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); diff --git a/.github/workflows/dependabot-bundler.md b/.github/workflows/dependabot-bundler.md index 405ea07868..09fca95903 100644 --- a/.github/workflows/dependabot-bundler.md +++ b/.github/workflows/dependabot-bundler.md @@ -17,7 +17,6 @@ tools: - id: campaigns branch-name: memory/campaigns file-glob: [security-alert-burndown/**] - campaign-id: security-alert-burndown cache-memory: edit: bash: diff --git a/.github/workflows/dependabot-burner.lock.yml b/.github/workflows/dependabot-burner.lock.yml index e266a9b646..c698b1c1ad 100644 --- a/.github/workflows/dependabot-burner.lock.yml +++ b/.github/workflows/dependabot-burner.lock.yml @@ -206,14 +206,10 @@ jobs: "name": "noop" }, { - "description": "Add or update items in GitHub Projects v2 boards. Can add issues/PRs to a project and update custom field values. Requires the project URL, content type (issue or pull_request), and content number. Use campaign_id to group related items.\n\nThree usage modes:\n1. Add/update project item: Requires project + content_type. For 'issue' or 'pull_request', also requires content_number. For 'draft_issue', requires draft_title.\n2. Create project fields: Requires project + operation='create_fields' + field_definitions.\n3. Create project view: Requires project + operation='create_view' + view.", + "description": "Add or update items in GitHub Projects v2 boards. Can add issues/PRs to a project and update custom field values. Requires the project URL, content type (issue or pull_request), and content number.\n\nThree usage modes:\n1. Add/update project item: Requires project + content_type. For 'issue' or 'pull_request', also requires content_number. For 'draft_issue', requires draft_title.\n2. Create project fields: Requires project + operation='create_fields' + field_definitions.\n3. Create project view: Requires project + operation='create_view' + view.", "inputSchema": { "additionalProperties": false, "properties": { - "campaign_id": { - "description": "Campaign identifier to group related project items. Used to track items created by the same campaign or workflow run.", - "type": "string" - }, "content_number": { "description": "Issue or pull request number to add to the project. This is the numeric ID from the GitHub URL (e.g., 123 in github.com/owner/repo/issues/123 for issue #123, or 456 in github.com/owner/repo/pull/456 for PR #456). Required when content_type is 'issue' or 'pull_request'.", "type": "number" @@ -280,7 +276,7 @@ jobs: "type": "object" }, "operation": { - "description": "Optional operation mode. Use create_fields to create required campaign fields up-front, or create_view to add a project view. When omitted, the tool adds/updates project items.", + "description": "Optional operation mode. Use create_fields to create required fields up-front, or create_view to add a project view. When omitted, the tool adds/updates project items.", "enum": [ "create_fields", "create_view" @@ -398,11 +394,6 @@ jobs: "update_project": { "defaultMax": 10, "fields": { - "campaign_id": { - "type": "string", - "sanitize": true, - "maxLength": 128 - }, "content_number": { "optionalPositiveInteger": true }, diff --git a/.github/workflows/poem-bot.lock.yml b/.github/workflows/poem-bot.lock.yml index 3bbf4003ef..abc4a490e5 100644 --- a/.github/workflows/poem-bot.lock.yml +++ b/.github/workflows/poem-bot.lock.yml @@ -469,7 +469,7 @@ jobs: ] }, "labels": { - "description": "Replace the issue labels with this list (e.g., ['bug', 'campaign:foo']). Labels must exist in the repository.", + "description": "Replace the issue labels with this list (e.g., ['bug', 'tracking:foo']). Labels must exist in the repository.", "items": { "type": "string" }, diff --git a/.github/workflows/secret-scanning-triage.lock.yml b/.github/workflows/secret-scanning-triage.lock.yml index 65ec01f9fd..a2adf42c71 100644 --- a/.github/workflows/secret-scanning-triage.lock.yml +++ b/.github/workflows/secret-scanning-triage.lock.yml @@ -25,7 +25,7 @@ # Imports: # - shared/reporting.md # -# frontmatter-hash: 0df9f27fc6a053c505fbc25cf294fcaac3501531f3e9ead36c26a2756bab0d08 +# frontmatter-hash: 22c6d377698a33ccc0baa87481ab6e2d4fbe911ae3a997dcb02b201517ed3188 name: "Secret Scanning Triage" "on": @@ -1280,7 +1280,6 @@ jobs: MAX_FILE_SIZE: 10240 MAX_FILE_COUNT: 100 FILE_GLOB_FILTER: "security-alert-burndown/**" - GH_AW_CAMPAIGN_ID: security-alert-burndown with: script: | const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); diff --git a/.github/workflows/secret-scanning-triage.md b/.github/workflows/secret-scanning-triage.md index 4790e0157b..763a5e6bd5 100644 --- a/.github/workflows/secret-scanning-triage.md +++ b/.github/workflows/secret-scanning-triage.md @@ -17,7 +17,6 @@ tools: - id: campaigns branch-name: memory/campaigns file-glob: [security-alert-burndown/**] - campaign-id: security-alert-burndown cache-memory: edit: bash: diff --git a/.github/workflows/security-alert-burndown.lock.yml b/.github/workflows/security-alert-burndown.lock.yml index e73a43f2d7..583f6df90e 100644 --- a/.github/workflows/security-alert-burndown.lock.yml +++ b/.github/workflows/security-alert-burndown.lock.yml @@ -246,14 +246,10 @@ jobs: "name": "noop" }, { - "description": "Add or update items in GitHub Projects v2 boards. Can add issues/PRs to a project and update custom field values. Requires the project URL, content type (issue or pull_request), and content number. Use campaign_id to group related items.\n\nThree usage modes:\n1. Add/update project item: Requires project + content_type. For 'issue' or 'pull_request', also requires content_number. For 'draft_issue', requires draft_title.\n2. Create project fields: Requires project + operation='create_fields' + field_definitions.\n3. Create project view: Requires project + operation='create_view' + view.", + "description": "Add or update items in GitHub Projects v2 boards. Can add issues/PRs to a project and update custom field values. Requires the project URL, content type (issue or pull_request), and content number.\n\nThree usage modes:\n1. Add/update project item: Requires project + content_type. For 'issue' or 'pull_request', also requires content_number. For 'draft_issue', requires draft_title.\n2. Create project fields: Requires project + operation='create_fields' + field_definitions.\n3. Create project view: Requires project + operation='create_view' + view.", "inputSchema": { "additionalProperties": false, "properties": { - "campaign_id": { - "description": "Campaign identifier to group related project items. Used to track items created by the same campaign or workflow run.", - "type": "string" - }, "content_number": { "description": "Issue or pull request number to add to the project. This is the numeric ID from the GitHub URL (e.g., 123 in github.com/owner/repo/issues/123 for issue #123, or 456 in github.com/owner/repo/pull/456 for PR #456). Required when content_type is 'issue' or 'pull_request'.", "type": "number" @@ -320,7 +316,7 @@ jobs: "type": "object" }, "operation": { - "description": "Optional operation mode. Use create_fields to create required campaign fields up-front, or create_view to add a project view. When omitted, the tool adds/updates project items.", + "description": "Optional operation mode. Use create_fields to create required fields up-front, or create_view to add a project view. When omitted, the tool adds/updates project items.", "enum": [ "create_fields", "create_view" @@ -471,11 +467,6 @@ jobs: "update_project": { "defaultMax": 10, "fields": { - "campaign_id": { - "type": "string", - "sanitize": true, - "maxLength": 128 - }, "content_number": { "optionalPositiveInteger": true }, diff --git a/.github/workflows/security-fix-pr.lock.yml b/.github/workflows/security-fix-pr.lock.yml index f40252073b..21ae46191a 100644 --- a/.github/workflows/security-fix-pr.lock.yml +++ b/.github/workflows/security-fix-pr.lock.yml @@ -21,7 +21,7 @@ # # Identifies and automatically fixes code security issues by creating autofixes via GitHub Code Scanning # -# frontmatter-hash: 9507342e8275a292c3181cedb701ea27e9f017e1a4b81800606f27c92da09b0c +# frontmatter-hash: 2c0874c7d5caae7ad004f661800b012b5e12571c5c4ba5f776cd3e7eb8c0d484 name: "Security Fix PR" "on": @@ -1129,7 +1129,6 @@ jobs: MAX_FILE_SIZE: 10240 MAX_FILE_COUNT: 100 FILE_GLOB_FILTER: "security-alert-burndown/**" - GH_AW_CAMPAIGN_ID: security-alert-burndown with: script: | const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); diff --git a/.github/workflows/security-fix-pr.md b/.github/workflows/security-fix-pr.md index 214d0b39fc..79d8ae47ba 100644 --- a/.github/workflows/security-fix-pr.md +++ b/.github/workflows/security-fix-pr.md @@ -22,7 +22,6 @@ tools: - id: campaigns branch-name: memory/campaigns file-glob: [security-alert-burndown/**] - campaign-id: security-alert-burndown cache-memory: safe-outputs: add-labels: diff --git a/.github/workflows/smoke-project.lock.yml b/.github/workflows/smoke-project.lock.yml index 38a7a0bfe8..31cc5c8eac 100644 --- a/.github/workflows/smoke-project.lock.yml +++ b/.github/workflows/smoke-project.lock.yml @@ -217,14 +217,10 @@ jobs: "name": "noop" }, { - "description": "Add or update items in GitHub Projects v2 boards. Can add issues/PRs to a project and update custom field values. Requires the project URL, content type (issue or pull_request), and content number. Use campaign_id to group related items.\n\nThree usage modes:\n1. Add/update project item: Requires project + content_type. For 'issue' or 'pull_request', also requires content_number. For 'draft_issue', requires draft_title.\n2. Create project fields: Requires project + operation='create_fields' + field_definitions.\n3. Create project view: Requires project + operation='create_view' + view.", + "description": "Add or update items in GitHub Projects v2 boards. Can add issues/PRs to a project and update custom field values. Requires the project URL, content type (issue or pull_request), and content number.\n\nThree usage modes:\n1. Add/update project item: Requires project + content_type. For 'issue' or 'pull_request', also requires content_number. For 'draft_issue', requires draft_title.\n2. Create project fields: Requires project + operation='create_fields' + field_definitions.\n3. Create project view: Requires project + operation='create_view' + view.", "inputSchema": { "additionalProperties": false, "properties": { - "campaign_id": { - "description": "Campaign identifier to group related project items. Used to track items created by the same campaign or workflow run.", - "type": "string" - }, "content_number": { "description": "Issue or pull request number to add to the project. This is the numeric ID from the GitHub URL (e.g., 123 in github.com/owner/repo/issues/123 for issue #123, or 456 in github.com/owner/repo/pull/456 for PR #456). Required when content_type is 'issue' or 'pull_request'.", "type": "number" @@ -291,7 +287,7 @@ jobs: "type": "object" }, "operation": { - "description": "Optional operation mode. Use create_fields to create required campaign fields up-front, or create_view to add a project view. When omitted, the tool adds/updates project items.", + "description": "Optional operation mode. Use create_fields to create required fields up-front, or create_view to add a project view. When omitted, the tool adds/updates project items.", "enum": [ "create_fields", "create_view" @@ -492,11 +488,6 @@ jobs: "update_project": { "defaultMax": 10, "fields": { - "campaign_id": { - "type": "string", - "sanitize": true, - "maxLength": 128 - }, "content_number": { "optionalPositiveInteger": true }, diff --git a/.github/workflows/sub-issue-closer.lock.yml b/.github/workflows/sub-issue-closer.lock.yml index 30959b36d8..d99114d4c4 100644 --- a/.github/workflows/sub-issue-closer.lock.yml +++ b/.github/workflows/sub-issue-closer.lock.yml @@ -209,7 +209,7 @@ jobs: ] }, "labels": { - "description": "Replace the issue labels with this list (e.g., ['bug', 'campaign:foo']). Labels must exist in the repository.", + "description": "Replace the issue labels with this list (e.g., ['bug', 'tracking:foo']). Labels must exist in the repository.", "items": { "type": "string" }, diff --git a/.github/workflows/test-project-url-default.lock.yml b/.github/workflows/test-project-url-default.lock.yml index b95c6d386e..69d5cb8536 100644 --- a/.github/workflows/test-project-url-default.lock.yml +++ b/.github/workflows/test-project-url-default.lock.yml @@ -202,14 +202,10 @@ jobs: "name": "noop" }, { - "description": "Add or update items in GitHub Projects v2 boards. Can add issues/PRs to a project and update custom field values. Requires the project URL, content type (issue or pull_request), and content number. Use campaign_id to group related items.\n\nThree usage modes:\n1. Add/update project item: Requires project + content_type. For 'issue' or 'pull_request', also requires content_number. For 'draft_issue', requires draft_title.\n2. Create project fields: Requires project + operation='create_fields' + field_definitions.\n3. Create project view: Requires project + operation='create_view' + view.", + "description": "Add or update items in GitHub Projects v2 boards. Can add issues/PRs to a project and update custom field values. Requires the project URL, content type (issue or pull_request), and content number.\n\nThree usage modes:\n1. Add/update project item: Requires project + content_type. For 'issue' or 'pull_request', also requires content_number. For 'draft_issue', requires draft_title.\n2. Create project fields: Requires project + operation='create_fields' + field_definitions.\n3. Create project view: Requires project + operation='create_view' + view.", "inputSchema": { "additionalProperties": false, "properties": { - "campaign_id": { - "description": "Campaign identifier to group related project items. Used to track items created by the same campaign or workflow run.", - "type": "string" - }, "content_number": { "description": "Issue or pull request number to add to the project. This is the numeric ID from the GitHub URL (e.g., 123 in github.com/owner/repo/issues/123 for issue #123, or 456 in github.com/owner/repo/pull/456 for PR #456). Required when content_type is 'issue' or 'pull_request'.", "type": "number" @@ -276,7 +272,7 @@ jobs: "type": "object" }, "operation": { - "description": "Optional operation mode. Use create_fields to create required campaign fields up-front, or create_view to add a project view. When omitted, the tool adds/updates project items.", + "description": "Optional operation mode. Use create_fields to create required fields up-front, or create_view to add a project view. When omitted, the tool adds/updates project items.", "enum": [ "create_fields", "create_view" @@ -477,11 +473,6 @@ jobs: "update_project": { "defaultMax": 10, "fields": { - "campaign_id": { - "type": "string", - "sanitize": true, - "maxLength": 128 - }, "content_number": { "optionalPositiveInteger": true }, diff --git a/.github/workflows/workflow-generator.lock.yml b/.github/workflows/workflow-generator.lock.yml index 633af55e63..2643b20798 100644 --- a/.github/workflows/workflow-generator.lock.yml +++ b/.github/workflows/workflow-generator.lock.yml @@ -245,7 +245,7 @@ jobs: ] }, "labels": { - "description": "Replace the issue labels with this list (e.g., ['bug', 'campaign:foo']). Labels must exist in the repository.", + "description": "Replace the issue labels with this list (e.g., ['bug', 'tracking:foo']). Labels must exist in the repository.", "items": { "type": "string" }, diff --git a/.github/workflows/workflow-health-manager.lock.yml b/.github/workflows/workflow-health-manager.lock.yml index 6becffb0a4..01bf1c6fe1 100644 --- a/.github/workflows/workflow-health-manager.lock.yml +++ b/.github/workflows/workflow-health-manager.lock.yml @@ -266,7 +266,7 @@ jobs: ] }, "labels": { - "description": "Replace the issue labels with this list (e.g., ['bug', 'campaign:foo']). Labels must exist in the repository.", + "description": "Replace the issue labels with this list (e.g., ['bug', 'tracking:foo']). Labels must exist in the repository.", "items": { "type": "string" }, diff --git a/.serena/project.yml b/.serena/project.yml index e9afd2fde0..81a0cea3cc 100644 --- a/.serena/project.yml +++ b/.serena/project.yml @@ -81,6 +81,27 @@ excluded_tools: [] # initial prompt for the project. It will always be given to the LLM upon activating the project # (contrary to the memories, which are loaded on demand). initial_prompt: "" - +# the name by which the project can be referenced within Serena project_name: "gh-aw" + +# list of tools to include that would otherwise be disabled (particularly optional tools that are disabled by default) included_optional_tools: [] + +# list of mode names to that are always to be included in the set of active modes +# The full set of modes to be activated is base_modes + default_modes. +# If the setting is undefined, the base_modes from the global configuration (serena_config.yml) apply. +# Otherwise, this setting overrides the global configuration. +# Set this to [] to disable base modes for this project. +# Set this to a list of mode names to always include the respective modes for this project. +base_modes: + +# list of mode names that are to be activated by default. +# The full set of modes to be activated is base_modes + default_modes. +# If the setting is undefined, the default_modes from the global configuration (serena_config.yml) apply. +# Otherwise, this overrides the setting from the global configuration (serena_config.yml). +# This setting can, in turn, be overridden by CLI parameters (--mode). +default_modes: + +# fixed set of tools to use as the base tool set (if non-empty), replacing Serena's default set of tools. +# This cannot be combined with non-empty excluded_tools or included_optional_tools. +fixed_tools: [] diff --git a/actions/setup/js/campaign_labels.cjs b/actions/setup/js/campaign_labels.cjs deleted file mode 100644 index b66c3388c8..0000000000 --- a/actions/setup/js/campaign_labels.cjs +++ /dev/null @@ -1,45 +0,0 @@ -// @ts-check - -/** - * Campaign Labels Helper - * - * Utility functions for handling campaign labels in safe outputs. - * These functions normalize campaign IDs and retrieve campaign labels from environment variables. - */ - -const DEFAULT_AGENTIC_CAMPAIGN_LABEL = "agentic-campaign"; - -/** - * Normalize campaign IDs to the same label format used by campaign discovery. - * Mirrors actions/setup/js/campaign_discovery.cjs. - * @param {string} campaignId - * @returns {string} - */ -function formatCampaignLabel(campaignId) { - return `z_campaign_${String(campaignId) - .toLowerCase() - .replace(/[_\s]+/g, "-")}`; -} - -/** - * 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(); - - if (!campaignId) { - return { enabled: false, labels: [] }; - } - - return { - enabled: true, - labels: [DEFAULT_AGENTIC_CAMPAIGN_LABEL, formatCampaignLabel(campaignId)], - }; -} - -module.exports = { - formatCampaignLabel, - getCampaignLabelsFromEnv, -}; diff --git a/actions/setup/js/campaign_labels.test.cjs b/actions/setup/js/campaign_labels.test.cjs deleted file mode 100644 index fe3f5c1131..0000000000 --- a/actions/setup/js/campaign_labels.test.cjs +++ /dev/null @@ -1,136 +0,0 @@ -// @ts-check -import { describe, it, expect, afterEach } from "vitest"; -import { formatCampaignLabel, getCampaignLabelsFromEnv } from "./campaign_labels.cjs"; - -describe("formatCampaignLabel", () => { - it("should format simple campaign ID", () => { - expect(formatCampaignLabel("test")).toBe("z_campaign_test"); - }); - - it("should convert uppercase to lowercase", () => { - expect(formatCampaignLabel("TEST")).toBe("z_campaign_test"); - expect(formatCampaignLabel("MyTest")).toBe("z_campaign_mytest"); - }); - - it("should replace spaces with hyphens", () => { - expect(formatCampaignLabel("my test")).toBe("z_campaign_my-test"); - expect(formatCampaignLabel("my test")).toBe("z_campaign_my-test"); - }); - - it("should replace underscores with hyphens", () => { - expect(formatCampaignLabel("my_test")).toBe("z_campaign_my-test"); - expect(formatCampaignLabel("my___test")).toBe("z_campaign_my-test"); - }); - - it("should handle mixed spaces and underscores", () => { - expect(formatCampaignLabel("my_ _test")).toBe("z_campaign_my-test"); - expect(formatCampaignLabel("my _ test")).toBe("z_campaign_my-test"); - }); - - it("should handle numeric campaign IDs", () => { - expect(formatCampaignLabel("123")).toBe("z_campaign_123"); - expect(formatCampaignLabel("2024_Q1")).toBe("z_campaign_2024-q1"); - }); - - it("should handle empty string", () => { - expect(formatCampaignLabel("")).toBe("z_campaign_"); - }); - - it("should handle complex campaign IDs", () => { - expect(formatCampaignLabel("My_Test Campaign 123")).toBe("z_campaign_my-test-campaign-123"); - }); -}); - -describe("getCampaignLabelsFromEnv", () => { - // Store original env var - const originalEnv = process.env.GH_AW_CAMPAIGN_ID; - - afterEach(() => { - // Restore original env var - if (originalEnv !== undefined) { - process.env.GH_AW_CAMPAIGN_ID = originalEnv; - } else { - delete process.env.GH_AW_CAMPAIGN_ID; - } - }); - - it("should return disabled with empty labels when no campaign ID", () => { - delete process.env.GH_AW_CAMPAIGN_ID; - const result = getCampaignLabelsFromEnv(); - - expect(result.enabled).toBe(false); - expect(result.labels).toEqual([]); - }); - - it("should return disabled with empty labels when campaign ID is empty string", () => { - process.env.GH_AW_CAMPAIGN_ID = ""; - const result = getCampaignLabelsFromEnv(); - - expect(result.enabled).toBe(false); - expect(result.labels).toEqual([]); - }); - - it("should return disabled with empty labels when campaign ID is whitespace", () => { - process.env.GH_AW_CAMPAIGN_ID = " "; - const result = getCampaignLabelsFromEnv(); - - expect(result.enabled).toBe(false); - expect(result.labels).toEqual([]); - }); - - it("should return enabled with generic and specific labels", () => { - process.env.GH_AW_CAMPAIGN_ID = "test"; - const result = getCampaignLabelsFromEnv(); - - expect(result.enabled).toBe(true); - expect(result.labels).toEqual(["agentic-campaign", "z_campaign_test"]); - }); - - it("should trim whitespace from campaign ID", () => { - process.env.GH_AW_CAMPAIGN_ID = " test "; - const result = getCampaignLabelsFromEnv(); - - expect(result.enabled).toBe(true); - expect(result.labels).toEqual(["agentic-campaign", "z_campaign_test"]); - }); - - it("should handle uppercase campaign ID", () => { - process.env.GH_AW_CAMPAIGN_ID = "TEST"; - const result = getCampaignLabelsFromEnv(); - - expect(result.enabled).toBe(true); - expect(result.labels).toEqual(["agentic-campaign", "z_campaign_test"]); - }); - - it("should handle campaign ID with spaces", () => { - process.env.GH_AW_CAMPAIGN_ID = "my test"; - const result = getCampaignLabelsFromEnv(); - - expect(result.enabled).toBe(true); - expect(result.labels).toEqual(["agentic-campaign", "z_campaign_my-test"]); - }); - - it("should handle campaign ID with underscores", () => { - process.env.GH_AW_CAMPAIGN_ID = "my_test"; - const result = getCampaignLabelsFromEnv(); - - expect(result.enabled).toBe(true); - expect(result.labels).toEqual(["agentic-campaign", "z_campaign_my-test"]); - }); - - it("should handle complex campaign ID", () => { - process.env.GH_AW_CAMPAIGN_ID = "My_Test Campaign 123"; - const result = getCampaignLabelsFromEnv(); - - expect(result.enabled).toBe(true); - expect(result.labels).toEqual(["agentic-campaign", "z_campaign_my-test-campaign-123"]); - }); - - it("should handle numeric campaign ID", () => { - process.env.GH_AW_CAMPAIGN_ID = "123"; - const result = getCampaignLabelsFromEnv(); - - expect(result.enabled).toBe(true); - expect(result.labels).toEqual(["agentic-campaign", "z_campaign_123"]); - }); -}); diff --git a/actions/setup/js/create_project.cjs b/actions/setup/js/create_project.cjs index a9e20fa3de..f1834774d3 100644 --- a/actions/setup/js/create_project.cjs +++ b/actions/setup/js/create_project.cjs @@ -291,7 +291,7 @@ async function main(config = {}, githubClient = null) { // Extract configuration const defaultTargetOwner = config.target_owner || ""; const maxCount = config.max || 1; - const titlePrefix = config.title_prefix || "Campaign"; + const titlePrefix = config.title_prefix || "Project"; const configuredViews = Array.isArray(config.views) ? config.views : []; // Use the provided github client, or fall back to the global github object @@ -341,18 +341,18 @@ async function main(config = {}, githubClient = null) { // Generate a title if not provided by the agent if (!title) { - // Try to generate a campaign title from the issue context + // Try to generate a project title from the issue context const issueTitle = context.payload?.issue?.title; const issueNumber = context.payload?.issue?.number; if (issueTitle) { // Use the issue title with the configured prefix title = `${titlePrefix}: ${issueTitle}`; - core.info(`Generated campaign title from issue: "${title}"`); + core.info(`Generated title from issue: "${title}"`); } else if (issueNumber) { // Fallback to issue number if no title is available title = `${titlePrefix} #${issueNumber}`; - core.info(`Generated campaign title from issue number: "${title}"`); + core.info(`Generated title from issue number: "${title}"`); } else { throw new Error("Missing required field 'title' in create_project call and unable to generate from context"); } diff --git a/actions/setup/js/glob_pattern_helpers.test.cjs b/actions/setup/js/glob_pattern_helpers.test.cjs index eb5f2b180b..138bdb3084 100644 --- a/actions/setup/js/glob_pattern_helpers.test.cjs +++ b/actions/setup/js/glob_pattern_helpers.test.cjs @@ -98,7 +98,7 @@ describe("glob_pattern_helpers.cjs", () => { expect(flexibleRegex.test("metrics/daily/file.json")).toBe(true); }); - it("should match campaign-specific patterns", () => { + it("should match prefix-scoped patterns", () => { const cursorRegex = globPatternToRegex("security-q1/cursor.json"); const metricsRegex = globPatternToRegex("security-q1/metrics/**"); @@ -277,7 +277,7 @@ describe("glob_pattern_helpers.cjs", () => { expect(matchesGlobPattern("script.js", filter)).toBe(false); }); - it("should work with campaign patterns", () => { + it("should work with prefix-scoped patterns", () => { const filter = "security-q1/cursor.json security-q1/metrics/**"; expect(matchesGlobPattern("security-q1/cursor.json", filter)).toBe(true); diff --git a/actions/setup/js/push_repo_memory.cjs b/actions/setup/js/push_repo_memory.cjs index 3616d9ea09..055669ac25 100644 --- a/actions/setup/js/push_repo_memory.cjs +++ b/actions/setup/js/push_repo_memory.cjs @@ -30,8 +30,6 @@ const { globPatternToRegex } = require("./glob_pattern_helpers.cjs"); * INCORRECT pattern: "memory/code-metrics/*.jsonl" (includes branch name) * * The branch name is used for git operations (checkout, push) but not for pattern matching. - * GH_AW_CAMPAIGN_ID: Optional campaign ID override. When set with MEMORY_ID=campaigns, - * enforces all FILE_GLOB_FILTER patterns are under /... * GH_TOKEN: GitHub token for authentication * GITHUB_RUN_ID: Workflow run ID for commit messages */ @@ -73,114 +71,6 @@ async function main() { } } - // ============================================================================ - // CAMPAIGN-SPECIFIC VALIDATION FUNCTIONS - // ============================================================================ - // The following functions implement validation for the campaign convention: - // When memoryId is "campaigns" and file-glob matches "/**", - // enforce specific JSON schemas for cursor.json and metrics/*.json files. - // - // This is a domain-specific convention used by Campaign Workflows to maintain - // durable state in repo-memory. See docs/guides/campaigns/ for details. - // ============================================================================ - - /** @param {any} obj @param {string} campaignId @param {string} relPath */ - function validateCampaignCursor(obj, campaignId, relPath) { - if (!isPlainObject(obj)) { - throw new Error(`Cursor must be a JSON object: ${relPath}`); - } - - // Cursor payload is intentionally treated as an opaque checkpoint. - // We only enforce that it is valid JSON and (optionally) self-identifies the campaign. - if (obj.campaign_id !== undefined) { - if (typeof obj.campaign_id !== "string" || obj.campaign_id.trim() === "") { - throw new Error(`Cursor 'campaign_id' must be a non-empty string when present: ${relPath}`); - } - if (obj.campaign_id !== campaignId) { - throw new Error(`Cursor 'campaign_id' must match '${campaignId}' when present: ${relPath}`); - } - } - - // Allow optional date metadata if the cursor chooses to include it. - if (obj.date !== undefined) { - if (typeof obj.date !== "string" || obj.date.trim() === "") { - throw new Error(`Cursor 'date' must be a non-empty string (YYYY-MM-DD) when present: ${relPath}`); - } - if (!/^\d{4}-\d{2}-\d{2}$/.test(obj.date)) { - throw new Error(`Cursor 'date' must be YYYY-MM-DD when present: ${relPath}`); - } - } - } - - /** @param {any} obj @param {string} campaignId @param {string} relPath */ - function validateCampaignMetricsSnapshot(obj, campaignId, relPath) { - if (!isPlainObject(obj)) { - throw new Error(`Metrics snapshot must be a JSON object: ${relPath}`); - } - if (typeof obj.campaign_id !== "string" || obj.campaign_id.trim() === "") { - throw new Error(`Metrics snapshot must include non-empty 'campaign_id': ${relPath}`); - } - if (obj.campaign_id !== campaignId) { - throw new Error(`Metrics snapshot 'campaign_id' must match '${campaignId}': ${relPath}`); - } - if (typeof obj.date !== "string" || obj.date.trim() === "") { - throw new Error(`Metrics snapshot must include non-empty 'date' (YYYY-MM-DD): ${relPath}`); - } - if (!/^\d{4}-\d{2}-\d{2}$/.test(obj.date)) { - throw new Error(`Metrics snapshot 'date' must be YYYY-MM-DD: ${relPath}`); - } - - // Require these to be present and non-negative integers (aligns with CampaignMetricsSnapshot). - const requiredIntFields = ["tasks_total", "tasks_completed"]; - for (const field of requiredIntFields) { - const value = obj[field]; - if (value === null || value === undefined) { - throw new Error(`Metrics snapshot '${field}' is required but was ${value === null ? "null" : "undefined"}: ${relPath}`); - } - if (typeof value !== "number") { - throw new Error(`Metrics snapshot '${field}' must be a number, got ${typeof value} (value: ${JSON.stringify(value)}): ${relPath}`); - } - if (!Number.isInteger(value)) { - throw new Error(`Metrics snapshot '${field}' must be an integer, got ${value}: ${relPath}`); - } - if (value < 0) { - throw new Error(`Metrics snapshot '${field}' must be non-negative, got ${value}: ${relPath}`); - } - } - - // Optional numeric fields, if present. - const optionalIntFields = ["tasks_in_progress", "tasks_blocked"]; - for (const field of optionalIntFields) { - const value = obj[field]; - if (value !== undefined && value !== null) { - if (typeof value !== "number") { - throw new Error(`Metrics snapshot '${field}' must be a number when present, got ${typeof value} (value: ${JSON.stringify(value)}): ${relPath}`); - } - if (!Number.isInteger(value)) { - throw new Error(`Metrics snapshot '${field}' must be an integer when present, got ${value}: ${relPath}`); - } - if (value < 0) { - throw new Error(`Metrics snapshot '${field}' must be non-negative when present, got ${value}: ${relPath}`); - } - } - } - if (obj.velocity_per_day !== undefined && obj.velocity_per_day !== null) { - const value = obj.velocity_per_day; - if (typeof value !== "number") { - throw new Error(`Metrics snapshot 'velocity_per_day' must be a number when present, got ${typeof value} (value: ${JSON.stringify(value)}): ${relPath}`); - } - if (value < 0) { - throw new Error(`Metrics snapshot 'velocity_per_day' must be non-negative when present, got ${value}: ${relPath}`); - } - } - if (obj.estimated_completion !== undefined && obj.estimated_completion !== null) { - const value = obj.estimated_completion; - if (typeof value !== "string") { - throw new Error(`Metrics snapshot 'estimated_completion' must be a string when present, got ${typeof value} (value: ${JSON.stringify(value)}): ${relPath}`); - } - } - } - // Validate required environment variables if (!artifactDir || !memoryId || !targetRepo || !branchName || !ghToken) { core.setFailed("Missing required environment variables: ARTIFACT_DIR, MEMORY_ID, TARGET_REPO, BRANCH_NAME, GH_TOKEN"); @@ -191,61 +81,8 @@ async function main() { // The artifactDir IS the memory directory (no nested structure needed) const sourceMemoryPath = artifactDir; - // ============================================================================ - // CAMPAIGN MODE DETECTION - // ============================================================================ - // Campaign Workflows use a convention-based pattern in repo-memory: - // - memoryId: "campaigns" - // - file-glob: one or more patterns like "/**" or "/metrics/**" - // - Optional: GH_AW_CAMPAIGN_ID environment variable to explicitly set campaign ID - // - // When this pattern is detected, we enforce campaign-specific validation: - // 1. All patterns must be under "/..." subdirectory - // 2. cursor.json must exist and follow the cursor schema - // 3. At least one metrics/*.json file must exist and follow the metrics schema - // - // This ensures campaigns maintain durable state consistency across workflow runs. - // Non-campaign repo-memory configurations bypass this validation entirely. - // ============================================================================ - - // Allow explicit campaign ID override via environment variable - const explicitCampaignId = process.env.GH_AW_CAMPAIGN_ID || ""; - - // Parse file glob patterns (can be space-separated) - const patterns = fileGlobFilter.trim().split(/\s+/).filter(Boolean); - - // Determine campaign ID from patterns or explicit override - let campaignId = explicitCampaignId; - - // If no explicit campaign ID, try to extract from patterns when memoryId is "campaigns" - if (!campaignId && memoryId === "campaigns" && patterns.length > 0) { - // Try to extract campaign ID from first pattern matching "/**" - // This only works for simple patterns without wildcards in the campaign ID portion - // For patterns like "campaign-id-*/**", use GH_AW_CAMPAIGN_ID environment variable - const campaignMatch = /^([^*?/]+)\/\*\*/.exec(patterns[0]); - if (campaignMatch) { - campaignId = campaignMatch[1]; - } - } - - const isCampaignMode = Boolean(campaignId); - - // Validate all patterns are under campaign-id when in campaign mode - if (isCampaignMode && patterns.length > 0) { - for (const pattern of patterns) { - if (!pattern.startsWith(`${campaignId}/`)) { - core.setFailed(`Campaign mode requires all file patterns to be under '${campaignId}/' subdirectory. Invalid pattern: ${pattern}`); - return; - } - } - } - // Check if artifact memory directory exists if (!fs.existsSync(sourceMemoryPath)) { - if (isCampaignMode) { - core.setFailed(`Campaign repo-memory is enabled but no campaign state was written. Expected to find cursor and metrics under: ${sourceMemoryPath}/${campaignId}/`); - return; - } core.info(`Memory directory not found in artifact: ${sourceMemoryPath}`); return; } @@ -295,9 +132,6 @@ async function main() { // Recursively scan and collect files from artifact directory let filesToCopy = []; - // Track campaign files for validation - let campaignCursorFound = false; - let campaignMetricsCount = 0; // Log the file glob filter configuration if (fileGlobFilter) { @@ -369,20 +203,6 @@ async function main() { throw new Error("File size validation failed"); } - // Campaign-specific JSON validation (only when campaign mode is active) - // This enforces the campaign state file schemas for cursor and metrics - if (isCampaignMode && relativeFilePath.startsWith(`${campaignId}/`)) { - if (relativeFilePath === `${campaignId}/cursor.json`) { - const obj = tryParseJSONFile(fullPath); - validateCampaignCursor(obj, campaignId, relativeFilePath); - campaignCursorFound = true; - } else if (relativeFilePath.startsWith(`${campaignId}/metrics/`) && relativeFilePath.endsWith(".json")) { - const obj = tryParseJSONFile(fullPath); - validateCampaignMetricsSnapshot(obj, campaignId, relativeFilePath); - campaignMetricsCount++; - } - } - filesToCopy.push({ relativePath: relativeFilePath, source: fullPath, @@ -408,22 +228,6 @@ async function main() { return; } - // Campaign mode validation: ensure required state files were found - // This enforcement is only active when campaign mode is detected - if (isCampaignMode) { - if (!campaignCursorFound) { - core.error(`Missing required campaign cursor file: ${campaignId}/cursor.json`); - core.setFailed("Campaign cursor validation failed"); - return; - } - - if (campaignMetricsCount === 0) { - core.error(`Missing required campaign metrics snapshots under: ${campaignId}/metrics/*.json`); - core.setFailed("Campaign metrics validation failed"); - return; - } - } - // Validate file count if (filesToCopy.length > maxFileCount) { core.setFailed(`Too many files (${filesToCopy.length} > ${maxFileCount})`); diff --git a/actions/setup/js/push_repo_memory.test.cjs b/actions/setup/js/push_repo_memory.test.cjs index 575b965ad3..e2a6d74b4c 100644 --- a/actions/setup/js/push_repo_memory.test.cjs +++ b/actions/setup/js/push_repo_memory.test.cjs @@ -97,19 +97,19 @@ describe("push_repo_memory.cjs - globPatternToRegex helper", () => { expect(flexibleRegex.test("metrics/daily/file.json")).toBe(true); }); - it("should match campaign-specific patterns", () => { - const cursorRegex = globPatternToRegex("security-q1/cursor.json"); - const metricsRegex = globPatternToRegex("security-q1/metrics/**"); + it("should match subdirectory-specific patterns", () => { + const cursorRegex = globPatternToRegex("project-1/cursor.json"); + const metricsRegex = globPatternToRegex("project-1/metrics/**"); - expect(cursorRegex.test("security-q1/cursor.json")).toBe(true); - expect(cursorRegex.test("security-q1/metrics/file.json")).toBe(false); + expect(cursorRegex.test("project-1/cursor.json")).toBe(true); + expect(cursorRegex.test("project-1/metrics/file.json")).toBe(false); - expect(metricsRegex.test("security-q1/metrics/2024-12-29.json")).toBe(true); - expect(metricsRegex.test("security-q1/metrics/daily/snapshot.json")).toBe(true); - expect(metricsRegex.test("security-q1/cursor.json")).toBe(false); + expect(metricsRegex.test("project-1/metrics/2024-12-29.json")).toBe(true); + expect(metricsRegex.test("project-1/metrics/daily/snapshot.json")).toBe(true); + expect(metricsRegex.test("project-1/cursor.json")).toBe(false); }); - it("should match flexible campaign pattern for both dated and non-dated structures", () => { + it("should match flexible prefix pattern for both dated and non-dated structures", () => { // Pattern: go-file-size-reduction-project64*/** // This should match BOTH: // - go-file-size-reduction-project64-2025-12-31/ (with date suffix) @@ -124,9 +124,9 @@ describe("push_repo_memory.cjs - globPatternToRegex helper", () => { expect(flexibleRegex.test("go-file-size-reduction-project64/cursor.json")).toBe(true); expect(flexibleRegex.test("go-file-size-reduction-project64/metrics/2025-12-31.json")).toBe(true); - // Should not match other campaigns - expect(flexibleRegex.test("other-campaign/file.json")).toBe(false); - expect(flexibleRegex.test("security-q1/cursor.json")).toBe(false); + // Should not match other prefixes + expect(flexibleRegex.test("other-prefix/file.json")).toBe(false); + expect(flexibleRegex.test("project-1/cursor.json")).toBe(false); }); it("should match multiple file extensions", () => { @@ -664,13 +664,13 @@ describe("push_repo_memory.cjs - glob pattern security tests", () => { describe("multi-pattern filter support", () => { it("should support multiple space-separated patterns", () => { - // Test multiple patterns like "campaign-id/cursor.json campaign-id/metrics/**" - const patterns = "security-q1/cursor.json security-q1/metrics/**".split(/\s+/).filter(Boolean); + // Test multiple patterns like "project-1/cursor.json project-1/metrics/**" + const patterns = "project-1/cursor.json project-1/metrics/**".split(/\s+/).filter(Boolean); // Each pattern should be validated independently expect(patterns).toHaveLength(2); - expect(patterns[0]).toBe("security-q1/cursor.json"); - expect(patterns[1]).toBe("security-q1/metrics/**"); + expect(patterns[0]).toBe("project-1/cursor.json"); + expect(patterns[1]).toBe("project-1/metrics/**"); }); it("should validate each pattern in multi-pattern filter", () => { @@ -698,9 +698,8 @@ describe("push_repo_memory.cjs - glob pattern security tests", () => { expect(regexPatterns[1].test("data/file.json")).toBe(false); }); - it("should handle campaign-specific multi-pattern filters", () => { - // Real-world campaign use case: multiple specific patterns - const patterns = "security-q1/cursor.json security-q1/metrics/**".split(/\s+/).filter(Boolean); + it("should handle multi-pattern filters with nested directories", () => { + const patterns = "project-1/cursor.json project-1/metrics/**".split(/\s+/).filter(Boolean); const regexPatterns = patterns.map(pattern => { const regexPattern = pattern @@ -713,81 +712,14 @@ describe("push_repo_memory.cjs - glob pattern security tests", () => { }); // First pattern: exact cursor file - expect(regexPatterns[0].test("security-q1/cursor.json")).toBe(true); - expect(regexPatterns[0].test("security-q1/cursor.txt")).toBe(false); - expect(regexPatterns[0].test("security-q1/metrics/2024-12-29.json")).toBe(false); + expect(regexPatterns[0].test("project-1/cursor.json")).toBe(true); + expect(regexPatterns[0].test("project-1/cursor.txt")).toBe(false); + expect(regexPatterns[0].test("project-1/metrics/2024-12-29.json")).toBe(false); // Second pattern: any metrics files - expect(regexPatterns[1].test("security-q1/metrics/2024-12-29.json")).toBe(true); - expect(regexPatterns[1].test("security-q1/metrics/daily/snapshot.json")).toBe(true); - expect(regexPatterns[1].test("security-q1/cursor.json")).toBe(false); - }); - }); - - describe("campaign ID validation", () => { - it("should extract campaign ID from first pattern", () => { - // Test extracting campaign ID from pattern like "security-q1/**" - const pattern = "security-q1/**"; - const match = /^([^*?/]+)\/\*\*/.exec(pattern); - - expect(match).not.toBeNull(); - expect(match[1]).toBe("security-q1"); - }); - - it("should validate all patterns start with campaign ID", () => { - // Test that all patterns must be under campaign-id/ subdirectory - const campaignId = "security-q1"; - const validPatterns = ["security-q1/cursor.json", "security-q1/metrics/**", "security-q1/data/*.txt"]; - - for (const pattern of validPatterns) { - expect(pattern.startsWith(`${campaignId}/`)).toBe(true); - } - - const invalidPatterns = ["other-campaign/cursor.json", "cursor.json", "metrics/**"]; - - for (const pattern of invalidPatterns) { - expect(pattern.startsWith(`${campaignId}/`)).toBe(false); - } - }); - - it("should handle campaign ID with hyphens and underscores", () => { - // Test various campaign ID formats - const patterns = ["security-q1-2025/**", "incident_response/**", "rollout-v2_phase1/**"]; - - for (const pattern of patterns) { - const match = /^([^*?/]+)\/\*\*/.exec(pattern); - expect(match).not.toBeNull(); - - // Extracted campaign ID should match the prefix - const campaignId = match[1]; - expect(pattern.startsWith(`${campaignId}/`)).toBe(true); - } - }); - - it("should reject patterns not under campaign ID subdirectory", () => { - // Test enforcement that patterns must be under campaign-id/ - const campaignId = "security-q1"; - - // Valid: under campaign-id/ - expect("security-q1/metrics/**".startsWith(`${campaignId}/`)).toBe(true); - expect("security-q1/cursor.json".startsWith(`${campaignId}/`)).toBe(true); - - // Invalid: not under campaign-id/ - expect("metrics/**".startsWith(`${campaignId}/`)).toBe(false); - expect("other-campaign/data.json".startsWith(`${campaignId}/`)).toBe(false); - expect("cursor.json".startsWith(`${campaignId}/`)).toBe(false); - }); - - it("should support explicit GH_AW_CAMPAIGN_ID override", () => { - // Test that environment variable can override campaign ID detection - // This would be simulated in the actual code by process.env.GH_AW_CAMPAIGN_ID - const explicitCampaignId = "rollout-v2"; - const patterns = ["rollout-v2/cursor.json", "rollout-v2/metrics/**"]; - - // All patterns should validate against explicit campaign ID - for (const pattern of patterns) { - expect(pattern.startsWith(`${explicitCampaignId}/`)).toBe(true); - } + expect(regexPatterns[1].test("project-1/metrics/2024-12-29.json")).toBe(true); + expect(regexPatterns[1].test("project-1/metrics/daily/snapshot.json")).toBe(true); + expect(regexPatterns[1].test("project-1/cursor.json")).toBe(false); }); }); @@ -928,16 +860,16 @@ describe("push_repo_memory.cjs - glob pattern security tests", () => { it("should match patterns against relative paths, not branch-prefixed paths", () => { // This test validates the fix for: https://github.com/github/gh-aw/actions/runs/20613564835 - // Campaign workflows specify patterns relative to the memory directory, + // Workflows specify patterns relative to the memory directory, // not including the branch name prefix. // // Example scenario: - // - Branch name: memory/campaigns + // - Branch name: memory/tracking // - File in artifact: go-file-size-reduction-project64/cursor.json // - Pattern: go-file-size-reduction-project64/** // // The pattern should match the file's relative path within the memory directory, - // NOT the full branch path (memory/campaigns/go-file-size-reduction-project64/cursor.json). + // NOT the full branch path (memory/tracking/go-file-size-reduction-project64/cursor.json). const fileGlobFilter = "go-file-size-reduction-project64/**"; const relativeFilePath = "go-file-size-reduction-project64/cursor.json"; @@ -962,17 +894,17 @@ describe("push_repo_memory.cjs - glob pattern security tests", () => { expect(matchesRelativePath).toBe(true); // Verify it would NOT match if we incorrectly prepended branch name - const branchName = "memory/campaigns"; + const branchName = "memory/tracking"; const branchRelativePath = `${branchName}/${relativeFilePath}`; const matchesBranchPath = patterns.some(p => p.test(branchRelativePath)); expect(matchesBranchPath).toBe(false); // This is the bug we're fixing! - // Additional test cases for the campaign pattern + // Additional test cases for the pattern const testFiles = [ { path: "go-file-size-reduction-project64/cursor.json", shouldMatch: true }, { path: "go-file-size-reduction-project64/metrics/2024-12-31.json", shouldMatch: true }, { path: "go-file-size-reduction-project64/data/config.yaml", shouldMatch: true }, - { path: "other-campaign/cursor.json", shouldMatch: false }, + { path: "other-prefix/cursor.json", shouldMatch: false }, { path: "cursor.json", shouldMatch: false }, ]; @@ -983,7 +915,7 @@ describe("push_repo_memory.cjs - glob pattern security tests", () => { }); it("should allow filtering out legacy files from previous runs", () => { - // Real-world scenario: The memory/campaigns branch had old files with incorrect + // Real-world scenario: A repo-memory branch had old files with incorrect // nesting (memory/default/...) from before a bug fix. When cloning this branch, // these old files are present alongside new correctly-structured files. // The glob filter should match only the new files, allowing old files to be skipped. @@ -995,7 +927,7 @@ describe("push_repo_memory.cjs - glob pattern security tests", () => { // Legacy files with incorrect nesting (should not match) expect(currentPattern.test("memory/default/go-file-size-reduction-20610415309/metrics/2025-12-31.json")).toBe(false); - expect(currentPattern.test("memory/campaigns/go-file-size-reduction-project64/cursor.json")).toBe(false); + expect(currentPattern.test("memory/tracking/go-file-size-reduction-project64/cursor.json")).toBe(false); // This behavior allows push_repo_memory.cjs to skip legacy files instead of failing, // enabling gradual migration from old to new structure without manual branch cleanup. @@ -1062,258 +994,4 @@ describe("push_repo_memory.cjs - glob pattern security tests", () => { // Patterns should match against the relative path within the artifact, not the branch path. }); }); - - describe("metrics validation error messages", () => { - // Helper function to simulate the validation logic from push_repo_memory.cjs - function validateCampaignMetricsSnapshot(obj, campaignId, relPath) { - function isPlainObject(value) { - return typeof value === "object" && value !== null && !Array.isArray(value); - } - - if (!isPlainObject(obj)) { - throw new Error(`Metrics snapshot must be a JSON object: ${relPath}`); - } - if (typeof obj.campaign_id !== "string" || obj.campaign_id.trim() === "") { - throw new Error(`Metrics snapshot must include non-empty 'campaign_id': ${relPath}`); - } - if (obj.campaign_id !== campaignId) { - throw new Error(`Metrics snapshot 'campaign_id' must match '${campaignId}': ${relPath}`); - } - if (typeof obj.date !== "string" || obj.date.trim() === "") { - throw new Error(`Metrics snapshot must include non-empty 'date' (YYYY-MM-DD): ${relPath}`); - } - if (!/^\d{4}-\d{2}-\d{2}$/.test(obj.date)) { - throw new Error(`Metrics snapshot 'date' must be YYYY-MM-DD: ${relPath}`); - } - - // Require these to be present and non-negative integers (aligns with CampaignMetricsSnapshot). - const requiredIntFields = ["tasks_total", "tasks_completed"]; - for (const field of requiredIntFields) { - const value = obj[field]; - if (value === null || value === undefined) { - throw new Error(`Metrics snapshot '${field}' is required but was ${value === null ? "null" : "undefined"}: ${relPath}`); - } - if (typeof value !== "number") { - throw new Error(`Metrics snapshot '${field}' must be a number, got ${typeof value} (value: ${JSON.stringify(value)}): ${relPath}`); - } - if (!Number.isInteger(value)) { - throw new Error(`Metrics snapshot '${field}' must be an integer, got ${value}: ${relPath}`); - } - if (value < 0) { - throw new Error(`Metrics snapshot '${field}' must be non-negative, got ${value}: ${relPath}`); - } - } - - // Optional numeric fields, if present. - const optionalIntFields = ["tasks_in_progress", "tasks_blocked"]; - for (const field of optionalIntFields) { - const value = obj[field]; - if (value !== undefined && value !== null) { - if (typeof value !== "number") { - throw new Error(`Metrics snapshot '${field}' must be a number when present, got ${typeof value} (value: ${JSON.stringify(value)}): ${relPath}`); - } - if (!Number.isInteger(value)) { - throw new Error(`Metrics snapshot '${field}' must be an integer when present, got ${value}: ${relPath}`); - } - if (value < 0) { - throw new Error(`Metrics snapshot '${field}' must be non-negative when present, got ${value}: ${relPath}`); - } - } - } - if (obj.velocity_per_day !== undefined && obj.velocity_per_day !== null) { - const value = obj.velocity_per_day; - if (typeof value !== "number") { - throw new Error(`Metrics snapshot 'velocity_per_day' must be a number when present, got ${typeof value} (value: ${JSON.stringify(value)}): ${relPath}`); - } - if (value < 0) { - throw new Error(`Metrics snapshot 'velocity_per_day' must be non-negative when present, got ${value}: ${relPath}`); - } - } - if (obj.estimated_completion !== undefined && obj.estimated_completion !== null) { - const value = obj.estimated_completion; - if (typeof value !== "string") { - throw new Error(`Metrics snapshot 'estimated_completion' must be a string when present, got ${typeof value} (value: ${JSON.stringify(value)}): ${relPath}`); - } - } - } - - it("should provide clear error message when tasks_total is null", () => { - const metricsSnapshot = { - date: "2025-12-31", - campaign_id: "test-campaign", - tasks_total: null, - tasks_completed: 5, - }; - - expect(() => { - validateCampaignMetricsSnapshot(metricsSnapshot, "test-campaign", "test-campaign/metrics/2025-12-31.json"); - }).toThrow("Metrics snapshot 'tasks_total' is required but was null: test-campaign/metrics/2025-12-31.json"); - }); - - it("should provide clear error message when tasks_total is undefined", () => { - const metricsSnapshot = { - date: "2025-12-31", - campaign_id: "test-campaign", - tasks_completed: 5, - }; - - expect(() => { - validateCampaignMetricsSnapshot(metricsSnapshot, "test-campaign", "test-campaign/metrics/2025-12-31.json"); - }).toThrow("Metrics snapshot 'tasks_total' is required but was undefined: test-campaign/metrics/2025-12-31.json"); - }); - - it("should provide clear error message when tasks_total is a string", () => { - const metricsSnapshot = { - date: "2025-12-31", - campaign_id: "test-campaign", - tasks_total: "10", - tasks_completed: 5, - }; - - expect(() => { - validateCampaignMetricsSnapshot(metricsSnapshot, "test-campaign", "test-campaign/metrics/2025-12-31.json"); - }).toThrow("Metrics snapshot 'tasks_total' must be a number, got string (value: \"10\"): test-campaign/metrics/2025-12-31.json"); - }); - - it("should provide clear error message when tasks_total is a float", () => { - const metricsSnapshot = { - date: "2025-12-31", - campaign_id: "test-campaign", - tasks_total: 10.5, - tasks_completed: 5, - }; - - expect(() => { - validateCampaignMetricsSnapshot(metricsSnapshot, "test-campaign", "test-campaign/metrics/2025-12-31.json"); - }).toThrow("Metrics snapshot 'tasks_total' must be an integer, got 10.5: test-campaign/metrics/2025-12-31.json"); - }); - - it("should provide clear error message when tasks_total is negative", () => { - const metricsSnapshot = { - date: "2025-12-31", - campaign_id: "test-campaign", - tasks_total: -5, - tasks_completed: 5, - }; - - expect(() => { - validateCampaignMetricsSnapshot(metricsSnapshot, "test-campaign", "test-campaign/metrics/2025-12-31.json"); - }).toThrow("Metrics snapshot 'tasks_total' must be non-negative, got -5: test-campaign/metrics/2025-12-31.json"); - }); - - it("should accept valid integer values for required fields", () => { - const metricsSnapshot = { - date: "2025-12-31", - campaign_id: "test-campaign", - tasks_total: 10, - tasks_completed: 5, - }; - - expect(() => { - validateCampaignMetricsSnapshot(metricsSnapshot, "test-campaign", "test-campaign/metrics/2025-12-31.json"); - }).not.toThrow(); - }); - - it("should accept zero for required integer fields", () => { - const metricsSnapshot = { - date: "2025-12-31", - campaign_id: "test-campaign", - tasks_total: 0, - tasks_completed: 0, - }; - - expect(() => { - validateCampaignMetricsSnapshot(metricsSnapshot, "test-campaign", "test-campaign/metrics/2025-12-31.json"); - }).not.toThrow(); - }); - - it("should allow optional fields to be undefined", () => { - const metricsSnapshot = { - date: "2025-12-31", - campaign_id: "test-campaign", - tasks_total: 10, - tasks_completed: 5, - // tasks_in_progress, tasks_blocked, velocity_per_day, estimated_completion are undefined - }; - - expect(() => { - validateCampaignMetricsSnapshot(metricsSnapshot, "test-campaign", "test-campaign/metrics/2025-12-31.json"); - }).not.toThrow(); - }); - - it("should allow optional fields to be null", () => { - const metricsSnapshot = { - date: "2025-12-31", - campaign_id: "test-campaign", - tasks_total: 10, - tasks_completed: 5, - tasks_in_progress: null, - tasks_blocked: null, - velocity_per_day: null, - estimated_completion: null, - }; - - expect(() => { - validateCampaignMetricsSnapshot(metricsSnapshot, "test-campaign", "test-campaign/metrics/2025-12-31.json"); - }).not.toThrow(); - }); - - it("should validate optional integer fields when present", () => { - const metricsSnapshot = { - date: "2025-12-31", - campaign_id: "test-campaign", - tasks_total: 10, - tasks_completed: 5, - tasks_in_progress: "3", // Invalid: string instead of number - }; - - expect(() => { - validateCampaignMetricsSnapshot(metricsSnapshot, "test-campaign", "test-campaign/metrics/2025-12-31.json"); - }).toThrow("Metrics snapshot 'tasks_in_progress' must be a number when present, got string (value: \"3\"): test-campaign/metrics/2025-12-31.json"); - }); - - it("should validate optional float fields when present", () => { - const metricsSnapshot = { - date: "2025-12-31", - campaign_id: "test-campaign", - tasks_total: 10, - tasks_completed: 5, - velocity_per_day: "2.5", // Invalid: string instead of number - }; - - expect(() => { - validateCampaignMetricsSnapshot(metricsSnapshot, "test-campaign", "test-campaign/metrics/2025-12-31.json"); - }).toThrow("Metrics snapshot 'velocity_per_day' must be a number when present, got string (value: \"2.5\"): test-campaign/metrics/2025-12-31.json"); - }); - - it("should accept valid optional fields", () => { - const metricsSnapshot = { - date: "2025-12-31", - campaign_id: "test-campaign", - tasks_total: 10, - tasks_completed: 5, - tasks_in_progress: 3, - tasks_blocked: 1, - velocity_per_day: 2.5, - estimated_completion: "2026-01-15", - }; - - expect(() => { - validateCampaignMetricsSnapshot(metricsSnapshot, "test-campaign", "test-campaign/metrics/2025-12-31.json"); - }).not.toThrow(); - }); - - it("should validate tasks_completed field with same rigor as tasks_total", () => { - const metricsSnapshot = { - date: "2025-12-31", - campaign_id: "test-campaign", - tasks_total: 10, - tasks_completed: "5", // Invalid: string - }; - - expect(() => { - validateCampaignMetricsSnapshot(metricsSnapshot, "test-campaign", "test-campaign/metrics/2025-12-31.json"); - }).toThrow("Metrics snapshot 'tasks_completed' must be a number, got string (value: \"5\"): test-campaign/metrics/2025-12-31.json"); - }); - }); }); diff --git a/actions/setup/js/safe_output_handler_manager.cjs b/actions/setup/js/safe_output_handler_manager.cjs index 379647f3a7..de615d7f8d 100644 --- a/actions/setup/js/safe_output_handler_manager.cjs +++ b/actions/setup/js/safe_output_handler_manager.cjs @@ -17,100 +17,6 @@ const { setCollectedMissings } = require("./missing_messages_helper.cjs"); const { writeSafeOutputSummaries } = require("./safe_output_summary.cjs"); const { getIssuesToAssignCopilot } = require("./create_issue.cjs"); -const DEFAULT_AGENTIC_CAMPAIGN_LABEL = "agentic-campaign"; - -/** - * Normalize campaign IDs to the same label format used by campaign discovery. - * Mirrors actions/setup/js/campaign_discovery.cjs. - * @param {string} campaignId - * @returns {string} - */ -function formatCampaignLabel(campaignId) { - return `z_campaign_${String(campaignId) - .toLowerCase() - .replace(/[_\s]+/g, "-")}`; -} - -/** - * 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(); - - if (!campaignId) { - return { enabled: false, labels: [] }; - } - - // Only use the new z_campaign_ format, no legacy support - const labels = [DEFAULT_AGENTIC_CAMPAIGN_LABEL, formatCampaignLabel(campaignId)]; - - return { enabled: true, labels }; -} - -/** - * Merge labels with trimming + case-insensitive de-duplication. - * @param {string[]|undefined} existing - * @param {string[]} extra - * @returns {string[]} - */ -function mergeLabels(existing, extra) { - const out = []; - const seen = new Set(); - - for (const raw of [...(existing || []), ...(extra || [])]) { - const label = String(raw || "").trim(); - if (!label) { - continue; - } - - const key = label.toLowerCase(); - if (seen.has(key)) { - continue; - } - - seen.add(key); - out.push(label); - } - - return out; -} - -/** - * Apply campaign labels to supported output messages. - * This keeps worker output labeling centralized and avoids coupling campaign logic - * into individual safe output handlers. - * - * @param {any} message - * @param {{enabled: boolean, labels: string[]}} campaignLabels - * @returns {any} - */ -function applyCampaignLabelsToMessage(message, campaignLabels) { - if (!campaignLabels.enabled) { - return message; - } - - if (!message || typeof message !== "object") { - return message; - } - - const type = message.type; - if (type !== "create_issue" && type !== "create_pull_request") { - return message; - } - - const existing = Array.isArray(message.labels) ? message.labels : []; - const merged = mergeLabels(existing, campaignLabels.labels); - - // Avoid cloning unless we actually need to mutate - if (merged.length === existing.length && merged.every((v, i) => v === existing[i])) { - return message; - } - - return { ...message, labels: merged }; -} - /** * Handler map configuration * Maps safe output types to their handler module file paths @@ -288,9 +194,6 @@ function collectMissingMessages(messages) { async function processMessages(messageHandlers, messages) { const results = []; - // Campaign context: when present, always label created issues/PRs for discovery. - const campaignLabels = getCampaignLabelsFromEnv(); - // Collect missing_tool and missing_data messages first const missings = collectMissingMessages(messages); @@ -313,7 +216,7 @@ async function processMessages(messageHandlers, messages) { // Process messages in order of appearance for (let i = 0; i < messages.length; i++) { - const message = applyCampaignLabelsToMessage(messages[i], campaignLabels); + const message = messages[i]; const messageType = message.type; if (!messageType) { diff --git a/actions/setup/js/safe_output_handler_manager.test.cjs b/actions/setup/js/safe_output_handler_manager.test.cjs index ea26caf1c9..d6273b15de 100644 --- a/actions/setup/js/safe_output_handler_manager.test.cjs +++ b/actions/setup/js/safe_output_handler_manager.test.cjs @@ -19,7 +19,6 @@ describe("Safe Output Handler Manager", () => { afterEach(() => { // Clean up environment variables delete process.env.GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG; - delete process.env.GH_AW_CAMPAIGN_ID; delete process.env.GH_AW_TRACKER_LABEL; }); @@ -114,33 +113,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"; - - const messages = [ - { type: "create_issue", title: "Issue", labels: ["Bug"] }, - { type: "create_pull_request", title: "PR", labels: ["Bug", "agentic-campaign"] }, - ]; - - const handler = vi.fn().mockImplementation(async message => { - expect(Array.isArray(message.labels)).toBe(true); - expect(message.labels).toContain("agentic-campaign"); - expect(message.labels).toContain("z_campaign_security-alert-burndown"); - return { success: true }; - }); - - const handlers = new Map([ - ["create_issue", handler], - ["create_pull_request", handler], - ]); - - const result = await processMessages(handlers, messages); - - expect(result.success).toBe(true); - expect(result.results).toHaveLength(2); - expect(handler).toHaveBeenCalledTimes(2); - }); - it("should process messages in order of appearance", async () => { const messages = [ { type: "add_comment", body: "Comment" }, diff --git a/actions/setup/js/safe_output_unified_handler_manager.cjs b/actions/setup/js/safe_output_unified_handler_manager.cjs index 1d2b79fec9..b812c77660 100644 --- a/actions/setup/js/safe_output_unified_handler_manager.cjs +++ b/actions/setup/js/safe_output_unified_handler_manager.cjs @@ -21,72 +21,9 @@ const { generateMissingInfoSections } = require("./missing_info_formatter.cjs"); const { setCollectedMissings } = require("./missing_messages_helper.cjs"); const { writeSafeOutputSummaries } = require("./safe_output_summary.cjs"); const { getIssuesToAssignCopilot } = require("./create_issue.cjs"); -const { getCampaignLabelsFromEnv } = require("./campaign_labels.cjs"); const { sortSafeOutputMessages } = require("./safe_output_topological_sort.cjs"); const { loadCustomSafeOutputJobTypes } = require("./safe_output_helpers.cjs"); -/** - * Merge labels with trimming + case-insensitive de-duplication. - * @param {string[]|undefined} existing - * @param {string[]} extra - * @returns {string[]} - */ -function mergeLabels(existing, extra) { - const out = []; - const seen = new Set(); - - for (const raw of [...(existing || []), ...(extra || [])]) { - const label = String(raw || "").trim(); - if (!label) { - continue; - } - - const key = label.toLowerCase(); - if (seen.has(key)) { - continue; - } - - seen.add(key); - out.push(label); - } - - return out; -} - -/** - * Apply campaign labels to supported output messages. - * This keeps worker output labeling centralized and avoids coupling campaign logic - * into individual safe output handlers. - * - * @param {any} message - * @param {{enabled: boolean, labels: string[]}} campaignLabels - * @returns {any} - */ -function applyCampaignLabelsToMessage(message, campaignLabels) { - if (!campaignLabels.enabled) { - return message; - } - - if (!message || typeof message !== "object") { - return message; - } - - const type = message.type; - if (type !== "create_issue" && type !== "create_pull_request") { - return message; - } - - const existing = Array.isArray(message.labels) ? message.labels : []; - const merged = mergeLabels(existing, campaignLabels.labels); - - // Avoid cloning unless we actually need to mutate - if (merged.length === existing.length && merged.every((v, i) => v === existing[i])) { - return message; - } - - return { ...message, labels: merged }; -} - /** * Handler map configuration for regular handlers * Maps safe output types to their handler module file paths @@ -386,9 +323,6 @@ function collectMissingMessages(messages) { async function processMessages(messageHandlers, messages, projectOctokit = null) { const results = []; - // Campaign context: when present, always label created issues/PRs for discovery. - const campaignLabels = getCampaignLabelsFromEnv(); - // Collect missing_tool and missing_data messages first const missings = collectMissingMessages(messages); @@ -429,7 +363,7 @@ async function processMessages(messageHandlers, messages, projectOctokit = null) // Process messages in topologically sorted order for (let i = 0; i < sortedMessages.length; i++) { - const message = applyCampaignLabelsToMessage(sortedMessages[i], campaignLabels); + const message = sortedMessages[i]; const messageType = message.type; if (!messageType) { diff --git a/actions/setup/js/safe_outputs_tools.json b/actions/setup/js/safe_outputs_tools.json index 8239da81e6..fd8538af7d 100644 --- a/actions/setup/js/safe_outputs_tools.json +++ b/actions/setup/js/safe_outputs_tools.json @@ -600,7 +600,7 @@ }, { "name": "update_project", - "description": "Add or update items in GitHub Projects v2 boards. Can add issues/PRs to a project and update custom field values. Requires the project URL, content type (issue or pull_request), and content number. Use campaign_id to group related items.\n\nThree usage modes:\n1. Add/update project item: Requires project + content_type. For 'issue' or 'pull_request', also requires content_number. For 'draft_issue', requires draft_title.\n2. Create project fields: Requires project + operation='create_fields' + field_definitions.\n3. Create project view: Requires project + operation='create_view' + view.", + "description": "Add or update items in GitHub Projects v2 boards. Can add issues/PRs to a project and update custom field values. Requires the project URL, content type (issue or pull_request), and content number.\n\nThree usage modes:\n1. Add/update project item: Requires project + content_type. For 'issue' or 'pull_request', also requires content_number. For 'draft_issue', requires draft_title.\n2. Create project fields: Requires project + operation='create_fields' + field_definitions.\n3. Create project view: Requires project + operation='create_view' + view.", "inputSchema": { "type": "object", "required": ["project"], @@ -613,7 +613,7 @@ "operation": { "type": "string", "enum": ["create_fields", "create_view"], - "description": "Optional operation mode. Use create_fields to create required campaign fields up-front, or create_view to add a project view. When omitted, the tool adds/updates project items." + "description": "Optional operation mode. Use create_fields to create required fields up-front, or create_view to add a project view. When omitted, the tool adds/updates project items." }, "content_type": { "type": "string", @@ -688,10 +688,6 @@ }, "additionalProperties": false }, - "campaign_id": { - "type": "string", - "description": "Campaign identifier to group related project items. Used to track items created by the same campaign or workflow run." - }, "create_if_missing": { "type": "boolean", "description": "Whether to create the project if it doesn't exist. Defaults to false. Requires projects:write permission when true." @@ -736,7 +732,7 @@ "properties": { "title": { "type": "string", - "description": "Title for the new project. Should be descriptive and unique within the owner's projects. If not provided, will be auto-generated using the title-prefix configuration (default: 'Campaign') as ': ' or ' #' based on the issue context." + "description": "Title for the new project. Should be descriptive and unique within the owner's projects. If not provided, will be auto-generated using the title-prefix configuration (default: 'Project') as ': ' or ' #' based on the issue context." }, "owner": { "type": "string", diff --git a/actions/setup/js/update_issue_campaign_generator.test.cjs b/actions/setup/js/update_issue_generator.test.cjs similarity index 83% rename from actions/setup/js/update_issue_campaign_generator.test.cjs rename to actions/setup/js/update_issue_generator.test.cjs index 69714e3833..1e1f670b8a 100644 --- a/actions/setup/js/update_issue_campaign_generator.test.cjs +++ b/actions/setup/js/update_issue_generator.test.cjs @@ -18,7 +18,7 @@ const mockCore = { // Set up global mocks expected by the scripts we import global.core = mockCore; -describe("update_issue.cjs - campaign generator payload", () => { +describe("update_issue.cjs - generator payload", () => { beforeEach(async () => { vi.clearAllMocks(); vi.resetModules(); @@ -29,8 +29,8 @@ describe("update_issue.cjs - campaign generator payload", () => { const { success, data } = updateIssueModule.buildIssueUpdateData( { - // This is representative of how the campaign generator should update the triggering issue. - body: "## Campaign setup status\n\n**Status:** Ready for PR review\n\nDocs: https://github.github.com/gh-aw/guides/campaigns/getting-started/\n", + // Representative of a generator updating the triggering issue. + body: "## Run status\n\n**Status:** Ready for review\n", }, {} ); @@ -39,7 +39,7 @@ describe("update_issue.cjs - campaign generator payload", () => { // The handler should keep the raw body + operation so it can append + add footer. expect(data._operation).toBe("append"); - expect(data._rawBody).toContain("## Campaign setup status"); + expect(data._rawBody).toContain("## Run status"); // The actual API body is computed later (after fetching current issue body). expect(data.body).toBeUndefined(); diff --git a/actions/setup/js/update_project.cjs b/actions/setup/js/update_project.cjs index c33dec19f5..c38c820fef 100644 --- a/actions/setup/js/update_project.cjs +++ b/actions/setup/js/update_project.cjs @@ -5,26 +5,6 @@ const { loadAgentOutput } = require("./load_agent_output.cjs"); const { getErrorMessage } = require("./error_helpers.cjs"); const { loadTemporaryIdMap, resolveIssueNumber } = require("./temporary_id.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 @@ -266,23 +246,6 @@ async function resolveProjectV2(projectInfo, projectNumberInt, github) { throw new Error(`Project #${projectNumberInt} not found or not accessible for ${who}.${total} Accessible Projects v2: ${summary}`); } -/** - * Generate a campaign ID for the project - * @param {string} projectUrl - Project URL - * @param {string} projectNumber - Project number - * @returns {string} Campaign ID - */ -function generateCampaignId(projectUrl, projectNumber) { - const urlMatch = projectUrl.match(/github\.com\/(users|orgs)\/([^/]+)\/projects/); - const base = `${urlMatch ? urlMatch[2] : "project"}-project-${projectNumber}` - .toLowerCase() - .replace(/[^a-z0-9]+/g, "-") - .replace(/^-+|-+$/g, "") - .substring(0, 30); - const timestamp = Date.now().toString(36).substring(0, 8); - return `${base}-${timestamp}`; -} - /** * Check if a field name conflicts with unsupported GitHub built-in field types * @param {string} fieldName - Original field name @@ -362,7 +325,6 @@ async function updateProject(output, temporaryIdMap = new Map(), githubClient = const { owner, repo } = context.repo; const projectInfo = parseProjectUrl(output.project); const projectNumberFromUrl = projectInfo.projectNumber; - const campaignId = output.campaign_id; const wantsCreateView = output?.operation === "create_view" || @@ -695,7 +657,7 @@ async function updateProject(output, temporaryIdMap = new Map(), githubClient = // Detect expected field type based on field name and value heuristics const datePattern = /^\d{4}-\d{2}-\d{2}$/; const isDateField = fieldName.toLowerCase().includes("_date") || fieldName.toLowerCase().includes("date"); - const isTextField = "classification" === fieldName.toLowerCase() || "campaign_id" === fieldName.toLowerCase() || ("string" == typeof fieldValue && fieldValue.includes("|")); + const isTextField = "classification" === fieldName.toLowerCase() || ("string" == typeof fieldValue && fieldValue.includes("|")); let expectedDataType; if (isDateField && typeof fieldValue === "string" && datePattern.test(fieldValue)) { expectedDataType = "DATE"; @@ -730,7 +692,7 @@ async function updateProject(output, temporaryIdMap = new Map(), githubClient = core.warning(`Field "${fieldName}" looks like a date field but value "${fieldValue}" is not in YYYY-MM-DD format. Skipping field creation.`); continue; } - } else if ("classification" === fieldName.toLowerCase() || "campaign_id" === fieldName.toLowerCase() || ("string" == typeof fieldValue && fieldValue.includes("|"))) + } else if ("classification" === fieldName.toLowerCase() || ("string" == typeof fieldValue && fieldValue.includes("|"))) try { field = ( await github.graphql( @@ -859,13 +821,6 @@ async function updateProject(output, temporaryIdMap = new Map(), githubClient = { projectId, contentId } ) ).addProjectV2ItemById.item.id; - if (campaignId) { - try { - await github.rest.issues.addLabels({ owner, repo, issue_number: contentNumber, labels: [formatCampaignLabel(campaignId)] }); - } catch (labelError) { - core.warning(`Failed to add campaign label: ${getErrorMessage(labelError)}`); - } - } } const fieldsToUpdate = output.fields ? { ...output.fields } : {}; if (Object.keys(fieldsToUpdate).length > 0) { @@ -891,7 +846,7 @@ async function updateProject(output, temporaryIdMap = new Map(), githubClient = // Detect expected field type based on field name and value heuristics const datePattern = /^\d{4}-\d{2}-\d{2}$/; const isDateField = fieldName.toLowerCase().includes("_date") || fieldName.toLowerCase().includes("date"); - const isTextField = "classification" === fieldName.toLowerCase() || "campaign_id" === fieldName.toLowerCase() || ("string" == typeof fieldValue && fieldValue.includes("|")); + const isTextField = "classification" === fieldName.toLowerCase() || ("string" == typeof fieldValue && fieldValue.includes("|")); let expectedDataType; if (isDateField && typeof fieldValue === "string" && datePattern.test(fieldValue)) { expectedDataType = "DATE"; @@ -926,7 +881,7 @@ async function updateProject(output, temporaryIdMap = new Map(), githubClient = core.warning(`Field "${fieldName}" looks like a date field but value "${fieldValue}" is not in YYYY-MM-DD format. Skipping field creation.`); continue; } - } else if ("classification" === fieldName.toLowerCase() || "campaign_id" === fieldName.toLowerCase() || ("string" == typeof fieldValue && fieldValue.includes("|"))) + } else if ("classification" === fieldName.toLowerCase() || ("string" == typeof fieldValue && fieldValue.includes("|"))) try { field = ( await github.graphql( @@ -1133,7 +1088,7 @@ async function main(config = {}, githubClient = null) { } // Create configured fields once before processing the first message - // This ensures campaign-required fields exist even if the agent doesn't explicitly emit operation=create_fields. + // This ensures configured fields exist even if the agent doesn't explicitly emit operation=create_fields. if (!fieldsCreated && configuredFieldDefinitions.length > 0 && firstProjectUrl) { const operation = typeof resolvedMessage?.operation === "string" ? resolvedMessage.operation : ""; if (operation !== "create_fields") { @@ -1217,4 +1172,4 @@ async function main(config = {}, githubClient = null) { }; } -module.exports = { updateProject, parseProjectInput, generateCampaignId, main }; +module.exports = { updateProject, parseProjectInput, main }; diff --git a/actions/setup/js/update_project.test.cjs b/actions/setup/js/update_project.test.cjs index 0a274ab498..305b7366e4 100644 --- a/actions/setup/js/update_project.test.cjs +++ b/actions/setup/js/update_project.test.cjs @@ -2,7 +2,6 @@ import { describe, it, expect, beforeAll, beforeEach, afterEach, vi } from "vite let updateProject; let parseProjectInput; -let generateCampaignId; let updateProjectHandlerFactory; const mockCore = { @@ -53,7 +52,6 @@ beforeAll(async () => { const exports = mod.default || mod; updateProject = exports.updateProject; parseProjectInput = exports.parseProjectInput; - generateCampaignId = exports.generateCampaignId; updateProjectHandlerFactory = exports.main; // Call main to execute the module if (exports.main) { @@ -102,7 +100,7 @@ describe("update_project handler config: field_definitions", () => { const handler = await updateProjectHandlerFactory({ max: 10, - field_definitions: [{ name: "campaign_id", data_type: "TEXT" }], + field_definitions: [{ name: "classification", data_type: "TEXT" }], }); await handler( @@ -298,15 +296,6 @@ describe("parseProjectInput", () => { }); }); -describe("generateCampaignId", () => { - it("builds a slug with a timestamp suffix", () => { - const nowSpy = vi.spyOn(Date, "now").mockReturnValue(1734470400000); - const id = generateCampaignId("https://github.com/orgs/acme/projects/42", "42"); - expect(id).toBe("acme-project-42-m4syw5xc"); - nowSpy.mockRestore(); - }); -}); - describe("updateProject", () => { it("creates a view for an org-owned project", async () => { const projectUrl = "https://github.com/orgs/testowner/projects/60"; @@ -415,25 +404,6 @@ describe("updateProject", () => { await expect(updateProject(output)).rejects.toThrow(/not found or not accessible/); }); - it("respects a custom campaign id", async () => { - const projectUrl = "https://github.com/orgs/testowner/projects/60"; - const output = { - type: "update_project", - project: projectUrl, - campaign_id: "custom-id-2025", - content_type: "issue", - content_number: 42, - }; - - queueResponses([repoResponse(), viewerResponse(), orgProjectV2Response(projectUrl, 60, "project456"), issueResponse("issue-id-42"), emptyItemsResponse(), { addProjectV2ItemById: { item: { id: "item-custom" } } }]); - - await updateProject(output); - - const labelCall = mockGithub.rest.issues.addLabels.mock.calls[0][0]; - expect(labelCall.labels).toEqual(["z_campaign_custom-id-2025"]); - expect(getOutput("item-id")).toBe("item-custom"); - }); - it("adds an issue to a project board", async () => { const projectUrl = "https://github.com/orgs/testowner/projects/60"; const output = { type: "update_project", project: projectUrl, content_type: "issue", content_number: 42 }; @@ -442,31 +412,11 @@ describe("updateProject", () => { await updateProject(output); - // No campaign label should be added when campaign_id is not provided + // update_project no longer adds labels as a side effect expect(mockGithub.rest.issues.addLabels).not.toHaveBeenCalled(); expect(getOutput("item-id")).toBe("item123"); }); - it("adds an issue to a project board with campaign label when campaign_id provided", async () => { - const projectUrl = "https://github.com/orgs/testowner/projects/60"; - const output = { type: "update_project", project: projectUrl, content_type: "issue", content_number: 42, campaign_id: "my-campaign" }; - - queueResponses([repoResponse(), viewerResponse(), orgProjectV2Response(projectUrl, 60, "project123"), issueResponse("issue-id-42"), emptyItemsResponse(), { addProjectV2ItemById: { item: { id: "item123" } } }]); - - await updateProject(output); - - const labelCall = mockGithub.rest.issues.addLabels.mock.calls[0][0]; - expect(labelCall).toEqual( - expect.objectContaining({ - owner: "testowner", - repo: "testrepo", - issue_number: 42, - }) - ); - expect(labelCall.labels).toEqual(["z_campaign_my-campaign"]); - expect(getOutput("item-id")).toBe("item123"); - }); - it("adds a draft issue to a project board", async () => { const projectUrl = "https://github.com/orgs/testowner/projects/60"; const output = { @@ -521,7 +471,7 @@ describe("updateProject", () => { await updateProject(output); - // No campaign label should be added when campaign_id is not provided + // update_project no longer adds labels as a side effect expect(mockGithub.rest.issues.addLabels).not.toHaveBeenCalled(); }); @@ -535,7 +485,7 @@ describe("updateProject", () => { expect(mockCore.warning).toHaveBeenCalledWith('Field "issue" deprecated; use "content_number" instead.'); - // No campaign label should be added when campaign_id is not provided + // update_project no longer adds labels as a side effect expect(mockGithub.rest.issues.addLabels).not.toHaveBeenCalled(); expect(getOutput("item-id")).toBe("legacy-item"); }); @@ -743,39 +693,11 @@ describe("updateProject", () => { expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining('Failed to create field "NonExistentField"')); }); - it("warns when adding the campaign label fails", async () => { - const projectUrl = "https://github.com/orgs/testowner/projects/60"; - const output = { type: "update_project", project: projectUrl, content_type: "issue", content_number: 50, campaign_id: "test-campaign" }; - - queueResponses([repoResponse(), viewerResponse(), orgProjectV2Response(projectUrl, 60, "project-label"), issueResponse("issue-id-50"), emptyItemsResponse(), { addProjectV2ItemById: { item: { id: "item-label" } } }]); - - mockGithub.rest.issues.addLabels.mockRejectedValueOnce(new Error("Labels disabled")); - - await updateProject(output); - - expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining("Failed to add campaign label")); - }); - it("rejects non-URL project identifier", async () => { - const output = { type: "update_project", project: "My Campaign", campaign_id: "my-campaign-123" }; + const output = { type: "update_project", project: "Engineering Roadmap" }; await expect(updateProject(output)).rejects.toThrow(/full GitHub project URL/); }); - it("accepts URL project identifier when campaign_id is present", async () => { - const projectUrl = "https://github.com/orgs/testowner/projects/60"; - const output = { - type: "update_project", - project: projectUrl, - campaign_id: "my-campaign-123", - }; - - queueResponses([repoResponse(), viewerResponse(), orgProjectV2Response(projectUrl, 60, "project123")]); - - await updateProject(output); - - expect(mockCore.error).not.toHaveBeenCalled(); - }); - it("correctly identifies DATE fields and uses date format (not singleSelectOptionId)", async () => { const projectUrl = "https://github.com/orgs/testowner/projects/60"; const output = { @@ -1211,7 +1133,7 @@ describe("updateProject", () => { expect(updateFieldCall).toBeUndefined(); }); - it("creates campaign_id field as TEXT type (not SINGLE_SELECT)", async () => { + it("creates classification field as TEXT type (not SINGLE_SELECT)", async () => { const projectUrl = "https://github.com/orgs/testowner/projects/60"; const output = { type: "update_project", @@ -1219,24 +1141,24 @@ describe("updateProject", () => { content_type: "issue", content_number: 100, fields: { - campaign_id: "my-campaign-123", + classification: "high", }, }; queueResponses([ repoResponse(), viewerResponse(), - orgProjectV2Response(projectUrl, 60, "project-campaign-id"), + orgProjectV2Response(projectUrl, 60, "project-id-60"), issueResponse("issue-id-100"), - existingItemResponse("issue-id-100", "item-campaign-id"), - // No existing fields - will need to create campaign_id as TEXT + existingItemResponse("issue-id-100", "item-id-100"), + // No existing fields - will need to create Classification as TEXT fieldsResponse([]), - // Response for creating campaign_id field as TEXT type (not SINGLE_SELECT) + // Response for creating Classification field as TEXT type { createProjectV2Field: { projectV2Field: { - id: "field-campaign-id", - name: "Campaign Id", + id: "field-id-classification", + name: "Classification", }, }, }, @@ -1245,20 +1167,20 @@ describe("updateProject", () => { await updateProject(output); - // Verify that campaign_id field was created with TEXT type (not SINGLE_SELECT) + // Verify that field was created with TEXT type (not SINGLE_SELECT) const createCalls = mockGithub.graphql.mock.calls.filter(([query]) => query.includes("createProjectV2Field")); expect(createCalls.length).toBe(1); // Check that the field was created with TEXT dataType expect(createCalls[0][1].dataType).toBe("TEXT"); - expect(createCalls[0][1].name).toBe("Campaign Id"); + expect(createCalls[0][1].name).toBe("Classification"); // Verify that singleSelectOptions was NOT provided (which would indicate SINGLE_SELECT) expect(createCalls[0][1].singleSelectOptions).toBeUndefined(); // Verify the field value was set using text format const updateCalls = mockGithub.graphql.mock.calls.filter(([query]) => query.includes("updateProjectV2ItemFieldValue")); expect(updateCalls.length).toBe(1); - expect(updateCalls[0][1].value).toEqual({ text: "my-campaign-123" }); + expect(updateCalls[0][1].value).toEqual({ text: "high" }); }); it("should reject update_project message with missing project field", async () => { diff --git a/docs/src/content/docs/reference/frontmatter-full.md b/docs/src/content/docs/reference/frontmatter-full.md index 004d5ad6e0..1387af92f1 100644 --- a/docs/src/content/docs/reference/frontmatter-full.md +++ b/docs/src/content/docs/reference/frontmatter-full.md @@ -1671,11 +1671,6 @@ tools: # (optional) create-orphan: true - # Campaign ID for campaign-specific repo-memory (optional, used to correlate - # memory with campaign workflows) - # (optional) - campaign-id: "example-value" - # Option 4: Array of repo-memory configurations for multiple memory locations repo-memory: [] # Array items: object @@ -1934,11 +1929,10 @@ safe-outputs: # github-token override). NOTE: Projects v2 requires a Personal Access Token (PAT) # or GitHub App token with appropriate permissions; the GITHUB_TOKEN cannot be # used for Projects v2. Safe output items produced by the agent use - # type=update_project Configuration also supports an optional views array for + # type=update_project. Configuration also supports an optional views array for # declaring project views to create. Safe output items produced by the agent use # type=update_project and may include: project (board name), content_type - # (issue|pull_request), content_number, fields, campaign_id, and - # create_if_missing. + # (issue|pull_request), content_number, fields, and create_if_missing. update-project: # Maximum number of project operations to perform (default: 10). Each operation # may add a project item, or update its fields. @@ -1961,7 +1955,7 @@ safe-outputs: # (optional) views: [] # Array items: - # The name of the view (e.g., 'Sprint Board', 'Campaign Roadmap') + # The name of the view (e.g., 'Sprint Board', 'Roadmap') name: "My Workflow" # The layout type of the view @@ -1981,12 +1975,11 @@ safe-outputs: # (optional) description: "Description of the workflow" - # Optional array of project custom fields to create up-front. Useful for campaign - # projects that require a fixed set of fields. + # Optional array of project custom fields to create up-front. # (optional) field-definitions: [] # Array items: - # The field name to create (e.g., 'status', 'campaign_id') + # The field name to create (e.g., 'status', 'priority') name: "My Workflow" # The GitHub Projects v2 custom field type @@ -2030,8 +2023,8 @@ safe-outputs: # (optional) target-owner: "example-value" - # Optional prefix for auto-generated project titles (default: 'Campaign'). When - # the agent doesn't provide a title, the project title is auto-generated as + # Optional prefix for auto-generated project titles (default: 'Project'). When the + # agent doesn't provide a title, the project title is auto-generated as # ': ' or ' #' based on the # issue context. # (optional) @@ -2043,7 +2036,7 @@ safe-outputs: # (optional) views: [] # Array items: - # The name of the view (e.g., 'Sprint Board', 'Campaign Roadmap') + # The name of the view (e.g., 'Sprint Board', 'Roadmap') name: "My Workflow" # The layout type of the view @@ -2064,11 +2057,11 @@ safe-outputs: description: "Description of the workflow" # Optional array of project custom fields to create automatically after project - # creation. Useful for campaign projects that require a fixed set of fields. + # creation. # (optional) field-definitions: [] # Array items: - # The field name to create (e.g., 'Campaign Id', 'Priority') + # The field name to create (e.g., 'Priority', 'Classification') name: "My Workflow" # The GitHub Projects v2 custom field type @@ -2094,7 +2087,7 @@ safe-outputs: # progress. Requires a Personal Access Token (PAT) or GitHub App token with # Projects: Read+Write permission. The GITHUB_TOKEN cannot be used for Projects # v2. Status updates are created on the specified project board and appear in the - # Updates tab. Typically used by campaign orchestrators to post run summaries with + # Updates tab. Typically used by orchestrators to post run summaries with # progress, findings, and next steps. create-project-status-update: # Maximum number of status updates to create (default: 1). Typically 1 per diff --git a/pkg/cli/context_cancellation_test.go b/pkg/cli/context_cancellation_test.go index bb4b9df3ed..0076e15a12 100644 --- a/pkg/cli/context_cancellation_test.go +++ b/pkg/cli/context_cancellation_test.go @@ -71,7 +71,7 @@ func TestDownloadWorkflowLogsWithCancellation(t *testing.T) { cancel() // Try to download logs with a cancelled context - err := DownloadWorkflowLogs(ctx, "", 10, "", "", "/tmp/test-logs", "", "", 0, 0, "", false, false, false, false, false, false, false, 0, false, "", "") + err := DownloadWorkflowLogs(ctx, "", 10, "", "", "/tmp/test-logs", "", "", 0, 0, "", false, false, false, false, false, false, false, 0, "", "") // Should return context.Canceled error assert.ErrorIs(t, err, context.Canceled, "Should return context.Canceled error when context is cancelled") @@ -111,7 +111,7 @@ func TestDownloadWorkflowLogsTimeoutRespected(t *testing.T) { start := time.Now() // Use a workflow name that doesn't exist to avoid actual network calls - _ = DownloadWorkflowLogs(ctx, "nonexistent-workflow-12345", 100, "", "", "/tmp/test-logs", "", "", 0, 0, "", false, false, false, false, false, false, false, 1, false, "", "") + _ = DownloadWorkflowLogs(ctx, "nonexistent-workflow-12345", 100, "", "", "/tmp/test-logs", "", "", 0, 0, "", false, false, false, false, false, false, false, 1, "", "") elapsed := time.Since(start) // Should complete within reasonable time (give 5 seconds buffer for test overhead) diff --git a/pkg/cli/logs_ci_scenario_test.go b/pkg/cli/logs_ci_scenario_test.go index 4def556896..6ab304abff 100644 --- a/pkg/cli/logs_ci_scenario_test.go +++ b/pkg/cli/logs_ci_scenario_test.go @@ -48,7 +48,6 @@ func TestLogsJSONOutputWithNoRuns(t *testing.T) { false, // parse true, // jsonOutput - THIS IS KEY 10, // timeout - false, // campaignOnly "summary.json", // summaryFile "", // safeOutputType ) diff --git a/pkg/cli/logs_command.go b/pkg/cli/logs_command.go index eb36fb4404..59f222a5c7 100644 --- a/pkg/cli/logs_command.go +++ b/pkg/cli/logs_command.go @@ -45,8 +45,8 @@ Downloaded artifacts include: - workflow-logs/: GitHub Actions workflow run logs (job logs organized in subdirectory) - summary.json: Complete metrics and run data for all downloaded runs -Campaign Orchestrator Usage: - In a campaign orchestrator workflow, use this command in a pre-step to download logs, +Orchestrator Usage: + In an orchestrator workflow, use this command in a pre-step to download logs, then access the data in subsequent steps without needing GitHub CLI access: steps: @@ -67,11 +67,11 @@ Campaign Orchestrator Usage: **DO NOT call 'gh aw logs' or any GitHub CLI commands** - they will not work in your environment. All data you need is in the summary.json file. - Live Tracking with Project Boards: - Use the summary.json data to update your campaign project board, treating issues/PRs (workers) - on the board as the real-time view of progress, ownership, and status. The orchestrator workflow - can use the 'update-project' safe output to sync status fields without modifying worker workflow - files. Workers remain unchanged while the campaign board reflects current execution state. + Live Tracking with Project Boards: + Use the summary.json data to update your project board, treating issues/PRs (workers) + on the board as the real-time view of progress, ownership, and status. The orchestrator workflow + can use the 'update-project' safe output to sync status fields without modifying worker workflow + files. Workers remain unchanged while the board reflects current execution state. For incremental updates, pull data for each worker based on the last pull time using --start-date (e.g., --start-date -1d for daily updates) and align with existing board items. Compare run data @@ -167,7 +167,6 @@ Examples: jsonOutput, _ := cmd.Flags().GetBool("json") timeout, _ := cmd.Flags().GetInt("timeout") repoOverride, _ := cmd.Flags().GetString("repo") - campaignOnly, _ := cmd.Flags().GetBool("campaign") summaryFile, _ := cmd.Flags().GetString("summary-file") safeOutputType, _ := cmd.Flags().GetString("safe-output") @@ -204,7 +203,7 @@ Examples: logsCommandLog.Printf("Executing logs download: workflow=%s, count=%d, engine=%s", workflowName, count, engine) - return DownloadWorkflowLogs(cmd.Context(), workflowName, count, startDate, endDate, outputDir, engine, ref, beforeRunID, afterRunID, repoOverride, verbose, toolGraph, noStaged, firewallOnly, noFirewall, parse, jsonOutput, timeout, campaignOnly, summaryFile, safeOutputType) + return DownloadWorkflowLogs(cmd.Context(), workflowName, count, startDate, endDate, outputDir, engine, ref, beforeRunID, afterRunID, repoOverride, verbose, toolGraph, noStaged, firewallOnly, noFirewall, parse, jsonOutput, timeout, summaryFile, safeOutputType) }, } @@ -222,7 +221,6 @@ Examples: logsCmd.Flags().Bool("no-staged", false, "Filter out staged workflow runs (exclude runs with staged: true in aw_info.json)") logsCmd.Flags().Bool("firewall", false, "Filter to only runs with firewall enabled") logsCmd.Flags().Bool("no-firewall", false, "Filter to only runs without firewall enabled") - logsCmd.Flags().Bool("campaign", false, "Filter to only campaign orchestrator workflows") logsCmd.Flags().String("safe-output", "", "Filter to runs containing a specific safe output type (e.g., create-issue, missing-tool, missing-data)") logsCmd.Flags().Bool("parse", false, "Run JavaScript parsers on agent logs and firewall logs, writing Markdown to log.md and firewall.md") addJSONFlag(logsCmd) diff --git a/pkg/cli/logs_command_test.go b/pkg/cli/logs_command_test.go index 25ca2ccae5..70398f04e2 100644 --- a/pkg/cli/logs_command_test.go +++ b/pkg/cli/logs_command_test.go @@ -248,7 +248,7 @@ func TestLogsCommandHelpText(t *testing.T) { // Verify long description contains expected sections expectedSections := []string{ "Download workflow run logs", - "Campaign Orchestrator Usage", + "Orchestrator Usage", "Examples:", "gh aw logs", } diff --git a/pkg/cli/logs_download_test.go b/pkg/cli/logs_download_test.go index 950a725d40..543db49cab 100644 --- a/pkg/cli/logs_download_test.go +++ b/pkg/cli/logs_download_test.go @@ -21,7 +21,7 @@ func TestDownloadWorkflowLogs(t *testing.T) { // Test the DownloadWorkflowLogs function // This should either fail with auth error (if not authenticated) // or succeed with no results (if authenticated but no workflows match) - err := DownloadWorkflowLogs(context.Background(), "", 1, "", "", "./test-logs", "", "", 0, 0, "", false, false, false, false, false, false, false, 0, false, "summary.json", "") + err := DownloadWorkflowLogs(context.Background(), "", 1, "", "", "./test-logs", "", "", 0, 0, "", false, false, false, false, false, false, false, 0, "summary.json", "") // If GitHub CLI is authenticated, the function may succeed but find no results // If not authenticated, it should return an auth error @@ -209,7 +209,7 @@ func TestDownloadWorkflowLogsWithEngineFilter(t *testing.T) { if !tt.expectError { // For valid engines, test that the function can be called without panic // It may still fail with auth errors, which is expected - err := DownloadWorkflowLogs(context.Background(), "", 1, "", "", "./test-logs", tt.engine, "", 0, 0, "", false, false, false, false, false, false, false, 0, false, "summary.json", "") + err := DownloadWorkflowLogs(context.Background(), "", 1, "", "", "./test-logs", tt.engine, "", 0, 0, "", false, false, false, false, false, false, false, 0, "summary.json", "") // Clean up any created directories os.RemoveAll("./test-logs") diff --git a/pkg/cli/logs_json_stderr_order_test.go b/pkg/cli/logs_json_stderr_order_test.go index 14f3b48351..f622b21772 100644 --- a/pkg/cli/logs_json_stderr_order_test.go +++ b/pkg/cli/logs_json_stderr_order_test.go @@ -56,7 +56,6 @@ func TestLogsJSONOutputBeforeStderr(t *testing.T) { false, // parse true, // jsonOutput - THIS IS KEY 10, // timeout - false, // campaignOnly "summary.json", // summaryFile "", // safeOutputType ) @@ -177,7 +176,6 @@ func TestLogsJSONAndStderrRedirected(t *testing.T) { false, true, // jsonOutput 10, - false, "summary.json", "", // safeOutputType ) diff --git a/pkg/cli/logs_orchestrator.go b/pkg/cli/logs_orchestrator.go index 4c166a1135..c55ecf2c5f 100644 --- a/pkg/cli/logs_orchestrator.go +++ b/pkg/cli/logs_orchestrator.go @@ -6,7 +6,7 @@ // - Coordinating the main download workflow (DownloadWorkflowLogs) // - Managing pagination and iteration through workflow runs // - Concurrent downloading of artifacts from multiple runs -// - Applying filters (engine, firewall, staged, campaign, etc.) +// - Applying filters (engine, firewall, staged, etc.) // - Building and rendering output (console, JSON, tool graphs) package cli @@ -40,8 +40,8 @@ func getMaxConcurrentDownloads() int { } // DownloadWorkflowLogs downloads and analyzes workflow logs with metrics -func DownloadWorkflowLogs(ctx context.Context, workflowName string, count int, startDate, endDate, outputDir, engine, ref string, beforeRunID, afterRunID int64, repoOverride string, verbose bool, toolGraph bool, noStaged bool, firewallOnly bool, noFirewall bool, parse bool, jsonOutput bool, timeout int, campaignOnly bool, summaryFile string, safeOutputType string) error { - logsOrchestratorLog.Printf("Starting workflow log download: workflow=%s, count=%d, startDate=%s, endDate=%s, outputDir=%s, campaignOnly=%v, summaryFile=%s, safeOutputType=%s", workflowName, count, startDate, endDate, outputDir, campaignOnly, summaryFile, safeOutputType) +func DownloadWorkflowLogs(ctx context.Context, workflowName string, count int, startDate, endDate, outputDir, engine, ref string, beforeRunID, afterRunID int64, repoOverride string, verbose bool, toolGraph bool, noStaged bool, firewallOnly bool, noFirewall bool, parse bool, jsonOutput bool, timeout int, summaryFile string, safeOutputType string) error { + logsOrchestratorLog.Printf("Starting workflow log download: workflow=%s, count=%d, startDate=%s, endDate=%s, outputDir=%s, summaryFile=%s, safeOutputType=%s", workflowName, count, startDate, endDate, outputDir, summaryFile, safeOutputType) // Check context cancellation at the start select { @@ -208,24 +208,10 @@ func DownloadWorkflowLogs(ctx context.Context, workflowName string, count int, s awInfoPath := filepath.Join(result.LogsPath, "aw_info.json") // Only parse if we need it for any filter - if engine != "" || noStaged || firewallOnly || noFirewall || campaignOnly { + if engine != "" || noStaged || firewallOnly || noFirewall { awInfo, awInfoErr = parseAwInfo(awInfoPath, verbose) } - // Apply campaign filtering if --campaign flag is specified - if campaignOnly { - // Campaign orchestrator workflows end with .campaign.lock.yml - isCampaign := strings.HasSuffix(result.Run.WorkflowName, " Campaign Orchestrator") || - strings.Contains(result.Run.WorkflowPath, ".campaign.lock.yml") - - if !isCampaign { - if verbose { - fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Skipping run %d: not a campaign orchestrator workflow", result.Run.DatabaseID))) - } - continue - } - } - // Apply engine filtering if specified if engine != "" { // Check if the run's engine matches the filter diff --git a/pkg/cli/project_command.go b/pkg/cli/project_command.go index f255f08ae5..93d48ca751 100644 --- a/pkg/cli/project_command.go +++ b/pkg/cli/project_command.go @@ -19,13 +19,13 @@ var projectLog = logger.New("cli:project") // ProjectConfig holds configuration for creating a GitHub Project type ProjectConfig struct { - Title string // Project title - Owner string // Owner login (user or org) - OwnerType string // "user" or "org" - Description string // Project description (note: not currently supported by GitHub Projects V2 API during creation) - Repo string // Repository to link project to (optional, format: owner/repo) - Verbose bool // Verbose output - WithCampaignSetup bool // Whether to create standard campaign views and fields + Title string // Project title + Owner string // Owner login (user or org) + OwnerType string // "user" or "org" + Description string // Project description (note: not currently supported by GitHub Projects V2 API during creation) + Repo string // Repository to link project to (optional, format: owner/repo) + Verbose bool // Verbose output + WithProjectSetup bool // Whether to create standard project views and fields } // NewProjectCommand creates the project command @@ -70,34 +70,34 @@ Token Requirements: Set GH_AW_PROJECT_GITHUB_TOKEN environment variable or configure your gh CLI with a token that has the required permissions. -Campaign Setup: - Use --with-campaign-setup to automatically create: - - Standard views (Progress Board, Task Tracker, Campaign Roadmap) - - Custom fields (Campaign Id, Worker Workflow, Target Repo, Priority, Size, dates) +Project Setup: + Use --with-project-setup to automatically create: + - Standard views (Progress Board, Task Tracker, Roadmap) + - Custom fields (Tracker Id, Worker Workflow, Target Repo, Priority, Size, dates) - Enhanced Status field with "Review Required" option Examples: gh aw project new "My Project" --owner @me # Create user project gh aw project new "Team Board" --owner myorg # Create org project gh aw project new "Bugs" --owner myorg --link myorg/myrepo # Create and link to repo - gh aw project new "Campaign Q1" --owner myorg --with-campaign-setup # With campaign setup`, + gh aw project new "Project Q1" --owner myorg --with-project-setup # With project setup`, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { owner, _ := cmd.Flags().GetString("owner") link, _ := cmd.Flags().GetString("link") verbose, _ := cmd.Flags().GetBool("verbose") - withCampaignSetup, _ := cmd.Flags().GetBool("with-campaign-setup") + withProjectSetup, _ := cmd.Flags().GetBool("with-project-setup") if owner == "" { return fmt.Errorf("--owner flag is required. Use '@me' for current user or specify org name") } config := ProjectConfig{ - Title: args[0], - Owner: owner, - Repo: link, - Verbose: verbose, - WithCampaignSetup: withCampaignSetup, + Title: args[0], + Owner: owner, + Repo: link, + Verbose: verbose, + WithProjectSetup: withProjectSetup, } return RunProjectNew(cmd.Context(), config) @@ -106,7 +106,7 @@ Examples: cmd.Flags().StringP("owner", "o", "", "Project owner: '@me' for current user or organization name (required)") cmd.Flags().StringP("link", "l", "", "Repository to link project to (format: owner/repo)") - cmd.Flags().Bool("with-campaign-setup", false, "Create standard campaign views and custom fields") + cmd.Flags().Bool("with-project-setup", false, "Create standard project views and custom fields") _ = cmd.MarkFlagRequired("owner") return cmd @@ -174,7 +174,7 @@ func RunProjectNew(ctx context.Context, config ProjectConfig) error { } projectNumber := int(projectNumberFloat) - if config.WithCampaignSetup { + if config.WithProjectSetup { fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Creating standard project views...")) if err := createStandardViews(ctx, projectURL, config.Verbose); err != nil { fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Warning: Failed to create views: %v", err))) @@ -190,7 +190,7 @@ func RunProjectNew(ctx context.Context, config ProjectConfig) error { } } - if config.WithCampaignSetup { + if config.WithProjectSetup { fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Enhancing Status field...")) if err := ensureStatusOption(ctx, projectURL, "Review Required", config.Verbose); err != nil { fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Warning: Failed to update Status field: %v", err))) @@ -432,7 +432,7 @@ func parseProjectURL(projectURL string) (projectURLInfo, error) { }, nil } -// createStandardViews creates the standard campaign views +// createStandardViews creates the standard project views func createStandardViews(ctx context.Context, projectURL string, verbose bool) error { projectLog.Print("Creating standard views") console.LogVerbose(verbose, "Creating standard project views...") @@ -448,7 +448,7 @@ func createStandardViews(ctx context.Context, projectURL string, verbose bool) e }{ {name: "Progress Board", layout: "board"}, {name: "Task Tracker", layout: "table"}, - {name: "Campaign Roadmap", layout: "roadmap"}, + {name: "Roadmap", layout: "roadmap"}, } for _, view := range views { @@ -489,7 +489,7 @@ func createView(ctx context.Context, info projectURLInfo, name, layout string, v return nil } -// createStandardFields creates the standard campaign fields +// createStandardFields creates the standard project fields func createStandardFields(ctx context.Context, projectURL string, projectNumber int, owner string, verbose bool) error { projectLog.Print("Creating standard fields") console.LogVerbose(verbose, "Creating custom fields...") @@ -502,7 +502,7 @@ func createStandardFields(ctx context.Context, projectURL string, projectNumber dataType string options []string // For SINGLE_SELECT fields }{ - {"Campaign Id", "TEXT", nil}, + {"Tracker Id", "TEXT", nil}, {"Worker Workflow", "TEXT", nil}, {"Target Repo", "TEXT", nil}, {"Priority", "SINGLE_SELECT", []string{"High", "Medium", "Low"}}, diff --git a/pkg/cli/project_command_test.go b/pkg/cli/project_command_test.go index 1c70b7ff75..3b4d4118dc 100644 --- a/pkg/cli/project_command_test.go +++ b/pkg/cli/project_command_test.go @@ -172,10 +172,10 @@ func TestProjectNewCommandFlags(t *testing.T) { linkFlag := cmd.Flags().Lookup("link") require.NotNil(t, linkFlag, "Should have --link flag") - // Check campaign setup flag - campaignFlag := cmd.Flags().Lookup("with-campaign-setup") - require.NotNil(t, campaignFlag, "Should have --with-campaign-setup flag") - assert.Equal(t, "bool", campaignFlag.Value.Type(), "Campaign setup flag should be boolean") + // Check project setup flag + projectSetupFlag := cmd.Flags().Lookup("with-project-setup") + require.NotNil(t, projectSetupFlag, "Should have --with-project-setup flag") + assert.Equal(t, "bool", projectSetupFlag.Value.Type(), "Project setup flag should be boolean") // Verify removed flags don't exist viewsFlag := cmd.Flags().Lookup("views") @@ -384,31 +384,31 @@ func TestSingleSelectOptionsEqual(t *testing.T) { } } -func TestProjectConfigWithCampaignSetup(t *testing.T) { +func TestProjectConfigWithProjectSetup(t *testing.T) { tests := []struct { name string config ProjectConfig description string }{ { - name: "with campaign setup", + name: "with project setup", config: ProjectConfig{ - Title: "Campaign Project", - Owner: "myorg", - OwnerType: "org", - WithCampaignSetup: true, + Title: "Project With Setup", + Owner: "myorg", + OwnerType: "org", + WithProjectSetup: true, }, - description: "Should have campaign setup enabled", + description: "Should have project setup enabled", }, { - name: "without campaign setup", + name: "without project setup", config: ProjectConfig{ - Title: "Basic Project", - Owner: "myorg", - OwnerType: "org", - WithCampaignSetup: false, + Title: "Basic Project", + Owner: "myorg", + OwnerType: "org", + WithProjectSetup: false, }, - description: "Should have campaign setup disabled", + description: "Should have project setup disabled", }, } @@ -418,10 +418,10 @@ func TestProjectConfigWithCampaignSetup(t *testing.T) { assert.NotEmpty(t, tt.config.Owner, "Project owner should not be empty") // Verify flag settings - if tt.config.WithCampaignSetup { - assert.True(t, tt.config.WithCampaignSetup, "Campaign setup should be enabled") + if tt.config.WithProjectSetup { + assert.True(t, tt.config.WithProjectSetup, "Project setup should be enabled") } else { - assert.False(t, tt.config.WithCampaignSetup, "Campaign setup should be disabled") + assert.False(t, tt.config.WithProjectSetup, "Project setup should be disabled") } }) } diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index 625464d147..c4df83ad3d 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -238,20 +238,6 @@ const DefaultMCPRegistryURL URL = "https://api.mcp.github.com/v0.1" // Used when github tool is configured with mode: remote. const GitHubCopilotMCPDomain = "api.githubcopilot.com" -// DefaultCampaignTemplateProjectURL is the default source project URL for copying campaign templates. -// This points to the githubnext "[TEMPLATE: Agentic Campaign]" project (Project 74). -const DefaultCampaignTemplateProjectURL URL = "https://github.com/orgs/githubnext/projects/74" - -// AgenticCampaignLabel is the label applied to all campaign-related issues, PRs, and discussions. -// This label marks content as part of an agentic campaign, preventing other workflows from -// processing these items to avoid interference with campaign orchestration. -const AgenticCampaignLabel = "agentic-campaign" - -// CampaignLabelPrefix is the prefix used for campaign-specific labels. -// 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 CampaignLabelPrefix = "z_campaign_" - // DefaultClaudeCodeVersion is the default version of the Claude Code CLI. const DefaultClaudeCodeVersion Version = "2.1.29" diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index 678c78308b..750a56de55 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -3286,10 +3286,6 @@ "create-orphan": { "type": "boolean", "description": "Create orphaned branch if it doesn't exist (default: true)" - }, - "campaign-id": { - "type": "string", - "description": "Campaign ID for campaign-specific repo-memory (optional, used to correlate memory with campaign workflows)" } }, "additionalProperties": false, @@ -3364,10 +3360,6 @@ "create-orphan": { "type": "boolean", "description": "Create orphaned branch if it doesn't exist (default: true)" - }, - "campaign-id": { - "type": "string", - "description": "Campaign ID for campaign-specific repo-memory (optional, used to correlate memory with campaign workflows)" } }, "additionalProperties": false @@ -3896,7 +3888,7 @@ "oneOf": [ { "type": "object", - "description": "Configuration for managing GitHub Projects v2 boards. Smart tool that can add issue/PR items and update custom fields on existing items. By default it is update-only: if the project does not exist, the job fails with instructions to create it manually. To allow workflows to create missing projects, explicitly opt in via the agent output field create_if_missing=true (and/or provide a github-token override). NOTE: Projects v2 requires a Personal Access Token (PAT) or GitHub App token with appropriate permissions; the GITHUB_TOKEN cannot be used for Projects v2. Safe output items produced by the agent use type=update_project Configuration also supports an optional views array for declaring project views to create. Safe output items produced by the agent use type=update_project and may include: project (board name), content_type (issue|pull_request), content_number, fields, campaign_id, and create_if_missing.", + "description": "Configuration for managing GitHub Projects v2 boards. Smart tool that can add issue/PR items and update custom fields on existing items. By default it is update-only: if the project does not exist, the job fails with instructions to create it manually. To allow workflows to create missing projects, explicitly opt in via the agent output field create_if_missing=true (and/or provide a github-token override). NOTE: Projects v2 requires a Personal Access Token (PAT) or GitHub App token with appropriate permissions; the GITHUB_TOKEN cannot be used for Projects v2. Safe output items produced by the agent use type=update_project. Configuration also supports an optional views array for declaring project views to create. Safe output items produced by the agent use type=update_project and may include: project (board name), content_type (issue|pull_request), content_number, fields, and create_if_missing.", "required": ["project"], "properties": { "max": { @@ -3925,7 +3917,7 @@ "properties": { "name": { "type": "string", - "description": "The name of the view (e.g., 'Sprint Board', 'Campaign Roadmap')" + "description": "The name of the view (e.g., 'Sprint Board', 'Roadmap')" }, "layout": { "type": "string", @@ -3953,14 +3945,14 @@ }, "field-definitions": { "type": "array", - "description": "Optional array of project custom fields to create up-front. Useful for campaign projects that require a fixed set of fields.", + "description": "Optional array of project custom fields to create up-front.", "items": { "type": "object", "required": ["name", "data-type"], "properties": { "name": { "type": "string", - "description": "The field name to create (e.g., 'status', 'campaign_id')" + "description": "The field name to create (e.g., 'status', 'priority')" }, "data-type": { "type": "string", @@ -4019,7 +4011,7 @@ }, "title-prefix": { "type": "string", - "description": "Optional prefix for auto-generated project titles (default: 'Campaign'). When the agent doesn't provide a title, the project title is auto-generated as ': ' or ' #' based on the issue context." + "description": "Optional prefix for auto-generated project titles (default: 'Project'). When the agent doesn't provide a title, the project title is auto-generated as ': ' or ' #' based on the issue context." }, "views": { "type": "array", @@ -4031,7 +4023,7 @@ "properties": { "name": { "type": "string", - "description": "The name of the view (e.g., 'Sprint Board', 'Campaign Roadmap')" + "description": "The name of the view (e.g., 'Sprint Board', 'Roadmap')" }, "layout": { "type": "string", @@ -4059,14 +4051,14 @@ }, "field-definitions": { "type": "array", - "description": "Optional array of project custom fields to create automatically after project creation. Useful for campaign projects that require a fixed set of fields.", + "description": "Optional array of project custom fields to create automatically after project creation.", "items": { "type": "object", "required": ["name", "data-type"], "properties": { "name": { "type": "string", - "description": "The field name to create (e.g., 'Campaign Id', 'Priority')" + "description": "The field name to create (e.g., 'Priority', 'Classification')" }, "data-type": { "type": "string", @@ -4105,7 +4097,7 @@ "oneOf": [ { "type": "object", - "description": "Configuration for creating GitHub Project status updates. Status updates provide stakeholder communication and historical record of project progress. Requires a Personal Access Token (PAT) or GitHub App token with Projects: Read+Write permission. The GITHUB_TOKEN cannot be used for Projects v2. Status updates are created on the specified project board and appear in the Updates tab. Typically used by campaign orchestrators to post run summaries with progress, findings, and next steps.", + "description": "Configuration for creating GitHub Project status updates. Status updates provide stakeholder communication and historical record of project progress. Requires a Personal Access Token (PAT) or GitHub App token with Projects: Read+Write permission. The GITHUB_TOKEN cannot be used for Projects v2. Status updates are created on the specified project board and appear in the Updates tab. Typically used by orchestrators to post run summaries with progress, findings, and next steps.", "required": ["project"], "properties": { "max": { diff --git a/pkg/stringutil/identifiers.go b/pkg/stringutil/identifiers.go index 78d91b7085..48390e802f 100644 --- a/pkg/stringutil/identifiers.go +++ b/pkg/stringutil/identifiers.go @@ -3,8 +3,6 @@ package stringutil import ( "path/filepath" "strings" - - "github.com/github/gh-aw/pkg/constants" ) // NormalizeWorkflowName removes .md and .lock.yml extensions from workflow names. @@ -127,25 +125,3 @@ func IsAgenticWorkflow(path string) bool { func IsLockFile(path string) bool { return strings.HasSuffix(path, ".lock.yml") } - -// FormatCampaignLabel generates a campaign-specific label from a campaign ID. -// Campaign-specific labels follow the format "z_campaign_" where is the campaign identifier. -// The "z_" prefix ensures these labels sort last in label lists for better visibility. -// -// This function sanitizes the campaign ID by replacing invalid label characters (spaces, special chars) -// with hyphens to ensure GitHub label compatibility. -// -// Examples: -// -// FormatCampaignLabel("security-q1-2025") // returns "z_campaign_security-q1-2025" -// FormatCampaignLabel("Security Q1 2025") // returns "z_campaign_security-q1-2025" -// FormatCampaignLabel("dependency_updates") // returns "z_campaign_dependency-updates" -func FormatCampaignLabel(campaignID string) string { - // Sanitize campaign ID for label compatibility - // Replace spaces and underscores with hyphens, convert to lowercase - sanitized := strings.ToLower(campaignID) - sanitized = strings.ReplaceAll(sanitized, " ", "-") - sanitized = strings.ReplaceAll(sanitized, "_", "-") - - return constants.CampaignLabelPrefix + sanitized -} diff --git a/pkg/workflow/compiler_jobs_test.go b/pkg/workflow/compiler_jobs_test.go index b224e46dba..2067b9d484 100644 --- a/pkg/workflow/compiler_jobs_test.go +++ b/pkg/workflow/compiler_jobs_test.go @@ -181,29 +181,28 @@ func TestBuildActivationJobWithReaction(t *testing.T) { } } -// TestBuildActivationJobCampaignOrchestratorFilename tests that campaign orchestrator -// workflows (.campaign.g.md) generate correct GH_AW_WORKFLOW_FILE (.campaign.lock.yml) -func TestBuildActivationJobCampaignOrchestratorFilename(t *testing.T) { +// TestBuildActivationJobLockFilename tests that lock filenames are passed through +// unchanged to the activation job environment. +func TestBuildActivationJobLockFilename(t *testing.T) { compiler := NewCompiler() workflowData := &WorkflowData{ - Name: "Test Campaign", + Name: "Test Workflow", SafeOutputs: &SafeOutputsConfig{}, } - // Test with campaign orchestrator filename (with .g.) - job, err := compiler.buildActivationJob(workflowData, false, "", "example.campaign.lock.yml") + job, err := compiler.buildActivationJob(workflowData, false, "", "example.workflow.lock.yml") if err != nil { t.Fatalf("buildActivationJob() returned error: %v", err) } - // Check that GH_AW_WORKFLOW_FILE uses .campaign.lock.yml (without .g.) + // Check that GH_AW_WORKFLOW_FILE uses the lock filename exactly stepsContent := strings.Join(job.Steps, "") - if !strings.Contains(stepsContent, `GH_AW_WORKFLOW_FILE: "example.campaign.lock.yml"`) { - t.Errorf("Expected GH_AW_WORKFLOW_FILE to be 'example.campaign.lock.yml', got steps content:\n%s", stepsContent) + if !strings.Contains(stepsContent, `GH_AW_WORKFLOW_FILE: "example.workflow.lock.yml"`) { + t.Errorf("Expected GH_AW_WORKFLOW_FILE to be 'example.workflow.lock.yml', got steps content:\n%s", stepsContent) } // Verify it does NOT contain the incorrect .g. version - if strings.Contains(stepsContent, "example.campaign.g.lock.yml") { + if strings.Contains(stepsContent, "example.workflow.g.lock.yml") { t.Error("GH_AW_WORKFLOW_FILE should not contain '.g.' in the filename") } } diff --git a/pkg/workflow/create_project_test.go b/pkg/workflow/create_project_test.go index e90331d70c..5542c533e3 100644 --- a/pkg/workflow/create_project_test.go +++ b/pkg/workflow/create_project_test.go @@ -36,7 +36,7 @@ func TestParseCreateProjectsConfig(t *testing.T) { "max": 1, "github-token": "${{ secrets.PROJECTS_PAT }}", "target-owner": "myorg", - "title-prefix": "Campaign", + "title-prefix": "Project", }, }, expectedConfig: &CreateProjectsConfig{ @@ -45,7 +45,7 @@ func TestParseCreateProjectsConfig(t *testing.T) { }, GitHubToken: "${{ secrets.PROJECTS_PAT }}", TargetOwner: "myorg", - TitlePrefix: "Campaign", + TitlePrefix: "Project", }, }, { @@ -55,7 +55,7 @@ func TestParseCreateProjectsConfig(t *testing.T) { "max": 1, "views": []any{ map[string]any{ - "name": "Campaign Roadmap", + "name": "Roadmap", "layout": "roadmap", "filter": "is:issue is:pr", }, @@ -73,7 +73,7 @@ func TestParseCreateProjectsConfig(t *testing.T) { }, Views: []ProjectView{ { - Name: "Campaign Roadmap", + Name: "Roadmap", Layout: "roadmap", Filter: "is:issue is:pr", }, @@ -272,7 +272,7 @@ func TestCreateProjectsConfig_FieldDefinitionsParsing(t *testing.T) { "max": 1, "field-definitions": []any{ map[string]any{ - "name": "Campaign Id", + "name": "Tracking Id", "data-type": "TEXT", }, map[string]any{ @@ -293,7 +293,7 @@ func TestCreateProjectsConfig_FieldDefinitionsParsing(t *testing.T) { require.Len(t, config.FieldDefinitions, 3, "Should parse 3 field definitions") // Check first field - assert.Equal(t, "Campaign Id", config.FieldDefinitions[0].Name) + assert.Equal(t, "Tracking Id", config.FieldDefinitions[0].Name) assert.Equal(t, "TEXT", config.FieldDefinitions[0].DataType) assert.Empty(t, config.FieldDefinitions[0].Options) @@ -341,13 +341,13 @@ func TestCreateProjectsConfig_ViewsAndFieldDefinitions(t *testing.T) { "target-owner": "myorg", "views": []any{ map[string]any{ - "name": "Campaign Board", + "name": "Task Board", "layout": "board", }, }, "field-definitions": []any{ map[string]any{ - "name": "Campaign Id", + "name": "Tracking Id", "data-type": "TEXT", }, map[string]any{ @@ -364,12 +364,12 @@ func TestCreateProjectsConfig_ViewsAndFieldDefinitions(t *testing.T) { // Check views require.Len(t, config.Views, 1, "Should have 1 view") - assert.Equal(t, "Campaign Board", config.Views[0].Name) + assert.Equal(t, "Task Board", config.Views[0].Name) assert.Equal(t, "board", config.Views[0].Layout) // Check field definitions require.Len(t, config.FieldDefinitions, 2, "Should have 2 field definitions") - assert.Equal(t, "Campaign Id", config.FieldDefinitions[0].Name) + assert.Equal(t, "Tracking Id", config.FieldDefinitions[0].Name) assert.Equal(t, "TEXT", config.FieldDefinitions[0].DataType) assert.Equal(t, "Size", config.FieldDefinitions[1].Name) assert.Equal(t, "SINGLE_SELECT", config.FieldDefinitions[1].DataType) diff --git a/pkg/workflow/frontmatter_types.go b/pkg/workflow/frontmatter_types.go index 70e7c048f2..31b066de73 100644 --- a/pkg/workflow/frontmatter_types.go +++ b/pkg/workflow/frontmatter_types.go @@ -49,95 +49,6 @@ type PermissionsConfig struct { OrganizationPackages string `json:"organization-packages,omitempty"` } -// ProjectConfig represents the project tracking configuration for a workflow -// When configured, this automatically enables project board management operations -// and can trigger campaign orchestrator generation when campaign fields are present -type ProjectConfig struct { - URL string `json:"url,omitempty"` // GitHub Project URL - Scope []string `json:"scope,omitempty"` // Repositories/organizations this workflow can operate on (e.g., ["owner/repo", "org:name"]) - MaxUpdates int `json:"max-updates,omitempty"` // Maximum number of project updates per run (default: 100) - MaxStatusUpdates int `json:"max-status-updates,omitempty"` // Maximum number of status updates per run (default: 1) - GitHubToken string `json:"github-token,omitempty"` // Optional custom GitHub token for project operations - DoNotDowngradeDoneItems *bool `json:"do-not-downgrade-done-items,omitempty"` // Prevent moving items backward (e.g., Done -> In Progress) - - // Campaign orchestration fields (optional) - // When present, triggers automatic generation of a campaign orchestrator workflow - ID string `json:"id,omitempty"` // Campaign identifier (optional, derived from filename if not set) - Workflows []string `json:"workflows,omitempty"` // Associated workflow IDs - MemoryPaths []string `json:"memory-paths,omitempty"` // Repo-memory paths - MetricsGlob string `json:"metrics-glob,omitempty"` // Metrics file glob pattern - CursorGlob string `json:"cursor-glob,omitempty"` // Cursor file glob pattern - TrackerLabel string `json:"tracker-label,omitempty"` // Label for discovering items - Owners []string `json:"owners,omitempty"` // Campaign owners - RiskLevel string `json:"risk-level,omitempty"` // Risk level (low/medium/high) - State string `json:"state,omitempty"` // Lifecycle state - Tags []string `json:"tags,omitempty"` // Categorization tags - Governance *CampaignGovernanceConfig `json:"governance,omitempty"` // Campaign governance policies - Bootstrap *CampaignBootstrapConfig `json:"bootstrap,omitempty"` // Campaign bootstrap configuration - Workers []WorkerMetadata `json:"workers,omitempty"` // Worker workflow metadata -} - -// CampaignGovernanceConfig represents governance policies for campaigns -type CampaignGovernanceConfig struct { - MaxNewItemsPerRun int `json:"max-new-items-per-run,omitempty"` - MaxDiscoveryItemsPerRun int `json:"max-discovery-items-per-run,omitempty"` - MaxDiscoveryPagesPerRun int `json:"max-discovery-pages-per-run,omitempty"` - OptOutLabels []string `json:"opt-out-labels,omitempty"` - DoNotDowngradeDoneItems *bool `json:"do-not-downgrade-done-items,omitempty"` - MaxProjectUpdatesPerRun int `json:"max-project-updates-per-run,omitempty"` - MaxCommentsPerRun int `json:"max-comments-per-run,omitempty"` -} - -// CampaignBootstrapConfig represents bootstrap configuration for campaigns -type CampaignBootstrapConfig struct { - Mode string `json:"mode,omitempty"` - SeederWorker *SeederWorkerConfig `json:"seeder-worker,omitempty"` - ProjectTodos *ProjectTodosBootstrapConfig `json:"project-todos,omitempty"` -} - -// SeederWorkerConfig represents seeder worker configuration -type SeederWorkerConfig struct { - WorkflowID string `json:"workflow-id,omitempty"` - Payload map[string]any `json:"payload,omitempty"` - MaxItems int `json:"max-items,omitempty"` -} - -// ProjectTodosBootstrapConfig represents project todos bootstrap configuration -type ProjectTodosBootstrapConfig struct { - StatusField string `json:"status-field,omitempty"` - TodoValue string `json:"todo-value,omitempty"` - MaxItems int `json:"max-items,omitempty"` - RequireFields []string `json:"require-fields,omitempty"` -} - -// WorkerMetadata represents metadata for worker workflows -type WorkerMetadata struct { - ID string `json:"id,omitempty"` - Name string `json:"name,omitempty"` - Description string `json:"description,omitempty"` - Capabilities []string `json:"capabilities,omitempty"` - PayloadSchema map[string]WorkerPayloadField `json:"payload-schema,omitempty"` - OutputLabeling WorkerOutputLabeling `json:"output-labeling,omitempty"` - IdempotencyStrategy string `json:"idempotency-strategy,omitempty"` - Priority int `json:"priority,omitempty"` -} - -// WorkerPayloadField represents a field in worker payload schema -type WorkerPayloadField struct { - Type string `json:"type,omitempty"` - Description string `json:"description,omitempty"` - Required bool `json:"required,omitempty"` - Example any `json:"example,omitempty"` -} - -// WorkerOutputLabeling represents output labeling configuration for workers -type WorkerOutputLabeling struct { - Labels []string `json:"labels,omitempty"` - KeyInTitle bool `json:"key-in-title,omitempty"` - KeyFormat string `json:"key-format,omitempty"` - MetadataFields []string `json:"metadata-fields,omitempty"` -} - // FrontmatterConfig represents the structured configuration from workflow frontmatter // This provides compile-time type safety and clearer error messages compared to map[string]any type FrontmatterConfig struct { diff --git a/pkg/workflow/js/safe_outputs_tools.json b/pkg/workflow/js/safe_outputs_tools.json index 6392cedbf0..a7a85800d7 100644 --- a/pkg/workflow/js/safe_outputs_tools.json +++ b/pkg/workflow/js/safe_outputs_tools.json @@ -536,7 +536,7 @@ "items": { "type": "string" }, - "description": "Replace the issue labels with this list (e.g., ['bug', 'campaign:foo']). Labels must exist in the repository." + "description": "Replace the issue labels with this list (e.g., ['bug', 'tracking:foo']). Labels must exist in the repository." }, "assignees": { "type": "array", @@ -774,7 +774,7 @@ }, { "name": "update_project", - "description": "Add or update items in GitHub Projects v2 boards. Can add issues/PRs to a project and update custom field values. Requires the project URL, content type (issue or pull_request), and content number. Use campaign_id to group related items.\n\nThree usage modes:\n1. Add/update project item: Requires project + content_type. For 'issue' or 'pull_request', also requires content_number. For 'draft_issue', requires draft_title.\n2. Create project fields: Requires project + operation='create_fields' + field_definitions.\n3. Create project view: Requires project + operation='create_view' + view.", + "description": "Add or update items in GitHub Projects v2 boards. Can add issues/PRs to a project and update custom field values. Requires the project URL, content type (issue or pull_request), and content number.\n\nThree usage modes:\n1. Add/update project item: Requires project + content_type. For 'issue' or 'pull_request', also requires content_number. For 'draft_issue', requires draft_title.\n2. Create project fields: Requires project + operation='create_fields' + field_definitions.\n3. Create project view: Requires project + operation='create_view' + view.", "inputSchema": { "type": "object", "required": [ @@ -792,7 +792,7 @@ "create_fields", "create_view" ], - "description": "Optional operation mode. Use create_fields to create required campaign fields up-front, or create_view to add a project view. When omitted, the tool adds/updates project items." + "description": "Optional operation mode. Use create_fields to create required fields up-front, or create_view to add a project view. When omitted, the tool adds/updates project items." }, "content_type": { "type": "string", @@ -887,10 +887,6 @@ }, "additionalProperties": false }, - "campaign_id": { - "type": "string", - "description": "Campaign identifier to group related project items. Used to track items created by the same campaign or workflow run." - }, "create_if_missing": { "type": "boolean", "description": "Whether to create the project if it doesn't exist. Defaults to false. Requires projects:write permission when true." @@ -935,7 +931,7 @@ "properties": { "title": { "type": "string", - "description": "Title for the new project. Should be descriptive and unique within the owner's projects. If not provided, will be auto-generated using the title-prefix configuration (default: 'Campaign') as ': ' or ' #' based on the issue context." + "description": "Title for the new project. Should be descriptive and unique within the owner's projects. If not provided, will be auto-generated using the title-prefix configuration (default: 'Project') as ': ' or ' #' based on the issue context." }, "owner": { "type": "string", diff --git a/pkg/workflow/repo_memory.go b/pkg/workflow/repo_memory.go index 555a0b555b..77ed9057ca 100644 --- a/pkg/workflow/repo_memory.go +++ b/pkg/workflow/repo_memory.go @@ -49,7 +49,6 @@ type RepoMemoryEntry struct { MaxFileCount int `yaml:"max-file-count,omitempty"` // maximum file count per commit (default: 100) Description string `yaml:"description,omitempty"` // optional description for this memory CreateOrphan bool `yaml:"create-orphan,omitempty"` // create orphaned branch if missing (default: true) - CampaignID string `yaml:"campaign-id,omitempty"` // campaign ID for campaign-specific repo-memory (optional) } // RepoMemoryToolConfig represents the configuration for repo-memory in tools @@ -261,13 +260,6 @@ func (c *Compiler) extractRepoMemoryConfig(toolsConfig *ToolsConfig) (*RepoMemor } } - // Parse campaign-id - if campaignID, exists := memoryMap["campaign-id"]; exists { - if idStr, ok := campaignID.(string); ok { - entry.CampaignID = idStr - } - } - config.Memories = append(config.Memories, entry) } } @@ -377,13 +369,6 @@ func (c *Compiler) extractRepoMemoryConfig(toolsConfig *ToolsConfig) (*RepoMemor } } - // Parse campaign-id - if campaignID, exists := configMap["campaign-id"]; exists { - if idStr, ok := campaignID.(string); ok { - entry.CampaignID = idStr - } - } - config.Memories = []RepoMemoryEntry{entry} return config, nil } @@ -631,9 +616,6 @@ func (c *Compiler) buildPushRepoMemoryJob(data *WorkflowData, threatDetectionEna // Quote the value to prevent YAML alias interpretation of patterns like *.md fmt.Fprintf(&step, " FILE_GLOB_FILTER: \"%s\"\n", fileGlobFilter) } - if memory.CampaignID != "" { - fmt.Fprintf(&step, " GH_AW_CAMPAIGN_ID: %s\n", memory.CampaignID) - } step.WriteString(" with:\n") step.WriteString(" script: |\n") diff --git a/pkg/workflow/repo_memory_test.go b/pkg/workflow/repo_memory_test.go index a3730a88c5..0fb91cce05 100644 --- a/pkg/workflow/repo_memory_test.go +++ b/pkg/workflow/repo_memory_test.go @@ -666,52 +666,6 @@ func TestRepoMemoryMaxFileCountValidationArray(t *testing.T) { } } -// TestRepoMemoryConfigWithCampaignID tests repo-memory configuration with campaign-id field -func TestRepoMemoryConfigWithCampaignID(t *testing.T) { - toolsMap := map[string]any{ - "repo-memory": []any{ - map[string]any{ - "id": "campaigns", - "branch-name": "memory/campaigns", - "file-glob": []any{"go-file-size-reduction-project64/**"}, - "campaign-id": "go-file-size-reduction-project64", - }, - }, - } - - toolsConfig, err := ParseToolsConfig(toolsMap) - if err != nil { - t.Fatalf("Failed to parse tools config: %v", err) - } - - compiler := NewCompiler() - config, err := compiler.extractRepoMemoryConfig(toolsConfig) - if err != nil { - t.Fatalf("Failed to extract repo-memory config: %v", err) - } - - if config == nil { - t.Fatal("Expected non-nil config") - } - - if len(config.Memories) != 1 { - t.Fatalf("Expected 1 memory, got %d", len(config.Memories)) - } - - memory := config.Memories[0] - if memory.ID != "campaigns" { - t.Errorf("Expected ID 'campaigns', got '%s'", memory.ID) - } - - if memory.CampaignID != "go-file-size-reduction-project64" { - t.Errorf("Expected campaign ID 'go-file-size-reduction-project64', got '%s'", memory.CampaignID) - } - - if len(memory.FileGlob) != 1 || memory.FileGlob[0] != "go-file-size-reduction-project64/**" { - t.Errorf("Expected file glob 'go-file-size-reduction-project64/**', got %v", memory.FileGlob) - } -} - // TestBranchPrefixValidation tests the validateBranchPrefix function func TestBranchPrefixValidation(t *testing.T) { tests := []struct { @@ -727,7 +681,7 @@ func TestBranchPrefixValidation(t *testing.T) { }, { name: "valid prefix - alphanumeric", - prefix: "campaigns", + prefix: "memories", wantErr: false, }, { diff --git a/pkg/workflow/safe_output_validation_config.go b/pkg/workflow/safe_output_validation_config.go index 5a346ad9c7..7dac4506e5 100644 --- a/pkg/workflow/safe_output_validation_config.go +++ b/pkg/workflow/safe_output_validation_config.go @@ -234,11 +234,7 @@ var ValidationConfig = map[string]TypeValidationConfig{ "update_project": { DefaultMax: 10, 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 "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}, + "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)"}, "content_type": {Type: "string", Enum: []string{"issue", "pull_request", "draft_issue"}}, "content_number": {OptionalPositiveInteger: true}, "issue": {OptionalPositiveInteger: true}, // Legacy diff --git a/pkg/workflow/safe_outputs_env.go b/pkg/workflow/safe_outputs_env.go index 65497900d0..4ae1dc1fdb 100644 --- a/pkg/workflow/safe_outputs_env.go +++ b/pkg/workflow/safe_outputs_env.go @@ -10,22 +10,6 @@ import ( var safeOutputsEnvLog = logger.New("workflow:safe_outputs_env") -// getCampaignIDFromRepoMemory returns the first configured campaign-id from repo-memory (if any). -// Campaign worker workflows typically carry campaign context via tools.repo-memory.*.campaign-id. -func getCampaignIDFromRepoMemory(data *WorkflowData) string { - if data == nil || data.RepoMemoryConfig == nil { - return "" - } - - for _, memory := range data.RepoMemoryConfig.Memories { - if strings.TrimSpace(memory.CampaignID) != "" { - return strings.TrimSpace(memory.CampaignID) - } - } - - return "" -} - // ======================================== // Safe Output Environment Variables // ======================================== @@ -152,11 +136,6 @@ func (c *Compiler) buildStandardSafeOutputEnvVars(data *WorkflowData, targetRepo targetRepoSlug, )...) - // Campaign context (optional): used by safe output pipeline to label outputs for discovery. - if campaignID := getCampaignIDFromRepoMemory(data); campaignID != "" { - customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_CAMPAIGN_ID: %q\n", campaignID)) - } - // Add messages config if present if data.SafeOutputs.Messages != nil { messagesJSON, err := serializeMessagesConfig(data.SafeOutputs.Messages) diff --git a/pkg/workflow/safe_outputs_test.go b/pkg/workflow/safe_outputs_test.go index 8b30978cca..c4e24d51a9 100644 --- a/pkg/workflow/safe_outputs_test.go +++ b/pkg/workflow/safe_outputs_test.go @@ -954,21 +954,18 @@ func TestBuildStandardSafeOutputEnvVars(t *testing.T) { }, }, { - name: "with repo-memory campaign-id", + name: "with repo-memory", workflowData: &WorkflowData{ Name: "Test Workflow", SafeOutputs: &SafeOutputsConfig{}, RepoMemoryConfig: &RepoMemoryConfig{ Memories: []RepoMemoryEntry{{ - ID: "campaigns", - CampaignID: "security-alert-burndown", + ID: "notes", }}, }, }, targetRepoSlug: "", - expectedInVars: []string{ - "GH_AW_CAMPAIGN_ID: \"security-alert-burndown\"", - }, + expectedInVars: []string{}, }, } diff --git a/pkg/workflow/update_project_handler_config_test.go b/pkg/workflow/update_project_handler_config_test.go index 761b9f8f40..da3a2eb28b 100644 --- a/pkg/workflow/update_project_handler_config_test.go +++ b/pkg/workflow/update_project_handler_config_test.go @@ -15,21 +15,23 @@ import ( func TestUpdateProjectHandlerConfigIncludesFieldDefinitions(t *testing.T) { tmpDir := testutil.TempDir(t, "handler-config-test") - testContent := `--- -name: Test Update Project Handler Config -on: workflow_dispatch -engine: copilot -safe-outputs: - update-project: - max: 1 - project: "https://github.com/orgs/test-org/projects/1" - field-definitions: - - name: "campaign_id" - data-type: "TEXT" ---- - -Test workflow -` + testContent := strings.Join([]string{ + "---", + "name: Test Update Project Handler Config", + "on: workflow_dispatch", + "engine: copilot", + "safe-outputs:", + " update-project:", + " max: 1", + " project: \"https://github.com/orgs/test-org/projects/1\"", + " field-definitions:", + " - name: \"tracker_id\"", + " data-type: \"TEXT\"", + "---", + "", + "Test workflow", + "", + }, "\n") mdFile := filepath.Join(tmpDir, "test-workflow.md") err := os.WriteFile(mdFile, []byte(testContent), 0600) diff --git a/schemas/agent-output.json b/schemas/agent-output.json index cb970bd28d..e5e4df07b1 100644 --- a/schemas/agent-output.json +++ b/schemas/agent-output.json @@ -498,10 +498,6 @@ "description": "Project title, number, or GitHub project URL", "minLength": 1 }, - "campaign_id": { - "type": "string", - "description": "Optional campaign ID for tracking (auto-generated if not provided). Format: slug-timestamp" - }, "content_type": { "type": "string", "enum": ["issue", "pull_request", "draft_issue"],