Навигация
GitRiver CI — Руководство пользователя
Конвейеры сборки: файл описания, задания и шаги, зависимости, переменные и секреты, кеш, артефакты, исполнители
Рабочий процесс GitRiver CI описывается YAML-файлом в репозитории, и основа его — скрипты
run:. Готовые действия (uses:) поддержаны частично, а их внешние источники администратор установки ограничивает списком разрешённых доменов — см. «uses— готовые действия».
Быстрый старт
Создайте файл .gitriver/workflows/ci.yml в корне репозитория:
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
test:
steps:
- run: echo "Hello, GitRiver CI!"
При отправке в main или открытии запроса на слияние запустится рабочий процесс с одним заданием test.
Структура файла рабочего процесса
name — имя рабочего процесса
name: Build and Deploy
Отображается в интерфейсе. Если не указано — используется имя файла.
on — триггеры
Определяют, когда запускается рабочий процесс.
push — отправка
on:
push:
branches: [main, develop, 'release/**']
branches-ignore: ['feature/wip-*']
tags: ['v*']
tags-ignore: ['v*-rc*']
paths: ['src/**', 'Cargo.toml']
paths-ignore: ['docs/**', '*.md']
branches/branches-ignore— маски ветокtags/tags-ignore— маски теговpaths/paths-ignore— рабочий процесс запускается только если изменены файлы по маскеbranchesиbranches-ignoreвзаимоисключающие (нельзя оба)- Если указаны
paths, рабочий процесс пропускается, когда ни один файл не совпал
Отправкой считается любая запись в ветку, а не только git push из консоли:
правка файла через интерфейс, пакетный коммит и сборка черновиков знания
поднимают то же событие и запускают те же рабочие процессы.
Маски путей
Правило одно на все маски CI — branches, tags, paths/paths-ignore
в on: и paths в artifacts::
- маска сопоставляется с путём целиком (для
artifacts:— от рабочей копии задания), а не с именем файла:*.so— этоlib.soв корне, но неtarget/release/lib.so; *— любая последовательность символов внутри одного сегмента,?— ровно один символ; ни тот, ни другой не пересекают/;**— отдельный сегмент маски: ноль или больше сегментов пути.src/**— и самsrc, и всё, что под ним;**/*.rs— иmain.rs, иsrc/main.rs;a/**/b— иa/b, иa/x/y/b;- маска без
*и?— это точный путь. Классы символов ([abc]) не поддерживаются и сравниваются как обычные символы; - в
branchesиtagsмаска из одной звёздочки ('*') читается как «любое значение», включая имена со слэшем:branches: ['*']— все ветки.
pull_request — запрос на слияние
on:
pull_request:
types: [opened, synchronize, reopened]
branches: [main]
paths: ['src/**']
types— какие события запроса на слияние запускают рабочий процесс (по умолчанию:opened, synchronize, reopened)branches— целевая ветка запроса (куда сливают)paths— фильтр по изменённым файлам
Сервер порождает три действия: opened — при создании запроса, reopened — при
повторном открытии, synchronize — при каждой отправке в исходную ветку, пока
запрос открыт. Правка заголовка и описания прогона не запускает; сменить целевую
ветку у открытого запроса нельзя.
paths считается по разнице всего запроса — от точки ветвления до его вершины,
а не по одной отправке: результат не зависит от того, каким по счёту коммитом
внесено изменение.
schedule — расписание cron
Точность расписания. Cron-выражения проверяются раз в минуту, поэтому фактический запуск может отстать от заданного времени на несколько десятков секунд. Повторное совпадение по одной и той же ветке в пределах 120 секунд не запускает вторую сборку — рабочий процесс с несколькими близкими cron-правилами не задваивает запуск.
on:
schedule:
- cron: '0 2 * * *' # Каждый день в 02:00 UTC
- cron: '0 */6 * * *' # Каждые 6 часов
- Стандартный cron из 5 полей (
минута час день месяц день_недели) - Выполняется на ветке по умолчанию
- Минимальный интервал: 15 минут
У расписания есть автор — тот, чьей отправкой cron попал в ветку по умолчанию.
Перед каждым запуском проверяется, что этот человек по-прежнему существует, не
отключён внешним управлением (SCIM) и сохраняет право ci:trigger в репозитории.
Иначе расписание приостанавливается: сборка не идёт, а причина видна на
вкладке CI/CD (GET /api/v1/repos/{owner}/{name}/ci/schedules). Так уход
сотрудника не оставляет после себя работающего механизма развёртывания: и
отправка, и ручной запуск такому человеку тоже отказывают. В архивном
репозитории расписание приостанавливается по той же причине — запись в него
запрещена.
Приостановка снимается сама: право проверяется заново на каждом сроке запуска, и восстановленный доступ возвращает расписание к работе без вмешательства. Если учётной записи автора больше нет, расписание заводится заново — изменением cron-выражения в рабочем процессе: новая пара (файл, cron) получает автором того, кто её внёс.
Если отправку, внёсшую расписание, сделал не человек (токен задания CI или токен развёртывания), а также у расписаний, для которых автор не записан, автором считается владелец репозитория. Такое расписание встаёт, когда владельца отключают внешним управлением, и возобновляется, как только учётную запись включат обратно или репозиторий передадут действующему владельцу.
Ручной запуск (workflow_dispatch)
on:
workflow_dispatch:
inputs:
environment:
description: 'Окружение для развёртывания'
required: true
type: choice
options: [staging, production]
default: staging
debug:
description: 'Включить отладку'
type: boolean
default: false
version:
description: 'Версия для развёртывания'
type: string
replicas:
description: 'Количество реплик'
type: number
default: 3
Типы параметров запуска (inputs): string, boolean, choice, number.
Значения доступны в переменных: $INPUT_ENVIRONMENT, $INPUT_DEBUG и т.д.
Комбинирование триггеров
on:
push:
branches: [main]
pull_request:
schedule:
- cron: '0 2 * * *'
workflow_dispatch:
Рабочий процесс запустится при любом из перечисленных событий.
env — глобальные переменные окружения
env:
RUST_LOG: info
CARGO_TERM_COLOR: always
REGISTRY: registry.example.com
Доступны всем заданиям и шагам. Переопределяются на уровне задания и шага.
concurrency — группа одновременности
concurrency:
group: deploy-$CI_COMMIT_BRANCH
cancel-in-progress: true
group— имя группы (поддерживает подстановку$VAR)cancel-in-progress: true— автоматически отменяет предыдущий запуск в той же группе
Группа обещает одно: в ней одновременно идёт не больше одного конвейера.
cancel-in-progress выбирает, чем это обещание держится:
cancel-in-progress |
Что происходит с новым запуском | Что происходит с идущим |
|---|---|---|
true |
идёт сразу | отменяется |
false (по умолчанию) |
ждёт в очереди группы | доходит до конца |
Ждущий конвейер заведён и виден в списке — с отметкой «в очереди группы», — но исполнителей не занимает. Как только место освободится, он начнётся сам; отдельно запускать его не нужно.
Ждать может только последний: если за время ожидания пришла ещё одна отправка, предыдущий ожидающий отменяется. Так на активной ветке не копится ряд заведомо устаревших сборок.
Пока конвейер ждёт места:
- ручной запуск задания и его повтор отвечают отказом — задание пошло бы мимо группы, заняв исполнителей, пока в ней идёт другая сборка;
- перезапуск сервера ждущий конвейер не роняет: освободившееся место он займёт сам;
- конвейер снимается, если репозиторий архивировали или рабочий процесс исчез из коммита (ветку переписали) — ждать стало нечего;
- конвейер из форка, ожидающий разрешения сопровождающего, места не занимает и проходит очередь уже после разрешения.
Группа действует в пределах репозитория: одноимённая группа в другом репозитории — другая группа, и запуски двух проектов друг друга не отменяют и друг друга не ждут.
Прерываемость по ref работает как обычно и при объявленной группе: задание с
interruptible: true прервётся новой отправкой.
Пример: при отправке в main новое развёртывание отменяет старое, ещё не завершённое.
Краткая форма:
concurrency: deploy-$CI_COMMIT_BRANCH
Эквивалентна group: ..., cancel-in-progress: false — то есть с ожиданием
в очереди, а не с отменой идущего.
jobs — задания
Задание — единица выполнения. Задания по умолчанию запускаются параллельно.
jobs:
build:
name: Сборка проекта
steps:
- run: cargo build --release
test:
name: Тесты
needs: [build]
steps:
- run: cargo test
needs — зависимости между заданиями
jobs:
build:
steps:
- run: cargo build
unit-tests:
needs: [build]
steps:
- run: cargo test --lib
integration-tests:
needs: [build]
steps:
- run: cargo test --test '*'
deploy:
needs: [unit-tests, integration-tests]
steps:
- run: ./deploy.sh
- Задания из
needsдолжны завершиться успешно до запуска - Без
needsзадание запускается сразу (параллельно с другими) - Циклические зависимости — ошибка проверки файла
Зависимость можно записать и словарём:
deploy:
needs:
- job: tests
- job: lint
optional: true
optional: true означает, что зависимость может не выполняться: если lint
пропущен своим условием if:, deploy всё равно запустится — после tests.
Провал необязательной зависимости останавливает зависимое задание так же, как
провал обязательной. Чтобы её провал не мешал зависимым, объявите у неё
allow-failure: true.
if — условие запуска задания
jobs:
deploy:
if: $CI_COMMIT_BRANCH == "main"
steps:
- run: ./deploy.sh
notify-on-fail:
if: failure()
needs: [deploy]
steps:
- run: curl -X POST $SLACK_WEBHOOK
Поддерживаемые выражения:
| Выражение | Описание |
|---|---|
$VAR == "value" |
Сравнение строк |
$VAR != "value" |
Неравенство |
$VAR =~ /pattern/ |
Совпадение с регулярным выражением |
$VAR |
Истина, если переменная задана и не пуста |
!expr |
Логическое отрицание |
expr1 && expr2 |
Логическое И |
expr1 || expr2 |
Логическое ИЛИ |
(expr) |
Группировка |
success() |
Все предыдущие задания успешны (по умолчанию) |
failure() |
Хотя бы одно предыдущее задание провалилось |
always() |
Всегда выполнять (даже при отмене) |
cancelled() |
Конвейер отменён |
Условие вычисляет сервер — до того, как задание попадёт исполнителю, и одинаково
для встроенного исполнителя и внешнего исполнителя. Задание с ложным условием
получает состояние «пропущено» и исполнителю не выдаётся вовсе, поэтому runs-on
на действие if: не влияет: deploy с условием по ветке с другой ветки не
развернётся ни на одном исполнителе.
Пропуск — не провал. Для зависящих от него заданий пропущенное задание значит «не выполнялось»:
- задание с условием по умолчанию (
success()) тоже пропускается, если зависимость не объявлена сoptional: true; failure()истинно, только если зависимость провалилась. В примере вышеnotify-on-failна ветке, отличной отmain, гдеdeployпропущен, не выполняется: провала не было;- отменённая зависимость тоже останавливает задание с условием по умолчанию, но провалом не считается.
Чтобы зависимое задание выполнялось при любом исходе зависимостей,
объявляйте его с if: always().
image — Docker-образ
jobs:
build:
image: rust:1.82-slim
steps:
- run: cargo build --release
Задание выполняется внутри указанного контейнера через docker run.
Если образ не указан — скрипт выполняется напрямую в окружении исполнителя.
Скрипт выполняется в контейнере от root, поэтому созданные им файлы получают
владельца root и права по umask образа. Сразу после скрипта исполнитель
возвращает себе права на рабочую копию — тем же образом задания, коротким
контейнером. Благодаря этому на образе с umask 0077 (например, redis:7)
артефакты, отчёты и выходные данные задания собираются как обычно. Если вернуть
права не удалось, в журнале задания появляется предупреждение. Исполнитель
старее сервера этот шаг не выполняет — обновляйте исполнителей вместе с
сервером.
services — сервисные контейнеры
jobs:
test:
image: rust:1.82
services:
- image: postgres:16
alias: db
env:
POSTGRES_DB: test
POSTGRES_USER: test
POSTGRES_PASSWORD: test
- image: redis:7
alias: cache
env:
DATABASE_URL: postgres://test:test@db:5432/test
REDIS_URL: redis://cache:6379
steps:
- run: cargo test
alias— имя узла для доступа к сервису из контейнера заданияenv— переменные окружения для сервиса- Если
aliasне указан — используется имя образа до:(postgres, redis) - Сервисы запускаются в сети Docker и доступны по
aliasкак по имени узла - Требуется
image:— без образа Docker у задания сервисы не поднимаются; причина пишется в журнал задания, а не пропускается молча
Директива работает одинаково у встроенного исполнителя и у внешнего исполнителя:
имя, по которому скрипт обращается к сервису, на обоих одно и то же; сеть и
контейнеры поднимает тот, кто выполняет задание, на своём Docker. Исполнитель
старее сервера поле services не разберёт и выполнит задание без сервисов —
обновляйте исполнителей вместе с сервером.
timeout — срок задания
jobs:
build:
timeout: 30m # 30 минут
steps:
- run: cargo build --release
Форматы: 30s, 10m, 1h, 1h30m. По умолчанию: 1h. Максимум: 6h.
Задание, не уложившееся в срок, снимается и числится провалившимся (не
отменённым: отмена — решение человека). Последняя строка его журнала — «Задание
снято по истечении срока», и повтор retry: when: [stuck_or_timeout_failure]
рассчитан именно на этот исход. after_script при этом выполняется — у него
свой срок (ci_after_script_timeout_secs).
allow-failure — допустимый провал
jobs:
lint:
allow-failure: true
steps:
- run: cargo clippy -- -D warnings
Если задание провалится, конвейер упавшим не считается. В интерфейсе задание отмечается предупреждением.
retry — автоповтор при провале
jobs:
test:
retry: 2 # Повторить до 2 раз при любой ошибке
steps:
- run: cargo test
Расширенная форма:
jobs:
test:
retry:
max: 2
when: [script_failure, stuck_or_timeout_failure]
steps:
- run: cargo test
max— максимальное количество повторов (0–2)when— при каких ошибках повторять:always— при любой ошибке (по умолчанию)script_failure— при ненулевом коде выходаstuck_or_timeout_failure— при истёкшем сроке
Отмена старше retry:: отменённое задание не повторяется ни при каком when —
отмена — решение пользователя, а не сбой, который стоит переигрывать.
Между попытками в журнал записывается разделитель ──── Повтор 1/2 ────.
У задания с image: перед разделителем идёт строка
Снятие контейнера прошлой попытки: <имя>: контейнер прерванной попытки
снимается, чтобы следующая могла стартовать.
Политика действует и на заданиях внешнего исполнителя. Повтор там серверный: задание
сбрасывается и ставится в очередь исполнителей заново — новую попытку вправе взять
другой исполнитель. Невыполнение задания в срок исполнитель сообщает отдельно
от провала скрипта, поэтому when: [stuck_or_timeout_failure] различает их и
на нём. Исполнитель старее сервера сообщает любой провал как script_failure.
interruptible — прерываемость при новой отправке
jobs:
test:
interruptible: true
steps:
- run: cargo test
Если interruptible: true и приходит новая отправка в тот же ref, идущее задание автоматически отменяется в пользу нового конвейера. Полезно для тестов, бесполезно для развёртывания.
Отличие от concurrency:
concurrency— держит в группе (по имени группы, внутри своего репозитория) не больше одного идущего конвейера: сcancel-in-progress: trueотменяя идущий, без него — ставя новый в очередьinterruptible— отменяет по ref (ветке/тегу), без явного указания группы
runs-on — метка исполнителя
Задание с
runs-on, отличным отdefault, выполняет внешний исполнитель (gitriver-runner) с подходящими метками — см. «Клонирование репозитория внешним исполнителем». Если исполнители на установке не настроены вовсе, задание выполняется встроенным исполнителем.Правило одно для всех способов запуска: первичный запуск конвейера, повтор задания и ручной запуск одинаково отдают задание с
runs-onвнешнему исполнителю — результат повтора равен результату первого запуска.Если встроенный исполнитель выключен настройкой
ci_local_executor_enabled(установка без Docker), метки не рассматриваются вовсе: внешним исполнителям уходит вся очередь, включаяruns-on: defaultи задания безruns-on. Задание без меток вправе взять исполнитель с любыми метками, аruns-on: default— исполнитель, у которого меткаdefaultобъявлена.Запасного варианта «выполнить на сервере» в этом режиме нет: если исполнители не настроены или очередь недоступна, задание сразу завершается ошибкой, а причина пишется в его журнал («встроенный исполнитель отключён, нет доступного исполнителя»). Исполнитель, который зарегистрирован, но не в сети, — другой случай: задание ждёт его в очереди до
ci_runner_max_wait_secs.
runs-on: default # Встроенный исполнитель
runs-on: [linux, docker] # Исполнитель с обеими метками
steps — шаги внутри задания
Каждый шаг — одна команда или блок команд.
steps:
- name: Установка зависимостей
run: |
apt-get update
apt-get install -y protobuf-compiler
- name: Сборка
run: cargo build --release 2>&1
env:
RUSTFLAGS: '-C target-cpu=native'
- name: Уведомление
if: failure()
run: echo "Сборка провалилась"
- name: Уборка
if: always()
run: rm -rf tmp/
Поля шага
| Поле | Описание |
|---|---|
name |
Отображаемое имя (необязательно) |
id |
Идентификатор шага (необязательно). Сослаться на него нечем: выходные данные шагов не собираются |
run |
Команда или многострочный скрипт |
uses |
Готовое действие вместо команды — см. «uses — готовые действия». С run в одном шаге несовместимо |
with |
Входные параметры действия (только для uses) — приходят в него как INPUT_* |
if |
Условие выполнения. if: always() — шаг выполняется при любом исходе предыдущих шагов, if: failure() — только после их провала; такие шаги идут в after_script — см. ниже |
env |
Переменные окружения (только для этого шага) |
timeout |
Срок шага (только для run) |
continue-on-error |
true — шаг не ломает задание при ошибке |
working-directory |
Рабочий каталог (только для run) |
shell |
Оболочка шага run: sh (по умолчанию) или bash |
Шаги не изолированы друг от друга. Они идут в одном сеансе командной оболочки: команда
cd, экспортированная переменная или файл, которые создал один шаг, видны и следующим без явной передачи — как и рабочий каталог, они общие для всех шагов задания.Исключение — шаг с
shell: bash,timeoutилиworking-directory: он выполняется отдельным процессом, и егоcdиexportследующим шагам не достаются (созданные им файлы — достаются).
Многострочные скрипты
- run: |
echo "Шаг 1"
echo "Шаг 2"
if [ -f config.yml ]; then
echo "Конфиг найден"
fi
Выполняется как один скрипт оболочки sh с set -e (остановка при первой ошибке).
uses — готовые действия
Шаг с uses: выполняет готовое действие — код, который лежит не в этом
файле рабочего процесса. С run: в одном шаге поле несовместимо: шаг — либо команда,
либо действие.
steps:
- name: Проверка формата
uses: myorg/actions/fmt@v1
with:
check-only: 'true'
Четыре формы источника
uses: |
Откуда берётся действие | @ref |
|---|---|---|
docker://образ:тег |
Образ запускается напрямую, action.yml не нужен |
не нужен |
./путь, ../путь |
Каталог рабочей копии — путь от текущего каталога шага, то есть от её корня, если шаги до этого не делали cd |
не нужен |
owner/repo@ref, owner/repo/подпапка@ref |
Репозиторий этой установки | обязателен |
хост/owner/repo@ref, хост/owner/repo/подпапка@ref |
Внешний git-сервер, клон по HTTPS | необязателен (умолчание main) |
Формы различает точка в первом сегменте пути — это имя хоста. Точка в теге
(myorg/action@v1.2.3) или в имени репозитория (myorg/my.action@v1) источник
не меняет: такое действие берётся с этой же установки.
@ref — имя ветки или тега; SHA коммита в этом месте не принимается.
Хвост за owner/repo (и за хост/owner/repo) — подпапка действия внутри
репозитория, а не часть его адреса: клонируется репозиторий, а action.yml
читается из подпапки.
Действие с этой установки клонируется учётной записью задания (CI_JOB_TOKEN),
поэтому приватный репозиторий доступен — в пределах прав задания (см.
«Что может CI_JOB_TOKEN»). Внешний источник
клонируется анонимно: приватный внешний репозиторий не склонируется.
Клон действия кешируется по паре «адрес + ref» в каталоге /tmp/gitriver-actions
(переопределяется переменной GITRIVER_ACTION_CACHE). Кеш живёт там, где идёт
скрипт задания: у задания без image: — на машине исполнителя, у задания с
image: — в его контейнере, то есть ровно одно задание. Пока клон в кеше есть,
он не обновляется — движение подвижной ссылки (@main) исполнитель не
подхватит, указывайте неподвижный тег.
Ограничение внешних источников
Внешние действия подчиняются списку разрешённых доменов, который задаёт
администратор установки — allowed_external_action_domains (см.
руководство по установке):
| Настройка | Что разрешено |
|---|---|
| не задана | любой внешний источник |
[] — пустой список |
ни одного: любое внешнее действие запрещено |
["git.example.com"] |
весь домен |
["git.example.com/actions"] |
только этот владелец |
["git.example.com/actions/checkout"] |
только этот репозиторий |
Сравнение регистронезависимо и идёт по границе сегмента: git.example.com не
совпадает с git.example.com.evil.test, а git.example.com/actions — с
git.example.com/actions-evil.
Запрещённое действие отклоняет весь файл рабочего процесса: конвейер по нему не создаётся вовсе — ни зелёный, ни красный. Причину автор видит на коммите — см. «Отклонённый файл рабочего процесса».
Как выполняется действие
docker://образ запускается сразу: контейнер этого образа с рабочей копией,
смонтированной в /workspace (он же рабочий каталог). У остальных форм
платформа читает action.yml (или action.yaml) в каталоге действия и смотрит
runs.using:
runs.using |
Что делает платформа | Что нужно в среде задания |
|---|---|---|
composite |
Выполняет шаги run: действия |
python3 (разбор action.yml) и bash |
docker |
Берёт runs.image: Dockerfile — собирает на месте, docker://… или имя образа — запускает готовый. Рабочая копия монтируется в /github/workspace |
docker |
node12, node16, node20, node* |
Запускает runs.main в каталоге действия |
node |
Любой другой using (как и отсутствие action.yml) — ошибка шага. Оболочка у
составного действия своя, не как у задания, — см. «Оболочка».
Права рабочей копии после контейнера действия платформа возвращает сама, и его же снимает — см. «Права файлов после контейнеров» и «Судьба контейнеров после задания».
with — входные параметры
Каждая пара из with: приходит в действие переменной окружения INPUT_<ИМЯ>:
имя переводится в верхний регистр, - заменяется на _. Имя, которое не
является идентификатором окружения, пропускается молча. Docker-действие получает
те же INPUT_*.
- uses: myorg/actions/fmt@v1
with:
check-only: 'true' # внутри действия: $INPUT_CHECK_ONLY
Экспорт живёт до конца задания, а не до конца шага: INPUT_* одного действия
остаются в окружении и достаются следующему — локальному, клонированному и
действию с using: docker (шаг uses: docker://… — исключение, ему передаются
только его собственные). Это следствие общего сеанса: если действие ведёт себя
по-разному при заданном и незаданном параметре, задавайте параметр явно.
Чего из GitHub Actions нет
| Не поддержано | Что это значит на практике |
|---|---|
Выражения ${{ … }} |
Не вычисляются нигде, в том числе в with: — значение уходит в действие буквально. В run: подстановку делает оболочка ($VAR) |
inputs: из action.yml |
default: не подставляется, required: не проверяется: действие получает ровно то, что задано в with: |
| Выходные данные действия | $GITHUB_OUTPUT, ::set-output и steps.<id>.outputs.* не читаются. Данные между заданиями передаются через $CI_OUTPUT (см. «outputs») |
args:, entrypoint: docker-действия |
Игнорируются: контейнер идёт с ENTRYPOINT/CMD своего образа |
runs.env, runs.pre, runs.post |
Не выполняются |
| Вложенность составного действия | Берутся только его шаги run:; uses:, shell:, if: и working-directory внутри действия игнорируются |
timeout, working-directory, shell на шаге |
Работают только на шаге run:. У шага uses: они игнорируются; if, env и continue-on-error действуют у обоих |
| Изоляция шага | Действие идёт в общем сеансе со всеми run:-шагами задания: те же переменные, тот же каталог |
| Каталог действий GitHub (Marketplace) | Источников ровно четыре, перечисленных выше; своего каталога действий у установки тоже нет |
Оболочка
- run: echo $HOME
shell: bash # По умолчанию: sh
Доступные оболочки: sh, bash. При shell: bash шаг выполняется отдельным
процессом bash.
Оболочка одна у обоих исполнителей — sh с set -e. Она есть в любом
образе, включая alpine и distroless, где bash не установлен. Правило
действует одинаково для скрипта задания и after_script, на хосте и в
контейнере образа.
Шаг, который выполняется отдельным процессом (shell: bash, timeout),
set -e не наследует: многострочный скрипт такого шага не останавливается на
первой ошибке, а исход шага берётся от его последней команды. Нужна остановка
на первой ошибке — начните такой скрипт с set -e.
pipefail не включён: этой команды нет в POSIX sh, поэтому исход
конвейера команд берётся от его ПОСЛЕДНЕЙ команды —
- run: тесты | tee отчёт.log # падение `тесты` задание НЕ уронит
Нужна строгость конвейера команд — включите её сами, в своём шаге: вы знаете, какая оболочка в вашем образе, а исполнитель нет.
- run: set -o pipefail; тесты | tee отчёт.log
shell: bash
Составное действие (uses: с using: composite) — исключение: его шаги идут
через bash -eo pipefail. Такому шагу нужен bash в образе задания, хотя его
собственному скрипту он не нужен.
after_script: где он выполняется
Шаги с if: always() и if: failure() выполняются отдельной частью задания —
after_script: после всех остальных шагов, в порядке объявления. Шаг с
always() выполняется при любом исходе скрипта, шаг с failure() — только
если скрипт провалился (в том числе снят по истечении срока). Исход скрипта
after_script видит в переменной CI_JOB_STATUS: success или failed.
Задать её своим значением нельзя — платформа ставит её поверх переменных
задания.
after_script выполняется там же, где скрипт задания. Правило одно у
встроенного исполнителя и у внешнего исполнителя:
| Задание | Где идёт after_script |
|---|---|
С image: |
В контейнере того же образа |
Без image: |
Оболочкой исполнителя на хосте — как и сам скрипт |
Отменённое задание after_script не выполняет — ни у одного из исполнителей:
отмена значит «прекратить», а не «сделать ещё шаг». Задание, снятое по сроку,
after_script выполняет.
Своей директивы срока у after_script нет: срок задания (timeout:) к этому
моменту израсходован его скриптом. Предел задаёт установка —
GITRIVER_CI_AFTER_SCRIPT_TIMEOUT_SECS (умолчание 300 с), и он один на обоих
исполнителей. По его истечении работа прекращается: процессов после себя
after_script не оставляет.
Что это значит для задания с image::
- команды образа (
npm,mvn,cargoиз него) вafter_scriptдоступны — на хосте их могло не быть вовсе или быть другой версии; $CI_WORKSPACE,$CI_PROJECT_DIRи$CI_OUTPUTуказывают внутрь контейнера — туда же, куда и во время скрипта;- сервисы задания ещё подняты, и алиасы
services:разрешаются; - контейнер
after_script— отдельный контейнер того же образа. Наследуется только рабочая копия: пакеты, установленные скриптом вне её, файлы вне рабочего каталога и фоновые процессы контейнера задания доafter_scriptне доживают; - шаг
uses:сif: always()идёт в контейнере тоже — значит docker-действию нужен docker внутри образа задания, ровно как такому же шагу безif: always().
build:
image: node:20
steps:
- run: npm ci && npm test
# npm — команда образа; на хосте исполнителя её может не быть
- run: npm run report -- --out отчёт.txt
if: always()
Права файлов после контейнеров
Контейнер работает от root, а рабочая копия монтируется в него на запись. У
части образов umask = 0077 (например, redis:7), и файл, созданный в
контейнере, выходит -rw------- root:root: исполнитель, который работает от
обычного пользователя, такой файл не открывает. Симптом всегда один — задание
успешно, а артефактов, отчётов и $CI_OUTPUT нет.
Платформа сама возвращает себе доступ ко всей рабочей копии до всего, что
читает её с хоста: сбора артефактов, отчётов, выходных данных и кеша. У задания
с image: это происходит после after_script — он идёт в контейнере того
же образа и тоже оставляет файлы от root; у задания без image: — сразу
после скрипта, и ещё раз после after_script, если в нём поднимался контейнер.
| Контейнер | Права возвращает |
|---|---|
Контейнер задания (image:) |
Платформа — образом задания |
Контейнер действия (uses: docker://…, действие с using: docker, в том числе image: Dockerfile) |
Платформа — образом этого контейнера |
Контейнер, запущенный самим скриптом (docker run, docker build в run:) |
Автор рабочего процесса |
Граница проходит по тому, кто контейнер запустил: о контейнерах, которые
запускает сам скрипт, платформа не знает ничего — ни образа, ни того, какие его
файлы заданию ещё нужны. Список образов своих контейнеров платформа держит в
служебном файле .ci-action-images в корне рабочей копии; в артефакты и кеш
он не попадает.
Судьба контейнеров после задания
По той же границе платформа их и снимает — до возврата прав, сбора артефактов и снятия рабочей копии:
| Контейнер | Как опознаётся | Кто снимает |
|---|---|---|
Контейнер задания (image:) и контейнер after_script |
По имени | Платформа |
Контейнер действия (uses:) |
По метке gitriver-ci-job=$CI_JOB_ID — имени у него нет |
Платформа |
Контейнер, запущенный самим скриптом (docker run в run:) |
Никак | Автор рабочего процесса |
Это важно при сроке и отмене: они убивают процессы задания, но не контейнеры
— убит клиент docker run, а контейнер демона работает дальше. Свои платформа
снимает сама; контейнер, поднятый скриптом задания вручную, переживёт его, если
автор не запустил его с --rm и не снял в after_script.
Уборка прерванного задания, которое запускало контейнеры, может занять до нескольких секунд: платформа дожидается контейнеров, которые демон ещё не успел создать.
Если ваш шаг поднимает контейнер сам, верните права в том же шаге — одним из двух способов:
steps:
# 1. Контейнер идёт от вашего пользователя — подходит, если ему не нужен root
- run: docker run --rm --user "$(id -u):$(id -g)" -v "$PWD:/w" -w /w alpine sh -c 'echo дано > out.txt'
# 2. Контейнеру нужен root — верните владельца сразу после него
- run: |
docker run --rm -v "$PWD:/w" -w /w alpine sh -c 'umask 077; make build'
docker run --rm -v "$PWD:/w" --entrypoint sh alpine -c 'chown -Rh "$(stat -c "%u:%g" /w)" /w'
Когда возврат прав не удался (например, демон с userns-remap), платформа не
молчит: в журнале задания появляется строка о том, что права рабочей копии не
приведены, — она и объясняет пустые артефакты ниже по журналу.
strategy — матричные сборки
jobs:
test:
strategy:
matrix:
os: [ubuntu, alpine]
rust: ['1.80', '1.82', stable]
fail-fast: false
max-parallel: 4
image: rust:$MATRIX_RUST
steps:
- run: cargo test
- Порождает задание для каждой комбинации (2 × 3 = 6 заданий)
- Значения доступны через
$MATRIX_<KEY>(в верхнем регистре) fail-fast: true(по умолчанию) — отменяет остальные при первом провалеmax-parallel— сколько заданий матрицы выполняется одновременно. Предел считает задания, а не ресурсы сервера: он одинаково действует на задания встроенного исполнителя и на задания сruns-on(внешние исполнители), а также на повтор и ручной запуск отдельного задания матрицы. Значение0запрещено — рабочий процесс отклоняется при разборе
Имя каждого задания матрицы собирается как <имя> (<значения>) и служит именем
файлов его журнала, выходных данных и артефактов. Поэтому значения матрицы не
должны содержать /, \, .. и нулевой байт: рабочий процесс со значением вида
alpine/git отклоняется с ошибкой при запуске. Полное имя (вместе с суффиксом)
ограничено 255 символами.
include / exclude
strategy:
matrix:
os: [ubuntu, alpine]
rust: [stable, nightly]
include:
- os: ubuntu
rust: nightly
features: experimental # Дополнительная переменная
exclude:
- os: alpine
rust: nightly # Не тестируем nightly на alpine
include— добавляет комбинации или дополняет существующиеexclude— исключает конкретные комбинации
artifacts — артефакты задания
jobs:
build:
steps:
- run: cargo build --release
artifacts:
paths:
- target/release/myapp
- target/release/*.so
expire-in: 7d
paths— пути и маски того, что сохранить. Читаются они общим правилом масок: от рабочей копии задания,*не пересекает/,**— ноль или больше сегментовexpire-in— время хранения (по умолчанию:30d)- Форматы:
1h,7d,30d,1y,never - Артефакты доступны заданиям, которые зависят через
needs. Задание безneedsполучает артефакты тех заданий своего конвейера, что уже успели их сохранить (кроме собственных: повторный запуск начинается с чистой рабочей копии), — порядок выполнения задают толькоneeds, поэтому рассчитывать на такой набор можно лишь объявив зависимость - Скачиваются через интерфейс
- Каталог сохраняется целиком: и когда назван путём (
paths: [public]), и когда его накрыла маска (pub*,public/**) — вместе с вложенными файлами и пустыми подкаталогами. Внутрь символических ссылок раскрытие не идёт: сама ссылка в архив попадает, ассылка/*файлов не даст — иначе ссылка на каталог вне рабочей копии вынесла бы в артефакт чужие файлы. Путь без маски проходит и через ссылку (ссылка/index.html): его автор назвал явно - Вся рабочая копия (
paths: ['.'],paths: ['**']) сохраняется её содержимым: каждый файл и каталог корня — своей записью, каталоги по-прежнему целиком - Служебные записи исполнителя в архив не попадают — ни под маской, ни
названные путём:
.git-credentials(учётная запись задания с действующимCI_JOB_TOKEN),.docker(учётная запись реестра контейнеров послеdocker login),.ci-output(файл$CI_OUTPUT) и.ci-action-images(образы контейнеров, которые платформа запустила из скрипта задания, — см. Права файлов после контейнеров). Их кладёт в корень рабочей копии сам исполнитель. Отgitони скрыты.git/info/exclude, поэтомуgit add -Aв скрипте задания их не добавляет .gitв архив не попадает — ни под маской, ни названный путём: в.git/configрабочей копии лежит доступ задания к LFS (lfs.urlи заголовокAuthorization: Basicс токеном задания), иpaths: ['.']вынес бы его в архив открытым текстом. В обратную сторону правило то же: при восстановлении записи.gitиз архива пропускаются —.gitрабочей копии всегда даёт клон её собственного задания. Задание, которое сохраняло репозиторий с историей (paths: ['.']для развёртывания), получит архив без.git: нужную историю следует класть в артефакт явным путём (например,git bundle create история.bundle --all)- Состав архива по одним и тем же
pathsодинаков у встроенного исполнителя и у внешнего исполнителя - Число файлов под маской не ограничено:
paths: ['**/*.js']в проекте сnode_modulesсобирается целиком, сколько бы файлов маска ни накрыла - Имя файла вне UTF-8, попавшее под маску (или в корне рабочей копии, когда сохраняется она целиком), роняет задание со строкой в журнале, а сам файл в архив не попадает — его нужно переименовать
- Имя файла с обратным слешем (
\) в архив не попадает и роняет задание той же строкой — его нужно переименовать: разные сборкиtarчитают такое имя по-разному, и надёжно упаковать его нельзя - Внешний исполнитель получает артефакты зависимостей с сервера своим токеном и
только в пределах своего задания (его
needs, а без них — своего конвейера); состав называет сервер, каждый исход пишется в журнал задания
Неполный архив — провал задания. Если объявленный файл не удалось прочитать (нет прав доступа, недоступен каталог, в котором его искали), задание завершается с состоянием «провалено», а в его журнале появляется строка «Сбор артефактов: не удалось прочитать часть файлов — в архив попало не всё» с причиной от упаковщика. Правило одно у встроенного исполнителя и внешнего исполнителя. Собранная часть архива сохраняется — по ней видно, чего не хватает. Безобидные предупреждения задание не роняют и лишь отмечаются строкой в журнале: файл, который дописывался во время упаковки, и путь из
paths, которого в рабочей копии не оказалось (такой путь просто пропускается — как и маска, не совпавшая ни с чем). Если не нашлось ничего, в журнале задания появляется «Сбор артефактов: ни один из указанных путей не найден в рабочей копии», и задание остаётся успешным.
Отчёты (
artifacts: reports:) задание не роняют. Объявленного отчёта (junit,coverage_report,dotenv) может не оказаться — например, шаг упал раньше, чем успел его создать, — поэтому неудача чтения даёт лишь строку в журнале задания: «Отчёт JUnit ‘report.xml’ не прочитан: файла нет в рабочей копии задания» (или причину от ОС, если файл есть, но недоступен). Строка одна и та же у встроенного исполнителя и внешнего исполнителя: по ней видно, почему вкладка «Тесты» пуста, а переменныеdotenvне дошли до зависимых заданий. Файл$CI_OUTPUT— исключение: его путь не объявляют, файл создаёт сам скрипт, поэтому строку даёт только недоступный файл, а не его отсутствие.
expire-in: просроченные артефакты проверяются и автоматически удаляются не реже раза в час — то есть фактическое удаление может отстать отexpire-inна этот срок. Срок ставит сервер — одинаково для архива встроенного исполнителя и внешнего; еслиexpire-inне задан или значение не распознано, действует умолчание30d. Размер архива идёт в расход квотыmax_ci_artifactsи снимается с неё при удалении. Предел действует у ОБОИХ исполнителей: архив, не уложившийся в остаток квоты, не сохраняется, а задание проваливается — артефакты объявлены, и зависимые задания их ждут.
cache — кеширование между запусками
jobs:
build:
steps:
- run: cargo build --release
cache:
key: cargo-$CI_COMMIT_BRANCH
paths:
- target/
- ~/.cargo/registry/
policy: pull-push
key— ключ кеша (поддерживает подстановку$VAR)paths— каталоги для кеширования. В отличие отartifacts: paths:это именно пути, а не маски: кешируют каталоги целиком, и раскрывать здесь нечего. Путь с*или?поэтому в кеш не попадает вовсе — как несуществующий- Вся рабочая копия (
paths: ['.']) кешируется её содержимым: каждый файл и каталог корня — своей записью, каталоги по-прежнему целиком - Служебные записи исполнителя в архив кеша не попадают — то же правило и
тот же список, что у
artifacts:.git-credentials,.docker,.ci-output,.ci-action-images .gitв кеш не попадает и из кеша не восстанавливается — то же правило, что уartifacts- Имя с обратным слешем (
\) в кеш не попадает — то же правило, что уartifacts; в журнале задания об этом появляется строка, но задание неполный кеш не роняет policy:pull-push(по умолчанию) — читать и записыватьpull— только читать (для заданий, которые не должны обновлять кеш)push— только записывать
При восстановлении кеша права файлов переносятся из архива (биты rwx), чтобы сборочные скрипты cargo и установленные в кеше инструменты оставались исполняемыми. Специальные биты (setuid/setgid/sticky) и запись для прочих из архива не переносятся никогда. То же правило действует для артефактов зависимых заданий; владелец и группа файлам задания не назначаются из архива ни в каком случае.
Время изменения файлов (mtime) переносится из архива тоже — и у кеша, и у артефактов зависимых заданий. Поэтому инкрементальные сборщики (cargo, make, ninja) правильно видят, какие файлы из кеша устарели относительно исходников коммита, и пересобирают их.
Время из архива проверяется:
- значение из будущего по часам исполнителя не применяется — файл остаётся со временем распаковки;
- нулевое время (битый заголовок) не применяется;
- время старше часов исполнителя переносится как есть;
- каталогам время ставится последним проходом, после записи их содержимого;
- жёсткой ссылке время не ставится: она делит inode с целью, у которой время уже своё;
- символической ссылке время ставится самой ссылке, а не файлу, на который она указывает;
- если поставить время не удалось (файловая система его не хранит, распаковка идёт не от владельца), задание не падает — как и с неполным кешем; в журнале сервера или исполнителя появляется строка с числом таких записей.
Следствие, которое стоит знать при настройке кеша: файлы рабочей копии клон
кладёт заново каждый запуск, то есть со временем «сейчас». Поэтому крейты
самого репозитория пересобираются в любом случае, а кеш ускоряет то, что
приходит из архива со своим временем целиком, — зависимости (~/.cargo,
target/ под них, node_modules).
Кеш хранится на сервере, в каталоге данных CI (ci_data_path), и при
восстановлении распаковывается в рабочий каталог задания. Кеш общий для всех
конвейеров и веток репозитория: разделяет их только ключ, поэтому в него и
подставляют $CI_COMMIT_REF_SLUG. Кеш чужого репозитория недостижим даже по
совпадающему ключу.
Конвейер запроса из форка пишет кеш отдельно и подменить общий кеш
репозитория — тот, что получают сборки защищённых веток, — не может. Читать
общий кеш он по-прежнему может: сначала ищется его собственный, затем общий, —
поэтому сборки запросов из форка не начинаются с пустого кеша. От автора
рабочего процесса это ничего не требует: key и paths те же.
Кеш сохраняется только после успешного задания: кеш упавшей сборки испортил бы и следующие запуски.
Кеш входит в квоту и вытесняется по сроку. Суммарный размер архивов
считается в категории max_ci_cache владельца репозитория — отдельно от
артефактов. При исчерпанном пределе кеш не сохраняется, а задание остаётся
успешным — в его журнал уходит строка «Сохранение кеша: архив не сохранён на
сервере. Квота превышена…».
Прежний архив по тому же ключу при этом цел. Замена архива по существующему
ключу считается по ПРИРОСТУ размера, а не по полному размеру нового архива:
кеш, однажды заполнивший предел, продолжает обновляться, а замена, которая
занятое не увеличивает (новый архив того же размера или меньше), проходит даже
при исчерпанной квоте.
Архив, к которому не обращались дольше ci_cache_retention_days (по умолчанию
14 дней), снимается фоновым обслуживанием. Срок считается от последнего
ОБРАЩЕНИЯ — записи или выдачи заданию, — поэтому кеш обновляемой сборки не
вытесняется никогда, а ключ ветки, которой уже нет, уходит сам. Ноль в этой
настройке отключает вытеснение по сроку: расход тогда ограничивает только
квота.
В отличие от артефактов неполный кеш задание не роняет — он лишь ускоряет
следующий запуск. Но если упаковщик не смог прочитать часть файлов, в журнале
задания появляется строка «Сохранение кеша: не удалось прочитать часть
файлов…», объясняющая, почему следующая сборка пойдёт без части кеша. Пути
paths, которых в рабочей копии нет, просто пропускаются.
Директива работает одинаково у встроенного исполнителя и у внешнего исполнителя:
исполнитель получает ключ уже с подставленными переменными и обменивается
архивом с сервером, а каждый шаг пишет строку в журнал задания — восстановлен
кеш, не найден по ключу или сохранён. Исполнитель старее сервера может не знать
обмена кешем: тогда он выполняет cache: вхолостую, без ошибки в журнале.
Обновляйте исполнителей вместе с сервером.
environment — окружение развёртывания
jobs:
deploy:
environment:
name: production
url: https://app.example.com
steps:
- run: ./deploy.sh
Задание связывается с окружением: заданию достаются переменные этого окружения
(см. «Уровни переменных»), а список окружений с последним
состоянием доступен в настройках репозитория (Настройки → Окружения) и через API
GET /api/v1/repos/{owner}/{name}/ci/environments.
Журнал задания
Журнал задания устроен одинаково у встроенного и у внешнего исполнителя: шапка, разделы и итоговая строка у обоих те же, а строка исполнителя в шапке называет того, кто взял задание: у встроенного — пометка «встроенный исполнитель», у внешнего — имя раннера.
Журнал пишется на языке установки (настройка language, см. руководство по
установке), а не на языке читателя. Внешний исполнитель своей настройки языка
не имеет — язык приходит ему от сервера вместе с заданием. Язык выбирается ОДИН
раз на задание: смена настройки посреди сборки не даёт журнала на двух языках,
а отметка повтора и строка о снятии задания сервером идут на том же языке, что
и остальной журнал. Ручной повтор начинает журнал заново — на языке установки в
момент повтора. Вывод самих команд скрипта не переводится: его пишут программы
задания.
При языке ru журнал выглядит так:
Выполняется на GitRiver CI
Исполнитель: встроенный исполнитель
Конвейер: #01a097ac · Сборка · refs/heads/main
Коммит: 7f081dca — починить разбор ключа
Подготовка окружения
Образ: rust:1.82
Сервисы: postgres:15
Получение исходного кода
$ git clone …
Восстановление артефактов
Артефакты задания build восстановлены
Восстановление кеша
Ключ: cargo-linux-9f2c
Кеш восстановлен
Запуск сервисов
Сервис postgres:15 доступен по имени postgres
Выполнение скрипта
…вывод скрипта…
After script
…вывод after_script…
Сохранение артефактов
Сбор артефактов: архив сохранён, 12345 байт
Сохранение кеша
Ключ: cargo-linux-9f2c
Сохранение кеша: архив сохранён, 98765 байт
Задание выполнено успешно
Раздел появляется только тогда, когда ему есть что сказать: без image: и
сервисов «Подготовки окружения» не будет, без cache: — разделов кеша.
Последняя строка называет ИТОГ, и он же определяет значок задания в интерфейсе:
| Строка журнала | Состояние |
|---|---|
Задание выполнено успешно |
успех |
Задание завершилось с ошибкой (exit N) |
провал |
Задание провалено: артефакты собраны не полностью |
провал |
Задание снято по истечении срока |
провал |
Задание отменено |
отменено |
Задание, которое списал сам сервер (исполнитель замолчал либо не взял задание за
ci_runner_max_wait_secs), тоже объясняется строкой в журнале, а не пустотой.
outputs — передача данных между заданиями
Задания могут передавать данные через файл $CI_OUTPUT:
jobs:
prepare:
steps:
- run: |
VERSION=$(cat VERSION)
echo "version=$VERSION" >> $CI_OUTPUT
echo "should_deploy=true" >> $CI_OUTPUT
deploy:
needs: [prepare]
if: $NEEDS_PREPARE_OUTPUTS_SHOULD_DEPLOY == "true"
steps:
- run: echo "Deploying version $NEEDS_PREPARE_OUTPUTS_VERSION"
Как это работает:
- Задание записывает пары
key=valueв файл$CI_OUTPUT(по одной на строку) - После завершения задания выходные данные сохраняются на сервере
- Зависимое задание получает значения через переменные
$NEEDS_<JOB>_OUTPUTS_<KEY>- Имя задания и ключ приводятся к верхнему регистру
- Дефисы в именах заменяются на
_
- Переменные подставляет сервер — одинаково для заданий встроенного исполнителя
и внешних исполнителей, и до решения
if:задания: условие по выходным данным зависимости (пример выше) действует на обоих исполнителях. Так же наследуются переменные изartifacts.reports.dotenvзависимости.
Выходные данные собирают оба исполнителя: внешний присылает содержимое своего
$CI_OUTPUT серверу, а разбирает его всегда сервер — правила у обоих одни.
Исполнитель старее сервера переменную $CI_OUTPUT может не задавать и файла не
присылать: зависимые задания получат пустую подстановку, поэтому обновляйте
исполнителей вместе с сервером.
Пример с несколькими зависимостями:
jobs:
detect:
steps:
- run: |
echo "backend=true" >> $CI_OUTPUT
echo "frontend=false" >> $CI_OUTPUT
build:
steps:
- run: |
echo "artifact_path=dist/app.tar.gz" >> $CI_OUTPUT
deploy:
needs: [detect, build]
if: $NEEDS_DETECT_OUTPUTS_BACKEND == "true"
steps:
- run: echo "Artifact: $NEEDS_BUILD_OUTPUTS_ARTIFACT_PATH"
Переменные окружения
Приоритет (от низшего к высшему)
- Переменные уровней установки, группы, репозитория и окружения (экраны «Переменные CI/CD»)
- Глобальные
env:файла рабочего процесса env:на уровне задания- Ввод запуска:
INPUT_*изworkflow_dispatchиCI_MERGE_REQUEST_* - Предопределённые переменные
CI_* env:на уровне шага — применяется внутри скрипта и действует для своего шага
Файл рабочего процесса сильнее настроек, а не наоборот: переменная,
заданная в настройках, не может подменить то, что задано в файле, — в том числе
значения матрицы и текст скрипта шага. Имена пространств CI_, GITRIVER_,
MATRIX_, NEEDS_, __GR_ пользователю закрыты — задать такую переменную
нельзя ни на одном уровне.
Ввод запуска сильнее файла: номер запроса на слияние и параметры ручного
запуска задаёт сам запуск, и env: их не переопределяет.
Отсюда практическое правило: если значение задаётся и в настройках, и в
env:, до задания дойдёт значение из env:. Нужно, чтобы побеждала настройка —
уберите имя из env: файла.
Предопределённые переменные
| Переменная | Описание | Пример |
|---|---|---|
CI |
Всегда true |
true |
CI_PIPELINE_ID |
Идентификатор конвейера | 550e8400-... |
CI_PIPELINE_SOURCE |
Источник запуска (web — ручной запуск) |
push, pull_request, schedule, web |
CI_COMMIT_SHA |
Полный SHA коммита | a1b2c3d4... |
CI_COMMIT_SHORT_SHA |
Короткий SHA | a1b2c3d |
CI_COMMIT_BRANCH |
Ветка (для тега не задана) | main |
CI_COMMIT_TAG |
Тег (для ветки не задана) | v1.0.0 |
CI_COMMIT_REF_NAME |
Полное имя ссылки — ветки или тега | refs/heads/main или refs/tags/v1.0.0 |
CI_COMMIT_REF_SLUG |
Имя ветки или тега, пригодное для адресов и имён файлов | feature-my-branch |
CI_COMMIT_MESSAGE |
Сообщение коммита | fix: bug #123 |
CI_SERVER_URL |
Адрес установки | https://git.example.com |
CI_REPOSITORY_NAME |
Имя репозитория | myapp |
CI_REPOSITORY_OWNER |
Владелец | myteam |
CI_REPOSITORY_FULL_NAME |
Владелец и имя | myteam/myapp |
CI_REPOSITORY_URL |
Адрес репозитория | https://git.example.com/myteam/myapp |
CI_JOB_ID |
Идентификатор текущего задания | 550e8400-... |
CI_JOB_NAME |
Имя задания | build |
CI_JOB_TOKEN |
Токен задания — см. «Что может CI_JOB_TOKEN» |
eyJ... |
CI_REGISTRY |
Адрес реестра контейнеров | registry.example.com |
CI_REGISTRY_IMAGE |
Имя образа репозитория в реестре | registry.example.com/myteam/myapp |
CI_REGISTRY_USER |
Имя пользователя для реестра | gitriver-ci |
CI_REGISTRY_PASSWORD |
Токен для реестра | eyJ... |
CI_WORKSPACE |
Корень рабочей копии | /builds |
CI_PROJECT_DIR |
То же, что CI_WORKSPACE |
/builds |
CI_WORKSPACE_HOST |
Та же рабочая копия, каким её путь видит демон docker, — для docker run -v из скрипта задания |
/var/lib/gitriver/…/repo |
CI_OUTPUT |
Файл выходных данных в корне рабочей копии | /builds/.ci-output |
CI_CGROUP_PARENT |
Срез ресурсов CI: cgroup, в которую исполнитель кладёт все контейнеры задания. Пусто — среза нет (см. «Срез ресурсов CI и сборка образов») | gitriver.slice |
DOCKER_CONFIG |
Каталог учётных данных docker — свой у каждого задания | /builds/.docker |
Пути рабочей копии зависят от того, где идёт задание, и жёстко их писать
нельзя. У задания с image: рабочая копия смонтирована в контейнер, и
$CI_WORKSPACE указывает внутрь него; у задания без image: — на каталог
хоста. Сама точка монтирования у двух исполнителей разная (/builds у
встроенного, /workspace у внешнего исполнителя), поэтому путь и отдаётся
переменной: рабочий процесс, написанный через $CI_PROJECT_DIR, идёт у обоих
одинаково, а написанный через /builds — только у одного.
Своему контейнеру рабочую копию отдавайте по $CI_WORKSPACE_HOST, а не по
$CI_WORKSPACE. Сокет демона заданиям проброшен, и поднять свой контейнер
скрипт может — но демон ищет пути в файловой системе ХОСТА. Когда сервер
GitRiver сам работает в контейнере, $CI_WORKSPACE указывает внутрь его
пространства имён: такого пути на хосте нет, docker молча заводит по нему пустой
каталог, и контейнер получает пустой рабочий каталог вместо репозитория — отказ
приходит уже изнутри, строкой вида «нет такого файла».
- name: Упаковка сторонним образом
run: |
docker run --rm -v "$CI_WORKSPACE_HOST:/work" -w /work \
registry.example.com/tools/nfpm:latest pkg --config packaging/nfpm.yaml --packager deb
Вне контейнера (внешний исполнитель на машине, сервер из пакета) переменная
равна $CI_WORKSPACE — писать через неё можно всегда.
В $DOCKER_CONFIG исполнитель заранее делает docker login в реестр
контейнеров установки за автора репозитория: docker push $CI_REGISTRY_IMAGE
работает без ручного входа. Каталог свой у каждого задания и лежит в его рабочей
копии — учётные данные не остаются на машине после задания и не попадают ни в
архив артефактов, ни в кеш, ни в git status.
Для workflow_dispatch
Параметры запуска доступны как $INPUT_<NAME> (в верхнем регистре):
on:
workflow_dispatch:
inputs:
target:
type: string
# В steps: $INPUT_TARGET
Для matrix
Значения матрицы доступны как $MATRIX_<KEY> (в верхнем регистре):
strategy:
matrix:
node: [18, 20]
# В steps: $MATRIX_NODE
Для pull_request
| Переменная | Описание |
|---|---|
CI_MERGE_REQUEST_IID |
Номер запроса на слияние |
CI_MERGE_REQUEST_SOURCE_BRANCH_NAME |
Исходная ветка |
CI_MERGE_REQUEST_TARGET_BRANCH_NAME |
Целевая ветка |
CI_MERGE_REQUEST_TITLE |
Заголовок запроса |
Для outputs задания
Выходные данные зависимостей доступны как $NEEDS_<JOB>_OUTPUTS_<KEY> (в верхнем регистре):
# Если задание "build" записало "version=1.0" в $CI_OUTPUT,
# то в зависимом задании доступна переменная:
$NEEDS_BUILD_OUTPUTS_VERSION # → "1.0"
Переменные репозитория
Настраиваются в разделе Настройки → Переменные CI/CD.
| Поле | Описание |
|---|---|
| Name | Имя (A-Z_0-9, без префиксов CI_, GITRIVER_, MATRIX_, NEEDS_, __GR_ — они заняты предопределёнными переменными) |
| Value | Значение |
| Masked | Скрывать в журналах (значение заменяется на [MASKED]) |
| Protected | Выдавать только конвейерам защищённых веток. Конвейер по тегу такую переменную НЕ получает: защиты тегов в GitRiver нет, и признак «защищённый ref» для тега всегда ложен |
Секретные переменные (Masked): значение вырезается из каждой строки журнала
задания и заменяется на [MASKED]. Маскируются и распространённые кодировки
значения (base64, hex, кодирование для URL) — на случай, если скрипт вывел
секрет закодированным.
env: из файла рабочего процесса перезаписывает переменную репозитория — см.
«Приоритет» выше.
Значение секретной переменной — одна строка длиной не меньше 8 символов.
Короткое значение маскирование не скрывает (оно угадывается), зато портит
журнал: секрет 1 превратил бы в [MASKED] каждую единицу в выводе.
Многострочное не находится построчной маскировкой и утекло бы целиком.
Уровни переменных
Кроме репозитория переменные задаются ещё на трёх уровнях. Каждый следующий уровень перезаписывает предыдущий по имени:
| Уровень | Где задаётся | Кому достаётся |
|---|---|---|
| Установка | Администрирование → Переменные CI/CD (администратор) | всем конвейерам установки |
| Группа | Страница группы → Переменные CI/CD (сопровождающий и выше) | репозиториям группы и её подгрупп |
| Репозиторий | Настройки → Переменные CI/CD | конвейерам репозитория |
| Окружение | Настройки → Окружения → строка окружения → Переменные CI/CD | только заданиям с этим environment: |
Уровень окружения адресуется заданием, а не конвейером: переменные окружения
production получит задание с environment: production, а соседнее задание того
же конвейера без environment: — нет. Задание с окружением, для которого
переменных не задано, получает набор остальных уровней без изменений.
deploy:
environment: production # получит переменные окружения production
steps:
- run: ./deploy.sh
build:
steps: # окружения нет — переменных production не увидит
- run: make
Правила Masked и Protected, фильтр доверия (запрос из форка, защищённый ref) и
маскирование журналов действуют на всех уровнях одинаково.
Кому выдаются секреты
Набор переменных задания фильтруется контекстом доверия, который фиксируется при создании конвейера и дальше не пересчитывается:
| Конвейер | Обычные | Masked | Protected | CI_JOB_TOKEN |
|---|---|---|---|---|
| Защищённая ветка | да | да | да | да |
| Обычная ветка | да | да | нет | да |
| Тег | да | да | нет | да |
| Запрос из форка | да | нет | нет | нет |
Строка «Тег» — не оплошность таблицы: защита в GitRiver задаётся только веткам,
поэтому ни один тег защищённым не считается. Отсюда правило для выпуска:
переменные, нужные конвейеру по тегу (учётные данные реестра, токен публикации),
пометить Protected нельзя — они не придут, и задание выпуска упадёт на проверке
своих секретов. Маскировать их (Masked) при этом нужно: маскирование от тега не
зависит.
CI_JOB_TOKEN (и равный ему CI_REGISTRY_PASSWORD) — это действующий доступ:
он пишет в реестр контейнеров репозитория и используется как учётная запись git
задания. Скрипт задания видит его в своём окружении, поэтому конвейеру запроса из
форка он не выдаётся — иначе автор запроса получил бы права записи. Задания
форка при этом выполняются: собрать и проверить код им ничто не мешает.
Правило одинаково для встроенного и внешнего исполнителя. Повтор задания, ручной запуск и перезапуск конвейера используют доверие исходного конвейера — перезапуск задания запроса из форка секретов родительского репозитория не получит.
Разрешение на запуск чужого кода
Открыть запрос из форка может всякий, кто пишет в свой форк, — прав на базовый репозиторий для этого не требуется. Но проверки занимают исполнителей, поэтому у кода со стороны есть вторая преграда, помимо секретов:
Конвейер запроса, автор которого не имеет права записи в базовый репозиторий, заводится, но не запускается. Он виден в списке конвейеров и на странице запроса с пометкой «ждёт разрешения»; задания стоят. Запуск разрешает любой участник с правом записи — кнопкой «Разрешить запуск» на странице конвейера или запросом:
POST /api/v1/repos/{owner}/{name}/pipelines/{id}/approve
Разрешение одноразовое и относится к одному конвейеру: следующая отправка в ветку запроса заведёт новый, и его тоже придётся разрешить. Доверие при разрешении не пересчитывается — оно взято с конвейера, поэтому секреты закрыты и после нажатия, даже если автор к тому времени получил права.
Запрос из форка, открытый участником базового репозитория, разрешения не ждёт: преграда стоит на чужом коде, а не на форке как таковом. Секреты такому конвейеру всё равно не выдаются — код приходит из другого репозитория.
Что может CI_JOB_TOKEN
Токен действует только в репозитории своего конвейера — и в git, и в
реестре контейнеров. Задание читает и пишет свой репозиторий; к любому другому
репозиторию этой установки, включая публичный, доступа нет (403). Область одна и та
же для встроенного исполнителя и внешнего исполнителя.
Отправка по токену подчиняется правилам защиты веток наравне с обычной: запрет отправки и удаления, запрет принудительной отправки и требование подписи коммитов действуют. Не действует только список допуска на отправку — токен не привязан к пользователю. Значит ветку, в которую задание не должно писать, закрывают правилом защиты, а не расчётом на область токена.
Срок жизни токена задаёт ci_job_token_ttl_secs (по умолчанию 8 часов). До
истечения он принимается и после завершения задания, поэтому долгий срок на
установке с недоверенными участниками лучше сократить.
CI_JOB_TOKEN и CI_REGISTRY_PASSWORD — предопределённые имена: значение под
ними всегда выдаёт сервер. Задать их переменной репозитория или через env: в
рабочем процессе нельзя — такое значение снимается, а не подставляется.
Секреты на внешнем исполнителе
Внешний исполнитель получает секреты вместе с заданием, когда его забирает, и вместе с ними — список имён секретных переменных. Исполнитель вырезает их значения из журналов до отправки на сервер, а сервер вырезает их повторно при приёме журнала.
Исполнитель ограничен своей областью (установка, группа или репозиторий), поэтому секреты репозитория уходят только исполнителю, которому этот репозиторий доступен. Отсюда следствие для администратора: исполнитель области «установка» забирает задания любого репозитория и получает их секреты — регистрируйте таких исполнителей только на машинах, которым доверяете; для сторонних машин заводите исполнителя репозитория или группы. Исполнитель группы с признаком «обслуживает подгруппы» забирает задания всего её поддерева — а значит, получает и секреты подгрупп, и выполняет их сборки на своей машине. Признак включает владелец машины; по умолчанию он выключен.
CI_JOB_TOKEN маскируется в журналах внешнего исполнителя так же, как и
пользовательские секреты.
Клонирование репозитория внешним исполнителем
Встроенный исполнитель берёт код с диска сервера, а внешний исполнитель клонирует репозиторий по HTTP, поэтому сервер выдаёт ему вместе с заданием отдельные учётные данные только для клонирования:
- только чтение и только репозиторий своего задания — отправка и запись в
реестр контейнеров этим токеном запрещены (
403), в отличие отCI_JOB_TOKEN; - выдаются и заданиям запроса из форка: без них приватный репозиторий не склонировать, а прав сверх чтения того самого кода, который задание и так выполняет, токен не даёт;
- не попадают в окружение скрипта задания, в
.git/configрабочей копии и в список аргументов процессов. Значение маскируется в журналах.
Срок жизни — 1 час: клонирование начинается сразу после получения задания.
Задание выполняется на коммите своего конвейера. Клон поверхностный
(--depth=1), и если ветка успела уйти вперёд, пока задание стояло в очереди,
нужный коммит дозагружается по SHA. Если получить его не удалось, задание
завершается ошибкой — на чужом коде оно не выполняется.
Git и LFS в рабочей копии внешнего исполнителя
Рабочая копия готовится так же, как у встроенного исполнителя.
git push и git fetch из скрипта задания работают без настройки. Исполнитель
прописывает заданию учётные данные git из CI_JOB_TOKEN — собирать адрес вида
https://пользователь:$CI_JOB_TOKEN@сервер/... вручную не нужно, git push origin достаточно. Токен лежит в отдельном файле учётных данных, а не в
адресе удалённого репозитория, поэтому в .git/config и в выводе
git remote -v он не появляется. Задание запроса из форка CI_JOB_TOKEN не получает
(см. таблицу выше) — отправка из него невозможна.
LFS-файлы приходят содержимым, а не указателями. Исполнитель настраивает
адрес LFS этой установки и токен к нему до получения файлов рабочей копии
(checkout) — иначе задание собрало бы файлы-указатели вместо содержимого без
единой ошибки в журнале. Токен доступен только на чтение LFS-объектов
репозитория своего задания и маскируется в журналах. В отличие от
CI_JOB_TOKEN он хранится в .git/config (lfs.url и заголовок
Authorization: Basic) — потому .git и не попадает в архивы артефактов и
кеша, даже под paths: ['.']. То же и у встроенного исполнителя. После
checkout исполнитель вызывает git lfs pull; если git-lfs на машине
исполнителя не установлен, задание продолжается, а в журнал уходит
предупреждение. Предел этой операции — --lfs-timeout
(GITRIVER_RUNNER_LFS_TIMEOUT, по умолчанию 600 секунд).
Пути рабочей копии и реестр контейнеров — те же, что у встроенного
исполнителя. Исполнитель отдаёт скрипту $CI_WORKSPACE, $CI_PROJECT_DIR,
$CI_WORKSPACE_HOST, $CI_OUTPUT и $DOCKER_CONFIG и заранее входит в реестр
установки, поэтому рабочий процесс, написанный под встроенный исполнитель,
переносится на внешнего исполнителя без правок. Каталог учётных данных docker —
свой у каждого задания, внутри его рабочей копии: docker login, сделанный
самим скриптом, тоже не остаётся в настройках пользователя исполнителя и не
достаётся следующему заданию.
Установка без встроенного исполнителя
Настройка ci_local_executor_enabled = false (GITRIVER_CI_LOCAL_EXECUTOR)
выключает встроенный исполнитель: сервер задания не выполняет, Docker ему не
нужен, вся очередь достаётся внешним исполнителям. Как поднять такую установку,
сказано в руководстве по установке.
Что меняется для авторов рабочих процессов:
- Метки не рассматриваются. Исполнитель нужен любому заданию, включая
runs-on: defaultи задание вовсе безruns-on. - Исполнителю нужна метка
default. Задание безruns-onзаберёт любой исполнитель, аruns-on: default— только исполнитель, у которого меткаdefaultобъявлена. Если такой метки нет ни у кого, задания сruns-on: defaultждут исполнителя доci_runner_max_wait_secs(по умолчанию час) и завершаются ошибкой. Панель администратора предупреждает об этом на странице исполнителей, мастер установки — при первом запуске. - Запасного варианта «выполнить на сервере» нет. Если исполнители не настроены или очередь недоступна, задание сразу завершается ошибкой, а причина пишется в его журнал.
cache:,artifacts:иneeds:работают как обычно. Кеш между запусками внешний исполнитель восстанавливает и сохраняет через сервер, артефакты зависимостей получает оттуда же; хранится и то, и другое всё так же на сервере вci_data_path(см.cache). Единственное условие — исполнитель не старее сервера: исполнитель, не знающий обмена кешем, выполняетcache:вхолостую и молча, а не знающий обмена артефактами — получает в журнал401и собирается без файлов зависимостей.
Несколько файлов рабочего процесса
.gitriver/
workflows/
ci.yml # Тесты на каждую отправку
release.yml # Выпуск при создании тега
nightly.yml # Ночная сборка по расписанию
deploy.yml # Развёртывание (ручной запуск)
Каждый файл — самостоятельный рабочий процесс со своими событиями запуска. При отправке GitRiver проверяет все файлы и запускает те, чьи on: совпадают с событием.
Отклонённый файл рабочего процесса
Файл, который не удалось разобрать или собрать в конвейер, из запуска выпадает: конвейер по нему не создаётся. Остальные файлы это не затрагивает — отказ всегда пофайловый.
Причину автор видит на коммите: платформа ставит проверку на коммите
в состоянии error с контекстом gitriver-ci/<имя файла> и текстом причины.
Она видна во всплывающей подсказке у коммита и в проверках запроса на слияние.
Проверка привязана к коммиту и снимается сама, как только тот же файл на том же коммите принят: например, администратор расширил список разрешённых доменов, и следующий запуск (по расписанию или вручную) прошёл приём. Исправление в новом коммите тем более даёт чистое состояние — у нового коммита свои проверки.
Отказ попадает в общее состояние коммита, поэтому правило защиты ветки «CI должен пройти» такой файл не пропустит: запрос на слияние с неразобранным рабочим процессом остановится на этой проверке, а не сольётся так, словно проверок не было вовсе.
Причины бывают четырёх видов:
| Что не так | Пример текста |
|---|---|
| YAML не разбирается | неверный YAML рабочего процесса: … |
| файл разобран, но содержит ошибку | циклическая зависимость в 'needs': задание 'build' |
| в файле ключ, которого платформа не знает | задание 'build': неизвестный ключ 'runs_on'; вероятно, имелся в виду 'runs-on' |
| действие запрещено списком доменов | внешний action 'evil.test/o/r' запрещён политикой: домен/владелец 'evil.test' отсутствует в списке разрешённых (allowed_external_action_domains) |
След одинаков на всех путях запуска — отправка, запрос на слияние, расписание, ручной запуск и перезапуск. Ручной запуск и перезапуск, кроме состояния, возвращают ту же причину прямо в ответе API (код 400): за ней не нужно идти на страницу коммита.
Длинные причины в описании проверки сокращаются до 200 символов — полный текст остаётся в журнале сервера.
Примеры
Проект на Rust — сборка и выпуск
# .gitriver/workflows/ci.yml
name: CI
on:
push:
branches: [main, develop]
paths-ignore: ['docs/**', '*.md']
pull_request:
env:
CARGO_TERM_COLOR: always
jobs:
check:
image: rust:1.82
steps:
- run: cargo check --all-targets
cache:
key: cargo-check
paths: [target/, ~/.cargo/registry/]
test:
needs: [check]
image: rust:1.82
retry: 2
services:
- image: postgres:16
alias: db
env:
POSTGRES_DB: test
POSTGRES_PASSWORD: test
env:
DATABASE_URL: postgres://postgres:test@db:5432/test
steps:
- run: cargo test --all
cache:
key: cargo-test
paths: [target/]
clippy:
image: rust:1.82
allow-failure: true
steps:
- run: cargo clippy -- -D warnings
cache:
key: cargo-clippy
paths: [target/]
policy: pull
fmt:
image: rust:1.82
steps:
- run: cargo fmt -- --check
# .gitriver/workflows/release.yml
name: Release
on:
push:
tags: ['v*']
jobs:
build:
image: rust:1.82
steps:
- run: cargo build --release
artifacts:
paths: [target/release/myapp]
docker:
needs: [build]
steps:
- run: |
docker build -t $CI_REGISTRY/$CI_REPOSITORY_OWNER/$CI_REPOSITORY_NAME:$CI_COMMIT_TAG .
docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
docker push $CI_REGISTRY/$CI_REPOSITORY_OWNER/$CI_REPOSITORY_NAME:$CI_COMMIT_TAG
Node.js — тесты матрицей
name: Node.js CI
on:
push:
branches: [main]
pull_request:
jobs:
test:
strategy:
matrix:
node: [18, 20, 22]
fail-fast: false
image: node:$MATRIX_NODE
retry: 1
steps:
- run: npm ci
- run: npm test
cache:
key: npm-$MATRIX_NODE
paths: [node_modules/]
Развёртывание с ручным подтверждением
name: Deploy
on:
workflow_dispatch:
inputs:
environment:
description: 'Окружение'
type: choice
options: [staging, production]
required: true
skip_tests:
description: 'Пропустить тесты'
type: boolean
default: false
concurrency:
group: deploy-$INPUT_ENVIRONMENT
cancel-in-progress: false
jobs:
test:
if: $INPUT_SKIP_TESTS != "true"
image: rust:1.82
steps:
- run: cargo test
# Без тестов развёртывание идёт сразу; с тестами — только после их успеха
deploy:
needs:
- job: test
optional: true
environment:
name: $INPUT_ENVIRONMENT
url: https://$INPUT_ENVIRONMENT.example.com
steps:
- run: ./scripts/deploy.sh $INPUT_ENVIRONMENT
Монорепозиторий — условные задания через outputs
name: Monorepo CI
on:
push:
branches: [main]
pull_request:
jobs:
detect-changes:
steps:
- run: |
# Проверяем, какие пути изменились, через git diff
if git diff --name-only HEAD~1 | grep -q '^backend/'; then
echo "backend=true" >> $CI_OUTPUT
else
echo "backend=false" >> $CI_OUTPUT
fi
if git diff --name-only HEAD~1 | grep -q '^frontend/'; then
echo "frontend=true" >> $CI_OUTPUT
else
echo "frontend=false" >> $CI_OUTPUT
fi
backend-test:
needs: [detect-changes]
if: $NEEDS_DETECT_CHANGES_OUTPUTS_BACKEND == "true"
image: rust:1.82
steps:
- run: cd backend && cargo test
frontend-test:
needs: [detect-changes]
if: $NEEDS_DETECT_CHANGES_OUTPUTS_FRONTEND == "true"
image: node:20
steps:
- run: cd frontend && npm test
Конвейер с повтором и уведомлением об отказе
name: CI with notifications
on:
push:
branches: [main]
jobs:
test:
image: rust:1.82
interruptible: true
retry:
max: 2
when: [script_failure]
steps:
- run: cargo test
notify:
needs: [test]
if: failure()
allow-failure: true
steps:
- run: |
curl -X POST "$SLACK_WEBHOOK" \
-H "Content-Type: application/json" \
-d "{\"text\": \"CI failed on $CI_COMMIT_BRANCH ($CI_COMMIT_SHORT_SHA)\"}"
Перенос файлов из GitLab CI
Рабочие процессы GitRiver CI берутся только из каталога .gitriver/workflows/.
Файлы .gitlab-ci.yml и .gitriver-ci.yml сервер не читает: их нужно
переписать в формат этого руководства — по таблице ниже.
Соответствие форматов
| GitLab CI | GitRiver CI |
|---|---|
stages: + stage: |
needs: (явные зависимости) |
script: |
steps: [{run: ...}] |
before_script: |
Первый шаг в списке |
after_script: |
Последний шаг с if: always() (идёт в контейнере image:, как в GitLab) |
when: on_failure |
if: failure() — у задания или у шага |
variables: |
env: |
rules: [{if:}] |
if: на уровне задания |
rules: [{changes:}] |
on: push: paths: |
only/except |
on: + if: |
extends: |
Нет (якоря YAML, если нужно) |
include: |
Отдельные файлы рабочего процесса |
when: manual |
workflow_dispatch или if: |
retry: |
retry: (сохранён) |
allow_failure: |
allow-failure: |
interruptible: |
interruptible: или concurrency: cancel-in-progress |
services: |
services: (сохранён) |
artifacts: |
artifacts: (сохранён) |
cache: |
cache: (сохранён) |
environment: |
environment: (сохранён) |
Пределы ресурсов задания
Задания встроенного исполнителя — с image: и без него — выполняются под
одинаковыми пределами. Значения задаёт администратор установки (см.
руководство по установке); умолчания такие:
| Ресурс | Значение | Задание с image: |
Задание без image: |
|---|---|---|---|
| Память | 2g (ci_docker_memory) |
docker --memory |
memory.max cgroup задания |
| Своп | столько же, сколько памяти | умолчание Docker (--memory-swap = 2×память) |
memory.swap.max cgroup задания |
| Процессы и потоки | 512 (ci_job_pids_limit) |
docker --pids-limit |
pids.max cgroup задания |
| ЦП | 2 (ci_docker_cpus) |
docker --cpus |
не ограничивается |
| Открытые файлы | 1024 |
предел образа | ulimit -n скрипта |
| Размер журнала | 100 МБ | — | — |
| Время выполнения | 1 час, максимум 6 часов (timeout) |
— | — |
Предел памяти считает занятую память, а не адресное пространство. Среды выполнения, резервирующие десятки гигабайт адресов (JVM, Go, Rust с jemalloc, санитайзеры), под ним работают нормально: значение имеет только то, что задание действительно заняло.
Предел числа процессов принадлежит заданию. Он не зависит ни от нагрузки на сервер, ни от числа одновременно выполняемых заданий: каждое считает только свои процессы и потоки.
Исчерпав предел, задание падает, а причина пишется в его журнал отдельной строкой — со значением предела и именем настройки, которой он поднимается:
Исчерпан предел числа процессов задания: pids.max=512, отклонённых запусков — 12.
Предел поднимается настройкой ci_job_pids_limit.
Срез объясняет себя в журнале задания. Предел среза стоит не на задании:
упершись в него, задание получает Killed и код 137 — от собственной ошибки
сборки это не отличить. Поэтому исполнитель называет причину отдельной строкой:
Срез ресурсов CI исчерпал предел памяти: ядро завершило процессов — 1.
Срез общий для всех заданий CI, поэтому завершено могло быть и соседнее задание.
Предел поднимается командой: systemctl set-property gitriver.slice MemoryMax=…
Задание ждало ЦП 12 с: срез ресурсов CI упирался в свою квоту. Срез общий для
всех заданий CI — ждать могли и соседние. Квота поднимается командой:
systemctl set-property gitriver.slice CPUQuota=…
Обе строки считаются ЗА ВРЕМЯ задания, а не за всю жизнь среза, и обе говорят, что срез общий: причина может быть не в самом задании, а в занятости машины.
Заданию без image: пределы накладывает cgroup v2. Если серверу она недоступна
(cgroup v1, нет делегирования), задание выполняется без пределов памяти и
процессов — сервер сообщает об этом в свой журнал при первом задании. Пределы
задания с image: от этого не зависят: их накладывает Docker.
Задания внешних исполнителей этими настройками не ограничиваются — ресурсы задаёт машина исполнителя.
Срез ресурсов CI и сборка образов
Пределы из таблицы выше принадлежат ОДНОМУ заданию — и потому считают не весь
его расход. Шаг docker build внутри задания исполняет не контейнер задания, а
демон BuildKit: отдельный контейнер, который демон docker создаёт рядом с
контейнером задания, а не внутри него. Ни --memory, ни --cpus задания на
него не распространяются.
Поэтому поверх пределов задания стоит срез ресурсов CI — общая cgroup,
в которую попадает всё, что запускает сам исполнитель: контейнер задания,
after_script, сервисы services:, контейнеры действий (uses:), служебные
контейнеры уборки рабочей копии и сам исполнитель вместе с заданиями без
image:. Сборщик образов BuildKit в срез не попадает — о нём ниже. Имя среза
задание видит в переменной CI_CGROUP_PARENT; чем срез ограничен, задаёт
администратор установки (см. руководство по установке).
Своему сборщику образов задавайте пределы сами. Сборщик, который заводит
рабочий процесс, в срез не попадает и попасть не может: его создаёт демон
docker по просьбе скрипта, рядом с контейнером задания. Опция --driver-opt cgroup-parent=… у docker buildx create проверку проходит, но до контейнера
сборщика не доходит — он остаётся в system.slice. Работают пределы САМОГО
сборщика:
- run: |
docker buildx create --name builder-$CI_JOB_ID --use \
--driver-opt memory=6g --driver-opt cpu-quota=300000
docker buildx build --push -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA" .
cpu-quota задаётся в микросекундах на период в 100 000 мкс: 300000 — три
ядра. Без этих двух опций сборка не ограничена ничем. Сборщик за автора
рабочего процесса платформа не заводит.
Шаг docker build без buildx (сборщик по умолчанию) исполняет сам демон
docker, и его шаги ложатся в system.slice — ни в срез, ни под пределы
задания, ни даже под пределы службы docker: потомком docker.service они не
являются. Ограничить их можно только умолчанием самого демона — ключ
cgroup-parent в /etc/docker/daemon.json, — и оно подействует на ВСЕ
контейнеры машины, а не только на CI.
Поэтому сборкам образов надёжнее давать buildx со своим сборщиком и его
пределами — или выносить их на внешний исполнитель отдельной машины.
Переменная CI_CGROUP_PARENT остаётся полезной там, где контейнер поднимает
сам скрипт: docker run --cgroup-parent "$CI_CGROUP_PARENT" … кладёт его в
общий срез (действия uses: платформа запускает именно так).
Ограничения
- Максимум 20 файлов рабочего процесса на репозиторий
- Максимум 50 заданий на рабочий процесс
- Максимум 100 шагов на задание
- Максимум 10 уровней вложенности
needs - Матрица: максимум 256 комбинаций
- Срок задания: по умолчанию 1 час, максимум 6 часов
- Расписание: минимум 15 минут между запусками
- Размер файла рабочего процесса: максимум 1 МБ
paths/paths-ignore: максимум 100 масок
Интерфейс
Вкладка CI/CD в репозитории
- Список прогонов конвейера — с фильтрами по состоянию и источнику
- Каждый прогон показывает: состояние, ref (ветка/тег), коммит, источник, время, длительность
- Постраничная навигация
- Кнопка «Запустить конвейер»
Страница прогона
- Задания сгруппированы по этапам
- Состояние каждого задания и длительность (обновление на ходу для идущих)
- Задания матрицы группируются:
test (node: 18),test (node: 20), … - Нажатие на задание открывает его журнал в реальном времени
- Кнопки: «Отменить», «Перезапустить», «Перезапустить ошибки», «Запустить» (для ручных заданий), «Перезапустить» у отдельного задания
- Граф зависимостей заданий (переключатель «Этапы / DAG» при наличии
needs) - Состояния конвейера обновляются на ходу
Ручной запуск
- Кнопка «Запустить» на вкладке CI/CD
- Выбор рабочего процесса и заполнение параметров
inputs(поля по типу: текст, флажок, список, число) - Свои переменные окружения
- Запуск и переход к прогону
Значки
https://git.example.com/owner/repo/badge.svg
https://git.example.com/owner/repo/badge.svg?branch=develop
SVG-значок в стиле shields.io с текущим состоянием последнего конвейера.
Состояние реализации
Полностью реализовано
- Автозапуск при отправке по условиям
on:каждого файла рабочего процесса - Фильтрация по
paths/paths-ignore,branches/tags - Триггер запроса на слияние
- Расписание (cron) — проверка раз в минуту, повтор по одной и той же ветке в пределах 120 секунд не запускает вторую сборку
- Проверки на коммите — запись и чтение через API, общее состояние коммита, учёт в проверках перед слиянием запроса
- Ручной запуск (
workflow_dispatch) с параметрамиinputs: string, boolean, choice, number - Зависимости между заданиями (
needs), в том числе необязательные (optional) - Вычисление условий
if:— сравнения, регулярные выражения, функции, логические операторы - Матрица (
strategy: matrix) сinclude/exclude,fail-fast,max-parallel - Группы одновременности (
concurrency) сcancel-in-progress - Прерываемые задания (
interruptible) — автоматическая отмена при новой отправке в тот же ref - Выполнение в Docker через
image: - Сервисные контейнеры (
services) сaliasиenv - Артефакты — сохранение и скачивание архивом по маскам путей
- Кеш (
cache) — сохранение и восстановление по ключу,policy: pull/push/pull-push - Повтор (
retry) — автоматический, сmaxи условиямиwhen - Допустимый провал (
allow-failure) - Ручные задания (запуск через API и интерфейс)
- Передача данных между заданиями (
$CI_OUTPUTи$NEEDS_<JOB>_OUTPUTS_<KEY>) - Переменные репозитория, в том числе скрываемые в журнале (
Masked) - Журналы и состояния конвейера в реальном времени
- Несколько файлов рабочего процесса
- SVG-значки состояния
- Отмена и повторный запуск конвейера и отдельных заданий
- Повторный запуск только провалившихся заданий
- Граф зависимостей заданий
- Автоматическое удаление просроченных артефактов
- API окружений развёртывания — список окружений с последним состоянием
- Исполнители: API управления и распределение заданий по меткам, программа
внешнего исполнителя (
gitriver-runner: получение задания, выполнение, журнал в реальном времени, артефакты и кеш, отмена)
Частично реализовано
| Возможность | Что есть | Чего нет |
|---|---|---|
| Выполнение шагов | Шаги задания идут в одном сеансе оболочки: cd, переменные и файлы одного шага видны следующим (кроме шагов, которые идут отдельным процессом) |
Нет изолированного выполнения шагов и отдельных кодов выхода у каждого шага |
| Условия шагов | if: у шага: выражения, if: always(), if: failure() |
always() и failure() действуют, только когда составляют условие шага целиком: в составе выражения (failure() && $X) они говорят об исходе зависимостей задания, а не о предыдущих шагах |
Действия (uses:) — см. раздел |
Четыре формы источника, using: composite/docker/node, with: → INPUT_* |
Выражения ${{ }}, выходные данные действий, inputs.default, args/entrypoint, runs.pre/post |