Skip to content

Only push release tags#1968

Open
jordanstephens wants to merge 6 commits intomainfrom
jordan/only-push-release-tag
Open

Only push release tags#1968
jordanstephens wants to merge 6 commits intomainfrom
jordan/only-push-release-tag

Conversation

@jordanstephens
Copy link
Member

@jordanstephens jordanstephens commented Mar 6, 2026

Description

Update the release tasks to print the tag when making it, and only push the release tags

Linked Issues

Checklist

  • I have performed a self-review of my code
  • I have added appropriate tests
  • I have updated the Defang CLI docs and/or README to reflect my changes, if necessary

Summary by CodeRabbit

  • Chores
    • Improved release automation: version increments now reliably compute and expose the new tag, record it temporarily, and push only that specific tag to the remote.
    • Reduced risk of accidental multi-tag pushes and added cleanup of the temporary tag record for cleaner release runs.

The minor version bump was using $$((NF-1)) on the right-hand side of
the awk assignment, which was being evaluated as a shell arithmetic
expression rather than an awk field reference. Replace with $$(NF-1)
to consistently use awk field reference syntax on both sides.
@jordanstephens jordanstephens requested a review from lionello as a code owner March 6, 2026 21:52
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 6, 2026

📝 Walkthrough

Walkthrough

Makefile version targets replaced multi-line git-tag pipelines with explicit shell variable extraction (LATEST_TAG, NEW_TAG), write NEW_TAG to .new_tag, and tag locally. Release targets now push only the specific tag read from .new_tag via git push origin refs/tags/... and remove .new_tag.

Changes

