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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 19 additions & 4 deletions actions/setup/js/handle_agent_failure.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,14 @@ async function findPullRequestForCurrentBranch() {
/**
* Search for or create the parent issue for all agentic workflow failures
* @param {number|null} previousParentNumber - Previous parent issue number if creating due to limit
* @param {string} [ownerOverride] - Repository owner override (from failure-issue-repo config)
* @param {string} [repoOverride] - Repository name override (from failure-issue-repo config)
* @returns {Promise<{number: number, node_id: string}>} Parent issue number and node ID
*/
async function ensureParentIssue(previousParentNumber = null) {
const { owner, repo } = context.repo;
async function ensureParentIssue(previousParentNumber = null, ownerOverride, repoOverride) {
const { owner: contextOwner, repo: contextRepo } = context.repo;
const owner = ownerOverride || contextOwner;
const repo = repoOverride || contextRepo;
const parentTitle = "[aw] Failed runs";
const parentLabel = "agentic-workflows";

Expand Down Expand Up @@ -696,7 +700,18 @@ async function main() {
return;
}

const { owner, repo } = context.repo;
// Determine the target repository for failure issues
// If GH_AW_FAILURE_ISSUE_REPO is set, use that repo instead of the current repo
const failureIssueRepo = process.env.GH_AW_FAILURE_ISSUE_REPO || "";
let owner, repo;
if (failureIssueRepo && failureIssueRepo.includes("/")) {
const parts = failureIssueRepo.split("/");
owner = parts[0];
repo = parts[1];
core.info(`Using configured failure issue repo: ${owner}/${repo}`);
Comment on lines +707 to +711

Copilot AI Mar 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Parsing GH_AW_FAILURE_ISSUE_REPO with split("/") and taking the first two elements can silently mis-handle invalid values (e.g. owner/, /repo, or owner/repo/extra), leading to API calls against the wrong repo or to hard-to-diagnose failures. Recommend validating that the value matches exactly owner/repo (two non-empty segments, no extra /) and logging a warning + falling back to context.repo when invalid.

This issue also appears on line 703 of the same file.

Suggested change
if (failureIssueRepo && failureIssueRepo.includes("/")) {
const parts = failureIssueRepo.split("/");
owner = parts[0];
repo = parts[1];
core.info(`Using configured failure issue repo: ${owner}/${repo}`);
if (failureIssueRepo) {
const match = failureIssueRepo.match(/^([^/]+)\/([^/]+)$/);
if (match) {
owner = match[1];
repo = match[2];
core.info(`Using configured failure issue repo: ${owner}/${repo}`);
} else {
core.warning(
`Invalid GH_AW_FAILURE_ISSUE_REPO value "${failureIssueRepo}". Expected format "owner/repo". Falling back to context.repo.`
);
({ owner, repo } = context.repo);
}

Copilot uses AI. Check for mistakes.
} else {
({ owner, repo } = context.repo);
}

// Try to find a pull request for the current branch
const pullRequest = await findPullRequestForCurrentBranch();
Expand All @@ -708,7 +723,7 @@ async function main() {
let parentIssue;
if (groupReports) {
try {
parentIssue = await ensureParentIssue();
parentIssue = await ensureParentIssue(null, owner, repo);
} catch (error) {
core.warning(`Could not create parent issue, proceeding without parent: ${getErrorMessage(error)}`);
// Continue without parent issue
Expand Down
11 changes: 11 additions & 0 deletions docs/src/content/docs/reference/glossary.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,17 @@ safe-outputs:

See [Safe Outputs Reference](/gh-aw/reference/safe-outputs/).

### Failure Issue Repository (`failure-issue-repo:`)

A `safe-outputs` option that redirects failure tracking issues to a different repository. Useful when the workflow's repository has issues disabled:

```yaml
safe-outputs:
failure-issue-repo: github/docs-engineering
```

See [Safe Outputs Reference](/gh-aw/reference/safe-outputs/).

### Upload Assets

A safe output capability for uploading generated files (screenshots, charts, reports) to an orphaned git branch for persistent storage. The AI calls the `upload_asset` tool to register files, which are committed to a dedicated assets branch by a separate permission-controlled job. Assets are accessible via GitHub raw URLs. Commonly used for visual testing artifacts, data visualizations, and generated documentation.
Expand Down
12 changes: 12 additions & 0 deletions docs/src/content/docs/reference/safe-outputs.md
Original file line number Diff line number Diff line change
Expand Up @@ -1232,6 +1232,18 @@ safe-outputs:

This mirrors the `noop.report-as-issue` pattern. Use this to silence noisy failure reports for workflows where failures are expected or handled externally.

### Failure Issue Repository (`failure-issue-repo:`)

Redirects failure tracking issues to a different repository. Useful when the current repository has issues disabled (e.g. `github/docs-internal`).

```yaml wrap
safe-outputs:
failure-issue-repo: github/docs-engineering
create-issue:
```

The value must be in `owner/repo` format. The `GITHUB_TOKEN` used must have permission to create issues in the target repository. When not set, failure issues are created in the current repository.

### Group Reports (`group-reports:`)

Controls whether failed workflow runs are grouped under a parent "[aw] Failed runs" issue. This is opt-in and defaults to `false`.
Expand Down
6 changes: 6 additions & 0 deletions pkg/parser/schemas/main_workflow_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -7198,6 +7198,12 @@
"default": true,
"examples": [false, true]
},
"failure-issue-repo": {
"type": "string",
"description": "Repository to create failure tracking issues in, in the format 'owner/repo'. Useful when the current repository has issues disabled. Defaults to the current repository.",
"pattern": "^[^/]+/[^/]+$",
"examples": ["github/docs-engineering", "myorg/infra-alerts"]
},
"max-bot-mentions": {
"description": "Maximum number of bot trigger references (e.g. 'fixes #123', 'closes #456') allowed in output before all of them are neutralized. Default: 10. Supports integer or GitHub Actions expression (e.g. '${{ inputs.max-bot-mentions }}').",
"oneOf": [
Expand Down
1 change: 1 addition & 0 deletions pkg/workflow/compiler_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,7 @@ type SafeOutputsConfig struct {
Footer *bool `yaml:"footer,omitempty"` // Global footer control - when false, omits visible footer from all safe outputs (XML markers still included)
GroupReports bool `yaml:"group-reports,omitempty"` // If true, create parent "Failed runs" issue for agent failures (default: false)
ReportFailureAsIssue *bool `yaml:"report-failure-as-issue,omitempty"` // If false, disables creating failure tracking issues when workflows fail (default: true)
FailureIssueRepo string `yaml:"failure-issue-repo,omitempty"` // Repository to create failure issues in (format: "owner/repo"), defaults to current repo
MaxBotMentions *string `yaml:"max-bot-mentions,omitempty"` // Maximum bot trigger references (e.g. 'fixes #123') allowed before filtering. Default: 10. Supports integer or GitHub Actions expression.
Steps []any `yaml:"steps,omitempty"` // User-provided steps injected after setup/checkout and before safe-output code
IDToken *string `yaml:"id-token,omitempty"` // Override id-token permission: "write" to force-add, "none" to disable auto-detection
Expand Down
3 changes: 3 additions & 0 deletions pkg/workflow/imports.go
Original file line number Diff line number Diff line change
Expand Up @@ -687,6 +687,9 @@ func mergeSafeOutputConfig(result *SafeOutputsConfig, config map[string]any, c *
if !result.GroupReports && importedConfig.GroupReports {
result.GroupReports = true
}
if result.FailureIssueRepo == "" && importedConfig.FailureIssueRepo != "" {
result.FailureIssueRepo = importedConfig.FailureIssueRepo
}
if result.MaxBotMentions == nil && importedConfig.MaxBotMentions != nil {
result.MaxBotMentions = importedConfig.MaxBotMentions
}
Expand Down
5 changes: 5 additions & 0 deletions pkg/workflow/notify_comment.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,11 @@ func (c *Compiler) buildConclusionJob(data *WorkflowData, mainJobName string, sa
agentFailureEnvVars = append(agentFailureEnvVars, " GH_AW_FAILURE_REPORT_AS_ISSUE: \"true\"\n")
}

// Pass failure-issue-repo configuration (optional, defaults to current repo)
if data.SafeOutputs != nil && data.SafeOutputs.FailureIssueRepo != "" {
agentFailureEnvVars = append(agentFailureEnvVars, fmt.Sprintf(" GH_AW_FAILURE_ISSUE_REPO: %q\n", data.SafeOutputs.FailureIssueRepo))
}
Comment on lines +208 to +211

Copilot AI Mar 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

buildConclusionJob now conditionally emits GH_AW_FAILURE_ISSUE_REPO, but TestConclusionJob in notify_comment_test.go doesn't assert anything about this env var (it already checks other env vars like GH_AW_AGENT_CONCLUSION). Please add a unit test case that sets SafeOutputs.FailureIssueRepo and verifies the rendered conclusion job YAML includes GH_AW_FAILURE_ISSUE_REPO with the expected value.

Copilot uses AI. Check for mistakes.

// Pass timeout minutes value so the failure handler can provide an actionable hint when timed out
timeoutValue := strings.TrimPrefix(data.TimeoutMinutes, "timeout-minutes: ")
if timeoutValue != "" {
Expand Down
8 changes: 8 additions & 0 deletions pkg/workflow/safe_outputs_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,14 @@ func (c *Compiler) extractSafeOutputsConfig(frontmatter map[string]any) *SafeOut
}
}

// Handle failure-issue-repo (repository for failure issues, format: "owner/repo")
if failureIssueRepo, exists := outputMap["failure-issue-repo"]; exists {
if failureIssueRepoStr, ok := failureIssueRepo.(string); ok && failureIssueRepoStr != "" {
config.FailureIssueRepo = failureIssueRepoStr
safeOutputsConfigLog.Printf("Failure issue repo: %s", failureIssueRepoStr)
Comment on lines +474 to +476

Copilot AI Mar 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

failure-issue-repo is accepted as any non-empty string, but downstream handle_agent_failure.cjs assumes an owner/repo format and splits on /. As-is, values like owner/, /repo, or owner/repo/extra will be propagated and can break or misdirect issue creation. Consider validating against the same ^[^/]+/[^/]+$ pattern here (and either ignore invalid values with a warning or return an explicit configuration error).

Suggested change
if failureIssueRepoStr, ok := failureIssueRepo.(string); ok && failureIssueRepoStr != "" {
config.FailureIssueRepo = failureIssueRepoStr
safeOutputsConfigLog.Printf("Failure issue repo: %s", failureIssueRepoStr)
if failureIssueRepoStr, ok := failureIssueRepo.(string); ok {
trimmed := strings.TrimSpace(failureIssueRepoStr)
if trimmed != "" {
parts := strings.Split(trimmed, "/")
if len(parts) == 2 && parts[0] != "" && parts[1] != "" {
config.FailureIssueRepo = trimmed
safeOutputsConfigLog.Printf("Failure issue repo: %s", trimmed)
} else {
safeOutputsConfigLog.Printf("Ignoring invalid failure-issue-repo %q; expected format owner/repo", failureIssueRepoStr)
}
}

Copilot uses AI. Check for mistakes.
}
}

// Handle max-bot-mentions (templatable integer)
if err := preprocessIntFieldAsString(outputMap, "max-bot-mentions", safeOutputsConfigLog); err != nil {
safeOutputsConfigLog.Printf("max-bot-mentions: %v", err)
Expand Down
Loading