Amiga
Оглавление курса · урок 5 из 14

Модуль 2 · CI/CD · урок 5 из 14

Codex в GitLab CI

Код агентства живёт в self-managed GitLab, поэтому основной сценарий автоматизации для нас — job в .gitlab-ci.yml. Готового компонента для GitLab CI, как GitHub Action, нет (встроенная интеграция с GitLab для ревью MR есть, но работает в облаке Codex — о ней в уроке про ревью). Это даже хорошо: всё делается обычным codex exec, и каждую деталь видно. В уроке соберём job, который проверяет merge request и показывает замечания в виджете Code Quality, и разберём, как не раздать при этом ключи всем подряд.

Из чего состоит схема

  • Раннер. Docker-раннер, которому разрешён выход к API OpenAI. Удобно завести для Codex отдельный раннер с тегом codex: проще следить за нагрузкой и сетевыми правилами.
  • Образ. Любой образ с Node.js 18+ для установки CLI через npm. Если проверка должна запускать тесты, берите образ проекта и доустанавливайте Codex в него.
  • Ключ. API-ключ OpenAI в переменной CI/CD. Для автоматизации документация рекомендует именно API-ключ, а не вход через аккаунт ChatGPT.
  • Файлы в репозитории. Промпт и JSON Schema лежат рядом с кодом в .gitlab/codex/ и меняются через MR.
  • Результат. Отчёт в формате GitLab Code Quality (CodeClimate JSON) и журнал событий — в артефактах.

Если раннер ходит в интернет через корпоративный прокси с подменой TLS-сертификатов, укажите путь к корневому сертификату в переменной CODEX_CA_CERTIFICATE (запасной вариант — SSL_CERT_FILE).

Ключ: переменные CI/CD

Создайте отдельный API-ключ для CI — не личный ключ разработчика. Тогда его можно ограничить, отслеживать расход и отозвать, не задев никого.

В GitLab: Settings → CI/CD → Variables → Add variable.

  • Key: OPENAI_API_KEY.
  • Masked — чтобы значение не попало в лог job.
  • Environment scope: codex. По умолчанию переменная доступна всем job пайплайна: линтеру, тестам, сборке фронта. Если в любом из них окажется скомпрометированная зависимость, она прочитает ключ. С областью окружения переменную получит только job, где указано environment: codex. Сама документация GitLab рекомендует ограничивать так чувствительные переменные.
  • Учтите границу этой защиты. Область окружения спасает от скомпрометированной зависимости в соседнем job, но не от того, кто может пушить в репозиторий: в своей ветке он может поменять .gitlab-ci.yml и указать нужное окружение в любом job. Codex с ключом в CI — для проектов, где право push есть только у вашей команды.
  • Protected — осторожно. Защищённая переменная доступна только в пайплайнах защищённых веток и тегов. Пайплайн MR из обычной feature-ветки её не увидит, и job молча останется без ключа.

Внутри job ключ передаётся только вызову Codex: CODEX_API_KEY="$OPENAI_API_KEY" codex exec .... Так рекомендует документация Codex: не выставлять ключ на уровне всего job, где выполняется код из репозитория.

Для Enterprise есть ещё вариант с токеном доступа Codex (CODEX_ACCESS_TOKEN) для доверенной автоматизации — см. Access tokens.

Песочница в контейнере

Для проверки MR агенту достаточно читать файлы, поэтому запускаем с --sandbox read-only. Но в Docker есть нюанс. На Linux песочница Codex построена на bubblewrap и seccomp, и документация предупреждает: в контейнере она может не заработать, если конфигурация хоста или контейнера запрещает нужные операции с пространствами имён. Тогда job упадёт с ошибкой создания песочницы.

У вас два пути:

  1. Поставить в образ пакет bubblewrap (в примере ниже он ставится в before_script) и настроить раннер так, чтобы контейнеру были доступны нужные операции с пространствами имён. Второе — вопрос к администратору раннера.
  2. Сделать границей безопасности сам контейнер и запускать Codex с --sandbox danger-full-access внутри него. Это документированный вариант, но тогда агент внутри контейнера может всё: писать файлы и ходить в сеть. Значит, в контейнере не должно быть ничего лишнего — ни чужих секретов, ни токенов с правом записи.

