Skip to content

Suppress unenforceable action-failure expiry markers when no maintenance workflow will exist to close them - #51425

Merged
pelikhan merged 6 commits into
mainfrom
copilot/fix-action-failure-expiry-workflow
Aug 8, 2026
Merged

Suppress unenforceable action-failure expiry markers when no maintenance workflow will exist to close them#51425
pelikhan merged 6 commits into
mainfrom
copilot/fix-action-failure-expiry-workflow

Conversation

CopilotAI commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Action-failure issues get a 168-hour expiration marker regardless of whether agentics-maintenance.yml (the workflow that closes expired issues) is actually generated. scanWorkflowsForExpires only considers explicit safe-output/no-op expiry, so the implicit action-failure default silently produces issues with an expiry deadline nobody enforces. Grouped parent failure issues also carried the marker but their reuse path never checked it.

Compile-time: opt-in maintenance generation

  • RepoConfig now tracks whether maintenance.action_failure_issue_expires was explicitly set in aw.json (vs. left at the implicit default) — required a secondary raw-JSON pass since Go can't distinguish "absent" from "explicit zero" via omitempty.
  • scanWorkflowsForExpires treats an explicit action_failure_issue_expires as an opt-in trigger for agentics-maintenance.yml, folding it into the min-expires scheduling calculation when any workflow may report failures as issues.
  • When no expiry source ends up generating maintenance (implicit-default-only case, or maintenance: false), the already-compiled lock file's GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS is patched from 168 down to 0 — there being no scheduled consumer to enforce it. The marker is left untouched when another expiry source already triggers maintenance.

Runtime: honor disabled state and expire parent issues

  • getActionFailureIssueExpiresHours() in handle_agent_failure.cjs now treats an explicit "0" as "expiration disabled" instead of falling back to the 168h default; only a missing/invalid value falls back.
  • ensureParentIssue() now checks the existing grouped parent issue's expiration marker before reusing it (previously only sub-issue count was checked), falling through to create a new parent chained to the expired one — mirroring the existing per-run issue reuse logic.
{
"maintenance": {
"action_failure_issue_expires": 72
}
}

Setting this explicitly now generates agentics-maintenance.yml if nothing else would; leaving it unset with no other expiring safe output means failure issues are created without a marker instead of an unenforceable one.

Docs

  • ephemerals.md updated to describe the opt-in semantics of action_failure_issue_expires.

Out of scope: side-repository (failure-issue-repo) maintenance coverage is left as a follow-up.


Generated by 👨‍🍳 PR Sous Chef · gpt54 · 13.5 AIC · ⌖ 6.44 AIC · ⊞ 8.5K ·
Comment /souschef to run again

@github-actions

Copy link
Copy Markdown
Contributor

Thanks for the detailed plan on action-failure expiry! 👋 This is a comprehensive issue addressing a real defect in maintenance workflow generation.

Since this is in WIP stage, here's what you'll want to focus on as the implementation progresses:

  • Add test coverage — This is a complex issue affecting maintenance workflow compilation and runtime failure handling. Unit tests should cover the scenarios outlined in your implementation plan:

    1. Implicit default (168h) with no maintenance source → no unenforceable marker
    2. Explicit action_failure_issue_expires value → maintenance generated
    3. Coexistence with shorter/longer safe-output expiry values
    4. Parent and per-run failure issues both closed when cleanup is enabled
  • Verify the implementation aligns with the design constraints — The suggestion to avoid making the implicit 168-hour default an unconditional maintenance trigger is solid and should be preserved in the final solution.

The plan looks well-structured and directly addresses the root causes identified in issue #50923. Once you've implemented the changes and added tests, this should be ready to go!

Generated by ✅ Contribution Check · auto · 66 AIC · ⌖ 3.95 AIC · ⊞ 8.7K ·

CopilotAIand others added 2 commits August 8, 2026 17:07
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
CopilotAI changed the title [WIP] Fix action-failure expiry not triggering maintenance workflow generationSuppress unenforceable action-failure expiry markers when no maintenance workflow will exist to close themAug 8, 2026
CopilotAI requested a review from pelikhanAugust 8, 2026 17:09
@pelikhan
pelikhan marked this pull request as ready for review August 8, 2026 17:12
CopilotAI balanced review requested due to automatic review settings August 8, 2026 17:12
@github-actions

github-actionsBot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Design Decision Gate 🏗️ failed during design decision gate check.

