Skip to content

Commit 4066aaa

Browse files
authored
perf: optimize CI format job + widen lefthook + add ruff for Python (CopilotKit#4812)
## Summary - **lefthook**: Widen pre-commit glob from JS/TS-only to match CI's full file coverage (json, md, css, yml, yaml, html, vue, py). Add `ruff format` for Python files. - **CI format job**: Replace full `pnpm install` (~8min) with standalone binary installs (`oxfmt` + `ruff`, ~10s). Add Python formatting to both PR and push-to-main paths. ## Why The format job took 8+ minutes because it ran `pnpm install --frozen-lockfile` to get the oxfmt binary. oxfmt and ruff are both standalone Rust binaries that don't need the monorepo's node_modules. Local lefthook only covered JS/TS extensions, so JSON/YAML/MD/Python formatting was only enforced in CI — after the push. ## Test plan - [x] lefthook.yml YAML validates - [x] static_quality.yml YAML validates - [ ] CI format job runs in <30s (was ~8min) - [ ] Lefthook pre-commit catches formatting in JSON/MD/Python files
2 parents 090ddb2 + 57d2631 commit 4066aaa

5 files changed

Lines changed: 195 additions & 197 deletions

File tree

.github/workflows/static_quality.yml

Lines changed: 22 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ env:
2727
jobs:
2828
format:
2929
runs-on: ubuntu-latest
30-
timeout-minutes: 10
30+
timeout-minutes: 5
3131
permissions:
3232
contents: write
3333

@@ -41,21 +41,11 @@ jobs:
4141
# tip to scope the formatter to PR-changed files.
4242
fetch-depth: 0
4343

44-
- name: Setup pnpm
45-
uses: pnpm/action-setup@v4
46-
with:
47-
version: "10.13.1"
44+
- name: Install oxfmt
45+
run: npm install -g oxfmt@0.36
4846

49-
- name: Use Node.js 20
50-
uses: actions/setup-node@v4
51-
with:
52-
node-version: 20.x
53-
# setup-node built-in cache is fork-safe (fork PRs can't write to base repo cache)
54-
cache: "pnpm"
55-
cache-dependency-path: "**/pnpm-lock.yaml"
56-
57-
- name: Install dependencies
58-
run: pnpm install --frozen-lockfile
47+
- name: Install ruff
48+
run: pipx install ruff
5949

6050
- name: Collect PR-changed files for formatting
6151
if: github.event_name == 'pull_request'
@@ -84,7 +74,7 @@ jobs:
8474
'*.js' '*.jsx' '*.ts' '*.tsx' '*.mjs' '*.cjs' \
8575
'*.json' '*.jsonc' '*.json5' \
8676
'*.md' \
87-
'*.css' '*.yml' '*.yaml' '*.html' '*.vue' \
77+
'*.css' '*.yml' '*.yaml' '*.html' '*.vue' '*.py' \
8878
':!**/package-lock.json' ':!**/pnpm-lock.yaml' ':!**/yarn.lock' \
8979
> .pr-format-files.txt
9080
: > .pr-format-files.existing.txt
@@ -100,18 +90,29 @@ jobs:
10090
run: |
10191
if [ "${{ github.event_name }}" = "pull_request" ]; then
10292
if [ "${{ steps.changed.outputs.count }}" = "0" ]; then
103-
echo "No formattable files changed in this PR — skipping oxfmt."
93+
echo "No formattable files changed in this PR — skipping."
10494
exit 0
10595
fi
106-
if ! xargs -a .pr-format-files.existing.txt pnpm exec oxfmt --no-error-on-unmatched-pattern --write; then
107-
echo "::warning::Formatter exited with error — auto-fix may be incomplete"
96+
# oxfmt: auto-fix JS/TS/JSON/MD/CSS/YAML/HTML/Vue
97+
if ! xargs -a .pr-format-files.existing.txt oxfmt --no-error-on-unmatched-pattern --write; then
98+
echo "::warning::oxfmt exited with error — auto-fix may be incomplete"
99+
fi
100+
# ruff: auto-fix Python
101+
py_files=$(grep -E '\.py$' .pr-format-files.existing.txt || true)
102+
if [ -n "$py_files" ]; then
103+
echo "$py_files" | xargs ruff format || echo "::warning::ruff format exited with error"
108104
fi
109105
if [ -n "$(git diff --name-only)" ]; then
110106
echo "format_fixed=true" >> $GITHUB_ENV
111107
fi
112-
xargs -a .pr-format-files.existing.txt pnpm exec oxfmt --no-error-on-unmatched-pattern --check
108+
# Check mode: verify everything is formatted
109+
xargs -a .pr-format-files.existing.txt oxfmt --no-error-on-unmatched-pattern --check
110+
if [ -n "$py_files" ]; then
111+
echo "$py_files" | xargs ruff format --check
112+
fi
113113
else
114-
pnpm run check-format
114+
oxfmt --check .
115+
ruff format --check .
115116
fi
116117
117118
- name: Commit formatting fixes

lefthook.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ pre-commit:
2929
# still invokes this hook with an empty expansion, and oxlint/oxfmt
3030
# would default to operating on the current directory, defeating the
3131
# scoping entirely. stage_fixed re-stages whatever the hooks modify.
32-
glob: "*.{js,jsx,ts,tsx,mjs,cjs}"
32+
glob: "*.{js,jsx,ts,tsx,mjs,cjs,json,jsonc,json5,md,css,yml,yaml,html,vue,py}"
3333
# Mirror the ignorePatterns in .oxlintrc.json / .oxfmtrc.json at the
3434
# hook layer. Without this, a docs-only commit expands {staged_files}
3535
# to a single docs/** path; oxlint silently processes 0 files, then
@@ -39,7 +39,9 @@ pre-commit:
3939
- "docs/**"
4040
run: |
4141
if [ -n "{staged_files}" ]; then
42-
pnpm exec oxlint --fix {staged_files} && pnpm exec oxfmt --write {staged_files}
42+
pnpm exec oxlint --fix {staged_files} &&
43+
pnpm exec oxfmt --write {staged_files} ;
44+
ruff format {staged_files} 2>/dev/null || true
4345
fi
4446
stage_fixed: true
4547
test-and-check-packages:
Lines changed: 56 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { describe, it, expect, vi, beforeEach } from "vitest";
1+
import { describe, it, expect, vi } from "vitest";
22
import type { LiveStatusMap, StatusRow } from "@/lib/live-status";
33
import type { Integration, Feature } from "@/lib/registry";
44

@@ -10,62 +10,27 @@ vi.mock("@/lib/registry", () => ({
1010
getFeatureCategories: vi.fn(() => []),
1111
}));
1212

13-
// Mock live-status module before importing the function under test
14-
vi.mock("@/lib/live-status", async () => {
15-
const actual =
16-
await vi.importActual<typeof import("@/lib/live-status")>(
17-
"@/lib/live-status",
18-
);
19-
return {
20-
...actual,
21-
keyFor: vi.fn(actual.keyFor),
22-
resolveCell: vi.fn(),
23-
};
24-
});
25-
2613
import { computeColumnTallyDetail } from "@/components/feature-grid";
27-
import { keyFor, resolveCell } from "@/lib/live-status";
28-
import type { CellState, BadgeRender } from "@/lib/live-status";
29-
30-
const mockedResolveCell = vi.mocked(resolveCell);
3114

3215
/* ------------------------------------------------------------------ */
3316
/* Helpers */
3417
/* ------------------------------------------------------------------ */
3518

36-
function makeRow(overrides: Partial<StatusRow> = {}): StatusRow {
19+
function makeRow(
20+
key: string,
21+
dimension: string,
22+
state: StatusRow["state"],
23+
): StatusRow {
3724
return {
38-
id: "row-1",
39-
key: "health:test-slug",
40-
dimension: "health",
41-
state: "green",
25+
id: key,
26+
key,
27+
dimension,
28+
state,
4229
signal: null,
4330
observed_at: "2026-04-28T00:00:00Z",
4431
transitioned_at: "2026-04-28T00:00:00Z",
4532
fail_count: 0,
4633
first_failure_at: null,
47-
...overrides,
48-
};
49-
}
50-
51-
function makeBadge(tone: string): BadgeRender {
52-
return {
53-
tone: tone as BadgeRender["tone"],
54-
label: tone,
55-
tooltip: "",
56-
row: null,
57-
};
58-
}
59-
60-
function makeCellState(e2eTone: string): CellState {
61-
return {
62-
e2e: makeBadge(e2eTone),
63-
smoke: makeBadge("gray"),
64-
health: makeBadge("gray"),
65-
d2: makeBadge("gray"),
66-
d5: makeBadge("gray"),
67-
d6: makeBadge("gray"),
68-
rollup: e2eTone as CellState["rollup"],
6934
};
7035
}
7136

@@ -100,10 +65,6 @@ function makeFeature(id: string, name: string): Feature {
10065
/* ------------------------------------------------------------------ */
10166

10267
describe("computeColumnTallyDetail", () => {
103-
beforeEach(() => {
104-
vi.clearAllMocks();
105-
});
106-
10768
it("returns unknown: true with empty arrays when connection is error", () => {
10869
const integration = makeIntegration("test-int", ["feat-1"]);
10970
const features = [makeFeature("feat-1", "Feature 1")];
@@ -122,27 +83,21 @@ describe("computeColumnTallyDetail", () => {
12283
red: [],
12384
unknown: true,
12485
});
125-
// resolveCell should NOT be called when connection is error
126-
expect(mockedResolveCell).not.toHaveBeenCalled();
12786
});
12887

129-
it("collects health green + 1 e2e green + 1 e2e red", () => {
88+
it("green D3 cells land in green bucket", () => {
13089
const integration = makeIntegration("my-int", ["feat-a", "feat-b"]);
13190
const features = [
13291
makeFeature("feat-a", "Feature A"),
13392
makeFeature("feat-b", "Feature B"),
13493
];
13594

136-
const healthKey = keyFor("health", "my-int");
95+
// Both features have green D3 → achievedDepth=3, ceilingDepth=3 → green chip
13796
const liveStatus: LiveStatusMap = new Map([
138-
[healthKey, makeRow({ key: healthKey, state: "green" })],
97+
["e2e:my-int/feat-a", makeRow("e2e:my-int/feat-a", "e2e", "green")],
98+
["e2e:my-int/feat-b", makeRow("e2e:my-int/feat-b", "e2e", "green")],
13999
]);
140100

141-
// feat-a e2e → green, feat-b e2e → red
142-
mockedResolveCell
143-
.mockReturnValueOnce(makeCellState("green"))
144-
.mockReturnValueOnce(makeCellState("red"));
145-
146101
const result = computeColumnTallyDetail(
147102
integration,
148103
features,
@@ -152,27 +107,26 @@ describe("computeColumnTallyDetail", () => {
152107

153108
expect(result.unknown).toBe(false);
154109
expect(result.green).toEqual([
155-
{ label: "Health (Up)", dimension: "health" },
156110
{ label: "Feature A", dimension: "e2e", featureId: "feat-a" },
157-
]);
158-
expect(result.red).toEqual([
159111
{ label: "Feature B", dimension: "e2e", featureId: "feat-b" },
160112
]);
161113
expect(result.amber).toEqual([]);
114+
expect(result.red).toEqual([]);
162115
});
163116

164-
it("collects amber e2e features when no health data exists", () => {
165-
const integration = makeIntegration("no-health", ["feat-x", "feat-y"]);
117+
it("red D3 cells are gray (achievedDepth=0) and excluded", () => {
118+
const integration = makeIntegration("my-int", ["feat-a", "feat-b"]);
166119
const features = [
167-
makeFeature("feat-x", "Feature X"),
168-
makeFeature("feat-y", "Feature Y"),
120+
makeFeature("feat-a", "Feature A"),
121+
makeFeature("feat-b", "Feature B"),
169122
];
170-
const liveStatus: LiveStatusMap = new Map();
171123

172-
// Both features → amber
173-
mockedResolveCell
174-
.mockReturnValueOnce(makeCellState("amber"))
175-
.mockReturnValueOnce(makeCellState("amber"));
124+
// feat-a: D3=red → achievedDepth=0, ceilingDepth=3 → gray (skipped)
125+
// feat-b: D3=green → green chip
126+
const liveStatus: LiveStatusMap = new Map([
127+
["e2e:my-int/feat-a", makeRow("e2e:my-int/feat-a", "e2e", "red")],
128+
["e2e:my-int/feat-b", makeRow("e2e:my-int/feat-b", "e2e", "green")],
129+
]);
176130

177131
const result = computeColumnTallyDetail(
178132
integration,
@@ -182,24 +136,24 @@ describe("computeColumnTallyDetail", () => {
182136
);
183137

184138
expect(result.unknown).toBe(false);
185-
expect(result.green).toEqual([]);
186-
expect(result.red).toEqual([]);
187-
expect(result.amber).toEqual([
188-
{ label: "Feature X", dimension: "e2e", featureId: "feat-x" },
189-
{ label: "Feature Y", dimension: "e2e", featureId: "feat-y" },
139+
expect(result.green).toEqual([
140+
{ label: "Feature B", dimension: "e2e", featureId: "feat-b" },
190141
]);
142+
expect(result.amber).toEqual([]);
143+
expect(result.red).toEqual([]);
191144
});
192145

193-
it("skips features that have no matching demo", () => {
146+
it("features without demos are gray (unwired) and excluded", () => {
194147
// Integration only has demo for feat-1, not feat-2
195148
const integration = makeIntegration("partial", ["feat-1"]);
196149
const features = [
197150
makeFeature("feat-1", "Feature 1"),
198151
makeFeature("feat-2", "Feature 2"),
199152
];
200-
const liveStatus: LiveStatusMap = new Map();
201153

202-
mockedResolveCell.mockReturnValueOnce(makeCellState("green"));
154+
const liveStatus: LiveStatusMap = new Map([
155+
["e2e:partial/feat-1", makeRow("e2e:partial/feat-1", "e2e", "green")],
156+
]);
203157

204158
const result = computeColumnTallyDetail(
205159
integration,
@@ -209,23 +163,20 @@ describe("computeColumnTallyDetail", () => {
209163
);
210164

211165
expect(result.unknown).toBe(false);
212-
// Only feat-1 appears (has a demo); feat-2 is absent
166+
// Only feat-1 appears (has a demo); feat-2 is unwired → gray → excluded
213167
expect(result.green).toEqual([
214168
{ label: "Feature 1", dimension: "e2e", featureId: "feat-1" },
215169
]);
216170
expect(result.amber).toEqual([]);
217171
expect(result.red).toEqual([]);
218-
// resolveCell was only called once (for feat-1)
219-
expect(mockedResolveCell).toHaveBeenCalledTimes(1);
220172
});
221173

222-
it("routes degraded health to amber bucket", () => {
223-
const integration = makeIntegration("degraded-int", []);
174+
it("health-only data produces no tally items (health not in cell model)", () => {
175+
const integration = makeIntegration("health-only", []);
224176
const features: Feature[] = [];
225177

226-
const healthKey = keyFor("health", "degraded-int");
227178
const liveStatus: LiveStatusMap = new Map([
228-
[healthKey, makeRow({ key: healthKey, state: "degraded" })],
179+
["health:health-only", makeRow("health:health-only", "health", "red")],
229180
]);
230181

231182
const result = computeColumnTallyDetail(
@@ -235,21 +186,26 @@ describe("computeColumnTallyDetail", () => {
235186
"live",
236187
);
237188

238-
expect(result.amber).toEqual([
239-
{ label: "Health (Up)", dimension: "health" },
240-
]);
189+
// No features → no cells → nothing counted
241190
expect(result.green).toEqual([]);
191+
expect(result.amber).toEqual([]);
242192
expect(result.red).toEqual([]);
243193
expect(result.unknown).toBe(false);
244194
});
245195

246-
it("routes red health to red bucket", () => {
247-
const integration = makeIntegration("red-int", []);
248-
const features: Feature[] = [];
196+
it("not_supported_features are gray and excluded", () => {
197+
const integration = {
198+
...makeIntegration("ns-int", ["feat-a", "feat-b"]),
199+
not_supported_features: ["feat-b"],
200+
};
201+
const features = [
202+
makeFeature("feat-a", "Feature A"),
203+
makeFeature("feat-b", "Feature B"),
204+
];
249205

250-
const healthKey = keyFor("health", "red-int");
251206
const liveStatus: LiveStatusMap = new Map([
252-
[healthKey, makeRow({ key: healthKey, state: "red" })],
207+
["e2e:ns-int/feat-a", makeRow("e2e:ns-int/feat-a", "e2e", "green")],
208+
["e2e:ns-int/feat-b", makeRow("e2e:ns-int/feat-b", "e2e", "green")],
253209
]);
254210

255211
const result = computeColumnTallyDetail(
@@ -259,9 +215,12 @@ describe("computeColumnTallyDetail", () => {
259215
"live",
260216
);
261217

262-
expect(result.red).toEqual([{ label: "Health (Up)", dimension: "health" }]);
263-
expect(result.green).toEqual([]);
264-
expect(result.amber).toEqual([]);
265218
expect(result.unknown).toBe(false);
219+
// feat-a: green; feat-b: unsupported → gray → excluded
220+
expect(result.green).toEqual([
221+
{ label: "Feature A", dimension: "e2e", featureId: "feat-a" },
222+
]);
223+
expect(result.amber).toEqual([]);
224+
expect(result.red).toEqual([]);
266225
});
267226
});

0 commit comments

Comments
 (0)