ci: add quality gates (semgrep, lint, e2e smoke, advisory PR-Agent) #1

Merged
xavierk merged 5 commits from ci/quality-gates into main 2026-10-05 14:27:41 +00:00
15 changed files with 41 additions and 30 deletions
Showing only changes of commit 5b18db3a77 - Show all commits
+29
View File
@@ -10,6 +10,35 @@ export default tseslint.config(
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs.flat.recommended,
{
rules: {
// Leading underscore marks an intentionally unused binding (omitting a key via rest, unused
// override parameters).
"@typescript-eslint/no-unused-vars": [
"error",
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_", caughtErrorsIgnorePattern: "^_", ignoreRestSiblings: true },
],
// The BOM and NBSP are written literally on purpose in the CSV exporters and the text sanitiser.
"no-irregular-whitespace": ["error", { skipStrings: true, skipTemplates: true, skipRegExps: true }],
},
},
{
// The React Compiler rules shipped in eslint-plugin-react-hooks 7 flag long-standing patterns
// here (load data on mount, "latest value" refs written during render). They are legitimate
// design feedback but not bugs, and rewriting them blind risks behaviour changes in an
// effect-heavy app, so they stay visible as warnings until each site is reworked.
files: ["src/**/*.{ts,tsx}"],
rules: {
"react-hooks/set-state-in-effect": "warn",
"react-hooks/refs": "warn",
"react-hooks/immutability": "warn",
},
},
{
// Test helper that reads react-pdf's untyped internal layout data.
files: ["src/pdf/testing/**/*.ts"],
rules: { "@typescript-eslint/no-explicit-any": "warn" },
},
{
files: ["src/**/*.{ts,tsx}"],
languageOptions: { globals: globals.browser },
-1
View File
@@ -52,7 +52,6 @@ function wordCommands(font) {
function rotate(cmds) {
const pts = cmds.flatMap((c) => c.args);
const xs = pts.filter((_, i) => i % 2 === 0);
const ys = pts.filter((_, i) => i % 2 === 1);
const inkLen = Math.max(...xs) - Math.min(...xs);
const s = INK_LENGTH / inkLen;
const map = (x, y) => [-y * s, -x * s];
+1 -2
View File
@@ -102,7 +102,6 @@ export async function runE2eSteps(log: StepLog, reportPath: string): Promise<voi
},
);
let inv!: Invoice;
const issued = await log.run("e2e: issue and archive", async () => {
const outcome = await issueAndArchive(input, prefs, deps);
invoice = outcome.invoice;
@@ -112,7 +111,7 @@ export async function runE2eSteps(log: StepLog, reportPath: string): Promise<voi
};
});
if (!issued.ok || !invoice) return;
inv = invoice as Invoice;
const inv = invoice as Invoice;
await log.run("e2e: archive status", async () => {
const st = await api.archiveStatus(inv.id);
+1
View File
@@ -22,6 +22,7 @@ export function sanitize(value: string | null | undefined): string {
.replace(/\r\n?/g, "\n")
.replace(/[\t\n\v\f]/g, " ")
.replace(/ /g, " ")
// eslint-disable-next-line no-control-regex -- stripping control characters is the point
.replace(/[\u0000-\u001F\u007F-\u009F​]/g, "")
.replace(/ {2,}/g, " ")
.trim();
+1 -1
View File
@@ -21,7 +21,7 @@ import { RenderTxt } from "./blocks/RenderTxt";
import { TableGuard, guardPresence } from "./blocks/TableGuard";
import { renderCore } from "./render/core";
import { loadCanvas, inkPixels } from "./testing/rasterize";
import { byId, doc, FAMILY, find, frameFor, PAD, PAGE_W, rawLayout, txt } from "./testing/docs";
import { byId, doc, FAMILY, find, frameFor, PAD, rawLayout, txt } from "./testing/docs";
import { FONTS_DIR, setupPdfTest } from "./testing/setup";
import { stack } from "./fonts/register";
-2
View File
@@ -15,8 +15,6 @@ export const REVERSE_GAP = 4;
export const HEADING_AFTER = 4;
/** Space between the closing block's two columns. */
export const CLOSING_COLUMN_GAP = 16;
/** Slate: space between a mark and the business name in the dark band. */
const NAME_AFTER_LOGO = 7.8;
/** Space above and below the thin rule that separates the stack from TOTAL. */
export const STACK_RULE = { before: 2, after: 8, thickness: 0.75 };
const MIN_NAME_W = 40;
+2 -10
View File
@@ -1,17 +1,9 @@
import { mkdirSync, writeFileSync } from "node:fs";
import path from "node:path";
import { describe, expect, it } from "vitest";
import type { AuditIssue } from "../engine/audit";
import { PAGE_SIZES } from "../engine/geometry";
import { PAGE_LABEL_ID, TABLE_HEAD_ID, TABLE_ROW_PREFIX, CLOSING_ID, TOTALS_ID } from "../engine/ids";
import { walk, type LayoutNode, type LayoutPage, type Rect } from "../engine/layoutTree";
import { forbiddenProbe, SLOT_RULES, slotContent, type SlotName } from "../model/slots";
import { frameForPrefs, renderInvoicePdf } from "../render/invoice";
import { loadCanvas, pagePng } from "../testing/rasterize";
import { SLOT_ID } from "../blocks/slots";
import { META_ID, SUPPLIER_ID, TITLE_ID } from "./classic/ids";
import { HEADER_GAP, headerCols } from "./classic/plan";
import { FIXTURE_GSTIN, FIXTURES, buildFixture, ensureSetup, makeItems, PAGE_SIZE_IDS, prefsFor, renderFixture, type FixtureSpec, type Rendered } from "./harness";
import { renderInvoicePdf } from "../render/invoice";
import { FIXTURES, buildFixture, ensureSetup, PAGE_SIZE_IDS, prefsFor, renderFixture, type FixtureSpec } from "./harness";
import { TEMPLATES } from "./registry";
/**
+1 -1
View File
@@ -1,6 +1,6 @@
import { Image, View } from "@react-pdf/renderer";
import type { ReactNode } from "react";
import { colsDetailed, pinned } from "../../engine/columns";
import { pinned } from "../../engine/columns";
import { CLOSING_ID, TABLE_HEAD_ID, TABLE_ROW_PREFIX, TOTALS_ID } from "../../engine/ids";
import { AvailableWidthProvider } from "../../blocks/AvailableWidth";
import { BottomSpacer } from "../../blocks/BottomSpacer";
+1 -1
View File
@@ -202,7 +202,7 @@ function Parties({ ctx }: { ctx: Ctx }) {
const blocks: ReactNode[] = [<ClientBlock key="a" ctx={ctx} width={text[0]} />];
let k = 1;
if (plan.info.hasBank) blocks.push(<PayableBlock key="b" ctx={ctx} width={text[k++]} />);
if (plan.info.hasIds) blocks.push(<IdsBlock key="c" ctx={ctx} width={text[k++]} />);
if (plan.info.hasIds) blocks.push(<IdsBlock key="c" ctx={ctx} width={text[k]} />);
const specs: ColSpec[] = cols.map((w, i) => (i === cols.length - 1 ? { fill: true } : { fixed: w }));
return (
<View style={{ ...pinned(plan.W), paddingLeft: v.inset.left } as never}>
+1 -1
View File
@@ -151,7 +151,7 @@ export function planMarble(model: RenderModel, frame: PageFrame, v: MarbleVarian
let k = 0;
const clientW = textW(k++);
const bankW = hasBank ? textW(k++) : 0;
const idsW = hasIds ? textW(k++) : 0;
const idsW = hasIds ? textW(k) : 0;
const head = leading(r.heading) + v.parties.headingGap;
let clientH = head + textHeight(r.body, c.name, clientW);
for (const l of c.addressLines) clientH += textHeight(r.body, l, clientW);
+1 -1
View File
@@ -296,7 +296,7 @@ export function planNeutral(model: RenderModel, frame: PageFrame, v: NeutralVari
for (const l of lines) payH += textHeight(r.address, l, payW);
}
if (v.id === "serenity") signH += leading(r.label) + 4;
let closingH = 0;
let closingH: number;
if (v.payTo.where === "closing") closingH = Math.max(payH, signH);
else closingH = signH;
if (model.cancelled) closingH += 10 + textHeight(r.small, CANCELLED_SENTENCE, W);
+1 -1
View File
@@ -11,7 +11,7 @@ import type { StepResult } from "./lib/selfTestSteps";
const WATCHDOG_MS = 120_000;
export async function maybeRunSelfTest(): Promise<void> {
let reportPath: string | null = null;
let reportPath: string | null;
try {
reportPath = await invoke<string | null>("selftest_config");
} catch {
-1
View File
@@ -24,7 +24,6 @@ import {
TabPanels,
Tabs,
Tile,
Toggle,
} from "@carbon/react";
import { Add, Save, TrashCan } from "@carbon/icons-react";
import { confirm } from "@tauri-apps/plugin-dialog";
+1 -7
View File
@@ -48,8 +48,7 @@ import type {
Settings,
TaxType,
} from "../lib/types";
import { computeLineAmount, computeTotals } from "../lib/invoice";
import { amountInWords } from "../lib/numberToWords";
import { computeLineAmount } from "../lib/invoice";
import { gstinFullError } from "../lib/validators";
import { addDays, formatAmount, todayIso } from "../lib/format";
import { modelFromAssets, prefsForInvoice } from "../lib/pdf";
@@ -256,11 +255,6 @@ export default function NewInvoice({ settings, onSettingsChange, active, onActiv
? "cgst_sgst"
: "igst";
const clientGstinError = gstinFullError(clientGstin);
const totals = useMemo(
() => computeTotals(items, discount, effectiveTaxType, taxRate),
[items, discount, effectiveTaxType, taxRate],
);
const words = useMemo(() => amountInWords(totals.total), [totals.total]);
const bank = useMemo(() => banks.find((b) => b.id === bankId) ?? null, [banks, bankId]);
const payload = useMemo(
+1 -1
View File
@@ -29,7 +29,7 @@ export default function SeriesSettings({
embedded?: boolean;
}) {
const toast = useToast();
const [active, setActive] = useState<InvoiceSeries | null>(null);
const [, setActive] = useState<InvoiceSeries | null>(null);
const [all, setAll] = useState<InvoiceSeries[]>([]);
const [loading, setLoading] = useState(true);