Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 45 additions & 43 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ jobs:
name: Rust — fmt + clippy + deny + tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false

- name: Install Linux dependencies
run: |
Expand All @@ -29,11 +31,11 @@ jobs:
librsvg2-dev \
patchelf

- uses: dtolnay/rust-toolchain@stable
- uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable
with:
components: rustfmt, clippy, llvm-tools-preview

- uses: Swatinem/rust-cache@v2
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
workspaces: src-tauri

Expand All @@ -44,7 +46,7 @@ jobs:
run: cargo clippy --manifest-path src-tauri/Cargo.toml --workspace -- -D warnings

- name: Install cargo tooling
uses: taiki-e/install-action@v2
uses: taiki-e/install-action@43aecc8d72668fbcfe75c31400bc4f890f1c5853 # v2
with:
tool: cargo-llvm-cov,cargo-deny

Expand All @@ -59,7 +61,7 @@ jobs:
--lcov \
--output-path lcov.info

- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: rust-coverage
path: lcov.info
Expand All @@ -68,16 +70,21 @@ jobs:
name: Frontend — oxlint + oxfmt + tsc + knip + vitest
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false

- uses: actions/setup-node@v4
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22
cache: npm

- name: npm ci
run: npm ci

- name: npm audit
run: npm audit

- name: oxlint
run: npx oxlint . --max-warnings=0

Expand All @@ -96,7 +103,7 @@ jobs:
- name: vitest + coverage
run: npx vitest run --coverage --passWithNoTests

- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: frontend-coverage
path: coverage/
Expand All @@ -105,42 +112,23 @@ jobs:
name: Secrets scan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false

- name: Check for secret files in repo
run: |
# `(^|/)\.npmrc$|(^|/)\.pypirc$` catches nested forms (e.g.
# `tools/.npmrc`) on top of the root-level files.
SECRET_FILES=$(git ls-files | grep -iE '\.(env|env\..+|pem|key|p12|pfx|secret|creds|aws|netrc)$|(^|/)(\.env|\.secrets|secrets)/|(^|/)\.npmrc$|(^|/)\.pypirc$' || true)
if [ -n "$SECRET_FILES" ]; then
echo "::error::Secret files in repo:"
echo "$SECRET_FILES"
exit 1
fi
- name: Test secrets scanner
run: ./scripts/test-no-secrets.sh

- name: Check for API key patterns in tracked files
run: |
# Patterns aligned with scripts/no-secrets.sh — defence in depth.
# All distinctive prefixes (AKIA, sk-ant, sk-, ghp_, github_pat_,
# glpat-, AIza), so manifests like package.json / Cargo.toml are
# safe to scan and are no longer excluded.
# `ASIA…` covers temporary STS credentials (vs long-lived `AKIA…`).
PATTERNS='AKIA[0-9A-Z]{16}|ASIA[0-9A-Z]{16}|sk-ant-[A-Za-z0-9_-]{20,}|sk-[A-Za-z0-9]{32,}|ghp_[A-Za-z0-9]{36}|github_pat_[A-Za-z0-9_]{82}|glpat-[A-Za-z0-9_-]{20}|AIza[0-9A-Za-z_-]{35}'
MATCHES=$(git grep -nE "$PATTERNS" -- ':(exclude)*.lock' || true)
if [ -n "$MATCHES" ]; then
echo "::error::API key pattern detected:"
# Strip the matched secret value from the output — `git grep -n`
# returns `file:line:matched_text`; emit only `file:line` so CI
# logs never reveal the raw credential.
echo "$MATCHES" | head -10 | awk -F: '{print $1 ":" $2}'
exit 1
fi
- name: Scan tracked files for secrets
run: ./scripts/no-secrets.sh --all

forbidden-tools:
name: Forbidden tools
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false

- name: Reject pnpm-lock.yaml / yarn.lock
run: |
Expand Down Expand Up @@ -213,15 +201,17 @@ jobs:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false

- uses: dtolnay/rust-toolchain@stable
- uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable

- uses: Swatinem/rust-cache@v2
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
workspaces: src-tauri

- uses: actions/setup-node@v4
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22
cache: npm
Expand All @@ -240,15 +230,27 @@ jobs:
run: npm ci

- name: Tauri build
run: npx tauri build --config .github/tauri.ci.conf.json
id: tauri-build
shell: bash
run: npx tauri build --config .github/tauri.ci.conf.json 2>&1 | tee tauri-build.log

- name: Upload Tauri build diagnostics
if: failure() && steps.tauri-build.outcome == 'failure'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: tauri-build-diagnostics-${{ matrix.os }}
path: tauri-build.log
if-no-files-found: warn
retention-days: 7

changelog-check:
name: CHANGELOG updated
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
fetch-depth: 0

