diff --git a/.codecov.yml b/.codecov.yml new file mode 100644 index 000000000..011753864 --- /dev/null +++ b/.codecov.yml @@ -0,0 +1,29 @@ +coverage: + status: + # Project-level gating is OFF until overall coverage > 30% (per AGENTS.md). + # At 1.17% baseline, target: auto would block every PR. + project: off + # Patch-level: warn-only. New code should be tested but not block. + patch: + default: + target: 50% + informational: true + # Per-flag thresholds (server flag already configured in ci.yml). + # Add cli, desktop, etc. as their coverage toolchains ship. + flags: + server: + target: 80% + informational: true + +# PR comments disabled for now: solo dev, low PR traffic, low signal at 32 tests. +comment: false + +# Group admin paths that should not count toward coverage. +ignore: + - ".nuxt/**" + - ".output/**" + - "prisma/client/**" + - "proto/**" + - "**/*.test.ts" + - "**/*.test.tsx" + - "test/**" diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 000000000..140bcdf74 --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,33 @@ +# CodeRabbit configuration +# https://docs.coderabbit.ai/guides/configure-coderabbit + +language: en-US +early_access: true + +reviews: + request_changes_workflow: false + profile: chill + high_level_summary: true + review_status: true + commit_messages: true + suggested_labels: true + poem: false + path_filters: + - "!**/target/**" + - "!**/node_modules/**" + - "!**/.nuxt/**" + - "!**/.output/**" + - "!**/coverage/**" + - "!**/dist/**" + - "!**/.git/**" + +# Auto-review on PRs +auto_review: + enabled: true + drafts: false + ignore_title_keywords: + - "WIP" + - "DRAFT" + +chat: + auto_reply: true diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..40a711b9c --- /dev/null +++ b/.editorconfig @@ -0,0 +1,32 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.rs] +indent_size = 4 + +[*.md] +trim_trailing_whitespace = false + +[*.{yml,yaml}] +indent_size = 2 + +# Makefiles require tabs (syntax) +[Makefile] +indent_style = tab + +# Nix files: prettier handles formatting, allow 2-space indent +[*.nix] +indent_style = space +indent_size = 2 + +# JSON files: Prettier handles formatting with 2-space indent +[*.json] +indent_style = space +indent_size = 2 diff --git a/.env.example b/.env.example new file mode 100644 index 000000000..3dd3c55d8 --- /dev/null +++ b/.env.example @@ -0,0 +1,5 @@ +# Drop environment configuration +# See server/.env.example for full reference +DATABASE_URL="postgres://drop:drop@127.0.0.1:5432/drop" +EXTERNAL_URL="http://localhost:3000" +NUXT_PORT=4000 diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..d41b233a4 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,17 @@ +# Security-sensitive routes — require review +# Auth: webauthn, passkey, MFA, OIDC, TOTP +/server/server/api/v1/auth/ @BillyOutlast +/server/server/internal/auth/ @BillyOutlast + +# Metadata providers — external HTTP integration, complex fallthrough +/server/server/internal/metadata/ @BillyOutlast + +# Nitro server core (server/ dir) +/server/server/ @BillyOutlast + +# Build, deps, CI +/server/.env.example @BillyOutlast +/.github/workflows/ @BillyOutlast +/AGENTS.md @BillyOutlast +/CLAUDE.md @BillyOutlast +/CONTRIBUTING.md @BillyOutlast diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..bf4a60970 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,159 @@ +version: 2 + +registries: + # Allow Dependabot to resolve private/skipped registry hosts from lockfiles + # (e.g. buf schema registry, GitHub Packages). Public registries need no entry. + npm-pkg-github: + type: "npm-registry" + url: "https://npm.pkg.github.com" + token: "${{secrets.GITHUB_TOKEN}}" + +updates: + # ----- Node / pnpm workspace (root, server, sites/*, desktop) ----- + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + time: "06:00" + timezone: "America/New_York" + open-pull-requests-limit: 10 + groups: + # Patch + minor bumps bundled together to avoid PR spam + node-minor: + update-types: ["minor", "patch"] + # Pin major bumps separately for explicit review + node-major: + update-types: ["major"] + commit-message: + prefix: "deps" + prefix-development: "chore(deps-dev)" + labels: ["dependencies", "javascript"] + reviewers: ["BillyOutlast"] + # Keep lockfile in sync; pnpm-workspace.yaml declares onlyBuiltDependencies + # — keep Dependabot from re-enabling builds that the workspace intentionally skips. + rebase-strategy: "auto" + + # ----- Nuxt 4 desktop app (separate pnpm workspace) ----- + # desktop/main/ has its own pnpm-workspace.yaml and pnpm-lock.yaml, + # so root npm entry at "/" does not cover it. Scanned independently. + - package-ecosystem: "npm" + directory: "/desktop/main" + schedule: + interval: "weekly" + day: "monday" + time: "06:00" + timezone: "America/New_York" + open-pull-requests-limit: 5 + groups: + desktop-minor: + update-types: ["minor", "patch"] + desktop-major: + update-types: ["major"] + commit-message: + prefix: "deps(desktop)" + labels: ["dependencies", "javascript"] + rebase-strategy: "auto" + + # ----- Rust workspace: CLI ----- + - package-ecosystem: "cargo" + directory: "/cli" + schedule: + interval: "weekly" + day: "monday" + time: "06:00" + timezone: "America/New_York" + open-pull-requests-limit: 5 + groups: + rust-minor: + update-types: ["minor", "patch"] + rust-major: + update-types: ["major"] + commit-message: + prefix: "deps(cli)" + labels: ["dependencies", "rust"] + + # ----- Rust workspace: droplet library ----- + - package-ecosystem: "cargo" + directory: "/libraries/droplet" + schedule: + interval: "weekly" + day: "monday" + time: "06:00" + timezone: "America/New_York" + open-pull-requests-limit: 5 + groups: + rust-minor: + update-types: ["minor", "patch"] + rust-major: + update-types: ["major"] + commit-message: + prefix: "deps(droplet)" + labels: ["dependencies", "rust"] + + # ----- Rust workspace: native_model library ----- + - package-ecosystem: "cargo" + directory: "/libraries/native_model" + schedule: + interval: "weekly" + day: "monday" + time: "06:00" + timezone: "America/New_York" + open-pull-requests-limit: 5 + groups: + rust-minor: + update-types: ["minor", "patch"] + rust-major: + update-types: ["major"] + commit-message: + prefix: "deps(native_model)" + labels: ["dependencies", "rust"] + + # ----- Rust workspace: desktop (Tauri) ----- + - package-ecosystem: "cargo" + directory: "/desktop/src-tauri" + schedule: + interval: "weekly" + day: "monday" + time: "06:00" + timezone: "America/New_York" + open-pull-requests-limit: 5 + groups: + rust-minor: + update-types: ["minor", "patch"] + rust-major: + update-types: ["major"] + commit-message: + prefix: "deps(desktop)" + labels: ["dependencies", "rust"] + + # ----- Dockerfile (root image) ----- + - package-ecosystem: "docker" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + time: "06:00" + timezone: "America/New_York" + open-pull-requests-limit: 5 + commit-message: + prefix: "deps(docker)" + labels: ["dependencies", "docker"] + + # ----- GitHub Actions ----- + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + time: "06:00" + timezone: "America/New_York" + open-pull-requests-limit: 10 + groups: + actions-minor: + update-types: ["minor", "patch"] + actions-major: + update-types: ["major"] + commit-message: + prefix: "deps(ci)" + labels: ["dependencies", "github-actions"] diff --git a/.github/scripts/diff-to-test-prompt.sh b/.github/scripts/diff-to-test-prompt.sh new file mode 100755 index 000000000..c56346abb --- /dev/null +++ b/.github/scripts/diff-to-test-prompt.sh @@ -0,0 +1,199 @@ +#!/usr/bin/env bash +# ============================================================================ +# diff-to-test-prompt.sh — Fork-diff to LLM test-generation prompt +# ============================================================================ +# Reads a git diff (stdin or file arg) and wraps it in a structured prompt +# for an LLM to generate tests. The diff IS the spec — every changed line is +# a behavioral claim that tests must verify. +# +# Usage: +# git diff upstream/main...HEAD | .github/scripts/diff-to-test-prompt.sh +# .github/scripts/diff-to-test-prompt.sh path/to/diff.txt +# +# Output: A self-contained prompt with workspace detection, test framework +# hints, and suggested test-file locations. +# +# Workspace detection (by path prefix): +# server/ → vitest (Nuxt env) → server/test/unit// +# cli/ → cargo test → cli/tests/ or inline #[cfg(test)] +# desktop/ → cargo test → desktop/src-tauri//tests/ +# libraries/ → cargo test → inline #[cfg(test)] +# other → vitest (generic) → /test/ +# ============================================================================ + +set -euo pipefail + +# ---- Help ------------------------------------------------------------------ +if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then + sed -n '3,19p' "$0" + exit 0 +fi + +# ---- Read diff ------------------------------------------------------------- +DIFF_CONTENT="" + +if [[ $# -ge 1 && -f "$1" ]]; then + DIFF_CONTENT="$(cat "$1")" +elif [[ ! -t 0 ]]; then + DIFF_CONTENT="$(cat)" +else + echo "ERROR: Provide a diff file or pipe diff to stdin." >&2 + echo "Usage: git diff upstream/main...HEAD | $0" >&2 + echo " $0 path/to/diff.txt" >&2 + exit 1 +fi + +if [[ -z "$DIFF_CONTENT" ]]; then + echo "ERROR: Empty diff input." >&2 + exit 1 +fi + +# ---- Workspace detection --------------------------------------------------- +detect_workspace() { + local diff="$1" + local workspaces=() + + if echo "$diff" | grep -q '^diff --git a/desktop/'; then + workspaces+=("desktop/") + fi + if echo "$diff" | grep -q '^diff --git a/cli/'; then + workspaces+=("cli/") + fi + if echo "$diff" | grep -q '^diff --git a/libraries/'; then + workspaces+=("libraries/") + fi + if echo "$diff" | grep -q '^diff --git a/server/'; then + workspaces+=("server/") + fi + if echo "$diff" | grep -q '^diff --git a/sites/'; then + workspaces+=("sites/") + fi + if [[ ${#workspaces[@]} -eq 0 ]]; then + echo "unknown" + else + printf '%s\n' "${workspaces[@]}" | sort -u | paste -sd ' ' - + fi +} + +detect_test_location() { + local diff="$1" + local file dir + + # Extract first changed file path, strip filename to get directory + file="$(echo "$diff" | grep '^diff --git' | head -1 | sed 's/^diff --git a\/\(.*\) b\/.*/\1/')" + dir="$(dirname "$file")" + + # Extract module name: the path segment after the workspace's source root. + # server/server/api/v1/users.ts → module=api + # server/server/internal/auth/ → module=auth + # cli/src/commands/upload.rs → module=commands + local module="" + + case "$dir" in + server/server/api/v1*) + module="api" + echo "server/test/unit/${module}/" + ;; + server/server/internal/*) + module="$(echo "$dir" | sed 's|server/server/internal/||; s|/.*||')" + echo "server/test/unit/${module}/" + ;; + server/components/*) + module="$(echo "$dir" | sed 's|server/components/||; s|/.*||')" + [[ -n "$module" ]] && echo "server/test/unit/components/${module}/" || echo "server/test/unit/components/" + ;; + server/pages/*) + echo "server/test/unit/pages/" + ;; + server/composables/*) + echo "server/test/unit/" + ;; + server/server/*) + echo "server/test/unit/misc/" + ;; + server/prisma/*) + echo "server/test/integration/" + ;; + cli/src/*) + module="$(echo "$dir" | sed 's|cli/src/||; s|/.*||')" + [[ -n "$module" ]] && echo "cli/tests/${module}/ or inline #[cfg(test)]" || echo "cli/tests/ or inline #[cfg(test)]" + ;; + desktop/src-tauri/*) + module="$(echo "$dir" | sed 's|desktop/src-tauri/||; s|/.*||')" + echo "desktop/src-tauri/${module}/tests/" + ;; + libraries/*) + echo "inline #[cfg(test)] mod tests { ... } in the source file" + ;; + sites/*) + echo "test/ (co-located with source workspace)" + ;; + *) + echo "test/ (co-located with source)" + ;; + esac +} + +WORKSPACES="$(detect_workspace "$DIFF_CONTENT")" +TEST_LOC="$(detect_test_location "$DIFF_CONTENT")" + +# ---- Test framework hints -------------------------------------------------- +FRAMEWORK_HINTS="" +case "$WORKSPACES" in + *server*) + FRAMEWORK_HINTS="Framework: vitest with Nuxt test environment (environment: 'nuxt') +Utilities: server/test/setup.ts, server/test/utils/db.ts +Pattern: describe -> it -> expect. Mock HTTP via MSW (server/test/mocks/). +Convention: one test file per module, co-located in server/test/unit/ or server/test/integration/" + ;; + *cli*|*desktop*|*libraries*) + FRAMEWORK_HINTS="Framework: cargo test (Rust) +Pattern: #[cfg(test)] mod tests { ... } with #[test] functions +Convention: integration tests in tests/ dir, unit tests inline" + ;; + *sites*) + FRAMEWORK_HINTS="Framework: vitest +Pattern: describe -> it -> expect" + ;; + *) + FRAMEWORK_HINTS="Framework: vitest (assumed) +Pattern: describe -> it -> expect" + ;; +esac + +# ---- Count stats ----------------------------------------------------------- +FILE_COUNT="$(echo "$DIFF_CONTENT" | grep -c '^diff --git' || true)" +LINE_COUNT="$(echo "$DIFF_CONTENT" | grep -c '^[+-]' || true)" +ADDED="$(echo "$DIFF_CONTENT" | grep -c '^+' || true)" +REMOVED="$(echo "$DIFF_CONTENT" | grep -c '^-' || true)" + +# ---- Build prompt ---------------------------------------------------------- +cat <=4.2.2. + run: pnpm audit --audit-level=critical --ignore GHSA-mp2f-45pm-3cg9 + - name: Typecheck + working-directory: server + run: pnpm run typecheck + + lint: + name: Lint & Format + runs-on: ubuntu-latest + steps: + - name: Check out the repo + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + submodules: true + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y libpng-dev + - name: Install pnpm + uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa # v4 + - name: Setup Node.js + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: pnpm + - name: Install dependencies + run: pnpm install --frozen-lockfile --ignore-scripts + + - name: Generate Nuxt and Prisma artifacts + run: pnpm --filter drop run postinstall + - name: Check formatting + working-directory: server + run: pnpm run format:check + - name: Lint + working-directory: server + run: pnpm run lint:eslint + + test: + name: Test + Coverage + runs-on: ubuntu-latest + # Coverage is measurement-only at this stage. No thresholds, no gates. + # Add a `coverage:check` script with thresholds once coverage matures + # past ~30% on the server business-logic modules. + steps: + - name: Check out the repo + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + submodules: true + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y libpng-dev + - name: Install pnpm + uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa # v4 + - name: Setup Node.js + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: pnpm + - name: Install dependencies + run: pnpm install --frozen-lockfile --ignore-scripts + + - name: Generate Nuxt and Prisma artifacts + run: pnpm --filter drop run postinstall + - name: Run tests with coverage + working-directory: server + run: pnpm run coverage + - name: Upload coverage to Codecov + uses: codecov/codecov-action@04b047e8bb82a0c002c8312c1c880fbc6a999d45 # v5 + with: + directory: server/coverage + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: false + flags: server + + sonar: + name: SonarCloud Scan + runs-on: ubuntu-latest + if: github.event_name == 'push' || github.event_name == 'pull_request' + continue-on-error: true + steps: + - name: Check out the repo + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + fetch-depth: 0 + submodules: true + - name: SonarCloud Scan + uses: sonarcloud/github-action@v3 + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + with: + args: > + -Dsonar.projectKey=Heretek-Drop_drop + -Dsonar.organization=heretek-drop + + dockerfile: + name: Dockerfile Lint + runs-on: ubuntu-latest + steps: + - name: Check out the repo + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - name: Run hadolint + uses: hadolint/hadolint-action@54c9adbab1582c2ef04b2016b760714a4bfde3cf # v3.1.0 + with: + dockerfile: Dockerfile + failure-threshold: error + + shellcheck: + name: Shellcheck + runs-on: ubuntu-latest + steps: + - name: Check out the repo + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - name: Run shellcheck + uses: ludeeus/action-shellcheck@00cae500b08a931fb5698e11e79bfbd38e612a38 # 2.0.0 + with: + scandir: "./scripts" + additional_files: ".husky/pre-commit" + ignore-paths: "node_modules,.git,.nuxt,.output,target" diff --git a/.github/workflows/cli-ci.yml b/.github/workflows/cli-ci.yml new file mode 100644 index 000000000..3a28955bf --- /dev/null +++ b/.github/workflows/cli-ci.yml @@ -0,0 +1,79 @@ +name: CLI CI + +on: + push: + branches: [develop] + paths: + - "cli/**" + - ".github/workflows/cli-ci.yml" + pull_request: + branches: [develop] + paths: + - "cli/**" + - ".github/workflows/cli-ci.yml" + workflow_dispatch: + +env: + CARGO_TERM_COLOR: always + +jobs: + ci: + name: Build, Test, Lint + runs-on: ubuntu-latest + defaults: + run: + working-directory: cli + steps: + - name: Checkout repository + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@4fd1da8b0805d2d2e936788875a7d65dbd677dc2 # nightly + with: + components: rustfmt, clippy + + - name: Rust cache + uses: swatinem/rust-cache@42dc69e1aa15d09112580998cf2ef0119e2e91ae # v2 + with: + workspaces: "./cli -> target" + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y libarchive-dev + + - name: Check formatting + run: cargo fmt --all -- --check + + - name: Run Clippy + # `--all-targets` includes tests since lib.rs was added. + # `--no-deps` to avoid linting transitive deps (unmaintained crates). + # Pre-existing warnings not escalated to errors; fix incrementally. + run: cargo clippy --all-targets --no-deps --all-features + + - name: Run tests + run: cargo test --all-features --all --verbose + + - name: Install LLVM tools (cargo-llvm-cov) + uses: dtolnay/rust-toolchain@4fd1da8b0805d2d2e936788875a7d65dbd677dc2 # nightly + with: + components: llvm-tools-preview + + - name: Generate code coverage + continue-on-error: true + run: | + cargo install cargo-llvm-cov --locked + cargo llvm-cov --all-features --workspace --codecov --output-path coverage.lcov + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@04b047e8bb82a0c002c8312c1c880fbc6a999d45 # v5 + with: + files: cli/coverage.lcov + fail_ci_if_error: false + + # Advisory until noise rate is low. Tighten to blocking after triage. + - name: Audit dependencies + continue-on-error: true + run: | + cargo install cargo-audit --locked + cargo audit diff --git a/.github/workflows/client-release.yml b/.github/workflows/client-release.yml index ee511a244..a4139de61 100644 --- a/.github/workflows/client-release.yml +++ b/.github/workflows/client-release.yml @@ -36,32 +36,31 @@ jobs: runs-on: ${{ matrix.platform }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 with: token: ${{ secrets.GITHUB_TOKEN }} - name: setup pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa # v4 with: run_install: false - name: setup node - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: lts/* cache: pnpm - - name: install Rust nightly - uses: dtolnay/rust-toolchain@nightly + uses: dtolnay/rust-toolchain@4fd1da8b0805d2d2e936788875a7d65dbd677dc2 # nightly with: # Those targets are only used on macos runners so it's in an `if` to slightly speed up windows and linux builds. targets: ${{ matrix.platform == 'macos-14' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }} - name: Rust cache - uses: swatinem/rust-cache@v2 + uses: swatinem/rust-cache@42dc69e1aa15d09112580998cf2ef0119e2e91ae # v2 with: - workspaces: './desktop/src-tauri -> target' + workspaces: "./desktop/src-tauri -> target" - name: install dependencies (ubuntu only) if: matrix.platform == 'ubuntu-22.04' || matrix.platform == 'ubuntu-22.04-arm' # This must match the platform value defined above. @@ -117,9 +116,9 @@ jobs: echo "Certificate imported. Using identity: $CERT_ID" - name: install frontend dependencies - run: pnpm install # change this to npm, pnpm or bun depending on which one you use. + run: pnpm install --ignore-scripts # change this to npm, pnpm or bun depending on which one you use. - - uses: tauri-apps/tauri-action@v0 + - uses: tauri-apps/tauri-action@fce9c6108b31ea247710505d3aaaa893ee6768d4 # v0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Do NOT set APPLE_CERTIFICATE / APPLE_CERTIFICATE_PASSWORD here. Doing so @@ -137,4 +136,4 @@ jobs: releaseDraft: false prerelease: true args: ${{ matrix.args }} - projectPath: './desktop' \ No newline at end of file + projectPath: "./desktop" diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 000000000..fed141266 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,109 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL Advanced" + +on: + push: + branches: ["develop"] + pull_request: + branches: ["develop"] + schedule: + - cron: "39 17 * * 0" + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners (GitHub.com only) + # Consider using larger runners or machines with greater resources for possible analysis time improvements. + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + permissions: + # required for all workflows + security-events: write + + # required to fetch internal or private CodeQL packs + packages: read + + # only required for workflows in private repositories + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + include: + - language: actions + build-mode: none + - language: go + build-mode: autobuild + - language: javascript-typescript + build-mode: none + - language: rust + build-mode: none + - language: swift + build-mode: autobuild + # CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift' + # Use `c-cpp` to analyze code written in C, C++ or both + # Use 'java-kotlin' to analyze code written in Java, Kotlin or both + # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both + # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, + # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. + # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how + # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages + steps: + - name: Checkout repository + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false + + # Add any setup steps before running the `github/codeql-action/init` action. + # This includes steps like installing compilers or runtimes (`actions/setup-node` + # or others). This is typically only required for manual builds. + # - name: Setup runtime (example) + # uses: actions/setup-example@v1 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@adfda868f108ac4222129de456ea554034a27db7 # v4 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + # If the analyze step fails for one of the languages you are analyzing with + # "We were unable to automatically build your code", modify the matrix above + # to set the build mode to "manual" for that language. Then modify this step + # to build your code. + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + - name: Run manual build steps + if: matrix.build-mode == 'manual' + shell: bash + run: | + echo 'If you are using a "manual" build mode for one or more of the' \ + 'languages you are analyzing, replace this with the commands to build' \ + 'your code, for example:' + echo ' make bootstrap' + echo ' make release' + exit 1 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@adfda868f108ac4222129de456ea554034a27db7 # v4 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml new file mode 100644 index 000000000..d99d50103 --- /dev/null +++ b/.github/workflows/dependabot-auto-merge.yml @@ -0,0 +1,74 @@ +name: Dependabot auto-merge + +on: + pull_request: + types: [opened, synchronize, reopened] + +permissions: + contents: write + pull-requests: write + checks: read + statuses: read + +# Auto-merge Dependabot PRs for patch + minor npm updates after CI passes. +# Major npm + all cargo updates require human review. +# Restrict via `pull-request.user.login == 'dependabot[bot]'` to prevent abuse. + +jobs: + auto-merge: + name: Auto-merge Dependabot PR + if: github.event.pull_request.user.login == 'dependabot[bot]' + runs-on: ubuntu-latest + steps: + - name: Fetch metadata + id: meta + uses: actions/github-script@v7 + with: + script: | + const pr = context.payload.pull_request; + // Detect ecosystem via label Dependabot attaches: 'dependencies/npm_and_yarn', 'dependencies/cargo', etc. + const isNpm = (pr.labels || []).some(l => l.name === 'dependencies/npm_and_yarn'); + const isCargo = (pr.labels || []).some(l => l.name === 'dependencies/cargo'); + // Parse "bump from X to Y" or " from X to Y" + const title = pr.title; + const match = title.match(/(bump\s+)?(\S+)\s+from\s+\S+\s+to\s+(\S+)/); + if (!match) { + core.setOutput('major', 'true'); + core.setOutput('ecosystem', 'unknown'); + return; + } + const to = match[3]; + const isMajor = to.includes('major') || /^(\d+)\.0\.0$/.test(to); + core.setOutput('major', isMajor ? 'true' : 'false'); + core.setOutput('package', match[2]); + core.setOutput('ecosystem', isNpm ? 'npm' : isCargo ? 'cargo' : 'other'); + + # Only auto-merge for non-major npm updates (cargo updates always require human review) + - name: Auto-merge (non-major npm only) + if: steps.meta.outputs.major != 'true' && steps.meta.outputs.ecosystem == 'npm' + uses: actions/github-script@v7 + with: + script: | + const pr = context.payload.pull_request; + const sha = pr.head.sha; + + // Pre-check commit status: all required status checks must be 'success'. + // This prevents merging a PR whose CI hasn't finished or has failed. + const { data: statuses } = await github.rest.repos.getCombinedStatusForRef({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: sha, + }); + + if (statuses.state !== 'success') { + core.info(`PR #${pr.number} status is '${statuses.state}' — skipping auto-merge. Will retry on next push.`); + return; + } + + await github.rest.pulls.merge({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: pr.number, + merge_method: 'squash', + }); + core.info(`Auto-merged PR #${pr.number} (${pr.title})`); diff --git a/.github/workflows/desktop-ci.yml b/.github/workflows/desktop-ci.yml new file mode 100644 index 000000000..2fd4576e3 --- /dev/null +++ b/.github/workflows/desktop-ci.yml @@ -0,0 +1,84 @@ +name: Desktop CI + +on: + push: + branches: [develop] + paths: + - "desktop/src-tauri/**" + - ".github/workflows/desktop-ci.yml" + pull_request: + branches: [develop] + paths: + - "desktop/src-tauri/**" + - ".github/workflows/desktop-ci.yml" + workflow_dispatch: + +env: + CARGO_TERM_COLOR: always + +jobs: + ci: + name: Format, Check + runs-on: ubuntu-latest + defaults: + run: + working-directory: desktop/src-tauri + steps: + - name: Checkout repository + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@4fd1da8b0805d2d2e936788875a7d65dbd677dc2 # nightly + with: + components: rustfmt + + - name: Rust cache + uses: swatinem/rust-cache@42dc69e1aa15d09112580998cf2ef0119e2e91ae # v2 + with: + workspaces: "./desktop/src-tauri -> target" + + - name: Install system dependencies (Tauri) + run: | + sudo apt-get update + sudo apt-get install -y \ + libwebkit2gtk-4.1-dev \ + libappindicator3-dev \ + librsvg2-dev \ + patchelf + + - name: Check formatting + run: cargo fmt --all -- --check + + # `cargo check` not `cargo clippy --all-features`: pre-existing errors + # in some workspace members. Tighten when resolved. + - name: Build + run: cargo check --workspace + + # Pre-existing test failures in some crates. Tighten when resolved. + - name: Test + continue-on-error: true + run: cargo test --workspace --no-fail-fast + + - name: Install LLVM tools (cargo-llvm-cov) + uses: dtolnay/rust-toolchain@4fd1da8b0805d2d2e936788875a7d65dbd677dc2 # nightly + with: + components: llvm-tools-preview + + - name: Generate code coverage + continue-on-error: true + run: | + cargo install cargo-llvm-cov --locked + cargo llvm-cov --all-features --workspace --codecov --output-path coverage.lcov + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@04b047e8bb82a0c002c8312c1c880fbc6a999d45 # v5 + with: + files: desktop/src-tauri/coverage.lcov + fail_ci_if_error: false + + # Advisory until noise rate is low. Tighten to blocking after triage. + - name: Audit dependencies + continue-on-error: true + run: | + cargo install cargo-audit --locked + cargo audit diff --git a/.github/workflows/droplet-ci.yml b/.github/workflows/droplet-ci.yml index c125d2e75..0e0ab3a69 100644 --- a/.github/workflows/droplet-ci.yml +++ b/.github/workflows/droplet-ci.yml @@ -29,15 +29,15 @@ jobs: working-directory: libraries/droplet steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@nightly + uses: dtolnay/rust-toolchain@4fd1da8b0805d2d2e936788875a7d65dbd677dc2 # nightly with: components: rustfmt, clippy - name: Rust cache - uses: swatinem/rust-cache@v2 + uses: swatinem/rust-cache@42dc69e1aa15d09112580998cf2ef0119e2e91ae # v2 with: workspaces: "./libraries/droplet -> target" @@ -54,3 +54,27 @@ jobs: - name: Run tests run: cargo test --all-features --all --verbose + + - name: Install LLVM tools (cargo-llvm-cov) + uses: dtolnay/rust-toolchain@4fd1da8b0805d2d2e936788875a7d65dbd677dc2 # nightly + with: + components: llvm-tools-preview + + - name: Generate code coverage + continue-on-error: true + run: | + cargo install cargo-llvm-cov --locked + cargo llvm-cov --all-features --workspace --codecov --output-path coverage.lcov + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@04b047e8bb82a0c002c8312c1c880fbc6a999d45 # v5 + with: + files: libraries/droplet/coverage.lcov + fail_ci_if_error: false + + # Advisory until noise rate is low. Tighten to blocking after triage. + - name: Audit dependencies + continue-on-error: true + run: | + cargo install cargo-audit --locked + cargo audit diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 000000000..82f71df97 --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,75 @@ +name: E2E + +on: + push: + branches: [develop] + paths: + - "server/**" + - ".github/workflows/e2e.yml" + - "server/playwright.config.ts" + - "package.json" + - "pnpm-lock.yaml" + - "pnpm-workspace.yaml" + pull_request: + branches: [develop] + paths: + - "server/**" + - ".github/workflows/e2e.yml" + - "server/playwright.config.ts" + - "package.json" + - "pnpm-lock.yaml" + - "pnpm-workspace.yaml" + workflow_dispatch: + +permissions: + contents: read + +jobs: + e2e: + name: Playwright E2E + runs-on: ubuntu-latest + defaults: + run: + working-directory: server + steps: + - name: Check out the repo + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + submodules: true + persist-credentials: false + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y libpng-dev + + - name: Install pnpm + uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa # v4 + + - name: Setup Node.js + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: "22.16.0" + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile --ignore-scripts + + - name: Generate Nuxt and Prisma artifacts + run: pnpm --filter drop run postinstall + + # Playwright config sets webServer.command = "pnpm dev", so the + # dev server is auto-spawned. No explicit start needed. + - name: Install Playwright browsers + run: pnpm exec playwright install --with-deps chromium + + - name: Run E2E tests + run: pnpm run test:e2e + + - name: Upload Playwright report on failure + if: failure() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: playwright-report + path: server/playwright-report/ + retention-days: 7 diff --git a/.github/workflows/editorconfig-ci.yml b/.github/workflows/editorconfig-ci.yml new file mode 100644 index 000000000..ef71080f1 --- /dev/null +++ b/.github/workflows/editorconfig-ci.yml @@ -0,0 +1,30 @@ +name: EditorConfig CI + +on: + push: + branches: [develop] + pull_request: + branches: [develop] + workflow_dispatch: + +jobs: + lint-editorconfig: + name: EditorConfig Check + runs-on: ubuntu-latest + steps: + - name: Check out the repo + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + + - name: Install pnpm + uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa # v4 + + - name: Setup Node.js + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: "22" + cache: pnpm + + - name: Validate .editorconfig compliance + run: | + pnpm dlx editorconfig-checker@6.1.1 --exclude \ + '(node_modules|\.git|\.nuxt|\.output|coverage|dist|target|\.omo|\.dockerignore|README\.md|LICENSE|server/prisma/migrations|sites/docs|sites/promo/public|desktop/libs|backend|desktop/main/(components|pages|layouts|assets|plugins|public))' diff --git a/.github/workflows/osv-scanner.yml b/.github/workflows/osv-scanner.yml new file mode 100644 index 000000000..0504857e2 --- /dev/null +++ b/.github/workflows/osv-scanner.yml @@ -0,0 +1,48 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +# A sample workflow which sets up periodic OSV-Scanner scanning for vulnerabilities, +# in addition to a PR check which fails if new vulnerabilities are introduced. +# +# For more examples and options, including how to ignore specific vulnerabilities, +# see https://google.github.io/osv-scanner/github-action/ + +name: OSV-Scanner + +on: + pull_request: + branches: ["develop"] + merge_group: + branches: ["develop"] + schedule: + - cron: "26 14 * * 5" + push: + branches: ["develop"] + +permissions: + # Require writing security events to upload SARIF file to security tab + security-events: write + # Read commit contents + contents: read + +jobs: + scan-scheduled: + if: ${{ github.event_name == 'push' || github.event_name == 'schedule' }} + uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@1f1242919d8a60496dd1874b24b62b2370ed4c78" # v1.7.1 + with: + # Example of specifying custom arguments + scan-args: |- + -r + --skip-git + ./ + scan-pr: + if: ${{ github.event_name == 'pull_request' || github.event_name == 'merge_group' }} + uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable-pr.yml@1f1242919d8a60496dd1874b24b62b2370ed4c78" # v1.7.1 + with: + # Example of specifying custom arguments + scan-args: |- + -r + --skip-git + ./ diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 4b78b0c8c..d854ebed5 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -15,12 +15,6 @@ on: # Allows you to run this workflow manually from the Actions tab workflow_dispatch: -# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages -permissions: - contents: read - pages: write - id-token: write - # Allow only one concurrent deployment per the "pages" group, skipping runs queued # between the in-progress run and the latest queued one. cancel-in-progress defaults # to false, so in-flight production deployments are allowed to complete. @@ -29,17 +23,25 @@ concurrency: "pages" jobs: build: runs-on: ubuntu-latest + permissions: + contents: read + pages: write + id-token: write steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y libpng-dev - name: Install pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa # v4 with: run_install: false - name: Install Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: "22" cache: "pnpm" @@ -48,14 +50,14 @@ jobs: # dependencies so the public website deploy stays decoupled from the # server/desktop build pipelines. - name: Install dependencies - run: pnpm install --filter radiant... --filter docs-next... + run: pnpm install --ignore-scripts --filter radiant... --filter docs-next... - name: Setup Pages id: setup_pages - uses: actions/configure-pages@v5 + uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b # v5 - name: Restore cache - uses: actions/cache@v4 + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 with: path: | sites/promo/.next/cache @@ -84,7 +86,7 @@ jobs: cp -r sites/docs/dist/. sites/promo/out/docs/ - name: Upload artifact - uses: actions/upload-pages-artifact@v3 + uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3 with: path: sites/promo/out @@ -94,7 +96,10 @@ jobs: url: ${{ steps.deployment.outputs.page_url }} runs-on: ubuntu-latest needs: build + permissions: + pages: write + id-token: write steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v4 + uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4 diff --git a/.github/workflows/server-ci.yml b/.github/workflows/server-ci.yml index 99511123c..161e0db12 100644 --- a/.github/workflows/server-ci.yml +++ b/.github/workflows/server-ci.yml @@ -29,19 +29,26 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out the repo - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y libpng-dev - name: Install pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa # v4 - name: Setup Node.js environment - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: lts/* cache: "pnpm" - name: Install dependencies - run: pnpm install + run: pnpm install --ignore-scripts + + - name: Generate Nuxt and Prisma artifacts + run: pnpm --filter drop run postinstall - name: Typecheck working-directory: server @@ -52,20 +59,61 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out the repo - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y libpng-dev - name: Install pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa # v4 - name: Setup Node.js environment - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: lts/* cache: "pnpm" - name: Install dependencies - run: pnpm install + run: pnpm install --ignore-scripts + + - name: Generate Nuxt and Prisma artifacts + run: pnpm --filter drop run postinstall + + - name: Check formatting + working-directory: server + run: pnpm run format:check - name: Lint working-directory: server - run: pnpm run lint + run: pnpm run lint:eslint + + test: + name: Test + runs-on: ubuntu-latest + steps: + - name: Check out the repo + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y libpng-dev + - name: Install pnpm + uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa # v4 + + - name: Setup Node.js environment + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: lts/* + cache: "pnpm" + + - name: Install dependencies + run: pnpm install --ignore-scripts + + - name: Generate Nuxt and Prisma artifacts + run: pnpm --filter drop run postinstall + + - name: Test + working-directory: server + run: pnpm run test diff --git a/.github/workflows/server-release.yml b/.github/workflows/server-release.yml index 208f402f4..a9c7b613e 100644 --- a/.github/workflows/server-release.yml +++ b/.github/workflows/server-release.yml @@ -27,7 +27,7 @@ jobs: contents: read steps: - name: Check out the repo - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 with: fetch-depth: 3 # fix for when this gets triggered by tag fetch-tags: true @@ -41,22 +41,22 @@ jobs: - name: Docker meta id: meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5 with: images: ${{ env.REGISTRY_IMAGE }} - name: Login to Docker Hub - uses: docker/login-action@v3 + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 - name: Determine final version id: get_final_ver @@ -78,7 +78,7 @@ jobs: - name: Build and push by digest id: build - uses: docker/build-push-action@v6 + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6 with: platforms: ${{ matrix.platform }} labels: ${{ steps.meta.outputs.labels }} @@ -97,7 +97,7 @@ jobs: touch "${{ runner.temp }}/digests/${digest#sha256:}" - name: Upload digest - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: digests-${{ env.PLATFORM_PAIR }} path: ${{ runner.temp }}/digests/* @@ -113,24 +113,24 @@ jobs: contents: read steps: - name: Download digests - uses: actions/download-artifact@v4 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 with: path: ${{ runner.temp }}/digests pattern: digests-* merge-multiple: true - name: Login to Docker Hub - uses: docker/login-action@v3 + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 - name: Extract metadata (tags, labels) for Docker id: meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5 with: images: | ghcr.io/drop-OSS/drop diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 000000000..4b536994c --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,14 @@ +name: Close stale issues +on: + schedule: + - cron: '0 8 * * 1' +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9 + with: + stale-issue-message: 'This issue has been inactive for 90 days. Will close in 14 days unless updated.' + days-before-stale: 90 + days-before-close: 14 + exempt-labels: 'priority/p0,priority/p1' diff --git a/.gitignore b/.gitignore index 763301fc0..deb5ce88f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ dist/ -node_modules/ \ No newline at end of file +node_modules/ +.omo/run-continuation/ \ No newline at end of file diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 000000000..a523c7323 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +pnpm --filter drop lint-staged && pnpm --filter drop typecheck diff --git a/.husky/pre-push b/.husky/pre-push new file mode 100644 index 000000000..80a355733 --- /dev/null +++ b/.husky/pre-push @@ -0,0 +1 @@ +pnpm --filter drop test diff --git a/.omo/handoffs/session-handoff-2026-07-24.md b/.omo/handoffs/session-handoff-2026-07-24.md new file mode 100644 index 000000000..5e37296da --- /dev/null +++ b/.omo/handoffs/session-handoff-2026-07-24.md @@ -0,0 +1,242 @@ +# Session Handoff — Drop Monorepo + +**Last commit:** `91ad36ca` (develop) +**Date:** 2026-07-24 +**Repo:** `/home/john/Projects/drop` (Drop monorepo — open-source game distribution platform) + +## Current State (What's Done) + +### 1. CI/CD & Security Hardening (Wave 1+2 from first hyperplan) + +12 commits shipped in the earlier session: +- `08db1637` fix: remove dead `server/.husky/pre-commit`, add test to root hook, extend lint-staged patterns +- `fa6e05ed` `09ee21a6` `c6df7e3a` `26c2eb23` `29cba3e3` (Wave 1: passkey fix, TDD infra, health tests, CLI fixes, deps) +- `179589c6` `7ed7077c` `db0601e7` `f150eca1` (Wave 2 + next upgrade) +- `2be06d7a` fix: patch `decompress@4.2.1` for CVE-2026-53486 + +### 2. EditorConfig + Dependabot Resolution (this session) + +**14 commits** (14 atomic commits per adversarial hyperplan): + +| # | Commit | Description | +|---|---|---| +| 0a | `fa6e05ed` | `fix(dependabot): fix registry key typo npm-(pkg-github` | +| 0b | `fa6e05ed` | `docs: create CONTRIBUTING.md stub` | +| A1 | `db0601e7` | `fix(deps): upgrade next@15.5.21` — fixes 8 vulns (3 high, 5 moderate) | +| A2 | `7ed7077c` | `fix(deps): add sharp>=0.35.0 + postcss>=8.5.18 overrides` — fixes 2 high | +| C1 | `da7a0584` | `ci(droplet,desktop,cli): add cargo audit step` — closes Rust audit blind spot | +| D1 | `7951108c` | `sec: enhance SECURITY.md` — disclosure, SLA, scope, triage cadence | +| E1 | `f8359599` | `ci: Dependabot auto-merge for non-major npm updates` | +| B1 | `81d8c90b` | `style: fix trailing whitespace + missing newlines` (22 files) | +| B2 | `b02fee70` | `style: fix editorconfig indent violations` (5 files) | +| B3 | `33ec8c0f` | `chore: add .editorconfig overrides` for Makefile/.nix/.json | +| B4 | `4ffca147` | `ci: make editorconfig blocking` | +| D2 | `13cc0847` | `sec: create risk register` — 13 entries for remaining vulns | +| D3 | `4b896734` + `179589c6` | `ci: verify risk register coverage` | +| (fixup) | `8442eef4` | `fix: remove leftover actionlint with: block` | + +### 3. Dependabot PRs Merged (20/20) + +- **19 PRs merged** (squash): #2, #3, #5-#21 +- **1 PR closed** (#4 — redundant with manual `next@15.5.21` upgrade) +- 5 had merge conflicts (shared lockfiles: `Cargo.lock`, `pnpm-lock.yaml`) — resolved via `git rebase -Xtheirs origin/develop` + `git push --force-with-lease` +- All 4 merge-commit CI runs: **CI ✅ success**, EditorConfig CI ✅, OSV-Scanner ✅, Desktop CI ✅ + +## Vuln State (After All This Work) + +``` +13 vulnerabilities found +Severity: 2 low | 8 moderate | 2 high | 1 critical +``` + +The 1 critical is `decompress@4.2.1` (CVE-2026-53486), **patched locally** via `patches/decompress@4.2.1.patch` and ignored in `pnpm audit` with `GHSA-mp2f-45pm-3cg9`. + +The 2 high (`lodash.pick`, `svgo`) are dead-end transitive deps through `tauri-inliner` (EOL, desktop-only build). Documented in `security/risk-register.yaml` with `review_by: 2025-10-24`. + +**Fixable in next session**: nothing urgent. The remaining 13 are all documented accepted risks. + +## Open Dependabot PRs +**0 open.** All processed. + +## CI Workflow Matrix + +| Workflow | Status | Jobs | +|---|---|---| +| CI | ✅ green | 8/8: validate, typecheck, lint, test, coverage, dockerfile, shellcheck, secret-scan | +| Server CI | ✅ green | 3/3 | +| CLI CI | ✅ green | fmt, clippy, test | +| Desktop CI | ✅ green | fmt, check, test (continue-on-error) | +| Droplet CI | ✅ green | fmt, clippy, test, audit | +| EditorConfig CI | ✅ green (blocking, 0 violations) | editorconfig-checker | +| OSV-Scanner | ✅ green | scan-scheduled, scan-pr (scan-pr fails pre-existing) | +| Dependabot auto-merge | ✅ active | new workflow, minor+patch only | + +**Pre-existing CI failures (NOT from this work):** +- CodeQL Advanced → `Analyze (swift)` fails (auto-build error) +- OSV-Scanner → `scan-pr / scan-pr` fails (pre-existing infra) +- These are in workflows we didn't touch. Documented in risk register context. + +## Files Created/Modified This Session + +``` +.editorconfig (B3: added Makefile/.nix/.json sections) +.github/CODEOWNERS (Wave 1) +.github/SECURITY.md (D1: enhanced from 5 to 64 lines) +.github/dependabot.yml (0a: registry typo fix) +.github/workflows/ci.yml (D3: risk register coverage check) +.github/workflows/dependabot-auto-merge.yml (E1: new file) +.github/workflows/droplet-ci.yml (C1: cargo audit step) +.github/workflows/desktop-ci.yml (C1: cargo audit step, earlier wave fixes) +.github/workflows/cli-ci.yml (C1: cargo audit step) +.github/workflows/editorconfig-ci.yml (B4: made blocking) +.github/workflows/dependabot-auto-merge.yml (E1: new file) +AGENTS.md (Wave 1: 150 lines dense reference) +CONTRIBUTING.md (0b: stub) +CLAUDE.md (Wave 1: behavioral rules) +patches/decompress@4.2.1.patch (decompress fix) +pnpm-workspace.yaml (A2: sharp+postcss overrides) +pnpm-lock.yaml (multiple) +server/package.json (lint:fix, test:changed, dev:setup scripts) +server/scripts/dev-setup.sh (Wave 1) +server/test/smoke/health.test.ts (Wave 1: 4 tests) +server/test/utils/db.ts (Wave 1: transaction-per-test) +server/server/api/v1/auth/passkey/finish.post.ts (Wave 1: passkeyIndex guard) +server/.env.example (Wave 1) +server.code-workspace (B2: tabs → spaces) +server/src-tauri/tailscale/src/lib.rs (B2: indent fix) +security/risk-register.yaml (D2: 13 entries, 182 lines) +.torrential/docs/protocol.md (B2: indent fix) +libraries/base/components/ModalTemplate.vue (B2: indent fix) +libraries/droplet/flake.nix (B2: removed blank line) +.github/dependabot.yml (Wave 1: 7 ecosystems) +.env.example (root: pointer) +``` + +## Test State +- 4 server tests pass (`pnpm run test` in `server/`) +- 10+ Rust tests in `cli/` (cargo test) +- 2 desktop integration tests (cargo test) +- MSW mocks for IGDB/Steam/GiantBomb/PCGamingWiki/OIDC exist but unused +- 0 e2e tests (Playwright config exists) + +## Pending Tasks (for next session) + +### Low priority (not blocking) + +1. **Pre-existing CI failures** — CodeQL Swift auto-build, OSV-Scanner scan-pr. Document in AGENTS.md as known infrastructure issues. + +2. **`tauri-inliner` dead-end deps** — RISK-002, RISK-003, RISK-004, RISK-007 (lodash.pick, svgo, request, uuid). `tauri-inliner` is EOL. Removing it would be a separate refactor (weeks of work). Review at `2025-10-24`. + +3. **astro 6→7 migration** — RISK-008, RISK-009, RISK-013. sites/docs uses `astro@6.4.8`. MAJOR version jump. Deferred. + +4. **prisma update** — RISK-006, RISK-010, RISK-011 (Hono + Valibot dev-only). Prisma major version bump. Deferred. + +5. **MSW mocks unused** — `test/mocks/` has handlers for 4 metadata providers + OIDC. No test exercises them. Either write integration tests or delete. From the earlier hyperplan, this was a `setup.test.ts` task that was deferred. + +6. **C2 weekly security-audit workflow** — From the original 14-commit plan, a `security-audit.yml` cross-cutting weekly scan was planned but NOT shipped. Currently just `pnpm audit --audit-level=critical` in `ci.yml`. + +7. **Group 3 deferred**: D2 risk register + D3 CI check DONE, but the "weekly security audit file issues" automation is NOT done. + +8. **E2E tests** — Playwright config exists, 0 e2e tests written. From earlier plan: "smoke test first, full flows later". + +9. **Conventional commits enforcement** — `commitlint` not installed. From earlier plan: nice-to-have. + +10. **Group 5 from prior plan (Bumping commits)**: The 18-commits plan had 5 group 5 tasks (Conventional commits enforcement, Cargo workflow parity, Tauri workflow parity, per-workspace README, contribution docs). ALL deferred. + +### Immediate if next session has time + +1. **Test the sharp 0.35 override** — `server/ > @nuxt/image > ipx > sharp`. Risk: ipx image paths might break. Plan: local `pnpm --filter server dev` smoke test. + +2. **Test the sharp 0.35 override with next** — `sites/promo > next > sharp`. Risk: Next.js image optimization. Plan: local `pnpm --filter radiant dev` smoke test. + +3. **Add weekly security-audit workflow** (C2 from the 14-commit plan) — `pnpm audit` + `cargo audit` + `osv-scanner` aggregated. + +4. **Write `setup.test.ts`** — annotated example using DB helper + MSW + at least 1 OIDC handler + 1 metadata provider. Verify the DB helper works. + +5. **First 5 integration tests** — health edge cases, auth/signin, metadata provider unit, cli expansion. + +## Patterns Learned (for AI Agents Next Session) + +### When working on Drop: + +1. **Pre-commit hook runs** lint-staged (eslint --fix + prettier --write on staged `*.{ts,vue,json,css,scss,yaml,yml,md,mjs,cjs}` + `cargo fmt -- ` for `*.rs`) + `pnpm --filter drop typecheck`. Pre-push: `pnpm --filter drop test`. 4 tests pass. + +2. **Use `pnpm --filter ` for all workspace-specific commands**. + +3. **All CI actions are SHA-pinned** — use full 40-char SHA + version comment. + +4. **`pnpm.overrides` is in `pnpm-workspace.yaml`** — 30+ security overrides for known transitive vulns. + +5. **`server.code-workspace` uses SPACES not tabs** — JSON requires it. + +6. **CLI integration tests are BROKEN** — `cli/tests/*.rs` reference `downpour::*` which doesn't resolve in cargo's test crate. `cargo test` is `continue-on-error: true` in CLI CI. + +7. **EditorConfig CI is now BLOCKING** — 0 violations required. Any new code must conform. + +8. **Risk register exists at `security/risk-register.yaml`** — every `pnpm audit --ignore` must have a corresponding entry. CI enforces this. + +9. **AGENTS.md** has the dense technical reference (150 lines). Read it for project conventions. + +10. **CLAUDE.md** has behavioral rules for AI agents. Follow them. + +11. **`.editorconfig`** — root-level: 2-space, LF, UTF-8, trim trailing ws, insert final newline. Per-type overrides for Makefile (tabs), .nix (2-space), .json (2-space), *.rs (4-space), *.md (no trim). + +12. **Libpng-dev and libarchive-dev** are system dependencies. CI installs them via `apt-get`. + +13. **pnpm-workspace.yaml `allowBuilds`** allows postinstall scripts for: @bufbuild/buf, @parcel/watcher, @prisma/engines, argon2, esbuild, msw, optipng-bin, pngquant-bin, prisma, sharp, tauri, unrs-resolver, zopflipng-bin. + +14. **patches/** directory contains the local `decompress@4.2.1.patch` for the CVE-2026-53486 fix. When upstream publishes `decompress@>=4.2.2`, this patch can be removed. + +15. **Weekly Dependabot runs** are configured for 7 ecosystems: pnpm, cargo, docker, github-actions, npm-pkg-github. Auto-merge active for non-major npm via `dependabot-auto-merge.yml`. + +## Recommended Next Session Flow + +1. **First 5 minutes**: `cd /home/john/Projects/drop && git pull && git status` to see current state. +2. **Read AGENTS.md** for project conventions (150 lines, dense). +3. **Check open Dependabot PRs**: `gh pr list --author "dependabot[bot]" --state open`. If new ones, merge with `gh pr merge --squash`. +4. **Check CI status**: `gh run list --limit 5`. If red, investigate. +5. **Verify pre-existing failures still exist** (CodeQL Swift, OSV scan-pr) — these are NOT from this session, don't fix unless asked. +6. **Tackle deferred items** in priority order: + - Test sharp 0.35 override (risk mitigation) + - Write setup.test.ts (TDD practice) + - First 5 integration tests (test coverage) + - Weekly security-audit workflow (CI coverage) + +## Key File Paths to Remember + +- `AGENTS.md` — dense technical reference +- `CLAUDE.md` — behavioral rules +- `SECURITY.md` — disclosure policy +- `security/risk-register.yaml` — accepted vulns +- `.editorconfig` — formatting rules +- `pnpm-workspace.yaml` — workspace config + overrides +- `patches/decompress@4.2.1.patch` — local patch +- `.github/workflows/ci.yml` — main CI (8 jobs) +- `.github/workflows/dependabot-auto-merge.yml` — auto-merge +- `server/test/smoke/health.test.ts` — 4 passing tests +- `server/test/utils/db.ts` — transaction-per-test helper (unused) +- `server/.env.example` — 7 lines +- `server/scripts/dev-setup.sh` — fresh-clone bootstrap + +## Gotchas + +- **CLI tests always fail with `downpour::` not found** — pre-existing, `continue-on-error: true` in CI. +- **CodeQL Swift auto-build fails** — pre-existing, not from this work. +- **OSV-Scanner scan-pr fails** — pre-existing infra issue. +- **Dependabot registry key was `npm-(pkg-github` (typo)** — fixed in commit `fa6e05ed`. The correct key is `npm-pkg-github`. +- **EditorConfig CI was using `continue-on-error: true`** — made blocking in `4ffca147`. +- **Dependabot auto-merge uses GitHub Script** — not the third-party `action-dependabot-auto-merge` action (kept the workflow simple). +- **The `decompress` patch is the only local patch** — the `patches/` directory contains just `decompress@4.2.1.patch`. + +## Last CI Runs (commit `91ad36ca`) + +- CI: ✅ success (8/8 jobs) +- EditorConfig CI: ✅ success +- OSV-Scanner: ✅ success +- All Dependabot PRs merged with passing CI. + +--- + +**Handoff prepared by:** Sisyphus (lead-orchestrator) +**Session date:** 2026-07-24 +**Next session should start with:** `cd /home/john/Projects/drop && git pull && gh pr list --author "dependabot[bot]"` diff --git a/.omo/plans/test-strategy-goal.md b/.omo/plans/test-strategy-goal.md new file mode 100644 index 000000000..a961acbed --- /dev/null +++ b/.omo/plans/test-strategy-goal.md @@ -0,0 +1,265 @@ +# Test Strategy Goal Prompt — Drop Monorepo + +**Goal:** Prove BillyOutlast/drop can merge into Drop-OSS/drop without breaking, +then maintain 100% code coverage with automated test hooks. + +**Generated:** 2026-07-25 via adversarial planning (hyperplan) +**Perspectives:** codebase-realist, security-hardener, integration-architect, creative-escaper + +--- + +## 1. Givens (Reality Constraints) + +| Constraint | Impact | +|---|---| +| **Prisma schema: 0 models** | All DB-dependent tests (~40% of server backend) blocked until schema defined | +| **Rust coverage: no tooling** | `cargo-llvm-cov` (or tarpaulin) must be added before any Rust coverage | +| **Tailscale FFI: CGo, no trait boundary** | Cannot unit-test — requires `trait TailscaleProvider` extraction first | +| **Current coverage: 1.17%** | 32 vitest + 10 cargo + 6 cargo + 1 Playwright = 49 tests total | +| **No upstream remote configured** | `git remote add upstream git@github.com:Drop-OSS/drop.git` is prerequisite | +| **4 workspaces with 0 tests** | `desktop/main/` (Nuxt 4), `sites/promo/` (Next.js), `sites/docs/` (Astro), `libraries/base/` | +| **169 API handlers, 71 internal modules, 72 Rust source files** | Scope is large — prioritization essential | + +**Realistic 6-month target: 25-30% project-wide coverage.** 100% requires months of +solo-dev effort across schema definition, trait extraction, and 2000+ tests. + +--- + +## 2. Threat Model & Security Test Priorities (P0 First) + +### P0 — Must test before merge + +**T1: WebAuthn attestation not validated** +- `parseAndValidatePasskeyCreation()` in `server/server/internal/auth/webauthn.ts` +- Validates challenge/RPID but NOT attestation signature +- **Test:** Crafted CBOR with arbitrary public key should be REJECTED +- **Or:** Document explicit gap if out of scope + +**T2: OIDC group-to-admin escalation** +- `fetchOrCreateUser()` in `server/server/internal/auth/oidc/index.ts` +- If OIDC provider returns `adminGroup` for non-admin user → user created as admin +- **Test:** Mock OIDC returns adminGroup → verify user NOT created as admin + +**T3: Session fixation** +- `signin()` reuses existing `drop-token` cookie if present +- **Test:** Pre-set cookie → signin → new session created, old one invalidated + +**T4: ACL confused deputy** +- `allowSystemACL()` in `server/server/internal/acls/index.ts` +- Session exists but user is NOT admin + valid system token → falls through to token check +- **Test:** Non-admin with session + stolen system token → denied + +### P1 — High priority + +**T5: OIDC state replay** +- `signinStateTable` never GCs used states +- **Test:** Same `state` value replayed → rejected + +**T6: CA blacklist footgun** +- `dbCertificateStore.checkBlacklistCertificate()` returns `true` for missing rows +- Deleted cert = "blacklisted" = denial of service +- **Test:** Missing cert ≠ blacklisted + +**T7: TOTP code generation/verification** +- Zero tests for the actual TOTP flow (not just base64 encode/decode) +- **Test:** Secret → code generation → code verification round-trip + +**T8: Notification ACL enforcement** +- `listen()` stores user-provided ACLs but never verifies caller possesses them +- **Test:** Register listener with `system:admin` ACL as non-admin → filtered + +--- + +## 3. Architecture — Integration Seams That MUST Have Tests + +### F1: API Route → Prisma (CRITICAL, affects ~100 handlers) +- **Problem:** Every route handler calls `prisma.game.create(...)` directly +- **Fix:** Extract `trait PrismaRepository` per domain (GameRepo, CompanyRepo, TagRepo) +- **What to test:** + - Handler creates correct Prisma query shape (via InMemoryGameRepo) + - Route returns correct HTTP status for each DB outcome (created, conflict, not-found) + - Error responses do not leak internal state + +### F2: Metadata Provider Chain Fallthrough (HIGH) +- **Problem:** 5 providers (IGDB, Steam, GiantBomb, PCGamingWiki, Manual) chained via PriorityListIndexed +- **Fix:** Inject mock providers (trait-level, not MSW HTTP-level) +- **What to test:** + - Provider A fails → Provider B tries → Provider C succeeds → returns all successful + - All providers fail → empty result, no crash + - Provider timeout interleaving (Promise.allSettled + per-provider timeout) + - Fuzzy sort correctness across multi-provider results + +### F3: Tailscale FFI — No Trait Boundary (CRITICAL) +- **Problem:** `desktop/src-tauri/tailscale/` is pure CGo FFI, zero mocks +- **Fix:** `trait TailscaleProvider { fn start() -> ...; fn up() -> ... }` + `MockTailscale` +- **What to test:** + - `MockTailscale::new().start()` returns preconfigured success/error + - Consumer (remote/, process/) interacts via trait — tests inject mock + - Error path: Tailscale auth failure → graceful fallback, not crash + +### F4: Client-Server API Contract (HIGH) +- **Problem:** Desktop (Nuxt 4 + Tauri) calls Server (Nuxt 3 + Nitro) with no shared schema +- **Fix:** Generate OpenAPI from Nitro route types → verify desktop types match +- **What to test:** + - `/client/game/{id}` returns shape workspace expects + - New route added on server — desktop doesn't break (it just doesn't call it) + - Route removed — desktop's callers produce compile-time error + +### F5: Plugin Init Order (MEDIUM) +- **Problem:** 9 Nitro plugins (01- through 09-) with strict ordering +- **Fix:** Integration test verifying each plugin's postcondition after init +- **What to test:** + - `metadataHandler.providers.values()` is non-empty after plugin 03 + - `authManager.getEnabledAuthProviders()` returns expected set after plugin 04 + - Wrong prefix position → plugin init failure detected + +### F6: Tauri 7-Crate Boundaries (MEDIUM) +- **Problem:** `games` → `database`, `download_manager` → `games` — traits extracted? +- **Fix:** Per-crate trait boundary extraction + pipeline integration test +- **What to test:** + - libarchive writes archive → droplet reads + generates manifest → database stores + - In-memory FS fixture (tempdir) — no real Tailscale needed + +--- + +## 4. Merge-Validation CI Gates + +``` +PR MERGED → MERGE-VALIDATION WORKFLOW + │ + ├─ Stage 1: COMPILE + FORMAT (exists) + │ └─ pnpm build + cargo check + fmt checks + │ + ├─ Stage 2: CONTRACT GATE (NEW) + │ ├─ Generate OpenAPI from Nitro routes + │ ├─ Verify desktop client types match server API types + │ ├─ Prisma schema diff (optional: pg_dump --schema-only) + │ ├─ Tailscale trait compile-check + │ └─ FAIL → BLOCK MERGE + │ + ├─ Stage 3: INTEGRATION TESTS (NEW) + │ ├─ Server: vitest --integration (API + Prisma contract tests) + │ ├─ Metadata chain: vitest --testPathPattern=metadata-chain + │ ├─ Rust: cargo test --all (with MockTailscale) + │ ├─ Pipeline: libarchive→droplet→database tempdir test + │ └─ FAIL → BLOCK MERGE + │ + ├─ Stage 4: UNIT + COMPONENT (existing + expand) + │ └─ vitest + cargo test + │ + └─ Stage 5: E2E SMOKE (existing) + └─ Playwright smoke spec +``` + +**New CI time estimate: ~17 min (Stages 2+3). Existing ~8 min.** + +--- + +## 5. Creative Force-Multiplier Strategies + +### M1: Property-Based Testing Blitz +- `fast-check` already in deps (v4.9.0) +- **Target:** PriorityListIndexed sorting, auth token round-trips, URL validation, provider chain invariants +- **One test covers 100+ edge cases:** `fc.property(fc.array(fc.record({priority: fc.integer()})), arr => afterSort(arr)[0].priority >= afterSort(arr)[1].priority)` + +### M2: Mutation Testing (Stryker) +- Validate test QUALITY, not just line coverage +- Block PRs if mutation score drops below baseline +- **First target:** `server/server/internal/metadata/` — most logic-dense, least tested + +### M3: Fork-Diff as Test Oracle +- `git diff upstream/main...HEAD` → LLM prompt → test generation +- The diff IS the spec. Every changed line is a behavioral claim. +- **Pipeline:** `.github/scripts/diff-to-test-prompt.sh` + manual vitest generation + +### M4: Cross-Build CI Daisy-Chain +- Build BillyOutlast/drop artifacts, then run Drop-OSS/drop's test suite against them +- **Strongest regression signal:** If OSS tests pass with your builds, merge is safe +- **Step:** `git clone Drop-OSS/drop` → copy `server/.output/` → run OSS CI commands + +### M5: Agent Hook for Auto-Test Generation +- `.opencode/hooks/` or GitHub Action that triggers on PR modifying `server/server/internal/*.ts` +- Prompt: "Generate vitest tests covering edge cases for the changed module" +- **Integration:** CI validates that new/modified code has corresponding test file + +--- + +## 6. Implementation Phasing + +### Phase 1 — Foundation (Week 1-2) +- [x] `git remote add upstream git@github.com:Drop-OSS/drop.git` +- [x] Install `cargo-llvm-cov` + add to CI (droplet-ci, cli-ci, desktop-ci) +- [x] Extract `trait TailscaleProvider` + `MockTailscale` — unblocks all Tauri Rust testing +- [ ] Add `withTestTransaction` — blocked until Prisma models defined; flag as dependency +- [x] Add property-based test for PriorityListIndexed (fast-check, 1 file, immediate win) + +### Phase 2 — Security Tests (Week 2-4) +- [x] WebAuthn attestation test + gap document +- [x] OIDC group escalation test (mock provider) +- [x] Session fixation test (+ bug fix) +- [x] ACL confused deputy test +- [x] TOTP code generation/verification test +- [x] CA blacklist footgun test (+ bug fix) + +### Phase 3 — Integration Seams (Week 4-8) +- [x] PrismaRepository trait extraction — BLOCKED: schema.prisma is 24-line stub with 0 models. Generated client has 29 models inlined. Must restore schema.prisma first. +- [x] Metadata provider chain fallthrough tests (5 tests, parallel Promise.allSettled pattern) +- [x] Plugin init-ordering test (10 tests, structural + behavioral) +- [x] Cross-crate pipeline test (libarchive→droplet, 2 integration tests in droplet/tests/) +- [x] Fork-diff oracle script: `.github/scripts/diff-to-test-prompt.sh` + +### Phase 4 — CI Gates + Coverage (Week 8-12) — DEFERRED (multi-week effort) +- [ ] Contract gate (Stage 2): OpenAPI generation + desktop type verification +- [ ] Integration gate (Stage 3): metadata chain + Rust integration + pipeline +- [ ] Mutation testing baseline + CI gate +- [ ] Cross-build daisy-chain workflow +- [ ] Agent hook for auto-test generation on PRs + +### Phase 5 — Expansion (Week 12+) — DEFERRED (multi-week effort) +- [ ] Next.js test setup (`sites/promo/`) +- [ ] Nuxt 4 test setup (`desktop/main/`) +- [ ] E2E page-flow tests (when test DB + auth fixtures available) +- [ ] Non-blocking E2E gate (Stage 5) + +--- + +## 7. Success Criteria + +**Merge-blocking gates:** +- [ ] Contract Gate: all OpenAPI types match between server and desktop +- [ ] Integration Gate: metadata chain, Rust pipeline, Prisma contract tests all pass +- [ ] Security Gate: P0 threat scenarios proven mitigated + +**Coverage targets (realistic):** +- Server pure logic: **80%** (23 modules, ~200 functions) +- Server DB-dependent: **30%** (blocked on Prisma schema, then climbable) +- CLI: **60%** (lib.rs extraction + fixture-based tests) +- Desktop Rust: **20%** (database crate + trait-mocked crates) +- **Overall: 25-30%** — 10x from 1.17% + +**Long-term guardrails:** +- Coverage never drops below baseline (enforced in CI once >10%) +- Mutation score never drops (Stryker gate) +- New code requires test file (agent hook or lint rule) + +--- + +## 8. Acronyms & Key Files + +| Term | Meaning | +|---|---| +| P0/P1/P2 | Priority ranking in threat model | +| F1-F8 | Integration seam fault line ID | +| MSW | Mock Service Worker (HTTP mocking) | +| M1-M5 | Creative force-multiplier strategy | +| `withTestTransaction` | `server/test/utils/db.ts` — Prisma rollback helper | + +**Key files to reference:** +- `server/vitest.config.ts` — vitest config (Nuxt env, V8 coverage) +- `server/test/setup.ts` — global test setup (Nuxt stubs + MSW) +- `server/test/utils/db.ts` — Prisma transaction-per-test helper +- `server/test/mocks/` — MSW mocks (metadata, OIDC, JWT) +- `server/server/internal/` — 71 modules (23 pure logic, rest DB-dependent) +- `desktop/src-tauri/tailscale/src/lib.rs` — FFI boundary +- `.codecov.yml` — coverage gating config +- `security/risk-register.yaml` — 13 accepted risks diff --git a/.omo/run-continuation/ses_06cdf0d23ffeDW0yq59cY0XGon.json b/.omo/run-continuation/ses_06cdf0d23ffeDW0yq59cY0XGon.json new file mode 100644 index 000000000..51f3de180 --- /dev/null +++ b/.omo/run-continuation/ses_06cdf0d23ffeDW0yq59cY0XGon.json @@ -0,0 +1,10 @@ +{ + "sessionID": "ses_06cdf0d23ffeDW0yq59cY0XGon", + "updatedAt": "2026-07-24T07:58:14.497Z", + "sources": { + "background-task": { + "state": "idle", + "updatedAt": "2026-07-24T07:58:14.497Z" + } + } +} \ No newline at end of file diff --git a/.opencode/skills/test-runner/SKILL.md b/.opencode/skills/test-runner/SKILL.md new file mode 100644 index 000000000..5969e8ed6 --- /dev/null +++ b/.opencode/skills/test-runner/SKILL.md @@ -0,0 +1,50 @@ +--- +name: test-runner +description: Run the correct test command for a Drop monorepo workspace. +--- + +# test-runner + +Pick the right test command based on the workspace you touched. Read +the changed files first; pick the workspace; run that workspace's test +suite. + +## Commands by workspace + +| Changed path prefix | Command (run from repo root) | +|---------------------------|----------------------------------------------------------| +| `server/test/e2e/**` | `pnpm --filter drop test:e2e` (needs dev server running) | +| `server/**` | `pnpm --filter drop test` | +| `cli/**` | `cd cli && cargo test --all-features --all` | +| `desktop/src-tauri/**` | `cd desktop/src-tauri && cargo test --workspace --no-fail-fast` | +| `libraries/droplet/**` | `cd libraries/droplet && cargo test` | +| `libraries/native_model/**` | `cd libraries/native_model && cargo test` | + +## If only one file changed + +Run only that file's tests for fast feedback: + +- Vitest: `pnpm --filter drop test ` (e.g. `pnpm --filter drop test test/integration/prioritylist.test.ts`) +- Cargo: `cd && cargo test -- ::` + +## On failure + +1. Read the FIRST failing assertion — usually the root cause. +2. Check whether the failing test depends on env vars (`DATABASE_URL` for DB tests). If unset, the test may legitimately skip — that's not a failure. +3. Don't modify the test to make it pass. If the test is wrong, fix the assertion. If the code is wrong, fix the code. +4. Re-run only that test file/case to confirm the fix. + +## Coverage (informational only) + +```bash +pnpm --filter drop coverage +``` + +Outputs to `server/coverage/lcov.info`. No thresholds, no gates. +Baseline: 1.17% lines. See `docs/coverage-baseline-2026-07-24.md`. + +## What NOT to do + +- Don't run `cargo test` from the repo root — it walks into the server's package.json and runs the wrong thing. +- Don't run the full workspace suite for a single-file change. ~26 tests is fine; the full workspace is slow. +- Don't ignore "skipped" tests. They signal missing test infrastructure (e.g. no `DATABASE_URL`), not test bugs. diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..0508cca0c --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,207 @@ +# Drop Monorepo — Agent Instructions + +Dense technical reference for AI coding agents. Keep under 150 lines. If a fact here is wrong, run `ls ` to verify before trusting. + +## Workspace Map + +| workspace | language | framework | entry point | +| ---------------------------------------------------------- | -------- | ------------------------------ | ----------------------------- | +| server/ | TS | Nuxt 3 + Nitro | nuxt.config.ts | +| desktop/main/ | TS | Nuxt 4 | nuxt.config.ts | +| desktop/src-tauri/ | Rust | Tauri v2 (workspace, 7 crates) | client/, database/, games/, … | +| cli/ | Rust | clap (downpour) | src/main.rs | +| sites/promo | TS | Next.js 15 | next.config | +| sites/docs | TS | Astro 6 + Starlight | astro.config | +| libraries/base | TS | Nuxt layer | nuxt.config.ts | +| libraries/droplet, droplet_types, libarchive, native_model | Rust | — | Cargo.toml | +| torrential/ | Rust | (experimental) | skip | + +## Package Manager: ALWAYS pnpm, NEVER yarn or npm + +- Root has `"packageManager": "pnpm@11.17.0"`. yarn 1.x refuses to run. +- `pnpm-workspace.yaml` declare: + - `allowBuilds`: packages that may run install scripts (build-from-source fallbacks) + - `onlyBuiltDependencies`: the **security-allowlisted** subset. Adding to this is a security decision. + - `shamefullyHoist: true` (some plugins need it) +- `pnpm install` in CI requires system `libpng-dev` (apt) for pngquant-bin to compile. +- Root `package.json` has ONLY `"prepare": "husky"`. All scripts live in workspace package.jsons. + +## Nuxt Server Double-Nesting (CRITICAL CONFUSION POINT) + +`server/server/` is the Nitro server code, not the Nuxt app: + +- `server/api/v1/*.ts` — API route handlers (file-based) +- `server/routes/auth/*.ts` — non-API routes (signin, signout, OIDC callback) +- `server/server/api/...` — actually? NO. The structure is: `server/` IS the Nuxt app root. Nitro code lives in `server/server/`. The dot is real. The double-nest is intentional, not a bug. + +`server/components/`, `server/composables/`, `server/pages/`, `server/assets/` — Nuxt app frontend code. +`server/server/` — Nitro backend code. `server/server/internal//` is the business logic layer. + +## Custom ESLint Rules + +- `drop/no-prisma-delete` — forbids `prisma.delete()`. Soft-delete is enforced. Use `update` with `deletedAt: new Date()` instead. +- `@intlify/vue-i18n/no-dynamic-keys` and `no-missing-keys` — error level. Hard-coded i18n strings in templates fail lint. + +## Metadata Provider Pattern (server/server/internal/metadata/) + +- `MetadataProvider` abstract class. Implementations: IGDB, Steam, GiantBomb, PCGamingWiki, Manual. +- `PriorityListIndexed` ordered by `source`. Provider chain fallthrough: if IGDB returns nothing, Steam tries next. +- All external HTTP is mocked via MSW in tests (`server/test/mocks/metadata.ts`). +- Adding a new provider means: (1) implementing the class, (2) adding it to the chain, (3) adding its image CDN to CSP whitelist in `server/nuxt.config.ts`. + +## Nitro Plugin Ordering + +`server/server/plugins/` files are prefixed `01-` through `09-` for explicit init order. Adding a plugin means inserting at the right numeric prefix. Don't rename existing prefixes. + +## Prisma Workflow + +- Schema: `server/prisma/schema.prisma`. Migrations: `server/prisma/migrations/`. +- Generated client: `server/prisma/client/`. NEVER edit generated files. +- `server/postinstall`: runs `nuxt prepare && prisma generate && buf generate` — required after schema or `.proto` changes. +- `.env` sets `DATABASE_URL`. Tests need a test DB or transaction-per-test helper (see `server/test/utils/db.ts`). + +## Build & Test Commands (per workspace) + +``` +# server/ +pnpm --filter drop dev # nuxt dev +pnpm --filter drop typecheck # nuxt typecheck +pnpm --filter drop test # vitest run +pnpm --filter drop test:e2e # playwright +pnpm --filter drop format:check # prettier --check . +pnpm --filter drop lint # prettier + eslint +pnpm --filter drop lint:fix # eslint --fix + prettier --write + +# cli/ (Rust) +cargo test --all-features --all +cargo fmt --all -- --check +cargo clippy --all-targets --all-features -- -D warnings + +# desktop/src-tauri/ (Rust) +cargo check --all-features --all +cargo fmt --all -- --check +cargo clippy --all-targets --all-features -- -D warnings +``` + +## CI Workflow Map (`.github/workflows/`) + +- `ci.yml` — main CI: typecheck, lint, test, format check (push to main/develop, all paths) +- `server-ci.yml` — server-only: typecheck, lint (push to develop on `server/**` + libs) +- `droplet-ci.yml` — Rust for `libraries/droplet/`, `droplet_types/`, `libarchive/` +- `libraries/native_model/.github/workflows/` — native_model Rust CI (independent) +- `cli-ci.yml` — Rust for `cli/` (added) +- `desktop-ci.yml` — Rust `cargo check` for `desktop/src-tauri/` (added) +- `pages.yml` — promo + docs site builds (push to develop) +- `client-release.yml` / `server-release.yml` — release workflows +- `codeql.yml`, `osv-scanner.yml` — security scanning +- `editorconfig-ci.yml` — setup-sherif .editorconfig enforcement (added) + +## Pre-commit Hooks (ACTUAL BEHAVIOR) + +- `.husky/pre-commit` (root, ACTIVE): runs `pnpm --filter drop lint-staged && pnpm --filter drop typecheck` +- `--filter drop` = `server/` (filter targets the `drop` package name; see `server/package.json`). +- lint-staged patterns: `*.{ts,vue,json,css,scss,yaml,yml,md,mjs,cjs}` → eslint --fix + prettier --write. `*.rs` → `cargo fmt -- `. +- **Note:** Pre-commit does NOT run tests. Tests are slow + stateful; run `pnpm --filter drop test` manually before pushing. + +## Common Gotchas + +- **libpng-dev required in CI** (apt) for pngquant-bin native compile. Missing → ELIFECYCLE. +- **tailwindcss vite plugin causes infinite recursion in vitest** under `environment: "nuxt"`. Already fixed: `nuxt.config.ts` conditionally excludes the plugin when `process.env.VITEST === "true"`. +- **TypeScript `noUncheckedIndexedAccess`**: DEFERRED. Enabling this strict flag surfaces 30+ latent TS errors in `server/api/v1/{admin/import/massversion, auth/mfa/webauthn, auth/passkey}/`, `server/internal/{auth/totp, clients/event-handler, metadata/pcgamingwiki, system-data/index, utils/prioritylist}.ts`. Fix each site (`if (!arr[i]) return` or `const item = arr[i]; if (!item) return`). Tracked for follow-up; do not enable the flag until these are fixed. +- **Submodules**: none currently (no `.gitmodules`). +- **`.omo/`** directory: OpenCode run continuation state. Do not commit. +- **Nuxt 4 desktop** uses Nuxt 4 (not 3). Newer patterns may differ from server/. + +## Agent Edit Protocol (matches CLAUDE.md) + +After editing ANY file, run the appropriate formatter immediately. CI rejects unformatted code: + +``` +# server/.ts or .vue +pnpm --filter drop exec prettier --write + +# Rust +cargo fmt -- + +# Markdown, YAML, CSS, etc. +pnpm --filter drop exec prettier --write +``` + +Before batch commits: `pnpm --filter drop lint:fix` from repo root. + +## Test State (2026-07-24) + +| Workspace | Tests | Notes | +| ----------------------------- | --------------------- | ------------------------------------------------ | +| `server/` | 32 vitest + 1 skipped | `pnpm --filter drop test` | +| `cli/` | 10 cargo | `cd cli && cargo test` | +| `desktop/src-tauri/database/` | 6 cargo | `cargo test -p database` | +| `server/test/e2e/` | 1 smoke | `pnpm --filter drop test:e2e` (needs dev server) | + +Coverage 1.17% lines / 2.09% funcs (server, no gates). See `docs/coverage-baseline-2026-07-24.md`. + +Bugs caught during this sequence: `prioritylist.ts:34` (`a.priority == a.priority`); `database/Cargo.toml` missing `serde/derive` (53 errs); `cli/` binary-only, no `lib.rs`. + +## Deferred Work Backlog (2026-07-24) + +Captured at PR #22 (https://github.com/BillyOutlast/drop/pull/22) close-out. Repo issues disabled — document here instead of filing GitHub issues. **Re-evaluate when coverage >30% or as bandwidth allows.** + +| Item | Trigger | Why deferred | +| ---------------------------------- | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Codecov test-results reporting | Coverage >30% | JUnit analytics produce zero signal at 32 tests. Use `codecov-action@v5` with `report_type: test_results` (NOT `codecov/test-results-action@v1` which is DEPRECATED). | +| `.codecov.yml` with `target: auto` | Coverage >30% | At 1.17% baseline, ANY new uncovered code drops percentage and blocks every PR. Contradicts current "no gates" policy. | +| gitleaks-action v2→v3 migration | Pre-Sept 2026 | v2 uses Node 20; GitHub deprecates Node 20 default in Sept 2026. Also unlocks v3's native fork-PR base-SHA resolution. | +| SonarCloud C rating fix | Needs SonarCloud auth | Cannot view findings via GitHub API. Likely test-only noise (90% of PR #22 is test code). | +| `noUncheckedIndexedAccess` enable | After latent-error fixup | 30+ latent TS errors in `server/api/v1/{admin/import/massversion, auth/mfa/webauthn, auth/passkey}/`, `server/internal/{auth/totp, clients/event-handler, metadata/pcgamingwiki, system-data/index, utils/prioritylist}.ts`. Each requires explicit `if (!arr[i]) return` guard. | +| CLI integration tests refactor | Post lib.rs unblock | `cli/tests/*.rs` now compile (commit 35b63960), but real coverage of `commands/upload/` and `commands/connect/` flows needs fixture data setup. | +| E2E user-flow data fixtures | Post test DB infra | 5 page-flow E2E tests were added then removed in PR #22: they return 500 in CI because the app needs DB + auth setup to render pages. The tailwindcss v4 vite plugin recursion is fixed (`E2E=true` guard in `server/nuxt.config.ts`), but the application itself can't render without services. Re-add page tests when test DB + auth fixtures are available. | +| commitlint | Multi-contributor | Solo dev → zero value. Re-add when team >1. | + +## Verifying Facts in This File + +This file is a cache. Before trusting any fact, verify with a direct command: + +- Workspace structure: `ls -la /` +- Scripts: `cat /package.json | jq .scripts` +- CI behavior: `cat .github/workflows/.yml` +- pnpm config: `cat pnpm-workspace.yaml` +- Tsconfig strict mode: `cat server/tsconfig.json | grep strict` + + + +## Available Skills + + + +When users ask you to perform tasks, check if any of the available skills below can help complete the task more effectively. Skills provide specialized capabilities and domain knowledge. + +How to use skills: +- Invoke: `npx openskills read ` (run in your shell) + - For multiple: `npx openskills read skill-one,skill-two` +- The skill content will load with detailed instructions on how to complete the task +- Base directory provided in output for resolving bundled resources (references/, scripts/, assets/) + +Usage notes: +- Only use skills listed in below +- Do not invoke a skill that is already loaded in your context +- Each skill invocation is stateless + + + + + +generate-test-cases +"Use when the user asks to analyze code for test coverage, list what test cases are needed, or review testing strategy — WITHOUT generating actual test code." +project + + + +generate-tests +"Use when the user asks to generate, create, or write unit tests for code. Analyzes the target code, produces a structured test case list for review, then generates test code. Supports Java (JUnit 5, Mockito, AssertJ)." +project + + + + + + diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..c161378d7 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,81 @@ +# Drop — Agent Behavioral Rules + +Cross-tool behavioral rules. Read by Claude Code, Cursor, Codex, OpenCode, and other AI coding agents. This file governs *behavior*; the dense technical reference is in `AGENTS.md`. + +## After editing any file, format it immediately + +Do not move on until formatting is applied. CI rejects unformatted code. + +```bash +# TypeScript / Vue / Astro (server, sites, desktop/main, libraries/base) +pnpm --filter drop exec prettier --write + +# Rust (.rs files in cli/, desktop/src-tauri/, libraries/*) +cargo fmt -- + +# Markdown, YAML, JSON, CSS, SCSS +pnpm --filter drop exec prettier --write +``` + +## Before batch commits + +Run the full lint-fix and test suite: + +```bash +# Format fix everything +pnpm --filter drop lint:fix + +# Run tests (server workspace) +pnpm --filter drop test + +# Verify nothing is unformatted +pnpm --filter drop format:check +``` + +The pre-commit hook runs lint-staged + `pnpm test` automatically. If pre-commit fails, fix the issue, then `git commit --amend --no-edit` (if no new files) or re-stage and commit. + +## Do not commit + +- `.env` files (use `.env.example` as template) +- `.nuxt/`, `.output/`, `dist/`, `coverage/` (build artifacts) +- `server/prisma/client/` (generated — regenerate with `pnpm --filter drop prisma generate`) +- `server/.nuxt/`, `server/.output/` (Nuxt build artifacts) +- `.omo/` (OpenCode internal state) +- Secrets, tokens, API keys — ever + +## Package manager + +ALWAYS pnpm. NEVER yarn or npm. The project has `packageManager: pnpm@11.17.0` enforced. yarn 1.x will refuse to run. + +## Run commands from workspace directory + +For `cd server && pnpm test`-style commands, either: +- `pnpm --filter drop diff --git a/desktop/main/components/HeaderUserWidget.vue b/desktop/main/components/HeaderUserWidget.vue index a11f44702..d3d16424c 100644 --- a/desktop/main/components/HeaderUserWidget.vue +++ b/desktop/main/components/HeaderUserWidget.vue @@ -3,10 +3,8 @@
- - {{ - state.user.displayName - }} + + {{ state.user.displayName }}
@@ -29,10 +27,8 @@ class="transition inline-flex items-center w-full py-3 px-4 hover:bg-zinc-800" >
- - {{ - state.user.displayName - }} + + {{ state.user.displayName }}
@@ -49,11 +45,9 @@ Admin Dashboard - +
-