mirror of
https://github.com/cozystack/cozystack.git
synced 2026-11-05 03:27:07 +09:00
Two compounding issues caused merging a backport PR to spawn another
backport (e.g. #2581 [Backport release-1.2] [Backport release-1.3] ...
from #2579):
1. pr-labeler auto-added the `backport` label to any PR whose title
started with `[Backport release-X.Y]` (regression introduced in
c8ed1c65 when pr-labeler.yaml was first added). korthout/backport-
action puts that prefix on every backport PR it creates, so the
label was being re-applied to every backport, satisfying the
workflow's gate when the backport itself was merged.
2. Even with that fixed, an external labeling bot (dosubot) still
re-applies `backport-previous` to backport PRs based on similarity
to the parent. That bot is configured at the org level and cannot
be controlled from this repo.
Drop the `toAdd.add('backport')` line from pr-labeler so the label
isn't propagated by us, and gate both backport jobs on
`pull_request.base.ref == 'main'` so backport PRs (which target
release-X.Y) cannot trigger the workflow regardless of what labels
end up on them.
Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
206 lines
8.0 KiB
YAML
206 lines
8.0 KiB
YAML
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');
|
|
}
|
|
|
|
// 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: or BREAKING-CHANGE: footer in body.
|
|
// Conventional Commits 1.0 spec item 16 treats them as synonymous.
|
|
if (/^BREAKING[ -]CHANGE:/m.test(body)) {
|
|
breaking = true;
|
|
}
|
|
|
|
// 5. Apply kind/* from type.
|
|
if (type) {
|
|
if (typeToKind[type]) {
|
|
toAdd.add(typeToKind[type]);
|
|
} else {
|
|
core.warning(`type "${type}" has no kind/* mapping — typo or new type? See .github/workflows/pr-labeler.yaml typeToKind`);
|
|
}
|
|
}
|
|
|
|
// 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]);
|
|
} else {
|
|
core.warning(`scope "${s}" has no area/* mapping — consider extending scopeToArea in .github/workflows/pr-labeler.yaml if it recurs`);
|
|
}
|
|
}
|
|
|
|
// 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(', ')}`);
|