mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-20 08:03:28 +00:00
Merge branch 'main' into workflow-execution-time-display
This commit is contained in:
@@ -1,5 +1,19 @@
|
||||
# Changelog
|
||||
|
||||
## [1.721.0](https://github.com/windmill-labs/windmill/compare/v1.720.0...v1.721.0) (2026-06-09)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* deployed↔draft compare + AI-session draft bar ([#9435](https://github.com/windmill-labs/windmill/issues/9435)) ([b0b330c](https://github.com/windmill-labs/windmill/commit/b0b330c7864d0159af4b0f17dbb3c09bd015145b))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **cli:** reconcile case-only path drift during sync on case-insensitive filesystems (WIN-2020) ([#9485](https://github.com/windmill-labs/windmill/issues/9485)) ([c258928](https://github.com/windmill-labs/windmill/commit/c258928ab62adc1327913c21556520dfd1e5c24c))
|
||||
* drop archived items from fork compare (spurious 'not visible' warning) ([#9481](https://github.com/windmill-labs/windmill/issues/9481)) ([92c21bb](https://github.com/windmill-labs/windmill/commit/92c21bbe6586f3c285796a98692e976515a629d5))
|
||||
* require auth to view approval details when user_auth_required ([#9482](https://github.com/windmill-labs/windmill/issues/9482)) ([5f41ddd](https://github.com/windmill-labs/windmill/commit/5f41ddd3a592bcd504f94fc99060ca5d79c36190))
|
||||
|
||||
## [1.720.0](https://github.com/windmill-labs/windmill/compare/v1.719.0...v1.720.0) (2026-06-08)
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import type { CompletedJob, Flow, Script } from '../../../frontend/src/lib/gen'
|
||||
import type { CompletedJob, Flow, Job, Script } from '../../../frontend/src/lib/gen'
|
||||
import type {
|
||||
DataTableTables,
|
||||
DataTableTableSchema,
|
||||
@@ -29,18 +29,36 @@ export interface BenchmarkWorkspaceFlow {
|
||||
value: Flow['value']
|
||||
}
|
||||
|
||||
export interface BenchmarkWorkspaceJob {
|
||||
/** Stable id so a case prompt can reference a specific run (e.g. for get_job_logs). */
|
||||
id?: string
|
||||
jobKind?: CompletedJob['job_kind']
|
||||
scriptPath?: string
|
||||
createdBy?: string
|
||||
label?: string
|
||||
success?: boolean
|
||||
logs?: string
|
||||
}
|
||||
|
||||
export interface BenchmarkWorkspaceRunnables {
|
||||
scripts?: BenchmarkWorkspaceScript[]
|
||||
flows?: BenchmarkWorkspaceFlow[]
|
||||
datatables?: BenchmarkDatatableSeed[]
|
||||
jobs?: BenchmarkWorkspaceJob[]
|
||||
}
|
||||
|
||||
type BenchmarkCompletedJob = CompletedJob & { type: 'CompletedJob' }
|
||||
|
||||
const benchmarkWorkspaces = new Set<string>()
|
||||
const benchmarkWorkspaceRunnables = new Map<string, BenchmarkWorkspaceRunnables>()
|
||||
// Keyed by `${workspace}::${jobId}` so concurrent attempts (or distinct cases)
|
||||
// can seed the same fixed job id without clobbering each other's entry.
|
||||
const benchmarkJobs = new Map<string, { workspace: string; job: BenchmarkCompletedJob }>()
|
||||
|
||||
function benchmarkJobKey(workspace: string, jobId: string): string {
|
||||
return `${workspace}::${jobId}`
|
||||
}
|
||||
|
||||
export function resetBenchmarkMockBackend(): void {
|
||||
benchmarkWorkspaces.clear()
|
||||
benchmarkWorkspaceRunnables.clear()
|
||||
@@ -62,6 +80,19 @@ export function registerBenchmarkWorkspaceRunnables(
|
||||
...runnables,
|
||||
datatables: runnables.datatables ? structuredClone(runnables.datatables) : undefined
|
||||
})
|
||||
// Seed any fixture jobs so list_runs / get_job_logs have data to return.
|
||||
for (const seed of runnables.jobs ?? []) {
|
||||
createBenchmarkCompletedJob({
|
||||
workspace,
|
||||
id: seed.id,
|
||||
jobKind: seed.jobKind ?? 'script',
|
||||
success: seed.success,
|
||||
scriptPath: seed.scriptPath,
|
||||
createdBy: seed.createdBy,
|
||||
label: seed.label,
|
||||
logs: seed.logs
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function unregisterBenchmarkWorkspace(workspace: string): void {
|
||||
@@ -131,14 +162,17 @@ export function createBenchmarkCompletedJob(input: {
|
||||
scriptPath?: string
|
||||
scriptHash?: string
|
||||
args?: Record<string, unknown>
|
||||
id?: string
|
||||
createdBy?: string
|
||||
label?: string
|
||||
}): string {
|
||||
const jobId = `benchmark-job-${randomUUID()}`
|
||||
const jobId = input.id ?? `benchmark-job-${randomUUID()}`
|
||||
const now = new Date().toISOString()
|
||||
const job: BenchmarkCompletedJob = {
|
||||
type: 'CompletedJob',
|
||||
id: jobId,
|
||||
workspace_id: input.workspace,
|
||||
created_by: 'ai-evals',
|
||||
created_by: input.createdBy ?? 'ai-evals',
|
||||
created_at: now,
|
||||
started_at: now,
|
||||
completed_at: now,
|
||||
@@ -156,10 +190,11 @@ export function createBenchmarkCompletedJob(input: {
|
||||
is_skipped: false,
|
||||
email: 'ai-evals@local',
|
||||
visible_to_owner: true,
|
||||
tag: 'benchmark'
|
||||
tag: 'benchmark',
|
||||
labels: input.label ? [input.label] : undefined
|
||||
}
|
||||
|
||||
benchmarkJobs.set(jobId, { workspace: input.workspace, job })
|
||||
benchmarkJobs.set(benchmarkJobKey(input.workspace, jobId), { workspace: input.workspace, job })
|
||||
return jobId
|
||||
}
|
||||
|
||||
@@ -167,13 +202,42 @@ export function getBenchmarkCompletedJob(
|
||||
workspace: string,
|
||||
jobId: string
|
||||
): BenchmarkCompletedJob | null {
|
||||
const entry = benchmarkJobs.get(jobId)
|
||||
if (!entry || entry.workspace !== workspace) {
|
||||
const entry = benchmarkJobs.get(benchmarkJobKey(workspace, jobId))
|
||||
if (!entry) {
|
||||
return null
|
||||
}
|
||||
return structuredClone(entry.job)
|
||||
}
|
||||
|
||||
/**
|
||||
* List seeded/recorded jobs for a benchmark workspace, most recent first —
|
||||
* the shape `JobService.listJobs` returns. Returns `null` for a non-benchmark
|
||||
* workspace so the caller can fall through to the real backend. Server-side
|
||||
* filters (path/creator/status/limit) are intentionally not applied: global
|
||||
* eval cases assert on the recorded `list_runs` tool call, not on filtering.
|
||||
*/
|
||||
export function listBenchmarkJobs(workspace: string): Job[] | null {
|
||||
if (!hasBenchmarkWorkspace(workspace)) {
|
||||
return null
|
||||
}
|
||||
return [...benchmarkJobs.values()]
|
||||
.filter((entry) => entry.workspace === workspace)
|
||||
.map((entry) => structuredClone(entry.job) as Job)
|
||||
.sort((a, b) => (b.created_at ?? '').localeCompare(a.created_at ?? ''))
|
||||
}
|
||||
|
||||
/**
|
||||
* Mirror `JobService.getJobLogs` (response is the raw log string). Throws a
|
||||
* "not found" error for an unknown id, matching the backend 404.
|
||||
*/
|
||||
export function getBenchmarkJobLogs(workspace: string, jobId: string): string {
|
||||
const job = getBenchmarkCompletedJob(workspace, jobId)
|
||||
if (!job) {
|
||||
throw new Error(`Job Logs not found for "${jobId}"`)
|
||||
}
|
||||
return job.logs ?? ''
|
||||
}
|
||||
|
||||
// ============= Datatables (best-effort in-memory SQL) =============
|
||||
|
||||
/**
|
||||
|
||||
@@ -37,11 +37,13 @@ vi.mock('$lib/gen', async () => {
|
||||
getBenchmarkCompletedJobResultMaybe,
|
||||
getBenchmarkDatatableSchema,
|
||||
getBenchmarkFlowByPath,
|
||||
getBenchmarkJobLogs,
|
||||
getBenchmarkScriptByHash,
|
||||
getBenchmarkScriptByPath,
|
||||
hasBenchmarkWorkspace,
|
||||
listBenchmarkDatatables,
|
||||
listBenchmarkFlows,
|
||||
listBenchmarkJobs,
|
||||
listBenchmarkScripts,
|
||||
createBenchmarkHttpTrigger,
|
||||
createBenchmarkSchedule,
|
||||
@@ -199,7 +201,15 @@ vi.mock('$lib/gen', async () => {
|
||||
getCompletedJobResultMaybe: async (data: { workspace: string; id: string }) =>
|
||||
hasBenchmarkWorkspace(data.workspace)
|
||||
? getBenchmarkCompletedJobResultMaybe({ workspace: data.workspace, id: data.id })
|
||||
: actual.JobService.getCompletedJobResultMaybe(data)
|
||||
: actual.JobService.getCompletedJobResultMaybe(data),
|
||||
listJobs: async (data: { workspace: string }) =>
|
||||
hasBenchmarkWorkspace(data.workspace)
|
||||
? (listBenchmarkJobs(data.workspace) ?? [])
|
||||
: actual.JobService.listJobs(data),
|
||||
getJobLogs: async (data: { workspace: string; id: string }) =>
|
||||
hasBenchmarkWorkspace(data.workspace)
|
||||
? getBenchmarkJobLogs(data.workspace, data.id)
|
||||
: actual.JobService.getJobLogs(data)
|
||||
}),
|
||||
WorkspaceService: wrapService(actual.WorkspaceService, {
|
||||
listDataTableTables: async (data: { workspace: string }) =>
|
||||
|
||||
@@ -821,3 +821,52 @@
|
||||
- uses the wmill.datatable() SDK to query the orders table and sum the order totals
|
||||
- returns the total revenue from the script
|
||||
- leaves the result as an AI draft and does not deploy or save it
|
||||
|
||||
- id: global-test27-list-recent-runs
|
||||
prompt: |-
|
||||
What are the most recent runs in this workspace? Give me a quick rundown of what ran and whether it succeeded.
|
||||
initial: ai_evals/fixtures/frontend/global/initial/jobs_seed.json
|
||||
runtime:
|
||||
maxTurns: 6
|
||||
validate:
|
||||
draftCountExactly: 0
|
||||
toolExpect:
|
||||
requiredToolsUsed:
|
||||
- list_runs
|
||||
forbiddenToolsUsed:
|
||||
- deploy_workspace_item
|
||||
- delete_workspace_item
|
||||
- write_script
|
||||
# Read-only job inspection produces no draft, so the global judge (which only
|
||||
# sees the drafts artifact) would score it empty — validate via tool use.
|
||||
skipJudge: true
|
||||
judgeChecklist:
|
||||
- lists the recent runs from the workspace rather than fabricating them
|
||||
- summarizes each run's path and success/failure from the returned data
|
||||
|
||||
- id: global-test28-fetch-failed-run-logs
|
||||
prompt: |-
|
||||
The run with id 01920000-0000-7000-8000-0000000000f1 failed. Pull its logs and tell me what went wrong.
|
||||
initial: ai_evals/fixtures/frontend/global/initial/jobs_seed.json
|
||||
runtime:
|
||||
maxTurns: 6
|
||||
validate:
|
||||
draftCountExactly: 0
|
||||
toolExpect:
|
||||
requiredToolsUsed:
|
||||
- get_job_logs
|
||||
forbiddenToolsUsed:
|
||||
- deploy_workspace_item
|
||||
- delete_workspace_item
|
||||
- write_script
|
||||
toolCallArgs:
|
||||
- tool: get_job_logs
|
||||
field: id
|
||||
stringIncludesAnyOf:
|
||||
- 01920000-0000-7000-8000-0000000000f1
|
||||
# Same as above — no draft is produced, so rely on the deterministic tool-use
|
||||
# and argument checks rather than the judge.
|
||||
skipJudge: true
|
||||
judgeChecklist:
|
||||
- fetches the logs for the requested job id
|
||||
- explains the failure from the returned logs (connection refused to the upstream API)
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"workspace": {
|
||||
"jobs": [
|
||||
{
|
||||
"id": "01920000-0000-7000-8000-0000000000f1",
|
||||
"scriptPath": "f/etl/sync_customers",
|
||||
"jobKind": "script",
|
||||
"createdBy": "alice",
|
||||
"success": false,
|
||||
"logs": "Starting customer sync...\nFetched 0 records\nERROR: connection refused to https://api.upstream.example.com\n at fetchCustomers (sync_customers.ts:42)\nJob failed with exit code 1"
|
||||
},
|
||||
{
|
||||
"id": "01920000-0000-7000-8000-0000000000f2",
|
||||
"scriptPath": "f/reports/daily_digest",
|
||||
"jobKind": "script",
|
||||
"createdBy": "bob",
|
||||
"success": true,
|
||||
"logs": "Generating daily digest...\nDigest emailed to 12 recipients\nDone in 1.2s"
|
||||
},
|
||||
{
|
||||
"id": "01920000-0000-7000-8000-0000000000f3",
|
||||
"scriptPath": "f/billing/charge_invoices",
|
||||
"jobKind": "flow",
|
||||
"createdBy": "alice",
|
||||
"success": true,
|
||||
"logs": "Processing invoices...\nCharged 8 invoices totalling $1,240.00\nDone"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
Generated
+78
-78
@@ -13782,7 +13782,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-nats",
|
||||
@@ -13864,7 +13864,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-ai"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"async-stream",
|
||||
"async-trait",
|
||||
@@ -13897,7 +13897,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-alerting"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"axum 0.8.9",
|
||||
"chrono",
|
||||
@@ -13910,7 +13910,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argon2",
|
||||
@@ -14048,7 +14048,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-agent-workers"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"axum 0.8.9",
|
||||
"chrono",
|
||||
@@ -14071,7 +14071,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-assets"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"axum 0.8.9",
|
||||
"chrono",
|
||||
@@ -14084,7 +14084,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-auth"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.8.9",
|
||||
@@ -14110,7 +14110,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-client"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"reqwest 0.12.28",
|
||||
"serde",
|
||||
@@ -14120,7 +14120,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-configs"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"axum 0.8.9",
|
||||
"chrono",
|
||||
@@ -14137,7 +14137,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-debug"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"axum 0.8.9",
|
||||
"base64 0.22.1",
|
||||
@@ -14159,7 +14159,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-embeddings"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.8.9",
|
||||
@@ -14182,7 +14182,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-flow-conversations"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"axum 0.8.9",
|
||||
"chrono",
|
||||
@@ -14198,7 +14198,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-flows"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"axum 0.8.9",
|
||||
"chrono",
|
||||
@@ -14219,7 +14219,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-groups"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"axum 0.8.9",
|
||||
"chrono",
|
||||
@@ -14240,7 +14240,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-inputs"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"axum 0.8.9",
|
||||
"chrono",
|
||||
@@ -14254,7 +14254,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-integration-tests"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-nats",
|
||||
@@ -14289,7 +14289,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-jobs"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.8.9",
|
||||
@@ -14314,7 +14314,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-npm-proxy"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"axum 0.8.9",
|
||||
"flate2",
|
||||
@@ -14332,7 +14332,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-openapi"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.8.9",
|
||||
@@ -14354,7 +14354,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-schedule"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"axum 0.8.9",
|
||||
"chrono",
|
||||
@@ -14374,7 +14374,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-scripts"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"axum 0.8.9",
|
||||
"chrono",
|
||||
@@ -14405,7 +14405,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-settings"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.8.9",
|
||||
@@ -14433,7 +14433,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-sse"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"serde",
|
||||
@@ -14445,7 +14445,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-users"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"argon2",
|
||||
"axum 0.8.9",
|
||||
@@ -14470,7 +14470,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-workers"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"axum 0.8.9",
|
||||
"chrono",
|
||||
@@ -14484,7 +14484,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-workspaces"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"axum 0.8.9",
|
||||
"chrono",
|
||||
@@ -14517,7 +14517,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-audit"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"lazy_static",
|
||||
@@ -14531,7 +14531,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-autoscaling"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.8.9",
|
||||
@@ -14550,7 +14550,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-common"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"aho-corasick",
|
||||
@@ -14651,7 +14651,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-dep-map"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"itertools 0.14.0",
|
||||
@@ -14670,7 +14670,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-git-sync"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"regex",
|
||||
"serde",
|
||||
@@ -14685,7 +14685,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-indexer"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"astral-tokio-tar",
|
||||
@@ -14709,7 +14709,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-jseval"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"futures",
|
||||
@@ -14726,7 +14726,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-macros"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"itertools 0.14.0",
|
||||
"lazy_static",
|
||||
@@ -14742,7 +14742,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-mcp"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -14763,7 +14763,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-native-triggers"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -14794,7 +14794,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-oauth"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
@@ -14819,7 +14819,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-object-store"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-stream",
|
||||
@@ -14853,7 +14853,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-operator"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"futures",
|
||||
@@ -14871,7 +14871,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"convert_case 0.6.0",
|
||||
"serde",
|
||||
@@ -14880,7 +14880,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-bash"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -14892,7 +14892,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-csharp"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -14904,7 +14904,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-go"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"gosyn",
|
||||
@@ -14916,7 +14916,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-graphql"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -14928,7 +14928,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-java"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -14940,7 +14940,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-nu"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"nu-parser",
|
||||
@@ -14951,7 +14951,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-php"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
@@ -14962,7 +14962,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
@@ -14974,7 +14974,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py-asset"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"rustpython-ast",
|
||||
@@ -14985,7 +14985,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py-imports"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -15007,7 +15007,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-r"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -15019,7 +15019,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ruby"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -15033,7 +15033,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-rust"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"convert_case 0.6.0",
|
||||
@@ -15050,7 +15050,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-sql"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -15063,7 +15063,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-sql-asset"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
@@ -15075,7 +15075,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ts"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -15093,7 +15093,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ts-asset"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde-wasm-bindgen",
|
||||
@@ -15109,7 +15109,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-wac"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"rustpython-ast",
|
||||
@@ -15125,7 +15125,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-yaml"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
@@ -15136,7 +15136,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-queue"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -15174,7 +15174,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-runtime-nativets"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"const_format",
|
||||
@@ -15212,7 +15212,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-sql-datatype-parser-wasm"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"getrandom 0.3.4",
|
||||
"wasm-bindgen",
|
||||
@@ -15223,7 +15223,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-store"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -15255,7 +15255,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-test-utils"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -15279,7 +15279,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -15312,7 +15312,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-azure"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -15345,7 +15345,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-email"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -15365,7 +15365,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-gcp"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -15399,7 +15399,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-http"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -15435,7 +15435,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-kafka"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -15458,7 +15458,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-mqtt"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -15482,7 +15482,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-nats"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-nats",
|
||||
@@ -15506,7 +15506,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-postgres"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -15541,7 +15541,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-sqs"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -15569,7 +15569,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-websocket"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -15594,7 +15594,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-types"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 2.13.0",
|
||||
@@ -15613,7 +15613,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-worker"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-once-cell",
|
||||
@@ -15723,7 +15723,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-worker-volumes"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures",
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "windmill"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
@@ -87,7 +87,7 @@ members = [
|
||||
exclude = ["./windmill-duckdb-ffi-internal", "./parsers/windmill-parser-wasm"]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
|
||||
edition = "2021"
|
||||
|
||||
|
||||
+24
-24
@@ -6183,7 +6183,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windmill-common"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"anyhow",
|
||||
@@ -6263,7 +6263,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-macros"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -6275,7 +6275,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"convert_case",
|
||||
"serde",
|
||||
@@ -6284,7 +6284,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-bash"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -6296,7 +6296,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-csharp"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -6308,7 +6308,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-go"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"gosyn",
|
||||
@@ -6320,7 +6320,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-graphql"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -6332,7 +6332,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-java"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -6344,7 +6344,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-nu"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"nu-parser",
|
||||
@@ -6355,7 +6355,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-php"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
@@ -6366,7 +6366,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
@@ -6378,7 +6378,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py-asset"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"rustpython-ast",
|
||||
@@ -6389,7 +6389,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py-imports"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -6411,7 +6411,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-r"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -6423,7 +6423,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ruby"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -6437,7 +6437,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-rust"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"convert_case",
|
||||
@@ -6454,7 +6454,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-sql"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -6467,7 +6467,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-sql-asset"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
@@ -6479,7 +6479,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ts"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -6497,7 +6497,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ts-asset"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde-wasm-bindgen",
|
||||
@@ -6513,7 +6513,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-wac"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"rustpython-ast",
|
||||
@@ -6529,7 +6529,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-wasm"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"getrandom 0.2.17",
|
||||
@@ -6561,7 +6561,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-yaml"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
@@ -6572,7 +6572,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-types"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags",
|
||||
|
||||
@@ -12,7 +12,7 @@ resolver = "2"
|
||||
members = ["."]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
edition = "2021"
|
||||
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: "3.0.3"
|
||||
|
||||
info:
|
||||
version: 1.720.0
|
||||
version: 1.721.0
|
||||
title: Windmill API
|
||||
|
||||
contact:
|
||||
|
||||
@@ -1077,6 +1077,15 @@ async fn get_otel_tracing_proxy_envs(
|
||||
/// through the proxy without TLS interception, so clients that pin their own CA (kubectl,
|
||||
/// helm, terraform, etc.) keep working. Empty when unset, matching the prior behavior of
|
||||
/// intercepting all destinations including loopback.
|
||||
///
|
||||
/// The worker's own `NO_PROXY` env is merged in so that enabling HTTP request tracing never
|
||||
/// silently narrows exclusions an operator already configured at the container level: hosts
|
||||
/// reachable directly before tracing was turned on stay reachable directly afterwards. The
|
||||
/// upstream-relay side (`build_no_proxy_intercept`) already honors the container `NO_PROXY`;
|
||||
/// this keeps the injected-into-jobs side symmetric. Note that job runtimes match `NO_PROXY`
|
||||
/// by hostname/suffix, not by resolving against CIDR ranges, so CIDR-only entries (e.g.
|
||||
/// `10.0.0.0/8`) carry over but won't match a hostname target — operators still need an
|
||||
/// explicit host/suffix entry for those.
|
||||
#[cfg(all(feature = "private", feature = "enterprise"))]
|
||||
async fn build_tracing_proxy_no_proxy() -> String {
|
||||
let configured = OTEL_TRACING_PROXY_SETTINGS
|
||||
@@ -1084,22 +1093,23 @@ async fn build_tracing_proxy_no_proxy() -> String {
|
||||
.await
|
||||
.no_proxy_hosts
|
||||
.clone();
|
||||
normalize_no_proxy_hosts(configured.as_deref())
|
||||
normalize_no_proxy_hosts([configured.as_deref(), NO_PROXY.as_deref()])
|
||||
}
|
||||
|
||||
/// Split a comma-separated NO_PROXY value, trim whitespace, drop empty entries, and
|
||||
/// deduplicate while preserving order. `None` returns an empty string.
|
||||
/// Split comma-separated NO_PROXY sources, trim whitespace, drop empty entries, and
|
||||
/// deduplicate while preserving first-occurrence order. Sources are concatenated in the
|
||||
/// order given (earlier sources win on ordering). Returns an empty string when all sources
|
||||
/// are `None`/empty.
|
||||
#[cfg(all(feature = "private", feature = "enterprise"))]
|
||||
fn normalize_no_proxy_hosts(configured: Option<&str>) -> String {
|
||||
let Some(configured) = configured else {
|
||||
return String::new();
|
||||
};
|
||||
fn normalize_no_proxy_hosts<'a>(sources: impl IntoIterator<Item = Option<&'a str>>) -> String {
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
let mut out: Vec<&str> = Vec::new();
|
||||
for entry in configured.split(',') {
|
||||
let trimmed = entry.trim();
|
||||
if !trimmed.is_empty() && seen.insert(trimmed) {
|
||||
out.push(trimmed);
|
||||
for source in sources.into_iter().flatten() {
|
||||
for entry in source.split(',') {
|
||||
let trimmed = entry.trim();
|
||||
if !trimmed.is_empty() && seen.insert(trimmed) {
|
||||
out.push(trimmed);
|
||||
}
|
||||
}
|
||||
}
|
||||
out.join(",")
|
||||
@@ -1111,26 +1121,58 @@ mod no_proxy_tests {
|
||||
|
||||
#[test]
|
||||
fn unset_returns_empty() {
|
||||
assert_eq!(normalize_no_proxy_hosts(None), "");
|
||||
assert_eq!(normalize_no_proxy_hosts([None]), "");
|
||||
assert_eq!(normalize_no_proxy_hosts([None, None]), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_and_whitespace_only_returns_empty() {
|
||||
assert_eq!(normalize_no_proxy_hosts(Some("")), "");
|
||||
assert_eq!(normalize_no_proxy_hosts(Some(" , ,\t")), "");
|
||||
assert_eq!(normalize_no_proxy_hosts([Some("")]), "");
|
||||
assert_eq!(normalize_no_proxy_hosts([Some(" , ,\t")]), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trims_and_skips_empties() {
|
||||
assert_eq!(
|
||||
normalize_no_proxy_hosts(Some(" *.eks.amazonaws.com ,, *.internal ")),
|
||||
normalize_no_proxy_hosts([Some(" *.eks.amazonaws.com ,, *.internal ")]),
|
||||
"*.eks.amazonaws.com,*.internal"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dedupes_preserving_first_occurrence_order() {
|
||||
assert_eq!(normalize_no_proxy_hosts(Some("a,b,a,c,b,d")), "a,b,c,d");
|
||||
assert_eq!(normalize_no_proxy_hosts([Some("a,b,a,c,b,d")]), "a,b,c,d");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merges_configured_with_container_no_proxy() {
|
||||
// Configured tracing hosts come first, then the container NO_PROXY is appended.
|
||||
assert_eq!(
|
||||
normalize_no_proxy_hosts([
|
||||
Some("gitlab.internal"),
|
||||
Some("localhost,127.0.0.1,10.0.0.0/8,.cluster.local")
|
||||
]),
|
||||
"gitlab.internal,localhost,127.0.0.1,10.0.0.0/8,.cluster.local"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merges_dedupe_across_sources() {
|
||||
// Entries present in both sources are not duplicated.
|
||||
assert_eq!(
|
||||
normalize_no_proxy_hosts([Some("localhost,gitlab.internal"), Some("localhost,.svc")]),
|
||||
"localhost,gitlab.internal,.svc"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn container_no_proxy_carries_over_when_unconfigured() {
|
||||
// Tracing setting unset but container NO_PROXY present: exclusions still carry over,
|
||||
// so enabling tracing does not silently drop them.
|
||||
assert_eq!(
|
||||
normalize_no_proxy_hosts([None, Some(".cluster.local,gitlab.internal")]),
|
||||
".cluster.local,gitlab.internal"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ import { sleep } from "https://deno.land/x/sleep@v1.2.1/mod.ts";
|
||||
import * as windmill from "https://deno.land/x/windmill@v1.174.0/mod.ts";
|
||||
import * as api from "https://deno.land/x/windmill@v1.174.0/windmill-api/index.ts";
|
||||
|
||||
export const VERSION = "v1.720.0";
|
||||
export const VERSION = "v1.721.0";
|
||||
|
||||
export async function login(email: string, password: string): Promise<string> {
|
||||
return await windmill.UserService.login({
|
||||
|
||||
@@ -1836,6 +1836,247 @@ export interface Skips {
|
||||
includeKey?: boolean | undefined;
|
||||
}
|
||||
|
||||
// Detect paths (and their parent directories) within a single set that differ
|
||||
// only by letter case — e.g. a remote workspace that genuinely holds both
|
||||
// f/Caps/a and f/caps/b. On a case-insensitive filesystem (Windows, default
|
||||
// macOS) these cannot be represented as two distinct files/directories at all,
|
||||
// so we can only warn. Returns one group per collision, each listing the
|
||||
// distinct casings sorted for stable output.
|
||||
//
|
||||
// Only the shallowest clash is reported: when two case-variant folders also
|
||||
// contain same-named files (f/Caps/main.ts + f/caps/main.ts), the folder clash
|
||||
// is the root cause, so the nested per-file group is suppressed rather than
|
||||
// inflating the count with one entry per duplicated leaf.
|
||||
export function findCaseInsensitiveCollisions(
|
||||
paths: Iterable<string>,
|
||||
): string[][] {
|
||||
// lowercased prefix -> set of distinct original casings observed
|
||||
const byLower = new Map<string, Set<string>>();
|
||||
for (const full of paths) {
|
||||
// Compare on normalized forward-slash prefixes so a Windows-style "\" map
|
||||
// key and a remote "/" key collapse to the same prefix.
|
||||
const segs = full.split(/[\\/]/).filter((s) => s.length > 0);
|
||||
let acc = "";
|
||||
for (let i = 0; i < segs.length; i++) {
|
||||
// Accumulate every directory prefix as well as the full file path, so a
|
||||
// "f/Caps" vs "f/caps" folder clash is caught even when the leaf files
|
||||
// (e.g. a.ts vs b.ts) don't themselves collide.
|
||||
acc = i === 0 ? segs[i] : `${acc}/${segs[i]}`;
|
||||
const lower = acc.toLowerCase();
|
||||
let set = byLower.get(lower);
|
||||
if (!set) {
|
||||
set = new Set();
|
||||
byLower.set(lower, set);
|
||||
}
|
||||
set.add(acc);
|
||||
}
|
||||
}
|
||||
const collidingLowers = new Set<string>();
|
||||
for (const [lower, set] of byLower) {
|
||||
if (set.size > 1) collidingLowers.add(lower);
|
||||
}
|
||||
const collisions: string[][] = [];
|
||||
for (const lower of collidingLowers) {
|
||||
// Drop this group if any ancestor prefix is itself a collision — the
|
||||
// shallower folder clash already names the root cause.
|
||||
const parts = lower.split("/");
|
||||
let hasCollidingAncestor = false;
|
||||
for (let i = 1; i < parts.length; i++) {
|
||||
if (collidingLowers.has(parts.slice(0, i).join("/"))) {
|
||||
hasCollidingAncestor = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!hasCollidingAncestor) {
|
||||
collisions.push([...byLower.get(lower)!].sort());
|
||||
}
|
||||
}
|
||||
return collisions;
|
||||
}
|
||||
|
||||
type CaseTrieNode = {
|
||||
// lowercased segment -> child, recording the canonical (server) casing and
|
||||
// whether the server holds more than one casing of that segment (ambiguous).
|
||||
children: Map<
|
||||
string,
|
||||
{ canonical: string; ambiguous: boolean; node: CaseTrieNode }
|
||||
>;
|
||||
};
|
||||
|
||||
// Rewrite `localMap` keys to the canonical casing recorded on the server
|
||||
// (`remoteMap`) when they differ only by letter case. This is the core
|
||||
// WIN-2020 fix: on a case-insensitive filesystem a folder such as `f/Caps`
|
||||
// can have its on-disk casing silently drift (e.g. to `f/caps`) — Windows
|
||||
// stores whatever case the directory was first created with and reports that
|
||||
// from readdir, regardless of the server's path. Without this, the diff sees
|
||||
// the drifted local path as an entirely different item and emits a destructive
|
||||
// "delete f/Caps + add f/caps" pair, so a single capitalized folder appears to
|
||||
// vanish and a lowercase clone shows up out of nowhere. Adopting the server
|
||||
// casing collapses that phantom and leaves the canonical path on the server
|
||||
// untouched.
|
||||
//
|
||||
// Canonicalization is segment-by-segment against a trie of remote paths, so it
|
||||
// also applies the server's folder casing to brand-new local files that have no
|
||||
// exact remote match (e.g. adding f/caps/New.ts under a drifted f/Caps folder
|
||||
// becomes f/Caps/New.ts) — otherwise the push would recreate the case-only
|
||||
// collision the fix is meant to prevent. A segment is only adopted when the
|
||||
// server casing is unambiguous; at the first ambiguous or unknown segment the
|
||||
// remainder of the path keeps its local casing.
|
||||
//
|
||||
// Returns the rewritten map, the per-key rewrites, and any genuinely ambiguous
|
||||
// server-side groups (two distinct remote paths differing only by case) — those
|
||||
// can't be canonicalized to a single target and are left for the caller to warn
|
||||
// about.
|
||||
export function canonicalizeCaseInsensitiveKeys(
|
||||
localMap: Record<string, string>,
|
||||
remoteMap: Record<string, string>,
|
||||
): {
|
||||
map: Record<string, string>;
|
||||
ambiguous: string[][];
|
||||
rewritten: { from: string; to: string }[];
|
||||
} {
|
||||
const root: CaseTrieNode = { children: new Map() };
|
||||
for (const k of Object.keys(remoteMap)) {
|
||||
let node = root;
|
||||
for (const seg of k.split(/[\\/]/)) {
|
||||
if (seg.length === 0) continue;
|
||||
const lk = seg.toLowerCase();
|
||||
let entry = node.children.get(lk);
|
||||
if (!entry) {
|
||||
entry = { canonical: seg, ambiguous: false, node: { children: new Map() } };
|
||||
node.children.set(lk, entry);
|
||||
} else if (entry.canonical !== seg) {
|
||||
entry.ambiguous = true;
|
||||
}
|
||||
node = entry.node;
|
||||
}
|
||||
}
|
||||
|
||||
const out: Record<string, string> = {};
|
||||
const rewritten: { from: string; to: string }[] = [];
|
||||
for (const [k, v] of Object.entries(localMap)) {
|
||||
// Preserve the key's own separator style so the rewritten key still matches
|
||||
// the rest of the map (and round-trips through push) on every platform.
|
||||
const sep = k.includes("\\") ? "\\" : "/";
|
||||
const segs = k.split(/[\\/]/);
|
||||
const canonSegs: string[] = [];
|
||||
let node: CaseTrieNode | undefined = root;
|
||||
let changed = false;
|
||||
for (const seg of segs) {
|
||||
if (seg.length === 0) {
|
||||
canonSegs.push(seg);
|
||||
continue;
|
||||
}
|
||||
const entry = node?.children.get(seg.toLowerCase());
|
||||
if (entry && !entry.ambiguous) {
|
||||
if (entry.canonical !== seg) changed = true;
|
||||
canonSegs.push(entry.canonical);
|
||||
node = entry.node;
|
||||
} else {
|
||||
// No unambiguous server guidance for this segment: keep the local
|
||||
// casing here and below (deeper server structure is unknown).
|
||||
canonSegs.push(seg);
|
||||
node = undefined;
|
||||
}
|
||||
}
|
||||
const canonKey = canonSegs.join(sep);
|
||||
if (changed && canonKey !== k) {
|
||||
out[canonKey] = v;
|
||||
rewritten.push({ from: k, to: canonKey });
|
||||
} else {
|
||||
out[k] = v;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
map: out,
|
||||
ambiguous: findCaseInsensitiveCollisions(Object.keys(remoteMap)),
|
||||
rewritten,
|
||||
};
|
||||
}
|
||||
|
||||
// Summarize case-only key rewrites by their differing path prefix (typically a
|
||||
// folder such as f/caps -> f/Caps) so a folder whose casing drifted is reported
|
||||
// once instead of once per contained file.
|
||||
export function summarizeCaseRewrites(
|
||||
rewritten: { from: string; to: string }[],
|
||||
): string[] {
|
||||
const seen = new Set<string>();
|
||||
const out: string[] = [];
|
||||
for (const { from, to } of rewritten) {
|
||||
const fromSegs = from.split(/[\\/]/);
|
||||
const toSegs = to.split(/[\\/]/);
|
||||
// Find the shortest prefix at which the two casings first differ; that is
|
||||
// the folder (or file) whose casing actually changed.
|
||||
let i = 0;
|
||||
while (
|
||||
i < fromSegs.length &&
|
||||
i < toSegs.length &&
|
||||
fromSegs[i] === toSegs[i]
|
||||
) {
|
||||
i++;
|
||||
}
|
||||
const fromPrefix = fromSegs.slice(0, i + 1).join("/");
|
||||
const toPrefix = toSegs.slice(0, i + 1).join("/");
|
||||
const key = `${fromPrefix} -> ${toPrefix}`;
|
||||
if (!seen.has(key)) {
|
||||
seen.add(key);
|
||||
out.push(key);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Emit a single grouped warning for case-only collisions that cannot be
|
||||
// represented on a case-insensitive filesystem (two distinct server paths
|
||||
// differing only by case). Unlike the drift handled by
|
||||
// canonicalizeCaseInsensitiveKeys, these require the user to rename one side.
|
||||
function warnUnrepresentableCaseCollisions(collisions: string[][]): void {
|
||||
if (collisions.length === 0) return;
|
||||
const groups = collisions.map((g) => ` - ${g.join(" <-> ")}`).join("\n");
|
||||
log.warn(
|
||||
`Found ${collisions.length} path(s) that differ only by letter case:\n` +
|
||||
`${groups}\n` +
|
||||
`On case-insensitive filesystems (Windows, default macOS) these collapse ` +
|
||||
`into a single file/directory and cannot both be synced. Rename one side ` +
|
||||
`to a distinct path to make the tree sync reliably across platforms.`,
|
||||
);
|
||||
}
|
||||
|
||||
// Probe (and cache) whether `dir` lives on a case-insensitive filesystem.
|
||||
// Auto-detected by round-tripping a probe file under two casings, with an
|
||||
// explicit WMILL_CASE_INSENSITIVE_FS=true/false override so Windows behaviour
|
||||
// can be forced (or emulated for cross-platform repos / tests) on any host.
|
||||
let _caseInsensitiveFsCache: boolean | undefined;
|
||||
export async function isCaseInsensitiveFilesystem(
|
||||
dir: string,
|
||||
): Promise<boolean> {
|
||||
const override = (process.env.WMILL_CASE_INSENSITIVE_FS ?? "")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
if (override === "true" || override === "1") return true;
|
||||
if (override === "false" || override === "0") return false;
|
||||
if (_caseInsensitiveFsCache !== undefined) return _caseInsensitiveFsCache;
|
||||
let result = false;
|
||||
try {
|
||||
const upper = path.join(dir, `.wmill-CASEPROBE-${process.pid}.tmp`);
|
||||
const lower = path.join(dir, `.wmill-caseprobe-${process.pid}.tmp`);
|
||||
await writeFile(upper, "", "utf-8");
|
||||
try {
|
||||
await stat(lower);
|
||||
result = true; // lowercase name resolves to the file we wrote uppercase
|
||||
} catch {
|
||||
result = false;
|
||||
}
|
||||
await rm(upper).catch(() => {});
|
||||
await rm(lower).catch(() => {});
|
||||
} catch {
|
||||
result = false;
|
||||
}
|
||||
_caseInsensitiveFsCache = result;
|
||||
return result;
|
||||
}
|
||||
|
||||
async function compareDynFSElement(
|
||||
els1: DynFSElement,
|
||||
els2: DynFSElement | undefined,
|
||||
@@ -1848,14 +2089,55 @@ async function compareDynFSElement(
|
||||
specificItems?: SpecificItemsConfig,
|
||||
branchOverride?: string,
|
||||
isEls1Remote?: boolean,
|
||||
caseInsensitiveFs?: boolean,
|
||||
): Promise<{ changes: Change[]; localMap: Record<string, string> }> {
|
||||
const [m1, m2] = els2
|
||||
let [m1, m2] = els2
|
||||
? await Promise.all([
|
||||
elementsToMap(els1, ignore, json, skips, specificItems, branchOverride, isEls1Remote),
|
||||
elementsToMap(els2, ignore, json, skips, specificItems, branchOverride, !isEls1Remote),
|
||||
])
|
||||
: [await elementsToMap(els1, ignore, json, skips, specificItems, branchOverride, isEls1Remote), {}];
|
||||
|
||||
// Reconcile letter-case differences between the local tree and the
|
||||
// authoritative server casing. Only meaningful for an actual two-sided diff
|
||||
// (els2 defined) where we know which side is the remote.
|
||||
if (els2 && isEls1Remote !== undefined) {
|
||||
const remoteMap = isEls1Remote ? m1 : m2;
|
||||
|
||||
// Always warn about server paths that differ only by case (e.g. f/Caps and
|
||||
// f/caps as two distinct items). These cannot coexist on a case-insensitive
|
||||
// filesystem, so flag them on every platform — a Linux author needs to know
|
||||
// their tree won't round-trip for a Windows/macOS teammate.
|
||||
warnUnrepresentableCaseCollisions(
|
||||
findCaseInsensitiveCollisions(Object.keys(remoteMap)),
|
||||
);
|
||||
|
||||
// On a case-insensitive filesystem, the local on-disk casing of a folder
|
||||
// can drift from the server's (Windows reports the case the directory was
|
||||
// first created with). Rewrite those drifted local keys to the server
|
||||
// casing so the diff treats them as the same item instead of a destructive
|
||||
// delete+add pair. This is the WIN-2020 fix.
|
||||
if (caseInsensitiveFs) {
|
||||
const { map, rewritten } = canonicalizeCaseInsensitiveKeys(
|
||||
isEls1Remote ? m2 : m1,
|
||||
remoteMap,
|
||||
);
|
||||
if (isEls1Remote) {
|
||||
m2 = map;
|
||||
} else {
|
||||
m1 = map;
|
||||
}
|
||||
const summary = summarizeCaseRewrites(rewritten);
|
||||
if (summary.length > 0) {
|
||||
log.info(
|
||||
`Reconciled ${summary.length} local path(s) to the server's casing ` +
|
||||
`(case-insensitive filesystem):\n` +
|
||||
summary.map((s) => ` ${s}`).join("\n"),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const changes: Change[] = [];
|
||||
|
||||
function parseYaml(k: string, v: string) {
|
||||
@@ -2613,6 +2895,7 @@ export async function pull(
|
||||
specificItems,
|
||||
wsNameForFiles,
|
||||
true, // els1 (remote) is the remote source
|
||||
await isCaseInsensitiveFilesystem(process.cwd()),
|
||||
);
|
||||
|
||||
log.info(
|
||||
@@ -3182,6 +3465,14 @@ function removeSuffix(str: string, suffix: string) {
|
||||
return str.slice(0, str.length - suffix.length);
|
||||
}
|
||||
|
||||
// Shown after a `wmill sync push --dry-run` preview that has changes. `sync push`
|
||||
// deploys to the remote workspace and is destructive (it overwrites and prunes
|
||||
// remote items that differ from or are absent locally), so the preview reminds
|
||||
// the caller — especially an AI agent that ran the dry-run to inspect changes —
|
||||
// to get explicit user confirmation before applying it for real.
|
||||
const SYNC_PUSH_DESTRUCTIVE_WARNING =
|
||||
"`wmill sync push` is destructive: applying it deploys these changes to the remote workspace and overwrites or deletes remote items that differ from or are absent locally — this is not automatically reversible. If you are an AI agent, do NOT run `wmill sync push` (without --dry-run) until the user has explicitly confirmed this deploy, unless your custom instructions explicitly allow bypassing that confirmation.";
|
||||
|
||||
export async function push(
|
||||
opts: GlobalOptions & SyncOptions & { repository?: string; branch?: string; acceptOverridingPermissionedAsWithSelf?: boolean },
|
||||
) {
|
||||
@@ -3361,6 +3652,7 @@ export async function push(
|
||||
specificItems,
|
||||
wsNameForFiles,
|
||||
false, // els1 (local) is not the remote source
|
||||
await isCaseInsensitiveFilesystem(process.cwd()),
|
||||
);
|
||||
|
||||
// Detect resources/variables that the local config flags as ws_specific
|
||||
@@ -3717,6 +4009,9 @@ export async function push(
|
||||
: {}),
|
||||
})),
|
||||
total: changes.length,
|
||||
...(changes.length > 0
|
||||
? { warning: SYNC_PUSH_DESTRUCTIVE_WARNING }
|
||||
: {}),
|
||||
};
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
return;
|
||||
@@ -3760,6 +4055,7 @@ export async function push(
|
||||
|
||||
if (opts.dryRun) {
|
||||
log.info(colors.gray(`Dry run complete.`));
|
||||
log.warn(colors.yellow(`\n⚠ ${SYNC_PUSH_DESTRUCTIVE_WARNING}`));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,4 +10,4 @@ export const WM_FORK_PREFIX = "wm-fork";
|
||||
// (e.g. utils.ts) can read it without importing main.ts and creating a circular
|
||||
// dependency (main → workspace → utils → main) that triggers a TDZ.
|
||||
// Re-exported from main.ts for backwards compatibility.
|
||||
export const VERSION = "1.720.0";
|
||||
export const VERSION = "1.721.0";
|
||||
|
||||
@@ -5266,10 +5266,10 @@ Text/HTML/inline parts are placed inline in \`body\` as strings.
|
||||
|
||||
## CLI Commands
|
||||
|
||||
After writing, tell the user they can run these commands (do NOT run them yourself):
|
||||
\`wmill sync push\` deploys local changes to the workspace and can be destructive to remote state — only suggest/run it when the user explicitly asks to deploy/publish/push, not when they say "run", "try", or "test". \`sync pull\` is safe to run yourself — it never mutates remote state, though it does overwrite local files to match the remote (use \`sync pull --dry-run\` to only preview).
|
||||
|
||||
\`\`\`bash
|
||||
# Push trigger configuration
|
||||
# Push trigger configuration — only when the user explicitly asks to deploy
|
||||
wmill sync push
|
||||
|
||||
# Pull triggers from Windmill
|
||||
@@ -5317,10 +5317,10 @@ Windmill uses 6-field cron expressions (includes seconds):
|
||||
|
||||
## CLI Commands
|
||||
|
||||
After writing, tell the user they can run these commands (do NOT run them yourself):
|
||||
\`wmill sync push\` deploys local changes to the workspace and can be destructive to remote state — only suggest/run it when the user explicitly asks to deploy/publish/push, not when they say "run", "try", or "test". The commands below never mutate remote state, so they're safe to run yourself — note that \`sync pull\` does overwrite local files to match the remote (use \`sync pull --dry-run\` to only preview), while \`schedule\` just lists.
|
||||
|
||||
\`\`\`bash
|
||||
# Push schedules to Windmill
|
||||
# Push schedules to Windmill — only when the user explicitly asks to deploy
|
||||
wmill sync push
|
||||
|
||||
# Pull schedules from Windmill
|
||||
@@ -5574,7 +5574,8 @@ wmill resource-type list --schema
|
||||
# Get specific resource type schema
|
||||
wmill resource-type get postgresql
|
||||
|
||||
# Push resources (tell the user to run this, do NOT run it yourself)
|
||||
# Push resources to Windmill — deploys to the workspace and can be destructive to
|
||||
# remote state, so only run it when the user explicitly asks to deploy/publish/push
|
||||
wmill sync push
|
||||
\`\`\`
|
||||
`,
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
import { expect, test } from "bun:test";
|
||||
|
||||
import {
|
||||
findCaseInsensitiveCollisions,
|
||||
canonicalizeCaseInsensitiveKeys,
|
||||
summarizeCaseRewrites,
|
||||
} from "../src/commands/sync/sync.ts";
|
||||
|
||||
// =============================================================================
|
||||
// Case-insensitive sync handling (WIN-2020)
|
||||
//
|
||||
// Windmill paths are case-sensitive, but Windows and the default macOS setup
|
||||
// use case-insensitive filesystems. The real-world failure is NOT a user
|
||||
// deliberately authoring both f/Caps and f/caps — it is a single capitalized
|
||||
// folder whose on-disk casing drifts (Windows reports the case the directory
|
||||
// was first created with), so the diff sees a brand-new lowercase path and a
|
||||
// destructive delete of the real one. These tests pin:
|
||||
// 1. findCaseInsensitiveCollisions — warn about genuinely unrepresentable
|
||||
// server-side collisions (two distinct remote paths differing only by
|
||||
// case).
|
||||
// 2. canonicalizeCaseInsensitiveKeys — the fix: rewrite drifted local keys
|
||||
// to the server's casing so no phantom delete+add is produced.
|
||||
// =============================================================================
|
||||
|
||||
function normalize(groups: string[][]): string[][] {
|
||||
return groups
|
||||
.map((g) => [...g])
|
||||
.sort((a, b) => a.join().localeCompare(b.join()));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// findCaseInsensitiveCollisions
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test("collision detection: sibling folders differing only by case", () => {
|
||||
const collisions = findCaseInsensitiveCollisions([
|
||||
"f/Caps/a.script.ts",
|
||||
"f/caps/b.script.ts",
|
||||
]);
|
||||
expect(normalize(collisions)).toEqual([["f/Caps", "f/caps"]]);
|
||||
});
|
||||
|
||||
test("collision detection: leaf files differing only by case", () => {
|
||||
const collisions = findCaseInsensitiveCollisions([
|
||||
"f/team/Report.script.ts",
|
||||
"f/team/report.script.ts",
|
||||
]);
|
||||
expect(normalize(collisions)).toEqual([
|
||||
["f/team/Report.script.ts", "f/team/report.script.ts"],
|
||||
]);
|
||||
});
|
||||
|
||||
test("collision detection: none for case-consistent distinct paths", () => {
|
||||
const collisions = findCaseInsensitiveCollisions([
|
||||
"f/caps/foo.script.ts",
|
||||
"f/caps/bar.script.ts",
|
||||
"f/other/baz.script.ts",
|
||||
]);
|
||||
expect(collisions).toEqual([]);
|
||||
});
|
||||
|
||||
test("collision detection: normalizes mixed forward/back slashes", () => {
|
||||
const collisions = findCaseInsensitiveCollisions([
|
||||
"f\\Caps\\a.script.ts",
|
||||
"f/caps/b.script.ts",
|
||||
]);
|
||||
expect(normalize(collisions)).toEqual([["f/Caps", "f/caps"]]);
|
||||
});
|
||||
|
||||
test("collision detection: reports only the shallowest clash for same-named leaves", () => {
|
||||
// Two case-variant folders that ALSO hold a same-named file must report the
|
||||
// single folder clash, not the folder group plus a nested per-file group.
|
||||
const collisions = findCaseInsensitiveCollisions([
|
||||
"f/Caps/main.script.ts",
|
||||
"f/caps/main.script.ts",
|
||||
]);
|
||||
expect(normalize(collisions)).toEqual([["f/Caps", "f/caps"]]);
|
||||
});
|
||||
|
||||
test("collision detection: groups three distinct casings together", () => {
|
||||
const collisions = findCaseInsensitiveCollisions([
|
||||
"f/caps/a.script.ts",
|
||||
"f/CAPS/b.script.ts",
|
||||
"f/Caps/c.script.ts",
|
||||
]);
|
||||
expect(normalize(collisions)).toEqual([["f/CAPS", "f/Caps", "f/caps"]]);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// canonicalizeCaseInsensitiveKeys (the WIN-2020 fix)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test("canonicalize: drifted local folder casing adopts the server casing", () => {
|
||||
// Server (remote) is authoritative: f/Caps. The local tree drifted to
|
||||
// f/caps on a case-insensitive FS.
|
||||
const remote = {
|
||||
"f/Caps/x.script.ts": "content",
|
||||
"f/Caps/x.script.yaml": "meta",
|
||||
};
|
||||
const local = {
|
||||
"f/caps/x.script.ts": "content",
|
||||
"f/caps/x.script.yaml": "meta",
|
||||
};
|
||||
const { map, ambiguous, rewritten } = canonicalizeCaseInsensitiveKeys(
|
||||
local,
|
||||
remote,
|
||||
);
|
||||
// Local keys are rewritten to the server casing, so a subsequent exact-key
|
||||
// diff sees identical paths — no phantom delete+add.
|
||||
expect(Object.keys(map).sort()).toEqual([
|
||||
"f/Caps/x.script.ts",
|
||||
"f/Caps/x.script.yaml",
|
||||
]);
|
||||
expect(ambiguous).toEqual([]);
|
||||
expect(rewritten).toEqual([
|
||||
{ from: "f/caps/x.script.ts", to: "f/Caps/x.script.ts" },
|
||||
{ from: "f/caps/x.script.yaml", to: "f/Caps/x.script.yaml" },
|
||||
]);
|
||||
});
|
||||
|
||||
test("canonicalize: preserves content values while rewriting keys", () => {
|
||||
const remote = { "f/MyFolder/Script.script.ts": "remote" };
|
||||
const local = { "f/myfolder/Script.script.ts": "LOCAL EDIT" };
|
||||
const { map } = canonicalizeCaseInsensitiveKeys(local, remote);
|
||||
expect(map["f/MyFolder/Script.script.ts"]).toEqual("LOCAL EDIT");
|
||||
expect(map["f/myfolder/Script.script.ts"]).toBeUndefined();
|
||||
});
|
||||
|
||||
test("canonicalize: leaves keys with no case-insensitive remote match", () => {
|
||||
const remote = { "f/Caps/x.script.ts": "a" };
|
||||
const local = {
|
||||
"f/Caps/x.script.ts": "a",
|
||||
"f/brand_new/y.script.ts": "b", // genuinely local-only add
|
||||
};
|
||||
const { map, rewritten } = canonicalizeCaseInsensitiveKeys(local, remote);
|
||||
expect(rewritten).toEqual([]);
|
||||
expect(Object.keys(map).sort()).toEqual([
|
||||
"f/Caps/x.script.ts",
|
||||
"f/brand_new/y.script.ts",
|
||||
]);
|
||||
});
|
||||
|
||||
test("canonicalize: does NOT rewrite when the server casing is ambiguous", () => {
|
||||
// The server itself holds two paths differing only by case — we must not
|
||||
// silently pick one. Leave the local key untouched and report the ambiguity.
|
||||
const remote = {
|
||||
"f/Caps/x.script.ts": "a",
|
||||
"f/caps/x.script.ts": "b",
|
||||
};
|
||||
const local = { "f/CAPS/x.script.ts": "local" };
|
||||
const { map, ambiguous, rewritten } = canonicalizeCaseInsensitiveKeys(
|
||||
local,
|
||||
remote,
|
||||
);
|
||||
expect(rewritten).toEqual([]);
|
||||
expect(Object.keys(map)).toEqual(["f/CAPS/x.script.ts"]);
|
||||
// Reported as the shallowest (folder) clash, not the nested per-file group.
|
||||
expect(normalize(ambiguous)).toEqual([["f/Caps", "f/caps"]]);
|
||||
});
|
||||
|
||||
test("canonicalize: new local file under a drifted folder adopts the server folder casing", () => {
|
||||
// Regression for the P1 review finding: a brand-new local file has no exact
|
||||
// remote match, but it still lives under a folder whose casing drifted. It
|
||||
// must inherit the server's folder casing (f/Caps), otherwise push would
|
||||
// create f/caps/New beside f/Caps/* and reintroduce the case-only collision.
|
||||
const remote = { "f/Caps/Existing.script.ts": "remote" };
|
||||
const local = {
|
||||
"f/caps/Existing.script.ts": "remote", // drifted, existing
|
||||
"f/caps/New.script.ts": "brand new", // drifted folder, local-only file
|
||||
};
|
||||
const { map, rewritten } = canonicalizeCaseInsensitiveKeys(local, remote);
|
||||
expect(Object.keys(map).sort()).toEqual([
|
||||
"f/Caps/Existing.script.ts",
|
||||
"f/Caps/New.script.ts",
|
||||
]);
|
||||
expect(map["f/Caps/New.script.ts"]).toEqual("brand new");
|
||||
expect(rewritten).toContainEqual({
|
||||
from: "f/caps/New.script.ts",
|
||||
to: "f/Caps/New.script.ts",
|
||||
});
|
||||
});
|
||||
|
||||
test("canonicalize: stops at the first segment with no server guidance", () => {
|
||||
// Only the folder prefix that exists on the server is canonicalized; deeper
|
||||
// local-only directories keep their own casing.
|
||||
const remote = { "f/Caps/x.script.ts": "a" };
|
||||
const local = { "f/caps/SubDir/y.script.ts": "b" };
|
||||
const { map } = canonicalizeCaseInsensitiveKeys(local, remote);
|
||||
expect(Object.keys(map)).toEqual(["f/Caps/SubDir/y.script.ts"]);
|
||||
});
|
||||
|
||||
test("canonicalize: identical casing is a no-op", () => {
|
||||
const remote = { "f/Caps/x.script.ts": "a" };
|
||||
const local = { "f/Caps/x.script.ts": "a" };
|
||||
const { map, rewritten, ambiguous } = canonicalizeCaseInsensitiveKeys(
|
||||
local,
|
||||
remote,
|
||||
);
|
||||
expect(rewritten).toEqual([]);
|
||||
expect(ambiguous).toEqual([]);
|
||||
expect(map).toEqual({ "f/Caps/x.script.ts": "a" });
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// summarizeCaseRewrites
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test("summarize: collapses per-file rewrites into one folder entry", () => {
|
||||
const summary = summarizeCaseRewrites([
|
||||
{ from: "f/caps/x.script.ts", to: "f/Caps/x.script.ts" },
|
||||
{ from: "f/caps/x.script.yaml", to: "f/Caps/x.script.yaml" },
|
||||
{ from: "f/caps/y.script.ts", to: "f/Caps/y.script.ts" },
|
||||
]);
|
||||
expect(summary).toEqual(["f/caps -> f/Caps"]);
|
||||
});
|
||||
|
||||
test("summarize: reports a leaf-file casing change at file granularity", () => {
|
||||
const summary = summarizeCaseRewrites([
|
||||
{ from: "f/team/report.script.ts", to: "f/team/Report.script.ts" },
|
||||
]);
|
||||
expect(summary).toEqual([
|
||||
"f/team/report.script.ts -> f/team/Report.script.ts",
|
||||
]);
|
||||
});
|
||||
|
||||
test("summarize: reports independent folder drifts separately", () => {
|
||||
const summary = summarizeCaseRewrites([
|
||||
{ from: "f/caps/x.script.ts", to: "f/Caps/x.script.ts" },
|
||||
{ from: "u/alice/y.script.ts", to: "u/Alice/y.script.ts" },
|
||||
]);
|
||||
expect(summary.sort()).toEqual(["f/caps -> f/Caps", "u/alice -> u/Alice"]);
|
||||
});
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
import { expect, test } from "bun:test";
|
||||
import * as path from "node:path";
|
||||
import { writeFile, readFile, stat } from "node:fs/promises";
|
||||
import { writeFile, readFile, stat, rename } from "node:fs/promises";
|
||||
import { withTestBackend } from "./test_backend.ts";
|
||||
import { addWorkspace } from "../workspace.ts";
|
||||
import { parseJsonFromCLIOutput } from "./test_config_helpers.ts";
|
||||
@@ -686,6 +686,173 @@ excludes: []
|
||||
});
|
||||
});
|
||||
|
||||
// The core WIN-2020 reproduction. The real failure is NOT a user authoring
|
||||
// both f/Caps and f/caps — it is a SINGLE capitalized folder whose on-disk
|
||||
// casing drifts on a case-insensitive filesystem (Windows stores/reports the
|
||||
// case the directory was first created with). The diff then sees a brand-new
|
||||
// lowercase path plus a destructive delete of the real one.
|
||||
//
|
||||
// This test runs on BOTH the Linux and Windows CI jobs:
|
||||
// - On the Windows runner (real case-insensitive NTFS) the CLI auto-detects
|
||||
// case-insensitivity via its filesystem probe — no override, real behavior.
|
||||
// - On Linux (case-sensitive) we reproduce the drift with an explicit rename
|
||||
// and force the same code path with WMILL_CASE_INSENSITIVE_FS=true, and we
|
||||
// additionally assert that WITHOUT the fix the destructive phantom appears
|
||||
// (which can only be observed on a case-sensitive FS).
|
||||
test("Mixed Case Paths: case-only folder drift reconciles to server casing (WIN-2020)", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
await createFolder(backend, "Caps");
|
||||
await createScript(
|
||||
backend,
|
||||
"f/Caps/MyScript",
|
||||
'export async function main() { return "drift repro"; }',
|
||||
"Drift Script"
|
||||
);
|
||||
|
||||
await writeFile(
|
||||
path.join(tempDir, "wmill.yaml"),
|
||||
`defaultTs: bun
|
||||
includes:
|
||||
- "**"
|
||||
excludes: []
|
||||
`,
|
||||
"utf-8"
|
||||
);
|
||||
|
||||
const pullResult = await backend.runCLICommand(["sync", "pull", "--yes"], tempDir);
|
||||
expect(pullResult.code).toEqual(0);
|
||||
|
||||
const upperDir = path.join(tempDir, "f", "Caps");
|
||||
const lowerDir = path.join(tempDir, "f", "caps");
|
||||
const pulledExists = await stat(path.join(upperDir, "MyScript.ts"))
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
expect(pulledExists).toBeTruthy();
|
||||
|
||||
// Simulate the on-disk casing drift (a no-op-in-spirit case-only rename).
|
||||
// On Windows this is a real case-only rename of the same directory; on
|
||||
// Linux it produces a genuinely lowercase sibling.
|
||||
await rename(upperDir, lowerDir);
|
||||
|
||||
const onWindows = process.platform === "win32";
|
||||
|
||||
// Without case-insensitive handling, the drift is a destructive
|
||||
// delete(f/Caps) + add(f/caps) phantom. Only observable on a
|
||||
// case-sensitive FS (Linux), where the override defaults off.
|
||||
if (!onWindows) {
|
||||
const buggy = await backend.runCLICommand(
|
||||
["sync", "push", "--yes", "--dry-run", "--json-output"],
|
||||
tempDir
|
||||
);
|
||||
expect(buggy.code).toEqual(0);
|
||||
const buggyChanges = parseJsonFromCLIOutput(buggy.stdout).changes || [];
|
||||
const hasDelete = buggyChanges.some(
|
||||
(c: any) => c.type === "deleted" && c.path.replace(/\\/g, "/").startsWith("f/Caps/")
|
||||
);
|
||||
const hasAdd = buggyChanges.some(
|
||||
(c: any) => c.type === "added" && c.path.replace(/\\/g, "/").startsWith("f/caps/")
|
||||
);
|
||||
expect(hasDelete).toBeTruthy();
|
||||
expect(hasAdd).toBeTruthy();
|
||||
}
|
||||
|
||||
// With case-insensitive handling in effect (auto on Windows via the FS
|
||||
// probe, forced via env on Linux) the drifted local casing is reconciled
|
||||
// to the server's casing, so the push is a clean no-op.
|
||||
if (!onWindows) process.env.WMILL_CASE_INSENSITIVE_FS = "true";
|
||||
try {
|
||||
const fixed = await backend.runCLICommand(
|
||||
["sync", "push", "--yes", "--dry-run", "--json-output"],
|
||||
tempDir
|
||||
);
|
||||
expect(fixed.code).toEqual(0);
|
||||
const fixedChanges = parseJsonFromCLIOutput(fixed.stdout).changes || [];
|
||||
if (fixedChanges.length !== 0) {
|
||||
console.error(
|
||||
"Expected no changes after reconciliation, got:",
|
||||
JSON.stringify(fixedChanges, null, 2)
|
||||
);
|
||||
}
|
||||
expect(fixedChanges.length).toEqual(0);
|
||||
|
||||
// P1 regression: a brand-new item added under the drifted (lowercase)
|
||||
// folder must be pushed under the server's folder casing (f/Caps), not
|
||||
// recreate the case-only collision as f/caps. A self-contained variable
|
||||
// YAML is used so the assertion targets path canonicalization without
|
||||
// dragging in script lock/metadata generation.
|
||||
await writeFile(
|
||||
path.join(lowerDir, "NewVar.variable.yaml"),
|
||||
`value: hello\nis_secret: false\ndescription: new under drifted folder\nis_oauth: false\n`,
|
||||
"utf-8"
|
||||
);
|
||||
const added = await backend.runCLICommand(
|
||||
["sync", "push", "--yes", "--dry-run", "--json-output"],
|
||||
tempDir
|
||||
);
|
||||
expect(added.code).toEqual(0);
|
||||
const addedChanges = parseJsonFromCLIOutput(added.stdout).changes || [];
|
||||
const addedPaths = addedChanges.map((c: any) =>
|
||||
c.path.replace(/\\/g, "/")
|
||||
);
|
||||
expect(
|
||||
addedPaths.some((p: string) => p === "f/Caps/NewVar.variable.yaml")
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
addedPaths.some((p: string) => p.startsWith("f/caps/"))
|
||||
).toBeFalsy();
|
||||
} finally {
|
||||
if (!onWindows) delete process.env.WMILL_CASE_INSENSITIVE_FS;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// A genuine, unrepresentable server-side collision: two DISTINCT server folders
|
||||
// that differ only by case (f/Caps and f/caps). These cannot both exist on a
|
||||
// case-insensitive filesystem, so the CLI must warn. Skipped on Windows, where
|
||||
// the pull physically cannot lay both folders down; the warning string itself
|
||||
// is exercised platform-independently by the unit tests.
|
||||
const collisionTest = process.platform === "win32" ? test.skip : test;
|
||||
collisionTest("Mixed Case Paths: distinct server folders differing only by case warn", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
await createFolder(backend, "Caps");
|
||||
await createFolder(backend, "caps");
|
||||
await createScript(
|
||||
backend,
|
||||
"f/Caps/upper",
|
||||
'export async function main() { return "upper"; }',
|
||||
"Upper"
|
||||
);
|
||||
await createScript(
|
||||
backend,
|
||||
"f/caps/lower",
|
||||
'export async function main() { return "lower"; }',
|
||||
"Lower"
|
||||
);
|
||||
|
||||
await writeFile(
|
||||
path.join(tempDir, "wmill.yaml"),
|
||||
`defaultTs: bun
|
||||
includes:
|
||||
- "**"
|
||||
excludes: []
|
||||
`,
|
||||
"utf-8"
|
||||
);
|
||||
|
||||
const pullResult = await backend.runCLICommand(["sync", "pull", "--yes"], tempDir);
|
||||
expect(pullResult.code).toEqual(0);
|
||||
|
||||
const combinedOutput = pullResult.stdout + pullResult.stderr;
|
||||
expect(combinedOutput.includes("differ only by letter case")).toBeTruthy();
|
||||
expect(combinedOutput.includes("f/Caps")).toBeTruthy();
|
||||
expect(combinedOutput.includes("f/caps")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
test("Mixed Case Paths: CamelCase folder names with numbers", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@windmill-labs/components",
|
||||
"version": "1.720.0",
|
||||
"version": "1.721.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@windmill-labs/components",
|
||||
"version": "1.720.0",
|
||||
"version": "1.721.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@windmill-labs/components",
|
||||
"version": "1.720.0",
|
||||
"version": "1.721.0",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"dev:ui-builder": "mv static/ui_builder static/ui_builder.dev-disabled 2>/dev/null || true ; trap 'mv static/ui_builder.dev-disabled static/ui_builder 2>/dev/null || true' EXIT ; vite dev",
|
||||
|
||||
@@ -67,7 +67,40 @@ vi.mock('$lib/gen', async () => {
|
||||
success: true,
|
||||
result: { ok: true },
|
||||
logs: 'test logs'
|
||||
}))
|
||||
})),
|
||||
getJobLogs: vi.fn(async () => 'job log line 1\njob log line 2'),
|
||||
listJobs: vi.fn(async () => [
|
||||
{
|
||||
type: 'CompletedJob',
|
||||
id: 'completed-1',
|
||||
job_kind: 'script',
|
||||
script_path: 'f/team/runner',
|
||||
created_by: 'alice',
|
||||
created_at: '2026-06-09T10:00:00Z',
|
||||
started_at: '2026-06-09T10:00:01Z',
|
||||
duration_ms: 1200,
|
||||
success: true,
|
||||
canceled: false,
|
||||
is_flow_step: false,
|
||||
tag: 'default',
|
||||
// fields that must be stripped from the summary
|
||||
logs: 'verbose logs',
|
||||
args: { secret: 'do-not-leak' },
|
||||
result: { value: 42 }
|
||||
},
|
||||
{
|
||||
type: 'QueuedJob',
|
||||
id: 'queued-1',
|
||||
job_kind: 'flow',
|
||||
script_path: 'f/team/pipeline',
|
||||
created_by: 'bob',
|
||||
created_at: '2026-06-09T10:05:00Z',
|
||||
running: true,
|
||||
canceled: false,
|
||||
is_flow_step: false,
|
||||
tag: 'default'
|
||||
}
|
||||
])
|
||||
}),
|
||||
FlowService: wrapService(actual.FlowService, {
|
||||
existsFlowByPath: vi.fn(async () => false),
|
||||
@@ -238,6 +271,78 @@ describe('global AI tools', () => {
|
||||
expect(names).toContain('test_run_script')
|
||||
expect(names).toContain('test_run_flow')
|
||||
expect(names).toContain('test_run_step')
|
||||
expect(names).toContain('get_job_logs')
|
||||
expect(names).toContain('list_runs')
|
||||
})
|
||||
|
||||
it('lists recent runs with compact summaries and forwarded filters', async () => {
|
||||
const result = await callGlobalTool('list_runs', {
|
||||
path: 'f/team/runner',
|
||||
created_by: 'alice',
|
||||
success: true,
|
||||
limit: 10
|
||||
})
|
||||
|
||||
expect(JobService.listJobs).toHaveBeenCalledWith({
|
||||
workspace: WORKSPACE,
|
||||
scriptPathExact: 'f/team/runner',
|
||||
createdBy: 'alice',
|
||||
label: undefined,
|
||||
success: true,
|
||||
running: undefined,
|
||||
perPage: 10
|
||||
})
|
||||
|
||||
const runs = JSON.parse(result)
|
||||
expect(runs).toHaveLength(2)
|
||||
expect(runs[0]).toMatchObject({
|
||||
id: 'completed-1',
|
||||
status: 'success',
|
||||
path: 'f/team/runner',
|
||||
duration_ms: 1200
|
||||
})
|
||||
expect(runs[1]).toMatchObject({ id: 'queued-1', status: 'running' })
|
||||
// Heavy / sensitive fields must not leak into the summary.
|
||||
expect(result).not.toContain('verbose logs')
|
||||
expect(result).not.toContain('do-not-leak')
|
||||
// The result must be surfaced to the tool display, otherwise the details
|
||||
// panel shows "No result yet" even though the call succeeded.
|
||||
expect(toolCallbacks.setToolStatus).toHaveBeenCalledWith(
|
||||
'test-list_runs',
|
||||
expect.objectContaining({ result })
|
||||
)
|
||||
})
|
||||
|
||||
it('defaults list_runs to 30 results when no limit is given', async () => {
|
||||
await callGlobalTool('list_runs', {})
|
||||
expect(JobService.listJobs).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ workspace: WORKSPACE, perPage: 30 })
|
||||
)
|
||||
})
|
||||
|
||||
it('fetches job logs by id and always suppresses the backend ansi hint line', async () => {
|
||||
const result = await callGlobalTool('get_job_logs', { id: 'job-123' })
|
||||
|
||||
expect(JobService.getJobLogs).toHaveBeenCalledWith({
|
||||
workspace: WORKSPACE,
|
||||
id: 'job-123',
|
||||
removeAnsiWarnings: true
|
||||
})
|
||||
expect(result).toBe('job log line 1\njob log line 2')
|
||||
// The logs must be surfaced as the tool result so the details panel shows
|
||||
// them rather than "No result yet".
|
||||
expect(toolCallbacks.setToolStatus).toHaveBeenCalledWith(
|
||||
'test-get_job_logs',
|
||||
expect.objectContaining({ result: 'job log line 1\njob log line 2' })
|
||||
)
|
||||
})
|
||||
|
||||
it('reports when a job has no logs', async () => {
|
||||
vi.mocked(JobService.getJobLogs).mockResolvedValueOnce(' ')
|
||||
|
||||
const result = await callGlobalTool('get_job_logs', { id: 'job-empty' })
|
||||
|
||||
expect(result).toBe('No logs available for this job.')
|
||||
})
|
||||
|
||||
it('searches hub scripts without fetching script contents', async () => {
|
||||
|
||||
@@ -23,6 +23,7 @@ import type {
|
||||
CreateVariable,
|
||||
Flow,
|
||||
FlowValue,
|
||||
Job,
|
||||
ListableApp,
|
||||
ListableResource,
|
||||
ListableVariable,
|
||||
@@ -140,10 +141,10 @@ const ACTIVE_GLOBAL_EDITOR_DRAFTS: readonly {
|
||||
itemKind: LiveEditorDraftKind
|
||||
type: ActiveGlobalEditorType
|
||||
}[] = [
|
||||
{ itemKind: 'script', type: 'script' },
|
||||
{ itemKind: 'flow', type: 'flow' },
|
||||
{ itemKind: 'raw_app', type: 'app' }
|
||||
]
|
||||
{ itemKind: 'script', type: 'script' },
|
||||
{ itemKind: 'flow', type: 'flow' },
|
||||
{ itemKind: 'raw_app', type: 'app' }
|
||||
]
|
||||
|
||||
export type GlobalActiveEditorContext = {
|
||||
type: ActiveGlobalEditorType
|
||||
@@ -348,6 +349,34 @@ const searchResourceTypesSchema = z.object({
|
||||
.describe('Max number of resource types to return. Defaults to 5.')
|
||||
})
|
||||
|
||||
const getJobLogsSchema = z.object({
|
||||
id: z.string().describe('The UUID of the job to fetch logs for.')
|
||||
})
|
||||
|
||||
const listRunsSchema = z.object({
|
||||
path: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('Filter to runs of this exact script or flow path.'),
|
||||
created_by: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('Filter by the username that started the run.'),
|
||||
label: z.string().optional().describe('Filter by job label.'),
|
||||
success: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe('Only completed runs with this outcome (true = succeeded, false = failed).'),
|
||||
running: z.boolean().optional().describe('Only currently running runs.'),
|
||||
limit: z
|
||||
.number()
|
||||
.int()
|
||||
.min(1)
|
||||
.max(100)
|
||||
.optional()
|
||||
.describe('Max number of runs to return, most recent first. Defaults to 30.')
|
||||
})
|
||||
|
||||
const deleteWorkspaceItemSchema = z.object({
|
||||
type: itemTypeSchema,
|
||||
path: z.string().describe('Workspace path of the item to delete.'),
|
||||
@@ -602,13 +631,13 @@ Rules:
|
||||
- Use search_resource_types before write_resource.
|
||||
- Use get_instructions before writing scripts, flows, resources, or apps. For scripts, pass the target language.
|
||||
- After creating or editing a script or flow draft, run test_run_script, test_run_flow, or test_run_step with representative args before reporting that it works. These tools prefer local drafts, so testing does not require deployment.
|
||||
- Use list_runs to find recent runs (optionally filtered by path, creator, label, or status), then get_job_logs with a returned id to inspect a specific run's logs — without starting a new test run.
|
||||
- When a required decision is ambiguous, use askUserQuestion with two to ten clear proposed answer strings instead of guessing. The user can also type a custom answer when none of the proposed answers fit.
|
||||
- Keep context targeted.${
|
||||
previewTools
|
||||
- Keep context targeted.${previewTools
|
||||
? `
|
||||
- After writing or substantially editing a script / flow / app draft, show it via open_preview(kind, path) so the user sees the editor and live preview right next to the chat. First check whether it is already shown: if unsure, call get_preview_status. Only call open_preview (or offer to) when no preview is open or it is showing a different item — don't re-open a preview already showing the item you just edited.`
|
||||
: ''
|
||||
}
|
||||
}
|
||||
|
||||
Flows:
|
||||
- read_workspace_item returns compact flow JSON. Inline script bodies appear as "inline_script.<moduleId>".
|
||||
@@ -745,6 +774,36 @@ function variableToItem(variable: ListableVariable): WorkspaceItem {
|
||||
}
|
||||
}
|
||||
|
||||
// Compact metadata for one run. The raw Job carries args/result/logs/raw_code
|
||||
// which can be huge — list_runs returns only what's needed to identify a run.
|
||||
function summarizeRun(job: Job): Record<string, unknown> {
|
||||
const base = {
|
||||
id: job.id,
|
||||
job_kind: job.job_kind,
|
||||
path: job.script_path,
|
||||
created_by: job.created_by,
|
||||
created_at: job.created_at,
|
||||
started_at: job.started_at,
|
||||
schedule_path: job.schedule_path,
|
||||
is_flow_step: job.is_flow_step,
|
||||
tag: job.tag,
|
||||
worker: job.worker
|
||||
}
|
||||
if ('success' in job) {
|
||||
// CompletedJob
|
||||
return {
|
||||
...base,
|
||||
status: job.canceled ? 'canceled' : job.success ? 'success' : 'failure',
|
||||
duration_ms: job.duration_ms
|
||||
}
|
||||
}
|
||||
// QueuedJob (running or still waiting in the queue)
|
||||
return {
|
||||
...base,
|
||||
status: job.running ? 'running' : 'queued'
|
||||
}
|
||||
}
|
||||
|
||||
// ============= App helpers =============
|
||||
|
||||
type BackendRunnableInput = z.infer<typeof backendRunnableSchema>
|
||||
@@ -807,11 +866,11 @@ function buildPersistedRunnable(
|
||||
): PersistedRunnable {
|
||||
const fields = input.staticInputs
|
||||
? Object.fromEntries(
|
||||
Object.entries(input.staticInputs).map(([k, v]) => [
|
||||
k,
|
||||
{ type: 'static', value: v, fieldType: 'object' }
|
||||
])
|
||||
)
|
||||
Object.entries(input.staticInputs).map(([k, v]) => [
|
||||
k,
|
||||
{ type: 'static', value: v, fieldType: 'object' }
|
||||
])
|
||||
)
|
||||
: (existing?.fields ?? {})
|
||||
|
||||
if (input.type === 'inline') {
|
||||
@@ -1667,6 +1726,65 @@ export const globalTools: Tool<{}>[] = [
|
||||
showDetails: true,
|
||||
autoCollapseDetails: false
|
||||
},
|
||||
{
|
||||
def: createToolDef(
|
||||
listRunsSchema,
|
||||
'list_runs',
|
||||
"List recent runs (jobs), most recent first. Optionally filter by path, creator, label, or status. Returns compact metadata only — use get_job_logs with a returned id to read a run's logs."
|
||||
),
|
||||
showDetails: true,
|
||||
fn: async ({ args, workspace, toolId, toolCallbacks }) => {
|
||||
const parsed = listRunsSchema.parse(args)
|
||||
toolCallbacks.setToolStatus(toolId, { content: 'Listing runs...' })
|
||||
const jobs = await JobService.listJobs({
|
||||
workspace,
|
||||
scriptPathExact: parsed.path,
|
||||
createdBy: parsed.created_by,
|
||||
label: parsed.label,
|
||||
success: parsed.success,
|
||||
running: parsed.running,
|
||||
perPage: parsed.limit ?? 30
|
||||
})
|
||||
const runs = jobs.map(summarizeRun)
|
||||
const result = JSON.stringify(runs, null, 2)
|
||||
toolCallbacks.setToolStatus(toolId, {
|
||||
content: `Listed ${runs.length} run(s)`,
|
||||
result
|
||||
})
|
||||
return result
|
||||
}
|
||||
},
|
||||
{
|
||||
def: createToolDef(
|
||||
getJobLogsSchema,
|
||||
'get_job_logs',
|
||||
'Fetch the logs of a job by its id. Use this to inspect the output of an existing run.'
|
||||
),
|
||||
showDetails: true,
|
||||
fn: async ({ args, workspace, toolId, toolCallbacks }) => {
|
||||
const parsed = getJobLogsSchema.parse(args)
|
||||
toolCallbacks.setToolStatus(toolId, {
|
||||
content: `Fetching logs for job ${parsed.id}...`
|
||||
})
|
||||
const logs = await JobService.getJobLogs({
|
||||
workspace,
|
||||
id: parsed.id,
|
||||
// Always suppress the "to remove ansi colors, use: sed ..." hint the
|
||||
// backend otherwise prepends — it is noise for the model and is not
|
||||
// actual ANSI stripping (the raw logs are returned either way).
|
||||
removeAnsiWarnings: true
|
||||
})
|
||||
const hasLogs = typeof logs === 'string' && logs.trim().length > 0
|
||||
const result = hasLogs ? logs : 'No logs available for this job.'
|
||||
toolCallbacks.setToolStatus(toolId, {
|
||||
content: hasLogs
|
||||
? `Fetched logs for job ${parsed.id}`
|
||||
: `No logs available for job ${parsed.id}`,
|
||||
result
|
||||
})
|
||||
return result
|
||||
}
|
||||
},
|
||||
{
|
||||
def: createToolDef(
|
||||
deployWorkspaceItemSchema,
|
||||
@@ -2326,9 +2444,9 @@ async function writeScheduleDraft(args: NewSchedule, ctx: WriteDraftCtx): Promis
|
||||
? existingDraft
|
||||
: backendExists
|
||||
? ((await ScheduleService.getSchedule({
|
||||
workspace,
|
||||
path: args.path
|
||||
})) as ScheduleDraftConfig)
|
||||
workspace,
|
||||
path: args.path
|
||||
})) as ScheduleDraftConfig)
|
||||
: undefined
|
||||
const draft = mergeDraftConfig<ScheduleDraftConfig>(base, args as DraftConfig, args.path)
|
||||
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
[packages]
|
||||
wmill = ">=1.720.0"
|
||||
wmill = ">=1.721.0"
|
||||
sendgrid = "*"
|
||||
mysql-connector-python = "*"
|
||||
pymongo = "*"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: '3.0.3'
|
||||
|
||||
info:
|
||||
version: 1.720.0
|
||||
version: 1.721.0
|
||||
title: OpenFlow Spec
|
||||
contact:
|
||||
name: Ruben Fiszel
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
RootModule = 'WindmillClient.psm1'
|
||||
|
||||
# Version number of this module.
|
||||
ModuleVersion = '1.720.0'
|
||||
ModuleVersion = '1.721.0'
|
||||
|
||||
# Supported PSEditions
|
||||
# CompatiblePSEditions = @()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "wmill"
|
||||
version = "1.720.0"
|
||||
version = "1.721.0"
|
||||
description = "A client library for accessing Windmill server wrapping the Windmill client API"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://windmill.dev"
|
||||
|
||||
@@ -563,7 +563,8 @@ wmill resource-type list --schema
|
||||
# Get specific resource type schema
|
||||
wmill resource-type get postgresql
|
||||
|
||||
# Push resources (tell the user to run this, do NOT run it yourself)
|
||||
# Push resources to Windmill — deploys to the workspace and can be destructive to
|
||||
# remote state, so only run it when the user explicitly asks to deploy/publish/push
|
||||
wmill sync push
|
||||
\`\`\`
|
||||
`;
|
||||
|
||||
@@ -242,6 +242,7 @@ wmill resource-type list --schema
|
||||
# Get specific resource type schema
|
||||
wmill resource-type get postgresql
|
||||
|
||||
# Push resources (tell the user to run this, do NOT run it yourself)
|
||||
# Push resources to Windmill — deploys to the workspace and can be destructive to
|
||||
# remote state, so only run it when the user explicitly asks to deploy/publish/push
|
||||
wmill sync push
|
||||
```
|
||||
|
||||
@@ -39,10 +39,10 @@ Windmill uses 6-field cron expressions (includes seconds):
|
||||
|
||||
## CLI Commands
|
||||
|
||||
After writing, tell the user they can run these commands (do NOT run them yourself):
|
||||
`wmill sync push` deploys local changes to the workspace and can be destructive to remote state — only suggest/run it when the user explicitly asks to deploy/publish/push, not when they say "run", "try", or "test". The commands below never mutate remote state, so they're safe to run yourself — note that `sync pull` does overwrite local files to match the remote (use `sync pull --dry-run` to only preview), while `schedule` just lists.
|
||||
|
||||
```bash
|
||||
# Push schedules to Windmill
|
||||
# Push schedules to Windmill — only when the user explicitly asks to deploy
|
||||
wmill sync push
|
||||
|
||||
# Pull schedules from Windmill
|
||||
|
||||
@@ -61,10 +61,10 @@ Text/HTML/inline parts are placed inline in `body` as strings.
|
||||
|
||||
## CLI Commands
|
||||
|
||||
After writing, tell the user they can run these commands (do NOT run them yourself):
|
||||
`wmill sync push` deploys local changes to the workspace and can be destructive to remote state — only suggest/run it when the user explicitly asks to deploy/publish/push, not when they say "run", "try", or "test". `sync pull` is safe to run yourself — it never mutates remote state, though it does overwrite local files to match the remote (use `sync pull --dry-run` to only preview).
|
||||
|
||||
```bash
|
||||
# Push trigger configuration
|
||||
# Push trigger configuration — only when the user explicitly asks to deploy
|
||||
wmill sync push
|
||||
|
||||
# Pull triggers from Windmill
|
||||
|
||||
@@ -237,6 +237,7 @@ wmill resource-type list --schema
|
||||
# Get specific resource type schema
|
||||
wmill resource-type get postgresql
|
||||
|
||||
# Push resources (tell the user to run this, do NOT run it yourself)
|
||||
# Push resources to Windmill — deploys to the workspace and can be destructive to
|
||||
# remote state, so only run it when the user explicitly asks to deploy/publish/push
|
||||
wmill sync push
|
||||
```
|
||||
|
||||
@@ -34,10 +34,10 @@ Windmill uses 6-field cron expressions (includes seconds):
|
||||
|
||||
## CLI Commands
|
||||
|
||||
After writing, tell the user they can run these commands (do NOT run them yourself):
|
||||
`wmill sync push` deploys local changes to the workspace and can be destructive to remote state — only suggest/run it when the user explicitly asks to deploy/publish/push, not when they say "run", "try", or "test". The commands below never mutate remote state, so they're safe to run yourself — note that `sync pull` does overwrite local files to match the remote (use `sync pull --dry-run` to only preview), while `schedule` just lists.
|
||||
|
||||
```bash
|
||||
# Push schedules to Windmill
|
||||
# Push schedules to Windmill — only when the user explicitly asks to deploy
|
||||
wmill sync push
|
||||
|
||||
# Pull schedules from Windmill
|
||||
|
||||
@@ -56,10 +56,10 @@ Text/HTML/inline parts are placed inline in `body` as strings.
|
||||
|
||||
## CLI Commands
|
||||
|
||||
After writing, tell the user they can run these commands (do NOT run them yourself):
|
||||
`wmill sync push` deploys local changes to the workspace and can be destructive to remote state — only suggest/run it when the user explicitly asks to deploy/publish/push, not when they say "run", "try", or "test". `sync pull` is safe to run yourself — it never mutates remote state, though it does overwrite local files to match the remote (use `sync pull --dry-run` to only preview).
|
||||
|
||||
```bash
|
||||
# Push trigger configuration
|
||||
# Push trigger configuration — only when the user explicitly asks to deploy
|
||||
wmill sync push
|
||||
|
||||
# Pull triggers from Windmill
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@windmill/windmill",
|
||||
"version": "1.720.0",
|
||||
"version": "1.721.0",
|
||||
"exports": "./src/index.ts",
|
||||
"publish": {
|
||||
"exclude": ["!src", "./s3Types.ts", "./sqlUtils.ts", "./client.ts"]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "windmill-client",
|
||||
"description": "Windmill SDK client for browsers and Node.js",
|
||||
"version": "1.720.0",
|
||||
"version": "1.721.0",
|
||||
"author": "Ruben Fiszel",
|
||||
"license": "Apache 2.0",
|
||||
"sideEffects": false,
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
1.720.0
|
||||
1.721.0
|
||||
|
||||
Reference in New Issue
Block a user