Cohort / File(s) Summary
Version Increment Targets
src/Makefile
increment-patch-version, increment-minor-version, increment-major-version changed from chained git/tag pipelines to explicit shell variable assignments for LATEST_TAG and NEW_TAG, echo of NEW_TAG, git tag NEW_TAG, and writing NEW_TAG to .new_tag. Major now formats as vX.0.0 from the first field.
Release Targets
src/Makefile
patch-release, minor-release, major-release changed push behavior from --follow-tags --tags to pushing only the specific tag via git push origin refs/tags/<tag>:refs/tags/<tag> (reads tag from .new_tag) and removes .new_tag after push.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐇
I nudged the Makefile, soft and spry,
Pipelines trimmed, new tags hop by,
A tiny file holds the name,
One precise push — no wider flame,
Version carrots gleam — oh my! 🍂

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Only push release tags' directly reflects the main change: modifying release push behavior to push only specific release tags instead of all annotated tags.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch jordan/only-push-release-tag

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/Makefile`:
- Around line 150-158: The release recipes (minor-release and major-release)
currently push all local tags via git push 'refs/tags/v*:refs/tags/v*'; change
them to push only the specific tag produced by the increment targets by passing
the computed NEW_TAG back to the release target—e.g., have
increment-minor-version / increment-major-version write NEW_TAG to a temporary
file (or echo it so the parent recipe runs in the same shell) and then use that
value in minor-release / major-release to run git push origin refs/tags/$(shell
cat NEW_TAG_FILE) or git push origin refs/tags/$(NEW_TAG) instead of the v*
glob; update the Makefile targets increment-minor-version,
increment-major-version, minor-release, and major-release to implement this flow
and ensure the temp file is cleaned up after push.
- Around line 129-146: The targets that compute NEW_TAG
(increment-patch-version, increment-minor-version, increment-major-version)
assume LATEST_TAG exists; add a guard immediately after computing LATEST_TAG
that checks if it's empty and, if so, prints an error to stderr and exits with
non‑zero status to fail fast. Specifically, in each target (where LATEST_TAG and
NEW_TAG are set) check LATEST_TAG (the variable created by git tag -l 'v*'
--sort=-v:refname | head -n1) and call something like echo "No existing v* tags
found" >&2; exit 1 before running the awk and git tag steps so you avoid
creating invalid tags when no prior release exists. Ensure the guard is added in
the anonymous first block (patch), increment-minor-version, and
increment-major-version blocks referencing LATEST_TAG and NEW_TAG.
- Around line 129-146: The recipe lines that compute and print NEW_TAG (the
multi-line shell commands assigning LATEST_TAG/NEW_TAG and running git tag) are
being echoed by make; prefix each recipe line with @ to suppress command echoing
so the targets produce clean output—apply this to the unnamed tag-increment
target (the block that sets LATEST_TAG/NEW_TAG and runs git tag), the
increment-minor-version target, and the increment-major-version target, keeping
the existing tab indentation and preserving the exact shell commands
(LATEST_TAG=..., NEW_TAG=..., echo $$NEW_TAG; git tag $$NEW_TAG).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7bc298ac-1250-4707-b8c8-f24a5220e6e7

📥 Commits

Reviewing files that changed from the base of the PR and between 70f85a4 and 241701f.

📒 Files selected for processing (1)
  • src/Makefile

src/Makefile Outdated
Comment on lines +129 to +146
LATEST_TAG=$$(git tag -l 'v*' --sort=-v:refname | head -n1); \
NEW_TAG=$$(echo $$LATEST_TAG | awk -F. '{$$NF = $$NF + 1;} 1' OFS=.); \
echo $$NEW_TAG; \
git tag $$NEW_TAG

.PHONY: increment-minor-version
increment-minor-version:
@git tag $$( \
git tag -l 'v*' --sort=-v:refname | \
head -n1 | \
awk -F. '{$$(NF-1) = $$((NF-1)) + 1; $$NF = 0;} 1' OFS=. \
)
LATEST_TAG=$$(git tag -l 'v*' --sort=-v:refname | head -n1); \
NEW_TAG=$$(echo $$LATEST_TAG | awk -F. '{$$(NF-1) = $$(NF-1) + 1; $$NF = 0;} 1' OFS=.); \
echo $$NEW_TAG; \
git tag $$NEW_TAG

.PHONY: increment-major-version
increment-major-version:
@git tag $$( \
git tag -l 'v*' --sort=-v:refname | \
head -n1 | \
awk -F. '{maj=substr($$1,2)+1; printf "v%d.0.0\n", maj}' \
)
LATEST_TAG=$$(git tag -l 'v*' --sort=-v:refname | head -n1); \
NEW_TAG=$$(echo $$LATEST_TAG | awk -F. '{maj=substr($$1,2)+1; printf "v%d.0.0\n", maj}'); \
echo $$NEW_TAG; \
git tag $$NEW_TAG
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

head -200 src/Makefile | tail -80

Repository: DefangLabs/defang

Length of output: 2452


🏁 Script executed:

wc -l src/Makefile && sed -n '129,146p' src/Makefile

Repository: DefangLabs/defang

Length of output: 756


🏁 Script executed:

#!/bin/bash
# Simulate the behavior when no tags match
LATEST_TAG=$$(git tag -l 'v*' --sort=-v:refname | head -n1)
echo "LATEST_TAG value: '${LATEST_TAG}'"
echo "LATEST_TAG is empty: $([ -z "$LATEST_TAG" ] && echo 'yes' || echo 'no')"

# Test what happens when we try to use the empty tag in the awk command
echo "Testing awk with empty LATEST_TAG:"
NEW_TAG=$$(echo ${LATEST_TAG} | awk -F. '{$$NF = $$NF + 1;} 1' OFS=.)
echo "NEW_TAG result: '${NEW_TAG}'"

Repository: DefangLabs/defang

Length of output: 207


🏁 Script executed:

#!/bin/bash
# Test if the proposed guard syntax works in shell (which Makefile uses)
cat > /tmp/test_guard.sh << 'EOF'
#!/bin/bash
LATEST_TAG=""
test -n "$LATEST_TAG" || { echo "no existing v* tag found" >&2; exit 1; }
echo "Tag exists: $LATEST_TAG"
EOF

chmod +x /tmp/test_guard.sh
echo "Testing guard with empty tag:"
/tmp/test_guard.sh || echo "Guard correctly exited with code $?"

# Test with non-empty tag
cat > /tmp/test_guard2.sh << 'EOF'
#!/bin/bash
LATEST_TAG="v1.0.0"
test -n "$LATEST_TAG" || { echo "no existing v* tag found" >&2; exit 1; }
echo "Tag exists: $LATEST_TAG"
EOF

chmod +x /tmp/test_guard2.sh
echo -e "\nTesting guard with valid tag:"
/tmp/test_guard2.sh

Repository: DefangLabs/defang

Length of output: 217


🏁 Script executed:

git tag -l 'v*' --sort=-v:refname | head -5

Repository: DefangLabs/defang

Length of output: 43


🏁 Script executed:

#!/bin/bash
# Simulate empty LATEST_TAG and see what error the awk command produces
LATEST_TAG=""
echo "Testing patch-version logic with empty LATEST_TAG:"
NEW_TAG=$(echo $LATEST_TAG | awk -F. '{$NF = $NF + 1;} 1' OFS=.) 2>&1
echo "Result: '$NEW_TAG'"

# Test with minor version
echo -e "\nTesting minor-version logic with empty LATEST_TAG:"
NEW_TAG=$(echo $LATEST_TAG | awk -F. '{$(NF-1) = $(NF-1) + 1; $NF = 0;} 1' OFS=.) 2>&1
echo "Result: '$NEW_TAG'"

# Test with major version
echo -e "\nTesting major-version logic with empty LATEST_TAG:"
NEW_TAG=$(echo $LATEST_TAG | awk -F. '{maj=substr($1,2)+1; printf "v%d.0.0\n", maj}') 2>&1
echo "Result: '$NEW_TAG'"

Repository: DefangLabs/defang

Length of output: 343


🏁 Script executed:

#!/bin/bash
# Verify the guard syntax works in a Makefile context
# by testing the actual escape sequence as it appears in Makefile
cat > /tmp/test_makefile.mk << 'EOF'
.PHONY: test-guard
test-guard:
	LATEST_TAG=$$(git tag -l 'v*' --sort=-v:refname | head -n1); \
	test -n "$$LATEST_TAG" || { echo "no existing v* tag found" >&2; exit 1; }; \
	echo "Tag found: $$LATEST_TAG"

.PHONY: test-guard-fail
test-guard-fail:
	LATEST_TAG=""; \
	test -n "$$LATEST_TAG" || { echo "no existing v* tag found" >&2; exit 1; }; \
	echo "Tag found: $$LATEST_TAG"
EOF

echo "Testing Makefile guard syntax (will find no tags, so guard should trigger):"
make -f /tmp/test_makefile.mk test-guard 2>&1 || echo "Expected failure"

echo -e "\nTesting Makefile guard with explicitly empty tag:"
make -f /tmp/test_makefile.mk test-guard-fail 2>&1 || echo "Expected failure"

Repository: DefangLabs/defang

Length of output: 309


🏁 Script executed:

#!/bin/bash
# Simulate the exact shell commands as they would be expanded in Makefile
# Test what happens with the current code (without guard)

echo "=== Current behavior (without guard) ==="

echo -e "\n1. Patch version with no tags:"
LATEST_TAG=$(git tag -l 'v*' --sort=-v:refname | head -n1)
echo "LATEST_TAG: '$LATEST_TAG'"
NEW_TAG=$(echo $LATEST_TAG | awk -F. '{$NF = $NF + 1;} 1' OFS=.)
echo "NEW_TAG produced: '$NEW_TAG'"
echo "Would create tag: 'v$NEW_TAG' (INCORRECT - should fail)"

echo -e "\n2. Minor version with no tags:"
LATEST_TAG=$(git tag -l 'v*' --sort=-v:refname | head -n1)
echo "LATEST_TAG: '$LATEST_TAG'"
NEW_TAG=$(echo $LATEST_TAG | awk -F. '{$(NF-1) = $(NF-1) + 1; $NF = 0;} 1' OFS=.) 2>&1
echo "NEW_TAG produced: '$NEW_TAG'"
echo "Error occurred: YES"

echo -e "\n3. Major version with no tags:"
LATEST_TAG=$(git tag -l 'v*' --sort=-v:refname | head -n1)
echo "LATEST_TAG: '$LATEST_TAG'"
NEW_TAG=$(echo $LATEST_TAG | awk -F. '{maj=substr($1,2)+1; printf "v%d.0.0\n", maj}')
echo "NEW_TAG produced: '$NEW_TAG'"
echo "Would create tag: '$NEW_TAG' (INCORRECT - should fail)"

echo -e "\n=== With proposed guard ==="
echo -e "\n4. With guard (empty LATEST_TAG):"
LATEST_TAG=""
test -n "$LATEST_TAG" && echo "Would proceed" || { echo "Guard triggered: no existing v* tag found"; exit 1; }

Repository: DefangLabs/defang

Length of output: 628


Add guard to fail fast when no matching release tag exists.

When git tag -l 'v*' returns nothing, LATEST_TAG is empty, causing real problems:

  • increment-patch-version silently creates tag v1 (incorrect)
  • increment-minor-version crashes with awk: attempt to access field -1
  • increment-major-version silently creates tag v1.0.0 (incorrect)

Add the guard to all three targets:

Proposed fix
increment-patch-version:
	LATEST_TAG=$$(git tag -l 'v*' --sort=-v:refname | head -n1); \
+	test -n "$$LATEST_TAG" || { echo "no existing v* tag found" >&2; exit 1; }; \
	NEW_TAG=$$(echo $$LATEST_TAG | awk -F. '{$$NF = $$NF + 1;} 1' OFS=.); \
	echo $$NEW_TAG; \
	git tag $$NEW_TAG

increment-minor-version:
	LATEST_TAG=$$(git tag -l 'v*' --sort=-v:refname | head -n1); \
+	test -n "$$LATEST_TAG" || { echo "no existing v* tag found" >&2; exit 1; }; \
	NEW_TAG=$$(echo $$LATEST_TAG | awk -F. '{$$(NF-1) = $$(NF-1) + 1; $$NF = 0;} 1' OFS=.); \
	echo $$NEW_TAG; \
	git tag $$NEW_TAG

increment-major-version:
	LATEST_TAG=$$(git tag -l 'v*' --sort=-v:refname | head -n1); \
+	test -n "$$LATEST_TAG" || { echo "no existing v* tag found" >&2; exit 1; }; \
	NEW_TAG=$$(echo $$LATEST_TAG | awk -F. '{maj=substr($$1,2)+1; printf "v%d.0.0\n", maj}'); \
	echo $$NEW_TAG; \
	git tag $$NEW_TAG
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
LATEST_TAG=$$(git tag -l 'v*' --sort=-v:refname | head -n1); \
NEW_TAG=$$(echo $$LATEST_TAG | awk -F. '{$$NF = $$NF + 1;} 1' OFS=.); \
echo $$NEW_TAG; \
git tag $$NEW_TAG
.PHONY: increment-minor-version
increment-minor-version:
@git tag $$( \
git tag -l 'v*' --sort=-v:refname | \
head -n1 | \
awk -F. '{$$(NF-1) = $$((NF-1)) + 1; $$NF = 0;} 1' OFS=. \
)
LATEST_TAG=$$(git tag -l 'v*' --sort=-v:refname | head -n1); \
NEW_TAG=$$(echo $$LATEST_TAG | awk -F. '{$$(NF-1) = $$(NF-1) + 1; $$NF = 0;} 1' OFS=.); \
echo $$NEW_TAG; \
git tag $$NEW_TAG
.PHONY: increment-major-version
increment-major-version:
@git tag $$( \
git tag -l 'v*' --sort=-v:refname | \
head -n1 | \
awk -F. '{maj=substr($$1,2)+1; printf "v%d.0.0\n", maj}' \
)
LATEST_TAG=$$(git tag -l 'v*' --sort=-v:refname | head -n1); \
NEW_TAG=$$(echo $$LATEST_TAG | awk -F. '{maj=substr($$1,2)+1; printf "v%d.0.0\n", maj}'); \
echo $$NEW_TAG; \
git tag $$NEW_TAG
LATEST_TAG=$$(git tag -l 'v*' --sort=-v:refname | head -n1); \
test -n "$$LATEST_TAG" || { echo "no existing v* tag found" >&2; exit 1; }; \
NEW_TAG=$$(echo $$LATEST_TAG | awk -F. '{$$NF = $$NF + 1;} 1' OFS=.); \
echo $$NEW_TAG; \
git tag $$NEW_TAG
.PHONY: increment-minor-version
increment-minor-version:
LATEST_TAG=$$(git tag -l 'v*' --sort=-v:refname | head -n1); \
test -n "$$LATEST_TAG" || { echo "no existing v* tag found" >&2; exit 1; }; \
NEW_TAG=$$(echo $$LATEST_TAG | awk -F. '{$$(NF-1) = $$(NF-1) + 1; $$NF = 0;} 1' OFS=.); \
echo $$NEW_TAG; \
git tag $$NEW_TAG
.PHONY: increment-major-version
increment-major-version:
LATEST_TAG=$$(git tag -l 'v*' --sort=-v:refname | head -n1); \
test -n "$$LATEST_TAG" || { echo "no existing v* tag found" >&2; exit 1; }; \
NEW_TAG=$$(echo $$LATEST_TAG | awk -F. '{maj=substr($$1,2)+1; printf "v%d.0.0\n", maj}'); \
echo $$NEW_TAG; \
git tag $$NEW_TAG
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Makefile` around lines 129 - 146, The targets that compute NEW_TAG
(increment-patch-version, increment-minor-version, increment-major-version)
assume LATEST_TAG exists; add a guard immediately after computing LATEST_TAG
that checks if it's empty and, if so, prints an error to stderr and exits with
non‑zero status to fail fast. Specifically, in each target (where LATEST_TAG and
NEW_TAG are set) check LATEST_TAG (the variable created by git tag -l 'v*'
--sort=-v:refname | head -n1) and call something like echo "No existing v* tags
found" >&2; exit 1 before running the awk and git tag steps so you avoid
creating invalid tags when no prior release exists. Ensure the guard is added in
the anonymous first block (patch), increment-minor-version, and
increment-major-version blocks referencing LATEST_TAG and NEW_TAG.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

