diff --git a/.github/workflows/project-get-item-id.yaml b/.github/workflows/project-get-item-id.yaml new file mode 100644 index 00000000..aaa7df40 --- /dev/null +++ b/.github/workflows/project-get-item-id.yaml @@ -0,0 +1,79 @@ +name: Project - Get Item ID Within the Project +# This workflow gets the project-specific ID for an item within a project +# All downstream queries and mutations of fields within the project require this ID + +on: + workflow_call: + inputs: + PROJECT_ID: + description: "The Project's graphQL node ID" + type: string + required: true + + ITEM_NODE_ID: + description: "The issue or PR's graphQL node ID" + type: string + required: true + + secrets: + PROJECT_MANAGEMENT_SECRET: + description: "Project Access Token" + required: true + + outputs: + ITEM_PROJECT_ID: + description: "The item's project-specific ID" + value: ${{ jobs.get_items_project_id.outputs.ITEM_PROJECT_ID }} + +jobs: + get_items_project_id: + runs-on: ubuntu-latest + outputs: + ITEM_PROJECT_ID: ${{ steps.get_item_id.outputs.ITEM_PROJECT_ID }} + + steps: + - name: Sleep 1s + id: sleep_1s + run: | + sleep 1 # Ensure the PR is added to the project before we query its ID + + - name: Get Item Project ID + id: get_item_id + env: + GH_TOKEN: ${{ secrets.PROJECT_MANAGEMENT_SECRET }} + run: | + # Query up to 10 projects for the PR + # There's no graphQL filter configured to query by a specific project + # So we need to query all projects and filter the result ourselves + gh api graphql -f query=' + query { + node(id: "${{ inputs.ITEM_NODE_ID }}") { + ... on PullRequest { + projectItems(first: 10) { + nodes { + id + project { + id + } + } + } + } + ... on Issue { + projectItems(first: 10) { + nodes { + id + project { + id + } + } + } + } + } + }' > project_data.json + + # Use jq to do the actual filtering + item_project_id=$(jq -r '.data.node.projectItems.nodes[] | + select(.project.id == "${{ inputs.PROJECT_ID }}") | + .id' project_data.json) + echo "ITEM_PROJECT_ID=$item_project_id" >> $GITHUB_OUTPUT + continue-on-error: true diff --git a/.github/workflows/project-get-set-iteration-field.yaml b/.github/workflows/project-get-set-iteration-field.yaml new file mode 100644 index 00000000..503cb16a --- /dev/null +++ b/.github/workflows/project-get-set-iteration-field.yaml @@ -0,0 +1,132 @@ +name: Project - Get Item ID Within the Project +# This workflow gets the project-specific ID for an item within a project +# All downstream queries and mutations of fields within the project require this ID + +on: + workflow_call: + inputs: + PROJECT_ID: + description: "The Project's graphQL node ID" + type: string + required: true + + ITERATION_FIELD_NAME: + description: "The name of the iteration field" + type: string + required: true + + ITERATION_FIELD_ID: + description: "The graphQL node ID of the iteration field" + type: string + required: true + + ITEM_PROJECT_ID: + description: "The issue or PR's graphQL project-specific ID" + type: string + required: true + + UPDATE_ITEM: + description: "Whether to update the item's iteration field" + default: false + type: boolean + + # Optional fields, used if UPDATE_ITEM is set to true + ITEM_NODE_ID: + description: "The issue or PR's graphQL node ID, only needed if updating linked issues" + default: null + type: string + + UPDATE_LINKED_ISSUES: + description: "Whether to update the linked issues' iteration fields" + default: false + type: boolean + + secrets: + PROJECT_MANAGEMENT_SECRET: + description: "Project Access Token" + required: true + + outputs: + ITERATION_OPTION_ID: + value: ${{ jobs.get_set_iteration_option_id.outputs.ITERATION_OPTION_ID }} + description: "The iteration option ID" + +jobs: + get_set_iteration_option_id: + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + ITERATION_OPTION_ID: ${{ steps.get_iteration_option_id.outputs.ITERATION_OPTION_ID }} + + steps: + - name: Get Iteration Option ID + id: get_iteration_option_id + env: + GH_TOKEN: ${{ secrets.PROJECT_MANAGEMENT_SECRET }} + run: | + # Get current iteration iteration id + # The current iteration is always the first element in the returned list + gh api graphql -f query=' + query { + node(id: "${{ inputs.PROJECT_ID }}") { + ... on ProjectV2 { + id + field(name: "${{ inputs.ITERATION_FIELD_NAME }}") { + ... on ProjectV2IterationField { + id + name + configuration { + iterations { + id + } + } + } + } + } + } + }' > iteration_option_data.json + current_iteration_option_id=$(jq -r '.data.node.field.configuration.iterations[0].id' iteration_option_data.json) + echo "ITERATION_OPTION_ID=$current_iteration_option_id" >> "$GITHUB_OUTPUT" + + - name: Update item iteration field + id: update_item_iteration_field + if: ${{ inputs.UPDATE_ITEM == true }} + env: + GH_TOKEN: ${{ secrets.PROJECT_MANAGEMENT_SECRET }} + run: | + # Set the iteration based on the query above + # This overwrites whatever was in it before, we may want to make an "OVERWRITE" option + gh api graphql -f query=' + mutation { + updateProjectV2ItemFieldValue( + input: { + projectId: "${{ inputs.PROJECT_ID }}" + itemId: "${{ inputs.ITEM_PROJECT_ID }}" + fieldId: "${{ inputs.ITERATION_FIELD_ID }}" + value: { + iterationId: "${{ steps.get_iteration_option_id.outputs.ITERATION_OPTION_ID }}" + } + } + ) { + projectV2Item { + id + } + } + }' + continue-on-error: true + + update_linked_issues: + if: ${{ inputs.UPDATE_LINKED_ISSUES == true }} + permissions: + contents: read + uses: ./.github/workflows/project-update-linked-issues.yaml + needs: get_set_iteration_option_id + with: + PROJECT_ID: ${{ inputs.PROJECT_ID }} + PR_PROJECT_ID: ${{ inputs.ITEM_PROJECT_ID }} + PR_NODE_ID: ${{ inputs.ITEM_NODE_ID }} + UPDATE_FIELD_TYPE: "iteration" + UPDATE_FIELD_ID: ${{ inputs.ITERATION_FIELD_ID }} + UPDATE_FIELD_VALUE: ${{ needs.get_set_iteration_option_id.outputs.ITERATION_OPTION_ID }} + secrets: inherit diff --git a/.github/workflows/project-get-set-single-select-field.yaml b/.github/workflows/project-get-set-single-select-field.yaml new file mode 100644 index 00000000..38fde2d7 --- /dev/null +++ b/.github/workflows/project-get-set-single-select-field.yaml @@ -0,0 +1,131 @@ +name: Project - Get Item ID Within the Project +# This workflow gets the project-specific ID for an item within a project +# All downstream queries and mutations of fields within the project require this ID + +on: + workflow_call: + inputs: + PROJECT_ID: + description: "The Project's graphQL node ID" + type: string + required: true + + SINGLE_SELECT_FIELD_NAME: + description: "The name of the single-select field" + type: string + required: true + + SINGLE_SELECT_OPTION_VALUE: + description: "The value of the option we'd like to get/set" + type: string + required: true + + SINGLE_SELECT_FIELD_ID: + description: "The graphQL node ID of the single-select field" + type: string + required: true + + ITEM_PROJECT_ID: + description: "The issue or PR's graphQL project-specific ID" + type: string + required: true + + UPDATE_ITEM: + description: "Whether to update the item's single-select field" + default: false + type: boolean + + # Optional fields, used if UPDATE_ITEM is set to true + ITEM_NODE_ID: + description: "The issue or PR's graphQL node ID, only needed if updating linked issues" + default: null + type: string + + UPDATE_LINKED_ISSUES: + description: "Whether to update the linked issues' single_select fields" + default: false + type: boolean + + secrets: + PROJECT_MANAGEMENT_SECRET: + description: "Project Access Token" + required: true + + outputs: + SINGLE_SELECT_OPTION_ID: + value: ${{ jobs.get_set_single_select_option_id.outputs.SINGLE_SELECT_OPTION_ID }} + description: "The single_select option ID" + +jobs: + get_set_single_select_option_id: + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + SINGLE_SELECT_OPTION_ID: ${{ steps.get_single_select_option_id.outputs.SINGLE_SELECT_OPTION_ID }} + + steps: + - name: Get single_select Option ID + id: get_single_select_option_id + env: + GH_TOKEN: ${{ secrets.PROJECT_MANAGEMENT_SECRET }} + run: | + # Get single_select option id + gh api graphql -f query=' + query { + node(id: "${{ inputs.PROJECT_ID }}") { + ... on ProjectV2 { + id + field(name: "${{ inputs.SINGLE_SELECT_FIELD_NAME }}") { + ... on ProjectV2SingleSelectField { + id + options(names: "${{ inputs.SINGLE_SELECT_OPTION_VALUE }}") {id} + } + } + } + } + }' > single_select_option_data.json + current_single_select_option_id=$(jq -r '.data.node.field.options[0].id' single_select_option_data.json) + echo "SINGLE_SELECT_OPTION_ID=$current_single_select_option_id" >> "$GITHUB_OUTPUT" + + - name: Update item single_select field + id: update_item_single_select_field + if: ${{ inputs.UPDATE_ITEM == true }} + env: + GH_TOKEN: ${{ secrets.PROJECT_MANAGEMENT_SECRET }} + run: | + # Set the single_select based on the query above + # This overwrites whatever was in it before, we may want to make an "OVERWRITE" option + gh api graphql -f query=' + mutation { + updateProjectV2ItemFieldValue( + input: { + projectId: "${{ inputs.PROJECT_ID }}" + itemId: "${{ inputs.ITEM_PROJECT_ID }}" + fieldId: "${{ inputs.SINGLE_SELECT_FIELD_ID }}" + value: { + singleSelectOptionId: "${{ steps.get_single_select_option_id.outputs.SINGLE_SELECT_OPTION_ID }}" + } + } + ) { + projectV2Item { + id + } + } + }' + continue-on-error: true + + update_linked_issues: + if: ${{ inputs.UPDATE_LINKED_ISSUES == true }} + permissions: + contents: read + uses: ./.github/workflows/project-update-linked-issues.yaml + needs: get_set_single_select_option_id + with: + PROJECT_ID: ${{ inputs.PROJECT_ID }} + PR_PROJECT_ID: ${{ inputs.ITEM_PROJECT_ID }} + PR_NODE_ID: ${{ inputs.ITEM_NODE_ID }} + UPDATE_FIELD_TYPE: "single_select" + UPDATE_FIELD_ID: ${{ inputs.SINGLE_SELECT_FIELD_ID }} + UPDATE_FIELD_VALUE: ${{ needs.get_set_single_select_option_id.outputs.SINGLE_SELECT_OPTION_ID }} + secrets: inherit diff --git a/.github/workflows/project-set-text-date-numeric-field.yaml b/.github/workflows/project-set-text-date-numeric-field.yaml new file mode 100644 index 00000000..71eabdb2 --- /dev/null +++ b/.github/workflows/project-set-text-date-numeric-field.yaml @@ -0,0 +1,116 @@ +name: Project - Get Item ID Within the Project +# This workflow gets the project-specific ID for an item within a project +# All downstream queries and mutations of fields within the project require this ID + +on: + workflow_call: + inputs: + PROJECT_ID: + description: "The Project's graphQL node ID" + type: string + required: true + + FIELD_TYPE: + description: "The type of the field [text, date, number]" + type: string + required: true + + SET_VALUE: + description: "The value we'd like to set" + type: string + required: true + + FIELD_ID: + description: "The graphQL node ID of the field" + type: string + required: true + + ITEM_PROJECT_ID: + description: "The issue or PR's graphQL project-specific ID" + type: string + required: true + + ITEM_NODE_ID: + description: "The issue or PR's graphQL node ID" + default: null + type: string + required: true + + # Optional field + UPDATE_LINKED_ISSUES: + description: "Whether to update the linked issues' single_select fields" + default: false + type: boolean + + secrets: + PROJECT_MANAGEMENT_SECRET: + description: "Project Access Token" + required: true + +jobs: + set_text_date_numeric_option_id: + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - name: Update item text/date/numeric field + id: update_item_text_date_numeric_field + env: + GH_TOKEN: ${{ secrets.PROJECT_MANAGEMENT_SECRET }} + run: | + # Set the field based on the inputted desired value + # This overwrites whatever was in it before, we may want to make an "OVERWRITE" option + if [ "${{ inputs.FIELD_TYPE }}" == "date" ] || \ + [ "${{ inputs.FIELD_TYPE }}" == "text" ]; then + gh api graphql -f query=" + mutation { + updateProjectV2ItemFieldValue( + input: { + projectId: \"${{ inputs.PROJECT_ID }}\" + itemId: \"${{ inputs.ITEM_PROJECT_ID }}\" + fieldId: \"${{ inputs.FIELD_ID }}\" + value: { ${{ inputs.FIELD_TYPE }}: \"${{ inputs.SET_VALUE }}\" } + } + ) { + projectV2Item { + id + } + } + }" + + elif [ "${{ inputs.FIELD_TYPE }}" == "number" ]; then + gh api graphql -f query=" + mutation { + updateProjectV2ItemFieldValue( + input: { + projectId: \"${{ inputs.PROJECT_ID }}\" + itemId: \"${{ inputs.ITEM_PROJECT_ID }}\" + fieldId: \"${{ inputs.FIELD_ID }}\" + value: { ${{ inputs.FIELD_TYPE }}: ${{ inputs.SET_VALUE }} } + } + ) { + projectV2Item { + id + } + } + }" + + else + echo "Invalid field type" + fi + continue-on-error: true + + update_linked_issues: + if: ${{ inputs.UPDATE_LINKED_ISSUES == true }} + permissions: + contents: read + uses: ./.github/workflows/project-update-linked-issues.yaml + with: + PROJECT_ID: ${{ inputs.PROJECT_ID }} + PR_PROJECT_ID: ${{ inputs.ITEM_PROJECT_ID }} + PR_NODE_ID: ${{ inputs.ITEM_NODE_ID }} + UPDATE_FIELD_TYPE: ${{inputs.FIELD_TYPE}} + UPDATE_FIELD_ID: ${{ inputs.FIELD_ID }} + UPDATE_FIELD_VALUE: ${{ inputs.SET_VALUE }} + secrets: inherit diff --git a/.github/workflows/project-update-linked-issues.yaml b/.github/workflows/project-update-linked-issues.yaml new file mode 100644 index 00000000..7a99833f --- /dev/null +++ b/.github/workflows/project-update-linked-issues.yaml @@ -0,0 +1,135 @@ +name: Project - Update Linked Issues +# This workflow takes a PR and updates the linked issues to match the PR +# Issues do not have a connection back to the PRs, so this workflow can only be called by the PR +# It's flexible what fields you update +# This workflow will primarily be called by the 'get-set' workflows + +on: + workflow_call: + inputs: + PROJECT_ID: + description: "The Project's graphQL node ID" + type: string + required: true + + PR_PROJECT_ID: + description: "The PR's graphQL project-specific ID " + type: string + required: true + + PR_NODE_ID: + description: "The PR's graphQL node ID" + default: null + type: string + + UPDATE_FIELD_TYPE: + description: "The type of field to update - [text, number, date, single_select, iteration]" + type: string + required: true + + UPDATE_FIELD_ID: + description: "The graphQL node ID of the iteration field" + type: string + required: true + + UPDATE_FIELD_VALUE: + description: "The value to set the field to" + type: string + required: true + + secrets: + PROJECT_MANAGEMENT_SECRET: + description: "Project Access Token" + required: true + + +jobs: + synchronize_linked_issues: + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - name: Sync Linked Issues + id: sync_linked_issues + env: + GITHUB_TOKEN: ${{ secrets.PROJECT_MANAGEMENT_SECRET }} + run: | + # Find the linked issues to the PR + # If an issue is passed in, the json will return null and the for loop won't trigger + # Potential future improvement could be some nicer error messaging on incorrect input + gh api graphql -f query=' + query { + node(id: "${{ inputs.PR_NODE_ID }}") { + ... on PullRequest { + closingIssuesReferences(first: 10) { + nodes { + projectItems(first: 10) { + nodes {id, project{id}} + } + } + } + } + } + }' > linked_issues.json + issue_ids=$(jq -r '.data.node.closingIssuesReferences.nodes[].projectItems.nodes[] | + select(.project.id == "${{ inputs.PROJECT_ID }}") | .id' linked_issues.json) + + for issue_id in $issue_ids; do + # Each field type has a different `value` that is needed by the mutation. + + if [ "${{ inputs.UPDATE_FIELD_TYPE }}" == "iteration" ]; then + gh api graphql -f query=' + mutation { + updateProjectV2ItemFieldValue( + input: { + projectId: "${{ inputs.PROJECT_ID }}" + itemId: "'"$issue_id"'" + fieldId: "${{ inputs.UPDATE_FIELD_ID }}" + value: {iterationId: "${{ inputs.UPDATE_FIELD_VALUE }}"}}) + {projectV2Item {id}}}' + + elif [ "${{ inputs.UPDATE_FIELD_TYPE }}" == "single_select" ]; then + gh api graphql -f query=' + mutation { + updateProjectV2ItemFieldValue( + input: { + projectId: "${{ inputs.PROJECT_ID }}" + itemId: "'"$issue_id"'" + fieldId: "${{ inputs.UPDATE_FIELD_ID }}" + value: {singleSelectOptionId: "${{ inputs.UPDATE_FIELD_VALUE }}"}}) + {projectV2Item {id}}}' + + elif [ "${{ inputs.UPDATE_FIELD_TYPE }}" == "date" ] || \ + [ "${{ inputs.UPDATE_FIELD_TYPE }}" == "text" ]; then + gh api graphql -f query=" + mutation { + updateProjectV2ItemFieldValue( + input: { + projectId: \"${{ inputs.PROJECT_ID }}\" + itemId: \"$issue_id\" + fieldId: \"${{ inputs.UPDATE_FIELD_ID }}\" + value: {${{ inputs.UPDATE_FIELD_TYPE }}: \"${{ inputs.UPDATE_FIELD_VALUE }}\"} + } + ) { + projectV2Item { + id + } + } + }" + + elif [ "${{ inputs.UPDATE_FIELD_TYPE }}" == "number" ]; then + gh api graphql -f query=" + mutation { + updateProjectV2ItemFieldValue( + input: { + projectId: \"${{ inputs.PROJECT_ID }}\" + itemId: \"$issue_id\" + fieldId: \"${{ inputs.UPDATE_FIELD_ID }}\" + value: {${{ inputs.UPDATE_FIELD_TYPE }}: ${{ inputs.UPDATE_FIELD_VALUE }}}} + ) {projectV2Item {id}}}" + + else + echo "Invalid field type" + fi + done