Непрерывная интеграция: принципы, триггеры, кэш, матрицы сборок
Зачем это вообще нужно: боль, из которой выросла идея
Представьте команду из восьми человек, которая пишет один продукт. Каждый работает в своей ветке две-три недели. Потом наступает «неделя интеграции»: восемь наборов изменений сливают в одну кодовую базу, и выясняется, что двое переименовали один и тот же класс по-разному, третий поменял сигнатуру функции, которой пользуются пятеро, а четвёртый обновил библиотеку до мажорной версии. Конфликты слияния — самая безобидная часть проблемы: их хотя бы видно. Хуже семантические конфликты, когда код собирается, но ведёт себя не так.
Ключевое наблюдение, из которого выросла непрерывная интеграция: стоимость интеграции растёт нелинейно от времени, в течение которого ветки живут раздельно. Два дня расхождения — десять минут разбирательства. Три недели — три дня и потерянные изменения. Значит, лекарство — не «лучше сливать», а «сливать чаще, пока сливать дёшево».
Мартин Фаулер сформулировал это так: continuous integration — практика, при которой участники команды интегрируют свою работу как минимум ежедневно, и каждая интеграция проверяется автоматической сборкой, включая тесты (martinfowler.com/articles/continuousIntegration.html). Обратите внимание, чего в определении нет: слова «Jenkins», «GitHub Actions», «пайплайн». CI — это в первую очередь дисциплина работы с ветками, и только во вторую — сервер сборки. Команда с идеально настроенным пайплайном и трёхнедельными feature-ветками не практикует CI. Она просто запускает тесты.
Практическое следствие: если вы внедряете «CI» и первым делом покупаете раннеры — вы, скорее всего, автоматизируете существующую боль вместо того, чтобы её убрать.
Пять принципов, без которых пайплайн бесполезен
1. Единая ветка правды и короткоживущие ветки. Trunk-based development: ветки живут часы,
максимум день-полтора, и вливаются в main. Если ваш git-flow с develop, release/* и
hotfix/* держит изменения неделями — вы вернулись к «неделе интеграции», просто с красивым
названием. Подробный разбор моделей ветвления и того, как они влияют на релизы, будет в статье
про стратегии релизов.
2. Self-testing build. Сборка должна сама себя проверять. «Собралось» — недостаточный сигнал; нужен ответ на вопрос «работает ли». Без автотестов CI вырождается в компилятор по расписанию.
3. Каждый коммит запускает сборку на интеграционной машине. Не «у меня локально всё работало» — на чистой машине, из чистого клона, из зафиксированных версий зависимостей.
4. Сборка должна быть быстрой. Классическое эмпирическое правило — десять минут. Это не магическое число, а граница внимания: пока сборка идёт меньше десяти минут, разработчик ждёт результат и правит по горячим следам. Дольше — переключается на другую задачу, и цена ошибки вырастает в разы. Всё дальнейшее в этой статье — кэш, матрицы, шардирование — существует ради удержания этой границы.
5. Красная сборка — приоритет номер один. Сломанный main блокирует всю команду. Правило
простое: либо чинишь за 10 минут, либо откатываешь коммит. Никаких «я потом посмотрю».
Метрики, по которым видно, что CI работает
Не «количество пайплайнов», а измеримые вещи. Программа DORA (dora.dev/guides/dora-metrics-four-keys) даёт четыре показателя, из которых CI напрямую влияет на два:
| Метрика | Что измеряет | Ориентир для здоровой команды |
|---|---|---|
| Deployment frequency | как часто изменения доезжают до прода | от нескольких раз в день |
| Lead time for changes | коммит → прод | менее суток |
| Change failure rate | доля релизов, вызвавших инцидент | 0–15 % |
| Failed deployment recovery | время восстановления | менее часа |
Плюс две «внутренние» метрики самого CI, которые стоит вешать на дашборд: p50/p95 длительности пайплайна (p95 важнее среднего — именно хвост убивает поток работы) и доля флейки-падений от общего числа красных сборок. Если больше 20 % падений — ложные, команда перестаёт доверять пайплайну и начинает жать «retry» не глядя.
Анатомия пайплайна
Логически любой CI-пайплайн — направленный ациклический граф этапов. Быстрые и дешёвые проверки идут первыми, чтобы отказ приходил как можно раньше (fail fast).
Два соображения по этому графу:
- Порядок этапов — это оптимизация стоимости обратной связи. Линтер, который ловит опечатку за 40 секунд, должен стоять до тестов, которые идут 6 минут. Но не переусердствуйте: строго последовательный пайплайн с десятью «быстрыми» этапами по 30 секунд тратит 5 минут на одни только накладные расходы запуска job (клон, установка тулчейна, восстановление кэша). Часто дешевле склеить lint + typecheck + format в одну job.
- Параллелизм ограничен не только графом, но и лимитом раннеров. Если у вас 5 конкурентных job на весь аккаунт, матрица из 12 job не выполнится параллельно — она встанет в очередь, и p95 вырастет вместо того чтобы упасть.
Триггеры: когда именно запускать сборку
Триггер — самая недооценённая часть конфигурации. Неправильные триггеры дают либо дыры в покрытии («а на этой ветке проверки не запускались»), либо счёт за минуты в конце месяца.
Основные виды
| Триггер | Когда используют | Подводные камни |
|---|---|---|
push в ветку |
проверка ветки разработчика | легко получить двойной прогон вместе с pull_request |
pull_request |
проверка результата слияния | GitHub тестирует «виртуальный мёрж» с базой, а не ваш HEAD |
merge_group |
merge queue перед вливанием | без него trunk ломается «зелёными по отдельности» PR |
schedule (cron) |
ночные сборки, скан зависимостей | на GitHub cron может задержаться на десятки минут |
workflow_dispatch |
ручной запуск с параметрами | нужен для откатов и разовых операций |
repository_dispatch / webhook |
внешние системы | требует токена с правом записи |
теги (v*) |
релизная сборка | не забудьте fetch-depth: 0 для версии из git |
Ключевая тонкость: pull_request тестирует не то, что вы думаете
GitHub Actions для события pull_request собирает merge-коммит — результат слияния вашей
ветки с текущим main. Это хорошо: вы проверяете то, что реально появится в trunk. Но это
означает, что зелёный статус PR устаревает, как только main уезжает вперёд. Классический
сценарий поломки: два PR зелёные по отдельности, один удаляет функцию, второй начинает её
использовать — после слияния обоих main красный.
Лечится merge queue: платформа перед вливанием строит спекулятивную цепочку
main + PR₁ + PR₂ и проверяет её. Схема ниже.
лишних спекулятивных сборок
Стоимость merge queue — дополнительные прогоны пайплайна на каждую позицию в очереди. При десяти PR в день это терпимо; при сотне спекулятивные сборки становятся заметной статьёй расходов, и глубину очереди приходится ограничивать.
Фильтры путей и отмена устаревших запусков
Два приёма, которые в монорепозитории экономят больше денег, чем всё остальное вместе взятое.
# .github/workflows/api.yml
name: api
on:
push:
branches: [main]
paths: # запускаем, только если менялся сервис или общий код
- 'services/api/**'
- 'libs/shared/**'
- '.github/workflows/api.yml'
pull_request:
paths:
- 'services/api/**'
- 'libs/shared/**'
# Новый push в ту же ветку отменяет предыдущий незавершённый прогон.
# На main отменять нельзя — там каждый коммит должен быть проверен.
concurrency:
group: api-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
permissions:
contents: read # принцип наименьших привилегий по умолчанию
Ловушка paths: если проверка помечена как required, а фильтр не дал ей запуститься, PR
зависнет в «ожидании обязательной проверки» навсегда. Решение — либо
required workflows,
либо парная job-заглушка, которая всегда запускается и рапортует успех, когда изменений
в путях не было.
Аналог в GitLab CI выразительнее — там правила первого класса:
# .gitlab-ci.yml
workflow:
rules:
# не плодим дубли: если для ветки открыт MR, пайплайн ветки не запускаем
- if: $CI_PIPELINE_SOURCE == "push" && $CI_OPEN_MERGE_REQUEST_LABELS
when: never
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
- if: $CI_COMMIT_TAG
test:api:
stage: test
rules:
- changes: ["services/api/**/*", "libs/shared/**/*"]
interruptible: true # аналог cancel-in-progress
script:
- make -C services/api test
Кэш: главный рычаг скорости
Посмотрите, куда уходит время в типичной сборке сервиса на Node/Go/JVM:
Вывод из картинки важнее конкретных цифр: кэш убирает установку зависимостей, промежуточную компиляцию и слои образа, но не трогает тесты. Довели сборку до состояния «почти всё время — тесты» — дальше рычаг только один: параллелизм (матрицы и шардирование).
Три разных вещи, которые называют одним словом
- Кэш зависимостей —
~/.npm,~/.m2,~/go/pkg/mod,~/.cargo. Восстанавливается по ключу от lock-файла. Потеря кэша означает лишние минуты, но не ломает сборку. - Кэш компиляции —
target/,.gradle/,build/, кэшsccache/ccache, кэшgo build. Самый капризный: чувствителен к версии компилятора и флагам. - Кэш слоёв образа — BuildKit,
--cache-from/--cache-to. Живёт в реестре или на диске раннера, устроен принципиально иначе (по хэшам слоёв, а не по вашему ключу).
Отдельно стоят артефакты: cache — это ускорение, потеря которого допустима, а artifact —
данные, передаваемые между job, потеря которых ломает пайплайн. Путать их — частая ошибка:
передавать скомпилированный бинарник из job build в job test через кэш нельзя, потому что
кэш не гарантирует доставку.
Ключи и инвалидация
Правило: ключ = хэш того, что определяет содержимое кэша. Для зависимостей это lock-файл плюс версия рантайма плюс ОС.
- uses: actions/cache@v4
with:
path: |
~/.npm
node_modules/.cache
# точное совпадение: тот же lock — тот же кэш
key: ${{ runner.os }}-node22-${{ hashFiles('**/package-lock.json') }}
# частичное: lock поменялся, но старый кэш всё равно даёт 90 % пользы
restore-keys: |
${{ runner.os }}-node22-
restore-keys — важнейшая деталь. Без него любое изменение lock-файла означает полностью
холодную сборку. С ним npm/pnpm доустанавливает разницу поверх почти актуального кэша.
Три ошибки, которые встречаются постоянно:
- Ключ без ОС и версии рантайма. Кэш
node_modulesс нативными модулями, собранными под glibc, приезжает на Alpine — и падает сinvalid ELF headerв самом неожиданном месте. - Кэширование
node_modulesцеликом вместо~/.npm. Кэш становится огромным, распаковка сотен тысяч мелких файлов на медленном диске занимает столько же, сколько чистая установка. Кэшируйте архив менеджера пакетов, а не развёрнутое дерево. - Отсутствие ограничения на область видимости. На GitHub кэш ветки виден из PR, но не наоборот, а общий лимит — 10 ГБ на репозиторий с вытеснением по LRU. Пять матричных job, каждая пишущая по 2 ГБ, выбьют друг друга, и hit rate упадёт до нуля при формально «настроенном» кэше.
Проверять hit rate нужно явно — по логам, а не по ощущениям:
$ gh run view 12345678 --log | grep -iE 'cache (restored|saved|not found)'
Cache restored from key: Linux-node22-a3f9c1e8b2...
Cache restored successfully
Cache Size: ~412 MB (432013312 B)
Кэш слоёв Docker: отдельная дисциплина
Слоёвый кэш работает, только если Dockerfile написан с учётом порядка инвалидации: сначала редко меняющееся, потом часто меняющееся.
# syntax=docker/dockerfile:1.7
FROM node:22-bookworm-slim AS deps
WORKDIR /app
# Копируем ТОЛЬКО манифесты — слой переиспользуется, пока не менялся lock.
COPY package.json package-lock.json ./
# cache mount живёт вне слоя: не раздувает образ и переживает изменение lock-файла.
RUN --mount=type=cache,target=/root/.npm \
npm ci --omit=dev
FROM deps AS build
COPY . .
RUN npm run build
FROM node:22-bookworm-slim AS runtime
WORKDIR /app
ENV NODE_ENV=production
COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
USER node
CMD ["node", "dist/main.js"]
Классическая антипаттерн-строка COPY . . перед установкой зависимостей уничтожает кэш при
изменении любого файла, включая README. Проверить легко:
$ docker buildx build --progress=plain -t api:dev . 2>&1 | grep -c CACHED
7
В CI слоёвый кэш нужно вынести из эфемерного раннера в реестр:
- uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
cache-from: type=registry,ref=ghcr.io/${{ github.repository }}:buildcache
cache-to: type=registry,ref=ghcr.io/${{ github.repository }}:buildcache,mode=max
mode=max кэширует промежуточные стадии multi-stage-сборки, а не только финальную — для
приведённого Dockerfile это разница между «переиспользуем стадию deps» и «собираем заново».
Плата — объём в реестре и трафик. Детально про сборку образов, их размер и безопасность —
в статье про контейнеры в доставке.
Матрицы сборок
Матрица — декартово произведение осей, из которого можно вычитать (exclude) и в которое можно
добавлять точки вне решётки (include).
jobs:
test:
runs-on: ${{ matrix.os }}
timeout-minutes: 20 # всегда ставьте: зависшая job жжёт минуты до лимита
strategy:
fail-fast: false # для PR-проверок хотим ВСЕ падения, а не первое
max-parallel: 6 # держим лимит конкурентности аккаунта
matrix:
os: [ubuntu-24.04, macos-14, windows-2022]
node: [20, 22, 24]
exclude:
- { os: macos-14, node: 20 } # платформа × версия, которую никто не использует
- { os: windows-2022, node: 20 }
include:
- { os: ubuntu-24.04, node: nightly, experimental: true }
continue-on-error: ${{ matrix.experimental == true }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
cache: npm # setup-* уже умеет кэш — не изобретайте свой
- run: npm ci
- run: npm test
Про fail-fast есть неочевидный компромисс. fail-fast: true (по умолчанию) экономит минуты:
первое падение убивает остальные job. Но разработчик получает неполную картину и чинит ошибки
по одной, за несколько итераций. Практическое правило: false для PR-проверок
(человеку нужна полная картина), true для ночных прогонов широких матриц (там важна экономия).
Шардирование: матрица как инструмент параллелизма
Когда матрица используется не для покрытия платформ, а для разрезания одного тестового прогона:
strategy:
matrix:
shard: [1, 2, 3, 4]
steps:
- run: npx jest --shard=${{ matrix.shard }}/4 --ci
Ускорение шардированием ограничено законом Амдала. Если фиксированные накладные расходы job (клон, установка тулчейна, восстановление кэша) — 90 секунд, а тесты идут 600 секунд, то:
T(N) = overhead + tests/N + сборка_отчёта
T(1) = 90 + 600 = 690 с
T(4) = 90 + 150 + 20 = 260 с (ускорение 2,65×, а не 4×)
T(8) = 90 + 75 + 20 = 185 с (ускорение 3,7×)
T(16)= 90 + 38 + 20 = 148 с (ускорение 4,7× при 16× стоимости)
Минуты при этом растут строго линейно: 690 → 1040 → 1480 → 2368 машино-секунд. Разумная точка для большинства команд — 4–8 шардов; дальше вы платите вдвое за 15 % ускорения. И почти всегда дешевле сначала убрать 90 секунд оверхеда (предсобранный образ раннера с тулчейном) и уменьшить сам объём тестов, чем добавлять шарды.
Важный нюанс: шардирование должно быть детерминированным и сбалансированным. Наивное деление
по алфавиту файлов даёт шард с одним медленным интеграционным тестом на 8 минут и три шарда
по 40 секунд. Хорошие раннеры (jest, pytest-split, RSpec с knapsack) умеют делить по истории
длительности — включайте это.
Жизненный цикл сборки
Полезно смотреть на пайплайн как на конечный автомат — это сразу проясняет, где нужны таймауты, где ретраи, а где ретраи запрещены.
Разница между «инфраструктурным сбоем» и «падением теста» — принципиальная. Сеть отвалилась
при npm ci, реестр вернул 503, раннер вытеснили как spot-инстанс — это ретраить нужно.
Упавший ассерт ретраить нельзя: три попытки превращают детектор багов в генератор случайных чисел.
Работа с флейки-тестами строится не через retry, а через процесс:
- Детектируем: тест, который на одном и том же SHA даёт разный результат.
- Помечаем и выносим в карантин — отдельная job, не блокирующая мёрж.
- Заводим задачу с дедлайном. Тест без владельца через две недели удаляется.
Без шага 3 карантин превращается на свалку, и через полгода четверть набора не проверяет ничего.
Сколько это стоит: managed-минуты против своих раннеров
Здесь начинается инженерия, а не мода. Возьмём типичный сервис: 60 прогонов пайплайна в рабочий
день, средняя сборка — 8 минут на 4 ядрах, матрица из 4 job. Итого примерно
60 × 8 × 4 × 22 ≈ 42 000 машино-минут в месяц на 4-ядерных раннерах.
| Вариант | Прямые расходы в месяц | Порог входа | Эксплуатационная нагрузка |
|---|---|---|---|
| GitHub-hosted, 4 vCPU (~0,016 $/мин) | ≈ 670 $ сверх бесплатного лимита | минуты: положил YAML — работает | нулевая |
| GitHub-hosted для public repo | 0 $ | минимальный | нулевая |
| Self-hosted на Hetzner CCX33 (8 vCPU, 32 ГБ) ×2 | ≈ €100 | день-два на ARC/образ | 2–6 ч/мес: обновления, диск, безопасность |
| Self-hosted на выделенном сервере AX-серии | ≈ €50–90 | несколько дней | выше: железо, диски, доступность |
| Свой Jenkins на своём железе | амортизация + место в стойке | недели | максимальная: свой SRE-бюджет |
| Managed GitLab SaaS (минуты ~10 $/1000) | ≈ 420 $ | минуты | нулевая |
Три честных вывода:
- Считайте полную стоимость владения, а не только счёт от провайдера. Два раннера за €100 против 670 $ выглядят как экономия 570 $ в месяц. Но 4 часа инженерного времени в месяц по внутренней ставке 60 $–100/час съедают до половины выигрыша, а инцидент «раннеры лежат, вся команда не может влить код» стоит дороже годовой экономии.
- Порог перехода на свои раннеры лежит примерно в районе 500 $–1000/мес счёта за минуты или требований, которые managed не закрывает: GPU, специфическое железо, доступ во внутреннюю сеть, соответствие регуляторным требованиям, тяжёлый Docker-кэш на локальном NVMe.
- Самый дешёвый способ сократить счёт — не менять провайдера, а сократить работу.
Фильтры путей,
cancel-in-progress, кэш и отказ от матрицы там, где она не нужна, обычно дают 40–70 % экономии за один рабочий день инженера. Делайте это до миграции.
Спот-инстансы и автомасштабирование (Actions Runner Controller в Kubernetes, GitLab autoscaler через docker-machine/fleeting) меняют картину ещё сильнее: платите за секунды работы, а не за круглосуточную VM. Плата — сложность: раннер могут вытеснить посреди сборки, значит нужны идемпотентные шаги и ретраи на инфраструктурном уровне. Развёрнутое сравнение платформ — в статье про современные CI-платформы, про экономику инфраструктуры в целом — в FinOps и стоимости.
Декларативно и переносимо: пайплайн как код
Три реализации одной идеи. GitHub Actions:
name: ci
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with: { go-version: '1.23', cache: true }
- run: make lint test build
Jenkins (декларативный Jenkinsfile — подробности в статье про Jenkins):
pipeline {
agent { docker { image 'golang:1.23'; args '-v $HOME/go/pkg/mod:/go/pkg/mod' } }
options { timeout(time: 20, unit: 'MINUTES'); disableConcurrentBuilds() }
stages {
stage('Lint') { steps { sh 'make lint' } }
stage('Test') { steps { sh 'make test' }
post { always { junit 'reports/*.xml' } } }
stage('Build') { steps { sh 'make build' } }
}
}
Обратите внимание на общий знаменатель: во всех трёх случаях реальная логика живёт в make
(или task, или just), а не в YAML. Это не эстетика, а страховка:
- разработчик воспроизводит сборку локально одной командой —
make test, а не «открой логи CI»; - миграция между CI-платформами становится переписыванием 30 строк YAML, а не всего пайплайна;
- логика сборки версионируется и ревьюится как обычный код, а не как настройки в веб-интерфейсе.
Правило «никакой бизнес-логики в YAML» — одна из немногих практик, которая окупается в любом масштабе. Пайплайн, собранный кликами в UI Jenkins, невозможно ни отревьюить, ни откатить, ни воспроизвести.
Типичные ошибки
- Тесты, зависящие от порядка и общего состояния. На одной машине проходят, при шардировании
падают. Проверяется прогоном с
--random-orderили-shuffle=on— сделайте это до внедрения параллелизма, а не после. - Плавающие версии тулчейна.
runs-on: ubuntu-latestиnode-version: 22(без точной версии) однажды утром сломают сборку из-за обновления образа раннера. Для критичных пайплайнов фиксируйтеubuntu-24.04и.nvmrc/.tool-versions. - Секреты в pull_request от форков. GitHub намеренно не отдаёт секреты в такие прогоны.
Использовать
pull_request_targetради доступа к секретам — известная дыра: код форка выполнится с правами вашего репозитория. Подробно — в статье про безопасность конвейера. - Пайплайн без
timeout-minutes. Зависшая на сетевом вызове job крутится до платформенного лимита (6 часов на GitHub) и стоит как весь день сборок. - Незакреплённые сторонние actions.
uses: some/action@v1— это доверие к перемещаемому тегу. Для чувствительных пайплайнов пиньте по SHA:uses: some/action@a1b2c3d.... - Кэш как способ спрятать медленную сборку. Если холодная сборка идёт 40 минут, у вас проблема архитектуры сборки, а не кэша. Кэш выйдет из строя в самый неудобный момент.
- Один гигантский workflow на монорепозиторий. Любое изменение запускает всё. Режьте
по сервисам через
paths, либо берите инструмент с графом зависимостей — Bazel, Nx, Turborepo, Pants — которые считают, что реально затронуто изменением.
Практика: чек-лист живого пайплайна
- p95 сборки на PR — до 10 минут; есть дашборд с длительностью и hit rate кэша.
- Логика в
Makefile/Taskfile, YAML — только оркестрация и триггеры. - Кэш зависимостей с
restore-keys; ключ содержит ОС, версию рантайма и хэш lock-файла. - Docker-сборка через BuildKit с
cache-from/cache-toв реестр, multi-stage,mode=max. concurrencyс отменой устаревших прогонов на ветках и без отмены наmain.timeout-minutesна каждой job;permissions: contents: readпо умолчанию.- Матрица закрывает реальные поддерживаемые платформы, а не «все, какие бывают».
- Флейки в карантине с владельцем и дедлайном; автоматический retry — только для инфраструктуры.
- Merge queue или обязательная актуальность ветки перед вливанием в trunk.
- Артефакты сборки неизменяемы и адресуются по
git sha, а не по тегуlatest.
Мини-итог
Непрерывная интеграция — практика частого слияния, а не набор YAML-файлов. Пайплайн лишь делает эту практику дешёвой: даёт быструю и надёжную обратную связь по каждому изменению. Отсюда следуют все инженерные решения. Триггеры определяют, что и когда проверяется, и напрямую формируют счёт. Кэш убирает повторяющуюся работу — установку зависимостей, компиляцию, слои образов, — но бессилен против самих тестов. Матрицы и шардирование дают параллелизм с сублинейной отдачей из-за фиксированных накладных расходов, поэтому сначала убирают оверхед и лишнюю работу, и только потом добавляют job. А выбор между managed-минутами и своими раннерами — не вопрос вкуса: до нескольких сотен долларов в месяц managed выигрывает почти всегда, потому что эксплуатация стоит дороже счёта.
Источники
- Martin Fowler. Continuous Integration — martinfowler.com/articles/continuousIntegration.html
- Jez Humble, David Farley. Continuous Delivery (Addison-Wesley, 2010) — главы 3 и 5 о конвейере развёртывания
- Nicole Forsgren, Jez Humble, Gene Kim. Accelerate (IT Revolution, 2018)
- DORA. DORA Metrics — dora.dev/guides/dora-metrics-four-keys
- GitHub Docs. Caching dependencies to speed up workflows — docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows
- GitHub Docs. Merge queue — docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/configuring-pull-request-merges/managing-a-merge-queue
- Docker Docs. Cache management with BuildKit — docs.docker.com/build/cache/
- GitLab Docs. Choose when to run jobs (
rules) — docs.gitlab.com/ee/ci/jobs/job_control.html - Google Testing Blog. Flaky Tests at Google and How We Mitigate Them — testing.googleblog.com/2016/05/flaky-tests-at-google-and-how-we.html
- Paul Hammant. Trunk Based Development — trunkbaseddevelopment.com
Что дальше
Мы разобрали, из чего состоит непрерывная интеграция как практика и как инженерная система. Следующий шаг — конкретный инструмент, который тридцать лет держал эту область и до сих пор крутит пайплайны в половине корпоративных контуров: где у него архитектурные пределы, как писать Jenkinsfile, как жить с агентами и плагинами и в каких случаях его сегодня всё ещё выбирают осознанно.
Jenkins: архитектура, Jenkinsfile, агенты, плагины и когда он всё ещё уместен