Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
57 changes: 57 additions & 0 deletions pkg/cli/compile_maintenance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,63 @@ Test workflow that creates issues without expiration.
}
}

func TestCompileSpecificFiles_PreservesDisabledImplicitActionFailureExpiry(t *testing.T) {
tempDir := testutil.TempDir(t, "test-*")
workflowsDir := filepath.Join(tempDir, ".github/workflows")
if err := os.MkdirAll(workflowsDir, 0755); err != nil {
t.Fatalf("Failed to create workflows directory: %v", err)
}

t.Chdir(tempDir)

initCmd := exec.Command("git", "init")
initCmd.Dir = tempDir
if err := initCmd.Run(); err != nil {
t.Fatalf("Failed to initialize git repo: %v", err)
}

workflowContent := `---
name: "Test Workflow No Expires"
on:
workflow_dispatch:
engine: copilot
---

Test workflow without expiration.
`
workflowPath := filepath.Join(workflowsDir, "test-no-expires.md")
if err := os.WriteFile(workflowPath, []byte(workflowContent), 0644); err != nil {
t.Fatalf("Failed to write workflow file: %v", err)
}

config := CompileConfig{}
if _, err := CompileWorkflows(context.Background(), config); err != nil {
t.Fatalf("Full CompileWorkflows failed: %v", err)
}

lockPath := filepath.Join(workflowsDir, "test-no-expires.lock.yml")
fullCompileContent, err := os.ReadFile(lockPath)
if err != nil {
t.Fatalf("Failed to read full compile output: %v", err)
}
if !strings.Contains(string(fullCompileContent), `GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "0"`) {
t.Fatal("Full compile should disable the implicit action failure expiry")
}

config.MarkdownFiles = []string{"test-no-expires"}
if _, err := CompileWorkflows(context.Background(), config); err != nil {
t.Fatalf("Targeted CompileWorkflows failed: %v", err)
}

targetedCompileContent, err := os.ReadFile(lockPath)
if err != nil {
t.Fatalf("Failed to read targeted compile output: %v", err)
}
if string(targetedCompileContent) != string(fullCompileContent) {
t.Fatal("Targeted compile should produce the same lock file as a full compile")
}
}

