Skip to content

Network Hub

Network Hub #119

Workflow file for this run

name: KubeJS Scripts Lint & Format Review
on:
pull_request:
paths:
- "kubejs/**/*.js"
- ".eslintrc*"
- "eslint.config*"
- ".prettierrc*"
- "package.json"
permissions:
contents: read
pull-requests: write
jobs:
lint-and-review:
name: Lint KubeJS Scripts
runs-on: ubuntu-latest
steps:
- name: Checkout PR branch
uses: actions/checkout@v4
with:
# Full history so we can diff against base
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Run ESLint
id: eslint
run: |
npx eslint . \
--format json \
--output-file eslint-report.json || true
- name: Run Typecheck
id: typecheck
run: |
npm run lint:typecheck || true
- name: Run Prettier (check mode + diff)
id: prettier
run: |
# Write a diff of what Prettier would change
npx prettier --check . 2>&1 | tee prettier-check.txt || true
# Generate per-file diffs for the review suggestions
mkdir -p prettier-diffs
mapfile -t changed_files < <(git diff --name-only "origin/${{ github.base_ref }}...HEAD" -- 'kubejs/**/*.js')
echo "Checking ${#changed_files[@]} changed file(s) for formatting..."
for file in "${changed_files[@]}"; do
[[ -f "$file" ]] || continue
echo " formatting check: $file"
formatted=$(node_modules/.bin/prettier "$file" 2>/dev/null)
original=$(cat "$file")
if [ "$formatted" != "$original" ]; then
# Save the formatted version - workflow script picks it up
safe=$(echo "$file" | tr '/' '_')
echo "$formatted" > "prettier-diffs/${safe}.formatted"
echo "$file" >> prettier-changed-files.txt
fi
done
- name: Post PR Review
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const owner = context.repo.owner;
const repo = context.repo.repo;
const pullNumber = context.payload.pull_request.number;
const headSha = context.payload.pull_request.head.sha;
function diffToSuggestions(originalLines, formattedLines, filePath) {
const suggestions = [];
let i = 0, j = 0;
while (i < originalLines.length || j < formattedLines.length) {
if (originalLines[i] === formattedLines[j]) {
i++; j++;
continue;
}
const hunkStart = i;
let oi = i + 1, fj = j + 1;
while (
(oi < originalLines.length || fj < formattedLines.length) &&
originalLines[oi] !== formattedLines[fj]
) { oi++; fj++; }
const hunkEnd = oi - 1;
const replacement = formattedLines.slice(j, fj).join('\n');
suggestions.push({
path: filePath,
line: hunkEnd + 1,
start_line: hunkStart + 1,
body: `\`\`\`suggestion\n${replacement}\n\`\`\``,
});
i = oi;
j = fj;
}
return suggestions;
}
const prettierComments = [];
let changedFiles = [];
try {
changedFiles = fs.readFileSync('prettier-changed-files.txt', 'utf8')
.split('\n').filter(Boolean);
} catch (_) {}
for (const filePath of changedFiles) {
const safe = filePath.replace(/\//g, '_');
const formattedPath = `prettier-diffs/${safe}.formatted`;
if (!fs.existsSync(formattedPath)) continue;
const original = fs.readFileSync(filePath, 'utf8').split('\n');
const formatted = fs.readFileSync(formattedPath, 'utf8').split('\n');
const hunks = diffToSuggestions(original, formatted, filePath);
for (const h of hunks.slice(0, 50)) {
prettierComments.push(h);
}
}
let eslintResults = [];
try {
eslintResults = JSON.parse(fs.readFileSync('eslint-report.json', 'utf8'));
} catch (_) {}
const eslintComments = [];
for (const result of eslintResults) {
const relPath = result.filePath.replace(process.cwd() + '/', '');
for (const msg of result.messages) {
const severity = msg.severity === 2 ? '🔴 **Error**' : '🟡 **Warning**';
const body = [
`${severity} \`${msg.ruleId ?? 'eslint'}\``,
msg.message,
msg.fix
? `\`\`\`suggestion\n${
// apply the fix inline when possible
// (single-line fixes only for suggestions)
(() => {
try {
const src = fs.readFileSync(relPath, 'utf8').split('\n');
const fixed = src[msg.line - 1]
.slice(0, msg.fix.range[0] - (src.slice(0, msg.line - 1).join('\n').length + 1))
+ msg.fix.text
+ src[msg.line - 1]
.slice(msg.fix.range[1] - (src.slice(0, msg.line - 1).join('\n').length + 1));
return fixed;
} catch (_) { return null; }
})()
}\n\`\`\`` : '',
].filter(Boolean).join('\n\n');
eslintComments.push({
path: relPath,
line: msg.endLine ?? msg.line,
start_line: msg.line !== (msg.endLine ?? msg.line) ? msg.line : undefined,
body,
});
}
}
const COMMENT_BODY_LIMIT = 300;
const INLINE_COMMENT_CAP = 20;
const allComments = [...prettierComments, ...eslintComments];
const cleanedComments = allComments
.filter(c => c.line > 0)
.map(({ start_line, line, body, ...rest }) => ({
...rest,
body: body.length > COMMENT_BODY_LIMIT
? body.slice(0, COMMENT_BODY_LIMIT) + '\n\n_…truncated_'
: body,
line,
...(start_line && start_line < line ? { start_line, start_side: 'RIGHT' } : {}),
side: 'RIGHT',
}));
const inlineComments = cleanedComments.slice(0, INLINE_COMMENT_CAP);
const omittedCount = cleanedComments.length - inlineComments.length;
const hasIssues = cleanedComments.length > 0;
let summaryBody = hasIssues
? [
'## KubeJS Scripts Lint & Format Review',
'',
`Found **${prettierComments.length}** formatting issue(s) and **${eslintComments.length}** lint issue(s).`,
'',
omittedCount > 0
? `> Showing first **${inlineComments.length}** inline comment(s) — **${omittedCount}** additional issue(s) not shown.`
: '',
'> Prettier suggestions can be committed directly from this review.',
'> Run `npm run format:fix && npm run lint:fix` locally to fix everything at once.',
].filter(l => l !== '').join('\n')
: [
'## ✅ KubeJS Scripts Lint & Format Review',
'',
'All scripts pass formatting and lint checks. Nice work!',
].join('\n');
if (hasIssues) {
try {
await github.rest.pulls.createReview({
owner,
repo,
pull_number: pullNumber,
commit_id: headSha,
body: summaryBody,
event: 'REQUEST_CHANGES',
comments: inlineComments,
});
console.log(`Review posted: REQUEST_CHANGES with ${inlineComments.length} inline comment(s) (${omittedCount} omitted).`);
} catch (error) {
if (error.status === 422 && error.response?.data?.message?.includes('Unprocessable Entity')) {
console.log('Inline comments failed to resolve, posting summary comment instead...');
const detailsBody = [
summaryBody,
'',
'<details><summary>Detailed Issues</summary>',
'',
'### ESLint Issues (' + eslintComments.length + ')',
eslintComments.length > 0
? eslintComments.map(c => `- **${c.path}:${c.line}** - ${c.body.split('\n')[0]}`).join('\n')
: 'None',
'',
'### Prettier Formatting Issues (' + prettierComments.length + ')',
prettierComments.length > 0
? prettierComments.map(c => `- **${c.path}:${c.line}**`).join('\n')
: 'None',
'',
'</details>',
].join('\n');
await github.rest.issues.createComment({
owner,
repo,
issue_number: pullNumber,
body: detailsBody,
});
console.log(`Comment posted with ${eslintComments.length} eslint and ${prettierComments.length} prettier issues.`);
} else {
throw error;
}
}
} else {
await github.rest.issues.createComment({
owner,
repo,
issue_number: pullNumber,
body: summaryBody,
});
console.log(`Comment posted: All checks passed.`);
}