For the complete documentation index, see llms.txt. This page is also available as Markdown.

2026

2026-09-02

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.

  • 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

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 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 for setup instructions.

Custom failure text per policy

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.

  • 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.

  • 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.

  • 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.

  • 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.

  • 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

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.

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.

  • 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

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.

  • 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.

  • 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.

  • 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.

  • 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.

  • 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.

  • 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.

  • 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.

  • 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.

  • 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.

  • 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.

  • 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

Features

Customizable checks report templates

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.

  • 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

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_attimestamp 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

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.

  • 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.

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.

  • 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.

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.

  • 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

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.

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.

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.

  • 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.

  • 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

Features

Break-glass gate bypasses

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

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

Features

Release Ledger dashboard

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.

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.

  • 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.

2026-07-21

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.

  • 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.

  • 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.

  • 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

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

Features

Collector data dependencies: the after-json hook

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

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

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

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

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

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.

  • 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

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.

2026-06-23

Features

Buildkite support

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

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

Features

Out-of-band collection

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

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

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

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.

Last updated