Files
windmill/.github/workflows/backend-test.yml
T
9a1c6e5081 feat: let a workspace withdraw operator schedule and trigger writes (#11226)
* feat: let a workspace withdraw operator schedule and trigger writes

Operators can create, edit and delete schedules and triggers today through the
API, CLI and MCP, while the operator_settings flags beside them only hide those
pages. An admin who wants operators to see what is scheduled without letting
them change it cannot express that. Add manage_schedules and manage_triggers as
enforced settings, gated at the schedule handlers and at the generic TriggerCrud
routes so every trigger kind is covered by one check.

They name capabilities operators already hold, so they are granted unless
withdrawn, and absence has to mean "never configured" rather than a value. The
read coalesces to true; the update endpoint merges into the stored jsonb with
the two fields as Option<bool>, so an omitted key keeps what is stored.
operator_settings is git-synced as a whole object, so a settings file written
before these keys existed reaches the endpoint on every pull, and a serde or SQL
default of either polarity would turn that pull into a silent withdrawal or
restoration.

The rights are read through a per-process cache, so withdrawing one publishes a
notify_event that drops the entry on every replica.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dsf6VC4MVLisiEoeQkgbr4

* feat: enforce operator write rights on the router and in the UI

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

* fix: close the capture gap and gate the trigger editors' write actions

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

* fix: gate acl writes and the native trigger drawer behind manage rights

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

* fix: refuse operator writes with 403 and gate sharing at the drawer

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

* perf: resolve identity in the operator write gate only for writes

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

* fix: gate the suspended-jobs actions and stop the route check refusing reads

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

* fix: explain the empty-state create button when operator writes are withdrawn

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

* fix: audit operator settings changes and fold path writes into native rows

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

* fix: open locked editors read-only and group the operator settings

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

* fix: skip email and azure lookups on editor open while triggers are locked

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

* docs: state each operator-rights rationale once in comments

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

* fix: address CI review findings on operator write rights

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

* fix: keep capture move gated and skip it in the builders while locked

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

* fix: keep admin and operator exclusive when setting a workspace role

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

* refactor: use the shared section component for operator settings groups

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2026-09-25 17:34:58 +00:00

299 lines
13 KiB
YAML

name: Backend only integration tests
on:
workflow_dispatch:
push:
branches:
- "main"
paths:
- "backend/**"
- ".github/workflows/backend-test.yml"
pull_request:
types: [opened, synchronize, reopened]
paths:
- "backend/**"
- ".github/workflows/backend-test.yml"
defaults:
run:
working-directory: ./backend
jobs:
cargo_test:
runs-on: ubicloud-standard-16
services:
postgres:
image: postgres
ports:
- 5432:5432
env:
POSTGRES_DB: windmill
POSTGRES_PASSWORD: changeme
POSTGRES_INITDB_ARGS: "-c max_connections=500"
options: >-
--health-cmd pg_isready --health-interval 10s --health-timeout 5s
--health-retries 5 --shm-size=256mb
mysql:
image: mysql:8.0
ports:
- 3306:3306
env:
MYSQL_ROOT_PASSWORD: changeme
MYSQL_DATABASE: windmill_test
options: >-
--health-cmd "mysqladmin ping -h localhost" --health-interval 10s
--health-timeout 5s --health-retries 5
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: "9.0.x"
- uses: denoland/setup-deno@v2
with:
# Pin to the Deno version shipped in the runtime image (Dockerfile) so CI
# tests what production runs, instead of floating on the latest v2.x.
deno-version: 2.2.1
- uses: actions/setup-go@v7
with:
go-version: 1.21.5
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.4.0
- uses: actions/setup-node@v7
with:
node-version: "24"
- uses: astral-sh/setup-uv@v7
with:
version: "0.11.24"
- uses: shivammathur/setup-php@v2
with:
php-version: "8.3"
tools: composer
- uses: ruby/setup-ruby@v1
with:
ruby-version: "3.3"
bundler-cache: false
- name: Install windmill CLI from source
run: |
cd $GITHUB_WORKSPACE/cli
bash gen_wm_client.sh
bun install
mkdir -p "$HOME/.local/bin"
printf '#!/bin/sh\nexec bun run "%s/cli/src/main.ts" "$@"\n' "$GITHUB_WORKSPACE" > "$HOME/.local/bin/wmill"
chmod +x "$HOME/.local/bin/wmill"
echo "$HOME/.local/bin" >> $GITHUB_PATH
working-directory: /
- name: Install PowerShell, mold and clang
run: |
sudo apt-get update && sudo apt-get install -y powershell mold clang libcurl4-openssl-dev
working-directory: /
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
cache-workspaces: backend
toolchain: 1.97.0
- name: Fix stale v8 build cache
working-directory: ./backend
run: |
# Cargo cache may preserve v8 build fingerprints without the actual
# librusty_v8.a library. Since fingerprints look valid, cargo skips
# build.rs re-run, causing "could not find native static library rusty_v8".
for profile in debug release; do
if [ -d "target/$profile/.fingerprint" ] && [ ! -f "target/$profile/gn_out/obj/librusty_v8.a" ]; then
echo "Cleaning stale v8 build artifacts in target/$profile"
rm -rf "target/$profile/build/v8-"* "target/$profile/.fingerprint/v8-"*
fi
done
- name: Read EE repo commit hash
run: |
echo "ee_repo_ref=$(cat ./ee-repo-ref.txt)" >> "$GITHUB_ENV"
- uses: actions/checkout@v4
with:
repository: windmill-labs/windmill-ee-private
path: ./windmill-ee-private
ref: ${{ env.ee_repo_ref }}
token: ${{ secrets.WINDMILL_EE_PRIVATE_ACCESS }}
fetch-depth: 0
- name: Substitute EE code (EE logic is behind feature flag)
run: |
./substitute_ee_code.sh --copy --dir ./windmill-ee-private
- name: Setup private npm registry with test package
working-directory: /tmp
run: |
set -e
# Install Verdaccio globally
npm install -g verdaccio
# Create Verdaccio config that requires authentication for @windmill-test packages
mkdir -p /tmp/verdaccio/storage
cat > /tmp/verdaccio/config.yaml << 'VERDACCIO_CONFIG'
storage: /tmp/verdaccio/storage
auth:
htpasswd:
file: /tmp/verdaccio/htpasswd
max_users: 100
uplinks:
npmjs:
url: https://registry.npmjs.org/
packages:
'@windmill-test/*':
access: $authenticated
publish: $authenticated
'@*/*':
access: $all
publish: $authenticated
proxy: npmjs
'**':
access: $all
publish: $authenticated
proxy: npmjs
server:
keepAliveTimeout: 60
middlewares:
audit:
enabled: true
log: { type: stdout, format: pretty, level: warn }
VERDACCIO_CONFIG
# Create empty htpasswd file (users will be created via API)
touch /tmp/verdaccio/htpasswd
# Start Verdaccio in background
verdaccio --config /tmp/verdaccio/config.yaml &
VERDACCIO_PID=$!
# Wait for Verdaccio to be ready
echo "Waiting for Verdaccio to start..."
for i in {1..30}; do
if curl -s http://localhost:4873/-/ping > /dev/null 2>&1; then
echo "Verdaccio is ready"
break
fi
sleep 1
done
# Login to get a token
echo "Getting auth token..."
RESPONSE=$(curl -s -X PUT \
-H "Content-Type: application/json" \
-d '{"name":"testuser","password":"testpass123"}' \
http://localhost:4873/-/user/org.couchdb.user:testuser)
echo "Auth response: $RESPONSE"
NPM_TOKEN=$(echo "$RESPONSE" | jq -r '.token')
if [ -z "$NPM_TOKEN" ] || [ "$NPM_TOKEN" = "null" ]; then
echo "Failed to get NPM token from response"
exit 1
fi
echo "NPM_TOKEN=${NPM_TOKEN}" >> $GITHUB_ENV
{
echo "TEST_NPMRC<<NPMRC_EOF"
echo "@windmill-test:registry=http://localhost:4873/"
echo "//localhost:4873/:_authToken=${NPM_TOKEN}"
echo "NPMRC_EOF"
} >> $GITHUB_ENV
echo "Got NPM token successfully: ${NPM_TOKEN:0:10}..."
# Configure npm globally with the auth token
echo "//localhost:4873/:_authToken=${NPM_TOKEN}" > ~/.npmrc
echo "Configured ~/.npmrc with auth token"
# Create a simple test package
mkdir -p /tmp/windmill-test-private-pkg
cat > /tmp/windmill-test-private-pkg/package.json << 'PKG_JSON'
{
"name": "@windmill-test/private-pkg",
"version": "1.0.0",
"main": "index.js"
}
PKG_JSON
cat > /tmp/windmill-test-private-pkg/index.js << 'PKG_JS'
module.exports.greet = (name) => `Hello from private package, ${name}!`;
PKG_JS
# Publish to Verdaccio with auth
cd /tmp/windmill-test-private-pkg
echo "Publishing package..."
npm publish --registry http://localhost:4873
echo "Package published successfully"
# Verify the package requires auth by trying anonymous access (should fail)
rm -f ~/.npmrc
echo "Testing anonymous access (should fail)..."
if npm view @windmill-test/private-pkg --registry http://localhost:4873 2>/dev/null; then
echo "ERROR: Package should require authentication but anonymous access worked"
exit 1
fi
echo "Verified: Package requires authentication for @windmill-test/private-pkg"
- name: Cache DuckDB FFI module build
uses: actions/cache@v3
with:
path: ./backend/windmill-duckdb-ffi-internal/target
key: ${{ runner.os }}-duckdb-ffi-${{ hashFiles('./backend/windmill-duckdb-ffi-internal/src/**/*.rs', './backend/windmill-duckdb-ffi-internal/Cargo.toml', './backend/windmill-duckdb-ffi-internal/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-duckdb-ffi-
- name: cargo test
timeout-minutes: 30
env:
# setup-rust-toolchain exports RUSTFLAGS=-D warnings, and the RUSTFLAGS env
# var fully REPLACES (never merges with) target.*.rustflags in
# backend/.cargo/config.toml. That silently drops the config's
# `-C link-arg=-fuse-ld=mold`, so CI links the many large integration-test
# binaries (v8 + duckdb + every language runtime, statically linked) with the
# default bfd linker. Its peak memory across ~12 parallel links OOM-kills the
# runner mid-link (SIGTERM => exit 143, before any test runs). Re-add the mold
# link arg here so CI links with mold like local dev, keeping -D warnings.
# (config.toml's `linker = "clang"` still applies; env only overrides rustflags.)
RUSTFLAGS: "-D warnings -C link-arg=-fuse-ld=mold"
SQLX_OFFLINE: true
DATABASE_URL: postgres://postgres:changeme@localhost:5432/windmill
DISABLE_EMBEDDING: true
RUST_LOG: "off"
RUST_LOG_STYLE: never
CARGO_NET_GIT_FETCH_WITH_CLI: true
# Cap parallel rustc/link jobs below the 16 available cores. The tail of
# the build links ~128 full-graph test binaries (one per tests/*.rs file
# across the workspace); at high parallelism enough heavy codegen+link
# units (rustc ~2.6GB, mold ~1GB each) overlap to exhaust the 64GB
# runner. Matches backend-test-windows.yml, which already uses 8.
CARGO_BUILD_JOBS: 8
# Incremental compilation is per-run dead weight in CI: rust-cache
# (cache-workspaces above) restores compiled dependency artifacts but
# never persists target/**/incremental, so there is no prior state to
# reuse in a one-shot `cargo test`. It only adds per-crate memory
# overhead and extra disk. Off here (kept on for local dev via
# .cargo/config.toml). Matches backend-test-windows.yml.
CARGO_INCREMENTAL: "0"
# backend/Cargo.toml keeps line tables on profile.dev for the (large)
# windmill workspace crates; that debug info is emitted into every
# object file and embedded in each test binary. Across the full
# --all --features build it drives the memory/disk peak when mold
# links the windmill-api-integration-tests binary, tipping the runner
# over (lost runner reported as a canceled step). CI reads no
# backtraces, so drop it entirely for the dev/test profiles here.
# (test profile inherits dev, but the workspace crates link in as
# dev-profile deps, so both must be set.) CI-only; local dev builds
# are unaffected.
CARGO_PROFILE_DEV_DEBUG: "0"
CARGO_PROFILE_TEST_DEBUG: "0"
# Tests' poll-time stack frames (deep nested async fn chains in
# debug builds) reach ~1.8MB, leaving very thin headroom on the
# default 2MB thread stack. 4MB gives ~2x buffer against flaky
# overflows under parallel-test contention.
RUST_MIN_STACK: 4194304
WMDEBUG_FORCE_V0_WORKSPACE_DEPENDENCIES: 1
WMDEBUG_FORCE_RUNNABLE_SETTINGS_V0: 1
WMDEBUG_FORCE_NO_LEGACY_DEBOUNCING_COMPAT: 1
TEST_NPM_REGISTRY: "http://localhost:4873/:_authToken=${{ env.NPM_TOKEN }}"
run: |
deno --version && bun -v && node --version && go version && python3 --version && php --version && ruby --version && pwsh --version && dotnet --version
# The FFI crate is excluded from the workspace, so the `cargo test` below
# never reaches it. Pin the target dir (matching the cache step above) so
# its own tests run off this compile rather than a second bundled build.
(cd windmill-duckdb-ffi-internal && export CARGO_TARGET_DIR="$PWD/target" && ./build_dev.sh && cargo test --release -p windmill_duckdb_ffi_internal)
DENO_PATH=$(which deno) BUN_PATH=$(which bun) NODE_BIN_PATH=$(which node) GO_PATH=$(which go) UV_PATH=$(which uv) PHP_PATH=$(which php) COMPOSER_PATH=$(which composer) RUBY_PATH=$(which ruby) RUBY_BUNDLE_PATH=$(which bundle) RUBY_GEM_PATH=$(which gem) POWERSHELL_PATH=$(which pwsh) DOTNET_PATH=$(which dotnet) cargo test --features enterprise,deno_core,duckdb,license,python,rust,scoped_cache,parquet,private,private_registry_test,csharp,php,ruby,mysql,quickjs,mcp,run_inline,smtp,http_trigger --all -- --nocapture --test-threads=10