GitHub Actions checkout v7: як не запускати код із форка з привілеями

GitHub Actionscheckoutpull_request_targetworkflow_runбезпекаCI

Схема безпечного поділу перевірки коду з форка і привілейованих дій у GitHub Actions

У багатьох команд є знайома схема: pull request із форка приходить у репозиторій, а робочий процес на pull_request_target одразу забирає код із цього PR. Це зручно, бо далі можна запускати перевірки, коментувати результат і навіть робити дії в репозиторії. Але саме тут і живе проблема: робочий процес уже має доступ до секретів, GITHUB_TOKEN базового репозиторію та середовища CI, а код при цьому приходить від зовнішнього контриб’ютора.

18 червня 2026 GitHub змінив поведінку actions/checkout v7: тепер він за замовчуванням відмовляється забирати небезпечний код із форка у сценаріях на кшталт pull_request_target і workflow_run. Це не косметична правка. Це зміна, яка змушує команду окремо вирішити, де у вас довірений код, а де недовірений код.

До змін

Ось типовий ризикований приклад:

name: pr-check

on:
  pull_request_target:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
        with:
          ref: ${{ github.event.pull_request.head.sha }}
      - run: npm test

На перший погляд це виглядає нормально: ми просто хочемо протестувати код із PR. Але в pull_request_target код запускається в контексті базового репозиторію. Якщо PR з форка змінить скрипти, workflow-файли або будь-яку логіку, що торкається секретів, наслідок може бути набагато гіршим за зіпсований білдів.

Після змін

Після оновлення checkout v7 відповідь GitHub проста: якщо робочий процес має привілейований доступ, не змішуйте його з checkout незнайомого коду за замовчуванням.

Практичний варіант виглядає так:

  1. Виконуйте перевірки коду форка в pull_request, де немає доступу до секретів.
  2. Використовуйте pull_request_target лише для безпечних дій над базовим репозиторієм.
  3. Якщо привілейований доступ і checkout коду з форка все ж потрібні, зробіть це явним винятком після рев’ю.
name: fork-validation

on:
  pull_request:
    branches: [main]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - run: npm ci
      - run: npm test
name: privileged-follow-up

on:
  pull_request_target:
    branches: [main]

jobs:
  comment:
    runs-on: ubuntu-latest
    steps:
      - run: echo "Safe repo-side action only"

Тут логіка проста: недовірений код перевіряється в непривілейованому середовищі, а привілейований робочий процес більше не мусить тягнути у себе зовнішній код лише заради зручності.

Що означає нова типова поведінка

actions/checkout v7 тепер не дає випадково зробити те, що раніше часто проходило “по звичці”. Якщо вам справді треба саме такий checkout, GitHub очікує явного opt-in через allow-unsafe-pr-checkout: true.

Це важливий сигнал для команди:

  • якщо без цього прапорця робочий процес ламається, спочатку перевірте модель довіри;
  • якщо робочий процес читає секрети, майже завжди краще розділити його на два етапи;
  • якщо небезпечний checkout лишається, він має бути наслідком рішення, а не побічним ефектом.

Коли явне увімкнення ще може бути доречним

Є сценарії, де команда свідомо хоче checkout коду з форка у привілейованому робочому процесі. Наприклад, дуже специфічний DevEx пайплайн або внутрішня автоматизація з додатковими обмеженнями. Але навіть тоді варто пройти короткий чек:

  • чи точно це завдання не може виконати те саме без checkout коду з форка;
  • чи винесені секрети і write-операції в окремий крок;
  • чи є ручне рев’ю або політика, яка має дозволити небезпечний режим перед увімкненням;
  • чи задокументовано, чому цей виняток існує.

Якщо на ці питання немає чітких відповідей, типова поведінка GitHub тут допомагає не “стати менш зручним”, а не допустити типову помилку з надмірною довірою.

Швидке дерево рішень

Якщо вам треба вирішити, що робити з існуючим робочим процесом, почніть з трьох питань:

  1. Чи потрібні цьому завданню секрети або привілейований токен?
  2. Чи бере він код безпосередньо з форка?
  3. Чи можна розділити перевірку коду і привілейовану дію на два окремі потоки?

Якщо відповідь на перше “так”, а на друге “так” теж, це майже завжди знак, що робочий процес потрібно перепроєктувати. Якщо відповідь на перше “ні”, використовуйте менш привілейований тригер. Якщо відповідь на третє “так”, розділення є кращим за небезпечне явне увімкнення.

Що перевірити після міграції

Після переходу на v7 варто швидко підтвердити три речі:

  • fork PR більше не отримує привілейований checkout випадково;
  • довірений робочий процес не читає і не виконує недовірений код;
  • усі винятки з allow-unsafe-pr-checkout описані та зрозумілі командам, що супроводжують репозиторій.

Висновок

GitHub не прибрав ваші можливості. Він просто прибрав випадковість. Якщо ви і раніше будували pull_request_target обережно, v7 дає вам кращу типову поведінку. Якщо ж робочий процес був зібраний на “далі якось розберемося”, це саме той момент, коли варто зупинити автоматизм і відділити недовірений код від привілейованого доступу.

Далі почитати

Короткий чеклист

  • Знайти всі кроки, яким потрібні секрети або право запису.
  • Винести перевірку коду з форка в pull_request, якщо це можливо.
  • Тримати привілейовані дії окремо від checkout коду.
  • Вважати allow-unsafe-pr-checkout: true винятком, який потребує рев’ю.
  • Перевірити, що процес після зміни відповідає правилам репозиторію.

План безпечної міграції з небезпечного fork checkout

Ти допомагаєш команді, яка вже використовує pull_request_target або workflow_run і інколи робить checkout коду з форка. Проаналізуй workflow і дай короткий план: 1. Які кроки тут належать до довіреного коду, а які до недовіреного коду? 2. Чи можна винести перевірку fork-коду в pull_request без секретів? 3. Якщо потрібен privileged job, які кроки треба відділити від checkout? 4. Коли явне увімкнення allow-unsafe-pr-checkout: true ще може бути виправданим? 5. Який мінімальний план переходу й перевірки допоможе не зламати CI? Поверни відповідь як практичний список дій для власника репозиторію або інженера, який відповідає за CI.