Files
Triple-C/.gitea/workflows/build-app.yml
T
shadow-testandClaude Opus 5 704d3b8f79 CI: fix the MSVC exit-code check and verify .NET 3.5 before bundling
Two follow-ups to the provisioning step.

The exit-code check never worked. %VSEXIT% and %ERRORLEVEL% inside a
parenthesised cmd block are substituted when the block is PARSED, not
when it runs, so the installer's real result was never read — the log
printed "installer failed with " with an empty code, then continued
anyway. It happened to be harmless because the install had in fact
succeeded, but a genuine failure would have sailed past. Now uses
delayed expansion.

Added a .NET 3.5 check. WiX 3.x candle.exe is a .NET 2.0/3.5
application, and Tauri aborts the entire bundle when the MSI target
fails — so a missing runtime silently costs the NSIS installer too, not
just the MSI. Windows 11 ships NetFx3 as DisabledWithPayloadRemoved and
Windows Update could not supply the payload on our runner even across a
reboot; it needed /Source from a mounted ISO. Rather than guess, the job
now fails early with the exact dism command.

Verified on the runner: MSVC Build Tools 2022 installed, the Rust build
completed in 3m59s and produced triple-c.exe, and NetFx3 is now Enabled
with v2.0.50727 present.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 21:50:02 -07:00

668 lines
26 KiB
YAML

