From ed2057e4a23edbe6319ae320800293abff52eb09 Mon Sep 17 00:00:00 2001 From: Bastiaan <17990055+bbassie@users.noreply.github.com> Date: Fri, 30 Oct 2026 20:57:48 +0100 Subject: [PATCH] Merge pull request #1149 from bbassie/feature/compat-ignore-control Ignore control in the version compat message check --- docs/contributing.md | 1 + scripts/version-compat-test.js | 33 ++++++++++++++++++++++++++------- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/docs/contributing.md b/docs/contributing.md index 48094a79..5a9adfdb 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -254,6 +254,7 @@ If there are no changelog entries to add, remove the code block and start the ch The version-compat job in CI runs the local controller with the latest published host, and the published controller with the local host. It fails if a link message changed its src, dst, data or response schema since that release. +Control is left out of src and dst, and messages that only go between control and the controller are not checked, as the web UI is served by the controller it talks to. If the break is intended, add an entry to [scripts/version-compat-breaks.json](../scripts/version-compat-breaks.json) in the same pull request. Breaking changes are only accepted if the next release is intended to include them, see our decision on [breaking changes](/docs/decisions.md#breaking-changes). The entry is keyed by the published version directly before the breaking change was introduced, which is the latest release at the time. diff --git a/scripts/version-compat-test.js b/scripts/version-compat-test.js index b300833e..e9e7aa32 100644 --- a/scripts/version-compat-test.js +++ b/scripts/version-compat-test.js @@ -12,7 +12,10 @@ * added and removed messages are only warned about since breaking * compatibility is unavoidable there. Optional properties that only one * version has are left out of the comparison, as the other version ignores - * them when received and does not need them when sent. + * them when received and does not need them when sent. Control clients are + * left out too, as the web UI is served by the controller it talks to, so + * control is removed from src and dst and messages that only go between + * control and the controller are skipped. * * Intended breaks are listed in scripts/version-compat-breaks.json, see * scripts/version-compat-breaks.js for the format. @@ -196,6 +199,15 @@ const dumpMessageSpecs = ` console.log(JSON.stringify(specs)); `; +function withoutControl(spec) { + const strip = types => types.filter(type => type !== "control"); + return { ...spec, src: strip(spec.src), dst: strip(spec.dst) }; +} + +function isControlOnly(spec) { + return [...spec.src, ...spec.dst].every(type => type === "control" || type === "controller"); +} + async function messageSpecs(libPath) { const result = await execFile(process.execPath, ["-e", dumpMessageSpecs, libPath]); return new Map(Object.entries(JSON.parse(result.stdout))); @@ -215,12 +227,19 @@ async function checkMessageCompat(publishedVersion, knownBreaks) { console.log(`| comparing ${localSpecs.size} local with ${publishedSpecs.size} published link messages`); const changed = []; - for (const [name, localSpec] of localSpecs) { - const publishedSpec = publishedSpecs.get(name); - if (!publishedSpec) { - annotate(`Link ${name} does not exist in ${publishedVersion} which cannot receive it`); + for (const [name, fullLocalSpec] of localSpecs) { + const fullPublishedSpec = publishedSpecs.get(name); + if (!fullPublishedSpec) { + if (!isControlOnly(fullLocalSpec)) { + annotate(`Link ${name} does not exist in ${publishedVersion} which cannot receive it`); + } continue; } + if (isControlOnly(fullLocalSpec) && isControlOnly(fullPublishedSpec)) { + continue; + } + const localSpec = withoutControl(fullLocalSpec); + const publishedSpec = withoutControl(fullPublishedSpec); const differs = Object.keys(localSpec).filter( field => stableStringify(localSpec[field]) !== stableStringify(publishedSpec[field]) ); @@ -238,8 +257,8 @@ async function checkMessageCompat(publishedVersion, knownBreaks) { changed.push({ name, fields, localSpec, publishedSpec }); } } - for (const name of publishedSpecs.keys()) { - if (!localSpecs.has(name)) { + for (const [name, publishedSpec] of publishedSpecs) { + if (!localSpecs.has(name) && !isControlOnly(publishedSpec)) { annotate(`Link ${name} was removed after ${publishedVersion} which may still send it`); } }