云端 Mac CI 工作区权限漂移排查与治理

云端 Mac CI 工作区权限漂移排查与治理

同一条 Xcode 构建任务连续运行数天后,突然开始在写入 DerivedData、更新依赖缓存或删除旧产物时报告 Permission denied。重新检出仓库可能暂时恢复,但下一次并行任务又会复发。此时不要先怀疑 Xcode,也不要直接扩大目录权限;更常见的根因是某个脚本换了执行用户、继承了不同的 umask,或给工作区留下了额外 ACL。

先保存失败现场

权限问题最怕被“清理后重跑”抹掉。先记录任务的执行身份、主目录、当前目录和默认权限掩码,再检查第一个失败路径,不必立即扫描整个磁盘。

printf 'user=%s
' "$(id -un)"
printf 'groups=%s
' "$(id -Gn)"
printf 'home=%s
' "$HOME"
printf 'cwd=%s
' "$PWD"
printf 'umask=%s
' "$(umask)"

target="${FAILED_PATH:?set FAILED_PATH first}"
stat -f 'owner=%Su group=%Sg mode=%Sp path=%N' "$target"
ls -led "$target"

stat 回答“谁拥有它、基础模式是什么”,ls -le 则会显示 ACL。目录模式看起来允许写入,并不代表访问一定成功:父目录缺少执行权限、ACL 中存在限制项,或者文件属于另一个任务用户,都可能阻断操作。

只对首个失败路径及其父目录逐层检查。对整个工作区执行递归改权既会破坏现场,也可能把密钥、缓存和构建产物暴露给无关进程。

区分三类权限漂移

所有者发生变化

最常见的情况是某个步骤通过提权命令安装依赖或复制文件,随后把产物留给了其他用户。可以先列出不属于当前任务用户的内容:

workspace="${WORKSPACE:?set WORKSPACE first}"
find "$workspace" -x ! -user "$(id -un)" -print

若输出集中在单个任务目录,应回查创建它的脚本,而不是用递归 chown 掩盖来源。CI 工作区最好从创建开始就由任务用户拥有,必须执行高权限操作时,也不要把输出写回仓库、缓存或 DerivedData。

ACL 超出预期

Finder 操作、迁移脚本或复制工具可能保留 ACL。ls -le 中基础权限行下方若出现编号条目,就应确认其来源。只有在目录确定为可重建的临时工作区时,才可定点移除 ACL:

job_dir="${JOB_DIR:?set JOB_DIR first}"
chmod -RN "$job_dir"

不要对用户主目录或凭据目录执行该命令。修复后再次运行 ls -led,确认 ACL 已消失且基础模式仍符合要求。

umask 不一致

SSH 交互会话、CI 守护进程和独立脚本不一定读取相同的 Shell 配置。若一个任务以 077 创建缓存,后续任务即使属于同组也可能无法复用。与其依赖启动文件,不如在任务入口明确声明:

umask 022
install -d -m 0755 "$JOB_DIR"
install -d -m 0755 "$JOB_DIR/DerivedData"
install -d -m 0755 "$JOB_DIR/Artifacts"

敏感材料应使用独立目录和更严格的模式,不能为了共享构建缓存而统一放宽。

每个任务使用独立工作区

并行任务共用一个 DerivedData 或归档目录时,一个任务的清理动作可能撞上另一个任务正在写入的文件。建议用仓库标识、提交标识和任务编号组成目录,并将路径显式传给构建命令。

run_id="${CI_RUN_ID:?set CI_RUN_ID first}"
root="$HOME/ci-runs/$run_id"
src="$root/source"
derived="$root/DerivedData"
artifacts="$root/Artifacts"

umask 022
install -d -m 0755 "$src" "$derived" "$artifacts"

xcodebuild \
  -workspace App.xcworkspace \
  -scheme App \
  -configuration Release \
  -derivedDataPath "$derived" \
  archive \
  -archivePath "$artifacts/App.xcarchive"

共享缓存和任务输出应分开:依赖下载缓存可以只读复用,DerivedData、归档和日志则按任务隔离。这样既减少权限互相污染,也能让失败现场在任务结束前保持完整。

路径类型 建议归属 生命周期 是否允许并行写入
源码检出 单个任务 单次运行
DerivedData 单个任务 单次运行
归档与日志 单个任务 验收后清理
下载缓存 固定任务用户 跨任务 仅由缓存流程更新

在构建前后设置验收门槛

权限检查应成为任务入口的一部分,而不是出错后的人工操作。构建前确认目录可写、所有者正确且没有意外 ACL;构建后检查是否生成了异主文件。下面的检查会在发现漂移时直接终止任务:

test -d "$JOB_DIR"
test -w "$JOB_DIR"

unexpected_owner="$(
  find "$JOB_DIR" -x ! -user "$(id -un)" -print -quit
)"

if [ -n "$unexpected_owner" ]; then
  printf 'unexpected owner: %s
' "$unexpected_owner" >&2
  exit 1
fi

if ls -led "$JOB_DIR" | tail -n +2 | grep -q '^[[:space:]]*[0-9]:'; then
  printf 'unexpected ACL on job directory
' >&2
  exit 1
fi

若团队需要共享缓存,应单独定义缓存写入者、更新时机和原子替换方式,不要让所有构建任务同时修改同一目录。任务完成后的清理也只应删除本次 run_id 对应的路径,并先校验路径位于预期根目录之下。

把修复落到脚本源头

一次可靠的修复,应能回答四个问题:哪个步骤创建了异常文件,创建时使用哪个用户,继承了什么 umask,为什么写入了共享目录。找到答案后,把目录创建、执行身份和输出位置写进脚本,而不是依赖人工登录后的环境。

在 RunnerVM 的云端 Mac 上执行 CI 时,同样应在任务开头输出最小环境快照,并在控制台确认当前可选配置。机器重启或任务迁移后,只要入口脚本能够重新建立目录与权限基线,构建就不需要依赖上一次运行遗留的状态。最终目标不是消除所有权限限制,而是让每个文件的创建者、读写范围和清理责任都可预测。

常见问题

遇到 Permission denied 时可以直接对整个工作区执行 chmod 777 吗?

不建议。它会掩盖所有者、ACL 和任务隔离问题,还会扩大其他进程修改构建文件的范围。应先用 stat 与 ls -le 检查具体路径,再只修复已确认的任务目录。

为什么同一脚本在 SSH 中成功,在 CI 任务中却失败?

两者可能使用不同的执行用户、HOME、umask、PATH 和启动环境。应在任务开头记录 id、HOME、umask 与工作目录,并在脚本内显式设置权限基线。

Runner M4

让下一次构建运行在独享物理 Mac 上

按任务周期选择云端 Mac 节点,在控制台管理订单、连接信息与支持工单。

选择方案并下单