diff --git a/integrations/validate-data-type-anchors.ts b/integrations/validate-data-type-anchors.ts index 0de789aa11..e61dd0d6ad 100644 --- a/integrations/validate-data-type-anchors.ts +++ b/integrations/validate-data-type-anchors.ts @@ -1,9 +1,19 @@ import type { AstroIntegration } from 'astro'; +import rawApiSchema from '../public/api-schemas.json'; import rawConfigSchema from '../public/mergify-configuration-schema.json'; import { missingDataTypeAnchors } from '../src/util/dataTypeAnchors'; +// Both synced schemas can carry the engine's `x-has-data-type` marker: the +// configuration schema for types you write in `.mergify.yml`, the OpenAPI spec +// for types the API only reports (a batch status, say). Both arrive by the same +// bot sync, so both need the same gate. +const SCHEMAS: { file: string; schema: unknown }[] = [ + { file: 'public/mergify-configuration-schema.json', schema: rawConfigSchema }, + { file: 'public/api-schemas.json', schema: rawApiSchema }, +]; + /** - * Fail the build when the config schema flags a documented data type whose + * Fail the build when a synced schema flags a documented data type whose * derived anchor (slugified `title`) has no matching heading on the * data-types page. This is the enforcement point that actually guards the * drift path: schema syncs land as direct bot pushes to main (no PR, so no @@ -16,14 +26,18 @@ export function validateDataTypeAnchors(): AstroIntegration { name: 'validate-data-type-anchors', hooks: { 'astro:build:start': () => { - const missing = missingDataTypeAnchors(rawConfigSchema); - if (missing.length > 0) { + const problems = SCHEMAS.flatMap(({ file, schema }) => { + const missing = missingDataTypeAnchors(schema); + return missing.length > 0 ? [`${file}: ${missing.join(', ')}`] : []; + }); + + if (problems.length > 0) { throw new Error( - `Documented data type(s) in public/mergify-configuration-schema.json have no ` + - `matching heading anchor on src/content/docs/configuration/data-types.mdx: ` + - `${missing.join(', ')}. A marked node's slugified title must equal the anchor ` + - `of its section heading (add the missing section, or fix the title next to the ` + - `engine's DocsDataType annotation).` + `Documented data type(s) in a synced schema have no matching heading anchor on ` + + `src/content/docs/configuration/data-types.mdx — ${problems.join('; ')}. ` + + `A marked node's slugified title must equal the anchor of its section heading ` + + `(add the missing section, or fix the title next to the engine's DocsDataType ` + + `annotation).` ); } }, diff --git a/package.json b/package.json index dc9b586eba..3a14f9a7ba 100644 --- a/package.json +++ b/package.json @@ -102,6 +102,7 @@ "rehype-autolink-headings": "^7.1.0", "rehype-format": "^5.0.1", "rehype-raw": "^7.0.0", + "rehype-sanitize": "^6.0.0", "rehype-slug": "^6.0.0", "rehype-stringify": "^10.0.1", "remark-lint": "^10.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 22042d9e3a..8fcdfeedc2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -94,6 +94,9 @@ importers: rehype-raw: specifier: ^7.0.0 version: 7.0.0 + rehype-sanitize: + specifier: ^6.0.0 + version: 6.0.0 rehype-slug: specifier: ^6.0.0 version: 6.0.0 @@ -3069,6 +3072,9 @@ packages: hast-util-raw@9.1.0: resolution: {integrity: sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==} + hast-util-sanitize@5.0.2: + resolution: {integrity: sha512-3yTWghByc50aGS7JlGhk61SPenfE/p1oaFeNwkOOyrscaOkMGrcW9+Cy/QAIOBpZxP1yqDIzFMR0+Np0i0+usg==} + hast-util-select@6.0.4: resolution: {integrity: sha512-RqGS1ZgI0MwxLaKLDxjprynNzINEkRHY2i8ln4DDjgv9ZhcYVIHN9rlpiYsqtFwrgpYU361SyWDQcGNIBVu3lw==} @@ -4242,6 +4248,9 @@ packages: rehype-recma@1.0.0: resolution: {integrity: sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==} + rehype-sanitize@6.0.0: + resolution: {integrity: sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg==} + rehype-slug@6.0.0: resolution: {integrity: sha512-lWyvf/jwu+oS5+hL5eClVd3hNdmwM1kAC0BUvEGD19pajQMIzcNUd/k9GsfQ+FfECvX+JE+e9/btsKH0EjJT6A==} @@ -8765,6 +8774,12 @@ snapshots: web-namespaces: 2.0.1 zwitch: 2.0.4 + hast-util-sanitize@5.0.2: + dependencies: + '@types/hast': 3.0.5 + '@ungap/structured-clone': 1.3.1 + unist-util-position: 5.0.0 + hast-util-select@6.0.4: dependencies: '@types/hast': 3.0.5 @@ -10298,6 +10313,11 @@ snapshots: transitivePeerDependencies: - supports-color + rehype-sanitize@6.0.0: + dependencies: + '@types/hast': 3.0.5 + hast-util-sanitize: 5.0.2 + rehype-slug@6.0.0: dependencies: '@types/hast': 3.0.5 diff --git a/public/api-schemas.json b/public/api-schemas.json index a28b89baa7..ba668754ab 100644 --- a/public/api-schemas.json +++ b/public/api-schemas.json @@ -9497,7 +9497,22 @@ "waiting_for_checks_slot", "frozen" ], - "title": "Code" + "title": "Batch Status", + "x-enum-descriptions": { + "bisecting": "The batch failed, so Mergify is splitting it into smaller sub-batches and testing those to identify the pull request responsible and merge the others.", + "failed": "The batch failed, either because Mergify could not prepare it for testing or because its checks did not pass. It is removed from the queue and its pull requests have to be fixed.", + "frozen": "The batch's checks passed, but merges are frozen. It merges once the freeze is lifted.", + "merged": "The batch was merged.", + "preparing": "Mergify is setting the batch up so it can be tested.", + "running": "The batch is set up and its CI checks are running.", + "waiting_for_batch": "Mergify is waiting for more pull requests to fill the batch, or for the maximum wait time to elapse, before starting it.", + "waiting_for_checks_slot": "The batch is ready to be tested, but every slot for running checks in parallel is taken. It starts as soon as one frees up.", + "waiting_for_merge": "The batch's checks passed. It merges once the batches ahead of it have merged.", + "waiting_for_previous_batches": "A batch ahead in the queue failed, so this batch is held while Mergify identifies the cause. It has either not started its own checks yet, or already ran and failed and is parked until the cause is known.", + "waiting_for_requeue": "A batch ahead in the queue failed, so this batch is requeued and retested against the updated queue.", + "waiting_schedule": "The batch's merge conditions are met, but a schedule condition is not. It continues once the schedule allows it." + }, + "x-has-data-type": true }, "batch_filled_slots": { "anyOf": [ diff --git a/src/components/Tables/BatchStatusCodes.tsx b/src/components/Tables/BatchStatusCodes.tsx new file mode 100644 index 0000000000..e9b1657f01 --- /dev/null +++ b/src/components/Tables/BatchStatusCodes.tsx @@ -0,0 +1,50 @@ +import apiSchema from '../../../public/api-schemas.json'; + +import { renderMarkdown } from './utils'; + +// A batch's `status.code` in the merge queue API. The engine is the single +// source of truth: the codes come from the property's enum, and a one-line +// description for each is published alongside it under `x-enum-descriptions` +// (keyed by the raw code), so this table can't drift from what the API +// returns. Same convention as the queue dequeue reason table, sourced from the +// OpenAPI spec rather than the configuration schema because a batch status is +// something the API reports, never something you write in `.mergify.yml`. +// +// The lookup is optional-chained through a loose cast so a future schema +// reshape (renamed model or property) degrades to an empty table rather than +// throwing at module load and crashing the Astro build. +const codeProp: unknown = ( + apiSchema as { + components?: { schemas?: Record }> }; + } +).components?.schemas?.BatchStatus?.properties?.code; + +export default function BatchStatusCodes() { + const node = codeProp as + | { enum?: string[]; 'x-enum-descriptions'?: Record } + | undefined; + const descriptions = node?.['x-enum-descriptions'] ?? {}; + + return ( +
+ + + + + + + + + {(node?.enum ?? []).map((code) => ( + + + + ))} + +
StatusDescription
+ {code} + +
+
+ ); +} diff --git a/src/components/Tables/utils.test.ts b/src/components/Tables/utils.test.ts new file mode 100644 index 0000000000..45868ac543 --- /dev/null +++ b/src/components/Tables/utils.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from 'vitest'; +import { renderMarkdown } from './utils'; + +// `renderMarkdown` output is injected with `dangerouslySetInnerHTML` by every +// schema-driven table, so what it lets through is a security property, not a +// formatting detail. These lock it. +describe('renderMarkdown', () => { + it('renders the markdown the schema descriptions actually use', () => { + const html = renderMarkdown('A [real link](https://example.com) and `code`.'); + expect(html).toContain('real link'); + expect(html).toContain('code'); + }); + + it('keeps relative links, anchors and mailto', () => { + expect(renderMarkdown('[a](/merge-queue/batches)')).toContain('href="/merge-queue/batches"'); + expect(renderMarkdown('[a](#batch-status)')).toContain('href="#batch-status"'); + expect(renderMarkdown('[a](mailto:x@example.com)')).toContain('href="mailto:x@example.com"'); + }); + + it('strips javascript: and data: URLs rather than emitting a live link', () => { + expect(renderMarkdown('[click](javascript:alert(1))')).not.toContain('javascript:'); + expect(renderMarkdown('[click](JaVaScRiPt:alert(1))')).not.toContain('alert(1)'); + expect(renderMarkdown('![x](data:text/html;base64,PHNjcmlwdD4=)')).not.toContain( + 'data:text/html' + ); + }); + + it('drops raw HTML, including event handlers and script tags', () => { + expect(renderMarkdown('')).not.toContain('onerror'); + expect(renderMarkdown('')).not.toContain(' +## Batch Status + +This describes what a [batch](/merge-queue/batches) is currently doing in the +merge queue. It is reported as the `status.code` field of each batch returned by +the [merge queue status API](/api/merge-queue), and the dashboard shows it on +each batch. + +A status describes the batch as a whole, not an individual pull request: a batch +that is running its checks reports `running` for every pull request it carries. + +The following statuses can be reported: + + + ## Report Mode Report modes allow you to choose the type of report you want for your actions. diff --git a/src/content/docs/merge-queue/batches.mdx b/src/content/docs/merge-queue/batches.mdx index 75df5e21cf..cfa087d6bb 100644 --- a/src/content/docs/merge-queue/batches.mdx +++ b/src/content/docs/merge-queue/batches.mdx @@ -379,6 +379,11 @@ Note that this system is completely automatic and there is no need to intervene. The number of maximum splits can be controlled by [`batch_max_failure_resolution_attempts`](/configuration/file-format#queue-rules). +While this runs, the batches involved report a +[batch status](/configuration/data-types#batch-status) such as `bisecting`, +`waiting_for_previous_batches`, or `waiting_for_requeue`, so you can follow the +resolution from [the dashboard or the CLI](/merge-queue/monitoring). + :::tip Each split carries metadata about the batches it came from. You can use it to [re-run only the tests that failed in the parent diff --git a/src/content/docs/merge-queue/monitoring.mdx b/src/content/docs/merge-queue/monitoring.mdx index 67e3600084..05a3f02312 100644 --- a/src/content/docs/merge-queue/monitoring.mdx +++ b/src/content/docs/merge-queue/monitoring.mdx @@ -126,6 +126,10 @@ This displays: - **Waiting PRs**: queued pull requests with priority, queue time, and estimated merge time +Each batch reports a [batch status](/configuration/data-types#batch-status) +describing what it is doing, such as running its checks or waiting for a +schedule. + To filter results to a specific branch: ```bash diff --git a/src/util/dataType.test.ts b/src/util/dataType.test.ts index c68bf2172d..6fc157206e 100644 --- a/src/util/dataType.test.ts +++ b/src/util/dataType.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from 'vitest'; +import apiSchema from '../../public/api-schemas.json'; import configSchema from '../../public/mergify-configuration-schema.json'; import { collectDataTypeTitles, getDataTypeHref, isDataType } from './dataType'; import { dataTypesHeadingAnchors, missingDataTypeAnchors } from './dataTypeAnchors'; @@ -57,6 +58,8 @@ describe('data-types page anchors', () => { 'priority', 'report-mode', 'schedule', + // marked in the OpenAPI spec rather than the configuration schema + 'batch-status', // anchors hardcoded in ConfigOptions.tsx link maps 'commit', 'commit-author', @@ -83,4 +86,12 @@ describe('data-types page anchors', () => { it('covers every documented data type flagged in the config schema', () => { expect(missingDataTypeAnchors(configSchema)).toEqual([]); }); + + // The same convention, for types the engine marks in the OpenAPI spec + // instead — a data type the API reports but you never write in + // `.mergify.yml`. Both schemas arrive by the same bot sync, so both are + // gated in integrations/validate-data-type-anchors.ts. + it('covers every documented data type flagged in the API schema', () => { + expect(missingDataTypeAnchors(apiSchema)).toEqual([]); + }); });