diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f55b0d6..fba3e1d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/README.md b/README.md index 365aa7e..6640e99 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 │ diff --git a/dist/index.js b/dist/index.js index 348965c..c0f9242 100644 --- a/dist/index.js +++ b/dist/index.js @@ -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; } @@ -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) { @@ -32484,27 +32484,29 @@ 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, @@ -32512,16 +32514,15 @@ async function run() { 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)); @@ -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'); @@ -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); } diff --git a/src/dead-code.ts b/src/dead-code.ts index aad61f9..77d41e3 100644 --- a/src/dead-code.ts +++ b/src/dead-code.ts @@ -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; } diff --git a/src/index.ts b/src/index.ts index d82644c..d9ce640 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,14 +9,14 @@ import { findDeadCode, formatPrComment } from './dead-code'; async function createZipArchive(workspacePath: string): Promise { 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; } @@ -30,17 +30,23 @@ async function generateIdempotencyKey(workspacePath: string): Promise { 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 { 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') || '[]'); @@ -48,17 +54,15 @@ async function run(): Promise { 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', @@ -70,20 +74,18 @@ async function run(): Promise { 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); @@ -103,7 +105,7 @@ async function run(): Promise { body: comment, }); - core.info('Posted PR comment'); + core.info('Posted findings to PR'); } else { core.warning('GITHUB_TOKEN not available, skipping PR comment'); } @@ -114,10 +116,19 @@ async function run(): Promise { // 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 {