From 3ff23aae9a8aba084a52688405f6a708b9d8b41f Mon Sep 17 00:00:00 2001 From: hugocasa Date: Tue, 21 Apr 2026 18:10:51 +0200 Subject: [PATCH] fix(cli): use wmill.yaml key (not branch name) for workspace-specific filenames Co-Authored-By: Claude Opus 4.7 (1M context) --- cli/src/commands/script/script.ts | 15 ++- cli/src/commands/trigger/trigger.ts | 12 +- cli/src/core/specific_items.ts | 12 ++ ...workspace_key_filename_integration.test.ts | 117 ++++++++++++++++++ 4 files changed, 147 insertions(+), 9 deletions(-) create mode 100644 cli/test/workspace_key_filename_integration.test.ts diff --git a/cli/src/commands/script/script.ts b/cli/src/commands/script/script.ts index 0120988146..cd437c58f7 100644 --- a/cli/src/commands/script/script.ts +++ b/cli/src/commands/script/script.ts @@ -179,20 +179,25 @@ export async function findResourceFile(path: string) { let contentBasePathJSON = splitPath[0] + "." + splitPath[1] + ".json"; let contentBasePathYAML = splitPath[0] + "." + splitPath[1] + ".yaml"; - // Check for branch-specific metadata files first + // Check for workspace-specific metadata files first, using the wmill.yaml + // config key for the current git branch as the filename suffix (falls back + // to the branch name when no matching workspace entry exists). const currentBranch = getCurrentGitBranch(); + const wsName = currentBranch + ? await specificItems.resolveWsNameForGitBranch(currentBranch) + : null; const candidates = [contentBasePathJSON, contentBasePathYAML]; - if (currentBranch) { - // Add branch-specific candidates at the beginning (higher priority) + if (wsName) { + // Add workspace-specific candidates at the beginning (higher priority) const branchSpecificJSON = specificItems.toWorkspaceSpecificPath( contentBasePathJSON, - currentBranch + wsName ); const branchSpecificYAML = specificItems.toWorkspaceSpecificPath( contentBasePathYAML, - currentBranch + wsName ); candidates.unshift(branchSpecificJSON, branchSpecificYAML); } diff --git a/cli/src/commands/trigger/trigger.ts b/cli/src/commands/trigger/trigger.ts index df2657427c..4cffc90427 100644 --- a/cli/src/commands/trigger/trigger.ts +++ b/cli/src/commands/trigger/trigger.ts @@ -33,6 +33,7 @@ import { import { fromWorkspaceSpecificPath, isWorkspaceSpecificFile, + resolveWsNameForGitBranch, } from "../../core/specific_items.ts"; import { getCurrentGitBranch } from "../../utils/git.ts"; import { requireLogin } from "../../core/auth.ts"; @@ -567,14 +568,17 @@ function checkIfValidTrigger(kind: string | undefined): kind is TriggerType { } } -function extractTriggerKindFromPath(filePath: string): string | undefined { +async function extractTriggerKindFromPath(filePath: string): Promise { let pathToAnalyze = filePath; - // If this is a branch-specific file, convert it to the base path first + // If this is a workspace-specific file, convert it to the base path first. + // Resolve the wmill.yaml config key for the current branch (falls back to + // the branch name when no matching workspace entry exists). if (isWorkspaceSpecificFile(filePath)) { const currentBranch = getCurrentGitBranch(); if (currentBranch) { - pathToAnalyze = fromWorkspaceSpecificPath(filePath, currentBranch); + const wsName = await resolveWsNameForGitBranch(currentBranch); + pathToAnalyze = fromWorkspaceSpecificPath(filePath, wsName); } } @@ -598,7 +602,7 @@ async function push(opts: GlobalOptions, filePath: string, remotePath: string) { console.log(colors.bold.yellow("Pushing trigger...")); - const triggerKind = extractTriggerKindFromPath(filePath); + const triggerKind = await extractTriggerKindFromPath(filePath); if (!checkIfValidTrigger(triggerKind)) { throw new Error("Invalid trigger kind: " + triggerKind); } diff --git a/cli/src/core/specific_items.ts b/cli/src/core/specific_items.ts index 3d0d9df248..fbdffad153 100644 --- a/cli/src/core/specific_items.ts +++ b/cli/src/core/specific_items.ts @@ -4,10 +4,22 @@ import { isFileResource, isFilesetResource } from "../utils/utils.ts"; import { SyncOptions, findWorkspaceByGitBranch, + readConfigFile, WorkspaceEntryConfig, } from "./conf.ts"; import { TRIGGER_TYPES } from "../types.ts"; +/** + * Resolve the effective workspace name (wmill.yaml config key) for a given + * git branch. Falls back to the branch name itself when no matching workspace + * entry exists (legacy behavior). + */ +export async function resolveWsNameForGitBranch(branchName: string): Promise { + const config = await readConfigFile({ warnIfMissing: false }); + const match = findWorkspaceByGitBranch(config.workspaces, branchName); + return match ? match[0] : branchName; +} + export interface SpecificItemsConfig { variables?: string[]; resources?: string[]; diff --git a/cli/test/workspace_key_filename_integration.test.ts b/cli/test/workspace_key_filename_integration.test.ts new file mode 100644 index 0000000000..55a5cc513c --- /dev/null +++ b/cli/test/workspace_key_filename_integration.test.ts @@ -0,0 +1,117 @@ +import { describe, expect, test } from "bun:test"; +import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises"; +import { execSync } from "node:child_process"; +import os from "node:os"; +import path from "node:path"; +import { stringify as yamlStringify } from "yaml"; + +import { resolveWsNameForGitBranch } from "../src/core/specific_items.ts"; +import { findResourceFile } from "../src/commands/script/script.ts"; + +// Integration tests covering the bug where workspace-specific filenames used +// the raw git branch name instead of the wmill.yaml workspace config key. +// A per-item helper (findResourceFile) now resolves the effective wsName from +// wmill.yaml before falling back to the branch name. + +async function withGitRepoAndConfig( + config: unknown, + branch: string, + fn: (tempDir: string) => Promise, +): Promise { + const tempDir = await mkdtemp(path.join(os.tmpdir(), "wmill_wskey_")); + const originalCwd = process.cwd(); + try { + // Set up a minimal git repo on the target branch so getCurrentGitBranch() + // returns the expected value. An initial commit is needed so HEAD points + // somewhere and `git rev-parse --abbrev-ref HEAD` succeeds. + execSync(`git init -q -b ${branch}`, { cwd: tempDir }); + execSync(`git config user.email test@example.com`, { cwd: tempDir }); + execSync(`git config user.name test`, { cwd: tempDir }); + + await writeFile( + path.join(tempDir, "wmill.yaml"), + yamlStringify(config), + ); + + execSync(`git add wmill.yaml && git commit -q -m init`, { cwd: tempDir }); + + process.chdir(tempDir); + await fn(tempDir); + } finally { + process.chdir(originalCwd); + await rm(tempDir, { recursive: true, force: true }); + } +} + +describe("resolveWsNameForGitBranch", () => { + test("returns the wmill.yaml config key for a branch matched via gitBranch field", async () => { + await withGitRepoAndConfig( + { + workspaces: { + myKey: { gitBranch: "main", workspaceId: "prod" }, + }, + }, + "main", + async () => { + const wsName = await resolveWsNameForGitBranch("main"); + expect(wsName).toEqual("myKey"); + }, + ); + }); + + test("returns the wmill.yaml config key when the branch equals the key and gitBranch is not set", async () => { + await withGitRepoAndConfig( + { + workspaces: { + staging: { workspaceId: "stg_workspace" }, + }, + }, + "staging", + async () => { + const wsName = await resolveWsNameForGitBranch("staging"); + expect(wsName).toEqual("staging"); + }, + ); + }); + + test("falls back to the branch name when no matching workspace entry exists", async () => { + await withGitRepoAndConfig( + { + workspaces: { + production: { gitBranch: "main" }, + }, + }, + "feature-x", + async () => { + const wsName = await resolveWsNameForGitBranch("feature-x"); + expect(wsName).toEqual("feature-x"); + }, + ); + }); +}); + +describe("findResourceFile picks the wsName-named file, not the branch-named file", () => { + test("finds the workspace-specific resource file using the config key as suffix", async () => { + await withGitRepoAndConfig( + { + workspaces: { + myKey: { gitBranch: "main", workspaceId: "prod" }, + }, + }, + "main", + async (tempDir) => { + // Only the config-key-named file exists on disk. A wmill without this + // fix would look for `f/foo.main.resource.yaml` (the branch name) and + // either miss this file or pick a stale branch-named file. + await mkdir(path.join(tempDir, "f"), { recursive: true }); + await writeFile( + path.join(tempDir, "f/foo.myKey.resource.yaml"), + "value: {}\nresource_type: text\n", + ); + + const found = await findResourceFile("f/foo.resource.file.txt"); + expect(found).toEqual("f/foo.myKey.resource.yaml"); + }, + ); + }); +});