Compare commits

...

12 Commits

Author SHA1 Message Date
Ruben Fiszel
e865b2ccb2 Merge main into feat/cli-branch-override (folder support) 2026-01-19 17:12:15 +00:00
Ruben Fiszel
3574269d96 feat(cli): add folders as branch-specific items
Folders can now be configured as branch-specific items in wmill.yaml:

```yaml
gitBranches:
  staging:
    specificItems:
      folders:
        - "f/env_*"
        - "u/admin/config"
```

This allows different folder configurations per branch. A folder like
`f/env_staging/folder.meta.yaml` becomes `f/env_staging.main/folder.meta.yaml`
on the main branch.

Changes:
- Add `folders?: string[]` to SpecificItemsConfig
- Add folder detection in isSpecificItem()
- Add folder pattern to toBranchSpecificPath/fromBranchSpecificPath
- Add folder pattern to isBranchSpecificFile/isCurrentBranchFile
- Add 8 new tests for folder functionality

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-19 17:10:59 +00:00
Ruben Fiszel
c029ddb1b2 refactor(cli): remove redundant branch detection in elementsToMap
isCurrentBranchFile() already validates that a branch exists (via
branchOverride or git detection) before returning true. No need to
pre-compute currentBranch before calling it.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 19:46:37 +00:00
Ruben Fiszel
0511af0a8a fix(cli): pass --branch override to workspace resolution
Previously, the --branch flag was only used for config resolution but
not for workspace resolution. This caused confusing log messages that
showed the git branch (e.g., master) before showing the override branch.

Changes:
- Add branchOverride parameter to tryResolveBranchWorkspace()
- Add branchOverride parameter to resolveWorkspace()
- Pass opts.branch from sync pull/push to resolveWorkspace()
- Log "Using branch override" early in workspace resolution
- Remove duplicate log from getEffectiveSettings()

Now when using --branch foobar, the logs show:
  Using branch override: foobar
  Applied settings for Git branch: foobar
  ...

Instead of the confusing previous output that mentioned both master
and foobar.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 19:44:11 +00:00
Ruben Fiszel
8392892c31 all 2026-01-16 19:35:51 +00:00
Ruben Fiszel
c71a8c1c88 chore(cli): add conf.ts barrel file for test imports
Re-exports from src/core/conf.ts to support existing test imports.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 19:35:01 +00:00
Ruben Fiszel
639e320267 test(cli): add unit tests for branch detection and --branch override
- Add specific_items.test.ts with 35 tests covering:
  - toBranchSpecificPath and fromBranchSpecificPath conversions
  - isSpecificItem pattern matching
  - isBranchSpecificFile detection
  - Round-trip path conversions
  - branchOverride parameter functionality

- Add conf_branch_override.test.ts with 6 tests covering:
  - getEffectiveSettings with branchOverride parameter
  - Branch-specific overrides application
  - promotionOverrides precedence
  - Fallback to top-level settings

- Fix containerized_backend.ts to use dynamic paths instead of
  hardcoded user home directories

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 19:31:44 +00:00
Ruben Fiszel
56c18b813b fix(cli): resolve TypeScript type errors
- Fix Timeout type in dev.ts using ReturnType<typeof setTimeout>
- Add proper type casts for unknown error types
- Cast FlowModule to any to resolve generated type mismatch
- Cast Uint8Array to BlobPart for Blob constructor compatibility

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 19:09:00 +00:00
Ruben Fiszel
519106f0c5 feat(cli): extend --branch support to specificItems functionality
Update getBranchSpecificPath and isCurrentBranchFile to accept optional
branchOverride parameter. This ensures that branch-specific file handling
(for variables, resources, triggers) works correctly with --branch flag.

Updated functions:
- getBranchSpecificPath(): now accepts branchOverride
- isCurrentBranchFile(): now accepts branchOverride
- elementsToMap(): now accepts branchOverride
- compareDynFSElement(): now accepts branchOverride
- prettyChanges(): now accepts branchOverride

