PR Lint and Format Check workflow (Wei-Shaw/claude-relay-service)
The PR Lint and Format Check workflow from Wei-Shaw/claude-relay-service, explained and optimized by Latchkey.
CI health: C - fair
Point runs-on at Latchkey and get run de-duplication, job timeouts, SHA-pinned actions, self-healing for flaky steps, and up to 58% lower cost, applied automatically.
What it does
This is the PR Lint and Format Check workflow from the Wei-Shaw/claude-relay-service repository, a real project running GitHub Actions. It is shown here with attribution under its MIT license.
Below, Latchkey shows a faster, safer version produced by its optimization engine.
The workflow
name: PR Lint and Format Check
on:
pull_request:
types: [opened, synchronize, reopened]
paths:
- '**.js'
- '**.jsx'
- '**.ts'
- '**.tsx'
- '**.vue'
- '**.json'
- '**.cjs'
- '**.mjs'
- '.prettierrc'
- '.eslintrc.cjs'
- 'package.json'
- 'web/admin-spa/**'
permissions:
contents: read
pull-requests: write
issues: write
jobs:
lint-and-format:
runs-on: ubuntu-latest
name: Check Code Quality
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
- name: Cache dependencies
uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Install dependencies
run: |
npm ci --prefer-offline --no-audit
# 安装 web 目录的依赖(如果存在)
if [ -d "web/admin-spa" ] && [ -f "web/admin-spa/package.json" ]; then
cd web/admin-spa
npm ci --prefer-offline --no-audit
cd ../..
fi
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v41
with:
files: |
**/*.js
**/*.jsx
**/*.ts
**/*.tsx
**/*.vue
**/*.cjs
**/*.mjs
**/*.json
files_ignore: |
node_modules/**
dist/**
build/**
coverage/**
.git/**
logs/**
temp/**
tmp/**
- name: Check Prettier formatting
if: steps.changed-files.outputs.any_changed == 'true'
id: prettier-check
run: |
echo "🔍 Checking Prettier formatting for changed files..."
echo "Changed files: ${{ steps.changed-files.outputs.all_changed_files }}"
# 初始化标志
PRETTIER_FAILED=false
PRETTIER_OUTPUT=""
# 检查每个改变的文件
for file in ${{ steps.changed-files.outputs.all_changed_files }}; do
if [ -f "$file" ]; then
echo "Checking: $file"
# 根据文件位置选择正确的 prettier 配置
if [[ "$file" == web/admin-spa/* ]]; then
# 前端文件:进入前端目录运行 prettier
cd web/admin-spa
RELATIVE_FILE="${file#web/admin-spa/}"
if ! npx prettier --check "$RELATIVE_FILE" 2>&1; then
PRETTIER_FAILED=true
DIFF=$(npx prettier "$RELATIVE_FILE" | diff -u "$RELATIVE_FILE" - || true)
if [ -n "$DIFF" ]; then
PRETTIER_OUTPUT="${PRETTIER_OUTPUT}❌ File needs formatting: $file\n"
PRETTIER_OUTPUT="${PRETTIER_OUTPUT}\`\`\`diff\n${DIFF}\n\`\`\`\n\n"
fi
else
echo "✅ $file is properly formatted"
fi
cd ../..
else
# 后端文件:使用根目录的 prettier
if ! npx prettier --check "$file" 2>&1; then
PRETTIER_FAILED=true
DIFF=$(npx prettier "$file" | diff -u "$file" - || true)
if [ -n "$DIFF" ]; then
PRETTIER_OUTPUT="${PRETTIER_OUTPUT}❌ File needs formatting: $file\n"
PRETTIER_OUTPUT="${PRETTIER_OUTPUT}\`\`\`diff\n${DIFF}\n\`\`\`\n\n"
fi
else
echo "✅ $file is properly formatted"
fi
fi
fi
done
# 输出结果
if [ "$PRETTIER_FAILED" = true ]; then
echo "prettier_failed=true" >> $GITHUB_OUTPUT
echo -e "$PRETTIER_OUTPUT" > prettier-report.md
echo "❌ Some files are not properly formatted."
echo "Please run: npm run format (backend) or cd web/admin-spa && npm run format (frontend)"
exit 1
else
echo "prettier_failed=false" >> $GITHUB_OUTPUT
echo "✅ All files are properly formatted"
fi
- name: Run ESLint
if: steps.changed-files.outputs.any_changed == 'true'
id: eslint-check
run: |
echo "🔍 Running ESLint on changed files..."
# 分离前端和后端文件
BACKEND_FILES=""
FRONTEND_FILES=""
for file in ${{ steps.changed-files.outputs.all_changed_files }}; do
if [[ "$file" =~ \.(js|jsx|vue|cjs|mjs)$ ]] && [ -f "$file" ]; then
if [[ "$file" == web/admin-spa/* ]]; then
FRONTEND_FILES="$FRONTEND_FILES ${file#web/admin-spa/}"
else
BACKEND_FILES="$BACKEND_FILES $file"
fi
fi
done
ESLINT_FAILED=false
ESLINT_OUTPUT=""
# 检查后端文件
if [ -n "$BACKEND_FILES" ]; then
echo "Linting backend files: $BACKEND_FILES"
set +e
BACKEND_OUTPUT=$(npx eslint $BACKEND_FILES --format stylish 2>&1)
BACKEND_EXIT_CODE=$?
set -e
if [ $BACKEND_EXIT_CODE -ne 0 ]; then
ESLINT_FAILED=true
ESLINT_OUTPUT="${ESLINT_OUTPUT}### Backend ESLint Issues\n\`\`\`\n${BACKEND_OUTPUT}\n\`\`\`\n\n"
fi
fi
# 检查前端文件
if [ -n "$FRONTEND_FILES" ]; then
echo "Linting frontend files: $FRONTEND_FILES"
cd web/admin-spa
set +e
FRONTEND_OUTPUT=$(npx eslint $FRONTEND_FILES --format stylish 2>&1)
FRONTEND_EXIT_CODE=$?
set -e
cd ../..
if [ $FRONTEND_EXIT_CODE -ne 0 ]; then
ESLINT_FAILED=true
ESLINT_OUTPUT="${ESLINT_OUTPUT}### Frontend ESLint Issues\n\`\`\`\n${FRONTEND_OUTPUT}\n\`\`\`\n\n"
fi
fi
# 输出结果
if [ "$ESLINT_FAILED" = true ]; then
echo "eslint_failed=true" >> $GITHUB_OUTPUT
echo "❌ ESLint found issues"
# 创建错误报告
echo "## ESLint Report" > eslint-report.md
echo "$ESLINT_OUTPUT" >> eslint-report.md
echo "" >> eslint-report.md
echo "Please fix these issues by running:" >> eslint-report.md
echo '```bash' >> eslint-report.md
echo "# Backend: npm run lint" >> eslint-report.md
echo "# Frontend: cd web/admin-spa && npm run lint" >> eslint-report.md
echo '```' >> eslint-report.md
exit 1
else
echo "eslint_failed=false" >> $GITHUB_OUTPUT
echo "✅ ESLint check passed"
fi
- name: Debug PR Context
if: failure()
run: |
echo "PR Number: ${{ github.event.pull_request.number }}"
echo "Repo: ${{ github.repository }}"
echo "Event Name: ${{ github.event_name }}"
echo "Actor: ${{ github.actor }}"
- name: Comment PR with results
if: failure()
continue-on-error: true # 即使评论失败也继续
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GH_PAT || secrets.GITHUB_TOKEN }}
script: |
const fs = require('fs');
let comment = '## 🚨 Code Quality Check Failed\n\n';
// 读取 Prettier 报告
if (fs.existsSync('prettier-report.md')) {
const prettierReport = fs.readFileSync('prettier-report.md', 'utf8');
comment += '### Prettier Formatting Issues\n\n';
comment += prettierReport;
comment += '\n**Fix command:**\n```bash\nnpm run format\n```\n\n';
}
// 读取 ESLint 报告
if (fs.existsSync('eslint-report.md')) {
const eslintReport = fs.readFileSync('eslint-report.md', 'utf8');
comment += '### ESLint Issues\n\n';
comment += eslintReport;
}
comment += '\n---\n';
comment += '💡 **提示**: 在本地运行以下命令来自动修复大部分问题:\n';
comment += '```bash\n';
comment += '# 后端代码\n';
comment += 'npm run format # 修复后端 Prettier 格式问题\n';
comment += 'npm run lint # 修复后端 ESLint 问题\n';
comment += '\n';
comment += '# 前端代码\n';
comment += 'cd web/admin-spa\n';
comment += 'npm run format # 修复前端 Prettier 格式问题\n';
comment += 'npm run lint # 修复前端 ESLint 问题\n';
comment += '```\n';
// 查找是否已有机器人评论
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
const botComment = comments.find(comment =>
comment.user.type === 'Bot' &&
comment.body.includes('Code Quality Check Failed')
);
if (botComment) {
// 更新现有评论
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
body: comment
});
} else {
// 创建新评论
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: comment
});
}
- name: Success comment
if: success() && steps.changed-files.outputs.any_changed == 'true'
continue-on-error: true # 即使评论失败也继续
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GH_PAT || secrets.GITHUB_TOKEN }}
script: |
// 查找是否已有失败的评论
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
const botComment = comments.find(comment =>
comment.user.type === 'Bot' &&
comment.body.includes('Code Quality Check Failed')
);
if (botComment) {
// 如果之前有失败评论,更新为成功
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
body: '## ✅ Code Quality Check Passed\n\nAll files are properly formatted and pass linting checks!'
});
}
The same workflow, on Latchkey
Removes redundant runs and caps runaway jobs. Added and changed lines are highlighted.
name: PR Lint and Format Check on: pull_request: types: [opened, synchronize, reopened] paths: - '**.js' - '**.jsx' - '**.ts' - '**.tsx' - '**.vue' - '**.json' - '**.cjs' - '**.mjs' - '.prettierrc' - '.eslintrc.cjs' - 'package.json' - 'web/admin-spa/**' permissions: contents: read pull-requests: write issues: write concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: lint-and-format: timeout-minutes: 30 runs-on: latchkey-small name: Check Code Quality steps: - name: Checkout code uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '18' cache: 'npm' - name: Cache dependencies uses: actions/cache@v3 with: path: ~/.npm key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} restore-keys: | ${{ runner.os }}-node- - name: Install dependencies run: | npm ci --prefer-offline --no-audit # 安装 web 目录的依赖(如果存在) if [ -d "web/admin-spa" ] && [ -f "web/admin-spa/package.json" ]; then cd web/admin-spa npm ci --prefer-offline --no-audit cd ../.. fi - name: Get changed files id: changed-files uses: tj-actions/changed-files@v41 with: files: | **/*.js **/*.jsx **/*.ts **/*.tsx **/*.vue **/*.cjs **/*.mjs **/*.json files_ignore: | node_modules/** dist/** build/** coverage/** .git/** logs/** temp/** tmp/** - name: Check Prettier formatting if: steps.changed-files.outputs.any_changed == 'true' id: prettier-check run: | echo "🔍 Checking Prettier formatting for changed files..." echo "Changed files: ${{ steps.changed-files.outputs.all_changed_files }}" # 初始化标志 PRETTIER_FAILED=false PRETTIER_OUTPUT="" # 检查每个改变的文件 for file in ${{ steps.changed-files.outputs.all_changed_files }}; do if [ -f "$file" ]; then echo "Checking: $file" # 根据文件位置选择正确的 prettier 配置 if [[ "$file" == web/admin-spa/* ]]; then # 前端文件:进入前端目录运行 prettier cd web/admin-spa RELATIVE_FILE="${file#web/admin-spa/}" if ! npx prettier --check "$RELATIVE_FILE" 2>&1; then PRETTIER_FAILED=true DIFF=$(npx prettier "$RELATIVE_FILE" | diff -u "$RELATIVE_FILE" - || true) if [ -n "$DIFF" ]; then PRETTIER_OUTPUT="${PRETTIER_OUTPUT}❌ File needs formatting: $file\n" PRETTIER_OUTPUT="${PRETTIER_OUTPUT}\`\`\`diff\n${DIFF}\n\`\`\`\n\n" fi else echo "✅ $file is properly formatted" fi cd ../.. else # 后端文件:使用根目录的 prettier if ! npx prettier --check "$file" 2>&1; then PRETTIER_FAILED=true DIFF=$(npx prettier "$file" | diff -u "$file" - || true) if [ -n "$DIFF" ]; then PRETTIER_OUTPUT="${PRETTIER_OUTPUT}❌ File needs formatting: $file\n" PRETTIER_OUTPUT="${PRETTIER_OUTPUT}\`\`\`diff\n${DIFF}\n\`\`\`\n\n" fi else echo "✅ $file is properly formatted" fi fi fi done # 输出结果 if [ "$PRETTIER_FAILED" = true ]; then echo "prettier_failed=true" >> $GITHUB_OUTPUT echo -e "$PRETTIER_OUTPUT" > prettier-report.md echo "❌ Some files are not properly formatted." echo "Please run: npm run format (backend) or cd web/admin-spa && npm run format (frontend)" exit 1 else echo "prettier_failed=false" >> $GITHUB_OUTPUT echo "✅ All files are properly formatted" fi - name: Run ESLint if: steps.changed-files.outputs.any_changed == 'true' id: eslint-check run: | echo "🔍 Running ESLint on changed files..." # 分离前端和后端文件 BACKEND_FILES="" FRONTEND_FILES="" for file in ${{ steps.changed-files.outputs.all_changed_files }}; do if [[ "$file" =~ \.(js|jsx|vue|cjs|mjs)$ ]] && [ -f "$file" ]; then if [[ "$file" == web/admin-spa/* ]]; then FRONTEND_FILES="$FRONTEND_FILES ${file#web/admin-spa/}" else BACKEND_FILES="$BACKEND_FILES $file" fi fi done ESLINT_FAILED=false ESLINT_OUTPUT="" # 检查后端文件 if [ -n "$BACKEND_FILES" ]; then echo "Linting backend files: $BACKEND_FILES" set +e BACKEND_OUTPUT=$(npx eslint $BACKEND_FILES --format stylish 2>&1) BACKEND_EXIT_CODE=$? set -e if [ $BACKEND_EXIT_CODE -ne 0 ]; then ESLINT_FAILED=true ESLINT_OUTPUT="${ESLINT_OUTPUT}### Backend ESLint Issues\n\`\`\`\n${BACKEND_OUTPUT}\n\`\`\`\n\n" fi fi # 检查前端文件 if [ -n "$FRONTEND_FILES" ]; then echo "Linting frontend files: $FRONTEND_FILES" cd web/admin-spa set +e FRONTEND_OUTPUT=$(npx eslint $FRONTEND_FILES --format stylish 2>&1) FRONTEND_EXIT_CODE=$? set -e cd ../.. if [ $FRONTEND_EXIT_CODE -ne 0 ]; then ESLINT_FAILED=true ESLINT_OUTPUT="${ESLINT_OUTPUT}### Frontend ESLint Issues\n\`\`\`\n${FRONTEND_OUTPUT}\n\`\`\`\n\n" fi fi # 输出结果 if [ "$ESLINT_FAILED" = true ]; then echo "eslint_failed=true" >> $GITHUB_OUTPUT echo "❌ ESLint found issues" # 创建错误报告 echo "## ESLint Report" > eslint-report.md echo "$ESLINT_OUTPUT" >> eslint-report.md echo "" >> eslint-report.md echo "Please fix these issues by running:" >> eslint-report.md echo '```bash' >> eslint-report.md echo "# Backend: npm run lint" >> eslint-report.md echo "# Frontend: cd web/admin-spa && npm run lint" >> eslint-report.md echo '```' >> eslint-report.md exit 1 else echo "eslint_failed=false" >> $GITHUB_OUTPUT echo "✅ ESLint check passed" fi - name: Debug PR Context if: failure() run: | echo "PR Number: ${{ github.event.pull_request.number }}" echo "Repo: ${{ github.repository }}" echo "Event Name: ${{ github.event_name }}" echo "Actor: ${{ github.actor }}" - name: Comment PR with results if: failure() continue-on-error: true # 即使评论失败也继续 uses: actions/github-script@v7 with: github-token: ${{ secrets.GH_PAT || secrets.GITHUB_TOKEN }} script: | const fs = require('fs'); let comment = '## 🚨 Code Quality Check Failed\n\n'; // 读取 Prettier 报告 if (fs.existsSync('prettier-report.md')) { const prettierReport = fs.readFileSync('prettier-report.md', 'utf8'); comment += '### Prettier Formatting Issues\n\n'; comment += prettierReport; comment += '\n**Fix command:**\n```bash\nnpm run format\n```\n\n'; } // 读取 ESLint 报告 if (fs.existsSync('eslint-report.md')) { const eslintReport = fs.readFileSync('eslint-report.md', 'utf8'); comment += '### ESLint Issues\n\n'; comment += eslintReport; } comment += '\n---\n'; comment += '💡 **提示**: 在本地运行以下命令来自动修复大部分问题:\n'; comment += '```bash\n'; comment += '# 后端代码\n'; comment += 'npm run format # 修复后端 Prettier 格式问题\n'; comment += 'npm run lint # 修复后端 ESLint 问题\n'; comment += '\n'; comment += '# 前端代码\n'; comment += 'cd web/admin-spa\n'; comment += 'npm run format # 修复前端 Prettier 格式问题\n'; comment += 'npm run lint # 修复前端 ESLint 问题\n'; comment += '```\n'; // 查找是否已有机器人评论 const { data: comments } = await github.rest.issues.listComments({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, }); const botComment = comments.find(comment => comment.user.type === 'Bot' && comment.body.includes('Code Quality Check Failed') ); if (botComment) { // 更新现有评论 await github.rest.issues.updateComment({ owner: context.repo.owner, repo: context.repo.repo, comment_id: botComment.id, body: comment }); } else { // 创建新评论 await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, body: comment }); } - name: Success comment if: success() && steps.changed-files.outputs.any_changed == 'true' continue-on-error: true # 即使评论失败也继续 uses: actions/github-script@v7 with: github-token: ${{ secrets.GH_PAT || secrets.GITHUB_TOKEN }} script: | // 查找是否已有失败的评论 const { data: comments } = await github.rest.issues.listComments({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, }); const botComment = comments.find(comment => comment.user.type === 'Bot' && comment.body.includes('Code Quality Check Failed') ); if (botComment) { // 如果之前有失败评论,更新为成功 await github.rest.issues.updateComment({ owner: context.repo.owner, repo: context.repo.repo, comment_id: botComment.id, body: '## ✅ Code Quality Check Passed\n\nAll files are properly formatted and pass linting checks!' }); }
What changed
- Run on Latchkey managed runners with one line (
runs-on), which apply the fixes below automatically and self-heal transient failures. This example useslatchkey-small; pick the runner size that fits the job. - Cancel superseded runs when a branch or PR gets a newer push.
- Add a job timeout so a hung step cannot burn hours of runner time.
1 third-party action is referenced by a movable tag. Pin it to the commit SHA (Latchkey resolves and applies this automatically) so a repointed tag cannot change what runs.
What Latchkey heals here
This workflow has steps that commonly fail on transient issues (network, registries, flaky browsers). On Latchkey managed runners they are detected, retried, and self-healed instead of failing your build:
- Dependency installs
This workflow runs 1 job per trigger. On Latchkey the same minutes cost up to 58% less than GitHub-hosted, with zero queue time.