Skip to content
Clone

chore(deploy): retire githost and wire KenShell settings

Remove the githost deployment, service, ingress route and SSH host-key wiring
from the charts, dev scripts and justfile, since KenShell now serves smart
HTTP and SSH.

Expose kenshell.endpoint/rpcKey/timeoutSecs and clone URLs through Helm
values so the apihost and synchost pods can reach KenShell.
ZhenYi committed
parent 89109d2 commit 1ff354c Browse files

11 files changed

+38 −311

File tree 11
Modified charts/core/templates/_helpers.tpl View charts/core/templates/_helpers.tpl
@@ -62,31 +62,24 @@
6262{{- define "gitdata.env" -}}
6363- name: APP_APIHOST__PORT
6464 value: {{ .Values.apihost.port | quote }}
65- name: APP_GITHOST__HTTP_PORT
66 value: {{ .Values.githost.httpPort | quote }}
6765- name: APP_IM__PORT
6866 value: {{ .Values.im.port | quote }}
6967- name: APP_HEALTH__ENABLED
7068 value: {{ .Values.health.enabled | quote }}
7169- name: APP_HEALTH__SYNCHOST__PORT
7270 value: {{ .Values.health.synchostPort | quote }}
73{{- if .Values.githost.ssh.enabled }}
74- name: APP_GIT__SSH__PORT
75 value: {{ .Values.githost.ssh.containerPort | quote }}
71- name: APP_GIT__KENSHELL_ENDPOINT
72 value: {{ .Values.kenshell.endpoint | quote }}
73- name: APP_GIT__KENSHELL_RPC_KEY
74 value: {{ .Values.kenshell.rpcKey | quote }}
75- name: APP_GIT__KENSHELL_TIMEOUT_SECS
76 value: {{ .Values.kenshell.timeoutSecs | quote }}
77- name: APP_GIT__HTTP_CLONE_BASE_URL
78 value: {{ .Values.kenshell.httpCloneBaseUrl | quote }}
7679- name: APP_GIT__SSH_CLONE_HOST
77 value: {{ .Values.githost.ssh.cloneHost | quote }}
80 value: {{ .Values.kenshell.sshCloneHost | quote }}
7881- name: APP_GIT__SSH_CLONE_PORT
79 value: {{ .Values.githost.ssh.clonePort | quote }}
80{{- else }}
81- name: APP_GIT__SSH__PORT
82 value: "0"
83{{- end }}
84- name: APP_GIT__REPO_ROOT
85 value: {{ printf "%s/repos" .Values.persistence.repos.mountPath | quote }}
86- name: APP_GIT__OBJECTS_ROOT
87 value: {{ printf "%s/objects" .Values.persistence.repos.mountPath | quote }}
88- name: APP_GIT__SSH__HOST_KEY_PATH
89 value: {{ printf "%s/ssh_host_ed25519" .Values.persistence.repos.mountPath | quote }}
82 value: {{ .Values.kenshell.sshClonePort | quote }}
9083{{- $base := printf "https://%s" .Values.ingress.host }}
9184- name: APP_WEB__WEB_BASE_URL
9285 value: {{ default $base .Values.config.env.APP_WEB__WEB_BASE_URL | quote }}
Deleted charts/core/templates/githost.yaml
@@ -1,162 +0,0 @@
1{{- if .Values.githost.enabled }}
2apiVersion: apps/v1
3kind: Deployment
4metadata:
5 name: {{ printf "%s-githost" (include "gitdata.fullname" .) }}
6 labels:
7 {{- include "gitdata.labels" . | nindent 4 }}
8 app.kubernetes.io/component: githost
9spec:
10 replicas: {{ .Values.githost.replicaCount }}
11 revisionHistoryLimit: 3
12 # host key 在共享 PVC 上,滚动更新时新旧 Pod 同读一把 key,无冲突
13 strategy:
14 type: RollingUpdate
15 rollingUpdate:
16 maxSurge: 1
17 maxUnavailable: 0
18 selector:
19 matchLabels:
20 {{- include "gitdata.selectorLabels" . | nindent 6 }}
21 app.kubernetes.io/component: githost
22 template:
23 metadata:
24 labels:
25 {{- include "gitdata.selectorLabels" . | nindent 8 }}
26 app.kubernetes.io/component: githost
27 {{- with .Values.podLabels }}
28 {{- toYaml . | nindent 8 }}
29 {{- end }}
30 annotations:
31 checksum/env: {{ toYaml .Values.config.env | sha256sum }}
32 {{- with .Values.podAnnotations }}
33 {{- toYaml . | nindent 8 }}
34 {{- end }}
35 spec:
36 serviceAccountName: {{ include "gitdata.serviceAccountName" . }}
37 {{- include "gitdata.imagePullSecrets" . | nindent 6 }}
38 terminationGracePeriodSeconds: 3660
39 securityContext:
40 {{- toYaml .Values.podSecurityContext | nindent 8 }}
41 {{- if .Values.githost.ssh.enabled }}
42 initContainers:
43 - name: ssh-hostkey
44 image: {{ include "gitdata.image" . }}
45 imagePullPolicy: {{ .Values.image.pullPolicy }}
46 command: ["/bin/sh", "-c"]
47 args:
48 - "test -f {{ .Values.persistence.repos.mountPath }}/ssh_host_ed25519 || ssh-keygen -q -t ed25519 -N '' -f {{ .Values.persistence.repos.mountPath }}/ssh_host_ed25519"
49 volumeMounts:
50 - name: repos
51 mountPath: {{ .Values.persistence.repos.mountPath }}
52 securityContext:
53 {{- toYaml .Values.securityContext | nindent 12 }}
54 {{- end }}
55 containers:
56 - name: githost
57 image: {{ include "gitdata.image" . }}
58 imagePullPolicy: {{ .Values.image.pullPolicy }}
59 args: ["githost"]
60 {{- include "gitdata.envFrom" . | nindent 10 }}
61 env:
62 {{- include "gitdata.env" . | nindent 12 }}
63 {{- with .Values.githost.extraEnv }}
64 {{- toYaml . | nindent 12 }}
65 {{- end }}
66 ports:
67 - name: smart-http
68 containerPort: {{ .Values.githost.httpPort }}
69 {{- if .Values.githost.ssh.enabled }}
70 - name: ssh
71 containerPort: {{ .Values.githost.ssh.containerPort }}
72 {{- end }}
73 livenessProbe:
74 httpGet: { path: /healthz, port: smart-http }
75 initialDelaySeconds: 15
76 periodSeconds: 20
77 readinessProbe:
78 httpGet: { path: /readyz, port: smart-http }
79 initialDelaySeconds: 5
80 periodSeconds: 10
81 securityContext:
82 {{- toYaml .Values.securityContext | nindent 12 }}
83 resources:
84 {{- toYaml .Values.githost.resources | nindent 12 }}
85 volumeMounts:
86 - name: repos
87 mountPath: {{ .Values.persistence.repos.mountPath }}
88 volumes:
89 - name: repos
90 {{- if .Values.persistence.repos.existingClaim }}
91 persistentVolumeClaim:
92 claimName: {{ .Values.persistence.repos.existingClaim }}
93 {{- else if .Values.persistence.repos.enabled }}
94 persistentVolumeClaim:
95 claimName: {{ printf "%s-repos" (include "gitdata.fullname" .) }}
96 {{- else }}
97 emptyDir: {}
98 {{- end }}
99---
100apiVersion: v1
101kind: Service
102metadata:
103 name: {{ printf "%s-githost" (include "gitdata.fullname" .) }}
104 labels:
105 {{- include "gitdata.labels" . | nindent 4 }}
106 app.kubernetes.io/component: githost
107spec:
108 type: ClusterIP
109 selector:
110 {{- include "gitdata.selectorLabels" . | nindent 4 }}
111 app.kubernetes.io/component: githost
112 ports:
113 - name: smart-http
114 port: {{ .Values.githost.httpPort }}
115 targetPort: smart-http
116{{- if .Values.githost.ssh.enabled }}
117---
118apiVersion: v1
119kind: Service
120metadata:
121 name: {{ printf "%s-githost-ssh" (include "gitdata.fullname" .) }}
122 labels:
123 {{- include "gitdata.labels" . | nindent 4 }}
124 app.kubernetes.io/component: githost
125 {{- with .Values.githost.ssh.service.annotations }}
126 annotations:
127 {{- toYaml . | nindent 4 }}
128 {{- end }}
129spec:
130 type: {{ .Values.githost.ssh.service.type }}
131 selector:
132 {{- include "gitdata.selectorLabels" . | nindent 4 }}
133 app.kubernetes.io/component: githost
134 {{- with .Values.githost.ssh.service.externalIPs }}
135 externalIPs:
136 {{- toYaml . | nindent 4 }}
137 {{- end }}
138 ports:
139 - name: ssh
140 port: {{ .Values.githost.ssh.service.port }}
141 targetPort: ssh
142 {{- if and (eq .Values.githost.ssh.service.type "NodePort") .Values.githost.ssh.service.nodePort }}
143 nodePort: {{ .Values.githost.ssh.service.nodePort }}
144 {{- end }}
145{{- end }}
146{{- if .Values.githost.pdb.enabled }}
147---
148apiVersion: policy/v1
149kind: PodDisruptionBudget
150metadata:
151 name: {{ printf "%s-githost" (include "gitdata.fullname" .) }}
152 labels:
153 {{- include "gitdata.labels" . | nindent 4 }}
154 app.kubernetes.io/component: githost
155spec:
156 minAvailable: {{ .Values.githost.pdb.minAvailable }}
157 selector:
158 matchLabels:
159 {{- include "gitdata.selectorLabels" . | nindent 6 }}
160 app.kubernetes.io/component: githost
161{{- end }}
162{{- end }}
Modified charts/core/templates/ingress.yaml View charts/core/templates/ingress.yaml
@@ -41,17 +41,6 @@
4141 - host: {{ .Values.ingress.host | quote }}
4242 http:
4343 paths:
44 # *.git 必须路由到 githost(反代契约 §3.1),排在 "/" 之前。
45 # 仅 nginx 走这里;traefik 见 templates/ingressroute-githost.yaml
46 {{- if and .Values.githost.enabled (eq $controller "nginx") }}
47 - path: {{ .Values.ingress.githostPath | quote }}
48 pathType: ImplementationSpecific
49 backend:
50 service:
51 name: {{ printf "%s-githost" $fullName }}
52 port:
53 name: smart-http
54 {{- end }}
5544 {{- if .Values.im.enabled }}
5645 # chat WS 长连接网关(最长前缀匹配优先于 "/")
5746 - path: /ws/v1
Deleted charts/core/templates/ingressroute-githost.yaml
@@ -1,39 +0,0 @@
1{{- /*
2 traefik 专用:把 *.git 的 git smart-HTTP 请求路由到 githost。
3
4 traefik 的 Kubernetes Ingress provider 只支持 Prefix/Exact,`ImplementationSpecific`
5 会被当作字面前缀,所以主 Ingress 里那条 `/(.*\.git)(/|$)` 正则在这里用
6 IngressRoute + PathRegexp 表达。cert-manager 的证书仍由主 Ingress 上的
7 `cert-manager.io/cluster-issuer` 注解签发,两边共用同一个 TLS secret。
8
9 仅在 .Values.githost.enabled 且 ingress.controller=traefik 时渲染。
10*/}}
11{{- if and .Values.ingress.enabled .Values.githost.enabled (eq (default "nginx" .Values.ingress.controller) "traefik") }}
12{{- $fullName := include "gitdata.fullname" . }}
13{{- $tlsSecret := default (printf "%s-tls" $fullName) .Values.ingress.tls.secretName }}
14apiVersion: traefik.io/v1alpha1
15kind: IngressRoute
16metadata:
17 name: {{ printf "%s-githost" $fullName }}
18 labels:
19 {{- include "gitdata.labels" . | nindent 4 }}
20 app.kubernetes.io/component: githost
21spec:
22 entryPoints:
23 - web
24 - websecure
25 routes:
26 # priority 高于主 Ingress 的 "/"(traefik 默认按 rule 长度比优先级,这里显式钉住)
27 - kind: Rule
28 match: Host(`{{ .Values.ingress.host }}`) && PathRegexp(`{{ .Values.ingress.githostPathRegexp }}`)
29 priority: 1000
30 services:
31 - name: {{ printf "%s-githost" $fullName }}
32 port: smart-http
33 # git push/pull 是长连接大包传输:不要做响应缓冲
34 passHostHeader: true
35 {{- if .Values.ingress.tls.enabled }}
36 tls:
37 secretName: {{ $tlsSecret }}
38 {{- end }}
39{{- end }}
Modified charts/core/templates/NOTES.txt View charts/core/templates/NOTES.txt
@@ -3,15 +3,9 @@
33Endpoints (ingress {{ .Values.ingress.enabled }}):
44{{- if .Values.ingress.enabled }}
55 https://{{ .Values.ingress.host }}/ -> apihost (REST/GraphQL/WS)
6 https://{{ .Values.ingress.host }}/{space}/{repo}.git/ -> githost smart HTTP
6 git clone https://{{ .Values.ingress.host }}/{space}/{repo}.git -> KenShell
77{{- else }}
88 kubectl port-forward svc/{{ .Values.fullnameOverride | default (printf "%s-%s" .Release.Name .Chart.Name) }}-apihost {{ .Values.apihost.port }}:{{ .Values.apihost.port }}
9{{- end }}
10{{- if and .Values.githost.enabled .Values.githost.ssh.enabled }}
11
12Git over SSH ({{ .Values.githost.ssh.service.type }}):
13 git clone {{ .Values.githost.ssh.cloneHost | default "<SSH-LB-IP>" }}:{{ "{{space}}" }}/{{ "{{repo}}" }}.git
14 (* SSH Service 已分配 EXTERNAL-IP 后,把 githost.ssh.cloneHost 设为该地址并 upgrade *)
159{{- end }}
1610
1711Check:
@@ -21,5 +15,5 @@
2115Reminders:
2216 - secret.env 中 APP_OAUTH__RPC_KEY / APP_INTERNAL__SERVICE_KEY / APP_POW__MAC_KEY / APP_AUTH__TOTP_ENCRYPTION_KEY 必填;
2317 生产环境建议 --set secret.existingSecret=<name> 并用外部 Secret 管理。
24 - repos PVC 需 RWX(NFS/CephFS/EFS);无 RWX 时将 githost/synchost 各缩到 1 副本并固定同节点。
18 - KenApi 不再持有 bare 仓库;clone/push 由 KenShell 提供,需配置 kenshell.endpoint。
2519 - 磁盘上的 config/local.toml 含真实密钥,部署前请轮换。
Modified charts/core/Chart.yaml View charts/core/Chart.yaml
@@ -1,6 +1,6 @@
11apiVersion: v2
22name: gitdata
3description: GitData — Rust workspace (apihost / githost / synchost / migrate)
3description: GitData — Rust workspace (apihost / synchost / webhook / migrate)
44type: application
55version: 0.1.0
66appVersion: "1.0.0"
Modified charts/core/values.yaml View charts/core/values.yaml
@@ -133,32 +133,15 @@
133133 requests: { cpu: 100m, memory: 128Mi }
134134 limits: { memory: 512Mi }
135135
136githost:
137 enabled: true
138 # SSH host key 落在共享 PVC 上;多副本前请预先放入同一把 key,
139 # 否则每个 Pod 各自生成会导致客户端 known_hosts 抖动
140 replicaCount: 1
141 httpPort: 9418
142 ssh:
143 enabled: true
144 # 非 root 进程绑不了 22,容器内监听 2222,Service 映射 22->2222
145 containerPort: 2222
146 service:
147 type: LoadBalancer
148 port: 22
149 nodePort: "" # type=NodePort 时可选
150 annotations: {}
151 externalIPs: []
152 # clone URL(对外展示/拼接用),一般填 LB 域名或 IP
153 cloneHost: ""
154 clonePort: 22
155 resources:
156 requests: { cpu: 250m, memory: 256Mi }
157 limits: { memory: 1Gi }
158 extraEnv: []
159 pdb:
160 enabled: false
161 minAvailable: 1
136kenshell:
137 # KenShell 是 Git 数据面:对象库、引用、pack、LFS、smart HTTP 与 SSH 都由它提供。
138 endpoint: "http://kenshell:50091"
139 rpcKey: ""
140 timeoutSecs: 3600
141 # 对外克隆地址(展示用),指向 KenShell 的 HTTP/SSH 入口
142 httpCloneBaseUrl: ""
143 sshCloneHost: ""
144 sshClonePort: 22
162145
163146synchost:
164147 enabled: true
@@ -177,7 +160,7 @@
177160 limits: { memory: 1Gi }
178161 extraEnv: []
179162
180# worker 类程序的独立健康端点(apihost/githost/im 挂在自己业务端口上)
163# worker 类程序的独立健康端点(apihost/im 挂在自己业务端口上)
181164health:
182165 enabled: true
183166 synchostPort: 3002
@@ -208,7 +191,7 @@
208191 minAvailable: 1
209192
210193# ---------------------------------------------------------------------------
211# 存储:githost 与 synchost 共享 bare 仓库,需要 RWX
194# 存储:保留给 search/synchost 的本地缓存目录(bare 仓库已迁到 KenShell)
212195# ---------------------------------------------------------------------------
213196persistence:
214197 repos:
@@ -220,26 +203,14 @@
220203 mountPath: /opt/gitdata
221204
222205# ---------------------------------------------------------------------------
223# Ingress:同源 TLS 终结;*.git -> githost,/ws/v1 -> im,其余 -> apihost
224#
225# controller 决定路由怎么写:
226# nginx —— 主 Ingress 用 regex path + ImplementationSpecific(配 use-regex 注解)
227# traefik —— 主 Ingress 只写前缀路由;*.git 由 templates/ingressroute-githost.yaml
228# 的 IngressRoute(PathRegexp)接管 —— traefik 的 Ingress provider 不支持
229# 正则 path,ImplementationSpecific 会被当字面前缀处理。
230# 其它控制器请把 controller 设成非 nginx 值,并按需在 className/annotations 里给出自己的语义。
206# Ingress:同源 TLS 终结;/ws/v1 -> im,其余 -> apihost。
207# git smart HTTP(*.git)由 KenShell 自己的入口接管,不在本 chart 内。
231208# ---------------------------------------------------------------------------
232209ingress:
233210 enabled: true
234211 controller: nginx
235212 className: nginx
236213 host: gitdata.example.com
237 # githost 路由匹配(仅 controller=nginx 使用)
238 githostPath: "^/(.*\\.git)(/|$)|^/[^/]+/[^/]+/(HEAD$|info/|objects/|git-upload-pack|git-receive-pack)"
239 # traefik 用的正则(覆盖 owner/repo.git 与多级 group);同时放行省略 .git 的 git
240 # 协议端点 —— actions/checkout 用 GITHUB_SERVER_URL + GITHUB_REPOSITORY 拼远程地址,
241 # 不带 .git 后缀,只按 *.git 匹配会把它打到 apihost 的 SPA 上。
242 githostPathRegexp: "^/.+\\.git(/|$)|^/[^/]+/[^/]+/(HEAD$|info/|objects/|git-upload-pack|git-receive-pack)"
243214 annotations: {}
244215 tls:
245216 enabled: true
Modified charts/prod/values/prod.yaml View charts/prod/values/prod.yaml
@@ -90,20 +90,13 @@
9090 periodSeconds: 60
9191
9292
93githost:
94 enabled: true
95 replicaCount: 1
96 httpPort: 9418
97 ssh:
98 enabled: true
99 containerPort: 2222
100 cloneHost: "ssh.gitdata.ai"
101 clonePort: 22
102 service:
103 type: LoadBalancer
104 port: 22
105 annotations:
106 metallb.universe.tf/loadBalancerIPs: "47.239.98.8"
93kenshell:
94 endpoint: "http://kenshell:50091"
95 rpcKey: ""
96 timeoutSecs: 3600
97 httpCloneBaseUrl: "https://gitdata.ai"
98 sshCloneHost: "ssh.gitdata.ai"
99 sshClonePort: 22
107100
108101webhook:
109102 enabled: true
Modified scripts/dev.ps1 View scripts/dev.ps1
@@ -28,18 +28,16 @@
2828$Services = [ordered]@{
2929 migrate = @{ Exe = 'migrate'; Port = 0; Health = $false }
3030 apihost = @{ Exe = 'apihost'; Port = 4081; Health = $true }
31 githost = @{ Exe = 'githost'; Port = 9418; Health = $true; Extra = @(4022) }
3231 im = @{ Exe = 'im'; Port = 4082; Health = $true }
3332 synchost = @{ Exe = 'synchost'; Port = 4083; Health = $true }
3433 searchhost = @{ Exe = 'searchhost'; Port = 0; Health = $false }
3534 webhook = @{ Exe = 'webhook'; Port = 3005; Health = $true }
3635 web = @{ Bun = 'web'; Port = 5173; Health = $false }
3736}
38$Order = @('migrate', 'apihost', 'githost', 'im', 'synchost', 'searchhost', 'webhook', 'web')
39$Backends = @('migrate', 'apihost', 'githost', 'im', 'synchost', 'searchhost', 'webhook')
37$Order = @('migrate', 'apihost', 'im', 'synchost', 'searchhost', 'webhook', 'web')
38$Backends = @('migrate', 'apihost', 'im', 'synchost', 'searchhost', 'webhook')
4039$Aliases = @{
4140 api = 'apihost'
42 git = 'githost'
4341 chat = 'im'
4442 sync = 'synchost'
4543 hook = 'webhook'
@@ -48,7 +46,6 @@
4846
4947$Urls = [ordered]@{
5048 apihost = 'http://127.0.0.1:4081 (Swagger: /api/docs)'
51 githost = 'http://127.0.0.1:9418 ssh://git@127.0.0.1:4022'
5249 im = 'ws://127.0.0.1:4082'
5350 synchost = 'http://127.0.0.1:4083/healthz'
5451 webhook = 'http://127.0.0.1:3005'
Modified scripts/dev.sh View scripts/dev.sh
@@ -26,8 +26,8 @@
2626DEV_HOST_KEY="$DEV_DIR/ssh_host_ed25519"
2727COMPOSE_FILE="$ROOT/docker-compose.dev.yml"
2828
29ORDER="apihost githost im synchost webhook web"
30BACKENDS="apihost githost im synchost webhook"
29ORDER="apihost im synchost webhook web"
30BACKENDS="apihost im synchost webhook"
3131INFRA="postgresql:5432 redis:6379 s3:9000 meilisearch:7700"
3232
3333if [ -t 1 ]; then
@@ -52,7 +52,6 @@
5252port_of() {
5353 case "$1" in
5454 apihost) echo 4081 ;;
55 githost) echo 9418 ;;
5655 im) echo 4082 ;;
5756 synchost) echo 4083 ;;
5857 webhook) echo 3005 ;;
@@ -63,7 +62,6 @@
6362
6463extra_port_of() {
6564 case "$1" in
66 githost) echo 4022 ;;
6765 *) echo 0 ;;
6866 esac
6967}
@@ -71,7 +69,6 @@
7169alias_of() {
7270 case "$1" in
7371 api) echo apihost ;;
74 git) echo githost ;;
7572 chat) echo im ;;
7673 sync) echo synchost ;;
7774 hook) echo webhook ;;
@@ -83,7 +80,6 @@
8380url_of() {
8481 case "$1" in
8582 apihost) echo "http://127.0.0.1:4081 (Swagger: /api/docs)" ;;
86 githost) echo "http://127.0.0.1:9418 ssh://git@127.0.0.1:4022" ;;
8783 im) echo "ws://127.0.0.1:4082" ;;
8884 synchost) echo "http://127.0.0.1:4083/healthz" ;;
8985 webhook) echo "http://127.0.0.1:3005" ;;
Modified justfile View justfile
@@ -44,10 +44,6 @@
4444infra-down:
4545 @{{ invoke }}"{{ dev }}" infra-down
4646
47# 生成 githost 使用的本地 SSH host key(target/dev/ssh_host_ed25519)
48hostkey:
49 @{{ invoke }}"{{ dev }}" hostkey
50
5147# 同步数据库 schema
5248migrate:
5349 @{{ invoke }}"{{ dev }}" migrate
@@ -66,7 +62,7 @@
6662up *svc='':
6763 @{{ invoke }}"{{ dev }}" up {{ svc }}
6864
69# 仅启动后端服务(apihost/githost/im/synchost/webhook)
65# 仅启动后端服务(apihost/im/synchost/webhook)
7066up-backend:
7167 @{{ invoke }}"{{ dev }}" up backends
7268
@@ -101,7 +97,6 @@
10197# URL 速查
10298urls:
10399 @echo " apihost http://127.0.0.1:4081 (Swagger: /api/docs)"
104 @echo " githost http://127.0.0.1:9418 ssh://git@127.0.0.1:4022"
105100 @echo " im ws://127.0.0.1:4082"
106101 @echo " synchost http://127.0.0.1:4083/healthz"
107102 @echo " webhook http://127.0.0.1:3005"