> For the complete documentation index, see [llms.txt](https://docs-lunar.earthly.dev/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs-lunar.earthly.dev/release-notes/product/2026.md).

# 2026

## 2026-09-15 <a href="#product-2026-09-15" id="product-2026-09-15"></a>

### Migrations and upgrade notes

* A `customization.checks_template` now renders once for the whole GitHub checks report instead of once per component. The template receives every matched component and pull-request scope in `.Components` — each carrying the component's full and short name, its repository-relative path, details URL, check sections, bypassed checks, evaluation state, and a one-line verdict summary — and it owns the report title, the component headings, and the evaluation notices that Lunar previously assembled around the rendered output. A template that reads the top-level `.Component`, `.DetailsURL`, `.Sections`, or `.Bypassed` still renders a report covering a single component scope, as GitLab merge-request notes and single-component pull requests do; on a GitHub report covering several scopes, Lunar logs the error and posts the built-in report instead, so move those accesses inside `{{range .Components}}` to keep customizing reports on a pull request that matches more than one component. When a report exceeds GitHub's size limit, Lunar re-renders it with `.Compact` set, so a custom template can supply its own condensed layout; the built-in layout then keeps each component's verdict and details link. The built-in report leaves components with nothing to report out of pull-request comments, while a custom template receives them and decides which to show. The full field contract and an example are documented under `customization.checks_template` in the Lunar configuration reference. See [Customizable checks report templates](#product-checks-report-templates).

### Security

* Collection data written by a CI job that authenticates with its forge-issued OIDC token is now stored together with the job's ref and whether the forge reports that ref as protected, and a commit Lunar knows to be on the component's default branch ignores records written from an unprotected ref when its Component JSON and its policy input are assembled. This closes a gap where a branch cut from the default branch with no new commits shares the default branch's head commit, so a job running on the unprotected branch could write collection data that the default branch's status then read. Records carrying no ref information — those written by the Hub's own scheduled runs, by a static hub token, by an out-of-band `lunar collect`, or before this release — count exactly as they did before, and nothing is excluded away from the default branch, so a pull request's data still comes from its own pipeline. See [CI job identity for GitHub Actions and GitLab CI](#product-ci-job-identity).

### Features

#### CI job identity for GitHub Actions and GitLab CI <a href="#product-ci-job-identity" id="product-ci-job-identity"></a>

A GitHub Actions or GitLab CI job can now authenticate to the Hub with the OIDC token its forge issues it, instead of carrying the shared Hub service token. The Hub verifies the token against the forge's published JWKS and against the audience it is configured with, and derives the job's identity from the token's claims: the repository, the actor who triggered it, the ref, and the commit.

Because a forge expires such a token within minutes, the Hub trades a verified token for a Hub session that lasts the job. The session carries the same coordinates, so a job's authority is confined to its own repository and its own commit — collecting and reading for the commits its ref binds it to — while everything outside that, such as creating a gate bypass, stays refused. A job whose repository is not yet in the catalog is refused until it is cataloged.

To use it, the job requests an OIDC token for the Hub's audience and passes it as `LUNAR_HUB_TOKEN`; the CLI performs the exchange on its own. Trusted issuers follow the forge configuration the Hub already has: GitHub Actions on each configured GitHub host, including GitHub Enterprise Server, and GitLab CI on each configured GitLab host. Buildkite jobs continue to use the service token, because a Buildkite OIDC token does not name the forge repository the build belongs to.

#### Choosing which configuration file a Hub loads <a href="#product-configuration-entry-point" id="product-configuration-entry-point"></a>

The in-repo path on a configuration URI now selects which configuration file is loaded, so one repository and one branch can serve several Hubs. `lunar hub pull github://acme-corp/lunar-repo/lunar-config.dev.yml@main` loads `lunar-config.dev.yml`, while `lunar hub pull github://acme-corp/lunar-repo@main` keeps loading `lunar-config.yml` at the repository root exactly as before. A path naming a directory resolves to the `lunar-config.yml` inside it, so a `configs/prod/` layout works too, and the GitLab form takes the path after the usual `/-/` marker.

Each entry point reads its own fragments: the fragment directory is the entry point's name with the extension replaced by `.d`, so `lunar-config.dev.yml` merges `lunar-config.dev.d/` and never the default's `lunar-config.d/`. A relative `uses:` resolves from the entry point's own directory — for the sibling-file layout that is the configuration root, so both entry points import a shared `collectors/` and `policies/` tree with the same `./collectors/foo`. Script provenance stays repository-root-relative, so a link in a pull-request comment opens the file that actually declared the script, fragment directory included.

A path that names nothing, climbs out with `..`, is absolute, or resolves through a symlink outside the repository fails the pull instead of falling back to the root configuration, and it fails even at a commit the Hub has already pulled.

A Hub loads one entry point at a time, and a configuration version is the commit alone, so pointing an existing Hub at a different entry point takes effect on its next new commit.

#### Per-person SQL API credentials <a href="#product-per-person-sql-api-credentials" id="product-per-person-sql-api-credentials"></a>

Someone signed in to the Hub with `lunar login` now receives a SQL API credential of their own instead of the shared read-only user. `lunar sql connection-string` mints a personal Postgres role for them, a member of the shared SQL API role, so it carries exactly the same read-only access to the SQL API views and the same session limits. The role expires after the lifetime the Hub is configured with — 30 days by default — and every call rotates its password and extends the expiry, so a person has one live credential at a time and re-running the command is how a lost or leaked one is replaced.

The Hub service token still receives the shared connection string, unchanged, so BI tools and other static integrations are unaffected. CI jobs cannot obtain a SQL credential at all.

Who holds SQL access is now answerable. `lunar sql credentials ls` lists every personal credential the Hub has issued — active, expired, revoked, or dropped — and the same record is queryable through a new `sql_credentials` view in the SQL API. A person's access is ended with `lunar sql credentials revoke <login>`, which disables their role and terminates its open sessions immediately; `lunar logout` does not, because the SQL credential is independent of the CLI session. Because each person queries under a role of their own, `pg_stat_activity` and the Postgres logs name them rather than one shared user.

On a self-hosted Hub an operator can turn personal credentials off, in which case signed-in people are refused and told to use the service token.

#### Signing in to Lunar as yourself <a href="#product-personal-sign-in" id="product-personal-sign-in"></a>

A person can now use Lunar under their own identity instead of the shared Hub service token. `lunar login` signs in with a GitHub or GitLab account on a forge host the Hub accepts, and the Hub issues a short-lived session only after checking at the forge that the presented token was issued to the Hub's own OAuth app — a token from any other application, including a personal access token, is refused. The Hub keeps neither the access token nor the refresh token.

Every request made with that session is authorized by the person's own role on the repository the request touches, as the forge reports it, so a command run by a signed-in engineer is allowed or denied according to their forge permissions rather than a single credential shared by everyone. `lunar whoami` reports the identity the Hub established for the current credential — a signed-in person, a CI job, or the service token.

Sessions are short-lived: at most 12 hours, and shorter when the forge token expires sooner. The CLI renews both the forge token and the session before they expire, and `lunar logout` forgets the login and revokes the forge token where the forge allows it. Personal sign-in is available on an installation whose operator has registered the forge OAuth apps to accept logins from; the service token and CI job credentials continue to work unchanged.

### Improvements

* Refreshing the materialized data behind the SQL API `components` and `components_latest` views — and the dashboards built on them — no longer stalls on the step that removes the rows it is about to rewrite. That step previously combined two match conditions into one delete per table, which made PostgreSQL scan the whole projection and, on a large refresh, rescan the staged rows repeatedly; each table's rows are now removed by two separate statements in the same transaction, so each can use its index. In a synthetic test with 300,000 projection rows and 50,000 staged rows, the two clears completed in about 0.2 and 0.4 seconds where they previously ran past a 12-second limit. Component data reaches the views and dashboards sooner on large installations, and query results are unchanged.
* Refreshing the materialized data behind the SQL API `prs` view no longer scans the whole stored pull-request projection to remove the rows it is about to rewrite. A narrow refresh — one triggered by activity on a few pull requests — now looks up only the affected `(component, pull request)` rows through the existing index, which took 1.75 ms rather than 364 ms in a benchmark with 300,000 stored rows and 100 rows to replace. Rows that stop deriving, for example after a repository association is removed from a component, are still cleared, and the view's contents are unchanged — only cheaper to keep current.
* A new `Earthly Lunar` check run or Lunar results comment on GitHub is now written by the App with the lowest App ID among those configured for the repository's host and owner, instead of the first App in configuration order, so reordering the configured Apps or adding another one leaves the posting identity unchanged. On an installation where the previously first-configured App is not the lowest-numbered one, new posts now come from a different App, which matters where a repository requires a particular App as the source of its check; adding a lower-numbered App or removing the selected one changes only the App used for new posts and never reassigns an existing one. Read spreading across the App pool and clone-token selection are unchanged, and an organization with a single App behaves exactly as before. See [Multiple GitHub Apps for one organization](#product-multiple-github-apps-per-owner).
* The navigation table on the pull-request details dashboard now heads its link to the change request with the forge's own noun — "PR URL" for GitHub and GitHub Enterprise Server components, "MR URL" for GitLab ones — instead of "Repo URL", which named the repository while the link opened the pull or merge request.
* The background refresh that materializes the data behind the SQL API `components` and `components_latest` views — and the dashboards built on them — no longer repeats the same newest-blob lookup for every historical version of a component. Each component-name and commit pair is now resolved once, and Component JSON is fetched only after the candidate rows have been deduplicated, so the refresh cost no longer multiplies with the number of Lunar configuration versions a component has accumulated. On a synthetic dataset of 100 commits across 200 component versions, the component derivation took 311 ms instead of 10.8 s and the hour-window derivation 440 ms instead of 11.1 s, returning identical rows; these are benchmark figures rather than production latencies.
* The Script runs dashboard loads much faster when filtered by component or commit SHA on large installations. The component filter is now applied as separate equality conditions on the indexed owner and repository columns, the SHA filter is applied while the runs are scanned rather than afterwards, and the lookup that resolves each run's component no longer scans the whole components table — together taking the runs table and its pagination count from several seconds to a few milliseconds on an installation with 63 million recorded script runs.
* A gate bypass created or revoked from the CLI by a logged-in user is now recorded against the identity the Hub authenticated rather than a self-declared name: the ledger's `actor` is that login, and `verified_role` is the role the Hub verified on the component (`github:maintain`, `gitlab:maintainer`), so the row reads as an identity-checked override wherever bypasses are audited — `lunar policy bypass-ls`, the SQL API `bypasses` and `bypassed_checks` views, and the dashboards. Passing `--actor` naming anyone other than the logged-in user is rejected, and omitting it records the bypass as that user. A bypass created with the Hub's service token behaves exactly as before: the actor is whatever the caller supplied, unverified, with `verified_role` left unset — which is how an audit tells a vouched-for override from an unverified one — and it is now rejected outright if no actor is supplied. See [Signing in to Lunar as yourself](#product-personal-sign-in).

### Bug fixes

* A component's rows can no longer disappear from the materialized data behind the SQL API `checks` and `checks_latest` views, and the dashboards built on them, while a newly created version of that component is still waiting for its repository association to be recorded — the window that follows a Lunar configuration update. The background refresh scoped to a component picked a representative version without regard to whether it was associated with a repository yet, so it derived no rows for the component name while still clearing the rows it already had, and a later refresh of the same component repeated the loss instead of restoring it. The refresh now chooses a version that is associated with a repository, so existing coverage survives and is restored on the next refresh.
* Components and domains dropped from the catalog by a cataloger re-merge are no longer counted on the Home, Domains, Domain details, and Initiatives dashboards. Because such entries are only marked as removed rather than deleted, the scorecards behind those pages kept counting them, so a reduced catalog still showed the pre-reduction component and domain counts, tags, passing-check totals, and score, while the Catalogers dashboard already showed the reduced numbers. The initiative passing and total component counts, and the domain hierarchy they are built from, are corrected the same way — the hierarchy previously also spanned every Lunar configuration version ever published rather than the current one. The corrected numbers appear on the next periodic refresh of the data behind these panels; no action is required on upgrade.
* An existing `Earthly Lunar` check run or Lunar results comment on GitHub is now updated with the App that created it, resolved from the creator GitHub reports on the resource, rather than with whichever App the Hub would use for a new post. On an organization configured with more than one GitHub App, an edit attempted by a different App was rejected with `403 Invalid app_id`, which could leave the check run or comment stale or, for a check run, produce a second run of the same name at the commit. Both lookups now page through every check run and every comment on the pull request, so a post that has scrolled onto a later page is found and updated instead of duplicated, and bypass and revoke writes follow the originating App too — revoking a completed run creates its replacement under the same App. If the creator an existing post reports is missing, or belongs to an App no longer configured for that organization, Lunar reports an error for that resource instead of editing it with the wrong credentials. See [Multiple GitHub Apps for one organization](#product-multiple-github-apps-per-owner).
* On GitHub, the Lunar pull-request comment and the `Earthly Lunar` check run now cover every component a pull request matches, instead of reporting whichever component posted last. The components a commit's collection was dispatched for are all included: each appears in the comment under its own heading, in a stable order, with its own details link to that component's dashboard, and the check run's title, summary, and conclusion are computed from the required checks of all of them together. A component that passes, or that has no applicable policy at all, can therefore no longer overwrite a sibling component's failing required checks and leave a blocked pull request reporting success with no trace of the failures — the case that made the check unusable as a merge gate on a monorepo. Required-ness is resolved per component, as is any active bypass, so a bypass granted for one component no longer clears another component's failures, and unbypassed failures keep blocking. While a matched component is still being evaluated the report says so and is refreshed once its results arrive. A pull request matching a single component keeps the report layout it had, and GitLab merge requests are unchanged.
* A failure posting one surface of a commit's results no longer stops the rest from being posted. Previously the first failure ended the posting run, so a problem writing the Lunar results comment on a pull request could leave the `Earthly Lunar` merge-gate check run unwritten; each surface now posts independently and every failure is still reported and retried.
* The Component filter on the Script runs dashboard now lists the components that have script runs, instead of staying empty. The query behind the dropdown scanned the whole script-run history on every page load, so on a large installation it was cut off by the request timeout before returning anything; the list is now read from the per-repository run state, which holds the same owner and repository spellings the listing filters on.

## 2026-09-08 <a href="#product-2026-09-08" id="product-2026-09-08"></a>

### Bug fixes

* The `Earthly Lunar` GitHub check run and GitLab commit status are again created only once Lunar has policy results for a commit, instead of being written as soon as collection starts. On a pull request where no visible policy check applied, the running placeholder was never replaced by a verdict, so the check stayed in progress indefinitely — and blocked the merge wherever `Earthly Lunar` is a required check. See [the running Lunar check](#product-2026-09-07).
* A script run — a collector, policy, or cataloger — is no longer failed outright when its container image cannot be pulled for a temporary reason, such as registry pull throttling or a registry that is briefly unreachable. Lunar now retries the run on a fresh pod within its existing five-attempt budget, so a momentary registry problem no longer cancels every run packed onto the same pod. An invalid image name or a container configuration error is still treated as permanent and fails the affected runs immediately.

## 2026-09-07 <a href="#product-2026-09-07" id="product-2026-09-07"></a>

### Features

#### Instance-wide GitLab tokens and token pools <a href="#product-gitlab-host-wide-token-pools" id="product-gitlab-host-wide-token-pools"></a>

A GitLab access token no longer has to be bound to a group. A token configured without a group is host-wide: it serves every group on its host that no group-bound token covers, so one instance service account can cover a whole GitLab Dedicated or self-managed instance, and a new group is brought into scope by inviting the account to it rather than by changing configuration. Group-bound tokens still take precedence, and the most specific matching group still wins, so a group that needs its own bot keeps it.

Several tokens configured for the same scope now form a pool instead of being rejected. Lunar spreads read traffic across the pool — project metadata, default branches, commit and merge-request listings, a merge request's changed files, and the tier probe behind the merge gate — so a busy group or a busy instance draws on every account's GitLab rate limit. Everything Lunar writes, or that has to keep one identity, stays on the first token listed for the scope: commit statuses, merge-request notes, project badges, webhook and merge-gate provisioning, the webhook signing secret, and Git clones. A scope with a single token behaves exactly as before.

Every account in a pool should hold the same role, so that what Lunar can do in a group does not depend on which token served a call.

### Improvements

* The check that decides whether a commit's code collectors have finished — the check a policy run's input data waits on — no longer reads that commit's run history twice. On a commit whose runs have accumulated across many Lunar configuration versions, for example a commit reused by hundreds of successive component generations, the check previously scanned the whole population once to find the latest code run and again to evaluate the current collection cycle, and on the largest cases it took long enough that a policy run's request for its input data timed out and was reported as an internal error. It now probes a bounded set of runs, reads the matching code runs once, and derives the latest six-hour collection cycle in the same pass: measured on a production-scale installation, a commit with roughly 690,000 candidate runs dropped from about 1.4 seconds to about 0.7 seconds, while an ordinary commit stayed near a millisecond. Runs recorded under superseded or removed component identities are still counted, so the answer the check gives is unchanged.
* The `Earthly Lunar` GitHub check run and GitLab commit status now appear as soon as Lunar starts collecting for a commit, reading "Earthly Lunar policy is running" and pointing at where the results will appear, instead of showing nothing until the commit's first policies finish. The running state is written only when Lunar has no status at that commit yet, so a verdict already posted is never flipped back to running, and a forge error while writing it does not stop the collection.
* The quiet-until-first-failure setting for the Lunar results comment is now spelled `customization.pr_comments.mode: only_failures`, matching the snake\_case used by every other key and value in the `customization` block. The original `only-failures` spelling remains accepted — in the Lunar configuration, in the generated JSON schema used by schema-aware editors, and in configurations already pulled — so no change is required, and it is treated exactly as `only_failures`. The documentation now shows only the underscore spelling. See [quiet-until-failure results comments](#product-2026-08-18).
* The quiet-until-first-failure setting for the Lunar results comment is now spelled `customization.pr_comments.mode: only_failures`, matching the snake\_case used by every other key and value in the `customization` block. The original `only-failures` spelling stays accepted — in the Lunar configuration, in the generated JSON schema used by schema-aware editors, and in configurations already pulled — and is treated exactly as `only_failures`, so no change is required. The validation error for an unknown mode and the documentation now show only the underscore spelling. See [quiet-until-failure results comments](#product-2026-08-18).

### Bug fixes

* `lunar policy bypass-ls` now reports a bypass as active only when it is still in effect on every bound the bypasses dashboard and the SQL API `bypasses` view already applied. A PR-gate bypass pinned to a commit the pull or merge request has since moved past, and one whose pull or merge request has merged or closed, are no longer listed as active, and `--active` no longer returns them — previously the listing considered only the revocation and expiry recorded on the bypass itself, so a commit-bound break-glass on a finished merge request could be reported as active indefinitely. Both surfaces now derive liveness from a single definition in the Hub, so the command and the dashboard agree row for row. What a bypass masks at the merge or release gate is unchanged.
* A GitHub Actions workflow run that has finished on GitHub is no longer left recorded as queued or in progress, which previously kept every policy for the affected commit pending indefinitely and made `lunar policy ok-release` poll until its timeout. Two things caused it. Deliveries for the same run — `queued`, `in_progress`, and `completed` — are handled concurrently and can finish out of order, and the later non-terminal delivery overwrote the terminal one; a completed run attempt is now terminal, and a late non-terminal delivery for it changes neither its status nor its timestamp. Separately, a `completed` delivery could reach the Hub and never be recorded, because component matching, the affected-file lookup, or commit enrichment failed or returned early first; the run's status is now stored before any of those steps, so the delivery closes an already-tracked run either way. A GitHub retry is unaffected: GitHub increments the run attempt, so the retry gets its own row and release doneness keeps following the highest attempt. Buildkite builds, whose jobs are retried within one build rather than as a new attempt, can still return to running after completing. Runs already stuck non-terminal before this release are not repaired by it.
* `lunar policy ok-release` and `lunar policy ok-pr` no longer wait for policy results the gate can never act on. Previously, if any policy evaluated at the commit had not finished — including policies at `score`, `report-pr`, or, for a release gate, `block-pr` — the gate kept polling until its `--timeout`, even though only checks at `block-release` or `block-pr-and-release` (plus `block-pr` for a PR gate) can decide the verdict. The gate now answers immediately, with a message that no policies gate the check, when the Lunar configuration applies no gating policy to the component. It still waits whenever a gating policy applies, whenever a result already recorded at the commit carries a gating enforcement level, and whenever Lunar cannot determine which policies apply — so an unresolvable configuration keeps blocking rather than releasing. This extends the earlier fix for gates with nothing to wait for; see [gates settling immediately when no policy blocks](#product-2026-07-28).
* Under `customization.pr_comments.mode: only_failures`, a check that is still waiting on data no longer posts the results comment — only a failing, erroring, or unknown check does. Previously a pending check counted as not passing, so on a component whose data arrives from CI the first evaluation almost always posted the comment and the quiet mode never engaged there. Pending checks remain visible in the `Earthly Lunar` GitHub check run and GitLab commit status, an existing comment keeps updating as before, and `always` mode is unchanged.
* A policy run can no longer be discarded because the download link for its input bundle had already expired. Since the Hub began sharing its cache of policy input bundles across replicas, the cached entry held a signed download URL; when that URL was signed just before the Hub's temporary object-storage credentials rolled over, every run that read the entry for the rest of its lifetime failed to fetch the bundle with an expired-token error, and because each retry fetched the same cached URL, the affected evaluations were dropped rather than delayed. The Hub now caches the bundle's storage location instead and signs a fresh download URL for every request, so the bundle is still built and uploaded only once.
* Setting `customization.pr_comments.mode: only_failures` in the Lunar configuration now changes when the results comment is posted. The mode was recorded when the configuration was synced but never read back when the comment was posted, so every installation behaved as `always` and the comment was posted on pull and merge requests whose checks all passed. See [quiet-until-failure results comments](#product-2026-08-18).
* The `meta` annotations and the `failureText` template written on a policy's `uses:` entry in `lunar-config.yml` now reach every sub-policy the entry imports, as the configuration reference describes. Previously both fields were dropped when the imported policies were loaded, so only policies declared inline in the Lunar configuration stored queryable `meta` on the SQL API `policies` view or rendered custom failure prose in the checks report; an import kept an empty `meta` and the default assertion bullets. Where a key is set both at the import site and by the imported plugin, the import site's value wins and the plugin's other keys are kept; an entry that sets neither field leaves the plugin's own values untouched. See [Queryable policy annotations with `meta`](#product-2026-08-25) and [Custom failure text per policy](#product-policy-failure-text-templates).
* Under `customization.pr_comments.mode: only_failures`, a check that is still waiting on data no longer posts the results comment — only a failing, erroring, or unknown check does. Previously a pending check counted as not passing, so on a component whose data arrives from CI the first evaluation almost always posted the comment and the quiet mode never engaged there. Pending checks stay visible in the `Earthly Lunar` GitHub check run and GitLab commit status, an existing comment keeps updating as before, and `always` mode is unchanged.
* Setting `customization.pr_comments.mode: only_failures` in the Lunar configuration now changes when the results comment is posted. The mode was recorded when the configuration was synced but never read back when the comment was posted, so every installation behaved as `always` and the comment appeared on pull and merge requests whose checks all passed. See [quiet-until-failure results comments](#product-2026-08-18).

## 2026-09-02 <a href="#product-2026-09-02" id="product-2026-09-02"></a>

### Improvements

* Lunar Hub's conditional requests to the GitHub API now keep saving rate-limit budget on repositories it revisits regularly: a cached response's retention is renewed every time GitHub confirms the resource is unchanged, instead of expiring a fixed period after the original full response and forcing a full re-fetch of a resource that never changed. Cached entries for repositories that stop being polled still age out as before, and the renewed entry keeps GitHub's live rate-limit headers.
* A GitHub pull-request event that never triggers collection — labeling, editing, assigning, requesting a review, marking ready for review, and the other metadata activity types — no longer makes Lunar list the pull request's changed files and commits on GitHub, so those deliveries stop consuming GitHub API rate-limit budget on installations with busy repositories. Lunar still refreshes the stored metadata of a pull request it already tracks, such as its title and state, so the dashboards and the SQL API `prs` view stay current; a pull request Lunar is not already tracking is no longer recorded from a metadata event, since which components a pull request touches is established when it is opened, synchronized, or reopened.
* Lunar now reuses the list of files a GitHub pull request changes across every event it receives for the same base and head commit — the pull-request event, the workflow-run events that follow it, and any redelivery — instead of listing the pull request's files from GitHub again each time. The stored result is identified by the two commit SHAs, so it describes exactly that diff and cannot go stale, and reusing it reduces the GitHub API rate-limit budget Lunar spends on repositories with active pull requests. Buildkite builds still list the files each time, because their payload does not carry the base commit needed to identify the diff.
* The Hub's periodic repository sync no longer re-reads a GitHub branch's commit history when nothing has been pushed to it. Once a sync has walked a branch to completion, later passes confirm the branch head with a single lightweight Git-ref lookup and skip the commit listing entirely, instead of always making one commit-listing call plus one head lookup. That ref lookup is served through the Hub's conditional-request (ETag) cache, so on an unchanged branch it revalidates as a rate-limit-free response and consumes no GitHub REST rate-limit budget once the cache has warmed; with personal-access-token authentication or ETag caching turned off, the unchanged branch costs one request instead of two. A branch whose head has moved, or one the Hub has no completed walk recorded for, takes the full history walk exactly as before, so gaps left by a missed webhook delivery still heal. Every repository takes one normal, complete sync after the upgrade before the shorter path applies, and GitLab-hosted repositories are unaffected.
* The Hub's periodic repository sync on GitHub no longer asks GitHub for the commit list of every open pull request on every reconciliation pass. Once a sync has persisted and associated a pull request's complete commit listing, Lunar records the base branch and head commit it did that for, and a later sync whose pull request still reports the same base branch and head skips the commit request while still refreshing the pull request's mutable details such as title, state, and author. A changed base branch or head, a pull request Lunar has not fully reconciled yet, or a commit listing GitHub could have truncated all fall back to the full listing, so the sync keeps healing anything a missed webhook left behind. Existing pull requests are reconciled in full once after the upgrade before the skip applies to them, and on installations with many open pull requests this removes a large share of the GitHub API requests the sync consumes.
* The SQL API views — `checks`, `checks_latest`, `components`, `components_latest`, `prs`, `policies`, `domains`, and `initiatives` — are now read directly from Lunar's materialized tables on every installation, instead of deriving recent rows from raw commit and run history at query time and combining them with an older projection. The materialized data is kept current in the background as collections and policy runs land, so the views return the same rows for far less work; serving the current check slice from the stored projection measured 1.28 seconds against 3.35 seconds on an installation with roughly 30,000 components. Dashboard panels that read those views benefit the same way.

### Bug fixes

* A failing check's assertion messages in the checks report posted with check results — the GitHub Check Run text, the GitHub pull-request comment, and the GitLab merge-request note — now appear in the order the policy asserted them, both in the default bullet list and where a custom failure-text template ranges over `failure_msgs`. Previously the messages of one check were stored without a recorded position and came back in an arbitrary order, so a policy asserting one condition and then another could render them the other way round. Checks recorded before this release keep whatever order they already had, since their original order was never stored.
* A script run that takes longer than 30 minutes now has its exit code, logs, and results recorded. Previously the monitoring of a script pod gave up after 30 minutes, so a longer collector, policy, or cataloger run was reported as failed with exit code -1 even when the script itself was still running or had already succeeded. Monitoring now lasts as long as the run's own execution budget.
* A component that has never had any activity recorded against it — common on an installation whose Lunar configuration and repositories rarely change — could be absent from the materialized data behind the SQL API `checks` and `checks_latest` views and the dashboards built on them, so it rendered as a component with no checks rather than reporting an error. The nightly deep reconcile now also walks the full component list and queues a refresh for any component that holds no materialized check rows, so such gaps close on their own within a day instead of persisting until the component next sees activity.
* Reading a pull request's changed files and its commits from GitHub is now spread across an organization's GitHub App pool, along with the other repository and workflow reads. These paginated reads were still pinned to the first App listed for the organization, so on a high-activity organization they consumed that one installation's REST API rate-limit budget instead of the pool's; identity-sensitive writes such as check runs and pull-request comments continue to use the first App. An organization with a single App behaves exactly as before. See [Multiple GitHub Apps for one organization](#product-multiple-github-apps-per-owner).
* A GitHub pull request with more than 100 commits now has every one of its commits ingested and associated with the pull request before its head commit is processed. Lunar previously read only the first page of the pull request's commits, so on a large pull request the head commit announced by the webhook could be dispatched for collection and policy evaluation before it had been stored, and the checks for that pull request failed with `failed to get commit: not found`. GitHub caps that listing at 250 commits, and a listing that is capped or that does not end at the announced head is still left to the Hub's periodic reconciliation rather than being treated as complete.
* The Queued tab on the runs and script-runs dashboards now honors the dashboard's filters, instead of showing the whole fleet's backlog beside filtered completed runs. Component, script name, script name prefix, script type, SHA, and pull request now scope the queued rows, the pagination count, and the "Queued (N)" count in the tab title, so the count always agrees with the rows on screen; arriving from a component, policy, collector, or pull-request page therefore lands on a Queued tab already scoped to that context, including monorepo sub-components. The status and rerun filters remain deliberately without effect, because a queued run has not started and has no terminal status to match or rerun to deduplicate. Queued rows carrying a pull request now link to the pull-request details dashboard rather than the component details, since the pull request is now read from jobs awaiting dispatch instead of being discarded.
* Reopening a closed GitHub pull request now dispatches code collection for the components its changes touch, so its checks are re-evaluated instead of waiting for the next push to the branch. This also covers a head commit that moved while the pull request was closed, which previously left the reopened pull request showing results for the old head.
* A script run — a collector, policy, or cataloger — whose container is killed for exceeding its memory limit (an `OOMKilled` termination, or exit code 137) is now retried on a fresh pod instead of being recorded as a failed run. The same now applies when a run's outcome cannot be observed at all: if its exit code cannot be read or its results cannot be submitted, every run sharing that pod is retried rather than accepted as failed, because there is no way to tell which individual results were lost. A retry reuses the run's existing identity, and a script that exits non-zero on its own is still treated as a genuine failure and is not retried.
* A collector, policy, or cataloger run executing in Kubernetes is no longer failed and retried as a whole batch when the Kubernetes watch used to observe its pod is interrupted. An expired resource version (`410 Gone`) is now relisted and a watch stream closed by the API server is reconnected with bounded backoff, instead of ending observation and recording the affected runs with a sentinel exit code before the batch is retried on a fresh pod. A run's exit code is still reconciled with a direct read of the pod when the watch ends, and a deleted pod, a missing container, or an expired context remain terminal and are reported with the same diagnostics as before.
* A pull request whose diff reaches GitHub's 3,000-file listing limit is no longer scoped against a possibly incomplete file list when the event carries no authoritative changed-file count — a workflow-run delivery, or a Buildkite build. Lunar now treats such a listing as unknown and collects every component the pull request could touch, as it already did when the reported count showed the list was incomplete, instead of silently skipping components whose paths fell beyond the first 3,000 files.

## 2026-08-25 <a href="#product-2026-08-25" id="product-2026-08-25"></a>

### Breaking changes

* The SQL API `prs` view now reports `pr_status` with one vocabulary for every forge — `open`, `closed`, or `merged`, as the view's documentation already described — instead of each forge's raw value. A merged GitHub pull request reports `merged` rather than `closed`, and an open GitLab merge request reports `open` rather than `opened`, so queries, reports, and dashboards that match the previous per-forge values have to be updated. GitHub pull requests merged before this release keep reporting `closed`, since the stored data does not distinguish them from abandoned ones.
* A Lunar configuration that sets a wildcard in `components.<id>.branch` — for example `branch: release-*` — now fails the configuration sync with an error naming the component and the value. `branch` is a single branch name: name the branch explicitly, or omit it to track the repository's default branch. A pattern was previously honored only in part — it matched when deciding which components a push reached, while everything downstream treated the component as tracking no resolvable ref, so the component's current commit could only ever be advanced by a push to a matching ref and had no way to recover from a missed one. Wildcards in the component key itself and in `paths`, where they are documented, are unaffected.

### Features

#### Multiple GitHub Apps for one organization <a href="#product-multiple-github-apps-per-owner" id="product-multiple-github-apps-per-owner"></a>

A Lunar Hub can now use more than one GitHub App installation for the same organization. Lunar spreads repository and workflow reads across the Apps, so a high-activity organization can use each installation's independent REST API rate-limit budget instead of exhausting a single installation.

Check runs and pull-request comments stay on the first App listed for the organization, because GitHub requires the App that created one to update it. Lunar spreads repository webhook setup and removal across the pool along with the reads. An organization with one App behaves exactly as before. See [Avoid GitHub rate limiting](/install/git-platforms/github.md#avoid-github-rate-limiting) for setup instructions.

#### Custom failure text per policy <a href="#product-policy-failure-text-templates" id="product-policy-failure-text-templates"></a>

A policy entry in `lunar-config.yml` can now set `failureText`, a Go `text/template` that replaces what a failing check writes into the checks report — the GitHub Check Run text, the GitHub pull-request comment, and the GitLab merge-request note — so a plugin's wording can be wrapped in your own prose and links without forking the policy.

The template renders as Markdown and can read three namespaces: `{{ .check.failure }}` for the whole default bullet list of failing assertions as one block, `{{ range .check.failure_msgs }}{{ .message }}{{ end }}` for the assertions one at a time, `{{ .check.name }}`, `{{ .meta.<key> }}` for a value from the policy's `meta`, and `{{ .policy.name }}` and `{{ .policy.description }}`. Because Go's template grammar reads a hyphen as subtraction, a hyphenated `meta` key has to be read with `{{ index .meta "enforce-control" }}`.

Setting `failureText` is a full override rather than an addition: a template that references neither `{{ .check.failure }}` nor `{{ .check.failure_msgs }}` produces a comment that says the check failed without saying why. Omitting the field keeps today's built-in assertion list, and a template that fails to parse or renders to nothing falls back to that same built-in list, so a typo costs the custom wording and never the failure detail. The field applies to every sub-policy in the entry it is written on, and it changes only the failing check's own block within the report — the report as a whole is still governed by [Customizable checks report templates](#product-checks-report-templates).

* The final verdict block `lunar policy ok-release` prints can now be customized: set `customization.ok_release_template` in the Lunar configuration to the path of a Go `text/template` file in the configuration repository, and Lunar renders it in place of the built-in summary line and check lists, so release pipeline logs can carry your own context such as runbook links or escalation instructions. The template receives the component name, the commit SHA, the verdict, the failing checks (name, policy, status, enforcement level, and failure messages) and the bypassed checks (actor, reason, and expiry or commit-bound). It is validated when the Lunar configuration is pulled — against a passing verdict, a passing verdict with bypassed checks, and a blocked verdict, each of which must render non-empty output of at most 64 KiB — and a stored template that fails at gate time falls back to the built-in output instead of blocking the release check. The template controls presentation only: the verdict, the exit code, the polling progress messages, and `lunar policy ok-pr` are unaffected. Printing the custom output also requires a CLI new enough to support it; an older CLI keeps the built-in output.
* A policy entry in `lunar-config.yml` can now carry arbitrary key-value annotations under `meta` — for example `enforce_control: CONTROL123` — and Lunar stores them and exposes them as a `meta` JSONB column on the SQL API `policies` view, so checks can be selected by an identifier of your own with a query such as `SELECT cl.* FROM checks_latest cl JOIN policies p ON cl.policy_id = p.id WHERE p.meta->>'enforce_control' = 'CONTROL123'`. Keys and values are free-form and are never shown to a person: they do not appear in pull-request comments or in the dashboards, a policy with no `meta` reads back as `{}`, and the annotations written on an entry apply to every sub-policy that entry imports, so mapping sub-policies to different values means importing the plugin once per sub-policy.
* The collector, policy, and cataloger dashboards now show a Definition column whose "view" link opens the exact place the script is declared — the `lunar-config.yml`, the `lunar-config.d/` fragment, or the imported plugin's own file — pinned to the commit that version of the Lunar configuration was read from and anchored at the declaring line. Links are built for github.com, GitHub Enterprise Server, and GitLab, including nested GitLab namespaces; a script whose definition has no browsable remote, such as one loaded from a local path, shows no link. Scripts stored before this release have no link until their Lunar configuration is read again.

### Improvements

* A component's check score now renders as a coloured gauge on the components listing, on the component details History tab, and on the Release Ledger, instead of coloured text, and a component with no checks shows a dash rather than a percentage. The History tab's Checks column is renamed Score, since the tab header already reports a different Checks figure, and the Release Ledger no longer repeats the Component column on a page already scoped to one component.
* Check and run status on the dashboards — component details, pull-request details, policy details, the Release Ledger, and the collector, run, and script-run listings — is now drawn with a status icon set instead of emoji, and each icon carries hover text saying what it means, such as "Failed - blocks the release" or "Failed, but reported only - does not block". A stale result, one not yet re-evaluated for the current commit, is now marked by a grey variant of its status icon, with a footnote beneath the table explaining it, replacing the `*` that used to be appended to the status.
* A gate bypass granted or withdrawn — through a `/lunar bypass` or `/lunar bypass rm` comment on a pull or merge request, or with `lunar policy bypass-release` and `lunar policy bypass-rm` — now refreshes the checks report in seconds. Lunar re-posts the results already stored for the commit instead of re-running every policy to recompute an answer a bypass cannot change, so the report, the GitLab project badge, and the component's score catch up with the ledger without waiting for a full evaluation. A commit that has never been evaluated still falls back to a full evaluation, and when an evaluation is already running for the commit, that evaluation's own post carries the bypass rather than a second one being started behind it. See [Break-glass gate bypasses](#product-break-glass-gate-bypasses).
* The Description column on the collector and policy details dashboards can now be inspected, so a description longer than the column width can be read in full instead of being cut off.
* The background materialization that keeps the SQL API tables up to date now holds a batch that has not filled for at most 5 seconds before running it, instead of up to 30 seconds, so check and component data materialized after a collection finishes becomes readable within a few seconds on a small or quiet installation. Batches that reach their size threshold still release immediately, so nothing changes for an installation whose updates arrive fast enough to fill them; the wait can be tuned with the `HUB_MAT_DRAIN_MAX_BATCH_WAIT` setting on the Hub when fewer, larger refreshes matter more than freshness.
* The Policies listing dashboard loads around 2.4× faster on large installations — 17.9 s to 7.4 s measured on an installation with roughly 30,000 components — because the query behind it no longer loses parallel execution while matching checks to policies.
* Policies at `score` enforcement now evaluate on pull requests, where they were previously dropped from dispatch regardless of their `runs_on` setting; `runs_on` — which defaults to `[prs, default-branch]` — is now the only thing deciding where a score policy runs. Their results appear on the pull-request and component dashboards and in the SQL API, and stay off the pull request itself: score checks are still excluded from the Lunar check run, the pull-request comment, and the merge-request note, cannot affect the merge gate, and do not contribute to the component's score from a pull-request run. This makes `score` the level at which to trial a guardrail — including one that only means anything in a pull request, such as a check on the pull request's title — before application teams see it. Score policies that declare `runs_on: [prs]` or the default now execute on every pull request, so policy run volume on pull requests increases accordingly.

### Bug fixes

* A break-glass command copied with its Markdown formatting is now understood: `/lunar bypass: <reason>` and `/lunar bypass rm` are recognized when the comment wraps them in backticks, as a code span or as a fenced code block. Previously such a comment did not start with a slash, so it was treated as ordinary conversation and the command was silently dropped — even though Lunar's own acknowledgement quotes the command that way. See [Break-glass gate bypasses](#product-break-glass-gate-bypasses).
* A customized bypass acknowledgement — `customization.bypass_template` or `customization.bypass_revocation_template` in the Lunar configuration — whose rendered reply would itself be read as a break-glass command is now replaced by the built-in default reply. Previously such a reply, for example one leading with a reason that spells out a bypass command, could be parsed as a new command when it arrived back as a comment, recording a bypass nobody asked for on the ledger that is the audit record for the control. See [Break-glass gate bypasses](#product-break-glass-gate-bypasses).
* A component that declares a `branch:` other than its repository's default branch is now collected and cataloged at the current commit on that branch. Scheduled `cron` collector runs, per-component catalogers with `clone-code: true`, and the re-evaluation triggered by granting or revoking a release-gate bypass previously took whichever branch of the repository was pushed to most recently, which is the right commit only when every component in the repository tracks the same branch. On a repository whose components track different branches, those runs used the wrong commit while still recording the result under the component's declared branch, so a cataloger checked out the wrong tree and a scheduled collector attributed another branch's data to the component.
* Rerunning code collectors now reaches components that track a branch other than the repository default. A rerun that covers every component built its clone reference from the repository's default branch, and any component declaring a different branch was then filtered out of the resulting collection, so the rerun silently collected nothing for it; it now issues one run per branch the repository's components track, each at that branch's current commit. Naming such a component explicitly with `lunar collector run` previously resolved its code collectors against the default branch while its cron collectors ran on the branch the component tracks, so a single command collected the component's data at two different commits; both now resolve the same commit. A component whose declared branch is a glob pattern has no single ref to run against: `lunar collector run` now fails with an error naming the component and the branch, before dispatching any collector, instead of collecting the default branch.
* Data collected against a commit that already has collected data now reaches Component JSON as served by the SQL API `components` and `components_latest` views and the dashboards built on them. Previously any write that appended to an existing commit's data — re-running `lunar collector run` against that commit, an out-of-band `lunar collect --sha` submitting release metadata against a commit built earlier, or a scheduled collector writing to an older commit — refreshed the checks derived from that data within minutes but left Component JSON serving the pre-append value indefinitely, because these views discovered data by when it was first collected for the commit rather than when it was last written. A commit already left stale becomes correct again the next time data is collected for it.
* A configuration pull that reruns global catalogers — `lunar hub pull --rerun-catalogers`, or `rerun-catalogers: "true"` in the config-sync job — no longer hangs when the Hub runs more than one replica. The pull now polls for each cataloger run's completion instead of listening only for runs finished by the replica serving the request, so a run picked up by a sibling replica is noticed and the command returns as soon as the catalogers are done. Previously such a pull kept its stream open until a ten-hour timeout, blocking the CI runner that issued it and every later pull queued behind it.
* The domain scorecards on the Home, Domains, and Domain details dashboards now center the medal in their Badge column, aligning it with the Score and Checks columns beside it, and give the Checks column enough room for large counts, so a ratio such as 4K/836.4K is shown in full instead of truncated.
* A GitLab API call that connects but never answers is now cut off after 60 seconds instead of hanging until the webhook delivery's own two-minute budget runs out. Previously one unresponsive call consumed the entire budget for a delivery and every remaining step was skipped — a `/lunar bypass:` command, for example, could record the bypass but never post its confirmation reply or trigger the policy re-evaluation, leaving the commenter unanswered until the delivery was retried.
* Collectors with a `cron` hook that declare `runs_on: [prs]` now run against open merge requests on GitLab-hosted components. The query listing a repository's open pull requests matched only GitHub's spelling of the open state, so it returned nothing for GitLab projects and scheduled collectors never ran in merge-request context there. Volume stays bounded by the existing limit of 50 open pull requests per component per scheduled run, but a GitLab installation should expect scheduled collection to step up once this work starts happening.
* A component that pins a branch other than its repository's default — `branch:` in the Lunar configuration — now resolves its current state from the head of that branch. Lunar tracks the current commit per component rather than one commit per repository, so the SQL API `checks_latest` and `components_latest` views, the dashboards built on them, and a component JSON lookup with no commit given (`lunar component get-json` without `--git-sha`) all report the pinned branch's newest commit; previously they resolved against the repository's default branch, so a component pinned elsewhere showed the state of a branch it does not track — or nothing at all once its default branch stopped advancing. A repository whose components track different branches now has a distinct current commit for each of them, and the pointer is kept current by pushes and repaired by the periodic repository sync if a webhook delivery is missed. Components that track their repository's default branch are unaffected.
* Collector and policy runs belonging to a merged GitHub pull request stay visible on the runs dashboards instead of disappearing the moment the pull request merges, and a merged pull request stays selectable in the dashboards' pull-request filters and listed on the component details dashboard. GitHub reports a merge through a field separate from a pull request's state, so Lunar stored a merged pull request as closed, indistinguishable from an abandoned one, and the dashboards — which hide runs from closed pull requests — dropped the guardrail history of exactly the changes that shipped. Merged and closed-without-merging are now recorded distinctly for both GitHub and GitLab. Pull requests merged on GitHub before this release stay hidden, because the data already stored cannot separate them from abandoned ones; GitLab merge requests were never affected.
* An out-of-band `lunar collect` now merges into a component's data at that commit in every case, instead of occasionally replacing it. When a Lunar configuration change had given the component a new identity and no collector had yet run under it, the out-of-band write was read as a fresh collection: the data collected under the previous version stopped being carried forward, leaving the component with only the newly written key and every check reporting no data. Data collected by CI, code, scheduled, and unknown-source collectors still ends the carry-forward as before. See [Out-of-band collection](#product-out-of-band-collection).
* The Hub's periodic repository sync now covers every branch the repository's components track — the `branch:` each component declares in the Lunar configuration — alongside the repository's default branch on the forge. Previously the sync walked the default branch only, so a component pinned to another branch never had that branch's commits ingested and open pull requests targeting it were dropped, leaving gaps whenever a webhook delivery was missed or a repository was newly tracked. A branch pattern such as `release-*` contributes no ref, a repository's sync covers at most eight branches with the rest named in a warning, and a tracked branch that no longer exists on the forge is skipped with a warning instead of failing the whole sync pass.
* A GitHub Actions workflow run that GitHub reports against a pull-request ref such as `refs/pull/41/head`, without associating it with the pull request itself, is no longer discarded. Lunar now recognizes the ref, matches components against the repository's default branch, records the run, links it to the pull request it came from, and starts the pull request's policy evaluation when CI completes. Previously such a run matched no component under any configuration, so nothing about it was recorded and policy evaluation was never triggered by its completion. A pull request whose base is not the default branch is unaffected: GitHub associates those runs with the pull request directly, and that path already knows the real base.

## 2026-08-19 <a href="#product-2026-08-19" id="product-2026-08-19"></a>

### Breaking changes

* A Lunar configuration declaring two components whose names differ only by the letter case of their repository identity now fails the configuration sync with an error naming both components. Because repository identity is compared case-insensitively, such a pair is one repository addressed two ways, with no defensible answer for which spelling to display — use one spelling. Components that differ only in the case of their monorepo subdirectory path are still distinct and remain allowed. See [Case-insensitive repository identity matching](#product-2026-08-19).

### Bug fixes

* A component's checks can no longer be left pending indefinitely when the component declares `after-json` or `missing-json` collectors but none of their hooks match the commit's Component JSON — for example an `after-json` hook on `.vcs.pr.title`, which is present on a merge request but absent on every branch push. Previously the evaluation waited forever for a collector wave that would never fire, so every check on the component stayed pending and `lunar policy ok-release` hung waiting on those policies; the evaluation now finalizes as soon as it is known that no hook will fire. See [Collector data dependencies: the `after-json` hook](#product-after-json-collector-hook).
* Pushes and merge-request events now reach a component whose declared repository identity differs in letter case from the identity the forge reports: the host, owner, and repository name — including GitLab namespaces and projects — are compared case-insensitively, matching how GitHub and GitLab themselves resolve them. Previously a GitLab component declared with a group's display-name casing (for example `GitOps/project`, where the group's URL path is `gitops`) silently received no webhook processing at all, so its collections and checks never ran. The spelling the author typed is still what Lunar stores and displays, and a monorepo component's subdirectory path remains case-sensitive, since it is a file path.

## 2026-08-18 <a href="#product-2026-08-18" id="product-2026-08-18"></a>

### Features

* The reply Lunar posts after a `/lunar bypass rm` revocation can be customized independently of the grant reply: set `customization.bypass_revocation_template` in the Lunar configuration to the path of a Go `text/template` file in the configuration repository. It accepts the same parameters and passes the same validation as `customization.bypass_template` — a missing file, invalid template, or unsupported parameter fails the configuration pull before anything is published — and the built-in revocation reply is used when the setting is unset. See [Break-glass gate bypasses](#product-break-glass-gate-bypasses).
* A blocked GitHub pull request can now be unblocked from its own comment thread, at parity with GitLab: an engineer whose repository role is `maintain` or `admin` comments `/lunar bypass: <reason>`, and Lunar records the bypass, marks the Lunar check on the pull request as passed for its current head commit, and confirms with a reply. The bypass is bound to that commit — a new push re-blocks — and is recorded on the same ledger as CLI-created bypasses, so it shows up in `lunar policy bypass-ls`, is queryable through the SQL API `bypasses` and `bypassed_checks` views, and leaves an audit trail of the checks it let through when the pull request merges. Commenting `/lunar bypass rm` withdraws the comment-driven bypasses on the pull request (CLI-created bypasses are left to `lunar policy bypass-rm`), immediately re-arms the check, and queues a re-evaluation so the settled verdict arrives without a new push. Replies honor the `customization.bypass_template` and `customization.bypass_revocation_template` settings. See [Break-glass gate bypasses](#product-break-glass-gate-bypasses).
* A GitLab merge-gate break-glass bypass can now be withdrawn from the merge request it was granted on: a user with the Maintainer role or above comments `/lunar bypass rm`, optionally adding a reason as `/lunar bypass rm: <reason>`. Lunar revokes the bypasses created through comments on that merge request, sets the Lunar status check back to pending, queues a policy re-evaluation so the settled verdict arrives on its own, and replies in the command's thread confirming the revocation. Bypasses created with `lunar policy bypass-pr` are left intact, and revoked bypasses remain on the ledger for auditing — marked with who revoked them and when — while dropping out of `lunar policy bypass-ls --active`. The reply posted after a grant now also mentions the new command. See [Break-glass gate bypasses](#product-break-glass-gate-bypasses).
* The Lunar results comment on a pull or merge request can now stay quiet until the first failure: set `customization.pr_comments.mode: only-failures` in the Lunar configuration. While every displayed check passes and Lunar has not yet commented on the request, no comment is posted; the first failing, erroring, or still-pending result posts the comment, and from then on it keeps live-updating as usual, including back to all-green once the failure is fixed. The trade-off is that an all-green request has no live view of collectors that have not reported yet. The default, `mode: always`, keeps the existing behavior, and commit statuses and the merge gate are unaffected either way.

### Improvements

* The Blocking checks column on the component dashboard's pull-requests list now counts failures against only the checks that can actually block the merge — those at `block-pr` or `block-pr-and-release` enforcement — instead of every check, so a merge request one failure from green reads `2/15` rather than `2/74`, and the pull-request details page shows the same fraction where it previously showed a bare count. The denominator describes the configuration, so it does not shrink when a bypass temporarily clears one of the checks.
* The checks report posted with check results — the GitHub Check Run text, the GitHub pull-request comment, and the GitLab merge-request note — now points a blocked author at the break-glass: when the report lists failing required checks, that section includes a hint to comment `/lunar bypass: <reason>` to override the merge gate. Advisory, pending, passing, and errored sections are unchanged, and a custom `report_template` is unaffected. See [Break-glass gate bypasses](#product-break-glass-gate-bypasses).
* The SQL API `checks` and `checks_latest` views — and the background process that keeps their materialized data fresh — are now around 3× faster for components that have accumulated many Lunar configuration versions (measured 4.3 s to 1.4 s for one such component on a large installation). Query results are unchanged.
* Refreshing the SQL API `checks` data is significantly faster on large installations: each incremental refresh previously scanned the entire checks projection to remove the rows it was about to rewrite — about 4.4 seconds and several gigabytes of I/O to delete roughly a thousand rows — and now removes only the affected rows, making the cleanup step around 15× faster on an installation with about 13 million check rows. New check results reach the `checks` and `checks_latest` views — and the dashboards built on them — sooner, especially during bursts of activity.
* SQL API tables now materialize concurrently: incremental refreshes that touch different repositories run in parallel instead of queuing behind one another cluster-wide, roughly tripling drain throughput, so the materialized data behind the SQL API views and the dashboards reflects recent activity sooner during bursts of commits and collections on large installations. Refreshes that touch the same repository still run one at a time, so results are unchanged — only fresher.
* The component and pull-request dashboards now surface active gate bypasses instead of showing a waived check as work still to do. A failing blocking check that an active bypass clears shows 🔓 rather than ❌, the header banner counts it separately — "42 checks are passing, 31 non-blocking checks are failing, 1 bypassed" — and the Blocking checks count and Ready-to-merge tick exclude waived checks, so the summary no longer contradicts the banner. A new Bypasses tab on each page lists the overrides standing against that gate — scope, policy, actor, verified role, source, reason, and remaining time — and the component page links into the Bypass Audit trail for the component's full bypass history across both gates. On a pull request, a component-wide bypass pinned to a different commit is not listed, since it masks nothing there. See [Break-glass gate bypasses](#product-break-glass-gate-bypasses).
* While the merge-gate banner on the pull-request dashboard or the release-gate banner on the component dashboard blocks, a hint line now appears beneath it telling the reader how to get past the block. By default the hint quotes the exact bypass invocation for that component and change request — the `/lunar bypass: <reason>` comment or `lunar policy bypass-pr` for the merge gate, `lunar policy bypass-release` for the release gate. Set `customization.bypass_hint.pr` and `customization.bypass_hint.release` in `lunar-config.yml` — one paragraph of inline Markdown each — to point readers at your own process instead, such as a Slack channel or runbook; an empty, multi-paragraph, or over-4-KiB hint fails the configuration sync with an error naming the field. See [Break-glass gate bypasses](#product-break-glass-gate-bypasses).
* The `/lunar bypass: <reason>` comment break-glass now works on GitLab tiers below Ultimate, where the Lunar commit status — what the "Pipelines must succeed" setting reads — is the merge gate rather than an Ultimate-only external status check. A granted bypass is recorded on the ledger whatever the tier, sets the commit status to success for the merge request's head commit immediately, and holds it green for that commit even while an evaluation is still pending or after a later re-evaluation; `/lunar bypass rm` returns the status to running. Previously such a command replied that there was nothing to bypass and recorded nothing. See [Break-glass gate bypasses](#product-break-glass-gate-bypasses).
* The component details dashboard's Deployment history tab is renamed History; its content is unchanged, but a saved bookmark of the old tab URL will land on the default tab.
* The background refresh that keeps the runs dashboard current now finds recently active components through an index-backed lookup instead of scanning the entire script-run history. On large installations that scan averaged over a minute per refresh cycle — most of the refresh's time budget spent before any listing rows were updated — which could leave the runs dashboard lagging well behind recent activity.
* Scoped materialization refreshes behind the SQL API `checks` and `components` views no longer rescan the entire commit table on every refresh, so their cost no longer grows with the installation's total commit history. On a production-scale installation under load, a scoped checks refresh is about 2.4× faster (1.05 s to 430 ms), so materialized check and component data keeps up better during bursts of commits.
* The background materialization that keeps the SQL API tables up to date now processes pending updates in larger batches: the cost of a refresh is dominated by a fixed scan, so a batch of 100 keys costs little more than one of 25, and a nearly empty batch is held for up to 30 seconds to let it fill before running. Materialized data keeps up better during sustained bursts of activity, at the cost of at most about 30 seconds of added latency on an otherwise quiet installation.
* The checks report posted with check results — the GitHub Check Run text, the GitHub pull-request comment, and the GitLab merge-request note — now names each check by the policy it came from, so two policies that declare the same check name render as two distinguishable lines (`container-scan.max-severity` and `iac-scan.max-severity`) instead of two identical rows. When the policy name already ends with the check name it is not repeated, and every rendered name is a selector `lunar policy bypass-pr --policy` accepts. Only the report body changes; merge gating, check conclusions, and recorded bypasses are unaffected.
* The background refresh that keeps the runs listing dashboard current no longer reads a component's entire run history to recompute its rows: it now reads only the rows on each repository's current head commits, so the work is proportional to what the listing shows rather than to accumulated history. On a production-scale installation this made a cold refresh of 100 components around 11× faster (16.7 s to 1.5 s), and refreshing the components with the largest histories dropped from 117 s to 41 s, so the listing stays current under load instead of falling behind. The listing's contents are unchanged.

### Bug fixes

* A `/lunar bypass` or `/lunar bypass rm` comment command that fails now posts a reply in the pull-request or merge-request thread saying the command did not happen and why — a GitHub rate limit, for example, names when its budget resets — instead of failing silently, and the webhook delivery is reported as failed so redelivering it retries the command safely. A custom reply template set with `customization.bypass_template` that predates the new `.Error` parameter is answered from the built-in template for that one reply. The exception is a command that fails after the bypass has already been recorded: the thread is left unanswered rather than told nothing happened, and redelivering the webhook completes the command without creating a duplicate. See [Break-glass gate bypasses](#product-break-glass-gate-bypasses).
* Granting a gate bypass through a pull-request or merge-request comment now triggers a policy re-evaluation for the affected components, so the Lunar results comment is refreshed to reflect the override. Previously only the check or status turned green while the comment went on listing the bypassed checks as failing until the next push. Applies on GitHub and GitLab alike. See [Break-glass gate bypasses](#product-break-glass-gate-bypasses).
* Component JSON no longer nondeterministically omits data that was collected and stored. After a Lunar configuration update, a component could accumulate several stored data snapshots for the same commit that tied on the ordering used to pick which one to serve, and the tie was resolved by physical database row order — so the same collected data could be visible on one deployment and absent on another, and could flip on any unrelated row update. The freshest snapshot, which contains all of the collected data, is now always the one served, in both the SQL API `components` and `components_latest` views and the stored Component JSON that collectors and policies read.
* A collector's scheduled `cron` run that calls `lunar collect` more than once now lands every write in the component's Component JSON. Previously only one arbitrary record from each cron run survived into the merged data — the rest were silently dropped, and which one survived could change between runs — so a cron collector writing several paths (for example an on-call collector writing the schedule, escalation, and summary) appeared to have collected almost nothing, and policies reading the missing paths could report false failures. Code and CI collector runs were never affected. The dropped records were stored all along, so each affected component repairs itself on its next scheduled run with no action needed. Within a single run, writes now also merge in the order the collector emitted them, so a key written twice in one run deterministically keeps the later value.
* Repository syncing no longer fails repeatedly for a repository that has been deleted on GitHub — or that the Lunar GitHub App can no longer access — while a component still references it. Because GitHub reports both states identically, the Hub now skips installing the repository's webhook with a warning instead of erroring on every sync attempt, defers the repository's next sync by one reconcile window instead of retrying the same failing sync on every periodic sweep, and probes it again afterwards — leaving the component's configuration intact, so a repository that is restored, or whose App access is restored, resumes syncing automatically with no intervention.
* The Lunar check posted on a GitHub pull request now honors active bypasses: a pull request whose failing blocking checks are all covered — by `lunar policy bypass-pr` or by the `/lunar bypass:` comment — turns green instead of staying red, while a partial bypass continues to block, and the bypass survives later re-evaluations of the same commit instead of being overwritten by the next posted result. The Lunar comment on the pull request now lists the bypassed checks with who authorized the override, instead of showing them as failing under a passing check. See [Break-glass gate bypasses](#product-break-glass-gate-bypasses).
* A GitHub webhook event whose processing fails — a push, pull-request, workflow-run, or bypass-command delivery — is now answered with an error status, so GitHub records the delivery as failed and it can be redelivered from the App's delivery log. Previously the Hub acknowledged every delivery as successful even when handling failed, so dropped events were invisible and could never be redelivered; on installations that hit rate limits, the delivery log will now show failures that were always happening but previously reported as successes.
* A GitLab merge request can no longer be left blocked indefinitely when the components it matched disappear before its code collection runs — for example when a Lunar configuration change removes them while the collection job is queued. The mandatory Lunar status check on the merge request previously went unanswered in that window; it is now answered as passed, the same "Lunar has no opinion" verdict a merge request that matches no component receives. GitHub pull requests are unaffected — nothing is posted for them in this case.
* A GitLab merge request on a tracked component that no policy targets is no longer blocked indefinitely: the "Earthly Lunar" external status check now answers with passed when an evaluation dispatches no policies. Previously the check was never answered on such components, and because Lunar enables "Status checks must succeed" on gated projects, the merge request could not be merged and there was no recovery.
* Processing triggered by a GitLab webhook — push, merge-request, and comment events — now runs to completion even after GitLab closes the connection at its ten-second delivery limit, matching the existing behavior for GitHub webhooks. Because GitLab does not retry a timed-out delivery, the disconnect previously cancelled whatever the Hub had left to do; for example, a comment-driven merge-gate bypass could be recorded and take effect without its acknowledgement reply ever being posted on the merge request.
* A GitLab repository webhook left pointing at a previous deployment's URL is now repaired automatically: when the Hub's public URL changes, its periodic webhook verification deletes the stale hook and recreates it at the current URL, matching what it already did on GitHub. Previously the Hub adopted such a hook as-is, so GitLab kept delivering pushes to the dead endpoint and no code collection ran for the repository.
* The Global catalog section of the Catalogers dashboard — the Tree and JSON views, the version-picker strip, and the Diff tab — now reflects the current catalog. Since scoped catalog materialization became the default, these panels silently served a snapshot frozen at the cutover, so their domain and component counts fell ever further behind the Catalogers table above them, and the version strip offered no newer date to pick. With no version selected the Tree now shows the live catalog, and the strip and Diff read the captured catalog version history. The rendered document can differ slightly from before: it no longer carries unmodelled keys or domain metadata that the old raw merged snapshot included.
* The Manifest panel on the home dashboard now links to the `lunar-config.yml` the Lunar configuration was read from for configurations hosted on GitLab — gitlab.com or self-managed — and on GitHub Enterprise Server. Previously the link was always built on `https://github.com/`, so for any other host it pointed at a nonexistent GitHub path and returned 404.
* The Repository column on the Home dashboard's Manifest panel now sizes itself instead of being pinned at 350px, so a long Lunar configuration repository URI — for example one on a self-managed GitLab host — is shown in full instead of truncating mid-path; the fixed width now applies to the version column, which holds a fixed-length commit SHA.
* The pull-request details dashboard no longer times out loading its check scorecard on large installations: the database lookup that resolves each policy's latest run against a pull request is now indexed, and the scorecard queries on the pull-request, component details, and Release Ledger dashboards no longer re-run that lookup once per check row. Measured on a production-scale installation, the pull-request scorecard query dropped from about 55 seconds to 31 milliseconds.
* The runs listing dashboard's incremental refresh no longer times out on large installations. Clearing a single component's rows before recomputing them previously scanned the entire listing table, so under load the refresh could back up by hours and the dashboard showed stale run results; the rows are now located directly, keeping run changes visible within seconds as they arrive.
* The runs listing dashboard no longer falls behind for every component when a few components have very large run histories. Previously, a component whose runs-listing refresh could not complete within its time budget was retried every minute without ever finishing, consuming refresh capacity and delaying updates for all other components; such a component now backs off with increasing delays instead, so everyone else's runs keep appearing promptly.
* Dashboards now decide whether a component is on GitLab from its actual repository host — matched against the GitLab hosts the Hub is configured with, plus gitlab.com — instead of guessing from a `gitlab` name prefix that only ever matched gitlab.com. Components on a self-managed GitLab host previously rendered as GitHub everywhere: tabs read "Active PRs" instead of "Active MRs", change requests showed `#` instead of `!`, the repository link was labelled `github`, and the repository and merge-request links were built in GitHub's URL shape and returned 404. Those components now render with GitLab's noun, sigil, and link shapes, including nested subgroups and monorepo sub-components. Rendering for gitlab.com, GitHub, and GitHub Enterprise Server components is unchanged.
* The Hub now cleans up its leftover stale webhooks on GitHub repositories and GitLab projects even when a healthy webhook is already installed; previously a leftover Lunar webhook pointing at an outdated Hub URL could linger on the repository indefinitely, with the SCM delivering its events nowhere. Removing Lunar's webhook from a repository — for example when the last component referencing it leaves the catalog — now deletes every Lunar-owned webhook rather than only the first, and webhook reconciliation on GitHub now finds Lunar's webhook even on repositories with more than 30 webhooks configured.
* The checks report posted with check results — the GitHub Check Run text, the GitHub pull-request comment, and the GitLab merge-request note — now spells a check's policy-qualified name the same way in every section: a bypassed plugin check is named exactly as it is in the failing sections, instead of rendering with its policy name doubled (for example `container-scan.max-severity.max-severity` under Bypassed while Failing showed `container-scan.max-severity`). A check that failed before it could report a name — for example when its policy's container image cannot be pulled — is now listed under its policy's name instead of an indistinguishable `Execution failure` row per policy; only a check with no policy still reads `Execution failure`. Within each status section, checks are now ordered by the name the report displays rather than an internal bare check name, so same-named checks from different policies sort deterministically.
* A cataloger's results can no longer be silently lost when two runs of the same cataloger write to the catalog at the same time — for example when pushes to different commits trigger concurrent runs, or when a globally scoped cataloger runs on more than one Hub replica at once. Previously such writes could collide, and the losing run's entire result was discarded without an error, leaving the catalog on the older data until the cataloger happened to run again; concurrent writes for the same cataloger are now serialized so every result is recorded.
* The Lunar release badge on a GitLab project now honors release-gate bypasses: when every failing release-gating check is covered by a `lunar policy bypass-release`, the badge reads "release bypassed" in yellow — naming the override rather than showing a plain green pass — while a partial bypass keeps it at "release blocked". Granting or revoking a release-gate bypass also triggers a fresh evaluation of the component's default-branch head, so the badge updates without waiting for the next push; previously a bypass cleared `lunar policy ok-release` while the project badge kept reading "release blocked" indefinitely. The Lunar commit status on the default branch deliberately keeps the un-bypassed verdict, so the two read together as blocked, and overridden. See [Break-glass gate bypasses](#product-break-glass-gate-bypasses).
* A check's multi-line assertion failure messages in the checks report posted with check results — the GitHub Check Run text, the GitHub pull-request comment, and the GitLab merge-request note — now render as a nested list, one item per line, instead of being collapsed into a single run-on line. A policy that emits one finding per line, such as a CVE list, reads as a list again; blank lines are dropped and a line's own leading list marker is replaced by the report's nested bullet. The injection safeguard is unchanged: user-influenced text still never opens a line of the report, so it cannot invoke a GitLab quick action.
* The background refresh that keeps the runs listing dashboard current no longer loses throughput after an occasional long refresh pass. Previously, a pass whose overall time budget expired mid-refresh also halved how much work later passes would take on — a penalty recovered only gradually — so a few slow passes an hour could quietly ratchet the refresh down and leave recent run activity waiting to appear. The pass budget is now sized so a slow individual refresh is deferred and retried on its own instead of ending the whole pass.

## 2026-08-13 <a href="#product-2026-08-13" id="product-2026-08-13"></a>

### Features

#### Customizable checks report templates <a href="#product-checks-report-templates" id="product-checks-report-templates"></a>

The Markdown report Lunar posts with check results can now be customized. Set `customization.checks_template` in the Lunar configuration to the path of a Go `text/template` file in the configuration repository, and that one template controls all three report bodies: the GitHub Check Run text, the GitHub pull-request comment, and the GitLab merge-request note.

Lunar validates the template when it pulls the configuration — a missing file, an invalid template, or an unsupported field fails the pull before a new configuration version is published — and stores the validated template with that version, so every report renders with the template of the configuration it was produced under. A template controls presentation only: check names, GitHub conclusions, GitLab states, required-check calculation, bypass decisions, and gate behavior are unaffected. If a stored custom template unexpectedly fails at render time, Lunar logs the error and posts the built-in report instead.

The template receives the checks grouped into status sections, any bypassed checks, and report context such as the component, commit, pull-request number, and dashboard URL. The full template contract — and the exact built-in template to start from — is documented in the Lunar configuration reference under `customization.checks_template`.

### Improvements

* The built-in checks report posted with check results — the GitHub Check Run text, the GitHub pull-request comment, and the GitLab merge-request note — is reorganized: checks are grouped into collapsible status sections, failing required checks are listed separately from failing non-required ones, a non-required failure is marked ⚠️ rather than ❌, check names render as inline code, and a single More Details link closes the report. See [Customizable checks report templates](#product-checks-report-templates).
* The component details header now shows a release-gate banner, mirroring the pull-request header's merge banner: when any check at `block-release` or `block-pr-and-release` enforcement is not passing on the component's default branch, it reads "N checks are required to release" in red; otherwise it summarizes in green how many checks pass and how many non-blocking checks fail.
* The component details dashboard's Release history tab is renamed Deployment history; its content is unchanged.
* The check scorecards on the component details, pull-request, and Release Ledger dashboards now share one status-icon set: a failing check that is not blocking shows ⚠️ instead of ❌ so it reads as a warning rather than a hard block, a pending check shows ⏱️, and an errored check shows ❗. The redundant "required" column is removed in favor of the enforcement-level column, which now renders color-coded by severity. The Release Ledger scorecard links each check name to its policy details and renames its Why column to Failure reason with the full text openable in the cell inspector, and the pull-request scorecard gains a Run ID column linking to the run's details, matching the component details dashboard.

### Bug fixes

* The SQL API `checks` and `checks_latest` views — and the dashboards built on them — no longer return a leftover blank row beside a component's real checks, which made a component with five checks report six rows and skewed every count over the SQL API. The derivation that produced these placeholder rows was fixed in the previous release; upgrading to this one also deletes the rows already written. A commit that was seen but had nothing checked keeps its single placeholder row, so such commits remain visible.
* A `runs_on` value set where a plugin is imported via `uses:` now applies to imported collectors and policies that declare no `runs_on` of their own. Previously the import-site value was silently dropped and the imported scripts kept the global default `[prs, default-branch]`, so a collector restricted to `runs_on: [prs]` still ran on the default branch. An import that leaves `runs_on` unset is unchanged: the imported script keeps its own default, so a cron-only collector still runs only on the default branch.

## 2026-08-12 <a href="#product-2026-08-12" id="product-2026-08-12"></a>

### Features

* The SQL API gains a `catalog` view: the catalog over time, as a series of full snapshots, complementing the existing `catalog_latest` view. Each row is a complete catalog JSON document with a `timestamp` column saying when the catalog last changed and a `captured_at` column saying when the version was recorded; the newest row is always the current catalog, matching `catalog_latest`. Order the history by `captured_at` — `timestamp` can repeat across rows. The history is sampled rather than exhaustive: versions are recorded periodically, so several changes landing close together may appear as one row. History begins once the Hub starts recording versions; for anything earlier, `lunar cataloger get-json --ts` reconstructs any past catalog from the cataloger delta history. Recorded versions are retained for 90 days by default, and the newest version is always kept.

### Improvements

* The Initiatives dashboard loads around 3× faster on large installations — 5.5 s to 1.7 s measured on an installation with roughly 30,000 components — because its backing database functions no longer force the queries that call them onto a single core.

## 2026-08-11 <a href="#product-2026-08-11" id="product-2026-08-11"></a>

### Breaking changes

* A collector's `after-json` hook now fires only when its declared Component JSON `path` ended up present once the component's collection settled; previously it fired whether or not the path was populated. A collector that relied on `after-json` to run as a fallback when the path is absent must declare the new `missing-json` hook instead, and a collector that needs both cases declares both hooks on the same path. See [Collector data dependencies: the `after-json` hook](#product-after-json-collector-hook).
* A GitLab merge-gate break-glass bypass must now be invoked as a slash command: comment `/lunar bypass: <reason>` on the blocked merge request. The previous bare `lunar bypass: <reason>` phrase no longer triggers a bypass, so ordinary discussion cannot accidentally invoke the break-glass path. Acknowledgements and rejections are now posted as replies to the triggering comment instead of as separate notes. See [Break-glass gate bypasses](#product-break-glass-gate-bypasses).

### Features

* The reply Lunar posts after a comment-driven gate bypass can now be customized: set `customization.bypass_template` in the Lunar configuration to the path of a Go `text/template` file in the configuration repository. Lunar validates the template when it pulls the configuration — a missing file, invalid template, or unsupported parameter fails the pull before anything is published — and uses its built-in acknowledgement when the setting is unset. See [Break-glass gate bypasses](#product-break-glass-gate-bypasses).
* The SQL API gains a `catalog_latest` view holding the current catalog — the full inventory of domains and components — as a single JSON document. It returns at most one row, and the document matches what `lunar cataloger get-json` returns, so a `lunar sql` query and the CLI always agree.
* Collectors can now declare a `missing-json` hook, the complement of `after-json`: it fires once a component's collection has settled for a commit, only when the declared Component JSON `path` is absent, making fallback collection explicit — for example generating an SBOM only when no other collector produced one. A path whose value is `null` counts as present, matching the Policy SDK's distinction between null and missing. See [Collector data dependencies: the `after-json` hook](#product-after-json-collector-hook).

### Improvements

* The documentation now has a Guides section, opening with a Cataloging Strategy guide: a three-phase, eight-step approach to cataloging for teams beginning a Lunar rollout.
* A new guide documents how to reach a Lunar Dedicated hub over inbound AWS PrivateLink. The existing outbound PrivateLink material is split into its own page, and the setup walkthrough now covers both directions.
* The SQL API `bypassed_checks` view now records a masking only when the bypass's verdict is actually consumed — when the pull request merges for the PR gate, or when `lunar policy ok-release` passes for the release gate — rather than at every gate re-evaluation, so bypass audit data reflects what actually shipped instead of how often the gate re-ran. See [Break-glass gate bypasses](#product-break-glass-gate-bypasses).
* The Collectors listing dashboard now serves its run counts from pre-computed data instead of recomputing them from raw run rows on every page load, making the page around 10× faster on large installations (13.6 s to 1.28 s measured on a production-scale tenant).
* Lunar Hub now uses conditional requests (ETag caching) for its GitHub API calls by default, reducing GitHub API rate-limit consumption. Every request is still revalidated with GitHub — a cached response is used only when GitHub confirms it is unchanged — so results are never stale.
* Catalog materialization is now scoped to what changed, by default on all installs: a catalog change updates only the affected components instead of rebuilding the whole catalog on every change.

### Bug fixes

* When a commit belongs to more than one pull request, the SQL API `checks` and `checks_latest` views — and the dashboards built on them — now show the commit's checks under each of its pull requests, deterministically. Previously which pull request a check row carried was decided by the database's query plan, so a plan change, a statistics update, or a Postgres upgrade could silently reassign the pull request shown on up to half of a component's checks, and the commit's other pull requests could show no checks at all.
* The periodic cleanup that resolves policy and collector runs stranded in a non-terminal state now sweeps in bounded batches, so it always makes progress and completes within its budget. On large installations the sweep previously timed out on every tick, leaving stuck runs unresolved indefinitely.

## 2026-08-07 <a href="#product-2026-08-07" id="product-2026-08-07"></a>

### Breaking changes

* The SQL API `bypassed_checks` view is now an append-only event log: every check masked at a gate evaluation is recorded as a separate immutable row with a `masked_at` timestamp, so the full masking history — how many times, and exactly when — is preserved for audit. The `first_masked_at`, `last_masked_at`, and `mask_count` columns are removed; queries that read them should aggregate over the event rows instead, for example `COUNT(*)` and `MIN`/`MAX` of `masked_at` grouped by `bypass_id`, `sha`, and `check_name`. See [Break-glass gate bypasses](#product-break-glass-gate-bypasses).

### Improvements

* A GitLab merge-gate bypass created through a merge-request comment is now recorded on the same ledger as bypasses created from the CLI: it shows up in `lunar policy bypass-ls`, can be revoked early with `lunar policy bypass-rm`, is queryable through the SQL API `bypasses` and `bypassed_checks` views, and behaves identically to a CLI-created bypass. See [Break-glass gate bypasses](#product-break-glass-gate-bypasses).

### Bug fixes

* The Active PRs panel on the component details dashboard no longer lists pull requests that touch none of the component's paths; in a monorepo, unrelated pull requests previously appeared as empty `0/0` rows. See [Monorepo support: path-scoped components](#product-monorepo-path-scoped-components).
* The Lunar status check posted on a GitLab merge request now honors bypasses created from the CLI: a merge request whose blocking checks are fully bypassed with `lunar policy bypass-pr` turns green instead of staying red, while a partial bypass continues to block. See [Break-glass gate bypasses](#product-break-glass-gate-bypasses).
* Merge-request numbers on GitLab-hosted components now display with GitLab's `!` sigil instead of `#` on the pull-request details and component dashboards.
* The green release-gating project badge on GitLab now reads "release ready" instead of "released", since Lunar observes the release gate rather than the deployment itself; projects with no release policies configured no longer show "released" incorrectly.
* A pull request's head-commit pointer now advances correctly when two commits share the same one-second-resolution timestamp; previously such a tie could leave the pointer on the wrong commit, making the pull request's checks disappear from the component dashboard. Commit ordering during GitLab repository syncing now follows the same rule.

## 2026-08-04 <a href="#product-2026-08-04" id="product-2026-08-04"></a>

### Features

#### Break-glass gate bypasses <a href="#product-break-glass-gate-bypasses" id="product-break-glass-gate-bypasses"></a>

When a blocking policy fails but a change has to ship anyway, a bypass overrides a component's release gate or PR/MR merge gate for a bounded window, instead of the policy being disabled for everyone. Every bypass expires — omitting a duration uses the configured `bypass.max_duration`, never infinity — and a duration longer than that cap is rejected rather than silently shortened. A bypassed gate never goes silently green: it still names every check it suppressed, who authorized the override, and until when, and checks the bypass does not cover keep blocking. A bypass covers one component and one gate, optionally narrowed to a commit, a pull request, or a single policy or check; a component-wide bypass is rejected unless it is deliberately narrowed or given an explicit duration. Revoking a bypass early is an audited soft-revoke — the record is retained — and bypasses, along with the individual checks they suppressed, are queryable through the SQL API `bypasses` and `bypassed_checks` views. Bypasses are created, listed, and revoked with the `lunar policy bypass-release`, `bypass-pr`, `bypass-ls`, and `bypass-rm` commands.

## 2026-07-30 <a href="#product-2026-07-30" id="product-2026-07-30"></a>

### Features

* `lunar-config.yml` accepts a new `image_replace` list that rewrites image references after they have been resolved, so every image the configuration produces — including images set by an imported plugin's `default_image`, which a consumer configuration could not previously override — can be redirected to an approved registry or internal mirror. Rules are evaluated in order and the first match wins, so two rules sharing a prefix cannot compound into a doubled registry path; `from_pattern` is an unanchored Go regular expression (anchor a registry prefix with `^`), and `to` may reference capture groups as `$1`, `$2`, and so on. The `native` value is never rewritten, since it names an execution mode rather than an image, and Lunar logs a warning for any rule that matched no images during a pull.

## 2026-07-28 <a href="#product-2026-07-28" id="product-2026-07-28"></a>

### Features

#### Release Ledger dashboard <a href="#product-release-ledger-dashboard" id="product-release-ledger-dashboard"></a>

Dashboards now include a Release Ledger — a per-release view of a component. For a chosen release commit it shows the release's check score and details, the initiatives that apply, and compliance evidence drawn from the release data collected into the component's `.vcs` arrays (pull requests and tickets).

The Release Notes tab lists the pull requests and tickets that went into the release as collapsible cards: the title stays visible, the full description renders as Markdown on expand, and each pull request number and ticket id links to its source. A From selector turns the tab into a range view — pick an earlier release as the baseline (it defaults to the previous scored release) and the tab aggregates the pull requests and tickets of every release in between, de-duplicated.

Each release listed in a component's Release history links to its Release Ledger.

* The component details dashboard gains a paginated Release history tab listing the component's releases — default-branch commits with at least one scored check — with each release's check pass rate, build timestamp, and the pull requests and tickets it carried. A release with nothing scored shows no value rather than 0%, and each row links to that release's Release Ledger. See [Release Ledger dashboard](#product-release-ledger-dashboard).

### Improvements

* A collector that fails terminally no longer fails the entire collection cycle for that commit. Previously the whole dispatch was retried, re-running every collector and re-evaluating every policy several times; the failed collector is now isolated, other collectors' results stand, and policies evaluate once on the data that was collected.
* When a repository cannot be cloned or fetched because of missing permissions, the error now names the real cause instead of a generic clone failure. With no SCM credentials configured for the repository's owner, it explains that the operation ran anonymously and that a private repository needs credentials with access; with configured credentials, it explains that the repository either does not exist or the credentials have not been granted access to it — for a GitHub App, the installation's Repository access settings. The diagnosis applies to Lunar configuration syncs (including `lunar hub pull`), plugin fetches via `uses:`, and repository syncing, on GitHub and GitLab alike.

### Bug fixes

* A collector's `after-json` hook now fires even when the component has no collected data yet for a commit: its conditions are evaluated against an empty Component JSON, so fallback collectors run instead of the dispatch failing and retrying. Previously a component whose only collectors were `after-json` fallbacks never ran them — on pull request commits this looked like the collector simply not firing. See [Collector data dependencies: the `after-json` hook](#product-after-json-collector-hook).
* The stored merged Component JSON now folds collection records oldest to newest, matching the SQL API's `components` view: when the same key is written more than once for a commit, the newest value wins, and array elements accumulate in chronological order without duplication. Previously the merged data could keep the oldest value for a re-written key — for example an environment marker re-stamped on the same commit during a promotion — and could reverse or duplicate array entries. The stored data is recomputed on each component's next collection.
* `lunar policy ok-release` and `lunar policy ok-pr` no longer wait until their timeout when there is nothing to wait for. A release check now settles for a monorepo component when the commit touched no component in the repository at all, and both checks now answer success immediately — with a message that no policies gate the check — when no policy at a blocking enforcement level applies to the component. When a gating policy exists but has not run yet, the check still waits.
* On the runs dashboard, a run's component link now opens the component details of the specific monorepo sub-component that produced the run, instead of the repository-level component. See [Monorepo support: path-scoped components](#product-monorepo-path-scoped-components).

## 2026-07-21 <a href="#product-2026-07-21" id="product-2026-07-21"></a>

### Improvements

* A collector's `after-json` hook now honors `clone-code: true`: the collector runs with a checkout of the component's repository, pinned to the commit whose collection settled, so a source-scanning fallback — for example an SBOM generator, a filesystem vulnerability scan, an IaC scan, or a secrets scan — can run against the source tree when nothing else produced the data. Because the checkout is pinned to the settled commit rather than the branch head, the tree always matches the merged Component JSON the collector reads. An `after-json` collector without `clone-code` still runs without a checkout, unchanged. See [Collector data dependencies: the `after-json` hook](#product-after-json-collector-hook).
* A failing check's individual assertion messages in the Lunar comment and status posted on a pull request — on GitHub and GitLab alike — are now truncated at 1000 characters with a `… (truncated)` marker, so a single very long message (for example, one enumerating a long list of vulnerabilities) can no longer push the comment past GitHub's size limit and prevent it from being posted; the full message remains available through the dashboard link in the comment.
* The Hub now shares its cache of policy input bundles — the packaged component data a policy run fetches before executing — across all Hub replicas, so a bundle built once is reused everywhere instead of being rebuilt independently by each replica. Previously, on installations running multiple Hub replicas, a large burst of concurrent policy runs could overload the Hub with redundant bundle builds, causing bundle fetches to time out with `failed to bundle` errors and leaving runs stuck initializing.

### Bug fixes

* A component whose configuration includes an `after-json` collector can no longer have its checks left pending indefinitely when concurrent policy evaluations race to trigger the collector for the same commit. The duplicate trigger — always harmless, since the collector fires at most once per component and commit — was treated as an internal error that prevented policy evaluation from finalizing; it is now tolerated and checks settle as expected. See [Collector data dependencies: the `after-json` hook](#product-after-json-collector-hook).
* A collector fired by an `after-json` hook on a GitLab or GitHub Enterprise Server component now runs against the component's actual repository host. Previously the dispatched run defaulted to `github.com`, so its result was recorded against the wrong host and the run never settled — leaving the component's checks, and any collector waiting on that data, pending indefinitely. Components hosted on github.com were unaffected. See [Collector data dependencies: the `after-json` hook](#product-after-json-collector-hook).
* Runs the Hub starts on its own — scheduled `cron` collector runs, code-collection reruns, and cataloger runs — now preserve the repository's host and SCM provider for components hosted outside github.com. Previously the Hub built a GitHub-style repository reference for a GitLab-hosted component, mis-routing clone authentication, and its collector and cataloger runs omitted the repository host from their records, so runs and the policy evaluation that follows could be attributed to a `github.com` repository of the same name on GitLab and GitHub Enterprise Server installations.
* Script runs that are executing when the Operator shuts down — during an upgrade or rollout — are now requeued, without consuming a retry attempt, for the replacement Operator to pick up, instead of being left stuck in a running state for around five hours before being retried.
* The Lunar status posted on a pull request — the GitHub check and the GitLab commit status alike — no longer reports a failure when a required check is legitimately skipped, for example a policy with nothing to evaluate on that commit. A skipped check previously counted as a failure, so the pull request showed a red failing check even though the merge gate and the Lunar comment correctly treated it as passing. Skipped checks are now counted separately and surfaced in the status title, such as "14 passed, 1 skipped of 15 required checks"; a real failure alongside a skip still fails the status.

## 2026-07-20 <a href="#product-2026-07-20" id="product-2026-07-20"></a>

### Improvements

* Catalog refreshes no longer enqueue a repository sync job for every component on every run: a sync job is now enqueued only when the repository is actually due for its periodic reconcile, and per-repository sync jobs are deduplicated over an hour instead of ten minutes. On large installations this removes tens of thousands of redundant background jobs per hour, and under GitHub rate limiting sync retries can no longer pile up into a backlog that delays code collection and policy evaluation.

### Bug fixes

* Repository syncing no longer fails repeatedly for archived GitHub repositories. Because an archived repository is read-only and emits no push or pull-request events, the Hub now skips installing its webhook with a warning instead of erroring, and the repository's commit and pull-request history syncs as usual.

## 2026-07-17 <a href="#product-2026-07-17" id="product-2026-07-17"></a>

### Features

#### Collector data dependencies: the `after-json` hook <a href="#product-after-json-collector-hook" id="product-after-json-collector-hook"></a>

Collectors can now depend on data rather than on a specific named upstream collector. A collector that declares an `after-json` hook with a Component JSON `path` — for example `.sbom` or `.sca` — fires once the component's collection has settled for a commit, after every other collector and CI workflow has finished, whether or not the path ended up populated. If another tool produced data at the path, the collector can enrich it regardless of which tool that was; if nothing did, the collector runs as a fallback and can supply the data itself.

An `after-json` collector fires at most once per component and commit, and runs without a repository checkout: it reads the component's accumulated data with `lunar component get-json` and submits results with `lunar collect`. The component's checks stay pending until the fired collectors finish, and a collector that fires but writes nothing still releases the gate, so checks cannot be left pending indefinitely. The hook is collector-only — it cannot be declared on a cataloger — and `path` is required, must start with `.`, and is rejected on any other hook type at configuration sync.

### Bug fixes

* The component details and pull-request dashboards now render GitLab-hosted components correctly: the repository link is labelled `gitlab` instead of `github`, repository and merge-request links resolve for GitLab's nested namespaces (`…/project/-/tree/<branch>`, `…/project/-/merge_requests/<n>`) instead of being truncated to a fixed three-segment path, and the component dashboard reads "MR" instead of "PR" for GitLab components. GitHub rendering is unchanged.
* During a burst of pushes, a commit's processing — code collection, catalogers, and the policy evaluation that follows — is no longer lost when GitHub times out the webhook delivery. The Hub now runs the processing to completion even after GitHub closes the connection at its roughly ten-second delivery limit; previously the disconnect cancelled the work mid-flight and, because GitHub does not retry failed deliveries, those commits were left without collected data or check results.
* The Queued tab on the runs dashboard now loads instead of failing with `permission denied for schema queue`; the read-only database role that backs the Grafana datasource was missing read access to the table of queued runs, so every query behind the tab was rejected.

## 2026-07-16 <a href="#product-2026-07-16" id="product-2026-07-16"></a>

### Bug fixes

* A scheduled CI run — a GitHub Actions `on: schedule` workflow or a Buildkite scheduled build — that runs collectors now merges its results into the component's existing component JSON, the same way a collector's scheduled `cron` runs do. Previously such a run was pinned to whatever commit CI checked out, so a scheduled pipeline (for example a CodeQL workflow on a cron trigger) could become the component's latest view with only its own partial data, dropping the other collectors' results and causing policies to evaluate against incomplete data until the next full collection.

## 2026-07-13 <a href="#product-2026-07-13" id="product-2026-07-13"></a>

### Bug fixes

* A Lunar configuration hosted on GitLab now syncs correctly: the Hub clones a `gitlab://` configuration URI the same way it clones `github://` ones, instead of treating the URI as a local path — which made `lunar hub pull` of a GitLab-hosted configuration fail with a git error regardless of credentials.
* The Hub now registers a repository's webhook before starting that repository's initial history sync, so commits pushed while the first sync is still running trigger code collection. Previously, on a newly tracked repository, the webhook was installed only after the sync completed — which can take minutes on a large history — and pushes made in that window were silently missed.

## 2026-07-11 <a href="#product-2026-07-11" id="product-2026-07-11"></a>

### Breaking changes

* A Lunar configuration in which two imports of the same type resolve to the same name — for example the same policy plugin imported twice via `uses:` with different `on:` targets — now fails the configuration sync with an error naming the conflict; give each duplicate import a unique `name:` to disambiguate. A collector and a policy may still share a name. Duplicates that were synced before this change are renamed automatically with a short unique suffix, preserving their run history; add explicit `name:` values to restore meaningful names.

### Bug fixes

* Checks from policies at the `block-release` enforcement level no longer appear in the Lunar comment and status posted on a pull request — on GitHub and GitLab alike — since they gate releases rather than merges; previously they inflated the PR's check count so it did not match the dashboard. Results posted for release commits still include them.
* Dashboards no longer duplicate a component's checks when the component currently resolves to more than one latest commit — for example while a burst of commits is being processed, or when a component spans multiple repositories. Previously every check row was repeated once per commit, transiently inflating check counts and the component score.
* A policy imported more than once — for example once at `score` and once at `block-release` enforcement — now shows one check row per applying import, each with its own enforcement level, on the component's Checks tab and in the SQL API `checks` and `checks_latest` views. Previously such checks were collapsed to a single enforcement and could appear as identical duplicated rows, so the enforcement shown was unreliable. Because the component score counts these rows, scores can shift for components that import the same policy multiple times.
* Closed and merged pull requests no longer remain shown as Active on the component details dashboard or reported as open by the SQL API `prs` view. Previously, a pull request that closed more than a few hours after it was opened could keep a stale open status in materialized data indefinitely; the refresh window is now keyed on when a pull request last changed rather than when it was created, and stale rows are rebuilt automatically on the first hourly materialization pass after upgrading. Pull requests backfilled by a repository sync now also appear in the view immediately instead of being skipped.
* A component's `.meta` declared in the Lunar configuration (`components.<id>.meta` in `lunar-config.yml`) now reaches collectors and policies through the `LUNAR_COMPONENT_META` environment variable; previously configuration-declared metadata was silently dropped during the configuration sync, and only cataloger-emitted metadata was surfaced.
* The SQL API `checks` and `checks_latest` views no longer include orphaned rows whose `policy_id` refers to a policy name that no longer exists in any version of the Lunar configuration; these stale rows, left behind when duplicated policy names were renamed to stable ones, are removed automatically while all still-valid check history is preserved. Dashboards were not affected.
* Checks queried through the SQL API no longer show a duplicate row per Lunar configuration version for a policy that was imported multiple times under the same name. The `~` suffix that disambiguates such imports — added for configurations that predate the requirement for a unique `name:` per duplicate import — is now derived from the import's own definition (type, name, enforcement, initiative, and targeting) instead of changing with every configuration publish, so each import keeps one stable name and one checks row. Existing suffixed names change once as part of this repair.

## 2026-07-09 <a href="#product-2026-07-09" id="product-2026-07-09"></a>

### Features

* Collectors and policies now receive the component's catalog `.meta` — an arbitrary key-to-value map, for example `pagerduty/service-id` — as a JSON object in the `LUNAR_COMPONENT_META` environment variable, set only when the component has metadata. Plugins can use it for per-component configuration, such as mapping a component to the service identity it has in an external tool.

### Bug fixes

* `lunar policy ok-release` no longer times out waiting for policy evaluations when the build and the release step run as jobs in the same GitHub workflow file.

## 2026-07-07 <a href="#product-2026-07-07" id="product-2026-07-07"></a>

### Bug fixes

* The runs dashboard now attributes each run to the monorepo sub-component that produced it instead of collapsing all runs into a single repository row, and drilling in from a sub-component shows its runs instead of no data. See [Monorepo support: path-scoped components](#product-monorepo-path-scoped-components).
* A component that references an undeclared domain no longer causes the entire catalog to be dropped. A reference authored in the Lunar configuration now fails the sync with an error naming the component and domain before anything is published, and a cataloger-discovered reference drops only that component while the rest of the catalog persists.

## 2026-07-02 <a href="#product-2026-07-02" id="product-2026-07-02"></a>

### Features

* Collectors, catalogers, and policies can now declare a `size` (`small`, `medium`, `large`, or `xlarge`) in the Lunar configuration; the size selects the script-runner pod's resource profile and how densely runs are packed into pods. Scripts without a declared size keep the previous defaults, unchanged.
* `lunar-config.yml` can now be split for readability: a file may contain multiple YAML documents separated by `---`, and every `*.yml`/`*.yaml` file in a sibling `lunar-config.d/` directory is loaded and merged in filename order. A key defined twice across documents is an error rather than a silent overwrite, and a single-file configuration keeps working unchanged.

### Improvements

* A cataloger `component-repo` hook that sets `clone-code: true` now runs with a checkout of the component's repository at the triggering commit, so it can read repository files such as `catalog-info.yaml` or `CODEOWNERS` from a working tree instead of fetching them through an API.
* Code collection triggered by SCM webhooks now runs as a durable queued job, so a Hub restart mid-collection no longer loses the collection or the policy evaluation that follows it.

### Bug fixes

* Dashboard links to components, commits, and pull requests now resolve correctly for GitHub Enterprise Server hosts and for monorepo sub-components; previously GHES links were broken and sub-component links pointed at the wrong repository path.
* An empty push (a commit with no changed files) no longer triggers code collectors for every component in a monorepo; it now runs none. See [Monorepo support: path-scoped components](#product-monorepo-path-scoped-components).

## 2026-06-23 <a href="#product-2026-06-23" id="product-2026-06-23"></a>

### Features

#### Buildkite support <a href="#product-buildkite-support" id="product-buildkite-support"></a>

Lunar now supports Buildkite as a CI provider alongside GitHub Actions. The CI/CD Tracer traces Buildkite builds through a Buildkite agent `command` hook, and the Hub ingests Buildkite build webhooks so policies that depend on CI results wait for the build to finish instead of finalizing early. Builds are associated with their pull requests, a finishing build re-evaluates the affected policies, and in monorepos pull-request builds are scoped to the components whose files actually changed.

#### GitHub Enterprise Server and multi-organization support <a href="#product-github-enterprise-server-support" id="product-github-enterprise-server-support"></a>

A single Lunar Hub can now serve GitHub Enterprise Server (GHES) organizations alongside github.com and GitHub Enterprise Cloud, and front multiple organizations that each install their own Lunar GitHub App — one entry per owner, with a per-App host and base URL. The CI/CD Tracer resolves the GitHub host from the CI run environment, so GHES runs are attributed to the right component with no extra configuration, and pull-request checks post back to the GHES host.

#### Monorepo support: path-scoped components <a href="#product-monorepo-path-scoped-components" id="product-monorepo-path-scoped-components"></a>

Components can be scoped to subdirectories of a repository with a configurable `paths` list, so a CI run or commit is attributed only to the sub-components whose files actually changed. An entry ending in `*` is a prefix match (`services/api/*` matches anything under `services/api/`), an entry without one must equal the changed path exactly (such as `go.mod`), and a component named after a monorepo subdirectory gets an implicit `<subdir>/*` pattern automatically. Path-based attribution applies to pull requests and pushes on GitHub Actions, and to pull-request builds on Buildkite.

### Bug fixes

* Running and queued collector runs on the Collector details dashboard now show in-progress indicators instead of being displayed as failures.

## 2026-06-17 <a href="#product-2026-06-17" id="product-2026-06-17"></a>

### Features

#### Out-of-band collection <a href="#product-out-of-band-collection" id="product-out-of-band-collection"></a>

`lunar collect` can now be called from outside a traced CI run — for example from a CD pipeline — to attach additional data to a component's Component JSON at a specific commit, using `--component` and `--sha`. This makes it possible to record facts that only become known after CI, such as the release tag or image published by a deploy job. The Hub records the write as an external collection and re-evaluates the component's policies for that commit.

* A collector with a `cron` hook can now run against open pull request heads, not just the default branch — opt in with `runs_on: [prs, default-branch]`, or `[prs]` for PR heads only. Each scheduled run is recorded against that PR's head commit, so a scheduled dependency check can start failing a pull request after a new CVE is published even though its code has not changed.

### Improvements

* The runs listing dashboard now refreshes incrementally as results arrive — a run change appears within seconds instead of about half a minute — and components whose most recent run is older than ten days are no longer hidden from the listing.

### Bug fixes

* An interrupted configuration sync — or a single unreachable repository — can no longer leave the component catalog empty; an unresolvable component is skipped with a warning instead of discarding all of them.
* A Lunar configuration change now becomes visible only once it is fully processed, closing a window during which CI collections for the affected components were silently dropped.
* In a monorepo, components whose paths are untouched by a commit no longer show pending checks indefinitely while waiting for code collectors that will never run; their checks now settle.
* A policy that resolves to no container image now fails the Lunar configuration sync with an error naming the policy and how to fix it, instead of being silently skipped at execution time.
* Queued script runs are now shown with a waiting indicator on the Script runs dashboard instead of the same error mark as a failed run.
* The Created column on the Script runs dashboard's Queued tab now shows how long ago a run was created instead of a raw timestamp.
* Collector and cataloger runs abandoned by a failed dispatch are now cleaned up automatically instead of counting as queued forever in the dashboards.

## 2026-05-27 <a href="#product-2026-05-27" id="product-2026-05-27"></a>

### Improvements

* Policy evaluation is more resilient: policy runs and the posting of their GitHub check results now survive Hub restarts, a run can no longer be lost between being recorded and being scheduled, duplicate policy runs for the same collection are prevented, and interrupted runs are cleaned up automatically instead of appearing queued forever.
* The Lunar check on a pull request now states explicitly when no PR-blocking checks are configured, instead of reporting "0 out of 0 required checks passed", and mentions failing or pending non-blocking checks in its title and summary while the check itself remains successful.
* Public repositories — such as Earthly's public plugin library — can now be referenced from the Lunar configuration without a GitHub App installation on their organization; Lunar falls back to anonymous access when no App is configured for that owner. Private repositories still require an App installation.
* The Queued tab on the runs dashboard now shows the full backlog of script runs waiting to execute — including runs queued but not yet dispatched — with pagination and an accurate filter by script type.
* The runs dashboards and their filters now show only scripts that are part of the current Lunar configuration, so collectors, policies, and catalogers that have been removed no longer clutter listings, filter dropdowns, or error counts.
* Runs listings filtered to narrow time windows load much faster on large installations — around 300× faster for a time-only filter and 6.5× faster for a one-day window.
* The Hub now remembers which repository webhooks it has already installed and skips redundant GitHub calls, so webhook setup on catalogs with hundreds of repositories stays under GitHub's rate limit; a webhook installation that fails on a transient GitHub error is retried instead of leaving the repository without change events.

### Bug fixes

* The setup-errors warning banner on the component and pull-request dashboards now counts only errors from the last ten days, matching the runs listing it links to; clicking the banner no longer lands on an empty list.
* On the runs dashboard, the collector and policy links from a component page now land on populated rows, queued policy script names link to the policy's details, and the Queued tab's Created column shows how long ago a run was created instead of a raw timestamp.
* A cataloger run that finishes after its Lunar configuration version has been superseded can no longer cause pushes to be evaluated against the outdated configuration; policies added in a configuration update now dispatch for new commits immediately.

## 2026-05-17 <a href="#product-2026-05-17" id="product-2026-05-17"></a>

### Improvements

* Repositories with the same name under different GitHub owners are now tracked as distinct repositories, so one Lunar installation can monitor same-named repositories across multiple organizations without their data mixing.

### Bug fixes

* The catalogers dashboards no longer fail to render when a cataloger's output contains a non-object `domains` or `components` field; such entries now count as empty instead of erroring the whole panel.
* Script runs whose container image cannot be pulled, or whose pod setup keeps failing, are now marked failed after a bounded number of retries instead of retrying indefinitely and appearing stuck in the queue.

## 2026-05-15 <a href="#product-2026-05-15" id="product-2026-05-15"></a>

### Breaking changes

* The badge service that rendered SVG status badges from policy results is removed. Policy status is surfaced through the Lunar check on pull requests and through the dashboards, which had already replaced badges in practice.

### Improvements

* Policy checks now finalize without waiting for CI results that will never arrive: on repositories with no CI workflows, on commits marked `[skip ci]`, and on commits where CI exists but GitHub emits no workflow run. Previously such commits could show pending checks indefinitely.
* Repository webhook setup and repository syncing now run in the background after a Lunar configuration sync instead of slowing it down, and the Hub removes its webhook from a repository when the last component referencing that repository leaves the catalog.
* Dashboard pages load significantly faster on large installations — the component checks query is around 12× faster and the components listing paginates before joining check data — and collector run counts on the collectors listing now match the runs page.
* Dashboards are reorganized on Grafana 13.1: the collector, policy, initiative, and domain detail pages group their content into tabs with live counts, the components listing adds pagination and filters by component, domain, owner, and tag, the domain page shows its components directly, and the runs listings add a Queued tab showing runs waiting to execute.
* Lunar Hub now authenticates to GitHub exclusively through GitHub App installations; its legacy personal-access-token authentication path is removed. App authentication carries finer-grained permissions and supports zero-downtime key rotation.
* SQL API tables now materialize hourly instead of every four hours, so queries against materialized data reflect recent activity sooner.

### Bug fixes

* A Lunar configuration update no longer causes components to return an empty Component JSON until new collections arrive; data collected under earlier configuration versions remains visible.
* Cataloger results are no longer silently dropped when catalogers run in Kubernetes; cataloger output — including output submitted in multiple batches — now reaches the catalog correctly.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs-lunar.earthly.dev/release-notes/product/2026.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
