Hamburger Cross Icon
Backstage Guardrails - Lunar Policy for Repository And Ownership

Backstage Guardrails

Policy Beta Repository And Ownership

Validates Backstage catalog-info.yaml entries for completeness and compliance. Checks that the catalog file exists and is valid, owner is set, lifecycle stage is defined, and system grouping is assigned.

Add backstage to your lunar-config.yml:
uses: github://earthly/lunar-lib/policies/backstage@v1.0.5

Included Guardrails

This policy includes 9 guardrails that enforce standards for your repository and ownership.

Guardrail

catalog-info-exists

Verifies that a catalog-info.yaml file exists in the repository. Every Backstage-managed service must have a catalog definition file. Fails if the backstage collector reports no catalog file found.

backstage catalog-info service catalog file exists
View Guardrail
Guardrail

catalog-info-valid

Verifies that the catalog-info.yaml file is syntactically valid and passes Backstage descriptor schema checks (no lint errors reported by the collector). Fails if the file is missing or has lint errors.

backstage catalog-info lint schema validation
View Guardrail
Guardrail

owner-set

Validates that the owner field (spec.owner) is populated in the catalog-info.yaml. Ownership is required for incident routing and accountability. Fails if no catalog file is present.

backstage ownership owner accountability
View Guardrail
Guardrail

lifecycle-set

Validates that the lifecycle stage (spec.lifecycle) is defined in the catalog-info.yaml. Lifecycle stages (production, experimental, deprecated) inform operational expectations and SLO requirements. Fails if no catalog file is present.

backstage lifecycle production deprecated
View Guardrail
Guardrail

system-set

Validates that the system grouping (spec.system) is defined in the catalog-info.yaml. System assignment enables dependency mapping and architectural visibility. Fails if no catalog file is present.

backstage system grouping architecture
View Guardrail
Guardrail

required-annotations

Verifies that catalog-info.yaml declares a configurable set of required annotation keys (e.g. backstage.io/source-location), and optionally that their values satisfy typed constraints — a declared data type (integer, number, boolean, string) with numeric bounds, string length, a regular-expression pattern, or an allowed-value set. Configure via the required_annotations input. Skipped when no annotations are required (the default). Fails if the catalog file is missing, a required key is absent or empty, or a value violates its declared constraint.

backstage annotations metadata required type validation value constraints
View Guardrail
Guardrail

required-tag-patterns

