diff --git a/src/components/Tables/ConfigOptions.tsx b/src/components/Tables/ConfigOptions.tsx index 76f6264624..9486f25d07 100644 --- a/src/components/Tables/ConfigOptions.tsx +++ b/src/components/Tables/ConfigOptions.tsx @@ -18,11 +18,20 @@ const valueTypeLinks: { [key: string]: string } = { }; const valueTypeFormatLinks: { [key: string]: string } = { - template: '/configuration/data-types#template', + template: '/configuration/data-types#legacy-template', 'date-time': '/configuration/data-types#timestamp', duration: '/configuration/data-types#duration', }; +// A simple-template field carries a per-set title (e.g. "Message template"); render +// the title as the field type, linking to the matching data-types section. +const simpleTemplateTitleLinks: { [key: string]: string } = { + 'Message template': '/configuration/data-types#message-template', + 'Copy title template': '/configuration/data-types#copy-title-template', + 'Copy body template': '/configuration/data-types#copy-body-template', + 'Workflow input template': '/configuration/data-types#workflow-input-template', +}; + export type OptionDefinitionRef = string; export interface OptionDefinitionProperties { @@ -189,6 +198,19 @@ export function getValueType(schema: object, definition: any): React.ReactElemen ); } else if ('const' in definition) { valueType = {definition.const}; + } else if (definition.format === 'simple-template') { + // A simple-template field IS a named data type: show its title and link to the + // matching data-types section, like Template/Timestamp/Duration. + const title: string = definition.title ?? 'simple-template'; + const titleLink = simpleTemplateTitleLinks[title]; + valueType = + titleLink !== undefined ? ( + + {title} + + ) : ( + {title} + ); } else if ('format' in definition) { const formatLink = valueTypeFormatLinks[definition.format]; if (formatLink !== undefined) { @@ -198,6 +220,13 @@ export function getValueType(schema: object, definition: any): React.ReactElemen ); } + } else if ( + definition.additionalProperties && + typeof definition.additionalProperties === 'object' + ) { + // A map field (e.g. github_actions inputs): show "map of " so the + // templated value type still links to its data-types section. + valueType = <>map of {getValueType(schema, definition.additionalProperties)}; } else { valueType = {definition.type}; } diff --git a/src/components/Tables/OptionsTable.tsx b/src/components/Tables/OptionsTable.tsx index 7b16d3fa16..0e272d166c 100644 --- a/src/components/Tables/OptionsTable.tsx +++ b/src/components/Tables/OptionsTable.tsx @@ -1,6 +1,7 @@ import * as yaml from 'js-yaml'; import configSchema from '../../util/sanitizedConfigSchema'; +import { extractTemplateVariables } from '../../util/templateVariables'; import Badge from '../Badge/Badge'; import { ConfigSchema, @@ -50,6 +51,19 @@ export function OptionsTableBase( : ''; const defaultIsMultiline = hasDefault && defaultDump.includes('\n'); const isDeprecated = Boolean(definition.deprecated); + // A simple-template field's description carries an "Allowed variables: …" + // list. Once the schema publishes x-mergify-template-variables, the data + // type renders a richer , so the duplicate list is + // stripped here. Until that annotation is synced in, keep the list — it is + // the only variable reference readers have. + const rawDescription = (definition as OptionDefinition).description; + const hasPublishedVariables = + definition.format === 'simple-template' && + extractTemplateVariables(definition).length > 0; + const description = + hasPublishedVariables && typeof rawDescription === 'string' + ? rawDescription.replace(/\s*Allowed variables:.*$/s, '').trim() + : rawDescription; const id = `${idPrefix}${optionKey}`; const href = `#${encodeURIComponent(id)}`; @@ -86,11 +100,11 @@ export function OptionsTableBase( {defaultDump} )} - {definition.description !== undefined && ( + {description !== undefined && (
)} diff --git a/src/components/Tables/TemplateVariablesTable.tsx b/src/components/Tables/TemplateVariablesTable.tsx new file mode 100644 index 0000000000..3db0f174e5 --- /dev/null +++ b/src/components/Tables/TemplateVariablesTable.tsx @@ -0,0 +1,46 @@ +import configSchema from '../../util/sanitizedConfigSchema'; +import { extractTemplateVariables } from '../../util/templateVariables'; + +import { ConfigSchema, Def } from './ConfigOptions'; +import { renderMarkdown } from './utils'; + +interface TemplateVariablesTableProps extends Def { + field: string; +} + +export default function TemplateVariablesTable({ def, field }: TemplateVariablesTableProps) { + const schema = configSchema as unknown as ConfigSchema; + const definition = schema.$defs[def]?.properties?.[field]; + const variables = extractTemplateVariables(definition); + + // Astro's React SSR rejects a component that conditionally returns null/undefined, + // so render an empty fragment (not null) when the field has no published variables + // — the graceful state before the engine schema with x-mergify-template-variables + // is synced into the docs. + if (variables.length === 0) { + return <>; + } + + return ( +
+ + + + + + + + + {variables.map((variable) => ( + + + + ))} + +
VariableDescription
+ {`{{ ${variable.name} }}`} + +
+
+ ); +} diff --git a/src/content/docs/configuration/data-types.mdx b/src/content/docs/configuration/data-types.mdx index 113f152e3b..9e295d6c40 100644 --- a/src/content/docs/configuration/data-types.mdx +++ b/src/content/docs/configuration/data-types.mdx @@ -4,6 +4,7 @@ description: The different data types you can find in Mergify configuration file --- import OptionsTable from '../../../components/Tables/OptionsTable'; +import TemplateVariablesTable from '../../../components/Tables/TemplateVariablesTable'; When using templates or conditions, data are made of different types. You will find below the different data types that are available and exposed in Mergify @@ -256,7 +257,67 @@ priority_rules: priority: 550 ``` -## Template +## Templates + +Some fields are rendered as templates before Mergify uses them. Most accept +*variable substitution* (short `{{ name }}` placeholders filled with pull +request data); a few still use the legacy [Jinja2](#legacy-template) language. + +### Variable substitution + +Several fields let you insert pull request data using variables: + +```text +Thank you {{ author }} for your contribution! +``` + +renders to: + +```text +Thank you jd for your contribution! +``` + +when the pull request author login is `jd`. + +A variable is written as `{{ name }}` (surrounding spaces are optional). Every +other character is kept as-is. Each templated field accepts a specific set of +variables, documented as its data type below. + +### Message template + +Used by the [`comment`](/workflow/actions/comment), +[`review`](/workflow/actions/review) and [`close`](/workflow/actions/close) +message fields. + + + +### Copy title template + +Used by the [`copy`](/workflow/actions/copy) and +[`backport`](/workflow/actions/backport) `title` field. + + + +### Copy body template + +Used by the [`copy`](/workflow/actions/copy) and +[`backport`](/workflow/actions/backport) `body` field. + + + +### Workflow input template + +Used by the [`github_actions`](/workflow/actions/github_actions) workflow input +values. + + + +### Legacy template + +:::note + Jinja2 is deprecated for fields that support [variable + substitution](#variable-substitution) and stops working on 2026-09-30. +::: The template data type is a regular string that is rendered using the [Jinja2 template language](https://jinja.palletsprojects.com/templates/). diff --git a/src/content/docs/workflow/actions/assign.mdx b/src/content/docs/workflow/actions/assign.mdx index cf6d64ea4c..12e59edad4 100644 --- a/src/content/docs/workflow/actions/assign.mdx +++ b/src/content/docs/workflow/actions/assign.mdx @@ -17,7 +17,7 @@ pull requests that require their attention. As the list of users in `add_users` or `remove_users` is based on -[templates](/configuration/data-types#template), you can use, e.g., +[templates](/configuration/data-types#legacy-template), you can use, e.g., `{{author}}` to assign the pull request to its author. :::caution diff --git a/src/content/docs/workflow/actions/backport.mdx b/src/content/docs/workflow/actions/backport.mdx index 9085bb968f..b3f166975e 100644 --- a/src/content/docs/workflow/actions/backport.mdx +++ b/src/content/docs/workflow/actions/backport.mdx @@ -32,19 +32,6 @@ strings. -As the title and body are templates, you can leverage any pull request -attributes to use as content, e.g., `{{author}}`. - -Note that the `commits` attribute here will be the list of cherry -picked commits. - -On top of that, you can also use the following additional variables: - -- `{{ destination_branch }}`: the name of the destination branch. - -- `{{ cherry_pick_error }}`: the cherry pick error message if any (only - available in body). - ## Examples @@ -101,10 +88,11 @@ In this configuration, a pull request is backported when it has the label `backp Then, when the backport is created and passes the check named `continuous-integration`, it will be automatically merged. -### Implementing `-x` option +### Including the cherry-picked commits -If you are used to the `-x` option of `git cherry-pick` that includes which -commits has been cherry-picked, you can implement the same thing with Mergify: +To record which commits were cherry-picked (similar to the `-x` option of `git +cherry-pick`), include their SHAs in the backport body with the +`{{ cherry_picked_commits }}` variable: ```yaml pull_request_rules: @@ -116,9 +104,7 @@ pull_request_rules: body: | {{ body }} - {% for c in commits %} - (cherry picked from commit {{ c.sha }}) - {% endfor %} + {{ cherry_picked_commits }} branches: - stable ``` diff --git a/src/content/docs/workflow/actions/copy.mdx b/src/content/docs/workflow/actions/copy.mdx index 65be0360c5..8b9b2c2ed2 100644 --- a/src/content/docs/workflow/actions/copy.mdx +++ b/src/content/docs/workflow/actions/copy.mdx @@ -34,19 +34,6 @@ request will be copied. The branch names should be specified as strings. -As the title and body are templates, you can leverage any pull request -attributes to use as content, e.g., `{{author}}`. - -Note that the `commits` attribute here will be the list of cherry -picked commits. - -On top of that, you can also use the following additional variables: - -- `{{ destination_branch }}`: the name of the destination branch. - -- `{{ cherry_pick_error }}`: the cherry pick error message if any (only - available in body). - ## Examples diff --git a/src/content/docs/workflow/actions/github_actions.mdx b/src/content/docs/workflow/actions/github_actions.mdx index cccec24864..b1f4c00a6a 100644 --- a/src/content/docs/workflow/actions/github_actions.mdx +++ b/src/content/docs/workflow/actions/github_actions.mdx @@ -40,7 +40,8 @@ following rule. Here, the `hello_world_workflow.yaml` workflow accepts two inputs, which are defined as `name` and `age`. The `dynamic_workflow.yaml` -takes the [template](/configuration/data-types#template) input `author`. +takes the `author` input, set with [variable +substitution](/configuration/data-types#variable-substitution). ```yaml pull_request_rules: diff --git a/src/content/docs/workflow/actions/merge.mdx b/src/content/docs/workflow/actions/merge.mdx index 03531fe571..45e099f884 100644 --- a/src/content/docs/workflow/actions/merge.mdx +++ b/src/content/docs/workflow/actions/merge.mdx @@ -247,7 +247,7 @@ commit_message_format: ### Migrating from `commit_message_template` The legacy `commit_message_template` setting is a -[template](/configuration/data-types#template) that renders the entire +[template](/configuration/data-types#legacy-template) that renders the entire commit message. `commit_message_format` covers the common patterns declaratively, with predictable output and no template engine. diff --git a/src/content/docs/workflow/actions/post_check.mdx b/src/content/docs/workflow/actions/post_check.mdx index 1da8976dc8..551cd82c50 100644 --- a/src/content/docs/workflow/actions/post_check.mdx +++ b/src/content/docs/workflow/actions/post_check.mdx @@ -74,7 +74,7 @@ status of a pull request based on Mergify's evaluation. As the `title` and `summary` are -[templates](/configuration/data-types#template), you can benefit from any [pull +[templates](/configuration/data-types#legacy-template), you can benefit from any [pull request attributes](/configuration/conditions#attributes-list), e.g. `{{author}}`, and also these additional variables: diff --git a/src/content/docs/workflow/rule-syntax.mdx b/src/content/docs/workflow/rule-syntax.mdx index b2f107f6cb..413d7f8110 100644 --- a/src/content/docs/workflow/rule-syntax.mdx +++ b/src/content/docs/workflow/rule-syntax.mdx @@ -60,7 +60,7 @@ pull_request_rules: - conflict ``` -The `{{author}}` placeholder is a [template](/configuration/data-types#template) +The `{{author}}` placeholder is a [template](/configuration/data-types#legacy-template) that Mergify fills in with the pull request's data. Browse every available action and its parameters in the [Actions catalog](/workflow/actions). diff --git a/src/util/templateVariables.test.ts b/src/util/templateVariables.test.ts new file mode 100644 index 0000000000..45f1ca534d --- /dev/null +++ b/src/util/templateVariables.test.ts @@ -0,0 +1,58 @@ +import { describe, expect, it } from 'vitest'; + +import { extractTemplateVariables } from './templateVariables'; + +describe('extractTemplateVariables', () => { + it('reads variables from a top-level simple-template field', () => { + const definition = { + type: 'string', + format: 'simple-template', + 'x-mergify-template-variables': [{ name: 'author', description: 'PR author login' }], + }; + expect(extractTemplateVariables(definition)).toEqual([ + { name: 'author', description: 'PR author login' }, + ]); + }); + + it('finds variables inside an anyOf branch (optional field like comment.message)', () => { + const definition = { + anyOf: [ + { + type: 'string', + format: 'simple-template', + 'x-mergify-template-variables': [{ name: 'number', description: 'PR number' }], + }, + { type: 'null' }, + ], + }; + expect(extractTemplateVariables(definition)).toEqual([ + { name: 'number', description: 'PR number' }, + ]); + }); + + it('finds variables under additionalProperties → anyOf (dict field like github_actions inputs)', () => { + const definition = { + type: 'object', + additionalProperties: { + anyOf: [ + { type: 'integer' }, + { type: 'boolean' }, + { + type: 'string', + format: 'simple-template', + 'x-mergify-template-variables': [{ name: 'base', description: 'base branch' }], + }, + ], + }, + }; + expect(extractTemplateVariables(definition)).toEqual([ + { name: 'base', description: 'base branch' }, + ]); + }); + + it('returns [] when no template variables are present', () => { + expect(extractTemplateVariables({ type: 'string' })).toEqual([]); + expect(extractTemplateVariables(null)).toEqual([]); + expect(extractTemplateVariables(undefined)).toEqual([]); + }); +}); diff --git a/src/util/templateVariables.ts b/src/util/templateVariables.ts new file mode 100644 index 0000000000..35ceedfff7 --- /dev/null +++ b/src/util/templateVariables.ts @@ -0,0 +1,54 @@ +export interface TemplateVariable { + name: string; + description: string; +} + +// Field definitions publish their allowlist under this custom JSON-schema key. +const TEMPLATE_VARIABLES_KEY = 'x-mergify-template-variables'; + +/** + * Find the published template-variable allowlist on a JSON-schema field + * definition. The annotation may sit: + * - directly on the field (a required simple-template field, e.g. `close.message`); + * - inside an `anyOf`/`oneOf`/`allOf` branch alongside a `null` branch (an optional + * field, e.g. `comment.message`); + * - under `additionalProperties`/`items` (a dict/array-valued field whose values are + * templated, e.g. the `github_actions` `inputs` map). + * Search recursively and return the first branch that carries it. + */ +export function extractTemplateVariables(definition: unknown): TemplateVariable[] { + if (!definition || typeof definition !== 'object') { + return []; + } + + const node = definition as Record; + + const direct = node[TEMPLATE_VARIABLES_KEY]; + if (Array.isArray(direct)) { + return direct as TemplateVariable[]; + } + + for (const key of ['anyOf', 'oneOf', 'allOf'] as const) { + const branches = node[key]; + if (Array.isArray(branches)) { + for (const branch of branches) { + const found = extractTemplateVariables(branch); + if (found.length > 0) { + return found; + } + } + } + } + + for (const key of ['additionalProperties', 'items'] as const) { + const child = node[key]; + if (child && typeof child === 'object') { + const found = extractTemplateVariables(child); + if (found.length > 0) { + return found; + } + } + } + + return []; +}