refactor: unify JwtOptions schema with obsidian-mcp + simplify deploy
Build Docker Image / build (push) Has been cancelled
Build Docker Image / deploy (push) Has been cancelled

- Config/JwtOptions: flatten SigningKeyCurrent/Previous into nested
  SigningKey { Current, Previous } class to match obsidian-mcp shape.
  Both services now bind the same env var pattern (Jwt__SigningKey__Current),
  removing the schema fork that caused gitea-mcp to start with empty keys
  when compose used the obsidian-mcp convention.
- Auth/JwtBearerSetup, appsettings.json, README: follow rename.
- .gitea/workflows/build-image.yml: deploy job no longer clones nas-infra
  to a temp dir (which lacks the gitignored .env.shared). Now cd directly
  into /volume1/docker/compose/gitea-mcp, exposed by gitea-runner mount.
This commit is contained in:
2026-05-16 17:24:09 +08:00
parent 8f35bf5b15
commit 0f07300cec
5 changed files with 28 additions and 35 deletions
+5 -17
View File
@@ -91,7 +91,10 @@ jobs:
needs: build needs: build
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
# deploy job 是独立 runner,凭据不跨 job 继承,必须再 login 一次 # 不再 clone nas-infradeploy 直接操作 NAS 上 /volume1/docker/compose/gitea-mcp/。
# 该目录由 gitea-runner 挂载暴露给 runnerhost 模式 + bind mount)。
# .env.shared 也在那一层(../.env.shared),不需要再注入凭据。
# nas-infra 的 compose 改动靠 NAS 上手动 `git pull` 同步,不进 CI 链路。
- name: Login to Gitea Container Registry - name: Login to Gitea Container Registry
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
@@ -100,26 +103,11 @@ jobs:
password: ${{ secrets.PACKAGES_TOKEN }} password: ${{ secrets.PACKAGES_TOKEN }}
- name: Pull and restart gitea-mcp - name: Pull and restart gitea-mcp
env:
NAS_INFRA_TOKEN: ${{ secrets.NAS_INFRA_TOKEN }}
run: | run: |
set -e set -e
cd /volume1/docker/compose/gitea-mcp
TMPDIR=$(mktemp -d)
trap 'rm -rf "$TMPDIR"' EXIT
if [ -n "$NAS_INFRA_TOKEN" ]; then
CLONE_URL="https://x-access-token:${NAS_INFRA_TOKEN}@git.zhengchentao.win/dev/nas-infra.git"
else
CLONE_URL="https://git.zhengchentao.win/dev/nas-infra.git"
fi
git clone --depth 1 "$CLONE_URL" "$TMPDIR/nas-infra"
cd "$TMPDIR/nas-infra/gitea-mcp"
docker compose pull docker compose pull
docker compose up -d docker compose up -d
sleep 3 sleep 3
docker compose ps docker compose ps
docker compose logs --tail=30 gitea-mcp docker compose logs --tail=30 gitea-mcp
+5 -5
View File
@@ -55,14 +55,14 @@ public static class JwtBearerSetup
/// </summary> /// </summary>
private static IEnumerable<SecurityKey> BuildSigningKeys(JwtOptions opts) private static IEnumerable<SecurityKey> BuildSigningKeys(JwtOptions opts)
{ {
if (string.IsNullOrWhiteSpace(opts.SigningKeyCurrent)) if (string.IsNullOrWhiteSpace(opts.SigningKey.Current))
throw new InvalidOperationException( throw new InvalidOperationException(
"Jwt:SigningKeyCurrent 未配置,gitea-mcp 无法启动。" + "Jwt:SigningKey:Current 未配置,gitea-mcp 无法启动。" +
"请在 .env.shared 设置 JWT_SIGNING_KEY_CURRENT 与 nas-auth 共用。"); "请在 .env.shared 设置 JWT_SIGNING_KEY_CURRENT 与 nas-auth 共用。");
yield return new SymmetricSecurityKey(Encoding.UTF8.GetBytes(opts.SigningKeyCurrent)); yield return new SymmetricSecurityKey(Encoding.UTF8.GetBytes(opts.SigningKey.Current));
if (!string.IsNullOrWhiteSpace(opts.SigningKeyPrevious)) if (!string.IsNullOrWhiteSpace(opts.SigningKey.Previous))
yield return new SymmetricSecurityKey(Encoding.UTF8.GetBytes(opts.SigningKeyPrevious)); yield return new SymmetricSecurityKey(Encoding.UTF8.GetBytes(opts.SigningKey.Previous));
} }
} }
+10 -7
View File
@@ -3,6 +3,7 @@ namespace GiteaMcp.Config;
/// <summary> /// <summary>
/// JWT 验签配置,与 nas-auth / obsidian-mcp 共用同款 HS256 对称密钥。 /// JWT 验签配置,与 nas-auth / obsidian-mcp 共用同款 HS256 对称密钥。
/// ValidIssuer = auth.zhengchentao.winValidAudience = gitea。 /// ValidIssuer = auth.zhengchentao.winValidAudience = gitea。
/// 环境变量:Jwt__Issuer, Jwt__Audience, Jwt__SigningKey__Current, Jwt__SigningKey__Previous
/// </summary> /// </summary>
public class JwtOptions public class JwtOptions
{ {
@@ -11,12 +12,14 @@ public class JwtOptions
public string Issuer { get; set; } = "https://auth.zhengchentao.win"; public string Issuer { get; set; } = "https://auth.zhengchentao.win";
public string Audience { get; set; } = "gitea"; public string Audience { get; set; } = "gitea";
/// <summary>当前签名密钥(HS256 对称密钥,base64 或原文均可,长度 >= 32 字节)</summary> public SigningKeyPair SigningKey { get; set; } = new();
public string SigningKeyCurrent { get; set; } = string.Empty;
/// <summary> public class SigningKeyPair
/// 上一轮密钥(轮换窗口内保留,允许旧 Token 继续使用)。 {
/// 留空表示不存在旧密钥。 /// <summary>当前签名密钥(HS256 对称密钥),env: Jwt__SigningKey__Current</summary>
/// </summary> public string Current { get; set; } = string.Empty;
public string SigningKeyPrevious { get; set; } = string.Empty;
/// <summary>上一轮密钥,密钥轮换过渡期用,env: Jwt__SigningKey__Previous(可为空)</summary>
public string? Previous { get; set; }
}
} }
+4 -4
View File
@@ -66,8 +66,8 @@ All tools require a valid JWT with `scope=read:gitea` issued by nas-auth.
| `Gitea__MaxFileBytes` | `1048576` | Max file read size in bytes (1MB) | | `Gitea__MaxFileBytes` | `1048576` | Max file read size in bytes (1MB) |
| `Jwt__Issuer` | `https://auth.zhengchentao.win` | Expected JWT issuer | | `Jwt__Issuer` | `https://auth.zhengchentao.win` | Expected JWT issuer |
| `Jwt__Audience` | `gitea` | Expected JWT audience | | `Jwt__Audience` | `gitea` | Expected JWT audience |
| `Jwt__SigningKeyCurrent` | *(required)* | HS256 signing key (shared with nas-auth) | | `Jwt__SigningKey__Current` | *(required)* | HS256 signing key (shared with nas-auth) |
| `Jwt__SigningKeyPrevious` | *(empty)* | Previous key for rotation window | | `Jwt__SigningKey__Previous` | *(empty)* | Previous key for rotation window |
| `ASPNETCORE_ENVIRONMENT` | `Production` | Use `Development` locally | | `ASPNETCORE_ENVIRONMENT` | `Production` | Use `Development` locally |
All secrets come from `/volume1/docker/compose/.env.shared` on NAS — never hardcode them. All secrets come from `/volume1/docker/compose/.env.shared` on NAS — never hardcode them.
@@ -95,7 +95,7 @@ dotnet user-jwts create \
--claim scope=read:gitea --claim scope=read:gitea
``` ```
Or use [jwt.io](https://jwt.io) with alg=HS256 and the key from `Jwt:SigningKeyCurrent`. Or use [jwt.io](https://jwt.io) with alg=HS256 and the key from `Jwt:SigningKey:Current`.
### 3. Test with MCP Inspector ### 3. Test with MCP Inspector
@@ -147,7 +147,7 @@ services:
- Gitea__RepoBlacklist= - Gitea__RepoBlacklist=
- Jwt__Issuer=https://auth.zhengchentao.win - Jwt__Issuer=https://auth.zhengchentao.win
- Jwt__Audience=gitea - Jwt__Audience=gitea
- Jwt__SigningKeyCurrent=${JWT_SIGNING_KEY_CURRENT} - Jwt__SigningKey__Current=${JWT_SIGNING_KEY_CURRENT}
- TZ=Asia/Shanghai - TZ=Asia/Shanghai
env_file: env_file:
- ../.env.shared - ../.env.shared
+4 -2
View File
@@ -17,8 +17,10 @@
"Jwt": { "Jwt": {
"Issuer": "https://auth.zhengchentao.win", "Issuer": "https://auth.zhengchentao.win",
"Audience": "gitea", "Audience": "gitea",
"SigningKeyCurrent": "", "SigningKey": {
"SigningKeyPrevious": "" "Current": "",
"Previous": ""
}
}, },
"Mcp": { "Mcp": {
"OAuthDiscovery": { "OAuthDiscovery": {