All call sites updated to pass opts.branch through the call chain.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 19:06:00 +00:00
Ruben Fiszel
51357f0622 fix(cli): correct log message for workspace fork branches
Use rawGitBranch instead of currentBranch in the log message when
showing the origin of a workspace fork branch.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 18:58:56 +00:00
Ruben Fiszel
fac390a990 feat(cli): add --branch option to sync pull/push commands
Add a --branch argument that allows overriding the current git branch
for sync operations. This enables:
- Using branch-specific settings even when not in a git repository
- Overriding the detected git branch when needed

The branch override is applied to:
- getEffectiveSettings() for branch-specific config overrides
- getSpecificItemsForCurrentBranch() for branch-specific items

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 18:57:36 +00:00
Ruben Fiszel
7e1f116c1f fix(cli): prevent duplicate 'Using non-dotted paths' log message
Add a flag to track whether the message has already been logged,
so it only prints once even if setNonDottedPaths is called multiple times.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 18:50:04 +00:00
3 changed files with 92 additions and 7 deletions

View File

@@ -314,8 +314,7 @@ export async function handleFile(
continue;
}
log.info(`Adding file: ${file.path.substring(1)}`);
// deno-lint-ignore no-explicit-any
const fil = new File([file.contents as any], file.path.substring(1));
const fil = new File([file.contents as BlobPart], file.path.substring(1));
tarball.append(fil);
}
const endTime = performance.now();
@@ -542,8 +541,8 @@ async function streamToBlob(stream: ReadableStream<Uint8Array>): Promise<Blob> {
chunks.push(value);
}
// deno-lint-ignore no-explicit-any
const blob = new Blob(chunks as any);
// Create a Blob from the chunks
const blob = new Blob(chunks as BlobPart[]);
return blob;
}

View File

