Skip to content

Commit e52f5ef

Browse files
authored
Merge pull request #20 from benjamin-747/main
refactor: migrate atomgit/portal integrations and system-level error …
2 parents b71c123 + 9c2336c commit e52f5ef

32 files changed

Lines changed: 1012 additions & 364 deletions

.env.example

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,13 @@ LOG_LEVEL=debug
77

88
# Go to https://smee.io/new set this to the URL that you are redirected to.
99
WEBHOOK_PROXY_URL=
10+
11+
# Backend API base (existing task API).
12+
API_ENDPOINT=
13+
14+
# Portal server base URL for mentor limits API.
15+
# Example: https://portal.example.com
16+
PORTAL_ENDPOINT=
17+
18+
# Bearer token for openatom integration APIs on portal.
19+
OPENATOM_INTEGRATION_TOKEN=

.github/workflows/bot-deploy.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,6 @@ jobs:
8181
# -p 3000:3000 --restart=always \
8282
# public.ecr.aws/m8q5m4u3/r2cn/bot:latest
8383
# # Atomgit (when enabled): add GitHub repo secrets and uncomment, e.g.
84-
# # -e ATOMGIT_WEBHOOK_SECRET=${{ secrets.ATOMGIT_WEBHOOK_SECRET }} \
84+
# # -e PORTAL_ENDPOINT=${{ secrets.PORTAL_ENDPOINT }} \
8585
# # -e ATOMGIT_API_BASE=${{ secrets.ATOMGIT_API_BASE }} \
8686
# # -e ATOMGIT_TOKEN=${{ secrets.ATOMGIT_TOKEN }} \

README.md

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,17 +38,14 @@ Configure the repository webhook URL to **`POST https://<host>:<port>/webhooks/a
3838

3939
| Variable | Description |
4040
|----------|-------------|
41-
| `ATOMGIT_WEBHOOK_SECRET` | Webhook secret: compared to `X-Gitlab-Token` / `X-AtomGit-Token` (default), or used for HMAC (see `ATOMGIT_WEBHOOK_VERIFY_MODE`). If unset, the route responds `200` with `{ ok: false }` and does not process payloads (avoids retry storms). |
42-
| `ATOMGIT_WEBHOOK_VERIFY_MODE` | `token` (default) or `hmac-sha256` (compare `X-AtomGit-Signature` / `X-Gitlab-Signature` as `sha256=<hex>` of raw body). |
41+
| `PORTAL_ENDPOINT` | Portal base URL. Atomgit webhook verification token is fetched by owner from `/api/integration/open-source-orgs/webhook-tokens` and cached for 5 minutes. |
42+
| `OPENATOM_INTEGRATION_TOKEN` | Bearer token used in `Authorization` header when calling portal openatom integration APIs. |
4343
| `ATOMGIT_API_BASE` | REST root for OpenAPI calls, e.g. `https://api.atomgit.com/api/v5`. Writes: comments `POST .../repos/:owner/:repo/issues/:number/comments`; labels `POST .../labels` (JSON **array of strings**); close/reopen issue uses `GET .../repos/:owner/:repo/issues/:number` then `PATCH .../repos/:owner/issues/:number` with **`application/x-www-form-urlencoded`** (`repo`, `title`, `body`, `state=close|reopen`) per [AtomGit](https://docs.atomgit.com/docs/apis/patch-api-v-5-repos-owner-issues-number). Required when handling Atomgit webhooks that need `AtomgitScmClient`. |
4444
| `ATOMGIT_TOKEN` | `Authorization: Bearer` token for OpenAPI. |
4545
| `ATOMGIT_API_VERSION` | Optional; default `2023-02-21` (`X-Api-Version` header). |
46-
| `ATOMGIT_DEFAULT_BRANCH` | Optional; default `main` — used for `getRepositoryContent` raw file reads. |
4746

4847
**Payload mapping** (see `src/webhooks/map-atomgit-to-canonical.ts`): `Note Hook` / `object_kind: note` on an **Issue**`IssueCommentCreated`; **Issue Hook**`IssueLabeled` when (1) `action: update` and `changes.labels` has **at least one** new label, or (2) **`action: open`** and labels (on `object_attributes` or top-level `issue`) already include an **`r2cn-*`** score label (create-issue-with-labels). Other events are acknowledged with **200** and a debug log (no handler).
4948

