# Polyglot Manifest License and End-of-Life Gate (`kingii98/polyglot-manifest-license-and-end-of-life-gate`) Actor

Read manifest files from more than one ecosystem. Report the license, the deprecation flag and the abandonment age of each package in one table.

- **URL**: https://apify.com/kingii98/polyglot-manifest-license-and-end-of-life-gate.md
- **Developed by:** [kingii98](https://apify.com/kingii98) (community)
- **Categories:**
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $20.00 / 1,000 manifest set run starts

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

## Polyglot Manifest License and End-of-Life Gate

Read manifest files from more than one ecosystem. Report the license, the
deprecation flag and the abandonment age of each package in one table.

A team that runs Node, Python, Go and Rust must usually run one license tool
for each ecosystem, and must then join the results by hand. This Actor reads
all four manifest kinds in one run and writes one flat table, plus one run
summary with a pass or fail gate verdict.

### Who this is for

An engineering lead or a legal reviewer at a small polyglot team. This person
must approve a release, or must answer a due-diligence question, and has no
commercial software composition analysis platform.

Run it once for each release tag, once each month as a compliance sweep, or
once before a due-diligence hand-over. The manifest set stays the same, so the
run repeats with no new setup.

### What the Actor does

1. Reads 1 to 25 manifest files. Give each one as pasted text or as a public
   URL.
2. Parses each manifest and finds the packages it names.
3. Resolves each package against its public registry: npm, PyPI, crates.io or
   the Go module proxy with deps.dev.
4. Writes one dataset row for each package in each manifest, and one run
   summary record.
5. Stores a baseline document for the manifest set, so the next run can name
   what changed.

The Actor makes HTTP GET calls only. It does not clone a repository, install a
package, or run any manifest content. It uses no browser, no proxy, no
language model and no paid API.

### Supported manifest types

| Manifest type | Ecosystem | Registry |
| --- | --- | --- |
| `package.json` | npm | registry.npmjs.org |
| `package-lock.json` | npm | registry.npmjs.org |
| `requirements.txt` | PyPI | pypi.org |
| `go.mod` | Go | proxy.golang.org and api.deps.dev |
| `Cargo.toml` | crates.io | crates.io |

The manifest type comes from the file name, so `services/api/go.mod` is a
`go.mod`. You can also state the `type` field when the file name does not say.

### Input

| Field | Type | Default | Meaning |
| --- | --- | --- | --- |
| `manifests` | array | four-ecosystem sample set | 1 to 25 manifests. Each item takes `name`, an optional `type`, and either `content` (pasted text) or `url` (a public URL). |
| `licensePolicy` | array of strings | `["GPL-3.0-only", "AGPL-3.0-only"]` | SPDX identifiers the report must flag. |
| `abandonmentDays` | integer | `730` | A package with no new release in this many days gets the abandoned flag. |
| `includeTransitive` | boolean | `false` | Resolve one level of transitive dependencies. |
| `failOnDeprecated` | boolean | `true` | A deprecated package counts as a policy failure. |
| `failOnAbandoned` | boolean | `false` | An abandoned package counts as a policy failure. |
| `failOnUnknownLicense` | boolean | `false` | A license the Actor cannot read as SPDX counts as a policy failure. |
| `baselineKey` | string | `""` | Key-value store key for the baseline. Empty means the Actor derives one key from the manifest names. |

Every field has a default, so a run with empty input `{}` succeeds and reports
the sample manifest set.

#### A manifest given as a URL

```json
{
  "manifests": [
    { "name": "go.mod", "url": "https://raw.githubusercontent.com/owner/repo/main/go.mod" }
  ]
}
```

A URL must be public. The Actor refuses loopback, private and reserved
addresses, refuses credentials in the URL, checks every redirect hop against
the same rule, allows at most 3 redirects, and reads at most 2 MB for each
manifest.

### Output

#### Package rows

One row for each package in each manifest.

| Field | Meaning |
| --- | --- |
| `recordType` | Always `package`. |
| `manifestName` | The manifest the package came from. |
| `ecosystem` | `npm`, `PyPI`, `Go` or `crates.io`. |
| `packageName` | The package name. |
| `dependencyKind` | `DIRECT` or `TRANSITIVE`. |
| `requestedRange` | The range the manifest states. |
| `resolvedVersion` | The version the Actor reports on. |
| `latestVersion` | The newest version the registry lists. |
| `spdxLicense` | The SPDX expression, or `UNKNOWN`. |
| `licenseIdentifiers` | The SPDX identifiers in the expression. |
| `licensePolicyVerdict` | `PASS`, `FAIL` or `UNKNOWN`. |
| `deniedLicenseIdentifiers` | The identifiers your policy denies. |
| `deprecated` | True when the registry marks the release deprecated. |
| `deprecationMessage` | The message the registry gives. |
| `lastPublishDate` | The newest release date of the package. |
| `daysSinceLastPublish` | Days from that date to the run. |
| `abandoned` | True when `daysSinceLastPublish` reaches `abandonmentDays`. |
| `repositoryUrl` | The source repository the registry states. |
| `repositoryArchived` | Always `null` in version 1. See the limitations. |
| `repositoryArchivedStatus` | Always `NOT_CHECKED` in version 1. |
| `resolutionStatus` | `RESOLVED`, `NOT_FOUND` or `ERROR`. |
| `resolutionError` | The reason the package did not resolve. |
| `policyFailure` | True when the row breaks the policy. |
| `policyFailureReasons` | One line for each reason. |
| `baselineStatus` | `BASELINE_CREATED`, `NEW`, `CHANGED` or `UNCHANGED`. |
| `changesSinceBaseline` | Field, previous value and current value for each change. |

#### Run summary

One record with `recordType` set to `run-summary`. It carries `gateVerdict`
(`PASS` or `FAIL`), `packageCountByEcosystem`, `policyFailureCount`,
`deprecatedCount`, `abandonedCount`, `unresolvedPackageCount`,
`resolutionRate`, `manifestErrors`, the baseline counts
(`baselineFirstRun`, `changedPackageCount`, `newPackageCount`,
`removedPackageCount`), and `truncated` with `packageLimit`.

The gate verdict is `FAIL` when at least one row is a policy failure.

`truncated` is `true` when the manifest set holds more packages than
`packageLimit`, and the table therefore shows only the first `packageLimit`
packages. Read it before you treat the table as a complete report.

### The baseline

The Actor keeps one baseline document for each manifest set in the named
key-value store `pkg-eol-gate-baselines`. The baseline holds the resolved
version, the license, the license verdict, the deprecation flag and the
abandonment flag of each package. One baseline slot is one package inside one
manifest (`ecosystem|package|manifest`), because two manifests can request
different ranges of the same package and therefore hold different values.

- The first run writes the baseline and reports the full table. Every row gets
  `BASELINE_CREATED` and reports no change.
- A later run compares each row against the baseline and marks it `NEW`,
  `CHANGED` or `UNCHANGED`. A changed row names each field that moved.

The default store cannot hold the baseline, because Apify purges it when a run
starts.

### The run always succeeds

A failed gate, an unreachable manifest URL, a package the registry does not
know, and an empty manifest are all report results, not malfunctions. The run
ends with status SUCCEEDED and carries the verdict in the status message and
in the run-summary record. Read `gateVerdict` to gate a release.

Invalid input is reported the same way: the Actor writes one record with
`gateVerdict` set to `ERROR` and an `inputError` message, and the run
succeeds.

### Pricing: pay per event

| Event | Unit | Counted as |
| --- | --- | --- |
| `manifest-set-run-start` | one run | Charged once, at the start of the run. |
| `package-resolved` | one unique package resolved against a registry | One charge for each unique (ecosystem, package name) pair sent to a registry. A package that two manifests name is charged once, also when the two manifests request different ranges of it. A lookup that ends `NOT_FOUND` or `ERROR` was still sent to a registry, so it is charged. |
| `policy-failure-recorded` | one policy failure written to the report | One charge for each failing row in the dataset. A failing package that two manifests name writes two rows, so it is charged twice. |

### Bounds

| Bound | Value |
| --- | --- |
| Manifests for each run | 25 |
| Characters for each pasted manifest | 2 000 000 |
| Bytes for each fetched manifest | 2 000 000 |
| Packages for each run | 1 500 |
| Redirects for each manifest URL | 3 |
| Registry requests in parallel | 6 |
| Timeout for each request | 25 s |
| Bytes for each registry response | 8 000 000 |
| Denied SPDX identifiers | 100 |

### Version 1 limitations

- **Version resolution is a documented rule, not a solver.** Each manifest is
  resolved against the range that it states, so two manifests that request
  different majors of one package get their own row, version and license. An
  exact version in the manifest wins. A caret or tilde range takes the highest registry
  release inside the range. Anything else takes the highest stable release.
  This is not what a real package manager resolves in every case. For an exact
  answer, give a lock file.
- **The repository archived flag is not read.** Version 1 makes no call to a
  source forge. The GitHub API does give `archived` without a token, but only
  at 60 requests each hour for each address, which a shared Actor cannot hold
  to. The column is present and holds `null`, with `repositoryArchivedStatus`
  set to `NOT_CHECKED`.
- **PyPI and crates.io have no deprecation flag.** A yanked release is the
  nearest equivalent, and it carries the same instruction: do not use this
  release. The Actor reports a yanked release as deprecated and says so in
  `deprecationMessage`.
- **Transitive resolution covers npm and PyPI only.** Those two registries
  state a package's dependencies in the same document the Actor already reads.
  crates.io does not, so a Cargo transitive level would need one more call for
  each crate. A Go `go.mod` already lists its indirect requirements, and the
  Actor keeps them when `includeTransitive` is on.
- **A license the registry does not state as SPDX is reported as `UNKNOWN`,
  not guessed.** Turn on `failOnUnknownLicense` when an unread license must
  stop a release.
- **The abandonment age measures the package, not the pinned release.** It
  uses the newest release date of the whole package, which is the right
  measure for "is this project still maintained".

### Local development

```bash
uv sync
uv run pytest
uv run ruff check .
```

# Actor input Schema

## `manifests` (type: `array`):

1 to 25 manifest files. Give each one as pasted text (content) or as a public URL (url). Supported types: package.json, package-lock.json, requirements.txt, go.mod and Cargo.toml. The type comes from the file name, or you can state it. No repository clone. No package install.

## `licensePolicy` (type: `array`):

SPDX identifiers that the report must flag, for example GPL-3.0-only and AGPL-3.0-only. A package whose license matches one of these gets the FAIL verdict. Deprecated identifiers such as GPL-3.0 are matched as well.

## `abandonmentDays` (type: `integer`):

A package with no new release in this many days gets the abandoned flag.

## `includeTransitive` (type: `boolean`):

Resolve one level of transitive dependencies for npm and PyPI packages, and keep the transitive entries that a lock file or a go.mod indirect marker already states. Off by default, because it multiplies the package count and the run cost.

## `failOnDeprecated` (type: `boolean`):

Count a package that the registry marks deprecated (or a yanked PyPI or crates.io release) as a policy failure.

## `failOnAbandoned` (type: `boolean`):

Count a package that is older than the abandonment age as a policy failure. Off by default, because an old release is often a stable release.

## `failOnUnknownLicense` (type: `boolean`):

Count a package whose license the registry does not state as an SPDX identifier as a policy failure.

## `baselineKey` (type: `string`):

Optional key-value store key that holds the baseline for this manifest set. Leave it empty and the Actor derives one key from the manifest names, so the same manifest set finds its own baseline on the next run.

## Actor input object example

```json
{
  "manifests": [
    {
      "name": "web/package.json",
      "type": "package.json",
      "content": "{\n  \"name\": \"demo-web\",\n  \"dependencies\": {\n    \"express\": \"^4.18.2\",\n    \"request\": \"^2.88.2\",\n    \"left-pad\": \"^1.3.0\"\n  }\n}\n"
    },
    {
      "name": "api/requirements.txt",
      "type": "requirements.txt",
      "content": "requests==2.31.0\ndjango>=4.2\n"
    },
    {
      "name": "gateway/go.mod",
      "type": "go.mod",
      "content": "module example.com/demo\n\ngo 1.21\n\nrequire (\n\tgithub.com/gorilla/mux v1.8.1\n\tgithub.com/sirupsen/logrus v1.9.3\n)\n"
    },
    {
      "name": "worker/Cargo.toml",
      "type": "Cargo.toml",
      "content": "[package]\nname = \"demo-worker\"\nversion = \"0.1.0\"\n\n[dependencies]\nserde = \"1.0\"\nrand = \"0.8\"\n"
    }
  ],
  "licensePolicy": [
    "GPL-3.0-only",
    "AGPL-3.0-only"
  ],
  "abandonmentDays": 730,
  "includeTransitive": false,
  "failOnDeprecated": true,
  "failOnAbandoned": false,
  "failOnUnknownLicense": false,
  "baselineKey": ""
}
```

# Actor output Schema

## `dataset` (type: `string`):

One row for each package in each manifest, plus one run-summary record that carries the gate verdict.

## `baseline` (type: `string`):

The named key-value store 'pkg-eol-gate-baselines' holds one baseline document for each manifest set, so the next run can name what changed.

# 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 = {
    "manifests": [
        {
            "name": "web/package.json",
            "type": "package.json",
            "content": "{\n  \"name\": \"demo-web\",\n  \"dependencies\": {\n    \"express\": \"^4.18.2\",\n    \"request\": \"^2.88.2\",\n    \"left-pad\": \"^1.3.0\"\n  }\n}\n"
        },
        {
            "name": "api/requirements.txt",
            "type": "requirements.txt",
            "content": "requests==2.31.0\ndjango>=4.2\n"
        },
        {
            "name": "gateway/go.mod",
            "type": "go.mod",
            "content": "module example.com/demo\n\ngo 1.21\n\nrequire (\n\tgithub.com/gorilla/mux v1.8.1\n\tgithub.com/sirupsen/logrus v1.9.3\n)\n"
        },
        {
            "name": "worker/Cargo.toml",
            "type": "Cargo.toml",
            "content": "[package]\nname = \"demo-worker\"\nversion = \"0.1.0\"\n\n[dependencies]\nserde = \"1.0\"\nrand = \"0.8\"\n"
        }
    ],
    "licensePolicy": [
        "GPL-3.0-only",
        "AGPL-3.0-only"
    ],
    "abandonmentDays": 730,
    "includeTransitive": false,
    "failOnDeprecated": true,
    "failOnAbandoned": false,
    "failOnUnknownLicense": false,
    "baselineKey": ""
};

