Hacktoberfest 2026:维护者为十月标记出来的 issue,仍然开放、适合新手。 浏览 Hacktoberfest issue

Declared `packageDirectory` reached through a symlink silently contributes zero components to a deploy

未关闭
#3,653 3 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看

还没有人认领这个 Issue。

评估

难度
3/5
预计耗时
1-2 天
新手友好度
68/100
Issue 类型
缺陷
描述清晰度
基本清楚
活跃度
活跃
技术栈
node.js, typescript
领域
cli, tooling

调研方向

Start in src/shared/local/localShadowRepo.ts, especially the packageDirectories mapping and git.statusMatrix call, then run the supplied repro.mjs with isomorphic-git 1.38.9 and 1.41.7 to confirm the walker behavior. Trace the fs wrapper and relevant source-tracking tests or entry points before choosing an approach; done means files under a symlinked packageDirectory are included in status and deploy processing rather than silently omitted.

由索引模型根据 Issue 内容生成。

描述

bug investigating validated

A packageDirectory declared in sfdx-project.json and reached through a symlink contributes no components to a deploy. No error, no warning, and the deploy reports Succeeded. The metadata never arrives and the failure surfaces later as something unrelated.

@salesforce/source-tracking passes declared packageDirectories to isomorphic-git's working-directory walker and relies on that walker descending symlinks. isomorphic-git stopped descending them in 1.38.10. The dependency is declared ^1.34.2, so this arrives on a fresh install with no CLI version change.

Reproduction

This script will generate a simple dx project setup and then call git.statusMatrix to show how different versions of isomorphic-git handle symlinks. Save as repro.mjs in an empty directory:

import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import git from 'isomorphic-git';

// A throwaway project: one package dir reached through a symlink to an ordinary
// sibling directory, and one ordinary package dir as a control.
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'sf-symlink-repro-'));

const linkedTarget = path.join(root, 'shared-lib', 'force-app', 'main', 'default', 'classes');
fs.mkdirSync(linkedTarget, { recursive: true });
fs.writeFileSync(path.join(linkedTarget, 'Linked.cls'), 'public class Linked {}');
fs.mkdirSync(path.join(root, 'links'));
fs.symlinkSync(path.join('..', 'shared-lib'), path.join(root, 'links', 'shared-lib'), 'dir');

const controlDir = path.join(root, 'force-app', 'main', 'default', 'classes');
fs.mkdirSync(controlDir, { recursive: true });
fs.writeFileSync(path.join(controlDir, 'Control.cls'), 'public class Control {}');

// Mirror what ShadowRepo does: init a shadow repo rooted at the project, then ask
// for the status of each declared packageDirectory.
const gitdir = path.join(root, '.shadow');
await git.init({ fs, dir: root, gitdir, defaultBranch: 'main' });

const status = async (filepath) =>
  (await git.statusMatrix({ fs, dir: root, gitdir, filepaths: [filepath] })).map((row) => row[0]);

console.log(`isomorphic-git ${git.version()}`);
console.log(`  symlinked packageDirectory 'links/shared-lib/force-app':`);
console.log(`    ${(await status('links/shared-lib/force-app')).join('\n    ') || '(nothing)'}`);
console.log(`  control packageDirectory 'force-app':`);
console.log(`    ${(await status('force-app')).join('\n    ') || '(nothing)'}`);

fs.rmSync(root, { recursive: true, force: true });
npm i [email protected] && node repro.mjs
npm i [email protected] && node repro.mjs

Output

isomorphic-git 1.38.9
  symlinked packageDirectory 'links/shared-lib/force-app':
    links/shared-lib
    links/shared-lib/force-app/main/default/classes/Linked.cls
  control packageDirectory 'force-app':
    force-app/main/default/classes/Control.cls

isomorphic-git 1.41.7
  symlinked packageDirectory 'links/shared-lib/force-app':
    links/shared-lib
  control packageDirectory 'force-app':
    force-app/main/default/classes/Control.cls

Under 1.41.7 the only entry for the symlinked package directory is the symlink itself, typed as a blob. Linked.cls is gone.

1.38.9 is the last good version. 1.38.10, 1.39.x, 1.40.0 and 1.41.7 all reproduce.

I can put together a full sfdx-project reproduction if the script above isn't enough.

Cause

ShadowRepo in @salesforce/source-tracking passes packageDirs to isomorphic-git, relying on its working directory walker to descend into subfolders and find all the files to report on. The folders passed may contain symlinks that are not resolved to real paths. And so is relying on how isomorphic-git handles symlinks. This behavior changed in 1.38.10 and so how Salesforce-cli handles symlinks is now at the mercy of whatever version of isomorphic-git you get installed. (The declared range includes both good and bad versions.)

ShadowRepo (src/shared/local/localShadowRepo.ts) rebases each packageDirectory to a project-relative path and relies on the working-directory walker to enumerate files:

this.packageDirs = options.packageDirs.map(packageDirToRelativePosixPath(options.projectPath));
// ...
this.status = await git.statusMatrix({
  fs,
  dir: this.projectPath,
  gitdir: this.gitDir,
  filepaths: this.packageDirs,
  filter: fileFilter(this.packageDirs),
});

