Skip to content

Commit

Permalink
feat(workflow): enable automatic project status update (#7105)
Browse files Browse the repository at this point in the history
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
Amygos authored Nov 12, 2024
1 parent f146233 commit 46af9cb
Show file tree
Hide file tree
Showing 4 changed files with 349 additions and 0 deletions.
60 changes: 60 additions & 0 deletions .github/workflows/change-status-on-labels.yml
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 }}
27 changes: 27 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,30 @@ Further references:

* [ns6 issue tracker archive](http://dev.nethserver.org)

## Label Management and Issue Status

When labels are added or removed from an issue, the issue's status in the projects is automatically updated:

- **Adding labels:**
- Adding the `testing` label sets the issue status to `Testing`.
- Adding the `verified` label sets the issue status to `Verified`.
- Adding one of these labels automatically removes the other if it exists.

- **Removing labels:**
- Removing the `testing` or `verified` label sets the issue status to `In Progress`.

This behavior is managed by a GitHub Actions workflow that runs the `update_issue_status.sh` script.
If an issue belongs to multiple projects, all projects are updated.

### Configuring the Personal Access Token (PAT)

To allow the workflow to update issue statuses in organization-level projects, an additional Personal Access Token (PAT) with the following minimum permissions is required:

- **`project`**: full access to projects.
- **`public_repo`**: full access to public repositories.
- **`repo`**: full access to private repositories (only required for private repositories).

To set up the PAT correctly:

1. Create a new PAT from your [GitHub account settings](https://github.com/settings/tokens), selecting the permissions listed above.
2. Add the PAT as a secret in the repository or organization, using the name `PROJECT_STATUS_BOT_TOKEN`.
65 changes: 65 additions & 0 deletions scripts/Readme.md
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.
197 changes: 197 additions & 0 deletions scripts/update_issue_status.sh
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."

0 comments on commit 46af9cb

Please sign in to comment.