В обоих случаях добавьте две настройки окружения для команд, которые запускает агент:

bash
-c shell_environment_policy.inherit=core
-c shell_environment_policy.ignore_default_excludes=false

Первая передаёт командам агента только базовый набор переменных окружения. Вторая включает автоматическое исключение переменных с KEY, SECRET и TOKEN в имени. По умолчанию это исключение выключено, а в окружении GitLab job всегда есть как минимум CI_JOB_TOKEN.

Рабочий пример

Три файла. Промпт:

markdown · .gitlab/codex/quality-prompt.md
Ты проверяешь merge request в проекте на OctoberCMS (PHP, Twig, JS).
Изменения: git diff <базовый коммит>...HEAD, базовый коммит указан в конце.

Ищи только существенные проблемы в изменённых строках:
- ошибки логики и необработанные исключения;
- SQL-инъекции, вывод через |raw в Twig без экранирования, отсутствие валидации входных данных;
- запросы к БД в циклах и выборки без ограничений;
- нарушения правил из раздела «Code Review Rules» в AGENTS.md.

Не сообщай о стиле и форматировании — это проверяет линтер.
Если не уверен в замечании — не включай его. Ничего не меняй в файлах.

path — путь от корня репозитория, lines.begin — номер строки в новой версии файла.
fingerprint — стабильная строка из пути, имени проверки и сути проблемы.
Если замечаний нет, верни пустой массив issues.

Схема ответа. GitLab ждёт массив, а корень ответа по схеме должен быть объектом, поэтому массив лежит в поле issues:

json · .gitlab/codex/quality.schema.json
{
  "type": "object",
  "properties": {
    "issues": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "description": { "type": "string" },
          "check_name": { "type": "string" },
          "fingerprint": { "type": "string" },
          "severity": { "type": "string", "enum": ["info", "minor", "major", "critical", "blocker"] },
          "location": {
            "type": "object",
            "properties": {
              "path": { "type": "string" },
              "lines": {
                "type": "object",
                "properties": { "begin": { "type": "integer" } },
                "required": ["begin"],
                "additionalProperties": false
              }
            },
            "required": ["path", "lines"],
            "additionalProperties": false
          }
        },
        "required": ["description", "check_name", "fingerprint", "severity", "location"],
        "additionalProperties": false
      }
    }
  },
  "required": ["issues"],
  "additionalProperties": false
}

И сам job:

yaml · .gitlab-ci.yml
stages:
  - test
  - review

codex_quality:
  stage: review
  image: node:22-bookworm
  tags: [codex]
  environment:
    name: codex
    action: prepare
  variables:
    CODEX_VERSION: "0.163.0"
    GIT_DEPTH: "0"
  rules:
    - if: '$CI_PIPELINE_SOURCE != "merge_request_event"'
      when: never
    - if: '$CI_MERGE_REQUEST_SOURCE_PROJECT_ID != $CI_PROJECT_ID'
      when: never
    - changes:
        - "**/*.{php,htm,js,twig}"
  interruptible: true
  timeout: 20 minutes
  allow_failure: true
  before_script:
    - apt-get update && apt-get install -y --no-install-recommends jq bubblewrap
    - npm install -g "@openai/codex@${CODEX_VERSION}"
    - codex --version
  script:
    - set -euo pipefail
    - mkdir -p codex-out
    - |
      {
        cat .gitlab/codex/quality-prompt.md
        echo
        echo "Базовый коммит: ${CI_MERGE_REQUEST_DIFF_BASE_SHA}"
      } | CODEX_API_KEY="$OPENAI_API_KEY" codex exec \
            --ephemeral \
            --sandbox read-only \
            -c shell_environment_policy.inherit=core \
            -c shell_environment_policy.ignore_default_excludes=false \
            --output-schema .gitlab/codex/quality.schema.json \
            -o codex-out/result.json \
            --json - > codex-out/events.jsonl
    - jq -e '.issues | type == "array"' codex-out/result.json > /dev/null
    - jq '.issues' codex-out/result.json > gl-code-quality-report.json
    - echo "Замечаний: $(jq length gl-code-quality-report.json)"
  artifacts:
    when: always
    expire_in: 14 days
    paths:
      - codex-out/
    reports:
      codequality: gl-code-quality-report.json

Разбор по строкам

