diff --git a/.github/labels.yml b/.github/labels.yml index 2ce04130..a5a43fba 100644 --- a/.github/labels.yml +++ b/.github/labels.yml @@ -21,7 +21,7 @@ # area/ subsystem; extensible — add when 3+ open issues exist # do-not-merge/ PR merge blockers # security/ security-finding severity and status (Cozystack-specific) -# size: PR size (auto-applied) +# size/ PR size (auto-applied) # # `aliases:` lets EndBug/label-sync rename existing labels without losing # references on already-tagged issues and PRs. @@ -295,32 +295,38 @@ description: Indicates a non-member PR is safe to run CI on # ────────────────────────────────────────────── -# size: — PR size (auto-applied by sizing bot) +# size/ — PR size (auto-applied by .github/workflows/pr-size.yaml) # ────────────────────────────────────────────── -- name: 'size:XS' +- name: size/XS color: '00ff00' description: This PR changes 0-9 lines, ignoring generated files + aliases: ['size:XS'] -- name: 'size:S' +- name: size/S color: '77b800' description: This PR changes 10-29 lines, ignoring generated files + aliases: ['size:S'] -- name: 'size:M' +- name: size/M color: 'ebb800' description: This PR changes 30-99 lines, ignoring generated files + aliases: ['size:M'] -- name: 'size:L' +- name: size/L color: 'eb9500' description: This PR changes 100-499 lines, ignoring generated files + aliases: ['size:L'] -- name: 'size:XL' +- name: size/XL color: 'ff823f' description: This PR changes 500-999 lines, ignoring generated files + aliases: ['size:XL'] -- name: 'size:XXL' +- name: size/XXL color: 'ffb8b8' description: This PR changes 1000+ lines, ignoring generated files + aliases: ['size:XXL'] # ────────────────────────────────────────────── # security/ — security-finding severity and status diff --git a/.github/workflows/pr-size.yaml b/.github/workflows/pr-size.yaml new file mode 100644 index 00000000..99b27e03 --- /dev/null +++ b/.github/workflows/pr-size.yaml @@ -0,0 +1,93 @@ +name: PR size label + +on: + pull_request_target: + types: [opened, synchronize, reopened] + +permissions: + contents: read + pull-requests: write + +concurrency: + group: pr-size-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + size: + runs-on: ubuntu-latest + steps: + - uses: actions/github-script@v7 + with: + script: | + const pr = context.payload.pull_request; + + // Skip files that should not count as "real diff" — they are produced by tools, + // not authored by humans, and including them lies to reviewers about review effort: + // - vendored Go deps, + // - kubebuilder/openapi inline generated files (zz_generated.{deepcopy,conversion,defaults,openapi}.go), + // - whole code-generated trees from client-gen / lister-gen / applyconfiguration-gen + // and protobuf (any path under a `generated/` directory or ending in `.pb.go`), + // - vendored Helm charts, + // - lockfiles. + const isIgnored = (path) => + path.startsWith('vendor/') || + /\bzz_generated\.[^/]*\.go$/.test(path) || + /(^|\/)generated\//.test(path) || + path.endsWith('.pb.go') || + /^packages\/system\/[^/]+\/charts\//.test(path) || + path === 'go.sum' || path.endsWith('/go.sum') || + path.endsWith('.lock') || path.endsWith('.lockb'); + + const files = await github.paginate(github.rest.pulls.listFiles, { + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: pr.number, + }); + const lines = files + .filter((f) => !isIgnored(f.filename)) + .reduce((acc, f) => acc + (f.additions || 0) + (f.deletions || 0), 0); + + // Thresholds match the descriptions of size/* labels in .github/labels.yml. + const bucket = + lines <= 9 ? 'XS' : + lines <= 29 ? 'S' : + lines <= 99 ? 'M' : + lines <= 499 ? 'L' : + lines <= 999 ? 'XL' : 'XXL'; + const target = `size/${bucket}`; + + // Match both legacy "size:" and canonical "size/" during the migration window. + const existing = (pr.labels || []).map((l) => l.name); + const oldSizes = existing.filter((n) => n.startsWith('size/') || n.startsWith('size:')); + const alreadyTarget = oldSizes.includes(target); + + // Remove every size/* label that is not the target. Tolerate 404 — concurrent + // runs or manual edits between event payload and execution can race here. + for (const name of oldSizes) { + if (name === target) continue; + try { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + name, + }); + } catch (e) { + if (e.status !== 404) throw e; + core.info(`label ${name} already gone (404)`); + } + } + + if (alreadyTarget && oldSizes.length === 1) { + core.info(`PR #${pr.number}: ${lines} lines, label already ${target}`); + return; + } + if (!alreadyTarget) { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + labels: [target], + }); + } + core.info(`PR #${pr.number}: ${lines} lines -> ${target}`); diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml new file mode 100644 index 00000000..17df206b --- /dev/null +++ b/.github/workflows/stale.yaml @@ -0,0 +1,56 @@ +name: Stale issues + +on: + schedule: + - cron: '37 4 * * *' # daily at 04:37 UTC + workflow_dispatch: + +permissions: + contents: read + issues: write + pull-requests: write + +concurrency: + group: stale + cancel-in-progress: false + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v10 + with: + stale-issue-label: lifecycle/stale + stale-pr-label: lifecycle/stale + close-issue-label: lifecycle/rotten + close-pr-label: lifecycle/rotten + # Exempt lists differ on purpose: issues sit longer than PRs by nature. + # An accepted issue (`triage/accepted`) is a known long-tail task, not stale; + # a held PR (`do-not-merge/hold`) is paused intentionally and shouldn't auto-close. + # Priority/security/regression labels protect issues that someone explicitly + # decided to keep visible. + exempt-issue-labels: >- + lifecycle/frozen,epic, + priority/critical-urgent,priority/important-soon,priority/important-longterm, + security/critical,security/high,security/confirmed, + security/triage-needed,security/in-progress, + triage/accepted,kind/regression + exempt-pr-labels: >- + lifecycle/frozen,do-not-merge/hold, + backport,backport-previous + days-before-stale: 60 + days-before-close: 14 + operations-per-run: 100 + remove-stale-when-updated: true + stale-issue-message: | + This issue has had no activity for 60 days and was marked `lifecycle/stale`. + It will be closed in 14 days unless commented or labelled `lifecycle/frozen`. + stale-pr-message: | + This PR has had no activity for 60 days and was marked `lifecycle/stale`. + It will be closed in 14 days unless commented or labelled `lifecycle/frozen`. + close-issue-message: | + Closed because no activity followed the `lifecycle/stale` warning. + Reopen if the issue is still relevant. + close-pr-message: | + Closed because no activity followed the `lifecycle/stale` warning. + Reopen if the change is still wanted.