Compare commits

...

3 Commits

Author SHA1 Message Date
Shaun Arman
d5e180740e docs: remove all Gitea/Gogs/172.0.0.29 references; update to GitHub
Some checks failed
Test / rust-test (push) Failing after 6s
Test / frontend-test (push) Failing after 54s
Replace every remaining reference to the old Gitea infrastructure with the
new GitHub-hosted equivalents across all documentation, wiki pages, test
files, and historical ticket summaries.

- README.md: CI badge, clone URL, releases link, CI/CD section, project structure
- docs/wiki/CICD-Pipeline.md: full rewrite for GitHub Actions + ghcr.io
- docs/wiki/Home.md: CI badge, releases link, phase status, tech stack
- docs/wiki/Troubleshooting.md: rewrite CI troubleshooting for GitHub Actions
- docs/architecture/README.md: update CI/CD pipeline diagram
- AGENTS.md: CI/CD section, environment references
- PLAN.md: directory structure, pipeline table
- SECURITY_AUDIT.md: mark C3 and L4 findings as resolved
- ticket-git-cliff-changelog.md: workflow path updated
- tickets/ci-runner-speed-optimization.md: image registry updated
- 2026-hackathon_AgenticFeature.md: workflow path updated
- tests: workflow path assertions updated in all three test files
2026-06-01 16:18:34 -05:00
Shaun Arman
3ce51c4cbc test: update release workflow tests to reference .github/workflows/release.yml 2026-06-01 14:14:00 -05:00
Shaun Arman
1023d7a944 ci: migrate CI/CD from Gogs/Gitea to GitHub Actions
- Replace all .gitea/workflows with GitHub Actions equivalents
- test.yml: port full Gitea pipeline (rust-test + frontend-test jobs)
  using ghcr.io/msicie/trcaa-linux-amd64:rust1.88-node22; triggers on
  main and feature/bug/fix branches plus PRs targeting main
- release.yml: port auto-tag pipeline; switch to GITHUB_TOKEN + gh CLI
  for tagging, changelog, and artifact uploads; add macos-13 Intel build
  job alongside macos-latest ARM64; replace wiki sync to point at GitHub
  wiki; all master refs updated to main
- build-images.yml: switch registry from local Gitea to ghcr.io/msicie,
  login with GITHUB_TOKEN
- Delete pr-review.yml (qwen3-coder-next replaced by native Copilot review)
- Add .github/CODEOWNERS with @Shaun-Arman-VFK387_moto + @github-copilot
- Update Makefile: replace Gogs API/repo refs with gh CLI for uploads
- Update CLAUDE.md: wiki URL, CI/CD section, branch refs (master→main)
2026-06-01 14:03:33 -05:00
26 changed files with 737 additions and 2182 deletions

View File

@ -1,687 +0,0 @@
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

@ -1,104 +0,0 @@
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

@ -1,333 +0,0 @@
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

View File

@ -1,186 +0,0 @@
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 Normal file
View File

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

View File

