From 953f7f916aa353c87fb9f9de8531c86e278aec4f Mon Sep 17 00:00:00 2001 From: Tauan BF <11513929+tauanbinato@users.noreply.github.com> Date: Fri, 25 Sep 2026 16:43:15 -0300 Subject: [PATCH 1/5] Build release binaries for Linux, macOS and Windows Each tag builds five targets, attaches the archives with SHA-256 checksums and build provenance to the GitHub release, and publishes to crates.io only when every target built. install.sh installs a checked binary, and cargo binstall finds the archives. CI also tests on macOS and Windows. --- .github/workflows/build.yml | 79 +++++++++++++++++++++++++++++++ .github/workflows/ci.yml | 14 ++++++ .github/workflows/release.yml | 41 ++++++++++++---- Cargo.toml | 9 ++++ install.sh | 88 +++++++++++++++++++++++++++++++++++ 5 files changed, 222 insertions(+), 9 deletions(-) create mode 100644 .github/workflows/build.yml create mode 100755 install.sh diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..e523b21 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,79 @@ +name: Build +# Release binaries for every supported platform. The release workflow calls +# this and attaches the archives to the GitHub release; pull requests that +# change the build or the dependencies run it too, so a platform that stops +# building is caught before a tag. +on: + workflow_call: + pull_request: + paths: [".github/workflows/build.yml", "Cargo.toml", "Cargo.lock"] +permissions: + contents: read +jobs: + build: + strategy: + fail-fast: false + matrix: + include: + - target: x86_64-unknown-linux-musl + os: ubuntu-latest + - target: aarch64-unknown-linux-musl + os: ubuntu-24.04-arm + - target: aarch64-apple-darwin + os: macos-latest + - target: x86_64-apple-darwin + os: macos-latest + # Cross-compiled on Apple silicon; runs only under Rosetta. + cross: true + - target: x86_64-pc-windows-msvc + os: windows-latest + runs-on: ${{ matrix.os }} + env: + TARGET: ${{ matrix.target }} + defaults: + run: + shell: bash + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Install Rust + run: rustup toolchain install stable --profile minimal --target "$TARGET" + - name: Install musl + if: contains(matrix.target, 'musl') + run: sudo apt-get update && sudo apt-get install -y musl-tools + - run: cargo +stable build --release --locked --target "$TARGET" + - name: Package + run: | + version=$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[0].version') + name="jevgate-$version-$TARGET" + exe=jevgate + [[ "$TARGET" == *windows* ]] && exe=jevgate.exe + mkdir -p "dist/$name" + cp "target/$TARGET/release/$exe" README.md CHANGELOG.md LICENSE-APACHE LICENSE-MIT NOTICE "dist/$name/" + cd dist + if [[ "$TARGET" == *windows* ]]; then + archive="$name.zip" + 7z a -tzip "$archive" "$name" > /dev/null + else + archive="$name.tar.gz" + tar -czf "$archive" "$name" + fi + if command -v sha256sum > /dev/null; then + sha256sum "$archive" > "$archive.sha256" + else + shasum -a 256 "$archive" > "$archive.sha256" + fi + echo "BINARY=dist/$name/$exe" >> "$GITHUB_ENV" + - name: Smoke test + if: ${{ !matrix.cross }} + run: | + "$BINARY" --version + "$BINARY" rules > /dev/null + "$BINARY" check --dry-run src/main.rs > /dev/null + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: dist-${{ matrix.target }} + path: | + dist/*.tar.gz + dist/*.zip + dist/*.sha256 + if-no-files-found: error diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b351753..2364788 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,15 +14,29 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Install Rust run: rustup toolchain install stable --profile minimal --component rustfmt,clippy + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 - run: cargo +stable fmt --check - run: cargo +stable clippy --locked --all-targets -- -D warnings - run: cargo +stable test --locked - run: cargo +stable package --locked + test: + strategy: + fail-fast: false + matrix: + os: [macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Install Rust + run: rustup toolchain install stable --profile minimal + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 + - run: cargo +stable test --locked msrv: runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - run: rustup toolchain install 1.90.0 --profile minimal + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 - run: cargo +1.90.0 check --locked deny: runs-on: ubuntu-latest diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index db44110..aa55b83 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,7 +1,8 @@ name: Release # Pushing an annotated tag `vX.Y.Z` on main publishes that version: the CI -# checks run first, then the crate goes to crates.io and the tag's message -# body becomes the GitHub release notes. Every step can be rerun safely. +# checks run and the binaries build first, then the crate goes to crates.io and +# the tag's message body becomes the notes of a GitHub release that carries the +# binaries, their checksums and build provenance. Every step can be rerun safely. on: push: tags: ["v*"] @@ -13,14 +14,17 @@ concurrency: jobs: ci: uses: ./.github/workflows/ci.yml + build: + uses: ./.github/workflows/build.yml publish: - needs: ci + needs: [ci, build] runs-on: ubuntu-latest # crates.io trusts only this workflow in this environment (Trusted Publishing). environment: crates-io permissions: contents: write id-token: write + attestations: write env: TAG: ${{ github.ref_name }} GH_TOKEN: ${{ github.token }} @@ -60,16 +64,35 @@ jobs: run: cargo +stable publish --locked env: CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }} + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + pattern: dist-* + path: dist + merge-multiple: true + - name: Collect the checksums + run: cat dist/*.sha256 > dist/SHA256SUMS + - uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 + with: + subject-path: | + dist/*.tar.gz + dist/*.zip - name: Create the GitHub release run: | if gh release view "$TAG" > /dev/null 2>&1; then - echo "Release $TAG already exists" + # A rerun attaches only the archives a failed run left out. + present=$(gh release view "$TAG" --json assets -q '.assets[].name') + for file in dist/*; do + grep -qxF "$(basename "$file")" <<< "$present" || gh release upload "$TAG" "$file" + done exit 0 fi - cat - "$RUNNER_TEMP/notes.md" > "$RUNNER_TEMP/release.md" <<'EOF' - ```sh - cargo install jevgate --locked - ``` + cat - "$RUNNER_TEMP/notes.md" > "$RUNNER_TEMP/release.md" < --repo $GITHUB_REPOSITORY\`. EOF - gh release create "$TAG" --verify-tag --latest --title "JevGate $VERSION" --notes-file "$RUNNER_TEMP/release.md" + gh release create "$TAG" dist/* --verify-tag --latest --title "JevGate $VERSION" --notes-file "$RUNNER_TEMP/release.md" diff --git a/Cargo.toml b/Cargo.toml index 36e38d0..caca02d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,15 @@ categories = ["command-line-utilities", "development-tools"] include = ["/src/**", "/tests/**", "/Cargo.toml", "/Cargo.lock", "/README.md", "/CHANGELOG.md", "/LICENSE-*", "/NOTICE"] license = "MIT OR Apache-2.0" +# `cargo binstall jevgate` downloads the archives the release workflow attaches. +[package.metadata.binstall] +pkg-url = "{ repo }/releases/download/v{ version }/{ name }-{ version }-{ target }{ archive-suffix }" +bin-dir = "{ name }-{ version }-{ target }/{ bin }{ binary-ext }" +pkg-fmt = "tgz" + +[package.metadata.binstall.overrides.x86_64-pc-windows-msvc] +pkg-fmt = "zip" + [dependencies] anyhow = "=1.0.104" clap = { version = "=4.6.6", features = ["derive"] } diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..62e2d73 --- /dev/null +++ b/install.sh @@ -0,0 +1,88 @@ +#!/bin/sh +# Install a JevGate release binary on Linux or macOS. +# +# curl -fsSL https://raw.githubusercontent.com/Tech-Byte-Frontier/jevgate/main/install.sh | sh +# +# JEVGATE_VERSION version to install, such as 0.17.0 (default: the latest release) +# JEVGATE_INSTALL_DIR directory to install into (default: ~/.local/bin) +# +# The archive is checked against its published SHA-256 before anything is +# installed. On Windows, use `cargo binstall jevgate` or download the zip from +# the release page. +set -eu + +repo="Tech-Byte-Frontier/jevgate" +install_dir="${JEVGATE_INSTALL_DIR:-$HOME/.local/bin}" + +fail() { + echo "jevgate install: $*" >&2 + exit 1 +} + +fetch() { + if command -v curl > /dev/null 2>&1; then + curl -fsSL --proto '=https' --tlsv1.2 -o "$2" "$1" + elif command -v wget > /dev/null 2>&1; then + wget -q --https-only -O "$2" "$1" + else + fail "curl or wget is needed" + fi +} + +sha256() { + if command -v sha256sum > /dev/null 2>&1; then + sha256sum "$1" | cut -d ' ' -f 1 + elif command -v shasum > /dev/null 2>&1; then + shasum -a 256 "$1" | cut -d ' ' -f 1 + else + fail "sha256sum or shasum is needed to check the download" + fi +} + +case "$(uname -s)-$(uname -m)" in + Linux-x86_64 | Linux-amd64) target=x86_64-unknown-linux-musl ;; + Linux-aarch64 | Linux-arm64) target=aarch64-unknown-linux-musl ;; + Darwin-x86_64) target=x86_64-apple-darwin ;; + Darwin-arm64) target=aarch64-apple-darwin ;; + *) fail "no release binary for $(uname -s) $(uname -m); build it with: cargo install jevgate --locked" ;; +esac + +tmp=$(mktemp -d) +trap 'rm -rf "$tmp"' EXIT + +version="${JEVGATE_VERSION:-}" +if [ -z "$version" ]; then + # github.com/…/releases/latest redirects to …/releases/tag/vX.Y.Z; unlike + # the API, it has no rate limit for runners sharing an address. + if command -v curl > /dev/null 2>&1; then + latest=$(curl -fsSLI --proto '=https' -o /dev/null -w '%{url_effective}' "https://github.com/$repo/releases/latest") || latest="" + else + latest=$(wget -q --https-only -S --spider "https://github.com/$repo/releases/latest" 2>&1 | sed -n 's/^ *[Ll]ocation: *//p' | tail -n 1) + fi + version="${latest##*/tag/}" + case "$latest" in */tag/v*) ;; *) version="" ;; esac + [ -n "$version" ] || fail "could not find the latest release; set JEVGATE_VERSION" +fi +version="${version#v}" + +archive="jevgate-$version-$target.tar.gz" +url="https://github.com/$repo/releases/download/v$version/$archive" +echo "Downloading JevGate $version for $target" +fetch "$url" "$tmp/$archive" || fail "could not download $url" +fetch "$url.sha256" "$tmp/$archive.sha256" || fail "could not download $url.sha256" + +expected=$(cut -d ' ' -f 1 < "$tmp/$archive.sha256") +actual=$(sha256 "$tmp/$archive") +[ "$expected" = "$actual" ] || fail "checksum mismatch for $archive (expected $expected, got $actual)" + +tar -xzf "$tmp/$archive" -C "$tmp" +mkdir -p "$install_dir" +cp "$tmp/jevgate-$version-$target/jevgate" "$install_dir/jevgate.tmp" +chmod 755 "$install_dir/jevgate.tmp" +mv -f "$install_dir/jevgate.tmp" "$install_dir/jevgate" + +echo "Installed $("$install_dir/jevgate" --version) to $install_dir/jevgate" +case ":$PATH:" in + *":$install_dir:"*) ;; + *) echo "Add $install_dir to your PATH to run jevgate" ;; +esac From e3022bf0e939c734b27ce02247b14dfce8b5b5ec Mon Sep 17 00:00:00 2001 From: Tauan BF <11513929+tauanbinato@users.noreply.github.com> Date: Fri, 25 Sep 2026 16:47:39 -0300 Subject: [PATCH 2/5] Build and test on Windows Imports and tests that only Unix uses are gated to it. The README installs release binaries first. --- README.md | 8 ++++++-- src/auth/file.rs | 3 ++- src/storage/mod.rs | 1 + tests/cli/main.rs | 6 ++---- tests/cli/preview.rs | 1 + tests/cli/watch.rs | 2 +- 6 files changed, 13 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 2592201..f6297c7 100644 --- a/README.md +++ b/README.md @@ -127,10 +127,14 @@ Other files, such as Kotlin, are listed as skipped with the reason and never fai ## Install ```sh -cargo install jevgate --locked +curl -fsSL https://raw.githubusercontent.com/Tech-Byte-Frontier/jevgate/main/install.sh | sh # Linux and macOS +cargo binstall jevgate # any platform, with cargo-binstall +cargo install jevgate --locked # build from source; needs Rust 1.90 or later ``` -JevGate needs Rust 1.90 or later to build, and a [TypeSafe API key](https://console.typesafe.ai/settings/keys) to review. Git is needed only for `--base` and the staleness rule. +Each [release](https://github.com/Tech-Byte-Frontier/jevgate/releases) has binaries for Linux (x86_64 and arm64, static), macOS (Apple silicon and Intel) and Windows (x86_64), with SHA-256 checksums and build provenance: `gh attestation verify --repo Tech-Byte-Frontier/jevgate`. The install script checks the checksum and installs to `~/.local/bin`; set `JEVGATE_VERSION` or `JEVGATE_INSTALL_DIR` to change the version or place. + +Reviewing needs a [TypeSafe API key](https://console.typesafe.ai/settings/keys). Git is needed only for `--base` and the staleness rule. ## Quick start diff --git a/src/auth/file.rs b/src/auth/file.rs index e8790b5..b50b48b 100644 --- a/src/auth/file.rs +++ b/src/auth/file.rs @@ -3,7 +3,7 @@ use super::secret::{MAX_KEY_BYTES, Secret}; use anyhow::{Context, Result, ensure}; use std::{ fs, - io::{Read, Write}, + io::Read, path::{Path, PathBuf}, }; @@ -116,6 +116,7 @@ pub fn save(path: &Path, secret: &Secret) -> Result<()> { } #[cfg(unix)] { + use std::io::Write; use std::os::unix::fs::{DirBuilderExt, OpenOptionsExt}; let parent = path.parent().context("Missing credential directory")?; fs::DirBuilder::new() diff --git a/src/storage/mod.rs b/src/storage/mod.rs index e513e7a..02c1959 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -271,6 +271,7 @@ mod tests { assert!(Store::open(&project.0).is_ok()); } + #[cfg(unix)] #[test] fn storage_rejects_symlinked_state_directory() { let project = crate::tests::Project::new(); diff --git a/tests/cli/main.rs b/tests/cli/main.rs index 54c3e59..b6a95f4 100644 --- a/tests/cli/main.rs +++ b/tests/cli/main.rs @@ -6,12 +6,10 @@ mod preview; mod rules; #[path = "../support/temp_dir.rs"] mod temp_dir; +#[cfg(unix)] mod watch; -use std::{ - process::{Command, Stdio}, - time::{Duration, Instant}, -}; +use std::process::{Command, Stdio}; struct Project(temp_dir::TempDir); impl Project { diff --git a/tests/cli/preview.rs b/tests/cli/preview.rs index fb48e76..eb72389 100644 --- a/tests/cli/preview.rs +++ b/tests/cli/preview.rs @@ -91,6 +91,7 @@ fn default_preview_sends_units_without_automatic_context_or_state() { #[test] fn browser_report_is_local_and_does_not_change_json_or_failure_status() { use std::os::unix::fs::PermissionsExt; + use std::time::{Duration, Instant}; let project = Project::new(); std::fs::write(project.0.join("api.py"), "def value(rows):\n total = 0\n for row in rows:\n total += row\n total *= 2\n return total\n").unwrap(); let bin = project.0.join("bin"); diff --git a/tests/cli/watch.rs b/tests/cli/watch.rs index 246cbb6..b761604 100644 --- a/tests/cli/watch.rs +++ b/tests/cli/watch.rs @@ -1,7 +1,7 @@ //! Watching: debounced updates and the writer lock. use super::*; +use std::time::{Duration, Instant}; -#[cfg(unix)] #[test] fn watcher_debounces_updates_and_releases_lock_after_credential_failure() { let project = Project::new(); From f25d8994091ab8e6f654963e243b05ed2404ad22 Mon Sep 17 00:00:00 2001 From: Tauan BF <11513929+tauanbinato@users.noreply.github.com> Date: Fri, 25 Sep 2026 16:57:18 -0300 Subject: [PATCH 3/5] Name files with forward slashes on Windows Repository-relative paths are built with / on every platform, so path rules (Next.js routes, Django modules, documentation roles) match on Windows and reports, requests and baselines name a file the same way everywhere. Requests on Unix are unchanged. --- src/context.rs | 5 ++--- src/discovery/mod.rs | 13 ++++++++++++- src/docs/discover.rs | 8 ++++---- src/init.rs | 2 +- src/inventory/django.rs | 4 ++-- src/inventory/mod.rs | 10 ++++------ 6 files changed, 25 insertions(+), 17 deletions(-) diff --git a/src/context.rs b/src/context.rs index e0abf53..834f42a 100644 --- a/src/context.rs +++ b/src/context.rs @@ -43,8 +43,7 @@ pub fn collect(args: &CheckArgs, context: &ConfigContext) -> Result Result Result { + let relative = path.strip_prefix(root)?; + Ok(if cfg!(windows) { + PathBuf::from(relative.to_string_lossy().replace('\\', "/")) + } else { + relative.to_path_buf() + }) +} pub const SKIPPED_DIRS: &[&str] = &[ "node_modules", diff --git a/src/docs/discover.rs b/src/docs/discover.rs index 68c2328..25d9c27 100644 --- a/src/docs/discover.rs +++ b/src/docs/discover.rs @@ -147,7 +147,7 @@ pub fn discover(root: &Path) -> Result { .build(); for entry in visible { let entry = entry.context("Failed while discovering documentation")?; - let relative = entry.path().strip_prefix(root)?.to_path_buf(); + let relative = crate::discovery::relative(entry.path(), root)?; if entry.file_type().is_some_and(|t| t.is_dir()) { found.directories.insert(relative); } else if agent_file(&relative) { @@ -202,7 +202,7 @@ fn walk_doc_dir(root: &Path, dir: &Path, found: &mut Found) -> Result<()> { .build(); for entry in walk { let entry = entry.context("Failed while discovering documentation")?; - let relative = entry.path().strip_prefix(root)?.to_path_buf(); + let relative = crate::discovery::relative(entry.path(), root)?; if entry.file_type().is_some_and(|t| t.is_file()) && project_doc(&relative) { found.project.insert(relative); } @@ -221,7 +221,7 @@ fn walk_agent_dir(root: &Path, dir: &Path, found: &mut Found) -> Result<()> { .build() { let entry = entry.context("Failed while discovering agent instructions")?; - let relative = entry.path().strip_prefix(root)?.to_path_buf(); + let relative = crate::discovery::relative(entry.path(), root)?; if !entry.file_type().is_some_and(|t| t.is_dir()) && agent_file(&relative) { add_agent(root, relative, found); } @@ -241,7 +241,7 @@ fn add_agent(root: &Path, relative: PathBuf, found: &mut Found) { let target = path .canonicalize() .ok() - .and_then(|t| t.strip_prefix(root).ok().map(Path::to_path_buf)); + .and_then(|t| crate::discovery::relative(&t, root).ok()); found.links.push((relative, target)); } else if metadata.is_file() { found.agent.insert(relative); diff --git a/src/init.rs b/src/init.rs index a4a5c7d..ca2f71a 100644 --- a/src/init.rs +++ b/src/init.rs @@ -37,7 +37,7 @@ fn source_patterns(root: &Path) -> Result> { if !entry.file_type().is_some_and(|t| t.is_file()) { continue; } - let relative = entry.path().strip_prefix(root)?; + let relative = &crate::discovery::relative(entry.path(), root)?; if !syntax::supported(relative) || !matches!(classifier.role(relative), "source" | "test") { continue; } diff --git a/src/inventory/django.rs b/src/inventory/django.rs index af51bcb..ba9ff28 100644 --- a/src/inventory/django.rs +++ b/src/inventory/django.rs @@ -32,7 +32,7 @@ pub(super) fn select_settings(context: &ConfigContext, boundary: &Boundary, inpu .filter(|e| e.file_type().is_some_and(|t| t.is_file())) .map(|e| e.path().to_path_buf()); for path in walked.chain(workflows) { - let Ok(relative) = path.strip_prefix(&context.root) else { + let Ok(relative) = &crate::discovery::relative(&path, &context.root) else { continue; }; if !crate::analysis::django::selection_file(relative) @@ -77,7 +77,7 @@ pub(super) fn unescaped_templates( let mut templates = Vec::new(); for entry in walker(&context.root).flatten() { let path = entry.path(); - let Ok(relative) = path.strip_prefix(&context.root) else { + let Ok(relative) = &crate::discovery::relative(path, &context.root) else { continue; }; let Some(name) = crate::analysis::django::template_name(relative) else { diff --git a/src/inventory/mod.rs b/src/inventory/mod.rs index b9c1301..33a98f3 100644 --- a/src/inventory/mod.rs +++ b/src/inventory/mod.rs @@ -142,7 +142,7 @@ fn source_paths( if !entry.file_type().is_some_and(|t| t.is_file()) { continue; } - let relative = path.strip_prefix(&context.root)?; + let relative = &discovery::relative(path, &context.root)?; if discovery::source(relative, &args.source_extension) && selected(relative) && boundary.permits(relative) @@ -167,7 +167,7 @@ fn configuration_files( let mut files = Vec::new(); if args.enabled(crate::catalog::ACCESS_CONTROL) { for entry in walker(&context.root).flatten() { - let relative = entry.path().strip_prefix(&context.root)?; + let relative = &discovery::relative(entry.path(), &context.root)?; if entry.file_type().is_some_and(|t| t.is_file()) && relative.extension().is_some_and(|e| e == "sql") && in_scope(relative) @@ -186,7 +186,7 @@ fn configuration_files( .flatten() { let path = entry.path(); - let relative = path.strip_prefix(&context.root)?; + let relative = &discovery::relative(&path, &context.root)?; if path.is_file() && path.extension().is_some_and(|e| e == "yml" || e == "yaml") && in_scope(relative) @@ -235,9 +235,7 @@ fn load( context: &ConfigContext, extra: &[super::context::ContextInput], ) -> Result { - let relative = path - .strip_prefix(&context.root) - .context("Source outside root")?; + let relative = &discovery::relative(&path, &context.root).context("Source outside root")?; let mut result = pending_result(relative, &role, args, extra); if !matches!(role.as_str(), "source" | "test") { return Ok(excluded(result, &role, relative)); From 3a4e60ffc335f88c030f9a4ccf8891915e64a12c Mon Sep 17 00:00:00 2001 From: Tauan BF <11513929+tauanbinato@users.noreply.github.com> Date: Fri, 25 Sep 2026 17:13:32 -0300 Subject: [PATCH 4/5] Name manifests with forward slashes on Windows --- src/docs/project.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/docs/project.rs b/src/docs/project.rs index 19ac891..c3a8cf1 100644 --- a/src/docs/project.rs +++ b/src/docs/project.rs @@ -58,7 +58,10 @@ pub fn read(root: &Path, visited: &BTreeSet) -> Project { fn manifests(root: &Path, base: &Path, linters: &mut BTreeSet) -> Vec { let dir = root.join(base); let read = |name: &str| std::fs::read_to_string(dir.join(name)).ok(); - let path = |name: &str| base.join(name).to_string_lossy().into_owned(); + let path = |name: &str| match base.to_string_lossy() { + base if base.is_empty() => name.to_string(), + base => format!("{base}/{name}"), + }; for (file, tool) in CONFIG_FILES { if dir.join(file).is_file() { linters.insert((*tool).into()); From 95cb54fb2886b1fa61c56df4debb6f181fdf3b49 Mon Sep 17 00:00:00 2001 From: Tauan BF <11513929+tauanbinato@users.noreply.github.com> Date: Fri, 25 Sep 2026 17:18:05 -0300 Subject: [PATCH 5/5] List the release binaries and the Windows fix in the changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d0f6c0..2810cb8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ Notable changes to JevGate. Versions follow [Semantic Versioning](https://semver ## [Unreleased] +- Release binaries for Linux (x86_64 and arm64, static), macOS (Apple silicon and Intel) and Windows (x86_64), with SHA-256 checksums and build provenance (`gh attestation verify`). `install.sh` installs a checked binary on Linux and macOS, and `cargo binstall jevgate` finds the archives on every platform. +- Windows: files are named with forward slashes, as on other platforms, so path rules (Next.js routes, Django modules, documentation roles) match there, and reports and baselines name a file the same way everywhere. Requests on Linux and macOS are unchanged, so cached answers stay valid. + ## [0.16.0] - 2026-09-25 Checked against 40 open-source projects in every supported stack (Rust, Python, JavaScript/TypeScript, Go, C#, Java, PHP, Ruby, Svelte, Astro, Vue, Supabase SQL, GitHub Actions), with every review and consider labeled by hand. On the first 17, reviews went from 140 to 107 and considers from 935 to 293, mostly false positives and repeated findings removed; undecided units stayed near 2%.