From 8f268f7fd4342364d00e56e84b5ebe54bfb9b606 Mon Sep 17 00:00:00 2001 From: Duncan McGreggor Date: Fri, 8 May 2026 10:13:50 -0500 Subject: [PATCH 1/2] Fixed formatting. --- Makefile | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/Makefile b/Makefile index acb5a6b..e80005d 100644 --- a/Makefile +++ b/Makefile @@ -422,7 +422,7 @@ push: release-dry-run: @echo "" @echo "$(CYAN)╔══════════════════════════════════════════════════════════╗$(RESET)" - @echo "$(CYAN)║$(RESET) $(BLUE)Dry Run: Releasing $(PROJECT_NAME)$(RESET) $(CYAN)║$(RESET)" + @echo "$(CYAN)║$(RESET) $(BLUE)Dry Run: Releasing $(PROJECT_NAME)$(RESET) $(CYAN)║$(RESET)" @echo "$(CYAN)╚══════════════════════════════════════════════════════════╝$(RESET)" @echo "" @echo "$(BLUE)Verifying module is ready for release...$(RESET)" @@ -447,6 +447,12 @@ release-dry-run: .PHONY: release release: + @echo "" + @echo "$(CYAN)╔══════════════════════════════════════════════════════════╗$(RESET)" + @echo "$(CYAN)║$(RESET) $(BLUE)Releasing $(PROJECT_NAME) $(VERSION)$(RESET) $(CYAN)║$(RESET)" + @echo "$(CYAN)╚══════════════════════════════════════════════════════════╝$(RESET)" + @echo "" + @if [ -z "$(VERSION)" ]; then \ echo "$(RED)Error: VERSION variable not set$(RESET)"; \ echo "Usage: make release VERSION=v0.1.0"; \ @@ -460,11 +466,6 @@ release: echo "$(RED)Error: tag $(VERSION) already exists$(RESET)"; \ exit 1; \ fi - @echo "" - @echo "$(CYAN)╔══════════════════════════════════════════════════════════╗$(RESET)" - @echo "$(CYAN)║$(RESET) $(BLUE)Releasing $(PROJECT_NAME) $(VERSION)$(RESET) $(CYAN)║$(RESET)" - @echo "$(CYAN)╚══════════════════════════════════════════════════════════╝$(RESET)" - @echo "" @echo "$(YELLOW)⚠ This will tag $(VERSION) and push the tag to:$(RESET)" @for remote in $(GIT_REMOTES); do echo " - $$remote"; done @echo "$(YELLOW)⚠ Module proxies (proxy.golang.org) will index the tag automatically$(RESET)" From ce08ed125e47a4e8f585182aba18e756d69cda63 Mon Sep 17 00:00:00 2001 From: Duncan McGreggor Date: Fri, 8 May 2026 11:06:06 -0500 Subject: [PATCH 2/2] =?UTF-8?q?make:=20release=20workflow=20=E2=80=94=20no?= =?UTF-8?q?tes,=20GH=20release,=20dry-run=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Why: - release-dry-run was broken on stdlib-only modules (cascade has no go.sum) and from stat-cache staleness after `go mod tidy` bumps mtime without changing content. `git status --porcelain` tolerates both: handles missing paths cleanly and is status-aware. - VERSION as a CLI argument was typo-prone and could mismatch the embedded VERSION file. Reading from the file with `override` locks the source of truth and prevents accidental override. - After tagging, the maintainer had to manually create each GH release. Auto-create via `gh release create --notes-file …` when a release notes file is present. - Releasing from a dirty tree embeds `-dirty` in the build metadata, invalidating the tag's reproducibility. Refuse on dirty tree. What: - Bug fixes: - release-dry-run: use `git status --porcelain` instead of `git diff --quiet go.mod go.sum` (closes the stdlib-only failure and the stat-cache flap) - VERSION: `override` + `:=` from internal/project/VERSION - New targets: - `$(WORKBENCH)` — `mkdir -p ./workbench` - `release-notes` — generates a placeholder at $(WORKBENCH)/release-notes-$(VERSION).md (no overwrite if exists) - Updated `release-dry-run`: reports presence/absence of the release notes file with the corresponding "will be used" / "won't auto- create GH release" guidance. - Updated `release`: - target-specific VERS = v$(VERSION); used everywhere a tag string is needed - VERSION-unknown guard - working-tree-clean refusal (new, via `git status --porcelain`) - tag-exists guard - after tag push: `gh release create` if RELEASE_NOTES exists, else a clear "skipping GH release" notice - Help: lists `make release-notes` and the updated `make release` description. Note: workbench/ is gitignored, so the generated release notes file lives only on the maintainer's machine + on the GH release page. The dirty-tree check correctly ignores the gitignored draft. Co-Authored-By: Claude Opus 4.7 (1M context) --- Makefile | 103 +++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 81 insertions(+), 22 deletions(-) diff --git a/Makefile b/Makefile index e80005d..d11e6a8 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,7 @@ CODE_NAME := "cascade" MODULE_PATH := github.com/geomyidia/cascade BIN_DIR := ./bin MODE := debug -VERSION := $(shell cat internal/project/VERSION 2>/dev/null || echo "unknown") +override VERSION := $(shell cat internal/project/VERSION 2>/dev/null || echo "unknown") GIT_COMMIT := $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown") GIT_BRANCH := $(shell git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown") GIT_SUMMARY := $(shell git describe --tags --dirty --always 2>/dev/null || echo "untagged") @@ -63,6 +63,12 @@ REMOTE_macpro := ssh://macpro.local:23231/geomyidia/$(CODE_NAME).git REMOTE_github := git@github.com:geomyidia/$(CODE_NAME).git REMOTE_codeberg := ssh://git@codeberg.org/geomyidia/$(CODE_NAME).git +# Release configuration +WORKBENCH = ./workbench +RELEASE_NOTES = $(WORKBENCH)/release-notes-$(VERSION).md +LAST_VERSION := $(shell git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0") +GH_PROJECT_URL := https://$(MODULE_PATH) + # Default target .DEFAULT_GOAL := help @@ -104,7 +110,8 @@ help: @echo "" @echo "$(GREEN)Releasing:$(RESET)" @echo " $(YELLOW)make release-dry-run$(RESET) - Verify module is ready to tag (tidy + lint + test + build)" - @echo " $(YELLOW)make release VERSION=v0.1.0$(RESET) - Tag a release and push tag to all remotes" + @echo " $(YELLOW)make release-notes$(RESET) - Generate placeholder release notes for the current VERSION" + @echo " $(YELLOW)make release$(RESET) - Tag, push to all remotes, and (if release-notes exist) create a GitHub release" @echo "" @echo "$(GREEN)Utilities:$(RESET)" @echo " $(YELLOW)make push$(RESET) - Push to all configured remotes" @@ -428,12 +435,27 @@ release-dry-run: @echo "$(BLUE)Verifying module is ready for release...$(RESET)" @echo "$(CYAN)• Checking go.mod is tidy...$(RESET)" @go mod tidy - @if ! git diff --quiet go.mod go.sum 2>/dev/null; then \ + @# Use `git status --porcelain` instead of `git diff --quiet`: porcelain + @# tolerates missing paths (cascade is stdlib-only, so go.sum may not + @# exist) and isn't fooled by stat-cache staleness when `go mod tidy` + @# bumps mtime without changing content. + @status=$$(git status --porcelain -- go.mod go.sum 2>/dev/null); \ + if [ -n "$$status" ]; then \ echo "$(RED)✗ go.mod/go.sum changed after 'go mod tidy' — commit before releasing$(RESET)"; \ - git --no-pager diff go.mod go.sum; \ + echo "$$status"; \ + git --no-pager diff -- go.mod go.sum 2>/dev/null || true; \ exit 1; \ fi @echo "$(GREEN)✓ Module is tidy$(RESET)" + @echo "$(CYAN)• Checking for release notes...$(RESET)" + @if [ -f $(RELEASE_NOTES) ]; then \ + echo "$(GREEN)✓ Release notes found at $(RELEASE_NOTES)$(RESET)"; \ + echo "$(CYAN) These will be used to auto-create a GitHub release during 'make release'.$(RESET)"; \ + else \ + echo "$(YELLOW)⚠ No release notes at $(RELEASE_NOTES)$(RESET)"; \ + echo "$(YELLOW) Without this file, 'make release' will only push the tag — no GitHub release will be created.$(RESET)"; \ + echo "$(YELLOW) Run 'make release-notes' to generate a placeholder if you want one.$(RESET)"; \ + fi @echo "$(CYAN)• Running lint...$(RESET)" @$(MAKE) --no-print-directory lint @echo "$(CYAN)• Running tests...$(RESET)" @@ -442,31 +464,55 @@ release-dry-run: @$(MAKE) --no-print-directory build-release @echo "" @echo "$(GREEN)✓ Module ready for release!$(RESET)" - @echo "$(CYAN)→ Run 'make release VERSION=vX.Y.Z' to tag and push$(RESET)" + @echo "$(CYAN)→ Run 'make release' to tag and push$(RESET)" @echo "" +# Workbench directory for release-prep artifacts (release notes, scratch docs). +$(WORKBENCH): + @mkdir -p $(WORKBENCH) + +.PHONY: release-notes +release-notes: $(WORKBENCH) + @if [ -f $(RELEASE_NOTES) ]; then \ + echo "$(YELLOW)⚠ Release notes already exist at $(RELEASE_NOTES)$(RESET)"; \ + echo "$(CYAN)→ This target will not overwrite an existing file. Edit it directly if needed.$(RESET)"; \ + else \ + printf '# v%s -- Release Notes\n\n## Overview\n\nTBD\n\n## Details\n\nTBD\n\n## Change Log\n\n [%s..%s](%s/compare/%s...v%s)\n\n' \ + "$(VERSION)" "$(LAST_VERSION)" "$(VERSION)" "$(GH_PROJECT_URL)" "$(LAST_VERSION)" "$(VERSION)" \ + > $(RELEASE_NOTES); \ + echo "$(GREEN)✓ Generated placeholder release notes at $(RELEASE_NOTES)$(RESET)"; \ + echo "$(CYAN)→ Edit the Overview / Details sections, commit the file, then run 'make release'.$(RESET)"; \ + fi + .PHONY: release +release: VERS = v$(VERSION) release: @echo "" @echo "$(CYAN)╔══════════════════════════════════════════════════════════╗$(RESET)" - @echo "$(CYAN)║$(RESET) $(BLUE)Releasing $(PROJECT_NAME) $(VERSION)$(RESET) $(CYAN)║$(RESET)" + @echo "$(CYAN)║$(RESET) $(BLUE)Releasing $(PROJECT_NAME) $(VERS)$(RESET) $(CYAN)║$(RESET)" @echo "$(CYAN)╚══════════════════════════════════════════════════════════╝$(RESET)" @echo "" - @if [ -z "$(VERSION)" ]; then \ - echo "$(RED)Error: VERSION variable not set$(RESET)"; \ - echo "Usage: make release VERSION=v0.1.0"; \ + @if [ "$(VERSION)" = "unknown" ]; then \ + echo "$(RED)Error: VERSION variable not properly set: VERSION=$(VERSION)$(RESET)"; \ exit 1; \ fi - @case "$(VERSION)" in \ - v[0-9]*.[0-9]*.[0-9]*) ;; \ - *) echo "$(RED)Error: VERSION must be semver-prefixed with 'v' (e.g. v0.1.0)$(RESET)"; exit 1 ;; \ - esac - @if git rev-parse "$(VERSION)" >/dev/null 2>&1; then \ - echo "$(RED)Error: tag $(VERSION) already exists$(RESET)"; \ + + @# Refuse to release from a dirty tree: any modified, staged, or + @# untracked-but-not-gitignored file disqualifies. Otherwise the build + @# metadata embedded in the tagged binary would carry a -dirty suffix + @# and the tag would not reproduce the working tree at tag time. + @if [ -n "$$(git status --porcelain 2>/dev/null)" ]; then \ + echo "$(RED)Error: working tree has uncommitted changes — commit or stash before releasing$(RESET)"; \ + git --no-pager status --short; \ exit 1; \ fi - @echo "$(YELLOW)⚠ This will tag $(VERSION) and push the tag to:$(RESET)" + + @if git rev-parse "$(VERS)" >/dev/null 2>&1; then \ + echo "$(RED)Error: tag $(VERS) already exists$(RESET)"; \ + exit 1; \ + fi + @echo "$(YELLOW)⚠ This will tag $(VERS) and push the tag to:$(RESET)" @for remote in $(GIT_REMOTES); do echo " - $$remote"; done @echo "$(YELLOW)⚠ Module proxies (proxy.golang.org) will index the tag automatically$(RESET)" @echo "" @@ -477,16 +523,29 @@ release: exit 1; \ fi @$(MAKE) --no-print-directory release-dry-run - @echo "$(BLUE)Creating annotated tag $(VERSION)...$(RESET)" - @git tag -a "$(VERSION)" -m "Release $(VERSION)" - @echo "$(GREEN)✓ Tagged $(VERSION)$(RESET)" + @echo "$(BLUE)Creating annotated tag $(VERS)...$(RESET)" + @git tag -a "$(VERS)" -m "Release $(VERS)" + @echo "$(GREEN)✓ Tagged $(VERS)$(RESET)" @for remote in $(GIT_REMOTES); do \ echo "$(CYAN)• Pushing tag to $$remote...$(RESET)"; \ - git push $$remote "$(VERSION)" && \ + git push $$remote "$(VERS)" && \ echo " $(GREEN)✓$(RESET) pushed" || \ { echo " $(RED)✗$(RESET) push to $$remote failed"; exit 1; }; \ done + @if [ -f $(RELEASE_NOTES) ]; then \ + echo "$(BLUE)Creating GitHub release with notes from $(RELEASE_NOTES)...$(RESET)"; \ + if gh release create "$(VERS)" --notes-file "$(RELEASE_NOTES)" --title "Release $(VERS)"; then \ + echo "$(GREEN)✓ GitHub release created$(RESET)"; \ + else \ + echo "$(RED)✗ GitHub release creation failed$(RESET)"; \ + echo "$(YELLOW) Tag $(VERS) was still pushed; create the GH release manually if needed.$(RESET)"; \ + exit 1; \ + fi; \ + else \ + echo "$(YELLOW)⚠ No release notes file found at $(RELEASE_NOTES); skipping GitHub release creation$(RESET)"; \ + echo "$(CYAN)→ Tag $(VERS) is pushed. Create a GH release manually if desired.$(RESET)"; \ + fi @echo "" - @echo "$(GREEN)✓ Released $(VERSION)$(RESET)" - @echo "$(CYAN)→ Consumers can install with: go install $(MODULE_PATH)/cmd/cascade@$(VERSION)$(RESET)" + @echo "$(GREEN)✓ Released $(VERS)$(RESET)" + @echo "$(CYAN)→ Consumers can install with: go install $(MODULE_PATH)/cmd/cascade@$(VERS)$(RESET)" @echo ""