# Dependency Release Cooldown Admission Ledger (`kingii98/dependency-release-cooldown-admission-ledger`) Actor

Check each pinned dependency version against a release-age cooldown policy. Report which pins are still inside the window, and the date on which each one becomes admissible.

- **URL**: https://apify.com/kingii98/dependency-release-cooldown-admission-ledger.md
- **Developed by:** [kingii98](https://apify.com/kingii98) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $12.00 / 1,000 run\_starteds

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?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## 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.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Dependency Release Cooldown Admission Ledger for Mixed Ecosystems

Check each pinned dependency version against a release-age cooldown policy.
The Actor reports which pins are still inside the cooldown window, and the
date on which each one becomes admissible.

A cooldown policy says: do not adopt a release until it is at least N days
old. `uv` applies such a policy with its `exclude-newer` setting, and other
tools block fresh releases by default. Other ecosystems have no such setting:
the Maven versions plugin, for example, has no release-age filter. This Actor
applies one policy across npm, PyPI, Maven Central and NuGet, and gives the
part that the native tools leave out: the date on which each blocked pin
becomes admissible. A blocked list is a stop sign; a dated calendar is a plan.

### What it does

1. Reads a bounded list of pins in the form `ecosystem:name@version`.
2. Asks each package registry for the release record of the package.
3. Compares the publish timestamp of the pinned version with the as-of date.
4. Writes one dataset row for each pin, one run summary, and one admission
   calendar when at least one pin is blocked.

The verdict is a function of three values only: the publish timestamp, the
as-of date and the cooldown in days. The Actor holds no state between runs, so
a run made today with an as-of date of last month gives the same answer as the
run made last month. That is what an audit needs.

### Input

| Field | Type | Default | Meaning |
| --- | --- | --- | --- |
| `pins` | array of text | five example pins | 1 to 1000 pins, each one `ecosystem:name@version`. One entry may hold several lines. |
| `cooldownDays` | integer | `30` | A release younger than this many days at the as-of date is inside the window. 0 to 3650. |
| `asOf` | text | run date | The date the policy is applied, `YYYY-MM-DD`. The field has no schema default: an omitted or empty `asOf` is always the run date, which is what a scheduled run needs. |
| `exemptPackages` | array of text | `["lodash"]` | Names the policy does not cover, for example first-party packages. |

Every field has a default, so a run with an empty input `{}` succeeds.

#### Pin format

| Ecosystem | Prefix | Example |
| --- | --- | --- |
| npm | `npm` | `npm:express@4.18.2`, `npm:@babel/core@7.24.0` |
| PyPI | `pypi` | `pypi:requests@2.31.0` |
| Maven Central | `maven` | `maven:com.google.guava:guava@32.1.3-jre` |
| NuGet | `nuget` | `nuget:Newtonsoft.Json@13.0.3` |

A Maven pin names the group and the artifact, separated by a colon. The
aliases `node`, `pip`, `python`, `maven-central`, `java` and `dotnet` are
accepted as well.

An exemption matches without regard to case, in the registry spelling or in
the normalized spelling (`Flask_SQLAlchemy` matches `flask-sqlalchemy`). For a
Maven pin the artifact alone also matches. Write `ecosystem:name`, for example
`npm:express`, to exempt the package in one ecosystem only.

### Output

One dataset row for each pin (`recordType` `pin`):

| Field | Meaning |
| --- | --- |
| `pin` | The pin as the buyer wrote it. |
| `ecosystem`, `packageName`, `version` | The three parts of the pin. |
| `publishedAt` | The publish timestamp from the registry release record. |
| `ageDays` | The age of the release at the as-of date. It is negative when the release is newer than the as-of date. |
| `cooldownDays` | The policy this run applied. |
| `verdict` | See the table below. |
| `admissibleOn` | The date the pin leaves the window. For an admissible pin it is the date it left the window, which an audit can read. |
| `daysRemaining` | Days from the as-of date to `admissibleOn`, 0 for an admissible pin. |
| `newestAdmissibleVersion` | The highest release that already satisfies the policy at the as-of date. |
| `newestAdmissiblePublishedAt` | The publish timestamp of that release. |
| `asOf`, `note` | The date applied, and the reason when a verdict needs one. |

| Verdict | Meaning |
| --- | --- |
| `admissible` | The release is at least `cooldownDays` old at the as-of date. |
| `inside_cooldown` | The release is younger than the policy allows. `admissibleOn` says when that ends. |
| `unknown_publish_date` | The registry has no publish date for this version: the package or the version is not found, or the registry call failed. `note` says which. |
| `exempt` | The package is on the exempt list, so the policy is not applied and no registry call is made. |
| `invalid_pin` | The entry could not be read. `note` says why. The run continues. |

One run-summary row (`recordType` `runSummary`) holds `countsByVerdict`, the
count per ecosystem, `earliestAdmissionDate` (the first date on which any
blocked pin becomes admissible) and `fullSetAdmissibleOn` (the date on which
the whole set becomes admissible).

One admission-calendar row (`recordType` `admissionCalendar`) groups the
blocked pins by the date on which each becomes admissible. It is written only
when the run finds at least one blocked pin, and it is also stored in the
key-value store under the key `admission-calendar`.

A blocked pin, an unreachable registry and an empty pin list are business
results. They are reported in the dataset and in the run status message, and
the run succeeds. A failed run means a real malfunction.

### Pricing (pay per event)

| Event | Unit | Counted as |
| --- | --- | --- |
| `run_started` | one run | Charged once at the start of every run, before the input is read, so that a run stopped early still pays for the work it started. |
| `pin_checked` | one pin checked | One unit for each pin checked against the policy. An exempt pin and an unreadable pin are not checked, so they are not charged. A pin with an unknown publish date is charged, because the registry lookup was made. |
| `admission_calendar_built` | one run with blocked pins | Charged once, and only for a run that finds at least one blocked pin and therefore builds the calendar. |

Two pins of one package cost one registry call, which keeps the run cost
below the charge.

### Bounds and safety

- At most 1000 pins and 500 exempt names in one run.
- Only four fixed hosts are contacted: `registry.npmjs.org`, `pypi.org`,
  `search.maven.org` and `api.nuget.org`. No host, scheme or path comes from
  the input, so the Actor sends no request to a private or reserved address.
  The one URL a registry itself supplies, a NuGet registration page, is used
  only when it stays on `api.nuget.org`.
- 25 s timeout for each call, at most 6 calls in parallel, at most 12 MB for
  each response body.
- HTTP only. No browser, no proxy, no login and no secret.
- A registry failure for one package is reported on that package's rows. It
  does not end the run.

### Version 1 limitations

- Version ordering uses one rule for the four ecosystems: the numeric parts
  are compared as numbers, a stable release sorts above its own prereleases,
  and the remaining text is compared as text. A Maven classifier-style suffix
  such as `-jre` is not read as a prerelease marker, but two such suffixes on
  one version number are ordered as text.
- `newestAdmissibleVersion` leaves out prereleases, unless the pin itself is a
  prerelease, and always leaves out a yanked or unlisted release.
- Maven Central is read through the search API. The run asks for each pinned
  version exactly, then reads the 200 newest releases of the coordinate for
  the newest-admissible answer.
- A NuGet package with many registration pages is read to a bound: the pages
  that hold a pinned version first, then the newest pages, at most 6 fetched
  pages.
- The publish timestamp is the one the registry states. A registry that
  restates a release changes the answer.

### Repeat use

The result changes without any action by the buyer, because a pin that is
blocked today is admissible tomorrow. Run the Actor on a daily or weekly
schedule and read `fullSetAdmissibleOn` to plan the merge window.

### Development

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

To run the Actor against the default fixture:

```bash
mkdir -p storage/key_value_stores/default
cp .actor/default_input.json storage/key_value_stores/default/INPUT.json
APIFY_LOCAL_STORAGE_DIR=$PWD/storage uv run python -m cooldown_ledger
```

The default input states a fixed as-of date, so the example run gives the same
answer on every day: three admissible pins, one pin inside the window with an
admission date, and one exempt pin. Only this fixture holds that date. A run
that does not state `asOf` uses the run date.

# Actor input Schema

## `pins` (type: `array`):

1 to 1000 pins, each one in the form ecosystem:name@version. Supported ecosystems: npm, pypi, maven and nuget. A Maven pin names the group and the artifact, for example maven:com.google.guava:guava@32.1.3-jre. One entry may hold several lines. An entry this Actor cannot read gets its own row with the invalid\_pin verdict; it does not stop the run.

## `cooldownDays` (type: `integer`):

A release younger than this many days at the as-of date is inside the cooldown window. 30 days is the policy that the uv exclude-newer setting and comparable tools use.

## `asOf` (type: `string`):

The date on which the policy is applied, in the form YYYY-MM-DD. Leave it empty for the run date, which is what a scheduled run needs. A stated date makes an earlier decision reproducible, which an audit needs. The field has no default: an omitted asOf is always the run date. The prefill 2023-10-20 is only a demonstration value, so that the example run always gives the same answer.

## `exemptPackages` (type: `array`):

Package names the cooldown policy does not cover, for example first-party packages. A name matches without regard to case, in the registry spelling or in the normalized spelling. Write ecosystem:name, for example npm:express, to exempt the package in one ecosystem only.

## Actor input object example

```json
{
  "pins": [
    "npm:express@4.18.2",
    "npm:lodash@4.17.21",
    "pypi:requests@2.31.0",
    "maven:com.google.guava:guava@32.1.3-jre",
    "nuget:Newtonsoft.Json@13.0.3"
  ],
  "cooldownDays": 30,
  "asOf": "2023-10-20",
  "exemptPackages": [
    "lodash"
  ]
}
```

# Actor output Schema

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

One row for each pin, one run-summary record with the counts per verdict, and one admission-calendar record when the run finds a blocked pin.

## `admissionCalendar` (type: `string`):

The admission calendar of this run, grouped by the date on which each blocked pin leaves the cooldown window. The record is absent when no pin is blocked.

# 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 = {
    "pins": [
        "npm:express@4.18.2",
        "npm:lodash@4.17.21",
        "pypi:requests@2.31.0",
        "maven:com.google.guava:guava@32.1.3-jre",
        "nuget:Newtonsoft.Json@13.0.3"
    ],
    "cooldownDays": 30,
    "asOf": "2023-10-20",
    "exemptPackages": [
        "lodash"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("kingii98/dependency-release-cooldown-admission-ledger").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 = {
    "pins": [
        "npm:express@4.18.2",
        "npm:lodash@4.17.21",
        "pypi:requests@2.31.0",
        "maven:com.google.guava:guava@32.1.3-jre",
        "nuget:Newtonsoft.Json@13.0.3",
    ],
    "cooldownDays": 30,
    "asOf": "2023-10-20",
    "exemptPackages": ["lodash"],
}

# Run the Actor and wait for it to finish
run = client.actor("kingii98/dependency-release-cooldown-admission-ledger").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 '{
  "pins": [
    "npm:express@4.18.2",
    "npm:lodash@4.17.21",
    "pypi:requests@2.31.0",
    "maven:com.google.guava:guava@32.1.3-jre",
    "nuget:Newtonsoft.Json@13.0.3"
  ],
  "cooldownDays": 30,
  "asOf": "2023-10-20",
  "exemptPackages": [
    "lodash"
  ]
}' |
apify call kingii98/dependency-release-cooldown-admission-ledger --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,kingii98/dependency-release-cooldown-admission-ledger"
        }
    }
}
```

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/BEXM2hVuTyuT8rHBU/builds/SJf0F8fcBmjv9fqlc/openapi.json