// Run the Actor and wait for it to finish
const run = await client.actor("kingii98/polyglot-manifest-license-and-end-of-life-gate").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 = {
    "manifests": [
        {
            "name": "web/package.json",
            "type": "package.json",
            "content": """{
  \"name\": \"demo-web\",
  \"dependencies\": {
    \"express\": \"^4.18.2\",
    \"request\": \"^2.88.2\",
    \"left-pad\": \"^1.3.0\"
  }
}
""",
        },
        {
            "name": "api/requirements.txt",
            "type": "requirements.txt",
            "content": """requests==2.31.0
django>=4.2
""",
        },
        {
            "name": "gateway/go.mod",
            "type": "go.mod",
            "content": """module example.com/demo

go 1.21

require (
\tgithub.com/gorilla/mux v1.8.1
\tgithub.com/sirupsen/logrus v1.9.3
)
""",
        },
        {
            "name": "worker/Cargo.toml",
            "type": "Cargo.toml",
            "content": """[package]
name = \"demo-worker\"
version = \"0.1.0\"

[dependencies]
serde = \"1.0\"
rand = \"0.8\"
""",
        },
    ],
    "licensePolicy": [
        "GPL-3.0-only",
        "AGPL-3.0-only",
    ],
    "abandonmentDays": 730,
    "includeTransitive": False,
    "failOnDeprecated": True,
    "failOnAbandoned": False,
    "failOnUnknownLicense": False,
    "baselineKey": "",
}

