Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 152 additions & 9 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -32488,6 +32488,74 @@ const fs = __importStar(__nccwpck_require__(1943));
const path = __importStar(__nccwpck_require__(6928));
const sdk_1 = __nccwpck_require__(6381);
const dead_code_1 = __nccwpck_require__(1655);
/** Fields that should be redacted from logs */
const SENSITIVE_KEYS = new Set([
'authorization',
'cookie',
'set-cookie',
'access_token',
'refresh_token',
'api_key',
'apikey',
'password',
'secret',
'token',
'x-api-key',
]);
const MAX_VALUE_LENGTH = 1000;
/**
* Safely serialize a value for logging, handling circular refs, BigInt, and large values.
* Redacts sensitive fields.
*/
function safeSerialize(value, maxLength = MAX_VALUE_LENGTH) {
try {
const seen = new WeakSet();
const serialized = JSON.stringify(value, (key, val) => {
// Redact sensitive keys
if (key && SENSITIVE_KEYS.has(key.toLowerCase())) {
return '[REDACTED]';
}
// Handle BigInt
if (typeof val === 'bigint') {
return val.toString();
}
// Handle circular references
if (typeof val === 'object' && val !== null) {
if (seen.has(val)) {
return '[Circular]';
}
seen.add(val);
}
return val;
}, 2);
// Truncate if too long
if (serialized && serialized.length > maxLength) {
return serialized.slice(0, maxLength) + '... [truncated]';
}
return serialized ?? '[undefined]';
}
catch {
return '[unserializable]';
}
}
/**
* Redact sensitive fields from an object (shallow copy).
*/
function redactSensitive(obj) {
if (!obj || typeof obj !== 'object') {
return null;
}
const result = {};
for (const [key, value] of Object.entries(obj)) {
if (SENSITIVE_KEYS.has(key.toLowerCase())) {
result[key] = '[REDACTED]';
}
else {
result[key] = value;
}
}
return result;
}
async function createZipArchive(workspacePath) {
const zipPath = path.join(workspacePath, '.dead-code-hunter-repo.zip');
core.info('Creating zip archive...');
Expand Down Expand Up @@ -32575,21 +32643,96 @@ async function run() {
}
}
catch (error) {
if (error.response) {
const status = error.response.status;
if (status === 401) {
core.error('Invalid API key. Get your key at https://dashboard.supermodeltools.com');
// Log error details for debugging (using debug level for potentially sensitive data)
core.info('--- Error Debug Info ---');
core.info(`Error type: ${error?.constructor?.name ?? 'unknown'}`);
core.info(`Error message: ${error?.message ?? 'no message'}`);
core.info(`Error name: ${error?.name ?? 'no name'}`);
// Check various error structures used by different HTTP clients
// Use core.debug for detailed/sensitive info, core.info for safe summaries
try {
if (error?.response) {
core.info(`Response status: ${error.response.status ?? 'unknown'}`);
core.info(`Response statusText: ${error.response.statusText ?? 'unknown'}`);
core.info(`Response data: ${safeSerialize(error.response.data)}`);
// Headers may contain sensitive values - use debug level
core.debug(`Response headers: ${safeSerialize(redactSensitive(error.response.headers))}`);
}
else {
core.error(`API error (${status})`);
if (error?.body) {
core.info(`Error body: ${safeSerialize(error.body)}`);
}
if (error?.status) {
core.info(`Error status: ${error.status}`);
}
if (error?.statusCode) {
core.info(`Error statusCode: ${error.statusCode}`);
}
if (error?.cause) {
core.debug(`Error cause: ${safeSerialize(error.cause)}`);
}
}
catch {
core.debug('Failed to serialize some error properties');
}
core.info('--- End Debug Info ---');
let errorMessage = 'An unknown error occurred';
let helpText = '';
// Try multiple error structures
const status = error?.response?.status || error?.status || error?.statusCode;
let apiMessage = '';
// Try to extract message from various locations
try {
apiMessage =
error?.response?.data?.message ||
error?.response?.data?.error ||
error?.response?.data ||
error?.body?.message ||
error?.body?.error ||
error?.message ||
'';
if (typeof apiMessage !== 'string') {
apiMessage = safeSerialize(apiMessage, 500);
}
}
if (error instanceof Error) {
core.setFailed(error.message);
catch {
apiMessage = '';
}
if (status === 401) {
errorMessage = 'Invalid API key';
helpText = 'Get your key at https://dashboard.supermodeltools.com';
}
else if (status === 500) {
errorMessage = apiMessage || 'Internal server error';
// Check for common issues and provide guidance
if (apiMessage.includes('Nested archives')) {
helpText = 'Your repository contains nested archive files (.zip, .tar, etc.). ' +
'Add them to .gitattributes with "export-ignore" to exclude from analysis. ' +
'Example: tests/fixtures/*.zip export-ignore';
}
else if (apiMessage.includes('exceeds maximum')) {
helpText = 'Your repository or a file within it exceeds size limits. ' +
'Consider excluding large files using .gitattributes with "export-ignore".';
}
}
else if (status === 413) {
errorMessage = 'Repository archive too large';
helpText = 'Reduce archive size by excluding large files in .gitattributes';
}
else if (status === 429) {
errorMessage = 'Rate limit exceeded';
helpText = 'Please wait before retrying';
}
else if (status) {
errorMessage = apiMessage || `API error (${status})`;
}
else {
core.setFailed('An unknown error occurred');
errorMessage = apiMessage || error?.message || 'An unknown error occurred';
}
core.error(`Error: ${errorMessage}`);
if (helpText) {
core.error(`Help: ${helpText}`);
}
core.setFailed(errorMessage);
}
}
run();
Expand Down
171 changes: 162 additions & 9 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,83 @@ import * as path from 'path';
import { Configuration, DefaultApi } from '@supermodeltools/sdk';
import { findDeadCode, formatPrComment } from './dead-code';

/** Fields that should be redacted from logs */
const SENSITIVE_KEYS = new Set([
'authorization',
'cookie',
'set-cookie',
'access_token',
'refresh_token',
'api_key',
'apikey',
'password',
'secret',
'token',
'x-api-key',
]);

const MAX_VALUE_LENGTH = 1000;

/**
* Safely serialize a value for logging, handling circular refs, BigInt, and large values.
* Redacts sensitive fields.
*/
function safeSerialize(value: unknown, maxLength = MAX_VALUE_LENGTH): string {
try {
const seen = new WeakSet();

const serialized = JSON.stringify(value, (key, val) => {
// Redact sensitive keys
if (key && SENSITIVE_KEYS.has(key.toLowerCase())) {
return '[REDACTED]';
}

// Handle BigInt
if (typeof val === 'bigint') {
return val.toString();
}

// Handle circular references
if (typeof val === 'object' && val !== null) {
if (seen.has(val)) {
return '[Circular]';
}
seen.add(val);
}

return val;
}, 2);

// Truncate if too long
if (serialized && serialized.length > maxLength) {
return serialized.slice(0, maxLength) + '... [truncated]';
}

return serialized ?? '[undefined]';
} catch {
return '[unserializable]';
}
}

/**
* Redact sensitive fields from an object (shallow copy).
*/
function redactSensitive(obj: Record<string, unknown> | null | undefined): Record<string, unknown> | null {
if (!obj || typeof obj !== 'object') {
return null;
}

const result: Record<string, unknown> = {};
for (const [key, value] of Object.entries(obj)) {
if (SENSITIVE_KEYS.has(key.toLowerCase())) {
result[key] = '[REDACTED]';
} else {
result[key] = value;
}
}
return result;
}

async function createZipArchive(workspacePath: string): Promise<string> {
const zipPath = path.join(workspacePath, '.dead-code-hunter-repo.zip');

Expand Down Expand Up @@ -120,20 +197,96 @@ async function run(): Promise<void> {
}

} catch (error: any) {
if (error.response) {
const status = error.response.status;
if (status === 401) {
core.error('Invalid API key. Get your key at https://dashboard.supermodeltools.com');
} else {
core.error(`API error (${status})`);
// Log error details for debugging (using debug level for potentially sensitive data)
core.info('--- Error Debug Info ---');
core.info(`Error type: ${error?.constructor?.name ?? 'unknown'}`);
core.info(`Error message: ${error?.message ?? 'no message'}`);
core.info(`Error name: ${error?.name ?? 'no name'}`);

// Check various error structures used by different HTTP clients
// Use core.debug for detailed/sensitive info, core.info for safe summaries
try {
if (error?.response) {
core.info(`Response status: ${error.response.status ?? 'unknown'}`);
core.info(`Response statusText: ${error.response.statusText ?? 'unknown'}`);
core.info(`Response data: ${safeSerialize(error.response.data)}`);
// Headers may contain sensitive values - use debug level
core.debug(`Response headers: ${safeSerialize(redactSensitive(error.response.headers))}`);
}
if (error?.body) {
core.info(`Error body: ${safeSerialize(error.body)}`);
}
if (error?.status) {
core.info(`Error status: ${error.status}`);
}
if (error?.statusCode) {
core.info(`Error statusCode: ${error.statusCode}`);
}
if (error?.cause) {
core.debug(`Error cause: ${safeSerialize(error.cause)}`);
}
} catch {
core.debug('Failed to serialize some error properties');
}
core.info('--- End Debug Info ---');

let errorMessage = 'An unknown error occurred';
let helpText = '';

// Try multiple error structures
const status = error?.response?.status || error?.status || error?.statusCode;
let apiMessage = '';

// Try to extract message from various locations
try {
apiMessage =
error?.response?.data?.message ||
error?.response?.data?.error ||
error?.response?.data ||
error?.body?.message ||
error?.body?.error ||
error?.message ||
'';
if (typeof apiMessage !== 'string') {
apiMessage = safeSerialize(apiMessage, 500);
}
} catch {
apiMessage = '';
}

if (error instanceof Error) {
core.setFailed(error.message);
if (status === 401) {
errorMessage = 'Invalid API key';
helpText = 'Get your key at https://dashboard.supermodeltools.com';
} else if (status === 500) {
errorMessage = apiMessage || 'Internal server error';

// Check for common issues and provide guidance
if (apiMessage.includes('Nested archives')) {
helpText = 'Your repository contains nested archive files (.zip, .tar, etc.). ' +
'Add them to .gitattributes with "export-ignore" to exclude from analysis. ' +
'Example: tests/fixtures/*.zip export-ignore';
} else if (apiMessage.includes('exceeds maximum')) {
helpText = 'Your repository or a file within it exceeds size limits. ' +
'Consider excluding large files using .gitattributes with "export-ignore".';
}
} else if (status === 413) {
errorMessage = 'Repository archive too large';
helpText = 'Reduce archive size by excluding large files in .gitattributes';
} else if (status === 429) {
errorMessage = 'Rate limit exceeded';
helpText = 'Please wait before retrying';
} else if (status) {
errorMessage = apiMessage || `API error (${status})`;
} else {
core.setFailed('An unknown error occurred');
errorMessage = apiMessage || error?.message || 'An unknown error occurred';
}

core.error(`Error: ${errorMessage}`);
if (helpText) {
core.error(`Help: ${helpText}`);
}

core.setFailed(errorMessage);
}
}

Expand Down