Skip to content

Commit 8df1ad1

Browse files
authored
Move error registry to infrastructure and adopt native Error (#439)
* Move error registry to infrastructure and adopt native Error * bump: sdk factory * chore: bump sdk version * Include preview push workflow in build triggers
1 parent c1068b6 commit 8df1ad1

33 files changed

Lines changed: 1741 additions & 233 deletions

File tree

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
name: Preview Plugin Image Build
2+
3+
on:
4+
pull_request:
5+
types: [opened, synchronize, reopened]
6+
branches: ['**']
7+
paths:
8+
- 'Dockerfile'
9+
- '.dockerignore'
10+
- 'apps/server/**'
11+
- 'packages/**'
12+
- 'sdk/**'
13+
- 'package.json'
14+
- 'pnpm-lock.yaml'
15+
- 'pnpm-workspace.yaml'
16+
- '.github/workflows/preview-image-build.yml'
17+
- '.github/workflows/preview-image-push.yml'
18+
19+
concurrency:
20+
group: preview-plugin-image-build-${{ github.event.pull_request.number || github.head_ref }}
21+
cancel-in-progress: true
22+
23+
permissions:
24+
contents: read
25+
26+
jobs:
27+
build:
28+
name: Build preview Server image
29+
runs-on: ubuntu-24.04
30+
31+
steps:
32+
- name: Checkout PR code
33+
uses: actions/checkout@v4
34+
with:
35+
ref: ${{ github.event.pull_request.head.sha }}
36+
repository: ${{ github.event.pull_request.head.repo.full_name }}
37+
38+
- name: Set up Docker Buildx
39+
uses: docker/setup-buildx-action@v3
40+
41+
- name: Build Docker image artifact
42+
uses: docker/build-push-action@v6
43+
with:
44+
context: .
45+
file: Dockerfile
46+
target: runner
47+
platforms: linux/amd64
48+
push: false
49+
tags: fastgpt-plugin-pr:${{ github.event.pull_request.head.sha }}
50+
labels: |
51+
org.opencontainers.image.source=https://github.com/${{ github.repository }}
52+
org.opencontainers.image.description=FastGPT Plugin PR preview image
53+
org.opencontainers.image.revision=${{ github.event.pull_request.head.sha }}
54+
outputs: type=docker,dest=/tmp/fastgpt-plugin-image.tar
55+
cache-from: type=gha,scope=preview-plugin-server
56+
cache-to: type=gha,mode=max,scope=preview-plugin-server
57+
58+
- name: Save PR metadata
59+
run: |
60+
echo "${{ github.event.pull_request.number }}" > /tmp/pr-number.txt
61+
echo "${{ github.event.pull_request.head.sha }}" > /tmp/pr-sha.txt
62+
echo "server" > /tmp/image-type.txt
63+
64+
- name: Upload Docker image artifact
65+
uses: actions/upload-artifact@v4
66+
with:
67+
name: preview-plugin-server-image
68+
path: |
69+
/tmp/fastgpt-plugin-image.tar
70+
/tmp/pr-number.txt
71+
/tmp/pr-sha.txt
72+
/tmp/image-type.txt
73+
retention-days: 1
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
name: Preview Plugin Image Push
2+
3+
on:
4+
workflow_run:
5+
workflows: ['Preview Plugin Image Build']
6+
types: [completed]
7+
8+
concurrency:
9+
group: preview-plugin-image-push
10+
cancel-in-progress: false
11+
12+
permissions:
13+
contents: read
14+
15+
jobs:
16+
check_pr_author:
17+
name: Check trusted PR author
18+
runs-on: ubuntu-24.04
19+
if: ${{ github.event.workflow_run.conclusion == 'success' }}
20+
permissions:
21+
contents: read
22+
pull-requests: read
23+
actions: read
24+
outputs:
25+
trusted: ${{ steps.trust.outputs.trusted }}
26+
should_publish: ${{ steps.trust.outputs.should_publish }}
27+
number: ${{ steps.trust.outputs.number }}
28+
sha: ${{ steps.trust.outputs.sha }}
29+
30+
steps:
31+
- name: Check trusted PR author
32+
id: trust
33+
uses: actions/github-script@v7
34+
with:
35+
script: |
36+
const allowedAssociations = new Set(['OWNER', 'MEMBER', 'COLLABORATOR']);
37+
const workflowRun = context.payload.workflow_run;
38+
let prNumber = workflowRun.pull_requests?.[0]?.number;
39+
40+
core.setOutput('trusted', 'false');
41+
core.setOutput('should_publish', 'false');
42+
43+
if (!prNumber) {
44+
const headOwner = workflowRun.head_repository?.owner?.login;
45+
const headBranch = workflowRun.head_branch;
46+
47+
if (headOwner && headBranch) {
48+
core.info(`workflow_run payload did not include a PR number; looking up PR by ${headOwner}:${headBranch}.`);
49+
const { data: pullRequests } = await github.rest.pulls.list({
50+
owner: context.repo.owner,
51+
repo: context.repo.repo,
52+
state: 'open',
53+
head: `${headOwner}:${headBranch}`
54+
});
55+
const matchedPullRequest = pullRequests.find((pullRequest) =>
56+
pullRequest.head.sha === workflowRun.head_sha
57+
) ?? pullRequests[0];
58+
prNumber = matchedPullRequest?.number;
59+
}
60+
}
61+
62+
if (!prNumber) {
63+
core.warning('No pull request was found on the workflow_run payload. Skipping privileged preview publish.');
64+
return;
65+
}
66+
67+
const { data: pullRequest } = await github.rest.pulls.get({
68+
owner: context.repo.owner,
69+
repo: context.repo.repo,
70+
pull_number: prNumber
71+
});
72+
73+
const trusted = allowedAssociations.has(pullRequest.author_association);
74+
core.setOutput('trusted', trusted ? 'true' : 'false');
75+
core.setOutput('number', String(pullRequest.number));
76+
core.setOutput('sha', pullRequest.head.sha);
77+
78+
if (!trusted) {
79+
core.warning(`PR #${pullRequest.number} author association is ${pullRequest.author_association}; skipping privileged preview publish.`);
80+
return;
81+
}
82+
83+
const artifacts = await github.paginate(github.rest.actions.listWorkflowRunArtifacts, {
84+
owner: context.repo.owner,
85+
repo: context.repo.repo,
86+
run_id: workflowRun.id,
87+
per_page: 100
88+
});
89+
const artifactNames = new Set(artifacts.map((artifact) => artifact.name));
90+
const shouldPublish = artifactNames.has('preview-plugin-server-image');
91+
92+
core.setOutput('should_publish', shouldPublish ? 'true' : 'false');
93+
core.info(`PR #${pullRequest.number} author association is ${pullRequest.author_association}; preview server image publish=${shouldPublish}.`);
94+
95+
push:
96+
name: Push preview Server image
97+
needs: check_pr_author
98+
runs-on: ubuntu-24.04
99+
if: ${{ needs.check_pr_author.outputs.trusted == 'true' && needs.check_pr_author.outputs.should_publish == 'true' }}
100+
permissions:
101+
contents: read
102+
packages: write
103+
pull-requests: write
104+
issues: write
105+
actions: read
106+
107+
steps:
108+
- name: Download build artifact
109+
uses: actions/download-artifact@v4
110+
with:
111+
name: preview-plugin-server-image
112+
path: /tmp
113+
run-id: ${{ github.event.workflow_run.id }}
114+
github-token: ${{ secrets.GITHUB_TOKEN }}
115+
116+
- name: Load Docker image
117+
run: docker load --input /tmp/fastgpt-plugin-image.tar
118+
119+
- name: Set up Docker Buildx
120+
uses: docker/setup-buildx-action@v3
121+
122+
- name: Login to GitHub Container Registry
123+
uses: docker/login-action@v3
124+
with:
125+
registry: ghcr.io
126+
username: ${{ github.repository_owner }}
127+
password: ${{ secrets.GITHUB_TOKEN }}
128+
129+
- name: Tag and push Docker image
130+
run: |
131+
SHA="${{ needs.check_pr_author.outputs.sha }}"
132+
PREVIEW_IMAGE="ghcr.io/${{ github.repository_owner }}/fastgpt-plugin-pr:server_${SHA}"
133+
docker tag "fastgpt-plugin-pr:${SHA}" "${PREVIEW_IMAGE}"
134+
docker push "${PREVIEW_IMAGE}"
135+
136+
- name: Format preview timestamp
137+
id: preview_time
138+
run: |
139+
echo "value=$(TZ='Asia/Shanghai' date '+%Y-%m-%d %H:%M:%S (UTC+8)')" >> "$GITHUB_OUTPUT"
140+
141+
- name: Add PR comment on success
142+
if: success() && needs.check_pr_author.outputs.number != ''
143+
uses: actions/github-script@v7
144+
with:
145+
script: |
146+
const prNumber = parseInt('${{ needs.check_pr_author.outputs.number }}', 10);
147+
const marker = '<!-- fastgpt-plugin-preview-server -->';
148+
const image = 'ghcr.io/${{ github.repository_owner }}/fastgpt-plugin-pr:server_${{ needs.check_pr_author.outputs.sha }}';
149+
150+
const { data: comments } = await github.rest.issues.listComments({
151+
owner: context.repo.owner,
152+
repo: context.repo.repo,
153+
issue_number: prNumber
154+
});
155+
156+
const existingComment = comments.find((comment) =>
157+
comment.body.includes(marker)
158+
);
159+
160+
const commentBody = [
161+
marker,
162+
'**Build Successful** - Preview Server image for this PR:',
163+
'',
164+
'```',
165+
image,
166+
'```',
167+
'',
168+
'Time: ${{ steps.preview_time.outputs.value }}'
169+
].join('\n');
170+
171+
if (existingComment) {
172+
await github.rest.issues.updateComment({
173+
owner: context.repo.owner,
174+
repo: context.repo.repo,
175+
comment_id: existingComment.id,
176+
body: commentBody
177+
});
178+
} else {
179+
await github.rest.issues.createComment({
180+
owner: context.repo.owner,
181+
repo: context.repo.repo,
182+
issue_number: prNumber,
183+
body: commentBody
184+
});
185+
}

apps/server/src/routes/plugin.route.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ export const makePluginRoute = (deps: PluginRouteDeps) => {
6565
});
6666

