Один и тот же сценарий сборки без ошибок работает в терминале SSH, но после переноса в облачный Mac CI завершается с ошибкой Permission denied. Самая распространённая ошибка при диагностике — предположить, что достаточно ещё раз выполнить chmod +x. На практике причина обычно затрагивает сразу четыре уровня: umask в точке запуска задания, исполняемый бит в индексе Git, правила обработки прав при копировании и распаковке, а также интерпретатор, который фактически запускает сценарий. Если исправить только один файл в рабочем каталоге, проблема вернётся при следующем чистом получении репозитория.
Сначала разделите ошибки прав доступа на четыре категории
Перед диагностикой зафиксируйте объект, на котором произошёл сбой, и тип операции. Невозможность запустить сценарий, записать данные в каталог, слишком широкие права на ключ и изменение режима после распаковки архива требуют совершенно разных подходов.
С помощью stat одновременно проверьте восьмеричный режим, владельца и тип файла:
target="${1:?path required}"
stat -f 'mode=%Sp octal=%OLp owner=%Su group=%Sg type=%HT path=%N' "$target"
ls -ldeO@ "$target"
Вторая команда также показывает ACL, файловые флаги и расширенные атрибуты. Если обычные права выглядят корректно, но операция по-прежнему завершается ошибкой, не повышайте привилегии сразу. Сначала убедитесь, что все родительские каталоги доступны для прохода, том разрешает выполнение файлов, а процесс работает под ожидаемой учётной записью.
rwx— не единственный критерий. Соответствующие права нужны на каждом уровне пути, а ACL может переопределять обычные биты режима. Постоянное использованиеsudoлишь создаёт в рабочем каталоге смесь владельцев, из-за чего последующие задания становится сложнее воспроизводить.
Соберите минимальный набор диагностических данных
Сохраните как минимум вывод id, значение umask, текущий каталог, результат stat для целевого файла и сведения о монтировании. Не записывайте в журнал полный набор переменных окружения: среди них могут быть токены. Диагностические данные должны охватывать идентификатор пользователя, путь и права, но не содержимое учётных данных.
Зафиксируйте umask в точке запуска задания
umask влияет только на вновь создаваемые объекты и не изменяет уже существующие файлы. При распространённом базовом значении 022 обычные файлы по умолчанию получают режим 644, а каталоги — 755. Для контролируемых каталогов с совместным доступом одной группы можно рассмотреть 002, но использовать его как универсальное исправление нельзя. Значение следует явно задавать в точке запуска задания и проверять с помощью нового тестового объекта:
set -euo pipefail
umask 022
probe_dir="$(mktemp -d)"
probe_file="$probe_dir/probe"
: > "$probe_file"
file_mode="$(stat -f '%OLp' "$probe_file")"
dir_mode="$(stat -f '%OLp' "$probe_dir")"
test "$file_mode" = "644"
test "$dir_mode" = "700"
rm -rf "$probe_dir"
Чтобы защитить временное содержимое, mktemp -d обычно создаёт каталог с режимом 700, поэтому с его помощью нельзя проверять, что обычные каталоги должны получать 755. Для проверки базового режима каталога выполните mkdir в заранее известном родительском каталоге, а затем отдельно проверьте результат. Из-за этого различия тесты прав доступа нередко выдают ложные ошибки.
Сохраняйте правильное намерение выполнения в Git
Git в основном отслеживает, является ли файл исполняемым, но не сохраняет полный набор прав Unix. Если на одной машине режим сценария вручную изменили на 755, это ещё не означает, что изменение записано в индексе. Проверяйте индекс, а не только рабочий каталог:
git ls-files --stage |
awk '$1 == "100755" {print $4}' |
sort
Чтобы сохранить исполняемый бит в коммите, выполните:
git update-index --chmod=+x scripts/build.sh
git diff --summary
git diff --cached --summary
И наоборот, если исполняемый бит случайно установлен у конфигурационного файла или документа, исправьте это с помощью git update-index --chmod=-x. У файлов .sh также проверяйте первую строку. Используйте интерпретатор, который гарантированно доступен в среде, например #!/bin/zsh или #!/usr/bin/env bash, и не допускайте окончания строк CRLF в сценариях.
Проверяйте сценарии перед слиянием
Можно вести явный список исполняемых сценариев и сравнивать его с результатами проверки индекса. Не вводите упрощённое правило, по которому все файлы .sh обязаны быть исполняемыми: библиотечным файлам, подключаемым через source, исполняемый бит может быть не нужен. Правило должно описывать назначение файла, а не делать вывод по его расширению.
Контролируйте правила обработки прав при копировании и архивации
cp, ditto, rsync и разные форматы архивов по-разному обрабатывают биты режима, ACL и расширенные атрибуты. В кэше сборки следует хранить только данные, которые можно создать заново, а права в поставляемом пакете нужно повторно проверять после распаковки.
Перед копированием рабочего каталога решите, нужно ли сохранять метаданные. Если требуется только содержимое исходных файлов, не допускайте случайного наследования прежнего владельца, ACL или расширенных атрибутов. Если исполняемый бит необходимо сохранить, выполните проверку копирования во временном каталоге:
src="scripts/build.sh"
tmp="$(mktemp -d)"
ditto "$src" "$tmp/build.sh"
before="$(stat -f '%OLp' "$src")"
after="$(stat -f '%OLp' "$tmp/build.sh")"
test "$before" = "$after"
rm -rf "$tmp"
При проверке архива недостаточно убедиться, что файлы существуют. Как минимум проверьте, что сценарий запуска является исполняемым, обычные конфигурационные файлы не являются исполняемыми, а конфиденциальные файлы недоступны для чтения группе и другим пользователям. Распаковывайте архив в новый каталог, чтобы режимы старых файлов не скрывали проблемы самого архива.
Превратите исправление в исполняемую политику
Надёжное решение — не массовый запуск chmod -R 777 после сбоя, а небольшой набор понятных проверок. Следующий код отклоняет файлы, доступные другим пользователям для записи, и проверяет, что сценарий запуска является исполняемым:
set -euo pipefail
entry="scripts/build.sh"
test -f "$entry"
test -x "$entry"
while IFS= read -r -d '' file; do
mode="$(stat -f '%OLp' "$file")"
other_write=$((8#$mode & 2))
if (( other_write != 0 )); then
printf 'world-writable file: %s mode=%s\n' "$file" "$mode" >&2
exit 1
fi
done < <(find . -type f -not -path './.git/*' -print0)
Храните сценарий политики в репозитории и используйте его как в локальных предварительных проверках, так и в CI. При ошибке выводите только путь, ожидаемый и фактический режимы: этого достаточно для диагностики и это не раскрывает содержимое файлов. При запуске заданий на выделенных физических узлах RentMini эти проверки также следует выполнять заново в каждом новом рабочем каталоге, не предполагая, что окружение осталось неизменным после предыдущего запуска.
В завершение выполните проверку на чистом получении репозитория: удалите временный рабочий каталог, заново клонируйте репозиторий и без ручных исправлений сразу запустите проверку прав и минимальную сборку. Только если этот сценарий проходит успешно, исправление прав действительно попало под контроль версий, а не осталось в отдельном удалённом сеансе.
Часто задаваемые вопросы
Почему после chmod +x конвейер всё равно получает Permission denied?
Нужно проверить фактический путь запуска, возможность исполнения на томе, наличие интерпретатора из shebang и последующие операции копирования или распаковки, способные снова удалить исполняемый бит.
Какой umask выбрать для облачного Mac CI?
Для большинства сборок одного пользователя подходит 022. Значение 002 уместно только для контролируемой групповой записи; итоговые режимы файлов необходимо проверять отдельно.
Как остановить дрейф прав до слияния изменений?
Проверяйте режимы в индексе Git, shebang скриптов, права чувствительных файлов и распакованных артефактов, завершая задачу ненулевым кодом при любом нарушении.
Подготовьте отдельный физический Mac mini к следующей сборке
Выберите конфигурацию M4, срок аренды и узел. Каждый заказ включает отдельное физическое устройство; актуальный статус доступности отображается в консоли в реальном времени.