name: Build App
on:
push:
branches: [main]
paths:
- "app/**"
- "VERSION"
- ".gitea/workflows/build-app.yml"
pull_request:
branches: [main]
paths:
- "app/**"
- "VERSION"
- ".gitea/workflows/build-app.yml"
workflow_dispatch:
env:
GITEA_URL: ${{ gitea.server_url }}
REPO: ${{ gitea.repository }}
jobs:
compute-version:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.VERSION }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Fetch all tags
run: git fetch --tags
- name: Compute version from VERSION file and tags
id: version
run: |
MAJOR_MINOR=$(cat VERSION | tr -d '[:space:]')
echo "Major.Minor: ${MAJOR_MINOR}"
# Find the latest tag matching v{MAJOR_MINOR}.N (exclude -mac, -win suffixes)
# `|| true` so an empty grep result doesn't fail the step under pipefail.
LATEST_TAG=$(git tag -l "v${MAJOR_MINOR}.*" --sort=-v:refname | grep -E "^v${MAJOR_MINOR}\.[0-9]+$" | head -1 || true)
if [ -n "$LATEST_TAG" ]; then
echo "Latest matching tag: ${LATEST_TAG}"
PATCH=$(git rev-list --count "${LATEST_TAG}..HEAD")
else
echo "No matching tag found for v${MAJOR_MINOR}.*, using total commit count"
PATCH=$(git rev-list --count HEAD)
fi
VERSION="${MAJOR_MINOR}.${PATCH}"
echo "VERSION=${VERSION}" >> $GITHUB_OUTPUT
echo "Computed version: ${VERSION}"
build-linux:
runs-on: ubuntu-latest
needs: [compute-version]
steps:
- name: Install Node.js 22
run: |
NEED_INSTALL=false
if command -v node >/dev/null 2>&1; then
NODE_MAJOR=$(node --version | sed 's/v\([0-9]*\).*/\1/')
OLD_NODE_DIR=$(dirname "$(which node)")
echo "Found Node.js $(node --version) at $(which node) (major: ${NODE_MAJOR})"
if [ "$NODE_MAJOR" -lt 22 ]; then
echo "Node.js ${NODE_MAJOR} is too old, removing before installing 22..."
sudo rm -f "${OLD_NODE_DIR}/node" "${OLD_NODE_DIR}/npm" "${OLD_NODE_DIR}/npx" "${OLD_NODE_DIR}/corepack"
hash -r
NEED_INSTALL=true
fi
else
echo "Node.js not found, installing 22..."
NEED_INSTALL=true
fi
if [ "$NEED_INSTALL" = true ]; then
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt-get install -y nodejs
hash -r
fi
echo "Node.js at: $(which node)"
node --version
npm --version
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set app version
run: |
VERSION="${{ needs.compute-version.outputs.version }}"
sed -i "s/\"version\": \".*\"/\"version\": \"${VERSION}\"/" app/src-tauri/tauri.conf.json
sed -i "s/\"version\": \".*\"/\"version\": \"${VERSION}\"/" app/package.json
sed -i "s/^version = \".*\"/version = \"${VERSION}\"/" app/src-tauri/Cargo.toml
echo "Patched version to ${VERSION}"
- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
libgtk-3-dev \
libwebkit2gtk-4.1-dev \
libayatana-appindicator3-dev \
librsvg2-dev \
libsoup-3.0-dev \
libssl-dev \
libxdo-dev \
patchelf \
pkg-config \
build-essential \
curl \
wget \
file \
xdg-utils
- name: Install Rust stable
run: |
if command -v rustup >/dev/null 2>&1; then
echo "Rust already installed: $(rustc --version)"
rustup update stable
rustup default stable
else
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable
fi
export PATH="$HOME/.cargo/bin:$PATH"
rustc --version
cargo --version
- name: Install frontend dependencies
working-directory: ./app
run: |
rm -rf node_modules package-lock.json
npm install
- name: Install Tauri CLI
working-directory: ./app
run: |
export PATH="$HOME/.cargo/bin:$PATH"
npx tauri --version || npm install @tauri-apps/cli
- name: Build Tauri app
working-directory: ./app
run: |
export PATH="$HOME/.cargo/bin:$PATH"
npx tauri build
- name: Collect artifacts
run: |
mkdir -p artifacts
cp app/src-tauri/target/release/bundle/appimage/*.AppImage artifacts/ 2>/dev/null || true
cp app/src-tauri/target/release/bundle/deb/*.deb artifacts/ 2>/dev/null || true
cp app/src-tauri/target/release/bundle/rpm/*.rpm artifacts/ 2>/dev/null || true
ls -la artifacts/
- name: Upload to Gitea release
if: gitea.event_name == 'push'
env:
TOKEN: ${{ secrets.REGISTRY_TOKEN }}
run: |
TAG="v${{ needs.compute-version.outputs.version }}"
# Create release
curl -s -X POST \
-H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"tag_name\": \"${TAG}\", \"name\": \"Triple-C ${TAG} (Linux)\", \"body\": \"Automated build from commit ${{ gitea.sha }}\"}" \
"${GITEA_URL}/api/v1/repos/${REPO}/releases" > release.json
RELEASE_ID=$(cat release.json | grep -o '"id":[0-9]*' | head -1 | grep -o '[0-9]*')
echo "Release ID: ${RELEASE_ID}"
# Upload each artifact
for file in artifacts/*; do
[ -f "$file" ] || continue
filename=$(basename "$file")
echo "Uploading ${filename}..."
curl -s -X POST \
-H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/octet-stream" \
--data-binary "@${file}" \
"${GITEA_URL}/api/v1/repos/${REPO}/releases/${RELEASE_ID}/assets?name=${filename}"
done
build-macos:
runs-on: macos-latest
needs: [compute-version]
steps:
- name: Install Node.js 22
run: |
NEED_INSTALL=false
if command -v node >/dev/null 2>&1; then
NODE_MAJOR=$(node --version | sed 's/v\([0-9]*\).*/\1/')
echo "Found Node.js $(node --version) (major: ${NODE_MAJOR})"
if [ "$NODE_MAJOR" -lt 22 ]; then
echo "Node.js ${NODE_MAJOR} is too old, upgrading to 22..."
NEED_INSTALL=true
fi
else
echo "Node.js not found, installing 22..."
NEED_INSTALL=true
fi
if [ "$NEED_INSTALL" = true ]; then
brew install node@22
brew link --overwrite node@22
fi
node --version
npm --version
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set app version
run: |
VERSION="${{ needs.compute-version.outputs.version }}"
sed -i '' "s/\"version\": \".*\"/\"version\": \"${VERSION}\"/" app/src-tauri/tauri.conf.json
sed -i '' "s/\"version\": \".*\"/\"version\": \"${VERSION}\"/" app/package.json
sed -i '' "s/^version = \".*\"/version = \"${VERSION}\"/" app/src-tauri/Cargo.toml
echo "Patched version to ${VERSION}"
- name: Install Rust stable
run: |
if command -v rustup >/dev/null 2>&1; then
echo "Rust already installed: $(rustc --version)"
rustup update stable
rustup default stable
else
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable
fi
export PATH="$HOME/.cargo/bin:$PATH"
rustup target add aarch64-apple-darwin x86_64-apple-darwin
rustc --version
cargo --version
- name: Install frontend dependencies
working-directory: ./app
run: |
rm -rf node_modules
npm install
- name: Install Tauri CLI
working-directory: ./app
run: |
export PATH="$HOME/.cargo/bin:$PATH"
npx tauri --version || npm install @tauri-apps/cli
- name: Build Tauri app (universal)
working-directory: ./app
run: |
export PATH="$HOME/.cargo/bin:$PATH"
npx tauri build --target universal-apple-darwin
- name: Collect artifacts
run: |
mkdir -p artifacts
cp app/src-tauri/target/universal-apple-darwin/release/bundle/dmg/*.dmg artifacts/ 2>/dev/null || true
cp app/src-tauri/target/universal-apple-darwin/release/bundle/macos/*.app.tar.gz artifacts/ 2>/dev/null || true
ls -la artifacts/
- name: Upload to Gitea release
if: gitea.event_name == 'push'
env:
TOKEN: ${{ secrets.REGISTRY_TOKEN }}
run: |
set -euo pipefail
TAG="v${{ needs.compute-version.outputs.version }}-mac"
# Idempotent get-or-create. macOS upload has historically failed
# mid-stream (curl exit 92, exit 28), leaving the release record
# with empty assets. A naive POST /releases on the next run hits
# 409 from Gitea for the duplicate tag, the JSON parse below
# then yields an empty RELEASE_ID, and pipefail aborts with an
# opaque exit 1. Look the release up by tag first; create only
# if it doesn't exist; reuse the existing id otherwise.
HTTP_CODE=$(curl -sS -o release.json -w '%{http_code}' \
-H "Authorization: token ${TOKEN}" \
"${GITEA_URL}/api/v1/repos/${REPO}/releases/tags/${TAG}")
case "${HTTP_CODE}" in
200)
echo "Release ${TAG} already exists, reusing"
;;
404)
echo "Release ${TAG} not found, creating"
curl -fsS -X POST \
-H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"tag_name\": \"${TAG}\", \"name\": \"Triple-C v${{ needs.compute-version.outputs.version }} (macOS)\", \"body\": \"Automated build from commit ${{ gitea.sha }}\"}" \
"${GITEA_URL}/api/v1/repos/${REPO}/releases" > release.json
;;
*)
echo "Unexpected HTTP ${HTTP_CODE} from get-release-by-tag" >&2
cat release.json >&2 || true
exit 1
;;
esac
RELEASE_ID=$(grep -o '"id":[0-9]*' release.json | head -1 | grep -o '[0-9]*' || true)
if [ -z "${RELEASE_ID}" ]; then
echo "Failed to parse release id; response was:" >&2
cat release.json >&2
exit 1
fi
echo "Release ID: ${RELEASE_ID}"
# Upload each artifact. If an asset with the same name already
# exists on the release (left over from a partial prior run),
# delete it first so the upload is replace-not-conflict.
# Network hardening: HTTP/1.1 to dodge HTTP/2 stream flakes
# the macOS runner has hit, retries with backoff for transient
# drops, and -f so HTTP errors stop being silently swallowed.
for file in artifacts/*; do
[ -f "$file" ] || continue
filename=$(basename "$file")
EXISTING_ID=$(curl -sS \
-H "Authorization: token ${TOKEN}" \
"${GITEA_URL}/api/v1/repos/${REPO}/releases/${RELEASE_ID}/assets" \
| python3 -c "import json,sys; t=sys.argv[1]; print(next((a['id'] for a in json.load(sys.stdin) if a.get('name')==t), ''))" "${filename}" || true)
if [ -n "${EXISTING_ID}" ]; then
echo "Deleting existing asset ${filename} (id ${EXISTING_ID})"
curl -fsS -X DELETE \
-H "Authorization: token ${TOKEN}" \
"${GITEA_URL}/api/v1/repos/${REPO}/releases/${RELEASE_ID}/assets/${EXISTING_ID}"
fi
echo "Uploading ${filename}..."
curl -fsS --http1.1 \
--retry 5 --retry-all-errors --retry-delay 5 \
--max-time 600 \
-X POST \
-H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/octet-stream" \
--data-binary "@${file}" \
"${GITEA_URL}/api/v1/repos/${REPO}/releases/${RELEASE_ID}/assets?name=${filename}"
done
build-windows:
runs-on: windows-latest
needs: [compute-version]
defaults:
run:
shell: cmd
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set app version
shell: powershell
run: |
$version = "${{ needs.compute-version.outputs.version }}"
(Get-Content app/src-tauri/tauri.conf.json) -replace '"version": ".*?"', "`"version`": `"$version`"" | Set-Content app/src-tauri/tauri.conf.json
(Get-Content app/package.json) -replace '"version": ".*?"', "`"version`": `"$version`"" | Set-Content app/package.json
(Get-Content app/src-tauri/Cargo.toml) -replace '^version = ".*?"', "version = `"$version`"" | Set-Content app/src-tauri/Cargo.toml
Write-Host "Patched version to $version"
- name: Install MSVC C++ build tools
shell: cmd
run: |
rem Tauri links with MSVC, so rustc needs link.exe and the Windows SDK.
rem This job previously assumed a hand-provisioned runner; a runner
rem without them registers fine, advertises windows-latest, accepts the
rem job, downloads the whole crate graph and only then fails at link
rem time with "linker `link.exe` not found".
rem
rem rustc finds MSVC via vswhere and the registry rather than PATH, so
rem installing is enough - no dev-shell activation needed here.
rem
rem Delayed expansion is required: %VAR% inside a parenthesised block
rem is substituted when the block is PARSED, not when it runs, so both
rem %ERRORLEVEL% and %VSEXIT% would read as their pre-block values.
setlocal enabledelayedexpansion
set "VCPATH="
set "VSWHERE=%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe"
if exist "%VSWHERE%" (
for /f "usebackq delims=" %%i in (`"%VSWHERE%" -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath`) do set "VCPATH=%%i"
)
if defined VCPATH (
echo MSVC build tools already present at !VCPATH!
) else (
echo MSVC build tools not found - installing Visual Studio Build Tools
curl -fSL -o "%TEMP%\vs_BuildTools.exe" https://aka.ms/vs/17/release/vs_BuildTools.exe || exit /b 1
"%TEMP%\vs_BuildTools.exe" --quiet --wait --norestart --nocache --add Microsoft.VisualStudio.Workload.VCTools --includeRecommended
set "VSEXIT=!ERRORLEVEL!"
del "%TEMP%\vs_BuildTools.exe" 2>nul
rem 3010 means installed, reboot pending - a success for our purposes.
if not "!VSEXIT!"=="0" if not "!VSEXIT!"=="3010" (
echo Visual Studio Build Tools installer failed with exit code !VSEXIT!
exit /b 1
)
echo Visual Studio Build Tools installed
)
endlocal
- name: Verify .NET 3.5 for the MSI bundler
shell: cmd
run: |
rem WiX 3.x candle.exe is a .NET 2.0/3.5 application, and Tauri aborts
rem the whole bundle when the MSI target fails - taking the NSIS
rem installer with it. Windows 11 ships NetFx3 as
rem DisabledWithPayloadRemoved, and enabling it needs a payload that
rem Windows Update cannot always supply, so this checks rather than
rem guesses and tells you exactly how to fix it on the runner.
if exist "%WINDIR%\Microsoft.NET\Framework64\v2.0.50727" (
echo .NET 3.5 runtime present - WiX can run
) else (
echo ERROR: .NET 3.5 is missing, so WiX candle.exe cannot run and the
echo MSI bundle will fail, which also suppresses the NSIS build.
echo.
echo Fix on this runner, with a Windows ISO mounted ^(e.g. D:^):
echo dism /Online /Enable-Feature /FeatureName:NetFx3 /All ^
echo /Source:D:\sources\sxs /LimitAccess
echo.
echo Windows Update alone is not sufficient when the feature reports
echo DisabledWithPayloadRemoved.
exit /b 1
)
- name: Install Rust stable
run: |
where rustup >nul 2>&1 && (
rustup update stable
rustup default stable
) || (
curl -fSL -o rustup-init.exe https://win.rustup.rs/x86_64
rustup-init.exe -y --default-toolchain stable
del rustup-init.exe
)
- name: Install Node.js
run: |
where node >nul 2>&1 && (
node --version
) || (
curl -fSL -o node-install.msi "https://nodejs.org/dist/v22.14.0/node-v22.14.0-x64.msi"
msiexec /i node-install.msi /quiet /norestart
del node-install.msi
)
- name: Verify tools
run: |
set "PATH=%USERPROFILE%\.cargo\bin;C:\Program Files\nodejs;%PATH%"
rustc --version
cargo --version
node --version
npm --version
- name: Install Tauri CLI via cargo
run: |
set "PATH=%USERPROFILE%\.cargo\bin;C:\Program Files\nodejs;%PATH%"
cargo install tauri-cli --version "^2"
- name: Fix npm platform detection
run: |
set "PATH=%USERPROFILE%\.cargo\bin;C:\Program Files\nodejs;%PATH%"
npm config set os win32
npm config list
- name: Install frontend dependencies
working-directory: ./app
run: |
set "PATH=%USERPROFILE%\.cargo\bin;C:\Program Files\nodejs;%PATH%"
if exist node_modules rmdir /s /q node_modules
npm ci
- name: Build frontend
working-directory: ./app
run: |
set "PATH=%USERPROFILE%\.cargo\bin;C:\Program Files\nodejs;%PATH%"
npm run build
- name: Build Tauri app
working-directory: ./app
env:
TAURI_CONFIG: "{\"build\":{\"beforeBuildCommand\":\"\"}}"
run: |
set "PATH=%USERPROFILE%\.cargo\bin;C:\Program Files\nodejs;%PATH%"
cargo tauri build
- name: Collect artifacts
run: |
set "PATH=%USERPROFILE%\.cargo\bin;C:\Program Files\nodejs;%PATH%"
mkdir artifacts
copy app\src-tauri\target\release\bundle\msi\*.msi artifacts\ 2>nul
copy app\src-tauri\target\release\bundle\nsis\*.exe artifacts\ 2>nul
dir artifacts\
- name: Upload to Gitea release
if: gitea.event_name == 'push'
shell: powershell
env:
TOKEN: ${{ secrets.REGISTRY_TOKEN }}
COMMIT_SHA: ${{ gitea.sha }}
VERSION: ${{ needs.compute-version.outputs.version }}
run: |
$ErrorActionPreference = "Stop"
$tag = "v$env:VERSION-win"
$headers = @{ Authorization = "token $env:TOKEN" }
$api = "$env:GITEA_URL/api/v1/repos/$env:REPO"
# Idempotent get-or-create. The old cmd-batch version swallowed
# curl errors and parsed the release id with findstr, so a 409 on
# a pre-existing tag yielded an empty RELEASE_ID and uploads went to
# a malformed .../releases//assets URL while the step still reported
# success. Look the release up by tag first; create only on 404.
try {
$release = Invoke-RestMethod -Method Get -Headers $headers -Uri "$api/releases/tags/$tag"
Write-Host "Release $tag already exists, reusing"
} catch {
if ($_.Exception.Response.StatusCode.value__ -eq 404) {
Write-Host "Release $tag not found, creating"
$body = @{
tag_name = $tag
name = "Triple-C v$env:VERSION (Windows)"
body = "Automated build from commit $env:COMMIT_SHA"
} | ConvertTo-Json
$release = Invoke-RestMethod -Method Post -Headers $headers `
-ContentType "application/json" -Body $body -Uri "$api/releases"
} else {
throw
}
}
$releaseId = $release.id
if (-not $releaseId) { throw "Failed to resolve release id for $tag" }
Write-Host "Release ID: $releaseId"
# Upload each artifact. Delete any same-named asset left over from a
# partial prior run first, so the upload replaces rather than 409s.
$existing = Invoke-RestMethod -Method Get -Headers $headers -Uri "$api/releases/$releaseId/assets"
foreach ($file in Get-ChildItem -File -Path artifacts\*) {
$name = $file.Name
$dupe = $existing | Where-Object { $_.name -eq $name }
if ($dupe) {
Write-Host "Deleting existing asset $name (id $($dupe.id))"
Invoke-RestMethod -Method Delete -Headers $headers -Uri "$api/releases/$releaseId/assets/$($dupe.id)" | Out-Null
}
Write-Host "Uploading $name..."
$uploadUri = "$api/releases/$releaseId/assets?name=$([uri]::EscapeDataString($name))"
curl.exe -fsS --retry 5 --retry-all-errors --retry-delay 5 --max-time 600 `
-X POST -H "Authorization: token $env:TOKEN" `
-H "Content-Type: application/octet-stream" `
--data-binary "@$($file.FullName)" $uploadUri
if ($LASTEXITCODE -ne 0) { throw "Upload of $name failed (curl exit $LASTEXITCODE)" }
}
create-tag:
runs-on: ubuntu-latest
needs: [compute-version, build-linux, build-macos, build-windows]
if: gitea.event_name == 'push'
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Create version tag
env:
TOKEN: ${{ secrets.REGISTRY_TOKEN }}
run: |
VERSION="${{ needs.compute-version.outputs.version }}"
TAG="v${VERSION}"
echo "Creating tag ${TAG}..."
# Create annotated tag via Gitea API
curl -s -X POST \
-H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"tag_name\": \"${TAG}\", \"target\": \"${{ gitea.sha }}\", \"message\": \"Release ${TAG}\"}" \
"${GITEA_URL}/api/v1/repos/${REPO}/tags" || echo "Tag may already exist (created by release)"
echo "Tag ${TAG} created successfully"
sync-to-github:
runs-on: ubuntu-latest
needs: [compute-version, build-linux, build-macos, build-windows]
if: gitea.event_name == 'push'
env:
GH_PAT: ${{ secrets.GH_PAT }}
GITHUB_REPO: shadowdao/triple-c
steps:
- name: Download artifacts from Gitea releases
env:
TOKEN: ${{ secrets.REGISTRY_TOKEN }}
VERSION: ${{ needs.compute-version.outputs.version }}
run: |
set -e
mkdir -p artifacts
# Download assets from all 3 platform releases
for TAG_SUFFIX in "" "-mac" "-win"; do
TAG="v${VERSION}${TAG_SUFFIX}"
echo "==> Fetching assets for release ${TAG}..."
RELEASE_JSON=$(curl -sf \
-H "Authorization: token ${TOKEN}" \
"${GITEA_URL}/api/v1/repos/${REPO}/releases/tags/${TAG}" 2>/dev/null || echo "{}")
echo "$RELEASE_JSON" | jq -r '.assets[]? | "\(.name) \(.browser_download_url)"' | while read -r NAME URL; do
[ -z "$NAME" ] && continue
echo " Downloading ${NAME}..."
curl -sfL \
-H "Authorization: token ${TOKEN}" \
-o "artifacts/${NAME}" \
"$URL"
done
done
echo "==> All downloaded artifacts:"
ls -la artifacts/
- name: Create GitHub release and upload artifacts
env:
VERSION: ${{ needs.compute-version.outputs.version }}
COMMIT_SHA: ${{ gitea.sha }}
run: |
set -e
TAG="v${VERSION}"
echo "==> Creating unified release ${TAG} on GitHub..."
# Delete existing release if present (idempotent re-runs)
EXISTING=$(curl -sf \
-H "Authorization: Bearer ${GH_PAT}" \
-H "Accept: application/vnd.github+json" \
"https://api.github.com/repos/${GITHUB_REPO}/releases/tags/${TAG}" 2>/dev/null || echo "{}")
EXISTING_ID=$(echo "$EXISTING" | jq -r '.id // empty')
if [ -n "$EXISTING_ID" ]; then
echo " Deleting existing GitHub release ${TAG} (id: ${EXISTING_ID})..."
curl -sf -X DELETE \
-H "Authorization: Bearer ${GH_PAT}" \
-H "Accept: application/vnd.github+json" \
"https://api.github.com/repos/${GITHUB_REPO}/releases/${EXISTING_ID}"
fi
RESPONSE=$(curl -sf -X POST \
-H "Authorization: Bearer ${GH_PAT}" \
-H "Accept: application/vnd.github+json" \
-H "Content-Type: application/json" \
"https://api.github.com/repos/${GITHUB_REPO}/releases" \
-d "{
\"tag_name\": \"${TAG}\",
\"name\": \"Triple-C ${TAG}\",
\"body\": \"Automated build from commit ${COMMIT_SHA}\n\nIncludes Linux, macOS, and Windows artifacts.\",
\"draft\": false,
\"prerelease\": false
}")
UPLOAD_URL=$(echo "$RESPONSE" | jq -r '.upload_url' | sed 's/{?name,label}//')
echo "==> Upload URL: ${UPLOAD_URL}"
for file in artifacts/*; do
[ -f "$file" ] || continue
FILENAME=$(basename "$file")
MIME="application/octet-stream"
echo "==> Uploading ${FILENAME}..."
curl -sf -X POST \
-H "Authorization: Bearer ${GH_PAT}" \
-H "Accept: application/vnd.github+json" \
-H "Content-Type: ${MIME}" \
--data-binary "@${file}" \
"${UPLOAD_URL}?name=$(python3 -c "import urllib.parse, sys; print(urllib.parse.quote(sys.argv[1]))" "${FILENAME}")"
done
echo "==> GitHub release sync complete."