From 57c684c6e6071d9df4f4609fda7c6f366a97f3af Mon Sep 17 00:00:00 2001 From: drew Date: Tue, 26 May 2026 23:11:41 +0400 Subject: [PATCH 1/2] ci: labeler Signed-off-by: drew --- .github/labeler-config-pr.yml | 29 +++++++ .github/labeler-config.yml | 49 ++++++++++++ .github/labels.yml | 139 ++++++++++++++++++++++++++++++++++ .github/pr-paths.yml | 73 ++++++++++++++++++ .github/workflows/labeler.yml | 38 ++++++++++ .github/workflows/release.yml | 6 +- 6 files changed, 331 insertions(+), 3 deletions(-) create mode 100644 .github/labeler-config-pr.yml create mode 100644 .github/labeler-config.yml create mode 100644 .github/labels.yml create mode 100644 .github/pr-paths.yml create mode 100644 .github/workflows/labeler.yml diff --git a/.github/labeler-config-pr.yml b/.github/labeler-config-pr.yml new file mode 100644 index 0000000..fc7cfe7 --- /dev/null +++ b/.github/labeler-config-pr.yml @@ -0,0 +1,29 @@ +# Keyword regex labels for PRs only. No area/* — pr-paths.yml handles those. + +"bug": '\b([Bb]ug(s)?|[Ff]ix(es)?)\b' + +"enhancement": '\b([Ff]eat(s)?)\b' + +"documentation": '\b([Dd]ocumentation|[Dd]ocs|[Rr]eadme)\b' + +"question": '\b([Qq]uestion(s)?|[Hh]elp|[Hh]ow to)\b' + +"performance": '\b([Pp]erformance|[Oo]ptimiz(e|ation)|[Ss]peed|[Ee]fficiency)\b' + +"dependencies": '([Cc]hore\(deps\)|[Ff]ix\(deps\))' + +"ci": '\b([Cc]i|[Cc]ontinuous integration|[Bb]uild|[Ww]orkflow)\b' + +"chore": '\b([Cc]hore|[Mm]aintenance|[Rr]efactor|[Cc]leanup)\b' + +"security": '\b([Ss]ecurity|[Ss]andbox(ed)?|[Ll]andlock|[Ss]eccomp|[Vv]ulnerabilit(y|ies)|[Cc][Vv][Ee])\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' diff --git a/.github/labeler-config.yml b/.github/labeler-config.yml new file mode 100644 index 0000000..8a82697 --- /dev/null +++ b/.github/labeler-config.yml @@ -0,0 +1,49 @@ +# Keyword regex labels for issues. PRs use labeler-config-pr.yml + pr-paths.yml. + +"bug": '\b([Bb]ug(s)?|[Ff]ix(es)?)\b' + +"enhancement": '\b([Ff]eat(s)?)\b' + +"documentation": '\b([Dd]ocumentation|[Dd]ocs|[Rr]eadme)\b' + +"question": '\b([Qq]uestion(s)?|[Hh]elp|[Hh]ow to)\b' + +"performance": '\b([Pp]erformance|[Oo]ptimiz(e|ation)|[Ss]peed|[Ee]fficiency)\b' + +"dependencies": '([Cc]hore\(deps\)|[Ff]ix\(deps\))' + +"ci": '\b([Cc]i|[Cc]ontinuous integration|[Bb]uild|[Ww]orkflow)\b' + +"chore": '\b([Cc]hore|[Mm]aintenance|[Rr]efactor|[Cc]leanup)\b' + +"security": '\b([Ss]ecurity|[Ss]andbox(ed)?|[Ll]andlock|[Ss]eccomp|[Vv]ulnerabilit(y|ies)|[Cc][Vv][Ee])\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/render": '\b([Rr]ender(ing|er)?|[Hh]alf[- ]?block|▀)\b' + +"area/protocol/kitty": '\b([Kk]itty( [Gg]raphics)?|[Gg]hostty|[Ww]ez[Tt]erm)\b' + +"area/protocol/sixel": '\b([Ss]ixel|[Ff]oot|[Mm]lterm|[Cc]ontour)\b' + +"area/protocol/halfblock": '\b([Hh]alf[- ]?block|U\+2580|Unicode block)\b' + +"area/decode": '\b([Dd]ecod(e|er|ing)|stb_image|stbi|[Jj]peg|[Pp]ng|[Gg]if|[Bb]mp|[Tt]ga|[Hh]dr|[Pp]sd|[Pp]nm)\b' + +"area/detect": '\b([Dd]etect(ion)?|[Pp]rotocol detect|TERM|TERM_PROGRAM|KITTY_WINDOW_ID)\b' + +"area/sandbox": '\b([Ss]andbox(ed)?|[Ll]andlock|[Ss]eccomp|[Ww]orker|[Ss]ubprocess)\b' + +"area/resize": '\b([Rr]esiz(e|ing)|[Ss]cal(e|ing)|[Bb]ilinear|[Aa]spect ratio)\b' + +"area/docs": '\b([Dd]ocumentation|[Dd]ocs|[Rr]eadme|[Mm]an ?page)\b' + +"area/build": '\b([Bb]uild|[Mm]akefile|[Gg]oreleaser|[Cc]go|[Cc][Gg][Oo])\b' diff --git a/.github/labels.yml b/.github/labels.yml new file mode 100644 index 0000000..5b888e0 --- /dev/null +++ b/.github/labels.yml @@ -0,0 +1,139 @@ +# 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 +- name: ci-failure + color: b60205 + description: Tracked CI failure on master + +# --- 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/render + color: c2e0c6 + description: Rendering pipeline +- name: area/protocol/kitty + color: c2e0c6 + description: Kitty graphics protocol +- name: area/protocol/sixel + color: c2e0c6 + description: DEC Sixel protocol +- name: area/protocol/halfblock + color: c2e0c6 + description: Unicode half-block fallback +- name: area/decode + color: c2e0c6 + description: Image decoder (stb_image / CGo) +- name: area/detect + color: c2e0c6 + description: Terminal capability detection +- name: area/sandbox + color: c2e0c6 + description: Sandbox worker / Landlock / seccomp +- name: area/resize + color: c2e0c6 + description: Image resize / scaling +- name: area/docs + color: c2e0c6 + description: Docs site / README +- name: area/build + color: c2e0c6 + description: Build / release / packaging + +# --- Size --- +- name: size/XS + color: "3cbf00" + description: "Diff: 0–10 lines" +- name: size/S + color: "5d9801" + description: "Diff: 11–50 lines" +- name: size/M + color: "eebb00" + description: "Diff: 51–200 lines" +- name: size/L + color: "ee9900" + description: "Diff: 201–800 lines" +- name: size/XL + color: "ee5500" + description: "Diff: 800+ lines" diff --git a/.github/pr-paths.yml b/.github/pr-paths.yml new file mode 100644 index 0000000..265346a --- /dev/null +++ b/.github/pr-paths.yml @@ -0,0 +1,73 @@ +# Path → label rules for actions/labeler@v6 (PR file paths). + +"area/render": + - changed-files: + - any-glob-to-any-file: + - render/** + +"area/protocol/kitty": + - changed-files: + - any-glob-to-any-file: + - render/kitty.go + +"area/protocol/sixel": + - changed-files: + - any-glob-to-any-file: + - render/sixel.go + - render/quantize.go + +"area/protocol/halfblock": + - changed-files: + - any-glob-to-any-file: + - render/halfblock.go + +"area/decode": + - changed-files: + - any-glob-to-any-file: + - decode/** + +"area/detect": + - changed-files: + - any-glob-to-any-file: + - detect/** + +"area/sandbox": + - changed-files: + - any-glob-to-any-file: + - sandbox/** + +"area/resize": + - changed-files: + - any-glob-to-any-file: + - internal/resize/** + +"area/docs": + - changed-files: + - any-glob-to-any-file: + - docs/** + - README.md + - "**/*.md" + +"area/build": + - changed-files: + - any-glob-to-any-file: + - .goreleaser*.yml + - .golangci.yml + +"ci": + - changed-files: + - any-glob-to-any-file: + - .github/workflows/** + - .github/labeler-config.yml + - .github/labeler-config-pr.yml + - .github/labels.yml + - .github/pr-paths.yml + - .github/release.yml + +"dependencies": + - changed-files: + - any-glob-to-any-file: + - go.mod + - go.sum + - docs/package.json + - docs/package-lock.json diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml new file mode 100644 index 0000000..016b41a --- /dev/null +++ b/.github/workflows/labeler.yml @@ -0,0 +1,38 @@ +name: "Labeler" + +on: + issues: + types: [opened, edited] + pull_request_target: + types: [opened, edited] + +jobs: + triage: + if: > + ${{ github.event_name == 'issues' + || !startsWith(github.event.pull_request.title, 'chore(deps):') }} + permissions: + contents: read + issues: write + pull-requests: write + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Apply Labels (issues — includes area/*) + if: github.event_name == 'issues' + uses: github/issue-labeler@v3.4 + with: + repo-token: "${{ secrets.BOT_GITHUB_TOKEN }}" + configuration-path: .github/labeler-config.yml + enable-versioned-regex: 0 + include-title: 1 + + - name: Apply Labels (PRs — no area/*, paths handle those) + if: github.event_name == 'pull_request_target' + uses: github/issue-labeler@v3.4 + with: + repo-token: "${{ secrets.BOT_GITHUB_TOKEN }}" + configuration-path: .github/labeler-config-pr.yml + enable-versioned-regex: 0 + include-title: 1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f52fc66..59e7350 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,12 +26,12 @@ jobs: id: tag_version uses: mathieudutour/github-tag-action@v6.2 with: - github_token: ${{ secrets.GITHUB_TOKEN }} + github_token: ${{ secrets.BOT_GITHUB_TOKEN }} - name: Fetch the new tag run: git fetch --tags env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.BOT_GITHUB_TOKEN }} - name: Run GoReleaser if: steps.tag_version.outputs.new_tag @@ -40,4 +40,4 @@ jobs: version: latest args: release --clean env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.BOT_GITHUB_TOKEN }} From 5ada6ce464dec4cb9828971a4c28dd7f738d3e9b Mon Sep 17 00:00:00 2001 From: drew Date: Tue, 26 May 2026 23:32:55 +0400 Subject: [PATCH 2/2] labeler + pr size + renovate + stale Signed-off-by: drew --- .github/workflows/label-sync.yml | 64 +++++++++++++++++ .github/workflows/pr-size.yml | 21 ++++++ .github/workflows/renovate.yml | 20 ++++++ .github/workflows/stale.yml | 120 +++++++++++++++++++++++++++++++ renovate.json | 21 ++++++ 5 files changed, 246 insertions(+) create mode 100644 .github/workflows/label-sync.yml create mode 100644 .github/workflows/pr-size.yml create mode 100644 .github/workflows/renovate.yml create mode 100644 .github/workflows/stale.yml create mode 100644 renovate.json diff --git a/.github/workflows/label-sync.yml b/.github/workflows/label-sync.yml new file mode 100644 index 0000000..e630e45 --- /dev/null +++ b/.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" diff --git a/.github/workflows/pr-size.yml b/.github/workflows/pr-size.yml new file mode 100644 index 0000000..f66f450 --- /dev/null +++ b/.github/workflows/pr-size.yml @@ -0,0 +1,21 @@ +name: PR Size Label + +on: + pull_request_target: + types: [opened, synchronize, reopened] + +permissions: + pull-requests: write + +jobs: + label: + runs-on: ubuntu-latest + steps: + - uses: codelytv/pr-size-labeler@v1 + with: + GITHUB_TOKEN: ${{ secrets.BOT_GITHUB_TOKEN }} + xs_max_size: 10 + s_max_size: 50 + m_max_size: 200 + l_max_size: 800 + fail_if_xl: false diff --git a/.github/workflows/renovate.yml b/.github/workflows/renovate.yml new file mode 100644 index 0000000..76210fd --- /dev/null +++ b/.github/workflows/renovate.yml @@ -0,0 +1,20 @@ +name: Renovate +on: + schedule: + - cron: "0 * * * *" + workflow_dispatch: +jobs: + renovate: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + - name: Run Renovate + uses: renovatebot/github-action@v46.1.12 + with: + token: ${{ secrets.BOT_GITHUB_TOKEN }} + configurationFile: renovate.json + env: + RENOVATE_REPOSITORIES: floatpane/matcha diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 0000000..dd251a5 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,120 @@ +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/github-script@v9 + with: + github-token: ${{ secrets.BOT_GITHUB_TOKEN }} + script: | + const IGNORED_AUTHORS = new Set(['floatpanebot', 'github-actions[bot]', 'renovate[bot]', 'dependabot[bot]']); + const STALE_LABEL = 'stale'; + const EXEMPT_ISSUE = ['pinned', 'security', 'help-wanted', 'good-first-issue', 'in-progress']; + const EXEMPT_PR = ['pinned', 'security', 'in-progress', 'blocked']; + const ISSUE_STALE_DAYS = 30; + const ISSUE_CLOSE_DAYS = 14; + const PR_STALE_DAYS = 45; + const PR_CLOSE_DAYS = 21; + const OPS_LIMIT = 100; + + const STALE_MSG = (days, close) => + `This has had no activity for ${days} days. It will be closed in ${close} days unless updated. ` + + `Comment or remove the \`stale\` label to keep it open.`; + const CLOSE_MSG = 'Closing due to inactivity. Reopen if still relevant.'; + + const now = Date.now(); + let ops = 0; + + async function lastHumanActivity(issue) { + let latest = new Date(issue.created_at).getTime(); + const comments = await github.paginate(github.rest.issues.listComments, { + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + per_page: 100, + }); + for (const c of comments) { + if (c.user && IGNORED_AUTHORS.has(c.user.login)) continue; + const t = new Date(c.created_at).getTime(); + if (t > latest) latest = t; + } + if (issue.pull_request) { + const commits = await github.paginate(github.rest.pulls.listCommits, { + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: issue.number, + per_page: 100, + }); + for (const c of commits) { + const t = new Date(c.commit.author.date).getTime(); + if (t > latest) latest = t; + } + } + return latest; + } + + const items = await github.paginate(github.rest.issues.listForRepo, { + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', + per_page: 100, + }); + + for (const item of items) { + if (ops >= OPS_LIMIT) { core.info('ops limit hit, stopping'); break; } + const isPR = !!item.pull_request; + const exempt = isPR ? EXEMPT_PR : EXEMPT_ISSUE; + const labels = item.labels.map(l => l.name || l); + if (exempt.some(l => labels.includes(l))) continue; + + const staleDays = isPR ? PR_STALE_DAYS : ISSUE_STALE_DAYS; + const closeDays = isPR ? PR_CLOSE_DAYS : ISSUE_CLOSE_DAYS; + const isStale = labels.includes(STALE_LABEL); + + const lastTs = await lastHumanActivity(item); + const ageDays = (now - lastTs) / 86400000; + + if (!isStale && ageDays >= staleDays) { + await github.rest.issues.addLabels({ + owner: context.repo.owner, repo: context.repo.repo, + issue_number: item.number, labels: [STALE_LABEL], + }); + await github.rest.issues.createComment({ + owner: context.repo.owner, repo: context.repo.repo, + issue_number: item.number, + body: STALE_MSG(staleDays, closeDays), + }); + core.info(`marked #${item.number} stale (age ${ageDays.toFixed(1)}d)`); + ops++; + } else if (isStale && ageDays < staleDays) { + try { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, repo: context.repo.repo, + issue_number: item.number, name: STALE_LABEL, + }); + core.info(`unstaled #${item.number} (real activity ${ageDays.toFixed(1)}d ago)`); + ops++; + } catch (e) { if (e.status !== 404) throw e; } + } else if (isStale && ageDays >= staleDays + closeDays) { + await github.rest.issues.createComment({ + owner: context.repo.owner, repo: context.repo.repo, + issue_number: item.number, body: CLOSE_MSG, + }); + await github.rest.issues.update({ + owner: context.repo.owner, repo: context.repo.repo, + issue_number: item.number, state: 'closed', state_reason: 'not_planned', + }); + core.info(`closed #${item.number} (age ${ageDays.toFixed(1)}d)`); + ops++; + } + } diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..f928fe5 --- /dev/null +++ b/renovate.json @@ -0,0 +1,21 @@ +{ + "$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"], + "matchDepNames": ["go"], + "rangeStrategy": "bump" + }, + { + "matchManagers": ["github-actions"], + "matchDepNames": ["go"], + "groupName": "go" + } + ] +}