Skip to content
Merged
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ jobs:
self-test:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v4

Expand Down
34 changes: 18 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# Dead Code Hunter

A GitHub Action that uses [Supermodel](https://supermodeltools.com) call graphs to find unreachable functions in your codebase.
A GitHub Action that uses [Supermodel](https://supermodeltools.com) to find unreachable functions in your codebase.

## What it does

1. Creates a zip archive of your repository using `git archive`
2. Sends it to Supermodel's call graph API
3. Analyzes the graph to find functions with no callers
2. Sends it to Supermodel's graph API for analysis
3. Identifies functions with no callers (dead code)
4. Filters out false positives (entry points, exports, tests)
5. Posts findings as a PR comment

Expand All @@ -16,11 +16,13 @@ A GitHub Action that uses [Supermodel](https://supermodeltools.com) call graphs
name: Dead Code Hunter
on:
pull_request:
workflow_dispatch:

jobs:
hunt:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v4
- uses: supermodeltools/dead-code-hunter@v1
Expand All @@ -30,8 +32,8 @@ jobs:

## Getting a Supermodel API Key

1. Sign up at [supermodeltools.com](https://supermodeltools.com)
2. Create an API key in the dashboard
1. Sign up at [dashboard.supermodeltools.com](https://dashboard.supermodeltools.com)
2. Create an API key
3. Add it as a repository secret named `SUPERMODEL_API_KEY`

## Configuration
Expand All @@ -57,16 +59,16 @@ When dead code is found, the action posts a comment like:

> ## Dead Code Hunter
>
> Found **7** potentially unused functions:
> Found **3** potentially unused functions:
>
> | Function | File | Line |
> |----------|------|------|
> | `unusedHelper` | src/utils.ts#L42 | L42 |
> | `oldValidator` | src/validation.ts#L15 | L15 |
> | ... | ... | ... |
> | `unusedHelperFunction` | src/example-dead-code.ts#L7 | L7 |
> | `formatUnusedData` | src/example-dead-code.ts#L12 | L12 |
> | `fetchUnusedData` | src/example-dead-code.ts#L17 | L17 |
>
> ---
> _Powered by [Supermodel](https://supermodeltools.com) call graph analysis_
> _Powered by [Supermodel](https://supermodeltools.com) graph analysis_

## False Positive Filtering

Expand All @@ -89,7 +91,7 @@ You can add custom ignore patterns:

## Supported Languages

Supermodel supports call graph analysis for:
Supermodel supports analysis for:

- TypeScript / JavaScript
- Python
Expand All @@ -102,10 +104,10 @@ Supermodel supports call graph analysis for:

```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ git archive │────▶│ Supermodel API │────▶│ Call Graph │
│ (create zip) │ │ /v1/graphs/call│ │ Analysis │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ git archive │────▶│ Supermodel API │────▶│ Graph
│ (create zip) │ │ /v1/graphs/ │ │ Analysis │
└─────────────────┘ │ supermodel │ └─────────────────┘
└─────────────────┘
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ PR Comment │◀────│ Filter False │◀────│ Find Uncalled │
Expand Down
36 changes: 23 additions & 13 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -32412,7 +32412,7 @@ ${rows}`;
if (deadCode.length > 50) {
comment += `\n\n_...and ${deadCode.length - 50} more. See action output for full list._`;
}
comment += `\n\n---\n_Powered by [Supermodel](https://supermodeltools.com) call graph analysis_`;
comment += `\n\n---\n_Powered by [Supermodel](https://supermodeltools.com) graph analysis_`;
return comment;
}

Expand Down Expand Up @@ -32467,12 +32467,12 @@ const sdk_1 = __nccwpck_require__(6381);
const dead_code_1 = __nccwpck_require__(1655);
async function createZipArchive(workspacePath) {
const zipPath = path.join(workspacePath, '.dead-code-hunter-repo.zip');
core.info('Creating zip archive using git archive...');
core.info('Creating zip archive...');
await exec.exec('git', ['archive', '-o', zipPath, 'HEAD'], {
cwd: workspacePath,
});
const stats = await fs.stat(zipPath);
core.info(`Created zip archive: ${(stats.size / 1024 / 1024).toFixed(2)} MB`);
core.info(`Archive size: ${(stats.size / 1024 / 1024).toFixed(2)} MB`);
return zipPath;
}
async function generateIdempotencyKey(workspacePath) {
Expand All @@ -32484,44 +32484,45 @@ async function generateIdempotencyKey(workspacePath) {
output += data.toString();
},
},
silent: true,
});
const commitHash = output.trim();
const repoName = path.basename(workspacePath);
return `${repoName}:call:${commitHash}`;
return `${repoName}:supermodel:${commitHash}`;
}
async function run() {
try {
const apiKey = core.getInput('supermodel-api-key', { required: true });
const apiKey = core.getInput('supermodel-api-key', { required: true }).trim();
if (!apiKey.startsWith('smsk_')) {
core.warning('API key format looks incorrect. Get your key at https://dashboard.supermodeltools.com');
}
const commentOnPr = core.getBooleanInput('comment-on-pr');
const failOnDeadCode = core.getBooleanInput('fail-on-dead-code');
const ignorePatterns = JSON.parse(core.getInput('ignore-patterns') || '[]');
const workspacePath = process.env.GITHUB_WORKSPACE || process.cwd();
core.info('Dead Code Hunter starting...');
core.info(`Workspace: ${workspacePath}`);
// Step 1: Create zip archive
const zipPath = await createZipArchive(workspacePath);
// Step 2: Generate idempotency key
const idempotencyKey = await generateIdempotencyKey(workspacePath);
core.info(`Idempotency key: ${idempotencyKey}`);
// Step 3: Call Supermodel API
core.info('Calling Supermodel API for call graph...');
core.info('Analyzing codebase with Supermodel...');
const config = new sdk_1.Configuration({
basePath: process.env.SUPERMODEL_BASE_URL || 'https://api.supermodeltools.com',
apiKey: apiKey,
});
const api = new sdk_1.DefaultApi(config);
const zipBuffer = await fs.readFile(zipPath);
const zipBlob = new Blob([zipBuffer], { type: 'application/zip' });
const response = await api.generateCallGraph({
const response = await api.generateSupermodelGraph({
idempotencyKey,
file: zipBlob,
});
core.info(`API response received. Stats: ${JSON.stringify(response.stats)}`);
// Step 4: Analyze for dead code
const nodes = response.graph?.nodes || [];
const relationships = response.graph?.relationships || [];
const deadCode = (0, dead_code_1.findDeadCode)(nodes, relationships, ignorePatterns);
core.info(`Found ${deadCode.length} potentially dead functions`);
core.info(`Found ${deadCode.length} potentially unused functions`);
// Step 5: Set outputs
core.setOutput('dead-code-count', deadCode.length);
core.setOutput('dead-code-json', JSON.stringify(deadCode));
Expand All @@ -32537,7 +32538,7 @@ async function run() {
issue_number: github.context.payload.pull_request.number,
body: comment,
});
core.info('Posted PR comment');
core.info('Posted findings to PR');
}
else {
core.warning('GITHUB_TOKEN not available, skipping PR comment');
Expand All @@ -32547,10 +32548,19 @@ async function run() {
await fs.unlink(zipPath);
// Step 8: Fail if configured and dead code found
if (deadCode.length > 0 && failOnDeadCode) {
core.setFailed(`Found ${deadCode.length} dead code functions`);
core.setFailed(`Found ${deadCode.length} potentially unused functions`);
}
}
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');
}
else {
core.error(`API error (${status})`);
}
}
if (error instanceof Error) {
core.setFailed(error.message);
}
Expand Down
2 changes: 1 addition & 1 deletion src/dead-code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ ${rows}`;
comment += `\n\n_...and ${deadCode.length - 50} more. See action output for full list._`;
}

comment += `\n\n---\n_Powered by [Supermodel](https://supermodeltools.com) call graph analysis_`;
comment += `\n\n---\n_Powered by [Supermodel](https://supermodeltools.com) graph analysis_`;

return comment;
}
39 changes: 25 additions & 14 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ import { findDeadCode, formatPrComment } from './dead-code';
async function createZipArchive(workspacePath: string): Promise<string> {
const zipPath = path.join(workspacePath, '.dead-code-hunter-repo.zip');

core.info('Creating zip archive using git archive...');
core.info('Creating zip archive...');

await exec.exec('git', ['archive', '-o', zipPath, 'HEAD'], {
cwd: workspacePath,
});

const stats = await fs.stat(zipPath);
core.info(`Created zip archive: ${(stats.size / 1024 / 1024).toFixed(2)} MB`);
core.info(`Archive size: ${(stats.size / 1024 / 1024).toFixed(2)} MB`);

return zipPath;
}
Expand All @@ -30,35 +30,39 @@ async function generateIdempotencyKey(workspacePath: string): Promise<string> {
output += data.toString();
},
},
silent: true,
});

const commitHash = output.trim();
const repoName = path.basename(workspacePath);

return `${repoName}:call:${commitHash}`;
return `${repoName}:supermodel:${commitHash}`;
}

