Hamburger Cross Icon
Backstage catalog-info.yaml Monorepo Cataloger - Lunar Cataloger

Backstage catalog-info.yaml Monorepo Cataloger

Cataloger Beta Service CatalogVcs

Scans each configured repository for every `catalog-info.yaml` — not just the root — and creates one Lunar component per file, keyed to the file's directory so a monorepo becomes one component per service. Each gets the owner, domain, and tags from its Backstage entity.

Add backstage-catalog-info-monorepo to your lunar-config.yml:
uses: github://earthly/lunar-lib/catalogers/backstage-catalog-info-monorepo@v1.0.5

What This Integration Syncs

This integration includes 1 cataloger that sync data from your systems.

Cataloger cron

discover

Walks each configured repository's file tree via the GitHub Git Trees API (one recursive call per repo), finds every catalog-info.yaml / catalog-info.yml, fetches each via the Contents API, parses it (multi-document supported), and CREATES one component per discovered file in the Catalog JSON. Each component is keyed to the file's location: a root file maps to <component_id_prefix><owner>/<repo>, and a file at <dir>/catalog-info.yaml maps to the monorepo subcomponent <component_id_prefix><owner>/<repo>/<dir>. The matching Component entity's owner, domain, and tags are written onto the created component (identical shaping to the backstage-catalog-info cataloger), plus a .domains stub for every referenced domain. Runs on a fixed cadence.

Schedule: 0 3 * * *
service catalog backstage catalog-info components discovery monorepo create ownership
Book a demo

How Catalogers Fit into Lunar

Lunar catalogers sync component metadata into your Lunar catalog from external systems or source code. They can run on a schedule or be triggered by code changes to keep your service registry up-to-date.

By automatically discovering components from GitHub organizations, service registries, or by detecting technology usage in source code, catalogers ensure your guardrails apply to all relevant services without manual configuration.

Learn How Lunar Works
1
Catalogers Sync Context This Integration
Sync component metadata from service catalogs, ownership systems, and external APIs
2
Guardrails Engine
Once cataloged, components are automatically analyzed by collectors and evaluated against your guardrails

Example Catalog Entry

This cataloger syncs component metadata into your Lunar catalog. Here's an example of a catalog entry it creates:

{ } catalog entry Catalog JSON
{
  "components": {
    "github.com/acme/monorepo/services/payments": {
      "owner": "group:default/team-payments",
      "domain": "platform.payments",
      "tags": ["bs-payments", "bs-type-service", "bs-lifecycle-production"]
    },
    "github.com/acme/monorepo/services/web": {
      "owner": "group:default/team-web",
      "domain": "platform.frontend",
      "tags": ["bs-frontend", "bs-type-website", "bs-lifecycle-production"]
    }
  },
  "domains": {
    "platform.payments": {},
    "platform.frontend": {}
  }
}

Configuration

Configure this cataloger in your lunar-config.yml.

Inputs

