🔨 chore: Add CI to Check console.log (#10333)

* lint: Clean breakpoints

* build: Add CI to check

* build: Add `next` branch

* build: Remove markdown files

* fix: CI hang out

* fix: Show warning on GitHub

* feat: Send comment

* fix: CI error

* fix: show file list
This commit is contained in:
René Wang
2025-11-21 14:18:10 +08:00
committed by GitHub
parent 4c7ebd5b39
commit c0542e80a3
7 changed files with 280 additions and 41 deletions

View File

@@ -0,0 +1,14 @@
{
"files": ["drizzle.config.ts"],
"patterns": [
"scripts/**",
"**/*.test.ts",
"**/*.test.tsx",
"**/*.spec.ts",
"**/*.spec.tsx",
"**/examples/**",
"e2e/**",
".github/scripts/**",
"apps/desktop/**"
]
}

117
.github/workflows/check-console-log.yml vendored Normal file
View File

@@ -0,0 +1,117 @@
name: Check Console Log (Warning)
on:
pull_request:
branches:
- main
- next
permissions:
contents: read
pull-requests: write
jobs:
check-console-log:
name: Check for console.log statements (non-blocking)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Install bun
uses: oven-sh/setup-bun@v2
- name: Run console.log check
id: check
run: |
OUTPUT=$(bunx tsx scripts/checkConsoleLog.mts 2>&1)
echo "$OUTPUT"
# Save output to file for later use
echo "$OUTPUT" > /tmp/console-log-output.txt
# Check if violations were found
if echo "$OUTPUT" | grep -q "Total violations:"; then
echo "has_violations=true" >> $GITHUB_OUTPUT
TOTAL=$(echo "$OUTPUT" | grep -oP "Total violations: \K\d+")
echo "total=$TOTAL" >> $GITHUB_OUTPUT
else
echo "has_violations=false" >> $GITHUB_OUTPUT
fi
- name: Comment on PR
if: steps.check.outputs.has_violations == 'true'
uses: actions/github-script@v7
env:
VIOLATION_COUNT: ${{ steps.check.outputs.total }}
with:
script: |
const fs = require('fs');
const output = fs.readFileSync('/tmp/console-log-output.txt', 'utf8');
const total = process.env.VIOLATION_COUNT || '0';
// Parse violations from output (format: " file:line" followed by " content")
const lines = output.split('\n');
const violations = [];
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// Match lines like " packages/database/src/client/db.ts:258"
const fileMatch = line.match(/^\s{2}(\S+:\d+)\s*$/);
if (fileMatch) {
const file = fileMatch[1];
const content = lines[i + 1]?.trim() || '';
violations.push({ file, content });
i++;
}
}
// Build comment body
const maxDisplay = 30;
let body = `## ⚠️ Console.log Check Warning\n\n`;
body += `Found **${total}** \`console.log\` statement(s) in this PR.\n\n`;
if (violations.length > 0) {
body += `<details>\n<summary>📋 Click to see violations (${Math.min(violations.length, maxDisplay)} of ${total} shown)</summary>\n\n`;
body += `| File | Code |\n|------|------|\n`;
violations.slice(0, maxDisplay).forEach(v => {
const escapedContent = v.content
.substring(0, 60)
.replace(/\|/g, '\\|')
.replace(/`/g, "'");
body += `| \`${v.file}\` | \`${escapedContent}${v.content.length > 60 ? '...' : ''}\` |\n`;
});
if (parseInt(total) > maxDisplay) {
body += `\n*...and ${parseInt(total) - maxDisplay} more violations*\n`;
}
body += `\n</details>\n\n`;
}
body += `> 💡 **Tip:** Remove \`console.log\` or add files to \`.console-log-whitelist.json\`\n`;
body += `> ✅ This check is **non-blocking** and won't prevent merging.`;
// Find existing comment to update instead of creating duplicates
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(c =>
c.user.type === 'Bot' && c.body.includes('Console.log Check Warning')
);
if (botComment) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
body,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body,
});
}

View File

