Compare commits

..

51 Commits

Author SHA1 Message Date
Ruben Fiszel
1aac43977e test(cli): add non-dotted path tests for generate-metadata and sync pull
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 04:30:21 +00:00
Travis
05d4d6dd48 fix(cli): preserve non-dotted flow lock filenames 2026-03-14 22:41:59 -04:00
Ruben Fiszel
b6da492d1b chore(main): release 1.657.0 (#8368)
* chore(main): release 1.657.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2026-03-14 04:52:06 +00:00
Ruben Fiszel
87215193ca system promps generate metadata 2026-03-14 04:51:54 +00:00
Alexander Petric
5df37fb0db feat: add datatable config support to CLI settings sync and backend export (#8024)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-14 04:44:11 +00:00
Ruben Fiszel
6fa2543647 chore(main): release 1.656.0 (#8346) 2026-03-13 22:32:57 +00:00
hugocasa
c431053a1e fix(frontend): prevent duplicate and reserved agent tool names (#8367)
* fix(frontend): prevent duplicate and reserved agent tool names

Extend tool name validation to detect duplicates within an agent step
and reserved names (like 'preprocessor', 'failure'). Show specific error
messages in the editor panel and red styling in the graph view.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(frontend): remove duplicate banner for agent tool name errors

The inline per-tool error messages are sufficient — the panel-level
banner was redundant and showed a double error.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-13 22:30:53 +00:00
Ruben Fiszel
a079dd500f i pkg 2026-03-13 22:21:28 +00:00
centdix
9d2c439e2a fix: resource drawer opening behind dialog in chat mode (#8328)
* fix: resource drawer opening behind dialog in chat mode

Integrate Modal into the Disposable z-index stacking system so drawers
opened from within a modal (e.g. "Add a new resource") correctly appear
above the dialog instead of behind it.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: resource drawer opening behind dialog in chat mode

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: simplify minZIndex tracking by removing unnecessary refcount

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: use map-based minZIndex tracking and conditional chat elevation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: use plain object instead of Map for reactive minZIndex tracking

$state(new Map()) is not deeply reactive in Svelte 5 — only plain
objects and arrays are proxied. Replaced with Record<string, number>
so that property assignments properly trigger $derived updates.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 20:33:16 +00:00
hugocasa
fb12b31df0 fix(frontend): improve native mode alert message and fix workspaced tag detection (#8361)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 20:32:29 +00:00
Pyra
51933be3ca fix(cli): suppress verbose lock generation messages in generate-metadata (#8357)
* fix(cli): suppress verbose lock generation messages in generate-metadata

Pass noStaleMessage flag through to updateRawAppRunnables and
updateAppInlineScripts to suppress verbose "Generating lock for..."
messages when running generate-metadata command. Also fixes a stray
`}` character in a template literal.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(cli): show updated inline scripts in generate-metadata output

Display inline script names that were relocked when processing flows
and apps in the generate-metadata command output. For example:

  [4/5] app    u/admin/test__raw_app: a, b, c

This provides visibility into what work was done without verbose
per-script logging that clutters the output.

- Add AppLocksResult and FlowLocksResult types to track updated scripts
- Update internal functions to return lists of updated script names
- Display script names inline with progress in generate-metadata

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-13 20:32:08 +00:00
Pyra
404ae09d42 fix(cli): normalize path separators in generate-metadata folder filter for Windows (#8358)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-13 20:31:44 +00:00
Diego Imbert
e0e78442b7 Support T | T[] in debounce (#8340)
* Detect union types in TS

* display union type arguments

* Handle single values at accumulation time

* nit propagate otyp

* Python support

* npm package update
2026-03-13 20:31:25 +00:00
hugocasa
0d31c35f3e fix(frontend): filter webhook/email tokens by scope instead of label (#8363)
The backend already filters tokens by scope matching the script/flow
path. Remove the redundant client-side label prefix filter so that all
tokens with matching scopes are shown, not just those with a specific
label convention.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 20:31:06 +00:00
Pyra
060687b1fa fix(cli): exclude raw app backend files from script metadata generation (#8362)
Files inside .raw_app/backend/ were incorrectly being processed by
`script generate-metadata` and `generate-metadata --skip-flows --skip-apps`
because the filter only checked isFlowPath and isAppPath, but not isRawAppPath.

This caused backend runnables to be treated as standalone scripts, creating
incorrect .script.yaml files at wrong locations.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-13 20:30:23 +00:00
HugoCasa
8301d86800 docs: rewrite Code Navigation section with MUST for outline/body and condensed limitations
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 17:45:06 +01:00
Ruben Fiszel
44dd3ee8cd fix(ci): remove provenance flag and use NPM_TOKEN for npm publish
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 12:43:45 +00:00
Ruben Fiszel
2a8e276b6d fix(ci): add NODE_AUTH_TOKEN for npm publish authentication
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 12:38:41 +00:00
Ruben Fiszel
bc35c94616 ci 2026-03-13 12:29:58 +00:00
Guilhem
b585dee64d fix(frontend): collapse flow topbar buttons to icon-only in narrow panes (#8322)
* feat: collapse flow topbar buttons to icon-only mode in narrow panes

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: show delete button on top-right of compact error handler

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: preserve bug icon and diff action bar in compact error handler

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: avoid duplicate delete buttons when diff action bar is active

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: use undefined instead of empty string for wrapperClasses

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-13 12:08:19 +00:00
hugocasa
96229575e6 chore: dev tooling — wm-ts-nav navigator, format hooks, review skill (#8337)
* chore: remove wm-cursor, add local-review skill, update PR skill for EE

- Remove the unused wm-cursor script and all references to it in
  README_WORKMUX_DEV.md and worktree-common.sh
- Add /local-review skill for code review (bugs + CLAUDE.md compliance)
- Add EE companion PR workflow to the /pr skill

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: add wm-ts-nav tree-sitter navigator and fix format hooks

- Add wm-ts-nav: standalone tree-sitter code navigator with SQLite index
  for fast symbol search, definition lookup, and file outlines across
  Rust, TypeScript, and Svelte files (~12ms warm, ~1s cold for 482 files)
- Fix format hooks: surface errors instead of swallowing with 2>/dev/null,
  use direct prettier path with svelte plugin, add success feedback
- Add wm-ts-nav commands to settings allow list
- Document wm-ts-nav usage in CLAUDE.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(wm-ts-nav): add refs command and --parent filter

- refs: find usages of a symbol in code, skipping comments and strings
  (tree-sitter AST walk, ~46ms for 482 files vs grep's 4ms but no noise)
- --parent filter on search: find all methods on a type across all files
  (e.g. search "%" --kind function --parent ServiceName)
- Update CLAUDE.md with clearer when-to-use guidance

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(wm-ts-nav): index refs in DB with import-path resolution

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(wm-ts-nav): add body, callers, callees commands and refs --file/--caller

- body: extract a symbol's source code from disk using indexed line ranges
- callers: cross-file call graph via SQL join of refs + symbols tables
- callees: list all identifiers referenced within a symbol's body
- refs --file: scope results to files matching a substring
- refs --caller: annotate each ref with the containing function name

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(wm-ts-nav): add auto-rebuilding wrapper script

The `wm-ts-nav/nav` wrapper checks if source files are newer than the
binary and rebuilds automatically. Invoked via `sh wm-ts-nav/nav` to
avoid needing executable permissions after clone.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: tighten CLAUDE.md nav section for actionable guidance

Remove redundant question→command mapping, latency numbers, and
excessive examples. Lead with "prefer wm-ts-nav over Read to save
context window" and keep only the patterns that change behavior.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: revert backend/Cargo.lock to main

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: promote wm-ts-nav in workflow, copy binary to worktrees

- CLAUDE.md: integrate wm-ts-nav into Workflow step 1 and Core
  Principles so agents use outline/body before full file reads
- workmux: copy built binary via files.copy
- worktree-common.sh: copy binary in wm_copy_dependencies for webmux

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(wm-ts-nav): fix double indexing, add TSX grammar, remove needless clone

- Reuse index stats from the pre-query update instead of indexing twice
  on the Index command
- Add Lang::Tsx variant so .tsx/.jsx files use LANGUAGE_TSX instead of
  LANGUAGE_TYPESCRIPT (Svelte stays on TS since script blocks are pure TS)
- Remove source.clone() for non-Svelte files — move directly instead

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(wm-ts-nav): fix svelte line numbers, add class methods, innermost caller

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 12:07:49 +00:00
Ruben Fiszel
2d5b72b3ce chore: update vite/vitest to stable v8/v4 and remove legacy-peer-deps (#8349)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-13 08:50:19 +00:00
Alexander Petric
2e430c4c0b feat: add GitHub Enterprise Server (GHES) support for GitHub App git sync (#8344)
* feat: add GitHub Enterprise Server (GHES) support for GitHub App git sync

Add a self-managed GitHub App mode alongside the existing managed
(stats.windmill.dev) mode, enabling git sync for GitHub Enterprise Server
and custom GitHub App installations.

Backend:
- Parameterize GitHub API URLs (no more hardcoded github.com)
- Add GITHUB_ENTERPRISE_APP_SETTING global setting
- Add OpenAPI specs for ghes_installation_callback and ghes_config endpoints

Frontend:
- Add instance settings UI for configuring self-managed GitHub Apps
  with setup instructions and validation
- GHES installation flow in gh_success page
- Dynamic installation URL based on GHES config
- Increase git sync test connection timeout to 10s
- Block "Review changes" save when settings are invalid

EE companion PR: windmill-labs/windmill-ee-private#<PR_NUMBER>

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: update ee-repo-ref to c74c86b78a66b976fd9968b21f77903723e668ec

This commit updates the EE repository reference after PR #459 was merged in windmill-ee-private.

Previous ee-repo-ref: 45e4550110799525b5502cf072c8af8132492638

New ee-repo-ref: c74c86b78a66b976fd9968b21f77903723e668ec

Automated by sync-ee-ref workflow.

* sqlx

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2026-03-13 08:21:15 +00:00
Pyra
4c2c165a5b feat(cli): add unified generate-metadata command (#8335)
* feat(cli): add unified generate-metadata command

- Add generate-metadata command that calls script, flow, and app handlers
- Export generateLocks from flow.ts and generateMetadata from script.ts
- Add deprecation warnings to individual metadata commands

* feat(cli): improve unified generate-metadata command

- Use internal handlers for single-pass collection of stale items
- Add --dry-run flag to show what would be updated
- Fix WASM parser init deprecation warning
- Add comprehensive tests for all flags
- Match original handler behavior for per-item messages

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(cli): add skip flags and comprehensive tests for generate-metadata

- Add --skip-scripts, --skip-flows, --skip-apps flags for granular control
- --schema-only now properly skips flows and apps (they only have locks)
- Dynamic "Checking X, Y, Z..." message based on what's being processed
- Show warning when all types are skipped
- Add comprehensive tests for all flags:
  - --dry-run shows stale items without updating
  - --schema-only only processes scripts
  - --skip-scripts, --skip-flows, --skip-apps work correctly
  - skipping all types shows warning
  - 'All metadata up-to-date' when nothing to update

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* improve output

Signed-off-by: pyranota <pyra@duck.com>

* refactor(cli): add shared test fixtures with cross-links

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(cli): add folder argument to generate-metadata command

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Signed-off-by: pyranota <pyra@duck.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-13 06:10:28 +00:00
wendrul
2d7f325bb8 add a hint to commit message on git sync for renames (#8343)
* add a hint to commit message on git sync for renames

* chore: update ee-repo-ref to 344e2aa60d8cafe08b8c57445d2f9555bad7625a

This commit updates the EE repository reference after PR #458 was merged in windmill-ee-private.

Previous ee-repo-ref: ad4c90cc96ad1b4511cba6b6ee28351895882156

New ee-repo-ref: 344e2aa60d8cafe08b8c57445d2f9555bad7625a

Automated by sync-ee-ref workflow.

---------

Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
2026-03-13 06:08:33 +00:00
Diego Imbert
0a838ca5dc separate storage tabs (#8341)
* Separate storage settings

* Separate VolumeStorageSettings

* nit
2026-03-13 06:05:27 +00:00
wendrul
8e3b8bdfd2 fix: Linked resources and vars not triggering both sync jobs on delete (#8342)
* fix: Linked resources and vars not triggering both sync jobs on delete

* prepare sqlx
2026-03-13 06:04:44 +00:00
Ruben Fiszel
d9d45cf2f9 fix: lower default indexer memory/batch settings to prevent OOM (#8347)
The windmill-indexer pod was crash-looping due to OOMKilled (exit 137)
with a 2Gi memory limit. Two concurrent tantivy IndexWriters each
allocating 300MB (600MB total), combined with large uncommitted batches
of 50K jobs, caused memory exhaustion during indexing.

- writer_memory_budget: 300MB → 150MB (2 writers = 300MB total)
- commit_job_max_batch_size: 50,000 → 10,000
- commit_log_max_batch_size: 10,000 → 5,000

These can still be overridden via env vars or global settings.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 06:03:45 +00:00
Ruben Fiszel
54202e4a96 nit 2026-03-13 06:03:31 +00:00
Ruben Fiszel
36b9db903b clean stale v8 build cache to prevent CI link failures (#8348)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 05:48:47 +00:00
Ruben Fiszel
aae77d6598 customUi sandbox 2026-03-13 05:48:20 +00:00
Ruben Fiszel
724d1350d0 fix: graceful shutdown instead of panic on job completion channel failure (#8345)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 05:14:02 +00:00
Ruben Fiszel
a0337e3b4a adjust exports 2026-03-13 04:30:54 +00:00
Ruben Fiszel
55755cb822 system_prompts update 2026-03-12 15:01:19 +00:00
Ruben Fiszel
749964e326 ci: add freshness check for auto-generated system prompts (#8338)
* ci: add freshness check for auto-generated system prompts

Add a CI workflow and script to verify system_prompts/auto-generated/
stays in sync with its source files (SDKs, schemas, CLI commands, etc).
Also remove the hardcoded CLI version from generated output to avoid
unnecessary churn on every release.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* imports

* imports

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* regenerate system prompts after rebase on main

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 14:44:27 +00:00
Ruben Fiszel
9f7f666af4 chore(main): release 1.655.0 (#8312)
* chore(main): release 1.655.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2026-03-12 14:05:09 +00:00
hugocasa
ec20d76216 feat: add auto_commit option to Kafka triggers with advanced UI badges (#8317)
* feat: add auto_commit option to Kafka triggers with manual commit API

Add ability to disable auto-commit on Kafka triggers so users can
manually commit offsets after processing messages. This prevents
message loss when processing fails.

Changes:
- Add `auto_commit` column to kafka_trigger table (default true)
- Add POST /kafka_triggers/commit_offsets/{path} endpoint using
  BaseConsumer with manual assign() to avoid rebalance
- Enrich trigger_info payload with partition and offset fields
- Conditionally commit based on auto_commit setting
- Add auto-commit toggle to frontend Kafka trigger config
- Add commitKafkaOffsets helpers to Python and TypeScript SDKs
- Add integration tests for auto_commit DB defaults

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: use DB-based pending commits for kafka manual offset commit

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: pass trigger_path to all v2 preprocessors, secure commit_offsets endpoint, fix commit semantics

- Add trigger_path to v2 preprocessor event for all trigger types (kafka, nats, sqs, mqtt, gcp, postgres, websocket, http, email)
- Secure commit_offsets endpoint: infer trigger from job token (OptJobAuthed) instead of requiring trigger path parameter
- Fix auto_commit: only commit offset after successful job push
- Fix pending commits: commit offset+1 (Kafka semantics) and use CommitMode::Sync
- Update TS/Python clients and frontend preprocessor templates

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add advanced section badges and reorganize kafka trigger settings

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: remove dead wm_trigger assertions from kafka e2e test

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* sqlx

* refactor: remove unused advancedCollapsed state from all trigger editors

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* update ref

* chore: update ee-repo-ref to ed2c9d360e6fab866b9744cc79f50038d1fc7152

This commit updates the EE repository reference after PR #452 was merged in windmill-ee-private.

Previous ee-repo-ref: 5b31116a1d5a042c6a780732901cfd89584d1773

New ee-repo-ref: ed2c9d360e6fab866b9744cc79f50038d1fc7152

Automated by sync-ee-ref workflow.

* fix: use path-based auth for kafka commit_offsets endpoint

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: update ee-repo-ref to fcd3ea52b0cc94fbe1159baf662a38da947456de

This commit updates the EE repository reference after PR #457 was merged in windmill-ee-private.

Previous ee-repo-ref: b3a5c33c92cb1b2caf7a65986d71da291ff72a35

New ee-repo-ref: fcd3ea52b0cc94fbe1159baf662a38da947456de

Automated by sync-ee-ref workflow.

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2026-03-12 14:00:30 +00:00
hugocasa
d2b9799ac4 test: git sync E2E tests + auto-manage git sync script version (#8253)
* test: add E2E git sync integration tests with Gitea

Add 7 end-to-end tests that verify the full git sync pipeline:
deploy objects in Windmill → DeploymentCallback job runs hub sync script →
correct files appear in a Gitea git repository.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: auto-manage git sync script version locked to Windmill release

- Add LATEST_GIT_SYNC_SCRIPT_PATH constant as single source of truth
- Backend auto-fills empty script_path with latest on save
- New repos use empty script_path (auto-managed by backend)
- Existing repos with pinned versions show warning with opt-in button
- cache_hub_scripts always caches the latest constant
- Rename hubPaths.json gitSync entries to deprecated_ prefix

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: update ee-repo-ref.txt for git-sync-tests branch

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: update ee-repo-ref.txt

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: remove upgrade_git_sync_script_paths from save path

Empty script_path is now resolved to latest at job dispatch time in EE,
not on save. Users opt in via the UI button.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: use Option<String> for git sync script_path

None means auto-managed (uses LATEST_GIT_SYNC_SCRIPT_PATH),
Some(path) means pinned to a specific script. Resolution happens
at job dispatch time via effective_script_path().

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: separate git sync tests into dedicated CI workflow

- Remove git_sync_test from default integration test suite
- Move gitea service to dedicated docker-compose.git-sync.yml
- Add run_git_sync.sh script
- New workflow triggers on changes to git sync crate, hub paths,
  ee-repo-ref, or the test files themselves

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test: add Rust integration tests for git sync filtering logic

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: run git sync E2E tests via cargo run instead of docker image

Build from source and run Windmill directly, start Gitea as a
standalone container. Tests run against localhost — no pre-built
Docker image needed, works on PRs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: add relevance check before running git sync E2E tests

Only run the expensive build+test when actually relevant:
- Direct git sync file changes: always run
- ee-repo-ref.txt changed: check if EE diff touches windmill-git-sync/
- Unrelated changes to workspaces.rs or other files: skip

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: remove broad path triggers from git sync workflow

Remove workspaces.rs and wmill_integration_test_utils.py from path
triggers - they change too often for unrelated reasons. Keep only
git-sync-specific paths + ee-repo-ref.txt (filtered by check-relevance).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test: rewrite git sync E2E tests with full coverage and fix test infra

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore: remove accidentally committed gen files

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test: remove unit/integration tests for git sync filtering (covered by E2E)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: use correct build features and pass license key to test step in CI

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: add workflow_dispatch trigger to git sync test workflow

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Update commit reference in ee-repo-ref.txt

* fix: update stats_oss stubs to match EE telemetry signature changes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: disable -D warnings for git sync e2e build step

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: allow test connection button in auto-managed git sync mode

The test connection button was disabled and runTestJob() bailed out
when script_path was unset. The test job uses a separate hub script
(gitSyncTest), not the sync script, so the guard was wrong.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: update ee-repo-ref to include auto-managed script_path fix

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: use full SHA in ee-repo-ref.txt

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address PR review feedback

- Initialize current_count before loop in wait_for_sync_jobs
- Clean up temp directories in clone helpers with addCleanup
- Fail CI startup steps if Gitea/Windmill never become ready
- Assert exact job count in exclude_path test
- Remove docs/git-sync-tests-plan.md (stale planning doc)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: remove orphaned git_sync.sql fixture

No longer referenced after Rust integration tests were removed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: assert old file removal in rename test

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: update ee-repo-ref to 612d96a66f9d0cfdae335ef3eb4881f3444ce7cd

This commit updates the EE repository reference after PR #442 was merged in windmill-ee-private.

Previous ee-repo-ref: a05004a7c82f3d1ee5f6863bb9f5a33827d30032

New ee-repo-ref: 612d96a66f9d0cfdae335ef3eb4881f3444ce7cd

Automated by sync-ee-ref workflow.

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
2026-03-12 12:48:38 +00:00
Ruben Fiszel
f3e9a29c13 add customUi props for History and Save to workspace editor bar buttons (#8336)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 12:28:17 +00:00
centdix
7fb729cc84 fix(cli): instruct agent to tell user about generate-metadata and sync push instead of running them (#8318)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-12 10:18:03 +00:00
wendrul
ca8a6274bc fix: use !inline ref for scripts inside flows (preproc, error, ai tool) (#8319)
* fix: use !inline ref for scripts inside flows (preproc, error, ai tool)

* add test

* path assign better
2026-03-12 10:15:00 +00:00
Ruben Fiszel
bf4340f40c fix: set min_connections(0) to prevent sqlx pool spin loop (#8334)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-11 23:46:24 +00:00
Clumsy
cbc7e78f8a fix: show diff editor content for resources without a language (#8331)
Resources like ansible_inventory have content but no language field,
causing the DiffEditor setupModel guard to skip initialization entirely.
Fall back to 'plaintext' when no language is provided but content exists.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 22:42:15 +00:00
hugocasa
d8b4132b9a fix: native mode now properly sets DB pool size and sleep queue (#8332)
Native mode (from DB config) was not accounted for when sizing the
connection pool or setting SLEEP_QUEUE, because both read NUM_WORKERS
from env which is never set when native mode is configured via the
worker group config in the database.

- Resolve native mode early (before connect_db) by querying the config
  table with the initial DB connection
- Pass num_workers directly to connect_db instead of re-reading env
- Replace SLEEP_QUEUE lazy_static with sleep_queue() function that
  checks NATIVE_MODE_RESOLVED at runtime (returns 300ms for native)
- Set NATIVE_MODE_RESOLVED immediately when is_native_mode_from_env()
- Allow native_mode in CE worker group config (was silently stripped)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 22:39:15 +00:00
Ruben Fiszel
4306c9e4fe fix: skip python preinstall on native workers (#8329)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-11 18:22:07 +00:00
hugocasa
fe1519f128 feat: support minimal telemetry mode (#8243)
* feat: support minimal telemetry mode for EE

When EE customers disable telemetry, send a reduced payload with only
license-compliance data instead of ignoring the setting. Job usage data
is excluded in minimal mode. The telemetry settings UI now shows in EE
with context-appropriate descriptions for both CE and EE.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: update ee-repo-ref for telemetry-minimal

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: make telemetry toggle label and description license-aware

Show "Minimal telemetry" with EE-specific description on EE, and
"Disable telemetry" with CE-specific description on CE.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: update ee-repo-ref

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Update commit hash in ee-repo-ref.txt

* Update reference hash in ee-repo-ref.txt

* chore: update ee-repo-ref to 2f52c015bc6c81391234fa87b27ee1d4cd3a48a3

This commit updates the EE repository reference after PR #440 was merged in windmill-ee-private.

Previous ee-repo-ref: 3628ed51426d8d29b3d5c62864ba256b7f9eab17

New ee-repo-ref: 2f52c015bc6c81391234fa87b27ee1d4cd3a48a3

Automated by sync-ee-ref workflow.

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
2026-03-11 19:12:47 +01:00
Ruben Fiszel
df1b1f9651 chore: fix 19 CVEs by upgrading php, docker, and node-tar (#8326)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 17:58:31 +00:00
Diego Imbert
ae019237d1 runs page nits (#8325) 2026-03-11 17:52:26 +00:00
Diego Imbert
577484d06a Separate asset parsers (#8321)
* Refactor asset_parser

* package update

* package lock
2026-03-11 16:54:05 +00:00
centdix
e7047761cf chore: webmux config (#8323) 2026-03-11 16:08:09 +00:00
hugocasa
8667329110 fix: skip token expiry notifications for debugger and mcp-oauth tokens (#8316)
* fix: skip token expiry notifications for debugger and mcp-oauth tokens

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: update frontend isUserToken to match backend filter

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore: add cross-reference comments to token filter functions

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-11 10:47:21 +00:00
243 changed files with 13306 additions and 4349 deletions

View File

@@ -13,8 +13,10 @@ fi
# Check if the file is in the backend directory and is a Rust file
if [[ "$FILE_PATH" == *"/backend/"* ]] && [[ "$FILE_PATH" =~ \.rs$ ]]; then
cd "$CLAUDE_PROJECT_DIR/backend" || exit 0
# Run rustfmt with config from rustfmt.toml (edition=2021)
rustfmt --config-path rustfmt.toml "$FILE_PATH" 2>/dev/null || true
# Run rustfmt, surface errors as context but don't block Claude
if rustfmt --config-path rustfmt.toml "$FILE_PATH" 2>&1; then
echo "Formatted $(basename "$FILE_PATH")"
fi
fi
exit 0

View File

@@ -15,8 +15,10 @@ if [[ "$FILE_PATH" == *"/frontend/"* ]]; then
# Check if it's a formattable file type
if [[ "$FILE_PATH" =~ \.(ts|js|svelte|json|css|html|md)$ ]]; then
cd "$CLAUDE_PROJECT_DIR/frontend" || exit 0
# Run prettier silently, don't fail the hook if prettier fails
npx prettier --write "$FILE_PATH" 2>/dev/null || true
# Run prettier, surface errors as context but don't block Claude
if ./node_modules/.bin/prettier --plugin prettier-plugin-svelte --write "$FILE_PATH" 2>&1; then
echo "Formatted $(basename "$FILE_PATH")"
fi
fi
fi

View File

@@ -28,6 +28,12 @@
"Bash(git show:*)",
"Bash(git blame:*)",
"Bash(cargo check:*)",
"Bash(cargo build --release:*)",
"Bash(sh wm-ts-nav/nav:*)",
"Bash(wm-ts-nav/nav:*)",
"Bash(./wm-ts-nav/nav:*)",
"Bash(wm-ts-nav/target/release/wm-ts-nav:*)",
"Bash(./wm-ts-nav/target/release/wm-ts-nav:*)",
"mcp__ide__getDiagnostics",
"Bash(npm run generate-backend-client:*)",
"Bash(npm run check:*)",

View File

@@ -0,0 +1,98 @@
---
name: local-review
user_invocable: true
description: Code review a pull request for bugs and CLAUDE.md compliance. MUST use when asked to review code.
---
# Local Code Review Skill
Review a pull request for real bugs and CLAUDE.md compliance violations. This review targets HIGH SIGNAL issues only.
## Review Philosophy
- **Only flag issues you are certain about.** If you are not sure an issue is real, do not flag it. False positives erode trust and waste reviewer time.
- Think like a senior engineer doing a final review — flag things that would cause incidents, not things that are merely imperfect.
## What to Flag
- Code that won't compile or parse (syntax errors, type errors, missing imports)
- Code that will definitely produce wrong results regardless of inputs
- Clear, unambiguous CLAUDE.md violations (quote the exact rule being violated)
- Security issues in introduced code (injection, auth bypass, data exposure)
- Incorrect logic that will fail in production
## What NOT to Flag
- Code style or quality concerns
- Potential issues that depend on specific inputs or runtime state
- Subjective suggestions or improvements
- Pre-existing issues not introduced by this PR
- Pedantic nitpicks a senior engineer wouldn't flag
- Issues a linter or type checker will catch
- General quality concerns unless explicitly prohibited in CLAUDE.md
- Issues silenced via lint ignore comments
## Execution Steps
1. **Determine the PR scope**:
- If an argument is provided, use it as the PR number or branch
- Otherwise, detect from the current branch vs main
- Run `gh pr view` if a PR exists, or use `git diff main...HEAD`
2. **Find relevant CLAUDE.md files**:
- Read the root `CLAUDE.md`
- Check for CLAUDE.md files in directories containing changed files
3. **Get the diff and metadata**:
- `gh pr diff` or `git diff main...HEAD` for the full diff
- `gh pr view` or `git log main..HEAD --oneline` for context
4. **Read changed files** where the diff alone is insufficient to understand context
5. **Review for**:
- CLAUDE.md compliance — check each rule against the changed code
- Bugs and logic errors — will this code work correctly?
- Security issues — injection, auth, data exposure in new code
6. **Self-validate each finding**: Before reporting, ask yourself:
- "Is this definitely a real issue, not a false positive?"
- "Would a senior engineer flag this in review?"
- If the answer to either is no, discard the finding
7. **Output findings** to the terminal (default) or post as PR comments (with `--comment` flag)
## Output Format
```
## Code review
Found N issues:
1. <description> (<reason: CLAUDE.md adherence | bug | security>)
<file_path:line_number>
2. <description> (<reason>)
<file_path:line_number>
```
If no issues are found:
```
## Code review
No issues found. Checked for bugs and CLAUDE.md compliance.
```
## Posting Comments (--comment flag)
If the user passes `--comment`, post findings as inline PR comments using:
```bash
gh pr review --comment --body "<summary>"
```
Or for inline comments on specific lines:
```bash
gh api repos/{owner}/{repo}/pulls/{pr}/reviews -f body="<summary>" -f event="COMMENT" -f comments="[...]"
```

View File

@@ -33,6 +33,7 @@ Follow conventional commit format for the PR title:
- Keep under 70 characters
- Use lowercase, imperative mood
- No period at the end
- If `*_ee.rs` files were modified, prefix with `[ee]`: `[ee] <type>: <description>`
## PR Body Format
@@ -85,3 +86,25 @@ Generated with [Claude Code](https://claude.com/claude-code)
)"
```
7. Return the PR URL to the user
## EE Companion PR (when `*_ee.rs` files were modified)
The `*_ee.rs` files in the windmill repo are **symlinks** to `windmill-ee-private` — changes won't appear in `git diff` of the windmill repo. Instead, check the EE repo for uncommitted or unpushed changes.
Follow the full EE PR workflow in `docs/enterprise.md`. The key PR-specific details:
1. Find the EE repo/worktree: see "Finding the EE Repo" in `docs/enterprise.md`
2. Check for changes: `git -C <ee-path> status --short`
- If there are no changes in the EE repo, skip this entire section
3. Follow steps 15 from the "EE PR Workflow" in `docs/enterprise.md`
4. Create the companion PR (title does NOT get the `[ee]` prefix):
```bash
gh pr create --draft --repo windmill-labs/windmill-ee-private --title "<type>: <description>" --body "$(cat <<'EOF'
Companion PR for windmill-labs/windmill#<PR_NUMBER>
---
Generated with [Claude Code](https://claude.com/claude-code)
EOF
)"
```
5. Commit `ee-repo-ref.txt` and push the updated windmill branch

View File

@@ -119,6 +119,18 @@ jobs:
with:
cache-workspaces: backend
toolchain: 1.93.0
- name: Fix stale v8 build cache
working-directory: ./backend
run: |
# Cargo cache may preserve v8 build fingerprints without the actual
# librusty_v8.a library. Since fingerprints look valid, cargo skips
# build.rs re-run, causing "could not find native static library rusty_v8".
for profile in debug release; do
if [ -d "target/$profile/.fingerprint" ] && [ ! -f "target/$profile/gn_out/obj/librusty_v8.a" ]; then
echo "Cleaning stale v8 build artifacts in target/$profile"
rm -rf "target/$profile/build/v8-"* "target/$profile/.fingerprint/v8-"*
fi
done
- name: cargo check
timeout-minutes: 16
working-directory: ./backend

View File

@@ -89,6 +89,18 @@ jobs:
with:
cache-workspaces: backend
toolchain: 1.93.0
- name: Fix stale v8 build cache
working-directory: ./backend
run: |
# Cargo cache may preserve v8 build fingerprints without the actual
# librusty_v8.a library. Since fingerprints look valid, cargo skips
# build.rs re-run, causing "could not find native static library rusty_v8".
for profile in debug release; do
if [ -d "target/$profile/.fingerprint" ] && [ ! -f "target/$profile/gn_out/obj/librusty_v8.a" ]; then
echo "Cleaning stale v8 build artifacts in target/$profile"
rm -rf "target/$profile/build/v8-"* "target/$profile/.fingerprint/v8-"*
fi
done
- name: Read EE repo commit hash
run: |
echo "ee_repo_ref=$(cat ./ee-repo-ref.txt)" >> "$GITHUB_ENV"

View File

@@ -0,0 +1,37 @@
name: Check system prompts freshness
on:
push:
paths:
- "system_prompts/**"
- "typescript-client/**"
- "python-client/wmill/wmill/client.py"
- "openflow.openapi.yaml"
- "backend/windmill-api/openapi.yaml"
- "cli/src/main.ts"
- "cli/src/commands/**"
pull_request:
paths:
- "system_prompts/**"
- "typescript-client/**"
- "python-client/wmill/wmill/client.py"
- "openflow.openapi.yaml"
- "backend/windmill-api/openapi.yaml"
- "cli/src/main.ts"
- "cli/src/commands/**"
jobs:
check-freshness:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install dependencies
run: pip install pyyaml
- name: Check auto-generated files are up-to-date
run: bash system_prompts/check-freshness.sh

209
.github/workflows/git-sync-test.yml vendored Normal file
View File

@@ -0,0 +1,209 @@
name: Git Sync Integration Tests
on:
workflow_dispatch:
push:
branches: [main]
paths:
- "backend/windmill-git-sync/**"
- "backend/windmill-api-integration-tests/tests/git_sync*"
- "backend/ee-repo-ref.txt"
- "integration_tests/test/git_sync_test.py"
- ".github/workflows/git-sync-test.yml"
pull_request:
types: [opened, synchronize, reopened]
paths:
- "backend/windmill-git-sync/**"
- "backend/windmill-api-integration-tests/tests/git_sync*"
- "backend/ee-repo-ref.txt"
- "integration_tests/test/git_sync_test.py"
- ".github/workflows/git-sync-test.yml"
concurrency:
group: git-sync-test-${{ github.ref }}
cancel-in-progress: true
jobs:
check-relevance:
runs-on: ubuntu-latest
outputs:
should_run: ${{ steps.check.outputs.should_run }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check if git sync related files changed
id: check
env:
WINDMILL_EE_PRIVATE_ACCESS: ${{ secrets.WINDMILL_EE_PRIVATE_ACCESS }}
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
BASE=${{ github.event.pull_request.base.sha }}
else
BASE=${{ github.event.before }}
fi
CHANGED_FILES=$(git diff --name-only "$BASE"..HEAD 2>/dev/null || echo "")
echo "Changed files:"
echo "$CHANGED_FILES"
# Direct git sync file changes — always relevant
if echo "$CHANGED_FILES" | grep -qE '^(backend/windmill-git-sync/|backend/windmill-api-integration-tests/tests/git_sync|integration_tests/test/git_sync|\.github/workflows/git-sync-test\.yml)'; then
echo "should_run=true" >> "$GITHUB_OUTPUT"
echo "Relevant: direct git sync file changes"
exit 0
fi
# If ee-repo-ref.txt changed, check if the EE diff touches windmill-git-sync/
if echo "$CHANGED_FILES" | grep -q '^backend/ee-repo-ref.txt$'; then
NEW_REF=$(cat backend/ee-repo-ref.txt)
OLD_REF=$(git show "$BASE:backend/ee-repo-ref.txt" 2>/dev/null || echo "")
if [ -n "$OLD_REF" ] && [ "$OLD_REF" != "$NEW_REF" ]; then
# Clone EE repo and check diff
git clone --bare "https://x-access-token:${WINDMILL_EE_PRIVATE_ACCESS}@github.com/windmill-labs/windmill-ee-private.git" /tmp/ee-repo 2>/dev/null
EE_CHANGED=$(git -C /tmp/ee-repo diff --name-only "$OLD_REF".."$NEW_REF" 2>/dev/null || echo "")
echo "EE changed files:"
echo "$EE_CHANGED"
if echo "$EE_CHANGED" | grep -q '^windmill-git-sync/'; then
echo "should_run=true" >> "$GITHUB_OUTPUT"
echo "Relevant: EE git sync files changed"
exit 0
fi
fi
fi
echo "should_run=false" >> "$GITHUB_OUTPUT"
echo "No git sync relevant changes detected, skipping tests"
git_sync_e2e:
needs: [check-relevance]
if: needs.check-relevance.outputs.should_run == 'true'
runs-on: ubicloud-standard-16
services:
postgres:
image: postgres:14
ports:
- 5432:5432
env:
POSTGRES_DB: windmill
POSTGRES_PASSWORD: changeme
options: >-
--health-cmd pg_isready --health-interval 10s --health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.ref }}
fetch-depth: 0
- name: Read EE repo commit hash
run: |
echo "ee_repo_ref=$(cat ./backend/ee-repo-ref.txt)" >> "$GITHUB_ENV"
- uses: actions/checkout@v4
with:
repository: windmill-labs/windmill-ee-private
path: ./windmill-ee-private
ref: ${{ env.ee_repo_ref }}
token: ${{ secrets.WINDMILL_EE_PRIVATE_ACCESS }}
fetch-depth: 0
- name: Substitute EE code
run: |
cd backend && ./substitute_ee_code.sh --copy --dir ./windmill-ee-private
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
cache-workspaces: backend
toolchain: 1.93.0
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.10
- uses: denoland/setup-deno@v2
with:
deno-version: v2.x
- uses: actions/setup-node@v4
with:
node-version: "20"
- name: Install wmill CLI
run: |
cd cli && bash gen_wm_client.sh && bun install
mkdir -p "$HOME/.local/bin"
printf '#!/bin/sh\nexec bun run "%s/cli/src/main.ts" "$@"\n' "$GITHUB_WORKSPACE" > "$HOME/.local/bin/wmill"
chmod +x "$HOME/.local/bin/wmill"
echo "$HOME/.local/bin" >> $GITHUB_PATH
- name: Build Windmill
working-directory: ./backend
env:
SQLX_OFFLINE: true
CARGO_BUILD_JOBS: 12
RUSTFLAGS: ""
run: |
cargo build --features enterprise,private,license,zip
- name: Start Gitea
run: |
docker run -d --name gitea \
-e GITEA__database__DB_TYPE=sqlite3 \
-e GITEA__security__INSTALL_LOCK=true \
-e GITEA__server__HTTP_PORT=3000 \
-e GITEA__server__ROOT_URL=http://localhost:3000 \
-e GITEA__service__DISABLE_REGISTRATION=false \
-p 3000:3000 \
gitea/gitea:1.22-rootless
echo "Waiting for Gitea to be ready..."
for i in $(seq 1 30); do
if curl -sf http://localhost:3000/api/v1/version > /dev/null 2>&1; then
echo "Gitea is ready"
break
fi
sleep 2
done
curl -sf http://localhost:3000/api/v1/version > /dev/null || { echo "Gitea failed to start"; exit 1; }
- name: Start Windmill
working-directory: ./backend
env:
DATABASE_URL: postgres://postgres:changeme@localhost:5432/windmill
LICENSE_KEY: ${{ secrets.WM_LICENSE_KEY_CI }}
DENO_PATH: deno
BUN_PATH: bun
NODE_BIN_PATH: node
run: |
./target/debug/windmill &
echo "Waiting for Windmill to be ready..."
for i in $(seq 1 60); do
if curl -sf http://localhost:8000/api/version > /dev/null 2>&1; then
echo "Windmill is ready"
break
fi
sleep 2
done
curl -sf http://localhost:8000/api/version > /dev/null || { echo "Windmill failed to start"; exit 1; }
- name: Run git sync E2E tests
timeout-minutes: 10
env:
GITEA_DOCKER_URL: http://localhost:3000
LICENSE_KEY: ${{ secrets.WM_LICENSE_KEY_CI }}
run: |
python3 -m venv .venv
.venv/bin/pip install -r integration_tests/requirements.txt
cd integration_tests && ../.venv/bin/python -m unittest -v test.git_sync_test
- name: Archive logs
uses: actions/upload-artifact@v4
if: always()
with:
name: Git Sync Integration Tests Logs
path: |
integration_tests/logs

View File

@@ -14,7 +14,7 @@ jobs:
with:
node-version: "20.x"
registry-url: "https://registry.npmjs.org"
- run: cd typescript-client && ./publish.sh && cd ..
- run: cd typescript-client && ./publish.sh --access public && cd ..
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
publish_cli:
@@ -28,6 +28,6 @@ jobs:
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- run: cd cli && ./build.sh && cd npm && npm publish
- run: cd cli && ./build.sh && cd npm && npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

1
.gitignore vendored
View File

@@ -27,3 +27,4 @@ typescript-client/node_modules
frontend/.svelte-kit
backend/chrome_profiler.json
.fast-check/
__pycache__/

View File

@@ -16,7 +16,8 @@ lifecycleHooks:
preRemove: bash ./scripts/pre-remove.sh
auto_name:
model: gemini-2.5-flash-lite
provider: claude
model: haiku
# Each service defines a port env var that webmux injects into pane and agent
# process environments when creating a worktree. Ports are auto-assigned:
@@ -32,7 +33,7 @@ services:
portStep: 10
profiles:
default:
full:
runtime: host
yolo: true
envPassthrough: []
@@ -44,7 +45,9 @@ profiles:
- Pane 2: frontend (npm run dev)
To check logs, use: \`tmux capture-pane -t .1 -p -S -50\` (backend) or \`tmux capture-pane -t .2 -p -S -50\` (frontend).
When restarting backend or frontend, make sure to use ${BACKEND_PORT} and ${FRONTEND_PORT}.
To connect to the database, use this connection string: ${DATABASE_URL}
Because we are running backend with cargo watch, to verify your changes, just check the logs in the backend pane. No need for cargo check.
IMPORTANT: Read docs/autonomous-mode.md before starting any work.
panes:
- id: agent
kind: agent
@@ -52,14 +55,51 @@ profiles:
- id: backend
kind: command
split: right
command: ROOT="$(git rev-parse --show-toplevel)"; [ -f "$ROOT/.env.local" ] && source "$ROOT/.env.local"; cd "$ROOT/backend" && PORT=${BACKEND_PORT:-8000} cargo watch -x "run ${CARGO_FEATURES:+--features $CARGO_FEATURES}"
command: ROOT="$(git rev-parse --show-toplevel)"; cd "$ROOT/backend" && PORT=${BACKEND_PORT:-8000} cargo watch -x "run ${CARGO_FEATURES:+--features $CARGO_FEATURES}"
- id: frontend
kind: command
split: bottom
command: ROOT="$(git rev-parse --show-toplevel)"; [ -f "$ROOT/.env.local" ] && source "$ROOT/.env.local"; cd "$ROOT/frontend" && npm run generate-backend-client && REMOTE=${REMOTE:-http://localhost:${BACKEND_PORT:-8000}} npm run dev -- --port ${FRONTEND_PORT:-3000} --host 0.0.0.0
command: ROOT="$(git rev-parse --show-toplevel)"; cd "$ROOT/frontend" && npm run generate-backend-client && REMOTE=${REMOTE:-http://localhost:${BACKEND_PORT:-8000}} npm run dev -- --port ${FRONTEND_PORT:-3000} --host 0.0.0.0
frontendOnly:
runtime: host
yolo: true
envPassthrough: []
systemPrompt: >
You are running inside a tmux session with other panes running services.
Pane layout (current window):
- Pane 0: this pane (claude agent)
- Pane 1: frontend (npm run dev)
To check logs, use: \`tmux capture-pane -t .1 -p -S -50\` (frontend).
When restarting frontend, make sure to use ${FRONTEND_PORT}.
To connect to the database, use this connection string: ${DATABASE_URL}
Because we are running frontend with npm run dev, to verify your changes, just check the logs in the frontend pane. No need for npm run build.
IMPORTANT: Read docs/autonomous-mode.md before starting any work.
panes:
- id: agent
kind: agent
focus: true
- id: frontend
kind: command
split: right
command: ROOT="$(git rev-parse --show-toplevel)"; cd "$ROOT/frontend" && npm run generate-backend-client && npm run dev -- --port ${FRONTEND_PORT:-3000} --host 0.0.0.0
agentOnly:
runtime: host
yolo: true
envPassthrough: []
systemPrompt: >
IMPORTANT: Read docs/autonomous-mode.md before starting any work.
panes:
- id: agent
kind: agent
focus: true
integrations:
github:
linkedRepos: []
linkedRepos:
- repo: windmill-labs/windmill-ee-private
alias: ee-private
dir: ../windmill-ee-private__worktrees
linear:
enabled: true

View File

@@ -67,6 +67,7 @@ files:
copy:
- backend/.env
- scripts/
- wm-ts-nav/target/release/wm-ts-nav
sandbox:
enabled: false

View File

@@ -1,5 +1,58 @@
# Changelog
## [1.657.0](https://github.com/windmill-labs/windmill/compare/v1.656.0...v1.657.0) (2026-03-14)
### Features
* add datatable config support to CLI settings sync and backend export ([#8024](https://github.com/windmill-labs/windmill/issues/8024)) ([5df37fb](https://github.com/windmill-labs/windmill/commit/5df37fb0dbf9190a430f066cf2d3c48914782e53))
## [1.656.0](https://github.com/windmill-labs/windmill/compare/v1.655.0...v1.656.0) (2026-03-13)
### Features
* add GitHub Enterprise Server (GHES) support for GitHub App git sync ([#8344](https://github.com/windmill-labs/windmill/issues/8344)) ([2e430c4](https://github.com/windmill-labs/windmill/commit/2e430c4c0b8540df7b6997434a7a9f9134858026))
* **cli:** add unified generate-metadata command ([#8335](https://github.com/windmill-labs/windmill/issues/8335)) ([4c2c165](https://github.com/windmill-labs/windmill/commit/4c2c165a5b757bd5f2f49074bb290407bce3b2fb))
### Bug Fixes
* **ci:** add NODE_AUTH_TOKEN for npm publish authentication ([2a8e276](https://github.com/windmill-labs/windmill/commit/2a8e276b6d2761bb2798b6bc5f8d90ab34fbb403))
* **ci:** remove provenance flag and use NPM_TOKEN for npm publish ([44dd3ee](https://github.com/windmill-labs/windmill/commit/44dd3ee8cd05d288828d1d46c84cbcdf40f8fa78))
* **cli:** exclude raw app backend files from script metadata generation ([#8362](https://github.com/windmill-labs/windmill/issues/8362)) ([060687b](https://github.com/windmill-labs/windmill/commit/060687b1fa6b627a7b06fbdc4b3f4eb0b63411c0))
* **cli:** normalize path separators in generate-metadata folder filter for Windows ([#8358](https://github.com/windmill-labs/windmill/issues/8358)) ([404ae09](https://github.com/windmill-labs/windmill/commit/404ae09d429fb545610ba17d747e1903c542d4a3))
* **cli:** suppress verbose lock generation messages in generate-metadata ([#8357](https://github.com/windmill-labs/windmill/issues/8357)) ([51933be](https://github.com/windmill-labs/windmill/commit/51933be3cabd853960d384cd358c7bcaef6bfa86))
* **frontend:** collapse flow topbar buttons to icon-only in narrow panes ([#8322](https://github.com/windmill-labs/windmill/issues/8322)) ([b585dee](https://github.com/windmill-labs/windmill/commit/b585dee64dfd63d20812ca969b17ff9ee9989493))
* **frontend:** filter webhook/email tokens by scope instead of label ([#8363](https://github.com/windmill-labs/windmill/issues/8363)) ([0d31c35](https://github.com/windmill-labs/windmill/commit/0d31c35f3e12d637c757a95fe350294002cbf640))
* **frontend:** improve native mode alert message and fix workspaced tag detection ([#8361](https://github.com/windmill-labs/windmill/issues/8361)) ([fb12b31](https://github.com/windmill-labs/windmill/commit/fb12b31df081b2f1ac63becea6e6538ca80f8c46))
* **frontend:** prevent duplicate and reserved agent tool names ([#8367](https://github.com/windmill-labs/windmill/issues/8367)) ([c431053](https://github.com/windmill-labs/windmill/commit/c431053a1e24ef29cd551a86de4d013fd7f158be))
* graceful shutdown instead of panic on job completion channel failure ([#8345](https://github.com/windmill-labs/windmill/issues/8345)) ([724d135](https://github.com/windmill-labs/windmill/commit/724d1350d070fcf078034a52166d3048fb74e6f3))
* Linked resources and vars not triggering both sync jobs on delete ([#8342](https://github.com/windmill-labs/windmill/issues/8342)) ([8e3b8bd](https://github.com/windmill-labs/windmill/commit/8e3b8bdfd2ded9652bc7e876c6bcd0ac2cfae148))
* lower default indexer memory/batch settings to prevent OOM ([#8347](https://github.com/windmill-labs/windmill/issues/8347)) ([d9d45cf](https://github.com/windmill-labs/windmill/commit/d9d45cf2f9235b0e7118d0fc97ccdc0776ca9726))
## [1.655.0](https://github.com/windmill-labs/windmill/compare/v1.654.0...v1.655.0) (2026-03-12)
### Features
* add auto_commit option to Kafka triggers with advanced UI badges ([#8317](https://github.com/windmill-labs/windmill/issues/8317)) ([ec20d76](https://github.com/windmill-labs/windmill/commit/ec20d76216492086842c4f5e4e3b36727a5631e9))
* partition audit log table by day with configurable retention ([#8292](https://github.com/windmill-labs/windmill/issues/8292)) ([2aef01d](https://github.com/windmill-labs/windmill/commit/2aef01d18c0723aedcc626f4f3991195620774ab))
* support minimal telemetry mode ([#8243](https://github.com/windmill-labs/windmill/issues/8243)) ([fe1519f](https://github.com/windmill-labs/windmill/commit/fe1519f1284aadd67d5dce46cf0cb52ab351f789))
### Bug Fixes
* **cli:** instruct agent to tell user about generate-metadata and sync push instead of running them ([#8318](https://github.com/windmill-labs/windmill/issues/8318)) ([7fb729c](https://github.com/windmill-labs/windmill/commit/7fb729cc8483a2e6966a8e8995678929f4d451a0))
* fix saved inputs popover infinite loop ([#8311](https://github.com/windmill-labs/windmill/issues/8311)) ([425a75e](https://github.com/windmill-labs/windmill/commit/425a75e030b15fe65676169f9069fbb7da19828e))
* native mode now properly sets DB pool size and sleep queue ([#8332](https://github.com/windmill-labs/windmill/issues/8332)) ([d8b4132](https://github.com/windmill-labs/windmill/commit/d8b4132b9ae90af759c6655f4f69479f6738e60a))
* prevent zombie jobs from looping forever ([#8313](https://github.com/windmill-labs/windmill/issues/8313)) ([48bc3e2](https://github.com/windmill-labs/windmill/commit/48bc3e244558dccb1f08f455b299600861788b0d))
* set min_connections(0) to prevent sqlx pool spin loop ([#8334](https://github.com/windmill-labs/windmill/issues/8334)) ([bf4340f](https://github.com/windmill-labs/windmill/commit/bf4340f40c1eb9cacee4c32e07ba44f2c92bf7c4))
* show diff editor content for resources without a language ([#8331](https://github.com/windmill-labs/windmill/issues/8331)) ([cbc7e78](https://github.com/windmill-labs/windmill/commit/cbc7e78f8a60bff1d8730a6183cdbc9125d8e2b1))
* skip python preinstall on native workers ([#8329](https://github.com/windmill-labs/windmill/issues/8329)) ([4306c9e](https://github.com/windmill-labs/windmill/commit/4306c9e4fef317e298a76924edb4f20aa7ced105))
* skip token expiry notifications for debugger and mcp-oauth tokens ([#8316](https://github.com/windmill-labs/windmill/issues/8316)) ([8667329](https://github.com/windmill-labs/windmill/commit/86673291100fd16aaf216ed33ca9b648b8a2b7a5))
* use !inline ref for scripts inside flows (preproc, error, ai tool) ([#8319](https://github.com/windmill-labs/windmill/issues/8319)) ([ca8a627](https://github.com/windmill-labs/windmill/commit/ca8a6274bc81ad49fa0c6166694ae4d65a4048cb))
## [1.654.0](https://github.com/windmill-labs/windmill/compare/v1.653.0...v1.654.0) (2026-03-10)

View File

@@ -4,7 +4,7 @@ Open-source platform for internal tools, workflows, API integrations, background
## Workflow
1. **Understand**: Before coding, read relevant docs from `docs/` to understand the area you're changing
1. **Understand**: Before coding, explore the codebase (see Code Navigation below). Use `outline` to understand file structure, `body` to read specific symbols, `def`/`callers`/`callees` to trace code, `Grep` to find usages. Read `docs/` for domain context.
2. **Plan**: For non-trivial changes, use plan mode. For large features, break into reviewable stages
3. **Execute**: Follow coding patterns from skills (`rust-backend`, `svelte-frontend`)
4. **Validate**: After every change, run the appropriate checks per `docs/validation.md`
@@ -14,7 +14,8 @@ Open-source platform for internal tools, workflows, API integrations, background
- **Validation**: `docs/validation.md` — what checks to run based on what you changed
- **Enterprise**: `docs/enterprise.md` — EE file conventions and PR workflow
- **Backend patterns**: use the `rust-backend` skill when writing Rust code
- **Frontend patterns**: use the `svelte-frontend` skill when writing Svelte code
- **Frontend patterns**: use the `svelte-frontend` skill when writing Svelte code. Do NOT edit svelte files unless you have read that skill.
- **Code review**: use `/local-review` to review a PR for bugs and CLAUDE.md compliance
- **Domain guides**: `.claude/skills/native-trigger/` and `frontend/tutorial-system-guide.mdc`
- **Brand/UI guidelines**: `frontend/brand-guidelines.md`
@@ -33,6 +34,7 @@ Open-source platform for internal tools, workflows, API integrations, background
Using `$bindable(default_value)` on props that can be `undefined` is **banned**. This pattern causes subtle bugs because the default value masks the `undefined` state.
**Bad:**
```svelte
let { my_prop = $bindable(default_value) }: { my_prop?: string } = $props()
```
@@ -40,6 +42,7 @@ let { my_prop = $bindable(default_value) }: { my_prop?: string } = $props()
**Correct alternatives:**
1. **Use `$derived` with nullish coalescing** — handle the potential `undefined` at the usage site:
```svelte
let { my_prop = $bindable() }: { my_prop?: string } = $props()
let effective_value = $derived(my_prop ?? default_value)
@@ -47,8 +50,35 @@ let { my_prop = $bindable(default_value) }: { my_prop?: string } = $props()
2. **Create a `useMyPropState()` helper** — encapsulate the undefined-handling logic in a reusable function and call it higher in the component tree, so the child component always receives a defined value.
## Code Navigation
`wm-ts-nav` is an AST-aware code navigator. Use **wm-ts-nav** for structural queries — it skips comments/strings and understands symbol boundaries.
**MUST use `outline` before `Read`** on unfamiliar files — a 500-line file costs ~500 lines of context, while `outline` costs ~20. Then **MUST use `body "X"`** instead of reading a full file to see one function/struct. Use `Read` with offset/limit only when you need surrounding context that `body` doesn't capture.
- `refs "X" --caller` instead of reading files to find which function contains each reference
- `callers "X"` / `callees "X"` for call-graph questions
```bash
NAV="sh wm-ts-nav/nav"
# Use --root backend for Rust, --root frontend/src for TS/Svelte
$NAV --root backend outline backend/path/to/file.rs # file structure
$NAV --root backend def "ServiceName" # find definition
$NAV --root backend body "decrypt_oauth_data" # extract source code
$NAV --root backend search "%" --parent ServiceName # methods on a type
$NAV --root backend search "Trigger" --kind struct # find by kind
$NAV --root backend refs "X" --file handler.rs --caller # scoped refs with caller
$NAV --root backend callers "X" # who calls X?
$NAV --root backend callees "X" # what does X call?
```
**Limitations** — syntax-level analysis, no type inference. Use **Grep** instead when completeness matters (finding all usages, exhaustiveness checks):
- `refs`/`callers`/`callees` can't follow re-exports, glob imports, or different import paths to the same symbol
- Trait impls, macro-generated symbols (`sqlx::FromRow`), and namespace member access (`ns.X`) are invisible
- `callees` shows all identifiers in a function body, not just actual calls
## Core Principles
- **MUST `outline` before `Read`** on unfamiliar files — then `body` or `Read` with offset/limit for specifics
- Search for existing code to reuse before writing new code
- Follow established patterns in the codebase
- Keep changes focused — don't refactor beyond what's asked

View File

@@ -268,11 +268,11 @@ RUN bun install -g windmill-cli \
RUN curl -fsSL https://claude.ai/install.sh | bash \
&& cp /root/.local/share/claude/versions/* /usr/bin/claude
COPY --from=php:8.3.7-cli /usr/local/bin/php /usr/bin/php
COPY --from=composer:2.7.6 /usr/bin/composer /usr/bin/composer
COPY --from=php:8.3.30-cli /usr/local/bin/php /usr/bin/php
COPY --from=composer:2.9.5 /usr/bin/composer /usr/bin/composer
# add the docker client to call docker from a worker if enabled
COPY --from=docker:dind /usr/local/bin/docker /usr/local/bin/
COPY --from=docker:29-dind /usr/local/bin/docker /usr/local/bin/
ENV RUSTUP_HOME="/tmp/windmill/cache/rustup"
ENV CARGO_HOME="/tmp/windmill/cache/cargo"

View File

@@ -192,70 +192,6 @@ sandbox:
This mounts both the main EE repo (used by the main worktree) and the EE worktrees directory (used by feature worktrees) into every sandbox container.
## Cursor SSH Integration (`wmc`)
`wm-cursor` (aliased as `wmc`) gives each worktree its own Cursor SSH remote window with an independently-focused tmux session. All windows are visible in the status bar across all Cursor terminals, but each one is focused on its own worktree.
This uses **grouped tmux sessions** — multiple sessions that share the same window list but track focus independently:
```
tmux session: main <-- your main Cursor terminal
tmux session: cursor-feat-a <-- Cursor window for feat-a (focused on wm-feat-a)
tmux session: cursor-feat-b <-- Cursor window for feat-b (focused on wm-feat-b)
\__ all three share the same windows in the status bar
```
### Setup
Run once from inside tmux on the remote:
```bash
./scripts/wm-cursor setup /home/hugo/projects/windmill
```
This:
1. **Merges `.vscode/settings.json`** — adds the `wm-tmux` terminal profile (auto-attaches to the `main` tmux session), disables auto port forwarding, configures forwarding for ports 8000/3000/5432, and stops rust-analyzer from auto-starting. Existing settings are preserved.
2. **Creates `.vscode/tasks.json`** — auto-starts the dev database (`start-dev-db.sh`) when the folder opens.
3. **Adds `wmc` alias to `~/.zshrc`** — so you can use `wmc` from any tmux window.
4. **Adds `eval "$(wmc completions)"`** to `~/.zshrc` — provides tab-completion for subcommands and worktree names (for `open`, `open-ee`, and `close`).
After setup, reopen Cursor's terminal to pick up the new profile.
### Usage
All commands run from inside a tmux session (i.e., from Cursor's integrated terminal after setup).
**Create a new worktree + open Cursor:**
```bash
wmc add -A -p "implement feature X"
```
This runs `workmux add`, creates a grouped tmux session, writes `.vscode/settings.json` in the worktree (with port forwarding matching the worktree's assigned ports), and opens a new Cursor window.
**Open Cursor for an existing worktree:**
```bash
wmc open my-feature
```
**Open the EE worktree in Cursor (no tmux session):**
```bash
wmc open-ee my-feature
```
This finds the matching `windmill-ee-private__worktrees/<name>` directory and opens it in a new Cursor window.
**Close a worktree's Cursor window and tmux window (keeps the worktree):**
```bash
wmc close my-feature
```
This kills the grouped tmux session and calls `workmux close` to close the tmux window. The worktree and branch are preserved. Grouped sessions are also automatically cleaned up when you `workmux rm` a worktree (via `scripts/worktree-cleanup`).
## Cargo Features
To build the backend with specific Cargo features (e.g., `enterprise`, `parquet`), pass them via `CARGO_FEATURES`. The backend pane reads this from `.env.local` and appends `--features <value>` to the `cargo watch` command.
@@ -270,20 +206,6 @@ CARGO_FEATURES="enterprise,parquet" wm add my-feature
This gets written to `.env.local` by the `post_create` hook (`scripts/worktree-env`), and the backend pane picks it up automatically.
**With `wmc` (wm-cursor):**
Use the `--features` flag:
```bash
# Create a new worktree with features
wmc add --features "enterprise,parquet" -A -p "implement feature X"
# Open an existing worktree with different features
wmc open my-feature --features "enterprise,parquet"
```
The `--features` flag exports `CARGO_FEATURES` so the `post_create` hook writes it to `.env.local`. When using `wmc open`, it updates the existing `.env.local` with the new features.
## Login
Default credentials: `admin@windmill.dev` / `changeme`

View File

@@ -0,0 +1,41 @@
{
"db_name": "PostgreSQL",
"query": "SELECT id, topic, partition, \"offset\" FROM kafka_pending_commits\n WHERE workspace_id = $1 AND kafka_trigger_path = $2\n ORDER BY id",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "topic",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "partition",
"type_info": "Int4"
},
{
"ordinal": 3,
"name": "offset",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false,
false,
false,
false
]
},
"hash": "038d2fde90fa9e99e30d15161777fa3ab402e33edfca46daa95b52e525424586"
}

View File

@@ -46,11 +46,11 @@
]
},
"nullable": [
false,
false,
false,
false,
false,
true,
true,
true,
true,
true,
true,
true
]

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE kafka_trigger\n SET\n kafka_resource_path = $1,\n group_id = $2,\n topics = $3,\n filters = $4,\n auto_offset_reset = $5,\n script_path = $6,\n path = $7,\n is_flow = $8,\n edited_by = $9,\n email = $10,\n edited_at = now(),\n server_id = NULL,\n error = NULL,\n error_handler_path = $13,\n error_handler_args = $14,\n retry = $15\n WHERE\n workspace_id = $11 AND path = $12\n ",
"query": "\n UPDATE kafka_trigger\n SET\n kafka_resource_path = $1,\n group_id = $2,\n topics = $3,\n filters = $4,\n auto_offset_reset = $5,\n auto_commit = $6,\n script_path = $7,\n path = $8,\n is_flow = $9,\n edited_by = $10,\n email = $11,\n edited_at = now(),\n server_id = NULL,\n error = NULL,\n error_handler_path = $14,\n error_handler_args = $15,\n retry = $16\n WHERE\n workspace_id = $12 AND path = $13\n ",
"describe": {
"columns": [],
"parameters": {
@@ -10,6 +10,7 @@
"VarcharArray",
"JsonbArray",
"Varchar",
"Bool",
"Varchar",
"Varchar",
"Bool",
@@ -24,5 +25,5 @@
},
"nullable": []
},
"hash": "12631fecee6aa11a45cf5c8d101c0dd8de50ac9b57e68198f637038d344fdd46"
"hash": "072e5ab78f929c6b7264f98c1588cb24cc635836276ee6faa2438f494bfbce04"
}

View File

@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT reset_offset FROM kafka_trigger WHERE workspace_id = $1 AND path = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "reset_offset",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false
]
},
"hash": "1df610a583e86edb70c374fd66c68554a6a4291426c09dd5b04fd832f9d31208"
}

View File

@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM variable WHERE path = $1 AND workspace_id = $2 RETURNING path",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "path",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false
]
},
"hash": "3317484a9c09c07c2c9db9debaecc4a4d518093ab48e79365dbb808068e0b8ff"
}

View File

@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT (config->>'native_mode')::boolean FROM config WHERE name = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "bool",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
null
]
},
"hash": "36b95bc7956eb7bba7cd6fa9cd829980a0bf4970b919cabad1daab16627404fc"
}

View File

@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO kafka_trigger (\n path, kafka_resource_path, topics, group_id, script_path,\n is_flow, workspace_id, edited_by, email, auto_commit\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"VarcharArray",
"Varchar",
"Varchar",
"Bool",
"Varchar",
"Varchar",
"Varchar",
"Bool"
]
},
"nullable": []
},
"hash": "45fc21026fa76e5d69f00a68a7be81abb3ec627578f2d14f0ce33896dc6ab4cf"
}

View File

@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE workspace_settings\n SET git_app_installations = (\n SELECT jsonb_agg(\n CASE\n WHEN (elem->>'installation_id')::bigint = $2 THEN $1::jsonb\n ELSE elem\n END\n )\n FROM jsonb_array_elements(git_app_installations) AS elem\n )\n WHERE workspace_id = $3\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Jsonb",
"Int8",
"Text"
]
},
"nullable": []
},
"hash": "48b394bd9ca63d33a7ea97113b0096bd0777da52c05e23262572089e0c3c6c46"
}

View File

@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT auto_commit FROM kafka_trigger WHERE workspace_id = $1 AND path = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "auto_commit",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false
]
},
"hash": "4b2a29b3ef7ec4802d81ec4b706623b991c938e40d0db25290b03dc0577c2740"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT kafka_resource_path, topics, group_id, mode AS \"mode: String\"\n FROM kafka_trigger\n WHERE workspace_id = $1 AND path = $2\n ",
"query": "\n SELECT kafka_resource_path, topics, group_id, mode AS \"mode: String\",\n auto_offset_reset, auto_commit, reset_offset\n FROM kafka_trigger\n WHERE workspace_id = $1 AND path = $2\n ",
"describe": {
"columns": [
{
@@ -33,6 +33,21 @@
}
}
}
},
{
"ordinal": 4,
"name": "auto_offset_reset",
"type_info": "Varchar"
},
{
"ordinal": 5,
"name": "auto_commit",
"type_info": "Bool"
},
{
"ordinal": 6,
"name": "reset_offset",
"type_info": "Bool"
}
],
"parameters": {
@@ -42,11 +57,14 @@
]
},
"nullable": [
false,
false,
false,
false,
false,
false,
false
]
},
"hash": "7e3bfb33fb771aec39b43a7550091ce7c9b1261b52d10f4a7f3273fed3c916df"
"hash": "4cf4be7a981173d3f242887d9313c7e60d23e9827f23c0de5b546ed56697d54a"
}

View File

@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT auto_commit\n FROM kafka_trigger\n WHERE workspace_id = $1 AND path = $2\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "auto_commit",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false
]
},
"hash": "50807b807bb901a380926798be655c13a18dfd26e237a8218d3006e2898b5aa3"
}

View File

@@ -15,7 +15,7 @@
]
},
"nullable": [
true
null
]
},
"hash": "5a219a2532517869578c4504ff3153c43903f929ae5d62fbba12610f89c36d55"

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO kafka_trigger (\n workspace_id,\n path,\n kafka_resource_path,\n group_id,\n topics,\n filters,\n auto_offset_reset,\n script_path,\n is_flow,\n mode,\n edited_by,\n email,\n edited_at,\n error_handler_path,\n error_handler_args,\n retry\n ) VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, now(), $13, $14, $15\n )\n ",
"query": "\n INSERT INTO kafka_trigger (\n workspace_id,\n path,\n kafka_resource_path,\n group_id,\n topics,\n filters,\n auto_offset_reset,\n auto_commit,\n script_path,\n is_flow,\n mode,\n edited_by,\n email,\n edited_at,\n error_handler_path,\n error_handler_args,\n retry\n ) VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, now(), $14, $15, $16\n )\n ",
"describe": {
"columns": [],
"parameters": {
@@ -12,6 +12,7 @@
"VarcharArray",
"JsonbArray",
"Varchar",
"Bool",
"Varchar",
"Bool",
{
@@ -35,5 +36,5 @@
},
"nullable": []
},
"hash": "4b5a711986017654bdd495893a16ddc6ab09c98cd8723865cbd341404bc6a02f"
"hash": "5dd6315ec270c268e905262e4b0a920837354d91a0ae16b1236c1267da71765f"
}

View File

@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM kafka_pending_commits WHERE id = ANY($1)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8Array"
]
},
"nullable": []
},
"hash": "80bad96cbec6b5eca57a6380e7515565490a271050dcc4b5aac2b730ae3a55b9"
}

View File

@@ -0,0 +1,28 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n (elem->>'installation_id')::bigint as installation_id,\n elem->>'github_base_url' as github_base_url\n FROM workspace_settings,\n LATERAL jsonb_array_elements(git_app_installations) AS elem\n WHERE workspace_id = $1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "installation_id",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "github_base_url",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
null,
null
]
},
"hash": "ad5fc9212a123a8328397496ef3b5eea1780226698e419ac59ba55012296913d"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n (elem->>'installation_id')::bigint as installation_id,\n elem->>'account_id' as account_id\n FROM workspace_settings,\n LATERAL jsonb_array_elements(git_app_installations) AS elem\n WHERE workspace_id = $1\n ",
"query": "\n SELECT\n (elem->>'installation_id')::bigint as installation_id,\n elem->>'account_id' as account_id,\n elem->>'github_base_url' as github_base_url\n FROM workspace_settings,\n LATERAL jsonb_array_elements(git_app_installations) AS elem\n WHERE workspace_id = $1\n ",
"describe": {
"columns": [
{
@@ -12,6 +12,11 @@
"ordinal": 1,
"name": "account_id",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "github_base_url",
"type_info": "Text"
}
],
"parameters": {
@@ -20,9 +25,10 @@
]
},
"nullable": [
null,
null,
null
]
},
"hash": "0ee14619dd81df460b2b8cc6df2b89646279f77469c35deffca8e17a11d7f6c8"
"hash": "ae7adc583cdd3f876164ed60569ed531b05eaa17fccc599306eb1a96a65ee761"
}

View File

@@ -1,16 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE workspace_settings\n SET git_app_installations = (\n SELECT jsonb_agg(\n CASE\n WHEN (elem->>'installation_id')::bigint = $2 THEN $1::jsonb\n ELSE elem\n END\n )\n FROM jsonb_array_elements(git_app_installations) AS elem\n )\n WHERE workspace_id = $3\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Jsonb",
"Int8",
"Text"
]
},
"nullable": []
},
"hash": "b4d48c820bf41619bffa8f62367e98369e1d93514e1618723a34bf96080d4ebc"
}

View File

@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n (elem->>'installation_id')::bigint as installation_id\n FROM workspace_settings,\n LATERAL jsonb_array_elements(git_app_installations) AS elem\n WHERE workspace_id = $1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "installation_id",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
null
]
},
"hash": "be00ac55e8668a0ed3befda7d8595c7cda0cba0b119d4fdb8a0dea1b28a1d560"
}

View File

@@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM resource WHERE path = $1 AND workspace_id = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": []
},
"hash": "bf2aeb9a1e649106d2a084c1d628690a44573c1869a206474811215714ba97c2"
}

View File

@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE kafka_trigger SET reset_offset = true, server_id = NULL WHERE workspace_id = $1 AND path = $2 RETURNING true",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "?column?",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
null
]
},
"hash": "c2f38c9e09aac73d10e8f327715927c07832badb2c9145d5996b829163bdf7d9"
}

View File

@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE kafka_trigger SET reset_offset = false WHERE workspace_id = $1 AND path = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": []
},
"hash": "ef15599f532fab2cbb487542ffec047cf3b7ce22ce868db1b1a63e6c10d0d12b"
}

View File

@@ -0,0 +1,18 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO kafka_pending_commits (workspace_id, kafka_trigger_path, topic, partition, \"offset\")\n VALUES ($1, $2, $3, $4, $5)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Varchar",
"Int4",
"Int8"
]
},
"nullable": []
},
"hash": "f67e5c96eb9cb35953d4c3e83e0fcbb5b647737e0366529a2f418218b1a74679"
}

254
backend/Cargo.lock generated
View File

@@ -169,9 +169,9 @@ dependencies = [
[[package]]
name = "anstream"
version = "0.6.21"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
dependencies = [
"anstyle",
"anstyle-parse",
@@ -184,15 +184,15 @@ dependencies = [
[[package]]
name = "anstyle"
version = "1.0.13"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
[[package]]
name = "anstyle-parse"
version = "0.2.7"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
dependencies = [
"utf8parse",
]
@@ -1860,9 +1860,9 @@ dependencies = [
[[package]]
name = "bon"
version = "3.9.0"
version = "3.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d13a61f2963b88eef9c1be03df65d42f6996dfeac1054870d950fcf66686f83"
checksum = "f47dbe92550676ee653353c310dfb9cf6ba17ee70396e1f7cf0a2020ad49b2fe"
dependencies = [
"bon-macros",
"rustversion",
@@ -1870,9 +1870,9 @@ dependencies = [
[[package]]
name = "bon-macros"
version = "3.9.0"
version = "3.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d314cc62af2b6b0c65780555abb4d02a03dd3b799cd42419044f0c38d99738c0"
checksum = "519bd3116aeeb42d5372c29d982d16d0170d3d4a5ed85fc7dd91642ffff3c67c"
dependencies = [
"darling 0.23.0",
"ident_case",
@@ -2208,9 +2208,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.2.56"
version = "1.2.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2"
checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423"
dependencies = [
"find-msvc-tools",
"jobserver",
@@ -2323,9 +2323,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.60"
version = "4.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a"
checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351"
dependencies = [
"clap_builder",
"clap_derive",
@@ -2333,9 +2333,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.60"
version = "4.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876"
checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
dependencies = [
"anstream",
"anstyle",
@@ -2345,9 +2345,9 @@ dependencies = [
[[package]]
name = "clap_derive"
version = "4.5.55"
version = "4.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5"
checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a"
dependencies = [
"heck 0.5.0",
"proc-macro2",
@@ -2357,9 +2357,9 @@ dependencies = [
[[package]]
name = "clap_lex"
version = "1.0.0"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831"
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
[[package]]
name = "clipboard-win"
@@ -2418,9 +2418,9 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
[[package]]
name = "colorchoice"
version = "1.0.4"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
[[package]]
name = "combine"
@@ -9383,9 +9383,9 @@ checksum = "80adb31078122c880307e9cdfd4e3361e6545c319f9b9dcafcb03acd3b51a575"
[[package]]
name = "once_cell"
version = "1.21.3"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "once_cell_polyfill"
@@ -9460,9 +9460,9 @@ dependencies = [
[[package]]
name = "openssl"
version = "0.10.75"
version = "0.10.76"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328"
checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf"
dependencies = [
"bitflags 2.9.4",
"cfg-if",
@@ -9507,9 +9507,9 @@ dependencies = [
[[package]]
name = "openssl-sys"
version = "0.9.111"
version = "0.9.112"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321"
checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb"
dependencies = [
"cc",
"libc",
@@ -10641,9 +10641,9 @@ dependencies = [
[[package]]
name = "quick_cache"
version = "0.6.18"
version = "0.6.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ada44a88ef953a3294f6eb55d2007ba44646015e18613d2f213016379203ef3"
checksum = "530e84778a55de0f52645a51d4e3b9554978acd6a1e7cd50b6a6784692b3029e"
dependencies = [
"ahash 0.8.12",
"equivalent",
@@ -13854,9 +13854,9 @@ dependencies = [
[[package]]
name = "tempfile"
version = "3.26.0"
version = "3.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [
"fastrand",
"getrandom 0.4.2",
@@ -14697,9 +14697,9 @@ dependencies = [
[[package]]
name = "tracing-subscriber"
version = "0.3.22"
version = "0.3.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e"
checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
dependencies = [
"matchers",
"nu-ansi-term",
@@ -15741,7 +15741,7 @@ dependencies = [
[[package]]
name = "windmill"
version = "1.654.0"
version = "1.657.0"
dependencies = [
"anyhow",
"async-nats",
@@ -15808,7 +15808,7 @@ dependencies = [
[[package]]
name = "windmill-alerting"
version = "1.654.0"
version = "1.657.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -15821,7 +15821,7 @@ dependencies = [
[[package]]
name = "windmill-api"
version = "1.654.0"
version = "1.657.0"
dependencies = [
"anyhow",
"argon2",
@@ -15941,6 +15941,7 @@ dependencies = [
"windmill-parser-py",
"windmill-parser-py-imports",
"windmill-parser-sql",
"windmill-parser-sql-asset",
"windmill-parser-ts",
"windmill-queue",
"windmill-store",
@@ -15961,7 +15962,7 @@ dependencies = [
[[package]]
name = "windmill-api-agent-workers"
version = "1.654.0"
version = "1.657.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -15984,7 +15985,7 @@ dependencies = [
[[package]]
name = "windmill-api-assets"
version = "1.654.0"
version = "1.657.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -15997,7 +15998,7 @@ dependencies = [
[[package]]
name = "windmill-api-auth"
version = "1.654.0"
version = "1.657.0"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -16023,7 +16024,7 @@ dependencies = [
[[package]]
name = "windmill-api-client"
version = "1.654.0"
version = "1.657.0"
dependencies = [
"reqwest 0.12.28",
"serde",
@@ -16033,7 +16034,7 @@ dependencies = [
[[package]]
name = "windmill-api-configs"
version = "1.654.0"
version = "1.657.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16050,7 +16051,7 @@ dependencies = [
[[package]]
name = "windmill-api-debug"
version = "1.654.0"
version = "1.657.0"
dependencies = [
"axum 0.7.9",
"base64 0.22.1",
@@ -16073,7 +16074,7 @@ dependencies = [
[[package]]
name = "windmill-api-embeddings"
version = "1.654.0"
version = "1.657.0"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -16096,7 +16097,7 @@ dependencies = [
[[package]]
name = "windmill-api-flow-conversations"
version = "1.654.0"
version = "1.657.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16112,7 +16113,7 @@ dependencies = [
[[package]]
name = "windmill-api-flows"
version = "1.654.0"
version = "1.657.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16132,7 +16133,7 @@ dependencies = [
[[package]]
name = "windmill-api-groups"
version = "1.654.0"
version = "1.657.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16152,7 +16153,7 @@ dependencies = [
[[package]]
name = "windmill-api-inputs"
version = "1.654.0"
version = "1.657.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16166,7 +16167,7 @@ dependencies = [
[[package]]
name = "windmill-api-integration-tests"
version = "1.654.0"
version = "1.657.0"
dependencies = [
"anyhow",
"async-nats",
@@ -16186,6 +16187,7 @@ dependencies = [
"windmill-api-auth",
"windmill-api-client",
"windmill-common",
"windmill-git-sync",
"windmill-native-triggers",
"windmill-test-utils",
"windmill-worker",
@@ -16193,7 +16195,7 @@ dependencies = [
[[package]]
name = "windmill-api-jobs"
version = "1.654.0"
version = "1.657.0"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -16218,7 +16220,7 @@ dependencies = [
[[package]]
name = "windmill-api-npm-proxy"
version = "1.654.0"
version = "1.657.0"
dependencies = [
"axum 0.7.9",
"flate2",
@@ -16236,7 +16238,7 @@ dependencies = [
[[package]]
name = "windmill-api-openapi"
version = "1.654.0"
version = "1.657.0"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -16257,7 +16259,7 @@ dependencies = [
[[package]]
name = "windmill-api-schedule"
version = "1.654.0"
version = "1.657.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16277,7 +16279,7 @@ dependencies = [
[[package]]
name = "windmill-api-scripts"
version = "1.654.0"
version = "1.657.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16307,7 +16309,7 @@ dependencies = [
[[package]]
name = "windmill-api-settings"
version = "1.654.0"
version = "1.657.0"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -16334,7 +16336,7 @@ dependencies = [
[[package]]
name = "windmill-api-sse"
version = "1.654.0"
version = "1.657.0"
dependencies = [
"lazy_static",
"serde",
@@ -16346,7 +16348,7 @@ dependencies = [
[[package]]
name = "windmill-api-users"
version = "1.654.0"
version = "1.657.0"
dependencies = [
"argon2",
"axum 0.7.9",
@@ -16369,7 +16371,7 @@ dependencies = [
[[package]]
name = "windmill-api-workers"
version = "1.654.0"
version = "1.657.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16383,7 +16385,7 @@ dependencies = [
[[package]]
name = "windmill-api-workspaces"
version = "1.654.0"
version = "1.657.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16414,7 +16416,7 @@ dependencies = [
[[package]]
name = "windmill-audit"
version = "1.654.0"
version = "1.657.0"
dependencies = [
"chrono",
"lazy_static",
@@ -16428,7 +16430,7 @@ dependencies = [
[[package]]
name = "windmill-autoscaling"
version = "1.654.0"
version = "1.657.0"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -16447,7 +16449,7 @@ dependencies = [
[[package]]
name = "windmill-common"
version = "1.654.0"
version = "1.657.0"
dependencies = [
"aes-gcm",
"anyhow",
@@ -16546,7 +16548,7 @@ dependencies = [
[[package]]
name = "windmill-dep-map"
version = "1.654.0"
version = "1.657.0"
dependencies = [
"chrono",
"itertools 0.14.0",
@@ -16565,7 +16567,7 @@ dependencies = [
[[package]]
name = "windmill-git-sync"
version = "1.654.0"
version = "1.657.0"
dependencies = [
"regex",
"serde",
@@ -16580,7 +16582,7 @@ dependencies = [
[[package]]
name = "windmill-indexer"
version = "1.654.0"
version = "1.657.0"
dependencies = [
"anyhow",
"astral-tokio-tar",
@@ -16604,7 +16606,7 @@ dependencies = [
[[package]]
name = "windmill-jseval"
version = "1.654.0"
version = "1.657.0"
dependencies = [
"anyhow",
"futures",
@@ -16621,7 +16623,7 @@ dependencies = [
[[package]]
name = "windmill-macros"
version = "1.654.0"
version = "1.657.0"
dependencies = [
"itertools 0.14.0",
"lazy_static",
@@ -16637,7 +16639,7 @@ dependencies = [
[[package]]
name = "windmill-mcp"
version = "1.654.0"
version = "1.657.0"
dependencies = [
"anyhow",
"async-trait",
@@ -16658,7 +16660,7 @@ dependencies = [
[[package]]
name = "windmill-native-triggers"
version = "1.654.0"
version = "1.657.0"
dependencies = [
"anyhow",
"async-trait",
@@ -16689,7 +16691,7 @@ dependencies = [
[[package]]
name = "windmill-oauth"
version = "1.654.0"
version = "1.657.0"
dependencies = [
"anyhow",
"async-oauth2",
@@ -16713,7 +16715,7 @@ dependencies = [
[[package]]
name = "windmill-object-store"
version = "1.654.0"
version = "1.657.0"
dependencies = [
"anyhow",
"async-stream",
@@ -16747,7 +16749,7 @@ dependencies = [
[[package]]
name = "windmill-operator"
version = "1.654.0"
version = "1.657.0"
dependencies = [
"anyhow",
"futures",
@@ -16765,7 +16767,7 @@ dependencies = [
[[package]]
name = "windmill-parser"
version = "1.654.0"
version = "1.657.0"
dependencies = [
"convert_case 0.6.0",
"serde",
@@ -16774,7 +16776,7 @@ dependencies = [
[[package]]
name = "windmill-parser-bash"
version = "1.654.0"
version = "1.657.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -16786,7 +16788,7 @@ dependencies = [
[[package]]
name = "windmill-parser-csharp"
version = "1.654.0"
version = "1.657.0"
dependencies = [
"anyhow",
"serde_json",
@@ -16798,7 +16800,7 @@ dependencies = [
[[package]]
name = "windmill-parser-go"
version = "1.654.0"
version = "1.657.0"
dependencies = [
"anyhow",
"gosyn",
@@ -16810,7 +16812,7 @@ dependencies = [
[[package]]
name = "windmill-parser-graphql"
version = "1.654.0"
version = "1.657.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -16822,7 +16824,7 @@ dependencies = [
[[package]]
name = "windmill-parser-java"
version = "1.654.0"
version = "1.657.0"
dependencies = [
"anyhow",
"serde_json",
@@ -16834,7 +16836,7 @@ dependencies = [
[[package]]
name = "windmill-parser-nu"
version = "1.654.0"
version = "1.657.0"
dependencies = [
"anyhow",
"nu-parser",
@@ -16845,7 +16847,7 @@ dependencies = [
[[package]]
name = "windmill-parser-php"
version = "1.654.0"
version = "1.657.0"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -16856,7 +16858,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py"
version = "1.654.0"
version = "1.657.0"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -16864,12 +16866,22 @@ dependencies = [
"rustpython-parser",
"serde_json",
"windmill-parser",
"windmill-parser-sql",
]
[[package]]
name = "windmill-parser-py-asset"
version = "1.657.0"
dependencies = [
"anyhow",
"rustpython-ast",
"rustpython-parser",
"windmill-parser",
"windmill-parser-sql-asset",
]
[[package]]
name = "windmill-parser-py-imports"
version = "1.654.0"
version = "1.657.0"
dependencies = [
"anyhow",
"async-recursion",
@@ -16893,7 +16905,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ruby"
version = "1.654.0"
version = "1.657.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -16907,7 +16919,7 @@ dependencies = [
[[package]]
name = "windmill-parser-rust"
version = "1.654.0"
version = "1.657.0"
dependencies = [
"anyhow",
"convert_case 0.6.0",
@@ -16924,7 +16936,7 @@ dependencies = [
[[package]]
name = "windmill-parser-sql"
version = "1.654.0"
version = "1.657.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -16932,6 +16944,17 @@ dependencies = [
"regex-lite",
"serde",
"serde_json",
"windmill-parser",
"windmill-types",
]
[[package]]
name = "windmill-parser-sql-asset"
version = "1.657.0"
dependencies = [
"anyhow",
"serde",
"serde_json",
"sqlparser 0.59.0",
"windmill-parser",
"windmill-types",
@@ -16939,7 +16962,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ts"
version = "1.654.0"
version = "1.657.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -16953,12 +16976,27 @@ dependencies = [
"triomphe",
"wasm-bindgen",
"windmill-parser",
"windmill-parser-sql",
]
[[package]]
name = "windmill-parser-ts-asset"
version = "1.657.0"
dependencies = [
"anyhow",
"serde-wasm-bindgen",
"swc_common",
"swc_ecma_ast",
"swc_ecma_parser",
"swc_ecma_visit",
"triomphe",
"wasm-bindgen",
"windmill-parser",
"windmill-parser-sql-asset",
]
[[package]]
name = "windmill-parser-wac"
version = "1.654.0"
version = "1.657.0"
dependencies = [
"anyhow",
"rustpython-ast",
@@ -16974,7 +17012,7 @@ dependencies = [
[[package]]
name = "windmill-parser-yaml"
version = "1.654.0"
version = "1.657.0"
dependencies = [
"anyhow",
"serde",
@@ -16985,7 +17023,7 @@ dependencies = [
[[package]]
name = "windmill-queue"
version = "1.654.0"
version = "1.657.0"
dependencies = [
"anyhow",
"async-recursion",
@@ -17022,7 +17060,7 @@ dependencies = [
[[package]]
name = "windmill-runtime-nativets"
version = "1.654.0"
version = "1.657.0"
dependencies = [
"anyhow",
"const_format",
@@ -17060,7 +17098,7 @@ dependencies = [
[[package]]
name = "windmill-sql-datatype-parser-wasm"
version = "1.654.0"
version = "1.657.0"
dependencies = [
"getrandom 0.3.4",
"wasm-bindgen",
@@ -17071,7 +17109,7 @@ dependencies = [
[[package]]
name = "windmill-store"
version = "1.654.0"
version = "1.657.0"
dependencies = [
"anyhow",
"async-recursion",
@@ -17100,7 +17138,7 @@ dependencies = [
[[package]]
name = "windmill-test-utils"
version = "1.654.0"
version = "1.657.0"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -17123,7 +17161,7 @@ dependencies = [
[[package]]
name = "windmill-trigger"
version = "1.654.0"
version = "1.657.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17156,7 +17194,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-email"
version = "1.654.0"
version = "1.657.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17176,7 +17214,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-gcp"
version = "1.654.0"
version = "1.657.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17210,7 +17248,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-http"
version = "1.654.0"
version = "1.657.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17245,7 +17283,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-kafka"
version = "1.654.0"
version = "1.657.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17268,7 +17306,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-mqtt"
version = "1.654.0"
version = "1.657.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17292,7 +17330,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-nats"
version = "1.654.0"
version = "1.657.0"
dependencies = [
"anyhow",
"async-nats",
@@ -17316,7 +17354,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-postgres"
version = "1.654.0"
version = "1.657.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17351,7 +17389,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-sqs"
version = "1.654.0"
version = "1.657.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17379,7 +17417,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-websocket"
version = "1.654.0"
version = "1.657.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17402,7 +17440,7 @@ dependencies = [
[[package]]
name = "windmill-types"
version = "1.654.0"
version = "1.657.0"
dependencies = [
"anyhow",
"bitflags 2.9.4",
@@ -17420,7 +17458,7 @@ dependencies = [
[[package]]
name = "windmill-worker"
version = "1.654.0"
version = "1.657.0"
dependencies = [
"anyhow",
"async-once-cell",
@@ -17526,7 +17564,7 @@ dependencies = [
[[package]]
name = "windmill-worker-volumes"
version = "1.654.0"
version = "1.657.0"
dependencies = [
"bytes",
"futures",

View File

@@ -1,6 +1,6 @@
[package]
name = "windmill"
version = "1.654.0"
version = "1.657.0"
authors.workspace = true
edition.workspace = true
@@ -59,6 +59,7 @@ members = [
"./windmill-oauth",
"./parsers/windmill-parser",
"./parsers/windmill-parser-ts",
"./parsers/windmill-parser-ts-asset",
"./parsers/windmill-parser-go",
"./parsers/windmill-parser-rust",
"./parsers/windmill-parser-csharp",
@@ -67,8 +68,11 @@ members = [
"./parsers/windmill-parser-ruby",
"./parsers/windmill-parser-bash",
"./parsers/windmill-parser-py",
"./parsers/windmill-parser-py-asset",
"./parsers/windmill-parser-py-imports",
"./parsers/windmill-parser-wac",
"./parsers/windmill-parser-sql",
"./parsers/windmill-parser-sql-asset",
"./parsers/windmill-sql-datatype-parser-wasm",
"./parsers/windmill-parser-yaml", "windmill-macros", "parsers/windmill-parser-nu",
"./windmill-worker-volumes",
@@ -78,7 +82,7 @@ members = [
exclude = ["./windmill-duckdb-ffi-internal"]
[workspace.package]
version = "1.654.0"
version = "1.657.0"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"
@@ -320,7 +324,9 @@ windmill-api-workers = { path = "./windmill-api-workers" }
windmill-store = { path = "./windmill-store" }
windmill-parser = { path = "./parsers/windmill-parser" }
windmill-parser-ts = { path = "./parsers/windmill-parser-ts" }
windmill-parser-ts-asset = { path = "./parsers/windmill-parser-ts-asset" }
windmill-parser-py = { path = "./parsers/windmill-parser-py" }
windmill-parser-py-asset = { path = "./parsers/windmill-parser-py-asset" }
windmill-parser-py-imports = { path = "./parsers/windmill-parser-py-imports" }
windmill-parser-go = { path = "./parsers/windmill-parser-go" }
windmill-parser-rust = { path = "./parsers/windmill-parser-rust" }
@@ -331,6 +337,7 @@ windmill-parser-ruby = { path = "./parsers/windmill-parser-ruby" }
windmill-parser-nu = { path = "./parsers/windmill-parser-nu" }
windmill-parser-bash = { path = "./parsers/windmill-parser-bash" }
windmill-parser-sql = { path = "./parsers/windmill-parser-sql" }
windmill-parser-sql-asset = { path = "./parsers/windmill-parser-sql-asset" }
windmill-parser-graphql = { path = "./parsers/windmill-parser-graphql" }
windmill-parser-php = { path = "./parsers/windmill-parser-php" }
windmill-parser-wac = { path = "./parsers/windmill-parser-wac" }

View File

@@ -1 +1 @@
cef4dfc45e6d6344c5d8d107bd2b4d1bf9bbdd64
c74c86b78a66b976fd9968b21f77903723e668ec

View File

@@ -0,0 +1,2 @@
DROP TABLE IF EXISTS kafka_pending_commits;
ALTER TABLE kafka_trigger DROP COLUMN auto_commit;

View File

@@ -0,0 +1,14 @@
ALTER TABLE kafka_trigger ADD COLUMN auto_commit BOOLEAN NOT NULL DEFAULT TRUE;
CREATE TABLE kafka_pending_commits (
id BIGSERIAL PRIMARY KEY,
workspace_id VARCHAR(50) NOT NULL,
kafka_trigger_path VARCHAR(255) NOT NULL,
topic VARCHAR(255) NOT NULL,
partition INTEGER NOT NULL,
"offset" BIGINT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
FOREIGN KEY (workspace_id, kafka_trigger_path) REFERENCES kafka_trigger(workspace_id, path) ON DELETE CASCADE
);
CREATE INDEX idx_kafka_pending_commits_trigger ON kafka_pending_commits (workspace_id, kafka_trigger_path);

View File

@@ -0,0 +1,16 @@
[package]
name = "windmill-parser-py-asset"
version.workspace = true
edition.workspace = true
authors.workspace = true
[lib]
name = "windmill_parser_py_asset"
path = "./src/lib.rs"
[dependencies]
windmill-parser.workspace = true
windmill-parser-sql-asset.workspace = true
rustpython-parser.workspace = true
rustpython-ast = { version = "0.4.0", features = ["visitor"] }
anyhow.workspace = true

View File

@@ -215,7 +215,7 @@ impl AssetsFinder {
_ => return Err(()),
};
// We use the SQL parser to detect RW, specific tables, etc.
let sql_assets = windmill_parser_sql::parse_wmill_sdk_sql_assets(
let sql_assets = windmill_parser_sql_asset::parse_wmill_sdk_sql_assets(
*kind,
path,
schema.as_deref(),

View File

@@ -10,7 +10,6 @@ path = "./src/lib.rs"
[dependencies]
windmill-parser.workspace = true
windmill-parser-sql.workspace = true
rustpython-parser.workspace = true
itertools.workspace = true
serde_json.workspace = true

View File

@@ -21,13 +21,54 @@ use rustpython_parser::{
Parse,
};
pub mod asset_parser;
pub mod pydantic_parser;
pub use asset_parser::parse_assets;
const FUNCTION_CALL: &str = "<function call>";
/// Get the simple type name from an expression (e.g. `str`, `int`).
fn simple_type_name(e: &Expr) -> Option<&str> {
match e {
Expr::Name(ExprName { id, .. }) => Some(id.as_ref()),
_ => None,
}
}
/// If `e` is `list[T]` or `List[T]`, return the inner expression `T`.
fn list_elem_expr(e: &Expr) -> Option<&Expr> {
match e {
Expr::Subscript(x) => match x.value.as_ref() {
Expr::Name(ExprName { id, .. }) if id == "list" || id == "List" => {
Some(x.slice.as_ref())
}
_ => None,
},
_ => None,
}
}
/// Detect `T | list[T]` or `list[T] | T` union patterns.
/// Returns the original type string (e.g. "str | list[str]") for use as `otyp`.
fn detect_py_union_array_otyp(e: &Expr) -> Option<String> {
let Expr::BinOp(x) = e else { return None };
// T | list[T]
if let (Some(scalar), Some(elem)) = (simple_type_name(&x.left), list_elem_expr(&x.right)) {
if let Some(elem_name) = simple_type_name(elem) {
if scalar == elem_name {
return Some(format!("{} | list[{}]", scalar, elem_name));
}
}
}
// list[T] | T
if let (Some(elem), Some(scalar)) = (list_elem_expr(&x.left), simple_type_name(&x.right)) {
if let Some(elem_name) = simple_type_name(elem) {
if scalar == elem_name {
return Some(format!("list[{}] | {}", elem_name, scalar));
}
}
}
None
}
/// Cheap string-based check to see if code might contain Pydantic models or dataclasses.
/// Returns true if we should do full AST parsing for type detection, false otherwise.
/// This avoids expensive parsing for the common case where scripts don't use these features.
@@ -393,8 +434,19 @@ pub fn parse_python_signature(
_ => {}
}
// Detect T | list[T] union types and set otyp for
// debounce accumulation support. Falls back to docstring
// description if no union array pattern is found.
let union_otyp = params.args[i]
.as_arg()
.annotation
.as_ref()
.and_then(|ann| detect_py_union_array_otyp(ann.as_ref()));
Arg {
otyp: metadata.descriptions.get(&arg_name).map(|d| d.to_string()),
otyp: union_otyp.or_else(|| {
metadata.descriptions.get(&arg_name).map(|d| d.to_string())
}),
name: arg_name,
typ,
has_default: has_default || default.is_some(),
@@ -444,6 +496,9 @@ fn parse_expr(
Expr::Constant(ExprConstant { value: Constant::None, .. })
) {
(parse_expr(&x.left, enums, module).0, true)
} else if detect_py_union_array_otyp(e.as_ref()).is_some() {
// T | list[T] — parsed type is Unknown; otyp is set separately
(Typ::Unknown, false)
} else {
(Typ::Unknown, false)
}
@@ -1049,6 +1104,33 @@ def main(a: str, b: Optional[str], c: str | None): return
Ok(())
}
#[test]
fn test_parse_python_union_array_type() -> anyhow::Result<()> {
let code = r#"
def main(items: str | list[str], numbers: list[int] | int, plain: str):
pass
"#;
let result = parse_python_signature(code, None, false)?;
assert_eq!(result.args.len(), 3);
// str | list[str] → otyp set, typ Unknown
assert_eq!(result.args[0].name, "items");
assert_eq!(result.args[0].otyp, Some("str | list[str]".to_string()));
assert_eq!(result.args[0].typ, Typ::Unknown);
// list[int] | int → otyp set, typ Unknown
assert_eq!(result.args[1].name, "numbers");
assert_eq!(result.args[1].otyp, Some("list[int] | int".to_string()));
assert_eq!(result.args[1].typ, Typ::Unknown);
// plain str → no otyp
assert_eq!(result.args[2].name, "plain");
assert_eq!(result.args[2].otyp, None);
assert_eq!(result.args[2].typ, Typ::Str(None));
Ok(())
}
#[test]
fn test_parse_python_sig_enum() -> anyhow::Result<()> {
let code = r#"

View File

@@ -0,0 +1,17 @@
[package]
name = "windmill-parser-sql-asset"
version.workspace = true
edition.workspace = true
authors.workspace = true
[lib]
name = "windmill_parser_sql_asset"
path = "./src/lib.rs"
[dependencies]
windmill-parser.workspace = true
windmill-types.workspace = true
anyhow.workspace = true
serde_json.workspace = true
serde.workspace = true
sqlparser = { version = "0.59.0", features = ["visitor"] }

View File

@@ -0,0 +1,4 @@
mod asset_parser;
mod asset_parser_utils;
pub use asset_parser::parse_assets;
pub use asset_parser_utils::parse_wmill_sdk_sql_assets;

View File

@@ -20,5 +20,4 @@ windmill-types.workspace = true
anyhow.workspace = true
lazy_static.workspace = true
serde_json.workspace = true
serde.workspace = true
sqlparser = { version = "0.59.0", features = ["visitor"] }
serde.workspace = true

View File

@@ -20,11 +20,6 @@ pub use windmill_parser::{Arg, MainArgSignature, ObjectType, Typ};
pub const SANITIZED_ENUM_STR: &str = "__sanitized_enum__";
pub const SANITIZED_RAW_STRING_STR: &str = "__sanitized_raw_string__";
mod asset_parser;
mod asset_parser_utils;
pub use asset_parser::parse_assets;
pub use asset_parser_utils::parse_wmill_sdk_sql_assets;
pub fn parse_mysql_sig(code: &str) -> anyhow::Result<MainArgSignature> {
let parsed = parse_mysql_file(&code)?;
if let Some(x) = parsed {

View File

@@ -0,0 +1,23 @@
[package]
name = "windmill-parser-ts-asset"
version.workspace = true
edition.workspace = true
authors.workspace = true
[lib]
name = "windmill_parser_ts_asset"
path = "./src/lib.rs"
[target.'cfg(target_arch = "wasm32")'.dependencies]
wasm-bindgen.workspace = true
serde-wasm-bindgen.workspace = true
[dependencies]
windmill-parser.workspace = true
windmill-parser-sql-asset.workspace = true
swc_common.workspace = true
triomphe.workspace = true
swc_ecma_parser.workspace = true
swc_ecma_ast.workspace = true
swc_ecma_visit.workspace = true
anyhow.workspace = true

View File

@@ -259,7 +259,7 @@ impl Visit for AssetsFinder {
});
// We use the SQL parser to detect RW, specific tables, etc.
let sql_assets = windmill_parser_sql::parse_wmill_sdk_sql_assets(
let sql_assets = windmill_parser_sql_asset::parse_wmill_sdk_sql_assets(
*kind,
asset_name,
schema.as_deref(),

View File

@@ -15,7 +15,6 @@ serde-wasm-bindgen.workspace = true
[dependencies]
windmill-parser.workspace = true
windmill-parser-sql.workspace = true
swc_common.workspace = true
triomphe.workspace = true
swc_ecma_parser.workspace = true

View File

@@ -211,8 +211,6 @@ pub enum TypeDecl {
Interface(TsInterfaceDecl),
Alias(TsTypeAliasDecl),
}
pub mod asset_parser;
pub use asset_parser::parse_assets;
/// skip_params is a micro optimization for when we just want to find the main
/// function without parsing all the params.
@@ -365,8 +363,12 @@ fn parse_param(
let r = match param.pat {
Pat::Ident(ident) => {
let (name, typ, nullable) = binding_ident_to_arg(symbol_table, type_resolver, &ident);
let otyp = ident
.type_ann
.as_ref()
.and_then(|ta| detect_union_array_otyp(&ta.type_ann));
Ok(Arg {
otyp: None,
otyp,
name,
typ,
default: None,
@@ -376,13 +378,21 @@ fn parse_param(
}
// Pat::Object(ObjectPat { ... }) = todo!()
Pat::Assign(AssignPat { left, right, .. }) => {
let (name, mut typ, _nullable) = match *left {
Pat::Ident(ident) => binding_ident_to_arg(symbol_table, type_resolver, &ident),
let (name, mut typ, _nullable, otyp) = match *left {
Pat::Ident(ident) => {
let otyp = ident
.type_ann
.as_ref()
.and_then(|ta| detect_union_array_otyp(&ta.type_ann));
let (name, typ, nullable) =
binding_ident_to_arg(symbol_table, type_resolver, &ident);
(name, typ, nullable, otyp)
}
Pat::Object(ObjectPat { type_ann, .. }) => {
let (typ, nullable) = eval_type_ann(symbol_table, type_resolver, &type_ann);
*counter += 1;
let name = format!("anon{}", counter);
(name, typ, nullable)
(name, typ, nullable, None)
}
_ => {
return Err(anyhow::anyhow!(
@@ -418,7 +428,7 @@ fn parse_param(
if typ == Typ::Unknown && dflt.is_some() {
typ = json_to_typ(dflt.as_ref().unwrap(), false);
}
Ok(Arg { otyp: None, name, typ, default: dflt, has_default: true, oidx: None })
Ok(Arg { otyp, name, typ, default: dflt, has_default: true, oidx: None })
}
Pat::Object(ObjectPat { type_ann, .. }) => {
let (typ, nullable) = eval_type_ann(symbol_table, type_resolver, &type_ann);
@@ -963,6 +973,75 @@ fn one_of_properties(
.collect()
}
fn ts_type_to_string(ts_type: &TsType) -> Option<String> {
match ts_type {
TsType::TsKeywordType(t) => Some(
match t.kind {
TsKeywordTypeKind::TsStringKeyword => "string",
TsKeywordTypeKind::TsNumberKeyword => "number",
TsKeywordTypeKind::TsBooleanKeyword => "boolean",
TsKeywordTypeKind::TsObjectKeyword => "object",
TsKeywordTypeKind::TsBigIntKeyword => "bigint",
TsKeywordTypeKind::TsAnyKeyword => "any",
_ => return None,
}
.to_string(),
),
TsType::TsTypeRef(TsTypeRef { type_name, .. }) => match type_name {
TsEntityName::Ident(Ident { sym, .. }) => Some(sym.to_string()),
_ => None,
},
_ => None,
}
}
fn get_array_elem_type(ts_type: &TsType) -> Option<&TsType> {
match ts_type {
TsType::TsArrayType(TsArrayType { elem_type, .. }) => Some(elem_type),
_ => None,
}
}
/// Detects union types of the form `T | T[]` or `T[] | T` and returns
/// the original type string (e.g. "string | string[]").
fn detect_union_array_otyp(ts_type: &TsType) -> Option<String> {
let TsType::TsUnionOrIntersectionType(TsUnionOrIntersectionType::TsUnionType(TsUnionType {
types,
..
})) = ts_type
else {
return None;
};
if types.len() != 2 {
return None;
}
// Check pattern: T | T[]
if let (Some(scalar_name), Some(array_elem)) =
(ts_type_to_string(&types[0]), get_array_elem_type(&types[1]))
{
if let Some(elem_name) = ts_type_to_string(array_elem) {
if scalar_name == elem_name {
return Some(format!("{} | {}[]", scalar_name, elem_name));
}
}
}
// Check pattern: T[] | T
if let (Some(array_elem), Some(scalar_name)) =
(get_array_elem_type(&types[0]), ts_type_to_string(&types[1]))
{
if let Some(elem_name) = ts_type_to_string(array_elem) {
if scalar_name == elem_name {
return Some(format!("{}[] | {}", elem_name, scalar_name));
}
}
}
None
}
fn find_undefined(types: &Vec<Box<TsType>>) -> Option<usize> {
types.into_iter().position(|x| match **x {
TsType::TsKeywordType(TsKeywordType { kind, .. }) => {

View File

@@ -646,6 +646,55 @@ mod tests {
);
}
#[test]
fn test_parse_union_array_type() {
let code = r#"
export async function main(
items: string | string[],
numbers: number[] | number,
plain: string
) {
return { items, numbers, plain };
}
"#;
let sig = parse_deno_signature(code, false, false, None).unwrap();
assert_eq!(
sig,
MainArgSignature {
star_args: false,
star_kwargs: false,
args: vec![
Arg {
name: "items".to_string(),
otyp: Some("string | string[]".to_string()),
typ: Typ::Unknown,
default: None,
has_default: false,
oidx: None,
},
Arg {
name: "numbers".to_string(),
otyp: Some("number[] | number".to_string()),
typ: Typ::Unknown,
default: None,
has_default: false,
oidx: None,
},
Arg {
name: "plain".to_string(),
otyp: None,
typ: Typ::Str(None),
default: None,
has_default: false,
oidx: None,
},
],
no_main_func: Some(false),
has_preprocessor: Some(false),
}
);
}
#[test]
fn test_parse_invalid_typescript() {
let code = r#"

View File

@@ -39,6 +39,7 @@ nu-parser = [ "dep:windmill-parser-nu"]
java-parser = [ "dep:windmill-parser-java"]
ruby-parser = [ "dep:windmill-parser-ruby"]
wac-parser = [ "dep:windmill-parser-wac"]
asset-parser = [ "dep:windmill-parser-ts-asset", "dep:windmill-parser-py-asset", "dep:windmill-parser-sql-asset"]
[dependencies]
anyhow.workspace = true
@@ -57,6 +58,9 @@ windmill-parser-nu = { workspace = true, optional = true }
windmill-parser-java = { workspace = true, optional = true }
windmill-parser-ruby = { workspace = true, optional = true }
windmill-parser-wac = { workspace = true, optional = true }
windmill-parser-ts-asset = { workspace = true, optional = true }
windmill-parser-py-asset = { workspace = true, optional = true }
windmill-parser-sql-asset = { workspace = true, optional = true }
wasm-bindgen.workspace = true
serde_json.workspace = true

View File

@@ -61,6 +61,11 @@ const targets = [
desc: "Workflow-as-Code",
features: "wac-parser",
env: "default",
}, {
ident: "asset",
desc: "Asset parsers (TS, Python, SQL) with SQL AST",
features: "asset-parser",
env: "default",
},
# ^^^ Add new entry here ^^^
];

View File

@@ -33,3 +33,6 @@ popd
pushd "pkg-java" && npm publish ${args}
popd
pushd "pkg-asset" && npm publish ${args}
popd

View File

@@ -187,28 +187,28 @@ pub fn parse_ruby(code: &str) -> String {
wrap_sig(windmill_parser_ruby::parse_ruby_signature(code))
}
#[cfg(feature = "sql-parser")]
#[cfg(feature = "asset-parser")]
#[wasm_bindgen]
pub fn parse_assets_sql(code: &str) -> String {
match windmill_parser_sql::parse_assets(code) {
match windmill_parser_sql_asset::parse_assets(code) {
Ok(r) => serde_json::to_string(&r).unwrap(),
Err(err) => format!("err: {:?}", err),
}
}
#[cfg(feature = "ts-parser")]
#[cfg(feature = "asset-parser")]
#[wasm_bindgen]
pub fn parse_assets_ts(code: &str) -> String {
match windmill_parser_ts::parse_assets(code) {
match windmill_parser_ts_asset::parse_assets(code) {
Ok(r) => serde_json::to_string(&r).unwrap(),
Err(err) => format!("err: {:?}", err),
}
}
#[cfg(feature = "py-parser")]
#[cfg(feature = "asset-parser")]
#[wasm_bindgen]
pub fn parse_assets_py(code: &str) -> String {
match windmill_parser_py::parse_assets(code) {
match windmill_parser_py_asset::parse_assets(code) {
Ok(r) => serde_json::to_string(&r).unwrap(),
Err(err) => format!("err: {:?}", err),
}

View File

@@ -20,6 +20,7 @@ pub async fn connect_db(
server_mode: bool,
indexer_mode: bool,
worker_mode: bool,
num_workers: i32,
#[cfg(feature = "private")] mut killpill_rx: tokio::sync::broadcast::Receiver<()>,
) -> anyhow::Result<sqlx::Pool<sqlx::Postgres>> {
use anyhow::Context;
@@ -34,13 +35,7 @@ pub async fn connect_db(
} else if indexer_mode {
DEFAULT_MAX_CONNECTIONS_INDEXER
} else {
DEFAULT_MAX_CONNECTIONS_WORKER
+ std::env::var("NUM_WORKERS")
.ok()
.map(|x| x.parse().ok())
.flatten()
.unwrap_or(1)
- 1
DEFAULT_MAX_CONNECTIONS_WORKER + (num_workers.max(1) as u32) - 1
}
}
};
@@ -103,9 +98,8 @@ pub async fn connect(
use sqlx::Executor;
use std::time::Duration;
let mut pool_options = sqlx::postgres::PgPoolOptions::new()
.min_connections((max_connections / 5).clamp(1, max_connections))
.min_connections(0)
.max_connections(max_connections)
.acquire_timeout(Duration::from_secs(5))
.max_lifetime(Duration::from_secs(30 * 60)); // 30 mins
if worker_mode {
pool_options = pool_options.idle_timeout(Duration::from_secs(60));

View File

@@ -243,7 +243,14 @@ async fn cache_hub_scripts(file_path: Option<String>) -> anyhow::Result<()> {
create_dir_all(&*HUB_CACHE_DIR)?;
create_dir_all(&*BUN_BUNDLE_CACHE_DIR)?;
for path in paths.values() {
// Ensure the latest git sync script is always cached, regardless of hubPaths.json contents
let mut all_paths: Vec<String> = paths.into_values().collect();
let latest_git_sync = windmill_common::workspaces::LATEST_GIT_SYNC_SCRIPT_PATH.to_string();
if !all_paths.contains(&latest_git_sync) {
all_paths.push(latest_git_sync);
}
for path in &all_paths {
tracing::info!("Caching hub script at {path}");
let res = get_hub_script_content_and_requirements(Some(path), None).await?;
if res
@@ -694,6 +701,7 @@ async fn windmill_main() -> anyhow::Result<()> {
let mut num_workers = if mode == Mode::Server || mode == Mode::Indexer || mode == Mode::MCP {
0
} else if is_native_mode_from_env() {
NATIVE_MODE_RESOLVED.store(true, std::sync::atomic::Ordering::Relaxed);
println!("Native mode enabled: forcing NUM_WORKERS=8");
8
} else {
@@ -866,6 +874,30 @@ async fn windmill_main() -> anyhow::Result<()> {
}
}
// Resolve native mode early (before connect_db) so connection pool size accounts for it.
// native_mode can come from env OR from the DB worker group config.
if worker_mode && !is_native_mode_from_env() {
if let Some(db) = conn.as_sql() {
let native_from_db: bool = sqlx::query_scalar!(
"SELECT (config->>'native_mode')::boolean FROM config WHERE name = $1",
format!("worker__{}", *windmill_common::worker::WORKER_GROUP)
)
.fetch_optional(db)
.await
.ok()
.flatten()
.flatten()
.unwrap_or(false);
if native_from_db {
NATIVE_MODE_RESOLVED.store(true, std::sync::atomic::Ordering::Relaxed);
num_workers = 8;
tracing::info!(
"Native mode detected from worker config (early): forcing NUM_WORKERS=8"
);
}
}
}
let conn = if mode == Mode::Agent {
conn
} else {
@@ -878,6 +910,7 @@ async fn windmill_main() -> anyhow::Result<()> {
server_mode,
indexer_mode,
worker_mode,
num_workers,
#[cfg(feature = "private")]
killpill_rx.resubscribe(),
)
@@ -982,16 +1015,6 @@ Windmill Community Edition {GIT_VERSION}
)
.await;
// native_mode may also be set via DB worker group config (not just env).
// NATIVE_MODE_RESOLVED is updated by load_worker_config during initial_load.
if worker_mode
&& !is_native_mode_from_env()
&& NATIVE_MODE_RESOLVED.load(std::sync::atomic::Ordering::Relaxed)
{
num_workers = 8;
tracing::info!("Native mode detected from worker config: forcing NUM_WORKERS=8");
}
monitor_db(
&conn,
&base_internal_url,
@@ -1884,7 +1907,7 @@ pub async fn run_workers(
tracing::info!(
"Starting {num_workers} workers and SLEEP_QUEUE={}ms",
*windmill_worker::SLEEP_QUEUE
windmill_worker::sleep_queue()
);
for i in 1..(num_workers + 1) {

View File

@@ -251,9 +251,8 @@ pub async fn initial_load(
.map(|x| x.tags.clone())
.unwrap_or_default();
// we only check from env as native_mode is not stored in the token
// NATIVE_MODE_RESOLVED is already set in main.rs during startup
let native_mode = windmill_common::worker::is_native_mode_from_env();
windmill_common::worker::NATIVE_MODE_RESOLVED
.store(native_mode, std::sync::atomic::Ordering::Relaxed);
*config = WorkerConfig {
worker_tags,
env_vars: load_env_vars(
@@ -880,10 +879,19 @@ struct TokenRow {
workspace_id: Option<String>,
}
/// When updating this filter, also update:
/// - `register_token_expiry_notification` in windmill-api-auth/src/lib.rs
/// - `isUserToken` in frontend/src/lib/components/settings/TokensTable.svelte
fn is_user_token(label: Option<&str>) -> bool {
match label {
None => true,
Some(l) => l != "session" && !l.starts_with("ephemeral") && !l.starts_with("Ephemeral"),
Some(l) => {
l != "session"
&& !l.starts_with("ephemeral")
&& !l.starts_with("Ephemeral")
&& l != "debugger-token"
&& !l.starts_with("mcp-oauth-")
}
}
}

View File

@@ -109,7 +109,9 @@ job_result_stream_v2: job_id(uuid), workspace_id(text), stream(text), idx(int)
job_settings: job_id(uuid), runnable_settings(bigint)
job_stats: workspace_id(char), job_id(uuid), metric_id(char), metric_name(char), metric_kind(metric_kind), scalar_int(int), scalar_float(float), timestamps(ts), timeseries_int(int[]), timeseries_float(float[])
FK: (workspace_id) -> workspace(id)
kafka_trigger: path(char), kafka_resource_path(char), topics(char), group_id(char), script_path(char), is_flow(bool), workspace_id(char), edited_by(char), email(char), edited_at(ts), extra_perms(jsonb), server_id(char), last_server_ping(ts), error(text), error_handler_path(char), error_handler_args(jsonb), retry(jsonb), mode(trigger_mode), filters(jsonb[])
kafka_pending_commits: id(bigint), workspace_id(char), kafka_trigger_path(char), topic(char), partition(int), offset(bigint), created_at(ts)
FK: (workspace_id, kafka_trigger_path) -> kafka_trigger(workspace_id, path)
kafka_trigger: path(char), kafka_resource_path(char), topics(char), group_id(char), script_path(char), is_flow(bool), workspace_id(char), edited_by(char), email(char), edited_at(ts), extra_perms(jsonb), server_id(char), last_server_ping(ts), error(text), error_handler_path(char), error_handler_args(jsonb), retry(jsonb), mode(trigger_mode), filters(jsonb[]), auto_commit(bool)
log_file: hostname(char), log_ts(ts), ok_lines(bigint), err_lines(bigint), mode(log_mode), worker_group(char), file_path(char), json_fmt(bool)
magic_link: email(char), token(char), expiration(ts)
mcp_oauth_client: mcp_server_url(text), client_id(text), client_secret(text), client_secret_expires_at(ts), token_endpoint(text), created_at(ts)

View File

@@ -581,6 +581,9 @@ pub async fn create_token_internal(
}
/// Insert a pending expiry notification row for user tokens that have an expiration.
/// When updating this filter, also update:
/// - `is_user_token` in src/monitor.rs
/// - `isUserToken` in frontend/src/lib/components/settings/TokensTable.svelte
pub async fn register_token_expiry_notification(
tx: &mut sqlx::PgConnection,
token: &str,
@@ -589,7 +592,12 @@ pub async fn register_token_expiry_notification(
) {
let Some(expiration) = expiration else { return };
if label == Some("session")
|| label.is_some_and(|l| l.starts_with("ephemeral") || l.starts_with("Ephemeral"))
|| label.is_some_and(|l| {
l.starts_with("ephemeral")
|| l.starts_with("Ephemeral")
|| l == "debugger-token"
|| l.starts_with("mcp-oauth-")
})
{
return;
}

View File

@@ -129,11 +129,12 @@ async fn update_config(
#[cfg(not(feature = "enterprise"))]
let config = if name.starts_with("worker__") {
// In CE, only allow setting worker_tags, cache_clear, and init_bash
// In CE, only allow setting worker_tags, cache_clear, init_bash, and native_mode
serde_json::json!({
"worker_tags": config.get("worker_tags"),
"cache_clear": config.get("cache_clear"),
"init_bash": config.get("init_bash")
"init_bash": config.get("init_bash"),
"native_mode": config.get("native_mode")
})
} else {
config

View File

@@ -10,8 +10,8 @@ path = "src/lib.rs"
[features]
default = []
private = ["windmill-test-utils/private", "dep:aws-config", "dep:aws-credential-types", "dep:aws-sdk-sqs"]
enterprise = ["windmill-test-utils/enterprise", "dep:base64"]
private = ["windmill-test-utils/private", "dep:aws-config", "dep:aws-credential-types", "dep:aws-sdk-sqs", "windmill-git-sync/private"]
enterprise = ["windmill-test-utils/enterprise", "dep:base64", "windmill-git-sync/enterprise"]
deno_core = ["windmill-test-utils/deno_core"]
mcp = []
run_inline = ["dep:windmill-worker", "windmill-test-utils/run_inline", "windmill-test-utils/duckdb"]
@@ -22,6 +22,7 @@ windmill-api-client.workspace = true
windmill-common = { workspace = true, default-features = false }
windmill-native-triggers = { workspace = true, features = ["native_trigger"] }
windmill-api-auth.workspace = true
windmill-git-sync.workspace = true
windmill-worker = { workspace = true, optional = true }
sqlx.workspace = true
serde_json.workspace = true

View File

@@ -251,7 +251,8 @@ async fn test_websocket_e2e(db: Pool<Postgres>) -> anyhow::Result<()> {
"test-workspace",
"test-user",
"test@windmill.dev",
&[json!({"type": "RawMessage", "content": "hello from e2e test"})] as &[serde_json::Value],
&[json!({"type": "RawMessage", "content": "hello from e2e test"})]
as &[serde_json::Value],
)
.execute(&db)
.await?;
@@ -302,9 +303,11 @@ async fn test_postgres_e2e(db: Pool<Postgres>) -> anyhow::Result<()> {
sqlx::query("CREATE TABLE test_trigger_table (id serial PRIMARY KEY, data text)")
.execute(&db)
.await?;
sqlx::query(&format!("CREATE PUBLICATION {pub_name} FOR TABLE test_trigger_table"))
.execute(&db)
.await?;
sqlx::query(&format!(
"CREATE PUBLICATION {pub_name} FOR TABLE test_trigger_table"
))
.execute(&db)
.await?;
sqlx::query(&format!(
"SELECT pg_create_logical_replication_slot('{slot_name}', 'pgoutput')"
))
@@ -313,10 +316,9 @@ async fn test_postgres_e2e(db: Pool<Postgres>) -> anyhow::Result<()> {
// Extract the test DB name from the pool so the resource points here,
// not at the main windmill database.
let test_db_name: String =
sqlx::query_scalar("SELECT current_database()")
.fetch_one(&db)
.await?;
let test_db_name: String = sqlx::query_scalar("SELECT current_database()")
.fetch_one(&db)
.await?;
insert_resource(
&db,

View File

@@ -214,12 +214,9 @@ async fn test_capture_delete(db: Pool<Postgres>) -> anyhow::Result<()> {
.execute(&db)
.await?;
let count = sqlx::query_scalar!(
"SELECT COUNT(*) FROM capture WHERE id = $1",
id,
)
.fetch_one(&db)
.await?;
let count = sqlx::query_scalar!("SELECT COUNT(*) FROM capture WHERE id = $1", id,)
.fetch_one(&db)
.await?;
assert_eq!(count, Some(0));
@@ -385,7 +382,10 @@ async fn test_capture_api_list_captures(db: Pool<Postgres>) -> anyhow::Result<()
.send()
.await?;
assert!(response.status().is_success(), "list captures should succeed");
assert!(
response.status().is_success(),
"list captures should succeed"
);
let captures: Vec<CaptureResponse> = response.json().await?;
assert_eq!(captures.len(), 3);
@@ -480,12 +480,9 @@ async fn test_capture_api_delete(db: Pool<Postgres>) -> anyhow::Result<()> {
assert!(response.status().is_success(), "delete should succeed");
let count = sqlx::query_scalar!(
"SELECT COUNT(*) FROM capture WHERE id = $1",
id,
)
.fetch_one(&db)
.await?;
let count = sqlx::query_scalar!("SELECT COUNT(*) FROM capture WHERE id = $1", id,)
.fetch_one(&db)
.await?;
assert_eq!(count, Some(0));
@@ -933,7 +930,8 @@ async fn test_kafka_trigger_insert(db: Pool<Postgres>) -> anyhow::Result<()> {
let trigger = sqlx::query!(
r#"
SELECT kafka_resource_path, topics, group_id, mode AS "mode: String"
SELECT kafka_resource_path, topics, group_id, mode AS "mode: String",
auto_offset_reset, auto_commit, reset_offset
FROM kafka_trigger
WHERE workspace_id = $1 AND path = $2
"#,
@@ -947,6 +945,50 @@ async fn test_kafka_trigger_insert(db: Pool<Postgres>) -> anyhow::Result<()> {
assert_eq!(trigger.topics, vec!["topic-a", "topic-b"]);
assert_eq!(trigger.group_id, "my-consumer-group");
assert_eq!(trigger.mode, "enabled");
assert_eq!(trigger.auto_offset_reset, "latest");
assert_eq!(trigger.auto_commit, true);
assert_eq!(trigger.reset_offset, false);
Ok(())
}
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
async fn test_kafka_trigger_insert_auto_commit_disabled(db: Pool<Postgres>) -> anyhow::Result<()> {
sqlx::query!(
r#"
INSERT INTO kafka_trigger (
path, kafka_resource_path, topics, group_id, script_path,
is_flow, workspace_id, edited_by, email, auto_commit
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
"#,
"f/test/kafka_trigger_no_commit",
"u/admin/kafka_resource",
&["topic-c"] as &[&str],
"my-consumer-group-2",
"f/test/kafka_handler",
false,
"test-workspace",
"test-user",
"test@windmill.dev",
false,
)
.execute(&db)
.await?;
let trigger = sqlx::query!(
r#"
SELECT auto_commit
FROM kafka_trigger
WHERE workspace_id = $1 AND path = $2
"#,
"test-workspace",
"f/test/kafka_trigger_no_commit",
)
.fetch_one(&db)
.await?;
assert_eq!(trigger.auto_commit, false);
Ok(())
}

View File

@@ -570,6 +570,7 @@ pub async fn send_stats(Extension(db): Extension<DB>, authed: ApiAuthed) -> Resu
&HTTP_CLIENT,
&db,
windmill_common::stats_oss::SendStatsReason::Manual,
false,
)
.await?;
@@ -582,6 +583,7 @@ pub async fn get_stats(Extension(db): Extension<DB>, authed: ApiAuthed) -> Resul
let stats = windmill_common::stats_oss::get_stats_payload(
&db,
&windmill_common::stats_oss::SendStatsReason::Manual,
false,
)
.await?;
let encrypted = windmill_common::stats_oss::encrypt_stats(&stats)?;

View File

@@ -63,6 +63,7 @@ windmill-object-store.workspace = true
windmill-audit.workspace = true
windmill-parser.workspace = true
windmill-parser-sql.workspace = true
windmill-parser-sql-asset.workspace = true
windmill-parser-ts.workspace = true
windmill-parser-py = { workspace = true, optional = true }
windmill-parser-py-imports = { workspace = true, optional = true }

View File

@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.654.0
version: 1.657.0
title: Windmill API
contact:
@@ -1939,6 +1939,58 @@ paths:
"200":
description: Successfully imported the installation
/w/{workspace}/github_app/ghes_installation_callback:
post:
summary: GHES installation callback
description: Register a self-managed GitHub App installation from GitHub Enterprise Server
operationId: ghesInstallationCallback
tags:
- Git Sync
parameters:
- $ref: "#/components/parameters/WorkspaceId"
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- installation_id
properties:
installation_id:
type: integer
format: int64
description: The GitHub App installation ID from GHES
responses:
"200":
description: GHES installation registered successfully
/github_app/ghes_config:
get:
summary: Get GHES app config
description: Returns the GitHub Enterprise Server app configuration (without private key) for constructing the installation URL
operationId: getGhesConfig
tags:
- Git Sync
responses:
"200":
description: GHES app configuration
content:
application/json:
schema:
type: object
properties:
base_url:
type: string
app_slug:
type: string
client_id:
type: string
required:
- base_url
- app_slug
- client_id
/users/accept_invite:
post:
summary: accept invite to workspace
@@ -12017,6 +12069,39 @@ paths:
"200":
description: kafka trigger offsets reset successfully
/w/{workspace}/kafka_triggers/commit_offsets/{path}:
post:
summary: commit kafka offsets for a specific trigger
operationId: commitKafkaOffsets
tags:
- kafka_trigger
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Path"
requestBody:
description: offsets to commit
required: true
content:
application/json:
schema:
type: object
properties:
topic:
type: string
partition:
type: integer
format: int32
offset:
type: integer
format: int64
required:
- topic
- partition
- offset
responses:
"200":
description: kafka offsets committed successfully
/w/{workspace}/nats_triggers/create:
post:
summary: create nats trigger
@@ -22098,6 +22183,10 @@ components:
- earliest
default: latest
description: "Initial offset behavior when consumer group has no committed offset. 'latest' starts from new messages only, 'earliest' starts from the beginning."
auto_commit:
type: boolean
default: true
description: "When true (default), offsets are committed automatically after receiving each message. When false, you must manually commit offsets using the commit_offsets endpoint."
server_id:
type: string
description: ID of the server currently handling this trigger (internal)
@@ -22165,6 +22254,10 @@ components:
- earliest
default: latest
description: "Initial offset behavior when consumer group has no committed offset."
auto_commit:
type: boolean
default: true
description: "When true (default), offsets are committed automatically after receiving each message. When false, you must manually commit offsets using the commit_offsets endpoint."
mode:
$ref: "#/components/schemas/TriggerMode"
error_handler_path:
@@ -22224,6 +22317,10 @@ components:
- earliest
default: latest
description: "Initial offset behavior when consumer group has no committed offset."
auto_commit:
type: boolean
default: true
description: "When true (default), offsets are committed automatically after receiving each message. When false, you must manually commit offsets using the commit_offsets endpoint."
path:
type: string
description: The unique path identifier for this trigger
@@ -23532,7 +23629,6 @@ components:
items:
$ref: "#/components/schemas/GitSyncObjectType"
required:
- script_path
- git_repo_resource_path
MetricMetadata:

View File

@@ -1012,6 +1012,7 @@ async fn http_payload(
.to_v2_preprocessor_args(
&http_trigger_config.route_path,
&route_path,
"",
&params,
headers,
query,

View File

@@ -4833,7 +4833,7 @@ fn register_potential_assets_on_inline_execution(
preview: &PreviewInline,
) {
let assets = if preview.language == ScriptLang::DuckDb {
Some(windmill_parser_sql::parse_assets(&preview.content).map(|a| a.assets))
Some(windmill_parser_sql_asset::parse_assets(&preview.content).map(|a| a.assets))
} else if preview.language == ScriptLang::Postgresql {
let datatable = preview
.args
@@ -4851,7 +4851,7 @@ fn register_potential_assets_on_inline_execution(
(None, None)
};
let content = content.as_deref().unwrap_or(&preview.content);
windmill_parser_sql::parse_wmill_sdk_sql_assets(
windmill_parser_sql_asset::parse_wmill_sdk_sql_assets(
AssetKind::DataTable,
datatable,
schema.as_deref(),

View File

@@ -468,6 +468,7 @@ async fn route_job(
.to_args_from_format(
&trigger.route_path,
&called_path,
&trigger.path,
&params,
runnable_format,
trigger.wrap_body,

View File

@@ -68,6 +68,7 @@ struct HttpTriggerPreprocessorEvent<'a> {
kind: String,
route: &'a str,
path: &'a str,
trigger_path: &'a str,
body: Box<RawValue>,
raw_string: Option<String>,
params: &'a HashMap<String, String>,
@@ -117,6 +118,7 @@ impl HttpTriggerArgs {
self,
route_path: &str,
called_path: &str,
trigger_path: &str,
params: &HashMap<String, String>,
format: RunnableFormat,
wrap_body: bool,
@@ -126,7 +128,14 @@ impl HttpTriggerArgs {
match format {
RunnableFormat { has_preprocessor: true, version: RunnableFormatVersion::V2 } => {
// we don't care about wrap_body in v2
self.to_v2_preprocessor_args(route_path, called_path, params, headers, query)
self.to_v2_preprocessor_args(
route_path,
called_path,
trigger_path,
params,
headers,
query,
)
}
RunnableFormat { has_preprocessor: true, version: RunnableFormatVersion::V1 } => self
.to_v1_preprocessor_args(
@@ -177,6 +186,7 @@ impl HttpTriggerArgs {
self,
route_path: &str,
called_path: &str,
trigger_path: &str,
params: &HashMap<String, String>,
headers: HashMap<String, Box<RawValue>>,
query: HashMap<String, Box<RawValue>>,
@@ -193,6 +203,7 @@ impl HttpTriggerArgs {
method: (&self.0.metadata.method).try_into()?,
route: route_path,
path: called_path,
trigger_path,
params,
}),
);

View File

@@ -283,6 +283,8 @@ struct SimplifiedSettings {
#[serde(skip_serializing_if = "Option::is_none")]
operator_settings: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
datatable: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
slack_team_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
slack_name: Option<String>,
@@ -323,6 +325,8 @@ struct SimplifiedSettingsLegacy {
#[serde(skip_serializing_if = "Option::is_none")]
operator_settings: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
datatable: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
slack_team_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
slack_name: Option<String>,
@@ -347,6 +351,7 @@ struct SettingsRow {
mute_critical_alerts: Option<bool>,
color: Option<String>,
operator_settings: Option<serde_json::Value>,
datatable: Option<Value>,
slack_team_id: Option<String>,
slack_name: Option<String>,
slack_command_script: Option<String>,
@@ -955,6 +960,7 @@ pub(crate) async fn tarball_workspace(
mute_critical_alerts,
color,
operator_settings,
datatable,
slack_team_id,
slack_name,
slack_command_script
@@ -983,6 +989,7 @@ pub(crate) async fn tarball_workspace(
mute_critical_alerts: row.mute_critical_alerts,
color: row.color.clone(),
operator_settings: row.operator_settings.clone(),
datatable: row.datatable.clone(),
slack_team_id: row.slack_team_id.clone(),
slack_name: row.slack_name.clone(),
slack_command_script: row.slack_command_script.clone(),
@@ -1045,6 +1052,7 @@ pub(crate) async fn tarball_workspace(
mute_critical_alerts: row.mute_critical_alerts,
color: row.color,
operator_settings: row.operator_settings,
datatable: row.datatable,
slack_team_id: row.slack_team_id,
slack_name: row.slack_name,
slack_command_script: row.slack_command_script,

View File

@@ -21,15 +21,14 @@ pub fn prepend_token_to_github_url(
) -> crate::error::Result<String> {
let url = Url::parse(github_url)?;
if url.host_str() != Some("github.com") {
return Err(crate::error::Error::BadRequest(
"Invalid: not a github URL".to_string(),
));
}
let host = url.host_str().ok_or_else(|| {
crate::error::Error::BadRequest("Invalid GitHub URL: no host".to_string())
})?;
Ok(format!(
"https://x-access-token:{}@github.com{}",
"https://x-access-token:{}@{}{}",
installation_token,
host,
url.path()
))
}

View File

@@ -58,6 +58,7 @@ pub const OTEL_TRACING_PROXY_SETTING: &str = "otel_tracing_proxy";
pub const APP_WORKSPACED_ROUTE_SETTING: &str = "app_workspaced_route";
pub const SECRET_BACKEND_SETTING: &str = "secret_backend";
pub const MIN_KEEP_ALIVE_VERSION_SETTING: &str = "min_keep_alive_version";
pub const GITHUB_ENTERPRISE_APP_SETTING: &str = "github_enterprise_app";
pub const ENV_SETTINGS: &[&str] = &[
"DISABLE_NSJAIL",

View File

@@ -21,9 +21,9 @@ pub struct TantivyIndexerSettings {
impl Default for TantivyIndexerSettings {
fn default() -> Self {
TantivyIndexerSettings {
writer_memory_budget: 300_000_000,
commit_job_max_batch_size: 50_000,
commit_log_max_batch_size: 10_000,
writer_memory_budget: 150_000_000,
commit_job_max_batch_size: 10_000,
commit_log_max_batch_size: 5_000,
refresh_index_period: 300,
refresh_log_index_period: 300,
max_indexed_job_log_size: 1_000_000,

View File

@@ -32,6 +32,7 @@ pub async fn send_stats(
_http_client: &reqwest::Client,
_db: &DB,
_reason: SendStatsReason,
_minimal: bool,
) -> Result<()> {
// stats details are closed source
Ok(())
@@ -56,7 +57,11 @@ pub async fn get_user_usage<'c, E: sqlx::Executor<'c, Database = Postgres>>(
pub struct Stats {}
#[cfg(not(feature = "private"))]
pub async fn get_stats_payload(_db: &DB, _reason: &SendStatsReason) -> Result<Stats> {
pub async fn get_stats_payload(
_db: &DB,
_reason: &SendStatsReason,
_minimal: bool,
) -> Result<Stats> {
// stats details are closed source
Ok(Stats {})
}

View File

@@ -149,11 +149,16 @@ pub enum ObjectType {
WorkspaceDependencies,
}
pub const LATEST_GIT_SYNC_SCRIPT_PATH: &str = "hub/28160/sync-script-to-git-repo-windmill";
#[derive(Serialize, Deserialize, Debug)]
pub struct GitRepositorySettings {
#[serde(skip_serializing_if = "Option::is_none")]
pub exclude_types_override: Option<Vec<ObjectType>>,
pub script_path: String,
/// None means auto-managed: always use LATEST_GIT_SYNC_SCRIPT_PATH.
/// Some(path) means pinned to a specific script.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub script_path: Option<String>,
pub git_repo_resource_path: String,
pub use_individual_branch: Option<bool>,
pub group_by_folder: Option<bool>,
@@ -164,23 +169,26 @@ pub struct GitRepositorySettings {
}
impl GitRepositorySettings {
pub fn effective_script_path(&self) -> &str {
self.script_path
.as_deref()
.unwrap_or(LATEST_GIT_SYNC_SCRIPT_PATH)
}
pub fn is_script_meets_min_version(&self, min_version: u32) -> error::Result<bool> {
let path = self.effective_script_path();
// example: "hub/28102/sync-script-to-git-repo-windmill"
let current = self
.script_path
let current = path
.split("/") // -> ["hub" "28102" "sync-script-to-git-repo-windmill"]
.skip(1) // omit "hub"
.next() // get numeric id
.ok_or(Error::InternalErr(format!(
"cannot get script version id from: {}",
&self.script_path
path
)))?
.parse()
.unwrap_or_else(|e| {
tracing::warn!(
"cannot get script version id from: {}. e: {e}",
&self.script_path
);
tracing::warn!("cannot get script version id from: {}. e: {e}", path);
u32::MAX
});

View File

@@ -3030,8 +3030,16 @@ impl PulledJobResult {
if let Some(s) = str_o.as_ref() {
match serde_json::from_str::<Vec<Box<RawValue>>>(s) {
Ok(ref mut vec) => accumulated_arg.append(vec),
Err(e) => {
return Err(error::Error::ArgumentErr(format!("cannot consolidate arguments of non-list type. Type provided for argument `{arg_name_to_accumulate}` is not a list\nUnwrapped Error: {e}")));
Err(_) => {
// Value is not an array — wrap the scalar into a
// single-element array. This supports union types
// like T | T[] where the caller may pass a bare T.
match RawValue::from_string(s.to_string()) {
Ok(raw) => accumulated_arg.push(raw),
Err(e) => {
return Err(error::Error::ArgumentErr(format!("cannot consolidate argument `{arg_name_to_accumulate}`: value is neither a valid list nor a valid JSON value\nUnwrapped Error: {e}")));
}
}
}
}
}

View File

@@ -891,12 +891,12 @@ async fn delete_resource(
.fetch_optional(&mut *tx)
.await?;
not_found_if_none(deleted_path, "Resource", &path)?;
sqlx::query!(
"DELETE FROM variable WHERE path = $1 AND workspace_id = $2",
let deleted_linked_variable = sqlx::query_scalar!(
"DELETE FROM variable WHERE path = $1 AND workspace_id = $2 RETURNING path",
path,
w_id
)
.execute(&mut *tx)
.fetch_optional(&mut *tx)
.await?;
audit_log(
&mut *tx,
@@ -924,9 +924,34 @@ async fn delete_resource(
webhook.send_message(
w_id.clone(),
WebhookMessage::DeleteResource { workspace: w_id, path: path.to_owned() },
WebhookMessage::DeleteResource { workspace: w_id.clone(), path: path.to_owned() },
);
if deleted_linked_variable.is_some() {
handle_deployment_metadata(
&authed.email,
&authed.username,
&db,
&w_id,
DeployedObject::Variable {
path: path.to_string(),
parent_path: Some(path.to_string()),
},
Some(format!(
"Variable '{}' deleted (linked resource deleted)",
path
)),
true,
None,
)
.await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::DeleteVariable { workspace: w_id, path: path.to_owned() },
);
}
Ok(format!("resource {} deleted", path))
}

View File

@@ -536,12 +536,12 @@ async fn delete_variable(
)
.execute(&mut *tx)
.await?;
sqlx::query!(
"DELETE FROM resource WHERE path = $1 AND workspace_id = $2",
let deleted_linked_resource = sqlx::query_scalar!(
"DELETE FROM resource WHERE path = $1 AND workspace_id = $2 RETURNING path",
path,
w_id
)
.execute(&mut *tx)
.fetch_optional(&mut *tx)
.await?;
audit_log(
&mut *tx,
@@ -575,9 +575,34 @@ async fn delete_variable(
webhook.send_message(
w_id.clone(),
WebhookMessage::DeleteVariable { workspace: w_id, path: path.to_owned() },
WebhookMessage::DeleteVariable { workspace: w_id.clone(), path: path.to_owned() },
);
if deleted_linked_resource.is_some() {
handle_deployment_metadata(
&authed.email,
&authed.username,
&db,
&w_id,
DeployedObject::Resource {
path: path.to_string(),
parent_path: Some(path.to_string()),
},
Some(format!(
"Resource '{}' deleted (linked variable deleted)",
path
)),
true,
None,
)
.await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::DeleteResource { workspace: w_id, path: path.to_owned() },
);
}
Ok(format!("variable {} deleted", path))
}

View File

@@ -322,7 +322,7 @@ impl Listener for WebsocketTrigger {
db: &DB,
listening_trigger: &ListeningTrigger<Self::TriggerConfig>,
payload: Self::Payload,
trigger_info: HashMap<String, Box<RawValue>>,
mut trigger_info: HashMap<String, Box<RawValue>>,
extra: Option<Self::Extra>,
) -> Result<()> {
let ListeningTrigger {
@@ -338,6 +338,7 @@ impl Listener for WebsocketTrigger {
let WebsocketConfig { url, .. } = trigger_config;
trigger_info.insert("trigger_path".to_string(), to_raw_value(path));
let args = WebsocketTrigger::build_job_args(
&script_path,
*is_flow,

View File

@@ -21,6 +21,7 @@ use windmill_common::{
jobs::JobTriggerKind,
triggers::{TriggerKind, TriggerMetadata},
utils::report_critical_error,
worker::to_raw_value,
DB, INSTANCE_NAME,
};
@@ -467,9 +468,13 @@ pub trait Listener: TriggerCrud + TriggerJobArgs {
db: &DB,
listening_trigger: &ListeningTrigger<Self::TriggerConfig>,
payload: Self::Payload,
trigger_info: HashMap<String, Box<RawValue>>,
mut trigger_info: HashMap<String, Box<RawValue>>,
_extra: Option<Self::Extra>,
) -> Result<()> {
trigger_info.insert(
"trigger_path".to_string(),
to_raw_value(&listening_trigger.path),
);
let args = Self::build_job_args(
&listening_trigger.script_path,
listening_trigger.is_flow,
@@ -552,6 +557,11 @@ pub trait Listener: TriggerCrud + TriggerJobArgs {
return Ok(());
}
let mut trigger_info = trigger_info;
trigger_info.insert(
"trigger_path".to_string(),
to_raw_value(&listening_trigger.path),
);
let (main_args, preprocessor_args) = Self::build_capture_payloads(&payload, trigger_info);
if let Err(err) = insert_capture_payload(
db,

View File

@@ -422,11 +422,14 @@ pub fn start_background_processor(
}
async fn send_job_completed(job_completed_tx: JobCompletedSender, jc: JobCompleted) {
job_completed_tx
if let Err(e) = job_completed_tx
.send_job(jc, true)
.with_context(windmill_common::otel_oss::otel_ctx())
.await
.expect("send job completed")
{
tracing::error!("send job completed failed, triggering worker shutdown: {e:#}");
job_completed_tx.send_worker_killpill();
}
}
pub async fn process_result(

View File

@@ -303,7 +303,7 @@ pub struct PowershellRepo {
lazy_static::lazy_static! {
pub static ref SLEEP_QUEUE: u64 = std::env::var("SLEEP_QUEUE")
static ref SLEEP_QUEUE_BASE: u64 = std::env::var("SLEEP_QUEUE")
.ok()
.and_then(|x| x.parse::<u64>().ok())
.unwrap_or_else(|| {
@@ -647,6 +647,14 @@ lazy_static::lazy_static! {
pub static ref FLOW_RUNNER_RUNNING: Mutex<bool> = Mutex::new(false);
}
pub fn sleep_queue() -> u64 {
if NATIVE_MODE_RESOLVED.load(std::sync::atomic::Ordering::Relaxed) {
300
} else {
*SLEEP_QUEUE_BASE
}
}
type Envs = Vec<(String, String)>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -887,6 +895,19 @@ impl JobCompletedSender {
pub fn is_sql(&self) -> bool {
matches!(self, Self::Sql(_))
}
pub fn set_worker_killpill(&mut self, killpill_tx: KillpillSender) {
if let Self::Sql(sql) = self {
sql.worker_killpill_tx = Some(killpill_tx);
}
}
pub fn send_worker_killpill(&self) {
if let Self::Sql(SqlJobCompletedSender { worker_killpill_tx: Some(killpill_tx), .. }) = self
{
killpill_tx.send();
}
}
}
#[derive(Clone)]
@@ -894,6 +915,7 @@ pub struct SqlJobCompletedSender {
sender: flume::Sender<SendResult>,
unbounded_sender: flume::Sender<SendResult>,
killpill_tx: broadcast::Sender<()>,
worker_killpill_tx: Option<KillpillSender>,
}
pub struct JobCompletedReceiver {
@@ -918,7 +940,12 @@ impl JobCompletedSender {
let (unbounded_sender, unbounded_rx) = flume::unbounded::<SendResult>();
let (killpill_tx, killpill_rx) = broadcast::channel::<()>(10);
(
Self::Sql(SqlJobCompletedSender { sender, unbounded_sender, killpill_tx }),
Self::Sql(SqlJobCompletedSender {
sender,
unbounded_sender,
killpill_tx,
worker_killpill_tx: None,
}),
JobCompletedReceiver { bounded_rx: receiver, killpill_rx, unbounded_rx },
)
}
@@ -1373,7 +1400,7 @@ fn start_interactive_worker_shell(
{
Duration::from_secs(WORKER_SHELL_NAP_TIME_DURATION)
}
_ => Duration::from_millis(*SLEEP_QUEUE * 10),
_ => Duration::from_millis(sleep_queue() * 10),
};
tokio::select! {
_ = tokio::time::sleep(nap_time) => {
@@ -1386,7 +1413,7 @@ fn start_interactive_worker_shell(
Err(err) => {
tracing::error!(worker = %worker_name, hostname = %hostname, "Failed to pull jobs: {}", err);
tokio::time::sleep(Duration::from_millis(*SLEEP_QUEUE * 20)).await;
tokio::time::sleep(Duration::from_millis(sleep_queue() * 20)).await;
}
};
}
@@ -1432,7 +1459,7 @@ pub async fn run_worker(
tracing::debug!(worker = %worker_name, hostname = %hostname, worker_dir = %worker_dir, "Creating worker dir");
#[cfg(feature = "python")]
{
if !NATIVE_MODE_RESOLVED.load(std::sync::atomic::Ordering::Relaxed) {
let (conn, worker_name, hostname, worker_dir) = (
conn.clone(),
worker_name.clone(),
@@ -1714,7 +1741,8 @@ pub async fn run_worker(
let (same_worker_tx, mut same_worker_rx) = mpsc::channel::<SameWorkerPayload>(5);
let (job_completed_tx, job_completed_rx) = JobCompletedSender::new(&conn, 10);
let (mut job_completed_tx, job_completed_rx) = JobCompletedSender::new(&conn, 10);
job_completed_tx.set_worker_killpill(killpill_tx.clone());
let same_worker_queue_size = Arc::new(AtomicU16::new(0));
let same_worker_tx = SameWorkerSender(same_worker_tx, same_worker_queue_size.clone());
@@ -2699,7 +2727,7 @@ pub async fn run_worker(
None
};
tokio::time::sleep(Duration::from_millis(*SLEEP_QUEUE)).await;
tokio::time::sleep(Duration::from_millis(sleep_queue())).await;
#[cfg(feature = "benchmark")]
{
@@ -2720,7 +2748,7 @@ pub async fn run_worker(
}
Err(err) => {
tracing::error!(worker = %worker_name, hostname = %hostname, "Failed to pull jobs: {}", err);
tokio::time::sleep(Duration::from_millis(*SLEEP_QUEUE * 5)).await;
tokio::time::sleep(Duration::from_millis(sleep_queue() * 5)).await;
}
};
}

View File

@@ -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.654.0";
export const VERSION = "v1.657.0";
export async function login(email: string, password: string): Promise<string> {
return await windmill.UserService.login({

View File

@@ -275,6 +275,9 @@ const command = new Command()
"Default TypeScript runtime (bun or deno)"
)
.action(async (opts: any, appFolder: string | undefined) => {
log.warn(
colors.yellow('This command is deprecated. Use "wmill generate-metadata" instead.')
);
const { generateLocksCommand } = await import("./app_metadata.ts");
await generateLocksCommand(opts, appFolder);
});

View File

@@ -93,7 +93,16 @@ async function generateAppHash(
}
/**
* Updates locks for inline scripts in an app
* Result of generating app locks, including which scripts were updated
*/
export interface AppLocksResult {
path: string;
updatedScripts: string[];
}
/**
* Updates locks for inline scripts in an app.
* Returns the path if dry-run, or AppLocksResult with updated scripts if actual update occurred.
*/
export async function generateAppLocksInternal(
appFolder: string,
@@ -105,7 +114,7 @@ export async function generateAppLocksInternal(
},
justUpdateMetadataLock?: boolean,
noStaleMessage?: boolean
): Promise<string | void> {
): Promise<string | AppLocksResult | void> {
if (appFolder.endsWith(SEP)) {
appFolder = appFolder.substring(0, appFolder.length - 1);
}
@@ -157,7 +166,7 @@ export async function generateAppLocksInternal(
return remote_path;
}
if (Object.keys(filteredDeps).length > 0) {
if (Object.keys(filteredDeps).length > 0 && !noStaleMessage) {
log.info(
(await blueColor())(
`Found workspace dependencies (${workspaceDependenciesLanguages
@@ -167,6 +176,8 @@ export async function generateAppLocksInternal(
);
}
let updatedScripts: string[] = [];
if (!justUpdateMetadataLock) {
const changedScripts = [];
// Find hashes that do not correspond to previous hashes
@@ -180,9 +191,11 @@ export async function generateAppLocksInternal(
}
if (changedScripts.length > 0) {
log.info(
`Recomputing locks of ${changedScripts.join(", ")} in ${appFolder}`
);
if (!noStaleMessage) {
log.info(
`Recomputing locks of ${changedScripts.join(", ")} in ${appFolder}`
);
}
if (rawApp) {
const runnablesPath = path.join(appFolder, APP_BACKEND_FOLDER);
@@ -199,13 +212,14 @@ export async function generateAppLocksInternal(
replaceInlineScripts(runnables, runnablesPath + SEP, false);
// Update the app runnables with new locks (writes to separate files)
await updateRawAppRunnables(
updatedScripts = await updateRawAppRunnables(
workspace,
runnables,
remote_path,
appFolder,
filteredDeps,
opts.defaultTs
opts.defaultTs,
noStaleMessage
);
// Note: updateRawAppRunnables now writes each runnable to its own file
} else {
@@ -215,14 +229,17 @@ export async function generateAppLocksInternal(
replaceInlineScripts(normalAppFile.value, appFolder + SEP, false);
// Update the app value with new locks
normalAppFile.value = await updateAppInlineScripts(
const result = await updateAppInlineScripts(
workspace,
normalAppFile.value,
remote_path,
appFolder,
filteredDeps,
opts.defaultTs
opts.defaultTs,
noStaleMessage
);
normalAppFile.value = result.value;
updatedScripts = result.updatedScripts;
// Write the updated app file (only for normal apps, raw apps use separate files)
writeIfChanged(
@@ -230,7 +247,7 @@ export async function generateAppLocksInternal(
yamlStringify(appFile as Record<string, any>, yamlOptions)
);
}
} else {
} else if (!noStaleMessage) {
log.info(colors.gray(`No scripts changed in ${appFolder}`));
}
}
@@ -246,7 +263,11 @@ export async function generateAppLocksInternal(
for (const [scriptPath, hash] of Object.entries(hashes)) {
await updateMetadataGlobalLock(appFolder, hash, scriptPath);
}
log.info(colors.green(`App ${remote_path} lockfiles updated`));
if (!noStaleMessage) {
log.info(colors.green(`App ${remote_path} lockfiles updated`));
}
return { path: remote_path, updatedScripts };
}
/**
@@ -336,6 +357,7 @@ async function traverseAndProcessInlineScripts(
* Updates locks for all runnables in a raw app, generating locks inline script by inline script.
* Writes each runnable to its own YAML file in the backend folder (new format).
* Also writes content and lock files to the runnables folder.
* Returns the list of runnable IDs that had their locks updated.
*/
async function updateRawAppRunnables(
workspace: Workspace,
@@ -343,8 +365,10 @@ async function updateRawAppRunnables(
remotePath: string,
appFolder: string,
rawDeps?: Record<string, string>,
defaultTs: "bun" | "deno" = "bun"
): Promise<void> {
defaultTs: "bun" | "deno" = "bun",
noStaleMessage?: boolean
): Promise<string[]> {
const updatedRunnables: string[] = [];
const runnablesFolder = path.join(appFolder, APP_BACKEND_FOLDER);
// Ensure runnables folder exists
@@ -410,12 +434,11 @@ async function updateRawAppRunnables(
continue;
}
log.info(
colors.gray(
`Generating lock for runnable ${runnableId} (${language})
}`
)
);
if (!noStaleMessage) {
log.info(
colors.gray(`Generating lock for runnable ${runnableId} (${language})`)
);
}
try {
const lock = await generateInlineScriptLock(
@@ -455,11 +478,15 @@ async function updateRawAppRunnables(
// Write the runnable to its own YAML file
writeRunnableToBackend(runnablesFolder, runnableId, simplifiedRunnable);
log.info(
colors.gray(
` Written ${runnableId}.yaml, ${basePath}${ext}${lock ? ` and ${basePath}lock` : ""}`
)
);
updatedRunnables.push(runnableId);
if (!noStaleMessage) {
log.info(
colors.gray(
` Written ${runnableId}.yaml, ${basePath}${ext}${lock ? ` and ${basePath}lock` : ""}`
)
);
}
} catch (error: any) {
log.error(
colors.red(
@@ -470,11 +497,14 @@ async function updateRawAppRunnables(
writeRunnableToBackend(runnablesFolder, runnableId, runnable);
}
}
return updatedRunnables;
}
/**
* Updates locks for all inline scripts in a normal app, similar to updateRawAppRunnables
* but for the app.value structure instead of app.runnables
* but for the app.value structure instead of app.runnables.
* Returns a tuple of [updated app value, list of script names that were updated].
*/
async function updateAppInlineScripts(
workspace: Workspace,
@@ -482,9 +512,11 @@ async function updateAppInlineScripts(
remotePath: string,
appFolder: string,
rawDeps?: Record<string, string>,
defaultTs: "bun" | "deno" = "bun"
): Promise<any> {
defaultTs: "bun" | "deno" = "bun",
noStaleMessage?: boolean
): Promise<{ value: any; updatedScripts: string[] }> {
const pathAssigner = newPathAssigner(defaultTs, { skipInlineScriptSuffix: getNonDottedPaths() });
const updatedScripts: string[] = [];
const processor: InlineScriptProcessor = async (inlineScript, context) => {
const language = inlineScript.language as SupportedLanguage;
@@ -514,13 +546,15 @@ async function updateAppInlineScripts(
try {
let lock: string | undefined;
if (language !== "frontend") {
log.info(
colors.gray(
`Generating lock for inline script "${scriptName}" at ${context.path.join(
"."
)} (${language})`
)
);
if (!noStaleMessage) {
log.info(
colors.gray(
`Generating lock for inline script "${scriptName}" at ${context.path.join(
"."
)} (${language})`
)
);
}
lock = await generateInlineScriptLock(
workspace,
@@ -549,11 +583,18 @@ async function updateAppInlineScripts(
const inlineLockRef =
lock && lock !== "" ? `!inline ${basePath}lock` : "";
log.info(
colors.gray(
` Written ${basePath}${ext}${lock ? ` and ${basePath}lock` : ""}`
)
);
if (!noStaleMessage) {
log.info(
colors.gray(
` Written ${basePath}${ext}${lock ? ` and ${basePath}lock` : ""}`
)
);
}
// Track that this script was updated (only for non-frontend scripts that needed locks)
if (language !== "frontend") {
updatedScripts.push(scriptName);
}
return {
...inlineScript,
@@ -573,7 +614,8 @@ async function updateAppInlineScripts(
}
};
return await traverseAndProcessInlineScripts(appValue, processor);
const updatedValue = await traverseAndProcessInlineScripts(appValue, processor);
return { value: updatedValue, updatedScripts };
}
/**
@@ -767,7 +809,7 @@ export async function inferRunnableSchemaFromFile(
}
}
function getAppFolders(elems: Record<string, any>, extension: string) {
export function getAppFolders(elems: Record<string, any>, extension: string) {
return Object.keys(elems)
.filter((p) => p.endsWith(SEP + extension))
.map((p) => p.substring(0, p.length - (SEP + extension).length));

View File

@@ -56,13 +56,20 @@ export async function pushFlow(
}
const localFlow = (await yamlParseFile(localPath + "flow.yaml")) as FlowFile;
const fileReader = async (path: string) => await readFile(localPath + path, "utf-8");
await replaceInlineScripts(
localFlow.value.modules,
async (path: string) => await readFile(localPath + path, "utf-8"),
fileReader,
log,
localPath,
SEP
);
if (localFlow.value.failure_module) {
await replaceInlineScripts([localFlow.value.failure_module], fileReader, log, localPath, SEP);
}
if (localFlow.value.preprocessor_module) {
await replaceInlineScripts([localFlow.value.preprocessor_module], fileReader, log, localPath, SEP);
}
if (flow) {
if (isSuperset(localFlow, flow)) {
@@ -252,13 +259,20 @@ async function preview(
const localFlow = (await yamlParseFile(flowPath + "flow.yaml")) as FlowFile;
// Replace inline scripts with their actual content
const fileReader = async (path: string) => await readFile(flowPath + path, "utf-8");
await replaceInlineScripts(
localFlow.value.modules,
async (path: string) => await readFile(flowPath + path, "utf-8"),
fileReader,
log,
flowPath,
SEP
);
if (localFlow.value.failure_module) {
await replaceInlineScripts([localFlow.value.failure_module], fileReader, log, flowPath, SEP);
}
if (localFlow.value.preprocessor_module) {
await replaceInlineScripts([localFlow.value.preprocessor_module], fileReader, log, flowPath, SEP);
}
const input = opts.data ? await resolve(opts.data) : {};
@@ -294,12 +308,15 @@ async function preview(
}
}
async function generateLocks(
export async function generateLocks(
opts: GlobalOptions & {
yes?: boolean;
} & SyncOptions,
folder: string | undefined
) {
log.warn(
colors.yellow('This command is deprecated. Use "wmill generate-metadata" instead.')
);
const workspace = await resolveWorkspace(opts);
await requireLogin(opts);
opts = await mergeConfigWithConfigFile(opts);

View File

@@ -19,6 +19,7 @@ import {
} from "../../utils/metadata.ts";
import { ScriptLanguage } from "../../utils/script_common.ts";
import { extractInlineScripts as extractInlineScriptsForFlows } from "../../../windmill-utils-internal/src/inline-scripts/extractor.ts";
import { newPathAssigner } from "../../../windmill-utils-internal/src/path-utils/path-assigner.ts";
import { generateHash, getHeaders, writeIfChanged } from "../../utils/utils.ts";
import { exts } from "../script/script.ts";
@@ -28,7 +29,10 @@ import { FlowFile } from "./flow.ts";
import { FlowValue } from "../../../gen/types.gen.ts";
import { replaceInlineScripts } from "../../../windmill-utils-internal/src/inline-scripts/replacer.ts";
import { workspaceDependenciesLanguages } from "../../utils/script_common.ts";
import { extractNameFromFolder, getFolderSuffix } from "../../utils/resource_folders.ts";
import {
extractNameFromFolder,
getNonDottedPaths,
} from "../../utils/resource_folders.ts";
const TOP_HASH = "__flow_hash";
async function generateFlowHash(
@@ -50,6 +54,14 @@ async function generateFlowHash(
}
return { ...hashes, [TOP_HASH]: await generateHash(JSON.stringify(hashes)) };
}
/**
* Result of generating flow locks, including which scripts were updated
*/
export interface FlowLocksResult {
path: string;
updatedScripts: string[];
}
export async function generateFlowLockInternal(
folder: string,
dryRun: boolean,
@@ -59,7 +71,7 @@ export async function generateFlowLockInternal(
},
justUpdateMetadataLock?: boolean,
noStaleMessage?: boolean
): Promise<string | void> {
): Promise<string | FlowLocksResult | void> {
if (folder.endsWith(SEP)) {
folder = folder.substring(0, folder.length - 1);
}
@@ -97,7 +109,7 @@ export async function generateFlowLockInternal(
return remote_path;
}
if (Object.keys(filteredDeps).length > 0) {
if (Object.keys(filteredDeps).length > 0 && !noStaleMessage) {
log.info(
(await blueColor())(
`Found workspace dependencies (${workspaceDependenciesLanguages
@@ -108,8 +120,9 @@ export async function generateFlowLockInternal(
}
let changedScripts: string[] = [];
if (!justUpdateMetadataLock) {
const changedScripts = [];
//find hashes that do not correspond to previous hashes
for (const [path, hash] of Object.entries(hashes)) {
if (path == TOP_HASH) {
@@ -120,15 +133,24 @@ export async function generateFlowLockInternal(
}
}
log.info(`Recomputing locks of ${changedScripts.join(", ")} in ${folder}`);
if (!noStaleMessage) {
log.info(`Recomputing locks of ${changedScripts.join(", ")} in ${folder}`);
}
const fileReader = async (path: string) => await readFile(folder + SEP + path, "utf-8");
await replaceInlineScripts(
flowValue.value.modules,
async (path: string) => await readFile(folder + SEP + path, "utf-8"),
fileReader,
log,
folder + SEP!,
SEP,
changedScripts
);
if (flowValue.value.failure_module) {
await replaceInlineScripts([flowValue.value.failure_module], fileReader, log, folder + SEP!, SEP, changedScripts);
}
if (flowValue.value.preprocessor_module) {
await replaceInlineScripts([flowValue.value.preprocessor_module], fileReader, log, folder + SEP!, SEP, changedScripts);
}
//removeChangedLocks
flowValue.value = await updateFlow(
@@ -138,12 +160,22 @@ export async function generateFlowLockInternal(
filteredDeps
);
const lockAssigner = newPathAssigner(opts.defaultTs ?? "bun", {
skipInlineScriptSuffix: getNonDottedPaths(),
});
const inlineScripts = extractInlineScriptsForFlows(
flowValue.value.modules,
{},
SEP,
opts.defaultTs
opts.defaultTs,
lockAssigner
);
if (flowValue.value.failure_module) {
inlineScripts.push(...extractInlineScriptsForFlows([flowValue.value.failure_module], {}, SEP, opts.defaultTs, lockAssigner));
}
if (flowValue.value.preprocessor_module) {
inlineScripts.push(...extractInlineScriptsForFlows([flowValue.value.preprocessor_module], {}, SEP, opts.defaultTs, lockAssigner));
}
inlineScripts.forEach((s) => {
writeIfChanged(process.cwd() + SEP + folder + SEP + s.path, s.content);
});
@@ -164,7 +196,16 @@ export async function generateFlowLockInternal(
for (const [path, hash] of Object.entries(hashes)) {
await updateMetadataGlobalLock(folder, hash, path);
}
log.info(colors.green(`Flow ${remote_path} lockfiles updated`));
if (!noStaleMessage) {
log.info(colors.green(`Flow ${remote_path} lockfiles updated`));
}
// Return the list of updated scripts (extract just the filename from the path)
const updatedScripts = changedScripts.map(p => {
const parts = p.split(SEP);
return parts[parts.length - 1].replace(/\.[^.]+$/, ""); // Remove extension
});
return { path: remote_path, updatedScripts };
}
/**
@@ -176,7 +217,15 @@ async function filterWorkspaceDependenciesForFlow(
rawWorkspaceDependencies: Record<string, string>,
folder: string
): Promise<Record<string, string>> {
const inlineScripts = extractInlineScriptsForFlows(structuredClone(flowValue.modules), {}, SEP, undefined);
const clonedValue = structuredClone(flowValue);
const depAssigner = newPathAssigner("bun");
const inlineScripts = extractInlineScriptsForFlows(clonedValue.modules, {}, SEP, undefined, depAssigner);
if (clonedValue.failure_module) {
inlineScripts.push(...extractInlineScriptsForFlows([clonedValue.failure_module], {}, SEP, undefined, depAssigner));
}
if (clonedValue.preprocessor_module) {
inlineScripts.push(...extractInlineScriptsForFlows([clonedValue.preprocessor_module], {}, SEP, undefined, depAssigner));
}
// Filter out lock files and map to common interface
const scripts = inlineScripts

View File

@@ -0,0 +1,344 @@
import { Command } from "@cliffy/command";
import { Confirm } from "@cliffy/prompt/confirm";
import { colors } from "@cliffy/ansi/colors";
import { sep as SEP } from "node:path";
import { GlobalOptions } from "../../types.ts";
import { SyncOptions, mergeConfigWithConfigFile } from "../../core/conf.ts";
import { resolveWorkspace } from "../../core/context.ts";
import { requireLogin } from "../../core/auth.ts";
import * as log from "../../core/log.ts";
import {
generateScriptMetadataInternal,
getRawWorkspaceDependencies,
} from "../../utils/metadata.ts";
import { generateFlowLockInternal, FlowLocksResult } from "../flow/flow_metadata.ts";
import { generateAppLocksInternal, getAppFolders, AppLocksResult } from "../app/app_metadata.ts";
import {
elementsToMap,
FSFSElement,
ignoreF,
} from "../sync/sync.ts";
import { exts } from "../script/script.ts";
import { isFlowPath, isAppPath, isRawAppPath } from "../../utils/resource_folders.ts";
import { listSyncCodebases } from "../../utils/codebase.ts";
interface StaleItem {
type: "script" | "flow" | "app";
path: string;
folder: string;
isRawApp?: boolean;
}
async function generateMetadata(
opts: GlobalOptions & {
yes?: boolean;
lockOnly?: boolean;
schemaOnly?: boolean;
dryRun?: boolean;
skipScripts?: boolean;
skipFlows?: boolean;
skipApps?: boolean;
} & SyncOptions,
folder?: string
) {
if (folder === "") {
folder = undefined;
}
const workspace = await resolveWorkspace(opts);
await requireLogin(opts);
opts = await mergeConfigWithConfigFile(opts);
const rawWorkspaceDependencies = await getRawWorkspaceDependencies();
const codebases = await listSyncCodebases(opts);
const ignore = await ignoreF(opts);
const staleItems: StaleItem[] = [];
// --schema-only implies skipping flows and apps (they only have locks, no schemas)
const skipScripts = opts.skipScripts ?? false;
const skipFlows = opts.skipFlows ?? opts.schemaOnly ?? false;
const skipApps = opts.skipApps ?? opts.schemaOnly ?? false;
const checking: string[] = [];
if (!skipScripts) checking.push("scripts");
if (!skipFlows) checking.push("flows");
if (!skipApps) checking.push("apps");
if (checking.length === 0) {
log.info(colors.yellow("Nothing to check (all types skipped)"));
return;
}
log.info(colors.gray(`Checking ${checking.join(", ")}...`));
// === Collect stale scripts ===
if (!skipScripts) {
// TODO: run elementsToMap only once but for all runnable types.
const scriptElems = await elementsToMap(
await FSFSElement(process.cwd(), codebases, false),
(p, isD) => {
return (
(!isD && !exts.some((ext) => p.endsWith(ext))) ||
ignore(p, isD) ||
isFlowPath(p) ||
isAppPath(p) ||
isRawAppPath(p)
);
},
false,
{}
);
for (const e of Object.keys(scriptElems)) {
const candidate = await generateScriptMetadataInternal(
e,
workspace,
opts,
true, // dryRun
true, // noStaleMessage
rawWorkspaceDependencies,
codebases,
false
);
if (candidate) {
staleItems.push({ type: "script", path: candidate, folder: e });
}
}
}
// === Collect stale flows ===
if (!skipFlows) {
const flowElems = Object.keys(
await elementsToMap(
await FSFSElement(process.cwd(), [], true),
(p, isD) => {
return (
ignore(p, isD) ||
(!isD &&
!p.endsWith(SEP + "flow.yaml") &&
!p.endsWith(SEP + "flow.json"))
);
},
false,
{}
)
).map((x) => x.substring(0, x.lastIndexOf(SEP)));
for (const folder of flowElems) {
const candidate = await generateFlowLockInternal(
folder,
true, // dryRun
workspace,
opts,
false,
true // noStaleMessage
);
if (candidate) {
staleItems.push({ type: "flow", path: candidate, folder });
}
}
}
// === Collect stale apps ===
if (!skipApps) {
const elems = await elementsToMap(
await FSFSElement(process.cwd(), [], true),
(p, isD) => {
return (
ignore(p, isD) ||
(!isD &&
!p.endsWith(SEP + "raw_app.yaml") &&
!p.endsWith(SEP + "app.yaml"))
);
},
false,
{}
);
const rawAppFolders = getAppFolders(elems, "raw_app.yaml");
const appFolders = getAppFolders(elems, "app.yaml");
for (const appFolder of rawAppFolders) {
const candidate = await generateAppLocksInternal(
appFolder,
true, // rawApp
true, // dryRun
workspace,
opts,
false,
true // noStaleMessage
);
if (candidate) {
staleItems.push({ type: "app", path: candidate, folder: appFolder, isRawApp: true });
}
}
for (const appFolder of appFolders) {
const candidate = await generateAppLocksInternal(
appFolder,
false, // rawApp
true, // dryRun
workspace,
opts,
false,
true // noStaleMessage
);
if (candidate) {
staleItems.push({ type: "app", path: candidate, folder: appFolder, isRawApp: false });
}
}
}
// === Filter by folder if specified ===
let filteredItems = staleItems;
if (folder) {
// Normalize to forward slashes (Windows users may use backslashes)
folder = folder.replaceAll("\\", "/");
// Strip trailing slash to match deprecated flow/app handler behavior
if (folder.endsWith("/")) {
folder = folder.substring(0, folder.length - 1);
}
// Normalize item.folder for comparison (Windows file paths use backslashes)
filteredItems = staleItems.filter((item) => {
const normalizedFolder = item.folder.replaceAll("\\", "/");
return normalizedFolder === folder || normalizedFolder.startsWith(folder + "/");
});
}
// === Show stale items and confirm ===
if (filteredItems.length === 0) {
log.info(colors.green("All metadata up-to-date"));
return;
}
// Group items by type for display
const scripts = filteredItems.filter((i) => i.type === "script");
const flows = filteredItems.filter((i) => i.type === "flow");
const apps = filteredItems.filter((i) => i.type === "app");
log.info("");
log.info(`Found ${filteredItems.length} item(s) with stale metadata:`);
if (scripts.length > 0) {
log.info(colors.gray(` Scripts (${scripts.length}):`));
for (const item of scripts) {
log.info(colors.yellow(` ${item.path}`));
}
}
if (flows.length > 0) {
log.info(colors.gray(` Flows (${flows.length}):`));
for (const item of flows) {
log.info(colors.yellow(` ${item.path}`));
}
}
if (apps.length > 0) {
log.info(colors.gray(` Apps (${apps.length}):`));
for (const item of apps) {
log.info(colors.yellow(` ${item.path}`));
}
}
if (opts.dryRun) {
return;
}
log.info("");
if (
!opts.yes &&
!(await Confirm.prompt({
message: "Update metadata?",
default: true,
}))
) {
return;
}
log.info("");
// === Process all stale items with progress counter ===
const total = filteredItems.length;
const maxWidth = `[${total}/${total}]`.length;
let current = 0;
const formatProgress = (n: number) => {
const bracket = `[${n}/${total}]`;
return colors.gray(bracket.padEnd(maxWidth, " "));
};
// Process scripts
for (const item of scripts) {
current++;
log.info(`${formatProgress(current)} script ${colors.cyan(item.path)}`);
await generateScriptMetadataInternal(
item.folder,
workspace,
opts,
false, // dryRun
true, // noStaleMessage - we handle output
rawWorkspaceDependencies,
codebases,
false
);
}
// Process flows
for (const item of flows) {
current++;
const result = await generateFlowLockInternal(
item.folder,
false, // dryRun
workspace,
opts,
false,
true // noStaleMessage - we handle output
) as FlowLocksResult | void;
const scriptsInfo = result?.updatedScripts?.length
? `: ${colors.gray(result.updatedScripts.join(", "))}`
: "";
log.info(`${formatProgress(current)} flow ${colors.cyan(item.path)}${scriptsInfo}`);
}
// Process apps
for (const item of apps) {
current++;
const result = await generateAppLocksInternal(
item.folder,
item.isRawApp!, // rawApp
false, // dryRun
workspace,
opts,
false,
true // noStaleMessage - we handle output
) as AppLocksResult | void;
const scriptsInfo = result?.updatedScripts?.length
? `: ${colors.gray(result.updatedScripts.join(", "))}`
: "";
log.info(`${formatProgress(current)} app ${colors.cyan(item.path)}${scriptsInfo}`);
}
log.info("");
log.info(colors.green(`Done. Updated ${total} item(s).`));
}
const command = new Command()
.description("Generate metadata (locks, schemas) for all scripts, flows, and apps")
.arguments("[folder:string]")
.option("--yes", "Skip confirmation prompt")
.option("--dry-run", "Show what would be updated without making changes")
.option("--lock-only", "Re-generate only the lock files")
.option("--schema-only", "Re-generate only script schemas (skips flows and apps)")
.option("--skip-scripts", "Skip processing scripts")
.option("--skip-flows", "Skip processing flows")
.option("--skip-apps", "Skip processing apps")
.option(
"-i --includes <patterns:file[]>",
"Comma separated patterns to specify which files to include"
)
.option(
"-e --excludes <patterns:file[]>",
"Comma separated patterns to specify which files to exclude"
)
.action(generateMetadata as any);
export default command;

View File

@@ -58,6 +58,7 @@ import {
isFlowInlineScriptPath as isFlowInlineScriptPathInternal,
isFlowPath,
isAppPath,
isRawAppPath,
} from "../../utils/resource_folders.ts";
export interface ScriptFile {
@@ -978,7 +979,7 @@ export type GlobalDeps = Map<
Record<string, string>
>;
async function generateMetadata(
export async function generateMetadata(
opts: GlobalOptions & {
lockOnly?: boolean;
schemaOnly?: boolean;
@@ -986,6 +987,9 @@ async function generateMetadata(
} & SyncOptions,
scriptPath: string | undefined
) {
log.warn(
colors.yellow('This command is deprecated. Use "wmill generate-metadata" instead.')
);
log.info(
"This command only works for workspace scripts, for flows inline scripts use `wmill flow generate-locks`"
);
@@ -1024,7 +1028,8 @@ async function generateMetadata(
(!isD && !exts.some((ext) => p.endsWith(ext))) ||
ignore(p, isD) ||
isFlowPath(p) ||
isAppPath(p)
isAppPath(p) ||
isRawAppPath(p)
);
},
false,

View File

@@ -592,14 +592,35 @@ function ZipFSElement(
}
let inlineScripts;
try {
const assigner = newPathAssigner(defaultTs, { skipInlineScriptSuffix: getNonDottedPaths() });
inlineScripts = extractInlineScriptsForFlows(
flow.value.modules as any,
{},
SEP,
defaultTs,
undefined, // pathAssigner - let it create one
assigner,
{ skipInlineScriptSuffix: getNonDottedPaths() },
);
if (flow.value.failure_module) {
inlineScripts.push(...extractInlineScriptsForFlows(
[flow.value.failure_module],
{},
SEP,
defaultTs,
assigner,
{ skipInlineScriptSuffix: getNonDottedPaths() },
));
}
if (flow.value.preprocessor_module) {
inlineScripts.push(...extractInlineScriptsForFlows(
[flow.value.preprocessor_module],
{},
SEP,
defaultTs,
assigner,
{ skipInlineScriptSuffix: getNonDottedPaths() },
));
}
} catch (error) {
log.error(
`Failed to extract inline scripts for flow at path: ${p}`,

Some files were not shown because too many files have changed in this diff Show More