Technologie DevOps // Wykład 02 // 01

CI i SDLC

Od zmiany w kodzie do informacji, której zespół może zaufać.

Cykl życiaInformacja zwrotnaRodzaje testów

Najpierw proces. Potem pierwszy działający workflow, jego mechanizmy i reguły bezpiecznej integracji.

Software Development Life Cycle

Rozwój to pętla

Sześć etapów cyklu życia oprogramowania Zgodnie z ruchem wskazówek zegara: planowanie, analiza wymagań, projekt, implementacja, testy, wydanie i utrzymanie. Informacja z utrzymania wraca do planowania; wyniki testów mogą cofnąć pracę do projektu lub implementacji.

Potrzeby użytkowników prowadzą do zmian. Wyniki testów, review i obserwacje z eksploatacji wracają do zespołu.

  • Etapy nie muszą być osobnymi, zamkniętymi fazami.
  • Krótka iteracja może przejść przez cały cykl.
  • CI skraca pętlę informacji zwrotnej podczas pracy nad zmianą.
Continuous Integration

Mała zmiana. Szybka odpowiedź.

Integruj często

Krótkie gałęzie i niewielkie zmiany ograniczają liczbę możliwych przyczyn problemu.

Sprawdzaj automatycznie

Build i testy uruchamiają się według tych samych reguł dla kolejnych zmian.

Reaguj na wynik

Czerwony wynik wymaga diagnozy i poprawki. Sam plik YAML nie tworzy praktyki CI.

Błąd wykryty po kilku minutach łatwiej połączyć z konkretną zmianą niż problem odkryty po tygodniu.

Trzy procesy // różne efekty

Zielony build nie jest wdrożeniem

ProcesCo robi?Efekt
Continuous Integration (CI)Integruje i weryfikuje kod.Artefakt builda, wyniki testów i raporty.
Continuous DeliveryAutomatyzuje przygotowanie, dostarczenie i konfigurację wydania.Wersja gotowa do wdrożenia na żądanie; decyzja o produkcji pozostaje ręczna.
Continuous DeploymentAutomatycznie wdraża zmiany, które przeszły wymagane kontrole.Nowa wersja dostępna dla użytkownika bez osobnej ręcznej zgody na każdą zmianę.

Jedna platforma może realizować wszystkie trzy procesy. O tym, co się stało, decydują wykonane kroki, nie nazwa narzędzia.

Co obejmuje sprawdzenie?

Testy mają różny zakres

Jednostkowe

Mały fragment logiki. Zależności często zastępują duble. Zwykle krótki czas i precyzyjna diagnoza.

Integracyjne

Współpraca elementów, np. API i bazy danych. Sprawdzają granice, których nie obejmuje test z mockiem.

End-to-end

Scenariusz użytkownika przez cały system, często z przeglądarką. Szerszy sygnał, większy koszt środowiska i diagnozy.

Lint i analiza statyczna uzupełniają testy. Zielony wynik dowodzi przejścia skonfigurowanych sprawdzeń, nie braku wszystkich błędów.

Wspólna decyzja zespołu

Automat sprawdza. Człowiek ocenia.

Zmiana
→
Pull request
→
CI + review
→
Merge

CI: czy kod da się zbudować i czy przechodzi ustalone kontrole?

Review: czy zmiana rozwiązuje właściwy problem i czy będzie zrozumiała za miesiąc?

02 // Współpraca w Git

Dokąd trafia zmiana?

Najpierw ustalamy model integracji. Dopiero potem konfigurujemy automatyzację.

GitHub FlowMerge, squash, fast-forwardGit Flow

Gałąź jest nazwanym wskaźnikiem na commit. Nazwy main i develop otrzymują znaczenie od zespołu, nie od samego Gita.

Jedna główna linia pracy

GitHub Flow: main + feature

Krótka gałąź feature jest scalana do main Czas biegnie w prawo. Feature powstaje z B. F1 i F2 są sprawdzane w PR-ze. Main otrzymuje w tym czasie C, a następnie commit scalenia M z rodzicami C i F2. mainfeature ABCMF1F2 PR → CI + review → poprawkiczas →
  • Zmianę zaczynamy z aktualnego main; PR można otworzyć jeszcze jako draft.
  • Kolejne commity aktualizują ten sam PR i jego sprawdzenia.
  • Po akceptacji integrujemy zmianę, usuwamy feature i utrzymujemy main gotowy do wydania.
Ta sama zmiana // inna historia

Merge zachowuje gałąź. Squash scala zmianę.

Merge commit

Merge commit M ma dwóch rodzicówMain: B, C, M. Feature: B, F1, F2. Rodzicami M są C i F2; F1 i F2 pozostają w historii main. BCMF1F2

M ma dwóch rodziców. Commity F1 i F2 zachowują tożsamość oraz relację z gałęzią.

Squash and merge

Squash tworzy nowy commit SMain: B, C, S. S ma jednego rodzica C i zawiera połączoną zmianę F1 oraz F2. Przerywana linia opisuje przeniesienie zmian, a nie krawędź rodzica w Git. BCSF1F2

S ma jednego rodzica. Zawiera łączną zmianę PR-a, ale nie włącza F1 i F2 jako przodków main.

Bez nowego commita scalenia

Fast-forward przesuwa wskaźnik

Main przesuwa się z A do CA, B i C leżą na jednej linii historii. Przed fast-forward main wskazuje A, a feature C. Po operacji main wskazuje istniejący C. Nie powstaje merge commit. ABC main przedfeaturemain po
git switch main
git pull --ff-only

--ff-only odmawia integracji rozbieżnej historii. Chroni np. wtedy, gdy na lokalnym main zostały własne commity, a zdalny main też poszedł naprzód.

To kontrola historii commitów, nie sposób zapisywania niezacommitowanych plików. GitHub „Rebase and merge” jest inną operacją niż lokalny fast-forward.

Main + develop + gałęzie wydania

Release oddziela stabilizację od rozwoju

Feature i release w Git FlowFeature wraca do develop. Z develop wychodzi release, podczas gdy rozwój trwa dalej. Poprawiona wersja z release trafia do main jako v1.1 oraz z powrotem do develop. maindevelopfeaturerelease v1.1rozwój kolejnej wersjistabilizacja wydania

Feature: develop → feature → develop. Release: develop → release → main i develop, żeby poprawki nie zginęły.

Naprawa poza bieżącym rozwojem

Hotfix wraca do obu linii

Hotfix v1.1.1Hotfix wychodzi z v1.1 na main. Poprawka trafia do main jako v1.1.1 oraz do develop. Diagram zakłada brak aktywnej gałęzi release. mainhotfixdevelop v1.1v1.1.1pilna poprawka

Nie trzeba wydawać całego niestabilnego develop, aby poprawić błąd produkcyjny.

Przy aktywnej gałęzi release hotfix przenosimy także do niej; późniejsze scalenie release zasili develop.

Proces powinien pasować do sposobu wydawania

Więcej gałęzi ≠ lepsza integracja

GitHub Flow

Main i krótkie feature branches. Pasuje do częstej integracji niewielkich zmian.

Checks i review chronią powrót zmiany do main.

Git Flow

Osobne linie rozwoju i stabilizacji. Może pasować do wersjonowanych wydań przygotowywanych przez dłuższy czas.

Checks muszą chronić również właściwe gałęzie develop i release.

Autor Git Flow sam zaleca prostszy model dla oprogramowania dostarczanego ciągle.

03 // Narzędzia

Proces CI i miejsce wykonania

Platforma organizuje pracę. Runner ją wykonuje. To nie musi być ta sama usługa.

PlatformyIntegracjeInfrastruktura buildów
Te same potrzeby // różne ekosystemy

Nie tylko GitHub Actions

PlatformaGdzie pasuje?Co warto uwzględnić?
GitHub ActionsWorkflow obok repozytorium i PR-ów GitHuba.Actions, runnery i polityki organizacji.
GitLab CI/CDPipeline i merge request w GitLab.Wariant chmurowy lub własna instalacja.
JenkinsSerwer automatyzacji z agentami i wtyczkami.Elastyczność oraz koszt utrzymania.
Azure PipelinesAzure DevOps, także z repozytoriami GitHub.Agenci, połączenia i uprawnienia.
CircleCIZewnętrzna platforma połączona z repozytorium.Własny pipeline i wynik odsyłany do PR-a.
TeamCityAutomatyzacja JetBrains: Cloud lub On-Premises.Konfiguracje buildów i zarządzanie agentami.
Nie każda usługa CI zastępuje orkiestrator

Kto planuje? Kto wykonuje?

GitHub Actions
zdarzenie + graf jobs
→
Runner
maszyna wykonawcza
→
Build / test / wynik

Blacksmith

Zarządzana infrastruktura runnerów dla GitHub Actions. Workflow może pozostać w GitHubie, a job działa na runnerze dostawcy.

Depot

Zarządzane runnery GitHub Actions oraz zdalna infrastruktura budowania, m.in. obrazów kontenerowych z BuildKit.

Porównujemy zgodność środowiska, cache, sieć, koszt i granice zaufania — nie tylko czas jednego builda.

Standardowe runnery GitHub-hosted są bezpłatne dla publicznych repozytoriów. Limity prywatnych zależą od planu; większe runnery są płatne także publicznie.

04 // Pierwsze uruchomienie

Od kilku kluczy do działającego workflow

YAML opisuje strukturę. Schemat GitHub Actions nadaje tej strukturze znaczenie.

Mapy i listyHello CIEdytor i walidacja
Fragment struktury workflow

Mapa ma klucze. Lista ma elementy.

jobs:
  hello:
    runs-on: ubuntu-24.04
    steps:
      - run: echo "Cześć"
      - run: pwd
  • klucz: wartość tworzy wpis w mapie.
  • hello jest kluczem mapy jobs.
  • - rozpoczyna element listy steps.
  • Wcięcia ze spacji pokazują zagnieżdżenie.

Workflow zapisujemy w repozytorium jako plik .github/workflows/hello.yml lub .yaml.

Cały plik .github/workflows/hello.yml

Pierwszy workflow: Hello CI

name: Hello CI
on: push
permissions: {}
jobs:
  hello:
    name: Powitanie
    runs-on: ubuntu-24.04
    steps:
      - name: Wiadomość
        shell: bash
        run: echo "CI działa!"
      - name: System
        shell: bash
        run: uname -s

Po wysłaniu pliku do GitHuba zdarzenie push uruchomi jedno zadanie z dwoma krokami.

Nie potrzebujemy repozytorium na dysku runnera ani uprawnień do API. Wykonujemy tylko polecenia powłoki.

System wybraliśmy jawnie: ubuntu-24.04.

Plik → zdarzenie → wykonanie

Powiąż wynik z commitem

  1. Na liście Actions znajdź Hello CI.
  2. W run sprawdź zdarzenie push, gałąź i SHA.
  3. Graf zawiera job Powitanie.
  4. W logu kroku Wiadomość odczytaj CI działa!.
GitHub · widok z przeglądarki

Hello CI: wynik dla commita

Podsumowanie uruchomienia Hello CI w GitHub Actions: push, gałąź, SHA i job Powitanie.
Zdarzenie, rewizja i wynik joba w jednym podsumowaniu.Otwórz pełny rozmiar ↗
Co dodaje runner?

Nie każdy krok logu pochodzi z YAML

Set up job

Przygotowanie środowiska: obraz systemu, runner i efektywne uprawnienia tokena.

Wiadomość → System

Nasze kroki, w kolejności z pliku. uname -s pokaże system runnera, nie laptopa autora.

Po pracy mogą pojawić się także kroki sprzątania. Nazwa kroku ułatwia połączenie logu z definicją.

