Skip to content

Commit 746592c

Browse files
committed
chore: implement pre lint auto-fix pipeline
Related: AAP-64628
1 parent 4266bc7 commit 746592c

File tree

4 files changed

+102
-78
lines changed

4 files changed

+102
-78
lines changed

.github/workflows/pre.yml

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
---
2+
name: pre
3+
on:
4+
merge_group:
5+
branches: ["main", "devel/*"]
6+
push:
7+
branches: ["main", "devel/*"]
8+
tags:
9+
- "v*.*"
10+
pull_request:
11+
# 'closed' is missing to avoid double triggering on PR merge
12+
# 'edited' is missing to allow us to edit PR title/description without triggering
13+
types: [synchronize, opened, reopened]
14+
branches: ["main", "devel/*"]
15+
permissions:
16+
contents: write
17+
id-token: write
18+
pull-requests: write
19+
checks: write
20+
jobs:
21+
prek:
22+
runs-on: ubuntu-24.04
23+
steps:
24+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
25+
with:
26+
fetch-depth: 0 # we need tags for dynamic versioning
27+
show-progress: false
28+
29+
# needed by our prek system hooks like toml
30+
- uses: astral-sh/setup-uv@v7
31+
32+
# needed by our prek systems hooks like biome
33+
- name: Set up Node.js
34+
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
35+
36+
- name: yarn install
37+
run: |
38+
npx yarn install --immutable
39+
40+
- uses: j178/prek-action@v1
41+
with:
42+
install-only: true
43+
44+
- name: Run prek with auto-fix
45+
id: prek
46+
run: |
47+
EXIT_CODE=0
48+
prek run --all-files --color=always || EXIT_CODE=$?
49+
if [ -n "$(git status --porcelain)" ]; then
50+
git config --global user.email ansible-devtools@redhat.com
51+
git config --global user.name "Ansible DevTools"
52+
git add -A
53+
git commit -m 'Prek auto-fixes'
54+
prek run --all-files --color=always || EXIT_CODE=$?
55+
if [ $EXIT_CODE -eq 0 ]; then
56+
git push origin || EXIT_CODE=$?
57+
fi
58+
fi
59+
exit $EXIT_CODE

.pre-commit-config.yaml

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,20 +47,18 @@ repos:
4747
language: system
4848
pass_filenames: false
4949
always_run: true
50-
entry: biome check --write --unsafe
50+
entry: npx biome check --write --unsafe
5151
- id: knip
5252
name: knip (typescript declutter)
53-
entry: yarn knip --fix
53+
entry: npx knip --fix
5454
language: system
5555
pass_filenames: false
5656
always_run: true
5757
- id: eslint
5858
name: eslint
59-
entry: npm
59+
entry: npx
6060
language: system
6161
args:
62-
- exec
63-
- --
6462
- eslint
6563
- --no-warn-ignored
6664
- --color
@@ -81,7 +79,7 @@ repos:
8179
- id: renovate-config-validator
8280
name: renovate config validator
8381
alias: renovate
84-
entry: renovate-config-validator
82+
entry: npx --yes --package renovate -- renovate-config-validator
8583
language: system
8684
pass_filenames: false
8785
files: "renovate.json"

eslint.config.mjs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,7 @@ import { defineConfig } from "eslint/config";
1414
import { createRequire } from "module";
1515

1616
const require = createRequire(import.meta.url);
17-
const noUnsafeSpawnRule =
18-
require("./out/client/test/eslint/no-unsafe-spawn.js").default;
17+
const noUnsafeSpawnRule = require("./test/eslint/no-unsafe-spawn.cjs");
1918

2019
const __filename = fileURLToPath(import.meta.url); // get the resolved path to the file
2120
const __dirname = path.dirname(__filename); // get the name of the directory
@@ -68,6 +67,19 @@ export default defineConfig(
6867
"@typescript-eslint/no-unsafe-argument": "off",
6968
},
7069
},
70+
{
71+
// CommonJS rule file: Node globals (module, require, exports)
72+
files: ["test/eslint/**/*.cjs"],
73+
languageOptions: {
74+
globals: {
75+
...globals.node,
76+
},
77+
parserOptions: {
78+
ecmaVersion: 2022,
79+
sourceType: "script",
80+
},
81+
},
82+
},
7183
eslint.configs.recommended,
7284
prettierRecommendedConfig,
7385
tseslint.configs.recommended,
Lines changed: 25 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,72 +1,25 @@
1+
"use strict";
12
/**
23
* ESLint rule to detect unsafe child_process spawn/spawnSync calls
34
* that use a single string argument instead of separate command and args array.
45
*
56
* This prevents shell injection vulnerabilities and ensures proper argument handling.
67
*/
78

