Skip to content

Commit 46af9cb

Browse files
authored
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.
1 parent f146233 commit 46af9cb

File tree

4 files changed

+349
-0
lines changed

4 files changed

+349
-0
lines changed
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
name: Update Project Status on Label Changes
2+
3+
on:
4+
issues:
5+
types:
6+
- labeled
7+
- unlabeled
8+
9+
permissions:
10+
issues: write
11+
12+
jobs:
13+
update-project-status:
14+
runs-on: ubuntu-latest
15+
env:
16+
GH_TOKEN: ${{ github.token }}
17+
OWNER: ${{ github.repository_owner }}
18+
REPO: ${{ github.event.repository.name }}
19+
ISSUE_NUMBER: ${{ github.event.issue.number }}
20+
ACTION: ${{ github.event.action }}
21+
LABEL_CHANGED: ${{ github.event.label.name }}
22+
23+
steps:
24+
- name: Checkout code
25+
uses: actions/checkout@v4
26+
- name: Determine new status on added label
27+
if: ${{ github.event.action == 'labeled' }}
28+
id: labeled
29+
run: |
30+
# Check if the label added is 'testing' or 'verified'
31+
# If yes, set the status to 'Testing' or 'Verified' respectively
32+
# Also, remove the other label if it exists
33+
if [ "$LABEL_CHANGED" == "testing" ]; then
34+
echo "status=Testing" >> $GITHUB_OUTPUT
35+
gh issue edit "$ISSUE_NUMBER" -R "$OWNER/$REPO" --remove-label "verified" || true
36+
elif [ "$LABEL_CHANGED" == "verified" ]; then
37+
echo "status=Verified" >> $GITHUB_OUTPUT
38+
gh issue edit "$ISSUE_NUMBER" -R "$OWNER/$REPO" --remove-label "testing" || true
39+
else
40+
echo "status=skip" >> $GITHUB_OUTPUT
41+
fi
42+
- name: Determine new status on removed Label
43+
if: ${{ github.event.action == 'unlabeled' }}
44+
id: unlabeled
45+
run: |
46+
# Check if the label removed is 'testing' or 'verified'
47+
# If yes, set the status to 'In Progress'
48+
if [ "$LABEL_CHANGED" == "testing" ] || [ "$LABEL_CHANGED" == "verified" ]; then
49+
echo "status=In Progress" >> $GITHUB_OUTPUT
50+
else
51+
echo "status=skip" >> $GITHUB_OUTPUT
52+
fi
53+
- name: Set new status
54+
id: status
55+
if: ${{ steps.labeled.outputs.status != 'skip' || steps.unlabeled.outputs.status != 'skip' }}
56+
run: |
57+
scripts/update_issue_status.sh --owner "$OWNER" --repo "$REPO" --issue-number "$ISSUE_NUMBER" --new-status "$NEW_STATUS"
58+
env:
59+
GH_TOKEN: ${{ secrets.PROJECT_STATUS_BOT_TOKEN }}
60+
NEW_STATUS: ${{ steps.labeled.outputs.status || steps.unlabeled.outputs.status }}

CONTRIBUTING.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,30 @@ Further references:
2222

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

