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
31 changes: 18 additions & 13 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,30 @@ permissions:
contents: read

jobs:
test:
runs-on: ubuntu-latest
name: Ruby ${{ matrix.ruby }}
rust:
runs-on: ${{ matrix.os }}
name: Rust on ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
ruby:
- "3.2"
- "3.3"
- "3.4"
os:
- ubuntu-latest
- macos-latest
- windows-latest

steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Ruby
uses: ruby/setup-ruby@v1
- uses: dtolnay/rust-toolchain@stable
with:
ruby-version: ${{ matrix.ruby }}
bundler-cache: true
- name: Run tests and RuboCop
run: bundle exec rake
components: rustfmt, clippy
- uses: Swatinem/rust-cache@v2
- name: Check formatting
if: runner.os == 'Linux'
run: cargo fmt --all --check
- name: Clippy
run: cargo clippy --workspace --all-targets --locked -- -D warnings
- name: Test
run: cargo test --workspace --locked --no-fail-fast

16 changes: 8 additions & 8 deletions .github/workflows/lintus.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Lintus linting itself. Uses the action defined in this repository, so it also
# serves as a smoke test of action.yml. Skipped on forks without the secret.
# Lintus linting itself, built from this checkout. It also checks, on every pull
# request, that a real call to the Jev API works. Skipped on forks without the secret.
name: Lintus

on:
Expand All @@ -18,12 +18,12 @@ jobs:
with:
fetch-depth: 0
persist-credentials: false
- name: Set up Ruby
- uses: dtolnay/rust-toolchain@stable
if: env.JEV_API_KEY != ''
- uses: Swatinem/rust-cache@v2
if: env.JEV_API_KEY != ''
uses: ruby/setup-ruby@v1
with:
ruby-version: "3.3"
bundler-cache: true
- name: Run lintus from this checkout
if: env.JEV_API_KEY != ''
run: bundle exec exe/lintus --format github --diff "origin/${{ github.base_ref }}"
env:
BASE_REF: ${{ github.base_ref }}
run: cargo run --release --locked -- --format github --diff "origin/$BASE_REF"
158 changes: 136 additions & 22 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,41 +1,155 @@
# Publishes the gem to rubygems.org when a version tag is pushed.
# Builds lintus for every platform when a version tag is pushed, attaches the
# binaries to a GitHub release, and publishes the crates to crates.io.
#
# Uses RubyGems trusted publishing (OIDC), so no API key is stored in the
# repository. One-time setup on rubygems.org, before the first release:
# Profile > Pending trusted publishers > Add: gem "lintus",
# repository virolea/lintus, workflow release.yml (no environment).
# After the first release the publisher moves to the gem's own settings.
# To release:
# 1. On main, set the version in Cargo.toml (and in crates/jev/Cargo.toml if
# the client changed), and move the CHANGELOG.md entries under a heading
# for the version: "## [0.3.0] - 2026-10-01".
# 2. git tag v0.3.0 && git push origin v0.3.0
#
# To release: bump lib/lintus/version.rb and CHANGELOG.md on main, then
# git tag v0.1.0 && git push origin v0.1.0
# Running this workflow by hand (Actions > Release > Run workflow) only builds
# and tests the binaries, without releasing anything: a dry run before tagging.
#
# crates.io uses trusted publishing (OIDC), so no token is stored in the
# repository. It can only be set up for a crate that exists, so publish
# jev-api and lintus once by hand with `cargo publish`, then add this
# repository and workflow as a trusted publisher in each crate's settings.
name: Release

on:
push:
tags:
- "v*"
workflow_dispatch:

permissions:
contents: read

jobs:
check:
if: github.event_name == 'push'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Check the tag matches the version in Cargo.toml
run: |
version="v$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[] | select(.name == "lintus") | .version')"
if [ "$GITHUB_REF_NAME" != "$version" ]; then
echo "Tag $GITHUB_REF_NAME does not match the lintus version in Cargo.toml ($version)" >&2
exit 1
fi

