Both init (initModuleGroup) and release (processModule) now compare the
generated content against the file on disk before writing. If identical,
the file is left untouched — preserving any compatible local edits.
Also fix processModule commit message scope to chore(<prefix>): add
.goreleaser.yaml, matching the convention already used by initModuleGroup.
- Track isNewFile before writing so updates to existing files are never
auto-committed (mirrors the same rule already in processModule/release)
- Gate auto-bump on isNewFile as well — no tag when just refreshing yaml
- Fix commit message scope: chore(<prefix>): add .goreleaser.yaml
(was chore(release): add .goreleaser.yaml for <name>)
The go.mod boundary check added to findMainPackages was stopping the
--recursive walk from descending into child module directories, so
modules like auth/csvauth/ were never found.
The check was redundant: groupByModule already uses findModuleRoot to
attribute each binary to its nearest go.mod ancestor, and initModuleGroup
already skips modules with uncommitted changes (an untracked go.mod
counts as an uncommitted change). Removing the stop restores the
pre-regression behaviour.
Add Step 3 "git push && git push --tags" to the generated release script,
between the tag creation step and the goreleaser build step. This fixes
HTTP 422 "Release.target_commitish is invalid" errors from the GitHub API,
which occur when the local commit (auto-created for a new .goreleaser.yaml)
has not yet been pushed to the remote.
If .goreleaser.yaml did not exist:
→ write it, commit it, auto-tag patch (if sole new commit since last tag)
If .goreleaser.yaml already existed:
→ write the updated file, stop — no auto-commit, no auto-tag
monorel release now mirrors the init lifecycle for the goreleaser config:
1. Write .goreleaser.yaml (always regenerate; warn on stock {{ .ProjectName }})
2. git add + commit it if the file changed
3. Auto-tag patch — but only if the yaml commit is the sole new commit
since the last stable tag (same heuristic as monorel init).
If there are >1 new commits, print a note and skip auto-tag so the
user can choose the right semver component with monorel bump.
4. Compute version info *after* the yaml commit and auto-tag, so the
generated script reflects the tag that was just created.
The previous commit was too conservative — it prevented release from
updating an existing config. Restore the original write-always behaviour
but keep the {{ .ProjectName }} monorepo warning. The file is written
unconditionally; the user commits it manually when satisfied.
Previously `monorel release` always overwrote .goreleaser.yaml, discarding
any manual customisations the user had made.
New behaviour:
- Missing → write the monorel-generated config and commit it (same as init)
- Exists, stock {{ .ProjectName }} in a monorepo subdir → warn and ask the
user to update it manually before re-running
- Exists, looks fine → print "leaving unchanged" and continue
Instead of emitting a runtime git log command in Step 4, run git log at
monorel-invocation time and embed the actual commit list as a
single-quoted bash string (with '\'' escaping for embedded apostrophes).
my_release_notes='- abc1234 first commit
- def5678 fix: don'\''t crash'
This makes the generated script self-contained and shows the user exactly
which commits will appear in the release notes before they run the script.
Removes the now-unused gitPathSpec local variable.
- findMainPackages: stop descending into child directories that have their
own go.mod on disk (even if untracked), treating them as independent
module roots so they are never double-counted with the parent module
- initModuleGroup: skip if the module has uncommitted changes (excluding
files inside child module directories so a freshly-run --cmd step does
not block the parent); new helpers hasUncommittedChanges,
findChildModuleRoots
- monorel init --cmd: scans recursively for direct children of cmd/
directories that contain package main but have no go.mod yet, runs
go mod init + tidy for each, and prints a suggested commit at the end;
new helpers initCmdModules, readModulePath, runPrintIn
Add buildTrackedDirs which runs "git ls-files" once from the walk root
and builds a set of directories containing at least one tracked file.
findMainPackages uses this set to skip untracked directories (dist/,
vendor/, node_modules/, build artifacts, etc.) without having to
enumerate git-ignored paths explicitly.
Falls back to walking everything when git is unavailable or the
directory is not inside a repository.
- All three subcommands now print "found binary …" and "found module …"
before processing each module group, with a blank line between groups
- initModuleGroup, bumpModuleTag, processModule: downgrade the prefix==""
(repository root) case from fatal error to a skip warning so that
-recursive runs continue past root-level go.mod packages instead of
aborting
Extract findLatestStableTag helper to avoid duplicating tag-scan logic.
In initModuleGroup, count commits since the last stable tag after writing
.goreleaser.yaml. Only auto-bump when ≤1 new commit exists (i.e. the
goreleaser.yaml addition is the sole change). When more commits are present,
print a note and let the user run 'monorel bump' explicitly with the right
semver component.
- Same-commit guard: downgrade from fatal error to a skip warning so
-recursive bump continues to the next module instead of aborting
- bump/init: add -dry-run flag; prints what would happen without making
any git commits or tags
- findMainPackages: skip dot/underscore-prefixed dirs by default; warn
(not error) on ReadDir failures when -A is set
- expandPaths, runRelease, runBump, runInit: thread -A flag through
- bumpModuleTag: refuse to tag a commit already tagged by the previous
stable release; -force creates an empty bump commit instead
-recursive flag (all three subcommands)
expandPaths() / findMainPackages() walk the directory tree looking for
`package main` directories, honouring stopMarkers (skips .git dirs so
the walk never crosses a repository boundary). Passes collected paths
straight to groupByModule, which handles module grouping as before.
monorel init -recursive . # init every module under .
monorel bump -recursive . # bump patch for every module
monorel release -recursive . # release script for every module
Remove GORELEASER_CURRENT_TAG from generated release script
The non-pro goreleaser does not support it. VERSION is still exported
for use via {{.Env.VERSION}} in the goreleaser YAML templates.
Indent the goreleaser subshell in the release script
Before: ( cd "auth/csvauth" && goreleaser release ... )
After:
(
cd "auth/csvauth"
goreleaser release --clean --skip=validate,announce
)
In a monorepo the module's most recent commit is often behind HEAD
(other modules may have been committed on top).
git log --format=%H -1 -- .
run from the module root returns the SHA of the last commit that
touched that directory; we pass it explicitly to `git tag <tag> <sha>`
instead of letting git default to HEAD.
Restructure monorel to use flag.FlagSet-based subcommand dispatch so that
future subcommands can each carry their own flags cleanly.
monorel release <binary-path>...
Existing behaviour: write .goreleaser.yaml and print a bash release
script. Now a named subcommand; no behaviour change.
monorel bump [-m major|minor|patch] <binary-path>...
Create a new semver git tag at HEAD for each module.
-m defaults to "patch". Using a flag (-m) rather than a positional
argument avoids ambiguity with binary paths that might literally be
named "minor" or "patch".
monorel init <binary-path>...
For each module (in command-line order): write .goreleaser.yaml,
commit it (skipped when file is unchanged), then run bump patch.
1. Script paths relative to invoking CWD (not module root):
- git log pathspec: "-- relPath/" instead of "-- ./"
- artifact globs: relPath/dist/ instead of ./dist/
- goreleaser only: ( cd "relPath" && goreleaser ... ) inline subshell
- when relPath==".": all paths use ./ and no cd is emitted
The outer ( subshell ) wrapper is removed; each command is now
copy-pasteable from the directory where monorel was invoked.
2. POSIX variable for release notes:
RELEASE_NOTES= → <project>_release_notes= (no export; goreleaser
does not need it; multiple modules no longer share the same name).
3. Warn before overwriting .goreleaser.yaml when:
- the existing file contains {{ .ProjectName }} (stock config), AND
- the module is a monorepo subdirectory (go.mod not adjacent to .git/)
The file is still updated; the warning alerts the user that a
non-monorel config was replaced.
Add checkPackageMain(), which uses go/parser with PackageClauseOnly mode
(reads only the package-declaration token of each .go file — very fast)
to verify the resolved binary directory declares `package main`.
Called in groupByModule for every argument after resolving the absolute
path, before findModuleRoot. Produces a helpful error, e.g.:
monorel: error: .../io/transform/gsheet2csv is package "gsheet2csv",
not a main package
Major refactor: monorel no longer requires being run from the module root.
It now accepts paths to binary packages from any ancestor directory and
walks up from each path to find go.mod (groupByModule / findModuleRoot).
Binaries sharing a module root are grouped together. When the script must
cd into a module directory (multi-module run or different CWD) the per-module
output is wrapped in a bash subshell to prevent directory leakage.
The .git stop-marker now only triggers on a .git DIRECTORY, not a .git FILE.
A .git file means the path is inside a submodule whose real repository root
is further up the tree, so the search continues upward.
Any directory on the path from the module root to a binary that
contains its own go.mod is a separate module; monorel should not try
to manage it. Two cases are now caught with distinct errors:
../other → "outside the module directory"
./cmd/go.mod → "has its own go.mod" (intermediate dir)
./cmd/foo/go.mod → "has its own go.mod" (binary dir itself)
Both suggest the correct fix: cd into that directory and run monorel
from there.
The tool now requires at least one positional argument — the path(s) to
the Go main package(s) to build — and must be run from the module root
(the directory containing go.mod).
# single binary (module root is the main package)
monorel .
# multiple binaries under one module
monorel ./cmd/gsheet2csv ./cmd/gsheet2tsv ./cmd/gsheet2env
Changes:
- Add `binary` struct {name, mainPath} and `parseBinaries()`
- "." is special-cased: binary name is taken from the CWD, not "."
- filepath.Clean's "./"-stripping is undone so goreleaser sees an
explicit relative path (./cmd/foo not cmd/foo)
- `goreleaserYAML` now takes `projectName + []binary`
- Each binary gets its own `builds` entry (with `id:` and `main:`)
and its own `archives` entry (with `ids:` to link it to the build)
- `main:` is omitted when mainPath is "." (goreleaser default)
- Checksum is named <projectName>_VERSION_checksums.txt
- `printScript` takes `projectName + []binary`
- Summary line says "Binaries:" (plural) when more than one
- Upload step globs tar.gz + zip for every binary, then the checksum
- Require go.mod in CWD; error out with usage message when no args given
Also regenerates cmd/tcpfwd/.goreleaser.yaml via the new code path
(adds `id: tcpfwd` to builds/archives; no functional change otherwise).
Adds a standalone Go CLI tool (tools/monorel) that automates the
goreleaser + gh release workflow for modules living in a subdirectory
of a monorepo where goreleaser Pro is not available.
Run from any module subdirectory (e.g. cmd/tcpfwd):
monorel # writes .goreleaser.yaml + prints release script
monorel --help # (flag defaults)
What the tool does:
- Detects module path and binary name from git prefix
- Lists and semver-sorts tags matching <prefix>/v* (e.g. cmd/tcpfwd/v*)
- Computes version: exact tag → stable release; commits/dirty → pre-release
- Writes (or updates) .goreleaser.yaml with the binary name hard-coded,
{{.Env.VERSION}} used for filenames instead of the prefixed tag,
and release.disable: true (gh handles the GitHub Release)
- Prints a numbered bash script covering env vars, optional git tag,
goreleaser build, release notes, and gh release create/upload/publish
Also updates cmd/tcpfwd/.goreleaser.yaml (first output from monorel):
- Fixes stray trailing quote in ldflags
- Sets release.disable: true (was release.footer)
- Adds generated-by header comment
- connEntry.isIdle(now, threshold): caller supplies now instead of time.Since
- connRegistry.closeIdle(now, threshold): passes now through to isIdle
- trackingConn gains a clock func() time.Time field used in Read/Write
- handleConn takes clock func() time.Time; uses it to init lastRead/lastWrite
and passes it to trackingConn
- Call sites in main pass time.Now or time.Now() explicitly
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
On SIGINT/SIGTERM:
- Stop accepting new connections
- Close connections idle longer than --idle-timeout (default 5s),
determined by LastRead/LastWrite timestamps tracked per connection pair
- Wait for active connections to drain up to --shutdown-timeout (default 30s)
- Force-close any remaining connections if the timeout is exceeded
Also switches isClosedConn to use errors.Is(err, net.ErrClosed) and
exits the accept loop cleanly when a listener is closed during shutdown.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Accept any number of local-port:remote-host:remote-port forwards as
positional arguments. Backward-compatible with existing --port/--target
flags. Adds --version/-V/version and --help/help handling matching the
auth-proxy pattern, including printVersion printed to stderr at startup.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>