푸시를 했는데 배포 승인 알림이 오지 않았다. CI는 성공으로 끝나 있었다.
사내 서비스는 CI가 이미지를 빌드한 뒤 관리 콘솔로 웹훅을 보낸다. 콘솔이 웹훅을 받아야 배포가 대기열에 올라가고 텔레그램으로 승인 요청이 온다. CI 로그를 보니 웹훅 응답이 invalid body였다.
원인
웹훅 본문은 셸 스크립트 안에서 문자열을 이어 붙여 만들고 있었다. 커밋 제목을 작은따옴표로 감싸서 JSON 사이에 넣는 방식이었다.
"commitMessage": "'"$(echo '${{ env.COMMIT_MSG }}' | sed 's/"/\\"/g')"'",
${{ }} 표현식은 GitHub Actions가 스크립트를 실행하기 전에 텍스트로 치환한다. 커밋 제목에 console's처럼 아포스트로피가 있으면 작은따옴표가 그 자리에서 닫히고, 나머지 글자는 셸 문법으로 해석된다. 큰따옴표는 sed로 이스케이프하고 있었지만 작은따옴표는 처리하지 않았다. 콘솔은 깨진 JSON을 받고 거부했다. 같은 구조라면 커밋 제목에 넣은 셸 명령이 러너에서 실행될 수도 있었다.
CI가 성공으로 표시된 이유는 두 가지였다. 이 스텝에 continue-on-error가 걸려 있었고, curl은 서버가 400을 돌려줘도 종료 코드 0으로 끝난다.
같은 버그를 세 번 고쳤다
처음은 8월 29일, 쇼핑 서비스였다. 그 저장소의 워크플로만 고쳤다. 9월 22일에 다른 서비스에서 같은 증상이 나왔고, 이번에도 그 저장소만 고쳤다. 10월 4일 세 번째 저장소에서 다시 나왔을 때 원인이 템플릿에 있다는 걸 확인했다. 서비스 12개가 같은 템플릿에서 만들어져 같은 블록을 각자 갖고 있었다.
수정
- 커밋 제목은 스크립트에 치환하지 않고 환경 변수로 넘긴다. 셸이 변수 값으로 읽기 때문에 따옴표가 문법으로 해석되지 않는다.
- JSON은
jq로 만들고,jq가 없는 러너에서는 python으로 만든다. continue-on-error를 빼고 curl에--fail-with-body를 붙였다. 웹훅이 거부되면 스텝이 실패로 표시되고 응답 내용이 로그에 남는다.- 빌드가 실패해도 웹훅을 보내도록 했다. 실패한 빌드도 콘솔에 기록된다.
- 템플릿을 먼저 고치고, 같은 블록을 나머지 저장소에 넣었다. 워크플로만 바꾸는 커밋에는
[skip ci]를 붙여서, 저장소 10여 개에 한꺼번에 푸시해도 빌드와 승인 요청이 동시에 쏟아지지 않게 했다.