func TestCompileWithCustomDir_SkipsMaintenanceWorkflow(t *testing.T) {
// Create temporary directory structure
tempDir := testutil.TempDir(t, "test-*")
Expand Down
6 changes: 6 additions & 0 deletions pkg/cli/compile_pipeline.go
Original file line number Diff line number Diff line change
Expand Up @@ -861,6 +861,12 @@ func runPostProcessing(
// Note: Maintenance workflow generation requires parsing all workflows in the directory
// to check for expires fields, so we skip it when compiling specific files to avoid
// unnecessary parsing and warnings from unrelated workflows
if !config.NoEmit && config.WorkflowDir == "" {

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.

pkg/cli/compile_pipeline.go:864: yagni: one-call helper for a tiny branch. Inline the file-existence check in runPostProcessing and call disableDefaultActionFailureExpiryMarkers directly.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

In 85e8e16 the branch grew beyond a tiny file-existence check (it now also verifies close-expired-entities isn't disabled and that maintenance isn't explicitly off), so it's kept as a named helper (isActionFailureExpiryEnforced) rather than inlined.

if gitRoot, err := gitutil.FindGitRoot(); err == nil {
absWorkflowDir := getAbsoluteWorkflowDir(getWorkflowsDir(), gitRoot)
workflow.DisableDefaultActionFailureExpiryMarkersIfUnenforced(workflowDataList, absWorkflowDir)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 85e8e16: reconciliation now uses the actual emitted lock file path collected per compiled workflow (fileResult.lockFile) instead of reconstructing workflowDir + WorkflowID, so direct-path targets outside .github/workflows are patched correctly and unrelated lock files sharing the same workflow ID are left untouched.

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.

WorkflowDir == "" is not enough to identify the emitted lock file here: targeted compilation accepts direct paths outside .github/workflows, but this helper always rewrites <gitRoot>/.github/workflows/<workflowID>.lock.yml. That means custom/foo.md still keeps its stale marker while an unrelated .github/workflows/foo.lock.yml can be mutated instead.

💡 Reconcile the actual emitted lock path instead of reconstructing one from the workflow ID

The compile path already knows each generated lock filename (compileWorkflowFileResult.lockFile). Thread those concrete paths into post-processing, or filter targeted reconciliations to workflows whose resolved source lives under the default workflow directory before touching any lock file. That keeps the full compile and targeted compile outputs aligned without risking cross-directory corruption.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 85e8e16: compileSpecificFiles now accumulates compiledLockFiles from fileResult.lockFile and threads it into runPostProcessing/DisableDefaultActionFailureExpiryMarkersIfUnenforced, so the actual emitted lock path is reconciled. Covered by TestCompileSpecificFiles_PatchesDirectPathOutsideDefaultWorkflowDir.

}
}

// Prune stale gh-aw-actions entries before saving
pruneStaleActionCacheEntries(compiler, actionCache)
Expand Down
15 changes: 15 additions & 0 deletions pkg/workflow/maintenance_workflow_expiry.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package workflow

import (
"os"
"path/filepath"
)

// DisableDefaultActionFailureExpiryMarkersIfUnenforced disables implicit
// action-failure expiry markers when no maintenance workflow exists to enforce them.
// The generated maintenance workflow always includes the global expiry sweeper.
func DisableDefaultActionFailureExpiryMarkersIfUnenforced(workflowDataList []*WorkflowData, workflowDir string) {
if _, err := os.Stat(filepath.Join(workflowDir, "agentics-maintenance.yml")); os.IsNotExist(err) {

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.

File existence is the wrong enforcement check. agentics-maintenance.yml can already exist while maintenance.disabled_jobs removes close-expired-entities, which means no expiry sweeper is generated even though this helper preserves a positive expiration marker.

💡 Base the decision on whether maintenance would actually emit the expiry jobs

Use the same repo-config logic that scanWorkflowsForExpires and maintenance generation use, or pass enough provenance into this helper to tell whether the close-expired jobs are enabled. Otherwise targeted compile still advertises expiration that no workflow will ever enforce.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 85e8e16: isActionFailureExpiryEnforced now checks repoConfig.Maintenance.IsJobDisabled("close-expired-entities") and maintenance: false in addition to file existence. Covered by TestCompileSpecificFiles_DisablesExpiryWhenCloseExpiredJobDisabled.

disableDefaultActionFailureExpiryMarkers(workflowDataList, workflowDir)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 85e8e16: isActionFailureExpiryEnforced now also checks repoConfig.Maintenance.IsJobDisabled("close-expired-entities") (and maintenance: false) in addition to file existence, so a maintenance file that omits the sweeper no longer preserves the marker.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 85e8e16: DisableDefaultActionFailureExpiryMarkersIfUnenforced now takes *RepoConfig and returns early when repoConfig.IsActionFailureIssueExpiresExplicit() is true, so an explicit maintenance.action_failure_issue_expires value is never rewritten. Added TestCompileSpecificFiles_PreservesExplicitActionFailureExpiry to cover this.

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.

This helper still rewrites every positive GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS value to "0", so a targeted compile can silently erase an explicit maintenance.action_failure_issue_expires opt-in when no maintenance file is present yet. Full compile keeps that explicit value because it generates maintenance, so the deterministic-output bug remains and user configuration gets discarded.

💡 Only disable the implicit default marker, never an explicit configured expiry

The helper needs provenance, not just the rendered lock text. Thread the repo config or an explicit/implicit flag through post-processing, then rewrite only lock files whose expiration came from the implicit 168-hour default. Add a regression that compiles with aw.json setting maintenance.action_failure_issue_expires and verifies targeted compile preserves that explicit value.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 85e8e16: DisableDefaultActionFailureExpiryMarkersIfUnenforced now accepts *RepoConfig and skips rewriting entirely when repoConfig.IsActionFailureIssueExpiresExplicit() is true. Added TestCompileSpecificFiles_PreservesExplicitActionFailureExpiry (aw.json with maintenance.action_failure_issue_expires: 48, no pre-existing maintenance file) to verify targeted compile preserves the explicit value.

}
}
Loading