From f405dff2e22681dc8d4f3a9b7427e278c6cfb0cc Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Sat, 31 Jan 2026 22:36:00 +0000 Subject: [PATCH] fix: preserve script envs field during sync push Co-Authored-By: Claude Opus 4.5 --- cli/src/commands/script/script.ts | 4 +- cli/test/script_envs_sync.test.ts | 294 ++++++++++++++++++++++++++++++ 2 files changed, 297 insertions(+), 1 deletion(-) create mode 100644 cli/test/script_envs_sync.test.ts diff --git a/cli/src/commands/script/script.ts b/cli/src/commands/script/script.ts index 6e38f3b736..ff962a3eeb 100644 --- a/cli/src/commands/script/script.ts +++ b/cli/src/commands/script/script.ts @@ -438,6 +438,7 @@ export async function handleFile( codebase: await codebase?.getDigest(forceTar), timeout: typed?.timeout, on_behalf_of_email: typed?.on_behalf_of_email, + envs: typed?.envs, }; // console.log(requestBodyCommon.codebase); @@ -478,7 +479,8 @@ export async function handleFile( typed.debounce_key == remote["debounce_key"] && typed.debounce_delay_s == remote["debounce_delay_s"] && typed.codebase == remote.codebase && - typed.on_behalf_of_email == remote.on_behalf_of_email) + typed.on_behalf_of_email == remote.on_behalf_of_email && + deepEqual(typed.envs, remote.envs)) ) { log.info(colors.green(`Script ${remotePath} is up to date`)); return true; diff --git a/cli/test/script_envs_sync.test.ts b/cli/test/script_envs_sync.test.ts new file mode 100644 index 0000000000..6e0c03bea1 --- /dev/null +++ b/cli/test/script_envs_sync.test.ts @@ -0,0 +1,294 @@ +/** + * Script envs sync test + * + * Tests that script metadata envs field is correctly preserved during sync push. + * Reproduces issue: env variables set in metadata of scripts don't work correctly + * if created by API, then sync pulled, then script modified, then sync push, + * the env variables aren't there anymore. + */ + +import { assertEquals, assert } from "https://deno.land/std@0.224.0/assert/mod.ts"; +import { ensureDir } from "https://deno.land/std@0.224.0/fs/mod.ts"; +import { withTestBackend } from "./test_backend.ts"; + +Deno.test({ + name: "Integration: Script envs field is preserved during sync pull/push cycle", + sanitizeResources: false, + sanitizeOps: false, + async fn() { + await withTestBackend(async (backend, tempDir) => { + const uniqueId = Date.now(); + const scriptPath = `f/test/envs_script_${uniqueId}`; + + // Step 1: Create a script via API with envs set + await ensureDir(`${tempDir}/f/test`); + + // Create folder first + const folderResp = await backend.apiRequest!(`/api/w/${backend.workspace}/folders/create`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ name: "test" }), + }); + // Ignore error if folder already exists + + // Create a script with envs via API + const createResp = await backend.apiRequest!(`/api/w/${backend.workspace}/scripts/create`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + path: scriptPath, + content: `export async function main() {\n return "Hello world";\n}`, + summary: "Test script with envs", + description: "A script to test envs preservation", + language: "bun", + envs: ["MY_ENV_VAR", "ANOTHER_VAR"], + kind: "script", + }), + }); + + assertEquals( + createResp.ok, + true, + `Failed to create script: ${await createResp.text()}`, + ); + + // Verify the script was created with envs + const getResp = await backend.apiRequest!( + `/api/w/${backend.workspace}/scripts/get/p/${scriptPath}`, + ); + const createdScriptText = await getResp.text(); + assertEquals(getResp.ok, true, `Failed to get script: ${createdScriptText}`); + const createdScript = JSON.parse(createdScriptText); + assertEquals( + createdScript.envs, + ["MY_ENV_VAR", "ANOTHER_VAR"], + "Script should have envs after creation", + ); + + // Step 2: Create wmill.yaml and sync pull + await Deno.writeTextFile( + `${tempDir}/wmill.yaml`, + `defaultTs: bun +includes: + - "f/test/envs_script_${uniqueId}**" +excludes: [] +`, + ); + + const pullResult = await backend.runCLICommand(["sync", "pull", "--yes"], tempDir); + assertEquals( + pullResult.code, + 0, + `Pull should succeed.\nstdout: ${pullResult.stdout}\nstderr: ${pullResult.stderr}`, + ); + + // Verify the pulled metadata contains envs + const metadataPath = `${tempDir}/f/test/envs_script_${uniqueId}.script.yaml`; + const metadataContent = await Deno.readTextFile(metadataPath); + assert( + metadataContent.includes("envs:") || + metadataContent.includes("MY_ENV_VAR") || + metadataContent.includes("ANOTHER_VAR"), + `Pulled metadata should contain envs. Content:\n${metadataContent}`, + ); + + // Step 3: Modify the script locally (change content) + const scriptFilePath = `${tempDir}/f/test/envs_script_${uniqueId}.ts`; + const originalContent = await Deno.readTextFile(scriptFilePath); + await Deno.writeTextFile( + scriptFilePath, + originalContent.replace("Hello world", "Hello world modified"), + ); + + // Step 4: Sync push + const pushResult = await backend.runCLICommand(["sync", "push", "--yes"], tempDir); + assertEquals( + pushResult.code, + 0, + `Push should succeed.\nstdout: ${pushResult.stdout}\nstderr: ${pushResult.stderr}`, + ); + + // Step 5: Verify envs are still present on the remote + const getResp2 = await backend.apiRequest!( + `/api/w/${backend.workspace}/scripts/get/p/${scriptPath}`, + ); + const updatedScriptText = await getResp2.text(); + assertEquals(getResp2.ok, true, `Failed to get script after push: ${updatedScriptText}`); + const updatedScript = JSON.parse(updatedScriptText); + + assertEquals( + updatedScript.envs, + ["MY_ENV_VAR", "ANOTHER_VAR"], + `Script envs should be preserved after push. Got: ${JSON.stringify(updatedScript.envs)}`, + ); + + // Also verify the content was updated + assert( + updatedScript.content.includes("Hello world modified"), + "Script content should be updated", + ); + }); + }, +}); + +Deno.test({ + name: "Integration: Script envs field changes are detected and pushed", + sanitizeResources: false, + sanitizeOps: false, + async fn() { + await withTestBackend(async (backend, tempDir) => { + const uniqueId = Date.now(); + const scriptPath = `f/test/envs_change_${uniqueId}`; + + // Create folder + await ensureDir(`${tempDir}/f/test`); + await backend.apiRequest!(`/api/w/${backend.workspace}/folders/create`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ name: "test" }), + }); + + // Create a script with initial envs + const createResp = await backend.apiRequest!(`/api/w/${backend.workspace}/scripts/create`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + path: scriptPath, + content: `export async function main() {\n return "Hello";\n}`, + summary: "Test envs change", + description: "", + language: "bun", + envs: ["INITIAL_VAR"], + kind: "script", + }), + }); + assertEquals(createResp.ok, true, `Failed to create script: ${await createResp.text()}`); + + // Setup wmill.yaml + await Deno.writeTextFile( + `${tempDir}/wmill.yaml`, + `defaultTs: bun +includes: + - "f/test/envs_change_${uniqueId}**" +excludes: [] +`, + ); + + // Pull + const pullResult = await backend.runCLICommand(["sync", "pull", "--yes"], tempDir); + assertEquals(pullResult.code, 0, `Pull failed: ${pullResult.stderr}`); + + // Modify envs in the local metadata file + const metadataPath = `${tempDir}/f/test/envs_change_${uniqueId}.script.yaml`; + let metadataContent = await Deno.readTextFile(metadataPath); + + // Replace the envs line(s) + if (metadataContent.includes("envs:")) { + // Replace existing envs + metadataContent = metadataContent.replace( + /envs:\s*\n(\s+-\s+\S+\n?)*/, + "envs:\n - NEW_VAR1\n - NEW_VAR2\n", + ); + } else { + // Add envs if not present + metadataContent += "\nenvs:\n - NEW_VAR1\n - NEW_VAR2\n"; + } + await Deno.writeTextFile(metadataPath, metadataContent); + + // Push + const pushResult = await backend.runCLICommand(["sync", "push", "--yes"], tempDir); + assertEquals(pushResult.code, 0, `Push failed: ${pushResult.stderr}`); + + // Verify envs were updated on remote + const getResp = await backend.apiRequest!( + `/api/w/${backend.workspace}/scripts/get/p/${scriptPath}`, + ); + const scriptText = await getResp.text(); + assertEquals(getResp.ok, true, `Failed to get script: ${scriptText}`); + const script = JSON.parse(scriptText); + + assertEquals( + script.envs, + ["NEW_VAR1", "NEW_VAR2"], + `Script envs should be updated to new values. Got: ${JSON.stringify(script.envs)}`, + ); + }); + }, +}); + +Deno.test({ + name: "Integration: Script with empty envs is handled correctly", + sanitizeResources: false, + sanitizeOps: false, + async fn() { + await withTestBackend(async (backend, tempDir) => { + const uniqueId = Date.now(); + const scriptPath = `f/test/empty_envs_${uniqueId}`; + + // Create folder + await ensureDir(`${tempDir}/f/test`); + await backend.apiRequest!(`/api/w/${backend.workspace}/folders/create`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ name: "test" }), + }); + + // Create a script WITHOUT envs + const createResp = await backend.apiRequest!(`/api/w/${backend.workspace}/scripts/create`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + path: scriptPath, + content: `export async function main() {\n return "No envs";\n}`, + summary: "Test no envs", + description: "", + language: "bun", + kind: "script", + }), + }); + assertEquals(createResp.ok, true, `Failed to create script: ${await createResp.text()}`); + + // Setup wmill.yaml + await Deno.writeTextFile( + `${tempDir}/wmill.yaml`, + `defaultTs: bun +includes: + - "f/test/empty_envs_${uniqueId}**" +excludes: [] +`, + ); + + // Pull + const pullResult = await backend.runCLICommand(["sync", "pull", "--yes"], tempDir); + assertEquals(pullResult.code, 0, `Pull failed: ${pullResult.stderr}`); + + // Modify content + const scriptFilePath = `${tempDir}/f/test/empty_envs_${uniqueId}.ts`; + await Deno.writeTextFile( + scriptFilePath, + `export async function main() {\n return "Modified no envs";\n}`, + ); + + // Push + const pushResult = await backend.runCLICommand(["sync", "push", "--yes"], tempDir); + assertEquals(pushResult.code, 0, `Push failed: ${pushResult.stderr}`); + + // Verify script was updated and envs is still null/empty + const getResp = await backend.apiRequest!( + `/api/w/${backend.workspace}/scripts/get/p/${scriptPath}`, + ); + const script = await getResp.json(); + + assert( + script.content.includes("Modified no envs"), + "Script content should be updated", + ); + + // envs should be null, empty, or undefined + assert( + !script.envs || script.envs.length === 0, + `Script envs should remain empty. Got: ${JSON.stringify(script.envs)}`, + ); + }); + }, +});