diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fdbcd4e..61953a4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,5 +1,10 @@ name: CI +# A newer push to the same PR obsoletes the running build: cancel it. +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + on: pull_request: branches: [main] @@ -25,9 +30,6 @@ jobs: - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 - - name: Check - run: cargo check --verbose - - name: Format run: cargo fmt --all -- --check diff --git a/Cargo.toml b/Cargo.toml index 3ed9a2e..0f5bc15 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,8 @@ name = "colony" version = "0.8.0" edition = "2021" +# Enforced floor for the README's "Rust 1.80+" claim (std::sync::LazyLock). +rust-version = "1.80" [dependencies] anyhow = "1.0.100" diff --git a/docs/architecture.md b/docs/architecture.md index 35f0da7..7d5833f 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -30,9 +30,12 @@ src/ ├── update.rs — Handlers for each Message variant ├── github.rs — GitHub API, ETag cache, manifests, release │ asset resolution, platform auto-detection -├── download.rs — Asset/archive downloads, extraction, self-update -├── signing.rs — ed25519 verification of signed launcher updates -├── persistence.rs — Data dirs, install state, on-disk caches, favorites +├── download.rs — Asset/archive downloads, extraction, app-signature +│ verification, self-update +├── signing.rs — ed25519 verification (launcher AND app releases) +├── icons.rs — PNG decoding for per-app grid icons +├── persistence.rs — Data dirs, install state, on-disk caches, favorites, +│ desktop entries (Linux) ├── config.rs — Locating external config (categories.json, colony.toml) ├── oauth.rs — Device Flow OAuth (login, token, keychain) ├── scan.rs — System application scanning (Linux/Windows/macOS) @@ -45,7 +48,9 @@ src/ ├── app_grid.rs — Application card grid with search ├── detail.rs — Detail view (README, changelog, license, actions) ├── settings.rs — Settings panel (theme, language, about, updates) - └── github_panel.rs — GitHub connect/disconnect, Device Flow UI + ├── github_panel.rs — GitHub connect/disconnect, Device Flow UI + ├── markdown_blocks.rs — Cached-block Markdown rendering + └── tutorial.rs — First-launch guided tour (spotlight overlay) ``` ## Data flow (Elm architecture) @@ -94,13 +99,14 @@ All async operations (API calls, downloads, scanning) return a `Task` t 1. Compares `CARGO_PKG_VERSION` vs latest release from `Project-Colony/Colony` 2. Downloads the platform-specific binary to `update-staging/` -3. Replacement sequence: backup to `.old` → copy new binary → chmod 755 +3. Replacement sequence: backup to `.old` → write the signature-verified + bytes (never a re-read of the staged file) → chmod 755 4. Automatic rollback if copy fails 5. Spawns the new binary → exits the old one ## Tests -62 unit tests covering: +105 unit tests covering: - `colony.json` manifest parsing (full, minimal, with pattern, with archives) - Platform auto-detection from release assets - `release_files` construction from assets @@ -109,5 +115,9 @@ All async operations (API calls, downloads, scanning) return a `Task` t - Environment variable expansion - Application categorization - Section filters -- Localization (EN/FR) +- Localization (EN/FR, key parity between languages) - Preferences serialization +- Update-loop state transitions (catalog refresh, update queue, badges, + launcher-check outcomes, cancel semantics) via a hermetic test App +- filePattern globs with exclusions, signature parsing (strict ed25519), + typed HTTP-status classification diff --git a/src/github.rs b/src/github.rs index 1d55e91..8b60870 100644 --- a/src/github.rs +++ b/src/github.rs @@ -1187,6 +1187,71 @@ mod tests { ); } + #[test] + fn spec_conformant_manifest_parses_field_for_field() { + // Locks docs/colony-spec.md <-> code parity: this sample uses every + // documented manifest field with the spec's exact camelCase names. + // If a rename or removal breaks the spec, this test fails first. + let json = r#"{ + "name": "Lilypad", + "category": "Security", + "platforms": ["windows", "linux", "macos", "macos-x86"], + "icon": "assets/icons/icon.png", + "signed": true, + "releaseFiles": { + "linux": { + "tag": "latest", + "filePattern": "lilypad-*-linux.tar.gz, !*-arm64*", + "binary": "lilypad-cli", + "sha256": "abc123" + }, + "windows": { + "tag": "v1.0.0", + "file": "lilypad-windows.zip", + "binary": "lilypad-cli.exe" + } + } + }"#; + let m: ColonyManifest = serde_json::from_str(json).expect("spec sample must parse"); + assert_eq!(m.name, "Lilypad"); + assert_eq!(m.category, "Security"); + assert_eq!(m.platforms.len(), 4); + assert_eq!(m.icon.as_deref(), Some("assets/icons/icon.png")); + assert!(m.signed); + let linux = &m.release_files["linux"]; + assert_eq!(linux.tag, "latest"); + assert_eq!( + linux.file_pattern.as_deref(), + Some("lilypad-*-linux.tar.gz, !*-arm64*") + ); + assert_eq!(linux.binary.as_deref(), Some("lilypad-cli")); + assert_eq!(linux.sha256.as_deref(), Some("abc123")); + let windows = &m.release_files["windows"]; + assert_eq!(windows.tag, "v1.0.0"); + assert_eq!(windows.file.as_deref(), Some("lilypad-windows.zip")); + // Every spec category value (and its documented aliases) maps to a + // real category - never silently to Other (except Other itself). + for cat in [ + "Development", + "Graphics", + "Network", + "Office", + "Multimedia", + "System", + "Utility", + "Utilities", + "Security", + "Game", + "Games", + ] { + assert_ne!( + crate::scan::AppCategory::from_name(cat), + crate::scan::AppCategory::Other, + "spec category '{cat}' must not fall back to Other" + ); + } + } + #[test] fn manifest_signed_flag_parses_and_defaults_off() { let json = r#"{ "name": "App", "category": "Utility", "signed": true }"#;