@@ -85,67 +85,66 @@ jobs:
8585 state=$(gh api repos/${{ github.repository }}/issues/$issue --jq '.state')
8686 # Find item id in project
8787 itemId=$(echo "$resp" | jq -r --arg n "$issue" '.data.user.projectV2.items.nodes[] | select(.content.number==($n|tonumber)) | .id')
88- if [ -z "$itemId" ]; then
89- # Attempt to add
90- addMutation='mutation($project:ID!,$issue:ID!){ addProjectV2ItemById(input:{projectId:$project contentId:$issue}){ item { id } } }'
91- issueNode=$(gh api graphql -f query='query($owner:String!,$repo:String!,$num:Int!){ repository(owner:$owner,name:$repo){ issue(number:$num){ id } } }' -F owner='${{ github.repository_owner }}' -F repo='${{ github.event.repository.name }}' -F num=$issue --jq '.data.repository.issue.id')
92- gh api graphql -f query="$addMutation" -f project=$projectId -f issue=$issueNode >/dev/null || true
93- # Refresh project items for new item
94- resp=$(gh api graphql -f query="$projectQuery" -F login="$LOGIN" -F number=$PROJECT_NUM)
95- itemId=$(echo "$resp" | jq -r --arg n "$issue" '.data.user.projectV2.items.nodes[] | select(.content.number==($n|tonumber)) | .id')
96- fi
97- if [ "$state" = "closed" ]; then
98- # Set Done
99- updateMutation='mutation($project:ID!,$item:ID!,$field:ID!,$option: String!){ updateProjectV2ItemFieldValue(input:{projectId:$project,itemId:$item,fieldId:$field,value:{singleSelectOptionId:$option}}){ projectV2Item { id } } }'
100- gh api graphql -f query="$updateMutation" -f project=$projectId -f item=$itemId -f field=$statusFieldId -f option=$optDone >/dev/null || true
101- else
102- # Mark In Progress when there is at least one commit referencing it
103- updateMutation='mutation($project:ID!,$item:ID!,$field:ID!,$option: String!){ updateProjectV2ItemFieldValue(input:{projectId:$project,itemId:$item,fieldId:$field,value:{singleSelectOptionId:$option}}){ projectV2Item { id } } }'
104- gh api graphql -f query="$updateMutation" -f project=$projectId -f item=$itemId -f field=$statusFieldId -f option=$optInProg >/dev/null || true
105- fi
106- done
107-
108- sync-from-pr :
109- name : Sync referenced issues from PR
110- if : github.event_name == 'pull_request'
111- runs-on : ubuntu-latest
112- steps :
113- - name : Gather referenced issues
114- id : refs
115- run : |
116- body="${{ github.event.pull_request.body }}\n${{ github.event.pull_request.title }}"
117- issues=$(echo "$body" | grep -Eo '#[0-9]+' | tr -d '#' | sort -u || true)
118- echo "issues=$issues" >> $GITHUB_OUTPUT
119- - name : Install gh + jq
120- if : steps.refs.outputs.issues != ''
121- run : |
122- sudo apt-get update && sudo apt-get install -y jq
123- curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg
124- sudo chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg
125- echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null
126- sudo apt-get update && sudo apt-get install gh -y
127- - name : Update project status -> In Progress
128- if : steps.refs.outputs.issues != ''
129- env :
130- ISSUES : ${{ steps.refs.outputs.issues }}
131- run : |
132- gh auth status || gh auth login --with-token <<<"${GITHUB_TOKEN}"
133- LOGIN=${{ env.GITHUB_USER }}
134- PROJECT_NUM=${{ env.PROJECT_NUMBER }}
135- projectQuery='query($login:String!,$number:Int!){ user(login:$login){ projectV2(number:$number){ id fields(first:50){ nodes { __typename ... on ProjectV2SingleSelectField { id name options { id name } } } } items(first:200){ nodes { id content { __typename ... on Issue { number id state } } } } } } }'
136- resp=$(gh api graphql -f query="$projectQuery" -F login="$LOGIN" -F number=$PROJECT_NUM)
137- projectId=$(echo "$resp" | jq -r '.data.user.projectV2.id')
138- statusFieldId=$(echo "$resp" | jq -r '.data.user.projectV2.fields.nodes[] | select(.name=="Status") | .id')
139- optInProg=$(echo "$resp" | jq -r '.data.user.projectV2.fields.nodes[] | select(.name=="Status") | .options[] | select(.name=="'"${STATUS_INPROGRESS_NAME}"'") | .id')
140- updateMutation='mutation($project:ID!,$item:ID!,$field:ID!,$option: String!){ updateProjectV2ItemFieldValue(input:{projectId:$project,itemId:$item,fieldId:$field,value:{singleSelectOptionId:$option}}){ projectV2Item { id } } }'
141- for issue in $ISSUES; do
142- itemId=$(echo "$resp" | jq -r --arg n "$issue" '.data.user.projectV2.items.nodes[] | select(.content.number==($n|tonumber)) | .id')
143- if [ -z "$itemId" ]; then
144- # Add issue first
145- issueNode=$(gh api graphql -f query='query($owner:String!,$repo:String!,$num:Int!){ repository(owner:$owner,name:$repo){ issue(number:$num){ id } } }' -F owner='${{ github.repository_owner }}' -F repo='${{ github.event.repository.name }}' -F num=$issue --jq '.data.repository.issue.id')
146- addMutation='mutation($project:ID!,$issue:ID!){ addProjectV2ItemById(input:{projectId:$project contentId:$issue}){ item { id } } }'
147- gh api graphql -f query="$addMutation" -f project=$projectId -f issue=$issueNode >/dev/null || true
148- # Refresh
88+ steps:
89+ - name: Sync issues via github-script
90+ uses: actions/github-script@v7
91+ with:
92+ script: |
93+ const login = process.env.GITHUB_USER;
94+ const projectNumber = parseInt(process.env.PROJECT_NUMBER, 10);
95+ const statusTodo = process.env.STATUS_TODO_NAME;
96+ const statusInProg = process.env.STATUS_INPROGRESS_NAME;
97+ const statusDone = process.env.STATUS_DONE_NAME;
98+ const commits = context.payload.commits || [];
99+ const issueSet = new Set();
100+ for (const c of commits) {
101+ const matches = (c.message.match(/#[0-9]+/g) || []);
102+ for (const m of matches) issueSet.add(parseInt(m.substring(1),10));
103+ }
104+ if (issueSet.size === 0) { core.info('No referenced issues in commit messages.'); return; }
105+ core.info(`Referenced issues: ${[...issueSet].join(', ')}`);
106+ const qProject = `query($login:String!,$number:Int!){ user(login:$login){ projectV2(number:$number){ id fields(first:50){ nodes { ... on ProjectV2SingleSelectField { id name options { id name } } } } items(first:200){ nodes { id content { ... on Issue { id number state } } } } } } }`;
107+ const projectResp = await github.graphql(qProject, { login, number: projectNumber });
108+ const project = projectResp.user.projectV2; if(!project) { core.setFailed('Project not found'); return; }
109+ const projectId = project.id;
110+ // Find Status field
111+ const statusField = project.fields.nodes.find(f=>f && f.name==='Status');
112+ if(!statusField) { core.setFailed('Status field not found'); return; }
113+ const optMap = {}; if(statusField.options){ for(const o of statusField.options){ optMap[o.name]=o.id; } }
114+ const ensureIssueNodeId = async (issueNumber)=>{
115+ const qIssue = `query($owner:String!,$repo:String!,$num:Int!){ repository(owner:$owner,name:$repo){ issue(number:$num){ id number state } } }`;
116+ const ir = await github.graphql(qIssue,{ owner: context.repo.owner, repo: context.repo.repo, num: issueNumber });
117+ return ir.repository.issue;
118+ };
119+ const existingItems = new Map();
120+ for(const item of project.items.nodes){ if(item.content && item.content.number) existingItems.set(item.content.number, item); }
121+ const mAdd = `mutation($project:ID!,$content:ID!){ addProjectV2ItemById(input:{projectId:$project contentId:$content}){ item { id } } }`;
122+ const mUpdate = `mutation($project:ID!,$item:ID!,$field:ID!,$option:String!){ updateProjectV2ItemFieldValue(input:{projectId:$project,itemId:$item,fieldId:$field,value:{singleSelectOptionId:$option}}){ projectV2Item { id } } }`;
123+ for(const issueNumber of issueSet){
124+ let item = existingItems.get(issueNumber);
125+ let issueNode = null;
126+ if(!item){
127+ issueNode = await ensureIssueNodeId(issueNumber);
128+ if(!issueNode){ core.warning(`Issue #${issueNumber} not found`); continue; }
129+ try { await github.graphql(mAdd,{ project: projectId, content: issueNode.id }); core.info(`Added issue #${issueNumber} to project`); } catch(e){ core.warning(`Add failed for #${issueNumber}: ${e.message}`); }
130+ } else {
131+ // Retrieve issue state
132+ issueNode = { id: item.content.id, number: item.content.number, state: item.content.state };
133+ }
134+ if(!issueNode){ issueNode = await ensureIssueNodeId(issueNumber); }
135+ const desired = issueNode.state === 'CLOSED' ? statusDone : statusInProg;
136+ if(!optMap[desired]) { core.warning(`Desired status ${desired} option missing`); continue; }
137+ // Need item id; refresh to capture newly added items when necessary
138+ if(!item){
139+ const refresh = await github.graphql(qProject,{ login, number: projectNumber });
140+ for(const it of refresh.user.projectV2.items.nodes){ if(it.content && it.content.number) existingItems.set(it.content.number, it); }
141+ item = existingItems.get(issueNumber);
142+ }
143+ if(item){
144+ try { await github.graphql(mUpdate,{ project: projectId, item: item.id, field: statusField.id, option: optMap[desired] }); core.info(`Set #${issueNumber} -> ${desired}`); } catch(e){ core.warning(`Status update failed for #${issueNumber}: ${e.message}`); }
145+ }
146+ }
147+ core.info('Sync complete.');
149148 resp=$(gh api graphql -f query="$projectQuery" -F login="$LOGIN" -F number=$PROJECT_NUM)
150149 itemId=$(echo "$resp" | jq -r --arg n "$issue" '.data.user.projectV2.items.nodes[] | select(.content.number==($n|tonumber)) | .id')
151150 fi
0 commit comments