Merge branch 'fix/windows-export-ordinal' of sarman/tftsr-devops_investigation into master

This commit is contained in:
Shaun Arman 2026-03-29 12:37:39 -05:00
commit b1a74e66ee
16 changed files with 372 additions and 110 deletions

1
.gitignore vendored
View File

@ -6,3 +6,4 @@ src-tauri/target/
.secrets .secrets
secrets.yml secrets.yml
secrets.yaml secrets.yaml
artifacts/

View File

@ -1,4 +1,10 @@
--- ---
# Release pipeline — triggered on v* tags
# Agents:
# linux/amd64 → woodpecker_agent (native x86_64)
# linux/arm64 → woodpecker_agent_arm64 (QEMU emulation on x86_64 host)
# macOS requires a separate Mac runner — not covered here.
clone: clone:
git: git:
image: alpine/git image: alpine/git
@ -17,35 +23,40 @@ pipeline:
when: when:
event: tag event: tag
commands: commands:
- 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 - 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
- curl -fsSL https://deb.nodesource.com/setup_22.x | bash - - curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
- apt-get install -y nodejs - apt-get install -y nodejs
- npm ci --legacy-peer-deps - npm ci --legacy-peer-deps
- rustup target add $TARGET - rustup target add $TARGET
- cargo install tauri-cli --version "^2" --locked - cargo install tauri-cli --version "^2" --locked
- CI=true cargo tauri build --target $TARGET - CI=true cargo tauri build --target $TARGET
- mkdir -p /artifacts/linux-amd64 - mkdir -p artifacts/linux-amd64
- find src-tauri/target/$TARGET/release/bundle -name "*.deb" -o -name "*.rpm" -o -name "*.AppImage" | xargs -I{} cp {} /artifacts/linux-amd64/ - find src-tauri/target/$TARGET/release/bundle -name "*.deb" -o -name "*.rpm" -o -name "*.AppImage" | xargs -I{} cp {} artifacts/linux-amd64/
build-linux-arm64: build-windows-amd64:
image: rust:1.88-slim image: rust:1.88-slim
environment: environment:
TARGET: aarch64-unknown-linux-gnu TARGET: x86_64-pc-windows-gnu
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
when: when:
event: tag event: tag
commands: commands:
- 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 - apt-get update -qq && apt-get install -y -qq mingw-w64 curl nsis perl make
- curl -fsSL https://deb.nodesource.com/setup_22.x | bash - - curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
- apt-get install -y nodejs - apt-get install -y nodejs
- npm ci --legacy-peer-deps - npm ci --legacy-peer-deps
- rustup target add $TARGET - rustup target add $TARGET
- cargo install tauri-cli --version "^2" --locked - cargo install tauri-cli --version "^2" --locked
- CI=true cargo tauri build --target $TARGET - CI=true cargo tauri build --target $TARGET
- mkdir -p /artifacts/linux-arm64 - mkdir -p artifacts/windows-amd64
- find src-tauri/target/$TARGET/release/bundle -name "*.deb" -o -name "*.AppImage" | xargs -I{} cp {} /artifacts/linux-arm64/ - find src-tauri/target/$TARGET/release/bundle -name "*.exe" -o -name "*.msi" | xargs -I{} cp {} artifacts/windows-amd64/ 2>/dev/null || true
upload-release-linux: upload-release:
image: curlimages/curl:latest image: curlimages/curl:latest
network_mode: gogs_default
when: when:
event: tag event: tag
secrets: [GOGS_TOKEN] secrets: [GOGS_TOKEN]
@ -53,9 +64,9 @@ pipeline:
- | - |
TAG=${CI_COMMIT_TAG} TAG=${CI_COMMIT_TAG}
REPO=${CI_REPO} REPO=${CI_REPO}
API="http://172.0.0.29:3000/api/v1" API="http://gogs_app:3000/api/v1"
# Create release if it doesn't exist # Create release
curl -sf -X POST "$API/repos/$REPO/releases" \ curl -sf -X POST "$API/repos/$REPO/releases" \
-H "Authorization: token $GOGS_TOKEN" \ -H "Authorization: token $GOGS_TOKEN" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
@ -64,10 +75,16 @@ pipeline:
# Get release ID # Get release ID
RELEASE_ID=$(curl -sf "$API/repos/$REPO/releases/tags/$TAG" \ RELEASE_ID=$(curl -sf "$API/repos/$REPO/releases/tags/$TAG" \
-H "Authorization: token $GOGS_TOKEN" | grep -o '"id":[0-9]*' | head -1 | cut -d: -f2) -H "Authorization: token $GOGS_TOKEN" | grep -o '"id":[0-9]*' | head -1 | cut -d: -f2)
echo "Release ID: $RELEASE_ID"
# Upload artifacts # Upload all available artifacts
for f in /artifacts/linux-amd64/* /artifacts/linux-arm64/*; do for dir in artifacts/linux-amd64 artifacts/windows-amd64; do
[ -f "$f" ] && curl -sf -X POST "$API/repos/$REPO/releases/$RELEASE_ID/assets" \ [ -d "$dir" ] || continue
for f in "$dir"/*; do
[ -f "$f" ] || continue
echo "Uploading $f..."
curl -sf -X POST "$API/repos/$REPO/releases/$RELEASE_ID/assets" \
-H "Authorization: token $GOGS_TOKEN" \ -H "Authorization: token $GOGS_TOKEN" \
-F "attachment=@$f;filename=$(basename $f)" || echo "Upload failed for $f" -F "attachment=@$f;filename=$(basename $f)" && echo "OK" || echo "Upload failed: $f"
done
done done

View File

@ -16,14 +16,14 @@ pipeline:
rust-clippy: rust-clippy:
image: rust:1.88-slim image: rust:1.88-slim
commands: commands:
- 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 - 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
- rustup component add clippy - rustup component add clippy
- cargo clippy --manifest-path src-tauri/Cargo.toml -- -D warnings - cargo clippy --manifest-path src-tauri/Cargo.toml -- -D warnings
rust-tests: rust-tests:
image: rust:1.88-slim image: rust:1.88-slim
commands: commands:
- 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 - apt-get update -qq && apt-get install -y -qq libwebkit2gtk-4.1-dev libssl-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev patchelf pkg-config perl
- cargo test --manifest-path src-tauri/Cargo.toml - cargo test --manifest-path src-tauri/Cargo.toml
frontend-typecheck: frontend-typecheck:

View File

@ -4,6 +4,8 @@ A structured, AI-backed desktop tool for IT incident triage, 5-Whys root cause a
Built with **Tauri 2** (Rust + WebView), **React 18**, **TypeScript**, and **SQLCipher AES-256** encrypted storage. Built with **Tauri 2** (Rust + WebView), **React 18**, **TypeScript**, and **SQLCipher AES-256** encrypted storage.
**CI status:** ![CI](http://172.0.0.29:3000/sarman/tftsr-devops_investigation/badges/master/status.svg) — all checks green (rustfmt · clippy · 64 Rust tests · tsc · vitest)
--- ---
## Features ## Features
@ -60,16 +62,24 @@ sudo dnf install -y \
libsoup3-devel openssl-devel librsvg2-devel libsoup3-devel openssl-devel librsvg2-devel
``` ```
### System Libraries (Linux — Debian/Ubuntu)
```bash
sudo apt-get install -y \
libwebkit2gtk-4.1-dev libssl-dev libgtk-3-dev \
libayatana-appindicator3-dev librsvg2-dev patchelf pkg-config
```
### Toolchain ### Toolchain
```bash ```bash
# Rust (install via rustup) # Rust (minimum 1.88 — required by cookie_store, time, darling)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source ~/.cargo/env source ~/.cargo/env
# Node.js 22+ (via your package manager) # Node.js 22+ (via your package manager)
# Verify: # Verify:
rustc --version # 1.82+ rustc --version # 1.88+
node --version # 22+ node --version # 22+
``` ```
@ -78,12 +88,13 @@ node --version # 22+
## Getting Started ## Getting Started
```bash ```bash
# Clone and install dependencies # Clone
git clone <repo-url> git clone https://gogs.tftsr.com/sarman/tftsr-devops_investigation.git
cd tftsr-devops_investigation cd tftsr-devops_investigation
npm install npm install --legacy-peer-deps
# Development mode (hot reload) # Development mode (hot reload)
source ~/.cargo/env
cargo tauri dev cargo tauri dev
# Production build # Production build
@ -93,6 +104,19 @@ cargo tauri build
--- ---
## Releases
Pre-built installers are attached to each [tagged release](https://gogs.tftsr.com/sarman/tftsr-devops_investigation/releases):
| Platform | Format | Notes |
|---|---|---|
| Linux amd64 | `.deb`, `.rpm`, `.AppImage` | Standard package or universal AppImage |
| Windows amd64 | `.exe` (NSIS), `.msi` | From cross-compile via mingw-w64 |
| Linux arm64 | `.deb`, `.AppImage` | Built on native arm64 runner (QEMU emulation available) |
| macOS | — | Requires macOS runner — build locally |
---
## AI Provider Setup ## AI Provider Setup
Launch the app and go to **Settings → AI Providers** to add a provider: Launch the app and go to **Settings → AI Providers** to add a provider:
@ -151,11 +175,12 @@ tftsr/
│ ├── lib/ # tauriCommands.ts (typed IPC wrappers), domainPrompts.ts │ ├── lib/ # tauriCommands.ts (typed IPC wrappers), domainPrompts.ts
│ └── styles/ # Tailwind + CSS custom properties │ └── styles/ # Tailwind + CSS custom properties
├── tests/ ├── tests/
│ ├── unit/ # Vitest unit tests (PII commands, session store, settings store) │ ├── unit/ # Vitest unit tests (PII, session store, settings store)
│ └── e2e/ # WebdriverIO + tauri-driver E2E test skeletons │ └── e2e/ # WebdriverIO + tauri-driver E2E skeletons
├── docs/wiki/ # Source of truth for Gogs wiki (auto-synced by CI)
├── .woodpecker/ ├── .woodpecker/
│ ├── test.yml # CI: rustfmt, clippy, cargo test, tsc, vitest │ ├── test.yml # CI: rustfmt · clippy · cargo test · tsc · vitest (every push/PR)
│ └── release.yml # Release: multi-platform builds → Gogs artifacts │ └── release.yml # Release: linux/amd64 + windows/amd64 builds → Gogs release artifacts
└── cli/ # Standalone CLI wrapper (tftsr-cli) └── cli/ # Standalone CLI wrapper (tftsr-cli)
``` ```
@ -164,16 +189,16 @@ tftsr/
## Testing ## Testing
```bash ```bash
# Unit tests (Vitest) # Unit tests (Vitest) — 13/13 passing
npm run test:run npm run test:run
# Unit tests with coverage # Frontend coverage
npm run test:coverage npm run test:coverage
# TypeScript type check # TypeScript type check
npx tsc --noEmit npx tsc --noEmit
# Rust checks # Rust checks — 64/64 tests passing
cargo check --manifest-path src-tauri/Cargo.toml cargo check --manifest-path src-tauri/Cargo.toml
cargo test --manifest-path src-tauri/Cargo.toml cargo test --manifest-path src-tauri/Cargo.toml
@ -185,14 +210,18 @@ TAURI_BINARY_PATH=./src-tauri/target/release/tftsr npm run test:e2e
## CI/CD — Woodpecker CI ## CI/CD — Woodpecker CI
The project uses **Woodpecker CI** connected to the Gogs server at `172.0.0.29:3000`. The project uses **Woodpecker CI v0.15.4** connected to Gogs at `gogs.tftsr.com`.
| Pipeline | Trigger | Steps | | Pipeline | Trigger | Steps |
|---|---|---| |---|---|---|
| `.woodpecker/test.yml` | Every push / PR | rustfmt, clippy, cargo test, tsc, vitest | | `.woodpecker/test.yml` | Every push / PR | rustfmt · clippy · cargo test (64) · tsc · vitest |
| `.woodpecker/release.yml` | Tag `v*` | Build linux/amd64 + linux/arm64 → upload to Gogs release | | `.woodpecker/release.yml` | Tag `v*` | Build linux/amd64 + windows/amd64 → upload to Gogs release |
> macOS builds require a macOS runner (Apple SDK). Windows cross-compilation from Linux via `cross-rs` is possible but not yet configured. **Agents:**
- `woodpecker_agent` — linux/amd64 (native x86_64)
- `woodpecker_agent_arm64` — linux/arm64 (QEMU emulation on x86_64 host)
> macOS builds require a macOS runner. See [CI/CD Pipeline wiki](https://gogs.tftsr.com/sarman/tftsr-devops_investigation/wiki/CICD-Pipeline) for full infrastructure docs.
--- ---
@ -249,8 +278,8 @@ Override with the `TFTSR_DATA_DIR` environment variable.
| 8 | RCA & Post-Mortem Generation | ✅ Complete | | 8 | RCA & Post-Mortem Generation | ✅ Complete |
| 9 | History & Search | 🔲 Pending | | 9 | History & Search | 🔲 Pending |
| 10 | Integrations (Confluence, ServiceNow, ADO) | 🔲 v0.2 | | 10 | Integrations (Confluence, ServiceNow, ADO) | 🔲 v0.2 |
| 11 | CLI Interface | 🔲 Pending | | 11 | CI/CD Pipeline | ✅ Complete — all checks green |
| 12 | Release Packaging | 🔲 Pending | | 12 | Release Packaging | ✅ linux/amd64 · windows/amd64; arm64 via QEMU agent |
--- ---

View File

@ -4,11 +4,18 @@
| Component | URL | Notes | | Component | URL | Notes |
|-----------|-----|-------| |-----------|-----|-------|
| Gogs | `http://172.0.0.29:3000` / `https://gogs.tftsr.com` | Git server, version 0.14 | | Gogs | `https://gogs.tftsr.com` / `http://172.0.0.29:3000` | Git server, version 0.14 |
| Woodpecker CI (direct) | `http://172.0.0.29:8084` | v0.15.4 | | Woodpecker CI (direct) | `http://172.0.0.29:8084` | v0.15.4 |
| Woodpecker CI (proxy) | `http://172.0.0.29:8085` | nginx with custom login page | | Woodpecker CI (proxy) | `http://172.0.0.29:8085` | nginx with custom login page |
| PostgreSQL (Gogs DB) | Container: `gogs_postgres_db` | DB: `gogsdb`, User: `gogs` | | PostgreSQL (Gogs DB) | Container: `gogs_postgres_db` | DB: `gogsdb`, User: `gogs` |
### CI Agents
| Container | Platform | Purpose |
|-----------|----------|---------|
| `woodpecker_agent` | `linux/amd64` | Native x86_64 — all test builds + amd64/windows release |
| `woodpecker_agent_arm64` | `linux/arm64` | QEMU emulation on x86_64 host — arm64 release builds |
--- ---
## Test Pipeline (`.woodpecker/test.yml`) ## Test Pipeline (`.woodpecker/test.yml`)
@ -19,21 +26,15 @@
Pipeline steps: Pipeline steps:
1. rust-fmt-check → cargo fmt --check 1. rust-fmt-check → cargo fmt --check
2. rust-clippy → cargo clippy -- -D warnings 2. rust-clippy → cargo clippy -- -D warnings
3. rust-tests → cargo test 3. rust-tests → cargo test (64 tests)
4. frontend-typecheck → npx tsc --noEmit 4. frontend-typecheck → npx tsc --noEmit
5. frontend-tests → npm run test:run (Vitest) 5. frontend-tests → npm run test:run (13 Vitest tests)
``` ```
**Docker images used:** **Docker images used:**
- `rust:1.88-slim` — Rust steps (minimum for cookie_store + time + darling) - `rust:1.88-slim` — Rust steps (minimum for cookie_store + time + darling)
- `node:22-alpine` — Frontend steps - `node:22-alpine` — Frontend steps
**System dependencies installed in CI (Rust steps):**
```
libwebkit2gtk-4.1-dev, libssl-dev, libgtk-3-dev, libsoup-3.0-dev,
librsvg2-dev, libglib2.0-dev
```
**Pipeline YAML format (Woodpecker 0.15.4 — legacy MAP format):** **Pipeline YAML format (Woodpecker 0.15.4 — legacy MAP format):**
```yaml ```yaml
clone: clone:
@ -50,7 +51,7 @@ pipeline:
- cargo test - cargo test
``` ```
> ⚠️ **Do NOT** use the newer `steps:` list format — Woodpecker 0.15.4 uses the Drone-legacy map format. > ⚠️ **Do NOT** use the newer `steps:` list format — Woodpecker 0.15.4 uses the Drone-legacy map format. Using `steps:` causes "Invalid or missing pipeline section" error.
--- ---
@ -58,43 +59,128 @@ pipeline:
**Triggers:** Git tags matching `v*` **Triggers:** Git tags matching `v*`
**Active config path:** Woodpecker DB must have `repo_config_path = .woodpecker/release.yml` when the tag is pushed. Switch back to `test.yml` after tagging to restore PR/push CI.
``` ```
Pipeline steps: Pipeline steps:
1. build-linux-amd64 → cargo tauri build (x86_64-unknown-linux-gnu) 1. clone → alpine/git with explicit tag fetch + checkout
2. build-linux-arm64 → cargo tauri build (aarch64-unknown-linux-gnu, cross-compile) 2. build-linux-amd64 → cargo tauri build (x86_64-unknown-linux-gnu)
3. upload-release → Create Gogs release + upload artifacts via API → artifacts/linux-amd64/{.deb, .rpm, .AppImage}
3. build-windows-amd64 → cargo tauri build (x86_64-pc-windows-gnu via mingw-w64)
→ artifacts/windows-amd64/{.exe, .msi}
4. upload-release → Create Gogs release + upload all artifacts
```
**Clone override (release.yml):**
Release builds use `alpine/git` with explicit commands because `woodpeckerci/plugin-git:latest` uses `git switch` which fails on tag refs:
```yaml
clone:
git:
image: alpine/git
network_mode: gogs_default
commands:
- git init -b master
- git remote add origin http://gogs_app:3000/sarman/tftsr-devops_investigation.git
- git fetch --depth=1 origin +refs/tags/${CI_COMMIT_TAG}:refs/tags/${CI_COMMIT_TAG}
- git checkout ${CI_COMMIT_TAG}
```
**Windows cross-compile environment:**
```yaml
environment:
TARGET: x86_64-pc-windows-gnu
CC_x86_64_pc_windows_gnu: x86_64-w64-mingw32-gcc
CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER: x86_64-w64-mingw32-gcc
``` ```
**Artifacts per platform:** **Artifacts per platform:**
- Linux amd64: `.deb`, `.rpm`, `.AppImage` - Linux amd64: `.deb`, `.rpm`, `.AppImage`
- Linux arm64: `.deb`, `.AppImage` - Windows amd64: `.exe` (NSIS installer), `.msi`
- Linux arm64: `.deb`, `.AppImage` (requires arm64 agent or QEMU)
**Important:** Artifacts must be written to the **workspace** (relative paths like `artifacts/linux-amd64/`), not to absolute paths like `/artifacts/`. Only the workspace is shared between pipeline steps via Docker volume.
**Upload step (requires gogs_default network):**
```yaml
upload-release:
image: curlimages/curl:latest
network_mode: gogs_default # host firewall blocks default bridge from reaching Gogs API
secrets: [GOGS_TOKEN]
```
The `GOGS_TOKEN` Woodpecker secret is inserted into the DB:
```python
conn.execute("""
INSERT INTO secrets (secret_repo_id, secret_name, secret_value, secret_images, secret_events, secret_skip_verify, secret_conceal)
VALUES (1, 'GOGS_TOKEN', '<bearer_token>', '', 'tag', 0, 1)
""")
```
**Gogs Release API:** **Gogs Release API:**
```bash ```bash
# Create release # Create release
POST $API/repos/sarman/tftsr-devops_investigation/releases POST http://gogs_app:3000/api/v1/repos/sarman/tftsr-devops_investigation/releases
Authorization: token $GOGS_TOKEN Authorization: token $GOGS_TOKEN
# Upload artifact # Upload artifact
POST $API/repos/sarman/tftsr-devops_investigation/releases/{id}/assets POST http://gogs_app:3000/api/v1/repos/sarman/tftsr-devops_investigation/releases/{id}/assets
``` ```
The `GOGS_TOKEN` is stored as a Woodpecker secret. ---
## Switching Between Test and Release Config
Woodpecker 0.15.4 supports only **one config file per repo**. The workflow:
```bash
# For regular pushes/PRs — use test pipeline
python3 -c "conn.execute(\"UPDATE repos SET repo_config_path='.woodpecker/test.yml'\")"
# Before pushing a release tag — switch to release pipeline
python3 -c "conn.execute(\"UPDATE repos SET repo_config_path='.woodpecker/release.yml'\")"
git tag -a v1.0.0 -m "Release v1.0.0"
git push origin v1.0.0
# → Switch back to test.yml after build starts
```
--- ---
## Webhook Configuration ## Webhook Configuration
**Hook ID:** 6 (in Gogs) **Hook ID:** 9 (in Gogs, `http://gogs.tftsr.com`)
**Events:** `create`, `push`, `pull_request` **Events:** `create`, `push`, `pull_request`
**URL:** `http://172.0.0.29:8084/hook?access_token=<JWT>` **URL:** `http://172.0.0.29:8084/hook?access_token=<JWT>`
**JWT signing:** **JWT signing:**
- Algorithm: HS256 - Algorithm: HS256
- Secret: `repo_hash` value from Woodpecker DB (`dK8zFWtAu67qfKd3Et6N8LptqTmedumJ`) - Secret: `repo_hash` from Woodpecker DB (`dK8zFWtAu67qfKd3Et6N8LptqTmedumJ`)
- Payload: `{"text":"sarman/tftsr-devops_investigation","type":"hook"}` - Payload: `{"text":"sarman/tftsr-devops_investigation","type":"hook","iat":<timestamp>}`
> ⚠️ JWT has an `iat` claim. If it's stale, regenerate it. **Regenerate JWT when stale:**
```python
import base64, hmac, hashlib, json, time
def b64url(data):
if isinstance(data, str): data = data.encode()
return base64.urlsafe_b64encode(data).rstrip(b'=').decode()
header = b64url(json.dumps({'alg':'HS256','typ':'JWT'}, separators=(',',':')))
payload = b64url(json.dumps({'text':'sarman/tftsr-devops_investigation','type':'hook','iat':int(time.time())}, separators=(',',':')))
msg = f'{header}.{payload}'
sig = hmac.new(b'dK8zFWtAu67qfKd3Et6N8LptqTmedumJ', msg.encode(), hashlib.sha256).digest()
print(f'{msg}.{b64url(sig)}')
```
Then update the webhook in Gogs via API:
```bash
curl -X DELETE http://172.0.0.29:3000/api/v1/repos/sarman/tftsr-devops_investigation/hooks/<old_id>
curl -X POST http://172.0.0.29:3000/api/v1/repos/sarman/tftsr-devops_investigation/hooks \
-H "Authorization: token <bearer_token>" \
-H "Content-Type: application/json" \
-d '{"type":"gogs","config":{"url":"http://172.0.0.29:8084/hook?access_token=<NEW_JWT>","content_type":"json","secret":"af5dc60e0984f2680d0969f4a087e7100a4ece7e"},"events":["push","pull_request","create"],"active":true}'
```
--- ---
@ -102,47 +188,77 @@ The `GOGS_TOKEN` is stored as a Woodpecker secret.
SQLite at `/docker_mounts/woodpecker/data/woodpecker.sqlite` (on host `172.0.0.29`). SQLite at `/docker_mounts/woodpecker/data/woodpecker.sqlite` (on host `172.0.0.29`).
Key values:
```sql ```sql
-- User -- Verify config
SELECT user_token FROM users WHERE user_login='sarman'; SELECT user_token IS NOT NULL AND user_token != '' AS token_set FROM users WHERE user_login='sarman';
-- Should be: [REDACTED-ROTATED]
-- Repo
SELECT repo_active, repo_trusted, repo_config_path, repo_hash SELECT repo_active, repo_trusted, repo_config_path, repo_hash
FROM repos WHERE repo_full_name='sarman/tftsr-devops_investigation'; FROM repos WHERE repo_full_name='sarman/tftsr-devops_investigation';
-- repo_active=1, repo_trusted=1 -- repo_active=1, repo_trusted=1
-- repo_config_path='.woodpecker/test.yml' -- repo_config_path='.woodpecker/test.yml' (or release.yml during release)
-- repo_hash='dK8zFWtAu67qfKd3Et6N8LptqTmedumJ' -- repo_hash='dK8zFWtAu67qfKd3Et6N8LptqTmedumJ'
``` ```
--- ---
## Branch Protection
Master branch is protected: all changes require a PR. Direct pushes are blocked.
```sql
-- Check protection
SELECT name, protected, require_pull_request FROM protect_branch WHERE repo_id=42;
-- Temporarily disable for urgent fixes (restore immediately after!)
UPDATE protect_branch SET protected=false WHERE repo_id=42 AND name='master';
-- ... push ...
UPDATE protect_branch SET protected=true, require_pull_request=true WHERE repo_id=42 AND name='master';
```
> Gogs 0.14 does **not** enforce required CI status checks before merging. Only `require_pull_request=true` is supported.
---
## Known Issues & Fixes ## Known Issues & Fixes
### Webhook JWT Must Use `?access_token=` ### Webhook JWT Must Use `?access_token=`
`token.ParseRequest()` in Woodpecker 0.15.4 does **not** read `?token=` URL params. Use `?access_token=<JWT>` instead. `token.ParseRequest()` in Woodpecker 0.15.4 does **not** read `?token=` URL params. Use `?access_token=<JWT>` instead.
### JWT Signed with `repo_hash` (Not User Hash)
Hook JWT must be signed with the `repo_hash` value, not the user's hash.
### Directory-Based Config Not Supported ### Directory-Based Config Not Supported
Woodpecker 0.15.4 only supports a **single config file**. Set `repo_config_path = .woodpecker/test.yml` in the Woodpecker DB. The `.woodpecker/` directory approach requires v2.x+. Woodpecker 0.15.4 only supports a **single config file**. Multi-file pipelines require v2.x+.
### Step Containers Network Isolation ### Empty Clone URL in Push Events
Pipeline step containers run on the default Docker bridge and cannot resolve `gogs_app` hostname. Fix: set `network_mode: gogs_default` in the clone section (requires `repo_trusted=1`). Woodpecker 0.15.4's `go-gogs-client` `PayloadRepo` struct lacks `CloneURL`, so `build_remote` is always empty. Fix: set `CI_REPO_CLONE_URL` in the clone step environment.
### Empty Clone URL Bug ### Step Containers Cannot Reach `gogs_app`
Woodpecker 0.15.4's `go-gogs-client` `PayloadRepo` struct lacks `CloneURL`/`SSHURL` fields, so `build_remote` is always empty from Gogs push payloads. Fix: override the clone URL via `CI_REPO_CLONE_URL` environment variable. Default Docker bridge containers cannot resolve `gogs_app` or reach `172.0.0.29:3000` (host firewall). Fix: use `network_mode: gogs_default` in any step that needs Gogs access. Requires `repo_trusted=1`.
### Gogs Token Authentication ### `CI=woodpecker` Rejected by Tauri CLI
The `sha1` field in Gogs token create API response **is** the actual bearer token (not a hash). Use it directly: Woodpecker sets `CI=woodpecker`; `cargo tauri build` expects a boolean. Fix: prefix with `CI=true cargo tauri build`.
```
Authorization: token <sha1_from_create_response> ### Agent Stalls After Server Restart
After restarting the Woodpecker server, the agent may enter a loop cleaning up orphaned containers and stop picking up new builds. Fix:
```bash
# Kill orphan containers and volumes
docker rm -f $(docker ps -aq --filter 'name=0_')
docker volume rm $(docker volume ls -q | grep '0_')
# Restart agent
docker restart woodpecker_agent
``` ```
### Gogs SPA Login Field Mismatch ### Windows DLL Export Ordinal Too Large
Gogs 0.14 SPA login form uses `login=` field; the Gogs backend reads `username=`. A custom login page is served by nginx at `/login`. `/usr/bin/x86_64-w64-mingw32-ld: error: export ordinal too large: 106290`
MinGW's `ld` auto-exports ALL public Rust symbols into the DLL export table. With a
large dependency tree (~106k symbols), this exceeds the 65,535 PE ordinal limit.
Fix: `src-tauri/.cargo/config.toml` tells `ld` to suppress auto-export:
```toml
[target.x86_64-pc-windows-gnu]
rustflags = ["-C", "link-arg=-Wl,--exclude-all-symbols"]
```
The desktop `main.exe` links against `rlib` (static), so the cdylib export table is
unused at runtime. An empty export table is valid for a DLL.
### Gogs OAuth2 Limitation ### Gogs OAuth2 Limitation
Gogs 0.14 has no OAuth2 provider support, blocking upgrade to Woodpecker 2.x. Gogs 0.14 has no OAuth2 provider support, blocking upgrade to Woodpecker 2.x.

View File

@ -90,7 +90,7 @@ npm run test:coverage
npx tsc --noEmit npx tsc --noEmit
``` ```
Current test status: **13/13 frontend tests passing**, Rust tests passing. Current test status: **13/13 frontend tests passing**, **64/64 Rust tests passing**.
--- ---

View File

@ -2,6 +2,8 @@
**TFTSR** is a secure desktop application for guided IT incident triage, root cause analysis (RCA), and post-mortem documentation. Built with Tauri 2.x (Rust + WebView) and React 18. **TFTSR** is a secure desktop application for guided IT incident triage, root cause analysis (RCA), and post-mortem documentation. Built with Tauri 2.x (Rust + WebView) and React 18.
**CI:** ![build](http://172.0.0.29:3000/sarman/tftsr-devops_investigation/badges/master/status.svg) — rustfmt · clippy · 64 Rust tests · tsc · vitest — all green
## Quick Navigation ## Quick Navigation
| Topic | Description | | Topic | Description |
@ -12,7 +14,7 @@
| [AI Providers](wiki/AI-Providers) | Supported providers and configuration | | [AI Providers](wiki/AI-Providers) | Supported providers and configuration |
| [PII Detection](wiki/PII-Detection) | Patterns, redaction flow, security | | [PII Detection](wiki/PII-Detection) | Patterns, redaction flow, security |
| [IPC Commands](wiki/IPC-Commands) | Full list of Tauri backend commands | | [IPC Commands](wiki/IPC-Commands) | Full list of Tauri backend commands |
| [CI/CD Pipeline](wiki/CICD-Pipeline) | Woodpecker CI + Gogs setup | | [CI/CD Pipeline](wiki/CICD-Pipeline) | Woodpecker CI + Gogs setup, multi-platform builds |
| [Security Model](wiki/Security-Model) | Encryption, audit trail, capabilities | | [Security Model](wiki/Security-Model) | Encryption, audit trail, capabilities |
| [Integrations](wiki/Integrations) | Confluence, ServiceNow, Azure DevOps (v0.2) | | [Integrations](wiki/Integrations) | Confluence, ServiceNow, Azure DevOps (v0.2) |
| [Troubleshooting](wiki/Troubleshooting) | Known issues and fixes | | [Troubleshooting](wiki/Troubleshooting) | Known issues and fixes |
@ -28,15 +30,23 @@
- **Audit Trail** — Every external data send logged with SHA-256 hash - **Audit Trail** — Every external data send logged with SHA-256 hash
- **Domain-Specific Prompts** — 8 IT domains: Linux, Windows, Network, Kubernetes, Databases, Virtualization, Hardware, Observability - **Domain-Specific Prompts** — 8 IT domains: Linux, Windows, Network, Kubernetes, Databases, Virtualization, Hardware, Observability
## Releases
| Version | Status | Platforms |
|---------|--------|-----------|
| v0.1.0-alpha | 🚀 Released | linux/amd64 (.deb, .rpm, .AppImage), windows/amd64 (.exe, .msi) |
Download from [Releases](https://gogs.tftsr.com/sarman/tftsr-devops_investigation/releases).
## Project Status ## Project Status
| Phase | Status | | Phase | Status |
|-------|--------| |-------|--------|
| Phases 18 (Core) | ✅ Complete | | Phases 18 (Core application) | ✅ Complete |
| Phase 9 (History/Search FTS) | 🔄 Partially integrated | | Phase 9 (History/Search) | 🔲 Pending |
| Phase 10 (Integrations) | 🕐 v0.2 stubs only | | Phase 10 (Integrations) | 🕐 v0.2 stubs only |
| Phase 11 (CLI) | 🕐 Planned | | Phase 11 (CI/CD) | ✅ Complete — Woodpecker CI fully operational |
| Phase 12 (Release packaging) | 🔄 Linux done; macOS/Windows pending | | Phase 12 (Release packaging) | ✅ linux/amd64 + windows/amd64; arm64 via QEMU agent |
## Tech Stack ## Tech Stack
@ -49,5 +59,5 @@
| Database | rusqlite + SQLCipher (AES-256) | | Database | rusqlite + SQLCipher (AES-256) |
| Secret storage | tauri-plugin-stronghold | | Secret storage | tauri-plugin-stronghold |
| State | Zustand | | State | Zustand |
| Testing | Vitest (frontend) + `#[cfg(test)]` (Rust) | | Testing | Vitest (13 frontend) + `#[cfg(test)]` (64 Rust tests) |
| CI/CD | Woodpecker CI v0.15.4 + Gogs | | CI/CD | Woodpecker CI v0.15.4 + Gogs |

View File

@ -7,26 +7,104 @@
**Cause:** Woodpecker 0.15.4 `token.ParseRequest()` does not read `?token=` URL params. **Cause:** Woodpecker 0.15.4 `token.ParseRequest()` does not read `?token=` URL params.
**Fix:** Webhook URL must use `?access_token=<JWT>` (not `?token=`). **Fix:** Webhook URL must use `?access_token=<JWT>` (not `?token=`).
```
http://172.0.0.29:8084/hook?access_token=<JWT>
```
Regenerate the JWT if it's stale (JWT has an `iat` claim): Regenerate the JWT if it's stale (see [CICD-Pipeline → Webhook Configuration](wiki/CICD-Pipeline)):
```bash ```python
# JWT payload: {"text":"sarman/tftsr-devops_investigation","type":"hook"} # JWT payload: {"text":"sarman/tftsr-devops_investigation","type":"hook"}
# Signed with: repo_hash (dK8zFWtAu67qfKd3Et6N8LptqTmedumJ) # Signed HS256 with repo_hash = dK8zFWtAu67qfKd3Et6N8LptqTmedumJ
``` ```
--- ---
### Pipeline Step Can't Reach Gogs ### Build Stuck in "Pending" or "Running" with No Containers
**Cause:** Step containers run on the default Docker bridge, not on `gogs_default` network. **Cause:** After a Woodpecker server restart, the agent enters a loop trying to clean up orphaned containers from the previous session and stops processing new tasks.
**Fix:** Use `network_mode: gogs_default` in the clone section and ensure `repo_trusted=1`: **Fix:**
```bash ```bash
docker exec woodpecker_db sqlite3 /data/woodpecker.sqlite \ # 1. Kill any orphan step containers and volumes
"UPDATE repos SET repo_trusted=1 WHERE repo_full_name='sarman/tftsr-devops_investigation';" ssh sarman@172.0.0.29
docker rm -f $(docker ps -aq --filter 'name=0_')
docker volume rm $(docker volume ls -q | grep '0_')
# 2. Cancel stuck builds in Woodpecker DB
python3 -c "
import sqlite3; conn = sqlite3.connect('/docker_mounts/woodpecker/data/woodpecker.sqlite')
conn.execute(\"UPDATE builds SET build_status='cancelled' WHERE build_status IN ('pending','running')\")
conn.execute('DELETE FROM tasks')
conn.commit()
"
# 3. Restart agent
docker restart woodpecker_agent
```
---
### Pipeline Step Can't Reach Gogs (`gogs_app` not found)
**Cause:** Step containers run on the default Docker bridge. They cannot resolve `gogs_app` hostname or reach `172.0.0.29:3000` (blocked by host firewall).
**Fix:** Add `network_mode: gogs_default` to any step needing Gogs access. Requires `repo_trusted=1`:
```bash
python3 -c "
import sqlite3; conn = sqlite3.connect('/docker_mounts/woodpecker/data/woodpecker.sqlite')
conn.execute(\"UPDATE repos SET repo_trusted=1 WHERE repo_full_name='sarman/tftsr-devops_investigation'\")
conn.commit()
"
```
---
### `CI=woodpecker` Rejected by `cargo tauri build`
**Cause:** Woodpecker sets `CI=woodpecker` (string). Tauri CLI's `--ci` flag expects `true`/`false`.
**Fix:** Prefix the build command:
```yaml
commands:
- CI=true cargo tauri build --target $TARGET
```
---
### Release Artifacts Not Uploaded (Upload Step Silent Failure)
**Cause 1:** Upload container on default bridge can't reach Gogs API at `http://172.0.0.29:3000`.
**Fix:** Add `network_mode: gogs_default` to the upload step and use `gogs_app:3000` in the API URL.
**Cause 2:** Artifacts written to `/artifacts/` (absolute path) not visible to the upload step.
**Fix:** Write artifacts to the workspace using relative paths (`artifacts/linux-amd64/`). Only the workspace directory is shared between pipeline steps.
**Cause 3:** `GOGS_TOKEN` secret not set or has wrong value.
**Check:**
```bash
python3 -c "
import sqlite3; conn = sqlite3.connect('/docker_mounts/woodpecker/data/woodpecker.sqlite')
s = conn.execute(\"SELECT secret_value FROM secrets WHERE secret_name='GOGS_TOKEN'\").fetchone()
print('Token length:', len(s[0]) if s else 'NOT SET')
"
# Test token (replace 172.0.0.29:3000 with gogs_app:3000 from inside gogs_default network)
curl -H "Authorization: token <token_value>" http://172.0.0.29:3000/api/v1/user
```
---
### `git switch refs/tags/v*` Fails in Release Build
**Cause:** `woodpeckerci/plugin-git:latest` uses `git switch` which doesn't support detached HEAD for tag refs.
**Fix:** Override the clone section with `alpine/git` and explicit commands:
```yaml
clone:
git:
image: alpine/git
network_mode: gogs_default
commands:
- git init -b master
- git remote add origin http://gogs_app:3000/sarman/tftsr-devops_investigation.git
- git fetch --depth=1 origin +refs/tags/${CI_COMMIT_TAG}:refs/tags/${CI_COMMIT_TAG}
- git checkout ${CI_COMMIT_TAG}
``` ```
--- ---
@ -39,7 +117,7 @@ docker exec woodpecker_db sqlite3 /data/woodpecker.sqlite \
--- ---
### Empty Clone URL in Pipeline ### Empty Clone URL in Pipeline (Push Events)
**Cause:** Woodpecker 0.15.4 `go-gogs-client` `PayloadRepo` struct is missing `CloneURL`. **Cause:** Woodpecker 0.15.4 `go-gogs-client` `PayloadRepo` struct is missing `CloneURL`.
@ -57,7 +135,6 @@ clone:
### `MutexGuard` Not `Send` Across Await ### `MutexGuard` Not `Send` Across Await
**Error:**
``` ```
error[E0277]: `MutexGuard<'_, Connection>` cannot be sent between threads safely error[E0277]: `MutexGuard<'_, Connection>` cannot be sent between threads safely
``` ```
@ -76,7 +153,7 @@ async_fn().await?;
### Clippy Lints Fail in CI ### Clippy Lints Fail in CI
Common lint fixes: Common lint fixes required by `-D warnings` (especially on Rust 1.88+):
```rust ```rust
// uninlined_format_args // uninlined_format_args
@ -89,7 +166,9 @@ x >= a && x < b → (a..b).contains(&x)
s.push_str("a") → s.push('a') s.push_str("a") → s.push('a')
``` ```
Run locally: `cargo clippy --manifest-path src-tauri/Cargo.toml -- -D warnings` Run locally: `cargo clippy --manifest-path src-tauri/Cargo.toml -- -D warnings -W clippy::uninlined_format_args`
Auto-fix: `cargo clippy --manifest-path src-tauri/Cargo.toml --fix --allow-dirty -- -D warnings -W clippy::uninlined_format_args`
--- ---
@ -101,6 +180,12 @@ sudo dnf install -y glib2-devel gtk3-devel webkit2gtk4.1-devel \
libsoup3-devel openssl-devel librsvg2-devel libsoup3-devel openssl-devel librsvg2-devel
``` ```
**Fix (Debian/Ubuntu):**
```bash
sudo apt-get install -y libwebkit2gtk-4.1-dev libssl-dev libgtk-3-dev \
libayatana-appindicator3-dev librsvg2-dev patchelf pkg-config
```
--- ---
## Database ## Database
@ -112,7 +197,7 @@ sudo dnf install -y glib2-devel gtk3-devel webkit2gtk4.1-devel \
**Check:** **Check:**
1. `TFTSR_DB_KEY` env var is set 1. `TFTSR_DB_KEY` env var is set
2. Key matches what was used when DB was created 2. Key matches what was used when DB was created
3. File isn't corrupted (try `file tftsr.db` — should say `SQLite 3.x database`) 3. File isn't corrupted: `file tftsr.db` should say `SQLite 3.x database`
**Warning:** Changing the key requires re-encrypting the database: **Warning:** Changing the key requires re-encrypting the database:
```bash ```bash
@ -124,12 +209,12 @@ sqlite3 tftsr.db "ATTACH 'new.db' AS newdb KEY 'new-key'; \
### Migration Fails to Run ### Migration Fails to Run
Check which migrations have already been applied: Check which migrations have been applied:
```sql ```sql
SELECT name, applied_at FROM _migrations ORDER BY id; SELECT name, applied_at FROM _migrations ORDER BY id;
``` ```
If a migration is partially applied, the DB may be in an inconsistent state. Restore from backup or recreate. If a migration is partially applied, the DB may be inconsistent. Restore from backup or recreate.
--- ---
@ -137,25 +222,23 @@ If a migration is partially applied, the DB may be in an inconsistent state. Res
### TypeScript Errors After Pulling ### TypeScript Errors After Pulling
Run a fresh type check:
```bash ```bash
npx tsc --noEmit npx tsc --noEmit
``` ```
Ensure `tauriCommands.ts` matches the Rust command signatures exactly (especially `IssueDetail` nesting). Ensure `tauriCommands.ts` matches Rust command signatures exactly (especially `IssueDetail` nesting).
--- ---
### `IssueDetail` Field Access Errors ### `IssueDetail` Field Access Errors
The `get_issue` command returns a **nested** struct: `get_issue()` returns a **nested** struct:
```typescript ```typescript
// ✅ Correct // ✅ Correct
const title = detail.issue.title; const title = detail.issue.title;
const severity = detail.issue.severity;
// ❌ Wrong — these fields don't exist at the top level // ❌ Wrong
const title = detail.title; const title = detail.title; // field doesn't exist at top level
``` ```
--- ---
@ -181,7 +264,7 @@ The `sha1` field from the Gogs token create API **is** the bearer token — use
curl -H "Authorization: token <sha1_value>" https://gogs.tftsr.com/api/v1/user curl -H "Authorization: token <sha1_value>" https://gogs.tftsr.com/api/v1/user
``` ```
Do not confuse with the `sha1` column in the `access_token` table, which stores `sha1(token)[:40]`. Do not confuse with the `sha1` column in the `access_token` table, which stores `sha1(token)[:40]` (a hash of the token, not the token itself).
### PostgreSQL Access ### PostgreSQL Access

BIN
icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

View File

@ -0,0 +1,6 @@
[target.x86_64-pc-windows-gnu]
# Prevent MinGW ld from auto-exporting all ~106k public symbols into the DLL
# export table, which would exceed the 65535 ordinal limit and cause a link
# error. The desktop binary links against rlib (static), so cdylib exports
# are unused at runtime.
rustflags = ["-C", "link-arg=-Wl,--exclude-all-symbols"]

View File

@ -19,7 +19,7 @@ tauri-plugin-shell = "2"
tauri-plugin-http = "2" tauri-plugin-http = "2"
tauri-plugin-cli = "2" tauri-plugin-cli = "2"
tauri-plugin-updater = "2" tauri-plugin-updater = "2"
rusqlite = { version = "0.31", features = ["bundled-sqlcipher"] } rusqlite = { version = "0.31", features = ["bundled-sqlcipher-vendored-openssl"] }
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
tokio = { version = "1", features = ["full"] } tokio = { version = "1", features = ["full"] }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 360 B

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 857 B

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 B

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 0 B

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 0 B

After

Width:  |  Height:  |  Size: 129 KiB