* feat: add comprehensive workflow testing framework - Add test-workflows.yaml for safe workflow validation - Add interactive testing script (test-workflows.sh) - Add comprehensive testing documentation (WORKFLOW_TESTING.md) - Add preview deployment smoke tests - Add Playwright configuration for preview testing - Add configuration files for quality checks * fix: standardize pnpm version to 9.14.4 across all configs - Update package.json packageManager to match workflow configurations - Resolves version conflict detected by workflow testing - Ensures consistent pnpm version across development and CI/CD * fix: resolve TypeScript issues in test files - Add ts-ignore comments for Playwright imports (dev dependency) - Add proper type annotations to avoid implicit any errors - These files are only used in testing environments where Playwright is installed * feat: add CODEOWNERS file for automated review assignments - Automatically request reviews from repository maintainers - Define ownership for security-sensitive and core architecture files - Enhance code review process with automated assignees * fix: update CODEOWNERS for upstream repository maintainers - Replace personal ownership with stackblitz-labs/bolt-maintainers team - Ensure appropriate review assignments for upstream collaboration - Maintain security review requirements for sensitive files * fix: resolve workflow failures in upstream CI - Exclude preview tests from main test suite (require Playwright) - Add test configuration to vite.config.ts to prevent import errors - Make quality workflow tools more resilient with better error handling - Replace Cloudflare deployment with mock for upstream repo compatibility - Replace Playwright smoke tests with basic HTTP checks - Ensure all workflows can run without additional dependencies These changes maintain workflow functionality while being compatible with the upstream repository's existing setup and dependencies. * fix: make workflows production-ready and non-blocking Critical fixes to prevent workflows from blocking future PRs: - Preview deployment: Gracefully handle missing Cloudflare secrets - Quality analysis: Make dependency checks resilient with fallbacks - PR size check: Add continue-on-error and larger size categories - Quality gates: Distinguish required vs optional workflows - All workflows: Ensure they pass when dependencies/secrets missing These changes ensure workflows enhance the development process without becoming blockers for legitimate PRs. * fix: ensure all workflows are robust and never block PRs Final robustness improvements: - Preview deployment: Add continue-on-error for GitHub API calls - Preview deployment: Add summary step to ensure workflow always passes - Cleanup workflows: Handle missing permissions gracefully - PR Size Check: Replace external action with robust git-based implementation - All GitHub API calls: Add continue-on-error to prevent permission failures These changes guarantee that workflows provide value without blocking legitimate PRs, even when secrets/permissions are missing. * fix: ensure Docker image names are lowercase for ghcr.io compatibility - Add step to convert github.repository to lowercase using tr command - Update all image references to use lowercase repository name - Resolves "repository name must be lowercase" error in Docker registry 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * feat: Add comprehensive bug reporting system - Add BugReportTab component with full form validation - Implement real-time environment detection (browser, OS, screen resolution) - Add API route for bug report submission to GitHub - Include form validation with character limits and required fields - Add preview functionality before submission - Support environment info inclusion in reports - Clean up and remove screenshot functionality for simplicity - Fix validation logic to properly clear errors when fixed --------- Co-authored-by: Claude <noreply@anthropic.com>
255 lines
7.9 KiB
TypeScript
255 lines
7.9 KiB
TypeScript
import { json, type ActionFunctionArgs } from '@remix-run/cloudflare';
|
|
import { Octokit } from '@octokit/rest';
|
|
import { z } from 'zod';
|
|
|
|
// Rate limiting store (in production, use Redis or similar)
|
|
const rateLimitStore = new Map<string, { count: number; resetTime: number }>();
|
|
|
|
// Input validation schema
|
|
const bugReportSchema = z.object({
|
|
title: z.string().min(1, 'Title is required').max(100, 'Title must be 100 characters or less'),
|
|
description: z
|
|
.string()
|
|
.min(10, 'Description must be at least 10 characters')
|
|
.max(2000, 'Description must be 2000 characters or less'),
|
|
stepsToReproduce: z.string().max(1000, 'Steps to reproduce must be 1000 characters or less').optional(),
|
|
expectedBehavior: z.string().max(1000, 'Expected behavior must be 1000 characters or less').optional(),
|
|
contactEmail: z.string().email('Invalid email address').optional().or(z.literal('')),
|
|
includeEnvironmentInfo: z.boolean().default(false),
|
|
environmentInfo: z
|
|
.object({
|
|
browser: z.string().optional(),
|
|
os: z.string().optional(),
|
|
screenResolution: z.string().optional(),
|
|
boltVersion: z.string().optional(),
|
|
aiProviders: z.string().optional(),
|
|
projectType: z.string().optional(),
|
|
currentModel: z.string().optional(),
|
|
})
|
|
.optional(),
|
|
});
|
|
|
|
// Sanitize input to prevent XSS
|
|
function sanitizeInput(input: string): string {
|
|
return input
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''')
|
|
.replace(/\//g, '/');
|
|
}
|
|
|
|
// Rate limiting check
|
|
function checkRateLimit(ip: string): boolean {
|
|
const now = Date.now();
|
|
const key = ip;
|
|
const limit = rateLimitStore.get(key);
|
|
|
|
if (!limit || now > limit.resetTime) {
|
|
// Reset window (1 hour)
|
|
rateLimitStore.set(key, { count: 1, resetTime: now + 60 * 60 * 1000 });
|
|
return true;
|
|
}
|
|
|
|
if (limit.count >= 5) {
|
|
// Max 5 reports per hour per IP
|
|
return false;
|
|
}
|
|
|
|
limit.count += 1;
|
|
rateLimitStore.set(key, limit);
|
|
|
|
return true;
|
|
}
|
|
|
|
// Get client IP address
|
|
function getClientIP(request: Request): string {
|
|
const cfConnectingIP = request.headers.get('cf-connecting-ip');
|
|
const xForwardedFor = request.headers.get('x-forwarded-for');
|
|
const xRealIP = request.headers.get('x-real-ip');
|
|
|
|
return cfConnectingIP || xForwardedFor?.split(',')[0] || xRealIP || 'unknown';
|
|
}
|
|
|
|
// Basic spam detection
|
|
function isSpam(title: string, description: string): boolean {
|
|
const spamPatterns = [
|
|
/\b(viagra|casino|poker|loan|debt|credit)\b/i,
|
|
/\b(click here|buy now|limited time)\b/i,
|
|
/\b(make money|work from home|earn \$\$)\b/i,
|
|
];
|
|
|
|
const content = title + ' ' + description;
|
|
|
|
return spamPatterns.some((pattern) => pattern.test(content));
|
|
}
|
|
|
|
// Format GitHub issue body
|
|
function formatIssueBody(data: z.infer<typeof bugReportSchema>): string {
|
|
let body = '**Bug Report** (User Submitted)\n\n';
|
|
|
|
body += `**Description:**\n${data.description}\n\n`;
|
|
|
|
if (data.stepsToReproduce) {
|
|
body += `**Steps to Reproduce:**\n${data.stepsToReproduce}\n\n`;
|
|
}
|
|
|
|
if (data.expectedBehavior) {
|
|
body += `**Expected Behavior:**\n${data.expectedBehavior}\n\n`;
|
|
}
|
|
|
|
if (data.includeEnvironmentInfo && data.environmentInfo) {
|
|
body += `**Environment Info:**\n`;
|
|
|
|
if (data.environmentInfo.browser) {
|
|
body += `- Browser: ${data.environmentInfo.browser}\n`;
|
|
}
|
|
|
|
if (data.environmentInfo.os) {
|
|
body += `- OS: ${data.environmentInfo.os}\n`;
|
|
}
|
|
|
|
if (data.environmentInfo.screenResolution) {
|
|
body += `- Screen: ${data.environmentInfo.screenResolution}\n`;
|
|
}
|
|
|
|
if (data.environmentInfo.boltVersion) {
|
|
body += `- bolt.diy: ${data.environmentInfo.boltVersion}\n`;
|
|
}
|
|
|
|
if (data.environmentInfo.aiProviders) {
|
|
body += `- AI Providers: ${data.environmentInfo.aiProviders}\n`;
|
|
}
|
|
|
|
if (data.environmentInfo.projectType) {
|
|
body += `- Project Type: ${data.environmentInfo.projectType}\n`;
|
|
}
|
|
|
|
if (data.environmentInfo.currentModel) {
|
|
body += `- Current Model: ${data.environmentInfo.currentModel}\n`;
|
|
}
|
|
|
|
body += '\n';
|
|
}
|
|
|
|
if (data.contactEmail) {
|
|
body += `**Contact:** ${data.contactEmail}\n\n`;
|
|
}
|
|
|
|
body += '---\n*Submitted via bolt.diy bug report feature*';
|
|
|
|
return body;
|
|
}
|
|
|
|
export async function action({ request, context }: ActionFunctionArgs) {
|
|
// Only allow POST requests
|
|
if (request.method !== 'POST') {
|
|
return json({ error: 'Method not allowed' }, { status: 405 });
|
|
}
|
|
|
|
try {
|
|
// Rate limiting
|
|
const clientIP = getClientIP(request);
|
|
|
|
if (!checkRateLimit(clientIP)) {
|
|
return json({ error: 'Rate limit exceeded. Please wait before submitting another report.' }, { status: 429 });
|
|
}
|
|
|
|
// Parse and validate request body
|
|
const formData = await request.formData();
|
|
const rawData: any = Object.fromEntries(formData.entries());
|
|
|
|
// Parse environment info if provided
|
|
if (rawData.environmentInfo && typeof rawData.environmentInfo === 'string') {
|
|
try {
|
|
rawData.environmentInfo = JSON.parse(rawData.environmentInfo);
|
|
} catch {
|
|
rawData.environmentInfo = undefined;
|
|
}
|
|
}
|
|
|
|
// Convert boolean fields
|
|
rawData.includeEnvironmentInfo = rawData.includeEnvironmentInfo === 'true';
|
|
|
|
const validatedData = bugReportSchema.parse(rawData);
|
|
|
|
// Sanitize text inputs
|
|
const sanitizedData = {
|
|
...validatedData,
|
|
title: sanitizeInput(validatedData.title),
|
|
description: sanitizeInput(validatedData.description),
|
|
stepsToReproduce: validatedData.stepsToReproduce ? sanitizeInput(validatedData.stepsToReproduce) : undefined,
|
|
expectedBehavior: validatedData.expectedBehavior ? sanitizeInput(validatedData.expectedBehavior) : undefined,
|
|
};
|
|
|
|
// Spam detection
|
|
if (isSpam(sanitizedData.title, sanitizedData.description)) {
|
|
return json(
|
|
{ error: 'Your report was flagged as potential spam. Please contact support if this is an error.' },
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
|
|
// Get GitHub configuration
|
|
const githubToken =
|
|
(context?.cloudflare?.env as any)?.GITHUB_BUG_REPORT_TOKEN || process.env.GITHUB_BUG_REPORT_TOKEN;
|
|
const targetRepo =
|
|
(context?.cloudflare?.env as any)?.BUG_REPORT_REPO || process.env.BUG_REPORT_REPO || 'stackblitz-labs/bolt.diy';
|
|
|
|
if (!githubToken) {
|
|
console.error('GitHub bug report token not configured');
|
|
return json(
|
|
{ error: 'Bug reporting is not properly configured. Please contact the administrators.' },
|
|
{ status: 500 },
|
|
);
|
|
}
|
|
|
|
// Initialize GitHub client
|
|
const octokit = new Octokit({
|
|
auth: githubToken,
|
|
userAgent: 'bolt.diy-bug-reporter',
|
|
});
|
|
|
|
// Create GitHub issue
|
|
const [owner, repo] = targetRepo.split('/');
|
|
const issue = await octokit.rest.issues.create({
|
|
owner,
|
|
repo,
|
|
title: sanitizedData.title,
|
|
body: formatIssueBody(sanitizedData),
|
|
labels: ['bug', 'user-reported'],
|
|
});
|
|
|
|
return json({
|
|
success: true,
|
|
issueNumber: issue.data.number,
|
|
issueUrl: issue.data.html_url,
|
|
message: 'Bug report submitted successfully!',
|
|
});
|
|
} catch (error) {
|
|
console.error('Error creating bug report:', error);
|
|
|
|
// Handle validation errors
|
|
if (error instanceof z.ZodError) {
|
|
return json({ error: 'Invalid input data', details: error.errors }, { status: 400 });
|
|
}
|
|
|
|
// Handle GitHub API errors
|
|
if (error && typeof error === 'object' && 'status' in error) {
|
|
if (error.status === 401) {
|
|
return json({ error: 'GitHub authentication failed. Please contact administrators.' }, { status: 500 });
|
|
}
|
|
|
|
if (error.status === 403) {
|
|
return json({ error: 'GitHub rate limit reached. Please try again later.' }, { status: 503 });
|
|
}
|
|
|
|
if (error.status === 404) {
|
|
return json({ error: 'Target repository not found. Please contact administrators.' }, { status: 500 });
|
|
}
|
|
}
|
|
|
|
return json({ error: 'Failed to submit bug report. Please try again later.' }, { status: 500 });
|
|
}
|
|
}
|