From 9b122b997fcfd8e4c3b3d969ef831771826d1840 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Feb 2026 13:07:13 +0000 Subject: [PATCH 1/2] Initial plan From 4e6974edf370a00e789370f723f11db79da4a294 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Feb 2026 13:23:20 +0000 Subject: [PATCH 2/2] Stabilize frontmatter hash across LF/CRLF newline conventions Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/parser/frontmatter_hash.go | 4 + .../frontmatter_hash_consistency_test.go | 160 ++++++++++++++++++ 2 files changed, 164 insertions(+) diff --git a/pkg/parser/frontmatter_hash.go b/pkg/parser/frontmatter_hash.go index 71e1d64031d..aa13113a3c0 100644 --- a/pkg/parser/frontmatter_hash.go +++ b/pkg/parser/frontmatter_hash.go @@ -416,6 +416,10 @@ func extractRelevantTemplateExpressions(markdown string) []string { // extractFrontmatterAndBodyText extracts frontmatter as raw text without parsing YAML // Returns: frontmatterText, markdownBody, error func extractFrontmatterAndBodyText(content string) (string, string, error) { + // Normalize CRLF to LF so that files with Windows line-endings produce the + // same frontmatter text (and therefore the same hash) as equivalent LF files. + content = strings.ReplaceAll(content, "\r\n", "\n") + lines := strings.Split(content, "\n") // Check if content starts with frontmatter delimiter diff --git a/pkg/parser/frontmatter_hash_consistency_test.go b/pkg/parser/frontmatter_hash_consistency_test.go index 3f501e612e2..0ed3391d0b4 100644 --- a/pkg/parser/frontmatter_hash_consistency_test.go +++ b/pkg/parser/frontmatter_hash_consistency_test.go @@ -433,6 +433,166 @@ engine: copilot } } +// TestHashConsistency_LFvsCRLF validates that workflow files with identical content but +// different newline conventions (LF vs CRLF) produce the same frontmatter hash. +// This is a regression test for cross-platform hash stability. +func TestHashConsistency_LFvsCRLF(t *testing.T) { + tempDir := t.TempDir() + cache := NewImportCache("") + + testCases := []struct { + name string + content string // LF version; CRLF version is derived automatically + }{ + { + name: "simple frontmatter", + content: `--- +engine: copilot +description: Test workflow +on: + schedule: daily +--- + +# Test Workflow +`, + }, + { + name: "complex frontmatter", + content: `--- +engine: claude +description: Complex workflow +tracker-id: complex-test +timeout-minutes: 30 +on: + schedule: daily + workflow_dispatch: true +permissions: + contents: read + actions: read +tools: + playwright: + version: v1.41.0 +labels: + - test + - complex +bots: + - copilot +--- + +# Complex Workflow +`, + }, + { + name: "frontmatter with env template expressions", + content: `--- +engine: copilot +description: Workflow with template expressions +--- + +# Test + +Use environment variable: ${{ env.MY_VAR }} +Use config variable: ${{ vars.MY_CONFIG }} +`, + }, + { + name: "frontmatter with inlined-imports", + content: `--- +engine: copilot +inlined-imports: true +description: Inlined imports workflow +--- + +# Inlined body content +Some multi-line +content here +`, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + lfContent := tc.content + crlfContent := strings.ReplaceAll(tc.content, "\n", "\r\n") + + lfFile := filepath.Join(tempDir, "lf-"+strings.ReplaceAll(tc.name, " ", "-")+".md") + crlfFile := filepath.Join(tempDir, "crlf-"+strings.ReplaceAll(tc.name, " ", "-")+".md") + + err := os.WriteFile(lfFile, []byte(lfContent), 0644) + require.NoError(t, err, "Should write LF test file") + err = os.WriteFile(crlfFile, []byte(crlfContent), 0644) + require.NoError(t, err, "Should write CRLF test file") + + lfHash, err := ComputeFrontmatterHashFromFile(lfFile, cache) + require.NoError(t, err, "Should compute hash for LF file") + assert.Len(t, lfHash, 64, "LF hash should be 64 characters") + + crlfHash, err := ComputeFrontmatterHashFromFile(crlfFile, cache) + require.NoError(t, err, "Should compute hash for CRLF file") + assert.Len(t, crlfHash, 64, "CRLF hash should be 64 characters") + + assert.Equal(t, lfHash, crlfHash, "LF and CRLF variants must produce identical hashes") + t.Logf(" ✓ LF=CRLF hash: %s", lfHash) + }) + } +} + +// TestHashConsistency_LFvsCRLF_WithImports validates that LF/CRLF invariance holds +// when workflows import other workflows. +func TestHashConsistency_LFvsCRLF_WithImports(t *testing.T) { + tempDir := t.TempDir() + sharedDir := filepath.Join(tempDir, "shared") + err := os.MkdirAll(sharedDir, 0755) + require.NoError(t, err, "Should create shared directory") + + sharedLF := `--- +tools: + playwright: + version: v1.41.0 +labels: + - shared + - common +--- + +# Shared Content +` + mainLF := `--- +engine: copilot +description: Main workflow +imports: + - shared/common.md +labels: + - main +--- + +# Main Workflow +` + + cache := NewImportCache("") + + // LF variant: both main and shared use LF + err = os.WriteFile(filepath.Join(sharedDir, "common.md"), []byte(sharedLF), 0644) + require.NoError(t, err) + err = os.WriteFile(filepath.Join(tempDir, "main-lf.md"), []byte(mainLF), 0644) + require.NoError(t, err) + lfHash, err := ComputeFrontmatterHashFromFile(filepath.Join(tempDir, "main-lf.md"), cache) + require.NoError(t, err, "Should compute hash for LF files with imports") + + // CRLF variant: both main and shared use CRLF + sharedCRLF := strings.ReplaceAll(sharedLF, "\n", "\r\n") + mainCRLF := strings.ReplaceAll(mainLF, "\n", "\r\n") + err = os.WriteFile(filepath.Join(sharedDir, "common.md"), []byte(sharedCRLF), 0644) + require.NoError(t, err) + err = os.WriteFile(filepath.Join(tempDir, "main-crlf.md"), []byte(mainCRLF), 0644) + require.NoError(t, err) + crlfHash, err := ComputeFrontmatterHashFromFile(filepath.Join(tempDir, "main-crlf.md"), cache) + require.NoError(t, err, "Should compute hash for CRLF files with imports") + + assert.Equal(t, lfHash, crlfHash, "LF and CRLF import variants must produce identical hashes") + t.Logf(" ✓ LF with imports hash: %s", lfHash) + t.Logf(" ✓ CRLF with imports hash: %s", crlfHash) +} + // computeHashViaNode computes the hash using the JavaScript implementation via Node.js func computeHashViaNode(workflowPath string) (string, error) { // Get working directory and construct path to JavaScript file