In isomorphic-git's GitWalkerFs, entry types come from lstat, which describes the link rather than its target, so a symlinked directory types as blob (a regular directory has type tree).

1.38.10 added a guard making readdir respect that type:

async readdir(entry) {
  if ((await entry.type()) !== 'tree') return null   // added in 1.38.10
  const filepath = entry._fullpath;
  const names = await fs.readdir(join(dir, filepath));

fs.readdir does follow symlinks, so before 1.38.10 the walker descended symlinked directories regardless of type. But git does not follow symlinks. 1.38.10 closed isomorphic-git#1215, which is a legit discrepancy between the behavior of git and isomorphic-git.

The shadow repo is using git as a change detector over directories the user explicitly declared. A symlinked package directory is a pointer to files the user asked to have tracked. That distinction has to be made in source-tracking.

This is not an isomorphic-git bug.

Where it showed up

From a failed deploy log:

CustomField  Budget__c.Eligible_for_Billing__c
Field $Label.common_label_yes does not exist. Check spelling. (648:13)

The label exists and is spelled correctly. It lives in a labels file in a symlinked package directory, so the label is never deployed and a field referencing it in a later package directory failed. Nothing in the output pointed at the package directory with the symlink and the label, which is where the problem actually occurred.

sfdx-project.json declares:

{ "path": "sf-lib-links/psenterprise-lib/force-app", "default": false }

where sf-lib-links/psenterprise-lib is a symlink to a sibling package in the same repo. This is an ordinary layout for a monorepo with shared library code.

Possible fixes

As we pass fs into isomorphic-git we have the opportunity to wrap it. We could wrap it so that it sees symlinks as type tree (isDirectory() === true) and the walker will descend into them. This uses isomorphic-git's own extension point to get the behavior we want. I have validated this approach is viable.

(We can't resolve declared packageDirectories to their real paths before walking them. Looking at the parameters we pass to statusMatrix, filepaths is relative to dir- a resolved symlink frequently points outside dir, so there's no relative path you could pass.)

Other ideas:

  • We could pin the isomorphic-git dependency. The range ^1.34.2 allowed a transitive behavioral change to alter source-tracking behavior with no release involved. However it doesn't give a long-term fix for sharing code via symlinks in dx projects.
  • Warn when a declared packageDirectory contributes zero components. This would make the investigation easier, as the error above is pointing in completely the wrong place.

System information

{
  "architecture": "linux-x64",
  "cliVersion": "@salesforce/cli/2.150.6",
  "nodeVersion": "node-v24.19.0",
  "osVersion": "Linux 6.1.182",
  "rootPath": "/home/jenkins/workspace/9-investigate-sf-2.150.6-upgrade/common/temp/node_modules/.pnpm/@[email protected]_@[email protected]_@[email protected]/node_modules/@salesforce/cli",
  "shell": "sh",
  "pluginVersions": [
    "@oclif/plugin-autocomplete 3.3.0 (core)",
    "@oclif/plugin-commands 4.2.0 (core)",
    "@oclif/plugin-help 6.3.0 (core)",
    "@oclif/plugin-not-found 3.3.0 (core)",
    "@oclif/plugin-plugins 5.5.1 (core)",
    "@oclif/plugin-search 1.3.0 (core)",
    "@oclif/plugin-update 4.8.0 (core)",
    "@oclif/plugin-version 2.3.0 (core)",
    "@oclif/plugin-warn-if-update-available 3.2.0 (core)",
    "@oclif/plugin-which 3.3.0 (core)",
    "@salesforce/cli 2.150.6 (core)",
    "agent 2.0.5 (core)",
    "apex 4.1.1 (core)",
    "api 2.0.9 (core)",
    "auth 5.0.6 (core)",
    "data 5.1.7 (core)",
    "deploy-retrieve 4.1.2 (core)",
    "info 4.0.9 (core)",
    "limits 4.0.4 (core)",
    "marketplace 2.0.5 (core)",
    "org 6.0.11 (core)",
    "packaging 3.0.6 (core)",
    "schema 4.0.6 (core)",
    "settings 3.0.6 (core)",
    "sobject 2.0.5 (core)",
    "telemetry 4.0.6 (core)",
    "templates 57.0.11 (core)",
    "trust 4.0.10 (core)",
    "user 5.0.2 (core)"
  ]
}

Dependency chain: @salesforce/[email protected] → @salesforce/[email protected] →
isomorphic-git: "^1.34.2" → resolves to 1.41.7 on a fresh install.

主要语言
没有语言数据
星标
571
派生
80
平均合并
23 小时 43 分钟
30 天内合并 PR
5

环境准备

  • 没有 Dockerfile 或 Docker Compose 文件
  • 没有 Pull Request 模板
  • 阅读贡献指南

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

forcedotcom/cli 的其他 Issue

查看 forcedotcom/cli 的全部 Issue

相似的 Issue

更多 CLI Issue

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。