# Production Deploy Commit Lag and Divergence Gate (`kingii98/production-deploy-commit-lag-and-divergence-gate`) Actor

Compares the commit that a production site serves with the head of its git release branch. Measures how long production lags across scheduled runs, flags a commit that is not on the branch, and gives one pass or fail gate. Works for Vercel, Netlify, Cloud

- **URL**: https://apify.com/kingii98/production-deploy-commit-lag-and-divergence-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 $2.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?

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

## Production Deploy Commit Lag and Divergence Gate

This Actor compares the commit that a production site serves with the head of its git release branch. It tells you three things for each site:

- Does production serve the head of the branch?
- If not, for how long has production lagged?
- Does production serve a commit that is not on the branch?

It works for any public site that exposes its build commit, on Vercel, Netlify, Cloudflare Pages, GitHub Pages or any other host. It gives one pass or fail gate for all sites, so a CI job or an agent can trust production before it continues.

The Actor is HTTP only. It uses no browser, no proxy and no database.

### When to run it

- **On a schedule**, for example every 15 minutes. The lag clock needs more than one run.
- **From a post-merge CI job**, to confirm that the merge reached production.
- **At the start of an agent session**, before the agent trusts that production matches `main`.

### How it works

For each site, the Actor does these steps:

1. It reads the build identifier from the live site with the `build_id_locator`. It reads 7 to 40 hex characters.
2. It reads the branch head from the git ref advertisement of the remote (`<repo_url>/info/refs?service=git-upload-pack`). The read stops after the branch line and never goes past 64 KB. Some large repositories list more than 64 KB of branch names before the target branch. Only for these, the Actor sends one git protocol v2 `ls-refs` request that asks for the branch only, again under 64 KB. This needs no API token and no API rate limit.
3. It compares the two SHAs. A short live SHA matches when the branch head starts with it.
4. It updates the lag clock in the key-value store.
5. When you give a `github_token` and the SHAs differ on a github.com remote, it makes one GitHub compare call. This gives the relation and the commits-behind count.
6. It writes one dataset record with the verdict. For a breach or a divergence, it sends one alert to your webhook.

At the end it writes one summary record with the gate result.

### Input

| Field | Required | Default | Description |
| --- | --- | --- | --- |
| `sites` | Yes | One public demo site | 1 to 50 items. Each item has `site_url`, `repo_url`, `branch` (default `main`) and `build_id_locator`. |
| `max_lag_minutes` | No | 30 | A mismatch older than this value is a breach. 1 to 10080. |
| `github_token` | No | none | A read-only token. The Actor sends it only to `api.github.com`, for the compare call. Without it, `commits_behind` is null for a site that does not match. |
| `alert_webhook_url` | No | empty | One public HTTPS URL. The Actor sends one JSON POST for each breach or divergence. |

#### The four locator forms

There are no regular expressions. Each form reads 7 to 40 hex characters at the start of the value, or directly after the prefix.

| Form | Example | Where the Actor reads |
| --- | --- | --- |
| `json` | `{"type": "json", "url": "/version.json", "key": "commit"}` | The text value at `key` in the JSON document. A dotted key such as `git.sha` reads a nested value. |
| `meta` | `{"type": "meta", "name": "git-sha"}` | The `content` of `<meta name="git-sha">` on `site_url`. |
| `header` | `{"type": "header", "name": "x-commit-sha"}` | The response header of `site_url`. |
| `asset` | `{"type": "asset", "url": "/app.js", "prefix": "build:"}` | The text directly after the first occurrence of the fixed prefix in the asset. |

A relative `url` is resolved against `site_url`.

Example input:

```json
{
  "sites": [
    {
      "site_url": "https://www.example.com/",
      "repo_url": "https://github.com/example/website",
      "branch": "main",
      "build_id_locator": {"type": "json", "url": "/version.json", "key": "commit"}
    }
  ],
  "max_lag_minutes": 30,
  "alert_webhook_url": "https://hooks.example.com/deploy-gate"
}
```

