강조색 계산함수 제작 #185
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Issue → Notion Sync | |
| on: | |
| issues: | |
| types: [opened, labeled, unlabeled, milestoned, demilestoned, closed, reopened] | |
| pull_request: | |
| types: [closed] | |
| jobs: | |
| sync-to-notion: | |
| runs-on: ubuntu-latest | |
| env: | |
| GH_TOKEN: ${{ secrets.GH_TOKEN }} | |
| NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }} | |
| NOTION_DATABASE_ID: ${{ secrets.NOTION_DATABASE_ID }} | |
| steps: | |
| - name: Sync GitHub Issue to Notion | |
| uses: actions/github-script@v6 | |
| with: | |
| github-token: ${{ secrets.GH_TOKEN }} | |
| script: | | |
| const notionHeaders = { | |
| "Authorization": `Bearer ${process.env.NOTION_TOKEN}`, | |
| "Notion-Version": "2022-06-28", | |
| "Content-Type": "application/json" | |
| }; | |
| const issue = context.payload.issue; | |
| const action = context.payload.action; | |
| const createdDate = new Date(issue.created_at).toISOString().split('T')[0]; | |
| const targetMatch = issue.body?.match(/⏰ Target 날짜[\s\S]*?\n+(\d{4}-\d{2}-\d{2})/); | |
| const targetDate = targetMatch ? targetMatch[1] : createdDate; | |
| const milestone = issue.milestone ? issue.milestone.title : null; | |
| const findNotionPage = async () => { | |
| const res = await fetch("https://api.notion.com/v1/databases/" + process.env.NOTION_DATABASE_ID + "/query", { | |
| method: "POST", | |
| headers: notionHeaders, | |
| body: JSON.stringify({ | |
| filter: { | |
| property: "Issue Number", | |
| rich_text: { equals: `#${issue.number}` } | |
| } | |
| }) | |
| }); | |
| const data = await res.json(); | |
| return data.results.length > 0 ? data.results[0].id : null; | |
| }; | |
| if (action === "opened") { | |
| const existingPageId = await findNotionPage(); | |
| if (existingPageId) { | |
| console.log(`⚠️ 이미 등록된 이슈입니다: #${issue.number}`); | |
| return; | |
| } | |
| const notionBody = { | |
| parent: { database_id: process.env.NOTION_DATABASE_ID }, | |
| properties: { | |
| Issue: { | |
| title: [{ text: { content: issue.title } }] | |
| }, | |
| "Issue Number": { | |
| rich_text: [{ text: { content: `#${issue.number}` } }] | |
| }, | |
| Date: { | |
| date: { start: createdDate, end: targetDate } | |
| }, | |
| Status: { | |
| select: { name: "Todo" } | |
| }, | |
| URL: { | |
| url: issue.html_url | |
| } | |
| } | |
| }; | |
| if (milestone) { | |
| notionBody.properties.Milestone = { | |
| rich_text: [{ text: { content: milestone } }] | |
| }; | |
| } | |
| const res = await fetch("https://api.notion.com/v1/pages", { | |
| method: "POST", | |
| headers: notionHeaders, | |
| body: JSON.stringify(notionBody) | |
| }); | |
| if (!res.ok) { | |
| const error = await res.text(); | |
| throw new Error(`❌ Notion 등록 실패: ${res.status} ${error}`); | |
| } | |
| console.log(`✅ Notion에 등록 완료: ${issue.title}`); | |
| } | |
| if (action === "labeled" || action === "unlabeled") { | |
| const label = context.payload.label.name; | |
| const allowed = ["Todo", "In Progress", "Done"]; | |
| if (!allowed.includes(label)) return; | |
| const pageId = await findNotionPage(); | |
| if (!pageId) return; | |
| const res = await fetch("https://api.notion.com/v1/pages/" + pageId, { | |
| method: "PATCH", | |
| headers: notionHeaders, | |
| body: JSON.stringify({ | |
| properties: { | |
| Status: { select: { name: label } } | |
| } | |
| }) | |
| }); | |
| if (!res.ok) { | |
| const error = await res.text(); | |
| throw new Error(`❌ Status 업데이트 실패: ${res.status} ${error}`); | |
| } | |
| console.log(`✅ Status 동기화 완료: ${label}`); | |
| } | |
| if (action === "milestoned" || action === "demilestoned") { | |
| const milestoneName = issue.milestone ? issue.milestone.title : null; | |
| const pageId = await findNotionPage(); | |
| if (!pageId) return; | |
| const res = await fetch("https://api.notion.com/v1/pages/" + pageId, { | |
| method: "PATCH", | |
| headers: notionHeaders, | |
| body: JSON.stringify({ | |
| properties: { | |
| Milestone: milestoneName | |
| ? { rich_text: [{ text: { content: milestoneName } }] } | |
| : { rich_text: [] } | |
| } | |
| }) | |
| }); | |
| if (!res.ok) { | |
| const error = await res.text(); | |
| throw new Error(`❌ Milestone 업데이트 실패: ${res.status} ${error}`); | |
| } | |
| console.log(`✅ Milestone 업데이트 완료: ${milestoneName ?? "제거됨"}`); | |
| } | |
| if (action === "closed") { | |
| const pageId = await findNotionPage(); | |
| if (!pageId) return; | |
| const res = await fetch("https://api.notion.com/v1/pages/" + pageId, { | |
| method: "PATCH", | |
| headers: notionHeaders, | |
| body: JSON.stringify({ | |
| properties: { | |
| Status: { select: { name: "Done" } } | |
| } | |
| }) | |
| }); | |
| if (!res.ok) { | |
| const error = await res.text(); | |
| throw new Error(`❌ 수동 종료 반영 실패: ${res.status} ${error}`); | |
| } | |
| console.log(`✅ 이슈 종료 → Status: Done`); | |
| } | |
| if (action === "reopened") { | |
| const pageId = await findNotionPage(); | |
| if (!pageId) return; | |
| const res = await fetch("https://api.notion.com/v1/pages/" + pageId, { | |
| method: "PATCH", | |
| headers: notionHeaders, | |
| body: JSON.stringify({ | |
| properties: { | |
| Status: { select: { name: "Todo" } } | |
| } | |
| }) | |
| }); | |
| if (!res.ok) { | |
| const error = await res.text(); | |
| throw new Error(`❌ reopen 반영 실패: ${res.status} ${error}`); | |
| } | |
| console.log(`✅ reopen → Status: Todo 반영 완료`); | |
| } | |
| if (context.eventName === "pull_request" && context.payload.action === "closed") { | |
| const pr = context.payload.pull_request; | |
| const prBody = pr.body || ""; | |
| const prUrl = pr.html_url; | |
| const issueMatches = prBody.match(/(?:Fixes|Closes|Resolves):? #\d+/gi); | |
| if (!issueMatches) return; | |
| for (const match of issueMatches) { | |
| const issueNumber = match.match(/\d+/)[0]; | |
| const issueRes = await fetch(`https://api.github.com/repos/${context.repo.owner}/${context.repo.repo}/issues/${issueNumber}`, { | |
| headers: { Authorization: `Bearer ${process.env.GH_TOKEN}` } | |
| }); | |
| const issueData = await issueRes.json(); | |
| const notionSearch = await fetch("https://api.notion.com/v1/databases/" + process.env.NOTION_DATABASE_ID + "/query", { | |
| method: "POST", | |
| headers: notionHeaders, | |
| body: JSON.stringify({ | |
| filter: { | |
| property: "Issue Number", | |
| rich_text: { equals: `#${issueNumber}` } | |
| } | |
| }) | |
| }); | |
| const notionResult = await notionSearch.json(); | |
| if (notionResult.results.length === 0) continue; | |
| const pageId = notionResult.results[0].id; | |
| const notionPage = await fetch(`https://api.notion.com/v1/pages/${pageId}`, { | |
| method: "GET", | |
| headers: notionHeaders | |
| }); | |
| const notionData = await notionPage.json(); | |
| const existingPRText = notionData.properties?.PR_URL?.rich_text?.map(rt => rt.plain_text).join('\n') || ""; | |
| const updatedText = existingPRText | |
| ? `${existingPRText}\n${prUrl}` | |
| : prUrl; | |
| const updateRes = await fetch("https://api.notion.com/v1/pages/" + pageId, { | |
| method: "PATCH", | |
| headers: notionHeaders, | |
| body: JSON.stringify({ | |
| properties: { | |
| PR_URL: { | |
| rich_text: updatedText.split('\n').map(url => ({ text: { content: url } })) | |
| }, | |
| Status: { | |
| select: { name: "Done" } | |
| } | |
| } | |
| }) | |
| }); | |
| if (!updateRes.ok) { | |
| const errorText = await updateRes.text(); | |
| throw new Error(`❌ PR URL 저장 실패: ${updateRes.status} ${errorText}`); | |
| } else { | |
| console.log(`✅ PR URL 저장 완료: ${prUrl}`); | |
| } | |
| } | |
| } |