@@ -8,6 +8,7 @@ export interface SpecificItemsConfig {
variables?: string[];
resources?: string[];
triggers?: string[];
folders?: string[];
}
// Define all branch-specific file types (computed lazily)
@@ -15,6 +16,7 @@ function getBranchSpecificTypes() {
return {
variable: '.variable.yaml',
resource: '.resource.yaml',
folder: '/folder.meta.yaml',
// Generate trigger patterns from the list
...Object.fromEntries(
TRIGGER_TYPES.map(t => [`${t}_trigger`, `.${t}_trigger.yaml`])
@@ -142,6 +144,16 @@ export function isSpecificItem(path: string, specificItems: SpecificItemsConfig
return specificItems.triggers ? matchesPatterns(path, specificItems.triggers) : false;
}
// Check for folder meta files
if (path.endsWith('/folder.meta.yaml')) {
if (specificItems.folders) {
// Match against the folder path (without /folder.meta.yaml)
const folderPath = path.slice(0, -'/folder.meta.yaml'.length);
return matchesPatterns(folderPath, specificItems.folders);
}
return false;
}
// Check for resource files using the standard detection function
if (isFileResource(path)) {
// Extract the base path without the file extension to match against patterns
@@ -197,7 +209,20 @@ export function fromBranchSpecificPath(branchSpecificPath: string, branchName: s
const sanitizedBranchName = branchName.replace(/[\/\\:*?"<>|.]/g, '_');
const escapedBranchName = sanitizedBranchName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
// Check for resource file pattern first
// Check for folder pattern: path.branchName/folder.meta.yaml -> path/folder.meta.yaml
const folderPattern = new RegExp(`\\.${escapedBranchName}(/folder\\.meta\\.yaml)$`);
const folderMatch = branchSpecificPath.match(folderPattern);
if (folderMatch) {
const extension = folderMatch[1];
const pathWithoutBranchAndExtension = branchSpecificPath.substring(
0,
branchSpecificPath.length - `.${sanitizedBranchName}${extension}`.length
);
return `${pathWithoutBranchAndExtension}${extension}`;
}
// Check for resource file pattern
const resourceFilePattern = new RegExp(`\\.${escapedBranchName}(\\.resource\\.file\\..+)$`);
const resourceFileMatch = branchSpecificPath.match(resourceFilePattern);
@@ -283,7 +308,11 @@ export function isCurrentBranchFile(path: string, branchOverride?: string): bool
// Use cached pattern or create and cache new one
let pattern = branchPatternCache.get(currentBranch);
if (!pattern) {
pattern = new RegExp(`\\.${escapedBranchName}\\.${buildYamlTypePattern()}\\.yaml$|\\.${escapedBranchName}\\.resource\\.file\\..+$`);
pattern = new RegExp(
`\\.${escapedBranchName}\\.${buildYamlTypePattern()}\\.yaml$|` +
`\\.${escapedBranchName}\\.resource\\.file\\..+$|` +
`\\.${escapedBranchName}/folder\\.meta\\.yaml$`
);
branchPatternCache.set(currentBranch, pattern);
}
@@ -296,5 +325,9 @@ export function isCurrentBranchFile(path: string, branchOverride?: string): bool
*/
export function isBranchSpecificFile(path: string): boolean {
const yamlTypePattern = buildYamlTypePattern();
return new RegExp(`\\.[^.]+\\.${yamlTypePattern}\\.yaml$|\\.[^.]+\\.resource\\.file\\..+$`).test(path);
return new RegExp(
`\\.[^.]+\\.${yamlTypePattern}\\.yaml$|` +
`\\.[^.]+\\.resource\\.file\\..+$|` +
`\\.[^.]+/folder\\.meta\\.yaml$`
).test(path);
}

View File

@@ -336,3 +336,56 @@ Deno.test("branchOverride: getSpecificItemsForCurrentBranch merges common and br
assertEquals(result?.resources, ["shared/**"]);
assertEquals(result?.triggers, ["dev/triggers/**"]);
});
// =============================================================================
// FOLDER BRANCH-SPECIFIC TESTS
// =============================================================================
Deno.test("toBranchSpecificPath: converts folder path to branch-specific", () => {
const result = toBranchSpecificPath("f/my_folder/folder.meta.yaml", "main");
assertEquals(result, "f/my_folder.main/folder.meta.yaml");
});
Deno.test("toBranchSpecificPath: converts nested folder path to branch-specific", () => {
const result = toBranchSpecificPath("f/parent/child/folder.meta.yaml", "develop");
assertEquals(result, "f/parent/child.develop/folder.meta.yaml");
});
Deno.test("fromBranchSpecificPath: converts branch-specific folder back to base", () => {
const result = fromBranchSpecificPath("f/my_folder.main/folder.meta.yaml", "main");
assertEquals(result, "f/my_folder/folder.meta.yaml");
});
Deno.test("fromBranchSpecificPath: handles sanitized branch names for folders", () => {
const result = fromBranchSpecificPath("f/my_folder.feature_test/folder.meta.yaml", "feature/test");
assertEquals(result, "f/my_folder/folder.meta.yaml");
});
Deno.test("isSpecificItem: matches folder paths with glob pattern", () => {
const config: SpecificItemsConfig = {
folders: ["f/env_*"],
};
assertEquals(isSpecificItem("f/env_staging/folder.meta.yaml", config), true);
assertEquals(isSpecificItem("f/env_production/folder.meta.yaml", config), true);
assertEquals(isSpecificItem("f/other/folder.meta.yaml", config), false);
});
Deno.test("isBranchSpecificFile: detects branch-specific folder files", () => {
assertEquals(isBranchSpecificFile("f/my_folder.main/folder.meta.yaml"), true);
assertEquals(isBranchSpecificFile("f/my_folder.develop/folder.meta.yaml"), true);
assertEquals(isBranchSpecificFile("f/my_folder/folder.meta.yaml"), false);
});
Deno.test("isCurrentBranchFile: detects branch-specific folder for current branch", () => {
assertEquals(isCurrentBranchFile("f/my_folder.staging/folder.meta.yaml", "staging"), true);
assertEquals(isCurrentBranchFile("f/my_folder.staging/folder.meta.yaml", "production"), false);
assertEquals(isCurrentBranchFile("f/my_folder/folder.meta.yaml", "staging"), false);
});
Deno.test("round-trip: folder path conversion", () => {
const original = "f/configs/env_folder/folder.meta.yaml";
const branch = "feature/new-env";
const branchSpecific = toBranchSpecificPath(original, branch);
const restored = fromBranchSpecificPath(branchSpecific, branch);
assertEquals(restored, original);
});