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
54 changes: 54 additions & 0 deletions .github/workflows/ores-lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Managed by .ores-lint/ - regenerated by the rollout script.
name: ores-lint

on:
pull_request:
workflow_dispatch:
# Add `push:` here once this repo's lint debt is paid down.

jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Detect project types
id: detect
run: |
{ [ -f package.json ] || git ls-files '*.js' '*.mjs' '*.cjs' '*.ts' '*.tsx' | grep -q .; } \
&& echo "js=true" >> "$GITHUB_OUTPUT" || echo "js=false" >> "$GITHUB_OUTPUT"
{ [ -f Cargo.toml ] || git ls-files '*.rs' | grep -q .; } \
&& echo "rs=true" >> "$GITHUB_OUTPUT" || echo "rs=false" >> "$GITHUB_OUTPUT"
{ [ -f pubspec.yaml ] || git ls-files '*.dart' | grep -q .; } \
&& echo "dart=true" >> "$GITHUB_OUTPUT" || echo "dart=false" >> "$GITHUB_OUTPUT"
{ [ -f gleam.toml ] || git ls-files '*.gleam' | grep -q .; } \
&& echo "gleam=true" >> "$GITHUB_OUTPUT" || echo "gleam=false" >> "$GITHUB_OUTPUT"

# Node is the runner for ESLint and for the cross-language require-send
# scanner, so it is installed whenever any supported language is present.
- uses: actions/setup-node@v4
if: steps.detect.outputs.js == 'true' || steps.detect.outputs.rs == 'true' || steps.detect.outputs.dart == 'true' || steps.detect.outputs.gleam == 'true'
with:
node-version: '22'

# ESLint is a global tool here, not a repo dependency - so this job never
# runs `npm install` and never needs the repo's node_modules.
- name: Install eslint globally
if: steps.detect.outputs.js == 'true'
run: npm i -g eslint typescript-eslint

- name: Install clippy
if: steps.detect.outputs.rs == 'true'
run: rustup component add clippy || true

# Dart/Gleam SDKs are optional. When absent, dart.sh / gleam.sh skip
# with an actionable message; require-send.mjs still runs via Node.
- uses: dart-lang/setup-dart@v1
if: steps.detect.outputs.dart == 'true'
continue-on-error: true

- name: Run ores-lint
run: sh .ores-lint/lint.sh
env:
# Warn-only. Set to 1 here to make lint findings fail this repo's CI.
ORES_LINT_STRICT: '0'
112 changes: 112 additions & 0 deletions .ores-lint/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# ores-lint

A vendored, dependency-free lint baseline for every JavaScript/TypeScript and
Rust repo in the org fleet. Everything it needs is in this directory — there is
nothing to install from a registry and nothing to keep in version sync.

## Running it

```sh
sh .ores-lint/lint.sh # lints whatever this repo contains
sh .ores-lint/selftest.sh # verifies the toolkit still works after a toolchain upgrade
```

For JS repos it is also wired into `npm run lint:ores`, and runs automatically
before `npm run build` (`prebuild`) and before `npm publish` (`prepublishOnly`).

## What it enforces

**JavaScript / TypeScript** — via the repo's own ESLint (flat config):

| rule | why |
|---|---|
| `semi` | house style: semicolons are required, missing ones warn |
| `ores/require-send` | a logging chain that reaches `.info()`/`.warn()`/… but never calls `.send()` builds an event that is never delivered |
| correctness set | `eqeqeq`, `no-unreachable`, `no-dupe-keys`, `use-isnan`, `valid-typeof`, `no-async-promise-executor`, and similar low-false-positive checks |

**Rust** — via clippy:

| lint | why |
|---|---|
| `clippy::implicit_return` | house style: prefer an explicit `return` at tail position |
| `clippy::correctness`, `clippy::suspicious` | real defects |
| `unwrap_used`, `expect_used`, `panic_in_result_fn`, `todo`, `dbg_macro` | things that should not reach a publish |

## The two things worth knowing

**1. Implicit-return findings are capped.** `clippy::implicit_return` fires once
per implicit return, which on a real crate is hundreds of identical lines. The
lint stays fully enabled so nothing is missed, but `rust.sh` collapses it into a
single warning showing at most 5 concrete locations plus `... and N more`. The
same cap applies to every ESLint rule via `eslint/formatter.mjs`. Change it with
`ORES_LINT_MAX_EXAMPLES`.

**2. `clippy::needless_return` had to be disabled.** It ships enabled in
clippy's default `style` group and warns on exactly the explicit returns this
house style asks for. Enabling `implicit_return` without allowing
`needless_return` makes the two lints contradict each other on every function in
the crate. `selftest.sh` asserts this stays true.

## Warn-only, by design

`lint.sh` exits 0 no matter what it finds. It is wired into build and publish
hooks across hundreds of repos, so it is built to be incapable of breaking one
unless a human opts in.

