Skip to content

Commit a790f8a

Browse files
committed
refactor: header check
Signed-off-by: Ryan Johnson <[email protected]>
1 parent 1fc9249 commit a790f8a

File tree

4 files changed

+245
-60
lines changed

4 files changed

+245
-60
lines changed

.github/workflows/govmomi-go-lint.yaml

Lines changed: 4 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -28,20 +28,9 @@ jobs:
2828
- name: Go Lint
2929
run: make lint
3030

31-
boilerplate:
32-
name: Boilerplate Check
31+
header:
32+
name: Header Check
3333
runs-on: ubuntu-latest
34-
strategy:
35-
fail-fast: false # Keep running if one leg fails.
36-
matrix:
37-
extension:
38-
- go
39-
40-
# Map between extension and human-readable name.
41-
include:
42-
- extension: go
43-
language: go
44-
4534
steps:
4635
- name: Check Repository
4736
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
@@ -52,47 +41,5 @@ jobs:
5241
go-version-file: go.mod
5342
id: go
5443

55-
- name: Install Tools
56-
run: |
57-
TEMP_PATH="$(mktemp -d)"
58-
cd "$TEMP_PATH"
59-
60-
echo '::group::🐶 Installing reviewdog ... https://github.com/reviewdog/reviewdog'
61-
curl -sfL https://raw.githubusercontent.com/reviewdog/reviewdog/master/install.sh | sh -s -- -b "${TEMP_PATH}" 2>&1
62-
echo '::endgroup::'
63-
64-
echo '::group:: Installing boilerplate-check ... https://github.com/mattmoor/boilerplate-check'
65-
go install github.com/mattmoor/boilerplate-check/cmd/boilerplate-check@latest
66-
echo '::endgroup::'
67-
68-
echo "${TEMP_PATH}" >> "$GITHUB_PATH"
69-
70-
- id: boilerplate_txt
71-
uses: andstor/file-existence-action@076e0072799f4942c8bc574a82233e1e4d13e9d6 # v3.0.0
72-
with:
73-
files: ./hack/boilerplate/boilerplate.${{ matrix.extension }}.txt
74-
75-
- name: ${{ matrix.language }} license boilerplate
76-
shell: bash
77-
if: ${{ steps.boilerplate_txt.outputs.files_exists == 'true' }}
78-
env:
79-
REVIEWDOG_GITHUB_API_TOKEN: ${{ github.token }}
80-
run: |
81-
set -e
82-
cd "${GITHUB_WORKSPACE}" || exit 1
83-
84-
echo '::group:: Running github.com/mattmoor/boilerplate-check for ${{ matrix.language }} with reviewdog 🐶 ...'
85-
# Don't fail because of boilerplate-check
86-
set +o pipefail
87-
boilerplate-check check \
88-
--boilerplate ./hack/boilerplate/boilerplate.${{ matrix.extension }}.txt \
89-
--file-extension ${{ matrix.extension }} \
90-
--exclude "((vim25/json)|vendor|third_party|dist)/" |
91-
reviewdog -efm="%A%f:%l: %m" \
92-
-efm="%C%.%#" \
93-
-name="${{ matrix.language }} headers" \
94-
-reporter="github-pr-check" \
95-
-filter-mode="diff_context" \
96-
-fail-on-error="true" \
97-
-level="error"
98-
echo '::endgroup::'
44+
- name: Run Header Check
45+
run: go run hack/header/main.go -config="hack/header/config.json"

hack/boilerplate/boilerplate.go.txt

Lines changed: 0 additions & 3 deletions
This file was deleted.

hack/header/config.json

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"headerLines": [
3+
"© Broadcom. All Rights Reserved.",
4+
"The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries.",
5+
"SPDX-License-Identifier: Apache-2.0"
6+
],
7+
"fileCommentPrefixes": {
8+
".go": "//",
9+
".sh": "#",
10+
".mk": "#",
11+
".yaml": "#",
12+
".yml": "#"
13+
},
14+
"ignoredPaths": [
15+
".chglog/**",
16+
".github/**",
17+
".golangci.yml",
18+
".goreleaser.yml",
19+
"scripts/**",
20+
"vim25/json/**",
21+
"vim25/xml/**",
22+
"govc/**/*.sh"
23+
],
24+
"maxScanLines": 10
25+
}

