diff --git a/.github/workflows/check_wdk.yml b/.github/workflows/check_wdk.yml index af216279d3..87f0ebff56 100644 --- a/.github/workflows/check_wdk.yml +++ b/.github/workflows/check_wdk.yml @@ -16,10 +16,13 @@ on: permissions: contents: read # Required by actions/checkout to fetch code. issues: write # Required to create issues. + repository-projects: write # Required to set Type=Task on project items. jobs: check: runs-on: windows-2025 + outputs: + issue_node_id: ${{ steps.create_or_update_issue.outputs.issue_node_id }} steps: - name: Harden Runner uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1 @@ -50,6 +53,7 @@ jobs: "wdk_version=$wdkVersion" >> $env:GITHUB_OUTPUT - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd + id: create_or_update_issue if: steps.check_wdk.outputs.wdk_version != steps.check_repo_wdk.outputs.wdk_version env: TITLE: 'Update the Windows Driver Kit' @@ -60,7 +64,7 @@ jobs: ```powershell .\scripts\Update-WdkVersion.ps1 ``` - LABELS: bug,ci/cd + LABELS: ci/cd with: script: | const owner = process.env.GITHUB_REPOSITORY.split('/')[0] @@ -87,15 +91,25 @@ jobs: repo, body, }); + core.setOutput('issue_node_id', issue.node_id); return; } } // Existing issue not found, create a new one. console.log(`Creating issue ${title}`); - await github.rest.issues.create({ + const created_issue = await github.rest.issues.create({ owner: owner, repo: repo, title: title, body: body, labels: label_array, }); + core.setOutput('issue_node_id', created_issue.data.node_id); + + set_issue_type: + needs: check + if: ${{ needs.check.outputs.issue_node_id != '' }} + uses: ./.github/workflows/reusable-set-issue-type.yml + with: + issue_node_id: ${{ needs.check.outputs.issue_node_id }} + issue_type: Task diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index 3804a73036..20f8405dce 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -38,6 +38,7 @@ permissions: checks: read # Required by reusable-test.yml to check build status. security-events: write # Required by codeql task. issues: write # Required to create issues. + repository-projects: write # Required by reusable workflows that set Project Type. jobs: # Jobs to run on pull, push, and schedule. diff --git a/.github/workflows/netperf.yml b/.github/workflows/netperf.yml index c54a742bae..44794dcb3a 100644 --- a/.github/workflows/netperf.yml +++ b/.github/workflows/netperf.yml @@ -29,6 +29,7 @@ on: permissions: contents: read issues: write # Required to create issues. + repository-projects: write # Required to set Type=Bug on project items. jobs: netperf: @@ -69,6 +70,8 @@ jobs: needs: netperf if: ${{ failure() }} runs-on: ubuntu-latest + outputs: + issue_node_id: ${{ steps.create_or_update_issue.outputs.issue_node_id }} steps: - name: Harden Runner uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1 @@ -86,7 +89,8 @@ jobs: # Note: Simplified condition compared to reusable-test.yml because netperf # is only called from cicd.yml on schedule/workflow_dispatch events. - - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd + - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + id: create_or_update_issue if: steps.skip_check.outputs.should_skip != 'true' env: TITLE: Workflow failed - netperf @@ -94,7 +98,7 @@ jobs: [Failed Run](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) [Codebase](https://github.com/${{ github.repository }}/tree/${{ github.sha }}) Test name - `netperf` - LABELS: bug,ci/cd + LABELS: ci/cd with: script: | @@ -122,15 +126,25 @@ jobs: repo, body, }); + core.setOutput('issue_node_id', issue.node_id); return; } } // Existing issue not found, create a new one. console.log(`Creating issue ${title}`); - await github.rest.issues.create({ + const created_issue = await github.rest.issues.create({ owner: owner, repo: repo, title: title, body: body, labels: label_array, }); + core.setOutput('issue_node_id', created_issue.data.node_id); + + set_issue_type: + needs: create_or_update_issue + if: ${{ needs.create_or_update_issue.outputs.issue_node_id != '' }} + uses: ./.github/workflows/reusable-set-issue-type.yml + with: + issue_node_id: ${{ needs.create_or_update_issue.outputs.issue_node_id }} + issue_type: Bug diff --git a/.github/workflows/reusable-set-issue-type.yml b/.github/workflows/reusable-set-issue-type.yml new file mode 100644 index 0000000000..70a5de45f0 --- /dev/null +++ b/.github/workflows/reusable-set-issue-type.yml @@ -0,0 +1,141 @@ +# Copyright (c) eBPF for Windows contributors +# SPDX-License-Identifier: MIT + +--- +name: Reusable Set Issue Type Workflow + +on: + workflow_call: + inputs: + issue_node_id: + description: 'Issue node ID to update' + required: true + type: string + issue_type: + description: 'Project v2 Type option name (e.g., Bug, Task)' + required: true + type: string + +permissions: + issues: read + repository-projects: write + +jobs: + set_issue_type: + runs-on: ubuntu-latest + steps: + - name: Harden Runner + uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1 + with: + disable-sudo: true + egress-policy: block + allowed-endpoints: > + api.github.com:443 + + - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + ISSUE_NODE_ID: ${{ inputs.issue_node_id }} + ISSUE_TYPE: ${{ inputs.issue_type }} + with: + script: | + const owner = process.env.GITHUB_REPOSITORY.split('/')[0]; + const repo = process.env.GITHUB_REPOSITORY.split('/')[1]; + const issue_node_id = process.env.ISSUE_NODE_ID; + const issue_type = process.env.ISSUE_TYPE; + + const projects_response = await github.graphql( + `query($owner: String!, $repo: String!) { + repository(owner: $owner, name: $repo) { + projectsV2(first: 20) { + nodes { + id + title + } + } + } + }`, + {owner, repo}); + + const issue_response = await github.graphql( + `query($issueId: ID!) { + node(id: $issueId) { + ... on Issue { + projectItems(first: 100) { + nodes { + id + project { + id + } + } + } + } + } + }`, + {issueId: issue_node_id}); + + const existing_items = new Map( + issue_response.node.projectItems.nodes.map((item) => [item.project.id, item.id])); + + for (const project of projects_response.repository.projectsV2.nodes) { + const fields_response = await github.graphql( + `query($projectId: ID!) { + node(id: $projectId) { + ... on ProjectV2 { + fields(first: 50) { + nodes { + ... on ProjectV2SingleSelectField { + id + name + options { + id + name + } + } + } + } + } + } + }`, + {projectId: project.id}); + + const type_field = fields_response.node.fields.nodes.find((field) => field.name === 'Type'); + const type_option = type_field?.options?.find((option) => option.name === issue_type); + if (!type_field || !type_option) { + continue; + } + + let item_id = existing_items.get(project.id); + if (!item_id) { + const add_response = await github.graphql( + `mutation($projectId: ID!, $issueId: ID!) { + addProjectV2ItemById(input: {projectId: $projectId, contentId: $issueId}) { + item { + id + } + } + }`, + {projectId: project.id, issueId: issue_node_id}); + item_id = add_response.addProjectV2ItemById.item.id; + } + + await github.graphql( + `mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) { + updateProjectV2ItemFieldValue(input: { + projectId: $projectId, + itemId: $itemId, + fieldId: $fieldId, + value: {singleSelectOptionId: $optionId} + }) { + projectV2Item { + id + } + } + }`, + { + projectId: project.id, + itemId: item_id, + fieldId: type_field.id, + optionId: type_option.id + }); + console.log(`Set Type=${issue_type} for issue node ${issue_node_id} in project ${project.title}`); + } diff --git a/.github/workflows/reusable-test.yml b/.github/workflows/reusable-test.yml index 6e832ecd25..ffa8266385 100644 --- a/.github/workflows/reusable-test.yml +++ b/.github/workflows/reusable-test.yml @@ -64,6 +64,7 @@ permissions: checks: read # Required by fountainhead/action-wait-for-check to wait for another GitHub check to complete. contents: read # Required by actions/checkout to fetch code. issues: write # Required to create issues. + repository-projects: write # Required to set Type=Bug on project items. jobs: run_test: @@ -513,6 +514,8 @@ jobs: needs: run_test if: ${{ failure() }} runs-on: ubuntu-latest + outputs: + issue_node_id: ${{ steps.create_or_update_issue.outputs.issue_node_id }} steps: - name: Harden Runner uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1 @@ -528,7 +531,8 @@ jobs: cancel_others: 'false' paths_ignore: '["**.md", "**/docs/**"]' - - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd + - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + id: create_or_update_issue if: (github.event_name == 'schedule') || (github.event_name == 'push' || github.event_name == 'merge_group') && (steps.skip_check.outputs.should_skip != 'true') env: TITLE: Workflow failed - ${{inputs.name}} @@ -537,7 +541,7 @@ jobs: [Codebase](https://github.com/${{ github.repository }}/tree/${{ github.sha }}) Test name - `${{ inputs.name }}` Event - ${{ github.event_name }} - LABELS: bug,ci/cd${{ github.event_name == 'merge_group' && ',merge-queue-regression' || '' }} + LABELS: ci/cd${{ github.event_name == 'merge_group' && ',merge-queue-regression' || '' }} with: script: | @@ -565,15 +569,25 @@ jobs: repo, body, }); + core.setOutput('issue_node_id', issue.node_id); return; } } // Existing issue not found, create a new one. console.log(`Creating issue ${title}`); - await github.rest.issues.create({ + const created_issue = await github.rest.issues.create({ owner: owner, repo: repo, title: title, body: body, labels: label_array, }); + core.setOutput('issue_node_id', created_issue.data.node_id); + + set_issue_type: + needs: create_or_update_issue + if: ${{ needs.create_or_update_issue.outputs.issue_node_id != '' }} + uses: ./.github/workflows/reusable-set-issue-type.yml + with: + issue_node_id: ${{ needs.create_or_update_issue.outputs.issue_node_id }} + issue_type: Bug