stale-issue-cleanup
skill✓Find and close stale bug issues in microsoft/AzureStorageExplorer. Use this when asked to clean up stale issues, close inactive bugs, or perform issue hygiene.
apm::install
apm install @microsoft/stale-issue-cleanupapm::skill.md
---
name: stale-issue-cleanup
description: "Find and close stale bug issues in microsoft/AzureStorageExplorer. Use this when asked to clean up stale issues, close inactive bugs, or perform issue hygiene."
---
## Definition of Stale
An issue is stale when ALL of the following are true:
1. The issue is open.
2. The most recent comment was posted by a member of the
`microsoft/azure-storage-explorer` GitHub team.
3. That comment was posted **14 or more days ago**.
4. No one outside the team has commented since — including the original
author and any other community members.
## Important Design Decisions
- **Prefer GraphQL over REST.** Use `gh api graphql` for all GitHub queries.
GraphQL lets us fetch issue type, last comment, and author in a single
request instead of N+1 REST calls.
- **Only target Bug-type issues.** Feature requests and tasks should be
left alone, even if they are old. Filter by `issueType.name == "Bug"`.
- **Never close accessibility issues.** Any issue with a label starting
with `:accessibility:` must be skipped, regardless of staleness.
## Step-by-Step Procedure
### Step 1: Get team members
Run this command to get the list of Storage Explorer team member logins:
```
gh api --paginate /orgs/microsoft/teams/azure-storage-explorer/members --jq '.[].login'
```
Store these logins for comparison throughout the process.
### Step 2: Find candidate bug issues
Use a single GraphQL query to get open Bug issues with their last comment:
```
gh api graphql -f query='
{
repository(owner: "microsoft", name: "AzureStorageExplorer") {
issues(first: 50, states: OPEN, orderBy: {field: UPDATED_AT, direction: ASC}) {
nodes {
number
title
updatedAt
issueType { name }
labels(first: 50) {
nodes { name }
}
comments(last: 1) {
nodes {
author { login }
createdAt
body
}
}
}
pageInfo { hasNextPage endCursor }
}
}
}'
```
Use pagination (pass `after: "{endCursor}"`) to fetch additional pages if needed.
Filter results to issues where:
1. `issueType.name` is "Bug"
2. No label starts with `:accessibility:`
3. `updatedAt` is ≥14 days ago
4. The last comment exists and its author is a team member (from Step 1)
5. The last comment's `createdAt` is ≥14 days ago
### Step 3: Verify each candidate
For each candidate, read the last comment's body. Only mark as stale if the
team member was asking the reporter for something (e.g., repro steps, logs,
version info, confirmation, follow-up). If the team member's comment was
just an acknowledgment or internal note, **skip** the issue.
### Step 4: Close stale issues
For each confirmed stale issue, do the following **in order**:
#### 4a. Post a contextual closing comment
Read the team member's last comment to understand what they asked for.
Then post a comment using `gh issue comment`.
The comment should follow this template:
> Closing due to inactivity. If you're still experiencing this issue, [ACTION].
Where [ACTION] is tuned to what the team member asked for. Examples:
- "...please reply with the requested logs and we can revisit."
- "...please reply with the repro steps and we'll take another look."
- "...please reply to confirm whether the issue persists on the latest version."
- "...please reply and let us know if the suggested workaround resolved it."
Keep it to ONE sentence. Do not restate what was asked — just tell them what
to provide if they want to re-engage.
Use:
```
gh issue comment {number} --repo microsoft/AzureStorageExplorer --body "{comment}"
```
#### 4b. Close the issue as not planned
```
gh issue close {number} --repo microsoft/AzureStorageExplorer --reason "not planned"
```
#### 4c. Set the Resolution project field to "Stale"
Use GraphQL to update the project field:
1. Find the issue's project item ID on the Storage Explorer project.
2. Find the field ID for "Resolution" and the option ID for "Stale".
3. Update the field value.
Combine lookups into a single query where possible:
```
gh api graphql -f query='
query {
repository(owner: "microsoft", name: "AzureStorageExplorer") {
issue(number: {NUMBER}) {
projectItems(first: 10) {
nodes {
id
project { title id }
}
}
}
}
}
'
```
Then update:
```
gh api graphql -f query='
mutation {
updateProjectV2ItemFieldValue(input: {
projectId: "{PROJECT_ID}"
itemId: "{ITEM_ID}"
fieldId: "{FIELD_ID}"
value: { singleSelectOptionId: "{STALE_OPTION_ID}" }
}) {
projectV2Item { id }
}
}
'
```
If the token lacks `read:project` scope, note this in the summary and skip
this step.
### Step 5: Report
After processing all issues, print a summary:
- Total open issues scanned
- Number of stale issues found and closed
- List each closed issue: number, title, and what was requested
If no stale issues were found, say so.
## Important Rules
- **DRY RUN**: If the user says "dry run", list the stale issues and what you
would do, but do NOT post comments, close issues, or update projects.
- **Never close issues where the last comment is from a non-team member.**
The reporter may have replied — always verify.
- **Be conservative.** If you're unsure whether an issue is stale, skip it.
- **Rate limiting.** Pause briefly between API calls if processing many issues.