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
runs-on: ubuntu-latest
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
uses: docker/login-action@v3
with:
@@ -100,26 +103,11 @@ jobs:
password: ${{ secrets.PACKAGES_TOKEN }}
- name: Pull and restart gitea-mcp
env:
NAS_INFRA_TOKEN: ${{ secrets.NAS_INFRA_TOKEN }}
run: |
set -e
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"
cd /volume1/docker/compose/gitea-mcp
docker compose pull
docker compose up -d
sleep 3
docker compose ps
docker compose logs --tail=30 gitea-mcp
+5 -5
View File
@@ -55,14 +55,14 @@ public static class JwtBearerSetup
/// </summary>
private static IEnumerable<SecurityKey> BuildSigningKeys(JwtOptions opts)
{
if (string.IsNullOrWhiteSpace(opts.SigningKeyCurrent))
if (string.IsNullOrWhiteSpace(opts.SigningKey.Current))
throw new InvalidOperationException(
"Jwt:SigningKeyCurrent 未配置,gitea-mcp 无法启动。" +
"Jwt:SigningKey:Current 未配置,gitea-mcp 无法启动。" +
"请在 .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))
yield return new SymmetricSecurityKey(Encoding.UTF8.GetBytes(opts.SigningKeyPrevious));
if (!string.IsNullOrWhiteSpace(opts.SigningKey.Previous))
yield return new SymmetricSecurityKey(Encoding.UTF8.GetBytes(opts.SigningKey.Previous));
}
}
+10 -7
View File
@@ -3,6 +3,7 @@ namespace GiteaMcp.Config;
/// <summary>
/// JWT 验签配置,与 nas-auth / obsidian-mcp 共用同款 HS256 对称密钥。
/// ValidIssuer = auth.zhengchentao.winValidAudience = gitea。
/// 环境变量:Jwt__Issuer, Jwt__Audience, Jwt__SigningKey__Current, Jwt__SigningKey__Previous
/// </summary>
public class JwtOptions
{
@@ -11,12 +12,14 @@ public class JwtOptions
public string Issuer { get; set; } = "https://auth.zhengchentao.win";
public string Audience { get; set; } = "gitea";
/// <summary>当前签名密钥(HS256 对称密钥,base64 或原文均可,长度 >= 32 字节)</summary>
public string SigningKeyCurrent { get; set; } = string.Empty;
public SigningKeyPair SigningKey { get; set; } = new();
/// <summary>
/// 上一轮密钥(轮换窗口内保留,允许旧 Token 继续使用)。
/// 留空表示不存在旧密钥。
/// </summary>
public string SigningKeyPrevious { get; set; } = string.Empty;
public class SigningKeyPair
{
/// <summary>当前签名密钥(HS256 对称密钥),env: Jwt__SigningKey__Current</summary>
public string Current { 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) |
| `Jwt__Issuer` | `https://auth.zhengchentao.win` | Expected JWT issuer |
| `Jwt__Audience` | `gitea` | Expected JWT audience |
| `Jwt__SigningKeyCurrent` | *(required)* | HS256 signing key (shared with nas-auth) |
| `Jwt__SigningKeyPrevious` | *(empty)* | Previous key for rotation window |
| `Jwt__SigningKey__Current` | *(required)* | HS256 signing key (shared with nas-auth) |
| `Jwt__SigningKey__Previous` | *(empty)* | Previous key for rotation window |
| `ASPNETCORE_ENVIRONMENT` | `Production` | Use `Development` locally |
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
```
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
@@ -147,7 +147,7 @@ services:
- Gitea__RepoBlacklist=
- Jwt__Issuer=https://auth.zhengchentao.win
- Jwt__Audience=gitea
- Jwt__SigningKeyCurrent=${JWT_SIGNING_KEY_CURRENT}
- Jwt__SigningKey__Current=${JWT_SIGNING_KEY_CURRENT}
- TZ=Asia/Shanghai
env_file:
- ../.env.shared
+4 -2
View File
@@ -17,8 +17,10 @@
"Jwt": {
"Issuer": "https://auth.zhengchentao.win",
"Audience": "gitea",
"SigningKeyCurrent": "",
"SigningKeyPrevious": ""
"SigningKey": {
"Current": "",
"Previous": ""
}
},
"Mcp": {
"OAuthDiscovery": {