@@ -63,6 +63,7 @@
"lint:circular": "npm run lint:circular:main && npm run lint:circular:packages",
"lint:circular:main": "dpdm src/**/*.ts --no-warning --no-tree --exit-code circular:1 --no-progress -T true --skip-dynamic-imports circular",
"lint:circular:packages": "dpdm packages/**/src/**/*.ts --no-warning --no-tree --exit-code circular:1 --no-progress -T true --skip-dynamic-imports circular",
"lint:console": "tsx scripts/checkConsoleLog.mts",
"lint:md": "remark . --silent --output",
"lint:mdx": "npm run workflow:mdx && remark \"docs/**/*.mdx\" -r ./.remarkrc.mdx.js --silent --output && eslint \"docs/**/*.mdx\" --quiet --fix",
"lint:style": "stylelint \"{src,tests}/**/*.{js,jsx,ts,tsx}\" --fix",

148
scripts/checkConsoleLog.mts Normal file
View File

@@ -0,0 +1,148 @@
#!/usr/bin/env tsx
import { execSync } from 'node:child_process';
import { readFileSync } from 'node:fs';
interface WhitelistConfig {
files?: string[];
patterns?: string[];
}
const WHITELIST_PATH = '.console-log-whitelist.json';
/**
* Load whitelist configuration
*/
const loadWhitelist = (): WhitelistConfig => {
try {
const content = readFileSync(WHITELIST_PATH, 'utf8');
return JSON.parse(content);
} catch {
return { files: [], patterns: [] };
}
};
/**
* Check if a file is whitelisted
*/
const isWhitelisted = (filePath: string, whitelist: WhitelistConfig): boolean => {
const normalizedPath = filePath.replaceAll('\\', '/');
// Check exact file matches
if (whitelist.files?.some((f) => normalizedPath.includes(f.replaceAll('\\', '/')))) {
return true;
}
// Check pattern matches (simple glob-like patterns)
if (whitelist.patterns) {
for (const pattern of whitelist.patterns) {
// Escape dots and replace glob patterns
// Use a placeholder for ** to avoid conflicts with single *
let regexPattern = pattern
.replaceAll('.', '\\.')
.replaceAll('**', '\u0000DOUBLESTAR\u0000')
.replaceAll('*', '[^/]*')
.replaceAll('\u0000DOUBLESTAR\u0000', '.*');
// If pattern ends with /**, match everything under that directory
// If pattern ends with **, just match everything from that point
const regex = new RegExp(`^${regexPattern}`);
if (regex.test(normalizedPath)) {
return true;
}
}
}
return false;
};
/**
* Main check function
*/
const checkConsoleLogs = () => {
const whitelist = loadWhitelist();
console.log('🔍 Checking for console.log statements...\n');
try {
// Search for console.log in TypeScript and JavaScript files
const output = execSync(
`git grep -n "console\\.log" -- "*.ts" "*.tsx" "*.js" "*.jsx" "*.mts" "*.cts" || true`,
{ encoding: 'utf8' },
);
if (!output.trim()) {
console.log('✅ No console.log statements found!');
return;
}
const lines = output.trim().split('\n');
const violations: Array<{ content: string, file: string; line: string; }> = [];
for (const line of lines) {
// Parse git grep output: filename:lineNumber:content
const match = line.match(/^([^:]+):(\d+):(.+)$/);
if (!match) continue;
const [, filePath, lineNumber, content] = match;
// Skip if whitelisted
if (isWhitelisted(filePath, whitelist)) {
continue;
}
// Skip comments
const trimmedContent = content.trim();
if (trimmedContent.startsWith('//') || trimmedContent.startsWith('*')) {
continue;
}
violations.push({
content: content.trim(),
file: filePath,
line: lineNumber,
});
}
if (violations.length === 0) {
console.log('✅ No console.log violations found (all matches are whitelisted or in comments)!');
return;
}
// Report violations as warnings
console.log('⚠️ Found console.log statements in the following files:\n');
// Use GitHub Actions annotation format for better visibility in CI
const isCI = process.env.CI === 'true' || process.env.GITHUB_ACTIONS === 'true';
for (const violation of violations) {
if (isCI) {
// GitHub Actions warning annotation format
console.log(`::warning file=${violation.file},line=${violation.line}::console.log found: ${violation.content}`);
} else {
console.log(` ${violation.file}:${violation.line}`);
console.log(` ${violation.content}\n`);
}
}
console.log(`\n💡 Total violations: ${violations.length}`);
console.log(
`\n📝 To whitelist files, add them to ${WHITELIST_PATH} in the following format:`,
);
console.log(`{
"files": ["path/to/file.ts"],
"patterns": ["scripts/**/*.mts", "**/*.test.ts"]
}\n`);
// Exit with 0 to not block CI, but warnings will still be visible
process.exit(0);
} catch (error: unknown) {
if (error instanceof Error && 'status' in error && error.status !== 0) {
console.error('❌ Error running git grep:', error.message);
process.exit(1);
}
throw error;
}
};
checkConsoleLogs();

