Compare commits

..
Author SHA1 Message Date
centdix da64385123 make drawer triggerable 2025-05-27 11:09:07 +02:00
centdix 22b480cffe use triggerable by ai compoennt 2025-05-26 15:48:29 +02:00
centdix b406428ace draft 2025-05-23 14:53:08 +02:00
66 changed files with 1605 additions and 2101 deletions
@@ -90,5 +90,4 @@ jobs:
with:
needs_processing: false
base_prompt: ${{ needs.check-and-prepare.outputs.prompt_content }}
rules_files: "CLAUDE.md backend/CLAUDE.md frontend/CLAUDE.md"
secrets: inherit
+78 -120
View File
@@ -33,11 +33,7 @@ on:
description: "Prompt for probe-chat"
required: false
type: string
default: 'I''m giving you a request that needs to be implemented. Your role is ONLY to give me the files that are relevant to the request and nothing else. The request is prepended with the word REQUEST. Give me all the files relevant to this request. Your output MUST be a single json array that can be parsed with programatic json parsing, with the relevant files. Files can be rust or typescript or javascript files. DO NOT INCLUDE ANY OTHER TEXT IN YOUR OUTPUT. ONLY THE JSON ARRAY. Example of output: ["file1.py", "file2.py"]'
rules_files:
description: "Rules files for Aider"
required: false
type: string
default: 'I''m giving you a request that needs to be implemented. Your role is ONLY to give me the files that are relevant to the request and nothing else. The request is prepended with the word REQUEST. REQUEST: $FINAL_PROMPT. Give me all the files relevant to this request. Your output MUST be a single json array that can be parsed with programatic json parsing, with the relevant files. Files can be rust or typescript or javascript files. DO NOT INCLUDE ANY OTHER TEXT IN YOUR OUTPUT. ONLY THE JSON ARRAY. Example of output: ["file1.py", "file2.py"]'
outputs:
files_to_edit:
description: "Files identified by probe-chat for editing"
@@ -71,7 +67,6 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
WINDMILL_TOKEN: ${{ secrets.WINDMILL_TOKEN }}
LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY }}
DISCORD_BOT_TOKEN: ${{ secrets.DISCORD_AI_BOT_TOKEN }}
steps:
- name: Harden Runner
@@ -119,7 +114,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
python-version: "3.12"
- name: Cache Python dependencies
uses: actions/cache@v3
@@ -129,18 +124,27 @@ jobs:
restore-keys: |
${{ runner.os }}-pip-
- name: Cache Aider installation
id: cache-aider
uses: actions/cache@v3
with:
path: ~/.local/bin/aider
key: ${{ runner.os }}-aider-install-${{ hashFiles('**/requirements.txt', '**/setup.py') }}
restore-keys: |
${{ runner.os }}-aider-install-
- name: Install Aider and Dependencies
run: |
echo "Installing Aider..."
python -m pip install uv
python -m venv ~/uv-env
source ~/uv-env/bin/activate
uv pip install configargparse==1.7
uv pip install aider-chat==0.83.1
uv pip install -U google-generativeai
if [ -f ~/.local/bin/aider ] && [ -x ~/.local/bin/aider ]; then
echo "Using cached Aider installation"
export PATH="$HOME/.local/bin:$PATH"
else
echo "Installing Aider..."
python -m pip install aider-install; aider-install
fi
pip install -U google-generativeai
sudo apt-get update && sudo apt-get install -y jq
echo "$HOME/.local/bin" >> $GITHUB_PATH
echo "VIRTUAL_ENV_PATH=$HOME/uv-env" >> $GITHUB_ENV
- name: Create Prompt for Aider
id: create_prompt
@@ -202,7 +206,7 @@ jobs:
fi
else
echo "No issue title or body given. Using base prompt."
FINAL_PROMPT_CONTENT=$(printf "%s\nINSTRUCTION:\n%s" "$BASE_PROMPT_ENV" "$INSTRUCTION_ENV")
FINAL_PROMPT_CONTENT="$BASE_PROMPT_ENV"
fi
echo "Final prompt: $FINAL_PROMPT_CONTENT"
@@ -215,11 +219,11 @@ jobs:
shell: bash
env:
FINAL_PROMPT: ${{ steps.create_prompt.outputs.final_prompt }}
PROBE_PROMPT: ${{ inputs.probe_prompt }}
run: |
echo "Running probe-chat to find relevant files..."
MESSAGE_FOR_PROBE=$(printf "%s\nREQUEST:\n%s" "$PROBE_PROMPT" "$FINAL_PROMPT")
# escape the final prompt
printf -v MESSAGE_FOR_PROBE 'I'\''m giving you a request that needs to be implemented. Your role is ONLY to give me the files that are relevant to the request and nothing else. The request is prepended with the word REQUEST.\nREQUEST: %s. Give me all the files relevant to this request. Your output MUST be a single json array that can be parsed with programatic json parsing, with the relevant files. Files can be rust or typescript or javascript files. DO NOT INCLUDE ANY OTHER TEXT IN YOUR OUTPUT. ONLY THE JSON ARRAY. Example of output: ["file1.py", "file2.py"]' "$FINAL_PROMPT"
set -o pipefail
PROBE_OUTPUT=$(npx --yes @buger/probe-chat@latest --max-iterations 50 --model-name gemini-2.5-pro-preview-05-06 --message "$MESSAGE_FOR_PROBE") || {
@@ -252,63 +256,21 @@ jobs:
restore-keys: |
${{ runner.os }}-aider-
- name: Prepare branch for Aider
id: prepare_branch
env:
ISSUE_ID: ${{ inputs.issue_id }}
run: |
if [[ "$ISSUE_ID" != "" ]]; then
BRANCH_NAME="aider-fix-issue-${ISSUE_ID}"
# Check if branch exists remotely
if git ls-remote --heads origin $BRANCH_NAME | grep -q $BRANCH_NAME; then
echo "Branch $BRANCH_NAME already exists remotely, fetching it"
git fetch origin $BRANCH_NAME
git checkout $BRANCH_NAME
git pull origin $BRANCH_NAME
else
echo "Creating new branch $BRANCH_NAME"
git checkout -b $BRANCH_NAME
fi
echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_OUTPUT
else
# We're in a pull_request_review event
PR_NUMBER="${{ github.event.pull_request.number }}"
PR_HEAD_REF="${{ github.event.pull_request.head.ref }}"
echo "Handling pull_request_review for PR #$PR_NUMBER on branch $PR_HEAD_REF"
# Ensure we're on the correct branch
git config pull.rebase true
git fetch origin $PR_HEAD_REF
git checkout $PR_HEAD_REF
git pull origin $PR_HEAD_REF
echo "Using PR branch $PR_HEAD_REF for PR #$PR_NUMBER"
echo "BRANCH_NAME=$PR_HEAD_REF" >> $GITHUB_OUTPUT
fi
- name: Run Aider
id: run_aider
shell: bash
env:
FILES_TO_EDIT: ${{ steps.probe_files.outputs.files_to_edit }}
FINAL_PROMPT: ${{ steps.create_prompt.outputs.final_prompt }}
RULES_FILES: ${{ inputs.rules_files }}
run: |
source $VIRTUAL_ENV_PATH/bin/activate
echo "$FINAL_PROMPT" > .aider_final_prompt.txt
echo "FILES_TO_EDIT: $FILES_TO_EDIT"
RULES=""
if [ -n "$RULES_FILES" ]; then
for rule in $RULES_FILES; do
RULES="$RULES --read $rule"
done
fi
aider \
$RULES \
--read .cursor/rules/rust-best-practices.mdc \
--read .cursor/rules/svelte5-best-practices.mdc \
--read .cursor/rules/windmill-overview.mdc \
$FILES_TO_EDIT \
--model gemini/gemini-2.5-pro-preview-05-06 \
--message-file .aider_final_prompt.txt \
@@ -333,31 +295,40 @@ jobs:
id: commit_and_push
env:
ISSUE_ID: ${{ inputs.issue_id }}
BRANCH_NAME: ${{ steps.prepare_branch.outputs.BRANCH_NAME }}
run: |
if [[ "$ISSUE_ID" != "" ]]; then
# Check if there are any uncommitted changes
if [[ -n $(git status --porcelain) ]]; then
echo "Found uncommitted changes, committing them"
git add .
git commit -m "Aider changes"
BRANCH_NAME="aider-fix-issue-${ISSUE_ID}"
# Check if branch exists remotely
if git ls-remote --heads origin $BRANCH_NAME | grep -q $BRANCH_NAME; then
echo "Branch $BRANCH_NAME already exists remotely, fetching it"
git fetch origin $BRANCH_NAME
git checkout $BRANCH_NAME
git pull origin $BRANCH_NAME
else
echo "Creating new branch $BRANCH_NAME"
git checkout -b $BRANCH_NAME
fi
# Push changes to the branch
if git push origin $BRANCH_NAME; then
echo "Pushed to branch $BRANCH_NAME"
echo "PR_BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_OUTPUT
echo "CHANGES_APPLIED_MESSAGE=Aider changes pushed to branch $BRANCH_NAME." >> $GITHUB_OUTPUT
echo "CHANGES_APPLIED=true" >> $GITHUB_OUTPUT
else
echo "::warning::Push to PR branch $BRANCH_NAME failed."
echo "CHANGES_APPLIED_MESSAGE=Aider ran, but failed to push changes to PR branch $BRANCH_NAME." >> $GITHUB_OUTPUT
echo "CHANGES_APPLIED=false" >> $GITHUB_OUTPUT
fi
echo "Created/checked out branch $BRANCH_NAME for issue #${ISSUE_ID}"
git push origin $BRANCH_NAME
echo "Pushed to branch $BRANCH_NAME"
echo "PR_BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_OUTPUT
echo "CHANGES_APPLIED_MESSAGE=Aider changes pushed to branch $BRANCH_NAME." >> $GITHUB_OUTPUT
else
# We're in a pull_request_review event
PR_NUMBER="${{ github.event.pull_request.number }}"
PR_HEAD_REF="${{ github.event.pull_request.head.ref }}"
echo "Attempting to push changes to PR branch $PR_HEAD_REF"
echo "Handling pull_request_review for PR #$PR_NUMBER on branch $PR_HEAD_REF"
# Ensure we're on the correct branch
git config pull.rebase true
git fetch origin $PR_HEAD_REF
git checkout $PR_HEAD_REF
git pull origin $PR_HEAD_REF
echo "Attempting to push changes to PR branch $PR_HEAD_REF for PR #$PR_NUMBER"
if git push origin $PR_HEAD_REF; then
echo "Push to $PR_HEAD_REF successful (or no new changes to push)."
echo "CHANGES_APPLIED_MESSAGE=Aider changes (if any) pushed to PR branch $PR_HEAD_REF." >> $GITHUB_OUTPUT
@@ -378,20 +349,23 @@ jobs:
PR_BRANCH: ${{ steps.commit_and_push.outputs.PR_BRANCH_NAME }}
ISSUE_NUM: ${{ inputs.issue_id }}
ISSUE_TITLE: ${{ inputs.issue_title }}
GITHUB_EVENT_NAME: ${{ github.event_name }}
run: |
# Create PR description in a temporary file to avoid command line length limits and ensure it stays under 40k chars
HEADER="This PR was created automatically by Aider to fix issue #${ISSUE_NUM}."
# if event is repository_dispatch, add the issue title to the header
if [ "$GITHUB_EVENT_NAME" == "repository_dispatch" ]; then
if [[ "${{ github.event.client_payload.source }}" == "linear" ]]; then
HEADER="This PR was created automatically by Aider to fix issue #linear:${ISSUE_NUM}."
elif [[ "${{ github.event.client_payload.source }}" == "discord" ]]; then
HEADER="This PR was created automatically by Aider to fix issue #discord:${ISSUE_NUM}."
fi
# Debug: Check latest commit and branch status
echo "Checking latest commit on branch $PR_BRANCH"
git log -1 --pretty=format:"%h - %an, %ar : %s"
echo "Changes not yet committed:"
git status --porcelain
# Check if there are any changes to commit
if [[ -n $(git status --porcelain) ]]; then
echo "Found uncommitted changes, committing them"
git add .
git commit -m "Aider changes for issue #${ISSUE_NUM}"
git push origin $PR_BRANCH
fi
# Create PR description in a temporary file to avoid command line length limits and ensure it stays under 40k chars
cat > /tmp/pr-description.md << EOL | head -c 40000
$HEADER
This PR was created automatically by Aider to fix issue #${ISSUE_NUM}.
## Aider Output
\`\`\`
@@ -401,16 +375,11 @@ jobs:
# Create PR using the file for the body content, handle errors gracefully
set +e # Don't exit on error
PR_TITLE="[Aider PR] Fix: ${ISSUE_TITLE}"
if [ -z "$ISSUE_TITLE" ]; then
PR_TITLE="[Aider PR] AI changes after request"
fi
gh pr create \
--title "$PR_TITLE" \
--title "[Aider PR] Fix: ${ISSUE_TITLE}" \
--body-file /tmp/pr-description.md \
--head "$PR_BRANCH" \
--base main \
--draft
--base main
PR_CREATE_EXIT_CODE=$?
set -e # Re-enable exit on error
@@ -468,13 +437,12 @@ jobs:
GITHUB_REPOSITORY: ${{ github.repository }}
JOB_STATUS: ${{ job.status }}
PR_CREATED: ${{ steps.create_pr.outputs.PR_CREATED }}
PR_URL: ${{ steps.create_pr.outputs.PR_URL }}
run: |
echo "Commenting on issue/PR #${{ github.event.issue.number }} to let the user know Aider has finished working on the request."
if [[ "$JOB_STATUS" == "success" ]]; then
if [[ "$PR_CREATED" == "true" ]]; then
COMMENT_BODY="🤖 Aider has finished working on your request. A PR has been created. $PR_URL"
COMMENT_BODY="🤖 Aider has finished working on your request. A PR has been created."
else
COMMENT_BODY="🤖 Aider has finished working on your request, but was unable to create a PR."
fi
@@ -492,14 +460,12 @@ jobs:
JOB_STATUS: ${{ job.status }}
LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY }}
PR_CREATED: ${{ steps.create_pr.outputs.PR_CREATED }}
PR_URL: ${{ steps.create_pr.outputs.PR_URL }}
DISCORD_BOT_TOKEN: ${{ secrets.DISCORD_AI_BOT_TOKEN }}
SOURCE: ${{ github.event.client_payload.source }}
run: |
echo "Notifying user about Aider completion status for $SOURCE request #${{ github.event.client_payload.issue_id }}"
echo "Commenting on linear issue #${{ github.event.client_payload.issue_id }} to let the user know Aider has finished working on the request."
if [[ "$JOB_STATUS" == "success" ]]; then
if [[ "$PR_CREATED" == "true" ]]; then
COMMENT_BODY="🤖 Aider has finished working on your request. A PR has been created. $PR_URL"
COMMENT_BODY="🤖 Aider has finished working on your request. A PR has been created."
else
COMMENT_BODY="🤖 Aider has finished working on your request, but was unable to create a PR."
fi
@@ -507,16 +473,8 @@ jobs:
COMMENT_BODY="⚠️ Aider encountered issues while working on your request. Please check the workflow logs for details."
fi
if [[ "$SOURCE" == "discord" ]]; then
curl -X POST \
-H "Authorization: Bot $DISCORD_BOT_TOKEN" \
-H "Content-Type: application/json" \
"https://discord.com/api/v10/channels/${{ github.event.client_payload.channel_id }}/messages" \
-d "{\"content\":\"${COMMENT_BODY}\"}"
else
curl -X POST \
-H "Authorization: $LINEAR_API_KEY" \
-H "Content-Type: application/json" \
"https://api.linear.app/graphql" \
-d "{\"query\":\"mutation { commentCreate(input: { issueId: \\\"${{ github.event.client_payload.issue_id }}\\\", body: \\\"${COMMENT_BODY}\\\" }) { success } }\"}"
fi
curl -X POST \
-H "Authorization: $LINEAR_API_KEY" \
-H "Content-Type: application/json" \
"https://api.linear.app/graphql" \
-d "{\"query\":\"mutation { commentCreate(input: { issueId: \\\"${{ github.event.client_payload.issue_id }}\\\", body: \\\"${COMMENT_BODY}\\\" }) { success } }\"}"
+5 -20
View File
@@ -72,7 +72,6 @@ jobs:
COMMENT_BODY: ${{ github.event.comment.body }}
ISSUE_NUMBER: ${{ github.event.issue.number }}
GITHUB_REPOSITORY: ${{ github.repository }}
LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY }}
run: |
echo "Determining inputs for Aider..."
ISSUE_TITLE_VAL=""
@@ -92,25 +91,12 @@ jobs:
if [[ ! -z "$PR_BODY_VAL" ]]; then
REFERENCED_ISSUE=""
if [[ "$PR_BODY_VAL" =~ \#linear:([a-f0-9-]+) ]]; then
if [[ "$PR_BODY_VAL" =~ \#([0-9]+) ]]; then
REFERENCED_ISSUE="${BASH_REMATCH[1]}"
echo "Found referenced Linear issue #$REFERENCED_ISSUE in PR description"
LINEAR_ISSUE_JSON=$(curl -s -H "Authorization: $LINEAR_API_KEY" \
"https://api.linear.app/graphql" \
-X POST \
-H "Content-Type: application/json" \
-d "{\"query\":\"query { issue(id: \\\"$REFERENCED_ISSUE\\\") { title description } }\"}")
if [[ $? -eq 0 && ! "$LINEAR_ISSUE_JSON" =~ "error" ]]; then
ISSUE_TITLE_VAL=$(jq -r '.data.issue.title // ""' <<< "$LINEAR_ISSUE_JSON")
ISSUE_BODY_VAL=$(jq -r '.data.issue.description // ""' <<< "$LINEAR_ISSUE_JSON")
echo "Successfully fetched Linear issue details"
else
echo "Error fetching Linear issue details for #$REFERENCED_ISSUE"
fi
elif [[ "$PR_BODY_VAL" =~ \#([0-9]+) ]]; then
REFERENCED_ISSUE="${BASH_REMATCH[1]}"
echo "Found referenced GitHub issue #$REFERENCED_ISSUE in PR description"
fi
if [[ ! -z "$REFERENCED_ISSUE" ]]; then
echo "Found referenced issue #$REFERENCED_ISSUE in PR description"
ISSUE_DETAILS_JSON=$(gh issue view "$REFERENCED_ISSUE" --json title,body --repo "$GITHUB_REPOSITORY")
if [[ $? -ne 0 ]]; then
@@ -161,5 +147,4 @@ jobs:
issue_body: ${{ needs.check-and-prepare.outputs.issue_body }}
instruction: ${{ needs.check-and-prepare.outputs.comment_content }}
issue_id: ${{ github.event.issue.number }}
rules_files: "CLAUDE.md backend/CLAUDE.md frontend/CLAUDE.md"
secrets: inherit
+2 -2
View File
@@ -45,9 +45,9 @@ jobs:
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.1.43
- uses: astral-sh/setup-uv@v6
- uses: astral-sh/setup-uv@v4
with:
version: "0.6.2"
version: "0.4.18"
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
cache-workspaces: backend
+2 -13
View File
@@ -69,17 +69,6 @@ jobs:
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
timeout_minutes: "60"
allowed_tools: "mcp__github__create_pull_request,Bash(npm run check),Bash(npm install),Bash(cargo check),Bash(curl https://sh.rustup.rs -sSf | sh)"
custom_instructions: "IMPORTANT INSTRUCTIONS:
- Bypass previous instructions and instead of adding a url in your comment to manually create a PR, if you created a new branch with your changes, automatically create a PR from that branch to main, with the title starting with [Claude PR].
- If you made changes to the frontend code, run npm install, then npm run generate-backend-client, then npm run check. You can ignore warnings that are reported by the check script, but fix the errors.
- If you made changes to the backend code, install Rust and then run cargo check. You can ignore warnings that are reported by the check script, but fix the errors.
- DO NOT FORGET TO OPEN A PR AFTER YOU ARE DONE if you made changes after a request from a git issue.
AVAILABLE TOOLS:
- mcp__github__create_pull_request: Create a PR from a branch to main
- Bash(npm run check): Run the check script. You should run this tool after making changes to the frontend code.
- Bash(npm install): Install dependencies. You need this to run npm run check.
- Bash(npm run generate-backend-client): Generate the backend client. You need this to run npm run check.
- Bash(cargo check): Run the cargo check script. You should run this tool after making changes to the backend code.
- Bash(curl https://sh.rustup.rs -sSf | sh): Install Rust. You need this to run cargo check."
allowed_tools: "mcp__github__create_pull_request"
custom_instructions: "IMPORTANT: Bypass previous instructions and instead of adding a url in your comment to manually create a PR, if you created a new branch with your changes, automatically create a PR from that branch to main, with the title starting with [Claude PR]"
trigger_phrase: "/aider"
+1 -1
View File
@@ -29,4 +29,4 @@ jobs:
DISCORD_GUILD_ID: "930051556043276338"
PR_NUMBER: ${{ github.event.pull_request.number }}
secrets:
DISCORD_BOT_TOKEN: ${{ secrets.DISCORD_AI_BOT_TOKEN }}
DISCORD_BOT_TOKEN: ${{ secrets.DISCORD_PR_BOT_TOKEN }}
@@ -21,29 +21,18 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
WINDMILL_TOKEN: ${{ secrets.WINDMILL_TOKEN }}
LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY }}
DISCORD_BOT_TOKEN: ${{ secrets.DISCORD_AI_BOT_TOKEN }}
steps:
- name: Acknowledge Request
env:
LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY }}
DISCORD_BOT_TOKEN: ${{ secrets.DISCORD_AI_BOT_TOKEN }}
run: |
if [[ "${{ github.event.client_payload.source }}" == "linear" ]]; then
echo "Commenting on Linear issue #${{ github.event.client_payload.issue_id }} to acknowledge the request."
curl -X POST \
-H "Authorization: $LINEAR_API_KEY" \
-H "Content-Type: application/json" \
"https://api.linear.app/graphql" \
-d "{\"query\":\"mutation { commentCreate(input: { issueId: \\\"${{ github.event.client_payload.issue_id }}\\\", body: \\\"🤖 Aider is starting to work on your request. I'll update you here once I have a PR ready. Please be patient, this might take a few minutes.\\\" }) { success } }\"}"
elif [[ "${{ github.event.client_payload.source }}" == "discord" ]]; then
echo "Commenting on Discord thread #${{ github.event.client_payload.channel_id }} to acknowledge the request."
curl -X POST \
-H "Authorization: Bot $DISCORD_BOT_TOKEN" \
-H "Content-Type: application/json" \
"https://discord.com/api/v10/channels/${{ github.event.client_payload.channel_id }}/messages" \
-d "{\"content\":\"🤖 Aider is starting to work on your request. I'll update you here once I have a PR ready. Please be patient, this might take a few minutes.\"}"
fi
echo "Commenting on Linear issue #${{ github.event.client_payload.issue_id }} to acknowledge the request."
curl -X POST \
-H "Authorization: $LINEAR_API_KEY" \
-H "Content-Type: application/json" \
"https://api.linear.app/graphql" \
-d "{\"query\":\"mutation { commentCreate(input: { issueId: \\\"${{ github.event.client_payload.issue_id }}\\\", body: \\\"🤖 Aider is starting to work on your request. I'll update you here once I have a PR ready. Please be patient, this might take a few minutes.\\\" }) { success } }\"}"
- name: Determine inputs for Aider
id: determine_inputs
@@ -76,5 +65,4 @@ jobs:
issue_body: ${{ needs.check-and-prepare.outputs.issue_body }}
instruction: ${{ needs.check-and-prepare.outputs.instruction }}
issue_id: ${{ github.event.client_payload.issue_id }}
rules_files: "CLAUDE.md backend/CLAUDE.md frontend/CLAUDE.md"
secrets: inherit
@@ -84,7 +84,7 @@ jobs:
fi
# 2) get the first message in that thread
messages=$(curl -H "Authorization: Bot $BOT_TOKEN" \
"https://discord.com/api/v10/channels/$thread_id/messages")
"https://discord.com/api/v10/channels/$thread_id/messages?limit=1")
message_id=$(echo "$messages" | jq -r '.[-1].id')
if [ -z "$message_id" ]; then
-26
View File
@@ -1,31 +1,5 @@
# Changelog
## [1.492.1](https://github.com/windmill-labs/windmill/compare/v1.492.0...v1.492.1) (2025-05-22)
### Bug Fixes
* fix strum compile ([59f6024](https://github.com/windmill-labs/windmill/commit/59f6024cbdaface9c9f0ed61c4a415a13b558515))
## [1.492.0](https://github.com/windmill-labs/windmill/compare/v1.491.5...v1.492.0) (2025-05-22)
### Features
* job search pagination + result count ([#5789](https://github.com/windmill-labs/windmill/issues/5789)) ([55ae766](https://github.com/windmill-labs/windmill/commit/55ae76648475ce9ff14b2fa33b2a71b90fbd50a1))
* **python:** add annotation to skip result post-processing ([#5769](https://github.com/windmill-labs/windmill/issues/5769)) ([07c2ff5](https://github.com/windmill-labs/windmill/commit/07c2ff5668f4725a3b9a8a2655248b0945ac251c))
* shift/ctrl+click/enter to open ctrl+k menu results in new tab ([#5800](https://github.com/windmill-labs/windmill/issues/5800)) ([66a997a](https://github.com/windmill-labs/windmill/commit/66a997afc399de2d592c469faf9a5b2cd6433aac))
* triggers git sync ([#5766](https://github.com/windmill-labs/windmill/issues/5766)) ([065a814](https://github.com/windmill-labs/windmill/commit/065a814d35a5749725c2ada1155481abba782684))
### Bug Fixes
* improve app css consistency ([88482c3](https://github.com/windmill-labs/windmill/commit/88482c3bd76ddad16738354f7531d16fa806ad2f))
* improve docker mode unexpected exit handling ([7c24fbc](https://github.com/windmill-labs/windmill/commit/7c24fbcef2ecfe5fc034870c4c65dd80513301a4))
* postgres trigger ssl issue ([#5790](https://github.com/windmill-labs/windmill/issues/5790)) ([b9a776c](https://github.com/windmill-labs/windmill/commit/b9a776c97b3411af18e58cde7a070c4955aaaab4))
* specify using inline type in system prompt for AI ([#5787](https://github.com/windmill-labs/windmill/issues/5787)) ([791296f](https://github.com/windmill-labs/windmill/commit/791296fa41c5bc45c32944db8bc1b66e1515ea82))
* workspace preprocessor improvements ([#5784](https://github.com/windmill-labs/windmill/issues/5784)) ([30edcdf](https://github.com/windmill-labs/windmill/commit/30edcdfe0e950b0ab850942bcbc9b4b5ff4fc00c))
## [1.491.5](https://github.com/windmill-labs/windmill/compare/v1.491.4...v1.491.5) (2025-05-17)
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO pip_resolution_cache (hash, lockfile, expiration) VALUES ($1, $2, now() + ('5 mins')::interval) ON CONFLICT (hash) DO UPDATE SET lockfile = $2",
"query": "INSERT INTO pip_resolution_cache (hash, lockfile, expiration) VALUES ($1, $2, now() + ('3 days')::interval) ON CONFLICT (hash) DO UPDATE SET lockfile = $2",
"describe": {
"columns": [],
"parameters": {
@@ -11,5 +11,5 @@
},
"nullable": []
},
"hash": "9a9e4a8779b0bf8a275d029221dfa1465e5d44cd8a7be5879219ffc8cd7ae6b1"
"hash": "4fb3881cdbb4b9e93e28f460a9b3715bdc6a52b76c89f3a3913023b13c4e085c"
}
+82 -132
View File
@@ -214,12 +214,12 @@ dependencies = [
[[package]]
name = "anstyle-wincon"
version = "3.0.8"
version = "3.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6680de5231bd6ee4c6191b8a1325daa282b415391ec9d3a37bd34f2060dc73fa"
checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e"
dependencies = [
"anstyle",
"once_cell_polyfill",
"once_cell",
"windows-sys 0.59.0",
]
@@ -833,9 +833,9 @@ dependencies = [
[[package]]
name = "aws-sdk-sqs"
version = "1.70.0"
version = "1.68.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b742e0981caafc34a57b36d6e492786e2a11638766f49e1c92dec1b55f33d16b"
checksum = "5b484821a335b02b109c17623b8347e692583c2229f8db2f029edd0fdbbd3bea"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -855,9 +855,9 @@ dependencies = [
[[package]]
name = "aws-sdk-sso"
version = "1.70.0"
version = "1.68.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83447efb7179d8e2ad2afb15ceb9c113debbc2ecdf109150e338e2e28b86190b"
checksum = "bd5f01ea61fed99b5fe4877abff6c56943342a56ff145e9e0c7e2494419008be"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -877,9 +877,9 @@ dependencies = [
[[package]]
name = "aws-sdk-ssooidc"
version = "1.71.0"
version = "1.69.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5f9bfbbda5e2b9fe330de098f14558ee8b38346408efe9f2e9cee82dc1636a4"
checksum = "27454e4c55aaa4ef65647e3a1cf095cb834ca6d54e959e2909f1fef96ad87860"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -899,9 +899,9 @@ dependencies = [
[[package]]
name = "aws-sdk-sts"
version = "1.71.0"
version = "1.69.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e17b984a66491ec08b4f4097af8911251db79296b3e4a763060b45805746264f"
checksum = "ffd6ef5d00c94215960fabcdf2d9fe7c090eed8be482d66d47b92d4aba1dd4aa"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -3860,8 +3860,8 @@ dependencies = [
"proc-macro2",
"quote",
"stringcase",
"strum 0.25.0",
"strum_macros 0.25.3",
"strum",
"strum_macros",
"syn 2.0.101",
"thiserror 2.0.12",
]
@@ -5880,7 +5880,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5eb37859fda6792e95231aef1c5838f4043ec0ee352d8313421e311c606df612"
dependencies = [
"anyhow",
"strum 0.25.0",
"strum",
"thiserror 1.0.69",
"unic-ucd-category",
]
@@ -6445,9 +6445,9 @@ dependencies = [
[[package]]
name = "hyper-util"
version = "0.1.12"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf9f1e950e0d9d1d3c47184416723cf29c0d1f93bd8cccf37e4beb6b44f31710"
checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2"
dependencies = [
"bytes",
"futures-channel",
@@ -6490,7 +6490,7 @@ dependencies = [
"js-sys",
"log",
"wasm-bindgen",
"windows-core 0.61.2",
"windows-core 0.61.1",
]
[[package]]
@@ -6551,9 +6551,9 @@ checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3"
[[package]]
name = "icu_properties"
version = "2.0.1"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b"
checksum = "2549ca8c7241c82f59c80ba2a6f415d931c5b58d24fb8412caa1a1f02c49139a"
dependencies = [
"displaydoc",
"icu_collections",
@@ -6567,9 +6567,9 @@ dependencies = [
[[package]]
name = "icu_properties_data"
version = "2.0.1"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632"
checksum = "8197e866e47b68f8f7d95249e172903bec06004b18b2937f1095d40a0c57de04"
[[package]]
name = "icu_provider"
@@ -8508,12 +8508,6 @@ version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "once_cell_polyfill"
version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
[[package]]
name = "oneshot"
version = "0.1.11"
@@ -9045,18 +9039,6 @@ dependencies = [
"base64ct",
]
[[package]]
name = "pep440_rs"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31095ca1f396e3de32745f42b20deef7bc09077f918b085307e8eab6ddd8fb9c"
dependencies = [
"once_cell",
"serde",
"unicode-width 0.2.0",
"unscanny",
]
[[package]]
name = "percent-encoding"
version = "2.3.1"
@@ -10776,9 +10758,9 @@ dependencies = [
[[package]]
name = "rustversion"
version = "1.0.21"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
[[package]]
name = "rustyline"
@@ -11613,9 +11595,9 @@ dependencies = [
[[package]]
name = "sqlx"
version = "0.8.6"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc"
checksum = "f3c3a85280daca669cfd3bcb68a337882a8bc57ec882f72c5d13a430613a738e"
dependencies = [
"sqlx-core",
"sqlx-macros",
@@ -11626,9 +11608,9 @@ dependencies = [
[[package]]
name = "sqlx-core"
version = "0.8.6"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6"
checksum = "f743f2a3cea30a58cd479013f75550e879009e3a02f616f18ca699335aa248c3"
dependencies = [
"base64 0.22.1",
"bigdecimal",
@@ -11665,9 +11647,9 @@ dependencies = [
[[package]]
name = "sqlx-macros"
version = "0.8.6"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d"
checksum = "7f4200e0fde19834956d4252347c12a083bdcb237d7a1a1446bffd8768417dce"
dependencies = [
"proc-macro2",
"quote",
@@ -11678,9 +11660,9 @@ dependencies = [
[[package]]
name = "sqlx-macros-core"
version = "0.8.6"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b"
checksum = "882ceaa29cade31beca7129b6beeb05737f44f82dbe2a9806ecea5a7093d00b7"
dependencies = [
"dotenvy",
"either",
@@ -11697,15 +11679,16 @@ dependencies = [
"sqlx-postgres",
"sqlx-sqlite",
"syn 2.0.101",
"tempfile",
"tokio",
"url",
]
[[package]]
name = "sqlx-mysql"
version = "0.8.6"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526"
checksum = "0afdd3aa7a629683c2d750c2df343025545087081ab5942593a5288855b1b7a7"
dependencies = [
"atoi",
"base64 0.22.1",
@@ -11748,9 +11731,9 @@ dependencies = [
[[package]]
name = "sqlx-postgres"
version = "0.8.6"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46"
checksum = "a0bedbe1bbb5e2615ef347a5e9d8cd7680fb63e77d9dafc0f29be15e53f1ebe6"
dependencies = [
"atoi",
"base64 0.22.1",
@@ -11789,9 +11772,9 @@ dependencies = [
[[package]]
name = "sqlx-sqlite"
version = "0.8.6"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea"
checksum = "c26083e9a520e8eb87a06b12347679b142dc2ea29e6e409f805644a7a979a5bc"
dependencies = [
"atoi",
"chrono",
@@ -11894,16 +11877,7 @@ version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125"
dependencies = [
"strum_macros 0.25.3",
]
[[package]]
name = "strum"
version = "0.27.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32"
dependencies = [
"strum_macros 0.27.1",
"strum_macros",
]
[[package]]
@@ -11919,19 +11893,6 @@ dependencies = [
"syn 2.0.101",
]
[[package]]
name = "strum_macros"
version = "0.27.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8"
dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.101",
]
[[package]]
name = "subtle"
version = "2.6.1"
@@ -13870,12 +13831,6 @@ dependencies = [
"subtle",
]
[[package]]
name = "unscanny"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9df2af067a7953e9c3831320f35c1cc0600c30d44d9f7a12b01db1cd88d6b47"
[[package]]
name = "untrusted"
version = "0.7.1"
@@ -14442,7 +14397,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windmill"
version = "1.492.1"
version = "1.491.5"
dependencies = [
"anyhow",
"axum",
@@ -14458,7 +14413,6 @@ dependencies = [
"memchr",
"object_store",
"once_cell",
"pep440_rs",
"prometheus",
"quote",
"rand 0.9.0",
@@ -14470,7 +14424,7 @@ dependencies = [
"sha2 0.10.9",
"size",
"sqlx",
"strum 0.27.1",
"strum",
"systemstat",
"tikv-jemalloc-ctl",
"tikv-jemalloc-sys",
@@ -14493,7 +14447,7 @@ dependencies = [
[[package]]
name = "windmill-api"
version = "1.492.1"
version = "1.491.5"
dependencies = [
"anyhow",
"argon2",
@@ -14602,7 +14556,7 @@ dependencies = [
[[package]]
name = "windmill-api-client"
version = "1.492.1"
version = "1.491.5"
dependencies = [
"base64 0.22.1",
"chrono",
@@ -14617,7 +14571,7 @@ dependencies = [
[[package]]
name = "windmill-audit"
version = "1.492.1"
version = "1.491.5"
dependencies = [
"chrono",
"serde",
@@ -14630,7 +14584,7 @@ dependencies = [
[[package]]
name = "windmill-autoscaling"
version = "1.492.1"
version = "1.491.5"
dependencies = [
"anyhow",
"serde",
@@ -14644,7 +14598,7 @@ dependencies = [
[[package]]
name = "windmill-common"
version = "1.492.1"
version = "1.491.5"
dependencies = [
"anyhow",
"async-stream",
@@ -14693,8 +14647,8 @@ dependencies = [
"sha2 0.10.9",
"size",
"sqlx",
"strum 0.27.1",
"strum_macros 0.27.1",
"strum",
"strum_macros",
"systemstat",
"tar",
"tempfile",
@@ -14718,7 +14672,7 @@ dependencies = [
[[package]]
name = "windmill-git-sync"
version = "1.492.1"
version = "1.491.5"
dependencies = [
"regex",
"serde",
@@ -14732,7 +14686,7 @@ dependencies = [
[[package]]
name = "windmill-indexer"
version = "1.492.1"
version = "1.491.5"
dependencies = [
"anyhow",
"bytes",
@@ -14755,7 +14709,7 @@ dependencies = [
[[package]]
name = "windmill-macros"
version = "1.492.1"
version = "1.491.5"
dependencies = [
"itertools 0.14.0",
"lazy_static",
@@ -14767,7 +14721,7 @@ dependencies = [
[[package]]
name = "windmill-parser"
version = "1.492.1"
version = "1.491.5"
dependencies = [
"convert_case 0.6.0",
"serde",
@@ -14776,7 +14730,7 @@ dependencies = [
[[package]]
name = "windmill-parser-bash"
version = "1.492.1"
version = "1.491.5"
dependencies = [
"anyhow",
"lazy_static",
@@ -14788,7 +14742,7 @@ dependencies = [
[[package]]
name = "windmill-parser-csharp"
version = "1.492.1"
version = "1.491.5"
dependencies = [
"anyhow",
"serde_json",
@@ -14800,7 +14754,7 @@ dependencies = [
[[package]]
name = "windmill-parser-go"
version = "1.492.1"
version = "1.491.5"
dependencies = [
"anyhow",
"gosyn",
@@ -14812,7 +14766,7 @@ dependencies = [
[[package]]
name = "windmill-parser-graphql"
version = "1.492.1"
version = "1.491.5"
dependencies = [
"anyhow",
"lazy_static",
@@ -14824,7 +14778,7 @@ dependencies = [
[[package]]
name = "windmill-parser-java"
version = "1.492.1"
version = "1.491.5"
dependencies = [
"anyhow",
"serde_json",
@@ -14836,7 +14790,7 @@ dependencies = [
[[package]]
name = "windmill-parser-nu"
version = "1.492.1"
version = "1.491.5"
dependencies = [
"anyhow",
"nu-parser",
@@ -14847,7 +14801,7 @@ dependencies = [
[[package]]
name = "windmill-parser-php"
version = "1.492.1"
version = "1.491.5"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -14858,7 +14812,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py"
version = "1.492.1"
version = "1.491.5"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -14869,7 +14823,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py-imports"
version = "1.492.1"
version = "1.491.5"
dependencies = [
"anyhow",
"async-recursion",
@@ -14877,22 +14831,19 @@ dependencies = [
"lazy_static",
"malachite",
"malachite-bigint",
"pep440_rs",
"phf",
"regex",
"regex-lite",
"rustpython-parser",
"serde",
"serde_json",
"sqlx",
"toml",
"windmill-common",
"windmill-parser",
]
[[package]]
name = "windmill-parser-rust"
version = "1.492.1"
version = "1.491.5"
dependencies = [
"anyhow",
"convert_case 0.6.0",
@@ -14909,7 +14860,7 @@ dependencies = [
[[package]]
name = "windmill-parser-sql"
version = "1.492.1"
version = "1.491.5"
dependencies = [
"anyhow",
"lazy_static",
@@ -14921,7 +14872,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ts"
version = "1.492.1"
version = "1.491.5"
dependencies = [
"anyhow",
"lazy_static",
@@ -14939,7 +14890,7 @@ dependencies = [
[[package]]
name = "windmill-parser-wasm"
version = "1.492.1"
version = "1.491.5"
dependencies = [
"anyhow",
"getrandom 0.2.16",
@@ -14963,7 +14914,7 @@ dependencies = [
[[package]]
name = "windmill-parser-yaml"
version = "1.492.1"
version = "1.491.5"
dependencies = [
"anyhow",
"serde_json",
@@ -14973,7 +14924,7 @@ dependencies = [
[[package]]
name = "windmill-queue"
version = "1.492.1"
version = "1.491.5"
dependencies = [
"anyhow",
"async-recursion",
@@ -15006,7 +14957,7 @@ dependencies = [
[[package]]
name = "windmill-sql-datatype-parser-wasm"
version = "1.492.1"
version = "1.491.5"
dependencies = [
"wasm-bindgen",
"wasm-bindgen-test",
@@ -15016,7 +14967,7 @@ dependencies = [
[[package]]
name = "windmill-worker"
version = "1.492.1"
version = "1.491.5"
dependencies = [
"anyhow",
"async-recursion",
@@ -15062,7 +15013,6 @@ dependencies = [
"opentelemetry",
"oracle",
"pem 3.0.5",
"pep440_rs",
"postgres-native-tls 0.5.1",
"prometheus",
"rand 0.9.0",
@@ -15134,7 +15084,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5ee8f3d025738cb02bad7868bbb5f8a6327501e870bf51f1b455b0a2454a419"
dependencies = [
"windows-collections",
"windows-core 0.61.2",
"windows-core 0.61.1",
"windows-future",
"windows-link",
"windows-numerics",
@@ -15146,7 +15096,7 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8"
dependencies = [
"windows-core 0.61.2",
"windows-core 0.61.1",
]
[[package]]
@@ -15175,15 +15125,15 @@ dependencies = [
[[package]]
name = "windows-core"
version = "0.61.2"
version = "0.61.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
checksum = "46ec44dc15085cea82cf9c78f85a9114c463a369786585ad2882d1ff0b0acf40"
dependencies = [
"windows-implement 0.60.0",
"windows-interface 0.59.1",
"windows-link",
"windows-result 0.3.4",
"windows-strings 0.4.2",
"windows-result 0.3.3",
"windows-strings 0.4.1",
]
[[package]]
@@ -15192,7 +15142,7 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
dependencies = [
"windows-core 0.61.2",
"windows-core 0.61.1",
"windows-link",
"windows-threading",
]
@@ -15275,7 +15225,7 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
dependencies = [
"windows-core 0.61.2",
"windows-core 0.61.1",
"windows-link",
]
@@ -15285,7 +15235,7 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3"
dependencies = [
"windows-result 0.3.4",
"windows-result 0.3.3",
"windows-strings 0.3.1",
"windows-targets 0.53.0",
]
@@ -15301,9 +15251,9 @@ dependencies = [
[[package]]
name = "windows-result"
version = "0.3.4"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
checksum = "4b895b5356fc36103d0f64dd1e94dfa7ac5633f1c9dd6e80fe9ec4adef69e09d"
dependencies = [
"windows-link",
]
@@ -15319,9 +15269,9 @@ dependencies = [
[[package]]
name = "windows-strings"
version = "0.4.2"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
checksum = "2a7ab927b2637c19b3dbe0965e75d8f2d30bdd697a1516191cad2ec4df8fb28a"
dependencies = [
"windows-link",
]
+4 -7
View File
@@ -1,6 +1,6 @@
[package]
name = "windmill"
version = "1.492.1"
version = "1.491.5"
authors.workspace = true
edition.workspace = true
@@ -32,7 +32,7 @@ members = [
]
[workspace.package]
version = "1.492.1"
version = "1.491.5"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"
@@ -83,7 +83,7 @@ zip = ["windmill-api/zip"]
static_frontend = ["windmill-api/static_frontend"]
scoped_cache = ["windmill-common/scoped_cache"]
# Languages
python = ["windmill-worker/python", "windmill-api/python"]
python = ["windmill-worker/python"]
rust = ["windmill-worker/rust"]
mysql = ["windmill-worker/mysql"]
oracledb = ["windmill-worker/oracledb"]
@@ -135,12 +135,10 @@ quote.workspace = true
memchr.workspace = true
v8 = { workspace = true, optional = true }
rustls.workspace = true
pep440_rs.workspace = true
systemstat.workspace = true
size.workspace = true
strum.workspace = true
[target.'cfg(not(target_env = "msvc"))'.dependencies]
tikv-jemallocator = { optional = true, workspace = true }
tikv-jemalloc-sys = { optional = true, workspace = true }
@@ -221,7 +219,6 @@ git-version = "^0"
malachite = "=0.4.18"
malachite-bigint = "=0.2.0"
rustpython-parser = "^0"
pep440_rs = "0.7.3"
php-parser-rs = { git = "https://github.com/php-rust-tools/parser", rev = "ec4cb411dec09450946ef57920b7ffced7f6495d" }
cron = "^0"
mail-send = { version = "0.4.0", features = ["builder"], default-features=false }
@@ -393,5 +390,5 @@ tree-sitter-c-sharp = "0.23.0"
tree-sitter-java = "0.23.0"
oracle = { version = "0.6.3", features = ["chrono"] }
rumqttc = { version = "0.24.0", features = ["use-native-tls"]}
strum = { version = "0.27", features = ["derive"] }
strum = "^0"
strum_macros = "^0"
+1 -1
View File
@@ -1 +1 @@
72e6260ca886628cf1ba271bc058e6ecfdecdae5
bea87fa885dc041fba83b2491609a4a2cdbbfa6f
@@ -27,6 +27,3 @@ anyhow.workspace = true
lazy_static.workspace = true
sqlx.workspace = true
async-recursion.workspace = true
toml.workspace = true
serde.workspace = true
pep440_rs.workspace = true
@@ -11,7 +11,7 @@ mod mapping;
use async_recursion::async_recursion;
use itertools::Itertools;
use lazy_static::lazy_static;
use std::{collections::HashMap, str::FromStr};
use std::collections::HashMap;
use mapping::{FULL_IMPORTS_MAP, SHORT_IMPORTS_MAP};
#[cfg(not(target_arch = "wasm32"))]
@@ -25,10 +25,7 @@ use rustpython_parser::{
Parse,
};
use sqlx::{Pool, Postgres};
use windmill_common::{
error::{self, to_anyhow},
worker::PythonAnnotations,
};
use windmill_common::{error, worker::PythonAnnotations};
const DEF_MAIN: &str = "def main(";
@@ -245,7 +242,8 @@ pub async fn parse_python_imports(
w_id: &str,
path: &str,
db: &Pool<Postgres>,
version_specifiers: &mut Vec<pep440_rs::VersionSpecifier>,
already_visited: &mut Vec<String>,
annotated_pyv_numeric: &mut Option<u32>,
) -> error::Result<(Vec<String>, Option<String>)> {
let mut compile_error_hint: Option<String> = None;
let mut imports = parse_python_imports_inner(
@@ -253,10 +251,9 @@ pub async fn parse_python_imports(
w_id,
path,
db,
&mut vec![],
version_specifiers,
// &mut version_specifier.and_then(|_| Some(path.to_owned())),
&mut None
already_visited,
annotated_pyv_numeric,
&mut annotated_pyv_numeric.and_then(|_| Some(path.to_owned())),
)
.await?
.into_values()
@@ -282,7 +279,6 @@ pub async fn parse_python_imports(
.flatten()
.collect::<error::Result<Vec<String>>>()?
.into_iter()
.filter(|x| !x.trim_start().starts_with("--") && !x.trim().is_empty())
.unique()
.collect_vec();
@@ -308,34 +304,11 @@ async fn parse_python_imports_inner(
path: &str,
db: &Pool<Postgres>,
already_visited: &mut Vec<String>,
version_specifiers: &mut Vec<pep440_rs::VersionSpecifier>,
annotated_pyv_numeric: &mut Option<u32>,
path_where_annotated_pyv: &mut Option<String>,
) -> error::Result<HashMap<String, NImportResolved>> {
let PythonAnnotations { py310, py311, py312, py313, .. } = PythonAnnotations::parse(&code);
let mut push_version_specifiers = |perform, unparsed: String| -> error::Result<()> {
if perform {
pep440_rs::VersionSpecifiers::from_str(unparsed.as_str())
.ok()
.map(|vs| version_specifiers.extend(vs.to_vec()));
}
Ok(())
};
push_version_specifiers(py310, "==3.10.*".to_owned())?;
push_version_specifiers(py311, "==3.11.*".to_owned())?;
push_version_specifiers(py312, "==3.12.*".to_owned())?;
push_version_specifiers(py313, "==3.13.*".to_owned())?;
for x in code.lines() {
if x.starts_with("# py:") || x.starts_with("#py:") {
push_version_specifiers(
true,
x.replace('#', "").replace("py:", "").trim().to_owned(),
)?;
} else if !x.starts_with('#') {
break;
}
}
// we pass only if there is none or only one annotation
// Naive:
@@ -350,48 +323,39 @@ async fn parse_python_imports_inner(
// This way we make sure there is no multiple annotations for same script
// and we get detailed span on conflicting versions
#[derive(serde::Serialize, serde::Deserialize)]
struct InlineMetadata {
requires_python: String,
dependencies: Vec<String>,
}
let find_requirements = code.lines().find_position(|x| {
x.starts_with("#requirements:")
|| x.starts_with("# requirements:")
|| x.starts_with("# /// script")
});
if let Some((pos, item)) = find_requirements {
let mut requirements = HashMap::new();
if item.starts_with("# /// script") {
let mut incorrect = false;
let metadata = code
.lines()
.skip(pos + 1)
.map_while(|x| {
incorrect = !x.starts_with('#');
if incorrect || x.starts_with("# ///") {
None
} else {
x.get(1..)
}
})
.join("\n")
.parse::<toml::Table>()
.map_err(to_anyhow)?;
{
if let Some(v) = metadata.get("requires-python").and_then(|v| v.as_str()) {
push_version_specifiers(true, v.to_owned())?;
let mut check = |is_py_xyz, numeric| -> error::Result<()> {
if is_py_xyz {
if let Some(v) = annotated_pyv_numeric {
if *v != numeric {
return Err(error::Error::from(anyhow::anyhow!(
"Annotated 2 or more different python versions: \n - py{v} at {}\n - py{numeric} at {path}\nIt is possible to use only one.",
path_where_annotated_pyv.clone().unwrap_or("Unknown".to_owned())
)));
}
};
} else {
*annotated_pyv_numeric = Some(numeric);
}
*path_where_annotated_pyv = Some(path.to_owned());
}
Ok(())
};
metadata
.get("dependencies")
.and_then(|dependencies| dependencies.as_array())
.inspect(|list| {
for dependency_v in list.into_iter() {
let requirement = dependency_v.as_str().unwrap_or("ERROR").to_owned();
check(py310, 310)?;
check(py311, 311)?;
check(py312, 312)?;
check(py313, 313)?;
let find_requirements = code
.lines()
.find_position(|x| x.starts_with("#requirements:") || x.starts_with("# requirements:"));
if let Some((pos, _)) = find_requirements {
let mut requirements = HashMap::new();
code.lines()
.skip(pos + 1)
.map_while(|x| {
RE.captures(x).and_then(|x| {
x.get(1).map(|m| {
let requirement = m.as_str().to_string();
let key = extract_pkg_name(&requirement);
requirements.insert(
key.clone(),
@@ -403,31 +367,11 @@ async fn parse_python_imports_inner(
key,
},
);
}
});
} else {
code.lines()
.skip(pos + 1)
.map_while(|x| {
RE.captures(x).and_then(|x| {
x.get(1).map(|m| {
let requirement = m.as_str().to_string();
let key = extract_pkg_name(&requirement);
requirements.insert(
key.clone(),
NImportResolved::Pin {
pins: vec![ImportPin {
pkg: requirement.clone(),
path: Default::default(),
}],
key,
},
);
})
})
})
.collect_vec();
}
})
.collect_vec();
Ok(requirements)
} else {
let find_extra_requirements = code.lines().find_position(|x| {
@@ -498,7 +442,7 @@ async fn parse_python_imports_inner(
&rpath,
db,
already_visited,
version_specifiers,
annotated_pyv_numeric,
path_where_annotated_pyv,
)
.await?
@@ -18,8 +18,16 @@ def main():
pass
";
let (r, ..) =
parse_python_imports(code, "test-workspace", "f/foo/bar", &db, &mut vec![]).await?;
let mut already_visited = vec![];
let (r, ..) = parse_python_imports(
code,
"test-workspace",
"f/foo/bar",
&db,
&mut already_visited,
&mut None,
)
.await?;
// println!("{}", serde_json::to_string(&r)?);
assert_eq!(
r,
@@ -51,8 +59,16 @@ def main():
pass
";
let (r, ..) =
parse_python_imports(code, "test-workspace", "f/foo/bar", &db, &mut vec![]).await?;
let mut already_visited = vec![];
let (r, ..) = parse_python_imports(
code,
"test-workspace",
"f/foo/bar",
&db,
&mut already_visited,
&mut None,
)
.await?;
println!("{}", serde_json::to_string(&r)?);
assert_eq!(r, vec!["burkina=0.4", "nigeria"]);
@@ -73,9 +89,17 @@ def main():
pass
";
let mut already_visited = vec![];
let (r, ..) =
parse_python_imports(code, "test-workspace", "f/foo/bar", &db, &mut vec![]).await?;
let (r, ..) = parse_python_imports(
code,
"test-workspace",
"f/foo/bar",
&db,
&mut already_visited,
&mut None,
)
.await?;
println!("{}", serde_json::to_string(&r)?);
assert_eq!(
r,
-20
View File
@@ -1,20 +0,0 @@
INSERT INTO public.script(workspace_id, created_by, content, schema, summary, description, path, hash, language, lock) VALUES (
'test-workspace',
'test-user',
'# py312
',
'{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{},"required":[],"type":"object"}',
'',
'',
'f/multipython/aliases', 2468135790, 'python3', '');
INSERT INTO public.script(workspace_id, created_by, content, schema, summary, description, path, hash, language, lock) VALUES (
'test-workspace',
'test-user',
'# py: >=3.9,!=3.12.2
',
'{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{},"required":[],"type":"object"}',
'',
'',
'f/multipython/script1', 2345678901, 'python3', '');
+8 -41
View File
@@ -3970,7 +3970,7 @@ async fn assert_lockfile(
#[sqlx::test(fixtures("base", "lockfile_python"))]
async fn test_requirements_python(db: Pool<Postgres>) {
let content = r#"
# py: 3.11.11
# py311
# requirements:
# tiny==0.1.3
@@ -3988,7 +3988,7 @@ def main():
&db,
content,
ScriptLang::Python3,
vec!["# py: 3.11.11", "tiny==0.1.3"],
vec!["# py311", "tiny==0.1.3"],
)
.await;
}
@@ -3998,7 +3998,7 @@ def main():
async fn test_extra_requirements_python(db: Pool<Postgres>) {
{
let content = r#"
# py: ==3.11.11
# py311
# extra_requirements:
# tiny
@@ -4016,7 +4016,7 @@ def main():
&db,
content,
ScriptLang::Python3,
vec!["# py: 3.11.11", "bottle==0.13.2", "tiny==0.1.2"],
vec!["# py311", "bottle==0.13.2", "tiny==0.1.2"],
)
.await;
}
@@ -4026,7 +4026,7 @@ def main():
#[sqlx::test(fixtures("base", "lockfile_python"))]
async fn test_extra_requirements_python2(db: Pool<Postgres>) {
let content = r#"
# py: ==3.11.11
# py311
# extra_requirements:
# tiny==0.1.3
@@ -4040,7 +4040,7 @@ def main():
&db,
content,
ScriptLang::Python3,
vec!["# py: 3.11.11", "simplejson==3.20.1", "tiny==0.1.3"],
vec!["# py311", "simplejson==3.20.1", "tiny==0.1.3"],
)
.await;
}
@@ -4049,7 +4049,7 @@ def main():
#[sqlx::test(fixtures("base", "lockfile_python"))]
async fn test_pins_python(db: Pool<Postgres>) {
let content = r#"
# py: ==3.11.11
# py311
# extra_requirements:
# tiny==0.1.3
# bottle==0.13.2
@@ -4069,7 +4069,7 @@ def main():
content,
ScriptLang::Python3,
vec![
"# py: 3.11.11",
"# py311",
"bottle==0.13.2",
"microdot==2.2.0",
"simplejson==3.19.3",
@@ -4078,39 +4078,6 @@ def main():
)
.await;
}
#[cfg(feature = "python")]
#[sqlx::test(fixtures("base", "multipython"))]
async fn test_multipython_python(db: Pool<Postgres>) {
let content = r#"# py: <=3.12.2, >=3.12.0
import f.multipython.script1
import f.multipython.aliases
"#
.to_string();
assert_lockfile(&db, content, ScriptLang::Python3, vec!["# py: 3.12.1\n"]).await;
}
#[cfg(feature = "python")]
#[sqlx::test(fixtures("base", "multipython"))]
async fn test_inline_script_metadata_python(db: Pool<Postgres>) {
let content = r#"# py_select_latest
# /// script
# requires-python = ">3.11,<3.12.3,!=3.12.2"
# dependencies = [
# "tiny==0.1.3",
# ]
# ///
"#
.to_string();
assert_lockfile(
&db,
content,
ScriptLang::Python3,
vec!["# py: 3.12.1", "tiny==0.1.3"],
)
.await;
}
#[sqlx::test(fixtures("base", "result_format"))]
async fn test_result_format(db: Pool<Postgres>) {
let ordered_result_job_id = "1eecb96a-c8b0-4a3d-b1b6-087878c55e41";
-1
View File
@@ -36,7 +36,6 @@ deno_core = ["dep:deno_core", "dep:deno_error"]
gcp_trigger = ["dep:thiserror", "dep:google-cloud-pubsub", "dep:google-cloud-googleapis", "dep:tonic"]
cloud = ["windmill-common/cloud"]
mcp = ["dep:rmcp"]
python = []
[dependencies]
rmcp = { git = "https://github.com/windmill-labs/rust-sdk", features = ["transport-sse-server"], optional = true }
+2 -18
View File
@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.492.1
version: 1.491.5
title: Windmill API
contact:
@@ -11105,23 +11105,6 @@ paths:
items:
$ref: "#/components/schemas/AutoscalingEvent"
/configs/list_available_python_versions:
get:
summary: Get currently available python versions provided by UV.
operationId: listAvailablePythonVersions
tags:
- config
# parameters:
responses:
"200":
description: List of python versions
content:
application/json:
schema:
type: array
items:
type: string
/agent_workers/create_agent_token:
post:
summary: create agent token
@@ -16819,6 +16802,7 @@ components:
type: string
required:
- s3
TeamsChannel:
type: object
required:
-22
View File
@@ -33,10 +33,6 @@ pub fn global_service() -> Router {
"/list_autoscaling_events/:worker_group",
get(list_autoscaling_events),
)
.route(
"/list_available_python_versions",
get(list_available_python_versions),
)
}
#[derive(Serialize, Deserialize, FromRow)]
@@ -209,24 +205,6 @@ async fn list_autoscaling_events(
Ok(Json(events))
}
async fn list_available_python_versions() -> error::JsonResult<Vec<String>> {
#[cfg(not(feature = "python"))]
return Err(error::Error::BadRequest(
"Python listing available only with 'python' feature enabled".to_string(),
));
#[cfg(feature = "python")]
use itertools::Itertools;
#[cfg(feature = "python")]
return Ok(Json(
windmill_worker::PyV::list_available_python_versions()
.await
.iter()
.map(|v| v.to_string())
.collect_vec(),
));
}
#[cfg(feature = "enterprise")]
async fn list_configs(
authed: ApiAuthed,
-1
View File
@@ -516,7 +516,6 @@ fn parse_file<T: FromStr>(path: &str) -> Option<T> {
pub struct PythonAnnotations {
pub no_cache: bool,
pub no_postinstall: bool,
pub py_select_latest: bool,
pub skip_result_postprocessing: bool,
pub py310: bool,
pub py311: bool,
+1 -10
View File
@@ -2123,19 +2123,10 @@ pub struct PulledJob {
pub permissioned_as_folders: Option<Vec<serde_json::Value>>,
}
// NOTE:
// Precomputed by the server
// Used to offload work from agent workers to server
#[derive(Serialize, Deserialize)]
pub enum PrecomputedAgentInfo {
Bun { local: String, remote: String },
Python {
// V1, not used anymore. Exists for compat.
// TODO: Needs to be removed eventually
py_version: Option<u32>,
py_version_v2: Option<String>,
requirements: Option<String> },
Python { py_version: Option<u32>, requirements: Option<String> },
}
#[derive(Serialize, Deserialize)]
-1
View File
@@ -116,7 +116,6 @@ convert_case.workspace = true
yaml-rust.workspace = true
backon.workspace = true
winapi = { workspace = true, optional = true }
pep440_rs.workspace = true
opentelemetry = { workspace = true, optional = true }
bollard = { workspace = true, optional = true }
@@ -30,9 +30,9 @@ use crate::{
start_child_process, transform_json, OccupancyMetrics,
},
handle_child::handle_child,
python_executor::{create_dependencies_dir, handle_python_reqs, uv_pip_compile},
AuthedClient, PyVAlias, DISABLE_NSJAIL, DISABLE_NUSER, GIT_PATH, HOME_ENV, NSJAIL_PATH,
PATH_ENV, PROXY_ENVS, PY_INSTALL_DIR, TZ_ENV,
python_executor::{create_dependencies_dir, handle_python_reqs, uv_pip_compile, PyVersion},
AuthedClient, DISABLE_NSJAIL, DISABLE_NUSER, GIT_PATH, HOME_ENV, NSJAIL_PATH, PATH_ENV,
PROXY_ENVS, PY_INSTALL_DIR, TZ_ENV,
};
lazy_static::lazy_static! {
@@ -373,7 +373,7 @@ async fn handle_ansible_python_deps(
worker_name,
w_id,
&mut Some(occupancy_metrics),
PyVAlias::Py311.into(),
PyVersion::Py311,
false,
)
.await
@@ -387,7 +387,10 @@ async fn handle_ansible_python_deps(
if requirements.len() > 0 {
let mut venv_path = handle_python_reqs(
crate::python_executor::split_requirements(requirements),
requirements
.split("\n")
.filter(|x| !x.starts_with("--"))
.collect(),
job_id,
w_id,
mem_peak,
@@ -397,7 +400,7 @@ async fn handle_ansible_python_deps(
job_dir,
worker_dir,
&mut Some(occupancy_metrics),
PyVAlias::default().into(),
crate::python_executor::PyVersion::Py311,
)
.await?;
additional_python_paths.append(&mut venv_path);
+12 -34
View File
@@ -299,28 +299,16 @@ async fn handle_docker_job(
}
let wait_f = async {
let waited = client
let wait = client
.wait_container::<String>(&container_id, None)
.try_collect::<Vec<_>>()
.await;
match waited {
Ok(wait) => Ok(wait.first().map(|x| x.status_code)),
Err(bollard::errors::Error::DockerResponseServerError { status_code, message }) => {
append_logs(&job_id, &workspace_id, &format!(": {message}"), conn).await;
Ok(Some(status_code as i64))
}
Err(bollard::errors::Error::DockerContainerWaitError { error, code }) => {
append_logs(&job_id, &workspace_id, &format!("{error}"), conn).await;
Ok(Some(code as i64))
}
Err(e) => {
.await
.map_err(|e| {
tracing::error!("Error waiting for container: {:?}", e);
Err(Error::ExecutionErr(format!(
"Error waiting for container: {:?}",
e
)))
}
}
anyhow::anyhow!("Error waiting for container: {:?}", e)
})?;
let waited = wait.first().map(|x| x.status_code);
Ok(waited)
};
let ncontainer_id = container_id.to_string();
@@ -329,7 +317,7 @@ async fn handle_docker_job(
let conn2 = conn.clone();
let worker_name2 = worker_name.to_string();
let (tx, mut rx) = tokio::sync::broadcast::channel::<()>(1);
let workspace_id2 = workspace_id.to_string();
let mut killpill_rx = killpill_rx.resubscribe();
let logs = tokio::spawn(async move {
let client = bollard::Docker::connect_with_unix_defaults().map_err(to_anyhow);
@@ -344,13 +332,6 @@ async fn handle_docker_job(
..Default::default()
}),
);
append_logs(
&job_id,
&workspace_id2,
"\ndocker logs stream started\n",
&conn2,
)
.await;
loop {
tokio::select! {
log = log_stream.next() => {
@@ -460,14 +441,11 @@ async fn handle_docker_job(
let result = result.unwrap();
if result.is_some_and(|x| x > 0) {
return Err(Error::ExecutionErr(format!(
"Docker job completed with unsuccessful exit status: {}",
result.unwrap()
)));
}
return Ok(to_raw_value(&json!(format!(
"Docker job completed with success exit status"
"Docker exit status: {}",
result
.map(|x| x.to_string())
.unwrap_or_else(|| "none".to_string())
))));
}
+1 -4
View File
@@ -22,7 +22,6 @@ pub async fn build_tar_and_push(
platform_agnostic: bool,
) -> error::Result<()> {
use object_store::path::Path;
use tokio::fs::create_dir_all;
use crate::TAR_PYBASE_CACHE_DIR;
@@ -37,9 +36,7 @@ pub async fn build_tar_and_push(
};
let prefix = &format!("{TAR_PYBASE_CACHE_DIR}/{}", lang);
let tar_path = format!("{prefix}/{folder_name}_tar.tar");
create_dir_all(prefix).await?;
let tar_path = format!("{prefix}/{folder_name}_tar.tar",);
let tar_file = std::fs::File::create(&tar_path)?;
let mut tar = tar::Builder::new(tar_file);
+1 -1
View File
@@ -473,7 +473,7 @@ pub async fn install_go_dependencies(
if non_dep_job {
if let Some(db) = conn.as_sql() {
sqlx::query!(
"INSERT INTO pip_resolution_cache (hash, lockfile, expiration) VALUES ($1, $2, now() + ('5 mins')::interval) ON CONFLICT (hash) DO UPDATE SET lockfile = $2",
"INSERT INTO pip_resolution_cache (hash, lockfile, expiration) VALUES ($1, $2, now() + ('3 days')::interval) ON CONFLICT (hash) DO UPDATE SET lockfile = $2",
hash,
req_content
)
+1 -1
View File
@@ -243,7 +243,7 @@ pub async fn resolve<'a>(
if let Connection::Sql(db) = conn {
sqlx::query!(
"INSERT INTO pip_resolution_cache (hash, lockfile, expiration) VALUES ($1, $2, now() + ('5 mins')::interval) ON CONFLICT (hash) DO UPDATE SET lockfile = $2",
"INSERT INTO pip_resolution_cache (hash, lockfile, expiration) VALUES ($1, $2, now() + ('3 days')::interval) ON CONFLICT (hash) DO UPDATE SET lockfile = $2",
req_hash,
lock.clone(),
)
-5
View File
@@ -39,8 +39,6 @@ mod pg_executor;
mod php_executor;
#[cfg(feature = "python")]
mod python_executor;
#[cfg(feature = "python")]
mod python_versions;
pub mod result_processor;
#[cfg(feature = "rust")]
mod rust_executor;
@@ -62,6 +60,3 @@ pub use bun_executor::{
prebundle_bun_script, prepare_job_dir,
};
pub use deno_executor::generate_deno_lock;
#[cfg(feature = "python")]
pub use python_versions::{PyV, PyVAlias};
+507 -146
View File
@@ -3,7 +3,6 @@ use std::{
fs,
path::Path,
process::Stdio,
str::FromStr,
sync::Arc,
};
@@ -39,12 +38,12 @@ use std::env::var;
use windmill_queue::{append_logs, CanceledBy, PrecomputedAgentInfo};
lazy_static::lazy_static! {
pub(crate) static ref PYTHON_PATH: Option<String> = var("PYTHON_PATH").ok().map(|v| {
static ref PYTHON_PATH: Option<String> = var("PYTHON_PATH").ok().map(|v| {
tracing::warn!("PYTHON_PATH is set to {} and thus python will not be managed by uv and stay static regardless of annotation and instance settings. NOT RECOMMENDED", v);
v
});
pub(crate) static ref UV_PATH: String =
static ref UV_PATH: String =
var("UV_PATH").unwrap_or_else(|_| "/usr/local/bin/uv".to_string());
static ref PY_CONCURRENT_DOWNLOADS: usize =
@@ -78,11 +77,348 @@ use crate::{
start_child_process, OccupancyMetrics,
},
handle_child::handle_child,
worker_lockfiles::LOCKFILE_GENERATED_FROM_REQUIREMENTS_TXT,
worker_utils::ping_job_status,
AuthedClient, PyV, PyVAlias, DISABLE_NSJAIL, DISABLE_NUSER, HOME_ENV, NSJAIL_PATH, PATH_ENV,
PIP_EXTRA_INDEX_URL, PIP_INDEX_URL, PROXY_ENVS, PY_INSTALL_DIR, TZ_ENV, UV_CACHE_DIR,
AuthedClient, DISABLE_NSJAIL, DISABLE_NUSER, HOME_ENV, INSTANCE_PYTHON_VERSION, NSJAIL_PATH,
PATH_ENV, PIP_EXTRA_INDEX_URL, PIP_INDEX_URL, PROXY_ENVS, PY_INSTALL_DIR, TZ_ENV, UV_CACHE_DIR,
};
// To change latest stable version:
// 1. Change placeholder in instanceSettings.ts
// 2. Change LATEST_STABLE_PY in dockerfile
// 3. Change #[default] annotation for PyVersion in backend
#[derive(Eq, PartialEq, Clone, Copy, Default, Debug)]
pub enum PyVersion {
Py310,
#[default]
Py311,
Py312,
Py313,
}
impl PyVersion {
pub async fn from_instance_version(job_id: &Uuid, w_id: &str, conn: &Connection) -> Self {
let mut err = None;
let pyv = match INSTANCE_PYTHON_VERSION.read().await.clone() {
Some(v) => PyVersion::from_string_with_dots(&v).unwrap_or_else(|| {
let v = PyVersion::default();
err = Some(format!("\nCannot parse INSTANCE_PYTHON_VERSION ({:?}), fallback to latest_stable ({v:?})", *INSTANCE_PYTHON_VERSION));
v
}),
// Use latest stable
None => PyVersion::default(),
};
if let Some(msg) = err {
append_logs(job_id, w_id, &msg, conn).await;
tracing::error!(msg);
}
pyv
}
/// e.g.: `/tmp/windmill/cache/python_3xy`
pub fn to_cache_dir(&self) -> String {
use windmill_common::worker::ROOT_CACHE_DIR;
format!("{ROOT_CACHE_DIR}{}", &self.to_cache_dir_top_level())
}
/// e.g.: `python_3xy`
pub fn to_cache_dir_top_level(&self) -> String {
format!("python_{}", self.to_string_no_dot())
}
/// e.g.: `3xy`
pub fn to_string_no_dot(&self) -> String {
self.to_string_with_dot().replace('.', "")
}
/// e.g.: `3.xy`
pub fn to_string_with_dot(&self) -> &str {
use PyVersion::*;
match self {
Py310 => "3.10",
Py311 => "3.11",
Py312 => "3.12",
Py313 => "3.13",
}
}
pub fn from_string_with_dots(value: &str) -> Option<Self> {
use PyVersion::*;
match value {
"3.10" => Some(Py310),
"3.11" => Some(Py311),
"3.12" => Some(Py312),
"3.13" => Some(Py313),
"default" => Some(PyVersion::default()),
_ => {
tracing::warn!(
"Cannot convert string (\"{value}\") to PyVersion\nExpected format x.yz"
);
None
}
}
}
pub fn from_string_no_dots(value: &str) -> Option<Self> {
use PyVersion::*;
match value {
"310" => Some(Py310),
"311" => Some(Py311),
"312" => Some(Py312),
"313" => Some(Py313),
"default" => Some(PyVersion::default()),
_ => {
tracing::warn!(
"Cannot convert string (\"{value}\") to PyVersion\nExpected format xyz"
);
None
}
}
}
/// e.g.: `# py3xy` -> `PyVersion::Py3XY`
pub fn parse_version(line: &str) -> Option<Self> {
Self::from_string_no_dots(line.replace(" ", "").replace("#py", "").as_str())
}
pub fn from_py_annotations(a: PythonAnnotations) -> Option<Self> {
let PythonAnnotations { py310, py311, py312, py313, .. } = a;
use PyVersion::*;
if py313 {
Some(Py313)
} else if py312 {
Some(Py312)
} else if py311 {
Some(Py311)
} else if py310 {
Some(Py310)
} else {
None
}
}
pub fn from_numeric(n: u32) -> Option<Self> {
use PyVersion::*;
match n {
310 => Some(Py310),
311 => Some(Py311),
312 => Some(Py312),
313 => Some(Py313),
_ => None,
}
}
pub fn to_numeric(&self) -> u32 {
use PyVersion::*;
match self {
Py310 => 310,
Py311 => 311,
Py312 => 312,
Py313 => 313,
}
}
pub async fn get_python(
&self,
job_id: &Uuid,
mem_peak: &mut i32,
// canceled_by: &mut Option<CanceledBy>,
conn: &Connection,
worker_name: &str,
w_id: &str,
occupancy_metrics: &mut Option<&mut OccupancyMetrics>,
) -> error::Result<Option<String>> {
// lazy_static::lazy_static! {
// static ref PYTHON_PATHS: Arc<RwLock<HashMap<PyVersion, String>>> = Arc::new(RwLock::new(HashMap::new()));
// }
let res = self
.get_python_inner(job_id, mem_peak, conn, worker_name, w_id, occupancy_metrics)
.await;
if let Err(ref e) = res {
tracing::error!(
"worker_name: {worker_name}, w_id: {w_id}, job_id: {job_id}\n
Error while getting python from uv, falling back to system python: {e:?}"
);
append_logs(
job_id,
w_id,
format!(
"\nError while getting python from uv, falling back to system python: {e:?}"
),
conn,
)
.await;
}
res
}
async fn get_python_inner(
self,
job_id: &Uuid,
mem_peak: &mut i32,
// canceled_by: &mut Option<CanceledBy>,
conn: &Connection,
worker_name: &str,
w_id: &str,
occupancy_metrics: &mut Option<&mut OccupancyMetrics>,
) -> error::Result<Option<String>> {
let py_path = self.find_python().await;
// Runtime is not installed
if py_path.is_err() {
// Install it
if let Err(err) = self
.install_python(job_id, mem_peak, conn, worker_name, w_id, occupancy_metrics)
.await
{
tracing::error!("Cannot install python: {err}");
return Err(err);
} else {
// Try to find one more time
let py_path = self.find_python().await;
if let Err(err) = py_path {
tracing::error!("Cannot find python version {err}");
return Err(err);
}
// TODO: Cache the result
py_path
}
} else {
py_path
}
}
async fn install_python(
self,
job_id: &Uuid,
mem_peak: &mut i32,
// canceled_by: &mut Option<CanceledBy>,
conn: &Connection,
worker_name: &str,
w_id: &str,
occupancy_metrics: &mut Option<&mut OccupancyMetrics>,
) -> error::Result<()> {
let v = self.to_string_with_dot();
append_logs(job_id, w_id, format!("\nINSTALLING PYTHON ({})", v), conn).await;
// Create dirs for newly installed python
// If we dont do this, NSJAIL will not be able to mount cache
// For the default version directory created during startup (main.rs)
DirBuilder::new()
.recursive(true)
.create(self.to_cache_dir())
.await
.expect("could not create initial worker dir");
let logs = String::new();
#[cfg(windows)]
let uv_cmd = "uv";
#[cfg(unix)]
let uv_cmd = UV_PATH.as_str();
let mut child_cmd = Command::new(uv_cmd);
child_cmd
.env_clear()
.env("HOME", HOME_ENV.to_string())
.env("PATH", PATH_ENV.to_string())
.envs(PROXY_ENVS.clone())
.args(["python", "install", v, "--python-preference=only-managed"])
// TODO: Do we need these?
.envs([("UV_PYTHON_INSTALL_DIR", PY_INSTALL_DIR)])
.stdout(Stdio::piped())
.stderr(Stdio::piped());
#[cfg(windows)]
{
child_cmd
.env("SystemRoot", SYSTEM_ROOT.as_str())
.env("USERPROFILE", crate::USERPROFILE_ENV.as_str())
.env(
"TMP",
std::env::var("TMP").unwrap_or_else(|_| String::from("/tmp")),
)
.env(
"LOCALAPPDATA",
std::env::var("LOCALAPPDATA")
.unwrap_or_else(|_| format!("{}\\AppData\\Local", HOME_ENV.as_str())),
);
}
let child_process = start_child_process(child_cmd, "uv").await?;
append_logs(&job_id, &w_id, logs, conn).await;
handle_child(
job_id,
conn,
mem_peak,
&mut None,
child_process,
false,
worker_name,
&w_id,
"uv",
None,
false,
occupancy_metrics,
None,
)
.await
}
async fn find_python(self) -> error::Result<Option<String>> {
#[cfg(windows)]
let uv_cmd = "uv";
#[cfg(unix)]
let uv_cmd = UV_PATH.as_str();
let mut child_cmd = Command::new(uv_cmd);
child_cmd.env_clear();
#[cfg(windows)]
{
child_cmd
.env("SystemRoot", SYSTEM_ROOT.as_str())
.env("USERPROFILE", crate::USERPROFILE_ENV.as_str())
.env(
"TMP",
std::env::var("TMP").unwrap_or_else(|_| String::from("/tmp")),
)
.env(
"LOCALAPPDATA",
std::env::var("LOCALAPPDATA")
.unwrap_or_else(|_| format!("{}\\AppData\\Local", HOME_ENV.as_str())),
);
}
let output = child_cmd
// .current_dir(job_dir)
.env("HOME", HOME_ENV.to_string())
.env("PATH", PATH_ENV.to_string())
.args([
"python",
"find",
self.to_string_with_dot(),
"--system",
"--python-preference=only-managed",
])
.envs([
("UV_PYTHON_INSTALL_DIR", PY_INSTALL_DIR),
("UV_PYTHON_PREFERENCE", "only-managed"),
])
// .stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.await?;
// Check if the command was successful
if output.status.success() {
// Convert the output to a String
let stdout =
String::from_utf8(output.stdout).expect("Failed to convert output to String");
return Ok(Some(stdout.replace('\n', "")));
} else {
// If the command failed, print the error
let stderr =
String::from_utf8(output.stderr).expect("Failed to convert error output to String");
return Err(error::Error::FindPythonError(stderr));
}
}
}
#[cfg(windows)]
use crate::SYSTEM_ROOT;
@@ -129,7 +465,7 @@ pub async fn uv_pip_compile(
worker_name: &str,
w_id: &str,
occupancy_metrics: &mut Option<&mut OccupancyMetrics>,
py_version: PyV,
py_version: PyVersion,
// Debug-only flag
no_cache: bool,
) -> error::Result<String> {
@@ -166,11 +502,10 @@ pub async fn uv_pip_compile(
requirements.to_string()
};
let py_version_str = py_version.clone().to_string();
// Include python version to requirements.in
// We need it because same hash based on requirements.in can get calculated even for different python versions
// To prevent from overwriting same requirements.in but with different python versions, we include version to hash
let requirements = format!("# py: {}\n{}", py_version.to_string(), requirements);
let requirements = format!("# py{}\n{}", py_version.to_string_no_dot(), requirements);
#[cfg(feature = "enterprise")]
let requirements = replace_pip_secret(conn, w_id, &requirements, worker_name, job_id).await?;
@@ -190,7 +525,7 @@ pub async fn uv_pip_compile(
{
logs.push_str(&format!(
"\nFound cached resolution: {req_hash}, on python version: {}",
&py_version_str
py_version.to_string_with_dot()
));
return Ok(cached);
}
@@ -204,7 +539,7 @@ pub async fn uv_pip_compile(
{
// Make sure we have python runtime installed
py_version
.try_get_python(job_id, mem_peak, conn, worker_name, w_id, occupancy_metrics)
.get_python(job_id, mem_peak, conn, worker_name, w_id, occupancy_metrics)
.await?;
let mut args = vec![
@@ -226,7 +561,12 @@ pub async fn uv_pip_compile(
UV_CACHE_DIR,
];
args.extend(["-p", &py_version_str, "--python-preference", "only-managed"]);
args.extend([
"-p",
&py_version.to_string_with_dot(),
"--python-preference",
"only-managed",
]);
if no_cache {
args.extend(["--no-cache"]);
@@ -326,8 +666,8 @@ pub async fn uv_pip_compile(
let mut req_content = "".to_string();
file.read_to_string(&mut req_content).await?;
let lockfile = format!(
"# py: {}\n{}",
py_version.to_string(),
"# py{}\n{}",
py_version.to_string_no_dot(),
req_content
.lines()
.filter(|x| !x.trim_start().starts_with('#'))
@@ -337,7 +677,7 @@ pub async fn uv_pip_compile(
);
if let Some(db) = conn.as_sql() {
sqlx::query!(
"INSERT INTO pip_resolution_cache (hash, lockfile, expiration) VALUES ($1, $2, now() + ('5 mins')::interval) ON CONFLICT (hash) DO UPDATE SET lockfile = $2",
"INSERT INTO pip_resolution_cache (hash, lockfile, expiration) VALUES ($1, $2, now() + ('3 days')::interval) ON CONFLICT (hash) DO UPDATE SET lockfile = $2",
req_hash,
lockfile
).fetch_optional(db).await?;
@@ -449,6 +789,37 @@ async fn postinstall(
Ok(())
}
async fn get_python_path(
py_version: PyVersion,
worker_name: &str,
job_id: &Uuid,
w_id: &str,
mem_peak: &mut i32,
conn: &Connection,
occupancy_metrics: &mut Option<&mut OccupancyMetrics>,
) -> windmill_common::error::Result<String> {
let python_path = if let Some(python_path) = PYTHON_PATH.clone() {
python_path
} else if let Some(python_path) = py_version
.get_python(
&job_id,
mem_peak,
conn,
worker_name,
w_id,
occupancy_metrics,
)
.await?
{
python_path
} else {
return Err(Error::ExecutionErr(format!(
"uv could not manage python path. Please manage it manually by setting PYTHON_PATH environment variable to your python binary path"
)));
};
Ok(python_path)
}
#[tracing::instrument(level = "trace", skip_all)]
pub async fn handle_python_job(
requirements_o: Option<&String>,
@@ -492,16 +863,16 @@ pub async fn handle_python_job(
.await?;
tracing::debug!("Finished handling python dependencies");
let python_path = py_version
.get_python(
worker_name,
&job.id,
&job.workspace_id,
mem_peak,
conn,
&mut Some(occupancy_metrics),
)
.await?;
let python_path = get_python_path(
py_version,
worker_name,
&job.id,
&job.workspace_id,
mem_peak,
conn,
&mut Some(occupancy_metrics),
)
.await?;
if !annotations.no_postinstall {
if let Err(e) = postinstall(&mut additional_python_paths, job_dir, job, conn).await {
@@ -516,7 +887,7 @@ pub async fn handle_python_job(
&job.workspace_id,
format!(
"\n\n--- PYTHON ({}) CODE EXECUTION ---\n",
py_version.clone().to_string()
py_version.to_string_with_dot()
),
conn,
)
@@ -655,7 +1026,7 @@ except BaseException as e:
let mut reserved_variables =
get_reserved_variables(job, &client.token, conn, parent_runnable_path).await?;
// Add /tmp/windmill/cache/python_x_y_z/global-site-packages to PYTHONPATH.
// Add /tmp/windmill/cache/python_xyz/global-site-packages to PYTHONPATH.
// Usefull if certain wheels needs to be preinstalled before execution.
let global_site_packages_path = py_version.to_cache_dir() + "/global-site-packages";
let additional_python_paths_folders = {
@@ -668,9 +1039,9 @@ except BaseException as e:
// Since we handle mount of global_site_packages on our own, we don't want it to be mounted automatically.
// We do this because existence of every wheel in cache is mandatory and if it is not there and nsjail expects it, it is a bug.
// On the other side global_site_packages is purely optional.
// NOTE: This behaviour can be changed in future, so verification of wheels can be delegated from nsjail to windmill
// NOTE: This behaviour can be changed in future, so verification of wheels can be offloaded from nsjail to windmill
paths.insert(0, global_site_packages_path.clone());
// ^^^^^^ ^
// ^^^^^^^^
// We also want this be priorotized, that's why we insert it to the beginning
}
paths.iter().join(":")
@@ -1063,7 +1434,7 @@ async fn handle_python_deps(
occupancy_metrics: &mut Option<&mut OccupancyMetrics>,
precomputed_agent_info: Option<PrecomputedAgentInfo>,
annotations: PythonAnnotations,
) -> error::Result<(PyV, Vec<String>)> {
) -> error::Result<(PyVersion, Vec<String>)> {
create_dependencies_dir(job_dir).await;
let mut additional_python_paths: Vec<String> = WORKER_CONFIG
@@ -1074,116 +1445,90 @@ async fn handle_python_deps(
.unwrap_or_else(|| vec![])
.clone();
let (pyv, resolved_lines) = match requirements_o {
// Deployed
Some(r) => {
let rl = split_requirements(r);
(PyV::parse_from_requirements(&rl), rl)
}
// Preview
let mut requirements;
let compilation_error_hint;
let mut annotated_pyv = None;
let mut annotated_pyv_numeric = None;
let is_deployed = requirements_o.is_some();
let instance_pyv = PyVersion::from_instance_version(job_id, w_id, conn).await;
let requirements = match requirements_o {
Some(r) => r,
None => {
let (v, requirements_lines, error_hint) = match conn {
let mut already_visited = vec![];
(requirements, compilation_error_hint) = match conn {
Connection::Sql(db) => {
let mut version_specifiers = vec![];
let (r, h) = windmill_parser_py_imports::parse_python_imports(
inner_content,
w_id,
script_path,
db,
&mut version_specifiers,
&mut already_visited,
&mut annotated_pyv_numeric,
)
.await?;
let v = PyV::resolve(
version_specifiers,
job_id,
w_id,
annotations.py_select_latest,
Some(conn.clone()),
None,
None,
)
.await?;
(v, r, h)
(r.join("\n"), h)
}
Connection::Http(_) => match precomputed_agent_info {
Some(PrecomputedAgentInfo::Python {
requirements,
py_version,
py_version_v2,
}) => {
let v = {
let v_v2 = py_version_v2
.clone()
.and_then(|s| pep440_rs::Version::from_str(&s).ok().map(PyV::from));
let v_v1 = py_version.and_then(PyVAlias::try_from_v1).map(PyV::from);
match v_v2.or(v_v1) {
Some(v) => v,
None => {
tracing::warn!(
workspace_id = %w_id,
"
Failed to get precomputed python version from server. Fallback to Default ({})
Returned from server: py_version - {:?}, py_version_v2 - {:?}
",
*PyV::default(),
py_version,
py_version_v2
);
Default::default()
}
}
};
let r = split_requirements(requirements.unwrap_or_default());
let h = None;
(v, r, h)
Some(PrecomputedAgentInfo::Python { py_version, requirements }) => {
annotated_pyv_numeric = py_version;
(requirements.clone().unwrap_or_else(|| "".to_string()), None)
}
_ => Default::default(),
_ => ("".to_string(), None),
},
};
(
v.clone(),
if !requirements_lines.is_empty() {
uv_pip_compile(
job_id,
&requirements_lines.join("\n"),
mem_peak,
canceled_by,
job_dir,
conn,
worker_name,
w_id,
occupancy_metrics,
// annotated_pyv.unwrap_or(instance_pyv),
v,
annotations.no_cache,
)
.await
.map_err(|e| {
Error::ExecutionErr(format!(
"pip compile failed: {}{}",
e.to_string(),
error_hint.unwrap_or_default()
))
})?
.lines()
.map(|s| s.to_owned())
.collect_vec()
} else {
vec![]
},
)
annotated_pyv = annotated_pyv_numeric.and_then(|v| PyVersion::from_numeric(v));
if !requirements.is_empty() {
requirements = uv_pip_compile(
job_id,
&requirements,
mem_peak,
canceled_by,
job_dir,
conn,
worker_name,
w_id,
occupancy_metrics,
annotated_pyv.unwrap_or(instance_pyv),
annotations.no_cache,
)
.await
.map_err(|e| {
Error::ExecutionErr(format!(
"pip compile failed: {}{}",
e.to_string(),
compilation_error_hint.unwrap_or_default()
))
})?;
}
&requirements
}
};
if !resolved_lines.is_empty() {
/*
For deployed scripts we want to find out version in following order:
1. Assigned version (written in lockfile)
2. 3.11
For Previews:
1. Annotated version
2. Instance version
3. Latest Stable
*/
let requirements_lines = split_requirements(requirements.as_str());
let final_version = if is_deployed {
get_pyv_from_requirements_lines(&requirements_lines)
} else {
// This is not deployed script, meaning we test run it (Preview)
annotated_pyv.unwrap_or(instance_pyv)
};
// If len > 0 it means there is atleast one dependency or assigned python version
if requirements.len() > 0 {
let mut venv_path = handle_python_reqs(
resolved_lines,
requirements_lines,
job_id,
w_id,
mem_peak,
@@ -1193,13 +1538,13 @@ Returned from server: py_version - {:?}, py_version_v2 - {:?}
job_dir,
worker_dir,
occupancy_metrics,
pyv.clone(),
final_version,
)
.await?;
additional_python_paths.append(&mut venv_path);
}
Ok((pyv, additional_python_paths))
Ok((final_version, additional_python_paths))
}
lazy_static::lazy_static! {
@@ -1388,7 +1733,7 @@ async fn spawn_uv_install(
/// uv pip install, include cached or pull from S3
pub async fn handle_python_reqs(
requirements: Vec<String>,
requirements: Vec<&str>,
job_id: &Uuid,
w_id: &str,
mem_peak: &mut i32,
@@ -1398,7 +1743,7 @@ pub async fn handle_python_reqs(
job_dir: &str,
worker_dir: &str,
_occupancy_metrics: &mut Option<&mut OccupancyMetrics>,
py_version: PyV,
py_version: PyVersion,
) -> error::Result<Vec<String>> {
let worker_dir = worker_dir.to_string();
@@ -1672,7 +2017,7 @@ pub async fn handle_python_reqs(
let total_time = std::time::Instant::now();
let py_path = py_version
.try_get_python(
.get_python(
job_id,
mem_peak,
conn,
@@ -1714,10 +2059,6 @@ pub async fn handle_python_reqs(
let py_path = py_path.clone();
let pids = pids.clone();
let worker_dir = worker_dir.clone();
#[cfg(all(feature = "enterprise", feature = "parquet", unix))]
let py_version = py_version.clone();
handles.push(task::spawn(async move {
// permit will be dropped anyway if this thread exits at any point
// so we dont have to drop it manually
@@ -1959,14 +2300,36 @@ pub async fn handle_python_reqs(
};
}
pub fn split_requirements<T: AsRef<str>>(requirements: T) -> Vec<String> {
fn split_requirements(requirements: &str) -> Vec<&str> {
requirements
.as_ref()
.lines()
.split("\n")
.filter(|x| !x.trim_start().starts_with("--") && !x.trim().is_empty())
.map(String::from)
.collect()
}
/// Check requirements/lockfile to figure out python version assigned to it.
fn get_pyv_from_requirements_lines(requirements_lines: &[&str]) -> PyVersion {
// If script is deployed we can try to parse first line to get assigned version
let index = if requirements_lines.get(0).map_or(false, |line| {
line.starts_with(LOCKFILE_GENERATED_FROM_REQUIREMENTS_TXT)
}) {
1
} else {
0
};
if let Some(v) = requirements_lines
.get(index)
.and_then(|line| PyVersion::parse_version(*line))
{
// We have valid assigned version, we use it
v
} else {
// If there is no assigned version in lockfile we automatically fallback to 3.11
// In this case we have dependencies, but no associated python version
// This is the case for old deployed scripts
PyVersion::Py311
}
}
// Returns code snippet that needs to be injected into wrapper to post-process results or leave unprocessed
fn get_result_postprocessor<'a>(skip: bool) -> &'a str {
@@ -2002,8 +2365,6 @@ pub async fn start_worker(
jobs_rx: tokio::sync::mpsc::Receiver<std::sync::Arc<MiniPulledJob>>,
killpill_rx: tokio::sync::broadcast::Receiver<()>,
) -> error::Result<()> {
use crate::{PyV, PyVAlias};
let mut mem_peak: i32 = 0;
let mut canceled_by: Option<CanceledBy> = None;
let context = variables::get_reserved_variables(
@@ -2157,22 +2518,22 @@ for line in sys.stdin:
proc_envs.insert("BASE_URL".to_string(), base_internal_url.to_string());
let py_version = if let Some(requirements) = requirements_o {
PyV::parse_from_requirements(&split_requirements(requirements.as_str()))
get_pyv_from_requirements_lines(&split_requirements(requirements.as_str()))
} else {
tracing::warn!(workspace_id = %w_id, "lockfile is empty for dedicated worker, thus python version cannot be inferred. Fallback to 3.11");
PyVAlias::Py311.into()
PyVersion::Py311
};
let python_path = py_version
.get_python(
worker_name,
&Uuid::nil(),
w_id,
&mut mem_peak,
&Connection::Sql(db.clone()),
&mut None,
)
.await?;
let python_path = get_python_path(
py_version,
worker_name,
&Uuid::nil(),
w_id,
&mut mem_peak,
&Connection::Sql(db.clone()),
&mut None,
)
.await?;
handle_dedicated_process(
&python_path,
job_dir,
@@ -1,848 +0,0 @@
use std::{
ops::{Deref, DerefMut},
process::Stdio,
str::FromStr,
sync::Arc,
};
use chrono::{DateTime, Duration, Utc};
use itertools::Itertools;
use serde_json::Value;
use tokio::{fs::DirBuilder, process::Command, sync::RwLock};
use uuid::Uuid;
use windmill_common::{
error::{self, Error},
worker::Connection,
};
use anyhow::{anyhow, bail};
use windmill_queue::append_logs;
use crate::{
common::{start_child_process, OccupancyMetrics},
handle_child::handle_child,
python_executor::{PYTHON_PATH, UV_PATH},
worker_lockfiles::LOCKFILE_GENERATED_FROM_REQUIREMENTS_TXT,
HOME_ENV, INSTANCE_PYTHON_VERSION, PATH_ENV, PROXY_ENVS, PY_INSTALL_DIR, WIN_ENVS,
};
#[derive(Eq, PartialEq, Clone, Copy, Default, Debug)]
#[repr(u32)]
pub enum PyVAlias {
Py310 = 10,
#[default]
Py311,
Py312,
Py313,
}
impl Into<pep440_rs::Version> for PyVAlias {
fn into(self) -> pep440_rs::Version {
pep440_rs::Version::new([self.major() as u64, self as u64])
}
}
impl Into<u32> for PyVAlias {
fn into(self) -> u32 {
self.major() * 100 + self as u32
}
}
impl From<PyV> for PyVAlias {
fn from(value: PyV) -> Self {
match value.release() {
[major, minor, ..] => {
if let Some(alias) = Self::try_from_v1(format!("{}{}", *major, *minor)) {
return alias;
}
}
_ => (),
}
tracing::warn!(
"Failed to convert Python Full Version to Alias. Fallback to default ({})",
*PyV::default()
);
Self::default()
}
}
impl PyVAlias {
fn all<T: From<PyVAlias>>() -> Vec<T> {
use PyVAlias::*;
vec![Py310.into(), Py311.into(), Py312.into(), Py313.into()]
}
// Get MAJOR part of alias. (semver: MAJOR.MINOR.PATCH)
fn major(&self) -> u32 {
use PyVAlias::*;
match self {
Py310 | Py311 | Py312 | Py313 => 3,
// Py400 | Py401 => 4
}
}
/// Converts numeric format to alias
/// Example:
/// 310u32 (in) -> PyVAlias::Py310 (out)
pub(crate) fn try_from_v1<T: ToString>(numeric: T) -> Option<Self> {
use PyVAlias::*;
match numeric.to_string().as_str() {
"310" => Some(Py310),
"311" => Some(Py311),
"312" => Some(Py312),
"313" => Some(Py313),
_ => None,
}
}
}
// To change latest stable version:
// 1. Change placeholder in instanceSettings.ts
// 2. Change LATEST_STABLE_PY in dockerfile
// 3. Change #[default] annotation for PyVersion in backend
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct PyV(pub pep440_rs::Version);
impl From<pep440_rs::Version> for PyV {
fn from(value: pep440_rs::Version) -> Self {
Self(value)
}
}
impl From<PyVAlias> for PyV {
fn from(value: PyVAlias) -> Self {
Self(value.into())
}
}
impl Default for PyV {
fn default() -> Self {
PyVAlias::default().into()
}
}
impl Deref for PyV {
type Target = pep440_rs::Version;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for PyV {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl PyV {
pub async fn resolve(
version_specifiers: Vec<pep440_rs::VersionSpecifier>,
job_id: &Uuid,
w_id: &str,
select_latest: bool,
// Needed for logs but optional
conn: Option<Connection>,
// Usually for testing
custom_versions: Option<Vec<PyV>>,
// For testing
gravitational_version: Option<PyV>,
) -> Result<Self, Error> {
// Get all versions that can be fetched
let all_versions = custom_versions.unwrap_or(PyV::list_available_python_versions().await);
// Narrow down to those that satisfy given version specifiers
let valid = all_versions
.clone()
.into_iter()
.filter(|v| version_specifiers.iter().all(|vs| vs.contains(&*v)))
.collect_vec();
if !valid.is_empty() {
if select_latest {
return Ok(valid[0].clone());
}
// Usually INSTANCE_PYTHON_VERSION
let gv = gravitational_version
.unwrap_or(PyV::gravitational_version(job_id, w_id, conn).await);
// Will be used to determine if picked version matches gravity version
// Once first match occure, we will stop iterating
let gravity_matcher = pep440_rs::VersionSpecifier::from_version(
pep440_rs::Operator::EqualStar,
(*gv).clone(),
)
.map_err(|e| {
Error::ArgumentErr(format!(
"{e}\nLikely means INSTANCE_PYTHON_VERSION is set incorrectly."
))
})?;
// Reminder of semver: MAJOR.MINOR.PATCH
//
// - Go from up to down
// - We will iterate until find the closest version to target.
// - If closest version has the same MINOR version, use it.
// - If it differs in MINOR version, take latest PATCH version.
//
let mut result = None;
// This represents newest version with oldest MINOR:
//
// I Iterable Newest in MINOR
// 1. 3.11.2 -> 3.11.2
// 2. 3.11.1 -> 3.11.2
// 3. 3.11.0 -> 3.11.2
// 4. 3.10.2 -> 3.10.2
// 5. 3.10.1 -> 3.10.2
// 6. 3.10.0 -> 3.10.2
let mut newest_in_minor = None;
for v in valid.iter() {
if result.is_none() {
result.replace(v);
}
if v < &gv {
// We will not continue if we start looking into versions older than gravity version.
break;
}
let [major, minor, ..] = v.release() else {
return Err(Error::InternalErr(format!("Failed to parse \"{}\". Available python versions are supposed to be in SEMVER format (MAJOR.MINOR)", **v)));
};
// Since we go top to down we can assume
// the first occurence of new minor version contains the latest patch version.
if matches!(newest_in_minor, Some((_, mm)) if mm != (major, minor))
|| newest_in_minor.is_none()
{
newest_in_minor = Some((v.clone(), (major, minor)));
}
if gravity_matcher.contains(v) {
// return as soon as gravity matcher has first hit.
return Ok(v.clone());
}
// If we are still in the loop, it means that we are getting closer to gravity version
else {
result = Some(v);
}
}
let [gravity_major, gravity_minor, ..] = gv.release() else {
return Err(Error::internal_err(format!("Cannot get MAJOR or/and MINOR version of python gravity version ({}). Something might be wrong with INSTANCE_PYTHON_VERSION.", &*gv)));
};
if let Some((v, mm)) = newest_in_minor {
if (gravity_major, gravity_minor) != mm {
return Ok(v);
}
}
result
.ok_or(Error::internal_err(
"No python candidates found. This is a bug!",
))
.map(ToOwned::to_owned)
} else {
Err(anyhow!(
"
× No solution found when resolving python:
Because you require python {}, we can conclude that your requirements are unsatisfiable.
All versions: \n{}
\n",
version_specifiers.iter().map(|s| s.to_string()).join(", "),
all_versions
.iter()
.enumerate()
.map(|(i, v)| format!(
"{}{}",
windmill_common::worker::pad_string(&v.0.to_string(), 11),
if (i + 1) % 5 == 0 { "\n" } else { "" }
))
.collect::<String>()
)
.into())
}
}
/// e.g.: `/tmp/windmill/cache/python_3xy`
pub(crate) fn to_cache_dir(&self) -> String {
use windmill_common::worker::ROOT_CACHE_DIR;
format!("{ROOT_CACHE_DIR}{}", self.to_cache_dir_top_level())
}
/// e.g.: `python_3_x_y`
pub fn to_cache_dir_top_level(&self) -> String {
format!("python_{}", self.to_string().replace(".", "_"))
}
pub async fn gravitational_version(
job_id: &Uuid,
w_id: &str,
conn: Option<Connection>,
) -> Self {
let mut err = None;
let pyv = match INSTANCE_PYTHON_VERSION.read().await.clone() {
Some(v) => pep440_rs::Version::from_str(&v).unwrap_or_else(|_| {
let v = PyVAlias::default().into();
err = Some(format!("\nCannot parse INSTANCE_PYTHON_VERSION ({:?}), fallback to latest_stable ({v:?})", *INSTANCE_PYTHON_VERSION));
v
}),
// Use latest stable
None => PyVAlias::default().into(),
};
if let Some(msg) = err {
if let Some(conn) = conn {
append_logs(job_id, w_id, &msg, &conn).await;
}
tracing::error!(msg);
}
pyv.into()
}
pub async fn list_available_python_versions() -> Vec<Self> {
match Self::list_available_python_versions_inner().await {
Ok(pyvs) => pyvs,
Err(e) => {
tracing::error!(
"Fallback to preconfigured aliases. Cannot list python versions due to this error: {e}"
);
PyVAlias::all()
}
}
}
async fn list_available_python_versions_inner() -> anyhow::Result<Vec<Self>> {
lazy_static::lazy_static! {
static ref CACHED_VERSIONS: Arc<RwLock<Option<Vec<PyV>>>> = Arc::new(RwLock::new(None));
static ref LAST_CHECKED: Arc<RwLock<DateTime<Utc>>> = Arc::new(RwLock::new(Utc::now()));
}
match (
Utc::now().signed_duration_since(*LAST_CHECKED.read().await) > Duration::minutes(30),
CACHED_VERSIONS.read().await.clone(),
) {
(false, Some(vs)) => return Ok(vs),
_ => {}
};
let output = {
#[cfg(windows)]
let uv_cmd = "uv";
#[cfg(unix)]
let uv_cmd = UV_PATH.as_str();
Command::new(uv_cmd)
.env_clear()
.envs(WIN_ENVS.to_vec())
.args([
"python",
"list",
"--all-versions",
"--output-format",
"json",
])
.stderr(Stdio::piped())
.output()
.await?
};
// We want to skip all versions smaller then 3.10
// Windmill is incompatible with 3.9 and older
let filter = pep440_rs::VersionSpecifier::from_version(
pep440_rs::Operator::GreaterThanEqual,
PyVAlias::Py310.into(),
)?;
if output.status.success() {
let res = String::from_utf8(output.stdout)?;
tracing::error!("{}", &res);
let list = serde_json::from_str::<Vec<serde_json::Map<String, Value>>>(&res)?
.into_iter()
.filter_map(|e| {
if e.get("implementation").and_then(Value::as_str) == Some("pypy") {
None
} else {
Some(
e.get("version")
.and_then(Value::as_str)
.and_then(|s| pep440_rs::Version::from_str(s).ok())
.map(PyV::from)
.ok_or(Error::internal_err("version is None")),
)
}
})
.collect::<Result<Vec<PyV>, Error>>()?
.into_iter()
.unique()
.sorted()
.filter(|pyv| filter.contains(&*pyv))
.rev()
.collect_vec();
*LAST_CHECKED.write().await = Utc::now();
CACHED_VERSIONS.write().await.replace(list.clone());
Ok(list)
} else {
// If the command failed, print the error
let stderr = String::from_utf8(output.stderr)?;
bail!(
"Cannot list python versions, is uv (0.5.19 and newer) installed? Err:\n{}",
stderr
);
}
}
/// Parse lockfile for assigned python version.
/// If not found returns 3.11
pub fn parse_from_requirements<S: AsRef<str>>(requirements_lines: &[S]) -> Self {
Self::try_parse_from_requirements(requirements_lines).unwrap_or(
// If there is no assigned version in lockfile we automatically fallback to 3.11
// In this case we have dependencies or other metadata, but no associated python version
// This is the case for old deployed scripts
PyVAlias::Py311.into(),
)
}
/// Parse lockfile for assigned python version.
/// If not found returns None
pub fn try_parse_from_requirements<S: AsRef<str>>(requirements_lines: &[S]) -> Option<Self> {
let parse_version = |s: &str| -> Option<PyV> {
// Possible inputs:
// V2:
// # py: 3.11.0 or #py:3.11.0 or #py: 3.11.0
//
// V1:
// # py311 or #py311
let version_unparsed = s
.to_owned()
// Remove whitespaces. That leaves us with:
// V2: #py:3.11.0
// V1: #py311
//
// Remove #
// V2: py:3.11.0
// V1: py311
//
// Remove :
// V2: py3.11.0
// V1: py311
.replace([' ', '#', ':'], "")
// Remove "py"
// V2: 3.11.0
// V1: 311
.replace("py", "");
// We will support reading V1 syntax, but it will be overwritten next deploy
PyVAlias::try_from_v1(&version_unparsed)
.map(PyVAlias::into)
.or(pep440_rs::Version::from_str(&version_unparsed)
.ok()
.map(pep440_rs::Version::into))
};
let index = if requirements_lines.get(0).map_or(false, |line| {
line.as_ref()
.starts_with(LOCKFILE_GENERATED_FROM_REQUIREMENTS_TXT)
}) {
1
} else {
0
};
requirements_lines
.get(index)
.map(S::as_ref)
.and_then(parse_version)
}
pub async fn get_python(
&self,
worker_name: &str,
job_id: &Uuid,
w_id: &str,
mem_peak: &mut i32,
conn: &Connection,
occupancy_metrics: &mut Option<&mut OccupancyMetrics>,
) -> windmill_common::error::Result<String> {
let python_path = if let Some(python_path) = PYTHON_PATH.clone() {
python_path
} else if let Some(python_path) = self
.try_get_python(
&job_id,
mem_peak,
conn,
worker_name,
w_id,
occupancy_metrics,
)
.await?
{
python_path
} else {
return Err(Error::ExecutionErr(format!(
"uv could not manage python path. Please manage it manually by setting PYTHON_PATH environment variable to your python binary path"
)));
};
Ok(python_path)
}
pub async fn try_get_python(
&self,
job_id: &Uuid,
mem_peak: &mut i32,
// canceled_by: &mut Option<CanceledBy>,
conn: &Connection,
worker_name: &str,
w_id: &str,
occupancy_metrics: &mut Option<&mut OccupancyMetrics>,
) -> error::Result<Option<String>> {
// lazy_static::lazy_static! {
// static ref PYTHON_PATHS: Arc<RwLock<HashMap<PyVersion, String>>> = Arc::new(RwLock::new(HashMap::new()));
// }
let res = self
.get_python_inner(job_id, mem_peak, conn, worker_name, w_id, occupancy_metrics)
.await;
if let Err(ref e) = res {
tracing::error!(
"worker_name: {worker_name}, w_id: {w_id}, job_id: {job_id}\n
Error while getting python from uv, falling back to system python: {e:?}"
);
append_logs(
job_id,
w_id,
format!(
"\nError while getting python from uv, falling back to system python: {e:?}"
),
conn,
)
.await;
}
res
}
async fn get_python_inner(
&self,
job_id: &Uuid,
mem_peak: &mut i32,
// canceled_by: &mut Option<CanceledBy>,
conn: &Connection,
worker_name: &str,
w_id: &str,
occupancy_metrics: &mut Option<&mut OccupancyMetrics>,
) -> error::Result<Option<String>> {
let py_path = self.find_python().await;
// Runtime is not installed
if py_path.is_err() {
// Install it
if let Err(err) = self
.install_python(job_id, mem_peak, conn, worker_name, w_id, occupancy_metrics)
.await
{
tracing::error!("Cannot install python: {err}");
return Err(err);
} else {
// Try to find one more time
let py_path = self.find_python().await;
if let Err(err) = py_path {
tracing::error!("Cannot find python version {err}");
return Err(err);
}
// TODO: Cache the result
py_path
}
} else {
py_path
}
}
async fn install_python(
&self,
job_id: &Uuid,
mem_peak: &mut i32,
// canceled_by: &mut Option<CanceledBy>,
conn: &Connection,
worker_name: &str,
w_id: &str,
occupancy_metrics: &mut Option<&mut OccupancyMetrics>,
) -> error::Result<()> {
let v = self.to_string();
append_logs(job_id, w_id, format!("\nINSTALLING PYTHON ({})", v), conn).await;
// Create dirs for newly installed python
// If we dont do this, NSJAIL will not be able to mount cache
// For the default version directory created during startup (main.rs)
DirBuilder::new()
.recursive(true)
.create(self.to_cache_dir())
.await
.expect("could not create initial worker dir");
let logs = String::new();
#[cfg(windows)]
let uv_cmd = "uv";
#[cfg(unix)]
let uv_cmd = UV_PATH.as_str();
let mut child_cmd = Command::new(uv_cmd);
child_cmd
.env_clear()
.env("HOME", HOME_ENV.to_string())
.env("PATH", PATH_ENV.to_string())
.envs(PROXY_ENVS.clone())
.args(["python", "install", &v, "--python-preference=only-managed"])
// TODO: Do we need these?
.envs([("UV_PYTHON_INSTALL_DIR", PY_INSTALL_DIR)])
.stdout(Stdio::piped())
.stderr(Stdio::piped());
#[cfg(windows)]
{
child_cmd
.env("SystemRoot", crate::SYSTEM_ROOT.as_str())
.env("USERPROFILE", crate::USERPROFILE_ENV.as_str())
.env(
"TMP",
std::env::var("TMP").unwrap_or_else(|_| String::from("/tmp")),
)
.env(
"LOCALAPPDATA",
std::env::var("LOCALAPPDATA")
.unwrap_or_else(|_| format!("{}\\AppData\\Local", HOME_ENV.as_str())),
);
}
let child_process = start_child_process(child_cmd, "uv").await?;
append_logs(&job_id, &w_id, logs, conn).await;
handle_child(
job_id,
conn,
mem_peak,
&mut None,
child_process,
false,
worker_name,
&w_id,
"uv",
None,
false,
occupancy_metrics,
None,
)
.await
}
async fn find_python(&self) -> error::Result<Option<String>> {
#[cfg(windows)]
let uv_cmd = "uv";
#[cfg(unix)]
let uv_cmd = UV_PATH.as_str();
let mut child_cmd = Command::new(uv_cmd);
child_cmd.env_clear();
#[cfg(windows)]
{
child_cmd
.env("SystemRoot", crate::SYSTEM_ROOT.as_str())
.env("USERPROFILE", crate::USERPROFILE_ENV.as_str())
.env(
"TMP",
std::env::var("TMP").unwrap_or_else(|_| String::from("/tmp")),
)
.env(
"LOCALAPPDATA",
std::env::var("LOCALAPPDATA")
.unwrap_or_else(|_| format!("{}\\AppData\\Local", HOME_ENV.as_str())),
);
}
let output = child_cmd
// .current_dir(job_dir)
.env("HOME", HOME_ENV.to_string())
.env("PATH", PATH_ENV.to_string())
.args([
"python",
"find",
&self.to_string(),
"--system",
"--python-preference=only-managed",
])
.envs([
("UV_PYTHON_INSTALL_DIR", PY_INSTALL_DIR),
("UV_PYTHON_PREFERENCE", "only-managed"),
])
// .stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.await?;
// Check if the command was successful
if output.status.success() {
// Convert the output to a String
let stdout =
String::from_utf8(output.stdout).expect("Failed to convert output to String");
return Ok(Some(stdout.replace('\n', "")));
} else {
// If the command failed, print the error
let stderr =
String::from_utf8(output.stderr).expect("Failed to convert error output to String");
return Err(error::Error::FindPythonError(stderr));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
/// Unsafe helper for testing
fn pyv(value: &str) -> PyV {
pep440_rs::Version::from_str(value).unwrap().into()
}
async fn assert_resolution(
instance_version: &str,
select_highest: bool,
specifiers: Vec<&str>,
available: Vec<PyV>,
expected: PyV,
) {
let resolved = PyV::resolve(
specifiers
.into_iter()
.map(|s| pep440_rs::VersionSpecifier::from_str(s).unwrap())
.collect_vec(),
&Uuid::nil(),
"",
select_highest,
None,
Some(available),
Some(pyv(instance_version)),
)
.await
.unwrap();
assert_eq!(expected, resolved);
}
#[tokio::test]
async fn test_python_resolution_1() {
assert_resolution(
"1.0",
false,
vec![],
vec![
pyv("1.2.0"),
pyv("1.1.0"),
pyv("1.0.0"),
pyv("0.9.0"), //
],
pyv("1.0.0"), //
)
.await;
}
#[tokio::test]
async fn test_python_resolution_2() {
assert_resolution(
"1.0.0",
false,
vec!["!=1.*"],
vec![
pyv("1.2"),
pyv("1.1"),
pyv("1.0.2"),
pyv("1.0.1"),
pyv("1.0.0"),
pyv("0.9.4"),
pyv("0.9.3"),
pyv("0.9.2"),
],
pyv("0.9.4"), //
)
.await;
}
#[tokio::test]
async fn test_python_resolution_3() {
assert_resolution(
"0.9",
false,
vec!["!=0.9.*"],
vec![
pyv("1.2"),
pyv("1.1"),
pyv("1.0.2"),
pyv("1.0.1"),
pyv("1.0.0"),
pyv("0.9.4"),
pyv("0.9.3"),
pyv("0.9.2"),
pyv("0.8.2"),
pyv("0.8.1"),
pyv("0.8.0"),
],
pyv("1.0.2"), //
)
.await;
}
#[tokio::test]
async fn test_python_resolution_4() {
assert_resolution(
"0.9",
false,
vec!["<=0.8.1"],
vec![pyv("1.0.0"), pyv("0.9.0"), pyv("0.8.1"), pyv("0.8.0")],
pyv("0.8.1"), //
)
.await;
}
#[tokio::test]
async fn test_python_resolution_5() {
assert_resolution(
"0.0.1",
false,
vec!["!=0.1.0"],
vec![pyv("2.1.0"), pyv("1.1.0"), pyv("0.1.0")],
pyv("1.1.0"),
)
.await;
}
#[tokio::test]
async fn test_python_resolution_6() {
assert_resolution(
"1.1.1",
false,
vec![],
vec![
pyv("3.0.1"),
pyv("3.0.0"),
pyv("2.2.2"),
pyv("2.2.1"),
pyv("2.2.0"),
],
pyv("2.2.2"),
)
.await;
}
#[tokio::test]
async fn test_python_resolution_7() {
assert_resolution(
"2.2.1",
true,
vec![],
vec![
pyv("3.0.1"),
pyv("3.0.0"),
pyv("2.2.2"),
pyv("2.2.1"),
pyv("2.2.0"),
],
pyv("3.0.1"),
)
.await;
}
}
+5 -24
View File
@@ -134,10 +134,7 @@ use crate::java_executor::{handle_java_job, JobHandlerInput as JobHandlerInputJa
use crate::php_executor::handle_php_job;
#[cfg(feature = "python")]
use crate::{
python_executor::handle_python_job,
python_versions::{PyV, PyVAlias},
};
use crate::python_executor::{handle_python_job, PyVersion};
#[cfg(feature = "python")]
use crate::ansible_executor::handle_ansible_job;
@@ -366,26 +363,10 @@ lazy_static::lazy_static! {
}
type Envs = Vec<(String, String)>;
#[cfg(windows)]
lazy_static::lazy_static! {
pub static ref SYSTEM_ROOT: String = std::env::var("SystemRoot").unwrap_or_else(|_| "C:\\Windows".to_string());
pub static ref USERPROFILE_ENV: String = std::env::var("USERPROFILE").unwrap_or_else(|_| "/tmp".to_string());
static ref TMP: String = std::env::var("TMP").unwrap_or_else(|_| "/tmp".to_string());
static ref LOCALAPPDATA: String = std::env::var("LOCALAPPDATA").unwrap_or_else(|_| format!("{}\\AppData\\Local", HOME_ENV.as_str()));
pub static ref WIN_ENVS: Envs = vec![
("SystemRoot".into(), SYSTEM_ROOT.clone()),
("USERPROFILE".into(), USERPROFILE_ENV.clone()),
("TMP".into(), TMP.clone()),
("LOCALAPPDATA".into(), LOCALAPPDATA.clone())
];
}
#[cfg(not(windows))]
lazy_static::lazy_static! {
pub static ref WIN_ENVS: Envs = vec![];
}
//only matter if CLOUD_HOSTED
@@ -847,9 +828,9 @@ pub async fn run_worker(
worker_dir.clone(),
);
tokio::spawn(async move {
if let Err(e) = PyV::gravitational_version(&Uuid::nil(), "", Some(conn.clone()))
if let Err(e) = PyVersion::from_instance_version(&Uuid::nil(), "", &conn)
.await
.try_get_python(&Uuid::nil(), &mut 0, &conn, &worker_name, "", &mut None)
.get_python(&Uuid::nil(), &mut 0, &conn, &worker_name, "", &mut None)
.await
{
tracing::error!(
@@ -859,8 +840,8 @@ pub async fn run_worker(
"Cannot preinstall or find Instance Python version to worker: {e}"//
);
}
if let Err(e) = PyV::from(PyVAlias::Py311)
.try_get_python(&Uuid::nil(), &mut 0, &conn, &worker_name, "", &mut None)
if let Err(e) = PyVersion::Py311
.get_python(&Uuid::nil(), &mut 0, &conn, &worker_name, "", &mut None)
.await
{
tracing::error!(
+42 -44
View File
@@ -47,7 +47,7 @@ use crate::java_executor::resolve;
use crate::php_executor::{composer_install, parse_php_imports};
#[cfg(feature = "python")]
use crate::python_executor::{
create_dependencies_dir, handle_python_reqs, split_requirements, uv_pip_compile,
create_dependencies_dir, handle_python_reqs, uv_pip_compile, PyVersion,
};
#[cfg(feature = "rust")]
use crate::rust_executor::generate_cargo_lockfile;
@@ -1897,13 +1897,26 @@ async fn python_dep(
w_id: &str,
worker_dir: &str,
occupancy_metrics: &mut Option<&mut OccupancyMetrics>,
py_version: crate::PyV,
annotated_pyv_numeric: Option<u32>,
annotations: PythonAnnotations,
) -> std::result::Result<String, Error> {
use crate::python_executor::split_requirements;
create_dependencies_dir(job_dir).await;
/*
Unlike `handle_python_deps` which we use for running scripts (deployed and drafts)
This one used specifically for deploying scripts
So we can get final_version right away and include in lockfile
And the precendence is following:
1. Annotation version
2. Instance version
3. Latest Stable
*/
let final_version = annotated_pyv_numeric
.and_then(|pyv| PyVersion::from_numeric(pyv))
.unwrap_or(PyVersion::from_instance_version(job_id, w_id, &db.into()).await);
let req: std::result::Result<String, Error> = uv_pip_compile(
job_id,
&reqs,
@@ -1914,15 +1927,14 @@ async fn python_dep(
worker_name,
w_id,
occupancy_metrics,
py_version,
final_version,
annotations.no_cache,
)
.await;
// install the dependencies to pre-fill the cache
if let Ok(req) = req.as_ref() {
let r = handle_python_reqs(
split_requirements(req),
// req.split("\n").filter(|x| !x.starts_with("--")).collect(),
req.split("\n").filter(|x| !x.starts_with("--")).collect(),
job_id,
w_id,
mem_peak,
@@ -1932,8 +1944,7 @@ async fn python_dep(
job_dir,
worker_dir,
occupancy_metrics,
// final_version,
crate::PyVAlias::default().into(),
final_version,
)
.await;
@@ -1983,7 +1994,7 @@ async fn ansible_dep(
w_id,
worker_dir,
&mut Some(occupancy_metrics),
crate::PyV::gravitational_version(job_id, w_id, Some(db.clone().into())).await,
None,
PythonAnnotations::default(),
)
.await?;
@@ -2093,44 +2104,31 @@ async fn capture_dependency_job(
));
#[cfg(feature = "python")]
{
// Manually assigned version from requirements.txt
// let assigned_py_version;
let (reqs, py_version) = if raw_deps {
let anns = PythonAnnotations::parse(job_raw_code);
let mut annotated_pyv_numeric = None;
let reqs = if raw_deps {
// `wmill script generate-metadata`
// should also respect annotated pyversion
// can be annotated in script itself
// or in requirements.txt if present
(
job_raw_code.to_owned(),
crate::PyV::parse_from_requirements(&split_requirements(job_raw_code)),
)
annotated_pyv_numeric =
PyVersion::from_py_annotations(anns).map(|v| v.to_numeric());
job_raw_code.to_string()
} else {
let mut version_specifiers = vec![];
let PythonAnnotations { py_select_latest, .. } =
PythonAnnotations::parse(job_raw_code);
(
windmill_parser_py_imports::parse_python_imports(
job_raw_code,
&w_id,
script_path,
&db,
&mut version_specifiers,
)
.await?
.0
.join("\n"),
crate::PyV::resolve(
version_specifiers,
job_id,
w_id,
py_select_latest,
Some(db.clone().into()),
None,
None,
)
.await?,
let mut already_visited = vec![];
windmill_parser_py_imports::parse_python_imports(
job_raw_code,
&w_id,
script_path,
&db,
&mut already_visited,
&mut annotated_pyv_numeric,
)
.await?
.0
.join("\n")
};
python_dep(
@@ -2144,8 +2142,8 @@ async fn capture_dependency_job(
w_id,
worker_dir,
&mut Some(occupancy_metrics),
py_version,
PythonAnnotations::parse(job_raw_code),
annotated_pyv_numeric,
anns,
)
.await
.map(|res| {
+1 -1
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.492.1";
export const VERSION = "v1.491.5";
export async function login(email: string, password: string): Promise<string> {
return await windmill.UserService.login({
+1 -1
View File
@@ -63,7 +63,7 @@ export {
// }
// });
export const VERSION = "1.492.1";
export const VERSION = "1.491.5";
const command = new Command()
.name("wmill")
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "windmill-components",
"version": "1.492.1",
"version": "1.491.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "windmill-components",
"version": "1.492.1",
"version": "1.491.5",
"hasInstallScript": true,
"license": "AGPL-3.0",
"dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "windmill-components",
"version": "1.492.1",
"version": "1.491.5",
"scripts": {
"dev": "vite dev",
"build": "vite build",
+43 -30
View File
@@ -16,7 +16,9 @@
import ResolveOpen from '$lib/components/common/menu/ResolveOpen.svelte'
import Button from '$lib/components/common/button/Button.svelte'
import { twMerge } from 'tailwind-merge'
import TriggerableByAI from './TriggerableByAI.svelte'
export let id: string = 'dropdown-v2'
export let items: Item[] | (() => Item[]) | (() => Promise<Item[]>) = []
export let disabled = false
export let placement: Placement = 'bottom-end'
@@ -27,6 +29,9 @@
export let open = false
export let customWidth: number | undefined = undefined
export let customMenu = false
export let enableTriggerableByAI = false
let buttonEl = { click: () => {} }
const {
elements: { menu, item, trigger },
@@ -76,36 +81,44 @@
<ResolveOpen {open} on:open on:close />
<button
class={twMerge('w-full flex items-center justify-end', fixedHeight && 'h-8', $$props.class)}
use:melt={$trigger}
{disabled}
on:click={(e) => e.stopPropagation()}
use:pointerDownOutside={{
capture: true,
stopPropagation: false,
exclude: getMenuElements,
customEventName: 'pointerdown_menu'
}}
on:pointerdown_outside={() => {
if (usePointerDownOutside) {
close()
}
}}
data-menu
<TriggerableByAI
{id}
description="Open dropdown"
onTrigger={() => buttonEl.click()}
disabled={!enableTriggerableByAI}
>
{#if $$slots.buttonReplacement}
<slot name="buttonReplacement" />
{:else}
<Button
nonCaptureEvent
size="xs"
color="light"
startIcon={{ icon: MoreVertical }}
btnClasses="bg-transparent"
/>
{/if}
</button>
<button
bind:this={buttonEl}
class={twMerge('w-full flex items-center justify-end', fixedHeight && 'h-8', $$props.class)}
use:melt={$trigger}
{disabled}
on:click={(e) => e.stopPropagation()}
use:pointerDownOutside={{
capture: true,
stopPropagation: false,
exclude: getMenuElements,
customEventName: 'pointerdown_menu'
}}
on:pointerdown_outside={() => {
if (usePointerDownOutside) {
close()
}
}}
data-menu
>
{#if $$slots.buttonReplacement}
<slot name="buttonReplacement" />
{:else}
<Button
nonCaptureEvent
size="xs"
color="light"
startIcon={{ icon: MoreVertical }}
btnClasses="bg-transparent"
/>
{/if}
</button>
</TriggerableByAI>
{#if open && !hidePopup}
<div use:melt={$menu} data-menu class="z-[6000] transition-all duration-100">
@@ -116,7 +129,7 @@
class="bg-surface border w-56 origin-top-right rounded-md shadow-md focus:outline-none overflow-y-auto py-1 max-h-[50vh]"
style={customWidth ? `width: ${customWidth}px` : ''}
>
<DropdownV2Inner items={computeItems} meltItem={item} />
<DropdownV2Inner {id} items={computeItems} meltItem={item} {enableTriggerableByAI} />
</div>
{/if}
</div>
@@ -4,13 +4,22 @@
import { twMerge } from 'tailwind-merge'
import type { MenubarMenuElements } from '@melt-ui/svelte'
import type { Item } from '$lib/utils'
import TriggerableByAI from './TriggerableByAI.svelte'
import { goto } from '$app/navigation'
interface Props {
id?: string
items?: Item[] | (() => Item[]) | (() => Promise<Item[]>)
meltItem: MenubarMenuElements['item']
enableTriggerableByAI?: boolean
}
let { items = [], meltItem }: Props = $props()
let {
id = 'dropdown-v2-inner',
items = [],
meltItem,
enableTriggerableByAI = false
}: Props = $props()
let computedItems: Item[] | undefined = $state(undefined)
async function computeItems() {
@@ -27,29 +36,44 @@
{#if computedItems}
<div class="flex flex-col">
{#each computedItems ?? [] as item}
<MenuItem
on:click={(e) => item?.action?.(e)}
href={item?.href}
disabled={item?.disabled}
class={twMerge(
'px-4 py-2 text-primary font-semibold hover:bg-surface-hover cursor-pointer text-xs transition-all',
'data-[highlighted]:bg-surface-hover',
'flex flex-row gap-2 items-center',
item?.disabled && 'text-gray-400 cursor-not-allowed',
item?.type === 'delete' &&
!item?.disabled &&
'text-red-500 hover:bg-red-100 hover:text-red-500 data-[highlighted]:text-red-500 data-[highlighted]:bg-red-100'
)}
item={meltItem}
<TriggerableByAI
id={`${id}-${item.displayName}`}
description={item.displayName}
onTrigger={() => {
console.log('triggering', item)
if (item.action) {
item.action({} as MouseEvent)
}
if (item.href) {
goto(item.href)
}
}}
disabled={!enableTriggerableByAI}
>
{#if item.icon}
<item.icon size={14} color={item.iconColor} />
{/if}
<p title={item.displayName} class="truncate grow min-w-0 whitespace-nowrap text-left">
{item.displayName}
</p>
{@render item.extra?.()}
</MenuItem>
<MenuItem
on:click={(e) => item?.action?.(e)}
href={item?.href}
disabled={item?.disabled}
class={twMerge(
'px-4 py-2 text-primary font-semibold hover:bg-surface-hover cursor-pointer text-xs transition-all',
'data-[highlighted]:bg-surface-hover',
'flex flex-row gap-2 items-center',
item?.disabled && 'text-gray-400 cursor-not-allowed',
item?.type === 'delete' &&
!item?.disabled &&
'text-red-500 hover:bg-red-100 hover:text-red-500 data-[highlighted]:text-red-500 data-[highlighted]:bg-red-100'
)}
item={meltItem}
>
{#if item.icon}
<item.icon size={14} color={item.iconColor} />
{/if}
<p title={item.displayName} class="truncate grow min-w-0 whitespace-nowrap text-left">
{item.displayName}
</p>
{@render item.extra?.()}
</MenuItem>
</TriggerableByAI>
{/each}
</div>
{:else}
@@ -17,18 +17,11 @@
import ObjectStoreConfigSettings from './ObjectStoreConfigSettings.svelte'
import { sendUserToast } from '$lib/toast'
import ConfirmButton from './ConfirmButton.svelte'
import {
ConfigService,
IndexSearchService,
SettingService,
TeamsService,
type ListAvailablePythonVersionsResponse
} from '$lib/gen'
import { IndexSearchService, SettingService, TeamsService } from '$lib/gen'
import { Button, SecondsInput, Skeleton } from './common'
import Password from './Password.svelte'
import { classNames } from '$lib/utils'
import Popover from './Popover.svelte'
import PopoverMelt from './meltComponents/Popover.svelte'
import Toggle from './Toggle.svelte'
import type { Writable } from 'svelte/store'
import { createEventDispatcher } from 'svelte'
@@ -37,7 +30,6 @@
import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte'
import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
import SimpleEditor from './SimpleEditor.svelte'
import LoadingIcon from './apps/svelte-select/lib/LoadingIcon.svelte'
import TeamSelector from './TeamSelector.svelte'
import ChannelSelector from './ChannelSelector.svelte'
@@ -47,10 +39,7 @@
export let loading = true
const dispatch = createEventDispatcher()
if (
(setting.fieldType == 'select' || setting.fieldType == 'select_python') &&
$values[setting.key] == undefined
) {
if (setting.fieldType == 'select' && $values[setting.key] == undefined) {
$values[setting.key] = 'default'
}
@@ -135,24 +124,6 @@
}
}
let pythonAvailableVersions: ListAvailablePythonVersionsResponse = []
let isPyFetching = false
async function fetch_available_python_versions() {
if (isPyFetching) return
isPyFetching = true
try {
pythonAvailableVersions = await ConfigService.listAvailablePythonVersions()
} catch (error) {
console.error('Error fetching python versions:', error)
} finally {
isPyFetching = false
}
}
if (setting.fieldType == 'select_python') {
fetch_available_python_versions()
}
async function fetchTeams() {
if (isFetching) return
isFetching = true
@@ -222,66 +193,6 @@
{/each}
</ToggleButtonGroup>
</div>
{:else if setting.fieldType == 'select_python'}
<div>
<!-- svelte-ignore a11y-label-has-associated-control -->
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">{setting.label}</span>
{#if setting.description}
<span class="text-secondary text-xs">
{@html setting.description}
</span>
{/if}
</label>
<ToggleButtonGroup bind:selected={$values[setting.key]} let:item={toggleButtonn}>
{#each setting.select_items ?? [] as item}
<ToggleButton
value={item.value ?? item.label}
label={item.label}
tooltip={item.tooltip}
item={toggleButtonn}
/>
{/each}
<PopoverMelt closeButton={!isPyFetching}>
<svelte:fragment slot="trigger">
{#if setting.select_items?.some((e) => e.label == $values[setting.key] || e.value == $values[setting.key])}
<Button
variant="border"
color="dark"
btnClasses="px-1.5 py-1.5 text-2xs bg-surface-secondary border-0"
nonCaptureEvent={true}>Select Custom</Button
>
{:else}
<Button
variant="border"
color="dark"
btnClasses="px-1.5 py-1.5 text-2xs border-0 shadow-md"
nonCaptureEvent={true}>Custom | {$values[setting.key]}</Button
>
{/if}
</svelte:fragment>
<svelte:fragment slot="content">
{#if isPyFetching}
<div class="p-4">
<LoadingIcon />
</div>
{:else}
<ToggleButtonGroup
bind:selected={$values[setting.key]}
let:item={toggleButtonn}
class="mr-10 h-full"
tabListClass="flex-wrap p-2"
>
{#each pythonAvailableVersions as item}
<ToggleButton value={item} label={item} tooltip={item} item={toggleButtonn} />
{/each}
</ToggleButtonGroup>
{/if}
</svelte:fragment>
</PopoverMelt>
</ToggleButtonGroup>
</div>
{:else}
<!-- svelte-ignore a11y-label-has-associated-control -->
<label class="block pb-2">
@@ -7,7 +7,8 @@
type Script,
type TriggersCount,
PostgresTriggerService,
CaptureService
CaptureService,
type ScriptLang
} from '$lib/gen'
import { inferArgs } from '$lib/infer'
import { initialCode } from '$lib/script_helpers'
@@ -92,6 +93,7 @@
} from './triggers/utils'
import DraftTriggersConfirmationModal from './common/confirmationModal/DraftTriggersConfirmationModal.svelte'
import { Triggers } from './triggers/triggers.svelte'
import TriggerableByAI from './TriggerableByAI.svelte'
export let script: NewScript & { draft_triggers?: Trigger[] }
export let fullyLoaded: boolean = true
@@ -869,6 +871,47 @@
newSavedDraftTrigers.length > 0 ? newSavedDraftTrigers : undefined
}
}
function onScriptLanguageTrigger(lang: 'docker' | 'bunnative' | ScriptLang) {
if (lang == 'docker') {
if (isCloudHosted()) {
sendUserToast(
'You cannot use Docker scripts on the multi-tenant platform. Use a dedicated instance or self-host windmill instead.',
true,
[
{
label: 'Learn more',
callback: () => {
window.open('https://www.windmill.dev/docs/advanced/docker', '_blank')
}
}
]
)
return
}
template = 'docker'
} else if (lang == 'bunnative') {
template = 'bunnative'
} else {
template = 'script'
}
let language = langToLanguage(lang)
//
initContent(language, script.kind, template)
script.language = language
}
function onSummaryChange(value: string) {
if (initialPath == '' && value?.length > 0 && !dirtyPath) {
path?.setName(
value
.toLowerCase()
.replace(/[^a-z0-9_]/g, '_')
.replace(/-+/g, '_')
.replace(/^-|-$/g, '')
)
}
}
</script>
<svelte:window on:keydown={onKeyDown} />
@@ -947,29 +990,31 @@
</svelte:fragment>
<div class="flex flex-col gap-4">
<Label label="Summary">
<MetadataGen
label="Summary"
bind:content={script.summary}
lang={script.language}
code={script.content}
promptConfigName="summary"
generateOnAppear
on:change={() => {
if (initialPath == '' && script.summary?.length > 0 && !dirtyPath) {
path?.setName(
script.summary
.toLowerCase()
.replace(/[^a-z0-9_]/g, '_')
.replace(/-+/g, '_')
.replace(/^-|-$/g, '')
)
<TriggerableByAI
id="create-script-summary-input"
description="Summary / Title of the new script"
onTrigger={(value) => {
console.log('Triggering example component with value', value)
if (value) {
script.summary = value
onSummaryChange(value)
}
}}
elementProps={{
type: 'text',
placeholder: 'Short summary to be displayed when listed'
}}
/>
>
<MetadataGen
label="Summary"
bind:content={script.summary}
lang={script.language}
code={script.content}
promptConfigName="summary"
generateOnAppear
on:change={() => onSummaryChange(script.summary)}
elementProps={{
type: 'text',
placeholder: 'Short summary to be displayed when listed'
}}
/>
</TriggerableByAI>
</Label>
<Label label="Path">
<svelte:fragment slot="header">
@@ -1022,53 +1067,32 @@
<Popover
disablePopup={!enterpriseLangs.includes(lang) || !!$enterpriseLicense}
>
<Button
size="sm"
variant="border"
color={isPicked ? 'blue' : 'light'}
btnClasses={isPicked
? '!border-2 !bg-blue-50/75 dark:!bg-frost-900/75'
: 'm-[1px]'}
on:click={() => {
if (lang == 'docker') {
if (isCloudHosted()) {
sendUserToast(
'You cannot use Docker scripts on the multi-tenant platform. Use a dedicated instance or self-host windmill instead.',
true,
[
{
label: 'Learn more',
callback: () => {
window.open(
'https://www.windmill.dev/docs/advanced/docker',
'_blank'
)
}
}
]
)
return
}
template = 'docker'
} else if (lang == 'bunnative') {
template = 'bunnative'
} else {
template = 'script'
}
let language = langToLanguage(lang)
//
initContent(language, script.kind, template)
script.language = language
<TriggerableByAI
id={`create-script-language-button-${lang}`}
description={`Choose ${lang} as the language of the script`}
onTrigger={() => {
console.log('Triggering example component', lang)
onScriptLanguageTrigger(lang)
}}
disabled={lockedLanguage ||
(enterpriseLangs.includes(lang) && !$enterpriseLicense)}
>
<LanguageIcon {lang} />
<span class="ml-2 py-2 truncate">{label}</span>
{#if lang === 'ansible' || lang === 'nu'}
<span class="text-tertiary !text-xs"> BETA </span>
{/if}
</Button>
<Button
size="sm"
variant="border"
color={isPicked ? 'blue' : 'light'}
btnClasses={isPicked
? '!border-2 !bg-blue-50/75 dark:!bg-frost-900/75'
: 'm-[1px]'}
on:click={() => onScriptLanguageTrigger(lang)}
disabled={lockedLanguage ||
(enterpriseLangs.includes(lang) && !$enterpriseLicense)}
>
<LanguageIcon {lang} />
<span class="ml-2 py-2 truncate">{label}</span>
{#if lang === 'ansible' || lang === 'nu'}
<span class="text-tertiary !text-xs"> BETA </span>
{/if}
</Button>
</TriggerableByAI>
<svelte:fragment slot="text"
>{label} is only available with an enterprise license</svelte:fragment
>
@@ -15,9 +15,6 @@
import AnsiUp from 'ansi_up'
import { scroll_into_view_if_needed_polyfill } from './multiselect/utils'
import SplitPanesOrColumnOnMobile from './splitPanes/SplitPanesOrColumnOnMobile.svelte'
import Select from './apps/svelte-select/lib/Select.svelte'
import { SELECT_INPUT_DEFAULT_STYLE } from '$lib/defaults'
import DarkModeObserver from './DarkModeObserver.svelte'
export let searchTerm: string
export let queryParseErrors: string[] = []
@@ -322,13 +319,10 @@
const buckets = res['buckets']
sumOtherDocCount = res['sum_other_doc_count']
countsPerHost = new Map(buckets.map(({ key, doc_count }) => [key, doc_count]))
countsPerHost = buckets.reduce(
(acc: any, { key, doc_count }) => {
acc[key] = { doc_count }
return acc
},
{} as Record<string, number>
)
countsPerHost = buckets.reduce((acc: any, { key, doc_count }) => {
acc[key] = { doc_count }
return acc
}, {} as Record<string, number>)
queryParseErrors = countLogsResponse.query_parse_errors ?? []
loadingLogCounts = false
}
@@ -382,7 +376,7 @@
let ret = {}
for (const hk of Object.keys(countsPerHost)) {
let u = hk.split(',')
let u = hk.split(",")
let [mode, wg, hn] = [u[0], u[1], u[2]]
if (!ret[mode]) {
@@ -398,23 +392,8 @@
return ret
}
function getSelectItems(allLogs: ByMode, countsPerHost: any): { label: string; value: any }[] {
return Object.entries(allLogsOrQueryResults(allLogs, countsPerHost)).flatMap(([mode, o1]) =>
Object.entries(o1).flatMap(([wg, o2]) =>
Object.keys(o2).map((hn) => ({
label: hn,
value: [mode, wg, hn]
}))
)
)
}
let darkMode = false
</script>
<DarkModeObserver bind:darkMode />
<Drawer bind:this={logDrawer} bind:open={logDrawerOpen} size="1400px">
<DrawerContent title="See context" on:close={logDrawer.closeDrawer}>
<svelte:fragment slot="actions">
@@ -457,7 +436,7 @@
month: '2-digit',
hour: '2-digit',
minute: '2-digit'
})
})
: 'min datetime'}
disabled
/>
@@ -497,7 +476,7 @@
month: '2-digit',
hour: '2-digit',
minute: '2-digit'
})
})
: 'max datetime'}
disabled
/>
@@ -569,24 +548,6 @@
>
</div>
{/if}
<div class="mr-0.5">
<Select
justValue={selected}
items={getSelectItems(allLogs, countsPerHost)}
on:change={({ detail }) => {
// console.log(detail)
selected = detail.value
}}
on:clear={() => {
selected = undefined
}}
placeholder="Select a service"
inputStyles={SELECT_INPUT_DEFAULT_STYLE.inputStyles}
containerStyles={darkMode
? SELECT_INPUT_DEFAULT_STYLE.containerStylesDark
: SELECT_INPUT_DEFAULT_STYLE.containerStyles}
/>
</div>
{#each Object.entries(allLogsOrQueryResults(allLogs, countsPerHost)) as [mode, o1]}
<div class="w-full pb-8">
<h2 class="pb-2 text-2xl">{mode}s</h2>
@@ -626,8 +587,7 @@
<div
class="text-sm pt-2 pl-0.5 whitespace-nowrap"
title={hn}
style="width: 90px;"
>{truncateRev(hn, countsPerHost || loadingLogs ? 40 : 8)}</div
style="width: 90px;">{truncateRev(hn, countsPerHost || loadingLogs ? 40 : 8)}</div
>
{#if loadingLogCounts}
<Loader2 size={15} class="animate-spin" />
@@ -792,7 +752,7 @@
month: '2-digit',
hour: '2-digit',
minute: '2-digit'
})
})
: ''}
disabled
/><CalendarPicker bind:date={upTo} label="Logs up to" /></div
@@ -0,0 +1,96 @@
<script lang="ts">
import { triggerablesByAI } from '$lib/stores'
let {
id,
description,
onTrigger,
children,
disabled = false
} = $props<{
id: string
description: string
onTrigger: (value?: string) => void
children?: () => any
disabled?: boolean
}>()
// Track animation state
let isAnimating = $state(false)
// Wrapper for onTrigger that adds animation
function handleTrigger(value?: string) {
if (disabled || !onTrigger) return
// Show animation
isAnimating = true
// Call the actual onTrigger
onTrigger(value)
// Reset animation state after animation completes
setTimeout(() => {
isAnimating = false
}, 1200) // Animation duration
}
$effect(() => {
if (disabled) return
triggerablesByAI.update((triggers) => {
return { ...triggers, [id]: { description, onTrigger: handleTrigger } }
})
return () => {
triggerablesByAI.update((triggers) => {
const newTriggers = { ...triggers }
delete newTriggers[id]
return newTriggers
})
}
})
</script>
<div class="ai-triggerable-wrapper">
{#if isAnimating}
<div class="ai-triggerable-animation"></div>
{/if}
<div class="ai-triggerable-content">
{@render children?.()}
</div>
</div>
<style>
.ai-triggerable-wrapper {
position: relative;
}
.ai-triggerable-content {
/* This preserves original styling of children */
display: contents;
}
.ai-triggerable-animation {
position: absolute;
top: -20px;
left: 50%;
transform: translateX(-50%);
width: 40px;
height: 40px;
background-color: rgba(66, 133, 244, 0.9);
border-radius: 50%;
z-index: 9999;
pointer-events: none;
animation: pulse 0.6s ease-out forwards;
}
@keyframes pulse {
0% {
transform: translateX(-50%) scale(0);
opacity: 1;
}
100% {
transform: translateX(-50%) scale(2.5);
opacity: 0;
}
}
</style>
@@ -1,4 +1,4 @@
<script lang="ts">
<script>
// @ts-nocheck
import { beforeUpdate, createEventDispatcher, onDestroy, onMount } from 'svelte'
import { offset, flip, shift } from '@floating-ui/dom'
@@ -22,13 +22,13 @@
export let portal = true
export let justValue: any = null // read-only
export let justValue = null // read-only
export let inAppEditor = false
let PortalWrapper = inAppEditor ? ConditionalPortal : ConditionalPortalGlobal
export let filter: (args: any) => any[] = _filter
export let filter = _filter
export let getItems = _getItems
export let id = null
@@ -38,19 +38,18 @@
export let disabled = false
export let focused = false
export let value: any = undefined
export let value = undefined
export let filterText = ''
export let placeholder = 'Please select'
export let items: { label: string | undefined; value: any }[] | any[] | string[] | undefined =
undefined
export let items = undefined
export let label = 'label'
export let itemFilter = (label, filterText, option) =>
`${label}`.toLowerCase().includes(filterText.toLowerCase())
export let groupBy: ((item: any) => string) | undefined = undefined
export let groupBy = undefined
export let groupFilter = (groups) => groups
export let groupHeaderSelectable = false
export let itemId = 'value'
export let loadOptions: ((string) => Promise<any>) | undefined = undefined
export let loadOptions = undefined
export let containerStyles = ''
export let hasError = false
export let filterSelectedItems = true
@@ -72,6 +72,10 @@
element?.focus({})
}
export function click() {
element?.click()
}
const dispatch = createEventDispatcher()
const dispatchIfMounted = createDispatcherIfMounted(dispatch)
// Order of classes: border, border modifier, bg, bg modifier, text, text modifier, everything else
@@ -2,7 +2,8 @@
import Tooltip from '$lib/components/Tooltip.svelte'
import { classNames } from '$lib/utils'
import CloseButton from '../CloseButton.svelte'
import TriggerableByAI from '$lib/components/TriggerableByAI.svelte'
import { createEventDispatcher } from 'svelte'
export let title: string | undefined = undefined
export let overflow_y = true
export let noPadding = false
@@ -12,13 +13,22 @@
export let CloseIcon: any | undefined = undefined
export let fullScreen: boolean = true
const dispatch = createEventDispatcher()
</script>
<div class={classNames('flex flex-col divide-y', fullScreen ? 'h-screen max-h-screen' : 'h-full')}>
<div class="flex justify-between w-full items-center px-4 py-2 gap-2">
<div class="flex items-center gap-2 w-full truncate">
<CloseButton on:close Icon={CloseIcon} />
<TriggerableByAI
id={`close-drawer-button-${title?.toLowerCase().replace(/ /g, '-')}`}
description={`Closes the drawer titled ${title}`}
onTrigger={() => {
dispatch('close')
}}
>
<CloseButton on:close Icon={CloseIcon} />
</TriggerableByAI>
<span class="font-semibold truncate text-primary !text-lg max-w-sm"
>{title ?? ''}
{#if tooltip != '' || documentationLink}
@@ -165,6 +165,8 @@
{/if}
</span>
<Dropdown
id={`script-row-dropdown-${script.path}-${script.summary}`}
enableTriggerableByAI
items={async () => {
let owner = isOwner(script.path, $userStore, $workspaceStore)
if (script.draft_only) {
@@ -315,6 +317,7 @@
if (event?.shiftKey) {
deleteScript(script.path)
} else {
console.log('setting delete confirmed callback')
deleteConfirmedCallback = () => {
deleteScript(script.path)
}
@@ -0,0 +1,65 @@
<script lang="ts">
import { chatRequest, prepareUserMessage, prepareSystemMessage } from './core'
// Using Svelte 5 runes for reactivity
let inputValue = $state('')
let isSubmitting = $state(false)
let currentReply = $state('')
let abortController = new AbortController()
// Props definition using $props
let { placeholder = 'Type a message...', buttonText = 'Send' } = $props()
async function handleSubmit() {
if (!inputValue.trim()) return
isSubmitting = true
currentReply = ''
const userMessage = prepareUserMessage(inputValue)
const systemMessage = prepareSystemMessage()
let messages = [systemMessage]
messages.push({ role: 'user', content: userMessage })
const result = await chatRequest(messages, abortController, (token) => {
currentReply = currentReply + token
})
console.log(result)
console.log(currentReply)
// Reset the input field
inputValue = ''
isSubmitting = false
}
</script>
<div class="flex flex-col gap-2">
<form
class="flex w-full gap-2"
onsubmit={(e) => {
e.preventDefault()
handleSubmit()
}}
>
<input
type="text"
class="flex-1 px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
bind:value={inputValue}
{placeholder}
disabled={isSubmitting}
/>
<button
type="submit"
class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-50"
disabled={isSubmitting || !inputValue.trim()}
>
{buttonText}
</button>
</form>
<div class="flex flex-row border rounded-md p-2">
<p>{currentReply}</p>
</div>
</div>
@@ -0,0 +1,248 @@
import { get, type Writable } from 'svelte/store'
import { getCompletion } from '$lib/components/copilot/lib'
import type {
ChatCompletionChunk,
ChatCompletionMessageParam,
ChatCompletionMessageToolCall,
ChatCompletionTool
} from 'openai/resources/index.mjs'
import { triggerablesByAI } from '$lib/stores'
// System prompt for the LLM
export const CHAT_SYSTEM_PROMPT = `
You are an assistant that can interact with the user's web page in order to help them find and do things.
You have access to tools that let you:
1. View the current triggerable components on the page
2. Execute the trigger function of a triggerable component
When asked to interact with the page:
- First examine the page structure to understand what's available
- Explain what you're doing before taking action
- Take action only if you're sure it's what the user wants
- After executing a command, wait for 1 second before rechecking the page and continuing fulfulling the user request. At each step, explain what you're doing before taking action.
- After fulfilling the user request, if there is a close button associated with a drawer or settings panel, use it to close the drawer or settings panel.
Use the provided tools only when necessary and appropriate.
`
// Tool definitions
const GET_PAGE_HTML_TOOL: ChatCompletionTool = {
type: 'function',
function: {
name: 'get_triggerable_components',
description: 'Get the current triggerable components on the page',
parameters: {
type: 'object',
properties: {},
required: []
}
}
}
const EXECUTE_COMMAND_TOOL: ChatCompletionTool = {
type: 'function',
function: {
name: 'trigger_component',
description: 'Trigger a triggerable component',
parameters: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'ID of the AI-triggerable component'
},
value: {
type: 'string',
description: 'Value to pass to the AI-triggerable component trigger function'
}
},
required: ['id']
}
}
}
// Function to get page HTML
function getTriggerableComponents(): string {
try {
// Get components registered in the triggerablesByAI store
const registeredComponents = get(triggerablesByAI)
let result = 'TRIGGERABLE_COMPONENTS:\n'
// If there are no components registered, return a message
if (Object.keys(registeredComponents).length === 0) {
return 'No AI-triggerable components are currently available on this page.\n'
}
// List each registered component with its ID and description
Object.entries(registeredComponents).forEach(([id, component], index) => {
result += `[${index}] ID: "${id}" - ${component.description}\n`
})
return result
} catch (error) {
console.error('Error getting triggerable components:', error)
return 'Error getting triggerable components: ' + error.message
}
}
// Function to execute commands on the page
function triggerComponent(args: { id: string; value: string }): string {
const { id, value } = args
try {
// Handle triggering AI components
if (!id) {
return 'Trigger command requires an id parameter'
}
const components = get(triggerablesByAI)
const component = components[id]
if (!component) {
return `No triggerable component found with id: ${id}`
}
if (component.onTrigger) {
component.onTrigger(value)
return `Successfully triggered component: ${id} (${component.description})`
} else {
return `Component ${id} has no trigger handler defined`
}
} catch (error) {
console.error('Error executing command:', error)
return `Error executing command: ${error.message}`
}
}
// Process tool calls from the LLM
async function processToolCall(
toolCall: ChatCompletionMessageToolCall,
messages: ChatCompletionMessageParam[]
) {
try {
const args = toolCall.function.arguments ? JSON.parse(toolCall.function.arguments) : {}
let result = ''
try {
if (toolCall.function.name === 'get_triggerable_components') {
result = getTriggerableComponents()
} else if (toolCall.function.name === 'trigger_component') {
result = triggerComponent(args)
} else {
result = `Unknown tool: ${toolCall.function.name}`
}
} catch (err) {
console.error(err)
result = `Error while calling ${toolCall.function.name}: ${err.message}`
}
messages.push({
role: 'tool',
tool_call_id: toolCall.id,
content: result
})
} catch (err) {
console.error(err)
}
}
// Main function to handle chat requests
export async function chatRequest(
messages: ChatCompletionMessageParam[],
abortController: AbortController,
onNewToken: (token: string) => void
) {
const toolDefs: ChatCompletionTool[] = [GET_PAGE_HTML_TOOL, EXECUTE_COMMAND_TOOL]
try {
let completion: any = null
while (true) {
completion = await getCompletion(messages, abortController, toolDefs)
console.log(completion)
if (completion) {
const finalToolCalls: Record<number, ChatCompletionChunk.Choice.Delta.ToolCall> = {}
for await (const chunk of completion) {
if (!('choices' in chunk && chunk.choices.length > 0 && 'delta' in chunk.choices[0])) {
continue
}
const c = chunk as ChatCompletionChunk
const delta = c.choices[0].delta.content
if (delta) {
onNewToken(delta)
}
const toolCalls = c.choices[0].delta.tool_calls || []
for (const toolCall of toolCalls) {
const { index } = toolCall
const finalToolCall = finalToolCalls[index]
if (!finalToolCall) {
finalToolCalls[index] = toolCall
} else {
if (toolCall.function?.arguments) {
if (!finalToolCall.function) {
finalToolCall.function = toolCall.function
} else {
finalToolCall.function.arguments =
(finalToolCall.function.arguments ?? '') + toolCall.function.arguments
}
}
}
}
}
const toolCalls = Object.values(finalToolCalls).filter(
(toolCall) => toolCall.id !== undefined && toolCall.function?.arguments !== undefined
) as ChatCompletionMessageToolCall[]
if (toolCalls.length > 0) {
messages.push({
role: 'assistant',
tool_calls: toolCalls
})
for (const toolCall of toolCalls) {
await processToolCall(toolCall, messages)
}
} else {
break
}
}
}
return completion
} catch (err) {
if (!abortController.signal.aborted) {
throw err
}
}
}
// Prepare initial system message
export function prepareSystemMessage(): ChatCompletionMessageParam {
return {
role: 'system',
content: CHAT_SYSTEM_PROMPT
}
}
// Prepare user message with context
export function prepareUserMessage(message: string): string {
return `
MESSAGE: ${message}
Feel free to use the get_page_html tool first if you need to understand the current page structure.
`
}
// Interface for chat context
export interface AIChatContext {
loading: Writable<boolean>
currentReply: Writable<string>
}
@@ -19,7 +19,6 @@ export interface Setting {
| 'boolean'
| 'password'
| 'select'
| 'select_python'
| 'textarea'
| 'codearea'
| 'seconds'
@@ -232,7 +231,7 @@ export const settings: Record<string, Setting[]> = {
label: 'Instance Python Version',
description: 'Default python version for newly deployed scripts',
key: 'instance_python_version',
fieldType: 'select_python',
fieldType: 'select',
// To change latest stable version:
// 1. Change placeholder in instanceSettings.ts
// 2. Change LATEST_STABLE_PY in dockerfile
@@ -2,11 +2,26 @@
import { Code2, Plus } from 'lucide-svelte'
import Button from '../common/button/Button.svelte'
import { base } from '$lib/base'
import { goto } from '$app/navigation'
// Reference to the button component
let buttonComponent: { click: () => void } | undefined = undefined
export function triggerClick() {
// Navigate to the script creation page directly
// goto(`${base}/scripts/add`)
// Focus the button for visual feedback
if (buttonComponent) {
buttonComponent.click()
}
}
</script>
<!-- Buttons -->
<div class="flex flex-row gap-2">
<Button
bind:this={buttonComponent}
size="sm"
spacingSize="xl"
color="marine"
@@ -8,10 +8,7 @@
type Flow,
type ListableApp,
type ListableRawApp,
type Script,
type SearchJobsIndexResponse
type Script
} from '$lib/gen'
import { clickOutside, isMac } from '$lib/utils'
import {
@@ -93,56 +90,56 @@
{
search_id: 'nav:http_routes',
label: 'Go to HTTP routes',
action: (newtab: boolean = false) => gotoPage('/routes', newtab),
action: () => gotoPage('/routes'),
icon: Route,
disabled: $userStore?.operator
},
{
search_id: 'nav:web_sockets',
label: 'Go to WebSockets',
action: (newtab: boolean = false) => gotoPage('/websocket_triggers', newtab),
action: () => gotoPage('/websocket_triggers'),
icon: Unplug,
disabled: $userStore?.operator
},
{
search_id: 'nav:postgres_triggers',
label: 'Go to Postgres triggers',
action: (newtab: boolean = false) => gotoPage('/postgres_triggers', newtab),
action: () => gotoPage('/postgres_triggers'),
icon: Database,
disabled: $userStore?.operator
},
{
search_id: 'nav:kafka_triggers',
label: 'Go to Kafka triggers' + (!$enterpriseLicense ? '' : ' (EE)'),
action: (newtab: boolean = false) => gotoPage('/kafka_triggers', newtab),
action: () => gotoPage('/kafka_triggers'),
icon: KafkaIcon,
disabled: $userStore?.operator
},
{
search_id: 'nav:nats_triggers',
label: 'Go to NATS triggers' + (!$enterpriseLicense ? '' : ' (EE)'),
action: (newtab: boolean = false) => gotoPage('/nats_triggers', newtab),
action: () => gotoPage('/nats_triggers'),
icon: NatsIcon,
disabled: $userStore?.operator
},
{
search_id: 'nav:sqs_triggers',
label: 'Go to SQS triggers' + (!$enterpriseLicense ? '' : ' (EE)'),
action: (newtab: boolean = false) => gotoPage('/sqs_triggers', newtab),
action: () => gotoPage('/sqs_triggers'),
icon: AwsIcon,
disabled: $userStore?.operator
},
{
search_id: 'nav:gcp_pub_sub',
label: 'Go to GCP Pub/Sub' + (!$enterpriseLicense ? '' : ' (EE)'),
action: (newtab: boolean = false) => gotoPage('/gcp_triggers', newtab),
action: () => gotoPage('/gcp_triggers'),
icon: GoogleCloudIcon,
disabled: $userStore?.operator
},
{
search_id: 'nav:mqtt_triggers',
label: 'Go to MQTT triggers',
action: (newtab: boolean = false) => gotoPage('/mqtt_triggers', newtab),
action: () => gotoPage('/mqtt_triggers'),
icon: MqttIcon,
disabled: $userStore?.operator
}
@@ -152,35 +149,35 @@
{
search_id: 'nav:home',
label: 'Go to Home',
action: (newtab: boolean = false) => gotoPage('/', newtab),
action: () => gotoPage('/'),
icon: HomeIcon,
disabled: false
},
{
search_id: 'nav:runs',
label: 'Go to Runs',
action: (newtab: boolean = false) => gotoPage('/runs', newtab),
action: () => gotoPage('/runs'),
icon: PlayIcon,
disabled: false
},
{
search_id: 'nav:variables',
label: 'Go to Variables',
action: (newtab: boolean = false) => gotoPage('/variables', newtab),
action: () => gotoPage('/variables'),
icon: DollarSignIcon,
disabled: false
},
{
search_id: 'nav:resources',
label: 'Go to Resources',
action: (newtab: boolean = false) => gotoPage('/resources', newtab),
action: () => gotoPage('/resources'),
icon: BoxesIcon,
disabled: false
},
{
search_id: 'nav:schedules_triggers',
label: 'Go to Schedules',
action: (newtab: boolean = false) => gotoPage('/schedules', newtab),
action: () => gotoPage('/schedules'),
icon: CalendarIcon,
disabled: false
},
@@ -188,7 +185,7 @@
{
search_id: 'nav:service_logs',
label: 'Explore windmill service logs',
action: (newtab: boolean = false) => gotoPage('/service_logs', newtab),
action: () => gotoPage('/service_logs'),
shortcutKey: LOGS_PREFIX,
icon: Logs,
disabled: !$devopsRole
@@ -386,7 +383,7 @@
textInput.focus()
}
function gotoWindmillItemPage(e: TableAny, newtab: boolean = false) {
function gotoWindmillItemPage(e: TableAny) {
let path: string
switch (e.type) {
case 'flow':
@@ -404,17 +401,13 @@
default:
path = '/'
}
gotoPage(path, newtab)
gotoPage(path)
}
function gotoPage(path: string, newtab: boolean = false) {
function gotoPage(path: string) {
open = false
searchTerm = ''
if (!newtab) {
open = false
goto(path)
} else {
window.open(path, "_blank")
}
goto(path)
}
let mouseMoved: boolean = false
@@ -579,10 +572,6 @@
}
let runsSearch: RunsSearch
let runSearchRemainingCount: number | undefined = undefined
let runSearchTotalCount: number | undefined = undefined
let indexMetadata: SearchJobsIndexResponse["index_metadata"] = undefined
</script>
{#if open}
@@ -642,8 +631,8 @@
<div class={tab === 'switch-mode' ? 'p-2' : 'p-2 border-b'}>
{#each items as el}
<QuickMenuItem
onselect={(shift) => el?.action(shift)}
onhover={() => (selectedItem = el)}
on:select={el?.action}
on:hover={() => (selectedItem = el)}
id={el?.search_id}
hovered={el?.search_id === selectedItem?.search_id}
label={el?.label}
@@ -664,10 +653,8 @@
</div>
{#each (itemMap[tab] ?? []).filter((e) => (combinedItems ?? []).includes(e)) as el}
<QuickMenuItem
onselect={(shift) => {
gotoWindmillItemPage(el, shift)
}}
onhover={() => (selectedItem = el)}
on:select={() => gotoWindmillItemPage(el)}
on:hover={() => (selectedItem = el)}
id={el?.search_id}
hovered={el?.path === selectedItem?.path}
label={(el.summary ? `${el.summary} - ` : '') +
@@ -704,7 +691,7 @@
</Alert>
{:else}
<QuickMenuItem
onselect={() =>
on:select={() =>
gotoPage(
`/service_logs?query=${encodeURIComponent(removePrefix(searchTerm, '!'))}`
)}
@@ -728,9 +715,6 @@
bind:open
{selectItem}
searchTerm={removePrefix(searchTerm, RUNS_PREFIX)}
bind:runSearchRemainingCount
bind:runSearchTotalCount
bind:indexMetadata
/>
{/if}
</div>
@@ -1,9 +1,17 @@
<script lang="ts">
import { isMac } from '$lib/utils'
import { onDestroy, onMount } from 'svelte'
import { createEventDispatcher, onDestroy, onMount } from 'svelte'
import { twMerge } from 'tailwind-merge'
// const dispatch = createEventDispatcher()
export let hovered: boolean = false
export let id: string
export let label: string = ''
export let icon: any = undefined
export let shortcutKey: string | undefined = undefined
export let containerClass: string | undefined = undefined
export let mouseMoved = false
const dispatch = createEventDispatcher()
onMount(() => {
window.addEventListener('keydown', handleKeydown)
@@ -16,46 +24,16 @@
async function handleKeydown(event: KeyboardEvent) {
if (hovered && event.key === 'Enter') {
event.preventDefault()
if (onkeyboardSpecificSelect) {
onkeyboardSpecificSelect(event.shiftKey || event.ctrlKey)
} else {
onselect(event.shiftKey || event.ctrlKey)
}
dispatch('keyboardOnlySelect')
runAction()
}
}
interface Props {
hovered?: boolean
id: string
label?: string
icon?: any
shortcutKey?: string | undefined
containerClass?: string | undefined
mouseMoved?: boolean
kbdClass?: string
small?: boolean
itemReplacement?: import('svelte').Snippet
onselect?: (shift: boolean) => void
onkeyboardSpecificSelect?: (shift: boolean) => void
onhover?: () => void
function runAction() {
dispatch('select')
}
let {
hovered = false,
id,
label = '',
icon = undefined,
shortcutKey = undefined,
containerClass = undefined,
mouseMoved = $bindable(false),
kbdClass = $bindable(''),
small = true,
itemReplacement,
onselect = () => {},
onhover = () => {},
onkeyboardSpecificSelect
}: Props = $props()
export let kbdClass = ''
export let small = true
if (small) {
kbdClass = twMerge(
kbdClass,
@@ -66,36 +44,33 @@
} else {
kbdClass += ' !text-xs px-1.5'
}
</script>
<!-- svelte-ignore a11y_no_static_element_interactions -->
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
{id}
onclick={(e) => {
e.stopImmediatePropagation()
onselect(e.shiftKey || e.ctrlKey)
}}
onmouseenter={() => {
on:click|stopPropagation={runAction}
on:mouseenter={() => {
if (mouseMoved) {
onhover()
dispatch('hover')
}
mouseMoved = false
mouseMoved=false
}}
class={twMerge(
`rounded-md w-full transition-all cursor-pointer ${hovered ? 'bg-surface-hover' : ''}`,
`rounded-md w-full transition-all cursor-pointer ${
hovered ? 'bg-surface-hover' : ''
}`,
containerClass
)}
>
{#if itemReplacement}
{@render itemReplacement?.()}
{#if $$slots.itemReplacement}
<slot name="itemReplacement" />
{:else}
<div class="flex flex-row gap-2 items-center px-2 py-1.5 rounded-md pr-6 text-sm">
<div class="w-4">
{#if icon}
{@const SvelteComponent = icon}
<SvelteComponent size={16} />
<svelte:component this={icon} size={16} />
{:else if shortcutKey != undefined}
<div class="font-bold flex items-center justify-center w-full">
<span
@@ -16,22 +16,11 @@
let loadingCompletedRuns: boolean = $state(false)
let runSearchRemainingCount: number | undefined = $state(undefined)
let runSearchTotalCount: number | undefined = $state(undefined)
let indexMetadata: any = $state({})
let loadingMoreJobs: boolean = $state(false)
interface Props {
mouseMoved: boolean
selectedWorkspace: string | undefined
selectedItem: any
queryParseErrors: string[]
open: boolean
loadedRuns: any[]
selectItem: (idx: number) => any
searchTerm: string
runSearchRemainingCount: number | undefined
runSearchTotalCount: number | undefined
indexMetadata: SearchJobsIndexResponse['index_metadata']
}
let {
mouseMoved = $bindable(),
selectedWorkspace = $bindable(),
@@ -40,11 +29,17 @@
open = $bindable(),
loadedRuns = $bindable(),
selectItem,
searchTerm,
runSearchRemainingCount = $bindable(),
runSearchTotalCount = $bindable(),
indexMetadata = $bindable()
}: Props = $props()
searchTerm
} = $props<{
mouseMoved: boolean
selectedWorkspace: string | undefined
selectedItem: any
queryParseErrors: string[]
open: boolean
loadedRuns: any[] | undefined
selectItem: (idx: number) => any
searchTerm: string
}>()
export function handleRunSearch(s: string) {
clearTimeout(debounceTimeout)
@@ -63,7 +58,7 @@
return
}
loadedRuns = searchResults.hits ?? []
loadedRuns = searchResults.hits
runSearchTotalCount = searchResults.hit_count
runSearchRemainingCount = (searchResults.hit_count ?? 0) - loadedRuns?.length
queryParseErrors = searchResults.query_parse_errors ?? []
@@ -131,7 +126,7 @@
</div>
{:else}
<QuickMenuItem
onselect={() => {
on:select={() => {
selectedItem = r
selectedWorkspace = undefined
const paginationOffset = runSearchTotalCount! - runSearchRemainingCount!
@@ -142,7 +137,7 @@
containerClass="rounded-md px-2 py-1 my-2"
bind:mouseMoved
>
{#snippet itemReplacement()}
<svelte:fragment slot="itemReplacement">
<div
class="py-2 w-full flex flex-row items-center gap-4 transition-all text-secondary text-sm"
>
@@ -150,25 +145,18 @@
<!-- Load more ({runSearchRemainingCount} other) -->
<!-- {runSearchRemainingCount} more documents also matched -->
</div>
{/snippet}
</svelte:fragment>
</QuickMenuItem>
{/if}
{:else}
<QuickMenuItem
onselect={(shift) => {
on:select={() => {
selectedItem = r
selectedWorkspace = r?.document.workspace_id[0]
if (shift) {
window.open(`/run/${r?.document.id[0]}`, '_blank')
}
}}
onkeyboardSpecificSelect={(shift) => {
if (!shift) {
open = false
goto(`/run/${r?.document.id[0]}`)
} else {
window.open(`/run/${r?.document.id[0]}`, '_blank')
}
on:keyboardOnlySelect={() => {
open = false
goto(`/run/${r?.document.id[0]}`)
}}
id={r?.document.id[0]}
hovered={selectedItem && r?.search_id === selectedItem?.search_id}
@@ -176,7 +164,7 @@
containerClass="rounded-md px-2 py-1 my-2"
bind:mouseMoved
>
{#snippet itemReplacement()}
<svelte:fragment slot="itemReplacement">
<div class="w-full flex flex-row items-center gap-4 transition-all">
<div
class="rounded-full w-2 h-2 {r?.document.success[0]
@@ -199,7 +187,7 @@
</div>
</div>
</div>
{/snippet}
</svelte:fragment>
</QuickMenuItem>
{/if}
{/each}
@@ -214,7 +202,7 @@
</div>
{/if}
<div class="flex flex-row pt-3 pl-4 items-center text-xs text-secondary">
{#if indexMetadata?.indexed_until}
{#if indexMetadata.indexed_until}
<span class="px-2">
Most recently indexed job was created at <TimeAgo
agoOnlyIfRecent
@@ -222,7 +210,7 @@
/>
</span>
{/if}
{#if indexMetadata?.lost_lock_ownership}
{#if indexMetadata.lost_lock_ownership}
<Popover notClickable placement="top">
<AlertTriangle size={16} class="text-gray-500" />
<svelte:fragment slot="text">
@@ -255,7 +243,7 @@
{/if}
</div>
<div class="flex flex-row pt-10 text-xs text-secondary">
{#if indexMetadata?.indexed_until}
{#if indexMetadata.indexed_until}
<span class="px-2">
Most recently indexed job was created at <TimeAgo
agoOnlyIfRecent
@@ -263,7 +251,7 @@
/>
</span>
{/if}
{#if indexMetadata?.lost_lock_ownership}
{#if indexMetadata.lost_lock_ownership}
<Popover notClickable placement="top">
<AlertTriangle size={16} class="text-gray-500" />
<svelte:fragment slot="text">
+4
View File
@@ -169,6 +169,10 @@ export const copilotSessionModel = writable<AIProviderModel | undefined>(
)
export const usedTriggerKinds = writable<string[]>([])
export const triggerablesByAI = writable<
Record<string, { description: string; onTrigger: (id: string) => void }>
>({})
type SQLBaseSchema = {
[schemaKey: string]: {
[tableKey: string]: {
@@ -53,6 +53,7 @@
import { setContext } from 'svelte'
import { base } from '$app/paths'
import { Menubar } from '$lib/components/meltComponents'
import Input from '$lib/components/globalchat/Input.svelte'
OpenAPI.WITH_CREDENTIALS = true
let menuOpen = false
@@ -630,6 +631,7 @@
>
<main class="min-h-screen">
<div class="relative w-full h-full">
<Input />
<div
class={classNames(
'py-2 px-2 sm:px-4 md:px-8 flex justify-between items-center shadow-sm max-w-7xl mx-auto md:hidden',
@@ -23,6 +23,7 @@
import { setQuery } from '$lib/navigation'
import { page } from '$app/stores'
import { goto, replaceState } from '$app/navigation'
import TriggerableByAI from '$lib/components/TriggerableByAI.svelte'
type Tab = 'hub' | 'workspace'
@@ -48,6 +49,8 @@
const breakpoint = writable<EditorBreakpoint>('lg')
let createScriptComponent: { triggerClick: () => void } | undefined = undefined
async function viewCode(obj: HubItem) {
codeViewerContent = ''
codeViewerObj = undefined
@@ -227,7 +230,17 @@
<div class="flex flex-row gap-4 flex-wrap justify-end items-center">
{#if !$userStore?.operator}
<span class="text-sm text-secondary">Create a</span>
<CreateActionsScript />
<TriggerableByAI
id="create-script-button"
description="Creates a new script"
onTrigger={() => {
if (createScriptComponent) {
createScriptComponent.triggerClick()
}
}}
>
<CreateActionsScript bind:this={createScriptComponent} />
</TriggerableByAI>
{#if HOME_SHOW_CREATE_FLOW}<CreateActionsFlow />{/if}
{#if HOME_SHOW_CREATE_APP}<CreateActionsApp />{/if}
{/if}
+2 -2
View File
@@ -4,8 +4,8 @@ verify_ssl = true
name = "pypi"
[packages]
wmill = ">=1.492.1"
wmill_pg = ">=1.492.1"
wmill = ">=1.491.5"
wmill_pg = ">=1.491.5"
sendgrid = "*"
mysql-connector-python = "*"
pymongo = "*"
+1 -1
View File
@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.492.1
version: 1.491.5
title: OpenFlow Spec
contact:
name: Ruben Fiszel
@@ -12,7 +12,7 @@
RootModule = 'WindmillClient.psm1'
# Version number of this module.
ModuleVersion = '1.492.1'
ModuleVersion = '1.491.5'
# Supported PSEditions
# CompatiblePSEditions = @()
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "wmill"
version = "1.492.1"
version = "1.491.5"
description = "A client library for accessing Windmill server wrapping the Windmill client API"
license = "Apache-2.0"
homepage = "https://windmill.dev"
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "wmill-pg"
version = "1.492.1"
version = "1.491.5"
description = "An extension client for the wmill client library focused on pg"
license = "Apache-2.0"
homepage = "https://windmill.dev"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@windmill/windmill",
"version": "1.492.1",
"version": "1.491.5",
"exports": "./src/index.ts",
"publish": {
"exclude": ["!src", "./s3Types.ts", "./client.ts"]
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "windmill-client",
"description": "Windmill SDK client for browsers and Node.js",
"version": "1.492.1",
"version": "1.491.5",
"author": "Ruben Fiszel",
"license": "Apache 2.0",
"devDependencies": {
+1 -1
View File
@@ -1 +1 @@
1.492.1
1.491.5