Skip to content

Improve releasing#75

Merged
MusicalNinjaDad merged 40 commits into
mainfrom
releasing
Oct 30, 2025
Merged

Improve releasing#75
MusicalNinjaDad merged 40 commits into
mainfrom
releasing

Conversation

@MusicalNinjaDad

@MusicalNinjaDad MusicalNinjaDad commented Oct 30, 2025

Copy link
Copy Markdown
Owner

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:

  • Add internal utilities for parsing/setting doc comments, copying files, and building test binaries
  • Introduce a go:generate tool to sync the main command’s docstring and README with snaggle --help output
  • Add GitHub Actions workflows to validate changelog entries, ensure help-text consistency, and auto-create version tags

Enhancements:

  • Refactor link logic to use internal.Copy and consolidate file-copy code
  • Refactor CLI tests to use internal.Build and RemoveBuildDir helpers
  • Mark autogenerated sections in README and main.go docstring for maintainability

CI:

  • Add checks.yml workflow for changelog and help-text validation
  • Add auto-release.yml workflow to tag new versions on version.go changes
  • Include a pre-commit script to ensure changelog entries match the version

Documentation:

  • Update README.md with autogenerated markers around the CLI help output

Tests:

  • Add unit tests for GetDocComment, SetDocComment, and ReplaceBetween functions

Chores:

  • Add version.go with Version set to “0.3.0”

@sourcery-ai

sourcery-ai Bot commented Oct 30, 2025

Copy link
Copy Markdown

Reviewer's Guide

This 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:generate

sequenceDiagram
  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
Loading

Class diagram for new and refactored internal package utilities

classDiagram
  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
Loading

File-Level Changes

Change Details Files
Extracted and exported utility functions into internal package
  • Moved file copy logic into internal.Copy and updated calls in snaggle.go
  • Exported HashFile and updated sameHash to use it
  • Introduced Build and RemoveBuildDir for test binary management
  • Exposed ReplaceBetween and SetDocComment for content manipulation
internal/copy.go
internal/build.go
internal/comparisons.go
internal/generators.go
snaggle.go
cmd/snaggle/cli_test.go
Implemented doc comment management and auto-generation
  • Added getDocComment and SetDocComment to parse/update multi-line doc comments
  • Added ReplaceBetween to swap text between markers
  • Added go:generate directive and update_docstring_and_readme.go to sync help text
  • Updated main.go and README.md to include auto-generated sections
internal/generators.go
cmd/snaggle/main.go
cmd/snaggle/update_docstring_and_readme.go
README.md
Expanded test coverage and refactored tests to use new utilities
  • Added tests for GetDoccomment, SetDocComment, and ReplaceBetween
  • Refactored cli_test.go to use internal.Build and RemoveBuildDir
internal/internal_test.go
cmd/snaggle/cli_test.go
Added CI workflows for changelog validation and automated releases
  • Added GitHub Actions for changelog entry checks and help-text consistency
  • Created workflow to tag new releases on version.go updates
  • Added pre-commit script to enforce CHANGELOG entries
  • Bumped Version to 0.3.0
.github/workflows/checks.yml
.github/workflows/auto-release.yml
.pre-commit/check_changelog.sh
CHANGELOG
version.go

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@codecov

codecov Bot commented Oct 30, 2025

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 69.23077% with 32 lines in your changes missing coverage. Please review.
✅ Project coverage is 74.02%. Comparing base (58615fb) to head (ff852f1).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
internal/copy.go 60.00% 12 Missing and 4 partials ⚠️
internal/build.go 55.55% 5 Missing and 3 partials ⚠️
internal/generators.go 80.95% 4 Missing and 4 partials ⚠️
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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

sourcery-ai[bot]
sourcery-ai Bot previously requested changes Oct 30, 2025

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread internal/generators.go
Comment thread internal/build.go
Comment thread cmd/snaggle/update_docstring_and_readme.go
@MusicalNinjaDad MusicalNinjaDad dismissed sourcery-ai[bot]’s stale review October 30, 2025 17:32

no user data involvement

@MusicalNinjaDad MusicalNinjaDad enabled auto-merge (squash) October 30, 2025 17:36
@MusicalNinjaDad MusicalNinjaDad merged commit 0809630 into main Oct 30, 2025
9 of 11 checks passed
@MusicalNinjaDad MusicalNinjaDad deleted the releasing branch October 30, 2025 17:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant