From 93ead1362fd6bd5bba3bf71d04d0c0f12966fe1f Mon Sep 17 00:00:00 2001 From: Shaun Arman Date: Sat, 4 Apr 2026 21:19:13 -0500 Subject: [PATCH] fix(ci): guarantee release jobs run after auto-tag Run linux/windows/macos/arm release build and upload jobs in the auto-tag workflow with needs:auto-tag outputs so release execution no longer depends on a second tag-triggered workflow dispatch path. Made-with: Cursor --- .gitea/workflows/auto-tag.yml | 342 ++++++++++++++++++++++ docs/wiki/CICD-Pipeline.md | 4 + tests/unit/autoTagWorkflowTrigger.test.ts | 12 + 3 files changed, 358 insertions(+) diff --git a/.gitea/workflows/auto-tag.yml b/.gitea/workflows/auto-tag.yml index 81215b3b..24fb4018 100644 --- a/.gitea/workflows/auto-tag.yml +++ b/.gitea/workflows/auto-tag.yml @@ -13,8 +13,12 @@ jobs: runs-on: linux-amd64 container: image: alpine:latest + outputs: + release_tag: ${{ steps.bump.outputs.release_tag }} + tag_created: ${{ steps.bump.outputs.tag_created }} steps: - name: Bump patch version and create tag + id: bump env: RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }} run: | @@ -39,6 +43,7 @@ jobs: fi echo "Latest tag: ${LATEST:-none} → Next: $NEXT" + echo "release_tag=$NEXT" >> "$GITHUB_OUTPUT" # Create and push the tag via git so the tag push event triggers release.yml. git init @@ -50,10 +55,347 @@ jobs: if git ls-remote --exit-code --tags origin "refs/tags/$NEXT" >/dev/null 2>&1; then echo "Tag $NEXT already exists; skipping." + echo "tag_created=false" >> "$GITHUB_OUTPUT" exit 0 fi git tag -a "$NEXT" -m "Release $NEXT" git push origin "refs/tags/$NEXT" + echo "tag_created=true" >> "$GITHUB_OUTPUT" echo "Tag $NEXT pushed successfully" + + build-linux-amd64: + needs: auto-tag + if: needs.auto-tag.outputs.tag_created == 'true' + runs-on: linux-amd64 + container: + image: rust:1.88-slim + steps: + - name: Checkout + run: | + apt-get update -qq && apt-get install -y -qq git + git init + git remote add origin http://172.0.0.29:3000/sarman/tftsr-devops_investigation.git + git fetch --depth=1 origin "$GITHUB_SHA" + git checkout FETCH_HEAD + - name: Install dependencies + run: | + apt-get update -qq && apt-get install -y -qq \ + libwebkit2gtk-4.1-dev libssl-dev libgtk-3-dev \ + libayatana-appindicator3-dev librsvg2-dev patchelf \ + pkg-config curl perl jq + curl -fsSL https://deb.nodesource.com/setup_22.x | bash - + apt-get install -y nodejs + - name: Build + run: | + npm ci --legacy-peer-deps + rustup target add x86_64-unknown-linux-gnu + cargo install tauri-cli --version "^2" --locked + CI=true cargo tauri build --target x86_64-unknown-linux-gnu + - name: Upload artifacts + env: + RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }} + run: | + set -eu + API="http://172.0.0.29:3000/api/v1/repos/$GITHUB_REPOSITORY" + TAG="${{ needs.auto-tag.outputs.release_tag }}" + echo "Creating release for $TAG..." + curl -sf -X POST "$API/releases" \ + -H "Authorization: token $RELEASE_TOKEN" \ + -H "Content-Type: application/json" \ + -d "{\"tag_name\":\"$TAG\",\"name\":\"TFTSR $TAG\",\"body\":\"Release $TAG\",\"draft\":false}" || true + RELEASE_ID=$(curl -sf "$API/releases/tags/$TAG" \ + -H "Authorization: token $RELEASE_TOKEN" | jq -r '.id') + if [ -z "$RELEASE_ID" ] || [ "$RELEASE_ID" = "null" ]; then + echo "ERROR: Failed to get release ID for $TAG" + exit 1 + fi + echo "Release ID: $RELEASE_ID" + ARTIFACTS=$(find src-tauri/target/x86_64-unknown-linux-gnu/release/bundle -type f \ + \( -name "*.deb" -o -name "*.rpm" -o -name "*.AppImage" \)) + if [ -z "$ARTIFACTS" ]; then + echo "ERROR: No Linux 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" \ + | 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: + needs: auto-tag + if: needs.auto-tag.outputs.tag_created == 'true' + runs-on: linux-amd64 + container: + image: rust:1.88-slim + steps: + - name: Checkout + run: | + apt-get update -qq && apt-get install -y -qq git + git init + git remote add origin http://172.0.0.29:3000/sarman/tftsr-devops_investigation.git + git fetch --depth=1 origin "$GITHUB_SHA" + git checkout FETCH_HEAD + - name: Install dependencies + run: | + apt-get update -qq && apt-get install -y -qq mingw-w64 curl nsis perl make jq + curl -fsSL https://deb.nodesource.com/setup_22.x | bash - + apt-get install -y nodejs + - name: Build + env: + CC_x86_64_pc_windows_gnu: x86_64-w64-mingw32-gcc + CXX_x86_64_pc_windows_gnu: x86_64-w64-mingw32-g++ + AR_x86_64_pc_windows_gnu: x86_64-w64-mingw32-ar + CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER: x86_64-w64-mingw32-gcc + OPENSSL_NO_VENDOR: "0" + OPENSSL_STATIC: "1" + run: | + npm ci --legacy-peer-deps + rustup target add x86_64-pc-windows-gnu + cargo install tauri-cli --version "^2" --locked + CI=true cargo tauri build --target x86_64-pc-windows-gnu + - name: Upload artifacts + env: + RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }} + run: | + set -eu + API="http://172.0.0.29:3000/api/v1/repos/$GITHUB_REPOSITORY" + TAG="${{ needs.auto-tag.outputs.release_tag }}" + echo "Creating release for $TAG..." + curl -sf -X POST "$API/releases" \ + -H "Authorization: token $RELEASE_TOKEN" \ + -H "Content-Type: application/json" \ + -d "{\"tag_name\":\"$TAG\",\"name\":\"TFTSR $TAG\",\"body\":\"Release $TAG\",\"draft\":false}" || true + RELEASE_ID=$(curl -sf "$API/releases/tags/$TAG" \ + -H "Authorization: token $RELEASE_TOKEN" | jq -r '.id') + if [ -z "$RELEASE_ID" ] || [ "$RELEASE_ID" = "null" ]; then + echo "ERROR: Failed to get release ID for $TAG" + exit 1 + fi + echo "Release ID: $RELEASE_ID" + 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" \ + | 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: + needs: auto-tag + if: needs.auto-tag.outputs.tag_created == 'true' + runs-on: macos-arm64 + steps: + - name: Checkout + run: | + git init + git remote add origin http://172.0.0.29:3000/sarman/tftsr-devops_investigation.git + git fetch --depth=1 origin "$GITHUB_SHA" + git checkout FETCH_HEAD + - name: Build + env: + MACOSX_DEPLOYMENT_TARGET: "11.0" + run: | + npm ci --legacy-peer-deps + rustup target add aarch64-apple-darwin + cargo install tauri-cli --version "^2" --locked + CI=true cargo tauri build --target aarch64-apple-darwin --bundles app + APP=$(find src-tauri/target/aarch64-apple-darwin/release/bundle/macos -maxdepth 1 -type d -name "*.app" | head -n 1) + if [ -z "$APP" ]; then + echo "ERROR: Could not find macOS app bundle" + exit 1 + fi + APP_NAME=$(basename "$APP" .app) + codesign --deep --force --sign - "$APP" + mkdir -p src-tauri/target/aarch64-apple-darwin/release/bundle/dmg + DMG=src-tauri/target/aarch64-apple-darwin/release/bundle/dmg/${APP_NAME}.dmg + hdiutil create -volname "$APP_NAME" -srcfolder "$APP" -ov -format UDZO "$DMG" + - name: Upload artifacts + env: + RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }} + run: | + set -eu + API="http://172.0.0.29:3000/api/v1/repos/$GITHUB_REPOSITORY" + TAG="${{ needs.auto-tag.outputs.release_tag }}" + echo "Creating release for $TAG..." + curl -sf -X POST "$API/releases" \ + -H "Authorization: token $RELEASE_TOKEN" \ + -H "Content-Type: application/json" \ + -d "{\"tag_name\":\"$TAG\",\"name\":\"TFTSR $TAG\",\"body\":\"Release $TAG\",\"draft\":false}" || true + RELEASE_ID=$(curl -sf "$API/releases/tags/$TAG" \ + -H "Authorization: token $RELEASE_TOKEN" | jq -r '.id') + if [ -z "$RELEASE_ID" ] || [ "$RELEASE_ID" = "null" ]; then + echo "ERROR: Failed to get release ID for $TAG" + exit 1 + fi + echo "Release ID: $RELEASE_ID" + 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" \ + | 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: + needs: auto-tag + if: needs.auto-tag.outputs.tag_created == 'true' + runs-on: linux-arm64 + container: + image: rust:1.88-slim + steps: + - name: Checkout + run: | + apt-get update -qq && apt-get install -y -qq git + git init + git remote add origin http://172.0.0.29:3000/sarman/tftsr-devops_investigation.git + git fetch --depth=1 origin "$GITHUB_SHA" + git checkout FETCH_HEAD + - name: Install dependencies + run: | + apt-get update -qq && apt-get install -y -qq \ + libwebkit2gtk-4.1-dev libssl-dev libgtk-3-dev \ + libayatana-appindicator3-dev librsvg2-dev patchelf \ + pkg-config curl perl jq + curl -fsSL https://deb.nodesource.com/setup_22.x | bash - + apt-get install -y nodejs + - name: Build + run: | + npm ci --legacy-peer-deps + cargo install tauri-cli --version "^2" --locked + CI=true cargo tauri build + - name: Upload artifacts + env: + RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }} + run: | + set -eu + API="http://172.0.0.29:3000/api/v1/repos/$GITHUB_REPOSITORY" + TAG="${{ needs.auto-tag.outputs.release_tag }}" + echo "Creating release for $TAG..." + curl -sf -X POST "$API/releases" \ + -H "Authorization: token $RELEASE_TOKEN" \ + -H "Content-Type: application/json" \ + -d "{\"tag_name\":\"$TAG\",\"name\":\"TFTSR $TAG\",\"body\":\"Release $TAG\",\"draft\":false}" || true + RELEASE_ID=$(curl -sf "$API/releases/tags/$TAG" \ + -H "Authorization: token $RELEASE_TOKEN" | jq -r '.id') + if [ -z "$RELEASE_ID" ] || [ "$RELEASE_ID" = "null" ]; then + echo "ERROR: Failed to get release ID for $TAG" + exit 1 + fi + echo "Release ID: $RELEASE_ID" + ARTIFACTS=$(find src-tauri/target/release/bundle -type f \ + \( -name "*.deb" -o -name "*.rpm" -o -name "*.AppImage" \)) + if [ -z "$ARTIFACTS" ]; then + echo "ERROR: No Linux arm64 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" \ + | 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 diff --git a/docs/wiki/CICD-Pipeline.md b/docs/wiki/CICD-Pipeline.md index d351ae0d..54ad3345 100644 --- a/docs/wiki/CICD-Pipeline.md +++ b/docs/wiki/CICD-Pipeline.md @@ -72,6 +72,10 @@ steps: Auto tags are created by `.gitea/workflows/auto-tag.yml` using `git tag` + `git push` (not the tag API endpoint), so the tag push event reliably triggers this workflow. +In addition, `.gitea/workflows/auto-tag.yml` now runs the same release build/upload +jobs after tagging (`needs: auto-tag`) to guarantee release execution even if the +separate tag-triggered workflow is not dispatched by the server. + ``` Jobs (run in parallel): build-linux-amd64 → cargo tauri build (x86_64-unknown-linux-gnu) diff --git a/tests/unit/autoTagWorkflowTrigger.test.ts b/tests/unit/autoTagWorkflowTrigger.test.ts index 8abf1913..edd69c08 100644 --- a/tests/unit/autoTagWorkflowTrigger.test.ts +++ b/tests/unit/autoTagWorkflowTrigger.test.ts @@ -14,4 +14,16 @@ describe("auto-tag workflow release triggering", () => { expect(workflow).toContain("git push origin \"refs/tags/$NEXT\""); expect(workflow).not.toContain("POST \"$API/tags\""); }); + + it("runs release build jobs after auto-tag succeeds", () => { + const workflow = readFileSync(autoTagWorkflowPath, "utf-8"); + + expect(workflow).toContain("build-linux-amd64:"); + expect(workflow).toContain("build-windows-amd64:"); + expect(workflow).toContain("build-macos-arm64:"); + expect(workflow).toContain("build-linux-arm64:"); + expect(workflow).toContain("needs: auto-tag"); + expect(workflow).toContain("needs.auto-tag.outputs.tag_created == 'true'"); + expect(workflow).toContain("TAG=\"${{ needs.auto-tag.outputs.release_tag }}\""); + }); });