Input Required Default Description
repos Required Comma-separated list of `<owner>/<repo>` repositories to scan (e.g. `acme/monorepo,acme/platform`). Each is walked in full for `catalog-info.yaml` files. Required — leave empty and the cataloger has nothing to scan. Org-wide auto-discovery (scan every repo in an org, or every repo already cataloged by `github-org`) is a planned follow-up; see the README.
filenames Optional catalog-info.yaml,catalog-info.yml Comma-separated list of file basenames that count as a Backstage catalog descriptor. A file anywhere in the repo tree whose basename matches is treated as a catalog-info file. Defaults match the conventional Backstage names.
branch Required Git ref to read the tree and files from. Empty means each repo's default branch.
exclude_paths Optional catalog-info.yaml,catalog-info.yml Comma-separated list of file paths (relative to the repo root) to skip during discovery. An entry matches a discovered file by exact path or as a glob (e.g. `legacy/*/catalog-info.yaml`). Excluded files never become components. The default excludes a root `catalog-info.yaml` / `.yml`, because this cataloger is designed to run alongside the `backstage-catalog-info` cataloger, which already reads the root file onto the repo-level component — so out of the box this one stays out of the root and just adds the per-subdirectory subcomponents, no double-writes. Set it to an empty string to also process the root file (mapping it to the repo-level component id) when you run this cataloger on its own, or extend it to fence off additional paths. This is the platform-controlled exclusion: it lives in `lunar-config.yml`, so dev teams can't override it from their repos.
component_id_prefix Optional github.com/ String prepended to `<owner>/<repo>[/<dir>]` when constructing the created component's id. `github.com/` yields ids like `github.com/acme/monorepo/services/payments`.
domain_annotation Required Annotation key used to source the component's domain when `spec.domain` is absent. When set and the entity carries this annotation, its value wins over `spec.domain` / `spec.system`. Leave empty to use the canonical Backstage fields only. Same semantics as the `backstage-catalog-info` cataloger.
tag_prefix Optional bs- Prefix added to Backstage `metadata.tags` (and to derived `type-*` / `lifecycle-*` tags) when mapped to Lunar tags. Empty string disables the prefix. Same semantics as the `backstage-catalog-info` cataloger.
include_derived_tags Optional true When `true`, emits derived tags from `spec.type` (e.g. `bs-type-service`) and `spec.lifecycle` (e.g. `bs-lifecycle-production`) in addition to `metadata.tags`.
owner_format Optional as-is How to write `spec.owner` into the Lunar `owner` field. - `as-is` — pass the Backstage entity ref through verbatim (e.g. `group:default/team-payments`). - `bare-name` — strip the `<kind>:<namespace>/` prefix and write only the trailing name. Same semantics as the `backstage-catalog-info` cataloger.
default_owner Required Fallback owner applied (verbatim) to a created component whose matched entity has no `spec.owner`. Leave empty to skip.
default_domain Required Fallback domain applied (verbatim) to a created component that resolves to no domain. A stub `.domains["<default_domain>"]` entry is written alongside so hub domain validation accepts the reference. Leave empty to leave such components without a domain.
allow_ignore_annotation Optional false When `true`, a discovered `Component` whose `catalog-info.yaml` carries the ignore annotation (see `ignore_annotation`) set to a truthy value (`true` / `yes` / `1`) is skipped — no component is created for it. This delegates opt-out to the dev teams that own the `catalog-info.yaml` files. Leave `false` (the default) to ignore the annotation entirely, keeping exclusion under platform control via `exclude_paths`.
ignore_annotation Optional lunar.io/ignore Annotation key checked when `allow_ignore_annotation` is `true`. A `Component` with this annotation set to `true` / `yes` / `1` is skipped. Ignored unless `allow_ignore_annotation` is enabled.

Secrets

This cataloger requires the following secrets to be configured in Lunar:

Secret Description
GH_TOKEN GitHub token used to walk each repo's tree (Git Trees API) and fetch each `catalog-info.yaml` (Contents API). Needs `Contents: Read` on the target repos (`repo` scope on a classic PAT, or `contents: read` on a fine-grained PAT / GitHub App installation token). Reused across most lunar-lib GitHub plugins, so if `GH_TOKEN` is already set for `github-org` this cataloger picks it up automatically.

Documentation

View on GitHub

Backstage catalog-info.yaml Monorepo Cataloger

Discovers every catalog-info.yaml in a repository — including files in subdirectories — and creates one Lunar component per discovered file.

Overview

Where the backstage-catalog-info cataloger augments components that already exist (reading one catalog-info.yaml per repo), this cataloger creates components by discovering all the catalog-info.yaml files a repository contains and mapping each to its own Lunar component. A monorepo with services/payments/catalog-info.yaml and services/web/catalog-info.yaml becomes two components, each keyed to the file's directory and populated with the owner, domain, and tags from its Backstage Component entity.

It runs on a schedule, walking each configured repository's file tree via the GitHub API. Use it when a single repo holds several Backstage entities in different subdirectories and each should be its own component.

Synced Data

This cataloger writes to the following Catalog JSON paths, once per discovered catalog-info.yaml:

Path Type Description
.components["<id>"] object Created for each discovered file. <id> is <component_id_prefix><owner>/<repo> for a root file, or <component_id_prefix><owner>/<repo>/<dir> for a file at <dir>/catalog-info.yaml
.components["<id>"].owner string spec.owner of the matched Backstage Component (or default_owner fallback)
.components["<id>"].domain string metadata.annotations[<domain_annotation>] when configured and present; otherwise spec.domain, falling back to spec.system, then to default_domain
.components["<id>"].tags[] array metadata.tags plus derived type-* / lifecycle-* tags, all with tag_prefix
.domains["<domain>"] object Stub entry ({}) for each domain a created component references, so hub catalog validation accepts the reference. When the same catalog-info.yaml declares a matching kind: Domain / kind: System entity, its description and owner are propagated

The owner / domain / tag shaping is identical to the backstage-catalog-info cataloger — the same inputs (domain_annotation, tag_prefix, include_derived_tags, owner_format, default_owner, default_domain) apply. The difference is that this cataloger creates the component keyed to the discovered file's path, rather than augmenting an existing one.