Most hosts do not publish a build commit by default. Add one at build time, for example write `VERCEL_GIT_COMMIT_SHA`, `COMMIT_REF` (Netlify), `CF_PAGES_COMMIT_SHA` or `GITHUB_SHA` into `/version.json` or into a meta tag.

The default input checks the public forum of the Discourse project. The forum writes its commit into its generator meta tag, and the project deploys the public `tests-passed` branch.

### Output

#### One record for each site

| Field | Description |
| --- | --- |
| `live_sha` | The SHA that production serves, or null. |
| `branch_head_sha` | The head SHA of the branch, or null. |
| `match` | True when the branch head starts with the live SHA. Null when one SHA is missing. |
| `commits_behind` | The number of branch commits that production does not have. 0 for a match. Null without a token, or when the compare call gives no answer. |
| `relation` | `identical`, `behind`, `diverged`, `ahead` or `unknown`. |
| `head_first_seen_at` | The time the Actor first saw the current branch head. |
| `lag_minutes` | The time since the Actor first saw the current branch head while production did not match it. 0 for a match. |
| `verdict` | See the table below. |
| `alert_status`, `alert_error` | The result of the webhook POST, when one was sent. |
| `note` | The reason for a missing value. |

| Verdict | Meaning | Gate |
| --- | --- | --- |
| `in-sync` | Production serves the branch head. | Pass |
| `lagging` | Production does not match, and the lag is not more than `max_lag_minutes`. | Pass |
| `breach` | Production does not match, and the lag is more than `max_lag_minutes`. | Fail |
| `diverged` | Production serves a commit that is not on the branch (relation `diverged` or `ahead`). | Fail |
| `locator-missing` | The Actor could not read a build identifier from the site. | Fail |
| `head-unreadable` | The Actor could not read the branch head from the git remote. | Fail |

The contract names the first five verdicts. `head-unreadable` is added so that a wrong `repo_url` or `branch` is not reported as a missing locator.

#### One summary record

`site_count`, `in_sync_count`, `lagging_count`, `breach_count`, `diverged_count`, `locator_missing_count`, `head_unreadable_count` and `gate_pass`. `gate_pass` is true only when every site is `in-sync` or `lagging`.

The run ends SUCCEEDED for every verdict. A failed gate is a dataset result and a status message, not a failed run.

### The lag clock

The Actor keeps one record for each site in the named key-value store `production-deploy-commit-lag-state`. The record holds the last branch head SHA, the time the Actor first saw it, and the time since when production has not matched it. A named store stays after the run ends, so the clock continues from one scheduled run to the next.

- The first run that sees a new head starts the clock. A first run with a mismatch reports `lagging` with a lag of 0.
- A new branch head restarts the clock.
- A match stops the clock.

### Pricing

This Actor uses pay-per-event pricing.

| Event | Unit | Price (USD) | When it is charged |
| --- | --- | --- | --- |
| `run-started` | one Actor run | 0.002 | Once for each run, after the input is valid and before the first request. |
| `site-checked` | one site build identifier compared with one branch head in one run | 0.001 | For each site where the Actor read both the live SHA and the branch head. A `locator-missing` or `head-unreadable` site is not charged. |
| `lag-breach-flagged` | one site that lags past max\_lag\_minutes or serves a diverged commit | 0.01 | For each site with the verdict `breach` or `diverged`, in each run that finds it. |

The default maximum total charge for one run is USD 1.00. When a run reaches the maximum total charge, the Actor starts no more sites and lists them in `sites_not_checked`.

#### Examples

| Case | Runs | Sites per run | Breaches per run | Cost per run (USD) | Total (USD) |
| --- | --- | --- | --- | --- | --- |
| 3 sites every 15 minutes for one day, no breach | 96 | 3 | 0 | 0.005 | 0.48 |
| 3 sites every 15 minutes for one day, 1 breach in each run | 96 | 3 | 1 | 0.015 | 1.44 |
| One post-merge CI call for 10 sites, no breach | 1 | 10 | 0 | 0.012 | 0.012 |
| One run of 50 sites with a breach on every site | 1 | 50 | 50 | 0.552 | 0.552 |

The largest run that the input allows costs USD 0.552, which is less than the default maximum total charge of USD 1.00.

### Safety and limits