head -n 160 src/Makefile | tail -n +120

Repository: DefangLabs/defang

Length of output: 1299


🏁 Script executed:

cat > /tmp/test_makefile << 'EOF'
.PHONY: test1
test1:
	echo "line 1"; \
	echo "line 2"

.PHONY: test2
test2:
	`@echo` "line 1"; \
	echo "line 2"

.PHONY: test3
test3:
	`@echo` "line 1"; \
	`@echo` "line 2"
EOF

echo "=== Test 1: No @ prefix ===" && make -f /tmp/test_makefile test1 2>&1 || true
echo ""
echo "=== Test 2: @ on first line only ===" && make -f /tmp/test_makefile test2 2>&1 || true
echo ""
echo "=== Test 3: @ on all lines ===" && make -f /tmp/test_makefile test3 2>&1 || true

Repository: DefangLabs/defang

Length of output: 290


🌐 Web query:

GNU Make @ prefix recipe echo suppression

💡 Result:

In GNU Make, recipe lines are echoed (printed) before they run by default. If you prefix a recipe command with @, make will still execute the command but will not print that command line. (gnu.org)

Example:

all:
	`@echo` "This message prints, but the command line is not echoed"
	echo "This command line *is* echoed, then its output prints"

Related options:

  • Silence everything for a run: make -s / make --silent (as if every recipe line began with @). (ftp.gnu.org)
  • .ONESHELL caveat: with .ONESHELL, GNU Make checks special prefixes like @ only on the first recipe line (so put @ on the first line if you want the whole recipe silent). (stackoverflow.com)

