English | Русский
Important
Форматируйте весь Rust код с помощью nightly rustfmt.
Подробнее
cargo +nightly fmt --Почему это важно?
Единообразное форматирование улучшает читаемость, уменьшает конфликты при слиянии и упрощает код-ревью. Это гарантирует, что код каждого члена команды соответствует единому стандарту.
Примеры и пояснения
Например, хорошо отформатированная кодовая база позволяет новым членам команды быстро понять структуру и логику проекта. Автоматическое форматирование экономит время и минимизирует стилистические споры при код-ревью.
Tip
Используйте следующую конфигурацию .rustfmt.toml для обеспечения единообразного форматирования в проекте.
Конфигурация
# Не добавлять запятую, если только один элемент
trailing_comma = "Never"
# Держать скобки на той же строке где возможно
brace_style = "SameLineWhere"
# Выравнивать поля структуры, если их длина ниже порога
struct_field_align_threshold = 20
# Форматировать комментарии внутри документации
wrap_comments = true
format_code_in_doc_comments = true
# Не сворачивать литералы структур в одну строку
struct_lit_single_line = false
# Максимальная ширина строки
max_width = 99
# Группировка импортов
imports_granularity = "Crate" # Группировать импорты по крейту
group_imports = "StdExternalCrate" # Разделять группы: std, внешние крейты, локальные
reorder_imports = true # Сортировать импорты внутри групп
# Включить нестабильные функции (только nightly)
unstable_features = trueПочему это важно?
Эта конфигурация обеспечивает ясность и консистентность. Она уменьшает ненужные различия в pull request'ах, упрощает код-ревью и гарантирует предсказуемость стиля и читаемости для всей команды.
Примеры и пояснения
use std::fmt; use std::io; use serde::Serialize;
struct Person {name:String,age:u32}
impl Person{
pub fn new(name:String,age:u32)->Self{
Self{name,age}
}
}use std::{fmt, io};
use serde::Serialize;
struct Person {
name: String,
age: u32,
}
impl Person {
pub fn new(name: String, age: u32) -> Self {
Self {
name,
age,
}
}
}Обратите внимание: импорты сгруппированы и отсортированы, поля структуры выровнены для читаемости, скобки расставлены консистентно. Это уменьшает шум в диффах и делает кодовую базу понятной как для новичков, так и для опытных разработчиков.
Important
Используйте ясные, описательные имена, отражающие назначение. Следуйте snake_case для переменных/функций, PascalCase для типов и SCREAMING_SNAKE_CASE для констант.
Подробнее
Описательные имена:
create_user_handler– OKcreate_user_service– OKcreate– NOcreate_user– NOСледуйте Rust snake_case для переменных и функций.
Используйте PascalCase для структур и енамов (например,
TransactionStatus).Константы должны быть в SCREAMING_SNAKE_CASE.
Почему описательные имена?
Описательные имена уменьшают неоднозначность, облегчают онбординг и улучшают поддерживаемость. Ясные имена делают очевидным, что делает функция или переменная, избегая недоразумений и конфликтов.
Примеры и пояснения
Например,
create_user_handlerуказывает, что функция отвечает за обработку создания пользователя в веб-контексте, тогда как общее имяcreateне дает контекста.
Important
Пишите чистый, поддерживаемый код. Избегайте ненужной сложности, паник и клонирования. Минимизируйте глобальное состояние и ограничьте использование :: операторами импорта. Не используйте файлы mod.rs.
Подробнее
- Пишите чистый и поддерживаемый код.
- Избегайте ненужной сложности.
- Избегайте ненужных
unwrap()иclone().- Минимизируйте глобальное состояние и побочные эффекты.
- Используйте
::только в операторах импорта.- Не используйте файлы
mod.rs.Примеры и пояснения
Вместо написания
some_option.unwrap(), предпочтите:let value = some_option.ok_or("Expected a value, but found None")?;Это правильно пробрасывает ошибки и избегает падения приложения. Аналогично, организуйте модули в отдельных файлах
module_name.rsвместо устаревших файловmod.rs, что упрощает структуру проекта и улучшает обнаружение модулей.
Note
Каждая ветка, коммит и PR должны соответствовать номеру GitHub Issue. Это обеспечивает автоматическую связь, чистую историю и полную отслеживаемость.
Подробнее
Создавайте ветку только с номером Issue: Имя ветки должно быть точно номером Issue. Пример:
git checkout -b 123Используйте авто-связывание в коммитах: Чтобы GitHub автоматически связывал коммиты с Issue, всегда начинайте сообщение коммита с
#и номера Issue с пробелом. Пример:#123 implement login session restore #123 fix null pointer in user handlerЗаголовок Pull Request = Имя ветки: Заголовок PR должен совпадать с именем ветки (просто номер Issue). Пример:
123Добавьте ссылку для автозакрытия: В описании PR всегда включайте:
Closes #123Это автоматически закрывает Issue при слиянии PR.
Очистка после слияния: Включите "Delete branch on merge" в настройках репозитория, чтобы слитые ветки автоматически удалялись. Цепочка Issue -> Branch -> Commits -> PR -> Merge остается полностью связанной.
Держите репозиторий чистым: Каждая ветка должна соответствовать активному Issue. Никаких осиротевших или экспериментальных веток после слияния.
Реальный пример и пояснение
Предположим, вам назначен Issue
#123для исправления бага сессии логина. Вы создаете ветку123и начинаете коммитить с сообщениями:#123 implement login session restore #123 add retry logic for session token refreshЗатем открываете PR с заголовком
123и описанием:Closes #123Когда PR слит, GitHub автоматически закрывает Issue #123, удаляет ветку и показывает все связанные коммиты в таймлайне Issue. Это создает идеально отслеживаемый и автоматизированный рабочий процесс с минимумом ручных шагов.
Tip
Следуйте лучшим практикам для поддержания высокого качества кода.
Подробнее
- Используйте
cargo clippyдля линтинга.- Обрабатывайте ошибки грациозно с помощью
ResultиOption.- Избегайте ненужных паник.
Примеры и пояснения
Вместо написания:
let value = some_option.unwrap();используйте:
let value = some_option.ok_or("Expected a value, but found None")?;Этот паттерн гарантирует, что ошибки пробрасываются и обрабатываются должным образом, увеличивая надежность вашего приложения.
Important
Избегайте паник в продакшене; используйте правильную обработку ошибок с Result и оператором ?.
Подробнее
- Избегайте паник в продакшен коде.
- Не рекомендуется: Избегайте
unwrap()иexpect(), если не уверены абсолютно, что ошибка не может произойти.- Рекомендуется: Используйте правильную обработку ошибок с
Resultи оператором?.Примеры и пояснения
Например, вместо:
let config = Config::from_file("config.toml").unwrap();используйте:
let config = Config::from_file("config.toml") .map_err(|e| format!("Failed to load config: {}", e))?;Этот подход логирует детальные сообщения об ошибках и грациозно пробрасывает ошибки вверх по стеку вызовов, приводя к более надежной и поддерживаемой системе.
Реальный инцидент: падение Cloudflare (ноябрь 2025)
18 ноября 2025 года один вызов
.unwrap()в Rust коде вызвал массовый сбой в 330+ дата-центрах Cloudflare. Сервисы ChatGPT, X, Canva и многие другие были недоступны около 3 часов.Причина: изменение конфигурации привело к тому, что файл фич содержал больше записей, чем ожидалось. Rust код проверял лимит, но использовал
unwrap()на пути ошибки вместо грациозной обработки. При превышении лимита код запаниковал с сообщением:"thread fl2_worker_thread panicked: called Result::unwrap() on an Err value"Урок: Этот
.unwrap()был в кодовой базе долгое время, но никогда не срабатывал, пока неожиданные входные данные не достигли этого пути в коде. Вот почему продакшен код должен явно обрабатывать все случаи ошибок.
Important
Все коммиты должны проходить pre-commit проверки. Тесты, форматирование, линтинг и сканирование безопасности применяются как локально (через pre-commit хуки), так и удаленно (через CI).
Подробнее
Pre-commit хуки
- Устанавливаются через:
cargo make install-hooks- Автоматически запускаются перед каждым коммитом:
cargo +nightly fmt --cargo clippy -D warningscargo test --all- Предотвращают коммит неотформатированного кода, предупреждений или падающих тестов.
Unit тесты
- Покрывают публичные функции и случаи ошибок.
- Тесты не должны полагаться на
unwrap()илиexpect().Интеграционные тесты
- Покрывают публичный API, размещаются в директории
tests/.Doctests
- Все примеры
///должны компилироваться и проходить сcargo test --doc.Coverage (cargo-llvm-cov + Codecov)
- Установка:
cargo install cargo-llvm-cov- Локальный запуск:
cargo llvm-cov --all-features --workspace --html- Конфигурация CI:
- name: Install cargo-llvm-cov uses: taiki-e/install-action@cargo-llvm-cov - name: Generate code coverage run: cargo llvm-cov --all-features --workspace --codecov --output-path codecov.json - name: Upload coverage to Codecov uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} files: codecov.json fail_ci_if_error: trueПочему cargo-llvm-cov + Codecov?
- Точность: LLVM-инструментирование обеспечивает точное покрытие строк и веток, надежнее source-based инструментов
- Скорость: Значительно быстрее tarpaulin, особенно на больших кодовых базах с множеством зависимостей
- Нативный формат: Прямой вывод в codecov.json без промежуточных шагов конвертации
- Визуализация: Дашборд Codecov показывает тренды покрытия, diff покрытия в PR и интерактивные sunburst-диаграммы
- Интеграция с PR: Автоматические отчеты о покрытии в комментариях PR, показывающие какие именно строки покрыты/не покрыты
- Branch protection: Настройка минимальных порогов покрытия для падения CI при снижении coverage
- Rust toolchain: Использует встроенное инструментирование rustc, гарантируя совместимость со всеми фичами Rust
Примеры и пояснения
#[cfg(test)] mod tests { use super::*; #[test] fn test_basic_math() { assert_eq!(2 + 2, 4); } }// tests/config_tests.rs use my_crate::load_config; #[test] fn load_valid_config() { let result = load_config("tests/data/valid.toml"); assert!(result.is_ok()); }Этот рабочий процесс обеспечивает корректность на каждом шаге: разработчики не могут закоммитить сломанный код, а CI гарантирует, что ничего не проскользнет при слиянии.
Note
Никаких инлайн комментариев в коде. Все пояснения находятся в doc-блоках, прикрепленных к модулям, структурам, енамам, трейтам, функциям и методам.
Подробнее
Никаких строчных комментариев в коде: Избегайте
// ...и/* ... */для объяснения поведения, намерений или инвариантов. Держите код чистым и самоочевидным.Используйте Rust doc комментарии консистентно:
- Крейт/модуль: используйте
//!в началеlib.rsили файлов модулей для документации уровня модуля.- Элементы (структуры, енамы, трейты, функции, методы): используйте
///на элементе.Структурируйте doc-блоки для IDE и LSP: Используйте заголовки, которые понимает Rustdoc, чтобы всплывающие подсказки и Treesitter outlines были стабильными и информативными:
# Overviewкраткое назначение# Examplesминимальные, компилируемые примеры# Errorsточные режимы сбоя дляResult# Panicsтолько если неизбежно (должно быть редко)# Safetyесли используетсяunsafe(не должно быть)# Performanceесли важна сложность или аллокацииПишите для других инженеров: Будьте явными о контрактах, входах, выходах, инвариантах и граничных случаях. Держите примеры запускаемыми. Предпочитайте ясность изощренности.
Держите документацию рядом с кодом: Обновляйте doc-блоки вместе с изменениями кода в том же PR. Устаревшая документация хуже, чем её отсутствие.
Правильно vs Неправильно (Rust)
Неправильно (инлайн комментарии, которые не появятся во всплывающих подсказках):
// Calculates checksum and validates header // Returns Err if invalid pub fn verify(pkt: &Packet) -> Result<(), VerifyError> { // fast path if pkt.header.len() < MIN { return Err(VerifyError::TooShort); } // slow path... Ok(()) }Правильно (doc-блоки; IDE hover показывает контракт):
/// # Overview /// Verifies packet header and payload consistency. /// /// # Examples /// ``` /// # use mynet::{Packet, verify}; /// # fn demo(mut p: Packet) { /// # // prepare p... /// # let _ = verify(&p).unwrap(); /// # } /// ``` /// /// # Errors /// - `VerifyError::TooShort` when header is smaller than the required minimum. /// - `VerifyError::ChecksumMismatch` when computed checksum differs. pub fn verify(pkt: &Packet) -> Result<(), VerifyError> { if pkt.header.len() < MIN { return Err(VerifyError::TooShort); } // internal micro-notes for maintainers are allowed if they aid refactoring // (but not to explain business logic). Keep them brief. Ok(()) }Документация уровня модуля вместо баннера комментариев:
//! Cryptographic key management and signing primitives. //! //! Provides deterministic ECDSA with explicit domain separation. //! //! # Examples //! ``` //! # use keys::{Keypair, Signer}; //! # fn demo() { //! # let kp = Keypair::generate(); //! # let sig = kp.sign(b"payload"); //! # assert!(kp.verify(b"payload", &sig).is_ok()); //! # } //! ``` pub mod crypto { /* ... */ }Реальное обоснование
Эта политика обеспечивает стабильные всплывающие подсказки IDE/LSP, лучшие Treesitter outlines и надежную навигацию. Инженеры сразу видят контракты, CI может линтить документацию, а примеры остаются компилируемыми. Код остается чистым, а документация — обнаруживаемой и точной.
Tip
Используйте комплексную методологию код-ревью для систематического поиска уязвимостей, проблем производительности и качества.
Подробнее
Доступно на двух языках:
Быстрые ссылки:
Тема EN RU Шпаргалка Cheat Sheet Шпаргалка Безопасность Vulnerabilities Уязвимости Производительность Issues Проблемы Качество кода Quality Качество Rust паттерны Specifics Специфика Примеры Real Cases Примеры Что покрыто
Уязвимости безопасности:
- Replay атаки и обход аутентификации
- SQL/Command инъекции
- Утечки секретов и проблемы криптографии
- Проблемы валидации входных данных
Проблемы производительности:
- Неэффективные аллокации и ненужное клонирование
- O(n^2) алгоритмы где возможен O(n)
- Дублирование операций и двойной парсинг
- Блокирующие операции в async коде
Качество кода:
- Нарушения DRY и дублирование кода
- Именование и читаемость
- Стандарты документации
- Покрытие тестами
Rust-специфика:
- Паттерны ownership и borrowing
- Обработка Panic vs Result
- Ревью unsafe кода
- Trait bounds и generics
Быстрый 5-минутный чеклист
Безопасность (2 мин):
- Нет секретов в коде
- Нет
unwrap()/expect()в продакшене- Входные данные валидируются
- Нет SQL/Command инъекций
Производительность (1 мин):
- Нет очевидных O(n^2)
- Нет дублирования операций
Vec::with_capacity()где нужноКачество (2 мин):
- Нет дублирования кода (> 3 раз)
- Функции < 50 строк
- Тесты для новой логики
Important
Используйте cargo-chef для кэширования слоёв Docker и registry cache для CI. Это кардинально сокращает время сборки при неизменных зависимостях.
Подробнее
Проблема:
- Компиляция Rust медленная, особенно при большом дереве зависимостей
- Docker пересобирает всё при любом изменении файла
--mount=type=cacheне сохраняется между CI раннерами- Каждый запуск CI начинается с нуля без правильного кэширования
Решение: cargo-chef + Registry Cache
- cargo-chef отделяет компиляцию зависимостей от компиляции исходников
- Registry cache сохраняет слои Docker между запусками CI
- Зависимости кэшируются в отдельный слой, который пересобирается только при изменении Cargo.toml/Cargo.lock
Паттерн Dockerfile
# syntax=docker/dockerfile:1 ARG RUST_VERSION=1.83.0 # Chef stage - установка cargo-chef FROM rust:${RUST_VERSION} AS chef RUN cargo install cargo-chef --locked WORKDIR /app # Planner - создание recipe только из зависимостей FROM chef AS planner COPY Cargo.toml Cargo.lock ./ COPY my-crate/Cargo.toml my-crate/ COPY crates crates/ RUN cargo chef prepare --recipe-path recipe.json # Builder - сборка зависимостей, затем исходников FROM chef AS builder # Сборка зависимостей (кэшируется если recipe.json не изменился) COPY --from=planner /app/recipe.json recipe.json RUN cargo chef cook --release --recipe-path recipe.json # Сборка приложения (только этот слой пересобирается при изменении кода) COPY . . RUN cargo build --release && strip target/release/my-binary # Runtime - минимальный образ FROM debian:bookworm-slim COPY --from=builder /app/target/release/my-binary /usr/local/bin/ CMD ["my-binary"]Ключевые моменты:
- Planner stage копирует только Cargo.toml файлы (не исходный код)
cargo chef prepareсоздаёт recipe.json из зависимостейcargo chef cookкомпилирует зависимости - этот слой кэшируется- Исходный код копируется после сборки зависимостей
- Только финальный
cargo buildперекомпилируется при изменении кодаПаттерн GitHub Actions CI
jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - uses: docker/setup-buildx-action@v3 - uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ secrets.REGISTRY_USERNAME }} password: ${{ secrets.REGISTRY_TOKEN }} - name: Build image uses: docker/build-push-action@v6 with: context: . file: ./Dockerfile push: false load: true tags: ${{ env.REGISTRY }}/my-image:${{ env.TAG }} cache-from: | type=registry,ref=${{ env.REGISTRY }}/my-image:cache cache-to: | type=registry,ref=${{ env.REGISTRY }}/my-image:cache,mode=maxКлючевые моменты:
cache-fromскачивает закэшированные слои из registry перед сборкойcache-toзагружает новые слои кэша после сборкиmode=maxкэширует все промежуточные слои (не только финальный)- Тег кэша отдельный от тегов образа (например,
:cache)- Работает на разных CI раннерах и ветках
Чего НЕ делать
Не используйте
--mount=type=cacheдля CI:# ПЛОХО - кэш не сохраняется между CI раннерами RUN --mount=type=cache,target=/usr/local/cargo/registry \ cargo build --releaseНе копируйте все исходники до зависимостей:
# ПЛОХО - любое изменение файла инвалидирует кэш зависимостей COPY . . RUN cargo build --releaseНе используйте GHA cache для больших Rust сборок:
# ПЛОХО - GHA cache имеет лимит 10GB, target/ Rust легко его превышает - uses: actions/cache@v4 with: path: target/ key: rust-${{ hashFiles('Cargo.lock') }}Влияние на производительность
Сценарий Без кэширования С cargo-chef + Registry Cache Первая сборка 15-30 мин 15-30 мин Только изменение кода 15-30 мин 2-5 мин Изменение зависимостей 15-30 мин 15-30 мин Без изменений 15-30 мин 30 сек - 1 мин Ключевой инсайт: большинство CI запусков меняют только код приложения, не зависимости. С правильным кэшированием такие сборки пропускают 90%+ времени компиляции.
Tip
Помимо базовых тестов и линтеров, профессиональные Rust проекты должны включать проверку лицензий, стабильности API, MSRV и аудит зависимостей в CI.
Подробнее
Инструмент Назначение Когда использовать cargo-denyСоответствие лицензий, дублирующиеся зависимости, security advisories Любой проект с зависимостями cargo-semver-checksОбнаружение ломающих изменений API Библиотеки публикуемые на crates.io MSRV check Проверка минимальной поддерживаемой версии Rust Проекты с rust-versionв Cargo.tomlcargo-macheteПоиск неиспользуемых зависимостей Уменьшение bloat, быстрее сборка Doctests Проверка компилируемости примеров в документации Проекты с ///doc комментариямиcargo-qualityКачество кода с hardcoded стандартами Zero-config проверка качества rust-diff-analyzerСемантический анализ размера PR Контроль обозримости PR sql-query-analyzerСтатический анализ SQL + LLM оптимизация Проекты с SQL запросами cargo-deny: Лицензии и безопасность
Установка:
cargo install cargo-denyКонфигурация (
deny.toml):[advisories] db-path = "~/.cargo/advisory-db" vulnerability = "deny" unmaintained = "warn" yanked = "deny" [licenses] allow = ["MIT", "Apache-2.0", "BSD-3-Clause", "ISC", "Zlib"] copyleft = "deny" unlicensed = "deny" [bans] multiple-versions = "warn" wildcards = "deny" [sources] unknown-registry = "deny" unknown-git = "deny"Интеграция в CI:
- name: Check licenses and advisories run: cargo deny checkПочему это важно:
- Предотвращает случайные GPL/AGPL зависимости в MIT проектах
- Ловит известные уязвимости (RustSec)
- Предупреждает о дублирующихся версиях зависимостей (bloat)
cargo-semver-checks: Стабильность API
Установка:
cargo install cargo-semver-checksИспользование:
# Сравнение с последней опубликованной версией cargo semver-checks check-release # Сравнение с конкретной версией cargo semver-checks check-release --baseline-version 1.2.0Интеграция в CI:
- name: Check semver compliance if: github.event_name == 'pull_request' run: | cargo install cargo-semver-checks cargo semver-checks check-releaseЧто отлавливает:
- Удаление публичных функций/типов (breaking)
- Изменение сигнатур функций (breaking)
- Добавление обязательных полей в структуры (breaking)
- Изменение вариантов enum (breaking)
Когда использовать: Любая библиотека публикуемая на crates.io, от API которой зависят пользователи.
MSRV Check: Минимальная поддерживаемая версия Rust
В Cargo.toml:
[package] rust-version = "1.83" # MSRV edition = "2024"Интеграция в CI:
jobs: msrv: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - name: Extract MSRV id: msrv run: | MSRV=$(grep '^rust-version' Cargo.toml | sed 's/.*"\(.*\)"/\1/') echo "version=$MSRV" >> $GITHUB_OUTPUT - uses: dtolnay/rust-toolchain@master with: toolchain: ${{ steps.msrv.outputs.version }} - run: cargo check --all-featuresПочему это важно:
- Edition 2024 требует Rust 1.85+
- Пользователи на старых версиях Rust получают понятные ошибки
- Предотвращает случайное использование новых фич
cargo-machete: Неиспользуемые зависимости
Установка:
cargo install cargo-macheteИспользование:
cargo macheteИнтеграция в CI:
- name: Check for unused dependencies run: | cargo install cargo-machete cargo macheteПреимущества:
- Быстрее время компиляции
- Меньше размер бинарника
- Уменьшенная поверхность атаки
- Чище дерево зависимостей
Doctests: Примеры в документации
Запуск doctests явно:
cargo test --docИнтеграция в CI:
- name: Run doctests run: cargo test --doc --all-featuresПример doctest:
/// Вычисляет сумму двух чисел. /// /// # Examples /// /// ``` /// use mylib::add; /// assert_eq!(add(2, 3), 5); /// ``` pub fn add(a: i32, b: i32) -> i32 { a + b }Зачем отдельные doctests:
cargo testзапускает unit + integration + doc тесты вместе- Doctests часто требуют другие feature flags
- Быстрее фидбек когда меняется документация, но не код
cargo-quality: Zero-Config проверка качества
Проблема:
- Команды разбрасывают
.rustfmt.toml,.clippy.tomlпо репозиториям- В разных проектах разные стандарты
- Новые разработчики не знают какие правила применяются
Решение: Все стандарты зашиты в один бинарник. Установил один раз — используешь везде.
Установка:
cargo install cargo-qualityКоманды:
cargo qual check src/ # Анализ без изменений cargo qual fix --dry-run # Предпросмотр исправлений cargo qual fix # Применить исправления cargo qual fmt # Форматирование (max_width: 99)Четыре анализатора:
Анализатор Обнаруживает Авто-фикс path_importПрямые пути модулей, которые должны быть импортами Да format_argsПозиционные аргументы в format макросах Да empty_linesПустые строки в функциях (признак сложности) Да inline_commentsКомментарии, которые должны быть doc-блоками Нет Интеграция в CI:
- uses: RAprogramm/cargo-quality@v0 with: path: 'src/' fail_on_issues: 'true' post_comment: 'true'Почему cargo-quality:
- Единый источник истины для всех репозиториев
- Ловит паттерны, которые rustfmt/clippy пропускают
- 86% покрытие тестами
rust-diff-analyzer: Семантический анализ PR
Проблема:
- Лимиты по количеству строк бессмысленны (500 строк тестов ≠ 500 строк prod)
- Большие PR скрывают баги и замедляют ревью
- Тестовый код не должен учитываться в размере PR
Решение: AST-анализ, который понимает семантику Rust кода.
Установка:
cargo install rust-diff-analyzerИспользование:
git diff main | rust-diff-analyzer rust-diff-analyzer --diff-file changes.diff --max-units 50Система взвешенных баллов:
Тип единицы Public Private Function 3 1 Struct 3 1 Trait 4 4 Impl Block 2 2 Умная классификация:
tests/,benches/,examples/→ тестовый код (исключён)#[test],#[cfg(test)]→ тестовый код (исключён)- Всё остальное → production код (учитывается в лимитах)
Интеграция в CI:
- uses: RAprogramm/rust-prod-diff-checker@v1 with: max_prod_units: 30 max_weighted_score: 100 fail_on_exceed: 'true' post_comment: 'true'Почему семантический анализ:
- 100 строк тестов ≠ 100 строк бизнес-логики
- Изменения публичного API требуют больше внимания
- Управление размером PR на основе данных
sql-query-analyzer: Статический анализ SQL
Проблема:
- SQL баги обнаруживаются в продакшене (отсутствующие индексы, N+1)
- Проблемы безопасности (UPDATE без WHERE) проходят через ревью
- Нет анализа со знанием схемы в существующих инструментах
Решение: 18 детерминированных правил + опциональная LLM-оптимизация.
Установка:
cargo install sql-query-analyzerИспользование:
# Статический анализ (мгновенно, без API ключа) sql-query-analyzer analyze -s schema.sql -q queries.sql # SARIF для GitHub Code Scanning sql-query-analyzer analyze -s schema.sql -q queries.sql -f sarif > results.sarif18 встроенных правил:
Категория Правила Примеры Performance (11) PERF001-011 Unbounded SELECT, leading wildcards, N+1 Security (2) SEC001-002 UPDATE/DELETE без WHERE Style (2) STYLE001-002 SELECT *, отсутствующие алиасы Schema (3) SCHEMA001-003 Отсутствующие индексы, невалидные колонки Интеграция в CI:
- uses: RAprogramm/sql-query-analyzer@v1 with: schema: db/schema.sql queries: db/queries.sql upload-sarif: 'true' post-comment: 'true'Почему sql-query-analyzer:
- Знает схему (ваши индексы и колонки)
- Ловит N+1 паттерны до продакшена
- ~1000 запросов за <100мс (параллелизм rayon)
Ссылки: GitHub
Полный Quality Gate в CI
jobs: quality: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@stable with: components: clippy, rustfmt - name: Format check run: cargo +nightly fmt -- --check - name: Clippy run: cargo clippy --all-targets -- -D warnings - name: Tests run: cargo test --all-features - name: Doctests run: cargo test --doc --all-features - name: Unused dependencies run: | cargo install cargo-machete cargo machete - name: License & security run: | cargo install cargo-deny cargo deny check # Качество кода (архитектурные паттерны) - uses: RAprogramm/cargo-quality@v0 with: fail_on_issues: 'true' post_comment: 'true' msrv: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@1.83.0 - run: cargo check --all-features semver: if: github.event_name == 'pull_request' runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@stable - run: | cargo install cargo-semver-checks cargo semver-checks check-release pr-size: if: github.event_name == 'pull_request' runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 with: fetch-depth: 0 - uses: RAprogramm/rust-prod-diff-checker@v1 with: max_prod_units: 30 max_weighted_score: 100 fail_on_exceed: 'true' post_comment: 'true' sql-analysis: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - uses: RAprogramm/sql-query-analyzer@v1 with: schema: db/schema.sql queries: db/queries/ upload-sarif: 'true'
Important
Используйте один файл CI workflow с множеством jobs вместо множества отдельных workflow файлов. Это обеспечивает лучший контроль, видимость и управление ресурсами.
Подробнее
Проблемы множества Workflows:
- Нет способа синхронизировать jobs между разными workflows
- Нельзя определить зависимости (Job C запускается после Job A и B)
- Сложнее управлять конкурентностью и отменой
- Дублирование конфигурации триггеров между файлами
- Распределённая логика CI усложняет отладку
- Множество запусков workflow на один коммит тратят больше ресурсов
Решение: Один Workflow с множеством Jobs
- Один файл workflow содержит всю CI/CD логику
- Jobs обрабатывают разные задачи (тест, сборка, деплой)
needsопределяет зависимости между jobs- Reusable workflows (
_*.yml) извлекают общие паттерны- Concurrency groups предотвращают дублирование запусков
Архитектурный паттерн
.github/workflows/ ├── ci.yml # Главный CI workflow (триггерится на push/PR) ├── _build-service.yml # Reusable: сборка Docker образа ├── _deploy-service.yml # Reusable: деплой в k8s └── _quality-check.yml # Reusable: запуск тестов/линтеровКлючевой принцип: Файлы начинающиеся с
_- это reusable workflows, вызываемые черезuses:. Толькоci.ymlопределяет триггеры.Зависимости Jobs с `needs`
jobs: detect-changes: runs-on: ubuntu-latest outputs: api: ${{ steps.filter.outputs.api }} client: ${{ steps.filter.outputs.client }} quality-check: needs: [detect-changes] if: needs.detect-changes.outputs.api == 'true' build-api: needs: [detect-changes, quality-check] if: | always() && needs.detect-changes.outputs.api == 'true' && needs.quality-check.result == 'success' deploy-api: needs: [build-api] if: needs.build-api.result == 'success'Ключевые моменты:
needsсоздаёт цепочку зависимостей- Jobs выполняются параллельно, если
needsне задаёт порядок- Используйте
if: always()для запуска даже если зависимости были пропущены- Проверяйте
needs.<job>.resultдля условного выполненияКонтроль конкурентности
name: CI/CD Pipeline on: push: branches: [main] pull_request: concurrency: group: ci-${{ github.ref }} cancel-in-progress: trueЧто это делает:
- Группирует запуски по ветке/PR (
github.ref)- Новый push отменяет предыдущий запущенный workflow
- Предотвращает трату ресурсов на устаревшие коммиты
- Только один активный запуск на ветку в момент времени
Reusable Workflows
Основной workflow вызывает reusable:
# ci.yml jobs: build-api: uses: ./.github/workflows/_build-service.yml with: service_name: api-server dockerfile: ./api-server/Dockerfile secrets: registry_token: ${{ secrets.REGISTRY_TOKEN }}Определение reusable workflow:
# _build-service.yml name: Build Service on: workflow_call: inputs: service_name: required: true type: string dockerfile: required: true type: string secrets: registry_token: required: true outputs: image_tag: value: ${{ jobs.build.outputs.tag }} jobs: build: runs-on: ubuntu-latest outputs: tag: ${{ steps.meta.outputs.tag }} steps: # ... логика сборкиПреимущества:
- DRY: одна логика сборки для всех сервисов
- Inputs/outputs для конфигурации
- Секреты передаются явно (безопасность)
- Легко обновить в одном месте
Независимые сборки сервисов
jobs: build-api: needs: [detect-changes, quality-api] if: needs.detect-changes.outputs.api == 'true' uses: ./.github/workflows/_build-service.yml build-client: needs: [detect-changes, quality-client] if: needs.detect-changes.outputs.client == 'true' uses: ./.github/workflows/_build-service.yml deploy-api: needs: [build-api] # Зависит только от своей сборки if: needs.build-api.result == 'success' deploy-client: needs: [build-client] # Независим от api if: needs.build-client.result == 'success'Ключевой принцип: Деплой каждого сервиса зависит только от своей сборки, не от других сервисов. Если сборка api-server падает, client всё равно может задеплоиться.
Чего НЕ делать
Не создавайте отдельные файлы workflow для каждой задачи:
# ПЛОХО - синхронизация невозможна .github/workflows/ ├── test.yml ├── build-api.yml ├── build-client.yml ├── deploy-api.yml ├── deploy-client.yml └── cleanup.ymlНе делайте все деплои зависимыми от всех сборок:
# ПЛОХО - client ждёт api даже если не связаны deploy-client: needs: [build-api, build-client, build-worker]Не пропускайте контроль конкурентности:
# ПЛОХО - множество запусков тратят ресурсы on: push: branches: [main] # Отсутствует: concurrency groupПолный пример структуры
name: CI/CD Pipeline on: push: branches: [main] workflow_dispatch: inputs: deploy_all: type: boolean default: false concurrency: group: ci-${{ github.ref }} cancel-in-progress: true jobs: # 1. Определяем что изменилось detect-changes: runs-on: ubuntu-latest outputs: api: ${{ steps.filter.outputs.api }} client: ${{ steps.filter.outputs.client }} steps: - uses: dorny/paths-filter@v3 id: filter with: filters: | api: - 'api-server/**' client: - 'client/**' # 2. Quality gates (параллельно) quality-api: needs: [detect-changes] if: needs.detect-changes.outputs.api == 'true' uses: ./.github/workflows/_quality-check.yml quality-client: needs: [detect-changes] if: needs.detect-changes.outputs.client == 'true' uses: ./.github/workflows/_quality-check.yml # 3. Сборка (после quality) build-api: needs: [detect-changes, quality-api] if: needs.quality-api.result == 'success' uses: ./.github/workflows/_build-service.yml build-client: needs: [detect-changes, quality-client] if: needs.quality-client.result == 'success' uses: ./.github/workflows/_build-service.yml # 4. Деплой (независимо для каждого сервиса) deploy-api: needs: [build-api] if: needs.build-api.result == 'success' uses: ./.github/workflows/_deploy-service.yml deploy-client: needs: [build-client] if: needs.build-client.result == 'success' uses: ./.github/workflows/_deploy-service.yml
Следование этим рекомендациям гарантирует, что наш Rust код будет высококачественным, поддерживаемым и масштабируемым.