hack/header/main.go

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
// © Broadcom. All Rights Reserved.
2+
// The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries.
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
package main
6+
7+
import (
8+
"bufio"
9+
"encoding/json"
10+
"flag"
11+
"fmt"
12+
"os"
13+
"path/filepath"
14+
"strings"
15+
)
16+
17+
// Config defines the structure of the JSON configuration file.
18+
type Config struct {
19+
HeaderLines []string `json:"headerLines"` // Expected file header.
20+
FileCommentPrefixes map[string]string `json:"fileCommentPrefixes"` // Comment styles for file types.
21+
IgnoredPaths []string `json:"ignoredPaths"` // Paths to ignore.
22+
MaxScanLines int `json:"maxScanLines"` // Number of lines to check for the header.
23+
}
24+
25+
// Global variables to hold the configuration and results.
26+
var config Config
27+
var filesWithIssues []string
28+
var filesWithError []string
29+
var processedCount = 0
30+
31+
var providedConfigPath = flag.String("config", "", "Path to the configuration file")
32+
33+
// main is the entry point for the header check.
34+
func main() {
35+
flag.Parse()
36+
37+
if *providedConfigPath == "" {
38+
fmt.Println("Provide a JSON configuration using the --config flag.")
39+
os.Exit(1)
40+
}
41+
42+
fmt.Printf("Configuration file: %s\n", *providedConfigPath)
43+
configPathToLoad := *providedConfigPath
44+
45+
// Load the configuration file.
46+
if err := loadConfig(configPathToLoad); err != nil {
47+
fmt.Printf("Error loading configuration: %v\n", err)
48+
os.Exit(1) // Configuration error is critical.
49+
}
50+
51+
// Start directory traversal.
52+
err := filepath.Walk(".", func(path string, info os.FileInfo, err error) error {
53+
if err != nil {
54+
return err
55+
}
56+
57+
// Skip directories.
58+
if info.IsDir() {
59+
return nil
60+
}
61+
62+
// Skip ignored paths.
63+
if isIgnored(path) {
64+
return nil
65+
}
66+
67+
// Check for file types supported by `fileCommentPrefixes`.
68+
ext := filepath.Ext(path)
69+
if commentPrefix, ok := config.FileCommentPrefixes[ext]; ok {
70+
processedCount++
71+
currentHeader := transformHeader(config.HeaderLines, commentPrefix)
72+
73+
// Check the file for the required header.
74+
if missing, err := checkHeader(path, currentHeader); err != nil {
75+
filesWithError = append(filesWithError, fmt.Sprintf("%s (error: %v)", path, err))
76+
} else if missing {
77+
filesWithIssues = append(filesWithIssues, path)
78+
}
79+
}
80+
81+
return nil
82+
})
83+
84+
if err != nil {
85+
fmt.Printf("❌ Error during directory traversal: %v\n", err)
86+
os.Exit(2) // Critical error during file traversal.
87+
}
88+
89+
// Print the summary and get the exit code.
90+
exitCode := printSummary()
91+
os.Exit(exitCode)
92+
}
93+
94+
// loadConfig loads the JSON configuration into the global config variable.
95+
func loadConfig(filepath string) error {
96+
file, err := os.Open(filepath)
97+
if err != nil {
98+
return fmt.Errorf("failed to open configuration file: %w", err)
99+
}
100+
defer file.Close()
101+
102+
decoder := json.NewDecoder(file)
103+
if err := decoder.Decode(&config); err != nil {
104+
return fmt.Errorf("failed to decode configuration: %w", err)
105+
}
106+
107+
return nil
108+
}
109+
110+
// isIgnored determines if a file or directory should be skipped based on ignoredPaths.
111+
func isIgnored(path string) bool {
112+
normalizedPath := strings.ReplaceAll(path, string(filepath.Separator), "/")
113+
for _, pattern := range config.IgnoredPaths {
114+
if matched, _ := filepath.Match(pattern, normalizedPath); matched {
115+
return true
116+
}
117+
if strings.Contains(pattern, "**") {
118+
prefix := strings.Split(pattern, "**")[0]
119+
if strings.HasPrefix(normalizedPath, prefix) {
120+
return true
121+
}
122+
}
123+
}
124+
return false
125+
}
126+
127+
// transformHeader adjusts the header format for the target file's comment style.
128+
func transformHeader(header []string, prefix string) []string {
129+
transformed := make([]string, len(header))
130+
for i, line := range header {
131+
transformed[i] = prefix + " " + strings.TrimPrefix(line, "// ")
132+
}
133+
return transformed
134+
}
135+
136+
// checkHeader verifies if the file contains the required header.
137+
func checkHeader(path string, headerLines []string) (bool, error) {
138+
file, err := os.Open(path)
139+
if err != nil {
140+
return false, err
141+
}
142+
defer file.Close()
143+
144+
scanner := bufio.NewScanner(file)
145+
lines := []string{}
146+
skipShebang := filepath.Ext(path) == ".sh" // Handle shebang for shell scripts.
147+
148+
// Only scan the first `MaxScanLines` lines.
149+
for i := 0; i < config.MaxScanLines && scanner.Scan(); i++ {
150+
line := strings.TrimSpace(scanner.Text())
151+
152+
// Skip shebang if present.
153+
if skipShebang && strings.HasPrefix(line, "#!") {
154+
skipShebang = false
155+
continue
156+
}
157+
158+
lines = append(lines, line)
159+
}
160+
161+
if err := scanner.Err(); err != nil {
162+
return false, err
163+
}
164+
165+
return !containsHeader(lines, headerLines), nil
166+
}
167+
168+
// containsHeader checks if the required header lines exist in a file.
169+
func containsHeader(fileLines, headerLines []string) bool {
170+
i := 0
171+
for _, line := range fileLines {
172+
if line == headerLines[i] {
173+
i++
174+
if i == len(headerLines) {
175+
return true
176+
}
177+
}
178+
}
179+
return false
180+
}
181+
182+
// printSummary displays the results after processing all files and returns the exit code.
183+
func printSummary() int {
184+
fmt.Println("Processing complete.")
185+
fmt.Printf("Total files processed: %d\n", processedCount)
186+
187+
exitCode := 0
188+
189+
if len(filesWithIssues) > 0 {
190+
fmt.Printf("Missing headers: %d\n", len(filesWithIssues))
191+
for _, file := range filesWithIssues {
192+
fmt.Printf(" - %s\n", file)
193+
}
194+
195+
exitCode = 1 // Missing headers found.
196+
}
197+
198+
if len(filesWithError) > 0 {
199+
fmt.Printf("Errors encountered in files: %d\n", len(filesWithError))
200+
for _, file := range filesWithError {
201+
fmt.Printf(" - %s\n", file)
202+
}
203+
204+
if exitCode == 1 {
205+
exitCode = 3 // Both missing headers and errors.
206+
} else {
207+
exitCode = 2 // Only errors occurred.
208+
}
209+
}
210+
211+
if len(filesWithIssues) == 0 && len(filesWithError) == 0 {
212+
fmt.Println("All processed files have the expected header.")
213+
}
214+
215+
return exitCode
216+
}

0 commit comments

Comments
 (0)