6767
if (err) {
68-
return R.fail(c, 400, err.reason);
68+
return R.fail(c, 400, err.error);
6969
}
7070

7171
return R.success(c, result);
@@ -105,7 +105,7 @@ export const makePluginRoute = (deps: PluginRouteDeps) => {
105105
const [, err] = await pluginConfirmUC({ uniqueIds });
106106

107107
if (err) {
108-
return R.fail(c, 500, err.reason);
108+
return R.fail(c, 500, err.error);
109109
}
110110

111111
return c.json({ ok: true }, 200);
@@ -139,7 +139,7 @@ export const makePluginRoute = (deps: PluginRouteDeps) => {
139139
const [result, err] = await pluginPruneDisabledUC();
140140

141141
if (err) {
142-
return R.fail(c, 500, err.reason);
142+
return R.fail(c, 500, err.error);
143143
}
144144

145145
return R.success(c, result);
@@ -178,7 +178,7 @@ export const makePluginRoute = (deps: PluginRouteDeps) => {
178178
const [, err] = await pluginDeleteUC(body);
179179

180180
if (err) {
181-
return R.fail(c, 500, err.reason);
181+
return R.fail(c, 500, err.error);
182182
}
183183

184184
return R.empty(c);
@@ -225,7 +225,7 @@ export const makePluginRoute = (deps: PluginRouteDeps) => {
225225
});
226226

227227
if (err) {
228-
return R.fail(c, 500, err.reason);
228+
return R.fail(c, 500, err.error);
229229
}
230230

231231
return R.success(c, result);
@@ -263,7 +263,7 @@ export const makePluginRoute = (deps: PluginRouteDeps) => {
263263
const [result, err] = await pluginListUC(query);
264264

265265
if (err) {
266-
return R.fail(c, 500, err.reason);
266+
return R.fail(c, 500, err.error);
267267
}
268268

269269
return R.success(c, result);
@@ -301,7 +301,7 @@ export const makePluginRoute = (deps: PluginRouteDeps) => {
301301
const [result, err] = await pluginVersionsUC(query);
302302

303303
if (err) {
304-
return R.fail(c, 500, err.reason);
304+
return R.fail(c, 500, err.error);
305305
}
306306

307307
return R.success(c, result);
@@ -335,7 +335,7 @@ export const makePluginRoute = (deps: PluginRouteDeps) => {
335335
const [result, err] = await pluginTagListUC({});
336336

337337
if (err) {
338-
return R.fail(c, 500, err.reason);
338+
return R.fail(c, 500, err.error);
339339
}
340340

341341
return R.success(c, result);
@@ -381,7 +381,7 @@ export const makePluginRoute = (deps: PluginRouteDeps) => {
381381
});
382382

383383
if (err) {
384-
return R.fail(c, 500, err.reason);
384+
return R.fail(c, 500, err.error);
385385
}
386386

387387
return R.success(c, result);
@@ -423,7 +423,7 @@ export const makePluginRoute = (deps: PluginRouteDeps) => {
423423
});
424424

425425
if (err) {
426-
return R.fail(c, 500, err.reason);
426+
return R.fail(c, 500, err.error);
427427
}
428428

429429
return R.empty(c);
@@ -464,7 +464,7 @@ export const makePluginRoute = (deps: PluginRouteDeps) => {
464464
});
465465

466466
if (err) {
467-
return R.fail(c, 500, err.reason);
467+
return R.fail(c, 500, err.error);
468468
}
469469

470470
return R.empty(c);

apps/server/src/routes/runtime.route.ts

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
import { createRoute, z } from '@hono/zod-openapi';
2+
import { ErrorResponseDTOSchema } from '@interface-adapter/contracts/dto/common.dto';
23

3-
import {
4-
makeRuntimeMetricsUC,
5-
type RuntimeMetricsUCDeps
6-
} from '@usecase/runtime/runtime-metrics.uc';
4+
import { makeRuntimeMetricsUC, type RuntimeMetricsUCDeps } from '@usecase/runtime/runtime-metrics.uc';
75
import { createOpenAPIHono, R } from '@infrastructure/hono/utils/response';
86
import { getLogger, root } from '@infrastructure/logger';
97

@@ -34,11 +32,7 @@ export const makeRuntimeRoute = (deps: RuntimeRouteDeps) => {
3432
content: {
3533
'application/json': {
3634
schema: z.object({
37-
error: z.object({
38-
en: z.string(),
39-
'zh-CN': z.string().optional(),
40-
'zh-Hant': z.string().optional()
41-
})
35+
error: ErrorResponseDTOSchema
4236
})
4337
}
4438
}
@@ -50,7 +44,7 @@ export const makeRuntimeRoute = (deps: RuntimeRouteDeps) => {
5044
const [result, err] = await runtimeMetricsUC({});
5145

5246
if (err) {
53-
return R.fail(c, 500, err.reason);
47+
return R.fail(c, 500, err.error);
5448
}
5549

5650
return R.success(c, result);

0 commit comments

Comments
 (0)