feat: add configurable preview job tag override in default tags settings (#8649)

* feat: add configurable preview job tag override in default tags settings

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

* fix: skip re-tagging for FlowPreview jobs when preview override is active

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ruben Fiszel
2026-03-31 18:59:23 +00:00
committed by GitHub
parent 040a199685
commit da8886be85
9 changed files with 120 additions and 36 deletions

View File

@@ -51,7 +51,7 @@ use windmill_common::{
MONITOR_LOGS_ON_OBJECT_STORE_SETTING, NO_DEFAULT_MAVEN_SETTING,
NPM_CONFIG_REGISTRY_SETTING, NUGET_CONFIG_SETTING, OAUTH_SETTING, OTEL_SETTING,
OTEL_TRACING_PROXY_SETTING, PIP_INDEX_URL_SETTING, POWERSHELL_REPO_PAT_SETTING,
POWERSHELL_REPO_URL_SETTING, REQUEST_SIZE_LIMIT_SETTING,
POWERSHELL_REPO_URL_SETTING, PREVIEW_TAGS_OVERRIDE_SETTING, REQUEST_SIZE_LIMIT_SETTING,
REQUIRE_PREEXISTING_USER_FOR_OAUTH_SETTING, RESTART_COORDINATION_SETTING,
RETENTION_PERIOD_SECS_SETTING, RUBY_REPOS_SETTING, SAML_METADATA_SETTING,
SCIM_TOKEN_SETTING, SMTP_SETTING, TEAMS_SETTING, TIMEOUT_WAIT_RESULT_SETTING,
@@ -99,16 +99,17 @@ use windmill_worker::{
};
use crate::monitor::{
initial_load, load_keep_job_dir, load_metrics_debug_enabled, load_require_preexisting_user,
load_tag_per_workspace_enabled, load_tag_per_workspace_workspaces, monitor_db,
reload_app_workspaced_route_setting, reload_audit_log_retention_days_setting,
reload_base_url_setting, reload_bunfig_install_scopes_setting,
reload_critical_alert_mute_ui_setting, reload_critical_alerts_on_token_expiry_setting,
reload_critical_error_channels_setting, reload_extra_pip_index_url_setting,
reload_http_route_workspaced_route_setting, reload_hub_api_secret_setting,
reload_hub_base_url_setting, reload_instance_events_webhook_setting,
reload_job_default_timeout_setting, reload_job_isolation_setting, reload_jwt_secret_setting,
reload_license_key, reload_npm_config_registry_setting, reload_otel_tracing_proxy_setting,
initial_load, load_keep_job_dir, load_metrics_debug_enabled, load_preview_tags_override,
load_require_preexisting_user, load_tag_per_workspace_enabled,
load_tag_per_workspace_workspaces, monitor_db, reload_app_workspaced_route_setting,
reload_audit_log_retention_days_setting, reload_base_url_setting,
reload_bunfig_install_scopes_setting, reload_critical_alert_mute_ui_setting,
reload_critical_alerts_on_token_expiry_setting, reload_critical_error_channels_setting,
reload_extra_pip_index_url_setting, reload_http_route_workspaced_route_setting,
reload_hub_api_secret_setting, reload_hub_base_url_setting,
reload_instance_events_webhook_setting, reload_job_default_timeout_setting,
reload_job_isolation_setting, reload_jwt_secret_setting, reload_license_key,
reload_npm_config_registry_setting, reload_otel_tracing_proxy_setting,
reload_pip_index_url_setting, reload_retention_period_setting, reload_scim_token_setting,
reload_smtp_config, reload_uv_index_strategy_setting, reload_worker_config, MonitorIteration,
};
@@ -1742,6 +1743,11 @@ async fn process_notify_event(
);
}
}
PREVIEW_TAGS_OVERRIDE_SETTING => {
if let Err(e) = load_preview_tags_override(db).await {
tracing::error!("Error loading preview tags override: {e:#}");
}
}
SMTP_SETTING => {
reload_smtp_config(db).await;
}

View File

