프로젝트 부팅 시에만 CI 적용
출처: Dev.to
I did not add CI to Knot Forget before the Django project existed. There would not have been much point: nothing meaningful to install, lint, or test.
Knot Forget에 Django 프로젝트가 존재하기 전에 CI를 추가하지 않았습니다. 그때는 큰 의미가 없었을 것입니다: 설치할 것이 전혀 없었고, 린트도 테스트도 할 것이 없었기 때문입니다.
I added it at the first moment where it could prove something useful. The project could boot, dependencies were managed, settings loaded, Ruff had rules to enforce, and pytest could run a smoke test against a minimal home view. There was still no domain logic, no models, no real API, and no feature work worth protecting by hand.
첫 번째 유용할 수 있는 시점에 추가했습니다. 프로젝트가 부팅되고, 의존성이 관리되며, 설정이 로드되고, Ruff 규칙이 적용되며, pytest로 메인 페이지에 대한 최소 테스트를 실행할 수 있었습니다. 그때는 도메인 로직도 없고, 모델도 없고, 실제 API도 없으며, 보호가 필요할 만한 기능 작업도 없었습니다.
That timing is the part I care about.
그 시점이 제가 가장 신경 쓰는 부분입니다.
Add CI after the project has a real baseline, but before the codebase feels important enough for exceptions.
프로젝트에 실제 베이스라인이 생기면 CI를 추가하고, 코드베이스가 예외가 필요할 만큼 중요해지기 전에 합니다.
The first CI pipeline does not need to predict the future. Mine has two jobs: lint and test.
첫 번째 CI 파이프라인은 미래를 예측할 필요가 없습니다. 제 파이프라인에는 두 개의 작업만 있습니다: lint와 test입니다.
The lint job installs the project dependencies, runs Ruff checks, and verifies formatting. The test job installs the same dependencies and runs pytest. There is no deployment step, no Docker image publishing, no coverage threshold, and no database service yet. Those can come later, when the project actually needs them.
lint 작업은 프로젝트 의존성을 설치하고 Ruff 검사를 실행하며 포맷을 검증합니다. test 작업은 동일한 의존성을 설치하고 pytest를 실행합니다. 아직 배포 단계도, Docker 이미지 게시도, 커버리지 제한도, 데이터베이스 서비스도 없습니다. 나중에 프로젝트가 실제로 필요할 때 추가할 수 있습니다.
At this stage, CI has a narrower job: make every pull request prove that the project still has a working baseline.
이 단계에서는 CI의 임무가 좁아집니다: 모든 풀 리퀘스트가 프로젝트가 여전히 정상적인 베이스라인을 유지하고 있음을 증명하도록 하는 것입니다.
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v6
with:
enable-cache: true
- run: uv sync --frozen
- run: uv run ruff check .
- run: uv run ruff format --check .
test:
runs-on: ubuntu-latest
env:
DJANGO_SECRET_KEY: ci-dummy-secret-key-not-used-in-production
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v6
with:
enable-cache: true
- run: uv sync --frozen
- run: uv run pytest
The stack is specific to this project: GitHub Actions, uv, Ruff, pytest, Django. The shape is not. Install the project reproducibly, check the code, run the tests, and keep the first version small enough that a failure means something.
이 스택은 이 프로젝트에 특화되어 있습니다: GitHub Actions, uv, Ruff, pytest, Django입니다. 형태는 그렇지 않습니다. 프로젝트를 재현 가능하게 설치하고, 코드를 검사하고, 테스트를 실행하며, 첫 번째 버전을 충분히 작게 유지하여 실패가 의미 있는 결과를 낳도록 해야 합니다.
A complicated first pipeline is easy to explain away. A boring one is harder to argue with.
복잡한 초기 파이프라인은 쉽게 변명할 수 있지만, 평범한 하나는 논쟁하기 어렵습니다.
Adding a workflow file is not the whole job. If CI runs but failed checks do not block merging, the pipeline is mostly informational: useful, but not structural. It tells you something went wrong, then leaves the decision to whoever is tired enough to merge anyway.
워크플로우 파일을 추가하는 것이 전부가 아닙니다. CI가 실행되지만 실패한 검사가 병합을 차단하지 않는다면 파이프라인은 주로 정보적인 역할을 하며 구조적이지 않습니다. 유용하지만 실제로는 구조적이지 않으며, 문제가 발생하면 지치기까지 한 사람에게 결정을 맡깁니다.
The important step is making the checks required on main.
중요한 단계는 메인 브랜치에 검사를 필수적으로 만드는 것입니다.
For Knot Forget, the branch ruleset requires both lint and test before a pull request can merge. Once that is true, CI becomes part of the repository contract.
Knot Forget에서는 브랜치 규칙이 풀 리퀘스트가 병합되기 전에 lint와 test 둘 다 필요로 합니다. 이를 만족하면 CI는 저장소 계약의 일부가 됩니다.
Nobody has to remember to ask whether the checks passed, and nobody has to decide whether this lint failure is acceptable because the change is small.
누구도 검사가 통과했는지 기억하려 애쓰지 않고, 변경이 작다고 해서 lint 실패가 수용 가능하다고 판단할 필요도 없습니다.
The repository answers before the merge button does.
저장소는 머지 버튼보다 먼저 답변합니다.
That matters even on a solo project. Especially on a solo project. The person most likely to bypass a weak process is usually the same person who created it, late in the evening, convinced the patch is harmless.
이것은 단독 프로젝트에서도 중요합니다. 특히 단독 프로젝트일 때는 더욱 그렇습니다. 약한 프로세스를 우회할 가능성이 가장 높은 사람은 대체로 그 자체를 만든 사람이며, 늦은 저녁에 패치가 무해하다고 생각하면서 작업합니다.
Required checks remove that negotiation.
필수 검사는 그러한 협상을 없애줍니다.
The first version of the workflow did not include an env block in the test job.
첫 번째 워크플로우 버전에는 테스트 작업에 env 블록이 포함되지 않았습니다.
Locally, tests passed. That made sense: my machine already had the project environment in place. The .env file existed, the settings could read what they needed, and Django could start.
локально 테스트가 통과했습니다. 이는 자연스러운 일입니다. 내 기계는 이미 프로젝트 환경을 갖추고 있었기 때문입니다. .env 파일이 존재했고, 설정이 필요한 값을 읽을 수 있었으며, Django가 시작될 수 있었습니다.
Then CI ran for the first time, on a clean GitHub runner, with only the repository and the workflow instructions available. The test job failed before it reached any meaningful application behavior. Django tried to load settings, settings required DJANGO_SECRET_KEY, and the runner did not have one.
그 후 CI가 처음으로 깨끗한 GitHub 실행 환경에서 실행되었습니다. 이때 리포지토리와 워크플로우 지침만 사용되었습니다. 테스트 작업이 의미 있는 애플리케이션 동작에 도달하기 전에 실패했습니다. Django는 설정을 로드하려고 시도했고, 설정에 DJANGO_SECRET_KEY가 필요했으나 런너에는 없었습니다.
That was not a problem with CI. That was CI doing exactly what I added it to do.
이건 CI의 문제였습니다. 이는 내가 추가하도록 한 그대로 CI가 정확히 수행한 것이었습니다.
The first useful thing CI did was disagree with my machine.
CI가 처음으로 유용하게 작동한 것은 내 기계와 의견을 달리하는 것이었습니다.
The pipeline had found a real assumption: the project needed an environment variable to boot, and the workflow had not declared it. Without CI, I could have kept running tests locally and missed that detail for longer, because the code looked healthy on the one machine that already had the missing piece.
파이프라인은 실제 가정 하나를 발견했습니다: 프로젝트가 부팅하려면 환경 변수가 필요하고 워크플로우에 이를 선언하지 않았다는 점입니다. CI가 없었다면 현지에서는 계속 테스트를 실행하며 이 세부 사항을 더 오래 간과했을 것입니다. 왜냐하면 코드가 이미 해당 요소를 갖춘 한 대의 기계에서 정상적으로 보였기 때문입니다.
The fix was small:
test: runs-on: ubuntu-latest env: DJANGO_SECRET_KEY: ci-dummy-secret-key-not-used-in-production steps: - uses: actions/checkout@v4 - uses: astral-sh/setup-uv@v6 with: enable-cache: true - run: uv sync —frozen - run: uv run pytest
The important part is not that the value is fake. Of course a test job should not use a production secret. The important part is that the test job now declares what the project needs in order to start.
가짜 값이라는 사실이 중요한 것이 아닙니다. 물론 테스트 작업에서는 프로덕션 비밀번호를 사용해서는 안 됩니다. 중요한 점은 이제 테스트 작업이 프로젝트가 시작하기 위해 필요한 사항을 선언하고 있다는 것입니다.
That is what even a basic CI pipeline gives you. With only lint and test, the repository has to prove that a fresh runner can install the project, check the code, and run the test suite. That is a stronger statement than “it works on my laptop”. It means the code works from the instructions committed to the repo.
기본 CI 파이프라인조차도 제공합니다. lint와 test만으로 저장소는 신규 실행 환경이 프로젝트를 설치하고 코드를 검사하며 테스트 스위트를 실행할 수 있음을 입증해야 합니다. 이는 “내 랩톱에서 작동한다”는 주장보다 더 강력합니다. 이는 리포지토리에 커밋된 지침을 통해 코드가 동작함을 의미합니다.
At this point in the project, there was not much behavior to test. That was fine. The smoke test only needed to prove that Django could start and serve the minimal home view; it was not pretending to validate domain behavior that did not exist yet.
이 시점에서는 검증할 동작이 별로 없었습니다.それで 충분했습니다. 연기 테스트는 Django가 시작하고 메인 페이지의 최소 뷰를 제공할 수 있음을 증명하는 데만 필요했으며, 아직 존재하지 않는 도메인 행동을 검증하려 했다는 것이 아니었습니다.
That small test still carried weight. It forced the settings module to load, forced required environment to be present, and made CI exercise the same bootstrap path future tests will build on.
그 작은 테스트에도 무게가 있었습니다. 이는 설정을 로드하고 필요한 환경을 존재하게 만들었으며, CI는 향후 테스트가 구축할 부트스트랩 경로를 동일하게 실행하도록 했습니다.
A first smoke test is not about coverage. It is about proving the project can stand up in a clean environment.
첫 연기 테스트는 커버리지에 관한 것이 아니라, 프로젝트가 깨끗한 환경에서 작동할 수 있음을 증명하는 데 있습니다.
That phrase matters. Local tests are necessary, but they are not enough to prove bootstrap. Your machine accumulates state: a .env file exists because you created it earlier, a dependency may already be installed, a shell variable may still be set. A command can pass locally for reasons that are not visible in the repository.
그 표현이 중요합니다. 현지 테스트는 필요하지만 부트스트랩을 증명하기에는 충분하지 않습니다. 당신의 기계는 상태를 누적합니다: .env 파일이 존재할 수 있고, 의존성이 이미 설치되어 있으며, 셸 변수가 아직 설정되어 있을 수 있습니다. 명령어가 현지에서 성공할 이유는 저장소 안에서는 보이지 않을 수 있습니다.
CI removes most of that accidental help. For a new project, one smoke test on a clean runner can reveal missing setup assumptions before they harden into undocumented project knowledge.
CI는 대부분의 우연적 도움을 제거합니다. 새로운 프로젝트라면 깨끗한 실행 환경에서 단 하나의 연기 테스트만으로 설정 상정이 누락된 것을 발견할 수 있어, 그것이 비공식적인 프로젝트 지식으로 굳어지는 것을 방지할 수 있습니다.
There is one awkward detail in GitHub branch protection: a status check has to exist before it can be selected as required.
GitHub 브랜치 보호에 한 가지 어색한 점이 있습니다: 상태 검사가 존재하기 전에 필수로 선택될 수 없습니다.
So the sequence is not quite “decide the check names, require them, then add CI”. In practice, it is closer to this: add the workflow, open a pull request, let the jobs run once, fix whatever the clean runner exposes, then add the resulting lint and test checks to the branch ruleset.
따라서 순서는 ‘체크 이름을 정하고, 필요로 지정하고, 그다음에 CI를 추가한다’는 것이 아니라, 실제로는 다음과 가깝습니다: 워크플로우를 추가하고, 풀 리퀘스트를 열어서 한 번 실행한 뒤, 깨끗한 런너가 노출하는 문제를 수정하고, resulting lint와 test checks 를 브랜치 규칙에 필수적으로 지정합니다.
That first run is not just ceremony to make GitHub display the check names. It is the first clean‑room execution of the project. It is where missing environment, incomplete setup instructions, and false local assumptions show up.
그 첫 번째 실행은 GitHub에서 체크 이름을 표시하기 위한 의례 그 이상이 아닙니다. 그것은 프로젝트의 첫 번째 청정실(깨끗한) 실행이며, 여기서 누락된 환경, 불완전한 설정 지침, 그리고 현지 가정의 오류가 나타납니다.
Once that run is green, requiring the checks means something. You are not requiring an imagined pipeline. You are requiring a command path that has already survived outside your machine.
그 실행이 녹색이면 검사를 필수로 하는 의미가 생깁니다. 당신은 상상이 아닌 실제 파이프라인을 요구하는 것이 아니라, 기기 밖에서 이미 검증된 명령 경로를 요구하는 것입니다.
“Set up CI from day one” is close, but not quite the rule I would use.
“첫 날부터 CI를 설정하라”는 말은 거의 맞지만, 제가 사용하고 싶은 규칙은 아닙니다.
Day one might be too early. Before the project can boot, CI is mostly ceremony: you can add an empty workflow, but it does not protect much. “Set up CI later” is worse, because later usually means after feature work begins, after conventions have started drifting, and after local assumptions have become invisible.
첫 날은 너무 이른 시점일 수 있습니다. 프로젝트가 부팅하기 전에는 CI는 대부분 의례에 불과합니다: 빈 워크플로우를 추가해도 큰 보호를 제공하지 않습니다. “나중에 CI를 설정하라”는 더 나쁜데, 이는 대체로 기능 작업이 시작된 뒤, 관행이 흔들리기 시작하고 현지 가정치가 무의식적으로 사라진 뒤에 이루어집니다.
The useful moment is in between.
유용한 시점은 그 사이에 있습니다.
Add CI when the project can boot: when there is one real command to lint, one real command to test, and one minimal path through application startup. Then run those commands somewhere clean enough to disagree with your machine.
프로젝트가 부팅할 수 있을 때 CI를 추가하세요: 린트 명령 하나, 테스트 명령 하나, 그리고 최소한의 애플리케이션 시작 경로가 존재할 때입니다. 그런 다음 기기와의 의견 차이를 일으킬 만큼 깨끗한 곳에서 그 명령을 실행합니다.
That disagreement is the point.
그 의견 차이는 핵심적인 점입니다.
In my case, the first CI run immediately caught that the test job was missing DJANGO_SECRET_KEY. The fix was one line, but the signal was larger: the project was not fully reproducible from the repository yet.
제 경우 첫 번째 CI 실행은 테스트 작업이 DJANGO_SECRET_KEY를 누락하고 있음을 바로 포착했습니다. 수정은 한 줄뿐이었지만, 신호는 더 컸습니다: 프로젝트가 아직 저장소에서 완전히 재현 가능하지 않다는 점이었습니다.
That is why I want CI as soon as the project can meaningfully pass through it. Not because the pipeline is mature, and not because there is much behavior to test yet, but because even a small lint + test