cursor AI와 함께 코드를 작성하는 연습을 하다보니, 알잘딱깔센으로 commit, push를 해주지 않아서 내가 신경써서 해달라고 요청하지 않는 이상 commit, push를 작은 단위 별로 꼼꼼하게 남겨놓는게 쉽지 않았다.
그래서 생각한게, 요즘 cursor ai + mcp 결합이 유행이던데, mcp를 조사해보면 자동화 구현에 도움이 되지 않을까 싶어서 시작하게 된 여정이다.
내가 원하는 기능은 딱 2가지였다.
1-1. 비슷한 원리로, issue에서 특정 이슈 deleted 됐을때, Todo에서도 삭제하기 (closed는 삭제 X. PR해서 closed된 이슈마저 삭제하면 안되기 때문)
📌 추가로, 원하는 기능이지만 github actions가 필요한게 아닌, cursor prompt에 명령하는 것 만으로도 구현이 가능했던 기능
👉🏻 특정 명령어 입력하면 PR 알아서 올려주기!
(gpt에게 질문했던 프롬프트)
커서와 기능 구현할때 커밋, 푸쉬는 내가 명령해서 수동으로 할거야. 그리고 PR template도 내가 알아서 .github에 넣어둘거야.
근데 내가 PR이 하고싶어지면 커서에게 '@pr closes #20, #30' 이런식으로 이슈번호가 포함된 특정 명령어를 입력하고, 커서가 알아서 해당 이슈들, 코드를 검토해서 PR template에 내용을 알아서 정리해서 PR올리도록 만들고싶어.
📌 추가 2. 내가 원하는 기능이었지만 기본 workflow에 있는 기능이라서 굳이 action을 만들 필요가 없었던 기능
👉🏻 merge 되는 순간 projects의 in progress에 있던 해당 이슈들은 DONE으로 이동
- 전제 조건
- default branch로의 merge일 것
- pr discription 내부에 'closes #이슈번호' 적혀 있을 것
위의 3가지 기능을 알아보니 github actions + github api만으로도 구현이 가능한 기능이었지만, 구현하는 과정에서 mcp도 결합할 수 있다 하여 gpt, cursor ai와 함께하는 github 자동화 여정을 시작한다.
✅ “Issue가 생성되면 GitHub Projects의 ‘To Do’ 열로 자동 이동”
이걸 GitHub Actions + GitHub API + MCP 주석을 함께 써서 직접 구현해보자!
(projects v2 (beta)라는 가정 하에)
1) project_id
(또는 project_number)
2) column_name 혹은 status name (예: "To Do")
3) 인증용 GITHUB_TOKEN (자동으로 제공됨)
(GITHUB_TOKEN ❌ secrets.PROJECT_ACCESS_TOKEN ✅ 로 문제 개선 됨)
✅ 1단계: GraphQL Explorer 접속
✅ 2단계: 쿼리문 복사해서 붙여넣기
query {
user(login: "innes-k") {
projectV2(number: 4) {
id
title
fields(first: 20) {
nodes {
... on ProjectV2Field {
id
name
}
... on ProjectV2SingleSelectField {
id
name
options {
id
name
}
}
}
}
}
}
}
✅ 3단계 : 실행하여 아래 3가지 추출
projectV2.id
"Status" 필드의 id
"Todo" 옵션의 id
📌 폴더 관리 계획
- github actions 관련 : .github/workflows/[파일명.yml]
- cursor, mcp 등 자동화 관련 : root경로/dev-helper/[파일명]
❌ 1차 시도
-> 개선 필요 : query문 및 GITHUB_TOKEN을 PAT(Project Access Token)로 개선
name: Add Issue to Project Todo
on:
issues:
types: [opened]
jobs:
add-to-project:
runs-on: ubuntu-latest
steps:
- name: Add issue to project
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const query = `
mutation {
addProjectV2ItemById(input: {
projectId: "PVT_kwHOCNlLx84A3hwL",
contentId: "${{ github.event.issue.node_id }}"
}) {
item {
id
}
}
}
`;
const result = await github.graphql(query);
const itemId = result.addProjectV2ItemById.item.id;
const setStatus = `
mutation {
updateProjectV2ItemFieldValue(input: {
projectId: "PVT_kwHOCNlLx84A3hwL",
itemId: "${itemId}",
fieldId: "PVTSSF_lAHOCNlLx84A3hwLzgspl6M",
value: {
singleSelectOptionId: "f75ad846"
}
}) {
projectV2Item {
id
}
}
}
`;
await github.graphql(setStatus);
✅ 정상 동작
name: Add Issue to Project Todo
on:
issues:
types: [opened]
permissions:
issues: write
contents: read
jobs:
add-to-project:
runs-on: ubuntu-latest
steps:
- name: Add issue to project and set status to Todo
uses: actions/github-script@v7
with:
github-token: ${{ secrets.PROJECT_ACCESS_TOKEN }}
script: |
// 1. 이슈를 프로젝트에 추가
const addToProjectQuery = `
mutation {
addProjectV2ItemById(input: {
projectId: "PVT_kwHOCNlLx84A3hwL",
contentId: "${{ github.event.issue.node_id }}"
}) {
item {
id
}
}
}
`;
const addResult = await github.graphql(addToProjectQuery);
const itemId = addResult.addProjectV2ItemById.item.id;
// 2. 항목의 상태를 Todo로 설정
const setStatusQuery = `
mutation {
updateProjectV2ItemFieldValue(input: {
projectId: "PVT_kwHOCNlLx84A3hwL",
itemId: "${itemId}",
fieldId: "PVTSSF_lAHOCNlLx84A3hwLzgspl6M",
value: {
singleSelectOptionId: "f75ad846"
}
}) {
projectV2Item {
id
}
}
}
`;
await github.graphql(setStatusQuery);
// 3. 이슈 제목에 이슈 번호 추가
const issueNumber = context.issue.number;
const issueTitle = context.payload.issue.title;
if (!issueTitle.includes(`#${issueNumber}`)) {
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
title: `#${issueNumber} ${issueTitle}`
});
}
변경사항이 있다면 push까지 마쳐야 .github를 인식해서 action workflow가 정상적으로 업로드된다.
"Resource not accessible by integration"
오류는 GitHub Action이 GitHub Project에 접근할 수 있는 권한이 부족해서 발생하는 문제
Read and write permissions
로 변경해주기이후 한번 더 push해야 적용됨!
git commit --allow-empty -m "trigger workflow again after permission fix"
git push origin main
github-token: ${{ secrets.GITHUB_TOKEN }}
github-token: ${{ secrets.PROJECT_ACCESS_TOKEN }}
📝 PROJECT_ACCESS_TOKEN 생성 및 등록 방법
특정 저장소의 GitHub Actions가 제대로 작동하려면 Project 접근 권한이 있는 Personal Access Token(PAT)이 필요합니다.
repo
(전체 선택)admin:org
> write:org
선택 (Project 접근에 필요)project
(전체 선택)이제 GitHub Actions 워크플로우에서 ${{ secrets.PROJECT_ACCESS_TOKEN }}
으로 해당 토큰에 접근할 수 있습니다.
새로운 action을 만들거야.
일단 너가 PR을 알아서 올릴 수 있도록 PR방식을 먼저 설명해줄게.
내가 "@pr closes #20 #30" 형식으로 명령어를 작성하면, 지금까지 작성한 코드와 내가 #으로 남겨준 이슈번호를 토대로 검토 후, .github > pull_request_template 파일의 PR template 양식을 바탕으로 현재 브랜치의 작업 내용을 PR로 작성 후, dev 브랜치로 PR 업로드해줘.그리고 PR이 github에 업로드되는 순간, PR closes 뒤에 적어둔 이슈 번호들이 적힌 projects Todo들을 in progress 단계로 자동으로 이동시키는 actions을 만들어줘.
📌 여기에 추가해야 할 내용
(이 프롬프트 이후에 개선한 내용)
PR 제목을 브랜치 명이 아닌, 명령어에 포함되어 있는 이슈들의 제목들과 코드로 구현한 내용을 검토한 후, 모든 내용을 포괄할 수 있는 말로 PR 제목을 선정해줘.
PR template 안에 관련 이슈번호 적을때 closes #1 #2 #3 이 형식이 아니라,
closes #1
closes #2
closes #3
이렇게 적도록 해줘.
name: Move Issues to In Progress when PR Created
on:
pull_request:
types: [opened, reopened]
permissions:
pull-requests: read
contents: read
jobs:
move-issues-to-in-progress:
runs-on: ubuntu-latest
steps:
- name: Move related issues to In Progress
uses: actions/github-script@v7
with:
github-token: ${{ secrets.PROJECT_ACCESS_TOKEN }}
script: |
// PR 내용 가져오기
const prNumber = context.payload.pull_request.number;
const prBody = context.payload.pull_request.body;
// PR 설명에서 "closes #숫자" 패턴 찾기 (여러 이슈 번호 지원)
// 1. 이슈 키워드(closes, fixes 등) 뒤에 오는 첫 번째 이슈 번호 찾기
const closesPattern = /(?:close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved)\s+#(\d+)/gi;
const issueNumbers = [];
let match;
while ((match = closesPattern.exec(prBody)) !== null) {
issueNumbers.push(parseInt(match[1]));
}
// 2. PR 설명에서 모든 #숫자 형식 찾기
const hashNumberPattern = /#(\d+)/g;
const allMatches = [...prBody.matchAll(hashNumberPattern)];
// 이미 찾은 이슈 번호 외의 새로운 이슈 번호만 추가
for (const match of allMatches) {
const num = parseInt(match[1]);
if (!issueNumbers.includes(num)) {
issueNumbers.push(num);
}
}
console.log(`Found issue numbers: ${issueNumbers.join(', ')}`);
if (issueNumbers.length === 0) {
console.log('No related issues found in PR description');
return;
}
// 1. 프로젝트 아이템 조회
const findItemsQuery = `
query {
user(login: "${{ github.repository_owner }}") {
projectV2(number: 4) {
items(first: 100) {
nodes {
id
content {
... on Issue {
id
number
}
}
fieldValues(first: 10) {
nodes {
... on ProjectV2ItemFieldSingleSelectValue {
name
field {
... on ProjectV2SingleSelectField {
id
name
}
}
}
}
}
}
}
}
}
}
`;
const result = await github.graphql(findItemsQuery);
const projectItems = result.user.projectV2.items.nodes;
// 2. 각 이슈에 대해 처리
for (const issueNumber of issueNumbers) {
// 2.1 프로젝트에서 해당 이슈를 찾기
const projectItem = projectItems.find(item =>
item.content &&
item.content.number === issueNumber
);
if (!projectItem) {
console.log(`Issue #${issueNumber} not found in project`);
continue;
}
// 2.2 현재 상태가 Todo인 경우에만 In Progress로 변경
const statusField = projectItem.fieldValues.nodes.find(
node => node.field && node.field.name === 'Status'
);
if (!statusField || statusField.name !== 'Todo') {
console.log(`Issue #${issueNumber} is not in Todo status`);
continue;
}
// 2.3 상태를 In Progress로 변경
const updateStatusQuery = `
mutation {
updateProjectV2ItemFieldValue(input: {
projectId: "PVT_kwHOCNlLx84A3hwL",
itemId: "${projectItem.id}",
fieldId: "PVTSSF_lAHOCNlLx84A3hwLzgspl6M",
value: {
singleSelectOptionId: "47fc9ee4"
}
}) {
projectV2Item {
id
}
}
}
`;
await github.graphql(updateStatusQuery);
console.log(`Successfully moved issue #${issueNumber} to In Progress`);
}
1) GitHub CLI 설치
# macOS (Homebrew 사용)
brew install gh
# Windows (Scoop 사용)
scoop install gh
# Linux (Debian/Ubuntu)
sudo apt install gh
2) GitHub CLI에 로그인
gh auth login
주의 : default branch 를 dev로 바꿔두어야 dev에 PR 올림과 동시에 해당 이슈들이 닫힘.
이렇게 적어야 PR merge시 이슈들이 전부 제대로 닫힌다.