Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Issue #2495] Set points and sprint on close #2519

Merged
merged 9 commits into from
Oct 21, 2024
8 changes: 5 additions & 3 deletions .github/linters/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
32 changes: 32 additions & 0 deletions .github/linters/queries/getFieldMetadata.graphql
Original file line number Diff line number Diff line change
@@ -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
}
}
}
49 changes: 49 additions & 0 deletions .github/linters/queries/getItemMetadata.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
query (
$url: URI!
$sprintField: String = "Sprint"
$pointsField: String = "Points"
) {
resource(url: $url) {
... on Issue {
issueType {
name
}
# 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
}
}
}
}
218 changes: 218 additions & 0 deletions .github/linters/scripts/set-points-and-sprint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
#! /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" \
# --dry-run # allows users to run in dry run mode


# #######################################################
# 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
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")
field_query=$(cat "queries/getFieldMetadata.graphql")

# #######################################################
# Fetch issue metadata
# #######################################################

gh api graphql \
--header 'GraphQL-Features:issue_types' \
--field url="${issue_url}" \
--field sprintField="${sprint_field}" \
--field pointsField="${points_field}" \
-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}) |

# 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,
}
" $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 [[ -s $item_data_file ]]; then

# 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")
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

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}"
fi

# #######################################################
# Set the sprint value, if empty
# #######################################################

if jq -e ".sprint == null" $item_data_file > /dev/null; then

# 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}"
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
13 changes: 13 additions & 0 deletions .github/workflows/ci-project-linters.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ 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"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you expand on what UNSET_ISSUE means here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great callout! Just added some comments in this commit: docs: Explains what UNSET_ISSUE is in ci-project-linters.yml

steps:
- uses: actions/checkout@v4

Expand All @@ -28,3 +31,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
29 changes: 29 additions & 0 deletions .github/workflows/lint-set-points-and-sprint.yml
Original file line number Diff line number Diff line change
@@ -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"
Loading