build:
needs: check
if: ${{ !cancelled() && needs.check.result != 'failure' }}
name: Build ${{ matrix.target }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
# Linux binaries are static (musl), so they run on any distribution.
- target: x86_64-unknown-linux-musl
os: ubuntu-latest
- target: aarch64-unknown-linux-musl
os: ubuntu-24.04-arm
- target: x86_64-apple-darwin
os: macos-latest
- target: aarch64-apple-darwin
os: macos-latest
- target: x86_64-pc-windows-msvc
os: windows-latest

steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Install musl
if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install -y musl-tools
- name: Test
if: matrix.target != 'x86_64-apple-darwin'
run: cargo test --workspace --locked --target ${{ matrix.target }}
- name: Build
run: cargo build --release --locked --target ${{ matrix.target }}
- name: Package
shell: bash
env:
TARGET: ${{ matrix.target }}
run: |
name="lintus-$TARGET"
mkdir "$name"
cp README.md LICENSE.txt CHANGELOG.md "$name/"
if [ "$RUNNER_OS" = "Windows" ]; then
cp "target/$TARGET/release/lintus.exe" "$name/"
(cd "$name" && 7z a -tzip "../$name.zip" . >/dev/null)
archive="$name.zip"
sha256sum "$archive" > "$archive.sha256"
else
cp "target/$TARGET/release/lintus" "$name/"
tar -czf "$name.tar.gz" -C "$name" .
archive="$name.tar.gz"
shasum -a 256 "$archive" > "$archive.sha256"
fi
- uses: actions/upload-artifact@v4
with:
name: ${{ matrix.target }}
path: |
lintus-${{ matrix.target }}.tar.gz*
lintus-${{ matrix.target }}.zip*

release:
needs: build
if: github.event_name == 'push'
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/download-artifact@v4
with:
path: dist
merge-multiple: true
- name: Create the release
env:
GH_TOKEN: ${{ github.token }}
run: |
version="${GITHUB_REF_NAME#v}"
awk -v heading="## [$version]" 'index($0, heading) == 1 { found = 1; next } found && /^## \[/ { exit } found' CHANGELOG.md > notes.md
if [ ! -s notes.md ]; then echo "See CHANGELOG.md." > notes.md; fi
cp install.sh dist/
gh release create "$GITHUB_REF_NAME" --title "$GITHUB_REF_NAME" --notes-file notes.md dist/*

crates:
needs: release
runs-on: ubuntu-latest
permissions:
contents: write # rake release pushes the tag if it is not there yet
id-token: write # trusted publishing
steps:
- uses: actions/checkout@v4
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: "3.3"
bundler-cache: true
- name: Check the tag matches the gem version
persist-credentials: false
- uses: dtolnay/rust-toolchain@stable
- uses: rust-lang/crates-io-auth-action@v1
id: auth
- name: Publish jev-api, unless this version is already out
env:
CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }}
run: |
version="v$(ruby -Ilib -e 'require "lintus/version"; print Lintus::VERSION')"
if [ "$GITHUB_REF_NAME" != "$version" ]; then
echo "Tag $GITHUB_REF_NAME does not match Lintus::VERSION ($version)" >&2
exit 1
version=$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[] | select(.name == "jev-api") | .version')
if curl -sf "https://crates.io/api/v1/crates/jev-api/$version" > /dev/null; then
echo "jev-api $version is already on crates.io"
else
cargo publish --locked -p jev-api
fi
- name: Run tests
run: bundle exec rake test
- name: Build and push to rubygems.org
uses: rubygems/release-gem@v1
- name: Publish lintus
env:
CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }}
run: cargo publish --locked -p lintus
11 changes: 1 addition & 10 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,10 +1 @@
/.bundle/
/coverage/
/doc/
/pkg/
/tmp/
/vendor/bundle/

*.gem
/.irbrc
Gemfile.lock
/target/
36 changes: 13 additions & 23 deletions .lintus.yml
Original file line number Diff line number Diff line change
@@ -1,37 +1,27 @@
# Lintus lints itself with these rules. See README.md for the format.

paths:
- "lib/**/*.rb"
- "exe/*"

exclude:
- "lib/lintus/version.rb"
- "src/**/*.rs"
- "crates/*/src/**/*.rs"

rules:
no_debugging_leftovers:
description: Debugging statements must not be committed.
question: Does this file contain leftover debugging code, such as binding.irb, debugger, or a puts/p/pp used to inspect a value?
question: Does this file contain leftover debugging code, such as dbg!, or a println!/eprintln! used to inspect a value?
criteria:
"true": A debugger breakpoint or a throwaway print statement is present.
"true": A dbg! call, or a print statement that dumps an internal value rather than telling the user something, is present.
"false": Any output is deliberate program behaviour, or there is none.

errors_are_actionable:
description: Raised error messages must tell the user what was wrong and what to do about it.
question: Does this file raise an error whose message would leave a user unsure of what went wrong or how to fix it?
description: Error messages must tell the user what was wrong and what to do about it.
question: Does this file build an error message that would leave a user unsure of what went wrong or how to fix it?
criteria:
"true": At least one raised message is vague, such as "invalid" or "failed", with no detail.
"false": Every raised message names the problem, or no errors are raised.
"true": At least one message is vague, such as "invalid" or "failed", with no detail.
"false": Every message names the problem, or the file builds no error messages.

classes_are_documented:
description: Each class and module should open with a comment describing its responsibility.
question: Is there a class or module defined in this file whose definition is not preceded by a comment describing what it is for?
modules_are_documented:
description: Each module should open with a comment describing its responsibility.
question: Does this file start without a module-level doc comment (//!) explaining what the module is for?
criteria:
"true": >-
A class or module with a body of its own is introduced without a descriptive comment on the
lines right above its `class` or `module` line.
"false": >-
Every class or module that carries behaviour has such a comment, or the file defines none.
Ignore namespace wrappers whose body only nests other definitions, classes reopened only to
nest another definition, and one-line subclasses such as `class Error < StandardError; end`.
exclude:
- "exe/*"
"true": The file does not open with a //! comment, or that comment does not say what the module does.
"false": The file opens with a //! comment describing its purpose.
12 changes: 7 additions & 5 deletions .pre-commit-hooks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,19 @@
#
# repos:
# - repo: https://github.com/virolea/lintus
# rev: v0.1.0
# rev: v0.3.0
# hooks:
# - id: lintus
#
# pre-commit passes the staged file names as arguments, so only the files in
# the commit are sent to the model. JEV_API_KEY must be set in the environment.
# pre-commit builds lintus from source the first time (it installs a Rust
# toolchain for that if the machine has none, but a C compiler is needed), then
# reuses the build. It passes
# the staged file names as arguments, so only the files in the commit are sent
# to the model. The API key comes from JEV_API_KEY or `lintus auth login`.
- id: lintus
name: lintus
description: Lint staged files against the plain-language rules in .lintus.yml, using the Jev model.
entry: lintus
language: ruby
language: rust
pass_filenames: true
require_serial: true
additional_dependencies: []
36 changes: 0 additions & 36 deletions .rubocop.yml

This file was deleted.

16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
## [Unreleased]

- Lintus is now a single binary, rewritten in Rust: no Ruby needed. Install it with the
script or the archives on the GitHub releases, or `cargo install lintus`. Config files,
flags, output formats and exit statuses are unchanged, checked by a conformance suite that
both implementations pass.
- `lintus auth login`, `status` and `logout` save the Jev API key in the user's config
directory. A run takes the key from `--api-key`, then `JEV_API_KEY`, then the saved key.
- `JEV_API_URL` sends requests to another endpoint, such as a proxy.
- The GitHub Action downloads the binary instead of setting up Ruby; its `ruby-version`
input is ignored. The pre-commit hook builds lintus from source.
- A request that cannot reach the API fails its file instead of stopping the run.
- `--diff` and `--staged` together are an error; before, the last one given won.
- Skipped and failed files are listed in path order, not in the order they finished.
- The Jev client ships as its own crate, `jev-api`.
- The Ruby implementation is removed. The `lintus` gem stays at 0.2.0, and is no longer
updated.

## [0.2.0] - 2026-09-21

- A progress counter on stderr while files are being checked.
Expand Down
Loading
Loading