Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions .github/workflows/dev-merge-close-issues.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
name: Close issues on dev merge

on:
pull_request:
types: [closed]

permissions:
contents: read
issues: write
pull-requests: read
repository-projects: write

jobs:
close-linked-issues:
if: github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev'
runs-on: ubuntu-latest
env:
GH_TOKEN: ${{ secrets.DECODED_GITHUB_TOKEN || github.token }}
REPO: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_URL: ${{ github.event.pull_request.html_url }}
PROJECT_ID: PVT_kwDOCf1dEc4BUHn-
PROJECT_NUMBER: "3"
STATUS_FIELD_ID: PVTSSF_lADOCf1dEc4BUHn-zhBR8Fk
STATUS_DONE_OPTION_ID: "98236657"
steps:
- name: Extract closing issue references
id: refs
shell: bash
run: |
set -euo pipefail
body=$(jq -r '.pull_request.body // ""' "$GITHUB_EVENT_PATH")
issues=$(
printf '%s\n' "$body" |
perl -0777 -ne 'while (/\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+(?:https:\/\/github\.com\/decodedcorp\/decoded\/issues\/)?#?([0-9]+)/ig) { print "$1\n" }' |
sort -n -u
)

if [ -z "$issues" ]; then
echo "No closing issue references found in PR body."
echo "issues=" >> "$GITHUB_OUTPUT"
exit 0
fi

{
echo "issues<<EOF"
echo "$issues"
echo "EOF"
} >> "$GITHUB_OUTPUT"

- name: Close issues and mark project Done
if: steps.refs.outputs.issues != ''
shell: bash
run: |
set -euo pipefail
owner="${REPO%%/*}"
repo="${REPO#*/}"

while IFS= read -r issue; do
[ -z "$issue" ] && continue

state=$(gh issue view "$issue" --repo "$REPO" --json state --jq '.state')
if [ "$state" = "OPEN" ]; then
gh issue close "$issue" \
--repo "$REPO" \
--comment "Closed automatically because PR #${PR_NUMBER} was merged into \`dev\`: ${PR_URL}"
else
echo "Issue #${issue} is already ${state}; skipping close."
fi

item_id=$(
gh api graphql \
-f query='query($owner:String!, $repo:String!, $number:Int!) { repository(owner:$owner, name:$repo) { issue(number:$number) { projectItems(first:20) { nodes { id project { ... on ProjectV2 { number } } } } } } }' \
-f owner="$owner" \
-f repo="$repo" \
-F number="$issue" \
--jq ".data.repository.issue.projectItems.nodes[] | select(.project.number == ${PROJECT_NUMBER}) | .id" |
head -n 1
) || true

if [ -z "$item_id" ]; then
echo "Issue #${issue} is not in project #${PROJECT_NUMBER}; skipping project status update."
continue
fi

if ! gh api graphql \
-f query='mutation($project:ID!, $item:ID!, $field:ID!, $option:String!) { updateProjectV2ItemFieldValue(input:{projectId:$project, itemId:$item, fieldId:$field, value:{singleSelectOptionId:$option}}) { projectV2Item { id } } }' \
-f project="$PROJECT_ID" \
-f item="$item_id" \
-f field="$STATUS_FIELD_ID" \
-f option="$STATUS_DONE_OPTION_ID" >/dev/null; then
echo "::warning::Issue #${issue} was closed, but project status update failed."
continue
fi

echo "Issue #${issue} closed and project status set to Done."
done <<< "${{ steps.refs.outputs.issues }}"
79 changes: 79 additions & 0 deletions .github/workflows/project-pr-status.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
name: Track PR status in project

on:
pull_request:
types: [opened, reopened, ready_for_review, converted_to_draft, closed]

permissions:
contents: read
pull-requests: read
repository-projects: write

jobs:
update-pr-status:
runs-on: ubuntu-latest
env:
GH_TOKEN: ${{ secrets.DECODED_GITHUB_TOKEN || github.token }}
REPO: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
PROJECT_ID: PVT_kwDOCf1dEc4BUHn-
PROJECT_NUMBER: "3"
STATUS_FIELD_ID: PVTSSF_lADOCf1dEc4BUHn-zhBR8Fk
STATUS_IN_PROGRESS_OPTION_ID: "47fc9ee4"
STATUS_DONE_OPTION_ID: "98236657"
steps:
- name: Set PR project status
shell: bash
run: |
set -euo pipefail
owner="${REPO%%/*}"
repo="${REPO#*/}"

if [ "${{ github.event.action }}" = "closed" ]; then
status_option="$STATUS_DONE_OPTION_ID"
status_name="Done"
else
status_option="$STATUS_IN_PROGRESS_OPTION_ID"
status_name="In Progress"
fi

pr_data=$(
gh api graphql \
-f query='query($owner:String!, $repo:String!, $number:Int!) { repository(owner:$owner, name:$repo) { pullRequest(number:$number) { id projectItems(first:20) { nodes { id project { ... on ProjectV2 { number } } } } } } }' \
-f owner="$owner" \
-f repo="$repo" \
-F number="$PR_NUMBER"
)

pr_id=$(jq -r '.data.repository.pullRequest.id' <<<"$pr_data")
item_id=$(
jq -r ".data.repository.pullRequest.projectItems.nodes[] | select(.project.number == ${PROJECT_NUMBER}) | .id" <<<"$pr_data" |
head -n 1
)

