Jev Gate
A GitHub Action that fails a pipeline when code breaks a quality rule that only a reader could check. Rules are yes/no questions in a YAML file; code picks what each question applies to, Jev answers with a probability, and code compares it with a threshold.
Contents
Usage
- uses: actions/checkout@v7
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.sha }}
- uses: your-org/jev-gate@v1
with:
config: .github/jev-gates.yml
mode: diff
base: ${{ github.event.pull_request.base.sha }}
env:
TYPESAFE_API_KEY: ${{ secrets.TYPESAFE_API_KEY }}
fetch-depth: 0 is required in diff mode: the base commit must be in the
history. Checking out the head commit keeps the merge commit of a pull
request out of the commit scope.
| Exit code | Meaning |
|---|---|
| 0 | No gate fired |
| 1 | At least one gate fired |
| 2 | The tool could not finish |
Command line
Outside GitHub Actions, install the package from PyPI and call it directly:
pip install jev-gate
export TYPESAFE_API_KEY=...
jev-gate --config .github/jev-gates.yml --mode diff --base origin/main
It takes the same options as the Action, plus --root, the repository
directory, and returns the same exit codes.
Gates file
Top-level keys:
| Key | Purpose |
|---|---|
model |
Pinned Jev version, such as jev-1.13.0. Aliases are refused. |
oversize |
fail (default) or skip |
paths |
include and exclude globs; ** crosses directories |
scopes |
Custom syntax scopes (see below) |
gates |
The gates |
Gate keys: id, rule, scope, allow, state, ask, fail_above,
modes, oversize. A gate fires when the probability is strictly above
fail_above. allow lists candidate names the gate never judges: the
identifier for type, function and variable, the raw test name for
test, the path for file and hunk, and the subject for commit.
where filters are not supported yet.
Gates of one candidate that send the same state fields share one request.
Built-in scopes and the fields they offer to state:
| Scope | Fields | Modes |
|---|---|---|
file |
path, text |
diff, full |
hunk |
path, before, after |
diff |
commit |
subject, message, diff, tests |
diff |
test |
phrase, message, assertion, source |
diff, full |
changed_test |
before, after |
diff |
type |
name |
diff, full |
function |
name, returns |
diff, full |
variable |
name |
diff, full |
throw |
message, statement |
diff, full |
catch |
handler |
diff, full |
log |
statement |
diff, full |
Custom scopes
A custom scope gives a tree-sitter query per language pack. Captures name
the fields; @candidate marks the node, and captures starting with _
are ignored. See a custom scope for a gate that uses one.
Examples
Every example below is a complete gates file. The test suite validates each one, so they stay in step with the code.
Built-in scopes
file
Catches a module whose content does not match its file name.
model: jev-1.13.0
gates:
- id: module-content-disagrees-with-path
scope: file
state: [path, text]
ask: Does `text` hold code about a different subject than `path` names?
fail_above: 0.9
hunk
Catches a change that comments code out instead of deleting it. Diff mode only.
model: jev-1.13.0
gates:
- id: code-commented-out
scope: hunk
state: [path, before, after]
ask: >-
Comparing `before` with `after` in `path`, does the change turn working
code into a comment instead of deleting it?
fail_above: 0.85
commit
Catches a commit that changes behaviour without touching a test. Diff mode only.
model: jev-1.13.0
gates:
- id: behaviour-changed-without-tests
scope: commit
state: [subject, message, diff, tests]
ask: >-
`subject` and `message` describe the commit and `diff` is its change.
Does `diff` change behaviour while `tests` shows no test for it?
fail_above: 0.9
test
Catches a test whose name promises something its assertion does not check.
model: jev-1.13.0
gates:
- id: assertion-misses-the-claim
rule: testing.md 1.2
scope: test
state: [phrase, message, assertion, source]
ask: >-
`phrase` is what the test claims and `source` is its body. Do
`assertion` and `message` check something other than `phrase`?
fail_above: 0.9
changed_test
Catches an edited test that was weakened to make it pass. Diff mode only.
model: jev-1.13.0
gates:
- id: test-weakened
scope: changed_test
state: [before, after]
ask: Does `after` check less than `before` did?
fail_above: 0.8
type
Catches a class named after what it does rather than what it is.
model: jev-1.13.0
gates:
- id: class-named-after-a-doer
rule: eo.md 2.1
scope: type
state: [name]
ask: >-
Does the class name `name` end in -er or -or and describe what its
objects do rather than what they are?
fail_above: 0.85
function
Catches a method named as a verb that returns a value.
model: jev-1.13.0
gates:
- id: builder-named-as-a-verb
rule: eo.md 2.2
scope: function
state: [name, returns]
ask: >-
`returns` is the return type of `name`. Is `returns` something other
than nothing while `name` is a verb?
fail_above: 0.9
variable
Catches a variable with a compound name.
model: jev-1.13.0
gates:
- id: compound-variable-name
rule: eo.md 2.3
scope: variable
state: [name]
ask: Is `name` built from more than one noun?
fail_above: 0.9
throw
Catches an error text that does not say what failed.
model: jev-1.13.0
gates:
- id: error-text-hides-what-failed
scope: throw
state: [message, statement]
ask: >-
`statement` raises an error with `message`. Does `message` fail to tell
the reader what went wrong?
fail_above: 0.9
catch
Catches a handler that swallows the error.
model: jev-1.13.0
gates:
- id: swallowed-exception
scope: catch
state: [handler]
ask: Does `handler` drop the error without logging, rethrowing or returning it?
fail_above: 0.9
log
Catches a log line that writes a secret.
model: jev-1.13.0
gates:
- id: secret-in-log
scope: log
state: [statement]
ask: Does `statement` write a password, token or key to the log?
fail_above: 0.8
Options
allow
Catches doer class names, except the two names the rule permits.
model: jev-1.13.0
gates:
- id: class-named-after-a-doer-allowed
scope: type
state: [name]
allow: [User, Computer]
ask: Does the class name `name` describe what its objects do?
fail_above: 0.85
Modes
Catches vague test names on a schedule only, and compound type names on
both pull requests and schedules. A gate without modes runs in both;
gates of diff-only scopes are left out of full runs.
model: jev-1.13.0
gates:
- id: vague-test-name
scope: test
state: [phrase]
modes: [full]
ask: Does `phrase` fail to name one behaviour of one object?
fail_above: 0.9
- id: compound-type-name
scope: type
state: [name]
modes: [diff, full]
ask: Is the type name `name` built from more than one noun?
fail_above: 0.9
Oversize
Catches unclear commit subjects and skips commits too large to send, while every other gate fails on oversize input as the top level says.
model: jev-1.13.0
oversize: fail
gates:
- id: commit-subject-hides-intent
scope: commit
state: [subject, diff]
oversize: skip
ask: Does `subject` fail to say why `diff` was made?
fail_above: 0.95
- id: file-mixes-concerns
scope: file
state: [text]
ask: Does `text` mix more than one concern?
fail_above: 0.9
The same file with oversize: skip at the top level skips oversize input
for every gate that does not set its own oversize.
Paths
Catches lying error texts in source and tests, but not in generated code or vendored files.
model: jev-1.13.0
paths:
include: ["src/**", "tests/**"]
exclude: ["**/generated/**", "src/vendor/**"]
gates:
- id: error-text-lies
scope: throw
state: [message, statement]
ask: Does `message` describe a different failure than `statement` raises?
fail_above: 0.9
Shared requests
Catches failure messages that disagree with the test name and ones that
are empty of meaning, in one request per test, because both gates send
[phrase, message].
model: jev-1.13.0
gates:
- id: failure-message-disagrees-with-name
rule: testing.md 1.3
scope: test
state: [phrase, message]
ask: >-
`phrase` is what the test claims. `message` is the text shown when the
test fails. Does `message` describe a different behaviour, or a
different object, than `phrase`?
fail_above: 0.9
- id: failure-message-says-nothing
scope: test
state: [message, phrase]
ask: Does `message` say nothing a reader of `phrase` would not already know?
fail_above: 0.9
A custom scope
Catches BDD steps in Python whose phrase does not describe what the step does.
model: jev-1.13.0
scopes:
bdd_step:
fields: [phrase, source]
packs:
python:
match: |
((decorated_definition
(decorator) @_d
definition: (function_definition name: (identifier) @phrase)) @candidate @source
(#match? @_d "^@(given|when|then)"))
gates:
- id: step-phrase-disagrees-with-body
scope: bdd_step
state: [phrase, source]
ask: Does `source` do something other than `phrase` says?
fail_above: 0.9
A complete gates file
A starting point to copy to .github/jev-gates.yml.
model: jev-1.13.0
oversize: fail
paths:
include: ["src/**", "tests/**"]
exclude: ["**/generated/**"]
gates:
- id: failure-message-disagrees-with-name
rule: testing.md 1.3
scope: test
state: [phrase, message]
ask: >-
`phrase` is what the test claims. `message` is the text shown when the
test fails. Does `message` describe a different behaviour, or a
different object, than `phrase`?
fail_above: 0.90
- id: class-named-after-a-doer
rule: eo.md 2.1
scope: type
state: [name]
allow: [User, Computer]
ask: >-
Does the class name `name` end in -er or -or and describe what its
objects do rather than what they are?
fail_above: 0.85
- id: error-text-hides-what-failed
scope: throw
state: [message]
ask: Does `message` fail to tell the reader what went wrong?
fail_above: 0.90
- id: swallowed-exception
scope: catch
state: [handler]
ask: Does `handler` drop the error without logging, rethrowing or returning it?
fail_above: 0.90
- id: commit-subject-hides-intent
scope: commit
state: [subject, diff]
oversize: skip
ask: Does `subject` fail to say why `diff` was made?
fail_above: 0.95
Workflows
Diff mode on pull requests
Judges only what a pull request touches.
name: Jev Gate
on:
pull_request:
permissions:
contents: read
jobs:
gate:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.sha }}
- uses: your-org/jev-gate@v1
with:
config: .github/jev-gates.yml
mode: diff
base: ${{ github.event.pull_request.base.sha }}
env:
TYPESAFE_API_KEY: ${{ secrets.TYPESAFE_API_KEY }}
Full mode on a schedule
Judges every candidate in the repository once a week.
name: Jev Gate Full
on:
schedule:
- cron: "17 3 * * 1"
workflow_dispatch:
permissions:
contents: read
jobs:
gate:
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v7
- uses: your-org/jev-gate@v1
with:
config: .github/jev-gates.yml
mode: full
env:
TYPESAFE_API_KEY: ${{ secrets.TYPESAFE_API_KEY }}
Development
uv venv .venv && uv pip install --python .venv -e ".[dev]"
make unit # fast tests with coverage
make e2e # deep tests
make black flake8 ruff
How to report issues
Enhancements
Open an issue labelled enhancement that describes the behaviour you want
and why it helps. You don't need to write code.
Bugs in code
Open a pull request, not an issue. It must include a test that fails on the
current code, marked with @pytest.mark.skip so CI stays green:
@pytest.mark.skip(reason="Reproduces #42, unskip once fixed")
def test_skips_hunk_with_only_whitespace_changes():
...
The fix can come in the same pull request or a later one, which removes the skip. If you can't push to the repository, fork it first.
Bugs outside code
For bugs a test can't reproduce (documentation, action.yml wiring,
packaging, CI workflows), open an issue labelled bug that gives the
expected behaviour, the actual behaviour and how to observe it.
Metadata
Release files for jev-gate 0.0.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| jev_gate-0.0.2.tar.gz | 66.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| jev_gate-0.0.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 106.6 kB
Release files / jev_gate-0.0.2.tar.gz
| Download URL | jev_gate-0.0.2.tar.gz |
|---|---|
| Size | 66.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
72b17790910f8221588ed7da5488827794ddfae320ccfe4854e11fc45a8602c8
|
|
BLAKE2b-256 checksum How to use checksums |
1f78834bc81a196832ca310be6a50263db0a0ece3a71e221684f7807988a3ec8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.0 {"installer":{"name":"uv","version":"0.13.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / jev_gate-0.0.2-py3-none-any.whl
| Download URL | jev_gate-0.0.2-py3-none-any.whl |
|---|---|
| Size | 40.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
202355cbbeb3399419391f2efc25cadfaa7ee58d03dfd4069367f403ac732e58
|
|
BLAKE2b-256 checksum How to use checksums |
078429cb1115352b0401959047fc53df5bd43c935a261a8283377b025c2ca840
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.0 {"installer":{"name":"uv","version":"0.13.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|