GitHub · widok z przeglądarki

Runner: Set up job

Log joba Powitanie z rozwiniętym Set up job, informacją o obrazie runnera i uprawnieniach tokena.
Przygotowanie środowiska jest osobnym krokiem, przed poleceniami z YAML.Otwórz pełny rozmiar ↗
Oba fragmenty oznaczają tę samą strukturę

Lista nie zawsze wymaga dodatkowego wcięcia

steps:
  - name: Wiadomość
    run: echo "Cześć"
  - name: Katalog
    run: pwd
steps:
- name: Wiadomość
  run: echo "Cześć"
- name: Katalog
  run: pwd
  • YAML dopuszcza niewciętą sekwencję blokową jako wartość klucza mapy.
  • name i run nadal muszą należeć do tego samego elementu.
  • Wybierz jedną konwencję. Nie zastępuj spacji tabulatorami.
Składnia YAML ≠ schemat Actions

on: push to skalar zaakceptowany przez Actions

Skalar

on: push

Lista

on:
  - push
  - pull_request

Mapa z konfiguracją

on:
  push:
    branches: [main]

YAML pozwala przypisać kluczowi tekst, listę lub mapę. Schemat GitHub Actions dopuszcza te postacie właśnie dla on.

To nie znaczy, że dowolny klucz workflow przyjmuje dowolny typ wartości.

Kiedy cytować wartość?

Cudzysłów chroni znaczenie tekstu

name: "Build: API #1"
env:
  FLAG: "false"
  NOTICE: "! to jest tekst"
  LABEL: 'Raport #1'

# Komentarz, nie wartość
permissions: {contents: read}
  • false jest wartością logiczną, a "false" tekstem.
  • : ze spacją może wprowadzić kolejny wpis mapy.
  • # rozpoczyna komentarz, gdy stoi poza cytowanym skalarem w miejscu komentarza, np. po spacji.
  • "Raport #1" zachowuje znak # jako część tekstu.
Dwie reprezentacje tekstu

| zachowuje linie. >- je składa.

Skrypt z kilkoma poleceniami

- shell: bash
  run: |
    echo "Początek"
    pwd
    uname -s

Jedno polecenie

- run: >-
    printf '%s\n'
    'Jeden argument tekstowy'

> zwykle składa kolejne linie w spacje; znak - usuwa końcowy znak nowej linii. Puste i dodatkowo wcięte linie mają osobne reguły.

Wewnątrz run: | znajduje się tekst skryptu. Jego komentarze i cudzysłowy interpretuje już powłoka.

Mapy, listy, skalary

JSON mieści się w YAML

on: [push, workflow_dispatch]
permissions:
  contents: read
{
  "on": ["push", "workflow_dispatch"],
  "permissions": {"contents": "read"}
}

YAML 1.2 jest nadzbiorem JSON. Wartość można zapisać w stylu JSON; da się tak zapisać również cały workflow.

Plik nadal umieszczamy w .github/workflows/ z rozszerzeniem .yml lub .yaml, nie .json.

Jeden plik // kilka interpreterów

W run i script jest kolejny język

Tylko YAML

- shell: bash
  run: echo "$GITHUB_SHA"
- uses: actions/github-script@v9
  with: {script: "core.notice('CI');"}
- if: ${{ github.ref == 'refs/heads/main' }}
  shell: bash
  run: echo main

YAML + osadzone języki

- shell: bash
  run: echo "$GITHUB_SHA"
- uses: actions/github-script@v9
  with: {script: "core.notice('CI');"}
- if: ${{ github.ref == 'refs/heads/main' }}
  shell: bash
  run: echo main

Tekst jest identyczny. Po prawej mechanizm strony koloruje jawny Bash, github-script i expressions podczas budowania HTML. To nie jest symulacja interfejsu IDE.

IDE · rzeczywisty widok edytora

Jeden workflow, kilka języków

Workflow w IDE: YAML, skrypt Bash, JavaScript w actions/github-script i warunek GitHub expressions, z komentarzami language injection.
Kolorowanie osadzonych języków w prawdziwym edytorze, nie w podglądzie HTML.Otwórz pełny rozmiar ↗
Różne narzędzia sprawdzają różne warstwy

Podpowiedź, walidacja, inspekcja

JetBrains / VS Code

Schemat workflow, podpowiedzi inputów with i nawigacja do metadata action.

# language=githubexpressionlanguage
if: >-
  ${{ github.ref == 'refs/heads/main' }}

Komentarz jest wskazówką language injection dla odpowiednio skonfigurowanego IDE JetBrains; nie zmienia wykonania w GitHubie.

CLI

actionlint
yq '.jobs.hello.steps' \
  .github/workflows/hello.yml

actionlint: błędy specyficzne dla Actions.
yq: odczyt struktury YAML, nie pełna walidacja workflow.

IDE · rzeczywisty widok edytora

Podpowiedzi wynikają z metadanych action

Autocomplete w with dla actions/github-script: github-token i user-agent wraz z opisami; domyślne github.token jest wyrażeniem, nie ujawnioną wartością tokenu.
IDE podpowiada nazwy inputów, ich opisy i wartości domyślne.Otwórz pełny rozmiar ↗
05 // Anatomia wykonania

Co uruchomiliśmy w Hello CI?

Oddziel definicję procesu, konkretne wykonanie i środowisko, w którym działa kod.

Workflow i runJob i runnerStep i action
Definicja ≠ wykonanie

Workflow opisuje. Run jest zdarzeniem w historii.

Workflow
plik YAML
→
Run
jedno uruchomienie
→
Jobs
zadania na runnerach
→
Steps
kolejne czynności
  • Runner to środowisko wykonujące job.
  • Step uruchamia polecenie run albo komponent uses.
  • Action to komponent wywołany w kroku, nie dodatkowy poziom między jobem a runnerem.
Nie każde id jest polem id w YAML

name to etykieta. id to identyfikator.

name: Kontrola projektu
run-name: Sprawdzenie zmiany
jobs:
  verify:
    name: Build i testy
    runs-on: ubuntu-24.04
    steps:
      - name: Wersja systemu
        id: system
        run: uname -s
  • name workflow jest widoczne na liście Actions.
  • run-name opisuje konkretne uruchomienie.
  • verify jest identyfikatorem joba — kluczem mapy.
  • system jest opcjonalnym id kroku.
  • GitHub nadaje też własne identyfikatory workflow i runs używane w API.
Kolejność w pliku nie jest zależnością jobs

Kroki kolejno. Jobs mogą równolegle.

jobs:
  lint:
    runs-on: ubuntu-24.04
    steps:
      - run: echo "Krok 1"
      - run: echo "Krok 2"
  docs:
    runs-on: ubuntu-24.04
    steps:
      - run: echo "Osobny job"

W lint drugi krok czeka na pierwszy. Oba korzystają z workspace tego samego joba.

lint i docs mogą działać jednocześnie, jeśli dostępne są runnery.

Standardowe joby GitHub-hosted mają osobne środowiska. Plik utworzony w jednym nie pojawia się automatycznie w drugim.

Jawna krawędź grafu

needs mówi: najpierw to zadanie

jobs:
  build:
    runs-on: ubuntu-24.04
    steps:
      - run: echo "Budowanie"
  verify:
    needs: build
    runs-on: ubuntu-24.04
    steps:
      - run: echo "Weryfikacja"
build
→
verify

verify domyślnie ruszy dopiero po sukcesie build.

Lista needs: [build, lint] oznacza oczekiwanie na obie zależności.

Zależność ustala kolejność, ale nie kopiuje plików.

GitHub · widok z przeglądarki

Graf: równoległość i zależność

Graf GitHub Actions z dwoma niezależnymi zadaniami i zadaniem połączonym zależnością needs.
Strzałka oznacza zależność; niezależne jobs mogą wykonywać się równolegle.Otwórz pełny rozmiar ↗
Dwie postacie kroku

Polecenie albo gotowy komponent

run

- name: Pokaż narzędzie
  shell: bash
  run: dotnet --info

Uruchamia skrypt w wybranej powłoce. Można ustawić working-directory.

uses + with

- name: Przygotuj .NET
  uses: actions/setup-dotnet@v6
  with:
    dotnet-version: '10.x'

with przekazuje inputy opisane przez autora action. Ich nazwy sprawdzamy w dokumentacji lub action.yml.

Nie łączymy run i uses w jednym kroku. Komenda node skrypt.js nadal jest krokiem run, a nie automatycznie JavaScript action.

Środowisko nie wynika z systemu autora

System wybiera runs-on

PrzykładŚrodowiskoTypowa powłoka
ubuntu-24.04GitHub-hosted LinuxBash
windows-latestGitHub-hosted WindowsPowerShell
macos-latestGitHub-hosted macOSBash
[self-hosted, linux, x64]Runner z pasującymi etykietamiZależna od konfiguracji

Actions nie działa zawsze na Linuksie ani na Twoim laptopie. W tych przykładach wybieramy Ubuntu, żeby pokazywać jedną spójną składnię Bash.

Lista etykiet wymaga ich jednoczesnego dopasowania; nie oznacza kilku systemów do uruchomienia.

Dwie osobne odpowiedzialności

Kod i narzędzia przygotowujemy osobno

steps:
  - uses: actions/checkout@v7
  - uses: actions/setup-node@v7
    with:
      node-version: '24'
      package-manager-cache: false
  - run: node --version
  • Checkout pobiera kod do GITHUB_WORKSPACE.
  • Setup udostępnia wybraną wersję narzędzia i ustawia środowisko.
  • Setup Node nie instaluje automatycznie zależności aplikacji.
  • Do samego echo albo odczytu API checkout nie jest potrzebny.

Numer przy nazwie action określa jej wersję. Wersja instalowanego Node jest osobnym inputem.

Fragmenty trzech różnych plików action.yml

Typ action zapisuje jej autor

JavaScript

runs:
  using: node24
  main: dist/index.js

Zapakowany kod JS uruchamiany przez runnera.

Docker

runs:
  using: docker
  image: Dockerfile

Kod i narzędzia w obrazie. Wymaga runnera Linux z Dockerem.

Composite

runs:
  using: composite
  steps:
    - shell: bash
      run: echo "CI"

Zestaw kroków w istniejącym jobie.

Wszystkie wywołujemy przez step z uses. Action nie wybiera własnego runs-on. Runtime JS action jest niezależny od Node instalowanego dla aplikacji.

Dwa różne zastosowania kontenerów

Gdzie działa skrypt? Gdzie działa baza?

container — środowisko joba

runs-on: ubuntu-24.04
container: node:24-bookworm
steps:
  - run: node --version

Kroki run wykonują się wewnątrz kontenera joba. Domyślną powłoką jest tu sh, nie Bash.

services — proces obok joba

runs-on: ubuntu-24.04
services:
  postgres:
    image: postgres:17
    env:
      POSTGRES_PASSWORD: ci-only
    ports: ['5432:5432']

Skrypt działa na runnerze, baza w osobnym kontenerze. Tutaj łączy się przez localhost:5432.

Gdy sam job działa w kontenerze, usługę osiąga po nazwie postgres:5432. Przed testami trzeba zaczekać na gotowość bazy, np. przez healthcheck.

Kontener implementacji action

Docker action nie zamienia całego joba w kontener

# Fragment steps w jobie na runnerze Linux
- run: uname -s
- uses: docker://alpine:3.22
  with:
    entrypoint: /bin/sh
    args: -c "uname -s"
- run: echo "Znowu krok na runnerze"
  • Kontener dostarcza implementację tego jednego kroku.
  • To nie jobs.<id>.container ani długotrwała usługa z services.
  • GameCI unity-builder@v4 uruchamia kontenery, ale ma runs.using: node20. To JavaScript action, nie Docker action.
