fix(cli): use wmill.yaml key (not branch name) for workspace-specific filenames

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
hugocasa
2026-04-21 18:10:51 +02:00
parent f8c916cb60
commit 3ff23aae9a
4 changed files with 147 additions and 9 deletions
+10 -5
View File
@@ -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);
}
+8 -4
View File
@@ -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<string | undefined> {
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);
}
+12
View File
@@ -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<string> {
const config = await readConfigFile({ warnIfMissing: false });
const match = findWorkspaceByGitBranch(config.workspaces, branchName);
return match ? match[0] : branchName;
}
export interface SpecificItemsConfig {
variables?: string[];
resources?: string[];
@@ -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<void>,
): Promise<void> {
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");
},
);
});
});