diff --git a/.github/scripts/reconcile-issue-metadata.mjs b/.github/scripts/reconcile-issue-metadata.mjs index 0839fb8eb8..b8bcb8e4b9 100644 --- a/.github/scripts/reconcile-issue-metadata.mjs +++ b/.github/scripts/reconcile-issue-metadata.mjs @@ -55,19 +55,12 @@ export const ASSIGNEE_BY_MODULE = Object.freeze({ platform: "1hyok", }); -// #1910: 담당 이관은 모듈마다 소급 범위가 다르다. 설정은 이미 열려 있는 이슈까지 곧바로 옮기고, -// 온보딩은 이 결정 전에 열린 이슈를 옮기지 않는다. -// -// 소급 여부가 왜 선택지인가. issue-metadata-guard 의 스케줄 실행은 열린 이슈 전체를 훑으므로, -// 지도만 바꾸면 진행 중인 이슈까지 전부 새 담당자에게 넘어간다. 그러면 그 이슈로 열어 둔 PR 이 -// validate-pr-issue-link 에서 "작성자가 대표 이슈의 담당자가 아니다" 로 빨개지고, 옮겨 받은 -// 사람은 남이 절반쯤 짜 둔 브랜치를 떠안는다. 그래서 소급은 기본값이 아니라 결정 사항이다. -// -// fromIssue 미만의 이슈는 before 가 계속 맡는다. 경계는 결정을 적은 이슈 번호이고, 그보다 큰 -// 번호는 전부 결정 이후에 열린 이슈다. -export const HANDOVER_BY_MODULE = Object.freeze({ - onboarding: Object.freeze({ before: "1hyok", fromIssue: 1910 }), -}); +// #1910: 담당 이관은 모듈마다 소급 범위를 고를 수 있다. 경계(fromIssue)를 두면 그 번호 미만의 +// 열린 이슈는 before 가 계속 맡고, 경계가 없는 모듈은 스케줄 리컨사일이 열린 이슈 전체를 새 +// 담당자에게 옮긴다. 온보딩은 #1910 에서 경계를 두었다가 온보딩 전체를 준혁이 맡기로 하면서 +// 경계를 걷어 소급했다(0911). 그 결과 이관 전 이슈로 열어 둔 PR 은 작성자와 담당자가 달라 +// validate-pr-issue-link 가 빨개지고, 리뷰어가 풀어야 한다. +export const HANDOVER_BY_MODULE = Object.freeze({}); /** 이관 경계를 반영한 담당자. 경계가 없는 모듈은 지도 그대로다. */ export function assigneeForIssue(moduleKey, issueNumber) { diff --git a/.github/scripts/reconcile-issue-metadata.test.mjs b/.github/scripts/reconcile-issue-metadata.test.mjs index 5e4f7461a9..93bbc1be71 100644 --- a/.github/scripts/reconcile-issue-metadata.test.mjs +++ b/.github/scripts/reconcile-issue-metadata.test.mjs @@ -155,9 +155,10 @@ test("a module without a handover entry moves its open issues right away", () => test("an unknown issue number fails the handover judgement instead of guessing", () => { // 경계 판정을 못 하면 어느 쪽으로도 접지 않는다. 접으면 옛 이슈가 조용히 새 담당자에게 간다. - const module = Object.keys(HANDOVER_BY_MODULE)[0]; - assert.throws(() => assigneeForIssue(module, undefined), /이슈 번호/); - assert.throws(() => assigneeForIssue(module, "1910번"), /이슈 번호/); + for (const module of Object.keys(HANDOVER_BY_MODULE)) { + assert.throws(() => assigneeForIssue(module, undefined), /이슈 번호/); + assert.throws(() => assigneeForIssue(module, "1910번"), /이슈 번호/); + } }); test("maps every work type to exactly one classification label", () => { diff --git a/.github/workflows/pr-assign-author.yml b/.github/workflows/pr-assign-author.yml new file mode 100644 index 0000000000..2524879ce9 --- /dev/null +++ b/.github/workflows/pr-assign-author.yml @@ -0,0 +1,50 @@ +# PR 작성자 자동 어사인 +# +# 어사인이 비어 있는 PR 은 보드·필터에서 담당자가 사라진다. 이슈는 +# issue-metadata-guard 가 어사인을 강제하는데 PR 은 손에 맡겨져 있었다. +# PR 이 열리면 작성자를 어사인하고, 누가 떼어 내도 다시 건다. +# review-request-all 과 같은 조건(봇·외부 기여자 제외)으로 돈다. +name: pr-assign-author + +on: + pull_request: + types: [opened, reopened, ready_for_review, unassigned] + +permissions: + pull-requests: write + +jobs: + assign: + if: github.event.pull_request.state == 'open' + runs-on: ubuntu-latest + timeout-minutes: 3 + steps: + - name: 작성자를 어사인 + env: + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + PR_NUMBER: ${{ github.event.pull_request.number }} + AUTHOR: ${{ github.event.pull_request.user.login }} + ASSOCIATION: ${{ github.event.pull_request.author_association }} + run: | + set -euo pipefail + + case "$AUTHOR" in + *'[bot]'|dependabot|github-actions) echo "봇 PR — 건너뜀"; exit 0 ;; + esac + + # 외부 기여자 PR 은 토큰이 read-only 라 어사인할 수 없다. + case "$ASSOCIATION" in + OWNER|MEMBER|COLLABORATOR) ;; + *) echo "외부 기여자($ASSOCIATION) — 건너뜀"; exit 0 ;; + esac + + current=$(gh api "repos/$REPO/pulls/$PR_NUMBER" \ + --jq '[.assignees[].login] | join(" ")') + case " $current " in + *" $AUTHOR "*) echo "이미 어사인됨"; exit 0 ;; + esac + + gh api -X POST "repos/$REPO/issues/$PR_NUMBER/assignees" \ + -f "assignees[]=$AUTHOR" >/dev/null + echo "어사인: $AUTHOR"