idea_world_labDEV JOURNAL
2026년 6월 25일 목요일

Qwen PR Review Workflow 구조와 개선 기록

작성일: 2026년 6월 25일

목적

이 문서는 qwen-code 기반 PR 리뷰 workflow를 가져와 현재 저장소에 맞게 커스텀하는 과정을 기록한다.

이번 작업의 핵심은 단순히 workflow가 성공으로 끝나는 것이 아니라, Qwen이 생성한 리뷰 결과가 실제 GitHub PR 댓글로 남도록 만드는 것이다.

배경

이 저장소는 CodeRabbit, Sourcery, Qodo 같은 외부 리뷰 봇도 함께 사용한다. 다만 별도로 qwen-code workflow를 가져와 OpenRouter 경로로 동작시키고, PR마다 Qwen 기반 리뷰를 남기는 흐름도 실험하고 있다.

처음에는 비용 문제 때문에 AI PR 리뷰 API 흐름을 멈췄다. 이후 OpenRouter free tier에서 openai/gpt-oss-120b:free를 활용할 수 있어, 이 경로로 다시 시도하는 방향으로 README를 수정했다.

원래 workflow 구조

대상 파일:

.github/workflows/qwen-code-pr-review.yml

원래 workflow 이름:

🧐 Qwen Pull Request Review

주요 trigger:

on:
  pull_request_target:
    types: ['opened']
  pull_request_review_comment:
    types: ['created']
  pull_request_review:
    types: ['submitted']
  workflow_dispatch:
    inputs:
      pr_number:
        required: true
        type: 'number'

job 조건은 다음 경우에 실행되도록 되어 있었다.

  • workflow_dispatch 수동 실행
  • pull_request_targetopened 이벤트이고 PR 작성자가 OWNER, MEMBER, COLLABORATOR인 경우
  • PR review comment에 @qwen /review가 들어간 경우
  • PR review 본문에 @qwen /review가 들어간 경우

workflow의 주요 단계는 다음과 같았다.

  1. PR 코드 checkout
  2. PR 제목, 본문, 변경 파일, diff 정보 수집
  3. QwenLM/qwen-code-action 실행
  4. prompt 안에서 Qwen에게 리뷰를 만들고 gh pr comment로 댓글을 달라고 지시

관찰된 문제

PR #5에서 workflow를 실제로 관찰했다.

PR:

https://github.com/yyeongjin/idea_world/pull/5

PR은 정상 생성되었고 다음 봇들은 반응했다.

  • CodeRabbit
  • Sourcery
  • Qodo

Qwen workflow도 pull_request_target 이벤트에서는 실행되었다.

관찰된 run:

workflow: 🧐 Qwen Pull Request Review
event: pull_request_target
conclusion: success

하지만 실제 PR에는 Qwen 리뷰 댓글이 남지 않았다.

workflow log를 보면 Qwen은 리뷰 내용을 작성한 뒤 실제 도구 호출을 실행하지 않고, 다음과 같은 형태의 JSON 스타일 지시를 텍스트로 출력했다.

{"tool":"write_file", ...}
{"tool":"run_shell_command", ...}

즉 workflow 자체는 success였지만, 사용자가 기대한 결과인 “PR 댓글 작성”은 완료되지 않았다.

원인 정리

문제는 Qwen에게 댓글 작성까지 맡긴 구조였다.

원래 prompt는 Qwen에게 다음을 직접 수행하라고 지시했다.

1. write_file("review.md", "<your detailed review feedback here>")
2. gh pr comment $PR_NUMBER --body-file review.md --repo $REPOSITORY

하지만 실제 실행에서는 Qwen이 이 작업을 도구 호출로 수행하지 않고, 해야 할 작업을 텍스트로만 출력했다.

따라서 모델이 도구를 제대로 호출하지 않으면 workflow가 success여도 PR에는 아무 댓글이 남지 않는다.

추가로 확인한 구조 문제

workflow의 job 조건에는 issue_comment 이벤트가 포함되어 있었다.

(github.event_name == 'issue_comment' && ... contains(github.event.comment.body, '@qwen /review') ...)

하지만 on: trigger에는 issue_comment가 없었다.

즉 PR conversation에 @qwen /review를 댓글로 남겨도 workflow가 아예 시작되지 않는 구조였다.

또한 pull_request_target trigger는 opened에만 반응했다.

pull_request_target:
  types: ['opened']

그래서 PR 브랜치에 새 커밋이 push되는 synchronize 이벤트에는 자동으로 다시 실행되지 않았다.

개선 방향

모델에게 GitHub 댓글 작성을 맡기지 않고, GitHub Actions step이 댓글 작성을 책임지도록 바꾼다.

개선 후 책임 분리는 다음과 같다.

Qwen
  -> 리뷰 Markdown 텍스트만 생성

GitHub Actions
  -> Qwen 출력값을 파일로 저장
  -> gh pr comment로 PR에 댓글 작성

이렇게 하면 Qwen이 도구 호출을 텍스트로 출력하더라도, 최소한 workflow step에서 qwen_result를 받아 댓글을 남길 수 있다.

변경 내용

1. trigger 보강

기존:

pull_request_target:
  types: ['opened']

변경:

