diff --git a/src/components/Tables/PullRequestAttributes.tsx b/src/components/Tables/PullRequestAttributes.tsx
index 20605d1580..a9623e116e 100644
--- a/src/components/Tables/PullRequestAttributes.tsx
+++ b/src/components/Tables/PullRequestAttributes.tsx
@@ -1,8 +1,11 @@
+import { getAttributeSource } from '../../util/attributeMetadata';
import configSchema from '../../util/sanitizedConfigSchema';
import { getValueType } from './ConfigOptions';
import { renderMarkdown } from './utils';
+type Attributes = typeof configSchema.$defs.PullRequestAttributes.properties;
+
// The engine annotates each condition attribute with its authoritative operators
// and modifiers (`x-allowed-operators` / `x-modifiers`). The generated TypeScript
// types drop `x-` keys, so they are read from the imported JSON via this shape.
@@ -15,7 +18,10 @@ interface ConditionMeta {
}
interface Props {
- staticAttributes: typeof configSchema.$defs.PullRequestAttributes.properties;
+ staticAttributes?: Attributes;
+ // When set, render only attributes whose metadata source matches (e.g. "github").
+ // When unset, render only attributes that have no source (config-writable ones).
+ source?: string;
}
function renderOperators(operators: string[]) {
@@ -60,8 +66,17 @@ function renderModifiers(modifiers: ConditionMeta['x-modifiers']) {
return parts.flatMap((part, index) => (index === 0 ? [part] : [' ', part]));
}
-export default function PullRequestAttributes({ staticAttributes }: Props) {
- const attributes = staticAttributes ?? configSchema.$defs.PullRequestAttributes.properties;
+export default function PullRequestAttributes({ staticAttributes, source }: Props) {
+ // The search-highlight path passes a pre-filtered subset; render it as-is.
+ // Otherwise partition the canonical list by metadata source.
+ const attributes =
+ staticAttributes ??
+ (Object.fromEntries(
+ Object.entries(configSchema.$defs.PullRequestAttributes.properties).filter(([, value]) => {
+ const attributeSource = getAttributeSource(value);
+ return source ? attributeSource === source : attributeSource === undefined;
+ })
+ ) as Attributes);
const entries = Object.entries(attributes).sort(([keyA], [keyB]) => (keyA > keyB ? 1 : -1));
diff --git a/src/content/docs/configuration/conditions.mdx b/src/content/docs/configuration/conditions.mdx
index 529153954a..e8b6f09fb2 100644
--- a/src/content/docs/configuration/conditions.mdx
+++ b/src/content/docs/configuration/conditions.mdx
@@ -67,8 +67,23 @@ Boolean attributes are used on their own or negated with `-`:
## Attributes List
+Conditions are evaluated against the following pull request attributes, split
+into Mergify attributes and a read-only set loaded from GitHub.
+
+### Mergify Attributes
+
+### GitHub Rulesets and Branch Protection Attributes
+
+Mergify loads these attributes from the GitHub rulesets and branch protection
+rules active on the pull request's base branch. They are read-only: Mergify
+populates their values from GitHub, so you cannot set them in your
+configuration. Use them in conditions to gate on the same review and protection
+requirements that GitHub enforces.
+
+
+
## Operators List
diff --git a/src/util/attributeMetadata.test.ts b/src/util/attributeMetadata.test.ts
new file mode 100644
index 0000000000..a1a11c0a04
--- /dev/null
+++ b/src/util/attributeMetadata.test.ts
@@ -0,0 +1,24 @@
+import { describe, expect, test } from 'vitest';
+import { getAttributeSource } from './attributeMetadata';
+
+describe('getAttributeSource', () => {
+ test('returns the source when present', () => {
+ expect(getAttributeSource({ 'x-mergify-attribute-metadata': { source: 'github' } })).toBe(
+ 'github'
+ );
+ });
+
+ test('returns undefined when the attribute has no metadata', () => {
+ expect(getAttributeSource({ type: 'boolean' })).toBeUndefined();
+ });
+
+ test('returns undefined for a missing or malformed source', () => {
+ expect(getAttributeSource({ 'x-mergify-attribute-metadata': {} })).toBeUndefined();
+ expect(getAttributeSource({ 'x-mergify-attribute-metadata': { source: 42 } })).toBeUndefined();
+ });
+
+ test('returns undefined for non-object input', () => {
+ expect(getAttributeSource(null)).toBeUndefined();
+ expect(getAttributeSource('github')).toBeUndefined();
+ });
+});
diff --git a/src/util/attributeMetadata.ts b/src/util/attributeMetadata.ts
new file mode 100644
index 0000000000..8acfa955fa
--- /dev/null
+++ b/src/util/attributeMetadata.ts
@@ -0,0 +1,23 @@
+// Pull-request attribute definitions publish provenance under this custom
+// JSON-schema key.
+const ATTRIBUTE_METADATA_KEY = 'x-mergify-attribute-metadata';
+
+/**
+ * Return the `source` recorded on a pull-request attribute definition, or
+ * `undefined` when the attribute carries no metadata. A `github` source marks
+ * attributes that Mergify loads from GitHub rulesets and branch protection:
+ * they are read-only and cannot be set in the configuration.
+ */
+export function getAttributeSource(definition: unknown): string | undefined {
+ if (!definition || typeof definition !== 'object') {
+ return undefined;
+ }
+
+ const metadata = (definition as Record)[ATTRIBUTE_METADATA_KEY];
+ if (metadata && typeof metadata === 'object') {
+ const source = (metadata as Record).source;
+ return typeof source === 'string' ? source : undefined;
+ }
+
+ return undefined;
+}
diff --git a/src/util/schemaToMarkdown.test.ts b/src/util/schemaToMarkdown.test.ts
index 78fcf7f659..974793a9b7 100644
--- a/src/util/schemaToMarkdown.test.ts
+++ b/src/util/schemaToMarkdown.test.ts
@@ -30,6 +30,20 @@ describe('expandMdxComponents', () => {
expect(result).not.toContain('PullRequestAttributesTable');
});
+ test('default PullRequestAttributesTable excludes GitHub-sourced attributes', () => {
+ const result = expandMdxComponents('');
+ expect(result).toContain('`author`');
+ expect(result).not.toContain('`github-review-approved`');
+ });
+
+ test('PullRequestAttributesTable source="github" lists only GitHub-sourced attributes', () => {
+ const result = expandMdxComponents('');
+ expect(result).toContain('`github-review-approved`');
+ expect(result).toContain('`github-code-owner-review-satisfied`');
+ expect(result).not.toContain('`author`');
+ expect(result).not.toContain('PullRequestAttributesTable');
+ });
+
test('removes import statements', () => {
const input = [
'import OptionsTable from "../../../components/Tables/OptionsTable";',
diff --git a/src/util/schemaToMarkdown.ts b/src/util/schemaToMarkdown.ts
index 094df78fee..72de1b786f 100644
--- a/src/util/schemaToMarkdown.ts
+++ b/src/util/schemaToMarkdown.ts
@@ -1,5 +1,6 @@
import jsonpointer from 'jsonpointer';
+import { getAttributeSource } from './attributeMetadata';
import configSchema from './sanitizedConfigSchema';
type Schema = typeof configSchema;
@@ -99,9 +100,14 @@ function generateOptionsTable(defName: string): string {
return [headerLine, separatorLine, ...rowLines].join('\n');
}
-function generatePullRequestAttributesTable(): string {
+function generatePullRequestAttributesTable(source?: string): string {
const attributes = (configSchema as any).$defs.PullRequestAttributes.properties;
- const entries = Object.entries(attributes).sort(([a], [b]) => a.localeCompare(b));
+ const entries = Object.entries(attributes)
+ .filter(([, value]) => {
+ const attributeSource = getAttributeSource(value);
+ return source ? attributeSource === source : attributeSource === undefined;
+ })
+ .sort(([a], [b]) => a.localeCompare(b));
const rows: string[][] = [];
for (const [key, value] of entries) {
@@ -178,9 +184,10 @@ export function expandMdxComponents(source: string): string {
generateOptionsTable(def)
);
- // Replace
- source = source.replace(//g, () =>
- generatePullRequestAttributesTable()
+ // Replace (optionally filtered by source)
+ source = source.replace(
+ //g,
+ (_match, attributeSource) => generatePullRequestAttributesTable(attributeSource)
);
// Collapse runs of 3+ blank lines to 2