From 791a41c2a9a9f946ef44251d8ea22877473c149d Mon Sep 17 00:00:00 2001 From: Kevin Schiffer Date: Fri, 2 Feb 2024 13:23:41 +0900 Subject: [PATCH] dev: Add copyright header year check --- .github/workflows/general.yml | 4 ++- tools/mage/headers.go | 67 +++++++++++++++++++++++++++++++++-- 2 files changed, 68 insertions(+), 3 deletions(-) diff --git a/.github/workflows/general.yml b/.github/workflows/general.yml index 4195345caf..5374f4b906 100644 --- a/.github/workflows/general.yml +++ b/.github/workflows/general.yml @@ -10,6 +10,8 @@ jobs: steps: - name: Check out code uses: actions/checkout@v4 + - name: Fetch base ref + run: git fetch origin ${{ github.base_ref }} - name: Install Go and Dependencies uses: ./.github/actions/install-go-and-deps - name: Build Mage @@ -17,7 +19,7 @@ jobs: - name: Install Node and Dependencies uses: ./.github/actions/install-node-and-deps - name: Check headers - run: tools/bin/mage headers:check + run: tools/bin/mage headers:check headers:checkNewFiles - name: Fix common spelling mistakes run: tools/bin/mage dev:misspell - name: Format SQL files diff --git a/tools/mage/headers.go b/tools/mage/headers.go index cb09df2e18..c40ad6f6fd 100644 --- a/tools/mage/headers.go +++ b/tools/mage/headers.go @@ -19,9 +19,12 @@ import ( "bytes" "fmt" "os" + "os/exec" "path/filepath" "regexp" + "strconv" "strings" + "time" "github.com/magefile/mage/mg" "gopkg.in/yaml.v2" @@ -163,17 +166,33 @@ func (errs errorSlice) Error() string { case 0: return "" case 1: - return errs[0].Error() + // Return the formatted error string for a single error. + return formatErrorForGitHub(errs[0]) default: var b strings.Builder b.WriteString("multiple errors:\n") for _, err := range errs { - b.WriteString(" - " + err.Error() + "\n") + formattedError := formatErrorForGitHub(err) + b.WriteString(formattedError + "\n") } return b.String() } } +// formatErrorForGitHub formats an error for GitHub annotations. +func formatErrorForGitHub(err error) string { + switch e := err.(type) { + case *checkErr: + // GitHub expects the path to be relative to the repository root. + relativePath, _ := filepath.Rel(".", e.Path) + // Escape the message to prevent command injection. + message := strings.ReplaceAll(e.Reason, "\"", "\\\"") + return fmt.Sprintf("::error file=%s::%s", relativePath, message) + default: + return err.Error() + } +} + // Check checks that all files contain the required file header. func (h Headers) Check() error { mg.Deps(Headers.loadFile) @@ -203,6 +222,50 @@ func (h Headers) Check() error { return nil } +// CheckNewFiles checks that all new files contain the required file header with the correct year. +func (h Headers) CheckNewFiles() error { + mg.Deps(Headers.loadFile) + base := "origin/" + os.Getenv("GITHUB_BASE_REF") + + currentYear := time.Now().Year() + correctHeader := fmt.Sprintf("// Copyright © %d ", currentYear) + + cmd := exec.Command("git", "diff", "--name-only", "--diff-filter=A", base) + output, err := cmd.Output() + if err != nil { + return fmt.Errorf("failed to get list of new files: %w", err) + } + + var checkErrs errorSlice + for _, path := range strings.Split(strings.TrimSpace(string(output)), "\n") { + // Check if the file matches the HeaderRule before checking its header. + if rule := headerConfig.get(path); rule == nil { + continue // Skip files that do not match any HeaderRule. + } + + fileContent, err := os.ReadFile(path) + if err != nil { + return fmt.Errorf("failed to read file %s: %w", path, err) + } + + // Check if the first line of the file contains the correct copyright header. + scanner := bufio.NewScanner(bytes.NewReader(fileContent)) + if scanner.Scan() { + firstLine := scanner.Text() + if !strings.Contains(firstLine, correctHeader) { + checkErrs = append(checkErrs, &checkErr{Path: path, Reason: "incorrect year in copyright header; should be " + strconv.Itoa(currentYear)}) + } + } else { + checkErrs = append(checkErrs, &checkErr{Path: path, Reason: "empty file or missing header"}) + } + } + + if len(checkErrs) > 0 { + return checkErrs + } + return nil +} + func init() { preCommitChecks = append(preCommitChecks, Headers.Check) }