fix(fleet): preserve exact v1 source strings
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful
This commit is contained in:
@@ -348,7 +348,8 @@ export async function previewV1ToV2Migration(
|
||||
|
||||
assertV1Root(raw, blockers);
|
||||
validateV1FieldCatalog(raw, blockers);
|
||||
blockPresentEmptyV1Fields(raw, blockers);
|
||||
blockPresentEmptyV1Fields(raw, decisions.fleetHost, blockers);
|
||||
blockLossyV1StringNormalization(raw, decisions.fleetHost, blockers);
|
||||
const tmux = record(raw.tmux);
|
||||
const defaults = record(raw.defaults);
|
||||
const runtimes = normalizeRuntimes(record(raw.runtimes), blockers);
|
||||
@@ -439,13 +440,13 @@ export async function previewV1ToV2Migration(
|
||||
continue;
|
||||
}
|
||||
|
||||
const runtime = stringValue(rawAgent.runtime);
|
||||
const runtime = exactStringValue(rawAgent.runtime);
|
||||
if (!isRuntime(runtime)) {
|
||||
blockers.push(
|
||||
blocker('unsupported-runtime', `${path}.runtime`, 'Runtime must be supported.', name),
|
||||
);
|
||||
}
|
||||
const requestedClass = stringValue(rawAgent.class) || 'worker';
|
||||
const requestedClass = exactStringValue(rawAgent.class) ?? 'worker';
|
||||
const className = resolveClass(
|
||||
requestedClass,
|
||||
decision.classDisposition,
|
||||
@@ -454,7 +455,7 @@ export async function previewV1ToV2Migration(
|
||||
blockers,
|
||||
);
|
||||
const requestedToolPolicy =
|
||||
stringValue(rawAgent.tool_policy) || stringValue(rawAgent.toolPolicy);
|
||||
exactStringValue(rawAgent.tool_policy) ?? exactStringValue(rawAgent.toolPolicy);
|
||||
const toolPolicy = resolveToolPolicy(
|
||||
requestedToolPolicy,
|
||||
decision.toolPolicyDisposition,
|
||||
@@ -511,7 +512,7 @@ export async function previewV1ToV2Migration(
|
||||
if (!runtime || !className || !toolPolicy || !desiredState) continue;
|
||||
candidateAgents.push({
|
||||
name,
|
||||
alias: stringValue(rawAgent.alias) || name,
|
||||
alias: exactStringValue(rawAgent.alias) ?? name,
|
||||
class: className,
|
||||
runtime,
|
||||
provider: decision.provider,
|
||||
@@ -519,10 +520,10 @@ export async function previewV1ToV2Migration(
|
||||
reasoning: decision.reasoning,
|
||||
tool_policy: toolPolicy,
|
||||
working_directory:
|
||||
stringValue(rawAgent.working_directory) ||
|
||||
stringValue(rawAgent.workingDirectory) ||
|
||||
stringValue(defaults.working_directory) ||
|
||||
stringValue(defaults.workingDirectory) ||
|
||||
exactStringValue(rawAgent.working_directory) ??
|
||||
exactStringValue(rawAgent.workingDirectory) ??
|
||||
exactStringValue(defaults.working_directory) ??
|
||||
exactStringValue(defaults.workingDirectory) ??
|
||||
'~/src',
|
||||
persistent_persona: persistentPersona,
|
||||
reset_between_tasks:
|
||||
@@ -596,12 +597,14 @@ export async function previewV1ToV2Migration(
|
||||
tmux: {
|
||||
socket_name: socketName,
|
||||
holder_session:
|
||||
stringValue(tmux.holder_session) || stringValue(tmux.holderSession) || '_holder',
|
||||
exactStringValue(tmux.holder_session) ??
|
||||
exactStringValue(tmux.holderSession) ??
|
||||
'_holder',
|
||||
},
|
||||
defaults: {
|
||||
working_directory:
|
||||
stringValue(defaults.working_directory) ||
|
||||
stringValue(defaults.workingDirectory) ||
|
||||
exactStringValue(defaults.working_directory) ??
|
||||
exactStringValue(defaults.workingDirectory) ??
|
||||
'~/src',
|
||||
runtime: defaultRuntime,
|
||||
},
|
||||
@@ -769,8 +772,8 @@ function classifyAgentLocality(
|
||||
agent: Record<string, unknown>,
|
||||
fleetHost: string | undefined,
|
||||
): AgentLocality {
|
||||
const host = stringValue(agent.host);
|
||||
const ssh = stringValue(agent.ssh);
|
||||
const host = exactStringValue(agent.host);
|
||||
const ssh = exactStringValue(agent.ssh);
|
||||
const hasHost = agent.host !== undefined;
|
||||
const hasSsh = agent.ssh !== undefined;
|
||||
if (!hasHost && !hasSsh) return 'local';
|
||||
@@ -837,6 +840,7 @@ function inventoryEntry(path: string): MigrationInventoryEntry {
|
||||
|
||||
function blockPresentEmptyV1Fields(
|
||||
raw: Record<string, unknown>,
|
||||
fleetHost: string | undefined,
|
||||
blockers: MigrationBlocker[],
|
||||
): void {
|
||||
const tmux = record(raw.tmux);
|
||||
@@ -852,7 +856,14 @@ function blockPresentEmptyV1Fields(
|
||||
);
|
||||
}
|
||||
for (const [index, agentValue] of (Array.isArray(raw.agents) ? raw.agents : []).entries()) {
|
||||
blockPresentEmptyField(record(agentValue), ['alias'], `agents[${index}]`, blockers);
|
||||
const agent = record(agentValue);
|
||||
if (classifyAgentLocality(agent, fleetHost) === 'remote') continue;
|
||||
blockPresentEmptyField(
|
||||
agent,
|
||||
['alias', 'class', 'tool_policy', 'toolPolicy'],
|
||||
`agents[${index}]`,
|
||||
blockers,
|
||||
);
|
||||
blockPresentEmptyField(
|
||||
record(agentValue),
|
||||
['working_directory', 'workingDirectory'],
|
||||
@@ -881,6 +892,55 @@ function blockPresentEmptyField(
|
||||
}
|
||||
}
|
||||
|
||||
function blockLossyV1StringNormalization(
|
||||
raw: Record<string, unknown>,
|
||||
fleetHost: string | undefined,
|
||||
blockers: MigrationBlocker[],
|
||||
): void {
|
||||
const tmux = record(raw.tmux);
|
||||
blockPaddedV1Field(tmux, ['holder_session', 'holderSession'], 'tmux', blockers);
|
||||
const defaults = record(raw.defaults);
|
||||
blockPaddedV1Field(defaults, ['working_directory', 'workingDirectory'], 'defaults', blockers);
|
||||
for (const [runtimeName, runtimeValue] of Object.entries(record(raw.runtimes))) {
|
||||
blockPaddedV1Field(
|
||||
record(runtimeValue),
|
||||
['reset_command', 'resetCommand'],
|
||||
`runtimes.${runtimeName}`,
|
||||
blockers,
|
||||
);
|
||||
}
|
||||
for (const [index, agentValue] of (Array.isArray(raw.agents) ? raw.agents : []).entries()) {
|
||||
const agent = record(agentValue);
|
||||
if (classifyAgentLocality(agent, fleetHost) === 'remote') continue;
|
||||
blockPaddedV1Field(
|
||||
agent,
|
||||
['alias', 'tool_policy', 'toolPolicy', 'working_directory', 'workingDirectory'],
|
||||
`agents[${index}]`,
|
||||
blockers,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function blockPaddedV1Field(
|
||||
value: Record<string, unknown>,
|
||||
fields: readonly string[],
|
||||
path: string,
|
||||
blockers: MigrationBlocker[],
|
||||
): void {
|
||||
for (const field of fields) {
|
||||
const source = exactStringValue(value[field]);
|
||||
if (source !== undefined && source !== source.trim()) {
|
||||
blockers.push(
|
||||
blocker(
|
||||
'lossy-v1-string-normalization',
|
||||
`${path}.${field}`,
|
||||
'A whitespace-padded v1 value cannot be silently normalized by the v2 compiler.',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function validateV1FieldCatalog(raw: Record<string, unknown>, blockers: MigrationBlocker[]): void {
|
||||
rejectUnknownKeys(raw, ROOT_FIELDS, 'root', blockers);
|
||||
requireObjectWhenPresent(raw.tmux, 'tmux', blockers);
|
||||
@@ -942,7 +1002,7 @@ function validateV1FieldCatalog(raw: Record<string, unknown>, blockers: Migratio
|
||||
blocker('invalid-v1-field-type', `${path}.name`, 'A valid agent name is required.'),
|
||||
);
|
||||
}
|
||||
if (stringValue(agent.runtime) === undefined) {
|
||||
if (exactStringValue(agent.runtime) === undefined) {
|
||||
blockers.push(
|
||||
blocker('invalid-v1-field-type', `${path}.runtime`, 'Agent runtime is required.'),
|
||||
);
|
||||
@@ -1219,7 +1279,7 @@ function normalizeRuntimes(
|
||||
}
|
||||
const runtime = record(value);
|
||||
const resetCommand =
|
||||
stringValue(runtime.reset_command) || stringValue(runtime.resetCommand) || '/clear';
|
||||
exactStringValue(runtime.reset_command) ?? exactStringValue(runtime.resetCommand) ?? '/clear';
|
||||
result[name] = { reset_command: resetCommand };
|
||||
}
|
||||
return result;
|
||||
@@ -1233,7 +1293,7 @@ function resolveClass(
|
||||
blockers: MigrationBlocker[],
|
||||
): string | undefined {
|
||||
const canonical = canonicalizeRoleClass(requested).canonicalClass;
|
||||
if (canonical !== requested || AUTOMATIC_CLASSES.has(canonical)) {
|
||||
if (AUTOMATIC_CLASSES.has(canonical)) {
|
||||
if (disposition !== undefined) {
|
||||
blockers.push(
|
||||
blocker(
|
||||
|
||||
Reference in New Issue
Block a user