Skip to content
Arbinel

CI quota

The CI quota counts the time each user or organization spends on runners you provide, and refuses new jobs once their limit is used up. It covers Forgejo Actions and external CI systems, such as Crow CI, that report their usage to Arbinel. Read Quotas first, because CI limits are ordinary quota rules.

[quota]
ENABLED = true

[quota.ci]
ENABLED = true
PLATFORM_RUNNERS = instance:hosted-linux-1, instance:hosted-linux-2

[quota.ci.default]
TOTAL = 500m
Key Default Meaning
ENABLED false Records CI time and enforces time: rules. Needs [quota] ENABLED. While it is off, nothing is recorded and CI usage reads as zero.
PLATFORM_RUNNERS empty Actions runners whose time is billed, as instance:<name>, owner:<user-or-org-id>:<name> or repo:<repository-id>:<name>. These survive a runner’s re-registration.
PLATFORM_RUNNER_IDS empty Further Actions runners by database ID. Update them after a runner re-registers, since that changes the ID.
RECORD_RETENTION_PERIODS 12 How many months of individual run records to keep. Monthly totals are never dropped. Values below 1 read as 1.
NOTIFY_THRESHOLDS 80,100 Percentages of a CI rule at which the owner is mailed. See Warning mails.
Key Default Meaning
TOTAL -1 The monthly CI time for an owner none of whose groups has a time: rule. Write it as a duration, such as 500m or 10h. Absent or negative means unlimited.

Only list scopes whose runner registration you control. A user who registers a runner named hosted-linux-1 on their own account gets owner:<their-id>:hosted-linux-1, which does not match instance:hosted-linux-1, so their own runner stays free.

Subject Counts
time:ci:all All billed CI time
time:ci:actions Time on billed Forgejo Actions runners
time:ci:external Time reported by external CI systems
time:ci:class:linux Time on Linux runners, from any source
time:ci:class:windows Time on Windows runners, from any source
time:ci:class:macos Time on macOS runners, from any source

Limits for time: subjects are in seconds in the API. A rule of 30,000 seconds is 500 minutes. Usage resets at the start of each calendar month in UTC. A run is charged to the month in which it stopped.

time:ci:actions and time:ci:external are children of time:ci:all, so a time:ci:all rule covers both. A group whose only CI rule is time:ci:external does not grant Actions time, and its members’ Actions jobs are refused.

The class subjects work differently. They can only lower a limit and never grant time. A class that no rule names is limited by the source rules alone. For example, you can give an owner a large overall budget and a smaller one for a more expensive runner class:

curl -X POST -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
  https://forge.example.com/api/v1/admin/quota/groups \
  -d '{"name": "ci-free", "rules": [
        {"name": "ci-free-all", "limit": 30000, "subjects": ["time:ci:all"]},
        {"name": "ci-free-macos", "limit": 3000, "subjects": ["time:ci:class:macos"]}
      ]}'

Add ci-free to [quota] DEFAULT_GROUPS next to your storage group. A group that only has time: rules does not affect storage, and a group that only has size: rules does not affect CI time.

Arbinel reads a runner’s class from its labels. linux maps to Linux, windows to Windows, and macos or darwin to macOS. A runner with none of these labels counts as Linux.

Arbinel checks the quota when a billed runner asks for a job. If the owner is over a rule for the source, or for the class of that runner, the job is cancelled before it starts.

  • Only that job is cancelled. Other jobs of the run can still run on self-hosted runners, or on a runner class whose budget is not used up.
  • Jobs that are already running are never stopped. Their time is charged, and the next job is refused.
  • Jobs on self-hosted runners are never refused.

The run shows a banner that names the rule, the time used and the limit. When billing is on, it also points owners to the Add-ons settings tab. Rerunning the run after the quota resets or is raised clears the banner.

An external CI system reports the time it spent through /api/v1/quota/ci. It authenticates with a token of its own, not a user token. Register it with one section per system:

[quota.ci.reporter.crow]
TOKEN_HASH = 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
DISPLAY_NAME = Crow CI
PLATFORM_RUNNER_IDS = hosted-agent-1, hosted-agent-2
Key Meaning
Section name The reporter’s name, such as crow. Every record it writes is booked under this name. Lower-case letters, digits, - and _, up to 40 characters, and not actions.
TOKEN_HASH The hex SHA-256 of the reporter’s token. Required. Create it with printf %s "$TOKEN" | sha256sum.
DISPLAY_NAME Optional label shown in usage breakdowns.
PLATFORM_RUNNER_IDS The reporter’s own agent identities whose time is billed. Reports for other agents are accepted but not counted.

Arbinel ignores a section with an invalid name, a malformed hash, or a hash another reporter already uses, and logs an error.

The reporter uses two endpoints with Authorization: Bearer <token>:

  • GET /api/v1/quota/ci/check?repo=<owner>/<name>&runner_id=<id>&class=<class> answers whether work may start, with allowed, remaining_seconds, reason and reason_code. Asking reserves nothing, so several jobs started at once can all be allowed against the same budget.
  • POST /api/v1/quota/ci/usage books finished work with repo, runner_id, external_ref, started, stopped, and optionally seconds and class. Sending the same external_ref again charges once, so a reporter can retry safely.

The full request and response shapes are in the API reference at /api/swagger.

Arbinel mails an owner when their CI time reaches a percentage of one of their time: rules. Each percentage is sent at most once per rule per month. An owner who crosses several at once is mailed only about the highest. The mail goes to the user, or to every member of an organization’s Owners team, and links to the CI/CD usage page. Set [quota.ci] NOTIFY_THRESHOLDS to an empty value to turn the mails off.

cleanup_quota_ci_records runs every night and deletes run records and sent-mail markers older than RECORD_RETENTION_PERIODS. It appears on the admin cron page as “Delete CI usage records past the retention window”.

With quota and CI quota on, owners can read their usage:

  • GET /api/v1/user/quota/ci and GET /api/v1/user/quota/ci/records
  • GET /api/v1/orgs/{org}/quota/ci and GET /api/v1/orgs/{org}/quota/ci/records

The web view is the CI/CD usage settings page, described in Usage and add-ons.