Citations:


🏁 Script executed:

grep -n "ONESHELL\|SHELL" src/Makefile | head -20

Repository: DefangLabs/defang

Length of output: 76


Add @ to suppress Make's recipe echo so these targets print clean output.

Without the @ prefix, GNU Make echoes each recipe line before executing it. This means the entire multi-line command will be printed to stdout before echo $$NEW_TAG runs, polluting the output for any consumer of this target's result.

🔧 Proposed fix
 increment-patch-version:
-	LATEST_TAG=$$(git tag -l 'v*' --sort=-v:refname | head -n1); \
+	`@LATEST_TAG`=$$(git tag -l 'v*' --sort=-v:refname | head -n1); \
 	NEW_TAG=$$(echo $$LATEST_TAG | awk -F. '{$$NF = $$NF + 1;} 1' OFS=.); \
 	echo $$NEW_TAG; \
 	git tag $$NEW_TAG

 increment-minor-version:
-	LATEST_TAG=$$(git tag -l 'v*' --sort=-v:refname | head -n1); \
+	`@LATEST_TAG`=$$(git tag -l 'v*' --sort=-v:refname | head -n1); \
 	NEW_TAG=$$(echo $$LATEST_TAG | awk -F. '{$$(NF-1) = $$(NF-1) + 1; $$NF = 0;} 1' OFS=.); \
 	echo $$NEW_TAG; \
 	git tag $$NEW_TAG

 increment-major-version:
