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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -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
14 changes: 14 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
41 changes: 32 additions & 9 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -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*"]
Expand All @@ -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 }}
Expand Down Expand Up @@ -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" <<EOF
\`\`\`sh
curl -fsSL https://raw.githubusercontent.com/$GITHUB_REPOSITORY/$TAG/install.sh | sh
cargo binstall jevgate # or: cargo install jevgate --locked
\`\`\`

Verify a download with \`gh attestation verify <archive> --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"
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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%.
Expand Down
9 changes: 9 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <archive> --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

Expand Down
88 changes: 88 additions & 0 deletions install.sh
Original file line number Diff line number Diff line change
@@ -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
3 changes: 2 additions & 1 deletion src/auth/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
};

Expand Down Expand Up @@ -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()
Expand Down
5 changes: 2 additions & 3 deletions src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,7 @@ pub fn collect(args: &CheckArgs, context: &ConfigContext) -> Result<Vec<ContextI
let mut bytes = 0;
for name in &args.context {
let path = context.input_path(name);
let relative = path
.strip_prefix(&context.root)
let relative = &crate::discovery::relative(&path, &context.root)
.context("Context must be inside the repository root")?;
ensure_visible_path(relative)?;
ensure!(
Expand All @@ -58,7 +57,7 @@ pub fn collect(args: &CheckArgs, context: &ConfigContext) -> Result<Vec<ContextI
);
if inputs
.iter()
.any(|i: &ContextInput| i.file.path == relative)
.any(|i: &ContextInput| i.file.path == *relative)
{
continue;
}
Expand Down
13 changes: 12 additions & 1 deletion src/discovery/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,18 @@ pub use copies::{generated_header, generated_source, vendored};
use crate::{boundary::globs, config::Config};
use anyhow::Result;
use globset::GlobSet;
use std::path::Path;
use std::path::{Path, PathBuf, StripPrefixError};

/// `path` relative to `root`, written with `/` on every platform, so reports,
/// requests, baselines and path rules name a file the same way everywhere.
pub fn relative(path: &Path, root: &Path) -> Result<PathBuf, StripPrefixError> {
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",
Expand Down
Loading
Loading