Когда проектов в GitLab становится много, довольно быстро появляется одна и та же задача: найти, где используется конкретный API, URL, env-переменная или конфигурационный параметр.

Пока репозиториев мало, всё просто: открыл поиск, ввел строку, получил результат. Но когда проектов уже больше сотни, а нужные вхождения лежат не только в коде, но и в YAML-конфигах, Helm-чартах, .env и JSON-файлах, жизнь становится менее романтичной.

Первый лобовой вариант — просто скачать все проекты локально и искать по ним через grep, ripgrep или IDE. Работает, но тащить 100+ репозиториев на локальную машину ради одной проверки — идея так себе. Ноутбук, скорее всего, энтузиазма не разделит.

Мне хотелось искать прямо поверх GitLab, без локального зеркала всей группы репозиториев. Я начал с просмотра готовых вариантов, а в итоге пришёл к своему гибридному краулеру: код ищется через GitLab API, а конфиги добираются отдельным глубоким обходом файлов. В результате поиск по 100+ проектам сократился с часов до нескольких минут.

Коротко: я не строил “универсальную платформу поиска по коду”. Я решал вполне прикладную инженерную задачу: быстро проверить большую группу проектов и не пропустить то, что лежит в конфигах.

Чтобы не перегружать статью полным листингом, я вынес полную версию скрипта в отдельный репозиторий на GitHub. В статье оставил только ключевые фрагменты: логику API Search, Deep Search и параллельную обработку.


Сначала я посмотрел, какие вообще есть варианты

Перед тем как писать своё решение, я прошёлся по вариантам, которые напрашиваются первыми.

Сравнение подходов

Подход

Плюсы

Минусы

Почему не подошёл

GitLab UI Search

Быстро, просто, ничего не нужно настраивать

Неудобно на 100+ проектах, неполно по конфигам

Для массовой проверки слишком ручной

Локальный поиск по клонам (grep, ripgrep)

Полный контроль, точный поиск

Нужно клонировать и обновлять все репозитории

Слишком тяжело для разовой или периодической задачи

GitLab API Search

Быстро, работает прямо поверх GitLab

Те же ограничения индексированного поиска

Хорошо для кода, но не для всех конфигов

Sourcegraph и похожие инструменты

Очень быстро, удобно, мощно

Это уже отдельный инструмент и отдельное внедрение

Слишком тяжёлое решение под мою задачу

Свой гибридный краулер

Гибко, без отдельной инфраструктуры, можно заточить под задачу

Нужно написать и поддерживать

Оказался лучшим компромиссом

Я не пытался заменить Sourcegraph или построить корпоративный поиск по коду. Задача была заметно проще: быстро и надёжно искать по большой группе GitLab-проектов без лишней инфраструктуры.


Почему я не остановился на одном способе

Очень быстро стало понятно, что разные типы файлов ведут себя по-разному.

Тип файлов

Что лучше работает

Почему

Код (.py, .js, .go, .ts)

API Search

GitLab нормально индексирует код

Документация (.md, .txt)

API Search

Быстро и обычно этого достаточно

Конфиги (.yml, .yaml, .json, .env)

Deep Search

Здесь встроенный поиск чаще промахивается

Бинарные файлы

Не искать

Это просто лишняя трата времени

Отсюда и родилась основная идея: не искать всё одним способом.

Там, где GitLab уже умеет искать быстро, лучше использовать его сильную сторону. Там, где он периодически промахивается, нужно добирать результат вручную.


Идея решения: гибридный поиск

Мой скрипт работает по довольно простой логике:

  1. Получает список проектов из GitLab-группы.

  2. Отфильтровывает архивные и неактуальные.

  3. Для каждого проекта запускает:

    • быстрый API Search по коду;

    • глубокий обход конфигов.

  4. Объединяет результаты.

  5. Формирует отчёты.

Получается такая схема:

GitLab Group
    │
    ├── Получить список проектов
    │
    ├── Отфильтровать архивные / неактуальные
    │
    ├── Для каждого проекта:
    │      ├── API Search → код, docs, scripts
    │      └── Deep Search → yaml, json, env
    │
    ├── Объединить результаты
    │
    └── Сформировать:
           ├── Детальный отчёт
           ├── Суммарный отчёт
           └── Файл ошибок

В каком-то смысле всё решение строится на одной простой мысли: не делать вид, что один инструмент одинаково хорош во всём.


Как устроен скрипт

1. Сначала получаю список проектов

group = gl.groups.get(GROUP_ID)
all_projects = group.projects.list(include_subgroups=True, all=True)

Дальше отбрасываю архивные и неактуальные репозитории, чтобы не тратить на них время.

На практике это важно. Если без фильтрации пройтись по всему подряд, время растёт быстрее пользы.

2. Для кода использую API Search

def api_search(gl, project_id, search_terms):
    results = {term: [] for term in search_terms}

    for term in search_terms:
        blobs = gl.search('blobs', term, project_id=project_id)

        for blob in blobs:
            file_path = blob.get('path', '')
            file_ext = os.path.splitext(file_path)[1].lower()

            if file_ext in CODE_EXTENSIONS or file_ext == '':
                results[term].append({
                    'path': file_path,
                    'url': blob.get('web_url', '#'),
                    'found_by': 'API'
                })

    return results

Именно API Search даёт основную скорость на кодовых файлах и документации.

3. Для конфигов делаю Deep Search

