@@ -4,13 +4,24 @@ name: Smart Bidirectional Sync
44' on ' :
55 issues :
66 types : [labeled, closed, reopened]
7- projects_v2_item :
8- types : [edited]
7+ workflow_dispatch :
8+ inputs :
9+ direction :
10+ description : ' Sync direction (project-to-issue requires PROJECTS_TOKEN secret)'
11+ required : true
12+ type : choice
13+ options :
14+ - project-to-issue
15+ - issue-to-project
16+ issue_number :
17+ description : ' Issue number to sync'
18+ required : true
19+ type : string
920
1021# Prevent sync loops with debouncing
1122concurrency :
12- group : smart-sync-${{ github.event.issue.number || github.event.projects_v2_item.node_id }}
13- cancel-in-progress : true # Cancel pending runs (debouncing effect)
23+ group : smart-sync-${{ github.event.issue.number || github.event.inputs.issue_number || github.run_id }}
24+ cancel-in-progress : true
1425
1526jobs :
1627 determine-direction :
1930 permissions :
2031 contents : read
2132 issues : read
22- id-token : write
2333
2434 outputs :
2535 should_sync : ${{ steps.check.outputs.should_sync }}
@@ -45,29 +55,32 @@ jobs:
4555 # Check which event triggered this workflow
4656 if [ "${{ github.event_name }}" = "issues" ]; then
4757 # Issue event → sync to project board
48- echo "direction=issue-to-project" >> $GITHUB_OUTPUT
49- echo "issue_number=${{ github.event.issue.number }}" >> $GITHUB_OUTPUT
58+ echo "direction=issue-to-project" >> " $GITHUB_OUTPUT"
59+ echo "issue_number=${{ github.event.issue.number }}" >> " $GITHUB_OUTPUT"
5060
5161 # Only sync on status label changes or state changes
52- if [[ "${{ github.event.action }}" == "labeled" && "${{ github.event.label.name }}" == status:* ]] || \
53- [ "${{ github.event.action }}" = "closed" ] || \
54- [ "${{ github.event.action }}" = "reopened" ]; then
55- echo "should_sync=true" >> $GITHUB_OUTPUT
56- echo "✅ Will sync: Issue #${{ github.event.issue.number }} → Project Board"
62+ ACTION="${{ github.event.action }}"
63+ LABEL="${{ github.event.label.name }}"
64+ if { [ "$ACTION" = "labeled" ] && echo "$LABEL" | grep -q "^status:"; } || \
65+ [ "$ACTION" = "closed" ] || \
66+ [ "$ACTION" = "reopened" ]; then
67+ echo "should_sync=true" >> "$GITHUB_OUTPUT"
68+ echo "Will sync: Issue #${{ github.event.issue.number }} to Project Board"
5769 else
58- echo "should_sync=false" >> $GITHUB_OUTPUT
59- echo "⏭️ Skipping: Not a status change or state change"
70+ echo "should_sync=false" >> " $GITHUB_OUTPUT"
71+ echo "Skipping: Not a status change or state change"
6072 fi
6173
62- elif [ "${{ github.event_name }}" = "projects_v2_item" ]; then
63- # Project event → sync to issue
64- echo "direction=project-to-issue" >> $GITHUB_OUTPUT
65- echo "should_sync=true" >> $GITHUB_OUTPUT
66- echo "✅ Will sync: Project Board → Issue"
74+ elif [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
75+ # Manual trigger
76+ echo "direction=${{ github.event.inputs.direction }}" >> "$GITHUB_OUTPUT"
77+ echo "issue_number=${{ github.event.inputs.issue_number }}" >> "$GITHUB_OUTPUT"
78+ echo "should_sync=true" >> "$GITHUB_OUTPUT"
79+ echo "Will sync: Manual trigger (${{ github.event.inputs.direction }})"
6780
6881 else
69- echo "should_sync=false" >> $GITHUB_OUTPUT
70- echo "⚠️ Unknown event type"
82+ echo "should_sync=false" >> " $GITHUB_OUTPUT"
83+ echo "Unknown event type"
7184 fi
7285
7386 rate-limit-check :
@@ -136,7 +149,6 @@ jobs:
136149 permissions :
137150 contents : read
138151 issues : read
139- id-token : write
140152
141153 steps :
142154 - name : Checkout repository
@@ -147,11 +159,12 @@ jobs:
147159 - name : Sync Issue to Project Board
148160 env :
149161 GH_TOKEN : ${{ secrets.PROJECTS_TOKEN }}
162+ ISSUE_TITLE : ${{ github.event.issue.title }}
150163 run : |
151- echo "# Issue → Project Board Sync"
152- echo "** Issue** : #${{ github.event.issue.number }} \"${{ github.event.issue.title }}\" "
153- echo "** State** : ${{ github.event.issue.state }}"
154- echo "** Action** : ${{ github.event.action }}"
164+ echo "# Issue to Project Board Sync"
165+ echo "Issue: #${{ github.event.issue.number }} $ISSUE_TITLE "
166+ echo "State: ${{ github.event.issue.state }}"
167+ echo "Action: ${{ github.event.action }}"
155168
156169 # Step 1: Check if in Project
157170 PROJECT_ITEM=$(gh api graphql -f query='
@@ -277,7 +290,6 @@ jobs:
277290 permissions :
278291 contents : read
279292 issues : write
280- id-token : write
281293
282294 steps :
283295 - name : Checkout repository
@@ -288,53 +300,35 @@ jobs:
288300 - name : Sync Project Board to Issue
289301 env :
290302 GH_TOKEN : ${{ secrets.PROJECTS_TOKEN }}
303+ ISSUE_NUMBER : ${{ needs.determine-direction.outputs.issue_number }}
291304 run : |
292- echo "# Project Board → Issue Sync"
293- echo "**Project Item**: ${{ github.event.projects_v2_item.node_id }}"
294- echo "**Content**: ${{ github.event.projects_v2_item.content_node_id }}"
295- echo "**Changed By**: @${{ github.event.sender.login }}"
296-
297- # Step 1: Get Issue Number
298- CONTENT_ID="${{ github.event.projects_v2_item.content_node_id }}"
299-
300- ISSUE_DATA=$(gh api graphql -f query='
301- query {
302- node(id: "${{ github.event.projects_v2_item.node_id }}") {
303- ... on ProjectV2Item {
304- content {
305- ... on Issue {
306- number
307- url
308- state
309- title
310- }
311- }
312- }
313- }
314- }
315- ')
316-
317- ISSUE_NUMBER=$(echo "$ISSUE_DATA" | jq -r '.data.node.content.number')
305+ echo "# Project Board to Issue Sync"
306+ echo "Issue: #$ISSUE_NUMBER"
307+ echo "Triggered by: @${{ github.event.sender.login }}"
318308
319309 if [ -z "$ISSUE_NUMBER" ] || [ "$ISSUE_NUMBER" = "null" ]; then
320- echo "⏭️ Not an issue (might be PR or other content) "
321- exit 0
310+ echo "No issue number provided "
311+ exit 1
322312 fi
323313
324- echo "Issue Number: $ISSUE_NUMBER"
325-
326- # Step 2: Get Project Status
327- STATUS=$(gh api graphql -f query='
314+ # Get project item for this issue
315+ PROJECT_ITEM=$(gh api graphql -f query='
328316 query {
329- node(id : "${{ github.event.projects_v2_item.node_id }} ") {
330- ... on ProjectV2Item {
331- fieldValues (first: 20 ) {
317+ repository(owner : "borghei", name: "Claude-Skills ") {
318+ issue(number: '"$ISSUE_NUMBER"') {
319+ projectItems (first: 10 ) {
332320 nodes {
333- ... on ProjectV2ItemFieldSingleSelectValue {
334- name
335- field {
336- ... on ProjectV2SingleSelectField {
321+ id
322+ project { number }
323+ fieldValues(first: 20) {
324+ nodes {
325+ ... on ProjectV2ItemFieldSingleSelectValue {
337326 name
327+ field {
328+ ... on ProjectV2SingleSelectField {
329+ name
330+ }
331+ }
338332 }
339333 }
340334 }
@@ -343,16 +337,25 @@ jobs:
343337 }
344338 }
345339 }
346- ' --jq '.data.node.fieldValues.nodes[] | select(.field.name == "Status") | .name')
340+ ')
341+
342+ # Extract status from project 9
343+ STATUS=$(echo "$PROJECT_ITEM" | jq -r '
344+ .data.repository.issue.projectItems.nodes[]
345+ | select(.project.number == 9)
346+ | .fieldValues.nodes[]
347+ | select(.field.name == "Status")
348+ | .name
349+ ')
347350
348351 if [ -z "$STATUS" ]; then
349- echo "⏭️ No status field found"
352+ echo "No status field found in project board "
350353 exit 0
351354 fi
352355
353356 echo "Project Status: $STATUS"
354357
355- # Step 3: Map Status to Label
358+ # Map status to label
356359 case "$STATUS" in
357360 "To triage") NEW_LABEL="status: triage" ;;
358361 "Backlog") NEW_LABEL="status: backlog" ;;
@@ -361,36 +364,36 @@ jobs:
361364 "In Review") NEW_LABEL="status: in-review" ;;
362365 "Done") NEW_LABEL="status: done" ;;
363366 *)
364- echo "⏭️ Unknown status: $STATUS"
367+ echo "Unknown status: $STATUS"
365368 exit 0
366369 ;;
367370 esac
368371
369372 echo "Target Label: $NEW_LABEL"
370373
371- # Step 4: Update Issue Labels
372- CURRENT_LABELS=$(gh issue view $ISSUE_NUMBER --json labels --jq '[.labels[].name] | join(",")')
374+ # Update issue labels
375+ CURRENT_LABELS=$(gh issue view "$ISSUE_NUMBER" --json labels \
376+ --jq '[.labels[].name] | join(",")')
373377
374- # Remove all status: labels
375- for label in "status: triage" "status: backlog" "status: ready" "status: in-progress" "status: in-review" "status: done"; do
378+ for label in " status: triage" "status: backlog" "status: ready" \
379+ "status: in-progress" "status: in-review" "status: done"; do
376380 if echo "$CURRENT_LABELS" | grep -q "$label"; then
377- gh issue edit $ISSUE_NUMBER --remove-label "$label" 2>/dev/null || true
381+ gh issue edit " $ISSUE_NUMBER" --remove-label "$label" 2>/dev/null || true
378382 fi
379383 done
380384
381- # Add new status label
382- gh issue edit $ISSUE_NUMBER --add-label "$NEW_LABEL"
383- echo "✅ Label updated to: $NEW_LABEL"
385+ gh issue edit "$ISSUE_NUMBER" --add-label "$NEW_LABEL"
386+ echo "Label updated to: $NEW_LABEL"
384387
385- # Step 5: Handle Issue State
386- CURRENT_STATE=$(gh issue view $ISSUE_NUMBER --json state --jq '.state')
388+ # Handle issue state
389+ CURRENT_STATE=$(gh issue view " $ISSUE_NUMBER" --json state --jq '.state')
387390
388391 if [ "$STATUS" = "Done" ] && [ "$CURRENT_STATE" = "OPEN" ]; then
389- gh issue close $ISSUE_NUMBER --reason completed
390- echo "✅ Issue closed (moved to Done)"
392+ gh issue close " $ISSUE_NUMBER" --reason completed
393+ echo "Issue closed (moved to Done)"
391394 elif [ "$STATUS" != "Done" ] && [ "$CURRENT_STATE" = "CLOSED" ]; then
392- gh issue reopen $ISSUE_NUMBER
393- echo "✅ Issue reopened (moved from Done)"
395+ gh issue reopen " $ISSUE_NUMBER"
396+ echo "Issue reopened (moved from Done)"
394397 fi
395398
396- echo "✅ Sync complete: Issue #$ISSUE_NUMBER updated to $STATUS"
399+ echo "Sync complete: Issue #$ISSUE_NUMBER updated to $STATUS"
0 commit comments