Диагностика дрейфа прав рабочей зоны CI на облачном Mac

Диагностика дрейфа прав рабочей зоны CI на облачном Mac

После нескольких дней непрерывной работы одна и та же задача сборки Xcode внезапно может начать выдавать Permission denied при записи в DerivedData, обновлении кеша зависимостей или удалении старых артефактов. Повторное извлечение репозитория иногда временно решает проблему, но при следующем параллельном запуске она возникает снова. В такой ситуации не спешите винить 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 унаследовал и почему записал данные в общий каталог. Найдя ответы, явно задайте в скрипте создание каталогов, пользователя выполнения и расположение результатов вместо того, чтобы полагаться на окружение после ручного входа в систему.

При запуске CI на облачном Mac RunnerVM также выводите минимальный снимок окружения в начале задачи и проверяйте в консоли доступные в текущий момент конфигурации. Если скрипт точки входа способен заново установить исходные каталоги и права после перезагрузки машины или переноса задачи, сборке не придётся зависеть от состояния, оставшегося после предыдущего запуска. Конечная цель состоит не в устранении всех ограничений доступа, а в том, чтобы для каждого файла были предсказуемы создатель, область чтения и записи, а также ответственность за очистку.

Часто задаваемые вопросы

Можно ли после ошибки Permission denied выполнить chmod 777 для всей рабочей зоны?

Нет. Это скрывает неверного владельца и ACL, а также разрешает посторонним процессам изменять файлы сборки. Сначала проверьте проблемный путь командами stat и ls -le.

Почему сценарий работает через SSH, но завершается ошибкой в задании CI?

У задания могут отличаться пользователь, HOME, umask, PATH и среда запуска. Запишите эти значения в начале задания и явно установите требуемую маску прав в сценарии.

Runner M4

Запускайте следующую сборку на выделенном физическом Mac

Выберите облачный Mac-узел под длительность задачи, а затем управляйте заказами, данными для подключения и обращениями в службу поддержки через консоль.

Выбрать тариф и оформить заказ