diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 1a119259..fefdab82 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,7 +1,7 @@ --- name: Bug report about: Create a report to help us improve -labels: 'bug' +labels: 'kind/bug' assignees: '' --- diff --git a/.github/labels.yml b/.github/labels.yml new file mode 100644 index 00000000..1369aa48 --- /dev/null +++ b/.github/labels.yml @@ -0,0 +1,371 @@ +# Cozystack repository labels +# +# Label conventions follow the Kubernetes scheme: +# https://github.com/kubernetes/test-infra/blob/master/label_sync/labels.md +# +# Synced into the repository by .github/workflows/labels.yaml +# (EndBug/label-sync@v2). Edit this file via pull request — UI changes +# will be overwritten on the next sync. +# +# Constraints (enforced by the validate job in labels.yaml): +# - description ≤ 100 characters (GitHub REST API limit) +# - color is a 6-character hex string (no leading #) +# - label names are unique +# - aliases do not collide with top-level names +# +# Categories: +# kind/ issue or PR type +# priority/ urgency +# triage/ review state +# lifecycle/ issue or PR lifecycle +# 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) +# +# `aliases:` lets EndBug/label-sync rename existing labels without losing +# references on already-tagged issues and PRs. +# +# GitHub-default labels not migrated here (`wontfix`, `invalid`) currently +# carry zero issues/PRs in this repo and will be removed in a follow-up +# cleanup PR rather than aliased to a different namespace. + +# ────────────────────────────────────────────── +# kind/ — issue or PR type +# ────────────────────────────────────────────── + +- name: kind/bug + color: 'd73a4a' + description: Categorizes issue or PR as related to a bug + aliases: ['bug'] + +- name: kind/feature + color: 'a2eeef' + description: Categorizes issue or PR as related to a new feature + aliases: ['enhancement'] + +- name: kind/documentation + color: '0075ca' + description: Categorizes issue or PR as related to documentation + aliases: ['documentation'] + +- name: kind/support + color: 'd876e3' + description: Categorizes issue as a support question + aliases: ['question'] + +- name: kind/cleanup + color: 'c7def8' + description: Categorizes issue or PR as related to cleanup of code, process, or technical debt + +- name: kind/regression + color: 'e11d21' + description: Categorizes issue or PR as related to a regression from a prior release + +- name: kind/flake + color: 'f7c6c7' + description: Categorizes issue or PR as related to a flaky test + +- name: kind/failing-test + color: 'e11d21' + description: Categorizes issue or PR as related to a consistently or frequently failing test + +- name: kind/api-change + color: 'c7def8' + description: Categorizes issue or PR as related to adding, removing, or otherwise changing an API + +- name: kind/breaking-change + color: 'e11d21' + description: Indicates the change introduces a breaking API or behaviour change + +# ────────────────────────────────────────────── +# priority/ — urgency +# ────────────────────────────────────────────── + +- name: priority/critical-urgent + color: 'e11d21' + description: Highest priority. Must be actively worked on as someone's top priority right now + +- name: priority/important-soon + color: 'eb6420' + description: Must be staffed and worked on either currently, or very soon, ideally in time for the next release + +- name: priority/important-longterm + color: 'fbca04' + description: Important over the long term, but may not be staffed and/or may need multiple releases to complete + +- name: priority/backlog + color: 'fef2c0' + description: General backlog priority. Lower than priority/important-longterm + +# ────────────────────────────────────────────── +# triage/ — review state +# ────────────────────────────────────────────── + +- name: triage/needs-triage + color: 'ededed' + description: Indicates an issue needs triage by a maintainer + +- name: triage/accepted + color: '0e8a16' + description: Indicates an issue is ready to be actively worked on + +- name: triage/needs-information + color: 'fbca04' + description: Indicates an issue needs more information in order to work on it + +- name: triage/not-reproducible + color: 'fbca04' + description: Indicates an issue can not be reproduced as described + +- name: triage/duplicate + color: 'cfd3d7' + description: Indicates an issue is a duplicate of another issue + aliases: ['duplicate'] + +- name: triage/unresolved + color: 'cfd3d7' + description: Indicates an issue that can not or will not be resolved + +# ────────────────────────────────────────────── +# lifecycle/ — issue or PR lifecycle +# ────────────────────────────────────────────── + +- name: lifecycle/active + color: '1d76db' + description: Indicates that an issue or PR is actively being worked on by a contributor + +- name: lifecycle/frozen + color: 'db5dd6' + description: Indicates that an issue or PR should not be auto-closed due to staleness + aliases: ['frozen'] + +- name: lifecycle/stale + color: 'dadada' + description: Denotes an issue or PR has remained open with no activity and has become stale + aliases: ['stale'] + +- name: lifecycle/rotten + color: '795548' + description: Denotes an issue or PR that has aged beyond stale and will be auto-closed + +# ────────────────────────────────────────────── +# area/ — subsystem (extensible) +# Add a new area/* when there are 3+ open issues on the topic. +# ────────────────────────────────────────────── + +- name: area/api + color: 'bfd4f2' + description: Issues or PRs related to the cozystack-api aggregated API server + +- name: area/ai + color: 'bfd4f2' + description: Issues or PRs related to AI agent guides, AGENTS.md, docs/agents/ + +- name: area/build + color: 'bfd4f2' + description: Issues or PRs related to image build infrastructure, multi-arch support + +- name: area/ci + color: 'bfd4f2' + description: Issues or PRs related to CI workflows, GitHub Actions, automation + +- name: area/dashboard + color: 'bfd4f2' + description: Issues or PRs related to the dashboard / UI + +- name: area/extra + color: 'bfd4f2' + description: Issues or PRs related to tenant-specific modules (packages/extra/) + +- name: area/database + color: 'bfd4f2' + description: Issues or PRs related to managed databases (postgres, mariadb, redis, etcd, kafka, clickhouse) + +- name: area/kubernetes + color: 'bfd4f2' + description: Issues or PRs related to the tenant Kubernetes app + +- name: area/monitoring + color: 'bfd4f2' + description: Issues or PRs related to the monitoring stack (vlogs, vmstack, grafana, workloadmonitor) + +- name: area/networking + color: 'bfd4f2' + description: Issues or PRs related to networking (ingress, gateway, vpn, metallb, cilium, kube-ovn) + +- name: area/platform + color: 'bfd4f2' + description: Issues or PRs related to platform infrastructure (bundle, flux, talos, installer) + +- name: area/release + color: 'bfd4f2' + description: Issues or PRs related to release tooling (changelog, backport, release pipeline) + +- name: area/storage + color: 'bfd4f2' + description: Issues or PRs related to storage (linstor, seaweedfs, bucket, velero, harbor) + +- name: area/testing + color: 'bfd4f2' + description: Issues or PRs related to testing (e2e, bats, unit tests) + +- name: area/virtualization + color: 'bfd4f2' + description: Issues or PRs related to virtualization (kubevirt, cdi, vmi, vm-import) + +- name: area/uncategorized + color: 'fbca04' + description: PR auto-labeler could not map title scope to a known area/*; please review + +# ────────────────────────────────────────────── +# do-not-merge/ — PR merge blockers (Prow convention) +# ────────────────────────────────────────────── + +- name: do-not-merge/work-in-progress + color: 'e11d21' + description: Indicates that a PR should not merge because it is a work in progress + # Both legacy spellings collapse here. EndBug processes aliases sequentially; + # the second rename hits a name collision and logs a warning — the legacy + # label survives and gets cleaned up in the follow-up dedup PR. + aliases: ['do-not-merge', 'do not merge'] + +- name: do-not-merge/hold + color: 'e11d21' + description: Indicates that a PR should not merge because someone has issued /hold + +# ────────────────────────────────────────────── +# Cozystack-specific (preserved) +# ────────────────────────────────────────────── + +- name: epic + color: 'A335EE' + description: A large development increment that brings definite value to Cozystack users + +- name: community + color: '97458A' + description: Community contributions are welcome in this issue + +- name: help wanted + color: '008672' + description: Extra attention is needed + +- name: good first issue + color: '7057ff' + description: Good for newcomers + +- name: quality-of-life + color: 'aaaaaa' + description: QoL improvements + +- name: upstream-issue + color: 'aaaaaa' + description: Requires resolving an issue in an upstream project + +- name: backport + color: 'FBCA04' + description: Should change be backported on previous release + +- name: backport-previous + color: 'fbd876' + description: Backport target — previous release line + +- name: release + color: 'aaaaaa' + description: Releasing a new Cozystack version + +- name: automated + color: 'ededed' + description: Created by automation + +- name: debug + color: '704479' + description: Debugging in progress + +- name: sponsored + color: '00FF00' + description: Sponsored work + +- name: lgtm + color: '238636' + description: This PR has been approved by a maintainer + +- name: ok-to-test + color: '00FF00' + description: Indicates a non-member PR is safe to run CI on + +# ────────────────────────────────────────────── +# size: — PR size (auto-applied by sizing bot) +# ────────────────────────────────────────────── + +- name: 'size:XS' + color: '00ff00' + description: This PR changes 0-9 lines, ignoring generated files + +- name: 'size:S' + color: '77b800' + description: This PR changes 10-29 lines, ignoring generated files + +- name: 'size:M' + color: 'ebb800' + description: This PR changes 30-99 lines, ignoring generated files + +- name: 'size:L' + color: 'eb9500' + description: This PR changes 100-499 lines, ignoring generated files + +- name: 'size:XL' + color: 'ff823f' + description: This PR changes 500-999 lines, ignoring generated files + +- name: 'size:XXL' + color: 'ffb8b8' + description: This PR changes 1000+ lines, ignoring generated files + +# ────────────────────────────────────────────── +# security/ — security-finding severity and status +# ────────────────────────────────────────────── + +- name: security + color: 'aaaaaa' + description: Security-related issues and features + +- name: security/critical + color: 'd73a4a' + description: Critical security vulnerability + +- name: security/high + color: 'e99695' + description: High severity security finding + +- name: security/medium + color: 'f9c513' + description: Medium severity security finding + +- name: security/low + color: '0e8a16' + description: Low severity security finding + +- name: security/triage-needed + color: 'fbca04' + description: Needs security triage + +- name: security/confirmed + color: '1d76db' + description: Confirmed vulnerability + +- name: security/false-positive + color: 'c5def5' + description: Triaged as false positive + +- name: security/accepted-risk + color: 'bfd4f2' + description: Risk accepted with justification + +- name: security/in-progress + color: '0075ca' + description: Fix in progress + +- name: security/fixed + color: '0e8a16' + description: Fix released diff --git a/.github/workflows/labels.yaml b/.github/workflows/labels.yaml new file mode 100644 index 00000000..69c18329 --- /dev/null +++ b/.github/workflows/labels.yaml @@ -0,0 +1,84 @@ +name: Labels + +on: + pull_request: + paths: + - .github/labels.yml + - .github/workflows/labels.yaml + push: + branches: [main] + paths: + - .github/labels.yml + - .github/workflows/labels.yaml + workflow_dispatch: + schedule: + - cron: '17 4 * * 1' # Mondays at 04:17 UTC + +permissions: + contents: read + +concurrency: + group: labels-sync + cancel-in-progress: false + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Validate labels.yml schema + run: | + python3 - <<'PY' + import re, sys, yaml + + path = '.github/labels.yml' + data = yaml.safe_load(open(path)) + errors = [] + + # 1. description ≤ 100 chars (GitHub REST API limit) + for label in data: + desc = label.get('description', '') or '' + if len(desc) > 100: + errors.append(f"{label['name']}: description {len(desc)} chars (max 100)") + + # 2. color is 6-char hex without leading # + for label in data: + color = label.get('color', '') or '' + if not re.match(r'^[0-9A-Fa-f]{6}$', color): + errors.append(f"{label['name']}: bad color {color!r} (must be 6-char hex without #)") + + # 3. unique top-level names + names = [label['name'] for label in data] + dups = sorted({n for n in names if names.count(n) > 1}) + for n in dups: + errors.append(f"duplicate name: {n}") + + # 4. aliases do not collide with any top-level name + name_set = set(names) + for label in data: + for alias in (label.get('aliases') or []): + if alias in name_set: + errors.append(f"alias {alias!r} (under {label['name']!r}) collides with a top-level name") + + if errors: + for err in errors: + print(f"::error::{err}") + sys.exit(1) + + print(f"labels.yml schema OK ({len(data)} labels)") + PY + + sync: + needs: validate + if: github.event_name != 'pull_request' + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + steps: + - uses: actions/checkout@v4 + - uses: EndBug/label-sync@v2 + with: + config-file: .github/labels.yml + delete-other-labels: false diff --git a/.github/workflows/pr-labeler.yaml b/.github/workflows/pr-labeler.yaml new file mode 100644 index 00000000..d882bc50 --- /dev/null +++ b/.github/workflows/pr-labeler.yaml @@ -0,0 +1,199 @@ +name: PR Auto-Label + +on: + pull_request_target: + types: [opened, edited, reopened, synchronize] + +permissions: + contents: read + pull-requests: write + +jobs: + label: + runs-on: ubuntu-latest + steps: + - name: Apply labels from PR title + uses: actions/github-script@v7 + with: + script: | + // Conventional Commits types accepted by Cozystack (per docs/agents/contributing.md): + // feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert + // Mapping below maps a subset to kind/* — types not listed do not produce a kind/*. + const typeToKind = { + feat: 'kind/feature', + fix: 'kind/bug', + docs: 'kind/documentation', + chore: 'kind/cleanup', + refactor: 'kind/cleanup', + // style, perf, test, build, ci, revert — no kind mapping + }; + + // scope -> area/* mapping. Keys are the scopes observed in cozystack issues + // and PRs. Add new entries when a scope recurs (3+ times). + const scopeToArea = { + // area/api + 'api': 'area/api', + 'cozystack-api': 'area/api', + + // area/ai + 'agents': 'area/ai', + 'ai': 'area/ai', + + // area/build + 'build': 'area/build', + + // area/ci + 'ci': 'area/ci', + + // area/dashboard + 'dashboard': 'area/dashboard', + + // area/database + 'postgres': 'area/database', + 'postgres-operator': 'area/database', + 'mariadb': 'area/database', + 'mariadb-operator': 'area/database', + 'redis': 'area/database', + 'etcd': 'area/database', + 'kafka': 'area/database', + 'clickhouse': 'area/database', + + // area/extra + 'extra': 'area/extra', + + // area/kubernetes + 'kubernetes': 'area/kubernetes', + 'apps/kubernetes': 'area/kubernetes', + + // area/monitoring + 'monitoring': 'area/monitoring', + 'vlogs': 'area/monitoring', + 'vmstack': 'area/monitoring', + 'grafana': 'area/monitoring', + 'workloadmonitor': 'area/monitoring', + + // area/networking + 'ingress': 'area/networking', + 'ingress-nginx': 'area/networking', + 'gateway': 'area/networking', + 'vpn': 'area/networking', + 'metallb': 'area/networking', + 'cilium': 'area/networking', + 'kube-ovn': 'area/networking', + 'tcp-balancer': 'area/networking', + 'securitygroups': 'area/networking', + 'cozy-proxy': 'area/networking', + + // area/platform + 'platform': 'area/platform', + 'bundle': 'area/platform', + 'flux': 'area/platform', + 'fluxcd': 'area/platform', + 'cluster-api': 'area/platform', + 'talos': 'area/platform', + 'installer': 'area/platform', + 'cozyctl': 'area/platform', + 'cozystack-engine': 'area/platform', + 'cozy-lib': 'area/platform', + + // area/release + 'backport': 'area/release', + 'release': 'area/release', + + // area/storage + 'seaweedfs': 'area/storage', + 'seaweedfs-cosi-driver': 'area/storage', + 'bucket': 'area/storage', + 'linstor': 'area/storage', + 'velero': 'area/storage', + 'harbor': 'area/storage', + 'backups': 'area/storage', + + // area/testing + 'tests': 'area/testing', + 'e2e': 'area/testing', + + // area/virtualization + 'kubevirt': 'area/virtualization', + 'cdi': 'area/virtualization', + 'vmi': 'area/virtualization', + 'vm-import': 'area/virtualization', + 'virtual-machine': 'area/virtualization', + 'hami': 'area/virtualization', + 'gpu-operator': 'area/virtualization', + }; + + const pr = context.payload.pull_request; + const title = pr.title || ''; + const body = pr.body || ''; + const existing = new Set(pr.labels.map(l => l.name)); + const toAdd = new Set(); + + // 1. Strip "[Backport release-1.x]" prefix if present. + const backportMatch = title.match(/^\[Backport ([^\]]+)\]\s+(.+)$/); + const cleanTitle = backportMatch ? backportMatch[2] : title; + if (backportMatch) { + toAdd.add('area/release'); + toAdd.add('backport'); + } + + // 2. Try Conventional Commits form: type(scope)?(!)?: description + const conv = cleanTitle.match(/^([a-z]+)(?:\(([^)]+)\))?(!)?:\s*.+$/); + // 3. Fall back to bracket form: [scope] description + const bracket = !conv && cleanTitle.match(/^\[([^\]]+)\]\s+.+$/); + + let type = null, scopeStr = null, breaking = false; + if (conv) { + type = conv[1]; + scopeStr = conv[2] || null; + breaking = !!conv[3]; + } else if (bracket) { + scopeStr = bracket[1]; + } + + // 4. Detect BREAKING CHANGE: footer in body. + if (/^BREAKING CHANGE:/m.test(body)) { + breaking = true; + } + + // 5. Apply kind/* from type. + if (type && typeToKind[type]) { + toAdd.add(typeToKind[type]); + } + + // 6. Apply area/* from scope. Composite scopes split on comma. + const scopes = (scopeStr || '') + .split(/,\s*/) + .map(s => s.trim()) + .filter(Boolean); + for (const s of scopes) { + if (scopeToArea[s]) { + toAdd.add(scopeToArea[s]); + } + } + + // 7. kind/breaking-change. + if (breaking) { + toAdd.add('kind/breaking-change'); + } + + // 8. Fallback: no area/* applied -> area/uncategorized. + const hasArea = [...toAdd].some(l => l.startsWith('area/')); + if (!hasArea) { + toAdd.add('area/uncategorized'); + } + + // 9. Additive only — never remove existing labels. + const newLabels = [...toAdd].filter(l => !existing.has(l)); + if (newLabels.length === 0) { + core.info('No new labels to apply'); + return; + } + + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + labels: newLabels, + }); + core.info(`Applied labels: ${newLabels.join(', ')}`); diff --git a/.github/workflows/tags.yaml b/.github/workflows/tags.yaml index 90f04829..1b68ee32 100644 --- a/.github/workflows/tags.yaml +++ b/.github/workflows/tags.yaml @@ -223,7 +223,7 @@ jobs: repo: context.repo.repo, head, base, - title: `Release v${version}`, + title: `chore(release): cut v${version}`, body: `This PR prepares the release \`v${version}\`.`, draft: false }); @@ -411,7 +411,7 @@ jobs: repo: context.repo.repo, head: changelogBranch, base: baseBranch, - title: `docs: add changelog for v${version}`, + title: `docs(release): add changelog for v${version}`, body: `This PR adds the changelog for release \`v${version}\`.\n\n✅ Changelog has been automatically generated in \`docs/changelogs/v${version}.md\`.`, draft: false }); @@ -421,7 +421,7 @@ jobs: owner: context.repo.owner, repo: context.repo.repo, issue_number: pr.data.number, - labels: ['documentation', 'automated'] + labels: ['kind/documentation', 'automated'] }); console.log(`Created PR #${pr.data.number} for changelog`); diff --git a/AGENTS.md b/AGENTS.md index fe9aa8e1..5298f6fd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -27,6 +27,12 @@ working with the **Cozystack** project. - Read: [`contributing.md`](./docs/agents/contributing.md) - Action: Read the file to understand git workflow, commit format, PR process +- **Issue and PR labeling, triage** (e.g., "label this issue", "what label should I use", "triage this", "categorize") + - Read: [`.github/labels.yml`](./.github/labels.yml) + - Action: Use labels defined there. Conventions follow the Kubernetes scheme — `kind/*` (type), `area/*` (subsystem), `priority/*` (urgency), `triage/*` (review state), `lifecycle/*` (auto-close), `do-not-merge/*` (PR blockers), `security/*` (severity) + - For `area/*`: accuracy outweighs reuse. If no existing `area/*` truly fits the change, propose a new one via PR (extend `labels.yml` and the scope mapping in `pr-labeler.yaml`) — do not shoehorn the change into a wrong area. `area/uncategorized` is the auto-labeler fallback; treat it as a signal to pick a fit, create a new area, or correct the PR title + - PR titles: a Conventional Commits header (`type(scope): description`, types from [`contributing.md`](./docs/agents/contributing.md)) auto-applies `kind/*` and `area/*` via `.github/workflows/pr-labeler.yaml`. Append `!` (or add a `BREAKING CHANGE:` footer) to apply `kind/breaking-change` + **Important rules:** - ✅ **ONLY read the file if the task matches the documented process scope** - do not read files for tasks that don't match their purpose - ✅ **ALWAYS read the file FIRST** before starting the task (when applicable) diff --git a/docs/agents/contributing.md b/docs/agents/contributing.md index 658dab74..6ff6eef1 100644 --- a/docs/agents/contributing.md +++ b/docs/agents/contributing.md @@ -57,6 +57,49 @@ git commit --signoff -m "fix(postgres): update operator to version 1.2.3" git commit --signoff -m "docs(contributing): add installation guide" ``` +## PR Title Auto-Labeling + +`.github/workflows/pr-labeler.yaml` parses the PR title on `opened`, `edited`, `reopened`, and `synchronize` events and applies labels additively (never removes). The title is expected to follow Conventional Commits — same format as commit messages above. + +**Type → `kind/*`:** + +| type | label | +| --------- | ------------------ | +| feat | kind/feature | +| fix | kind/bug | +| docs | kind/documentation | +| chore | kind/cleanup | +| refactor | kind/cleanup | +| style, perf, test, build, ci, revert | (no kind label)| + +**Scope → `area/*`** (full mapping in `.github/workflows/pr-labeler.yaml`): + +| scope (examples) | label | +| --- | --- | +| agents, ai | area/ai | +| api, cozystack-api | area/api | +| build | area/build | +| ci | area/ci | +| dashboard | area/dashboard | +| postgres, mariadb, redis, etcd, kafka, clickhouse, postgres-operator, mariadb-operator | area/database | +| extra | area/extra | +| kubernetes | area/kubernetes | +| monitoring, vlogs, vmstack, grafana, workloadmonitor | area/monitoring | +| ingress, gateway, vpn, metallb, cilium, kube-ovn, cozy-proxy, … | area/networking | +| platform, bundle, flux, fluxcd, cluster-api, talos, installer, cozyctl, cozystack-engine, cozy-lib | area/platform | +| backport, release | area/release | +| seaweedfs, bucket, linstor, velero, harbor, backups | area/storage | +| tests, e2e | area/testing | +| kubevirt, cdi, vmi, vm-import, virtual-machine, hami, gpu-operator | area/virtualization | + +**Special handling:** + +- `[Backport release-1.x]` prefix is stripped before parsing; `area/release` and `backport` labels are added. +- Composite scope (`feat(platform, system, apps): …`) — each comma-separated part is mapped independently. +- `!` after type or `BREAKING CHANGE:` footer in the body → `kind/breaking-change`. +- Unmapped scope or non-conventional title → `area/uncategorized` (signals the PR needs manual area selection). +- Bracket-style fallback (`[scope] description`) maps `scope` → `area/*` but cannot infer `kind/*`. + ### AI Agent Attribution When an AI agent authors or materially assists with a commit, add an `Assisted-By:` trailer naming the model: