Featured image of post 迁移到 Gitea Actions:kill-PAT 完整方案

迁移到 Gitea Actions:kill-PAT 完整方案

本文记录了将代码仓库从 GitHub 迁移到自建 Gitea 过程中遇到的 runner 配置问题及解决方案,重点介绍了如何通过 DEFAULT_ACTIONS_URL=self 机制替代 Personal Access Token,实现跨仓 action 的无缝访问。

语速

由于 GitHub 近期的不稳定,以及自建的 runner 消耗比较多的上下行流量,我尝试将代码仓库迁移到 gitea。在这个过程中,我踩了不少 runner 的坑。这里记录一下。

TL;DR

如果读者希望自己的 LLM 不踩坑,可以这样:

这是别人迁移 gitea 的时候踩的坑,我希望你做参考。<本文链接>

状态:闭环验证通过(2026-06-17,run #39404 / PR #5)(inside id) 适用:self-hosted Gitea 1.26.x + gitea-runner 1.0.8 + 自签证书环境 详细调查记录journal/2026-06-17-default-actions-url-self-investigation.md(续一~续四)(svtter/ops journal)


背景

把 GitHub Actions workflow(sun-praise/latex-agent 21 个 workflow)迁到内网 Gitea。目标是杀掉 Personal Access Token(PAT),让所有跨仓 action 访问用 gitea 原生的 job token + DEFAULT_ACTIONS_URL=self 机制。

PAT 方案(06-14 journal 记录)能用但脏:每个消费方仓要存 OPENCODE_ACTIONS_PAT secret,action 代码要做本地 checkout workaround。kill-PAT 后 workflow 写法干净(uses: org/action@ref 直接生效),跟 GitHub Actions 一致。


前置条件

要求
Gitea 版本≥ 1.26.2(含 PR #32562 collaborative owners + #36173 job token 跨仓权限)
Runnergitea-runner 1.0.8(act_runner 改名后的新仓 gitea.com/gitea/runner
网络runner host 能访问 gitea server(同内网)
证书自签或受信任 CA(本方案处理自签场景)

核心配置(5 处改动)

1. Gitea server:启用 self 模式

/volume1/docker/gitea/gitea/gitea/conf/app.ini(容器内 /data/gitea/conf/app.ini):

1
2
3
[actions]
ENABLED = true
DEFAULT_ACTIONS_URL = self

重启 gitea:docker compose restart server

验证:改成 bogus_value 重启应启动失败,证明配置在读。

2. Runner 注册地址对齐 gitea AppURL

/var/lib/act_runner/.runner(runner host 上):

1
2
3
4
{
  "address": "https://gitea.my-nas.lan",
  ...
}

必须跟 gitea 的 ROOT_URL / AppURL host 完全一致。否则 shouldCloneURLUseTokenact/runner/reusable_workflow.go:306)做 host 字符串比较会 mismatch → 跨仓 clone 不带 token → 401。

我们这里 gitea.local(旧注册)→ gitea.my-nas.lan(gitea AppURL)。

3. Runner image:自构建(cert + gitconfig + curlrc)

官方 gitea/runner-images:ubuntu-latest 在自签证书环境会撞 TLS 校验。自构建一层 overlay:

1
2
3
4
5
6
7
8
FROM gitea/runner-images:ubuntu-latest
COPY gitea-my-nas.crt /usr/local/share/ca-certificates/gitea-my-nas.crt
RUN update-ca-certificates
RUN git config --global http.sslVerify false
RUN git config --system http.sslVerify false
# curl 单独一条线,跟 git 的 TLS 校验独立
RUN echo "insecure" > /root/.curlrc
RUN echo "insecure" > /home/ubuntu/.curlrc && chown ubuntu:ubuntu /home/ubuntu/.curlrc

构建 + 打 tag:

1
sudo docker build -t gitea/runner-images:ubuntu-latest-gitea-self ~/runner-image-build

关键gitea-my-nas.crt 是 gitea 的 self-signed leaf 证书(NAS 上 /volume1/docker/gitea/gitea/gitea/npm-gitea.cert.pem)。update-ca-certificates 装它其实无效(leaf 不是 CA),但留着没坏处。真正起作用的是 git/curl 的 sslVerify/insecure。

4. Runner config.yaml:labels + force_pull

/var/lib/act_runner/config.yaml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
runner:
  labels:
    - ubuntu-latest:docker://gitea/runner-images:ubuntu-latest-gitea-self
    - ubuntu-22.04:docker://gitea/runner-images:ubuntu-22.04
    - ubuntu-20.04:docker://gitea/runner-images:ubuntu-20.04

container:
  force_pull: false    # 默认 true,离线环境必须关
  options: --user ubuntu -v /etc/ssl/certs/ca-certificates.crt:/etc/ssl/certs/ca-certificates.crt:ro
  valid_volumes:
    - '**'

两个关键点

  • config.yaml 是 source of truth,daemon 启动时覆盖 .runner 文件。改 .runner labels 无效。
  • force_pull: true 每次都去 docker hub 拉 image,runner host 访问不了 hub 就全挂。离线必须 false

5. actions org + 公共 action 镜像

消费方 workflow 里 uses: actions/cache@v5 这种裸写的公共 action,在 self 模式下会解析到 gitea.local/actions/cache。需要镜像。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# 建 org(用 admin token)
curl -X POST "$GITEA/api/v1/orgs" -H "Authorization: token $TOKEN" \
  -d '{"username":"actions","visibility":"public"}'

# 镜像 actions/cache(pull mirror,168h 自动同步)
curl -X POST "$GITEA/api/v1/repos/migrate" -H "Authorization: token $TOKEN" \
  -d '{
    "clone_addr": "https://github.com/actions/cache.git",
    "repo_owner": "actions",
    "repo_name": "cache",
    "service": "github",
    "mirror": true,
    "mirror_interval": "168h",
    "private": false,
    "releases": true
  }'

org 必须是 public——private org 即使 repo 标 public,对外仍不可见(anonymous git 访问 401)。

按 workflow 实际 uses: 列表镜像。常见需要:actions/checkout(如果不用绝对 URL)、actions/cacheactions/setup-node 等。我们这次 workflow 对 actions/checkout 用了绝对 URL(uses: https://github.com/actions/checkout@v4),只需镜像 actions/cache(multi-review 传递依赖)。


Workflow 改造

消费方仓的 workflow 从 PAT+local-checkout 配方改成干净 native uses:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
name: review
on: [pull_request]

permissions:
  contents: read
  pull-requests: write

jobs:
  review:
    runs-on: ubuntu-latest
    env:
      GIT_SSL_NO_VERIFY: '1'    # action 内部 git 的 TLS 兜底
    steps:
      # 公共 action:绝对 URL 绕过 self 解析,直接从 github 拉
      - uses: https://github.com/actions/checkout@v4
        with:
          fetch-depth: 0

      # 自有 action:裸写,self 解析到本地 gitea
      - uses: sun-praise/opencode-actions/multi-review@v4
        with:
          model: litellm/deepseek-v4-flash
          cache: false
          default-team: "quality:1"
          timeout-seconds: "300"
          gitea-token: ${{ secrets.GITHUB_TOKEN }}
          litellm-url: ${{ secrets.LITELLM_URL }}
          litellm-api-key: ${{ secrets.LITELLM_API_KEY }}
        env:
          GITEA_API_URL: ${{ github.server_url }}/api/v1
          GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}

关键约定

  • GITHUB_TOKEN(gitea Actions 自动注入的 job token)替代 PAT。所有跨仓认证用它。
  • permissions: 显式声明,受 org/repo Actions 设置 clamp(PR #36173)。
  • 公共 action 用绝对 URL 或镜像——二选一,看是否要 forward compat(绝对 URL 写法在 GitHub 上也认,镜像写法只在 gitea 上认)。

Gitea 仓级配置

消费方仓的 Settings → Actions → General

  • Actions enabled:开
  • Default Actions Permissions:Allow all actions(或按需 restrict)
  • Cross-Repository Access:Selected,勾上消费方要用的所有自有 action repo(如 opencode-actions)——这是 PR #32562 collaborative owners 功能,让 job token 能跨仓读

自有 action 仓(sun-praise/opencode-actions):

  • 可以是 private(job token 通过 collaborative owners 有读权限)

验证

闭环验证(推荐)

消费方仓提一个测试 PR,触发 on: pull_request workflow。预期:

  1. workflow run status → success
  2. PR 上出现 review 评论(或 action 的预期产出)

分层验证(debug 用)

验证方法
gitea self 配置app.inibogus_value 重启应失败
runner URL 对齐.runner address == gitea AppURL host
runner image TLScontainer 内 curl -sI https://gitea.../api/v1/version 不报证书错
actions mirrorcurl https://gitea.local/actions/cache.git/info/refs?service=git-upload-pack 匿名 200
job token 跨仓DB 查 action_run_job.token_permissions,Code unit 有 read 权限

真正 kill PAT(闭环验证通过后)

  1. 删消费方仓的 OPENCODE_ACTIONS_PAT secret
    1
    2
    
    curl -X DELETE "$GITEA/api/v1/repos/<owner>/<repo>/actions/secrets/OPENCODE_ACTIONS_PAT" \
      -H "Authorization: token $ADMIN_TOKEN"
    
  2. 删 org 级 PAT(如果之前配在 org):同上换 /orgs/<org>/actions/secrets/
  3. revoke admin 用户 token:web UI → Settings → Applications → Revoke
  4. workflow 文件清理:去掉 PAT 相关的 with: 和本地 checkout workaround

踩坑总结(按踩中顺序)

  1. 以为 act_runner 有 bug——读了三仓源码,写了 patch,最后发现是配置问题。教训:先怀疑配置,再怀疑代码。
  2. .runner 文件改了不生效——daemon 用 config.yaml 覆盖。改 labels 要改 config.yaml。
  3. update-ca-certificates 装自签 leaf 无效——leaf 不是 CA,不能签其他证书。要靠 sslVerify/insecure 绕。
  4. git 和 curl 的 TLS 校验独立——git 的 sslVerify=false 不管 curl。curl 读 ~/.curlrc(不是 /etc/curlrc)。
  5. force_pull: true 离线致命——每次 run 强拉 docker hub。离线必关。
  6. private org 的 public repo 仍不可见——org visibility 是外层约束。镜像公共 action 的 org 必须 public。
  7. host 字符串严格相等——gitea.localgitea.my-nas.lan,即使解析到同 IP。runner 注册地址必须跟 gitea AppURL 完全一致。
  8. 闭环验证才算数——每层"通了"都是局部判断。multi-review 真跑通 PR review + post 评论,端到端通了才算。

参考链接

  • 调查 journal:journal/2026-06-17-default-actions-url-self-investigation.md(续一~续四)
  • gitea PR #32562(collaborative owners):https://github.com/go-gitea/gitea/pull/32562
  • gitea PR #36173(job token 权限):https://github.com/go-gitea/gitea/pull/36173
  • gitea issue #36817(self 模式的误报):https://github.com/go-gitea/gitea/issues/36817
  • act_runner 源码:https://gitea.com/gitea/runner
  • runner image 源码:https://gitea.com/gitea/runner_images

附录:本次验证的实际 run

  • PR:https://gitea.my-nas.lan/sun-praise/test-opencode-consumer/pulls/5
  • Run:https://gitea.my-nas.lan/sun-praise/test-opencode-consumer/actions/runs/39404
  • 结果:multi-review 拿到 PR diff(1282 chars)→ 调 LiteLLM/DeepSeek review → post 评论到 PR。全程零 PAT。