# GitHub Scraper: Archived, Moved & Relicensed Repos (`aiqlabs/github-repository-audit`) Actor

Checks GitHub repositories against what the package registry claims. Catches what a registry never reports: a package served with no deprecation notice whose repository is archived, a repository that moved owner while requests keep returning 200, and a licence that changed after you installed it.

- **URL**: https://apify.com/aiqlabs/github-repository-audit.md
- **Developed by:** [Ai-Q Labs](https://apify.com/aiqlabs) (community)
- **Categories:** Developer tools, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## GitHub Repository Audit: Archived, Moved & Relicensed

Every dependency has two sides. The package registry tells you a version, a licence and — sometimes — a deprecation notice. The repository tells you whether anyone still works on it, who owns it today, and what licence the code is actually under.

Nothing keeps those two sides in sync. This Actor reads both and reports where they disagree.

### The three things a registry will never tell you

**A package that is still served normally while its repository is finished.** `cross-env` is downloaded millions of times a week. npm shows no deprecation notice. Its repository, `kentcdodds/cross-env`, has been archived — read-only, no fixes, no security patches. `npm install` looks exactly the same as it did five years ago.

**A repository that quietly moved.** Ask GitHub for `facebook/create-react-app` and you get `200 OK`. What you actually got back is `react/create-react-app`, in a different organisation. GitHub follows renames and transfers silently, so nothing in the response says the owner changed. Your `package.json`, your security policy and your vendor list all still name the old one. npm's record for `enzyme` still points at `airbnb/enzyme`, which has been `enzymejs/enzyme` for years.

**A licence that changed after you installed it.** The registry keeps the licence that was true when that version was published. The repository has whatever the licence is now. When a project relicenses — BUSL, SSPL, the Elastic Licence — the registry record for old versions does not change, and neither does your audit spreadsheet.

### What comes back

One row per dependency, about forty fields, plus three key-value records:

| Record | What is in it |
|---|---|
| **Dataset** | Every target: registry facts, repository facts, and the findings for each |
| `ACTION_LIST` | Only the rows with something to fix, hardest problems first |
| `LICENSE_REPORT` | The licence inventory, with source-available licences marked and unresolved ones named |
| `SUMMARY` | Counts, the GitHub allowance that was used, and every caveat that applies to this run |

#### Telling apart what you asked for

One call can mix three kinds of target: repositories you listed, packages you listed, and everything a dependency manifest expanded into. A manifest URL can turn one field into forty rows.

So every row and every entry starts with the same two fields:

- **`input`** — the exact string that produced this row, as you supplied it
- **`source`** — where it came from: `repos`, `packages`, or `manifest:<type>`

`SUMMARY` repeats its counts per source in `bySource`, beside the flat `byRiskLevel` totals. If a run reports two critical findings, `bySource` says which of your inputs they belong to. This matters most when a caller is not a person looking at a form — an agent reads the counts and reports them, and a count it cannot attribute is a count it cannot act on.

#### Findings

| Code | Severity | Meaning |
|---|---|---|
| `silent_abandonment` | critical | The registry shows no deprecation, the repository is archived |
| `repo_archived` | critical | Read-only; no fixes are coming |
| `repo_gone` | critical | GitHub answers 404 |
| `license_mismatch` | critical | The registry and the repository name different licences |
| `release_yanked` | critical | The exact version you depend on was withdrawn |
| `deprecated_on_registry` | high | The registry says so, with the maintainer's message |
| `license_now_source_available` | high | BUSL, SSPL, the Elastic Licence or similar |
| `known_vulnerabilities` | high | The registry lists advisory records for this version |
| `repo_moved` | medium | Renamed or transferred; requests still return 200 |
| `stale_no_push` | medium | No commit for longer than your threshold |
| `license_declared_but_absent` | medium | The registry grants a licence the repository has no file for |
| `fork_behind_upstream` | medium | You depend on a fork; the original is more active |
| `single_maintainer` | medium | One person accounts for almost every commit |
| `license_needs_review` | low | GitHub cannot classify the licence file |
| `no_license_file` | low | No licence anywhere |
| `issues_disabled` | low | No public channel to report a bug |
| `no_repository_link` | low | The registry record links to no source at all |
| `repository_not_on_github` | low | It links somewhere this Actor cannot check |
| `not_checked` | low | The GitHub allowance ran out before this row |

### Input

Give it repositories, package names, or a manifest — or all three.

```json
{
  "manifestUrl": "https://raw.githubusercontent.com/you/yours/main/package.json",
  "manifestGroups": ["dependencies", "devDependencies"],
  "onlyIssues": true
}
```

```json
{
  "packages": ["npm:cross-env", "npm:enzyme", "pypi:requests==2.31.0"],
  "repos": ["facebook/create-react-app"]
}
```

Repository references are accepted in every shape they actually occur in: `owner/name`, a browser URL, `git+ssh://git@github.com/owner/name.git`, `github:owner/name`, and a `tree/` URL deep inside a monorepo (the package directory is kept as a separate field).

Manifests: `package.json`, `requirements.txt`, `go.mod`. Lockfiles and `pyproject.toml` are deliberately **not** supported — they need a real parser, and half-parsing a manifest produces findings about dependencies you do not have.

### Rate limits, plainly

GitHub allows **60 requests an hour per IP address** without a token, and on shared infrastructure that allowance is shared with everyone else on the same machine. This Actor is built around that:

- **One request per repository.** Archived, moved, licence, fork status, last push and open issues all come from the same response.
- **Repositories are deduplicated first.** A manifest with forty `@babel/*` packages is one repository, not forty.
- **Deeper checks are opt-in.** Contributor concentration and last release each cost an extra request per repository, so they are off by default.
- **Running out is reported, not hidden.** Everything collected before the limit is kept, and `SUMMARY` says exactly how many repositories went unchecked and when the allowance resets.

Supplying a `githubToken` — any personal access token, no scopes needed — raises the limit to 5,000 an hour. It is only ever sent to `api.github.com`.

### What it will not do

- **It will not invent a licence conclusion.** Where either side is unresolvable — `SEE LICENSE IN LICENSE`, an empty PyPI licence field, a file GitHub cannot classify — `licenseMatch` is `null` and no mismatch is reported. A false "your licence changed" costs a lawyer's afternoon.
- **It will not call a 404 a deletion.** Without a token GitHub answers 404 for a private repository as well as a deleted one, and those are different problems.
- **It will not turn staleness into abandonment.** "Archived" is the maintainer's own declaration; "no commit for a year" is an inference. Only the first is strong enough for the headline finding.
- **It will not warn about healthy things.** GitHub fails to classify the licence files of lodash, jQuery UI and UglifyJS — all ordinary MIT/BSD projects whose text carries an extra paragraph. That note exists, at the lowest severity, so it can never sit above a real problem.
- **It does not scrape.** Three documented JSON APIs, no key required for any of them: `api.github.com`, `registry.npmjs.org`, `pypi.org`.

### Sources

| | Endpoint | Key needed |
|---|---|---|
| GitHub | `api.github.com/repos/{owner}/{name}` | No (optional token raises the limit) |
| npm | `registry.npmjs.org/{name}/{version or latest}` | No |
| PyPI | `pypi.org/pypi/{name}/{version}/json` | No |

Where a manifest pins an exact version, that version is read rather than the newest one — it is what you actually depend on, and the per-version document is a few kilobytes where the whole-project document can be three megabytes.

***

Built by **Ai-Q Labs**. Other Actors: [Dead Link Checker](https://apify.com/aiqlabs/dead-link-checker), [Article Extractor](https://apify.com/aiqlabs/article-extractor), [SEO Audit](https://apify.com/aiqlabs/seo-audit-tool), [PDF Table Extractor](https://apify.com/aiqlabs/pdf-table-extractor), [Public Procurement Data](https://apify.com/aiqlabs/public-procurement-data).

# Actor input Schema

## `repos` (type: `array`):

GitHub repositories to audit. Any of these forms work: owner/name, https://github.com/owner/name, git+ssh://git@github.com/owner/name.git, github:owner/name. Every output row carries a "source" field naming which input it came from, so rows from this list read as source=repos. Nothing is audited that you did not name here, in Packages, or in a manifest. A repository on its own has no registry side, so registryDeprecated, licenseMatch and silentAbandonment come back null on these rows because they could not be checked, not because they came back clean. Give the package name in Packages instead if you need those three.

## `packages` (type: `array`):

Package names to audit. Prefix with the registry - npm:express, pypi:requests - or leave the prefix off and set the default registry below. Each package is read from its registry, then checked against the repository it points at. Every output row carries a "source" field naming which input it came from, so rows from this list read as source=packages.

## `defaultRegistry` (type: `string`):

Which registry to assume for package names given without a prefix.

## `manifestUrl` (type: `string`):

A raw package.json, requirements.txt or go.mod to read the dependency list from. Use the raw file URL, not the GitHub page.

## `manifestText` (type: `string`):

Paste a package.json, requirements.txt or go.mod here instead of hosting it somewhere. Takes precedence over the manifest URL.

## `manifestType` (type: `string`):

Leave empty to detect it from the file name and contents. pyproject.toml and lockfiles are not supported - they need a real parser, and half-parsing a manifest produces findings about dependencies you do not have.

## `manifestGroups` (type: `array`):

Which dependency blocks to take from a package.json.

## `githubToken` (type: `string`):

Without a token GitHub allows 60 requests an hour per IP address, and on shared infrastructure that allowance is shared with other tenants. Any personal access token with no scopes raises it to 5,000 an hour. The token is only sent to api.github.com.

## `staleAfterDays` (type: `integer`):

How long without a pushed commit before a repository is reported as stale. This is a judgement call, so it is reported at medium severity and never used for the archived-but-not-deprecated finding, which relies only on the maintainer's own archive flag.

## `includeContributors` (type: `boolean`):

Adds one GitHub request per repository and reports what share of commits the single busiest contributor accounts for.

## `includeReleases` (type: `boolean`):

Adds one GitHub request per repository and reports the newest tagged release, which is often years behind the newest commit.

## `onlyIssues` (type: `boolean`):

Leaves out the dependencies that came back clean.

## `maxTargets` (type: `integer`):

A ceiling on how many packages or repositories one run will look at.

## `maxConcurrency` (type: `integer`):

Kept low on purpose. Running wider does not buy extra GitHub allowance, and these are other people's servers.

## `requestTimeoutSecs` (type: `integer`):

How long to wait for any single API response before giving up on it.

## Actor input object example

```json
{
  "repos": [
    "facebook/create-react-app",
    "babel/babel-eslint"
  ],
  "packages": [
    "npm:request",
    "npm:babel-eslint",
    "npm:left-pad"
  ],
  "defaultRegistry": "npm",
  "manifestType": "",
  "manifestGroups": [
    "dependencies"
  ],
  "staleAfterDays": 365,
  "includeContributors": false,
  "includeReleases": false,
  "onlyIssues": false,
  "maxTargets": 200,
  "maxConcurrency": 4,
  "requestTimeoutSecs": 30
}
```

# Actor output Schema

## `results` (type: `string`):

No description

## `csv` (type: `string`):

No description

## `actionList` (type: `string`):

No description

## `licenseReport` (type: `string`):

No description

## `summary` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "repos": [
        "facebook/create-react-app",
        "babel/babel-eslint"
    ],
    "packages": [
        "npm:request",
        "npm:babel-eslint",
        "npm:left-pad"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("aiqlabs/github-repository-audit").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "repos": [
        "facebook/create-react-app",
        "babel/babel-eslint",
    ],
    "packages": [
        "npm:request",
        "npm:babel-eslint",
        "npm:left-pad",
    ],
}

# Run the Actor and wait for it to finish
run = client.actor("aiqlabs/github-repository-audit").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "repos": [
    "facebook/create-react-app",
    "babel/babel-eslint"
  ],
  "packages": [
    "npm:request",
    "npm:babel-eslint",
    "npm:left-pad"
  ]
}' |
apify call aiqlabs/github-repository-audit --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,aiqlabs/github-repository-audit"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/8mTGNip79blZxXffn/builds/AhZvisnvQVUvY6W0S/openapi.json
