feat(sse): add RTK filters for kubectl, docker-build, composer, gh (#2824)

Integrated into release/v3.8.6.
This commit is contained in:
Lenine Júnior
2026-05-29 00:43:51 -04:00
committed by GitHub
parent 9dfe482c1c
commit 32adf6275a
6 changed files with 478 additions and 0 deletions

View File

@@ -55,6 +55,9 @@ const COMMAND_PREFIXES = [
"golangci-lint",
"bundle",
"rubocop",
"kubectl",
"composer",
"gh",
"docker",
"aws",
"gcloud",

View File

@@ -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."
}
]
}

View File

@@ -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"
}
]
}

View 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]"
}
]
}

View File

@@ -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}"
}
]
}

View 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(", ")}`
);
});
});