fix: systematic bug fixes — code-quality parser, env key, config warnings, auth cleanup, deployment safety

- code-quality/repository.ts: fix tsErrorMatch[3] → [4] for type field (group 3 is column, 4 is error|warning)
- code-quality/repository.ts: fix ESLint regex to make rule brackets optional (not all formatters include them)
- code-quality/repository.ts: fix Vitest test count — parse 'Tests' line (individual tests) instead of 'Test Files' (file count); improve Jest regex to capture pass/fail independently
- env/repository.ts: replace raw process.env.ENCRYPTION_KEY with config.ENCRYPTION_KEY so the validated default flows through a single source of truth
- config.ts: add startup console.warn when CSRF_SECRET or ENCRYPTION_KEY are using insecure defaults
- deployments/orchestrator.ts: refactor runDeploymentScript to use try/catch/finally — deployment record is now always written in the finally block, preventing zombie 'running' states if updateDeployment itself throws
- auth.tsx: remove dead 'user &&' guard (user is always truthy after the !user check above); remove debug console.log calls, keep console.error

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Hermes VM 2026-05-27 13:09:56 +00:00
parent cdc23696b2
commit 31b414d62b
5 changed files with 55 additions and 46 deletions

View File

@ -31,5 +31,13 @@ const envSchema = z.object({
export const config = envSchema.parse(process.env); export const config = envSchema.parse(process.env);
// Warn loudly when insecure default keys are in use
if (config.CSRF_SECRET === 'default-csrf-secret-change-in-production') {
console.warn('[config] WARNING: CSRF_SECRET is using the insecure default — set CSRF_SECRET in .env before deploying to production');
}
if (config.ENCRYPTION_KEY === 'default-encryption-key-change-in-production') {
console.warn('[config] WARNING: ENCRYPTION_KEY is using the insecure default — set ENCRYPTION_KEY in .env before deploying to production');
}
export const productId = productIdentity.productId; export const productId = productIdentity.productId;
export const productName = productIdentity.name; export const productName = productIdentity.name;

View File

@ -148,7 +148,7 @@ function parseTypeScriptOutput(output: string, projectPath: string): CodeQuality
if (tsErrorMatch) { if (tsErrorMatch) {
issues.push({ issues.push({
id: `ts-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`, id: `ts-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
type: tsErrorMatch[3] as 'error' | 'warning', type: tsErrorMatch[4] as 'error' | 'warning', // group 4 = type; group 3 = column
category: 'typescript', category: 'typescript',
file: tsErrorMatch[1], file: tsErrorMatch[1],
line: parseInt(tsErrorMatch[2]), line: parseInt(tsErrorMatch[2]),
@ -167,10 +167,12 @@ function parseEslintOutput(output: string, projectPath: string): CodeQualityIssu
const lines = output.split('\n'); const lines = output.split('\n');
for (const line of lines) { for (const line of lines) {
// ESLint format: file:line:col message [rule] // ESLint unix format: file:line:col: message [rule]
const eslintMatch = line.match(/(.+\.tsx?):(\d+):(\d+)\s+(.+?)\s+\[(.+)\]/); // Rule part in brackets may or may not be present depending on formatter
const eslintMatch = line.match(/(.+\.tsx?):(\d+):(\d+)[:\s]+(.+?)(?:\s+\[([^\]]+)\])?$/);
if (eslintMatch) { if (eslintMatch) {
const severity = eslintMatch[4].includes('error') ? 'error' : 'warning'; const msgAndLevel = eslintMatch[4];
const severity = /\berror\b/i.test(msgAndLevel) ? 'error' : 'warning';
issues.push({ issues.push({
id: `eslint-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`, id: `eslint-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
type: severity, type: severity,
@ -178,8 +180,8 @@ function parseEslintOutput(output: string, projectPath: string): CodeQualityIssu
file: eslintMatch[1], file: eslintMatch[1],
line: parseInt(eslintMatch[2]), line: parseInt(eslintMatch[2]),
column: parseInt(eslintMatch[3]), column: parseInt(eslintMatch[3]),
message: eslintMatch[4], message: msgAndLevel,
rule: eslintMatch[5], rule: eslintMatch[5] ?? 'unknown',
}); });
} }
} }
@ -210,18 +212,24 @@ function parseTestOutput(output: string): { passed: number; failed: number } {
let passed = 0; let passed = 0;
let failed = 0; let failed = 0;
// Try to parse Vitest output // Try to parse Vitest output — use "Tests" line (individual tests), not "Test Files" line
const vitestMatch = output.match(/Test Files\s+(\d+)\s+\((\d+)\s+failed/); // Format: " Tests 3 failed | 5 passed (8)" or " Tests 8 passed (8)"
if (vitestMatch) { const vitestFailMatch = output.match(/\bTests\b\s+(\d+)\s+failed[^|]*\|\s*(\d+)\s+passed/);
failed = parseInt(vitestMatch[2]); const vitestPassMatch = output.match(/\bTests\b\s+(\d+)\s+passed/);
passed = parseInt(vitestMatch[1]) - failed; if (vitestFailMatch) {
failed = parseInt(vitestFailMatch[1]);
passed = parseInt(vitestFailMatch[2]);
} else if (vitestPassMatch) {
passed = parseInt(vitestPassMatch[1]);
failed = 0;
} }
// Try to parse Jest output // Try to parse Jest output: "Tests: 5 passed, 2 failed" or "Tests: 2 failed, 5 passed"
const jestMatch = output.match(/Tests:\s+(\d+)\s+passed,?\s*(\d+)\s+failed/); const jestPassMatch = output.match(/Tests:.*?(\d+)\s+passed/);
if (jestMatch) { const jestFailMatch = output.match(/Tests:.*?(\d+)\s+failed/);
passed = parseInt(jestMatch[1]); if (jestPassMatch || jestFailMatch) {
failed = parseInt(jestMatch[2]); passed = jestPassMatch ? parseInt(jestPassMatch[1]) : 0;
failed = jestFailMatch ? parseInt(jestFailMatch[1]) : 0;
} }
return { passed, failed }; return { passed, failed };

View File

@ -30,6 +30,10 @@ async function runDeploymentScript(service: Service, deploymentId: string) {
const scriptDir = join(process.cwd(), '../../'); // Go to bytelyst-devops-tools root const scriptDir = join(process.cwd(), '../../'); // Go to bytelyst-devops-tools root
const scriptPath = join(scriptDir, service.scriptPath); const scriptPath = join(scriptDir, service.scriptPath);
let finalStatus: 'success' | 'failed' = 'failed';
let logs = '';
let version: string | undefined;
try { try {
const { stdout, stderr } = await execAsync(`bash ${scriptPath}`, { const { stdout, stderr } = await execAsync(`bash ${scriptPath}`, {
cwd: scriptDir, cwd: scriptDir,
@ -40,15 +44,9 @@ async function runDeploymentScript(service: Service, deploymentId: string) {
}, },
}); });
const logs = `STDOUT:\n${stdout}\n\nSTDERR:\n${stderr}`; logs = `STDOUT:\n${stdout}\n\nSTDERR:\n${stderr}`;
finalStatus = 'success';
// Update deployment as success version = extractVersion(stdout + stderr) || 'unknown';
await updateDeployment(deploymentId, {
status: 'success',
logs,
completedAt: new Date().toISOString(),
version: extractVersion(stdout + stderr) || 'unknown',
});
// Update service status // Update service status
const { getServiceById, updateService } = await import('../services/repository.js'); const { getServiceById, updateService } = await import('../services/repository.js');
@ -57,21 +55,14 @@ async function runDeploymentScript(service: Service, deploymentId: string) {
await updateService(service.id, { await updateService(service.id, {
status: 'up', status: 'up',
lastDeployedAt: new Date().toISOString(), lastDeployedAt: new Date().toISOString(),
version: extractVersion(stdout + stderr) || svc.version, version: version || svc.version,
}); });
} }
} catch (error: any) { } catch (error: any) {
const logs = error instanceof Error logs = error instanceof Error
? `ERROR: ${error.message}\n\n${(error as any).stdout ? `STDOUT:\n${(error as any).stdout}\n\n` : ''}${(error as any).stderr ? `STDERR:\n${(error as any).stderr}` : ''}` ? `ERROR: ${error.message}\n\n${(error as any).stdout ? `STDOUT:\n${(error as any).stdout}\n\n` : ''}${(error as any).stderr ? `STDERR:\n${(error as any).stderr}` : ''}`
: String(error); : String(error);
// Update deployment as failed
await updateDeployment(deploymentId, {
status: 'failed',
logs,
completedAt: new Date().toISOString(),
});
// Update service status to down // Update service status to down
const { getServiceById, updateService } = await import('../services/repository.js'); const { getServiceById, updateService } = await import('../services/repository.js');
const svc = await getServiceById(service.id); const svc = await getServiceById(service.id);
@ -80,6 +71,18 @@ async function runDeploymentScript(service: Service, deploymentId: string) {
status: 'down', status: 'down',
}); });
} }
} finally {
// Always write final status — ensures the deployment never gets stuck in 'running'
try {
await updateDeployment(deploymentId, {
status: finalStatus,
logs,
completedAt: new Date().toISOString(),
...(version ? { version } : {}),
});
} catch (updateError) {
console.error(`Failed to persist final deployment status for ${deploymentId}:`, updateError);
}
} }
} }