🏗️ ADR gate enforced by Design Decision Gate 🏗️

@github-actions

github-actionsBot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer

@github-actions

github-actionsBot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

PR Code Quality Reviewer completed the code quality review.

Warning

Threat Detection Engine Failure — The analysis engine could not complete. This is a tooling failure, not a security finding.

What happened

The threat detection engine failed to produce results.

Review the workflow run logs for details.

Warning

Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • api.individual.githubcopilot.com

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
allowed:
- defaults
- "api.individual.githubcopilot.com"

See Network Configuration for more information.

🔎 Code quality review by PR Code Quality Reviewer

@github-actions

github-actionsBot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

🧪 Test quality analysis by Test Quality Sentinel

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Aligns action-failure expiration markers with available maintenance cleanup.

Changes:

  • Detects explicit expiry configuration and generates maintenance accordingly.
  • Disables unenforceable markers and handles expired grouped parents.
  • Adds regression tests and documentation.
Show a summary per file
FileDescription
pkg/workflow/repo_config.goTracks explicit expiry configuration.
pkg/workflow/repo_config_test.goTests explicit-setting detection.
pkg/workflow/maintenance_workflow.goCoordinates maintenance generation and marker suppression.
pkg/workflow/maintenance_workflow_test.goTests expiry scheduling and marker behavior.
docs/src/content/docs/reference/ephemerals.mdDocuments opt-in expiration semantics.
actions/setup/js/handle_agent_failure.cjsSupports disabled expiration and expired parents.
actions/setup/js/handle_agent_failure.test.cjsTests runtime expiration behavior.

Review details

Tip

Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

  • Files reviewed: 7/7 changed files
  • Comments generated: 6
  • Review effort level: Balanced


