mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-11-01 04:02:35 +00:00
The public Managed Agent contract's info.version is allocated by hand on every contract-changing pull request, and nothing checked it: on 2026-10-09 four such pull requests were in flight with 1.34.0 claimed twice while main was already at 1.35.0, and the stall had reached main once before — #13163 changed the document with no version movement. A duplicate Flyway version refuses to boot within minutes, but a regressed or stalled contract version breaks nothing at runtime; it silently misinforms every consumer that keys capability detection on it. On every pull request the Flyway lane now also compares the merge result's document with the base branch's head copy — fetched at depth one, since the default checkout is one commit deep — and fails when a changed document declares an equal or lower version, compared numerically per segment so 1.9.0 ranks below 1.37.0, naming whether the number stalled or regressed; a pull request arriving at the base ref's own bytes owes no bump. Each refusal arm has a test that fails when its guard is removed, and any path echoed into an ::error:: line is bound to the same byte alphabet check-flyway-migrations.js applies, so a hostile name cannot forge a second error line into a decoded log. After every push to main that changes the document, and every 30 minutes, a new workflow re-checks each open pull request's changed paths and document through the API — no pull request code is fetched, checked out or run — and sets the 'API contract version uniqueness (latest main)' status, the #13770 shape for the version number instead of the migrations: failure when the declared version no longer advances past main's, success otherwise, naming the other open pull requests claiming the same version, and error when a file list or head document could not be read in full. A head that already carries the status is re-evaluated even after it stops changing the document, so a stale failure clears. Both scripts ran live before this commit: the pull-request check over four merge shapes (stall, regression, advance, untouched) against a depth-one clone and filtered base fetch, and the re-check as a dry run against the open queue, whose six contract-changing pull requests it correctly read as stalled or regressed against main at 1.38.0. Fixes #13804
122 lines
4.6 KiB
JavaScript
122 lines
4.6 KiB
JavaScript
/**
|
|
* @license
|
|
* Copyright 2026 Qwen Team
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
// Every pull request that changes the public Managed Agent contract bumps
|
|
// its info.version by hand, and until #13804 nothing checked that number:
|
|
// on 2026-10-09 two in-flight pull requests declared 1.34.0 while main was
|
|
// already at 1.35.0, and the stall had reached main once before — #13163
|
|
// changed the document with no version movement at all. A duplicate Flyway
|
|
// version refuses to boot within minutes, but a regressed or stalled
|
|
// contract version breaks nothing at runtime; it silently misinforms every
|
|
// consumer that keys capability detection on it. This is the PR-time check
|
|
// of #13804, run by the SDK Java workflow's fast guard lane on every pull
|
|
// request: given the base ref's copy of the document and the merge
|
|
// result's copy, it fails when a CHANGED document declares a version equal
|
|
// to or below the base ref's, naming which of the two modes it is. The
|
|
// comparison keys on the documents' contents, not a changed-paths list, so
|
|
// a merge result whose copy already advanced (another pull request landed)
|
|
// reads as regressed-or-stalled instead of passing on a stale branch
|
|
// point; the version comparison itself is numeric per segment, so 1.9.0
|
|
// ranks below 1.37.0.
|
|
|
|
import { existsSync, readFileSync } from 'node:fs';
|
|
import { compareVersions, documentVersion } from './contract-version-utils.js';
|
|
import { escapeWorkflowCommand } from './release-script-utils.js';
|
|
|
|
// The same binding check-flyway-migrations.js applies: a `::error::`
|
|
// command's data is runner-DECODED when the downloadable log is rendered,
|
|
// so nothing outside a path's real alphabet may reach it — every other
|
|
// byte becomes %XX first, with the shared helper's escape on top, so no
|
|
// decode pass can turn the text back into control bytes. A path reaching
|
|
// this script through its command line could otherwise smuggle a forged
|
|
// second `::error::` line into the log.
|
|
const boundWorkflowPath = (text) =>
|
|
escapeWorkflowCommand(
|
|
String(text).replace(
|
|
/[^A-Za-z0-9._/-]/g,
|
|
(char) =>
|
|
`%${char.codePointAt(0).toString(16).toUpperCase().padStart(2, '0')}`,
|
|
),
|
|
);
|
|
|
|
const [baseFile, headFile, extra] = process.argv.slice(2);
|
|
if (!baseFile || !headFile || extra) {
|
|
console.error(
|
|
'usage: node scripts/check-contract-version.js <base-document> <head-document>',
|
|
);
|
|
process.exit(2);
|
|
}
|
|
|
|
const fail = (message) => {
|
|
console.error(`::error::${message}`);
|
|
process.exit(1);
|
|
};
|
|
|
|
const read = (file) =>
|
|
existsSync(file) ? readFileSync(file, 'utf8').trim() : '';
|
|
const base = read(baseFile);
|
|
const head = read(headFile);
|
|
|
|
// The head first: a renamed document empties both reads, and its message
|
|
// names what happened. A missing or empty base copy can only mean the
|
|
// fetch or the path itself rotted — answered as an error too, never a
|
|
// silent pass.
|
|
if (!head) {
|
|
fail(
|
|
`${boundWorkflowPath(headFile)}: the contract document is missing or ` +
|
|
'empty; if it moved, point this check at the new location',
|
|
);
|
|
}
|
|
if (!base) {
|
|
fail(
|
|
`${boundWorkflowPath(baseFile)}: could not read the base ref's copy ` +
|
|
'of the contract document; nothing to compare against',
|
|
);
|
|
}
|
|
|
|
// Same bytes, same version: the pull request changed nothing the version
|
|
// exists to signal, even when the changed-paths list names the document.
|
|
// This arm runs before either copy is parsed so a baseline anomaly never
|
|
// blocks a pull request that makes no claim on it.
|
|
if (head === base) {
|
|
console.log(
|
|
`${boundWorkflowPath(headFile)}: identical to the base ref's copy; no version check needed`,
|
|
);
|
|
process.exit(0);
|
|
}
|
|
|
|
const headRead = documentVersion(head);
|
|
if (!headRead.version) {
|
|
fail(
|
|
`${boundWorkflowPath(headFile)}: the contract document ${headRead.error}`,
|
|
);
|
|
}
|
|
const baseRead = documentVersion(base);
|
|
if (!baseRead.version) {
|
|
fail(`${boundWorkflowPath(baseFile)}: the base ref's copy ${baseRead.error}`);
|
|
}
|
|
|
|
const order = compareVersions(headRead.version, baseRead.version);
|
|
if (order > 0) {
|
|
console.log(
|
|
`${boundWorkflowPath(headFile)}: info.version ${headRead.version} advances past ` +
|
|
`the base ref's ${baseRead.version}`,
|
|
);
|
|
process.exit(0);
|
|
}
|
|
if (order === 0) {
|
|
fail(
|
|
`${boundWorkflowPath(headFile)} declares info.version ` +
|
|
`${headRead.version}, which does not advance past the base ref's ` +
|
|
`${baseRead.version} while the document changed; bump the version`,
|
|
);
|
|
}
|
|
fail(
|
|
`${boundWorkflowPath(headFile)} declares info.version ` +
|
|
`${headRead.version}, which regresses below the base ref's ` +
|
|
`${baseRead.version} while the document changed; bump info.version ` +
|
|
`past ${baseRead.version}`,
|
|
);
|