rules. Job идёт только в пайплайнах merge request, только для MR из этого же проекта и только если изменились файлы кода. Форки отсекаются: в них нельзя доверять ни коду, ни тексту MR. Правило про изменения экономит запуски на правках документации. Не ставьте в rules условие на наличие OPENAI_API_KEY: GitLab предупреждает, что переменные с областью окружения могут быть не определены в момент вычисления правил.

environment: codex с action: prepare. Именно это даёт job доступ к переменной с областью codex. prepare означает, что job не считается деплоем и не создаёт записей о развёртывании.

CODEX_VERSION. Версия CLI закреплена. Флаги и поведение меняются между релизами, и пайплайн не должен ломаться оттого, что вчера вышла новая версия. Обновляйте её осознанно, отдельным MR. Последний стабильный релиз на 22 октября 2026 года — 0.163.0.

GIT_DEPTH: "0". Полная история нужна, чтобы базовый коммит MR был в клоне и git diff сработал. Для больших репозиториев можно подобрать глубину поменьше.

--ephemeral. Сессия не сохраняется: продолжать её в CI незачем, а лишние файлы в контейнере ни к чему.

set -euo pipefail и jq -e. Если Codex упал или вернул что-то не то, job падает, а не публикует пустой отчёт. Упавший запуск — не «замечаний нет».

allow_failure: true. Пока вы не откалибровали промпт, проверка не должна блокировать мерж. Когда доля полезных замечаний вас устроит, флаг можно убрать.

interruptible и timeout. Новый пуш в MR отменит устаревший запуск, а зависший агент не займёт раннер на час.

artifacts. Отчёт codequality GitLab покажет в виджете MR — это работает во всех редакциях, включая Free. Журнал events.jsonl остаётся для разбора: какие команды запускал агент и сколько токенов ушло.

Ограничения прав: чек-лист

  • Ключ — отдельный для CI, с областью окружения, Masked, с лимитами и регулярной ротацией.
  • --sandbox read-only для анализа. Права на запись — только job, которым они действительно нужны (см. автопочинку CI).
  • Если песочница в контейнере не работает и вы перешли на danger-full-access, в job не должно быть токенов с правом записи в GitLab.
  • Команды агента получают урезанное окружение (shell_environment_policy).
  • Форки и MR от внешних участников не запускают Codex.
  • Публикация результатов в MR — отдельным job со своим токеном. Как это сделать — в уроке про ревью merge request.

Учтите и то, что сам процесс codex держит ключ в своём окружении, а агент работает от того же пользователя. Считайте, что ключ потенциально доступен агенту, и стройте защиту так, чтобы утечка не стала катастрофой: отдельный ключ, ограничения, мониторинг расхода. Подробнее — в уроке «Безопасность автоматизации».

Похожий пример есть в OpenAI Cookbook: там же разобран разбор отчёта SAST. Учтите, что примеры Cookbook используют устаревший флаг --full-auto и разбирают ответ по текстовым маркерам. --output-schema надёжнее.

Попробуйте сами

10–15 мин на рабочем месте
  1. Заведите переменную OPENAI_API_KEY с областью окружения codex в тестовом проекте. Добавьте job из урока и откройте MR с заведомо проблемным кодом — например, с выводом пользовательского ввода через |raw.
  2. Посмотрите, появилось ли замечание в виджете Code Quality, и откройте codex-out/events.jsonl из артефактов: какие команды выполнял агент?
  3. Создайте второй job без environment: codex и выведите в нём echo "${OPENAI_API_KEY:-нет ключа}". Убедитесь, что ключа там нет.

Коротко

  • Codex в GitLab CI — это codex exec в обычном job. Промпт и схема лежат в репозитории.
  • Ключ: отдельный, Masked, с областью окружения codex. В job он передаётся только вызову Codex.
  • Песочница в Docker может не заработать. Тогда граница безопасности — контейнер, и в нём не должно быть лишних секретов.
  • Версия CLI закреплена, упавший запуск роняет job, отчёт Code Quality виден в MR.
  • Форки не запускают Codex. Публикация результатов идёт отдельным job.
Отметка хранится только в вашем браузере
Хотите связаться с владельцем
компании напрямую?
Дмитрий Тарасов
Дмитрий Тарасов
СЕО

НАПИСАТЬ

Написать в MAX