prodlint 0.7.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +297 -253
- package/action.yml +152 -152
- package/dist/cli.js +325 -1
- package/dist/index.d.ts +44 -1
- package/dist/index.js +243 -1
- package/dist/mcp.js +300 -3
- package/package.json +90 -83
package/action.yml
CHANGED
|
@@ -1,152 +1,152 @@
|
|
|
1
|
-
name: 'Prodlint'
|
|
2
|
-
description: '
|
|
3
|
-
branding:
|
|
4
|
-
icon: 'shield'
|
|
5
|
-
color: 'green'
|
|
6
|
-
|
|
7
|
-
inputs:
|
|
8
|
-
path:
|
|
9
|
-
description: 'Path to scan (default: current directory)'
|
|
10
|
-
required: false
|
|
11
|
-
default: '.'
|
|
12
|
-
threshold:
|
|
13
|
-
description: 'Minimum score to pass (0-100). Fails the check if score is below this.'
|
|
14
|
-
required: false
|
|
15
|
-
default: '0'
|
|
16
|
-
ignore:
|
|
17
|
-
description: 'Glob patterns to ignore (comma-separated)'
|
|
18
|
-
required: false
|
|
19
|
-
default: ''
|
|
20
|
-
comment:
|
|
21
|
-
description: 'Post a PR comment with results (true/false)'
|
|
22
|
-
required: false
|
|
23
|
-
default: 'true'
|
|
24
|
-
|
|
25
|
-
outputs:
|
|
26
|
-
score:
|
|
27
|
-
description: 'Overall prodlint score (0-100)'
|
|
28
|
-
value: ${{ steps.scan.outputs.score }}
|
|
29
|
-
critical:
|
|
30
|
-
description: 'Number of critical findings'
|
|
31
|
-
value: ${{ steps.scan.outputs.critical }}
|
|
32
|
-
|
|
33
|
-
runs:
|
|
34
|
-
using: 'composite'
|
|
35
|
-
steps:
|
|
36
|
-
- name: Setup Node.js
|
|
37
|
-
uses: actions/setup-node@v4
|
|
38
|
-
with:
|
|
39
|
-
node-version: '20'
|
|
40
|
-
|
|
41
|
-
- name: Run prodlint
|
|
42
|
-
id: scan
|
|
43
|
-
shell: bash
|
|
44
|
-
env:
|
|
45
|
-
INPUT_PATH: ${{ inputs.path }}
|
|
46
|
-
INPUT_IGNORE: ${{ inputs.ignore }}
|
|
47
|
-
run: |
|
|
48
|
-
CMD_ARGS=("$INPUT_PATH" "--json")
|
|
49
|
-
if [ -n "$INPUT_IGNORE" ]; then
|
|
50
|
-
IFS=',' read -ra PATTERNS <<< "$INPUT_IGNORE"
|
|
51
|
-
for p in "${PATTERNS[@]}"; do
|
|
52
|
-
trimmed=$(echo "$p" | xargs)
|
|
53
|
-
CMD_ARGS+=("--ignore" "$trimmed")
|
|
54
|
-
done
|
|
55
|
-
fi
|
|
56
|
-
|
|
57
|
-
OUTPUT=$(npx -y prodlint@latest "${CMD_ARGS[@]}" 2>&1) || true
|
|
58
|
-
|
|
59
|
-
SCORE=$(echo "$OUTPUT" | node -e "
|
|
60
|
-
const d = JSON.parse(require('fs').readFileSync(0, 'utf8'));
|
|
61
|
-
console.log(Number(d.overallScore) || 0);
|
|
62
|
-
" 2>/dev/null || echo "0")
|
|
63
|
-
SCORE=$(echo "$SCORE" | tr -cd '0-9')
|
|
64
|
-
|
|
65
|
-
CRITICAL=$(echo "$OUTPUT" | node -e "
|
|
66
|
-
const d = JSON.parse(require('fs').readFileSync(0, 'utf8'));
|
|
67
|
-
console.log(Number(d.summary.critical) || 0);
|
|
68
|
-
" 2>/dev/null || echo "0")
|
|
69
|
-
CRITICAL=$(echo "$CRITICAL" | tr -cd '0-9')
|
|
70
|
-
|
|
71
|
-
echo "score=${SCORE:-0}" >> "$GITHUB_OUTPUT"
|
|
72
|
-
echo "critical=${CRITICAL:-0}" >> "$GITHUB_OUTPUT"
|
|
73
|
-
echo "$OUTPUT" > /tmp/prodlint-result.json
|
|
74
|
-
|
|
75
|
-
- name: Generate comment body
|
|
76
|
-
if: inputs.comment == 'true' && github.event_name == 'pull_request'
|
|
77
|
-
id: comment
|
|
78
|
-
shell: bash
|
|
79
|
-
env:
|
|
80
|
-
SCAN_SCORE: ${{ steps.scan.outputs.score }}
|
|
81
|
-
INPUT_THRESHOLD: ${{ inputs.threshold }}
|
|
82
|
-
run: |
|
|
83
|
-
SCORE="$SCAN_SCORE"
|
|
84
|
-
THRESHOLD="$INPUT_THRESHOLD"
|
|
85
|
-
|
|
86
|
-
if [ "$SCORE" -ge 80 ]; then
|
|
87
|
-
EMOJI="✅"
|
|
88
|
-
COLOR="brightgreen"
|
|
89
|
-
elif [ "$SCORE" -ge 60 ]; then
|
|
90
|
-
EMOJI="⚠️"
|
|
91
|
-
COLOR="yellow"
|
|
92
|
-
else
|
|
93
|
-
EMOJI="🚨"
|
|
94
|
-
COLOR="red"
|
|
95
|
-
fi
|
|
96
|
-
|
|
97
|
-
# Build comment from JSON with markdown sanitization
|
|
98
|
-
TMPFILE=$(mktemp "${RUNNER_TEMP:-/tmp}/prodlint-comment-XXXXXX.md")
|
|
99
|
-
node -e "
|
|
100
|
-
const fs = require('fs');
|
|
101
|
-
const d = JSON.parse(fs.readFileSync('/tmp/prodlint-result.json', 'utf8'));
|
|
102
|
-
const esc = s => String(s).replace(/[[\]()\\*_\`<>]/g, c => '\\\\' + c);
|
|
103
|
-
const lines = [];
|
|
104
|
-
lines.push('## ' + '$EMOJI' + '
|
|
105
|
-
lines.push('');
|
|
106
|
-
lines.push('| Category | Score | Issues |');
|
|
107
|
-
lines.push('|----------|-------|--------|');
|
|
108
|
-
for (const c of d.categoryScores) {
|
|
109
|
-
const icon = c.score >= 80 ? '🟢' : c.score >= 60 ? '🟡' : '🔴';
|
|
110
|
-
lines.push('| ' + icon + ' ' + esc(c.category) + ' | ' + c.score + '/100 | ' + c.findingCount + ' |');
|
|
111
|
-
}
|
|
112
|
-
if (d.summary.critical > 0) {
|
|
113
|
-
lines.push('');
|
|
114
|
-
lines.push('### Critical Issues');
|
|
115
|
-
lines.push('');
|
|
116
|
-
const crits = d.findings.filter(f => f.severity === 'critical');
|
|
117
|
-
for (const f of crits.slice(0, 10)) {
|
|
118
|
-
lines.push('- \`' + esc(f.ruleId) + '\` \`' + esc(f.file) + ':' + f.line + '\` — ' + esc(f.message));
|
|
119
|
-
}
|
|
120
|
-
if (crits.length > 10) {
|
|
121
|
-
lines.push('- ...and ' + (crits.length - 10) + ' more');
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
lines.push('');
|
|
125
|
-
lines.push('---');
|
|
126
|
-
lines.push('*Scanned ' + d.filesScanned + ' files in ' + d.scanDurationMs + 'ms · [prodlint](https://prodlint.com)*');
|
|
127
|
-
fs.writeFileSync('$TMPFILE', lines.join('\n'));
|
|
128
|
-
"
|
|
129
|
-
|
|
130
|
-
# Use the generated file for the comment step
|
|
131
|
-
cp "$TMPFILE" /tmp/prodlint-comment.md
|
|
132
|
-
|
|
133
|
-
- name: Post PR comment
|
|
134
|
-
if: inputs.comment == 'true' && github.event_name == 'pull_request'
|
|
135
|
-
uses: marocchino/sticky-pull-request-comment@v2
|
|
136
|
-
with:
|
|
137
|
-
header: prodlint
|
|
138
|
-
path: /tmp/prodlint-comment.md
|
|
139
|
-
|
|
140
|
-
- name: Check threshold
|
|
141
|
-
if: inputs.threshold != '0'
|
|
142
|
-
shell: bash
|
|
143
|
-
env:
|
|
144
|
-
SCAN_SCORE: ${{ steps.scan.outputs.score }}
|
|
145
|
-
INPUT_THRESHOLD: ${{ inputs.threshold }}
|
|
146
|
-
run: |
|
|
147
|
-
SCORE="$SCAN_SCORE"
|
|
148
|
-
THRESHOLD="$INPUT_THRESHOLD"
|
|
149
|
-
if [ "$SCORE" -lt "$THRESHOLD" ]; then
|
|
150
|
-
echo "::error::Prodlint score ($SCORE) is below threshold ($THRESHOLD)"
|
|
151
|
-
exit 1
|
|
152
|
-
fi
|
|
1
|
+
name: 'Prodlint'
|
|
2
|
+
description: 'Production readiness for vibe-coded apps — check your AI code before you ship'
|
|
3
|
+
branding:
|
|
4
|
+
icon: 'shield'
|
|
5
|
+
color: 'green'
|
|
6
|
+
|
|
7
|
+
inputs:
|
|
8
|
+
path:
|
|
9
|
+
description: 'Path to scan (default: current directory)'
|
|
10
|
+
required: false
|
|
11
|
+
default: '.'
|
|
12
|
+
threshold:
|
|
13
|
+
description: 'Minimum score to pass (0-100). Fails the check if score is below this.'
|
|
14
|
+
required: false
|
|
15
|
+
default: '0'
|
|
16
|
+
ignore:
|
|
17
|
+
description: 'Glob patterns to ignore (comma-separated)'
|
|
18
|
+
required: false
|
|
19
|
+
default: ''
|
|
20
|
+
comment:
|
|
21
|
+
description: 'Post a PR comment with results (true/false)'
|
|
22
|
+
required: false
|
|
23
|
+
default: 'true'
|
|
24
|
+
|
|
25
|
+
outputs:
|
|
26
|
+
score:
|
|
27
|
+
description: 'Overall prodlint score (0-100)'
|
|
28
|
+
value: ${{ steps.scan.outputs.score }}
|
|
29
|
+
critical:
|
|
30
|
+
description: 'Number of critical findings'
|
|
31
|
+
value: ${{ steps.scan.outputs.critical }}
|
|
32
|
+
|
|
33
|
+
runs:
|
|
34
|
+
using: 'composite'
|
|
35
|
+
steps:
|
|
36
|
+
- name: Setup Node.js
|
|
37
|
+
uses: actions/setup-node@v4
|
|
38
|
+
with:
|
|
39
|
+
node-version: '20'
|
|
40
|
+
|
|
41
|
+
- name: Run prodlint
|
|
42
|
+
id: scan
|
|
43
|
+
shell: bash
|
|
44
|
+
env:
|
|
45
|
+
INPUT_PATH: ${{ inputs.path }}
|
|
46
|
+
INPUT_IGNORE: ${{ inputs.ignore }}
|
|
47
|
+
run: |
|
|
48
|
+
CMD_ARGS=("$INPUT_PATH" "--json")
|
|
49
|
+
if [ -n "$INPUT_IGNORE" ]; then
|
|
50
|
+
IFS=',' read -ra PATTERNS <<< "$INPUT_IGNORE"
|
|
51
|
+
for p in "${PATTERNS[@]}"; do
|
|
52
|
+
trimmed=$(echo "$p" | xargs)
|
|
53
|
+
CMD_ARGS+=("--ignore" "$trimmed")
|
|
54
|
+
done
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
OUTPUT=$(npx -y prodlint@latest "${CMD_ARGS[@]}" 2>&1) || true
|
|
58
|
+
|
|
59
|
+
SCORE=$(echo "$OUTPUT" | node -e "
|
|
60
|
+
const d = JSON.parse(require('fs').readFileSync(0, 'utf8'));
|
|
61
|
+
console.log(Number(d.overallScore) || 0);
|
|
62
|
+
" 2>/dev/null || echo "0")
|
|
63
|
+
SCORE=$(echo "$SCORE" | tr -cd '0-9')
|
|
64
|
+
|
|
65
|
+
CRITICAL=$(echo "$OUTPUT" | node -e "
|
|
66
|
+
const d = JSON.parse(require('fs').readFileSync(0, 'utf8'));
|
|
67
|
+
console.log(Number(d.summary.critical) || 0);
|
|
68
|
+
" 2>/dev/null || echo "0")
|
|
69
|
+
CRITICAL=$(echo "$CRITICAL" | tr -cd '0-9')
|
|
70
|
+
|
|
71
|
+
echo "score=${SCORE:-0}" >> "$GITHUB_OUTPUT"
|
|
72
|
+
echo "critical=${CRITICAL:-0}" >> "$GITHUB_OUTPUT"
|
|
73
|
+
echo "$OUTPUT" > /tmp/prodlint-result.json
|
|
74
|
+
|
|
75
|
+
- name: Generate comment body
|
|
76
|
+
if: inputs.comment == 'true' && github.event_name == 'pull_request'
|
|
77
|
+
id: comment
|
|
78
|
+
shell: bash
|
|
79
|
+
env:
|
|
80
|
+
SCAN_SCORE: ${{ steps.scan.outputs.score }}
|
|
81
|
+
INPUT_THRESHOLD: ${{ inputs.threshold }}
|
|
82
|
+
run: |
|
|
83
|
+
SCORE="$SCAN_SCORE"
|
|
84
|
+
THRESHOLD="$INPUT_THRESHOLD"
|
|
85
|
+
|
|
86
|
+
if [ "$SCORE" -ge 80 ]; then
|
|
87
|
+
EMOJI="✅"
|
|
88
|
+
COLOR="brightgreen"
|
|
89
|
+
elif [ "$SCORE" -ge 60 ]; then
|
|
90
|
+
EMOJI="⚠️"
|
|
91
|
+
COLOR="yellow"
|
|
92
|
+
else
|
|
93
|
+
EMOJI="🚨"
|
|
94
|
+
COLOR="red"
|
|
95
|
+
fi
|
|
96
|
+
|
|
97
|
+
# Build comment from JSON with markdown sanitization
|
|
98
|
+
TMPFILE=$(mktemp "${RUNNER_TEMP:-/tmp}/prodlint-comment-XXXXXX.md")
|
|
99
|
+
node -e "
|
|
100
|
+
const fs = require('fs');
|
|
101
|
+
const d = JSON.parse(fs.readFileSync('/tmp/prodlint-result.json', 'utf8'));
|
|
102
|
+
const esc = s => String(s).replace(/[[\]()\\*_\`<>]/g, c => '\\\\' + c);
|
|
103
|
+
const lines = [];
|
|
104
|
+
lines.push('## ' + '$EMOJI' + ' Production Readiness: **' + d.overallScore + '/100**');
|
|
105
|
+
lines.push('');
|
|
106
|
+
lines.push('| Category | Score | Issues |');
|
|
107
|
+
lines.push('|----------|-------|--------|');
|
|
108
|
+
for (const c of d.categoryScores) {
|
|
109
|
+
const icon = c.score >= 80 ? '🟢' : c.score >= 60 ? '🟡' : '🔴';
|
|
110
|
+
lines.push('| ' + icon + ' ' + esc(c.category) + ' | ' + c.score + '/100 | ' + c.findingCount + ' |');
|
|
111
|
+
}
|
|
112
|
+
if (d.summary.critical > 0) {
|
|
113
|
+
lines.push('');
|
|
114
|
+
lines.push('### Critical Issues');
|
|
115
|
+
lines.push('');
|
|
116
|
+
const crits = d.findings.filter(f => f.severity === 'critical');
|
|
117
|
+
for (const f of crits.slice(0, 10)) {
|
|
118
|
+
lines.push('- \`' + esc(f.ruleId) + '\` \`' + esc(f.file) + ':' + f.line + '\` — ' + esc(f.message));
|
|
119
|
+
}
|
|
120
|
+
if (crits.length > 10) {
|
|
121
|
+
lines.push('- ...and ' + (crits.length - 10) + ' more');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
lines.push('');
|
|
125
|
+
lines.push('---');
|
|
126
|
+
lines.push('*Scanned ' + d.filesScanned + ' files in ' + d.scanDurationMs + 'ms · [prodlint](https://prodlint.com)*');
|
|
127
|
+
fs.writeFileSync('$TMPFILE', lines.join('\n'));
|
|
128
|
+
"
|
|
129
|
+
|
|
130
|
+
# Use the generated file for the comment step
|
|
131
|
+
cp "$TMPFILE" /tmp/prodlint-comment.md
|
|
132
|
+
|
|
133
|
+
- name: Post PR comment
|
|
134
|
+
if: inputs.comment == 'true' && github.event_name == 'pull_request'
|
|
135
|
+
uses: marocchino/sticky-pull-request-comment@v2
|
|
136
|
+
with:
|
|
137
|
+
header: prodlint
|
|
138
|
+
path: /tmp/prodlint-comment.md
|
|
139
|
+
|
|
140
|
+
- name: Check threshold
|
|
141
|
+
if: inputs.threshold != '0'
|
|
142
|
+
shell: bash
|
|
143
|
+
env:
|
|
144
|
+
SCAN_SCORE: ${{ steps.scan.outputs.score }}
|
|
145
|
+
INPUT_THRESHOLD: ${{ inputs.threshold }}
|
|
146
|
+
run: |
|
|
147
|
+
SCORE="$SCAN_SCORE"
|
|
148
|
+
THRESHOLD="$INPUT_THRESHOLD"
|
|
149
|
+
if [ "$SCORE" -lt "$THRESHOLD" ]; then
|
|
150
|
+
echo "::error::Prodlint score ($SCORE) is below threshold ($THRESHOLD)"
|
|
151
|
+
exit 1
|
|
152
|
+
fi
|