From 8aa9633b9901d799df553c57c3c7436d6022c7ee Mon Sep 17 00:00:00 2001 From: jmj Date: Thu, 20 Aug 2026 14:40:43 +0900 Subject: [PATCH] =?UTF-8?q?fix(ci):=20PR=20=EC=A0=9C=EB=AA=A9=EC=9D=B4=20?= =?UTF-8?q?=EC=85=B8=EC=97=90=20=EA=B7=B8=EB=8C=80=EB=A1=9C=20=EC=82=BD?= =?UTF-8?q?=EC=9E=85=EB=90=98=EB=8D=98=20Discord=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EC=9B=8C=ED=81=AC=ED=94=8C=EB=A1=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 직전 PR(#190) 제목에 작은따옴표(`'잠시 중단'`)가 들어가자 notify 잡이 실패했다: curl: (3) bad range specification in URL position 253 `${{ github.event.pull_request.title }}` 를 작은따옴표로 감싼 셸 문자열 안에 그대로 끼워 넣고 있었다. `${{ }}` 는 셸 실행 **전에** 치환되므로, 제목에 따옴표가 있으면 그 자리에서 문자열이 끝나고 나머지가 셸 토큰으로 해석된다. 깨지는 것만이 문제가 아니다. 같은 이유로 **러너에서 임의 명령을 실행할 수 있다** (GitHub Actions script injection). 이 잡에는 `DISCORD_WEBHOOK` 시크릿이 env 로 들어온다. fork PR 에는 시크릿이 전달되지 않지만, 같은 저장소 브랜치에서는 전달된다. - 사용자 제어 값(제목·브랜치·actor)을 전부 `env:` 로만 넘긴다 — 러너가 직접 주입하므로 셸이 그 내용을 파싱하지 않는다 - JSON 은 `jq --arg` 가 만든다 — 따옴표·개행·백슬래시가 알아서 이스케이프된다 - Discord embed title 상한(256자)에 맞춰 자른다 - `curl -f` 추가. 지금까지는 Discord 가 400 을 돌려줘도 잡이 성공으로 끝나 잘못된 페이로드를 알 수 없었다 --- .github/workflows/discord-pr-notify.yml | 57 ++++++++++++++----------- 1 file changed, 32 insertions(+), 25 deletions(-) diff --git a/.github/workflows/discord-pr-notify.yml b/.github/workflows/discord-pr-notify.yml index 287929e7..b40f404e 100644 --- a/.github/workflows/discord-pr-notify.yml +++ b/.github/workflows/discord-pr-notify.yml @@ -9,31 +9,38 @@ jobs: runs-on: ubuntu-latest steps: - name: Send Discord Notification + # PR 제목·브랜치명은 사용자가 정하는 값이다. ${{ }} 로 run 스크립트에 직접 끼워 넣으면 + # 셸이 그 문자열을 코드로 해석한다 — 따옴표 하나에 명령이 깨지고(실제로 깨졌다), + # 작정하면 러너에서 임의 명령을 실행할 수 있다(GitHub Actions script injection). + # 값은 env 로만 넘기고(러너가 직접 주입, 셸 파싱 없음) JSON 은 jq 가 이스케이프한다. env: DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK_URL }} + PR_TITLE: ${{ github.event.pull_request.title }} + PR_URL: ${{ github.event.pull_request.html_url }} + PR_AVATAR: ${{ github.event.pull_request.user.avatar_url }} + PR_ACTOR: ${{ github.actor }} + PR_HEAD: ${{ github.event.pull_request.head.ref }} + PR_BASE: ${{ github.event.pull_request.base.ref }} run: | - curl -H "Content-Type: application/json" \ - -d '{ - "content": "🚀 **새로운 PR이 도착했습니다!**", - "embeds": [{ - "title": "${{ github.event.pull_request.title }}", - "url": "${{ github.event.pull_request.html_url }}", - "color": 3447003, - "thumbnail": { - "url": "${{ github.event.pull_request.user.avatar_url }}" - }, - "fields": [ - { - "name": "작성자", - "value": "${{ github.actor }}", - "inline": true - }, - { - "name": "브랜치", - "value": "${{ github.event.pull_request.head.ref }} ➡️ ${{ github.event.pull_request.base.ref }}", - "inline": true - } - ] - }] - }' \ - $DISCORD_WEBHOOK \ No newline at end of file + set -euo pipefail + payload=$(jq -n \ + --arg title "$PR_TITLE" \ + --arg url "$PR_URL" \ + --arg avatar "$PR_AVATAR" \ + --arg actor "$PR_ACTOR" \ + --arg branches "$PR_HEAD ➡️ $PR_BASE" \ + '{ + content: "🚀 **새로운 PR이 도착했습니다!**", + embeds: [{ + title: ($title | .[0:256]), + url: $url, + color: 3447003, + thumbnail: { url: $avatar }, + fields: [ + { name: "작성자", value: $actor, inline: true }, + { name: "브랜치", value: $branches, inline: true } + ] + }] + }') + # -f 가 없으면 Discord 가 400 을 줘도 성공으로 끝난다(지금까지 그랬다). + curl -sS -f -H "Content-Type: application/json" -d "$payload" "$DISCORD_WEBHOOK"