Skip to content

Update README.md

Update README.md #2

# ===================================================================

Check failure on line 1 in .github/workflows/PROJECT-REVERSE-SYNC-MANAGER.yaml

View workflow run for this annotation

GitHub Actions / .github/workflows/PROJECT-REVERSE-SYNC-MANAGER.yaml

Invalid workflow file

(Line: 43, Col: 3): Unexpected value 'projects_v2_item'
# 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;
}