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);
// 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 productName = productIdentity.name;

View File

@ -148,7 +148,7 @@ function parseTypeScriptOutput(output: string, projectPath: string): CodeQuality
if (tsErrorMatch) {
issues.push({
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',
file: tsErrorMatch[1],
line: parseInt(tsErrorMatch[2]),
@ -167,10 +167,12 @@ function parseEslintOutput(output: string, projectPath: string): CodeQualityIssu
const lines = output.split('\n');
for (const line of lines) {
// ESLint format: file:line:col message [rule]
const eslintMatch = line.match(/(.+\.tsx?):(\d+):(\d+)\s+(.+?)\s+\[(.+)\]/);
// ESLint unix format: file:line:col: message [rule]
// Rule part in brackets may or may not be present depending on formatter
const eslintMatch = line.match(/(.+\.tsx?):(\d+):(\d+)[:\s]+(.+?)(?:\s+\[([^\]]+)\])?$/);
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({
id: `eslint-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
type: severity,
@ -178,8 +180,8 @@ function parseEslintOutput(output: string, projectPath: string): CodeQualityIssu
file: eslintMatch[1],
line: parseInt(eslintMatch[2]),
column: parseInt(eslintMatch[3]),
message: eslintMatch[4],
rule: eslintMatch[5],
message: msgAndLevel,
rule: eslintMatch[5] ?? 'unknown',
});
}
}
@ -210,18 +212,24 @@ function parseTestOutput(output: string): { passed: number; failed: number } {
let passed = 0;
let failed = 0;
// Try to parse Vitest output
const vitestMatch = output.match(/Test Files\s+(\d+)\s+\((\d+)\s+failed/);
if (vitestMatch) {
failed = parseInt(vitestMatch[2]);
passed = parseInt(vitestMatch[1]) - failed;
// Try to parse Vitest output — use "Tests" line (individual tests), not "Test Files" line
// Format: " Tests 3 failed | 5 passed (8)" or " Tests 8 passed (8)"
const vitestFailMatch = output.match(/\bTests\b\s+(\d+)\s+failed[^|]*\|\s*(\d+)\s+passed/);
const vitestPassMatch = output.match(/\bTests\b\s+(\d+)\s+passed/);
if (vitestFailMatch) {
failed = parseInt(vitestFailMatch[1]);
passed = parseInt(vitestFailMatch[2]);
} else if (vitestPassMatch) {
passed = parseInt(vitestPassMatch[1]);
failed = 0;
}
// Try to parse Jest output
const jestMatch = output.match(/Tests:\s+(\d+)\s+passed,?\s*(\d+)\s+failed/);
if (jestMatch) {
passed = parseInt(jestMatch[1]);
failed = parseInt(jestMatch[2]);
// Try to parse Jest output: "Tests: 5 passed, 2 failed" or "Tests: 2 failed, 5 passed"
const jestPassMatch = output.match(/Tests:.*?(\d+)\s+passed/);
const jestFailMatch = output.match(/Tests:.*?(\d+)\s+failed/);
if (jestPassMatch || jestFailMatch) {
passed = jestPassMatch ? parseInt(jestPassMatch[1]) : 0;
failed = jestFailMatch ? parseInt(jestFailMatch[1]) : 0;
}
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 scriptPath = join(scriptDir, service.scriptPath);
let finalStatus: 'success' | 'failed' = 'failed';
let logs = '';
let version: string | undefined;
try {
const { stdout, stderr } = await execAsync(`bash ${scriptPath}`, {
cwd: scriptDir,
@ -40,15 +44,9 @@ async function runDeploymentScript(service: Service, deploymentId: string) {
},
});
const logs = `STDOUT:\n${stdout}\n\nSTDERR:\n${stderr}`;
// Update deployment as success
await updateDeployment(deploymentId, {
status: 'success',
logs,
completedAt: new Date().toISOString(),
version: extractVersion(stdout + stderr) || 'unknown',
});
logs = `STDOUT:\n${stdout}\n\nSTDERR:\n${stderr}`;
finalStatus = 'success';
version = extractVersion(stdout + stderr) || 'unknown';
// Update service status
const { getServiceById, updateService } = await import('../services/repository.js');
@ -57,21 +55,14 @@ async function runDeploymentScript(service: Service, deploymentId: string) {
await updateService(service.id, {
status: 'up',
lastDeployedAt: new Date().toISOString(),
version: extractVersion(stdout + stderr) || svc.version,
version: version || svc.version,
});
}
} 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}` : ''}`
: String(error);
// Update deployment as failed
await updateDeployment(deploymentId, {
status: 'failed',
logs,
completedAt: new Date().toISOString(),
});
// Update service status to down
const { getServiceById, updateService } = await import('../services/repository.js');
const svc = await getServiceById(service.id);
@ -80,6 +71,18 @@ async function runDeploymentScript(service: Service, deploymentId: string) {
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 {
const token = getAccessTokenFromStorage();
if (!token) {
console.log('No token found in storage');
setLoading(false);
return;
}
console.log('Checking auth with token...');
// Add timeout to prevent hanging
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Auth check timeout')), 10000)
@ -90,15 +87,12 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
timeoutPromise
]) as MeResponse;
console.log('User data received:', userData);
setUser(userData);
// Simplified admin check - just check global admin role
const globalRole = userData.role;
const hasAdminAccess = globalRole === 'admin';
setIsAdmin(hasAdminAccess);
console.log('Admin access:', hasAdminAccess);
} catch (error) {
console.error('Auth check failed:', error);
clearAuthTokens();
@ -109,9 +103,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
async function login(email: string, password: string, productId: string) {
try {
console.log('Attempting login for:', email, 'with productId:', productId);
const response = await authApi.login({ email, password, productId });
console.log('Login response received:', response);
setAccessToken(response.accessToken);
setRefreshToken(response.refreshToken);
@ -120,8 +112,6 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
// Check if user has admin access (global admin role)
const hasAdminAccess = response.user.role === 'admin';
setIsAdmin(hasAdminAccess);
console.log('Login successful, admin access:', hasAdminAccess);
} catch (error) {
console.error('Login failed:', 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>;
}
if (user && !isAdmin) {
if (!isAdmin) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">

File diff suppressed because one or more lines are too long