diff --git a/.github/aw/schemas/agentic-workflow.json b/.github/aw/schemas/agentic-workflow.json index ebb6ab3328..39e75b56ba 100644 --- a/.github/aw/schemas/agentic-workflow.json +++ b/.github/aw/schemas/agentic-workflow.json @@ -119,7 +119,23 @@ }, "on": { "description": "Workflow triggers that define when the agentic workflow should run. Supports standard GitHub Actions trigger events plus special command triggers for /commands (required)", - "examples": [{ "issues": { "types": ["opened"] } }, { "pull_request": { "types": ["opened", "synchronize"] } }, "workflow_dispatch", { "schedule": "daily at 9am" }, "/my-bot"], + "examples": [ + { + "issues": { + "types": ["opened"] + } + }, + { + "pull_request": { + "types": ["opened", "synchronize"] + } + }, + "workflow_dispatch", + { + "schedule": "daily at 9am" + }, + "/my-bot" + ], "oneOf": [ { "type": "string", @@ -2335,8 +2351,19 @@ ] }, "examples": [ - [{ "prompt": "Analyze the issue and create a plan" }], - [{ "uses": "actions/checkout@v4" }, { "prompt": "Review the code and suggest improvements" }], + [ + { + "prompt": "Analyze the issue and create a plan" + } + ], + [ + { + "uses": "actions/checkout@v4" + }, + { + "prompt": "Review the code and suggest improvements" + } + ], [ { "name": "Download logs from last 24 hours", @@ -2395,7 +2422,20 @@ "engine": { "description": "AI engine configuration that specifies which AI processor interprets and executes the markdown content of the workflow. Defaults to 'copilot'.", "default": "copilot", - "examples": ["copilot", "claude", "codex", { "id": "copilot", "version": "beta" }, { "id": "claude", "model": "claude-3-5-sonnet-20241022", "max-turns": 15 }], + "examples": [ + "copilot", + "claude", + "codex", + { + "id": "copilot", + "version": "beta" + }, + { + "id": "claude", + "model": "claude-3-5-sonnet-20241022", + "max-turns": 15 + } + ], "$ref": "#/$defs/engine_config" }, "mcp-servers": { @@ -2433,7 +2473,27 @@ "tools": { "type": "object", "description": "Tools and MCP (Model Context Protocol) servers available to the AI engine for GitHub API access, browser automation, file editing, and more", - "examples": [{ "playwright": { "version": "v1.41.0" } }, { "github": { "mode": "remote" } }, { "github": { "mode": "local", "version": "latest" } }, { "bash": null }], + "examples": [ + { + "playwright": { + "version": "v1.41.0" + } + }, + { + "github": { + "mode": "remote" + } + }, + { + "github": { + "mode": "local", + "version": "latest" + } + }, + { + "bash": null + } + ], "properties": { "github": { "description": "GitHub API tools for repository operations (issues, pull requests, content management)", @@ -3446,6 +3506,10 @@ } ], "description": "Time until the issue expires and should be automatically closed. Supports integer (days) or relative time format. When set, a maintenance workflow will be generated." + }, + "github-token": { + "type": "string", + "description": "GitHub token to use for this safe output operation. Overrides the global safe-outputs github-token if specified. Supports GitHub Actions expressions like ${{ secrets.MY_TOKEN }}." } }, "additionalProperties": false, @@ -3604,6 +3668,10 @@ } ], "description": "Time until the discussion expires and should be automatically closed. Supports integer (days) or relative time format like '2h' (2 hours), '7d' (7 days), '2w' (2 weeks), '1m' (1 month), '1y' (1 year). When set, a maintenance workflow will be generated." + }, + "github-token": { + "type": "string", + "description": "GitHub token to use for this safe output operation. Overrides the global safe-outputs github-token if specified. Supports GitHub Actions expressions like ${{ secrets.MY_TOKEN }}." } }, "additionalProperties": false, @@ -3675,6 +3743,10 @@ "target-repo": { "type": "string", "description": "Target repository in format 'owner/repo' for cross-repository operations. Takes precedence over trial target repo settings." + }, + "github-token": { + "type": "string", + "description": "GitHub token to use for this safe output operation. Overrides the global safe-outputs github-token if specified. Supports GitHub Actions expressions like ${{ secrets.MY_TOKEN }}." } }, "additionalProperties": false, @@ -3732,6 +3804,10 @@ "target-repo": { "type": "string", "description": "Target repository in format 'owner/repo' for cross-repository discussion updates. Takes precedence over trial target repo settings." + }, + "github-token": { + "type": "string", + "description": "GitHub token to use for this safe output operation. Overrides the global safe-outputs github-token if specified. Supports GitHub Actions expressions like ${{ secrets.MY_TOKEN }}." } }, "additionalProperties": false @@ -3772,6 +3848,10 @@ "target-repo": { "type": "string", "description": "Target repository in format 'owner/repo' for cross-repository operations. Takes precedence over trial target repo settings." + }, + "github-token": { + "type": "string", + "description": "GitHub token to use for this safe output operation. Overrides the global safe-outputs github-token if specified. Supports GitHub Actions expressions like ${{ secrets.MY_TOKEN }}." } }, "additionalProperties": false, @@ -3880,6 +3960,10 @@ "type": "string", "enum": ["spam", "abuse", "off_topic", "outdated", "resolved"] } + }, + "github-token": { + "type": "string", + "description": "GitHub token to use for this safe output operation. Overrides the global safe-outputs github-token if specified. Supports GitHub Actions expressions like ${{ secrets.MY_TOKEN }}." } }, "additionalProperties": false, @@ -4334,6 +4418,10 @@ "target-repo": { "type": "string", "description": "Target repository in format 'owner/repo' for cross-repository issue updates. Takes precedence over trial target repo settings." + }, + "github-token": { + "type": "string", + "description": "GitHub token to use for this safe output operation. Overrides the global safe-outputs github-token if specified. Supports GitHub Actions expressions like ${{ secrets.MY_TOKEN }}." } }, "additionalProperties": false @@ -4459,6 +4547,10 @@ "type": "string", "enum": ["spam", "abuse", "off_topic", "outdated", "resolved"] } + }, + "github-token": { + "type": "string", + "description": "GitHub token to use for this safe output operation. Overrides the global safe-outputs github-token if specified. Supports GitHub Actions expressions like ${{ secrets.MY_TOKEN }}." } }, "additionalProperties": false @@ -4586,6 +4678,10 @@ "type": "string", "description": "Target repository for cross-repo release updates (format: owner/repo). If not specified, updates releases in the workflow's repository.", "pattern": "^[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+$" + }, + "github-token": { + "type": "string", + "description": "GitHub token to use for this safe output operation. Overrides the global safe-outputs github-token if specified. Supports GitHub Actions expressions like ${{ secrets.MY_TOKEN }}." } }, "additionalProperties": false @@ -4693,6 +4789,10 @@ "items": { "$ref": "#/properties/githubActionsStep" } + }, + "github-token": { + "type": "string", + "description": "GitHub token to use for this safe output operation. Overrides the global safe-outputs github-token if specified. Supports GitHub Actions expressions like ${{ secrets.MY_TOKEN }}." } }, "additionalProperties": false @@ -4903,6 +5003,10 @@ "description": "Maximum number of mentions allowed per message. Default: 50", "minimum": 1, "default": 50 + }, + "github-token": { + "type": "string", + "description": "GitHub token to use for this safe output operation. Overrides the global safe-outputs github-token if specified. Supports GitHub Actions expressions like ${{ secrets.MY_TOKEN }}." } }, "additionalProperties": false diff --git a/.github/workflows/daily-copilot-token-report.lock.yml b/.github/workflows/daily-copilot-token-report.lock.yml index 3d51283333..598c7d74ba 100644 --- a/.github/workflows/daily-copilot-token-report.lock.yml +++ b/.github/workflows/daily-copilot-token-report.lock.yml @@ -30,7 +30,6 @@ name: "Daily Copilot Token Consumption Report" "on": schedule: - cron: "0 11 * * 1-5" - # Friendly format: daily (scattered) workflow_dispatch: permissions: {} diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index ebb6ab3328..39e75b56ba 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -119,7 +119,23 @@ }, "on": { "description": "Workflow triggers that define when the agentic workflow should run. Supports standard GitHub Actions trigger events plus special command triggers for /commands (required)", - "examples": [{ "issues": { "types": ["opened"] } }, { "pull_request": { "types": ["opened", "synchronize"] } }, "workflow_dispatch", { "schedule": "daily at 9am" }, "/my-bot"], + "examples": [ + { + "issues": { + "types": ["opened"] + } + }, + { + "pull_request": { + "types": ["opened", "synchronize"] + } + }, + "workflow_dispatch", + { + "schedule": "daily at 9am" + }, + "/my-bot" + ], "oneOf": [ { "type": "string", @@ -2335,8 +2351,19 @@ ] }, "examples": [ - [{ "prompt": "Analyze the issue and create a plan" }], - [{ "uses": "actions/checkout@v4" }, { "prompt": "Review the code and suggest improvements" }], + [ + { + "prompt": "Analyze the issue and create a plan" + } + ], + [ + { + "uses": "actions/checkout@v4" + }, + { + "prompt": "Review the code and suggest improvements" + } + ], [ { "name": "Download logs from last 24 hours", @@ -2395,7 +2422,20 @@ "engine": { "description": "AI engine configuration that specifies which AI processor interprets and executes the markdown content of the workflow. Defaults to 'copilot'.", "default": "copilot", - "examples": ["copilot", "claude", "codex", { "id": "copilot", "version": "beta" }, { "id": "claude", "model": "claude-3-5-sonnet-20241022", "max-turns": 15 }], + "examples": [ + "copilot", + "claude", + "codex", + { + "id": "copilot", + "version": "beta" + }, + { + "id": "claude", + "model": "claude-3-5-sonnet-20241022", + "max-turns": 15 + } + ], "$ref": "#/$defs/engine_config" }, "mcp-servers": { @@ -2433,7 +2473,27 @@ "tools": { "type": "object", "description": "Tools and MCP (Model Context Protocol) servers available to the AI engine for GitHub API access, browser automation, file editing, and more", - "examples": [{ "playwright": { "version": "v1.41.0" } }, { "github": { "mode": "remote" } }, { "github": { "mode": "local", "version": "latest" } }, { "bash": null }], + "examples": [ + { + "playwright": { + "version": "v1.41.0" + } + }, + { + "github": { + "mode": "remote" + } + }, + { + "github": { + "mode": "local", + "version": "latest" + } + }, + { + "bash": null + } + ], "properties": { "github": { "description": "GitHub API tools for repository operations (issues, pull requests, content management)", @@ -3446,6 +3506,10 @@ } ], "description": "Time until the issue expires and should be automatically closed. Supports integer (days) or relative time format. When set, a maintenance workflow will be generated." + }, + "github-token": { + "type": "string", + "description": "GitHub token to use for this safe output operation. Overrides the global safe-outputs github-token if specified. Supports GitHub Actions expressions like ${{ secrets.MY_TOKEN }}." } }, "additionalProperties": false, @@ -3604,6 +3668,10 @@ } ], "description": "Time until the discussion expires and should be automatically closed. Supports integer (days) or relative time format like '2h' (2 hours), '7d' (7 days), '2w' (2 weeks), '1m' (1 month), '1y' (1 year). When set, a maintenance workflow will be generated." + }, + "github-token": { + "type": "string", + "description": "GitHub token to use for this safe output operation. Overrides the global safe-outputs github-token if specified. Supports GitHub Actions expressions like ${{ secrets.MY_TOKEN }}." } }, "additionalProperties": false, @@ -3675,6 +3743,10 @@ "target-repo": { "type": "string", "description": "Target repository in format 'owner/repo' for cross-repository operations. Takes precedence over trial target repo settings." + }, + "github-token": { + "type": "string", + "description": "GitHub token to use for this safe output operation. Overrides the global safe-outputs github-token if specified. Supports GitHub Actions expressions like ${{ secrets.MY_TOKEN }}." } }, "additionalProperties": false, @@ -3732,6 +3804,10 @@ "target-repo": { "type": "string", "description": "Target repository in format 'owner/repo' for cross-repository discussion updates. Takes precedence over trial target repo settings." + }, + "github-token": { + "type": "string", + "description": "GitHub token to use for this safe output operation. Overrides the global safe-outputs github-token if specified. Supports GitHub Actions expressions like ${{ secrets.MY_TOKEN }}." } }, "additionalProperties": false @@ -3772,6 +3848,10 @@ "target-repo": { "type": "string", "description": "Target repository in format 'owner/repo' for cross-repository operations. Takes precedence over trial target repo settings." + }, + "github-token": { + "type": "string", + "description": "GitHub token to use for this safe output operation. Overrides the global safe-outputs github-token if specified. Supports GitHub Actions expressions like ${{ secrets.MY_TOKEN }}." } }, "additionalProperties": false, @@ -3880,6 +3960,10 @@ "type": "string", "enum": ["spam", "abuse", "off_topic", "outdated", "resolved"] } + }, + "github-token": { + "type": "string", + "description": "GitHub token to use for this safe output operation. Overrides the global safe-outputs github-token if specified. Supports GitHub Actions expressions like ${{ secrets.MY_TOKEN }}." } }, "additionalProperties": false, @@ -4334,6 +4418,10 @@ "target-repo": { "type": "string", "description": "Target repository in format 'owner/repo' for cross-repository issue updates. Takes precedence over trial target repo settings." + }, + "github-token": { + "type": "string", + "description": "GitHub token to use for this safe output operation. Overrides the global safe-outputs github-token if specified. Supports GitHub Actions expressions like ${{ secrets.MY_TOKEN }}." } }, "additionalProperties": false @@ -4459,6 +4547,10 @@ "type": "string", "enum": ["spam", "abuse", "off_topic", "outdated", "resolved"] } + }, + "github-token": { + "type": "string", + "description": "GitHub token to use for this safe output operation. Overrides the global safe-outputs github-token if specified. Supports GitHub Actions expressions like ${{ secrets.MY_TOKEN }}." } }, "additionalProperties": false @@ -4586,6 +4678,10 @@ "type": "string", "description": "Target repository for cross-repo release updates (format: owner/repo). If not specified, updates releases in the workflow's repository.", "pattern": "^[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+$" + }, + "github-token": { + "type": "string", + "description": "GitHub token to use for this safe output operation. Overrides the global safe-outputs github-token if specified. Supports GitHub Actions expressions like ${{ secrets.MY_TOKEN }}." } }, "additionalProperties": false @@ -4693,6 +4789,10 @@ "items": { "$ref": "#/properties/githubActionsStep" } + }, + "github-token": { + "type": "string", + "description": "GitHub token to use for this safe output operation. Overrides the global safe-outputs github-token if specified. Supports GitHub Actions expressions like ${{ secrets.MY_TOKEN }}." } }, "additionalProperties": false @@ -4903,6 +5003,10 @@ "description": "Maximum number of mentions allowed per message. Default: 50", "minimum": 1, "default": 50 + }, + "github-token": { + "type": "string", + "description": "GitHub token to use for this safe output operation. Overrides the global safe-outputs github-token if specified. Supports GitHub Actions expressions like ${{ secrets.MY_TOKEN }}." } }, "additionalProperties": false diff --git a/pkg/workflow/compiler_safe_outputs_core.go b/pkg/workflow/compiler_safe_outputs_core.go index e1b59902b7..63560ea43a 100644 --- a/pkg/workflow/compiler_safe_outputs_core.go +++ b/pkg/workflow/compiler_safe_outputs_core.go @@ -646,8 +646,11 @@ func (c *Compiler) buildHandlerManagerStep(data *WorkflowData) []string { c.addAllSafeOutputConfigEnvVars(&steps, data) // With section for github-token + // Use the first individual handler token if any handler has one, + // otherwise fall back to global safe-outputs token + individualToken := c.getFirstHandlerIndividualToken(data) steps = append(steps, " with:\n") - c.addSafeOutputGitHubTokenForConfig(&steps, data, "") + c.addSafeOutputGitHubTokenForConfig(&steps, data, individualToken) steps = append(steps, " script: |\n") steps = append(steps, " const { setupGlobals } = require('"+SetupActionDestination+"/setup_globals.cjs');\n") @@ -658,6 +661,50 @@ func (c *Compiler) buildHandlerManagerStep(data *WorkflowData) []string { return steps } +// getFirstHandlerIndividualToken returns the first non-empty individual token from handler-managed safe outputs +// This is used when multiple handlers are consolidated into a single step and we need to determine +// which token to use. Returns empty string if no individual tokens are set. +func (c *Compiler) getFirstHandlerIndividualToken(data *WorkflowData) string { + if data.SafeOutputs == nil { + return "" + } + + // Check each handler-managed safe output type for an individual token + // Order matches the order they are checked in buildConsolidatedSafeOutputsJob + if data.SafeOutputs.CreateIssues != nil && data.SafeOutputs.CreateIssues.GitHubToken != "" { + return data.SafeOutputs.CreateIssues.GitHubToken + } + if data.SafeOutputs.AddComments != nil && data.SafeOutputs.AddComments.GitHubToken != "" { + return data.SafeOutputs.AddComments.GitHubToken + } + if data.SafeOutputs.CreateDiscussions != nil && data.SafeOutputs.CreateDiscussions.GitHubToken != "" { + return data.SafeOutputs.CreateDiscussions.GitHubToken + } + if data.SafeOutputs.CloseIssues != nil && data.SafeOutputs.CloseIssues.GitHubToken != "" { + return data.SafeOutputs.CloseIssues.GitHubToken + } + if data.SafeOutputs.CloseDiscussions != nil && data.SafeOutputs.CloseDiscussions.GitHubToken != "" { + return data.SafeOutputs.CloseDiscussions.GitHubToken + } + if data.SafeOutputs.AddLabels != nil && data.SafeOutputs.AddLabels.GitHubToken != "" { + return data.SafeOutputs.AddLabels.GitHubToken + } + if data.SafeOutputs.UpdateIssues != nil && data.SafeOutputs.UpdateIssues.GitHubToken != "" { + return data.SafeOutputs.UpdateIssues.GitHubToken + } + if data.SafeOutputs.UpdateDiscussions != nil && data.SafeOutputs.UpdateDiscussions.GitHubToken != "" { + return data.SafeOutputs.UpdateDiscussions.GitHubToken + } + if data.SafeOutputs.LinkSubIssue != nil && data.SafeOutputs.LinkSubIssue.GitHubToken != "" { + return data.SafeOutputs.LinkSubIssue.GitHubToken + } + if data.SafeOutputs.UpdateRelease != nil && data.SafeOutputs.UpdateRelease.GitHubToken != "" { + return data.SafeOutputs.UpdateRelease.GitHubToken + } + + return "" +} + // addHandlerManagerConfigEnvVar adds a JSON config environment variable for the handler manager // This config indicates which handlers should be loaded and includes their type-specific options // The presence of a config key indicates that handler is enabled (no explicit "enabled" field needed) diff --git a/pkg/workflow/individual_github_token_integration_test.go b/pkg/workflow/individual_github_token_integration_test.go index f76986d9e6..1bdd234ed4 100644 --- a/pkg/workflow/individual_github_token_integration_test.go +++ b/pkg/workflow/individual_github_token_integration_test.go @@ -191,9 +191,10 @@ This workflow tests that add-labels uses its own github-token. yamlContent := string(content) - // Verify that the safe_outputs job is generated with add_labels step - if !strings.Contains(yamlContent, "id: add_labels") { - t.Error("Expected safe_outputs job with add_labels step to be generated") + // Verify that the safe_outputs job is generated with handler manager step + // (add-labels is now handled by the consolidated handler manager) + if !strings.Contains(yamlContent, "id: process_safe_outputs") { + t.Error("Expected safe_outputs job with process_safe_outputs step to be generated") } if !strings.Contains(yamlContent, "github-token: ${{ secrets.LABELS_PAT }}") { diff --git a/pkg/workflow/safe_outputs_integration_test.go b/pkg/workflow/safe_outputs_integration_test.go index 0dd313a188..77d2ca3eb9 100644 --- a/pkg/workflow/safe_outputs_integration_test.go +++ b/pkg/workflow/safe_outputs_integration_test.go @@ -517,9 +517,8 @@ func TestConsolidatedSafeOutputsJobIntegration(t *testing.T) { "SHARED_VAR", }, expectedStepNames: []string{ - "create_issue", + "process_safe_outputs", // create_issue and add_comment are handled by the handler manager "create_pull_request", - "add_comment", // Note: "noop" is not included in consolidated job }, },