if [ -z "$item_id" ]; then
item_id=$(
gh api graphql \
-f query='mutation($project:ID!, $content:ID!) { addProjectV2ItemById(input:{projectId:$project, contentId:$content}) { item { id } } }' \
-f project="$PROJECT_ID" \
-f content="$pr_id" \
--jq '.data.addProjectV2ItemById.item.id'
) || true
fi

if [ -z "$item_id" ]; then
echo "::warning::PR #${PR_NUMBER} project item was not found or created; skipping status update."
exit 0
fi

if ! gh api graphql \
-f query='mutation($project:ID!, $item:ID!, $field:ID!, $option:String!) { updateProjectV2ItemFieldValue(input:{projectId:$project, itemId:$item, fieldId:$field, value:{singleSelectOptionId:$option}}) { projectV2Item { id } } }' \
-f project="$PROJECT_ID" \
-f item="$item_id" \
-f field="$STATUS_FIELD_ID" \
-f option="$status_option" >/dev/null; then
echo "::warning::PR #${PR_NUMBER} project status update failed."
exit 0
fi

echo "PR #${PR_NUMBER} project status set to ${status_name}."
25 changes: 23 additions & 2 deletions docs/GIT-WORKFLOW.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,10 +124,12 @@ hotfix/* ──PR──▶ main (긴급 시에만)
1. `dev`에서 작업 브랜치 생성: `git checkout -b feat/<issue#>-xxx dev`
2. **즉시 Draft PR 생성** — 프로젝트 보드가 자동으로 **In Progress** 전환
3. 작업 완료 → Draft 해제 → 리뷰 요청
4. 리뷰 통과 후 `dev`에 머지 → 프로젝트 보드 자동 **Done** + 이슈 close
4. 리뷰 통과 후 `dev`에 머지 → `.github/workflows/dev-merge-close-issues.yml` 이 연결 이슈 close + 프로젝트 보드 **Done** 처리
5. 릴리스 준비 시 `dev` → `main` PR 생성
6. CI 체크 통과 + 리뷰 후 `main`에 머지 → Vercel 자동 배포

완료 기준은 `dev` 머지다. `main`은 GitHub default branch라 `Closes #N` native auto-close 대상이지만, 팀 운영에서는 `dev` 머지 이후 `main` 반영이 필수 흐름이므로 feature/bug/docs 이슈는 `dev` 머지 시 닫는다.

## 프로젝트 보드 자동 추적

[decoded-monorepo 프로젝트 #3](https://github.com/orgs/decodedcorp/projects/3)의 활성 자동화:
Expand All @@ -136,13 +138,32 @@ hotfix/* ──PR──▶ main (긴급 시에만)
|--------|------|
| 신규 이슈/PR 생성 | Todo로 자동 추가 |
| **PR이 이슈에 링크됨** (`Closes #N`) | **In Progress** |
| PR 머지 | Done + 이슈 자동 close |
| PR opened/reopened/ready_for_review | `.github/workflows/project-pr-status.yml` 이 PR item을 **In Progress** 보정 |
| PR closed | `.github/workflows/project-pr-status.yml` 이 PR item을 **Done** 보정 |
| `dev` 대상 PR 머지 | `.github/workflows/dev-merge-close-issues.yml` 이 `Closes #N` 연결 이슈 close + Done 보정 |

### 중요

- **브랜치 생성만으로는 전환 안 됨** — Draft PR 필요
- 브랜치 이름에 이슈 번호 포함 권장: `feat/27-follow-api`
- 리뷰 전이라도 Draft PR을 먼저 올려 진행 가시화
- `decoded`의 default branch는 `main`이므로 GitHub native `Closes #N`만으로는 `dev` 머지 시 이슈가 자동 close되지 않는다. `dev` 머지 close는 `.github/workflows/dev-merge-close-issues.yml` 이 담당한다.
- Project v2 상태 보정까지 동작하려면 repository secret `DECODED_GITHUB_TOKEN`이 필요하다. 이 토큰은 `decoded` issue/PR 읽기, issue close, org Project #3 item/status 쓰기 권한을 가져야 한다. secret이 없으면 workflow는 `GITHUB_TOKEN`으로 시도하지만 org Project 업데이트는 실패할 수 있다.
- GitHub Actions workflow 활성화 기준은 default branch(`main`) 반영이다. 이 변경은 팀 흐름대로 `dev`에 먼저 머지하되, 자동화가 실제 기준선으로 안정 동작하는 시점은 `dev` 이후 `main`까지 반영된 뒤다.
- 자동화 도입 이전에 `dev`로 머지된 PR은 수동 백필 대상이다. `Closes/Fixes/Resolves #N`가 있는 merged PR을 감사해 열린 이슈가 남아 있으면 PR 번호를 남기고 close한다.

### Dev merge close 감사

정기적으로 다음 조건을 확인한다.

1. 대상: `base=dev`, `state=merged` PR
2. PR 본문에 `Closes #N`, `Fixes #N`, `Resolves #N`가 있음
3. 연결 이슈가 아직 `OPEN`
4. 실제 완료 기준이 `dev` 머지라면 이슈에 머지 PR 번호를 코멘트하고 close

`refs #N`는 단순 참조이므로 close 대상이 아니다.

2026-05-21 백필 결과: 자동화 도입 전 누락된 15개 이슈를 닫았고, 재검사에서 closing reference가 남긴 열린 이슈는 0건이다. `refs #518`는 문서 마이그레이션 장기 트래킹이라 열린 상태로 유지한다.

### 숏컷: `scripts/start-issue.sh`

Expand Down
Loading