- name: Verify CHANGELOG.md updated when code changes
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- **Lot 0 maintenance baseline (MAT-126–MAT-128)**: restored CI by allowing non-secret `.npmrc` configuration through the shared redacted scanner, updated compatible npm and Rust dependencies to clear security audits, pinned CI actions on their Node 24 releases, kept the codebase clean under the current Rust stable Clippy, and added multi-OS Tauri build diagnostics.
- **Task 40 follow-up — PR #153 review fixes round 6** (scope `download`, sprint task 40, PR #153 review): `chatgpt-codex-connector[bot]` flagged a cancellation race in the failover loop. After `AttemptOutcome::Failed`, the loop bumped `mirror_idx`, ran the file/meta cleanup, and unconditionally published `MirrorSwitched` (which `progress_bridge` persists to `current_mirror_index`). A user-cancel that landed in the window between the failed attempt returning and the next attempt starting — including while `tokio::task::spawn_blocking` was running the cleanup — would still bump the persisted cursor through the bridge before the next iteration's `run_mirror_attempt` observed `cancel_token.is_cancelled()` and reported `AttemptOutcome::Cancelled`. The retry would then resume from a slot the user never asked for. Two `cancel_token.is_cancelled()` guards added inside the failover branch: one before bumping `mirror_idx` (catches a cancel that arrived between the attempt finishing and the loop re-entering), one after the cleanup completes (catches a cancel that arrived during the spawn_blocking). Both paths emit `DomainEvent::DownloadCancelled` and break out, so no `MirrorSwitched` ever fires for an aborted switch.
- **Task 40 follow-up — PR #153 review fixes round 5** (scope `download`, sprint task 40, PR #153 review): `coderabbitai[bot]` flagged that the round-2 cursor-reset path mapped every `DomainEvent::DownloadFailed` to `MirrorCursorReset`, but `extract_archive::extract_archive_handler` (and the domain `Download::fail()` max-retries path) also publish `DownloadFailed` for post-download errors. A successful download from mirror N followed by an extract / verify failure would zero the cursor and lose the last-known-good slot, so a manual re-download would walk the full mirror list from the top instead of going straight back to the mirror that already produced the bytes. New domain event `DomainEvent::AllMirrorsExhausted { id }` published by the engine's failover loop right before the existing `DownloadFailed`; `progress_bridge` now keys the cursor reset on `AllMirrorsExhausted` and ignores generic `DownloadFailed`. Wired through `tauri_bridge` (`mirrors-exhausted` event name, `{ "id": <id> }` payload) and the download log bridge (silent — no log line). Three tests: `test_event_to_message_maps_all_mirrors_exhausted_to_cursor_reset`, `test_event_to_message_does_not_reset_cursor_on_generic_download_failed` (asserts `event_to_message(&DownloadFailed)` returns `None`), `test_all_mirrors_exhausted_resets_persisted_mirror_cursor_to_zero` (write-through). The previous round-2 tests were renamed in place.
- **Task 40 follow-up — PR #153 review fixes round 4** (scope `download`, sprint task 40, PR #153 review): `chatgpt-codex-connector[bot]` flagged that the round-3 `MirrorSwitched` handler clears the cursor + segment rows but leaves `downloads.downloaded_bytes` / `total_bytes` pinned at the failed mirror's last value. Because `update_download_progress` enforces `downloaded_bytes = MAX(downloaded_bytes, ?)` so concurrent state-transition saves cannot regress the counter, the new mirror's progress events would be silently swallowed until the new attempt crossed the failed mirror's high-water mark — the UI would show a frozen / inflated progress bar (and ETA) until then. `progress_bridge` now adds a `reset_download_bytes` step inside the `MirrorSwitched` handler that runs `UPDATE downloads SET downloaded_bytes = 0, total_bytes = NULL WHERE id = ?` between the cursor write and the segments purge. The next attempt's `DownloadProgress` event repopulates `total_bytes` through the existing `COALESCE(NULLIF(...))` guard, and `downloaded_bytes` rises monotonically from 0 under the same `MAX` guard. Two regression tests: `test_reset_download_bytes_zeroes_downloaded_and_clears_total` (write-through) and `test_reset_download_bytes_then_progress_repopulates_total` (the round-trip with a follow-up `update_download_progress` call).
Expand Down
12 changes: 12 additions & 0 deletions deny.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,18 @@ ignore = [
"RUSTSEC-2025-0081",
"RUSTSEC-2025-0098",
"RUSTSEC-2025-0100",
# proc-macro-error2 2.0.1 is unmaintained through sea-bae →
# sea-orm-macros 1.1.20. It runs only while compiling, so it has no runtime
# input surface. Mitigation: follow SeaORM 1.x updates and remove this entry
# when its macros migrate. Review by 2026-10-15.
"RUSTSEC-2026-0173",
# quick-xml <0.41 is pulled by tauri-winrt-notification 0.7.2 only for XML
# escaping, and by wayland-scanner 0.31.10 for trusted build-time protocol
# XML. Neither vulnerable parser path processes remote XML in Vortex.
# Mitigation: keep parent crates current and remove when they adopt 0.41+.
# Review by 2026-10-15.
"RUSTSEC-2026-0194",
"RUSTSEC-2026-0195",
]

# === License compliance (GPL-3.0 compatible) ===
Expand Down
Loading
Loading