Прогон в непрерывной интеграции подчиняется трём требованиям, и все три следуют из отсутствия человека. Он должен быть неинтерактивным - спрашивать некого, и любой запрос подтверждения превращается в повисшее задание, которое в лучшем случае убьёт таймаут. Он должен быть ограничен по репозиторию, ветке и правам токена - иначе одна неверная настройка повторяется на каждом прогоне и живёт ровно столько, сколько живёт файл пайплайна. И он должен заканчиваться машиночитаемым свидетельством - иначе результат нечем проверить, кроме как прочитать лог глазами, а лог никто не читает, пока не случится инцидент.
Наивный подход - взять готовый пример из документации и считать вопрос безопасности решённым производителем. Cursor публикует рабочий шаблон, и он действительно рабочий: на нём прогон запустится с первого раза. Но модель угроз остаётся вашей. В вашем окружении есть форки, чужие скрипты сборки, свои секреты и свои правила ветвления - всё то, о чём шаблон знать не может, потому что писался он под демонстрацию механизма, а не под вашу организацию. Готовый файл - это отправная точка, а не политика.
Полезно один раз увидеть минимальный обзорный прогон. Ниже - запуск на пул-реквесте с правами только на чтение содержимого, установка командной строки, обзор изменений с выводом в файл и сохранение результата артефактом. Ключевое здесь не то, что агент умеет читать diff, а то, чего в этом файле нет: прав на запись, разрешения менять файлы и широких токенов. Ограничение времени задания стоит рядом с правами по той же причине - и то и другое ограничивает ущерб от прогона, который повёл себя не так, как задумано.
Права выдают строго под требуемое поведение. Обзору достаточно чтения; комментировать пул-реквест потребует права записи в обсуждения; коммитить - права на содержимое. Каждое расширение делают отдельным решением, потому что в непрерывной интеграции цена ошибки умножается на число прогонов: разрешение на всякий случай срабатывает не один раз, а каждый день и на каждой ветке. Обратный ход стоит закладывать сразу - права проще сузить, пока пайплайн новый, чем после того, как от них начали зависеть соседние задания.
Отдельная осторожность - к пул-реквестам из форков. Это код, написанный посторонним, и запускать привилегированный процесс на нём без отдельной модели безопасности опасно: содержимое ветки фактически управляет тем, что выполнится на раннере. У платформ своя механика секретов для форков, и полагаться на неё вслепую нельзя. Правильный ход - развести доверенный и недоверенный прогоны так, чтобы недоверенный не имел доступа к секретам вообще, а всё, что требует привилегий, выполнялось отдельным заданием на уже проверенном содержимом.
Есть и более тонкий риск, о котором забывают: сам установщик. Строка, скачивающая и исполняющая скрипт, выполняется на раннере с вашими секретами в окружении, и её содержимое может измениться между вчерашним прогоном и сегодняшним, не меняя ни строки в вашем репозитории. В зрелом пайплайне артефакт установки проверяют и раздают через внутренний канал, а действия закрепляют по версии. Это та же цепочка поставки, что и для зависимостей приложения, только про инструменты - и её обычно упускают, потому что инструменты не попадают в отчёты о зависимостях.
Машиночитаемый вывод нужен не ради аккуратности, а ради следующего шага. Файл с результатом можно разобрать и превратить в действие: пометить задание неуспешным при находках определённой серьёзности, оставить комментарий, сложить находки в общий отчёт по репозиторию. Пока результат остаётся текстом в логе, ничего этого не построить, и обзор превращается в ритуал, который никто не читает. При этом заранее решают, что именно делается с находками: обзор, блокирующий слияние, и обзор, остающийся советом, - разные инструменты с разной ценой ложного срабатывания.
У такого прогона есть цена и граница применимости. Каждый пуш в ветку запускает обзор заново, и на активной ветке это десятки прогонов в день, каждый со своим временем и своей стоимостью; ограничение по путям и событиям экономит здесь больше, чем любая шлифовка формулировки запроса. Граница проходит там, где начинается недетерминированность: одна и та же правка может получить разный по составу разбор, и строить на этом жёсткий пропускной критерий - значит получить случайно падающие задания. Признак, по которому в реальной работе понимают, что что-то пошло не так, - обычно не ошибка, а тишина: задание зелёное, артефакт пуст или содержит отказ агента, и этого не замечают, пока не начнут искать причину пропущенного дефекта.
Инженерный вывод простой: обзорный прогон и изменяющий прогон - разные пайплайны с разными правами. Смешивать их удобно и опасно: стоит добавить в обзорный процесс разрешение менять файлы, и он превращается в автоматическую правку без человека, причём на каждом пуше. Пусть обзор остаётся только читающим и заканчивается артефактом, а мутация живёт отдельно, в изолированном процессе с явным владельцем, явным триггером и явной точкой, где изменения попадают на глаза человеку.
Типичные провалы предсказуемы. Взять шаблон как готовую политику и не тронуть права. Разрешить изменение файлов в обзорном задании. Запустить привилегированный прогон на пул-реквесте из форка. Не поставить ограничение времени и оплатить зависший прогон. И не сохранить машиночитаемый результат, оставив себе для разбора только лог.
name: cursor-review
on:
pull_request:
permissions:
contents: read # обзору достаточно чтения
jobs:
review:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- name: Установить командную строку Cursor
run: |
curl https://cursor.com/install -fsS | bash
echo "$HOME/.cursor/bin" >> $GITHUB_PATH
- name: Обзор изменений
env:
CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }}
run: agent -p --output-format json "Review this PR diff; do not edit files" > cursor-review.json
- uses: actions/upload-artifact@v4
with:
name: cursor-review
path: cursor-review.json