fix(ci): harden release asset uploads for reruns

Make all release upload steps fail fast when expected artifacts are missing, replace existing same-name assets before uploading, and print HTTP/body details on upload failures so Linux/Windows publishing issues are diagnosable and reruns remain deterministic.

Made-with: Cursor
This commit is contained in:
Shaun Arman 2026-04-04 21:09:03 -05:00
parent b22d508f25
commit 2d35e2a2c1
3 changed files with 140 additions and 16 deletions

View File

@ -36,6 +36,7 @@ jobs:
env:
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
run: |
set -eu
API="http://172.0.0.29:3000/api/v1/repos/$GITHUB_REPOSITORY"
TAG="$GITHUB_REF_NAME"
echo "Creating release for $TAG..."
@ -57,10 +58,33 @@ jobs:
exit 1
fi
printf '%s\n' "$ARTIFACTS" | while IFS= read -r f; do
echo "Uploading $(basename $f)..."
curl -sf -X POST "$API/releases/$RELEASE_ID/assets" \
NAME=$(basename "$f")
echo "Uploading $NAME..."
EXISTING_IDS=$(curl -sf "$API/releases/$RELEASE_ID" \
-H "Authorization: token $RELEASE_TOKEN" \
-F "attachment=@$f;filename=$(basename $f)" && echo "✓ Uploaded $(basename $f)" || echo "✗ Upload failed: $f"
| jq -r --arg name "$NAME" '.assets[]? | select(.name == $name) | .id')
if [ -n "$EXISTING_IDS" ]; then
printf '%s\n' "$EXISTING_IDS" | while IFS= read -r id; do
[ -n "$id" ] || continue
echo "Deleting existing asset id=$id name=$NAME before upload..."
curl -sf -X DELETE "$API/releases/$RELEASE_ID/assets/$id" \
-H "Authorization: token $RELEASE_TOKEN"
done
fi
RESP_FILE=$(mktemp)
HTTP_CODE=$(curl -sS -o "$RESP_FILE" -w "%{http_code}" -X POST "$API/releases/$RELEASE_ID/assets" \
-H "Authorization: token $RELEASE_TOKEN" \
-F "attachment=@$f;filename=$NAME")
if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then
echo "✓ Uploaded $NAME"
else
echo "✗ Upload failed for $NAME (HTTP $HTTP_CODE)"
python - "$RESP_FILE" <<'PY'
import pathlib, sys
print(pathlib.Path(sys.argv[1]).read_text(errors="replace")[:2000])
PY
exit 1
fi
done
build-windows-amd64:
@ -97,6 +121,7 @@ jobs:
env:
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
run: |
set -eu
API="http://172.0.0.29:3000/api/v1/repos/$GITHUB_REPOSITORY"
TAG="$GITHUB_REF_NAME"
echo "Creating release for $TAG..."
@ -111,12 +136,40 @@ jobs:
exit 1
fi
echo "Release ID: $RELEASE_ID"
find src-tauri/target/x86_64-pc-windows-gnu/release/bundle \
\( -name "*.exe" -o -name "*.msi" \) 2>/dev/null | while read f; do
echo "Uploading $(basename $f)..."
curl -sf -X POST "$API/releases/$RELEASE_ID/assets" \
ARTIFACTS=$(find src-tauri/target/x86_64-pc-windows-gnu/release/bundle -type f \
\( -name "*.exe" -o -name "*.msi" \) 2>/dev/null)
if [ -z "$ARTIFACTS" ]; then
echo "ERROR: No Windows amd64 artifacts were found to upload."
exit 1
fi
printf '%s\n' "$ARTIFACTS" | while IFS= read -r f; do
NAME=$(basename "$f")
echo "Uploading $NAME..."
EXISTING_IDS=$(curl -sf "$API/releases/$RELEASE_ID" \
-H "Authorization: token $RELEASE_TOKEN" \
-F "attachment=@$f;filename=$(basename $f)" && echo "✓ Uploaded $(basename $f)" || echo "✗ Upload failed: $f"
| jq -r --arg name "$NAME" '.assets[]? | select(.name == $name) | .id')
if [ -n "$EXISTING_IDS" ]; then
printf '%s\n' "$EXISTING_IDS" | while IFS= read -r id; do
[ -n "$id" ] || continue
echo "Deleting existing asset id=$id name=$NAME before upload..."
curl -sf -X DELETE "$API/releases/$RELEASE_ID/assets/$id" \
-H "Authorization: token $RELEASE_TOKEN"
done
fi
RESP_FILE=$(mktemp)
HTTP_CODE=$(curl -sS -o "$RESP_FILE" -w "%{http_code}" -X POST "$API/releases/$RELEASE_ID/assets" \
-H "Authorization: token $RELEASE_TOKEN" \
-F "attachment=@$f;filename=$NAME")
if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then
echo "✓ Uploaded $NAME"
else
echo "✗ Upload failed for $NAME (HTTP $HTTP_CODE)"
python - "$RESP_FILE" <<'PY'
import pathlib, sys
print(pathlib.Path(sys.argv[1]).read_text(errors="replace")[:2000])
PY
exit 1
fi
done
build-macos-arm64:
@ -153,6 +206,7 @@ jobs:
env:
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
run: |
set -eu
API="http://172.0.0.29:3000/api/v1/repos/$GITHUB_REPOSITORY"
TAG="$GITHUB_REF_NAME"
# Create release (idempotent)
@ -171,12 +225,39 @@ jobs:
exit 1
fi
echo "Release ID: $RELEASE_ID"
# Upload DMG
find src-tauri/target/aarch64-apple-darwin/release/bundle -name "*.dmg" | while read f; do
echo "Uploading $(basename $f)..."
curl -sf -X POST "$API/releases/$RELEASE_ID/assets" \
ARTIFACTS=$(find src-tauri/target/aarch64-apple-darwin/release/bundle -type f -name "*.dmg")
if [ -z "$ARTIFACTS" ]; then
echo "ERROR: No macOS arm64 DMG artifacts were found to upload."
exit 1
fi
printf '%s\n' "$ARTIFACTS" | while IFS= read -r f; do
NAME=$(basename "$f")
echo "Uploading $NAME..."
EXISTING_IDS=$(curl -sf "$API/releases/$RELEASE_ID" \
-H "Authorization: token $RELEASE_TOKEN" \
-F "attachment=@$f;filename=$(basename $f)" && echo "✓ Uploaded $(basename $f)" || echo "✗ Upload failed: $f"
| jq -r --arg name "$NAME" '.assets[]? | select(.name == $name) | .id')
if [ -n "$EXISTING_IDS" ]; then
printf '%s\n' "$EXISTING_IDS" | while IFS= read -r id; do
[ -n "$id" ] || continue
echo "Deleting existing asset id=$id name=$NAME before upload..."
curl -sf -X DELETE "$API/releases/$RELEASE_ID/assets/$id" \
-H "Authorization: token $RELEASE_TOKEN"
done
fi
RESP_FILE=$(mktemp)
HTTP_CODE=$(curl -sS -o "$RESP_FILE" -w "%{http_code}" -X POST "$API/releases/$RELEASE_ID/assets" \
-H "Authorization: token $RELEASE_TOKEN" \
-F "attachment=@$f;filename=$NAME")
if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then
echo "✓ Uploaded $NAME"
else
echo "✗ Upload failed for $NAME (HTTP $HTTP_CODE)"
python - "$RESP_FILE" <<'PY'
import pathlib, sys
print(pathlib.Path(sys.argv[1]).read_text(errors="replace")[:2000])
PY
exit 1
fi
done
build-linux-arm64:
@ -210,6 +291,7 @@ jobs:
env:
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
run: |
set -eu
API="http://172.0.0.29:3000/api/v1/repos/$GITHUB_REPOSITORY"
TAG="$GITHUB_REF_NAME"
echo "Creating release for $TAG..."
@ -231,8 +313,31 @@ jobs:
exit 1
fi
printf '%s\n' "$ARTIFACTS" | while IFS= read -r f; do
echo "Uploading $(basename $f)..."
curl -sf -X POST "$API/releases/$RELEASE_ID/assets" \
NAME=$(basename "$f")
echo "Uploading $NAME..."
EXISTING_IDS=$(curl -sf "$API/releases/$RELEASE_ID" \
-H "Authorization: token $RELEASE_TOKEN" \
-F "attachment=@$f;filename=$(basename $f)" && echo "✓ Uploaded $(basename $f)" || echo "✗ Upload failed: $f"
| jq -r --arg name "$NAME" '.assets[]? | select(.name == $name) | .id')
if [ -n "$EXISTING_IDS" ]; then
printf '%s\n' "$EXISTING_IDS" | while IFS= read -r id; do
[ -n "$id" ] || continue
echo "Deleting existing asset id=$id name=$NAME before upload..."
curl -sf -X DELETE "$API/releases/$RELEASE_ID/assets/$id" \
-H "Authorization: token $RELEASE_TOKEN"
done
fi
RESP_FILE=$(mktemp)
HTTP_CODE=$(curl -sS -o "$RESP_FILE" -w "%{http_code}" -X POST "$API/releases/$RELEASE_ID/assets" \
-H "Authorization: token $RELEASE_TOKEN" \
-F "attachment=@$f;filename=$NAME")
if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then
echo "✓ Uploaded $NAME"
else
echo "✗ Upload failed for $NAME (HTTP $HTTP_CODE)"
python - "$RESP_FILE" <<'PY'
import pathlib, sys
print(pathlib.Path(sys.argv[1]).read_text(errors="replace")[:2000])
PY
exit 1
fi
done