async function run(): Promise<void> {
try {
const apiKey = core.getInput('supermodel-api-key', { required: true });
const apiKey = core.getInput('supermodel-api-key', { required: true }).trim();

if (!apiKey.startsWith('smsk_')) {
core.warning('API key format looks incorrect. Get your key at https://dashboard.supermodeltools.com');
}

const commentOnPr = core.getBooleanInput('comment-on-pr');
const failOnDeadCode = core.getBooleanInput('fail-on-dead-code');
const ignorePatterns = JSON.parse(core.getInput('ignore-patterns') || '[]');

const workspacePath = process.env.GITHUB_WORKSPACE || process.cwd();

core.info('Dead Code Hunter starting...');
core.info(`Workspace: ${workspacePath}`);

// Step 1: Create zip archive
const zipPath = await createZipArchive(workspacePath);

// Step 2: Generate idempotency key
const idempotencyKey = await generateIdempotencyKey(workspacePath);
core.info(`Idempotency key: ${idempotencyKey}`);

// Step 3: Call Supermodel API
core.info('Calling Supermodel API for call graph...');
core.info('Analyzing codebase with Supermodel...');

const config = new Configuration({
basePath: process.env.SUPERMODEL_BASE_URL || 'https://api.supermodeltools.com',
Expand All @@ -70,20 +74,18 @@ async function run(): Promise<void> {
const zipBuffer = await fs.readFile(zipPath);
const zipBlob = new Blob([zipBuffer], { type: 'application/zip' });

const response = await api.generateCallGraph({
const response = await api.generateSupermodelGraph({
idempotencyKey,
file: zipBlob,
});

core.info(`API response received. Stats: ${JSON.stringify(response.stats)}`);

// Step 4: Analyze for dead code
const nodes = response.graph?.nodes || [];
const relationships = response.graph?.relationships || [];

const deadCode = findDeadCode(nodes, relationships, ignorePatterns);

core.info(`Found ${deadCode.length} potentially dead functions`);
core.info(`Found ${deadCode.length} potentially unused functions`);

// Step 5: Set outputs
core.setOutput('dead-code-count', deadCode.length);
Expand All @@ -103,7 +105,7 @@ async function run(): Promise<void> {
body: comment,
});

core.info('Posted PR comment');
core.info('Posted findings to PR');
} else {
core.warning('GITHUB_TOKEN not available, skipping PR comment');
}
Expand All @@ -114,10 +116,19 @@ async function run(): Promise<void> {

// Step 8: Fail if configured and dead code found
if (deadCode.length > 0 && failOnDeadCode) {
core.setFailed(`Found ${deadCode.length} dead code functions`);
core.setFailed(`Found ${deadCode.length} potentially unused functions`);
}

} 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})`);
}
}

} catch (error) {
if (error instanceof Error) {
core.setFailed(error.message);
} else {
Expand Down