View File

@@ -143,40 +143,20 @@ const NoteEditorModal = memo<NoteEditorModalProps>(
const handleOpenChange = (isOpen: boolean) => {
// When modal opens, load document content if in edit mode
if (isOpen && documentId && editor) {
console.log('[NoteEditorModal] Loading content:', {
cachedEditorDataPreview: cachedEditorData
? JSON.stringify(cachedEditorData).slice(0, 100)
: null,
cachedEditorDataType: typeof cachedEditorData,
documentId,
documentTitle,
hasCachedEditorData: !!cachedEditorData,
});
// If editorData is already cached (from list), use it directly
if (cachedEditorData) {
console.log('[NoteEditorModal] Using cached editorData', cachedEditorData);
setNoteTitle(documentTitle || '');
editor.setDocument('json', JSON.stringify(cachedEditorData));
return;
}
// Otherwise, fetch full content from API
console.log('[NoteEditorModal] Fetching from API');
documentService
.getDocumentById(documentId)
.then((doc) => {
if (doc && doc.content) {
setNoteTitle(doc.title || doc.filename || '');
console.log('[NoteEditorModal] Fetched doc.editorData:', {
editorDataPreview: doc.editorData
? JSON.stringify(doc.editorData).slice(0, 100)
: null,
editorDataType: typeof doc.editorData,
hasEditorData: !!doc.editorData,
});
editor.setDocument('json', doc.editorData);
}
})

View File

@@ -177,19 +177,6 @@ const MasonryFileItem = memo<MasonryFileItemProps>(
const isMarkdown = isMarkdownFile(name, fileType);
const isNote = isCustomNote(fileType);
// Debug: Log editorData for notes
useEffect(() => {
if (isNote) {
console.log('[MasonryFileItem] Note item:', {
editorDataPreview: editorData ? JSON.stringify(editorData).slice(0, 100) : null,
editorDataType: typeof editorData,
hasEditorData: !!editorData,
id,
name,
});
}
}, [isNote, id, name, editorData]);
const cardRef = useRef<HTMLDivElement>(null);
const [isInView, setIsInView] = useState(false);
@@ -288,12 +275,6 @@ const MasonryFileItem = memo<MasonryFileItemProps>(
)}
onClick={() => {
if (isNote) {
console.log('[MasonryFileItem] Opening note modal with:', {
editorDataType: typeof editorData,
hasEditorData: !!editorData,
id,
name,
});
setIsNoteModalOpen(true);
} else {
onOpen(id);
@@ -384,7 +365,6 @@ const MasonryFileItem = memo<MasonryFileItemProps>(
editorData={editorData}
knowledgeBaseId={knowledgeBaseId}
onClose={() => {
console.log('[MasonryFileItem] Closing note modal');
setIsNoteModalOpen(false);
}}
open={isNoteModalOpen}

View File

@@ -36,7 +36,6 @@ const AddButton = ({ knowledgeBaseId }: { knowledgeBaseId?: string }) => {
const handleCreateFolder = () => {
setIsModalOpen(false);
console.log('create folder');
};
const items = useMemo<MenuProps['items']>(