mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-20 00:02:19 +00:00
cli: git sync improvements v2 (#6220)
* log when override is applied vs default taken in git sync * simplify cli merging options + add explicit override test * gitsync-settings pull/push ask for confirmation or --yes if tty * cli legacy backend repo setting detected + interactive migration
This commit is contained in:
+20
-6
@@ -108,31 +108,45 @@ export function getEffectiveSettings(
|
||||
): SyncOptions {
|
||||
// Start with empty object - no defaults
|
||||
let effective = {} as SyncOptions;
|
||||
|
||||
|
||||
// Merge top-level settings from config (which contains user's chosen defaults)
|
||||
Object.keys(config).forEach(key => {
|
||||
if (key !== 'overrides' && config[key as keyof SyncOptions] !== undefined) {
|
||||
(effective as any)[key] = config[key as keyof SyncOptions];
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
if (!config.overrides) {
|
||||
if (repo) {
|
||||
log.info(`No overrides found in wmill.yaml, using top-level settings (repository flag ignored)`);
|
||||
}
|
||||
return effective;
|
||||
}
|
||||
|
||||
|
||||
// Construct override keys using the single format
|
||||
const workspaceKey = `${baseUrl}:${workspaceId}:*`;
|
||||
const repoKey = `${baseUrl}:${workspaceId}:${repo}`;
|
||||
|
||||
|
||||
let appliedOverrides: string[] = [];
|
||||
|
||||
// Apply workspace-level overrides
|
||||
if (config.overrides[workspaceKey]) {
|
||||
Object.assign(effective, config.overrides[workspaceKey]);
|
||||
appliedOverrides.push("workspace-level");
|
||||
}
|
||||
|
||||
|
||||
// Apply repository-specific overrides (overrides workspace-level)
|
||||
if (config.overrides[repoKey]) {
|
||||
Object.assign(effective, config.overrides[repoKey]);
|
||||
appliedOverrides.push("repository-specific");
|
||||
} else if (repo) {
|
||||
// Repository was specified but no override found
|
||||
log.info(`Repository override not found for "${repo}", using ${appliedOverrides.length > 0 ? appliedOverrides.join(" + ") : "top-level"} settings`);
|
||||
}
|
||||
|
||||
|
||||
if (appliedOverrides.length > 0) {
|
||||
log.info(`Applied ${appliedOverrides.join(" + ")} overrides${repo ? ` for repository "${repo}"` : ""}`);
|
||||
}
|
||||
|
||||
return effective;
|
||||
}
|
||||
|
||||
+282
-54
@@ -1,4 +1,4 @@
|
||||
import { colors, Command, log, yamlStringify } from "./deps.ts";
|
||||
import { colors, Command, Confirm, log, yamlStringify } from "./deps.ts";
|
||||
import { GlobalOptions } from "./types.ts";
|
||||
import { requireLogin } from "./auth.ts";
|
||||
import { resolveWorkspace } from "./context.ts";
|
||||
@@ -164,6 +164,147 @@ function includeTypeToSyncOptions(
|
||||
};
|
||||
}
|
||||
|
||||
// Shared migration function for legacy repositories
|
||||
async function handleLegacyRepositoryMigration(
|
||||
selectedRepo: any,
|
||||
gitSyncSettings: any,
|
||||
workspace: any,
|
||||
opts: { yes?: boolean },
|
||||
operationName: string = "operation"
|
||||
): Promise<any> {
|
||||
if (selectedRepo.settings) {
|
||||
return selectedRepo; // Already migrated
|
||||
}
|
||||
|
||||
// This repository is in legacy format - handle migration
|
||||
if (!gitSyncSettings.include_path || !gitSyncSettings.include_type) {
|
||||
throw new Error(
|
||||
`Repository "${selectedRepo.git_repo_resource_path}" has legacy format but workspace-level include_path or include_type is missing. This indicates corrupted git-sync settings.`
|
||||
);
|
||||
}
|
||||
|
||||
const workspaceIncludePath = gitSyncSettings.include_path;
|
||||
const workspaceIncludeType = gitSyncSettings.include_type;
|
||||
|
||||
if (Deno.stdout.isTerminal() && !opts.yes) {
|
||||
// Interactive mode - show migration prompt
|
||||
console.log(colors.yellow('\n⚠️ Legacy git-sync settings detected!'));
|
||||
console.log(`\nRepository "${selectedRepo.git_repo_resource_path}" has legacy settings format.`);
|
||||
console.log('The new format allows per-repository filter configuration.');
|
||||
if (operationName === "push") {
|
||||
console.log('This repository must be migrated before pushing settings.\n');
|
||||
} else {
|
||||
console.log('\n');
|
||||
}
|
||||
|
||||
console.log(colors.bold('Current workspace-level settings:'));
|
||||
console.log(` Include paths: ${workspaceIncludePath.join(', ')}`);
|
||||
console.log(` Include types: ${workspaceIncludeType.join(', ')}\n`);
|
||||
|
||||
// Show what the migration will do
|
||||
let finalIncludeType = [...workspaceIncludeType];
|
||||
if (selectedRepo.exclude_types_override && selectedRepo.exclude_types_override.length > 0) {
|
||||
const originalCount = finalIncludeType.length;
|
||||
finalIncludeType = finalIncludeType.filter(
|
||||
type => !selectedRepo.exclude_types_override.includes(type)
|
||||
);
|
||||
const excludedCount = originalCount - finalIncludeType.length;
|
||||
console.log(colors.yellow(`Repository excludes ${excludedCount} types: ${selectedRepo.exclude_types_override.join(', ')}`));
|
||||
}
|
||||
|
||||
console.log(colors.bold('\nAfter migration, repository will have:'));
|
||||
console.log(` Include paths: ${workspaceIncludePath.join(', ')}`);
|
||||
console.log(` Include types: ${finalIncludeType.join(', ')}\n`);
|
||||
|
||||
const confirm = await Confirm.prompt({
|
||||
message: operationName === "push"
|
||||
? 'Do you want to migrate this repository before pushing?'
|
||||
: 'Do you want to migrate this repository?',
|
||||
default: true
|
||||
});
|
||||
|
||||
if (!confirm) {
|
||||
const message = operationName === "push"
|
||||
? '\n⚠️ Migration skipped. Cannot push to legacy repository.'
|
||||
: '\n⚠️ Migration skipped. You can migrate later via the UI.';
|
||||
console.log(colors.yellow(message));
|
||||
if (operationName === "push") {
|
||||
return null; // Signal to exit push operation
|
||||
}
|
||||
throw new Error('Migration cancelled by user');
|
||||
}
|
||||
|
||||
// Perform the migration
|
||||
let migratedIncludeType = [...workspaceIncludeType];
|
||||
if (selectedRepo.exclude_types_override && selectedRepo.exclude_types_override.length > 0) {
|
||||
migratedIncludeType = migratedIncludeType.filter(
|
||||
type => !selectedRepo.exclude_types_override.includes(type)
|
||||
);
|
||||
}
|
||||
|
||||
const migratedRepo = {
|
||||
...selectedRepo,
|
||||
settings: {
|
||||
include_path: [...workspaceIncludePath],
|
||||
include_type: migratedIncludeType,
|
||||
exclude_path: [],
|
||||
extra_include_path: []
|
||||
}
|
||||
};
|
||||
|
||||
// Remove the old field
|
||||
delete migratedRepo.exclude_types_override;
|
||||
|
||||
// Update the backend with migrated repository
|
||||
const updatedRepositories = gitSyncSettings.repositories.map((repo: any) => {
|
||||
if (repo.git_repo_resource_path === selectedRepo.git_repo_resource_path) {
|
||||
return migratedRepo;
|
||||
}
|
||||
return repo;
|
||||
});
|
||||
|
||||
await wmill.editWorkspaceGitSyncConfig({
|
||||
workspace: workspace.workspaceId,
|
||||
requestBody: {
|
||||
git_sync_settings: {
|
||||
repositories: updatedRepositories,
|
||||
// Keep workspace-level settings if other repos are still legacy
|
||||
...(gitSyncSettings.repositories.some((r: any) => r.git_repo_resource_path !== selectedRepo.git_repo_resource_path && !r.settings) && {
|
||||
include_path: workspaceIncludePath,
|
||||
include_type: workspaceIncludeType
|
||||
})
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log(colors.green('\n✓ Repository migration completed successfully!'));
|
||||
if (operationName === "push") {
|
||||
console.log('Now proceeding with push operation...\n');
|
||||
}
|
||||
return migratedRepo;
|
||||
|
||||
} else {
|
||||
// Non-interactive mode - show error
|
||||
console.error(colors.red('\n❌ Legacy git-sync settings detected!'));
|
||||
console.error(`\nRepository "${selectedRepo.git_repo_resource_path}" has legacy settings format.`);
|
||||
if (operationName === "push") {
|
||||
console.error('This repository must be migrated before pushing settings.');
|
||||
}
|
||||
console.error('Please choose one of the following options:\n');
|
||||
console.error('1. Go to the Windmill UI > Workspace Settings > Git Sync');
|
||||
console.error(' Review and save this repository to migrate to the new format.\n');
|
||||
console.error('2. Run this command in interactive mode (with TTY) to migrate.');
|
||||
console.error(` Example: wmill gitsync-settings ${operationName}\n`);
|
||||
if (operationName === "push") {
|
||||
console.error('3. Pull settings first to migrate: wmill gitsync-settings pull\n');
|
||||
} else {
|
||||
console.error('3. Push local settings to override backend settings:');
|
||||
console.error(' wmill gitsync-settings push\n');
|
||||
}
|
||||
Deno.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Convert SyncOptions boolean flags to backend include_type array
|
||||
function syncOptionsToIncludeType(opts: SyncOptions): string[] {
|
||||
const includeTypes: string[] = [];
|
||||
@@ -279,6 +420,31 @@ function displayChanges(
|
||||
}
|
||||
}
|
||||
|
||||
async function selectAndLogRepository(
|
||||
repositories: GitSyncRepository[],
|
||||
repository?: string,
|
||||
): Promise<GitSyncRepository> {
|
||||
let selectedRepo: GitSyncRepository;
|
||||
|
||||
if (repository) {
|
||||
const found = repositories.find(
|
||||
(r: GitSyncRepository) =>
|
||||
r.git_repo_resource_path === repository ||
|
||||
r.git_repo_resource_path === `$res:${repository}`,
|
||||
);
|
||||
if (!found) {
|
||||
throw new Error(`Repository ${repository} not found`);
|
||||
}
|
||||
selectedRepo = found;
|
||||
const repoPath = selectedRepo.git_repo_resource_path.replace(/^\$res:/, "");
|
||||
log.info(colors.cyan(`Using repository: ${colors.bold(repoPath)}`));
|
||||
} else {
|
||||
selectedRepo = await selectRepository(repositories);
|
||||
}
|
||||
|
||||
return selectedRepo;
|
||||
}
|
||||
|
||||
async function pullGitSyncSettings(
|
||||
opts: GlobalOptions & {
|
||||
repository?: string;
|
||||
@@ -289,6 +455,7 @@ async function pullGitSyncSettings(
|
||||
replace?: boolean;
|
||||
override?: boolean;
|
||||
withBackendSettings?: string;
|
||||
yes?: boolean;
|
||||
},
|
||||
) {
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
@@ -401,22 +568,19 @@ async function pullGitSyncSettings(
|
||||
}
|
||||
|
||||
// Find the repository to work with
|
||||
let selectedRepo: GitSyncRepository;
|
||||
if (opts.repository) {
|
||||
const found = settings.git_sync.repositories.find(
|
||||
(r: GitSyncRepository) =>
|
||||
r.git_repo_resource_path === opts.repository ||
|
||||
r.git_repo_resource_path === `$res:${opts.repository}`,
|
||||
);
|
||||
if (!found) {
|
||||
throw new Error(`Repository ${opts.repository} not found`);
|
||||
}
|
||||
selectedRepo = found;
|
||||
} else {
|
||||
selectedRepo = await selectRepository(
|
||||
settings.git_sync.repositories,
|
||||
);
|
||||
}
|
||||
let selectedRepo = await selectAndLogRepository(
|
||||
settings.git_sync.repositories,
|
||||
opts.repository,
|
||||
);
|
||||
|
||||
// Check if the selected repository needs migration and handle it
|
||||
selectedRepo = await handleLegacyRepositoryMigration(
|
||||
selectedRepo,
|
||||
settings.git_sync,
|
||||
workspace,
|
||||
opts,
|
||||
"pull"
|
||||
);
|
||||
|
||||
// Convert backend settings to SyncOptions format
|
||||
const backendSyncOptions: SyncOptions = {
|
||||
@@ -534,7 +698,7 @@ async function pullGitSyncSettings(
|
||||
);
|
||||
} else {
|
||||
if (hasChanges) {
|
||||
log.info("Changes that would be made:");
|
||||
log.info("Changes that would be applied locally:");
|
||||
const changes = generateChanges(normalizedCurrent, normalizedBackend);
|
||||
|
||||
if (Object.keys(changes).length === 0) {
|
||||
@@ -573,7 +737,14 @@ async function pullGitSyncSettings(
|
||||
);
|
||||
const hasConflict = !deepEqual(gitSyncBackend, gitSyncCurrent);
|
||||
|
||||
if (hasConflict && Deno.stdin.isTerminal()) {
|
||||
if (hasConflict && !opts.yes && Deno.stdin.isTerminal()) {
|
||||
// Show the diff first
|
||||
log.info("Changes that would be applied locally:");
|
||||
const changes = generateChanges(currentSettings, backendSyncOptions);
|
||||
if (Object.keys(changes).length > 0) {
|
||||
displayChanges(changes);
|
||||
}
|
||||
|
||||
// Interactive mode - ask user
|
||||
const { Select } = await import("./deps.ts");
|
||||
const choice = await Select.prompt({
|
||||
@@ -607,6 +778,16 @@ async function pullGitSyncSettings(
|
||||
repoPath,
|
||||
);
|
||||
}
|
||||
} else if (hasConflict && opts.yes) {
|
||||
// --yes flag: default to override behavior for conflicts
|
||||
writeMode = "override";
|
||||
const repoPath = normalizeRepoPath(selectedRepo.git_repo_resource_path);
|
||||
overrideKey = constructOverrideKey(
|
||||
workspace.remote,
|
||||
workspace.workspaceId,
|
||||
repoPath,
|
||||
);
|
||||
log.info(colors.yellow("Settings conflict detected. Using --override behavior (default for --yes)."));
|
||||
} else if (hasConflict) {
|
||||
// Non-interactive mode with conflicts - show message and exit
|
||||
if (opts.jsonOutput) {
|
||||
@@ -664,11 +845,19 @@ async function pullGitSyncSettings(
|
||||
// Apply the settings based on write mode
|
||||
let updatedConfig: SyncOptions;
|
||||
|
||||
// Log which settings mode is being used
|
||||
const repoPath = selectedRepo.git_repo_resource_path.replace(/^\$res:/, "");
|
||||
if (writeMode === "override") {
|
||||
log.info(`Applied repository-specific overrides for repository "${repoPath}"`);
|
||||
} else {
|
||||
log.info(`Applied settings for repository "${repoPath}"`);
|
||||
}
|
||||
|
||||
if (writeMode === "replace") {
|
||||
// Preserve existing local config and update only git-sync fields
|
||||
updatedConfig = { ...localConfig };
|
||||
// Remove overrides since we're in replace mode
|
||||
delete updatedConfig.overrides;
|
||||
// Clear overrides since we're in replace mode, but keep empty object for consistency
|
||||
updatedConfig.overrides = {};
|
||||
// Update with backend git-sync settings
|
||||
Object.assign(updatedConfig, backendSyncOptions);
|
||||
} else if (writeMode === "override" && overrideKey) {
|
||||
@@ -761,6 +950,7 @@ async function pushGitSyncSettings(
|
||||
diff?: boolean;
|
||||
jsonOutput?: boolean;
|
||||
withBackendSettings?: string;
|
||||
yes?: boolean;
|
||||
},
|
||||
) {
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
@@ -888,21 +1078,23 @@ async function pushGitSyncSettings(
|
||||
}
|
||||
|
||||
// Find the repository to work with
|
||||
let selectedRepo: GitSyncRepository;
|
||||
if (opts.repository) {
|
||||
const found = settings.git_sync.repositories.find(
|
||||
(r: GitSyncRepository) =>
|
||||
r.git_repo_resource_path === opts.repository ||
|
||||
r.git_repo_resource_path === `$res:${opts.repository}`,
|
||||
);
|
||||
if (!found) {
|
||||
throw new Error(`Repository ${opts.repository} not found`);
|
||||
}
|
||||
selectedRepo = found;
|
||||
} else {
|
||||
selectedRepo = await selectRepository(
|
||||
settings.git_sync.repositories,
|
||||
);
|
||||
let selectedRepo = await selectAndLogRepository(
|
||||
settings.git_sync.repositories,
|
||||
opts.repository,
|
||||
);
|
||||
|
||||
// Check if the selected repository needs migration and handle it
|
||||
selectedRepo = await handleLegacyRepositoryMigration(
|
||||
selectedRepo,
|
||||
settings.git_sync,
|
||||
workspace,
|
||||
opts,
|
||||
"push"
|
||||
);
|
||||
|
||||
// If migration was cancelled, exit
|
||||
if (selectedRepo === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get effective settings for this workspace/repo
|
||||
@@ -922,24 +1114,25 @@ async function pushGitSyncSettings(
|
||||
extra_include_path: effectiveSettings.extraIncludes || [],
|
||||
};
|
||||
|
||||
// Calculate diff for all modes
|
||||
const currentBackend = selectedRepo.settings;
|
||||
|
||||
// Convert current backend settings to SyncOptions for user-friendly display
|
||||
const currentSyncOptions: SyncOptions = {
|
||||
includes: currentBackend.include_path || [],
|
||||
excludes: currentBackend.exclude_path || [],
|
||||
extraIncludes: currentBackend.extra_include_path || [],
|
||||
...includeTypeToSyncOptions(currentBackend.include_type || []),
|
||||
};
|
||||
|
||||
const normalizedCurrent = normalizeSyncOptions(currentSyncOptions);
|
||||
const normalizedEffective = normalizeSyncOptions(effectiveSettings);
|
||||
const gitSyncCurrent = extractGitSyncFields(normalizedCurrent);
|
||||
const gitSyncEffective = extractGitSyncFields(normalizedEffective);
|
||||
const hasChanges = !deepEqual(gitSyncEffective, gitSyncCurrent);
|
||||
|
||||
if (opts.diff) {
|
||||
// Show what would be pushed
|
||||
const currentBackend = selectedRepo.settings;
|
||||
|
||||
// Convert current backend settings to SyncOptions for user-friendly display
|
||||
const currentSyncOptions: SyncOptions = {
|
||||
includes: currentBackend.include_path || [],
|
||||
excludes: currentBackend.exclude_path || [],
|
||||
extraIncludes: currentBackend.extra_include_path || [],
|
||||
...includeTypeToSyncOptions(currentBackend.include_type || []),
|
||||
};
|
||||
|
||||
const normalizedCurrent = normalizeSyncOptions(currentSyncOptions);
|
||||
const normalizedEffective = normalizeSyncOptions(effectiveSettings);
|
||||
const gitSyncCurrent = extractGitSyncFields(normalizedCurrent);
|
||||
const gitSyncEffective = extractGitSyncFields(normalizedEffective);
|
||||
const hasChanges = !deepEqual(gitSyncEffective, gitSyncCurrent);
|
||||
|
||||
// --diff flag: show differences and exit
|
||||
if (opts.jsonOutput) {
|
||||
// Generate structured diff using the same normalized objects
|
||||
const structuredDiff = hasChanges
|
||||
@@ -958,7 +1151,7 @@ async function pushGitSyncSettings(
|
||||
);
|
||||
} else {
|
||||
if (hasChanges) {
|
||||
log.info("Changes that would be pushed:");
|
||||
log.info("Changes that would be pushed to Windmill:");
|
||||
const changes = generateChanges(
|
||||
normalizedCurrent,
|
||||
normalizedEffective,
|
||||
@@ -976,6 +1169,39 @@ async function pushGitSyncSettings(
|
||||
return;
|
||||
}
|
||||
|
||||
// Default behavior: show changes and ask for confirmation (unless --yes is passed)
|
||||
if (hasChanges) {
|
||||
if (!opts.jsonOutput) {
|
||||
const changes = generateChanges(
|
||||
normalizedCurrent,
|
||||
normalizedEffective,
|
||||
);
|
||||
|
||||
if (Object.keys(changes).length === 0) {
|
||||
log.info(colors.green("No changes to push"));
|
||||
return;
|
||||
} else {
|
||||
log.info("Changes that would be pushed to Windmill:");
|
||||
displayChanges(changes);
|
||||
}
|
||||
}
|
||||
|
||||
// Ask for confirmation unless --yes is passed or not in TTY
|
||||
if (!opts.yes && Deno.stdin.isTerminal()) {
|
||||
const confirmed = await Confirm.prompt({
|
||||
message: `Do you want to apply these changes to the remote?`,
|
||||
default: true,
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.info(colors.green("No changes to push"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (opts.withBackendSettings) {
|
||||
// Skip backend update when using simulated settings
|
||||
if (opts.jsonOutput) {
|
||||
@@ -1104,6 +1330,7 @@ const command = new Command()
|
||||
"--with-backend-settings <json:string>",
|
||||
"Use provided JSON settings instead of querying backend (for testing)",
|
||||
)
|
||||
.option("--yes", "Skip interactive prompts and use default behavior")
|
||||
.action(pullGitSyncSettings as any)
|
||||
.command("push")
|
||||
.description(
|
||||
@@ -1119,6 +1346,7 @@ const command = new Command()
|
||||
"--with-backend-settings <json:string>",
|
||||
"Use provided JSON settings instead of querying backend (for testing)",
|
||||
)
|
||||
.option("--yes", "Skip interactive prompts and use default behavior")
|
||||
.action(pushGitSyncSettings as any);
|
||||
|
||||
export { pullGitSyncSettings, pushGitSyncSettings };
|
||||
|
||||
@@ -143,6 +143,7 @@ const command = new Command()
|
||||
skipFolders: DEFAULT_SYNC_OPTIONS.skipFolders,
|
||||
includeSchedules: DEFAULT_SYNC_OPTIONS.includeSchedules,
|
||||
includeTriggers: DEFAULT_SYNC_OPTIONS.includeTriggers,
|
||||
overrides: {},
|
||||
};
|
||||
|
||||
await Deno.writeTextFile(
|
||||
|
||||
+5
-38
@@ -56,43 +56,8 @@ function mergeCliWithEffectiveOptions<T extends GlobalOptions & SyncOptions & {
|
||||
cliOpts: T,
|
||||
effectiveOpts: SyncOptions
|
||||
): T {
|
||||
// Start with effective options from config, then overlay only explicitly provided CLI flags
|
||||
const mergedOpts = Object.assign({}, effectiveOpts) as T;
|
||||
|
||||
// Always preserve these operational CLI flags
|
||||
if (cliOpts.dryRun !== undefined) mergedOpts.dryRun = cliOpts.dryRun;
|
||||
if (cliOpts.yes !== undefined) mergedOpts.yes = cliOpts.yes;
|
||||
if (cliOpts.stateful !== undefined) mergedOpts.stateful = cliOpts.stateful;
|
||||
if (cliOpts.skipPull !== undefined) mergedOpts.skipPull = cliOpts.skipPull;
|
||||
if (cliOpts.failConflicts !== undefined) mergedOpts.failConflicts = cliOpts.failConflicts;
|
||||
if (cliOpts.plainSecrets !== undefined) mergedOpts.plainSecrets = cliOpts.plainSecrets;
|
||||
if (cliOpts.json !== undefined) mergedOpts.json = cliOpts.json;
|
||||
if (cliOpts.message !== undefined) mergedOpts.message = cliOpts.message;
|
||||
if (cliOpts.parallel !== undefined) mergedOpts.parallel = cliOpts.parallel;
|
||||
if (cliOpts.jsonOutput !== undefined) mergedOpts.jsonOutput = cliOpts.jsonOutput;
|
||||
if (cliOpts.repository !== undefined) mergedOpts.repository = cliOpts.repository;
|
||||
|
||||
// Always preserve CLI include flags (they should override config file settings)
|
||||
if (cliOpts.includeUsers !== undefined) mergedOpts.includeUsers = cliOpts.includeUsers;
|
||||
if (cliOpts.includeGroups !== undefined) mergedOpts.includeGroups = cliOpts.includeGroups;
|
||||
if (cliOpts.includeSettings !== undefined) mergedOpts.includeSettings = cliOpts.includeSettings;
|
||||
if (cliOpts.includeKey !== undefined) mergedOpts.includeKey = cliOpts.includeKey;
|
||||
if (cliOpts.includeSchedules !== undefined) mergedOpts.includeSchedules = cliOpts.includeSchedules;
|
||||
if (cliOpts.includeTriggers !== undefined) mergedOpts.includeTriggers = cliOpts.includeTriggers;
|
||||
|
||||
// Always preserve CLI skip flags (they should override config file settings)
|
||||
if (cliOpts.skipScripts !== undefined) mergedOpts.skipScripts = cliOpts.skipScripts;
|
||||
if (cliOpts.skipFolders !== undefined) mergedOpts.skipFolders = cliOpts.skipFolders;
|
||||
if (cliOpts.skipVariables !== undefined) mergedOpts.skipVariables = cliOpts.skipVariables;
|
||||
if (cliOpts.skipResources !== undefined) mergedOpts.skipResources = cliOpts.skipResources;
|
||||
if (cliOpts.skipResourceTypes !== undefined) mergedOpts.skipResourceTypes = cliOpts.skipResourceTypes;
|
||||
if (cliOpts.skipSecrets !== undefined) mergedOpts.skipSecrets = cliOpts.skipSecrets;
|
||||
if (cliOpts.skipFlows !== undefined) mergedOpts.skipFlows = cliOpts.skipFlows;
|
||||
if (cliOpts.skipApps !== undefined) mergedOpts.skipApps = cliOpts.skipApps;
|
||||
|
||||
|
||||
|
||||
return mergedOpts;
|
||||
// overlay CLI options on top (undefined cliOpts won't override effectiveOpts)
|
||||
return Object.assign({}, effectiveOpts, cliOpts) as T;
|
||||
}
|
||||
|
||||
// Resolve effective sync options with smart repository detection
|
||||
@@ -164,12 +129,14 @@ async function resolveEffectiveSyncOptions(
|
||||
} else {
|
||||
// Non-interactive mode - list options and use top-level
|
||||
log.warn(`Multiple repository overrides found: ${applicableRepos.join(', ')}`);
|
||||
log.warn(`Use --repository flag to specify which one to use. Using top-level settings.`);
|
||||
log.warn(`Running in non-interactive mode. Use --repository flag to specify which one to use.`);
|
||||
log.info(`Falling back to top-level settings (no repository-specific overrides applied)`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No repository overrides found or selected - use top-level settings
|
||||
log.info(`No repository overrides found, using top-level settings`);
|
||||
return getEffectiveSettings(
|
||||
localConfig,
|
||||
workspace.remote,
|
||||
|
||||
@@ -55,7 +55,7 @@ excludes: []`);
|
||||
// Read updated config
|
||||
const updatedConfig = await Deno.readTextFile(`${tempDir}/wmill.yaml`);
|
||||
const backendUrl = new URL(backend.baseUrl).toString();
|
||||
|
||||
|
||||
// Should create workspace wildcard override
|
||||
assertStringIncludes(updatedConfig, `'${backendUrl}:${backend.workspace}:*':`);
|
||||
assertStringIncludes(updatedConfig, "overrides:");
|
||||
@@ -109,14 +109,14 @@ skipVariables: false`);
|
||||
|
||||
// Read updated config
|
||||
const updatedConfig = await Deno.readTextFile(`${tempDir}/wmill.yaml`);
|
||||
|
||||
|
||||
// Should update top-level settings, not create overrides
|
||||
assertStringIncludes(updatedConfig, "includes:\n - f/special/**");
|
||||
assertStringIncludes(updatedConfig, "excludes:\n - '*.test.ts'");
|
||||
assertStringIncludes(updatedConfig, "extraIncludes:\n - g/**");
|
||||
|
||||
// Should NOT have overrides section
|
||||
assertEquals(updatedConfig.includes("overrides:"), false, "Default mode should not create overrides");
|
||||
|
||||
// Should have empty overrides section for consistency
|
||||
assertStringIncludes(updatedConfig, "overrides: {}", "Should have empty overrides section for consistency");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -168,10 +168,10 @@ skipResources: false`);
|
||||
], tempDir);
|
||||
|
||||
assertEquals(result.code, 0);
|
||||
|
||||
|
||||
// Should show differences
|
||||
assertStringIncludes(result.stdout, "Changes that would be made:");
|
||||
assertStringIncludes(result.stdout, "Changes that would be applied locally:");
|
||||
// Should show the change for skipResources (ignoring ANSI color codes)
|
||||
assertStringIncludes(result.stdout, "skipResources:");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -126,8 +126,8 @@ Deno.test("Init: --use-backend flag applies git-sync settings", async () => {
|
||||
assertStringIncludes(wmillYaml, "*.test.ts", "Should include backend's exclude_path");
|
||||
assertStringIncludes(wmillYaml, "g/**", "Should include backend's extra_include_path");
|
||||
|
||||
// Should NOT have overrides section since we're starting fresh
|
||||
assertEquals(wmillYaml.includes("overrides:"), false, "Init should not create overrides section");
|
||||
// Should have empty overrides section for consistency
|
||||
assertStringIncludes(wmillYaml, "overrides: {}");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -180,6 +180,6 @@ Deno.test("Init: --use-default bypasses backend settings check", async () => {
|
||||
|
||||
// Should NOT have backend-specific settings
|
||||
assertEquals(wmillYaml.includes("f/should-be-ignored/**"), false, "Should not include backend settings");
|
||||
assertEquals(wmillYaml.includes("overrides:"), false, "Should not create overrides when using defaults");
|
||||
assertStringIncludes(wmillYaml, "overrides: {}", "Should have empty overrides section for consistency");
|
||||
});
|
||||
});
|
||||
@@ -24,7 +24,7 @@ Deno.test("Multi-Instance: gitsync-settings pull with new format", async () => {
|
||||
await withContainerizedBackend(async (backend, tempDir) => {
|
||||
// Set up workspace profile
|
||||
await setupWorkspaceProfile(backend, "multi_instance_test");
|
||||
|
||||
|
||||
// Create wmill.yaml with new format overrides for different instances
|
||||
const backendUrl = new URL(backend.baseUrl).toString(); // Normalize URL
|
||||
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
||||
@@ -38,22 +38,22 @@ overrides:
|
||||
includeTriggers: true
|
||||
includeSchedules: true
|
||||
skipVariables: true
|
||||
|
||||
|
||||
# Different instance (won't match)
|
||||
"https://app.windmill.dev/:${backend.workspace}:u/test/test_repo":
|
||||
includeTriggers: false
|
||||
includeSchedules: false
|
||||
skipVariables: false`);
|
||||
|
||||
|
||||
// Pull settings - should use the matching instance override (skipVariables: true)
|
||||
const pullResult = await backend.runCLICommand([
|
||||
'gitsync-settings', 'pull',
|
||||
'--repository', 'u/test/test_repo',
|
||||
'--diff'
|
||||
], tempDir, "multi_instance_test");
|
||||
|
||||
|
||||
assertEquals(pullResult.code, 0);
|
||||
assertStringIncludes(pullResult.stdout, "Changes that would be made:");
|
||||
assertStringIncludes(pullResult.stdout, "Changes that would be applied locally:");
|
||||
// includeSchedules should show as a change since backend default is false
|
||||
assertStringIncludes(pullResult.stdout, "includeSchedules");
|
||||
});
|
||||
@@ -61,9 +61,9 @@ overrides:
|
||||
|
||||
Deno.test("Multi-Instance: gitsync-settings push with overrides", async () => {
|
||||
await withContainerizedBackend(async (backend, tempDir) => {
|
||||
// Set up workspace profile
|
||||
// Set up workspace profile
|
||||
await setupWorkspaceProfile(backend, "push_override_test");
|
||||
|
||||
|
||||
// Create wmill.yaml with specific settings that differ from backend defaults
|
||||
const backendUrl = new URL(backend.baseUrl).toString();
|
||||
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
||||
@@ -78,16 +78,16 @@ overrides:
|
||||
"${backendUrl}:${backend.workspace}:u/test/test_repo":
|
||||
includeSchedules: true
|
||||
skipVariables: true`);
|
||||
|
||||
|
||||
// Push settings - should show changes because includeSchedules differs from backend
|
||||
const pushResult = await backend.runCLICommand([
|
||||
'gitsync-settings', 'push',
|
||||
'--repository', 'u/test/test_repo',
|
||||
'--diff'
|
||||
], tempDir, "push_override_test");
|
||||
|
||||
|
||||
assertEquals(pushResult.code, 0);
|
||||
assertStringIncludes(pushResult.stdout, "Changes that would be pushed:");
|
||||
assertStringIncludes(pushResult.stdout, "Changes that would be pushed to Windmill:");
|
||||
assertStringIncludes(pushResult.stdout, "includeSchedules");
|
||||
});
|
||||
});
|
||||
@@ -95,7 +95,7 @@ overrides:
|
||||
Deno.test("Multi-Instance: sync with repository-specific overrides", async () => {
|
||||
await withContainerizedBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend, "my-workspace_123");
|
||||
|
||||
|
||||
const backendUrl = new URL(backend.baseUrl).toString();
|
||||
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
||||
includes:
|
||||
@@ -105,22 +105,22 @@ excludes: []
|
||||
overrides:
|
||||
"${backendUrl}:${backend.workspace}:u/test/test_repo":
|
||||
skipApps: true`);
|
||||
|
||||
|
||||
const result = await backend.runCLICommand([
|
||||
'sync', 'pull',
|
||||
'--repository', 'u/test/test_repo',
|
||||
'--dry-run',
|
||||
'--json-output'
|
||||
], tempDir, "my-workspace_123");
|
||||
|
||||
|
||||
assertEquals(result.code, 0);
|
||||
|
||||
|
||||
const data = parseJsonFromCLIOutput(result.stdout);
|
||||
|
||||
|
||||
// Test is designed to verify that the new format works correctly
|
||||
|
||||
|
||||
// The test app should NOT appear in changes because skipApps: true
|
||||
const hasTestApp = (data.changes || []).some((change: any) =>
|
||||
const hasTestApp = (data.changes || []).some((change: any) =>
|
||||
change.path?.includes('f/test_dashboard')
|
||||
);
|
||||
assertEquals(hasTestApp, false, "Test app should be skipped due to skipApps override");
|
||||
@@ -130,7 +130,7 @@ overrides:
|
||||
Deno.test("Multi-Instance: auto-detection of single repository override", async () => {
|
||||
await withContainerizedBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend, "auto_detect_test");
|
||||
|
||||
|
||||
const backendUrl = new URL(backend.baseUrl).toString();
|
||||
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
||||
includes:
|
||||
@@ -142,21 +142,21 @@ overrides:
|
||||
"${backendUrl}:${backend.workspace}:u/test/test_repo":
|
||||
skipApps: true
|
||||
includeSchedules: true`);
|
||||
|
||||
|
||||
// Don't specify --repository, it should auto-detect
|
||||
const result = await backend.runCLICommand([
|
||||
'sync', 'pull',
|
||||
'--dry-run',
|
||||
'--json-output'
|
||||
], tempDir, "auto_detect_test");
|
||||
|
||||
|
||||
assertEquals(result.code, 0);
|
||||
assertStringIncludes(result.stdout, "Auto-selected repository: u/test/test_repo");
|
||||
|
||||
|
||||
const data = parseJsonFromCLIOutput(result.stdout);
|
||||
|
||||
|
||||
// The test app should NOT appear because of auto-detected skipApps: true
|
||||
const hasTestApp = (data.changes || []).some((change: any) =>
|
||||
const hasTestApp = (data.changes || []).some((change: any) =>
|
||||
change.path?.includes('f/test_dashboard')
|
||||
);
|
||||
assertEquals(hasTestApp, false, "Test app should be skipped due to auto-detected override");
|
||||
@@ -166,7 +166,7 @@ overrides:
|
||||
Deno.test("Multi-Instance: workspace wildcards with new format", async () => {
|
||||
await withContainerizedBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend, "wildcard_test");
|
||||
|
||||
|
||||
const backendUrl = new URL(backend.baseUrl).toString();
|
||||
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
||||
includes:
|
||||
@@ -178,22 +178,22 @@ overrides:
|
||||
"${backendUrl}:${backend.workspace}:*":
|
||||
skipVariables: true
|
||||
skipResources: true`);
|
||||
|
||||
|
||||
const result = await backend.runCLICommand([
|
||||
'sync', 'pull',
|
||||
'--repository', 'u/test/test_repo',
|
||||
'--dry-run',
|
||||
'--json-output'
|
||||
], tempDir, "wildcard_test");
|
||||
|
||||
|
||||
assertEquals(result.code, 0);
|
||||
|
||||
|
||||
const data = parseJsonFromCLIOutput(result.stdout);
|
||||
|
||||
|
||||
// Variables should be skipped due to wildcard override
|
||||
const hasTestVariable = (data.changes || []).some((change: any) =>
|
||||
const hasTestVariable = (data.changes || []).some((change: any) =>
|
||||
change.path?.includes('u/admin/test_config.variable.yaml')
|
||||
);
|
||||
assertEquals(hasTestVariable, false, "Variables should be skipped due to wildcard override");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -30,7 +30,7 @@ Deno.test("Integration: wmill.yaml configuration produces expected results", asy
|
||||
await withContainerizedBackend(async (backend, tempDir) => {
|
||||
// Set up workspace profile with name "localhost_test"
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
|
||||
// Create wmill.yaml with settings
|
||||
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
||||
includes:
|
||||
@@ -43,7 +43,7 @@ skipResources: true
|
||||
includeSettings: true
|
||||
includeSchedules: true
|
||||
includeTriggers: true`);
|
||||
|
||||
|
||||
// Test pull with wmill.yaml configuration
|
||||
const yamlResult = await backend.runCLICommand(['sync', 'pull', '--dry-run', '--json-output'], tempDir);
|
||||
if (yamlResult.code !== 0) {
|
||||
@@ -53,21 +53,21 @@ includeTriggers: true`);
|
||||
console.log("Stderr:", yamlResult.stderr);
|
||||
}
|
||||
assertEquals(yamlResult.code, 0);
|
||||
|
||||
|
||||
// Extract JSON from CLI output (skip log messages)
|
||||
const yamlData = parseJsonFromCLIOutput(yamlResult.stdout);
|
||||
|
||||
|
||||
// Should include settings.yaml due to includeSettings: true
|
||||
const hasSettings = (yamlData.changes || []).some((change: any) =>
|
||||
const hasSettings = (yamlData.changes || []).some((change: any) =>
|
||||
change.type === 'added' && change.path === 'settings.yaml'
|
||||
);
|
||||
assertEquals(hasSettings, true);
|
||||
|
||||
|
||||
// Should NOT include resources or variables (due to skip flags)
|
||||
const hasResources = (yamlData.changes || []).some((change: any) =>
|
||||
const hasResources = (yamlData.changes || []).some((change: any) =>
|
||||
change.type === 'added' && change.path?.includes('.resource.yaml')
|
||||
);
|
||||
const hasVariables = (yamlData.changes || []).some((change: any) =>
|
||||
const hasVariables = (yamlData.changes || []).some((change: any) =>
|
||||
change.type === 'added' && change.path?.includes('.variable.yaml')
|
||||
);
|
||||
assertEquals(hasResources, false);
|
||||
@@ -79,35 +79,35 @@ Deno.test("Integration: settings.yaml inclusion respects includeSettings flag",
|
||||
await withContainerizedBackend(async (backend, tempDir) => {
|
||||
// Set up workspace profile with name "localhost_test"
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
// Test 1: includeSettings: true should include settings.yaml
|
||||
|
||||
// Test 1: includeSettings: true should include settings.yaml
|
||||
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
||||
includes:
|
||||
- "**"
|
||||
includeSettings: true`);
|
||||
|
||||
|
||||
const includeResult = await backend.runCLICommand(['sync', 'pull', '--dry-run', '--json-output'], tempDir);
|
||||
assertEquals(includeResult.code, 0);
|
||||
|
||||
|
||||
// Extract JSON from CLI output (skip log messages)
|
||||
const includeData = parseJsonFromCLIOutput(includeResult.stdout);
|
||||
const hasSettingsInclude = (includeData.changes || []).some((change: any) =>
|
||||
const hasSettingsInclude = (includeData.changes || []).some((change: any) =>
|
||||
change.type === 'added' && change.path === 'settings.yaml'
|
||||
);
|
||||
assertEquals(hasSettingsInclude, true);
|
||||
|
||||
|
||||
// Test 2: includeSettings: false should NOT include settings.yaml
|
||||
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
||||
includes:
|
||||
- "**"
|
||||
includeSettings: false`);
|
||||
|
||||
|
||||
const excludeResult = await backend.runCLICommand(['sync', 'pull', '--dry-run', '--json-output'], tempDir);
|
||||
assertEquals(excludeResult.code, 0);
|
||||
|
||||
|
||||
// Extract JSON from CLI output (skip log messages)
|
||||
const excludeData = parseJsonFromCLIOutput(excludeResult.stdout);
|
||||
const hasSettingsExclude = (excludeData.changes || []).some((change: any) =>
|
||||
const hasSettingsExclude = (excludeData.changes || []).some((change: any) =>
|
||||
change.type === 'added' && change.path === 'settings.yaml'
|
||||
);
|
||||
assertEquals(hasSettingsExclude, false);
|
||||
@@ -118,30 +118,86 @@ Deno.test("Integration: resource/variable filtering respects skip flags", async
|
||||
await withContainerizedBackend(async (backend, tempDir) => {
|
||||
// Set up workspace profile with name "localhost_test"
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
// Test skipResources: true
|
||||
|
||||
// Test skipResources: true
|
||||
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
||||
includes:
|
||||
- "**"
|
||||
skipResources: true
|
||||
skipVariables: false`);
|
||||
|
||||
|
||||
const result = await backend.runCLICommand(['sync', 'pull', '--dry-run', '--json-output'], tempDir);
|
||||
assertEquals(result.code, 0);
|
||||
|
||||
|
||||
// Extract JSON from CLI output (skip log messages)
|
||||
const data = parseJsonFromCLIOutput(result.stdout);
|
||||
|
||||
|
||||
// Should NOT include resources
|
||||
const hasResources = (data.changes || []).some((change: any) =>
|
||||
const hasResources = (data.changes || []).some((change: any) =>
|
||||
change.type === 'added' && change.path?.includes('.resource.yaml')
|
||||
);
|
||||
assertEquals(hasResources, false);
|
||||
|
||||
|
||||
// Should include variables (not skipped)
|
||||
const hasVariables = (data.changes || []).some((change: any) =>
|
||||
const hasVariables = (data.changes || []).some((change: any) =>
|
||||
change.type === 'added' && change.path?.includes('.variable.yaml')
|
||||
);
|
||||
assertEquals(hasVariables, true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// CLI FLAG OVERRIDE TESTS
|
||||
// Tests for CLI flags overriding configuration file settings
|
||||
// =============================================================================
|
||||
|
||||
Deno.test("CLI skip flags override wmill.yaml configuration", async () => {
|
||||
await withContainerizedBackend(async (backend, tempDir) => {
|
||||
// Set up workspace profile with name "localhost_test"
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
// Create wmill.yaml that INCLUDES resources by default (skipResources: false)
|
||||
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
||||
includes:
|
||||
- f/**
|
||||
- u/**
|
||||
skipResources: false
|
||||
skipResourceTypes: false
|
||||
includeSettings: true`);
|
||||
|
||||
// Test 1: Without CLI flags - should respect wmill.yaml (include resources)
|
||||
const configResult = await backend.runCLICommand(['sync', 'pull', '--dry-run', '--json-output'], tempDir);
|
||||
assertEquals(configResult.code, 0);
|
||||
|
||||
const configData = parseJsonFromCLIOutput(configResult.stdout);
|
||||
|
||||
|
||||
// Should include resources (not skipped by config)
|
||||
const hasResources = (configData.changes || []).some((change: any) =>
|
||||
change.type === 'added' && change.path?.includes('.resource.yaml')
|
||||
);
|
||||
assertEquals(hasResources, true, "Resources should be included by wmill.yaml config");
|
||||
|
||||
// Test 2: With CLI --skip-resources flag - should override wmill.yaml to skip resources
|
||||
const overrideResult = await backend.runCLICommand([
|
||||
'sync', 'pull', '--dry-run', '--json-output',
|
||||
'--skip-resources', // CLI flag should override config to skip resources
|
||||
'--skip-resource-types' // CLI flag should override config to skip resource types
|
||||
], tempDir);
|
||||
assertEquals(overrideResult.code, 0);
|
||||
|
||||
const overrideData = parseJsonFromCLIOutput(overrideResult.stdout);
|
||||
|
||||
// Should NOT include resources (CLI flag overrides config)
|
||||
const hasResourcesOverride = (overrideData.changes || []).some((change: any) =>
|
||||
change.type === 'added' && change.path?.includes('.resource.yaml')
|
||||
);
|
||||
assertEquals(hasResourcesOverride, false, "CLI --skip-resources flag should override wmill.yaml to exclude resources");
|
||||
|
||||
// Should NOT include resource types (CLI flag overrides config)
|
||||
const hasResourceTypesOverride = (overrideData.changes || []).some((change: any) =>
|
||||
change.type === 'added' && change.path?.includes('.resource-type.yaml')
|
||||
);
|
||||
assertEquals(hasResourceTypesOverride, false, "CLI --skip-resource-types flag should override wmill.yaml to exclude resource types");
|
||||
});
|
||||
});
|
||||
|
||||
+2
-2
@@ -2,7 +2,7 @@
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-nocheck This file is copied from a JS project, so it's not type-safe.
|
||||
|
||||
import { log, encodeHex, SEP } from "./deps.ts";
|
||||
import { colors, log, encodeHex, SEP } from "./deps.ts";
|
||||
import crypto from "node:crypto";
|
||||
|
||||
export function deepEqual<T>(a: T, b: T): boolean {
|
||||
@@ -167,7 +167,7 @@ export async function selectRepository<T extends Repository>(
|
||||
|
||||
if (repositories.length === 1) {
|
||||
const repoPath = repositories[0].git_repo_resource_path.replace(/^\$res:/, "");
|
||||
log.info(`Using repository: ${repoPath}`);
|
||||
log.info(colors.cyan(`Auto-selected repository: ${colors.bold(repoPath)}`));
|
||||
return repositories[0];
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user