View File

@ -73,12 +73,16 @@ steps:
Jobs (run in parallel):
build-linux-amd64 → cargo tauri build (x86_64-unknown-linux-gnu)
→ {.deb, .rpm, .AppImage} uploaded to Gitea release
→ fails fast if no Linux artifacts are produced
build-windows-amd64 → cargo tauri build (x86_64-pc-windows-gnu) via mingw-w64
→ {.exe, .msi} uploaded to Gitea release
→ fails fast if no Windows artifacts are produced
build-linux-arm64 → cargo tauri build (aarch64-unknown-linux-gnu)
→ {.deb, .rpm, .AppImage} uploaded to Gitea release
→ fails fast if no Linux artifacts are produced
build-macos-arm64 → cargo tauri build (aarch64-apple-darwin) — runs on local Mac
→ {.dmg} uploaded to Gitea release
→ existing same-name assets are deleted before upload (rerun-safe)
→ unsigned; after install run: xattr -cr /Applications/TFTSR.app
```

View File

@ -21,4 +21,19 @@ describe("release workflow cross-platform artifact handling", () => {
expect(workflow).toContain("ERROR: No Linux amd64 artifacts were found to upload.");
expect(workflow).toContain("ERROR: No Linux arm64 artifacts were found to upload.");
});
it("fails windows uploads when no artifacts are found", () => {
const workflow = readFileSync(releaseWorkflowPath, "utf-8");
expect(workflow).toContain(
"ERROR: No Windows amd64 artifacts were found to upload.",
);
});
it("replaces existing release assets before uploading reruns", () => {
const workflow = readFileSync(releaseWorkflowPath, "utf-8");
expect(workflow).toContain("Deleting existing asset id=$id name=$NAME before upload...");
expect(workflow).toContain("-X DELETE \"$API/releases/$RELEASE_ID/assets/$id\"");
});
});