From 74855e0590d34a15784b04ce21c9625ac26b7ec7 Mon Sep 17 00:00:00 2001 From: Arnaud Jeansen Date: Fri, 7 Nov 2025 11:21:57 +0100 Subject: [PATCH] Add organization support for GitHub Actions artifacts analysis MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a new `analyze-org` command to analyze GitHub Actions artifacts across all repositories in an organization, filling a key gap where the tool previously only supported individual user repositories. New Features: - New command: `analyze-org ` to analyze organization repositories - Supports both public and private organization repositories - Excludes forked repositories (consistent with user repo behavior) - Shows organization name in analysis results Implementation: - Added analyzeOrganizationRepositories() method to analyzer - Updated CLI with new analyze-org command - Enhanced error handling for organization-specific scenarios - Updated documentation with usage examples Token Requirements: - Requires `read:org` scope for organization access - May require SSO authorization for SSO-enabled organizations Breaking Changes: None - fully backwards compatible Usage: github-artifacts analyze-org my-company-org github-artifacts analyze-org my-org --cleanup --top 20 šŸ¤– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- README.md | 29 +++++++++++++++-- src/analyzer.ts | 83 +++++++++++++++++++++++++++++++++++++++++++++++-- src/index.ts | 54 ++++++++++++++++++++++++++++++-- src/reporter.ts | 6 +++- 4 files changed, 165 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index ad56309..de9003e 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ npm run build - `repo` (Full control of private repositories) - `read:user` (Read access to user profile data) - `actions:read` (Read access to actions and workflows) + - `read:org` (Read organization membership - required for organization analysis) ### 2. Set Environment Variable (Recommended) @@ -94,6 +95,21 @@ github-artifacts analyze --format csv --output results.csv github-artifacts analyze --cleanup ``` +### Analyze Organization +```bash +# Analyze all repositories in an organization +github-artifacts analyze-org my-company-org + +# Show top 20 repositories +github-artifacts analyze-org my-org --top 20 + +# Export to JSON +github-artifacts analyze-org my-org --format json --output org-results.json + +# Interactive cleanup mode for organization +github-artifacts analyze-org my-org --cleanup +``` + ### Analyze Specific Repository ```bash # Analyze a single repository @@ -123,6 +139,14 @@ github-artifacts repo shanselman hanselminutes-core --cleanup - `--top `: Show top N repositories by storage usage [default: 10] - `--cleanup`: Interactive cleanup mode - delete artifacts to save space [default: false] +#### Analyze Organization Command Options +- `-f, --format `: Output format (table|json|csv) [default: table] +- `-o, --output `: Output file path +- `--include-expired`: Include expired artifacts in analysis [default: false] +- `--min-size `: Minimum artifact size to include in bytes [default: 0] +- `--top `: Show top N repositories by storage usage [default: 10] +- `--cleanup`: Interactive cleanup mode - delete artifacts to save space [default: false] + #### Repository Command Options - `-f, --format `: Output format (table|json|csv) [default: table] - `--include-expired`: Include expired artifacts in analysis [default: false] @@ -285,7 +309,8 @@ MIT License - see LICENSE file for details. **"Access forbidden - check token permissions"** - Verify your token has `repo`, `read:user`, and `actions:read` scopes -- For organization repositories, you might need additional permissions +- For organization repositories, you also need the `read:org` scope +- Some organizations require SSO authorization for tokens - you may need to authorize your token **"No artifacts found"** - Repository might not have any GitHub Actions workflows @@ -301,13 +326,13 @@ MIT License - see LICENSE file for details. ## šŸ“ˆ Roadmap +- [x] Organization-wide analysis - [ ] Bulk artifact deletion functionality - [ ] Integration with GitHub CLI - [ ] Webhook support for real-time monitoring - [ ] Dashboard web interface - [ ] Artifact content analysis - [ ] Cost estimation features -- [ ] Organization-wide analysis --- diff --git a/src/analyzer.ts b/src/analyzer.ts index 6d1c02b..a30b223 100644 --- a/src/analyzer.ts +++ b/src/analyzer.ts @@ -38,7 +38,7 @@ class GitHubArtifactsAnalyzer { hasMore = false; } else { // Filter to only repos owned by the target user (not organizations) - const userRepos = repos.filter(repo => + const userRepos = repos.filter(repo => repo.owner.login === username && !repo.fork ); @@ -102,7 +102,7 @@ class GitHubArtifactsAnalyzer { try { const analysis = await this.analyzeRepository(repo.owner.login, repo.name, options); repositories.push(analysis); - + if (analysis.totalArtifacts > 0) { console.log(chalk.green(` āœ“ Found ${analysis.totalArtifacts} artifacts (${this.formatBytes(analysis.totalSizeBytes)})`)); } @@ -126,6 +126,85 @@ class GitHubArtifactsAnalyzer { }; } + async analyzeOrganizationRepositories( + orgName: string, + options = { + includeExpired: false, + minSize: 0 + } + ) { + console.log(chalk.blue(`\nšŸ“Š Analyzing organization: ${orgName}\n`)); + + const repositories = []; + let page = 1; + let hasMore = true; + + while (hasMore) { + try { + // Fetch organization repositories + const { data: repos } = await this.octokit.repos.listForOrg({ + org: orgName, + type: 'all', // all, public, private, forks, sources, member + per_page: 100, + page, + sort: 'updated' + }); + + if (repos.length === 0) { + hasMore = false; + } else { + // Filter out forks (keep only source repos) + const filteredRepos = repos.filter(repo => !repo.fork); + + // Process each repository + for (const repo of filteredRepos) { + console.log(chalk.gray(` Checking ${repo.full_name}${repo.private ? ' (private)' : ''}...`)); + + try { + const analysis = await this.analyzeRepository( + repo.owner.login, + repo.name, + { + includeExpired: options.includeExpired ?? false, + minSize: options.minSize ?? 0 + } + ); + repositories.push(analysis); + + if (analysis.totalArtifacts > 0) { + console.log(chalk.green( + ` āœ“ Found ${analysis.totalArtifacts} artifacts (${this.formatBytes(analysis.totalSizeBytes)})` + )); + } + } catch (error) { + console.log(chalk.yellow(` ⚠ Skipped (${error?.message || 'Unknown error'})`)); + } + + // Rate limit protection + await this.sleep(100); + } + page++; + } + } catch (error) { + if (error.status === 404) { + throw new Error(`Organization '${orgName}' not found or you don't have access`); + } else if (error.status === 403) { + throw new Error('Access forbidden - check token has read:org permission'); + } + throw error; + } + } + + // Calculate summary + const summary = this.calculateSummary(repositories); + + return { + organizationName: orgName, + repositories, + summary + }; + } + async analyzeRepository(owner, repo, options = { includeExpired: false, minSize: 0 }) { const analysis = { owner, diff --git a/src/index.ts b/src/index.ts index a8fd4c8..9b323f8 100755 --- a/src/index.ts +++ b/src/index.ts @@ -36,12 +36,13 @@ program } const spinner = ora('Initializing GitHub API...').start(); - + try { const analyzer = new GitHubArtifactsAnalyzer(token); const reporter = new ReportGenerator(); spinner.text = 'Fetching repositories...'; + const analysis = await analyzer.analyzeAllRepositories( options.username, { @@ -86,7 +87,7 @@ program } const spinner = ora(`Analyzing ${owner}/${repo}...`).start(); - + try { const analyzer = new GitHubArtifactsAnalyzer(token); const reporter = new ReportGenerator(); @@ -113,6 +114,55 @@ program } }); +program + .command('analyze-org') + .description('Analyze artifacts across all repositories in an organization') + .argument('', 'GitHub organization name') + .option('-t, --token ', 'GitHub Personal Access Token (or set GITHUB_TOKEN env var)') + .option('-f, --format ', 'Output format (table|json|csv)', 'table') + .option('-o, --output ', 'Output file path') + .option('--include-expired', 'Include expired artifacts in analysis', false) + .option('--min-size ', 'Minimum artifact size to include (in bytes)', '0') + .option('--top ', 'Show top N repositories by storage usage', '10') + .option('--cleanup', 'Interactive cleanup mode - delete artifacts to save space', false) + .action(async (org, options) => { + const token = options.token || process.env.GITHUB_TOKEN; + if (!token) { + console.error(chalk.red('Error: GitHub token is required. Use --token or set GITHUB_TOKEN environment variable')); + process.exit(1); + } + + const spinner = ora(`Analyzing organization: ${org}...`).start(); + + try { + const analyzer = new GitHubArtifactsAnalyzer(token); + const reporter = new ReportGenerator(); + + spinner.text = 'Fetching organization repositories...'; + const analysis = await analyzer.analyzeOrganizationRepositories(org, { + includeExpired: options.includeExpired, + minSize: parseInt(options.minSize), + }); + + spinner.succeed('Analysis complete!'); + + if (options.cleanup) { + await reporter.runCleanupMode(analysis, analyzer); + } else { + await reporter.generateReport(analysis, { + format: options.format, + outputFile: options.output, + topCount: parseInt(options.top), + }); + } + + } catch (error) { + spinner.fail('Analysis failed'); + console.error(chalk.red('Error:'), error?.message || 'Unknown error'); + process.exit(1); + } + }); + if (import.meta.url === `file://${process.argv[1]}`) { program.parse(); } diff --git a/src/reporter.ts b/src/reporter.ts index 9c51bdd..ea6f325 100644 --- a/src/reporter.ts +++ b/src/reporter.ts @@ -36,7 +36,11 @@ class ReportGenerator { generateTableReport(analysis, topCount) { // Summary table - console.log(chalk.bold.blue('\nšŸš€ GitHub Artifacts Storage Analysis Summary')); + const title = analysis.organizationName + ? `šŸš€ GitHub Artifacts Analysis - Organization: ${analysis.organizationName}` + : 'šŸš€ GitHub Artifacts Storage Analysis Summary'; + + console.log(chalk.bold.blue(`\n${title}`)); console.log(chalk.gray('='.repeat(60))); const summaryTable = new Table({