06 // Kiedy i dla jakiego kodu?

Trigger wybiera więcej niż moment startu

Zdarzenie określa kontekst, sprawdzaną rewizję i granicę zaufania.

Push i PRSchedule i dispatchRef, SHA i merge ref
Przykład dla main i krótkich feature branches

PR przed integracją. Push po integracji.

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
  • push sprawdza commit wysłany do main, także po merge.
  • pull_request domyślnie reaguje na otwarcie, ponowne otwarcie i synchronizację PR-a.
  • Filtr pull_request.branches dotyczy gałęzi docelowej, nie gałęzi autora.

W Git Flow trzeba uwzględnić również PR-y do develop lub release. Push na każdą gałąź obok triggera PR może tworzyć dwa runs dla jednej zmiany.

Podobna nazwa // inna granica zaufania

Uwaga: pull_request_target

pull_request

Do budowania i testowania proponowanej zmiany. Fork PR ma domyślnie ograniczony token i nie otrzymuje sekretów repozytorium.

pull_request_target

Workflow w kontekście bazowego repozytorium. Może mieć dostęp do prawa zapisu i sekretów. Służy np. do zaufanej automatyzacji metadanych.

Nie pobieraj i nie wykonuj niezaufanego kodu PR-a z uprawnieniami bazowego repozytorium.

Zmiana triggera nie jest bezpieczną „naprawą” braku sekretów w zwykłym PR.

Okresowe uruchomienie // GitHub.com

Harmonogram korzysta z gałęzi domyślnej

on:
  schedule:
    - cron: '15 5 * * 1-5'
      timezone: Europe/Warsaw

05:15 od poniedziałku do piątku w strefie Europe/Warsaw. Bez timezone domyślną strefą jest UTC.

  • Cron: minuta, godzina, dzień miesiąca, miesiąc, dzień tygodnia. Najkrótszy obsługiwany interwał to 5 minut.
  • Plik workflow i wykonywany commit pochodzą z gałęzi domyślnej.
  • Obciążenie usługi może opóźnić lub spowodować pominięcie run. To nie zegar czasu rzeczywistego.
  • W publicznym repozytorium 60 dni bez aktywności wyłącza zaplanowane uruchomienia.
Decyzja człowieka staje się zdarzeniem

Ręczny start przez Run workflow

on:
  workflow_dispatch:
  • Najpierw umieść workflow na gałęzi domyślnej, aby korzystać z przycisku Run workflow.
  • W UI wybieramy gałąź uruchomienia; API i CLI również potrafią wywołać dispatch.
  • Można zdefiniować formularz inputów — zrobimy to w następnej sekcji.
  • Checks z workflow_dispatch nie spełniają wymaganych checks w rulesecie gałęzi, nawet dla tego samego SHA. Do sprawdzeń PR używamy pull_request.
Pominięcie workflow nie oznacza sukcesu

Skip jest częścią komunikatu commita

Krótki znacznik

docs: popraw opis [skip ci]

Także: [ci skip], [skip actions], [actions skip], [no ci].

Pełny komunikat z trailerem

docs: popraw opis

Uzupełnij przykład w dokumentacji.


skip-checks: true

Przed trailerem są dwie puste linie. git commit --cleanup=verbatim pozwala je zachować.

Dotyczy push i pull_request; dla PR liczy się komunikat HEAD commita. Nie wyłącza pull_request_target, schedule ani dispatch.

Wymagany check pominiętego workflow może pozostać Pending / Expected i blokować merge.

Otwarty PR #42: feature/date → main

Którą rewizję sprawdza PR?

Polepush do mainpull_request #42
github.refrefs/heads/mainrefs/pull/42/merge
github.shaCommit z pushPrzygotowany commit scalenia PR-a
github.head_refPusty tekstfeature/date
github.base_refPusty tekstmain
github.event.pull_request.head.shaBrak pola PROstatni commit gałęzi autora

Domyślny checkout dla zwykłego PR sprawdza przygotowane połączenie zmian z bazą, a nie tylko HEAD autora.

Zielony wynik opisuje konkretną rewizję

Main może pójść naprzód po teście

Test połączenia PR-a z bazą nie zamraża mainFeature wychodzi z A. Main przechodzi do B. Testowy merge łączy B i F. Późniejszy commit C na main nie jest częścią wcześniejszego testowanego połączenia. mainfeature ABCFtest B + F
  • Merge ref pozwala testować połączenie nawet wtedy, gdy feature powstał ze starszego main.
  • Konflikt scalenia może uniemożliwić start zwykłego workflow PR.
  • Nie zakładaj, że każdy późniejszy push do main ponownie sprawdzi wszystkie otwarte PR-y.

Wymaganie aktualnej bazy albo merge queue pomaga kontrolować stan tuż przed scaleniem.

Dwa niezależne wybory

@v7 nie jest wersją aplikacji

- uses: actions/checkout@v7
  with:
    ref: ${{ github.sha }}
ZapisCo wybiera?
actions/checkout@v7Kod action checkout w repozytorium jej autora.
with.refRewizję repozytorium pobieraną przez checkout.
owner/repo/path@refKatalog z metadata action przy wskazanym ref.
./.github/actions/backendLokalny katalog action, bez dopisywania @ref.

Zwykle pomijamy with.ref, pozostawiając domyślną rewizję zdarzenia. W samym uses ref musi być statyczny — nie tworzymy go wyrażeniem.

07 // Konfiguracja wykonania

Skąd workflow bierze dane?

Formularz, zdarzenie, runner i ustawienia repozytorium są różnymi źródłami wartości.

InputsEnv, vars, secretsGITHUB_TOKEN
Fragment on.workflow_dispatch

Zadeklaruj formularz uruchomienia

on:
  workflow_dispatch:
    inputs:
      configuration:
        description: Konfiguracja builda
        type: choice
        options: [Debug, Release]
        default: Release
        required: true
      verbose:
        description: Pokaż szczegóły
        type: boolean
        default: false

choice tworzy wybór wartości. boolean daje przełącznik.

Nazwy configuration i verbose będą kluczami kontekstu inputs.

Input nie jest miejscem na hasło czy token.

GitHub · widok z przeglądarki

Inputy: formularz Run workflow

Rozwinięty formularz Run workflow z gałęzią, konfiguracją Debug lub Release i polem Pokaż szczegóły.
Typy choice i boolean tworzą różne kontrolki formularza.Otwórz pełny rozmiar ↗
Odczyt pól z poprzedniego slajdu

Wartość formularza trafia do inputs

run-name: "CI · ${{ inputs.configuration }}"

Odczyt tego samego inputu w liście steps:

- name: Pokaż wybrany tryb
  env:
    CONFIGURATION: ${{ inputs.configuration }}
  run: |
    printf 'Tryb: %s\n' "$CONFIGURATION"
- name: Dodatkowe informacje
  if: ${{ inputs.verbose }}
  run: echo "Włączono szczegółowe informacje"
  • ${{ ... }} odczytuje wartość w silniku Actions.
  • env przekazuje ją jako tekst do procesu Bash.
  • if może bezpośrednio użyć boolean z typowanego inputu.
Typ zależy od granicy

Typy deklarujemy na wejściu workflow

InterfejsTypy / sposób odczytu
workflow_dispatch.inputsstring, boolean, number, choice, environment. Choice i environment wybierają tekst.
workflow_call.inputsstring, boolean, number. Caller przekazuje zgodną wartość.
Inputy action, także compositeTekst; brak deklaracji type: boolean jak w workflow_call.
env i outputsTekst, nawet jeśli wygląda jak false, liczba lub JSON.

W dispatch używaj kontekstu inputs, który zachowuje typy. Wariant github.event.inputs przekazuje wartości tekstowe.

Konsument nadal powinien sprawdzić dozwolone wartości i zakres, np. czy liczba jest dodatnia.

Odczytuj konkretne pola, nie cały obiekt

Kontekst ma konkretne źródło

PolePrzydatna informacja
github.event_nameRodzaj zdarzenia, np. pull_request.
github.ref / github.shaRef i rewizja konkretnego zdarzenia.
github.actorKto zainicjował uruchomienie; to nie zawsze autor PR-a.
github.event.pull_request.user.loginAutor PR-a, gdy zdarzenie zawiera ten obiekt.
runner.os / runner.archSystem i architektura, np. Linux / X64.
runner.tool_cacheKatalog narzędzi runnera; nie cache zależności aplikacji.

Nie każdy kontekst jest dostępny w każdym polu. Na przykład runner nie istnieje jeszcze podczas oceny warunku joba.

Konfiguracja procesu i ustawienia repozytorium

Jawna konfiguracja i chronione wartości

env:
  CONFIGURATION: ${{ vars.BUILD_MODE }}
  DOCS_TOKEN: ${{ secrets.DOCS_TOKEN }}

env udostępnia zmienne procesowi. Samo wstawienie wartości do env nie sprawia, że jest ona sekretna.

  • vars: niesekretna konfiguracja organizacji, repozytorium lub environment.
  • secrets: chronione wartości dostępne tylko w dopuszczonym kontekście.
  • Nie wypisujemy całych kontekstów ani wartości sekretów.
GitHub · widok z przeglądarki

Variables: konfiguracja Actions

Zakładka Variables w ustawieniach GitHub Actions z nazwą BUILD_MODE i zasłoniętymi wartościami zmiennych.
BUILD_MODE odczytujemy przez vars. Wartości zasłonięto na potrzeby zrzutu.Otwórz pełny rozmiar ↗
GitHub · widok z przeglądarki

Secrets: nazwa, nie wartość

Zakładka Secrets w ustawieniach GitHub Actions pokazująca nazwy sekretów, bez ich wartości.
Lista pozwala sprawdzić nazwę sekretu bez pokazywania chronionej wartości.Otwórz pełny rozmiar ↗
Zanim wywołamy API

Token dostaje tylko potrzebne prawa

# Domyślnie w workflow
permissions:
  contents: read

# Osobno w jobie komentującym PR
# permissions:
#   pull-requests: write
  • GITHUB_TOKEN jest krótkotrwałym tokenem dostarczanym dla joba.
  • contents: read wystarcza do checkout kodu.
  • Odczyt listy plików PR wymaga odpowiedniego odczytu PR; komentarz wymaga zapisu.
  • Gdy określamy zestaw permissions, niewymienione uprawnienia mają wartość none.

Permissions joba określa jego zestaw praw — nie dopisuje automatycznie pozostałych praw z workflow. Fork i polityki organizacji mogą dodatkowo ograniczyć dostęp.

08 // Wybór i warunki

Kiedy wykonać krok? Jaką wybrać wartość?

Expressions to mały język GitHub Actions, nie JavaScript ani Bash.

IfFunkcjeTypy i case()
Najpierw pojedynczy warunek

if decyduje przed uruchomieniem kroku

- name: Informacja po zmianie main
  if: ${{ github.ref == 'refs/heads/main' }}
  run: echo "To uruchomienie dotyczy main"
  • == porównuje wartości. Tekst wewnątrz expression zapisujemy w pojedynczych apostrofach.
  • W polu if zwykle wolno pominąć ${{ }}.
  • if: ${{ !cancelled() }} chroni początkowe ! przed interpretacją jako tag YAML.
  • Job-level if jest oceniane przed przydziałem runnera i rozwinięciem matrix.
Łączenie warunków i zapis wieloliniowy

Warunek może być czytelny także w kilku liniach

- name: Informacja dla autora PR
  if: >-
    ${{ github.event_name == 'pull_request'
        && github.base_ref == 'main'
        && github.event.pull_request.user.login != 'dependabot[bot]' }}
  run: echo "PR do main od autora innego niż Dependabot"

