fix(monorel): explicit paths, POSIX vars, goreleaser.yaml warning

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.
This commit is contained in:
AJ ONeal 2026-02-28 11:24:54 -07:00
parent 76fbf74444
commit 9812f52ee9
No known key found for this signature in database

View File

@ -80,7 +80,6 @@ func main() {
} }
cwd, _ := os.Getwd() cwd, _ := os.Getwd()
multiModule := len(groups) > 1
// Emit the bash header exactly once. // Emit the bash header exactly once.
fmt.Println("#!/usr/bin/env bash") fmt.Println("#!/usr/bin/env bash")
@ -90,10 +89,7 @@ func main() {
for _, group := range groups { for _, group := range groups {
relPath, _ := filepath.Rel(cwd, group.root) relPath, _ := filepath.Rel(cwd, group.root)
relPath = filepath.ToSlash(relPath) relPath = filepath.ToSlash(relPath)
// Wrap in a subshell when the script has to cd somewhere, so multiple processModule(group, relPath)
// module sections don't interfere with each other.
wrap := multiModule || relPath != "."
processModule(group, relPath, wrap)
} }
} }
@ -217,9 +213,9 @@ func groupByModule(args []string) ([]*moduleGroup, error) {
// processModule writes .goreleaser.yaml and emits the release-script section // processModule writes .goreleaser.yaml and emits the release-script section
// for one module group. relPath is the path from the caller's CWD to the // for one module group. relPath is the path from the caller's CWD to the
// module root (used for the cd step in the script). wrap=true wraps the // module root; it is used in the script for all paths so that the script can
// output in a bash subshell. // be run from the directory where monorel was invoked.
func processModule(group *moduleGroup, relPath string, wrap bool) { func processModule(group *moduleGroup, relPath string) {
modRoot := group.root modRoot := group.root
bins := group.bins bins := group.bins
@ -279,15 +275,27 @@ func processModule(group *moduleGroup, relPath string, wrap bool) {
} }
// Write .goreleaser.yaml next to go.mod. // Write .goreleaser.yaml next to go.mod.
// Warn if an existing file uses {{ .ProjectName }} (stock goreleaser config)
// and the module is a monorepo subdirectory (go.mod not adjacent to .git/).
yamlContent := goreleaserYAML(projectName, bins) yamlContent := goreleaserYAML(projectName, bins)
yamlPath := filepath.Join(modRoot, ".goreleaser.yaml") yamlPath := filepath.Join(modRoot, ".goreleaser.yaml")
if existing, err := os.ReadFile(yamlPath); err == nil {
hasProjectName := strings.Contains(string(existing), "{{ .ProjectName }}") ||
strings.Contains(string(existing), "{{.ProjectName}}")
gitInfo, gitErr := os.Stat(filepath.Join(modRoot, ".git"))
atGitRoot := gitErr == nil && gitInfo.IsDir()
if hasProjectName && !atGitRoot {
fmt.Fprintf(os.Stderr, "warning: %s: contains {{ .ProjectName }} but module is a monorepo subdirectory;\n", yamlPath)
fmt.Fprintf(os.Stderr, " replacing stock goreleaser config with monorel-generated config.\n")
}
}
if err := os.WriteFile(yamlPath, []byte(yamlContent), 0o644); err != nil { if err := os.WriteFile(yamlPath, []byte(yamlContent), 0o644); err != nil {
fatalf("writing %s: %v", yamlPath, err) fatalf("writing %s: %v", yamlPath, err)
} }
fmt.Fprintf(os.Stderr, "wrote %s\n", yamlPath) fmt.Fprintf(os.Stderr, "wrote %s\n", yamlPath)
headSHA := mustRunIn(modRoot, "git", "rev-parse", "HEAD") headSHA := mustRunIn(modRoot, "git", "rev-parse", "HEAD")
printModuleScript(relPath, wrap, projectName, bins, printModuleScript(relPath, projectName, bins,
version, currentTag, prevTag, repoPath, headSHA, version, currentTag, prevTag, repoPath, headSHA,
isPreRelease, needsNewTag, isDirty) isPreRelease, needsNewTag, isDirty)
} }
@ -463,10 +471,17 @@ func goreleaserYAML(projectName string, bins []binary) string {
// ── Release script generation ────────────────────────────────────────────── // ── Release script generation ──────────────────────────────────────────────
// printModuleScript emits one module's release steps to stdout. // printModuleScript emits one module's release steps to stdout.
// If wrap=true the output is enclosed in a bash subshell ( ... ) with a cd //
// at the top, so multiple modules in one script don't interfere. // All paths in the generated script are relative to relPath so that the
// script can be run from the directory where monorel was invoked:
// - git commands use relPath/ as the pathspec (instead of ./)
// - goreleaser is wrapped in ( cd "relPath" && goreleaser ... ) when needed
// - artifact globs use relPath/dist/ instead of ./dist/
//
// When relPath is "." (monorel was run from the module root), ./ paths are
// used and no cd is required for any command.
func printModuleScript( func printModuleScript(
relPath string, wrap bool, relPath string,
projectName string, bins []binary, projectName string, bins []binary,
version, currentTag, prevTag, repoPath, headSHA string, version, currentTag, prevTag, repoPath, headSHA string,
isPreRelease, needsNewTag, isDirty bool, isPreRelease, needsNewTag, isDirty bool,
@ -479,16 +494,30 @@ func printModuleScript(
fmt.Println(strings.Repeat("─", max(0, 52-len(title)))) fmt.Println(strings.Repeat("─", max(0, 52-len(title))))
} }
if wrap { // Paths used in the generated script, all relative to the invoking CWD.
blank() var gitPathSpec, distDir string
rule := strings.Repeat("═", 54) if relPath == "." {
fmt.Printf("# %s\n", rule) gitPathSpec = "./"
fmt.Printf("# Module: %s\n", relPath) distDir = "./dist"
fmt.Printf("# %s\n", rule) } else {
fmt.Println("(") gitPathSpec = relPath + "/"
fmt.Printf("cd %q\n", relPath) distDir = relPath + "/dist"
} }
// Safe bash variable name for the release-notes capture (no export needed).
notesVar := strings.ReplaceAll(projectName, "-", "_") + "_release_notes"
// Module header.
blank()
rule := strings.Repeat("═", 54)
fmt.Printf("# %s\n", rule)
modLabel := relPath
if modLabel == "." {
modLabel = projectName + " (current directory)"
}
fmt.Printf("# Module: %s\n", modLabel)
fmt.Printf("# %s\n", rule)
if isDirty { if isDirty {
blank() blank()
line("# ⚠ WARNING: working tree has uncommitted changes.") line("# ⚠ WARNING: working tree has uncommitted changes.")
@ -527,15 +556,19 @@ func printModuleScript(
section("Step 3: Build with goreleaser") section("Step 3: Build with goreleaser")
line("# release.disable=true in .goreleaser.yaml; goreleaser only builds.") line("# release.disable=true in .goreleaser.yaml; goreleaser only builds.")
line("goreleaser release --clean --skip=validate,announce") if relPath == "." {
line("goreleaser release --clean --skip=validate,announce")
} else {
line("( cd %q && goreleaser release --clean --skip=validate,announce )", relPath)
}
section("Step 4: Generate release notes") section("Step 4: Generate release notes")
if prevTag != "" { if prevTag != "" {
line("RELEASE_NOTES=$(git --no-pager log %q..HEAD \\", prevTag) line("%s=$(git --no-pager log %q..HEAD \\", notesVar, prevTag)
line(" --pretty=format:'- %%h %%s' -- ./)") line(" --pretty=format:'- %%h %%s' -- %s)", gitPathSpec)
} else { } else {
line("RELEASE_NOTES=$(git --no-pager log \\") line("%s=$(git --no-pager log \\", notesVar)
line(" --pretty=format:'- %%h %%s' -- ./)") line(" --pretty=format:'- %%h %%s' -- %s)", gitPathSpec)
} }
section("Step 5: Create draft GitHub release") section("Step 5: Create draft GitHub release")
@ -543,7 +576,7 @@ func printModuleScript(
title := projectName + " " + tagVersion title := projectName + " " + tagVersion
line("gh release create %q \\", currentTag) line("gh release create %q \\", currentTag)
line(" --title %q \\", title) line(" --title %q \\", title)
line(" --notes \"${RELEASE_NOTES}\" \\") line(" --notes \"${%s}\" \\", notesVar)
if isPreRelease { if isPreRelease {
line(" --prerelease \\") line(" --prerelease \\")
} }
@ -553,19 +586,15 @@ func printModuleScript(
section("Step 6: Upload artifacts") section("Step 6: Upload artifacts")
line("gh release upload %q \\", currentTag) line("gh release upload %q \\", currentTag)
for _, bin := range bins { for _, bin := range bins {
line(" ./dist/%s_*.tar.gz \\", bin.name) line(" %s/%s_*.tar.gz \\", distDir, bin.name)
line(" ./dist/%s_*.zip \\", bin.name) line(" %s/%s_*.zip \\", distDir, bin.name)
} }
line(" \"./dist/%s_%s_checksums.txt\" \\", projectName, version) line(" \"%s/%s_%s_checksums.txt\" \\", distDir, projectName, version)
line(" --clobber") line(" --clobber")
section("Step 7: Publish release (remove draft)") section("Step 7: Publish release (remove draft)")
line("gh release edit %q --draft=false", currentTag) line("gh release edit %q --draft=false", currentTag)
if wrap {
blank()
fmt.Println(")")
}
blank() blank()
} }