-	LATEST_TAG=$$(git tag -l 'v*' --sort=-v:refname | head -n1); \
+	`@LATEST_TAG`=$$(git tag -l 'v*' --sort=-v:refname | head -n1); \
 	NEW_TAG=$$(echo $$LATEST_TAG | awk -F. '{maj=substr($$1,2)+1; printf "v%d.0.0\n", maj}'); \
 	echo $$NEW_TAG; \
 	git tag $$NEW_TAG
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Makefile` around lines 129 - 146, The recipe lines that compute and print
NEW_TAG (the multi-line shell commands assigning LATEST_TAG/NEW_TAG and running
git tag) are being echoed by make; prefix each recipe line with @ to suppress
command echoing so the targets produce clean output—apply this to the unnamed
tag-increment target (the block that sets LATEST_TAG/NEW_TAG and runs git tag),
the increment-minor-version target, and the increment-major-version target,
keeping the existing tab indentation and preserving the exact shell commands
(LATEST_TAG=..., NEW_TAG=..., echo $$NEW_TAG; git tag $$NEW_TAG).

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/Makefile`:
- Around line 129-133: The Makefile recipe that computes LATEST_TAG and NEW_TAG
and runs "git tag $$NEW_TAG" can mask failures because the semicolon-separated
commands return the last command's exit code; update the increment targets to
enable immediate exit on failure by adding a "set -e" (or equivalent "set -o
errexit") at the start of the recipe so any failure (especially in "git tag
$$NEW_TAG") aborts the recipe and prevents writing an incorrect .new_tag; ensure
this change is applied to the targets that define LATEST_TAG and NEW_TAG and
perform "git tag" so errors are surfaced correctly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5f462068-2d8e-4594-999b-4965a110a89d

📥 Commits

Reviewing files that changed from the base of the PR and between 241701f and fc293ed.

📒 Files selected for processing (1)
  • src/Makefile

Comment on lines +129 to +133
@LATEST_TAG=$$(git tag -l 'v*' --sort=-v:refname | head -n1); \
NEW_TAG=$$(echo $$LATEST_TAG | awk -F. '{$$NF = $$NF + 1;} 1' OFS=.); \
echo $$NEW_TAG; \
git tag $$NEW_TAG; \
echo $$NEW_TAG > .new_tag
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -u

cat >/tmp/makefile-release-check.mk <<'EOF'
bad:
	`@NEW_TAG`=v1.2.3; \
	echo $$NEW_TAG; \
	false; \
	echo $$NEW_TAG > /tmp/.new_tag.bad

good:
	`@set` -e; \
	NEW_TAG=v1.2.3; \
	echo $$NEW_TAG; \
	false; \
	echo $$NEW_TAG > /tmp/.new_tag.good
EOF

rm -f /tmp/.new_tag.bad /tmp/.new_tag.good

echo "== bad target =="
if make -f /tmp/makefile-release-check.mk bad; then
  echo "recipe reported success: yes"
else
  echo "recipe reported success: no"
fi
test -f /tmp/.new_tag.bad && echo ".new_tag written after failure: yes" || echo ".new_tag written after failure: no"

echo
echo "== good target =="
if make -f /tmp/makefile-release-check.mk good; then
  echo "recipe reported success: yes"
else
  echo "recipe reported success: no"
fi
test -f /tmp/.new_tag.good && echo ".new_tag written after failure: yes" || echo ".new_tag written after failure: no"

Repository: DefangLabs/defang

Length of output: 327


🏁 Script executed:

# First, let's look at the actual Makefile to see the current state
head -n 160 src/Makefile | tail -n 35

Repository: DefangLabs/defang

Length of output: 1171


🏁 Script executed:

# Now let's test the bash behavior directly (without make) to verify the failure-masking claim
#!/bin/bash
set -u

echo "=== Testing bash failure-masking behavior ==="
echo

# Test 1: Multiple commands with last one succeeding (bash -c, no set -e)
echo "Test 1: Last command succeeds (no set -e) - recipe should report success"
bash -c 'false; echo "result" > /tmp/test1.txt' && echo "Exit status: success" || echo "Exit status: failure"
test -f /tmp/test1.txt && echo "File written: yes" || echo "File written: no"
echo

# Test 2: Same with set -e
echo "Test 2: Last command succeeds (with set -e) - recipe should still fail on false"
bash -c 'set -e; false; echo "result" > /tmp/test2.txt' && echo "Exit status: success" || echo "Exit status: failure"
test -f /tmp/test2.txt && echo "File written: yes" || echo "File written: no"
echo

# Test 3: Simulating the actual Makefile pattern without set -e
echo "Test 3: Makefile-like pattern - NEW_TAG fails, then echo succeeds"
bash -c 'NEW_TAG=$(false); echo "$NEW_TAG" > /tmp/test3.txt' 2>/dev/null && echo "Exit status: success" || echo "Exit status: failure"
test -f /tmp/test3.txt && echo "File written: yes" || echo "File written: no"
echo

# Test 4: Same pattern with set -e
echo "Test 4: Makefile-like pattern with set -e"
bash -c 'set -e; NEW_TAG=$(false); echo "$NEW_TAG" > /tmp/test4.txt' 2>/dev/null && echo "Exit status: success" || echo "Exit status: failure"
test -f /tmp/test4.txt && echo "File written: yes" || echo "File written: no"

Repository: DefangLabs/defang

Length of output: 525


Add set -e to the increment targets to prevent masking git tag failures.

In semicolon-separated bash recipes, the exit status is determined by the last command. Here, if git tag $$NEW_TAG fails but echo $$NEW_TAG > .new_tag succeeds, the recipe exits with status 0. The release targets then proceed to push whatever tag is in .new_tag, potentially pushing an old or unintended version.

Suggested fix
 increment-patch-version:
-	`@LATEST_TAG`=$$(git tag -l 'v*' --sort=-v:refname | head -n1); \
+	`@set` -e; \
+	LATEST_TAG=$$(git tag -l 'v*' --sort=-v:refname | head -n1); \
 	NEW_TAG=$$(echo $$LATEST_TAG | awk -F. '{$$NF = $$NF + 1;} 1' OFS=.); \
 	echo $$NEW_TAG; \
 	git tag $$NEW_TAG; \
 	echo $$NEW_TAG > .new_tag

 increment-minor-version:
-	`@LATEST_TAG`=$$(git tag -l 'v*' --sort=-v:refname | head -n1); \
+	`@set` -e; \
+	LATEST_TAG=$$(git tag -l 'v*' --sort=-v:refname | head -n1); \
 	NEW_TAG=$$(echo $$LATEST_TAG | awk -F. '{$$(NF-1) = $$(NF-1) + 1; $$NF = 0;} 1' OFS=.); \
 	echo $$NEW_TAG; \
 	git tag $$NEW_TAG; \
 	echo $$NEW_TAG > .new_tag

 increment-major-version:
-	`@LATEST_TAG`=$$(git tag -l 'v*' --sort=-v:refname | head -n1); \
+	`@set` -e; \
+	LATEST_TAG=$$(git tag -l 'v*' --sort=-v:refname | head -n1); \
 	NEW_TAG=$$(echo $$LATEST_TAG | awk -F. '{maj=substr($$1,2)+1; printf "v%d.0.0\n", maj}'); \
 	echo $$NEW_TAG; \
 	git tag $$NEW_TAG; \
 	echo $$NEW_TAG > .new_tag
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Makefile` around lines 129 - 133, The Makefile recipe that computes
LATEST_TAG and NEW_TAG and runs "git tag $$NEW_TAG" can mask failures because
the semicolon-separated commands return the last command's exit code; update the
increment targets to enable immediate exit on failure by adding a "set -e" (or
equivalent "set -o errexit") at the start of the recipe so any failure
(especially in "git tag $$NEW_TAG") aborts the recipe and prevents writing an
incorrect .new_tag; ensure this change is applied to the targets that define
LATEST_TAG and NEW_TAG and perform "git tag" so errors are surfaced correctly.

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