Skip to content
Open
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
29 changes: 27 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -123,6 +139,14 @@ github-artifacts repo shanselman hanselminutes-core --cleanup
- `--top <count>`: 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 <format>`: Output format (table|json|csv) [default: table]
- `-o, --output <file>`: Output file path
- `--include-expired`: Include expired artifacts in analysis [default: false]
- `--min-size <bytes>`: Minimum artifact size to include in bytes [default: 0]
- `--top <count>`: 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 <format>`: Output format (table|json|csv) [default: table]
- `--include-expired`: Include expired artifacts in analysis [default: false]
Expand Down Expand Up @@ -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
Expand All @@ -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

---

Expand Down
83 changes: 81 additions & 2 deletions src/analyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
);

Expand Down Expand Up @@ -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)})`));
}
Expand All @@ -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,
Expand Down
54 changes: 52 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
{
Expand Down Expand Up @@ -86,7 +87,7 @@ program
}

const spinner = ora(`Analyzing ${owner}/${repo}...`).start();

try {
const analyzer = new GitHubArtifactsAnalyzer(token);
const reporter = new ReportGenerator();
Expand All @@ -113,6 +114,55 @@ program
}
});

program
.command('analyze-org')
.description('Analyze artifacts across all repositories in an organization')
.argument('<org>', 'GitHub organization name')
.option('-t, --token <token>', 'GitHub Personal Access Token (or set GITHUB_TOKEN env var)')
.option('-f, --format <format>', 'Output format (table|json|csv)', 'table')
.option('-o, --output <file>', 'Output file path')
.option('--include-expired', 'Include expired artifacts in analysis', false)
.option('--min-size <bytes>', 'Minimum artifact size to include (in bytes)', '0')
.option('--top <count>', '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();
}
Expand Down
6 changes: 5 additions & 1 deletion src/reporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down