Skip to content

Commit 10a397f

Browse files
committed
chore: add Docker deployment workflow
1 parent 1d825a0 commit 10a397f

File tree

6 files changed

+296
-24
lines changed

6 files changed

+296
-24
lines changed

.dockerignore

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
.git
2+
.github
3+
.gradle
4+
.idea
5+
.kotlin
6+
**/build
7+
**/node_modules
8+
acidify-core-js
9+
acidify-docs
10+
kotlin-js-store
11+
yogurt-scripting
12+
yogurt-npm-packages
13+
*.iml

.github/workflows/build-yogurt-pmhq.yml

Lines changed: 98 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,56 @@ on:
55

66
permissions:
77
contents: write
8+
packages: write
89

910
jobs:
11+
resolve-meta:
12+
runs-on: ubuntu-latest
13+
14+
outputs:
15+
release_version: ${{ steps.meta.outputs.release_version }}
16+
release_tag: ${{ steps.meta.outputs.release_tag }}
17+
release_name: ${{ steps.meta.outputs.release_name }}
18+
docker_image: ${{ steps.meta.outputs.docker_image }}
19+
docker_version_tag: ${{ steps.meta.outputs.docker_version_tag }}
20+
docker_sha_tag: ${{ steps.meta.outputs.docker_sha_tag }}
21+
22+
steps:
23+
- name: Checkout repository
24+
uses: actions/checkout@v5
25+
26+
- name: Resolve release metadata
27+
id: meta
28+
run: |
29+
set -euo pipefail
30+
31+
version="$(python3 <<'PY'
32+
import re
33+
from pathlib import Path
34+
35+
gradle_file = Path("yogurt") / "build.gradle.kts"
36+
content = gradle_file.read_text(encoding="utf-8")
37+
match = re.search(r'version\s*=\s*"([^"]+)"', content)
38+
if not match:
39+
raise SystemExit("Cannot find version in yogurt/build.gradle.kts")
40+
print(match.group(1))
41+
PY
42+
)"
43+
short_hash="$(git rev-parse --short=7 HEAD)"
44+
release_tag="v${version}+${short_hash}"
45+
docker_version_tag="${version}-${short_hash}"
46+
docker_sha_tag="sha-${short_hash}"
47+
docker_image="ghcr.io/llonebot/yogurt-pmhq"
48+
49+
echo "release_version=${version}" >> "$GITHUB_OUTPUT"
50+
echo "release_tag=${release_tag}" >> "$GITHUB_OUTPUT"
51+
echo "release_name=${release_tag}" >> "$GITHUB_OUTPUT"
52+
echo "docker_image=${docker_image}" >> "$GITHUB_OUTPUT"
53+
echo "docker_version_tag=${docker_version_tag}" >> "$GITHUB_OUTPUT"
54+
echo "docker_sha_tag=${docker_sha_tag}" >> "$GITHUB_OUTPUT"
55+
1056
build-native:
57+
needs: resolve-meta
1158
runs-on: ${{ matrix.runs-on }}
1259

