From 6d06f3825cc9005cc67b56ec58ca526c29e26515 Mon Sep 17 00:00:00 2001 From: Masahiro Nagano Date: Sat, 18 Jul 2026 00:32:18 +0900 Subject: [PATCH 1/7] rewrite with cubicdaiya/gonp and refactor and repository transfer --- .github/dependabot.yml | 24 ++++ .github/workflows/release.yml | 35 +++-- .github/workflows/test.yml | 19 +-- .goreleaser.yml | 9 +- Makefile | 12 +- README.md | 2 +- go.mod | 15 +- go.sum | 38 ++--- main.go | 238 ++++++++++++++++++------------- main_test.go | 254 ++++++++++++++++++++++++++++++++++ 10 files changed, 482 insertions(+), 164 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 main_test.go diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..badd0a1 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,24 @@ +version: 2 + +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: daily + commit-message: + prefix: "ci:" + groups: + dependencies: + patterns: + - "*" + + - package-ecosystem: gomod + directory: / + schedule: + interval: daily + commit-message: + prefix: "go:" + groups: + dependencies: + patterns: + - "*" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 76ef607..84777c9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,26 +1,35 @@ -name: release +name: tagpr on: push: - tags: - - "v[0-9]+.[0-9]+.[0-9]+" + branches: + - main jobs: - goreleaser: + tagpr: runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + issues: write steps: - - name: Checkout - uses: actions/checkout@v2 - with: - fetch-depth: 0 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4 + + - uses: Songmu/tagpr@b3fb89424646b06c8aa460f50307c60b6a541425 # v1 + id: tagpr + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Setup Go - uses: actions/setup-go@v2 + uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v4 with: - go-version: 1.15 + go-version-file: go.mod + if: ${{ steps.tagpr.outputs.tag != ''}} - name: Run GoReleaser - uses: goreleaser/goreleaser-action@v2 + uses: goreleaser/goreleaser-action@ec59f474b9834571250b370d4735c50f8e2d1e29 # v7.0.0 with: version: latest - args: release --rm-dist + args: release --clean env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CGO_ENABLED: 0 + if: ${{ steps.tagpr.outputs.tag != ''}} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c241ec3..f229ed8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,16 +6,19 @@ on: jobs: test: runs-on: ubuntu-latest + permissions: + contents: read steps: - - name: Checkout - uses: actions/checkout@v2 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4 + - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v4 with: - fetch-depth: 0 - - - name: Setup Go - uses: actions/setup-go@v2 - with: - go-version: 1.15 + go-version-file: go.mod - name: test run: make check + + - name: Snapshot GoReleaser + uses: goreleaser/goreleaser-action@ec59f474b9834571250b370d4735c50f8e2d1e29 # v7.0.0 + with: + version: latest + args: build --snapshot diff --git a/.goreleaser.yml b/.goreleaser.yml index 4614622..cb102b3 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,18 +1,15 @@ builds: - - binary: check-diff + - binary: mackerel-plugin-jsonl + main: ./cmd/mackerel-plugin-jsonl goos: - - darwin - linux goarch: - amd64 - arm64 - ignore: - - goos: darwin - goarch: arm64 archives: - format: zip name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}" release: github: - owner: kazeburo + owner: monitoring-forge name: check-diff diff --git a/Makefile b/Makefile index d2e2922..9077bdc 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,6 @@ VERSION=0.0.5 -LDFLAGS=-ldflags "-w -s -X main.version=${VERSION}" +GITCOMMIT?=$(shell git describe --dirty --always) +LDFLAGS=-ldflags "-w -s -X main.version=${VERSION} -X main.commit=${GITCOMMIT}" all: check-diff @@ -12,12 +13,5 @@ linux: main.go GOOS=linux GOARCH=amd64 go build $(LDFLAGS) -o check-diff main.go check: - go test ./... + go test -v ./... -fmt: - go fmt ./... - -tag: - git tag v${VERSION} - git push origin v${VERSION} - git push origin master diff --git a/README.md b/README.md index 8fa14e7..2fede54 100644 --- a/README.md +++ b/README.md @@ -47,4 +47,4 @@ command = "/usr/local/bin/check-diff -- cat /etc/passwd" ## Install -Please download release page or `mkr plugin install kazeburo/check-diff`. \ No newline at end of file +Please download release page or `mkr plugin install monitoring-forge/check-diff`. \ No newline at end of file diff --git a/go.mod b/go.mod index 4c94ad7..4493079 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,15 @@ -module github.com/kazeburo/check-diff +module github.com/monitoring-forge/check-diff -go 1.14 +go 1.25.0 require ( - github.com/jessevdk/go-flags v1.4.0 - github.com/mackerelio/checkers v0.0.0-20200428063449-52cfb2c2c52c + github.com/jessevdk/go-flags v1.6.1 + github.com/mackerelio/checkers v0.2.1 +) + +require github.com/cubicdaiya/gonp v1.0.4 + +require ( + github.com/mackerelio/golib v1.2.2 + golang.org/x/sys v0.47.0 // indirect ) diff --git a/go.sum b/go.sum index 89117e7..3ff45c5 100644 --- a/go.sum +++ b/go.sum @@ -1,28 +1,10 @@ -github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA= -github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= -github.com/mackerelio/checkers v0.0.0-20200428063449-52cfb2c2c52c h1:gTJ7KeXinv4dbCCh5zXV7clWNjk0ESazARKhm87U0NE= -github.com/mackerelio/checkers v0.0.0-20200428063449-52cfb2c2c52c/go.mod h1:h/TgaBtgIw4WZMY8OiHhxhd49mY4hEPo6r2IaqpMGS8= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200426102838-f3a5411a4c3b h1:zSzQJAznWxAh9fZxiPy2FZo+ZZEYoYFYYDYdOrU7AaM= -golang.org/x/tools v0.0.0-20200426102838-f3a5411a4c3b/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +github.com/cubicdaiya/gonp v1.0.4 h1:ky2uIAJh81WiLcGKBVD5R7KsM/36W6IqqTy6Bo6rGws= +github.com/cubicdaiya/gonp v1.0.4/go.mod h1:iWGuP/7+JVTn02OWhRemVbMmG1DOUnmrGTYYACpOI0I= +github.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bBK4= +github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc= +github.com/mackerelio/checkers v0.2.1 h1:vRXzAT+CEyaLjSrHVxKUBjyWq1kfo8QcI8ZoSKZATQs= +github.com/mackerelio/checkers v0.2.1/go.mod h1:CW3k/5bvHhxDrfKgWvMvNH0R51zco141ZVxlI7o/KAc= +github.com/mackerelio/golib v1.2.2 h1:fQfW1o2KvPyIsAH/7+M2J0gpf2jLu+NyYpDCN79zxW4= +github.com/mackerelio/golib v1.2.2/go.mod h1:itcSOfrsT5XvkzflFPebT43RrliaKDz2T6YGYt3jclw= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= diff --git a/main.go b/main.go index bffb71d..23d5b4e 100644 --- a/main.go +++ b/main.go @@ -1,37 +1,39 @@ package main import ( + "bufio" "bytes" - "crypto/md5" + "crypto/sha256" "fmt" - "io/ioutil" "os" "os/exec" "os/user" "path/filepath" - "regexp" "runtime" "strings" + "github.com/cubicdaiya/gonp" "github.com/jessevdk/go-flags" "github.com/mackerelio/checkers" + "github.com/mackerelio/golib/pluginutil" ) -// version by Makefile var version string - -type cmdOpts struct { - OptArgs []string - OptCommand string - OptIdentifier string `long:"identifier" description:"indetify a file store the command result with given string"` - OptWarn bool `short:"w" long:"warn" description:"Set the error level to warning"` - Version bool `short:"v" long:"version" description:"Show version"` +var commit string + +type Opt struct { + Args []string + Command string + Identifier string `long:"identifier" description:"identify a file store the command result with given string"` + Warn bool `short:"w" long:"warn" description:"Set the error level to warning"` + Workdir string `long:"workdir" description:"Set the working directory"` + Version bool `short:"v" long:"version" description:"Show version"` } -func runCmd(curFile *os.File, opts cmdOpts) error { - cmd := exec.Command(opts.OptCommand, opts.OptArgs...) +func (opt *Opt) cmd(file *os.File) error { + cmd := exec.Command(opt.Command, opt.Args...) var stderr bytes.Buffer - cmd.Stdout = curFile + cmd.Stdout = file cmd.Stderr = &stderr if err := cmd.Start(); err != nil { return err @@ -43,126 +45,167 @@ func runCmd(curFile *os.File, opts cmdOpts) error { return nil } -func calcDiff(diffCmd, from, to string) ([]byte, error) { - return exec.Command(diffCmd, "-U", "1", from, to).Output() -} - -func fileExists(filename string) bool { - _, err := os.Stat(filename) - return err == nil -} +func (opt *Opt) run() *checkers.Checker { -func checkDiff(opts cmdOpts) *checkers.Checker { - diffCmd, err := exec.LookPath("diff") + hasher := sha256.New() + hasher.Write([]byte(opt.Identifier)) + hasher.Write([]byte("-")) + hasher.Write([]byte(opt.Command)) + hasher.Write([]byte("-")) + for _, v := range opt.Args { + hasher.Write([]byte(v)) + hasher.Write([]byte("-")) + } + curUser, err := user.Current() if err != nil { return checkers.Critical(err.Error()) } - tmpDir := os.TempDir() - - hasher := md5.New() - hasher.Write([]byte(opts.OptIdentifier)) - hasher.Write([]byte(opts.OptCommand)) - for _, v := range opts.OptArgs { - hasher.Write([]byte(v)) + prevPath := filepath.Join(opt.Workdir, fmt.Sprintf("check-diff-%s-%x", curUser.Uid, hasher.Sum(nil))) + newFile, err := os.CreateTemp(opt.Workdir, "check-diff-") + if err != nil { + return checkers.Critical(err.Error()) } - commandKey := fmt.Sprintf("%x", hasher.Sum(nil)) - - curUser, _ := user.Current() - prevPath := filepath.Join(tmpDir, "check-diff-"+curUser.Uid+"-"+commandKey) - curFile, err := ioutil.TempFile(tmpDir, "temp") + err = opt.cmd(newFile) if err != nil { + newFile.Close() + os.Remove(newFile.Name()) return checkers.Critical(err.Error()) } - defer os.Remove(curFile.Name()) - - err = runCmd(curFile, opts) + err = newFile.Close() if err != nil { return checkers.Critical(err.Error()) } if !fileExists(prevPath) { - err = os.Rename(curFile.Name(), prevPath) + err = os.Rename(newFile.Name(), prevPath) if err != nil { return checkers.Critical(err.Error()) } msg := "" - if len(opts.OptArgs) > 0 { - msg = fmt.Sprintf("first time execution command: '%s %s'", opts.OptCommand, strings.Join(opts.OptArgs, " ")) + if len(opt.Args) > 0 { + msg = fmt.Sprintf("first time execution command: '%s %s'", opt.Command, strings.Join(opt.Args, " ")) } else { - msg = fmt.Sprintf("first time execution command: '%s'", opts.OptCommand) + msg = fmt.Sprintf("first time execution command: '%s'", opt.Command) } return checkers.Ok(msg) } - // diff - diffOut, diffError := calcDiff(diffCmd, prevPath, curFile.Name()) - err = os.Rename(curFile.Name(), prevPath) + diff, err := diff(prevPath, newFile.Name()) if err != nil { return checkers.Critical(err.Error()) } - if diffError == nil { - // no difference - curOpen, err := os.Open(curFile.Name()) - if err != nil { - return checkers.Critical(err.Error()) - } - defer curOpen.Close() + err = os.Rename(newFile.Name(), prevPath) + if err != nil { + return checkers.Critical(err.Error()) + } - fileinfo, _ := curOpen.Stat() - data := make([]byte, 128) - count, err := curOpen.Read(data) + if diff == "" { + msg, err := buildNoDifferenceMsg(prevPath) if err != nil { return checkers.Critical(err.Error()) } - cur := string(data[0:count]) - cur = regexp.MustCompile("(\r\n|\r|\n)$").ReplaceAllString(cur, "") - msg := "" - if fileinfo.Size() > 128 { - msg = fmt.Sprintf("no difference: ```%s...```\n", cur) - } else { - msg = fmt.Sprintf("no difference: ```%s```\n", cur) - } return checkers.Ok(msg) - } else if regexp.MustCompile("exit status 1").MatchString(diffError.Error()) { - // found diff - diffRet := strings.Split(string(diffOut), "\n") - diffRetString := strings.Join(diffRet[2:], "\n") - diffRetString = regexp.MustCompile("(\r\n|\r|\n)$").ReplaceAllString(diffRetString, "") - msg := "" - if len(diffRetString) > 512 { - msg = fmt.Sprintf("found difference: ```%s...```\n", diffRetString[0:512]) - } else { - msg = fmt.Sprintf("found difference: ```%s```\n", diffRetString) - } - if opts.OptWarn { - return checkers.Warning(msg) - } - return checkers.Critical(msg) } - return checkers.Critical(diffError.Error()) + diffMsg := buildDiffMsg(diff) + if opt.Warn { + return checkers.Warning(diffMsg) + } + return checkers.Critical(diffMsg) } -func printVersion() { - fmt.Printf(`%s %s -Compiler: %s %s -`, - os.Args[0], - version, - runtime.Compiler, - runtime.Version()) +func buildNoDifferenceMsg(filename string) (string, error) { + file, err := os.Open(filename) + if err != nil { + return "", err + } + defer file.Close() + + fileinfo, _ := file.Stat() + b := make([]byte, 128) + count, err := file.Read(b) + if err != nil { + return "", err + } + o := string(strings.TrimRight(string(b[0:count]), "\r\n")) + if fileinfo.Size() > 128 { + return fmt.Sprintf("no difference: ```%s...```", o), nil + } + return fmt.Sprintf("no difference: ```%s```", o), nil +} + +func buildDiffMsg(diff string) string { + o := diff + if len(diff) > 512 { + o = diff[0:512] + } + o = strings.TrimRight(o, "\r\n") + + if len(diff) > 512 { + return fmt.Sprintf("found difference: ```%s...```", o) + } + return fmt.Sprintf("found difference: ```%s```", o) +} + +func getLines(filename string) ([]string, error) { + file, err := os.Open(filename) + if err != nil { + return nil, err + } + defer file.Close() + + scanner := bufio.NewScanner(file) + lines := make([]string, 0) + for scanner.Scan() { + lines = append(lines, scanner.Text()) + } + if err := scanner.Err(); err != nil { + return nil, err + } + return lines, nil +} + +func diff(prev, new string) (string, error) { + prevLines, err := getLines(prev) + if err != nil { + return "", err + } + newLines, err := getLines(new) + if err != nil { + return "", err + } + + diff := gonp.New(prevLines, newLines) + diff.Compose() + + return diff.SprintUniHunks(diff.UnifiedHunks()), nil +} + +func fileExists(filename string) bool { + _, err := os.Stat(filename) + return err == nil } func main() { - opts := cmdOpts{} - psr := flags.NewParser(&opts, flags.HelpFlag|flags.PassDoubleDash) + opt := &Opt{} + psr := flags.NewParser(opt, flags.HelpFlag|flags.PassDoubleDash) psr.Usage = "[OPTIONS] -- command args1 args2" args, err := psr.Parse() - if opts.Version { - printVersion() + if opt.Version { + if commit == "" { + commit = "dev" + } + fmt.Printf( + "%s-%s\n%s/%s, %s, %s\n", + filepath.Base(os.Args[0]), + version, + runtime.GOOS, + runtime.GOARCH, + runtime.Version(), + commit) os.Exit(0) } if err != nil { @@ -173,12 +216,17 @@ func main() { psr.WriteHelp(os.Stderr) os.Exit(1) } - opts.OptCommand = args[0] + opt.Args = []string{} + opt.Command = args[0] if len(args) > 1 { - opts.OptArgs = args[1:] + opt.Args = args[1:] + } + + if opt.Workdir == "" { + opt.Workdir = pluginutil.PluginWorkDir() } - ckr := checkDiff(opts) + ckr := opt.run() ckr.Name = "check-diff" ckr.Exit() } diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..95a91c9 --- /dev/null +++ b/main_test.go @@ -0,0 +1,254 @@ +package main + +import ( + "bufio" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/mackerelio/checkers" +) + +// Test cmd +func TestCmd(t *testing.T) { + workdir := t.TempDir() + opt := Opt{ + Command: "echo", + Args: []string{"Hello, World!"}, + Identifier: "test", + Workdir: workdir, + } + filename := filepath.Join(workdir, "test.txt") + file, err := os.Create(filename) + if err != nil { + t.Fatalf("Failed to create file: %v", err) + } + defer file.Close() + + err = opt.cmd(file) + if err != nil { + t.Fatalf("cmd failed: %v", err) + } + + file.Seek(0, 0) // Reset file pointer to the beginning + scanner := bufio.NewScanner(file) + if scanner.Scan() { + output := scanner.Text() + expected := "Hello, World!" + if output != expected { + t.Errorf("Expected output %q, got %q", expected, output) + } + } else { + t.Errorf("No output from command") + } +} + +func TestCmdFailedCase(t *testing.T) { + workdir := t.TempDir() + opt := Opt{ + Command: "nonexistent_command", + Args: []string{}, + Identifier: "test", + Workdir: workdir, + } + filename := filepath.Join(workdir, "test.txt") + file, err := os.Create(filename) + if err != nil { + t.Fatalf("Failed to create file: %v", err) + } + defer file.Close() + + err = opt.cmd(file) + if err == nil { + t.Fatalf("Expected cmd to fail, but it succeeded") + } +} + +func TestCmdFailedCommand(t *testing.T) { + workdir := t.TempDir() + opt := Opt{ + Command: "ls", + Args: []string{"nonexistent_directory"}, + Identifier: "test", + Workdir: workdir, + } + filename := filepath.Join(workdir, "test.txt") + file, err := os.Create(filename) + if err != nil { + t.Fatalf("Failed to create file: %v", err) + } + defer file.Close() + + err = opt.cmd(file) + if err == nil { + t.Fatalf("Expected cmd to fail, but it succeeded") + } +} + +func TestBuildNoDifferenceMsg(t *testing.T) { + file, err := os.CreateTemp("", "testfile") + if err != nil { + t.Fatalf("Failed to create temp file: %v", err) + } + defer os.Remove(file.Name()) + defer file.Close() + shortMsg := "This is a test message." + file.WriteString(shortMsg + "\n") + expected := "no difference: ```" + shortMsg + "```" + result, err := buildNoDifferenceMsg(file.Name()) + if err != nil { + t.Fatalf("buildNoDifferenceMsg failed: %v", err) + } + if result != expected { + t.Errorf("Expected %q, got %q", expected, result) + } +} + +func TestBuildNoDifferenceMsgLargeResult(t *testing.T) { + file, err := os.CreateTemp("", "testfile") + if err != nil { + t.Fatalf("Failed to create temp file: %v", err) + } + defer os.Remove(file.Name()) + defer file.Close() + largeMsg := make([]byte, 600) + for i := range largeMsg { + largeMsg[i] = 'A' + } + file.Write(largeMsg) + expected := "no difference: ```" + string(largeMsg)[:128] + "...```" + result, err := buildNoDifferenceMsg(file.Name()) + if err != nil { + t.Fatalf("buildNoDifferenceMsg failed: %v", err) + } + if result != expected { + t.Errorf("Expected %q, got %q", expected, result) + } +} + +func TestBuildDiffMsg(t *testing.T) { + diff := "This is a test diff message.\nThis is the second line of the diff.\n" + expected := "found difference: ```This is a test diff message.\nThis is the second line of the diff.```" + result := buildDiffMsg(diff) + if result != expected { + t.Errorf("Expected %q, got %q", expected, result) + } +} + +func TestBuildDiffMsgLargeResult(t *testing.T) { + diff := make([]byte, 600) + for i := range diff { + diff[i] = 'A' + } + expected := "found difference: ```" + string(diff)[:512] + "...```" + result := buildDiffMsg(string(diff)) + if result != expected { + t.Errorf("Expected %q, got %q", expected, result) + } +} + +func TestDiff(t *testing.T) { + file1, err := os.CreateTemp("", "file1") + if err != nil { + t.Fatalf("Failed to create temp file: %v", err) + } + defer os.Remove(file1.Name()) + defer file1.Close() + file1.WriteString("Hello, World!\n") + + file2, err := os.CreateTemp("", "file2") + if err != nil { + t.Fatalf("Failed to create temp file: %v", err) + } + defer os.Remove(file2.Name()) + defer file2.Close() + file2.WriteString("Hello, Go!\n") + + diffResult, err := diff(file1.Name(), file2.Name()) + if err != nil { + t.Fatalf("diff failed: %v", err) + } + if diffResult == "" { + t.Errorf("Expected a diff result, got empty string") + } + if !strings.Contains(diffResult, "Hello, World!") || !strings.Contains(diffResult, "Hello, Go!") { + t.Errorf("Diff result does not contain expected content: %q", diffResult) + } +} + +func TestDiffNoDifference(t *testing.T) { + file1, err := os.CreateTemp("", "file1") + if err != nil { + t.Fatalf("Failed to create temp file: %v", err) + } + defer os.Remove(file1.Name()) + defer file1.Close() + file1.WriteString("Hello, World!\n") + + file2, err := os.CreateTemp("", "file2") + if err != nil { + t.Fatalf("Failed to create temp file: %v", err) + } + defer os.Remove(file2.Name()) + defer file2.Close() + file2.WriteString("Hello, World!\n") + + diffResult, err := diff(file1.Name(), file2.Name()) + if err != nil { + t.Fatalf("diff failed: %v", err) + } + if diffResult != "" { + t.Errorf("Expected no diff result, got: %q", diffResult) + } +} + +func TestRun(t *testing.T) { + workdir := t.TempDir() + opt := Opt{ + Command: "echo", + Args: []string{"Hello, World!"}, + Identifier: "test", + Workdir: workdir, + } + ckr := opt.run() + if ckr.Status != checkers.OK { + t.Errorf("Expected OK status, got %v", ckr.Status) + } + if !strings.Contains(ckr.Message, "first time execution command") { + t.Errorf("Expected first time execution message, got %q", ckr.Message) + } + + // Run again to check for no difference + ckr = opt.run() + if ckr.Status != checkers.OK { + t.Errorf("Expected OK status, got %v", ckr.Status) + } + if !strings.Contains(ckr.Message, "no difference") { + t.Errorf("Expected no difference message, got %q", ckr.Message) + } +} + +func TestRunWithDifference(t *testing.T) { + workdir := t.TempDir() + opt := Opt{ + Command: "date", + Args: []string{}, + Identifier: "test", + Workdir: workdir, + } + // First run to create the initial state + opt.run() + + // Change the command to produce a different output + time.Sleep(2 * time.Second) // Ensure the date command produces a different output + opt.Command = "date" + ckr := opt.run() + if ckr.Status != checkers.CRITICAL { + t.Errorf("Expected CRITICAL status, got %v", ckr.Status) + } + if !strings.Contains(ckr.Message, "found difference") { + t.Errorf("Expected found difference message, got %q", ckr.Message) + } +} From 98f7722be310f93f51b5bd07c7e540512009bed1 Mon Sep 17 00:00:00 2001 From: Masahiro Nagano Date: Sat, 18 Jul 2026 00:44:29 +0900 Subject: [PATCH 2/7] open with no_follow --- Makefile | 8 ++++---- main.go | 4 ++-- open_unix.go | 13 +++++++++++++ open_windows.go | 10 ++++++++++ 4 files changed, 29 insertions(+), 6 deletions(-) create mode 100644 open_unix.go create mode 100644 open_windows.go diff --git a/Makefile b/Makefile index 9077bdc..2c878b8 100644 --- a/Makefile +++ b/Makefile @@ -6,11 +6,11 @@ all: check-diff .PHONY: check-diff -check-diff: main.go - go build $(LDFLAGS) -o check-diff main.go +check-diff: main.go open_unix.go + go build $(LDFLAGS) -o check-diff open_unix.go main.go -linux: main.go - GOOS=linux GOARCH=amd64 go build $(LDFLAGS) -o check-diff main.go +linux: main.go open_unix.go + GOOS=linux GOARCH=amd64 go build $(LDFLAGS) -o check-diff open_unix.go main.go check: go test -v ./... diff --git a/main.go b/main.go index 23d5b4e..5f2857d 100644 --- a/main.go +++ b/main.go @@ -118,7 +118,7 @@ func (opt *Opt) run() *checkers.Checker { } func buildNoDifferenceMsg(filename string) (string, error) { - file, err := os.Open(filename) + file, err := openRD(filename) if err != nil { return "", err } @@ -151,7 +151,7 @@ func buildDiffMsg(diff string) string { } func getLines(filename string) ([]string, error) { - file, err := os.Open(filename) + file, err := openRD(filename) if err != nil { return nil, err } diff --git a/open_unix.go b/open_unix.go new file mode 100644 index 0000000..06d7031 --- /dev/null +++ b/open_unix.go @@ -0,0 +1,13 @@ +//go:build !windows +// +build !windows + +package main + +import ( + "os" + "syscall" +) + +func openRD(filename string) (*os.File, error) { + return os.OpenFile(filename, os.O_RDONLY|syscall.O_NOFOLLOW, 0) +} diff --git a/open_windows.go b/open_windows.go new file mode 100644 index 0000000..bfb80a6 --- /dev/null +++ b/open_windows.go @@ -0,0 +1,10 @@ +//go:build windows +// +build windows + +package main + +import "os" + +func openRD(filename string) (*os.File, error) { + return os.OpenFile(filename, os.O_RDONLY, 0) +} From 35c79aee983991cf5a081ad0519284db2b2908f2 Mon Sep 17 00:00:00 2001 From: Masahiro Nagano Date: Sat, 18 Jul 2026 00:48:06 +0900 Subject: [PATCH 3/7] fix --- .goreleaser.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.goreleaser.yml b/.goreleaser.yml index cb102b3..f73c945 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,6 +1,5 @@ builds: - - binary: mackerel-plugin-jsonl - main: ./cmd/mackerel-plugin-jsonl + - binary: check-diff goos: - linux goarch: From f8a5c2997544955e41be923560cea35a84fe0ac8 Mon Sep 17 00:00:00 2001 From: Masahiro Nagano Date: Sat, 18 Jul 2026 00:58:12 +0900 Subject: [PATCH 4/7] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- main.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/main.go b/main.go index 5f2857d..f4af304 100644 --- a/main.go +++ b/main.go @@ -24,7 +24,7 @@ var commit string type Opt struct { Args []string Command string - Identifier string `long:"identifier" description:"identify a file store the command result with given string"` + Identifier string `long:"identifier" description:"identify the file used to store the command result with the given string"` Warn bool `short:"w" long:"warn" description:"Set the error level to warning"` Workdir string `long:"workdir" description:"Set the working directory"` Version bool `short:"v" long:"version" description:"Show version"` @@ -124,7 +124,10 @@ func buildNoDifferenceMsg(filename string) (string, error) { } defer file.Close() - fileinfo, _ := file.Stat() + fileinfo, err := file.Stat() + if err != nil { + return "", err + } b := make([]byte, 128) count, err := file.Read(b) if err != nil { @@ -158,6 +161,7 @@ func getLines(filename string) ([]string, error) { defer file.Close() scanner := bufio.NewScanner(file) + scanner.Buffer(make([]byte, 64*1024), 10*1024*1024) lines := make([]string, 0) for scanner.Scan() { lines = append(lines, scanner.Text()) From 331ec9e157266588033912ea3ae872a1ec75912a Mon Sep 17 00:00:00 2001 From: Masahiro Nagano Date: Sat, 18 Jul 2026 16:23:34 +0900 Subject: [PATCH 5/7] refactor test --- main_test.go | 157 +++++++++++++++++---------------------------------- 1 file changed, 53 insertions(+), 104 deletions(-) diff --git a/main_test.go b/main_test.go index 95a91c9..7356404 100644 --- a/main_test.go +++ b/main_test.go @@ -11,24 +11,27 @@ import ( "github.com/mackerelio/checkers" ) -// Test cmd -func TestCmd(t *testing.T) { +func getCmdOpt(t *testing.T, command string, args []string) (Opt, *os.File) { workdir := t.TempDir() - opt := Opt{ - Command: "echo", - Args: []string{"Hello, World!"}, - Identifier: "test", - Workdir: workdir, - } filename := filepath.Join(workdir, "test.txt") file, err := os.Create(filename) if err != nil { t.Fatalf("Failed to create file: %v", err) } + return Opt{ + Command: command, + Args: args, + Identifier: "test", + Workdir: workdir, + }, file +} + +// Test cmd +func TestCmd(t *testing.T) { + opt, file := getCmdOpt(t, "echo", []string{"Hello, World!"}) defer file.Close() - err = opt.cmd(file) - if err != nil { + if err := opt.cmd(file); err != nil { t.Fatalf("cmd failed: %v", err) } @@ -46,56 +49,42 @@ func TestCmd(t *testing.T) { } func TestCmdFailedCase(t *testing.T) { - workdir := t.TempDir() - opt := Opt{ - Command: "nonexistent_command", - Args: []string{}, - Identifier: "test", - Workdir: workdir, - } - filename := filepath.Join(workdir, "test.txt") - file, err := os.Create(filename) - if err != nil { - t.Fatalf("Failed to create file: %v", err) - } + opt, file := getCmdOpt(t, "nonexistent_command", []string{}) defer file.Close() - err = opt.cmd(file) - if err == nil { + if err := opt.cmd(file); err == nil { t.Fatalf("Expected cmd to fail, but it succeeded") } } func TestCmdFailedCommand(t *testing.T) { - workdir := t.TempDir() - opt := Opt{ - Command: "ls", - Args: []string{"nonexistent_directory"}, - Identifier: "test", - Workdir: workdir, - } - filename := filepath.Join(workdir, "test.txt") - file, err := os.Create(filename) - if err != nil { - t.Fatalf("Failed to create file: %v", err) - } + opt, file := getCmdOpt(t, "ls", []string{"nonexistent_directory"}) defer file.Close() - - err = opt.cmd(file) - if err == nil { + if err := opt.cmd(file); err == nil { t.Fatalf("Expected cmd to fail, but it succeeded") } } -func TestBuildNoDifferenceMsg(t *testing.T) { - file, err := os.CreateTemp("", "testfile") +func createTempFile(t *testing.T, content string) (*os.File, func()) { + tmpfile, err := os.CreateTemp("", "testfile") if err != nil { t.Fatalf("Failed to create temp file: %v", err) } - defer os.Remove(file.Name()) - defer file.Close() + if _, err := tmpfile.WriteString(content); err != nil { + t.Fatalf("Failed to write to temp file: %v", err) + } + if err := tmpfile.Close(); err != nil { + t.Fatalf("Failed to close temp file: %v", err) + } + return tmpfile, func() { + os.Remove(tmpfile.Name()) + } +} + +func TestBuildNoDifferenceMsg(t *testing.T) { shortMsg := "This is a test message." - file.WriteString(shortMsg + "\n") + file, cleanup := createTempFile(t, shortMsg+"\n") + defer cleanup() expected := "no difference: ```" + shortMsg + "```" result, err := buildNoDifferenceMsg(file.Name()) if err != nil { @@ -107,18 +96,11 @@ func TestBuildNoDifferenceMsg(t *testing.T) { } func TestBuildNoDifferenceMsgLargeResult(t *testing.T) { - file, err := os.CreateTemp("", "testfile") - if err != nil { - t.Fatalf("Failed to create temp file: %v", err) - } - defer os.Remove(file.Name()) - defer file.Close() - largeMsg := make([]byte, 600) - for i := range largeMsg { - largeMsg[i] = 'A' - } - file.Write(largeMsg) - expected := "no difference: ```" + string(largeMsg)[:128] + "...```" + largeMsg := strings.Repeat("A", 600) + file, cleanup := createTempFile(t, largeMsg+"\n") + defer cleanup() + + expected := "no difference: ```" + largeMsg[:128] + "...```" result, err := buildNoDifferenceMsg(file.Name()) if err != nil { t.Fatalf("buildNoDifferenceMsg failed: %v", err) @@ -138,33 +120,20 @@ func TestBuildDiffMsg(t *testing.T) { } func TestBuildDiffMsgLargeResult(t *testing.T) { - diff := make([]byte, 600) - for i := range diff { - diff[i] = 'A' - } - expected := "found difference: ```" + string(diff)[:512] + "...```" - result := buildDiffMsg(string(diff)) + diff := strings.Repeat("A", 600) + expected := "found difference: ```" + diff[:512] + "...```" + result := buildDiffMsg(diff) if result != expected { t.Errorf("Expected %q, got %q", expected, result) } } func TestDiff(t *testing.T) { - file1, err := os.CreateTemp("", "file1") - if err != nil { - t.Fatalf("Failed to create temp file: %v", err) - } - defer os.Remove(file1.Name()) - defer file1.Close() - file1.WriteString("Hello, World!\n") + file1, cleanup1 := createTempFile(t, "Hello, World!\n") + defer cleanup1() - file2, err := os.CreateTemp("", "file2") - if err != nil { - t.Fatalf("Failed to create temp file: %v", err) - } - defer os.Remove(file2.Name()) - defer file2.Close() - file2.WriteString("Hello, Go!\n") + file2, cleanup2 := createTempFile(t, "Hello, Go!\n") + defer cleanup2() diffResult, err := diff(file1.Name(), file2.Name()) if err != nil { @@ -179,21 +148,11 @@ func TestDiff(t *testing.T) { } func TestDiffNoDifference(t *testing.T) { - file1, err := os.CreateTemp("", "file1") - if err != nil { - t.Fatalf("Failed to create temp file: %v", err) - } - defer os.Remove(file1.Name()) - defer file1.Close() - file1.WriteString("Hello, World!\n") + file1, cleanup1 := createTempFile(t, "Hello, World!\n") + defer cleanup1() - file2, err := os.CreateTemp("", "file2") - if err != nil { - t.Fatalf("Failed to create temp file: %v", err) - } - defer os.Remove(file2.Name()) - defer file2.Close() - file2.WriteString("Hello, World!\n") + file2, cleanup2 := createTempFile(t, "Hello, World!\n") + defer cleanup2() diffResult, err := diff(file1.Name(), file2.Name()) if err != nil { @@ -205,13 +164,8 @@ func TestDiffNoDifference(t *testing.T) { } func TestRun(t *testing.T) { - workdir := t.TempDir() - opt := Opt{ - Command: "echo", - Args: []string{"Hello, World!"}, - Identifier: "test", - Workdir: workdir, - } + opt, file := getCmdOpt(t, "echo", []string{"Hello, World!"}) + defer file.Close() ckr := opt.run() if ckr.Status != checkers.OK { t.Errorf("Expected OK status, got %v", ckr.Status) @@ -231,13 +185,8 @@ func TestRun(t *testing.T) { } func TestRunWithDifference(t *testing.T) { - workdir := t.TempDir() - opt := Opt{ - Command: "date", - Args: []string{}, - Identifier: "test", - Workdir: workdir, - } + opt, file := getCmdOpt(t, "date", []string{}) + defer file.Close() // First run to create the initial state opt.run() From d69b531e983d11b43033d0ebf99349bb041256dc Mon Sep 17 00:00:00 2001 From: Masahiro Nagano Date: Sat, 18 Jul 2026 16:38:58 +0900 Subject: [PATCH 6/7] refactor run --- main.go | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/main.go b/main.go index f4af304..e4408f1 100644 --- a/main.go +++ b/main.go @@ -47,15 +47,14 @@ func (opt *Opt) cmd(file *os.File) error { func (opt *Opt) run() *checkers.Checker { + identifier := strings.Join([]string{ + opt.Identifier, + opt.Command, + strings.Join(opt.Args, "-"), + }, "-") hasher := sha256.New() - hasher.Write([]byte(opt.Identifier)) - hasher.Write([]byte("-")) - hasher.Write([]byte(opt.Command)) - hasher.Write([]byte("-")) - for _, v := range opt.Args { - hasher.Write([]byte(v)) - hasher.Write([]byte("-")) - } + hasher.Write([]byte(identifier)) + curUser, err := user.Current() if err != nil { return checkers.Critical(err.Error()) @@ -84,13 +83,10 @@ func (opt *Opt) run() *checkers.Checker { if err != nil { return checkers.Critical(err.Error()) } - msg := "" if len(opt.Args) > 0 { - msg = fmt.Sprintf("first time execution command: '%s %s'", opt.Command, strings.Join(opt.Args, " ")) - } else { - msg = fmt.Sprintf("first time execution command: '%s'", opt.Command) + return checkers.Ok(fmt.Sprintf("first time execution command: '%s %s'", opt.Command, strings.Join(opt.Args, " "))) } - return checkers.Ok(msg) + return checkers.Ok(fmt.Sprintf("first time execution command: '%s'", opt.Command)) } diff, err := diff(prevPath, newFile.Name()) @@ -110,6 +106,7 @@ func (opt *Opt) run() *checkers.Checker { } return checkers.Ok(msg) } + diffMsg := buildDiffMsg(diff) if opt.Warn { return checkers.Warning(diffMsg) From b252a861041faa409ee27e9488471f0382ae9b5e Mon Sep 17 00:00:00 2001 From: Masahiro Nagano Date: Sat, 18 Jul 2026 16:44:20 +0900 Subject: [PATCH 7/7] refactor Makefile and comments --- Makefile | 4 ++-- README.md | 2 +- open_unix.go | 2 ++ open_windows.go | 2 ++ 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 2c878b8..652283c 100644 --- a/Makefile +++ b/Makefile @@ -6,8 +6,8 @@ all: check-diff .PHONY: check-diff -check-diff: main.go open_unix.go - go build $(LDFLAGS) -o check-diff open_unix.go main.go +check-diff: main.go open_unix.go open_windows.go + go build $(LDFLAGS) -o check-diff . linux: main.go open_unix.go GOOS=linux GOARCH=amd64 go build $(LDFLAGS) -o check-diff open_unix.go main.go diff --git a/README.md b/README.md index 2fede54..b8983fe 100644 --- a/README.md +++ b/README.md @@ -47,4 +47,4 @@ command = "/usr/local/bin/check-diff -- cat /etc/passwd" ## Install -Please download release page or `mkr plugin install monitoring-forge/check-diff`. \ No newline at end of file +Please download release page or `mkr plugin install monitoring-forge/check-diff`. diff --git a/open_unix.go b/open_unix.go index 06d7031..d4ff433 100644 --- a/open_unix.go +++ b/open_unix.go @@ -8,6 +8,8 @@ import ( "syscall" ) +// openRD opens a file for reading with O_NOFOLLOW flag to prevent symlink attacks. +// This function is specific to Unix-like systems where syscall.O_NOFOLLOW is available. func openRD(filename string) (*os.File, error) { return os.OpenFile(filename, os.O_RDONLY|syscall.O_NOFOLLOW, 0) } diff --git a/open_windows.go b/open_windows.go index bfb80a6..1f6d94d 100644 --- a/open_windows.go +++ b/open_windows.go @@ -5,6 +5,8 @@ package main import "os" +// openRD opens a file for reading with appropriate flags for Windows. +// Since Windows doesn't support O_NOFOLLOW, we simply open the file with O_RDONLY. func openRD(filename string) (*os.File, error) { return os.OpenFile(filename, os.O_RDONLY, 0) }