To make findings blocking for a single repo, create `.ores-lint/local.sh`:

```sh
ORES_LINT_STRICT=1
```

`local.sh` is yours — the rollout script never overwrites it. Everything else in
this directory is managed and will be replaced on the next rollout.

## Knobs

| variable | default | meaning |
|---|---|---|
| `ORES_LINT_MAX_EXAMPLES` | `5` | example locations shown per rule |
| `ORES_LINT_STRICT` | `0` | `1` makes any finding exit non-zero |
| `ORES_LINT_SKIP_JS` / `ORES_LINT_SKIP_RUST` | `0` | skip one half |
| `ORES_LINT_RUST_ALL_TARGETS` | `0` | `1` also lints tests/benches/examples |
| `ORES_LINT_RUST_EXTRA` | — | extra flags appended to the clippy invocation |

## Graceful degradation

Nothing here is allowed to fail loudly for an environmental reason. ESLint not
installed, too old, clippy not installed, no TypeScript parser available, crate
deps not fetchable — each is reported as an actionable skip, not an error.
Repo-specific ESLint config that already existed is never overwritten.

CI follows the same model: the workflow runs `npm i -g eslint typescript-eslint`
and never runs `npm install` for the repo itself, so linting a PR does not
require the repo's dependency tree to resolve.

## Per-repo customisation

`eslint.config.mjs` at the repo root takes options:

```js
export default await oresConfig({
requireSend: { loggerNames: ['myLogger'], terminalMethods: ['send', 'flush'] },
rules: { 'no-console': 'warn' },
ignores: ['**/generated/**'],
});
```

Once you edit that file the rollout script leaves it alone.

---

## Fleet operations (from the `codes` directory)

```sh
node .ores-lint-toolkit/rollout.mjs --dry-run # preview
node .ores-lint-toolkit/rollout.mjs # install / re-install everywhere
node .ores-lint-toolkit/rollout.mjs --only ores-otel
node .ores-lint-toolkit/verify.mjs # assert every repo is correctly installed
```

Re-run the rollout after editing anything in `.ores-lint-toolkit/` — it is
idempotent and propagates the change to every repo.
1 change: 1 addition & 0 deletions .ores-lint/VERSION
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1.0.0
35 changes: 35 additions & 0 deletions .ores-lint/config.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#!/bin/sh
# ores-lint shared configuration. Sourced by lint.sh, js.sh and rust.sh.
# Every value can be overridden from the environment, or per repo in local.sh.

# Maximum number of concrete example locations shown for any one rule.
: "${ORES_LINT_MAX_EXAMPLES:=5}"

# Warn-only by default: lint.sh exits 0 no matter what it finds.
# Flip to 1 (per repo, or in CI) once a repo's debt is paid down.
: "${ORES_LINT_STRICT:=0}"

: "${ORES_LINT_SKIP_RUST:=0}"
: "${ORES_LINT_SKIP_JS:=0}"

# Include tests/benches/examples in the Rust pass. Off by default so the
# pre-publish signal is about shipped code.
: "${ORES_LINT_RUST_ALL_TARGETS:=0}"

# Minimum ESLint major version. Flat config needs 9+. ESLint is expected to be
# installed GLOBALLY, once - see required-tools.json. Nothing is ever installed
# into a repo's node_modules.
: "${ORES_LINT_ESLINT_MIN_MAJOR:=9}"

# The exact clippy diagnostic text for `clippy::implicit_return`. selftest.sh
# verifies this still matches, so a future clippy rewording surfaces as a test
# failure rather than as a silently empty report.
: "${ORES_LINT_IMPLICIT_RETURN_MSG:=missing \`return\` statement}"

export ORES_LINT_MAX_EXAMPLES ORES_LINT_STRICT ORES_LINT_SKIP_RUST ORES_LINT_SKIP_JS
export ORES_LINT_RUST_ALL_TARGETS ORES_LINT_ESLINT_MIN_MAJOR ORES_LINT_IMPLICIT_RETURN_MSG

# Repo-local overrides, never overwritten by the rollout script. Sourced last so
# anything set here wins.
ORES_LINT_CFG_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
[ -f "$ORES_LINT_CFG_DIR/local.sh" ] && . "$ORES_LINT_CFG_DIR/local.sh"
139 changes: 139 additions & 0 deletions .ores-lint/eslint/base.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/**
* ores-lint :: shared flat-config factory
*
* Everything here degrades gracefully. A repo missing TypeScript tooling gets
* its JS linted rather than an error; an ESLint that has dropped core `semi`
* falls back to the vendored rule. The point is that a lint config rolled out
* to hundreds of heterogeneous repos must never be the thing that breaks them.
*/

import { createRequire } from 'node:module';
import { pathToFileURL } from 'node:url';
import oresPlugin from './plugin.mjs';

