From aaba4711e8f6d1cd8c83e393533b9607d9a7befa Mon Sep 17 00:00:00 2001 From: chaeyn Date: Sat, 1 Aug 2026 01:00:38 +0900 Subject: [PATCH] =?UTF-8?q?chore:=20=ED=98=91=EC=97=85=20=EC=9E=90?= =?UTF-8?q?=EB=8F=99=ED=99=94=20=EC=84=A4=EC=A0=95=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/ISSUE_TEMPLATE/config.yml | 2 + .github/ISSUE_TEMPLATE/task.yml | 37 ++++++++++++++ .github/pull_request_template.md | 11 +++++ .github/workflows/convention-check.yml | 48 ++++++++++++++++++ .github/workflows/issue-owner.yml | 59 +++++++++++++++++++++++ .github/workflows/pr-review-lifecycle.yml | 31 ++++++++++++ 6 files changed, 188 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/task.yml create mode 100644 .github/pull_request_template.md create mode 100644 .github/workflows/convention-check.yml create mode 100644 .github/workflows/issue-owner.yml create mode 100644 .github/workflows/pr-review-lifecycle.yml diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..8005e32 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,2 @@ +blank_issues_enabled: false +contact_links: [] diff --git a/.github/ISSUE_TEMPLATE/task.yml b/.github/ISSUE_TEMPLATE/task.yml new file mode 100644 index 0000000..d4242ee --- /dev/null +++ b/.github/ISSUE_TEMPLATE/task.yml @@ -0,0 +1,37 @@ +name: 작업 이슈 +description: Framework의 작업을 등록해요. +title: "" +labels: ["작업"] +body: + - type: markdown + attributes: + value: | + 제목에는 작업 내용을 한국어로 적어 주세요. 또한 생성 후 5시간 안에 이슈 번호를 포함한 브랜치를 만들어 주세요. + - type: dropdown + id: type + attributes: + label: 작업 유형 + description: 브랜치와 PR에도 같은 유형을 사용해 주세요. + options: [feat, fix, refactor, hotfix, docs, style, remove, delete, test, chore, comment] + validations: + required: true + - type: dropdown + id: assignee + attributes: + label: 담당자 + options: [chaeyn, itzjb, Finefinee, hjbin-25, daehyeong2, jdw09] + validations: + required: true + - type: input + id: deadline + attributes: + label: 마감일 + placeholder: "2026-08-05 18:00" + validations: + required: true + - type: textarea + id: goal + attributes: + label: 작업 목표 + validations: + required: true diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..073468f --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,11 @@ +## 변경 내용 + + + +## 확인 방법 + + + +## 해결한 이슈 + +Closes # \ No newline at end of file diff --git a/.github/workflows/convention-check.yml b/.github/workflows/convention-check.yml new file mode 100644 index 0000000..a56913c --- /dev/null +++ b/.github/workflows/convention-check.yml @@ -0,0 +1,48 @@ +name: 컨벤션 검사 + +on: + pull_request: + types: [opened, edited, synchronize, reopened] + +permissions: + contents: read + pull-requests: read + +jobs: + validate: + name: 컨벤션 검사 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + - name: PR 제목·브랜치·커밋 컨벤션 검사 + uses: actions/github-script@v7 + env: + ALLOWED_TYPES: feat|fix|refactor|hotfix|docs|style|remove|delete|test|chore|comment + with: + script: | + const types = process.env.ALLOWED_TYPES; + const branch = context.payload.pull_request.head.ref; + const title = context.payload.pull_request.title; + const commits = await github.paginate(github.rest.pulls.listCommits, { + owner: context.repo.owner, repo: context.repo.repo, + pull_number: context.issue.number, per_page: 100 + }); + const errors = []; + const branchMatch = branch.match(new RegExp(`^(${types})/[a-z0-9]+(?:-[a-z0-9]+)*/#(\\d+)$`)); + const titleMatch = title.match(new RegExp(`^(${types}):\\s+[a-z0-9]+(?:-[a-z0-9]+)*/#(\\d+)$`)); + if (!branchMatch) errors.push('브랜치명: //#'); + if (!titleMatch) errors.push('PR 제목: : /#'); + if (branchMatch && titleMatch && branchMatch[2] !== titleMatch[2]) errors.push('브랜치와 PR 제목의 이슈 번호가 서로 다릅니다.'); + if (branchMatch) { + try { + await github.rest.issues.get({ owner: context.repo.owner, repo: context.repo.repo, issue_number: Number(branchMatch[2]) }); + } catch { + errors.push(`연결 이슈 #${branchMatch[2]}를 찾을 수 없습니다.`); + } + } + for (const commit of commits) { + if (!(new RegExp(`^(${types}):\\s+.*[가-힣].*$`)).test(commit.commit.message.split('\\n')[0])) errors.push(`커밋 제목: ${commit.sha.slice(0, 7)}`); + } + if (errors.length) core.setFailed(`컨벤션 위반\\n- ${errors.join('\\n- ')}`); diff --git a/.github/workflows/issue-owner.yml b/.github/workflows/issue-owner.yml new file mode 100644 index 0000000..9ef4f15 --- /dev/null +++ b/.github/workflows/issue-owner.yml @@ -0,0 +1,59 @@ +name: 이슈 담당자·마감일 확인 + +on: + issues: + types: [opened] + +permissions: + issues: write + +jobs: + assign-owner: + name: 이슈 담당자·마감일 확인 + runs-on: ubuntu-latest + steps: + - name: 담당자 자동 지정 및 마감일 검증 + uses: actions/github-script@v7 + with: + script: | + const body = context.payload.issue.body ?? ''; + const field = (label) => { + const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const match = body.match(new RegExp(`### ${escaped}\\s*\\n+([^\\n]+)`, 'i')); + return match?.[1]?.trim() ?? ''; + }; + const type = field('작업 유형'); + const assignee = field('담당자'); + const deadline = field('마감일'); + const types = new Set(['feat', 'fix', 'refactor', 'hotfix', 'docs', 'style', 'remove', 'delete', 'test', 'chore', 'comment']); + const allowed = new Set(['chaeyn', 'itzjb', 'Finefinee', 'hjbin-25', 'daehyeong2', 'jdw09']); + const deadlineFormat = /^20\\d{2}-(0[1-9]|1[0-2])-([0-2]\\d|3[01]) ([01]\\d|2[0-3]):[0-5]\\d$/; + const problems = []; + if (!types.has(type)) problems.push('작업 유형을 목록에서 선택해 주세요.'); + if (!allowed.has(assignee)) problems.push('담당자를 목록에서 선택해 주세요.'); + if (!deadlineFormat.test(deadline)) problems.push('마감일은 `YYYY-MM-DD HH:mm` 형식으로 입력해 주세요.'); + if (problems.length) { + await github.rest.issues.createComment({ + owner: context.repo.owner, repo: context.repo.repo, + issue_number: context.issue.number, + body: `이슈 자동화 확인 실패\\n\\n${problems.map((item) => `- ${item}`).join('\\n')}` + }); + core.setFailed(problems.join(' ')); + return; + } + await github.rest.issues.addAssignees({ + owner: context.repo.owner, repo: context.repo.repo, + issue_number: context.issue.number, assignees: [assignee] + }); + await github.rest.issues.addLabels({ + owner: context.repo.owner, repo: context.repo.repo, + issue_number: context.issue.number, labels: ['작업'] + }); + const cleanTitle = context.payload.issue.title + .replace(/^\\[(?:feat|fix|refactor|hotfix|docs|style|remove|delete|test|chore|comment)\\]\\s*/i, '') + .trim(); + await github.rest.issues.update({ + owner: context.repo.owner, repo: context.repo.repo, + issue_number: context.issue.number, + title: `[${type}] ${cleanTitle}` + }); diff --git a/.github/workflows/pr-review-lifecycle.yml b/.github/workflows/pr-review-lifecycle.yml new file mode 100644 index 0000000..34dca14 --- /dev/null +++ b/.github/workflows/pr-review-lifecycle.yml @@ -0,0 +1,31 @@ +name: PR 리뷰 흐름 + +on: + pull_request: + types: [ready_for_review] + +permissions: + pull-requests: write + +jobs: + request-team-review: + runs-on: ubuntu-latest + env: + REVIEW_TEAM: ${{ vars.REVIEW_TEAM }} + steps: + - name: 담당 팀에 리뷰 요청 + uses: actions/github-script@v7 + with: + script: | + const team = process.env.REVIEW_TEAM; + if (!team) { + core.notice('REVIEW_TEAM이 설정되지 않아 자동 리뷰 요청을 건너뛰어요.'); + return; + } + await github.rest.pulls.requestReviewers({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.payload.pull_request.number, + team_reviewers: [team] + }); + core.notice(`${team} 에 리뷰를 요청했어요.`);