@ -1,43 +1,68 @@
name: Auto Tag name: Release
# Runs on every merge to master — reads the latest semver tag, increments # Runs on every merge to main — reads the latest semver tag, increments
# the patch version, pushes a new tag, then runs release builds in this workflow. # the patch version, pushes a new tag, generates a changelog, then builds
# workflow_dispatch allows manual triggering when Gitea drops a push event. # multi-platform release artifacts and uploads them to GitHub Releases.
# workflow_dispatch allows manual triggering.
on: on:
push: push:
branches: branches:
- master - main
workflow_dispatch: workflow_dispatch:
concurrency: concurrency:
group: auto-tag-master group: release-main
cancel-in-progress: false cancel-in-progress: false
permissions:
contents: write
packages: read
jobs: jobs:
autotag: autotag:
runs-on: linux-amd64 runs-on: ubuntu-latest
container: outputs:
image: alpine:latest release_tag: ${{ steps.bump.outputs.release_tag }}
steps: 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 - name: Bump patch version and create tag
id: bump id: bump
env:
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
run: | run: |
set -eu set -eu
apk add --no-cache curl jq git
API="http://172.0.0.29:3000/api/v1/repos/$GITHUB_REPOSITORY" # 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) # Get the latest clean semver tag (vX.Y.Z only)
LATEST=$(curl -s "$API/tags?limit=50" \ LATEST=$(git tag --sort=-version:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -1 || echo "")
-H "Authorization: token $RELEASE_TOKEN" | \ echo "Latest git tag: ${LATEST:-none}"
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 if [ -z "$LATEST" ]; then
NEXT="v0.1.0" 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
else else
MAJOR=$(echo "$LATEST" | cut -d. -f1 | tr -d 'v') MAJOR=$(echo "$LATEST" | cut -d. -f1 | tr -d 'v')
MINOR=$(echo "$LATEST" | cut -d. -f2) MINOR=$(echo "$LATEST" | cut -d. -f2)
@ -47,53 +72,136 @@ jobs:
echo "Latest tag: ${LATEST:-none} → Next: $NEXT" 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 if git ls-remote --exit-code --tags origin "refs/tags/$NEXT" >/dev/null 2>&1; then
echo "Tag $NEXT already exists; skipping." echo "Tag $NEXT already exists; builds will target this tag."
exit 0 else
git tag -a "$NEXT" -m "Release $NEXT"
git push origin "refs/tags/$NEXT"
echo "Tag $NEXT pushed successfully"
fi fi
git tag -a "$NEXT" -m "Release $NEXT" echo "release_tag=$NEXT" >> "$GITHUB_OUTPUT"
git push origin "refs/tags/$NEXT"
echo "Tag $NEXT pushed successfully" 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"
wiki-sync: wiki-sync:
runs-on: linux-amd64 runs-on: ubuntu-latest
container:
image: alpine:latest
steps: steps:
- name: Install dependencies - name: Checkout
run: apk add --no-cache git
- name: Checkout main repository
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
fetch-depth: 1 fetch-depth: 1
- name: Configure git - name: Configure git
run: | run: |
git config --global user.email "actions@gitea.local" git config --global user.email "github-actions[bot]@users.noreply.github.com"
git config --global user.name "Gitea Actions" git config --global user.name "github-actions[bot]"
git config --global credential.helper ''
- name: Clone and sync wiki - name: Clone and sync wiki
env: env:
WIKI_TOKEN: ${{ secrets.Wiki }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: | run: |
cd /tmp cd /tmp
if [ -n "$WIKI_TOKEN" ]; then WIKI_URL="https://x-access-token:${GH_TOKEN}@github.com/msicie/apollo_nxt-trcaa.wiki.git"
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 if ! git clone "$WIKI_URL" wiki 2>/dev/null; then
echo "Wiki doesn't exist yet, creating initial structure..." echo "Wiki doesn't exist yet, creating initial structure..."
@ -115,11 +223,10 @@ jobs:
git add -A git add -A
if ! git diff --staged --quiet; then if ! git diff --staged --quiet; then
git commit -m "docs: sync from docs/wiki/ at commit ${GITHUB_SHA:0:8}" git commit -m "docs: sync from docs/wiki/ at commit ${GITHUB_SHA:0:8}"
echo "Pushing to wiki..."
if git push origin master; then if git push origin master; then
echo "✓ Wiki successfully synced" echo "✓ Wiki successfully synced"
else else
echo "⚠ Wiki push failed - check token permissions" echo "⚠ Wiki push failed"
exit 1 exit 1
fi fi
else else
@ -128,102 +235,91 @@ jobs:
build-linux-amd64: build-linux-amd64:
needs: autotag needs: autotag
runs-on: linux-amd64 runs-on: ubuntu-latest
container: container:
image: rust:1.88-slim image: ghcr.io/msicie/trcaa-linux-amd64:rust1.88-node22
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
fetch-depth: 1 fetch-depth: 1
- name: Install dependencies - name: Cache cargo registry
run: | uses: actions/cache@v4
apt-get update -qq && apt-get install -y -qq \ with:
libwebkit2gtk-4.1-dev libssl-dev libgtk-3-dev \ path: |
libayatana-appindicator3-dev librsvg2-dev patchelf \ ~/.cargo/registry/index
pkg-config curl perl jq ~/.cargo/registry/cache
curl -fsSL https://deb.nodesource.com/setup_22.x | bash - ~/.cargo/git/db
apt-get install -y nodejs 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 - name: Build
env:
APPIMAGE_EXTRACT_AND_RUN: "1"
run: | run: |
npm ci --legacy-peer-deps npm ci --legacy-peer-deps
rustup target add x86_64-unknown-linux-gnu
CI=true npx tauri build --target x86_64-unknown-linux-gnu CI=true npx tauri build --target x86_64-unknown-linux-gnu
- name: Upload artifacts - name: Upload artifacts to GitHub release
env: env:
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }} RELEASE_TAG: ${{ needs.autotag.outputs.release_tag }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: | run: |
set -eu set -eu
API="http://172.0.0.29:3000/api/v1/repos/$GITHUB_REPOSITORY" TAG="${RELEASE_TAG}"
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 \ ARTIFACTS=$(find src-tauri/target/x86_64-unknown-linux-gnu/release/bundle -type f \
\( -name "*.deb" -o -name "*.rpm" -o -name "*.AppImage" \)) \( -name "*.deb" -o -name "*.rpm" \))
if [ -z "$ARTIFACTS" ]; then if [ -z "$ARTIFACTS" ]; then
echo "ERROR: No Linux amd64 artifacts were found to upload." echo "ERROR: No Linux amd64 artifacts found."
exit 1 exit 1
fi fi
printf '%s\n' "$ARTIFACTS" | while IFS= read -r f; do printf '%s\n' "$ARTIFACTS" | while IFS= read -r f; do
NAME=$(basename "$f") NAME="linux-amd64-$(basename "$f")"
UPLOAD_NAME="linux-amd64-$NAME" echo "Uploading $NAME..."
echo "Uploading $UPLOAD_NAME..." gh release delete-asset "$TAG" "$NAME" --yes 2>/dev/null || true
EXISTING_IDS=$(curl -sf "$API/releases/$RELEASE_ID" \ gh release upload "$TAG" "$f#$NAME"
-H "Authorization: token $RELEASE_TOKEN" \ echo "✓ Uploaded $NAME"
| 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 done
build-windows-amd64: build-windows-amd64:
needs: autotag needs: autotag
runs-on: linux-amd64 runs-on: ubuntu-latest
container: container:
image: rust:1.88-slim image: ghcr.io/msicie/trcaa-windows-cross:rust1.88-node22
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
fetch-depth: 1 fetch-depth: 1
- name: Install dependencies - name: Cache cargo registry
run: | uses: actions/cache@v4
apt-get update -qq && apt-get install -y -qq mingw-w64 curl nsis perl make jq with:
curl -fsSL https://deb.nodesource.com/setup_22.x | bash - path: |
apt-get install -y nodejs ~/.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 - name: Build
env: env:
CC_x86_64_pc_windows_gnu: x86_64-w64-mingw32-gcc CC_x86_64_pc_windows_gnu: x86_64-w64-mingw32-gcc
@ -234,65 +330,26 @@ jobs:
OPENSSL_STATIC: "1" OPENSSL_STATIC: "1"
run: | run: |
npm ci --legacy-peer-deps npm ci --legacy-peer-deps
rustup target add x86_64-pc-windows-gnu
CI=true npx tauri build --target x86_64-pc-windows-gnu CI=true npx tauri build --target x86_64-pc-windows-gnu
- name: Upload artifacts - name: Upload artifacts to GitHub release
env: env:
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }} RELEASE_TAG: ${{ needs.autotag.outputs.release_tag }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: | run: |
set -eu set -eu
API="http://172.0.0.29:3000/api/v1/repos/$GITHUB_REPOSITORY" TAG="${RELEASE_TAG}"
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 \ ARTIFACTS=$(find src-tauri/target/x86_64-pc-windows-gnu/release/bundle -type f \
\( -name "*.exe" -o -name "*.msi" \) 2>/dev/null) \( -name "*.exe" -o -name "*.msi" \) 2>/dev/null)
if [ -z "$ARTIFACTS" ]; then if [ -z "$ARTIFACTS" ]; then
echo "ERROR: No Windows amd64 artifacts were found to upload." echo "ERROR: No Windows amd64 artifacts found."
exit 1 exit 1
fi fi
printf '%s\n' "$ARTIFACTS" | while IFS= read -r f; do printf '%s\n' "$ARTIFACTS" | while IFS= read -r f; do
NAME=$(basename "$f") NAME="windows-amd64-$(basename "$f")"
echo "Uploading $NAME..." echo "Uploading $NAME..."
EXISTING_IDS=$(curl -sf "$API/releases/$RELEASE_ID" \ gh release delete-asset "$TAG" "$NAME" --yes 2>/dev/null || true
-H "Authorization: token $RELEASE_TOKEN" \ gh release upload "$TAG" "$f#$NAME"
| jq -r --arg name "$NAME" '.assets[]? | select(.name == $name) | .id') echo "✓ Uploaded $NAME"
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 done
build-macos-arm64: build-macos-arm64:
@ -320,112 +377,101 @@ jobs:
mkdir -p src-tauri/target/aarch64-apple-darwin/release/bundle/dmg mkdir -p src-tauri/target/aarch64-apple-darwin/release/bundle/dmg
DMG=src-tauri/target/aarch64-apple-darwin/release/bundle/dmg/${APP_NAME}.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" hdiutil create -volname "$APP_NAME" -srcfolder "$APP" -ov -format UDZO "$DMG"
- name: Upload artifacts - name: Upload artifacts to GitHub release
env: env:
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }} RELEASE_TAG: ${{ needs.autotag.outputs.release_tag }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: | run: |
set -eu set -eu
API="http://172.0.0.29:3000/api/v1/repos/$GITHUB_REPOSITORY" TAG="${RELEASE_TAG}"
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") ARTIFACTS=$(find src-tauri/target/aarch64-apple-darwin/release/bundle -type f -name "*.dmg")
if [ -z "$ARTIFACTS" ]; then if [ -z "$ARTIFACTS" ]; then
echo "ERROR: No macOS arm64 DMG artifacts were found to upload." echo "ERROR: No macOS arm64 DMG artifacts found."
exit 1 exit 1
fi fi
printf '%s\n' "$ARTIFACTS" | while IFS= read -r f; do printf '%s\n' "$ARTIFACTS" | while IFS= read -r f; do
NAME=$(basename "$f") NAME="macos-arm64-$(basename "$f")"
echo "Uploading $NAME..." echo "Uploading $NAME..."
EXISTING_IDS=$(curl -sf "$API/releases/$RELEASE_ID" \ gh release delete-asset "$TAG" "$NAME" --yes 2>/dev/null || true
-H "Authorization: token $RELEASE_TOKEN" \ gh release upload "$TAG" "$f#$NAME"
| jq -r --arg name "$NAME" '.assets[]? | select(.name == $name) | .id') echo "✓ Uploaded $NAME"
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 done
build-linux-arm64: build-macos-intel:
needs: autotag needs: autotag
runs-on: linux-amd64 runs-on: macos-13
container:
image: ubuntu:22.04
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
fetch-depth: 1 fetch-depth: 1
- name: Install dependencies - name: Build
env: env:
DEBIAN_FRONTEND: noninteractive MACOSX_DEPLOYMENT_TARGET: "10.15"
run: | run: |
# Step 1: Host tools + cross-compiler (all amd64, no multiarch yet) npm ci --legacy-peer-deps
apt-get update -qq rustup target add x86_64-apple-darwin
apt-get install -y -qq curl git gcc g++ make patchelf pkg-config perl jq \ CI=true npx tauri build --target x86_64-apple-darwin --bundles app
gcc-aarch64-linux-gnu g++-aarch64-linux-gnu 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"
done
# Step 2: Multiarch — Ubuntu uses ports.ubuntu.com for arm64, build-linux-arm64:
# keeping it on a separate mirror from amd64 (archive.ubuntu.com). needs: autotag
# This avoids the binary-all index duplication and -dev package runs-on: ubuntu-latest
# conflicts that plagued the Debian single-mirror approach. container:
dpkg --add-architecture arm64 image: ghcr.io/msicie/trcaa-linux-arm64:rust1.88-node22
sed -i 's|^deb http://archive.ubuntu.com|deb [arch=amd64] http://archive.ubuntu.com|g' /etc/apt/sources.list credentials:
sed -i 's|^deb http://security.ubuntu.com|deb [arch=amd64] http://security.ubuntu.com|g' /etc/apt/sources.list username: ${{ github.actor }}
printf '%s\n' \ password: ${{ secrets.GITHUB_TOKEN }}
'deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports jammy main restricted universe multiverse' \ steps:
'deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports jammy-updates main restricted universe multiverse' \ - name: Checkout
'deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports jammy-security main restricted universe multiverse' \ uses: actions/checkout@v4
> /etc/apt/sources.list.d/arm64-ports.list with:
apt-get update -qq fetch-depth: 1
- name: Cache cargo registry
# Step 3: ARM64 dev libs — libayatana omitted (no tray icon in this app) uses: actions/cache@v4
apt-get install -y -qq \ with:
libwebkit2gtk-4.1-dev:arm64 \ path: |
libssl-dev:arm64 \ ~/.cargo/registry/index
libgtk-3-dev:arm64 \ ~/.cargo/registry/cache
librsvg2-dev:arm64 ~/.cargo/git/db
key: ${{ runner.os }}-cargo-arm64-${{ hashFiles('**/Cargo.lock') }}
# Step 4: Node.js restore-keys: |
curl -fsSL https://deb.nodesource.com/setup_22.x | bash - ${{ runner.os }}-cargo-arm64-
apt-get install -y nodejs - name: Cache npm
uses: actions/cache@v4
# Step 5: Rust (not pre-installed in ubuntu:22.04) with:
# source "$HOME/.cargo/env" in the Build step handles PATH — no GITHUB_PATH needed path: ~/.npm
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y \ key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
--default-toolchain 1.88.0 --profile minimal --no-modify-path restore-keys: |
${{ runner.os }}-npm-
- name: Build - name: Build
env: env:
CC_aarch64_unknown_linux_gnu: aarch64-linux-gnu-gcc CC_aarch64_unknown_linux_gnu: aarch64-linux-gnu-gcc
@ -439,66 +485,25 @@ jobs:
OPENSSL_STATIC: "1" OPENSSL_STATIC: "1"
APPIMAGE_EXTRACT_AND_RUN: "1" APPIMAGE_EXTRACT_AND_RUN: "1"
run: | run: |
. "$HOME/.cargo/env"
npm ci --legacy-peer-deps 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 CI=true npx tauri build --target aarch64-unknown-linux-gnu --bundles deb,rpm
- name: Upload artifacts - name: Upload artifacts to GitHub release
env: env:
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }} RELEASE_TAG: ${{ needs.autotag.outputs.release_tag }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: | run: |
set -eu set -eu
API="http://172.0.0.29:3000/api/v1/repos/$GITHUB_REPOSITORY" TAG="${RELEASE_TAG}"
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 \ ARTIFACTS=$(find src-tauri/target/aarch64-unknown-linux-gnu/release/bundle -type f \
\( -name "*.deb" -o -name "*.rpm" -o -name "*.AppImage" \)) \( -name "*.deb" -o -name "*.rpm" -o -name "*.AppImage" \))
if [ -z "$ARTIFACTS" ]; then if [ -z "$ARTIFACTS" ]; then
echo "ERROR: No Linux arm64 artifacts were found to upload." echo "ERROR: No Linux arm64 artifacts found."
exit 1 exit 1
fi fi
printf '%s\n' "$ARTIFACTS" | while IFS= read -r f; do printf '%s\n' "$ARTIFACTS" | while IFS= read -r f; do
NAME=$(basename "$f") NAME="linux-arm64-$(basename "$f")"
UPLOAD_NAME="linux-arm64-$NAME" echo "Uploading $NAME..."
echo "Uploading $UPLOAD_NAME..." gh release delete-asset "$TAG" "$NAME" --yes 2>/dev/null || true
EXISTING_IDS=$(curl -sf "$API/releases/$RELEASE_ID" \ gh release upload "$TAG" "$f#$NAME"
-H "Authorization: token $RELEASE_TOKEN" \ echo "✓ Uploaded $NAME"
| 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 done

View File

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

View File

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

View File

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

View File

@ -77,8 +77,9 @@ cargo tauri build # Outputs to src-tauri/target/release/bundle/
### CI/CD ### CI/CD
- **Test pipeline**: `.woodpecker/test.yml` — runs on every push/PR - **Test pipeline**: `.github/workflows/test.yml` — runs on every push/PR targeting `main`
- **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` - **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`
--- ---
@ -162,24 +163,21 @@ 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`. 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`.
### Woodpecker CI + Gogs Compatibility ### GitHub Actions CI
**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. All pipelines run on GitHub Actions at `https://github.com/msicie/apollo_nxt-trcaa/actions`.
Known issues with Woodpecker 0.15.4 + Gogs 0.14: - `GITHUB_TOKEN` is the only credential needed — no external secrets required
- `token.ParseRequest()` does not read `?token=` URL params (only `Authorization` header and `user_sess` cookie) - Builder images are hosted on `ghcr.io/msicie/` (GitHub Container Registry)
- The SPA login form uses `login=` field; Gogs backend reads `username=` — a custom login page is served by nginx at `/login` and `/login/form` - Branch protection on `main` requires `rust-test` and `frontend-test` checks to pass, plus Copilot code review, before merging
- 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 ## Wiki Maintenance
The project wiki lives at `https://gogs.tftsr.com/sarman/tftsr-devops_investigation/wiki`. The project wiki lives at `https://github.com/msicie/apollo_nxt-trcaa/wiki`.
**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. **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`.
**When making code changes, update the corresponding wiki file in `docs/wiki/` before committing:** **When making code changes, update the corresponding wiki file in `docs/wiki/` before committing:**
@ -189,7 +187,7 @@ The project wiki lives at `https://gogs.tftsr.com/sarman/tftsr-devops_investigat
| DB schema or migrations (`db/migrations.rs`, `db/models.rs`) | `docs/wiki/Database.md` | | 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` | | New/changed AI provider (`ai/*.rs`) | `docs/wiki/AI-Providers.md` |
| PII patterns or detection logic (`pii/`) | `docs/wiki/PII-Detection.md` | | PII patterns or detection logic (`pii/`) | `docs/wiki/PII-Detection.md` |
| CI/CD pipeline changes (`.woodpecker/*.yml`) | `docs/wiki/CICD-Pipeline.md` | | CI/CD pipeline changes (`.github/workflows/*.yml`) | `docs/wiki/CICD-Pipeline.md` |
| Rust architecture or module layout (`lib.rs`, `state.rs`) | `docs/wiki/Architecture.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` | | Security-relevant changes (capabilities, audit, Stronghold) | `docs/wiki/Security-Model.md` |
| Dev setup, prerequisites, build commands | `docs/wiki/Development-Setup.md` | | Dev setup, prerequisites, build commands | `docs/wiki/Development-Setup.md` |
@ -198,7 +196,7 @@ The project wiki lives at `https://gogs.tftsr.com/sarman/tftsr-devops_investigat
To manually push wiki changes without waiting for CI: To manually push wiki changes without waiting for CI:
```bash ```bash
cd /tmp/tftsr-wiki # local clone of the wiki git repo cd /tmp/apollo-wiki # local clone of the wiki git repo
# edit *.md files, then: # edit *.md files, then:
git add -A && git commit -m "docs: ..." && git push git add -A && git commit -m "docs: ..." && git push
``` ```

View File

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

13
PLAN.md
View File

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

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

View File

@ -51,16 +51,9 @@ These expose internal service infrastructure to anyone reading the source and in
--- ---
### C3. Private Gogs Server IP Exposed in All CI Workflows ### C3. Private Gogs Server IP in CI Workflows — RESOLVED
**Files**: **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.
- `.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.
--- ---
@ -263,13 +256,9 @@ style-src 'self' 'unsafe-inline'
--- ---
### L4. Username `sarman` Embedded in CI Workflows and Makefile ### L4. Username in CI Workflows and Makefile — RESOLVED
**Files**: `.gitea/workflows/*.yml`, `Makefile` line 2 **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.
**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.
--- ---
@ -317,7 +306,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 `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** | 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 | Medium | | **P0** | ~~Replace or parameterize private IP (`172.0.0.29`) and username in all `.gitea/` workflows~~ **RESOLVED** — migrated to GitHub Actions | — |
| **P1** | Replace hardcoded dev encryption keys with auto-generated per-install keys | Small | | **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** | 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 | | **P1** | Auto-generate encryption key for credential storage (mirror `connection.rs` pattern) | Small |

View File

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

View File

@ -4,67 +4,39 @@
| Component | URL | Notes | | Component | URL | Notes |
|-----------|-----|-------| |-----------|-----|-------|
| Gitea | `https://gogs.tftsr.com` / `http://172.0.0.29:3000` | Git server (migrated from Gogs 0.14) | | GitHub | `https://github.com/msicie/apollo_nxt-trcaa` | Git server and CI/CD platform |
| Woodpecker CI (direct) | `http://172.0.0.29:8084` | v2.x | | GitHub Actions | Native to GitHub | Hosted runners (`ubuntu-latest`, `macos-latest`, `macos-13`) |
| Woodpecker CI (proxy) | `http://172.0.0.29:8085` | nginx reverse proxy | | ghcr.io | `ghcr.io/msicie/` | GitHub Container Registry for pre-baked CI images |
| 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 ## Pre-baked Builder Images
CI build and test jobs use pre-baked Docker images pushed to the local Gitea registry 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.
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 | | Image | Used by jobs | Contents |
|-------|-------------|----------| |-------|-------------|----------|
| `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 | | `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 |
| `172.0.0.29:3000/sarman/trcaa-windows-cross:rust1.88-node22` | `build-windows-amd64` | Rust 1.88 + mingw-w64 + NSIS + Node.js 22 | | `ghcr.io/msicie/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 | | `ghcr.io/msicie/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. **Rebuild triggers:** Rust toolchain version bump, webkit2gtk/gtk major version change, Node.js major version change.
**How to rebuild images:** **How to rebuild images:**
1. Trigger `build-images.yml` via `workflow_dispatch` in the Gitea Actions UI 1. Trigger `build-images.yml` via Actions → Build CI Docker Images → Run workflow
2. Confirm all 3 images appear in the Gitea package/container registry at `172.0.0.29:3000` 2. Confirm all 3 images appear in `ghcr.io/msicie/`
3. Only then merge workflow changes that depend on the new image contents 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 ## Cargo and npm Caching
All Rust and build jobs use `actions/cache@v3` to cache downloaded package artifacts. All Rust and build jobs use `actions/cache@v4` to cache downloaded package artifacts.
Gitea 1.22 implements the GitHub Actions cache API natively.
**Cargo cache** (Rust jobs): **Cargo cache** (Rust jobs):
```yaml ```yaml
- name: Cache cargo registry - name: Cache cargo registry
uses: actions/cache@v3 uses: actions/cache@v4
with: with:
path: | path: |
~/.cargo/registry/index ~/.cargo/registry/index
@ -78,7 +50,7 @@ Gitea 1.22 implements the GitHub Actions cache API natively.
**npm cache** (frontend and build jobs): **npm cache** (frontend and build jobs):
```yaml ```yaml
- name: Cache npm - name: Cache npm
uses: actions/cache@v3 uses: actions/cache@v4
with: with:
path: ~/.npm path: ~/.npm
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }} key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
@ -92,168 +64,108 @@ Cache keys for cross-compile jobs use a suffix to avoid collisions:
--- ---
## Test Pipeline (`.gitea/workflows/test.yml`) ## Test Pipeline (`.github/workflows/test.yml`)
**Triggers:** Pull requests only. **Triggers:** Every push to `main`, `feature/**`, `bug/**`, `fix/**` branches; pull requests targeting `main`.
``` ```
Pipeline jobs (run in parallel): Jobs (run in parallel):
1. rust-fmt-check → cargo fmt --check rust-test → cargo fmt --check
2. rust-clippy → cargo clippy -- -D warnings → cargo clippy -- -D warnings
3. rust-tests → cargo test (64 tests) → cargo test (64 tests)
4. frontend-typecheck → npx tsc --noEmit frontend-test → npx tsc --noEmit
5. frontend-tests → npm run test:run (13 Vitest tests) → npm run test:run (Vitest tests)
``` ```
**Docker images used:** **Docker image used:**
- `172.0.0.29:3000/sarman/trcaa-linux-amd64:rust1.88-node22` — Rust steps (replaces `rust:1.88-slim`) - `ghcr.io/msicie/trcaa-linux-amd64:rust1.88-node22` — both Rust and frontend steps
- `node:22-alpine` — Frontend steps
Job names `rust-test` and `frontend-test` must match exactly — these are the required status check names in branch protection on `main`.
--- ---
## Release Pipeline (`.gitea/workflows/auto-tag.yml`) ## Release Pipeline (`.github/workflows/release.yml`)
**Triggers:** Pushes to `master` (auto-tag), then release build/upload jobs run after `autotag`. **Triggers:** Push to `main` (auto-tag job), then release build/upload jobs run after `autotag` completes on `v*` tags.
Auto tags are created by `.gitea/workflows/auto-tag.yml` using `git tag` + `git push`. Auto-tags are created by the `autotag` job using `git tag` + `git push`. Release jobs depend on `autotag` and run in parallel.
Release jobs are executed in the same workflow and depend on `autotag` completion.
``` ```
Jobs (run in parallel after autotag): Jobs (run in parallel after autotag):
build-linux-amd64 → image: trcaa-linux-amd64:rust1.88-node22 build-linux-amd64 → image: ghcr.io/msicie/trcaa-linux-amd64:rust1.88-node22
→ cargo tauri build (x86_64-unknown-linux-gnu) → cargo tauri build (x86_64-unknown-linux-gnu)
→ {.deb, .rpm, .AppImage} uploaded to Gitea release → {.deb, .rpm, .AppImage} uploaded to GitHub Release
→ fails fast if no Linux artifacts are produced → 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
```
**Per-step agent routing (Woodpecker 2.x labels):** 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
```yaml build-linux-arm64 → image: ghcr.io/msicie/trcaa-linux-arm64:rust1.88-node22
steps: → cargo tauri build (aarch64-unknown-linux-gnu)
- name: build-linux-amd64 → {.deb, .rpm, .AppImage} uploaded to GitHub Release
labels: → fails fast if no Linux artifacts are produced
platform: linux/amd64 # → woodpecker_agent on 172.0.0.29
- name: build-linux-arm64 build-macos-arm64 → runs-on: macos-latest (Apple Silicon)
labels: → cargo tauri build (aarch64-apple-darwin) natively
platform: linux/arm64 # → woodpecker-agent.service on local arm64 machine → {.dmg} uploaded to GitHub Release
``` → existing same-name assets are deleted before upload (rerun-safe)
→ unsigned; after install run: xattr -cr /Applications/TRCAA.app
**Multi-agent workspace isolation:** build-macos-intel → runs-on: macos-13 (Intel x86_64)
→ cargo tauri build (x86_64-apple-darwin) natively
Steps routed to different agents do **not** share a workspace. The arm64 step clones → {.dmg} uploaded to GitHub Release
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:** **Windows cross-compile environment:**
```yaml ```yaml
environment: env:
TARGET: x86_64-pc-windows-gnu TARGET: x86_64-pc-windows-gnu
CC_x86_64_pc_windows_gnu: x86_64-w64-mingw32-gcc CC_x86_64_pc_windows_gnu: x86_64-w64-mingw32-gcc
CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER: 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:** **Artifacts per platform:**
- Linux amd64: `.deb`, `.rpm`, `.AppImage` - Linux amd64: `.deb`, `.rpm`, `.AppImage`
- Windows amd64: `.exe` (NSIS installer), `.msi` - Windows amd64: `.exe` (NSIS installer), `.msi`
- Linux arm64: `.deb`, `.rpm`, `.AppImage` - Linux arm64: `.deb`, `.rpm`, `.AppImage`
- macOS ARM64: `.dmg`
**Upload step (requires gogs_default network for amd64, host IP for arm64):** - macOS Intel: `.dmg`
```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.
--- ---
## Multi-File Pipeline Support (Woodpecker 2.x) ## Build Images Workflow (`.github/workflows/build-images.yml`)
Woodpecker 2.x supports multiple pipeline files in the `.woodpecker/` directory. **Triggers:** Changes to `.docker/**` on `main`; manual `workflow_dispatch`.
All `.yml` files are evaluated on every trigger; `when:` conditions control which
pipelines actually run.
Current files: Builds and pushes all three pre-baked CI images to `ghcr.io/msicie/`:
- `.woodpecker/test.yml` — runs on every push/PR ```
- `.woodpecker/release.yml` — runs on `v*` tags only Jobs (all run on ubuntu-latest):
build-linux-amd64-image → .docker/Dockerfile.linux-amd64
→ ghcr.io/msicie/trcaa-linux-amd64:rust1.88-node22
No DB config path switching needed (unlike Woodpecker 0.15.4). build-windows-cross-image → .docker/Dockerfile.windows-cross
→ ghcr.io/msicie/trcaa-windows-cross:rust1.88-node22
--- build-linux-arm64-image → .docker/Dockerfile.linux-arm64
→ ghcr.io/msicie/trcaa-linux-arm64:rust1.88-node22
```
## Webhook Configuration Authentication: `docker login ghcr.io -u ${{ github.actor }} --password-stdin <<< "${{ secrets.GITHUB_TOKEN }}"`. Requires `packages: write` permission.
**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 ## Branch Protection
Master branch is protected: all changes require a PR. `main` branch requires:
- All changes via PR
```sql - 1 approving review
-- Gitea branch protection (via psql on gogs_postgres_db container) - CODEOWNER review (`@Shaun-Arman-VFK387_moto` and `@github-copilot`)
-- Check protection - Required status checks: `rust-test`, `frontend-test`
SELECT name, protected, require_pull_request FROM protect_branch WHERE repo_id=42; - `enforce_admins: false` — owner and admins can bypass with `gh pr merge --admin`
-- 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';
```
--- ---
@ -264,7 +176,7 @@ Configuration lives in `cliff.toml` at the repo root.
### How it works ### How it works
A `changelog` job in `auto-tag.yml` runs in parallel with the build jobs, immediately A `changelog` job in `release.yml` runs in parallel with the build jobs, immediately
after `autotag` completes: after `autotag` completes:
1. Clones the full repo history with all tags (`--depth=2147483647` — git-cliff needs 1. Clones the full repo history with all tags (`--depth=2147483647` — git-cliff needs
@ -272,9 +184,9 @@ after `autotag` completes:
2. Downloads the git-cliff v2.7.0 static musl binary (~5 MB, no image change needed). 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. 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. 4. Runs `git-cliff --latest --strip all` to produce release notes for the new tag only.
5. PATCHes the Gitea release body with those notes (replaces the static `"Release vX.Y.Z"`). 5. Updates the GitHub Release body with those notes via `gh release edit`.
6. Commits `CHANGELOG.md` to master with `[skip ci]` appended to the message. 6. Commits `CHANGELOG.md` to `main` with `[skip ci]` appended to the message.
The `[skip ci]` token prevents `auto-tag.yml` from re-triggering on the CHANGELOG commit. The `[skip ci]` token prevents `release.yml` from re-triggering on the CHANGELOG commit.
7. Uploads `CHANGELOG.md` as a release asset (replaces any previous version). 7. Uploads `CHANGELOG.md` as a release asset (replaces any previous version).
### cliff.toml reference ### cliff.toml reference
@ -289,17 +201,9 @@ after `autotag` completes:
### Loop prevention ### Loop prevention
The `[skip ci]` suffix on the CHANGELOG commit message is recognised by Gitea Actions The `[skip ci]` suffix on the CHANGELOG commit message is recognised by GitHub Actions
and causes the workflow to be skipped for that push. Without it, the CHANGELOG commit and causes the workflow to be skipped for that push. Without it, the CHANGELOG commit
would trigger `auto-tag.yml` again, incrementing the patch version forever. would trigger `release.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.
--- ---
@ -312,27 +216,16 @@ 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 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. held broken packages` and cannot be fixed by tweaking `sources.list` entries.
**Fix**: Use `ubuntu:22.04` as the container image. Ubuntu routes arm64 through **Fix**: Use `ubuntu:22.04` as the container image (see `Dockerfile.linux-arm64`). Ubuntu
`ports.ubuntu.com/ubuntu-ports` — a separate mirror from `archive.ubuntu.com` (amd64). routes arm64 through `ports.ubuntu.com/ubuntu-ports` — a separate mirror from
There are no cross-arch index overlaps and the dependency resolver succeeds. Rust must be `archive.ubuntu.com` (amd64). There are no cross-arch index overlaps and the dependency
installed manually via `rustup` since it is not pre-installed in the Ubuntu base image. resolver succeeds. Rust must be installed manually via `rustup`.
### Step Containers Cannot Reach `gitea_app` ### `CI=true` Required by Tauri CLI
Default Docker bridge containers cannot resolve `gitea_app` or reach `172.0.0.29:3000` GitHub Actions sets `CI=true` by default — `cargo tauri build` accepts this without
(host firewall). Fix: use `network_mode: gogs_default` in any step that needs Gitea modification. Prefix the command explicitly if your runner environment is unusual:
access. Requires `repo_trusted=1`. ```yaml
- run: CI=true cargo tauri build --target $TARGET
### `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 ### Windows DLL Export Ordinal Too Large
@ -344,32 +237,17 @@ Fix: `src-tauri/.cargo/config.toml`:
rustflags = ["-C", "link-arg=-Wl,--exclude-all-symbols"] rustflags = ["-C", "link-arg=-Wl,--exclude-all-symbols"]
``` ```
### GOGS_TOKEN Secret Must Be Recreated After Migration ### Release Artifacts Not Uploaded
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.
## Gitea PostgreSQL Access Verify the `release.yml` permissions block includes:
```yaml
```bash permissions:
docker exec gogs_postgres_db psql -U gogs -d gogsdb -c "SELECT id, lower_name FROM repository;" contents: write
``` ```
> Database name is `gogsdb` (unchanged from Gogs migration). ### 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.
## 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. **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](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 **CI:** ![build](https://github.com/msicie/apollo_nxt-trcaa/actions/workflows/test.yml/badge.svg) — rustfmt · clippy · 64 Rust tests · tsc · vitest — all green
## Quick Navigation ## Quick Navigation
@ -15,7 +15,7 @@
| [LiteLLM + Bedrock Setup](wiki/LiteLLM-Bedrock-Setup) | AWS Bedrock integration via LiteLLM proxy | | [LiteLLM + Bedrock Setup](wiki/LiteLLM-Bedrock-Setup) | AWS Bedrock integration via LiteLLM proxy |
| [PII Detection](wiki/PII-Detection) | Patterns, redaction flow, security | | [PII Detection](wiki/PII-Detection) | Patterns, redaction flow, security |
| [IPC Commands](wiki/IPC-Commands) | Full list of Tauri backend commands | | [IPC Commands](wiki/IPC-Commands) | Full list of Tauri backend commands |
| [CI/CD Pipeline](wiki/CICD-Pipeline) | Gitea Actions setup, multi-platform builds, act_runner config | | [CI/CD Pipeline](wiki/CICD-Pipeline) | GitHub Actions setup, multi-platform builds, pre-baked ghcr.io images |
| [Security Model](wiki/Security-Model) | Encryption, audit trail, capabilities | | [Security Model](wiki/Security-Model) | Encryption, audit trail, capabilities |
| [Integrations](wiki/Integrations) | Confluence, ServiceNow, Azure DevOps (v0.2) | | [Integrations](wiki/Integrations) | Confluence, ServiceNow, Azure DevOps (v0.2) |
| [Troubleshooting](wiki/Troubleshooting) | Known issues and fixes | | [Troubleshooting](wiki/Troubleshooting) | Known issues and fixes |
@ -45,7 +45,7 @@
**Platforms:** linux/amd64 · linux/arm64 · windows/amd64 (.deb, .rpm, .AppImage, .exe, .msi) **Platforms:** linux/amd64 · linux/arm64 · windows/amd64 (.deb, .rpm, .AppImage, .exe, .msi)
Download from [Releases](https://gogs.tftsr.com/sarman/tftsr-devops_investigation/releases). All builds are produced natively (no QEMU emulation). Download from [Releases](https://github.com/msicie/apollo_nxt-trcaa/releases). All builds are produced natively (no QEMU emulation).
## Project Status ## Project Status
@ -54,8 +54,8 @@ Download from [Releases](https://gogs.tftsr.com/sarman/tftsr-devops_investigatio
| Phases 18 (Core application) | ✅ Complete | | Phases 18 (Core application) | ✅ Complete |
| Phase 9 (History/Search) | 🔲 Pending | | Phase 9 (History/Search) | 🔲 Pending |
| Phase 10 (Integrations) | ✅ Complete — Confluence, ServiceNow, Azure DevOps fully implemented with OAuth2 | | Phase 10 (Integrations) | ✅ Complete — Confluence, ServiceNow, Azure DevOps fully implemented with OAuth2 |
| Phase 11 (CI/CD) | ✅ Complete — Gitea Actions fully operational | | Phase 11 (CI/CD) | ✅ Complete — GitHub Actions fully operational |
| Phase 12 (Release packaging) | ✅ linux/amd64 · linux/arm64 (native) · windows/amd64 | | Phase 12 (Release packaging) | ✅ linux/amd64 · linux/arm64 · windows/amd64 · macOS ARM64 · macOS Intel |
## Tech Stack ## Tech Stack
@ -69,4 +69,4 @@ Download from [Releases](https://gogs.tftsr.com/sarman/tftsr-devops_investigatio
| Secret storage | tauri-plugin-stronghold | | Secret storage | tauri-plugin-stronghold |
| State | Zustand | | State | Zustand |
| Testing | Vitest (13 frontend) + `#[cfg(test)]` (64 Rust tests) | | Testing | Vitest (13 frontend) + `#[cfg(test)]` (64 Rust tests) |
| CI/CD | Gitea Actions (act_runner v0.3.1) + Gitea | | CI/CD | GitHub Actions + ghcr.io |

View File

@ -1,71 +1,41 @@
# Troubleshooting # Troubleshooting
## CI/CD — Gitea Actions ## CI/CD — GitHub Actions
### Build Not Triggering After Push ### Build Not Triggering After Push
**Check:** **Check:**
1. Verify the workflow file exists in `.gitea/workflows/` on the pushed branch 1. Verify the workflow file exists in `.github/workflows/` on the pushed branch
2. Check the Actions tab at `http://172.0.0.29:3000/sarman/tftsr-devops_investigation/actions` 2. Check the Actions tab at `https://github.com/msicie/apollo_nxt-trcaa/actions`
3. Confirm the act_runner is online: `docker logs gitea_act_runner_amd64 --since 5m` 3. Confirm the workflow trigger matches the branch (e.g., `push: branches: [main, 'bug/**']`)
--- ---
### Job Container Can't Reach Gitea (`172.0.0.29:3000` blocked) ### Jobs Stuck in Queue
**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. **Cause:** GitHub Enterprise organizations may restrict which repositories can use
GitHub-hosted runners via runner group policies.
**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: **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
```yaml initial ghcr.io images).
# /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`
--- ---
### `Unable to locate package git` in Rust Job ### `manifest unknown` — Image Pull Fails
**Cause:** `rust:1.88-slim` has an empty apt package cache. **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.
**Fix:** Always run `apt-get update` before `apt-get install`: **Fix:**
```yaml 1. Go to Actions → Build CI Docker Images → Run workflow
- name: Checkout 2. Wait for all 3 image builds to complete
run: | 3. Confirm images appear at `https://github.com/orgs/msicie/packages`
apt-get update -qq && apt-get install -y -qq git 4. Re-run the failing workflow
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
```
--- ---
### `exec: "node": executable file not found in $PATH` ### Job Skipped on Tag Push
**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: **Cause:** Pattern matching issue with `on: push: tags:`. Use unquoted glob in the workflow:
@ -92,29 +62,17 @@ 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 ### Release Artifacts Not Uploaded
**Cause 1:** `RELEASE_TOKEN` secret not set or expired. **Cause:** `GITHUB_TOKEN` permissions insufficient. The `release.yml` workflow requires
```bash `contents: write` to create releases and upload assets.
# 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). Verify the permissions block:
```yaml
permissions:
contents: write
packages: write
```
--- ---
@ -158,7 +116,7 @@ Auto-fix: `cargo clippy --manifest-path src-tauri/Cargo.toml --fix --allow-dirty
**Fix (Fedora/RHEL):** **Fix (Fedora/RHEL):**
```bash ```bash
sudo dnf install -y glib2-devel gtk3-devel webkit2gtk4.1-devel \ sudo dnf install -y glib2-devel gtk3-devel webkit2gtk4.1-devel \
libsoup3-devel openssl-devel librsvg2-devel libsoup3-devel openssl-devel librsvg2-dev
``` ```
**Fix (Debian/Ubuntu):** **Fix (Debian/Ubuntu):**
@ -169,6 +127,17 @@ 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 ## Database
### DB Won't Open in Production ### DB Won't Open in Production
@ -220,27 +189,3 @@ const title = detail.title;
Common causes: Common causes:
- Mocked `invoke()` return type doesn't match updated command signature - Mocked `invoke()` return type doesn't match updated command signature
- `sessionStore` state not reset between tests (call `store.reset()` in `beforeEach`) - `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( const autoTagWorkflowPath = path.resolve(
process.cwd(), process.cwd(),
".gitea/workflows/auto-tag.yml", ".github/workflows/release.yml",
); );
describe("auto-tag workflow release triggering", () => { describe("auto-tag workflow release triggering", () => {
it("creates tags via git push instead of Gitea tag API", () => { it("creates tags via git push instead of API call", () => {
const workflow = readFileSync(autoTagWorkflowPath, "utf-8"); const workflow = readFileSync(autoTagWorkflowPath, "utf-8");
expect(workflow).toContain("git push origin \"refs/tags/$NEXT\""); expect(workflow).toContain("git push origin \"refs/tags/$NEXT\"");
@ -23,7 +23,13 @@ describe("auto-tag workflow release triggering", () => {
expect(workflow).toContain("build-macos-arm64:"); expect(workflow).toContain("build-macos-arm64:");
expect(workflow).toContain("build-linux-arm64:"); expect(workflow).toContain("build-linux-arm64:");
expect(workflow).toContain("needs: autotag"); expect(workflow).toContain("needs: autotag");
expect(workflow).toContain("TAG=$(curl -s \"$API/tags?limit=50\""); expect(workflow).toContain("git tag --sort=-version:refname");
expect(workflow).toContain("ERROR: Could not resolve release tag from repository tags."); });
it("also includes macOS Intel build job", () => {
const workflow = readFileSync(autoTagWorkflowPath, "utf-8");
expect(workflow).toContain("build-macos-intel:");
expect(workflow).toContain("macos-13");
}); });
}); });

View File

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

View File

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

52
ticket-727547-summary.md Normal file
View File

@ -0,0 +1,52 @@
# 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, - `cliff.toml` (new) — git-cliff configuration; defines commit parsers, ignored tags,
output template, and which commit types appear in the changelog output template, and which commit types appear in the changelog
- `CHANGELOG.md` (new) — bootstrapped from all existing tags; maintained by CI going forward - `CHANGELOG.md` (new) — bootstrapped from all existing tags; maintained by CI going forward
- `.gitea/workflows/auto-tag.yml` — new `changelog` job that runs after `autotag` - `.github/workflows/release.yml` — `changelog` job that runs after `autotag`
- `docs/wiki/CICD-Pipeline.md` — "Changelog Generation" section added - `docs/wiki/CICD-Pipeline.md` — "Changelog Generation" section added
## Acceptance Criteria ## Acceptance Criteria
- [ ] `cliff.toml` present at repo root with working Tera template - [ ] `cliff.toml` present at repo root with working Tera template
- [ ] `CHANGELOG.md` present at repo root, bootstrapped from all existing semver tags - [ ] `CHANGELOG.md` present at repo root, bootstrapped from all existing semver tags
- [ ] `changelog` job in `auto-tag.yml` runs after `autotag` (parallel with build jobs) - [ ] `changelog` job in `release.yml` runs after `autotag` (parallel with build jobs)
- [ ] Each Gitea release body shows grouped conventional-commit entries instead of - [ ] Each GitHub Release body shows grouped conventional-commit entries instead of
static `"Release vX.Y.Z"` static `"Release vX.Y.Z"`
- [ ] `CHANGELOG.md` committed to master on every release with `[skip ci]` suffix - [ ] `CHANGELOG.md` committed to `main` on every release with `[skip ci]` suffix
(no infinite re-trigger loop) (no infinite re-trigger loop)
- [ ] `CHANGELOG.md` uploaded as a downloadable release asset - [ ] `CHANGELOG.md` uploaded as a downloadable release asset
- [ ] CI/chore/build/test/style commits excluded from changelog output - [ ] 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]` - Covers all tagged versions from `v0.1.0` through `v0.2.49` plus `[Unreleased]`
- 267 lines covering the full project history - 267 lines covering the full project history
### `.gitea/workflows/auto-tag.yml` — `changelog` job ### `.github/workflows/release.yml` — `changelog` job
- `needs: autotag` — waits for the new tag to exist before running - `needs: autotag` — waits for the new tag to exist before running
- Full history clone: `git fetch --tags --depth=2147483647` so git-cliff can resolve - Full history clone: `git fetch --tags --depth=2147483647` so git-cliff can resolve
all version boundaries all version boundaries
- git-cliff v2.7.0 downloaded as a static x86_64 musl binary (~5 MB); no custom - git-cliff v2.7.0 downloaded as a static x86_64 musl binary (~5 MB); no custom
image required image required
- Generates full `CHANGELOG.md` and per-release notes (`--latest --strip all`) - Generates full `CHANGELOG.md` and per-release notes (`--latest --strip all`)
- PATCHes the Gitea release body via API with JSON-safe escaping (`jq -Rs .`) - Updates the GitHub Release body via `gh release edit` with JSON-safe escaping (`jq -Rs .`)
- Commits `CHANGELOG.md` to master with `[skip ci]` to prevent workflow re-trigger - Commits `CHANGELOG.md` to `main` with `[skip ci]` to prevent workflow re-trigger
- Deletes any existing `CHANGELOG.md` asset before re-uploading (rerun-safe) - Deletes any existing `CHANGELOG.md` asset before re-uploading (rerun-safe)
- Runs in parallel with all build jobs — no added wall-clock latency - 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 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 The repository already contains pre-baked builder Docker images (`.docker/Dockerfile.*`) and a
`build-images.yml` workflow to push them to the local Gitea registry at `172.0.0.29:3000`. `build-images.yml` workflow to push them to `ghcr.io/msicie/`.
These images were never referenced by the actual CI jobs — a critical gap. This work closes 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. 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-amd64` includes `rustfmt` and `clippy` components
- [ ] `Dockerfile.linux-arm64` includes `rustfmt` and `clippy` components - [ ] `Dockerfile.linux-arm64` includes `rustfmt` and `clippy` components
- [ ] `test.yml` Rust jobs use `172.0.0.29:3000/sarman/trcaa-linux-amd64:rust1.88-node22` - [ ] `test.yml` Rust jobs use `ghcr.io/msicie/trcaa-linux-amd64:rust1.88-node22`
- [ ] `test.yml` Rust jobs have no inline `apt-get` or `rustup component add` steps - [ ] `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` Rust jobs include `actions/cache@v3` for `~/.cargo/registry`
- [ ] `test.yml` frontend jobs include `actions/cache@v3` for `~/.npm` - [ ] `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 Added `&& /root/.cargo/bin/rustup component add rustfmt clippy` appended to the
existing `rustup` installation RUN command (chained with `&&` to keep it one layer). existing `rustup` installation RUN command (chained with `&&` to keep it one layer).
### `.gitea/workflows/test.yml` ### `.github/workflows/test.yml`
- **rust-fmt-check**, **rust-clippy**, **rust-tests**: switched container image from - **rust-test** (fmt + clippy + tests): switched container image from
`rust:1.88-slim``172.0.0.29:3000/sarman/trcaa-linux-amd64:rust1.88-node22`. `rust:1.88-slim``ghcr.io/msicie/trcaa-linux-amd64:rust1.88-node22`.
Removed `apt-get install git` from Checkout steps (git is pre-installed in image). Removed `apt-get install git` from Checkout steps (git is pre-installed in image).
Removed `apt-get install libwebkit2gtk-...` steps. Removed `apt-get install libwebkit2gtk-...` steps.
Removed `rustup component add rustfmt` and `rustup component add clippy` 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). - **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. Added `actions/cache@v3` step for `~/.npm` keyed on `package-lock.json` hash.
### `.gitea/workflows/auto-tag.yml` ### `.github/workflows/release.yml`
- **build-linux-amd64**: image `rust:1.88-slim``trcaa-linux-amd64:rust1.88-node22`. - **build-linux-amd64**: image `rust:1.88-slim``ghcr.io/msicie/trcaa-linux-amd64:rust1.88-node22`.
Removed Checkout apt-get install git, removed entire Install dependencies step. 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. Removed `rustup target add x86_64-unknown-linux-gnu` from Build step. Added cargo + npm cache.
- **build-windows-amd64**: image `rust:1.88-slim``trcaa-windows-cross:rust1.88-node22`. - **build-windows-amd64**: image `rust:1.88-slim``ghcr.io/msicie/trcaa-windows-cross:rust1.88-node22`.
Removed Checkout apt-get install git, removed entire Install dependencies step. Removed Checkout apt-get install git, removed entire Install dependencies step.
Removed `rustup target add x86_64-pc-windows-gnu` from Build step. Removed `rustup target add x86_64-pc-windows-gnu` from Build step.
Added cargo (with `-windows-` suffix key to avoid collision) + npm cache. Added cargo (with `-windows-` suffix key to avoid collision) + npm cache.
- **build-linux-arm64**: image `ubuntu:22.04``trcaa-linux-arm64:rust1.88-node22`. - **build-linux-arm64**: image `ubuntu:22.04``ghcr.io/msicie/trcaa-linux-arm64:rust1.88-node22`.
Removed Checkout apt-get install git, removed entire Install dependencies step (~40 lines). 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 `. "$HOME/.cargo/env"` (PATH already set via `ENV` in Dockerfile).
Removed `rustup target add aarch64-unknown-linux-gnu` from Build step. 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: Added two new sections before the Test Pipeline section:
- **Pre-baked Builder Images**: table of all three images and their contents, rebuild - **Pre-baked Builder Images**: table of all three images and their contents, rebuild
triggers, how-to-rebuild instructions, and the insecure-registries Docker daemon triggers, how-to-rebuild instructions, and the insecure-registries Docker daemon
prerequisite for 172.0.0.29. prerequisite for ghcr.io access.
- **Cargo and npm Caching**: documents the `actions/cache@v3` key patterns in use, - **Cargo and npm Caching**: documents the `actions/cache@v3` key patterns in use,
including the per-platform cache key suffixes for cross-compile jobs. including the per-platform cache key suffixes for cross-compile jobs.
Updated the Test Pipeline section to reference the correct pre-baked image name. Updated the Test Pipeline section to reference the correct pre-baked image name.
@ -75,21 +75,16 @@ Updated the Release Pipeline job table to show which image each build job uses.
## Testing Needed ## Testing Needed
1. **Pre-build images** (prerequisite): Trigger `build-images.yml` via `workflow_dispatch` 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/`.
on Gitea Actions UI. Confirm all 3 images are pushed and visible in the registry.
2. **Server prerequisite**: Confirm `/etc/docker/daemon.json` on `172.0.0.29` contains 2. **PR test suite**: Open a PR with these changes. Verify:
`{"insecure-registries":["172.0.0.29:3000"]}` and Docker was restarted after. - `rust-test` and `frontend-test` jobs pass
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 - Job logs show no `apt-get` or `rustup component add` output
- Cache hit messages appear on second run - Cache hit messages appear on second run
4. **Release build**: Merge to master. Verify `auto-tag.yml` runs and: 3. **Release build**: Merge to `main`. Verify `release.yml` runs and:
- All 3 Linux/Windows build jobs start without Install dependencies step - All build jobs start without Install dependencies step
- Artifacts are produced and uploaded to the Gitea release - Artifacts are produced and uploaded to the GitHub Release
- Total release time is significantly reduced (~7 min vs ~25 min before) - Total release time is significantly reduced (~7 min vs ~25 min before)
5. **Expected time savings after caching warms up**: 5. **Expected time savings after caching warms up**: