Команды, скрипты и коды завершения
Курс «Лаборатория GitLab CI/CD»
Конфигурация course-card вызывает две программы, но пока мы смотрели на них как на готовые команды. Теперь разберём их устройство и связь с job. YAML должен описывать процесс, а программа — конкретную работу с данными. Если смешать большие HTML-строки, обработку JSON и несколько оболочек в одной строке script, причины ошибки станут труднее различимы.
В архиве есть полные файлы build.mjs и verify.mjs. Здесь покажем существенные фрагменты, явно называя их место. Программы не запускались при подготовке курса. Ожидаемые сообщения нужны как критерии для самостоятельной работы, а не как отчёт о полученном результате.
Подготовка данных до HTML
В начале build.mjs программа читает JSON и проверяет, что верхний уровень является непустым массивом. Затем проверяет каждый объект: slug состоит из допустимых символов, title непустой, level входит в известный набор, lessons является положительным целым числом. Такой договор нужен до генерации страницы: ошибка исходных данных должна указывать на конкретную запись.
const courses = JSON.parse(await readFile('courses.json', 'utf8'));
if (!Array.isArray(courses) || courses.length === 0) {
throw new Error('courses.json: expected a non-empty array');
}
Этот фрагмент использует импорт readFile из node:fs/promises, который уже есть в полном файле архива. Без импорта он не является самостоятельной программой. Далее валидатор проверяет поля и уникальность slug. Две записи с одним идентификатором нельзя молча считать разными карточками: позднее метаданные выпуска должны однозначно описывать каждый курс.
При вставке заголовка в HTML программа экранирует специальные символы. Текст HTML <основы> должен отображаться как название, а не превращаться в разметку. В нашем проекте данные локальные, но правильная граница между текстом и HTML полезна и здесь. Не следует считать любую строку из JSON готовым безопасным HTML-фрагментом.
Ошибка должна сохраняться
В конце программы используем обработку исключения, которая пишет понятное сообщение и устанавливает ненулевой код. Ошибка чтения, неверный JSON или отсутствующий шаблон должны завершать job как failed. Если просто напечатать сообщение и вернуться с нулевым кодом, GitLab увидит успех, хотя результата нет.
main().catch((error) => {
console.error(`build failed: ${error.message}`);
process.exitCode = 1;
});
process.exitCode задаёт итоговый код, позволяя текущему выводу завершиться. main определён выше в полном файле. Это не название встроенного метода. Такой небольшой общий обработчик не заменяет предметные сообщения внутри проверки: они должны объяснять, какое поле или файл нарушил договор.
В YAML вызываем программу одной командой. Не добавляйте || true к обязательной сборке. Такое выражение может скрыть ненулевой код и сделать job успешным. Оно допустимо только тогда, когда процесс действительно предусматривает игнорирование выбранной ошибки, а причина указана. Для лаборатории отсутствие каталога всегда является существенной ошибкой.
Последовательность команд
build-site:
stage: build
script:
- node scripts/build.mjs
- node -e 'console.log("artifact prepared")'
artifacts:
paths:
- dist/
Если первая команда завершается ошибкой, последующее сообщение не должно создавать впечатление готового результата. При обычной обработке списка GitLab прекращает выполнение на ошибке. Для сложных многострочных блоков и shell-операторов правила чтения статуса могут быть менее очевидными, поэтому держим критические шаги отдельными командами. Особенности перечислены в документации script и журналов.
Каждый job запускает свою командную среду. Переменная, созданная через export в build, не появится автоматически в verify. Для передачи значения потребуется механизм метаданных или dotenv-артефакт, который рассмотрим позже. Вынос Bash-функции в один job также не делает её доступной следующему. Программа в репозитории надёжнее неявного состояния оболочки.
Поэтому используем node scripts/build.mjs, а не полагаемся на установленную у конкретного runner пользовательскую функцию build_site. Файл хранится вместе с версией исходников и может быть прочитан при разборе ошибки. Если команда отсутствует в образе, это отдельная ошибка среды; если файл программы не найден, нужно проверить путь и состав commit.
Диагностика по слою
Измените мысленно lessons первого курса с 8 на строку "eight". Ожидаем сообщение о неверном числе уроков и ошибку build. Сохранённый артефакт при стандартном режиме успеха не появляется. Это отличается от ситуации, когда build корректен, но verify не получил files. Разные слои должны выдавать разные причины.
Вторая программа читает release.json, получает перечень файлов и сверяет SHA-256 каждого из них. При нарушении условия она создаёт отчёт с причиной и затем возвращает код 1. Отчёт не делает ошибку допустимой: он лишь сохраняет сведения для просмотра. Позже мы добавим artifacts:when:always, чтобы этот файл был доступен и при failed job.
| Причина | Где искать | Ожидаемый результат |
|---|---|---|
| Неверный JSON | Build script | Понятная ошибка разбора или поля |
| Нет команды node | Среда job | Ошибка до работы с данными |
| Нет dist | Передача артефакта | Verify сообщает об отсутствующем файле |
| Изменился HTML | Проверка выпуска | Несовпадение ожидаемого и фактического хэша |
Сообщение не должно печатать секреты или всё окружение ради удобства диагностики. Для нашей лаборатории достаточно имени этапа, относительного пути и нарушения договора. Наличие тысячи строк вывода не делает ошибку понятнее. Сначала выберите, какое решение должен принять человек после чтения сообщения, затем добавьте нужные данные.
В журнале полезно сохранять краткую границу этапа: чтение данных, подготовка HTML, запись метаданных и завершение. Не выводите всё содержимое файла при любой ошибке: для большого каталога это создаст шум, а для закрытых данных может раскрыть лишнее. Сообщение с индексом записи и именем поля обычно помогает найти причину быстрее. При этом код завершения должен соответствовать обязательности этапа. Если вывод предупреждает о потерянном шаблоне, а job успешен, человек не сможет доверять состоянию без ручного чтения каждого запуска. Именно поэтому программа различает необязательный кэш и обязательные входы.
Чистые входы и явные выходы
Сборка начинает с очистки собственного dist/, а затем создаёт его заново. Это не позволяет старой карточке остаться после удаления курса из JSON. При этом программа работает только с известной папкой проекта; не получает произвольный путь из пользовательского ввода. Такие границы полезны даже в учебном примере, где нет внешней публикации.
Результаты verify записываются в reports/, отдельно от dist/. Папка выпуска должна содержать только то, что передаётся как сайт и метаданные. Смешение журнала проверки с HTML усложнило бы подсчёт хэшей и договор staging. Каждому выходу даём назначение и сохраняем его на соответствующем этапе.
После урока ожидаем команды, которые отражают результат через код завершения, и сообщения, объясняющие конкретный слой ошибки. В следующем шаге решим, при каких событиях вообще создавать pipeline, чтобы одинаковая работа не запускалась дважды для одного изменения.