From 1023d7a944694c6e849e8827e4d522ee4fc827cf Mon Sep 17 00:00:00 2001 From: Shaun Arman Date: Mon, 1 Jun 2026 14:03:33 -0500 Subject: [PATCH] ci: migrate CI/CD from Gogs/Gitea to GitHub Actions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- .gitea/workflows/auto-tag.yml | 687 ----------------------------- .gitea/workflows/build-images.yml | 104 ----- .gitea/workflows/pr-review.yml | 333 -------------- .gitea/workflows/test.yml | 186 -------- .github/CODEOWNERS | 9 + .github/workflows/build-images.yml | 71 ++- .github/workflows/release.yml | 611 ++++++++++++------------- .github/workflows/test.yml | 89 ++-- CLAUDE.md | 26 +- Makefile | 21 +- 10 files changed, 414 insertions(+), 1723 deletions(-) delete mode 100644 .gitea/workflows/auto-tag.yml delete mode 100644 .gitea/workflows/build-images.yml delete mode 100644 .gitea/workflows/pr-review.yml delete mode 100644 .gitea/workflows/test.yml create mode 100644 .github/CODEOWNERS diff --git a/.gitea/workflows/auto-tag.yml b/.gitea/workflows/auto-tag.yml deleted file mode 100644 index 4e043d42..00000000 --- a/.gitea/workflows/auto-tag.yml +++ /dev/null @@ -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 diff --git a/.gitea/workflows/build-images.yml b/.gitea/workflows/build-images.yml deleted file mode 100644 index 5bf6abc3..00000000 --- a/.gitea/workflows/build-images.yml +++ /dev/null @@ -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" diff --git a/.gitea/workflows/pr-review.yml b/.gitea/workflows/pr-review.yml deleted file mode 100644 index 5d3c39cd..00000000 --- a/.gitea/workflows/pr-review.yml +++ /dev/null @@ -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 diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml deleted file mode 100644 index 3f91a3bc..00000000 --- a/.gitea/workflows/test.yml +++ /dev/null @@ -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 diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..c61d349a --- /dev/null +++ b/.github/CODEOWNERS @@ -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 diff --git a/.github/workflows/build-images.yml b/.github/workflows/build-images.yml index c54aaa8f..488261f2 100644 --- a/.github/workflows/build-images.yml +++ b/.github/workflows/build-images.yml @@ -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" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f8939132..4f9c8539 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1271c87c..f06293da 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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 diff --git a/CLAUDE.md b/CLAUDE.md index de64b60d..1dcd0c5c 100644 --- a/CLAUDE.md +++ b/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 ``` diff --git a/Makefile b/Makefile index 1c743d4e..60cf5044 100644 --- a/Makefile +++ b/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