# Run the Actor and wait for it to finish
run = client.actor("kingii98/polyglot-manifest-license-and-end-of-life-gate").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 '{
  "manifests": [
    {
      "name": "web/package.json",
      "type": "package.json",
      "content": "{\\n  \\"name\\": \\"demo-web\\",\\n  \\"dependencies\\": {\\n    \\"express\\": \\"^4.18.2\\",\\n    \\"request\\": \\"^2.88.2\\",\\n    \\"left-pad\\": \\"^1.3.0\\"\\n  }\\n}\\n"
    },
    {
      "name": "api/requirements.txt",
      "type": "requirements.txt",
      "content": "requests==2.31.0\\ndjango>=4.2\\n"
    },
    {
      "name": "gateway/go.mod",
      "type": "go.mod",
      "content": "module example.com/demo\\n\\ngo 1.21\\n\\nrequire (\\n\\tgithub.com/gorilla/mux v1.8.1\\n\\tgithub.com/sirupsen/logrus v1.9.3\\n)\\n"
    },
    {
      "name": "worker/Cargo.toml",
      "type": "Cargo.toml",
      "content": "[package]\\nname = \\"demo-worker\\"\\nversion = \\"0.1.0\\"\\n\\n[dependencies]\\nserde = \\"1.0\\"\\nrand = \\"0.8\\"\\n"
    }
  ],
  "licensePolicy": [
    "GPL-3.0-only",
    "AGPL-3.0-only"
  ],
  "abandonmentDays": 730,
  "includeTransitive": false,
  "failOnDeprecated": true,
  "failOnAbandoned": false,
  "failOnUnknownLicense": false,
  "baselineKey": ""
}' |
apify call kingii98/polyglot-manifest-license-and-end-of-life-gate --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,kingii98/polyglot-manifest-license-and-end-of-life-gate"
        }
    }
}

```

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/3fOYoGTFZWMvxexHv/builds/Lb6NTH6JFGIW7dsMz/openapi.json
