Files
qwen-code/scripts/check-flyway-migrations.js
1f39a80bf0 feat(managed-agent): consolidate persistence into a 32-table baseline (#14069)
* feat(managed-agent): consolidate persistence into a 32-table baseline

Preserve journal, idempotency, runtime, physical storage and recovery boundaries in a shared fresh-database schema. Add fixed snapshots, typed operations, scoped resource collection and atomic automation projections with MySQL fault and load coverage.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(managed-agent): restore schema CI and channel claim fencing

Avoid nested snapshot connection borrowing while preserving committed read-lease expiry and retirement checks. Carry reserved channel reconciliation identities into private journal commands and retain UNKNOWN no-resend behavior.

Update canonical-schema fixtures, migration discovery and application packaging. Preserve the 32-table fresh-database boundary and record local versus pending CI acceptance in both languages.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(managed-agent): refuse system catalogs before schema initialization

Co-authored-by: Qwen-Coder <[email protected]>

* fix(managed-agent): restore fresh-schema E2E and MariaDB validation

Port the real-stack runner and its audits to canonical tables, retain stream and approval scopes, and register fresh Linux storage before Harness admission.

Validate MariaDB CHECK enforcement and InnoDB without relying on MySQL-only metadata or negative-test syntax. Add regression coverage and document current platform and acceptance boundaries.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(managed-agent): normalize boolean CASE catalog literals

Accept MariaDB's numeric representation of bare SQL boolean literals while retaining structural CHECK validation, quoted values and enforcement guards.

Add alias and tamper regression coverage and synchronize the bilingual verification evidence.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(ci): raise managed schema MariaDB packet budget

Co-authored-by: Qwen-Coder <[email protected]>

* fix(managed-agent): support MariaDB load-test query plans

Co-authored-by: Qwen-Coder <[email protected]>

* fix(managed-agent): Restore native storage fixtures and const read witnesses

Preserve measured row counts while bounding MariaDB optimization-stage page reads, and register Linux Hosted test storage through the production guard before tool admission. Keep recovery registration across restarts and resolve physical owners through aliases.

Co-authored-by: Qwen-Coder <[email protected]>

* test(managed-agent): Await snapshot publication in retraction checks

Keep the strict terminal snapshot watermark assertion while allowing after-commit publication to finish under the concurrent materializer.

Co-authored-by: Qwen-Coder <[email protected]>

---------

Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: Shaojin Wen <[email protected]>
2026-10-14 00:58:38 +00:00

195 lines
7.7 KiB
JavaScript

/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
// managed-agent-server allocates Flyway versions by hand across two
// directories that share one sequence — src/main/resources/db/migration (SQL)
// and src/main/java/db/migration (BaseJavaMigration classes). On 2026-09-28
// two in-flight PRs each added a V16 and merged 23 minutes apart; each branch
// was green in isolation, and Flyway refused to boot the merged result
// ("Found more than one migration with version 16"), leaving main red for two
// hours (#12940). The test that catches a duplicate boots a database and runs
// only in the slow database-backed lanes, after the merge has landed. This
// check is the fast lane: it scans both locations and fails when two files
// claim the same version, naming them, in milliseconds and with no database.
import { existsSync, readdirSync } from 'node:fs';
import path from 'node:path';
import {
LOCATIONS,
isMigrationFile,
migrationFiles,
migrationVersion,
} from './flyway-migration-utils.js';
import { escapeWorkflowCommand } from './release-script-utils.js';
// A `::error::` command's data is runner-DECODED when the downloadable log
// is rendered, so escaping only CR/LF/% is not enough: the decode restores
// them exactly where the main-red consumer parses, and a directory or file
// name can carry a whole forged `::error::` line — a second flyway identity
// — or the `: N migrations claim version M: ` phrase that suppresses the
// genuine one from the same line. Nothing outside a migration path's real
// alphabet may reach the command data: 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 or pattern-shaped phrases.
const boundWorkflowPath = (text) =>
escapeWorkflowCommand(
String(text).replace(
/[^A-Za-z0-9._/-]/g,
(char) =>
`%${char.codePointAt(0).toString(16).toUpperCase().padStart(2, '0')}`,
),
);
const modules = [...new Set(process.argv.slice(2))];
if (modules.length === 0) {
console.error(
'usage: node scripts/check-flyway-migrations.js <maven-module-dir>...',
);
process.exit(2);
}
// Files shaped like a versioned migration of this location's kind, anywhere
// under the source root EXCEPT inside the configured location. A renamed
// location (db/migration → db/changelog) leaves the guard scanning an empty
// directory, so the renamed-away state must fail here — and so must a
// location whose files moved while its sibling stayed populated.
const outOfPlaceFiles = (module, location) => {
const sourceRoot = path.join(module, ...location.dir.slice(0, -2));
const configured = LOCATIONS.filter(
(entry) => entry.suffix === location.suffix,
).map((entry) => path.join(module, ...entry.dir));
const found = [];
const walk = (dir) => {
if (!existsSync(dir)) return;
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const file = path.join(dir, entry.name);
if (entry.isDirectory()) {
if (!configured.includes(file)) walk(file);
} else if (isMigrationFile(entry.name, location.suffix)) {
found.push(file);
}
}
};
walk(sourceRoot);
return found;
};
// A db/migration* sibling under the location's OWN db root — including an
// emptied rename such as db/migrations — marks the location as moved even
// when nothing migration-shaped survives anywhere. A module with no such
// directory at all simply has no sequence to check,
// which is what lets one invocation cover every module that shares the
// classpath namespace below.
const hasMigrationDir = (module, location) => {
const dbRoot = path.join(module, ...location.dir.slice(0, -1));
return (
existsSync(dbRoot) &&
readdirSync(dbRoot, { withFileTypes: true }).some(
(entry) =>
entry.isDirectory() && entry.name.startsWith(location.dir.at(-1)),
)
);
};
let failed = false;
// Flyway resolves classpath:db/migration across every jar on the classpath,
// and managed-agent-server depends on the other modules at compile scope, so
// the modules given in one invocation share one version namespace — a version
// claimed in two modules collides exactly like two files in one module do.
// Collisions are reported only after every module is scanned: one ::error::
// line per collided version names EVERY claimant (the consumer keys its issue
// on the module and version in this line, so a second line for the same
// version would file a second issue), and a claimant module never prints an
// "all versions unique" summary.
const claimants = new Map();
const scanned = [];
for (const module of modules) {
if (!existsSync(module)) {
failed = true;
console.error(
`::error::${boundWorkflowPath(module)}: no such Maven module directory`,
);
continue;
}
let errored = false;
let count = 0;
const probedRoots = new Set();
for (const location of LOCATIONS) {
const files = migrationFiles(
path.join(module, ...location.dir),
location.suffix,
);
// Out-of-place files must not join the claimants: invisible to Flyway,
// they cannot collide at runtime — they are the moved location's error,
// not a second claimant for their version.
const rootKey = `${location.dir.slice(0, -2).join('/')}:${location.suffix}`;
const misplaced = probedRoots.has(rootKey)
? []
: outOfPlaceFiles(module, location);
probedRoots.add(rootKey);
if (misplaced.length > 0 && files.length > 0) {
// The location IS populated, so nothing moved: a stray file sits
// outside the only place Flyway scans. Say that — claiming the
// location is empty would send the reader hunting a rename that never
// happened, and the consumer titles the issue from this line.
failed = true;
errored = true;
console.error(
`::error::${boundWorkflowPath(module)}: found migration files outside ` +
`${location.dir.join('/')}: ` +
`${misplaced.map(boundWorkflowPath).join(', ')}`,
);
} else if (
misplaced.length > 0 ||
// A location that EXISTS is not moved, however empty: the rename
// probe fires only when the location directory itself is gone and a
// migration* sibling took its place.
(files.length === 0 &&
!existsSync(path.join(module, ...location.dir)) &&
hasMigrationDir(module, location))
) {
failed = true;
errored = true;
console.error(
`::error::${boundWorkflowPath(module)}: found no migration under ` +
`${location.dir.join('/')}; if it moved, point this check at the ` +
`new location${
misplaced.length > 0
? `: ${misplaced.map(boundWorkflowPath).join(', ')}`
: ''
}`,
);
}
for (const file of files) {
count += 1;
const version = migrationVersion(path.basename(file));
const group = claimants.get(version) ?? [];
group.push({ module, file });
claimants.set(version, group);
}
}
if (!errored && count === 0) {
console.log(`${module}: no migration directories`);
continue;
}
scanned.push({ module, count, errored });
}
const collided = new Set();
for (const [version, group] of claimants) {
if (group.length < 2) continue;
failed = true;
console.error(
`::error::${boundWorkflowPath(group[0].module)}: ${group.length} migrations claim version ` +
`${escapeWorkflowCommand(version)}: ${group.map(({ file }) => boundWorkflowPath(file)).join(', ')}`,
);
for (const { module } of group) collided.add(module);
}
for (const { module, count, errored } of scanned) {
if (errored || collided.has(module)) continue;
console.log(`${module}: ${count} migrations, all versions unique`);
}
process.exitCode = failed ? 1 : 0;