From e8f37840fa72dd526889bb68bffae75b2c8144c5 Mon Sep 17 00:00:00 2001 From: Paul Frederiksen Date: Fri, 16 Jan 2026 17:14:08 -0800 Subject: [PATCH 1/2] feat: Implement GitHub Action outputs and golden tests Add GitHub Actions integration with has-changes and diff-output outputs. Convert output format tests to use golden files for better maintainability. Changes: - Add writeGitHubOutputs() to write GitHub Actions outputs - Detect GITHUB_OUTPUT env var and write outputs in correct format - Convert TestGenerateStat to golden file tests (4 test cases) - Convert TestGenerateSideBySide to golden file tests (5 test cases) - Convert TestGenerateGitDiff to golden file tests (5 test cases) - Add comprehensive tests for GitHub Actions output functionality - Fix variable redeclaration in compareFiles() Addresses PR review comments from #11: - Issue 3: GitHub Action outputs now fully implemented - Issue 4: Golden tests added for all new output formats Co-Authored-By: Claude Sonnet 4.5 --- cmd/configdiff/compare.go | 35 +++- cmd/configdiff/main_test.go | 108 +++++++++++ report/report_test.go | 226 +++++++++++++++------- testdata/report/git_diff_add.txt | 5 + testdata/report/git_diff_empty.txt | 0 testdata/report/git_diff_modify.txt | 6 + testdata/report/git_diff_multiple.txt | 10 + testdata/report/git_diff_remove.txt | 5 + testdata/report/side_by_side_add.txt | 8 + testdata/report/side_by_side_empty.txt | 1 + testdata/report/side_by_side_modify.txt | 8 + testdata/report/side_by_side_multiple.txt | 14 ++ testdata/report/side_by_side_remove.txt | 8 + testdata/report/stat_empty.txt | 1 + testdata/report/stat_mixed.txt | 6 + testdata/report/stat_multiple.txt | 4 + testdata/report/stat_single_modify.txt | 2 + 17 files changed, 379 insertions(+), 68 deletions(-) create mode 100644 testdata/report/git_diff_add.txt create mode 100644 testdata/report/git_diff_empty.txt create mode 100644 testdata/report/git_diff_modify.txt create mode 100644 testdata/report/git_diff_multiple.txt create mode 100644 testdata/report/git_diff_remove.txt create mode 100644 testdata/report/side_by_side_add.txt create mode 100644 testdata/report/side_by_side_empty.txt create mode 100644 testdata/report/side_by_side_modify.txt create mode 100644 testdata/report/side_by_side_multiple.txt create mode 100644 testdata/report/side_by_side_remove.txt create mode 100644 testdata/report/stat_empty.txt create mode 100644 testdata/report/stat_mixed.txt create mode 100644 testdata/report/stat_multiple.txt create mode 100644 testdata/report/stat_single_modify.txt diff --git a/cmd/configdiff/compare.go b/cmd/configdiff/compare.go index 5bef81e..15cf899 100644 --- a/cmd/configdiff/compare.go +++ b/cmd/configdiff/compare.go @@ -117,8 +117,9 @@ func compareFiles(oldFile, newFile string) (bool, error) { } // Format and output results (unless quiet mode) + var output string if !quiet { - output, err := cli.FormatOutput(result, cli.OutputOptions{ + output, err = cli.FormatOutput(result, cli.OutputOptions{ Format: outputFormat, NoColor: noColor, MaxValueLength: maxValueLength, @@ -132,8 +133,17 @@ func compareFiles(oldFile, newFile string) (bool, error) { fmt.Println(output) } + // Write GitHub Actions outputs if in GHA environment + hasChanges := cli.HasChanges(result) + if githubOutput := os.Getenv("GITHUB_OUTPUT"); githubOutput != "" { + if err := writeGitHubOutputs(githubOutput, hasChanges, output); err != nil { + // Log error but don't fail the command + fmt.Fprintf(os.Stderr, "Warning: Failed to write GitHub Actions outputs: %v\n", err) + } + } + // Return whether changes were found - return cli.HasChanges(result), nil + return hasChanges, nil } // compareDirectories recursively compares two directories. @@ -255,3 +265,24 @@ func fileExists(path string) bool { } return !info.IsDir() } + +// writeGitHubOutputs writes GitHub Actions outputs to the GITHUB_OUTPUT file +func writeGitHubOutputs(outputFile string, hasChanges bool, diffOutput string) error { + f, err := os.OpenFile(outputFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return err + } + defer f.Close() + + // Write has-changes output + if _, err := fmt.Fprintf(f, "has-changes=%v\n", hasChanges); err != nil { + return err + } + + // Write diff-output using heredoc format + if _, err := fmt.Fprintf(f, "diff-output< Date: Fri, 16 Jan 2026 18:23:50 -0800 Subject: [PATCH 2/2] fix: Use randomized delimiter for GitHub Actions heredoc output Fix heredoc delimiter injection vulnerability by using a cryptographically random delimiter instead of fixed "EOF" string. Security issue: A config file containing "EOF" on its own line could prematurely terminate the heredoc and potentially inject arbitrary GitHub Actions workflow commands. Changes: - Generate random 16-byte delimiter prefixed with "ghadelimiter_" - Update tests to verify random delimiter format - Add test case specifically for EOF injection attack - Add reference to GitHub Actions multiline string documentation Co-Authored-By: Claude Sonnet 4.5 --- cmd/configdiff/compare.go | 14 +++++++-- cmd/configdiff/main_test.go | 60 +++++++++++++++++++++++++++++++++---- 2 files changed, 67 insertions(+), 7 deletions(-) diff --git a/cmd/configdiff/compare.go b/cmd/configdiff/compare.go index 15cf899..0a49a28 100644 --- a/cmd/configdiff/compare.go +++ b/cmd/configdiff/compare.go @@ -1,6 +1,8 @@ package main import ( + "crypto/rand" + "encoding/hex" "fmt" "os" "path/filepath" @@ -279,8 +281,16 @@ func writeGitHubOutputs(outputFile string, hasChanges bool, diffOutput string) e return err } - // Write diff-output using heredoc format - if _, err := fmt.Fprintf(f, "diff-output< len("diff-output<<") && line[:13] == "diff-output<<" { + delimiter = line[13:] + diffStartIdx = i + 1 + break + } } - if !contains(output, "\nEOF\n") { - t.Error("Output missing diff-output heredoc end") + + if delimiter == "" { + t.Error("Failed to extract delimiter from output") + } else { + // Verify delimiter ends the heredoc + found := false + for i := diffStartIdx; i < len(lines); i++ { + if lines[i] == delimiter { + found = true + break + } + } + if !found { + t.Errorf("Delimiter %q not found at end of heredoc", delimiter) + } } + + // Verify diff content is present if tt.diffOutput != "" && !contains(output, tt.diffOutput) { t.Errorf("Output missing expected diff content: %q", tt.diffOutput) }