Skip to content

Commit 44c9367

Browse files
committed
Fix project sync ci
1 parent 922d443 commit 44c9367

File tree

1 file changed

+60
-61
lines changed

1 file changed

+60
-61
lines changed

.github/workflows/project-sync.yml

Lines changed: 60 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)