-
Notifications
You must be signed in to change notification settings - Fork 78
156 lines (141 loc) · 6.49 KB
/
Copy pathpr-opened.yml
File metadata and controls
156 lines (141 loc) · 6.49 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# PR Opened: Auto Label + DingTalk Notification
# ===============================================
# 1. Automatically labels the PR based on changed file paths.
# Label rules are read dynamically from .github/CODEOWNERS (# auto-label: annotations).
# Adding a new component to CODEOWNERS is sufficient — no workflow changes needed.
# 2. Sends a DingTalk notification.
# Requires secrets: DINGTALK_WEBHOOK, DINGTALK_SECRET
name: PR Opened
on:
pull_request_target:
types: [opened]
branches: [main]
permissions:
contents: read
pull-requests: write
jobs:
label:
name: Auto Label
runs-on: anolisa-k8s-general-ci-x64
steps:
- name: Apply component and scope labels
uses: actions/github-script@v7
with:
script: |
const { owner, repo } = context.repo;
const prNumber = context.payload.pull_request.number;
// ── Parse CODEOWNERS # auto-label: annotations ────────────────
const { data: coFile } = await github.rest.repos.getContent(
{ owner, repo, path: '.github/CODEOWNERS' }
);
const coContent = Buffer.from(coFile.content, 'base64').toString();
const rules = [];
for (const raw of coContent.split('\n')) {
const line = raw.trim();
if (!line || line.startsWith('#')) continue;
const m = line.match(/#\s*auto-label:\s*(\S+)/);
if (!m) continue;
const rawPrefix = line.split(/\s+/)[0];
// Detect glob patterns like *.md (suffix match), otherwise prefix match
const isGlob = rawPrefix.startsWith('*');
const prefix = isGlob ? rawPrefix.slice(1) : rawPrefix.replace(/^\//, '');
const label = m[1];
const type = label.startsWith('component:') ? 'component' : 'scope';
rules.push({ prefix, label, type, isGlob });
}
// ── Fetch changed files (paginate up to 300) ──────────────────
let filenames = [];
for (let page = 1; page <= 3; page++) {
const { data } = await github.rest.pulls.listFiles({
owner, repo, pull_number: prNumber, per_page: 100, page
});
filenames.push(...data.map(f => f.filename));
if (data.length < 100) break;
}
// ── Match files against rules ─────────────────────────────────
const labels = new Set();
const scopeHit = new Set();
const reasons = {}; // label → first matched file
for (const fn of filenames) {
let componentMatch = null, componentPrefix = null;
let scopeMatch = null, scopePrefix = null;
for (const { prefix, label, type, isGlob } of rules) {
const matched = isGlob ? fn.endsWith(prefix) : fn.startsWith(prefix);
if (matched) {
if (type === 'component') { componentMatch = label; componentPrefix = prefix; }
else { scopeMatch = label; scopePrefix = prefix; }
}
}
if (componentMatch && !labels.has(componentMatch)) {
labels.add(componentMatch);
reasons[componentMatch] = `${fn} matches prefix '${componentPrefix}'`;
}
if (scopeMatch) {
if (!labels.has(scopeMatch)) {
labels.add(scopeMatch);
reasons[scopeMatch] = `${fn} matches prefix '${scopePrefix}'`;
}
scopeHit.add(scopeMatch);
}
}
const hasComponent = Array.from(labels).some(l => l.startsWith('component:'));
if (scopeHit.size === 0 && !hasComponent) {
labels.add('scope:chore');
reasons['scope:chore'] = 'no component or scope matched (fallback)';
}
if (labels.size > 0) {
await github.rest.issues.addLabels({
owner, repo, issue_number: prNumber,
labels: Array.from(labels)
});
core.info('=== Auto-label results ===');
for (const [label, reason] of Object.entries(reasons)) {
core.info(` [${label}] ← ${reason}`);
}
}
notify:
name: DingTalk Notification
runs-on: anolisa-k8s-general-ci-x64
env:
DINGTALK_WEBHOOK: ${{ secrets.DINGTALK_WEBHOOK }}
DINGTALK_SECRET: ${{ secrets.DINGTALK_SECRET }}
steps:
- name: 📣 Send DingTalk message
if: env.DINGTALK_WEBHOOK != ''
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_TITLE: ${{ github.event.pull_request.title }}
PR_URL: ${{ github.event.pull_request.html_url }}
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
PR_BRANCH: ${{ github.event.pull_request.head.ref }}
PR_BODY: ${{ github.event.pull_request.body }}
run: |
python3 << 'PYEOF'
import hmac, hashlib, base64, urllib.parse, time, os, subprocess, json
ts = str(round(time.time() * 1000))
secret = os.environ.get('DINGTALK_SECRET', '')
sig = hmac.new(secret.encode(), (ts + '\n' + secret).encode(), hashlib.sha256).digest()
sign = urllib.parse.quote_plus(base64.b64encode(sig).decode())
pr_number = os.environ.get('PR_NUMBER', '')
pr_title = os.environ.get('PR_TITLE', '')
pr_url = os.environ.get('PR_URL', '')
pr_author = os.environ.get('PR_AUTHOR', '')
pr_branch = os.environ.get('PR_BRANCH', '')
webhook = os.environ.get('DINGTALK_WEBHOOK', '')
text = (
f"## 🔁 ANOLISA New PR\n\n"
f"**PR**: [#{pr_number} {pr_title}]({pr_url})\n\n"
f"**Branch**: `{pr_branch}` → `main`\n\n"
f"**Author**: {pr_author}\n\n"
f"> 🤖 ANOLISA CI Bot"
)
payload = json.dumps({
"msgtype": "markdown",
"markdown": {"title": f"New PR #{pr_number}", "text": text}
})
url = f"{webhook}×tamp={ts}&sign={sign}"
subprocess.run(
["curl", "-s", "-X", "POST", url, "-H", "Content-Type: application/json", "-d", payload],
check=True
)
PYEOF