Налаштований CI/CD-конвеєр для Solana-програми автоматично перевіряє, чи збирається код під ціль BPF, чи проходять інтеграційні тести з local validator, чи відповідає код стилістичним стандартам і чи не регресує покриття тестами. Нижче — повна конфігурація GitHub Actions для Anchor-проєкту з поясненням кожного етапу, обмежень та планом відкату.
Конфігурація GitHub Actions для Anchor
Передумови та середовище
Конвеєр розрахований на репозиторій зі стандартною структурою Anchor-проєкту (папки programs/, tests/, файл Anchor.toml). Збірка відбувається на Ubuntu runner у GitHub Actions. Залежності: Rust з ціллю bpfel-unknown-unknown, Solana CLI, Anchor CLI, Node.js (для тестового харнесу на TypeScript).
Перед створенням конфігурації переконайтеся, що локально встановлені версії Solana CLI та Anchor CLI збігаються з тими, що вказані в конвеєрі. Розбіжність версій — найчастіша причина хибних падінь у CI.
Повний робочий процес
name: Solana CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
env:
SOLANA_VERSION: "1.18.0"
ANCHOR_VERSION: "0.30.0"
NODE_VERSION: "20"
jobs:
lint-and-format:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: bpfel-unknown-unknown
- name: Check formatting
run: cargo fmt --all -- --check
- name: Run Clippy
run: cargo clippy --target bpfel-unknown-unknown -- -D warnings
build-and-test:
runs-on: ubuntu-latest
needs: lint-and-format
steps:
- uses: actions/checkout@v4
- name: Cache Solana CLI
uses: actions/cache@v4
with:
path: ~/.local/share/solana
key: solana-${{ env.SOLANA_VERSION }}
- name: Install Solana CLI
run: sh -c "$(curl -sSfL https://release.anza.xyz/v${SOLANA_VERSION}/install)"
- name: Install Anchor
run: cargo install anchor-cli --version ${{ env.ANCHOR_VERSION }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- name: Install JS dependencies
run: npm ci
- name: Build programs
run: anchor build
- name: Run tests
run: anchor test --skip-build
Ключові рішення та їхнє обґрунтування
- Розділення на два jobs. Лінтинг і форматування швидкі (30–60 секунд), тоді як збірка Anchor CLI з вихідного коду займає 3–8 хвилин. Якщо форматування не пройшло, немає сенсу витрачати час на збірку.
- Кешування Solana CLI. Бінарні файли Solana важать близько 200 МБ. Без кешу кожен запуск завантажує їх заново, що збільшує час конвеєра на 30–60 секунд.
- --skip-build у anchor test. Програма вже зібрана на попередньому кроці. Повторна збірка подвоює час тестування без жодної додаткової перевірки.
- Залежність needs: lint-and-format. Тести запускаються лише за умови, що код відповідає стилістичним стандартам. Це змушує розробників виправляти форматування перед злиттям.
Очікуваний результат та перевірка
Після пушу в гілку main або develop або при відкритті PR конвеєр має пройти обидва jobs із зеленим статусом. У логах build-and-test ви маєте побачити вихід anchor test із результатами кожного тест-кейсу. Якщо хочете локально відтворити те саме середовище, виконайте команди з кроків послідовно у чистій Ubuntu (наприклад, у Docker-контейнері ubuntu:22.04).
План відкату
Якщо конвеєр почав хибно падіти після оновлення версій у env, відкотіть коміт із змінами SOLANA_VERSION або ANCHOR_VERSION. Якщо проблема в самій конфігурації — видаліть файл .github/workflows/solana-ci.yml і повторіть пуш. Збірка та розгортання програми на кластер не залежать від цього конвеєра, тому відкат CI не впливає на працюючу інфраструктуру.
Запуск local validator у CI
Команда anchor test за замовчуванням автоматично запускає solana-test-validator, розгортає ваші програми та виконує тести з папки tests/. Проте в CI-середовищі це працює інакше, ніж на локальній машині, і вимагає уваги до кількох параметрів.
Чому local validator у CI відрізняється від локального
- Обмеження пам'яті. GitHub Actions runner має 7 ГБ RAM. solana-test-validator за замовчуванням резервує значну частину пам'яті. Якщо тести створюють багато акаунтів або завантажують великі стейти, валідатор може бути вбитий OOM-killer'ом.
- Час старту. У CI валідатор стартує повільніше через обмеження CPU на віртуальній машині. Типовий старт — 5–15 секунд, але з великими програмами або завантаженими акаунтами може сягати 30+ секунд.
- Порти. У CI середовищі порт 8899 (за замовчуванням) зазвичай вільний, але якщо ви паралельно запускаєте кілька конвеєрів на self-hosted runner, виникне конфлікт.
Налаштування валідатора через Anchor.toml
Замість того, щоб передавати аргументи безпосередньо в CLI, налаштуйте валідатор у Anchor.toml — так конфігурація буде однаковою для локального запуску та CI:
[test]
startup_wait = 10000
[test.validator]
rpc_port = 8899
ledger = ".anchor/test-ledger"
[[test.validator.clone]]
address = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"
Параметр startup_wait вказує Anchor чекати 10 секунд перед запуском тестів. У CI це критично: без достатнього очікування тести почнуть виконуватись до того, як валідатор стане готовим приймати RPC-запити, і ви отримаєте помилку з'єднання, а не реальну помилку логіки програми.
Секція [[test.validator.clone]] клонує вказаний акаунт з devnet у локальний валідатор. Це необхідно, якщо ваші тести взаємодіють із існуючими токен-майдаками або програмами, які не розгортаються вашим Anchor-проєктом. Перевірте актуальність адреси програми в офіційному репозиторії Solana Program Library перед додаванням.
Типова помилка: Connection refused замість логічної помилки тесту
Якщо ви бачите Error: connect ECONNREFUSED 127.0.0.1:8899 у логах CI, це не означає, що валідатор не запустився. Найчастіша причина — startup_wait занадто малий для CI-середовища. Збільште значення до 15000 або 20000 мілісекунд і повторіть запуск. Не намагайтеся вирішити це retry-логікою в тестах: проблема не в мережі, а в часі ініціалізації.
Межі застосування
Local validator у CI підходить для інтеграційного тестування логіки вашої програми. Він не відтворює умови production-кластера: немає мережевої латентності між валідаторами, немає конкурентних транзакцій від інших користувачів, немає реального слотингу. Якщо вам потрібно відтворити production-помилку, пов'язану з умовами кластера, зверніться до матеріалу про те, як відтворити production-помилку в local validator.
Автоматичне перевірка coverage
Цей розділ вимагає чесного обговорення обмежень, оскільки екосистема Solana наразі не має production-ready інструменту для вимірювання покриття коду BPF-програм.
Чому стандартні інструменти не працюють
Solana-програми компілюються в ціль bpfel-unknown-unknown (Berkeley Packet Filter). Інструменти покриття для Rust — cargo-tarpaulin та cargo-llvm-cov — працюють лише з нативними цільовими платформами (x86_64, aarch64). Вони не можуть інструментувати BPF-бінарник, а Solana runtime не генерує профілювальну інформацію під час виконання програми. Будь-хто, хто пропонує вам "просто додати cargo-llvm-cov" для Solana-програми, пропонує непрацююче рішення.
Що реально можна зробити
Є два компромісні підходи, кожен із власними обмеженнями:
1. Покриття тестового харнесу (TypeScript/JavaScript)
Ваші тести в папці tests/ написані на TypeScript. Ви можете виміряти покриття цього коду за допомогою nyc (Istanbul). Це покаже, чи всі функції вашого тестового харнесу викликаються, але не покаже, які гілки Rust-коду в самій програмі були виконані.
npm install --save-dev nyc
nyc anchor test --skip-build
Додайте у package.json:
"scripts": {
"test:coverage": "nyc anchor test --skip-build"
}
2. Ручне інструментування через логування
Додайте умовні виклики msg! у критичні гілки логіки програми. Під час виконання тестів перевірте логи валідатора на наявність цих повідомлень. Це не дає відсоткового покриття, але дозволяє підтвердити, що конкретна гілка коду була досягнута.
Додавання перевірки в CI
Для покриття харнесу додайте крок у job build-and-test:
- name: Check test harness coverage
run: nyc --reporter=text --reporter=lcov anchor test --skip-build
- name: Enforce minimum coverage
run: |
COVERAGE=$(nyc --reporter=text anchor test --skip-build 2>&1 | grep "All files" | awk '{print $NF}' | tr -d '%')
if (( $(echo "$COVERAGE < 80" | bc -l) )); then
echo "Coverage $COVERAGE% is below threshold 80%"
exit 1
fi
Зверніть увагу: цей поріг стосується лише вашого TypeScript-коду тестів, а не Rust-програми. Вказуйте це в коментарях до конвеєра, щоб не створювати хибного відчуття безпеки в команді.
Обмеження та наступні кроки
Поки в екосистемі не з'явиться інструмент покриття на рівні BPF, повна автоматична перевірка coverage Solana-програм неможлива. Моніторте оновлення в репозиторії Solana CLI та Anchor — відповідний інструмент може з'явитися як частина solana-test-validator --log або як окремий crate. Якщо для вашого проєкту критично покриття на рівні байткоду, розгляньте ручне код-рев'ю кожного новий інструкції разом із аудитом.
Інтеграція з linter та форматувальниками
Лінтинг та форматування мають бути обов'язковими gate'ами в конвеєрі, а не рекомендаціями. Solana-програма — це смарт-контракт, що керує реальними активами; неформатований код ускладнює аудит і маскує логічні помилки.
Rust: cargo fmt та cargo clippy
У наведеній конфігурації ці інструменти вже включені в job lint-and-format. Важливі нюанси для BPF-цілі:
- cargo fmt --all -- --check перевіряє форматування без зміни файлів. Якщо є розбіжності, крок падає і виводить diff. Для локального виправлення виконайте cargo fmt --all без прапорця --check.
- cargo clippy --target bpfel-unknown-unknown -- -D warnings запускає Clippy саме для BPF-цілі. Це важливо: деякі ліби залежать від платформи, і Clippy може видавати різні попередження для x86 та BPF. Прапорець -D warnings перетворює всі попередження на помилки, що блокує конвеєр.
Придушення хибних спрацьовувань Clippy
Clippy для BPF-цілі іноді видає хибні попередження на макроси Anchor (#[account], #[instruction] тощо). Не придушуйте їх глобально в .clippy.toml. Замість цього використовуйте локальні атрибути над конкретною функцією:
#[allow(clippy::manual_range_contains)]
pub fn process_instruction(...) { ... }
Кожне придушення має супроводжуватися коментарем із поясненням, чому попередження хибне в цьому контексті. Без коментаря код-рев'юер не зможе відрізнити виправдане придушення від лінивого ігнорування реальної проблеми.
TypeScript/JavaScript: ESLint та Prettier
Тестовий харнес також потребує лінтингу. Додайте відповідні кроки в job lint-and-format:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- name: Install JS dependencies
run: npm ci
- name: Run ESLint
run: npx eslint tests/ --max-warnings 0
- name: Check Prettier
run: npx prettier --check "tests/**/*.ts"
Прапорець --max-warnings 0 у ESLint гарантує, що навіть попередження блокують конвеєр. Без нього розробники можуть ігнорувати попередження, поки вони не переростуть у помилки.
Уніфікована конфігурація
Щоб локальне середовище та CI були ідентичними, переконайтеся, що:
- Файл .rustfmt.toml (або rustfmt.toml) знаходиться в корені репозиторію і закомічений.
- Конфігурація ESLint (.eslintrc.js або eslint.config.js) знаходиться в корені репозиторію.
- Конфігурація Prettier (.prettierrc) знаходиться в корені репозиторію.
- Версії інструментів зафіксовані в package.json (використовуйте npm ci, а не npm install у CI).
Типова помилка: Clippy проходить локально, але падає в CI
Причина майже завжди одна: різні версії Rust toolchain. Локально ви можете мати стабільну версію 1.75, а в CI dtolnay/rust-toolchain@stable встановлює 1.76, де додали нове правило Clippy. Виправлення — зафіксувати версію в CI:
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
toolchain: "1.75.0"
targets: bpfel-unknown-unknown
Перевірте актуальну стабільну версію на офіційному сайті Rust перед вказанням. Оновлюйте її свідомо, а не автоматично через @stable.
Резюме конвеєра
Після впровадження всіх чотирьох компонентів ваш CI-конвеєр виконує таку послідовність: форматування Rust → Clippy для BPF → ESLint та Prettier для тестів → збірка програми → запуск local validator → виконання тестів → перевірка покриття харнесу. Кожен етап є обов'язковим gate'ом. Якщо будь-який крок падає, конвеєр зупиняється, а розробник отримує конкретну причину в логах, а не припущення.