Verifies that catalog-info.yaml carries at least one tag matching each configured glob pattern (e.g. location/*, runs-on/*). Configure via the required_tag_patterns input. Skipped when no patterns are required (the default). Fails if the catalog file is missing or any pattern is unmatched by the component's tags.

backstage tags tag patterns glob required metadata
View Guardrail
Guardrail

disallowed-annotations

Verifies that catalog-info.yaml declares none of a configurable set of forbidden annotation keys. Configure via the disallowed_annotations input. Skipped when no annotations are forbidden (the default). Fails if any forbidden annotation key is present in metadata.annotations.

backstage annotations metadata disallowed forbidden
View Guardrail
Guardrail

disallowed-tag-patterns

Verifies that none of the component's tags match a configured glob pattern. Configure via the disallowed_tag_patterns input. Skipped when no patterns are forbidden (the default). Fails if any tag matches any forbidden pattern.

backstage tags tag patterns glob disallowed forbidden
View Guardrail

How Guardrails Fit into Lunar

Lunar guardrails define your engineering standards as code. They evaluate data collected by integrations and produce pass/fail checks with actionable feedback.

Policies support gradual enforcement—from silent scoring to blocking PRs or deployments—letting you roll out standards at your own pace without disrupting existing workflows.

Learn How Lunar Works
1
Integrations Gather Data
Collectors extract metadata from code, CI pipelines, tool outputs, and scans
2
{ } Centralized as JSON
All data merged into each component's unified metadata document
3
Guardrails Enforce Standards This Policy
Real-time feedback in PRs and AI workflows

Required Integrations

This policy evaluates data gathered by one or more of the following integration(s). Make sure to enable them in your lunar-config.yml.

Configuration

Configure this policy in your lunar-config.yml.

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.

Documentation

View on GitHub

Backstage Guardrails

Enforce Backstage service catalog standards for catalog-info.yaml completeness.

Overview

Validates that Backstage catalog entries include required metadata for service ownership, lifecycle management, and system architecture. These checks apply to repositories that use Backstage as their service catalog and must be paired with the backstage collector.

The five core checks fail when no catalog-info.yaml is present — a repository enabled for this policy is expected to be registered in Backstage. The four configurable checks (required-* / disallowed-*) are opt-in and skipped until configured; see Examples for their no-file behavior.

Policies

This plugin provides the following policies (use include to select a subset):

Policy Description
catalog-info-exists Verifies catalog-info.yaml exists in the repository
catalog-info-valid Verifies catalog-info.yaml passes lint/schema checks
owner-set Validates that spec.owner is populated
lifecycle-set Validates that spec.lifecycle is defined
system-set Validates that spec.system is defined
required-annotations Validates that configured annotation keys are present, and optionally that their values match typed constraints (opt-in via the required_annotations input)
required-tag-patterns Validates that the component's tags match configured glob patterns (opt-in via the required_tag_patterns input)
disallowed-annotations Fails if any forbidden annotation key is present (opt-in via the disallowed_annotations input)
disallowed-tag-patterns Fails if any tag matches a forbidden glob pattern (opt-in via the disallowed_tag_patterns input)

Required Data

This policy reads from the following Component JSON paths. The presence of .catalog.native.backstage indicates that a catalog-info file was found; its absence means no file exists.

Path Type Provided By
.catalog.native.backstage object backstage collector (namespace present ⇔ file found)
.catalog.native.backstage.valid boolean backstage collector
.catalog.native.backstage.errors[] array backstage collector
.catalog.native.backstage.spec.owner string backstage collector
.catalog.native.backstage.spec.lifecycle string backstage collector
.catalog.native.backstage.spec.system string backstage collector
.catalog.native.backstage.metadata.annotations object backstage collector (read by required-annotations / disallowed-annotations)
.catalog.native.backstage.metadata.tags array backstage collector (read by required-tag-patterns / disallowed-tag-patterns)

Note: Ensure the backstage collector is configured before enabling this policy.

Installation

Add to your lunar-config.yml:

policies:
  - uses: github://earthly/lunar-lib/policies/backstage@v1.0.0
    on: ["domain:your-domain"]
    enforcement: report-pr
    # include: [catalog-info-exists, owner-set]  # Only run specific checks
    # Opt in to the configurable checks by setting their inputs:
    with:
      required_annotations: "backstage.io/source-location"
      required_tag_patterns: "location/*,runs-on/*"
      disallowed_annotations: "backstage.io/skip-checks"
      disallowed_tag_patterns: "deprecated/*"

All four inputs are comma-separated lists; leave them unset (the default) and the corresponding check is skipped. required_annotations additionally accepts a YAML list for validating annotation values against typed constraints — see Typed value constraints below. Tag patterns are glob-style (location/* matches location/us-east-1), matched case-insensitively. required-tag-patterns needs each pattern matched by at least one tag; disallowed-tag-patterns fails if any tag matches any pattern. required-annotations needs each key present and non-empty; disallowed-annotations fails if any forbidden key is present at all.

Examples

Passing Example

{
  "catalog": {
    "native": {
      "backstage": {
        "valid": true,
        "errors": [],
        "path": "catalog-info.yaml",
        "apiVersion": "backstage.io/v1alpha1",
        "kind": "Component",
        "metadata": { "name": "payment-api" },
        "spec": {
          "type": "service",
          "owner": "team-payments",
          "lifecycle": "production",
          "system": "payment-platform"
        }
      }
    }
  }
}

Failing Example (spec fields missing)

{
  "catalog": {
    "native": {
      "backstage": {
        "valid": true,
        "errors": [],
        "path": "catalog-info.yaml",
        "apiVersion": "backstage.io/v1alpha1",
        "kind": "Component",
        "metadata": { "name": "payment-api" },
        "spec": {
          "type": "service"
        }
      }
    }
  }
}

Failing Example (no catalog-info.yaml)

{}

The .catalog.native.backstage namespace is simply absent. The five core checks fail. The required-* checks fail too if configured; the disallowed-* checks pass (nothing forbidden can be present without a file). All four are skipped if unconfigured.

Failure messages:

  • "No catalog-info.yaml found"
  • "catalog-info.yaml has lint errors: <details>"
  • "Owner (spec.owner) is not set in catalog-info.yaml"
  • "Lifecycle stage (spec.lifecycle) is not set in catalog-info.yaml"
  • "System (spec.system) is not set in catalog-info.yaml"

Configurable checks: required and disallowed annotations / tag patterns

With required_annotations: "backstage.io/source-location", required_tag_patterns: "location/*,runs-on/*", disallowed_annotations: "backstage.io/skip-checks", and disallowed_tag_patterns: "deprecated/*" configured, this component passes all four configurable checks:

{
  "catalog": {
    "native": {
      "backstage": {
        "metadata": {
          "annotations": { "backstage.io/source-location": "url:https://github.com/acme/payment-api" },
          "tags": ["location/us-east-1", "runs-on/self-hosted", "tier1"]
        }
      }
    }
  }
}

Remove the backstage.io/source-location annotation and required-annotations fails: "catalog-info.yaml is missing required annotation(s): backstage.io/source-location". Drop every runs-on/* tag and required-tag-patterns fails: "catalog-info.yaml has no tag matching required pattern(s): runs-on/*". Conversely, add a backstage.io/skip-checks annotation and disallowed-annotations fails; add a deprecated/legacy tag and disallowed-tag-patterns fails: "catalog-info.yaml has tag(s) matching disallowed pattern(s): deprecated/* (deprecated/legacy)".

Typed value constraints on required annotations

required-annotations can also assert that an annotation's value meets a constraint, not just that the key is present. Pass required_annotations as a YAML list instead of a comma-separated string; each entry is either a bare key (presence-only, as before) or a mapping with a key and one or more constraints:

with:
  required_annotations: |
    - key: example.com/service-tier      # integer in 0–5
      type: integer
      min: 0
      max: 5
    - key: example.com/contact-email     # must look like an email address
      type: string
      pattern: '^[^@]+@[^@]+\.[^@]+$'
    - key: example.com/environment       # one of a fixed set
      enum: [production, staging, development]
    - backstage.io/source-location       # bare key = presence-only

Supported constraints:

Constraint Applies to Meaning
type all string (default), integer, number, or boolean. The value is coerced to this type before the other constraints run.
min / max integer, number Inclusive numeric bounds.
min_length / max_length string Inclusive length bounds.
pattern string Full-match regular expression. Quote it with single quotes ('...') so backslashes pass through literally.
enum all The value must be one of the listed values (compared after coercion — see below).

Backstage annotation values are strings, so type validates that the value parses as the declared type: "2" satisfies type: integer, "2.5" does not.

enum inherits type. Enum entries are coerced to the declared type before comparison, so the value and the allowed set are always compared in the same domain. This matters because YAML types the entries on parse: enum: [1, 2, 3] yields integers, but with the default type: string the annotation value is a string, so the entries are coerced to "1", "2", "3" and "2" matches. Use type: integer to compare as integers instead. An enum entry that can't be coerced to the declared type (e.g. type: integer with enum: [1, two, 3]) is a misconfiguration.

Constraints must match the declared type. min/max apply to integer/number; min_length/max_length/pattern apply to string; enum and type apply to any type. Pairing a constraint with the wrong type (e.g. pattern on an integer, or min on a string) is a misconfiguration, not a silent no-op.

A value that violates its constraint fails the check with a specific message (for example, annotation "example.com/service-tier": value "7" is above maximum 5). A malformed constraint spec — an unknown type, min greater than max, an invalid regex, a constraint on the wrong type, or an enum entry not of the declared type — makes the check error rather than fail, so the misconfiguration surfaces immediately instead of silently passing.

The comma-separated form (required_annotations: "key1,key2") still works and remains presence-only; it is equivalent to a YAML list of bare keys.

Remediation

When this policy fails, resolve it by updating your catalog-info.yaml:

  1. Missing file - Create a catalog-info.yaml in the repository root following the Backstage descriptor format
  2. Lint errors - Review .catalog.native.backstage.errors[] in the component payload and fix the reported issues
  3. Missing owner - Add spec.owner with a valid team or user reference (e.g., team-payments)
  4. Missing lifecycle - Add spec.lifecycle with a stage: production, experimental, or deprecated
  5. Missing system - Add spec.system referencing the parent system that groups related components

Open Source

This policy is open source and available on GitHub. Contribute improvements, report issues, or fork it for your own use.

View Repository

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.

Works with any process
check AI agent rules & prompt files
check Post-mortem action items
check Security & compliance policies
check Testing & quality requirements
Automate Now
Paste your AGENTS.md or manual process doc and get guardrails in minutes
Book a Demo