작업 일지

커밋 제목의 따옴표 하나로 배포가 누락됐다

커밋 제목에 아포스트로피가 들어가면 배포 웹훅 본문이 깨졌다. CI는 성공으로 끝나서 실패가 드러나지 않았다. 같은 문제를 세 저장소에서 따로 고친 다음에야 템플릿을 고쳤다.

2분#infra#ci#workflow

푸시를 했는데 배포 승인 알림이 오지 않았다. 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여 개에 한꺼번에 푸시해도 빌드와 승인 요청이 동시에 쏟아지지 않게 했다.