fix(detection): add xcodegen and Xcode bundle support to project detection (#1882)
* fix: detect Xcode bundles by suffix scan in worktree health check (#1882) Xcode project directories have project-specific names (e.g. Sudokuxyz.xcodeproj) that cannot be matched by the exact-filename PROJECT_FILES list. Add a readdirSync suffix scan for *.xcodeproj and *.xcworkspace so iOS/macOS projects are not incorrectly treated as greenfield when the health check runs. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: replace empty catch with debugLog in Xcode bundle scan The silent-catch-diagnostics test (#3348) bans empty catch blocks in migrated auto-mode files. Replace the bare `catch { /* best-effort */ }` with a debugLog call to satisfy the workflow-logger requirement. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
4cb6252b9b
commit
261e2a6d5f
2 changed files with 48 additions and 6 deletions
|
|
@ -27,7 +27,7 @@ import { debugLog } from "../debug-logger.js";
|
|||
import { PROJECT_FILES } from "../detection.js";
|
||||
import { MergeConflictError } from "../git-service.js";
|
||||
import { join, basename, dirname, parse as parsePath } from "node:path";
|
||||
import { existsSync, cpSync } from "node:fs";
|
||||
import { existsSync, cpSync, readdirSync } from "node:fs";
|
||||
import { logWarning, logError } from "../workflow-logger.js";
|
||||
import { gsdRoot } from "../paths.js";
|
||||
import { atomicWriteSync } from "../atomic-write.js";
|
||||
|
|
@ -1009,11 +1009,20 @@ export async function runUnitPhase(
|
|||
}
|
||||
const hasProjectFile = PROJECT_FILES.some((f) => deps.existsSync(join(s.basePath, f)));
|
||||
const hasSrcDir = deps.existsSync(join(s.basePath, "src"));
|
||||
// Xcode bundles have project-specific names (*.xcodeproj, *.xcworkspace)
|
||||
// that cannot be matched by exact filename — scan the directory by suffix.
|
||||
let hasXcodeBundle = false;
|
||||
try {
|
||||
const entries = deps.existsSync(s.basePath) ? readdirSync(s.basePath) : [];
|
||||
hasXcodeBundle = entries.some((e: string) => e.endsWith(".xcodeproj") || e.endsWith(".xcworkspace"));
|
||||
} catch (err) {
|
||||
debugLog("runUnitPhase", { phase: "xcode-bundle-scan-failed", basePath: s.basePath, error: String(err) });
|
||||
}
|
||||
// Monorepo support (#2347): if no project files in the worktree directory,
|
||||
// walk parent directories up to the filesystem root. In monorepos,
|
||||
// package.json / Cargo.toml etc. live in a parent directory.
|
||||
let hasProjectFileInParent = false;
|
||||
if (!hasProjectFile && !hasSrcDir) {
|
||||
if (!hasProjectFile && !hasSrcDir && !hasXcodeBundle) {
|
||||
let checkDir = dirname(s.basePath);
|
||||
const { root } = parsePath(checkDir);
|
||||
while (checkDir !== root) {
|
||||
|
|
@ -1027,11 +1036,11 @@ export async function runUnitPhase(
|
|||
checkDir = dirname(checkDir);
|
||||
}
|
||||
}
|
||||
if (!hasProjectFile && !hasSrcDir && !hasProjectFileInParent) {
|
||||
if (!hasProjectFile && !hasSrcDir && !hasXcodeBundle && !hasProjectFileInParent) {
|
||||
// Greenfield projects won't have project files yet — the first task creates them.
|
||||
// Log a warning but allow execution to proceed. The .git check above is sufficient
|
||||
// to ensure we're in a valid working directory.
|
||||
debugLog("runUnitPhase", { phase: "worktree-health-warn-greenfield", basePath: s.basePath, hasProjectFile, hasSrcDir });
|
||||
debugLog("runUnitPhase", { phase: "worktree-health-warn-greenfield", basePath: s.basePath, hasProjectFile, hasSrcDir, hasXcodeBundle });
|
||||
ctx.ui.notify(`Warning: ${s.basePath} has no recognized project files — proceeding as greenfield project`, "warning");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
import { describe, test, beforeEach, afterEach } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
|
||||
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, readdirSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { execSync } from "node:child_process";
|
||||
|
|
@ -57,13 +57,20 @@ function hasRecognizedProjectFiles(basePath: string, existsSyncFn: (p: string) =
|
|||
return false;
|
||||
}
|
||||
|
||||
/** Simulate the phases.ts Xcode-bundle detection (readdirSync suffix scan). */
|
||||
function hasXcodeBundle(basePath: string): boolean {
|
||||
try {
|
||||
return readdirSync(basePath).some((e) => e.endsWith(".xcodeproj") || e.endsWith(".xcworkspace"));
|
||||
} catch { return false; }
|
||||
}
|
||||
|
||||
import { existsSync } from "node:fs";
|
||||
|
||||
// ─── Tests ───────────────────────────────────────────────────────────────────
|
||||
|
||||
test("PROJECT_FILES is exported and contains expected multi-ecosystem entries", () => {
|
||||
assert.ok(Array.isArray(PROJECT_FILES), "PROJECT_FILES is an array");
|
||||
assert.ok(PROJECT_FILES.length >= 17, `expected >= 17 entries, got ${PROJECT_FILES.length}`);
|
||||
assert.ok(PROJECT_FILES.length >= 18, `expected >= 18 entries, got ${PROJECT_FILES.length}`);
|
||||
// Spot-check key ecosystems
|
||||
assert.ok(PROJECT_FILES.includes("Cargo.toml"), "includes Rust marker");
|
||||
assert.ok(PROJECT_FILES.includes("go.mod"), "includes Go marker");
|
||||
|
|
@ -140,3 +147,29 @@ describe("health check without git repo", () => {
|
|||
assert.ok(!wouldPassHealthCheck(dir, existsSync), "no-git directory should fail health check");
|
||||
});
|
||||
});
|
||||
|
||||
describe("health check with xcodegen and Xcode bundles", () => {
|
||||
let dir: string;
|
||||
beforeEach(() => { dir = createGitRepo(); });
|
||||
afterEach(() => { rmSync(dir, { recursive: true, force: true }); });
|
||||
|
||||
test("health check passes for xcodegen project (project.yml, no Package.swift)", () => {
|
||||
writeFileSync(join(dir, "project.yml"), "name: MyApp\ntargets:\n MyApp:\n type: application\n");
|
||||
assert.ok(wouldPassHealthCheck(dir, existsSync), "xcodegen project should pass health check");
|
||||
});
|
||||
|
||||
// Regression for the real-world failure in #1882: an iOS project with a
|
||||
// project-specific Xcode bundle (Sudokuxyz.xcodeproj/) was blocked because
|
||||
// PROJECT_FILES only probes exact filenames, not suffix-based directory names.
|
||||
test("Xcode bundle (*.xcodeproj) is not in PROJECT_FILES but detected by suffix scan", () => {
|
||||
mkdirSync(join(dir, "Sudokuxyz.xcodeproj"), { recursive: true });
|
||||
mkdirSync(join(dir, "Sources", "Sudokuxyz"), { recursive: true });
|
||||
writeFileSync(join(dir, "Sources", "Sudokuxyz", "ContentView.swift"), "import SwiftUI\n");
|
||||
// PROJECT_FILES uses exact names — cannot match project-specific bundle names
|
||||
assert.ok(!hasRecognizedProjectFiles(dir, existsSync), "xcodeproj bundle must NOT be in PROJECT_FILES");
|
||||
// The readdirSync suffix scan used in phases.ts detects it
|
||||
assert.ok(hasXcodeBundle(dir), "readdirSync suffix scan detects .xcodeproj bundle");
|
||||
// Health check passes regardless (only requires .git)
|
||||
assert.ok(wouldPassHealthCheck(dir, existsSync), "Xcode bundle project should pass health check");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue