@uzysjung/agent-harness 26.148.0 → 26.149.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-PTUM2NI3.js → chunk-YSW3OLH4.js} +50 -9
- package/dist/chunk-YSW3OLH4.js.map +1 -0
- package/dist/index.js +228 -109
- package/dist/index.js.map +1 -1
- package/dist/trust-tier-drift.js +1 -1
- package/package.json +2 -1
- package/templates/rules/ship-checklist.md +4 -3
- package/templates/rules/test-policy.md +12 -3
- package/templates/skills/humanize-korean/SKILL.md +228 -0
- package/templates/skills/north-star/NORTH_STAR.template.md +106 -47
- package/templates/skills/north-star/SKILL.md +230 -74
- package/templates/skills/north-star/references/roadmap-method.md +62 -12
- package/templates/skills/self-hosted-github-runner/SKILL.md +328 -0
- package/templates/skills/verification-loop/SKILL.md +23 -0
- package/dist/chunk-PTUM2NI3.js.map +0 -1
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: self-hosted-github-runner
|
|
3
|
+
description: >
|
|
4
|
+
GitHub 호스티드 러너를 못 쓰게 됐을 때, 레포의 기존 워크플로를 **그대로** 자기 머신의 Docker
|
|
5
|
+
self-hosted runner 에서 돌리게 만든다. 결제 실패·지출한도·분(minutes) 소진·조직 쿼터·Actions
|
|
6
|
+
장애로 CI 가 멈췄을 때, "CI 를 로컬에서 돌리자"·"self-hosted runner"·"act 로 돌릴까"·"CI 죽었는데
|
|
7
|
+
머지·배포 어떻게 하냐"는 말이 나올 때 쓴다. **누군가 "CI 스텝을 스크립트에 옮겨 적어 로컬에서
|
|
8
|
+
돌리자"고 제안할 때도 반드시 이 스킬을 켠다** — 그 안은 워크플로를 두 벌로 갈라놓고, 갈라진
|
|
9
|
+
순간부터 로컬 초록이 CI 초록을 뜻하지 않게 되기 때문이다.
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Self-Hosted GitHub Runner
|
|
13
|
+
|
|
14
|
+
CI 가 멈췄을 때 **검증을 포기하지 않고, 동시에 검증을 위조하지도 않는** 방법.
|
|
15
|
+
|
|
16
|
+
## 이 스킬이 막으려는 것
|
|
17
|
+
|
|
18
|
+
CI 가 멈추면 두 가지 유혹이 온다.
|
|
19
|
+
|
|
20
|
+
1. **그냥 배포한다.** 게이트를 한 번 건너뛰면 다음에도 건너뛴다.
|
|
21
|
+
2. **CI 스텝을 로컬 스크립트에 베껴 적는다.** 이게 더 위험하다 — 초록이 나오니까 검증한 것처럼
|
|
22
|
+
보이는데, 워크플로에 스텝이 하나 추가되는 순간 스크립트는 그걸 모른 채 계속 초록을 낸다.
|
|
23
|
+
**검증을 잃은 것보다 나쁜 것은, 잃은 줄 모르는 것이다.**
|
|
24
|
+
|
|
25
|
+
self-hosted runner 는 워크플로 파일 **자체**를 실행한다. 복제본이 0이므로 드리프트가 구조적으로
|
|
26
|
+
불가능하고, PR 상태 체크·태그 트리거·필수 체크가 전부 그대로 살아 있다.
|
|
27
|
+
|
|
28
|
+
## Step 0 — 정말 인프라 문제인지 먼저 가른다
|
|
29
|
+
|
|
30
|
+
"CI 가 빨갛다"에서 곧바로 러너로 뛰지 마라. 코드가 깨진 건데 인프라 탓으로 돌리면 결함을
|
|
31
|
+
러너 뒤에 숨기게 된다. **대조군으로 가른다** — 시각 기준으로 갈리면 인프라, 커밋 기준으로
|
|
32
|
+
갈리면 코드다.
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
gh run list --workflow=<wf>.yml --limit 10 \
|
|
36
|
+
--json createdAt,headSha,conclusion,event \
|
|
37
|
+
--jq '.[] | "\(.createdAt) \(.headSha[0:7]) \(.event) \(.conclusion)"'
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
잡이 **스텝 0개로 몇 초 만에** 죽었다면 잡이 시작조차 못 한 것이다. 실제 사유는 어노테이션에 있다:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
JOB=$(gh api repos/<owner>/<repo>/actions/runs/<run-id>/jobs --jq '.jobs[0].id')
|
|
44
|
+
gh api repos/<owner>/<repo>/check-runs/$JOB/annotations --jq '.[].message'
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
실측 예: `"The job was not started because recent account payments have failed or your spending
|
|
48
|
+
limit needs to be increased."` — 이건 코드와 무관하다.
|
|
49
|
+
|
|
50
|
+
## Step 1 — 워크플로를 복제하지 않고 전환 가능하게 만든다
|
|
51
|
+
|
|
52
|
+
`runs-on` 은 `vars` 컨텍스트를 받는다(공식 Context availability 표). 그래서 **한 줄**로 옮길 수 있다:
|
|
53
|
+
|
|
54
|
+
```yaml
|
|
55
|
+
jobs:
|
|
56
|
+
build:
|
|
57
|
+
runs-on: ${{ vars.CI_RUNNER_LABEL || 'ubuntu-latest' }}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
변수를 설정하면 self-hosted 로, 지우면 호스티드로 돌아온다. 워크플로 파일을 복사하거나 `if:` 로
|
|
61
|
+
잡을 두 벌 만들지 마라 — 그게 바로 이 스킬이 막으려는 드리프트다.
|
|
62
|
+
|
|
63
|
+
전 잡에 적용해야 한다. 하나라도 빠뜨리면 그 잡만 호스티드를 기다리다 영원히 큐에 남는다.
|
|
64
|
+
|
|
65
|
+
## Step 2 — 아키텍처를 **재고 나서** 정한다
|
|
66
|
+
|
|
67
|
+
self-hosted 머신이 Apple Silicon 이면 arm64 다. 워크플로가 `ubuntu-latest`(x64)를 전제하고 쓴
|
|
68
|
+
것들이 여기서 깨진다. 두 선택지가 있고, **추측하지 말고 재라**:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
for P in linux/arm64 linux/amd64; do
|
|
72
|
+
docker run --rm --platform $P node:22-bookworm-slim node -e "
|
|
73
|
+
const s=Date.now(); let h=0; for(let i=0;i<8e6;i++){h=(h*31+i)>>>0;}
|
|
74
|
+
console.log('$P ms='+(Date.now()-s));"
|
|
75
|
+
done
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
실측 사례(M-series, Docker Desktop): arm64 **127ms** vs amd64 **497ms** — 에뮬레이션이 3.9배 느렸다.
|
|
79
|
+
|
|
80
|
+
**그런데 이 숫자로 고르지 마라.** 나도 이걸 보고 arm64 를 골랐다가 되돌렸다. 속도는 **의존성이
|
|
81
|
+
그 아키텍처에서 실제로 도는 다음**의 기준이다 — 돌지 않는 것이 빠를 수는 없다. 네이티브 모듈
|
|
82
|
+
하나만 prebuild 가 없어도 테스트 스위트가 통째로 죽는다.
|
|
83
|
+
|
|
84
|
+
**기본값은 `ubuntu-latest` 와 같은 amd64 로 두고**, arm64 는 "이 레포의 네이티브 의존성이 arm64
|
|
85
|
+
prebuild 를 갖고 있다"를 확인한 뒤에 성능 최적화로 택하라. 아키텍처를 맞춰 두면 실패했을 때
|
|
86
|
+
"CI 환경이 달라서인가"를 먼저 의심하지 않아도 된다 — 그 의심을 지우는 값이 3.9배보다 크다.
|
|
87
|
+
|
|
88
|
+
### 흔히 깨지는 것: 아키텍처 하드코딩된 도구 설치
|
|
89
|
+
|
|
90
|
+
```yaml
|
|
91
|
+
# 깨진다 — arm64 러너에서 x64 바이너리는 실행되지 않는다
|
|
92
|
+
curl -sSfL ".../gitleaks_${VERSION}_linux_x64.tar.gz" -o /tmp/t.tar.gz
|
|
93
|
+
echo "${EXPECTED} /tmp/t.tar.gz" | sha256sum -c -
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
```yaml
|
|
97
|
+
# 고친 것 — 핀을 버리지 않고 아키텍처별로 둔다
|
|
98
|
+
case "$(uname -m)" in
|
|
99
|
+
x86_64) ASSET=linux_x64; EXPECTED=<x64 sha> ;;
|
|
100
|
+
aarch64) ASSET=linux_arm64; EXPECTED=<arm64 sha> ;;
|
|
101
|
+
*) echo "지원하지 않는 아키텍처: $(uname -m) — 핀을 추가하세요" >&2; exit 1 ;;
|
|
102
|
+
esac
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**해시는 릴리스의 공식 checksums 파일에서 옮겨 적는다.** 직접 받아서 계산하면 "검증하려던 대상으로
|
|
106
|
+
검증값을 만드는" 꼴이라 공급망 핀의 의미가 사라진다. 옮겨 적은 뒤 기존 핀과 대조해 같은 출처인지
|
|
107
|
+
확인하면 좋다. 미지원 아키텍처는 조용히 넘어가지 말고 **exit 1** — 스킵은 "검사했는데 통과"로 읽힌다.
|
|
108
|
+
|
|
109
|
+
### 더 고약한 것: 캐시 키가 아키텍처를 구분하지 않는다
|
|
110
|
+
|
|
111
|
+
`runner.os` 는 x64 든 ARM64 든 **똑같이 `Linux`** 다. 그래서 이 키는 두 아키텍처에서 충돌한다:
|
|
112
|
+
|
|
113
|
+
```yaml
|
|
114
|
+
key: node-modules-${{ runner.os }}-node22-${{ hashFiles('package-lock.json') }} # 깨진다
|
|
115
|
+
key: node-modules-${{ runner.os }}-${{ runner.arch }}-node22-${{ hashFiles(...) }} # 고친 것
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
x64 러너가 올린 `node_modules` 를 ARM64 러너가 그대로 복원하고, 캐시 히트라 `npm ci` 는 건너뛴다.
|
|
119
|
+
네이티브 바인딩만 다른 아키텍처 것이라 **캐시·설치 스텝은 전부 초록으로 찍히고 두세 스텝 뒤에
|
|
120
|
+
테스트가 죽는다** — 실측:
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
Cannot find module '@rolldown/binding-linux-arm64-gnu'
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
원인 스텝과 증상 스텝이 떨어져 있어서 "테스트가 깨졌다"로 오진하기 딱 좋다. **아키텍처를 바꿔
|
|
127
|
+
러너를 옮길 때는 캐시 키부터 확인하라.** 같은 함정이 `~/.cache`·pip·cargo·Go 빌드 캐시에도 있다.
|
|
128
|
+
|
|
129
|
+
**이건 self-hosted 로 옮겨야만 드러나는 결함이다.** 호스티드만 쓰는 동안에는 전부 x64 라 영원히
|
|
130
|
+
숨어 있는다 — 옮기는 김에 잡은 것이고, 고친 값은 호스티드에서도 그대로 옳다.
|
|
131
|
+
|
|
132
|
+
## Step 3 — 러너를 컨테이너로 띄운다
|
|
133
|
+
|
|
134
|
+
호스트에 직접 붙이지 말고 Linux 컨테이너에서 돌린다. 워크플로는 리눅스를 전제한다(`apt-get`,
|
|
135
|
+
`sha256sum`, `/tmp` 경로, `--with-deps`). macOS 호스트에 러너를 붙이면 그 전제가 전부 깨진다.
|
|
136
|
+
|
|
137
|
+
### 이미지의 **베이스 OS 를 `ubuntu-latest` 와 맞춰라** — `latest` 태그를 믿지 마라
|
|
138
|
+
|
|
139
|
+
러너 이미지의 `latest` 가 최신 Ubuntu 라는 보장이 없다. 실측: `myoung34/github-runner:latest` 는
|
|
140
|
+
**Ubuntu 20.04(glibc 2.31)**, GitHub 의 `ubuntu-latest` 는 **24.04(glibc 2.39)** — 두 세대 차이다.
|
|
141
|
+
|
|
142
|
+
그 차이가 이렇게 나타난다:
|
|
143
|
+
|
|
144
|
+
```
|
|
145
|
+
Error: Cannot find addon '.' imported from '…/sodium-native/binding.js'
|
|
146
|
+
Candidates:
|
|
147
|
+
- file:///…/sodium-native/prebuilds/linux-x64/sodium-native.node ← 이 파일은 실제로 있다
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**메시지가 거짓말을 한다.** "찾을 수 없다"고 하지만 파일은 919KB 짜리로 멀쩡히 있고, 실제로는
|
|
151
|
+
`dlopen` 이 실패한 것이다. 원인은 `ldd` 로만 보인다:
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
ldd node_modules/<pkg>/prebuilds/linux-x64/<pkg>.node
|
|
155
|
+
# → version `GLIBC_2.33' not found (required by …)
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
**네이티브 모듈이 "파일을 못 찾는다"고 하면 파일 유무부터 확인하고, 있으면 `ldd` 를 걸어라.**
|
|
159
|
+
없는 파일을 찾아 헤매는 것이 이 함정의 본체다. 우리는 이걸 "설치가 깨졌나 → 아키텍처 문제인가 →
|
|
160
|
+
npm 캐시인가"로 몇 시간 쫓다가 결국 `ldd` 한 줄로 끝냈다.
|
|
161
|
+
|
|
162
|
+
착수 전에 재는 편이 싸다:
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
docker run --rm --entrypoint sh <이미지> -c 'grep PRETTY_NAME /etc/os-release; ldd --version | head -1'
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
띄우기·내리기·상태확인을 스크립트 하나로 묶어 두면 원복(Step 5)까지 한 곳에 남는다. 핵심은 이것이다:
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
TOKEN=$(gh api -X POST /repos/<owner>/<repo>/actions/runners/registration-token --jq .token)
|
|
172
|
+
|
|
173
|
+
ENVFILE=$(mktemp); chmod 600 "$ENVFILE"
|
|
174
|
+
cat > "$ENVFILE" <<EOF
|
|
175
|
+
REPO_URL=https://github.com/<owner>/<repo>
|
|
176
|
+
RUNNER_NAME=local-docker
|
|
177
|
+
RUNNER_TOKEN=$TOKEN
|
|
178
|
+
RUNNER_WORKDIR=/tmp/runner/work
|
|
179
|
+
LABELS=<고유 라벨>
|
|
180
|
+
EPHEMERAL=true
|
|
181
|
+
EOF
|
|
182
|
+
|
|
183
|
+
docker run -d --name ci-runner --env-file "$ENVFILE" \
|
|
184
|
+
-v ci-runner-work:/tmp/runner myoung34/github-runner:latest
|
|
185
|
+
rm -f "$ENVFILE"
|
|
186
|
+
|
|
187
|
+
gh variable set CI_RUNNER_LABEL --body '<고유 라벨>' --repo <owner>/<repo>
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### 안전한 기본값 세 가지 — 하나씩 이유가 있다
|
|
191
|
+
|
|
192
|
+
**1. 라벨에 `self-hosted` 를 쓰지 마라.** GitHub 이 **모든** self-hosted 러너에 자동으로 붙이는
|
|
193
|
+
기본 라벨이다. 타깃으로 쓰면 나중에 이 레포·조직에 러너가 하나만 더 붙어도 우리 잡을 가져간다.
|
|
194
|
+
`<repo>-local` 처럼 고유한 값을 쓴다.
|
|
195
|
+
|
|
196
|
+
**2. `EPHEMERAL=true`.** 비-ephemeral 러너는 잡 사이에 작업 디렉터리와 프로세스 상태가 남는다 —
|
|
197
|
+
한 잡이 심어 놓은 것이 다음 잡에서 실행된다(러너 오염). ephemeral 이면 잡 하나를 처리하고
|
|
198
|
+
등록을 스스로 해제한다. **다음 잡을 돌리려면 다시 띄워야 해서 불편한데, 그 불편이 곧 격리다.**
|
|
199
|
+
|
|
200
|
+
**3. 등록 토큰을 `-e` 로 넘기지 마라.** `docker inspect` 에 평문으로 남는다. 600 권한 임시 파일로
|
|
201
|
+
넘기고 곧바로 지운다. 토큰 자체는 1시간 유효·등록 시 1회 소비지만, 그 사이에 남길 이유가 없다.
|
|
202
|
+
|
|
203
|
+
### `services:` 를 쓸 때만 docker.sock 을 붙인다 — 기본은 끈다
|
|
204
|
+
|
|
205
|
+
워크플로에 `services: postgres` 같은 게 있으면 러너가 컨테이너를 만들 수 있어야 하고, 그래서
|
|
206
|
+
`/var/run/docker.sock` 을 마운트하게 된다.
|
|
207
|
+
|
|
208
|
+
**이건 사실상 호스트 장악 권한이다** — 그 소켓에 닿는 코드는 호스트 파일시스템을 마운트한 root
|
|
209
|
+
컨테이너를 띄울 수 있다. 필요 없는 잡까지 이 권한 아래 돌리지 마라. 워크플로에 `services:` 가
|
|
210
|
+
없으면 **빼라.**
|
|
211
|
+
|
|
212
|
+
**소켓만 주면 반쯤만 동작한다 — `--network host` 가 같이 필요하다.** 러너가 컨테이너 안이면
|
|
213
|
+
서비스 컨테이너는 **호스트 포트**에 게시되는데, 워크플로는 `localhost:5432` 를 본다. GitHub
|
|
214
|
+
호스티드는 잡이 VM 위에서 돌아 localhost 가 곧 게시 대상이라 이 차이가 안 보인다. 실측하면
|
|
215
|
+
`db:migrate` 가 **에러 메시지도 없이 exit 1** 한다 — 연결 실패라고 말해 주지 않아서 마이그레이션
|
|
216
|
+
파일을 먼저 뒤지게 된다. 두 옵션은 하나로 묶어 두는 게 낫다.
|
|
217
|
+
|
|
218
|
+
호스트 포트가 컨테이너에서 보이는지 먼저 재라(대조군 포함):
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
docker run -d --name probe -p 55999:5432 -e POSTGRES_PASSWORD=x postgres:16
|
|
222
|
+
docker run --rm postgres:16 pg_isready -h host.docker.internal -p 55999 # 대조군
|
|
223
|
+
docker run --rm --network host postgres:16 pg_isready -h localhost -p 55999
|
|
224
|
+
docker rm -f probe
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## 가장 큰 위험: PR 이 내 머신에서 돈다
|
|
228
|
+
|
|
229
|
+
워크플로가 `on: pull_request` 로 돌면, 러너가 켜져 있는 동안 **모든 PR 의 CI 가 내 머신에서**
|
|
230
|
+
실행된다. private 레포라고 안심하면 안 된다 — 위험은 "누가 PR 을 여는가"가 아니라 **"그 잡이
|
|
231
|
+
무슨 코드를 실행하는가"** 에서 온다:
|
|
232
|
+
|
|
233
|
+
- **의존성 bump PR**(dependabot 등)은 새 패키지를 설치한다 → 그 패키지의 `postinstall` 이 내
|
|
234
|
+
머신에서 실행된다. 워크플로 파일이 base 브랜치 것이어도 **설치되는 코드는 PR 것**이다.
|
|
235
|
+
- 저장소 협업자가 여는 PR 도 마찬가지다. 신뢰는 사람에 대한 것이지 그 사람이 끌어오는 트랜지티브
|
|
236
|
+
의존성에 대한 것이 아니다.
|
|
237
|
+
|
|
238
|
+
public 레포면 **하지 마라.** fork PR 이 임의 코드를 실행한다 — GitHub 이 명시적으로 말린다.
|
|
239
|
+
|
|
240
|
+
private 레포라면 착수 전에 실측해서 알린다:
|
|
241
|
+
|
|
242
|
+
```bash
|
|
243
|
+
grep -rl "pull_request" .github/workflows/
|
|
244
|
+
gh pr list --state open --json headRefName \
|
|
245
|
+
--jq '[.[] | select(.headRefName|startswith("dependabot/"))] | length'
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
**결정은 사용자 몫이다.** 짧게 쓰고 내릴 거면 그대로 두고, 오래 켜둘 거면 의존성 PR 을 먼저
|
|
249
|
+
닫거나, self-hosted 를 태그·수동 실행에만 쓰고 PR CI 는 복구될 때까지 비워 둔다. 조용히
|
|
250
|
+
켜 두지는 마라.
|
|
251
|
+
|
|
252
|
+
## Step 4 — 도는지 확인하고, 그다음에 "된다"고 말한다
|
|
253
|
+
|
|
254
|
+
등록됐다 ≠ 잡이 배정된다. 둘은 다른 사건이다.
|
|
255
|
+
|
|
256
|
+
```bash
|
|
257
|
+
gh api /repos/<owner>/<repo>/actions/runners \
|
|
258
|
+
--jq '.runners[] | "\(.name) status=\(.status) labels=" + ([.labels[].name]|join(","))'
|
|
259
|
+
|
|
260
|
+
gh workflow run <wf>.yml --ref <branch>
|
|
261
|
+
# 잡이 실제로 시작하는지 본다 — queued 에서 안 움직이면 라벨이 안 맞는 것이다
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
**결제 실패 계정에서도 self-hosted 러너에는 잡이 배정된다** — 2026-08-20 실측. 같은 레포에서
|
|
265
|
+
호스티드 잡은 `"The job was not started because recent account payments have failed…"` 로 2~4초 만에
|
|
266
|
+
스텝 0개로 죽었는데, self-hosted 로 라벨을 옮기니 같은 워크플로가 정상적으로 스텝을 밟았다.
|
|
267
|
+
공식 문서는 "GitHub Actions usage is free for self-hosted runners"까지만 말하고 이 경우를 명시하지
|
|
268
|
+
않으므로, 다른 계정 상태(정지·organization 제재 등)에서도 같으리라 단정하지는 마라 — 확인하고 쓴다.
|
|
269
|
+
|
|
270
|
+
**등록 API 의 `status` 를 믿지 마라.** 컨테이너 로그에 `Listening for Jobs` 가 찍힌 뒤에도 API 가
|
|
271
|
+
잠시 `offline` 을 돌려준다. 등록 확인은 **컨테이너 로그**로, 배정 확인은 **잡이 실제로
|
|
272
|
+
`in_progress` 로 넘어가는지**로 한다.
|
|
273
|
+
|
|
274
|
+
큐에서 안 움직일 때 볼 것: 라벨 불일치(`runs-on` 값 ≠ 러너 `LABELS`) · 변수 미설정 · 러너 offline ·
|
|
275
|
+
그 워크플로의 `if:` 조건 미충족 · **ephemeral 러너가 앞 잡을 처리하고 이미 내려갔다**(잡이 2개 이상인
|
|
276
|
+
워크플로에서 흔하다 — 두 번째 잡은 영원히 큐에 남는다).
|
|
277
|
+
|
|
278
|
+
## Step 5 — 원복 경로를 먼저 만들어 두고 시작한다
|
|
279
|
+
|
|
280
|
+
임시 조치는 **끝내는 방법이 있을 때만 임시**다. 없으면 그냥 새 영구 상태다.
|
|
281
|
+
|
|
282
|
+
```bash
|
|
283
|
+
gh variable delete CI_RUNNER_LABEL --repo <owner>/<repo> # 호스티드로 복귀
|
|
284
|
+
ID=$(gh api /repos/<owner>/<repo>/actions/runners --jq '.runners[]|select(.name=="local-docker")|.id')
|
|
285
|
+
gh api -X DELETE /repos/<owner>/<repo>/actions/runners/$ID # 등록 해제
|
|
286
|
+
docker rm -f ci-runner
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
**등록 해제를 빼먹지 마라.** 컨테이너만 지우면 GitHub 에는 offline 러너가 남고, 그 라벨로 오는
|
|
290
|
+
잡은 영영 큐에 머문다.
|
|
291
|
+
|
|
292
|
+
## 자원 — 조용히 느려지는 것보다 미리 말하는 게 낫다
|
|
293
|
+
|
|
294
|
+
Docker Desktop 기본 할당은 대개 CPU 2·RAM 2~4GB 다. 테스트 수백 파일 + 빌드 + 브라우저 E2E 를
|
|
295
|
+
돌리는 CI 에는 부족하고, **OOM 은 코드 결함처럼 보이는 실패로 나타난다.**
|
|
296
|
+
|
|
297
|
+
```bash
|
|
298
|
+
docker info --format 'CPU {{.NCPU}} · RAM {{.MemTotal}}'
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
부족하면 착수 전에 알린다(권장 CPU 6+ · RAM 12GB+). 여기서 막지는 말되, 나중에 "왜 이렇게
|
|
302
|
+
느리냐"는 질문이 나오게 두지도 마라.
|
|
303
|
+
|
|
304
|
+
## `act` 를 먼저 검토했는가
|
|
305
|
+
|
|
306
|
+
[`nektos/act`](https://github.com/nektos/act) 은 워크플로를 로컬 도커에서 실행한다. **드리프트 0이라는
|
|
307
|
+
장점은 같다.** 진짜 러너 대신 act 가 나은 경우가 있다:
|
|
308
|
+
|
|
309
|
+
| | self-hosted runner | `act` |
|
|
310
|
+
|---|---|---|
|
|
311
|
+
| PR 상태 체크·필수 체크 | **살아난다** | 안 된다(GitHub 이 결과를 모른다) |
|
|
312
|
+
| 태그·스케줄 트리거 | 그대로 | 수동 재현 |
|
|
313
|
+
| 결제·쿼터 의존 | 확인 필요(Step 4) | 완전 무관 |
|
|
314
|
+
| 설정 비용 | 등록·원복 필요 | 설치만 |
|
|
315
|
+
| 아키텍처 하드코딩 스텝 | 같이 깨진다 | 같이 깨진다 |
|
|
316
|
+
|
|
317
|
+
**PR 게이트·배포 게이트를 되살리는 게 목적이면 러너**, 그냥 로컬에서 워크플로를 돌려보고 싶은
|
|
318
|
+
거면 `act` 다. 목적을 먼저 묻고 고른다.
|
|
319
|
+
|
|
320
|
+
## 보고할 때
|
|
321
|
+
|
|
322
|
+
이 조치로 돌린 CI 는 **평소와 다른 환경**에서 돈 것이다. 그 사실을 숨기지 않는다 — 특히 배포
|
|
323
|
+
게이트로 쓴 경우.
|
|
324
|
+
|
|
325
|
+
적을 것: 어느 러너에서 돌았나(호스티드/self-hosted, 아키텍처) · 워크플로를 고쳤다면 무엇을 ·
|
|
326
|
+
평소 CI 와 달라진 것 · 아직 확인 못 한 것.
|
|
327
|
+
|
|
328
|
+
"CI 통과"라고만 적으면 다음 사람은 GitHub 호스티드에서 통과한 줄 안다.
|
|
@@ -49,6 +49,29 @@ API/service, CLI/TUI, library/SDK, documents/configuration, user flow. Each trac
|
|
|
49
49
|
observation and its own evidence record: read
|
|
50
50
|
[references/tracks.md](references/tracks.md).
|
|
51
51
|
|
|
52
|
+
## Then pick the depth — it scales with the risk
|
|
53
|
+
|
|
54
|
+
The Testing rule says depth follows risk and names what counts as high-risk (authentication,
|
|
55
|
+
authorization, payments and settlement, personal data, data integrity, concurrency, state
|
|
56
|
+
transitions, migrations). It deliberately stops there. **Which** instruments to widen with is a
|
|
57
|
+
per-change judgment, and this is where that menu lives:
|
|
58
|
+
|
|
59
|
+
| Instrument | Reach for it when |
|
|
60
|
+
|---|---|
|
|
61
|
+
| Regression beyond the directly affected scope | the change moves a shared type, a schema, a config default, or anything the affected scope was only *assumed* to bound |
|
|
62
|
+
| Integration / contract tests | it crosses a boundary someone else owns — a service, a queue, a stored format, a published API |
|
|
63
|
+
| Critical-path E2E | a user-visible flow that must not break can only be observed end to end |
|
|
64
|
+
| Mutation testing | **you doubt the tests you already have would catch a defect** |
|
|
65
|
+
|
|
66
|
+
**Mutation testing is an option, not a requirement.** It is expensive, so being labelled
|
|
67
|
+
high-risk is not by itself a reason to run it — the reason is uncertainty about detection power.
|
|
68
|
+
When the existing suite has already been shown to bite (a negative control, a caught regression),
|
|
69
|
+
the doubt it answers is not there and the cost buys nothing.
|
|
70
|
+
|
|
71
|
+
Two things this depth choice is *not*: it is not a coverage target, and it is not the scheduled
|
|
72
|
+
full run. Full regression, full E2E, full mutation, and periodic security scanning belong to the
|
|
73
|
+
CI/CD schedule — do not launch them here for one change.
|
|
74
|
+
|
|
52
75
|
## Verification Phases (static gates)
|
|
53
76
|
|
|
54
77
|
### Phase 1: Build Verification
|