25+
## Label Management and Issue Status
26+
27+
When labels are added or removed from an issue, the issue's status in the projects is automatically updated:
28+
29+
- **Adding labels:**
30+
- Adding the `testing` label sets the issue status to `Testing`.
31+
- Adding the `verified` label sets the issue status to `Verified`.
32+
- Adding one of these labels automatically removes the other if it exists.
33+
34+
- **Removing labels:**
35+
- Removing the `testing` or `verified` label sets the issue status to `In Progress`.
36+
37+
This behavior is managed by a GitHub Actions workflow that runs the `update_issue_status.sh` script.
38+
If an issue belongs to multiple projects, all projects are updated.
39+
40+
### Configuring the Personal Access Token (PAT)
41+
42+
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:
43+
44+
- **`project`**: full access to projects.
45+
- **`public_repo`**: full access to public repositories.
46+
- **`repo`**: full access to private repositories (only required for private repositories).
47+
48+
To set up the PAT correctly:
49+
50+
1. Create a new PAT from your [GitHub account settings](https://github.com/settings/tokens), selecting the permissions listed above.
51+
2. Add the PAT as a secret in the repository or organization, using the name `PROJECT_STATUS_BOT_TOKEN`.

scripts/Readme.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# Scripts Documentation
2+
3+
## update_issue_status.sh
4+
5+
### Description
6+
7+
`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.
8+
9+
### Prerequisites
10+
11+
- **GitHub CLI (`gh`)**: Ensure that the GitHub CLI is installed and authenticated. You can download it from [here](https://cli.github.com/).
12+
- **Permissions**: The authenticated user must have access to the repository and associated projects.
13+
14+
### Usage
15+
16+
```bash
17+
./update_issue_status.sh --owner OWNER --repo REPO --issue-number ISSUE_NUMBER --new-status NEW_STATUS
18+
```
19+
20+
#### Parameters
21+
22+
- `--owner`: The GitHub username or organization that owns the repository.
23+
- `--repo`: The name of the repository containing the issue.
24+
- `--issue-number`: The number of the issue to update.
25+
- `--new-status`: The new status to set for the issue in all associated projects.
26+
27+
#### Example
28+
29+
```bash
30+
./update_issue_status.sh --owner NethServer --repo dev --issue-number 123 --new-status Verified
31+
```
32+
33+
### How It Works
34+
35+
1. **Argument Parsing**: The script parses command-line arguments to obtain the required parameters.
36+
2. **Authentication**: Checks for the presence of the `gh` CLI and ensures it is authenticated.
37+
3. **Retrieve Issue Node ID**: Uses GraphQL queries to fetch the node ID of the specified issue.
38+
4. **Fetch Associated Projects**: Retrieves a list of all projects that the issue is associated with.
39+
5. **Update Status in Projects**:
40+
- For each project:
41+
- Retrieves the item ID corresponding to the issue.
42+
- Finds the ID of the `Status` field.
43+
- Obtains the option ID for the desired new status.
44+
- Updates the issue's status in the project using a GraphQL mutation.
45+
6. **Completion**: Outputs the status of each update and completes execution.
46+
47+
### Output
48+
49+
The script provides informative output at each step, including:
50+
51+
- Confirmation of parsed arguments.
52+
- IDs retrieved for the issue, projects, fields, and options.
53+
- Success or warning messages during the update process.
54+
55+
### Error Handling
56+
57+
- **Missing Arguments**: The script checks for all required arguments and displays usage instructions if any are missing.
58+
- **Authentication Errors**: If the `gh` CLI is not installed or authenticated, the script exits with an error message.
59+
- **GraphQL API Failures**: Errors in API calls are caught, and appropriate messages are displayed.
60+
61+
### Notes
62+
63+
- The script assumes that the `Status` field exists in the associated projects and that the `NEW_STATUS` provided is a valid option.
64+
- 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.
65+
- Ensure that the `gh` CLI has the necessary scopes and permissions to perform the operations.

scripts/update_issue_status.sh

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
#!/bin/bash
2+
3+
# Exit immediately if a command exits with a non-zero status
4+
set -e
5+
6+
# Check if 'gh' CLI is installed
7+
if ! command -v gh &> /dev/null; then
8+
echo "Error: GitHub CLI (gh) is not installed."
9+
exit 1
10+
fi
11+
12+
# Parse command-line arguments
13+
while [[ "$#" -gt 0 ]]; do
14+
case $1 in
15+
--owner) OWNER="$2"; shift ;;
16+
--repo) REPO="$2"; shift ;;
17+
--issue-number) ISSUE_NUMBER="$2"; shift ;;
18+
--new-status) NEW_STATUS="$2"; shift ;;
19+
*) echo "Error: Unknown argument: $1"; exit 1 ;;
20+
esac
21+
shift
22+
done
23+
24+
# Check required arguments
25+
if [ -z "$OWNER" ] || [ -z "$REPO" ] || [ -z "$ISSUE_NUMBER" ] || [ -z "$NEW_STATUS" ]; then
26+
echo "Usage: $0 --owner OWNER --repo REPO --issue-number ISSUE_NUMBER --new-status NEW_STATUS"
27+
echo "Example: $0 --owner NethServer --repo dev --issue-number 123 --new-status Verified"
28+
exit 1
29+
fi
30+
31+
echo "Owner: $OWNER"
32+
echo "Repository: $REPO"
33+
echo "Issue Number: $ISSUE_NUMBER"
34+
echo "New Status: $NEW_STATUS"
35+
36+
# Authenticate with GitHub (assumes 'gh' is already authenticated)
37+
export GH_TOKEN=$(gh auth token)
38+
39+
# Set the issue node ID
40+
ISSUE_NODE_ID=$(gh api graphql -f owner="$OWNER" -f repo="$REPO" -F issueNumber="$ISSUE_NUMBER" -f query='
41+
query($owner: String!, $repo: String!, $issueNumber: Int!) {
42+
repository(owner: $owner, name: $repo) {
43+
issue(number: $issueNumber) {
44+
id
45+
}
46+
}
47+
}' --jq '.data.repository.issue.id')
48+
49+
if [ -z "$ISSUE_NODE_ID" ]; then
50+
echo "Error: Failed to retrieve issue node ID."
51+
exit 1
52+
fi
53+
54+
echo "Issue Node ID: $ISSUE_NODE_ID"
55+
56+
# Get projects associated with the issue
57+
PROJECT_NUMBERS=$(gh api graphql -f owner="$OWNER" -f repo="$REPO" -F issueNumber="$ISSUE_NUMBER" -f query='
58+
query($owner: String!, $repo: String!, $issueNumber: Int!) {
59+
repository(owner: $owner, name: $repo) {
60+
issue(number: $issueNumber) {
61+
projectItems(first: 100) {
62+
nodes {
63+
project {
64+
number
65+
}
66+
}
67+
}
68+
}
69+
}
70+
}' --jq '.data.repository.issue.projectItems.nodes[].project.number')
71+
72+
if [ -z "$PROJECT_NUMBERS" ]; then
73+
echo "No projects found for issue #$ISSUE_NUMBER."
74+
exit 0
75+
fi
76+
77+
echo "Projects associated with the issue: $PROJECT_NUMBERS"
78+
79+
# Update the status in each project
80+
for PROJECT_NUMBER in $PROJECT_NUMBERS; do
81+
echo "Processing Project #$PROJECT_NUMBER"
82+
83+
# Get item ID
84+
ITEM_ID=$(gh api graphql -F org="$OWNER" -F projectNumber="$PROJECT_NUMBER" -f query='
85+
query($org: String! , $projectNumber: Int!) {
86+
organization(login: $org) {
87+
projectV2(number: $projectNumber) {
88+
items(first: 100) {
89+
nodes {
90+
id
91+
content {
92+
... on Issue {
93+
id
94+
}
95+
}
96+
}
97+
}
98+
}
99+
}
100+
}' --jq '.data.organization.projectV2.items.nodes[] | select(.content.id=="'$ISSUE_NODE_ID'") | .id')
101+
102+
if [ -z "$ITEM_ID" ]; then
103+
echo "Warning: Item ID not found in Project #$PROJECT_NUMBER."
104+
continue
105+
fi
106+
107+
echo "Item ID in Project: $ITEM_ID"
108+
109+
# Get Status field ID
110+
STATUS_FIELD_ID=$(gh api graphql -F org="$OWNER" -F projectNumber="$PROJECT_NUMBER" -f query='
111+
query($org: String!, $projectNumber: Int!) {
112+
organization(login: $org) {
113+
projectV2(number: $projectNumber) {
114+
fields(first: 100) {
115+
nodes {
116+
... on ProjectV2FieldCommon {
117+
id
118+
name
119+
}
120+
}
121+
}
122+
}
123+
}
124+
}' --jq '.data.organization.projectV2.fields.nodes[] | select(.name=="Status") | .id')
125+
126+
127+
if [ -z "$STATUS_FIELD_ID" ]; then
128+
echo "Warning: 'Status' field not found in Project #$PROJECT_NUMBER."
129+
continue
130+
fi
131+
132+
echo "Status Field ID: $STATUS_FIELD_ID"
133+
134+
# Get Status option ID
135+
STATUS_OPTION_ID=$(gh api graphql -F org="$OWNER" -F projectNumber="$PROJECT_NUMBER" -F fieldId="$STATUS_FIELD_ID" -f query='
136+
query($org: String!, $projectNumber: Int!) {
137+
organization(login: $org) {
138+
projectV2(number: $projectNumber) {
139+
fields(first: 100) {
140+
nodes {
141+
... on ProjectV2SingleSelectField {
142+
id
143+
name
144+
options {
145+
id
146+
name
147+
}
148+
}
149+
}
150+
}
151+
}
152+
}
153+
}' --jq ".data.organization.projectV2.fields.nodes[] | select(.name==\"Status\") | .options[] | select(.name==\"$NEW_STATUS\") | .id")
154+
155+
if [ -z "$STATUS_OPTION_ID" ]; then
156+
echo "Warning: Status option '$NEW_STATUS' not found in Project #$PROJECT_NUMBER."
157+
continue
158+
fi
159+
160+
echo "Status Option ID: $STATUS_OPTION_ID"
161+
162+
# Get project ID
163+
PROJECT_ID=$(gh api graphql -F org="$OWNER" -F projectNumber="$PROJECT_NUMBER" -f query='
164+
query($org: String!, $projectNumber: Int!) {
165+
organization(login: $org) {
166+
projectV2(number: $projectNumber) {
167+
id
168+
}
169+
}
170+
}' --jq '.data.organization.projectV2.id')
171+
172+
if [ -z "$PROJECT_ID" ]; then
173+
echo "Warning: Project ID not found for Project #$PROJECT_NUMBER."
174+
continue
175+
fi
176+
177+
echo "Project ID: $PROJECT_ID"
178+
179+
# Update the status of the item in the project
180+
gh api graphql --method POST -f query='
181+
mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {
182+
updateProjectV2ItemFieldValue(input: {
183+
projectId: $projectId,
184+
itemId: $itemId,
185+
fieldId: $fieldId,
186+
value: { singleSelectOptionId: $optionId }
187+
}) {
188+
projectV2Item {
189+
id
190+
}
191+
}
192+
}' -F projectId="$PROJECT_ID" -F itemId="$ITEM_ID" -F fieldId="$STATUS_FIELD_ID" -F optionId="$STATUS_OPTION_ID"
193+
194+
echo "Updated status in Project #$PROJECT_NUMBER to '$NEW_STATUS'."
195+
done
196+
197+
echo "Script execution completed."

0 commit comments

Comments
 (0)