Improve releasing#75
Merged
Merged
Conversation
This reverts commit 0e92aa046a31cf9eaa1873805e0b73f0a7f9d593.
Reviewer's GuideThis PR refactors and centralizes ancillary tooling into a new internal package, implements automated doc-comment handling and README synchronization via go:generate, broadens test coverage for these utilities, and enhances CI workflows to validate changelogs and automate release tagging. Sequence diagram for automated docstring and README update via go:generatesequenceDiagram
participant Dev as Developer
participant GoGen as "go generate"
participant UpdateScript as "update_docstring_and_readme.go"
participant Internal as "internal package"
participant Repo as "Repository Files"
Dev->>GoGen: run go generate
GoGen->>UpdateScript: execute update_docstring_and_readme.go
UpdateScript->>Internal: Build() to get CLI binary
UpdateScript->>UpdateScript: Run CLI binary with --help
UpdateScript->>Internal: SetDocComment(main.go, helptext)
UpdateScript->>Internal: ReplaceBetween(README.md, helptext)
UpdateScript->>Repo: Write updated main.go and README.md
Class diagram for new and refactored internal package utilitiesclassDiagram
class docComment {
+string Text
+token.Position Start
+token.Position End
}
class internal {
+Copy(sourcePath string, target string) error
+Build(tags []string) string
+RemoveBuildDir(bin string)
+SetDocComment(path string, comment string) error
+ReplaceBetween(original io.Reader, marker1 string, marker2 string, replacement []byte) []byte
+HashFile(path string) ([]byte, error)
}
docComment <.. internal: uses
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #75 +/- ##
==========================================
- Coverage 74.11% 74.02% -0.09%
==========================================
Files 8 11 +3
Lines 506 566 +60
==========================================
+ Hits 375 419 +44
- Misses 95 104 +9
- Partials 36 43 +7 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Hey there - I've reviewed your changes and they look great!
Blocking issues:
- Detected non-static command inside Command. Audit the input to 'exec.Command'. If unverified user data can reach this call site, this is a code injection vulnerability. A malicious actor can inject a malicious script to execute arbitrary code. (link)
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location> `internal/generators.go:36` </location>
<code_context>
+ return docComment{Text: doccomment, Start: startpos, End: endpos}, nil
+}
+
+func SetDocComment(path string, comment string) error {
+ comment = "/*\n" + comment + "*/\n"
+ oldComment, err := getDocComment(path)
</code_context>
<issue_to_address>
**issue (bug_risk):** File permissions are not preserved when writing the updated file.
os.WriteFile is called with a permission mode of 0, which removes all permissions. Pass the original file's permissions from Stat().Mode() to preserve them.
</issue_to_address>
### Comment 2
<location> `internal/build.go:13` </location>
<code_context>
+)
+
+// Cleans up the tmp directory created by Build()
+func RemoveBuildDir(bin string) {
+ buildDir := filepath.Dir(bin)
+ if err := os.RemoveAll(buildDir); err != nil {
</code_context>
<issue_to_address>
**suggestion (bug_risk):** RemoveBuildDir panics if it cannot remove the directory.
Instead of panicking, log or return the error to avoid unexpected crashes during cleanup, especially when used with defer.
Suggested implementation:
```golang
import (
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
)
```
```golang
// Cleans up the tmp directory created by Build()
func RemoveBuildDir(bin string) {
buildDir := filepath.Dir(bin)
if err := os.RemoveAll(buildDir); err != nil {
log.Printf("cannot remove temporary directory used for build output: %v", err)
}
}
```
</issue_to_address>
### Comment 3
<location> `cmd/snaggle/update_docstring_and_readme.go:23` </location>
<code_context>
+ . "github.com/MusicalNinjaDad/snaggle/internal"
+)
+
+func main() {
+ exitcode := 0
+
</code_context>
<issue_to_address>
**issue (complexity):** Consider refactoring repeated error handling and file update logic into helper functions to clarify intent and reduce boilerplate.
```go
// helpers.go
package main
import (
"fmt"
"os"
"os/exec"
"github.com/MusicalNinjaDad/snaggle/internal"
"golang.org/x/exp/slices"
)
// must is a helper that exits with code 3 on error.
func must(err error) {
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(3)
}
}
// mustHash returns the file’s hash or exits with code 3.
func mustHash(path string) []byte {
h, err := internal.HashFile(path)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(3)
}
return h
}
// runOrExit runs a command and returns its stdout or exits with code 3.
func runOrExit(name string, args ...string) []byte {
out, err := exec.Command(name, args...).Output()
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(3)
}
return out
}
// updateAndDetect applies updateFn to a file and returns true if its hash changed.
func updateAndDetect(path string, updateFn func()) bool {
before := mustHash(path)
updateFn()
after := mustHash(path)
return !slices.Equal(before, after)
}
```
```go
// main.go (refactored)
package main
import (
"fmt"
"os"
"path/filepath"
"runtime"
"github.com/MusicalNinjaDad/snaggle/internal"
)
func main() {
exitcode := 0
_, thisFile, _, _ := runtime.Caller(0)
thisDir := filepath.Dir(thisFile)
workspaceRoot := filepath.Join(thisDir, "../..")
// 1) Update main.go doc comment
mainGo := filepath.Join(thisDir, "main.go")
snaggleBin := Build(nil)
help := runOrExit(snaggleBin, "--help")
if updateAndDetect(mainGo, func() {
must(SetDocComment(mainGo, string(help)))
runOrExit("go", "fmt", mainGo)
}) {
fmt.Fprintf(os.Stderr, "%s updated\n", mainGo)
exitcode = 1
}
// 2) Update README.md code block
readme := filepath.Join(workspaceRoot, "README.md")
orig := mustHash(readme)
// ReplaceBetween reads, updates, and writes the file in one step
updated := ReplaceBetweenFile(readme, "snaggle --help", "```", help)
must(os.WriteFile(readme, updated, 0644))
if !slices.Equal(orig, mustHash(readme)) {
fmt.Fprintf(os.Stderr, "%s updated\n", readme)
exitcode = 1
}
os.Exit(exitcode)
}
```
Steps to apply:
1. Pull out the repeated error-checking into `must`, `mustHash`, and `runOrExit`.
2. Introduce `updateAndDetect` to handle the hash-compare-update pattern for any file.
3. Simplify `main()` to two high-level blocks (one for `main.go`, one for `README.md`), each invoking the helpers.
4. Add a `ReplaceBetweenFile` (or similar) to encapsulate read/modify/write for README.
This preserves all behavior, eliminates boilerplate, and makes each block’s intent clear.
</issue_to_address>
### Comment 4
<location> `internal/copy.go:15` </location>
<code_context>
+//
+// Errors returned will be of type [*fs.PathError] (unless they came from [io.Copy],
+// which sadly doesn't document error details ...)
+func Copy(sourcePath string, target string) error {
+ src, err := os.Open(sourcePath)
+ if err != nil {
</code_context>
<issue_to_address>
**issue (complexity):** Consider consolidating deferred resource cleanup and reducing repeated type casts to simplify the Copy function.
Here are two small refactorings that preserve exactly the same behavior, but remove a handful of defers and eliminate all of the repeated `Sys().(*syscall.Stat_t)` casts:
1) consolidate `Close` / `Sync` into a single defer
2) do the `Stat_t` cast once for `src` (and once for the error‐branch `dst`)
```go
func Copy(sourcePath, target string) (err error) {
src, err := os.Open(sourcePath)
if err != nil {
return err
}
// only need one defer for Close(src)
defer func() {
if c := src.Close(); c != nil {
err = errors.Join(err, c)
}
}()
srcInfo, err := src.Stat()
if err != nil {
return err
}
st := srcInfo.Sys().(*syscall.Stat_t)
su, sg := int(st.Uid), int(st.Gid)
dst, err := os.OpenFile(target,
os.O_WRONLY|os.O_CREATE|os.O_EXCL,
srcInfo.Mode(),
)
if err != nil {
return err
}
// single defer for Sync+Close(dst)
defer func() {
if s := dst.Sync(); s != nil {
err = errors.Join(err, s)
}
if c := dst.Close(); c != nil {
err = errors.Join(err, c)
}
}()
if _, err := io.Copy(dst, src); err != nil {
return err
}
if ch := dst.Chown(su, sg); ch != nil {
switch {
case errors.Is(ch, syscall.EPERM):
// ignore
default:
// get actual ownership to include in the error
dsts, _ := dst.Stat()
dt := dsts.Sys().(*syscall.Stat_t)
du, dg := int(dt.Uid), int(dt.Gid)
return fmt.Errorf("%w (src: %d:%d, dst: %d:%d)", ch, su, sg, du, dg)
}
}
return nil
}
```
This keeps all of your ownership and error‐joining logic exactly the same but:
- removes two extra deferred closures
- avoids repeating `Sys().(*syscall.Stat_t)` four times, replacing them with a single cast and local `su, sg` (and `du, dg` in the EPERM branch)
</issue_to_address>
### Comment 5
<location> `cmd/snaggle/update_docstring_and_readme.go:39` </location>
<code_context>
helptext, err := exec.Command(snaggleBin, "--help").Output()
</code_context>
<issue_to_address>
**security (go.lang.security.audit.dangerous-exec-command):** Detected non-static command inside Command. Audit the input to 'exec.Command'. If unverified user data can reach this call site, this is a code injection vulnerability. A malicious actor can inject a malicious script to execute arbitrary code.
*Source: opengrep*
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
no user data involvement
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary by Sourcery
Automate and streamline the release process by centralizing file utilities, generating and validating documentation, enforcing changelog and help-text consistency in CI, and auto-tagging new versions.
New Features:
snaggle --helpoutputEnhancements:
CI:
Documentation:
Tests:
Chores: