mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-23 00:00:33 +00:00
feat: git sync improvements (#6182)
* init checkpoint * ui second pass... * round 1 backend + saving settings + detecting changes... * checkpoint * fix openapi * saving + correct wmill.yaml diff * cli refactor * cli and tests refactor done * cli multi workspace support * cli support skip core types to align with ui * new test framework * sqlx * openapi spec * frontend * sync + settings changes * some fixes * some fixes * security: Remove hardcoded EE license key, use environment variable only - Remove hardcoded license key from containerized test backend - Environment variable EE_LICENSE_KEY now required for EE features - License key no longer stored in database during tests * sqlx * tests * fixing tests * fix tests * checkpoint * checkpoint * cli build * frontend - cli exchange * settings match * ee repo ref * npm check * openapi * tests * checkpoint * cli + tests * reset to preview on changes * merge issue ee * cleanup * hubscript * simplifications * ee repo ref * cli fixes * fix sync and add tests * extra test * git sync settings / key change aware * ee-repo ref * ee-repo ref * ee repo ref * ee ref * review 1 * ee ref * Update frontend/src/lib/components/PullGitRepoPopover.svelte Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> * Update frontend/src/routes/(root)/(logged)/workspace_settings/+page.svelte Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> * ee ref * remove extra includes from ui --------- Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
# Testing Guide for Windmill CLI
|
||||
|
||||
## Running Tests
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
deno test -A --no-check test/
|
||||
|
||||
# Run specific test files
|
||||
deno test -A --no-check test/gitsync_settings_features.test.ts
|
||||
deno test -A --no-check test/init_no_git_sync.test.ts
|
||||
deno test -A --no-check test/multi_instance_workspace.test.ts
|
||||
deno test -A --no-check test/override_settings_behavior.test.ts
|
||||
deno test -A --no-check test/sync_config_resolution.test.ts
|
||||
deno test -A --no-check test/workspace_conflicts.test.ts
|
||||
|
||||
# Run with specific test patterns
|
||||
deno test -A --no-check test/ --filter "workspace"
|
||||
deno test -A --no-check test/ --filter "sync"
|
||||
```
|
||||
|
||||
## Test Files
|
||||
|
||||
- **`gitsync_settings_features.test.ts`** - Git sync settings functionality
|
||||
- **`init_no_git_sync.test.ts`** - Init without git sync
|
||||
- **`multi_instance_workspace.test.ts`** - Multi-instance workspace handling
|
||||
- **`override_settings_behavior.test.ts`** - Settings override behavior
|
||||
- **`sync_config_resolution.test.ts`** - Sync configuration resolution
|
||||
- **`workspace_conflicts.test.ts`** - Workspace conflict detection
|
||||
|
||||
## Docker Requirements
|
||||
|
||||
```bash
|
||||
# Ensure Docker is running
|
||||
docker --version
|
||||
docker-compose --version
|
||||
|
||||
# Ensure EE license key is available
|
||||
echo $EE_LICENSE_KEY
|
||||
```
|
||||
|
||||
## Debugging Failed Tests
|
||||
|
||||
```bash
|
||||
# Run with verbose output
|
||||
deno test -A --no-check test/ --reporter=verbose
|
||||
|
||||
# Check container status
|
||||
docker ps
|
||||
|
||||
# View backend logs
|
||||
docker logs test-test_windmill_server-1
|
||||
|
||||
# Manual container management
|
||||
cd test
|
||||
docker compose -f docker-compose.test.yml up -d
|
||||
docker compose -f docker-compose.test.yml down
|
||||
docker compose -f docker-compose.test.yml down -v
|
||||
```
|
||||
+71
@@ -13,6 +13,10 @@ export interface SyncOptions {
|
||||
skipResources?: boolean;
|
||||
skipResourceTypes?: boolean;
|
||||
skipSecrets?: boolean;
|
||||
skipScripts?: boolean;
|
||||
skipFlows?: boolean;
|
||||
skipApps?: boolean;
|
||||
skipFolders?: boolean;
|
||||
includeSchedules?: boolean;
|
||||
includeTriggers?: boolean;
|
||||
includeUsers?: boolean;
|
||||
@@ -26,6 +30,8 @@ export interface SyncOptions {
|
||||
defaultTs?: "bun" | "deno";
|
||||
codebases?: Codebase[];
|
||||
parallel?: number;
|
||||
jsonOutput?: boolean;
|
||||
overrides?: { [key: string]: Partial<SyncOptions> };
|
||||
}
|
||||
|
||||
export interface Codebase {
|
||||
@@ -59,9 +65,74 @@ export async function readConfigFile(): Promise<SyncOptions> {
|
||||
}
|
||||
}
|
||||
|
||||
// Default sync options - shared across the codebase to prevent duplication
|
||||
export const DEFAULT_SYNC_OPTIONS: Readonly<Required<Pick<SyncOptions,
|
||||
'defaultTs' | 'includes' | 'excludes' | 'codebases' | 'skipVariables' | 'skipResources' |
|
||||
'skipResourceTypes' | 'skipSecrets' | 'includeSchedules' | 'includeTriggers' |
|
||||
'skipScripts' | 'skipFlows' | 'skipApps' | 'skipFolders' |
|
||||
'includeUsers' | 'includeGroups' | 'includeSettings' | 'includeKey'
|
||||
>>> = {
|
||||
defaultTs: 'bun',
|
||||
includes: ['f/**'],
|
||||
excludes: [],
|
||||
codebases: [],
|
||||
skipVariables: false,
|
||||
skipResources: false,
|
||||
skipResourceTypes: false,
|
||||
skipSecrets: true,
|
||||
skipScripts: false,
|
||||
skipFlows: false,
|
||||
skipApps: false,
|
||||
skipFolders: false,
|
||||
includeSchedules: false,
|
||||
includeTriggers: false,
|
||||
includeUsers: false,
|
||||
includeGroups: false,
|
||||
includeSettings: false,
|
||||
includeKey: false
|
||||
} as const;
|
||||
|
||||
export async function mergeConfigWithConfigFile<T>(
|
||||
opts: T
|
||||
): Promise<T & SyncOptions> {
|
||||
const configFile = await readConfigFile();
|
||||
return Object.assign(configFile ?? {}, opts);
|
||||
}
|
||||
|
||||
// Get effective settings by merging top-level settings and overrides
|
||||
export function getEffectiveSettings(
|
||||
config: SyncOptions,
|
||||
baseUrl: string,
|
||||
workspaceId: string,
|
||||
repo: string
|
||||
): 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) {
|
||||
return effective;
|
||||
}
|
||||
|
||||
// Construct override keys using the single format
|
||||
const workspaceKey = `${baseUrl}:${workspaceId}:*`;
|
||||
const repoKey = `${baseUrl}:${workspaceId}:${repo}`;
|
||||
|
||||
// Apply workspace-level overrides
|
||||
if (config.overrides[workspaceKey]) {
|
||||
Object.assign(effective, config.overrides[workspaceKey]);
|
||||
}
|
||||
|
||||
// Apply repository-specific overrides (overrides workspace-level)
|
||||
if (config.overrides[repoKey]) {
|
||||
Object.assign(effective, config.overrides[repoKey]);
|
||||
}
|
||||
|
||||
return effective;
|
||||
}
|
||||
|
||||
+58
-5
@@ -30,7 +30,7 @@ async function tryResolveWorkspace(
|
||||
if (cache) return { isError: false, value: cache };
|
||||
|
||||
if (opts.workspace) {
|
||||
const e = await getWorkspaceByName(opts.workspace);
|
||||
const e = await getWorkspaceByName(opts.workspace, opts.configDir);
|
||||
if (!e) {
|
||||
return {
|
||||
isError: true,
|
||||
@@ -57,8 +57,48 @@ export async function resolveWorkspace(
|
||||
): Promise<Workspace> {
|
||||
if (opts.baseUrl) {
|
||||
if (opts.workspace && opts.token) {
|
||||
const normalizedBaseUrl = new URL(opts.baseUrl).toString(); // add trailing slash if not present
|
||||
|
||||
// Try to find existing workspace profile by name, then by workspaceId + remote
|
||||
if (opts.workspace) {
|
||||
// Try by workspace name first
|
||||
let existingWorkspace = await getWorkspaceByName(opts.workspace, opts.configDir);
|
||||
|
||||
// If not found by name, try to find by workspaceId + remote match
|
||||
if (!existingWorkspace) {
|
||||
const { allWorkspaces } = await import("./workspace.ts");
|
||||
const workspaces = await allWorkspaces(opts.configDir);
|
||||
const matchingWorkspaces = workspaces.filter(
|
||||
w => w.workspaceId === opts.workspace && w.remote === normalizedBaseUrl
|
||||
);
|
||||
|
||||
// Due to uniqueness constraint, there can only be 0 or 1 match
|
||||
if (matchingWorkspaces.length === 1) {
|
||||
existingWorkspace = matchingWorkspaces[0];
|
||||
}
|
||||
}
|
||||
|
||||
if (existingWorkspace) {
|
||||
// Validate that the base URL matches the profile's remote
|
||||
if (existingWorkspace.remote !== normalizedBaseUrl) {
|
||||
log.info(
|
||||
colors.red(
|
||||
`Base URL mismatch: --base-url is ${normalizedBaseUrl} but workspace profile "${opts.workspace}" uses ${existingWorkspace.remote}`
|
||||
)
|
||||
);
|
||||
return Deno.exit(-1);
|
||||
}
|
||||
// Use the existing workspace profile (preserves workspace name)
|
||||
return {
|
||||
...existingWorkspace,
|
||||
token: opts.token, // Use the provided token
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// No existing profile found, create temporary workspace
|
||||
return {
|
||||
remote: new URL(opts.baseUrl).toString(), // add trailing slash if not present
|
||||
remote: normalizedBaseUrl,
|
||||
workspaceId: opts.workspace,
|
||||
name: opts.workspace,
|
||||
token: opts.token,
|
||||
@@ -95,20 +135,26 @@ export async function requireLogin(
|
||||
|
||||
try {
|
||||
return await wmill.globalWhoami();
|
||||
} catch {
|
||||
} catch (error) {
|
||||
// Check for network errors and provide clearer messages
|
||||
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||
if (errorMsg.includes('fetch') || errorMsg.includes('connection') || errorMsg.includes('ECONNREFUSED') || errorMsg.includes('refused')) {
|
||||
throw new Error(`Network error: Could not connect to Windmill server at ${workspace.remote}`);
|
||||
}
|
||||
|
||||
log.info(
|
||||
"! Could not reach API given existing credentials. Attempting to reauth..."
|
||||
);
|
||||
const newToken = await loginInteractive(workspace.remote);
|
||||
if (!newToken) {
|
||||
throw new Error("Could not reauth");
|
||||
throw new Error("Unauthorized: Could not authenticate with the provided credentials");
|
||||
}
|
||||
removeWorkspace(workspace.name, false, opts);
|
||||
workspace.token = newToken;
|
||||
addWorkspace(workspace, opts);
|
||||
|
||||
setClient(
|
||||
token,
|
||||
newToken,
|
||||
workspace.remote.substring(0, workspace.remote.length - 1)
|
||||
);
|
||||
return await wmill.globalWhoami();
|
||||
@@ -130,6 +176,13 @@ export async function fetchVersion(baseUrl: string): Promise<string> {
|
||||
new URL(new URL(baseUrl).origin + "/api/version"),
|
||||
{ headers: requestHeaders, method: "GET" }
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
// Consume response body even on error to avoid resource leak
|
||||
await response.text();
|
||||
throw new Error(`Failed to fetch version: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
return await response.text();
|
||||
}
|
||||
export async function tryResolveVersion(
|
||||
|
||||
Generated
+252
@@ -2184,6 +2184,258 @@
|
||||
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="
|
||||
}
|
||||
},
|
||||
"remote": {
|
||||
"https://deno.land/std@0.207.0/yaml/_dumper/dumper.ts": "717403d0e700de783f2ef5c906b3d7245383e1509fc050e7ff5d4a53a03dbf40",
|
||||
"https://deno.land/std@0.207.0/yaml/_dumper/dumper_state.ts": "f0d0673ceea288334061ca34b63954c2bb5feb5bf6de5e4cfe9a942cdf6e5efe",
|
||||
"https://deno.land/std@0.207.0/yaml/_error.ts": "b59e2c76ce5a47b1b9fa0ff9f96c1dd92ea1e1b17ce4347ece5944a95c3c1a84",
|
||||
"https://deno.land/std@0.207.0/yaml/_loader/loader.ts": "63ec7f0a265dbbabc54b25a4beefff7650e205160a2d75c7d8f8363b5f84851a",
|
||||
"https://deno.land/std@0.207.0/yaml/_loader/loader_state.ts": "0841870b467169269d7c2dfa75cd288c319bc06f65edd9e42c29e5fced91c7a4",
|
||||
"https://deno.land/std@0.207.0/yaml/_mark.ts": "dcd8585dee585e024475e9f3fe27d29740670fb64ebb970388094cad0fc11d5d",
|
||||
"https://deno.land/std@0.207.0/yaml/_state.ts": "ef03d55ec235d48dcfbecc0ab3ade90bfae69a61094846e08003421c2cf5cfc6",
|
||||
"https://deno.land/std@0.207.0/yaml/_type/binary.ts": "24d49614463a7339a8a16d894919c2ec18a10588ae360ec352093b60e2cc8b0d",
|
||||
"https://deno.land/std@0.207.0/yaml/_type/bool.ts": "5bfa75da84343d45347b521ba4e5aeace9fe6f53447405290d53315a3fc20e66",
|
||||
"https://deno.land/std@0.207.0/yaml/_type/float.ts": "056bd3cb9c5586238b20517511014fb24b0e36f98f9f6073e12da308b6b9808a",
|
||||
"https://deno.land/std@0.207.0/yaml/_type/function.ts": "ff574fe84a750695302864e1c31b93f12d14ada4bde79a5f93197fc33ad17471",
|
||||
"https://deno.land/std@0.207.0/yaml/_type/int.ts": "563ad074f0fa7aecf6b6c3d84135bcc95a8269dcc15de878de20ce868fd773fa",
|
||||
"https://deno.land/std@0.207.0/yaml/_type/map.ts": "7b105e4ab03a361c61e7e335a0baf4d40f06460b13920e5af3fb2783a1464000",
|
||||
"https://deno.land/std@0.207.0/yaml/_type/merge.ts": "8192bf3e4d637f32567917f48bb276043da9cf729cf594e5ec191f7cd229337e",
|
||||
"https://deno.land/std@0.207.0/yaml/_type/mod.ts": "060e2b3d38725094b77ea3a3f05fc7e671fced8e67ca18e525be98c4aa8f4bbb",
|
||||
"https://deno.land/std@0.207.0/yaml/_type/nil.ts": "606e8f0c44d73117c81abec822f89ef81e40f712258c74f186baa1af659b8887",
|
||||
"https://deno.land/std@0.207.0/yaml/_type/omap.ts": "cfe59a294726f5cea705c39a61fd2b08199cf48f4ccd6b040cb550ec0f38d0a1",
|
||||
"https://deno.land/std@0.207.0/yaml/_type/pairs.ts": "0032fdfe57558d21696a4f8cf5b5cfd1f698743177080affc18629685c905666",
|
||||
"https://deno.land/std@0.207.0/yaml/_type/regexp.ts": "1ce118de15b2da43b4bd8e4395f42d448b731acf3bdaf7c888f40789f9a95f8b",
|
||||
"https://deno.land/std@0.207.0/yaml/_type/seq.ts": "95333abeec8a7e4d967b8c8328b269e342a4bbdd2585395549b9c4f58c8533a2",
|
||||
"https://deno.land/std@0.207.0/yaml/_type/set.ts": "f28ba44e632ef2a6eb580486fd47a460445eeddbdf1dbc739c3e62486f566092",
|
||||
"https://deno.land/std@0.207.0/yaml/_type/str.ts": "a67a3c6e429d95041399e964015511779b1130ea5889fa257c48457bd3446e31",
|
||||
"https://deno.land/std@0.207.0/yaml/_type/timestamp.ts": "706ea80a76a73e48efaeb400ace087da1f927647b53ad6f754f4e06d51af087f",
|
||||
"https://deno.land/std@0.207.0/yaml/_type/undefined.ts": "94a316ca450597ccbc6750cbd79097ad0d5f3a019797eed3c841a040c29540ba",
|
||||
"https://deno.land/std@0.207.0/yaml/_utils.ts": "26b311f0d42a7ce025060bd6320a68b50e52fd24a839581eb31734cd48e20393",
|
||||
"https://deno.land/std@0.207.0/yaml/mod.ts": "28ecda6652f3e7a7735ee29c247bfbd32a2e2fc5724068e9fd173ec4e59f66f7",
|
||||
"https://deno.land/std@0.207.0/yaml/parse.ts": "1fbbda572bf3fff578b6482c0d8b85097a38de3176bf3ab2ca70c25fb0c960ef",
|
||||
"https://deno.land/std@0.207.0/yaml/schema.ts": "96908b78dc50c340074b93fc1598d5e7e2fe59103f89ff81e5a49b2dedf77a67",
|
||||
"https://deno.land/std@0.207.0/yaml/schema/core.ts": "fa406f18ceedc87a50e28bb90ec7a4c09eebb337f94ef17468349794fa828639",
|
||||
"https://deno.land/std@0.207.0/yaml/schema/default.ts": "0047e80ae8a4a93293bc4c557ae8a546aabd46bb7165b9d9b940d57b4d88bde9",
|
||||
"https://deno.land/std@0.207.0/yaml/schema/extended.ts": "0784416bf062d20a1626b53c03380e265b3e39b9409afb9f4cb7d659fd71e60d",
|
||||
"https://deno.land/std@0.207.0/yaml/schema/failsafe.ts": "d219ab5febc43f770917d8ec37735a4b1ad671149846cbdcade767832b42b92b",
|
||||
"https://deno.land/std@0.207.0/yaml/schema/json.ts": "5f41dd7c2f1ad545ef6238633ce9ee3d444dfc5a18101e1768bd5504bf90e5e5",
|
||||
"https://deno.land/std@0.207.0/yaml/schema/mod.ts": "4472e827bab5025e92bc2eb2eeefa70ecbefc64b2799b765c69af84822efef32",
|
||||
"https://deno.land/std@0.207.0/yaml/stringify.ts": "fffc09c65c68d3d63f8159e8cbaa3f489bc20a8e55b4fbb61a8c2e9f914d1d02",
|
||||
"https://deno.land/std@0.207.0/yaml/type.ts": "65553da3da3c029b6589c6e4903f0afbea6768be8fca61580711457151f2b30f",
|
||||
"https://deno.land/std@0.208.0/assert/_constants.ts": "8a9da298c26750b28b326b297316cdde860bc237533b07e1337c021379e6b2a9",
|
||||
"https://deno.land/std@0.208.0/assert/_diff.ts": "58e1461cc61d8eb1eacbf2a010932bf6a05b79344b02ca38095f9b805795dc48",
|
||||
"https://deno.land/std@0.208.0/assert/_format.ts": "a69126e8a469009adf4cf2a50af889aca364c349797e63174884a52ff75cf4c7",
|
||||
"https://deno.land/std@0.208.0/assert/assert.ts": "9a97dad6d98c238938e7540736b826440ad8c1c1e54430ca4c4e623e585607ee",
|
||||
"https://deno.land/std@0.208.0/assert/assert_almost_equals.ts": "e15ca1f34d0d5e0afae63b3f5d975cbd18335a132e42b0c747d282f62ad2cd6c",
|
||||
"https://deno.land/std@0.208.0/assert/assert_array_includes.ts": "6856d7f2c3544bc6e62fb4646dfefa3d1df5ff14744d1bca19f0cbaf3b0d66c9",
|
||||
"https://deno.land/std@0.208.0/assert/assert_equals.ts": "d8ec8a22447fbaf2fc9d7c3ed2e66790fdb74beae3e482855d75782218d68227",
|
||||
"https://deno.land/std@0.208.0/assert/assert_exists.ts": "407cb6b9fb23a835cd8d5ad804e2e2edbbbf3870e322d53f79e1c7a512e2efd7",
|
||||
"https://deno.land/std@0.208.0/assert/assert_false.ts": "0ccbcaae910f52c857192ff16ea08bda40fdc79de80846c206bfc061e8c851c6",
|
||||
"https://deno.land/std@0.208.0/assert/assert_greater.ts": "ae2158a2d19313bf675bf7251d31c6dc52973edb12ac64ac8fc7064152af3e63",
|
||||
"https://deno.land/std@0.208.0/assert/assert_greater_or_equal.ts": "1439da5ebbe20855446cac50097ac78b9742abe8e9a43e7de1ce1426d556e89c",
|
||||
"https://deno.land/std@0.208.0/assert/assert_instance_of.ts": "3aedb3d8186e120812d2b3a5dea66a6e42bf8c57a8bd927645770bd21eea554c",
|
||||
"https://deno.land/std@0.208.0/assert/assert_is_error.ts": "c21113094a51a296ffaf036767d616a78a2ae5f9f7bbd464cd0197476498b94b",
|
||||
"https://deno.land/std@0.208.0/assert/assert_less.ts": "aec695db57db42ec3e2b62e97e1e93db0063f5a6ec133326cc290ff4b71b47e4",
|
||||
"https://deno.land/std@0.208.0/assert/assert_less_or_equal.ts": "5fa8b6a3ffa20fd0a05032fe7257bf985d207b85685fdbcd23651b70f928c848",
|
||||
"https://deno.land/std@0.208.0/assert/assert_match.ts": "c4083f80600bc190309903c95e397a7c9257ff8b5ae5c7ef91e834704e672e9b",
|
||||
"https://deno.land/std@0.208.0/assert/assert_not_equals.ts": "9f1acab95bd1f5fc9a1b17b8027d894509a745d91bac1718fdab51dc76831754",
|
||||
"https://deno.land/std@0.208.0/assert/assert_not_instance_of.ts": "0c14d3dfd9ab7a5276ed8ed0b18c703d79a3d106102077ec437bfe7ed912bd22",
|
||||
"https://deno.land/std@0.208.0/assert/assert_not_match.ts": "3796a5b0c57a1ce6c1c57883dd4286be13a26f715ea662318ab43a8491a13ab0",
|
||||
"https://deno.land/std@0.208.0/assert/assert_not_strict_equals.ts": "4cdef83df17488df555c8aac1f7f5ec2b84ad161b6d0645ccdbcc17654e80c99",
|
||||
"https://deno.land/std@0.208.0/assert/assert_object_match.ts": "d8fc2867cfd92eeacf9cea621e10336b666de1874a6767b5ec48988838370b54",
|
||||
"https://deno.land/std@0.208.0/assert/assert_rejects.ts": "45c59724de2701e3b1f67c391d6c71c392363635aad3f68a1b3408f9efca0057",
|
||||
"https://deno.land/std@0.208.0/assert/assert_strict_equals.ts": "b1f538a7ea5f8348aeca261d4f9ca603127c665e0f2bbfeb91fa272787c87265",
|
||||
"https://deno.land/std@0.208.0/assert/assert_string_includes.ts": "b821d39ebf5cb0200a348863c86d8c4c4b398e02012ce74ad15666fc4b631b0c",
|
||||
"https://deno.land/std@0.208.0/assert/assert_throws.ts": "63784e951475cb7bdfd59878cd25a0931e18f6dc32a6077c454b2cd94f4f4bcd",
|
||||
"https://deno.land/std@0.208.0/assert/assertion_error.ts": "4d0bde9b374dfbcbe8ac23f54f567b77024fb67dbb1906a852d67fe050d42f56",
|
||||
"https://deno.land/std@0.208.0/assert/equal.ts": "9f1a46d5993966d2596c44e5858eec821859b45f783a5ee2f7a695dfc12d8ece",
|
||||
"https://deno.land/std@0.208.0/assert/fail.ts": "c36353d7ae6e1f7933d45f8ea51e358c8c4b67d7e7502028598fe1fea062e278",
|
||||
"https://deno.land/std@0.208.0/assert/mod.ts": "37c49a26aae2b254bbe25723434dc28cd7532e444cf0b481a97c045d110ec085",
|
||||
"https://deno.land/std@0.208.0/assert/unimplemented.ts": "d56fbeecb1f108331a380f72e3e010a1f161baa6956fd0f7cf3e095ae1a4c75a",
|
||||
"https://deno.land/std@0.208.0/assert/unreachable.ts": "4600dc0baf7d9c15a7f7d234f00c23bca8f3eba8b140286aaca7aa998cf9a536",
|
||||
"https://deno.land/std@0.208.0/fmt/colors.ts": "34b3f77432925eb72cf0bfb351616949746768620b8e5ead66da532f93d10ba2",
|
||||
"https://deno.land/std@0.208.0/path/_common/assert_path.ts": "061e4d093d4ba5aebceb2c4da3318bfe3289e868570e9d3a8e327d91c2958946",
|
||||
"https://deno.land/std@0.208.0/path/_common/basename.ts": "0d978ff818f339cd3b1d09dc914881f4d15617432ae519c1b8fdc09ff8d3789a",
|
||||
"https://deno.land/std@0.208.0/path/_common/common.ts": "9e4233b2eeb50f8b2ae10ecc2108f58583aea6fd3e8907827020282dc2b76143",
|
||||
"https://deno.land/std@0.208.0/path/_common/constants.ts": "e49961f6f4f48039c0dfed3c3f93e963ca3d92791c9d478ac5b43183413136e0",
|
||||
"https://deno.land/std@0.208.0/path/_common/dirname.ts": "2ba7fb4cc9fafb0f38028f434179579ce61d4d9e51296fad22b701c3d3cd7397",
|
||||
"https://deno.land/std@0.208.0/path/_common/format.ts": "11aa62e316dfbf22c126917f5e03ea5fe2ee707386555a8f513d27ad5756cf96",
|
||||
"https://deno.land/std@0.208.0/path/_common/from_file_url.ts": "ef1bf3197d2efbf0297a2bdbf3a61d804b18f2bcce45548ae112313ec5be3c22",
|
||||
"https://deno.land/std@0.208.0/path/_common/glob_to_reg_exp.ts": "5c3c2b79fc2294ec803d102bd9855c451c150021f452046312819fbb6d4dc156",
|
||||
"https://deno.land/std@0.208.0/path/_common/normalize.ts": "2ba7fb4cc9fafb0f38028f434179579ce61d4d9e51296fad22b701c3d3cd7397",
|
||||
"https://deno.land/std@0.208.0/path/_common/normalize_string.ts": "88c472f28ae49525f9fe82de8c8816d93442d46a30d6bb5063b07ff8a89ff589",
|
||||
"https://deno.land/std@0.208.0/path/_common/relative.ts": "1af19d787a2a84b8c534cc487424fe101f614982ae4851382c978ab2216186b4",
|
||||
"https://deno.land/std@0.208.0/path/_common/strip_trailing_separators.ts": "7ffc7c287e97bdeeee31b155828686967f222cd73f9e5780bfe7dfb1b58c6c65",
|
||||
"https://deno.land/std@0.208.0/path/_common/to_file_url.ts": "a8cdd1633bc9175b7eebd3613266d7c0b6ae0fb0cff24120b6092ac31662f9ae",
|
||||
"https://deno.land/std@0.208.0/path/_interface.ts": "6471159dfbbc357e03882c2266d21ef9afdb1e4aa771b0545e90db58a0ba314b",
|
||||
"https://deno.land/std@0.208.0/path/_os.ts": "30b0c2875f360c9296dbe6b7f2d528f0f9c741cecad2e97f803f5219e91b40a2",
|
||||
"https://deno.land/std@0.208.0/path/basename.ts": "04bb5ef3e86bba8a35603b8f3b69537112cdd19ce64b77f2522006da2977a5f3",
|
||||
"https://deno.land/std@0.208.0/path/common.ts": "f4d061c7d0b95a65c2a1a52439edec393e906b40f1caf4604c389fae7caa80f5",
|
||||
"https://deno.land/std@0.208.0/path/dirname.ts": "88a0a71c21debafc4da7a4cd44fd32e899462df458fbca152390887d41c40361",
|
||||
"https://deno.land/std@0.208.0/path/extname.ts": "2da4e2490f3b48b7121d19fb4c91681a5e11bd6bd99df4f6f47d7a71bb6ecdf2",
|
||||
"https://deno.land/std@0.208.0/path/format.ts": "3457530cc85d1b4bab175f9ae73998b34fd456c830d01883169af0681b8894fb",
|
||||
"https://deno.land/std@0.208.0/path/from_file_url.ts": "e7fa233ea1dff9641e8d566153a24d95010110185a6f418dd2e32320926043f8",
|
||||
"https://deno.land/std@0.208.0/path/glob_to_regexp.ts": "74d7448c471e293d03f05ccb968df4365fed6aaa508506b6325a8efdc01d8271",
|
||||
"https://deno.land/std@0.208.0/path/is_absolute.ts": "67232b41b860571c5b7537f4954c88d86ae2ba45e883ee37d3dec27b74909d13",
|
||||
"https://deno.land/std@0.208.0/path/is_glob.ts": "567dce5c6656bdedfc6b3ee6c0833e1e4db2b8dff6e62148e94a917f289c06ad",
|
||||
"https://deno.land/std@0.208.0/path/join.ts": "98d3d76c819af4a11a81d5ba2dbb319f1ce9d63fc2b615597d4bcfddd4a89a09",
|
||||
"https://deno.land/std@0.208.0/path/join_globs.ts": "9b84d5103b63d3dbed4b2cf8b12477b2ad415c7d343f1488505162dc0e5f4db8",
|
||||
"https://deno.land/std@0.208.0/path/mod.ts": "3defabebc98279e62b392fee7a6937adc932a8f4dcd2471441e36c15b97b00e0",
|
||||
"https://deno.land/std@0.208.0/path/normalize.ts": "aa95be9a92c7bd4f9dc0ba51e942a1973e2b93d266cd74f5ca751c136d520b66",
|
||||
"https://deno.land/std@0.208.0/path/normalize_glob.ts": "674baa82e1c00b6cb153bbca36e06f8e0337cb8062db6d905ab5de16076ca46b",
|
||||
"https://deno.land/std@0.208.0/path/parse.ts": "d87ff0deef3fb495bc0d862278ff96da5a06acf0625ca27769fc52ac0d3d6ece",
|
||||
"https://deno.land/std@0.208.0/path/posix/_util.ts": "ecf49560fedd7dd376c6156cc5565cad97c1abe9824f4417adebc7acc36c93e5",
|
||||
"https://deno.land/std@0.208.0/path/posix/basename.ts": "a630aeb8fd8e27356b1823b9dedd505e30085015407caa3396332752f6b8406a",
|
||||
"https://deno.land/std@0.208.0/path/posix/common.ts": "e781d395dc76f6282e3f7dd8de13194abb8b04a82d109593141abc6e95755c8b",
|
||||
"https://deno.land/std@0.208.0/path/posix/dirname.ts": "f48c9c42cc670803b505478b7ef162c7cfa9d8e751b59d278b2ec59470531472",
|
||||
"https://deno.land/std@0.208.0/path/posix/extname.ts": "ee7f6571a9c0a37f9218fbf510c440d1685a7c13082c348d701396cc795e0be0",
|
||||
"https://deno.land/std@0.208.0/path/posix/format.ts": "b94876f77e61bfe1f147d5ccb46a920636cd3cef8be43df330f0052b03875968",
|
||||
"https://deno.land/std@0.208.0/path/posix/from_file_url.ts": "b97287a83e6407ac27bdf3ab621db3fccbf1c27df0a1b1f20e1e1b5acf38a379",
|
||||
"https://deno.land/std@0.208.0/path/posix/glob_to_regexp.ts": "6ed00c71fbfe0ccc35977c35444f94e82200b721905a60bd1278b1b768d68b1a",
|
||||
"https://deno.land/std@0.208.0/path/posix/is_absolute.ts": "159900a3422d11069d48395568217eb7fc105ceda2683d03d9b7c0f0769e01b8",
|
||||
"https://deno.land/std@0.208.0/path/posix/is_glob.ts": "ec4fbc604b9db8487f7b56ab0e759b24a971ab6a45f7b0b698bc39b8b9f9680f",
|
||||
"https://deno.land/std@0.208.0/path/posix/join.ts": "0c0d84bdc344876930126640011ec1b888e6facf74153ffad9ef26813aa2a076",
|
||||
"https://deno.land/std@0.208.0/path/posix/join_globs.ts": "f4838d54b1f60a34a40625a3293f6e583135348be1b2974341ac04743cb26121",
|
||||
"https://deno.land/std@0.208.0/path/posix/mod.ts": "f1b08a7f64294b7de87fc37190d63b6ce5b02889af9290c9703afe01951360ae",
|
||||
"https://deno.land/std@0.208.0/path/posix/normalize.ts": "11de90a94ab7148cc46e5a288f7d732aade1d616bc8c862f5560fa18ff987b4b",
|
||||
"https://deno.land/std@0.208.0/path/posix/normalize_glob.ts": "10a1840c628ebbab679254d5fa1c20e59106102354fb648a1765aed72eb9f3f9",
|
||||
"https://deno.land/std@0.208.0/path/posix/parse.ts": "199208f373dd93a792e9c585352bfc73a6293411bed6da6d3bc4f4ef90b04c8e",
|
||||
"https://deno.land/std@0.208.0/path/posix/relative.ts": "e2f230608b0f083e6deaa06e063943e5accb3320c28aef8d87528fbb7fe6504c",
|
||||
"https://deno.land/std@0.208.0/path/posix/resolve.ts": "51579d83159d5c719518c9ae50812a63959bbcb7561d79acbdb2c3682236e285",
|
||||
"https://deno.land/std@0.208.0/path/posix/separator.ts": "0b6573b5f3269a3164d8edc9cefc33a02dd51003731c561008c8bb60220ebac1",
|
||||
"https://deno.land/std@0.208.0/path/posix/to_file_url.ts": "08d43ea839ee75e9b8b1538376cfe95911070a655cd312bc9a00f88ef14967b6",
|
||||
"https://deno.land/std@0.208.0/path/posix/to_namespaced_path.ts": "c9228a0e74fd37e76622cd7b142b8416663a9b87db643302fa0926b5a5c83bdc",
|
||||
"https://deno.land/std@0.208.0/path/relative.ts": "23d45ede8b7ac464a8299663a43488aad6b561414e7cbbe4790775590db6349c",
|
||||
"https://deno.land/std@0.208.0/path/resolve.ts": "5b184efc87155a0af9fa305ff68a109e28de9aee81fc3e77cd01380f19daf867",
|
||||
"https://deno.land/std@0.208.0/path/separator.ts": "40a3e9a4ad10bef23bc2cd6c610291b6c502a06237c2c4cd034a15ca78dedc1f",
|
||||
"https://deno.land/std@0.208.0/path/to_file_url.ts": "edaafa089e0bce386e1b2d47afe7c72e379ff93b28a5829a5885e4b6c626d864",
|
||||
"https://deno.land/std@0.208.0/path/to_namespaced_path.ts": "cf8734848aac3c7527d1689d2adf82132b1618eff3cc523a775068847416b22a",
|
||||
"https://deno.land/std@0.208.0/path/windows/_util.ts": "f32b9444554c8863b9b4814025c700492a2b57ff2369d015360970a1b1099d54",
|
||||
"https://deno.land/std@0.208.0/path/windows/basename.ts": "8a9dbf7353d50afbc5b221af36c02a72c2d1b2b5b9f7c65bf6a5a2a0baf88ad3",
|
||||
"https://deno.land/std@0.208.0/path/windows/common.ts": "e781d395dc76f6282e3f7dd8de13194abb8b04a82d109593141abc6e95755c8b",
|
||||
"https://deno.land/std@0.208.0/path/windows/dirname.ts": "5c2aa541384bf0bd9aca821275d2a8690e8238fa846198ef5c7515ce31a01a94",
|
||||
"https://deno.land/std@0.208.0/path/windows/extname.ts": "07f4fa1b40d06a827446b3e3bcc8d619c5546b079b8ed0c77040bbef716c7614",
|
||||
"https://deno.land/std@0.208.0/path/windows/format.ts": "343019130d78f172a5c49fdc7e64686a7faf41553268961e7b6c92a6d6548edf",
|
||||
"https://deno.land/std@0.208.0/path/windows/from_file_url.ts": "d53335c12b0725893d768be3ac6bf0112cc5b639d2deb0171b35988493b46199",
|
||||
"https://deno.land/std@0.208.0/path/windows/glob_to_regexp.ts": "290755e18ec6c1a4f4d711c3390537358e8e3179581e66261a0cf348b1a13395",
|
||||
"https://deno.land/std@0.208.0/path/windows/is_absolute.ts": "245b56b5f355ede8664bd7f080c910a97e2169972d23075554ae14d73722c53c",
|
||||
"https://deno.land/std@0.208.0/path/windows/is_glob.ts": "ec4fbc604b9db8487f7b56ab0e759b24a971ab6a45f7b0b698bc39b8b9f9680f",
|
||||
"https://deno.land/std@0.208.0/path/windows/join.ts": "e6600bf88edeeef4e2276e155b8de1d5dec0435fd526ba2dc4d37986b2882f16",
|
||||
"https://deno.land/std@0.208.0/path/windows/join_globs.ts": "f4838d54b1f60a34a40625a3293f6e583135348be1b2974341ac04743cb26121",
|
||||
"https://deno.land/std@0.208.0/path/windows/mod.ts": "d7040f461465c2c21c1c68fc988ef0bdddd499912138cde3abf6ad60c7fb3814",
|
||||
"https://deno.land/std@0.208.0/path/windows/normalize.ts": "9deebbf40c81ef540b7b945d4ccd7a6a2c5a5992f791e6d3377043031e164e69",
|
||||
"https://deno.land/std@0.208.0/path/windows/normalize_glob.ts": "344ff5ed45430495b9a3d695567291e50e00b1b3b04ea56712a2acf07ab5c128",
|
||||
"https://deno.land/std@0.208.0/path/windows/parse.ts": "120faf778fe1f22056f33ded069b68e12447668fcfa19540c0129561428d3ae5",
|
||||
"https://deno.land/std@0.208.0/path/windows/relative.ts": "026855cd2c36c8f28f1df3c6fbd8f2449a2aa21f48797a74700c5d872b86d649",
|
||||
"https://deno.land/std@0.208.0/path/windows/resolve.ts": "5ff441ab18a2346abadf778121128ee71bda4d0898513d4639a6ca04edca366b",
|
||||
"https://deno.land/std@0.208.0/path/windows/separator.ts": "ae21f27015f10510ed1ac4a0ba9c4c9c967cbdd9d9e776a3e4967553c397bd5d",
|
||||
"https://deno.land/std@0.208.0/path/windows/to_file_url.ts": "8e9ea9e1ff364aa06fa72999204229952d0a279dbb876b7b838b2b2fea55cce3",
|
||||
"https://deno.land/std@0.208.0/path/windows/to_namespaced_path.ts": "e0f4d4a5e77f28a5708c1a33ff24360f35637ba6d8f103d19661255ef7bfd50d",
|
||||
"https://deno.land/std@0.208.0/testing/asserts.ts": "605bbd2ef0695e2a4324d810c4ad22e56041d51afb9584fc0b4e81084b14b1d6",
|
||||
"https://deno.land/std@0.213.0/assert/_constants.ts": "a271e8ef5a573f1df8e822a6eb9d09df064ad66a4390f21b3e31f820a38e0975",
|
||||
"https://deno.land/std@0.213.0/assert/_diff.ts": "dcc63d94ca289aec80644030cf88ccbf7acaa6fbd7b0f22add93616b36593840",
|
||||
"https://deno.land/std@0.213.0/assert/_format.ts": "0ba808961bf678437fb486b56405b6fefad2cf87b5809667c781ddee8c32aff4",
|
||||
"https://deno.land/std@0.213.0/assert/assert.ts": "bec068b2fccdd434c138a555b19a2c2393b71dfaada02b7d568a01541e67cdc5",
|
||||
"https://deno.land/std@0.213.0/assert/assert_almost_equals.ts": "8b96b7385cc117668b0720115eb6ee73d04c9bcb2f5d2344d674918c9113688f",
|
||||
"https://deno.land/std@0.213.0/assert/assert_array_includes.ts": "1688d76317fd45b7e93ef9e2765f112fdf2b7c9821016cdfb380b9445374aed1",
|
||||
"https://deno.land/std@0.213.0/assert/assert_equals.ts": "4497c56fe7d2993b0d447926702802fc0becb44e319079e8eca39b482ee01b4e",
|
||||
"https://deno.land/std@0.213.0/assert/assert_exists.ts": "24a7bf965e634f909242cd09fbaf38bde6b791128ece08e33ab08586a7cc55c9",
|
||||
"https://deno.land/std@0.213.0/assert/assert_false.ts": "6f382568e5128c0f855e5f7dbda8624c1ed9af4fcc33ef4a9afeeedcdce99769",
|
||||
"https://deno.land/std@0.213.0/assert/assert_greater.ts": "4945cf5729f1a38874d7e589e0fe5cc5cd5abe5573ca2ddca9d3791aa891856c",
|
||||
"https://deno.land/std@0.213.0/assert/assert_greater_or_equal.ts": "573ed8823283b8d94b7443eb69a849a3c369a8eb9666b2d1db50c33763a5d219",
|
||||
"https://deno.land/std@0.213.0/assert/assert_instance_of.ts": "72dc1faff1e248692d873c89382fa1579dd7b53b56d52f37f9874a75b11ba444",
|
||||
"https://deno.land/std@0.213.0/assert/assert_is_error.ts": "6596f2b5ba89ba2fe9b074f75e9318cda97a2381e59d476812e30077fbdb6ed2",
|
||||
"https://deno.land/std@0.213.0/assert/assert_less.ts": "2b4b3fe7910f65f7be52212f19c3977ecb8ba5b2d6d0a296c83cde42920bb005",
|
||||
"https://deno.land/std@0.213.0/assert/assert_less_or_equal.ts": "b93d212fe669fbde959e35b3437ac9a4468f2e6b77377e7b6ea2cfdd825d38a0",
|
||||
"https://deno.land/std@0.213.0/assert/assert_match.ts": "ec2d9680ed3e7b9746ec57ec923a17eef6d476202f339ad91d22277d7f1d16e1",
|
||||
"https://deno.land/std@0.213.0/assert/assert_not_equals.ts": "f3edda73043bc2c9fae6cbfaa957d5c69bbe76f5291a5b0466ed132c8789df4c",
|
||||
"https://deno.land/std@0.213.0/assert/assert_not_instance_of.ts": "8f720d92d83775c40b2542a8d76c60c2d4aeddaf8713c8d11df8984af2604931",
|
||||
"https://deno.land/std@0.213.0/assert/assert_not_match.ts": "b4b7c77f146963e2b673c1ce4846473703409eb93f5ab0eb60f6e6f8aeffe39f",
|
||||
"https://deno.land/std@0.213.0/assert/assert_not_strict_equals.ts": "da0b8ab60a45d5a9371088378e5313f624799470c3b54c76e8b8abeec40a77be",
|
||||
"https://deno.land/std@0.213.0/assert/assert_object_match.ts": "e85e5eef62a56ce364c3afdd27978ccab979288a3e772e6855c270a7b118fa49",
|
||||
"https://deno.land/std@0.213.0/assert/assert_rejects.ts": "e9e0c8d9c3e164c7ac962c37b3be50577c5a2010db107ed272c4c1afb1269f54",
|
||||
"https://deno.land/std@0.213.0/assert/assert_strict_equals.ts": "0425a98f70badccb151644c902384c12771a93e65f8ff610244b8147b03a2366",
|
||||
"https://deno.land/std@0.213.0/assert/assert_string_includes.ts": "dfb072a890167146f8e5bdd6fde887ce4657098e9f71f12716ef37f35fb6f4a7",
|
||||
"https://deno.land/std@0.213.0/assert/assert_throws.ts": "edddd86b39606c342164b49ad88dd39a26e72a26655e07545d172f164b617fa7",
|
||||
"https://deno.land/std@0.213.0/assert/assertion_error.ts": "9f689a101ee586c4ce92f52fa7ddd362e86434ffdf1f848e45987dc7689976b8",
|
||||
"https://deno.land/std@0.213.0/assert/equal.ts": "fae5e8a52a11d3ac694bbe1a53e13a7969e3f60791262312e91a3e741ae519e2",
|
||||
"https://deno.land/std@0.213.0/assert/fail.ts": "f310e51992bac8e54f5fd8e44d098638434b2edb802383690e0d7a9be1979f1c",
|
||||
"https://deno.land/std@0.213.0/assert/mod.ts": "325df8c0683ad83a873b9691aa66b812d6275fc9fec0b2d180ac68a2c5efed3b",
|
||||
"https://deno.land/std@0.213.0/assert/unimplemented.ts": "47ca67d1c6dc53abd0bd729b71a31e0825fc452dbcd4fde4ca06789d5644e7fd",
|
||||
"https://deno.land/std@0.213.0/assert/unreachable.ts": "38cfecb95d8b06906022d2f9474794fca4161a994f83354fd079cac9032b5145",
|
||||
"https://deno.land/std@0.213.0/fmt/colors.ts": "aeaee795471b56fc62a3cb2e174ed33e91551b535f44677f6320336aabb54fbb",
|
||||
"https://deno.land/std@0.213.0/testing/_test_suite.ts": "f10a8a6338b60c403f07a76f3f46bdc9f1e1a820c0a1decddeb2949f7a8a0546",
|
||||
"https://deno.land/std@0.213.0/testing/bdd.ts": "3cbd17bd35f629a76ce63446238dfb4632240dd46b3b205027c45fa3dd67e554",
|
||||
"https://deno.land/std@0.224.0/assert/_constants.ts": "a271e8ef5a573f1df8e822a6eb9d09df064ad66a4390f21b3e31f820a38e0975",
|
||||
"https://deno.land/std@0.224.0/assert/assert.ts": "09d30564c09de846855b7b071e62b5974b001bb72a4b797958fe0660e7849834",
|
||||
"https://deno.land/std@0.224.0/assert/assert_almost_equals.ts": "9e416114322012c9a21fa68e187637ce2d7df25bcbdbfd957cd639e65d3cf293",
|
||||
"https://deno.land/std@0.224.0/assert/assert_array_includes.ts": "14c5094471bc8e4a7895fc6aa5a184300d8a1879606574cb1cd715ef36a4a3c7",
|
||||
"https://deno.land/std@0.224.0/assert/assert_equals.ts": "3bbca947d85b9d374a108687b1a8ba3785a7850436b5a8930d81f34a32cb8c74",
|
||||
"https://deno.land/std@0.224.0/assert/assert_exists.ts": "43420cf7f956748ae6ed1230646567b3593cb7a36c5a5327269279c870c5ddfd",
|
||||
"https://deno.land/std@0.224.0/assert/assert_false.ts": "3e9be8e33275db00d952e9acb0cd29481a44fa0a4af6d37239ff58d79e8edeff",
|
||||
"https://deno.land/std@0.224.0/assert/assert_greater.ts": "5e57b201fd51b64ced36c828e3dfd773412c1a6120c1a5a99066c9b261974e46",
|
||||
"https://deno.land/std@0.224.0/assert/assert_greater_or_equal.ts": "9870030f997a08361b6f63400273c2fb1856f5db86c0c3852aab2a002e425c5b",
|
||||
"https://deno.land/std@0.224.0/assert/assert_instance_of.ts": "e22343c1fdcacfaea8f37784ad782683ec1cf599ae9b1b618954e9c22f376f2c",
|
||||
"https://deno.land/std@0.224.0/assert/assert_is_error.ts": "f856b3bc978a7aa6a601f3fec6603491ab6255118afa6baa84b04426dd3cc491",
|
||||
"https://deno.land/std@0.224.0/assert/assert_less.ts": "60b61e13a1982865a72726a5fa86c24fad7eb27c3c08b13883fb68882b307f68",
|
||||
"https://deno.land/std@0.224.0/assert/assert_less_or_equal.ts": "d2c84e17faba4afe085e6c9123a63395accf4f9e00150db899c46e67420e0ec3",
|
||||
"https://deno.land/std@0.224.0/assert/assert_match.ts": "ace1710dd3b2811c391946954234b5da910c5665aed817943d086d4d4871a8b7",
|
||||
"https://deno.land/std@0.224.0/assert/assert_not_equals.ts": "78d45dd46133d76ce624b2c6c09392f6110f0df9b73f911d20208a68dee2ef29",
|
||||
"https://deno.land/std@0.224.0/assert/assert_not_instance_of.ts": "3434a669b4d20cdcc5359779301a0588f941ffdc2ad68803c31eabdb4890cf7a",
|
||||
"https://deno.land/std@0.224.0/assert/assert_not_match.ts": "df30417240aa2d35b1ea44df7e541991348a063d9ee823430e0b58079a72242a",
|
||||
"https://deno.land/std@0.224.0/assert/assert_not_strict_equals.ts": "37f73880bd672709373d6dc2c5f148691119bed161f3020fff3548a0496f71b8",
|
||||
"https://deno.land/std@0.224.0/assert/assert_object_match.ts": "411450fd194fdaabc0089ae68f916b545a49d7b7e6d0026e84a54c9e7eed2693",
|
||||
"https://deno.land/std@0.224.0/assert/assert_rejects.ts": "4bee1d6d565a5b623146a14668da8f9eb1f026a4f338bbf92b37e43e0aa53c31",
|
||||
"https://deno.land/std@0.224.0/assert/assert_strict_equals.ts": "b4f45f0fd2e54d9029171876bd0b42dd9ed0efd8f853ab92a3f50127acfa54f5",
|
||||
"https://deno.land/std@0.224.0/assert/assert_string_includes.ts": "496b9ecad84deab72c8718735373feb6cdaa071eb91a98206f6f3cb4285e71b8",
|
||||
"https://deno.land/std@0.224.0/assert/assert_throws.ts": "c6508b2879d465898dab2798009299867e67c570d7d34c90a2d235e4553906eb",
|
||||
"https://deno.land/std@0.224.0/assert/assertion_error.ts": "ba8752bd27ebc51f723702fac2f54d3e94447598f54264a6653d6413738a8917",
|
||||
"https://deno.land/std@0.224.0/assert/equal.ts": "bddf07bb5fc718e10bb72d5dc2c36c1ce5a8bdd3b647069b6319e07af181ac47",
|
||||
"https://deno.land/std@0.224.0/assert/fail.ts": "0eba674ffb47dff083f02ced76d5130460bff1a9a68c6514ebe0cdea4abadb68",
|
||||
"https://deno.land/std@0.224.0/assert/mod.ts": "48b8cb8a619ea0b7958ad7ee9376500fe902284bb36f0e32c598c3dc34cbd6f3",
|
||||
"https://deno.land/std@0.224.0/assert/unimplemented.ts": "8c55a5793e9147b4f1ef68cd66496b7d5ba7a9e7ca30c6da070c1a58da723d73",
|
||||
"https://deno.land/std@0.224.0/assert/unreachable.ts": "5ae3dbf63ef988615b93eb08d395dda771c96546565f9e521ed86f6510c29e19",
|
||||
"https://deno.land/std@0.224.0/cli/parse_args.ts": "5250832fb7c544d9111e8a41ad272c016f5a53f975ef84d5a9fe5fcb70566ece",
|
||||
"https://deno.land/std@0.224.0/fmt/colors.ts": "508563c0659dd7198ba4bbf87e97f654af3c34eb56ba790260f252ad8012e1c5",
|
||||
"https://deno.land/std@0.224.0/internal/diff.ts": "6234a4b493ebe65dc67a18a0eb97ef683626a1166a1906232ce186ae9f65f4e6",
|
||||
"https://deno.land/std@0.224.0/internal/format.ts": "0a98ee226fd3d43450245b1844b47003419d34d210fa989900861c79820d21c2",
|
||||
"https://deno.land/std@0.224.0/internal/mod.ts": "534125398c8e7426183e12dc255bb635d94e06d0f93c60a297723abe69d3b22e",
|
||||
"https://deno.land/std@0.224.0/yaml/_dumper/dumper.ts": "08b595b40841a2e1c75303f5096392323b6baf8e9662430a91e3b36fbe175fe9",
|
||||
"https://deno.land/std@0.224.0/yaml/_dumper/dumper_state.ts": "9e29f700ea876ed230b43f11fa006fcb1a62eedc1e27d32baaeaf3210f19f1e7",
|
||||
"https://deno.land/std@0.224.0/yaml/_error.ts": "f38cdebdb69cde16903d9aa2f3b8a3dd9d13e5f7f3570bf662bfaca69fef669e",
|
||||
"https://deno.land/std@0.224.0/yaml/_loader/loader.ts": "bf9e8a99770b59bc887b43ebccea108cbe9146ae32d91f7ce558d62c946d3fe3",
|
||||
"https://deno.land/std@0.224.0/yaml/_loader/loader_state.ts": "ee216de6040551940b85473c3185fdb7a6f3030b77153f87a6b7f63f82e489ea",
|
||||
"https://deno.land/std@0.224.0/yaml/_mark.ts": "61097a614857fcebf7b2ecad057916d74c90cd160117a33c9e74bac60457410a",
|
||||
"https://deno.land/std@0.224.0/yaml/_state.ts": "f3b1c1fd11860302f1f33e35e9ce089bf069d4943e8d67516cd6bedbba058c13",
|
||||
"https://deno.land/std@0.224.0/yaml/_type/binary.ts": "f1a6e1d83dcc52b21cc3639cd98be44051cfc54065cc4f2a42065bce07ebc07d",
|
||||
"https://deno.land/std@0.224.0/yaml/_type/bool.ts": "121743b23ba82a27ad6a3ec6298c7f5b0908f90e52707f8644a91f7ad51ed2ef",
|
||||
"https://deno.land/std@0.224.0/yaml/_type/float.ts": "c5ed84b0aec1ec5dc05f6abfaaff672e8890d4d44a42120b4445c9754fca4eba",
|
||||
"https://deno.land/std@0.224.0/yaml/_type/function.ts": "bbf705058942bf3370604b37eb77a10aadd72f986c237c9f69b43378a42202c1",
|
||||
"https://deno.land/std@0.224.0/yaml/_type/int.ts": "c2dc88438a60fccc8d2226042bd18b9967753adaf6bd145feb8b99d567e432ce",
|
||||
"https://deno.land/std@0.224.0/yaml/_type/map.ts": "ae2acb1cb837fb8e96c75c98611cfd45af847d0114ab5336333c318e7d4b12f4",
|
||||
"https://deno.land/std@0.224.0/yaml/_type/merge.ts": "ad0d971f91d2fb9f4ab3eba0c837eae357b1804d6b798adc99dc917bc5306b11",
|
||||
"https://deno.land/std@0.224.0/yaml/_type/mod.ts": "e8929d7b1c969a74f76338d4eb380ef8c4a26cd6441117d521f076b766e9c265",
|
||||
"https://deno.land/std@0.224.0/yaml/_type/nil.ts": "cbe4387d02d5933322c21b25d8955c5e6228c492e391a6fb82dcf4f498cc421c",
|
||||
"https://deno.land/std@0.224.0/yaml/_type/omap.ts": "cda915105ab22ba9e1d6317adacee8eec2d8ddaf864cc2f814e3e476946e72c6",
|
||||
"https://deno.land/std@0.224.0/yaml/_type/pairs.ts": "dd39bb44c1b9abaf6172c63f73350475933151f07e05253b81f7860c9b507177",
|
||||
"https://deno.land/std@0.224.0/yaml/_type/regexp.ts": "e49eb9e1c9356fd142bc15f7f323820d411fcc537b5ba3896df9a8b812d270a4",
|
||||
"https://deno.land/std@0.224.0/yaml/_type/seq.ts": "2deffc7f970869bc01a1541b4961d076329a1c2b30b95e07918f3132db7c3fe2",
|
||||
"https://deno.land/std@0.224.0/yaml/_type/set.ts": "be8a9e7237a7ffc92dfbe7f5e552d84b7eeba60f3f73cc77fc3c59d3506c74ea",
|
||||
"https://deno.land/std@0.224.0/yaml/_type/str.ts": "88f0a1ba12295520cd57e96cd78d53aa0787d53c7a1c506155f418c496c2f550",
|
||||
"https://deno.land/std@0.224.0/yaml/_type/timestamp.ts": "277a41a40fb93c3b2b3f5c373bf11b0b7856cc6a7b919e8ea130755e4029edc5",
|
||||
"https://deno.land/std@0.224.0/yaml/_type/undefined.ts": "9d215953c65740f1764e0bdca021007573473f0c49e087f00d9ff02817ecfc97",
|
||||
"https://deno.land/std@0.224.0/yaml/_utils.ts": "91bbe28b5e7000b9594e40ff5353f8fe7a7ba914eec917e1202cbaf5ac931c58",
|
||||
"https://deno.land/std@0.224.0/yaml/mod.ts": "54e9bfad77c8cd58f49b65f4d568045ff08989ed36318a2ca733a43cb6f1bc00",
|
||||
"https://deno.land/std@0.224.0/yaml/parse.ts": "f45278d9ebccb789af4eceeffa5c291e194bcf1fa9aab1b34ff52c2bd4a9d886",
|
||||
"https://deno.land/std@0.224.0/yaml/schema.ts": "a0f7956d997852b5d1c6564bd73eb7352175cfba439707ac819b65b5a2ec173a",
|
||||
"https://deno.land/std@0.224.0/yaml/schema/core.ts": "0a37c07710e3df4eb4edc02f4edf623bf8df5af72b34d8a7c0229d0bac2a7043",
|
||||
"https://deno.land/std@0.224.0/yaml/schema/default.ts": "1367fd30420c7071ecc67e5b470838474e8259aaf64460f314af4b6bd8da497c",
|
||||
"https://deno.land/std@0.224.0/yaml/schema/extended.ts": "248180c22697f37ed173057eae62ce4879865bb59f30c4908d698bed5edcc7c5",
|
||||
"https://deno.land/std@0.224.0/yaml/schema/failsafe.ts": "0ac1cae5b86d8fe2c83ad0a17f8adc33106a452b7139f84e4b0bfaee2206730e",
|
||||
"https://deno.land/std@0.224.0/yaml/schema/json.ts": "a0228a0c0bad7dece17ab848774fcadc2ccb5e51775c2d58d21d486917ba3ba1",
|
||||
"https://deno.land/std@0.224.0/yaml/schema/mod.ts": "0e1558a4823834f106675e48ddc15338e04f6f18469d1a7d6b3f0e1ab06abcb2",
|
||||
"https://deno.land/std@0.224.0/yaml/stringify.ts": "f0ed4e419cb40c807cf79ae4039d6cdf492be9a947121fff4d4b7cd1d4738bae",
|
||||
"https://deno.land/std@0.224.0/yaml/type.ts": "708dde5f20b01cc1096489b7155b6af79a217d585afb841128e78c3c2391eb5c"
|
||||
},
|
||||
"workspace": {
|
||||
"dependencies": [
|
||||
"jsr:@deno/dnt@~0.41.3",
|
||||
|
||||
+2
-1
@@ -13,7 +13,8 @@ await build({
|
||||
},
|
||||
],
|
||||
outDir: "./npm",
|
||||
shims: {
|
||||
test: false, // Disable all tests in npm build since they use Deno-specific APIs
|
||||
shims: {
|
||||
// see JS docs for overview and more options
|
||||
deno: true,
|
||||
// shims to only use in the tests
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -365,6 +365,7 @@ async function instancePull(opts: InstanceSyncOptions) {
|
||||
workspace: workspaceName,
|
||||
token: undefined,
|
||||
baseUrl: undefined,
|
||||
configDir: undefined,
|
||||
includeGroups: true,
|
||||
includeSchedules: true,
|
||||
includeTriggers: true,
|
||||
@@ -515,6 +516,7 @@ async function instancePush(opts: InstanceSyncOptions) {
|
||||
token: instance.token,
|
||||
workspace: undefined,
|
||||
baseUrl: undefined,
|
||||
configDir: undefined,
|
||||
create: true,
|
||||
createWorkspaceName: workspaceSettings.name,
|
||||
createUsername: undefined,
|
||||
@@ -533,6 +535,7 @@ async function instancePush(opts: InstanceSyncOptions) {
|
||||
workspace: localWorkspace.dir,
|
||||
token: undefined,
|
||||
baseUrl: undefined,
|
||||
configDir: undefined,
|
||||
includeGroups: true,
|
||||
includeSchedules: true,
|
||||
includeTriggers: true,
|
||||
|
||||
+347
-194
@@ -1,11 +1,11 @@
|
||||
import {
|
||||
Command,
|
||||
CompletionsCommand,
|
||||
UpgradeCommand,
|
||||
colors,
|
||||
esMain,
|
||||
log,
|
||||
yamlStringify,
|
||||
Command,
|
||||
CompletionsCommand,
|
||||
UpgradeCommand,
|
||||
colors,
|
||||
esMain,
|
||||
log,
|
||||
yamlStringify,
|
||||
} from "./deps.ts";
|
||||
import flow from "./flow.ts";
|
||||
import app from "./apps.ts";
|
||||
@@ -19,6 +19,7 @@ import folder from "./folder.ts";
|
||||
import schedule from "./schedule.ts";
|
||||
import trigger from "./trigger.ts";
|
||||
import sync from "./sync.ts";
|
||||
import gitsyncSettings from "./gitsync-settings.ts";
|
||||
import instance from "./instance.ts";
|
||||
import workerGroups from "./worker_groups.ts";
|
||||
|
||||
@@ -36,24 +37,25 @@ import queues from "./queues.ts";
|
||||
import { readLockfile } from "./metadata.ts";
|
||||
|
||||
export {
|
||||
flow,
|
||||
app,
|
||||
script,
|
||||
workspace,
|
||||
resource,
|
||||
user,
|
||||
variable,
|
||||
hub,
|
||||
folder,
|
||||
schedule,
|
||||
trigger,
|
||||
sync,
|
||||
instance,
|
||||
dev,
|
||||
hubPull,
|
||||
pull,
|
||||
push,
|
||||
workspaceAdd,
|
||||
flow,
|
||||
app,
|
||||
script,
|
||||
workspace,
|
||||
resource,
|
||||
user,
|
||||
variable,
|
||||
hub,
|
||||
folder,
|
||||
schedule,
|
||||
trigger,
|
||||
sync,
|
||||
gitsyncSettings,
|
||||
instance,
|
||||
dev,
|
||||
hubPull,
|
||||
pull,
|
||||
push,
|
||||
workspaceAdd,
|
||||
};
|
||||
|
||||
// addEventListener("error", (event) => {
|
||||
@@ -66,198 +68,349 @@ export {
|
||||
export const VERSION = "1.506.0";
|
||||
|
||||
const command = new Command()
|
||||
.name("wmill")
|
||||
.action(() =>
|
||||
log.info(`Welcome to Windmill CLI ${VERSION}. Use -h for help.`)
|
||||
)
|
||||
.description("Windmill CLI")
|
||||
.name("wmill")
|
||||
.action(() =>
|
||||
log.info(`Welcome to Windmill CLI ${VERSION}. Use -h for help.`),
|
||||
)
|
||||
.description("Windmill CLI")
|
||||
|
||||
.globalOption(
|
||||
"--workspace <workspace:string>",
|
||||
"Specify the target workspace. This overrides the default workspace."
|
||||
)
|
||||
.globalOption("--debug --verbose", "Show debug/verbose logs")
|
||||
.globalOption(
|
||||
"--show-diffs",
|
||||
"Show diff informations when syncing (may show sensitive informations)"
|
||||
)
|
||||
.globalOption(
|
||||
"--token <token:string>",
|
||||
"Specify an API token. This will override any stored token."
|
||||
)
|
||||
.globalOption(
|
||||
"--base-url <baseUrl:string>",
|
||||
"Specify the base URL of the API. If used, --token and --workspace are required and no local remote/workspace already set will be used."
|
||||
)
|
||||
.env(
|
||||
"HEADERS <headers:string>",
|
||||
"Specify headers to use for all requests. e.g: \"HEADERS='h1: v1, h2: v2'\""
|
||||
)
|
||||
.version(VERSION)
|
||||
.versionOption(false)
|
||||
.command("init", "Bootstrap a windmill project with a wmill.yaml file")
|
||||
.action(async () => {
|
||||
if (await Deno.stat("wmill.yaml").catch(() => null)) {
|
||||
log.error(colors.green("wmill.yaml already exists"));
|
||||
} else {
|
||||
await Deno.writeTextFile(
|
||||
"wmill.yaml",
|
||||
yamlStringify({
|
||||
defaultTs: "bun",
|
||||
includes: ["f/**"],
|
||||
excludes: [],
|
||||
codebases: [],
|
||||
skipVariables: true,
|
||||
skipResources: true,
|
||||
skipSecrets: true,
|
||||
includeSchedules: false,
|
||||
includeTriggers: false,
|
||||
})
|
||||
);
|
||||
log.info(colors.green("wmill.yaml created"));
|
||||
}
|
||||
await readLockfile();
|
||||
})
|
||||
.command("app", app)
|
||||
.command("flow", flow)
|
||||
.command("script", script)
|
||||
.command("workspace", workspace)
|
||||
.command("resource", resource)
|
||||
.command("user", user)
|
||||
.command("variable", variable)
|
||||
.command("hub", hub)
|
||||
.command("folder", folder)
|
||||
.command("schedule", schedule)
|
||||
.command("trigger", trigger)
|
||||
.command("dev", dev)
|
||||
.command("sync", sync)
|
||||
.command("instance", instance)
|
||||
.command("worker-groups", workerGroups)
|
||||
.command("workers", workers)
|
||||
.command("queues", queues)
|
||||
.command("version --version", "Show version information")
|
||||
.action(async (opts) => {
|
||||
console.log("CLI version: " + VERSION);
|
||||
try {
|
||||
const provider = new NpmProvider({ package: "windmill-cli" });
|
||||
const versions = await provider.getVersions("windmill-cli");
|
||||
if (versions.latest !== VERSION) {
|
||||
console.log(
|
||||
`CLI is outdated. Latest version ${versions.latest} is available. Run \`wmill upgrade\` to update.`
|
||||
);
|
||||
} else {
|
||||
console.log("CLI is up to date");
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(
|
||||
`Cannot fetch latest CLI version on npmjs to check if up-to-date: ${e}`
|
||||
);
|
||||
}
|
||||
const workspace = await getActiveWorkspace(opts as GlobalOptions);
|
||||
if (workspace) {
|
||||
try {
|
||||
const backendVersion = await fetchVersion(workspace.remote);
|
||||
console.log("Backend Version: " + backendVersion);
|
||||
} catch (e) {
|
||||
console.warn("Cannot fetch backend version: " + e);
|
||||
}
|
||||
} else {
|
||||
console.warn(
|
||||
"Cannot fetch backend version: no active workspace selected, choose one to pick a remote to fetch version of"
|
||||
);
|
||||
}
|
||||
})
|
||||
.command(
|
||||
"upgrade",
|
||||
new UpgradeCommand({
|
||||
provider: new NpmProvider({ package: "windmill-cli" }),
|
||||
}).error((e) => {
|
||||
log.error(e);
|
||||
log.info(
|
||||
"Try running with sudo and otherwise check the result of the command: npm uninstall windmill-cli && npm install -g windmill-cli"
|
||||
);
|
||||
.globalOption(
|
||||
"--workspace <workspace:string>",
|
||||
"Specify the target workspace. This overrides the default workspace.",
|
||||
)
|
||||
.globalOption("--debug --verbose", "Show debug/verbose logs")
|
||||
.globalOption(
|
||||
"--show-diffs",
|
||||
"Show diff informations when syncing (may show sensitive informations)",
|
||||
)
|
||||
.globalOption(
|
||||
"--token <token:string>",
|
||||
"Specify an API token. This will override any stored token.",
|
||||
)
|
||||
.globalOption(
|
||||
"--base-url <baseUrl:string>",
|
||||
"Specify the base URL of the API. If used, --token and --workspace are required and no local remote/workspace already set will be used.",
|
||||
)
|
||||
.globalOption(
|
||||
"--config-dir <configDir:string>",
|
||||
"Specify a custom config directory. Overrides WMILL_CONFIG_DIR environment variable and default ~/.config location.",
|
||||
)
|
||||
.env(
|
||||
"HEADERS <headers:string>",
|
||||
"Specify headers to use for all requests. e.g: \"HEADERS='h1: v1, h2: v2'\"",
|
||||
)
|
||||
.version(VERSION)
|
||||
.versionOption(false)
|
||||
.command("init", "Bootstrap a windmill project with a wmill.yaml file")
|
||||
.option("--use-default", "Use default settings without checking backend")
|
||||
.option("--use-backend", "Use backend git-sync settings if available")
|
||||
.option(
|
||||
"--repository <repo:string>",
|
||||
"Specify repository path (e.g., u/user/repo) when using backend settings",
|
||||
)
|
||||
.action(
|
||||
async (
|
||||
opts: {
|
||||
useDefault?: boolean;
|
||||
useBackend?: boolean;
|
||||
repository?: string;
|
||||
workspace?: string;
|
||||
debug?: unknown;
|
||||
showDiffs?: boolean;
|
||||
token?: string;
|
||||
baseUrl?: string;
|
||||
configDir?: string;
|
||||
},
|
||||
) => {
|
||||
if (await Deno.stat("wmill.yaml").catch(() => null)) {
|
||||
log.error(colors.red("wmill.yaml already exists"));
|
||||
} else {
|
||||
// Import DEFAULT_SYNC_OPTIONS from conf.ts
|
||||
const { DEFAULT_SYNC_OPTIONS } = await import("./conf.ts");
|
||||
|
||||
// Create initial config with defaults
|
||||
const initialConfig = {
|
||||
defaultTs: DEFAULT_SYNC_OPTIONS.defaultTs,
|
||||
includes: DEFAULT_SYNC_OPTIONS.includes,
|
||||
excludes: DEFAULT_SYNC_OPTIONS.excludes,
|
||||
codebases: DEFAULT_SYNC_OPTIONS.codebases,
|
||||
skipVariables: DEFAULT_SYNC_OPTIONS.skipVariables,
|
||||
skipResources: DEFAULT_SYNC_OPTIONS.skipResources,
|
||||
skipSecrets: DEFAULT_SYNC_OPTIONS.skipSecrets,
|
||||
skipScripts: DEFAULT_SYNC_OPTIONS.skipScripts,
|
||||
skipFlows: DEFAULT_SYNC_OPTIONS.skipFlows,
|
||||
skipApps: DEFAULT_SYNC_OPTIONS.skipApps,
|
||||
skipFolders: DEFAULT_SYNC_OPTIONS.skipFolders,
|
||||
includeSchedules: DEFAULT_SYNC_OPTIONS.includeSchedules,
|
||||
includeTriggers: DEFAULT_SYNC_OPTIONS.includeTriggers,
|
||||
};
|
||||
|
||||
await Deno.writeTextFile(
|
||||
"wmill.yaml",
|
||||
yamlStringify(initialConfig),
|
||||
);
|
||||
log.info(
|
||||
colors.green("wmill.yaml created with default settings"),
|
||||
);
|
||||
|
||||
// Create lock file
|
||||
await readLockfile();
|
||||
|
||||
// Check for backend git-sync settings unless --use-default is specified
|
||||
if (!opts.useDefault) {
|
||||
try {
|
||||
const { requireLogin, resolveWorkspace } = await import(
|
||||
"./context.ts"
|
||||
);
|
||||
|
||||
// Check if user has workspace configured
|
||||
const { getActiveWorkspace } = await import(
|
||||
"./workspace.ts"
|
||||
);
|
||||
const activeWorkspace = await getActiveWorkspace(opts as GlobalOptions);
|
||||
|
||||
if (!activeWorkspace) {
|
||||
log.info(
|
||||
"No workspace configured. Using default settings.",
|
||||
);
|
||||
log.info(
|
||||
"You can configure a workspace later with 'wmill workspace add'",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await requireLogin(opts as GlobalOptions);
|
||||
const workspace = await resolveWorkspace(opts as GlobalOptions);
|
||||
|
||||
const wmill = await import("./gen/services.gen.ts");
|
||||
const settings = await wmill.getSettings({
|
||||
workspace: workspace.workspaceId,
|
||||
});
|
||||
|
||||
if (
|
||||
settings.git_sync?.repositories &&
|
||||
settings.git_sync.repositories.length > 0
|
||||
) {
|
||||
let useBackendSettings = opts.useBackend;
|
||||
|
||||
// If repository is specified, implicitly use backend settings
|
||||
if (opts.repository && !opts.useDefault) {
|
||||
useBackendSettings = true;
|
||||
}
|
||||
|
||||
if (useBackendSettings === undefined) {
|
||||
// Interactive prompt
|
||||
const { Select } = await import("./deps.ts");
|
||||
const choice = await Select.prompt({
|
||||
message:
|
||||
"Git-sync settings found on backend. What would you like to do?",
|
||||
options: [
|
||||
{
|
||||
name: "Use backend git-sync settings",
|
||||
value: "backend",
|
||||
},
|
||||
{
|
||||
name: "Use default settings",
|
||||
value: "default",
|
||||
},
|
||||
{
|
||||
name: "Cancel",
|
||||
value: "cancel",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (choice === "cancel") {
|
||||
// Clean up the created files
|
||||
try {
|
||||
await Deno.remove("wmill.yaml");
|
||||
await Deno.remove("wmill-lock.yaml");
|
||||
} catch (e) {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
log.info("Init cancelled");
|
||||
Deno.exit(0);
|
||||
}
|
||||
|
||||
useBackendSettings = choice === "backend";
|
||||
}
|
||||
|
||||
if (useBackendSettings) {
|
||||
log.info(
|
||||
"Applying git-sync settings from backend...",
|
||||
);
|
||||
|
||||
// Import and run the pull git-sync settings logic
|
||||
const { pullGitSyncSettings } = await import(
|
||||
"./gitsync-settings.ts"
|
||||
);
|
||||
await pullGitSyncSettings({
|
||||
...(opts as GlobalOptions),
|
||||
repository: opts.repository,
|
||||
jsonOutput: false,
|
||||
diff: false,
|
||||
replace: true, // Auto-replace when using backend settings during init
|
||||
});
|
||||
|
||||
log.info(
|
||||
colors.green(
|
||||
"Git-sync settings applied from backend",
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// If there's an error checking backend settings, just continue with defaults
|
||||
log.warn(
|
||||
`Could not check backend for git-sync settings: ${error.message}`,
|
||||
);
|
||||
log.info("Continuing with default settings");
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
.command("app", app)
|
||||
.command("flow", flow)
|
||||
.command("script", script)
|
||||
.command("workspace", workspace)
|
||||
.command("resource", resource)
|
||||
.command("user", user)
|
||||
.command("variable", variable)
|
||||
.command("hub", hub)
|
||||
.command("folder", folder)
|
||||
.command("schedule", schedule)
|
||||
.command("trigger", trigger)
|
||||
.command("dev", dev)
|
||||
.command("sync", sync)
|
||||
.command("gitsync-settings", gitsyncSettings)
|
||||
.command("instance", instance)
|
||||
.command("worker-groups", workerGroups)
|
||||
.command("workers", workers)
|
||||
.command("queues", queues)
|
||||
.command("version --version", "Show version information")
|
||||
.action(async (opts) => {
|
||||
console.log("CLI version: " + VERSION);
|
||||
try {
|
||||
const provider = new NpmProvider({ package: "windmill-cli" });
|
||||
const versions = await provider.getVersions("windmill-cli");
|
||||
if (versions.latest !== VERSION) {
|
||||
console.log(
|
||||
`CLI is outdated. Latest version ${versions.latest} is available. Run \`wmill upgrade\` to update.`,
|
||||
);
|
||||
} else {
|
||||
console.log("CLI is up to date");
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(
|
||||
`Cannot fetch latest CLI version on npmjs to check if up-to-date: ${e}`,
|
||||
);
|
||||
}
|
||||
const workspace = await getActiveWorkspace(opts as GlobalOptions);
|
||||
if (workspace) {
|
||||
try {
|
||||
const backendVersion = await fetchVersion(workspace.remote);
|
||||
console.log("Backend Version: " + backendVersion);
|
||||
} catch (e) {
|
||||
console.warn("Cannot fetch backend version: " + e);
|
||||
}
|
||||
} else {
|
||||
console.warn(
|
||||
"Cannot fetch backend version: no active workspace selected, choose one to pick a remote to fetch version of",
|
||||
);
|
||||
}
|
||||
})
|
||||
)
|
||||
.command("completions", new CompletionsCommand());
|
||||
.command(
|
||||
"upgrade",
|
||||
new UpgradeCommand({
|
||||
provider: new NpmProvider({ package: "windmill-cli" }),
|
||||
}).error((e) => {
|
||||
log.error(e);
|
||||
log.info(
|
||||
"Try running with sudo and otherwise check the result of the command: npm uninstall windmill-cli && npm install -g windmill-cli",
|
||||
);
|
||||
}),
|
||||
)
|
||||
.command("completions", new CompletionsCommand());
|
||||
|
||||
export let showDiffs = false;
|
||||
|
||||
let isWin: boolean | undefined = undefined;
|
||||
|
||||
export async function getIsWin() {
|
||||
if (isWin === undefined) {
|
||||
const os = await import("node:os");
|
||||
isWin = os.platform() === "win32";
|
||||
}
|
||||
return isWin;
|
||||
if (isWin === undefined) {
|
||||
const os = await import("node:os");
|
||||
isWin = os.platform() === "win32";
|
||||
}
|
||||
return isWin;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
if (Deno.args.length === 0) {
|
||||
command.showHelp();
|
||||
}
|
||||
const LOG_LEVEL =
|
||||
Deno.args.includes("--verbose") || Deno.args.includes("--debug")
|
||||
? "DEBUG"
|
||||
: "INFO";
|
||||
// const NO_COLORS = Deno.args.includes("--no-colors");
|
||||
showDiffs = Deno.args.includes("--show-diffs");
|
||||
try {
|
||||
if (Deno.args.length === 0) {
|
||||
command.showHelp();
|
||||
}
|
||||
const LOG_LEVEL =
|
||||
Deno.args.includes("--verbose") || Deno.args.includes("--debug")
|
||||
? "DEBUG"
|
||||
: "INFO";
|
||||
// const NO_COLORS = Deno.args.includes("--no-colors");
|
||||
showDiffs = Deno.args.includes("--show-diffs");
|
||||
|
||||
log.setup({
|
||||
handlers: {
|
||||
console: new log.ConsoleHandler(LOG_LEVEL, {
|
||||
formatter: ({ msg }) => `${msg}`,
|
||||
useColors: isWin ? false : true,
|
||||
}),
|
||||
},
|
||||
loggers: {
|
||||
default: {
|
||||
level: LOG_LEVEL,
|
||||
handlers: ["console"],
|
||||
},
|
||||
},
|
||||
});
|
||||
log.debug("Debug logging enabled. CLI build against " + VERSION);
|
||||
log.setup({
|
||||
handlers: {
|
||||
console: new log.ConsoleHandler(LOG_LEVEL, {
|
||||
formatter: ({ msg }) => `${msg}`,
|
||||
useColors: isWin ? false : true,
|
||||
}),
|
||||
},
|
||||
loggers: {
|
||||
default: {
|
||||
level: LOG_LEVEL,
|
||||
handlers: ["console"],
|
||||
},
|
||||
},
|
||||
});
|
||||
log.debug("Debug logging enabled. CLI build against " + VERSION);
|
||||
|
||||
const extraHeaders = getHeaders();
|
||||
if (extraHeaders) {
|
||||
OpenAPI.HEADERS = extraHeaders;
|
||||
const extraHeaders = getHeaders();
|
||||
if (extraHeaders) {
|
||||
OpenAPI.HEADERS = extraHeaders;
|
||||
}
|
||||
await command.parse(Deno.args);
|
||||
} catch (e) {
|
||||
if (e.name === "ApiError") {
|
||||
console.log("Server failed. " + e.statusText + ": " + e.body);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
await command.parse(Deno.args);
|
||||
} catch (e) {
|
||||
if (e.name === "ApiError") {
|
||||
console.log("Server failed. " + e.statusText + ": " + e.body);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
function isMain() {
|
||||
// dnt-shim-ignore
|
||||
const { Deno } = globalThis as any;
|
||||
// dnt-shim-ignore
|
||||
const { Deno } = globalThis as any;
|
||||
|
||||
const isDeno = Deno != undefined;
|
||||
const isDeno = Deno != undefined;
|
||||
|
||||
if (isDeno) {
|
||||
const isMain = import.meta.main;
|
||||
if (isMain) {
|
||||
if (!Deno.args.includes("completions")) {
|
||||
if (Deno.env.get("SKIP_DENO_DEPRECATION_WARNING") !== "true") {
|
||||
log.warn(
|
||||
"Using the deno runtime for the Windmill CLI is deprecated, you can now use node: deno uninstall wmill && npm install -g windmill-cli. To skip this warning set SKIP_DENO_DEPRECATION_WARNING=true"
|
||||
);
|
||||
if (isDeno) {
|
||||
const isMain = import.meta.main;
|
||||
if (isMain) {
|
||||
if (!Deno.args.includes("completions")) {
|
||||
if (Deno.env.get("SKIP_DENO_DEPRECATION_WARNING") !== "true") {
|
||||
log.warn(
|
||||
"Using the deno runtime for the Windmill CLI is deprecated, you can now use node: deno uninstall wmill && npm install -g windmill-cli. To skip this warning set SKIP_DENO_DEPRECATION_WARNING=true",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return isMain;
|
||||
} else {
|
||||
//@ts-ignore
|
||||
return esMain.default(import.meta);
|
||||
}
|
||||
return isMain;
|
||||
} else {
|
||||
//@ts-ignore
|
||||
return esMain.default(import.meta);
|
||||
}
|
||||
}
|
||||
if (isMain()) {
|
||||
main();
|
||||
main();
|
||||
}
|
||||
|
||||
export default command;
|
||||
|
||||
+9
-4
@@ -13,15 +13,20 @@ function hash_string(str: string): number {
|
||||
return hash;
|
||||
}
|
||||
|
||||
export async function getRootStore(): Promise<string> {
|
||||
const store = (config_dir() ?? tmp_dir() ?? "/tmp/") + "/windmill/";
|
||||
export async function getRootStore(configDirOverride?: string): Promise<string> {
|
||||
const baseDir = configDirOverride ??
|
||||
Deno.env.get("WMILL_CONFIG_DIR") ??
|
||||
config_dir() ??
|
||||
tmp_dir() ??
|
||||
"/tmp/";
|
||||
const store = baseDir + "/windmill/";
|
||||
await ensureDir(store);
|
||||
return store;
|
||||
}
|
||||
|
||||
export async function getStore(baseUrl: string): Promise<string> {
|
||||
export async function getStore(baseUrl: string, configDirOverride?: string): Promise<string> {
|
||||
const baseHash = Math.abs(hash_string(baseUrl)).toString(16);
|
||||
const baseStore = (await getRootStore()) + baseHash + "/";
|
||||
const baseStore = (await getRootStore(configDirOverride)) + baseHash + "/";
|
||||
await ensureDir(baseStore);
|
||||
return baseStore;
|
||||
}
|
||||
|
||||
+286
-27
@@ -3,6 +3,8 @@ import {
|
||||
colors,
|
||||
Command,
|
||||
Confirm,
|
||||
Input,
|
||||
Select,
|
||||
ensureDir,
|
||||
minimatch,
|
||||
JSZip,
|
||||
@@ -34,8 +36,9 @@ import {
|
||||
} from "./script.ts";
|
||||
|
||||
import { handleFile } from "./script.ts";
|
||||
import { deepEqual, isFileResource } from "./utils.ts";
|
||||
import { SyncOptions, mergeConfigWithConfigFile } from "./conf.ts";
|
||||
import { deepEqual, isFileResource, Repository, selectRepository } from "./utils.ts";
|
||||
import { SyncOptions, mergeConfigWithConfigFile, readConfigFile, getEffectiveSettings } from "./conf.ts";
|
||||
import { Workspace } from "./workspace.ts";
|
||||
import { removePathPrefix } from "./types.ts";
|
||||
import { SyncCodebase, listSyncCodebases } from "./codebase.ts";
|
||||
import {
|
||||
@@ -46,6 +49,128 @@ import {
|
||||
import { FlowModule, OpenFlow, RawScript } from "./gen/types.gen.ts";
|
||||
import { pushResource } from "./resource.ts";
|
||||
|
||||
|
||||
// Merge CLI options with effective settings, preserving CLI flags as overrides
|
||||
function mergeCliWithEffectiveOptions<T extends GlobalOptions & SyncOptions & { repository?: string }>(
|
||||
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.skipFlows !== undefined) mergedOpts.skipFlows = cliOpts.skipFlows;
|
||||
if (cliOpts.skipApps !== undefined) mergedOpts.skipApps = cliOpts.skipApps;
|
||||
if (cliOpts.skipFolders !== undefined) mergedOpts.skipFolders = cliOpts.skipFolders;
|
||||
|
||||
return mergedOpts;
|
||||
}
|
||||
|
||||
// Resolve effective sync options with smart repository detection
|
||||
async function resolveEffectiveSyncOptions(
|
||||
workspace: Workspace,
|
||||
repositoryPath?: string
|
||||
): Promise<SyncOptions> {
|
||||
const localConfig = await readConfigFile();
|
||||
|
||||
// If repository path is already specified, use it directly
|
||||
if (repositoryPath) {
|
||||
return getEffectiveSettings(
|
||||
localConfig,
|
||||
workspace.remote,
|
||||
workspace.workspaceId,
|
||||
repositoryPath
|
||||
);
|
||||
}
|
||||
|
||||
// Auto-detect repository from overrides if not specified
|
||||
if (localConfig.overrides) {
|
||||
const prefix = `${workspace.remote}:${workspace.workspaceId}:`;
|
||||
const applicableRepos: string[] = [];
|
||||
|
||||
// Find all repository-specific overrides for this workspace
|
||||
for (const key of Object.keys(localConfig.overrides)) {
|
||||
if (key.startsWith(prefix) && !key.endsWith(':*')) {
|
||||
const repo = key.substring(prefix.length);
|
||||
if (repo) {
|
||||
applicableRepos.push(repo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (applicableRepos.length === 1) {
|
||||
// Single repository found - auto-select it
|
||||
log.info(`Auto-selected repository: ${applicableRepos[0]}`);
|
||||
return getEffectiveSettings(
|
||||
localConfig,
|
||||
workspace.remote,
|
||||
workspace.workspaceId,
|
||||
applicableRepos[0]
|
||||
);
|
||||
} else if (applicableRepos.length > 1) {
|
||||
// Multiple repositories found - prompt for selection
|
||||
const isInteractive = Deno.stdin.isTerminal() && Deno.stdout.isTerminal();
|
||||
|
||||
if (isInteractive) {
|
||||
const choices = [
|
||||
{ name: "Use top-level settings (no repository-specific override)", value: "" },
|
||||
...applicableRepos.map(repo => ({ name: repo, value: repo }))
|
||||
];
|
||||
|
||||
const selectedRepo = await Select.prompt({
|
||||
message: "Multiple repository overrides found. Select which to use:",
|
||||
options: choices
|
||||
});
|
||||
|
||||
if (selectedRepo) {
|
||||
log.info(`Selected repository: ${selectedRepo}`);
|
||||
}
|
||||
|
||||
return getEffectiveSettings(
|
||||
localConfig,
|
||||
workspace.remote,
|
||||
workspace.workspaceId,
|
||||
selectedRepo
|
||||
);
|
||||
} 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.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No repository overrides found or selected - use top-level settings
|
||||
return getEffectiveSettings(
|
||||
localConfig,
|
||||
workspace.remote,
|
||||
workspace.workspaceId,
|
||||
""
|
||||
);
|
||||
}
|
||||
|
||||
type DynFSElement = {
|
||||
isDirectory: boolean;
|
||||
path: string;
|
||||
@@ -678,7 +803,17 @@ export async function elementsToMap(
|
||||
if (skips.skipResourceTypes && path.endsWith(".resource-type" + ext))
|
||||
continue;
|
||||
|
||||
if (skips.skipVariables && path.endsWith(".variable" + ext)) continue;
|
||||
// Use getTypeStrFromPath for consistent type detection
|
||||
try {
|
||||
const fileType = getTypeStrFromPath(path);
|
||||
if (skips.skipVariables && fileType === "variable") continue;
|
||||
if (skips.skipScripts && fileType === "script") continue;
|
||||
if (skips.skipFlows && fileType === "flow") continue;
|
||||
if (skips.skipApps && fileType === "app") continue;
|
||||
if (skips.skipFolders && fileType === "folder") continue;
|
||||
} catch {
|
||||
// If getTypeStrFromPath can't determine the type, continue processing the file
|
||||
}
|
||||
|
||||
if (skips.skipResources && isFileResource(path)) continue;
|
||||
|
||||
@@ -733,6 +868,10 @@ export interface Skips {
|
||||
skipResources?: boolean | undefined;
|
||||
skipResourceTypes?: boolean | undefined;
|
||||
skipSecrets?: boolean | undefined;
|
||||
skipScripts?: boolean | undefined;
|
||||
skipFlows?: boolean | undefined;
|
||||
skipApps?: boolean | undefined;
|
||||
skipFolders?: boolean | undefined;
|
||||
skipScriptsMetadata?: boolean | undefined;
|
||||
includeSchedules?: boolean | undefined;
|
||||
includeTriggers?: boolean | undefined;
|
||||
@@ -1000,6 +1139,10 @@ export async function ignoreF(wmillconf: {
|
||||
extraIncludes?: string[];
|
||||
skipResourceTypes?: boolean;
|
||||
json?: boolean;
|
||||
includeUsers?: boolean;
|
||||
includeGroups?: boolean;
|
||||
includeSettings?: boolean;
|
||||
includeKey?: boolean;
|
||||
}): Promise<(p: string, isDirectory: boolean) => boolean> {
|
||||
let whitelist: { approve(file: string): boolean } | undefined = undefined;
|
||||
|
||||
@@ -1014,7 +1157,7 @@ export async function ignoreF(wmillconf: {
|
||||
wmillconf.includes?.some((i) => minimatch(file, i))) &&
|
||||
(!wmillconf?.excludes ||
|
||||
wmillconf.excludes!.every((i) => !minimatch(file, i))) &&
|
||||
(!wmillconf.extraIncludes ||
|
||||
(!wmillconf.extraIncludes || wmillconf.extraIncludes.length === 0 ||
|
||||
wmillconf.extraIncludes.some((i) => minimatch(file, i)))
|
||||
);
|
||||
},
|
||||
@@ -1035,6 +1178,28 @@ export async function ignoreF(wmillconf: {
|
||||
if (!isDirectory && p.endsWith(".resource-type" + ext)) {
|
||||
return wmillconf.skipResourceTypes ?? false;
|
||||
}
|
||||
|
||||
// Special files should bypass path-based filtering when their include flags are set
|
||||
if (!isDirectory) {
|
||||
try {
|
||||
const fileType = getTypeStrFromPath(p);
|
||||
if (wmillconf.includeUsers && fileType === "user") {
|
||||
return false; // Don't ignore, always include
|
||||
}
|
||||
if (wmillconf.includeGroups && fileType === "group") {
|
||||
return false; // Don't ignore, always include
|
||||
}
|
||||
if (wmillconf.includeSettings && fileType === "settings") {
|
||||
return false; // Don't ignore, always include
|
||||
}
|
||||
if (wmillconf.includeKey && fileType === "encryption_key") {
|
||||
return false; // Don't ignore, always include
|
||||
}
|
||||
} catch {
|
||||
// If getTypeStrFromPath can't determine the type, fall through to normal logic
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
!isWhitelisted(p) &&
|
||||
(isNotWmillFile(p, isDirectory) ||
|
||||
@@ -1094,9 +1259,7 @@ async function buildTracker(changes: Change[]) {
|
||||
return tracker;
|
||||
}
|
||||
|
||||
export async function pull(opts: GlobalOptions & SyncOptions) {
|
||||
opts = await mergeConfigWithConfigFile(opts);
|
||||
|
||||
export async function pull(opts: GlobalOptions & SyncOptions & { repository?: string }) {
|
||||
if (opts.stateful) {
|
||||
await ensureDir(path.join(Deno.cwd(), ".wmill"));
|
||||
}
|
||||
@@ -1104,6 +1267,12 @@ export async function pull(opts: GlobalOptions & SyncOptions) {
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
|
||||
// Resolve effective sync options with repository awareness
|
||||
const effectiveOpts = await resolveEffectiveSyncOptions(workspace, opts.repository);
|
||||
|
||||
// Merge CLI flags with resolved settings (CLI flags take precedence only for explicit overrides)
|
||||
opts = mergeCliWithEffectiveOptions(opts, effectiveOpts);
|
||||
|
||||
const codebases = await listSyncCodebases(opts);
|
||||
|
||||
log.info(
|
||||
@@ -1158,8 +1327,26 @@ export async function pull(opts: GlobalOptions & SyncOptions) {
|
||||
log.info(
|
||||
`remote (${workspace.name}) -> local: ${changes.length} changes to apply`
|
||||
);
|
||||
|
||||
// Handle JSON output for dry-run
|
||||
if (opts.dryRun && opts.jsonOutput) {
|
||||
const result = {
|
||||
success: true,
|
||||
changes: changes.map(change => ({
|
||||
type: change.name,
|
||||
path: change.path,
|
||||
...(change.name === "edited" && change.codebase ? { codebase_changed: true } : {})
|
||||
})),
|
||||
total: changes.length
|
||||
};
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
return;
|
||||
}
|
||||
|
||||
if (changes.length > 0) {
|
||||
prettyChanges(changes);
|
||||
if (!opts.jsonOutput) {
|
||||
prettyChanges(changes);
|
||||
}
|
||||
if (opts.dryRun) {
|
||||
log.info(colors.gray(`Dry run complete.`));
|
||||
return;
|
||||
@@ -1307,11 +1494,27 @@ export async function pull(opts: GlobalOptions & SyncOptions) {
|
||||
)} scripts were changed but ignoring for now`
|
||||
);
|
||||
}
|
||||
log.info(
|
||||
colors.bold.green.underline(
|
||||
`\nDone! All ${changes.length} changes applied locally and wmill-lock.yaml updated.`
|
||||
)
|
||||
);
|
||||
if (opts.jsonOutput) {
|
||||
const result = {
|
||||
success: true,
|
||||
message: `All ${changes.length} changes applied locally and wmill-lock.yaml updated`,
|
||||
changes: changes.map(change => ({
|
||||
type: change.name,
|
||||
path: change.path,
|
||||
...(change.name === "edited" && change.codebase ? { codebase_changed: true } : {})
|
||||
})),
|
||||
total: changes.length
|
||||
};
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
} else {
|
||||
log.info(
|
||||
colors.bold.green.underline(
|
||||
`\nDone! All ${changes.length} changes applied locally and wmill-lock.yaml updated.`
|
||||
)
|
||||
);
|
||||
}
|
||||
} else if (opts.jsonOutput) {
|
||||
console.log(JSON.stringify({ success: true, message: "No changes to apply", total: 0 }, null, 2));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1366,8 +1569,16 @@ function removeSuffix(str: string, suffix: string) {
|
||||
return str.slice(0, str.length - suffix.length);
|
||||
}
|
||||
|
||||
export async function push(opts: GlobalOptions & SyncOptions) {
|
||||
opts = await mergeConfigWithConfigFile(opts);
|
||||
export async function push(opts: GlobalOptions & SyncOptions & { repository?: string }) {
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
|
||||
// Resolve effective sync options with repository awareness
|
||||
const effectiveOpts = await resolveEffectiveSyncOptions(workspace, opts.repository);
|
||||
|
||||
// Merge CLI flags with resolved settings (CLI flags take precedence only for explicit overrides)
|
||||
opts = mergeCliWithEffectiveOptions(opts, effectiveOpts);
|
||||
|
||||
const codebases = await listSyncCodebases(opts);
|
||||
if (opts.raw) {
|
||||
log.info("--raw is now the default, you can remove it as a flag");
|
||||
@@ -1383,9 +1594,6 @@ export async function push(opts: GlobalOptions & SyncOptions) {
|
||||
}
|
||||
}
|
||||
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
|
||||
log.info(
|
||||
colors.gray(
|
||||
"Computing the files to update on the remote to match local (taking wmill.yaml includes/excludes into account)"
|
||||
@@ -1498,8 +1706,25 @@ export async function push(opts: GlobalOptions & SyncOptions) {
|
||||
`remote (${workspace.name}) <- local: ${changes.length} changes to apply`
|
||||
);
|
||||
|
||||
// Handle JSON output for dry-run
|
||||
if (opts.dryRun && opts.jsonOutput) {
|
||||
const result = {
|
||||
success: true,
|
||||
changes: changes.map(change => ({
|
||||
type: change.name,
|
||||
path: change.path,
|
||||
...(change.name === "edited" && change.codebase ? { codebase_changed: true } : {})
|
||||
})),
|
||||
total: changes.length
|
||||
};
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
return;
|
||||
}
|
||||
|
||||
if (changes.length > 0) {
|
||||
prettyChanges(changes);
|
||||
if (!opts.jsonOutput) {
|
||||
prettyChanges(changes);
|
||||
}
|
||||
if (opts.dryRun) {
|
||||
log.info(colors.gray(`Dry run complete.`));
|
||||
return;
|
||||
@@ -1860,13 +2085,30 @@ export async function push(opts: GlobalOptions & SyncOptions) {
|
||||
await Promise.race(pool);
|
||||
}
|
||||
}
|
||||
log.info(
|
||||
colors.bold.green.underline(
|
||||
`\nDone! All ${changes.length} changes pushed to the remote workspace ${
|
||||
workspace.workspaceId
|
||||
} named ${workspace.name} (${(performance.now() - start).toFixed(0)}ms)`
|
||||
)
|
||||
);
|
||||
if (opts.jsonOutput) {
|
||||
const result = {
|
||||
success: true,
|
||||
message: `All ${changes.length} changes pushed to the remote workspace ${workspace.workspaceId} named ${workspace.name}`,
|
||||
changes: changes.map(change => ({
|
||||
type: change.name,
|
||||
path: change.path,
|
||||
...(change.name === "edited" && change.codebase ? { codebase_changed: true } : {})
|
||||
})),
|
||||
total: changes.length,
|
||||
duration_ms: Math.round(performance.now() - start)
|
||||
};
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
} else {
|
||||
log.info(
|
||||
colors.bold.green.underline(
|
||||
`\nDone! All ${changes.length} changes pushed to the remote workspace ${
|
||||
workspace.workspaceId
|
||||
} named ${workspace.name} (${(performance.now() - start).toFixed(0)}ms)`
|
||||
)
|
||||
);
|
||||
}
|
||||
} else if (opts.jsonOutput) {
|
||||
console.log(JSON.stringify({ success: true, message: "No changes to push", total: 0 }, null, 2));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1891,6 +2133,10 @@ const command = new Command()
|
||||
.option("--skip-secrets", "Skip syncing only secrets variables")
|
||||
.option("--skip-resources", "Skip syncing resources")
|
||||
.option("--skip-resource-types", "Skip syncing resource types")
|
||||
.option("--skip-scripts", "Skip syncing scripts")
|
||||
.option("--skip-flows", "Skip syncing flows")
|
||||
.option("--skip-apps", "Skip syncing apps")
|
||||
.option("--skip-folders", "Skip syncing folders")
|
||||
// .option("--skip-scripts-metadata", "Skip syncing scripts metadata, focus solely on logic")
|
||||
.option("--include-schedules", "Include syncing schedules")
|
||||
.option("--include-triggers", "Include syncing triggers")
|
||||
@@ -1898,6 +2144,7 @@ const command = new Command()
|
||||
.option("--include-groups", "Include syncing groups")
|
||||
.option("--include-settings", "Include syncing workspace settings")
|
||||
.option("--include-key", "Include workspace encryption key")
|
||||
.option("--json-output", "Output results in JSON format")
|
||||
.option(
|
||||
"-i --includes <patterns:file[]>",
|
||||
"Comma separated patterns to specify which file to take into account (among files that are compatible with windmill). Patterns can include * (any string until '/') and ** (any string). Overrides wmill.yaml includes"
|
||||
@@ -1910,6 +2157,10 @@ const command = new Command()
|
||||
"--extra-includes <patterns:file[]>",
|
||||
"Comma separated patterns to specify which file to take into account (among files that are compatible with windmill). Patterns can include * (any string until '/') and ** (any string). Useful to still take wmill.yaml into account and act as a second pattern to satisfy"
|
||||
)
|
||||
.option(
|
||||
"--repository <repo:string>",
|
||||
"Specify repository path (e.g., u/user/repo) when multiple repositories exist"
|
||||
)
|
||||
// deno-lint-ignore no-explicit-any
|
||||
.action(pull as any)
|
||||
.command("push")
|
||||
@@ -1925,7 +2176,10 @@ const command = new Command()
|
||||
.option("--skip-secrets", "Skip syncing only secrets variables")
|
||||
.option("--skip-resources", "Skip syncing resources")
|
||||
.option("--skip-resource-types", "Skip syncing resource types")
|
||||
|
||||
.option("--skip-scripts", "Skip syncing scripts")
|
||||
.option("--skip-flows", "Skip syncing flows")
|
||||
.option("--skip-apps", "Skip syncing apps")
|
||||
.option("--skip-folders", "Skip syncing folders")
|
||||
// .option("--skip-scripts-metadata", "Skip syncing scripts metadata, focus solely on logic")
|
||||
.option("--include-schedules", "Include syncing schedules")
|
||||
.option("--include-triggers", "Include syncing triggers")
|
||||
@@ -1933,6 +2187,7 @@ const command = new Command()
|
||||
.option("--include-groups", "Include syncing groups")
|
||||
.option("--include-settings", "Include syncing workspace settings")
|
||||
.option("--include-key", "Include workspace encryption key")
|
||||
.option("--json-output", "Output results in JSON format")
|
||||
.option(
|
||||
"-i --includes <patterns:file[]>",
|
||||
"Comma separated patterns to specify which file to take into account (among files that are compatible with windmill). Patterns can include * (any string until '/') and ** (any string)"
|
||||
@@ -1950,6 +2205,10 @@ const command = new Command()
|
||||
"Include a message that will be added to all scripts/flows/apps updated during this push"
|
||||
)
|
||||
.option("--parallel <number>", "Number of changes to process in parallel")
|
||||
.option(
|
||||
"--repository <repo:string>",
|
||||
"Specify repository path (e.g., u/user/repo) when multiple repositories exist"
|
||||
)
|
||||
// deno-lint-ignore no-explicit-any
|
||||
.action(push as any);
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,74 @@
|
||||
version: "3.7"
|
||||
|
||||
x-logging: &default-logging
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
compress: "true"
|
||||
|
||||
services:
|
||||
test_db:
|
||||
image: postgres:16
|
||||
environment:
|
||||
POSTGRES_PASSWORD: testpass123
|
||||
POSTGRES_DB: windmill_test
|
||||
POSTGRES_USER: postgres
|
||||
ports:
|
||||
- "5433:5432" # Use different port to avoid conflicts
|
||||
volumes:
|
||||
- test_db_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres -d windmill_test"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
logging: *default-logging
|
||||
|
||||
test_windmill_server:
|
||||
image: windmill-test:latest
|
||||
environment:
|
||||
- DATABASE_URL=postgres://postgres:testpass123@test_db/windmill_test?sslmode=disable
|
||||
- MODE=server
|
||||
- LICENSE_KEY=${EE_LICENSE_KEY}
|
||||
- RUST_LOG=info
|
||||
- DISABLE_TELEMETRY=true
|
||||
- METRICS_ENABLED=false
|
||||
ports:
|
||||
- "8001:8000" # Use different port to avoid conflicts
|
||||
depends_on:
|
||||
test_db:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- test_worker_logs:/tmp/windmill/logs
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8000/api/version"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
start_period: 30s
|
||||
logging: *default-logging
|
||||
|
||||
test_windmill_worker:
|
||||
image: windmill-test:latest
|
||||
environment:
|
||||
- DATABASE_URL=postgres://postgres:testpass123@test_db/windmill_test?sslmode=disable
|
||||
- MODE=worker
|
||||
- WORKER_GROUP=default
|
||||
- LICENSE_KEY=${EE_LICENSE_KEY}
|
||||
- RUST_LOG=info
|
||||
- DISABLE_TELEMETRY=true
|
||||
- NUM_WORKERS=1
|
||||
- SLEEP_QUEUE=50
|
||||
depends_on:
|
||||
test_db:
|
||||
condition: service_healthy
|
||||
test_windmill_server:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- test_worker_logs:/tmp/windmill/logs
|
||||
logging: *default-logging
|
||||
|
||||
volumes:
|
||||
test_db_data: null
|
||||
test_worker_logs: null
|
||||
@@ -0,0 +1,177 @@
|
||||
import { assertEquals, assertStringIncludes } from "https://deno.land/std@0.224.0/assert/mod.ts";
|
||||
import { withContainerizedBackend } from "./containerized_backend.ts";
|
||||
import { addWorkspace } from "../workspace.ts";
|
||||
|
||||
// =============================================================================
|
||||
// GITSYNC-SETTINGS COMMAND FEATURES
|
||||
// Tests for additional gitsync-settings command functionality
|
||||
// =============================================================================
|
||||
|
||||
Deno.test("GitSync Settings: workspace-level wildcard settings", async () => {
|
||||
await withContainerizedBackend(async (backend, tempDir) => {
|
||||
// Set up workspace
|
||||
const testWorkspace = {
|
||||
remote: backend.baseUrl,
|
||||
workspaceId: backend.workspace,
|
||||
name: "workspace_level_test",
|
||||
token: backend.token
|
||||
};
|
||||
await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir });
|
||||
|
||||
// Configure backend with repository
|
||||
await backend.updateGitSyncConfig({
|
||||
git_sync_settings: {
|
||||
repositories: [{
|
||||
git_repo_resource_path: "u/test/workspace_repo",
|
||||
script_path: "f/**",
|
||||
group_by_folder: false,
|
||||
use_individual_branch: false,
|
||||
settings: {
|
||||
include_path: ["f/**"],
|
||||
include_type: ["script", "flow"],
|
||||
exclude_path: [],
|
||||
extra_include_path: []
|
||||
}
|
||||
}]
|
||||
}
|
||||
});
|
||||
|
||||
// Create initial wmill.yaml
|
||||
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
||||
includes:
|
||||
- f/**
|
||||
excludes: []`);
|
||||
|
||||
// Pull with workspace-level flag
|
||||
const result = await backend.runCLICommand([
|
||||
'gitsync-settings', 'pull',
|
||||
'--repository', 'u/test/workspace_repo',
|
||||
'--workspace-level',
|
||||
'--override'
|
||||
], tempDir);
|
||||
|
||||
assertEquals(result.code, 0, `Workspace-level pull should succeed: ${result.stderr}`);
|
||||
|
||||
// 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:");
|
||||
});
|
||||
});
|
||||
|
||||
Deno.test("GitSync Settings: default mode writes to top-level", async () => {
|
||||
await withContainerizedBackend(async (backend, tempDir) => {
|
||||
// Set up workspace
|
||||
const testWorkspace = {
|
||||
remote: backend.baseUrl,
|
||||
workspaceId: backend.workspace,
|
||||
name: "default_mode_test",
|
||||
token: backend.token
|
||||
};
|
||||
await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir });
|
||||
|
||||
// Configure backend with specific settings
|
||||
await backend.updateGitSyncConfig({
|
||||
git_sync_settings: {
|
||||
repositories: [{
|
||||
git_repo_resource_path: "u/test/default_repo",
|
||||
script_path: "f/**",
|
||||
group_by_folder: false,
|
||||
use_individual_branch: false,
|
||||
settings: {
|
||||
include_path: ["f/special/**"],
|
||||
include_type: ["script"],
|
||||
exclude_path: ["*.test.ts"],
|
||||
extra_include_path: ["g/**"]
|
||||
}
|
||||
}]
|
||||
}
|
||||
});
|
||||
|
||||
// Create initial wmill.yaml with different settings
|
||||
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
||||
includes:
|
||||
- f/**
|
||||
excludes: []
|
||||
skipVariables: false`);
|
||||
|
||||
// Pull with default flag
|
||||
const result = await backend.runCLICommand([
|
||||
'gitsync-settings', 'pull',
|
||||
'--repository', 'u/test/default_repo',
|
||||
'--default'
|
||||
], tempDir);
|
||||
|
||||
assertEquals(result.code, 0, `Default mode pull should succeed: ${result.stderr}`);
|
||||
|
||||
// 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");
|
||||
});
|
||||
});
|
||||
|
||||
// Removed test for non-existent repository error handling
|
||||
// as it was testing non-deterministic behavior
|
||||
|
||||
Deno.test("GitSync Settings: pull shows correct diff output", async () => {
|
||||
await withContainerizedBackend(async (backend, tempDir) => {
|
||||
// Set up workspace
|
||||
const testWorkspace = {
|
||||
remote: backend.baseUrl,
|
||||
workspaceId: backend.workspace,
|
||||
name: "diff_test",
|
||||
token: backend.token
|
||||
};
|
||||
await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir });
|
||||
|
||||
// Configure backend
|
||||
await backend.updateGitSyncConfig({
|
||||
git_sync_settings: {
|
||||
repositories: [{
|
||||
git_repo_resource_path: "u/test/diff_repo",
|
||||
script_path: "f/**",
|
||||
group_by_folder: false,
|
||||
use_individual_branch: false,
|
||||
settings: {
|
||||
include_path: ["f/**"],
|
||||
include_type: ["script", "flow"],
|
||||
exclude_path: [],
|
||||
extra_include_path: []
|
||||
}
|
||||
}]
|
||||
}
|
||||
});
|
||||
|
||||
// Create wmill.yaml with different settings
|
||||
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
||||
includes:
|
||||
- f/**
|
||||
excludes: []
|
||||
skipVariables: true
|
||||
skipResources: false`);
|
||||
|
||||
// Pull with diff flag
|
||||
const result = await backend.runCLICommand([
|
||||
'gitsync-settings', 'pull',
|
||||
'--repository', 'u/test/diff_repo',
|
||||
'--diff'
|
||||
], tempDir);
|
||||
|
||||
assertEquals(result.code, 0);
|
||||
|
||||
// Should show differences
|
||||
assertStringIncludes(result.stdout, "Changes that would be made:");
|
||||
// Should show the change for skipResources (ignoring ANSI color codes)
|
||||
assertStringIncludes(result.stdout, "skipResources:");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,193 @@
|
||||
import { assertEquals, assertStringIncludes, assert } from "https://deno.land/std@0.224.0/assert/mod.ts";
|
||||
import { withContainerizedBackend } from "./containerized_backend.ts";
|
||||
import { addWorkspace } from "../workspace.ts";
|
||||
import { parseJsonFromCLIOutput } from "./test_config_helpers.ts";
|
||||
|
||||
// =============================================================================
|
||||
// INCLUDE FLAGS BYPASS FILTERING TESTS
|
||||
// Tests that CLI include flags properly bypass path-based filtering
|
||||
// =============================================================================
|
||||
|
||||
// Helper function to set up workspace profile
|
||||
async function setupWorkspaceProfile(backend: any): Promise<void> {
|
||||
const testWorkspace = {
|
||||
remote: backend.baseUrl,
|
||||
workspaceId: backend.workspace,
|
||||
name: "localhost_test",
|
||||
token: backend.token
|
||||
};
|
||||
|
||||
await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir });
|
||||
}
|
||||
|
||||
// The ContainerizedBackend already creates test data we can use:
|
||||
// - admin user (admin@windmill.dev)
|
||||
// - test_group (created by seedTestData())
|
||||
// - workspace encryption key
|
||||
// - test apps, resources, variables via seedTestData()
|
||||
// No additional setup needed!
|
||||
|
||||
Deno.test("CLI include flags bypass restrictive path filtering", async () => {
|
||||
await withContainerizedBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
// Create wmill.yaml with very restrictive includes that would exclude special files
|
||||
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
||||
includes:
|
||||
- "f/**"
|
||||
excludes: []
|
||||
skipVariables: true
|
||||
skipResources: true
|
||||
includeUsers: false
|
||||
includeGroups: false
|
||||
includeSettings: false
|
||||
includeKey: false`);
|
||||
|
||||
// Test: CLI flags should override config and bypass path filtering
|
||||
const result = await backend.runCLICommand([
|
||||
'sync', 'pull',
|
||||
'--include-users',
|
||||
'--include-groups',
|
||||
'--include-settings',
|
||||
'--include-key',
|
||||
'--dry-run',
|
||||
'--json-output'
|
||||
], tempDir);
|
||||
|
||||
assertEquals(result.code, 0, `Command failed: ${result.stderr}`);
|
||||
|
||||
const output = parseJsonFromCLIOutput(result.stdout);
|
||||
const changePaths = output.changes.map((c: any) => c.path);
|
||||
|
||||
// Assert that special files are included despite restrictive path filtering
|
||||
const hasUser = changePaths.some((path: string) => path.includes('admin@windmill.dev.user.yaml'));
|
||||
const hasGroup = changePaths.some((path: string) => path.includes('groups/test_group.group.yaml'));
|
||||
const hasSettings = changePaths.some((path: string) => path === 'settings.yaml');
|
||||
const hasEncryptionKey = changePaths.some((path: string) => path === 'encryption_key.yaml');
|
||||
|
||||
assert(hasUser, `Admin user should be included despite restrictive includes. Found paths: ${changePaths.join(', ')}`);
|
||||
assert(hasGroup, `'test_group' should be included despite restrictive includes. Found paths: ${changePaths.join(', ')}`);
|
||||
assert(hasSettings, `Settings should be included despite restrictive includes. Found paths: ${changePaths.join(', ')}`);
|
||||
assert(hasEncryptionKey, `Encryption key should be included despite restrictive includes. Found paths: ${changePaths.join(', ')}`);
|
||||
});
|
||||
});
|
||||
|
||||
Deno.test("CLI flags override wmill.yaml include settings", async () => {
|
||||
await withContainerizedBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
// Config explicitly disables includes, but CLI should override
|
||||
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
||||
includes:
|
||||
- "**"
|
||||
excludes: []
|
||||
includeUsers: false
|
||||
includeGroups: false`);
|
||||
|
||||
// CLI flags should override config file settings
|
||||
const result = await backend.runCLICommand([
|
||||
'sync', 'pull',
|
||||
'--include-users',
|
||||
'--include-groups',
|
||||
'--dry-run',
|
||||
'--json-output'
|
||||
], tempDir);
|
||||
|
||||
assertEquals(result.code, 0, `Command failed: ${result.stderr}`);
|
||||
|
||||
const output = parseJsonFromCLIOutput(result.stdout);
|
||||
const changePaths = output.changes.map((c: any) => c.path);
|
||||
|
||||
const hasUser = changePaths.some((path: string) => path.includes('admin@windmill.dev.user.yaml'));
|
||||
const hasGroup = changePaths.some((path: string) => path.includes('groups/test_group.group.yaml'));
|
||||
|
||||
assert(hasUser, `CLI --include-users should override config includeUsers: false. Found paths: ${changePaths.join(', ')}`);
|
||||
assert(hasGroup, `CLI --include-groups should override config includeGroups: false. Found paths: ${changePaths.join(', ')}`);
|
||||
});
|
||||
});
|
||||
|
||||
Deno.test("Skip flags work correctly with getTypeStrFromPath and lock files", async () => {
|
||||
await withContainerizedBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
// Create wmill.yaml with skip flags enabled
|
||||
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
||||
includes:
|
||||
- "**"
|
||||
excludes: []
|
||||
skipScripts: true
|
||||
skipFlows: false
|
||||
includeUsers: true`);
|
||||
|
||||
const result = await backend.runCLICommand([
|
||||
'sync', 'pull',
|
||||
'--dry-run',
|
||||
'--json-output'
|
||||
], tempDir);
|
||||
|
||||
assertEquals(result.code, 0, `Command failed: ${result.stderr}`);
|
||||
|
||||
const output = parseJsonFromCLIOutput(result.stdout);
|
||||
const changePaths = output.changes.map((c: any) => c.path);
|
||||
|
||||
// Scripts should be skipped (including lock files) - the backend doesn't create scripts by default
|
||||
const hasScript = changePaths.some((path: string) =>
|
||||
path.endsWith('.py') || path.endsWith('.ts') || path.endsWith('.go') || path.endsWith('.sh')
|
||||
);
|
||||
const hasScriptLock = changePaths.some((path: string) => path.endsWith('.script.lock'));
|
||||
|
||||
// Apps should be included (the backend creates test apps)
|
||||
const hasApp = changePaths.some((path: string) => path.includes('test_dashboard') || path.endsWith('.app.yaml'));
|
||||
|
||||
// Users should still be included
|
||||
const hasUser = changePaths.some((path: string) => path.includes('admin@windmill.dev.user.yaml'));
|
||||
|
||||
assert(!hasScript, `Standalone scripts should be skipped when skipScripts: true. Found paths: ${changePaths.join(', ')}`);
|
||||
assert(!hasScriptLock, `Script lock files should be skipped when skipScripts: true. Found paths: ${changePaths.join(', ')}`);
|
||||
assert(hasApp, `Apps should be included (inline scripts are part of apps). Found paths: ${changePaths.join(', ')}`);
|
||||
assert(hasUser, `Users should be included when includeUsers: true. Found paths: ${changePaths.join(', ')}`);
|
||||
});
|
||||
});
|
||||
|
||||
Deno.test("Mixed include and skip flags work together", async () => {
|
||||
await withContainerizedBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
// Create restrictive config with mixed settings
|
||||
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
||||
includes:
|
||||
- "f/**"
|
||||
excludes: []
|
||||
skipScripts: true
|
||||
includeUsers: false
|
||||
includeSettings: false`);
|
||||
|
||||
const result = await backend.runCLICommand([
|
||||
'sync', 'pull',
|
||||
'--skip-scripts', // Reinforce script skipping
|
||||
'--include-users', // Override config to include users
|
||||
'--dry-run',
|
||||
'--json-output'
|
||||
], tempDir);
|
||||
|
||||
assertEquals(result.code, 0, `Command failed: ${result.stderr}`);
|
||||
|
||||
const output = parseJsonFromCLIOutput(result.stdout);
|
||||
const changePaths = output.changes.map((c: any) => c.path);
|
||||
|
||||
// Scripts should be excluded
|
||||
const hasScript = changePaths.some((path: string) =>
|
||||
path.endsWith('.py') || path.endsWith('.ts') || path.endsWith('.go') || path.endsWith('.sh')
|
||||
);
|
||||
|
||||
// Users should be included (CLI override)
|
||||
const hasUser = changePaths.some((path: string) => path.includes('admin@windmill.dev.user.yaml'));
|
||||
|
||||
// Settings should be excluded (no CLI override, restrictive path filtering)
|
||||
const hasSettings = changePaths.some((path: string) => path === 'settings.yaml');
|
||||
|
||||
assert(!hasScript, `Scripts should be excluded due to skipScripts. Found paths: ${changePaths.join(', ')}`);
|
||||
assert(hasUser, `Users should be included due to CLI --include-users override. Found paths: ${changePaths.join(', ')}`);
|
||||
assert(!hasSettings, `Settings should be excluded (no CLI override + restrictive paths). Found paths: ${changePaths.join(', ')}`);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,185 @@
|
||||
/**
|
||||
* Test to verify that wmill init handles workspaces with no git-sync settings correctly
|
||||
* This creates a unit test that directly tests the logic without needing a backend
|
||||
*/
|
||||
|
||||
import { assertEquals, assertStringIncludes, assert } from "https://deno.land/std@0.224.0/assert/mod.ts";
|
||||
import { DEFAULT_SYNC_OPTIONS } from "../conf.ts";
|
||||
import { withContainerizedBackend } from "./containerized_backend.ts";
|
||||
import { addWorkspace } from "../workspace.ts";
|
||||
|
||||
// Mock the workspace object
|
||||
const mockWorkspace = {
|
||||
remote: 'https://app.windmill.dev/',
|
||||
workspaceId: 'test-workspace',
|
||||
name: 'test-workspace'
|
||||
};
|
||||
|
||||
// Simulate the createWorkspaceProfile function logic for the "no repositories" case
|
||||
function createWorkspaceProfileNoRepos(workspace: any): any {
|
||||
const workspaceProfile: any = {
|
||||
baseUrl: workspace.remote,
|
||||
workspaceId: workspace.workspaceId,
|
||||
};
|
||||
|
||||
// Simulate the case where listRepositories returns empty array
|
||||
const repositories: any[] = [];
|
||||
|
||||
if (repositories.length === 0) {
|
||||
console.log(`No git repositories found in workspace '${workspace.workspaceId}'`);
|
||||
// This is the fix: include default sync settings when no repositories exist
|
||||
Object.assign(workspaceProfile, DEFAULT_SYNC_OPTIONS);
|
||||
return workspaceProfile;
|
||||
}
|
||||
|
||||
return workspaceProfile;
|
||||
}
|
||||
|
||||
Deno.test("Init: createWorkspaceProfile includes defaults when no repositories exist", () => {
|
||||
console.log('🧪 Testing init logic for workspace with no git-sync repositories...');
|
||||
|
||||
const workspaceProfile = createWorkspaceProfileNoRepos(mockWorkspace);
|
||||
|
||||
console.log('Generated workspace profile:', JSON.stringify(workspaceProfile, null, 2));
|
||||
|
||||
// Verify basic workspace info
|
||||
assertEquals(workspaceProfile.baseUrl, 'https://app.windmill.dev/');
|
||||
assertEquals(workspaceProfile.workspaceId, 'test-workspace');
|
||||
|
||||
// Verify default sync settings are included
|
||||
assert(Array.isArray(workspaceProfile.includes), 'Should have includes array');
|
||||
assertEquals(workspaceProfile.includes.length, 1, 'Should have one include pattern');
|
||||
assertEquals(workspaceProfile.includes[0], 'f/**', 'Should include f/** pattern');
|
||||
|
||||
assert(Array.isArray(workspaceProfile.excludes), 'Should have excludes array');
|
||||
assertEquals(workspaceProfile.excludes.length, 0, 'Should have empty excludes array');
|
||||
|
||||
assertEquals(workspaceProfile.defaultTs, 'bun', 'Should have bun as default TypeScript runtime');
|
||||
|
||||
console.log('✅ Workspace profile correctly includes default sync settings when no repositories exist');
|
||||
});
|
||||
|
||||
Deno.test("Init: verify DEFAULT_SYNC_OPTIONS has expected values", () => {
|
||||
console.log('🔍 Verifying DEFAULT_SYNC_OPTIONS contains expected values...');
|
||||
|
||||
console.log('DEFAULT_SYNC_OPTIONS:', JSON.stringify(DEFAULT_SYNC_OPTIONS, null, 2));
|
||||
|
||||
// Verify the default options include the expected f/** pattern
|
||||
assert(Array.isArray(DEFAULT_SYNC_OPTIONS.includes), 'DEFAULT_SYNC_OPTIONS should have includes array');
|
||||
assertEquals(DEFAULT_SYNC_OPTIONS.includes.length, 1, 'Should have one include pattern');
|
||||
assertEquals(DEFAULT_SYNC_OPTIONS.includes[0], 'f/**', 'Should default to f/** pattern');
|
||||
|
||||
assert(Array.isArray(DEFAULT_SYNC_OPTIONS.excludes), 'DEFAULT_SYNC_OPTIONS should have excludes array');
|
||||
assertEquals(DEFAULT_SYNC_OPTIONS.excludes.length, 0, 'Should have empty excludes array by default');
|
||||
|
||||
assertEquals(DEFAULT_SYNC_OPTIONS.defaultTs, 'bun', 'Should default to bun runtime');
|
||||
|
||||
console.log('✅ DEFAULT_SYNC_OPTIONS has expected values');
|
||||
});
|
||||
|
||||
Deno.test("Init: --use-backend flag applies git-sync settings", async () => {
|
||||
await withContainerizedBackend(async (backend, tempDir) => {
|
||||
// Set up workspace
|
||||
const testWorkspace = {
|
||||
remote: backend.baseUrl,
|
||||
workspaceId: backend.workspace,
|
||||
name: backend.workspace,
|
||||
token: backend.token
|
||||
};
|
||||
await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir });
|
||||
|
||||
// First create the git repository resource that the git-sync will reference
|
||||
await backend.createAdditionalGitRepo("u/test/init_repo", "Test init repository");
|
||||
|
||||
// Configure backend with git-sync settings
|
||||
await backend.updateGitSyncConfig({
|
||||
git_sync_settings: {
|
||||
repositories: [{
|
||||
git_repo_resource_path: "u/test/init_repo",
|
||||
script_path: "f/**",
|
||||
group_by_folder: false,
|
||||
use_individual_branch: false,
|
||||
settings: {
|
||||
include_path: ["f/backend/**"],
|
||||
include_type: ["script", "flow"],
|
||||
exclude_path: ["*.test.ts"],
|
||||
extra_include_path: ["g/**"]
|
||||
}
|
||||
}]
|
||||
}
|
||||
});
|
||||
|
||||
// Run init with --use-backend flag
|
||||
const result = await backend.runCLICommand([
|
||||
'init',
|
||||
'--use-backend',
|
||||
'--repository', 'u/test/init_repo'
|
||||
], tempDir);
|
||||
|
||||
assertEquals(result.code, 0, `Init with --use-backend should succeed: ${result.stderr}`);
|
||||
|
||||
// Verify wmill.yaml was created with backend settings
|
||||
const wmillYaml = await Deno.readTextFile(`${tempDir}/wmill.yaml`);
|
||||
|
||||
// Should have backend-applied settings written to top-level (not overrides)
|
||||
assertStringIncludes(wmillYaml, "f/backend/**", "Should include backend's include_path");
|
||||
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");
|
||||
});
|
||||
});
|
||||
|
||||
Deno.test("Init: --use-default bypasses backend settings check", async () => {
|
||||
await withContainerizedBackend(async (backend, tempDir) => {
|
||||
// Set up workspace
|
||||
const testWorkspace = {
|
||||
remote: backend.baseUrl,
|
||||
workspaceId: backend.workspace,
|
||||
name: backend.workspace,
|
||||
token: backend.token
|
||||
};
|
||||
await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir });
|
||||
|
||||
// Create git repository resource
|
||||
await backend.createAdditionalGitRepo("u/test/ignored_repo", "Test ignored repository");
|
||||
|
||||
// Configure backend with git-sync settings
|
||||
await backend.updateGitSyncConfig({
|
||||
git_sync_settings: {
|
||||
repositories: [{
|
||||
git_repo_resource_path: "u/test/ignored_repo",
|
||||
script_path: "f/**",
|
||||
group_by_folder: false,
|
||||
use_individual_branch: false,
|
||||
settings: {
|
||||
include_path: ["f/should-be-ignored/**"],
|
||||
include_type: ["script"],
|
||||
exclude_path: [],
|
||||
extra_include_path: []
|
||||
}
|
||||
}]
|
||||
}
|
||||
});
|
||||
|
||||
// Run init with --use-default (should ignore backend)
|
||||
const result = await backend.runCLICommand([
|
||||
'init',
|
||||
'--use-default'
|
||||
], tempDir);
|
||||
|
||||
assertEquals(result.code, 0, `Init with --use-default should succeed: ${result.stderr}`);
|
||||
|
||||
// Verify wmill.yaml was created with default settings only
|
||||
const wmillYaml = await Deno.readTextFile(`${tempDir}/wmill.yaml`);
|
||||
|
||||
// Should have default settings, not backend settings
|
||||
assertStringIncludes(wmillYaml, "includes:\n - f/**", "Should use default includes");
|
||||
assertStringIncludes(wmillYaml, "defaultTs: bun", "Should use default TypeScript runtime");
|
||||
|
||||
// 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");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,199 @@
|
||||
import { assertEquals, assert, assertStringIncludes } from "https://deno.land/std@0.224.0/assert/mod.ts";
|
||||
import { withContainerizedBackend } from "./containerized_backend.ts";
|
||||
import { addWorkspace } from "../workspace.ts";
|
||||
import { parseJsonFromCLIOutput } from "./test_config_helpers.ts";
|
||||
|
||||
// =============================================================================
|
||||
// MULTI-INSTANCE WORKSPACE TESTS
|
||||
// Tests for handling multiple Windmill instances with same workspace IDs
|
||||
// =============================================================================
|
||||
|
||||
// Helper function to set up workspace profile with specific name
|
||||
async function setupWorkspaceProfile(backend: any, workspaceName: string): Promise<void> {
|
||||
const testWorkspace = {
|
||||
remote: backend.baseUrl,
|
||||
workspaceId: backend.workspace,
|
||||
name: workspaceName,
|
||||
token: backend.token
|
||||
};
|
||||
|
||||
await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir });
|
||||
}
|
||||
|
||||
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
|
||||
includes:
|
||||
- f/**
|
||||
excludes: []
|
||||
|
||||
overrides:
|
||||
# Current backend instance (should match)
|
||||
"${backendUrl}:${backend.workspace}:u/test/test_repo":
|
||||
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:");
|
||||
// includeSchedules should show as a change since backend default is false
|
||||
assertStringIncludes(pullResult.stdout, "includeSchedules");
|
||||
});
|
||||
});
|
||||
|
||||
Deno.test("Multi-Instance: gitsync-settings push with overrides", async () => {
|
||||
await withContainerizedBackend(async (backend, tempDir) => {
|
||||
// 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
|
||||
includes:
|
||||
- f/**
|
||||
excludes: []
|
||||
skipVariables: false
|
||||
includeSchedules: false
|
||||
|
||||
overrides:
|
||||
# Override for current backend instance - set includeSchedules: true (backend default is false)
|
||||
"${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, "includeSchedules");
|
||||
});
|
||||
});
|
||||
|
||||
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:
|
||||
- f/**
|
||||
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) =>
|
||||
change.path?.includes('f/test_dashboard')
|
||||
);
|
||||
assertEquals(hasTestApp, false, "Test app should be skipped due to skipApps override");
|
||||
});
|
||||
});
|
||||
|
||||
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:
|
||||
- f/**
|
||||
excludes: []
|
||||
|
||||
overrides:
|
||||
# Single repository override - should be auto-detected
|
||||
"${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) =>
|
||||
change.path?.includes('f/test_dashboard')
|
||||
);
|
||||
assertEquals(hasTestApp, false, "Test app should be skipped due to auto-detected override");
|
||||
});
|
||||
});
|
||||
|
||||
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:
|
||||
- "**"
|
||||
excludes: []
|
||||
|
||||
overrides:
|
||||
# Wildcard for current backend instance
|
||||
"${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) =>
|
||||
change.path?.includes('u/admin/test_config.variable.yaml')
|
||||
);
|
||||
assertEquals(hasTestVariable, false, "Variables should be skipped due to wildcard override");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,273 @@
|
||||
import { assertEquals, assert } from "https://deno.land/std@0.224.0/assert/mod.ts";
|
||||
import { getEffectiveSettings } from "../conf.ts";
|
||||
import { withContainerizedBackend } from "./containerized_backend.ts";
|
||||
import { addWorkspace } from "../workspace.ts";
|
||||
|
||||
// =============================================================================
|
||||
// OVERRIDE SETTINGS BEHAVIOR TESTS
|
||||
// Tests for override inheritance and file filtering behavior
|
||||
// =============================================================================
|
||||
|
||||
Deno.test("Override Settings: override inherits non-overridden settings from base config", () => {
|
||||
const config = {
|
||||
includes: ["default/**"],
|
||||
skipVariables: true, // Base has this as true
|
||||
skipResources: true, // Base has this as true
|
||||
skipApps: false, // Base has this as false
|
||||
defaultTs: "bun" as const,
|
||||
overrides: {
|
||||
"http://localhost:8000/:test:u/user/repo": {
|
||||
includes: ["override/**"],
|
||||
skipApps: true // Override only changes skipApps, should inherit other skip flags
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const effective = getEffectiveSettings(
|
||||
config,
|
||||
"http://localhost:8000/",
|
||||
"test",
|
||||
"u/user/repo"
|
||||
);
|
||||
|
||||
// Override values should be used
|
||||
assertEquals(effective.includes, ["override/**"], "Must use override includes");
|
||||
assertEquals(effective.skipApps, true, "Must use override skipApps");
|
||||
|
||||
// Should inherit skip flags from base config
|
||||
assertEquals(effective.skipVariables, true, "Must inherit skipVariables=true from base config");
|
||||
assertEquals(effective.skipResources, true, "Must inherit skipResources=true from base config");
|
||||
assertEquals(effective.defaultTs, "bun", "Must inherit defaultTs from base config");
|
||||
});
|
||||
|
||||
Deno.test("Override Settings: workspace wildcards with repo-specific precedence", () => {
|
||||
const config = {
|
||||
includes: ["default/**"],
|
||||
skipVariables: false,
|
||||
overrides: {
|
||||
"http://localhost:8000/:test:*": {
|
||||
skipVariables: true,
|
||||
includes: ["workspace/**"]
|
||||
},
|
||||
"http://localhost:8000/:test:u/user/specific": {
|
||||
includes: ["specific/**"]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Test specific repo override (should take precedence over wildcard)
|
||||
const specificEffective = getEffectiveSettings(
|
||||
config,
|
||||
"http://localhost:8000/",
|
||||
"test",
|
||||
"u/user/specific"
|
||||
);
|
||||
assertEquals(specificEffective.includes, ["specific/**"], "Specific repo override must take precedence over wildcard");
|
||||
assertEquals(specificEffective.skipVariables, true, "Workspace wildcard setting must still apply");
|
||||
|
||||
// Test wildcard match
|
||||
const wildcardEffective = getEffectiveSettings(
|
||||
config,
|
||||
"http://localhost:8000/",
|
||||
"test",
|
||||
"u/user/other"
|
||||
);
|
||||
assertEquals(wildcardEffective.includes, ["workspace/**"], "Wildcard must match repos without specific overrides");
|
||||
assertEquals(wildcardEffective.skipVariables, true, "Workspace wildcard setting must apply");
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// INTEGRATION TESTS - File Filtering Behavior
|
||||
// =============================================================================
|
||||
|
||||
Deno.test("Integration: sync pull with skipVariables override excludes variable files", async () => {
|
||||
await withContainerizedBackend(async (backend, tempDir) => {
|
||||
// Set up workspace
|
||||
const testWorkspace = {
|
||||
remote: backend.baseUrl,
|
||||
workspaceId: backend.workspace,
|
||||
name: "skip_variables_test",
|
||||
token: backend.token
|
||||
};
|
||||
await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir });
|
||||
|
||||
// Create wmill.yaml with override that skips variables
|
||||
const backendUrl = new URL(backend.baseUrl).toString();
|
||||
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
||||
includes:
|
||||
- "**"
|
||||
skipVariables: false
|
||||
|
||||
overrides:
|
||||
"${backendUrl}:${backend.workspace}:u/test/test_repo":
|
||||
skipVariables: true`);
|
||||
|
||||
// Verify backend has test variable before pull
|
||||
const backendVariables = await backend.listAllVariables();
|
||||
const hasTestVariable = backendVariables.some(v => v.path === 'u/admin/test_config');
|
||||
assert(hasTestVariable, "Backend should have test variable before pull");
|
||||
|
||||
// Run sync pull (NOT dry-run) to actually write files
|
||||
const result = await backend.runCLICommand([
|
||||
'sync', 'pull',
|
||||
'--repository', 'u/test/test_repo',
|
||||
'--yes'
|
||||
], tempDir);
|
||||
|
||||
assertEquals(result.code, 0, `Sync pull should succeed: ${result.stderr}`);
|
||||
|
||||
// Verify variable files were NOT written to filesystem due to skipVariables: true
|
||||
const filesWritten = [];
|
||||
for await (const entry of Deno.readDir(tempDir)) {
|
||||
if (entry.isFile && entry.name.endsWith('.yaml')) {
|
||||
filesWritten.push(entry.name);
|
||||
}
|
||||
}
|
||||
|
||||
const hasVariableFile = filesWritten.some(file => file.includes('.variable.yaml'));
|
||||
assertEquals(hasVariableFile, false, "Variable files should NOT be written due to skipVariables override");
|
||||
|
||||
// Verify other files WERE written (since skipVariables only affects variables)
|
||||
// Check what files were actually written
|
||||
console.log("Files written:", filesWritten);
|
||||
|
||||
// Should have some files written (just not variable files)
|
||||
assert(filesWritten.length > 0, `Some files should be written when skipVariables is true. Got: ${filesWritten.join(', ')}`);
|
||||
|
||||
// Should not have only wmill.yaml file
|
||||
const nonWmillFiles = filesWritten.filter(f => !f.includes('wmill.yaml'));
|
||||
assert(nonWmillFiles.length > 0, `Non-wmill.yaml files should be written when skipVariables is true. Got: ${nonWmillFiles.join(', ')}`);
|
||||
});
|
||||
});
|
||||
|
||||
Deno.test("Integration: sync push with skipVariables override excludes variable files", async () => {
|
||||
await withContainerizedBackend(async (backend, tempDir) => {
|
||||
// Set up workspace
|
||||
const testWorkspace = {
|
||||
remote: backend.baseUrl,
|
||||
workspaceId: backend.workspace,
|
||||
name: "push_skip_test",
|
||||
token: backend.token
|
||||
};
|
||||
await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir });
|
||||
|
||||
// Create wmill.yaml with override that skips variables
|
||||
const backendUrl = new URL(backend.baseUrl).toString();
|
||||
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
||||
includes:
|
||||
- "**"
|
||||
skipVariables: false
|
||||
|
||||
overrides:
|
||||
"${backendUrl}:${backend.workspace}:u/test/test_repo":
|
||||
skipVariables: true`);
|
||||
|
||||
// Create local test files including variables and scripts
|
||||
const timestamp = Date.now();
|
||||
|
||||
// Create variable file
|
||||
await Deno.mkdir(`${tempDir}/u/admin`, { recursive: true });
|
||||
await Deno.writeTextFile(`${tempDir}/u/admin/test_push_var_${timestamp}.variable.yaml`,
|
||||
`value: test_value_${timestamp}
|
||||
description: Test variable for push override test
|
||||
is_secret: false`);
|
||||
|
||||
// Create script file
|
||||
await Deno.mkdir(`${tempDir}/f/test`, { recursive: true });
|
||||
await Deno.writeTextFile(`${tempDir}/f/test/push_script_${timestamp}.ts`,
|
||||
`export async function main() {
|
||||
return "Test script ${timestamp}";
|
||||
}`);
|
||||
await Deno.writeTextFile(`${tempDir}/f/test/push_script_${timestamp}.script.yaml`,
|
||||
`summary: Test Push Script ${timestamp}
|
||||
description: Script for testing push with override`);
|
||||
|
||||
// Get backend state before push
|
||||
const beforeVariables = await backend.listAllVariables();
|
||||
const beforeScripts = await backend.listAllScripts();
|
||||
|
||||
const variableExistsBefore = beforeVariables.some(v => v.path === `u/admin/test_push_var_${timestamp}`);
|
||||
const scriptExistsBefore = beforeScripts.some(s => s.path === `f/test/push_script_${timestamp}`);
|
||||
|
||||
assertEquals(variableExistsBefore, false, "Variable should not exist before push");
|
||||
assertEquals(scriptExistsBefore, false, "Script should not exist before push");
|
||||
|
||||
// Run sync push (NOT dry-run) to actually push files
|
||||
const result = await backend.runCLICommand([
|
||||
'sync', 'push',
|
||||
'--repository', 'u/test/test_repo',
|
||||
'--yes'
|
||||
], tempDir);
|
||||
|
||||
assertEquals(result.code, 0, `Sync push should succeed: ${result.stderr}`);
|
||||
|
||||
// Verify variable was NOT pushed due to skipVariables: true
|
||||
const afterVariables = await backend.listAllVariables();
|
||||
const variableExistsAfter = afterVariables.some(v => v.path === `u/admin/test_push_var_${timestamp}`);
|
||||
assertEquals(variableExistsAfter, false, "Variable should NOT be pushed due to skipVariables override");
|
||||
|
||||
// Verify script WAS pushed (not affected by skipVariables)
|
||||
const afterScripts = await backend.listAllScripts();
|
||||
const scriptExistsAfter = afterScripts.some(s => s.path === `f/test/push_script_${timestamp}`);
|
||||
assertEquals(scriptExistsAfter, true, "Script should be pushed normally");
|
||||
});
|
||||
});
|
||||
|
||||
Deno.test("Integration: sync pull respects includes override for file filtering", async () => {
|
||||
await withContainerizedBackend(async (backend, tempDir) => {
|
||||
// Set up workspace
|
||||
const testWorkspace = {
|
||||
remote: backend.baseUrl,
|
||||
workspaceId: backend.workspace,
|
||||
name: "includes_test",
|
||||
token: backend.token
|
||||
};
|
||||
await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir });
|
||||
|
||||
// Create wmill.yaml with override that only includes specific path
|
||||
const backendUrl = new URL(backend.baseUrl).toString();
|
||||
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
||||
includes:
|
||||
- "**"
|
||||
|
||||
overrides:
|
||||
"${backendUrl}:${backend.workspace}:u/test/test_repo":
|
||||
includes:
|
||||
- "u/admin/**" # Only include admin resources, exclude f/** apps/scripts`);
|
||||
|
||||
// Run sync pull to write files
|
||||
const result = await backend.runCLICommand([
|
||||
'sync', 'pull',
|
||||
'--repository', 'u/test/test_repo',
|
||||
'--yes'
|
||||
], tempDir);
|
||||
|
||||
assertEquals(result.code, 0, `Sync pull should succeed: ${result.stderr}`);
|
||||
|
||||
// Verify admin files were written (since we have includes: ["u/admin/**"])
|
||||
const adminFiles = [];
|
||||
try {
|
||||
for await (const entry of Deno.readDir(`${tempDir}/u/admin`)) {
|
||||
if (entry.isFile) {
|
||||
adminFiles.push(`u/admin/${entry.name}`);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Directory might not exist if no files matched
|
||||
}
|
||||
|
||||
// We expect admin files to be written since backend has u/admin/test_config variable
|
||||
assert(adminFiles.length > 0, `Admin files should be written due to includes override. Expected u/admin files but found: ${adminFiles.join(', ')}`);
|
||||
|
||||
// Verify f/** files were NOT written due to includes override
|
||||
let fDirectoryExists = false;
|
||||
try {
|
||||
await Deno.stat(`${tempDir}/f`);
|
||||
fDirectoryExists = true;
|
||||
} catch {
|
||||
// Directory doesn't exist, which is expected
|
||||
}
|
||||
|
||||
assertEquals(fDirectoryExists, false, "f/ directory should not exist due to includes override excluding f/**");
|
||||
});
|
||||
});
|
||||
@@ -1,48 +0,0 @@
|
||||
{
|
||||
"workspace_id": "starter",
|
||||
"name": "postgres",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"required": [
|
||||
"dbname",
|
||||
"user",
|
||||
"password"
|
||||
],
|
||||
"properties": {
|
||||
"host": {
|
||||
"type": "string",
|
||||
"description": "The instance host"
|
||||
},
|
||||
"port": {
|
||||
"type": "integer",
|
||||
"description": "The instance port"
|
||||
},
|
||||
"user": {
|
||||
"type": "string",
|
||||
"description": "The postgres username"
|
||||
},
|
||||
"dbname": {
|
||||
"type": "string",
|
||||
"description": "The database name"
|
||||
},
|
||||
"sslmode": {
|
||||
"enum": [
|
||||
"disable",
|
||||
"allow",
|
||||
"prefer",
|
||||
"require",
|
||||
"verify-ca",
|
||||
"verify-full"
|
||||
],
|
||||
"type": "string",
|
||||
"description": "The sslmode"
|
||||
},
|
||||
"password": {
|
||||
"type": "string",
|
||||
"description": "The postgres users password"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "A postgres database connection resource"
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"workspace_id": "starter",
|
||||
"name": "slack",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"required": [],
|
||||
"properties": {
|
||||
"token": {
|
||||
"type": "string",
|
||||
"description": "The slack token"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "A slack token to interact with a specific workspace. Can be obtained from the OAuth integration in the workspace settings."
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"workspace_id": "starter",
|
||||
"path": "g/all/demodb",
|
||||
"value": {
|
||||
"host": "demodb.service.consul",
|
||||
"port": "6543",
|
||||
"user": "postgres",
|
||||
"dbname": "demodb",
|
||||
"sslmode": "disable",
|
||||
"password": "demodb"
|
||||
},
|
||||
"description": "demodb",
|
||||
"resource_type": "postgres",
|
||||
"extra_perms": {},
|
||||
"is_oauth": false
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
import { assertEquals, assertStringIncludes, assert } from "https://deno.land/std@0.224.0/assert/mod.ts";
|
||||
import { readConfigFile, getEffectiveSettings } from "../conf.ts";
|
||||
import { withContainerizedBackend } from "./containerized_backend.ts";
|
||||
import { addWorkspace } from "../workspace.ts";
|
||||
import { parseJsonFromCLIOutput } from "./test_config_helpers.ts";
|
||||
|
||||
// =============================================================================
|
||||
// SYNC CONFIGURATION RESOLUTION TESTS
|
||||
// Tests for configuration resolution and integration with backend
|
||||
// =============================================================================
|
||||
|
||||
// Helper function to set up workspace profile with localhost_test name
|
||||
async function setupWorkspaceProfile(backend: any): Promise<void> {
|
||||
const testWorkspace = {
|
||||
remote: backend.baseUrl, // "http://localhost:8001/"
|
||||
workspaceId: backend.workspace, // "test"
|
||||
name: "localhost_test", // This is what the tests expect!
|
||||
token: backend.token
|
||||
};
|
||||
|
||||
await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir });
|
||||
}
|
||||
|
||||
|
||||
// =============================================================================
|
||||
// INTEGRATION TESTS WITH REAL BACKEND
|
||||
// =============================================================================
|
||||
|
||||
Deno.test("Integration: wmill.yaml configuration produces expected results", async () => {
|
||||
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:
|
||||
- f/**
|
||||
- settings.yaml
|
||||
excludes:
|
||||
- "*.test.ts"
|
||||
skipVariables: true
|
||||
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) {
|
||||
console.log("YAML command failed!");
|
||||
console.log("Exit code:", yamlResult.code);
|
||||
console.log("Stdout:", yamlResult.stdout);
|
||||
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) =>
|
||||
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) =>
|
||||
change.type === 'added' && change.path?.includes('.resource.yaml')
|
||||
);
|
||||
const hasVariables = (yamlData.changes || []).some((change: any) =>
|
||||
change.type === 'added' && change.path?.includes('.variable.yaml')
|
||||
);
|
||||
assertEquals(hasResources, false);
|
||||
assertEquals(hasVariables, false);
|
||||
});
|
||||
});
|
||||
|
||||
Deno.test("Integration: settings.yaml inclusion respects includeSettings flag", async () => {
|
||||
await withContainerizedBackend(async (backend, tempDir) => {
|
||||
// Set up workspace profile with name "localhost_test"
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
// 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) =>
|
||||
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) =>
|
||||
change.type === 'added' && change.path === 'settings.yaml'
|
||||
);
|
||||
assertEquals(hasSettingsExclude, false);
|
||||
});
|
||||
});
|
||||
|
||||
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
|
||||
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) =>
|
||||
change.type === 'added' && change.path?.includes('.resource.yaml')
|
||||
);
|
||||
assertEquals(hasResources, false);
|
||||
|
||||
// Should include variables (not skipped)
|
||||
const hasVariables = (data.changes || []).some((change: any) =>
|
||||
change.type === 'added' && change.path?.includes('.variable.yaml')
|
||||
);
|
||||
assertEquals(hasVariables, true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
import { getRootStore } from "../store.ts";
|
||||
|
||||
/**
|
||||
* Create a temporary config directory for testing that doesn't interfere with user's config
|
||||
*/
|
||||
export async function withTestConfig<T>(callback: (testConfigDir: string) => Promise<T>): Promise<T> {
|
||||
// Create a unique temporary directory for this test
|
||||
const testDir = await Deno.makeTempDir({ prefix: "wmill_test_config_" });
|
||||
|
||||
try {
|
||||
return await callback(testDir);
|
||||
} finally {
|
||||
// Clean up the temporary directory
|
||||
try {
|
||||
await Deno.remove(testDir, { recursive: true });
|
||||
} catch (error) {
|
||||
console.warn(`Failed to clean up test config directory ${testDir}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the remotes file in test config directory
|
||||
*/
|
||||
export async function clearTestRemotes(testConfigDir: string): Promise<void> {
|
||||
const remoteFile = (await getRootStore(testConfigDir)) + "remotes.ndjson";
|
||||
await Deno.writeTextFile(remoteFile, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse JSON output from CLI command, handling log messages that appear before JSON
|
||||
*/
|
||||
export function parseJsonFromCLIOutput(stdout: string): any {
|
||||
const jsonMatch = stdout.match(/\{[\s\S]*\}/);
|
||||
if (!jsonMatch) {
|
||||
throw new Error(`No JSON found in CLI output: ${stdout}`);
|
||||
}
|
||||
return JSON.parse(jsonMatch[0]);
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
import { assertEquals, assertRejects } from "https://deno.land/std@0.224.0/assert/mod.ts";
|
||||
import { addWorkspace, allWorkspaces } from "../workspace.ts";
|
||||
import { withTestConfig, clearTestRemotes } from "./test_config_helpers.ts";
|
||||
|
||||
// Test workspace conflict detection
|
||||
Deno.test("addWorkspace: prevents duplicate workspace names", async () => {
|
||||
await withTestConfig(async (testConfigDir) => {
|
||||
await clearTestRemotes(testConfigDir);
|
||||
|
||||
// Add first workspace
|
||||
const workspace1 = {
|
||||
name: "test_workspace",
|
||||
remote: "http://localhost:8001/",
|
||||
workspaceId: "workspace1",
|
||||
token: "token1"
|
||||
};
|
||||
|
||||
await addWorkspace(workspace1, { force: true, configDir: testConfigDir });
|
||||
|
||||
// Try to add workspace with same name but different details
|
||||
const workspace2 = {
|
||||
name: "test_workspace", // Same name
|
||||
remote: "http://localhost:8002/", // Different remote
|
||||
workspaceId: "workspace2", // Different ID
|
||||
token: "token2"
|
||||
};
|
||||
|
||||
// Should throw error in non-interactive mode without force
|
||||
await assertRejects(
|
||||
() => addWorkspace(workspace2, { configDir: testConfigDir }),
|
||||
Error,
|
||||
"Workspace name conflict. Use --force to overwrite or choose a different name."
|
||||
);
|
||||
|
||||
// Should succeed with force flag
|
||||
await addWorkspace(workspace2, { force: true, configDir: testConfigDir });
|
||||
|
||||
// Verify the workspace was overwritten
|
||||
const workspaces = await allWorkspaces(testConfigDir);
|
||||
assertEquals(workspaces.length, 1);
|
||||
assertEquals(workspaces[0].name, "test_workspace");
|
||||
assertEquals(workspaces[0].remote, "http://localhost:8002/");
|
||||
assertEquals(workspaces[0].workspaceId, "workspace2");
|
||||
});
|
||||
});
|
||||
|
||||
Deno.test("addWorkspace: prevents duplicate (remote, workspaceId) tuples", async () => {
|
||||
await withTestConfig(async (testConfigDir) => {
|
||||
await clearTestRemotes(testConfigDir);
|
||||
|
||||
// Add first workspace
|
||||
const workspace1 = {
|
||||
name: "first_workspace",
|
||||
remote: "http://localhost:8001/",
|
||||
workspaceId: "test",
|
||||
token: "token1"
|
||||
};
|
||||
|
||||
await addWorkspace(workspace1, { force: true, configDir: testConfigDir });
|
||||
|
||||
// Try to add workspace with same (remote, workspaceId) but different name
|
||||
const workspace2 = {
|
||||
name: "second_workspace", // Different name
|
||||
remote: "http://localhost:8001/", // Same remote
|
||||
workspaceId: "test", // Same workspaceId
|
||||
token: "token2"
|
||||
};
|
||||
|
||||
// Should throw error in non-interactive mode without force
|
||||
await assertRejects(
|
||||
() => addWorkspace(workspace2, { configDir: testConfigDir }),
|
||||
Error,
|
||||
'Backend constraint violation: (http://localhost:8001/, test) already exists as "first_workspace". Use --force to overwrite.'
|
||||
);
|
||||
|
||||
// Should succeed with force flag (overwrites first workspace)
|
||||
await addWorkspace(workspace2, { force: true, configDir: testConfigDir });
|
||||
|
||||
// Verify the first workspace was removed and second was added
|
||||
const workspaces = await allWorkspaces(testConfigDir);
|
||||
assertEquals(workspaces.length, 1);
|
||||
assertEquals(workspaces[0].name, "second_workspace");
|
||||
assertEquals(workspaces[0].remote, "http://localhost:8001/");
|
||||
assertEquals(workspaces[0].workspaceId, "test");
|
||||
});
|
||||
});
|
||||
|
||||
Deno.test("addWorkspace: allows same workspace (name, remote, workspaceId) with token update", async () => {
|
||||
await withTestConfig(async (testConfigDir) => {
|
||||
await clearTestRemotes(testConfigDir);
|
||||
|
||||
// Add first workspace
|
||||
const workspace1 = {
|
||||
name: "same_workspace",
|
||||
remote: "http://localhost:8001/",
|
||||
workspaceId: "test",
|
||||
token: "old_token"
|
||||
};
|
||||
|
||||
await addWorkspace(workspace1, { force: true, configDir: testConfigDir });
|
||||
|
||||
// Add same workspace with updated token
|
||||
const workspace2 = {
|
||||
name: "same_workspace", // Same name
|
||||
remote: "http://localhost:8001/", // Same remote
|
||||
workspaceId: "test", // Same workspaceId
|
||||
token: "new_token" // Different token
|
||||
};
|
||||
|
||||
// Should succeed without force (just token update)
|
||||
await addWorkspace(workspace2, { configDir: testConfigDir });
|
||||
|
||||
// Verify token was updated
|
||||
const workspaces = await allWorkspaces(testConfigDir);
|
||||
assertEquals(workspaces.length, 1);
|
||||
assertEquals(workspaces[0].name, "same_workspace");
|
||||
assertEquals(workspaces[0].token, "new_token");
|
||||
});
|
||||
});
|
||||
|
||||
Deno.test("addWorkspace: allows different workspaces on different remotes", async () => {
|
||||
await withTestConfig(async (testConfigDir) => {
|
||||
await clearTestRemotes(testConfigDir);
|
||||
|
||||
// Add workspace on first remote
|
||||
const workspace1 = {
|
||||
name: "workspace_remote1",
|
||||
remote: "http://localhost:8001/",
|
||||
workspaceId: "test",
|
||||
token: "token1"
|
||||
};
|
||||
|
||||
await addWorkspace(workspace1, { force: true, configDir: testConfigDir });
|
||||
|
||||
// Add workspace with same workspaceId on different remote (should be allowed)
|
||||
const workspace2 = {
|
||||
name: "workspace_remote2",
|
||||
remote: "http://localhost:8002/", // Different remote
|
||||
workspaceId: "test", // Same workspaceId (OK on different remote)
|
||||
token: "token2"
|
||||
};
|
||||
|
||||
// Should succeed (different remotes)
|
||||
await addWorkspace(workspace2, { configDir: testConfigDir });
|
||||
|
||||
// Verify both workspaces exist
|
||||
const workspaces = await allWorkspaces(testConfigDir);
|
||||
assertEquals(workspaces.length, 2);
|
||||
|
||||
const names = workspaces.map(w => w.name).sort();
|
||||
assertEquals(names, ["workspace_remote1", "workspace_remote2"]);
|
||||
});
|
||||
});
|
||||
+2
-2
@@ -49,6 +49,7 @@ export type GlobalOptions = {
|
||||
baseUrl: string | undefined;
|
||||
workspace: string | undefined;
|
||||
token: string | undefined;
|
||||
configDir: string | undefined;
|
||||
};
|
||||
|
||||
export function isSuperset(
|
||||
@@ -231,12 +232,11 @@ export function getTypeStrFromPath(
|
||||
parsed.ext == ".cs" ||
|
||||
parsed.ext == ".nu" ||
|
||||
parsed.ext == ".java" ||
|
||||
// for related places search: ADD_NEW_LANG
|
||||
// for related places search: ADD_NEW_LANG
|
||||
(parsed.ext == ".yml" && parsed.name.split(".").pop() == "playbook")
|
||||
) {
|
||||
return "script";
|
||||
}
|
||||
|
||||
if (parsed.name === "folder.meta") {
|
||||
return "folder";
|
||||
}
|
||||
|
||||
@@ -149,4 +149,51 @@ export function printSync(input: string | Uint8Array, to = Deno.stdout) {
|
||||
while (bytesWritten < bytes.length) {
|
||||
bytesWritten += to.writeSync(bytes.subarray(bytesWritten))
|
||||
}
|
||||
}
|
||||
|
||||
// Repository interface for shared selection logic
|
||||
export interface Repository {
|
||||
git_repo_resource_path: string;
|
||||
}
|
||||
|
||||
// Shared repository selection logic
|
||||
export async function selectRepository<T extends Repository>(
|
||||
repositories: T[],
|
||||
operation?: string
|
||||
): Promise<T> {
|
||||
if (repositories.length === 0) {
|
||||
throw new Error("No git-sync repositories configured in workspace");
|
||||
}
|
||||
|
||||
if (repositories.length === 1) {
|
||||
const repoPath = repositories[0].git_repo_resource_path.replace(/^\$res:/, "");
|
||||
log.info(`Using repository: ${repoPath}`);
|
||||
return repositories[0];
|
||||
}
|
||||
|
||||
// Check if we're in a non-interactive environment
|
||||
const isInteractive = Deno.stdin.isTerminal() && Deno.stdout.isTerminal();
|
||||
|
||||
if (!isInteractive) {
|
||||
const repoPaths = repositories.map(r => r.git_repo_resource_path.replace(/^\$res:/, ""));
|
||||
throw new Error(`Multiple repositories found: ${repoPaths.join(', ')}. Use --repository to specify which one to ${operation || 'use'}.`);
|
||||
}
|
||||
|
||||
// Import Select dynamically to avoid dependency issues
|
||||
const { Select } = await import("./deps.ts");
|
||||
|
||||
console.log(`\nMultiple repositories found. Please select which repository to ${operation || 'use'}:\n`);
|
||||
|
||||
const selectedRepo = await Select.prompt({
|
||||
message: `Select repository for ${operation || 'operation'}:`,
|
||||
options: repositories.map((repo, index) => {
|
||||
const displayPath = repo.git_repo_resource_path.replace(/^\$res:/, "");
|
||||
return {
|
||||
name: `${index + 1}. ${displayPath}`,
|
||||
value: repo.git_repo_resource_path
|
||||
};
|
||||
})
|
||||
});
|
||||
|
||||
return repositories.find((r) => r.git_repo_resource_path === selectedRepo)!;
|
||||
}
|
||||
+94
-16
@@ -2,7 +2,7 @@
|
||||
import { GlobalOptions } from "./types.ts";
|
||||
import { getRootStore } from "./store.ts";
|
||||
import { loginInteractive, tryGetLoginInfo } from "./login.ts";
|
||||
import { colors, Command, Input, log, setClient, Table } from "./deps.ts";
|
||||
import { colors, Command, Confirm, Input, log, setClient, Table } from "./deps.ts";
|
||||
|
||||
import * as wmill from "./gen/services.gen.ts";
|
||||
import { requireLogin } from "./context.ts";
|
||||
@@ -14,9 +14,9 @@ export interface Workspace {
|
||||
token: string;
|
||||
}
|
||||
|
||||
export async function allWorkspaces(): Promise<Workspace[]> {
|
||||
export async function allWorkspaces(configDirOverride?: string): Promise<Workspace[]> {
|
||||
try {
|
||||
const file = (await getRootStore()) + "remotes.ndjson";
|
||||
const file = (await getRootStore(configDirOverride)) + "remotes.ndjson";
|
||||
const txt = await Deno.readTextFile(file);
|
||||
return txt
|
||||
.split("\n")
|
||||
@@ -40,7 +40,7 @@ async function getActiveWorkspaceName(
|
||||
return opts?.workspace;
|
||||
}
|
||||
try {
|
||||
return await Deno.readTextFile((await getRootStore()) + "/activeWorkspace");
|
||||
return await Deno.readTextFile((await getRootStore(opts?.configDir)) + "/activeWorkspace");
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
@@ -53,13 +53,14 @@ export async function getActiveWorkspace(
|
||||
if (!name) {
|
||||
return undefined;
|
||||
}
|
||||
return await getWorkspaceByName(name);
|
||||
return await getWorkspaceByName(name, opts?.configDir);
|
||||
}
|
||||
|
||||
export async function getWorkspaceByName(
|
||||
workspaceName: string
|
||||
workspaceName: string,
|
||||
configDirOverride?: string
|
||||
): Promise<Workspace | undefined> {
|
||||
const workspaceStream = await allWorkspaces();
|
||||
const workspaceStream = await allWorkspaces(configDirOverride);
|
||||
for await (const workspace of workspaceStream) {
|
||||
if (workspace.name === workspaceName) {
|
||||
return workspace;
|
||||
@@ -69,7 +70,7 @@ export async function getWorkspaceByName(
|
||||
}
|
||||
|
||||
async function list(opts: GlobalOptions) {
|
||||
const workspaces = await allWorkspaces();
|
||||
const workspaces = await allWorkspaces(opts.configDir);
|
||||
const activeName = await getActiveWorkspaceName(opts);
|
||||
|
||||
new Table()
|
||||
@@ -101,7 +102,7 @@ async function switchC(opts: GlobalOptions, workspaceName: string) {
|
||||
return;
|
||||
}
|
||||
|
||||
const all = await allWorkspaces();
|
||||
const all = await allWorkspaces(opts.configDir);
|
||||
if (all.findIndex((x) => x.name === workspaceName) === -1) {
|
||||
log.info(
|
||||
colors.red.bold(
|
||||
@@ -115,13 +116,13 @@ async function switchC(opts: GlobalOptions, workspaceName: string) {
|
||||
return;
|
||||
}
|
||||
|
||||
await setActiveWorkspace(workspaceName);
|
||||
await setActiveWorkspace(workspaceName, opts.configDir);
|
||||
return;
|
||||
}
|
||||
|
||||
export async function setActiveWorkspace(workspaceName: string) {
|
||||
export async function setActiveWorkspace(workspaceName: string, configDirOverride?: string) {
|
||||
await Deno.writeTextFile(
|
||||
(await getRootStore()) + "/activeWorkspace",
|
||||
(await getRootStore(configDirOverride)) + "/activeWorkspace",
|
||||
workspaceName
|
||||
);
|
||||
}
|
||||
@@ -246,7 +247,7 @@ export async function add(
|
||||
},
|
||||
opts
|
||||
);
|
||||
await setActiveWorkspace(workspaceName);
|
||||
await setActiveWorkspace(workspaceName, opts.configDir);
|
||||
|
||||
log.info(
|
||||
colors.green.underline(
|
||||
@@ -257,8 +258,85 @@ export async function add(
|
||||
|
||||
export async function addWorkspace(workspace: Workspace, opts: any) {
|
||||
workspace.remote = new URL(workspace.remote).toString(); // add trailing slash in all cases!
|
||||
|
||||
// Check for conflicts before adding
|
||||
const existingWorkspaces = await allWorkspaces(opts.configDir);
|
||||
const isInteractive = Deno.stdin.isTerminal() && Deno.stdout.isTerminal() && !opts.force;
|
||||
|
||||
// Check 1: Workspace name already exists
|
||||
const nameConflict = existingWorkspaces.find(w => w.name === workspace.name);
|
||||
if (nameConflict) {
|
||||
// If it's the exact same workspace (same remote + workspaceId), just update the token
|
||||
if (nameConflict.remote === workspace.remote && nameConflict.workspaceId === workspace.workspaceId) {
|
||||
log.info(colors.yellow(`Updating token for existing workspace "${workspace.name}"`));
|
||||
} else {
|
||||
// Different remote or workspaceId - this is a conflict
|
||||
log.info(colors.red.bold(`❌ Workspace name "${workspace.name}" already exists!`));
|
||||
log.info(` Existing: ${nameConflict.workspaceId} on ${nameConflict.remote}`);
|
||||
log.info(` New: ${workspace.workspaceId} on ${workspace.remote}`);
|
||||
|
||||
if (!isInteractive) {
|
||||
// In non-interactive mode (tests, scripts), auto-overwrite with force flag
|
||||
if (opts.force) {
|
||||
log.info(colors.yellow("Force flag enabled, overwriting existing workspace."));
|
||||
} else {
|
||||
throw new Error("Workspace name conflict. Use --force to overwrite or choose a different name.");
|
||||
}
|
||||
} else {
|
||||
const overwrite = await Confirm.prompt({
|
||||
message: "Do you want to overwrite the existing workspace?",
|
||||
default: false,
|
||||
});
|
||||
|
||||
if (!overwrite) {
|
||||
log.info(colors.yellow("Operation cancelled."));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check 2: Same (remote, workspaceId) tuple already exists under different name
|
||||
const tupleConflict = existingWorkspaces.find(w =>
|
||||
w.remote === workspace.remote &&
|
||||
w.workspaceId === workspace.workspaceId &&
|
||||
w.name !== workspace.name
|
||||
);
|
||||
|
||||
if (tupleConflict) {
|
||||
log.info(colors.red.bold(`❌ Workspace ${workspace.workspaceId} on ${workspace.remote} already exists!`));
|
||||
log.info(` Existing name: "${tupleConflict.name}"`);
|
||||
log.info(` New name: "${workspace.name}"`);
|
||||
log.info(colors.yellow(`\nNote: Backend constraint prevents duplicate (remote, workspaceId) combinations.`));
|
||||
|
||||
if (!isInteractive) {
|
||||
// In non-interactive mode (tests, scripts), auto-overwrite with force flag
|
||||
if (opts.force) {
|
||||
log.info(colors.yellow(`Force flag enabled, overwriting existing workspace "${tupleConflict.name}".`));
|
||||
} else {
|
||||
throw new Error(`Backend constraint violation: (${workspace.remote}, ${workspace.workspaceId}) already exists as "${tupleConflict.name}". Use --force to overwrite.`);
|
||||
}
|
||||
} else {
|
||||
const overwrite = await Confirm.prompt({
|
||||
message: `Do you want to overwrite the existing workspace "${tupleConflict.name}"?`,
|
||||
default: false,
|
||||
});
|
||||
|
||||
if (!overwrite) {
|
||||
log.info(colors.yellow("Operation cancelled."));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the conflicting workspace
|
||||
await removeWorkspace(tupleConflict.name, true, opts);
|
||||
}
|
||||
|
||||
// Remove existing workspace with same name (if updating)
|
||||
await removeWorkspace(workspace.name, true, opts);
|
||||
const file = await Deno.open((await getRootStore()) + "remotes.ndjson", {
|
||||
|
||||
// Add the new workspace
|
||||
const file = await Deno.open((await getRootStore(opts.configDir)) + "remotes.ndjson", {
|
||||
append: true,
|
||||
write: true,
|
||||
read: true,
|
||||
@@ -274,7 +352,7 @@ export async function removeWorkspace(
|
||||
silent: boolean,
|
||||
opts: any
|
||||
) {
|
||||
const orgWorkspaces = await allWorkspaces();
|
||||
const orgWorkspaces = await allWorkspaces(opts.configDir);
|
||||
if (orgWorkspaces.findIndex((x) => x.name === name) === -1) {
|
||||
if (!silent) {
|
||||
log.info(
|
||||
@@ -290,7 +368,7 @@ export async function removeWorkspace(
|
||||
}
|
||||
|
||||
await Deno.writeTextFile(
|
||||
(await getRootStore()) + "remotes.ndjson",
|
||||
(await getRootStore(opts.configDir)) + "remotes.ndjson",
|
||||
orgWorkspaces
|
||||
.filter((x) => x.name !== name)
|
||||
.map((x) => JSON.stringify(x))
|
||||
|
||||
Reference in New Issue
Block a user