const require_ = createRequire(import.meta.url);

// ESLint and its optional TypeScript parser are expected to live in a single
// global install rather than in every repo's node_modules, so resolution has to
// look outside this file's own tree. js.sh passes the global root in.
const EXTRA_PATHS = [
process.env.ORES_LINT_GLOBAL_ROOT,
...(process.env.NODE_PATH ? process.env.NODE_PATH.split(':') : []),
].filter(Boolean);

function tryResolve(id) {
try { return require_.resolve(id); } catch { /* try the global root next */ }
if (EXTRA_PATHS.length) {
try { return require_.resolve(id, { paths: EXTRA_PATHS }); } catch { /* not installed */ }
}
return null;
}

/** Is core `semi` still shipped by the installed ESLint? */
async function coreSemiAvailable() {
for (const id of ['eslint/use-at-your-own-risk']) {
const resolved = tryResolve(id);
if (!resolved) continue;
try {
const { builtinRules } = await import(pathToFileURL(resolved).href);
return builtinRules.has('semi');
} catch { /* fall through */ }
}
return true; // could not introspect: assume core rules are intact
}

/** typescript-eslint, if the repo happens to have it. */
async function loadTsSupport() {
for (const id of ['typescript-eslint', '@typescript-eslint/parser']) {
const resolved = tryResolve(id);
if (!resolved) continue;
try {
// Import by resolved path: a bare specifier would not find a global install.
const mod = await import(pathToFileURL(resolved).href);
const m = mod.default || mod;
if (id === 'typescript-eslint' && m.parser) return { parser: m.parser, source: id };
if (m.parseForESLint || m.parse) return { parser: m, source: id };
} catch { /* fall through to the next candidate */ }
}
return null;
}

const JS_FILES = ['**/*.js', '**/*.mjs', '**/*.cjs', '**/*.jsx'];
const TS_FILES = ['**/*.ts', '**/*.mts', '**/*.cts', '**/*.tsx'];

const IGNORES = [
'**/node_modules/**', '**/dist/**', '**/build/**', '**/out/**', '**/target/**',
'**/coverage/**', '**/.next/**', '**/vendor/**', '**/*.min.js', '**/*.bundle.js',
'**/.ores-lint/**',
];

/**
* @param {object} [opts]
* @param {object} [opts.requireSend] options forwarded to ores/require-send
* @param {object} [opts.rules] extra rules merged last (repo overrides)
* @param {string[]} [opts.ignores] extra ignore globs
*/
export default async function oresConfig(opts = {}) {
const useCoreSemi = await coreSemiAvailable();
const ts = await loadTsSupport();

const semiRules = useCoreSemi
? { semi: ['warn', 'always'], 'no-extra-semi': 'warn', 'semi-style': ['warn', 'last'] }
: { 'ores/semi': 'warn' };

// Correctness rules chosen for a near-zero false-positive rate, because this
// config lands in repos nobody is going to hand-tune afterwards.
const correctness = {
'ores/require-send': ['warn', opts.requireSend || {}],
'no-unused-vars': ['warn', { args: 'after-used', argsIgnorePattern: '^_', varsIgnorePattern: '^_', caughtErrors: 'none' }],
eqeqeq: ['warn', 'smart'],
'no-fallthrough': 'warn',
'no-unreachable': 'warn',
'no-dupe-keys': 'warn',
'no-dupe-else-if': 'warn',
'no-duplicate-case': 'warn',
'no-self-compare': 'warn',
'no-unsafe-negation': 'warn',
'no-cond-assign': ['warn', 'always'],
'no-constant-condition': ['warn', { checkLoops: false }],
'no-async-promise-executor': 'warn',
'no-promise-executor-return': 'warn',
'no-compare-neg-zero': 'warn',
'no-irregular-whitespace': 'warn',
'no-template-curly-in-string': 'warn',
'valid-typeof': 'warn',
'use-isnan': 'warn',
'no-debugger': 'warn',
// Deliberately NOT enabled: no-undef. Without a `globals` package it fires
// on console/process/window everywhere, and TypeScript already covers it.
};

const configs = [
{ ignores: [...IGNORES, ...(opts.ignores || [])] },
{
files: JS_FILES,
plugins: { ores: oresPlugin },
languageOptions: { ecmaVersion: 'latest', sourceType: 'module' },
rules: { ...semiRules, ...correctness, ...(opts.rules || {}) },
},
];

if (ts) {
configs.push({
files: TS_FILES,
plugins: { ores: oresPlugin },
languageOptions: { parser: ts.parser, ecmaVersion: 'latest', sourceType: 'module' },
rules: {
...semiRules,
...correctness,
// TypeScript's own compiler reports unused symbols with better fidelity.
'no-unused-vars': 'off',
...(opts.rules || {}),
},
});
}

return configs;
}

export const meta = { tsSupport: null };
Loading
Loading