Skip to content

Commit 20404a2

Browse files
authored
Enable templatable report-failure-as-issue and shared-workflow propagation (#41821)
1 parent 0956abe commit 20404a2

13 files changed

Lines changed: 148 additions & 22 deletions

.github/aw/safe-outputs-runtime.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,8 +203,9 @@ Fields that influence permission computation (`add-comment.discussions`, `create
203203
- Increase this limit for long-running branches that touch many files
204204
- `group-reports:` - Group workflow failure reports as sub-issues (boolean, default: `false`)
205205
- When `true`, creates a parent `[aw] Failed runs` issue that tracks all workflow failures as sub-issues; useful for larger repositories
206-
- `report-failure-as-issue:` - Control whether workflow failures are reported as GitHub issues (boolean, default: `true`)
206+
- `report-failure-as-issue:` - Control whether workflow failures are reported as GitHub issues (boolean, expression, or category array; default: `true`)
207207
- When `false`, suppresses automatic failure issue creation for this workflow
208+
- Supports templatable boolean expressions, e.g. `report-failure-as-issue: ${{ inputs.report-failure-as-issue }}`
208209
- Use to silence noisy failure reports for workflows where failures are expected or handled externally
209210
- `failure-issue-repo:` - Repository to create failure tracking issues in (string, format: `"owner/repo"`)
210211
- Defaults to the current repository when not specified

actions/setup/js/handle_agent_failure.cjs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const { AWF_INFRA_LINE_RE } = require("./log_parser_shared.cjs");
1414
const { resolveFirewallAuditLogPath, resolveAICreditsFailureState, parseMaxAICreditsFromAuditLog, parseAICreditsErrorInfoFromAuditLog, parseUnknownModelAICreditsFromAuditLog } = require("./ai_credits_context.cjs");
1515
const { formatAICCredits } = require("./daily_aic_workflow_helpers.cjs");
1616
const { formatAIC } = require("./model_costs.cjs");
17+
const { parseBoolTemplatable } = require("./templatable.cjs");
1718
const { parseTokenUsageJsonl, generateTokenUsageSummary } = require("./parse_mcp_gateway_log.cjs");
1819
const { readDedupedTokenUsage, TOKEN_USAGE_PATHS } = require("./parse_token_usage.cjs");
1920
const { extractShellCommandFromToolData } = require("./tool_call_details.cjs");
@@ -2692,7 +2693,7 @@ async function main() {
26922693
const unknownModelAICreditsFromAudit = parseUnknownModelAICreditsFromAuditLog();
26932694
const unknownModelAICredits = unknownModelAICreditsFromAudit || (unknownModelAICreditsFromOutput && agentConclusion === "failure");
26942695
const pushRepoMemoryResult = process.env.GH_AW_PUSH_REPO_MEMORY_RESULT || "";
2695-
const reportFailureAsIssue = process.env.GH_AW_FAILURE_REPORT_AS_ISSUE !== "false"; // Default to true
2696+
const reportFailureAsIssue = parseBoolTemplatable(process.env.GH_AW_FAILURE_REPORT_AS_ISSUE, true);
26962697
// Parse included categories filter for report-failure-as-issue (optional JSON array of category strings)
26972698
const failureCategoriesFilterRaw = process.env.GH_AW_FAILURE_CATEGORIES_FILTER || "";
26982699
let failureCategoriesFilter = null;

actions/setup/js/handle_agent_failure.test.cjs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,34 @@ describe("handle_agent_failure", () => {
485485
expect(createIssueMock).not.toHaveBeenCalled();
486486
});
487487

488+
it("skips failure issue creation when the runtime report flag resolves to false", async () => {
489+
const searchMock = vi.fn();
490+
const createCommentMock = vi.fn();
491+
const createIssueMock = vi.fn();
492+
process.env.GH_AW_FAILURE_REPORT_AS_ISSUE = " False ";
493+
494+
global.github = {
495+
rest: {
496+
search: {
497+
issuesAndPullRequests: searchMock,
498+
},
499+
issues: {
500+
create: createIssueMock,
501+
createComment: createCommentMock,
502+
},
503+
pulls: { get: vi.fn() },
504+
},
505+
graphql: vi.fn(),
506+
};
507+
508+
await main();
509+
510+
expect(searchMock).not.toHaveBeenCalled();
511+
expect(createCommentMock).not.toHaveBeenCalled();
512+
expect(createIssueMock).not.toHaveBeenCalled();
513+
expect(global.core.info).toHaveBeenCalledWith("Failure issue reporting is disabled (report-failure-as-issue: false), skipping failure issue creation");
514+
});
515+
488516
it("adds a comment when existing issue metadata contains commas in free-form values", async () => {
489517
const createCommentMock = vi.fn(async () => ({ data: { id: 1001 } }));
490518
const createIssueMock = vi.fn();

actions/setup/js/templatable.cjs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
* - boolean `false` → `false`
2323
* - string `"true"` → `true`
2424
* - string `"false"` → `false`
25+
* - string variants that normalize to `"false"` after trim/lowercase
26+
* (for example `" False "`) → `false`
2527
* - any other string (e.g. a resolved GitHub Actions expression value
2628
* that was not "false") → `true`
2729
*
@@ -32,7 +34,7 @@
3234
*/
3335
function parseBoolTemplatable(value, defaultValue = true) {
3436
if (value === undefined || value === null) return defaultValue;
35-
return String(value) !== "false";
37+
return String(value).trim().toLowerCase() !== "false";
3638
}
3739

3840
/**

actions/setup/js/templatable.test.cjs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ describe("templatable.cjs", () => {
3333
expect(parseBoolTemplatable("false")).toBe(false);
3434
});
3535

36+
it("handles normalized false string variants", () => {
37+
expect(parseBoolTemplatable("False")).toBe(false);
38+
expect(parseBoolTemplatable(" false ")).toBe(false);
39+
});
40+
3641
it("treats a resolved expression value other than false as truthy", () => {
3742
// GitHub Actions expressions that resolve to something other than "false"
3843
// (e.g. "yes", "1", an empty object representation) should be truthy.

pkg/parser/schemas/main_workflow_schema.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10303,8 +10303,7 @@
1030310303
"report-failure-as-issue": {
1030410304
"oneOf": [
1030510305
{
10306-
"type": "boolean",
10307-
"description": "When false, disables creating failure tracking issues when workflows fail. When true, all failures trigger issues. Defaults to true."
10306+
"$ref": "#/$defs/templatable_boolean"
1030810307
},
1030910308
{
1031010309
"type": "array",
@@ -10318,7 +10317,7 @@
1031810317
}
1031910318
],
1032010319
"default": true,
10321-
"examples": [false, true, ["agent_failure", "missing_safe_outputs"], ["!inference_access_error", "!ai_credits_rate_limit_error"]]
10320+
"examples": [false, true, "${{ inputs.report-failure-as-issue }}", ["agent_failure", "missing_safe_outputs"], ["!inference_access_error", "!ai_credits_rate_limit_error"]]
1032210321
},
1032310322
"failure-issue-repo": {
1032410323
"type": "string",

pkg/workflow/compiler_safe_outputs_config_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3398,6 +3398,7 @@ func TestReportFailureAsIssueWithCategoriesFilter(t *testing.T) {
33983398
name string
33993399
reportValue any
34003400
expectBool *bool
3401+
expectString string
34013402
expectCategories []string
34023403
expectExcludedCategories []string
34033404
}{
@@ -3411,6 +3412,11 @@ func TestReportFailureAsIssueWithCategoriesFilter(t *testing.T) {
34113412
reportValue: false,
34123413
expectBool: boolPtr(false),
34133414
},
3415+
{
3416+
name: "templatable expression",
3417+
reportValue: "${{ inputs.report-failure-as-issue }}",
3418+
expectString: "${{ inputs.report-failure-as-issue }}",
3419+
},
34143420
{
34153421
name: "array of categories",
34163422
reportValue: []any{"agent_failure", "missing_safe_outputs"},
@@ -3454,6 +3460,11 @@ func TestReportFailureAsIssueWithCategoriesFilter(t *testing.T) {
34543460
require.True(t, ok, "ReportFailureAsIssue should be bool")
34553461
assert.Equal(t, *tt.expectBool, reportBool, "Boolean value should match")
34563462
}
3463+
if tt.expectString != "" {
3464+
reportString, ok := config.ReportFailureAsIssue.(string)
3465+
require.True(t, ok, "ReportFailureAsIssue should be string")
3466+
assert.Equal(t, tt.expectString, reportString, "String value should match")
3467+
}
34573468

34583469
if len(tt.expectCategories) > 0 {
34593470
assert.Equal(t, tt.expectCategories, config.ReportFailureAsIssueCategories, "Categories should match")

pkg/workflow/compiler_types.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -737,7 +737,7 @@ type SafeOutputsConfig struct {
737737
Mentions *MentionsConfig `yaml:"mentions,omitempty"` // Configuration for @mention filtering in safe outputs
738738
Footer *bool `yaml:"footer,omitempty"` // Global footer control - when false, omits visible footer from all safe outputs (XML markers still included)
739739
GroupReports bool `yaml:"group-reports,omitempty"` // If true, create parent "Failed runs" issue for agent failures (default: false)
740-
ReportFailureAsIssue any `yaml:"report-failure-as-issue,omitempty"` // Controls failure issue creation: bool (true/false) or []interface{} (parsed from YAML array, converted to []string in ReportFailureAsIssueCategories). Default: true
740+
ReportFailureAsIssue any `yaml:"report-failure-as-issue,omitempty"` // Controls failure issue creation: bool, templatable expression string, or []interface{} categories (parsed to ReportFailureAsIssueCategories/ExcludedCategories). Default: true
741741
ReportFailureAsIssueCategories []string `yaml:"-"` // Parsed failure categories for report-failure-as-issue (internal use only, included categories)
742742
ReportFailureAsIssueExcludedCategories []string `yaml:"-"` // Parsed excluded failure categories for report-failure-as-issue (internal use only, categories starting with "!")
743743
FailureIssueRepo string `yaml:"failure-issue-repo,omitempty"` // Repository to create failure issues in (format: "owner/repo"), defaults to current repo

pkg/workflow/imports.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,11 @@ func mergeSafeOutputConfig(result *SafeOutputsConfig, config map[string]any, c *
476476
if !result.GroupReports && importedConfig.GroupReports {
477477
result.GroupReports = true
478478
}
479+
if result.ReportFailureAsIssue == nil && importedConfig.ReportFailureAsIssue != nil {
480+
result.ReportFailureAsIssue = importedConfig.ReportFailureAsIssue
481+
result.ReportFailureAsIssueCategories = importedConfig.ReportFailureAsIssueCategories
482+
result.ReportFailureAsIssueExcludedCategories = importedConfig.ReportFailureAsIssueExcludedCategories
483+
}
479484
if result.FailureIssueRepo == "" && importedConfig.FailureIssueRepo != "" {
480485
result.FailureIssueRepo = importedConfig.FailureIssueRepo
481486
}

pkg/workflow/notify_comment_conclusion_helpers.go

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -311,18 +311,42 @@ func buildAgentFailureReportingPolicyVars(data *WorkflowData) []string {
311311
}
312312
if data.SafeOutputs.ReportFailureAsIssue == nil {
313313
envVars = append(envVars, " GH_AW_FAILURE_REPORT_AS_ISSUE: \"true\"\n")
314-
} else if reportBool, ok := data.SafeOutputs.ReportFailureAsIssue.(bool); ok && !reportBool {
315-
envVars = append(envVars, " GH_AW_FAILURE_REPORT_AS_ISSUE: \"false\"\n")
316314
} else {
317-
envVars = append(envVars, " GH_AW_FAILURE_REPORT_AS_ISSUE: \"true\"\n")
318-
if len(data.SafeOutputs.ReportFailureAsIssueCategories) > 0 {
319-
if categoriesJSON, err := json.Marshal(data.SafeOutputs.ReportFailureAsIssueCategories); err == nil {
320-
envVars = append(envVars, fmt.Sprintf(" GH_AW_FAILURE_CATEGORIES_FILTER: %q\n", string(categoriesJSON)))
315+
appendReportFailureEnvVar := func(enabled bool) {
316+
envVars = append(envVars, fmt.Sprintf(" GH_AW_FAILURE_REPORT_AS_ISSUE: %q\n", strconv.FormatBool(enabled)))
317+
}
318+
shouldIncludeCategoryFilters := true
319+
switch reportSetting := data.SafeOutputs.ReportFailureAsIssue.(type) {
320+
case bool:
321+
appendReportFailureEnvVar(reportSetting)
322+
shouldIncludeCategoryFilters = reportSetting
323+
case string:
324+
reportExpression := reportSetting
325+
switch reportExpression {
326+
case "true":
327+
appendReportFailureEnvVar(true)
328+
case "false":
329+
appendReportFailureEnvVar(false)
330+
shouldIncludeCategoryFilters = false
331+
default:
332+
envVars = append(envVars, buildTemplatableBoolEnvVar("GH_AW_FAILURE_REPORT_AS_ISSUE", &reportExpression)...)
333+
shouldIncludeCategoryFilters = false
321334
}
335+
case []any:
336+
appendReportFailureEnvVar(true)
337+
default:
338+
appendReportFailureEnvVar(true)
322339
}
323-
if len(data.SafeOutputs.ReportFailureAsIssueExcludedCategories) > 0 {
324-
if excludedJSON, err := json.Marshal(data.SafeOutputs.ReportFailureAsIssueExcludedCategories); err == nil {
325-
envVars = append(envVars, fmt.Sprintf(" GH_AW_FAILURE_EXCLUDED_CATEGORIES_FILTER: %q\n", string(excludedJSON)))
340+
if shouldIncludeCategoryFilters {
341+
if len(data.SafeOutputs.ReportFailureAsIssueCategories) > 0 {
342+
if categoriesJSON, err := json.Marshal(data.SafeOutputs.ReportFailureAsIssueCategories); err == nil {
343+
envVars = append(envVars, fmt.Sprintf(" GH_AW_FAILURE_CATEGORIES_FILTER: %q\n", string(categoriesJSON)))
344+
}
345+
}
346+
if len(data.SafeOutputs.ReportFailureAsIssueExcludedCategories) > 0 {
347+
if excludedJSON, err := json.Marshal(data.SafeOutputs.ReportFailureAsIssueExcludedCategories); err == nil {
348+
envVars = append(envVars, fmt.Sprintf(" GH_AW_FAILURE_EXCLUDED_CATEGORIES_FILTER: %q\n", string(excludedJSON)))
349+
}
326350
}
327351
}
328352
}

0 commit comments

Comments
 (0)