def deep_search_configs(gl, project_id, search_terms):
    results = {term: [] for term in search_terms}
    project = gl.projects.get(project_id)

    files = project.repository_tree(recursive=True, ref='master')

    for file in files:
        if file['type'] != 'blob':
            continue

        ext = os.path.splitext(file['name'])[1].lower()
        if ext not in CONFIG_EXTENSIONS:
            continue

        content = project.files.get(file_path=file['path'], ref='master').decode()
        content_lower = content.lower()

        for term in search_terms:
            if term.lower() in content_lower:
                results[term].append({
                    'path': file['path'],
                    'found_by': 'Deep'
                })

    return results

Да, это медленнее. Но именно здесь добираются YAML, .env и другие конфиги, которые встроенный поиск может пропустить.


Почему поиск только по master

В текущей версии скрипта поиск идёт только по master. Для моей задачи этого было достаточно: внутри компании проверять нужно было именно основную ветку, потому что актуальные конфиги и рабочие настройки лежали там.

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

Это как раз тот случай, когда сначала лучше закрыть рабочую задачу, а уже потом делать решение более универсальным.


Отдельное ускорение — параллельная обработка

Если обрабатывать 100+ проектов последовательно, скрипт становится заметно медленнее. Поэтому я добавил ThreadPoolExecutor:

with ThreadPoolExecutor(max_workers=3) as executor:
    futures = {executor.submit(process_one_project, gl, p): p for p in active_projects}

    for future in as_completed(futures):
        result = future.result()
        results_list.append(result)

Это дало хороший прирост по времени без лишнего усложнения.

Без параллельности скрипт тоже работал, но уже без особого энтузиазма. С параллельностью он стал ощущаться как нормальный рабочий инструмент, а не как процесс, который лучше запустить и уйти пить чай.


Как выглядит результат

Скрипт формирует два основных отчёта.

Детальный отчёт

Он показывает:

  • проект;

  • путь к файлу;

  • найденный термин;

  • строку;

  • способ поиска;

  • ссылку на GitLab.

Пример:

   ПРОЕКТ: service-a
   Путь: backend/service-a

  'SERVICE_EXAMPLE': 2 вхождения
      - deploy/prod/values.yml:42 [Deep]
        Строка 42: SERVICE_EXAMPLE: "{{ .Values.secrets.apiKey }}"
        https://gitlab.example.com/backend/service-a/-/blob/master/deploy/prod/values.yml#L42

Суммарный отчёт

Он показывает:

  • сколько проектов затронуто;

  • сколько всего вхождений;

  • сколько найдено через API;

  • сколько найдено через Deep Search.

Такой формат оказался удобен не только для самой проверки, но и для дальнейшей работы: результат можно быстро копировать в задачу, тикет или внутреннюю документацию.


Что дало основной выигрыш по времени

Здесь не было одной “волшебной кнопки”. Сработала комбинация из нескольких решений.

Что сделал

Что это дало

Не сканировал все файлы подряд

Убрал самый тяжёлый сценарий

Код оставил на API Search

Быстрый поиск там, где он и так работает

Конфиги вынес в Deep Search

Добрал пропущенные совпадения

Добавил параллельность

Сократил общее время обработки

Отсёк архивные проекты

Не тратил время впустую

Если совсем коротко: я не сделал поиск “умнее”, я сделал его более прагматичным. И этого уже хватило.


Ограничения текущей версии

Чтобы статья не выглядела как “я написал идеальный инструмент”, лучше честно проговорить ограничения.

Ограничение

Что это значит

Поиск только по master

Пока не ищу по всем веткам

Deep Search ограничен по типам файлов

Это сделано ради скорости

Нет regex

Ищу точные вхождения

Нет дедупликации API/Deep

Одинаковые находки можно улучшить позже

Нет отдельного UI

Сейчас это консольный инструмент


Где такой подход реально полезен

Такой вариант хорошо работает, когда нужно быстро проверить большую группу репозиториев по конкретным терминам.

Например:

  • найти использование API-ручки перед изменением контракта;

  • проверить, где ещё осталась env-переменная;

  • собрать список сервисов, где используется старый URL;

  • понять, в каких конфигах ещё не обновлён параметр;

  • быстро оценить влияние изменения какого-то внешнего сервиса.

Особенно хорошо такой подход заходит в сценариях “нужно проверить сейчас”, а не “давайте строить отдельную поисковую платформу на квартал”.


Что можно улучшить дальше

Следующая версия напрашивается сама собой:

  • определять default branch автоматически;

  • добавить поиск по нескольким веткам;

  • поддержать regex;

  • дедуплицировать результаты;

  • добавить CSV / JSON-экспорт;

  • аккуратнее обработать rate limits и retries;

  • сделать небольшой CLI или web-интерфейс.

То есть потенциал роста есть. Но даже текущая версия уже решает ту задачу, ради которой всё затевалось.


Итог

Встроенный поиск GitLab оказался хорош не везде одинаково: по коду — нормально, по конфигам — уже не так надёжно. Поэтому я не стал пытаться искать всё одним способом и сделал гибридный подход:

  • код — через API Search;

  • конфиги — через прямой обход файлов;

  • результаты — в один отчёт;

  • обработку — в несколько потоков.

В итоге поиск по 100+ GitLab-проектам перестал быть многочасовой рутиной и стал обычной технической операцией на несколько минут.

Главный вывод здесь довольно простой: не всегда нужно строить большой универсальный инструмент. Иногда достаточно честно разложить задачу на части и для каждой выбрать самый практичный способ.

Если у вас были похожие сценарии с GitLab-группами, конфигами или поиском по большой кодовой базе, интересно было бы сравнить подходы. В таких задачах чужие инженерные костыли обычно не менее полезны, чем свои.