View File

@ -73,13 +73,10 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
try { try {
const token = getAccessTokenFromStorage(); const token = getAccessTokenFromStorage();
if (!token) { if (!token) {
console.log('No token found in storage');
setLoading(false); setLoading(false);
return; return;
} }
console.log('Checking auth with token...');
// Add timeout to prevent hanging // Add timeout to prevent hanging
const timeoutPromise = new Promise((_, reject) => const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Auth check timeout')), 10000) setTimeout(() => reject(new Error('Auth check timeout')), 10000)
@ -90,15 +87,12 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
timeoutPromise timeoutPromise
]) as MeResponse; ]) as MeResponse;
console.log('User data received:', userData);
setUser(userData); setUser(userData);
// Simplified admin check - just check global admin role // Simplified admin check - just check global admin role
const globalRole = userData.role; const globalRole = userData.role;
const hasAdminAccess = globalRole === 'admin'; const hasAdminAccess = globalRole === 'admin';
setIsAdmin(hasAdminAccess); setIsAdmin(hasAdminAccess);
console.log('Admin access:', hasAdminAccess);
} catch (error) { } catch (error) {
console.error('Auth check failed:', error); console.error('Auth check failed:', error);
clearAuthTokens(); clearAuthTokens();
@ -109,9 +103,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
async function login(email: string, password: string, productId: string) { async function login(email: string, password: string, productId: string) {
try { try {
console.log('Attempting login for:', email, 'with productId:', productId);
const response = await authApi.login({ email, password, productId }); const response = await authApi.login({ email, password, productId });
console.log('Login response received:', response);
setAccessToken(response.accessToken); setAccessToken(response.accessToken);
setRefreshToken(response.refreshToken); setRefreshToken(response.refreshToken);
@ -120,8 +112,6 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
// Check if user has admin access (global admin role) // Check if user has admin access (global admin role)
const hasAdminAccess = response.user.role === 'admin'; const hasAdminAccess = response.user.role === 'admin';
setIsAdmin(hasAdminAccess); setIsAdmin(hasAdminAccess);
console.log('Login successful, admin access:', hasAdminAccess);
} catch (error) { } catch (error) {
console.error('Login failed:', error); console.error('Login failed:', error);
throw error; throw error;
@ -148,7 +138,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
return <div className="min-h-screen flex items-center justify-center">Redirecting to login...</div>; return <div className="min-h-screen flex items-center justify-center">Redirecting to login...</div>;
} }
if (user && !isAdmin) { if (!isAdmin) {
return ( return (
<div className="min-h-screen flex items-center justify-center"> <div className="min-h-screen flex items-center justify-center">
<div className="text-center"> <div className="text-center">

File diff suppressed because one or more lines are too long