From a46cb27b7aaa294d4bc46f8d09acd875b6ccaf3e Mon Sep 17 00:00:00 2001 From: widal001 Date: Fri, 18 Oct 2024 10:56:26 -0400 Subject: [PATCH 1/9] feat: Adds scripts and queries for set-points-and-sprint Creates scripts for a linter that sets the points and scripts fields to a default value if those fields are unset when an issue is closed --- .../linters/queries/getFieldMetadata.graphql | 32 ++++ .../linters/queries/getItemMetadata.graphql | 46 +++++ .../linters/scripts/set-points-and-sprint.sh | 166 ++++++++++++++++++ 3 files changed, 244 insertions(+) create mode 100644 .github/linters/queries/getFieldMetadata.graphql create mode 100644 .github/linters/queries/getItemMetadata.graphql create mode 100644 .github/linters/scripts/set-points-and-sprint.sh diff --git a/.github/linters/queries/getFieldMetadata.graphql b/.github/linters/queries/getFieldMetadata.graphql new file mode 100644 index 000000000..5fb011115 --- /dev/null +++ b/.github/linters/queries/getFieldMetadata.graphql @@ -0,0 +1,32 @@ +query ( + $org: String! + $project: Int! + $sprintField: String = "Sprint" + $pointsField: String = "Points" +) { + organization(login: $org) { + projectV2(number: $project) { + sprint: field(name: $sprintField) { + ...iterationMetadata + } + points: field(name: $pointsField) { + ...numberMetadata + } + } + } +} + +fragment numberMetadata on ProjectV2Field { + fieldId: id +} + +fragment iterationMetadata on ProjectV2IterationField { + fieldId: id + configuration { + iterations { + id + startDate + duration + } + } +} diff --git a/.github/linters/queries/getItemMetadata.graphql b/.github/linters/queries/getItemMetadata.graphql new file mode 100644 index 000000000..fec4af061 --- /dev/null +++ b/.github/linters/queries/getItemMetadata.graphql @@ -0,0 +1,46 @@ +query ( + $url: URI! + $sprintField: String = "Sprint" + $pointsField: String = "Points" +) { + resource(url: $url) { + ... on Issue { + # get all of the project items associated with this issue + projectItems(first: 10) { + nodes { + ... on ProjectV2Item { + # Get the project ID, number, and owner, as well as itemId + ...projectMetadata + + # Get the value of the "sprint" field, if set + sprint: fieldValueByName(name: $sprintField) { + ... on ProjectV2ItemFieldIterationValue { + iterationId + } + } + + # Get the value of the "points" field, if set + points: fieldValueByName(name: $pointsField) { + ... on ProjectV2ItemFieldNumberValue { + number + } + } + } + } + } + } + } +} + +fragment projectMetadata on ProjectV2Item { + itemId: id + project { + projectId: id + number + owner { + ... on Organization { + login + } + } + } +} diff --git a/.github/linters/scripts/set-points-and-sprint.sh b/.github/linters/scripts/set-points-and-sprint.sh new file mode 100644 index 000000000..addc52714 --- /dev/null +++ b/.github/linters/scripts/set-points-and-sprint.sh @@ -0,0 +1,166 @@ +#! /bin/bash +# Set default values for sprint and points when those fields are unset +# Usage: +# ./set-points-and-sprint.sh \ +# --url "https://github.com/HHS/simpler-grants-gov/issues/123" \ +# --org "HHS" \ +# --project 13 \ +# --sprint-field "Sprint" \ +# --points-field "Points" + + +# ####################################################### +# Parse command line args with format `--option arg` +# ####################################################### + +# see this stack overflow for more details: +# https://stackoverflow.com/a/14203146/7338319 +while [[ $# -gt 0 ]]; do + case $1 in + --dry-run) + echo "Running in dry run mode" + dry_run=YES + shift # past argument + ;; + --url) + issue_url="$2" + shift # past argument + shift # past value + ;; + --org) + org="$2" + shift # past argument + shift # past value + ;; + --project) + project="$2" + shift # past argument + shift # past value + ;; + --sprint-field) + sprint_field="$2" + shift # past argument + shift # past value + ;; + --points-field) + points_field="$2" + shift # past argument + shift # past value + ;; + -*|--*) + echo "Unknown option $1" + exit 1 + ;; + *) + POSITIONAL_ARGS+=("$1") # save positional arg + shift # past argument + ;; + esac +done + +# ####################################################### +# Set script-specific variables +# ####################################################### + +mkdir -p tmp +item_data_file="tmp/closed-issue-data.json" +field_data_file="tmp/field-data.json" +item_query=$(cat "queries/getItemMetadata.graphql") +field_query=$(cat "queries/getFieldMetadata.graphql") + +# ####################################################### +# Fetch issue metadata +# ####################################################### + +gh api graphql \ + --field url="${issue_url}" \ + --field sprintField="${sprint_field}" \ + --field pointsField="${points_field}" \ + -f query="${item_query}" \ + --jq ".data.resource.projectItems.nodes[] | + + # filter for the item in the given project + select(.project.number == ${project}) | + + # filter for items with the sprint or points fields unset + select (.sprint == null or .points == null or .points.number == 0) | + + # format the output + { + itemId, + projectId: .project.projectId, + sprint: .sprint.iterationId, + points: .points.number, + } + " > $item_data_file # write output to a file + +# ####################################################### +# Fetch project metadata +# ####################################################### + +# if the output file contains a record +if [[ -s $item_data_file ]]; then + # fetch the project metadata + gh api graphql \ + --field org="${org}" \ + --field project="${project}" \ + --field sprintField="${sprint_field}" \ + --field pointsField="${points_field}" \ + -f query="${field_query}" \ + --jq ".data.organization.projectV2 | + + # reformat the field metadata + { + points, + sprint: { + fieldId: .sprint.fieldId, + iterationId: .sprint.configuration.iterations[0].id, + } + }" > $field_data_file # write output to a file + + # get the itemId and the projectId + item_id=$(jq -r '.itemId' "$item_data_file") + project_id=$(jq -r '.projectId' "$item_data_file") + +# otherwise print a success message and exit +else + echo "Both sprint and points are set for issue: ${issue_url}" + exit 0 +fi + +# ####################################################### +# Set the sprint value, if empty +# ####################################################### + +if jq -e ".points == null or .points == 0" $item_data_file > /dev/null; then + + echo "Updating points field for issue: ${issue_url}" + point_field_id=$(jq -r '.points.fieldId' "$field_data_file") + gh project item-edit \ + --id "${item_id}" \ + --project-id "${project_id}" \ + --field-id "${point_field_id}" \ + --number 1 + +else + echo "Point value already set for issue: ${issue_url}" +fi + +# ####################################################### +# Set the sprint value, if empty +# ####################################################### + +if jq -e ".sprint == null" $item_data_file > /dev/null; then + + echo "Updating sprint field for issue: ${issue_url}" + sprint_field_id=$(jq -r '.sprint.fieldId' "$field_data_file") + iteration_id=$(jq -r '.sprint.iterationId' "$field_data_file") + gh project item-edit \ + --id "${item_id}" \ + --project-id "${project_id}" \ + --field-id "${sprint_field_id}" \ + --iteration-id "${iteration_id}" + +else + echo "Sprint value already set for issue: ${issue_url}" +fi From 5b969ec34bacef3a1359a59c9306fe59961b93d4 Mon Sep 17 00:00:00 2001 From: widal001 Date: Fri, 18 Oct 2024 10:57:06 -0400 Subject: [PATCH 2/9] ci: Adds GitHub action to set points and sprint --- .../workflows/lint-set-points-and-sprint.yml | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 .github/workflows/lint-set-points-and-sprint.yml diff --git a/.github/workflows/lint-set-points-and-sprint.yml b/.github/workflows/lint-set-points-and-sprint.yml new file mode 100644 index 000000000..5409e82db --- /dev/null +++ b/.github/workflows/lint-set-points-and-sprint.yml @@ -0,0 +1,29 @@ +name: Lint - Set points and sprint on close + +on: + # trigger on PRs that affect this file or a file used to run the linter + issues: + types: [closed] + +defaults: + run: + working-directory: ./.github/linters # ensures that this job runs from the ./linters sub-directory + +jobs: + run-project-linters: + name: Run set points and sprint values on close + runs-on: ubuntu-latest + env: + GH_TOKEN: ${{ secrets.GH_TOKEN_PROJECT_ACCESS }} + ISSUE_URL: ${{ github.event.issue.html_url }} + steps: + - uses: actions/checkout@v4 + + - name: Set default values for sprint and points if unset + run: | + ./set-points-and-sprint.sh \ + --url "$ISSUE_URL" \ + --org HHS \ + --project 13 \ + --sprint-field "Sprint" \ + --points-field "Story Points" From 7777214c0337080df84f562f6b004bd5f87354ee Mon Sep 17 00:00:00 2001 From: widal001 Date: Fri, 18 Oct 2024 10:57:22 -0400 Subject: [PATCH 3/9] docs: Adds new linter to README --- .github/linters/README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/linters/README.md b/.github/linters/README.md index 4ca761a61..7d1c0cee1 100644 --- a/.github/linters/README.md +++ b/.github/linters/README.md @@ -21,9 +21,10 @@ root ### Review automated linters -| Workflow name | Description | Interval | -| --------------------------------------------- | ----------------------------------------------------------------- | ------------------ | -| [Lint - Close done issues][close-done-issues] | Close issues that are marked as done in a project but still open. | Nightly at 12am ET | +| Workflow name | Description | Interval | +| ----------------------------------------------------- | ---------------------------------------------------------------------- | ------------------ | +| [Lint - Close done issues][close-done-issues] | Close issues that are marked as done in a project but still open. | Nightly at 12am ET | +| [Lint - Set points and sprint][set-points-and-sprint] | Sets default points and sprint value (if unset) when issues are closed | On issue close | ### Manually run the linters @@ -57,3 +58,4 @@ root [close-done-issues]: ../workflows/lint-close-done-issues.yml [close-done-issues-script]: ./scripts/close-issues-in-done-col.sh [get-project-items-query]: ./queries/get-project-items.graphql +[set-points-and-sprint]: ../workflows/lint-set-points-and-sprint.yml From 8b7f209eb47855623899b391be16c5d9efe3ce09 Mon Sep 17 00:00:00 2001 From: widal001 Date: Fri, 18 Oct 2024 11:00:20 -0400 Subject: [PATCH 4/9] fix: Updates script permissions --- .github/linters/scripts/set-points-and-sprint.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 .github/linters/scripts/set-points-and-sprint.sh diff --git a/.github/linters/scripts/set-points-and-sprint.sh b/.github/linters/scripts/set-points-and-sprint.sh old mode 100644 new mode 100755 From 8741653a8c2093e8a20908f4a5146e8f371772a8 Mon Sep 17 00:00:00 2001 From: widal001 Date: Fri, 18 Oct 2024 15:15:09 -0400 Subject: [PATCH 5/9] feat: Enables dry-run mode for set-points-and-sprint.sh Allows users to run linter without making changes in GitHub --- .../linters/scripts/set-points-and-sprint.sh | 46 ++++++++++++------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/.github/linters/scripts/set-points-and-sprint.sh b/.github/linters/scripts/set-points-and-sprint.sh index addc52714..ed5578374 100755 --- a/.github/linters/scripts/set-points-and-sprint.sh +++ b/.github/linters/scripts/set-points-and-sprint.sh @@ -6,7 +6,8 @@ # --org "HHS" \ # --project 13 \ # --sprint-field "Sprint" \ -# --points-field "Points" +# --points-field "Points" \ +# --dry-run # allows users to run in dry run mode # ####################################################### @@ -134,13 +135,19 @@ fi if jq -e ".points == null or .points == 0" $item_data_file > /dev/null; then - echo "Updating points field for issue: ${issue_url}" - point_field_id=$(jq -r '.points.fieldId' "$field_data_file") - gh project item-edit \ - --id "${item_id}" \ - --project-id "${project_id}" \ - --field-id "${point_field_id}" \ - --number 1 + if [[ $dry_run == "YES" ]]; then + echo "Would set points field to 1 for issue: ${issue_url}" + else + echo "Setting points field to 1 for issue: ${issue_url}" + # Get fieldId from field data + point_field_id=$(jq -r '.points.fieldId' "$field_data_file") + # Use GitHub CLI to update field + gh project item-edit \ + --id "${item_id}" \ + --project-id "${project_id}" \ + --field-id "${point_field_id}" \ + --number 1 + fi else echo "Point value already set for issue: ${issue_url}" @@ -152,14 +159,21 @@ fi if jq -e ".sprint == null" $item_data_file > /dev/null; then - echo "Updating sprint field for issue: ${issue_url}" - sprint_field_id=$(jq -r '.sprint.fieldId' "$field_data_file") - iteration_id=$(jq -r '.sprint.iterationId' "$field_data_file") - gh project item-edit \ - --id "${item_id}" \ - --project-id "${project_id}" \ - --field-id "${sprint_field_id}" \ - --iteration-id "${iteration_id}" + # Skip actual update in dry-run mode + if [[ $dry_run == "YES" ]]; then + echo "Would set sprint field to current sprint for issue: ${issue_url}" + else + echo "Setting sprint field to current sprint for issue: ${issue_url}" + # Get fieldId and iterationId from field data + sprint_field_id=$(jq -r '.sprint.fieldId' "$field_data_file") + iteration_id=$(jq -r '.sprint.iterationId' "$field_data_file") + # Use GitHub CLI to update project field + gh project item-edit \ + --id "${item_id}" \ + --project-id "${project_id}" \ + --field-id "${sprint_field_id}" \ + --iteration-id "${iteration_id}" + fi else echo "Sprint value already set for issue: ${issue_url}" From eefafba5794edae6cbc15faf8e177fb9da1785d5 Mon Sep 17 00:00:00 2001 From: widal001 Date: Fri, 18 Oct 2024 15:15:36 -0400 Subject: [PATCH 6/9] ci: Adds set-points-and-sprint.sh to linter CI checks --- .github/workflows/ci-project-linters.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/ci-project-linters.yml b/.github/workflows/ci-project-linters.yml index c2f5f236e..7eb5d1ad2 100644 --- a/.github/workflows/ci-project-linters.yml +++ b/.github/workflows/ci-project-linters.yml @@ -17,6 +17,7 @@ jobs: runs-on: ubuntu-latest env: GH_TOKEN: ${{ secrets.GH_TOKEN_PROJECT_ACCESS }} + UNSET_ISSUE: "https://github.com/HHS/simpler-grants-gov/issues/1932" steps: - uses: actions/checkout@v4 @@ -28,3 +29,13 @@ jobs: --status Done \ --batch 100 \ --dry-run + + - name: Dry run - Set points and sprint field + run: | + ./scripts/set-points-and-sprint.sh \ + --url "${UNSET_ISSUE}" \ + --org "HHS" \ + --project 13 \ + --sprint-field "Sprint" \ + --points-field "Story Points" \ + --dry-run From 8f85c670fb48a16bc9783786ea6a44f15f8d931d Mon Sep 17 00:00:00 2001 From: widal001 Date: Mon, 21 Oct 2024 11:43:10 -0400 Subject: [PATCH 7/9] ci: Updates set-points-and-sprint.sh - Filters issues by type - Adds comment after update --- .../linters/scripts/set-points-and-sprint.sh | 80 ++++++++++++++----- 1 file changed, 59 insertions(+), 21 deletions(-) diff --git a/.github/linters/scripts/set-points-and-sprint.sh b/.github/linters/scripts/set-points-and-sprint.sh index ed5578374..eda03f5f6 100755 --- a/.github/linters/scripts/set-points-and-sprint.sh +++ b/.github/linters/scripts/set-points-and-sprint.sh @@ -64,6 +64,7 @@ done # ####################################################### mkdir -p tmp +raw_data_file="tmp/closed-issue-data-raw.json" item_data_file="tmp/closed-issue-data.json" field_data_file="tmp/field-data.json" item_query=$(cat "queries/getItemMetadata.graphql") @@ -74,11 +75,18 @@ field_query=$(cat "queries/getFieldMetadata.graphql") # ####################################################### gh api graphql \ + --header 'GraphQL-Features:issue_types' \ --field url="${issue_url}" \ --field sprintField="${sprint_field}" \ --field pointsField="${points_field}" \ - -f query="${item_query}" \ - --jq ".data.resource.projectItems.nodes[] | + -f query="${item_query}" > $raw_data_file + +# ####################################################### +# Isolate the correct project item and issue type +# ####################################################### + +# Get project item +jq ".data.resource.projectItems.nodes[] | # filter for the item in the given project select(.project.number == ${project}) | @@ -93,31 +101,45 @@ gh api graphql \ sprint: .sprint.iterationId, points: .points.number, } - " > $item_data_file # write output to a file + " $raw_data_file > $item_data_file # read from raw and write to item_data_file + +# Get issue type +issue_type=$(jq -r ".data.resource.issueType.name" $raw_data_file) # ####################################################### # Fetch project metadata # ####################################################### -# if the output file contains a record +# If the output file contains a record if [[ -s $item_data_file ]]; then - # fetch the project metadata - gh api graphql \ - --field org="${org}" \ - --field project="${project}" \ - --field sprintField="${sprint_field}" \ - --field pointsField="${points_field}" \ - -f query="${field_query}" \ - --jq ".data.organization.projectV2 | - - # reformat the field metadata - { - points, - sprint: { - fieldId: .sprint.fieldId, - iterationId: .sprint.configuration.iterations[0].id, - } - }" > $field_data_file # write output to a file + + # If issue is a Task, Bug, or Enhancement, fetch the project metadata + case "${issue_type}" in + "Task"|"Bug"|"Enhancement") + gh api graphql \ + --field org="${org}" \ + --field project="${project}" \ + --field sprintField="${sprint_field}" \ + --field pointsField="${points_field}" \ + -f query="${field_query}" \ + --jq ".data.organization.projectV2 | + + # reformat the field metadata + { + points, + sprint: { + fieldId: .sprint.fieldId, + iterationId: .sprint.configuration.iterations[0].id, + } + }" > $field_data_file # write output to a file + ;; + + # If it's some other type, print a message and exit + *) + echo "Not updating because issue has type: ${issue_type}" + exit 0 + ;; + esac # get the itemId and the projectId item_id=$(jq -r '.itemId' "$item_data_file") @@ -178,3 +200,19 @@ if jq -e ".sprint == null" $item_data_file > /dev/null; then else echo "Sprint value already set for issue: ${issue_url}" fi + +# ####################################################### +# Set the sprint value, if empty +# ####################################################### + +comment="Beep boop: Automatically setting the point and sprint values for this issue " +comment+="in project ${org}/${project} because they were unset when the issue was closed." + +# Skip actual posting in dry-run mode +if [[ $dry_run == "YES" ]]; then + echo "Would post comment: ${comment}" +# otherwise post the comment +else + gh issue comment "${issue_url}" \ + --body "${comment}" +fi From 53189c270fe4aeb205f96fcf2ebbdcbe1271f433 Mon Sep 17 00:00:00 2001 From: widal001 Date: Mon, 21 Oct 2024 11:48:08 -0400 Subject: [PATCH 8/9] ci: Adds issueType to graphql for set-points-and-sprint.sh --- .github/linters/queries/getItemMetadata.graphql | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/linters/queries/getItemMetadata.graphql b/.github/linters/queries/getItemMetadata.graphql index fec4af061..ea8ff8a2a 100644 --- a/.github/linters/queries/getItemMetadata.graphql +++ b/.github/linters/queries/getItemMetadata.graphql @@ -5,6 +5,9 @@ query ( ) { resource(url: $url) { ... on Issue { + issueType { + name + } # get all of the project items associated with this issue projectItems(first: 10) { nodes { From 7ef7e0237e089aab4b4d2233711f34ed17efd825 Mon Sep 17 00:00:00 2001 From: widal001 Date: Mon, 21 Oct 2024 13:23:15 -0400 Subject: [PATCH 9/9] docs: Explains what UNSET_ISSUE is in ci-project-linters.yml --- .github/workflows/ci-project-linters.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci-project-linters.yml b/.github/workflows/ci-project-linters.yml index 7eb5d1ad2..91091e336 100644 --- a/.github/workflows/ci-project-linters.yml +++ b/.github/workflows/ci-project-linters.yml @@ -17,6 +17,8 @@ jobs: runs-on: ubuntu-latest env: GH_TOKEN: ${{ secrets.GH_TOKEN_PROJECT_ACCESS }} + # Test issue with points and sprint values unset + # to be used with set-points-and-sprint.sh test UNSET_ISSUE: "https://github.com/HHS/simpler-grants-gov/issues/1932" steps: - uses: actions/checkout@v4