Перед уроком
Обери свою групу й себе зі списку. Тоді відповіді й перевірки завдань запишуться саме на тебе.
Навчити бота перевіряти самого себе: написати автотести, які за секунду ловлять поломку, зробити скрипт deploy.sh, що не розгортає код із червоними тестами й сам повертає стару версію, якщо нова не запустилася, і підключити GitHub, який проганяє ті самі тести та ставить зелену галочку.
Замість <логін> підставляєш свій
логін виду bohdan.g9: ім’я, крапка, номер групи. Кутові дужки не пишуться.
У прикладах на цій сторінці скрізь стоїть bohdan.g9 — читай це як свій логін
і виправляй у кожній команді, яку копіюєш. Кабінет — сторінка, де проти кожного критерію
стоїть відмітка, зарахований він чи ні.
<домен> — назва, яку ти записав у файл
~/app/project.yaml на занятті 10, розділ server, поле
url. Сабдомен (із заняття 10) — це частина адреси перед
доменом: в імені bohdan.g9.klub.example домен — klub.example,
а сабдомен — bohdan.g9. Саме цю адресу ти сьогодні даєш гостям, тому перевір
її першою справою.
Перші два рядки ведуть на одне й те саме: за обома адресами nginx віддає
файли з твоєї теки ~/www. Бот у цьому не бере участі: він сам звертається
до Telegram.
Твій бот уже вміє багато: вітається, показує підказку, слухається лише власника, запускає скрипти, пише першим за розкладом. Кожна нова вправа ризикує зламати щось зі старого. Досі ти перевіряв усе очима: писав боту (або запускав try_bot.py) і дивився на відповідь. Чим більше команд, тим довше так перевіряти — і тим легше щось пропустити.
Сьогодні цю роботу візьме програма. У теці ~/app/tests з’являться автотести: короткі функції, які самі «пишуть» боту повідомлення й звіряють відповіді з очікуваними. Дванадцять тестів перевіряють /start, /help, /whoami, відмову чужому, створення й запуск скрипта, зупинку скрипта, що завис, і те, що розклад не спрацьовує двічі за день. Одна команда — і за секунду видно: або все гаразд, або що саме зламалося.
Друга половина заняття — безпечне оновлення. Скрипт deploy.sh перед розгортанням нової версії проганяє тести й відмовляється працювати, якщо хоч один червоний. А якщо тести зелені, але нова версія не запустилася, скрипт сам повертає код до останньої справної версії. Наприкінці GitHub почне сам проганяти ті самі тести після кожного git push і ставити біля коміту зелену галочку.
До кінця заняття в тебе буде:
tests/test_bot.py: щонайменше 12 тестів, зелені, плюс один твій власний;/help і замок власника — обидва разом дають червоний тест;deploy.sh: двічі поспіль дає однаковий результат, відмовляє при червоному тесті, відкочує при мовчазній службі (обидва випадки ти перевіриш у копії, не ризикуючи справжнім ботом);.github/workflows/tests.yml і зелена галочка на сторінці репозиторію.Telegram на цьому занятті не потрібен: тести розмовляють із функцією handle_update напряму, так само, як try_bot.py. Усе зробить і той, хто ще не має акаунта в Telegram.
Тарас на занятті 09 вирішив зробити код гарнішим і переписав у bot.py рядок із перевіркою власника. Перевірив: написав боту від свого імені /run privit — працює. Зберіг комітом. Тиждень потому виявилося, що тепер /run виконує будь-хто: у новому рядку Тарас випадково переплутав порівняння. Власника він перевіряв, чужого — ні, бо чужого в нього під рукою не було.
Така помилка має назву — регресія (від слова «регрес», рух назад): зламалося те, що раніше працювало, часто зовсім не там, де ти правив. Очима її шукати тяжко, бо перевіряєш лише щойно змінене.
Автотест — програма, яка сама виконує дію у твоєму коді й звіряє результат з очікуваним. Очікуване записують командою assert («стверджую, що це правда»):
assert 2 + 2 == 4 # правда: нічого не відбувається, програма йде далі
assert 2 + 2 == 5 # неправда: помилка AssertionError, тест «впав»
assert "/ask" in "/ask питання" # слово in для рядків: чи є шматок усерединіНабір тестів перевіряє все щоразу, зокрема те, чого ти не чіпав, і за секунду. Тому регресію видно ще до коміту. Схоже на ключ відповідей до контрольної: вчитель не розв’язує задачу заново, а звіряє твою відповідь із ключем.
Оля змінила привітання в /start, перевірила його у try_bot.py — гарно — і зберегла комітом. Через день виявилося, що бот перестав записувати людей у базу: разом із привітанням вона стерла сусідній рядок remember(sender). Що помітило б це одразу?
pytest — програма-«перевіряльник» для Python (вона вже стоїть у спільному середовищі сервера). Правила прості: бере файли test_*.py з теки tests, у кожному — функції, назва яких починається з test_, і по черзі їх виконує. Ось справжній тест із сьогоднішнього файлу:
def test_start_greets_by_name():
assert "Оля" in bot.handle_update(message("/start"))Функція message("/start") збирає таке саме повідомлення, яке Telegram надіслав би боту, від користувачки Олі. bot.handle_update(...) — та сама функція, що відповідає людям (заняття 02): вона отримує повідомлення й повертає текст. Тест перевіряє, що в привітанні є ім’я «Оля».
Результат pytest показує одним рядком: крапка — тест зелений (пройшов), літера F — червоний (упав). Нижче pytest пише, у якому рядку що не збіглося. Дванадцять крапок і 12 passed — усе гаразд.
Тест, який зелений завжди, нічого не стереже. Ось тест, що виглядає солідно, але марний:
def test_help():
assert bot.handle_update(message("/help")) # лише «відповідь не порожня»Він пройде, навіть якщо підказка стане словом «привіт». Справжній тест порівнює з тим, що саме має бути: assert "/ask" in bot.handle_update(message("/help")).
Тому кожен тест перевіряють так, як пожежну сигналізацію перевіряють димом: навмисно ламають те, що він стереже, і дивляться, чи він почервонів. Сьогодні ти зробиш це двічі, а автоперевірка зробить це з твоїм ботом сама: зламає копію (підказку, замок власника, ліміт часу, розклад) і подивиться, чи хоч один твій тест це помітив.
Який тест краще стереже перевірку власника?
Тести запускаються на тому самому сервері, де живе твій бот. Якби вони писали у справжню базу чи справжні скрипти, то кожен запуск лишав би сміття, а тест «чужий» міг би щось зіпсувати. Тому на початку файлу стоїть підготовка, до того як bot імпортується. Вона складається зі змінних оточення — налаштувань, які програма читає не з коду, а із середовища, де її запустили; bot.py читає їх на старті:
APP_DB — окрема тимчасова база в тимчасовій теці; справжня ~/data/app.db лишається цілою;SCRIPTS_DIR і SCHEDULE_FILE — тимчасові теки для скриптів і розкладу;STATS_FILE і APP_LOG — тимчасові сторінка статистики й журнал, щоб тести не переписали справжні ~/www/stats.html і ~/logs/app.log;OWNER_CHAT_ID=7 — у тестах власник завжди чат номер 7, а «чужий» — 8; твій справжній номер ніде не потрібен;BOT_POLLING=0 — бот не з’єднується з Telegram;LLM_BASE_URL «нікуди» — ШІ недоступний, і тест перевіряє, що бот не падає, а чемно відповідає.Ще одна хитрість — підміна (в pytest її роблять через monkeypatch): на час одного тесту підставляємо інше значення. Скрипт, що завис, зупиняється через 10 секунд — чекати стільки в кожному запуску довго, тож у тесті ліміт тимчасово стає 1 секунда. А замість справжнього надсилання в Telegram bot.tg підміняється функцією, яка лише складає повідомлення в список — і тест бачить, що саме бот «надіслав», нічого нікуди не відправляючи.
Побічна вигода: тести швидкі й повторювані. Запусти їх хоч сто разів, хоч уночі — результат той самий.
Розгортання (по-англійськи deploy) — це момент, коли нова версія коду починає працювати замість старої. У нашому випадку: код лежить у ~/app, а перезапуск служби club-bot змушує її прочитати нові файли. До перезапуску служба працює зі старим кодом у пам’яті, навіть якщо файли вже змінено.
Скрипт deploy.sh робить кроки в такому порядку, і порядок важливий:
| Крок | Що робить | Якщо не вдалося |
|---|---|---|
| 1. Чи все збережено комітом | перевіряє, що тестується саме той код, який у Git | зупиняється й просить спершу git commit |
| 2. Забрати нове | git pull з репозиторію на сервері | не страшно: може не бути нічого нового |
| 3. Запобіжник 1: тести | проганяє pytest | виходить з кодом 1, служба не чіпається: працює стара справна версія |
| 4. Перезапуск | перезапускає службу | — |
| 5. Запобіжник 2: перевірка | до 30 секунд чекає, поки бот оживе: служба active, а в last_poll свіжий час (бот уже спитав Telegram після перезапуску) | відкат на last-good, ще один перезапуск, вихід з кодом 1 |
| 6. Мітка | якщо служба відповіла — ставить тег git tag -f last-good на цей коміт | — |
Тег (англ. tag) — це постійне ім’я для конкретного коміту, як закладка в книзі. last-good означає «остання версія, яка запустилась і відповідала». Відкат — це git reset --hard last-good: гілка main і файли в теці стають такими, як у цій закладці. Саме це слово означає «відкат» у нашому курсі; про повернення до випущеної версії 1.0 буде на занятті 13. Зламану версію скрипт не губить: ставить на неї тег broken, і потім можна глянути, що було не так: git diff last-good broken.
Чому при червоних тестах відкату немає, а при мовчазній службі є? У першому випадку служба ще не перезапускалась і далі працює зі старим кодом — відкочувати нічого. У другому — вже запущено новий код, який не завівся, і його треба прибрати. А ще скрипт вимагає, щоб усе було збережено комітом: reset --hard стирає незбережені зміни, а правила безпеки такі — не стирати те, чого немає в історії.
Змінні оточення DEPLOY_RESTART, DEPLOY_CHECK, DEPLOY_WAIT дозволяють підмінити «як перезапустити», «як перевірити» і «скільки чекати» ззовні, не змінюючи файлу. Так ми перевірятимемо червоний тест і відкат на копії, не торкаючись справжньої служби.
Ти зберіг зміну комітом, запустив ./deploy.sh. Тести зелені, службу перезапущено, але бот 30 секунд не оживає (last_poll не оновлюється). Що робить скрипт?
Один тест червоний. Яке твердження про ./deploy.sh правильне?
Тести на твоєму сервері проходять — і це приємно, але це ще не все. На сервері в тебе лежать файли, яких немає в Git: .env, база, може, забутий scripts.py, що не потрапив у коміт. «У мене працює» — улюблене виправдання програмістів, і причина часто саме в цьому.
CI (безперервна перевірка, англ. continuous integration) вирішує це так: після кожного git push окремий комп’ютер береться з нуля, завантажує лише те, що є в репозиторії, і проганяє тести. На GitHub це робить GitHub Actions. Що саме робити, записано у файлі .github/workflows/tests.yml. Це YAML — формат, де структуру задають відступами (два пробіли на рівень):
on: [push, pull_request] — запускати на кожен push і на кожен pull request (запит «підтягни мої зміни», який роблять, коли хочуть додати свій код у чужий репозиторій);jobs → test → runs-on: ubuntu-latest — завдання (job) іде на чистій віртуальній машині з Linux (це «комп’ютер», який існує лише на час перевірки);steps — кроки: забрати код (actions/checkout), поставити Python, поставити пакети pyyaml (читає YAML-файли, його потребує код бота) і pytest, запустити python -m pytest -q.Біля коміту на сторінці репозиторію з’являється: жовте коло — іде перевірка, зелена галочка — усе пройшло, червоний хрестик — щось упало (можна клацнути й побачити журнал, як у термінала). Хрестик — не ганьба, а сигнал: хтось, зазвичай ти сам, щось забув зберегти комітом.
Секрети (.env, токен бота) на GitHub не потрапляють узагалі — вони в .gitignore. Тому тести й не вимагають справжнього токена: це ще одна причина, чому вони працюють із тимчасовими значеннями.
На сервері всі тести зелені, а на GitHub біля коміту червоний хрестик. Найімовірніша причина?
| Слово | Що означає |
|---|---|
| автотест | Програма, що сама виконує дію у коді й звіряє результат з очікуваним |
| pytest | Програма, що знаходить тести (tests/test_*.py), запускає їх і показує підсумок |
| assert | «Стверджую, що це правда». Неправда — тест червоний |
| зелений / червоний тест | Пройшов / упав. Крапка й літера F у виводі pytest |
| регресія | Поломка того, що раніше працювало, після неспоріднених правок |
| підміна (monkeypatch) | На час тесту підставити інше значення чи функцію: ліміт 1 с замість 10, фальшиве надсилання |
| розгортання (deploy) | Момент, коли нова версія починає працювати замість старої |
| тег (tag) | Постійне ім’я конкретного коміту; last-good — остання справна версія |
| відкат | Повернення гілки до попередньої справної версії: git reset --hard last-good |
| змінна оточення | Налаштування, яке програма читає із середовища запуску: DEPLOY_WAIT=3 ./deploy.sh |
| CI | Безперервна перевірка: після кожної зміни окремий комп’ютер сам проганяє тести |
| workflow, job, step | Сценарій GitHub Actions; завдання в ньому; крок завдання |
| зелена галочка | Знак біля коміту на GitHub: усі тести там пройшли |
Виконуй кроки по черзі. У прикладах <логін> заміни на свій, наприклад bohdan.g9 (дужки теж прибери). У кожного кроку є «що має вийти» — порівняй і, якщо не збіглося, дивись розділ «Якщо щось не працює» внизу. Тести запускай як /opt/club/venv/bin/python3 -m pytest -q: у цьому середовищі вже стоять pytest і потрібні пакети.
ssh <логін>@91.219.61.4
cd ~/app
git status --short
git log --oneline -3
systemctl --user is-active club-bot
cat ~/data/bot_state.json
ls /opt/club/kurs/12/ /opt/club/kurs/12/testsgit status --short нічого не друкує — усе попереднє збережено. Видно файли — спершу git add -A і git commit -m "Заняття 11: доробки" (deploy.sh сьогодні відмовиться працювати з кодом, не збереженим комітом, тож краще звикнути одразу);active — служба працює; inactive — systemctl --user start club-bot;cat показує короткий JSON з полем last_poll — часом, коли бот востаннє питав Telegram. Це й є «ознака життя», яку скрипт чекатиме після перезапуску (стара відмітка, зроблена до перезапуску, не рахується);deploy.sh tests tests.yml, а всередині tests — test_bot.py. No such file or directory — скажи викладачеві.mkdir -p ~/app/tests
cp /opt/club/kurs/12/tests/test_bot.py ~/app/tests/
echo ".pytest_cache/" >> ~/app/.gitignore
cd ~/app
/opt/club/venv/bin/python3 -m pytest -qЩо має вийти: рядок із дванадцяти крапок і підсумок на зразок 12 passed in 1.2s (час може відрізнятися). Рядок з echo просто додає службову теку .pytest_cache у .gitignore, щоб вона не потрапляла в коміти.
Якщо замість цього ModuleNotFoundError: No module named 'scripts' чи 'init_db' — не вистачає файлу з попереднього заняття: порівняй свою теку з ls /opt/club/kurs/09 і ls /opt/club/kurs/04. Червоні тести (F) при чужому коді — це не помилка pytest, а повідомлення про розбіжність: читай рядок після E у виводі, у ньому «чекали одне, отримали інше».
less ~/app/tests/test_bot.py
/opt/club/venv/bin/python3 -m pytest -vУ less прокручуй пробілом, вийти — q. Знайди очима: блок os.environ[…] = … на початку (розділ 4 теорії), функцію message(), яка збирає повідомлення, автофікстуру clean_state (перед кожним тестом вона стирає тимчасові скрипти й розклад, щоб тести не заважали один одному; спершу вона перевіряє, що стирає саме тимчасову теку, а не твої справжні скрипти), а далі дванадцять тестів у чотирьох групах: «вітання й підказка», «замок власника», «ліміт часу» і «розклад».
Команда з -v (від verbose, «докладно») друкує кожен тест окремим рядком: tests/test_bot.py::test_stranger_is_refused PASSED і так дванадцять разів, а в кінці знову 12 passed. Знайди тест, який перевіряє, що розклад не спрацьовує двічі, і прочитай його: він викликає bot.run_due("09:00") двічі й чекає рівно одне надіслане повідомлення.
cd ~/app
sed -i 's|"/whoami — твій|"/whoamy — твій|' bot.py
git diff bot.py
/opt/club/venv/bin/python3 -m pytest -qКоманда sed -i 's|було|стало|' файл замінює в файлі було на стало (символ | тут лише розділяє частини). Вона замінила в підказці /whoami на /whoamy (одна літера). git diff bot.py показує цю заміну: мінус — було, плюс — стало. Що має вийти від pytest:
.F.......... [100%]
=================================== FAILURES ===================================
___________________________ test_help_lists_commands ___________________________
def test_help_lists_commands():
reply = bot.handle_update(message("/help"))
> assert "/ask" in reply and "/whoami" in reply
E AssertionError: assert (...)
FAILED tests/test_bot.py::test_help_lists_commands
1 failed, 11 passed in 1.2sПерша крапка — зелений тест, друга позиція — F. Стрілка > вказує на рядок, де не збіглося. Одинадцять інших тестів зелені — отже, тест стереже саме підказку. Тепер поверни, як було:
git restore bot.py
/opt/club/venv/bin/python3 -m pytest -qЗнову 12 passed. git restore повертає файл до останнього коміту (заняття 03). Саме тому на початку було важливо зберегти все комітом.
Тепер повтори те, що трапилося з Тарасом: «спрощуємо» перевірку власника так, що вона вимикається.
cd ~/app
sed -i 's|if not OWNER or str(chat_id) != OWNER:|if False:|' bot.py
git diff bot.py
/opt/club/venv/bin/python3 -m pytest -qЩо має вийти: 1 failed, 11 passed і червоний test_stranger_is_refused. У рядку після E видно: чекали «лише для власника», а бот відповів Створено scripts/hack.sh… — чужий щойно створив скрипт. Без тестів ти б цього не побачив: як власник усе працювало б. Поверни замок:
git restore bot.py
/opt/club/venv/bin/python3 -m pytest -q
git status --shortМає бути 12 passed, а git status --short не друкує нічого про bot.py. Там можуть бути лише рядки про tests/ і .gitignore — це нормально: головне, що bot.py повернувся до справного стану.
Додай у кінець файлу тест на команду, якої ще немає в наборі: порожній розклад.
cat >> ~/app/tests/test_bot.py <<'EOF'
def test_schedule_is_empty_at_start():
assert "порожній" in bot.handle_update(message("/schedule"))
EOF
cd ~/app
/opt/club/venv/bin/python3 -m pytest -qЩо має вийти: 13 passed. Запис <<'EOF' означає «усе до рядка EOF — це текст, який треба дописати до файлу». Обов’язково переконайся, що твій тест може почервоніти. Для цього зламай не тест, а бота (тест має стерегти саме його): sed -i 's/Розклад порожній/Розклад ZZZ/' ~/app/scripts.py і знову pytest. Має бути 1 failed. Потім поверни: cd ~/app && git restore scripts.py і перевір, що знову 13 passed.
Як користуватися ШІ: можна попросити безкоштовний ШІ запропонувати тести для якоїсь команди. Але тест, який він написав, перевір так само: зламай бота й подивись, чи тест почервонів. Бувають тести, що «зеленіють» завжди, — а це гірше за відсутність тесту, бо дає хибну впевненість.
cd ~/app
git add -A
git commit -m "Заняття 12: автотести бота"
git status --shortПісля коміту git status --short порожній. Це потрібно далі: deploy.sh тестує лише те, що збережено комітом, а автоперевірка бере копію саме з Git.
cp /opt/club/kurs/12/deploy.sh ~/app/
chmod +x ~/app/deploy.sh
cd ~/app
less deploy.shУ less гортай пробілом, вийти — q. Прочитай скрипт по частинах і знайди відповідники з таблицею з розділу 5 теорії:
set -euo pipefail — «зупинися на першій же помилці, не йди далі наосліп»; cd "$(dirname "$0")" — працюй у теці, де лежить цей файл, а не там, звідки запустили;${DEPLOY_…:-…} — значення за замовчуванням, які можна підмінити ззовні: :- означає «якщо змінної немає, візьми це»;wait_alive — цикл до 30 разів по секунді, у кожному перевіряється, що служба club-bot active, а last_poll новіший за мить перезапуску;if ! … pytest …; then … exit 1 — запобіжник 1; if wait_alive; then git tag -f last-good — запобіжник 2 і тег; далі гілка відкату з git reset --hard last-good.Збережи його комітом, бо скрипт вимагає, щоб усе було збережено:
git add deploy.sh
git commit -m "Заняття 12: deploy.sh із тестами й відкатом"chmod +x дозволяє запускати файл як програму. Git запам’ятав це «право запуску» разом із комітом: без нього в копії (кроки 10–11) вийшло б Permission denied.
cd ~/app
./deploy.sh
echo "код виходу: $?"
./deploy.sh
echo "код виходу: $?"
git tagУвага: скрипт справді перезапускає бота на кілька секунд, а якщо нова версія не запуститься, сам поверне гілку main до тега last-good (reset --hard). Тому перед запуском усе має бути збережено комітом. Що має вийти після першого запуску (рядки From … та Already up to date. від git pull можуть відрізнятися):
==> перевіряю, що все збережено комітом
==> забираю нову версію
Already up to date.
==> тести
............. [100%]
13 passed in 1.3s
==> перезапуск служби
==> перевірка
бот живий; тег last-good тепер на 8afb66e
код виходу: 0Другий запуск друкує ті самі кроки (==>) і так само закінчується кодом 0 — це й означає «двічі поспіль однаковий результат»: скрипт можна запускати скільки завгодно, він нічого не накопичує і не ламає. git tag покаже last-good.
Якщо бачиш «є незбережені зміни» — у тебе лишилися правки без коміту: git status --short, потім git add -A і git commit. Якщо бачиш «бот не ожив за 30 с» — подивись tail -n 30 ~/logs/club-bot.log: часто це помилка в коді, і вона ж залишиться в журналі.
Ламати справжнього бота не будемо. Зробимо копію репозиторію у своїй домашній теці (~/proba) і випробуємо скрипт там. Змінна DEPLOY_RESTART підміняє «перезапуск» на безпечний echo — тож справжню службу ми не зачеплемо.
rm -rf ~/proba
git clone -q ~/app ~/proba
cd ~/proba
printf 'def test_zlamanyi():\n assert 1 == 2\n' > tests/test_zlamanyi.py
git add -A
git commit -qm "навмисно червоний тест"
DEPLOY_RESTART="echo ЯК-БИ-ПЕРЕЗАПУСТИВ" ./deploy.sh
echo "код виходу: $?"Команда printf створила в копії новий тест, який завжди падає (1 == 2), а git commit зберіг його. Що має вийти: кроки ==> до ==> тести, далі pytest з F і 1 failed, 13 passed, потім рядок ТЕСТИ ЧЕРВОНІ — не розгортаю. Служба далі працює зі старою справною версією. і код виходу: 1. Слів ЯК-БИ-ПЕРЕЗАПУСТИВ на екрані немає — перезапуск до нього не дійшов. Це і є запобіжник 1.
У цій самій копії приберемо червоний тест і зробимо коміт, після якого «бот не ожив». Для цього DEPLOY_CHECK підмінимо на команду «успіх, якщо файлу BROKEN немає». Спочатку git reset --hard HEAD~1 повертає копію на один коміт назад (HEAD~1 — «попередній коміт») і так прибирає червоний тест, а touch BROKEN створює порожній файл BROKEN:
cd ~/proba
git reset -q --hard HEAD~1
touch BROKEN
git add BROKEN
git commit -qm "служба мовчить"
DEPLOY_RESTART="echo перезапуск" DEPLOY_CHECK="test ! -e BROKEN" DEPLOY_WAIT=3 ./deploy.sh
echo "код виходу: $?"
ls BROKEN
git tag
git diff --stat last-good brokenЩо має вийти: тести зелені, перезапуск, потім бот не ожив за 3 с (чекали 3 секунди, бо DEPLOY_WAIT=3), далі ==> відкат до last-good, ще один перезапуск, відкотили: стара версія знову жива… і код виходу: 1. Команда ls BROKEN скаже No such file or directory — файл зник разом із поганим комітом. git tag покаже два теги: broken і last-good, а git diff --stat — що саме було в поганій версії (BROKEN | 0).
cd ~
rm -rf ~/probaКопію видалено. Справжня служба весь цей час працювала без перерви.
Остання частина — щоб GitHub сам перевіряв кожну зміну. Файл сценарію називається tests.yml і лежить у прихованій теці .github/workflows (крапка на початку імені робить теку прихованою).
Немає акаунта на GitHub (тобі ще немає 13 років або батьки не дозволили)? Зроби все до git commit включно, а git push і крок 13 пропусти: файл workflow однаково має бути збережений комітом, і автоперевірка його зарахує. Галочку тоді покаже викладач на своєму прикладі.
mkdir -p ~/app/.github/workflows
cp /opt/club/kurs/12/tests.yml ~/app/.github/workflows/
cd ~/app
cat .github/workflows/tests.yml
git add -A
git commit -m "Заняття 12: тести, deploy.sh з відкатом, GitHub Actions"
git push origin main
git push github maincat покаже сценарій: name: tests, on: [push, pull_request], завдання test на ubuntu-latest і чотири кроки. Дивись, щоб відступи були пробілами (клавішу Tab у YAML вживати не можна). Перевір, що файл читається як YAML: /opt/club/venv/bin/python3 -c "import yaml; print(list(yaml.safe_load(open('.github/workflows/tests.yml'))['jobs']))" має надрукувати ['test']; помилка означає збиті відступи. Кожен git push закінчується рядком main -> main; якщо в тебе гілка master, пиши master. Бачиш у git status --short файл .env — зупинись і скажи викладачеві: його не мало бути (заняття 03).
Відкрий у браузері свій репозиторій club-app на GitHub (https://github.com/<логін-github>/club-app) і вкладку Actions. Там з’явився запуск з назвою твого останнього коміту. Спершу жовте коло (іде перевірка, 20–40 секунд), потім зелена галочка. Така сама галочка стоїть поруч із комітом на головній сторінці репозиторію.
git status --short), або забутий git add -A;git push github main і те, що файл лежить саме в .github/workflows/;Автоперевірка не чіпає ні твоїх справжніх скриптів, ні бази, ні Telegram, ні служби: усе ламає в тимчасових копіях репозиторію й бере лише те, що збережено комітом. Спершу запускає твої тести й рахує зелені (поріг — шість, хоча в наборі їх дванадцять). Потім по черзі ламає в копії бота — підказку, /whoami, замок власника, запис у базу, ліміт часу скрипта, розклад — і дивиться, що тести хоч раз почервоніли. Далі в копії двічі запускає deploy.sh (результат має збігтися), додає червоний тест (скрипт має відмовитися) і «службу, що мовчить» (скрипт має відкотитися на last-good). Ще перевіряє файл tests.yml і те, що бот живий (служба club-bot active, last_poll свіжий). Галочку на GitHub і розповідь про два випадки (червоні тести, мовчазна служба) оцінює викладач.
Натисни «Перевірити практичну частину» в розділі «Перевірка виконаних завдань» нижче на сторінці. Перед цим збережи все комітом (git status --short має бути порожнім).
| Код | Що перевіряється | Якщо не зараховано |
|---|---|---|
| L12.1 | автотести бота: щонайменше 6 проходять (у наборі їх 12) автоперевірка | Скопіюй тести: mkdir -p ~/app/tests && cp /opt/club/kurs/12/tests/test_bot.py ~/app/tests/ . Запусти cd ~/app && /opt/club/venv/bin/python3 -m pytest -q — побачиш, який тест червоний і чому. |
| L12.2 | тести справжні: зламаний бот червонить хоч один автоперевірка | Перевірка ламає копію бота (підказку, /whoami, перевірку власника, запис у базу, ліміт часу, розклад) і дивиться, чи хоч один тест почервонів. «Тести лишилися зеленими» — додай assert, що порівнює з очікуваним текстом, а не лише перевіряє «не порожньо». «У чистій копії тести червоні» — вони червоні вже зараз: запусти pytest у ~/app і виправ, потім збережи комітом. «Не знайшов рядків, які я ламаю» — ти сильно переписав bot.py чи scripts.py: порівняй з /opt/club/kurs/09/. |
| L12.3 | deploy.sh двічі поспіль дає однаковий результат автоперевірка | cp /opt/club/kurs/12/deploy.sh ~/app/ && chmod +x ~/app/deploy.sh; збережи зміни комітом (git add -A && git commit) — скрипт відмовляється працювати з кодом, не збереженим комітом. Запусти двічі: ./deploy.sh && ./deploy.sh |
| L12.4 | deploy.sh відмовляє розгортати, коли тест червоний автоперевірка | Перевірка в тимчасовій копії додає червоний тест і запускає deploy.sh: він має зупинитися з кодом 1 і НЕ перезапускати службу. Перевір, що рядок restart стоїть після pytest і що pytest у if ! … ; then exit 1. |
| L12.5 | якщо служба не відповіла, deploy.sh відкочується на last-good автоперевірка | У копії виходить так: вдалий deploy ставить тег git tag -f last-good, наступний «поганий» (служба не відповідає) має зробити git reset --hard last-good, перезапустити службу й вийти з кодом 1. Візьми оригінал з /opt/club/kurs/12/deploy.sh. |
| L12.6 | є .github/workflows/tests.yml, що запускає pytest автоперевірка | mkdir -p ~/app/.github/workflows && cp /opt/club/kurs/12/tests.yml ~/app/.github/workflows/ ; потім збережи комітом: git add -A && git commit. У файлі мають бути on: [push, …], jobs → … runs-on, крок uses: actions/checkout і крок run: python -m pytest; відступи — пробіли (python -c "import yaml…" з кроку 12 покаже помилку). |
| L12.7 | після розгортання служба club-bot active і last_poll свіжий автоперевірка | Бот не ожив. systemctl --user status club-bot і tail -n 30 ~/logs/club-bot.log; якщо щойно був deploy.sh, він мав відкотитися — перевір git log --oneline -3 і git tag. |
| L12.8 | на GitHub біля останнього коміту зелена галочка викладач | Позначку ставить викладач на занятті або після нього. |
| L12.9 | пояснив, що робить deploy.sh при червоних тестах і при мовчазній службі викладач | Позначку ставить викладач на занятті або після нього. |
Кожне питання оцінюється відразу в браузері — це лишається так само. Ця кнопка лише надсилає підсумок учителю, щоб відповіді не зникли разом із закритою вкладкою.
Список критеріїв і що саме вони означають — у таблиці вище, «Що перевіряє автоматика». Тут — та сама перевірка, але прямо зараз, без очікування на найближчий прохід за розкладом.
tests/test_bot.py: (а) /every 25:00 privit від власника відповідає помилкою з «ГГ:ХХ»; (б) після /new x1 echo 1, /every 09:00 x1 команда /stop x1 відповідає «прибрано», а /schedule знову показує «порожній»; (в) /new Великі echo 1 відмовляє із «латиницею». Кожен тест перевір на живучість: тимчасово зламай відповідний рядок у scripts.py і переконайся, що тест червоніє, потім поверни. Має бути не менше 16 зелених.tests/ файл test_zlamanyi.py з assert 1 == 2, збережи комітом і зроби git push github main. Подивись у вкладці Actions хрестик і журнал. Прибери файл (git rm tests/test_zlamanyi.py), збережи комітом, відправ — і дочекайся галочки. Увага: не запускай ./deploy.sh, поки цей файл лежить у репозиторії. Наприкінці в тебе має бути зелена галочка: її бачить викладач.README.md під заголовком «## Тести й оновлення»): що робить deploy.sh при червоних тестах і що — коли служба не відповіла; чому в першому випадку відкату немає.git add -A, git commit -m "Заняття 12: домашня робота", git push origin main і git push github main.Ти запустив системний Python, а потрібні пакети стоять у спільному середовищі. Завжди пиши повну команду: /opt/club/venv/bin/python3 -m pytest -q. Щоб не набирати щоразу, у тій самій теці можна запускати ./deploy.sh: він уже використовує потрібний Python.
ModuleNotFoundError: No module named 'scripts' (чи init_db, db, env) — тест імпортує те, що має лежати в ~/app: ls ~/app. Нема scripts.py — скопіюй із /opt/club/kurs/09/; нема init_db.py чи db.py — із /opt/club/kurs/04/;test_start_greets_by_name, test_help_lists_commands — ти змінив тексти бота на попередніх заняттях. Прочитай рядок після E: чекали одне, отримали інше. Тест — це договір: або поверни старе слово в боті, або зміни очікування в тесті, але свідомо;test_every_message_is_counted: IndexError — у тимчасовій базі немає таблиці users. Дивись, чи є init_db.main() на початку тестів і чи не стерто remember(sender) у handle_update;LLM_BASE_URL з адресою «нікуди».Перевірка пише одне з трьох. «Я зламав бота, а тести лишилися зеленими» — назвала, що саме зламала. Знайди тест на це місце й подивись, що він порівнює. «У чистій копії тести червоні» — тести червоні вже зараз, ще до поломки: запусти pytest у ~/app і виправ (і переконайся, що все збережено комітом). «Не знайшов рядків, які я зазвичай ламаю» — ти сильно переписав bot.py чи scripts.py; порівняй з /opt/club/kurs/09/. Типова причина першого випадку: assert reply (відповідь «не порожня») замість assert "лише для власника" in reply. Додавай порівняння з конкретним текстом або наслідком (файл не створився, повідомлення пішло одне, а не два). Якщо ти видалив один із готових тестів — поверни його з /opt/club/kurs/12/tests/.
Permission denied або command not found — немає виконуваного права: chmod +x ~/app/deploy.sh, потім збережи комітом (git add deploy.sh && git commit);bad interpreter — перший рядок має бути точно #!/usr/bin/env bash; найкраще скопіювати оригінал заново з /opt/club/kurs/12/;є незбережені зміни — це не помилка скрипта, а запобіжник: git status --short, git add -A, git commit -m "…";бот не ожив за 30 с — подивись tail -n 30 ~/logs/club-bot.log і systemctl --user status club-bot. Якщо тег last-good вже був, скрипт сам відкотився, а git diff last-good broken покаже, що було не так;git pull: скрипт звертається до репозиторію на сервері. Зачекай хвилину; якщо виведе fatal — це не зупинить скрипт, але перевір, куди веде git remote -v, і що гілка в тебе справді main;Перевірка працює з копією того, що збережено комітом: що лежить лише на диску, вона не бачить. Виконай git status --short (маєш побачити порожньо) і git ls-files | grep -E "deploy.sh|tests" (обидва мають бути в списку). Якщо ти переписував скрипт сам — порівняй з оригіналом: diff ~/app/deploy.sh /opt/club/kurs/12/deploy.sh. Скрипт має: (1) зупинятися з exit 1 після червоного pytest до перезапуску; (2) після вдалої перевірки ставити git tag -f last-good; (3) у разі мовчазної служби робити git reset --hard last-good, перезапускати й виходити з кодом 1; (4) брати «як перезапустити» й «як перевірити» зі змінних DEPLOY_RESTART і DEPLOY_CHECK.
~/app/.github/workflows/tests.yml (з крапкою на початку першої теки);git add -A перед git commit: виконай обидві команди, потім git push github main;nano і виправ відступи (два пробіли на рівень);git push github main з цим файлом; перевір репозиторій: той, куди веде git remote -v для github.systemctl --user status club-bot і tail -n 30 ~/logs/club-bot.log. Якщо щойно був ./deploy.sh і він відкотився, то код уже той, що працював; подивись git log --oneline -3 та git tag. Служба не стоїть після перезапуску — запусти її: systemctl --user start club-bot, зачекай 5 секунд і повтори cat ~/data/bot_state.json: last_poll має бути свіжим (не старшим за дві хвилини); немає токена в .env — бот не питатиме Telegram і last_poll не з’явиться.
Відкрий запуск у вкладці Actions, клацни на червоний крок і прочитай рядок після E чи ModuleNotFoundError. Найчастіше це файл, що є на сервері, але не в Git: git status --short покаже його знаком ??. Додай git add, збережи комітом і відправ. Другий варіант — тест залежить від чогось зі свого сервера (наприклад, від справжнього .env): тести мають використовувати лише тимчасові значення з початку файлу.