fix(monorel): guard .goreleaser.yaml overwrite in release; loosen compat check

release subcommand:
- Replace yamlLooksCorrect with yamlIsCompatible: file is considered OK if
  {{ .ProjectName }} is absent AND at least one binary's VERSION string is
  present.  Extra hand-edited binaries (like fixtures) no longer trigger a
  rewrite.
- Before overwriting an existing file, prompt the user [Y/n].  --yes does
  not skip this prompt; --force does.  If stdin is not a terminal and
  --force is not set, the command errors rather than silently clobbering.

init subcommand: unchanged — still uses the strict yamlLooksCorrect check
(all binaries must be present, ldflags must include main.version).
This commit is contained in:
AJ ONeal 2026-03-01 01:32:37 -07:00
parent c14bc239f5
commit 1289f3e5b6
No known key found for this signature in database

View File

@ -112,11 +112,12 @@ func usage() {
func runRelease(args []string) { func runRelease(args []string) {
fs := flag.NewFlagSet("monorel release", flag.ExitOnError) fs := flag.NewFlagSet("monorel release", flag.ExitOnError)
var recursive, all, dryRun, yes, draft, prerelease bool var recursive, all, dryRun, yes, force, draft, prerelease bool
fs.BoolVar(&recursive, "recursive", false, "find all main packages recursively under each path") fs.BoolVar(&recursive, "recursive", false, "find all main packages recursively under each path")
fs.BoolVar(&all, "A", false, "include dot/underscore-prefixed directories; warn rather than error on failures") fs.BoolVar(&all, "A", false, "include dot/underscore-prefixed directories; warn rather than error on failures")
fs.BoolVar(&dryRun, "dry-run", false, "show each step without running it") fs.BoolVar(&dryRun, "dry-run", false, "show each step without running it")
fs.BoolVar(&yes, "yes", false, "run all steps without prompting") fs.BoolVar(&yes, "yes", false, "run all steps without prompting")
fs.BoolVar(&force, "force", false, "overwrite .goreleaser.yaml without prompting even if it has been modified")
fs.BoolVar(&draft, "draft", false, "keep the GitHub release in draft state after uploading (default: publish)") fs.BoolVar(&draft, "draft", false, "keep the GitHub release in draft state after uploading (default: publish)")
fs.BoolVar(&prerelease, "prerelease", false, "keep the GitHub release marked as pre-release even for clean tags (default: promote clean tags to stable)") fs.BoolVar(&prerelease, "prerelease", false, "keep the GitHub release marked as pre-release even for clean tags (default: promote clean tags to stable)")
fs.Usage = func() { fs.Usage = func() {
@ -161,7 +162,7 @@ func runRelease(args []string) {
printGroupHeader(cwd, group) printGroupHeader(cwd, group)
relPath, _ := filepath.Rel(cwd, group.root) relPath, _ := filepath.Rel(cwd, group.root)
relPath = filepath.ToSlash(relPath) relPath = filepath.ToSlash(relPath)
processModule(group, relPath, dryRun, yes, draft, prerelease) processModule(group, relPath, dryRun, yes, force, draft, prerelease)
} }
} }
@ -952,7 +953,7 @@ func printGroupHeader(cwd string, group *moduleGroup) {
// 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; it is used in the script for all paths so that the script can // module root; it is used in the script for all paths so that the script can
// be run from the directory where monorel was invoked. // be run from the directory where monorel was invoked.
func processModule(group *moduleGroup, relPath string, dryRun, yes, draft, prerelease bool) { func processModule(group *moduleGroup, relPath string, dryRun, yes, force, draft, prerelease bool) {
modRoot := group.root modRoot := group.root
bins := group.bins bins := group.bins
@ -971,18 +972,18 @@ func processModule(group *moduleGroup, relPath string, dryRun, yes, draft, prere
rawURL := mustRunIn(modRoot, "git", "remote", "get-url", "origin") rawURL := mustRunIn(modRoot, "git", "remote", "get-url", "origin")
repoPath := normalizeGitURL(rawURL) repoPath := normalizeGitURL(rawURL)
// 1. Write .goreleaser.yaml (always regenerate). // 1. Write .goreleaser.yaml when necessary.
// Track whether this is a first-time creation: auto-commit and auto-tag // For release, the file is considered compatible if it has no stock
// only apply when the file is new. If it already exists, just update it // {{ .ProjectName }} template and at least one binary uses the VERSION env
// on disk and leave committing to the user. // var — local edits that add extra binaries etc. are preserved.
// Auto-commit and auto-tag only apply when the file is brand new.
yamlContent := goreleaserYAML(projectName, bins) yamlContent := goreleaserYAML(projectName, bins)
yamlPath := filepath.Join(modRoot, ".goreleaser.yaml") yamlPath := filepath.Join(modRoot, ".goreleaser.yaml")
existing, readErr := os.ReadFile(yamlPath) existing, readErr := os.ReadFile(yamlPath)
isNewFile := readErr != nil isNewFile := readErr != nil
isChanged := isNewFile || !yamlLooksCorrect(string(existing), bins) isChanged := isNewFile || !yamlIsCompatible(string(existing), bins)
if !isNewFile && isChanged { if !isNewFile && isChanged {
// Warn if a stock {{ .ProjectName }} template is in use (one of the // Warn if a stock {{ .ProjectName }} template is in use.
// reasons yamlLooksCorrect may have returned false).
hasProjectName := strings.Contains(string(existing), "{{ .ProjectName }}") || hasProjectName := strings.Contains(string(existing), "{{ .ProjectName }}") ||
strings.Contains(string(existing), "{{.ProjectName}}") strings.Contains(string(existing), "{{.ProjectName}}")
gitInfo, gitErr := os.Stat(filepath.Join(modRoot, ".git")) gitInfo, gitErr := os.Stat(filepath.Join(modRoot, ".git"))
@ -991,6 +992,23 @@ func processModule(group *moduleGroup, relPath string, dryRun, yes, draft, prere
fmt.Fprintf(os.Stderr, "warning: %s: contains {{ .ProjectName }} but module is a monorepo subdirectory;\n", yamlPath) 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") fmt.Fprintf(os.Stderr, " replacing stock goreleaser config with monorel-generated config.\n")
} }
// Prompt before overwriting a modified file. --yes does not apply;
// use --force to skip the prompt. If stdin is not a terminal and
// --force is not set, refuse rather than silently clobber.
if !force {
fi, statErr := os.Stdin.Stat()
isTTY := statErr == nil && fi.Mode()&os.ModeCharDevice != 0
if !isTTY {
fatalf("%s needs updating but stdin is not a terminal; use --force to overwrite", cwdRelPath(yamlPath))
}
fmt.Fprintf(os.Stderr, "%s needs updating; overwrite? [Y/n] ", cwdRelPath(yamlPath))
reader := bufio.NewReader(os.Stdin)
line, _ := reader.ReadString('\n')
if resp := strings.ToLower(strings.TrimSpace(line)); resp == "n" || resp == "no" {
fmt.Fprintf(os.Stderr, "skipped %s\n", cwdRelPath(yamlPath))
return
}
}
} }
if isChanged { if isChanged {
if err := os.WriteFile(yamlPath, []byte(yamlContent), 0o644); err != nil { if err := os.WriteFile(yamlPath, []byte(yamlContent), 0o644); err != nil {
@ -1273,6 +1291,24 @@ func goreleaserYAML(projectName string, bins []binary) string {
return b.String() return b.String()
} }
// yamlIsCompatible is the looser check used by the release subcommand.
// It returns true when the file has no stock {{ .ProjectName }} template and
// at least one binary already uses the VERSION env var in its archive name.
// This preserves hand-edited files that add extra binaries or tweak settings,
// where yamlLooksCorrect would demand every declared binary be present.
func yamlIsCompatible(content string, bins []binary) bool {
if strings.Contains(content, "{{ .ProjectName }}") ||
strings.Contains(content, "{{.ProjectName}}") {
return false
}
for _, bin := range bins {
if strings.Contains(content, bin.name+"_{{ .Env.VERSION }}_") {
return true
}
}
return false
}
// yamlLooksCorrect returns true when content appears to be a valid monorel- // yamlLooksCorrect returns true when content appears to be a valid monorel-
// generated (or compatible) .goreleaser.yaml for the given binaries: // generated (or compatible) .goreleaser.yaml for the given binaries:
// //