From 0b5539147a9005066ab68c244279c3e9bce8ff84 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Feb 2026 15:55:04 +0000 Subject: [PATCH 1/4] Initial plan From c0a09fb40e3ce381e7858b58ecb3dcef13708fd5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Feb 2026 15:59:54 +0000 Subject: [PATCH 2/4] Sort imported/included files for deterministic header output Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/compiler_yaml.go | 13 ++- pkg/workflow/compiler_yaml_test.go | 158 +++++++++++++++++++++++++++++ 2 files changed, 169 insertions(+), 2 deletions(-) diff --git a/pkg/workflow/compiler_yaml.go b/pkg/workflow/compiler_yaml.go index dc1a528015..75fd733835 100644 --- a/pkg/workflow/compiler_yaml.go +++ b/pkg/workflow/compiler_yaml.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "path/filepath" + "sort" "strings" "github.com/github/gh-aw/pkg/constants" @@ -82,7 +83,11 @@ func (c *Compiler) generateWorkflowHeader(yaml *strings.Builder, data *WorkflowD if len(data.ImportedFiles) > 0 { yaml.WriteString("# Imports:\n") - for _, file := range data.ImportedFiles { + // Sort imports for deterministic output + sortedImports := make([]string, len(data.ImportedFiles)) + copy(sortedImports, data.ImportedFiles) + sort.Strings(sortedImports) + for _, file := range sortedImports { cleanFile := stringutil.StripANSIEscapeCodes(file) // Normalize to Unix paths (forward slashes) for cross-platform compatibility cleanFile = filepath.ToSlash(cleanFile) @@ -92,7 +97,11 @@ func (c *Compiler) generateWorkflowHeader(yaml *strings.Builder, data *WorkflowD if len(data.IncludedFiles) > 0 { yaml.WriteString("# Includes:\n") - for _, file := range data.IncludedFiles { + // Sort includes for deterministic output + sortedIncludes := make([]string, len(data.IncludedFiles)) + copy(sortedIncludes, data.IncludedFiles) + sort.Strings(sortedIncludes) + for _, file := range sortedIncludes { cleanFile := stringutil.StripANSIEscapeCodes(file) // Normalize to Unix paths (forward slashes) for cross-platform compatibility cleanFile = filepath.ToSlash(cleanFile) diff --git a/pkg/workflow/compiler_yaml_test.go b/pkg/workflow/compiler_yaml_test.go index 7944031bb8..1dca87813e 100644 --- a/pkg/workflow/compiler_yaml_test.go +++ b/pkg/workflow/compiler_yaml_test.go @@ -1237,3 +1237,161 @@ This is a test workflow.` }) } } + +// TestManifestHeaderOrderingDeterministic tests that imported and included files +// are always rendered in sorted order, regardless of input ordering. +// This ensures deterministic lock file output and prevents noisy diffs. +func TestManifestHeaderOrderingDeterministic(t *testing.T) { + tmpDir := testutil.TempDir(t, "manifest-ordering-test") + + // Create a simple workflow + workflowContent := `--- +name: Test Workflow +on: push +permissions: + contents: read +engine: copilot +strict: false +--- + +# Test Workflow + +Test content.` + + testFile := filepath.Join(tmpDir, "test.md") + if err := os.WriteFile(testFile, []byte(workflowContent), 0644); err != nil { + t.Fatal(err) + } + + // Test multiple orderings of imported and included files + tests := []struct { + name string + importedFiles []string + includedFiles []string + }{ + { + name: "reverse_alphabetical_imports", + importedFiles: []string{"z-file.md", "m-file.md", "a-file.md"}, + includedFiles: []string{}, + }, + { + name: "reverse_alphabetical_includes", + importedFiles: []string{}, + includedFiles: []string{"z-include.md", "m-include.md", "a-include.md"}, + }, + { + name: "mixed_order_both", + importedFiles: []string{"b-import.md", "a-import.md", "c-import.md"}, + includedFiles: []string{"y-include.md", "x-include.md", "z-include.md"}, + }, + { + name: "nested_paths", + importedFiles: []string{"shared/z.md", "common/a.md", "lib/m.md"}, + includedFiles: []string{"tools/y.md", "utils/b.md", "helpers/k.md"}, + }, + } + + // Expected sorted order for each test case + expectedImports := map[string][]string{ + "reverse_alphabetical_imports": {"a-file.md", "m-file.md", "z-file.md"}, + "reverse_alphabetical_includes": {}, + "mixed_order_both": {"a-import.md", "b-import.md", "c-import.md"}, + "nested_paths": {"common/a.md", "lib/m.md", "shared/z.md"}, + } + expectedIncludes := map[string][]string{ + "reverse_alphabetical_imports": {}, + "reverse_alphabetical_includes": {"a-include.md", "m-include.md", "z-include.md"}, + "mixed_order_both": {"x-include.md", "y-include.md", "z-include.md"}, + "nested_paths": {"helpers/k.md", "tools/y.md", "utils/b.md"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + compiler := NewCompiler() + + // Parse the workflow + workflowData, err := compiler.ParseWorkflowFile(testFile) + if err != nil { + t.Fatalf("ParseWorkflowFile() error: %v", err) + } + + // Set imported and included files in the specified (potentially unsorted) order + workflowData.ImportedFiles = tt.importedFiles + workflowData.IncludedFiles = tt.includedFiles + + // Compile with the modified data + if err := compiler.CompileWorkflowData(workflowData, testFile); err != nil { + t.Fatalf("CompileWorkflowData() error: %v", err) + } + + // Read the generated lock file + lockFile := filepath.Join(tmpDir, "test.lock.yml") + content, err := os.ReadFile(lockFile) + if err != nil { + t.Fatalf("Failed to read lock file: %v", err) + } + + lockContent := string(content) + + // Verify imports are in sorted order + if len(tt.importedFiles) > 0 { + expectedImportsList := expectedImports[tt.name] + for i, expected := range expectedImportsList { + expectedLine := fmt.Sprintf("# - %s", expected) + if !strings.Contains(lockContent, expectedLine) { + t.Errorf("Expected to find import '%s' in lock file", expected) + } + + // Verify ordering by checking that each import appears before the next one + if i < len(expectedImportsList)-1 { + nextExpected := expectedImportsList[i+1] + nextLine := fmt.Sprintf("# - %s", nextExpected) + + currentIdx := strings.Index(lockContent, expectedLine) + nextIdx := strings.Index(lockContent, nextLine) + + if currentIdx == -1 { + t.Errorf("Import '%s' not found in lock file", expected) + } + if nextIdx == -1 { + t.Errorf("Import '%s' not found in lock file", nextExpected) + } + if currentIdx != -1 && nextIdx != -1 && currentIdx >= nextIdx { + t.Errorf("Import '%s' should appear before '%s', but found in wrong order", expected, nextExpected) + } + } + } + } + + // Verify includes are in sorted order + if len(tt.includedFiles) > 0 { + expectedIncludesList := expectedIncludes[tt.name] + for i, expected := range expectedIncludesList { + expectedLine := fmt.Sprintf("# - %s", expected) + if !strings.Contains(lockContent, expectedLine) { + t.Errorf("Expected to find include '%s' in lock file", expected) + } + + // Verify ordering by checking that each include appears before the next one + if i < len(expectedIncludesList)-1 { + nextExpected := expectedIncludesList[i+1] + nextLine := fmt.Sprintf("# - %s", nextExpected) + + currentIdx := strings.Index(lockContent, expectedLine) + nextIdx := strings.Index(lockContent, nextLine) + + if currentIdx == -1 { + t.Errorf("Include '%s' not found in lock file", expected) + } + if nextIdx == -1 { + t.Errorf("Include '%s' not found in lock file", nextExpected) + } + if currentIdx != -1 && nextIdx != -1 && currentIdx >= nextIdx { + t.Errorf("Include '%s' should appear before '%s', but found in wrong order", expected, nextExpected) + } + } + } + } + }) + } +} From 3f11b988d4f91e8d86daba2b897b3111bd4a3338 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Feb 2026 16:01:50 +0000 Subject: [PATCH 3/4] Verify deterministic header ordering with tests Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/compiler_yaml_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/workflow/compiler_yaml_test.go b/pkg/workflow/compiler_yaml_test.go index 1dca87813e..3aaab1161e 100644 --- a/pkg/workflow/compiler_yaml_test.go +++ b/pkg/workflow/compiler_yaml_test.go @@ -1293,13 +1293,13 @@ Test content.` // Expected sorted order for each test case expectedImports := map[string][]string{ - "reverse_alphabetical_imports": {"a-file.md", "m-file.md", "z-file.md"}, + "reverse_alphabetical_imports": {"a-file.md", "m-file.md", "z-file.md"}, "reverse_alphabetical_includes": {}, "mixed_order_both": {"a-import.md", "b-import.md", "c-import.md"}, "nested_paths": {"common/a.md", "lib/m.md", "shared/z.md"}, } expectedIncludes := map[string][]string{ - "reverse_alphabetical_imports": {}, + "reverse_alphabetical_imports": {}, "reverse_alphabetical_includes": {"a-include.md", "m-include.md", "z-include.md"}, "mixed_order_both": {"x-include.md", "y-include.md", "z-include.md"}, "nested_paths": {"helpers/k.md", "tools/y.md", "utils/b.md"}, From 8f4ad5e41b23e1b96888900a79977d896729c524 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Feb 2026 16:09:05 +0000 Subject: [PATCH 4/4] Recompile workflows with sorted manifest headers Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/ci-coach.lock.yml | 2 +- .github/workflows/copilot-session-insights.lock.yml | 2 +- .github/workflows/python-data-charts.lock.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci-coach.lock.yml b/.github/workflows/ci-coach.lock.yml index ba414c0c68..d75a6339c2 100644 --- a/.github/workflows/ci-coach.lock.yml +++ b/.github/workflows/ci-coach.lock.yml @@ -25,9 +25,9 @@ # # Resolved workflow manifest: # Imports: +# - shared/ci-data-analysis.md # - shared/ci-optimization-strategies.md # - shared/jqschema.md -# - shared/ci-data-analysis.md # - shared/mood.md # - shared/reporting.md # diff --git a/.github/workflows/copilot-session-insights.lock.yml b/.github/workflows/copilot-session-insights.lock.yml index 82a458ce29..60e81d911b 100644 --- a/.github/workflows/copilot-session-insights.lock.yml +++ b/.github/workflows/copilot-session-insights.lock.yml @@ -25,8 +25,8 @@ # # Resolved workflow manifest: # Imports: -# - shared/jqschema.md # - shared/copilot-session-data-fetch.md +# - shared/jqschema.md # - shared/mood.md # - shared/python-dataviz.md # - shared/reporting.md diff --git a/.github/workflows/python-data-charts.lock.yml b/.github/workflows/python-data-charts.lock.yml index 7c4caa18fb..5ff034bf03 100644 --- a/.github/workflows/python-data-charts.lock.yml +++ b/.github/workflows/python-data-charts.lock.yml @@ -25,10 +25,10 @@ # # Resolved workflow manifest: # Imports: +# - shared/charts-with-trending.md # - shared/mood.md # - shared/python-dataviz.md # - shared/trends.md -# - shared/charts-with-trending.md # # frontmatter-hash: 86d56c5df1106cf5af7c52d643d1ddeb70dea4f6aec028a1e7324c9f0f3db65c