From ac7e53f324370464b3df952ec3a2ce8b822e8939 Mon Sep 17 00:00:00 2001 From: Quentin Pradet Date: Tue, 4 Nov 2025 17:27:15 +0400 Subject: [PATCH 1/7] Fix query parameters incorrectly typed as scalars --- output/openapi/elasticsearch-openapi.json | 28 ++++++++---- .../elasticsearch-serverless-openapi.json | 18 ++++++-- output/schema/schema.json | 45 ++++++++++++------- output/typescript/types.ts | 18 ++++---- .../field_caps/FieldCapabilitiesRequest.ts | 2 +- .../ClusterGetComponentTemplateRequest.ts | 4 +- .../add_block/IndicesAddBlockRequest.ts | 4 +- .../IndicesDataStreamsStatsRequest.ts | 4 +- .../remove_block/IndicesRemoveBlockRequest.ts | 4 +- .../SecurityClearCachedPrivilegesRequest.ts | 4 +- .../snapshot/delete/SnapshotDeleteRequest.ts | 4 +- .../FindFieldStructureRequest.ts | 4 +- .../FindMessageStructureRequest.ts | 4 +- 13 files changed, 89 insertions(+), 54 deletions(-) diff --git a/output/openapi/elasticsearch-openapi.json b/output/openapi/elasticsearch-openapi.json index 344955ce3c..2bf307a356 100644 --- a/output/openapi/elasticsearch-openapi.json +++ b/output/openapi/elasticsearch-openapi.json @@ -13699,7 +13699,7 @@ "required": true, "deprecated": false, "schema": { - "$ref": "#/components/schemas/_types.IndexName" + "$ref": "#/components/schemas/_types.Indices" }, "style": "simple" }, @@ -13825,7 +13825,7 @@ "required": true, "deprecated": false, "schema": { - "$ref": "#/components/schemas/_types.IndexName" + "$ref": "#/components/schemas/_types.Indices" }, "style": "simple" }, @@ -40409,7 +40409,7 @@ "required": true, "deprecated": false, "schema": { - "$ref": "#/components/schemas/_types.Name" + "$ref": "#/components/schemas/_types.Names" }, "style": "simple" } @@ -46528,7 +46528,7 @@ "required": true, "deprecated": false, "schema": { - "$ref": "#/components/schemas/_types.Name" + "$ref": "#/components/schemas/_types.Names" }, "style": "simple" }, @@ -49194,7 +49194,7 @@ "description": "If `format` is set to `delimited`, you can specify the column names in a comma-separated list.\nIf this parameter is not specified, the structure finder uses the column names from the header row of the text.\nIf the text does not have a header row, columns are named \"column1\", \"column2\", \"column3\", for example.", "deprecated": false, "schema": { - "type": "string" + "$ref": "#/components/schemas/_types.Names" }, "style": "form" }, @@ -137931,7 +137931,7 @@ "required": true, "deprecated": false, "schema": { - "$ref": "#/components/schemas/_types.Name" + "$ref": "#/components/schemas/_types.Names" }, "style": "simple" }, @@ -138906,7 +138906,17 @@ "description": "A comma-separated list of filters to apply to the response.", "deprecated": false, "schema": { - "type": "string" + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] }, "x-state": "Generally available; Added in 8.2.0", "style": "form" @@ -139970,7 +139980,7 @@ "required": true, "deprecated": false, "schema": { - "$ref": "#/components/schemas/_types.IndexName" + "$ref": "#/components/schemas/_types.Indices" }, "style": "simple" }, @@ -145860,7 +145870,7 @@ "description": "If the format is `delimited`, you can specify the column names in a comma-separated list.\nIf this parameter is not specified, the structure finder uses the column names from the header row of the text.\nIf the text does not have a header role, columns are named \"column1\", \"column2\", \"column3\", for example.", "deprecated": false, "schema": { - "type": "string" + "$ref": "#/components/schemas/_types.Names" }, "style": "form" }, diff --git a/output/openapi/elasticsearch-serverless-openapi.json b/output/openapi/elasticsearch-serverless-openapi.json index 68d188a72e..3676b0a1a8 100644 --- a/output/openapi/elasticsearch-serverless-openapi.json +++ b/output/openapi/elasticsearch-serverless-openapi.json @@ -7269,7 +7269,7 @@ "required": true, "deprecated": false, "schema": { - "$ref": "#/components/schemas/_types.IndexName" + "$ref": "#/components/schemas/_types.Indices" }, "style": "simple" }, @@ -7395,7 +7395,7 @@ "required": true, "deprecated": false, "schema": { - "$ref": "#/components/schemas/_types.IndexName" + "$ref": "#/components/schemas/_types.Indices" }, "style": "simple" }, @@ -84518,7 +84518,7 @@ "required": true, "deprecated": false, "schema": { - "$ref": "#/components/schemas/_types.Name" + "$ref": "#/components/schemas/_types.Names" }, "style": "simple" }, @@ -85239,7 +85239,17 @@ "description": "A comma-separated list of filters to apply to the response.", "deprecated": false, "schema": { - "type": "string" + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] }, "x-state": "Generally available", "style": "form" diff --git a/output/schema/schema.json b/output/schema/schema.json index ee1b8bbfa8..49d8024d7c 100644 --- a/output/schema/schema.json +++ b/output/schema/schema.json @@ -29606,11 +29606,26 @@ "name": "filters", "required": false, "type": { - "kind": "instance_of", - "type": { - "name": "string", - "namespace": "_builtins" - } + "kind": "union_of", + "items": [ + { + "kind": "instance_of", + "type": { + "name": "string", + "namespace": "_builtins" + } + }, + { + "kind": "array_of", + "value": { + "kind": "instance_of", + "type": { + "name": "string", + "namespace": "_builtins" + } + } + } + ] } }, { @@ -125893,7 +125908,7 @@ "type": { "kind": "instance_of", "type": { - "name": "Name", + "name": "Names", "namespace": "_types" } } @@ -153826,7 +153841,7 @@ "type": { "kind": "instance_of", "type": { - "name": "IndexName", + "name": "Indices", "namespace": "_types" } } @@ -156304,7 +156319,7 @@ "type": { "kind": "instance_of", "type": { - "name": "IndexName", + "name": "Indices", "namespace": "_types" } } @@ -167471,7 +167486,7 @@ "type": { "kind": "instance_of", "type": { - "name": "IndexName", + "name": "Indices", "namespace": "_types" } } @@ -241808,7 +241823,7 @@ "type": { "kind": "instance_of", "type": { - "name": "Name", + "name": "Names", "namespace": "_types" } } @@ -260043,7 +260058,7 @@ "type": { "kind": "instance_of", "type": { - "name": "Name", + "name": "Names", "namespace": "_types" } } @@ -266928,8 +266943,8 @@ "type": { "kind": "instance_of", "type": { - "name": "string", - "namespace": "_builtins" + "name": "Names", + "namespace": "_types" } } }, @@ -267375,8 +267390,8 @@ "type": { "kind": "instance_of", "type": { - "name": "string", - "namespace": "_builtins" + "name": "Names", + "namespace": "_types" } } }, diff --git a/output/typescript/types.ts b/output/typescript/types.ts index 9c56a76424..b812f96684 100644 --- a/output/typescript/types.ts +++ b/output/typescript/types.ts @@ -358,7 +358,7 @@ export interface FieldCapsRequest extends RequestBase { fields?: Fields ignore_unavailable?: boolean include_unmapped?: boolean - filters?: string + filters?: string | string[] types?: string[] include_empty_fields?: boolean project_routing?: ProjectRouting @@ -9693,7 +9693,7 @@ export interface ClusterExistsComponentTemplateRequest extends RequestBase { export type ClusterExistsComponentTemplateResponse = boolean export interface ClusterGetComponentTemplateRequest extends RequestBase { - name?: Name + name?: Names flat_settings?: boolean settings_filter?: string | string[] include_defaults?: boolean @@ -12379,7 +12379,7 @@ export interface IndicesAddBlockAddIndicesBlockStatus { } export interface IndicesAddBlockRequest extends RequestBase { - index: IndexName + index: Indices block: IndicesIndicesBlockOptions allow_no_indices?: boolean expand_wildcards?: ExpandWildcards @@ -12577,7 +12577,7 @@ export interface IndicesDataStreamsStatsDataStreamsStatsItem { } export interface IndicesDataStreamsStatsRequest extends RequestBase { - name?: IndexName + name?: Indices expand_wildcards?: ExpandWildcards } @@ -13519,7 +13519,7 @@ export interface IndicesRemoveBlockRemoveIndicesBlockStatus { } export interface IndicesRemoveBlockRequest extends RequestBase { - index: IndexName + index: Indices block: IndicesIndicesBlockOptions allow_no_indices?: boolean expand_wildcards?: ExpandWildcards @@ -20538,7 +20538,7 @@ export interface SecurityClearApiKeyCacheResponse { } export interface SecurityClearCachedPrivilegesRequest extends RequestBase { - application: Name + application: Names } export interface SecurityClearCachedPrivilegesResponse { @@ -21998,7 +21998,7 @@ export type SnapshotCreateRepositoryResponse = AcknowledgedResponseBase export interface SnapshotDeleteRequest extends RequestBase { repository: Name - snapshot: Name + snapshot: Names master_timeout?: Duration wait_for_completion?: boolean } @@ -22565,7 +22565,7 @@ export interface TextStructureTopHit { } export interface TextStructureFindFieldStructureRequest extends RequestBase { - column_names?: string + column_names?: Names delimiter?: string documents_to_sample?: uint ecs_compatibility?: TextStructureEcsCompatibilityType @@ -22600,7 +22600,7 @@ export interface TextStructureFindFieldStructureResponse { } export interface TextStructureFindMessageStructureRequest extends RequestBase { - column_names?: string + column_names?: Names delimiter?: string ecs_compatibility?: TextStructureEcsCompatibilityType explain?: boolean diff --git a/specification/_global/field_caps/FieldCapabilitiesRequest.ts b/specification/_global/field_caps/FieldCapabilitiesRequest.ts index 9fab39a69e..05d32711a4 100644 --- a/specification/_global/field_caps/FieldCapabilitiesRequest.ts +++ b/specification/_global/field_caps/FieldCapabilitiesRequest.ts @@ -91,7 +91,7 @@ export interface Request extends RequestBase { * @availability stack since=8.2.0 * @availability serverless */ - filters?: string + filters?: string | string[] /** * A comma-separated list of field types to include. * Any fields that do not match one of these types will be excluded from the results. diff --git a/specification/cluster/get_component_template/ClusterGetComponentTemplateRequest.ts b/specification/cluster/get_component_template/ClusterGetComponentTemplateRequest.ts index 4cb810c3d7..170a9cf2cc 100644 --- a/specification/cluster/get_component_template/ClusterGetComponentTemplateRequest.ts +++ b/specification/cluster/get_component_template/ClusterGetComponentTemplateRequest.ts @@ -18,7 +18,7 @@ */ import { RequestBase } from '@_types/Base' -import { Name } from '@_types/common' +import { Names } from '@_types/common' import { Duration } from '@_types/Time' /** @@ -47,7 +47,7 @@ export interface Request extends RequestBase { * Comma-separated list of component template names used to limit the request. * Wildcard (`*`) expressions are supported. */ - name?: Name + name?: Names } query_parameters: { /** diff --git a/specification/indices/add_block/IndicesAddBlockRequest.ts b/specification/indices/add_block/IndicesAddBlockRequest.ts index ddf5a41795..f337360b7b 100644 --- a/specification/indices/add_block/IndicesAddBlockRequest.ts +++ b/specification/indices/add_block/IndicesAddBlockRequest.ts @@ -18,7 +18,7 @@ */ import { RequestBase } from '@_types/Base' -import { ExpandWildcards, IndexName } from '@_types/common' +import { ExpandWildcards, Indices } from '@_types/common' import { Duration } from '@_types/Time' import { IndicesBlockOptions } from '@indices/_types/IndexSettings' @@ -46,7 +46,7 @@ export interface Request extends RequestBase { * To allow the adding of blocks to indices with `_all`, `*`, or other wildcard expressions, change the `action.destructive_requires_name` setting to `false`. * You can update this setting in the `elasticsearch.yml` file or by using the cluster update settings API. */ - index: IndexName + index: Indices /** * The block type to add to the index. */ diff --git a/specification/indices/data_streams_stats/IndicesDataStreamsStatsRequest.ts b/specification/indices/data_streams_stats/IndicesDataStreamsStatsRequest.ts index ec064fcd60..59b0661150 100644 --- a/specification/indices/data_streams_stats/IndicesDataStreamsStatsRequest.ts +++ b/specification/indices/data_streams_stats/IndicesDataStreamsStatsRequest.ts @@ -18,7 +18,7 @@ */ import { RequestBase } from '@_types/Base' -import { ExpandWildcards, IndexName } from '@_types/common' +import { ExpandWildcards, Indices } from '@_types/common' /** * Get data stream stats. @@ -48,7 +48,7 @@ export interface Request extends RequestBase { * Wildcard expressions (`*`) are supported. * To target all data streams in a cluster, omit this parameter or use `*`. */ - name?: IndexName + name?: Indices } query_parameters: { /** diff --git a/specification/indices/remove_block/IndicesRemoveBlockRequest.ts b/specification/indices/remove_block/IndicesRemoveBlockRequest.ts index 8f990fbb5f..9173bd1f70 100644 --- a/specification/indices/remove_block/IndicesRemoveBlockRequest.ts +++ b/specification/indices/remove_block/IndicesRemoveBlockRequest.ts @@ -18,7 +18,7 @@ */ import { RequestBase } from '@_types/Base' -import { ExpandWildcards, IndexName } from '@_types/common' +import { ExpandWildcards, Indices } from '@_types/common' import { Duration } from '@_types/Time' import { IndicesBlockOptions } from '@indices/_types/IndexSettings' @@ -47,7 +47,7 @@ export interface Request extends RequestBase { * To allow the removal of blocks from indices with `_all`, `*`, or other wildcard expressions, change the `action.destructive_requires_name` setting to `false`. * You can update this setting in the `elasticsearch.yml` file or by using the cluster update settings API. */ - index: IndexName + index: Indices /** * The block type to remove from the index. */ diff --git a/specification/security/clear_cached_privileges/SecurityClearCachedPrivilegesRequest.ts b/specification/security/clear_cached_privileges/SecurityClearCachedPrivilegesRequest.ts index 8af7ee807d..fbf96c1eb2 100644 --- a/specification/security/clear_cached_privileges/SecurityClearCachedPrivilegesRequest.ts +++ b/specification/security/clear_cached_privileges/SecurityClearCachedPrivilegesRequest.ts @@ -18,7 +18,7 @@ */ import { RequestBase } from '@_types/Base' -import { Name } from '@_types/common' +import { Names } from '@_types/common' /** * Clear the privileges cache. @@ -44,6 +44,6 @@ export interface Request extends RequestBase { * To clear all applications, use an asterism (`*`). * It does not support other wildcard patterns. */ - application: Name + application: Names } } diff --git a/specification/snapshot/delete/SnapshotDeleteRequest.ts b/specification/snapshot/delete/SnapshotDeleteRequest.ts index 299dbf6fae..eae6112d9a 100644 --- a/specification/snapshot/delete/SnapshotDeleteRequest.ts +++ b/specification/snapshot/delete/SnapshotDeleteRequest.ts @@ -18,7 +18,7 @@ */ import { RequestBase } from '@_types/Base' -import { Name } from '@_types/common' +import { Name, Names } from '@_types/common' import { Duration } from '@_types/Time' /** @@ -45,7 +45,7 @@ export interface Request extends RequestBase { * A comma-separated list of snapshot names to delete. * It also accepts wildcards (`*`). */ - snapshot: Name + snapshot: Names } query_parameters: { /** diff --git a/specification/text_structure/find_field_structure/FindFieldStructureRequest.ts b/specification/text_structure/find_field_structure/FindFieldStructureRequest.ts index 7a8e56beaf..cdc3de012f 100644 --- a/specification/text_structure/find_field_structure/FindFieldStructureRequest.ts +++ b/specification/text_structure/find_field_structure/FindFieldStructureRequest.ts @@ -18,7 +18,7 @@ */ import { RequestBase } from '@_types/Base' -import { Field, GrokPattern, IndexName } from '@_types/common' +import { Field, GrokPattern, IndexName, Names } from '@_types/common' import { uint } from '@_types/Numeric' import { Duration } from '@_types/Time' import { EcsCompatibilityType, FormatType } from '../_types/Structure' @@ -60,7 +60,7 @@ interface Request extends RequestBase { * If this parameter is not specified, the structure finder uses the column names from the header row of the text. * If the text does not have a header row, columns are named "column1", "column2", "column3", for example. */ - column_names?: string + column_names?: Names /** * If you have set `format` to `delimited`, you can specify the character used to delimit the values in each row. * Only a single character is supported; the delimiter cannot have multiple characters. diff --git a/specification/text_structure/find_message_structure/FindMessageStructureRequest.ts b/specification/text_structure/find_message_structure/FindMessageStructureRequest.ts index 8310002878..aa78fcf897 100644 --- a/specification/text_structure/find_message_structure/FindMessageStructureRequest.ts +++ b/specification/text_structure/find_message_structure/FindMessageStructureRequest.ts @@ -18,7 +18,7 @@ */ import { RequestBase } from '@_types/Base' -import { Field, GrokPattern } from '@_types/common' +import { Field, GrokPattern, Names } from '@_types/common' import { Duration } from '@_types/Time' import { EcsCompatibilityType, FormatType } from '../_types/Structure' @@ -59,7 +59,7 @@ interface Request extends RequestBase { * If this parameter is not specified, the structure finder uses the column names from the header row of the text. * If the text does not have a header role, columns are named "column1", "column2", "column3", for example. */ - column_names?: string + column_names?: Names /** * If you the format is `delimited`, you can specify the character used to delimit the values in each row. * Only a single character is supported; the delimiter cannot have multiple characters. From 8dd0dd1c0ad4aae810d7e7217d4a554be778cb78 Mon Sep 17 00:00:00 2001 From: Quentin Pradet Date: Wed, 5 Nov 2025 07:52:36 +0400 Subject: [PATCH 2/7] Fix cluster.get_component_template --- .../ClusterGetComponentTemplateRequest.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/specification/cluster/get_component_template/ClusterGetComponentTemplateRequest.ts b/specification/cluster/get_component_template/ClusterGetComponentTemplateRequest.ts index 170a9cf2cc..4cb810c3d7 100644 --- a/specification/cluster/get_component_template/ClusterGetComponentTemplateRequest.ts +++ b/specification/cluster/get_component_template/ClusterGetComponentTemplateRequest.ts @@ -18,7 +18,7 @@ */ import { RequestBase } from '@_types/Base' -import { Names } from '@_types/common' +import { Name } from '@_types/common' import { Duration } from '@_types/Time' /** @@ -47,7 +47,7 @@ export interface Request extends RequestBase { * Comma-separated list of component template names used to limit the request. * Wildcard (`*`) expressions are supported. */ - name?: Names + name?: Name } query_parameters: { /** From 9ec44a131a30e4b3b38371315cd799bc7f3443e7 Mon Sep 17 00:00:00 2001 From: Quentin Pradet Date: Wed, 5 Nov 2025 07:53:26 +0400 Subject: [PATCH 3/7] Run make contrib --- output/openapi/elasticsearch-openapi.json | 2 +- output/openapi/elasticsearch-serverless-openapi.json | 2 +- output/schema/schema.json | 2 +- output/typescript/types.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/output/openapi/elasticsearch-openapi.json b/output/openapi/elasticsearch-openapi.json index 2bf307a356..03d02c97b5 100644 --- a/output/openapi/elasticsearch-openapi.json +++ b/output/openapi/elasticsearch-openapi.json @@ -137931,7 +137931,7 @@ "required": true, "deprecated": false, "schema": { - "$ref": "#/components/schemas/_types.Names" + "$ref": "#/components/schemas/_types.Name" }, "style": "simple" }, diff --git a/output/openapi/elasticsearch-serverless-openapi.json b/output/openapi/elasticsearch-serverless-openapi.json index 3676b0a1a8..0052145b7c 100644 --- a/output/openapi/elasticsearch-serverless-openapi.json +++ b/output/openapi/elasticsearch-serverless-openapi.json @@ -84518,7 +84518,7 @@ "required": true, "deprecated": false, "schema": { - "$ref": "#/components/schemas/_types.Names" + "$ref": "#/components/schemas/_types.Name" }, "style": "simple" }, diff --git a/output/schema/schema.json b/output/schema/schema.json index 49d8024d7c..e02a577f46 100644 --- a/output/schema/schema.json +++ b/output/schema/schema.json @@ -125908,7 +125908,7 @@ "type": { "kind": "instance_of", "type": { - "name": "Names", + "name": "Name", "namespace": "_types" } } diff --git a/output/typescript/types.ts b/output/typescript/types.ts index b812f96684..3ed88ed014 100644 --- a/output/typescript/types.ts +++ b/output/typescript/types.ts @@ -9693,7 +9693,7 @@ export interface ClusterExistsComponentTemplateRequest extends RequestBase { export type ClusterExistsComponentTemplateResponse = boolean export interface ClusterGetComponentTemplateRequest extends RequestBase { - name?: Names + name?: Name flat_settings?: boolean settings_filter?: string | string[] include_defaults?: boolean From da2ef964ae4cebd39f99e706181f77d7f046044d Mon Sep 17 00:00:00 2001 From: Quentin Pradet Date: Wed, 5 Nov 2025 12:06:06 +0400 Subject: [PATCH 4/7] Stop relying on GitHub cache to validate APIs --- .github/validate-pr/compare-reports.js | 155 +++++++++++++++++++ .github/validate-pr/index.js | 205 ------------------------- .github/validate-pr/run-validation.js | 115 ++++++++++++++ .github/workflows/validate-apis.yml | 106 +++++++------ 4 files changed, 331 insertions(+), 250 deletions(-) create mode 100644 .github/validate-pr/compare-reports.js delete mode 100644 .github/validate-pr/index.js create mode 100644 .github/validate-pr/run-validation.js diff --git a/.github/validate-pr/compare-reports.js b/.github/validate-pr/compare-reports.js new file mode 100644 index 0000000000..09b9165a9a --- /dev/null +++ b/.github/validate-pr/compare-reports.js @@ -0,0 +1,155 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { readFile } from 'fs/promises' +import * as core from '@actions/core' + +const tick = '`' + +async function run() { + // Parse command line arguments + const [baselineReportPath, prReportPath] = process.argv.slice(2) + + if (!baselineReportPath || !prReportPath) { + console.error('Usage: node compare-reports.js ') + process.exit(1) + } + + // Load both validation reports + const baselineReports = JSON.parse(await readFile(baselineReportPath, 'utf-8')) + const prReports = JSON.parse(await readFile(prReportPath, 'utf-8')) + + // Compare reports and find changes + const changedApis = [] + + // Get all API names from both reports + const allApiNames = new Set([ + ...Object.keys(baselineReports), + ...Object.keys(prReports) + ]) + + for (const apiName of allApiNames) { + const baselineReport = baselineReports[apiName] + const prReport = prReports[apiName] + + // If API exists in PR but not baseline, or vice versa, or has changes + if (hasChanges(baselineReport, prReport)) { + changedApis.push({ + api: apiName, + baseline: baselineReport || null, + current: prReport || null + }) + } + } + + changedApis.sort((a, b) => a.api.localeCompare(b.api)) + + // Build PR comment + let comment = `Following you can find the validation changes against the target branch for the API${changedApis.length === 1 ? '' : 's'}.\n\n` + if (changedApis.length > 0) { + comment += '| API | Status | Request | Response |\n' + comment += '| --- | --- | --- | --- |\n' + for (const change of changedApis) { + comment += buildDiffTableLine(change) + } + } else { + comment += '**No changes detected**.\n' + } + comment += `\nYou can validate ${changedApis.length === 1 ? 'this' : 'these'} API${changedApis.length === 1 ? '' : 's'} yourself by using the ${tick}make validate${tick} target.\n` + + core.setOutput('comment_body', comment) + core.info('Done!') +} + +function hasChanges(baselineReport, prReport) { + // If one exists and the other doesn't, that's a change + if (!baselineReport && prReport) return true + if (baselineReport && !prReport) return true + if (!baselineReport && !prReport) return false + + return baselineReport.status !== prReport.status || + baselineReport.passingRequest !== prReport.passingRequest || + baselineReport.passingResponse !== prReport.passingResponse +} + +function buildDiffTableLine(change) { + const { api, baseline, current } = change + + // Handle cases where API exists in only one report + if (!baseline && current) { + // New API in PR + const status = generateStatus(current.status) + const request = generateRequest(current) + const response = generateResponse(current) + return `| ${tick}${api}${tick} | ➕ ${status} | ${request} | ${response} |\n` + } + + if (baseline && !current) { + // API removed in PR + const status = generateStatus(baseline.status) + const request = generateRequest(baseline) + const response = generateResponse(baseline) + return `| ${tick}${api}${tick} | ➖ ${status} | ${request} | ${response} |\n` + } + + // Both exist - show changes + const status = generateStatus(current.status) + const request = generateRequest(current) + const response = generateResponse(current) + + const baselineStatus = generateStatus(baseline.status) + const baselineRequest = generateRequest(baseline) + const baselineResponse = generateResponse(baseline) + + const statusDiff = status !== baselineStatus ? `${baselineStatus} → ${status}` : status + const requestDiff = request !== baselineRequest ? `${baselineRequest} → ${request}` : request + const responseDiff = response !== baselineResponse ? `${baselineResponse} → ${response}` : response + + return `| ${tick}${api}${tick} | ${statusDiff} | ${requestDiff} | ${responseDiff} |\n` +} + +function generateStatus (status) { + if (status === 'missing_types' || status === 'missing_request_type' || status === 'missing_response_type') { + return ':orange_circle:' + } + if (status === 'missing_test') { + return ':white_circle:' + } + if (status === 'passing') { + return ':green_circle:' + } + return ':red_circle:' +} + +function generateRequest (r) { + if (r.status === 'missing_test') return 'Missing test' + if (r.status === 'missing_types' || r.status == 'missing_request_type') return 'Missing type' + return `${r.passingRequest}/${r.totalRequest}` +} + +function generateResponse (r) { + if (r.status === 'missing_test') return 'Missing test' + if (r.status === 'missing_types' || r.status == 'missing_response_type') return 'Missing type' + return `${r.passingResponse}/${r.totalResponse}` +} + +run().catch((err) => { + core.error(err) + process.exit(1) +}) \ No newline at end of file diff --git a/.github/validate-pr/index.js b/.github/validate-pr/index.js deleted file mode 100644 index 1ce3dd5562..0000000000 --- a/.github/validate-pr/index.js +++ /dev/null @@ -1,205 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -/* global argv, path, cd */ - -import { dirname } from 'path' -import { fileURLToPath } from 'url' -import 'zx/globals' -import * as core from '@actions/core' -import { copyFile } from 'fs/promises' -import specification from '../../output/schema/schema.json' with { type: 'json' } -import baselineValidation from '../../../clients-flight-recorder/recordings/types-validation/types-validation.json' with { type: 'json' } -import { run as getReport } from '../../../clients-flight-recorder/scripts/types-validator/index.js' -import { - getNamespace, - getName -} from '../../../clients-flight-recorder/scripts/types-validator/utils.js' - -const __dirname = dirname(fileURLToPath(import.meta.url)) - - -const privateNames = ['_global'] -const tick = '`' -const tsValidationPath = path.join( - __dirname, - '..', - '..', - '..', - 'clients-flight-recorder', - 'scripts', - 'types-validator' -) - -async function run() { - await copyFile( - path.join(__dirname, '..', '..', 'output', 'typescript', 'types.ts'), - path.join(tsValidationPath, 'types.ts') - ) - const reports = new Map() - - - // Collect all APIs to validate - const apisToValidate = new Set() - - cd(path.join(__dirname, '..', '..')) - for (const file of await glob('specification/**/*.ts')) { - if (file.startsWith('specification/_types')) continue - if (file.startsWith('specification/node_modules')) continue - if (getApi(file).endsWith('_types')) { - const apis = specification.endpoints - .filter(endpoint => endpoint.name.split('.').filter(s => !privateNames.includes(s))[0] === getApi(file).split('.')[0]) - .map(endpoint => endpoint.name) - for (const api of apis) { - apisToValidate.add(api) - } - } else { - const api = getApi(file) - apisToValidate.add(api) - } - } - - cd(tsValidationPath) - console.log(`Validating ${apisToValidate.size} APIs...`) - - // Call getReport once with all APIs - const allApis = Array.from(apisToValidate).join(',') - const report = await getReport({ - api: allApis, - 'generate-report': false, - request: true, - response: true, - ci: false, - verbose: false - }) - - // Extract individual API reports from the combined result - for (const api of apisToValidate) { - const namespace = getNamespace(api) - if (report.has(namespace)) { - const namespaceReport = report.get(namespace).find(r => r.api === getName(api)) - if (namespaceReport) { - reports.set(api, namespaceReport) - } - } - } - - cd(path.join(__dirname, '..', '..')) - - // Compare current reports with baseline and find changes - const changedApis = [] - for (const [apiName, report] of reports) { - const baselineReport = findBaselineReport(apiName, baselineValidation) - if (baselineReport && hasChanges(baselineReport, report, apiName)) { - changedApis.push({ api: apiName, baseline: baselineReport, current: report }) - } - } - changedApis.sort((a, b) => a.api.localeCompare(b.api)) - - let comment = `Following you can find the validation changes against the target branch for the API${changedApis.length === 1 ? '' : 's'}.\n\n` - if (changedApis.length > 0) { - comment += '| API | Status | Request | Response |\n' - comment += '| --- | --- | --- | --- |\n' - for (const change of changedApis) { - comment += buildDiffTableLine(change) - } - } else { - comment += '**No changes detected**.\n' - } - comment += `\nYou can validate ${changedApis.length === 1 ? 'this' : 'these'} API${changedApis.length === 1 ? '' : 's'} yourself by using the ${tick}make validate${tick} target.\n` - core.setOutput('comment_body', comment) - - core.info('Done!') -} - -function getApi (file) { - return file.split('/').slice(1, 3).filter(s => !privateNames.includes(s)).filter(Boolean).join('.') -} - -function findBaselineReport(apiName, baselineValidation) { - let namespace, method = [null, null] - if (!apiName.includes('.')) { - [namespace, method] = ['global', apiName] - } else { - [namespace, method] = apiName.split('.') - } - - if (!baselineValidation.namespaces[namespace]) { - return null - } - - return baselineValidation.namespaces[namespace].apis.find(api => api.api === method) -} - -function hasChanges(baselineReport, report) { - if (!report) return false - - return baselineReport.status !== report.status || - baselineReport.passingRequest !== report.passingRequest || - baselineReport.passingResponse !== report.passingResponse -} - -function buildDiffTableLine(change) { - const { api, baseline, current } = change - - const status = generateStatus(current.status) - const request = generateRequest(current) - const response = generateResponse(current) - - const baselineStatus = generateStatus(baseline.status) - const baselineRequest = generateRequest(baseline) - const baselineResponse = generateResponse(baseline) - - const statusDiff = status !== baselineStatus ? `${baselineStatus} → ${status}` : status - const requestDiff = request !== baselineRequest ? `${baselineRequest} → ${request}` : request - const responseDiff = response !== baselineResponse ? `${baselineResponse} → ${response}` : response - - return `| ${tick}${api}${tick} | ${statusDiff} | ${requestDiff} | ${responseDiff} |\n` -} - - -function generateStatus (status) { - if (status === 'missing_types' || status === 'missing_request_type' || status === 'missing_response_type') { - return ':orange_circle:' - } - if (status === 'missing_test') { - return ':white_circle:' - } - if (status === 'passing') { - return ':green_circle:' - } - return ':red_circle:' -} - -function generateRequest (r) { - if (r.status === 'missing_test') return 'Missing test' - if (r.status === 'missing_types' || r.status == 'missing_request_type') return 'Missing type' - return `${r.passingRequest}/${r.totalRequest}` -} - -function generateResponse (r) { - if (r.status === 'missing_test') return 'Missing test' - if (r.status === 'missing_types' || r.status == 'missing_response_type') return 'Missing type' - return `${r.passingResponse}/${r.totalResponse}` -} - -run().catch((err) => { - core.error(err) - process.exit(1) -}) diff --git a/.github/validate-pr/run-validation.js b/.github/validate-pr/run-validation.js new file mode 100644 index 0000000000..d0cce8d4e5 --- /dev/null +++ b/.github/validate-pr/run-validation.js @@ -0,0 +1,115 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/* global argv, path, cd */ + +import { dirname } from 'path' +import { fileURLToPath } from 'url' +import 'zx/globals' +import { copyFile, writeFile } from 'fs/promises' +import specification from '../../output/schema/schema.json' with { type: 'json' } +import { run as getReport } from '../../../clients-flight-recorder/scripts/types-validator/index.js' +import { + getNamespace, + getName +} from '../../../clients-flight-recorder/scripts/types-validator/utils.js' + +const __dirname = dirname(fileURLToPath(import.meta.url)) + +const privateNames = ['_global'] +const tsValidationPath = path.join( + __dirname, + '..', + '..', + '..', + 'clients-flight-recorder', + 'scripts', + 'types-validator' +) + +async function run() { + await copyFile( + path.join(__dirname, '..', '..', 'output', 'typescript', 'types.ts'), + path.join(tsValidationPath, 'types.ts') + ) + const reports = new Map() + + // Collect all APIs to validate + const apisToValidate = new Set() + + cd(path.join(__dirname, '..', '..')) + for (const file of await glob('specification/**/*.ts')) { + if (file.startsWith('specification/_types')) continue + if (file.startsWith('specification/node_modules')) continue + if (getApi(file).endsWith('_types')) { + const apis = specification.endpoints + .filter(endpoint => endpoint.name.split('.').filter(s => !privateNames.includes(s))[0] === getApi(file).split('.')[0]) + .map(endpoint => endpoint.name) + for (const api of apis) { + apisToValidate.add(api) + } + } else { + const api = getApi(file) + apisToValidate.add(api) + } + } + + cd(tsValidationPath) + console.log(`Validating ${apisToValidate.size} APIs...`) + + // Call getReport once with all APIs + const allApis = Array.from(apisToValidate).join(',') + const report = await getReport({ + api: allApis, + 'generate-report': false, + request: true, + response: true, + ci: false, + verbose: false + }) + + // Extract individual API reports from the combined result + for (const api of apisToValidate) { + const namespace = getNamespace(api) + if (report.has(namespace)) { + const namespaceReport = report.get(namespace).find(r => r.api === getName(api)) + if (namespaceReport) { + reports.set(api, namespaceReport) + } + } + } + + // Save reports to JSON file + const reportsObject = Object.fromEntries(reports) + await writeFile( + path.join(__dirname, 'validation-report.json'), + JSON.stringify(reportsObject, null, 2) + ) + + console.log(`Validation complete. Report saved to validation-report.json`) +} + +function getApi (file) { + return file.split('/').slice(1, 3).filter(s => !privateNames.includes(s)).filter(Boolean).join('.') +} + +run().catch((err) => { + console.error(err) + process.exit(1) +}) \ No newline at end of file diff --git a/.github/workflows/validate-apis.yml b/.github/workflows/validate-apis.yml index f4ae6a4dd9..5c27b13610 100644 --- a/.github/workflows/validate-apis.yml +++ b/.github/workflows/validate-apis.yml @@ -6,30 +6,36 @@ on: branches: - main - '[0-9]+.[0-9]+' - push: - branches: - - main - - '[0-9]+.[0-9]+' permissions: pull-requests: write # To create/update PR comments jobs: - validate-apis: - name: build + generate-validation: + name: Generate ${{ matrix.type }} validation runs-on: ubuntu-latest + strategy: + matrix: + include: + - type: baseline + spec_ref: ${{ github.event.pull_request.base.sha }} + - type: pr + spec_ref: ${{ github.sha }} steps: - name: Check pull request was opened from branch - if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != 'elastic/elasticsearch-specification' + if: github.event.pull_request.head.repo.full_name != 'elastic/elasticsearch-specification' run: echo "Validation is not supported from forks"; exit 1 - - - uses: actions/checkout@v5 + + - name: Checkout elasticsearch-specification + uses: actions/checkout@v5 with: path: ./elasticsearch-specification + ref: ${{ matrix.spec_ref }} persist-credentials: false - - uses: actions/checkout@v5 + - name: Checkout clients-flight-recorder + uses: actions/checkout@v5 with: repository: elastic/clients-flight-recorder path: ./clients-flight-recorder @@ -42,15 +48,6 @@ jobs: with: node-version: 22 - - name: Restore JSON report - if: github.event_name == 'pull_request' - uses: actions/cache/restore@v4 - with: - path: ./clients-flight-recorder/recordings/types-validation/types-validation.json - key: types-validation-json-${{ format('{0}-{1}', github.base_ref, github.event.pull_request.base.sha) }} - restore-keys: | - types-validation-json-${{ github.base_ref }}- - - name: Install deps 1/2 working-directory: ./clients-flight-recorder run: | @@ -64,45 +61,65 @@ jobs: npm install --prefix .github/validate-pr make setup - - name: Generate specification and check generated types + - name: Generate specification working-directory: ./elasticsearch-specification - run: | - make contrib + run: make contrib - name: Download artifacts working-directory: ./clients-flight-recorder run: | - if [[ -n "${GITHUB_BASE_REF:-}" ]]; then - branch=$GITHUB_BASE_REF - else - branch=$GITHUB_REF_NAME - fi + branch=${{ github.base_ref }} echo "Using branch: $branch" node scripts/upload-recording/download.js --branch $branch --git node scripts/clone-elasticsearch/index.js --branch $branch - - name: Run validation (Push) - if: github.event_name == 'push' - working-directory: ./clients-flight-recorder/scripts/types-validator - env: - BRANCH: ${{ github.ref_name }} - run: node index.js --generate-report --ci --branch $BRANCH + - name: Run validation + working-directory: ./elasticsearch-specification + run: node .github/validate-pr/run-validation.js - - name: Save JSON report - if: github.event_name == 'push' - uses: actions/cache/save@v4 + - name: Upload validation report + uses: actions/upload-artifact@v4 with: - path: ./clients-flight-recorder/recordings/types-validation/types-validation.json - key: types-validation-json-${{ format('{0}-{1}', github.ref_name, github.sha) }} + name: ${{ matrix.type }}-validation-report + path: ./elasticsearch-specification/.github/validate-pr/validation-report.json + retention-days: 1 + + compare-and-comment: + name: Compare and comment + runs-on: ubuntu-latest + needs: generate-validation - - name: Run validation (PR) - if: github.event_name == 'pull_request' + steps: + - name: Checkout elasticsearch-specification (for comparison script) + uses: actions/checkout@v5 + with: + persist-credentials: false + + - name: Use Node.js 22 + uses: actions/setup-node@v5 + with: + node-version: 22 + + - name: Install dependencies + run: npm install --prefix .github/validate-pr + + - name: Download baseline validation report + uses: actions/download-artifact@v4 + with: + name: baseline-validation-report + path: ./reports/baseline + + - name: Download PR validation report + uses: actions/download-artifact@v4 + with: + name: pr-validation-report + path: ./reports/pr + + - name: Run comparison id: validation - working-directory: ./elasticsearch-specification - run: node .github/validate-pr + run: node .github/validate-pr/compare-reports.js ./reports/baseline/validation-report.json ./reports/pr/validation-report.json - name: Find existing comment - if: github.event_name == 'pull_request' uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3.1.0 id: find-comment with: @@ -111,11 +128,10 @@ jobs: body-includes: 'Following you can find the validation changes' - name: Create or update comment - if: github.event_name == 'pull_request' uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0 with: token: ${{ secrets.GITHUB_TOKEN }} comment-id: ${{ steps.find-comment.outputs.comment-id }} issue-number: ${{ github.event.pull_request.number }} body: ${{ steps.validation.outputs.comment_body }} - edit-mode: replace + edit-mode: replace \ No newline at end of file From aa28d9b40b07d95dbb53b9af428f7afad865d047 Mon Sep 17 00:00:00 2001 From: Quentin Pradet Date: Thu, 6 Nov 2025 11:12:27 +0400 Subject: [PATCH 5/7] Use string | string[] instead of Names --- .../find_field_structure/FindFieldStructureRequest.ts | 2 +- .../find_message_structure/FindMessageStructureRequest.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/specification/text_structure/find_field_structure/FindFieldStructureRequest.ts b/specification/text_structure/find_field_structure/FindFieldStructureRequest.ts index cdc3de012f..4f9905b52b 100644 --- a/specification/text_structure/find_field_structure/FindFieldStructureRequest.ts +++ b/specification/text_structure/find_field_structure/FindFieldStructureRequest.ts @@ -60,7 +60,7 @@ interface Request extends RequestBase { * If this parameter is not specified, the structure finder uses the column names from the header row of the text. * If the text does not have a header row, columns are named "column1", "column2", "column3", for example. */ - column_names?: Names + column_names?: string | string[] /** * If you have set `format` to `delimited`, you can specify the character used to delimit the values in each row. * Only a single character is supported; the delimiter cannot have multiple characters. diff --git a/specification/text_structure/find_message_structure/FindMessageStructureRequest.ts b/specification/text_structure/find_message_structure/FindMessageStructureRequest.ts index aa78fcf897..4bd892b685 100644 --- a/specification/text_structure/find_message_structure/FindMessageStructureRequest.ts +++ b/specification/text_structure/find_message_structure/FindMessageStructureRequest.ts @@ -59,7 +59,7 @@ interface Request extends RequestBase { * If this parameter is not specified, the structure finder uses the column names from the header row of the text. * If the text does not have a header role, columns are named "column1", "column2", "column3", for example. */ - column_names?: Names + column_names?: string | string[] /** * If you the format is `delimited`, you can specify the character used to delimit the values in each row. * Only a single character is supported; the delimiter cannot have multiple characters. From 61c8a7284349878177ae52d9e610a39ac2ccab76 Mon Sep 17 00:00:00 2001 From: Quentin Pradet Date: Thu, 6 Nov 2025 11:31:34 +0400 Subject: [PATCH 6/7] Run prettier --- .../find_field_structure/FindFieldStructureRequest.ts | 2 +- .../find_message_structure/FindMessageStructureRequest.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/specification/text_structure/find_field_structure/FindFieldStructureRequest.ts b/specification/text_structure/find_field_structure/FindFieldStructureRequest.ts index 4f9905b52b..9d72a59c09 100644 --- a/specification/text_structure/find_field_structure/FindFieldStructureRequest.ts +++ b/specification/text_structure/find_field_structure/FindFieldStructureRequest.ts @@ -18,7 +18,7 @@ */ import { RequestBase } from '@_types/Base' -import { Field, GrokPattern, IndexName, Names } from '@_types/common' +import { Field, GrokPattern, IndexName } from '@_types/common' import { uint } from '@_types/Numeric' import { Duration } from '@_types/Time' import { EcsCompatibilityType, FormatType } from '../_types/Structure' diff --git a/specification/text_structure/find_message_structure/FindMessageStructureRequest.ts b/specification/text_structure/find_message_structure/FindMessageStructureRequest.ts index 4bd892b685..ecb109ccf4 100644 --- a/specification/text_structure/find_message_structure/FindMessageStructureRequest.ts +++ b/specification/text_structure/find_message_structure/FindMessageStructureRequest.ts @@ -18,7 +18,7 @@ */ import { RequestBase } from '@_types/Base' -import { Field, GrokPattern, Names } from '@_types/common' +import { Field, GrokPattern } from '@_types/common' import { Duration } from '@_types/Time' import { EcsCompatibilityType, FormatType } from '../_types/Structure' From 95566e3f1d937e138ce21e76d07cc8f525e1c3a3 Mon Sep 17 00:00:00 2001 From: Quentin Pradet Date: Thu, 6 Nov 2025 17:46:29 +0400 Subject: [PATCH 7/7] Fix terms_enum too --- specification/_global/terms_enum/TermsEnumRequest.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/specification/_global/terms_enum/TermsEnumRequest.ts b/specification/_global/terms_enum/TermsEnumRequest.ts index c2c8dbc10d..f63332be68 100644 --- a/specification/_global/terms_enum/TermsEnumRequest.ts +++ b/specification/_global/terms_enum/TermsEnumRequest.ts @@ -18,7 +18,7 @@ */ import { RequestBase } from '@_types/Base' -import { Field, IndexName } from '@_types/common' +import { Field, Indices } from '@_types/common' import { integer } from '@_types/Numeric' import { QueryContainer } from '@_types/query_dsl/abstractions' import { Duration } from '@_types/Time' @@ -50,7 +50,7 @@ export interface Request extends RequestBase { * Wildcard (`*`) expressions are supported. * To search all data streams or indices, omit this parameter or use `*` or `_all`. */ - index: IndexName + index: Indices } body: { /** The string to match at the start of indexed terms. If not provided, all terms in the field are considered. */