@@ -62,7 +62,7 @@ use windmill_common::{
KEEP_JOB_DIR_SETTING, LICENSE_KEY_SETTING, MONITOR_LOGS_ON_OBJECT_STORE_SETTING,
NPMRC_SETTING, NPM_CONFIG_REGISTRY_SETTING, NUGET_CONFIG_SETTING, OTEL_SETTING,
OTEL_TRACING_PROXY_SETTING, PIP_INDEX_URL_SETTING, POWERSHELL_REPO_PAT_SETTING,
POWERSHELL_REPO_URL_SETTING, REQUEST_SIZE_LIMIT_SETTING,
POWERSHELL_REPO_URL_SETTING, PREVIEW_TAGS_OVERRIDE_SETTING, REQUEST_SIZE_LIMIT_SETTING,
REQUIRE_PREEXISTING_USER_FOR_OAUTH_SETTING, RETENTION_PERIOD_SECS_SETTING,
SAML_METADATA_SETTING, SCIM_TOKEN_SETTING, TIMEOUT_WAIT_RESULT_SETTING,
UV_INDEX_STRATEGY_SETTING,
@@ -79,8 +79,8 @@ use windmill_common::{
load_periodic_bash_script_interval_from_env, load_whitelist_env_vars_from_env,
load_worker_config, reload_custom_tags_setting, store_pull_query,
store_suspended_pull_query, Connection, WorkerConfig, DEFAULT_TAGS_PER_WORKSPACE,
DEFAULT_TAGS_WORKSPACES, INDEXER_CONFIG, SCRIPT_TOKEN_EXPIRY, SMTP_CONFIG, WINDMILL_DIR,
WORKER_CONFIG, WORKER_GROUP,
DEFAULT_TAGS_WORKSPACES, INDEXER_CONFIG, PREVIEW_TAGS_OVERRIDE, SCRIPT_TOKEN_EXPIRY,
SMTP_CONFIG, WINDMILL_DIR, WORKER_CONFIG, WORKER_GROUP,
},
KillpillSender, AUDIT_LOG_RETENTION_DAYS, BASE_URL, CRITICAL_ALERTS_ON_DB_OVERSIZE,
CRITICAL_ALERTS_ON_TOKEN_EXPIRY, CRITICAL_ALERT_MUTE_UI_ENABLED, CRITICAL_ERROR_CHANNELS, DB,
@@ -235,6 +235,10 @@ pub async fn initial_load(
if let Err(e) = load_tag_per_workspace_workspaces(db).await {
tracing::error!("Error loading default tag per workpsace workspaces: {e:#}");
}
if let Err(e) = load_preview_tags_override(db).await {
tracing::error!("Error loading preview tags override: {e:#}");
}
}
if server_mode {
@@ -499,6 +503,16 @@ pub async fn load_tag_per_workspace_workspaces(db: &DB) -> error::Result<()> {
Ok(())
}
pub async fn load_preview_tags_override(db: &DB) -> error::Result<()> {
let value = load_value_from_global_settings(db, PREVIEW_TAGS_OVERRIDE_SETTING).await;
match value {
Ok(Some(serde_json::Value::Bool(t))) => PREVIEW_TAGS_OVERRIDE.store(t, Ordering::Relaxed),
_ => (),
};
Ok(())
}
pub async fn reload_critical_alert_mute_ui_setting(conn: &Connection) -> error::Result<()> {
if let Ok(Some(serde_json::Value::Bool(t))) =
load_value_from_global_settings_with_conn(conn, CRITICAL_ALERT_MUTE_UI_SETTING, true).await

View File

@@ -1,6 +1,7 @@
pub const CUSTOM_TAGS_SETTING: &str = "custom_tags";
pub const DEFAULT_TAGS_PER_WORKSPACE_SETTING: &str = "default_tags_per_workspace";
pub const DEFAULT_TAGS_WORKSPACES_SETTING: &str = "default_tags_workspaces";
pub const PREVIEW_TAGS_OVERRIDE_SETTING: &str = "preview_tags_override";
pub const BASE_URL_SETTING: &str = "base_url";
pub const WS_BASE_URL_SETTING: &str = "ws_base_url";
pub const OAUTH_SETTING: &str = "oauths";

View File

@@ -243,6 +243,8 @@ pub struct GlobalSettings {
#[serde(skip_serializing_if = "Option::is_none")]
pub default_tags_per_workspace: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub preview_tags_override: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub disable_hub: Option<bool>,
// String settings

View File

@@ -205,6 +205,7 @@ lazy_static::lazy_static! {
pub static ref DEFAULT_TAGS_PER_WORKSPACE: AtomicBool = AtomicBool::new(false);
pub static ref DEFAULT_TAGS_WORKSPACES: Arc<RwLock<Option<Vec<String>>>> = Arc::new(RwLock::new(None));
pub static ref PREVIEW_TAGS_OVERRIDE: AtomicBool = AtomicBool::new(false);
pub static ref MAX_TIMEOUT: u64 = std::env::var("TIMEOUT")
.ok()

View File

@@ -77,8 +77,8 @@ use windmill_common::{
users::{SUPERADMIN_NOTIFICATION_EMAIL, SUPERADMIN_SECRET_EMAIL},
utils::{not_found_if_none, report_critical_error, StripPath, WarnAfterExt},
worker::{
to_raw_value, CLOUD_HOSTED, DISABLE_FLOW_SCRIPT, NO_LOGS, WORKER_PULL_QUERIES,
WORKER_SUSPENDED_PULL_QUERY,
to_raw_value, CLOUD_HOSTED, DISABLE_FLOW_SCRIPT, NO_LOGS, PREVIEW_TAGS_OVERRIDE,
WORKER_PULL_QUERIES, WORKER_SUSPENDED_PULL_QUERY,
},
DB, METRICS_ENABLED,
};
@@ -3302,7 +3302,10 @@ pub async fn pull(
};
if let Some(job) = job.as_ref() {
if job.is_flow() || job.is_dependency() {
if (job.is_flow() || job.is_dependency())
&& !(job.kind.is_preview()
&& PREVIEW_TAGS_OVERRIDE.load(std::sync::atomic::Ordering::Relaxed))
{
let per_workspace = per_workspace_tag(&job.workspace_id).await;
let base_tag = if job.is_flow() {
"flow".to_string()
@@ -5493,25 +5496,35 @@ async fn push_inner<'c, 'd>(
};
interpolated_tag.unwrap_or_else(|| {
language
.as_ref()
.map(|x| {
let tag_lang = if x == &ScriptLang::Bunnative {
if job_kind == JobKind::Dependencies {
ScriptLang::Bun.as_str()
if job_kind.is_preview()
&& PREVIEW_TAGS_OVERRIDE.load(std::sync::atomic::Ordering::Relaxed)
{
if per_workspace {
format!("preview-{}", workspace_id)
} else {
"preview".to_string()
}
} else {
language
.as_ref()
.map(|x| {
let tag_lang = if x == &ScriptLang::Bunnative {
if job_kind == JobKind::Dependencies {
ScriptLang::Bun.as_str()
} else {
ScriptLang::Nativets.as_str()
}
} else {
ScriptLang::Nativets.as_str()
x.as_str()
};
if per_workspace {
format!("{}-{}", tag_lang, workspace_id)
} else {
tag_lang.to_string()
}
} else {
x.as_str()
};
if per_workspace {
format!("{}-{}", tag_lang, workspace_id)
} else {
tag_lang.to_string()
}
})
.unwrap_or_else(default)
})
.unwrap_or_else(default)
}
})
};

View File

@@ -118,6 +118,10 @@ impl JobKind {
JobKind::FlowDependencies | JobKind::AppDependencies | JobKind::Dependencies
)
}
pub fn is_preview(&self) -> bool {
matches!(self, JobKind::Preview | JobKind::FlowPreview)
}
}
#[derive(sqlx::FromRow, Debug, Serialize, Clone)]

View File

@@ -4,7 +4,11 @@
import { SettingService, WorkerService, WorkspaceService } from '$lib/gen'
import { sendUserToast } from '$lib/toast'
import { enterpriseLicense, superadmin } from '$lib/stores'
import { DEFAULT_TAGS_PER_WORKSPACE_SETTING, DEFAULT_TAGS_WORKSPACES_SETTING } from '$lib/consts'
import {
DEFAULT_TAGS_PER_WORKSPACE_SETTING,
DEFAULT_TAGS_WORKSPACES_SETTING,
PREVIEW_TAGS_OVERRIDE_SETTING
} from '$lib/consts'
import Toggle from './Toggle.svelte'
import MultiSelect from './select/MultiSelect.svelte'
import { safeSelectItems } from './select/utils.svelte'
@@ -22,16 +26,19 @@
let defaultTags = $state<string[] | undefined>(undefined)
let limitToWorkspaces = $state(false)
let previewTagsOverride = $state(false)
// Change detection
let originalDefaultTagPerWorkspace = $state<boolean | undefined>(defaultTagPerWorkspace)
let originalDefaultTagWorkspaces = $state<string[]>(defaultTagWorkspaces)
let originalPreviewTagsOverride = $state(false)
// Detect changes
let hasChanges = $derived(
originalDefaultTagPerWorkspace !== defaultTagPerWorkspace ||
JSON.stringify($state.snapshot(originalDefaultTagWorkspaces)?.sort() || []) !==
JSON.stringify($state.snapshot(defaultTagWorkspaces)?.sort() || [])
JSON.stringify($state.snapshot(defaultTagWorkspaces)?.sort() || []) ||
originalPreviewTagsOverride !== previewTagsOverride
)
let workspaces: string[] = $state([])
@@ -47,6 +54,11 @@
key: DEFAULT_TAGS_WORKSPACES_SETTING
})) as any) ?? []
limitToWorkspaces = defaultTagWorkspaces ? defaultTagWorkspaces.length > 0 : false
previewTagsOverride =
((await SettingService.getGlobal({
key: PREVIEW_TAGS_OVERRIDE_SETTING
})) as any) ?? false
originalPreviewTagsOverride = previewTagsOverride
} catch (err) {
sendUserToast(`Could not load default tags: ${err}`, true)
}
@@ -68,10 +80,17 @@
: undefined
}
})
await SettingService.setGlobal({
key: PREVIEW_TAGS_OVERRIDE_SETTING,
requestBody: {
value: previewTagsOverride
}
})
// Update original state after save
originalDefaultTagPerWorkspace = defaultTagPerWorkspace
originalDefaultTagWorkspaces = [...(defaultTagWorkspaces || [])]
originalPreviewTagsOverride = previewTagsOverride
loadDefaultTags()
sendUserToast('Saved')
@@ -146,6 +165,18 @@
/>
{/if}
{/if}
<div class="flex flex-col gap-1">
<Toggle
bind:checked={previewTagsOverride}
options={{
right: 'route preview jobs to dedicated preview tag',
rightTooltip:
'When enabled, preview jobs (script previews and flow previews) will be routed to the "preview" tag instead of their language-specific tag, allowing you to dedicate specific workers for previews.'
}}
class="w-fit"
disabled={!$enterpriseLicense}
/>
</div>
</div>
<div class="flex gap-2 items-center mb-1">
@@ -168,6 +199,17 @@
</div>
</div>
{/each}
{#if previewTagsOverride}
<div class="flex gap-2 items-center">
<div class="w-36">
<Badge color="transparent">preview</Badge>
</div>
<div class="w-6 flex justify-center text-secondary">&rightarrow;</div>
<div class="flex-1">
<Badge color="blue">{defaultTagPerWorkspace ? 'preview-$workspace' : 'preview'}</Badge>
</div>
</div>
{/if}
</div>
{/if}
</Section>

View File

@@ -41,6 +41,7 @@ export const WORKER_S3_BUCKET_SYNC_SETTING = 'worker_s3_bucket_sync'
export const CUSTOM_TAGS_SETTING = 'custom_tags'
export const DEFAULT_TAGS_PER_WORKSPACE_SETTING = 'default_tags_per_workspace'
export const DEFAULT_TAGS_WORKSPACES_SETTING = 'default_tags_workspaces'
export const PREVIEW_TAGS_OVERRIDE_SETTING = 'preview_tags_override'
export const WORKSPACE_SLACK_BOT_TOKEN_PATH = 'f/slack_bot/bot_token'