// Scan workflows for expires fields and track the minimum expires value
hasExpires, minExpires, triggerReason := scanWorkflowsForExpires(workflowDataList)
hasExpires, minExpires, triggerReason := scanWorkflowsForExpires(workflowDataList, repoConfig)
Comment threadpkg/workflow/maintenance_workflow.go Outdated
return true
}
}
return !sawAny
// (see scanWorkflowsForExpires), disable the runtime expiration marker in
// the already-compiled lock files so failure issues do not claim an
// expiration that nothing will enforce.
disableDefaultActionFailureExpiryMarkers(workflowDataList, workflowDir)
Comment on lines 71 to 73
const parsed = Number.parseInt(raw, 10);
if (Number.isInteger(parsed) && parsed > 0) {
if (Number.isInteger(parsed) && parsed >= 0) {
return parsed;
Comment on lines 624 to 625
// Fall through to create a new parent issue, passing the previous parent number
previousParentNumber = existingIssue.number;
Comment on lines +525 to +528
content, err := os.ReadFile(lockFile)
if err != nil {
// Lock file may not exist (e.g. --no-emit compiles); nothing to patch.
continue

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The changes are well-structured and the logic is sound.

  • The getActionFailureIssueExpiresHours early-return for empty string before parseInt is a correctness improvement — avoids parseInt("", 10) returning NaN, good.
  • disableDefaultActionFailureExpiryMarkers uses a specific env var string match, making false-positive replacements very unlikely.
  • anyWorkflowMayReportFailureAsIssue conservatively returns true for empty/nil workflow lists — correct safe default.
  • The double-unmarshal in UnmarshalJSON (struct + map for presence detection) is idiomatic and functionally correct.
  • Test coverage is thorough across all new branches.

No blocking issues found.

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · sonnet46 · 29.2 AIC · ⌖ 9.93 AIC · ⊞ 5.5K

@github-actions

Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

Test Quality Score: 92/100 — Excellent

Analyzed 16 test(s): 16 design, 0 implementation, 0 violation(s).

📊 Metrics (16 tests)
MetricValue
Analyzed16 (Go: 13, JS: 3)
✅ Design16 (100%)
⚠️ Implementation0 (0%)
Edge/error coverage16 (100%)
Duplicate clusters0
InflationNo (all ratios < 2:1)
🚨 Violations0
TestFileClassificationIssues
implicit action-failure default does not trigger maintenancemaintenance_workflow_test.godesign_testNone
explicit action-failure expiry triggers maintenancemaintenance_workflow_test.godesign_testNone
explicit action-failure expiry coexists with shorter safe-output expirymaintenance_workflow_test.godesign_testNone
explicit action-failure expiry coexists with longer safe-output expirymaintenance_workflow_test.godesign_testNone
explicit action-failure expiry with no workflows enabling report-as-issue does not triggermaintenance_workflow_test.godesign_testNone
TestGenerateMaintenanceWorkflow_DisablesImplicitActionFailureExpiryMarkermaintenance_workflow_test.godesign_testNone
TestGenerateMaintenanceWorkflow_PreservesActionFailureExpiryMarkerWhenAnotherSourceTriggersMaintenancemaintenance_workflow_test.godesign_testNone
TestLoadRepoConfig_ActionFailureIssueExpiresNotExplicitWhenUnsetrepo_config_test.godesign_testNone
returns 0 (disabled) when the compiler explicitly opts out of expirationhandle_agent_failure.test.cjsdesign_testNone
returns default for invalid valueshandle_agent_failure.test.cjsdesign_testNone
creates a new parent issue when the existing parent issue has expiredhandle_agent_failure.test.cjsdesign_testNone

Test Quality Highlights

Strengths:

  • Comprehensive design coverage: All 16 tests verify behavioral contracts, not implementation details
  • Edge-case coverage: Implicit vs explicit config, coexistence logic, disabled features, expiry boundaries
  • Integration tests: File I/O, YAML transformation, GitHub API interaction with mock verification
  • No test inflation: All test:prod ratios well under 2:1 threshold
  • Clean patterns: Descriptive assertions with contextual failure messages; no mock-library violations

Key regression protections:

  • Implicit 168-hour action-failure markers are suppressed when no maintenance workflow will enforce them
  • Explicit markers survive when another expiry source triggers maintenance (generic close-expired-issues sweeper)
  • Expired parent issues trigger creation of new parent, preventing stale-issue reuse
  • Config explicitly differentiates implicit (from compiler default) vs explicit (from aw.json) settings

Verdict

Passed. 0% implementation tests (threshold: 30%). Test Quality Score: 92/100 (Excellent).

🧪 Test quality analysis by Test Quality Sentinel · haiku45 · 23.7 AIC · ⌖ 5.12 AIC · ⊞ 7.7K ·
Comment /review to run again

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ Test Quality Sentinel: 92/100 (Excellent). 0% implementation tests (threshold: 30%). All 16 tests are design-level behavioral contracts with comprehensive edge-case coverage and no violations.

github-actions[bot]
github-actionsBot previously requested changes Aug 8, 2026

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Skills-Based Review 🧠

Applied /diagnosing-bugs and /tdd — requesting changes on three targeted issues.

📋 Key Themes & Highlights

Key Themes

  • Lock-file patch fragility: strings.ReplaceAll is coupled to the exact numeric value of DefaultActionFailureIssueExpiresHours; a regex match on any non-zero value would be more robust.
  • Empty workflow list edge case: anyWorkflowMayReportFailureAsIssue returns true for an empty list, so an explicit action_failure_issue_expires with no workflows would incorrectly trigger maintenance generation.
  • Unhandled network error in parent-issue body fetch: github.rest.issues.get inside ensureParentIssue has no try/catch; a transient error aborts grouped-issue handling entirely.
  • Missing test for handleMaintenanceDisabled patch path: disableDefaultActionFailureExpiryMarkers is called from two code sites but only one is covered end-to-end.

Positive Highlights

  • ✅ Excellent compile/runtime separation: the compiler patches 0 into lock files and the runtime honours it — avoids runtime config lookups.
  • ✅ The ActionFailureIssueExpiresExplicit approach (double-unmarshal to detect key presence) is clean and minimally invasive.
  • ✅ Strong test coverage added for scanWorkflowsForExpires opt-in semantics — all four combination cases are exercised.
  • ✅ Parent-issue expiry check correctly short-circuits before the graphql sub-issue count query, reducing API calls on the expired path.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · sonnet46 · 54.2 AIC · ⌖ 8.03 AIC · ⊞ 7.1K
Comment /matt to run again

Comment threadpkg/workflow/maintenance_workflow.go Outdated
if !strings.Contains(string(content), defaultLine) {
continue
}
updated := strings.ReplaceAll(string(content), defaultLine, disabledLine)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[/diagnosing-bugs]strings.ReplaceAll matches the exact string GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "168". If DefaultActionFailureIssueExpiresHours ever changes, lock files compiled with the old default will silently be missed — no patch, no warning, unenforceable marker lives on.

💡 Suggestion

Match the non-zero value with a regex instead:

varactionFailureExpiryRE=regexp.MustCompile(`GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "[1-9]\d*"`)
// ...updated:=actionFailureExpiryRE.ReplaceAllString(string(content), `GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "0"`)

This patches any positive default regardless of its numeric value, and removes the coupling to DefaultActionFailureIssueExpiresHours at patch-time.

@copilot please address this.

Comment threadpkg/workflow/maintenance_workflow.go Outdated
return true
}
}
return !sawAny

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[/diagnosing-bugs]anyWorkflowMayReportFailureAsIssue returns !sawAny (i.e. true) when the list is empty. An empty workflowDataList passed to scanWorkflowsForExpires would therefore treat an explicit action_failure_issue_expires as an opt-in trigger even though there are no workflows to create failure issues — generating a maintenance workflow for no reason.

💡 Suggestion

Return false (not true) when sawAny is false — an empty list cannot produce failure issues:

if!sawAny {
returnfalse// no workflows → no failure issues possible
}
returnfalse// all workflows explicitly disabled

Add a unit test: scanWorkflowsForExpires(nil, repoConfigWithExplicit) should return hasExpires=false.

@copilot please address this.

if (subIssueCount !== null && subIssueCount >= MAX_SUB_ISSUES) {
core.warning(`Parent issue #${existingIssue.number} has ${subIssueCount} sub-issues (max: ${MAX_SUB_ISSUES})`);
core.info(`Creating a new parent issue (previous parent #${existingIssue.number} is full)`);
if (parentExpirationDate && parentExpirationDate.getTime() <= Date.now()) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[/diagnosing-bugs] The expiration check uses <= Date.now() (expired at or before now), which is correct. However, the branch that fetches the full issue body on a truncated search result has no error handling — if github.rest.issues.get throws (e.g. rate-limit, network error), the exception propagates uncaught up through ensureParentIssue, potentially aborting the entire grouped-issue flow rather than gracefully falling back to creating a new parent.

💡 Suggestion
letexistingBody;try{if(typeofexistingIssue.body==="string"){existingBody=existingIssue.body;}else{constissueResult=awaitgithub.rest.issues.get({ owner, repo,issue_number: existingIssue.number});existingBody=issueResult.data.body||"";}}catch(err){core.warning(`Could not fetch body for parent issue #${existingIssue.number}: ${err.message}. Treating as unexpired.`);existingBody="";}

Treating a fetch failure as "not expired" is conservative and mirrors the existing pattern of falling through to sub-issue count checks when data is unavailable.

@copilot please address this.

require.Contains(t, string(preserved), `GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "168"`)
}

func TestGenerateMaintenanceWorkflow_CreatesWorkflowDirRecursively(t *testing.T) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[/tdd] The new test TestGenerateMaintenanceWorkflow_DisablesImplicitActionFailureExpiryMarker only verifies the no-maintenance path. A corresponding test for the case where maintenance: false is explicitly set (the handleMaintenanceDisabled code path) is missing — disableDefaultActionFailureExpiryMarkers is called from two sites but only one is tested end-to-end.

💡 Suggested test outline
funcTestGenerateMaintenanceWorkflow_DisablesImplicitMarkerWhenMaintenanceDisabled(t*testing.T) {
tmpDir:=t.TempDir()
// write lock file with implicit 168h marker// call GenerateMaintenanceWorkflow with RepoConfig{Maintenance: {Disabled: true}}// assert marker patched to "0"// assert agentics-maintenance.yml does not exist
}

This closes the gap and ensures both call sites of disableDefaultActionFailureExpiryMarkers are covered.

@copilot please address this.

@gh-aw-bot

Copy link
Copy Markdown
Collaborator

@copilot Quick triage nudge for this PR.

Please refresh the branch if needed, re-check the remaining maintainer-facing feedback, run the pr-finisher skill, and push follow-up fixes.

Open review context (newest first):

  • github-actions — dismissed skills review still highlights follow-up worth confirming on the current head: make the lock-file expiry patch less brittle than a fixed numeric replacement, verify the explicit-expiry + empty-workflow-list path does not generate maintenance unnecessarily, add coverage for the second maintenance-disabled patch path, and ensure transient parent-issue fetch failures do not abort grouped handling.

Run: https://github.com/github/gh-aw/actions/runs/31269773406

Generated by 👨‍🍳 PR Sous Chef · gpt54 · 13.5 AIC · ⌖ 6.44 AIC · ⊞ 8.5K ·
Comment /souschef to run again

Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>

CopilotAI commented Aug 8, 2026

Copy link
Copy Markdown
ContributorAuthor

@copilot Quick triage nudge for this PR.

Please refresh the branch if needed, re-check the remaining maintainer-facing feedback, run the pr-finisher skill, and push follow-up fixes....

Addressed in 9300e4d520.

Follow-up fixes pushed:

  • made lock-file expiry suppression resilient to any positive GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS value (not a fixed numeric string)
  • prevented explicit action-failure expiry from triggering maintenance when workflow list is empty
  • added coverage for the maintenance-disabled marker-suppression path
  • hardened grouped-parent handling so transient parent-body fetch failures warn and continue instead of aborting grouped flow

Validation run locally: make fmt, targeted Go/JS tests, TEST_UNIT_RUN_FULL=1 make test-unit, make test, and BASE_REF=origin/main make agent-report-progress.

@github-actions

Copy link
Copy Markdown
Contributor

PR Triage

  • Category: feature (maintenance/expiry logic)
  • Risk: medium
  • Score: 65/100 (Impact 28 + Urgency 19 + Quality 18)
  • Recommendation:fast_track

Addresses unenforceable action-failure expiry markers when no maintenance workflow exists to close them; adds compile-time opt-in logic and runtime fixes with new regression tests + docs update. Sizable diff (568/-). CI mostly green, some jobs still in progress at triage time. AI reviewer approved (one prior review dismissed).

Generated by 🔧 PR Triage Agent · auto · 32.2 AIC · ⌖ 2.68 AIC · ⊞ 8K ·

@github-actions

Copy link
Copy Markdown
Contributor

🎉 This pull request is included in a new release.

Release: v0.86.2

kubaflo added a commit to dotnet/maui that referenced this pull request Aug 20, 2026
<!-- Please let the below note in for people that find this PR -->
> [!NOTE]
> Are you waiting for the changes in this PR to be merged?
> It would be very helpful if you could [test the resulting
artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from
this PR and let us know in a comment if this change resolves your issue.
Thank you!
## Summary
- regenerate all 14 Copilot-based agentic workflows with gh-aw v0.86.2
- stage the installed Copilot CLI in a portable runner-local path
instead of invoking `/usr/local/bin/copilot`
- explicitly disable shell access in the automatic issue/PR labeler, as
required when `min-integrity: none`
- adopt gh-aw v0.86.2's intentional disabled-expiry sentinel for
action-failure issues when no maintenance workflow exists
## Root cause
The failing runs install Copilot CLI successfully, but gh-aw v0.85.4
then invokes `/usr/local/bin/copilot`. That path no longer exists on the
hosted runner, so the engine exits with `ENOENT` before producing output
or making a model request. gh-aw v0.86.2 includes the upstream
path-portability fix and resolves the installed executable from `PATH`.
The same generated path affected every Copilot workflow, including the
labeler, whose failure reporting is intentionally disabled.
Regeneration also incorporates
[github/gh-aw#51425](github/gh-aw#51425).
Because this repository does not generate an `agentics-maintenance.yml`
consumer for the implicit action-failure expiry, all 14 lockfiles now
set `GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS` to `"0"`. In gh-aw
v0.86.2, explicit `0` means expiration is disabled; it prevents
unenforceable expiry markers and does not make failure issues expire
immediately.
## Validation
- `gh aw compile --strict --validate --actionlint --no-emit`
- verified all 14 lockfiles use gh-aw v0.86.2 and portable Copilot
staging
- verified no generated workflow references `/usr/local/bin/copilot`
- verified all 14 generated locks use gh-aw's `"0"` disabled-expiry
sentinel consistently
No existing open MAUI PR addresses this gh-aw/Copilot path failure.
Fixes#37430Fixes#37431Fixes#37432Fixes#37435Fixes#37442
---------
Co-authored-by: Copilot CI <copilot-ci@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 74cff858-a5a6-486e-a1d3-1d516fc3bdaa
Copilot-Session: d00747b7-96f3-4e7a-8dfb-e3a48db04b2d
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Action-failure expiry does not trigger maintenance workflow generation

4 participants

@gh-aw-bot@pelikhan