Lifecycle Set + Backstage Collector
Enforce Lifecycle Set using data collected by Backstage Collector. Automatically check repository and ownership standards on every PR.
How Backstage Collector Powers This Guardrail
The Backstage Collector gathers metadata from your service catalog systems. This data flows into Lunar's Component JSON, where the Lifecycle Set guardrail evaluates it against your standards.
When enabled, this check runs automatically on every PR and in AI coding workflows, providing real-time enforcement with actionable feedback.
Quick Start Configuration
Add both the collector and policy to your lunar-config.yml to enable this guardrail.
# Step 1: Enable the Backstage Collector
collectors:
- uses: github://earthly/lunar-lib/collectors/backstage@v1.0.5
# with: ...
# Step 2: Enable the Backstage Guardrails
policies:
- uses: github://earthly/lunar-lib/policies/backstage@v1.0.5
include: [lifecycle-set]
# with: ...
What Backstage Collector Collects
This collector gathers the following data that the Lifecycle Set guardrail evaluates.
catalog-info
Scans the repository for a catalog-info.yaml file (configurable paths)
and parses its contents as-is into .catalog.native.backstage
(apiVersion, kind, metadata, spec). Annotations are preserved verbatim
with their backstage.io/ or vendor prefixes. Lints the file for
schema/syntax issues and writes any findings to
.catalog.native.backstage.valid and .catalog.native.backstage.errors[].
When backstage_url is configured, it additionally performs a
referential-integrity check: for each grouping reference declared in the
file (spec.domain, spec.system) it queries the Backstage catalog API
(GET /api/catalog/entities/by-name/<kind>/<namespace>/<name>) and
records the outcome under .catalog.native.backstage.refs. Whenever
backstage_url is set the collector writes .refs.checked: true as the
"referential integrity ran" signal, plus a per-reference entry: a
definitive lookup records .refs.<kind> = {name, exists} (exists: true
on a 200, false on a 404), while a transient failure (timeout/5xx)
records .refs.<kind> = {name, error} so an outage stays distinguishable
from a real miss. The <namespace> is derived from the reference itself —
a qualified ref (ns/name) carries its own, otherwise the component's own
metadata.namespace is used, falling back to default — so no namespace
configuration is required. When backstage_url is empty (the default) no
lookups run and .refs is not written at all — behavior is identical to
today, and the policy's referential-integrity checks skip (pass) rather
than fail.
Example Data Flow
Here's an example of the data that Backstage Collector writes to the Component JSON, which Lifecycle Set then evaluates.
{
"catalog": {
"native": {
"backstage": {
"valid": true,
"errors": [],
"path": "catalog-info.yaml",
"apiVersion": "backstage.io/v1alpha1",
"kind": "Component",
"metadata": {
"name": "payment-api",
"description": "Payment processing API",
"annotations": {
"backstage.io/techdocs-ref": "dir:.",
"pagerduty.com/integration-key": "PXXXXXX",
"grafana/dashboard-selector": "https://grafana.example.com/d/abc123",
"backstage.io/source-location": "url:https://github.com/acme/payment-api"
},
"tags": ["payments", "api", "tier1"]
},
"spec": {
"type": "service",
"owner": "team-payments",
"lifecycle": "production",
"domain": "payments",
"system": "payment-platform",
"providesApis": ["payment-api"],
"consumesApis": ["user-api", "notification-api"],
"dependsOn": ["resource:database-payments", "resource:cache-redis"]
},
"refs": {
"checked": true,
"domain": { "name": "payments", "exists": true },
"system": { "name": "payment-platform", "exists": true }
}
}
}
}
}
Configuration Options
Backstage Collector Inputs
| Input | Required | Default | Description |
|---|---|---|---|
paths |
Optional |
catalog-info.yaml,catalog-info.yml
|
Comma-separated list of catalog file paths to check (first match wins) |
backstage_url |
Required | — | Base URL of the Backstage instance (e.g. https://backstage.example.com). When set, the collector cross-checks each declared spec.domain and spec.system against the Backstage catalog API and records the result under `.catalog.native.backstage.refs`. Empty (the default) disables referential-integrity lookups — the collector only parses and lints the local file, exactly as before. |
Backstage Guardrails Inputs
| Input | Required | Default | Description |
|---|---|---|---|
required_annotations |
Required | — | Annotation keys that must be present (and non-empty) in catalog-info.yaml `metadata.annotations`. Two forms are accepted. Comma-separated keys (presence-only, unchanged), e.g. `backstage.io/source-location,pagerduty.com/integration-key`. Or a YAML list, where each entry is either a bare key (presence-only) or a mapping with a `key` and optional value constraints: `type` (string|integer|number|boolean, default string), `min`/`max` (integer/number bounds), `min_length`/`max_length` (string length), `pattern` (full-match regex), and `enum` (allowed values). For example, a `key: example.com/service-tier` with `type: integer`, `min: 0`, `max: 5` requires that annotation to parse as an integer in 0–5. Annotation values are strings in Backstage, so `type` validates that the value *parses* as the declared type (`"2"` satisfies `integer`, `"2.5"` does not). `enum` inherits `type`: its entries are coerced to the declared type before comparison, so `type: integer` with `enum: [1,2,3]` compares integers while the default `type: string` compares strings. Constraints must match the declared type (`min`/`max` for integer/number; `min_length`/`max_length`/`pattern` for string). A value that violates a constraint fails the check; a malformed constraint (unknown type, min greater than max, invalid regex, a constraint on the wrong type, or an enum entry not of the declared type) makes it error. Empty (the default) disables the check. |
required_tag_patterns |
Required | — | Comma-separated list of glob patterns; the component's `metadata.tags` must include at least one tag matching each pattern. Example: `location/*,runs-on/*`. Matching is case-insensitive. Empty (the default) disables the `required-tag-patterns` check. |
disallowed_annotations |
Required | — | Comma-separated list of annotation keys that must NOT be present in catalog-info.yaml `metadata.annotations`. Example: `backstage.io/skip-checks`. Empty (the default) disables the `disallowed-annotations` check. |
disallowed_tag_patterns |
Required | — | Comma-separated list of glob patterns; the component's `metadata.tags` must NOT include any tag matching these patterns. Example: `deprecated/*,internal-only`. Matching is case-insensitive. Empty (the default) disables the `disallowed-tag-patterns` check. |
Ready to Automate Your Standards?
See how Lunar can turn your AGENTS.md, engineering wiki, compliance docs, or postmortem action items into automated guardrails with our 200+ built-in guardrails.