&& wymaga spełnienia wszystkich warunków; || daje alternatywę. Nawiasy pozwalają jawnie grupować warunki.

Najpierw sprawdzamy typ zdarzenia. W PR gałąź docelowa jest w base_ref, a autor w payloadzie PR-a.

Literówka może nie zgłosić błędu

Brak pola może oznaczać ''

env:
  TITLE: ${{ vars.REPORT_TITLE || 'Raport CI' }}
  SHA: ${{ github['sha'] }}
  • Odczyt nieistniejącej właściwości zwraca pusty tekst.
  • github.sha i github['sha'] wskazują to samo pole.
  • || jest wygodnym fallbackiem dla niepustej etykiety.

Brak właściwości w dostępnym kontekście to coś innego niż użycie niedozwolonego kontekstu albo błędna składnia expression.

Jeśli wartość jest obowiązkowa, waliduj ją zamiast ukrywać brak domyślnym tekstem.

Pytania o tekst i budowanie etykiet

Małe funkcje, konkretne zastosowania

WyrażenieWynik / zastosowanie
contains('frontend', 'end')true — fragment tekstu.
startsWith(github.ref, 'refs/tags/')Czy zdarzenie dotyczy tagu?
endsWith('ApiTests.cs', 'Tests.cs')true — końcówka tekstu.
format('CI #{0}', github.run_number)Etykieta, np. CI #42.
join(fromJSON('["api","ui"]'), ' + ')api + ui.

contains dla listy szuka całego elementu, a dla tekstu podtekstu. Funkcje porównujące tekst ignorują wielkość liter.

Tekst JSON a wartość w wyrażeniu

fromJSON odtwarza typ wartości

WyrażenieWynik
fromJSON('false')Boolean false.
fromJSON('3') > 1true — porównanie liczb.
fromJSON('["backend","frontend"]')Lista dwóch tekstów.
toJSON(true)Tekst JSON: true.

Pusty lub niepoprawny JSON powoduje błąd parsowania. Poprawny JSON nadal wymaga sprawdzenia oczekiwanego kształtu i wartości.

Przyda się przy przenoszeniu listy komponentów do matrix. Nie serializujemy całego kontekstu github z tokenem do logów.

Funkcja czyta pliki w workspace

Brak pliku to pusty hash, nie wyjątek

# Fragment steps, po checkout
- run: test -f package-lock.json
- name: Odcisk zależności
  env:
    LOCK_HASH: ${{ hashFiles('package-lock.json') }}
  run: printf '%s\n' "$LOCK_HASH"
  • hashFiles tworzy odcisk SHA-256 zestawu dopasowanych plików.
  • Wzorce odnoszą się do GITHUB_WORKSPACE, nie do working-directory kroku.
  • Brak dopasowań zwraca ''. Jeśli plik jest wymagany, osobny test -f wykrywa jego brak.
Nie ma operatora JavaScript ?:

A && B || C nie zawsze wybiera B

Wyrażenie / wartośćCo się stanie?
true && 'Release' || 'Debug'Release — niepusty tekst jest truthy.
true && 0 || 22 — zero uruchamia fallback.
true && false || truetrue — zamierzone false zostaje zastąpione.
false, 0, null, ''Wartości falsy.
'false', '0'Niepuste teksty, czyli truthy.

Dlatego tekstowej flagi z env nie używamy jako samodzielnego warunku: porównujemy ją do 'true' albo świadomie parsujemy.

case(warunek, wartość, …, domyślna)

case() zachowuje także false i zero

env:
  RUN_KIND: >-
    ${{ case(
      github.event_name == 'pull_request',
      'PR',
      startsWith(github.ref, 'refs/tags/'),
      'tag',
      'branch'
    ) }}

Wygrywa pierwsza pasująca para. Ostatni argument jest wartością domyślną.

Warunki muszą mieć typ boolean. Tekst 'true' ani liczba 1 go nie zastępują.

  • case(true, 0, 2) → 0
  • case(true, false, true) → false
  • case(true, '', 'fallback') → pusty tekst

Pary warunek–wynik i końcowy default dają nieparzystą liczbę argumentów, co najmniej 3. Po przekazaniu wyniku do env liczba lub boolean stają się tekstem.

Co po błędzie poprzedniego kroku?

Warunek statusu zmienia dalszy przebieg

WarunekTypowe zastosowanie
success()Domyślna kontynuacja po sukcesie; dodawana do if bez funkcji statusu.
failure()Dodatkowa diagnostyka po niepowodzeniu.
!cancelled()Zebranie wyniku także po błędzie, ale nie po anulowaniu.
always()Warunek prawdziwy także po błędzie i anulowaniu, np. dla końcowego gate.

Zielony krok zbierania raportu nie cofa błędu testów. Awaria runnera może uniemożliwić nawet krok z always().

09 // Wykonanie pracy

Workflow organizuje. Skrypt wykonuje.

Komendy, pliki i API wymagają kodu. Dobierz język do zadania, nie do długości YAML.

Bash i powłokiLogi i annotationsgithub-script i .NET
Fragment steps w repozytorium .NET

run: | zawiera zwykły skrypt

- name: Testy w wybranej konfiguracji
  shell: bash
  env:
    CONFIGURATION: ${{ inputs.configuration }}
  run: |
    case "$CONFIGURATION" in
      Debug|Release) ;;
      *) echo "::error::Nieznana konfiguracja"; exit 1 ;;
    esac
    dotnet test --configuration "$CONFIGURATION"

Actions przygotowuje env, a Bash odczytuje "$CONFIGURATION". Cudzysłów zachowuje wartość jako jeden argument.

Walidacja kontraktu jest częścią skryptu. SDK i kod repozytorium muszą być przygotowane we wcześniejszych krokach.

Wspólne ustawienia poleceń

Ustal domyślną powłokę i katalog

defaults:
  run:
    shell: bash
    working-directory: backend
  • Te wartości dotyczą kroków run w workflow.
  • Ścieżka musi istnieć, np. po checkout.
  • Nie zmieniają katalogu ani znaczenia inputów action wywołanej przez uses.

Powtarzającą się konfigurację zapisujemy raz, a lokalny wyjątek umieszczamy przy kroku, którego dotyczy.

Workflow → job → step

Najbliższe ustawienie wygrywa

# Job nadpisuje poprzednie defaults
jobs:
  docs:
    runs-on: ubuntu-24.04
    defaults:
      run:
        working-directory: docs
    steps:
      - uses: actions/checkout@v7
      - run: pwd
      - run: pwd
        working-directory: .

Workflow wybrał Bash i backend.

Job zmienia katalog na docs. Pierwszy pwd pokaże katalog dokumentacji.

Ostatni krok wybiera ., czyli korzeń workspace. Powłoką nadal jest Bash.

Kolor tekstu nie jest kodem zakończenia

Log z tee nie może ukryć błędu

- name: Testy z kopią logu
  shell: bash
  run: |
    set -u
    dotnet test --configuration Release 2>&1 | tee tests.log
    echo "Testy zakończone sukcesem"
  • Kod 0 oznacza sukces; niezerowy kod oznacza błąd.
  • Jawne shell: bash dodaje -e i -o pipefail. Błąd testów nie ginie za udanym tee.
  • set -u wykrywa użycie niezdefiniowanej zmiennej.
  • -e ma wyjątki, np. w warunkach if i listach &&/||. Krytyczny warunek sprawdzaj jawnie.

Domyślna powłoka bez jawnego shell może używać innych opcji. Nie dodawaj || true do obowiązkowych testów.

Wybór interpretera i runtime

Skrypt nie musi być Bashem

Custom shell · .NET 10

- uses: actions/setup-dotnet@v6
  with:
    dotnet-version: '10.x'
- name: Program C# jako krok
  shell: dotnet run --file {0}
  run: |
    var title = "Build";
    Console.WriteLine($"Length: {title.Length}");

Runner podstawia ścieżkę pliku za {0}. Z --file rozszerzenie .tmp nie przeszkadza; wynik to Length: 5.

Program plikowy .NET 10

// scripts/summary.cs
var title = "Build";
Console.WriteLine($"Length: {title.Length}");
dotnet run --file scripts/summary.cs

Ten sam program można zapisać w repozytorium. Wymaga SDK i pliku po checkout, ale nie osobnego .csproj.

Program plikowy uruchamiany przez dotnet run to nie dotnet-script. Ten drugi jest osobnym narzędziem.

Komunikat dla runnera, nie tylko tekst w terminalu

Powiąż ostrzeżenie z plikiem i linią

echo "::group::Sprawdzanie dokumentacji"
echo "::notice::Rozpoczęto analizę"
echo "::warning file=README.md,line=7::Brakuje opisu parametru"
echo "::endgroup::"
  • ::notice::, ::warning:: i ::error:: tworzą adnotacje.
  • Nazwa pliku i linia pomagają pokazać problem przy zmianie w PR.
  • Samo ::error:: nie ustawia niezerowego exit code. Gdy problem ma zatrzymać job, skrypt musi również zgłosić błąd wykonania.
GitHub · widok z przeglądarki

Adnotacja: ostrzeżenie przy pliku

Ostrzeżenie w pull requeście wskazujące plik README.md i linię 7.
Komunikat ma poziom i lokalizację; nie jest tylko wierszem zwykłego logu.Otwórz pełny rozmiar ↗
Markdown w podsumowaniu run

Nie zmuszaj odbiorcy do czytania całego logu

{
  printf '## Sprawdzenie zmiany\n\n'
  printf '%s\n' '- Build zakończony.'
  printf '%s\n' '- Raport jest dostępny w artefaktach.'
} >> "$GITHUB_STEP_SUMMARY"

Runner składa treść podsumowania z kroków joba. Piszemy do pliku wskazanego przez zmienną środowiskową.

Podsumowanie opisuje wynik, ale nie zmienia statusu joba. W praktyce treść powinna wynikać z rzeczywiście wykonanych sprawdzeń.

GitHub · widok z przeglądarki

Summary: czytelny wynik joba

Podsumowanie GitHub Actions z sekcją Sprawdzenie zmiany wygenerowaną przez GITHUB_STEP_SUMMARY.
Treść GITHUB_STEP_SUMMARY pojawia się jako Markdown, poza logiem polecenia.Otwórz pełny rozmiar ↗
github-script // fragment joba dla on: pull_request // bez checkout

Komentarz do PR przez API

if: >-
  ${{ github.event.pull_request.head.repo.full_name
      == github.repository }}
runs-on: ubuntu-24.04
permissions: {pull-requests: write}
steps:
  - uses: actions/github-script@v9
    with:
      script: |
        await github.rest.issues.createComment({
          ...context.repo,
          issue_number: context.issue.number,
          body: 'CI rozpoczęło sprawdzanie tej zmiany.'
        });

PR jest także issue. To zwykły komentarz, nie review linii. Rerun utworzy kolejny.

GitHub · widok z przeglądarki

API: komentarz github-actions[bot]

Komentarz github-actions[bot] w PR o treści CI rozpoczęło sprawdzanie tej zmiany.
Wywołanie issues.createComment tworzy zwykły komentarz w Conversation.Otwórz pełny rozmiar ↗
Ciało asynchronicznej funkcji JavaScript

Klient API jest już przygotowany

ObiektRola
githubUwierzytelniony klient Octokit: REST, GraphQL i paginacja.
contextDane repozytorium i zdarzenia w JavaScript.
coreLogi, adnotacje, summary, outputs i zgłaszanie niepowodzenia.
if (!context.payload.pull_request) {
  core.setFailed('Oczekiwano zdarzenia PR');
  return;
}

await czeka na operację API. Wyjątek lub core.setFailed() oznacza błąd; samo return false nie oznacza czerwonego kroku.

Ciało script w github-script // pull-requests: read

Czy zmienił się frontend?

const pr = context.payload.pull_request;
if (!pr) throw new Error('Oczekiwano PR');
const files = await github.paginate(github.rest.pulls.listFiles, {
  ...context.repo, pull_number: pr.number, per_page: 100
});
if (files.length >= 3000) throw new Error('Przekroczono limit tej reguły');
const paths = files.flatMap(file =>
  [file.filename, file.previous_filename].filter(Boolean));
require('node:fs').writeFileSync(
  'changed-paths.json', JSON.stringify(paths));
const frontend = paths.some(path => path.startsWith('frontend/'));
core.notice(`Zmiana frontendu: ${frontend}`);

paginate pobiera kolejne strony; endpoint nadal ma limit 3000 plików. Przy zmianie nazwy uwzględniamy również poprzednią ścieżkę.

Plik changed-paths.json wykorzystamy w sekcji przepływu danych. Błędu API nie zamieniamy w pustą listę i sukces.

Wspólne wejście: env.CONFIGURATION

Ten sam wynik, trzy narzędzia

Bash · run

printf 'Tryb: %s\n' \
  "$CONFIGURATION"

github-script · script

core.info('Tryb: ' +
  process.env.CONFIGURATION);

.NET 10 · summary.cs

var mode = Environment
  .GetEnvironmentVariable(
    "CONFIGURATION");
Console.WriteLine(
  $"Tryb: {mode}");

Bash dobrze łączy CLI i pliki. github-script upraszcza GitHub API. .NET pozwala wykorzystać znany język i rozbudować testowalną logikę w osobnym pliku.

Małej komendy nie trzeba opakowywać w API klienta. Rosnący algorytm warto wydzielić z YAML do wersjonowanego skryptu.

10 // Reuse

Wspólna lista kroków czy cały job?

Porównamy dwa opakowania tej samej procedury backendu: build i test w wybranej konfiguracji.

Composite actionReusable workflowDołączone skrypty
.github/actions/backend/action.yml

Composite grupuje kroki backendu

name: Backend checks
description: Build and test
inputs:
  configuration:
    description: Build mode
    default: Release
runs:
  using: composite
  steps:
    - shell: bash
      env:
        CONFIGURATION: ${{ inputs.configuration }}
      run: |
        dotnet build --configuration "$CONFIGURATION"
        dotnet test --configuration "$CONFIGURATION"

Action korzysta z runnera i workspace callera. Zakładamy przygotowany kod .NET i SDK; każdy run w composite podaje własny shell.

.github/workflows/backend.yml

Reusable dostarcza cały job backendu

on:
  workflow_call:
    inputs:
      configuration: {type: string, default: Release}
permissions: {contents: read}
jobs:
  backend:
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-dotnet@v6
        with: {dotnet-version: '10.x'}
      - env: {CONFIGURATION: '${{ inputs.configuration }}'}
        run: |
          dotnet build --configuration "$CONFIGURATION"
          dotnet test --configuration "$CONFIGURATION"

Te same polecenia; teraz callee wybiera runnera i przygotowuje kod oraz SDK.

Ten sam backend // ten sam parametr Release

Composite w kroku. Reusable jako job.

Fragment zwykłego joba

runs-on: ubuntu-24.04
steps:
  - uses: actions/checkout@v7
  - uses: actions/setup-dotnet@v6
    with: {dotnet-version: '10.x'}
  - uses: ./.github/actions/backend
    with:
      configuration: Release

Fragment mapy jobs

backend:
  uses: ./.github/workflows/backend.yml
  with:
    configuration: Release

Obok tego uses nie dodajemy runs-on ani steps.

Lokalna action musi być na dysku po checkout. Lokalny reusable workflow GitHub odczytuje z commita callera; checkout wewnątrz niego służy pracy z kodem aplikacji.

Dwa katalogi: workspace aplikacji i katalog action

Action może dostarczyć własny skrypt

# Fragment runs.steps w composite
- shell: bash
  env:
    ACTION_DIR: ${{ github.action_path }}
    CONFIGURATION: ${{ inputs.configuration }}
  run: bash "$ACTION_DIR/backend.sh"
  • github.action_path wskazuje katalog z dołączonym backend.sh.
  • Zdalne uses: owner/repo/path@ref pobiera pakiet action i jego skrypty. Nie trzeba osobno checkoutować jej repozytorium.
  • Checkout kodu aplikacji jest nadal potrzebny, jeśli skrypt ma go budować.
  • Reusable workflow nie pobiera automatycznie wszystkich skryptów ze swojego repozytorium; domyślny checkout dotyczy repozytorium callera.

Composite współdzieli środowisko joba. Reusable ma własny kontrakt inputs i secrets; nie otrzymuje automatycznie env callera i nie może podnieść jego uprawnień tokena.

11 // Jeden scenariusz, kolejne granice

Wykryj zmiany. Zbuduj. Podsumuj wynik.

Rozwijamy przykład z API PR-a, nie kolekcję niezależnych flag demonstracyjnych.

changed-paths.json
→
Plan komponentów
→
Wybrane buildy
→
Podsumowanie

Ten workflow nazywamy Component CI. Przekazujemy krótkie wartości przez outputs, a pliki przez artefakty.

Najpierw konfiguracja w jednym jobie

Env ma lokalny zakres

env:
  CONFIGURATION: Release
jobs:
  build:
    runs-on: ubuntu-24.04
    env:
      CONFIGURATION: Debug
    steps:
      - run: echo "$CONFIGURATION"
      - env:
          CONFIGURATION: Release
        run: echo "$CONFIGURATION"
      - run: echo "$CONFIGURATION"

Kolejne wartości to Debug → Release → Debug.

Definicja kroku wygrywa tylko na czas tego kroku. Nie zmienia trwale konfiguracji joba.

Env nie jest kanałem automatycznie przenoszącym dane do innego joba lub reusable workflow.

Ten sam job // kolejne procesy

Nowy krok to nowa powłoka

- name: Ustal katalog raportów
  run: echo "REPORT_DIR=reports" >> "$GITHUB_ENV"
- name: Przygotuj katalog
  run: |
    mkdir -p "$REPORT_DIR"
    printf 'Raporty: %s\n' "$REPORT_DIR"
  • Zwykłe export w jednym run nie przechodzi do następnego procesu.
  • GITHUB_ENV jest plikiem odczytywanym przez runnera po kroku.
  • Zapisana wartość jest dostępna w następnych krokach tego joba, nie wstecznie w bieżącej powłoce.

To ustawienie środowiska. Dla nazwanego wyniku producenta użyjemy GITHUB_OUTPUT.

Skrypt Bash po kroku API z poprzedniej sekcji

Lista ścieżek staje się planem pracy

jq -ce '
  def changed($root):
    any(.[]; startswith($root + "/")
      or startswith(".github/")
      or . == "global.json" or . == ".nvmrc");
  {frontend: changed("frontend"),
   backend: changed("backend")}
' changed-paths.json > plan.json

jq przetwarza JSON, nie wyszukuje przypadkowych fragmentów tekstu. Zmiana wspólnej konfiguracji uruchamia w tej regule oba komponenty.

{"frontend":true,"backend":false}

To przykładowy wynik dla zmiany tylko w frontend/. Reguły muszą uwzględniać również rzeczywiste współdzielone zależności projektu.

Fragment steps // plan.json już istnieje

Producent publikuje nazwane wartości

- name: Opublikuj plan
  id: scan
  shell: bash
  run: |
    for part in frontend backend; do
      value=$(jq -r --arg p "$part" '.[$p]' plan.json)
      printf '%s=%s\n' "$part" "$value" >> "$GITHUB_OUTPUT"
    done
    echo "plan-path=plan.json" >> "$GITHUB_OUTPUT"

id: scan pozwala odczytać np. steps.scan.outputs.frontend. Każda linia nazwa=wartość dodaje output kroku.

Wyniki 'true' i 'false' są tekstami. plan-path jest ścieżką — sam output nie kopiuje pliku.

Konsumenci w tym samym jobie

Run odbiera env. Action odbiera with.

Krok run

- name: Odczytaj decyzję
  env:
    FRONTEND: ${{ steps.scan.outputs.frontend }}
  run: |
    printf 'Frontend: %s\n' "$FRONTEND"

Krok uses

- uses: actions/upload-artifact@v7
  with:
    name: component-plan
    path: ${{ steps.scan.outputs.plan-path }}
    if-no-files-found: error

Mapowanie jest jawne. Output nie tworzy automatycznie zmiennej Bash o tej samej nazwie.

Warunek wykonania może sprawdzić steps.scan.outputs.frontend == 'true'. Sam niepusty tekst 'false' byłby truthy.

Publikujemy wynik poza job changes

Step → job → needs

Dopisujemy do joba changes

outputs:
  frontend: ${{ steps.scan.outputs.frontend }}
  backend: ${{ steps.scan.outputs.backend }}

To mapowanie wyników istniejącego kroku scan.

Job odbierający plan

backend:
  needs: changes
  if: >-
    ${{ needs.changes.outputs.backend == 'true' }}
  uses: ./.github/workflows/backend.yml
  with:
    configuration: Release

Backend jest budowany tylko wtedy, gdy wymaga tego plan. needs udostępnia wyniki bezpośrednio wskazanych zależności, nie wszystkich jobs w workflow.

Job po wykrywaniu zmian i backendzie

Decyzja planu ≠ wynik wykonania

summary:
  needs: [changes, backend]
  if: ${{ always() }}
  runs-on: ubuntu-24.04
  steps:
    - env:
        NEEDED: ${{ needs.changes.outputs.backend }}
        RESULT: ${{ needs.backend.result }}
      run: |
        printf 'Backend: potrzebny=%s, wynik=%s\n' \
          "$NEEDED" "$RESULT" >> "$GITHUB_STEP_SUMMARY"

true + success oznacza wykonaną pracę. false + skipped oznacza świadome pominięcie. true + failure wymaga uwagi.

Summary opisuje te stany. Osobny required gate będzie później egzekwował, które połączenia akceptujemy.

Te same kroki API i planowania grupujemy w action changes

Composite wystawia wynik wewnętrznego kroku

Fragment action.yml

outputs:
  frontend:
    description: Czy budować frontend
    value: ${{ steps.scan.outputs.frontend }}
  backend:
    description: Czy budować backend
    value: ${{ steps.scan.outputs.backend }}

Caller po checkout

- id: changed
  uses: ./.github/actions/changes
- env:
    FRONTEND: >-
      ${{ steps.changed.outputs.frontend }}
  run: printf '%s\n' "$FRONTEND"

Wewnętrzny step scan pozostaje wewnątrz action. Caller zna swoje id: changed i publiczne nazwy outputów.

Przenosimy do composite poznaną procedurę, nie zmieniamy reguły wykrywania ani tekstowego kontraktu wyniku.

Alternatywnie: procedura changes jako reusable workflow

Reusable mapuje job na wynik workflow

Fragment on.workflow_call

on:
  workflow_call:
    outputs:
      frontend:
        description: Czy budować frontend
        value: ${{ jobs.detect.outputs.frontend }}
      backend:
        description: Czy budować backend
        value: ${{ jobs.detect.outputs.backend }}

Fragment joba detect

outputs:
  frontend: ${{ steps.scan.outputs.frontend }}
  backend: ${{ steps.scan.outputs.backend }}

Job zawiera wcześniejsze kroki API, planowania i publikacji outputów.

Łańcuch to step → job → workflow. Kontekst jobs służy tutaj do wystawienia wyników reusable, nie do dowolnego odczytu w zwykłym callerze.

Fragment jobs callera Component CI

Caller czyta wynik pod własnym job ID

changes:
  uses: ./.github/workflows/changes.yml
backend:
  needs: changes
  if: ${{ needs.changes.outputs.backend == 'true' }}
  uses: ./.github/workflows/backend.yml
  with:
    configuration: Release

Caller nie zna wewnętrznego joba detect ani kroku scan. Zna interfejs wywołania changes.

Gdy output ma zasilić typowany input boolean, konwertujemy tekst przez fromJSON(...). Samo odczytanie outputu nie przywraca typu.

Ścieżka w output nie jest zawartością pliku

Artefakt przenosi rzeczywisty plik

Producent planu

- uses: actions/upload-artifact@v7
  with:
    name: component-plan
    path: plan.json
    if-no-files-found: error

Inny job, po needs: changes

- uses: actions/download-artifact@v8
  with:
    name: component-plan
    path: metadata
- run: jq . metadata/plan.json

Upload wiąże pliki z run. Download udostępnia je na innym runnerze. Ten sam mechanizm przenosi raporty testów, gotowy build albo PDF.

GitHub · widok z przeglądarki

Artifacts: component-plan

Sekcja Artifacts w GitHub Actions z artefaktem component-plan przypisanym do uruchomienia.
Plik plan.json jest przenoszony jako artefakt, nie jako tekst ścieżki w output.Otwórz pełny rozmiar ↗
To nie jest kolejne wywołanie workflow_call

Nowy workflow ma własne uruchomienie

on:
  workflow_run:
    workflows: ['Component CI']
    types: [completed]
permissions: {actions: read}
OdczytTożsamość
github.run_idBieżący run konsumenta.
github.event.workflow_run.idRun producenta, którego zakończenie wywołało zdarzenie.
github.event.workflow_run.head_shaRewizja związana z wykonaniem producenta.

Konsument nie otrzymuje needs poprzedniego run. Jego definicja musi istnieć na gałęzi domyślnej.

Fragment jobs nowego workflow // tylko odczyt danych

Wybierz run producenta, nie „ostatni plik”

read:
  if: >-
    ${{ github.event.workflow_run.conclusion == 'success'
        && github.event.workflow_run.head_repository.full_name
            == github.repository }}
  runs-on: ubuntu-24.04
  steps:
    - uses: actions/download-artifact@v8
      with:
        name: component-plan
        run-id: ${{ github.event.workflow_run.id }}
        github-token: ${{ secrets.GITHUB_TOKEN }}
        path: metadata
    - run: jq . metadata/plan.json

Pobieramy plan z run producenta. Token ma tylko actions: read.

Artefakt to dane, nie kod. Nie wykonuj go z większymi uprawnieniami konsumenta.

12 // Jeden szablon, wiele wykonań

Powiel konfigurację, nie listę kroków

Matrix opisuje warianty joba: systemy, wersje narzędzia albo komponenty z wcześniejszego planu.

Iloczyn wariantówInclude i excludeDynamiczna lista
Fragment joba

Dwa systemy × dwie wersje = cztery jobs

runs-on: ${{ matrix.os }}
strategy:
  matrix:
    os: [ubuntu-24.04, windows-latest]
    node: ['24', '26']
steps:
  - uses: actions/setup-node@v7
    with:
      node-version: ${{ matrix.node }}
      package-manager-cache: false
  - run: node --version
  • Każda kombinacja jest osobnym wykonaniem joba na własnym runnerze.
  • matrix.os i matrix.node opisują bieżący wariant.
  • Matrix nie instaluje Node. Robi to setup action.
  • Wybieramy tylko warianty, które rzeczywiście wspiera sprawdzany projekt.
GitHub · widok z przeglądarki

Matrix: cztery wykonania

GitHub Actions pokazujące cztery warianty matrix dla Ubuntu i Windows oraz Node 24 i 26.
Każda kombinacja systemu i wersji Node ma własne wykonanie i wynik.Otwórz pełny rozmiar ↗
Precyzyjny zestaw kombinacji

Usuń wyjątek. Dodaj wariant.

strategy:
  matrix:
    os: [ubuntu-24.04, windows-latest]
    node: ['24', '26']
    exclude:
      - os: windows-latest
        node: '26'
    include:
      - os: macos-latest
        node: '24'

Początkowo mamy 4 kombinacje.

exclude usuwa Windows + Node 26: zostają 3.

include dodaje macOS + Node 24: znowu mamy 4.

Include może także uzupełniać istniejące kombinacje o dodatkowe pola, jeśli nie nadpisuje ich pierwotnych wartości.

Liczba wariantów ≠ liczba jednoczesnych wykonań

Koszt i reakcję na błąd ustawiamy osobno

strategy:
  fail-fast: false
  max-parallel: 2
  matrix:
    node: ['22', '24', '26']

fail-fast

Domyślnie błąd wariantu może anulować inne warianty matrix. false pozwala zebrać ich wyniki niezależnie.

max-parallel

Ogranicza równoległość do dwóch wykonań. Nadal istnieją trzy warianty; jeden poczeka.

continue-on-error ma inną rolę: zmienia traktowanie niepowodzenia. Nie włączamy go zamiast naprawy wymaganych testów.

Rozszerzamy krok scan z plan.json

Plan dostarcza listę wariantów

components=$(jq -c \
  'to_entries | map(select(.value == true) | .key)' plan.json)
printf 'components=%s\n' "$components" >> "$GITHUB_OUTPUT"
# Dodaj do outputs joba changes
outputs:
  components: ${{ steps.scan.outputs.components }}

Dla obu zmienionych komponentów output zawiera tekst ["frontend","backend"]. Dla samej dokumentacji będzie to [].

To wciąż ten sam plan, a nie druga, niezależna reguła wykrywania zmian.

Fragment jobs // job changes już publikuje components

fromJSON zamienia output w matrix

selected:
  needs: changes
  if: ${{ needs.changes.outputs.components != '[]' }}
  runs-on: ubuntu-24.04
  strategy:
    matrix:
      component: ${{ fromJSON(needs.changes.outputs.components) }}
  steps:
    - env:
        COMPONENT: ${{ matrix.component }}
      run: |
        printf 'Wybrany komponent: %s\n' "$COMPONENT"

Job-level if jest oceniane przed matrix. Pusta lista świadomie pomija job; brak lub niepoprawny JSON powinien ujawnić błąd producenta, nie wybrać „wszystko” albo „nic” po cichu.

Wariant może następnie wywołać właściwą procedurę builda. Matrix nie zastępuje przygotowania jej narzędzi.

13 // Powtarzalność i koszt

Najpierw odtwarzalność. Potem przyspieszenie.

Manifest, lockfile, SDK i cache rozwiązują różne problemy.

Wersje wejśćCache zależnościArtefakty wyniku
Fragmenty package.json i package-lock.json

Deklaracja ≠ rozwiązany zestaw zależności

Manifest — dopuszczalna wersja

{
  "dependencies": {
    "is-number": "^7.0.0"
  }
}

Lockfile — konkretny pakiet

{
  "packages": {
    "node_modules/is-number": {
      "version": "7.0.0"
    }
  }
}

To wycinki. Pełny lockfile zapisuje także graf zależności przechodnich i dane potrzebne do odtworzenia pobrań, np. integralność pakietów.

Manifest i lockfile aktualizujemy świadomie razem. CI ma odtwarzać ustalony zestaw, a nie samoczynnie „naprawiać” jego wersje.

Źródło prawdy należy do projektu

Lockfile nie przypina SDK

global.json

{
  "sdk": {
    "version": "10.0.401",
    "rollForward": "disable"
  }
}

Przykład dokładnego wyboru .NET SDK.

Setup czyta plik z repozytorium

- uses: actions/setup-dotnet@v6
  with:
    global-json-file: global.json
- uses: actions/setup-node@v7
  with:
    node-version-file: .nvmrc
    package-manager-cache: false

Najpierw checkout. Wcześniejsze 10.x było prostym przykładem wyboru rodziny SDK; powtarzalny projekt potrzebuje świadomej polityki dokładnych wersji i aktualizacji.

Zestaw jawnych wejść

Ten sam commit to dopiero początek

  • Określ wersje narzędzi, zależności i konfigurację builda.
  • Wymagaj zgodności manifestu z lockfile; używaj trybu instalacji respektującego blokadę.
  • Kontroluj dodatkowe wejścia: dane testowe, zmienne i usługi zewnętrzne.
  • Utrzymuj możliwość zbudowania projektu bez cache i bez stanu poprzedniego runnera.
  • Powtarzalne wykonanie testów nie gwarantuje jeszcze identycznych bajtów artefaktu — np. timestamp też może być wejściem.

Cache optymalizuje koszt odtworzenia tych wejść. Nie określa, które wersje powinny zostać użyte.

Optymalizacja ≠ wynik

Cache można stracić. Wynik trzeba przekazać.

Cache zależności

Odtwarzalny zapas pobrań, np. magazyn npm lub pakiety NuGet. Klucz i zakres określają dostęp do wpisu.

Może wygasnąć albo nie istnieć. Build nadal powinien działać.

Artefakt workflow

Wynik konkretnego run: raport, PDF, paczka builda. Ma odbiorcę, nazwę i retencję.

Gdy inny job potrzebuje pliku wynikowego, używamy upload/download.

Odzyskanie cache nie zastępuje instalacji, kompilacji ani testów. Nie przechowujemy w nim sekretów.

Przykład magazynu pobrań npm, po checkout

Klucz opisuje zgodność danych

- id: packages
  uses: actions/cache@v6
  env:
    PREFIX: ${{ runner.os }}-${{ runner.arch }}-npm-v1
  with:
    path: ~/.npm
    key: ${{ env.PREFIX }}-${{ hashFiles('package-lock.json') }}
    restore-keys: ${{ env.PREFIX }}-
  • key: system, architektura, rodzaj danych i hash zależności.
  • restore-keys: zgodny prefiks zapasowy, gdy dokładnego wpisu nie ma.
  • Wpis cache nie jest aktualizowany w miejscu. Inne dane wymagają nowego klucza.

Tu cache zawiera pobrania npm, nie node_modules. Dla binariów zgodność może wymagać również wersji runtime, ABI i innych wejść builda.

Odzyskaj → zainstaluj → zbuduj i sprawdź

Hit zmienia koszt, nie kryteria sukcesu

Output cache-hitZnaczenieNastępna czynność
'true'Odzyskano dokładny key.Instalacja według lockfile, build i testy.
'false'Odzyskano wpis przez dopasowanie zapasowe.Uzupełnienie zależności, build i testy.
''Nie odzyskano wpisu.Pełne pobranie, build i testy.

Standardowa action cache zapisuje nowy wpis w fazie post-job po sukcesie, gdy nie było exact hit i zapis jest dozwolony.

GitHub · widok z przeglądarki

Cache hit: odzyskaj i zainstaluj

Log GitHub Actions z komunikatem przywrócenia cache i następującą po nim instalacją zależności.
Odzyskanie cache nie pomija instalacji według lockfile ani sprawdzania wyniku.Otwórz pełny rozmiar ↗
Jedna odpowiedzialność za dany magazyn

Setup action może obsłużyć cache zależności

npm

- uses: actions/setup-node@v7
  with:
    node-version-file: .nvmrc
    cache: npm
    cache-dependency-path: package-lock.json
- run: npm ci

NuGet

- uses: actions/setup-dotnet@v6
  with:
    global-json-file: global.json
    cache: true
    cache-dependency-path: '**/packages.lock.json'

W obu przypadkach wymagane są odpowiednie lockfiles. Setup cache nie jest poleceniem odtworzenia zależności aplikacji.

