Aman-Mittal opened a new pull request, #378:
URL: https://github.com/apache/fineract-backoffice-ui/pull/378
Closes the one Category X licence in the dependency tree, and turns the
policy that found it into a gate so the next one fails the build instead of an
audit.
## Business Value
The release-readiness audit found exactly one licence that could stop a
release: an LGPL-3.0 lint plugin. It was never bundled and never distributed,
but it was named in `package.json`, and `package.json` travels in a source
release — so it was a question the PMC would have had to answer before a vote
rather than one that could be scoped out.
Removing it takes that question off the table. Adding the gate means the
next one never gets as far as a release review: a Category X or unclassified
licence now fails CI on the pull request that introduces it, at the point where
the person who added it is still holding the context. Auditing for this by hand
once per release is how a dependency sits in a tree for two releases before
anyone notices.
Running the end-to-end suite on every push does the same thing for
behaviour. It is the only thing that exercises this application against a real
Fineract, and it previously waited for a pull request to open — long enough for
a branch to be several commits deep in a broken state before anything said so.
## What changed
**Removed** `eslint-plugin-sonarjs` — LGPL-3.0-only, ASF Category X.
**Added** the three requested replacements, licences verified from the
installed packages rather than assumed:
| Plugin | Version | Licence |
|---|---|---|
| `eslint-plugin-unicorn` | 65.0.1 | MIT |
| `eslint-plugin-security` | 4.0.1 | Apache-2.0 |
| `eslint-plugin-import` | 2.32.0 | MIT |
| `eslint-import-resolver-typescript` | 4.4.5 | ISC |
`unicorn` is pinned to `^65` rather than the current major: from 66 onwards
it requires ESLint ≥ 10.4 and this project is on 9. Forcing that resolution is
not an option — the `Dependency Integrity` job exists to forbid
`--legacy-peer-deps`.
The `unopinionated` unicorn set is used rather than `recommended`, which is
substantially stylistic (filename casing, abbreviation expansion, `for…of` over
`.forEach`) and would rewrite a great deal of working code without fixing a
defect.
## Cognitive complexity is preserved
It was enabled at `error` through the removed plugin's recommended set, so
removing the plugin would have dropped it silently. There was nothing
permissive to take instead: `eslint-plugin-cognitive-complexity` on npm is a
security placeholder, and the alternatives are single-author packages at v0.x.
Trading a licence problem for a supply-chain one is not a trade.
So it is implemented in `eslint-rules/cognitive-complexity.js`, from the
published metric rather than from anyone's source, **at the same threshold of
15**.
Its tests assert **exact scores** at a threshold of one below, not merely
that something was reported, because the way this rule would fail quietly is by
drifting stricter or laxer than what it replaced while still looking like it
works. That is not hypothetical — an early draft counted `??` as a branch and
failed this, on a real file:
> `loan-product-form.component.ts` has a template handler holding 23 `??`
defaults and almost no other branching. The previous rule passed it; the draft
scored it 23 and failed it.
`??` states a default rather than branching, so it is not counted, and a
test pins that. 26 tests, run by CI.
## The licence gate
`scripts/check-dependency-licenses.mjs`, wired into `License Compliance`:
```
Category A PASS permissive
Category B REVIEW reported for PMC awareness, does not fail
Category X BLOCK
Unknown BLOCK legal review
```
Current state, over the whole tree:
```
Dependency licences — 1197 packages (production and dev)
Category A 1188 pass
Acknowledged 7 pass, recorded BlueOak-1.0.0
Category B 2 review axe-core, @axe-core/playwright
(MPL-2.0)
Category X 0 block
Unknown 0 block
```
Production alone: **25 packages, all Category A**.
Three decisions worth review rather than assumption:
- **Unknown blocks.** The failure worth preventing is not a GPL dependency
arriving with a banner; it is a `license` field reading `SEE LICENSE IN
LICENSE.txt` arriving on a transitive bump. Treating that as a pass makes the
check decorative. Clearing one means identifying the licence or recording it in
`ACKNOWLEDGED` with a reason — not widening a category list until the build
goes green.
- **`ACKNOWLEDGED` exists** because the ASF's lists do not cover every SPDX
identifier in a modern npm tree. `BlueOak-1.0.0` is the only entry: plainly
permissive, named in neither list, reaching this tree only through build
tooling. Silently calling it Category A would be inventing a policy decision,
so it is written down and marked pending PMC confirmation.
- **devDependencies are in scope**, deliberately. They are not distributed,
but they are named in `package.json` — which is exactly how the plugin this PR
removes came to matter.
SPDX expressions are resolved rather than string-matched: `OR` takes the
branch that resolves best (the project may pick), `AND` takes the worst (every
term binds).
### The gate is tested, because a clean tree proves nothing
`npm run check:licenses:selftest` runs 26 expressions through the classifier
and asserts each verdict — `LGPL-3.0-only` → X, `MPL-2.0` → B, `(MIT OR
GPL-3.0)` → A, `(MIT AND GPL-3.0)` → X, undeclared → Unknown. CI runs it beside
the scan. A green scan over a clean tree says nothing about whether the rules
still bite; this is what says it.
Writing it surfaced a bug in itself worth naming: `\bOR\b` matches the `or`
inside `GPL-2.0-or-later`, because hyphens are word boundaries — so the
expression was detected as compound and then not split, and the classifier
recursed until the stack went. The compound test now has the same shape as the
split.
The existing `license-checker` allow-list stays. The two read npm's metadata
by different routes, so a package one misreads is still caught by the other.
## E2E on every push
```yaml
push:
branches: ['**']
```
The `pull_request` trigger stays alongside it rather than being replaced: a
fork's pushes land in the fork, so `push` alone would leave external
contributions untested. **A branch in this repository with an open PR will
therefore get both runs** — the same commit tested twice. Dropping
`pull_request` removes the duplicate at the cost of fork coverage; flagging it
rather than leaving it to be discovered in the Actions bill.
Superseded runs are now cancelled everywhere except `main` and `develop`,
where the point of the run is the record that the commit passed. Without that,
pushing three times in five minutes queues three full Fineract bring-ups.
## Two autofixes reverted, because lint cannot type-check
Both stayed green under ESLint and failed `npm run build`:
- `unicorn/prefer-array-find` rewrites `.filter(p).pop()` to `.findLast(p)`,
which needs lib ES2023; `tsconfig` targets ES2022 → **TS2550**.
- `unicorn/dom-node-dataset` rewrites `setAttribute('data-theme', …)` to
`dataset.theme`, and `noPropertyAccessFromIndexSignature` is on → **TS4111**.
Both rules are now off with that reasoning recorded beside them. Worth
stating plainly: had the build not run, this PR would have merged code the
compiler rejects.
## Rules switched off, each with its reason
The one worth naming is **`unicorn/prefer-global-this`** (106 reports).
Enforcing it would let `globalThis.localStorage` past `no-restricted-globals`,
which is a trust boundary (`security.md` §4), not a style preference. A style
rule does not get to open a hole in it.
The others: `security/detect-object-injection` (all 35 reports were
`obj[key]` with a typed key), `unicorn/no-array-for-each`,
`unicorn/no-negated-condition`, `unicorn/no-useless-undefined`,
`unicorn/prefer-top-level-await`.
## What has no replacement
A migration that claims parity it does not have is worse than one that names
its gaps. `DOCS/LINT_POLICY.md` records the full mapping; these genuinely have
no permissive equivalent:
- **`no-duplicate-string`** — this project had it on at `error`. It is why
`NAV_CONFIG` names its icons as constants. Review is now the only thing
catching a repeated literal.
- **`no-hardcoded-passwords`, `no-hardcoded-ip`, `sql-queries`** — small
loss here: the application issues no SQL, and CodeQL plus repository
secret-scanning cover credentials from another angle.
- **`no-identical-functions`, `no-duplicated-branches`,
`no-nested-conditional`, `no-invariant-returns`** — the deeper dataflow rules.
- **`deprecation`** — `@typescript-eslint/no-deprecated` covers it but needs
typed linting, which changes the cost of every lint run. Worth doing
deliberately, not in passing.
- **`assertions-in-tests`, `no-skipped-tests`, `no-empty-test-file`,
`no-fixed-wait-in-tests`**.
## Testing
Every gate run on this branch and observed:
| Gate | Result |
|---|---|
| `lint:prune` | pass |
| HTML lint | pass |
| `test:eslint-rules` | **26/26** |
| `format:check` | pass |
| `i18n:check`, `check:icons` | pass |
| `check:internal-endpoints`, `check:route-permissions` | pass |
| `typecheck:e2e` | pass |
| `api:surface` | pass — 142 services, 564 operations |
| `check-license.sh` | pass |
| `check:licenses:selftest` | **26/26 expressions** |
| `check:licenses:production` | pass — 25 packages, all Category A |
| `check:licenses` | pass — 0 Category X, 0 unclassified |
| `build` | pass |
| Unit tests | **1093 SUCCESS** |
| `ga:check` | 8/9, **0 blocking**; adapter backlog unchanged at 416 |
## Suppression baseline
27 pre-existing violations of the new rules are recorded in
`eslint-suppressions.json` rather than fixed here — 15 of them
`unicorn/no-array-sort`, which wants `toSorted()` in place of an in-place
`.sort()`. That is a real bug class (mutating an array the caller still holds)
and a semantic change; it does not belong in a licensing PR.
The file is a ratchet, not an amnesty: CI runs `--prune-suppressions`, which
fails when it lists a violation that no longer exists. The count only falls.
The 416 adapter-boundary suppressions the GA gate counts are untouched.
## Breaking changes
None to the application. Two for contributors: a Category X or unclassified
dependency licence now fails CI, and every push runs the end-to-end suite.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]