Update project number in workflow configuration #2
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Project Sync | ||
| on: | ||
| push: | ||
| branches: [ main, '**' ] | ||
| pull_request: | ||
| types: [opened, reopened, synchronize] | ||
| issues: | ||
| types: [opened] | ||
| permissions: | ||
| contents: read | ||
| issues: write | ||
| pull-requests: read | ||
| projects: write | ||
| env: | ||
| # Set these to match your environment. | ||
| GITHUB_USER: pynip | ||
| # Replace with the numeric project number for "Virtualization Studio ARM macOS" | ||
| PROJECT_NUMBER: 1 # <-- UPDATE this after creating the project | ||
| # Status names expected in the project's single-select Status field | ||
| STATUS_TODO_NAME: "To do" | ||
| STATUS_INPROGRESS_NAME: "In Progress" | ||
| STATUS_DONE_NAME: "Done" | ||
| jobs: | ||
| add-new-issues: | ||
| name: Add newly opened issues to project (To do) | ||
| if: github.event_name == 'issues' | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Add to project | ||
| uses: actions/[email protected] | ||
| with: | ||
| project-url: https://github.com/users/${{ env.GITHUB_USER }}/projects/${{ env.PROJECT_NUMBER }} | ||
| github-token: ${{ secrets.GITHUB_TOKEN }} | ||
| sync-from-push: | ||
| name: Sync referenced issues from push commits | ||
| if: github.event_name == 'push' | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Extract issue numbers from commit messages | ||
| id: extract | ||
| run: | | ||
| issues=$(echo "${{ toJson(github.event.commits) }}" | jq -r '.[].message' | grep -Eo '#[0-9]+' | tr -d '#' | sort -u || true) | ||
| if [ -n "$issues" ]; then | ||
| echo "issues=$issues" >> $GITHUB_OUTPUT | ||
| else | ||
| echo "issues=" >> $GITHUB_OUTPUT | ||
| fi | ||
| - name: Add referenced issues to project (if any) | ||
| if: steps.extract.outputs.issues != '' | ||
| uses: actions/[email protected] | ||
| with: | ||
| project-url: https://github.com/users/${{ env.GITHUB_USER }}/projects/${{ env.PROJECT_NUMBER }} | ||
| github-token: ${{ secrets.GITHUB_TOKEN }} | ||
| content-id: ${{ steps.extract.outputs.issues }} | ||
| continue-on-error: true | ||
| - name: Prepare status update script | ||
| if: steps.extract.outputs.issues != '' | ||
| uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: '20' | ||
| - name: Update status for closed issues (Done) | ||
| if: steps.extract.outputs.issues != '' | ||
| run: | | ||
| sudo apt-get update && sudo apt-get install -y jq | ||
| curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg | ||
| sudo chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg | ||
| 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 | ||
| sudo apt-get update && sudo apt-get install gh -y | ||
| gh auth status || gh auth login --with-token <<<"${GITHUB_TOKEN}" | ||
| LOGIN=${{ env.GITHUB_USER }} | ||
| PROJECT_NUM=${{ env.PROJECT_NUMBER }} | ||
| ISSUES="${{ steps.extract.outputs.issues }}" | ||
| # Fetch project + status field metadata | ||
| 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 } } } } } } }' | ||
| resp=$(gh api graphql -f query="$projectQuery" -F login="$LOGIN" -F number=$PROJECT_NUM) | ||
| projectId=$(echo "$resp" | jq -r '.data.user.projectV2.id') | ||
| statusFieldId=$(echo "$resp" | jq -r '.data.user.projectV2.fields.nodes[] | select(.name=="Status") | .id') | ||
| optDone=$(echo "$resp" | jq -r '.data.user.projectV2.fields.nodes[] | select(.name=="Status") | .options[] | select(.name=="'"${STATUS_DONE_NAME}"'") | .id') | ||
| optInProg=$(echo "$resp" | jq -r '.data.user.projectV2.fields.nodes[] | select(.name=="Status") | .options[] | select(.name=="'"${STATUS_INPROGRESS_NAME}"'") | .id') | ||
| for issue in $ISSUES; do | ||
| state=$(gh api repos/${{ github.repository }}/issues/$issue --jq '.state') | ||
| # Find item id in project | ||
| itemId=$(echo "$resp" | jq -r --arg n "$issue" '.data.user.projectV2.items.nodes[] | select(.content.number==($n|tonumber)) | .id') | ||
| if [ -z "$itemId" ]; then | ||
| # Attempt to add | ||
| addMutation='mutation($project:ID!,$issue:ID!){ addProjectV2ItemById(input:{projectId:$project contentId:$issue}){ item { id } } }' | ||
| 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') | ||
| gh api graphql -f query="$addMutation" -f project=$projectId -f issue=$issueNode >/dev/null || true | ||
| # Refresh project items for new item | ||
| resp=$(gh api graphql -f query="$projectQuery" -F login="$LOGIN" -F number=$PROJECT_NUM) | ||
| itemId=$(echo "$resp" | jq -r --arg n "$issue" '.data.user.projectV2.items.nodes[] | select(.content.number==($n|tonumber)) | .id') | ||
| fi | ||
| if [ "$state" = "closed" ]; then | ||
| # Set Done | ||
| updateMutation='mutation($project:ID!,$item:ID!,$field:ID!,$option: String!){ updateProjectV2ItemFieldValue(input:{projectId:$project,itemId:$item,fieldId:$field,value:{singleSelectOptionId:$option}}){ projectV2Item { id } } }' | ||
| gh api graphql -f query="$updateMutation" -f project=$projectId -f item=$itemId -f field=$statusFieldId -f option=$optDone >/dev/null || true | ||
| else | ||
| # Mark In Progress when there is at least one commit referencing it | ||
| updateMutation='mutation($project:ID!,$item:ID!,$field:ID!,$option: String!){ updateProjectV2ItemFieldValue(input:{projectId:$project,itemId:$item,fieldId:$field,value:{singleSelectOptionId:$option}}){ projectV2Item { id } } }' | ||
| gh api graphql -f query="$updateMutation" -f project=$projectId -f item=$itemId -f field=$statusFieldId -f option=$optInProg >/dev/null || true | ||
| fi | ||
| done | ||
| sync-from-pr: | ||
| name: Sync referenced issues from PR | ||
| if: github.event_name == 'pull_request' | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Gather referenced issues | ||
| id: refs | ||
| run: | | ||
| body="${{ github.event.pull_request.body }}\n${{ github.event.pull_request.title }}" | ||
| issues=$(echo "$body" | grep -Eo '#[0-9]+' | tr -d '#' | sort -u || true) | ||
| echo "issues=$issues" >> $GITHUB_OUTPUT | ||
| - name: Install gh + jq | ||
| if: steps.refs.outputs.issues != '' | ||
| run: | | ||
| sudo apt-get update && sudo apt-get install -y jq | ||
| curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg | ||
| sudo chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg | ||
| 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 | ||
| sudo apt-get update && sudo apt-get install gh -y | ||
| - name: Update project status -> In Progress | ||
| if: steps.refs.outputs.issues != '' | ||
| env: | ||
| ISSUES: ${{ steps.refs.outputs.issues }} | ||
| run: | | ||
| gh auth status || gh auth login --with-token <<<"${GITHUB_TOKEN}" | ||
| LOGIN=${{ env.GITHUB_USER }} | ||
| PROJECT_NUM=${{ env.PROJECT_NUMBER }} | ||
| 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 } } } } } } }' | ||
| resp=$(gh api graphql -f query="$projectQuery" -F login="$LOGIN" -F number=$PROJECT_NUM) | ||
| projectId=$(echo "$resp" | jq -r '.data.user.projectV2.id') | ||
| statusFieldId=$(echo "$resp" | jq -r '.data.user.projectV2.fields.nodes[] | select(.name=="Status") | .id') | ||
| optInProg=$(echo "$resp" | jq -r '.data.user.projectV2.fields.nodes[] | select(.name=="Status") | .options[] | select(.name=="'"${STATUS_INPROGRESS_NAME}"'") | .id') | ||
| updateMutation='mutation($project:ID!,$item:ID!,$field:ID!,$option: String!){ updateProjectV2ItemFieldValue(input:{projectId:$project,itemId:$item,fieldId:$field,value:{singleSelectOptionId:$option}}){ projectV2Item { id } } }' | ||
| for issue in $ISSUES; do | ||
| itemId=$(echo "$resp" | jq -r --arg n "$issue" '.data.user.projectV2.items.nodes[] | select(.content.number==($n|tonumber)) | .id') | ||
| if [ -z "$itemId" ]; then | ||
| # Add issue first | ||
| 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') | ||
| addMutation='mutation($project:ID!,$issue:ID!){ addProjectV2ItemById(input:{projectId:$project contentId:$issue}){ item { id } } }' | ||
| gh api graphql -f query="$addMutation" -f project=$projectId -f issue=$issueNode >/dev/null || true | ||
| # Refresh | ||
| resp=$(gh api graphql -f query="$projectQuery" -F login="$LOGIN" -F number=$PROJECT_NUM) | ||
| itemId=$(echo "$resp" | jq -r --arg n "$issue" '.data.user.projectV2.items.nodes[] | select(.content.number==($n|tonumber)) | .id') | ||
| fi | ||
| gh api graphql -f query="$updateMutation" -f project=$projectId -f item=$itemId -f field=$statusFieldId -f option=$optInProg >/dev/null || true | ||
| done | ||
| finalize-merged: | ||
| name: Mark closed issues Done after merge | ||
| if: github.event_name == 'pull_request' && github.event.pull_request.merged == true | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Extract issues from PR body/title | ||
| id: issues | ||
| run: | | ||
| body="${{ github.event.pull_request.body }}\n${{ github.event.pull_request.title }}" | ||
| issues=$(echo "$body" | grep -Eo '#[0-9]+' | tr -d '#' | sort -u || true) | ||
| echo "issues=$issues" >> $GITHUB_OUTPUT | ||
| - name: Install gh + jq | ||
| if: steps.issues.outputs.issues != '' | ||
| run: | | ||
| sudo apt-get update && sudo apt-get install -y jq | ||
| curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg | ||
| sudo chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg | ||
| 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 | ||
| sudo apt-get update && sudo apt-get install gh -y | ||
| - name: Set Status=Done | ||
| if: steps.issues.outputs.issues != '' | ||
| env: | ||
| ISSUES: ${{ steps.issues.outputs.issues }} | ||
| run: | | ||
| gh auth status || gh auth login --with-token <<<"${GITHUB_TOKEN}" | ||
| LOGIN=${{ env.GITHUB_USER }} | ||
| PROJECT_NUM=${{ env.PROJECT_NUMBER }} | ||
| 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 } } } } } } }' | ||
| resp=$(gh api graphql -f query="$projectQuery" -F login="$LOGIN" -F number=$PROJECT_NUM) | ||
| projectId=$(echo "$resp" | jq -r '.data.user.projectV2.id') | ||
| statusFieldId=$(echo "$resp" | jq -r '.data.user.projectV2.fields.nodes[] | select(.name=="Status") | .id') | ||
| optDone=$(echo "$resp" | jq -r '.data.user.projectV2.fields.nodes[] | select(.name=="Status") | .options[] | select(.name=="'"${STATUS_DONE_NAME}"'") | .id') | ||
| updateMutation='mutation($project:ID!,$item:ID!,$field:ID!,$option: String!){ updateProjectV2ItemFieldValue(input:{projectId:$project,itemId:$item,fieldId:$field,value:{singleSelectOptionId:$option}}){ projectV2Item { id } } }' | ||
| for issue in $ISSUES; do | ||
| itemId=$(echo "$resp" | jq -r --arg n "$issue" '.data.user.projectV2.items.nodes[] | select(.content.number==($n|tonumber)) | .id') | ||
| if [ -n "$itemId" ]; then | ||
| gh api graphql -f query="$updateMutation" -f project=$projectId -f item=$itemId -f field=$statusFieldId -f option=$optDone >/dev/null || true | ||
| fi | ||
| done | ||
| notes: | ||
| name: Guidance (no-op) | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Display guidance | ||
| run: | | ||
| echo "Project Sync workflow installed. IMPORTANT:" | ||
| echo "1. Update PROJECT_NUMBER in the workflow to match your project's number." | ||
| echo "2. Ensure the project has a 'Status' single-select field with options: To do, In Progress, Done." | ||
| echo "3. (Optional) Configure built-in project workflows: On item added -> Status=To do; On item closed -> Status=Done." | ||