- Only HTTPS. URLs with credentials are refused.
- Every request target, and every redirect hop, is checked. A loopback, private or reserved address is refused.
- At most 3 redirects. The GitHub compare call, the git `ls-refs` request and the webhook POST follow no redirect.
- The locator read stops at 256 KB, the ref read at 64 KB, the compare answer at 100 KB.
- Each request has a 15 s timeout and a 25 s deadline. At most 5 sites are checked at the same time.
- The token goes only to `api.github.com` and is never written to the dataset or the log.

### Scope

The Actor compares one build identifier with one branch head. It does not check preview deployments, cache headers, DNS or workflow runs.

# Actor input Schema

## `sites` (type: `array`):

1 to 50 items. Each item has site\_url (the public HTTPS production site), repo\_url (a public HTTPS git remote, for example https://github.com/owner/repo), branch (default main) and build\_id\_locator. build\_id\_locator is one of four forms, with no regular expressions: {"type": "json", "url": "/version.json", "key": "commit"}; {"type": "meta", "name": "git-sha"}; {"type": "header", "name": "x-commit-sha"}; or {"type": "asset", "url": "/app.js", "prefix": "build:"}. A relative url is resolved against site\_url. The Actor reads 7 to 40 hex characters at the start of the value, or directly after the first occurrence of the prefix.

## `max_lag_minutes` (type: `integer`):

A mismatch older than this value is a breach. The lag is the time since the Actor first saw the current branch head while production did not match it, so the clock needs at least two runs.

## `github_token` (type: `string`):

Optional. A read-only token. The Actor sends it only to api.github.com, for the one compare call that gives the relation and the commits-behind count of a github.com remote. Without it, commits\_behind is null and the relation is unknown for a site that does not match.

## `alert_webhook_url` (type: `string`):

Optional. One public HTTPS URL. The Actor sends one JSON POST for each breach or divergence. Redirects are not followed. Leave empty to send no alert.

## Actor input object example

```json
{
  "sites": [
    {
      "site_url": "https://meta.discourse.org/",
      "repo_url": "https://github.com/discourse/discourse",
      "branch": "tests-passed",
      "build_id_locator": {
        "type": "asset",
        "url": "https://meta.discourse.org/",
        "prefix": "https://github.com/discourse/discourse version "
      }
    }
  ],
  "max_lag_minutes": 30,
  "alert_webhook_url": ""
}
```

# Actor output Schema

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

No description

## `sites` (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 = {
    "sites": [
        {
            "site_url": "https://meta.discourse.org/",
            "repo_url": "https://github.com/discourse/discourse",
            "branch": "tests-passed",
            "build_id_locator": {
                "type": "asset",
                "url": "https://meta.discourse.org/",
                "prefix": "https://github.com/discourse/discourse version "
            }
        }
    ],
    "max_lag_minutes": 30
};

// Run the Actor and wait for it to finish
const run = await client.actor("kingii98/production-deploy-commit-lag-and-divergence-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 = {
    "sites": [{
            "site_url": "https://meta.discourse.org/",
            "repo_url": "https://github.com/discourse/discourse",
            "branch": "tests-passed",
            "build_id_locator": {
                "type": "asset",
                "url": "https://meta.discourse.org/",
                "prefix": "https://github.com/discourse/discourse version ",
            },
        }],
    "max_lag_minutes": 30,
}

# Run the Actor and wait for it to finish
run = client.actor("kingii98/production-deploy-commit-lag-and-divergence-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 '{
  "sites": [
    {
      "site_url": "https://meta.discourse.org/",
      "repo_url": "https://github.com/discourse/discourse",
      "branch": "tests-passed",
      "build_id_locator": {
        "type": "asset",
        "url": "https://meta.discourse.org/",
        "prefix": "https://github.com/discourse/discourse version "
      }
    }
  ],
  "max_lag_minutes": 30
}' |
apify call kingii98/production-deploy-commit-lag-and-divergence-gate --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,kingii98/production-deploy-commit-lag-and-divergence-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/xPMkUuYpj7OcGDPap/builds/q8f0Hl6xTKVvQ0drI/openapi.json