pull_request_target:
  types: ['opened', 'reopened', 'synchronize']
issue_comment:
  types: ['created']

의도:

  • PR이 새로 열릴 때 실행
  • PR이 다시 열릴 때 실행
  • PR 브랜치에 새 커밋이 올라올 때 실행
  • PR conversation에 @qwen /review를 남겼을 때 실행

추가 관찰:

on: trigger만 보강하면 충분하지 않았다. 실제로 PR 브랜치에 새 커밋을 push했을 때 pull_request_target run은 생성됐지만, job 조건이 아직 github.event.action == 'opened'만 허용하고 있어서 run이 skipped로 끝났다.

따라서 job 조건도 다음처럼 같이 수정해야 한다.

(github.event.action == 'opened' ||
 github.event.action == 'reopened' ||
 github.event.action == 'synchronize')

2. Qwen step에 id 부여

변경:

- name: 'Run Qwen PR Review'
  id: 'qwen_review'

의도:

후속 step에서 다음 output을 읽기 위함이다.

steps.qwen_review.outputs.qwen_result

3. prompt 책임 변경

기존 prompt는 Qwen에게 파일 작성과 PR 댓글 작성을 직접 지시했다.

변경 후 prompt는 Qwen에게 리뷰 Markdown 텍스트만 최종 답변으로 반환하도록 지시한다.

Return the review markdown text as your final answer.
Do not call write_file.
Do not call gh pr comment.
Do not output JSON tool-call instructions.
A later GitHub Actions step will post your final answer as a PR comment.

의도:

  • Qwen이 도구 호출 JSON을 출력하는 문제를 줄인다.
  • 댓글 작성 책임을 모델이 아니라 workflow step으로 옮긴다.

4. Qwen 출력값을 PR 댓글로 게시하는 step 추가

추가된 step:

- name: 'Post Qwen PR Review'
  if: |-
    ${{ always() && steps.qwen_review.outputs.summary != '' }}
  env:
    GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
    PR_NUMBER: '${{ steps.get_pr.outputs.pr_number || steps.get_pr_comment.outputs.pr_number }}'
    REPOSITORY: '${{ github.repository }}'
    QWEN_RESULT: '${{ steps.qwen_review.outputs.summary }}'
  run: |-
    {
      echo "## Qwen PR Review"
      echo
      printf '%s\n' "$QWEN_RESULT"
    } > qwen-review.md
    gh pr comment "$PR_NUMBER" --body-file qwen-review.md --repo "$REPOSITORY"

의도:

  • Qwen output이 비어 있지 않으면 무조건 PR 댓글로 남긴다.
  • 댓글 작성은 GITHUB_TOKENgh pr comment를 쓰는 deterministic한 step이 담당한다.

추가 관찰:

처음에는 output 이름을 steps.qwen_review.outputs.qwen_result로 잘못 읽었다. 실제 QwenLM/qwen-code-actionaction.yml을 확인해보니 composite 내부 step은 qwen_result를 만들지만, action 바깥으로 공개되는 output 이름은 summary였다.

outputs:
  summary:
    value: ${{ steps.qwen_run.outputs.qwen_result }}

그래서 후속 댓글 step은 steps.qwen_review.outputs.summary를 읽어야 한다. qwen_result를 읽으면 Qwen이 리뷰 본문을 정상 생성해도 post step이 skipped된다.

주의할 점

pull_request_target workflow는 PR 브랜치의 workflow 파일이 아니라 base branch의 workflow 파일을 기준으로 실행된다.

따라서 PR 브랜치에 workflow 수정을 올리는 것만으로는 현재 PR의 pull_request_target 동작이 바뀌지 않는다.

실제로 적용하려면 다음 중 하나가 필요하다.

  1. workflow 수정 자체를 먼저 main에 반영한다.
  2. 그 다음 PR에 @qwen /review를 남기거나 새 커밋을 push해서 변경된 workflow를 실행한다.

이번 작업에서는 workflow 수정은 main에 먼저 반영하고, PR #5에 다시 Qwen review를 트리거해서 실제 댓글 작성 여부를 확인하는 흐름으로 진행한다.

보안 기준

workflow 문서에는 API key, endpoint URL, provider base URL 같은 민감값을 기록하지 않는다.

GitHub Actions log에서도 다음 값은 secret masking 대상이어야 한다.

OPENAI_API_KEY
OPENAI_BASE_URL
OPENAI_MODEL
GITHUB_TOKEN

문서에는 모델 변경 사실과 workflow 구조만 남기고, 실제 secret 값은 기록하지 않는다.

현재 결론

이번 관찰로 확인한 것은 다음이다.

  • PR 생성 자체는 정상 동작했다.
  • CodeRabbit, Sourcery, Qodo는 정상적으로 PR에 반응했다.
  • Qwen workflow도 pull_request_target에서는 success로 끝났다.
  • 하지만 기존 구조에서는 Qwen이 실제 댓글을 쓰지 않고 도구 호출 지시를 텍스트로 출력할 수 있었다.
  • 따라서 “workflow 성공”과 “PR 댓글 작성 성공”은 별도로 봐야 한다.
  • Qwen 출력값을 GitHub Actions 후속 step이 직접 댓글로 게시하도록 바꾸는 것이 더 안전하다.