Ticket Guardrails
Enforce ticket hygiene across your organization. Verify PRs reference valid issue tracker tickets, check ticket status and type, enforce a specific tracker, and detect ticket reuse across multiple PRs. Works with Jira by default; other issue trackers require a compatible ticket collector.
ticket to your lunar-config.yml:uses: github://earthly/lunar-lib/policies/ticket@v1.0.5
Included Guardrails
This policy includes 7 guardrails that enforce standards for your devex build and ci.
ticket-present
Ensures PRs reference an issue tracker ticket in the title.
ticket-valid
Ensures the referenced ticket exists and is valid in the issue tracker.
ticket-source
Ensures the ticket comes from an approved issue tracker.
ticket-status
Ensures the ticket is in an acceptable workflow status.
ticket-type
Ensures the ticket is an acceptable issue type.
ticket-reuse
Prevents teams from recycling the same ticket across too many PRs.
ticket-field
Ensures a configured ticket field, such as a Jira custom field, has an allowed value.
How Guardrails Fit into Lunar
Lunar guardrails define your engineering standards as code. They evaluate data collected by integrations and produce pass/fail checks with actionable feedback.
Policies support gradual enforcement—from silent scoring to blocking PRs or deployments—letting you roll out standards at your own pace without disrupting existing workflows.
Learn How Lunar Works →Required Integrations
This policy evaluates data gathered by one or more of the following integration(s).
Make sure to enable them in your lunar-config.yml.
Configuration
Configure this policy in your lunar-config.yml.
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
allowed_sources
|
Required | — | Comma-separated list of allowed ticket sources (e.g. "jira,linear"). Empty = any. |
allowed_statuses
|
Required | — | Comma-separated list of allowed ticket statuses (empty = any) |
disallowed_statuses
|
Required | — | Comma-separated list of disallowed ticket statuses (empty = none) |
allowed_types
|
Required | — | Comma-separated list of allowed issue types (empty = any) |
max_ticket_reuse
|
Optional |
3
|
Maximum number of other PRs allowed to reference the same ticket |
ticket_field
|
Required | — | Field the `ticket-field` check reads, as a path relative to the ticket (e.g. native.jira.fields.customfield_10042 for a Jira custom field). Empty skips the check. |
allowed_field_values
|
Required | — | Values `ticket-field` accepts: comma-separated, one per line, or a JSON array for values that contain a comma. Empty accepts any value, so the field only has to be set. |
ticket_path
|
Optional |
.vcs.pr.ticket
|
Where every check reads the ticket from. Point a second import of this policy at the path a second jira collector import records to (e.g. .vcs.pr.architecture_review) to check that reference as well. |
skip_authors
|
Required | — | PR author logins whose PRs every check skips, such as dependency bots (e.g. dependabot[bot], renovate[bot]). Comma-separated or one per line; matched exactly, ignoring case, so keep the [bot] suffix GitHub gives an app. Empty skips no one. |
Documentation
View on GitHubTicket Guardrails
Enforce issue tracker ticket hygiene across your organization's pull requests. Works with any issue tracker (Jira, Linear, GitHub Issues, etc.).
Overview
This policy verifies that PRs reference valid tickets, checks ticket status and type, enforces a specific issue tracker, and detects ticket reuse across multiple PRs. It can also require any ticket field, such as a Jira custom field, to hold an allowed value, and run the same checks on a second reference like an architecture-review submission. It helps teams maintain traceability between code changes and project management.
Policies
This plugin provides the following policies (use include to select a subset):
| Policy | Description | Failure Meaning |
|---|---|---|
ticket-present |
PRs must reference a ticket | No ticket ID found in PR title |
ticket-valid |
Referenced ticket must exist | Ticket ID was parsed but the tracker did not confirm it exists |
ticket-source |
Ticket must come from an approved tracker | Ticket source not in allowed list |
ticket-status |
Ticket must be in an acceptable status | Ticket status is disallowed or not in allowed list |
ticket-type |
Ticket must be an acceptable issue type | Issue type not in allowed list |
ticket-reuse |
Same ticket can't be reused too many times | Ticket used in more PRs than the configured limit |
ticket-field |
A configured ticket field must have an allowed value | Field is empty or its value is not in allowed_field_values |
Required Data
This policy reads from the following Component JSON paths:
| Path | Type | Description |
|---|---|---|
.vcs.pr.ticket.id |
string | Ticket ID extracted from PR title |
.vcs.pr.ticket.source |
string | Issue tracker name (e.g. "jira", "linear") |
.vcs.pr.ticket.valid |
boolean | Whether ticket exists in the tracker |
.vcs.pr.ticket.tracker_error |
string | Why .valid is absent, when the collector reports it (not_found, unreachable) |
.vcs.pr.ticket.status |
string | Ticket workflow status |
.vcs.pr.ticket.type |
string | Issue type (e.g. Story, Bug) |
.vcs.pr.ticket.reuse_count |
number | Count of other PRs using same ticket |
.vcs.pr.ticket.native |
object | Raw tracker data (e.g. .native.jira), read by ticket-field |
.vcs.pr.author |
string | PR author login, read only when skip_authors is set (github and gitlab collectors) |
Every check reads from ticket_path instead of .vcs.pr.ticket when it is set.
Installation
Add to your lunar-config.yml:
policies:
- uses: github://earthly/lunar-lib/policies/ticket
on: ["domain:your-domain"]
enforcement: report-pr
include:
- ticket-present
- ticket-valid
- ticket-source
- ticket-status
- ticket-type
- ticket-reuse
- ticket-field # skips until ticket_field is set
with:
allowed_sources: "jira"
disallowed_statuses: "Done,Closed"
max_ticket_reuse: "3"
# ticket_field: "native.jira.fields.customfield_10042" # Optional: enables ticket-field
Skipping bot PRs
Bots such as Dependabot open PRs without a ticket. List their logins in skip_authors and every check skips their PRs instead of failing them:
with:
skip_authors: "dependabot[bot],renovate[bot]"
A login matches .vcs.pr.author exactly, ignoring case, so use the one GitHub shows, including an app's [bot] suffix. The skip covers the whole PR, including any commits someone else pushes to the bot's branch.
Checking a second reference
When a second jira collector import records another reference, such as an architecture-review submission at .vcs.pr.architecture_review, import the policy again with the same ticket_path. This one requires the PR to reference a submission that exists and is approved, and a custom field on it to be set:
policies:
- uses: github://earthly/lunar-lib/policies/ticket
name: architecture-review
on: ["domain:your-domain"]
enforcement: block-pr
include: [ticket-present, ticket-valid, ticket-status, ticket-field]
with:
ticket_path: ".vcs.pr.architecture_review"
allowed_statuses: "Approved"
ticket_field: "native.jira.fields.customfield_10042"
ticket_field is relative to the ticket. A Jira select-list option matches on its value, a status or component on its name, and a user on its displayName; a multi-value field passes when any of its values is allowed. Jira lists custom field IDs at GET /rest/api/3/field.
Examples
Passing — Valid ticket with acceptable status
{
"vcs": {
"pr": {
"ticket": {
"id": "ENG-456",
"source": "jira",
"url": "https://acme.atlassian.net/browse/ENG-456",
"valid": true,
"status": "In Progress",
"type": "Story",
"summary": "Add payment validation",
"assignee": "jane@acme.com",
"reuse_count": 0
}
}
}
}
Failing — No ticket in PR title
{}
Failure message: "PR does not reference a ticket. Include a ticket ID in the PR title (e.g. [ABC-123])."
Remediation
When this policy fails, you can resolve it by:
- ticket-present: Add a ticket reference to your PR title (e.g.
ABC-123 Your PR description) - ticket-valid: Verify the ticket ID exists in the issue tracker
- ticket-source: Use the approved issue tracker for your organization
- ticket-status: Move the ticket to an acceptable status before opening the PR
- ticket-type: Use an acceptable issue type (e.g. Story, Bug, Task)
- ticket-reuse: Create a new ticket for this work instead of reusing an existing one
- ticket-field: Set the field on the ticket to one of the allowed values
Open Source
This policy is open source and available on GitHub. Contribute improvements, report issues, or fork it for your own use.
Common Use Cases
Explore how individual guardrails work with specific integrations.
Ready to Automate Your Standards?
See how Lunar can turn your AGENTS.md, engineering wiki, compliance docs, or postmortem action items into automated guardrails with our 300+ built-in guardrails.