# Introduction

Earthly Lunar is a guardrails platform for monitoring and enforcing engineering practices across your software development lifecycle.

Lunar works by instrumenting your existing CI/CD pipelines (no YAML changes needed) and source code repositories to collect structured metadata about how code is built, tested, scanned, and deployed. This metadata is then continuously evaluated against policies that you define—policies that are flexible, testable, and expressive enough to reflect your real-world engineering standards.

Want to block deployments that would violate compliance rules, like using unapproved licenses or bypassing required security scans? Or fail a PR if it introduces stale dependencies or vulnerable CI plugins? Or ensure that security-sensitive services are collecting SBOMs, running code scans, and deploying frequently enough to avoid operational drift? Lunar makes all of that possible—without requiring a wholesale rewrite of every team's CI pipeline, and without sacrificing developer velocity.

Lunar is designed to work with the messy reality of modern engineering. It's not a one-size-fits-all template. Its instrumentation is flexible and centralized—meaning platform teams stay in control, app teams stay autonomous, and standards actually get enforced.

## Overview

Earthly Lunar helps organizations maintain high engineering standards by:

* Monitoring components (services, libraries, repositories) across your organization
* Collecting metadata about your software development lifecycle
* Enforcing policies and best practices
* Providing visibility into engineering health through checks

## Key Features

* **Component Monitoring**: Track the health and status of individual software components
* **SDLC Instrumentation**: Collect data from various stages of your software development lifecycle
* **Policy Enforcement**: Define and enforce engineering standards across your organization
* [**200+ Pre-Built Guardrails**](https://earthly.dev/lunar/guardrails/): Ready-to-use policies for testing, security, compliance, and operational readiness
* [**50+ Integrations**](https://earthly.dev/lunar/integrations/): Connect to GitHub, Kubernetes, Docker, Codecov, Snyk, and more
* **Extensible Platform**: Create custom collectors and policies using Bash or Python SDKs
* **SQL API**: Query and analyze your engineering data

## AI Skills

To get started quickly with developing guardrails, see the [AI skills](/install/skills) compatible with Claude Code, Codex, and Cursor.

## Getting Started

1. [Install Lunar](/install)
2. [Learn the basics](/basics)
3. [Understand key concepts](/docs/key-concepts)


# Install Lunar

Overview of Lunar installation, covering Hub, CLI, CI Agent for self-hosted and managed runners, and AI skills for building plugins.

Welcome to the Lunar installation guide.

This section contains step-by-step instructions for installing Lunar. Install these pieces, in this order:

1. **Lunar CLI** – an admin and development CLI. Install this first: the Hub install walkthrough uses `lunar licence` to derive your GHCR image-pull secret from your licence JWT before `helm install`.
   * [Install the Lunar CLI](/install/cli)
2. **Lunar Hub** – the central coordination service.
   * [Overview](/install/lunar-hub/hub-overview) – the lay of the land: services, dependencies, ports.
   * [Prerequisites](/install/lunar-hub/hub-prereqs) – what to have in place before `helm install`.
   * [Install walkthrough](/install/lunar-hub/hub-install) – step-by-step from zero to a working Hub.
   * [Day-2 operations](/install/lunar-hub/hub-day2) – upgrades, secret rotation, observability, uninstall.
3. **Lunar CI Agent** — instruments your CI runners to report data to the Hub. Choose based on how your CI is set up:
   * [Self-hosted runners](/install/lunar-ci-agent/agent-self-hosted)
   * [GitHub-hosted managed runners](/install/lunar-ci-agent/agent-managed) (the [earthly/lunar-ci-tracer](https://github.com/earthly/lunar-ci-tracer) action).

Optional:

* [`sync-config` GitHub Action](/install/github-actions) – pushes your config repo to Lunar Hub on every push.
* [AI Skills](/install/skills) – agent skills for building collectors and policies.

Want to try Lunar without installing anything? Get in touch for a guided demo or preview.

<a href="https://earthly.dev/earthly-lunar/demo" class="button primary" data-icon="calendar">Request a demo</a>

Before diving in, browse the [100+ pre-built guardrails](https://earthly.dev/lunar/guardrails/) and [30+ integrations](https://earthly.dev/lunar/integrations/) available out of the box.


# Lunar CLI

Install the Lunar CLI for managing configurations, inspecting components, running collectors, and testing policies locally.

The Lunar CLI is primarily an administration tool for platform engineers, with developer-focused capabilities secondarily. It uses the same `lunar` binary as the CI agent but provides different subcommands for interactive CLI usage versus CI instrumentation. It can be used to manage configurations, inspect components, run collectors, and test policies.

## Prerequisites

**Docker** is required for local development commands since collectors and policies run inside containers.

{% hint style="info" %}
**macOS Users:** If using Docker Desktop, you must enable the default Docker socket. Go to **Docker Desktop → Settings → Advanced** and enable **"Allow the default Docker socket to be used"**, then click **Apply & Restart**.
{% endhint %}

## Installation

{% stepper %}
{% step %}

## Download the binary

<a href="https://github.com/earthly/lunar-dist/releases" class="button primary" data-icon="download">Download the Lunar CLI</a>
{% endstep %}

{% step %}

## Move the binary into place

Move the binary to the `~/.lunar/bin` directory:

```bash
chmod +x lunar-linux-amd64 && mv ./lunar-linux-amd64 "$HOME/.lunar/bin/lunar"
```

If you prefer to install `lunar` to a different directory, you must set the `LUNAR_BIN_DIR` environment variable to the desired path (see below).
{% endstep %}

{% step %}

## Set environment variables

```bash
# Required
export LUNAR_HUB_TOKEN=your_hub_token
export LUNAR_HUB_HOST=your_hub_host
export LUNAR_HUB_GRPC_PORT=your_grpc_port
export LUNAR_HUB_HTTP_PORT=your_http_port
export PATH="$HOME/.lunar/bin:$PATH"

# Optional, if you want to override the default bin dir
export LUNAR_BIN_DIR="$HOME/.lunar/bin"
```

{% endstep %}

{% step %}

## Verify installation

```bash
lunar --help
```

{% endstep %}
{% endstepper %}

For usage examples and full CLI documentation, see the [Lunar CLI Docs](/docs/lunar-cli).

***

## Next Steps

Once installed, you can begin configuring:

* [Collectors](/configuration/lunar-config/collectors) to gather SDLC data
* [Policies](/configuration/lunar-config/policies) to enforce standards
* [Domains and Components](/docs/key-concepts) to organize your software landscape

For questions or enterprise onboarding:

<a href="https://earthly.dev/earthly-lunar/demo" class="button secondary" data-icon="envelope">Contact the Earthly team</a>


# Lunar Hub

Install and configure Lunar Hub, the central service that stores metadata, evaluates policies, and provides visibility into engineering health.


# Overview

This page is a high-level overview of what a Lunar Hub deployment looks like on Kubernetes. It's meant to provide a conceptual overview of the system before you dive into the installation process. When you're ready to install, start by setting up the [prerequisites](/install/lunar-hub/hub-prereqs).

## What you're installing

A Lunar deployment primarily consists of:

* the Lunar Hub (a Kubernetes service)
* the Lunar Operator (a Kubernetes controller)
* a fleet of transient policy/cataloger/collector batches managed by the operator
* and three external dependencies you provide (PostgreSQL, S3, and a GitHub App)

### External boundaries

```mermaid
flowchart LR
  CLI["Lunar CLI"]
  Agent["Lunar CI Agent"]

  Hub["Lunar Hub"]

  PG[("PostgreSQL")]
  S3[("S3 buckets<br/>logs · resources")]
  GH["GitHub"]

  CLI -- gRPC --> Hub
  Agent -- gRPC --> Hub
  GH -- webhooks --> Hub

  Hub -- SQL --> PG
  Hub -- reads/writes --> S3
  Hub -- API --> GH
```

#### Hub

This is the central API server. It:

* talks to Postgres, GitHub, and S3
* serves the gRPC API consumed by the CLI, CI agents, and the Lunar operator
* receives GitHub webhooks on its HTTP port
* issues pre-signed S3 URLs for bulk data (run resources, run logs) transferred between work units orchestrated by the operator

For more details on how this hooks into the rest of the system, see [Ports and protocols](#ports-and-protocols) below.

#### Lunar CLI

The `lunar` binary. Used by platform engineers to push configuration, inspect components, and run collectors/policies locally. [This is installed separately.](/install/cli).

#### Lunar CI Agent

Instruments your CI runners to report data to the Hub. This is also installed separately. We support [Self-hosted GitHub Actions](/install/lunar-ci-agent/agent-self-hosted) and [Managed GitHub Actions](/install/lunar-ci-agent/agent-managed) options.

### Inside the cluster

```mermaid
flowchart LR
  subgraph K8s["Kubernetes cluster"]
    subgraph CP["Control-plane namespace"]
      Hub["Lunar Hub"]
      Op["Lunar Operator"]
    end
    subgraph RP["Run-pods namespace"]
      SP["Run Pods<br/>(init + sidecar + user code)"]
    end
    Op -- creates --> SP
  end

  PG[("PostgreSQL")]

  Hub -- SQL --> PG
  Op -- polls queue --> PG
  SP -- gRPC --> Hub
```

#### Lunar Operator

A Kubernetes controller that groups enqueued runs into batches and materializes each batch as a short-lived Kubernetes pod. It manages the whole lifecycle of these pods, from creation to cleanup.

#### Run pods

Short-lived batch pods. The init container fetches needed data from S3 and GitHub and coordinates the user containers. User containers execute the specified cataloger, collector, or policy. The sidecar streams each container's logs to S3 and reports exit codes back to the Hub over gRPC.

Splitting the control plane (Hub + operator) and run pods into separate namespaces is recommended. This provides different blast radius, different resource profile, and different RBAC for distinctly different pieces of the system. Single-namespace installs also work; see [Step 1](/install/lunar-hub/hub-prereqs#step-1--plan-your-kubernetes-namespaces) of the prerequisites for details.

### Grafana

Grafana is the primary UI for Lunar today — dashboards for policy results, component health, and collection activity, reading from the same Postgres database as the Hub. Lunar ships those dashboards (plus the datasources and panel plugins they need) and **installs them into a target Grafana over its HTTP API**. You pick which Grafana based on what you already run:

| Your situation                                         | How Lunar reaches Grafana                                                                                                                                                          |
| ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **No Grafana of your own** — you want Lunar to run one | The chart deploys a stock Grafana alongside the Hub and installs the dashboards into it for you. Enabled by default; admin credentials are auto-generated.                         |
| **Grafana Cloud or Grafana Enterprise**                | Point Lunar at your Grafana and give it a **service-account token**.                                                                                                               |
| **Self-hosted OSS Grafana**                            | Point Lunar at your Grafana and give it **admin username + password** (OSS Grafana needs a server admin to install the dashboards' panel plugins — a service-account token can't). |

In every case the chart provisions the dashboards for you on `helm install` / `helm upgrade` via a post-install/post-upgrade hook Job (`grafana.provisioning`).

[Install Step 4](/install/lunar-hub/hub-install#grafana) walks through all three. To turn Grafana off entirely, set `grafana.mode: off`; full configuration is in the [chart README](https://github.com/earthly/charts/blob/main/README.md#grafana).

## Networking

### Connectivity

Use this table when planning ingress rules, egress allowlists, and NetworkPolicies. Specific Hub ports are detailed in the [next section](#ports-and-protocols).

| Source          | Destination      | Direction  | Purpose                                   |
| --------------- | ---------------- | ---------- | ----------------------------------------- |
| GitHub          | Hub HTTP ingress | Inbound    | Webhook delivery                          |
| CLI / CI agents | Hub gRPC ingress | Inbound    | API calls (config sync, results)          |
| CLI / CI agents | Hub HTTP ingress | Inbound    | Pre-signed log URL fetches                |
| Hub             | GitHub API       | Outbound   | Read repos, post checks/comments          |
| Hub             | Postgres         | Outbound   | Hub state (incl. work queue, migrations)  |
| Hub             | S3               | Outbound   | Run pod resource uploads                  |
| Operator        | Postgres         | Outbound   | Poll work queue (own schema)              |
| Run pods        | S3               | Outbound   | Upload logs, download resources           |
| Run pods        | Hub Service      | In-cluster | Exit codes (gRPC); log URL fetches (HTTP) |

### Ports and protocols

The Hub listens on three ports inside the pod. Only two are exposed externally; the third is for in-cluster health probes.

| Port   | Protocol | Purpose                                                          | Who talks to it                    |
| ------ | -------- | ---------------------------------------------------------------- | ---------------------------------- |
| `8000` | gRPC     | API — config sync, policy evaluation, run results                | CLI, CI agents, run pods (sidecar) |
| `8001` | HTTP     | GitHub webhook receiver + pre-signed URL redirector for run logs | GitHub, CLI, CI agents, sidecar    |
| `8002` | HTTP     | Liveness / readiness probes (`GET /health`)                      | Kubelet only                       |

The HTTP port serves exactly two path prefixes: `/webhooks/github` (webhook ingestion) and `/logs/runs/` (redirects to pre-signed S3 URLs for log upload/download). No other HTTP routes exist. The Hub does not currently expose Prometheus metrics; observability is via OpenTelemetry (OTLP).

## Next steps

* [Prerequisites](/install/lunar-hub/hub-prereqs) — external dependencies you need in place before `helm install`.
* [Install walkthrough](/install/lunar-hub/hub-install) — step-by-step from zero to a working Hub.


# Prerequisites

Before you run `helm install lunar`, the external dependencies below must be in place. The [install walkthrough](/install/lunar-hub/hub-install) assumes you have them.

If you want a total picture of what you're about to deploy, read the [overview](/install/lunar-hub/hub-overview) first.

{% hint style="info" %}
Lunar Hub is supported on Kubernetes only. Bare-metal and Docker installations are not supported.
{% endhint %}

{% hint style="info" %}
**Install the** [**Lunar CLI**](/install/cli) **first.** The walkthrough uses it on your workstation to derive the cluster's GHCR image-pull secret from your licence — before `helm install`.
{% endhint %}

## Before you begin

Make sure you have the following available before continuing.

| Step                                                 | What you need             | Detail                                                                                                                                                             |
| ---------------------------------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| —                                                    | Workstation tools         | [`lunar`](/install/cli), `kubectl`, `helm` 3.x                                                                                                                     |
| [2](#step-2--check-your-kubernetes-cluster)          | Kubernetes cluster        | 1.29+, with an ingress controller and a default StorageClass                                                                                                       |
| [2](#step-2--check-your-kubernetes-cluster)          | DNS hostnames + TLS certs | One hostname for the Hub (reachable from GitHub, or your GHES instance), plus a second for Grafana if you let the chart run it for you (reachable from your users) |
| [3](#step-3--provision-postgresql)                   | PostgreSQL instance       | 16+, where you can create a dedicated owner role                                                                                                                   |
| [4](#step-4--provision-s3-compatible-object-storage) | S3-compatible buckets     | Two private buckets (logs + resources) with IAM to read and write them                                                                                             |
| [5](#step-5--create-a-github-app)                    | GitHub org admin access   | Or a personal account, where you can create a GitHub App for your org                                                                                              |
| [6](#step-6--plan-your-kubernetes-secrets)           | Hub license key           | Provided by Earthly; mounted by the chart as `lunar-hub-licence`                                                                                                   |

{% hint style="info" %}
If you're standing up EKS from scratch, [`earthly/lunar-terraform-quickstart`](https://github.com/earthly/lunar-terraform-quickstart) is a working reference module you can fork if desired.
{% endhint %}

## Step 1 — Plan your Kubernetes namespaces

We recommend splitting the install across two namespaces:

* **Control-plane namespace** (e.g. `lunar`). This is the release namespace. It hosts the Lunar Hub, the Operator, and — if you use the chart's bundled Grafana — Grafana. These are the parts that need to stay up, and will be updated by Helm.
* **Run Pods namespace** (e.g. `lunar-scripts`). This hosts the short-lived pods the operator spawns to execute cataloger, collector, and policy batches. We recommend this separate namespace because:
  * this workload is ephemeral, and can be rather "bursty" in number and resource requirements. You can tune resources and limits independently here.
  * this code is user-supplied (e.g. your plugins, scripts, third-party catalogers), not Lunar's. This is a different trust boundary, where you can tighten RBAC, egress, and resource limits independently.

Both namespaces must exist **before** `helm install`. The chart will not create the run-pods namespace for you. Point the operator at it with `operator.snippetNamespace`.

Single-namespace installs also work — leave `operator.snippetNamespace` unset and everything runs in the release namespace. This is fine for trying things out, or small setups; but is *not recommended* for production configurations.

```bash
kubectl create namespace lunar
kubectl create namespace lunar-scripts
```

## Step 2 — Check your Kubernetes cluster

| Requirement            | Detail                                                                                                                                                                                                                                                                                                                                                         |
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Kubernetes**         | 1.29 or newer.                                                                                                                                                                                                                                                                                                                                                 |
| **Helm**               | 3.x.                                                                                                                                                                                                                                                                                                                                                           |
| **StorageClass**       | A StorageClass must be available — your cluster default is probably fine. The chart provisions a 10 GiB `ReadWriteOnce` PVC for Hub state. You can tune details if needed via `hub.persistence.*` ([chart README](https://github.com/earthly/charts/blob/main/README.md)).                                                                                     |
| **Ingress controller** | Must support gRPC backend routing. See [Ingress](https://github.com/earthly/charts/blob/main/README.md#ingress) in the chart README for an NGINX-tested example.                                                                                                                                                                                               |
| **DNS**                | A hostname for the Hub (e.g. `lunar.example.com`) pointing at your ingress controller's external IP — GitHub, CI integrations, and Lunar CLI users must reach it. If you let the chart run Grafana for you, add a second hostname for it (e.g. `grafana.lunar.example.com`) for your team's browser access; not needed if you point Lunar at your own Grafana. |
| **TLS certificate**    | The Hub (and the bundled Grafana, when enabled) listen plaintext. Terminate TLS at your ingress or an upstream load balancer for each hostname.                                                                                                                                                                                                                |

## Step 3 — Provision PostgreSQL

Lunar needs a single PostgreSQL database. Schema migrations run as a pre-rollout Job on each release, and the Hub manages several of its own schemas (including the default `public`). We recommend that you give it a dedicated DB where its role is the owner.

| Requirement         | Detail                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Version**         | PostgreSQL 16 or newer.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| **Connectivity**    | Reachable from both the Hub and Operator pod's network. The chart does **not** include Postgres.                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| **Role**            | Dedicated DB role for the Hub. Minimum: **database owner** (to create schemas, and grants on created objects) **plus cluster-level `CREATEROLE`** (to create the read-only `sqlapi_user` and `grafana_user` roles during migration). `SUPERUSER` also works.                                                                                                                                                                                                                                                                         |
| **Extensions**      | Optional: `pg_stat_statements` enabled in `shared_preload_libraries`. The Hub's diagnostics bundle uses it when present. Setting `shared_preload_libraries` requires a Postgres restart (or parameter-group reboot on RDS / Cloud SQL), so it's easier to enable at provisioning time than later.                                                                                                                                                                                                                                    |
| **Connection pool** | The Hub is stateless and runs as multiple replicas; each opens \~85 connections by default (`maxOpenConns + maxPoolConns + operatorPoolSize`), so total connections scale with `hub.replicaCount`. Size your Postgres (or PgBouncer) for `replicaCount × per-replica pool`. See [Scaling](/install/lunar-hub/hub-day2#scaling) in Day 2 Operations for tuning context.                                                                                                                                                               |
| **SSL**             | The Hub negotiates TLS by default via `hub.db.connectionOptions: "sslmode=require"`. Most managed Postgres (RDS, Aurora, Cloud SQL) ships with TLS forced and will connect out of the box. Plain Postgres deployments without TLS must set `hub.db.connectionOptions: "sslmode=disable"` explicitly. **Format is libpq KV pairs, space-separated** — to pass extra options write `"sslmode=require connect_timeout=10"` (NOT `&`-separated URL query). The default is not merged in when overridden, so include `sslmode=` yourself. |
| **Backups**         | Use your existing Postgres backup process; backups are your responsibility. All authoritative Hub and Operator state lives entirely in Postgres.                                                                                                                                                                                                                                                                                                                                                                                     |

{% hint style="info" %}
**Shared Postgres cluster?** Because `CREATEROLE` is a cluster-level attribute, Postgres has no mechanism to scope it to a single database. If granting it cluster-wide is too broad, pre-create the `sqlapi_user` role yourself (any password) and leave `HUB_SQLAPI_PASSWORD` unset. The migration's `IF NOT EXISTS` check skips role creation, leaving only schema-level grants — which the Hub's role can do as database owner.
{% endhint %}

## Step 4 — Provision S3-compatible object storage

Lunar needs two private S3 buckets, both writable by the Hub:

* A **Logs bucket**, which contains per-run log files. These can be short-lived; a 30-day lifecycle rule is reasonable here.
* A **Resources bucket**, which contains run-bundle archives fetched by init containers. Keep these as long as you might re-run historical catalogers, collectors, or policies.

Both buckets must block public access. Content may include credentials, user script source code, or PII surfaced from CI runs. The Hub serves all reads via time-limited pre-signed URLs.

{% hint style="warning" %}
**Object lifecycles are your responsibility.** Lunar never deletes from either bucket. Set S3 lifecycle rules yourself to cap storage growth. Check with your compliance requirements before settling on retention windows, since logs and run bundles may contain information that falls under your organization's data-retention policies.
{% endhint %}

The Hub *only* calls `PutObject`, `GetObject`, and `HeadObject` on both buckets, and issues pre-signed `GET`/`PUT` URLs for both. Minimum IAM policy on AWS:

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:PutObject"],
      "Resource": [
        "arn:aws:s3:::your-logs-bucket/*",
        "arn:aws:s3:::your-resources-bucket/*"
      ]
    }
  ]
}
```

### Region and credentials

The Hub picks up AWS credentials via the standard [SDK credential chain](https://docs.aws.amazon.com/sdkref/latest/guide/standardized-credentials.html) — the chart stays out of the credentials business. Region must be set explicitly:

```yaml
hub:
  extraEnv:
    - name: AWS_REGION
      value: us-east-1
```

Common patterns (see the [chart README](https://github.com/earthly/charts/blob/main/README.md#object-storage--aws-credentials) for full YAML):

* **EKS**. Annotate the chart's service account with an [IAM role for service accounts](https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html).
* **Static credentials**. Inject `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` from a secret via `hub.extraEnv`. Not recommended for production.
* **IMDS, pod identity, external secrets operators**. All the usual AWS-SDK-friendly mechanisms work.

### Non-AWS backends

MinIO, Cloudflare R2, and GCS in S3-compatibility mode all work — set `AWS_ENDPOINT_URL_S3` via `hub.extraEnv`:

```yaml
hub:
  extraEnv:
    - name: AWS_REGION
      value: auto
    - name: AWS_ENDPOINT_URL_S3
      value: https://your-minio.example.com
```

Lunar uses virtual-host-style S3 addressing only. Most MinIO, R2, and GCS deployments handle this out of the box. Path-style-only backends aren't currently supported.

## Step 5 — Create a GitHub App

The Hub authenticates to GitHub as a GitHub App. Use our hosted setup tool to create one in a couple of clicks.

{% hint style="info" %}
**Need to create the App manually?** If your GitHub instance is air-gapped, you're using GitHub Enterprise Server, or your security review needs every permission and event laid out before the App is created, see [manual GitHub App setup](/install/lunar-hub/hub-github-app-manual) instead.
{% endhint %}

### Using the setup tool

1. Visit [**earthly.dev/lunar/github-app-setup**](https://earthly.dev/lunar/github-app-setup/).
2. Follow the prompts. The tool uses GitHub's [manifest flow](https://docs.github.com/en/apps/creating-github-apps/setting-up-a-github-app-from-a-manifest/about-creating-github-apps-from-a-manifest) to register the App with the right permissions and events.
3. **Download the PEM private key when prompted.** GitHub shows it exactly once — if you click past this page, you'll have to generate a new key from the App settings later.
4. Click *Install App on GitHub* and select the org. Choose **All repositories** unless you have a specific reason not to — Lunar's actual monitoring scope is configured in `lunar-config.yml`, so a narrower scope here just means coming back to **Org Settings → GitHub Apps → Lunar → Repository access** every time you add a new repo to Lunar.

The hosted tool proxies the manifest exchange to GitHub and returns the credentials to your browser; we never persist them.

### Capture these before continuing

When you finish Step 5, you'll have four things — confirm you've saved all four before moving on:

| What                                                                       | Source                                                                                                                |
| -------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| **Owner** (the GitHub org or user the App is installed on, e.g. `earthly`) | The `<org>` in the install URL below                                                                                  |
| **App ID** (numeric, e.g. `3635822`)                                       | Setup tool result page                                                                                                |
| **Installation ID** (numeric)                                              | URL after install: `https://github.com/organizations/<org>/settings/installations/<INSTALL_ID>` — the trailing number |
| **PEM private key**                                                        | Downloaded from the setup tool — **shown only once, save it now**                                                     |

Set them via `HUB_GITHUB_APP_OWNER`, `HUB_GITHUB_APP_ID`, `HUB_GITHUB_APP_INSTALL_ID`, and `HUB_GITHUB_APP_PRIVATE_KEY` (or the chart equivalents under `hub.github.app.*`). All four are required in single-App mode; the Hub refuses to start without them.

For GitHub Enterprise Server, you also need to set `hub.github.baseUrl` in your values to point at your GHES instance.

### Installing in multiple GitHub orgs with one Hub

When the Hub fronts multiple customer orgs that each install their own Lunar App, register one entry per owner in `HUB_GITHUB_APPS`:

```bash
export HUB_GITHUB_APPS=$(cat <<'EOF'
[
  {"owner": "earthly", "app_id": 123, "private_key_path": "/secrets/github-apps/earthly.pem", "install_id": 100},
  {"owner": "acme",    "app_id": 456, "private_key_path": "/secrets/github-apps/acme.pem",    "install_id": 200}
]
EOF
)
```

Each entry pins its own App credentials and installation; the Hub mints tokens scoped to the right org per request. Owner matching is case-insensitive and trimmed.

For the PEM files, the recommended pattern is one Kubernetes Secret with multiple keys, mounted as a single volume:

```yaml
# Secret
data:
  earthly.pem: <base64-encoded PEM>
  acme.pem:    <base64-encoded PEM>

# Pod
volumes:
- name: github-apps
  secret: { secretName: github-apps }
volumeMounts:
- { name: github-apps, mountPath: /secrets/github-apps, readOnly: true }
```

`HUB_GITHUB_APPS` is mutually exclusive with the single-App env vars (`HUB_GITHUB_APP_OWNER` / `HUB_GITHUB_APP_ID` / `HUB_GITHUB_APP_PRIVATE_KEY` / `HUB_GITHUB_APP_INSTALL_ID`). Use one mode or the other.

## Step 6 — Plan your Kubernetes secrets

Several Kubernetes secrets come into play at install time. You create the first four below; the chart auto-generates the rest (with `helm.sh/resource-policy: keep`, so they survive upgrades and uninstalls).

| Secret                     | Who creates it             | Contents                                                                                                                                                                                                     |
| -------------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `lunar-db`                 | You                        | DB `username` and `password`                                                                                                                                                                                 |
| `lunar-github-app`         | You                        | `private-key` — the PEM from Step 5                                                                                                                                                                          |
| `lunar-hub-licence`        | You                        | `hub-licence.jwt` — Hub licence token                                                                                                                                                                        |
| `regcred`                  | You (derived from licence) | GHCR image-pull credentials — generated by [`lunar licence pull-secret`](/install/lunar-hub/hub-install#pulling-images-from-ghcr) from your licence JWT                                                      |
| `<release>-auth-token`     | Chart (auto-generated)     | Shared bearer token for the CLI and CI agents                                                                                                                                                                |
| `<release>-github-webhook` | Chart (auto-generated)     | Per-repo webhook signing secret — the Hub registers this with GitHub automatically when it creates per-repo webhooks                                                                                         |
| `<release>-grafana-admin`  | Chart (auto-generated)     | Grafana admin username and password — **only when the chart runs Grafana for you** ([Option A](/install/lunar-hub/hub-install#grafana))                                                                      |
| `<release>-grafana-db`     | Chart (auto-generated)     | Read-only `grafana_user` DB-role password (used by the dashboards' datasource) — whenever the provisioning Job runs (bundled **or** your own Grafana). BYO via `grafana.provisioning.dbPassword.secretName`. |

Pointing Lunar at **your own Grafana** ([Option B or C](/install/lunar-hub/hub-install#grafana))? The chart doesn't generate `<release>-grafana-admin` — you create a secret holding your Grafana service-account token (or admin username/password). The `<release>-grafana-db` secret above is still chart-managed. See [Install Step 4 → Grafana](/install/lunar-hub/hub-install#grafana).

{% hint style="info" %}
**Your licence also covers the CI agent.** The credential the chart uses to pull Lunar's container images — derived from your licence with [`lunar licence pull-secret`](/install/lunar-hub/hub-install#pulling-images-from-ghcr) for the `regcred` secret above — is also what the Hub uses to fetch the CI agent binary for your runners. You don't need a separate credential for the CI agent.

If your licence doesn't include CI agent distribution, the Lunar CLI reports a clear error the first time a runner tries to download the agent. If you plan to use the CI agent, make sure your licence covers it — contact Earthly if you're unsure.
{% endhint %}

**GitOps alternative.** If your setup needs deterministic secret management, you can pre-create any chart-managed secret and point the chart at it (e.g. `hub.github.webhookSecret.secretName`). See the [chart README](https://github.com/earthly/charts/blob/main/README.md) for the full list of `*.secretName` values you can override.

The [install walkthrough](/install/lunar-hub/hub-install#step-3--create-kubernetes-secrets) has the exact `kubectl create secret` commands for the three user-created secrets.

## Step 7 — Size for capacity

The chart sets **no** default resource requests or limits. The numbers below are reasonable starting points, but you should monitor and adjust them based on your specific needs.

| Component | CPU request | Memory request |
| --------- | ----------- | -------------- |
| Hub       | 500m        | 1 Gi           |
| Operator  | 100m        | 128 Mi         |

**Run pods** are short-lived batch pods spawned by the operator. Each pod contains N **user containers** (one per script in the batch) plus an init container and a sidecar. Per-user-container resources come from `operator.snippetContainerSpec*` — the operator's built-in defaults request 250m / 256 Mi for collectors and catalogers, 50m / 128 Mi for policies.

**Batch size** is per script type via `operator.batchMaxCount*` (defaults: 10 for collectors and catalogers, 20 for policies — policies pack denser because each container is lighter). Concurrent batch pods are capped by `operator.maxConcurrent` (default `10`), shared across script types.

Hub workers that feed run pods can be capped with `HUB_MAX_WORKERS_COLLECT`, `HUB_MAX_WORKERS_POLICY`, `HUB_MAX_WORKERS_CRON_COLLECT`, and `HUB_MAX_WORKERS_CATALOGER`; `0` means unlimited. Tune these when increasing `operator.maxConcurrent`. The Hub and operator have separate Postgres pool caps for operator work via `HUB_MAX_OPERATOR_POOL_SIZE` and `OPERATOR_MAX_POOL_SIZE`.

The run-pods namespace needs headroom for:

```
maxConcurrent × batchMaxCount(type) × per-user-container requests
```

At defaults that's \~25 GiB of concurrent-run memory per script type (10 pods × 10 collectors × 256 Mi, or 10 × 20 × 128 Mi for policies).

If you want sizing guidance once you're past initial install, please reach out.

## Next steps

When your prerequisites are in place:

* [Install walkthrough](/install/lunar-hub/hub-install) — step-by-step from zero to a working Hub.
* [Chart README](https://github.com/earthly/charts/blob/main/README.md) — complete `values.yaml` reference.


# Manual GitHub App Setup

This page is the fallback for [prereqs Step 5](/install/lunar-hub/hub-prereqs#step-5--create-a-github-app). Most users should use [the hosted setup tool](https://earthly.dev/lunar/github-app-setup/) — it creates the App with the right permissions and events in a couple of clicks.

Use the manual flow if:

* Your GitHub instance is air-gapped or otherwise can't reach `earthly.dev` from a browser session.
* You're standing the App up against **GitHub Enterprise Server** and prefer your GHES web UI for the audit trail.
* Your security review needs explicit visibility into every permission and event before the App is created.

## Create the App

Create the App at GitHub's [App creation page](https://github.com/settings/apps/new) (or for an org: **Org Settings → Developer settings → GitHub Apps → New GitHub App**). GitHub's [registering-a-github-app docs](https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/registering-a-github-app) walk through every form field if you want a reference.

For GHES, create the App on your GHES instance instead.

**Permissions:**

| Permission           | Access | Why                                           |
| -------------------- | ------ | --------------------------------------------- |
| `actions`            | read   | Read workflow runs for CI data collection     |
| `checks`             | write  | Post policy results as PR checks              |
| `contents`           | read   | Fetch config and source for policy evaluation |
| `metadata`           | read   | Required by GitHub on every App               |
| `pull_requests`      | write  | Post PR comments and statuses                 |
| `repository_hooks`   | write  | Auto-register per-repo webhooks               |
| `organization_hooks` | write  | Auto-register organization-level webhooks     |

**Subscribe to events:** `push`, `pull_request`, `workflow_run`.

**Other fields:**

* **Homepage URL** — any URL works (e.g. your internal Lunar URL, or `https://earthly.dev/lunar`).
* **Webhook** — uncheck "Active." The Hub registers its own per-repo webhooks at runtime; the App-level webhook is unused. URL can be a placeholder (e.g. `https://example.com/placeholder`) — GitHub requires a value but nothing will ever hit it.
* **Webhook secret** — leave blank. Since you unchecked "Active" above, GitHub won't deliver App-level events. The Hub's per-repo webhook signing secret is a separate thing — see [prereqs Step 6](/install/lunar-hub/hub-prereqs#step-6--plan-your-kubernetes-secrets).
* **Where can this app be installed?** — "Only on this account."

## After creating the App

1. **Generate a private key** (App settings → "Private keys" → "Generate a private key"). Save the `.pem` file — GitHub does not show it again.
2. **Install the App** on your organization (App settings → "Install App"). Choose **All repositories** unless you have a specific reason not to — Lunar's actual monitoring scope is configured in `lunar-config.yml`, so a narrower scope here just means coming back to this page every time you add a new repo to Lunar.

## Capture these before continuing

| What                                 | Source                                                                                                                |
| ------------------------------------ | --------------------------------------------------------------------------------------------------------------------- |
| **App ID** (numeric, e.g. `3635822`) | App settings page                                                                                                     |
| **Installation ID** (numeric)        | URL after install: `https://github.com/organizations/<org>/settings/installations/<INSTALL_ID>` — the trailing number |
| **PEM private key**                  | Downloaded from "Generate a private key" — **shown only once, save it now**                                           |

For GitHub Enterprise Server, you also need to set `hub.github.baseUrl` in your values to point at your GHES instance.

Return to [prereqs Step 6](/install/lunar-hub/hub-prereqs#step-6--plan-your-kubernetes-secrets) once you've captured all three.


# Install Walkthrough

This walkthrough takes you from zero to a working Lunar Hub. It assumes you've worked through the [prerequisites](/install/lunar-hub/hub-prereqs), and have:

* Kubernetes cluster with your namespaces created
* Postgres database
* two S3 buckets
* GitHub App
* hub licence key
* DNS name for the Hub

Assuming your prerequisites are in order, expect the install to take 15–30 minutes end to end. The bulk of that is waiting for external systems (e.g. first-boot migrations).

## Step 1 — Add the Helm repo

```bash
helm repo add earthly https://earthly.github.io/charts
helm repo update
```

Verify:

```bash
helm search repo earthly/lunar
```

You should see the chart listed with a version number.

## Step 2 — Verify your prerequisites

Before going further, sanity-check the external dependencies you provisioned in the [prerequisites](/install/lunar-hub/hub-prereqs). An issue here is cheaper to find now than after you've created secrets and written your `values.yaml`.

**DNS** ([prereqs Step 2](/install/lunar-hub/hub-prereqs#step-2--check-your-kubernetes-cluster)):

```bash
nslookup lunar.example.com
```

Should resolve to your ingress controller's external IP. If it doesn't yet, either wait or add an `A` / `CNAME` record now — GitHub webhook auto-registration needs a publicly reachable URL.

**Postgres reachability** ([step 3](/install/lunar-hub/hub-prereqs#step-3--provision-postgresql)):

```bash
kubectl run -it --rm --restart=Never -n lunar pg-check \
  --image=postgres:16 -- \
  psql "postgresql://<user>:<pass>@<host>:5432/lunar" \
  -c "SELECT 1;"
```

Should return `1`. If the connection fails or the role is rejected, fix that before installing — the Hub will crash-loop otherwise.

**S3 access** ([prereqs Step 4](/install/lunar-hub/hub-prereqs#step-4--provision-s3-compatible-object-storage)):

```bash
aws s3api head-bucket --bucket your-lunar-logs-bucket
aws s3api head-bucket --bucket your-lunar-resources-bucket
```

Both should succeed silently (exit code `0`). This requires the AWS CLI with credentials configured on your workstation — it confirms the buckets exist and that *some* credentials can reach them, but doesn't validate the in-cluster IRSA path. If your workstation isn't AWS-configured, skip this check; Hub logs surface S3 misconfiguration immediately on install.

## Step 3 — Create Kubernetes secrets

You'll need to create three Kubernetes secrets yourself:

* Database credentials
* The GitHub App private key (the PEM you saved during [prereqs Step 5](/install/lunar-hub/hub-prereqs#step-5--create-a-github-app))
* A signed Hub licence JWT (from your Earthly contact)

Other secrets, like the Hub auth token, GitHub webhook secret, and Grafana admin password — are auto-generated by the chart on first install and preserved across upgrades. You'll retrieve their values after install in [Step 6](#step-6--verify-the-install).

Create the three with `kubectl`, or provision them with your secret manager of choice, using the same names and keys as specified below.

**Database credentials:**

```bash
kubectl -n lunar create secret generic lunar-db \
  --from-literal=username='<db-user>' \
  --from-literal=password='<db-password>'
```

**GitHub App private key** — base64-encode the PEM and store it as the `private-key` field:

```bash
kubectl -n lunar create secret generic lunar-github-app \
  --from-literal=private-key="$(base64 < path/to/lunar-github-app-<id>.pem | tr -d '\n')"
```

{% hint style="info" %}
The PEM must be base64-encoded inside the secret value — the Hub decodes it after reading the env var. (Kubernetes then base64-encodes the whole secret again for etcd storage, so it's double-encoded at rest.)
{% endhint %}

**Hub licence JWT** — store the signed token in `hub-licence.jwt`:

```bash
kubectl -n lunar create secret generic lunar-hub-licence \
  --from-literal=hub-licence.jwt='<signed-licence-jwt>'
```

If your collectors need credentials at runtime (`github.*` collectors, Datadog, Jira, Linear, etc.), provision a fourth secret as well — see [Script runtime secrets](#script-runtime-secrets) at the bottom of this page.

## Step 4 — Write `values.yaml`

Create a `values.yaml` with the minimum configuration the chart needs. The example below is a working baseline — copy it, fill in the placeholders, and adjust for your environment. The GitHub App ID and installation ID come from [prereqs Step 5](/install/lunar-hub/hub-prereqs#step-5--create-a-github-app).

```yaml
# Cluster assumptions baked into this sample:
#   - NGINX ingress controller    → hub.ingress.className + grpcAnnotations
#   - cert-manager for TLS        → ingress annotations (drop if BYO secret)
#   - EKS IRSA for S3 credentials → serviceAccount.annotations (see "AWS credentials" below)

# IRSA on EKS: annotate the service account with your IAM role ARN.
# See "AWS credentials" below for non-EKS options.
serviceAccount:
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/lunar-hub-s3

hub:
  publicBaseURL: "https://lunar.example.com"
  licence:
    secretName: "lunar-hub-licence"
    secretKey: "hub-licence.jwt"

  # Run the Hub as multiple replicas. The Hub is stateless — all state lives
  # in Postgres and S3 — so replicas coordinate through Postgres (leader
  # election, advisory locks) with no peer-to-peer wiring. See "Replicas and
  # the Postgres connection budget" below before raising this.
  replicaCount: 3
  # Keep a quorum during node drains and rollouts. Enable this whenever
  # replicaCount > 1 (a PDB on a single replica blocks voluntary node drains).
  podDisruptionBudget:
    enabled: true
    maxUnavailable: 1

  # Pin all four image tags (hub, operator, initImage, sidecarImage) to the
  # same released hub version for a reproducible install. The latest is
  # published to ghcr.io/earthly/lunar-hub — see the repo's `lunar-hub-v*`
  # release tags. This example is bumped automatically on each Hub release.
  image:
    tag: "3.2.0"

  db:
    host: "your-db-host.example.com"
    name: "lunar"

  s3:
    logsBucket: "your-lunar-logs-bucket"
    resourcesBucket: "your-lunar-resources-bucket"

  github:
    app:
      # Don't forget the quotes, otherwise they might get rendered as scientific notation!
      id: "123456"
      installId: "78901234"
      # The GitHub org (or user) the App is installed on. Required since chart 2.2.0.
      owner: "<your-github-org>"

  # AWS region for S3. Credentials themselves come from the AWS SDK
  # credential chain (IRSA above, or see "AWS credentials" below).
  extraEnv:
    - name: AWS_REGION
      value: us-east-1

  # The chart sets no resource defaults. Numbers below are the starting
  # recommendations from prereqs Step 7 — tune as you observe real load.
  resources:
    requests:
      cpu: 500m
      memory: 1Gi

  ingress:
    enabled: true
    host: lunar.example.com
    className: nginx
    tls:
      - secretName: lunar-tls
        hosts: [lunar.example.com]
    annotations:
      # cert-manager provisions the `lunar-tls` secret automatically.
      # Remove this annotation if you supply the TLS secret yourself
      # (corporate CA, external LB, ACM, etc.).
      cert-manager.io/cluster-issuer: letsencrypt
    # gRPC backend-protocol applies only to the gRPC Ingress. Both
    # Ingresses are always rendered when ingress.enabled is true.
    grpcAnnotations:
      nginx.ingress.kubernetes.io/backend-protocol: "GRPC"

operator:
  # Run pods are created here. The namespace must already exist; the chart
  # will not create it. See prereqs Step 1 for the trust-boundary rationale.
  snippetNamespace: "lunar-scripts"

  image:
    tag: "3.2.0"
  initImage:
    tag: "3.2.0"
  sidecarImage:
    tag: "3.2.0"

  resources:
    requests:
      cpu: 100m
      memory: 128Mi

# Grafana — the primary UI for Lunar. Enabled by default: the chart runs a
# stock Grafana and installs Lunar's dashboards into it. This block just wires
# up its ingress. If you'd rather use a Grafana you already run (Cloud,
# Enterprise, or self-hosted OSS), drop this block and see the "Grafana"
# section below for how to point Lunar at it instead.
grafana:
  ingress:
    enabled: true
    hosts: [grafana.lunar.example.com]
    tls:
      - secretName: lunar-grafana-tls
        hosts: [grafana.lunar.example.com]
    annotations:
      cert-manager.io/cluster-issuer: letsencrypt
```

Everything else has a sensible default. See the [chart README](https://github.com/earthly/charts/blob/main/README.md#values-reference) for the full values reference.

### Replicas and the Postgres connection budget

The Hub is stateless, so `replicaCount` scales it horizontally with no extra wiring — but each replica opens its own pool of Postgres connections, so total connections scale roughly linearly with the replica count:

```
total ≈ replicaCount × (hub.db.maxOpenConns + hub.db.maxPoolConns + hub.db.operatorPoolSize)
```

The chart defaults (`40 + 40 + 5 ≈ 85` per replica) match the Hub's built-in values, so a single replica is unchanged. Before running several replicas, confirm the total stays within your Postgres `max_connections` — leave headroom for the migrate Job, `psql`, and superuser-reserved slots. For larger replica counts, **lower `hub.db.maxOpenConns` / `hub.db.maxPoolConns`** so the total fits, and/or front Postgres with a connection pooler. Keep `maxPoolConns` at or above your peak concurrent queue workers (the sum of `hub.maxWorkers.*`) — the pool is shared by every queue, so a pool below that serializes store queries under load.

The chart soft-spreads replicas across nodes by default (`topologySpreadConstraints`, `maxSkew 1` across `kubernetes.io/hostname`, `ScheduleAnyway`) so a single node loss doesn't take out the whole Hub. Override `hub.topologySpreadConstraints` to impose your own (e.g. a hard zone spread) — it replaces the default wholesale.

### A note on ingress

The Hub serves two protocols on separate ports — gRPC on `8000` (API) and plain HTTP on `8001` (webhook receiver + pre-signed URL redirector for run logs). Most ingress controllers apply `backend-protocol` per-Ingress rather than per-path, so the chart renders **two** Ingress resources sharing the same hostname and TLS config whenever `hub.ingress.enabled: true`. Per-protocol annotations layer over the shared `annotations` map: `grpcAnnotations` apply only to the gRPC Ingress, `httpAnnotations` only to the HTTP one. Both Ingresses are always created together — there's no per-protocol toggle.

If you'd rather front the Hub yourself (a controller with per-path backend protocols, a service mesh, or a LoadBalancer Service), set `hub.ingress.enabled: false` and route to the `hub-grpc` (port `8000`) and `hub-http` (port `8001`) service ports directly.

### Grafana

Lunar ships dashboards for policy results, component health, and collection activity, and installs them — with their datasources and panel plugins — into a Grafana over its HTTP API. There are three ways to provide that Grafana; pick the one that matches what you already run. The `grafana:` block in the values above shows the first (default) option.

In every case the chart auto-generates the read-only `grafana_user` datasource password (`<release>-grafana-db`) and wires it end-to-end for you. If you'd rather provide your own secret, set `grafana.provisioning.dbPassword.secretName` (recommended for GitOps).

#### Option A — bundled Grafana (default)

The default (`grafana.mode: chart`): the chart deploys a stock Grafana next to the Hub, runs a post-install/upgrade Job that installs the dashboards into it, and auto-generates the admin login (which the Hub uses to reach the pod).

Wire up the ingress in the `grafana:` block above so your team can reach it in a browser, and retrieve the admin login in [Step 6](#step-6--verify-the-install). Nothing else is required — `grafana.mode` defaults to `chart`.

#### Option B — Grafana Cloud or Grafana Enterprise (service-account token)

Point the Hub at your existing Grafana with a [service-account token](https://grafana.com/docs/grafana/latest/administration/service-accounts/). Create a service account with the **Admin** role, generate a token, then disable the bundled Grafana and wire the token in:

```yaml
grafana:
  mode: external                  # bring your own Grafana (no bundled pod)
  url: "https://your-org.grafana.net"
  auth:
    secretName: lunar-grafana     # holds the service-account token (created below)
    tokenKey: token
```

That's the whole configuration. On `helm install` / `helm upgrade` the chart's `post-install` / `post-upgrade` hook Job runs the provisioning tool against your Grafana — `deploy.sh` resolves the URL, credentials, and read-only datasource connection from the Hub over gRPC, then installs the plugins, datasources, and dashboards over Grafana's HTTP API. The Job is idempotent and non-blocking (a failed provision is logged but never rolls back the Hub upgrade), and it re-runs on every subsequent upgrade so the dashboards stay in sync with the Hub's schema. No manual step.

#### Option C — self-hosted OSS Grafana (admin username + password)

Same as Option B, but OSS Grafana can't install plugins with a service-account token — the dashboards' panel plugins need a **server admin**. Give the Hub an admin username and password instead of a token:

```yaml
grafana:
  mode: external
  url: "https://grafana.example.com"
  auth:
    secretName: lunar-grafana     # holds username + password (created below)
    # userKey / passwordKey default to "username" / "password"
```

Deploy the same way as Option B: `helm install` / `helm upgrade` runs the chart's hook Job automatically.

The examples reference a `lunar-grafana` secret you create — the key depends on your option: `token` for Option B, or `username` + `password` for Option C:

```bash
# Option B (Cloud / Enterprise)
kubectl -n lunar create secret generic lunar-grafana \
  --from-literal=token='<grafana-service-account-token>'

# Option C (OSS) — use these keys instead of `token`
kubectl -n lunar create secret generic lunar-grafana \
  --from-literal=username='admin' \
  --from-literal=password='<grafana-admin-password>'
```

{% hint style="info" %}
`grafana.url` + `grafana.auth` are all the chart needs for an external Grafana — it wires them into the Hub for you (as `HUB_GRAFANA_URL_BASE` and `HUB_GRAFANA_TOKEN`, or `HUB_GRAFANA_USER` / `HUB_GRAFANA_PASSWORD`), so there's no `hub.extraEnv` plumbing. The chart's provisioning Job runs whenever `grafana.mode != off` — the same Job that serves the bundled pod — installing the dashboards on every install/upgrade.
{% endhint %}

{% hint style="warning" %}
Options B and C need three network paths:

* **your Grafana → the Hub's Postgres** (as the read-only `grafana_user` role) — the Postgres datasource
* **your Grafana → the Hub's HTTP API** — the Infinity datasource
* **the in-cluster Job → your Grafana** (HTTP) — the deploy Job

Same-network self-hosted Grafana has all three. Grafana Cloud needs a route to both Hub endpoints (public endpoint, PrivateLink, or Private Data Source Connect) so the in-cluster Job can reach it.
{% endhint %}

### AWS credentials

The chart does not manage AWS credentials. The example above uses **IRSA on EKS** (annotation on the service account), which is the recommended pattern. The conceptual breakdown of patterns is in [prereqs Step 4 → Region and credentials](/install/lunar-hub/hub-prereqs#region-and-credentials).

### Pulling images from GHCR

Lunar Hub images are published to a private GitHub Container Registry (`ghcr.io/earthly/*`). To pull them your cluster needs an image pull secret. You can derive the pull secret with the [Lunar CLI](/install/cli):

```bash
lunar licence pull-secret \
  --licence-file=path/to/hub-licence.jwt \
  --namespace=lunar \
  --name=regcred | kubectl apply -f -
```

If your snippet pods run in a separate namespace (`operator.snippetNamespace`), run it again for that namespace:

```bash
lunar licence pull-secret \
  --licence-file=path/to/hub-licence.jwt \
  --namespace=lunar-scripts \
  --name=regcred | kubectl apply -f -
```

Reference the secret in your `values.yaml`:

```yaml
imagePullSecrets:
  - name: regcred

hub:
  image:
    repository: ghcr.io/earthly/lunar-hub

operator:
  image:
    repository: ghcr.io/earthly/lunar-snippet-operator
  initImage:
    repository: ghcr.io/earthly/lunar-snippet-init
  sidecarImage:
    repository: ghcr.io/earthly/lunar-snippet-sidecar
```

Lunar also ships the Grafana dashboards in a private deploy image, `ghcr.io/earthly/lunar-dashboards`, which the chart's dashboards-deploy Job pulls under the same `imagePullSecrets` — it uses that repository by default, so no extra image configuration is needed.

## Step 5 — Install

```bash
helm install lunar earthly/lunar \
  --namespace lunar \
  -f values.yaml
```

The chart fails fast with a clear error if any of the [required fields](https://github.com/earthly/charts/blob/main/README.md#required-values) are missing.

Watch the pods come up:

```bash
kubectl -n lunar get pods -w
```

Expected rollout, in order:

1. `lunar-hub-migrate-<hash>` runs first — a pre-install Helm hook Job that applies DB migrations and gates the rest of the rollout. It runs to `Completed` before any Hub pod is created.
2. `lunar-hub-<hash>` pods start (one per `replicaCount`). Each asserts the schema is current at boot and refuses to start if it's behind — so serving pods never run against an un-migrated schema. They flip to `Running 1/1` once the readiness probe (`/ready`) passes.
3. `lunar-operator-<hash>` depends on the applied migrations. Once the Hub is up, it flips to `Running 1/1` shortly after.

Migrations can take a minute or two on a warm DB. If the migrate Job fails, or a Hub pod restarts repeatedly, check the logs:

```bash
# Migrate Job (if it failed the hook, the whole install/upgrade is rolled back):
kubectl -n lunar logs -l app.kubernetes.io/component=hub-migrate --tail=200
# Hub pods:
kubectl -n lunar logs -l app.kubernetes.io/component=hub --tail=200
```

Common first-install failures (visible while the Hub is starting up):

| Symptom in logs                             | Fix                                                                                                                                                                                                                                                                                                                              |
| ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `pq: permission denied for schema public`   | The DB role lacks `CREATE` on the database. Grant it, or use a database-owner role. See [prerequisites step 3](/install/lunar-hub/hub-prereqs#step-3--provision-postgresql).                                                                                                                                                     |
| `dial tcp ...: i/o timeout`                 | The Hub pod can't reach Postgres on the network — security group, network policy, or firewall block between the pod and the DB. Check VPC routing and any `NetworkPolicy` in the `lunar` namespace.                                                                                                                              |
| `dial tcp ...: connect: connection refused` | Postgres is reachable but not listening on the configured port (`hub.db.port`, default `5432`), or the process is down.                                                                                                                                                                                                          |
| `lookup ...: no such host`                  | DNS issue — the DB hostname doesn't resolve from inside the cluster.                                                                                                                                                                                                                                                             |
| `failed to parse private key`               | The `lunar-github-app` secret doesn't contain a valid PEM, or the PEM wasn't base64-encoded before going into the secret value (see the base64 note in [Step 3](#step-3--create-kubernetes-secrets)). Recreate from the PEM you saved during [prerequisites step 5](/install/lunar-hub/hub-prereqs#step-5--create-a-github-app). |

The Hub validates Postgres at startup, but **defers GitHub and S3 to first use** — those misconfigurations won't appear in install logs. They surface once something exercises them (e.g. `lunar hub pull github://...` or a script run):

| Symptom (after triggering an operation)     | Likely cause                                                                                                                                                                                  |
| ------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `401 Unauthorized` from GitHub API          | Wrong `hub.github.app.id`, or the PEM in the `lunar-github-app` secret doesn't match the App GitHub knows about.                                                                              |
| `404 Not Found` on `installations/<id>/...` | Wrong `hub.github.app.installId`, or the App is no longer installed on the org.                                                                                                               |
| `AccessDenied` from S3                      | IAM policy too narrow — confirm `s3:GetObject` and `s3:PutObject` on both buckets. See [prerequisites step 4](/install/lunar-hub/hub-prereqs#step-4--provision-s3-compatible-object-storage). |
| `NoSuchBucket` from S3                      | Bucket name mismatch between `hub.s3.logsBucket` / `hub.s3.resourcesBucket` and what actually exists.                                                                                         |
| `PermanentRedirect` from S3                 | `AWS_REGION` doesn't match the bucket's region.                                                                                                                                               |

## Step 6 — Verify the install

Once the Hub and Operator pods are `Running 1/1`, confirm the Hub is healthy end to end.

**Retrieve the Hub auth token** — the chart generated this on first install. You'll need it for the CLI step below.

```bash
kubectl -n lunar get secret lunar-auth-token \
  -o jsonpath='{.data.token}' | base64 -d
```

**Grafana access** — this depends on which [Grafana option](#grafana) you chose.

If you use the **bundled Grafana** (Option A), retrieve its auto-generated admin credentials:

```bash
kubectl -n lunar get secret lunar-grafana-admin \
  -o jsonpath='{.data.username}' | base64 -d
  
kubectl -n lunar get secret lunar-grafana-admin \
  -o jsonpath='{.data.password}' | base64 -d
```

The commands above assume the release name `lunar` (from `helm install lunar ...`). For other release names, see [Required secrets](https://github.com/earthly/charts/blob/main/README.md#required-secrets) in the chart README for the resolved name.

Open `https://grafana.lunar.example.com` and log in with the credentials above. The home dashboard surfaces policy results, component health, and collection activity. If the page doesn't load, confirm the `grafana.lunar.example.com` `A` / `CNAME` record points at your ingress controller and the cert is valid.

If you pointed Lunar at **your own Grafana** (Option B or C), log in there with your existing credentials — the Lunar dashboards appear in a "Lunar" folder.

**HTTP ingress reachability** — the webhook endpoint rejects unsigned requests but returns a predictable status, so it's a decent routing smoke test:

```bash
curl -sS -o /dev/null -w "%{http_code}\n" https://lunar.example.com/webhooks/github
```

Expected: `400` (the Hub rejects the request for missing headers). `404` or `502` probably means the HTTP Ingress isn't reaching the Hub — confirm `hub.ingress.enabled: true` in your values, and that DNS still resolves to your ingress controller.

**CLI connectivity smoke test** — install the [Lunar CLI](/install/cli), then save this minimal [`lunar-config.yml`](/configuration/lunar-config) locally and run:

```yaml
version: 0
hub:
  host: lunar.example.com
  grpcPort: 443
  httpPort: 443
collectors: []
policies: []
```

```bash
export LUNAR_HUB_TOKEN=<token-from-step-above>
lunar secret list
```

This authenticates over gRPC and returns the configured script runtime secrets (an empty list on a fresh install). Success means the CLI can reach the Hub through ingress, TLS terminates correctly, and the auth token is valid. Adjust the ports if you're using NodePort or non-default ingress.

**End-to-end with a config repo** — if you have a [`lunar-config.yml`](/configuration/lunar-config) repository ready, pull it in:

```bash
lunar hub pull github://your-org/your-config-repo@main
```

This exercises the full path: CLI → Hub → GitHub (App auth) → repo fetch → Hub stores config. As a side effect, the Hub registers a webhook on every repo referenced in the config. Verify webhook registration by checking any of those repos → **Settings → Webhooks** in GitHub — you should see a new entry pointing at `https://lunar.example.com/webhooks/github`.

If you don't have a config repo yet, that's the natural next step — see [the lunar-config docs](/configuration/lunar-config) for setup.

## Script runtime secrets

Collectors or catalogers that hit external APIs (`github.*` collectors, Datadog, Jira, Linear, etc.) may need credentials at runtime. Each plugin's README in [`earthly/lunar-lib`](https://github.com/earthly/lunar-lib) lists the `LUNAR_SECRET_*` variables it expects — drop the prefix when writing the secret. The Hub re-adds `LUNAR_SECRET_` when it injects each key as an env var in the script pod, so `GH_TOKEN` in the secret surfaces as `$LUNAR_SECRET_GH_TOKEN` in the script.

Provision the secret with one `--from-literal` per variable:

```bash
kubectl -n lunar create secret generic lunar-script-secrets \
  --from-literal=GH_TOKEN='<your-pat>' \
  --from-literal=LINEAR_API_KEY='<your-linear-key>'
  # ... one --from-literal per LUNAR_SECRET_* variable your plugins expect
```

Wire it into `values.yaml` (`hub.secrets.*.perKey` requires chart `2.2.0+`). The same secret can back both scopes; omit the one you don't use:

```yaml
hub:
  secrets:
    collector:
      secretName: lunar-script-secrets
      perKey: true
    cataloger:
      secretName: lunar-script-secrets
      perKey: true
```

See [day-2 → Other secrets](/install/lunar-hub/hub-day2#other-secrets) for rotation.

## Next steps

* [Install the Lunar CLI](/install/cli) on your workstation.
* [Install the Lunar CI Agent](/install/lunar-ci-agent/agent-self-hosted) on your CI runners.
* [Day-2 operations](/install/lunar-hub/hub-day2) — upgrades, secret rotation, observability, uninstall.


# Day-2 Operations

This page covers what you need to run Lunar Hub after the initial install: upgrades, secret rotation, observability, and uninstall. For the per-version upgrade notes (what values changed, what to migrate), see the [chart README's Upgrading section](https://github.com/earthly/charts/blob/main/README.md#upgrading).

## Upgrading

```bash
helm repo update
helm upgrade lunar earthly/lunar \
  --namespace lunar \
  -f values.yaml
```

Migrations run once per release as a **pre-rollout Job** (`lunar-hub-migrate`), installed via a Helm pre-install/pre-upgrade hook that completes — and gates the rollout — before any new Hub pod is created. Serving pods then **assert the schema is current at boot and refuse to start if it's behind**, which also keeps scaled-up and restarted pods safe (they don't re-run the Job). Migrations are forward-only and an interrupted one is safe to retry.

**Before every upgrade:**

1. **Back up Postgres.** Migrations are forward-only. If you need to roll back, roll the database back.
2. **Preview the chart changes.** The [`helm-diff`](https://github.com/databus23/helm-diff) plugin is worth installing — `helm diff upgrade lunar earthly/lunar -f values.yaml` shows exactly what Kubernetes resources will change.
3. **Read the version-specific notes** in the [chart README](https://github.com/earthly/charts/blob/main/README.md#upgrading). Minor version bumps occasionally require values changes.
4. **Pin your new image tags** in `values.yaml`. Don't rely on the chart's default tags.

{% hint style="warning" %}
Migrations are forward-only — back up Postgres first (step 1). Author schema changes **expand/contract** (add first, drop in a later release) so the common upgrade stays zero-downtime: the migrate Job runs while the old-version pods keep serving, then Helm rolls the Deployment. Reserve a maintenance window only for an unavoidable breaking change, and confirm the budget with stakeholders — a heavy migration can take tens of minutes, during which the old version keeps serving.
{% endhint %}

With multiple replicas and a PodDisruptionBudget, Helm's rolling update replaces Hub pods a few at a time while the rest keep serving, so a routine upgrade has no API-unavailability window.

## Rotating secrets

Lunar's secrets split into two categories — **user-managed** (you create and rotate them) and **chart-managed** (auto-generated on first install, preserved across upgrades via `helm.sh/resource-policy: keep`). See [Required secrets](https://github.com/earthly/charts/blob/main/README.md#required-secrets) in the chart README for the full list and naming rules.

### Hub auth token (`<release>-auth-token`)

Used by the CLI and CI agents to authenticate to the Hub. The Hub accepts a single token today — rotation requires a coordinated cutover. To force a regeneration, delete the secret and re-run `helm upgrade` (the chart will generate a fresh token):

```bash
kubectl -n lunar delete secret lunar-auth-token
helm upgrade lunar earthly/lunar -n lunar -f values.yaml
kubectl -n lunar rollout restart deployment/lunar-hub
kubectl -n lunar rollout restart deployment/lunar-operator
```

Both the Hub and the Operator read this token from the same secret (the Operator uses it as `OPERATOR_HUB_TOKEN` to call the Hub), so restart both. Retrieve the new token with `kubectl -n lunar get secret lunar-auth-token -o jsonpath='{.data.token}' | base64 -d`, then update every CI agent's `LUNAR_HUB_TOKEN` and every developer's CLI config. Active builds authenticated with the old token will fail mid-run and need to be retried. Dual-token support is on the roadmap.

To pin to an externally-managed token instead, set `hub.auth.secretName` in values to a secret you control — the chart will consume it instead of generating its own.

### GitHub App private key (`lunar-github-app`)

GitHub supports multiple active private keys per App — rotate with zero downtime:

1. **Generate a new key** in GitHub → **Apps → your App → Private keys → Generate a private key**. Download the new PEM.
2. **Update the Kubernetes secret** in place, matching the install-time encoding (the Hub expects a base64-encoded PEM inside the secret value):

   ```bash
   kubectl -n lunar create secret generic lunar-github-app \
     --from-literal=private-key="$(base64 < path/to/new-key.pem | tr -d '\n')" \
     --dry-run=client -o yaml | kubectl apply -f -
   ```
3. **Roll the Hub** — `kubectl -n lunar rollout restart deployment/lunar-hub`. The App credentials (App ID, installation ID, private key) are read **once at boot** and frozen for the process lifetime, so updating the secret alone has no effect on running pods — a rolling restart is required. With multiple replicas the rollout is zero-downtime.
4. **Verify** webhooks still deliver successfully (GitHub → any repo → **Settings → Webhooks → Recent Deliveries**).
5. **Delete the old key** in GitHub.

### GitHub webhook secret (`<release>-github-webhook`)

Changing the webhook secret requires re-registering every repo's webhook. Easier path: delete and regenerate, then re-pull your primary config to trigger re-registration.

```bash
kubectl -n lunar delete secret lunar-github-webhook
helm upgrade lunar earthly/lunar -n lunar -f values.yaml
kubectl -n lunar rollout restart deployment/lunar-hub

# Re-pull triggers webhook re-registration on every repo in your config.
lunar hub pull github://your-org/your-config-repo@main
```

During the window between Hub restart and `lunar hub pull`, per-repo webhooks from GitHub are still signed with the old secret, and the new Hub rejects them with a `500`. **GitHub does not auto-retry failed webhook deliveries** — any events in that window are lost unless you manually redeliver them from **Settings → Webhooks → Recent Deliveries** on the affected repos. Minimize the gap by running `lunar hub pull` immediately after the Hub restart comes up healthy.

### Database password (`lunar-db`)

Standard Kubernetes secret rotation. If your Postgres supports multiple active passwords (e.g. RDS password grace), rotate without downtime:

1. Set the new password in Postgres (keeping the old one active).
2. Update the `lunar-db` secret.
3. Restart the Hub.
4. Revoke the old password.

If your Postgres doesn't support multiple active passwords, expect \~1 minute of Hub unavailability during the rotation.

### Other secrets

A few secrets don't get their own recipe above:

| Secret                                                                                       | Managed by | Consumed by                                                                             |
| -------------------------------------------------------------------------------------------- | ---------- | --------------------------------------------------------------------------------------- |
| `<release>-grafana-admin`                                                                    | Chart      | The bundled Grafana + the Hub (only in `grafana.mode: chart`)                           |
| `<release>-grafana-db` — read-only `grafana_user` role                                       | Chart      | Grafana datasource — whenever dashboards deploy (bundled **or** your own)               |
| Your own Grafana login (`HUB_GRAFANA_TOKEN`, or `HUB_GRAFANA_USER` + `HUB_GRAFANA_PASSWORD`) | You        | Hub — only with your own Grafana ([Option B/C](/install/lunar-hub/hub-install#grafana)) |
| Hub licence JWT (example `lunar-hub-licence`)                                                | You        | Hub                                                                                     |
| Per-scope runtime secrets (`hub.secrets.{collector,cataloger,policy}.secretName`)            | You        | Hub                                                                                     |

The rotation shape follows the same split as the sections above:

* **Chart-managed** (Grafana admin): `kubectl delete secret <name>` → `helm upgrade` to regenerate → restart the consumer deployment. This does **not** apply to `<release>-grafana-db` — treat it as immutable after install.
* **User-managed** (hub licence JWT, runtime secrets, your own Grafana credentials): update the secret in place (`kubectl apply` or `kubectl create --dry-run=client -o yaml | kubectl apply -f -`) → restart the consumer(s). After rotating your own Grafana credentials, restart the Hub so it picks up the new value — the next `helm upgrade` re-runs the chart's dashboard-deploy Job with it.

To find what consumes an arbitrary secret:

```bash
kubectl -n lunar get deployments -o yaml | grep -B2 '<secret-name>'
```

## Observability

### Logs

The Hub and Operator log in structured JSON by default (`logging.format: json`). Log level defaults to `info`; raise to `debug` temporarily by editing `values.yaml` and running `helm upgrade`.

The Hub and Operator don't ship logs anywhere themselves — they stream to stdout. To get them into your log aggregator, point a cluster-level log shipper (Fluent Bit, Vector, Datadog Agent, etc.) at the `lunar` namespace.

### Telemetry to Earthly

Hub tenant identity and telemetry destinations come from the signed licence JWT, not from `HUB_TENANT_ID`, `HUB_ELASTIC_*`, or `HUB_OTEL_*` environment variables.

The chart handles the mount and `HUB_LICENCE_FILE` wiring for you. Keep (or override) the licence secret reference in values:

```yaml
hub:
  licence:
    secretName: lunar-hub-licence
    secretKey: hub-licence.jwt
    # optional; defaults to /var/run/secrets/lunar/hub-licence.jwt
    # filePath: /var/run/secrets/lunar/hub-licence.jwt
```

Rotation flow:

* Request a new JWT from Earthly.
* Update the Kubernetes secret value (`hub-licence.jwt`) in place.
* The Hub re-validates every 5 minutes and also enforces the exact `exp` boundary.

### Metrics

The Hub exports request-duration histograms (HTTP and gRPC) via OTLP when the active licence includes `telemetry.otel.endpoint` and `telemetry.otel.token`.

If `telemetry.otel` is omitted, the Hub falls back to writing metrics to a local temp file.

Secure (TLS) OTLP is not implemented yet, so OTLP export runs insecure today.

{% hint style="info" %}
The Hub does not export distributed traces today — only metrics. Trace support is on the roadmap.
{% endhint %}

### Aggregating metrics across replicas

With multiple replicas, be deliberate about how dashboards and alerts aggregate the Hub's job-queue metrics. Queue-depth **gauges** (e.g. `river_jobs`) read from the queue tables in Postgres, which are **shared** by every replica, so each replica reports the same global count — aggregate with **`max`** (or `avg`), never `sum`, which multiplies the backlog by the replica count. Per-replica **counters** are the opposite: `river_insert_count_total` / `river_work_count_total` count work done by *each* replica, so `sum(rate(...))` is the correct cluster-wide throughput.

### Diagnostics bundle

The Hub can produce a diagnostics bundle (Postgres queue state, recent error logs, slow-query stats) for support requests. Generate one with:

```bash
lunar hub get-logs
```

This collects `kubectl` logs from the Hub pods plus Postgres telemetry from the Hub, then writes a `lunar-hub-logs-<timestamp>.tar.gz` in the current directory and prints its path. Run it from a machine with `kubectl` access to the Hub's cluster and your Lunar CLI configured to reach the Hub. Common flags:

* `-n`, `--namespace` — Kubernetes namespace to read Hub pods from (defaults to the current kubectl context's namespace).
* `-o`, `--output` — path to write the bundle to (defaults to `lunar-hub-logs-<timestamp>.tar.gz` in the current directory).
* `--tail` — log lines to capture per container (default: `1000`).

The bundle gathers extra Postgres telemetry when the `pg_stat_statements` extension is enabled — without it the bundle still works, just with less query-performance data. To enable the extension:

* **Amazon RDS / Aurora:** add `pg_stat_statements` to `shared_preload_libraries` in the DB parameter group, then reboot the instance.
* **Self-managed:** add `pg_stat_statements` to `shared_preload_libraries` in `postgresql.conf` and restart, then `CREATE EXTENSION pg_stat_statements;` as a superuser.

The Hub does not require this extension — it's only used by the diagnostics path.

## Scaling

The Hub is stateless — all durable state lives in Postgres and S3 — so it scales **horizontally**: raise `hub.replicaCount` and the replicas coordinate through Postgres (leader election, advisory locks) with no peer-to-peer wiring. Size the connection pool against your Postgres `max_connections` first — see [Replicas and the Postgres connection budget](/install/lunar-hub/hub-install#replicas-and-the-postgres-connection-budget). Add a PodDisruptionBudget (`hub.podDisruptionBudget.enabled: true`) so node drains and rollouts keep a quorum.

Vertical scaling still helps a single busy replica: CPU, memory, and the Postgres pool via `hub.db.maxOpenConns` / `hub.db.maxPoolConns`.

Run throughput scales independently — raise `operator.maxConcurrent` and size run pod resources accordingly. See [`operator.scriptContainerSpec*`](https://github.com/earthly/charts/blob/main/README.md#values-reference) in the chart README.

### Leader election and periodic jobs

Exactly one replica is the elected **leader** at a time; it runs the periodic and maintenance jobs (cleanup, reconcile, backfill). Jobs marked `RunOnStart` fire **immediately whenever leadership is (re)acquired** — not just at process boot — so a failover re-runs them on the new leader. Their handlers must be **idempotent**; the current `RunOnStart` jobs already are. Don't add one whose effect isn't safe to repeat on an arbitrary leadership flip.

## Prioritizing core services over script pods

By default the Operator creates script pods (collectors, policies, catalogers) at the cluster's default priority — the same priority as the Hub, Operator, and Grafana. When those script pods share a node with the core services, a burst of runs can drive the node into memory pressure and the kubelet may evict a *core* pod when it should be shedding an ephemeral script instead.

This matters in two setups:

* **Single-namespace installs** — the Operator runs scripts in the same namespace (and usually the same node group) as the Hub, so core and ephemeral workloads compete for the same memory.
* **Shared scratch namespaces or node groups** — scripts land on nodes shared with other workloads, and you want Lunar's ephemeral pods to yield first under contention.

`operator.scriptPodPriorityClassName` (chart `>= 2.3.0`) sets the [PriorityClass](https://kubernetes.io/docs/concepts/scheduling-eviction/pod-priority-preemption/) on every script pod the Operator creates. Point it at a class whose value sits *below* your core services, and the kubelet evicts scripts first under pressure — the scheduler also won't preempt a higher-priority pod to make room for a script.

### Setup

1. **Create the PriorityClass.** The chart references it by name but does not create it.

   ```yaml
   apiVersion: scheduling.k8s.io/v1
   kind: PriorityClass
   metadata:
     name: lunar-script
   value: -10           # negative → below the default (0) your core pods use
   globalDefault: false
   description: Ephemeral Lunar script pods; evicted before core services.
   ```

   ```bash
   kubectl apply -f lunar-script-priorityclass.yaml
   ```
2. **Point the Operator at it** in `values.yaml`:

   ```yaml
   operator:
     scriptPodPriorityClassName: lunar-script
   ```
3. **Upgrade and restart the Operator** — only script pods created *after* the restart inherit the class:

   ```bash
   helm upgrade lunar earthly/lunar -n lunar -f values.yaml
   kubectl -n lunar rollout restart deployment/lunar-operator
   ```

Leave the Hub, Operator, and Grafana on the cluster default priority — don't set a class on them. The gap between their `0` and the script class's negative value is what orders eviction; you lower scripts rather than raise core services. In a shared scratch node group, pick a value below whatever neighbouring workloads run at so Lunar's scripts are the first thing to go.

{% hint style="warning" %}
PriorityClasses are **cluster-scoped**, and a pod that references one that doesn't exist is rejected at admission — so the Operator will fail to launch scripts until the class is applied. Create the class *before* (or in the same change as) the `helm upgrade`, and delete it only once no workload references it.
{% endhint %}

{% hint style="info" %}
Priority orders *who gets evicted first* — it is not a resource cap. Keep your script pod requests/limits set (see [`operator.scriptContainerSpec*`](https://github.com/earthly/charts/blob/main/README.md#values-reference) in the chart README) so a single run can't consume a whole node before the kubelet reacts. Priority decides the order; requests and limits decide the ceiling.
{% endhint %}

## Backup and disaster recovery

Lunar's authoritative state lives in the systems you already back up:

* **Postgres** — use your existing Postgres backup process. Everything the Hub cares about (components, policies, run history, queue state) is here.
* **S3 buckets** — enable object versioning on both buckets and, if compliance requires, cross-region replication. Lost resource archives cause re-building the cache of catalogers, collectors, and policies; lost log archives mean lost history. Neither impedes Hub operation.
* **Hub local scratch** — each Hub pod's `/var/lib/lunar` is an ephemeral `emptyDir` (re-extracted runtimes and cached bundles; the authoritative copy is in S3). It is per-pod and regenerable, so there's nothing to back up.
* **Kubernetes secrets** — keep the GitHub App PEM, DB credentials, auth token, and webhook secret in your organization's secret-management system of record. Losing them means re-provisioning.

## Uninstalling

```bash
helm uninstall lunar --namespace lunar
```

Helm removes all resources the chart created: the Hub and operator deployments, services, ingress, RBAC, and service accounts.

**Not removed automatically:**

* The `lunar` namespace itself (and, if used, the Operator's execution namespace).
* Kubernetes secrets you created manually (e.g. `lunar-db`, `lunar-github-app`, `lunar-hub-licence`).
* Chart-managed secrets (`<release>-auth-token`, `<release>-github-webhook`, `<release>-grafana-admin`). These carry `helm.sh/resource-policy: keep` so a re-install reuses the same values — by design. Delete them explicitly if you want fresh credentials on the next install.
* Your Postgres database, S3 buckets, or their contents.
* The GitHub App. Webhooks previously registered by this Hub are tagged with the Hub's instance ID (`earthly-lunar-<tenant-id-from-licence>` as a URL fragment); they remain on your repos until you remove them manually or install a fresh Hub with the same tenant ID, which will clean up its own stale webhooks on next config pull.

To fully tear down:

```bash
helm uninstall lunar --namespace lunar
kubectl -n lunar delete secret lunar-db lunar-github-app \
  lunar-hub-licence \
  lunar-auth-token lunar-github-webhook lunar-grafana-admin \
  --ignore-not-found
kubectl delete namespace lunar

# Externally:
#   - Drop the Postgres database
#   - Delete or empty the S3 buckets
#   - Uninstall the GitHub App from your organization
```

## Getting help

* [Chart values reference](https://github.com/earthly/charts/blob/main/README.md#values-reference)
* [Chart source](https://github.com/earthly/charts)
* [Lunar source](https://github.com/earthly/lunar)
* For enterprise onboarding or production sizing guidance, [contact the Earthly team](https://earthly.dev/earthly-lunar/demo).


# Lunar Dedicated

Lunar Dedicated is a single-tenant Lunar install that Earthly provisions, operates, and upgrades end-to-end in a dedicated AWS account, reachable only over private networking.

Lunar Dedicated is a fully managed, single-tenant Lunar install. Earthly provisions, operates, and upgrades it end-to-end in an AWS account dedicated to you, and it runs behind private networking that you reach from your own network. It's for teams that want Lunar's guardrails without running the [Hub](/install/lunar-hub/hub-overview) themselves.

This page explains how it works, who it fits, and what Earthly needs from you to stand one up.

{% hint style="info" %}
Lunar Dedicated is a managed offering, distinct from the self-hosted [Lunar Hub](/install/lunar-hub/hub-overview). With Dedicated, Earthly runs the infrastructure; with the self-hosted Hub, you do. The CLI and CI agent are installed and configured the same way in both.
{% endhint %}

## How it works

```mermaid
flowchart LR
  GH["GitHub.com"]

  subgraph Earthly["Earthly (management account)"]
    Mgmt["Deploy and operate tooling"]
  end

  subgraph Dedicated["Your dedicated AWS account"]
    Hub["Lunar Hub<br/>(private)"]
    WH["Webhook listener<br/>(public)"]
    Data[("Postgres · S3 · secrets")]
    Hub --> Data
  end

  subgraph You["Your network"]
    CI["CI runners · Lunar CLI"]
  end

  Mgmt -- "assume-role (IAM)" --> Dedicated
  CI -- "PrivateLink / peering" --> Hub
  GH -- "webhooks" --> WH
```

* **One dedicated AWS account.** The strongest isolation AWS offers — a hard billing and security boundary, with nothing else running inside it.
* **Earthly operates it by assuming an IAM role.** Management is identity-based, so Earthly needs no VPN or peering to run your install.
* **The hub is private.** You reach it over PrivateLink or a joined network (peering / Transit Gateway). The only thing exposed to the public internet is a locked-down GitHub webhook listener.
* **Your data stays in the install.** Authoritative state (Postgres), run inputs and outputs (S3), and secrets all live in your dedicated account and region. Earthly stores none of your data centrally.
* **Earthly manages the whole lifecycle.** Provisioning, version upgrades, patching, and monitoring — on a [maintenance schedule](#maintenance-and-upgrades) you choose.

## Requirements

Lunar Dedicated fits cleanly when the following are true:

| Requirement                              | Detail                                                                                                                                                                                               |
| ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **A network to connect from**            | An AWS VPC you can peer/route to the install, or that can reach a PrivateLink endpoint. The hub is private and never exposed to the public internet.                                                 |
| **Network-reachable CI**                 | The Lunar CLI and [CI agent](/install/lunar-ci-agent/agent-self-hosted) must run where they can reach the private hub — self-hosted runners in your network, or runners with VPN/PrivateLink access. |
| **An outbound allowlist you can permit** | The install needs egress for operational telemetry, container image pulls, and AWS APIs.                                                                                                             |
| **A single region**                      | Each install lives in one AWS region.                                                                                                                                                                |
| **A GitHub App**                         | You create a GitHub App and install it on the org(s) and repositories Lunar should monitor.                                                                                                          |

{% hint style="info" %}
**Coming soon.** Support for GitHub-hosted (public) CI runners, connecting with no existing AWS footprint (site-to-site VPN), and multi-region or high-availability installs are on the roadmap.
{% endhint %}

{% hint style="info" %}
**Need a fully air-gapped, no-egress install?** Dedicated isn't air-gapped — it requires outbound egress to operate. For an air-gapped environment, the [self-hosted Lunar Hub](/install/lunar-hub/hub-overview) is a better fit — [book a demo](https://earthly.dev/book-demo/) to work with us.
{% endhint %}

## How you connect

Every install is private. There are two ways to reach it — Earthly helps you pick based on where your CI runs and whether Lunar needs to reach systems that aren't on the public internet.

|                                        | **PrivateLink** (typical starting point)                      | **Joined network** (peering / Transit Gateway)                          |
| -------------------------------------- | ------------------------------------------------------------- | ----------------------------------------------------------------------- |
| What it is                             | Private one-way "doors" between your network and the install  | Your network and the install's network are routed together              |
| Reaching the hub                       | Through one private endpoint you create                       | Over the joined network, at a private address                           |
| Lunar reaching *your* internal systems | One private door per internal target                          | Anything your routes and firewall rules allow                           |
| Address coordination                   | None — address ranges can overlap freely                      | Address ranges must not overlap                                         |
| Best when                              | You want tight, per-service exposure and minimal coordination | You already run a Transit Gateway, or Lunar needs many internal targets |

{% hint style="info" %}
Lunar's collectors, catalogers, and policies run *inside* your install and reach out to the systems they gather data from — which, for a dedicated customer, may be internal (internal Git, registries, ticketing, internal APIs). That's why connectivity is two-way, and why onboarding asks where your CI runs and what your collectors need to reach.
{% endhint %}

## What Earthly needs from you

These answers shape your install:

| What                    | Why                                                                                                                                        |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| **Region**              | Where the install (and your data) lives                                                                                                    |
| **Connectivity**        | PrivateLink or joined network — plus where your CI runs, and whether collectors need internal systems                                      |
| **DNS**                 | Use Earthly's subdomain (`you.dedicated.earthly.dev`), or delegate a subdomain of your own (e.g. `lunar.yourco.com`) for Earthly to manage |
| **GitHub App**          | You create it and tell us the org(s)/repos it's installed on                                                                               |
| **Sizing**              | Rough org/repo count and CI volume, which maps to a t-shirt size (final sizing confirmed together)                                         |
| **Maintenance windows** | A recurring weekly app window and a monthly infra window, in your timezone                                                                 |
| **Contacts**            | An operations contact (for maintenance notices) and a security contact (for incidents)                                                     |
| **WAF** (optional)      | A web-application firewall on the public webhook listener — off by default, or on with a rule group you provide                            |

## Setup

Earthly handles initial provisioning and setup. This includes account creation, infrastructure provisioning, bringing up your hub, every upgrade (within your chosen window), version selection, monitoring, backup/restore, and clean offboarding.

However, we do need some help from your end.

### What you do

{% stepper %}
{% step %}

### Kickoff

Answer the [questionnaire](#what-earthly-needs-from-you) above. These answers parameterize your install.
{% endstep %}

{% step %}

### Create your GitHub App

Create a GitHub App and install it on your org. Use the [hosted setup tool](https://earthly.dev/lunar/github-app-setup/), or see [manual GitHub App setup](/install/lunar-hub/hub-github-app-manual) if your security review needs every permission laid out first. Hand Earthly the App ID and the org(s) it's installed on.
{% endstep %}

{% step %}

### Go live

Earthly provisions the install and hands you an access token plus your install's Grafana login. Then:

* **Load your workload secrets** (the API keys your catalogers, collectors, and policies need) via the CLI:

  ```bash
  lunar secret set DATADOG_API_KEY --scope collector   # value read from stdin
  ```
* **Complete one network step** — for **PrivateLink**, create the inbound endpoint(s), and coordinate with Earthly; for a **joined network**, accept the network join and add the routes Earthly provides (usually your networking team). If you delegated a subdomain, add the NS records Earthly provides to hand off the zone — Earthly manages every record and the TLS cert under it.
  {% endstep %}
  {% endstepper %}

## Maintenance and upgrades

The Earthly team manages Lunar Hub updates for you; you choose *when* upgrades land. There are two recurring maintenance windows:

* **A weekly app window** for routine Lunar version bumps — a brief hub restart.
* **A monthly infra window** for node patches, add-on updates, and the occasional Kubernetes upgrade.

The hub runs as a single replica, so each upgrade is a brief, scheduled pause while it restarts. Planned windows are excluded from the uptime clock. Critical security patches and forced version deprecations can land out-of-window, with notice.

## Security and data

How a Dedicated install handles isolation, data, secrets, and audit:

* **Exclusive, isolated account.** Your install runs alone in a dedicated AWS account — no other workloads.
* **Your data stays put.** Authoritative state (Postgres), run inputs/outputs (S3), and secrets all live in your dedicated account and region. Earthly stores none of your data centrally.
* **Secrets are encrypted at rest.** Your GitHub App credentials and your workload secrets (API keys) live encrypted inside your install — workload secrets go straight to your hub via the CLI, never through Earthly's central systems.
* **Full audit visibility.** Every action Earthly's roles take is recorded in AWS CloudTrail.
* **Defined data lifecycle.** Run inputs and outputs are retained 30 days. On exit, Earthly returns your data (a portable database dump plus your storage buckets) and **provably deletes** the install by destroying its encryption keys, with the deletion recorded in CloudTrail.
* **Operational telemetry is required.** The install sends metrics and logs back to Earthly so it can monitor the install and meet the SLA. This covers operational data only — never your source code, data, or secrets — and Earthly details exactly what it includes in your security review. If you need telemetry fully off, the [self-hosted Lunar Hub](/install/lunar-hub/hub-overview) is a better fit — [book a demo](https://earthly.dev/book-demo/).

{% hint style="warning" %}
**Custody is audited, not zero-knowledge.** Your secrets are encrypted with a key that lives inside your account and is readable only by the hub. Routine operation can't read your secrets, and any access that could is a deliberate, audited action. It is not a cryptographic guarantee that Earthly *cannot* read them. If you need hard zero-knowledge, the [self-hosted Lunar Hub](/install/lunar-hub/hub-overview) is the better fit — [book a demo](https://earthly.dev/book-demo/).
{% endhint %}

## What's included

* A single-tenant, fully managed Lunar install in a dedicated AWS account
* Private hub access via PrivateLink or a joined network
* A single region per install
* Full lifecycle management: provisioning, upgrades, patching, monitoring, backup/restore, and clean offboarding

## Next steps

Earthly walks through the model live, answers your security team's questions, and maps out what onboarding looks like for your environment.

<a href="https://earthly.dev/book-demo/" class="button primary" data-icon="calendar">Book a demo</a>


# Lunar CI Agent


# Self-Hosted Runners

Install the Lunar CI Agent on self-hosted runners to instrument CI/CD pipelines and collect metadata during builds, tests, scans, and deploys.

The Lunar CI Agent instruments CI/CD pipelines to collect metadata during builds, tests, scans, and deployments. It wraps your existing runner process, monitors execution, and triggers scripts at the right moments.

This page covers the most common setup: **self-hosted runners** (including GitHub Actions self-hosted runners). If you're using GitHub-hosted managed runners, see [Managed Runners](/install/lunar-ci-agent/agent-managed).

## Prerequisites

You need an existing self-hosted runner infrastructure (e.g. GitHub Actions self-hosted runner). Lunar adds instrumentation on top of your existing setup — it does not manage the runner lifecycle.

## Adding to an Existing Runner

{% stepper %}
{% step %}

## Download and install the Lunar CLI

<a href="https://github.com/earthly/lunar-dist/releases/latest" class="button primary" data-icon="download">Download the Lunar CLI</a>

Or via the command line:

```bash
curl -LO https://github.com/earthly/lunar-dist/releases/download/v2.5.0/lunar-linux-amd64
chmod +x lunar-linux-amd64 && sudo mv lunar-linux-amd64 /usr/local/bin/lunar
```

{% hint style="info" %}
Replace the version above with the latest from the [releases page](https://github.com/earthly/lunar-dist/releases/latest).
{% endhint %}

The CI agent is no longer installed separately. The CLI fetches it on first use through your Lunar Hub (see the last step), so the only binary you install here is `lunar`.
{% endstep %}

{% step %}

## Set the required environment variables

```bash
export LUNAR_CI_TYPE=github
export LUNAR_HUB_TOKEN=your_hub_token
export LUNAR_HUB_HOST=your_hub_host
export LUNAR_HUB_GRPC_PORT=your_grpc_port
export LUNAR_HUB_HTTP_PORT=your_http_port
export LUNAR_RUN_CMD=path_to_github_runner_run.sh
```

The agent auto-detects state, cache, and bundle directories based on the running user:

* **Root** → system paths (`/var/lib/lunar`, `/var/cache/lunar/git-repos`, `/var/tmp/lunar/...`).
* **Non-root** → user paths under `$HOME/.lunar/`.

Override any of them by setting `LUNAR_STATE_DIR`, `LUNAR_GIT_CACHE_DIR`, `LUNAR_BUNDLE_DIR`, `LUNAR_SNIPPET_DIR`, `LUNAR_SCRIPT_LOG_DIR`, `LUNAR_BIN_DIR`, or `LUNAR_LOCK_DIR` — useful for read-only rootfs runners, systemd units with `ProtectHome=`, or non-standard home directories.
{% endstep %}

{% step %}

## Run the agent

```bash
lunar ci-tracer run
```

On first run, `lunar ci-tracer run` downloads the agent through your Hub, verifies it, caches it under `LUNAR_BIN_DIR` (`$HOME/.lunar/bin` by default), and then runs it. Subsequent runs reuse the cached binary and skip the download.

{% hint style="info" %}
The Hub must be reachable the first time you run this on a host (or whenever the cache is empty). Once the agent binary is cached, the agent only needs the Hub for its normal runtime traffic.
{% endhint %}

{% hint style="info" %}
**Upgrading from an earlier agent install?** Previous versions shipped a separate `lunar-ci-agent` binary that you downloaded and started yourself. That separate download no longer exists — install only the `lunar` CLI and start the agent with `lunar ci-tracer run`, which fetches the agent for you.
{% endhint %}

{% hint style="info" %}
For production usage, run `lunar ci-tracer run` under a process supervisor such as `systemd` so it restarts automatically on failure. See [Systemd Configuration](/install/lunar-ci-agent/systemd) for an example unit file.
{% endhint %}
{% endstep %}
{% endstepper %}

## Using a Custom Runner Image

If your runners are containerized, install the `lunar` CLI inside a `Dockerfile` and make `lunar ci-tracer run` the entrypoint. The agent binary is fetched on first boot through your Hub — the build host does not need Hub connectivity:

{% code title="Dockerfile" %}

```dockerfile
FROM my-custom-runner-image:ubuntu-slim

ENV LUNAR_HUB_HOST=my.cool.host.com
ENV LUNAR_HUB_GRPC_PORT=443
ENV LUNAR_HUB_HTTP_PORT=443
ENV LUNAR_CI_TYPE=github
ENV LUNAR_RUN_CMD=/home/ubuntu/actions-runner/run.sh

# Replace /home/ubuntu with your runner user's home directory
ENV LUNAR_STATE_DIR=/home/ubuntu/.lunar/state
ENV LUNAR_GIT_CACHE_DIR=/home/ubuntu/.lunar/git-repos
ENV LUNAR_BUNDLE_DIR=/home/ubuntu/.lunar/bundles
ENV LUNAR_SNIPPET_DIR=/home/ubuntu/.lunar/snippets
ENV LUNAR_SCRIPT_LOG_DIR=/home/ubuntu/.lunar/scripts
ENV LUNAR_BIN_DIR=/home/ubuntu/.lunar/bin
ENV LUNAR_LOCK_DIR=/home/ubuntu/.lunar/lock

RUN curl -LO https://github.com/earthly/lunar-dist/releases/download/v2.5.0/lunar-linux-amd64 && \
    chmod +x lunar-linux-amd64 && mv lunar-linux-amd64 /usr/local/bin/lunar

ENTRYPOINT ["lunar", "ci-tracer", "run"]
```

{% endcode %}

On the first container boot, `lunar ci-tracer run` accesses your Lunar Hub to download and cache the agent under `LUNAR_BIN_DIR`, then runs it. Subsequent boots reuse the cached binary **only if `LUNAR_BIN_DIR` is persisted** across container restarts (e.g. a mounted volume) — otherwise each fresh container downloads the agent again on first boot.

{% hint style="info" %}
**Optional: warm the cache at build time.** Run `lunar ci-tracer install` during the build to download and cache the agent without starting it. This requires the build host to reach the Hub. Pass the Hub token as a build secret so it never lands in an image layer:

```dockerfile
# syntax=docker/dockerfile:1
RUN --mount=type=secret,id=lunar_hub_token \
    LUNAR_HUB_TOKEN="$(cat /run/secrets/lunar_hub_token)" \
    lunar ci-tracer install
```

```bash
docker build --secret id=lunar_hub_token,env=LUNAR_HUB_TOKEN .
```

The agent is cached in `LUNAR_BIN_DIR`, so it's baked into the image layer. Make sure `LUNAR_BIN_DIR` points at a path that stays in the image.
{% endhint %}

{% hint style="info" %}
The GitHub Actions runner may not work correctly when run as root. See GitHub's [self-hosted runner documentation](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners) for details.
{% endhint %}

Bake everything into the image except secrets. Pass the Hub token at runtime:

```bash
docker run -e LUNAR_HUB_TOKEN=$(vault read /lunar/hub/token) runner-image:latest
```

Or in Kubernetes, reference a Secret:

```yaml
env:
  - name: LUNAR_HUB_TOKEN
    valueFrom:
      secretKeyRef:
        name: lunar-hub
        key: token
```

For all environment variable details, see the [Configuration Reference](/install/lunar-ci-agent/agent-config).

***

## Running `sudo` and setuid binaries in traced workflows

To trace command execution, the agent installs a seccomp filter on the runner process. Linux only permits installing that filter when the process either holds the `CAP_SYS_ADMIN` capability or has the `no_new_privs` flag set. When the runner does **not** have `CAP_SYS_ADMIN`, the agent falls back to setting `no_new_privs`, which makes the kernel ignore the setuid bit. Non-root processes can then no longer escalate through setuid-root binaries — `sudo`, for example, refuses to run:

```
sudo: The "no new privileges" flag is set, which prevents sudo from running as root.
```

`no_new_privs` is inherited by every child process and can never be cleared, so this affects any unprivileged user your workflow switches to, not just the top-level runner user.

To keep `sudo` and other setuid binaries working under tracing, run the runner **as root** in a container that adds `CAP_SYS_ADMIN`. The agent detects the capability on the runner process and skips `no_new_privs`, installing the seccomp filter through the capability instead.

{% code title="docker" %}

```bash
docker run --cap-add SYS_ADMIN runner-image:latest
```

{% endcode %}

{% code title="kubernetes" %}

```yaml
securityContext:
  capabilities:
    add: ["SYS_ADMIN"]
```

{% endcode %}

{% hint style="info" %}
The runner must run **as root** for this to take effect: a capability added to a container is only held by a root (uid 0) process. A non-root runner process does not gain `CAP_SYS_ADMIN` from the container, so the agent still falls back to `no_new_privs` there.
{% endhint %}

{% hint style="warning" %}
`CAP_SYS_ADMIN` is a broad, privileged capability — grant it only when your workflows genuinely need setuid escalation (e.g. `sudo`) under tracing. On [GitHub-hosted managed runners](/install/lunar-ci-agent/agent-managed) the job runs as the non-root `runner` user with no way to hold `CAP_SYS_ADMIN` — its passwordless `sudo` is a sudoers grant, not a capability — so setuid escalation is not supported while tracing there.
{% endhint %}

***

## Next Steps

Once installed, you can begin configuring:

* [Collectors](/configuration/lunar-config/collectors) to gather SDLC data
* [Policies](/configuration/lunar-config/policies) to enforce standards
* [Domains and Components](/docs/key-concepts) to organize your software landscape

For questions or enterprise onboarding:

<a href="https://earthly.dev/earthly-lunar/demo" class="button secondary" data-icon="envelope">Contact the Earthly team</a>


# Managed Runners

Use the Lunar CI Tracer action to instrument GitHub-hosted and self-hosted GitHub Actions runners with the Lunar CI Agent.

The [`earthly/lunar-ci-tracer`](https://github.com/earthly/lunar-ci-tracer) action is the easiest way to add the Lunar CI Agent to your GitHub Actions workflows. It works with both **GitHub-hosted** and **self-hosted** runners.

For **GitHub-hosted runners** (managed runners), this action is the only installation method — you cannot modify the runner startup process.

For **self-hosted runners**, you can either use this action or configure the agent to [wrap the runner's `run.sh` command](/install/lunar-ci-agent/agent-self-hosted) directly, which avoids adding a step to every job.

## Setup

Add the Lunar CI Tracer action as an early step in your workflow jobs:

```yaml
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Run Lunar CI Tracer
        id: lunar
        uses: earthly/lunar-ci-tracer@<latest-tag>
        env:
          LUNAR_HUB_TOKEN: ${{ secrets.LUNAR_HUB_TOKEN }}
          LUNAR_HUB_HOST: your_hub_host

      - uses: actions/checkout@v5
      # ... rest of your workflow
```

The action installs the `lunar` CLI for you (no pre-install step needed) and runs `lunar ci-tracer run`, which fetches the agent through your Lunar Hub on first use, verifies it, and attaches it to the job process. All subsequent steps in the job are automatically instrumented. Your Hub must be reachable when the action starts.

## How It Works

The action runs as a step in a job. It downloads the `lunar` CLI, then runs `lunar ci-tracer run`, which fetches the agent through your Hub on first use and execs it. The agent attaches to the current shell process via ptrace and traces all commands executed by subsequent steps. The agent exits automatically when the job completes.

The same [configuration reference](/install/lunar-ci-agent/agent-config) applies. The only difference is that `LUNAR_RUN_CMD` is not needed — the action handles process supervision internally.

{% hint style="warning" %}
For self-hosted runners that require the use of `sudo` in workflows, the `CAP_SYS_ADMIN` capability should be provided — see [Running `sudo` and setuid binaries in traced workflows](/install/lunar-ci-agent/agent-self-hosted).
{% endhint %}

## Failure Handling

Agent installation failures (CLI download, agent download through the Hub, agent startup) are gated by `LUNAR_STRICT_MODE`:

* `LUNAR_STRICT_MODE=true` — the step **fails** with the error output.
* Unset or `false` (default) — the action emits an `::error::` annotation plus a warning, sets the `agent-installed` output to `false`, and the step **succeeds** so the rest of the job continues uninstrumented.

The action exposes an `agent-installed` output (`'true'` / `'false'`) so downstream steps can branch on whether instrumentation is active:

```yaml
- name: Run checks that need the tracer
  if: steps.lunar.outputs.agent-installed == 'true'
  run: ./run-traced-tests.sh
```


# Configuration Reference

Reference for all environment variables that configure the Lunar CI Agent, including required settings, Docker options, and state directories.

## Required

| Variable              | Description                                                                                                                                                                                                                                              |
| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `LUNAR_HUB_TOKEN`     | Auth token for your Hub installation.                                                                                                                                                                                                                    |
| `LUNAR_HUB_HOST`      | Hostname of your Hub installation. Must be reachable from the runner.                                                                                                                                                                                    |
| `LUNAR_HUB_GRPC_PORT` | Hub's gRPC port. Used for configuration sync, collection results, and GitHub token resolution.                                                                                                                                                           |
| `LUNAR_HUB_HTTP_PORT` | Hub's HTTP port. Used for log uploads and script downloads.                                                                                                                                                                                              |
| `LUNAR_CI_TYPE`       | CI platform type: `github` (GitHub Actions) or `buildkite`. See [Buildkite](/install/lunar-ci-agent/buildkite) for Buildkite setup.                                                                                                                      |
| `LUNAR_RUN_CMD`       | Command to start the runner process. For GitHub Actions self-hosted runners, this is the path to `run.sh` (e.g. `/home/ubuntu/actions-runner/run.sh`). Not needed when using the [managed runners](/install/lunar-ci-agent/agent-managed) GitHub Action. |

## Optional

| Variable              | Default      | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| --------------------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `LUNAR_HUB_INSECURE`  | `false`      | Set to `true` when connecting to a Hub instance without TLS.                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `LUNAR_UPDATE_PERIOD` | `15s`        | How often the agent polls Hub for configuration updates.                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| `LUNAR_LOG_LEVEL`     | `info`       | Log verbosity. Set to `debug` for troubleshooting.                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `LUNAR_GITHUB_HOST`   | `github.com` | The GitHub host whose components this agent collects for (the `<host>` in `<host>/<org>/<repo>` component names). Auto-detected from `GITHUB_SERVER_URL` when the agent runs as a GitHub Actions step (including via the Lunar CI Action), so **GitHub Enterprise Server "just works"** there. Set it explicitly only when the agent can't see `GITHUB_SERVER_URL` — e.g. when wrapping a self-hosted runner's `run.sh` directly — or to override. (Replaces the former `LUNAR_GIT_BASE_URL`.) |

## Advanced

### Docker

These options are for environments where collectors or policies run in Docker containers (e.g. private registries, custom networks, or sidecar Docker daemons).

| Variable                     | Default  | Description                                                                |
| ---------------------------- | -------- | -------------------------------------------------------------------------- |
| `LUNAR_DOCKER_REGISTRY_USER` | *(none)* | Username for a private Docker registry containing collector/policy images. |
| `LUNAR_DOCKER_REGISTRY_PASS` | *(none)* | Password for a private Docker registry containing collector/policy images. |
| `LUNAR_DOCKER_NETWORK`       | *(none)* | Docker network for script container execution.                             |

### State Directories

The agent uses several directories for state, caching, and execution. The defaults listed below are the root-user paths. When running as a non-root user, the agent automatically falls back to `$HOME/.lunar/` paths (e.g. `$HOME/.lunar/state` instead of `/var/lib/lunar`), so manual overrides are usually unnecessary. You can still set these variables explicitly if you need non-standard locations.

| Variable               | Default                      | Description                                           |
| ---------------------- | ---------------------------- | ----------------------------------------------------- |
| `LUNAR_STATE_DIR`      | `/var/lib/lunar`             | Script execution state and embedded runtimes.         |
| `LUNAR_GIT_CACHE_DIR`  | `/var/cache/lunar/git-repos` | Cached git repository clones.                         |
| `LUNAR_BUNDLE_DIR`     | `/var/tmp/lunar/bundles`     | Component JSON bundles for policy evaluation.         |
| `LUNAR_SNIPPET_DIR`    | `/var/lib/lunar/snippets`    | Downloaded script code from Hub.                      |
| `LUNAR_SCRIPT_LOG_DIR` | `/var/tmp/lunar/scripts`     | Script execution logs (uploaded to Hub).              |
| `LUNAR_BIN_DIR`        | `/usr/lib/lunar`             | Embedded runtime binaries.                            |
| `LUNAR_LOCK_DIR`       | `/run/lock/lunar`            | Installation lock files to prevent parallel installs. |


# Buildkite

Run Lunar on Buildkite — trace builds with a Buildkite agent command hook and deliver build status to Lunar Hub with a Notification Service webhook.

Lunar instruments Buildkite with two pieces:

1. A Buildkite agent **`command` hook** (`command` is Buildkite's agent-hook type) that wraps each command with Lunar tracing, so collectors run and their data is attributed to your components.
2. A Buildkite **Notification Service webhook** to Lunar Hub, so Lunar Hub knows a build ran and CI-dependent policies wait for it instead of finalizing early.

{% hint style="info" %}
Lunar listens to **two** webhooks that drive different things: your **source-control (SCM)** webhook carries source events (push / pull request) and owns component identity, while the **Buildkite** webhook reports that a build ran so CI-dependent policies wait for it. Buildkite is the CI layer; your SCM is the source host — Buildkite tracing works against the source host your components are already registered with.
{% endhint %}

## Setup

### Step 1 — Agent command hook (tracing)

On your Buildkite agent, add a `command` hook (in the agent's `hooks/` directory) that exports the Lunar configuration and runs `lunar ci-tracer run` to trace the build's command:

```bash
# hooks/command
set -e

export LUNAR_CI_TYPE=buildkite
export LUNAR_RUN_CMD="bash -c \"$BUILDKITE_COMMAND\""
export LUNAR_HUB_HOST=hub.example.com
export LUNAR_HUB_TOKEN="$LUNAR_HUB_TOKEN"

lunar ci-tracer run
```

The `lunar` CLI installs the CI tracer on first use and caches it, so no separate agent binary needs to be installed on the runner. Source-control credentials (used for changed-file inference) are fetched from Lunar Hub, so no GitHub token is needed on the runner.

The Hub host and ports resolve in order: explicit `LUNAR_HUB_*` env vars, then the `hub:` block of a `lunar-config.yml` the runner can read, then a default of `443` for both gRPC and HTTP. Set `LUNAR_HUB_GRPC_PORT` / `LUNAR_HUB_HTTP_PORT` only for a Hub on non-default ports that isn't declared in a manifest. See the [Configuration Reference](/install/lunar-ci-agent/agent-config) for the full set of `LUNAR_*` variables.

### Step 2 — Notification Service webhook (build status)

Without the webhook, Lunar Hub never learns a Buildkite build happened, so CI-dependent policies would finalize before the build's data lands. In Buildkite, go to **Organization Settings → Notification Services → Add → Webhook** (the service is org-level, not per-pipeline) and configure it so Lunar Hub records each build:

* **URL**: `https://<your-hub-host>/webhooks/buildkite`
* **Token**: set a shared token and configure Lunar Hub with `HUB_BUILDKITE_WEBHOOK_TOKEN` to the same value. Lunar Hub verifies the `X-Buildkite-Token` header on every request; an unset token rejects all requests.
* **Events**: `build.scheduled`, `build.running`, `build.finished`.

{% hint style="warning" %}
The webhook endpoint must be reachable from Buildkite's servers, so it's exposed to the public internet — the same posture as your SCM webhook endpoint. Keep it behind TLS and rely on the shared `X-Buildkite-Token` for authentication; Lunar Hub rejects every request whose token doesn't match.
{% endhint %}

Lunar Hub creates a workflow-run record per build, associates it with the build's commit (and pull request, for PR builds), and re-evaluates CI-dependent policies when a build finishes.

## Component attribution

Lunar reads the standard `BUILDKITE_*` environment to attribute collected data to components. In a monorepo, set one of the following in your pipeline so a build maps to the right subdirectory component (see [Components](/configuration/lunar-config/components)):

* `LUNAR_COMPONENT` — name the component(s) explicitly (comma-separated).
* `ciPipelines` on the component — matches the Buildkite **pipeline name**.
* `LUNAR_COMPONENT_INFER=true` — infer from changed paths (PR builds) or the working directory.

## Scope and limitations

* **Command-level** collection is supported (`ci-before/after-command` hooks). Job- and step-level scopes are not yet collected by Lunar.
* **Changed-path** component inference resolves for **pull-request** builds; push builds fall back to working-directory inference (Buildkite provides no prior-commit SHA to diff against).
* **One CI provider per repo** — the first Buildkite build marks the repo as Buildkite-driven, and any other CI provider's doneness probe then skips it. Running two CI providers against the same repo isn't supported.


# Systemd

Run Lunar as a systemd service on Linux hosts with a sample unit file for reliable startup, restarts, and environment configuration.

Systemd is a system and service manager for Linux that provides a standardized way to define and control how services start, stop, and behave on boot.

For Lunar, systemd may be used to ensure that the application is reliably managed, automatically started at boot, and cleanly shut down or reloaded when needed.

Here's a simple systemd unit file that will run `lunar` in a resilient way.

{% code title="lunar.service" %}

```ini
[Unit]
Description=Lunar CI Agent
After=network.target

[Service]
Type=simple
WorkingDirectory=/home/ubuntu
ExecStart=/usr/local/bin/lunar ci-tracer run
EnvironmentFile=/etc/lunar.env
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
```

{% endcode %}


# Sync Config GitHub Action

Use the Lunar sync-config GitHub Action to push your config repo to Lunar Hub on every push.

The [`earthly/lunar-actions/sync-config`](https://github.com/earthly/lunar-actions/tree/main/sync-config) action pushes the latest config (manifest + snippets) from your config repo into Lunar Hub. Run it on every push to your config repo's primary branch so the hub stays in sync.

{% hint style="info" %}
**First time setting up a config repo?** Fork [`earthly/lunar-config-template`](https://github.com/earthly/lunar-config-template) — it ships with this workflow pre-wired so you only need to set the `LUNAR_HUB_TOKEN` secret and push.
{% endhint %}

## How it works

`sync-config` is a thin wrapper around the [`lunar hub pull`](/docs/lunar-cli#lunar-hub-pull) CLI command. The action's inputs map 1:1 to the equivalent CLI flags — see the [CLI reference](/docs/lunar-cli) for the full semantics of each.

## Example

```yaml
name: Sync Lunar Config

on:
  push:
    branches: [main]

jobs:
  sync:
    runs-on: ubuntu-latest
    steps:
      - uses: earthly/lunar-actions/sync-config@main
        with:
          manifest-url: github://my-org/my-config-repo@main
          hub-token: ${{ secrets.LUNAR_HUB_TOKEN }}
          hub-host: hub.example.com
          lunar-version: v2.5.0
          rerun-code-collectors: "true"
```

## Inputs

| Input                   | Required | Default | CLI equivalent                                                                                                  |
| ----------------------- | -------- | ------- | --------------------------------------------------------------------------------------------------------------- |
| `manifest-url`          | yes      | —       | `<repo>` positional argument (`github://<org>/<repo>@<branch>`)                                                 |
| `hub-token`             | yes      | —       | `LUNAR_HUB_TOKEN` env var                                                                                       |
| `hub-host`              | yes      | —       | `LUNAR_HUB_HOST` env var                                                                                        |
| `lunar-version`         | yes      | —       | Selects the [`lunar-dist`](https://github.com/earthly/lunar-dist/tags) release to download                      |
| `hub-grpc-port`         | no       | `443`   | `LUNAR_HUB_GRPC_PORT` env var                                                                                   |
| `hub-http-port`         | no       | `443`   | `LUNAR_HUB_HTTP_PORT` env var                                                                                   |
| `rerun-code-collectors` | no       | `false` | `--rerun-code-collectors` / `-l`                                                                                |
| `include-pr-commits`    | no       | `false` | `--include-pr-commits`                                                                                          |
| `pr-max-age-days`       | no       | `5`     | `--pr-max-age-days`                                                                                             |
| `rerun-catalogers`      | no       | `false` | `--rerun-catalogers` / `-t` (requires lunar `v1.1.2+` — see [`lunar hub pull`](/docs/lunar-cli#lunar-hub-pull)) |
| `log-level`             | no       | `debug` | `LUNAR_LOG_LEVEL` env var                                                                                       |

{% hint style="info" %}
By default, pulling the config does **not** rerun catalogers or code collectors. Set `rerun-catalogers: "true"` or `rerun-code-collectors: "true"` to opt in. Per-component catalogers (`component-repo`, `component-cron`) are unaffected by these flags and continue to run on their own hooks.
{% endhint %}

## Versioning

Pin to `@main` for the latest, or to a release tag for stability. The `lunar-version` input selects the CLI version that gets downloaded; check the [`lunar-dist` releases](https://github.com/earthly/lunar-dist/tags) for the latest stable tag.


# AI Skills

Install AI agent skills for Claude Code, Codex, Cursor, and 50+ other agents to help build Lunar collectors, policies, and SQL queries.

The [earthly/skills](https://github.com/earthly/skills) repository provides AI agent skills for working with Lunar. These skills enable AI assistants to help you build custom plugins, edit your `lunar-config.yml`, and query Lunar's data model.

## Available Skills

| Skill                                                                                 | Description                                                                                                         |
| ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| [lunar-collector](https://github.com/earthly/skills/tree/main/skills/lunar-collector) | Create Lunar collector plugins (Bash scripts) that gather SDLC metadata                                             |
| [lunar-cataloger](https://github.com/earthly/skills/tree/main/skills/lunar-cataloger) | Create Lunar cataloger plugins (Bash scripts) that build the software catalog from external systems or repo signals |
| [lunar-policy](https://github.com/earthly/skills/tree/main/skills/lunar-policy)       | Create Lunar policy plugins (Python scripts) that enforce engineering standards                                     |
| [lunar-config](https://github.com/earthly/skills/tree/main/skills/lunar-config)       | Edit `lunar-config.yml` — wire together components, domains, collectors, policies, catalogers, and initiatives      |
| [lunar-sql](https://github.com/earthly/skills/tree/main/skills/lunar-sql)             | Craft SQL queries against Lunar's data model (components, checks, policies, domains, PRs)                           |

## Installation

Install with the [`skills`](https://github.com/vercel-labs/skills) CLI:

```bash
# Install all Lunar skills globally
npx skills add earthly/skills -g

# Or install a specific skill globally
npx skills add earthly/skills -g --skill lunar-policy
```

The CLI auto-detects which coding agents you have installed (Claude Code, Codex, Cursor, and 50+ more) and copies the skills to the right location. See the [`skills` CLI docs](https://github.com/vercel-labs/skills) for more commands like `list`, `update`, and `remove`.

Drop `-g` to install into the current project's `.claude/skills/` (or equivalent) instead — useful if you want to commit the skills into a shared team repo.

## Usage

These skills are designed to be used with AI agents that support the Claude/Codex skill format. Each skill contains:

* `SKILL.md` - Main instructions and quick-start guide
* `references/` - Curated documentation for the AI to consult as needed

Once installed, your AI assistant will automatically detect and use these skills when you ask it to build a Lunar plugin, edit your config, or query the SQL API.

## Related Resources

* [Bash SDK](/plugin-sdks/bash-sdk) - Manual reference for building collectors and catalogers
* [Python SDK](/plugin-sdks/python-sdk) - Manual reference for building policies
* [lunar-config.yml](/configuration/lunar-config) - Manual reference for the configuration file
* [SQL API](/sql-api/sql-api) - Manual reference for querying Lunar data
* [lunar-lib Repository](https://github.com/earthly/lunar-lib) - Reference collectors, catalogers, and policies


# Learn the basics

Walk through your first Lunar setup — populating components, defining policies, and running checks against your repositories.

This guide will help you understand the basic concepts of Lunar and get started with monitoring your engineering practices.

## Prerequisites

Before you begin, make sure you have:

1. Installed Lunar following the [installation guide](/install)
2. Access to your code repositories
3. Basic understanding of your CI/CD pipeline setup

## Basic Concepts

Lunar operates on a few key concepts:

1. **Components**: These are your software projects (services, libraries, repositories)
2. **Collectors**: These gather information about your components
3. **Policies**: These define rules and standards for your components
4. **Checks**: These are the results of policy evaluations

## Your First Lunar Setup

{% stepper %}
{% step %}

## Populate your Lunar configuration

Start by creating a `lunar-config.yml` file in your project root:

{% code title="lunar-config.yml" %}

```yaml
version: 0

hub:
  host: <host>
  grpcPort: <grpc-port>
  httpPort: <http-port>

# Use the official Lunar image for running scripts in containers
default_image: earthly/lunar-scripts:1.0.0

domains:
  team1:
    description: Main organization domain

components:
  github.com/my-org/my-service:
    owner: jane@example.com
    domain: team1

collectors:
  - name: readme-lines
    runBash: |-
      if [ -f ./README.md ]; then
        lunar collect -j \
          "repo.readme_exists" true \
          "repo.readme_num_lines" "$(wc -l < ./README.md)"
      else
        lunar collect -j "repo.readme_exists" false
      fi
    hook:
      type: code

policies: []
```

{% endcode %}

The `default_image` setting runs all collectors and policies inside Docker containers using the official `earthly/lunar-scripts` image. This image includes Python, Bash, the `lunar` CLI, and the `lunar-policy` package pre-installed. For more details on image configuration, see [Images](/configuration/lunar-config/images).

You will need to replace `github.com/my-org/my-service` with a real repository you want to monitor.

{% hint style="info" %}
Lunar can also auto-discover components from external systems like GitHub, Backstage, and other sources using catalogers. See the [catalogers documentation](/configuration/lunar-config/catalogers) for details on setting up automated component discovery. For now, this example will focus on manually declared components.
{% endhint %}

Commit this code to a new repository called `lunar`. To apply this configuration, run the following command:

```bash
lunar hub pull github://my-org/lunar@main
```

You should be able to see the new domain, the new component, and its component JSON being populated in the Lunar UI.
{% endstep %}

{% step %}

## Define your first policy

Add a policy to check your component:

{% code title="lunar-config.yml" %}

```yaml
policies:
  - name: readme
    description: "README.md standards"
    on: ["domain:team1"]
    runPython: |-
      from lunar_policy import Check
      with Check("readme-exists", "Repository should have a README.md file") as c:
        c.assert_true(c.get_value(".repo.readme_exists"), "README.md file not found")
```

{% endcode %}

Since we're using the official `earthly/lunar-scripts` image (via `default_image`), the [Lunar Policy SDK](/plugin-sdks/python-sdk/policy) is already pre-installed—no `requirements.txt` needed.

Commit the code, and apply the new configuration:

```bash
lunar hub pull github://my-org/lunar@main
```

You should be able to see the new policy, and the checks being populated for this component in the Lunar UI.

Congratulations! You've just set up your first Lunar collector and policy.
{% endstep %}
{% endstepper %}

## See also

1. Learn more about [key concepts](/docs/key-concepts)
2. Explore [configuration options](/configuration/lunar-config)
3. Browse [200+ pre-built guardrails](https://earthly.dev/lunar/guardrails/) and [50+ integrations](https://earthly.dev/lunar/integrations/) instead of writing everything from scratch
4. Install [AI skills](/install/skills) for Claude Code, Codex, or Cursor to help build collectors and policies


# Key concepts

Overview of Lunar's core concepts — domains, components, collectors, catalogers, policies, and checks — and how they fit together.

This page describes the most important concepts in Lunar.

At a high-level, your software is organized into components (services or libraries), which often map to code repositories, or subdirectories in monorepos. The engineering processes of each software component in your system is instrumented by collectors. The collectors gather the information into a component JSON. Policies are then defined to create guardrails around your engineering practices. The policies are evaluated against the component JSON and create checks that have a pass or fail outcome.

## Domains

Domains are used to group related components together. Domains are hierarchical and can contain other domains.

Domains are useful to model complex inter-related systems, like multi-service applications, or hierarchical teams in large organizations.

## Components

Components are the building blocks of Lunar. They represent the entities that Lunar monitors. Components are independent pieces of software, such as microservices, or libraries.

Components usually correspond to git repositories, but they may also correspond to subdirectories in a monorepo.

## Component JSON

The Component JSON is an object that contains collected SDLC (software development lifecycle) metadata about a component. Collectors associated with a component contribute to the component JSON metadata via "metadata deltas". Together, all these deltas form a complete picture of the component in a merged JSON representation.

Examples of information that can be collected include:

* Project configuration information
* Project ownership information
* Code access control and branch protection configuration
* Detailed build and test status, including coverage reports
* Security scan results
* Software bill of materials (SBOM)
* Software composition analysis (SCA) results
* Infrastructure information
* Production deliverables and their metadata

For more information about the component JSON, see the [Component JSON](/docs/component-json) page.

## Collectors

Collectors are SDLC (software development lifecycle) instrumentation configurations. They are used to collect live information from the SDLC to associate with individual components.

Collectors are of different **types** that vary depending on the **hook** used. They can be based on code, cron schedules, or CI/CD hooks.

Once a hook is triggered, the collector **executes its custom logic**, which collects data from the SDLC. The data is later on merged to form the component metadata JSON.

Collectors may execute in different **contexts**, depending on the type - for example, in the context of a CI pipeline, or standalone in an ephemeral runner reacting to code changes. The Lunar CI Agent and the Lunar Runner are some of the pieces of the Lunar framework that facilitate the triggering and executions of collectors in such contexts.

Lunar ships with [50+ pre-built integrations](https://earthly.dev/lunar/integrations/) for tools like GitHub, Kubernetes, Docker, Codecov, Snyk, and more. To read more about how to configure collectors, see the [Collectors](/configuration/lunar-config/collectors) page.

### Earthly Lunar CI Agent

The Earthly Lunar CI Agent allows you to trigger and execute collectors related to CI/CD pipelines running on a CI runner. The agent is installed on the self-hosted runner of the CI (e.g. GitHub Actions, GitLab CI, Jenkins, Buildkite) and is responsible for instrumenting the pipelines executing on that host and running arbitrary logic defined by relevant collectors.

Installation on managed CI runners is also possible, although it requires additional setup in each project via the definition of the CI pipelines (typically the YAML definition). Earthly Lunar comes out of the box with policies to verify that the agent is installed properly in such situations.

The Lunar CI Agent is able to instrument individual processes executing within CI pipelines and is able to surgically inject custom logic safely in a way that does not interfere with the CI pipeline's normal execution. Special hooks are defined to trigger collectors to execute in the context of specific processes, no matter how deep in the process tree hierarchy these appear.

### Earthly Lunar Runner

The Earthly Lunar Runner is a standalone ephemeral runner that can be used to execute code-based or cron-based collectors.

You can think of the Lunar Runner as "global CI" where the logic is defined centrally via the Lunar configuration, and not specified within each project's CI pipeline definition. This allows a central platform team to collect information about the entire organization's codebases, running arbitrary logic, and using arbitrary scanners, tools, and scripts, without needing to modify each project's CI pipeline or needing to request permission from individual app teams.

## Policies

Policies are used to define the rules that Lunar uses to evaluate the health of components. Policies receive the component metadata as input and return checks. The checks can then be reported to a scorecarding system or monitored via the Lunar UI.

Some policies may help provide immediate feedback to developers via the PR status. In such a situation, Lunar typically shows up as a commit status check entry in the PR (similarly to how a CI might report status in a PR), showing the health of the component based on the policies.

Similarly, policies can also be used to block deployments based on the health of the component. This is useful to prevent deploying components that do not meet certain engineering standards of the organization.

Lunar includes [200+ pre-built guardrails](https://earthly.dev/lunar/guardrails/) covering testing, security, compliance, and operational readiness. For more information on how to configure policies, see the [Policies](/configuration/lunar-config/policies) page.

## Checks

Checks are the results of evaluating a policy against a component. You can think of Lunar checks as individual line item policies that appear in the final scorecard of a component.

Checks might have different outcomes, such as:

* `pass` - the policy check passes successfully
* `fail` - the policy check is failing
* `pending` - the data required by the policy check is still pending (e.g. a collector has not finished executing yet)
* `skipped` - the policy check was skipped

## Catalogers

Catalogers are used to synchronize component and/or domain data from external systems, such as other code repositories, databases, REST APIs, or IDPs, such as Backstage. Browse the [available integrations](https://earthly.dev/lunar/integrations/) for pre-built catalogers.

To read more about how to configure catalogers, see the [Catalogers](/configuration/lunar-config/catalogers) page.

## Catalog JSON

The Catalog JSON is an object that contains the component and domain data collected by the catalogers.

To read more about the Catalog JSON, see the [Catalog JSON](/docs/catalog-json) page.


# Component JSON

How the Component JSON aggregates SDLC metadata deltas from collectors into a single object that policies evaluate against.

* Type: `JSON`

The Component JSON is a JSON object that contains SDLC metadata associated with a component. The JSON object is formed by collecting metadata deltas via the different collectors.

The Component JSON is meant to represent the point-in-time state of a component. The JSON object is stored in the database and is used to evaluate the health of the component via policies.

The structure of the component JSON is arbitrary. The JSON object acts as an interface layer between the collectors and the policies. It can have any structure, as needed to convey the information used by the policies, or to track the state of a certain metric or characteristic over time for a given component or set of components.

To view the component JSON for a given component, you can use the `lunar component get-json` command:

```bash
lunar component get-json github.com/my-org/my-repo
```

To read more about how to configure the collectors that contribute deltas to the component JSON, see the [collectors](/configuration/lunar-config/collectors) page.

To read more about how to query the component JSONs via the SQL API see the [components view](/sql-api/views/components) page.


# Catalog JSON

Structure and precedence rules for the Catalog JSON, which merges domains and components from catalogers, lunar.yml, and lunar-config.yml.

* Type: `JSON`
* Form:

  ```json
  {
    "domains": {
      "<domain-name>": {
        "description": "<description>",
        "owner": "<owner>",
        "meta": {  // 🚧 Coming Soon
          "<meta-key>": "<meta-value>",
          ...
        }
      },
      ...
    },
    "components": {
      "<component-name>": {
        "owner": "<owner>",
        "domain": "<domain>",
        "branch": "<branch>",
        "description": "<description>",
        "paths": ["<path1>", "<path2>", ...],
        "tags": ["<tag1>", "<tag2>", ...],
        "ciPipelines": ["<ci-pipeline1>", "<ci-pipeline2>", ...],
        "meta": {  // 🚧 Coming Soon
          "<meta-key>": "<meta-value>",
          ...
        }
      },
      ...
    }
  }
  ```

The Catalog JSON is a JSON object that contains information about the domains and components.

The Catalog JSON, unlike the Component JSON, has a pre-defined structure. The semantics of the fields are defined in the [domains](/configuration/lunar-config/domains) and [components](/configuration/lunar-config/components) pages. The same fields used to configure domains and components in `lunar-config.yml` are used to define the structure of the Catalog JSON.

The JSON object is formed by collecting information from the catalogers, and then merging that information with any data from `lunar.yml` and `lunar-config.yml`. The precedence in which the information is used is as follows:

1. The information from `lunar-config.yml` in the `domains` and `components` section.
2. The information from `lunar.yml` in each component directory. 🚧 Coming Soon
3. The information from the catalogers, where the **last-defined cataloger takes precedence**.

So, for example, if a cataloger emits a component with the same name as a component defined in `lunar-config.yml`, the fields would be combined, and any fields that exist in both would be overridden by the `lunar-config.yml` values.

If you would like to inspect the Catalog JSON, you can do so by running the following command:

```bash
lunar cataloger get-json
```

If you would like to execute the catalogers in development mode, and see the Catalog JSON that would be generated, you can do so by running the following command (needs to be run in the root of the Lunar configuration repository):

```bash
lunar cataloger dev --output-json
```


# Lunar CLI Reference

Complete reference for every Lunar CLI command, subcommand, flag, and environment variable across collectors, catalogers, policies, and Hub operations.

This document provides a comprehensive reference for all available options and commands in the Lunar CLI.

## Global options

### `--config-dir <config-dir>`, `LUNAR_CONFIG_DIR=<config-dir>`

* Type: `string`
* Optional
* Default: `.`

The path to the directory containing `lunar-config.yml`. This path is relative to the current working directory.

### `--hub-host <hostname>`, `LUNAR_HUB_HOST=<hostname>`

* Type: `string`
* Optional

Override the URL of the Lunar Hub host name. This setting is inferred from the Lunar config if not specified.

### `--hub-grpc-port <port>`, `LUNAR_HUB_GRPC_PORT=<port>`

* Type: `integer`
* Optional

Override the GRPC port of the Lunar Hub. This setting is inferred from the Lunar config if not specified.

### `--hub-http-port <port>`, `LUNAR_HUB_HTTP_PORT=<port>`

* Type: `integer`
* Optional

Override the HTTP port of the Lunar Hub. This setting is inferred from the Lunar config if not specified.

### `--hub-insecure`, `LUNAR_HUB_INSECURE=true`

* Type: `boolean`
* Optional

If true, use insecure HTTP connections to the Hub server.

### `--no-hub`, `LUNAR_NO_HUB=true`

* Type: `boolean`
* Optional

Skip Hub interactions for dev commands (`lunar collector dev` and `lunar policy dev`). When enabled, the commands will run without connecting to Lunar Hub. Note that `--component-json` is required for `lunar policy dev` when this option is enabled, and `LUNAR_GITHUB_TOKEN` must be set if GitHub access is needed.

### `LUNAR_HUB_TOKEN`

* Type: `string`
* Required for commands that interact with the Hub server

The Lunar Hub token to use for authentication.

## Licence Commands

Inspect a Lunar licence JWT and extract artefacts for cluster bootstrap. These commands run **locally** — they verify the licence against a trust list embedded in the `lunar` binary, so they work before a Hub exists (the canonical bootstrap case).

### Shared options

#### `--licence-file <path>`, `LUNAR_LICENCE_FILE=<path>`

* Type: `string`
* Required (or set the env var)

Path to the licence JWT on disk. All `lunar licence` subcommands accept this flag, or read `LUNAR_LICENCE_FILE` if it's unset.

### `lunar licence verify`

* Form:

  ```bash
  lunar licence verify --licence-file <path>
  ```

Verify the licence signature against the trust list embedded in this binary and print a customer-facing summary: tenant, expiry, and whether the licence carries a GHCR image-pull credential. Credentials are never printed — use `lunar licence registry-token` if you need the credential itself for manual `docker login`.

Example output:

```
Licence valid for tenant: acme-corp
Expires:                  2027-12-31 (in 587 days)

GHCR pull credential: configured — run `lunar licence pull-secret` to provision
```

### `lunar licence pull-secret`

* Form:

  ```bash
  lunar licence pull-secret --licence-file <path> --namespace <ns> [--name <name>] [--out <path>]
  ```

Emit a Kubernetes `imagePullSecret` (type `kubernetes.io/dockerconfigjson`) that authenticates the cluster against `ghcr.io` using the credential carried in the licence. Pipe to `kubectl apply -f -`, commit to GitOps, whatever fits.

Errors if the licence does not carry a GHCR pull credential.

#### `--namespace <ns>` | `-n`

* Type: `string`
* Required

Kubernetes namespace for the Secret.

#### `--name <name>`

* Type: `string`
* Optional
* Default: `regcred`

Kubernetes Secret name. The Hub chart's `imagePullSecrets` reference defaults to `regcred`.

#### `--out <path>` | `-o`

* Type: `string`
* Optional

Write the manifest to a file instead of stdout. The file is created with mode `0600` because it contains a sensitive credential.

#### Examples

```bash
# Provision the pull secret in the lunar namespace, applied to the cluster directly.
lunar licence pull-secret \
  --licence-file=path/to/hub-licence.jwt \
  --namespace=lunar | kubectl apply -f -

# Same, but for the snippet pods namespace.
lunar licence pull-secret \
  --licence-file=path/to/hub-licence.jwt \
  --namespace=lunar-scripts | kubectl apply -f -

# Write the manifest to disk for GitOps.
lunar licence pull-secret \
  --licence-file=path/to/hub-licence.jwt \
  --namespace=lunar \
  --out=regcred.yaml
```

### `lunar licence registry-token`

* Form:

  ```bash
  lunar licence registry-token --licence-file <path>
  ```

Print the raw GHCR pull credential from the licence to stdout, suitable for piping into `docker login --password-stdin`. Errors if the licence does not carry one.

Example:

```bash
lunar licence registry-token --licence-file=path/to/hub-licence.jwt | \
  docker login ghcr.io -u earthly-bot --password-stdin
```

## Config Commands

### `lunar config schema`

* Form:

  ```bash
  lunar config schema
  ```

Print the JSON Schema (draft-07 format) for `lunar-config.yml` to stdout, generated from the same manifest definition the Hub uses. Redirect it into your config repo for editor autocomplete and inline structural feedback:

```bash
lunar config schema > lunar-config.schema.json
```

The schema is a structural aid only (it can't resolve `uses:` plugins). For authoritative pre-merge validation, use [`lunar hub pull --dry-run`](#lunar-hub-pull). See [Validating your config](/configuration/lunar-config/validation).

### `lunar hub pull`

* Form:

  ```bash
  lunar hub pull [--dry-run] [--rerun-code-collectors|-l] [--include-pr-commits] [--pr-max-age-days <days>] [--rerun-catalogers|-t] <repo>
  ```

The `lunar hub pull` command is used to instruct Lunar Hub to pull the latest configuration from a given repository.

For GitHub Actions workflows, the [`sync-config` action](/install/github-actions) is a thin wrapper around this command and exposes the same flags as inputs.

{% hint style="warning" %}
Using `--rerun-code-collectors` or `--rerun-catalogers` can trigger a large amount of work in Lunar Hub, especially across many components or PRs. Expect a backlog and elevated load while the reruns process.
{% endhint %}

#### `<repo>`

* Type: `string`
* Form: `github://<owner>/<repo>@<branch-or-sha>`
* Required

The repository to pull configuration from. This should be the main repository containing your lunar configuration files.

**Examples:**

* `github://acme-corp/lunar@main`
* `github://acme-corp/lunar@de4adbeef`

#### `--dry-run`

* Type: `boolean`
* Optional

Validate the configuration and exit **without** applying it. The dry run performs the Hub's own load-and-validate steps against the same code — clone the config, load `lunar-config.yml` + `lunar-config.d/` fragments, resolve every `uses:` plugin, validate the whole manifest, and resolve each unpinned component's default branch — then stops before persisting. It does not install per-snippet dependencies, so that one step could still fail on a real pull; see [Validating your config](/configuration/lunar-config/validation) for the details.

It needs **no Hub connection**, which makes it suitable as a pre-merge CI check. It does need GitHub access to resolve `uses:` plugins (set `LUNAR_GITHUB_TOKEN`, or configure the Hub as an auth source). Exits non-zero on any validation error. See [Validating your config](/configuration/lunar-config/validation) for a CI example.

#### `--rerun-code-collectors` | `-l`

* Type: `boolean`
* Optional

Rerun affected code collectors after applying the configuration.

#### `--include-pr-commits`

* Type: `boolean`
* Optional

Include PR commits when rerunning code collectors.

#### `--pr-max-age-days <days>`

* Type: `integer`
* Optional
* Default: `5`

Ignore PR commits older than this maximum number of days.

#### `--rerun-catalogers` | `-t`

* Type: `boolean`
* Optional
* Default: `false`

Rerun global catalogers after pulling the manifest. Catalogers are skipped by default — pulling a manifest no longer triggers them automatically. Per-component catalogers (`component-repo`, `component-cron`) are unaffected and continue to run on their own hooks.

### `lunar hub run-code-collectors`

* Form:

  ```bash
  lunar hub run-code-collectors [--pr-max-age-days <days>] [--include-pr-commits]
  ```

The `lunar hub run-code-collectors` command instructs Lunar Hub to rerun code collectors.

{% hint style="warning" %}
This command can trigger a large amount of work in Lunar Hub. It reruns code collectors across all components — and across recent PR commits when `--include-pr-commits` is set — which can produce a significant backlog and elevated load.
{% endhint %}

#### `--pr-max-age-days <days>`

* Type: `integer`
* Optional
* Default: `5`

Ignore PR commits older than this maximum number of days.

#### `--include-pr-commits`

* Type: `boolean`
* Optional

Include PR commits when running code collectors.

### `lunar hub get-logs`

* Form:

  ```bash
  lunar hub get-logs [--namespace <namespace>] [--output <format>] [--tail <lines>]
  ```

The `lunar hub get-logs` command retrieves logs from Lunar Hub.

#### `--namespace <namespace>` | `-n`

* Type: `string`
* Optional

The Kubernetes namespace to retrieve logs from.

#### `--output <format>` | `-o`

* Type: `string`
* Optional

Output format for the logs.

#### `--tail <lines>`

* Type: `integer`
* Optional

Number of lines to show from the end of the logs.

## Domain Commands

### `lunar domain ls`

{% hint style="info" %}
**Coming Soon** — This feature is not yet available.
{% endhint %}

* Form:

  ```bash
  lunar domain ls
  ```

The `lunar domain ls` command is used to list all domains.

## Component Commands

### `lunar component ls`

{% hint style="info" %}
**Coming Soon** — This feature is not yet available.
{% endhint %}

* Form:

  ```bash
  lunar component ls
  ```

The `lunar component ls` command is used to list all components.

### `lunar component get-json`

* Form:

  ```bash
  lunar component get-json [--git-sha <git-sha>] [--pr <pr-number>] [component-name]
  ```

The `lunar component get-json` command is used to retrieve the component JSON for a specified component.

#### `[component-name]`

* Type: `string`

The name of the component to retrieve the JSON for. If not provided, falls back to the `LUNAR_COMPONENT_ID` environment variable.

#### `--git-sha <git-sha>`

* Type: `string`
* Optional

The specific git SHA to retrieve the component JSON for. If not specified, the latest component JSON will be retrieved.

#### `--pr <pr-number>`

* Type: `integer`
* Optional

The PR number to retrieve the component JSON for. If not specified, and no git SHA is provided, the component JSON for the primary branch will be retrieved. This is ignored if `--git-sha` is specified.

#### `--pretty` | `-p`

* Type: `boolean`
* Optional

Pretty-print the JSON output.

Example:

```bash
lunar component get-json github.com/my-org/my-repo
```

## Cataloger Commands

`lunar cataloger` manages catalogers from outside. This is distinct from `lunar catalog`, which is the SDK command catalogers use *inside* their own scripts to emit catalog entries.

### `lunar cataloger get-json`

* Form:

  ```bash
  lunar cataloger get-json [--cataloger <name>] [--component <name>] [--ts <timestamp>] [--pretty]
  ```

The `lunar cataloger get-json` command retrieves the catalog JSON from Lunar Hub. By default it returns the latest merged catalog snapshot (the same JSON that all catalogers contribute to). Use `--cataloger` to fetch a single cataloger's most recent contribution (its delta), `--component` to scope a per-component cataloger to one component, and `--ts` to fetch a historical snapshot.

#### `--cataloger <name>`

* Type: `string`
* Optional

Name of the cataloger whose delta should be returned. If omitted, the merged catalog snapshot is returned. The cataloger is resolved by exact name in the current manifest; an unknown name returns an error.

Whether `--component` is allowed depends on the cataloger's hooks:

* A **global** cataloger (only `cron` / `repo` hooks) returns a single delta. Passing `--component` is rejected.
* A **per-component** cataloger (any `component-cron` / `component-repo` hook) stores one delta per component. `--component` is required; omitting it is rejected with a "per-component scoped" error.

#### `--component <name>`

* Type: `string`
* Optional

Component to scope the lookup to (e.g. `github.com/my-org/my-repo`). Only valid together with `--cataloger`, and only for per-component catalogers — required for those, rejected for purely global ones. Returns the delta that cataloger emitted for the named component.

#### `--ts <timestamp>`

* Type: `string`
* Optional

Return the row whose `created_at` is at or immediately before this timestamp. If omitted, the latest row is returned. The command exits with an error if no row exists at or before the timestamp.

Accepted formats (parsed in this order):

* Date only: `2026-05-01` (interpreted as `2026-05-01T00:00:00Z`)
* Date + time without timezone: `2026-05-01T12:34:56` (interpreted as UTC)
* Full RFC3339: `2026-05-01T12:34:56Z` or `2026-05-01T12:34:56-07:00`

#### `--pretty` | `-p`

* Type: `boolean`
* Optional

Pretty-print the JSON output.

Examples:

```bash
# Latest merged catalog snapshot
lunar cataloger get-json --pretty
# A global cataloger's most recent delta
lunar cataloger get-json --cataloger sbom --pretty
# A per-component cataloger's delta for one component, as of a date
lunar cataloger get-json --cataloger sbom --component github.com/my-org/my-repo --ts 2026-05-01 --pretty
```

### `lunar cataloger run`

* Form:

  ```bash
  lunar cataloger run [--cataloger <name>] [--component <name>] [--output-json]
  ```

The `lunar cataloger run` command triggers cataloger execution in Lunar Hub. The CLI enqueues the matching catalogers, then polls every 10 seconds and prints `queued / success / error` counters until every job is in a terminal state. With no flags, it runs every cataloger in the current manifest. Use `--cataloger` and/or `--component` to narrow the scope.

#### `--cataloger <name>`

* Type: `string`
* Optional

Run only the named cataloger (exact match or dotted-plugin prefix like `myplugin`). If omitted, every cataloger in the manifest participates. When the named cataloger has only global hooks, combining it with `--component` is rejected. When the named cataloger has per-component hooks and `--component` is omitted, it fans out to every component in the manifest.

#### `--component <name>`

* Type: `string`
* Optional

Scope per-component hooks (`component-repo` / `component-cron`) to this single component. When omitted, those hooks fan out across every component in the manifest. Global hooks (`cron` / `repo`) are unaffected.

#### `--output-json`

* Type: `boolean`
* Optional

After the polling loop completes, fetch and print the merged catalog JSON. If no catalog row exists yet (fresh manifest with no completed catalogers), prints `(no catalog yet)` to stderr and exits with the same code as if `--output-json` were not set.

Failures: for any job that ends in the `error` bucket, the CLI prints a one-line summary including a deep link to the Grafana run-details dashboard (if Hub is configured with `HUB_GRAFANA_URL_BASE`). When no snippet\_run is found for a job (rare — worker crashed before recording the run), the link is replaced with `(no run record; check hub logs)`.

Example:

```bash
lunar cataloger run --cataloger sbom --component github.com/my-org/my-repo --output-json
```

### `lunar cataloger dev`

* Form:

  ```bash
  lunar cataloger dev [cataloger-name] \
    [--component <name> | --component-dir <path>] \
    [--config <repo>] \
    [--git-sha <sha>] \
    [--script <path>] [--script-lang <bash|python|node>] \
    [--use-system-runtime] [--no-cache] [--verbose] \
    [--secrets <k=v,k=v>] \
    [--output-json]
  ```

{% hint style="warning" %}
Catalogers can be highly environment-dependent. Be mindful of "works on my machine" types of issues.
{% endhint %}

The `lunar cataloger dev` command runs catalogers locally on the user's machine without applying changes to the catalog. It mirrors `lunar collector dev` for catalogers: the cataloger snippet is fetched from the configured manifest, executed under the configured runtime (Python / Node / Bash, or Docker when the snippet has an image), and its output is printed. No data is written to Lunar Hub.

#### `[cataloger-name]`

* Type: `string`
* Optional

Cataloger name (or dotted plugin name like `myplugin.sync`) to execute. If omitted, every cataloger applicable to the resolved scope is run.

#### `--component <name>`

* Type: `string`
* Optional

Look the component up in the resolved manifest. Required when running a per-component cataloger. Mutually exclusive with `--component-dir`.

#### `--component-dir <path>`

* Type: `string`
* Optional

Treat the given local directory as the component checkout (no clone). Mutually exclusive with `--component` and `--config`.

#### `--config <repo>`

* Type: `string`
* Optional

Remote configuration repository to load the manifest from (e.g. `github.com/org/repo` or `github://org/repo@branch`). Mutually exclusive with `--component-dir`.

#### `--git-sha <sha>`

* Type: `string`
* Optional

Pin the component checkout to this SHA on the component's primary branch. Only meaningful for per-component catalogers. Defaults to the tip of the primary branch.

{% hint style="info" %}
Unlike `lunar collector dev`, there is no `--pr` flag — catalogers run on pushes to main, not on PR commits.
{% endhint %}

#### `--script <path>`

* Type: `string`
* Optional

Override the cataloger's `main` script with a local file path. Combine with `--script-lang` to control the runtime used.

#### `--script-lang <language>`

* Type: `string`
* Optional
* Default: `bash`

Language for `--script` (`bash` / `python` / `node`).

#### `--use-system-runtime`

* Type: `boolean`
* Optional

Use the host's system Python / Node / Bash instead of the bundled Lunar runtimes. Defaults to `true` on non-Linux-amd64 hosts.

#### `--no-cache`

* Type: `boolean`
* Optional

Delete cacheable temporary files before running. Use this to force a clean run when troubleshooting.

#### `--verbose`

* Type: `boolean`
* Optional

Stream the cataloger's engine stdout / stderr to the terminal as it runs.

#### `--secrets <k=v,k=v>`

* Type: `string`
* Optional

Supplemental secrets injected into the cataloger's environment, parsed as comma-separated `key=value` pairs.

#### `--output-json`

* Type: `boolean`
* Optional

Print the merged Catalog JSON the user would see if these catalogers ran in Hub (manifest base + cataloger deltas + manifest overrides), in the same shape as [Catalog JSON](/docs/catalog-json). Without this flag, each cataloger's raw emitted JSON is printed under a per-cataloger header.

Example:

```bash
lunar cataloger dev sbom --component github.com/my-org/my-repo --verbose
```

## Collector Commands

### `lunar collector run`

{% hint style="info" %}
**Coming Soon** — This feature is not yet available.
{% endhint %}

* Form:

  ```bash
  lunar collector run [--output-json] [--pr <pr-number>] \
    [--git-sha <git-sha>] [--only-code] [--only-cron] \
    [--collector <collector-name>] [--pr-max-age-days <days>] <component-name>
  ```

The `lunar collector run` command is used to rerun code and cron collectors for a given component. This command triggers execution in the cloud via Lunar Hub.

#### `<component-name>`

* Type: `string`

The name of the component to rerun collectors for.

#### `--pr <pr-number>`

* Type: `integer`
* Optional

The PR number to rerun collectors for. If not specified, collectors will be run for the component's primary branch.

#### `--git-sha <git-sha>`

* Type: `string`
* Optional

The specific git SHA to rerun collectors for. If specified, this takes precedence over `--pr`.

#### `--only-code`

Run only code collectors.

#### `--only-cron`

Run only cron collectors.

#### `--collector <collector-name>`

* Type: `string`
* Optional
* Repeatable

Run only the specified collector. This flag can be repeated to run multiple specific collectors.

#### `--pr-max-age-days <days>`

* Type: `integer`
* Optional
* Default: `5`

Ignore PR commits older than this maximum number of days.

#### `--output-json`

Output the results in JSON format.

Example:

```bash
# Rerun all collectors for all components for all PRs, limitting to 1 day old PRs
lunar collector run --pr-max-age-days 1
# Rerun collectors for a component (primary branch)
lunar collector run github.com/my-org/my-repo
# Rerun collectors for a specific PR
lunar collector run --pr 123 github.com/my-org/my-repo
# Rerun collectors for a specific git SHA
lunar collector run --git-sha abc123 github.com/my-org/my-repo
# Run only code collectors
lunar collector run --only-code github.com/my-org/my-repo
# Run only a specific collector
lunar collector run --collector collector-name github.com/my-org/my-repo
```

### `lunar collector dev`

* Name Form:

  ```bash
  lunar collector dev [--pr <pr-number>] [--git-sha <git-sha>] \
    [--component <component-name> | --component-dir <path>] \
    [--fake-ci-cmd <bash-command>] \
    <collector-name>
  ```
* Script Form:

  ```bash
  lunar collector dev \
    [--pr <pr-number>] [--git-sha <git-sha>] \
    [--component <component-name> | --component-dir <path>] \
    [--fake-ci-cmd <bash-command>] \
    --script <path-to-collector-script>
  ```

{% hint style="warning" %}
Collectors can be highly environment-dependent. Be mindful of "works on my machine" types of issues.
{% endhint %}

The `lunar collector dev` command is used to run a collector for a given component in development mode without applying changes. This command executes locally on the user's machine and outputs the resulting component JSON.

#### `<collector-name>`

* Type: `string`
* Required in Name Form

#### `--script <path-to-collector-script>`

* Type: `string`
* Required in Script Form

#### `--component <component-name>`

* Type: `string`

The name of the component to run collectors for. If not provided, falls back to the `LUNAR_COMPONENT_ID` environment variable. Mutually exclusive with `--component-dir`.

#### `--component-dir <path>`

* Type: `string`

Local directory containing the component to run collectors for. The directory must be a git repository. The component name is derived from the git remote URL. Mutually exclusive with `--component`.

#### `--pr <pr-number>`

* Type: `integer`
* Optional

The PR number to run collectors for. If not specified, collectors will be run for the component's primary branch.

#### `--git-sha <git-sha>`

* Type: `string`
* Optional

The specific git SHA to run collectors for. If specified, this takes precedence over `--pr`.

#### `<collector-name>`

* Type: `string`
* Optional

The name of the collector to run. If not specified, all collectors will be run.

The path to a bash collector script file to run in development mode.

#### `--fake-ci-cmd <bash-command>`

* Type: `string`
* Optional

A command that the CI would have executed, that would cause lunar instrumentation to trigger an event for. This command is not actually executed, it is merely used to test collector triggering logic (e.g. would the collector trigger regex match the command line). This option is used for testing CI collectors locally without requiring an actual CI pipeline execution.

#### `--config <repo>`

* Type: `string`
* Optional

Remote config repository to use.

#### `--use-system-runtime`

* Type: `boolean`
* Optional

Use the system runtime instead of a containerized environment.

#### `--no-cache`

* Type: `boolean`
* Optional

Disable caching.

#### `--verbose`

* Type: `boolean`
* Optional

Enable verbose output.

#### `--merge`

* Type: `boolean`
* Optional

Merge the collected data into the existing component JSON.

#### `--secrets`

* Type: `boolean`
* Optional

Fetch and inject secrets into the collector execution environment.

#### Examples

```bash
# Run collectors in development mode for a component (primary branch)
lunar collector dev --component github.com/my-org/my-repo
# Run collectors in development mode for a specific PR
lunar collector dev --component github.com/my-org/my-repo --pr 123
# Run a specific collector
lunar collector dev --component github.com/my-org/my-repo collector-name
# Run a specific collector script file
lunar collector dev --component github.com/my-org/my-repo --script ./path/to/collector.sh
# Test a CI collector with a fake CI command
lunar collector dev --component github.com/my-org/my-repo --fake-ci-cmd "npm test" ci-collector-name
# Run collectors against a local directory
lunar collector dev --component-dir ./my-local-repo collector-name
# Run collectors against a local directory with a specific git SHA
lunar collector dev --component-dir ./my-local-repo --git-sha abc123 collector-name
```

#### Credentials

What credentials `lunar collector dev` needs depends on how you name the component:

* **`--component-dir <path>`** runs against a checkout you already have. It performs only local git operations (no clone), so it needs **no Hub and no GitHub token**. This is the way to smoke-test a collector against a local component with no credentials — pair it with `--no-hub` to skip Hub entirely, and `--script` to run a single script without resolving the manifest.
* **`--component <name>`** must resolve the component's default branch and clone it, so it needs **either a configured Hub or `LUNAR_GITHUB_TOKEN`**. Without one, the command errors and points you at `--component-dir`.

## Policy Commands

### `lunar policy ls`

{% hint style="info" %}
**Coming Soon** — This feature is not yet available.
{% endhint %}

* Form:

  ```bash
  lunar policy ls
  ```

The `lunar policy ls` command is used to list all policies.

### `lunar policy check ls`

{% hint style="info" %}
**Coming Soon** — This feature is not yet available.
{% endhint %}

* Form:

  ```bash
  lunar policy check ls
  ```

The `lunar policy check ls` command is used to list all checks.

### `lunar policy run`

{% hint style="info" %}
**Coming Soon** — This feature is not yet available.
{% endhint %}

* Form:

  ```bash
  lunar policy run [--output-json] [--pr <pr-number>] \
    [--git-sha <git-sha>] [--policy <policy-name>] \
    [--initiative <initiative-name>] <component-name>
  ```

The `lunar policy run` command is used to rerun all policies for a given component. This command triggers execution in the cloud via Lunar Hub.

#### `<component-name>`

* Type: `string`

The name of the component to rerun policies for.

#### `--pr <pr-number>`

* Type: `integer`
* Optional

The PR number to rerun policies for. If not specified, policies will be run for the component's primary branch.

#### `--git-sha <git-sha>`

* Type: `string`
* Optional

The specific git SHA to rerun policies for. If specified, this takes precedence over `--pr`.

#### `--policy <policy-name>`

* Type: `string`
* Optional
* Repeatable

Run only the specified policy. This flag can be repeated to run multiple specific policies.

#### `--initiative <initiative-name>`

* Type: `string`
* Optional
* Repeatable

Run only policies under the specified initiative. This flag can be repeated to run policies under multiple initiatives.

#### `--output-json`

Output the results in JSON format.

Example:

```bash
# Rerun policies for a component (primary branch)
lunar policy run github.com/my-org/my-repo
# Rerun policies for a specific PR
lunar policy run --pr 123 github.com/my-org/my-repo
# Rerun policies for a specific git SHA
lunar policy run --git-sha abc123 github.com/my-org/my-repo
# Run only a specific policy
lunar policy run --policy policy-name github.com/my-org/my-repo
# Run policies under a specific initiative
lunar policy run --initiative initiative-name github.com/my-org/my-repo
```

### `lunar policy dev`

* Name Form:

  ```bash
  lunar policy dev [--component <component-name>] [--component-json <path-to-json-or-stdin>] \
    [--pr <pr-number>] [--git-sha <git-sha>] \
    <policy-name>
  ```
* Script Form:

  ```bash
  lunar policy dev [--component <component-name>] [--component-json <path-to-json-or-stdin>] \
    [--pr <pr-number>] [--git-sha <git-sha>] \
    --script <path-to-policy-script>
  ```

{% hint style="warning" %}
Policies can be highly environment-dependent. Be mindful of "works on my machine" types of issues.
{% endhint %}

The `lunar policy dev` command is used to run a policy against a component for local testing purposes. This command executes locally on the user's machine and outputs the check results in JSON format.

#### `<policy-name>`

* Type: `string`
* Required in Name Form

#### `--script <path-to-policy-script>`

* Type: `string`
* Required in Script Form

#### `--component <component-name>`

* Type: `string`

The name of the component to run the policy against. If not provided, falls back to the `LUNAR_COMPONENT_ID` environment variable.

#### `--component-json <path-to-json-or-stdin>`

* Type: `string`

The path to the component JSON file or `-` to read from stdin.

#### `--pr <pr-number>`

* Type: `integer`
* Optional

The PR number to run the policy against. If not specified, the policy will be run against the component's primary branch.

#### `--git-sha <git-sha>`

* Type: `string`
* Optional

The specific git SHA to run the policy against. If specified, this takes precedence over `--pr`.

#### `--with <args>`

* Type: `string`
* Optional

Arguments passed to the policy script.

#### `--script-lang <language>`

* Type: `string`
* Optional
* Default: `python`

The script programming language.

#### `--output <format>`

* Type: `string`
* Optional
* Values: `json`, `list`

Output format for the policy results.

#### `--config <repo>`

* Type: `string`
* Optional

Remote config repository to use.

#### `--use-system-runtime`

* Type: `boolean`
* Optional

Use the system runtime instead of a containerized environment.

#### `--no-cache`

* Type: `boolean`
* Optional

Disable caching.

#### `--verbose`

* Type: `boolean`
* Optional

Enable verbose output.

#### `--secrets`

* Type: `boolean`
* Optional

Fetch and inject secrets into the policy execution environment.

#### Example

```bash
# Run policy with component JSON from file
lunar policy dev --component-json path/to/component.json --script ./path/to/policy.py
# Run policy by specifying component directly
lunar policy dev --component github.com/my-org/my-repo --script ./path/to/policy.py
# Run policy with component JSON from stdin
lunar component get-json --git-sha ... github.com/my-org/my-repo | \
  lunar policy dev --component-json - --script ./path/to/policy.py
# Run specific policy from config
lunar policy dev --component github.com/my-org/my-repo my-policy
# Run policy for a specific PR
lunar policy dev --component github.com/my-org/my-repo --pr 123 --script ./path/to/policy.py
```

### `lunar policy ok-release`

* Form:

  ```bash
  lunar policy ok-release <component> <git_sha>
  lunar policy ok-release  # uses LUNAR_COMPONENT_ID and GITHUB_SHA
  ```

The `lunar policy ok-release` command is used to check if a component at a specific git SHA passes its release policies.

Either both positional arguments must be provided, or neither. When no arguments are given, both `LUNAR_COMPONENT_ID` and `GITHUB_SHA` environment variables must be set.

#### `<component>`

* Type: `string`

The name of the component to check. Falls back to the `LUNAR_COMPONENT_ID` environment variable when no arguments are given.

#### `<git_sha>`

* Type: `string`

The git SHA to check. Falls back to the `GITHUB_SHA` environment variable when no arguments are given.

#### `--poll-interval <duration>`

* Type: `duration`
* Optional
* Default: `10s`

How often to poll for results.

#### `--timeout <duration>`

* Type: `duration`
* Optional
* Default: `10m`

Maximum time to wait for results before timing out.

#### `--workflow-id <id>`

* Type: `string`
* Optional

The CI workflow ID to associate with the check.

#### `--pr <pr-number>`

* Type: `integer`
* Optional

The PR number to check policies for.

### `lunar policy ok-pr`

* Form:

  ```bash
  lunar policy ok-pr <component> <git_sha>
  lunar policy ok-pr  # uses LUNAR_COMPONENT_ID and GITHUB_SHA
  ```

The `lunar policy ok-pr` command is used to check if a component at a specific git SHA passes its PR policies.

Either both positional arguments must be provided, or neither. When no arguments are given, both `LUNAR_COMPONENT_ID` and `GITHUB_SHA` environment variables must be set.

#### `<component>`

* Type: `string`

The name of the component to check. Falls back to the `LUNAR_COMPONENT_ID` environment variable when no arguments are given.

#### `<git_sha>`

* Type: `string`

The git SHA to check. Falls back to the `GITHUB_SHA` environment variable when no arguments are given.

#### `--poll-interval <duration>`

* Type: `duration`
* Optional
* Default: `10s`

How often to poll for results.

#### `--timeout <duration>`

* Type: `duration`
* Optional
* Default: `10m`

Maximum time to wait for results before timing out.

#### `--workflow-id <id>`

* Type: `string`
* Optional

The CI workflow ID to associate with the check.

#### `--pr <pr-number>`

* Type: `integer`
* Optional

The PR number to check policies for.

## SDK Commands

### `lunar catalog`

Saves catalog-related information from within a cataloger.

For detailed documentation on the `lunar catalog` command and all its options, see the [Cataloger Bash SDK](/plugin-sdks/bash-sdk/cataloger) page.

### `lunar collect`

Collects SDLC metadata into a component's JSON. Pass `--component` and `--sha` to collect against a specific component and commit from anywhere.

For detailed documentation on the `lunar collect` command and all its options, see the [Collector Bash SDK](/plugin-sdks/bash-sdk/collector) page.

## SQL Commands

### `lunar sql connection-string`

* Form:

  ```bash
  lunar sql connection-string
  ```

The `lunar sql connection-string` command returns the PostgreSQL connection string that can be used with any PostgreSQL client. The access is **read-only** and restricted to only the views described in the [SQL API](/sql-api/sql-api) documentation.

Example:

```bash
# Get the connection string
lunar sql connection-string
# Connect using psql (interactive)
psql $(lunar sql connection-string)
# Export checks data as CSV
psql $(lunar sql connection-string) -c "COPY (
  SELECT *
  FROM checks
  WHERE component_id = 'github.com/my-org/my-repo'
    AND status = 'fail'
) TO STDOUT WITH CSV HEADER" > failed_checks.csv
# Export component data as JSON
psql $(lunar sql connection-string) -c "
  SELECT json_agg(row_to_json(c))
  FROM (
    SELECT *
    FROM components
    WHERE tags @> '{\"team\":\"ui\"}'
  ) c" > platform_components.json
# Make decisions based on check results
psql $(lunar sql connection-string) -t -c "
  SELECT COUNT(*)
  FROM checks c
  JOIN components comp ON c.component_id = comp.component_id
  WHERE comp.domain = 'payments'
    AND c.status = 'fail'
    AND c.enforcement IN ('block-pr-and-release', 'block-release')" | \
  grep -q "^0$" || \
  (echo "Release blocking checks are failing in payments domain!" && exit 1)
```

For more examples of the SQL API in action, see the [SQL API](/sql-api/sql-api) documentation.

### `lunar secret set`

```bash
  lunar secret set <name> [value]
```

Set a secret that will be available to collectors, policies, or catalogers as `LUNAR_SECRET_<NAME>`.

If `value` is omitted, it is read from stdin (recommended for sensitive values to avoid shell history exposure).

Secrets are encrypted at rest using AES-256-GCM. The Hub must have `HUB_SECRETS_ENCRYPTION_KEY` configured.

#### Options

* `--scope <scope>` — Secret scope: `collector` (default), `policy`, or `cataloger`.

#### Examples

```bash
# Set a collector secret (value as argument)
lunar secret set GH_TOKEN ghp_abc123

# Set a secret from stdin (recommended)
echo "ghp_abc123" | lunar secret set GH_TOKEN

# Set a policy secret
lunar secret set --scope policy JIRA_API_KEY my-api-key
```

### `lunar secret delete`

```bash
  lunar secret delete <name>
```

Delete a previously configured secret.

#### Options

* `--scope <scope>` — Secret scope: `collector` (default), `policy`, or `cataloger`.

#### Examples

```bash
lunar secret delete GH_TOKEN
lunar secret delete --scope policy JIRA_API_KEY
```

### `lunar secret list`

```bash
  lunar secret list
```

List the names of configured secrets for a given scope. Values are never displayed.

#### Options

* `--scope <scope>` — Secret scope: `collector` (default), `policy`, or `cataloger`.

#### Examples

```bash
# List collector secrets
lunar secret list

# List policy secrets
lunar secret list --scope policy
```

## Utility Commands

### `lunar clear-cache`

* Form:

  ```bash
  lunar clear-cache
  ```

The `lunar clear-cache` command deletes Git and installation file caches used by Lunar. This can be useful to resolve issues caused by stale cached data.

### `lunar version`

* Form:

  ```bash
  lunar version
  ```

The `lunar version` command prints the Lunar CLI version and commit SHA.


# lunar-config.yml

Reference for lunar-config.yml — the central configuration file defining hub connection, catalogers, domains, components, collectors, initiatives, and policies.

* `lunar-config.yml`
* Type: YAML file
* Form:

  ```yaml
  version: 0

  default_image: <default-image>
  default_image_ci_collectors: <default-image-ci>
  default_image_non_ci_collectors: <default-image-non-ci>
  default_image_policies: <default-image-policies>
  default_image_catalogers: <default-image-catalogers>

  hub:
    host: <hub-host>
    grpcPort: <grpc-port>
    httpPort: <http-port>
    insecure: <insecure-flag>

  catalogers:
    - <cataloger-object>
    - <cataloger-object>
    - ...

  domains:
    <domain-name>: <domain-object>
    <domain-name>: <domain-object>
    ...

  components:
    <component-name>: <component-object>
    <component-name>: <component-object>
    ...

  collectors:
    - <collector-object>
    - <collector-object>
    - ...

  initiatives:
    - <initiative-object>
    - <initiative-object>
    - ...

  policies:
    - <policy-object>
    - <policy-object>
    - ...
  ```

The file `lunar-config.yml` is used to configure the behavior of Lunar.

{% hint style="info" %}
It is recommended that you create a new code repository for all Lunar configuration and place this file in the root of it.
{% endhint %}

At a high-level, the file contains information about how the Lunar primitives are configured, ranging from how information is collected from the SDLC via collectors, to how components are organized into domains, and how the health of components is evaluated via policies.

## Splitting the configuration

A single `lunar-config.yml` works well and is the typical setup. If you'd prefer to spread a larger configuration across several files — for example, one per team — you can: any `*.yml` / `*.yaml` files placed in a `lunar-config.d/` directory next to `lunar-config.yml` are merged into it, in lexical filename order.

```
lunar-config.yml          # version, hub, default images
lunar-config.d/
  collectors.yml
  team-frontend.yml
  team-backend.yml
```

`lunar-config.yml` stays the entry point and holds the singleton fields (`version`, `hub`, `default_image*`); each fragment contributes additional sections. List sections (`collectors`, `policies`, …) are concatenated; map sections (`domains`, `components`) are unioned by key, and a key defined in more than one place is an error rather than a silent override.

## `version`

* `lunar-config.yml -> version`
* Type: `string`
* Required

The version field is used to specify the version of the configuration file. The current version is `0`.

## Default Images

* `lunar-config.yml -> default_image*`
* Optional

These fields configure the default Docker images used to run collectors, policies, and catalogers. When set, scripts will run inside containers instead of natively on the host.

A common configuration is:

{% code title="lunar-config.yml" %}

```yaml
default_image: earthly/lunar-scripts:1.0.0
default_image_ci_collectors: native
```

{% endcode %}

This runs most scripts in containers while keeping CI collectors native for direct access to CI environments.

For detailed documentation on default images, image resolution order, and the official `earthly/lunar-scripts` image, see [Images](/configuration/lunar-config/images).

## `hub`

* `lunar-config.yml -> hub`
* Type: `object`
* Required

The `hub` object contains configuration for the Lunar Hub server.

### `host`

* `lunar-config.yml -> hub.host`
* Type: `string`
* Required

The host field is used to specify the host of the Lunar Hub server.

### `grpcPort`

* `lunar-config.yml -> hub.grpcPort`
* Type: `integer`
* Required

The grpcPort field is used to specify the port of the Lunar Hub server for GRPC connections.

### `httpPort`

* `lunar-config.yml -> hub.httpPort`
* Type: `integer`
* Required

The httpPort field is used to specify the port of the Lunar Hub server for HTTP connections.

### `insecure`

* `lunar-config.yml -> hub.insecure`
* Type: `boolean`
* Optional
* Default: `false`

The insecure field is used to specify whether to use insecure HTTP connections to the Lunar Hub server.

## `catalogers`

* `lunar-config.yml -> catalogers`
* Type: `array`
* Optional

Catalogers are used to synchronize software catalog information (such as domains, and components) with external systems.

For information on how to configure catalogers, see [catalogers](/configuration/lunar-config/catalogers).

## `domains`

* `lunar-config.yml -> domains`
* Type: `object`
* Optional

Domains are used to group related components together. Domains are hierarchical and can contain other domains.

For information on how to configure domains, see [domains](/configuration/lunar-config/domains).

## `components`

* `lunar-config.yml -> components`
* Type: `object`
* Optional

Components are the units of code that Lunar monitors. A component can represent either a code repository, or a subdirectory in the case of a monorepo.

Components are associated with domains and can have tags. Through the tagging system, components are associated with collectors, and policies.

For information on how to configure components, see [components](/configuration/lunar-config/components).

## `collectors`

* `lunar-config.yml -> collectors`
* Type: `array`
* Required

Collectors are used to collect live information from various sources to associate with individual components.

For information on how to configure collectors, see [collectors](/configuration/lunar-config/collectors).

## `initiatives`

* `lunar-config.yml -> initiatives`
* Type: `array`
* Optional

Initiatives are used to group components together. Initiatives are associated with domains and can have tags.

For information on how to configure initiatives, see [initiatives](/configuration/lunar-config/initiatives).

## `policies`

* `lunar-config.yml -> policies`
* Type: `array`
* Required

Policies are used to define the rules that Lunar uses to evaluate the health of components. Policies are associated with domains and can be inherited by child domains.

For information on how to configure policies, see [policies](/configuration/lunar-config/policies).


# About images

Configure default container images for collectors, policies, and catalogers in lunar-config.yml, with override precedence rules.

Lunar supports running collectors, policies, and catalogers inside Docker containers. This provides isolation, reproducibility, and simplifies dependency management. Default images can be configured at multiple levels, with more specific settings overriding more general ones.

## Image Resolution Order

The image used to run a script is determined in the following order (first match wins):

1. **Script-level `image`** - Set directly on the collector, policy, or cataloger
2. **Plugin-level default** - Set in `lunar-collector.yml`, `lunar-policy.yml`, or `lunar-cataloger.yml`
3. **Global default** - Set in `lunar-config.yml`
4. **Implicit default** - `native` (no container)

## Global Default Images

Configure default images in `lunar-config.yml`:

{% code title="lunar-config.yml" %}

```yaml
version: 0

default_image: my-custom-image:alpine-1.2.3
default_image_ci_collectors: native
default_image_non_ci_collectors: my-image:v1.0
default_image_policies: another-image:latest
default_image_catalogers: yet-another-image:v2.0

# ... rest of configuration
```

{% endcode %}

### `default_image`

* `lunar-config.yml -> default_image`
* Type: `string`
* Optional
* Default: `native`

The global default image to use for all collectors, policies, and catalogers. This is overridden by the more specific `default_image_*` settings.

### `default_image_ci_collectors`

* `lunar-config.yml -> default_image_ci_collectors`
* Type: `string`
* Optional
* Default: value of `default_image`

The default image for CI collectors (collectors with hooks of type `ci-before-command`, `ci-after-command`, `ci-before-job`, `ci-after-job`). It is common to set this to `native` since CI collectors often need direct access to the CI environment.

### `default_image_non_ci_collectors`

* `lunar-config.yml -> default_image_non_ci_collectors`
* Type: `string`
* Optional
* Default: value of `default_image`

The default image for non-CI collectors (collectors with hooks of type `code`, `cron`).

### `default_image_policies`

* `lunar-config.yml -> default_image_policies`
* Type: `string`
* Optional
* Default: value of `default_image`

The default image for all policies.

### `default_image_catalogers`

* `lunar-config.yml -> default_image_catalogers`
* Type: `string`
* Optional
* Default: value of `default_image`

The default image for all catalogers.

## Plugin-Level Default Images

Plugins can define their own default images that override the global settings. This is useful when a plugin requires specific dependencies that are pre-installed in a custom image.

In `lunar-collector.yml`, `lunar-policy.yml`, or `lunar-cataloger.yml`:

```yaml
version: 0
name: my-plugin
description: A plugin with custom image defaults

default_image: my-org/my-plugin-image:v1.0
default_image_ci_collectors: native
default_image_non_ci_collectors: my-org/my-plugin-image:v1.0
default_image_policies: my-org/my-plugin-image:v1.0
default_image_catalogers: my-org/my-plugin-image:v1.0

# ... rest of plugin configuration
```

The same settings are available as at the global level. Plugin-level defaults override global defaults but are overridden by script-level `image` settings.

## Script-Level Image

Each individual collector, policy, or cataloger can specify its own `image` to override all defaults:

{% code title="lunar-config.yml" %}

```yaml
collectors:
  - runBash: lunar collect .file-count "$(find . | wc -l)"
    image: earthly/lunar-scripts:1.0.0
    hook:
      type: code
    on: [my-tag]
```

{% endcode %}

## The `native` Value

The special value `native` explicitly opts out of containerized execution. When `image: native` is set, the script runs directly on the host system using the native runtime (Python, Bash, etc.).

This is useful when:

* A default image has been configured, but a specific script needs to run natively
* CI collectors need direct access to the CI environment
* The script needs access to host-specific resources

## Common Configuration Pattern

A common configuration is to use containers for most scripts but run CI collectors natively:

{% code title="lunar-config.yml" %}

```yaml
default_image: earthly/lunar-scripts:1.0.0
default_image_ci_collectors: native
```

{% endcode %}

This configuration:

* Runs all policies in containers
* Runs all catalogers in containers
* Runs non-CI collectors (code, cron, repo hooks) in containers
* Runs CI collectors natively for direct access to CI environment variables and tools

## Official Image: `earthly/lunar-scripts`

Lunar provides an official Docker image `earthly/lunar-scripts` that includes:

* Alpine Linux (or `-debian` variant for tools requiring glibc)
* Python 3 with venv
* Bash
* The `lunar-policy` Python package
* The `lunar` CLI
* Common tools: `jq`, `yq`, `curl`, `parallel`, `wget`

### Dependency Handling

The official `earthly/lunar-scripts` image automatically executes any `requirements.txt` and/or `install.sh` files it finds in the plugin directory as part of its entrypoint. This is a convenience feature to help you get up and running quickly during development.

**For production use, we recommend baking all dependencies directly into your image.** This approach provides:

* Faster startup times (no runtime installation)
* Reproducible builds
* Better caching and smaller attack surface
* Elimination of network dependencies at runtime

### Recommended Approach: Custom Image Inheriting from Official

The recommended approach is to create a custom Dockerfile that inherits from the official `earthly/lunar-scripts` image and installs your dependencies at build time:

{% code title="Dockerfile" %}

```dockerfile
FROM earthly/lunar-scripts:1.0.0

# Install system dependencies (if needed)
RUN apt-get update && apt-get install -y jq curl && rm -rf /var/lib/apt/lists/*

# Copy and install Python dependencies
COPY requirements.txt /tmp/requirements.txt
RUN pip install --no-cache-dir -r /tmp/requirements.txt && rm /tmp/requirements.txt

# Copy and run install script (if needed)
COPY install.sh /tmp/install.sh
RUN /tmp/install.sh && rm /tmp/install.sh
```

{% endcode %}

This pattern gives you all the benefits of the official image (Python, Bash, `lunar` CLI, `lunar-policy` package) while ensuring your dependencies are baked in for production use.

## Image Entrypoint Contract

Lunar executes snippets by passing two arguments to the image's entrypoint:

```
<entrypoint> <language> <main-script-path>
```

| Argument | Description                           | Example             |
| -------- | ------------------------------------- | ------------------- |
| `$1`     | The snippet language                  | `python`, `bash`    |
| `$2`     | Absolute path to the main script file | `/app/exec/main.py` |

The entrypoint is responsible for invoking the correct language runtime on the given script. The official `earthly/lunar-scripts` image ships an entrypoint at `/app/entrypoint.sh` that handles this automatically.

### Requirements for Custom Images

If you build a custom image, it must meet these requirements:

1. **Entrypoint at `/app/entrypoint.sh`** that accepts `(language, script_path)` as `$1` and `$2`. The simplest implementation:

```bash
#!/bin/bash
set -e
exec "$1" "$2"
```

2. **Language runtime on `PATH`** — the binary matching the snippet's language must be available:

| Language | Required binary |
| -------- | --------------- |
| Python   | `python`        |
| Bash     | `bash`          |

{% hint style="info" %}
Many base images ship `python3` without a `python` symlink. Ensure `python` resolves correctly (e.g., `RUN ln -sf /usr/bin/python3 /usr/bin/python`). The official `earthly/lunar-scripts` image handles this automatically.
{% endhint %}

**The recommended approach is to inherit from the official image**, which satisfies both requirements out of the box:

{% code title="Dockerfile" %}

```dockerfile
FROM earthly/lunar-scripts:1.0.0
# Add your dependencies — the entrypoint and runtimes are already set up.
RUN pip install --no-cache-dir my-package
```

{% endcode %}

### How Execution Works

Both the Docker engine (local development) and the Kubernetes operator use the same entrypoint contract. Lunar passes `[language, script_path]` as arguments to the container, and the image's entrypoint handles dispatching to the correct runtime.

This means a custom image tested locally via the Docker engine will behave identically when deployed to Kubernetes — the entrypoint runs in both environments with the same arguments.

### Mount Points

The following directories are available inside the container in both Docker and Kubernetes execution modes:

| Container Path | Description                                                                   |
| -------------- | ----------------------------------------------------------------------------- |
| `/app/exec`    | Plugin directory — script and plugin files                                    |
| `/app/work`    | Working directory — the component's code (for collectors and some catalogers) |
| `/app/lib`     | Library directory — bundle data for policies                                  |

The container's working directory is set to `/app/work`, so your scripts can access the component's files using relative paths. The `LUNAR_PLUGIN_ROOT` environment variable is set to `/app/exec`.

## Private Registry Authentication

To pull images from private Docker registries, configure the following environment variables:

* `LUNAR_DOCKER_REGISTRY_USER` - Username for Docker registry authentication
* `LUNAR_DOCKER_REGISTRY_PASS` - Password or token for Docker registry authentication


# catalogers

Define the catalogers section of lunar-config.yml — scripts that synchronize domain and component metadata from external systems.

* `lunar-config.yml -> catalogers`
* Type: `array`
* Form:

  ```yaml
  - <cataloger-object>
  - <cataloger-object>
  - ...
  ```

Catalogers are used to synchronize software catalog information, such as domains, and components, with external systems.

The catalog information resulting from catalogers is merged in the order in which the catalogers are defined. This means that if two catalogers define the same field (e.g. `owner`) within the same component, the last one will take precedence.

After the cataloger information is merged, the information collected from any [`lunar.yml`](/configuration/lunar-yml) files 🚧 Coming Soon, and any component and domain information from [`lunar-config.yml`](/configuration/lunar-config), is merged to form the final catalog JSON, which is then used by Lunar to form the structure of domains and components internally.

Example catalogers definition:

{% code title="lunar-config.yml" %}

```yaml
catalogers:
  - name: GitHub repos
    runBash: |-
        gh repo list <my-org> --json | ... | \
        lunar catalog --json '.components' -
    hook:
      type: cron
      schedule: "0 2 * * *"
  - name: Backstage sync
    runBash: |-
        curl <curl-options> https://<backstage endpoint>/api/catalog/entities/by-query ... | ... | \
        lunar catalog --json '.components' -
    hook:
      type: cron
      schedule: "0 2 * * *"
  - name: DB sync
    runBash: |-
        psql ... -c 'COPY (select * FROM services WHERE ...) TO STDOUT WITH CSV HEADER' | \
        csvjson --no-header-row | ... | \
        lunar catalog --json '.components' -
    hook:
      type: cron
      schedule: "0 2 * * *"
  - name: Pick up catalog files from some central repo
    runBash: |-
      cat verticals.toml | ... | lunar catalog --json '.domains' -
      cat services.toml | ... | lunar catalog --json '.components' -
    hook:
      type: cron
      schedule: "0 2 * * *"
  - name: Label any repo that has a certain CI pipeline as "production"
    runBash: |-
      if grep -r --include="*.yaml" --include="*.yml" '^name: Deploy$' ./.github/workflows ; then
        lunar catalog component --tag production
      fi
    hook:
      type: component-repo
  - name: Complex operation
    mainBash: ./my-script.sh
    hook:
      type: cron
      schedule: "0 2 * * *"
  - name: Use an external cataloger
    uses: github://third-party/some-cataloger@v1
```

{% endcode %}

## Cataloger

* `lunar-config.yml -> catalogers.<cataloger-index>`
* Type: `object`
* Forms
  * Uses form (use an external cataloger plugin):

    ```yaml
    name: <cataloger-name>
    description: <description>
    uses: <cataloger-repo>
    include: <include-array>
    exclude: <exclude-array>
    with:
        <input-name>: <input-value>
        ...
    image: <docker-image>
    ```
  * Run form (define a cataloger inline):

    ```yaml
    name: <cataloger-name>
    description: <description>
    run<language>: <script>
    hook: <hook-configuration>
    hooks:
      - <hook-configuration>
      - <hook-configuration>
      - ...
    image: <docker-image>
    ```
  * Main form (define a cataloger inline with a main file):

    ```yaml
    name: <cataloger-name>
    description: <description>
    main<language>: <script>
    hook: <hook-configuration>
    hooks:
      - <hook-configuration>
      - <hook-configuration>
      - ...
    image: <docker-image>
    ```

Catalogers are used to dynamically import information about components and domains from external systems. They are run on a schedule or in response to certain events, such as a commit to a repository.

Catalogers can either be imported (Uses form), defined as an inline command (Run form), or defined as a script with a main file (Main form). When a cataloger is defined inline (Run and Main forms), a hook must be specified to determine when the cataloger should run.

### `name`

* `lunar-config.yml -> catalogers.<cataloger-index>.name`
* Type: `string`
* Required for Run and Main cataloger forms, Optional for Uses cataloger form

The name of the cataloger. This is used to identify the cataloger in the Lunar UI and logs.

### `description`

* `lunar-config.yml -> catalogers.<cataloger-index>.description`
* Type: `string`
* Optional

A description of the cataloger. This is used to describe the purpose of the cataloger in the Lunar UI and logs.

### `uses`

* `lunar-config.yml -> catalogers.<cataloger-index>.uses`
* Type: `string`
* Forms
  * GitHub form: `github://<org>/<repo>@<version>`
  * Local form: `./<path-to-cataloger>`
* Required for Uses cataloger form

The `uses` field specifies an external (plugin) cataloger to use. The cataloger can be a third-party cataloger, or a local cataloger defined in a subdirectory. Browse the [available integrations](https://earthly.dev/lunar/integrations/) to find catalogers for your tools.

### `with`

* `lunar-config.yml -> catalogers.<cataloger-index>.with`
* Type: `object`
* Optional

The `with` field specifies the inputs to pass to the cataloger plugin. The inputs are defined in the cataloger's configuration file. Input values are available to cataloger scripts as `LUNAR_VAR_*` environment variables.

Plugin authors can also reference inputs in their plugin YAML definitions using the `${{ inputs.NAME }}` syntax.

A consumer can then override the schedule via `with`:

{% code title="lunar-config.yml" %}

```yaml
catalogers:
  - name: my-sync
    uses: github://my-org/sync-plugin@v1
    with:
      schedule: "*/10 * * * *"
```

{% endcode %}

See [cataloger plugins](/plugin-sdks/plugins/cataloger-plugins#inputs) for more details on defining plugin inputs.

### `include`

* `lunar-config.yml -> catalogers.<cataloger-index>.include`
* Type: `array`
* Optional

The `include` field specifies which sub-catalogers to include from an imported cataloger plugin. When a cataloger is imported via `uses`, it may define (or import) multiple sub-catalogers. Use `include` to control which of those sub-catalogers are used.

If neither `include` nor `exclude` is specified, all sub-catalogers are included by default.

### `exclude`

* `lunar-config.yml -> catalogers.<cataloger-index>.exclude`
* Type: `array`
* Optional

The `exclude` field specifies which sub-catalogers to exclude from an imported cataloger plugin. Use `exclude` when you want to include most sub-catalogers but skip a few specific ones.

If neither `include` nor `exclude` is specified, all sub-catalogers are included by default.

For example, if a cataloger called `backstage` includes sub-catalogers named `components`, `domains`, and `users`:

{% code title="lunar-config.yml" %}

```yaml
catalogers:
  # Include only the components sub-cataloger
  - uses: ./dir/backstage
    include: [components]

  # Include all except users
  - uses: ./dir/backstage
    exclude: [users]

  # Include components and domains only
  - uses: ./dir/backstage
    include: [components, domains]
```

{% endcode %}

### `run<language>`

* `lunar-config.yml -> catalogers.<cataloger-index>.run<language>`
* Type: `string`
* Required in Run cataloger form

Defines the command to execute when the cataloger is invoked. Only `Bash` is supported currently. So `runBash` is the only valid field.

Running Bash supports [installing dependencies](/plugin-sdks/bash-sdk/dependencies).

#### `runBash`

* `lunar-config.yml -> catalogers.<cataloger-index>.runBash`
* Type: `string`

The `runBash` field specifies the bash cataloger script to run.

### `main<language>`

* `lunar-config.yml -> catalogers.<cataloger-index>.main<language>`
* Type: `string`
* Required in Main cataloger form

Defines the main file path used to execute when the cataloger is invoked. Only `Bash` is supported currently. So `mainBash` is the only valid field.

The file path is relative to the root of the Lunar configuration repository. In the case of an external plugin definition, the path is relative to the plugin directory.

Running Bash supports [installing dependencies](/plugin-sdks/bash-sdk/dependencies).

#### `mainBash`

* `lunar-config.yml -> catalogers.<cataloger-index>.mainBash`
* Type: `string`

The `mainBash` field specifies the bash cataloger script to run.

### `hook`

* `lunar-config.yml -> catalogers.<cataloger-index>.hook`
* Type: `object`
* One of `hook` or `hooks` is required in Run and Main cataloger forms

Using `hook` is equivalent to using a single hook in the `hooks` field. The `hook` field specifies when the cataloger should run.

For more information on how to configure hooks, see [hooks](/configuration/lunar-config/cataloger-hooks).

### `hooks`

* `lunar-config.yml -> catalogers.<cataloger-index>.hooks`
* Type: `array`
* One of `hook` or `hooks` is required in Run and Main cataloger forms

The `hooks` field specifies when the cataloger should run. Use this form when a cataloger needs multiple hooks.

For more information on how to configure hooks, see [hooks](/configuration/lunar-config/cataloger-hooks).

### `image`

* `lunar-config.yml -> catalogers.<cataloger-index>.image`
* Type: `string`
* Optional

The `image` field specifies the Docker image to use when running the cataloger. When set, the cataloger runs inside a container instead of natively on the host.

Use the special value `native` to explicitly run the cataloger without a container, even when a default image has been configured.

Example:

{% code title="lunar-config.yml" %}

```yaml
catalogers:
  # Run in a container
  - name: Sync from external system
    runBash: curl https://api.example.com/services | lunar catalog --json '.components' -
    image: earthly/lunar-scripts:1.0.0
    hook:
      type: cron
      schedule: "0 2 * * *"

  # Run natively (override any default image)
  - name: Local sync
    mainBash: ./sync-local.sh
    image: native
    hook:
      type: cron
      schedule: "0 3 * * *"
```

{% endcode %}

For more information about default images and container execution, see [Images](/configuration/lunar-config/images).


# catalogers/hooks

Configure cataloger hooks in lunar-config.yml — triggers like cron schedules or repository events that determine when catalogers run.

* `lunar-config.yml -> catalogers.<cataloger-index>.hook`
* `lunar-config.yml -> catalogers.<cataloger-index>.hooks`
* `lunar-cataloger.yml -> catalogers.<cataloger-index>.hook`
* `lunar-cataloger.yml -> catalogers.<cataloger-index>.hooks`
* Type: `object` (singular `hook`) or `array` (plural `hooks`)
* Form:

  ```yaml
  hook:
    type: <hook-type>
    <options>
  ```

  or

  ```yaml
  hooks:
    - type: <hook-type>
      <options>
    - type: <hook-type>
      <options>
    - ...
  ```

A cataloger hook defines a trigger point for when a cataloger should run. Catalogers can be triggered by various events such as code changes, or cron schedules. Both `hook` (singular) and `hooks` (plural array) are supported. Using `hook` is equivalent to using a single hook in the `hooks` field.

A hook has different configuration options depending on the type of event it is triggered by.

## Hook types

### `cron`

* Form:

  ```yaml
  type: cron
  schedule: <cron-schedule>
  ```

The `cron` type triggers the cataloger on a specified schedule. The schedule is defined using a cron expression.

### `repo`

* Form:

  ```yaml
  type: repo
  repo: github://<org>/<repo>
  ```

The `repo` type triggers the cataloger when a commit is made to a specified repository. The repository is defined using the GitHub URL format. This cataloger type is most useful for centralized repositories that contain information about domains and/or components.

### `component-repo`

* Form:

  ```yaml
  type: component-repo
  clone-code: true # optional
  ```

The `component-repo` type triggers the cataloger when a commit is made to a component repository. This cataloger type is most useful when additional information about components is available in each of the respective repositories.

Although this cataloger type cannot be used to define new components, it can be used to augment the metadata (such as owner, description and tags) associated with existing components.

Set `clone-code: true` to have the component's repository checked out before the cataloger runs, so it reads repo-resident files (for example `catalog-info.yaml`, `CODEOWNERS`, or `lunar.yml`) directly from a working tree rather than fetching them through an API. The checkout is taken at the pushed commit for pushes to the repository's default branch — the authoritative state the cataloger augments component metadata from; pushes to other branches (and branch deletions) run the cataloger without a checkout. Without `clone-code` the cataloger receives only the component identifier and no checkout.

### `component-cron`

* Form:

  ```yaml
  type: component-cron
  schedule: <cron-schedule>
  clone-code: true # optional
  ```

The `component-cron` type triggers a cataloger run for each component, on a specified schedule. Although this cataloger type cannot be used to define new components, it can be used to augment the metadata (such as owner, description and tags) based on its component JSON.

Set `clone-code: true` to have each component's repository checked out at its current default-branch HEAD before the cataloger runs, so it reads repo-resident files (for example `catalog-info.yaml`, `CODEOWNERS`, or `lunar.yml`) directly from a working tree rather than fetching them through an API. A component whose repository isn't tracked yet, or has no commit ingested, runs without a checkout. Without `clone-code` the cataloger receives only the component identifier and no checkout.


# domains

Define the domains section of lunar-config.yml — hierarchical groupings that organize related components for ownership and policy targeting.

* `lunar-config.yml -> domains`
* Type: `object`
* Form:

  ```yaml
  domains:
    <domain-path>: <domain-object>
    <domain-path>: <domain-object>
    ...
  ```

Domains are used to group related components together. Domains are hierarchical and can contain other domains.

A domain's path is a string that uniquely identifies the domain. If a domain `bar` is within the domain `foo`, then the domain path is `foo.bar`.

Components under a domain receive the special tag `domain:<domain-name>` automatically. For example, components in the domain `foo.bar` will receive the tag `domain:foo.bar`. This tag can be used in [tag matching expressions](/configuration/lunar-config/on) to target components by domain.

Example domains defintion:

{% code title="lunar-config.yml" %}

```yaml
domains:
  saas-product:
    description: Acme's SaaS product
    owner: roberto@example.com
  saas-product.frontend:
    description: The frontend of Acme's SaaS product
    owner: jacqueline@example.com
  saas-product.frontend.ui-components:
    description: Common UI components for the frontend
    owner: jill@example.com
  saas-product.frontend.ui-common:
    description: Common UI code for the frontend
    owner: jane@example.com
  saas-product.frontend.ui-dashboard:
    description: The dashboard for the frontend
    owner: mary@example.com
  saas-product.frontend.ui-login:
    description: The login page for the frontend
    owner: jessica@example.com
  saas-product.backend:
    description: The backend of Acme's SaaS product
    owner: john@example.com
  saas-product.backend.auth:
    description: The authentication service for the backend
    owner: jesse@example.com
  saas-product.backend.rev-proxy:
    description: The reverse proxy for the backend
    owner: mike@example.com
  saas-product.backend.gateway:
    description: The gateway for the backend
    owner: larry@example.com
  widget-product:
    description: Acme's widget product
    owner: noah@example.com
  widget-product.frontend:
    description: The frontend of Acme's widget product
    owner: corey@example.com
  widget-product.data-processing:
    description: Data processing for Acme's widget product
    owner: ann@example.com
  common-infra:
    description: Common infrastructure
    owner: alice@example.com
    meta: # 🚧 Coming Soon
      "okta-team": "infra"
  common-infra.monitoring:
    description: Monitoring for common infrastructure
    owner: bob@example.com
  common-infra.logging:
    description: Logging for common infrastructure
    owner: todd@example.com
```

{% endcode %}

Domains are associated with components using the `domain` field in the [component definition](/configuration/lunar-config/components).

## Domain

* `lunar-config.yml -> domains.<domain-path>`
* Type: `object`
* Form:

  ```yaml
  description: <description>
  owner: <email>
  meta:                          # 🚧 Coming Soon
    <meta-key>: <meta-value>
    <meta-key>: <meta-value>
    ...
  ```

A domain is a group of related components. Domains are hierarchical and can contain other domains.

### `description`

* `lunar-config.yml -> domains.<domain-path>.description`
* Type: `string`
* Optional

A description of the domain.

### `owner`

* `lunar-config.yml -> domains.<domain-path>.owner`
* Type: `string`
* Optional

The email address of the owner of the domain.

### `meta` 🚧 Coming Soon

* `lunar-config.yml -> domains.<domain-path>.meta`
* Type: `object`
* Optional

A key-value store of arbitrary metadata for the domain. This metadata is not used by Lunar, but can be used by collectors and policies.


# components

Define the components section of lunar-config.yml — repositories or monorepo subdirectories that Lunar monitors with metadata and tags.

* `lunar-config.yml -> components`
* Type: `object`
* Form:

  ```yaml
  components:
    <component-name>: <component-object>
    <component-name>: <component-object>
    ...
  ```

Components are the individual units of code that are monitored by Lunar. They represent a complete software deliverable, such as a microservice, binary, or a library. Components can be an entire repository, or a subdirectory within a repository in the case of monorepos.

The name of a component is the repository URL or a pattern that matches multiple repositories. For example, `github.com/my-org/my-repo` or `github.com/my-org/*`.

Each component automatically receives the special tag `component:<component-id>`. For example, the component `github.com/my-org/my-repo` will receive the tag `component:github.com/my-org/my-repo`. This tag can be used in [tag matching expressions](/configuration/lunar-config/on) to target specific components.

Components can be defined here, in the Lunar configuration file, or in a separate file, `lunar.yml`, in the root of the component directory. Both definitions can co-exist, complementing each other (some components can be defined centrally in `lunar-config.yml`, while others can be defined via `lunar.yml`).

Example components definition:

{% code title="lunar-config.yml" %}

```yaml
components:
  github.com/my-org/my-repo:
    owner: jane@example.com
    domain: widget-product.frontend
    branch: prod
    tags: [go, backend, pii]
  github.com/my-org/my-monorepo/*:
    tags: [tier1]
  github.com/my-org/my-monorepo/proj1:
    owner: jacqueline@example.com
    domain: widget-product.data-processing
    tags: [java, backend]
    meta: # 🚧 Coming Soon
      "pagerduty-escalation-policy": "P1"
  github.com/my-org/ui-*:
    tags: [frontend]
  github.com/my-org/ui-components:
    owner: jack@example.com
    domain: ui-common
    tags: [react, typescript]
```

{% endcode %}

## Component

* `lunar-config.yml -> components.<component-name>`
* Type: `object`
* Form:

  ```yaml
  owner: <email>
  domain: <domain-path>
  branch: <branch-name>
  tags: [<tag>, <tag>, ...]
  ciPipelines: [<ci-pipeline>, <ci-pipeline>, ...]
  description: <description>
  paths: [<path>, <path>, ...]
  meta:                          # 🚧 Coming Soon
    <meta-key>: <meta-value>
    <meta-key>: <meta-value>
    ...
  ```

A single component is a unit of code that is monitored by Lunar. It represents a complete software deliverable, such as a microservice, binary, or a library.

As an alternative to defining components in `lunar-config.yml`, they may also be defined in a separate file, `lunar.yml`, in the root of the component directory. The fields in `lunar.yml` are the same as those in `lunar-config.yml -> components.<component-name>`. For more information, see the [lunar.yml](/configuration/lunar-yml) page.

### `owner`

* `lunar-config.yml -> components.<component-name>.owner`
* Type: `string`
* Optional

The email address of the owner of the component.

### `domain`

* `lunar-config.yml -> components.<component-name>.domain`
* Type: `string`
* Optional

A component can only belong to one domain. This field specifies the domain that the component belongs to.

To associate a component with a subdomain, specify the entire domain path. For example, to associate a component with the domain `bar`, which is under the domain `foo`, use the domain `foo.bar`.

If a domain is not specified, the component is placed in the `other` domain.

When a component is associated with a domain, it automatically gets the tag `domain:<domain-name>`. See [Tag Matching with `on`](/configuration/lunar-config/on) for more details on how to use these tags.

### `branch`

* `lunar-config.yml -> components.<component-name>.branch`
* Type: `string`
* Optional

The branch that the component is monitored on. If not specified, the default branch is used.

### `tags`

* `lunar-config.yml -> components.<component-name>.tags`
* Type: `array`
* Optional

A list of tags that to apply to the component. Tags can be used to associate collectors and policies to specific components.

### `ciPipelines`

* `lunar-config.yml -> components.<component-name>.ciPipelines`
* Type: `array`
* Optional - defaults to all CI pipelines in the repository

A list of CI pipeline names that are associated with the component. The CI pipelines are used to trigger the collection of data for the component. A single CI pipeline may be associated with multiple components at a time. If no CI pipelines are specified, then all CI pipelines within the repository are associated with the component.

This setting can be useful in monorepos, when certain CI pipelines might not be relevant to a specific component.

The pipeline name in GitHub Actions is the name of the GitHub Actions **workflow** — the `name:` field of the workflow file (exposed as `GITHUB_WORKFLOW`), not an individual job. In Buildkite, it is the name of the Buildkite pipeline.

### `description`

* `lunar-config.yml -> components.<component-name>.description`
* Type: `string`
* Optional

A description of the component.

### `paths`

* `lunar-config.yml -> components.<component-name>.paths`
* Type: `array`
* Optional

A list of paths within the repository that are associated with the component. This is useful in monorepo setups to specify which subdirectories belong to a given component.

Paths are matched against each changed file path. An entry ending in `*` is a **prefix match** — `services/api/*` matches any file at or under `services/api/` — while an entry without a trailing `*` must equal the changed path **exactly** (e.g. `go.mod`). Only a single trailing `*` is honored; full globs such as `**` are **not** supported. A component named after a monorepo subdirectory also gets an implicit `<subdir>/*` pattern automatically.

### `meta` 🚧 Coming Soon

* `lunar-config.yml -> components.<component-name>.meta`
* Type: `object`
* Optional

A key-value store of arbitrary metadata for the component. This metadata is not used by Lunar, but can be used by collectors and policies.

## CI → component attribution

When the Lunar CI agent traces a CI run, it attributes the facts it collects to one or more components. In a monorepo, a single repository contains many components, so a CI run usually needs to map to a specific subdirectory component. Attribution is controlled by environment variables set in your CI workflow, together with the [`ciPipelines`](#cipipelines) and [`paths`](#paths) settings above.

### `LUNAR_COMPONENT`

Explicitly names the component(s) a CI run belongs to. Use a comma-separated list to attribute a single run to multiple components at once. Each entry is either:

* **repo-relative** — the subdirectory path, e.g. `services/api`; or
* **absolute** — the full component name, e.g. `github.com/my-org/my-monorepo/services/api`.

{% code title=".github/workflows/ci.yml" %}

```yaml
jobs:
  build-api:
    env:
      LUNAR_COMPONENT: "services/api,services/worker"
```

{% endcode %}

### `LUNAR_COMPONENT_INFER`

{% hint style="warning" %}
**Experimental.** Automatic inference is best-effort and has known cases where it can't resolve a component reliably — ambiguous or empty changed-path matches, working directories that don't map cleanly to a single component, and similar. For dependable attribution, prefer explicit `LUNAR_COMPONENT` or [`ciPipelines`](#cipipelines). Behavior may change.
{% endhint %}

Set to `true` to let the agent infer the component(s) automatically instead of naming them. Inference resolves in this order:

1. **Changed paths** — components whose [`paths`](#paths) intersect the files changed by the commit or pull request. (On pull requests the changed files come from the PR; on pushes, from the commit range.)
2. **Working directory** — if no changed paths match, the component whose subdirectory contains the traced command's working directory.

{% code title=".github/workflows/ci.yml" %}

```yaml
jobs:
  build:
    env:
      LUNAR_COMPONENT_INFER: "true"
```

{% endcode %}

### Precedence

`LUNAR_COMPONENT` (explicit) and [`ciPipelines`](#cipipelines) (workflow-name match) take precedence. `LUNAR_COMPONENT_INFER` is only consulted when neither of those selects a component. If nothing matches, the run is attributed to the repository-level component, when one is defined.


# collectors

Define the collectors section of lunar-config.yml — scripts that run on CI, code, or cron triggers to gather component metadata.

* `lunar-config.yml -> collectors`
* Type: `array`
* Form:

  ```yaml
  collectors:
    - <collector-object>
    - <collector-object>
    - ...
  ```

Collectors are used to collect live information from various sources to associate with individual components.

Example collectors definition:

{% code title="lunar-config.yml" %}

```yaml
collectors:
  - uses: github://third-party/some-collector@v1
    on: [auth, frontend]
    hook:
      type: ci-before-command
      pattern: ^go build.*
  - uses: ./my-collector
    on: ["domain:my-domain"]
    hook:
      type: ci-before-job
      pattern: .*
  - uses: ./my-collector
    on: ["domain:foo-product"]
    hook:
      type: code
  - uses: ./another-collector
    on: [go]
    hook:
      type: cron
      schedule: "0 2 * * *"
  - name: Hello world collector
    runBash: lunar collect '.hello' world
    on: [java]
    hook: 
      type: ci-after-command
      pattern: ^mvn install.*
  - name: Example script collector
    mainBash: ./my-script.sh
    on: [python]
    hook:
      type: cron
      schedule: "0 2 * * *"
```

{% endcode %}

## Collector

* `lunar-config.yml -> collectors.<collector-index>`
* Type: `object`
* Forms
  * Uses form (use an external collector plugin):

    ```yaml
    name: <collector-name>
    description: <description>
    uses: <collector-string>
    include: <include-array>
    exclude: <exclude-array>
    with:
      <input-name>: <input-value>
      ...
    on: <domain-array>
    runs_on: <runs-on-array>
    image: <docker-image>
    ```
  * Run form (define a collector inline):

    ```yaml
    name: <collector-name>
    description: <description>
    run<language>: <command-string>
    on: <domain-array>
    runs_on: <runs-on-array>
    image: <docker-image>
    hook: <hook-configuration>
    hooks:
      - <hook-configuration>
      - <hook-configuration>
      - ...
    ```
  * Main form (define a collector inline with a main file):

    ```yaml
    name: <collector-name>
    description: <description>
    main<language>: <main-file-path>
    on: <domain-array>
    runs_on: <runs-on-array>
    image: <docker-image>
    hook: <hook-configuration>
    hooks:
      - <hook-configuration>
      - <hook-configuration>
      - ...
    ```

Collectors are used to collect live information from various sources to associate with individual components. Collectors can be used to instrument CI/CD pipelines, run cron jobs, or execute arbitrary logic when code changes.

Collectors can either be imported (Uses form), defined as an inline command (Run form), or defined as a script to run (Main form). When a collector is defined inline (Run and Main forms), a hook must be specified to determine when the collector should run.

### `name`

* `lunar-config.yml -> collectors.<collector-index>.name`
* Type: `string`
* Required for Run and Main collector forms, Optional for Uses collector form

The `name` field is used to specify the name of the collector. If a name is not provided in the case of a collector plugin, the name from the collector plugin is used. The name must be unique within the configuration.

### `description`

* `lunar-config.yml -> collectors.<collector-index>.description`
* Type: `string`
* Optional

A description of the collector. This is used to describe the purpose of the collector in the Lunar UI and logs.

### `uses`

* `lunar-config.yml -> collectors.<collector-index>.uses`
* Type: `string`
* Forms
  * GitHub form: `github://<org>/<repo>@<version>`
  * Local form: `./<path-to-collector>`
* Required in Uses collector form

The `uses` field specifies an external (plugin) collector to use. The collector can be a third-party collector, or a local collector defined in a subdirectory. Browse the [30+ available integrations](https://earthly.dev/lunar/integrations/) to find collectors for your tools.

### `with`

* `lunar-config.yml -> collectors.<collector-index>.with`
* Type: `object`
* Optional

The `with` field specifies the inputs to pass to the collector plugin. The inputs are defined in the collector's configuration file. Input values are available to collector scripts as `LUNAR_VAR_*` environment variables.

Plugin authors can also reference inputs in their plugin YAML definitions using the `${{ inputs.NAME }}` syntax. This allows plugins to expose configurable fields -- such as hook parameters -- as explicit settings. See [collector plugins](/plugin-sdks/plugins/collector-plugins#inputs) for details.

### `include`

* `lunar-config.yml -> collectors.<collector-index>.include`
* Type: `array`
* Optional

The `include` field specifies which subcollectors to include from an imported collector plugin. When a collector is imported via `uses`, it may define (or import) multiple subcollectors. Use `include` to control which of those subcollectors are used.

If neither `include` nor `exclude` is specified, all subcollectors are included by default.

### `exclude`

* `lunar-config.yml -> collectors.<collector-index>.exclude`
* Type: `array`
* Optional

The `exclude` field specifies which subcollectors to exclude from an imported collector plugin. Use `exclude` when you want to include most subcollectors but skip a few specific ones.

If neither `include` nor `exclude` is specified, all subcollectors are included by default.

For example, if a collector called `go` includes subcollectors named `version`, `dependencies`, and `build-info`:

{% code title="lunar-config.yml" %}

```yaml
collectors:
  # Include only the version subcollector
  - uses: ./dir/go
    include: [version]

  # Include all except version
  - uses: ./dir/go
    exclude: [version]

  # Include version and dependencies only
  - uses: ./dir/go
    include: [version, dependencies]
```

{% endcode %}

### `run<language>`

* `lunar-config.yml -> collectors.<collector-index>.run<language>`
* Type: `string`
* Required in Run collector form

Defines the command to execute when the collector is invoked. Only `Bash` and `Python` are supported. So `runBash` and `runPython` are the only valid fields.

Running Bash supports [installing dependencies](/plugin-sdks/bash-sdk/dependencies).

#### `runBash`

* `lunar-config.yml -> collectors.<collector-index>.runBash`
* Type: `string`

The `runBash` field specifies the bash collector script to run.

#### `runPython`

* `lunar-config.yml -> collectors.<collector-index>.runPython`
* Type: `string`

The `runPython` field specifies the python collector script to run. Running Python supports [installing dependencies](/plugin-sdks/python-sdk/dependencies).

### `main<language>`

* `lunar-config.yml -> collectors.<collector-index>.main<language>`
* Type: `string`
* Required in Main collector form

Defines the main file path used to execute when the collector is invoked. Only `Bash` and `Python` are supported. So `mainBash` and `mainPython` are the only valid fields.

The file path is relative to the root of the Lunar configuration repository. In the case of an external plugin definition, the path is relative to the plugin directory.

Running Bash supports [installing dependencies](/plugin-sdks/bash-sdk/dependencies).

#### `mainBash`

* `lunar-config.yml -> collectors.<collector-index>.mainBash`
* Type: `string`

The `mainBash` field specifies the path to the bash main file to run.

#### `mainPython`

* `lunar-config.yml -> collectors.<collector-index>.mainPython`
* Type: `string`

The `mainPython` field specifies the path to the python main file to run. Running Python supports [installing dependencies](/plugin-sdks/python-sdk/dependencies).

### `on`

* `lunar-config.yml -> collectors.<collector-index>.on`
* Type: `array`
* Required

The `on` field specifies the tags that the collector should be associated with. The collector will only run when the component has one or more of the specified tags.

For detailed documentation on tag matching syntax, including domain/component targeting, expressions, and cross-references to other collectors or policies, see [Tag Matching with `on`](/configuration/lunar-config/on).

### `runs_on`

* `lunar-config.yml -> collectors.<collector-index>.runs_on`
* Type: `array`
* Optional
* Default:
  * `[prs, default-branch]` if the collector has a non-`cron` hook
  * `[default-branch]` if the collector's only hook is `cron`

Specifies the contexts in which the collector should run. The available values are:

* `prs` - the collector will run on pull requests
* `default-branch` - the collector will run on the default branch

By default, collectors run in both contexts. To restrict a collector to only run on pull requests, use `runs_on: [prs]`. To restrict a collector to only run on the default branch, use `runs_on: [default-branch]`.

For a collector whose only hook is a [`cron` hook](/configuration/lunar-config/collector-hooks#cron), the default is `[default-branch]` and [pull-request runs](/configuration/lunar-config/collector-hooks#running-on-pull-requests) are opt-in.

### `hook`

* `lunar-config.yml -> collectors.<collector-index>.hook`
* Type: `object`
* One of `hook` or `hooks` is required in Run collector form

Using `hook` is equivalent to using a single hook in the `hooks` field. The `hook` field specifies when the collector should run.

For more information about hook definitions see the [hooks configuration page](/configuration/lunar-config/collector-hooks).

### `hooks`

* `lunar-config.yml -> collectors.<collector-index>.hooks`
* Type: `array`
* One of `hook` or `hooks` is required in Run collector form

The `hooks` field specifies when the collector should run.

For more information about hook definitions see the [hooks configuration page](/configuration/lunar-config/collector-hooks).

### `image`

* `lunar-config.yml -> collectors.<collector-index>.image`
* Type: `string`
* Optional

The `image` field specifies the Docker image to use when running the collector. When set, the collector runs inside a container instead of natively on the host.

Use the special value `native` to explicitly run the collector without a container, even when a default image has been configured.

Example:

{% code title="lunar-config.yml" %}

```yaml
collectors:
  # Run in a container
  - runBash: lunar collect .file-count "$(find . | wc -l)"
    image: earthly/lunar-scripts:1.0.0
    hook:
      type: code
    on: [my-tag]

  # Run natively (override any default image)
  - runBash: lunar collect .ci-info "$CI_JOB_ID"
    image: native
    hook:
      type: ci-before-command
      pattern: ^.*
    on: [my-tag]
```

{% endcode %}

For more information about default images and container execution, see [Images](/configuration/lunar-config/images).


# collectors/hooks

Configure collector hooks in lunar-config.yml — triggers like CI commands, code events, or cron schedules that determine when collectors run.

* `lunar-config.yml -> collectors.<collector-index>.hooks`
* `lunar-collector.yml -> collectors.<collector-index>.hooks`
* Type: `array`
* Form:

  ```yaml
  hooks:
    - <hook-configuration>
    - <hook-configuration>
    - ...
  ```

Hooks defines when a collector should run.

Example hooks definition:

```yaml
hooks:
  - type: ci-before-command
    binary:
      name: go
    args:
      - value: build
  - type: code
  - type: cron
    schedule: "0 2 * * *"
```

## Collector Hook

* `lunar-config.yml -> collectors.<collector-index>.hooks.<hook-index>`
* `lunar-collector.yml -> collectors.<collector-index>.hooks.<hook-index>`
* Type: `object`
* Form:

  ```yaml
  type: <hook-type>
  <options>
  ```

A collector hook defines a trigger point for when a collector should run. Collectors can be triggered by various events such as code changes, CI pipeline events, or cron schedules.

Different hooks will cause the collector to execute in different contexts. For example, `ci-*` hooks will execute the collector in the context of the CI pipeline, while `code` and `cron` hooks will execute on a Lunar runner.

A hook has different configuration options depending on the type of event it is triggered by.

### `type`

* `lunar-config.yml -> collectors.<collector-index>.hooks.<hook-index>.type`
* Type: `string`
* Required

The type of hook. The available values are:

* `ci-before-job` - triggers before a CI job
* `ci-after-job` - triggers after a CI job
* `ci-before-step` - triggers before a CI step
* `ci-after-step` - triggers after a CI step
* `ci-before-command` - triggers before a command is executed
* `ci-after-command` - triggers after a command is executed
* `code` - triggers when code changes
* `cron` - triggers on a schedule

## Hook Types

### `ci-before-job` / `ci-after-job`

* Form:

  ```yaml
  type: ci-before-job | ci-after-job
  pattern: <regex-pattern>
  ```

The `ci-before-job` type triggers the collector before a CI job is run. The `ci-after-job` type triggers the collector after a CI job is run. The collector will run if the job name matches the specified regex pattern.

If no pattern is specified, the collector will run before/after every job.

#### `pattern`

* `lunar-config.yml -> collectors.<collector-index>.hooks.<hook-index>.pattern`
* Type: `string`
* Optional
* Default: `.*`

A regex pattern to match against the job name.

### `ci-before-step` / `ci-after-step`

* Form:

  ```yaml
  type: ci-before-step | ci-after-step
  pattern: <regex-pattern>
  ```

The `ci-before-step` type triggers the collector before a CI step is run. The `ci-after-step` type triggers the collector after a CI step is run. The collector will run if the step name matches the specified regex pattern.

If no pattern is specified, the collector will run before/after every step.

#### `pattern`

* `lunar-config.yml -> collectors.<collector-index>.hooks.<hook-index>.pattern`
* Type: `string`
* Optional
* Default: `.*`

A regex pattern to match against the step name.

### `ci-before-command` / `ci-after-command`

* Forms:
  * Simple form:

    ```yaml
    type: ci-before-command | ci-after-command
    binary:
      name: <name-string>
    args:
      - flag: <flag-string>
        value: <value-string>
      - ...
    ```
  * Advanced form:

    ```yaml
    type: ci-before-command | ci-after-command
    binary:
      name[_pattern]: <name-string-or-pattern>
      dir[_pattern]: <dir-string-or-pattern>
      use_path_dirs: <boolean>
    args:
      - flag[_pattern]: <flag-string-or-pattern>
        value[_pattern]: <value-string-or-pattern>
      - ...
    args_pattern: <args-regex-pattern>
    envs:
      - name[_pattern]: <name-string-or-pattern>
        value[_pattern]: <value-string-or-pattern>
      - ...
    include_children_depth: <integer>
    max_process_depth: <integer>
    ```
  * Pattern form (deprecated):

    ```yaml
    type: ci-before-command | ci-after-command
    pattern: <regex-pattern>
    ```

The `ci-before-command` type triggers the collector before a command is run in the CI pipeline. The `ci-after-command` type triggers the collector after a command is run.

The command can be any process within the CI pipeline even if it is wrapped in scripts or called from other commands.

There are two forms for matching commands: the **Simple form** provides a straightforward way to match commands for common use cases, while the **Advanced form** provides additional options for fine-grained control. The Simple form is a subset of the Advanced form. The **Pattern form** is deprecated and may be removed in a future release.

For the hook to trigger, all specified matchers (`binary`, `args`, `envs`) must match. This is an AND operation. To implement OR logic (e.g., matching both `-f` and `--file` flags), use regex patterns with alternation (e.g., `flag_pattern: ^(-f|--file)$`).

#### Matching Limitations

The argument matching algorithm operates on raw argument strings and does not have semantic knowledge of the command being executed. Specifically:

* The `flag` + `value` construct matches consecutive arguments (e.g., `--file foo.txt`) or arguments joined with `=` (e.g., `--file=foo.txt`). It does not know whether a command's flag actually accepts a value.
* In ambiguous cases, incorrect matches may occur. For example, given `mycommand --file --verbose`, a matcher with `flag: --verbose` would match, even though `--verbose` might actually be the value of `--file` (if `--file` accepts any string as its value).
* Positional argument matchers match in order but cannot distinguish between a true positional argument and a flag's value. For example, `value: build` would match both `go build ./...` (where `build` is a subcommand) and `go run ./cmd/mycmd.go --type build` (where `build` is the value of `--type`).

For most common CLI tools and usage patterns, these limitations do not cause issues. However, be aware of edge cases when matching commands with unusual argument structures.

For advanced edge cases, use `args_pattern` instead of `args`.

#### `binary`

* `lunar-config.yml -> collectors.<collector-index>.hooks.<hook-index>.binary`
* Type: `object`
* Optional

Specifies how to match the command's binary. All specified fields must match for the binary to be considered a match.

**`name[_pattern]`**

* `lunar-config.yml -> collectors.<collector-index>.hooks.<hook-index>.binary.name[_pattern]`
* Type: `string`
* Optional

Specifies how to match the binary name. Use `name` for an exact match, or `name_pattern` for a regex pattern. The two fields are mutually exclusive.

**`dir[_pattern]`**

* `lunar-config.yml -> collectors.<collector-index>.hooks.<hook-index>.binary.dir[_pattern]`
* Type: `string`
* Optional

Specifies how to match the binary's directory. Use `dir` for an exact match, or `dir_pattern` for a regex pattern. The two fields are mutually exclusive with each other and with `use_path_dirs`. If none of `dir`, `dir_pattern`, or `use_path_dirs` is provided, the hook matches any directory.

**`use_path_dirs` 🚧 Coming Soon**

* `lunar-config.yml -> collectors.<collector-index>.hooks.<hook-index>.binary.use_path_dirs`
* Type: `boolean`
* Optional

By default (if `dir`, `dir_pattern` or `use_path_dirs` are not provided), the hook matches any directory.

If this field is set to `true`, restricts matching to binaries located in directories that are present in the `PATH` environment variable. Mutually exclusive with `dir` and `dir_pattern`.

#### `args`

* `lunar-config.yml -> collectors.<collector-index>.hooks.<hook-index>.args`
* Type: `array`
* Optional

An array of argument matchers. All specified matchers must match for the hook to trigger.

Each argument matcher can specify a flag and/or a value. The `flag` and `value` fields work together to match arguments in both space-separated form (`--foo bar`) and equals form (`--foo=bar`).

**Ordering rules:**

* Matchers with only `value` (or `value_pattern`) and no `flag` are positional arguments and must be defined in the order they appear in the command.
* Matchers with `flag` (or `flag_pattern`) can be defined in any order, regardless of where they appear in the actual command.

**`flag[_pattern]`**

* `lunar-config.yml -> collectors.<collector-index>.hooks.<hook-index>.args.<arg-index>.flag[_pattern]`
* Type: `string`
* Optional

Specifies how to match the flag name. Use `flag` for an exact match (e.g., `-t`, `--tag`), or `flag_pattern` for a regex pattern (e.g., `^(-f|--file)$` to match both short and long forms). The two fields are mutually exclusive. Flag matchers can be defined in any order.

If neither `flag` nor `flag_pattern` is provided, the matcher is treated as a positional argument and only the value is matched. Positional matchers must be defined in the order they appear in the command.

**Note:** Only use `flag` for actual flags (arguments starting with `-` or `--`). For positional arguments like subcommands (e.g., `get` in `kubectl get pod`), use only `value` without `flag`.

For boolean flags (e.g., `--verbose`), provide only `flag` (or `flag_pattern`) without `value` or `value_pattern`. See the `value[_pattern]` documentation below for details on how boolean flags are matched.

**`value[_pattern]`**

* `lunar-config.yml -> collectors.<collector-index>.hooks.<hook-index>.args.<arg-index>.value[_pattern]`
* Type: `string`
* Optional

Specifies how to match the argument value. Use `value` for an exact match, or `value_pattern` for a regex pattern. The two fields are mutually exclusive.

* If neither `value` nor `value_pattern` is provided, the matcher assumes this is a boolean flag (a flag that takes no value, such as `--verbose`). In this case, `--flag` matches, but `--flag=` and `--flag=anything` do not match. Note that `--flag anything` would match because `anything` is treated as a separate argument, not a value for `--flag`.
* If `value` is set to an empty string (`""`), matches arguments with an explicitly empty value (e.g., `--flag=`), but not boolean flags without a value (e.g., `--flag`).

#### `args_pattern`

* `lunar-config.yml -> collectors.<collector-index>.hooks.<hook-index>.args_pattern`
* Type: `string`
* Optional

A regex pattern to match against all command arguments as a single space-concatenated string. This is an alternative to the `args` array for advanced matching scenarios where the structured `args` matchers are insufficient.

For example, if a command is invoked as `mycommand --flag value arg1 arg2`, the `args_pattern` would match against the string `--flag value arg1 arg2`.

The `args` array and `args_pattern` can be used together; both must match for the hook to trigger.

#### `envs`

* `lunar-config.yml -> collectors.<collector-index>.hooks.<hook-index>.envs`
* Type: `array`
* Optional

An array of environment variable matchers. All specified matchers must match for the hook to trigger.

**`name[_pattern]`**

* `lunar-config.yml -> collectors.<collector-index>.hooks.<hook-index>.envs.<env-index>.name[_pattern]`
* Type: `string`
* Optional

Specifies how to match the environment variable name. Use `name` for an exact match, or `name_pattern` for a regex pattern. The two fields are mutually exclusive.

**`value[_pattern]`**

* `lunar-config.yml -> collectors.<collector-index>.hooks.<hook-index>.envs.<env-index>.value[_pattern]`
* Type: `string`
* Optional

Specifies how to match the environment variable value. Use `value` for an exact match, or `value_pattern` for a regex pattern. The two fields are mutually exclusive.

#### `max_process_depth` 🚧 Coming Soon

* `lunar-config.yml -> collectors.<collector-index>.hooks.<hook-index>.max_process_depth`
* Type: `integer`
* Optional
* Default: infinite

Defines the maximum depth of the process itself in the process tree for which the hook will trigger. For example, if set to `1`, the hook will only trigger on top-level processes in the CI/CD pipeline.

#### `include_children_depth` 🚧 Coming Soon

* `lunar-config.yml -> collectors.<collector-index>.hooks.<hook-index>.include_children_depth`
* Type: `integer`
* Optional
* Default: `0`

If set to a value greater than zero, the hook will also trigger on child processes of the matched command, up to the specified depth. By default (`0`), the hook only triggers on the matched command itself.

#### `pattern` (deprecated)

* `lunar-config.yml -> collectors.<collector-index>.hooks.<hook-index>.pattern`
* Type: `string`
* Required in Pattern form
* Deprecated

A regex pattern to match against the full command line. This form is deprecated; use the Simple or Advanced form instead.

#### Examples

Match `go build` commands:

```yaml
type: ci-before-command
binary:
  name: go
args:
  - value: build
```

Match `kubectl get pod` (positional arguments in order):

```yaml
type: ci-before-command
binary:
  name: kubectl
args:
  - value: get
  - value: pod
```

Match `docker build` with any tag:

```yaml
type: ci-before-command
binary:
  name: docker
args:
  - value: build
  - flag_pattern: ^(-t|--tag)$
    value_pattern: .*
```

Match `npm run`, `npm test`, or `npm build`:

```yaml
type: ci-before-command
binary:
  name: npm
args:
  - value_pattern: ^(run|test|build)$
```

Match `pytest` with verbose flag:

```yaml
type: ci-after-command
binary:
  name: pytest
args:
  - flag: --verbose
```

Match commands with either `-f` or `--file` flag:

```yaml
type: ci-before-command
binary:
  name: mycommand
args:
  - flag_pattern: ^(-f|--file)$
    value_pattern: .*\.txt$
```

Match any Python binary in a specific directory:

```yaml
type: ci-before-command
binary:
  name_pattern: python[0-9]*
  dir: /usr/local/bin
```

Match `go` commands resolved via PATH:

```yaml
type: ci-before-command
binary:
  name: go
  use_path_dirs: true
args:
  - value_pattern: ^(build|test|run)$
```

Match commands with specific environment variable:

```yaml
type: ci-before-command
binary:
  name: make
envs:
  - name: DEBUG
    value: "1"
```

Match using `args_pattern` for advanced argument matching:

```yaml
type: ci-before-command
binary:
  name: terraform
args_pattern: (plan|apply).*-var-file=.*production
```

Match top-level `make` commands and their immediate children:

```yaml
type: ci-before-command
binary:
  name: make
max_process_depth: 1
include_children_depth: 1
```

Match using pattern form (deprecated):

```yaml
type: ci-before-command
pattern: ^go build.*
```

### `code`

* Form:

  ```yaml
  type: code
  ```

The `code` type triggers the collector when the code of the component changes (i.e. there are new commits).

### `cron`

* Form:

  ```yaml
  type: cron
  clone-code: <boolean>
  schedule: <cron-schedule>
  ```

The `cron` type triggers the collector on a cron schedule. The collector will run according to the specified cron schedule.

#### `schedule`

* `lunar-config.yml -> collectors.<collector-index>.hooks.<hook-index>.schedule`
* Type: `string`
* Required

A cron expression specifying when the collector should run (e.g., `"0 2 * * *"` for daily at 2am).

#### `clone-code`

* `lunar-config.yml -> collectors.<collector-index>.hooks.<hook-index>.clone-code`
* Type: `boolean`
* Optional
* Default: `false`

If set to `true`, the collector will execute in the context of the code repository. This means that the collector will have access to a git clone of the code repository to interact with.

#### Running on pull requests

By default a cron collector runs only against the **default branch**: a collector whose only hook is `cron` defaults its [`runs_on`](/configuration/lunar-config/collectors#runs_on) to `[default-branch]`. To also run the collector against the head of every **open pull request** on each scheduled tick, opt in via `runs_on`:

```yaml
collectors:
  - name: nightly-rescan
    runs_on: [prs, default-branch]   # default branch + every open PR head
    runBash: ./rescan.sh
    hook:
      type: cron
      schedule: "0 * * * *"
      clone-code: true
```

* `runs_on: [default-branch]` — default branch only (the default for a cron-only collector).
* `runs_on: [prs]` — open pull request heads only.
* `runs_on: [prs, default-branch]` — both.

Each pull-request run is recorded against that PR's head commit, so the PR's policy results refresh on the schedule. For example, a scheduled dependency check can begin failing a pull request after a vulnerability is disclosed for a dependency it already uses — even though the PR's code hasn't changed. The default-branch run is unaffected.

A collector that declares a `cron` hook **and** a CI/code hook keeps the global `[prs, default-branch]` default, so the CI/code hook's pull-request coverage is preserved; set `runs_on` explicitly to change it.


# initiatives

Define the initiatives section of lunar-config.yml — groupings of policies around shared goals or compliance requirements.

* `lunar-config.yml -> initiatives`
* Type: `array`
* Form:

  ```yaml
  initiatives:
    - name: <initiative-name>
      description: <initiative-description>
      owner: <initiative-owner>
      on: [<tag>, <tag>, ...]
    - name: <initiative-name>
      description: <initiative-description>
      owner: <initiative-owner>
      on: [<tag>, <tag>, ...]
    ...
  ```

Initiatives are used to group policies together around a specific goal or compliance requirement. They provide a way to organize policies and make them easier to manage and monitor.

Example initiatives definition:

{% code title="lunar-config.yml" %}

```yaml
initiatives:
  - name: security
    description: Security policies for all components
    owner: security-team@example.com
    on: [security, backend, frontend]
  - name: performance
    description: Performance optimization policies
    owner: platform-team@example.com
    on: [backend, api]
  - name: documentation
    description: Documentation compliance policies
    owner: docs-team@example.com
    on: [all]
```

{% endcode %}

## Initiative

* `lunar-config.yml -> initiatives.<initiative-index>`
* Type: `object`
* Form:

  ```yaml
  name: <initiative-name>
  description: <initiative-description>
  owner: <initiative-owner>
  on: [<tag>, <tag>, ...]
  ```

An initiative represents a collection of policies organized around a specific goal or purpose. Initiatives make it easier to manage and track related policies.

### `name`

* `lunar-config.yml -> initiatives.<initiative-index>.name`
* Type: `string`
* Required

The name field is used to specify the unique identifier for the initiative. The name `default` is reserved and cannot be used; a built-in "default" initiative is always created automatically for policies that don't specify an initiative.

### `description`

* `lunar-config.yml -> initiatives.<initiative-index>.description`
* Type: `string`
* Optional

The description field is used to provide a human-readable description of the initiative.

### `owner`

* `lunar-config.yml -> initiatives.<initiative-index>.owner`
* Type: `string`
* Optional

The owner field is used to specify the person or team responsible for the initiative. This is typically an email address.

### `on`

* `lunar-config.yml -> initiatives.<initiative-index>.on`
* Type: `array of strings`
* Optional

The `on` field specifies the tags that the initiative applies to. These tags are used to associate policies with the initiative.

For detailed documentation on tag matching syntax, including domain/component targeting, expressions, and cross-references to other collectors or policies, see [Tag Matching with `on`](/configuration/lunar-config/on).


# policies

Define the policies section of lunar-config.yml — rules that Lunar evaluates against components to enforce standards and check health.

* `lunar-config.yml -> policies`
* Type: `array`
* Form:

  ```yaml
  policies:
    - <policy-object>
    - <policy-object>
    - ...
  ```

Policies are used to define the rules that Lunar uses to evaluate the health of components.

Example policies definition:

{% code title="lunar-config.yml" %}

```yaml
policies:
  - uses: github://third-party/some-policy@v1
    on: [my-domain]
    enforcement: block-pr
  - uses: ./security-scanning
    on: [my-domain, another-domain]
    runs_on: [default-branch]
    enforcement: score
  - name: Collect code coverage information
    runPython: |
      from lunar_policy import Check, Path
      with Check("codecov-check", "Verify code coverage was collected") as check:
          check.assert_true(Path(".codecov.was_run"), "Code coverage data should be collected")
    on: [another-domain]
  - name: Should have unit tests
    mainPython: ./unit-tests.py
    on: [another-domain]
    enforcement: block-pr-and-release
```

{% endcode %}

## Policy

* `lunar-config.yml -> policies.<policy-index>`
* Type: `object`
* Forms:
  * Uses form:

    ```yaml
    name: <policy-name>
    uses: <policy-string>
    include: <include-array>
    exclude: <exclude-array>
    with:
      <input-name>: <input-value>
      ...
    on: <domain-array>
    runs_on: <runs-on-array>
    enforcement: <enforcement-level>
    initiative: <initiative-name>
    image: <docker-image>
    ```
  * Run form:

    ```yaml
    name: <policy-name>
    description: <policy-description>
    run<language>: <code-string>
    on: <domain-array>
    runs_on: <runs-on-array>
    enforcement: <enforcement-level>
    initiative: <initiative-name>
    image: <docker-image>
    ```
  * Main form:

    ```yaml
    name: <policy-name>
    description: <policy-description>
    main<language>: <main-file-path>
    on: <domain-array>
    runs_on: <runs-on-array>
    enforcement: <enforcement-level>
    initiative: <initiative-name>
    image: <docker-image>
    ```

Policies are used to define the rules that Lunar uses to evaluate the health of components. Policies are associated with domains and are automatically inherited by child domains.

### `name`

* `lunar-config.yml -> policies.<policy-index>.name`
* Type: `string`
* Required for Run and Main policy forms, Optional for Uses policy form

The `name` field is used to specify the name of the policy. If a name is not provided in the case of a policy plugin, the name from the policy plugin is used. The name must be unique within the configuration.

### `uses`

* `lunar-config.yml -> policies.<policy-index>.uses`
* Type `string`
* Forms
  * GitHub form: `github://<owner>/<repo>@<version>`
  * Local form: `./<path-to-policy>`
* Required in Uses policy form

The `uses` field is used to import an external (plugin) policy from a GitHub repository or a local file. The policy is then associated with a domain. Browse the [100+ available guardrails](https://earthly.dev/lunar/guardrails/) to find policies for your standards.

### `with`

* `lunar-config.yml -> policies.<policy-index>.with`
* Type: `object`
* Optional

The `with` field specifies the inputs to pass to the policy plugin. The inputs are defined in the policy's configuration file. Input values are available to policy scripts via the `variable_or_default` function from the `lunar_policy` SDK.

Plugin authors can also reference inputs in their plugin YAML definitions using the `${{ inputs.NAME }}` syntax. This allows plugins to expose configurable fields as explicit settings. See [policy plugins](/plugin-sdks/plugins/policy-plugins#inputs) for details.

### `include`

* `lunar-config.yml -> policies.<policy-index>.include`
* Type: `array`
* Optional

The `include` field specifies which sub-policies to include from an imported policy plugin. When a policy is imported via `uses`, it may define (or import) multiple sub-policies. Use `include` to control which of those sub-policies are used.

If neither `include` nor `exclude` is specified, all sub-policies are included by default.

### `exclude`

* `lunar-config.yml -> policies.<policy-index>.exclude`
* Type: `array`
* Optional

The `exclude` field specifies which sub-policies to exclude from an imported policy plugin. Use `exclude` when you want to include most sub-policies but skip a few specific ones.

If neither `include` nor `exclude` is specified, all sub-policies are included by default.

For example, if a policy called `security` includes sub-policies named `vulnerability-scan`, `license-check`, and `dependency-audit`:

{% code title="lunar-config.yml" %}

```yaml
policies:
  # Include only the vulnerability-scan sub-policy
  - uses: ./dir/security
    include: [vulnerability-scan]

  # Include all except license-check
  - uses: ./dir/security
    exclude: [license-check]

  # Include vulnerability-scan and license-check only
  - uses: ./dir/security
    include: [vulnerability-scan, license-check]
```

{% endcode %}

### `description`

* `lunar-config.yml -> policies.<policy-index>.description`
* Type: `string`
* Optional

The `description` field is used to specify a description of the policy. If a description is not provided in the case of a policy plugin, the description from the policy plugin is used.

### `run<language>`

* `lunar-config.yml -> policies.<policy-index>.run<language>`
* Type: `string`
* Required in Run policy form

Defines the command to execute when the policy is invoked. Only `Python` is supported. So `runPython` is the only valid field.

Running Python supports [installing dependencies](/plugin-sdks/python-sdk/dependencies).

#### `runPython`

* `lunar-config.yml -> policies.<policy-index>.runPython`
* Type: `string`

The `runPython` field specifies the python policy script to run. Running Python supports [installing dependencies](/plugin-sdks/python-sdk/dependencies).

### `main<language>`

* `lunar-config.yml -> policies.<policy-index>.main<language>`
* Type: `string`
* Required in Main policy form

Defines the main file path used to execute when the policy is invoked. Only `Python` is supported. So `mainPython` is the only valid field.

The file path is relative to the root of the Lunar configuration repository. In the case of an external plugin definition, the path is relative to the plugin directory.

Running Python supports [installing dependencies](/plugin-sdks/python-sdk/dependencies).

#### `mainPython`

* `lunar-config.yml -> policies.<policy-index>.mainPython`
* Type: `string`

The `mainPython` field specifies the path to the python main file to run. Running Python supports [installing dependencies](/plugin-sdks/python-sdk/dependencies).

### `on`

* `lunar-config.yml -> policies.<policy-index>.on`
* Type: `array`
* Required

The `on` field specifies the tags that the policy should be associated with. The policy will apply when the component has one or more of the specified tags.

For detailed documentation on tag matching syntax, including domain/component targeting, expressions, and cross-references to other collectors or policies, see [Tag Matching with `on`](/configuration/lunar-config/on).

### `runs_on`

* `lunar-config.yml -> policies.<policy-index>.runs_on`
* Type: `array`
* Default: `[prs, default-branch]`

Specifies the contexts in which the policy should run. The available values are:

* `prs` - the policy will run on pull requests
* `default-branch` - the policy will run on the default branch

By default, policies run in both contexts. To restrict a policy to only run on pull requests, use `runs_on: [prs]`. To restrict a policy to only run on the default branch, use `runs_on: [default-branch]`.

### `enforcement`

* `lunar-config.yml -> policies.<policy-index>.enforcement`
* Type: `string`. One of `draft`, `score`, `report-pr`, `block-pr`, `block-release`, `block-pr-and-release`
* Optional - defaults to `report-pr`

The `enforcement` field specifies the enforcement level of the policy. It determines how the policy affects the component.

The following enforcement levels are supported:

* `draft` - the policy is still under development and does not affect the score, and is not enforced or shown to application teams
* `score` - the checks under this policy only contribute to the score of the component, but are not reported in PRs
* `report-pr` - the checks under this policy report the results in PRs, but do not block them
* `block-pr` - the checks under this policy block PRs from being merged
* `block-release` - the checks under this policy block releases, but not PRs. This level may be useful for checks that don't necessarily run in PRs due to performance reasons, but are nevertheless important to gate the release process.
* `block-pr-and-release` - the checks under this policy block both PRs and releases

{% hint style="info" %}
When `block-release` or `block-pr-and-release` levels are used, the Lunar CLI command `lunar policy ok-release <component> <git_sha>` will return a non-zero exit code of `1` if the associated policy is failing for the given component. This command may be used in CD or release pipelines to prevent a deployment to production, or a release package to be published.

When `block-pr` or `block-pr-and-release` levels are used, the Lunar CLI command `lunar policy ok-pr <component> <git_sha>` will return a non-zero exit code of `1` if the associated policy is failing for the given component. This command may be used wherever needed to block PR merges or prevent PR deployment pipelines to staging environments.
{% endhint %}

### `initiative`

* `lunar-config.yml -> policies.<policy-index>.initiative`
* Type: `string`
* Optional - defaults to `default`

The `initiative` field specifies the initiative that the policy belongs to. Initiatives are used to group related policies together for easier management and reporting. If not specified, the policy will be associated with the built-in "default" initiative.

For information on how to configure initiatives, see [initiatives](/configuration/lunar-config/initiatives).

### `image`

* `lunar-config.yml -> policies.<policy-index>.image`
* Type: `string`
* Optional

The `image` field specifies the Docker image to use when running the policy. When set, the policy runs inside a container instead of natively on the host.

Use the special value `native` to explicitly run the policy without a container, even when a default image has been configured.

Example:

{% code title="lunar-config.yml" %}

```yaml
policies:
  # Run in a container
  - uses: ./my-policy
    image: earthly/lunar-scripts:1.0.0
    on: [my-tag]

  # Run natively (override any default image)
  - mainPython: ./local-policy.py
    image: native
    on: [my-tag]
```

{% endcode %}

For more information about default images and container execution, see [Images](/configuration/lunar-config/images).


# on (tag matching)

Reference for the on field in lunar-config.yml — tag-matching expressions used by collectors, policies, and initiatives to target components.

The `on` field is used by collectors, policies, and initiatives to specify which components they apply to. It supports two forms: an array form for simple matching, and an expression form for complex logic.

## Array Form

The array form accepts a list of tags. A component matches if it has **any** of the specified tags (OR logic):

```yaml
on: [tag1, tag2, tag3]
```

## Expression Form

The expression form accepts a string with `AND`, `OR`, and `NOT` operators for complex matching logic:

```yaml
on: "tag1 OR tag2 AND NOT tag3"
```

Only the keywords `AND`, `OR`, and `NOT` are allowed as operators.

### Operator Precedence

Operators are evaluated with standard precedence:

1. `NOT` (highest precedence)
2. `AND`
3. `OR` (lowest precedence)

For example:

* `"a OR b AND c"` is evaluated as `"a OR (b AND c)"`
* `"a AND NOT b"` is evaluated as `"a AND (NOT b)"`

## Special Tags

### Domain Tags

Use `domain:<domain-name>` to match components in a specific domain (and its sub-domains):

```yaml
on: ["domain:engineering"]
```

To match a nested domain, use dot notation:

```yaml
on: ["domain:engineering.payments"]
```

In expression form:

```yaml
on: "domain:engineering AND NOT domain:engineering.experimental"
```

### Component Tags

Use `component:<component-id>` to match a specific component by its identifier:

```yaml
on: ["component:github.com/foo/bar"]
```

### Collector Reference

Use `collector:<collector-name>` to apply to the same components that another collector applies to:

```yaml
on: ["collector:foo"]
```

This is useful when you want one collector or policy to follow the same targeting rules as an existing collector.

### Policy Reference

Use `policy:<policy-name>` to apply to the same components that another policy applies to:

```yaml
on: ["policy:foo"]
```

## Combining Array and Expression Forms

The array form is equivalent to an OR expression. These two are identical:

```yaml
on: [tag1, tag2, tag3]
```

```yaml
on: "tag1 OR tag2 OR tag3"
```

Use the array form for simple OR-based matching, and the expression form when you need AND, NOT, or complex combinations.

## Examples

### All components in a domain

```yaml
on: ["domain:engineering"]
```

### Components with specific tags

```yaml
on: [backend, api]
```

### All components except those with a specific tag

```yaml
on: "NOT internal"
```

Matches all components that do **not** have the `internal` tag.

### All except a specific component

```yaml
on: "NOT component:github.com/foo/bar"
```

Matches all components except the one specified.

### Domain with exclusions

```yaml
on: "domain:engineering AND NOT domain:engineering.payments"
```

Matches all components in the `engineering` domain and its sub-domains, **except** those in the `engineering.payments` sub-domain.

### Include back after exclusion

```yaml
on: "NOT internal OR soc2"
```

This matches:

* All components that are **not** tagged `internal`, OR
* All components tagged `soc2`

This means an `internal` component that is **also** tagged `soc2` **will** be included.

### Components that must have multiple tags

```yaml
on: "production AND soc2"
```

Matches components that have **both** the `production` and `soc2` tags.

### Domain filtering with required tag

```yaml
on: "domain:engineering AND NOT domain:engineering.experimental AND production"
```

Matches components that:

1. Are in the `engineering` domain (or its sub-domains)
2. Are **not** in the `engineering.experimental` sub-domain
3. Have the `production` tag

### Complex targeting across domains

```yaml
on: "domain:engineering.payments OR domain:engineering.api AND soc2"
```

Matches components in `engineering.payments` (regardless of other tags) OR components in `engineering.api` that also have the `soc2` tag.


# Validating your config

Validate lunar-config.yml at commit/PR time — before merge — with a Hub-less dry run that runs the same checks the Hub does, plus a generated JSON Schema.

`lunar hub pull` validates configuration on the Hub, but by then the change has already merged. To catch mistakes **before** they merge, validate the config in CI on the pull request.

There are two complementary tools:

1. **`lunar hub pull --dry-run`** — the authoritative check, meant to run in CI on your pull requests. It runs the Hub's own load-and-validate steps against the same code (load the config, resolve every `uses:` plugin, validate the whole manifest, and resolve each component's default branch) and stops before applying anything — so it catches the mistakes a real `hub pull` would reject after merge, without a separate re-implementation that could drift.
2. **A JSON Schema** — generated from the manifest definition, for editor autocomplete and inline structural feedback while you type.

{% hint style="info" %}
The dry run needs **no Hub connection**. It does need **GitHub access** (a `LUNAR_GITHUB_TOKEN`, or a configured Hub used only as an auth source) to resolve `uses:` plugins — the same access the Hub itself uses to fetch them. This is what lets it catch plugin-resolution problems that a purely structural check would miss.
{% endhint %}

## `lunar hub pull --dry-run`

```bash
lunar hub pull --dry-run <repo>
```

`<repo>` is the config repository ref, e.g. `github://acme-corp/lunar@my-branch`. The dry run clones it, loads `lunar-config.yml` (and any [`lunar-config.d/`](/configuration/lunar-config) fragments), resolves every `uses:` plugin, and runs the full manifest validation — required fields, enum values, hook/cron shape, [`on:`](/configuration/lunar-config/on) expressions, [domain](/configuration/lunar-config/domains) references, and whether each referenced plugin actually exists and resolves. It exits non-zero and prints the failure if anything is wrong, and **never** contacts the Hub or applies the manifest.

It runs the Hub's own `PullManifest` load-and-validate code — the `Fetch` pipeline plus the same component default-branch resolution the Hub performs before persisting — so a component pointing at a typo'd or inaccessible repo fails the dry run too, not just a real pull.

{% hint style="warning" %}
**One known gap:** the dry run does not install per-snippet dependencies (the `pip`/`npm` install the Hub runs for inline, non-image snippets), because that needs the runtime images and is too heavy for a CI gate. A dependency-install failure can therefore still surface only on a real `hub pull`. Everything else — structure, plugin resolution, and component branch resolution — is validated.
{% endhint %}

## Pre-merge validation in CI

Run the dry run on every pull request against your config repo. The example below is for **GitHub Actions** — it's provided as a starting point; adapt the CLI install and the invocation to whatever CI system you use. It needs a token that can read the config repo and the plugin repos it references.

It intentionally runs on **all** pull requests rather than only when `lunar-config.yml` changes: the dry run resolves and validates every referenced plugin, including local `./`-path plugins in `collectors/`, `policies/`, or `catalogers/`, so a PR that edits one of those without touching `lunar-config.yml` is still checked. (A `paths:` filter would skip those.)

```yaml
name: Validate Lunar config

on:
  pull_request:

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - name: Install Lunar CLI
        run: |
          curl -fsSL -o lunar "https://github.com/earthly/lunar-dist/releases/latest/download/lunar-linux-amd64"
          chmod +x lunar
          sudo mv lunar /usr/local/bin/lunar
      - name: Dry-run the config
        env:
          LUNAR_GITHUB_TOKEN: ${{ secrets.LUNAR_GITHUB_TOKEN }}
        run: |
          lunar hub pull --dry-run "github://${{ github.repository }}@${{ github.event.pull_request.head.sha }}"
```

{% hint style="info" %}
Pin the CLI to a specific release (e.g. `.../releases/download/v1.2.3/lunar-linux-amd64`) instead of `latest` for reproducible CI. The token needs read access to the config repo and any private plugin repos it `uses:`.
{% endhint %}

## JSON Schema (editor autocomplete)

For editor autocomplete and inline structural validation as you edit, the CLI emits a [JSON Schema](https://json-schema.org/) (draft-07 format) for `lunar-config.yml`, generated from the same manifest definition the Hub uses:

```bash
lunar config schema > lunar-config.schema.json
```

Commit that file and point your editor at it. In VS Code, for example:

```jsonc
// .vscode/settings.json
{
  "yaml.schemas": {
    "./lunar-config.schema.json": ["lunar-config.yml", "lunar-config.d/*.yml"]
  }
}
```

{% hint style="warning" %}
The JSON Schema is a **structural aid only** — it checks unknown/misspelled keys, types, and enum values. It does **not** resolve `uses:` plugins or component branches (the dry run does that), so it can't tell you a plugin or repo doesn't exist. (Neither the schema nor the dry run installs per-snippet dependencies — see the dry-run note above.) Use `lunar hub pull --dry-run` as the authoritative check; treat the schema as editor feedback.

If you also validate against the schema in CI, use a **YAML 1.2** validator such as [`check-jsonschema`](https://check-jsonschema.readthedocs.io/). YAML 1.1 parsers (e.g. Python's `yaml.safe_load`) coerce bare `on:`, `yes:`, `no:`, and `off:` mapping keys into booleans, which spuriously fails schema validation on the `on:` field.
{% endhint %}


# lunar.yml

Reference for lunar.yml, the optional per-repository file that configures a single component's owner, domain, branch, tags, and CI pipelines.

{% hint style="info" %}
**Coming Soon** — This feature is not yet available.
{% endhint %}

* `lunar.yml`
* Type: YAML file
* Form:

  ```yaml
  version: 0

  owner: <email>
  domain: <domain-path>
  branch: <branch-name>
  tags: [<tag>, <tag>, ...]
  ciPipelines: [<ci-pipeline>, <ci-pipeline>, ...]
  ```

This page describes the configuration of a component via lunar.yml.

The file lunar.yml is optional and it can be used to define the configuration of a single component. If lunar.yml is not provided, the component configuration can be defined in the Lunar configuration file, [lunar-config.yml](/configuration/lunar-config). With the exception of the version field, the same fields in `lunar-config.yml -> components.<component-name>` are used in `lunar.yml`.

If a component is defined in both lunar.yml and lunar-config.yml, the settings are merged, and the configuration in lunar-config.yml takes precedence when a scalar field (`owner`, `domain`, `branch`) is defined in both places. The arrays (`tags`, `ciPipelines`) are appended to each other.

## `version`

* `lunar.yml -> version`
* Type: `string`
* Required

The version field is used to specify the version of the component configuration file. The current version is `0`.

## Other Fields

The other fields in `lunar.yml` are the same as those in `lunar-config.yml -> components.<component-name>`. For more information, see the [lunar-config.yml components](/configuration/lunar-config/components) page.


# Plugins configuration

Overview of the three Lunar plugin types — catalogers, collectors, and policies — that extend the platform's capabilities.

Lunar supports three types of plugins:

## Plugin Types

### Catalogers

Catalogers discover and catalog components in your system. They help Lunar understand your architecture by identifying components and their hierarchy.

👉 [Learn more about Cataloger Plugins](/plugin-sdks/plugins/cataloger-plugins)

### Collectors

Collectors gather metrics and data from your components.

👉 [Learn more about Collector Plugins](/plugin-sdks/plugins/collector-plugins)

### Policies

Policies define rules and standards for your components.

👉 [Learn more about Policy Plugins](/plugin-sdks/plugins/policy-plugins)


# lunar-cataloger.yml

Reference for lunar-cataloger.yml, the manifest that defines a cataloger plugin used to sync software catalog information from external systems.

* `lunar-cataloger.yml`
* Type: YAML file
* Form:

  ```yaml
  version: 0

  name: <cataloger-name>
  description: <cataloger-description>
  author: <author-name>

  default_image: <default-image>

  catalogers:
    - <cataloger-object>
    - <cataloger-object>
    - ...

  inputs:
    <input-name>:
      description: <input-description>
      default: <input-default-value>
  ```

This page describes the configuration of a cataloger plugin. Cataloger plugins are used to synchronize software catalog information from external systems. Cataloger plugins can be imported from the Lunar configuration file, `lunar-config.yml` via the `uses` cataloger form.

Cataloger plugins can be defined either in a separate repository or in the same repository as the Lunar configuration, in a dedicated directory. Either way, the cataloger plugin must contain a `lunar-cataloger.yml` file in the root of the repository or directory. This file is used to configure the behavior of the cataloger plugin.

When using catalogers in the Main or Run forms, you can also install dependencies. See [installing dependencies in the Bash SDK](/plugin-sdks/bash-sdk/dependencies) for more details.

## `version`

* `lunar-cataloger.yml -> version`
* Type: `numeric`
* Required

The version field is used to specify the version of the cataloger configuration file. The current version is `0`.

## `name`

* `lunar-cataloger.yml -> name`
* Type: `string`
* Required

The name field is used to specify the name of the cataloger.

## `description`

* `lunar-cataloger.yml -> description`
* Type: `string`
* Optional

The description field is used to specify a description of the cataloger.

## `author`

* `lunar-cataloger.yml -> author`
* Type: `string`
* Optional

The author field is used to specify the author of the cataloger.

## `default_image`

* `lunar-cataloger.yml -> default_image`
* Type: `string`
* Optional

The default Docker image to use for all catalogers in this plugin. This overrides the global `default_image_catalogers` setting from `lunar-config.yml`.

Individual catalogers can still override this plugin default using the `image` field. Use the special value `native` to explicitly run without a container.

For more information about default images and container execution, see [Images](/configuration/lunar-config/images).

## `catalogers`

* `lunar-cataloger.yml -> catalogers`
* Type: `array`
* Required

The catalogers field is used to specify the configuration of the cataloger. The format of a cataloger is the same as in `lunar-config.yml`. To learn more about the configuration of a cataloger, see the [catalogers](/configuration/lunar-config/catalogers) page.

**Note:** When consumers import this plugin via `uses`, they can selectively include or exclude specific catalogers using the [`include`](/configuration/lunar-config/catalogers#include) field. This allows consumers to use only the catalogers they need from a plugin that defines multiple catalogers.

## `inputs`

* `lunar-cataloger.yml -> inputs`
* Type: `object`
* Optional

The inputs field is used to specify the inputs required by the cataloger. Each input is defined as a key-value pair, where the key is the input name.

Inputs are passed to the cataloger when invoked as environment variables with the prefix `LUNAR_VAR_` and the input name in uppercase. For example, an input named `api_url` is accessible as `$LUNAR_VAR_API_URL`.

Inputs can also be referenced in the plugin YAML definition itself using the `${{ inputs.NAME }}` syntax. This allows plugin authors to expose configurable fields as explicit settings. For example:

{% code title="lunar-cataloger.yml" %}

```yaml
inputs:
  schedule:
    description: Cron schedule for syncing
    default: "0 2 * * *"

catalogers:
  - name: org-sync
    mainBash: ./sync.sh
    hook:
      type: cron
      schedule: "${{ inputs.schedule }}"
```

{% endcode %}

When a consumer imports this plugin and passes `with: { schedule: "*/10 * * * *" }`, the variable is substituted before the plugin is processed. Substitution works in any string field of the plugin's snippet definitions.

### `description`

* `lunar-cataloger.yml -> inputs.<input-name>.description`
* Type: `string`
* Required

The description field is used to specify a description of the input.

### `default`

* `lunar-cataloger.yml -> inputs.<input-name>.default`
* Type: `string`
* Optional

The default field is used to specify the default value of the input. If no default value is specified, the input is required.


# lunar-collector.yml

Reference for lunar-collector.yml, the manifest that defines a collector plugin used to gather live information about components from various sources.

* `lunar-collector.yml`
* Type: YAML file
* Form:

  ```yaml
  version: 0

  name: <collector-name>
  description: <collector-description>
  author: <author-name>

  default_image: <default-image>
  default_image_ci_collectors: <default-image-ci>
  default_image_non_ci_collectors: <default-image-non-ci>

  collectors:
    - <collector-configuration>
    - <collector-configuration>
    - ...

  inputs:
    <input-name>:
      description: <input-description>
      default: <default-value>
    ...
  ```

This page describes the configuration of a collector plugin. Collector plugins are used to collect live information from various sources to associate with individual components. Collector plugins can be imported from the Lunar configuration file, `lunar-config.yml` via the `uses` collector form.

Collector plugins can be defined either in a separate repository or in the same repository as the Lunar configuration, in a dedicated directory. Either way, the collector plugin must contain a `lunar-collector.yml` file in the root of the repository or directory. This file is used to configure the behavior of the collector plugin.

When using collectors in the Main or Run forms, you can also install dependencies. See [installing dependencies in the Bash SDK](/plugin-sdks/bash-sdk/dependencies) or [installing dependencies in the Python SDK](/plugin-sdks/python-sdk/dependencies) for more details.

## `version`

* `lunar-collector.yml -> version`
* Type: `numeric`
* Required

The version field is used to specify the version of the collector configuration file. The current version is `0`.

## `name`

* `lunar-collector.yml -> name`
* Type: `string`
* Required

The name field is used to specify the name of the collector.

## `description`

* `lunar-collector.yml -> description`
* Type: `string`
* Optional

The description field is used to specify a description of the collector.

## `author`

* `lunar-collector.yml -> author`
* Type: `string`
* Optional

The author field is used to specify the author of the collector.

## Default Images

Collector plugins can define default Docker images that override the global defaults from `lunar-config.yml`. These settings apply to all collectors defined within the plugin.

### `default_image`

* `lunar-collector.yml -> default_image`
* Type: `string`
* Optional

The default image to use for all collectors in this plugin. Overrides the global `default_image` setting.

### `default_image_ci_collectors`

* `lunar-collector.yml -> default_image_ci_collectors`
* Type: `string`
* Optional

The default image for CI collectors (hooks: `ci-before-command`, `ci-after-command`, `ci-before-job`, `ci-after-job`). Overrides the global `default_image_ci_collectors` setting.

### `default_image_non_ci_collectors`

* `lunar-collector.yml -> default_image_non_ci_collectors`
* Type: `string`
* Optional

The default image for non-CI collectors (hooks: `code`, `cron`, `repo`). Overrides the global `default_image_non_ci_collectors` setting.

Individual collectors can still override these plugin defaults using the `image` field.

For more information about default images and container execution, see [Images](/configuration/lunar-config/images).

## `collectors`

* `lunar-collector.yml -> collectors`
* Type: `array`
* Required

The collectors field is used to specify the configuration of the collector. The format of a collector is the same as in `lunar-config.yml`, except that the `on` field is not allowed. To learn more about the configuration of a collector, see the [collectors](/configuration/lunar-config/collectors) page.

**Note:** When consumers import this plugin via `uses`, they can selectively include or exclude specific collectors using the [`include`](/configuration/lunar-config/collectors#include) field. This allows consumers to use only the collectors they need from a plugin that defines multiple collectors.

## `inputs`

* `lunar-collector.yml -> inputs`
* Type: `object`
* Optional

The inputs field is used to specify the inputs required by the collector. Each input is defined as a key-value pair, where the key is the input name.

Inputs are passed to the collector when invoked as environment variables with the prefix `LUNAR_VAR_` and the input name in uppercase. For example, an input named `api_url` is accessible as `$LUNAR_VAR_API_URL`.

Inputs can also be referenced in the plugin YAML definition itself using the `${{ inputs.NAME }}` syntax. This allows plugin authors to expose configurable fields as explicit settings. For example:

{% code title="lunar-collector.yml" %}

```yaml
inputs:
  binary_name:
    description: Binary name to match
    default: "go"

collectors:
  - name: check-binary
    runBash: echo "checking"
    hook:
      type: code
      binary:
        name: "${{ inputs.binary_name }}"
```

{% endcode %}

When a consumer imports this plugin and passes `with: { binary_name: "python3" }`, the variable is substituted before the plugin is processed. Substitution works in any string field of the plugin's snippet definitions.

### `description`

* `lunar-collector.yml -> inputs.<input-name>.description`
* Type: `string`
* Required

The description field is used to specify a description of the input.

### `default`

* `lunar-collector.yml -> inputs.<input-name>.default`
* Type: `string`
* Optional

The default field is used to specify the default value of the input. If no default value is specified, the input is required.


# lunar-policy.yml

Reference for lunar-policy.yml, the manifest that defines a policy plugin used to evaluate the health of components against rules.

* `lunar-policy.yml`
* Type: YAML file
* Form:

  ```yaml
  version: 0

  name: <policy-name>
  description: <policy-description>
  author: <author-name>

  default_image: <default-image>

  policies:
    - <policy-configuration>
    - <policy-configuration>
    - ...

  inputs:
    <input-name>:
      description: <input-description>
      default: <default-value>
    ...
  ```

This page describes the configuration of a policy plugin. Policy plugins are used to define the rules that Lunar uses to evaluate the health of components. Policy plugins can be imported from the Lunar configuration file, `lunar-config.yml` via the `uses` policy form.

Policy plugins can be defined either in a separate repository or in the same repository as the Lunar configuration, in a dedicated directory. Either way, the policy plugin must contain a `lunar-policy.yml` file in the root of the repository or directory. This file is used to configure the behavior of the policy plugin.

When using policies in the Main or Run forms, you can also install dependencies. See [installing dependencies in the Python SDK](/plugin-sdks/python-sdk/dependencies) for more details.

## `version`

* `lunar-policy.yml -> version`
* Type: `numeric`
* Required

The version field is used to specify the version of the policy configuration file. The current version is `0`.

## `name`

* `lunar-policy.yml -> name`
* Type: `string`
* Required

The name field is used to specify the name of the policy.

## `description`

* `lunar-policy.yml -> description`
* Type: `string`
* Optional

The description field is used to specify a description of the policy.

## `author`

* `lunar-policy.yml -> author`
* Type: `string`
* Optional

The author field is used to specify the author of the policy.

## `default_image`

* `lunar-policy.yml -> default_image`
* Type: `string`
* Optional

The default Docker image to use for all policies in this plugin. This overrides the global `default_image_policies` setting from `lunar-config.yml`.

Individual policies can still override this plugin default using the `image` field. Use the special value `native` to explicitly run without a container.

For more information about default images and container execution, see [Images](/configuration/lunar-config/images).

## `policies`

* `lunar-policy.yml -> policies`
* Type: `array`
* Required

The policies field is used to specify the configuration of the policy. The format of a policy is the same as in `lunar-config.yml`, except that the `on` and `enforcement` fields are not allowed (these are configured in `lunar-config.yml` only). To learn more about the configuration of a policy, see the [policies](/configuration/lunar-config/policies) page.

**Note:** When consumers import this plugin via `uses`, they can selectively include or exclude specific policies using the [`include`](/configuration/lunar-config/policies#include) field. This allows consumers to use only the policies they need from a plugin that defines multiple policies.

## `inputs`

* `lunar-policy.yml -> inputs`
* Type: `object`
* Optional

The inputs field is used to specify the inputs that the policy requires. Each input is defined as a key-value pair, where the key is the input name.

Inputs are accessed in policies using the `variable_or_default` function from the `lunar_policy` SDK. For example, an input named `threshold` is accessible as `variable_or_default("threshold", "10")` where the second argument is the fallback default value.

Inputs can also be referenced in the plugin YAML definition itself using the `${{ inputs.NAME }}` syntax. This allows plugin authors to expose configurable fields as explicit settings. For example:

{% code title="lunar-policy.yml" %}

```yaml
inputs:
  threshold:
    description: Score threshold
    default: "80"

policies:
  - name: verify-score
    description: "Verify score meets threshold of ${{ inputs.threshold }}"
    mainPython: ./main.py
```

{% endcode %}

When a consumer imports this plugin and passes `with: { threshold: "95" }`, the variable is substituted before the plugin is processed. Substitution works in any string field of the plugin's snippet definitions.

### `description`

* `lunar-policy.yml -> inputs.<input-name>.description`
* Type: `string`
* Required

The description field is used to specify a description of the input.

### `default`

* `lunar-policy.yml -> inputs.<input-name>.default`
* Type: `string`
* Optional

The default field is used to specify the default value of the input. If no default value is specified, the input is required.


# Bash SDK

Overview of the Lunar Bash SDK for building custom catalogers and collectors with shell scripts.

The Bash SDK allows you to create Lunar plugins using Bash scripting. This SDK is particularly useful for building custom catalogers and collectors.

## SDK Components

Learn more about the specific plugin types you can create with the Bash SDK:

* [Cataloger](/plugin-sdks/bash-sdk/cataloger) - Create custom catalogers to identify components in your codebase
* [Collector](/plugin-sdks/bash-sdk/collector) - Build collectors to gather metadata about your components


# Installing dependencies

How to install dependencies for Bash collectors and catalogers using a custom Docker image or an install.sh script.

If your collector or cataloger requires dependencies, you have two options depending on your execution environment:

1. **Custom Docker image** - Bake dependencies into your image for containerized execution
2. **Install script** - Use `install.sh` for native execution

## Custom Docker Image

When running in containers, create a custom Docker image with all dependencies pre-installed. This provides faster startup times, reproducible builds, and eliminates network dependencies at runtime.

Create a Dockerfile that inherits from the official `earthly/lunar-scripts` image:

{% code title="Dockerfile" %}

```dockerfile
FROM earthly/lunar-scripts:1.0.0

# Install system dependencies
RUN apt-get update && apt-get install -y jq curl && rm -rf /var/lib/apt/lists/*

# Install additional tools
RUN curl -L "https://github.com/keilerkonzept/dockerfile-json/releases/download/v1.2.2/dockerfile-json_Linux_x86_64.tar.gz" | tar xz \
    && mv dockerfile-json /usr/local/bin/
```

{% endcode %}

Then configure your plugin or `lunar-config.yml` to use this image:

```yaml
image: my-org/my-custom-image:v1.0
```

For more details on image configuration, see [Images](/configuration/lunar-config/images).

## Install Script for Native Execution

When running with `image: native` (no container), you can install dependencies using an `install.sh` script in the same directory as your `lunar-config.yml`, `lunar-collector.yml` or `lunar-cataloger.yml` file.

This install script is executed only once in each environment, before the collector or cataloger is run.

### Example

```bash
#!/bin/bash
curl -L "https://github.com/keilerkonzept/dockerfile-json/releases/download/v1.2.2/dockerfile-json_Linux_x86_64.tar.gz" | tar xz
mv dockerfile-json "$LUNAR_BIN_DIR/"
```

### Installation path

If you need to install a binary that will be used in the collector or cataloger, you can use the `LUNAR_BIN_DIR` environment variable. This will ensure the binary is available in the `PATH`.

### Multi-platform support

For platform-specific logic, you can use `install-<os>-<arch>.sh` or `install-<os>.sh` files. Making your script multi-platform is useful for local development when your local platform may differ from the one Lunar's runners or your CI is running on.

Platform-specific scripts are checked for existence in the following order:

1. `install-<os>-<arch>.sh`
2. `install-<os>.sh`
3. `install.sh`

Only the first script found is executed.

Examples of valid platform-specific script names:

* `install-linux-amd64.sh`
* `install-linux-arm64.sh`
* `install-linux.sh`
* `install-darwin-arm64.sh`
* `install-darwin.sh`

## Development convenience with `earthly/lunar-scripts`

The official `earthly/lunar-scripts` image automatically executes any `install.sh` script found in the plugin directory as part of its entrypoint. This is a convenience feature for quick development iteration, but baking dependencies into your image provides faster startup times in production.

To override this behavior, you can change the `ENTRYPOINT` in your Dockerfile. For example:

```dockerfile
ENTRYPOINT ["/bin/bash"]
```


# Cataloger

Write Lunar catalogers in Bash — environment variables and the lunar catalog CLI commands for saving catalog data.

The Cataloger Bash SDK is a set of Lunar CLI subcommands that allow you to save catalog-related information from within a cataloger.

## Cataloger environment

Earthly Lunar executes catalogers in an environment set up with the following variables:

* `LUNAR_HUB_HOST`: The host of the Lunar Hub.
* `LUNAR_HUB_INSECURE`: Whether to skip SSL verification of the Lunar Hub.
* `LUNAR_BIN_DIR`: The directory where the Lunar CLI is installed.
* `LUNAR_CATALOGER_NAME`: The name of the cataloger. 🚧 Coming Soon
* `LUNAR_CATALOGER_OWNER`: The owner of the cataloger. 🚧 Coming Soon
* `LUNAR_SECRET_<name>`: Any secret set in the Lunar Hub for the cataloger, via `HUB_CATALOGER_SECRETS=NAME:VALUE,NAME2:VALUE2`.
* `LUNAR_COMPONENT_ID`: The ID of the component that the cataloger is running for in `github.com/.../...` format. Only set for `component-repo` and `component-cron` hooks.

## `lunar catalog` CLI command

* Forms:
  * Raw form:

    ```bash
    lunar catalog raw [--json] <json-path> <value>
    ```
  * Component form:

    <div data-gb-custom-block data-tag="hint" data-style="info" class="hint hint-info"><p><strong>Coming Soon</strong> — This feature is not yet available.</p></div>

    ```bash
    lunar catalog component [--name <n>] [--owner <owner>] [--branch <branch>] [--domain <domain>] [--tag <tag>] [--ci-pipeline <ci-pipeline>] [--meta <key>=<value>] [--meta-json <key>=<value>] [<json-value>]
    ```
  * Domain form:

    <div data-gb-custom-block data-tag="hint" data-style="info" class="hint hint-info"><p><strong>Coming Soon</strong> — This feature is not yet available.</p></div>

    \`\`\`bash lunar catalog domain \[--name ] \[--description ] \[--owner ] \[--meta =] \[--meta-json =] \[] \`\`\`

The `lunar catalog` command is used to save catalog-related information from within a cataloger. The command takes a JSON path and a value as arguments. The JSON path is used to specify the location in the JSON object where the value should be stored.

### `--json`

The `--json` flag is used to specify that the value should be interpreted as a JSON object, JSON array, or JSON scalar (string, number, etc). If the flag is not provided, the value is interpreted as a raw string.

### `<json-path>`

* Type: `string`

The JSON path is used to specify the location in the JSON object where the value should be stored. The path is specified using dot notation, where each level of the object is separated by a dot. For example, `.foo.bar.baz` would specify the `baz` property of the `bar` object, which is a property of the `foo` object.

### `<value>`

By default, the value is interpreted as a raw string. If the `--json` flag is provided, the value is interpreted as a JSON object, JSON array, or JSON scalar (string, number, etc).

If the value is `-`, the value is read from standard input. This is useful for piping the output of a command into the `lunar catalog` command.

### `--name <name>`

* Type: `string`
* Optional

The `--name` flag is used to specify the name of the component or domain.

When using the `component-repo` or `component-cron` cataloger hooks, the name of the component and the domain will be inferred automatically from the cataloger context in which the command is executed (`LUNAR_COMPONENT_ID` is set for both hooks).

In Component and Domain forms, the name must be provided either via this flag, via the cataloger context (`component-repo` and `component-cron` hooks), or via the JSON value.

### `--owner <owner>`

* Type: `string`
* Optional

The `--owner` flag is used to specify the owner of the component or domain.

### `--branch <branch>`

* Type: `string`
* Optional

The `--branch` flag is used to specify the branch of the component.

### `--domain <domain>`

* Type: `string`
* Optional

The `--domain` flag is used to specify the domain of the component. The domain must be specified in the format `foo.bar`, where `foo` is the parent domain and `bar` is the child domain.

### `--tag <tag>`

* Type: `string`
* Optional

The `--tag` flag is used to specify the tag of the component. This flag may be specified multiple times to add multiple tags to the component.

### `--ci-pipeline <ci-pipeline>`

* Type: `string`
* Optional

The `--ci-pipeline` flag is used to specify the CI pipeline of the component. This flag may be specified multiple times to add multiple CI pipelines to the component.

### `--meta <key>=<value>`

* Type: `string`
* Optional

The `--meta` flag is used to specify a metadata key-value pair for the component or domain. This flag may be specified multiple times to add multiple metadata entries. The value is interpreted as a raw string.

### `--meta-json <key>=<value>`

* Type: `string`
* Optional

The `--meta-json` flag is used to specify a metadata key-value pair for the component or domain where the value is interpreted as JSON. This flag may be specified multiple times to add multiple metadata entries. The value must be valid JSON (object, array, or scalar).

### `--description <description>`

* Type: `string`
* Optional

The `--description` flag is used to specify the description of the domain.

### `<json-value>`

* Type: `JSON string`
* Optional

The JSON value is used to specify the value to be stored in the JSON object.


# Collector

The lunar collect CLI for collecting SDLC metadata into a component's JSON, and the environment available to Lunar collectors.

The Collector Bash SDK is a set of Lunar CLI subcommands that allow you to collect SDLC metadata from within a collector, or from external systems, such as your CI system directly.

## Collector environment

Earthly Lunar executes collectors in an environment set up with the following variables:

* `LUNAR_HUB_HOST`: The host of the Lunar Hub.
* `LUNAR_HUB_TOKEN`: The authentication token for the Lunar Hub.
* `LUNAR_HUB_GRPC_PORT`: The gRPC port of the Lunar Hub.
* `LUNAR_HUB_HTTP_PORT`: The HTTP port of the Lunar Hub.
* `LUNAR_HUB_INSECURE`: Whether to skip SSL verification of the Lunar Hub.
* `LUNAR_BIN_DIR`: The directory where the Lunar CLI is installed.
* `LUNAR_COLLECTOR_NAME`: The name of the collector.
* `LUNAR_COLLECTOR_CI_PIPELINE`: The CI pipeline of the component that the collector is running for (if this is a CI collector). 🚧 Coming Soon
* `LUNAR_COMPONENT_ID`: The ID of the component that the collector is running for in `github.com/.../...` format.
* `LUNAR_COMPONENT_DOMAIN`: The domain of the component.
* `LUNAR_COMPONENT_OWNER`: The owner of the component.
* `LUNAR_COMPONENT_HEAD_BRANCH`: The head branch of the PR (branch that contains the changes), if applicable.
* `LUNAR_COMPONENT_BASE_BRANCH`: The base branch of the PR (branch to merge the changes into), if applicable.
* `LUNAR_COMPONENT_PR`: The PR number of the component, if applicable.
* `LUNAR_COMPONENT_TAGS`: The tags of the component as a JSON array.
* `LUNAR_COMPONENT_GIT_SHA`: The Git SHA of the component that the collector is running for.
* `LUNAR_COMPONENT_META`: The component's `.meta` (an arbitrary key→value map, e.g. `pagerduty/service-id`) as a JSON object. Only set when the component has meta.
* `LUNAR_SECRET_<name>`: Any secret configured for collectors — either via `HUB_COLLECTOR_SECRETS=NAME:VALUE,NAME2:VALUE2` or at runtime using `lunar secret set <name> <value>`.

### CI collector environment

For `ci-{before,after}-job`, `ci-{before,after}-step` and `ci-{before,after}-command` hooks the following entries are also available:

* `LUNAR_CI`: The CI provider identifier (github, buildkite, ...).
* `LUNAR_CI_PIPELINE_RUN_ID`: Unique identifier for the current pipeline run. Pipeline are the top-level units of execution in a CI system. Also known as "workflow" in some CIs.
* `LUNAR_CI_PIPELINE_RUN_ATTEMPT`: Pipeline run attempt (1-based).
* `LUNAR_CI_PIPELINE_DEFINITION_REF`: URL to the pipeline source code.
* `LUNAR_CI_PIPELINE_NAME`: Name of the pipeline, if available.
* `LUNAR_CI_JOB_ID`: Job id. Jobs are units of parallel execution within a pipeline. Also known as "stages" in some CIs.
* `LUNAR_CI_JOB_NAME`: Job name, if available.

For `ci-{before,after}-step` and `ci-{before,after}-command` hooks the following entries are also available:

* `LUNAR_CI_STEP_INDEX`: Step index (1-based). Steps are the sequential units of execution within a job.
* `LUNAR_CI_STEP_NAME`: Step name, if available.
* `LUNAR_CI_STEP_ID`: Step ID, if available.
* `LUNAR_CI_STEP_USES`: Reference to the step definition, for reusable step definitions.
* `LUNAR_CI_STEP_RUN`: Step source code, for inline definitions.

For `ci-{before,after}-command` hooks the following entries are also available:

* `LUNAR_CI_COMMAND`: Command and arguments of the hooked command, as a JSON string.
* `LUNAR_CI_COMMAND_BIN`: Binary name only (no directory).
* `LUNAR_CI_COMMAND_BIN_DIR`: Directory containing the binary.
* `LUNAR_CI_COMMAND_ARGS`: Command arguments (excluding the binary) as a JSON array.
* `LUNAR_CI_COMMAND_PID`: Process ID of the hooked command.
* `LUNAR_CI_COMMAND_PPID`: Parent process PID of the hooked command.

## `lunar collect` CLI command

* Form:

  ```bash
  lunar collect [--component <name>] [--sha <commit>] [--pr <number>] \
                [--json] [--array-append] [--pretty] \
                <json-path> <value> [<json-path> <value> ...]
  ```

`lunar collect` collects SDLC metadata into a component's JSON, taking one or more `<json-path> <value>` pairs. Inside a collector the target component and commit default to the current run, so you collect by passing the paths and values to record — use `--json` for non-string values like numbers:

```bash
lunar collect --json .testing.coverage.percentage 87.4
lunar collect .testing.coverage.source.tool codecov
```

### Out-of-band collection

Collection can also happen outside of Lunar collectors — for example, in a CD pipeline, to associate release information with a previously built component's JSON. Pass `--component` and `--sha` to collect against a specific component and commit:

```bash
lunar collect --component github.com/my-org/my-repo --sha "$COMMIT_SHA" \
  .release.tag v1.2.3 \
  .release.image registry.example.com/my-app:v1.2.3
```

The Hub records the collection and re-evaluates policies for that SHA; verify the merged result with `lunar component get-json <component> --git-sha <sha>`.

### `--component <name>`

* Type: `string`
* Optional

The component the metadata is associated with, in `github.com/org/repo` form. Defaults to `$LUNAR_COMPONENT_ID` (set automatically inside a collector); pass it to collect against a specific component.

### `--sha <commit>`

* Type: `string`
* Optional

The full 40-character commit SHA to key the collection on. Defaults to `$GITHUB_SHA`.

### `--pr <number>`

* Type: `int`
* Optional

Optional PR number, to collect against a PR rather than a branch commit.

### `--json`

The `--json` flag is used to specify that the value should be interpreted as a JSON object, JSON array, or JSON scalar (string, number, etc). If the flag is not provided, the value is interpreted as a raw string.

Example:

```bash
lunar collect --json '.foo.bar1' 'true'              # interprets as a JSON scalar (boolean)
lunar collect '.foo.bar2' 'true'                     # interprets as a raw string
lunar collect --json '.foo.bar3' '{"baz": "value 3"}' # interprets as a JSON object
lunar collect --json '.foo.bar4' '["value 4"]'        # interprets as a JSON array
```

Will result in:

```json
{
  "foo": {
    "bar1": true,
    "bar2": "true",
    "bar3": {"baz": "value 3"},
    "bar4": ["value 4"]
  }
}
```

Note: Without `--json`, values are always treated as raw strings regardless of their content. For example, `lunar collect '.foo.bar' '{"key": "val"}'` would store the literal string `"{\"key\": \"val\"}"`, not a JSON object.

### `--array-append`

The `--array-append` flag is used to specify that the value should be appended to an array at the specified JSON path. If the path does not exist, a new array will be created. If the path exists and is not an array, the value will be replaced with a new array containing the existing value and the new value.

Since arrays are concatenated during the component JSON merge, using `--array-append` is equivalent to wrapping the value in a JSON array. The following two commands are equivalent:

```bash
lunar collect --array-append '.foo.bar' 'a value'
lunar collect --json '.foo.bar' '["a value"]'
```

Example:

```bash
lunar collect --json --array-append '.foo.bar' '{"baz": "value 1"}'
lunar collect --json --array-append '.foo.bar' '{"baz": "value 2"}'
```

Will result in:

```json
{
  "foo": {
    "bar": [
      {"baz": "value 1"},
      {"baz": "value 2"}
    ]
  }
}
```

### `--pretty` | `-p`

Pretty-print the JSON output.

### `<json-path>`

* Type: `string`

The JSON path is used to specify the location in the JSON object where the value should be stored. The path is specified using dot notation, where each level of the object is separated by a dot. For example, `.foo.bar.baz` would specify the `baz` property of the `bar` object, which is a property of the `foo` object.

### `<value>`

By default, the value is interpreted as a raw string. If the `--json` flag is provided, the value is interpreted as a JSON object, JSON array, or JSON scalar (string, number, etc).

If the value is `-`, the value is read from standard input. This is useful for piping the output of a command into the `lunar collect` command.


# Python SDK

Overview of Lunar's Python SDK for writing custom Lunar policies (with collector support coming soon).

The Python SDK provides interfaces for extending Lunar's functionality through Python-based plugins. This SDK enables you to create custom collectors and policies tailored to your organization's needs.

## SDK Components

Explore these pages to learn about the types of plugins you can create with the Python SDK:

* [Collector](/plugin-sdks/python-sdk/collector) - Create collectors for gathering component metadata (**coming soon**)
* [Policy](/plugin-sdks/python-sdk/policy) - Develop policies for validating components against custom rules


# Installing dependencies

How to install Python dependencies for Lunar policies, using a custom Docker image or native runtime execution.

If your policy requires dependencies, you have two options depending on your execution environment:

1. **Custom Docker image** - Install dependencies at build time in your Dockerfile
2. **Native execution** - Dependencies are installed automatically at runtime

Your `requirements.txt` should contain the `lunar_policy` package at a minimum:

```txt
lunar_policy==0.1.6
```

## Custom Docker Image

When running in containers, create a custom Docker image with all dependencies pre-installed. This provides faster startup times, reproducible builds, and eliminates network dependencies at runtime.

Create a Dockerfile that inherits from the official `earthly/lunar-scripts` image and installs your dependencies at build time:

{% code title="Dockerfile" %}

```dockerfile
FROM earthly/lunar-scripts:1.0.0

# Copy and install Python dependencies
COPY requirements.txt /tmp/requirements.txt
RUN pip install --no-cache-dir -r /tmp/requirements.txt && rm /tmp/requirements.txt
```

{% endcode %}

Then configure your plugin or `lunar-config.yml` to use this image:

```yaml
image: my-org/my-custom-image:v1.0
```

For more details on image configuration, see [Images](/configuration/lunar-config/images).

## Native Execution

When running with `image: native` (no container), place your `requirements.txt` in the same directory as your `lunar-config.yml` or `lunar-policy.yml` file. Dependencies are installed automatically before the policy is run, and only once in each environment.

## Development convenience with `earthly/lunar-scripts`

The official `earthly/lunar-scripts` image automatically installs dependencies from any `requirements.txt` file found in the plugin directory as part of its entrypoint. This is a convenience feature for quick development iteration, but baking dependencies into your image provides faster startup times in production.

To override this behavior, you can change the `ENTRYPOINT` in your Dockerfile. For example:

```dockerfile
ENTRYPOINT ["/usr/bin/python3"]
```


# Collector

Placeholder for the upcoming Python Collector SDK used to gather component metadata in Lunar.

Coming soon! This page will contain documentation for the upcoming Python Collector SDK.

In the meantime, see the [Collector Bash SDK](/plugin-sdks/bash-sdk/collector) for similar functionality in Bash.


# Policy

Write Lunar policies in Python with the lunar\_policy package — load component metadata, make assertions, and handle pending data.

The `lunar_policy` Python package provides utilities for working with Lunar policies, allowing you to load, query, and make assertions about component metadata, such as the [component JSON](/docs/component-json).

For the reference documentation check out:

* [Check](/plugin-sdks/python-sdk/policy/check) - Main class for making assertions about component data
* [Node](/plugin-sdks/python-sdk/policy/node) - Navigate and explore JSON data
* [CheckStatus](/plugin-sdks/python-sdk/policy/check-status)
* [NoDataError](/plugin-sdks/python-sdk/policy/no-data-error)
* [SkippedError](/plugin-sdks/python-sdk/policy/skipped-error)

## Installation

When using the official `earthly/lunar-scripts` Docker image (recommended), the `lunar-policy` package is already pre-installed. See [Images](/configuration/lunar-config/images) for details on configuring default images.

For native execution or custom images, the package is available through pip:

```bash
pip install lunar-policy
```

## Policy environment

Earthly Lunar executes policies in an environment set up with the following variables:

* `LUNAR_HUB_HOST`: The host of the Lunar Hub.
* `LUNAR_HUB_INSECURE`: Whether to skip SSL verification of the Lunar Hub.
* `LUNAR_POLICY_NAME`: The name of the policy being executed. 🚧 Coming Soon
* `LUNAR_INITIATIVE_NAME`: The name of the initiative the policy belongs to. 🚧 Coming Soon
* `LUNAR_POLICY_OWNER`: The owner of the policy. 🚧 Coming Soon
* `LUNAR_COMPONENT_ID`: The ID of the component being checked in `github.com/.../...` format.
* `LUNAR_COMPONENT_DOMAIN`: The domain of the component.
* `LUNAR_COMPONENT_OWNER`: The owner of the component.
* `LUNAR_COMPONENT_PR`: The PR number of the component, if applicable.
* `LUNAR_COMPONENT_GIT_SHA`: The Git SHA of the component that the policy is being executed for.
* `LUNAR_COMPONENT_TAGS`: The tags of the component.
* `LUNAR_COMPONENT_META`: The component's `.meta` (an arbitrary key→value map, e.g. `pagerduty/service-id`) as a JSON object. Only set when the component has meta.
* `LUNAR_BUNDLE_PATH`: Used internally by Lunar to pass in the component JSON and the deltas to the policy.
* `LUNAR_SECRET_<name>`: Any secret set in the Lunar Hub for the policy, via `HUB_POLICY_SECRETS=NAME:VALUE,NAME2:VALUE2`.

{% hint style="warning" %}
Policies are re-evaluated frequently as each piece of data becomes available, so design your policy execution to be fast (no external API calls, no heavy processing, etc.). If you need to perform any expensive operations, consider using a collector instead, and passing the necessary data via the component JSON.
{% endhint %}

{% hint style="warning" %}
Policies are re-evaluated between component data collections. This means that not all data is available from the beginning, and your policy should correctly report a `pending` status when it cannot make a decision yet. Much of this is handled automatically for you — see [Handling Missing Data](#handling-missing-data) below for more details.
{% endhint %}

## Core Components

The Policy SDK provides several key classes to help you write policies:

### Check

The `Check` class provides a fluent interface for making assertions about policy data. It tracks accessed data within the component JSON for traceability purposes.

```python
from lunar_policy import Check

# Create a check with a name and optional description
with Check("my-check", "Validates important properties") as check:
    # Get data using JSONPath
    value = check.get_value(".path.to.data")
    
    # Make assertions
    check.assert_equals(check.get_value(".api.endpoints[0].method"), "GET")
    check.assert_greater_or_equal(value, 50)
    check.assert_contains(check.get_value(".api.endpoints[0].path"), "/")
```

👉 For detailed reference, see the [Check reference documentation](/plugin-sdks/python-sdk/policy/check).

### Node

The `Node` class is used to navigate and explore JSON data.

```python
from lunar_policy import Check, Node

with Check("my-check", "Validates important properties") as check:
    # Create a node from a check
    node = check.get_node(".path.to.data")
    if node.exists():
        # Get the value of the node
        value = node.get_value()
        # Make assertions on the node
        node.assert_equals(value, "expected_value")
```

👉 For detailed reference, see the [Node reference documentation](/plugin-sdks/python-sdk/policy/node).

### NoDataError

The `NoDataError` exception is used to indicate that required data for a policy check is still pending (hasn't been collected yet).

👉 For detailed reference, see the [NoDataError reference documentation](/plugin-sdks/python-sdk/policy/no-data-error).

### SkippedError

The `SkippedError` exception is used to indicate that a check should be skipped because it is not applicable to the current component (e.g., Go-specific checks on a Java repository).

👉 For detailed reference, see the [SkippedError reference documentation](/plugin-sdks/python-sdk/policy/skipped-error).

## Automatic Data Loading

When a policy is executed through Lunar Hub, Lunar passes in the relevant context via `LUNAR_BUNDLE_PATH`. This context includes the [component JSON](/docs/component-json) for the component that is being checked. When you create a `Check` instance, the library automatically loads this data under the hood when `node` is not provided.

## Executing Policies Locally

To execute a policy locally for testing purposes, you can use the `lunar` CLI.

```bash
lunar policy dev --component-json path/to/component.json ./path/to/policy.py
```

If you would like to use the real component JSON of one of your components, you can do so via the command:

```bash
lunar policy dev --component github.com/my-org/my-repo ./path/to/policy.py
```

## Check outcomes

Checks can result in the following possible outcomes:

* `pass`: The check passed successfully. All assertions within the check were satisfied.
* `fail`: The check failed. One or more assertions within the check were not satisfied. This generally means that the developer working on the component needs to fix the issue.
* `pending`: The check could not be completed due to data not being available yet. This is due collection not having finished yet (code or cron collectors are still running, or the CI is still running).
* `error`: The check encountered an error during execution. This indicates an unexpected error occurred during the check execution, such as a runtime exception. This is generally a bug either in the collection logic or in the policy code.
* `skipped`: The check was intentionally skipped because it is not applicable to the current component. Skipped checks are not shown to end-user developers but are available in the SQL API for analysis purposes.

## Handling Missing Data

Lunar makes a clear distinction between temporarily missing component JSON data (data is pending), permanently missing component JSON data, and failing component JSON data. This is needed under the hood to be able to provide partial policy results while collectors are still running. Policies that have enough data will provide accurate results, while policies that don't have enough data will often report a `pending` status.

Here is a breakdown of how the different statuses are determined:

* Temporarily missing data (`pending` status): This is when some or all of the component JSON data required for assertions is not present, but the collector are still running. In this case, the check will report a `pending` status for now, thanks to methods like `get_value`, `exists` and `assert_exists` automatically raising `NoDataError` when not enough data is available and collectors are still running.
* Permanently missing data - sometimes expected on failures (`fail` status): This is when some of the component JSON data required for assertions is not present, and the collectors have finished running. `assert_exists` will report a failure in this case.
* Permanently missing data - unexpected (`error` status): This is when some of the component JSON data required for assertions is not present, and the collectors have finished running. `get_value` will report a `ValueError` after collectors finished, resulting in an `error` status.
* Failing data (`fail` status): This is when the data is present, but the assertions (e.g. `assert_equals`, `assert_true`, etc.) fail.

This means that you should use `get_value` to access data when you are assuming that the data will eventually be provided by a collector. Or use `assert_exists` (or `exists` within an if condition) to test for the existence of data if that's what the pass/fail outcome of the check should depend on.

### Best Practices for Handling Missing Data

Below are examples demonstrating different approaches to handling missing data in policies:

{% columns %}
{% column %}

#### Bad: Blindly Assuming Data Exists

This approach incorrectly assumes all fields exist after retrieving an object, which can lead to errors when data is temporarily missing:

```python
with Check("api-security-check") as check:
    # Bad: Gets the entire API object once and assumes all fields exist
    api = check.get_value(".api")
    
    # These will cause policy errors if api is None or missing expected fields
    check.assert_true(api["requires_auth"], "API should require authentication")
    rate_limit = api["rate_limit"]
    check.assert_equals(rate_limit, 100, f"API rate limit should be 100, but found {rate_limit}")
    check.assert_contains(api["security_headers"], "Content-Security-Policy")
```

{% endcolumn %}

{% column %}

#### Good: Targeted Access via the Check API

This approach uses targeted field access and relies on the Check API to handle missing data automatically:

```python
with Check("api-security-check") as check:
    # Direct field access - raises NoDataError if path doesn't exist yet
    check.assert_true(check.get_value(".api.requires_auth"), "API should require authentication")
    
    # Test for data existence when that's the actual requirement
    check.assert_exists(".api.requires_auth", "API is missing requires_auth field")
    
    # Conditional checks using exists() - only run assertions if data exists
    if check.exists(".api"):
        check.assert_true(check.get_value(".api.requires_auth"), "API should require authentication")
    
    # Working with nodes + exists() to navigate and explore data
    api_node = check.get_node(".api")
    if api_node.exists():
        rate_limit = api_node.get_value(".rate_limit") 
        check.assert_equals(rate_limit, 100, f"API rate limit should be 100, but found {rate_limit}")
        check.assert_contains(api_node.get_value(".security_headers"), "Content-Security-Policy")
```

{% endcolumn %}
{% endcolumns %}

## Writing Unit Tests for Policies

Let's assume that we have the following policy:

```python
from lunar_policy import Check, Node

def verify_readme(node=None):
    check = Check("readme-long-enough", node=node)
    with check:
        lines = check.get_value('.readme.lines')
        check.assert_greater_or_equal(
            lines, 50,
            f'README.md should have at least 50 lines. Current count: {lines}'
        )
    return check

if __name__ == "__main__":
    verify_readme()
```

Here's an example showing how to write unit tests:

```python
import unittest
from lunar_policy import Check, Node, CheckStatus

class TestReadmePolicy(unittest.TestCase):
    def test_not_long_enough(self):
        component_json = {
            "readme": {
                "lines": 49,
                "missing": False
            }
        }
        node = Node.from_component_json(component_json)
        check = verify_readme(node)
        
        # Verify the check failed because the README isn't long enough
        self.assertEqual(check.status, CheckStatus.FAIL)
        self.assertEqual(check.failure_reasons[0], "README.md should have at least 50 lines. Current count: 49")

if __name__ == "__main__":
    unittest.main()
```

You can run the test with:

```bash
python -m unittest test_readme_policy.py
```


# Check

Reference for the Check class in Lunar's Python Policy SDK — the fluent API for asserting on component metadata and tracking results.

The `Check` class provides a fluent interface for making assertions about policy data. The object keeps track of accessed data within the component JSON and records that for traceability purposes. The final result of a check will have not just the status (`pass`, `fail`, `pending`, `error`, or `skipped`), but complete information about which JSON paths were used to reach this conclusion. Designed to be used as a context manager with Python's `with` statement, the `Check` class automatically handles `NoDataError` (turns it into `pending` status), `SkippedError` (turns it into `skipped` status), and tracks result statuses.

## Constructor

```python
Check(name, description=None, node=None)
```

Creates a new check instance that can be used to make assertions about the component data. If the component data is not provided, it will be loaded automatically from the environment via `LUNAR_BUNDLE_PATH`.

* **name** (str): A unique identifier for this check
* **description** (str, optional): A human-readable description of what this check validates
* **node** (Node, optional): An alternate Node instance to use for this check, instead of loading it from the environment. Useful for unit testing.

## Context Manager

The `Check` class is designed to be used as a context manager with Python's `with` statement. This is the recommended way to use the class as it ensures proper setup, teardown, and error handling.

```python
with Check("check-name", "Check description") as check:
    # Make assertions using check methods
    check.assert_true(check.get_value(".path.to.data"))
```

When used as a context manager, the `Check` class:

1. **On enter**: Sets up the check context and automatically loads component data if not provided
2. **On exit**: Records the check result with its status and all accessed data paths
3. **Exception handling**:
   * Catches and suppresses `NoDataError`, and sets the check status to `pending`, if collectors are still running.
   * Otherwise, propagates `NoDataError` and records as `error` status since `NoDataError` is unexpected after collectors finished.
   * Catches and suppresses `SkippedError`, sets the check status to `skipped`, and drops any previously recorded assertions.
   * Propagates other exceptions and records them as `error` status with the exception message

## JSONPath Compatibility

{% hint style="info" %}
We do not support the entirety of JSONPath as defined in RFC 9535. However, we do support a strict subset of the language:

* `$` is implicit at the beginning of paths.
* You can access members of an object by key using either `.dot.syntax` or `['literal']['syntax']`.
* You can index into an array by number only. No wildcards or filters allowed.
  {% endhint %}

## Data Access Methods

### get\_value

```python
get_value(path=".")
```

Retrieves data from the component JSON using a JSONPath expression. This method raises `ValueError` if the path is invalid.

Missing data behavior:

* Raises `NoDataError` **before** collectors finished (results in `pending` status).
* Raises `ValueError` **after** collectors finished (results in `error` status).

This means that `get_value` is best used when you are assuming that the data will eventually be provided by a collector.

* **path** (str): JSONPath expression to query the component data (default: "." for root)
* **Returns**: The value at the specified path, or raises `NoDataError` or `ValueError`.

Example:

```python
# Get the number of lines in the README
lines = check.get_value(".readme.lines")

# Get the entire component data
all_data = check.get_value()
```

### get\_value\_or\_default

```python
get_value_or_default(path=".", default=None)
```

Retrieves data from the component JSON using a JSONPath expression. If the value is missing for any reason, it returns the specified default instead.

* **path** (str): JSON path relative to the root of the component JSON (default: ".")
* **default**: The value to return if there is no value at this path

Example:

```python
# Given this component JSON:
# {
#     "existing": {
#         "value": "hello"
#     }
# }

val = node.get_value_or_default(".existing.value", "goodbye") # val will be "hello"
val = node.get_value_or_default(".missing", "goodbye") # val will be "goodbye"
```

### get\_all\_values

```python
get_all_values(path=".")
```

Retrieves data from the component JSON deltas using a JSONPath expression. Use this when you collect something at the same path multiple times to help assert on all values. This method raises `ValueError` if the path is invalid.

Missing data behavior:

* Raises `NoDataError` **before** collectors finished (results in `pending` status).
* Raises `ValueError` **after** collectors finished (results in `error` status).

This means that `get_all_values` is best used when you are assuming that the data will eventually be provided by a collector.

* **path** (str): JSONPath expression to query the component data (default: "." for root)
* **Returns**: A list of all values found at the specified path, or raises `NoDataError` or `ValueError`.

Example:

```python
# Get the number of lines in the README
lines = check.get_all_values(".readme.lines")
```

### get\_node

```python
get_node(path)
```

Gets a Node at the given path. Uses lazy evaluation - no data access or path tracking until value is needed.

* **path** (str): JSONPath expression to query the component data
* **Returns**: A `Node` instance at the specified path
* **Raises**: `ValueError` if the path syntax is invalid

The returned Node object provides methods like `get_value()`, `get_node()`, `exists()`, and supports iteration. See the [Node class documentation](/plugin-sdks/python-sdk/policy/node) for complete details.

Example:

```python
config_node = check.get_node(".config")
if config_node.exists():
    config = config_node.get_value()
```

### exists

```python
exists(path=".")
```

Returns `True` if the path exists in the component data.

Missing path behavior:

* Raises `NoDataError` **before** collectors finished (results in `pending` status).
* Returns `False` **after** collectors finished.

## Node-like Iteration Methods

The `Check` class supports iteration methods that make it behave like a Node for duck-typing compatibility. These methods allow you to iterate over the root component data.

### Iterating over Check Fields

```python
for item in check:
    # Process item
```

Makes Check iterable like a Node. For dictionaries, yields keys. For arrays, yields Node objects.

* **For dict-like data**: Yields string keys
* **For array-like data**: Yields Node objects for each array element
* **Raises**: `ValueError` if the component data is not a dict or array, `NoDataError` if data is not available yet

Example:

```python
# Iterate over top-level keys in component data
with Check("iterate-check") as check:
    for key in check:
        print(f"Top-level key: {key}")
```

### items

```python
items()
```

Get key-value pairs when the Check points to dict-like component data.

* **Returns**: Iterator of (key, Node) tuples for dict-like data
* **Raises**: `ValueError` if the component data is not a dictionary, `NoDataError` if data is not available yet

Example:

```python
with Check("items-check") as check:
    for key, value_node in check.items():
        value = value_node.get_value()
        print(f"{key}: {value}")
```

## Control Flow Methods

### skip

```python
skip(reason="")
```

Unconditionally skips the check with an optional reason. When called, this method raises a `SkippedError` exception that is caught by the Check's context manager. The check is effectively canceled - any assertions (passed or failed) that may have been recorded before `skip` is called are dropped/ignored, and the final status of the check is marked as `skipped`.

Skipped checks are not shown to end-user developers but are available in the SQL API for analysis purposes.

* **reason** (str, optional): An optional message explaining why the check was skipped

Example:

```python
if not check.exists(".go"):
    check.skip("No Go files exist in repository")

if not check.exists(".kubernetes.manifests"):
    check.skip("No Kubernetes manifests exist")
```

### fail

```python
fail(message)
```

Unconditionally fails the check with a given message.

* **message** (str): The message to display when the check fails

Example:

```python
if my_complex_condition():
    check.fail("This is a policy failure")
```

## Assertion Methods

All assertion methods have these common parameters:

* **value**: The value to be asserted
* **failure\_message** (str, optional): Custom message to display if the assertion fails

Additionally, all assertion methods raise `NoDataError` if the path doesn't exist in the component data.

### assert\_true

```python
assert_true(value, failure_message=None)
```

Asserts that a value is `True`.

Example:

```python
# Assert that authentication is required
check.assert_true(check.get_value(".api.auth_required"), "API should require authentication")
```

### assert\_false

```python
assert_false(value, failure_message=None)
```

Asserts that a value is `False`.

Example:

```python
# Assert that the README.md file is not missing
check.assert_false(check.get_value(".readme.missing"), "README.md file should exist")
```

### assert\_equals

```python
assert_equals(value, expected, failure_message=None)
```

Asserts that a value equals the expected value.

* **expected**: The expected value to compare against

Example:

```python
# Assert that the API endpoint uses GET method
check.assert_equals(check.get_value(".api.endpoints[0].method"), "GET", "Endpoint should use GET method")
```

### assert\_exists

```python
assert_exists(path, failure_message=None)
```

Asserts that a path exists in the component data. If the path was not found, this method raises `NoDataError` before collectors finished, and fails the check after collectors finished.

Missing path behavior:

* Raises `NoDataError` **before** collectors finished (results in `pending` status).
* Fails the check **after** collectors finished (results in `FAIL` status).

Example:

```python
check.assert_exists(".api", "API data not found")
```

### assert\_contains

```python
assert_contains(value, expected, failure_message=None)
```

Asserts that a value contains the expected value (works for strings, lists, etc.).

* **expected**: The value that should be contained

Example:

```python
# Assert that the endpoint path contains a specific substring
check.assert_contains(check.get_value(".api.endpoints[0].path"), "/users")

# Assert that the tags list contains a specific tag
check.assert_contains(check.get_value(".tags"), "api")
```

### assert\_greater

```python
assert_greater(value, expected, failure_message=None)
```

Asserts that a numeric value is greater than the expected value.

* **expected**: The threshold value to compare against

Example:

```python
# Assert that the code coverage is greater than 80%
check.assert_greater(check.get_value(".coverage.percentage"), 80, "Code coverage should be greater than 80%")
```

### assert\_greater\_or\_equal

```python
assert_greater_or_equal(value, expected, failure_message=None)
```

Asserts that a numeric value is greater than or equal to the expected value.

* **expected**: The threshold value to compare against

Example:

```python
# Assert that README has at least 50 lines
check.assert_greater_or_equal(check.get_value(".readme.lines"), 50, "README should have at least 50 lines")
```

### assert\_less

```python
assert_less(value, expected, failure_message=None)
```

Asserts that a numeric value is less than the expected value.

* **expected**: The threshold value to compare against

Example:

```python
# Assert that cyclomatic complexity is less than 15
check.assert_less(check.get_value(".complexity.cyclomatic"), 15, "Cyclomatic complexity should be less than 15")
```

### assert\_less\_or\_equal

```python
assert_less_or_equal(value, expected, failure_message=None)
```

Asserts that a numeric value is less than or equal to the expected value.

* **expected**: The threshold value to compare against

Example:

```python
# Assert that build time is at most 5 minutes
check.assert_less_or_equal(check.get_value(".build.duration_minutes"), 5, "Build should take at most 5 minutes")
```

### assert\_match

```python
assert_match(value, pattern, failure_message=None)
```

Asserts that a string value matches a regular expression pattern.

* **pattern** (str): A regular expression pattern to match against

Example:

```python
# Assert that version follows semantic versioning
check.assert_match(check.get_value(".version"), r"^\d+\.\d+\.\d+$", "Version should follow semantic versioning")
```

## Instance Properties

After a check has been executed (typically after exiting the `with` context), the following properties are available:

### status

```python
status: CheckStatus
```

The final status of the check after execution.

* **Type**: `CheckStatus`
* **Values**: `PASS`, `FAIL`, `PENDING`, `ERROR`, or `SKIPPED`

### failure\_reasons

```python
failure_reasons: List[str]
```

The reasons for failure when the check status is `FAIL`. This property contains an array of detailed error messages from any failed assertions within the check.

* **Type**: `List[str]`
* **Available when**: `status` is `CheckStatus.FAIL`

### name

```python
name: str
```

The name of the check as specified in the constructor.

* **Type**: `str`


# CheckStatus

Reference for the CheckStatus enum used to mark Lunar policy outcomes as PASS, FAIL, PENDING, ERROR, or SKIPPED.

The `CheckStatus` class defines the possible statuses for a check result. It is used to represent the final outcome of a policy check execution.

## Usage

```python
from lunar_policy import CheckStatus
```

## Available Status Values

### PASS

```python
CheckStatus.PASS
```

The check resulted in `pass` status. It passed successfully, so all assertions within the check were satisfied.

### FAIL

```python
CheckStatus.FAIL
```

The check resulted in `fail` status. It failed, so one or more assertions within the check were not satisfied.

### PENDING

```python
CheckStatus.PENDING
```

The check resulted in `pending` status. It could not be completed due to data not being available. This occurs when required data is not yet available in the component JSON, typically because a code or cron collector has not finished running yet, or because the CI is still running (and a CI collector hasn't triggered yet).

### ERROR

```python
CheckStatus.ERROR
```

The check resulted in `error` status. It encountered an error during execution. This indicates an unexpected error occurred during the check execution, such as a runtime exception.

### SKIPPED

```python
CheckStatus.SKIPPED
```

The check resulted in `skipped` status. The check was intentionally skipped using the `skip()` method, typically because the check is not applicable to the current component (e.g., Go-specific checks on a Java repository). Skipped checks are not shown to end-user developers but are available in the SQL API for analysis purposes.


# Node

Reference for the Node class in Lunar's Python Policy SDK — used to navigate, query, and iterate over component JSON data.

The `Node` class represents a specific location in JSON data and allows navigation relative to that location. It provides a way to traverse and explore component data with lazy evaluation, meaning data is only accessed when explicitly requested through methods like `get_value()`, `exists()`, or iteration.

## Class Methods

### from\_component\_json

```python
@classmethod
from_component_json(cls, data)
```

Creates a `Node` instance from a JSON object. Note that a `Node` instance created from the component JSON only will not contain the deltas. While this can be useful for testing most policies, it will not represent a realistic component's data when testing `Check.get_all_values`.

* **data** (dict): A dictionary containing component metadata
* **Returns**: A new `Node` instance

Example:

```python
component_json = {
    "readme": {
        "lines": 50,
        "missing": False
    }
}
component_data = Node.from_component_json(component_json)
```

### from\_component\_json\_file

```python
@classmethod
from_component_json_file(cls, file_path)
```

Creates a `Node` instance from a JSON file. Note that a `Node` instance created from the component JSON only will not contain the deltas. While this can be useful for testing most policies, it will not represent a realistic component's data when testing `Check.get_all_values`.

* **file\_path** (str): Path to a JSON file containing component metadata
* **Returns**: A new `Node` instance

Example:

```python
component_data = Node.from_component_json_file("path/to/component.json")
```

### from\_bundle\_json

```python
@classmethod
from_bundle_json(cls, data)
```

Creates a `Node` instance from a bundle JSON object.

* **data** (dict): A dictionary containing bundle data
* **Returns**: A new `Node` instance

### from\_bundle\_file

```python
@classmethod
from_bundle_file(cls, file_path)
```

Creates a `Node` instance from a bundle JSON file.

* **file\_path** (str): Path to a JSON file containing bundle data
* **Returns**: A new `Node` instance

## Data Access Methods

### get\_value

```python
get_value(path=".")
```

Gets the raw value at the given path relative to this node.

* **path** (str): JSON path relative to this node (default: "." for this node's value)
* **Returns**: The raw value (dict, list, string, number, boolean, etc.)
* **Raises**: `ValueError` if the path is invalid or doesn't exist, `NoDataError` if data is not available yet

Example:

```python
# Get the current node's value
node = check.get_node(".config.database")
host = node.get_value(".host")  # Relative to .config.database
port = node.get_value(".port")  # Relative to .config.database

# Get the node's own value
database_config = node.get_value()  # Returns the entire database config object as a dict
```

### get\_value\_or\_default

```python
get_value_or_default(path=".", default=None)
```

Gets the raw value at the given path relative to this node. If the value is missing for any reason, it returns the specified default instead.

* **path** (str): JSON path relative to this node (default: "." for this node's value)
* **default**: The value to return if there is no value at this path

Example:

```python
# Given this component JSON:
# {
#     "existing": {
#         "value": "hello"
#     }
# }

val = node.get_value_or_default(".existing.value", "goodbye") # val will be "hello"
val = node.get_value_or_default(".missing", "goodbye") # val will be "goodbye"
```

### get\_node

```python
get_node(path)
```

Gets a Node at the given path relative to this node. Uses lazy evaluation - data is not accessed until value is needed.

* **path** (str): JSON path relative to this node
* **Returns**: A new Node instance at the specified path
* **Raises**: `ValueError` if the path syntax is invalid

### exists

```python
exists(path=".")
```

Checks if a path exists relative to this node.

Missing path behavior:

* Raises `NoDataError` **before** collectors finished (results in `pending` status).
* Returns `False` **after** collectors finished.

Example:

```python
host_node = check.get_node(".config.host")
if host_node.exists():
    host = host_node.get_value()
```

## Iteration Methods

### Iterating over Node

```python
for item in node:
    # Process item
```

Makes Node iterable. For dictionaries, yields keys. For arrays, yields Node objects.

* **For dict-like data**: Yields string keys
* **For array-like data**: Yields Node objects for each array element
* **Raises**: `ValueError` if the node doesn't point to a dict or array, `NoDataError` if data is not available yet

Example:

```python
# Iterate over dictionary keys
config_node = check.get_node(".config")
for key in config_node:
    print(f"Config key: {key}")

# Iterate over array elements
items_node = check.get_node(".items")
for item_node in items_node:
    name = item_node.get_value(".name")
    print(f"Item name: {name}")
```

### items

```python
items()
```

Get key-value pairs when this Node points to a dict-like structure.

* **Returns**: Iterator of (key, Node) tuples for dict-like data
* **Raises**: `ValueError` if the node doesn't point to a dictionary, `NoDataError` if data is not available yet

Example:

```python
config_node = check.get_node(".config")
for key, value_node in config_node.items():
    value = value_node.get_value()
    print(f"{key}: {value}")
```


# NoDataError

Reference for the NoDataError exception that signals required component data is not yet collected, marking a Lunar check as pending.

The `NoDataError` exception is used to indicate that there is not enough data to make a determination on whether the check should pass or fail. This is due to the required data not having been collected yet and thus not being present in the component JSON.

This exception is automatically caught by the `Check` context manager, which sets the check status to `pending` if collectors are still running. Note that the `NoDataError` is unexpected after collectors finished, and will result in an `error` status.

The purpose of `NoDataError` is to distinguish between:

1. **Data is pending**: When required data is yet to be collected (for example, a CI collector hasn't triggered yet, or a code collector hasn't finished running yet)
2. **Failed Assertions**: When data is present but doesn't meet the policy requirements (results in `fail` status)

This distinction is important because Lunar re-evaluates policies as data becomes available. A `pending` status indicates that the policy should be re-evaluated when more data is collected, while a `fail` status indicates a definitive policy violation.

## Constructor

```python
NoDataError(message=None)
```

* **message** (str, optional): A message describing why the data is missing


# SkippedError

Reference for the SkippedError exception used to skip a Lunar check when it does not apply to the current component.

The `SkippedError` exception is used to indicate that a check should be skipped and not evaluated. This is typically used for applicability detection - when a check is not relevant to the current component (e.g., Go-specific checks on a Java repository, or Kubernetes checks when no manifests are present).

This exception is automatically caught by the `Check` context manager, which:

* Sets the check status to `skipped`
* Drops any previously recorded assertions (both passed and failed)
* Suppresses the exception so it doesn't propagate

Skipped checks are not shown to end-user developers but are available in the SQL API for analysis and debugging purposes.

## Constructor

```python
SkippedError(message=None)
```

* **message** (str, optional): A message describing why the check was skipped

## Usage

The `SkippedError` is typically raised automatically by the `Check.skip()` method rather than being raised directly:

```python
# Recommended approach - use Check.skip()
with Check("go-version-check") as check:
    if not check.exists(".go"):
        check.skip("No Go files found in repository")
    
    # This code won't execute if skip() was called
    go_version = check.get_value(".go.version")
    check.assert_match(go_version, r"^1\.(19|20|21)$")
```


# Overview

Overview of Lunar's read-only SQL API for querying components, domains, policies, checks, and other Hub data via PostgreSQL clients.

The SQL API provides programmatic access to Lunar's data through SQL queries. Through this interface, you can access and analyze information about your components, domains, policies, and more.

## Accessing the SQL API

To access the SQL API, use the Lunar CLI command `lunar sql connection-string` to get a PostgreSQL connection string that can be used with any PostgreSQL client:

```bash
# Get the connection string
lunar sql connection-string

# Connect using psql (interactive)
psql $(lunar sql connection-string)

# Execute a query directly
psql $(lunar sql connection-string) -c "SELECT * FROM components LIMIT 5"
```

The access is **read-only** and restricted to only the views described in this documentation.

## Available Views

For detailed information about available views, explore these pages:

* [domains](/sql-api/views/domains)
* [components](/sql-api/views/components)
* [component\_deltas](/sql-api/views/component-deltas)
* [initiatives](/sql-api/views/initiatives)
* [policies](/sql-api/views/policies)
* [checks](/sql-api/views/checks)
* [prs](/sql-api/views/prs)
* [catalog](/sql-api/views/catalog) -- Coming Soon

## AI Skill

An AI skill is available to help you craft SQL queries against the Lunar data model. The [lunar-sql](https://github.com/earthly/skills/tree/main/skills/lunar-sql) skill provides your AI assistant with knowledge of the view schemas, JSONB query patterns, and common query examples.

See [Installing AI Skills](/install/skills) for setup instructions.


# Views


# domains

Schema reference for the domains SQL view — one row per domain with its description, owner, and metadata for grouping components.

```
domains
```

The `domains` view provides information about all domains defined in Lunar. Domains represent logical groupings of components that share similar characteristics or purposes.

## Schema

| Column        | Type    | Description                                                                            |
| ------------- | ------- | -------------------------------------------------------------------------------------- |
| `name`        | `TEXT`  | The identifier for the domain in hierarchical dotted format (e.g. `payments.checkout`) |
| `description` | `TEXT`  | A description of the domain                                                            |
| `owner`       | `TEXT`  | The owner of the domain                                                                |
| `meta`        | `JSONB` | Arbitrary metadata associated with the domain                                          |

## Notes

* Domains are defined primarily in the Lunar configuration but can also come from catalogers
* Domains can be used to organize and categorize components
* Components can be assigned to specific domains to create logical groupings
* Domains can be hierarchical, with a parent-child relationship represented by a dotted notation. For example, in the domain name `auth.sso.providers`, `auth` is the parent domain of `auth.sso`, and `auth.sso` is the parent domain of `auth.sso.providers`. The full domain name is stored in the `name` column using dotted notation.

## Usage examples

List all domains in the system:

```sql
SELECT *
FROM domains
ORDER BY name;
```

Find domains owned by a specific person or team:

```sql
SELECT *
FROM domains
WHERE owner = 'platform-team@example.com';
```

Get a count of components by domain:

```sql
SELECT 
  d.name AS domain_name,
  COUNT(c.name) AS component_count
FROM domains d
LEFT JOIN components c ON c.domain = d.name
GROUP BY d.name
ORDER BY component_count DESC;
```

Find all subdomains of a specific parent domain:

```sql
SELECT *
FROM domains
WHERE name LIKE 'payments.%'
ORDER BY name;
```

Find the direct subdomains of a specific domain (only one level down):

```sql
SELECT *
FROM domains
WHERE name LIKE 'payments.%'
  AND name NOT LIKE 'payments.%.%'
ORDER BY name;
```


# components

Schema reference for the components and components\_latest SQL views — time series of tracked components with merged collector metadata per commit.

```
components
components_latest
```

The `components` view is a time series representation of the collection of components that Lunar monitors.

A subset of this data is available in the `components_latest` view, which contains only the latest `git_sha` for each `pr`, in each component. To get the latest row for the default branch, you can filter this view by `pr IS NULL`.

## Schema

| Column           | Type        | Description                                                                                                                    |
| ---------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `component_id`   | `TEXT`      | The identifier for the component - e.g. `github.com/foo/bar/buz`                                                               |
| `timestamp`      | `TIMESTAMP` | The "committed at" UTC timestamp of the `git_sha`                                                                              |
| `git_sha`        | `TEXT`      | The Git commit SHA of the component JSON                                                                                       |
| `pr`             | `BIGINT`    | The pull request number if the commit is part of a pull request. Set to `NULL` for the default branch                          |
| `domain`         | `TEXT`      | The domain of the component in dotted path format (e.g. `payments.analytics.backend`)                                          |
| `owner`          | `TEXT`      | The owner of the component                                                                                                     |
| `tags`           | `TEXT[]`    | The tags associated with the component                                                                                         |
| `meta`           | `JSONB`     | Arbitrary metadata associated with the component                                                                               |
| `component_json` | `JSONB`     | The component JSON object resulting from merging component JSON deltas of the different collectors that ran for this component |

## Notes

* To get the component data for a given component version, you need to filter by `component_id`, and `git_sha`. This pair uniquely identifies each row in the table.
* For repeated updates to the same commit SHA, the `component_json` is updated with the latest JSON object. Individual updates (or "deltas") can be accessed through the [`component_deltas` view](/sql-api/views/component-deltas).

## Usage examples

Find the latest data for a particular component on the default branch. This query is guaranteed to return at most one row.

```sql
SELECT *
FROM components_latest
WHERE component_id = 'github.com/foo/bar/buz'
  AND pr IS NULL;
```

Code coverage of a component over time.

```sql
SELECT timestamp,
       component_json->'codecov'->'report'->'result'->'coverage'->>'total' AS coverage
FROM components
WHERE component_id = 'github.com/foo/bar/buz'
  AND pr IS NULL
  AND jsonb_path_exists(component_json, '$.codecov.report.result.coverage.total')
ORDER BY timestamp ASC;
```

Histogram of Go versions used across all components within a domain.

```sql
SELECT component_json->'go'->>'version' AS go_version,
       COUNT(*) AS count
FROM components_latest
WHERE jsonb_path_exists(component_json, '$.go.version')
  AND (domain = 'analytics' OR domain LIKE 'analytics.%')
  AND pr IS NULL
GROUP BY go_version
ORDER BY count DESC;
```


# component\_deltas

Schema reference for the component\_deltas SQL views — per-collector metadata deltas that merge into each component's full JSON over time.

```
component_deltas
component_deltas_latest
```

The `component_deltas` view is a time series representation of the collection of component metadata deltas that Lunar collects.

A subset of this data is available in the `component_deltas_latest` view, which contains only the deltas for the latest `git_sha` in each `pr`, in each component. To get the latest row for the default branch, you can filter this view by `pr IS NULL`.

## Schema

| Column                 | Type        | Description                                                                                           |
| ---------------------- | ----------- | ----------------------------------------------------------------------------------------------------- |
| `component_id`         | `TEXT`      | The identifier for the component - e.g. `github.com/foo/bar/buz`                                      |
| `timestamp`            | `TIMESTAMP` | The "committed at" UTC timestamp of the `git_sha`                                                     |
| `git_sha`              | `TEXT`      | The Git commit SHA of the component JSON                                                              |
| `pr`                   | `BIGINT`    | The pull request number if the commit is part of a pull request. Set to `NULL` for the default branch |
| `collector_name`       | `TEXT`      | The name of the collector that contributed this delta                                                 |
| `collection_timestamp` | `TIMESTAMP` | The UTC timestamp when the collector ran                                                              |
| `delta`                | `JSONB`     | The metadata delta that was contributed by the collector                                              |

## Notes

* To get the set of deltas for a given component version, you need to filter by `component_id`, and `git_sha`. The ordering of these deltas is defined by the `collection_timestamp`.
* Merging these deltas in the order of `collection_timestamp` will give you the final `component_json` object for a given `component_id` and `git_sha` that is found in the [`components` view](/sql-api/views/components).
* The `timestamp` column (not to be confused with `collection_timestamp`) is guaranteed to be the same for a given `git_sha` of a `component_id`. This timestamp will also match entries in other views such as [`components`](/sql-api/views/components) and [`checks`](/sql-api/views/checks).

## Usage example

Retrieve the deltas associated with the latest `git_sha` for a given component on the default branch.

```sql
SELECT *
FROM component_deltas_latest
WHERE component_id = 'github.com/foo/bar/buz'
  AND pr IS NULL
ORDER BY collection_timestamp ASC;
```


# initiatives

Schema reference for the initiatives SQL view — one row per initiative grouping policies around a shared goal or compliance requirement.

```
initiatives
```

The `initiatives` view provides information about initiatives in Lunar. Initiatives are collections of policies organized around a specific goal or purpose.

## Schema

| Column             | Type        | Description                                              |
| ------------------ | ----------- | -------------------------------------------------------- |
| `id`               | `TEXT`      | The identifier for the initiative                        |
| `description`      | `TEXT`      | A description of the initiative                          |
| `owner`            | `TEXT`      | The owner of the initiative                              |
| `manifest_version` | `TEXT`      | The version of the manifest that defines this initiative |
| `created_at`       | `TIMESTAMP` | When the initiative was created                          |

## Notes

* Initiatives are organized collections of policies designed around a specific goal or compliance requirement
* Each initiative can contain multiple policies and may span different components

## Usage examples

List all initiatives in the system:

```sql
SELECT *
FROM initiatives
ORDER BY id;
```

Find initiatives owned by a specific person:

```sql
SELECT *
FROM initiatives
WHERE owner = 'security-team@example.com';
```

Get a list of initiatives and the number of policies within each:

```sql
SELECT 
  i.id AS initiative_id,
  i.description,
  COUNT(p.id) AS policy_count
FROM initiatives i
LEFT JOIN policies p ON i.id = p.initiative_id
GROUP BY i.id, i.description
ORDER BY policy_count DESC;
```


# policies

Schema reference for the policies SQL view — one row per policy with its enforcement level and parent initiative.

```
policies
```

The `policies` view provides information about all policies defined in Lunar. Policies define specific checks or rules that are applied to components.

## Schema

| Column             | Type        | Description                                                                                                              |
| ------------------ | ----------- | ------------------------------------------------------------------------------------------------------------------------ |
| `id`               | `TEXT`      | The identifier for the policy                                                                                            |
| `description`      | `TEXT`      | A description of the policy                                                                                              |
| `enforcement`      | `TEXT`      | The enforcement level of the policy. Can be one of `draft`, `score`, `block-pr`, `block-release`, `block-pr-and-release` |
| `initiative_id`    | `TEXT`      | The identifier of the initiative this policy belongs to                                                                  |
| `manifest_version` | `TEXT`      | The version of the manifest that defines this policy                                                                     |
| `created_at`       | `TIMESTAMP` | When the policy was created                                                                                              |

## Notes

* Policies define specific checks that are applied to components
* Policies can have different enforcement levels that determine how violations are handled
* Policies are organized into initiatives

## Usage examples

List all policies with their enforcement levels:

```sql
SELECT id, description, enforcement
FROM policies
ORDER BY enforcement;
```

Find all blocking policies within a specific initiative:

```sql
SELECT *
FROM policies
WHERE initiative_id = 'security-compliance'
  AND enforcement IN ('block-pr', 'block-release', 'block-pr-and-release');
```

Get a count of policies by enforcement level:

```sql
SELECT 
  enforcement,
  COUNT(*) as policy_count
FROM policies
GROUP BY enforcement
ORDER BY policy_count DESC;
```


# checks

Schema reference for the checks and checks\_latest SQL views — every policy check evaluation with status, enforcement, component, and PR context.

```
checks
checks_latest
```

The `checks` view is a time series representation of the collection of checks that Lunar ran on components.

A subset of this data is available in the `checks_latest` view, which contains only the checks for the latest `git_sha` in each `pr`, in each component. To get the latest row for the default branch, you can filter this view by `pr IS NULL`.

## Schema

| Column             | Type        | Description                                                                                                             |
| ------------------ | ----------- | ----------------------------------------------------------------------------------------------------------------------- |
| `component_id`     | `TEXT`      | The identifier for the component - e.g. `github.com/foo/bar/buz`                                                        |
| `committed_at`     | `TIMESTAMP` | The "committed at" UTC timestamp of the `git_sha`                                                                       |
| `git_sha`          | `TEXT`      | The Git commit SHA of the component JSON                                                                                |
| `pr`               | `BIGINT`    | The pull request number if the commit is part of a pull request. Set to `NULL` for the default branch                   |
| `name`             | `TEXT`      | The name of the check that was run                                                                                      |
| `description`      | `TEXT`      | The description of the check that was run                                                                               |
| `initiative_id`    | `TEXT`      | The identifier of the initiative this check's policy belongs to                                                         |
| `policy_id`        | `TEXT`      | The identifier of the policy this check is part of                                                                      |
| `enforcement`      | `TEXT`      | The enforcement level of the check. Can be one of `draft`, `score`, `block-pr`, `block-release`, `block-pr-and-release` |
| `status`           | `TEXT`      | The status of the check. Can be one of `pass`, `fail`, `pending`, `error`, `skipped`                                    |
| `failure_reasons`  | `TEXT[]`    | Array of human-readable reasons the check failed. Set to `NULL` if the check passed.                                    |
| `staleness`        | `INTERVAL`  | The time since the check was last evaluated. Set to `NULL` if the check is not stale.                                   |
| `manifest_version` | `TEXT`      | The version of the manifest used for this check                                                                         |

## Notes

* To get the set of checks for a given component version, you need to filter by `component_id`, and `git_sha`.
* The `committed_at` is guaranteed to be the same for a given `git_sha` of a `component_id`. This timestamp will also match entries in other views such as [`components`](/sql-api/views/components) and [`component_deltas`](/sql-api/views/component-deltas).
* While a certain `git_sha` for a component is being evaluated, the row may use a stale result from a previous version of the code. In such cases, the `staleness` column will be set to the time since the check was last evaluated.

## Usage examples

Retrieve the checks associated with the latest `git_sha` for a given component, ordered by status.

```sql
SELECT *
FROM checks_latest
WHERE component_id = 'github.com/foo/bar/buz'
  AND pr IS NULL
ORDER BY  CASE status
            WHEN 'fail' THEN 1
            WHEN 'error' THEN 2
            WHEN 'pending' THEN 3
            WHEN 'pass' THEN 4
            WHEN 'skipped' THEN 5
          END ASC;
```

Retrieve time series data of the number of checks that passed, failed, had no data, or errored out over time.

```sql
SELECT
  committed_at,
  SUM(CASE WHEN status = 'pass' THEN 1 ELSE 0 END) AS passed,
  SUM(CASE WHEN status = 'fail' THEN 1 ELSE 0 END) AS failed,
  SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) AS pending,
  SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END) AS errored,
  SUM(CASE WHEN status = 'skipped' THEN 1 ELSE 0 END) AS skipped
FROM checks
WHERE component_id = 'github.com/foo/bar/buz'
  AND pr IS NULL
GROUP BY committed_at
ORDER BY committed_at ASC;
```

Retrieve the checks that are failing for all components with the tag `soc2`.

```sql
SELECT *
FROM checks_latest
WHERE component_id IN (
    SELECT component_id
    FROM components_latest
    WHERE 'soc2' = ANY(tags)
      AND pr IS NULL
  )
  AND pr IS NULL
  AND status = 'fail';
```

Retrieve the checks that are blocking PRs right now.

```sql
SELECT *
FROM checks_latest
WHERE pr IS NOT NULL
  AND status = 'fail'
  AND enforcement = 'block-pr';
```

Count the number of PRs that are blocked by checks for each domain.

```sql
WITH component_domains AS (
  SELECT
    component_id,
    domain
  FROM components_latest
  WHERE pr IS NULL
)
SELECT 
  domain,
  COUNT(DISTINCT pr) AS blocked_prs
FROM checks_latest
JOIN component_domains USING (component_id)
WHERE status = 'fail'
  AND enforcement = 'block-pr'
  AND pr IS NOT NULL
GROUP BY domain;
```

Total blocking checks for the domain `payments` over time.

```sql
WITH component_domains AS (
  SELECT
    component_id,
    domain
  FROM components_latest
  WHERE (domain = 'payments' OR domain LIKE 'payments.%')
    AND pr IS NULL
)
SELECT
  committed_at,
  COUNT(*) AS blocking_checks
FROM checks
JOIN component_domains USING (component_id)
WHERE status = 'fail'
  AND enforcement = 'block-pr'
  AND pr IS NOT NULL
GROUP BY committed_at
ORDER BY committed_at ASC;
```

All blocking failing checks of all PRs authored by Jane.

```sql
WITH janes_prs AS (
  SELECT component_id, pr
  FROM prs
  WHERE author_email = 'jane@example.com'
)
SELECT *
FROM checks_latest
JOIN janes_prs USING (component_id, pr)
WHERE status = 'fail'
  AND enforcement = 'block-pr';
```


# prs

Schema reference for the prs SQL view — pull request metadata mirrored from GitHub including author, status, and latest commit details.

```
prs
```

The `prs` view provides information about component pull requests. The information in this view is largely mirrored from the GitHub API, and is provided for convenience.

## Schema

| Column                    | Type        | Description                                                                 |
| ------------------------- | ----------- | --------------------------------------------------------------------------- |
| `component_id`            | `TEXT`      | The identifier for the component - e.g. `github.com/foo/bar/buz`            |
| `pr`                      | `BIGINT`    | The pull request number                                                     |
| `pr_opened_at`            | `TIMESTAMP` | The UTC timestamp when the pull request was opened                          |
| `pr_status`               | `TEXT`      | The status of the pull request. Can be one of `open`, `closed`, or `merged` |
| `title`                   | `TEXT`      | The title of the pull request                                               |
| `author_name`             | `TEXT`      | The name of the author of the pull request                                  |
| `author_email`            | `TEXT`      | The email of the author of the pull request                                 |
| `latest_git_sha`          | `TEXT`      | The latest Git commit SHA of the pull request                               |
| `latest_commit_timestamp` | `TIMESTAMP` | The "committed at" UTC timestamp of the latest commit in the pull request   |
| `latest_committer_name`   | `TEXT`      | The name of the committer of the latest commit in the pull request          |
| `latest_committer_email`  | `TEXT`      | The email of the committer of the latest commit in the pull request         |

## Usage example

Retrieve PR information for all the PRs that have failing checks in a component.

```sql
WITH failing_prs AS (
  SELECT DISTINCT pr
  FROM checks_latest
  WHERE component_id = 'github.com/foo/bar/buz'
    AND pr IS NOT NULL
    AND status = 'fail'
)
SELECT *
FROM prs
WHERE component_id = 'github.com/foo/bar/buz'
  AND pr IN (SELECT pr FROM failing_prs);
```


# catalog

Schema reference for the catalog and catalog\_latest SQL views — full catalog JSON snapshots produced by catalogers, with history over time.

{% hint style="info" %}
**Coming Soon** — This feature is not yet available.
{% endhint %}

```
catalog
catalog_latest
```

The `catalog` view provides a historical timeseries of catalog JSONs generated by running catalogers over time. Each row represents a catalog snapshot at a specific point in time.

A subset of this data is available in the `catalog_latest` view, which contains only the most recent catalog entry. The `catalog_latest` view is guaranteed to return at most one row.

## Schema

| Column         | Type        | Description                              |
| -------------- | ----------- | ---------------------------------------- |
| `timestamp`    | `TIMESTAMP` | When the catalog was generated           |
| `catalog_json` | `JSON`      | The complete catalog data in JSON format |

## Notes

* The catalog view contains the history of all catalogs generated by catalogers
* Each row represents a complete catalog snapshot at a specific point in time
* The catalog JSON contains the full inventory of components and their metadata. The format is defined on the [Catalog JSON](/docs/catalog-json) page
* This view enables historical analysis of how the component landscape has changed over time
* The catalog\_latest view is identical to the catalog view in structure, but only contains the single most recent catalog entry

## Usage examples

Get the most recent catalog entries:

```sql
SELECT timestamp, catalog_json
FROM catalog
ORDER BY timestamp DESC
LIMIT 5;
```