Example Catalog JSON output (a monorepo with two services)
{
  "components": {
    "github.com/acme/monorepo/services/payments": {
      "owner": "group:default/team-payments",
      "domain": "platform.payments",
      "tags": ["bs-payments", "bs-type-service", "bs-lifecycle-production"]
    },
    "github.com/acme/monorepo/services/web": {
      "owner": "group:default/team-web",
      "domain": "platform.frontend",
      "tags": ["bs-frontend", "bs-type-website", "bs-lifecycle-production"]
    }
  },
  "domains": {
    "platform.payments": {
      "description": "Payments platform — billing, ledger, settlement",
      "owner": "group:default/team-payments"
    },
    "platform.frontend": {}
  }
}

How Files Are Discovered and Keyed

  1. Tree walk. For each repository in repos, the cataloger makes one recursive Git Trees API call (GET /repos/<owner>/<repo>/git/trees/<branch>?recursive=1) and filters the tree to blobs whose basename is in filenames (default catalog-info.yaml,catalog-info.yml). One API call finds every descriptor in the repo, regardless of nesting depth.

  2. Fetch + parse. Each matched file is fetched via the Contents API (raw) and parsed (multi-document files supported).

  3. Component identity. The created component's id is derived from the file's location:

    File location Component id (with default prefix)
    catalog-info.yaml (repo root) github.com/<owner>/<repo>
    services/payments/catalog-info.yaml github.com/<owner>/<repo>/services/payments

    The subdirectory form matches Lunar's convention for monorepo subcomponents (github.com/acme/monorepo/service-a), so the component's path is the discovered file's directory — exactly the shape a monorepo needs.

  4. Entity selection. One component is created per discovered file. If a file declares exactly one kind: Component, that entity is used.

Restricting Synced Kinds

This cataloger only creates components from kind: Component entities. Domain, System, API, Resource, User, Group, Location, etc. are ignored for component creation — a Domain / System matching a created component's domain is still read to enrich the .domains stub (description + owner).

Files With Multiple Components

The common monorepo layout is one catalog-info.yaml per service directory, each declaring a single Component — one file, one component. A file that declares zero Component entities (e.g. only a System or Domain) or more than one Component is skipped, with a log line. This cataloger creates exactly one component per file and keys it to the file's directory, so it won't guess which of several Components a multi-Component file maps to — give each Component its own directory's catalog-info.yaml to have it discovered. (A Domain / System alongside the single Component is still read to enrich the .domains stub.)

Catalogers

Cataloger Description
discover Scheduled. Walks each configured repo's tree via the Git Trees API, finds every catalog-info.yaml, fetches and parses each, and creates one component per file keyed to the file's directory. Writes owner / domain / tags plus .domains stubs. Runs on a cron schedule. Requires GH_TOKEN.

Hook Type

Cataloger Hook Schedule / Trigger Description
discover cron 0 3 * * * Runs daily at 03:00 UTC, once per run (global), scanning every repo in repos

The cron hook is global (once per run, no repo checkout), which is what lets this cataloger create components — per-component hooks (component-cron, component-repo) can only augment components that already exist. Daily at 03:00 is offset from the standard 0 2 * * * so it lands after component-defining catalogers run. Tighten the cadence by overriding hook.schedule in a fork.

Installation

Add to your lunar-config.yml and list the repositories to scan:

catalogers:
  - uses: github.com/earthly/lunar-lib/catalogers/backstage-catalog-info-monorepo@v1.0.0
    with:
      repos: "acme/monorepo,acme/platform"

Set the GitHub token used to walk each repo's tree and fetch each catalog-info.yaml:

lunar secret set GH_TOKEN <your-github-token>

The token needs Contents: Read on every repo in repos (repo scope on a classic PAT; contents: read on a fine-grained PAT or GitHub App installation token). Many lunar-lib plugins reuse the same GH_TOKEN, so if you've already set it for github-org or the GitHub-API collectors, this cataloger picks it up automatically. All other inputs (filenames, branch, exclude_paths, component_id_prefix, domain_annotation, tag_prefix, include_derived_tags, owner_format, default_owner, default_domain, allow_ignore_annotation, ignore_annotation) are documented in lunar-cataloger.yml.

Layering With Component-Defining Catalogers

This cataloger and github-org both create components, at different granularities:

  • github-org creates one repo-level component per repository (github.com/acme/monorepo).
  • backstage-catalog-info-monorepo creates one component per catalog-info.yaml — for a monorepo, that's a subcomponent per service directory (github.com/acme/monorepo/services/payments).

Running both is the intended monorepo setup: github-org (and/or the augment backstage-catalog-info) owns the repo-level component, and this cataloger adds one subcomponent per catalog-info file. By default exclude_paths (catalog-info.yaml,catalog-info.yml) excludes a root catalog-info.yaml, so this cataloger creates subcomponents only and never competes for the repo-level id. Clear exclude_paths to also map a root file to github.com/<owner>/<repo> when you run this cataloger on its own.

