Build App (Preview) / compute-version (pull_request) Successful in 3s
Build App (Preview) / create-release (pull_request) Successful in 1s
Build App (Preview) / build-macos (pull_request) Successful in 2m44s
Build App (Preview) / build-windows (pull_request) Successful in 5m27s
Build App (Preview) / build-linux (pull_request) Successful in 5m36s
Every push to the PR started two workflows on the same commit. build-app.yml ran on pull_request and compiled all three platforms — then published nothing, because every publishing step in it is gated on `gitea.event_name == 'push'`. build-app-preview.yml compiled the same three and published them. Six OS builds per push, half of them unreachable. So the PR trigger moves to the preview workflow, which was already doing the identical compilation and has something to show for it. build-app.yml is now push-to-main and manual dispatch only: releases. Two things a pull_request event changes, handled rather than inherited: `gitea.sha` can be the merge ref — not the commit anyone is testing, and not something to hang a tag on — so the release's target comes from `git rev-parse HEAD` in the checkout; and `gitea.ref_name` is the PR number, so the release body uses `gitea.head_ref` when there is one. The cost is one prerelease per PR commit touching app/**, which the existing Cleanup Old Releases sweep already prunes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
694 lines
29 KiB
YAML
694 lines
29 KiB
YAML
name: Build App
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
paths:
|
|
- "app/**"
|
|
- "VERSION"
|
|
- ".gitea/workflows/build-app.yml"
|
|
workflow_dispatch:
|
|
|
|
# Deliberately **not** on pull_request. Every publishing step here is gated on
|
|
# `gitea.event_name == 'push'`, so a PR run compiled all three platforms and
|
|
# produced nothing — and it ran alongside build-app-preview.yml, which compiles
|
|
# the same three and publishes them. Six OS builds per push, one set of which
|
|
# was unreachable. Previews now carry the PR check; this workflow is releases.
|
|
|
|
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
|
|
# A minor line nobody has tagged yet is a *new* line, and a new line
|
|
# starts at .0 — that is what "we are moving to 0.4.x" means. The
|
|
# old fallback here counted every commit in the repository, which
|
|
# would have made the first 0.4 build 0.4.234.
|
|
echo "No v${MAJOR_MINOR}.* tag yet — starting this line at .0"
|
|
PATCH=0
|
|
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: Work around WOW64 redirection for 32-bit bundlers
|
|
shell: cmd
|
|
run: |
|
|
rem Tauri downloads its bundlers - candle.exe, light.exe and
|
|
rem makensis.exe - and every one of them is 32-bit. When the runner
|
|
rem runs as SYSTEM its %LOCALAPPDATA% is under
|
|
rem C:\Windows\System32\config\systemprofile, and WOW64 redirection
|
|
rem serves any 32-bit process reading System32 from SysWOW64 instead -
|
|
rem where those directories do not exist. The bundlers then cannot see
|
|
rem their own folder: candle exits 0x80131700 and makensis reports
|
|
rem "Unable to start child process, error 0x2". Tauri surfaces neither,
|
|
rem only "failed to run candle.exe", which is why this is worth a
|
|
rem comment this long.
|
|
rem
|
|
rem Junctioning the SysWOW64 view onto the System32 originals makes the
|
|
rem redirected path resolve to the same files. A runner running as a
|
|
rem normal user has a profile outside System32 and skips all of this.
|
|
echo.%LOCALAPPDATA%| find /I "\system32\" >nul
|
|
if errorlevel 1 goto skipwow
|
|
|
|
if not exist "%WINDIR%\System32\config\systemprofile\AppData\Local\tauri" mkdir "%WINDIR%\System32\config\systemprofile\AppData\Local\tauri"
|
|
if not exist "%WINDIR%\SysWOW64\config\systemprofile\AppData\Local" mkdir "%WINDIR%\SysWOW64\config\systemprofile\AppData\Local"
|
|
if not exist "%WINDIR%\SysWOW64\config\systemprofile\AppData\Local\tauri" mklink /J "%WINDIR%\SysWOW64\config\systemprofile\AppData\Local\tauri" "%WINDIR%\System32\config\systemprofile\AppData\Local\tauri"
|
|
|
|
if not exist "%WINDIR%\System32\config\systemprofile\.cache" mkdir "%WINDIR%\System32\config\systemprofile\.cache"
|
|
if not exist "%WINDIR%\SysWOW64\config\systemprofile\.cache" mklink /J "%WINDIR%\SysWOW64\config\systemprofile\.cache" "%WINDIR%\System32\config\systemprofile\.cache"
|
|
|
|
echo WOW64 junctions in place for the SYSTEM profile
|
|
goto :eof
|
|
|
|
:skipwow
|
|
echo Runner profile is outside System32 - WOW64 junctions not needed
|
|
|
|
- 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%"
|
|
rem Every Tauri bundler it downloads - candle.exe, light.exe and
|
|
rem makensis.exe - is 32-bit. A runner running as SYSTEM has
|
|
rem %LOCALAPPDATA% under C:\Windows\system32\config\systemprofile, and
|
|
rem WOW64 redirection sends 32-bit processes reading System32 to
|
|
rem SysWOW64, so they cannot see their own directory: candle exits
|
|
rem 0x80131700 and makensis reports "Unable to start child process,
|
|
rem error 0x2".
|
|
rem
|
|
rem The build VM carries junctions from the SysWOW64 view of
|
|
rem systemprofile\AppData\Local\tauri and systemprofile\.cache to the
|
|
rem System32 originals, which makes the redirected view resolve. A
|
|
rem runner running as a normal user needs no such patch.
|
|
cargo tauri build --bundles msi,nsis
|
|
|
|
- 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\ || exit /b 1
|
|
copy app\src-tauri\target\release\bundle\nsis\*.exe artifacts\ || exit /b 1
|
|
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."
|