Skip to content

Prevent compile panic when workflow directory contains only shared components#33029

Merged
pelikhan merged 4 commits into
mainfrom
copilot/aw-compat-fix-nil-pointer-panic
May 18, 2026
Merged

Prevent compile panic when workflow directory contains only shared components#33029
pelikhan merged 4 commits into
mainfrom
copilot/aw-compat-fix-nil-pointer-panic

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented May 18, 2026

gh aw compile/--strict could panic in maintenance post-processing when every .md file was a shared component (no top-level on:), because nil workflow entries were dereferenced. This change makes shared-only directories a clean, non-panicking compile path.

  • Post-processing guard (compile pipeline)

    • Stop propagating nil WorkflowData from per-file compilation results.
    • Gate maintenance generation on actual compiled workflow data (len(workflowDataList) > 0) so shared-only directories do not enter maintenance scanning.
  • Defensive nil handling (maintenance workflow)

    • Add nil guards in scanWorkflowsForExpires and handleMaintenanceDisabled.
    • Resolve setup-action pinning from the first non-nil workflow entry with a resolver, rather than assuming index 0 is safe.
  • Regression coverage

    • Add a compile regression test for a shared-only workflow directory, covering both strict and non-strict modes, ensuring no panic and no maintenance workflow emission.
// compile_pipeline.go
if fileResult.workflowData != nil {
    workflowDataList = append(workflowDataList, fileResult.workflowData)
}

if !config.NoEmit && config.WorkflowDir == "" && len(workflowDataList) > 0 {
    // generate maintenance workflow
}

// maintenance_workflow.go
for _, workflowData := range workflowDataList {
    if workflowData == nil || workflowData.SafeOutputs == nil {
        continue
    }
    // expires scan logic...
}

Copilot AI and others added 2 commits May 18, 2026 13:16
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 nil pointer panic in scanWorkflowsForExpires Prevent compile panic when workflow directory contains only shared components May 18, 2026
Copilot AI requested a review from pelikhan May 18, 2026 13:19
@pelikhan pelikhan marked this pull request as ready for review May 18, 2026 13:21
Copilot AI review requested due to automatic review settings May 18, 2026 13:21
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes a panic in gh aw compile --strict when compiling a workflow directory that contains only shared Markdown components (i.e., no compiled top-level workflows), by preventing nil workflow entries from being dereferenced during maintenance post-processing.

Changes:

  • Filter out nil WorkflowData results during compilation so downstream steps don’t receive nil entries.
  • Add defensive nil checks in maintenance workflow scanning and resolver selection.
  • Add a regression test ensuring shared-only workflow directories compile (strict + non-strict) without panicking and do not emit a maintenance workflow.
Show a summary per file
File Description
pkg/workflow/maintenance_workflow.go Adds nil-safe iteration for resolver selection and expires scanning/disabled handling.
pkg/cli/compile_pipeline.go Avoids appending nil workflow results and gates directory post-processing generation.
pkg/cli/compile_maintenance_test.go Adds regression test for shared-only workflow directory compilation (strict/non-strict).

Copilot's findings

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 3/3 changed files
  • Comments generated: 3

