Compare commits

..

No commits in common. "d5e180740e20c2201f57b39b71f88aa9989ab17e" and "e5d3ff42f53dc1e664b0052ad6b88e83d7e237d5" have entirely different histories.

26 changed files with 2184 additions and 739 deletions

View File

@ -0,0 +1,687 @@
name: Auto Tag
# Runs on every merge to master — reads the latest semver tag, increments
# the patch version, pushes a new tag, then runs release builds in this workflow.
# workflow_dispatch allows manual triggering when Gitea drops a push event.
on:
push:
branches:
- master
workflow_dispatch:
concurrency:
group: auto-tag-master
cancel-in-progress: false
jobs:
autotag:
runs-on: linux-amd64
container:
image: alpine:latest
outputs:
release_tag: ${{ steps.bump.outputs.release_tag }}
steps:
- name: Bump patch version and create tag
id: bump
env:
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
run: |
set -eu
apk add --no-cache curl jq git
API="http://172.0.0.29:3000/api/v1/repos/$GITHUB_REPOSITORY"
# Checkout the source so we can read Cargo.toml
git init
git remote add origin "http://oauth2:${RELEASE_TOKEN}@172.0.0.29:3000/${GITHUB_REPOSITORY}.git"
git fetch --depth=1 origin "$GITHUB_SHA"
git checkout FETCH_HEAD
git config user.name "gitea-actions[bot]"
git config user.email "gitea-actions@local"
# Read the version declared in Cargo.toml
CARGO_VERSION=$(grep '^version' src-tauri/Cargo.toml | head -1 | sed 's/version = "//;s/"//')
CARGO_TAG="v${CARGO_VERSION}"
echo "Cargo.toml declares: $CARGO_TAG"
# Get the latest clean semver tag (vX.Y.Z only, ignore rc/test suffixes)
LATEST=$(curl -s "$API/tags?limit=50" \
-H "Authorization: token $RELEASE_TOKEN" | \
jq -r '.[].name' | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | \
sort -V | tail -1)
echo "Latest git tag: ${LATEST:-none}"
# Version resolution:
# 1. Cargo.toml > latest tag → use Cargo.toml (major/minor bump)
# 2. Cargo.toml == latest tag → tag already exists, use it for builds
# 3. Cargo.toml < latest tag → auto-increment patch on latest tag
if [ -z "$LATEST" ]; then
NEXT="$CARGO_TAG"
elif [ "$(printf '%s\n' "$LATEST" "$CARGO_TAG" | sort -V | tail -1)" = "$CARGO_TAG" ]; then
# Cargo.toml >= latest tag (covers both "ahead" and "equal" cases)
NEXT="$CARGO_TAG"
if [ "$CARGO_TAG" = "$LATEST" ]; then
echo "Cargo.toml matches latest tag — reusing $NEXT for builds"
else
echo "Cargo.toml version $CARGO_TAG is ahead of $LATEST — using Cargo.toml"
fi
else
MAJOR=$(echo "$LATEST" | cut -d. -f1 | tr -d 'v')
MINOR=$(echo "$LATEST" | cut -d. -f2)
PATCH=$(echo "$LATEST" | cut -d. -f3)
NEXT="v${MAJOR}.${MINOR}.$((PATCH + 1))"
fi
echo "Latest tag: ${LATEST:-none} → Next: $NEXT"
if git ls-remote --exit-code --tags origin "refs/tags/$NEXT" >/dev/null 2>&1; then
echo "Tag $NEXT already exists; builds will target this tag."
else
git tag -a "$NEXT" -m "Release $NEXT"
git push origin "refs/tags/$NEXT"
echo "Tag $NEXT pushed successfully"
fi
# Export for downstream jobs — avoids git-describe guessing wrong tag
echo "release_tag=$NEXT" >> "$GITHUB_OUTPUT"
changelog:
needs: autotag
runs-on: linux-amd64
container:
image: alpine:latest
steps:
- name: Install dependencies
run: |
set -eu
apk add --no-cache git curl jq
- name: Checkout (full history + all tags)
env:
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
run: |
set -eu
git init
git remote add origin \
"http://oauth2:${RELEASE_TOKEN}@172.0.0.29:3000/${GITHUB_REPOSITORY}.git"
git fetch --unshallow origin || git fetch --depth=2147483647 origin || true
git fetch --tags origin
git checkout "$GITHUB_SHA" 2>/dev/null || git checkout FETCH_HEAD
git config user.name "gitea-actions[bot]"
git config user.email "gitea-actions@local"
- name: Install git-cliff
run: |
set -eu
CLIFF_VER="2.7.0"
curl -fsSL \
"https://github.com/orhun/git-cliff/releases/download/v${CLIFF_VER}/git-cliff-${CLIFF_VER}-x86_64-unknown-linux-musl.tar.gz" \
| tar -xz --strip-components=1 -C /usr/local/bin \
"git-cliff-${CLIFF_VER}/git-cliff"
- name: Generate changelog
env:
RELEASE_TAG: ${{ needs.autotag.outputs.release_tag }}
run: |
set -eu
CURRENT_TAG="${RELEASE_TAG}"
echo "Building changelog for $CURRENT_TAG"
# Verify the tag is present locally after fetch
if ! git rev-parse "refs/tags/${CURRENT_TAG}" >/dev/null 2>&1; then
echo "ERROR: tag ${CURRENT_TAG} not found locally after fetch"
exit 1
fi
git-cliff --config cliff.toml --output CHANGELOG.md
PREV_TAG=$(git tag --sort=-version:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' \
| grep -v "^${CURRENT_TAG}$" | head -1 || echo "")
if [ -n "$PREV_TAG" ]; then
git-cliff --config cliff.toml --tag "$CURRENT_TAG" --strip all > /tmp/release_body.md || true
else
echo "No previous tag found, generating from git commits"
git log --pretty=format:"- %s" > /tmp/release_body.md || true
fi
echo "=== Release body preview ==="
cat /tmp/release_body.md
- name: Create or update Gitea release
env:
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
RELEASE_TAG: ${{ needs.autotag.outputs.release_tag }}
run: |
set -eu
TAG="${RELEASE_TAG}"
API="http://172.0.0.29:3000/api/v1/repos/$GITHUB_REPOSITORY"
# Try to find an existing release for this tag
RELEASE_ID=$(curl -s "$API/releases/tags/$TAG" \
-H "Authorization: token $RELEASE_TOKEN" | jq -r '.id // empty')
if [ -z "$RELEASE_ID" ]; then
# First run: changelog job owns release creation so build jobs
# never race against a missing release object
echo "Creating release $TAG..."
RELEASE_ID=$(jq -n \
--arg tag "$TAG" \
--arg name "TFTSR $TAG" \
--rawfile body /tmp/release_body.md \
'{tag_name: $tag, name: $name, body: $body, draft: false}' \
| curl -sf -X POST "$API/releases" \
-H "Authorization: token $RELEASE_TOKEN" \
-H "Content-Type: application/json" \
--data @- \
| jq -r '.id')
echo "✓ Release created (id=$RELEASE_ID)"
else
# Re-run: patch the body only
echo "Updating existing release $TAG (id=$RELEASE_ID)..."
jq -n --rawfile body /tmp/release_body.md '{body: $body}' \
| curl -sf -X PATCH "$API/releases/$RELEASE_ID" \
-H "Authorization: token $RELEASE_TOKEN" \
-H "Content-Type: application/json" \
--data @-
echo "✓ Release body updated"
fi
if [ -z "$RELEASE_ID" ] || [ "$RELEASE_ID" = "null" ]; then
echo "ERROR: Failed to create or locate release for $TAG"
exit 1
fi
- name: Commit CHANGELOG.md to master
env:
RELEASE_TAG: ${{ needs.autotag.outputs.release_tag }}
run: |
set -euo pipefail
TAG="${RELEASE_TAG}"
if ! echo "$TAG" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+$'; then
echo "ERROR: Unexpected tag format: $TAG"
exit 1
fi
git add CHANGELOG.md
# Only commit if CHANGELOG.md actually changed — avoids ambiguous
# exit-code handling from 'git commit || echo' with set -e
if git diff --staged --quiet; then
echo "No CHANGELOG.md changes to commit"
else
git commit -m "chore: update CHANGELOG.md for ${TAG} [skip ci]"
fi
# HEAD:master works in detached HEAD state; 'git push origin master'
# would fail because there is no local branch named master
git push origin HEAD:master
echo "✓ CHANGELOG.md committed to master"
- name: Upload CHANGELOG.md as release asset
env:
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
RELEASE_TAG: ${{ needs.autotag.outputs.release_tag }}
run: |
set -eu
TAG="${RELEASE_TAG}"
API="http://172.0.0.29:3000/api/v1/repos/$GITHUB_REPOSITORY"
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: Could not find release for tag $TAG"
exit 1
fi
# Delete existing asset if present to allow re-upload
EXISTING_ID=$(curl -sf "$API/releases/$RELEASE_ID" \
-H "Authorization: token $RELEASE_TOKEN" \
| jq -r '.assets[]? | select(.name == "CHANGELOG.md") | .id')
if [ -n "$EXISTING_ID" ]; then
curl -sf -X DELETE "$API/releases/$RELEASE_ID/assets/$EXISTING_ID" \
-H "Authorization: token $RELEASE_TOKEN"
fi
curl -sf -X POST "$API/releases/$RELEASE_ID/assets" \
-H "Authorization: token $RELEASE_TOKEN" \
-F "attachment=@CHANGELOG.md;filename=CHANGELOG.md"
echo "✓ CHANGELOG.md uploaded"
wiki-sync:
runs-on: linux-amd64
container:
image: alpine:latest
steps:
- name: Install dependencies
run: apk add --no-cache git
- name: Checkout main repository
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: Configure git
run: |
git config --global user.email "actions@gitea.local"
git config --global user.name "Gitea Actions"
git config --global credential.helper ''
- name: Clone and sync wiki
env:
WIKI_TOKEN: ${{ secrets.Wiki }}
run: |
cd /tmp
if [ -n "$WIKI_TOKEN" ]; then
WIKI_URL="http://${WIKI_TOKEN}@172.0.0.29:3000/sarman/tftsr-devops_investigation.wiki.git"
else
WIKI_URL="http://172.0.0.29:3000/sarman/tftsr-devops_investigation.wiki.git"
fi
if ! git clone "$WIKI_URL" wiki 2>/dev/null; then
echo "Wiki doesn't exist yet, creating initial structure..."
mkdir -p wiki
cd wiki
git init
git checkout -b master
echo "# Wiki" > Home.md
git add Home.md
git commit -m "Initial wiki commit"
git remote add origin "$WIKI_URL"
fi
cd /tmp/wiki
if [ -d "$GITHUB_WORKSPACE/docs/wiki" ]; then
cp -v "$GITHUB_WORKSPACE"/docs/wiki/*.md . 2>/dev/null || echo "No wiki files to copy"
fi
git add -A
if ! git diff --staged --quiet; then
git commit -m "docs: sync from docs/wiki/ at commit ${GITHUB_SHA:0:8}"
echo "Pushing to wiki..."
if git push origin master; then
echo "✓ Wiki successfully synced"
else
echo "⚠ Wiki push failed - check token permissions"
exit 1
fi
else
echo "No wiki changes to commit"
fi
build-linux-amd64:
needs: autotag
runs-on: linux-amd64
container:
image: 172.0.0.29:3000/sarman/trcaa-linux-amd64:rust1.88-node22
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: Cache cargo registry
uses: actions/cache@v4
with:
path: |
~/.cargo/registry/index
~/.cargo/registry/cache
~/.cargo/git/db
key: ${{ runner.os }}-cargo-linux-amd64-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-linux-amd64-
- name: Cache npm
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-npm-
- name: Build
env:
APPIMAGE_EXTRACT_AND_RUN: "1"
run: |
npm ci --legacy-peer-deps
CI=true npx 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=$(curl -s "$API/tags?limit=50" \
-H "Authorization: token $RELEASE_TOKEN" | \
jq -r '.[].name' | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | \
sort -V | tail -1 || true)
if [ -z "$TAG" ]; then
echo "ERROR: Could not resolve release tag from repository tags."
exit 1
fi
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" \))
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")
UPLOAD_NAME="linux-amd64-$NAME"
echo "Uploading $UPLOAD_NAME..."
EXISTING_IDS=$(curl -sf "$API/releases/$RELEASE_ID" \
-H "Authorization: token $RELEASE_TOKEN" \
| jq -r --arg name "$UPLOAD_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=$UPLOAD_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=$UPLOAD_NAME")
if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then
echo "✓ Uploaded $UPLOAD_NAME"
else
echo "✗ Upload failed for $UPLOAD_NAME (HTTP $HTTP_CODE)"
python -c 'import pathlib,sys;print(pathlib.Path(sys.argv[1]).read_text(errors="replace")[:2000])' "$RESP_FILE"
exit 1
fi
done
build-windows-amd64:
needs: autotag
runs-on: linux-amd64
container:
image: 172.0.0.29:3000/sarman/trcaa-windows-cross:rust1.88-node22
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: Cache cargo registry
uses: actions/cache@v4
with:
path: |
~/.cargo/registry/index
~/.cargo/registry/cache
~/.cargo/git/db
key: ${{ runner.os }}-cargo-windows-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-windows-
- name: Cache npm
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-npm-
- 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
CI=true npx 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=$(curl -s "$API/tags?limit=50" \
-H "Authorization: token $RELEASE_TOKEN" | \
jq -r '.[].name' | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | \
sort -V | tail -1 || true)
if [ -z "$TAG" ]; then
echo "ERROR: Could not resolve release tag from repository tags."
exit 1
fi
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 -c 'import pathlib,sys;print(pathlib.Path(sys.argv[1]).read_text(errors="replace")[:2000])' "$RESP_FILE"
exit 1
fi
done
build-macos-arm64:
needs: autotag
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
CI=true npx 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=$(curl -s "$API/tags?limit=50" \
-H "Authorization: token $RELEASE_TOKEN" | \
jq -r '.[].name' | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | \
sort -V | tail -1 || true)
if [ -z "$TAG" ]; then
echo "ERROR: Could not resolve release tag from repository tags."
exit 1
fi
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 -c 'import pathlib,sys;print(pathlib.Path(sys.argv[1]).read_text(errors="replace")[:2000])' "$RESP_FILE"
exit 1
fi
done
build-linux-arm64:
needs: autotag
runs-on: linux-amd64
container:
image: 172.0.0.29:3000/sarman/trcaa-linux-arm64:rust1.88-node22
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: Cache cargo registry
uses: actions/cache@v4
with:
path: |
~/.cargo/registry/index
~/.cargo/registry/cache
~/.cargo/git/db
key: ${{ runner.os }}-cargo-arm64-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-arm64-
- name: Cache npm
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-npm-
- name: Build
env:
CC_aarch64_unknown_linux_gnu: aarch64-linux-gnu-gcc
CXX_aarch64_unknown_linux_gnu: aarch64-linux-gnu-g++
AR_aarch64_unknown_linux_gnu: aarch64-linux-gnu-ar
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc
PKG_CONFIG_SYSROOT_DIR: /usr/aarch64-linux-gnu
PKG_CONFIG_PATH: /usr/lib/aarch64-linux-gnu/pkgconfig
PKG_CONFIG_ALLOW_CROSS: "1"
OPENSSL_NO_VENDOR: "0"
OPENSSL_STATIC: "1"
APPIMAGE_EXTRACT_AND_RUN: "1"
run: |
npm ci --legacy-peer-deps
CI=true npx tauri build --target aarch64-unknown-linux-gnu --bundles deb,rpm
- 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=$(curl -s "$API/tags?limit=50" \
-H "Authorization: token $RELEASE_TOKEN" | \
jq -r '.[].name' | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | \
sort -V | tail -1 || true)
if [ -z "$TAG" ]; then
echo "ERROR: Could not resolve release tag from repository tags."
exit 1
fi
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-unknown-linux-gnu/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")
UPLOAD_NAME="linux-arm64-$NAME"
echo "Uploading $UPLOAD_NAME..."
EXISTING_IDS=$(curl -sf "$API/releases/$RELEASE_ID" \
-H "Authorization: token $RELEASE_TOKEN" \
| jq -r --arg name "$UPLOAD_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=$UPLOAD_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=$UPLOAD_NAME")
if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then
echo "✓ Uploaded $UPLOAD_NAME"
else
echo "✗ Upload failed for $UPLOAD_NAME (HTTP $HTTP_CODE)"
python -c 'import pathlib,sys;print(pathlib.Path(sys.argv[1]).read_text(errors="replace")[:2000])' "$RESP_FILE"
exit 1
fi
done

View File

@ -0,0 +1,104 @@
name: Build CI Docker Images
# Rebuilds the pre-baked builder images and pushes them to the local Gitea
# container registry (172.0.0.29:3000).
#
# WHEN TO RUN:
# - Automatically: whenever a Dockerfile under .docker/ changes on master.
# - Manually: via workflow_dispatch (e.g. first-time setup, forced rebuild).
#
# ONE-TIME SERVER PREREQUISITE (run once on 172.0.0.29 before first use):
# echo '{"insecure-registries":["172.0.0.29:3000"]}' \
# | sudo tee /etc/docker/daemon.json
# sudo systemctl restart docker
#
# Images produced:
# 172.0.0.29:3000/sarman/trcaa-linux-amd64:rust1.88-node22
# 172.0.0.29:3000/sarman/trcaa-windows-cross:rust1.88-node22
# 172.0.0.29:3000/sarman/trcaa-linux-arm64:rust1.88-node22
on:
push:
branches:
- master
paths:
- '.docker/**'
workflow_dispatch:
concurrency:
group: build-ci-images
cancel-in-progress: false
env:
REGISTRY: 172.0.0.29:3000
REGISTRY_USER: sarman
jobs:
linux-amd64:
runs-on: linux-amd64
container:
image: alpine:latest
steps:
- name: Checkout
run: |
apk add --no-cache git docker-cli
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 and push linux-amd64 builder
env:
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
run: |
echo "$RELEASE_TOKEN" | docker login $REGISTRY -u $REGISTRY_USER --password-stdin
docker build \
-t $REGISTRY/$REGISTRY_USER/trcaa-linux-amd64:rust1.88-node22 \
-f .docker/Dockerfile.linux-amd64 .
docker push $REGISTRY/$REGISTRY_USER/trcaa-linux-amd64:rust1.88-node22
echo "✓ Pushed $REGISTRY/$REGISTRY_USER/trcaa-linux-amd64:rust1.88-node22"
windows-cross:
runs-on: linux-amd64
container:
image: alpine:latest
steps:
- name: Checkout
run: |
apk add --no-cache git docker-cli
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 and push windows-cross builder
env:
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
run: |
echo "$RELEASE_TOKEN" | docker login $REGISTRY -u $REGISTRY_USER --password-stdin
docker build \
-t $REGISTRY/$REGISTRY_USER/trcaa-windows-cross:rust1.88-node22 \
-f .docker/Dockerfile.windows-cross .
docker push $REGISTRY/$REGISTRY_USER/trcaa-windows-cross:rust1.88-node22
echo "✓ Pushed $REGISTRY/$REGISTRY_USER/trcaa-windows-cross:rust1.88-node22"
linux-arm64:
runs-on: linux-amd64
container:
image: alpine:latest
steps:
- name: Checkout
run: |
apk add --no-cache git docker-cli
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 and push linux-arm64 builder
env:
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
run: |
echo "$RELEASE_TOKEN" | docker login $REGISTRY -u $REGISTRY_USER --password-stdin
docker build \
-t $REGISTRY/$REGISTRY_USER/trcaa-linux-arm64:rust1.88-node22 \
-f .docker/Dockerfile.linux-arm64 .
docker push $REGISTRY/$REGISTRY_USER/trcaa-linux-arm64:rust1.88-node22
echo "✓ Pushed $REGISTRY/$REGISTRY_USER/trcaa-linux-arm64:rust1.88-node22"

View File

@ -0,0 +1,333 @@
name: PR Review Automation
on:
pull_request:
types: [opened, synchronize, reopened, edited]
jobs:
review:
runs-on: ubuntu-latest
permissions:
pull-requests: write
container:
image: ubuntu:22.04
options: --dns 8.8.8.8 --dns 1.1.1.1
steps:
- name: Install dependencies
shell: bash
run: |
set -euo pipefail
apt-get update -qq && apt-get install -y -qq git curl jq python3
- name: Checkout code
shell: bash
env:
REPOSITORY: ${{ github.repository }}
run: |
set -euo pipefail
git init
git remote add origin "https://gogs.tftsr.com/${REPOSITORY}.git"
git fetch --depth=1 origin ${{ github.head_ref }}
git checkout FETCH_HEAD
- name: Build review context
id: context
shell: bash
run: |
set -euo pipefail
git fetch origin ${{ github.base_ref }}
# List changed source files (exclude generated/lock files)
git diff --name-only origin/${{ github.base_ref }}..HEAD \
-- ':!Cargo.lock' ':!package-lock.json' ':!*.lock' \
> /tmp/pr_files.txt
FILE_COUNT=$(wc -l < /tmp/pr_files.txt | tr -d ' ')
echo "files_changed=${FILE_COUNT}" >> $GITHUB_OUTPUT
if [ "$FILE_COUNT" -eq 0 ]; then
echo "No reviewable files changed."
echo "diff_size=0" >> $GITHUB_OUTPUT
exit 0
fi
# Build context: full file content for each changed file.
# Files <= 500 lines: include complete content.
# Files > 500 lines: include the per-file diff with generous context (±50 lines).
#
# Secret scrubbing: match actual credential VALUES only — known API key formats,
# or keyword="long_quoted_literal" (25+ chars). Never scrub on keyword alone,
# which would silently delete function signatures, variable declarations, and tests.
SECRET_PATTERN='AKIA[A-Z0-9]{16}|gh[opsu]_[A-Za-z0-9_]{36,}|xox[baprs]-[0-9]{10,13}-[0-9]{10,13}-[a-zA-Z0-9]{24}|(password|token|api_key|secret)[[:space:]]*=[[:space:]]*["'"'"'][A-Za-z0-9+/_!@#-]{25,}["'"'"']'
# Only strip lines that are ENTIRELY a long base64 blob (e.g. PEM cert bodies)
B64_PATTERN='^[[:space:]]*[A-Za-z0-9+/]{60,}={0,2}[[:space:]]*$'
> /tmp/pr_context.txt
while IFS= read -r file; do
[ -f "$file" ] || continue
lines=$(wc -l < "$file" | tr -d ' ')
printf '\n════════ FILE: %s (%s lines) ════════\n' "$file" "$lines" >> /tmp/pr_context.txt
if [ "$lines" -le 500 ]; then
# Full file — model sees the complete implementation
grep -v -E "$SECRET_PATTERN" "$file" \
| grep -v -E "$B64_PATTERN" \
>> /tmp/pr_context.txt || true
else
# Large file — emit annotated diff hunks (±50 lines of context each)
printf '[File too large for full view (%s lines) — showing changed sections only]\n' "$lines" >> /tmp/pr_context.txt
git diff -U50 origin/${{ github.base_ref }}..HEAD -- "$file" \
| grep -v -E "$SECRET_PATTERN" \
| grep -v -E "$B64_PATTERN" \
>> /tmp/pr_context.txt || true
fi
done < /tmp/pr_files.txt
TOTAL=$(wc -l < /tmp/pr_context.txt | tr -d ' ')
echo "diff_size=${TOTAL}" >> $GITHUB_OUTPUT
# Cap at 6000 lines so we stay within the model's context window
if [ "$TOTAL" -gt 6000 ]; then
head -n 6000 /tmp/pr_context.txt > /tmp/pr_context_capped.txt
mv /tmp/pr_context_capped.txt /tmp/pr_context.txt
echo "[CONTEXT TRUNCATED at 6000 lines — ${TOTAL} total]" >> /tmp/pr_context.txt
fi
- name: Build codebase index
id: index
if: steps.context.outputs.diff_size != '0'
shell: bash
run: |
set -euo pipefail
# Build a compact index of everything that EXISTS in this codebase.
# Included in the prompt so the model cannot invent functions/commands/tables
# that are not present — any finding referencing something absent from this
# index is immediately suspect.
{
echo "## CODEBASE INDEX"
echo "These are the ONLY Tauri commands, TypeScript exports, Rust public functions,"
echo "and database tables that exist in this project. Before raising any finding,"
echo "confirm that every symbol you cite appears in this list or in the file"
echo "contents below. If it does not appear in either, your finding is fabricated."
echo ""
echo "### Registered Tauri commands (lib.rs generate_handler![]):"
grep -oE 'commands::[a-z_]+::[a-z_]+' src-tauri/src/lib.rs 2>/dev/null \
| sort -u | sed 's/^/ /' || true
echo ""
echo "### TypeScript invoke wrappers (src/lib/tauriCommands.ts):"
grep -E '^export (const|interface|type) ' src/lib/tauriCommands.ts 2>/dev/null \
| sed 's/^/ /' || true
echo ""
echo "### Public Rust functions in src-tauri/src/commands/:"
grep -rh --include='*.rs' '^pub ' src-tauri/src/commands/ 2>/dev/null \
| grep 'fn ' | sed 's/^/ /' | sort || true
echo ""
echo "### Database tables (src-tauri/src/db/migrations.rs):"
grep -oE '"[0-9]+_[a-z_]+"' src-tauri/src/db/migrations.rs 2>/dev/null \
| tr -d '"' | sed 's/^/ /' || true
echo ""
} > /tmp/codebase_index.txt
INDEX_LINES=$(wc -l < /tmp/codebase_index.txt | tr -d ' ')
echo "index_lines=${INDEX_LINES}" >> $GITHUB_OUTPUT
echo "Built codebase index: ${INDEX_LINES} lines"
- name: Analyze with LLM
id: analyze
if: steps.context.outputs.diff_size != '0'
shell: bash
env:
LITELLM_URL: http://172.0.0.29:11434/v1
LITELLM_API_KEY: ${{ secrets.OLLAMA_API_KEY }}
PR_TITLE: ${{ github.event.pull_request.title }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
set -euo pipefail
CHANGED_FILES=$(tr '\n' ' ' < /tmp/pr_files.txt)
# Build prompt file. Use 'printf "%s\n" text' throughout so the format
# string is always "%s\n" and content with leading hyphens or embedded
# double-dashes is never misinterpreted as a printf option flag.
{
printf '%s\n\n' 'You are a senior engineer performing a code review.'
printf 'PR Title: %s\n' "$PR_TITLE"
printf 'Files changed: %s\n\n' "$CHANGED_FILES"
printf '%s\n' '---'
cat /tmp/codebase_index.txt
printf '%s\n\n' '---'
printf '%s\n\n' '## Changed file contents'
printf '%s\n' 'Each section is the COMPLETE, FINAL file after PR changes (not a diff).'
printf '%s\n\n' 'Files over 500 lines show only changed sections with surrounding context.'
printf '%s\n' '---'
cat /tmp/pr_context.txt
printf '%s\n\n' '---'
printf '%s\n\n' '## Instructions'
printf '%s\n' 'Before raising any finding:'
printf '%s\n' '1. Confirm every symbol you cite exists in the CODEBASE INDEX or file'
printf '%s\n' ' contents above. If absent from both, discard the finding.'
printf '%s\n' '2. Quote the exact line(s) from the file contents that support it.'
printf '%s\n' '3. Confirm the issue is genuine, not intentional design.'
printf '%s\n\n' '4. If any step fails, discard silently - do not mention it.'
printf '%s\n\n' 'Do NOT show reasoning. Only output confirmed issues.'
printf '%s\n' 'Severity:'
printf '%s\n' '- BLOCKER: fails to compile, corrupts data, or security vulnerability'
printf '%s\n' '- WARNING: real risk to address before merge'
printf '%s\n\n' '- SUGGESTION: minor improvement, follow-up PR fine'
printf '%s\n\n' 'Focus: security bugs, logic errors, data loss, injection, unhandled errors.'
printf '%s\n\n' 'Ignore: style, missing comments, speculative future concerns.'
printf '%s\n\n' '## Output format (strict)'
printf '%s\n\n' '**Summary** (2-3 sentences)'
printf '%s\n' '**Findings**'
printf '%s\n' '- [SEVERITY] file:line - description'
printf '%s\n' ' Evidence: quoted line'
printf '%s\n\n' ' Fix: concrete change'
printf '%s\n\n' '(Write "No findings." if none.)'
printf '%s\n' '**Verdict**: APPROVE / APPROVE WITH COMMENTS / REQUEST CHANGES'
} > /tmp/prompt.txt
# Write body to file — passing 100KB+ JSON as a shell arg hits ARG_MAX.
jq -cn \
--arg model "qwen3-coder-next" \
--rawfile content /tmp/prompt.txt \
'{model: $model, messages: [{role: "user", content: $content}], stream: false}' \
> /tmp/body.json
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] PR #${PR_NUMBER} - Calling liteLLM API ($(wc -c < /tmp/body.json) bytes)..."
HTTP_CODE=$(curl -s --max-time 300 --connect-timeout 30 \
--retry 3 --retry-delay 10 --retry-connrefused --retry-max-time 300 \
-o /tmp/llm_response.json -w "%{http_code}" \
-X POST "$LITELLM_URL/chat/completions" \
-H "Authorization: Bearer $LITELLM_API_KEY" \
-H "Content-Type: application/json" \
--data @/tmp/body.json)
echo "HTTP status: $HTTP_CODE"
echo "Response file size: $(wc -c < /tmp/llm_response.json) bytes"
if [ "$HTTP_CODE" != "200" ]; then
echo "ERROR: liteLLM returned HTTP $HTTP_CODE"
cat /tmp/llm_response.json
exit 1
fi
if ! jq empty /tmp/llm_response.json 2>/dev/null; then
echo "ERROR: Invalid JSON response from liteLLM"
cat /tmp/llm_response.json
exit 1
fi
REVIEW=$(jq -r '.choices[0].message.content // empty' /tmp/llm_response.json)
if [ -z "$REVIEW" ]; then
echo "ERROR: No content in liteLLM response"
exit 1
fi
echo "Review length: ${#REVIEW} chars"
echo "$REVIEW" > /tmp/pr_review.txt
- name: Verify findings against codebase
if: steps.analyze.outcome == 'success'
shell: bash
run: |
set -euo pipefail
# For each finding that contains a fenced code block under "Evidence:",
# grep at least one substantial line of that block against the FULL repository.
# Searching the full repo (not just changed files) prevents false UNVERIFIED
# tags when the model correctly quotes unchanged files, while still flagging
# fabricated code that doesn't exist anywhere in the codebase.
python3 - << 'PYEOF'
import re, os, subprocess
review = open('/tmp/pr_review.txt').read()
# Load ENTIRE tracked repository (all .rs, .ts, .tsx, .yml, .toml, .json files)
result = subprocess.run(
['git', 'ls-files', '--',
'*.rs', '*.ts', '*.tsx', '*.yml', '*.yaml', '*.toml', '*.json', '*.sql'],
capture_output=True, text=True
)
all_tracked = [f.strip() for f in result.stdout.splitlines() if f.strip()]
all_content_parts = []
for path in all_tracked:
if os.path.isfile(path):
try:
all_content_parts.append(open(path).read())
except Exception:
pass
all_content = '\n'.join(all_content_parts)
def evidence_exists(block: str) -> bool:
"""True if ≥1 significant line from the block is found verbatim in changed files."""
for raw in block.splitlines():
line = raw.lstrip('+-').strip()
# Skip blank, very short, pure-comment, or diff-header lines
if len(line) < 20:
continue
if line.startswith(('//','#','/*','*','Fix:','Evidence:','---','+++')):
continue
if line in all_content:
return True
return False
# Split on finding markers; re-join after optional tagging
severity_re = re.compile(r'\[(BLOCKER|WARNING|SUGGESTION)\]')
def tag_if_unverified(finding_text: str) -> str:
code_match = re.search(r'```[^\n]*\n(.*?)```', finding_text, re.DOTALL)
if code_match and not evidence_exists(code_match.group(1)):
# Replace first severity tag with a prefixed version
return severity_re.sub(
lambda m: f'[{m.group(1)} — ⚠️ UNVERIFIED: evidence not found in PR files]',
finding_text, count=1
)
return finding_text
# Split review into preamble + individual finding blocks
# Each block starts at a severity marker line
parts = re.split(r'(?=^\[(?:BLOCKER|WARNING|SUGGESTION)\])', review, flags=re.MULTILINE)
result = parts[0] # preamble (Summary, etc.)
for block in parts[1:]:
result += tag_if_unverified(block)
open('/tmp/pr_review.txt', 'w').write(result)
print(f"Verification complete — {len(parts)-1} finding(s) checked.")
PYEOF
- name: Post review comment
if: always() && steps.context.outputs.diff_size != '0'
shell: bash
env:
TF_TOKEN: ${{ secrets.TFT_GITEA_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
REPOSITORY: ${{ github.repository }}
run: |
set -euo pipefail
if [ -z "${TF_TOKEN:-}" ]; then
echo "ERROR: TFT_GITEA_TOKEN secret is not set"
exit 1
fi
if [ -f "/tmp/pr_review.txt" ] && [ -s "/tmp/pr_review.txt" ]; then
REVIEW_BODY=$(head -c 65536 /tmp/pr_review.txt)
BODY=$(jq -n \
--arg body "Automated PR Review (qwen3-coder-next via liteLLM):\n\n${REVIEW_BODY}" \
'{body: $body, event: "COMMENT"}')
else
BODY=$(jq -n \
'{body: "Automated PR Review could not be completed - LLM analysis failed or produced no output.", event: "COMMENT"}')
fi
HTTP_CODE=$(curl -s --max-time 30 --connect-timeout 10 \
-o /tmp/review_post_response.json -w "%{http_code}" \
-X POST "https://gogs.tftsr.com/api/v1/repos/${REPOSITORY}/pulls/${PR_NUMBER}/reviews" \
-H "Authorization: Bearer $TF_TOKEN" \
-H "Content-Type: application/json" \
-d "$BODY")
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] Post review HTTP status: $HTTP_CODE"
if [ "$HTTP_CODE" != "200" ] && [ "$HTTP_CODE" != "201" ]; then
echo "ERROR: Failed to post review (HTTP $HTTP_CODE)"
cat /tmp/review_post_response.json
exit 1
fi
- name: Cleanup
if: always()
shell: bash
run: rm -f /tmp/pr_diff.txt /tmp/pr_context.txt /tmp/codebase_index.txt /tmp/prompt.txt /tmp/body.json /tmp/llm_response.json /tmp/pr_review.txt /tmp/review_post_response.json /tmp/pr_files.txt

186
.gitea/workflows/test.yml Normal file
View File

@ -0,0 +1,186 @@
name: Test
on:
push:
branches:
- master
pull_request:
jobs:
rust-fmt-check:
runs-on: ubuntu-latest
container:
image: 172.0.0.29:3000/sarman/trcaa-linux-amd64:rust1.88-node22
steps:
- name: Checkout
run: |
set -eux
git init
git remote add origin http://172.0.0.29:3000/sarman/tftsr-devops_investigation.git
if [ -n "${GITHUB_SHA:-}" ] && git fetch --depth=1 origin "$GITHUB_SHA"; then
echo "Fetched commit SHA: $GITHUB_SHA"
elif [ -n "${GITHUB_REF_NAME:-}" ] && git fetch --depth=1 origin "$GITHUB_REF_NAME"; then
echo "Fetched ref name: $GITHUB_REF_NAME"
elif [ -n "${GITHUB_REF:-}" ]; then
REF_NAME="${GITHUB_REF#refs/heads/}"
git fetch --depth=1 origin "$REF_NAME"
echo "Fetched ref from GITHUB_REF: $REF_NAME"
else
git fetch --depth=1 origin master
echo "Fetched fallback ref: master"
fi
git checkout FETCH_HEAD
- name: Cache cargo registry
uses: actions/cache@v4
with:
path: |
~/.cargo/registry/index
~/.cargo/registry/cache
~/.cargo/git/db
key: ${{ runner.os }}-cargo-linux-amd64-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-linux-amd64-
- name: Install dependencies
run: npm install --legacy-peer-deps
- name: Update version from Git
run: node scripts/update-version.mjs
- run: cargo generate-lockfile --manifest-path src-tauri/Cargo.toml
- run: cargo fmt --manifest-path src-tauri/Cargo.toml --check
rust-clippy:
runs-on: ubuntu-latest
container:
image: 172.0.0.29:3000/sarman/trcaa-linux-amd64:rust1.88-node22
steps:
- name: Checkout
run: |
set -eux
git init
git remote add origin http://172.0.0.29:3000/sarman/tftsr-devops_investigation.git
if [ -n "${GITHUB_SHA:-}" ] && git fetch --depth=1 origin "$GITHUB_SHA"; then
echo "Fetched commit SHA: $GITHUB_SHA"
elif [ -n "${GITHUB_REF_NAME:-}" ] && git fetch --depth=1 origin "$GITHUB_REF_NAME"; then
echo "Fetched ref name: $GITHUB_REF_NAME"
elif [ -n "${GITHUB_REF:-}" ]; then
REF_NAME="${GITHUB_REF#refs/heads/}"
git fetch --depth=1 origin "$REF_NAME"
echo "Fetched ref from GITHUB_REF: $REF_NAME"
else
git fetch --depth=1 origin master
echo "Fetched fallback ref: master"
fi
git checkout FETCH_HEAD
- name: Cache cargo registry
uses: actions/cache@v4
with:
path: |
~/.cargo/registry/index
~/.cargo/registry/cache
~/.cargo/git/db
key: ${{ runner.os }}-cargo-linux-amd64-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-linux-amd64-
- run: cargo clippy --manifest-path src-tauri/Cargo.toml -- -D warnings
rust-tests:
runs-on: ubuntu-latest
container:
image: 172.0.0.29:3000/sarman/trcaa-linux-amd64:rust1.88-node22
steps:
- name: Checkout
run: |
set -eux
git init
git remote add origin http://172.0.0.29:3000/sarman/tftsr-devops_investigation.git
if [ -n "${GITHUB_SHA:-}" ] && git fetch --depth=1 origin "$GITHUB_SHA"; then
echo "Fetched commit SHA: $GITHUB_SHA"
elif [ -n "${GITHUB_REF_NAME:-}" ] && git fetch --depth=1 origin "$GITHUB_REF_NAME"; then
echo "Fetched ref name: $GITHUB_REF_NAME"
elif [ -n "${GITHUB_REF:-}" ]; then
REF_NAME="${GITHUB_REF#refs/heads/}"
git fetch --depth=1 origin "$REF_NAME"
echo "Fetched ref from GITHUB_REF: $REF_NAME"
else
git fetch --depth=1 origin master
echo "Fetched fallback ref: master"
fi
git checkout FETCH_HEAD
- name: Cache cargo registry
uses: actions/cache@v4
with:
path: |
~/.cargo/registry/index
~/.cargo/registry/cache
~/.cargo/git/db
key: ${{ runner.os }}-cargo-linux-amd64-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-linux-amd64-
- run: cargo test --manifest-path src-tauri/Cargo.toml -- --test-threads=1
frontend-typecheck:
runs-on: ubuntu-latest
container:
image: node:22-alpine
steps:
- name: Checkout
run: |
set -eux
apk add --no-cache git
git init
git remote add origin http://172.0.0.29:3000/sarman/tftsr-devops_investigation.git
if [ -n "${GITHUB_SHA:-}" ] && git fetch --depth=1 origin "$GITHUB_SHA"; then
echo "Fetched commit SHA: $GITHUB_SHA"
elif [ -n "${GITHUB_REF_NAME:-}" ] && git fetch --depth=1 origin "$GITHUB_REF_NAME"; then
echo "Fetched ref name: $GITHUB_REF_NAME"
elif [ -n "${GITHUB_REF:-}" ]; then
REF_NAME="${GITHUB_REF#refs/heads/}"
git fetch --depth=1 origin "$REF_NAME"
echo "Fetched ref from GITHUB_REF: $REF_NAME"
else
git fetch --depth=1 origin master
echo "Fetched fallback ref: master"
fi
git checkout FETCH_HEAD
- name: Cache npm
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-npm-
- run: npm ci --legacy-peer-deps
- run: npx tsc --noEmit
frontend-tests:
runs-on: ubuntu-latest
container:
image: node:22-alpine
steps:
- name: Checkout
run: |
set -eux
apk add --no-cache git
git init
git remote add origin http://172.0.0.29:3000/sarman/tftsr-devops_investigation.git
if [ -n "${GITHUB_SHA:-}" ] && git fetch --depth=1 origin "$GITHUB_SHA"; then
echo "Fetched commit SHA: $GITHUB_SHA"
elif [ -n "${GITHUB_REF_NAME:-}" ] && git fetch --depth=1 origin "$GITHUB_REF_NAME"; then
echo "Fetched ref name: $GITHUB_REF_NAME"
elif [ -n "${GITHUB_REF:-}" ]; then
REF_NAME="${GITHUB_REF#refs/heads/}"
git fetch --depth=1 origin "$REF_NAME"
echo "Fetched ref from GITHUB_REF: $REF_NAME"
else
git fetch --depth=1 origin master
echo "Fetched fallback ref: master"
fi
git checkout FETCH_HEAD
- name: Cache npm
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-npm-
- run: npm ci --legacy-peer-deps
- run: npm run test:run

9
.github/CODEOWNERS vendored
View File

@ -1,9 +0,0 @@
# All files require review from owner and GitHub Copilot
* @Shaun-Arman-VFK387_moto @github-copilot
# Rust backend
src-tauri/ @Shaun-Arman-VFK387_moto @github-copilot
# CI/CD pipelines and Docker build configs
.github/workflows/ @Shaun-Arman-VFK387_moto @github-copilot
.docker/ @Shaun-Arman-VFK387_moto @github-copilot

View File

@ -1,20 +1,26 @@
name: Build CI Docker Images
# Rebuilds the pre-baked builder images and pushes them to ghcr.io.
# Rebuilds the pre-baked builder images and pushes them to the local Gitea
# container registry (172.0.0.29:3000).
#
# WHEN TO RUN:
# - Automatically: whenever a Dockerfile under .docker/ changes on main.
# - Automatically: whenever a Dockerfile under .docker/ changes on master.
# - Manually: via workflow_dispatch (e.g. first-time setup, forced rebuild).
#
# ONE-TIME SERVER PREREQUISITE (run once on 172.0.0.29 before first use):
# echo '{"insecure-registries":["172.0.0.29:3000"]}' \
# | sudo tee /etc/docker/daemon.json
# sudo systemctl restart docker
#
# Images produced:
# ghcr.io/msicie/trcaa-linux-amd64:rust1.88-node22
# ghcr.io/msicie/trcaa-windows-cross:rust1.88-node22
# ghcr.io/msicie/trcaa-linux-arm64:rust1.88-node22
# 172.0.0.29:3000/sarman/trcaa-linux-amd64:rust1.88-node22
# 172.0.0.29:3000/sarman/trcaa-windows-cross:rust1.88-node22
# 172.0.0.29:3000/sarman/trcaa-linux-arm64:rust1.88-node22
on:
push:
branches:
- main
- master
paths:
- '.docker/**'
workflow_dispatch:
@ -24,61 +30,66 @@ concurrency:
cancel-in-progress: false
env:
REGISTRY: ghcr.io
REGISTRY_OWNER: msicie
permissions:
contents: read
packages: write
REGISTRY: 172.0.0.29:3000
REGISTRY_USER: sarman
jobs:
linux-amd64:
runs-on: ubuntu-latest
runs-on: linux-amd64
container:
image: docker:24-cli
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Log in to ghcr.io
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
- name: Build and push linux-amd64 builder
env:
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
run: |
echo "$RELEASE_TOKEN" | docker login $REGISTRY -u $REGISTRY_USER --password-stdin
docker build \
-t $REGISTRY/$REGISTRY_OWNER/trcaa-linux-amd64:rust1.88-node22 \
-t $REGISTRY/$REGISTRY_USER/trcaa-linux-amd64:rust1.88-node22 \
-f .docker/Dockerfile.linux-amd64 .
docker push $REGISTRY/$REGISTRY_OWNER/trcaa-linux-amd64:rust1.88-node22
echo "✓ Pushed $REGISTRY/$REGISTRY_OWNER/trcaa-linux-amd64:rust1.88-node22"
docker push $REGISTRY/$REGISTRY_USER/trcaa-linux-amd64:rust1.88-node22
echo "✓ Pushed $REGISTRY/$REGISTRY_USER/trcaa-linux-amd64:rust1.88-node22"
windows-cross:
runs-on: ubuntu-latest
runs-on: linux-amd64
container:
image: docker:24-cli
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Log in to ghcr.io
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
- name: Build and push windows-cross builder
env:
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
run: |
echo "$RELEASE_TOKEN" | docker login $REGISTRY -u $REGISTRY_USER --password-stdin
docker build \
-t $REGISTRY/$REGISTRY_OWNER/trcaa-windows-cross:rust1.88-node22 \
-t $REGISTRY/$REGISTRY_USER/trcaa-windows-cross:rust1.88-node22 \
-f .docker/Dockerfile.windows-cross .
docker push $REGISTRY/$REGISTRY_OWNER/trcaa-windows-cross:rust1.88-node22
echo "✓ Pushed $REGISTRY/$REGISTRY_OWNER/trcaa-windows-cross:rust1.88-node22"
docker push $REGISTRY/$REGISTRY_USER/trcaa-windows-cross:rust1.88-node22
echo "✓ Pushed $REGISTRY/$REGISTRY_USER/trcaa-windows-cross:rust1.88-node22"
linux-arm64:
runs-on: ubuntu-latest
runs-on: linux-amd64
container:
image: docker:24-cli
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Log in to ghcr.io
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
- name: Build and push linux-arm64 builder
env:
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
run: |
echo "$RELEASE_TOKEN" | docker login $REGISTRY -u $REGISTRY_USER --password-stdin
docker build \
-t $REGISTRY/$REGISTRY_OWNER/trcaa-linux-arm64:rust1.88-node22 \
-t $REGISTRY/$REGISTRY_USER/trcaa-linux-arm64:rust1.88-node22 \
-f .docker/Dockerfile.linux-arm64 .
docker push $REGISTRY/$REGISTRY_OWNER/trcaa-linux-arm64:rust1.88-node22
echo "✓ Pushed $REGISTRY/$REGISTRY_OWNER/trcaa-linux-arm64:rust1.88-node22"
docker push $REGISTRY/$REGISTRY_USER/trcaa-linux-arm64:rust1.88-node22
echo "✓ Pushed $REGISTRY/$REGISTRY_USER/trcaa-linux-arm64:rust1.88-node22"

View File

@ -1,68 +1,43 @@
name: Release
name: Auto Tag
# Runs on every merge to main — reads the latest semver tag, increments
# the patch version, pushes a new tag, generates a changelog, then builds
# multi-platform release artifacts and uploads them to GitHub Releases.
# workflow_dispatch allows manual triggering.
# Runs on every merge to master — reads the latest semver tag, increments
# the patch version, pushes a new tag, then runs release builds in this workflow.
# workflow_dispatch allows manual triggering when Gitea drops a push event.
on:
push:
branches:
- main
- master
workflow_dispatch:
concurrency:
group: release-main
group: auto-tag-master
cancel-in-progress: false
permissions:
contents: write
packages: read
jobs:
autotag:
runs-on: ubuntu-latest
outputs:
release_tag: ${{ steps.bump.outputs.release_tag }}
runs-on: linux-amd64
container:
image: alpine:latest
steps:
- name: Checkout (full history + all tags)
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Configure git
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- name: Bump patch version and create tag
id: bump
env:
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
run: |
set -eu
apk add --no-cache curl jq git
# Read the version declared in Cargo.toml
CARGO_VERSION=$(grep '^version' src-tauri/Cargo.toml | head -1 | sed 's/version = "//;s/"//')
CARGO_TAG="v${CARGO_VERSION}"
echo "Cargo.toml declares: $CARGO_TAG"
API="http://172.0.0.29:3000/api/v1/repos/$GITHUB_REPOSITORY"
# Get the latest clean semver tag (vX.Y.Z only)
LATEST=$(git tag --sort=-version:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -1 || echo "")
echo "Latest git tag: ${LATEST:-none}"
# Get the latest clean semver tag (vX.Y.Z only, ignore rc/test suffixes)
LATEST=$(curl -s "$API/tags?limit=50" \
-H "Authorization: token $RELEASE_TOKEN" | \
jq -r '.[].name' | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | \
sort -V | tail -1)
# Version resolution:
# 1. Cargo.toml > latest tag → use Cargo.toml (major/minor bump)
# 2. Cargo.toml == latest tag → reuse for builds (already tagged)
# 3. Cargo.toml < latest tag → auto-increment patch on latest tag
if [ -z "$LATEST" ]; then
NEXT="$CARGO_TAG"
elif [ "$(printf '%s\n' "$LATEST" "$CARGO_TAG" | sort -V | tail -1)" = "$CARGO_TAG" ]; then
NEXT="$CARGO_TAG"
if [ "$CARGO_TAG" = "$LATEST" ]; then
echo "Cargo.toml matches latest tag — reusing $NEXT for builds"
else
echo "Cargo.toml version $CARGO_TAG is ahead of $LATEST — using Cargo.toml"
fi
NEXT="v0.1.0"
else
MAJOR=$(echo "$LATEST" | cut -d. -f1 | tr -d 'v')
MINOR=$(echo "$LATEST" | cut -d. -f2)
@ -72,136 +47,53 @@ jobs:
echo "Latest tag: ${LATEST:-none} → Next: $NEXT"
# Create and push the tag via git.
git init
git remote add origin "http://oauth2:${RELEASE_TOKEN}@172.0.0.29:3000/${GITHUB_REPOSITORY}.git"
git fetch --depth=1 origin "$GITHUB_SHA"
git checkout FETCH_HEAD
git config user.name "gitea-actions[bot]"
git config user.email "gitea-actions@local"
if git ls-remote --exit-code --tags origin "refs/tags/$NEXT" >/dev/null 2>&1; then
echo "Tag $NEXT already exists; builds will target this tag."
else
git tag -a "$NEXT" -m "Release $NEXT"
git push origin "refs/tags/$NEXT"
echo "Tag $NEXT pushed successfully"
echo "Tag $NEXT already exists; skipping."
exit 0
fi
echo "release_tag=$NEXT" >> "$GITHUB_OUTPUT"
git tag -a "$NEXT" -m "Release $NEXT"
git push origin "refs/tags/$NEXT"
changelog:
needs: autotag
runs-on: ubuntu-latest
steps:
- name: Checkout (full history + all tags)
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Configure git
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- name: Install git-cliff
run: |
set -eu
CLIFF_VER="2.7.0"
curl -fsSL \
"https://github.com/orhun/git-cliff/releases/download/v${CLIFF_VER}/git-cliff-${CLIFF_VER}-x86_64-unknown-linux-musl.tar.gz" \
| tar -xz --strip-components=1 -C /usr/local/bin \
"git-cliff-${CLIFF_VER}/git-cliff"
- name: Generate changelog
env:
RELEASE_TAG: ${{ needs.autotag.outputs.release_tag }}
run: |
set -eu
CURRENT_TAG="${RELEASE_TAG}"
echo "Building changelog for $CURRENT_TAG"
if ! git rev-parse "refs/tags/${CURRENT_TAG}" >/dev/null 2>&1; then
echo "ERROR: tag ${CURRENT_TAG} not found locally after fetch"
exit 1
fi
git-cliff --config cliff.toml --output CHANGELOG.md
PREV_TAG=$(git tag --sort=-version:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' \
| grep -v "^${CURRENT_TAG}$" | head -1 || echo "")
if [ -n "$PREV_TAG" ]; then
git-cliff --config cliff.toml --tag "$CURRENT_TAG" --strip all > /tmp/release_body.md || true
else
echo "No previous tag found, generating from git commits"
git log --pretty=format:"- %s" > /tmp/release_body.md || true
fi
echo "=== Release body preview ==="
cat /tmp/release_body.md
- name: Create or update GitHub release
env:
RELEASE_TAG: ${{ needs.autotag.outputs.release_tag }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -eu
TAG="${RELEASE_TAG}"
BODY=$(cat /tmp/release_body.md)
if gh release view "$TAG" >/dev/null 2>&1; then
echo "Updating existing release $TAG..."
gh release edit "$TAG" --notes "$BODY"
echo "✓ Release body updated"
else
echo "Creating release $TAG..."
gh release create "$TAG" \
--title "TFTSR $TAG" \
--notes "$BODY"
echo "✓ Release created"
fi
- name: Commit CHANGELOG.md to main
env:
RELEASE_TAG: ${{ needs.autotag.outputs.release_tag }}
run: |
set -euo pipefail
TAG="${RELEASE_TAG}"
if ! echo "$TAG" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+$'; then
echo "ERROR: Unexpected tag format: $TAG"
exit 1
fi
git add CHANGELOG.md
if git diff --staged --quiet; then
echo "No CHANGELOG.md changes to commit"
else
git commit -m "chore: update CHANGELOG.md for ${TAG} [skip ci]"
git push origin HEAD:main
echo "✓ CHANGELOG.md committed to main"
fi
- name: Upload CHANGELOG.md as release asset
env:
RELEASE_TAG: ${{ needs.autotag.outputs.release_tag }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -eu
TAG="${RELEASE_TAG}"
# Remove existing asset if present to allow re-upload
gh release delete-asset "$TAG" CHANGELOG.md --yes 2>/dev/null || true
gh release upload "$TAG" CHANGELOG.md
echo "✓ CHANGELOG.md uploaded"
echo "Tag $NEXT pushed successfully"
wiki-sync:
runs-on: ubuntu-latest
runs-on: linux-amd64
container:
image: alpine:latest
steps:
- name: Checkout
- name: Install dependencies
run: apk add --no-cache git
- name: Checkout main repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Configure git
run: |
git config --global user.email "github-actions[bot]@users.noreply.github.com"
git config --global user.name "github-actions[bot]"
git config --global user.email "actions@gitea.local"
git config --global user.name "Gitea Actions"
git config --global credential.helper ''
- name: Clone and sync wiki
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
WIKI_TOKEN: ${{ secrets.Wiki }}
run: |
cd /tmp
WIKI_URL="https://x-access-token:${GH_TOKEN}@github.com/msicie/apollo_nxt-trcaa.wiki.git"
if [ -n "$WIKI_TOKEN" ]; then
WIKI_URL="http://${WIKI_TOKEN}@172.0.0.29:3000/sarman/tftsr-devops_investigation.wiki.git"
else
WIKI_URL="http://172.0.0.29:3000/sarman/tftsr-devops_investigation.wiki.git"
fi
if ! git clone "$WIKI_URL" wiki 2>/dev/null; then
echo "Wiki doesn't exist yet, creating initial structure..."
@ -223,10 +115,11 @@ jobs:
git add -A
if ! git diff --staged --quiet; then
git commit -m "docs: sync from docs/wiki/ at commit ${GITHUB_SHA:0:8}"
echo "Pushing to wiki..."
if git push origin master; then
echo "✓ Wiki successfully synced"
else
echo "⚠ Wiki push failed"
echo "⚠ Wiki push failed - check token permissions"
exit 1
fi
else
@ -235,91 +128,102 @@ jobs:
build-linux-amd64:
needs: autotag
runs-on: ubuntu-latest
runs-on: linux-amd64
container:
image: ghcr.io/msicie/trcaa-linux-amd64:rust1.88-node22
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
image: rust:1.88-slim
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Cache cargo registry
uses: actions/cache@v4
with:
path: |
~/.cargo/registry/index
~/.cargo/registry/cache
~/.cargo/git/db
key: ${{ runner.os }}-cargo-linux-amd64-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-linux-amd64-
- name: Cache npm
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-npm-
- 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
env:
APPIMAGE_EXTRACT_AND_RUN: "1"
run: |
npm ci --legacy-peer-deps
rustup target add x86_64-unknown-linux-gnu
CI=true npx tauri build --target x86_64-unknown-linux-gnu
- name: Upload artifacts to GitHub release
- name: Upload artifacts
env:
RELEASE_TAG: ${{ needs.autotag.outputs.release_tag }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
run: |
set -eu
TAG="${RELEASE_TAG}"
API="http://172.0.0.29:3000/api/v1/repos/$GITHUB_REPOSITORY"
TAG=$(curl -s "$API/tags?limit=50" \
-H "Authorization: token $RELEASE_TOKEN" | \
jq -r '.[].name' | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | \
sort -V | tail -1 || true)
if [ -z "$TAG" ]; then
echo "ERROR: Could not resolve release tag from repository tags."
exit 1
fi
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" \))
\( -name "*.deb" -o -name "*.rpm" -o -name "*.AppImage" \))
if [ -z "$ARTIFACTS" ]; then
echo "ERROR: No Linux amd64 artifacts found."
echo "ERROR: No Linux amd64 artifacts were found to upload."
exit 1
fi
printf '%s\n' "$ARTIFACTS" | while IFS= read -r f; do
NAME="linux-amd64-$(basename "$f")"
echo "Uploading $NAME..."
gh release delete-asset "$TAG" "$NAME" --yes 2>/dev/null || true
gh release upload "$TAG" "$f#$NAME"
echo "✓ Uploaded $NAME"
NAME=$(basename "$f")
UPLOAD_NAME="linux-amd64-$NAME"
echo "Uploading $UPLOAD_NAME..."
EXISTING_IDS=$(curl -sf "$API/releases/$RELEASE_ID" \
-H "Authorization: token $RELEASE_TOKEN" \
| jq -r --arg name "$UPLOAD_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=$UPLOAD_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=$UPLOAD_NAME")
if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then
echo "✓ Uploaded $UPLOAD_NAME"
else
echo "✗ Upload failed for $UPLOAD_NAME (HTTP $HTTP_CODE)"
python -c 'import pathlib,sys;print(pathlib.Path(sys.argv[1]).read_text(errors="replace")[:2000])' "$RESP_FILE"
exit 1
fi
done
build-windows-amd64:
needs: autotag
runs-on: ubuntu-latest
runs-on: linux-amd64
container:
image: ghcr.io/msicie/trcaa-windows-cross:rust1.88-node22
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
image: rust:1.88-slim
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Cache cargo registry
uses: actions/cache@v4
with:
path: |
~/.cargo/registry/index
~/.cargo/registry/cache
~/.cargo/git/db
key: ${{ runner.os }}-cargo-windows-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-windows-
- name: Cache npm
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-npm-
- 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
@ -330,26 +234,65 @@ jobs:
OPENSSL_STATIC: "1"
run: |
npm ci --legacy-peer-deps
rustup target add x86_64-pc-windows-gnu
CI=true npx tauri build --target x86_64-pc-windows-gnu
- name: Upload artifacts to GitHub release
- name: Upload artifacts
env:
RELEASE_TAG: ${{ needs.autotag.outputs.release_tag }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
run: |
set -eu
TAG="${RELEASE_TAG}"
API="http://172.0.0.29:3000/api/v1/repos/$GITHUB_REPOSITORY"
TAG=$(curl -s "$API/tags?limit=50" \
-H "Authorization: token $RELEASE_TOKEN" | \
jq -r '.[].name' | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | \
sort -V | tail -1 || true)
if [ -z "$TAG" ]; then
echo "ERROR: Could not resolve release tag from repository tags."
exit 1
fi
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 found."
echo "ERROR: No Windows amd64 artifacts were found to upload."
exit 1
fi
printf '%s\n' "$ARTIFACTS" | while IFS= read -r f; do
NAME="windows-amd64-$(basename "$f")"
NAME=$(basename "$f")
echo "Uploading $NAME..."
gh release delete-asset "$TAG" "$NAME" --yes 2>/dev/null || true
gh release upload "$TAG" "$f#$NAME"
echo "✓ Uploaded $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 -c 'import pathlib,sys;print(pathlib.Path(sys.argv[1]).read_text(errors="replace")[:2000])' "$RESP_FILE"
exit 1
fi
done
build-macos-arm64:
@ -377,101 +320,112 @@ jobs:
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 to GitHub release
- name: Upload artifacts
env:
RELEASE_TAG: ${{ needs.autotag.outputs.release_tag }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
run: |
set -eu
TAG="${RELEASE_TAG}"
API="http://172.0.0.29:3000/api/v1/repos/$GITHUB_REPOSITORY"
TAG=$(curl -s "$API/tags?limit=50" \
-H "Authorization: token $RELEASE_TOKEN" | \
jq -r '.[].name' | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | \
sort -V | tail -1 || true)
if [ -z "$TAG" ]; then
echo "ERROR: Could not resolve release tag from repository tags."
exit 1
fi
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 found."
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="macos-arm64-$(basename "$f")"
NAME=$(basename "$f")
echo "Uploading $NAME..."
gh release delete-asset "$TAG" "$NAME" --yes 2>/dev/null || true
gh release upload "$TAG" "$f#$NAME"
echo "✓ Uploaded $NAME"
done
build-macos-intel:
needs: autotag
runs-on: macos-13
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Build
env:
MACOSX_DEPLOYMENT_TARGET: "10.15"
run: |
npm ci --legacy-peer-deps
rustup target add x86_64-apple-darwin
CI=true npx tauri build --target x86_64-apple-darwin --bundles app
APP=$(find src-tauri/target/x86_64-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/x86_64-apple-darwin/release/bundle/dmg
DMG=src-tauri/target/x86_64-apple-darwin/release/bundle/dmg/${APP_NAME}.dmg
hdiutil create -volname "$APP_NAME" -srcfolder "$APP" -ov -format UDZO "$DMG"
- name: Upload artifacts to GitHub release
env:
RELEASE_TAG: ${{ needs.autotag.outputs.release_tag }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -eu
TAG="${RELEASE_TAG}"
ARTIFACTS=$(find src-tauri/target/x86_64-apple-darwin/release/bundle -type f -name "*.dmg")
if [ -z "$ARTIFACTS" ]; then
echo "ERROR: No macOS Intel DMG artifacts found."
exit 1
fi
printf '%s\n' "$ARTIFACTS" | while IFS= read -r f; do
NAME="macos-intel-$(basename "$f")"
echo "Uploading $NAME..."
gh release delete-asset "$TAG" "$NAME" --yes 2>/dev/null || true
gh release upload "$TAG" "$f#$NAME"
echo "✓ Uploaded $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 -c 'import pathlib,sys;print(pathlib.Path(sys.argv[1]).read_text(errors="replace")[:2000])' "$RESP_FILE"
exit 1
fi
done
build-linux-arm64:
needs: autotag
runs-on: ubuntu-latest
runs-on: linux-amd64
container:
image: ghcr.io/msicie/trcaa-linux-arm64:rust1.88-node22
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
image: ubuntu:22.04
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Cache cargo registry
uses: actions/cache@v4
with:
path: |
~/.cargo/registry/index
~/.cargo/registry/cache
~/.cargo/git/db
key: ${{ runner.os }}-cargo-arm64-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-arm64-
- name: Cache npm
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-npm-
- name: Install dependencies
env:
DEBIAN_FRONTEND: noninteractive
run: |
# Step 1: Host tools + cross-compiler (all amd64, no multiarch yet)
apt-get update -qq
apt-get install -y -qq curl git gcc g++ make patchelf pkg-config perl jq \
gcc-aarch64-linux-gnu g++-aarch64-linux-gnu
# Step 2: Multiarch — Ubuntu uses ports.ubuntu.com for arm64,
# keeping it on a separate mirror from amd64 (archive.ubuntu.com).
# This avoids the binary-all index duplication and -dev package
# conflicts that plagued the Debian single-mirror approach.
dpkg --add-architecture arm64
sed -i 's|^deb http://archive.ubuntu.com|deb [arch=amd64] http://archive.ubuntu.com|g' /etc/apt/sources.list
sed -i 's|^deb http://security.ubuntu.com|deb [arch=amd64] http://security.ubuntu.com|g' /etc/apt/sources.list
printf '%s\n' \
'deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports jammy main restricted universe multiverse' \
'deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports jammy-updates main restricted universe multiverse' \
'deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports jammy-security main restricted universe multiverse' \
> /etc/apt/sources.list.d/arm64-ports.list
apt-get update -qq
# Step 3: ARM64 dev libs — libayatana omitted (no tray icon in this app)
apt-get install -y -qq \
libwebkit2gtk-4.1-dev:arm64 \
libssl-dev:arm64 \
libgtk-3-dev:arm64 \
librsvg2-dev:arm64
# Step 4: Node.js
curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
apt-get install -y nodejs
# Step 5: Rust (not pre-installed in ubuntu:22.04)
# source "$HOME/.cargo/env" in the Build step handles PATH — no GITHUB_PATH needed
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y \
--default-toolchain 1.88.0 --profile minimal --no-modify-path
- name: Build
env:
CC_aarch64_unknown_linux_gnu: aarch64-linux-gnu-gcc
@ -485,25 +439,66 @@ jobs:
OPENSSL_STATIC: "1"
APPIMAGE_EXTRACT_AND_RUN: "1"
run: |
. "$HOME/.cargo/env"
npm ci --legacy-peer-deps
rustup target add aarch64-unknown-linux-gnu
CI=true npx tauri build --target aarch64-unknown-linux-gnu --bundles deb,rpm
- name: Upload artifacts to GitHub release
- name: Upload artifacts
env:
RELEASE_TAG: ${{ needs.autotag.outputs.release_tag }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
run: |
set -eu
TAG="${RELEASE_TAG}"
API="http://172.0.0.29:3000/api/v1/repos/$GITHUB_REPOSITORY"
TAG=$(curl -s "$API/tags?limit=50" \
-H "Authorization: token $RELEASE_TOKEN" | \
jq -r '.[].name' | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | \
sort -V | tail -1 || true)
if [ -z "$TAG" ]; then
echo "ERROR: Could not resolve release tag from repository tags."
exit 1
fi
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-unknown-linux-gnu/release/bundle -type f \
\( -name "*.deb" -o -name "*.rpm" -o -name "*.AppImage" \))
if [ -z "$ARTIFACTS" ]; then
echo "ERROR: No Linux arm64 artifacts found."
echo "ERROR: No Linux arm64 artifacts were found to upload."
exit 1
fi
printf '%s\n' "$ARTIFACTS" | while IFS= read -r f; do
NAME="linux-arm64-$(basename "$f")"
echo "Uploading $NAME..."
gh release delete-asset "$TAG" "$NAME" --yes 2>/dev/null || true
gh release upload "$TAG" "$f#$NAME"
echo "✓ Uploaded $NAME"
NAME=$(basename "$f")
UPLOAD_NAME="linux-arm64-$NAME"
echo "Uploading $UPLOAD_NAME..."
EXISTING_IDS=$(curl -sf "$API/releases/$RELEASE_ID" \
-H "Authorization: token $RELEASE_TOKEN" \
| jq -r --arg name "$UPLOAD_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=$UPLOAD_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=$UPLOAD_NAME")
if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then
echo "✓ Uploaded $UPLOAD_NAME"
else
echo "✗ Upload failed for $UPLOAD_NAME (HTTP $HTTP_CODE)"
python -c 'import pathlib,sys;print(pathlib.Path(sys.argv[1]).read_text(errors="replace")[:2000])' "$RESP_FILE"
exit 1
fi
done

View File

@ -1,53 +1,47 @@
name: Test
on:
push:
branches:
- main
- 'feature/**'
- 'bug/**'
- 'fix/**'
pull_request:
branches:
- main
jobs:
rust-test:
rust-fmt-check:
runs-on: ubuntu-latest
container:
image: ghcr.io/msicie/trcaa-linux-amd64:rust1.88-node22
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
image: rust:1.88-slim
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Cache cargo registry
uses: actions/cache@v4
with:
path: |
~/.cargo/registry/index
~/.cargo/registry/cache
~/.cargo/git/db
key: ${{ runner.os }}-cargo-linux-amd64-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-linux-amd64-
- name: Install npm dependencies
run: npm install --legacy-peer-deps
- name: Update version from Git
run: node scripts/update-version.mjs
- name: Generate lockfile
run: cargo generate-lockfile --manifest-path src-tauri/Cargo.toml
- name: Rust fmt check
run: cargo fmt --manifest-path src-tauri/Cargo.toml --check
- name: Rust clippy
run: cargo clippy --manifest-path src-tauri/Cargo.toml -- -D warnings
- name: Rust tests
run: cargo test --manifest-path src-tauri/Cargo.toml -- --test-threads=1
- run: rustup component add rustfmt
- run: cargo fmt --manifest-path src-tauri/Cargo.toml --check
frontend-test:
rust-clippy:
runs-on: ubuntu-latest
container:
image: rust:1.88-slim
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 1
- 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 perl
- run: rustup component add clippy
- run: cargo clippy --manifest-path src-tauri/Cargo.toml -- -D warnings
rust-tests:
runs-on: ubuntu-latest
container:
image: rust:1.88-slim
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 1
- 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 perl
- run: cargo test --manifest-path src-tauri/Cargo.toml
frontend-typecheck:
runs-on: ubuntu-latest
container:
image: node:22-alpine
@ -56,16 +50,17 @@ jobs:
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Cache npm
uses: actions/cache@v4
- run: npm ci --legacy-peer-deps
- run: npx tsc --noEmit
frontend-tests:
runs-on: ubuntu-latest
container:
image: node:22-alpine
steps:
- name: Checkout
uses: actions/checkout@v4
with:
path: ~/.npm
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-npm-
- name: Install dependencies
run: npm ci --legacy-peer-deps
- name: TypeScript type check
run: npx tsc --noEmit
- name: Run frontend tests
run: npm run test:run
fetch-depth: 1
- run: npm ci --legacy-peer-deps
- run: npm run test:run

View File

@ -1201,7 +1201,7 @@ Update the `bundle.externalBin` array (currently empty at line 42):
#### 5.3 Add to CI/CD Pipeline
**File: `.github/workflows/release.yml`**
**File: `.gitea/workflows/auto-tag.yml`**
Add kubectl download step before build:

View File

@ -81,20 +81,19 @@ TypeScript mirrors this shape exactly in `tauriCommands.ts`.
---
## CI/CD (GitHub Actions)
## CI/CD (Gitea Actions)
| Workflow | Trigger | Jobs |
|----------|---------|------|
| `.github/workflows/test.yml` | Every push/PR targeting `main` | `rust-test` (fmt → clippy → cargo test) · `frontend-test` (tsc → vitest) |
| `.github/workflows/release.yml` | Push to `main` (auto-tag), `v*` tags | Auto-tag, build linux/amd64 + linux/arm64 + windows/amd64 + macOS ARM64 + macOS Intel, upload to GitHub Releases |
| `.github/workflows/build-images.yml` | `.docker/**` changes on `main` | Build and push pre-baked CI images to `ghcr.io/msicie/` |
| `.gitea/workflows/test.yml` | Every push/PR | `rustfmt``clippy``cargo test` (64 tests) → `tsc --noEmit``vitest run` (13 tests) |
| `.gitea/workflows/auto-tag.yml` | Push to master | Auto-tag, build linux/amd64 + windows/amd64 + linux/arm64 + macOS, upload assets to Gitea release |
**Artifacts**: `src-tauri/target/{target}/release/bundle/`
**Environments**:
- Test CI images at `ghcr.io/msicie/` (pull `trcaa-*:rust1.88-node22`)
- GitHub repo: `https://github.com/msicie/apollo_nxt-trcaa`
- Wiki: sync from `docs/wiki/*.md``https://github.com/msicie/apollo_nxt-trcaa/wiki`
- Test CI images at `172.0.0.29:3000` (pull `trcaa-*:rust1.88-node22`)
- Gitea instance: `http://172.0.0.29:3000`
- Wiki: sync from `docs/wiki/*.md``https://gogs.tftsr.com/sarman/tftsr-devops_investigation/wiki`
---
@ -155,4 +154,4 @@ TypeScript mirrors this shape exactly in `tauriCommands.ts`.
3. **PII before AI**: Always redact and record hash before external send
4. **Port 1420**: Vite dev server is hard-coded to 1420, not 3000
5. **Build order**: Rust fmt → clippy → test → TS check → JS test
6. **CI images**: Use `ghcr.io/msicie/` registry for pre-baked builder images
6. **CI images**: Use `172.0.0.29:3000` registry for pre-baked builder images

View File

@ -77,9 +77,8 @@ cargo tauri build # Outputs to src-tauri/target/release/bundle/
### CI/CD
- **Test pipeline**: `.github/workflows/test.yml` — runs on every push/PR targeting `main`
- **Release pipeline**: `.github/workflows/release.yml` — runs on every push to `main`, auto-tags, produces multi-platform bundles (Linux amd64+arm64, Windows, macOS arm64+Intel), uploads to GitHub Releases at `https://github.com/msicie/apollo_nxt-trcaa/releases`
- **Docker builder images**: `.github/workflows/build-images.yml` — rebuilds `ghcr.io/msicie/trcaa-*` images when `.docker/**` changes on `main`
- **Test pipeline**: `.woodpecker/test.yml` — runs on every push/PR
- **Release pipeline**: `.woodpecker/release.yml` — runs on `v*` tags, produces Linux amd64+arm64 bundles, uploads to Gogs release at `http://172.0.0.29:3000/api/v1`
---
@ -163,21 +162,24 @@ On the TypeScript side, `tauriCommands.ts` mirrors this shape exactly.
Before any text is sent to an AI provider, `apply_redactions` must be called and the resulting SHA-256 hash recorded via `audit::log::write_audit_event`.
### GitHub Actions CI
### Woodpecker CI + Gogs Compatibility
All pipelines run on GitHub Actions at `https://github.com/msicie/apollo_nxt-trcaa/actions`.
**Status**: Woodpecker CI v0.15.4 is deployed at `http://172.0.0.29:8084` (direct) and `http://172.0.0.29:8085` (nginx proxy). Webhook delivery from Gogs works, but CI builds are not yet triggering due to hook authentication issues. See `PLAN.md § Phase 11` for full details.
- `GITHUB_TOKEN` is the only credential needed — no external secrets required
- Builder images are hosted on `ghcr.io/msicie/` (GitHub Container Registry)
- Branch protection on `main` requires `rust-test` and `frontend-test` checks to pass, plus Copilot code review, before merging
Known issues with Woodpecker 0.15.4 + Gogs 0.14:
- `token.ParseRequest()` does not read `?token=` URL params (only `Authorization` header and `user_sess` cookie)
- The SPA login form uses `login=` field; Gogs backend reads `username=` — a custom login page is served by nginx at `/login` and `/login/form`
- Gogs 0.14 has no OAuth2 provider support, blocking upgrade to Woodpecker 2.x
Gogs token quirk: the `sha1` value returned by `POST /api/v1/users/{user}/tokens` is the **actual bearer token**. The `sha1` and `sha256` columns in the Gogs DB are hashes of that token, not the token itself.
---
## Wiki Maintenance
The project wiki lives at `https://github.com/msicie/apollo_nxt-trcaa/wiki`.
The project wiki lives at `https://gogs.tftsr.com/sarman/tftsr-devops_investigation/wiki`.
**Source of truth**: `docs/wiki/*.md` in this repo. The `wiki-sync` job (in `.github/workflows/release.yml`) automatically pushes any changes to the GitHub wiki on every push to `main`.
**Source of truth**: `docs/wiki/*.md` in this repo. The `wiki-sync` CI step (in `.woodpecker/test.yml`) automatically pushes any changes to the Gogs wiki on every push to master.
**When making code changes, update the corresponding wiki file in `docs/wiki/` before committing:**
@ -187,7 +189,7 @@ The project wiki lives at `https://github.com/msicie/apollo_nxt-trcaa/wiki`.
| DB schema or migrations (`db/migrations.rs`, `db/models.rs`) | `docs/wiki/Database.md` |
| New/changed AI provider (`ai/*.rs`) | `docs/wiki/AI-Providers.md` |
| PII patterns or detection logic (`pii/`) | `docs/wiki/PII-Detection.md` |
| CI/CD pipeline changes (`.github/workflows/*.yml`) | `docs/wiki/CICD-Pipeline.md` |
| CI/CD pipeline changes (`.woodpecker/*.yml`) | `docs/wiki/CICD-Pipeline.md` |
| Rust architecture or module layout (`lib.rs`, `state.rs`) | `docs/wiki/Architecture.md` |
| Security-relevant changes (capabilities, audit, Stronghold) | `docs/wiki/Security-Model.md` |
| Dev setup, prerequisites, build commands | `docs/wiki/Development-Setup.md` |
@ -196,7 +198,7 @@ The project wiki lives at `https://github.com/msicie/apollo_nxt-trcaa/wiki`.
To manually push wiki changes without waiting for CI:
```bash
cd /tmp/apollo-wiki # local clone of the wiki git repo
cd /tmp/tftsr-wiki # local clone of the wiki git repo
# edit *.md files, then:
git add -A && git commit -m "docs: ..." && git push
```

View File

@ -1,9 +1,10 @@
GH_REPO := msicie/apollo_nxt-trcaa
GOGS_API := http://172.0.0.29:3000/api/v1
GOGS_REPO := sarman/tftsr-devops_investigation
TAG ?= v0.1.0-alpha
TARGET := aarch64-unknown-linux-gnu
# Build linux/arm64 release artifact natively inside a Docker container,
# then upload to the GitHub release for TAG.
# then upload to the Gogs release for TAG.
.PHONY: release-arm64
release-arm64: build-arm64 upload-arm64
@ -34,11 +35,15 @@ build-arm64:
.PHONY: upload-arm64
upload-arm64:
@test -n "$(GH_TOKEN)" || (echo "ERROR: set GH_TOKEN env var"; exit 1)
@for f in artifacts/linux-arm64/*; do \
@test -n "$(GOGS_TOKEN)" || (echo "ERROR: set GOGS_TOKEN env var"; exit 1)
@RELEASE_ID=$$(curl -sf "$(GOGS_API)/repos/$(GOGS_REPO)/releases/tags/$(TAG)" \
-H "Authorization: token $(GOGS_TOKEN)" | \
grep -o '"id":[0-9]*' | head -1 | cut -d: -f2); \
echo "Release ID: $$RELEASE_ID"; \
for f in artifacts/linux-arm64/*; do \
[ -f "$$f" ] || continue; \
NAME="linux-arm64-$$(basename $$f)"; \
echo "Uploading $$NAME..."; \
GH_TOKEN=$(GH_TOKEN) gh release upload $(TAG) "$$f#$$NAME" \
--repo $(GH_REPO) && echo "OK" || echo "FAIL: $$f"; \
echo "Uploading $$f..."; \
curl -sf -X POST "$(GOGS_API)/repos/$(GOGS_REPO)/releases/$$RELEASE_ID/assets" \
-H "Authorization: token $(GOGS_TOKEN)" \
-F "attachment=@$$f;filename=$$(basename $$f)" && echo "OK" || echo "FAIL: $$f"; \
done

13
PLAN.md
View File

@ -33,11 +33,9 @@ produces post-mortem documents (Markdown / PDF / DOCX).
```
tftsr/
├── .github/
│ └── workflows/
│ ├── test.yml # lint + unit tests on push / PR
│ ├── release.yml # multi-platform build on tag
│ └── build-images.yml # pre-baked CI image builds
├── .woodpecker/
│ ├── test.yml # lint + unit tests on push / PR
│ └── release.yml # multi-platform build on tag
├── cli/
│ ├── package.json
│ └── src/
@ -287,9 +285,8 @@ All frontend ↔ backend communication goes through Tauri's `invoke()`.
| Pipeline | Trigger | Steps |
|----------|---------|-------|
| `.github/workflows/test.yml` | push, PR | `rustfmt` check → Clippy → Rust tests → TS typecheck → Vitest |
| `.github/workflows/release.yml` | push to `main` (auto-tag), `v*` tag | Build linux-amd64 → linux-arm64 → windows-amd64 → macOS ARM64 → macOS Intel → Upload to GitHub Releases |
| `.github/workflows/build-images.yml` | `.docker/**` changes on `main` | Build and push pre-baked CI images to `ghcr.io/msicie/` |
| `.woodpecker/test.yml` | push, PR | `rustfmt` check → Clippy → Rust tests → TS typecheck → Vitest → coverage (main only) |
| `.woodpecker/release.yml` | `v*` tag | Build linux-amd64 → Build linux-arm64 → Upload to Gogs release |
---

View File

@ -4,7 +4,7 @@ A structured, AI-backed desktop tool for IT incident triage, 5-Whys root cause a
Built with **Tauri 2** (Rust + WebView), **React 18**, **TypeScript**, and **SQLCipher AES-256** encrypted storage.
**CI status:** ![CI](https://github.com/msicie/apollo_nxt-trcaa/actions/workflows/test.yml/badge.svg) — all checks green (rustfmt · clippy · 64 Rust tests · tsc · vitest)
**CI status:** ![CI](http://172.0.0.29:3000/sarman/tftsr-devops_investigation/actions/workflows/test.yml/badge.svg) — all checks green (rustfmt · clippy · 64 Rust tests · tsc · vitest)
---
@ -90,8 +90,8 @@ node --version # 22+
```bash
# Clone
git clone https://github.com/msicie/apollo_nxt-trcaa.git
cd apollo_nxt-trcaa
git clone https://gogs.tftsr.com/sarman/tftsr-devops_investigation.git
cd tftsr-devops_investigation
npm install --legacy-peer-deps
# Development mode (hot reload)
@ -107,15 +107,14 @@ cargo tauri build
## Releases
Pre-built installers are attached to each [tagged release](https://github.com/msicie/apollo_nxt-trcaa/releases):
Pre-built installers are attached to each [tagged release](https://gogs.tftsr.com/sarman/tftsr-devops_investigation/releases):
| Platform | Format | Notes |
|---|---|---|
| Linux amd64 | `.deb`, `.rpm`, `.AppImage` | Standard package or universal AppImage |
| Windows amd64 | `.exe` (NSIS), `.msi` | From cross-compile via mingw-w64 |
| Linux arm64 | `.deb`, `.rpm`, `.AppImage` | Built natively on arm64 runner |
| macOS ARM64 | `.dmg` | Native build on `macos-latest` |
| macOS Intel | `.dmg` | Native build on `macos-13` |
| macOS | — | Requires macOS runner — build locally |
---
@ -174,7 +173,7 @@ To use Claude via AWS Bedrock (ideal for enterprise environments with existing A
- API Key: `sk-your-secure-key` (from config)
- Model: `bedrock-claude`
For detailed setup including multiple AWS accounts and Claude Code integration, see the [LiteLLM + Bedrock wiki page](https://github.com/msicie/apollo_nxt-trcaa/wiki/LiteLLM-Bedrock-Setup).
For detailed setup including multiple AWS accounts and Claude Code integration, see the [LiteLLM + Bedrock wiki page](https://gogs.tftsr.com/sarman/tftsr-devops_investigation/wiki/LiteLLM-Bedrock-Setup).
---
@ -215,12 +214,11 @@ tftsr/
├── tests/
│ ├── unit/ # Vitest unit tests (PII, session store, settings store)
│ └── e2e/ # WebdriverIO + tauri-driver E2E skeletons
├── docs/wiki/ # Source of truth for GitHub wiki
└── .github/
├── docs/wiki/ # Source of truth for Gitea wiki
└── .gitea/
└── workflows/
├── test.yml # CI: rustfmt · clippy · cargo test · tsc · vitest (every push/PR)
├── release.yml # Auto tag + release: linux/amd64 + linux/arm64 + windows/amd64 + macOS ARM64 + macOS Intel
└── build-images.yml # Build and push pre-baked CI images to ghcr.io
├── test.yml # CI: rustfmt · clippy · cargo test · tsc · vitest (every push/PR)
└── auto-tag.yml # Auto tag + release: linux/amd64 + windows/amd64 + linux/arm64 + macOS
```
---
@ -247,27 +245,25 @@ TAURI_BINARY_PATH=./src-tauri/target/release/tftsr npm run test:e2e
---
## CI/CD — GitHub Actions
## CI/CD — Gitea Actions
The project uses **GitHub Actions** with pre-baked builder images hosted on `ghcr.io/msicie/`.
The project uses **Gitea Actions** (act_runner v0.3.1) connected to the Gitea instance at `gogs.tftsr.com`.
| Workflow | Trigger | Jobs |
|---|---|---|
| `.github/workflows/test.yml` | Every push / PR targeting `main` | `rust-test` (fmt · clippy · cargo test) · `frontend-test` (tsc · vitest) |
| `.github/workflows/release.yml` | Push to `main` (auto-tag), then `v*` tags | Auto-tag, build linux/amd64 + linux/arm64 + windows/amd64 + macOS ARM64 + macOS Intel, upload to GitHub Releases |
| `.github/workflows/build-images.yml` | Changes to `.docker/**` on `main` | Build and push pre-baked CI images to `ghcr.io/msicie/` |
| `.gitea/workflows/test.yml` | Every push / PR | rustfmt · clippy · cargo test (64) · tsc · vitest (13) |
| `.gitea/workflows/auto-tag.yml` | Push to `master` | Auto-tag, then build linux/amd64 + windows/amd64 + linux/arm64 + macOS and upload assets |
**Pre-baked CI images:**
**Runners:**
| Image | Purpose |
|---|---|
| `ghcr.io/msicie/trcaa-linux-amd64:rust1.88-node22` | Test pipeline + linux/amd64 + windows cross-compile |
| `ghcr.io/msicie/trcaa-linux-arm64:rust1.88-node22` | linux/arm64 release builds |
| `ghcr.io/msicie/trcaa-windows-cross:rust1.88-node22` | Windows amd64 cross-compile |
| Runner | Platform | Host | Purpose |
|---|---|---|---|
| `amd64-docker-runner` | linux/amd64 | 172.0.0.29 (Docker) | Test pipeline + amd64/windows release builds |
| `arm64-native-runner` | linux/arm64 | Local arm64 machine | Native arm64 release builds |
**Branch protection:** `main` requires a PR with `rust-test` + `frontend-test` + CODEOWNER review before merge.
**Branch protection:** master requires a PR approved by `sarman`, with all 5 CI checks passing before merge.
> See [CI/CD Pipeline wiki](https://github.com/msicie/apollo_nxt-trcaa/wiki/CICD-Pipeline) for full infrastructure docs.
> See [CI/CD Pipeline wiki](https://gogs.tftsr.com/sarman/tftsr-devops_investigation/wiki/CICD-Pipeline) for full infrastructure docs.
---
@ -325,7 +321,7 @@ Override with the `TFTSR_DATA_DIR` environment variable.
| 8 | RCA & Post-Mortem Generation | ✅ Complete |
| 9 | History & Search | 🔲 Pending |
| 10 | Integrations (Confluence, ServiceNow, ADO) | 🔲 v0.2 |
| 11 | CI/CD Pipeline | ✅ Complete — GitHub Actions, all checks green |
| 12 | Release Packaging | ✅ linux/amd64 · linux/arm64 · windows/amd64 · macOS ARM64 · macOS Intel |
| 11 | CI/CD Pipeline | ✅ Complete — Gitea Actions, all checks green |
| 12 | Release Packaging | ✅ linux/amd64 · linux/arm64 (native) · windows/amd64 |
---

View File

@ -51,9 +51,16 @@ These expose internal service infrastructure to anyone reading the source and in
---
### C3. Private Gogs Server IP in CI Workflows — RESOLVED
### C3. Private Gogs Server IP Exposed in All CI Workflows
**Status**: Resolved. All `.gitea/workflows/` files have been removed. CI/CD has been migrated to GitHub Actions (`.github/workflows/`). Container images are now hosted on `ghcr.io/msicie/` (public). No private IPs or internal infrastructure references remain in CI configuration.
**Files**:
- `.gitea/workflows/test.yml` (lines 17, 44, 72, 99, 126)
- `.gitea/workflows/auto-tag.yml` (lines 31, 52, 79, 95, 97, 141, 162, 227, 252, 313, 338, 401, 464)
- `.gitea/workflows/build-images.yml` (lines 4, 10, 11, 16-18, 33, 46, 69, 92)
**Issue**: All CI workflow files reference `172.0.0.29:3000` (a private Gogs instance) and `sarman` username. While the IP is RFC1918 private address space, it reveals internal infrastructure topology and the developer's username across dozens of lines. The `build-images.yml` also exposes `REGISTRY_USER: sarman` and container registry details.
**Recommended Fix**: Before open-sourcing, replace all workflow files with GitHub Actions equivalents, or at minimum replace the hardcoded private IP and username with parameterized variables or remove the `.gitea/` directory entirely if moving to GitHub.
---
@ -256,9 +263,13 @@ style-src 'self' 'unsafe-inline'
---
### L4. Username in CI Workflows and Makefile — RESOLVED
### L4. Username `sarman` Embedded in CI Workflows and Makefile
**Status**: Resolved. `.gitea/workflows/` removed. GitHub Actions workflows use `${{ github.actor }}` and `ghcr.io/msicie/` (org-scoped). Makefile updated to reference `msicie/apollo_nxt-trcaa` via `gh` CLI.
**Files**: `.gitea/workflows/*.yml`, `Makefile` line 2
**Issue**: The developer's username appears throughout CI configuration. While not a security vulnerability per se, it is a privacy concern for open-source release.
**Recommended Fix**: Parameterize the username in CI workflows. Update the Makefile to use a generic repository reference.
---
@ -306,7 +317,7 @@ The following practices are already well-implemented:
|----------|--------|--------|
| **P0** | Remove `GenAI API User Guide.md` and `HANDOFF-MSI-GENAI.md` from repo and git history | Small |
| **P0** | Remove `commandcentral.com` URLs from CSP and hardcoded MSI headers from `openai.rs` | Small |
| **P0** | ~~Replace or parameterize private IP (`172.0.0.29`) and username in all `.gitea/` workflows~~ **RESOLVED** — migrated to GitHub Actions | — |
| **P0** | Replace or parameterize private IP (`172.0.0.29`) and username in all `.gitea/` workflows | Medium |
| **P1** | Replace hardcoded dev encryption keys with auto-generated per-install keys | Small |
| **P1** | Use proper KDF (PBKDF2/HKDF) for AES key derivation in `auth.rs` | Small |
| **P1** | Auto-generate encryption key for credential storage (mirror `connection.rs` pattern) | Small |

View File

@ -685,39 +685,38 @@ graph LR
```mermaid
graph TB
subgraph "Source Control"
GITHUB[GitHub\ngithub.com/msicie/apollo_nxt-trcaa\nmain branch]
GOGS[Gogs / Gitea\ngogs.tftsr.com\nSarman Repository]
end
subgraph "CI/CD Triggers"
PR_TRIGGER[PR Opened/Updated\ntest.yml workflow]
MAIN_TRIGGER[Push to main\nrelease.yml workflow]
MASTER_TRIGGER[Push to master\nauto-tag.yml workflow]
DOCKER_TRIGGER[.docker/ changes\nbuild-images.yml workflow]
end
subgraph "Test Runner — ubuntu-latest"
subgraph "Test Runner — amd64-docker-runner"
RUSTFMT[1. rustfmt\nFormat Check]
CLIPPY[2. clippy\n-D warnings]
CARGO_TEST[3. cargo test\n64 Rust tests]
TSC[4. tsc --noEmit\nType Check]
VITEST[5. vitest run\nJS tests]
VITEST[5. vitest run\n13 JS tests]
end
subgraph "Release Builders (Parallel)"
AMD64[linux/amd64\nghcr.io/msicie/trcaa-linux-amd64\n.deb .rpm .AppImage]
WINDOWS[windows/amd64\nghcr.io/msicie/trcaa-windows-cross\n.exe .msi]
ARM64[linux/arm64\nghcr.io/msicie/trcaa-linux-arm64\n.deb .rpm .AppImage]
MACOS_ARM[macOS ARM64\nmacos-latest runner\n.dmg]
MACOS_INTEL[macOS Intel\nmacos-13 runner\n.dmg]
AMD64[linux/amd64\nDocker: trcaa-linux-amd64\n.deb .rpm .AppImage]
WINDOWS[windows/amd64\nDocker: trcaa-windows-cross\n.exe .msi]
ARM64[linux/arm64\narm64 native runner\n.deb .rpm .AppImage]
MACOS[macOS arm64\nnative macOS runner\n.app .dmg]
end
subgraph "Artifact Storage"
RELEASE[GitHub Releases\nv0.x.x tags\nAll platform assets]
REGISTRY[ghcr.io/msicie/\nCI Docker images]
RELEASE[Gitea Release\nv0.x.x tags\nAll platform assets]
REGISTRY[Gitea Container Registry\n172.0.0.29:3000\nCI Docker images]
end
GITHUB --> PR_TRIGGER
GITHUB --> MAIN_TRIGGER
GITHUB --> DOCKER_TRIGGER
GOGS --> PR_TRIGGER
GOGS --> MASTER_TRIGGER
GOGS --> DOCKER_TRIGGER
PR_TRIGGER --> RUSTFMT
RUSTFMT --> CLIPPY
@ -725,17 +724,15 @@ graph TB
CARGO_TEST --> TSC
TSC --> VITEST
MAIN_TRIGGER --> AMD64
MAIN_TRIGGER --> WINDOWS
MAIN_TRIGGER --> ARM64
MAIN_TRIGGER --> MACOS_ARM
MAIN_TRIGGER --> MACOS_INTEL
MASTER_TRIGGER --> AMD64
MASTER_TRIGGER --> WINDOWS
MASTER_TRIGGER --> ARM64
MASTER_TRIGGER --> MACOS
AMD64 --> RELEASE
WINDOWS --> RELEASE
ARM64 --> RELEASE
MACOS_ARM --> RELEASE
MACOS_INTEL --> RELEASE
MACOS --> RELEASE
DOCKER_TRIGGER --> REGISTRY

View File

@ -4,39 +4,67 @@
| Component | URL | Notes |
|-----------|-----|-------|
| GitHub | `https://github.com/msicie/apollo_nxt-trcaa` | Git server and CI/CD platform |
| GitHub Actions | Native to GitHub | Hosted runners (`ubuntu-latest`, `macos-latest`, `macos-13`) |
| ghcr.io | `ghcr.io/msicie/` | GitHub Container Registry for pre-baked CI images |
| Gitea | `https://gogs.tftsr.com` / `http://172.0.0.29:3000` | Git server (migrated from Gogs 0.14) |
| Woodpecker CI (direct) | `http://172.0.0.29:8084` | v2.x |
| Woodpecker CI (proxy) | `http://172.0.0.29:8085` | nginx reverse proxy |
| PostgreSQL (Gitea DB) | Container: `gogs_postgres_db` | DB: `gogsdb`, User: `gogs` |
### CI Agents
| Agent | Platform | Host | Purpose |
|-------|----------|------|---------|
| `gitea_act_runner_amd64` (Docker) | `linux-amd64` | 172.0.0.29 | Native x86_64 — test builds + amd64/windows release |
| `act_runner` (systemd) | `linux-arm64` | 172.0.0.29 | Native aarch64 — arm64 release builds |
| `act_runner` (launchd) | `macos-arm64` | sarman's local Mac | Native Apple Silicon — macOS `.dmg` release builds |
Agent labels configured in `~/.config/act_runner/config.yaml`:
```yaml
runner:
labels:
- "macos-arm64:host"
```
macOS runner runs jobs **directly on the host** (no Docker container) — macOS SDK cannot run in Docker.
---
## Pre-baked Builder Images
CI build and test jobs use pre-baked Docker images pushed to `ghcr.io/msicie/`. These images bake in all system dependencies (Tauri libs, Node.js, Rust toolchain, cross-compilers) so that CI jobs skip package installation entirely.
CI build and test jobs use pre-baked Docker images pushed to the local Gitea registry
at `172.0.0.29:3000`. These images bake in all system dependencies (Tauri libs, Node.js,
Rust toolchain, cross-compilers) so that CI jobs skip package installation entirely.
| Image | Used by jobs | Contents |
|-------|-------------|----------|
| `ghcr.io/msicie/trcaa-linux-amd64:rust1.88-node22` | `rust-test`, `build-linux-amd64` | Rust 1.88 + rustfmt + clippy + Tauri amd64 libs + Node.js 22 |
| `ghcr.io/msicie/trcaa-windows-cross:rust1.88-node22` | `build-windows-amd64` | Rust 1.88 + mingw-w64 + NSIS + Node.js 22 |
| `ghcr.io/msicie/trcaa-linux-arm64:rust1.88-node22` | `build-linux-arm64` | Rust 1.88 + aarch64 cross-toolchain + arm64 multiarch libs + Node.js 22 |
| `172.0.0.29:3000/sarman/trcaa-linux-amd64:rust1.88-node22` | `rust-fmt-check`, `rust-clippy`, `rust-tests`, `build-linux-amd64` | Rust 1.88 + rustfmt + clippy + Tauri amd64 libs + Node.js 22 |
| `172.0.0.29:3000/sarman/trcaa-windows-cross:rust1.88-node22` | `build-windows-amd64` | Rust 1.88 + mingw-w64 + NSIS + Node.js 22 |
| `172.0.0.29:3000/sarman/trcaa-linux-arm64:rust1.88-node22` | `build-linux-arm64` | Rust 1.88 + aarch64 cross-toolchain + arm64 multiarch libs + Node.js 22 |
**Rebuild triggers:** Rust toolchain version bump, webkit2gtk/gtk major version change, Node.js major version change.
**How to rebuild images:**
1. Trigger `build-images.yml` via Actions → Build CI Docker Images → Run workflow
2. Confirm all 3 images appear in `ghcr.io/msicie/`
1. Trigger `build-images.yml` via `workflow_dispatch` in the Gitea Actions UI
2. Confirm all 3 images appear in the Gitea package/container registry at `172.0.0.29:3000`
3. Only then merge workflow changes that depend on the new image contents
**Server prerequisite — insecure registry** (one-time, on 172.0.0.29):
```sh
echo '{"insecure-registries":["172.0.0.29:3000"]}' | sudo tee /etc/docker/daemon.json
sudo systemctl restart docker
```
This must be configured on every machine running an act_runner for the runner's Docker
daemon to pull from the local HTTP registry.
---
## Cargo and npm Caching
All Rust and build jobs use `actions/cache@v4` to cache downloaded package artifacts.
All Rust and build jobs use `actions/cache@v3` to cache downloaded package artifacts.
Gitea 1.22 implements the GitHub Actions cache API natively.
**Cargo cache** (Rust jobs):
```yaml
- name: Cache cargo registry
uses: actions/cache@v4
uses: actions/cache@v3
with:
path: |
~/.cargo/registry/index
@ -50,7 +78,7 @@ All Rust and build jobs use `actions/cache@v4` to cache downloaded package artif
**npm cache** (frontend and build jobs):
```yaml
- name: Cache npm
uses: actions/cache@v4
uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
@ -64,108 +92,168 @@ Cache keys for cross-compile jobs use a suffix to avoid collisions:
---
## Test Pipeline (`.github/workflows/test.yml`)
## Test Pipeline (`.gitea/workflows/test.yml`)
**Triggers:** Every push to `main`, `feature/**`, `bug/**`, `fix/**` branches; pull requests targeting `main`.
**Triggers:** Pull requests only.
```
Jobs (run in parallel):
rust-test → cargo fmt --check
→ cargo clippy -- -D warnings
→ cargo test (64 tests)
frontend-test → npx tsc --noEmit
→ npm run test:run (Vitest tests)
Pipeline jobs (run in parallel):
1. rust-fmt-check → cargo fmt --check
2. rust-clippy → cargo clippy -- -D warnings
3. rust-tests → cargo test (64 tests)
4. frontend-typecheck → npx tsc --noEmit
5. frontend-tests → npm run test:run (13 Vitest tests)
```
**Docker image used:**
- `ghcr.io/msicie/trcaa-linux-amd64:rust1.88-node22` — both Rust and frontend steps
Job names `rust-test` and `frontend-test` must match exactly — these are the required status check names in branch protection on `main`.
**Docker images used:**
- `172.0.0.29:3000/sarman/trcaa-linux-amd64:rust1.88-node22` — Rust steps (replaces `rust:1.88-slim`)
- `node:22-alpine` — Frontend steps
---
## Release Pipeline (`.github/workflows/release.yml`)
## Release Pipeline (`.gitea/workflows/auto-tag.yml`)
**Triggers:** Push to `main` (auto-tag job), then release build/upload jobs run after `autotag` completes on `v*` tags.
**Triggers:** Pushes to `master` (auto-tag), then release build/upload jobs run after `autotag`.
Auto-tags are created by the `autotag` job using `git tag` + `git push`. Release jobs depend on `autotag` and run in parallel.
Auto tags are created by `.gitea/workflows/auto-tag.yml` using `git tag` + `git push`.
Release jobs are executed in the same workflow and depend on `autotag` completion.
```
Jobs (run in parallel after autotag):
build-linux-amd64 → image: ghcr.io/msicie/trcaa-linux-amd64:rust1.88-node22
→ cargo tauri build (x86_64-unknown-linux-gnu)
→ {.deb, .rpm, .AppImage} uploaded to GitHub Release
→ fails fast if no Linux artifacts are produced
build-linux-amd64 → image: trcaa-linux-amd64:rust1.88-node22
→ 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 → image: trcaa-windows-cross:rust1.88-node22
→ 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 → image: trcaa-linux-arm64:rust1.88-node22 (ubuntu:22.04-based)
→ 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
```
build-windows-amd64 → image: ghcr.io/msicie/trcaa-windows-cross:rust1.88-node22
→ cargo tauri build (x86_64-pc-windows-gnu) via mingw-w64
→ {.exe, .msi} uploaded to GitHub Release
→ fails fast if no Windows artifacts are produced
**Per-step agent routing (Woodpecker 2.x labels):**
build-linux-arm64 → image: ghcr.io/msicie/trcaa-linux-arm64:rust1.88-node22
→ cargo tauri build (aarch64-unknown-linux-gnu)
→ {.deb, .rpm, .AppImage} uploaded to GitHub Release
→ fails fast if no Linux artifacts are produced
```yaml
steps:
- name: build-linux-amd64
labels:
platform: linux/amd64 # → woodpecker_agent on 172.0.0.29
build-macos-arm64 → runs-on: macos-latest (Apple Silicon)
→ cargo tauri build (aarch64-apple-darwin) natively
→ {.dmg} uploaded to GitHub Release
→ existing same-name assets are deleted before upload (rerun-safe)
→ unsigned; after install run: xattr -cr /Applications/TRCAA.app
- name: build-linux-arm64
labels:
platform: linux/arm64 # → woodpecker-agent.service on local arm64 machine
```
build-macos-intel → runs-on: macos-13 (Intel x86_64)
→ cargo tauri build (x86_64-apple-darwin) natively
→ {.dmg} uploaded to GitHub Release
**Multi-agent workspace isolation:**
Steps routed to different agents do **not** share a workspace. The arm64 step clones
the repo directly within its commands (using `http://172.0.0.29:3000`, accessible from
the local machine) and uploads its artifacts inline. The `upload-release` step (amd64)
handles amd64 + windows artifacts only.
**Clone override (auto-tag.yml — amd64 workspace):**
```yaml
clone:
git:
image: alpine/git
network_mode: gogs_default
commands:
- git init -b master
- git remote add origin http://gitea_app:3000/sarman/tftsr-devops_investigation.git
- git fetch --depth=1 origin +refs/tags/${CI_COMMIT_TAG}:refs/tags/${CI_COMMIT_TAG}
- git checkout ${CI_COMMIT_TAG}
```
**Windows cross-compile environment:**
```yaml
env:
environment:
TARGET: x86_64-pc-windows-gnu
CC_x86_64_pc_windows_gnu: x86_64-w64-mingw32-gcc
CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER: x86_64-w64-mingw32-gcc
OPENSSL_NO_VENDOR: "0"
OPENSSL_STATIC: "1"
```
**Artifacts per platform:**
- Linux amd64: `.deb`, `.rpm`, `.AppImage`
- Windows amd64: `.exe` (NSIS installer), `.msi`
- Linux arm64: `.deb`, `.rpm`, `.AppImage`
- macOS ARM64: `.dmg`
- macOS Intel: `.dmg`
**Upload step (requires gogs_default network for amd64, host IP for arm64):**
```yaml
# amd64 upload step
upload-release:
image: curlimages/curl:latest
labels:
platform: linux/amd64
network_mode: gogs_default
secrets: [GOGS_TOKEN]
```
The `GOGS_TOKEN` Woodpecker secret must be created via the Woodpecker UI or API after
migration. The secret name stays `GOGS_TOKEN` for pipeline compatibility.
**Gitea Release API (replaces Gogs API — same endpoints, different container name):**
```bash
# Create release
POST http://gitea_app:3000/api/v1/repos/sarman/tftsr-devops_investigation/releases
Authorization: token $GOGS_TOKEN
# Upload artifact
POST http://gitea_app:3000/api/v1/repos/sarman/tftsr-devops_investigation/releases/{id}/assets
```
From the arm64 agent (local machine), use `http://172.0.0.29:3000/api/v1` instead.
---
## Build Images Workflow (`.github/workflows/build-images.yml`)
## Multi-File Pipeline Support (Woodpecker 2.x)
**Triggers:** Changes to `.docker/**` on `main`; manual `workflow_dispatch`.
Woodpecker 2.x supports multiple pipeline files in the `.woodpecker/` directory.
All `.yml` files are evaluated on every trigger; `when:` conditions control which
pipelines actually run.
Builds and pushes all three pre-baked CI images to `ghcr.io/msicie/`:
```
Jobs (all run on ubuntu-latest):
build-linux-amd64-image → .docker/Dockerfile.linux-amd64
→ ghcr.io/msicie/trcaa-linux-amd64:rust1.88-node22
Current files:
- `.woodpecker/test.yml` — runs on every push/PR
- `.woodpecker/release.yml` — runs on `v*` tags only
build-windows-cross-image → .docker/Dockerfile.windows-cross
→ ghcr.io/msicie/trcaa-windows-cross:rust1.88-node22
No DB config path switching needed (unlike Woodpecker 0.15.4).
build-linux-arm64-image → .docker/Dockerfile.linux-arm64
→ ghcr.io/msicie/trcaa-linux-arm64:rust1.88-node22
```
---
Authentication: `docker login ghcr.io -u ${{ github.actor }} --password-stdin <<< "${{ secrets.GITHUB_TOKEN }}"`. Requires `packages: write` permission.
## Webhook Configuration
**Woodpecker 2.x with Gitea OAuth2:**
After migration, Woodpecker 2.x registers webhooks automatically when a repo is
activated via the UI. No manual JWT-signed webhook setup required.
1. Log in at `http://172.0.0.29:8085` via Gitea OAuth2
2. Add repo `sarman/tftsr-devops_investigation`
3. Woodpecker creates webhook in Gitea automatically
---
## Branch Protection
`main` branch requires:
- All changes via PR
- 1 approving review
- CODEOWNER review (`@Shaun-Arman-VFK387_moto` and `@github-copilot`)
- Required status checks: `rust-test`, `frontend-test`
- `enforce_admins: false` — owner and admins can bypass with `gh pr merge --admin`
Master branch is protected: all changes require a PR.
```sql
-- Gitea branch protection (via psql on gogs_postgres_db container)
-- Check protection
SELECT name, protected, require_pull_request FROM protect_branch WHERE repo_id=42;
-- Temporarily disable for urgent fixes (restore immediately after!)
UPDATE protect_branch SET protected=false WHERE repo_id=42 AND name='master';
-- ... push ...
UPDATE protect_branch SET protected=true, require_pull_request=true WHERE repo_id=42 AND name='master';
```
---
@ -176,7 +264,7 @@ Configuration lives in `cliff.toml` at the repo root.
### How it works
A `changelog` job in `release.yml` runs in parallel with the build jobs, immediately
A `changelog` job in `auto-tag.yml` runs in parallel with the build jobs, immediately
after `autotag` completes:
1. Clones the full repo history with all tags (`--depth=2147483647` — git-cliff needs
@ -184,9 +272,9 @@ after `autotag` completes:
2. Downloads the git-cliff v2.7.0 static musl binary (~5 MB, no image change needed).
3. Runs `git-cliff --output CHANGELOG.md` to regenerate the full cumulative changelog.
4. Runs `git-cliff --latest --strip all` to produce release notes for the new tag only.
5. Updates the GitHub Release body with those notes via `gh release edit`.
6. Commits `CHANGELOG.md` to `main` with `[skip ci]` appended to the message.
The `[skip ci]` token prevents `release.yml` from re-triggering on the CHANGELOG commit.
5. PATCHes the Gitea release body with those notes (replaces the static `"Release vX.Y.Z"`).
6. Commits `CHANGELOG.md` to master with `[skip ci]` appended to the message.
The `[skip ci]` token prevents `auto-tag.yml` from re-triggering on the CHANGELOG commit.
7. Uploads `CHANGELOG.md` as a release asset (replaces any previous version).
### cliff.toml reference
@ -201,9 +289,17 @@ after `autotag` completes:
### Loop prevention
The `[skip ci]` suffix on the CHANGELOG commit message is recognised by GitHub Actions
The `[skip ci]` suffix on the CHANGELOG commit message is recognised by Gitea Actions
and causes the workflow to be skipped for that push. Without it, the CHANGELOG commit
would trigger `release.yml` again, incrementing the patch version forever.
would trigger `auto-tag.yml` again, incrementing the patch version forever.
### Bootstrap
The initial `CHANGELOG.md` was generated locally before the first PR:
```sh
git-cliff --config cliff.toml --output CHANGELOG.md
```
Subsequent runs are fully automated by CI.
---
@ -216,16 +312,27 @@ index is duplicated and certain `-dev` package pairs cannot be co-installed beca
don't declare `Multi-Arch: same`. This produces `E: Unable to correct problems, you have
held broken packages` and cannot be fixed by tweaking `sources.list` entries.
**Fix**: Use `ubuntu:22.04` as the container image (see `Dockerfile.linux-arm64`). Ubuntu
routes arm64 through `ports.ubuntu.com/ubuntu-ports` — a separate mirror from
`archive.ubuntu.com` (amd64). There are no cross-arch index overlaps and the dependency
resolver succeeds. Rust must be installed manually via `rustup`.
**Fix**: Use `ubuntu:22.04` as the container image. Ubuntu routes arm64 through
`ports.ubuntu.com/ubuntu-ports` — a separate mirror from `archive.ubuntu.com` (amd64).
There are no cross-arch index overlaps and the dependency resolver succeeds. Rust must be
installed manually via `rustup` since it is not pre-installed in the Ubuntu base image.
### `CI=true` Required by Tauri CLI
GitHub Actions sets `CI=true` by default — `cargo tauri build` accepts this without
modification. Prefix the command explicitly if your runner environment is unusual:
```yaml
- run: CI=true cargo tauri build --target $TARGET
### Step Containers Cannot Reach `gitea_app`
Default Docker bridge containers cannot resolve `gitea_app` or reach `172.0.0.29:3000`
(host firewall). Fix: use `network_mode: gogs_default` in any step that needs Gitea
access. Requires `repo_trusted=1`.
### `CI=woodpecker` Rejected by Tauri CLI
Woodpecker sets `CI=woodpecker`; `cargo tauri build` expects a boolean. Fix: prefix with
`CI=true cargo tauri build`.
### Agent Stalls After Server Restart
After restarting the Woodpecker server, the agent may enter a loop cleaning up orphaned
containers and stop picking up new builds. Fix:
```bash
docker rm -f $(docker ps -aq --filter 'name=0_')
docker volume rm $(docker volume ls -q | grep '0_')
docker restart woodpecker_agent
```
### Windows DLL Export Ordinal Too Large
@ -237,17 +344,32 @@ Fix: `src-tauri/.cargo/config.toml`:
rustflags = ["-C", "link-arg=-Wl,--exclude-all-symbols"]
```
### Release Artifacts Not Uploaded
### GOGS_TOKEN Secret Must Be Recreated After Migration
After migrating from Woodpecker 0.15.4 to 2.x, recreate the `GOGS_TOKEN` secret:
1. Log in to Gitea, create a new API token under Settings → Applications
2. In Woodpecker UI → Repository → Secrets, add secret `GOGS_TOKEN` with the token value
**Cause:** `GITHUB_TOKEN` needs `contents: write` permission in the workflow.
---
Verify the `release.yml` permissions block includes:
```yaml
permissions:
contents: write
## Gitea PostgreSQL Access
```bash
docker exec gogs_postgres_db psql -U gogs -d gogsdb -c "SELECT id, lower_name FROM repository;"
```
### Runner Group Restrictions (Enterprise)
GitHub Enterprise organizations may restrict which repositories can use GitHub-hosted
runners. If jobs are stuck in queue, contact your org admin to add the repository to the
allowed runner group, or temporarily make the repo public for the bootstrap run.
> Database name is `gogsdb` (unchanged from Gogs migration).
---
## Migration Notes (Gogs 0.14 → Gitea)
Gitea auto-migrates the Gogs PostgreSQL schema on first start. Users, repos, teams, and
issues are preserved. API tokens stored in the DB are also migrated but should be
regenerated for security.
Key changes after migration:
- Container name: `gogs_app``gitea_app`
- Config dir: `/data/gitea` (was `/data/gogs` inside container, same host volume)
- Repo dir: `gogs-repositories``gitea-repositories` (renamed on host during migration)
- OAuth2 provider: Gitea now supports OAuth2 (Woodpecker 2.x uses this for login)
- Woodpecker 2.x multi-file pipeline support enabled (no more single config file limitation)

View File

@ -2,7 +2,7 @@
**Troubleshooting and RCA Assistant** is a secure desktop application for guided IT incident triage, root cause analysis (RCA), and post-mortem documentation. Built with Tauri 2.x (Rust + WebView) and React 18.
**CI:** ![build](https://github.com/msicie/apollo_nxt-trcaa/actions/workflows/test.yml/badge.svg) — rustfmt · clippy · 64 Rust tests · tsc · vitest — all green
**CI:** ![build](http://172.0.0.29:3000/sarman/tftsr-devops_investigation/actions/workflows/test.yml/badge.svg) — rustfmt · clippy · 64 Rust tests · tsc · vitest — all green
## Quick Navigation
@ -15,7 +15,7 @@
| [LiteLLM + Bedrock Setup](wiki/LiteLLM-Bedrock-Setup) | AWS Bedrock integration via LiteLLM proxy |
| [PII Detection](wiki/PII-Detection) | Patterns, redaction flow, security |
| [IPC Commands](wiki/IPC-Commands) | Full list of Tauri backend commands |
| [CI/CD Pipeline](wiki/CICD-Pipeline) | GitHub Actions setup, multi-platform builds, pre-baked ghcr.io images |
| [CI/CD Pipeline](wiki/CICD-Pipeline) | Gitea Actions setup, multi-platform builds, act_runner config |
| [Security Model](wiki/Security-Model) | Encryption, audit trail, capabilities |
| [Integrations](wiki/Integrations) | Confluence, ServiceNow, Azure DevOps (v0.2) |
| [Troubleshooting](wiki/Troubleshooting) | Known issues and fixes |
@ -45,7 +45,7 @@
**Platforms:** linux/amd64 · linux/arm64 · windows/amd64 (.deb, .rpm, .AppImage, .exe, .msi)
Download from [Releases](https://github.com/msicie/apollo_nxt-trcaa/releases). All builds are produced natively (no QEMU emulation).
Download from [Releases](https://gogs.tftsr.com/sarman/tftsr-devops_investigation/releases). All builds are produced natively (no QEMU emulation).
## Project Status
@ -54,8 +54,8 @@ Download from [Releases](https://github.com/msicie/apollo_nxt-trcaa/releases). A
| Phases 18 (Core application) | ✅ Complete |
| Phase 9 (History/Search) | 🔲 Pending |
| Phase 10 (Integrations) | ✅ Complete — Confluence, ServiceNow, Azure DevOps fully implemented with OAuth2 |
| Phase 11 (CI/CD) | ✅ Complete — GitHub Actions fully operational |
| Phase 12 (Release packaging) | ✅ linux/amd64 · linux/arm64 · windows/amd64 · macOS ARM64 · macOS Intel |
| Phase 11 (CI/CD) | ✅ Complete — Gitea Actions fully operational |
| Phase 12 (Release packaging) | ✅ linux/amd64 · linux/arm64 (native) · windows/amd64 |
## Tech Stack
@ -69,4 +69,4 @@ Download from [Releases](https://github.com/msicie/apollo_nxt-trcaa/releases). A
| Secret storage | tauri-plugin-stronghold |
| State | Zustand |
| Testing | Vitest (13 frontend) + `#[cfg(test)]` (64 Rust tests) |
| CI/CD | GitHub Actions + ghcr.io |
| CI/CD | Gitea Actions (act_runner v0.3.1) + Gitea |

View File

@ -1,41 +1,71 @@
# Troubleshooting
## CI/CD — GitHub Actions
## CI/CD — Gitea Actions
### Build Not Triggering After Push
**Check:**
1. Verify the workflow file exists in `.github/workflows/` on the pushed branch
2. Check the Actions tab at `https://github.com/msicie/apollo_nxt-trcaa/actions`
3. Confirm the workflow trigger matches the branch (e.g., `push: branches: [main, 'bug/**']`)
1. Verify the workflow file exists in `.gitea/workflows/` on the pushed branch
2. Check the Actions tab at `http://172.0.0.29:3000/sarman/tftsr-devops_investigation/actions`
3. Confirm the act_runner is online: `docker logs gitea_act_runner_amd64 --since 5m`
---
### Jobs Stuck in Queue
### Job Container Can't Reach Gitea (`172.0.0.29:3000` blocked)
**Cause:** GitHub Enterprise organizations may restrict which repositories can use
GitHub-hosted runners via runner group policies.
**Cause:** act_runner creates an isolated Docker network per job (when `container:` is specified). Traffic from the job container to `172.0.0.29:3000` is blocked by the host firewall.
**Fix:** Contact your org admin to add the repository to the allowed runner group, or
temporarily make the repo public for a one-time bootstrap run (e.g., to build the
initial ghcr.io images).
**Fix:** Ensure `container.network: host` is set in the act_runner config AND that `CONFIG_FILE=/data/config.yaml` is in the container's environment:
```yaml
# /docker_mounts/gitea/runner/amd64/config.yaml
container:
network: "host"
```
```yaml
# docker-compose.yml for act-runner-amd64
environment:
- CONFIG_FILE=/data/config.yaml
```
Also set `capacity: 1` — with capacity > 1, concurrent jobs may not get host networking:
```yaml
runner:
capacity: 1
```
Restart runner: `docker restart gitea_act_runner_amd64`
---
### `manifest unknown` — Image Pull Fails
### `Unable to locate package git` in Rust Job
**Cause:** The pre-baked CI image (`ghcr.io/msicie/trcaa-linux-amd64:rust1.88-node22`)
doesn't exist yet. This happens before the first `build-images.yml` run.
**Cause:** `rust:1.88-slim` has an empty apt package cache.
**Fix:**
1. Go to Actions → Build CI Docker Images → Run workflow
2. Wait for all 3 image builds to complete
3. Confirm images appear at `https://github.com/orgs/msicie/packages`
4. Re-run the failing workflow
**Fix:** Always run `apt-get update` before `apt-get install`:
```yaml
- 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
```
---
### Job Skipped on Tag Push
### `exec: "node": executable file not found in $PATH`
**Cause:** `actions/checkout@v4` is a Node.js action. `rust:1.88-slim` and similar slim images don't have Node.
**Fix:** Don't use `actions/checkout@v4` — use direct git commands instead (see above).
---
### Job Skipped (status 6) on Tag Push
**Cause:** Pattern matching issue with `on: push: tags:`. Use unquoted glob in the workflow:
@ -62,18 +92,30 @@ on:
---
### `CI=woodpecker` Rejected by `cargo tauri build`
**Cause:** CI runners set `CI=woodpecker` (string). Tauri CLI expects `true`/`false`.
**Fix:** Prefix the build command:
```yaml
- run: CI=true cargo tauri build --target $TARGET
```
---
### Release Artifacts Not Uploaded
**Cause:** `GITHUB_TOKEN` permissions insufficient. The `release.yml` workflow requires
`contents: write` to create releases and upload assets.
Verify the permissions block:
```yaml
permissions:
contents: write
packages: write
**Cause 1:** `RELEASE_TOKEN` secret not set or expired.
```bash
# Recreate via admin CLI:
docker exec -u git gitea_app gitea admin user generate-access-token \
--username sarman --token-name gitea-ci-token --raw \
--scopes 'write:repository,read:user'
# Add the token as RELEASE_TOKEN in repo Settings > Actions > Secrets
```
**Cause 2:** Each build job uploads its own artifacts independently. All jobs require host network on the runner (see above).
---
## Rust Compilation
@ -116,7 +158,7 @@ Auto-fix: `cargo clippy --manifest-path src-tauri/Cargo.toml --fix --allow-dirty
**Fix (Fedora/RHEL):**
```bash
sudo dnf install -y glib2-devel gtk3-devel webkit2gtk4.1-devel \
libsoup3-devel openssl-devel librsvg2-dev
libsoup3-devel openssl-devel librsvg2-devel
```
**Fix (Debian/Ubuntu):**
@ -127,17 +169,6 @@ sudo apt-get install -y libwebkit2gtk-4.1-dev libssl-dev libgtk-3-dev \
---
### Windows Cross-Compile: DLL Export Ordinal Too Large
`/usr/bin/x86_64-w64-mingw32-ld: error: export ordinal too large: 106290`
Fix: `src-tauri/.cargo/config.toml`:
```toml
[target.x86_64-pc-windows-gnu]
rustflags = ["-C", "link-arg=-Wl,--exclude-all-symbols"]
```
---
## Database
### DB Won't Open in Production
@ -189,3 +220,27 @@ const title = detail.title;
Common causes:
- Mocked `invoke()` return type doesn't match updated command signature
- `sessionStore` state not reset between tests (call `store.reset()` in `beforeEach`)
---
## Gitea
### API Token Authentication
```bash
curl -H "Authorization: token <token_value>" http://172.0.0.29:3000/api/v1/user
```
Create tokens in Gitea Settings > Applications > Access Tokens, or via admin CLI:
```bash
docker exec -u git gitea_app gitea admin user generate-access-token \
--username sarman --token-name mytoken --raw --scopes 'read:user,write:repository'
```
### PostgreSQL Access
```bash
docker exec gogs_postgres_db psql -U gogs -d gogsdb -c "SELECT id, lower_name, is_private FROM repository;"
```
Database is named `gogsdb`. The PostgreSQL instance uses SCRAM-SHA-256 auth (MD5 also configured for the `gogs` user for compatibility with older clients).

View File

@ -4,11 +4,11 @@ import path from "node:path";
const autoTagWorkflowPath = path.resolve(
process.cwd(),
".github/workflows/release.yml",
".gitea/workflows/auto-tag.yml",
);
describe("auto-tag workflow release triggering", () => {
it("creates tags via git push instead of API call", () => {
it("creates tags via git push instead of Gitea tag API", () => {
const workflow = readFileSync(autoTagWorkflowPath, "utf-8");
expect(workflow).toContain("git push origin \"refs/tags/$NEXT\"");
@ -23,13 +23,7 @@ describe("auto-tag workflow release triggering", () => {
expect(workflow).toContain("build-macos-arm64:");
expect(workflow).toContain("build-linux-arm64:");
expect(workflow).toContain("needs: autotag");
expect(workflow).toContain("git tag --sort=-version:refname");
});
it("also includes macOS Intel build job", () => {
const workflow = readFileSync(autoTagWorkflowPath, "utf-8");
expect(workflow).toContain("build-macos-intel:");
expect(workflow).toContain("macos-13");
expect(workflow).toContain("TAG=$(curl -s \"$API/tags?limit=50\"");
expect(workflow).toContain("ERROR: Could not resolve release tag from repository tags.");
});
});

View File

@ -100,10 +100,10 @@ describe("Dockerfile.linux-arm64", () => {
// ─── build-images.yml workflow ───────────────────────────────────────────────
describe("build-images.yml workflow", () => {
const wf = readFile(".github/workflows/build-images.yml");
const wf = readFile(".gitea/workflows/build-images.yml");
it("triggers on changes to .docker/ files on main", () => {
expect(wf).toContain("- main");
it("triggers on changes to .docker/ files on master", () => {
expect(wf).toContain("- master");
expect(wf).toContain("- '.docker/**'");
});
@ -111,28 +111,38 @@ describe("build-images.yml workflow", () => {
expect(wf).toContain("workflow_dispatch:");
});
it("authenticates to ghcr.io before pushing", () => {
expect(wf).toContain("docker login ghcr.io");
it("does not explicitly mount the Docker socket (act_runner mounts it automatically)", () => {
// act_runner already mounts /var/run/docker.sock; an explicit options: mount
// causes a 'Duplicate mount point' error and must not be present.
expect(wf).not.toContain("-v /var/run/docker.sock:/var/run/docker.sock");
});
it("authenticates to the local Gitea registry before pushing", () => {
expect(wf).toContain("docker login");
expect(wf).toContain("--password-stdin");
expect(wf).toContain("ghcr.io");
expect(wf).toContain("172.0.0.29:3000");
});
it("builds and pushes all three platform images to ghcr.io", () => {
expect(wf).toContain("ghcr.io/msicie/trcaa-linux-amd64:rust1.88-node22");
expect(wf).toContain("ghcr.io/msicie/trcaa-windows-cross:rust1.88-node22");
expect(wf).toContain("ghcr.io/msicie/trcaa-linux-arm64:rust1.88-node22");
it("builds and pushes all three platform images", () => {
expect(wf).toContain("trcaa-linux-amd64:rust1.88-node22");
expect(wf).toContain("trcaa-windows-cross:rust1.88-node22");
expect(wf).toContain("trcaa-linux-arm64:rust1.88-node22");
});
it("runs all three build jobs on ubuntu-latest runner", () => {
const matches = wf.match(/runs-on: ubuntu-latest/g) ?? [];
it("uses alpine:latest with docker-cli (not docker:24-cli which triggers duplicate socket mount in act_runner)", () => {
// act_runner v0.3.1 special-cases docker:* images and adds the socket bind;
// combined with its global socket bind this causes a 'Duplicate mount point' error.
expect(wf).toContain("alpine:latest");
expect(wf).toContain("docker-cli");
expect(wf).not.toContain("docker:24-cli");
});
it("runs all three build jobs on linux-amd64 runner", () => {
const matches = wf.match(/runs-on: linux-amd64/g) ?? [];
expect(matches.length).toBeGreaterThanOrEqual(3);
});
it("uses GITHUB_TOKEN for registry auth", () => {
expect(wf).toContain("secrets.GITHUB_TOKEN");
});
it("grants packages write permission", () => {
expect(wf).toContain("packages: write");
it("uses RELEASE_TOKEN secret for registry auth", () => {
expect(wf).toContain("secrets.RELEASE_TOKEN");
});
});

View File

@ -4,7 +4,7 @@ import path from "node:path";
const autoTagWorkflowPath = path.resolve(
process.cwd(),
".github/workflows/release.yml",
".gitea/workflows/auto-tag.yml",
);
describe("auto-tag release cross-platform artifact handling", () => {
@ -18,8 +18,8 @@ describe("auto-tag release cross-platform artifact handling", () => {
it("fails linux uploads when no artifacts are found", () => {
const workflow = readFileSync(autoTagWorkflowPath, "utf-8");
expect(workflow).toContain("ERROR: No Linux amd64 artifacts found.");
expect(workflow).toContain("ERROR: No Linux arm64 artifacts found.");
expect(workflow).toContain("ERROR: No Linux amd64 artifacts were found to upload.");
expect(workflow).toContain("ERROR: No Linux arm64 artifacts were found to upload.");
expect(workflow).toContain("CI=true npx tauri build");
expect(workflow).toContain("find src-tauri/target/aarch64-unknown-linux-gnu/release/bundle -type f");
expect(workflow).toContain("CC_aarch64_unknown_linux_gnu: aarch64-linux-gnu-gcc");
@ -30,16 +30,18 @@ describe("auto-tag release cross-platform artifact handling", () => {
it("fails windows uploads when no artifacts are found", () => {
const workflow = readFileSync(autoTagWorkflowPath, "utf-8");
expect(workflow).toContain("ERROR: No Windows amd64 artifacts found.");
expect(workflow).toContain(
"ERROR: No Windows amd64 artifacts were found to upload.",
);
});
it("replaces existing release assets before uploading reruns", () => {
const workflow = readFileSync(autoTagWorkflowPath, "utf-8");
expect(workflow).toContain("gh release delete-asset");
expect(workflow).toContain("gh release upload");
expect(workflow).toContain("linux-amd64-$(basename");
expect(workflow).toContain("linux-arm64-$(basename");
expect(workflow).toContain("Deleting existing asset id=$id name=$NAME before upload...");
expect(workflow).toContain("-X DELETE \"$API/releases/$RELEASE_ID/assets/$id\"");
expect(workflow).toContain("UPLOAD_NAME=\"linux-amd64-$NAME\"");
expect(workflow).toContain("UPLOAD_NAME=\"linux-arm64-$NAME\"");
});
it("uses pre-baked Ubuntu 22.04 cross-compiler image for arm64", () => {

View File

@ -4,7 +4,7 @@ import path from "node:path";
const autoTagWorkflowPath = path.resolve(
process.cwd(),
".github/workflows/release.yml",
".gitea/workflows/auto-tag.yml",
);
describe("auto-tag release macOS bundle path", () => {

View File

@ -1,52 +0,0 @@
# Ticket Summary — ADO #727547
## Description
Migrate the `tftsr-devops_investigation` repository from `gogs.tftsr.com/sarman/tftsr-devops_investigation` to `github.com/msicie/apollo_nxt-trcaa`. All CI/CD pipelines, container registries, and PR review tooling must be rebuilt from scratch targeting GitHub. The self-hosted qwen3-coder-next LLM reviewer is replaced by native GitHub Copilot code review. Branch protection on `main` must require passing CI checks and allow owner/CODEOWNERS to bypass.
## Acceptance Criteria
- [x] Repository exists at `github.com/msicie/apollo_nxt-trcaa` with `main` as the default branch
- [x] `.github/workflows/test.yml` runs Rust and frontend tests on every push/PR targeting `main`
- [x] `.github/workflows/release.yml` auto-tags, builds for Linux amd64+arm64, Windows amd64, macOS ARM64+Intel, and uploads artifacts to GitHub Releases
- [x] `.github/workflows/build-images.yml` builds and pushes pre-baked CI images to `ghcr.io/msicie/`
- [x] All Gogs/Gitea pipeline references (`.gitea/workflows/`, `172.0.0.29:3000`, `RELEASE_TOKEN`) removed
- [x] PR review uses native GitHub Copilot (no external LLM service required)
- [x] `.github/CODEOWNERS` requires `@Shaun-Arman-VFK387_moto` and `@github-copilot` on all PRs
- [x] `main` branch protection: requires `rust-test`, `frontend-test`, CODEOWNER review; admins can bypass
- [x] Wiki sync targets `github.com/msicie/apollo_nxt-trcaa/wiki`
- [ ] One-time: trigger `build-images.yml` to bootstrap `ghcr.io/msicie/` images
- [ ] One-time: enable Copilot Code Review in `msicie` org settings
## Work Implemented
| File | Change |
|---|---|
| `.github/workflows/test.yml` | Full rewrite — port from `.gitea/workflows/test.yml` (Gitea); jobs renamed to `rust-test` + `frontend-test` to match branch protection check names; uses `ghcr.io/msicie/trcaa-linux-amd64:rust1.88-node22` |
| `.github/workflows/release.yml` | Full rewrite — port from `.gitea/workflows/auto-tag.yml`; GITHUB_TOKEN replaces RELEASE_TOKEN; all Gogs API calls replaced with `gh` CLI; adds `build-macos-intel` job (`macos-13`); `master` refs → `main` |
| `.github/workflows/build-images.yml` | Rewrite — login to `ghcr.io` with GITHUB_TOKEN; push images to `ghcr.io/msicie/trcaa-*` |
| `.github/workflows/pr-review.yml` | **Deleted** — replaced by native GitHub Copilot |
| `.gitea/workflows/` | **Deleted** entire directory |
| `.github/CODEOWNERS` | **Created**`@Shaun-Arman-VFK387_moto @github-copilot` on all paths |
| `Makefile` | Replace `GOGS_API`/`GOGS_REPO`/`GOGS_TOKEN` with `GH_REPO`/`GH_TOKEN` and `gh release upload` |
| `CLAUDE.md` | Update wiki URL, CI/CD section, branch references (`master` → `main`) |
**Branch protection on `main`:**
- Require PRs before merging
- Require 1 approving review
- Require CODEOWNER review (`require_code_owner_reviews: true`)
- Required status checks: `rust-test`, `frontend-test`
- `enforce_admins: false` — owner and admins can bypass
**PR:** https://github.com/msicie/apollo_nxt-trcaa/pull/1
## Testing Needed
1. **Bootstrap Docker images** (one-time): Go to Actions → Build CI Docker Images → Run workflow. Verify all three images appear at `ghcr.io/msicie/`.
2. **Enable Copilot Code Review** (one-time manual): `msicie` org Settings → Copilot → Code Review → Enable.
3. **Test pipeline**: Verify `rust-test` and `frontend-test` checks appear and pass on PR #1.
4. **Copilot review**: After enabling, open a new PR and confirm `@github-copilot` is auto-requested.
5. **Branch protection enforcement**: Attempt to merge a PR with a failing check — confirm it is blocked.
6. **Owner bypass**: Confirm `@Shaun-Arman-VFK387_moto` can override protection and merge.
7. **Release pipeline**: Push a `v*` tag (e.g. `v0.3.10`) and confirm all 5 platform jobs complete and artifacts appear in GitHub Releases.
8. **Wiki sync**: Confirm release workflow wiki-sync job pushes content to `github.com/msicie/apollo_nxt-trcaa/wiki`.

View File

@ -16,17 +16,17 @@ Conventional Commits throughout but the information was never surfaced to end-us
- `cliff.toml` (new) — git-cliff configuration; defines commit parsers, ignored tags,
output template, and which commit types appear in the changelog
- `CHANGELOG.md` (new) — bootstrapped from all existing tags; maintained by CI going forward
- `.github/workflows/release.yml` — `changelog` job that runs after `autotag`
- `.gitea/workflows/auto-tag.yml` — new `changelog` job that runs after `autotag`
- `docs/wiki/CICD-Pipeline.md` — "Changelog Generation" section added
## Acceptance Criteria
- [ ] `cliff.toml` present at repo root with working Tera template
- [ ] `CHANGELOG.md` present at repo root, bootstrapped from all existing semver tags
- [ ] `changelog` job in `release.yml` runs after `autotag` (parallel with build jobs)
- [ ] Each GitHub Release body shows grouped conventional-commit entries instead of
- [ ] `changelog` job in `auto-tag.yml` runs after `autotag` (parallel with build jobs)
- [ ] Each Gitea release body shows grouped conventional-commit entries instead of
static `"Release vX.Y.Z"`
- [ ] `CHANGELOG.md` committed to `main` on every release with `[skip ci]` suffix
- [ ] `CHANGELOG.md` committed to master on every release with `[skip ci]` suffix
(no infinite re-trigger loop)
- [ ] `CHANGELOG.md` uploaded as a downloadable release asset
- [ ] CI/chore/build/test/style commits excluded from changelog output
@ -47,15 +47,15 @@ Conventional Commits throughout but the information was never surfaced to end-us
- Covers all tagged versions from `v0.1.0` through `v0.2.49` plus `[Unreleased]`
- 267 lines covering the full project history
### `.github/workflows/release.yml` — `changelog` job
### `.gitea/workflows/auto-tag.yml` — `changelog` job
- `needs: autotag` — waits for the new tag to exist before running
- Full history clone: `git fetch --tags --depth=2147483647` so git-cliff can resolve
all version boundaries
- git-cliff v2.7.0 downloaded as a static x86_64 musl binary (~5 MB); no custom
image required
- Generates full `CHANGELOG.md` and per-release notes (`--latest --strip all`)
- Updates the GitHub Release body via `gh release edit` with JSON-safe escaping (`jq -Rs .`)
- Commits `CHANGELOG.md` to `main` with `[skip ci]` to prevent workflow re-trigger
- PATCHes the Gitea release body via API with JSON-safe escaping (`jq -Rs .`)
- Commits `CHANGELOG.md` to master with `[skip ci]` to prevent workflow re-trigger
- Deletes any existing `CHANGELOG.md` asset before re-uploading (rerun-safe)
- Runs in parallel with all build jobs — no added wall-clock latency

View File

@ -7,7 +7,7 @@ on each job invocation: `apt-get update`, Tauri system libs, Node.js via nodesou
the arm64 job — a full `rustup` install. This was the primary cause of slow builds.
The repository already contains pre-baked builder Docker images (`.docker/Dockerfile.*`) and a
`build-images.yml` workflow to push them to `ghcr.io/msicie/`.
`build-images.yml` workflow to push them to the local Gitea registry at `172.0.0.29:3000`.
These images were never referenced by the actual CI jobs — a critical gap. This work closes
that gap and adds `actions/cache@v3` for Cargo and npm.
@ -15,7 +15,7 @@ that gap and adds `actions/cache@v3` for Cargo and npm.
- [ ] `Dockerfile.linux-amd64` includes `rustfmt` and `clippy` components
- [ ] `Dockerfile.linux-arm64` includes `rustfmt` and `clippy` components
- [ ] `test.yml` Rust jobs use `ghcr.io/msicie/trcaa-linux-amd64:rust1.88-node22`
- [ ] `test.yml` Rust jobs use `172.0.0.29:3000/sarman/trcaa-linux-amd64:rust1.88-node22`
- [ ] `test.yml` Rust jobs have no inline `apt-get` or `rustup component add` steps
- [ ] `test.yml` Rust jobs include `actions/cache@v3` for `~/.cargo/registry`
- [ ] `test.yml` frontend jobs include `actions/cache@v3` for `~/.npm`
@ -38,9 +38,9 @@ in the image rather than installing them at job runtime.
Added `&& /root/.cargo/bin/rustup component add rustfmt clippy` appended to the
existing `rustup` installation RUN command (chained with `&&` to keep it one layer).
### `.github/workflows/test.yml`
- **rust-test** (fmt + clippy + tests): switched container image from
`rust:1.88-slim``ghcr.io/msicie/trcaa-linux-amd64:rust1.88-node22`.
### `.gitea/workflows/test.yml`
- **rust-fmt-check**, **rust-clippy**, **rust-tests**: switched container image from
`rust:1.88-slim``172.0.0.29:3000/sarman/trcaa-linux-amd64:rust1.88-node22`.
Removed `apt-get install git` from Checkout steps (git is pre-installed in image).
Removed `apt-get install libwebkit2gtk-...` steps.
Removed `rustup component add rustfmt` and `rustup component add clippy` steps.
@ -49,15 +49,15 @@ existing `rustup` installation RUN command (chained with `&&` to keep it one lay
- **frontend-typecheck**, **frontend-tests**: kept `node:22-alpine` image (no change needed).
Added `actions/cache@v3` step for `~/.npm` keyed on `package-lock.json` hash.
### `.github/workflows/release.yml`
- **build-linux-amd64**: image `rust:1.88-slim``ghcr.io/msicie/trcaa-linux-amd64:rust1.88-node22`.
### `.gitea/workflows/auto-tag.yml`
- **build-linux-amd64**: image `rust:1.88-slim``trcaa-linux-amd64:rust1.88-node22`.
Removed Checkout apt-get install git, removed entire Install dependencies step.
Removed `rustup target add x86_64-unknown-linux-gnu` from Build step. Added cargo + npm cache.
- **build-windows-amd64**: image `rust:1.88-slim``ghcr.io/msicie/trcaa-windows-cross:rust1.88-node22`.
- **build-windows-amd64**: image `rust:1.88-slim``trcaa-windows-cross:rust1.88-node22`.
Removed Checkout apt-get install git, removed entire Install dependencies step.
Removed `rustup target add x86_64-pc-windows-gnu` from Build step.
Added cargo (with `-windows-` suffix key to avoid collision) + npm cache.
- **build-linux-arm64**: image `ubuntu:22.04``ghcr.io/msicie/trcaa-linux-arm64:rust1.88-node22`.
- **build-linux-arm64**: image `ubuntu:22.04``trcaa-linux-arm64:rust1.88-node22`.
Removed Checkout apt-get install git, removed entire Install dependencies step (~40 lines).
Removed `. "$HOME/.cargo/env"` (PATH already set via `ENV` in Dockerfile).
Removed `rustup target add aarch64-unknown-linux-gnu` from Build step.
@ -67,7 +67,7 @@ existing `rustup` installation RUN command (chained with `&&` to keep it one lay
Added two new sections before the Test Pipeline section:
- **Pre-baked Builder Images**: table of all three images and their contents, rebuild
triggers, how-to-rebuild instructions, and the insecure-registries Docker daemon
prerequisite for ghcr.io access.
prerequisite for 172.0.0.29.
- **Cargo and npm Caching**: documents the `actions/cache@v3` key patterns in use,
including the per-platform cache key suffixes for cross-compile jobs.
Updated the Test Pipeline section to reference the correct pre-baked image name.
@ -75,16 +75,21 @@ Updated the Release Pipeline job table to show which image each build job uses.
## Testing Needed
1. **Pre-build images** (prerequisite): Trigger `build-images.yml` via Actions → Build CI Docker Images → Run workflow. Confirm all 3 images are pushed and visible at `ghcr.io/msicie/`.
1. **Pre-build images** (prerequisite): Trigger `build-images.yml` via `workflow_dispatch`
on Gitea Actions UI. Confirm all 3 images are pushed and visible in the registry.
2. **PR test suite**: Open a PR with these changes. Verify:
- `rust-test` and `frontend-test` jobs pass
2. **Server prerequisite**: Confirm `/etc/docker/daemon.json` on `172.0.0.29` contains
`{"insecure-registries":["172.0.0.29:3000"]}` and Docker was restarted after.
3. **PR test suite**: Open a PR with these changes. Verify:
- All 5 test jobs pass (`rust-fmt-check`, `rust-clippy`, `rust-tests`,
`frontend-typecheck`, `frontend-tests`)
- Job logs show no `apt-get` or `rustup component add` output
- Cache hit messages appear on second run
3. **Release build**: Merge to `main`. Verify `release.yml` runs and:
- All build jobs start without Install dependencies step
- Artifacts are produced and uploaded to the GitHub Release
4. **Release build**: Merge to master. Verify `auto-tag.yml` runs and:
- All 3 Linux/Windows build jobs start without Install dependencies step
- Artifacts are produced and uploaded to the Gitea release
- Total release time is significantly reduced (~7 min vs ~25 min before)
5. **Expected time savings after caching warms up**: