CI i SDLC
Od zmiany w kodzie do informacji, której zespół może zaufać.
Najpierw proces. Potem pierwszy działający workflow, jego mechanizmy i reguły bezpiecznej integracji.
Od zmiany w kodzie do informacji, której zespół może zaufać.
Najpierw proces. Potem pierwszy działający workflow, jego mechanizmy i reguły bezpiecznej integracji.
Potrzeby użytkowników prowadzą do zmian. Wyniki testów, review i obserwacje z eksploatacji wracają do zespołu.
Krótkie gałęzie i niewielkie zmiany ograniczają liczbę możliwych przyczyn problemu.
Build i testy uruchamiają się według tych samych reguł dla kolejnych zmian.
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.
| Proces | Co robi? | Efekt |
|---|---|---|
| Continuous Integration (CI) | Integruje i weryfikuje kod. | Artefakt builda, wyniki testów i raporty. |
| Continuous Delivery | Automatyzuje przygotowanie, dostarczenie i konfigurację wydania. | Wersja gotowa do wdrożenia na żądanie; decyzja o produkcji pozostaje ręczna. |
| Continuous Deployment | Automatycznie 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.
Mały fragment logiki. Zależności często zastępują duble. Zwykle krótki czas i precyzyjna diagnoza.
Współpraca elementów, np. API i bazy danych. Sprawdzają granice, których nie obejmuje test z mockiem.
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.
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?
Najpierw ustalamy model integracji. Dopiero potem konfigurujemy automatyzację.
Gałąź jest nazwanym wskaźnikiem na commit. Nazwy main i develop otrzymują znaczenie od zespołu, nie od samego Gita.
main; PR można otworzyć jeszcze jako draft.M ma dwóch rodziców. Commity F1 i F2 zachowują tożsamość oraz relację z gałęzią.
S ma jednego rodzica. Zawiera łączną zmianę PR-a, ale nie włącza F1 i F2 jako przodków main.
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.
Feature: develop → feature → develop. Release: develop → release → main i develop, żeby poprawki nie zginęły.
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.
Main i krótkie feature branches. Pasuje do częstej integracji niewielkich zmian.
Checks i review chronią powrót zmiany do main.
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.
Platforma organizuje pracę. Runner ją wykonuje. To nie musi być ta sama usługa.
| Platforma | Gdzie pasuje? | Co warto uwzględnić? |
|---|---|---|
| GitHub Actions | Workflow obok repozytorium i PR-ów GitHuba. | Actions, runnery i polityki organizacji. |
| GitLab CI/CD | Pipeline i merge request w GitLab. | Wariant chmurowy lub własna instalacja. |
| Jenkins | Serwer automatyzacji z agentami i wtyczkami. | Elastyczność oraz koszt utrzymania. |
| Azure Pipelines | Azure DevOps, także z repozytoriami GitHub. | Agenci, połączenia i uprawnienia. |
| CircleCI | Zewnętrzna platforma połączona z repozytorium. | Własny pipeline i wynik odsyłany do PR-a. |
| TeamCity | Automatyzacja JetBrains: Cloud lub On-Premises. | Konfiguracje buildów i zarządzanie agentami. |
Zarządzana infrastruktura runnerów dla GitHub Actions. Workflow może pozostać w GitHubie, a job działa na runnerze dostawcy.
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.
Blacksmith — dokumentacja · Depot — dokumentacja · Rozliczanie GitHub Actions
YAML opisuje strukturę. Schemat GitHub Actions nadaje tej strukturze znaczenie.
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.Workflow zapisujemy w repozytorium jako plik .github/workflows/hello.yml lub .yaml.
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.
CI działa!.
Przygotowanie środowiska: obraz systemu, runner i efektywne uprawnienia tokena.
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ą.
steps:
- name: Wiadomość
run: echo "Cześć"
- name: Katalog
run: pwd
steps:
- name: Wiadomość
run: echo "Cześć"
- name: Katalog
run: pwd
name i run nadal muszą należeć do tego samego elementu.on: push to skalar zaakceptowany przez Actionson: pushon:
- push
- pull_requeston:
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.
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.| zachowuje linie. >- je składa.- shell: bash
run: |
echo "Początek"
pwd
uname -s- 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.
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.
run i script jest kolejny język- 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- 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 mainTekst jest identyczny. Po prawej mechanizm strony koloruje jawny Bash, github-script i expressions podczas budowania HTML. To nie jest symulacja interfejsu IDE.
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.
actionlint
yq '.jobs.hello.steps' \
.github/workflows/hello.yml
actionlint: błędy specyficzne dla Actions.
yq: odczyt struktury YAML, nie pełna walidacja workflow.
actionlint · yq
Oddziel definicję procesu, konkretne wykonanie i środowisko, w którym działa kod.
run albo komponent uses.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.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.
needs mówi: najpierw to zadaniejobs:
build:
runs-on: ubuntu-24.04
steps:
- run: echo "Budowanie"
verify:
needs: build
runs-on: ubuntu-24.04
steps:
- run: echo "Weryfikacja"
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.
- name: Pokaż narzędzie
shell: bash
run: dotnet --infoUruchamia skrypt w wybranej powłoce. Można ustawić working-directory.
- 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.
runs-on| Przykład | Środowisko | Typowa powłoka |
|---|---|---|
ubuntu-24.04 | GitHub-hosted Linux | Bash |
windows-latest | GitHub-hosted Windows | PowerShell |
macos-latest | GitHub-hosted macOS | Bash |
[self-hosted, linux, x64] | Runner z pasującymi etykietami | Zależ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.
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: '24'
package-manager-cache: false
- run: node --version
GITHUB_WORKSPACE.echo albo odczytu API checkout nie jest potrzebny.Numer przy nazwie action określa jej wersję. Wersja instalowanego Node jest osobnym inputem.
runs:
using: node24
main: dist/index.jsZapakowany kod JS uruchamiany przez runnera.
runs:
using: docker
image: DockerfileKod i narzędzia w obrazie. Wymaga runnera Linux z Dockerem.
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.
runs-on: ubuntu-24.04
container: node:24-bookworm
steps:
- run: node --versionKroki run wykonują się wewnątrz kontenera joba. Domyślną powłoką jest tu sh, nie Bash.
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.
# 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"
jobs.<id>.container ani długotrwała usługa z services.unity-builder@v4 uruchamia kontenery, ale ma runs.using: node20. To JavaScript action, nie Docker action.Actions z obrazów Docker · GameCI v4 — metadata · Metadata prawdziwej Docker action
Zdarzenie określa kontekst, sprawdzaną rewizję i granicę zaufania.
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.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.
Do budowania i testowania proponowanej zmiany. Fork PR ma domyślnie ograniczony token i nie otrzymuje sekretów repozytorium.
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.
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.
on:
workflow_dispatch:
workflow_dispatch nie spełniają wymaganych checks w rulesecie gałęzi, nawet dla tego samego SHA. Do sprawdzeń PR używamy pull_request.docs: popraw opis [skip ci]
Także: [ci skip], [skip actions], [actions skip], [no ci].
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.
| Pole | push do main | pull_request #42 |
|---|---|---|
github.ref | refs/heads/main | refs/pull/42/merge |
github.sha | Commit z push | Przygotowany commit scalenia PR-a |
github.head_ref | Pusty tekst | feature/date |
github.base_ref | Pusty tekst | main |
github.event.pull_request.head.sha | Brak pola PR | Ostatni commit gałęzi autora |
Domyślny checkout dla zwykłego PR sprawdza przygotowane połączenie zmian z bazą, a nie tylko HEAD autora.
Wymaganie aktualnej bazy albo merge queue pomaga kontrolować stan tuż przed scaleniem.
@v7 nie jest wersją aplikacji- uses: actions/checkout@v7
with:
ref: ${{ github.sha }}
| Zapis | Co wybiera? |
|---|---|
actions/checkout@v7 | Kod action checkout w repozytorium jej autora. |
with.ref | Rewizję repozytorium pobieraną przez checkout. |
owner/repo/path@ref | Katalog z metadata action przy wskazanym ref. |
./.github/actions/backend | Lokalny 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.
Formularz, zdarzenie, runner i ustawienia repozytorium są różnymi źródłami wartości.
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.
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.| Interfejs | Typy / sposób odczytu |
|---|---|
workflow_dispatch.inputs | string, boolean, number, choice, environment. Choice i environment wybierają tekst. |
workflow_call.inputs | string, boolean, number. Caller przekazuje zgodną wartość. |
| Inputy action, także composite | Tekst; brak deklaracji type: boolean jak w workflow_call. |
env i outputs | Tekst, 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.
| Pole | Przydatna informacja |
|---|---|
github.event_name | Rodzaj zdarzenia, np. pull_request. |
github.ref / github.sha | Ref i rewizja konkretnego zdarzenia. |
github.actor | Kto zainicjował uruchomienie; to nie zawsze autor PR-a. |
github.event.pull_request.user.login | Autor PR-a, gdy zdarzenie zawiera ten obiekt. |
runner.os / runner.arch | System i architektura, np. Linux / X64. |
runner.tool_cache | Katalog 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.
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.
# 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.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.
Expressions to mały język GitHub Actions, nie JavaScript ani Bash.
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.if zwykle wolno pominąć ${{ }}.if: ${{ !cancelled() }} chroni początkowe ! przed interpretacją jako tag YAML.if jest oceniane przed przydziałem runnera i rozwinięciem matrix.- 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.
''env:
TITLE: ${{ vars.REPORT_TITLE || 'Raport CI' }}
SHA: ${{ github['sha'] }}
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.
| Wyrażenie | Wynik / 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.
fromJSON odtwarza typ wartości| Wyrażenie | Wynik |
|---|---|
fromJSON('false') | Boolean false. |
fromJSON('3') > 1 | true — 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.
# 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.GITHUB_WORKSPACE, nie do working-directory kroku.''. Jeśli plik jest wymagany, osobny test -f wykrywa jego brak.A && B || C nie zawsze wybiera B| Wyrażenie / wartość | Co się stanie? |
|---|---|
true && 'Release' || 'Debug' | Release — niepusty tekst jest truthy. |
true && 0 || 2 | 2 — zero uruchamia fallback. |
true && false || true | true — 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() zachowuje także false i zeroenv:
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) → 0case(true, false, true) → falsecase(true, '', 'fallback') → pusty tekstPary 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.
| Warunek | Typowe 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().
Komendy, pliki i API wymagają kodu. Dobierz język do zadania, nie do długości YAML.
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.
defaults:
run:
shell: bash
working-directory: backend
run w workflow.uses.Powtarzającą się konfigurację zapisujemy raz, a lokalny wyjątek umieszczamy przy kroku, którego dotyczy.
# 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.
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"
0 oznacza sukces; niezerowy kod oznacza błąd.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.
- 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.
// scripts/summary.cs
var title = "Build";
Console.WriteLine($"Length: {title.Length}");dotnet run --file scripts/summary.csTen 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.
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.::error:: nie ustawia niezerowego exit code. Gdy problem ma zatrzymać job, skrypt musi również zgłosić błąd wykonania.
{
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ń.
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.
| Obiekt | Rola |
|---|---|
github | Uwierzytelniony klient Octokit: REST, GraphQL i paginacja. |
context | Dane repozytorium i zdarzenia w JavaScript. |
core | Logi, 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.
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.
printf 'Tryb: %s\n' \
"$CONFIGURATION"core.info('Tryb: ' +
process.env.CONFIGURATION);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.
Porównamy dwa opakowania tej samej procedury backendu: build i test w wybranej konfiguracji.
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.
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.
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: Releasebackend:
uses: ./.github/workflows/backend.yml
with:
configuration: ReleaseObok 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.
# 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.uses: owner/repo/path@ref pobiera pakiet action i jego skrypty. Nie trzeba osobno checkoutować jej repozytorium.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.
Rozwijamy przykład z API PR-a, nie kolekcję niezależnych flag demonstracyjnych.
Ten workflow nazywamy Component CI. Przekazujemy krótkie wartości przez outputs, a pliki przez artefakty.
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.
- 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"
export w jednym run nie przechodzi do następnego procesu.GITHUB_ENV jest plikiem odczytywanym przez runnera po kroku.To ustawienie środowiska. Dla nazwanego wyniku producenta użyjemy GITHUB_OUTPUT.
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.
- 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.
- name: Odczytaj decyzję
env:
FRONTEND: ${{ steps.scan.outputs.frontend }}
run: |
printf 'Frontend: %s\n' "$FRONTEND"- uses: actions/upload-artifact@v7
with:
name: component-plan
path: ${{ steps.scan.outputs.plan-path }}
if-no-files-found: errorMapowanie 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.
outputs:
frontend: ${{ steps.scan.outputs.frontend }}
backend: ${{ steps.scan.outputs.backend }}To mapowanie wyników istniejącego kroku scan.
backend:
needs: changes
if: >-
${{ needs.changes.outputs.backend == 'true' }}
uses: ./.github/workflows/backend.yml
with:
configuration: ReleaseBackend jest budowany tylko wtedy, gdy wymaga tego plan. needs udostępnia wyniki bezpośrednio wskazanych zależności, nie wszystkich jobs w workflow.
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.
outputs:
frontend:
description: Czy budować frontend
value: ${{ steps.scan.outputs.frontend }}
backend:
description: Czy budować backend
value: ${{ steps.scan.outputs.backend }}- 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.
on:
workflow_call:
outputs:
frontend:
description: Czy budować frontend
value: ${{ jobs.detect.outputs.frontend }}
backend:
description: Czy budować backend
value: ${{ jobs.detect.outputs.backend }}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.
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.
- uses: actions/upload-artifact@v7
with:
name: component-plan
path: plan.json
if-no-files-found: error- uses: actions/download-artifact@v8
with:
name: component-plan
path: metadata
- run: jq . metadata/plan.jsonUpload wiąże pliki z run. Download udostępnia je na innym runnerze. Ten sam mechanizm przenosi raporty testów, gotowy build albo PDF.
on:
workflow_run:
workflows: ['Component CI']
types: [completed]
permissions: {actions: read}
| Odczyt | Tożsamość |
|---|---|
github.run_id | Bieżący run konsumenta. |
github.event.workflow_run.id | Run producenta, którego zakończenie wywołało zdarzenie. |
github.event.workflow_run.head_sha | Rewizja związana z wykonaniem producenta. |
Konsument nie otrzymuje needs poprzedniego run. Jego definicja musi istnieć na gałęzi domyślnej.
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.
Matrix opisuje warianty joba: systemy, wersje narzędzia albo komponenty z wcześniejszego planu.
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
matrix.os i matrix.node opisują bieżący 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.
strategy:
fail-fast: false
max-parallel: 2
matrix:
node: ['22', '24', '26']
Domyślnie błąd wariantu może anulować inne warianty matrix. false pozwala zebrać ich wyniki niezależnie.
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.
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.
fromJSON zamienia output w matrixselected:
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.
Manifest, lockfile, SDK i cache rozwiązują różne problemy.
{
"dependencies": {
"is-number": "^7.0.0"
}
}{
"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.
{
"sdk": {
"version": "10.0.401",
"rollForward": "disable"
}
}Przykład dokładnego wyboru .NET SDK.
- uses: actions/setup-dotnet@v6
with:
global-json-file: global.json
- uses: actions/setup-node@v7
with:
node-version-file: .nvmrc
package-manager-cache: falseNajpierw checkout. Wcześniejsze 10.x było prostym przykładem wyboru rodziny SDK; powtarzalny projekt potrzebuje świadomej polityki dokładnych wersji i aktualizacji.
Cache optymalizuje koszt odtworzenia tych wejść. Nie określa, które wersje powinny zostać użyte.
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ć.
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.
- 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.Tu cache zawiera pobrania npm, nie node_modules. Dla binariów zgodność może wymagać również wersji runtime, ABI i innych wejść builda.
| Output cache-hit | Znaczenie | Nastę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.
- uses: actions/setup-node@v7
with:
node-version-file: .nvmrc
cache: npm
cache-dependency-path: package-lock.json
- run: npm ci- 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.
Wynik wykonania i reguła dopuszczająca merge to dwa osobne mechanizmy.
Nazwany context oraz stan:
pending, success, failure, error.
Typowy dla integracji przez Status API.
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.
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.
on:
pull_request:
branches: [main]
paths:
- 'frontend/**'
- '.nvmrc'
- '.github/**'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.
jobs:
required:
name: required
runs-on: ubuntu-24.04
steps:
- run: echo "Frontend"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.
| Zmiana | Kadr do porównania | Wynik / obserwacja |
|---|---|---|
| Tylko dokumentacja | 0 uruchomień | BLOCKED — brak wymaganego wyniku. |
| Tylko frontend | 1 run w trakcie pracy | Stan checka i dostępność merge przed zakończeniem. |
| Ten sam PR | 1 run zakończony sukcesem | CLEAN — 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.
| Moment | Wynik / obserwacja |
|---|---|
| Jeden sukces, drugi IN_PROGRESS | BLOCKED — pojedynczy sukces nie zakończył oczekiwania. |
| Oba zakończone sukcesem | CLEAN — oba uruchomione checks zakończyły się sukcesem. |
| Jeden sukces, drugi błąd | BLOCKED — 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.
on; powstaje dla każdego PR-a.if.
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
| Plan | Wynik | Akceptujemy? |
|---|---|---|
| true | success | Tak |
| false | skipped | Tak |
| true | failure / cancelled / skipped | Nie |
| brak / błędny | dowolny | Nie |
always() pozwala ocenić zależności także po ich błędzie. Wymagamy ponadto needs.changes.result == 'success'.
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.
Sprawdzamy nie tylko wynik testu, ale też pochodzenie kodu, danych i uprawnień.
write-all.Reusable workflow nie może podnieść uprawnień tokena callera. Sekrety przekazuje się przez jawny interfejs; samo env nie tworzy takiego kontraktu.
- 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}"
process.env.PR_TITLE albo bezpośredniego odczytu payloadu context.eval. Walidacja tekstu nie zastępuje oceny zaufania do kodu PR-a.GitHub — ograniczanie script injection · zizmor — wykrywanie niebezpiecznej interpolacji (template-injection)
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
| Odwołanie | Co zapewnia? |
|---|---|
| Gałąź / ruchomy major tag | Wygodna aktualizacja, ale kod pod nazwą może się zmienić. |
| Tag konkretnej wersji | Czytelna wersja; sama nazwa tagu nie gwarantuje niezmienności. |
| Pełny commit SHA | Konkretną rewizję kodu action, nie automatyczny dowód jej bezpieczeństwa. |
| Immutable release | Niezmienny 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.
Pinning third-party actions · Immutable releases · Wydania i ruchome tagi actions
tj-actions/changed-files zmieniono kod osiągalny przez istniejące tagi.Wykrywanie zmienionych plików jest przydatne, ale dodatkowa action to również dodatkowy kod do zaufania. Pokazany klient API pozwala zobaczyć samą regułę wprost.
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.
pnpm dlx uruchamia tu narzędzie pin-github-action. To ono przepisuje refs w uses; sprawdź diff i komunikaty o nierozwiązanych odwołaniach.zizmor · Dependabot dla Actions · Renovate — github-actions · pin-github-action 3.5.2
Praca dyplomowa: źródła w Git, kompilacja w CI, PDF dla promotora i wersjonowane wydania.
engineer.tex — główny dokument.sprz.cls i sprz/ — klasa, styl i zasoby.bibliography.bib — bibliografia, obsługiwana przez biber.engineer.pdf — wynik kompilacji.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.
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ę.
- 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ć.
| Źródło | Wynik | Granica bezpieczeństwa |
|---|---|---|
| Fork PR lub Dependabot | PDF jako artefakt. | Ograniczony token; bez publikacji release. |
| Same-repo PR bez zmian workflow | Artefakt 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 main | PDF 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.
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.');
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.
- 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.
- 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.
Nieznane polecenie albo brak wymaganego pliku może zatrzymać kompilację i zablokować publikację.
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.
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.
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.
W laboratorium zastosujemy te mechanizmy do aplikacji. Wykład pozostaje mapą pojęć, nie gotowym rozwiązaniem zadania.