Comment thread pkg/cli/compile_pipeline.go Outdated
Comment on lines +542 to +543
// Also skip when no compiled workflow data is available (for example shared-only directories).
if !config.NoEmit && config.WorkflowDir == "" && len(workflowDataList) > 0 {
Comment on lines 244 to 249
minExpires := 0 // Track minimum expires value in hours

for _, workflowData := range workflowDataList {
if workflowData.SafeOutputs == nil {
if workflowData == nil || workflowData.SafeOutputs == nil {
continue
}
Comment on lines +252 to +314
func TestCompileOnlySharedWorkflow_DoesNotPanic(t *testing.T) {
// Create temporary directory structure
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)
}

// Change to temp directory
originalDir, _ := os.Getwd()
defer os.Chdir(originalDir)
if err := os.Chdir(tempDir); err != nil {
t.Fatalf("Failed to change directory: %v", err)
}

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

// Create a shared workflow component (missing top-level "on")
sharedWorkflowContent := `---
description: "Shared Component"
engine: copilot
---

Shared workflow component.
`
sharedWorkflowPath := filepath.Join(workflowsDir, "shared-component.md")
if err := os.WriteFile(sharedWorkflowPath, []byte(sharedWorkflowContent), 0644); err != nil {
t.Fatalf("Failed to write shared workflow file: %v", err)
}

for _, strict := range []bool{false, true} {
t.Run("strict="+strconv.FormatBool(strict), func(t *testing.T) {
config := CompileConfig{
MarkdownFiles: []string{},
Verbose: false,
EngineOverride: "",
Validate: false,
Watch: false,
WorkflowDir: "",
SkipInstructions: false,
NoEmit: false,
Purge: false,
TrialMode: false,
TrialLogicalRepoSlug: "",
Strict: strict,
}

if _, err := CompileWorkflows(context.Background(), config); err != nil {
t.Fatalf("CompileWorkflows should succeed for shared-only workflow directory: %v", err)
}
})
}

maintenancePath := filepath.Join(workflowsDir, "agentics-maintenance.yml")
if _, err := os.Stat(maintenancePath); !os.IsNotExist(err) {
t.Error("Maintenance workflow should not be generated for shared-only workflow directory")
}
}
@github-actions github-actions Bot mentioned this pull request May 18, 2026
@github-actions
Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

Test Quality Score: 80/100

Excellent

Metric Value
New/modified tests analyzed 1 (TestCompileOnlySharedWorkflow_DoesNotPanic, 2 sub-cases via t.Run)
✅ Design tests (behavioral contracts) 1 (100%)
⚠️ Implementation tests (low value) 0 (0%)
Tests with error/edge cases 1 (100%)
Duplicate test clusters 0
Test inflation detected ⚠️ Yes — 65 test lines added vs ~19 production lines (~3.4:1, threshold 2:1)
🚨 Coding-guideline violations None

Test Classification Details

Test File Classification Issues Detected
TestCompileOnlySharedWorkflow_DoesNotPanic pkg/cli/compile_maintenance_test.go:251 ✅ Design Test inflation (3.4:1 ratio); see note below

Test Inflation Note

compile_maintenance_test.go added 65 lines while the two production files together added ~19 lines (compile_pipeline.go +10, maintenance_workflow.go +9), yielding a ratio of ~3.4:1 (threshold: 2:1).

This is acceptable here: the test requires substantial scaffolding (creating a temp dir, initialising a git repo, writing a fixture file, and changing directory) that dwarfs the single assertion being made. The scaffolding is necessary for a realistic end-to-end exercise of the compile pipeline. No action is required, but it is noted per scoring rules.


Analysis

TestCompileOnlySharedWorkflow_DoesNotPanic directly verifies the behavioral contract introduced by this PR:

When a workflow directory contains only shared components (no on: trigger), CompileWorkflows must not panic and must not generate a maintenance workflow.

  • Design invariant enforced: The system's panic-free contract and the maintenance-workflow generation guard. Both are observable by callers of CompileWorkflows.
  • Value if deleted: A regression that re-introduces the nil-pointer panic would go undetected. This is precisely the kind of test that catches the bug reported in the PR.
  • Edge case coverage: The test exercises a boundary condition (shared-only directory) with both strict=false and strict=true, covering two distinct execution paths.
  • Assertion messages: All t.Fatalf/t.Error calls include descriptive context — guideline followed ✅.
  • No mocks — tests real component interactions ✅.
  • Build tag present (//go:build !integration) ✅.

Verdict

Check passed. 0% of new tests are implementation tests (threshold: 30%). The single added test is a high-value behavioral contract test that directly guards the regression fixed in this PR.


📖 Understanding Test Classifications

Design Tests (High Value) verify what the system does:

  • Assert on observable outputs, return values, or state changes
  • Cover error paths and boundary conditions
  • Would catch a behavioral regression if deleted
  • Remain valid even after internal refactoring

Implementation Tests (Low Value) verify how the system does it:

  • Assert on internal function calls (mocking internals)
  • Only test the happy path with typical inputs
  • Break during legitimate refactoring even when behavior is correct
  • Give false assurance: they pass even when the system is wrong

Goal: Shift toward tests that describe the system's behavioral contract — the promises it makes to its users and collaborators.

References: §26036151113

🧪 Test quality analysis by Test Quality Sentinel · ● 5.1M ·

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

✅ Test Quality Sentinel: 80/100. Test quality is excellent — 0% of new tests are implementation tests (threshold: 30%). The single added test directly guards the shared-only-directory panic regression with both strict and non-strict execution paths covered.

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

Skills-Based Review 🧠

Applied /diagnose and /tdd — this is a targeted bug fix with regression coverage, which is exactly the right combination to evaluate.

Key Themes

  • Test naming (/tdd): The test name references the internal symptom (panic) rather than the behavioral contract being enforced. Small rename makes the intent clearer.
  • Missing coverage of compileSpecificFiles (/diagnose): Both code paths received the same nil guard fix, but the test only exercises the directory-scan path. The explicit-files path goes untested.
  • Comment accuracy (/diagnose): The new comment in GenerateMaintenanceWorkflow implies the pipeline can pass nil entries, but the upstream fix now prevents that. The comment should clarify it's defensive-only.

Positive Highlights

  • ✅ The root cause is properly addressed at the right level — nil entries are filtered out upstream in the pipeline, rather than relying solely on downstream guards
  • ✅ The len(workflowDataList) > 0 guard is clean and obvious — a reader immediately understands why maintenance generation is skipped
  • ✅ Regression test exercises CompileWorkflows through the real public interface (not a mock), which is exactly the right seam per /diagnose Phase 5
  • ✅ Both strict=false and strict=true variants are tested in a single test function

Verdict

No blocking issues — the fix is correct and the test coverage is good. The suggestions above are improvements worth considering, particularly the missing compileSpecificFiles test case.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · ● 5.6M

}
}

