-
Notifications
You must be signed in to change notification settings - Fork 18
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(workflow): enable automatic project status update (#7105)
Introduce a GitHub Actions workflow for auto-updating project status via issue label changes. Transition to **'Testing'** or **'Verified'** upon adding, revert to **'In Progress'** upon removal. A new script, leveraging the GitHub Projects V2 API, manages status changes. It dynamically extracts the repo owner and name from the GitHub event context, promoting reuse across repositories. Ensure to add a Personal Access Token (PAT) with `project` and `public_repo` permissions to the repository secrets for managing organization projects.
- Loading branch information
Showing
4 changed files
with
349 additions
and
0 deletions.
There are no files selected for viewing
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
name: Update Project Status on Label Changes | ||
|
||
on: | ||
issues: | ||
types: | ||
- labeled | ||
- unlabeled | ||
|
||
permissions: | ||
issues: write | ||
|
||
jobs: | ||
update-project-status: | ||
runs-on: ubuntu-latest | ||
env: | ||
GH_TOKEN: ${{ github.token }} | ||
OWNER: ${{ github.repository_owner }} | ||
REPO: ${{ github.event.repository.name }} | ||
ISSUE_NUMBER: ${{ github.event.issue.number }} | ||
ACTION: ${{ github.event.action }} | ||
LABEL_CHANGED: ${{ github.event.label.name }} | ||
|
||
steps: | ||
- name: Checkout code | ||
uses: actions/checkout@v4 | ||
- name: Determine new status on added label | ||
if: ${{ github.event.action == 'labeled' }} | ||
id: labeled | ||
run: | | ||
# Check if the label added is 'testing' or 'verified' | ||
# If yes, set the status to 'Testing' or 'Verified' respectively | ||
# Also, remove the other label if it exists | ||
if [ "$LABEL_CHANGED" == "testing" ]; then | ||
echo "status=Testing" >> $GITHUB_OUTPUT | ||
gh issue edit "$ISSUE_NUMBER" -R "$OWNER/$REPO" --remove-label "verified" || true | ||
elif [ "$LABEL_CHANGED" == "verified" ]; then | ||
echo "status=Verified" >> $GITHUB_OUTPUT | ||
gh issue edit "$ISSUE_NUMBER" -R "$OWNER/$REPO" --remove-label "testing" || true | ||
else | ||
echo "status=skip" >> $GITHUB_OUTPUT | ||
fi | ||
- name: Determine new status on removed Label | ||
if: ${{ github.event.action == 'unlabeled' }} | ||
id: unlabeled | ||
run: | | ||
# Check if the label removed is 'testing' or 'verified' | ||
# If yes, set the status to 'In Progress' | ||
if [ "$LABEL_CHANGED" == "testing" ] || [ "$LABEL_CHANGED" == "verified" ]; then | ||
echo "status=In Progress" >> $GITHUB_OUTPUT | ||
else | ||
echo "status=skip" >> $GITHUB_OUTPUT | ||
fi | ||
- name: Set new status | ||
id: status | ||
if: ${{ steps.labeled.outputs.status != 'skip' || steps.unlabeled.outputs.status != 'skip' }} | ||
run: | | ||
scripts/update_issue_status.sh --owner "$OWNER" --repo "$REPO" --issue-number "$ISSUE_NUMBER" --new-status "$NEW_STATUS" | ||
env: | ||
GH_TOKEN: ${{ secrets.PROJECT_STATUS_BOT_TOKEN }} | ||
NEW_STATUS: ${{ steps.labeled.outputs.status || steps.unlabeled.outputs.status }} |
This file contains 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
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
# Scripts Documentation | ||
|
||
## update_issue_status.sh | ||
|
||
### Description | ||
|
||
`update_issue_status.sh` is a Bash script that updates the status of a GitHub issue across all associated projects. It utilizes the GitHub CLI (`gh`) to interact with GitHub's GraphQL API. | ||
|
||
### Prerequisites | ||
|
||
- **GitHub CLI (`gh`)**: Ensure that the GitHub CLI is installed and authenticated. You can download it from [here](https://cli.github.com/). | ||
- **Permissions**: The authenticated user must have access to the repository and associated projects. | ||
|
||
### Usage | ||
|
||
```bash | ||
./update_issue_status.sh --owner OWNER --repo REPO --issue-number ISSUE_NUMBER --new-status NEW_STATUS | ||
``` | ||
|
||
#### Parameters | ||
|
||
- `--owner`: The GitHub username or organization that owns the repository. | ||
- `--repo`: The name of the repository containing the issue. | ||
- `--issue-number`: The number of the issue to update. | ||
- `--new-status`: The new status to set for the issue in all associated projects. | ||
|
||
#### Example | ||
|
||
```bash | ||
./update_issue_status.sh --owner NethServer --repo dev --issue-number 123 --new-status Verified | ||
``` | ||
|
||
### How It Works | ||
|
||
1. **Argument Parsing**: The script parses command-line arguments to obtain the required parameters. | ||
2. **Authentication**: Checks for the presence of the `gh` CLI and ensures it is authenticated. | ||
3. **Retrieve Issue Node ID**: Uses GraphQL queries to fetch the node ID of the specified issue. | ||
4. **Fetch Associated Projects**: Retrieves a list of all projects that the issue is associated with. | ||
5. **Update Status in Projects**: | ||
- For each project: | ||
- Retrieves the item ID corresponding to the issue. | ||
- Finds the ID of the `Status` field. | ||
- Obtains the option ID for the desired new status. | ||
- Updates the issue's status in the project using a GraphQL mutation. | ||
6. **Completion**: Outputs the status of each update and completes execution. | ||
|
||
### Output | ||
|
||
The script provides informative output at each step, including: | ||
|
||
- Confirmation of parsed arguments. | ||
- IDs retrieved for the issue, projects, fields, and options. | ||
- Success or warning messages during the update process. | ||
|
||
### Error Handling | ||
|
||
- **Missing Arguments**: The script checks for all required arguments and displays usage instructions if any are missing. | ||
- **Authentication Errors**: If the `gh` CLI is not installed or authenticated, the script exits with an error message. | ||
- **GraphQL API Failures**: Errors in API calls are caught, and appropriate messages are displayed. | ||
|
||
### Notes | ||
|
||
- The script assumes that the `Status` field exists in the associated projects and that the `NEW_STATUS` provided is a valid option. | ||
- If the `NEW_STATUS` is not found in a project's status options, the script will issue a warning and continue to the next project. | ||
- Ensure that the `gh` CLI has the necessary scopes and permissions to perform the operations. |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,197 @@ | ||
#!/bin/bash | ||
|
||
# Exit immediately if a command exits with a non-zero status | ||
set -e | ||
|
||
# Check if 'gh' CLI is installed | ||
if ! command -v gh &> /dev/null; then | ||
echo "Error: GitHub CLI (gh) is not installed." | ||
exit 1 | ||
fi | ||
|
||
# Parse command-line arguments | ||
while [[ "$#" -gt 0 ]]; do | ||
case $1 in | ||
--owner) OWNER="$2"; shift ;; | ||
--repo) REPO="$2"; shift ;; | ||
--issue-number) ISSUE_NUMBER="$2"; shift ;; | ||
--new-status) NEW_STATUS="$2"; shift ;; | ||
*) echo "Error: Unknown argument: $1"; exit 1 ;; | ||
esac | ||
shift | ||
done | ||
|
||
# Check required arguments | ||
if [ -z "$OWNER" ] || [ -z "$REPO" ] || [ -z "$ISSUE_NUMBER" ] || [ -z "$NEW_STATUS" ]; then | ||
echo "Usage: $0 --owner OWNER --repo REPO --issue-number ISSUE_NUMBER --new-status NEW_STATUS" | ||
echo "Example: $0 --owner NethServer --repo dev --issue-number 123 --new-status Verified" | ||
exit 1 | ||
fi | ||
|
||
echo "Owner: $OWNER" | ||
echo "Repository: $REPO" | ||
echo "Issue Number: $ISSUE_NUMBER" | ||
echo "New Status: $NEW_STATUS" | ||
|
||
# Authenticate with GitHub (assumes 'gh' is already authenticated) | ||
export GH_TOKEN=$(gh auth token) | ||
|
||
# Set the issue node ID | ||
ISSUE_NODE_ID=$(gh api graphql -f owner="$OWNER" -f repo="$REPO" -F issueNumber="$ISSUE_NUMBER" -f query=' | ||
query($owner: String!, $repo: String!, $issueNumber: Int!) { | ||
repository(owner: $owner, name: $repo) { | ||
issue(number: $issueNumber) { | ||
id | ||
} | ||
} | ||
}' --jq '.data.repository.issue.id') | ||
|
||
if [ -z "$ISSUE_NODE_ID" ]; then | ||
echo "Error: Failed to retrieve issue node ID." | ||
exit 1 | ||
fi | ||
|
||
echo "Issue Node ID: $ISSUE_NODE_ID" | ||
|
||
# Get projects associated with the issue | ||
PROJECT_NUMBERS=$(gh api graphql -f owner="$OWNER" -f repo="$REPO" -F issueNumber="$ISSUE_NUMBER" -f query=' | ||
query($owner: String!, $repo: String!, $issueNumber: Int!) { | ||
repository(owner: $owner, name: $repo) { | ||
issue(number: $issueNumber) { | ||
projectItems(first: 100) { | ||
nodes { | ||
project { | ||
number | ||
} | ||
} | ||
} | ||
} | ||
} | ||
}' --jq '.data.repository.issue.projectItems.nodes[].project.number') | ||
|
||
if [ -z "$PROJECT_NUMBERS" ]; then | ||
echo "No projects found for issue #$ISSUE_NUMBER." | ||
exit 0 | ||
fi | ||
|
||
echo "Projects associated with the issue: $PROJECT_NUMBERS" | ||
|
||
# Update the status in each project | ||
for PROJECT_NUMBER in $PROJECT_NUMBERS; do | ||
echo "Processing Project #$PROJECT_NUMBER" | ||
|
||
# Get item ID | ||
ITEM_ID=$(gh api graphql -F org="$OWNER" -F projectNumber="$PROJECT_NUMBER" -f query=' | ||
query($org: String! , $projectNumber: Int!) { | ||
organization(login: $org) { | ||
projectV2(number: $projectNumber) { | ||
items(first: 100) { | ||
nodes { | ||
id | ||
content { | ||
... on Issue { | ||
id | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
}' --jq '.data.organization.projectV2.items.nodes[] | select(.content.id=="'$ISSUE_NODE_ID'") | .id') | ||
|
||
if [ -z "$ITEM_ID" ]; then | ||
echo "Warning: Item ID not found in Project #$PROJECT_NUMBER." | ||
continue | ||
fi | ||
|
||
echo "Item ID in Project: $ITEM_ID" | ||
|
||
# Get Status field ID | ||
STATUS_FIELD_ID=$(gh api graphql -F org="$OWNER" -F projectNumber="$PROJECT_NUMBER" -f query=' | ||
query($org: String!, $projectNumber: Int!) { | ||
organization(login: $org) { | ||
projectV2(number: $projectNumber) { | ||
fields(first: 100) { | ||
nodes { | ||
... on ProjectV2FieldCommon { | ||
id | ||
name | ||
} | ||
} | ||
} | ||
} | ||
} | ||
}' --jq '.data.organization.projectV2.fields.nodes[] | select(.name=="Status") | .id') | ||
|
||
|
||
if [ -z "$STATUS_FIELD_ID" ]; then | ||
echo "Warning: 'Status' field not found in Project #$PROJECT_NUMBER." | ||
continue | ||
fi | ||
|
||
echo "Status Field ID: $STATUS_FIELD_ID" | ||
|
||
# Get Status option ID | ||
STATUS_OPTION_ID=$(gh api graphql -F org="$OWNER" -F projectNumber="$PROJECT_NUMBER" -F fieldId="$STATUS_FIELD_ID" -f query=' | ||
query($org: String!, $projectNumber: Int!) { | ||
organization(login: $org) { | ||
projectV2(number: $projectNumber) { | ||
fields(first: 100) { | ||
nodes { | ||
... on ProjectV2SingleSelectField { | ||
id | ||
name | ||
options { | ||
id | ||
name | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
}' --jq ".data.organization.projectV2.fields.nodes[] | select(.name==\"Status\") | .options[] | select(.name==\"$NEW_STATUS\") | .id") | ||
|
||
if [ -z "$STATUS_OPTION_ID" ]; then | ||
echo "Warning: Status option '$NEW_STATUS' not found in Project #$PROJECT_NUMBER." | ||
continue | ||
fi | ||
|
||
echo "Status Option ID: $STATUS_OPTION_ID" | ||
|
||
# Get project ID | ||
PROJECT_ID=$(gh api graphql -F org="$OWNER" -F projectNumber="$PROJECT_NUMBER" -f query=' | ||
query($org: String!, $projectNumber: Int!) { | ||
organization(login: $org) { | ||
projectV2(number: $projectNumber) { | ||
id | ||
} | ||
} | ||
}' --jq '.data.organization.projectV2.id') | ||
|
||
if [ -z "$PROJECT_ID" ]; then | ||
echo "Warning: Project ID not found for Project #$PROJECT_NUMBER." | ||
continue | ||
fi | ||
|
||
echo "Project ID: $PROJECT_ID" | ||
|
||
# Update the status of the item in the project | ||
gh api graphql --method POST -f query=' | ||
mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) { | ||
updateProjectV2ItemFieldValue(input: { | ||
projectId: $projectId, | ||
itemId: $itemId, | ||
fieldId: $fieldId, | ||
value: { singleSelectOptionId: $optionId } | ||
}) { | ||
projectV2Item { | ||
id | ||
} | ||
} | ||
}' -F projectId="$PROJECT_ID" -F itemId="$ITEM_ID" -F fieldId="$STATUS_FIELD_ID" -F optionId="$STATUS_OPTION_ID" | ||
|
||
echo "Updated status in Project #$PROJECT_NUMBER to '$NEW_STATUS'." | ||
done | ||
|
||
echo "Script execution completed." |