Wybierz setup action albo osobne actions/cache dla tego samego katalogu. Nie dodawaj dwóch konkurujących mechanizmów bez potrzeby.

14 // Od informacji do reguły

Kiedy wynik pozwala scalić PR?

Wynik wykonania i reguła dopuszczająca merge to dwa osobne mechanizmy.

Status i conclusionRulesetWarunkowa praca, stały gate
Dwa interfejsy publikowania wyniku dla commita

Status i check run to różne modele

Commit status

Nazwany context oraz stan:

pending, success, failure, error.

Typowy dla integracji przez Status API.

Check run

status opisuje przebieg: queued → in_progress → completed.

conclusion opisuje wynik, np. success, failure, cancelled, timed_out, neutral lub skipped.

Actions wystawia checks dla jobs, nie osobny check dla każdego kroku. Nazwa wyniku i SHA są ważniejsze niż sam zielony kolor.

GitHub · widok z przeglądarki

Jeden commit: nazwane wyniki

Rozwinięta lista statusów przy zdarzeniu scalenia PR-a, pokazująca commit status i check run.
Lista przy zdarzeniu scalenia PR-a pokazuje status z API obok check run.Otwórz pełny rozmiar ↗
Ochrona gałęzi docelowej

Reguła wybiera wymagany wynik

  • Ruleset może wymagać PR-a, review i określonych checks.
  • Wybieramy rzeczywistą nazwę checka i, jeśli potrzeba, aplikację źródłową.
  • Nowy commit wymaga wyniku dla właściwego SHA. Tryb strict wymaga również aktualności względem bazy.
  • Zmiana nazwy joba może wymagać zmiany reguły. Testujemy ochronę kontem bez uprawnienia bypass.

neutral i skipped mogą spełnić wymagany check. Brak całego workflow po filtrze może pozostawić Expected / Pending — to inna sytuacja niż pominięty job.

GitHub · widok z przeglądarki

Ruleset: wymagany Required CI

Widok rulesetu dla main tylko do odczytu: wymagany check Required CI ze źródła GitHub Actions.
Reguła wskazuje nazwę wymaganego wyniku i jego aplikację źródłową.Otwórz pełny rozmiar ↗
Dwa warianty triggera pull_request

Zmiany mogą uruchomić zero, jeden lub dwa workflows

frontend-ci.yml

on:
  pull_request:
    branches: [main]
    paths:
      - 'frontend/**'
      - '.nvmrc'
      - '.github/**'

backend-ci.yml

on:
  pull_request:
    branches: [main]
    paths:
      - 'backend/**'
      - 'global.json'
      - '.github/**'

branches i paths muszą być spełnione jednocześnie. Zmiana wspólnej konfiguracji może uruchomić oba CI; zmiana samej dokumentacji — żadnego.

Filtr jest decyzją o tym, czy powstanie run. Trzeba ją uzgodnić z polityką wymaganych checks.

Studium przypadku // dwa workflows z filtrami paths

Dwa workflows, jedna wymagana nazwa

Frontend

jobs:
  required:
    name: required
    runs-on: ubuntu-24.04
    steps:
      - run: echo "Frontend"

Backend

jobs:
  required:
    name: required
    runs-on: ubuntu-24.04
    steps:
      - run: echo "Backend"

Ruleset wybiera nazwę checka. Tutaj ustala ją name: required, nie samo id joba.

W badanej konfiguracji przeszły warianty z jednym sukcesem albo dwoma sukcesami. Wspólna nazwa nie oznaczała „dowolny zielony wystarczy”, gdy drugi check nadal trwał lub zakończył się błędem.

Pomiar GitHub // 17.10.2026

Jeden udany run może wystarczyć

ZmianaKadr do porównaniaWynik / obserwacja
Tylko dokumentacja0 uruchomieńBLOCKED — brak wymaganego wyniku.
Tylko frontend1 run w trakcie pracyStan checka i dostępność merge przed zakończeniem.
Ten sam PR1 run zakończony sukcesemCLEAN — drugi workflow nie musiał się uruchomić.

Brak uruchomienia nie tworzy zielonego wyniku. Żeby uniknąć tej pułapki, można zapewnić zawsze obecny, lekki gate.

Końcowe wyniki potwierdzono aktywnym rulesetem: nazwa required, źródło GitHub Actions, brak bypass także dla administratorów. Klasyczna ochrona gałęzi była wyłączona.

GitHub · pomiar z aktywnym rulesetem

Zero runów: brak wymaganego wyniku

PR tylko z dokumentacją: brak wymaganego checka i blokada scalania przez aktywny ruleset.
0 uruchomień → BLOCKED. Filtr paths nie tworzy zielonego wyniku.Otwórz pełny rozmiar ↗
GitHub · pomiar z aktywnym rulesetem

Jeden run: jeszcze trwa

PR ze zmianą frontendu: jeden check required IN_PROGRESS i merge zablokowany przez aktywny ruleset.
1 check IN_PROGRESS → BLOCKED. Aktywny ruleset nie dopuszcza jeszcze merge.Otwórz pełny rozmiar ↗
GitHub · pomiar z aktywnym rulesetem

Jeden sukces: CLEAN

PR z jednym zakończonym sukcesem checkiem required i dostępnym scalaniem pod aktywnym rulesetem.
1 udany run → CLEAN w badanej konfiguracji, bez bypass.Otwórz pełny rozmiar ↗
Ten sam PR i SHA // pomiar GitHub 17.10.2026

Dwa uruchomienia: czekamy na oba wyniki

MomentWynik / obserwacja
Jeden sukces, drugi IN_PROGRESSBLOCKED — pojedynczy sukces nie zakończył oczekiwania.
Oba zakończone sukcesemCLEAN — oba uruchomione checks zakończyły się sukcesem.
Jeden sukces, drugi błądBLOCKED — wspólna nazwa nie ukryła niepowodzenia.

Wyniki końcowe potwierdzono także aktywnym rulesetem. To obserwacja, nie gwarancja API dla dowolnej konfiguracji: znaczenie mają filtry, nazwa, źródło checka i SHA.

GitHub · pomiar z aktywnym rulesetem

Dwa runy: jeden sukces nie wystarcza

Dwa checki required: pierwszy success, drugi IN_PROGRESS; merge zablokowany przez aktywny ruleset.
success + IN_PROGRESS → BLOCKED. Drugi uruchomiony check nadal ma znaczenie.Otwórz pełny rozmiar ↗
GitHub · pomiar z aktywnym rulesetem

Dwa sukcesy: CLEAN

PR z dwoma zakończonymi sukcesem checkami required i dostępnym scalaniem pod aktywnym rulesetem.
success + success → CLEAN. Oba uruchomione checks zakończyły się sukcesem.Otwórz pełny rozmiar ↗
GitHub · pomiar z aktywnym rulesetem

Sukces i błąd: BLOCKED

Dwa checki required: success i failure oraz blokada scalania PR-a przez aktywny ruleset.
success + failure → BLOCKED. Wspólna nazwa nie ukryła niepowodzenia.Otwórz pełny rozmiar ↗
Wariant kanoniczny

Opcjonalna praca. Jeden stały gate.

Każdy PR
→
changes
→
frontend / backend
według planu
→
Required CI
  • Workflow nie ma filtra paths na poziomie on; powstaje dla każdego PR-a.
  • Poznana procedura changes publikuje plan. Poszczególne jobs korzystają z if.
  • Jeden unikalny check Required CI sprawdza sukces planowania i wyniki potrzebnych komponentów.
  • Ruleset wymaga gate, a nie nazw warunkowo pomijanych workflow.
GitHub · widok z przeglądarki

Graf: plan, komponenty i gate

Graf Component CI z planowaniem changes, warunkowymi komponentami frontend i backend oraz końcowym Required CI.
Warunkowe jobs mogą być pominięte; jednoznaczny Required CI ocenia cały plan.Otwórz pełny rozmiar ↗
Nie wystarczy always() i echo OK

Gate sprawdza decyzję i wykonanie

required:
  name: Required CI
  needs: [changes, frontend, backend]
  if: ${{ always() }}
  runs-on: ubuntu-24.04
  permissions: {contents: read}
  steps:
    - uses: actions/checkout@v7
    - env:
        RESULTS: ${{ toJSON(needs) }}
      run: node .github/scripts/required.cjs
PlanWynikAkceptujemy?
truesuccessTak
falseskippedTak
truefailure / cancelled / skippedNie
brak / błędnydowolnyNie

always() pozwala ocenić zależności także po ich błędzie. Wymagamy ponadto needs.changes.result == 'success'.

.github/scripts/required.cjs

Błędny lub brakujący wynik nie przechodzi

const results = JSON.parse(process.env.RESULTS);
if (results.changes?.result !== 'success') {
  throw new Error('Nie udało się ustalić planu');
}
for (const name of ['frontend', 'backend']) {
  const needed = results.changes.outputs?.[name];
  const result = results[name]?.result;
  const accepted = (needed === 'true' && result === 'success')
    || (needed === 'false' && result === 'skipped');
  if (!accepted) throw new Error(`Niespełniony warunek: ${name}`);
}

To jawna polityka: potrzebna praca musi zakończyć się sukcesem, a niepotrzebna może być pominięta.

Jak każdy element CI, skrypt gate i konfigurację workflow chronimy review. Zielony gate nie pomoże, jeśli PR może dowolnie usunąć jego kontrole.

15 // CI uruchamia kod

Automatyzacja potrzebuje granic zaufania

Sprawdzamy nie tylko wynik testu, ale też pochodzenie kodu, danych i uprawnień.

SekretyDane zamiast koduRef i aktualizacje
Najmniejszy potrzebny dostęp

Build nie potrzebuje prawa publikacji

  • Build i testy dostają odczyt kodu, nie domyślne write-all.
  • Zapis przez API wydzielamy do joba, który go rzeczywiście potrzebuje.
  • Nie przekazujemy sekretów produkcyjnych do testów z tymczasową bazą.
  • Sekrety nie trafiają do Git, outputs, artefaktów ani cache. Maskowanie logów jest tylko dodatkową ochroną.
  • Fork PR i kod zależności traktujemy jako niezaufane. Zgoda na uruchomienie nie czyni kodu bezpiecznym.

Reusable workflow nie może podnieść uprawnień tokena callera. Sekrety przekazuje się przez jawny interfejs; samo env nie tworzy takiego kontraktu.

Tytuł PR-a jest wejściem użytkownika

Nie interpoluj danych do źródła skryptu

- name: Sprawdź tytuł PR
  env:
    PR_TITLE: ${{ github.event.pull_request.title }}
  shell: bash
  run: |
    test -n "$PR_TITLE"
    printf 'Długość tytułu: %s\n' "${#PR_TITLE}"
  • Expression w env przekazuje dane, a nie fragment kodu Bash.
  • W github-script użyj process.env.PR_TITLE albo bezpośredniego odczytu payloadu context.
  • Nie wykonuj danych przez eval. Walidacja tekstu nie zastępuje oceny zaufania do kodu PR-a.
Kod action jest zależnością CI

Przypnij kod. Zaplanuj aktualizację.

- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
OdwołanieCo zapewnia?
Gałąź / ruchomy major tagWygodna aktualizacja, ale kod pod nazwą może się zmienić.
Tag konkretnej wersjiCzytelna wersja; sama nazwa tagu nie gwarantuje niezmienności.
Pełny commit SHAKonkretną rewizję kodu action, nie automatyczny dowód jej bezpieczeństwa.
Immutable releaseNiezmienny tag i assety, także dla prerelease. Tytuł, opis oraz ustawienia prerelease i latest nadal można edytować.

