CI/CD 기초 (2) - CI with GitHub Actions
workflow, event, job, step 네 층 구조부터 최소 워크플로, 린트와 테스트, branch protection까지 GitHub Actions로 첫 CI를 만드는 과정을 정리합니다.
1편에서 파이프라인이 왜 필요한지를 정리했습니다. 이번 편에서는 파이프라인의 앞부분인 CI를 GitHub Actions로 직접 만듭니다. 실습용 GitHub 저장소가 하나 필요합니다.
What is GitHub Actions
GitHub Actions의 규칙은 하나입니다. 저장소의 .github/workflows/ 폴더에 YAML 파일을 넣어두면, GitHub이 정해진 사건이 일어날 때 그 파일에 적힌 대로 실행한다. 별도 가입도, 서버 준비도 없습니다. 폴더와 파일 이름 규칙만 지키면 됩니다.
실행은 내 컴퓨터가 아니라 GitHub이 빌려주는 가상 머신에서 일어납니다. 이 머신을 runner라고 부릅니다. runner는 실행할 때마다 새로 만들어지고 끝나면 사라집니다. 매번 아무것도 깔려 있지 않은 깨끗한 상태에서 시작한다는 뜻이고, 이것이 1편에서 말한 “로컬에서는 됐는데”를 잡아주는 장치입니다.
네 층 구조
YAML을 읽고 쓰려면 네 개의 단어만 알면 됩니다. 큰 것부터 작은 것 순서입니다.
| 층 | 뜻 |
|---|---|
| workflow | 자동화 한 덩어리. YAML 파일 하나가 워크플로 하나 |
| event | 워크플로를 언제 실행할지. push, pull_request 등 |
| job | 워크플로 안의 작업 묶음. job 하나가 runner 하나를 받아서 돈다 |
| step | job 안의 개별 명령. 위에서 아래로 순서대로 실행된다 |
파일 하나(workflow)가 사건(event)에 반응해서, 가상 머신(job)에서, 명령들(step)을 순서대로 실행한다고 읽으면 됩니다.
First Workflow
가장 작은 워크플로부터 만들어 봅니다. 저장소에 아래 파일을 추가하고 push합니다. 파일 이름은 ci.yml이 아니어도 되지만 폴더 경로는 정확히 .github/workflows/여야 합니다.
1
2
3
4
5
6
7
8
9
10
# .github/workflows/ci.yml
name: CI
on: push
jobs:
hello:
runs-on: ubuntu-latest
steps:
- run: echo "파이프라인이 돌았습니다"
한 줄씩 읽으면 이렇습니다.
name: CI: 워크플로 이름입니다. GitHub 화면에 이 이름으로 표시됩니다.on: push: 사건 지정입니다. 이 저장소에 push가 일어날 때마다 실행합니다.jobs:아래hello: job의 이름입니다. 자유롭게 지으면 됩니다.runs-on: ubuntu-latest: runner로 우분투 최신 버전 머신을 씁니다.steps:아래- run: ...: 셸 명령 하나를 실행하는 step입니다.
push한 뒤 저장소 페이지의 Actions 탭에 들어가면 방금 실행된 워크플로가 목록에 보입니다. 클릭해서 들어가면 job과 step별 로그가 있고, echo가 찍은 문장을 확인할 수 있습니다. 여기까지 되면 GitHub Actions의 배선은 끝난 것입니다.
actions/checkout
이제 echo 대신 내 코드를 검사하게 만들 차례인데, 그 전에 짚을 것이 하나 있습니다. runner는 깨끗한 머신이라 내 저장소 코드도 없습니다. 코드를 검사하려면 먼저 코드를 runner로 가져와야 합니다.
이때 쓰는 것이 actions/checkout입니다. step에는 두 종류가 있는데, run:은 셸 명령을 직접 실행하는 것이고 uses:는 남이 만들어 둔 기성 부품(action)을 가져다 쓰는 것입니다. checkout은 GitHub이 만들어 둔 “저장소 코드를 runner로 복사하는” action입니다.
1
2
3
steps:
- uses: actions/checkout@v4 # 저장소 코드를 runner로 가져온다
- run: ls # 이제 내 파일들이 보인다
@v4는 action의 버전입니다. 버전을 박아두어야 남의 부품이 바뀌어도 내 파이프라인이 갑자기 다르게 동작하지 않습니다.
린트와 테스트
이제 파이썬 프로젝트를 검사하는 CI를 완성합니다. 저장소에 테스트가 하나라도 있다고 가정합니다. 없다면 아래처럼 아주 작은 것부터 만들면 됩니다.
1
2
3
# tests/test_smoke.py
def test_smoke():
assert 1 + 1 == 2
워크플로는 이렇게 바뀝니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install ruff pytest
- run: ruff check .
- run: pytest
바뀐 부분을 짚습니다.
on:이 두 갈래가 됐습니다. main에 push될 때, 그리고 모든 pull request에서 실행합니다. PR에서 돌게 하는 것이 핵심입니다. 합쳐지기 전에 검사한다는 뜻이기 때문입니다.actions/setup-python@v5: runner에 파이썬을 설치하는 action입니다.with:는 action에 넘기는 옵션이고, 여기서는 버전을 지정했습니다.- 이후 세 step은 로컬에서 치던 명령 그대로입니다. 의존성 설치, 린트, 테스트입니다.
step은 순서대로 실행되고, 하나라도 실패하면 그 뒤 step은 실행되지 않고 job 전체가 실패로 끝납니다. ruff가 걸리면 pytest까지 가지 않습니다. 빠르게 끝나는 검사를 앞에 두는 이유입니다.
실패하면 어떻게 보이나?
일부러 테스트를 깨뜨려서 push해 보면 세 군데에서 실패가 보입니다.
- 커밋 옆에 빨간 X가 붙습니다. 클릭하면 어떤 검사가 실패했는지 나옵니다.
- Actions 탭의 해당 실행이 빨간색이 되고, 로그에서 실패한 step과 에러 메시지를 볼 수 있습니다.
- 워크플로를 실행시킨 사람에게 실패 알림 메일이 갑니다.
초록 체크와 빨간 X, 이 두 가지가 CI의 출력 전부입니다. 중요한 것은 이 신호가 커밋 직후에 온다는 점입니다. 무엇을 고치다 깨졌는지 기억이 생생할 때 알림이 오므로, 며칠 뒤에 발견하는 것과는 수리 비용이 다릅니다.
branch protection
지금 상태의 CI는 알려주기만 합니다. 빨간 X가 떠도 PR을 합칠 수는 있습니다. 사람이 X를 무시하면 그만이라면 자동화의 의미가 절반입니다.
저장소의 Settings에서 Branches로 들어가 main에 대한 branch protection rule을 만들고, “Require status checks to pass”에 위의 test job을 지정합니다. 이렇게 하면 CI가 초록이 되기 전에는 merge 버튼이 눌리지 않습니다. “검사를 통과해야 합쳐진다”가 권장 사항이 아니라 저장소의 규칙이 됩니다.
PR과 branch protection 자체가 처음이라면 Git 스터디 노트 3편에 협업 흐름 관점의 설명이 있습니다.
정리
| 개념 | 한 줄 요약 |
|---|---|
| GitHub Actions | .github/workflows/의 YAML을 사건이 일어날 때 실행 |
| runner | 매번 새로 만들어지는 깨끗한 가상 머신 |
| 네 층 구조 | workflow가 event에 반응해 job에서 step을 순서대로 실행 |
| run과 uses | run은 셸 명령, uses는 남이 만든 action 부품 |
| checkout | runner에는 코드가 없으므로 먼저 가져와야 한다 |
| branch protection | CI 통과를 머지의 조건으로 만들어야 CI가 완성된다 |
다음 편은 파이프라인의 뒷부분입니다. 검사를 통과한 코드를 자동으로 내보내는 CD를 다룹니다.