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)
This commit is contained in:
parent
e5d3ff42f5
commit
1023d7a944
@ -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
|
||||
@ -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"
|
||||
@ -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
|
||||
@ -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
9
.github/CODEOWNERS
vendored
Normal 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
|
||||
71
.github/workflows/build-images.yml
vendored
71
.github/workflows/build-images.yml
vendored
@ -1,26 +1,20 @@
|
||||
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).
|
||||
# Rebuilds the pre-baked builder images and pushes them to ghcr.io.
|
||||
#
|
||||
# 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).
|
||||
#
|
||||
# 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
|
||||
# ghcr.io/msicie/trcaa-linux-amd64:rust1.88-node22
|
||||
# ghcr.io/msicie/trcaa-windows-cross:rust1.88-node22
|
||||
# ghcr.io/msicie/trcaa-linux-arm64:rust1.88-node22
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
paths:
|
||||
- '.docker/**'
|
||||
workflow_dispatch:
|
||||
@ -30,66 +24,61 @@ concurrency:
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
REGISTRY: 172.0.0.29:3000
|
||||
REGISTRY_USER: sarman
|
||||
REGISTRY: ghcr.io
|
||||
REGISTRY_OWNER: msicie
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
linux-amd64:
|
||||
runs-on: linux-amd64
|
||||
container:
|
||||
image: docker:24-cli
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- name: Log in to ghcr.io
|
||||
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
|
||||
- name: Build and push linux-amd64 builder
|
||||
env:
|
||||
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
run: |
|
||||
echo "$RELEASE_TOKEN" | docker login $REGISTRY -u $REGISTRY_USER --password-stdin
|
||||
docker build \
|
||||
-t $REGISTRY/$REGISTRY_USER/trcaa-linux-amd64:rust1.88-node22 \
|
||||
-t $REGISTRY/$REGISTRY_OWNER/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"
|
||||
docker push $REGISTRY/$REGISTRY_OWNER/trcaa-linux-amd64:rust1.88-node22
|
||||
echo "✓ Pushed $REGISTRY/$REGISTRY_OWNER/trcaa-linux-amd64:rust1.88-node22"
|
||||
|
||||
windows-cross:
|
||||
runs-on: linux-amd64
|
||||
container:
|
||||
image: docker:24-cli
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- name: Log in to ghcr.io
|
||||
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
|
||||
- name: Build and push windows-cross builder
|
||||
env:
|
||||
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
run: |
|
||||
echo "$RELEASE_TOKEN" | docker login $REGISTRY -u $REGISTRY_USER --password-stdin
|
||||
docker build \
|
||||
-t $REGISTRY/$REGISTRY_USER/trcaa-windows-cross:rust1.88-node22 \
|
||||
-t $REGISTRY/$REGISTRY_OWNER/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"
|
||||
docker push $REGISTRY/$REGISTRY_OWNER/trcaa-windows-cross:rust1.88-node22
|
||||
echo "✓ Pushed $REGISTRY/$REGISTRY_OWNER/trcaa-windows-cross:rust1.88-node22"
|
||||
|
||||
linux-arm64:
|
||||
runs-on: linux-amd64
|
||||
container:
|
||||
image: docker:24-cli
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- name: Log in to ghcr.io
|
||||
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
|
||||
- name: Build and push linux-arm64 builder
|
||||
env:
|
||||
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
run: |
|
||||
echo "$RELEASE_TOKEN" | docker login $REGISTRY -u $REGISTRY_USER --password-stdin
|
||||
docker build \
|
||||
-t $REGISTRY/$REGISTRY_USER/trcaa-linux-arm64:rust1.88-node22 \
|
||||
-t $REGISTRY/$REGISTRY_OWNER/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"
|
||||
docker push $REGISTRY/$REGISTRY_OWNER/trcaa-linux-arm64:rust1.88-node22
|
||||
echo "✓ Pushed $REGISTRY/$REGISTRY_OWNER/trcaa-linux-arm64:rust1.88-node22"
|
||||
|
||||
611
.github/workflows/release.yml
vendored
611
.github/workflows/release.yml
vendored
@ -1,43 +1,68 @@
|
||||
name: Auto Tag
|
||||
name: Release
|
||||
|
||||
# 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.
|
||||
# Runs on every merge to main — reads the latest semver tag, increments
|
||||
# the patch version, pushes a new tag, generates a changelog, then builds
|
||||
# multi-platform release artifacts and uploads them to GitHub Releases.
|
||||
# workflow_dispatch allows manual triggering.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: auto-tag-master
|
||||
group: release-main
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
packages: read
|
||||
|
||||
jobs:
|
||||
autotag:
|
||||
runs-on: linux-amd64
|
||||
container:
|
||||
image: alpine:latest
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
release_tag: ${{ steps.bump.outputs.release_tag }}
|
||||
steps:
|
||||
- name: Checkout (full history + all tags)
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Configure git
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
- name: Bump patch version and create tag
|
||||
id: bump
|
||||
env:
|
||||
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
run: |
|
||||
set -eu
|
||||
apk add --no-cache curl jq git
|
||||
|
||||
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)
|
||||
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)
|
||||
# Get the latest clean semver tag (vX.Y.Z only)
|
||||
LATEST=$(git tag --sort=-version:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -1 || echo "")
|
||||
echo "Latest git tag: ${LATEST:-none}"
|
||||
|
||||
# Version resolution:
|
||||
# 1. Cargo.toml > latest tag → use Cargo.toml (major/minor bump)
|
||||
# 2. Cargo.toml == latest tag → reuse for builds (already tagged)
|
||||
# 3. Cargo.toml < latest tag → auto-increment patch on latest tag
|
||||
if [ -z "$LATEST" ]; then
|
||||
NEXT="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
|
||||
MAJOR=$(echo "$LATEST" | cut -d. -f1 | tr -d 'v')
|
||||
MINOR=$(echo "$LATEST" | cut -d. -f2)
|
||||
@ -47,53 +72,136 @@ jobs:
|
||||
|
||||
echo "Latest tag: ${LATEST:-none} → Next: $NEXT"
|
||||
|
||||
# Create and push the tag via git.
|
||||
git init
|
||||
git remote add origin "http://oauth2:${RELEASE_TOKEN}@172.0.0.29:3000/${GITHUB_REPOSITORY}.git"
|
||||
git fetch --depth=1 origin "$GITHUB_SHA"
|
||||
git checkout FETCH_HEAD
|
||||
git config user.name "gitea-actions[bot]"
|
||||
git config user.email "gitea-actions@local"
|
||||
|
||||
if git ls-remote --exit-code --tags origin "refs/tags/$NEXT" >/dev/null 2>&1; then
|
||||
echo "Tag $NEXT already exists; skipping."
|
||||
exit 0
|
||||
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
|
||||
|
||||
git tag -a "$NEXT" -m "Release $NEXT"
|
||||
git push origin "refs/tags/$NEXT"
|
||||
echo "release_tag=$NEXT" >> "$GITHUB_OUTPUT"
|
||||
|
||||
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:
|
||||
runs-on: linux-amd64
|
||||
container:
|
||||
image: alpine:latest
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Install dependencies
|
||||
run: apk add --no-cache git
|
||||
|
||||
- name: Checkout main repository
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Configure git
|
||||
run: |
|
||||
git config --global user.email "actions@gitea.local"
|
||||
git config --global user.name "Gitea Actions"
|
||||
git config --global credential.helper ''
|
||||
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git config --global user.name "github-actions[bot]"
|
||||
|
||||
- name: Clone and sync wiki
|
||||
env:
|
||||
WIKI_TOKEN: ${{ secrets.Wiki }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
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
|
||||
WIKI_URL="https://x-access-token:${GH_TOKEN}@github.com/msicie/apollo_nxt-trcaa.wiki.git"
|
||||
|
||||
if ! git clone "$WIKI_URL" wiki 2>/dev/null; then
|
||||
echo "Wiki doesn't exist yet, creating initial structure..."
|
||||
@ -115,11 +223,10 @@ jobs:
|
||||
git add -A
|
||||
if ! git diff --staged --quiet; then
|
||||
git commit -m "docs: sync from docs/wiki/ at commit ${GITHUB_SHA:0:8}"
|
||||
echo "Pushing to wiki..."
|
||||
if git push origin master; then
|
||||
echo "✓ Wiki successfully synced"
|
||||
else
|
||||
echo "⚠ Wiki push failed - check token permissions"
|
||||
echo "⚠ Wiki push failed"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
@ -128,102 +235,91 @@ jobs:
|
||||
|
||||
build-linux-amd64:
|
||||
needs: autotag
|
||||
runs-on: linux-amd64
|
||||
runs-on: ubuntu-latest
|
||||
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:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
apt-get update -qq && apt-get install -y -qq \
|
||||
libwebkit2gtk-4.1-dev libssl-dev libgtk-3-dev \
|
||||
libayatana-appindicator3-dev librsvg2-dev patchelf \
|
||||
pkg-config curl perl jq
|
||||
curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
|
||||
apt-get install -y nodejs
|
||||
- name: 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
|
||||
rustup target add 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:
|
||||
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
RELEASE_TAG: ${{ needs.autotag.outputs.release_tag }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_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"
|
||||
TAG="${RELEASE_TAG}"
|
||||
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
|
||||
echo "ERROR: No Linux amd64 artifacts were found to upload."
|
||||
echo "ERROR: No Linux amd64 artifacts found."
|
||||
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
|
||||
NAME="linux-amd64-$(basename "$f")"
|
||||
echo "Uploading $NAME..."
|
||||
gh release delete-asset "$TAG" "$NAME" --yes 2>/dev/null || true
|
||||
gh release upload "$TAG" "$f#$NAME"
|
||||
echo "✓ Uploaded $NAME"
|
||||
done
|
||||
|
||||
build-windows-amd64:
|
||||
needs: autotag
|
||||
runs-on: linux-amd64
|
||||
runs-on: ubuntu-latest
|
||||
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:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
apt-get update -qq && apt-get install -y -qq mingw-w64 curl nsis perl make jq
|
||||
curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
|
||||
apt-get install -y nodejs
|
||||
- name: 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
|
||||
@ -234,65 +330,26 @@ jobs:
|
||||
OPENSSL_STATIC: "1"
|
||||
run: |
|
||||
npm ci --legacy-peer-deps
|
||||
rustup target add x86_64-pc-windows-gnu
|
||||
CI=true npx tauri build --target x86_64-pc-windows-gnu
|
||||
- name: Upload artifacts
|
||||
- name: Upload artifacts to GitHub release
|
||||
env:
|
||||
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
RELEASE_TAG: ${{ needs.autotag.outputs.release_tag }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_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"
|
||||
TAG="${RELEASE_TAG}"
|
||||
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."
|
||||
echo "ERROR: No Windows amd64 artifacts found."
|
||||
exit 1
|
||||
fi
|
||||
printf '%s\n' "$ARTIFACTS" | while IFS= read -r f; do
|
||||
NAME=$(basename "$f")
|
||||
NAME="windows-amd64-$(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
|
||||
gh release delete-asset "$TAG" "$NAME" --yes 2>/dev/null || true
|
||||
gh release upload "$TAG" "$f#$NAME"
|
||||
echo "✓ Uploaded $NAME"
|
||||
done
|
||||
|
||||
build-macos-arm64:
|
||||
@ -320,112 +377,101 @@ jobs:
|
||||
mkdir -p src-tauri/target/aarch64-apple-darwin/release/bundle/dmg
|
||||
DMG=src-tauri/target/aarch64-apple-darwin/release/bundle/dmg/${APP_NAME}.dmg
|
||||
hdiutil create -volname "$APP_NAME" -srcfolder "$APP" -ov -format UDZO "$DMG"
|
||||
- name: Upload artifacts
|
||||
- name: Upload artifacts to GitHub release
|
||||
env:
|
||||
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
RELEASE_TAG: ${{ needs.autotag.outputs.release_tag }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_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"
|
||||
TAG="${RELEASE_TAG}"
|
||||
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."
|
||||
echo "ERROR: No macOS arm64 DMG artifacts found."
|
||||
exit 1
|
||||
fi
|
||||
printf '%s\n' "$ARTIFACTS" | while IFS= read -r f; do
|
||||
NAME=$(basename "$f")
|
||||
NAME="macos-arm64-$(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
|
||||
gh release delete-asset "$TAG" "$NAME" --yes 2>/dev/null || true
|
||||
gh release upload "$TAG" "$f#$NAME"
|
||||
echo "✓ Uploaded $NAME"
|
||||
done
|
||||
|
||||
build-linux-arm64:
|
||||
build-macos-intel:
|
||||
needs: autotag
|
||||
runs-on: linux-amd64
|
||||
container:
|
||||
image: ubuntu:22.04
|
||||
runs-on: macos-13
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- name: Install dependencies
|
||||
- name: Build
|
||||
env:
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
MACOSX_DEPLOYMENT_TARGET: "10.15"
|
||||
run: |
|
||||
# Step 1: Host tools + cross-compiler (all amd64, no multiarch yet)
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq curl git gcc g++ make patchelf pkg-config perl jq \
|
||||
gcc-aarch64-linux-gnu g++-aarch64-linux-gnu
|
||||
npm ci --legacy-peer-deps
|
||||
rustup target add x86_64-apple-darwin
|
||||
CI=true npx tauri build --target x86_64-apple-darwin --bundles app
|
||||
APP=$(find src-tauri/target/x86_64-apple-darwin/release/bundle/macos -maxdepth 1 -type d -name "*.app" | head -n 1)
|
||||
if [ -z "$APP" ]; then
|
||||
echo "ERROR: Could not find macOS app bundle"
|
||||
exit 1
|
||||
fi
|
||||
APP_NAME=$(basename "$APP" .app)
|
||||
codesign --deep --force --sign - "$APP"
|
||||
mkdir -p src-tauri/target/x86_64-apple-darwin/release/bundle/dmg
|
||||
DMG=src-tauri/target/x86_64-apple-darwin/release/bundle/dmg/${APP_NAME}.dmg
|
||||
hdiutil create -volname "$APP_NAME" -srcfolder "$APP" -ov -format UDZO "$DMG"
|
||||
- name: Upload artifacts to GitHub release
|
||||
env:
|
||||
RELEASE_TAG: ${{ needs.autotag.outputs.release_tag }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
set -eu
|
||||
TAG="${RELEASE_TAG}"
|
||||
ARTIFACTS=$(find src-tauri/target/x86_64-apple-darwin/release/bundle -type f -name "*.dmg")
|
||||
if [ -z "$ARTIFACTS" ]; then
|
||||
echo "ERROR: No macOS Intel DMG artifacts found."
|
||||
exit 1
|
||||
fi
|
||||
printf '%s\n' "$ARTIFACTS" | while IFS= read -r f; do
|
||||
NAME="macos-intel-$(basename "$f")"
|
||||
echo "Uploading $NAME..."
|
||||
gh release delete-asset "$TAG" "$NAME" --yes 2>/dev/null || true
|
||||
gh release upload "$TAG" "$f#$NAME"
|
||||
echo "✓ Uploaded $NAME"
|
||||
done
|
||||
|
||||
# Step 2: Multiarch — Ubuntu uses ports.ubuntu.com for arm64,
|
||||
# keeping it on a separate mirror from amd64 (archive.ubuntu.com).
|
||||
# This avoids the binary-all index duplication and -dev package
|
||||
# conflicts that plagued the Debian single-mirror approach.
|
||||
dpkg --add-architecture arm64
|
||||
sed -i 's|^deb http://archive.ubuntu.com|deb [arch=amd64] http://archive.ubuntu.com|g' /etc/apt/sources.list
|
||||
sed -i 's|^deb http://security.ubuntu.com|deb [arch=amd64] http://security.ubuntu.com|g' /etc/apt/sources.list
|
||||
printf '%s\n' \
|
||||
'deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports jammy main restricted universe multiverse' \
|
||||
'deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports jammy-updates main restricted universe multiverse' \
|
||||
'deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports jammy-security main restricted universe multiverse' \
|
||||
> /etc/apt/sources.list.d/arm64-ports.list
|
||||
apt-get update -qq
|
||||
|
||||
# Step 3: ARM64 dev libs — libayatana omitted (no tray icon in this app)
|
||||
apt-get install -y -qq \
|
||||
libwebkit2gtk-4.1-dev:arm64 \
|
||||
libssl-dev:arm64 \
|
||||
libgtk-3-dev:arm64 \
|
||||
librsvg2-dev:arm64
|
||||
|
||||
# Step 4: Node.js
|
||||
curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
|
||||
apt-get install -y nodejs
|
||||
|
||||
# Step 5: Rust (not pre-installed in ubuntu:22.04)
|
||||
# source "$HOME/.cargo/env" in the Build step handles PATH — no GITHUB_PATH needed
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y \
|
||||
--default-toolchain 1.88.0 --profile minimal --no-modify-path
|
||||
build-linux-arm64:
|
||||
needs: autotag
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ghcr.io/msicie/trcaa-linux-arm64:rust1.88-node22
|
||||
credentials:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- name: Cache cargo registry
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry/index
|
||||
~/.cargo/registry/cache
|
||||
~/.cargo/git/db
|
||||
key: ${{ runner.os }}-cargo-arm64-${{ hashFiles('**/Cargo.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-cargo-arm64-
|
||||
- name: Cache npm
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-npm-
|
||||
- name: Build
|
||||
env:
|
||||
CC_aarch64_unknown_linux_gnu: aarch64-linux-gnu-gcc
|
||||
@ -439,66 +485,25 @@ jobs:
|
||||
OPENSSL_STATIC: "1"
|
||||
APPIMAGE_EXTRACT_AND_RUN: "1"
|
||||
run: |
|
||||
. "$HOME/.cargo/env"
|
||||
npm ci --legacy-peer-deps
|
||||
rustup target add aarch64-unknown-linux-gnu
|
||||
CI=true npx tauri build --target aarch64-unknown-linux-gnu --bundles deb,rpm
|
||||
- name: Upload artifacts
|
||||
- name: Upload artifacts to GitHub release
|
||||
env:
|
||||
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
RELEASE_TAG: ${{ needs.autotag.outputs.release_tag }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_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"
|
||||
TAG="${RELEASE_TAG}"
|
||||
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."
|
||||
echo "ERROR: No Linux arm64 artifacts found."
|
||||
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
|
||||
NAME="linux-arm64-$(basename "$f")"
|
||||
echo "Uploading $NAME..."
|
||||
gh release delete-asset "$TAG" "$NAME" --yes 2>/dev/null || true
|
||||
gh release upload "$TAG" "$f#$NAME"
|
||||
echo "✓ Uploaded $NAME"
|
||||
done
|
||||
|
||||
89
.github/workflows/test.yml
vendored
89
.github/workflows/test.yml
vendored
@ -1,47 +1,53 @@
|
||||
name: Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- 'feature/**'
|
||||
- 'bug/**'
|
||||
- 'fix/**'
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
rust-fmt-check:
|
||||
rust-test:
|
||||
runs-on: ubuntu-latest
|
||||
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:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- run: rustup component add rustfmt
|
||||
- run: cargo fmt --manifest-path src-tauri/Cargo.toml --check
|
||||
|
||||
rust-clippy:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: rust:1.88-slim
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Cache cargo registry
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- run: apt-get update -qq && apt-get install -y -qq libwebkit2gtk-4.1-dev libssl-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev patchelf pkg-config perl
|
||||
- run: rustup component add clippy
|
||||
- run: cargo clippy --manifest-path src-tauri/Cargo.toml -- -D warnings
|
||||
path: |
|
||||
~/.cargo/registry/index
|
||||
~/.cargo/registry/cache
|
||||
~/.cargo/git/db
|
||||
key: ${{ runner.os }}-cargo-linux-amd64-${{ hashFiles('**/Cargo.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-cargo-linux-amd64-
|
||||
- name: Install npm dependencies
|
||||
run: npm install --legacy-peer-deps
|
||||
- name: Update version from Git
|
||||
run: node scripts/update-version.mjs
|
||||
- name: Generate lockfile
|
||||
run: cargo generate-lockfile --manifest-path src-tauri/Cargo.toml
|
||||
- name: Rust fmt check
|
||||
run: cargo fmt --manifest-path src-tauri/Cargo.toml --check
|
||||
- name: Rust clippy
|
||||
run: cargo clippy --manifest-path src-tauri/Cargo.toml -- -D warnings
|
||||
- name: Rust tests
|
||||
run: cargo test --manifest-path src-tauri/Cargo.toml -- --test-threads=1
|
||||
|
||||
rust-tests:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: rust:1.88-slim
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- run: apt-get update -qq && apt-get install -y -qq libwebkit2gtk-4.1-dev libssl-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev patchelf pkg-config perl
|
||||
- run: cargo test --manifest-path src-tauri/Cargo.toml
|
||||
|
||||
frontend-typecheck:
|
||||
frontend-test:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: node:22-alpine
|
||||
@ -50,17 +56,16 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- run: npm ci --legacy-peer-deps
|
||||
- run: npx tsc --noEmit
|
||||
|
||||
frontend-tests:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: node:22-alpine
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Cache npm
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- run: npm ci --legacy-peer-deps
|
||||
- run: npm run test:run
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-npm-
|
||||
- name: Install dependencies
|
||||
run: npm ci --legacy-peer-deps
|
||||
- name: TypeScript type check
|
||||
run: npx tsc --noEmit
|
||||
- name: Run frontend tests
|
||||
run: npm run test:run
|
||||
|
||||
26
CLAUDE.md
26
CLAUDE.md
@ -77,8 +77,9 @@ cargo tauri build # Outputs to src-tauri/target/release/bundle/
|
||||
|
||||
### CI/CD
|
||||
|
||||
- **Test pipeline**: `.woodpecker/test.yml` — runs on every push/PR
|
||||
- **Release pipeline**: `.woodpecker/release.yml` — runs on `v*` tags, produces Linux amd64+arm64 bundles, uploads to Gogs release at `http://172.0.0.29:3000/api/v1`
|
||||
- **Test pipeline**: `.github/workflows/test.yml` — runs on every push/PR targeting `main`
|
||||
- **Release pipeline**: `.github/workflows/release.yml` — runs on every push to `main`, auto-tags, produces multi-platform bundles (Linux amd64+arm64, Windows, macOS arm64+Intel), uploads to GitHub Releases at `https://github.com/msicie/apollo_nxt-trcaa/releases`
|
||||
- **Docker builder images**: `.github/workflows/build-images.yml` — rebuilds `ghcr.io/msicie/trcaa-*` images when `.docker/**` changes on `main`
|
||||
|
||||
---
|
||||
|
||||
@ -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`.
|
||||
|
||||
### 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:
|
||||
- `token.ParseRequest()` does not read `?token=` URL params (only `Authorization` header and `user_sess` cookie)
|
||||
- The SPA login form uses `login=` field; Gogs backend reads `username=` — a custom login page is served by nginx at `/login` and `/login/form`
|
||||
- Gogs 0.14 has no OAuth2 provider support, blocking upgrade to Woodpecker 2.x
|
||||
|
||||
Gogs token quirk: the `sha1` value returned by `POST /api/v1/users/{user}/tokens` is the **actual bearer token**. The `sha1` and `sha256` columns in the Gogs DB are hashes of that token, not the token itself.
|
||||
- `GITHUB_TOKEN` is the only credential needed — no external secrets required
|
||||
- Builder images are hosted on `ghcr.io/msicie/` (GitHub Container Registry)
|
||||
- Branch protection on `main` requires `rust-test` and `frontend-test` checks to pass, plus Copilot code review, before merging
|
||||
|
||||
---
|
||||
|
||||
## 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:**
|
||||
|
||||
@ -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` |
|
||||
| New/changed AI provider (`ai/*.rs`) | `docs/wiki/AI-Providers.md` |
|
||||
| PII patterns or detection logic (`pii/`) | `docs/wiki/PII-Detection.md` |
|
||||
| CI/CD pipeline changes (`.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` |
|
||||
| Security-relevant changes (capabilities, audit, Stronghold) | `docs/wiki/Security-Model.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:
|
||||
```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:
|
||||
git add -A && git commit -m "docs: ..." && git push
|
||||
```
|
||||
|
||||
21
Makefile
21
Makefile
@ -1,10 +1,9 @@
|
||||
GOGS_API := http://172.0.0.29:3000/api/v1
|
||||
GOGS_REPO := sarman/tftsr-devops_investigation
|
||||
GH_REPO := msicie/apollo_nxt-trcaa
|
||||
TAG ?= v0.1.0-alpha
|
||||
TARGET := aarch64-unknown-linux-gnu
|
||||
|
||||
# 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
|
||||
release-arm64: build-arm64 upload-arm64
|
||||
|
||||
@ -35,15 +34,11 @@ build-arm64:
|
||||
|
||||
.PHONY: upload-arm64
|
||||
upload-arm64:
|
||||
@test -n "$(GOGS_TOKEN)" || (echo "ERROR: set GOGS_TOKEN env var"; exit 1)
|
||||
@RELEASE_ID=$$(curl -sf "$(GOGS_API)/repos/$(GOGS_REPO)/releases/tags/$(TAG)" \
|
||||
-H "Authorization: token $(GOGS_TOKEN)" | \
|
||||
grep -o '"id":[0-9]*' | head -1 | cut -d: -f2); \
|
||||
echo "Release ID: $$RELEASE_ID"; \
|
||||
for f in artifacts/linux-arm64/*; do \
|
||||
@test -n "$(GH_TOKEN)" || (echo "ERROR: set GH_TOKEN env var"; exit 1)
|
||||
@for f in artifacts/linux-arm64/*; do \
|
||||
[ -f "$$f" ] || continue; \
|
||||
echo "Uploading $$f..."; \
|
||||
curl -sf -X POST "$(GOGS_API)/repos/$(GOGS_REPO)/releases/$$RELEASE_ID/assets" \
|
||||
-H "Authorization: token $(GOGS_TOKEN)" \
|
||||
-F "attachment=@$$f;filename=$$(basename $$f)" && echo "OK" || echo "FAIL: $$f"; \
|
||||
NAME="linux-arm64-$$(basename $$f)"; \
|
||||
echo "Uploading $$NAME..."; \
|
||||
GH_TOKEN=$(GH_TOKEN) gh release upload $(TAG) "$$f#$$NAME" \
|
||||
--repo $(GH_REPO) && echo "OK" || echo "FAIL: $$f"; \
|
||||
done
|
||||
|
||||
Loading…
Reference in New Issue
Block a user