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

Copilot AI 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

github-actions Bot commented Aug 8, 2026

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 ·

Copilot AI and 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>
Copilot AI changed the title [WIP] Fix action-failure expiry not triggering maintenance workflow generation Suppress unenforceable action-failure expiry markers when no maintenance workflow will exist to close them Aug 8, 2026
Copilot AI requested a review from pelikhan August 8, 2026 17:09
@pelikhan
pelikhan marked this pull request as ready for review August 8, 2026 17:12
Copilot AI balanced review requested due to automatic review settings August 8, 2026 17:12
@github-actions

github-actions Bot 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-actions Bot 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-actions Bot 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-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

🧪 Test quality analysis by Test Quality Sentinel

Copilot AI 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
File Description
pkg/workflow/repo_config.go Tracks explicit expiry configuration.
pkg/workflow/repo_config_test.go Tests explicit-setting detection.
pkg/workflow/maintenance_workflow.go Coordinates maintenance generation and marker suppression.
pkg/workflow/maintenance_workflow_test.go Tests expiry scheduling and marker behavior.
docs/src/content/docs/reference/ephemerals.md Documents opt-in expiration semantics.
actions/setup/js/handle_agent_failure.cjs Supports disabled expiration and expired parents.
actions/setup/js/handle_agent_failure.test.cjs Tests 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 thread pkg/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-actions github-actions Bot 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

github-actions Bot commented Aug 8, 2026

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)
Metric Value
Analyzed 16 (Go: 13, JS: 3)
✅ Design 16 (100%)
⚠️ Implementation 0 (0%)
Edge/error coverage 16 (100%)
Duplicate clusters 0
Inflation No (all ratios < 2:1)
🚨 Violations 0
Test File Classification Issues
implicit action-failure default does not trigger maintenance maintenance_workflow_test.go design_test None
explicit action-failure expiry triggers maintenance maintenance_workflow_test.go design_test None
explicit action-failure expiry coexists with shorter safe-output expiry maintenance_workflow_test.go design_test None
explicit action-failure expiry coexists with longer safe-output expiry maintenance_workflow_test.go design_test None
explicit action-failure expiry with no workflows enabling report-as-issue does not trigger maintenance_workflow_test.go design_test None
TestGenerateMaintenanceWorkflow_DisablesImplicitActionFailureExpiryMarker maintenance_workflow_test.go design_test None
TestGenerateMaintenanceWorkflow_PreservesActionFailureExpiryMarkerWhenAnotherSourceTriggersMaintenance maintenance_workflow_test.go design_test None
TestLoadRepoConfig_ActionFailureIssueExpiresNotExplicitWhenUnset repo_config_test.go design_test None
returns 0 (disabled) when the compiler explicitly opts out of expiration handle_agent_failure.test.cjs design_test None
returns default for invalid values handle_agent_failure.test.cjs design_test None
creates a new parent issue when the existing parent issue has expired handle_agent_failure.test.cjs design_test None

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-actions github-actions Bot 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 github-actions Bot 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 thread pkg/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:

var actionFailureExpiryRE = 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 thread pkg/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 {
    return false // no workflows → no failure issues possible
}
return false // 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
let existingBody;
try {
  if (typeof existingIssue.body === "string") {
    existingBody = existingIssue.body;
  } else {
    const issueResult = await github.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
func TestGenerateMaintenanceWorkflow_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>

Copilot AI commented Aug 8, 2026

Copy link
Copy Markdown
Contributor Author

@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

github-actions Bot commented Aug 8, 2026

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 ·

Sign up for free to 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