50-
**Config YAML**: `loadBotConfig` still reads `r2cn-dev/...` via `ScmClient.getRepositoryContent`. On Atomgit-only installs, that path must exist on the same instance or you need a separate config strategy (docs §7).
51-
5249
## Docker
5350

5451
```sh

comment.en.yaml

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
project:
22
noneProjectComment: >
3-
Welcome to the R2CN project! Before getting started, you need to submit a PR first. For details, please refer to the documentation at https://r2cn.dev/docs/project/signup
3+
Welcome to the R2CN project! Before getting started, you need to apply on the portal for project registration and mentor qualification onboarding.
44
noneMaintainerComment: >
55
You currently do not have permission to create R2CN tasks. Please contact your repository or organization administrator to submit a PR to add your GitHub ID to the r2cn.yaml file. For reference, see https://r2cn.dev/docs/project/mentor-and-tasks/mentor.
66
7+
system:
8+
apiUnavailable: >
9+
The backend API service is unavailable. Please try again later.
10+
webhookTokenMismatch: >
11+
Project registration failed. Please contact the administrator.
12+
713
task:
814
success: >
915
Task created successfully.
@@ -13,8 +19,6 @@ task:
1319
Task score and other information cannot be modified after the task is completed.
1420
taskNotFound: >
1521
The task has not been created yet. Please use the "R2CN-score" label to create a task first.
16-
apiUnavailable: >
17-
The backend API service is unavailable. Please try again later.
1822
scoreUndefinedComment: >
1923
Please use the "R2CN-score" label to create a task. The score in the current label is incorrect.
2024
scoreInvalidComment: >
@@ -38,7 +42,7 @@ requestAssign:
3842
success: >
3943
Claim successful: Waiting for mentor review. Please contact your mentor for review in a timely manner.
4044
waitingInfoReview: >
41-
Claim failed: Please register as a student at https://summer-ospp.ac.cn/r2cnLogin and pass the review before claiming tasks.
45+
Claim failed: Please register as a student at https://internship.xuanwu.openatom.cn/en/signin and pass the review before claiming tasks.
4246
waitingContract: >
4347
Claim failed: Contract not signed. If you have confirmed signing, please contact the administrator in the R2CN Discord (https://discord.gg/WRp4TKv6rh) #signup-student channel to update the contract signing status.
4448
existTask: >

comment.zh.yaml

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
project:
22
noneProjectComment: >
3-
欢迎参与R2CN项目,在开始之前您需要先提交一个PR, 详情可参考文档 https://r2cn.dev/docs/project/signup
3+
欢迎参与R2CN项目,在开始之前您需要先去 portal 申请项目注册和导师资格录入,详情可参考文档 https://r2cn.dev/docs/project/signup
44
noneMaintainerComment: >
5-
您当前没有权限来创建R2CN任务,请联系您仓库或者组织管理员提交PR将您的GitHub ID加入r2cn.yaml文件,参考 https://r2cn.dev/docs/project/mentor-and-tasks/mentor
5+
您当前没有权限来创建R2CN任务,请联系开源实习方将您的信息维护到系统中.
6+
7+
system:
8+
apiUnavailable: >
9+
当前后端 API 服务不可用,请稍后重试。
10+
webhookTokenMismatch: >
11+
项目注册失败,请联系管理员处理。
612
713
task:
814
success: >
@@ -12,13 +18,11 @@ task:
1218
notAllowedModify: >
1319
任务完成后无法修改分值等信息.
1420
taskNotFound: >
15-
任务还未创建,需要使用“R2CN-分值”标签先创建任务.
16-
apiUnavailable: >
17-
当前后端 API 服务不可用,请稍后重试。
21+
任务还未创建,需要使用“系统管理员配置的标签前缀-分值”标签先创建任务.
1822
scoreUndefinedComment: >
19-
请使用“R2CN-分值”标签来创建任务,当前标签的分值不正确.
23+
请使用“系统管理员配置的标签前缀-分值”标签来创建任务,当前标签的分值不正确.
2024
scoreInvalidComment: >
21-
R2CN-分值超出组织管理员配置的上限,或者分值小于2.
25+
系统管理员配置的标签前缀-分值超出组织管理员配置的上限,或者分值小于2.
2226
multiScoreLabel: >
2327
只能拥有一个分值标签,请先移除所有r2cn-分数标签,然后再选择1个r2cn-分数标签来新建任务.
2428
userToomanyTask: >
@@ -38,7 +42,7 @@ requestAssign:
3842
success: >
3943
认领成功: 等待导师审核,请及时联系导师进行审核.
4044
waitingInfoReview: >
41-
认领失败: 请先在 https://summer-ospp.ac.cn/r2cnLogin 进行学生注册并通过审核后才能认领任务.
45+
认领失败: 请先在 https://internship.xuanwu.openatom.cn/zh-CN/signin 进行学生注册并通过审核后才能认领任务.
4246
waitingContract: >
4347
认领失败: 合同未签署,如果已经确认签署请在 R2CN Discord(https://discord.gg/WRp4TKv6rh) 的 #signup-student 频道联系管理员更新合同签署状态.
4448
existTask: >

docs/dual-webhook-scm-architecture.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -229,13 +229,13 @@ src/
229229

230230
| 变量 | 用途 |
231231
|------|------|
232-
| **`ATOMGIT_WEBHOOK_SECRET`** | Atomgit Webhook 验签密钥(具体算法与 Header 以官方文档为准) |
232+
| **`PORTAL_ENDPOINT`** | Portal 服务根地址;按 owner 从 `/api/integration/open-source-orgs/webhook-tokens` 拉取 Atomgit webhook token 并缓存 |
233233
| **`ATOMGIT_API_BASE`** | Atomgit OpenAPI 根 URL(例如 `https://api.atomgit.com/api/v5`,无尾斜杠)。发评论走 `POST /repos/:owner/:repo/issues/:number/comments`|
234234
| **`ATOMGIT_TOKEN`** | 调用 Atomgit API 的 token(或按组织拆为多 token,再增加 `ATOMGIT_TOKEN_*`|
235235

236236
#### 6.3.4 运维与对齐说明
237237

238-
- **密钥轮换**`WEBHOOK_SECRET` / `ATOMGIT_WEBHOOK_SECRET` 与平台控制台配置需同步更新
238+
- **密钥轮换**`WEBHOOK_SECRET` 与平台控制台配置需同步更新;Atomgit webhook token 由 portal 维护
239239
- **文档维护**:实现落地后,将**最终**变量名与是否必填同步到 README 或运维 runbook;本文档第 6.3 节作为设计期清单,可与代码 `process.env` 使用处交叉引用。
240240

241241
---

src/config/index.ts

Lines changed: 86 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export interface Config {
2222

2323
export interface BotComment {
2424
project: ProjectComment,
25+
system: SystemComment,
2526
task: TaskComment,
2627
command: CommandComment,
2728
requestAssign: RequestAssign,
@@ -40,12 +41,16 @@ interface ProjectComment {
4041
noneMaintainerComment: string,
4142
}
4243

44+
interface SystemComment {
45+
apiUnavailable: string,
46+
webhookTokenMismatch: string,
47+
}
48+
4349
interface TaskComment {
4450
success: string,
4551
successUpdate: string,
4652
notAllowedModify: string,
4753
taskNotFound: string,
48-
apiUnavailable: string,
4954
scoreUndefinedComment: string,
5055
multiScoreLabel: string,
5156
scoreInvalidComment: string,
@@ -110,6 +115,7 @@ export interface CommandRequest {
110115
export interface ApiResponse<T> {
111116
message: string;
112117
data: T;
118+
status?: number;
113119
}
114120

115121
const http = axios.create({
@@ -122,10 +128,47 @@ const NON_ERROR_MESSAGES = new Set([
122128
]);
123129

124130
export function isBackendApiError<T>(res: Pick<ApiResponse<T>, "message" | "data">): boolean {
131+
const withStatus = res as Pick<ApiResponse<T>, "message" | "data" | "status">;
132+
const message = String(res.message ?? "").trim();
125133
if (res.data != null) {
126134
return false;
127135
}
128-
return !NON_ERROR_MESSAGES.has(res.message);
136+
// Treat 4xx as business/domain failures, not service unavailability.
137+
if (withStatus.status != null && withStatus.status >= 400 && withStatus.status < 500) {
138+
return false;
139+
}
140+
// Explicit 5xx means backend/service failure.
141+
if (withStatus.status != null && withStatus.status >= 500) {
142+
return true;
143+
}
144+
if (message === "" || NON_ERROR_MESSAGES.has(message)) {
145+
return false;
146+
}
147+
// Network-level failures from Axios/fetch-like stacks.
148+
if (/(ECONNREFUSED|ECONNRESET|ENOTFOUND|ETIMEDOUT|EAI_AGAIN|fetch failed|network error|socket hang up)/i.test(message)) {
149+
return true;
150+
}
151+
// Keep this conservative: unknown textual messages without status are treated as business errors.
152+
return false;
153+
}
154+
155+
/**
156+
* Some task/command POST endpoints return `{ data: true }` on success; others return
157+
* `{ data: <Task | record> }` with HTTP 200. This treats both as success; `data === false` or
158+
* `data == null` counts as failure.
159+
*/
160+
export function isCommandApiDataSuccess<T>(res: Pick<ApiResponse<T>, "data">): boolean {
161+
const d = res.data as unknown;
162+
if (d === true) {
163+
return true;
164+
}
165+
if (d === false || d == null) {
166+
return false;
167+
}
168+
if (typeof d === "object") {
169+
return true;
170+
}
171+
return true;
129172
}
130173

131174
function toFailedApiResponse<T>(error: unknown): ApiResponse<T> {
@@ -137,23 +180,54 @@ function toFailedApiResponse<T>(error: unknown): ApiResponse<T> {
137180
backendMessage ??
138181
(status != null ? `HTTP ${status}: ${e.message}` : e.message) ??
139182
"Unknown error occurred";
140-
return { message: msg, data: null as unknown as T };
183+
return { message: msg, data: null as unknown as T, status };
141184
}
142185
if (error instanceof Error) {
143186
return { message: error.message, data: null as unknown as T };
144187
}
145188
return { message: "Unknown error occurred", data: null as unknown as T };
146189
}
147190

191+
function logApiAxiosError(
192+
method: "GET" | "POST",
193+
url: string,
194+
payload: unknown,
195+
error: unknown,
196+
failedMessage: string,
197+
): void {
198+
if (axios.isAxiosError(error)) {
199+
const e = error as AxiosError<unknown>;
200+
const base: Record<string, unknown> = {
201+
method,
202+
url,
203+
status: e.response?.status,
204+
body: e.response?.data,
205+
message: e.message,
206+
error: failedMessage,
207+
};
208+
if (method === "POST") {
209+
base.payload = payload;
210+
}
211+
console.error("[api] error", base);
212+
return;
213+
}
214+
console.error("[api] error", { method, url, error: failedMessage, cause: error });
215+
}
216+
148217
export const fetchData = async <T>(url: string): Promise<ApiResponse<T>> => {
149218
try {
150219
console.info("[api] request", { method: "GET", url });
151220
const response: AxiosResponse<ApiResponse<T>> = await http.get(url);
152-
console.info("[api] response", { method: "GET", url, message: response.data.message });
221+
console.info("[api] response", {
222+
method: "GET",
223+
url,
224+
status: response.status,
225+
body: response.data,
226+
});
153227
return response.data;
154228
} catch (error: unknown) {
155229
const failed = toFailedApiResponse<T>(error);
156-
console.error("[api] error", { method: "GET", url, error: failed.message });
230+
logApiAxiosError("GET", url, undefined, error, failed.message);
157231
return failed;
158232
}
159233
};
@@ -162,16 +236,16 @@ export const postData = async <T, U>(url: string, payload: U): Promise<ApiRespon
162236
try {
163237
console.info("[api] request", { method: "POST", url, payload });
164238
const response: AxiosResponse<ApiResponse<T>> = await http.post(url, payload);
165-
console.info("[api] response", { method: "POST", url, message: response.data.message });
166-
return response.data;
167-
} catch (error: unknown) {
168-
const failed = toFailedApiResponse<T>(error);
169-
console.error("[api] error", {
239+
console.info("[api] response", {
170240
method: "POST",
171241
url,
172-
error: failed.message,
173-
payload,
242+
status: response.status,
243+
body: response.data,
174244
});
245+
return response.data;
246+
} catch (error: unknown) {
247+
const failed = toFailedApiResponse<T>(error);
248+
logApiAxiosError("POST", url, payload, error, failed.message);
175249
return failed;
176250
}
177251
};

src/config/load-bot-config.ts

Lines changed: 0 additions & 95 deletions
This file was deleted.

0 commit comments

Comments
 (0)