Monorepo vs the augment cataloger

backstage-catalog-info-monorepo and backstage-catalog-info are complementary, not alternatives:

backstage-catalog-info-monorepo (this) backstage-catalog-info
Action Creates components Augments existing components
Files per repo Discovers all catalog-info.yaml files Reads one (first configured path)
Granularity One component per file (monorepo subcomponents) One existing component per repo
Hook cron (global — can create) component-cron / component-repo (per-component — augment only)

If you only need to enrich repo-level components that another cataloger already created, use backstage-catalog-info. If a repo holds several catalog-info files that should each become their own component, use this cataloger.

Running both on the same repo

Enabling both together is the expected setup, and by default they divide cleanly with no shared writes:

  • Subdirectory files (services/*/catalog-info.yaml, …) are exclusive to this cataloger. The augment cataloger only reads the first configured path (root-relative) and never descends, so it never sees them.
  • A root catalog-info.yaml is left to the augment cataloger. Because exclude_paths defaults to catalog-info.yaml,catalog-info.yml, this cataloger skips the root file and only creates subcomponents — so the two never write the same component id.

If you instead run this cataloger on its own and want the root file to produce the repo-level component too, clear exclude_paths. A root file then maps to github.com/<owner>/<repo> — the same id the augment cataloger and github-org use. That's still an idempotent re-assert when they share the same transform inputs, but keeping the default exclude_paths alongside backstage-catalog-info avoids any chance of the two writing divergent values (e.g. different tag_prefix) to the repo-level component.

Excluding files and components

Two mechanisms decide what does not become a component, at different levels of control:

  • exclude_paths (platform-controlled) — a list of repo-relative paths/globs skipped before a component is ever created. It lives in lunar-config.yml, so dev teams can't override it. The default excludes the root catalog-info.yaml; add globs like legacy/*/catalog-info.yaml to fence off more.
  • lunar.io/ignore annotation (dev-delegated, gated) — set allow_ignore_annotation: true and any Component whose catalog-info.yaml carries lunar.io/ignore: "true" (key configurable via ignore_annotation) opts itself out. Left off by default, so opt-out stays platform-controlled unless you choose to delegate it.

Targeting monorepos in a mixed fleet (monorepo + polyrepo)

An org often has both polyrepos (one service per repo, root catalog-info.yaml → the repo-level component) and monorepos (many services in subdirectories, and no root component wanted). The two strategies coexist without conflict: target the monorepos explicitly and let the normal repo-level flow handle everything else.

catalogers:
  # Repo-level components for the whole org, EXCEPT the monorepos.
  - uses: github.com/earthly/lunar-lib/catalogers/github-org@v1.0.0
    with:
      org_name: "acme"
      exclude_repos: "big-monorepo"       # no repo-level component for the monorepo

  # Polyrepos: augment each root catalog-info onto its repo-level component.
  - uses: github.com/earthly/lunar-lib/catalogers/backstage-catalog-info@v1.0.0

  # Monorepos: one component per subdirectory catalog-info, root excluded by default.
  - uses: github.com/earthly/lunar-lib/catalogers/backstage-catalog-info-monorepo@v1.0.0
    with:
      repos: "acme/big-monorepo"

Each polyrepo gets a repo-level component from its root catalog-info.yaml; the monorepo gets one component per subdirectory and no root component. The flows don't overlap: github-org excludes the monorepo, and this cataloger is scoped to it by an explicit repos list rather than running across every repo. (The augment cataloger harmlessly no-ops on the monorepo's subcomponents — it looks for a root file at a path that isn't a real repo and skips on the 404.) This is the "target the monorepo directly" posture; because targeting is just the repos list plus github-org's exclude_repos, a fleet can mix both strategies without a one-size-fits-all cataloger.

Source System

GitHub — the cataloger calls the Git Trees API once per configured repo to enumerate files, then the Contents API once per discovered catalog-info.yaml. Requirements:

  • GH_TOKEN secret with read access to every repo in repos.
  • GitHub-hosted repos. Component ids are constructed as <component_id_prefix><owner>/<repo>[/<dir>]; the default prefix is github.com/.

Scope and Roadmap

v1 targets the single-repo / explicit-repo-list case that motivated it (a monorepo whose services each ship a catalog-info.yaml). Two extensions are noted as follow-ups rather than built here:

  • Org-wide discovery — scan every repo in an org (or every repo already cataloged by github-org) instead of an explicit repos list.
  • Commit-triggered variant — a checkout-based companion (mirroring backstage-catalog-info's augment-on-commit) for near-real-time updates without the daily cron.

Open Source

This cataloger 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