func TestCompileOnlySharedWorkflow_DoesNotPanic(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 test name TestCompileOnlySharedWorkflow_DoesNotPanic describes an implementation artifact (panic) rather than the observable behavior being specified. Per TDD principles, test names should read as specifications.

Consider renaming to something like TestSharedOnlyDirectory_CompilesSuccessfullyWithNoMaintenanceWorkflow — this tells the next developer exactly what contract is being enforced without referencing internal crash behavior.

Verbose: false,
EngineOverride: "",
Validate: false,
Watch: false,
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.

[/diagnose] The compileSpecificFiles code path (triggered when MarkdownFiles is non-empty) received the same nil guard fix as compileAllFilesInDirectory, but this test only exercises the directory-scan path (MarkdownFiles: []string{}).

Consider adding a second sub-test that passes the shared component path explicitly:

config := CompileConfig{
    MarkdownFiles: []string{sharedWorkflowPath},
    // ...
}

Without this, the compileSpecificFiles fix is untested and could regress silently.

// Get the setup action reference (local or remote based on mode).
// Use the first available WorkflowData's ActionResolver to enable SHA pinning.
// Computed early so it is available in the !hasExpires path for side-repo workflows.
// Iterate to find the first non-nil entry because shared-only compilation paths
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.

[/diagnose] The comment says "may provide nil placeholders" but the upstream fix in compile_pipeline.go now actively filters out nil entries before calling this function, and the len(workflowDataList) > 0 guard prevents the call entirely for shared-only directories.

The nil loop here is therefore purely defensive (guarding against direct callers bypassing the pipeline). Consider wording the comment to reflect that:

// Defensive nil check: callers outside the standard pipeline may pass nil entries.

This avoids implying the pipeline itself passes nils, which is no longer accurate.

@pelikhan
Copy link
Copy Markdown
Collaborator

@copilot review all comments, lint go

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented May 18, 2026

@copilot review all comments, lint go

Addressed actionable review feedback in commit e28374d:

  • restored maintenance/centralized post-processing execution for empty workflow sets so stale generated files are cleaned up
  • made side-repo target collection nil-safe
  • expanded regression coverage to assert stale agentics-maintenance.yml and agentic_commands.yml are removed in shared-only directories

I also ran Go lint (make golint). It currently fails on pre-existing unrelated issues in pkg/workflow/gh_cli_permissions.go (gofmt) and pkg/parser/mcp.go (perfsprint).

@pelikhan pelikhan merged commit 9bea11f into main May 18, 2026
@pelikhan pelikhan deleted the copilot/aw-compat-fix-nil-pointer-panic branch May 18, 2026 13:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[aw-compat] Compiler panic: nil pointer in scanWorkflowsForExpires when directory contains only shared workflows

3 participants