Ruchomy major tag może istnieć obok immutable release, jeśli sam nie jest z takim wydaniem związany. Niektórzy autorzy z niego rezygnują; politykę i aktualizacje sprawdzamy u autora action.

Marzec 2025 // atak na zależność automatyzacji

Popularna action też może zostać przejęta

  • W incydencie tj-actions/changed-files zmieniono kod osiągalny przez istniejące tagi.
  • Złośliwa wersja ujawniała w logach dane z pamięci runnera, w tym potencjalnie sekrety.
  • Publiczne logi mogły uczynić wyciek dostępny dla innych osób.
  • Wniosek: ogranicz uprawnienia, przypinaj zweryfikowane rewizje, oceniaj zależności i reaguj na komunikaty bezpieczeństwa.

Wykrywanie zmienionych plików jest przydatne, ale dodatkowa action to również dodatkowy kod do zaufania. Pokazany klient API pozwala zobaczyć samą regułę wprost.

Kontrole automatyzacji też są automatyzowane

Workflow również wymaga utrzymania

actionlint
zizmor .github/workflows
pnpm dlx pin-github-action@3.5.2 \
  .github/workflows --comment ' pin@{ref}'

actionlint: składnia i kontrakty Actions.
zizmor: ryzykowne wzorce, uprawnienia, niezaufane dane i praktyki bezpieczeństwa.

  • Dependabot i Renovate mogą przygotowywać aktualizacje referencji actions.
  • Sprawdzaj repozytorium autora, nowy SHA, changelog i wymagania runnera.
  • Po aktualizacji uruchom CI. Przypięcie starej podatnej wersji nie jest rozwiązaniem.
  • pnpm dlx uruchamia tu narzędzie pin-github-action. To ono przepisuje refs w uses; sprawdź diff i komunikaty o nierozwiązanych odwołaniach.
16 // Nie tylko aplikacje

Dokument też ma proces budowania

Praca dyplomowa: źródła w Git, kompilacja w CI, PDF dla promotora i wersjonowane wydania.

pjatk_latexPDF jako artefaktPrerelease PR i release main
Publiczny przykład: pantadeusz/pjatk_latex // e660b9e

Promotor potrzebuje PDF z konkretnej rewizji

  • engineer.tex — główny dokument.
  • sprz.cls i sprz/ — klasa, styl i zasoby.
  • bibliography.bib — bibliografia, obsługiwana przez biber.
  • engineer.pdf — wynik kompilacji.

Co weryfikuje CI?

Składnię, dostępność ilustracji, pakietów i możliwość złożenia dokumentu.

Review nadal ocenia treść, argumentację i poprawność metod.

README projektu opisuje środowisko Debian z texlive-full i komendę make. W CI jawnie wybieramy tryb PDF i zatrzymanie po błędzie.

Możesz sforkować szablon na GitHubie, zachować atrybucję i napisać własną treść. Brak jawnej licencji nie daje automatycznie prawa do redystrybucji przykładowego tekstu, logo ani innych cudzych materiałów.

Fragment joba pdf // runner Ubuntu, permissions: contents: read

Kompiluj bez prawa publikacji

steps:
  - uses: actions/checkout@v7
    with:
      ref: ${{ github.event.pull_request.head.sha || github.sha }}
  - name: Przygotuj TeX
    shell: bash
    run: |
      sudo apt-get update
      sudo apt-get install -y \
        latexmk biber texlive-latex-extra texlive-bibtex-extra \
        texlive-fonts-recommended texlive-lang-polish texlive-science
  - name: Zbuduj PDF
    shell: bash
    run: |
      latexmk -norc -pdf -interaction=nonstopmode \
        -halt-on-error -file-line-error -no-shell-escape engineer.tex
      test -s engineer.pdf

PR: HEAD autora. Push: SHA zdarzenia. Późniejszy tag musi wskazywać tę samą rewizję.

Kolejne kroki joba pdf

PDF do pobrania. Log do diagnozy.

- uses: actions/upload-artifact@v7
  with:
    name: thesis-pdf
    path: engineer.pdf
    if-no-files-found: error
- if: ${{ !cancelled() }}
  uses: actions/upload-artifact@v7
  with:
    name: latex-log
    path: engineer.log
    if-no-files-found: warn

Udany build bez PDF jest błędem kontraktu. Log próbujemy zachować także po błędzie kompilacji; jeśli błąd wystąpił wcześniej, plik może jeszcze nie istnieć.

GitHub · widok z przeglądarki

LaTeX: artefakty kompilacji

Udane uruchomienie kompilacji LaTeX z artefaktami thesis-pdf i latex-log w GitHub Actions.
PDF i log należą do konkretnego runa, a więc do konkretnej rewizji źródeł.Otwórz pełny rozmiar ↗
Źródło zmiany określa sposób publikacji

Podgląd PR-a i wydanie z main

ŹródłoWynikGranica bezpieczeństwa
Fork PR lub DependabotPDF jako artefakt.Ograniczony token; bez publikacji release.
Same-repo PR bez zmian workflowArtefakt i prerelease dla rewizji PR-a.Osobny job publikujący po udanej kompilacji.
PR zmienia .github/workflows/PDF jako artefakt.GITHUB_TOKEN nie ma prawa Workflows: write potrzebnego do utworzenia release dla takiej rewizji.
Push do mainPDF jako artefakt i zwykły release dla commita main.Publikacja dopiero po przejściu zmian przez reguły main.

Workflow najpierw trafia na main. Nie podmieniamy docelowego SHA, żeby obejść ograniczenie API. To publikacja dokumentu, nie deployment aplikacji.

Fragment joba policy // tylko odczyt API, bez checkout

Zmiana workflow → tylko artefakt

runs-on: ubuntu-24.04
permissions: {pull-requests: read}
outputs:
  workflow-edits: ${{ steps.files.outputs.workflow-edits }}
steps:
  - id: files
    uses: actions/github-script@v9
    with:
      script: |
        const pr = context.payload.pull_request;
        const files = pr ? await github.paginate(github.rest.pulls.listFiles,
          { ...context.repo, pull_number: pr.number, per_page: 100 }) : [];
        if (files.length >= 3000) throw new Error('Niekompletna lista plików PR');
        const paths = files.flatMap(f => [f.filename, f.previous_filename]);
        const edited = paths.some(p => p?.startsWith('.github/workflows/'));
        core.setOutput('workflow-edits', edited);
        if (edited) core.notice('Zmiana workflow: PDF pozostaje artefaktem.');
GitHub · widok z przeglądarki

Zmiana workflow: PDF bez release

Run PR-a ze zmianą workflow: komunikat o PDF jako artefakcie, pominięta publikacja i dostępny thesis-pdf.
Policy wyjaśnia pominięcie publikacji; gotowy PDF pozostaje artefaktem.Otwórz pełny rozmiar ↗
Fragment jobs // workflow reaguje na PR oraz push do main

Prawo zapisu tylko w jobie publikacji

publish:
  needs: [pdf, policy]
  if: >-
    ${{ needs.policy.outputs.workflow-edits == 'false'
        && ((github.event_name == 'push' && github.ref == 'refs/heads/main')
            || (github.event_name == 'pull_request'
                && github.event.pull_request.head.repo.full_name == github.repository
                && github.event.pull_request.user.login != 'dependabot[bot]')) }}
  runs-on: ubuntu-24.04
  permissions: {contents: write}
  steps:
    - uses: actions/download-artifact@v8
      with:
        name: thesis-pdf
        path: release-files

Po sukcesie pdf i policy pobieramy gotowy plik. Nie wykonujemy kodu PR-a w jobie z prawem zapisu.

Następne kroki rozróżnią prerelease PR i zwykły release main.

Krok po download w chronionym jobie publish

Prerelease: PDF do review

- if: ${{ github.event_name == 'pull_request' }}
  shell: bash
  env:
    GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    GH_REPO: ${{ github.repository }}
    PR_NUMBER: ${{ github.event.number }}
    SOURCE_SHA: ${{ github.event.pull_request.head.sha }}
  run: |
    tag="pr-${PR_NUMBER}-run-${GITHUB_RUN_ID}-a${GITHUB_RUN_ATTEMPT}"
    gh release create "$tag" release-files/engineer.pdf \
      --prerelease --latest=false --target "$SOURCE_SHA" \
      --title "PDF PR #$PR_NUMBER" \
      --notes "Wersja do review; commit $SOURCE_SHA."

Unikalny tag dla run i próby nie nadpisuje poprzedniego wydania. GH_REPO wskazuje repozytorium bez potrzeby checkout.

GitHub · widok z przeglądarki

Prerelease: PDF konkretnego PR-a

Wydanie PDF PR w GitHub Releases z etykietą Pre-release, tagiem i plikiem engineer.pdf.
Wersja do review jest prerelease dla rewizji PR-a, nie wydaniem Latest.Otwórz pełny rozmiar ↗
Drugi krok w tym samym jobie publish

Release: PDF z zaakceptowanego main

- if: ${{ github.event_name == 'push' }}
  shell: bash
  env:
    GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    GH_REPO: ${{ github.repository }}
    SOURCE_SHA: ${{ github.sha }}
  run: |
    tag="pdf-main-${GITHUB_RUN_ID}-a${GITHUB_RUN_ATTEMPT}"
    gh release create "$tag" release-files/engineer.pdf \
      --latest --target "$SOURCE_SHA" \
      --title "PDF z main" \
      --notes "Wydanie dokumentu; commit $SOURCE_SHA."

Nie ustawiamy --prerelease. Release utworzony przez GITHUB_TOKEN nie uruchomi kolejnego workflow przez on: release.

GitHub · widok z przeglądarki

Release: PDF po integracji

Pełne wydanie PDF z main w GitHub Releases, z tagiem i plikiem engineer.pdf, bez etykiety Pre-release.
Zaakceptowana rewizja main ma zwykłe wydanie, odrębne od podglądu PR-a.Otwórz pełny rozmiar ↗
Co naprawdę dowodzi zbudowany PDF?

Brak błędu kompilacji ≠ gotowa praca

Błąd

Nieznane polecenie albo brak wymaganego pliku może zatrzymać kompilację i zablokować publikację.

Ostrzeżenie

Brakujące cytowanie lub odwołanie może pozostawić PDF i kod sukcesu. -halt-on-error nie zamienia każdego warning w błąd.

Jeśli zespół wymaga braku określonych ostrzeżeń, dopisuje jawną kontrolę logu. Ocena treści naukowej nadal należy do człowieka.

Ten sam model poza LaTeX

Automatyzuj sprawdzalne twierdzenia

Dokumentacja

Build, kontrola linków i brakujących zasobów. HTML lub PDF jako wynik do obejrzenia.

Błąd sieci do zewnętrznej strony trzeba odróżnić od nieprawidłowego linku.

Notebook lub raport

Wykonanie od początku, z określonymi danymi, bibliotekami i ziarnem losowym.

Brak ukrytego stanu sesji jest weryfikowalny; poprawność metody badawczej wymaga osobnej oceny.

W każdym przypadku ustalamy: wejście → polecenie → warunek błędu → wynik → odbiorca.

Od procesu do reguły zespołu

CI skraca pętlę. Zespół zamyka pętlę.

Mała zmiana
→
Automatyczne kontrole
→
Czytelny wynik
→
Review + decyzja
  1. Co sprawdziliśmy? Wskaż rewizję, środowisko i rzeczywisty zakres testów.
  2. Jak przenosimy wynik? Odróżnij output, env, artefakt i cache.
  3. Dlaczego merge jest dozwolony? Wskaż wymagany check, jego kontrakt i uprawnienia procesu.

W laboratorium zastosujemy te mechanizmy do aplikacji. Wykład pozostaje mapą pojęć, nie gotowym rozwiązaniem zadania.