Service catalog
- See what a repository already contains
- Browse a group's services
- Open a service
- Check which service owns a path
- Declare a service
- Correct a service
- Delete a service
- Declare an environment
- List the environments a repository can deploy to
- Change an environment's protection
- Give an environment its variables and secrets
- Build with an environment's settings
- Retire an environment
- See what is deployed
- Read the deployment board
- Find out how far behind a deployment is
- Trace what a service depends on
- See what checks exist before writing a scorecard
- Create a scorecard
- Read a scorecard's results
- Amend a scorecard, and run a campaign
- Attest a check no machine can see
- Delete a scorecard
Overview
The catalog answers the four questions nobody can answer from a repository list: what do we run, who owns it, where is it deployed, and is it any good? A service is a named unit of software mapped onto a set of repository paths, so one repository can hold five services and one service can span three repositories. Ownership is a real group — the one a failing check files work into, and the one that stands in as code owner wherever a repository requires code-owner review and no CODEOWNERS rule matches the path — so setting it does something rather than describing something.
You do not start from an empty page. Every repository already appears as a service, created with the repository itself, so your first job is splitting and correcting rather than filling in a form.
Quick start
Declare a service
Add a
.gitmoap/catalog.yamlto the repository, naming the service and the group that owns it:services: - slug: checkout owner: acme/platform
Merge it to the default branch. The service appears under Catalog, and its deployments are tracked from then on.
Declare an environment
Choose Catalog, then Environments, then New environment.
Give it a slug and a tier. At the production tier it arrives with approval required and deploys limited to
main.Choose Create environment. A pipeline step targets it by its slug.
Grade your services with a scorecard
Choose Catalog, then Scorecards, then New scorecard.
Pick which services it grades, and add the checks they must pass.
Choose Create scorecard. Every matching service in this group and beneath it is scored automatically.
Before you start
- A group, and a repository or two.The catalog fills itself from repositories you already have. Creating a repository creates one service covering all of it.
- Reporter access to read.A service inherits the visibility of the group that declares it: a public group's services answer anyone, even signed out, an internal group's answer anyone signed in, a private group's answer only its members — with
404rather than403, so nothing leaks by existing. Scorecards and dependency edges follow the same rule. Environments, their variables and every deployment read take no such shortcut and are the exception worth remembering: they need Reporter on the owning scope whatever the group's visibility, so somebody who can browse a public group's services still gets403on its environments. - Maintainer access to declare or edita service, an environment or a scorecard, on the group or repository that owns it — and to attest a check.
- Owner access to delete an environment, to create or change a production-tier environment, to change protection or tier at any tier, to write or delete an environment variable, and to arm a scorecard campaign. The privilege worth guarding is not deploying to production — it is removing the requirement to be approved, and then deploying entirely within your own rights. Deleting a service or a scorecard stays at Maintainer, the same level that declared it.
- Maintainer on the group you name as owner.Naming a team as a service's owner authorizes two things: the service, and the team. Without that second check, anyone who can edit a service could make another team responsible for its outages and fill their backlog while doing it.
- A token with the right scope, if you are calling the API or the CLI:
catalog:readto read anything on this page,catalog:manageto write anything. Scopes are chosen when you mint a token and cannot be added afterwards. - A pipeline, for the deployment half.Deployments are recorded from pipeline runs whose nodes name a service and an environment. No endpoint creates a deployment by hand.
How it works
A service is an overlay, not a repository
A service binds to a set of (repository, path glob) pairs. That single decision is what makes a monorepo native instead of an apology.
repository: acme/platform services├── services/checkout/** ─────────────────▶ checkout (owner: acme/payments)├── services/inventory/** ─────────────────▶ inventory (owner: acme/supply)├── libs/money/** ─────────────────▶ money (owner: acme/payments, kind: library)└── ** ─────────────────▶ (nothing — the wide binding was split away) repository: acme/checkout-worker└── ** ─────────────────▶ checkout ← the same service, second repositoryFour things fall out of the binding and each would otherwise be its own feature: per-service ownership, per-service technical debt, pipeline nodes that run only when their own folder changed, and a deploy that knows which of five services it deployed.
When two globs match one path, the winner is decided in a fixed order — longest literal prefix first, then the glob with fewer wildcards, then a stable tie-break. An exclude glob removes its own matches before any of that, so paths: ["services/checkout/**"] with exclude: ["**/vendor/**"] does what it reads like. The order is fixed rather than resolved by row order because a service's owner can gate a merge, and a gate that flips depending on which row was read first is not a gate.
Identity, address and where an entry came from
A service's id never changes. The slug is the address, it changes, and it is unique across your whole organisation rather than per group — a depends_on: entry in another repository's catalog file, a pipeline node's service: and the by-slug lookup all need payments-api to resolve without a path in front of it. The cost is real: at a couple of hundred services you will occasionally have to call something payments-api-v2.
Every entry records how it came to exist, and that is visible on purpose:
| Source | Created by | What it means |
|---|---|---|
inferred | the repository's own creation | nobody has vouched for this entry yet — it covers the whole repository |
declared | a .gitmoap/catalog.yaml in the repository | a file in git is the source of truth for it |
api | somebody called the API, the CLI or the web app | a person entered it directly |
A human has vouched for this is simply source not being inferred, which is a scorecard check you can turn on rather than a banner nobody dismisses.
A declaration is a refinement, and it never destroys history
.gitmoap/catalog.yaml — or the same path with a .yml extension, either works — on the default branch splits and corrects the catalog. An entry needs only a slug. kind defaults to service, lifecycle to production, display_name to the slug, and paths to ** when the file declares exactly one service — declare two and every entry must name its own paths. owner may be left out too, and the entry is then unowned, which is exactly what the has an owner check exists to surface, so name a team while you are there:
services: - slug: checkout kind: service owner: acme/payments paths: ["services/checkout/**"] exclude: ["**/vendor/**"]Exactly one of the two extensions may exist in a repository; both at once is a validation error naming them. The file is validated on every branch and applied only on the default one, and the result is posted as a gitmoap/catalog commit status — so a broken declaration is a red check inside the merge request, seen by the person who caused it, before it lands. A file with any error applies none of its services.
A file of forty near-identical entries repeats itself, and a defaults: block above services: is the remedy — kind, lifecycle, owner, exclude and annotations set there apply to every entry that does not state its own. It is the reuse mechanism on purpose: YAML anchors would not survive a change of format, and this one round-trips.
The first catalog file in a repository does not throw away what the catalog already knew. If exactly one declared service covers the whole repository — paths: ["**"], or no paths at all — it adopts the entry created with the repository: same id, same deployments, same check history, now marked as declared. Declare two or more, or narrow the paths, and you are splitting: the declared services are new entries and the original is orphaned rather than deleted, so its history stays readable while you decide what to do with it.
When a declaration disappears the service is orphaned, never deleted: nothing attached to it is touched. Most disappearances are a bad merge or a refactor in flight, and reaping a deploy history over one would destroy the record rather than tidy it. Bring the declaration back and the same service is revived, with its id, its deployments and its check history intact. Deleting is an explicit act by somebody who can see what they are destroying.
Environments are one row, resolved by a walk up the tree
An environment is a named place software runs, carrying the policy that governs deploying to it. It is exactly one row, owned by exactly one scope. Declaring production on your root group does not copy it into 400 repositories — it makes one row that every descendant may reference.
resolving environment: production from acme/platform/checkout 1. an environment named `production` on the repository itself → use it2. else the nearest ancestor group that declares one → use it3. else the run fails to resolve, before any node startsA nearer slug shadows a farther one, so a repository can have its own staging without disturbing the organisation's. slug is whatever you call it — prod, live, eu-west-1-blue — while tier is one of development, testing, staging or production, and the tier is what lets a check say deployed to production within 30 days without knowing your naming.
A production-tier environment is gated by default. Create one with no protection block and you get approval required, the people given access on the owning group named as its approvers, and deploys restricted to one branch — the repository's own default branch when you declare it on a repository, and main when you declare it on a group, since a group has no default branch to inherit. Turning any of that off is a deliberate edit, because a feature whose safe configuration requires reading the documentation is unsafe in the median case.
allowed_scopes is the other half, and it is an access rule rather than an approval rule: leave it unset and the declaring scope's subtree may use the environment; set it and only the groups and repositories you name may. Protection gates approval; this gates reach.
Deployments are recorded, never entered
Every deployment is an edge from a pipeline run to a (service, environment) at a commit, written by the platform when a node that named both starts and finishes.
Requested ──gate──▶ AwaitingApproval ──approve──▶ Running ──▶ Succeeded ──▶ Superseded │ │ ├──▶ Failed │ └──reject──▶ Rejected └──▶ Cancelled └──concurrency policy = skip──▶ SkippedTwo consequences worth knowing. A rollback is a new deployment, not a status change on the old one — redeploying v2.13.0 over v2.14.0 writes a fresh row and marks the previous one superseded, so the history and the current pointer never disagree about what was true at 14:00. And a deployment outlives its run: reaping pipeline history does not erase the record of what went to production.
Checks come in three kinds
| Kind | Where the answer comes from | How fresh | Example |
|---|---|---|---|
| Derived | a query over the platform's own records | exact, always | has_owner, branch_protection, released_within, deployed_within |
| Evidence | a report a pipeline node produced at a named commit | as of the last run that produced it | a coverage threshold, a smoke test, a latency metric |
| Attested | a person, with an expiry | until it expires | a disaster-recovery runbook exists and was rehearsed |
Each result is one of six states, and three of them exist to stop a scorecard lying. not_applicable leaves the denominator entirely — a library has no deployments, so marking it red teaches people to ignore red. unknown means never evaluated; stale means the thing that used to prove this ran again and no longer proves it; expired is an attestation whose renewal window has passed.
Scorecards union down the tree — a service is graded by every scorecard in its ancestor chain whose selector matches it, and a subgroup cannot switch off an organisation-wide baseline.
Dependency edges are observed, not asserted
Because the platform runs the npm, Cargo and PyPI registries, an edge is discovered rather than declared: when one service publishes acme-auth and another's lockfile resolves it, that edge names the real lockfile, the real commit and the real resolved version. A service declares what it provides; the lockfile says who consumes it. Two lockfile formats are read — Cargo.lock and package-lock.json — anywhere in the tree, so a monorepo's services/checkout/Cargo.lock counts exactly as a root one does. Consumption in any other ecosystem has to be declared, in depends_on:.
A declared edge and an observed edge for the same pair coexist, and their disagreement is the finding — you declare a dependency no lockfile mentions, or your lockfile depends on something your declaration does not admit. Both are checks you can turn on.
Anything the platform does not build — a vendor API, a managed database, a team on another forge — is an external entity: named, owned, and pointed at by edges, with nothing else asserted about it. External entities never enter a score silently; every aggregate reports them as a separate unscored count.
How to
See what a repository already contains
Every repository starts life with one service, minted alongside it and covering all of it. Start here before declaring anything, because the entry you want to correct probably already exists.
Open the repository and choose Catalog in the side navigation. Each row is a service bound to this repository, with its kind, lifecycle, owner and a promotion track showing how far the latest build has got — one stop per tier, from development through to production. Filter by kind, lifecycle or source, or type in the search box to match a name, a description or part of a slug. Entries whose declaration has disappeared are hidden until you tick Include orphaned.
gitmoap catalog list-repo-services --repo-id <id>curl -X GET https://api.gitmoap.example/repositories/{repo_id}/catalog/services \ -H "Authorization: Bearer $GITMOAP_TOKEN"Each row carries two facts per tier and they answer different questions: deployed is how far has this got, read from what is serving now, and status is is anything wrong, read from the latest attempt. A tier showing deployed with a failed status means the previous version is still serving and the attempt to replace it failed — which no single indicator can say.
A tier is not an environment. If production holds prod-eu, prod-us and prod-ap, the row still draws one stop: status is the worst of the three and deployed is true if any of them is serving. Open the service to see them apart.
Browse a group's services
Open the group and choose Catalog. The list is alphabetical by slug and carries the same filters and promotion track as the repository view, across every service the group declares. Selecting one opens its overview.
gitmoap catalog list-services --group-id <id>curl -X GET https://api.gitmoap.example/groups/{group_id}/catalog/services \ -H "Authorization: Bearer $GITMOAP_TOKEN"If you already know the slug, fetch it directly rather than paging — slugs are unique across the organisation, so any group in it resolves the same name.
Type the slug into the catalog search box; an exact slug match is pinned to the top of the results.
gitmoap catalog get-service-by-slug --group-id <id> --slug <id>curl -X GET https://api.gitmoap.example/groups/{group_id}/catalog/services/by-slug/{slug} \ -H "Authorization: Bearer $GITMOAP_TOKEN"Open a service
The overview is one call that returns everything a service page shows: its repositories and path globs, its owner, its latest release and its latest pipeline run, how many merge requests are open against it, its technical-debt rollup by path, what is deployed where, its scorecard verdicts, and how many dependency edges point each way.
Open the service from either catalog listing. The header carries the owner, kind and lifecycle; the panels below show repositories and paths, deployments by environment, scorecards with their level, dependencies in both directions, and the open bugs and markers found in the service's own paths.
gitmoap catalog get-service-overview --service-id <id>curl -X GET https://api.gitmoap.example/catalog/services/{service_id}/overview \ -H "Authorization: Bearer $GITMOAP_TOKEN"Everything on the overview is authorized per far end rather than per page. A service in a public group may bind paths in a private repository, and a reader who cannot see that repository gets a service with an empty repository list — not a 403, and not a hint that the repository exists.
Use the plain read when you want the entry itself without the derived panels.
The overview page's Details tab shows the stored entry — slug, kind, lifecycle, owner, links, annotations, published packages and API specs.
gitmoap catalog get-service --service-id <id>curl -X GET https://api.gitmoap.example/catalog/services/{service_id} \ -H "Authorization: Bearer $GITMOAP_TOKEN"Check which service owns a path
This is the question everything else rests on, and it takes up to 200 paths in one call so a client resolving a changed file set makes one request rather than one per file.
Open the repository's Catalog tab and use Resolve path. Paste one or more repository-relative paths; each answer shows the winning service and, where more than one glob matched, the others in resolution order so you can see the ambiguity rather than guess at it.
gitmoap catalog resolve-repo-path-service --repo-id <id>curl -X GET https://api.gitmoap.example/repositories/{repo_id}/catalog/resolve \ -H "Authorization: Bearer $GITMOAP_TOKEN"An answer with entries under overlapping means two declarations claim the same path. The winner is stable, but the overlap is worth fixing — narrow one of the globs, or add an exclude.
Declare a service
Most of the time you will declare services in .gitmoap/catalog.yaml, so the split lives in git beside the code it describes and arrives through a merge request. Declare one through the API when you are scripting a migration, or when the entry is an external entity with no code behind it.
Open the group, choose Catalog, then New service. Give it a slug, pick a kind and lifecycle, choose an owning team, and add one or more path bindings — a repository plus a glob, with optional exclusions. External entity hides the bindings and is for something the platform does not build.
gitmoap catalog create-service --group-id <id> --slug <slug>curl -X POST https://api.gitmoap.example/groups/{group_id}/catalog/services \ -H "Authorization: Bearer $GITMOAP_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "api_specs[].path": "<string>", "api_specs[].repository_id": "<uuid>", "api_specs[].spec_kind": "<one of asyncapi | grpc | openapi>", "links[].link_type": "<one of chat | dashboard | docs | other | runbook>", "links[].url": "<string>", "provides[].format": "<one of cargo | npm | pypi>", "provides[].name": "<string>", "repo_paths[].path_glob": "<string>", "repo_paths[].repository_id": "<uuid>", "slug": "<string>" }'Three rules are enforced at write time and are worth knowing before you hit them. A service that is not external must bind at least one path that includes something — exclusions alone are not a binding — and an external one must not bind any: inferring externality from a missing binding was rejected because a typo in a glob would then quietly turn every check not applicable, which reads as passing. Naming an owner group requires Maintainer standing on that group as well as on the declaring one, and naming an owner team requires that you maintain it or manage your organisation's teams. And binding a repository requires Maintainer on that repository too, so nobody can attach another team's code to a service they own; a repository you cannot write to answers 404 rather than 403, whether or not you can read it.
An owner may be a team — in the catalog file, owner: team:backend, a team of your organisation by its slug — and then the service is owned by whoever is on the team, and a code-owner approval from any of them counts. An owner may also be a person — owner: user:sam — and it satisfies the has an owner check. Prefer a group or a team anyway: a person is not a backlog, so a campaign filing work against a person-owned service falls back to the group the service is declared in, and every ownership rollup is a group-by over teams.
Correct a service
Renaming a slug, changing the owner, moving a lifecycle to deprecated, adding a runbook link — all of it is one partial update. Supply the version you read to make the write refuse rather than silently overwrite somebody else's edit.
Open the service and choose Edit. Everything is editable except three things the platform owns: the service's id, the group it is declared in, and how the entry came to exist. Saving with a stale copy open shows a conflict rather than overwriting.
gitmoap catalog update-service --service-id <id>curl -X PATCH https://api.gitmoap.example/catalog/services/{service_id} \ -H "Authorization: Bearer $GITMOAP_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "api_specs[].path": "<string>", "api_specs[].repository_id": "<uuid>", "api_specs[].spec_kind": "<one of asyncapi | grpc | openapi>", "links[].link_type": "<one of chat | dashboard | docs | other | runbook>", "links[].url": "<string>", "provides[].format": "<one of cargo | npm | pypi>", "provides[].name": "<string>", "repo_paths[].path_glob": "<string>", "repo_paths[].repository_id": "<uuid>" }'Renaming a slug through this endpoint is deliberate and safe: everything that points at the service points at its id, so deployments, check results and dependency edges survive. Renaming a slug inside a catalog file is not the same act — it reads as a delete plus a create, because a slug is a cross-repository identifier other services resolve against.
Delete a service
Warning
Deleting a service destroys its deployment history, every scorecard result and transition recorded against it, and every dependency edge that points at or away from it. None of that is recoverable, and rebuilding it means re-running the pipelines that produced it. Work items a campaign filed against it are the one thing that survives — they are somebody's backlog, so they are left behind rather than reaped, and they will name a service that no longer exists. If what you want is to stop maintaining an entry, set its lifecycle to retired, or remove its declaration from the catalog file — which orphans the entry and keeps everything attached to it.
Open the service, choose Settings, then Delete service. The confirmation names what goes with it — the deployment count, the scorecard results and the edges. Deleting requires Owner on the declaring group.
gitmoap catalog delete-service --service-id <id>curl -X DELETE https://api.gitmoap.example/catalog/services/{service_id} \ -H "Authorization: Bearer $GITMOAP_TOKEN"Deleting a repository does something narrower and deliberately different: an auto-created service bound only to that repository is deleted with it, while a declared service is orphaned so its history survives.
Declare an environment
Declare it on the group when several repositories deploy to the same place — which is almost always right for production — and on a repository when the place belongs to that repository alone.
Open the group, choose Catalog then Environments, and New environment. Give it a slug, pick a tier, and optionally a display name and a URL. The protection panel opens preconfigured for the tier you chose; at production tier it arrives with approval required, the owning group's people as approvers, and deploys restricted to main.
gitmoap catalog create-group-environment --group-id <id> --slug <slug>curl -X POST https://api.gitmoap.example/groups/{group_id}/catalog/environments \ -H "Authorization: Bearer $GITMOAP_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "slug": "<string>" }'The same form is on the repository's Catalog → Environments tab. A repository-owned environment shadows an ancestor group's environment of the same name for that repository only.
gitmoap catalog create-repo-environment --repo-id <id> --slug <slug>curl -X POST https://api.gitmoap.example/repositories/{repo_id}/catalog/environments \ -H "Authorization: Bearer $GITMOAP_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "slug": "<string>" }'Two refusals to expect. Requiring approval with no approvers is refused at create time — a gate nobody can open is a deploy that can never happen, and 02:00 during an incident is the worst possible time to learn that. And a Maintainer cannot create a production-tier environment at all; that one is Owner.
List the environments a repository can deploy to
Open the repository's Catalog → Environments tab. By default it lists the environments this repository owns. Tick Include inherited to see everything it may reference, with each row showing the scope that owns it — which is the difference between declared here and declared four levels up. Where a repository and an ancestor both use a slug, only the nearer one is shown, exactly as a pipeline would resolve it.
gitmoap catalog list-repo-environments --repo-id <id>curl -X GET https://api.gitmoap.example/repositories/{repo_id}/catalog/environments \ -H "Authorization: Bearer $GITMOAP_TOKEN"The group's Catalog → Environments tab is the same list one level up, with the same filters: tier, a search over the slug and display name, Include inherited for the ancestor groups' rows, and Include archived for the ones retired from resolution.
gitmoap catalog list-group-environments --group-id <id>curl -X GET https://api.gitmoap.example/groups/{group_id}/catalog/environments \ -H "Authorization: Bearer $GITMOAP_TOKEN"Selecting any environment row opens it — slug, tier, owning scope, URL, protection, concurrency policy, variable count and the version you need to edit it safely.
gitmoap catalog get-environment --environment-id <id>curl -X GET https://api.gitmoap.example/catalog/environments/{environment_id} \ -H "Authorization: Bearer $GITMOAP_TOKEN"Change an environment's protection
Open the environment and choose Edit. Protection sets who may approve, whether the person who triggered the run may approve their own deploy, how long to wait after approval before starting, which refs may deploy, and which scopes may reference the environment at all. Concurrency sets whether deploys serialise, and what happens to a run that arrives while another holds the lock — queue, skip, or cancel the one in flight.
gitmoap catalog update-environment --environment-id <id>curl -X PATCH https://api.gitmoap.example/catalog/environments/{environment_id} \ -H "Authorization: Bearer $GITMOAP_TOKEN"Owner is required to change protection, to change the tier, or to change anything at all on a production-tier environment. The last of those exists so an Owner-only environment cannot be reshaped one Maintainer-level field at a time.
Who may approve is a list of approvers, each a person, a team, or a role at a group or repository — everyone holding at least that role there. With direct_only, a role counts only people given access on that group or repository itself, not those who inherit it from a group above, so an organisation Owner does not walk through every team's gate. required is how many different approvers must approve before the deploy starts, and accept_from says which approvals count: people_signed_in by default — a person approving in the web app, or in the CLI after gitmoap login — or people, which also counts a personal access token, or people_and_service_accounts. <team-id> and <group-id> below are ids from your organisation:
{ "protection": { "requires_approval": true, "approvers": [ { "team": "<team-id>" }, { "role": { "at": { "group": "<group-id>" }, "min": "maintainer", "direct_only": true } } ], "required": 2, "accept_from": "people_signed_in", "prevent_self_approval": true, "allowed_refs": ["main"] }}A deployment's approvers are always named: anyone and code owners, which an approval rule on a merge request may use, are refused here. A pipeline step deploying to the environment may narrow these approvers, never add to them.
The wait timer starts when the gate clears, not when the run reaches it. Approved, deploying in ten minutes, cancel if you spot something is what the pause is for, and a timer that had been running while an approver deliberated would already have expired by the time they pressed the button.
Give an environment its variables and secrets
Variables reach a pipeline step as environment variables. A non-secret's value is stored where the API can read it back; a secret's is stored where no read can return it, and is redacted from the step's logs.
Open the environment and choose Variables, then Add variable. Names are shell-shaped — letters, digits and underscores, not starting with a digit — because they become environment variables inside the step. Tick Secret to store the value out of reach; leaving it unticked gives you a variable whose value is visible, which you will notice, rather than one you believe is secret and is not.
gitmoap catalog put-environment-variable \ --environment-id <id> \ --name <id> \ --value <value>curl -X PUT https://api.gitmoap.example/catalog/environments/{environment_id}/variables/{name} \ -H "Authorization: Bearer $GITMOAP_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "value": "<string>" }'The Variables tab lists every variable in name order, with who set it and when. A secret shows no value — not masked, absent, because nothing holds one to show. Reading the list is Reporter: the names are not the secret, the values are, and only writes and deletes go into the audit log.
gitmoap catalog list-environment-variables --environment-id <id>curl -X GET https://api.gitmoap.example/catalog/environments/{environment_id}/variables \ -H "Authorization: Bearer $GITMOAP_TOKEN"A handful of names are refused, because a variable able to set them would point a platform-signed token at a role of your choosing: anything beginning GITMOAP_, anything beginning AWS_CONTAINER_CREDENTIALS_, and AWS_ROLE_ARN, AWS_ROLE_SESSION_NAME, AWS_WEB_IDENTITY_TOKEN_FILE and AWS_REGION. Set a region or a role on the provider connection instead, where it is authorized.
Where a name is set both on a provider connection and on the environment, the environment wins — the more specific place is the more deliberate one. What is refused rather than resolved is a collision that changes a name's secrecy: a connection secret shadowed by a plain environment value would move a credential out of the redaction list, so the run fails at resolution naming both sources. The same holds for a group or repository pipeline variable that shares a name with an environment variable and differs from it in secrecy.
A step's configuration can read a non-secret variable of the environment it deploys to as well, by writing {"$var": "WEB_S3_BUCKET"} where the value belongs, so a bucket or an endpoint declared here once serves every pipeline that deploys here. A secret cannot be read that way, because configuration is not redacted from logs. See Read a variable in a step's configuration.
Warning
Deleting a variable is immediate and the value is gone — a secret's especially, since nothing else holds a copy. Any pipeline node that expected it runs without it on the next attempt, which usually surfaces as a failure inside your own build rather than a message from the platform.
On the Variables tab, choose the bin icon at the end of a row. The confirmation names the variable and whether it is a secret. Writing and deleting variables both require Owner.
gitmoap catalog delete-environment-variable \ --environment-id <id> \ --name <id>curl -X DELETE https://api.gitmoap.example/catalog/environments/{environment_id}/variables/{name} \ -H "Authorization: Bearer $GITMOAP_TOKEN"Build with an environment's settings
A web app's bundle usually carries its settings inside it: the API address, the sign-in client, each VITE_* or NEXT_PUBLIC_* value is baked in when the app is built. So the step that runs the build needs the environment's values, and it must not be the step that deploys there. Set variables_from on the build step instead of environment:
- name: build-web capability: node:build variables_from: production- name: deploy-web capability: aws:s3-sync environment: productionThe build receives the environment's non-secret variables as environment variables, and its configuration can read them with {"$var": …}. It gets nothing else from the environment: no approval wait, no lock, no secrets and no OIDC identity, and finishing it records no deployment. A secret is given only to a step that deploys there, so the build's log says how many of the environment's secrets it did not receive. A build-time secret, such as a token for uploading source maps, belongs on the step's connection or in a pipeline variable.
Two things are refused when you save the pipeline, and again when a run starts:
- One step naming both. A step that deploys already reads its own environment's variables.
- Shipping one environment's build to another. A deploy whose input was built with a different environment's variables — staging's settings bound for production — is refused, naming both steps. Build once per environment, each with that environment's variables. A build that reads no environment produces something you can deploy anywhere.
allowed_scopes applies to variables_from as it does to a deploy, because reading an environment's settings is using it. allowed_refs does not: a step that only reads variables reaches nothing, so a feature branch can build with production's settings to preview them.
Retire an environment
Warning
Deleting an environment is refused once anything has been deployed to it, and that refusal is the feature: deployment history is the record of what was in production, and a delete must not be a way to erase it. Deleting an environment that has no deployments also destroys its variables — the plain values and the secrets both, permanently.
Archiving is the reversible operation. An archived environment leaves resolution — no new run can target it — while every deployment recorded against it stays readable. Archive with the ordinary edit; delete only an environment created by mistake.
Open the environment, choose Settings, then Delete environment. If anything has ever deployed here the button offers Archive instead, and says why. Deleting requires Owner.
gitmoap catalog delete-environment --environment-id <id>curl -X DELETE https://api.gitmoap.example/catalog/environments/{environment_id} \ -H "Authorization: Bearer $GITMOAP_TOKEN"See what is deployed
The service overview's Deployments panel shows one entry per environment — the commit, when it landed and the run that put it there. This is a direct lookup rather than a scan of history, so it is the read to hang an incident page on.
gitmoap catalog get-service-current-deployments --service-id <id>curl -X GET https://api.gitmoap.example/catalog/services/{service_id}/deployments/current \ -H "Authorization: Bearer $GITMOAP_TOKEN"Choose History on the same panel for every attempt, newest first, filtered by environment or by status. Each entry carries who triggered it, who approved it, the commit and ref, the attempt number and, on a failure, the summary.
gitmoap catalog list-service-deployments --service-id <id>curl -X GET https://api.gitmoap.example/catalog/services/{service_id}/deployments \ -H "Authorization: Bearer $GITMOAP_TOKEN"From an environment, choose Deployments to invert the question: what is in this place, across every service. One row per service, most recently deployed first. It has no cursor — it is bounded by limit alone, which defaults to 20 and goes up to 100, so raise it when an environment holds more services than a page shows.
gitmoap catalog list-environment-deployments --environment-id <id>curl -X GET https://api.gitmoap.example/catalog/environments/{environment_id}/deployments \ -H "Authorization: Bearer $GITMOAP_TOKEN"Read the deployment board
The board is services down the side, environments across the top, the current deployment in each cell — the page people actually leave open.
Open the group's Catalog → Deployments tab. Choose which environments form the columns, or narrow them to one tier. Search matches a service's name, description or slug. An empty cell means nothing has deployed there, which is a different fact from a failure, and it is rendered differently.
gitmoap catalog list-group-deployments --group-id <id>curl -X GET https://api.gitmoap.example/groups/{group_id}/catalog/deployments \ -H "Authorization: Bearer $GITMOAP_TOKEN"The rows are the services this group declares, not its whole subtree. Open a subgroup's board to see that subgroup's services; the group you opened is the one that decides the rows.
Columns are read with inheritance, so a column can be an ancestor group's environment — which is the usual case, since most repositories deploy to a production declared far above them. A nearer slug shadows a farther one, archived environments are left out, and an ancestor's environment you have no access to is dropped rather than shown empty.
Every environment you can see is a column, on every page, in slug order. Paging moves through the services, never the columns. A board that would draw more than 500 columns is refused with 400 rather than drawn short: choose the environments you want or narrow to a tier.
The board deliberately does not carry drift. Working out how far behind each cell is costs a git walk per service, environment and repository, and a forty-by-four board would be 160 of them on a page nobody closes. Ask for drift one service at a time.
Find out how far behind a deployment is
Open the service and choose Drift on the deployments panel. Each row is one (environment, repository) pair with a current deployment, naming the environment, the repository and its default branch, the commit that is deployed, and how many commits behind that branch it is.
gitmoap catalog get-service-deployment-drift --service-id <id>curl -X GET https://api.gitmoap.example/catalog/services/{service_id}/deployments/drift \ -H "Authorization: Bearer $GITMOAP_TOKEN"Drift is reported per repository, never averaged. A service spanning three repositories has three answers and they are rendered as three rows, because a single number combining them is confidently wrong and a wrong number gets acted on. A row shows no count when the deployed commit can no longer be read — after a force-push, or a repository move — rather than failing the whole read.
Trace what a service depends on
Open the service and choose Dependencies. Each edge names the far service, what kind of coupling it is, and where the claim came from — a lockfile at a named commit, or somebody's declaration. An observed edge also carries the version the manifest asked for and the version the lockfile actually pinned.
gitmoap catalog list-service-dependencies --service-id <id>curl -X GET https://api.gitmoap.example/catalog/services/{service_id}/dependencies \ -H "Authorization: Bearer $GITMOAP_TOKEN"The Dependents tab is the reverse, and it is the question people actually ask during an upgrade: who is on the old version of this?
gitmoap catalog list-service-dependents --service-id <id>curl -X GET https://api.gitmoap.example/catalog/services/{service_id}/dependents \ -H "Authorization: Bearer $GITMOAP_TOKEN"Graph walks outward from the service, up to three hops, and lays the result out in rings by distance. Depth defaults to two.
gitmoap catalog get-service-graph --service-id <id>curl -X GET https://api.gitmoap.example/catalog/services/{service_id}/graph \ -H "Authorization: Bearer $GITMOAP_TOKEN"All three reads drop edges whose far end you cannot see and report how many they dropped, so a partial answer says so rather than looking complete. They are bounded rather than paged; each says whether its bound was reached.
See what checks exist before writing a scorecard
The scorecard editor's check picker is built from this registry. Each entry gives the check's name, what it reads, what makes it re-evaluate, whether it also re-runs on a schedule, and a form for its own parameters — a window in days for released recently, a tier and a window for deployed recently.
gitmoap catalog list-check-kindscurl -X GET https://api.gitmoap.example/catalog/check-kinds \ -H "Authorization: Bearer $GITMOAP_TOKEN"Thirteen derived checks ship: has_owner, human_declared, has_repository, has_pipeline, branch_protection, requires_review, released_within, deployed_within, has_runbook, has_api_spec, declared_dep_unobserved, observed_dep_undeclared and dependency_freshness. released_within takes a window in days, defaulting to 90; deployed_within takes a window defaulting to 30 days and a tier defaulting to production.
Applicability is decided by the facts a service has. Each check names what it reads — a binding, a repository, a deployment, a release, an interface — and a service with none of them resolves not applicable rather than failing, which is why a library the platform has never recorded a deployment for is never marked red for having none. The one check that reads kind rather than a fact is the API-spec check: an api or a service is expected to publish an interface description and a datastore is not. Everywhere else, kind decides which scorecards select a service; it does not excuse a check from one that already selected it.
Create a scorecard
Open the group, choose Catalog → Scorecards, then New scorecard. Give it a slug and a name, then a selector deciding which services it grades — by kind, by lifecycle, by annotation, or everything. Add checks from the registry, or bind one to evidence a pipeline produces, or make one attested with a renewal window. Levels are optional: name a rung and list the checks it requires.
gitmoap catalog create-scorecard --group-id <id> --slug <slug>curl -X POST https://api.gitmoap.example/groups/{group_id}/catalog/scorecards \ -H "Authorization: Bearer $GITMOAP_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "checks[].key": "<string>", "checks[].rule": "<object>", "levels[].name": "<string>", "slug": "<string>" }'Levels are a ladder, not a set of badges, so list them in the order you want them climbed. Grading walks them from the first and stops at the first rung whose required checks are not satisfied — a service cannot skip one, so nothing is ever graded Gold that never reached Bronze. A check counts as satisfied when it is passing or not applicable, which is what lets a library reach the top rung of a scorecard that includes a deployment check; a required check that has never been evaluated counts as unknown and does not satisfy.
A scorecard with no levels is a checklist, which is what most of them are.
Read a scorecard's results
Open the scorecard and read the results table. It opens worst-first, because the passing rows are not what you opened it for. Filter to one state, one check or one service. Beside the rows is the partition: how many services were scored and how they distribute across the levels, and how many were unscored with the reason — external entities live here, counted apart rather than folded into a percentage.
gitmoap catalog list-scorecard-results --scorecard-id <id>curl -X GET https://api.gitmoap.example/catalog/scorecards/{scorecard_id}/results \ -H "Authorization: Bearer $GITMOAP_TOKEN"The partition stays scorecard-wide whatever you filter by. Narrow to one service and the rows are that service's, but the counts beside them are still every service the scorecard grades — so read a per-service tally off the rows, not off the summary.
The group's Scorecards tab lists every scorecard that applies here, including those inherited from ancestor groups. Untick Include inherited to see only this group's own, or tick Enabled only to hide the ones switched off.
gitmoap catalog list-scorecards --group-id <id>curl -X GET https://api.gitmoap.example/groups/{group_id}/catalog/scorecards \ -H "Authorization: Bearer $GITMOAP_TOKEN"Selecting a scorecard opens its definition — selector, checks, levels, campaign switches and the version you need to amend it safely.
gitmoap catalog get-scorecard --scorecard-id <id>curl -X GET https://api.gitmoap.example/catalog/scorecards/{scorecard_id} \ -H "Authorization: Bearer $GITMOAP_TOKEN"From the other direction, a service's Scorecards panel shows one row per scorecard that applies to it, with the level it reaches and its counts by state.
gitmoap catalog get-service-scorecards --service-id <id>curl -X GET https://api.gitmoap.example/catalog/services/{service_id}/scorecards \ -H "Authorization: Bearer $GITMOAP_TOKEN"A result marked flapping means the check has changed state ten times today and the platform has stopped recording its edges. Read it as this check is not trustworthy — usually a threshold set at the noise floor, or a test that is not deterministic — and fix the check rather than the service.
Amend a scorecard, and run a campaign
Open the scorecard and choose Edit. Adding, removing and reconfiguring checks, editing levels and changing the selector are all Maintainer. Arming the campaign, and raising how many work items it may file, are Owner.
gitmoap catalog update-scorecard --scorecard-id <id>curl -X PATCH https://api.gitmoap.example/catalog/scorecards/{scorecard_id} \ -H "Authorization: Bearer $GITMOAP_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "checks[].key": "<string>", "checks[].rule": "<object>", "levels[].name": "<string>" }'A campaign turns red squares into work: a failing check whose Create work item box is ticked opens an item in the owning group's backlog, carrying a criterion bound to the check — so the item closes itself when the check passes and reopens if it regresses.
It is gated three times, and the gating is the design rather than caution. Work-item creation is off per check. The campaign is off per scorecard, so the whole set is inert until somebody switches it on. And each activation may file at most a set number of items — counted from zero each time the switch moves off to on, so a campaign paused and resumed next quarter does not start already exhausted. Four hundred services times twelve checks is 4,800 work items in one pass, which is the single most likely way this feature gets switched off across an organisation in week one.
Removing a check that a level still requires is refused, because the rung would be unreachable for ever.
Attest a check no machine can see
Some true statements about software are not measurable. We rehearsed the failover is one, and a system that demanded a metric for it would simply get nothing recorded.
Open the service's scorecard panel, find the attested check and choose Attest. Record it as passing or failing and add a note. The attestation expires after the check's renewal window, so we rehearsed it cannot quietly come to mean in 2023.
gitmoap catalog attest-service-check \ --service-id <id> \ --scorecard-id <id> \ --check-key <id> \ --state passing|failingcurl -X POST https://api.gitmoap.example/catalog/services/{service_id}/checks/{scorecard_id}/{check_key}/attest \ -H "Authorization: Bearer $GITMOAP_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "state": "<one of failing | passing>" }'Only passing and failing can be attested. stale, unknown and not_applicable are the platform's own vocabulary for what it could not determine, and letting a person write them would make nobody has looked indistinguishable from somebody said so. Attesting a derived or evidence check is refused for the same reason: a value the platform computes has exactly one writer.
Delete a scorecard
Warning
Deleting a scorecard destroys every result it produced and every state transition recorded against it — the whole history of what passed, what failed, and when each one changed. Recreating the scorecard starts the history over from unknown. If you want to stop grading without losing the record, switch the scorecard off instead: a disabled scorecard stops evaluating and keeps everything it has already found.
Open the scorecard, choose Settings, then Delete scorecard. The confirmation names the number of results going with it. Deleting requires Owner.
gitmoap catalog delete-scorecard --scorecard-id <id>curl -X DELETE https://api.gitmoap.example/catalog/scorecards/{scorecard_id} \ -H "Authorization: Bearer $GITMOAP_TOKEN"Reference
Services
9 endpoints
GET/groups/{group_id}/catalog/servicescatalog:read
List a group's services, each with its deployment promotion track
Token scope
catalog:read
Send
Path
- group_iduuidThe declaring group's id
Query
- kindstringoptionalFilter by service kind
- lifecyclestringoptionalFilter by lifecycle
- sourcestringoptionalFilter by how the entry came to exist
- include_orphanedbooleanoptionalInclude entries whose declaration has disappeared (excluded by default)
- searchstringoptionalMatch the name, description or a slug substring
- limitint32optionalPage size (1-100, default 20)
- next_tokenstringoptionalPagination cursor
Refuses
400Unknown filter value or malformed cursor401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404No such group, or it is not visible to the caller
Receive
items[]object10 fields
serviceobject10 fields
- display_namestring
- externalbooleanoptional
- group_iduuid
- kindapidatastorejoblibraryml_modelotherservicewebsiteWhat sort of thing a service is. The kind decides which scorecard checks apply: a
libraryhas no deployments, so a deployment check is not applicable to it rather than failing. For your own classifications, use annotations. - lifecycledeprecatedexperimentalproductionretiredWhere a service is in its life.
- orphaned_atobjectoptional
- ownerobjectoptional
- service_iduuid
- slugstring
- sourceapideclaredinferredHow the catalog entry came to exist. Set by the platform, never accepted from a request. Any
source other than
inferredmeans a person has vouched for the entry.
- next_tokenobjectoptional
GET/repositories/{repo_id}/catalog/servicescatalog:read
List the services bound to a repository, each with its deployment promotion track
Token scope
catalog:read
Send
Path
- repo_iduuidThe repository the services are bound to
Query
- kindstringoptionalFilter by service kind
- lifecyclestringoptionalFilter by lifecycle
- sourcestringoptionalFilter by how the entry came to exist
- include_orphanedbooleanoptionalInclude entries whose declaration has disappeared (excluded by default)
- searchstringoptionalMatch the name, description or a slug substring
- limitint32optionalPage size (1-100, default 20)
- next_tokenstringoptionalPagination cursor
Refuses
400Unknown filter value or malformed cursor401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404No such repository, or it is not visible to the caller
Receive
The same shape list_services returns — 2 fields. Go to it
Show it here
items[]object10 fields
serviceobject10 fields
- display_namestring
- externalbooleanoptional
- group_iduuid
- kindapidatastorejoblibraryml_modelotherservicewebsiteWhat sort of thing a service is. The kind decides which scorecard checks apply: a
libraryhas no deployments, so a deployment check is not applicable to it rather than failing. For your own classifications, use annotations. - lifecycledeprecatedexperimentalproductionretiredWhere a service is in its life.
- orphaned_atobjectoptional
- ownerobjectoptional
- service_iduuid
- slugstring
- sourceapideclaredinferredHow the catalog entry came to exist. Set by the platform, never accepted from a request. Any
source other than
inferredmeans a person has vouched for the entry.
- next_tokenobjectoptional
GET/catalog/services/{service_id}catalog:read
Get a service by id
Token scope
catalog:read
Send
Path
- service_iduuidThe service's id
Refuses
400Bad request — a path parameter is not a well-formed value of its type401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404No such service, or it is not visible to the caller
Receive
- annotationsobjectoptional
api_specs[]object3 fields
- pathstring
- repository_iduuid
- spec_kindasyncapigrpcopenapiWhich interface-description format a spec is written in.
- created_atstring
- created_byobjectoptional
- descriptionobjectoptional
- display_namestring
- externalbooleanoptionalWhether the service is built outside gitmoap — a vendor SaaS, a service on another forge, a database provisioned elsewhere. The catalog records its name, owner and the dependencies on it, and nothing else. An external service has no repository paths.
- group_iduuid
- kindapidatastorejoblibraryml_modelotherservicewebsiteWhat sort of thing a service is. The kind decides which scorecard checks apply: a
libraryhas no deployments, so a deployment check is not applicable to it rather than failing. For your own classifications, use annotations. - lifecycledeprecatedexperimentalproductionretiredWhere a service is in its life.
links[]object2 fields
- link_typechatdashboarddocsotherrunbookWhat a service link points at.
othercovers anything the listed kinds do not. - urlstring
- link_typechatdashboarddocsotherrunbookWhat a service link points at.
- orphaned_atobjectoptionalWhen the catalog file declaring this service stopped declaring it. The service is kept, with its history, rather than deleted.
- ownerobjectoptional
provides[]object2 fields
- formatcargonpmpypiThe package ecosystem a registry speaks. A group has at most one registry per format.
- namestring
repo_paths[]object4 fields
- is_excludebooleanoptionalWhether this glob removes its matches from the service's paths instead of adding them. It affects only its own service.
- path_globstring
- path_prefixstringThe longest literal prefix of
path_glob, worked out by the platform. Read-only. - repository_iduuid
- service_iduuid
- slugstring
GET/groups/{group_id}/catalog/services/by-slug/{slug}catalog:read
Get a service by its slug within a namespace
Token scope
catalog:read
Send
Path
- group_iduuidA group in the namespace to resolve within
- slugstringThe service's slug, unique per namespace
Refuses
400Bad request — a path parameter is not a well-formed value of its type401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404No such service, or it is not visible to the caller
Receive
The same shape get_service returns — 17 fields. Go to it
Show it here
- annotationsobjectoptional
api_specs[]object3 fields
- pathstring
- repository_iduuid
- spec_kindasyncapigrpcopenapiWhich interface-description format a spec is written in.
- created_atstring
- created_byobjectoptional
- descriptionobjectoptional
- display_namestring
- externalbooleanoptionalWhether the service is built outside gitmoap — a vendor SaaS, a service on another forge, a database provisioned elsewhere. The catalog records its name, owner and the dependencies on it, and nothing else. An external service has no repository paths.
- group_iduuid
- kindapidatastorejoblibraryml_modelotherservicewebsiteWhat sort of thing a service is. The kind decides which scorecard checks apply: a
libraryhas no deployments, so a deployment check is not applicable to it rather than failing. For your own classifications, use annotations. - lifecycledeprecatedexperimentalproductionretiredWhere a service is in its life.
links[]object2 fields
- link_typechatdashboarddocsotherrunbookWhat a service link points at.
othercovers anything the listed kinds do not. - urlstring
- link_typechatdashboarddocsotherrunbookWhat a service link points at.
- orphaned_atobjectoptionalWhen the catalog file declaring this service stopped declaring it. The service is kept, with its history, rather than deleted.
- ownerobjectoptional
provides[]object2 fields
- formatcargonpmpypiThe package ecosystem a registry speaks. A group has at most one registry per format.
- namestring
repo_paths[]object4 fields
- is_excludebooleanoptionalWhether this glob removes its matches from the service's paths instead of adding them. It affects only its own service.
- path_globstring
- path_prefixstringThe longest literal prefix of
path_glob, worked out by the platform. Read-only. - repository_iduuid
- service_iduuid
- slugstring
GET/catalog/services/{service_id}/overviewcatalog:read
Everything a service page shows, in one call
Token scope
catalog:read
Send
Path
- service_iduuidThe service's id
Refuses
400Bad request — a path parameter is not a well-formed value of its type401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404No such service, or it is not visible to the caller
Receive
current_deployments[]object8 fields
- commit_shastring
- deployed_atstring
- deployment_iduuid
- environment_iduuid
- pipeline_run_idobjectoptional
- repository_iduuid
- service_iduuid
- statusawaiting_approvalcancelledfailedrejectedrequestedrunningskippedsucceededsupersededWhere a deployment is in its life. It starts
requested; waits asawaiting_approvalwhen the environment needs approval, endingrejectedif refused; thenrunning, endingsucceeded,failedorcancelled.skippedmeans another deploy held the environment and its policy is to skip. A successful deployment becomessupersededwhen a later one replaces it.
debtobject8 fields
- driftedint32
- open_bugsint32
rows[]object6 fields
- driftedint32
- markersint32
- open_bugsint32
- open_storiesint32
- pathstring
- totalint32
dependency_countsobject3 fields
- hiddenint32Dependencies left out, in either direction, because you cannot see the service at the far end.
- inboundint32
- outboundint32
- latest_pipeline_runobjectoptional
- latest_releaseobjectoptional
- open_merge_requestsint32
- ownerobjectoptional
repositories[]object1 field
- default_branchstring
GET/repositories/{repo_id}/catalog/resolvecatalog:read
Resolve repository paths to the services that own them
Token scope
catalog:read
Send
Path
- repo_iduuidThe repository the paths belong to
Query
- patharrayA repository-relative path. Repeatable, up to 200 per call — one request per changed file set, not one per file
Refuses
400No paths given, or more than 200401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404No such repository, or it is not visible to the caller
Receive
resolved[]object2 fields
- pathstring
- serviceobjectoptional
POST/groups/{group_id}/catalog/servicescatalog:manage
Declare a service in a group
Token scope
catalog:manage
Counts against
services_per_scope
Send
Path
- group_iduuidThe declaring group's id
Body
- annotationsobjectoptional
api_specs[]object3 fields
- pathstring
- repository_iduuid
- spec_kindasyncapigrpcopenapiWhich interface-description format a spec is written in.
- descriptionobjectoptional
- display_nameobjectoptionalDefaults to the slug.
- externalbooleanoptional
- kindapidatastorejoblibraryml_modelotherservicewebsiteoptionalWhat sort of thing a service is. The kind decides which scorecard checks apply: a
libraryhas no deployments, so a deployment check is not applicable to it rather than failing. For your own classifications, use annotations. - lifecycledeprecatedexperimentalproductionretiredoptionalWhere a service is in its life.
links[]object2 fields
- link_typechatdashboarddocsotherrunbookWhat a service link points at.
othercovers anything the listed kinds do not. - urlstring
- link_typechatdashboarddocsotherrunbookWhat a service link points at.
- owner_group_idobjectoptional
- owner_principal_idobjectoptional
- owner_team_idobjectoptional
provides[]object2 fields
- formatcargonpmpypiThe package ecosystem a registry speaks. A group has at most one registry per format.
- namestring
repo_paths[]object3 fields
- is_excludebooleanoptional
- path_globstring
- repository_iduuid
- slugstring
Refuses
400Validation error401Unauthenticated — no credential, or an invalid one403Insufficient standing on the group, or on the group named as owner404No such group, or a named repository is not visible409The slug is already taken in this namespace429The namespace is at its service limit
Receive
The same shape get_service returns — 17 fields. Go to it
Show it here
- annotationsobjectoptional
api_specs[]object3 fields
- pathstring
- repository_iduuid
- spec_kindasyncapigrpcopenapiWhich interface-description format a spec is written in.
- created_atstring
- created_byobjectoptional
- descriptionobjectoptional
- display_namestring
- externalbooleanoptionalWhether the service is built outside gitmoap — a vendor SaaS, a service on another forge, a database provisioned elsewhere. The catalog records its name, owner and the dependencies on it, and nothing else. An external service has no repository paths.
- group_iduuid
- kindapidatastorejoblibraryml_modelotherservicewebsiteWhat sort of thing a service is. The kind decides which scorecard checks apply: a
libraryhas no deployments, so a deployment check is not applicable to it rather than failing. For your own classifications, use annotations. - lifecycledeprecatedexperimentalproductionretiredWhere a service is in its life.
links[]object2 fields
- link_typechatdashboarddocsotherrunbookWhat a service link points at.
othercovers anything the listed kinds do not. - urlstring
- link_typechatdashboarddocsotherrunbookWhat a service link points at.
- orphaned_atobjectoptionalWhen the catalog file declaring this service stopped declaring it. The service is kept, with its history, rather than deleted.
- ownerobjectoptional
provides[]object2 fields
- formatcargonpmpypiThe package ecosystem a registry speaks. A group has at most one registry per format.
- namestring
repo_paths[]object4 fields
- is_excludebooleanoptionalWhether this glob removes its matches from the service's paths instead of adding them. It affects only its own service.
- path_globstring
- path_prefixstringThe longest literal prefix of
path_glob, worked out by the platform. Read-only. - repository_iduuid
- service_iduuid
- slugstring
PATCH/catalog/services/{service_id}catalog:manage
Update a service
Token scope
catalog:manage
Send
Path
- service_iduuidThe service's id
Body
- annotationsobjectoptional
api_specs[]object3 fields
- pathstring
- repository_iduuid
- spec_kindasyncapigrpcopenapiWhich interface-description format a spec is written in.
- descriptionobjectoptional
- display_nameobjectoptional
- externalobjectoptional
- kindobjectoptional
- lifecycleobjectoptional
links[]object2 fields
- link_typechatdashboarddocsotherrunbookWhat a service link points at.
othercovers anything the listed kinds do not. - urlstring
- link_typechatdashboarddocsotherrunbookWhat a service link points at.
- owner_group_idobjectoptional
- owner_principal_idobjectoptional
- owner_team_idobjectoptional
provides[]object2 fields
- formatcargonpmpypiThe package ecosystem a registry speaks. A group has at most one registry per format.
- namestring
repo_paths[]object3 fields
- is_excludebooleanoptional
- path_globstring
- repository_iduuid
- slugobjectoptional
- versionint64optionalThe
versionyou last read. When given and the service has changed since, the update is refused with a409.
Refuses
400Validation error401Unauthenticated — no credential, or an invalid one403Insufficient standing on the group, or on the group named as the new owner404No such service, or it is not visible to the caller409The slug is taken, or the service was modified concurrently
Receive
The same shape get_service returns — 17 fields. Go to it
Show it here
- annotationsobjectoptional
api_specs[]object3 fields
- pathstring
- repository_iduuid
- spec_kindasyncapigrpcopenapiWhich interface-description format a spec is written in.
- created_atstring
- created_byobjectoptional
- descriptionobjectoptional
- display_namestring
- externalbooleanoptionalWhether the service is built outside gitmoap — a vendor SaaS, a service on another forge, a database provisioned elsewhere. The catalog records its name, owner and the dependencies on it, and nothing else. An external service has no repository paths.
- group_iduuid
- kindapidatastorejoblibraryml_modelotherservicewebsiteWhat sort of thing a service is. The kind decides which scorecard checks apply: a
libraryhas no deployments, so a deployment check is not applicable to it rather than failing. For your own classifications, use annotations. - lifecycledeprecatedexperimentalproductionretiredWhere a service is in its life.
links[]object2 fields
- link_typechatdashboarddocsotherrunbookWhat a service link points at.
othercovers anything the listed kinds do not. - urlstring
- link_typechatdashboarddocsotherrunbookWhat a service link points at.
- orphaned_atobjectoptionalWhen the catalog file declaring this service stopped declaring it. The service is kept, with its history, rather than deleted.
- ownerobjectoptional
provides[]object2 fields
- formatcargonpmpypiThe package ecosystem a registry speaks. A group has at most one registry per format.
- namestring
repo_paths[]object4 fields
- is_excludebooleanoptionalWhether this glob removes its matches from the service's paths instead of adding them. It affects only its own service.
- path_globstring
- path_prefixstringThe longest literal prefix of
path_glob, worked out by the platform. Read-only. - repository_iduuid
- service_iduuid
- slugstring
DELETE/catalog/services/{service_id}catalog:manage
Delete a service
Token scope
catalog:manage
Send
Path
- service_iduuidThe service's id
Refuses
400Bad request — a path parameter is not a well-formed value of its type401Unauthenticated — no credential, or an invalid one403Deleting a service requires Owner on its declaring group404No such service, or it is not visible to the caller409The request conflicts with the resource's current state; `reason` says how
Receive
No body.
Environments
7 endpoints
GET/groups/{group_id}/catalog/environmentscatalog:read
List a group's environments
Token scope
catalog:read
Send
Path
- group_iduuidThe owning group's id
Query
- tierstringoptionalFilter by tier
- include_archivedbooleanoptionalInclude archived environments (excluded by default)
- inheritedbooleanoptionalAlso return the ancestor-group environments this scope may reference, with a nearer slug shadowing a farther one
- searchstringoptionalMatch the slug or display name
- limitint32optionalPage size (1-100, default 20)
- next_tokenstringoptionalPagination cursor
Refuses
400Unknown filter value or malformed cursor401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404No such group, or it is not visible to the caller
Receive
items[]object7 fields
- archivedboolean
- display_nameobjectoptional
- environment_iduuid
- scopeobjectA group or a repository, whichever a setting belongs to.
- slugstring
- tierdevelopmentproductionstagingtestingWhat kind of environment this is, whatever it is called:
development,testing,stagingorproduction. Aproductionenvironment is protected by default. - urlobjectoptional
- next_tokenobjectoptional
GET/repositories/{repo_id}/catalog/environmentscatalog:read
List a repository's environments
Token scope
catalog:read
Send
Path
- repo_iduuidThe owning repository's id
Query
- tierstringoptionalFilter by tier
- include_archivedbooleanoptionalInclude archived environments (excluded by default)
- inheritedbooleanoptionalAlso return the ancestor-group environments this repository may reference — what a pipeline author needs, since a node's
environment:resolves up the chain - searchstringoptionalMatch the slug or display name
- limitint32optionalPage size (1-100, default 20)
- next_tokenstringoptionalPagination cursor
Refuses
400Unknown filter value or malformed cursor401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404No such repository, or it is not visible to the caller
Receive
The same shape list_group_environments returns — 2 fields. Go to it
Show it here
items[]object7 fields
- archivedboolean
- display_nameobjectoptional
- environment_iduuid
- scopeobjectA group or a repository, whichever a setting belongs to.
- slugstring
- tierdevelopmentproductionstagingtestingWhat kind of environment this is, whatever it is called:
development,testing,stagingorproduction. Aproductionenvironment is protected by default. - urlobjectoptional
- next_tokenobjectoptional
GET/catalog/environments/{environment_id}catalog:read
Get an environment by id
Token scope
catalog:read
Send
Path
- environment_iduuidThe environment's id
Refuses
400Bad request — a path parameter is not a well-formed value of its type401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404No such environment, or it is not visible to the caller
Receive
- approval_enforcedbooleanWhether the platform enforces
protection.requires_approvalon deploys, so a client can tell a policy that is applied from one that is only recorded. - archivedbooleanWhether the environment is archived. An archived environment can no longer be deployed to but keeps its deployment history, and archiving can be undone. An environment with deployments cannot be deleted, only archived.
concurrencyobject3 fields
- exclusivebooleanoptionalWhether deploys to this environment run one at a time. Off by default.
- max_lock_wait_secondsint32optionalHow long a queued deploy waits for its turn before failing, in seconds: from 60 (shorter
values are rounded up) to 21600.
0, the default, means it waits for as long as the pipeline's timeout allows. - modeobjectoptional
- created_atstring
- created_byobjectoptional
- display_nameobjectoptional
- environment_iduuid
- owner_full_pathstringThe full path of the group or repository that owns the environment (e.g.
acme/platform), which shows when an environment in a repository's listing comes from a group above it. With the slug it forms the environment's OIDC claim,{owner_full_path}:{slug}. protectionobject5 fields
- accept_fromobjectoptional
- prevent_self_approvalbooleanoptionalWhether the person who started the run is barred from approving its deployment. What counts is who started the run, not who wrote the commit: deploying somebody else's commit is still your deploy.
- requiredint32optionalDistinct approvals needed before the deployment starts, 1–10.
- requires_approvalbooleanoptionalWhen set, a deployment to this environment waits for approval before it starts. Nothing runs and no credentials are issued while it waits.
- wait_timer_secondsint32optionalHow long a deployment waits before it may start, in seconds, up to one hour.
0means no wait.
- scopeobjectA group or a repository, whichever a setting belongs to.
- slugstring
- tierdevelopmentproductionstagingtestingWhat kind of environment this is, whatever it is called:
development,testing,stagingorproduction. Aproductionenvironment is protected by default. - updated_atstring
- urlobjectoptional
- variable_countint32How many variables the environment holds.
- versionint64Increases with every change. Send it with an update to have the update refused with
409if someone else changed the environment first.
POST/groups/{group_id}/catalog/environmentscatalog:manage
Declare an environment on a group
Token scope
catalog:manage
Counts against
environments_per_scope
Send
Path
- group_iduuidThe owning group's id
Body
- concurrencyobjectoptional
- display_nameobjectoptional
- protectionobjectoptional
- slugstring
- tierdevelopmentproductionstagingtestingoptionalWhat kind of environment this is, whatever it is called:
development,testing,stagingorproduction. Aproductionenvironment is protected by default. - urlobjectoptional
Refuses
400Validation error, or a gate with no approver groups401Unauthenticated — no credential, or an invalid one403Insufficient standing, or Maintainer attempting a production-tier environment404No such group, or it is not visible to the caller409The slug is already taken on this scope429The scope is at its environment limit
Receive
The same shape get_environment returns — 16 fields. Go to it
Show it here
- approval_enforcedbooleanWhether the platform enforces
protection.requires_approvalon deploys, so a client can tell a policy that is applied from one that is only recorded. - archivedbooleanWhether the environment is archived. An archived environment can no longer be deployed to but keeps its deployment history, and archiving can be undone. An environment with deployments cannot be deleted, only archived.
concurrencyobject3 fields
- exclusivebooleanoptionalWhether deploys to this environment run one at a time. Off by default.
- max_lock_wait_secondsint32optionalHow long a queued deploy waits for its turn before failing, in seconds: from 60 (shorter
values are rounded up) to 21600.
0, the default, means it waits for as long as the pipeline's timeout allows. - modeobjectoptional
- created_atstring
- created_byobjectoptional
- display_nameobjectoptional
- environment_iduuid
- owner_full_pathstringThe full path of the group or repository that owns the environment (e.g.
acme/platform), which shows when an environment in a repository's listing comes from a group above it. With the slug it forms the environment's OIDC claim,{owner_full_path}:{slug}. protectionobject5 fields
- accept_fromobjectoptional
- prevent_self_approvalbooleanoptionalWhether the person who started the run is barred from approving its deployment. What counts is who started the run, not who wrote the commit: deploying somebody else's commit is still your deploy.
- requiredint32optionalDistinct approvals needed before the deployment starts, 1–10.
- requires_approvalbooleanoptionalWhen set, a deployment to this environment waits for approval before it starts. Nothing runs and no credentials are issued while it waits.
- wait_timer_secondsint32optionalHow long a deployment waits before it may start, in seconds, up to one hour.
0means no wait.
- scopeobjectA group or a repository, whichever a setting belongs to.
- slugstring
- tierdevelopmentproductionstagingtestingWhat kind of environment this is, whatever it is called:
development,testing,stagingorproduction. Aproductionenvironment is protected by default. - updated_atstring
- urlobjectoptional
- variable_countint32How many variables the environment holds.
- versionint64Increases with every change. Send it with an update to have the update refused with
409if someone else changed the environment first.
POST/repositories/{repo_id}/catalog/environmentscatalog:manage
Declare an environment on a repository
Token scope
catalog:manage
Counts against
environments_per_scope
Send
Path
- repo_iduuidThe owning repository's id
Body
- concurrencyobjectoptional
- display_nameobjectoptional
- protectionobjectoptional
- slugstring
- tierdevelopmentproductionstagingtestingoptionalWhat kind of environment this is, whatever it is called:
development,testing,stagingorproduction. Aproductionenvironment is protected by default. - urlobjectoptional
Refuses
400Validation error, or a gate with no approver groups401Unauthenticated — no credential, or an invalid one403Insufficient standing, or Maintainer attempting a production-tier environment404No such repository, or it is not visible to the caller409The slug is already taken on this scope429The scope is at its environment limit
Receive
The same shape get_environment returns — 16 fields. Go to it
Show it here
- approval_enforcedbooleanWhether the platform enforces
protection.requires_approvalon deploys, so a client can tell a policy that is applied from one that is only recorded. - archivedbooleanWhether the environment is archived. An archived environment can no longer be deployed to but keeps its deployment history, and archiving can be undone. An environment with deployments cannot be deleted, only archived.
concurrencyobject3 fields
- exclusivebooleanoptionalWhether deploys to this environment run one at a time. Off by default.
- max_lock_wait_secondsint32optionalHow long a queued deploy waits for its turn before failing, in seconds: from 60 (shorter
values are rounded up) to 21600.
0, the default, means it waits for as long as the pipeline's timeout allows. - modeobjectoptional
- created_atstring
- created_byobjectoptional
- display_nameobjectoptional
- environment_iduuid
- owner_full_pathstringThe full path of the group or repository that owns the environment (e.g.
acme/platform), which shows when an environment in a repository's listing comes from a group above it. With the slug it forms the environment's OIDC claim,{owner_full_path}:{slug}. protectionobject5 fields
- accept_fromobjectoptional
- prevent_self_approvalbooleanoptionalWhether the person who started the run is barred from approving its deployment. What counts is who started the run, not who wrote the commit: deploying somebody else's commit is still your deploy.
- requiredint32optionalDistinct approvals needed before the deployment starts, 1–10.
- requires_approvalbooleanoptionalWhen set, a deployment to this environment waits for approval before it starts. Nothing runs and no credentials are issued while it waits.
- wait_timer_secondsint32optionalHow long a deployment waits before it may start, in seconds, up to one hour.
0means no wait.
- scopeobjectA group or a repository, whichever a setting belongs to.
- slugstring
- tierdevelopmentproductionstagingtestingWhat kind of environment this is, whatever it is called:
development,testing,stagingorproduction. Aproductionenvironment is protected by default. - updated_atstring
- urlobjectoptional
- variable_countint32How many variables the environment holds.
- versionint64Increases with every change. Send it with an update to have the update refused with
409if someone else changed the environment first.
PATCH/catalog/environments/{environment_id}catalog:manage
Patch an environment
Token scope
catalog:manage
Send
Path
- environment_iduuidThe environment's id
Body
- archivedobjectoptional
- concurrencyobjectoptional
- display_nameobjectoptional
- protectionobjectoptional
- slugobjectoptional
- tierobjectoptional
- urlobjectoptional
- versionint64optionalOptimistic-concurrency guard. When supplied and stale, the write is a
409.
Refuses
400Validation error, or a gate with no approver groups401Unauthenticated — no credential, or an invalid one403Owner is required to edit protection, tier, or a production-tier environment404No such environment, or it is not visible to the caller409The slug is taken, or the supplied version is stale
Receive
The same shape get_environment returns — 16 fields. Go to it
Show it here
- approval_enforcedbooleanWhether the platform enforces
protection.requires_approvalon deploys, so a client can tell a policy that is applied from one that is only recorded. - archivedbooleanWhether the environment is archived. An archived environment can no longer be deployed to but keeps its deployment history, and archiving can be undone. An environment with deployments cannot be deleted, only archived.
concurrencyobject3 fields
- exclusivebooleanoptionalWhether deploys to this environment run one at a time. Off by default.
- max_lock_wait_secondsint32optionalHow long a queued deploy waits for its turn before failing, in seconds: from 60 (shorter
values are rounded up) to 21600.
0, the default, means it waits for as long as the pipeline's timeout allows. - modeobjectoptional
- created_atstring
- created_byobjectoptional
- display_nameobjectoptional
- environment_iduuid
- owner_full_pathstringThe full path of the group or repository that owns the environment (e.g.
acme/platform), which shows when an environment in a repository's listing comes from a group above it. With the slug it forms the environment's OIDC claim,{owner_full_path}:{slug}. protectionobject5 fields
- accept_fromobjectoptional
- prevent_self_approvalbooleanoptionalWhether the person who started the run is barred from approving its deployment. What counts is who started the run, not who wrote the commit: deploying somebody else's commit is still your deploy.
- requiredint32optionalDistinct approvals needed before the deployment starts, 1–10.
- requires_approvalbooleanoptionalWhen set, a deployment to this environment waits for approval before it starts. Nothing runs and no credentials are issued while it waits.
- wait_timer_secondsint32optionalHow long a deployment waits before it may start, in seconds, up to one hour.
0means no wait.
- scopeobjectA group or a repository, whichever a setting belongs to.
- slugstring
- tierdevelopmentproductionstagingtestingWhat kind of environment this is, whatever it is called:
development,testing,stagingorproduction. Aproductionenvironment is protected by default. - updated_atstring
- urlobjectoptional
- variable_countint32How many variables the environment holds.
- versionint64Increases with every change. Send it with an update to have the update refused with
409if someone else changed the environment first.
DELETE/catalog/environments/{environment_id}catalog:manage
Delete an environment
Token scope
catalog:manage
Send
Path
- environment_iduuidThe environment's id
Refuses
400Bad request — a path parameter is not a well-formed value of its type401Unauthenticated — no credential, or an invalid one403Owner is required to delete an environment404No such environment, or it is not visible to the caller409The environment has deployments; archive it instead501Environment variables are not configured on this deployment
Receive
No body.
Environment variables
3 endpoints
GET/catalog/environments/{environment_id}/variablescatalog:read
List an environment's variables
list_environment_variablesGET /catalog/environments/{environment_id}/variablesOpen in the API referenceToken scope
catalog:read
Send
Path
- environment_iduuidThe environment's id
Refuses
400Bad request — a path parameter is not a well-formed value of its type401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404No such environment, or it is not visible to the caller
Receive
items[]object6 fields
- created_atstring
- created_byobjectoptional
- namestring
- secretboolean
- updated_atstring
- valueobjectoptionalThe variable's value. Absent for a secret, whose value can be written but never read back.
- next_tokenobjectoptional
PUT/catalog/environments/{environment_id}/variables/{name}catalog:manage
Create or replace an environment variable
put_environment_variablePUT /catalog/environments/{environment_id}/variables/{name}Open in the API referenceToken scope
catalog:manage
Counts against
variables_per_environment
Send
Path
- environment_iduuidThe environment's id
- namestringThe variable's name (POSIX env-var shape)
Body
- secretbooleanoptionalWhether the value is a secret. A secret's value can never be read back and is redacted from
step logs; either way the step receives it as an environment variable. Defaults to
false. - valuestring
Refuses
400Invalid name or value, or a platform-reserved name401Unauthenticated — no credential, or an invalid one403Owner is required to write an environment variable404No such environment, or it is not visible to the caller409The request conflicts with the resource's current state; `reason` says how429The environment's variable quota is exhausted501Environment variables are not configured on this deployment
Receive
- created_atstring
- created_byobjectoptional
- namestring
- secretboolean
- updated_atstring
- valueobjectoptionalThe variable's value. Absent for a secret, whose value can be written but never read back.
DELETE/catalog/environments/{environment_id}/variables/{name}catalog:manage
Delete an environment variable
delete_environment_variableDELETE /catalog/environments/{environment_id}/variables/{name}Open in the API referenceToken scope
catalog:manage
Send
Path
- environment_iduuidThe environment's id
- namestringThe variable's name
Refuses
400Bad request — a path parameter is not a well-formed value of its type401Unauthenticated — no credential, or an invalid one403Owner is required to delete an environment variable404No such environment or no such variable409The request conflicts with the resource's current state; `reason` says how501Environment variables are not configured on this deployment
Receive
No body.
Deployments
5 endpoints
GET/catalog/services/{service_id}/deployments/currentcatalog:read
What a service has deployed right now
get_service_current_deploymentsGET /catalog/services/{service_id}/deployments/currentOpen in the API referenceToken scope
catalog:read
Send
Path
- service_iduuidThe service's id
Refuses
400Bad request — a path parameter is not a well-formed value of its type401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404No such service, or it is not visible to the caller
Receive
items[]object8 fields
- commit_shastring
- deployed_atstring
- deployment_iduuid
- environment_iduuid
- pipeline_run_idobjectoptional
- repository_iduuid
- service_iduuid
- statusawaiting_approvalcancelledfailedrejectedrequestedrunningskippedsucceededsupersededWhere a deployment is in its life. It starts
requested; waits asawaiting_approvalwhen the environment needs approval, endingrejectedif refused; thenrunning, endingsucceeded,failedorcancelled.skippedmeans another deploy held the environment and its policy is to skip. A successful deployment becomessupersededwhen a later one replaces it.
- next_tokenobjectoptional
GET/catalog/services/{service_id}/deploymentscatalog:read
A service's deployment history
Token scope
catalog:read
Send
Path
- service_iduuidThe service's id
Query
- environment_idstringoptionalOnly deployments to this environment
- statusstringoptionalFilter by deployment status
- limitint32optionalPage size (1-100, default 20)
- next_tokenstringoptionalPagination cursor
Refuses
400Unknown filter value or malformed cursor401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404No such service, or it is not visible to the caller
Receive
items[]object15 fields
- approved_byobjectoptional
- attemptint32
- commit_shastring
- deployment_iduuid
- environment_iduuid
- error_summaryobjectoptional
- finished_atobjectoptional
- node_idobjectoptional
- pipeline_run_idobjectoptional
- ref_nameobjectoptional
- repository_iduuid
- service_iduuid
- started_atstring
- statusawaiting_approvalcancelledfailedrejectedrequestedrunningskippedsucceededsupersededWhere a deployment is in its life. It starts
requested; waits asawaiting_approvalwhen the environment needs approval, endingrejectedif refused; thenrunning, endingsucceeded,failedorcancelled.skippedmeans another deploy held the environment and its policy is to skip. A successful deployment becomessupersededwhen a later one replaces it. - triggered_byobjectoptional
- next_tokenobjectoptional
GET/catalog/services/{service_id}/deployments/driftcatalog:read
How far behind its default branch each deployment is
get_service_deployment_driftGET /catalog/services/{service_id}/deployments/driftOpen in the API referenceToken scope
catalog:read
Send
Path
- service_iduuidThe service's id
Refuses
400Bad request — a path parameter is not a well-formed value of its type401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404No such service, or it is not visible to the caller
Receive
rows[]object13 fields
- commits_behindint32optionalHow many commits the default branch has that the deployed commit does not. Absent when the deployed commit can no longer be read, for example after a force-push.
- default_branchstring
- deployed_shastring
environmentobject7 fields
- archivedboolean
- display_nameobjectoptional
- environment_iduuid
- scopeobjectA group or a repository, whichever a setting belongs to.
- slugstring
- tierdevelopmentproductionstagingtestingWhat kind of environment this is, whatever it is called:
development,testing,stagingorproduction. Aproductionenvironment is protected by default. - urlobjectoptional
- repository_iduuid
- repository_pathstring
- truncatedbooleanoptional
- service_iduuid
GET/catalog/environments/{environment_id}/deploymentscatalog:read
What is deployed into an environment
list_environment_deploymentsGET /catalog/environments/{environment_id}/deploymentsOpen in the API referenceToken scope
catalog:read
Send
Path
- environment_iduuidThe environment's id
Query
- limitint32optionalPage size (1-100, default 20)
Refuses
400Bad request — a path parameter is not a well-formed value of its type401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404No such environment, or it is not visible to the caller
Receive
The same shape get_service_current_deployments returns — 2 fields. Go to it
Show it here
items[]object8 fields
- commit_shastring
- deployed_atstring
- deployment_iduuid
- environment_iduuid
- pipeline_run_idobjectoptional
- repository_iduuid
- service_iduuid
- statusawaiting_approvalcancelledfailedrejectedrequestedrunningskippedsucceededsupersededWhere a deployment is in its life. It starts
requested; waits asawaiting_approvalwhen the environment needs approval, endingrejectedif refused; thenrunning, endingsucceeded,failedorcancelled.skippedmeans another deploy held the environment and its policy is to skip. A successful deployment becomessupersededwhen a later one replaces it.
- next_tokenobjectoptional
GET/groups/{group_id}/catalog/deploymentscatalog:read
The deployment board — services × environments
Token scope
catalog:read
Send
Path
- group_iduuidThe group whose services the board lists
Query
- environment_idstringoptionalShow only these environments, as a comma-separated list of ids
- tierstringoptionalShow only environments of this tier
- searchstringoptionalMatch a service's name, description or slug substring
- limitint32optionalServices per page (1-100, default 20)
- next_tokenstringoptionalPagination cursor
Refuses
400Unknown filter value, malformed cursor, or more than 500 environment columns401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404No such group, or it is not visible to the caller
Receive
environments[]object7 fields
- archivedboolean
- display_nameobjectoptional
- environment_iduuid
- scopeobjectA group or a repository, whichever a setting belongs to.
- slugstring
- tierdevelopmentproductionstagingtestingWhat kind of environment this is, whatever it is called:
development,testing,stagingorproduction. Aproductionenvironment is protected by default. - urlobjectoptional
- next_tokenobjectoptional
rows[]object10 fields
serviceobject10 fields
- display_namestring
- externalbooleanoptional
- group_iduuid
- kindapidatastorejoblibraryml_modelotherservicewebsiteWhat sort of thing a service is. The kind decides which scorecard checks apply: a
libraryhas no deployments, so a deployment check is not applicable to it rather than failing. For your own classifications, use annotations. - lifecycledeprecatedexperimentalproductionretiredWhere a service is in its life.
- orphaned_atobjectoptional
- ownerobjectoptional
- service_iduuid
- slugstring
- sourceapideclaredinferredHow the catalog entry came to exist. Set by the platform, never accepted from a request. Any
source other than
inferredmeans a person has vouched for the entry.
Scorecards
9 endpoints
GET/catalog/check-kindscatalog:read
Describe every derived check the platform implements
Token scope
catalog:read
Send
Nothing — the address is the whole request.
Refuses
401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient
Receive
kinds[]object5 fields
- config_schemaobjectA JSON Schema over the fields — render a form from this.
- descriptionstringWhat the check means, and the one thing to do about a failure.
- keystring
- labelstring
- sweepsbooleanWhether this check is re-evaluated periodically as well as on activity — true for checks whose answer can change with time alone.
GET/groups/{group_id}/catalog/scorecardscatalog:read
List the scorecards that apply to a group
Token scope
catalog:read
Send
Path
- group_iduuidThe group's id
Query
- include_inheritedbooleanoptionalInclude ancestor scopes' scorecards. Defaults to true, because union inheritance is what makes a baseline a baseline
- enabled_onlybooleanoptionalOmit disabled scorecards
- limitint32optionalPage size (clamped)
- next_tokenstringoptionalOpaque pagination cursor
Refuses
400Invalid filter or cursor401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404No such group
Receive
items[]object8 fields
- campaign_activeboolean
- check_countint32
- enabledboolean
- level_countint32
- namestring
- scopeobjectA group or a repository, whichever a setting belongs to.
- scorecard_iduuid
- slugstring
- next_tokenobjectoptional
GET/catalog/scorecards/{scorecard_id}catalog:read
Read one scorecard
Token scope
catalog:read
Send
Path
- scorecard_iduuidThe scorecard's id
Refuses
400Bad request — a path parameter is not a well-formed value of its type401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404No such scorecard, or it is not visible
Receive
applies_toobject1 field
- annotationsobjectoptionalAnnotations a service must carry, as key–value pairs. Every entry must match; a service with extra annotations still matches.
- campaign_activebooleanWhether the scorecard's campaign is on. Checks that open work items on failure do so only while it is.
- campaign_enforcedbooleanWhether the platform acts on
campaign_active. Alwaystrue. - campaign_item_countint32How many work items the campaign has filed since it was last switched on. Read-only.
- campaign_max_itemsint32How many work items the campaign may file each time it is switched on. The count starts from
zero every time
campaign_activeis turned on. checks[]object4 fields
- configobjectoptionalThe check's own parameters, such as
{"days": 90}forreleased_within. An unknown parameter is refused with a400rather than ignored. - creates_work_itembooleanoptionalWhether a failure opens a work item in the owning group's backlog. Off by default, and it takes effect only while the scorecard's campaign is active.
- keystring
- ruleobjectHow a check decides its answer.
- configobjectoptionalThe check's own parameters, such as
- created_atstring
- created_byobjectoptional
- descriptionobjectoptional
- enabledboolean
levels[]object1 field
- namestring
- namestring
- scopeobjectA group or a repository, whichever a setting belongs to.
- scorecard_iduuid
- slugstring
- updated_atstring
- versionint64The scorecard's revision, incremented on every change. Send it with an update to have the
update refused with a
409if someone changed the scorecard since you read it.
POST/groups/{group_id}/catalog/scorecardscatalog:manage
Declare a scorecard on a group
Token scope
catalog:manage
Counts against
scorecards_per_scope
Send
Path
- group_iduuidThe owning group's id
Body
applies_toobject1 field
- annotationsobjectoptionalAnnotations a service must carry, as key–value pairs. Every entry must match; a service with extra annotations still matches.
- campaign_activebooleanoptionalWhether to switch the campaign on from the start. Setting it to
truerequires the Owner role. - campaign_max_itemsint32optionalHow many work items the campaign may file each time it is switched on. Defaults to 200; at most 1,000.
checks[]object4 fields
- configobjectoptionalThe check's own parameters, such as
{"days": 90}forreleased_within. An unknown parameter is refused with a400rather than ignored. - creates_work_itembooleanoptionalWhether a failure opens a work item in the owning group's backlog. Off by default, and it takes effect only while the scorecard's campaign is active.
- keystring
- ruleobjectHow a check decides its answer.
- configobjectoptionalThe check's own parameters, such as
- descriptionobjectoptional
- enabledbooleanoptional
levels[]object1 field
- namestring
- nameobjectoptionalDefaults to the slug.
- slugstring
Refuses
400Validation error, an unimplemented derived check, or a level requiring a check the scorecard does not carry401Unauthenticated — no credential, or an invalid one403Insufficient standing on the group, or Owner required to arm a campaign404No such group409The slug is already taken in this scope429The namespace is at its scorecard limit
Receive
The same shape get_scorecard returns — 17 fields. Go to it
Show it here
applies_toobject1 field
- annotationsobjectoptionalAnnotations a service must carry, as key–value pairs. Every entry must match; a service with extra annotations still matches.
- campaign_activebooleanWhether the scorecard's campaign is on. Checks that open work items on failure do so only while it is.
- campaign_enforcedbooleanWhether the platform acts on
campaign_active. Alwaystrue. - campaign_item_countint32How many work items the campaign has filed since it was last switched on. Read-only.
- campaign_max_itemsint32How many work items the campaign may file each time it is switched on. The count starts from
zero every time
campaign_activeis turned on. checks[]object4 fields
- configobjectoptionalThe check's own parameters, such as
{"days": 90}forreleased_within. An unknown parameter is refused with a400rather than ignored. - creates_work_itembooleanoptionalWhether a failure opens a work item in the owning group's backlog. Off by default, and it takes effect only while the scorecard's campaign is active.
- keystring
- ruleobjectHow a check decides its answer.
- configobjectoptionalThe check's own parameters, such as
- created_atstring
- created_byobjectoptional
- descriptionobjectoptional
- enabledboolean
levels[]object1 field
- namestring
- namestring
- scopeobjectA group or a repository, whichever a setting belongs to.
- scorecard_iduuid
- slugstring
- updated_atstring
- versionint64The scorecard's revision, incremented on every change. Send it with an update to have the
update refused with a
409if someone changed the scorecard since you read it.
PATCH/catalog/scorecards/{scorecard_id}catalog:manage
Amend a scorecard
Token scope
catalog:manage
Send
Path
- scorecard_iduuidThe scorecard's id
Body
- applies_toobjectoptional
- campaign_activeobjectoptionalSwitches the campaign on or off. Unlike every other field here, changing it requires the Owner role, because switching it on can file work items across every covered service.
- campaign_max_itemsint32optional
checks[]object4 fields
- configobjectoptionalThe check's own parameters, such as
{"days": 90}forreleased_within. An unknown parameter is refused with a400rather than ignored. - creates_work_itembooleanoptionalWhether a failure opens a work item in the owning group's backlog. Off by default, and it takes effect only while the scorecard's campaign is active.
- keystring
- ruleobjectHow a check decides its answer.
- configobjectoptionalThe check's own parameters, such as
- descriptionobjectoptional
- enabledobjectoptional
levels[]object1 field
- namestring
- nameobjectoptional
- slugobjectoptional
- versionint64optionalThe
versionyou last read. When given and the scorecard has changed since, the update is refused with a409.
Refuses
400Validation error, or a level requiring a check the amendment removed401Unauthenticated — no credential, or an invalid one403Insufficient standing, or Owner required to change the campaign switch404No such scorecard, or it is not visible409The scorecard was modified by someone else
Receive
The same shape get_scorecard returns — 17 fields. Go to it
Show it here
applies_toobject1 field
- annotationsobjectoptionalAnnotations a service must carry, as key–value pairs. Every entry must match; a service with extra annotations still matches.
- campaign_activebooleanWhether the scorecard's campaign is on. Checks that open work items on failure do so only while it is.
- campaign_enforcedbooleanWhether the platform acts on
campaign_active. Alwaystrue. - campaign_item_countint32How many work items the campaign has filed since it was last switched on. Read-only.
- campaign_max_itemsint32How many work items the campaign may file each time it is switched on. The count starts from
zero every time
campaign_activeis turned on. checks[]object4 fields
- configobjectoptionalThe check's own parameters, such as
{"days": 90}forreleased_within. An unknown parameter is refused with a400rather than ignored. - creates_work_itembooleanoptionalWhether a failure opens a work item in the owning group's backlog. Off by default, and it takes effect only while the scorecard's campaign is active.
- keystring
- ruleobjectHow a check decides its answer.
- configobjectoptionalThe check's own parameters, such as
- created_atstring
- created_byobjectoptional
- descriptionobjectoptional
- enabledboolean
levels[]object1 field
- namestring
- namestring
- scopeobjectA group or a repository, whichever a setting belongs to.
- scorecard_iduuid
- slugstring
- updated_atstring
- versionint64The scorecard's revision, incremented on every change. Send it with an update to have the
update refused with a
409if someone changed the scorecard since you read it.
DELETE/catalog/scorecards/{scorecard_id}catalog:manage
Delete a scorecard
Token scope
catalog:manage
Send
Path
- scorecard_iduuidThe scorecard's id
Refuses
400Bad request — a path parameter is not a well-formed value of its type401Unauthenticated — no credential, or an invalid one403Owner is required — a delete destroys every result keyed on it404No such scorecard, or it is not visible409The request conflicts with the resource's current state; `reason` says how
Receive
No body.
GET/catalog/scorecards/{scorecard_id}/resultscatalog:read
List a scorecard's results, worst state first
Token scope
catalog:read
Send
Path
- scorecard_iduuidThe scorecard's id
Query
- statestringoptionalFilter to one check state
- check_keystringoptionalFilter to one check
- service_idstringoptionalFilter to one service.
summaryis unaffected and stays scorecard-wide, so a per-service count is folded fromitems— which with this filter is that service's complete result set - limitint32optionalPage size (clamped)
- next_tokenstringoptionalOpaque pagination cursor
Refuses
400Invalid filter or cursor401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404No such scorecard, or it is not visible
Receive
items[]object15 fields
- attested_atobjectoptional
- attested_byobjectoptional
- check_keystring
- detailobjectoptional
- evaluated_atstring
- evidence_atobjectoptional
- evidence_commit_shaobjectoptional
- evidence_pipeline_idobjectoptional
- evidence_run_idobjectoptional
- expires_atobjectoptional
- flappingbooleanoptionalWhether the check has changed state ten times today, after which further changes are not recorded until tomorrow. Read it as "this check is not trustworthy": a threshold set at the noise floor, or a test that is not deterministic.
- observed_valuedoubleoptional
- scorecard_iduuid
- service_iduuid
- stateexpiredfailingnot_applicablepassingstaleunknownWhat a check currently says about a service, listed worst first.
failing: the check does not hold.stale: the evidence that used to prove it no longer does.expired: an attestation whose renewal window has passed.unknown: never evaluated.passing: the check holds.not_applicable: the check does not apply to this service, so it does not count against it.
- next_tokenobjectoptional
summaryobject8 fields
scoredobject8 fields
- by_levelobjectoptionalServices at each named level. Absent levels are absent keys, not zeroes.
checksobject6 fields
- expiredint32
- failingint32
- not_applicableint32
- passingint32
- staleint32
- unknownint32
- totalint32
GET/catalog/services/{service_id}/scorecardscatalog:read
Every scorecard's verdict on one service
Token scope
catalog:read
Send
Path
- service_iduuidThe service's id
Refuses
400Bad request — a path parameter is not a well-formed value of its type401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404No such service, or it is not visible
Receive
scorecards[]object10 fields
countsobject6 fields
- expiredint32
- failingint32
- not_applicableint32
- passingint32
- staleint32
- unknownint32
- levelobjectoptionalThe highest level every required check satisfies, or
nullwhen the scorecard has no levels or the service reaches none of them. - namestring
- scorecard_iduuid
- slugstring
POST/catalog/services/{service_id}/checks/{scorecard_id}/{check_key}/attestcatalog:manage
Attest a check no machine can see
attest_service_checkPOST /catalog/services/{service_id}/checks/{scorecard_id}/{check_key}/attestOpen in the API referenceToken scope
catalog:manage
Send
Path
- service_iduuidThe service's id
- scorecard_iduuidThe scorecard's id
- check_keystringThe check's key on that scorecard
Body
- noteobjectoptional
- statefailingpassingThe states an attestation may assert.
Refuses
400The note is too long401Unauthenticated — no credential, or an invalid one403Insufficient standing on the declaring group, or the caller is not a person404No such service, scorecard, or check409The check is not attestable: a derived or evidence check is computed by the platform alone
Receive
No body.
Dependencies
3 endpoints
GET/catalog/services/{service_id}/dependenciescatalog:read
List the services this service depends on
Token scope
catalog:read
Send
Path
- service_iduuidThe service's id
Refuses
400Bad request — a path parameter is not a well-formed value of its type401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404No such service, or it is not visible to the caller
Receive
edges[]object22 fields
- confidencefloatHow certain the dependency is:
1.0for anything the platform observed or a person stated, lower only for a derived one. detailobject5 fields
- formatobjectoptional
- lockfile_pathobjectoptional
- package_nameobjectoptional
- resolvedobjectoptionalThe version the lockfile actually pins.
- version_reqobjectoptionalWhat the manifest asked for, where the lockfile records it.
- from_service_iduuid
- kindapidatastoreotherpackageHow a service depends on the far end of a dependency — not what the far end is. The same
libraryservice can be consumed as a package by one service and called over HTTP by another. - observed_atobjectoptional
- observed_repoobjectoptional
- observed_shaobjectoptional
- sourcedeclaredinferredobserved_lockfileobserved_publishHow a dependency came to be known, and therefore how far it can be trusted. A declared and an observed dependency between the same two services are reported separately, so a declaration no lockfile mentions, or a lockfile entry nobody declared, stands out.
toobject10 fields
- display_namestring
- externalbooleanoptional
- group_iduuid
- kindapidatastorejoblibraryml_modelotherservicewebsiteWhat sort of thing a service is. The kind decides which scorecard checks apply: a
libraryhas no deployments, so a deployment check is not applicable to it rather than failing. For your own classifications, use annotations. - lifecycledeprecatedexperimentalproductionretiredWhere a service is in its life.
- orphaned_atobjectoptional
- ownerobjectoptional
- service_iduuid
- slugstring
- sourceapideclaredinferredHow the catalog entry came to exist. Set by the platform, never accepted from a request. Any
source other than
inferredmeans a person has vouched for the entry.
- confidencefloatHow certain the dependency is:
- hiddenint32Dependencies left out because you cannot see the service at the far end.
- truncatedbooleanWhether the list stopped at its limit of 500 and more exist.
GET/catalog/services/{service_id}/dependentscatalog:read
List the services that depend on this service
Token scope
catalog:read
Send
Path
- service_iduuidThe service's id
Refuses
400Bad request — a path parameter is not a well-formed value of its type401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404No such service, or it is not visible to the caller
Receive
The same shape list_service_dependencies returns — 3 fields. Go to it
Show it here
edges[]object22 fields
- confidencefloatHow certain the dependency is:
1.0for anything the platform observed or a person stated, lower only for a derived one. detailobject5 fields
- formatobjectoptional
- lockfile_pathobjectoptional
- package_nameobjectoptional
- resolvedobjectoptionalThe version the lockfile actually pins.
- version_reqobjectoptionalWhat the manifest asked for, where the lockfile records it.
- from_service_iduuid
- kindapidatastoreotherpackageHow a service depends on the far end of a dependency — not what the far end is. The same
libraryservice can be consumed as a package by one service and called over HTTP by another. - observed_atobjectoptional
- observed_repoobjectoptional
- observed_shaobjectoptional
- sourcedeclaredinferredobserved_lockfileobserved_publishHow a dependency came to be known, and therefore how far it can be trusted. A declared and an observed dependency between the same two services are reported separately, so a declaration no lockfile mentions, or a lockfile entry nobody declared, stands out.
toobject10 fields
- display_namestring
- externalbooleanoptional
- group_iduuid
- kindapidatastorejoblibraryml_modelotherservicewebsiteWhat sort of thing a service is. The kind decides which scorecard checks apply: a
libraryhas no deployments, so a deployment check is not applicable to it rather than failing. For your own classifications, use annotations. - lifecycledeprecatedexperimentalproductionretiredWhere a service is in its life.
- orphaned_atobjectoptional
- ownerobjectoptional
- service_iduuid
- slugstring
- sourceapideclaredinferredHow the catalog entry came to exist. Set by the platform, never accepted from a request. Any
source other than
inferredmeans a person has vouched for the entry.
- confidencefloatHow certain the dependency is:
- hiddenint32Dependencies left out because you cannot see the service at the far end.
- truncatedbooleanWhether the list stopped at its limit of 500 and more exist.
GET/catalog/services/{service_id}/graphcatalog:read
Walk the dependency graph outward from a service
Token scope
catalog:read
Send
Path
- service_iduuidThe service the walk starts from
Query
- depthint32optionalHow many hops to follow (1-3, default 2)
Refuses
400The depth is outside 1-3401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404No such service, or it is not visible to the caller
Receive
- depthint32How many hops were followed: the requested depth, at most 3.
edges[]object5 fields
- depthint32How many hops from the root service this dependency was reached at, from 1 to the graph's
depth. - from_service_iduuid
- kindapidatastoreotherpackageHow a service depends on the far end of a dependency — not what the far end is. The same
libraryservice can be consumed as a package by one service and called over HTTP by another. - sourcedeclaredinferredobserved_lockfileobserved_publishHow a dependency came to be known, and therefore how far it can be trusted. A declared and an observed dependency between the same two services are reported separately, so a declaration no lockfile mentions, or a lockfile entry nobody declared, stands out.
- to_service_iduuid
- depthint32How many hops from the root service this dependency was reached at, from 1 to the graph's
- hiddenint32Dependencies left out because you cannot see the service at the far end.
nodes[]object10 fields
- display_namestring
- externalbooleanoptional
- group_iduuid
- kindapidatastorejoblibraryml_modelotherservicewebsiteWhat sort of thing a service is. The kind decides which scorecard checks apply: a
libraryhas no deployments, so a deployment check is not applicable to it rather than failing. For your own classifications, use annotations. - lifecycledeprecatedexperimentalproductionretiredWhere a service is in its life.
- orphaned_atobjectoptional
- ownerobjectoptional
- service_iduuid
- slugstring
- sourceapideclaredinferredHow the catalog entry came to exist. Set by the platform, never accepted from a request. Any
source other than
inferredmeans a person has vouched for the entry.
rootobject7 fields
- display_namestring
- externalbooleanoptional
- group_iduuid
- kindapidatastorejoblibraryml_modelotherservicewebsiteWhat sort of thing a service is. The kind decides which scorecard checks apply: a
libraryhas no deployments, so a deployment check is not applicable to it rather than failing. For your own classifications, use annotations. - lifecycledeprecatedexperimentalproductionretiredWhere a service is in its life.
- orphaned_atobjectoptional
- ownerobjectoptional
Limits
Three of the catalog's caps move with your plan. Each is counted on the scope that holds the entry — the group for a service or a scorecard, the group or the repository for an environment — not across your whole organisation.
| Limit | Free | Pro | Team | Enterprise |
|---|---|---|---|---|
| Services per scope | 200 | 2,000 | 20,000 | 200,000 |
| Environments per scope | 10 | 25 | 50 | 100 |
| Scorecards per scope | 5 | 15 | 30 | 60 |
The services cap counts only entries a person declared — through the API, the CLI, the web app or a catalog file. The service created alongside each repository never counts against it, so a service limit can never be the reason a repository creation fails.
Variables are capped at 100 per environment on every plan, and that one is fixed rather than commercial: each secret is copied into the run for every node on every attempt, so a plan selling 500 of them would be selling a failure.
The rest are fixed bounds on the shape of a single entity:
| Bound | Value |
|---|---|
| Path globs per service, inclusions and exclusions together | 50 |
| Links per service | 20 |
| Published packages declared per service | 20 |
| API specs declared per service | 20 |
| Annotations per service, and the length of one value | 50, 500 characters |
| Declared dependencies per service | 50 |
| Checks per scorecard, and levels per scorecard | 50 checks, 20 levels |
| Evidence-backed checks one repository may have evaluated by a single pipeline run | 5,000 |
| Annotation matchers in one scorecard selector | 20 |
| Services in one catalog file | 100 |
| Catalog file size | 256 KiB |
| Lockfile size, and lockfiles read per push | 4 MiB each, 20 per push, 32 MiB in total |
| Paths per resolve request | 200 |
| Dependency edges per read | 500 |
| Dependency graph depth, and nodes returned | 3 hops, 200 nodes |
| Environment variable name, and value size | 128 characters, 4 KiB |
| Approvers, allowed refs, and allowed scopes on one environment | 50, 20, 100 |
| Approvals an environment can require | 1 to 10 |
| Wait timer after approval | up to 1 hour |
| Work items one campaign activation may file | 200 by default, 1,000 maximum |
| Recorded state changes per check per day | 10, after which the check is marked flapping |
| Attestation renewal window, and note length | up to 3,650 days, 2,000 characters |
Reaching a cap refuses the whole request with 429, and nothing is partially created. A create that would exceed the services limit writes no service and no bindings; a catalog file declaring more services than you have room for applies none of them and reports it on the commit status.
To raise a plan-backed limit, upgrade the plan, or — as an Owner of the group — ask for an increase on the group's own limits page, which records the request for review. The fixed bounds above are not raisable, and reaching one usually means the entity wants splitting: a service with more than fifty path globs is generally two services.
Troubleshooting
400 BAD_REQUESTmentioning a slug
- Cause
- The slug is not lowercase letters, digits and hyphens, 1–64 characters, without a leading or trailing hyphen
- What to do
- Rename it. Slugs appear in URLs, in files and in deployment identifiers, so the charset is deliberately narrow
400 INVALID_PATH_GLOB
- Cause
- A path binding carries an empty glob, or one that is only whitespace
- What to do
- Give it a pattern, written relative to the repository root and without a leading slash —
services/checkout/**. One glob may be up to 500 characters; a longer one is refused as a constraint violation
400 SERVICE_REQUIRES_PATHS
- Cause
- A service that is not external has no including binding — either none at all, or nothing but exclusions
- What to do
- Add at least one binding that includes a path, or mark the entry external if the platform does not build it. An exclusion narrows a set; it cannot be the whole set. On an update the rule reads the bindings the service ends up with, so an update that leaves them alone never trips it
400 EXTERNAL_REQUIRES_NO_PATHS
- Cause
- An external entity was given a path binding
- What to do
- Remove the bindings, or untick external — an external entity is a name and an owner, with no code behind it
400 BAD_REQUESTon an environment, mentioning approvers
- Cause
- Protection requires approval but names no approvers, names more than 50, or names
anyoneor code owners — orrequiredis outside 1 to 10 - What to do
- Name between one and 50 people, teams or roles, and require between one and 10 approvals. A gate nobody can open is refused at write time rather than discovered during an incident
404 NOT_FOUNDon an environment, naming a team or a group
- Cause
- An approver names a team, group or repository that is not in your organisation
- What to do
- Check the id. Approvers can only be people, teams and roles of the organisation the environment belongs to
400 BAD_REQUESTon an environment, mentioning allowed scopes
- Cause
- The allowlist was sent as an empty list, which means nobody rather than everybody
- What to do
- Omit the field entirely to let the declaring scope's subtree use the environment, or name at least one group or repository
400 BAD_REQUESTon a variable, naming the variable
- Cause
- The name is not shell-shaped, the value is over 4 KiB or holds a NUL, or the name is reserved by the platform
- What to do
- Rename it. Set a cloud region or role on the provider connection instead, where it is authorized
400 UNKNOWN_CHECKor 400 RESERVED_CHECK_KEY
- Cause
- A derived check names a key the platform does not implement, or an evidence check reuses a key the registry already owns
- What to do
- Pick a key from the check registry for a derived check, and a name of your own for an evidence one. Check keys are
snake_case, up to 64 characters —has-ownerandhas_ownerrender identically in a list, so hyphens are refused rather than silently made a second thing
400 BAD_REQUESTon a level
- Cause
- A level requires a check the scorecard does not carry, usually because the amendment removed it
- What to do
- Remove the key from the level, or keep the check. A rung requiring a missing check is unreachable for ever
400 BAD_REQUESTon resolve
- Cause
- No
pathwas given, or more than 200 were - What to do
- Send between one and 200 paths per call, and batch a changed file set into one request
400 BAD_REQUESTon the graph
- Cause
depthis outside 1–3- What to do
- Ask for at most three hops. Beyond that, ask the reverse question about one dependency instead
400 BAD_REQUESTon a listing
- Cause
- An unrecognised filter value — a tier, kind, lifecycle, source or status that does not exist — or a cursor from a different query
- What to do
- Check the spelling against the values the card lists. A page size over the maximum is clamped rather than refused, but a bad filter is refused, because a caller who filters and gets everything back has been told something false
401 UNAUTHORIZED
- Cause
- No credential, an expired one, or a token that has been revoked
- What to do
- Sign in again, or mint a new token. Services, dependencies and scorecards read without one when the declaring group is public; environments, variables and deployments never do
403 FORBIDDENon a create where reads work
- Cause
- Declaring a service, environment or scorecard needs Maintainer on the owning group or repository
- What to do
- Ask an Owner or Maintainer to raise your access level on that scope
403 FORBIDDENreading an environment or a deployment in a group whose services you can browse
- Cause
- Environments, their variables and deployments have no public-read shortcut. They need Reporter on the owning scope, which services and scorecards do not
- What to do
- Ask to be added to the group or the repository as a Reporter. The service, its scorecards and its dependency edges keep answering in the meantime
403 FORBIDDENwhen naming an owner
- Cause
- You have Maintainer on the service's group but not on the group you named as owner
- What to do
- Ask a Maintainer of that team to raise your level there, or ask them to make the change. This check is what stops ownership being conferred on a team that never agreed to it
403 FORBIDDENon deleting an environment
- Cause
- Deleting an environment is Owner, one level above creating it. Deleting a service or a scorecard needs only Maintainer
- What to do
- Ask an Owner of the group to delete the environment, or use the reversible operation instead — retire a service, archive an environment, disable a scorecard
403 FORBIDDENon a production environment
- Cause
- Creating a production-tier environment, changing a tier, or changing protection at any tier is Owner
- What to do
- Ask an Owner. Maintainer covers everything else about an environment
403 FORBIDDENon a variable write
- Cause
- Writing and deleting environment variables is Owner, at every tier
- What to do
- Ask an Owner. Reading the list is Reporter, so you can still see which names exist
403 FORBIDDENon the campaign switch
- Cause
- Arming a campaign — moving
campaign_active— is Owner even though the rest of the scorecard is Maintainer - What to do
- Ask an Owner to arm it. Everything else about the scorecard, including how many items a campaign may file, is yours to edit
403 INSUFFICIENT_SCOPE
- Cause
- The token is valid but was minted without
catalog:readorcatalog:manage - What to do
- Mint a new token carrying the scope. Scopes are fixed when a token is created and cannot be added to it afterwards
404 NOT_FOUNDon something you are sure exists
- Cause
- The declaring group is private and you are not a member, or the id is wrong
- What to do
- A private scope answers
404rather than403so its existence stays private. Check your membership of the declaring group, then check the id
404 NOT_FOUNDnaming a repository you can see
- Cause
- Binding a repository to a service needs Maintainer on that repository, and the refusal is reported as an absence so it cannot be used to probe for repositories
- What to do
- Ask a Maintainer or Owner of that repository to raise your level there, or ask them to add the binding
404 NOT_FOUNDon a variable
- Cause
- The environment exists but holds no variable by that name
- What to do
- List the variables first. Names are case-sensitive
409 CONFLICTon a service create, or on renaming one
- Cause
- Another service in the organisation already holds that slug — possibly an orphaned one, which keeps its slug
- What to do
- Pick another slug, or find the holder and delete or rename it. Slugs are unique across the whole organisation, not per group. The same collision from a catalog file is reported on the commit status instead, and that message names the repository holding it
409 CONFLICTon an environment create
- Cause
- The scope already has an environment with that slug
- What to do
- Pick another slug, or edit the existing one. A repository may reuse a slug an ancestor group declares — that shadows it, deliberately
409 CONFLICTon a scorecard create
- Cause
- The group already has a scorecard with that slug
- What to do
- Pick another slug, or amend the existing one. A scorecard slug is unique on its own scope only, so a subgroup may reuse a name an ancestor uses — and both then grade the services below it
409 CONFLICTon an update, mentioning the version
- Cause
- Somebody changed the entry since you read it
- What to do
- Re-read it, reapply your change on top and save again. Nothing was overwritten
409 CONFLICTon deleting an environment
- Cause
- Something has been deployed to it, and deployment history is not erasable through a delete
- What to do
- Archive it instead. An archived environment leaves resolution while its history stays readable
409 CHECK_NOT_ATTESTABLE
- Cause
- The check is derived or evidence-backed, so the platform computes it
- What to do
- Attest only checks defined as attested. If the automated answer is wrong, fix the check's configuration or the fact behind it
429 TOO_MANY_REQUESTS
- Cause
- The scope reached its services, environments or scorecards limit, or the environment reached 100 variables
- What to do
- Nothing was created. Delete something you no longer need, upgrade the plan, or ask an Owner to request an increase
A pushed catalog file changed nothing
- Cause
- It was pushed to a branch that is not the default one. Files are validated everywhere and applied only on the default branch
- What to do
- Merge it. The commit status on your branch already tells you whether it would apply cleanly
A red gitmoap/catalog commit status
- Cause
- The file failed validation — an unknown kind, an owner path that does not resolve, a slug another repository holds, or a bound exceeded
- What to do
- Read the status text; it names the line or the holder. Nothing was applied, so fixing the file and pushing again is the whole repair
A service shows as orphaned
- Cause
- Its declaration disappeared from the catalog file — usually a bad merge, a renamed file, or a slug edited in place
- What to do
- Restore the declaration and the same service is revived with all its history. A slug edit is a delete plus a create, so rename slugs through the API instead
The catalog only updates when somebody pushes directly
- Cause
- It should not — a merge request landing on the default branch syncs the same way a push does
- What to do
- If a merge changed a catalog file and the entry did not move, check that the file is at
.gitmoap/catalog.yaml, or the same path with a.ymlextension, and that only one of the two exists; both at once is a validation error naming them
The deployment board is empty for a service that deploys
- Cause
- The pipeline's deploy node names no environment, or no service in a repository holding more than one
- What to do
- Add
environment:andservice:to the node. An ambiguous deploy is a resolution error rather than a guess, because a mis-attributed production deployment is the worst thing this can produce
Drift shows no commit count
- Cause
- The deployed commit can no longer be read — a force-push rewrote it, or the repository moved
- What to do
- Deploy again to re-anchor the comparison. The rest of the row is still accurate
A dependency edge is missing
- Cause
- The lockfile is over 4 MiB, the repository holds more than 20 readable lockfiles or more than 32 MiB of them in total, or the format is one of the ones not read — only
Cargo.lockandpackage-lock.jsonare parsed - What to do
- Check the file's size and name. An unread lockfile is named in a warning on the catalog commit status, and it leaves the edges it established untouched rather than deleting them — so nothing was lost, but nothing was learned either. For any other ecosystem, declare the edge in
depends_on:
A dependency read reports hidden edges
- Cause
- Some far ends are in groups you cannot see, so those edges were dropped
- What to do
- Ask for access to the far group, or read the count as what it is — a total that says how much of itself is missing
A check sits at unknown
- Cause
- Nothing has evaluated it yet: an evidence check whose pipeline has not run, a derived check awaiting its next recompute, or a repository over the 5,000 evidence-binding ceiling, whose checks a run skips whole rather than in part
- What to do
- Run the pipeline that produces the evidence. Derived checks recompute on the activity that could change them, and some also sweep on a schedule. If every evidence check in one repository is stuck, narrow the selectors of the scorecards grading its services
A check went stale
- Cause
- The pipeline that last proved it ran again and produced no report for it this time — a deleted test, a renamed report, a step that was skipped
- What to do
- Restore the report to that pipeline's output, or point the binding at the pipeline that actually produces it. An unrelated pipeline running never stales a check, so the run to look at is the one that used to prove it
A library is marked failing on a deployment check
- Cause
- A check is excused only for a service the platform holds no such fact about, and one deployment ever recorded is enough to make the question apply again
- What to do
- Narrow the scorecard's selector so it grades only the kinds you mean, or drop the check from the level that requires it. Changing
kindalone will not fix it — kind decides which scorecards select the service, not which checks apply once one has
A step cannot see a variable you set
- Cause
- The name is reserved, or a provider connection or a group or repository pipeline variable sets the same name with a different secrecy
- What to do
- Rename the variable. A reserved name keeps its value but loses its export; a secrecy collision fails the run and names both sources
A run is refused, naming a step, a variable and this environment
- Cause
- The step's configuration reads that variable with
{"$var": …}, and the environment does not declare it — or declares it as a secret, which configuration cannot read - What to do
- Add the variable to the environment, without the secret flag if the configuration should read it. A secret stays readable inside the step as
$NAME
A deploy waits and never starts
- Cause
- The environment requires approval and not enough of its approvers have approved, or the wait timer has not elapsed
- What to do
- Ask the named approvers to decide it — a gate needing two approvals waits for two different people. Being an Owner of the organisation does not confer approval — being named does, and that is the point of the gate
Related
- Pipelinesdefining a run, and the nodes that name a service and an environment
- Using providersthe steps a pipeline runs, and the connections that hold their credentials
- Cloud authentication from a pipelinereaching your own cloud with a short-lived token whose claims name the environment being deployed to
- Repository rulesprotecting a branch, and the
CODEOWNERSfile that wins over the catalog wherever it matches - Groupsthe namespaces services are declared in, the teams that own them, and where usage and limits live
- Repositoriescreating one, which is also what mints its first service
- Work itemsthe backlog a failing check files work into
- Linking work to codethe anchors behind a service's technical-debt rollup
- Package registriesthe npm, Cargo and PyPI registries whose publishes and lockfiles become dependency edges
- Access tokensminting one that carries
catalog:readorcatalog:manage - Billing and plansupgrading, and asking for a limit increase