ci: add more workflows for issues (#1220)

Drew Smirnoff created

## What?

Adds a lot more workflows to help handle issues and PRs better

## Why?

It is getting hard to triage issues and pull requests, this will likely
help

Signed-off-by: drew <me@andrinoff.com>

Change summary

.github/ISSUE_TEMPLATE/bug_report.yml    |  23 ++++
.github/labeler-config.yml               |  48 ++++++++
.github/labels.yml                       | 140 +++++++++++++++++++++++++
.github/pr-paths.yml                     | 121 ++++++++++++++++++++++
.github/workflows/demo.yml               |  10 +
.github/workflows/feature-popularity.yml |  63 +++++++++++
.github/workflows/label-sync.yml         |  64 +++++++++++
.github/workflows/labeler-backfill.yml   |  72 +++++++++++++
.github/workflows/labeler.yml            |   6 
.github/workflows/needs-response.yml     |  45 ++++++++
.github/workflows/pr-labeler.yml         |  23 ++++
.github/workflows/screenshots.yml        |  11 +
.github/workflows/stale.yml              |  37 ++++++
.github/workflows/sync-gomod2nix.yml     |  12 ++
.github/workflows/update-flake.yml       |  12 ++
renovate.json                            |   4 
16 files changed, 681 insertions(+), 10 deletions(-)

Detailed changes

.github/ISSUE_TEMPLATE/bug_report.yml 🔗

@@ -50,11 +50,32 @@ body:
       description: "Run `go version` to find out. Only relevant if the issue is related to Go/compilation."
       placeholder: "e.g. 1.22.0"
 
-  - type: input
+  - type: dropdown
     id: os
     attributes:
       label: OS
+      options:
+        - Linux
+        - macOS
+        - Windows
+        - Other
+    validations:
+      required: true
+
+  - type: input
+    id: os-version
+    attributes:
+      label: OS version
       placeholder: "e.g. macOS 14, Ubuntu 24.04, Windows 11"
+
+  - type: dropdown
+    id: arch
+    attributes:
+      label: Architecture
+      options:
+        - amd64 (x86_64)
+        - arm64 (aarch64 / Apple Silicon)
+        - Other / unsure
     validations:
       required: true
 

.github/labeler-config.yml 🔗

@@ -1,6 +1,6 @@
-"bug": '\b([Bb]ug(s)?|[Ee]rror(s)?|[Ff]ix(es)?)\b'
+"bug": '\b([Bb]ug(s)?|[Ff]ix(es)?)\b'
 
-"enhancement": '\b([Ee]nhancement(s)?|[Ff]eature(s)?|[Ii]dea(s)?|[Ff]eat(s)?)\b'
+"enhancement": '\b([Ff]eat(s)?)\b'
 
 "documentation": '\b([Dd]ocumentation|[Dd]ocs|[Rr]eadme)\b'
 
@@ -13,3 +13,47 @@
 "ci": '\b([Cc]i|[Cc]ontinuous integration|[Bb]uild|[Ww]orkflow)\b'
 
 "chore": '\b([Cc]hore|[Mm]aintenance|[Rr]efactor|[Cc]leanup)\b'
+
+"os/windows": '\b([Ww]indows|[Ww]in(10|11)|[Mm]icrosoft)\b'
+
+"os/linux": '\b([Ll]inux|[Uu]buntu|[Dd]ebian|[Ff]edora|[Aa]rch [Ll]inux|[Nn]ix[Oo][Ss]|[Pp]op!?_?[Oo][Ss])\b'
+
+"os/macos": '\b([Mm]ac[Oo][Ss]?|[Oo][Ss] ?[Xx]|[Dd]arwin|[Mm]acbook)\b'
+
+"arch/arm64": '\b(arm64|aarch64|[Aa]pple ?[Ss]ilicon|[Mm][1-4]( [Pp]ro| [Mm]ax| [Uu]ltra)?|[Mm]ac(book)? [Mm][1-4])\b'
+
+"arch/amd64": '\b(amd64|x86[_-]?64|x64|[Ii]ntel( [Mm]ac)?)\b'
+
+"area/tui": '\b([Tt][Uu][Ii]|[Vv]iew|[Bb]ubble[Tt]ea|[Ll]ipgloss|[Kk]eybind(ing)?s?|[Tt]erminal [Uu][Ii])\b'
+
+"area/fetcher": '\b([Ff]etcher|[Ii][Mm][Aa][Pp]|[Ii]dle|[Mm]ailbox|[Ss]earch|[Ss]ub-?address)\b'
+
+"area/sender": '\b([Ss]ender|[Ss][Mm][Tt][Pp]|[Ss]end(ing)? [Mm]ail|[Cc]ompose)\b'
+
+"area/oauth": '\b([Oo][Aa]uth2?|[Xx][Oo][Aa][Uu][Tt][Hh]2?|[Gg]oogle [Aa]uth|[Mm]icrosoft [Aa]uth|[Tt]oken [Rr]efresh)\b'
+
+"area/calendar": '\b([Cc]alendar|[Ii][Cc]al|[Cc]al[Dd][Aa][Vv]|[Ee]vent(s)?)\b'
+
+"area/notify": '\b([Nn]otif(y|ication)s?|[Tt]oast|[Dd]esktop [Nn]otif)\b'
+
+"area/pgp": '\b([Pp][Gg][Pp]|[Gg][Pp][Gg]|[Ee]ncrypt(ion)?|[Dd]ecrypt|[Ss]ign(ing)?|[Kk]ey ?ring)\b'
+
+"area/plugin": '\b([Pp]lugin(s)?)\b'
+
+"area/theme": '\b([Tt]heme(s)?|[Cc]olor ?scheme|[Ss]tyling)\b'
+
+"area/i18n": '\b([Ii]18n|[Ll]10n|[Ll]ocali[sz]ation|[Tt]ranslat(e|ion)|[Ll]anguage [Pp]ack)\b'
+
+"area/config": '\b([Cc]onfig(uration)?|[Ss]ettings|[Cc]onfig\.yaml|[Cc]onfig\.toml|[Pp]references)\b'
+
+"area/cli": '\b([Cc][Ll][Ii]|[Cc]ommand[- ]?line|[Ff]lag(s)?|[Aa]rgument(s)?)\b'
+
+"area/daemon": '\b([Dd]aemon|[Rr][Pp][Cc]|[Bb]ackground [Ss]ervice|[Ss]ystemd)\b'
+
+"area/scard": '\b([Ss]mart ?card|[Ss]card|[Pp][Kk][Cc][Ss]#?11|[Yy]ubikey|[Hh]ardware [Tt]oken)\b'
+
+"area/nix": '\b([Nn]ix|[Nn]ix[Oo][Ss]|[Ff]lake|[Gg]omod2nix|[Hh]ome ?[Mm]anager)\b'
+
+"area/build": '\b([Bb]uild|[Mm]akefile|[Gg]oreleaser|[Ss]napcraft|[Ff]latpak|[Hh]omebrew|[Pp]ackag(e|ing))\b'
+
+"area/docs": '\b([Dd]ocumentation|[Dd]ocs|[Rr]eadme|[Mm]an ?page)\b'

.github/labels.yml 🔗

@@ -0,0 +1,140 @@
+# Label definitions. Synced by .github/workflows/label-sync.yml via `gh label`.
+
+# --- Type ---
+- name: bug
+  color: d73a4a
+  description: Something isn't working
+- name: enhancement
+  color: a2eeef
+  description: New feature or request
+- name: documentation
+  color: 0075ca
+  description: Documentation changes
+- name: question
+  color: d876e3
+  description: Further information requested
+- name: performance
+  color: fbca04
+  description: Performance improvement
+- name: dependencies
+  color: 0366d6
+  description: Dependency updates
+- name: ci
+  color: 1d76db
+  description: CI / build pipeline
+- name: chore
+  color: cfd3d7
+  description: Maintenance, refactor, cleanup
+- name: security
+  color: b60205
+  description: Security-related
+
+# --- Triage / lifecycle ---
+- name: needs-triage
+  color: ededed
+  description: Awaiting maintainer review
+- name: needs-response
+  color: fbca04
+  description: Waiting on issue author reply
+- name: stale
+  color: cccccc
+  description: No activity for extended period
+- name: pinned
+  color: 0e8a16
+  description: Exempt from stale
+- name: in-progress
+  color: 5319e7
+  description: Actively being worked on
+- name: blocked
+  color: b60205
+  description: Blocked on external dependency
+- name: help-wanted
+  color: 008672
+  description: Extra attention is needed
+- name: good-first-issue
+  color: 7057ff
+  description: Good for newcomers
+- name: popular
+  color: ff6f00
+  description: High community interest (10+ 👍)
+- name: duplicate
+  color: cfd3d7
+  description: Duplicate of another issue
+- name: wontfix
+  color: ffffff
+  description: Will not be worked on
+- name: invalid
+  color: e4e669
+  description: Not valid
+
+# --- OS ---
+- name: os/windows
+  color: 0078d4
+  description: Windows-specific
+- name: os/linux
+  color: fcc624
+  description: Linux-specific
+- name: os/macos
+  color: 333333
+  description: macOS-specific
+
+# --- Arch ---
+- name: arch/arm64
+  color: 6e40c9
+  description: arm64 / aarch64
+- name: arch/amd64
+  color: 1f6feb
+  description: amd64 / x86_64
+
+# --- Area ---
+- name: area/tui
+  color: c2e0c6
+  description: Terminal UI / view layer
+- name: area/fetcher
+  color: c2e0c6
+  description: IMAP fetch / IDLE / search
+- name: area/sender
+  color: c2e0c6
+  description: SMTP send path
+- name: area/oauth
+  color: c2e0c6
+  description: OAuth / XOAUTH2 / auth flows
+- name: area/calendar
+  color: c2e0c6
+  description: Calendar integration
+- name: area/notify
+  color: c2e0c6
+  description: Notifications
+- name: area/pgp
+  color: c2e0c6
+  description: PGP / encryption
+- name: area/plugin
+  color: c2e0c6
+  description: Plugin system
+- name: area/theme
+  color: c2e0c6
+  description: Theming / colors
+- name: area/i18n
+  color: c2e0c6
+  description: Localization / translations
+- name: area/config
+  color: c2e0c6
+  description: Configuration / settings
+- name: area/cli
+  color: c2e0c6
+  description: CLI flags / commands
+- name: area/daemon
+  color: c2e0c6
+  description: Daemon / RPC
+- name: area/scard
+  color: c2e0c6
+  description: Smart card / PKCS#11
+- name: area/nix
+  color: c2e0c6
+  description: Nix flake / packaging
+- name: area/build
+  color: c2e0c6
+  description: Build system / Makefile / packaging
+- name: area/docs
+  color: c2e0c6
+  description: Docs site / README

.github/pr-paths.yml 🔗

@@ -0,0 +1,121 @@
+# Path → label rules for actions/labeler@v5 (PR file paths).
+
+"area/tui":
+  - changed-files:
+      - any-glob-to-any-file:
+          - tui/**
+          - view/**
+
+"area/fetcher":
+  - changed-files:
+      - any-glob-to-any-file:
+          - fetcher/**
+
+"area/sender":
+  - changed-files:
+      - any-glob-to-any-file:
+          - sender/**
+
+"area/oauth":
+  - changed-files:
+      - any-glob-to-any-file:
+          - fetcher/xoauth2.go
+          - "**/oauth*"
+          - "**/xoauth*"
+
+"area/calendar":
+  - changed-files:
+      - any-glob-to-any-file:
+          - calendar/**
+
+"area/notify":
+  - changed-files:
+      - any-glob-to-any-file:
+          - notify/**
+
+"area/pgp":
+  - changed-files:
+      - any-glob-to-any-file:
+          - pgp/**
+
+"area/plugin":
+  - changed-files:
+      - any-glob-to-any-file:
+          - plugin/**
+          - plugins/**
+
+"area/theme":
+  - changed-files:
+      - any-glob-to-any-file:
+          - theme/**
+
+"area/i18n":
+  - changed-files:
+      - any-glob-to-any-file:
+          - i18n/**
+
+"area/config":
+  - changed-files:
+      - any-glob-to-any-file:
+          - config/**
+
+"area/cli":
+  - changed-files:
+      - any-glob-to-any-file:
+          - cli/**
+
+"area/daemon":
+  - changed-files:
+      - any-glob-to-any-file:
+          - daemon/**
+          - daemonclient/**
+          - daemonrpc/**
+
+"area/scard":
+  - changed-files:
+      - any-glob-to-any-file:
+          - pgp/yubikey.go
+          - "**/scard*"
+          - "**/pkcs11*"
+
+"area/nix":
+  - changed-files:
+      - any-glob-to-any-file:
+          - flake.nix
+          - flake.lock
+          - gomod2nix.toml
+
+"area/build":
+  - changed-files:
+      - any-glob-to-any-file:
+          - Makefile
+          - dist/**
+          - snapcraft.yaml
+          - com.floatpane.matcha.yaml
+          - .goreleaser*
+          - clib/**
+
+"area/docs":
+  - changed-files:
+      - any-glob-to-any-file:
+          - docs/**
+          - README.md
+          - "**/*.md"
+
+"ci":
+  - changed-files:
+      - any-glob-to-any-file:
+          - .github/workflows/**
+          - .github/labeler-config.yml
+          - .github/labels.yml
+          - .github/pr-paths.yml
+
+"dependencies":
+  - changed-files:
+      - any-glob-to-any-file:
+          - go.mod
+          - go.sum
+          - package.json
+          - package-lock.json
+          - docs/package.json
+          - docs/package-lock.json

.github/workflows/demo.yml 🔗

@@ -87,8 +87,14 @@ jobs:
           commit-message: "docs: update demo.gif from release"
           title: "docs: update demo.gif"
           body: |
-            This PR updates the demo GIF based on the latest release version.
-            Generated automatically by the Update Demo VHS workflow.
+            ## What?
+
+            Replaces `public/assets/demo.gif` with a freshly recorded VHS run.
+
+            ## Why?
+
+            Keeps the demo GIF aligned with the latest release so README and docs reflect current behaviour. Generated automatically by the Update Demo VHS workflow.
           branch: "update-demo-gif"
           base: "master"
           add-paths: public/assets/demo.gif
+          labels: documentation

.github/workflows/feature-popularity.yml 🔗

@@ -0,0 +1,63 @@
+name: "Feature Request Triage"
+
+on:
+  schedule:
+    - cron: "0 3 * * 1"
+  issues:
+    types: [opened, labeled]
+  workflow_dispatch:
+
+permissions:
+  issues: write
+
+jobs:
+  welcome:
+    if: github.event_name == 'issues' && github.event.action == 'opened' && contains(github.event.issue.labels.*.name, 'enhancement')
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/github-script@v7
+        with:
+          github-token: ${{ secrets.HOMEBREW_GITHUB_TOKEN }}
+          script: |
+            await github.rest.issues.createComment({
+              owner: context.repo.owner,
+              repo: context.repo.repo,
+              issue_number: context.payload.issue.number,
+              body: [
+                "Thanks for the suggestion!",
+                "",
+                "If you want to see this feature land, give the original post a 👍. Maintainers prioritize feature requests by reaction count.",
+                "Comment to add detail or use cases — extra context helps triage."
+              ].join("\n")
+            });
+
+  promote-popular:
+    if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/github-script@v7
+        with:
+          github-token: ${{ secrets.HOMEBREW_GITHUB_TOKEN }}
+          script: |
+            const threshold = 10;
+            const issues = await github.paginate(github.rest.issues.listForRepo, {
+              owner: context.repo.owner,
+              repo: context.repo.repo,
+              state: 'open',
+              labels: 'enhancement',
+              per_page: 100
+            });
+            for (const issue of issues) {
+              if (issue.pull_request) continue;
+              if (issue.labels.some(l => (l.name || l) === 'popular')) continue;
+              const upvotes = (issue.reactions && issue.reactions['+1']) || 0;
+              if (upvotes >= threshold) {
+                await github.rest.issues.addLabels({
+                  owner: context.repo.owner,
+                  repo: context.repo.repo,
+                  issue_number: issue.number,
+                  labels: ['popular']
+                });
+                core.info(`labeled #${issue.number} popular (${upvotes} 👍)`);
+              }
+            }

.github/workflows/label-sync.yml 🔗

@@ -0,0 +1,64 @@
+name: "Label Sync"
+
+on:
+  push:
+    branches: [master]
+    paths:
+      - .github/labels.yml
+      - .github/workflows/label-sync.yml
+  workflow_dispatch:
+    inputs:
+      prune:
+        description: "Delete labels not in labels.yml"
+        type: boolean
+        default: false
+
+permissions:
+  contents: read
+  issues: write
+  pull-requests: write
+
+jobs:
+  sync:
+    runs-on: ubuntu-latest
+    env:
+      GH_TOKEN: ${{ secrets.HOMEBREW_GITHUB_TOKEN }}
+      LABELS_FILE: .github/labels.yml
+      PRUNE: ${{ github.event.inputs.prune || 'false' }}
+    steps:
+      - uses: actions/checkout@v6
+
+      - name: Upsert labels
+        run: |
+          set -euo pipefail
+          count=$(yq '. | length' "$LABELS_FILE")
+          for i in $(seq 0 $((count - 1))); do
+            name=$(yq ".[$i].name" "$LABELS_FILE")
+            color=$(yq ".[$i].color" "$LABELS_FILE")
+            description=$(yq ".[$i].description // \"\"" "$LABELS_FILE")
+            echo "::group::$name"
+            gh label create "$name" \
+              --repo "$GITHUB_REPOSITORY" \
+              --color "$color" \
+              --description "$description" \
+              --force
+            echo "::endgroup::"
+          done
+
+      - name: Prune extra labels
+        if: env.PRUNE == 'true'
+        run: |
+          set -euo pipefail
+          desired=$(yq -r '.[].name' "$LABELS_FILE" | sort -u)
+          current=$(gh label list --repo "$GITHUB_REPOSITORY" --limit 500 --json name -q '.[].name' | sort -u)
+          extras=$(comm -23 <(echo "$current") <(echo "$desired"))
+          if [ -z "$extras" ]; then
+            echo "No extra labels."
+            exit 0
+          fi
+          echo "Deleting:"
+          echo "$extras"
+          while IFS= read -r name; do
+            [ -z "$name" ] && continue
+            gh label delete "$name" --repo "$GITHUB_REPOSITORY" --yes
+          done <<< "$extras"

.github/workflows/labeler-backfill.yml 🔗

@@ -0,0 +1,72 @@
+name: "Labeler Backfill"
+
+on:
+  workflow_dispatch:
+    inputs:
+      target:
+        description: "What to backfill"
+        type: choice
+        default: both
+        options:
+          - issues
+          - prs
+          - both
+
+permissions:
+  issues: write
+  pull-requests: write
+  contents: read
+
+jobs:
+  issues:
+    if: github.event.inputs.target == 'issues' || github.event.inputs.target == 'both'
+    runs-on: ubuntu-latest
+    env:
+      GH_TOKEN: ${{ secrets.HOMEBREW_GITHUB_TOKEN }}
+    steps:
+      - name: Re-trigger issue labeler via no-op edit
+        run: |
+          set -euo pipefail
+          mapfile -t numbers < <(gh issue list --repo "$GITHUB_REPOSITORY" --state open --limit 1000 --json number -q '.[].number')
+          for n in "${numbers[@]}"; do
+            body=$(gh issue view "$n" --repo "$GITHUB_REPOSITORY" --json body -q .body)
+            # PATCH with same body → fires `edited` → labeler.yml runs
+            gh api -X PATCH "repos/$GITHUB_REPOSITORY/issues/$n" -f body="$body" >/dev/null
+            echo "touched #$n"
+            sleep 1
+          done
+
+  prs:
+    if: github.event.inputs.target == 'prs' || github.event.inputs.target == 'both'
+    runs-on: ubuntu-latest
+    env:
+      GH_TOKEN: ${{ secrets.HOMEBREW_GITHUB_TOKEN }}
+    steps:
+      - uses: actions/checkout@v6
+
+      - name: Collect open PR numbers
+        id: prs
+        run: |
+          nums=$(gh pr list --repo "$GITHUB_REPOSITORY" --state open --limit 1000 --json number -q '[.[].number] | join(",")')
+          echo "list=$nums" >> "$GITHUB_OUTPUT"
+
+      - name: Path labeler on open PRs
+        if: steps.prs.outputs.list != ''
+        uses: actions/labeler@v5
+        with:
+          repo-token: ${{ secrets.HOMEBREW_GITHUB_TOKEN }}
+          configuration-path: .github/pr-paths.yml
+          sync-labels: false
+          dot: true
+          pr-number: ${{ steps.prs.outputs.list }}
+
+      - name: Re-trigger text labeler on PRs via no-op edit
+        run: |
+          set -euo pipefail
+          mapfile -t numbers < <(gh pr list --repo "$GITHUB_REPOSITORY" --state open --limit 1000 --json number -q '.[].number')
+          for n in "${numbers[@]}"; do
+            body=$(gh pr view "$n" --repo "$GITHUB_REPOSITORY" --json body -q .body)
+            gh api -X PATCH "repos/$GITHUB_REPOSITORY/pulls/$n" -f body="$body" >/dev/null
+            echo "touched PR #$n"
+            sleep 1
+          done

.github/workflows/labeler.yml 🔗

@@ -8,6 +8,10 @@ on:
 
 jobs:
   triage:
+    if: >
+      ${{ github.event_name == 'issues'
+          || (!startsWith(github.event.pull_request.title, 'chore(deps):')
+              && !contains(fromJSON('["chore/sync-gomod2nix","chore/update-flake-lock","update-screenshots","update-demo-gif"]'), github.head_ref)) }}
     permissions:
       contents: read
       issues: write
@@ -19,7 +23,7 @@ jobs:
       - name: Apply Labels
         uses: github/issue-labeler@v3.4
         with:
-          repo-token: "${{ secrets.GITHUB_TOKEN }}"
+          repo-token: "${{ secrets.HOMEBREW_GITHUB_TOKEN }}"
           configuration-path: .github/labeler-config.yml
           enable-versioned-regex: 0
           include-title: 1

.github/workflows/needs-response.yml 🔗

@@ -0,0 +1,45 @@
+name: "Needs Response"
+
+on:
+  issue_comment:
+    types: [created]
+
+permissions:
+  issues: write
+
+jobs:
+  toggle-label:
+    if: github.event.issue.pull_request == null
+    runs-on: ubuntu-latest
+    steps:
+      - name: Add needs-response when maintainer comments
+        if: >
+          contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association) &&
+          github.event.comment.user.login != github.event.issue.user.login
+        uses: actions/github-script@v7
+        with:
+          github-token: ${{ secrets.HOMEBREW_GITHUB_TOKEN }}
+          script: |
+            await github.rest.issues.addLabels({
+              owner: context.repo.owner,
+              repo: context.repo.repo,
+              issue_number: context.payload.issue.number,
+              labels: ['needs-response']
+            });
+
+      - name: Remove needs-response when issue author replies
+        if: github.event.comment.user.login == github.event.issue.user.login
+        uses: actions/github-script@v7
+        with:
+          github-token: ${{ secrets.HOMEBREW_GITHUB_TOKEN }}
+          script: |
+            try {
+              await github.rest.issues.removeLabel({
+                owner: context.repo.owner,
+                repo: context.repo.repo,
+                issue_number: context.payload.issue.number,
+                name: 'needs-response'
+              });
+            } catch (e) {
+              if (e.status !== 404) throw e;
+            }

.github/workflows/pr-labeler.yml 🔗

@@ -0,0 +1,23 @@
+name: "PR Path Labeler"
+
+on:
+  pull_request_target:
+    types: [opened, synchronize, reopened, ready_for_review]
+
+permissions:
+  contents: read
+  pull-requests: write
+
+jobs:
+  label:
+    if: >
+      ${{ !startsWith(github.event.pull_request.title, 'chore(deps):')
+          && !contains(fromJSON('["chore/sync-gomod2nix","chore/update-flake-lock","update-screenshots","update-demo-gif"]'), github.head_ref) }}
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/labeler@v5
+        with:
+          repo-token: ${{ secrets.HOMEBREW_GITHUB_TOKEN }}
+          configuration-path: .github/pr-paths.yml
+          sync-labels: false
+          dot: true

.github/workflows/screenshots.yml 🔗

@@ -124,9 +124,9 @@ jobs:
       - name: Generate PR Body
         id: pr-body
         run: |
-          BODY="This PR updates the feature screenshots based on the latest release version.
+          BODY="## What?
 
-          Generated automatically by the Generate Screenshots workflow.
+          Refreshes the feature screenshots in \`docs/docs/assets/features/\`.
 
           ### Screenshots included:"
 
@@ -137,6 +137,12 @@ jobs:
           - \`$filename\`"
           done
 
+          BODY="$BODY
+
+          ## Why?
+
+          Keeps documentation visuals aligned with the current TUI. Generated automatically by the Generate Screenshots workflow on the latest release."
+
           echo "body<<EOF" >> $GITHUB_OUTPUT
           echo "$BODY" >> $GITHUB_OUTPUT
           echo "EOF" >> $GITHUB_OUTPUT
@@ -151,3 +157,4 @@ jobs:
           branch: "update-screenshots"
           base: "master"
           add-paths: docs/docs/assets/features/
+          labels: documentation

.github/workflows/stale.yml 🔗

@@ -0,0 +1,37 @@
+name: "Stale issues and PRs"
+
+on:
+  schedule:
+    - cron: "0 2 * * *"
+  workflow_dispatch:
+
+permissions:
+  issues: write
+  pull-requests: write
+
+jobs:
+  stale:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/stale@v9
+        with:
+          repo-token: ${{ secrets.HOMEBREW_GITHUB_TOKEN }}
+          days-before-issue-stale: 30
+          days-before-issue-close: 14
+          days-before-pr-stale: 45
+          days-before-pr-close: 21
+          stale-issue-label: "stale"
+          stale-pr-label: "stale"
+          exempt-issue-labels: "pinned,security,help-wanted,good-first-issue,in-progress"
+          exempt-pr-labels: "pinned,security,in-progress,blocked"
+          stale-issue-message: >
+            This issue has had no activity for 30 days. It will be closed in 14 days unless updated.
+            Add a comment or remove the `stale` label to keep it open.
+          stale-pr-message: >
+            This PR has had no activity for 45 days. It will be closed in 21 days unless updated.
+          close-issue-message: >
+            Closing due to inactivity. Reopen if still relevant.
+          close-pr-message: >
+            Closing due to inactivity. Reopen if still relevant.
+          remove-stale-when-updated: true
+          operations-per-run: 100

.github/workflows/sync-gomod2nix.yml 🔗

@@ -39,5 +39,15 @@ jobs:
           git commit -m "chore: sync gomod2nix.toml"
           git push -f origin "$BRANCH"
           if ! gh pr list --head "$BRANCH" --state open | grep -q .; then
-            gh pr create --title "chore: sync gomod2nix.toml" --body "Automated gomod2nix.toml sync after Go dependency changes." --base master
+            BODY=$(cat <<'EOF'
+          ## What?
+
+          Regenerates `gomod2nix.toml` to reflect the current `go.mod` / `go.sum`.
+
+          ## Why?
+
+          Keeps the Nix build in sync with Go module changes. Without this, `nix build` fails when new or upgraded Go deps are missing from `gomod2nix.toml`. Generated automatically by the gomod2nix sync workflow.
+          EOF
+          )
+            gh pr create --title "chore: sync gomod2nix.toml" --body "$BODY" --base master --label chore --label area/nix
           fi

.github/workflows/update-flake.yml 🔗

@@ -34,5 +34,15 @@ jobs:
           git commit -m "chore: update flake.lock"
           git push -f origin "$BRANCH"
           if ! gh pr list --head "$BRANCH" --state open | grep -q .; then
-            gh pr create --title "chore: update flake.lock" --body "Automated flake.lock update." --base master
+            BODY=$(cat <<'EOF'
+          ## What?
+
+          Updates `flake.lock` to the latest revisions of all flake inputs (`nixpkgs`, `flake-utils`, etc.).
+
+          ## Why?
+
+          Keeps Nix inputs current so contributors and CI build against fresh `nixpkgs`. Picks up upstream security and toolchain fixes. Generated automatically by the flake-lock update workflow on changes to `go.sum`.
+          EOF
+          )
+            gh pr create --title "chore: update flake.lock" --body "$BODY" --base master --label chore --label area/nix
           fi

renovate.json 🔗

@@ -2,6 +2,10 @@
   "$schema": "https://docs.renovatebot.com/renovate-schema.json",
   "extends": ["config:recommended"],
   "postUpdateOptions": ["gomodTidy"],
+  "labels": ["dependencies"],
+  "commitMessage": "chore(deps): {{depName}} ^ {{newVersion}}",
+  "prTitle": "chore(deps): {{depName}} ^ {{newVersion}}",
+  "prBodyTemplate": "## What?\n\n{{{table}}}{{{notes}}}{{{changelogs}}}\n\n## Why?\n\nAutomated dependency update via Renovate.\n\n{{{configDescription}}}\n\n{{{warnings}}}\n\n---\n\n{{{controls}}}\n\n{{{footer}}}",
   "packageRules": [
     {
       "matchManagers": ["gomod"],