mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
feat(sse): add RTK filters for kubectl, docker-build, composer, gh (#2824)
Integrated into release/v3.8.6.
This commit is contained in:
@@ -55,6 +55,9 @@ const COMMAND_PREFIXES = [
|
||||
"golangci-lint",
|
||||
"bundle",
|
||||
"rubocop",
|
||||
"kubectl",
|
||||
"composer",
|
||||
"gh",
|
||||
"docker",
|
||||
"aws",
|
||||
"gcloud",
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
{
|
||||
"id": "composer",
|
||||
"label": "Composer (PHP)",
|
||||
"description": "Keep Composer install/update results, warnings, and dependency errors.",
|
||||
"category": "package",
|
||||
"priority": 65,
|
||||
"match": {
|
||||
"outputTypes": ["composer"],
|
||||
"commands": [
|
||||
"^composer\\s+(?:install|update|require|remove|dump-autoload|outdated)\\b",
|
||||
"^php\\s+composer(?:\\.phar)?\\s+(?:install|update|require)\\b"
|
||||
],
|
||||
"patterns": [
|
||||
"Package operations: \\d+ install",
|
||||
"Generating (?:optimized )?autoload files",
|
||||
"Your requirements could not be resolved",
|
||||
"Problem \\d+",
|
||||
"vulnerabilities found"
|
||||
]
|
||||
},
|
||||
"rules": {
|
||||
"stripAnsi": true,
|
||||
"includePatterns": [
|
||||
"Package operations: \\d+",
|
||||
"Generating (?:optimized )?autoload files",
|
||||
"Your requirements could not be resolved",
|
||||
"Problem \\d+",
|
||||
"vulnerabilities found",
|
||||
"WARNING",
|
||||
"ERROR",
|
||||
"Could not (?:find|resolve|delete)",
|
||||
"conflicts with",
|
||||
"requires .+ but",
|
||||
"- (?:Installing|Updating|Removing) \\S+",
|
||||
"PHP Fatal error",
|
||||
"Class .+ not found"
|
||||
],
|
||||
"dropPatterns": [
|
||||
"^\\s*$",
|
||||
"^Loading composer repositories",
|
||||
"^Updating dependencies",
|
||||
"^Writing lock file",
|
||||
"^Installing dependencies from lock file",
|
||||
"^Downloading \\(\\d+%\\)",
|
||||
"^\\s*-\\s+Downloading"
|
||||
],
|
||||
"collapsePatterns": ["^\\s*-\\s+Installing"],
|
||||
"deduplicate": true,
|
||||
"maxLines": 100,
|
||||
"headLines": 20,
|
||||
"tailLines": 50
|
||||
},
|
||||
"preserve": {
|
||||
"errorPatterns": [
|
||||
"Your requirements could not be resolved",
|
||||
"Problem \\d+",
|
||||
"ERROR",
|
||||
"PHP Fatal error",
|
||||
"Class .+ not found"
|
||||
],
|
||||
"summaryPatterns": [
|
||||
"Package operations: \\d+",
|
||||
"Generating (?:optimized )?autoload files",
|
||||
"vulnerabilities found"
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"name": "successful install",
|
||||
"command": "composer install",
|
||||
"input": "Loading composer repositories with package information\nUpdating dependencies\nLock file operations: 5 installs, 0 updates, 0 removals\n - Locking laravel/framework (v11.0.0)\nWriting lock file\nInstalling dependencies from lock file (including require-dev)\nPackage operations: 5 installs, 0 updates, 0 removals\n - Downloading laravel/framework (v11.0.0)\n - Installing laravel/framework (v11.0.0)\nGenerating optimized autoload files\n",
|
||||
"expected": "Package operations: 5 installs, 0 updates, 0 removals\n - Installing laravel/framework (v11.0.0)\nGenerating optimized autoload files"
|
||||
},
|
||||
{
|
||||
"name": "dependency conflict",
|
||||
"command": "composer require",
|
||||
"input": "Your requirements could not be resolved to an installable set of packages.\n\n Problem 1\n - laravel/framework[v11.0.0] requires php ^8.2 but your php version (8.1.0) does not satisfy that requirement.\n",
|
||||
"expected": "Your requirements could not be resolved to an installable set of packages.\n Problem 1\n - laravel/framework[v11.0.0] requires php ^8.2 but your php version (8.1.0) does not satisfy that requirement."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
{
|
||||
"id": "docker-build",
|
||||
"label": "Docker Build",
|
||||
"description": "Keep Docker build errors and final summary while stripping layer-by-layer progress.",
|
||||
"category": "docker",
|
||||
"priority": 82,
|
||||
"match": {
|
||||
"outputTypes": ["docker-build"],
|
||||
"commands": [
|
||||
"^docker\\s+(?:build|buildx\\s+build)\\b",
|
||||
"^(?:docker\\s+compose|docker-compose)\\s+build\\b"
|
||||
],
|
||||
"patterns": [
|
||||
"Successfully built \\w+",
|
||||
"Successfully tagged \\S+",
|
||||
"writing image sha256:",
|
||||
"ERROR \\[",
|
||||
"exit code: \\d+"
|
||||
]
|
||||
},
|
||||
"rules": {
|
||||
"stripAnsi": true,
|
||||
"includePatterns": [
|
||||
"ERROR",
|
||||
"ERR!",
|
||||
"exit code: \\d+",
|
||||
"failed to solve",
|
||||
"failed to compute cache key",
|
||||
"Cannot connect to the Docker daemon",
|
||||
"no such file or directory",
|
||||
"Successfully built \\w+",
|
||||
"Successfully tagged \\S+",
|
||||
"writing image sha256:",
|
||||
"naming to docker\\.io/\\S+",
|
||||
"DONE",
|
||||
"^Step \\d+/\\d+ :",
|
||||
"Sending build context",
|
||||
"WARN\\s*\\["
|
||||
],
|
||||
"dropPatterns": [
|
||||
"^#\\d+\\s+sha256:",
|
||||
"^\\s*$",
|
||||
"^\\s+\\d+\\.\\d+s$",
|
||||
"^\\s*-+>\\s*Running in",
|
||||
"^\\s*Removing intermediate container",
|
||||
"^\\s*-+>\\s*[a-f0-9]{12}$"
|
||||
],
|
||||
"collapsePatterns": ["^#\\d+\\s+CACHED", "^\\s*Pulling fs layer"],
|
||||
"deduplicate": false,
|
||||
"maxLines": 100,
|
||||
"headLines": 15,
|
||||
"tailLines": 50
|
||||
},
|
||||
"preserve": {
|
||||
"errorPatterns": ["ERROR", "exit code: \\d+", "failed to solve", "Cannot connect"],
|
||||
"summaryPatterns": ["Successfully built", "Successfully tagged", "writing image", "DONE"]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"name": "successful build",
|
||||
"command": "docker build",
|
||||
"input": "Step 1/3 : FROM node:20\nStep 2/3 : COPY . /app\nStep 3/3 : RUN npm install\n ---> Running in abc123\n ---> def456\nRemoving intermediate container abc123\nSuccessfully built def456\nSuccessfully tagged myapp:latest\n",
|
||||
"expected": "Step 1/3 : FROM node:20\nStep 2/3 : COPY . /app\nStep 3/3 : RUN npm install\nSuccessfully built def456\nSuccessfully tagged myapp:latest"
|
||||
},
|
||||
{
|
||||
"name": "build failure",
|
||||
"command": "docker build",
|
||||
"input": "Step 1/2 : FROM node:20\nStep 2/2 : RUN npm install\nERROR: failed to solve: process \"/bin/sh -c npm install\" did not complete successfully: exit code: 1\n",
|
||||
"expected": "Step 1/2 : FROM node:20\nStep 2/2 : RUN npm install\nERROR: failed to solve: process \"/bin/sh -c npm install\" did not complete successfully: exit code: 1"
|
||||
},
|
||||
{
|
||||
"name": "preserves buildkit-prefixed RUN step errors",
|
||||
"command": "docker build",
|
||||
"input": "#8 [build 3/4] RUN npm run build\n#8 0.456 src/a.ts:1:1 - ERROR TS2322: Type 'string' is not assignable\n#8 0.789 npm ERR! exit code 1\n#8 ERROR: process \"/bin/sh -c npm run build\" did not complete successfully: exit code: 1\n",
|
||||
"expected": "#8 0.456 src/a.ts:1:1 - ERROR TS2322: Type 'string' is not assignable\n#8 0.789 npm ERR! exit code 1\n#8 ERROR: process \"/bin/sh -c npm run build\" did not complete successfully: exit code: 1"
|
||||
}
|
||||
]
|
||||
}
|
||||
98
open-sse/services/compression/engines/rtk/filters/gh.json
Normal file
98
open-sse/services/compression/engines/rtk/filters/gh.json
Normal file
@@ -0,0 +1,98 @@
|
||||
{
|
||||
"id": "gh",
|
||||
"label": "GitHub CLI",
|
||||
"description": "Keep GitHub CLI command results, errors, and PR/issue summaries while stripping pagination noise. Skips `gh api` and `--json` invocations (structured output that would be corrupted by line filtering).",
|
||||
"category": "cloud",
|
||||
"priority": 70,
|
||||
"match": {
|
||||
"outputTypes": ["gh"],
|
||||
"commands": [
|
||||
"^gh\\s+(?:pr|issue|repo|run|workflow|release|auth|browse|gist|secret|label|search|status)(?!.*--json)\\b"
|
||||
],
|
||||
"patterns": [
|
||||
"^https?://github\\.com/",
|
||||
"Created pull request",
|
||||
"Created issue",
|
||||
"✓\\s+Logged in",
|
||||
"X\\s+\\S+",
|
||||
"GraphQL error",
|
||||
"HTTP \\d{3}:"
|
||||
]
|
||||
},
|
||||
"rules": {
|
||||
"stripAnsi": true,
|
||||
"includePatterns": [
|
||||
"^https?://github\\.com/",
|
||||
"Creat(?:ed|ing) pull request",
|
||||
"Creat(?:ed|ing) issue",
|
||||
"Merged pull request",
|
||||
"Closed (?:pull request|issue)",
|
||||
"Reopened",
|
||||
"✓\\s+",
|
||||
"X\\s+\\S+",
|
||||
"FAIL",
|
||||
"GraphQL error",
|
||||
"HTTP \\d{3}:",
|
||||
"error:",
|
||||
"not found",
|
||||
"must be authenticated",
|
||||
"rate limit",
|
||||
"completed\\s+\\w+",
|
||||
"queued\\s+\\w+",
|
||||
"in_progress\\s+\\w+",
|
||||
"#\\d+\\s+\\S"
|
||||
],
|
||||
"dropPatterns": [
|
||||
"^\\s*$",
|
||||
"^Showing \\d+ of \\d+",
|
||||
"^For more information on output formatting"
|
||||
],
|
||||
"collapsePatterns": [],
|
||||
"deduplicate": false,
|
||||
"maxLines": 80,
|
||||
"headLines": 20,
|
||||
"tailLines": 40
|
||||
},
|
||||
"preserve": {
|
||||
"errorPatterns": [
|
||||
"GraphQL error",
|
||||
"HTTP \\d{3}:",
|
||||
"error:",
|
||||
"not found",
|
||||
"must be authenticated",
|
||||
"rate limit"
|
||||
],
|
||||
"summaryPatterns": [
|
||||
"Creat(?:ed|ing) pull request",
|
||||
"Creat(?:ed|ing) issue",
|
||||
"Merged pull request",
|
||||
"^https?://github\\.com/"
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"name": "pr create",
|
||||
"command": "gh pr create",
|
||||
"input": "\nCreating pull request for feat/foo into main in owner/repo\n\nhttps://github.com/owner/repo/pull/123\n",
|
||||
"expected": "Creating pull request for feat/foo into main in owner/repo\nhttps://github.com/owner/repo/pull/123"
|
||||
},
|
||||
{
|
||||
"name": "auth error",
|
||||
"command": "gh pr list",
|
||||
"input": "error: must be authenticated to use this command\n",
|
||||
"expected": "error: must be authenticated to use this command"
|
||||
},
|
||||
{
|
||||
"name": "skips gh api (structured JSON output)",
|
||||
"command": "gh api repos/owner/repo/pulls/1",
|
||||
"input": "{\n \"number\": 1,\n \"title\": \"feat: x\",\n \"user\": { \"login\": \"alice\" }\n}\n",
|
||||
"expected": "{\n \"number\": 1,\n \"title\": \"feat: x\",\n \"user\": { \"login\": \"alice\" }\n}"
|
||||
},
|
||||
{
|
||||
"name": "skips --json output (would corrupt structured data)",
|
||||
"command": "gh pr list --json number,title",
|
||||
"input": "[\n { \"number\": 1, \"title\": \"feat: x\" },\n { \"number\": 2, \"title\": \"fix: y\" }\n]\n",
|
||||
"expected": "[\n { \"number\": 1, \"title\": \"feat: x\" },\n { \"number\": 2, \"title\": \"fix: y\" }\n]"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
{
|
||||
"id": "kubectl",
|
||||
"label": "kubectl",
|
||||
"description": "Keep kubectl status, rollout and error output. Skips logs/exec/top (arbitrary content) and structured -o json/yaml output.",
|
||||
"category": "cloud",
|
||||
"priority": 78,
|
||||
"match": {
|
||||
"outputTypes": ["kubectl"],
|
||||
"commands": [
|
||||
"^kubectl\\s+(?:get|describe|apply|delete|rollout|events?)(?!.*(?:-o|--output)(?:\\s+|=)(?:json|yaml|go-template|jsonpath|template|name))\\b"
|
||||
],
|
||||
"patterns": [
|
||||
"NAME\\s+READY\\s+STATUS",
|
||||
"NAME\\s+TYPE\\s+CLUSTER-IP",
|
||||
"CrashLoopBackOff",
|
||||
"ImagePullBackOff",
|
||||
"ErrImagePull",
|
||||
"FailedScheduling",
|
||||
"OOMKilled",
|
||||
"Error from server"
|
||||
]
|
||||
},
|
||||
"rules": {
|
||||
"stripAnsi": true,
|
||||
"includePatterns": [
|
||||
"Error from server",
|
||||
"CrashLoopBackOff",
|
||||
"ImagePullBackOff",
|
||||
"ErrImagePull",
|
||||
"FailedScheduling",
|
||||
"OOMKilled",
|
||||
"Evicted",
|
||||
"ContainerCannotRun",
|
||||
"BackOff",
|
||||
"Unhealthy",
|
||||
"Killing",
|
||||
"Restarting",
|
||||
"NAME\\s+READY\\s+STATUS",
|
||||
"NAME\\s+TYPE\\s+CLUSTER-IP",
|
||||
"Warning\\s+",
|
||||
"deployment\\.apps/\\S+\\s+(?:created|configured|unchanged)",
|
||||
"service/\\S+\\s+(?:created|configured)",
|
||||
"pod/\\S+\\s+(?:created|deleted)",
|
||||
"rollout (?:status|history)",
|
||||
"successfully rolled out"
|
||||
],
|
||||
"dropPatterns": [
|
||||
"^\\s*$",
|
||||
"^\\s*creationTimestamp:",
|
||||
"^\\s*resourceVersion:",
|
||||
"^\\s*uid:",
|
||||
"^\\s*managedFields:",
|
||||
"^\\s*selfLink:",
|
||||
"^\\s*generation:"
|
||||
],
|
||||
"collapsePatterns": ["Normal\\s+", "^\\s*-\\s+lastTransitionTime:"],
|
||||
"deduplicate": true,
|
||||
"maxLines": 120,
|
||||
"headLines": 20,
|
||||
"tailLines": 60
|
||||
},
|
||||
"preserve": {
|
||||
"errorPatterns": [
|
||||
"Error from server",
|
||||
"CrashLoopBackOff",
|
||||
"ImagePullBackOff",
|
||||
"OOMKilled",
|
||||
"FailedScheduling",
|
||||
"Evicted"
|
||||
],
|
||||
"summaryPatterns": ["successfully rolled out", "deployment\\.apps/\\S+\\s+\\w+"]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"name": "pod listing with failure",
|
||||
"command": "kubectl get",
|
||||
"input": "NAME READY STATUS RESTARTS AGE\napi-7d8 1/1 Running 0 2h\nweb-2c4 0/1 CrashLoopBackOff 5 12m\ndb-9f1 1/1 Running 0 2h\n",
|
||||
"expected": "NAME READY STATUS RESTARTS AGE\nweb-2c4 0/1 CrashLoopBackOff 5 12m"
|
||||
},
|
||||
{
|
||||
"name": "rollout success",
|
||||
"command": "kubectl rollout status",
|
||||
"input": "Waiting for deployment \"api\" rollout to finish: 0 of 3 updated replicas are available...\nWaiting for deployment \"api\" rollout to finish: 1 of 3 updated replicas are available...\ndeployment \"api\" successfully rolled out\n",
|
||||
"expected": "deployment \"api\" successfully rolled out"
|
||||
},
|
||||
{
|
||||
"name": "skips kubectl logs (arbitrary application output)",
|
||||
"command": "kubectl logs api-pod",
|
||||
"input": "2026-05-28T10:00:00 INFO request handled\n2026-05-28T10:00:01 INFO request handled\n",
|
||||
"expected": "2026-05-28T10:00:00 INFO request handled\n2026-05-28T10:00:01 INFO request handled"
|
||||
},
|
||||
{
|
||||
"name": "skips json output (would corrupt structured data)",
|
||||
"command": "kubectl get pods -o json",
|
||||
"input": "{\n \"apiVersion\": \"v1\",\n \"items\": [\n { \"metadata\": { \"name\": \"api\" } }\n ]\n}\n",
|
||||
"expected": "{\n \"apiVersion\": \"v1\",\n \"items\": [\n { \"metadata\": { \"name\": \"api\" } }\n ]\n}"
|
||||
}
|
||||
]
|
||||
}
|
||||
119
tests/unit/rtk-new-filters.test.ts
Normal file
119
tests/unit/rtk-new-filters.test.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
loadRtkFilters,
|
||||
matchRtkFilter,
|
||||
runRtkFilterTests,
|
||||
} from "../../open-sse/services/compression/index.ts";
|
||||
|
||||
const NEW_FILTERS = ["kubectl", "docker-build", "composer", "gh"] as const;
|
||||
|
||||
const MATCH_CASES: Array<[string, string, string]> = [
|
||||
[
|
||||
"kubectl",
|
||||
"NAME READY STATUS RESTARTS AGE\nweb-2c4 0/1 CrashLoopBackOff 5 12m",
|
||||
"kubectl get pods",
|
||||
],
|
||||
[
|
||||
"docker-build",
|
||||
"Step 1/2 : FROM node:20\nSuccessfully built abc123",
|
||||
"docker build -t myapp .",
|
||||
],
|
||||
[
|
||||
"composer",
|
||||
"Package operations: 5 installs, 0 updates, 0 removals\nGenerating optimized autoload files",
|
||||
"composer install",
|
||||
],
|
||||
[
|
||||
"gh",
|
||||
"Creating pull request for feat/foo into main in owner/repo\nhttps://github.com/owner/repo/pull/123",
|
||||
"gh pr create",
|
||||
],
|
||||
];
|
||||
|
||||
describe("RTK new-filter catalog (kubectl, docker-build, composer, gh)", () => {
|
||||
it("loads all 4 new filters from disk", () => {
|
||||
const filters = loadRtkFilters({ refresh: true, customFiltersEnabled: false });
|
||||
const ids = new Set(filters.map((f) => f.id));
|
||||
for (const id of NEW_FILTERS) {
|
||||
assert.ok(ids.has(id), `expected filter "${id}" to be loaded`);
|
||||
}
|
||||
});
|
||||
|
||||
it("matches each new filter from realistic command output", () => {
|
||||
for (const [expectedId, output, command] of MATCH_CASES) {
|
||||
const filter = matchRtkFilter(output, command, { customFiltersEnabled: false });
|
||||
assert.equal(filter?.id, expectedId, `wrong filter matched for "${command}"`);
|
||||
}
|
||||
});
|
||||
|
||||
it("kubectl skips arbitrary-content commands (logs, exec, top)", () => {
|
||||
const logOutput =
|
||||
"2026-05-28T10:00:00 INFO request handled\n2026-05-28T10:00:01 INFO request handled";
|
||||
for (const cmd of ["kubectl logs api-pod", "kubectl exec api-pod -- npm test", "kubectl top nodes"]) {
|
||||
const filter = matchRtkFilter(logOutput, cmd, { customFiltersEnabled: false });
|
||||
assert.notEqual(
|
||||
filter?.id,
|
||||
"kubectl",
|
||||
`kubectl filter must not claim "${cmd}" (arbitrary content)`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("kubectl skips structured -o json/yaml output to avoid corrupting it", () => {
|
||||
const jsonOutput = '{\n "apiVersion": "v1",\n "items": []\n}';
|
||||
for (const cmd of [
|
||||
"kubectl get pods -o json",
|
||||
"kubectl get pods -o yaml",
|
||||
"kubectl get pods --output=json",
|
||||
"kubectl get svc -o jsonpath='{.items[*].metadata.name}'",
|
||||
]) {
|
||||
const filter = matchRtkFilter(jsonOutput, cmd, { customFiltersEnabled: false });
|
||||
assert.notEqual(
|
||||
filter?.id,
|
||||
"kubectl",
|
||||
`kubectl filter must not claim structured output: "${cmd}"`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("gh skips `gh api` and --json invocations (structured output)", () => {
|
||||
const jsonOutput = '{\n "number": 1,\n "title": "feat: x"\n}';
|
||||
for (const cmd of [
|
||||
"gh api repos/owner/repo/pulls/1",
|
||||
"gh api graphql -f query=...",
|
||||
"gh pr list --json number,title",
|
||||
"gh issue list --json number,title,author",
|
||||
]) {
|
||||
const filter = matchRtkFilter(jsonOutput, cmd, { customFiltersEnabled: false });
|
||||
assert.notEqual(
|
||||
filter?.id,
|
||||
"gh",
|
||||
`gh filter must not claim structured output: "${cmd}"`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("docker-build matches both v2 (docker compose) and legacy (docker-compose) build", () => {
|
||||
const output = "Step 1/2 : FROM node:20\nSuccessfully built abc123";
|
||||
for (const cmd of ["docker build .", "docker compose build", "docker-compose build", "docker buildx build ."]) {
|
||||
const filter = matchRtkFilter(output, cmd, { customFiltersEnabled: false });
|
||||
assert.equal(filter?.id, "docker-build", `expected docker-build to match "${cmd}"`);
|
||||
}
|
||||
});
|
||||
|
||||
it("inline tests for new filters all pass", () => {
|
||||
const result = runRtkFilterTests({ requireAll: false, customFiltersEnabled: false });
|
||||
const newFilterOutcomes = result.outcomes.filter((o) =>
|
||||
NEW_FILTERS.includes(o.filterId as (typeof NEW_FILTERS)[number])
|
||||
);
|
||||
assert.ok(newFilterOutcomes.length > 0, "expected inline tests from new filters to run");
|
||||
const failed = newFilterOutcomes.filter((o) => !o.passed);
|
||||
assert.deepEqual(
|
||||
failed,
|
||||
[],
|
||||
`inline tests failed: ${failed.map((f) => `${f.filterId}/${f.testName}`).join(", ")}`
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user