1360
strategy:
@@ -75,6 +122,7 @@ jobs:
75122
path: yogurt/build/bin/${{ matrix.target }}/releaseExecutable/**
76123

77124
build-jvm:
125+
needs: resolve-meta
78126
runs-on: ubuntu-latest
79127

80128
env:
@@ -102,6 +150,7 @@ jobs:
102150

103151
release-yogurt:
104152
needs:
153+
- resolve-meta
105154
- build-native
106155
- build-jvm
107156
runs-on: ubuntu-latest
@@ -134,34 +183,59 @@ jobs:
134183
cp release-input/yogurt-jvm/yogurt-jvm-all.jar release-assets/yogurt-jvm-all.jar
135184
fi
136185
137-
- name: Resolve release metadata
138-
id: release_meta
139-
run: |
140-
set -euo pipefail
141-
version="$(python3 <<'PY'
142-
import re
143-
from pathlib import Path
144-
145-
gradle_file = Path("yogurt") / "build.gradle.kts"
146-
content = gradle_file.read_text(encoding="utf-8")
147-
match = re.search(r'version\s*=\s*"([^"]+)"', content)
148-
if not match:
149-
raise SystemExit("Cannot find version in yogurt/build.gradle.kts")
150-
print(match.group(1))
151-
PY
152-
)"
153-
short_hash="$(git rev-parse --short=7 HEAD)"
154-
tag="v${version}+${short_hash}"
155-
echo "release_version=${version}" >> "$GITHUB_OUTPUT"
156-
echo "release_tag=${tag}" >> "$GITHUB_OUTPUT"
157-
echo "release_name=${tag}" >> "$GITHUB_OUTPUT"
158-
159186
- name: Create GitHub release
160187
uses: softprops/action-gh-release@v2
161188
with:
162-
tag_name: ${{ steps.release_meta.outputs.release_tag }}
163-
name: ${{ steps.release_meta.outputs.release_name }}
189+
tag_name: ${{ needs.resolve-meta.outputs.release_tag }}
190+
name: ${{ needs.resolve-meta.outputs.release_name }}
164191
generate_release_notes: false
165192
files: release-assets/*
166193
body: |
167194
Yogurt PMHQ build for commit `${{ github.sha }}`
195+
196+
publish-docker:
197+
needs: resolve-meta
198+
runs-on: ubuntu-latest
199+
200+
steps:
201+
- name: Checkout repository
202+
uses: actions/checkout@v5
203+
204+
- name: Set up Docker Buildx
205+
uses: docker/setup-buildx-action@v3
206+
207+
- name: Log in to GHCR
208+
uses: docker/login-action@v3
209+
with:
210+
registry: ghcr.io
211+
username: ${{ github.actor }}
212+
password: ${{ secrets.GITHUB_TOKEN }}
213+
214+
- name: Extract Docker metadata
215+
id: docker_meta
216+
uses: docker/metadata-action@v5
217+
with:
218+
images: ${{ needs.resolve-meta.outputs.docker_image }}
219+
tags: |
220+
type=raw,value=latest
221+
type=raw,value=${{ needs.resolve-meta.outputs.release_version }}
222+
type=raw,value=${{ needs.resolve-meta.outputs.docker_version_tag }}
223+
type=raw,value=${{ needs.resolve-meta.outputs.docker_sha_tag }}
224+
labels: |
225+
org.opencontainers.image.title=yogurt-pmhq
226+
org.opencontainers.image.description=Yogurt PMHQ Docker image
227+
org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
228+
org.opencontainers.image.revision=${{ github.sha }}
229+
org.opencontainers.image.version=${{ needs.resolve-meta.outputs.release_tag }}
230+
231+
- name: Build and push Docker image
232+
uses: docker/build-push-action@v6
233+
with:
234+
context: .
235+
file: docker/Dockerfile
236+
platforms: linux/amd64
237+
push: true
238+
tags: ${{ steps.docker_meta.outputs.tags }}
239+
labels: ${{ steps.docker_meta.outputs.labels }}
240+
cache-from: type=gha
241+
cache-to: type=gha,mode=max

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,18 @@ Yogurt 启动并登录完成后,HTTP API 的地址是 `http://<host>:<port>/ap
8787

8888
[Yogurt 文档的“日志配置”部分](https://acidify.ntqqrev.org/yogurt/configuration#%E6%97%A5%E5%BF%97%E9%85%8D%E7%BD%AE)
8989

90+
## Docker 部署
91+
92+
如果准备把 Yogurt 和 PMHQ 一起部署,仓库里已经提供了一套可以直接使用的 Docker 文件,位于 `docker/` 目录。`docker/docker-compose.yml` 默认使用 `ghcr.io/llonebot/yogurt-pmhq:latest`,并将 `pmhq``yogurt` 放在同一个网络里。Yogurt 会直接通过容器名 `pmhq` 连接 `ws://pmhq:13000/ws`,不需要再额外处理容器间通信。
93+
94+
PMHQ 的 QQ 配置目录会挂载到独立卷 `qq_volume`。Yogurt 的运行目录会挂载到 `./data`,启动时自动生成 `config.json` 和脚本目录,适合长期保留登录状态与本地数据。镜像内部运行的是 `linuxX64` 的 Kotlin/Native 可执行文件,而不是 JVM fat jar,因此运行时占用会更小一些。
95+
96+
首次使用时,在仓库根目录执行 `docker compose -f docker/docker-compose.yml up -d` 即可。启动后,Yogurt 会根据容器环境变量自动写出配置文件。最常用的几项已经放在 compose 文件里,包括 `YOGURT_PORT``YOGURT_ACCESS_TOKEN``QUICK_LOGIN_UIN``PRELOAD_CONTACTS``REPORT_SELF_MESSAGE``SKIP_SECURITY_CHECK`
97+
98+
如果需要跨域访问,可以填写 `HTTP_CORS_ORIGINS`,多个来源使用逗号分隔。如果需要事件推送,可以填写 `WEBHOOK_URLS`,同样使用逗号分隔,并在需要时通过 `WEBHOOK_ACCESS_TOKEN` 配置推送鉴权。默认情况下,HTTP 接口会映射到宿主机的 `3000` 端口,PMHQ 不对外暴露端口,只在 compose 内部网络中提供服务。
99+
100+
Yogurt 容器额外写入了 `host.docker.internal:host-gateway`,因此 webhook 或其他回调服务如果运行在宿主机上,也可以直接从容器内访问。如果你确实需要从宿主机直接访问 PMHQ,再自行给 `pmhq` 服务补端口映射即可。
101+
90102
## 支持平台
91103

92104
- Kotlin/JVM

docker/Dockerfile

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
FROM eclipse-temurin:25-jdk AS builder
2+
3+
WORKDIR /workspace
4+
5+
RUN apt-get update \
6+
&& apt-get install -y --no-install-recommends build-essential clang curl unzip xz-utils \
7+
&& rm -rf /var/lib/apt/lists/*
8+
9+
COPY . .
10+
11+
RUN ./gradlew :yogurt:linkReleaseExecutableLinuxX64 --no-daemon \
12+
&& cp /workspace/yogurt/build/bin/linuxX64/releaseExecutable/yogurt.kexe /tmp/yogurt
13+
14+
FROM debian:bookworm-slim
15+
16+
RUN apt-get update \
17+
&& apt-get install -y --no-install-recommends ca-certificates jq libcurl4 \
18+
&& rm -rf /var/lib/apt/lists/*
19+
20+
WORKDIR /app
21+
22+
COPY --from=builder /tmp/yogurt /app/yogurt
23+
COPY docker/startup.sh /app/startup.sh
24+
25+
RUN chmod +x /app/startup.sh /app/yogurt
26+
27+
ENV YOGURT_DATA_DIR=/app/data
28+
29+
ENTRYPOINT ["/app/startup.sh"]

docker/docker-compose.yml

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
services:
2+
pmhq:
3+
image: linyuchen/pmhq:latest
4+
container_name: pmhq
5+
privileged: true
6+
environment:
7+
- ENABLE_HEADLESS=true
8+
- AUTO_LOGIN_QQ=
9+
volumes:
10+
- qq_volume:/root/.config/QQ
11+
networks:
12+
- app_network
13+
restart: unless-stopped
14+
healthcheck:
15+
test: ["CMD", "curl", "-f", "http://localhost:13000/health"]
16+
interval: 30s
17+
timeout: 10s
18+
retries: 3
19+
start_period: 40s
20+
21+
yogurt:
22+
image: ghcr.io/llonebot/yogurt-pmhq:latest
23+
container_name: yogurt-pmhq
24+
platform: linux/amd64
25+
environment:
26+
- PMHQ_HOST=pmhq
27+
- PMHQ_PORT=13000
28+
- YOGURT_HOST=0.0.0.0
29+
- YOGURT_PORT=3000
30+
- YOGURT_ACCESS_TOKEN=change-me
31+
- HTTP_CORS_ORIGINS=
32+
- WEBHOOK_URLS=
33+
- WEBHOOK_ACCESS_TOKEN=
34+
- QUICK_LOGIN_UIN=
35+
- PRELOAD_CONTACTS=false
36+
- REPORT_SELF_MESSAGE=true
37+
- TRANSFORM_INCOMING_MFACE_TO_IMAGE=false
38+
- SKIP_SECURITY_CHECK=true
39+
- ANSI_LEVEL=ANSI256
40+
- CORE_LOG_LEVEL=DEBUG
41+
extra_hosts:
42+
- "host.docker.internal:host-gateway"
43+
volumes:
44+
- ./data:/app/data
45+
ports:
46+
- "3000:3000"
47+
depends_on:
48+
pmhq:
49+
condition: service_healthy
50+
networks:
51+
- app_network
52+
restart: unless-stopped
53+
54+
volumes:
55+
qq_volume:
56+
57+
networks:
58+
app_network:
59+
driver: bridge

docker/startup.sh

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
#!/bin/sh
2+
set -eu
3+
4+
DATA_DIR="${YOGURT_DATA_DIR:-/app/data}"
5+
PMHQ_HOST="${PMHQ_HOST:-pmhq}"
6+
PMHQ_PORT="${PMHQ_PORT:-13000}"
7+
YOGURT_HOST="${YOGURT_HOST:-0.0.0.0}"
8+
YOGURT_PORT="${YOGURT_PORT:-3000}"
9+
YOGURT_ACCESS_TOKEN="${YOGURT_ACCESS_TOKEN:-}"
10+
HTTP_CORS_ORIGINS="${HTTP_CORS_ORIGINS:-}"
11+
WEBHOOK_URLS="${WEBHOOK_URLS:-}"
12+
WEBHOOK_ACCESS_TOKEN="${WEBHOOK_ACCESS_TOKEN:-}"
13+
QUICK_LOGIN_UIN="${QUICK_LOGIN_UIN:-}"
14+
PRELOAD_CONTACTS="${PRELOAD_CONTACTS:-false}"
15+
REPORT_SELF_MESSAGE="${REPORT_SELF_MESSAGE:-true}"
16+
TRANSFORM_INCOMING_MFACE_TO_IMAGE="${TRANSFORM_INCOMING_MFACE_TO_IMAGE:-false}"
17+
SKIP_SECURITY_CHECK="${SKIP_SECURITY_CHECK:-true}"
18+
ANSI_LEVEL="${ANSI_LEVEL:-ANSI256}"
19+
CORE_LOG_LEVEL="${CORE_LOG_LEVEL:-DEBUG}"
20+
21+
mkdir -p "$DATA_DIR" "$DATA_DIR/scripts"
22+
23+
if [ -n "$QUICK_LOGIN_UIN" ]; then
24+
QUICK_LOGIN_UIN_JSON="$QUICK_LOGIN_UIN"
25+
else
26+
QUICK_LOGIN_UIN_JSON="null"
27+
fi
28+
29+
jq -n \
30+
--arg pmhqUrl "ws://${PMHQ_HOST}:${PMHQ_PORT}/ws" \
31+
--argjson quickLoginUin "$QUICK_LOGIN_UIN_JSON" \
32+
--argjson preloadContacts "$PRELOAD_CONTACTS" \
33+
--argjson reportSelfMessage "$REPORT_SELF_MESSAGE" \
34+
--argjson transformIncomingMFaceToImage "$TRANSFORM_INCOMING_MFACE_TO_IMAGE" \
35+
--argjson skipSecurityCheck "$SKIP_SECURITY_CHECK" \
36+
--arg host "$YOGURT_HOST" \
37+
--arg accessToken "$YOGURT_ACCESS_TOKEN" \
38+
--arg corsOrigins "$HTTP_CORS_ORIGINS" \
39+
--arg webhookUrls "$WEBHOOK_URLS" \
40+
--arg webhookAccessToken "$WEBHOOK_ACCESS_TOKEN" \
41+
--arg ansiLevel "$ANSI_LEVEL" \
42+
--arg coreLogLevel "$CORE_LOG_LEVEL" \
43+
--argjson port "$YOGURT_PORT" \
44+
'{
45+
signApiUrl: "",
46+
pmhqUrl: $pmhqUrl,
47+
quickLoginUin: $quickLoginUin,
48+
protocol: {
49+
os: "Linux",
50+
version: "fetched"
51+
},
52+
androidCredentials: {
53+
uin: 0,
54+
password: ""
55+
},
56+
androidUseLegacySign: false,
57+
reportSelfMessage: $reportSelfMessage,
58+
preloadContacts: $preloadContacts,
59+
transformIncomingMFaceToImage: $transformIncomingMFaceToImage,
60+
httpConfig: {
61+
host: $host,
62+
port: $port,
63+
accessToken: $accessToken,
64+
corsOrigins: (
65+
$corsOrigins
66+
| if . == "" then [] else split(",") | map(gsub("^\\s+|\\s+$"; "")) | map(select(length > 0)) end
67+
)
68+
},
69+
webhookConfig: {
70+
url: (
71+
$webhookUrls
72+
| if . == "" then [] else split(",") | map(gsub("^\\s+|\\s+$"; "")) | map(select(length > 0)) end
73+
),
74+
accessToken: $webhookAccessToken
75+
},
76+
logging: {
77+
ansiLevel: $ansiLevel,
78+
coreLogLevel: $coreLogLevel
79+
},
80+
skipSecurityCheck: $skipSecurityCheck
81+
}' > "$DATA_DIR/config.json"
82+
83+
cd "$DATA_DIR"
84+
85+
exec /app/yogurt

0 commit comments

Comments
 (0)