8-
import type { Rule } from "eslint";
9-
10-
// Extend RuleContext to include report method which exists at runtime
11-
interface ExtendedRuleContext extends Rule.RuleContext {
12-
report(options: { node: Rule.Node; messageId: string }): void;
13-
}
14-
15-
// Extend RuleModule to require meta (it's optional in the base type)
16-
interface ExtendedRuleModule extends Rule.RuleModule {
17-
meta: {
18-
type: "problem";
19-
docs: {
20-
description: string;
21-
category: string;
22-
recommended: boolean;
23-
};
24-
fixable: undefined;
25-
schema: [];
26-
messages: {
27-
unsafeSpawn: string;
28-
unsafeSpawnSync: string;
29-
};
30-
};
31-
create(context: ExtendedRuleContext): Rule.RuleListener;
32-
}
33-
34-
const rule: ExtendedRuleModule = {
35-
meta: {
36-
type: "problem",
37-
docs: {
38-
description:
39-
"disallow child_process.spawn/spawnSync with single string argument containing spaces",
40-
category: "Security",
41-
recommended: true,
42-
},
43-
fixable: undefined,
44-
schema: [],
45-
messages: {
46-
unsafeSpawn:
47-
"Use spawn(command, args[]) instead of spawn(commandString). Split the command string into command and args array to prevent shell injection.",
48-
unsafeSpawnSync:
49-
"Use spawnSync(command, args[]) instead of spawnSync(commandString). Split the command string into command and args array to prevent shell injection.",
50-
},
51-
},
52-
create(context: ExtendedRuleContext): Rule.RuleListener {
9+
const rule = {
10+
create(context) {
5311
/**
5412
* Check if a call expression is spawn or spawnSync
5513
*/
56-
function isSpawnCall(
57-
node: Rule.Node,
58-
): node is Rule.Node & { type: "CallExpression" } {
14+
function isSpawnCall(node) {
5915
if (node.type !== "CallExpression" || !("callee" in node)) {
6016
return false;
6117
}
62-
6318
const callee = node.callee;
64-
6519
// Check for: spawnSync(...) or spawn(...) as identifier
6620
if (callee.type === "Identifier") {
6721
return callee.name === "spawnSync" || callee.name === "spawn";
6822
}
69-
7023
// Check for: child_process.spawnSync(...) or cp.spawnSync(...)
7124
if (
7225
callee.type === "MemberExpression" &&
@@ -77,31 +30,24 @@ const rule: ExtendedRuleModule = {
7730
const methodName = callee.property.name;
7831
return methodName === "spawnSync" || methodName === "spawn";
7932
}
80-
8133
return false;
8234
}
83-
8435
/**
8536
* Check if the call is unsafe (command string instead of command + args array)
8637
*/
87-
function isUnsafeCall(
88-
node: Rule.Node & { type: "CallExpression" },
89-
): boolean {
38+
function isUnsafeCall(node) {
9039
const args = node.arguments;
9140
if (!args || args.length === 0) {
9241
return false;
9342
}
94-
9543
const firstArg = args[0];
9644
if (!firstArg) {
9745
return false;
9846
}
99-
10047
// If there's a second argument that's an array, it's safe (proper usage)
10148
if (args.length > 1 && args[1].type === "ArrayExpression") {
10249
return false;
10350
}
104-
10551
// Check if second argument is an options object with shell: true
10652
// This is unsafe because it goes through shell interpretation
10753
if (
@@ -125,14 +71,12 @@ const rule: ExtendedRuleModule = {
12571
}
12672
}
12773
}
128-
12974
// Check if first argument is a string literal with spaces
13075
if (firstArg.type === "Literal" && typeof firstArg.value === "string") {
13176
const value = firstArg.value.trim();
13277
// Flag if it contains spaces (command + args) but allow single commands
13378
return value.includes(" ") && value.length > 0;
13479
}
135-
13680
// Check if it's a template literal
13781
if (firstArg.type === "TemplateLiteral") {
13882
const quasis = firstArg.quasis || [];
@@ -144,7 +88,6 @@ const rule: ExtendedRuleModule = {
14488
}
14589
}
14690
}
147-
14891
// Check if first argument is a variable and second is options (not array)
14992
// This is potentially unsafe - we can't know the variable's value statically,
15093
// but if it's not followed by an array, it's likely a command string
@@ -156,16 +99,13 @@ const rule: ExtendedRuleModule = {
15699
// Variable with options object (not array) - likely unsafe
157100
return true;
158101
}
159-
160102
return false;
161103
}
162-
163104
return {
164-
CallExpression(node: Rule.Node) {
105+
CallExpression(node) {
165106
if (!isSpawnCall(node)) {
166107
return;
167108
}
168-
169109
// Check if it's an unsafe call
170110
if (isUnsafeCall(node)) {
171111
const methodName =
@@ -177,19 +117,34 @@ const rule: ExtendedRuleModule = {
177117
node.callee.property.type === "Identifier"
178118
? node.callee.property.name
179119
: "spawn";
180-
181120
context.report({
182-
node,
183121
messageId:
184122
methodName === "spawnSync" ? "unsafeSpawnSync" : "unsafeSpawn",
123+
node,
185124
});
186125
}
187126
},
188127
};
189128
},
129+
meta: {
130+
docs: {
131+
category: "Security",
132+
description:
133+
"disallow child_process.spawn/spawnSync with single string argument containing spaces",
134+
recommended: true,
135+
},
136+
fixable: undefined,
137+
messages: {
138+
unsafeSpawn:
139+
"Use spawn(command, args[]) instead of spawn(commandString). Split the command string into command and args array to prevent shell injection.",
140+
unsafeSpawnSync:
141+
"Use spawnSync(command, args[]) instead of spawnSync(commandString). Split the command string into command and args array to prevent shell injection.",
142+
},
143+
schema: [],
144+
type: "problem",
145+
},
190146
};
191-
192-
export default {
147+
module.exports = {
193148
rules: {
194149
"no-unsafe-spawn": rule,
195150
},

0 commit comments

Comments
 (0)