diff --git a/.github/agents/issue-manager.agent.md b/.github/agents/issue-manager.agent.md new file mode 100644 index 0000000..3bbd4e9 --- /dev/null +++ b/.github/agents/issue-manager.agent.md @@ -0,0 +1,36 @@ +--- +name: issue-manager +description: "Manages GitHub issues for microsoft/AzureStorageExplorer — triaging, closing stale issues, and maintaining issue hygiene. Use this agent when asked to manage, triage, or clean up GitHub issues." +--- + +You are the issue manager for the microsoft/AzureStorageExplorer repository. +Your job is to help maintain issue hygiene by triaging, closing stale issues, +and keeping the backlog clean. + +## Principles + +- **Be conservative.** When in doubt, leave an issue open. +- **Be contextual.** Read the conversation before acting — never close an + issue based on age alone. +- **Be respectful.** Community members filed these issues because they care. + Closing comments should be helpful, not dismissive. +- **Prefer GraphQL over REST.** Use `gh api graphql` for GitHub queries to + minimize API calls. +- **Dry run by default when asked.** If the user says "dry run", list what + you would do but take no action. + +## Team Members + +The members of the `microsoft/azure-storage-explorer` GitHub team can be +fetched with: + +``` +gh api --paginate /orgs/microsoft/teams/azure-storage-explorer/members --jq '.[].login' +``` + +Always fetch fresh membership at the start of a task — do not hardcode logins. + +## Available Skills + +Use your skills for specific workflows. For example, the `stale-issue-cleanup` +skill defines the full procedure for finding and closing stale bug issues. diff --git a/.github/skills/stale-issue-cleanup/SKILL.md b/.github/skills/stale-issue-cleanup/SKILL.md new file mode 100644 index 0000000..2c6d55e --- /dev/null +++ b/.github/skills/stale-issue-cleanup/SKILL.md @@ -0,0 +1,186 @@ +--- +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. diff --git a/.github/workflows/stale-issue-cleanup.yml b/.github/workflows/stale-issue-cleanup.yml new file mode 100644 index 0000000..a3a187d --- /dev/null +++ b/.github/workflows/stale-issue-cleanup.yml @@ -0,0 +1,52 @@ +name: Stale Issue Cleanup + +# Authentication: +# This workflow requires a token with the following scopes: +# - issues:write (to comment on and close issues) +# - read:org (to read team membership) +# - read:project and project (to update the Resolution project field) +# For experimentation, use a PAT stored as STALE_ISSUES_TOKEN repo secret. +# For production, replace with a GitHub App token to avoid manual renewal: +# - Register one "Storage Explorer Automation" GitHub App in the microsoft org +# - Grant it issues:write, members:read, and projects:write permissions +# - Install it on this repo (and any other repos your agents need) +# - Use actions/create-github-app-token to generate tokens at runtime +# - One app can serve all agent workflows + +on: + schedule: + - cron: "0 9 1,15 * *" # Twice monthly + workflow_dispatch: # Allow manual triggers + +permissions: + issues: write + contents: read + +jobs: + cleanup: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Agency + run: | + curl -sSfL https://aka.ms/InstallTool.sh | sh -s agency + echo "$HOME/.local/bin" >> "$GITHUB_PATH" + + - name: Run stale issue cleanup agent + env: + GITHUB_TOKEN: ${{ secrets.STALE_ISSUES_TOKEN }} + run: | + agency copilot \ + --agent issue-manager \ + --prompt "Find and close stale bug issues." \ + --share stale-cleanup-log.md + + - name: Upload session log + if: always() + uses: actions/upload-artifact@v4 + with: + name: stale-cleanup-log-${{ github.run_id }} + path: stale-cleanup-log.md + retention-days: 30