Update README.md #2
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
| # =================================================================== | ||
| # GitHub Projects Status β Issue Label μλ°©ν₯ λκΈ°ν μν¬νλ‘μ° | ||
| # =================================================================== | ||
| # | ||
| # μ΄ μν¬νλ‘μ°λ GitHub Projectsμ Status λ³κ²½μ Issue Labelλ‘ λκΈ°νν©λλ€. | ||
| # | ||
| # μλ λ°©μ: | ||
| # 1. Projectsμμ Itemμ Statusκ° λ³κ²½λλ©΄ νΈλ¦¬κ±° | ||
| # 2. GraphQL APIλ‘ λ³κ²½λ Status κ° μ‘°ν | ||
| # 3. ν΄λΉ Issueμ κΈ°μ‘΄ Status Label μ κ±° | ||
| # 4. μλ‘μ΄ Status Label μΆκ° | ||
| # | ||
| # μ§μ κΈ°λ₯: | ||
| # - Projects Status λ³κ²½ β Issue Label μλ λκΈ°ν | ||
| # β’ μμ μ β μμ μ | ||
| # β’ μμ μ€ β μμ μ€ | ||
| # β’ νμΈ λκΈ° β νμΈ λκΈ° | ||
| # β’ νΌλλ°± β νΌλλ°± | ||
| # β’ μμ μλ£ β μμ μλ£ | ||
| # β’ μ·¨μ β μ·¨μ | ||
| # - PRμ μ²λ¦¬νμ§ μμ (Issueλ§ μ²λ¦¬) | ||
| # - 무ν 루ν λ°©μ§ λ‘μ§ ν¬ν¨ | ||
| # | ||
| # νμ μ€μ : | ||
| # - Organization Secret: _GITHUB_PAT_TOKEN | ||
| # β’ νμ κΆν: repo (μ 체), project (read:project, write:project) | ||
| # β’ Classic PAT νμ (Fine-grained tokenμ GraphQL API λ―Έμ§μ) | ||
| # | ||
| # νκ²½λ³μ μ€μ : | ||
| # - PROJECT_NUMBER: GitHub Projects λ²νΈ (νμ) | ||
| # - STATUS_FIELD: Projectsμ Status νλλͺ (κΈ°λ³Έκ°: "Status") | ||
| # - STATUS_LABELS: λκΈ°νν Status Label λͺ©λ‘ (JSON λ°°μ΄) | ||
| # | ||
| # κ΄λ ¨ μν¬νλ‘μ°: | ||
| # - PROJECT-COMMON-PROJECTS-SYNC-MANAGER.yaml: Label β Status λκΈ°ν (μ λ°©ν₯) | ||
| # - μ΄ μν¬νλ‘μ°: Status β Label λκΈ°ν (μλ°©ν₯) | ||
| # | ||
| # =================================================================== | ||
| name: PROJECT-REVERSE-SYNC-MANAGER | ||
| on: | ||
| projects_v2_item: | ||
| types: [edited] | ||
| # =================================================================== | ||
| # μ€μ λ³μ | ||
| # =================================================================== | ||
| env: | ||
| PROJECT_NUMBER: 6 | ||
| STATUS_FIELD: Status | ||
| STATUS_LABELS: '["μμ μ ","μμ μ€","νμΈ λκΈ°","νΌλλ°±","μμ μλ£","μ·¨μ"]' | ||
| permissions: | ||
| issues: write | ||
| contents: read | ||
| jobs: | ||
| # =================================================================== | ||
| # Job 1: Projects Status λ³κ²½ μ Issue Label λκΈ°ν | ||
| # =================================================================== | ||
| sync-status-to-label: | ||
| name: Projects Statusλ₯Ό Issue Labelλ‘ λκΈ°ν | ||
| # λ΄μ΄ νΈλ¦¬κ±°ν κ²½μ° λ¬΄ν 루ν λ°©μ§ | ||
| if: github.actor != 'github-actions[bot]' | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Project Item μ 보 μ‘°ν λ° Label λκΈ°ν | ||
| uses: actions/github-script@v7 | ||
| env: | ||
| PROJECT_NUMBER: ${{ env.PROJECT_NUMBER }} | ||
| STATUS_FIELD: ${{ env.STATUS_FIELD }} | ||
| STATUS_LABELS: ${{ env.STATUS_LABELS }} | ||
| with: | ||
| github-token: ${{ secrets._GITHUB_PAT_TOKEN }} | ||
| script: | | ||
| console.log('ββββββββββββββββββββββββββββββββββ'); | ||
| console.log('π Projects Status β Issue Label λκΈ°ν μμ'); | ||
| console.log('ββββββββββββββββββββββββββββββββββ'); | ||
| const itemNodeId = context.payload.projects_v2_item?.node_id; | ||
| if (!itemNodeId) { | ||
| console.log('β οΈ Project item node_idλ₯Ό μ°Ύμ μ μμ΅λλ€.'); | ||
| console.log(' νμ΄λ‘λ:', JSON.stringify(context.payload, null, 2)); | ||
| return; | ||
| } | ||
| console.log(`π Project Item Node ID: ${itemNodeId}`); | ||
| try { | ||
| // ===== 1. Project Item μμΈ μ 보 μ‘°ν ===== | ||
| const itemQuery = ` | ||
| query($itemId: ID!, $statusField: String!) { | ||
| node(id: $itemId) { | ||
| ... on ProjectV2Item { | ||
| content { | ||
| ... on Issue { | ||
| number | ||
| title | ||
| repository { | ||
| name | ||
| owner { | ||
| login | ||
| } | ||
| } | ||
| labels(first: 20) { | ||
| nodes { | ||
| name | ||
| } | ||
| } | ||
| } | ||
| ... on PullRequest { | ||
| number | ||
| title | ||
| } | ||
| } | ||
| fieldValueByName(name: $statusField) { | ||
| ... on ProjectV2ItemFieldSingleSelectValue { | ||
| name | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| `; | ||
| const itemData = await github.graphql(itemQuery, { | ||
| itemId: itemNodeId, | ||
| statusField: process.env.STATUS_FIELD | ||
| }); | ||
| console.log('π μ‘°νλ λ°μ΄ν°:', JSON.stringify(itemData, null, 2)); | ||
| const content = itemData.node?.content; | ||
| if (!content) { | ||
| console.log('β οΈ Project itemμ contentλ₯Ό μ°Ύμ μ μμ΅λλ€.'); | ||
| return; | ||
| } | ||
| // PRμΈ κ²½μ° μ€ν΅ | ||
| if (!content.repository) { | ||
| console.log('ββββββββββββββββββββββββββββββββββ'); | ||
| console.log('βΉοΈ Pull Requestλ μ²λ¦¬νμ§ μμ΅λλ€.'); | ||
| console.log('ββββββββββββββββββββββββββββββββββ'); | ||
| return; | ||
| } | ||
| const issueNumber = content.number; | ||
| const repoName = content.repository.name; | ||
| const repoOwner = content.repository.owner.login; | ||
| const currentLabels = content.labels.nodes.map(l => l.name); | ||
| const newStatus = itemData.node?.fieldValueByName?.name; | ||
| console.log(`π Issue: ${repoOwner}/${repoName}#${issueNumber}`); | ||
| console.log(`π νμ¬ Labels: ${currentLabels.join(', ') || '(μμ)'}`); | ||
| console.log(`π μ Status: "${newStatus}"`); | ||
| if (!newStatus) { | ||
| console.log('β οΈ Status κ°μ μ°Ύμ μ μμ΅λλ€.'); | ||
| return; | ||
| } | ||
| // ===== 2. Status Labels νμ± ===== | ||
| let statusLabels; | ||
| try { | ||
| statusLabels = JSON.parse(process.env.STATUS_LABELS); | ||
| } catch (e) { | ||
| console.error('β STATUS_LABELS νμ± μ€ν¨:', e.message); | ||
| statusLabels = ['μμ μ ', 'μμ μ€', 'νμΈ λκΈ°', 'νΌλλ°±', 'μμ μλ£', 'μ·¨μ']; | ||
| } | ||
| console.log(`π κ΄λ¦¬ λμ Status Labels: ${statusLabels.join(', ')}`); | ||
| // ===== 3. μ Statusκ° κ΄λ¦¬ λμμΈμ§ νμΈ ===== | ||
| if (!statusLabels.includes(newStatus)) { | ||
| console.log(`β οΈ "${newStatus}"λ κ΄λ¦¬ λμ Status Labelμ΄ μλλλ€.`); | ||
| console.log(' Label λκΈ°νλ₯Ό 건λλλλ€.'); | ||
| return; | ||
| } | ||
| // ===== 4. μ΄λ―Έ λμΌν Labelμ΄ μλμ§ νμΈ (무ν 루ν λ°©μ§) ===== | ||
| if (currentLabels.includes(newStatus)) { | ||
| console.log('ββββββββββββββββββββββββββββββββββ'); | ||
| console.log(`β μ΄λ―Έ "${newStatus}" Labelμ΄ μ‘΄μ¬ν©λλ€.`); | ||
| console.log(' λκΈ°νκ° νμνμ§ μμ΅λλ€.'); | ||
| console.log('ββββββββββββββββββββββββββββββββββ'); | ||
| return; | ||
| } | ||
| // ===== 5. κΈ°μ‘΄ Status Label μ κ±° ===== | ||
| const labelsToRemove = currentLabels.filter(label => | ||
| statusLabels.includes(label) && label !== newStatus | ||
| ); | ||
| console.log(`ποΈ μ κ±°ν Labels: ${labelsToRemove.join(', ') || '(μμ)'}`); | ||
| for (const label of labelsToRemove) { | ||
| try { | ||
| await github.rest.issues.removeLabel({ | ||
| owner: repoOwner, | ||
| repo: repoName, | ||
| issue_number: issueNumber, | ||
| name: label | ||
| }); | ||
| console.log(` β "${label}" Label μ κ±°λ¨`); | ||
| } catch (e) { | ||
| // Labelμ΄ μ΄λ―Έ μλ κ²½μ° λ¬΄μ | ||
| if (e.status !== 404) { | ||
| console.warn(` β οΈ "${label}" Label μ κ±° μ€ν¨:`, e.message); | ||
| } | ||
| } | ||
| } | ||
| // ===== 6. μ Status Label μΆκ° ===== | ||
| console.log(`β μΆκ°ν Label: "${newStatus}"`); | ||
| try { | ||
| await github.rest.issues.addLabels({ | ||
| owner: repoOwner, | ||
| repo: repoName, | ||
| issue_number: issueNumber, | ||
| labels: [newStatus] | ||
| }); | ||
| console.log(` β "${newStatus}" Label μΆκ°λ¨`); | ||
| } catch (e) { | ||
| console.error(` β "${newStatus}" Label μΆκ° μ€ν¨:`, e.message); | ||
| throw e; | ||
| } | ||
| console.log('ββββββββββββββββββββββββββββββββββ'); | ||
| console.log('π Label λκΈ°ν μλ£!'); | ||
| console.log(` β’ Issue: ${repoOwner}/${repoName}#${issueNumber}`); | ||
| console.log(` β’ Status: "${newStatus}"`); | ||
| console.log('ββββββββββββββββββββββββββββββββββ'); | ||
| } catch (error) { | ||
| console.error('ββββββββββββββββββββββββββββββββββ'); | ||
| console.error('β Label λκΈ°ν μ€ν¨'); | ||
| console.error('ββββββββββββββββββββββββββββββββββ'); | ||
| const errorMessage = error.message || JSON.stringify(error); | ||
| const errorStatus = error.status || (error.response && error.response.status); | ||
| if (errorStatus === 401 || errorMessage.includes('401') || errorMessage.includes('Unauthorized')) { | ||
| console.error('π μΈμ¦ μ€λ₯ (401 Unauthorized)'); | ||
| console.error(''); | ||
| console.error('μμΈ:'); | ||
| console.error(' 1. _GITHUB_PAT_TOKEN Secretμ΄ μ€μ λμ§ μμμ΅λλ€.'); | ||
| console.error(' 2. PAT ν ν°μ΄ λ§λ£λμμ΅λλ€.'); | ||
| console.error(' 3. PAT ν ν°μ νμν κΆν(repo, project)μ΄ μμ΅λλ€.'); | ||
| console.error(''); | ||
| console.error('ν΄κ²° λ°©λ²:'); | ||
| console.error(' 1. GitHub Settings > Developer settings > Personal access tokens'); | ||
| console.error(' 2. Classic PAT μμ± (Fine-grained tokenμ GraphQL λ―Έμ§μ)'); | ||
| console.error(' 3. κΆν λΆμ¬: repo (μ 체), project (read:project, write:project)'); | ||
| console.error(' 4. Organization Secretsμ _GITHUB_PAT_TOKENμΌλ‘ λ±λ‘'); | ||
| } else if (errorStatus === 404 || errorMessage.includes('404') || errorMessage.includes('Not Found')) { | ||
| console.error('π 리μμ€λ₯Ό μ°Ύμ μ μμ΅λλ€ (404 Not Found)'); | ||
| console.error(''); | ||
| console.error('μμΈ:'); | ||
| console.error(' 1. Issueκ° μ‘΄μ¬νμ§ μμ΅λλ€.'); | ||
| console.error(' 2. Labelμ΄ λ¦¬ν¬μ§ν 리μ μ μλμ΄ μμ§ μμ΅λλ€.'); | ||
| console.error(' 3. PAT ν ν°μ ν΄λΉ 리ν¬μ§ν 리 μ κ·Ό κΆνμ΄ μμ΅λλ€.'); | ||
| console.error(''); | ||
| console.error('ν΄κ²° λ°©λ²:'); | ||
| console.error(' 1. Issue μ‘΄μ¬ μ¬λΆ νμΈ'); | ||
| console.error(' 2. 리ν¬μ§ν 리 Labels μ€μ μμ Status Label μ‘΄μ¬ μ¬λΆ νμΈ'); | ||
| console.error(' 3. PAT ν ν° κΆν νμΈ'); | ||
| } else if (errorMessage.includes('rate limit') || errorMessage.includes('429')) { | ||
| console.error('β±οΈ API Rate Limit μ΄κ³Ό (429 Too Many Requests)'); | ||
| console.error(''); | ||
| console.error('ν΄κ²° λ°©λ²:'); | ||
| console.error(' μ μ ν λ€μ μλνμΈμ.'); | ||
| } else { | ||
| console.error('β μ μ μλ μ€λ₯'); | ||
| } | ||
| console.error(''); | ||
| console.error('μ 체 μλ¬ λ©μμ§:'); | ||
| console.error(errorMessage); | ||
| if (error.stack) { | ||
| console.error(''); | ||
| console.error('Stack Trace:'); | ||
| console.error(error.stack); | ||
| } | ||
| console.error('ββββββββββββββββββββββββββββββββββ'); | ||
| throw error; | ||
| } | ||