FastGPT maintains two sets of CI workflows simultaneously:
.github/workflows/— runs on GitHub-hosted runners , publishes to ghcr.io + Alibaba Cloud + Docker Hub, aimed at the open-source community..forgejo/workflows/— runs on internal Forgejo + self-hosted runners , publishes to the registry bundled with Forgejo, aimed at internal image distribution. The internal environment requires proxies, mirror sources, and a self-hosted registry.
The two workflows with the same name must remainfunctionally consistent: build steps, test logic, and trigger conditions aligned. However, the Forgejo version needs to remove GitHub-specific capabilities (attestations, github-script, PR comments) and inject internal proxy/mirror configuration.
Why do this: the internal Forgejo runner cannot access ghcr.io / Alibaba Cloud / Docker Hub login credentials, and has no GitHub PR comment API; but the build and tests themselves must run successfully on the internal network. So it is not "copy-paste", but "equivalent rewriting".
When to use this skill
- Modifying
.github/workflows/any workflow file under - Adding a new
build-*.yml/test-*.yamlworkflow - Changes involve image registry, artifact, permissions, runs-on, trigger
- When reviewing a PR, finding that only the GitHub side was changed and the Forgejo side was left untouched
Current pairing list (verify before making changes)
Existing Forgejo counterparts (9, must be synchronized when changed):
| GitHub | Forgejo | Type |
|---|---|---|
build-fastgpt.yml |
✅ | Image build |
build-code-sandbox.yml |
✅ | Image build |
build-agent-sandbox.yml |
✅ | Image build |
build-fastgpt-ide-agent.yml |
✅ | Image build |
build-marketplace.yml |
✅ | Image build |
build-mcp-server.yml |
✅ | Image build |
test-fastgpt.yaml |
✅ | Test + coverage |
test-sandbox.yaml |
✅ | Test |
test-rust-agent.yaml |
✅ | Test (currentlycompletely identical, no conversion) |
The list will change as the repository evolves. Before each operation, use
ls .forgejo/workflows/to verify the actual files; do not rely solely on the table above.
GitHub-only, currently with no Forgejo counterpart (13, usually no synchronization needed):
auto-close-issue.yml、build-admin.yml、build-browser-sandbox.yml、build-docs.yml、build-sso-service-image.yml、helm-release.yaml、preview-admin-build.yml、preview-admin-push.yml、preview-docs-build.yml、preview-docs-push.yml、preview-fastgpt-build.yml、preview-fastgpt-push.yml、test-fastgpt-pro.yaml
These are GitHub-exclusive capabilities (PR preview deployment, docs site build, auto-close issue, Pro edition testing), not needed internally. But if the user creates a newbuild image or run testsworkflow, you should proactively ask whether a Forgejo version needs to be created (see the decision tree below).
Decision tree
用户改了 .github/workflows/<name>
│
├─ .forgejo/workflows/<name> 存在?
│ ├─ 是 → 按【转换模式参考表】同步已有配对
│ └─ 否 → 该 workflow 是否属于"构建 OCI 镜像 / 跑单元测试 / 内网需要复用"?
│ ├─ 是 → 询问用户是否新建 Forgejo 版本;若同意,基于 GitHub 版本改写
│ └─ 否(preview/docs/admin/helm/auto-close 等专属能力)→ 无需同步,告知用户原因
│
└─ 用户改的是 .forgejo/workflows/<name>(罕见)
→ 反向核对 .github/workflows/<name> 是否也需要对应改动
Determine whether a new Forgejo version needs to be created
Usually needed:
build-*.yml— build and push OCI images (internal deployment depends on it)test-*.yaml— run unit tests / integration tests (internal PR checks depend on it)
Usually not needed:
preview-*-push/build.yml— PR preview deployment to GitHub Pages / Vercelbuild-docs.yml— docs site buildhelm-release.yaml— Helm chart releaseauto-close-issue.yml— GitHub issue auto-close- Any workflow strongly dependent on GitHub API (
actions/github-script, PR comments,ghCLI)
When unsureask the user, do not decide on the user's behalf.
Conversion Pattern Reference Table (GitHub → Forgejo)
The following 11 patterns are distilled from real diffs in the repository. When syncing, compare them one by one;build-type workflows usually match all of them, while test-type workflows match only some(e.g., test-rust-agent.yaml is currently identical on both sides, with zero conversions).
Pattern 1 · Container Registry Address
GitHub hardcodes ghcr.io, while Forgejo uses ${{ github.server_url }} to dynamically derive the registry host.
# GitHub
registry: ghcr.io
password: ${{ secrets.GITHUB_TOKEN }}
# outputs / tags 里
ghcr.io/${{ github.repository_owner }}/<repo>:<tag>
# Forgejo(需先在 step 里算出 REGISTRY)
- name: Set platform variables
run: |
REGISTRY="${{ github.server_url }}"
REGISTRY="${REGISTRY#https://}"
REGISTRY="${REGISTRY#http://}"
echo "REGISTRY=${REGISTRY}" >> "$GITHUB_ENV"
echo "SOURCE_URL=${{ github.server_url }}/${{ github.repository }}" >> "$GITHUB_ENV"
# 之后
registry: ${{ env.REGISTRY }}
password: ${{ secrets.REGISTRY_TOKEN }}
${{ env.REGISTRY }}/${{ github.repository_owner }}/<repo>:<tag>
Pattern 2 · Remove External Registry Login
GitHub logs in to ghcr.io + Alibaba Cloud + Docker Hub at the same time and tags multiple sets; Forgejo only logs in to the self-hosted registry,deletethe login steps for Alibaba Cloud and Docker Hub and the corresponding tag environment variables.
# GitHub(要删)
- name: Login to Ali Hub
uses: docker/login-action@v3
with:
registry: registry.cn-hangzhou.aliyuncs.com
username: ${{ secrets.FASTGPT_ALI_IMAGE_USER }}
password: ${{ secrets.FASTGPT_ALI_IMAGE_PSW }}
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_HUB_NAME }}
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
# env 计算里
echo "Ali_Tag=..." >> $GITHUB_ENV
echo "Ali_Latest=..." >> $GITHUB_ENV
echo "Docker_Hub_Tag=..." >> $GITHUB_ENV
echo "Docker_Hub_Latest=..." >> $GITHUB_ENV
# TAGS 拼接里
TAGS="$(echo -e "${Git_Tag}\n${Git_Latest}\n${Ali_Tag}\n${Ali_Latest}\n${Docker_Hub_Tag}\n${Docker_Hub_Latest}")"
# Forgejo(只留 Git_*)
TAGS="$(echo -e "${Git_Tag}\n${Git_Latest}")"
Pattern 3 · Action Reference Prefix
GitHub uses actions/*, while Forgejo uses forgejo/*(Forgejo Actions is compatible with GitHub Actions syntax, but its own action images are under the forgejo namespace).
# GitHub
uses: actions/upload-artifact@v4
uses: actions/download-artifact@v4
uses: actions/github-script@v7 # Forgejo 没有对应,整个 step 要删或改写(见模式 10/11)
# Forgejo
uses: forgejo/upload-artifact@v4
uses: forgejo/download-artifact@v4
Pattern 4 · permissions Block
Remove GitHub-specific attestation / id-token / issue / PR write permissions. The Forgejo runner does not do SLSA attestation, nor does it write PR comments.
# GitHub(要删的行)
permissions:
attestations: write
id-token: write
issues: write
pull-requests: write
# contents: read 通常两边都保留
Pattern 5 · Internal Network Proxy + Mirror Source Injection
The Forgejo runner is on the internal network, so pulling docker.io / npm / apt packages must go through the proxy and mirror. Everybuild jobmust have these two sections added at the beginning.vars.AB_* is a Forgejo repository variable.
# Forgejo 每个 build job 开头加
- name: Capture proxy envs from variables
run: |
echo "HTTP_PROXY=${{ vars.AB_HTTP_PROXY }}" >> "$GITHUB_ENV"
echo "HTTPS_PROXY=${{ vars.AB_HTTPS_PROXY }}" >> "$GITHUB_ENV"
echo "NO_PROXY=${{ vars.AB_NO_PROXY }}" >> "$GITHUB_ENV"
echo "APT_MIRROR_UBUNTU=${{ vars.AB_APT_MIRROR_UBUNTU }}" >> "$GITHUB_ENV"
echo "APT_MIRROR_DEBIAN=${{ vars.AB_APT_MIRROR_DEBIAN }}" >> "$GITHUB_ENV"
echo "APT_MIRROR_DEBIAN_SECURITY=${{ vars.AB_APT_MIRROR_DEBIAN_SECURITY }}" >> "$GITHUB_ENV"
echo "NPM_REGISTRY=${{ vars.AB_NPM_REGISTRY }}" >> "$GITHUB_ENV"
- name: Write buildkitd config
run: |
REGISTRY_HOST="${{ github.server_url }}"
REGISTRY_HOST="${REGISTRY_HOST#https://}"
REGISTRY_HOST="${REGISTRY_HOST#http://}"
echo "REGISTRY_HOST=${REGISTRY_HOST}" >> "$GITHUB_ENV"
mkdir -p /tmp/buildkit
cat > /tmp/buildkit/buildkitd.toml << EOF
[registry."docker.io"]
mirrors = ["mirrors.sangfor.com"]
[registry."${REGISTRY_HOST}"]
http = true
insecure = true
EOF
If a test-type workflow needs to download external binaries (such as MongoDB memory server), it must also inject the proxy and pre-download (see Pattern 11).
Pattern 6 · buildx driver-opts + config
GitHub uses a single line driver-opts: network=host; Forgejo needs to put the proxy, GODEBUG, and buildkitd config all into it.
# GitHub
driver-opts: network=host
# Forgejo
driver-opts: |
network=host
env.GODEBUG=madvdontneed=0
env.HTTP_PROXY=${{ env.HTTP_PROXY }}
env.HTTPS_PROXY=${{ env.HTTPS_PROXY }}
env.NO_PROXY=${{ env.REGISTRY_HOST }}
config: /tmp/buildkit/buildkitd.toml
The build step also needs to pass through build-args:
# Forgejo docker/build-push-action
build-args: |
HTTP_PROXY=${{ env.HTTP_PROXY }}
HTTPS_PROXY=${{ env.HTTPS_PROXY }}
NO_PROXY=${{ env.NO_PROXY }}
APT_MIRROR_UBUNTU=${{ env.APT_MIRROR_UBUNTU }}
APT_MIRROR_DEBIAN=${{ env.APT_MIRROR_DEBIAN }}
APT_MIRROR_DEBIAN_SECURITY=${{ env.APT_MIRROR_DEBIAN_SECURITY }}
NPM_REGISTRY=${{ env.NPM_REGISTRY }}
env:
GODEBUG: madvdontneed=0
Pattern 7 · artifact Download Method
Forgejo's download-artifact does not support pattern + merge-multiple, so it mustexplicitly download each architecture's digest by name。
# GitHub(多架构 merge)
- uses: actions/download-artifact@v4
with:
path: ${{ runner.temp }}/digests
pattern: digests-<repo>-${{ github.sha }}-*
merge-multiple: true
# Forgejo(逐个下载,continue-on-error 防缺架构)
- uses: forgejo/download-artifact@v4
with:
name: digests-<repo>-${{ github.sha }}-amd64
path: ${{ runner.temp }}/digests
- uses: forgejo/download-artifact@v4
with:
name: digests-<repo>-${{ github.sha }}-arm64
path: ${{ runner.temp }}/digests
Pattern 8 · runs-on Matrix Default Value
GitHub uses || 'ubuntu-24.04' at the job level as a fallback; Forgejo puts the default value into each entry of the matrix, and the job level directly uses matrix.runs-on。
# GitHub
strategy:
matrix:
archs:
- runs-on: ubuntu-24.04 # 可能省略
- runs-on: arm64-runner
runs-on: ${{ matrix.runs-on || 'ubuntu-24.04' }}
# Forgejo
strategy:
matrix:
archs:
- runs-on: ubuntu-24.04 # 每条都要显式写
- runs-on: arm64-runner
runs-on: ${{ matrix.runs-on }} # 不兜底
Pattern 9 · workflow_dispatch Version Input for Build-Type Workflows
GitHub relies only on tag triggers; Forgejo additionally supports workflow_dispatch manually passing the version number, and branches in the version parsing script.
# Forgejo 顶层 on
on:
push:
branches: [main]
tags: ['v*']
workflow_dispatch:
inputs:
version:
description: 'Image version tag (e.g. v1.0.0)'
required: true
type: string
# 版本解析 step(用 step output 传给下游 job)
- name: Validate release version
id: version
env:
EVENT_NAME: ${{ github.event_name }}
REF_TYPE: ${{ github.ref_type }}
TAG_VERSION: ${{ github.ref_name }}
INPUT_VERSION: ${{ github.event.inputs.version }}
run: |
if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then
VERSION="$INPUT_VERSION"
elif [[ "$REF_TYPE" == "tag" ]]; then
VERSION="$TAG_VERSION"
else
echo "::error::Workflow must be triggered by a tag or workflow_dispatch. Current event: ${EVENT_NAME}"
exit 1
fi
if [[ ! "$VERSION" =~ ^v ]]; then
echo "::error::Version must start with v. Current value: ${VERSION}"
exit 1
fi
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
Pattern 10 · Delete detect-changes job (github-script)
GitHub uses actions/github-script@v7 to run a piece of JS that decides which test scopes to run based on the files changed in the PR; Forgejo does not have github-script,delete the entire detect-changes job, and downstream jobs remove needs: detect-changes and if: needs.detect-changes.outputs.* conditions, changing to always run all scopes.
# GitHub(整个 job 删)
detect-changes:
runs-on: ubuntu-latest
outputs:
run_global: ${{ steps.scope.outputs.run_global }}
# ...
steps:
- uses: actions/github-script@v7
with:
script: |
// 根据 PR 文件判断 scope...
# 下游 job
test-global:
needs: detect-changes
if: ${{ needs.detect-changes.outputs.run_global == 'true' }}
# Forgejo
test-global: # 无 needs,无 if
test-service: # 无 needs,无 if
test-app: # 无 needs,无 if
report-coverage:
needs: [test-global, test-service, test-app]
if: ${{ always() }} # 不再依赖 should_report
Pattern 11 · Rewrite PR Coverage Comment as Step Summary
GitHub uses actions/github-script to call github.rest.issues.createComment to post a coverage table on the PR; Forgejo does not have a PR comment API, so change it toan inline node script writing to GITHUB_STEP_SUMMARY(it only appears in the workflow run summary, and is not posted to the PR). At the same time, artifact downloads must also be changed to download one by one by name + continue-on-error。
# GitHub
- uses: actions/download-artifact@v4
with:
path: coverage-artifacts
pattern: coverage-*
- name: Report Coverage
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const path = require('path');
// ... 拼 markdown 表格
await core.summary.addRaw(body).write();
await github.rest.issues.createComment({ ... });
# Forgejo
- uses: forgejo/download-artifact@v4
with:
name: coverage-global
path: coverage-artifacts/coverage-global
continue-on-error: true
- uses: forgejo/download-artifact@v4
with:
name: coverage-service
path: coverage-artifacts/coverage-service
continue-on-error: true
- uses: forgejo/download-artifact@v4
with:
name: coverage-app
path: coverage-artifacts/coverage-app
continue-on-error: true
- name: Generate Coverage Summary
if: github.event_name == 'pull_request'
run: |
node -e "
const fs = require('fs');
const path = require('path');
// ... 同样拼 markdown 表格
fs.writeFileSync(process.env.GITHUB_STEP_SUMMARY, body, { flag: 'a' });
"
When test-type workflows download external binaries such as MongoDB memory server, they must also go through the proxy:
# Forgejo test job 额外加
- name: Capture proxy envs from variables
run: |
echo "HTTP_PROXY=${{ vars.AB_HTTP_PROXY }}" >> "$GITHUB_ENV"
echo "HTTPS_PROXY=${{ vars.AB_HTTPS_PROXY }}" >> "$GITHUB_ENV"
echo "NO_PROXY=${{ vars.AB_NO_PROXY }}" >> "$GITHUB_ENV"
echo "MONGOMS_DOWNLOAD_DIR=$HOME/.cache/mongodb-binaries" >> "$GITHUB_ENV"
- name: Pre-download MongoDB binary
run: |
MONGODB_VERSION="7.0.14"
CACHE_DIR="$HOME/.cache/mongodb-binaries/mongodb-linux-x86_64-ubuntu2204-${MONGODB_VERSION}"
if [ -f "${CACHE_DIR}/mongod" ]; then
echo "MongoDB binary already cached"; exit 0
fi
mkdir -p "${CACHE_DIR}"
curl --proxy "${{ vars.AB_HTTP_PROXY }}" -sL \
"https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu2204-${MONGODB_VERSION}.tgz" \
| tar xz -C /tmp
cp /tmp/mongodb-linux-x86_64-ubuntu2204-${MONGODB_VERSION}/bin/mongod "${CACHE_DIR}/mongod"
Complete Before/After Snippets
With build-fastgpt.yml as an example of the release job registry login section (real diff excerpt):
# === GitHub 原版 ===
release:
needs: build
runs-on: ubuntu-24.04
permissions:
packages: write
attestations: write # ← Forgejo 删
id-token: write # ← Forgejo 删
steps:
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to Ali Hub # ← Forgejo 整段删
uses: docker/login-action@v3
with:
registry: registry.cn-hangzhou.aliyuncs.com
username: ${{ secrets.FASTGPT_ALI_IMAGE_USER }}
password: ${{ secrets.FASTGPT_ALI_IMAGE_PSW }}
- name: Login to Docker Hub # ← Forgejo 整段删
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_HUB_NAME }}
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
- uses: actions/download-artifact@v4
with:
pattern: digests-${{ matrix.sub_routes.repo }}-${{ github.sha }}-*
merge-multiple: true
# === Forgejo 同步后 ===
release:
needs: build
runs-on: ubuntu-24.04
permissions:
packages: write
# 无 attestations / id-token
steps:
- name: Set platform variables
run: |
REGISTRY="${{ github.server_url }}"
REGISTRY="${REGISTRY#https://}"
REGISTRY="${REGISTRY#http://}"
echo "REGISTRY=${REGISTRY}" >> "$GITHUB_ENV"
echo "SOURCE_URL=${{ github.server_url }}/${{ github.repository }}" >> "$GITHUB_ENV"
- name: Capture proxy envs from variables
run: |
echo "HTTP_PROXY=${{ vars.AB_HTTP_PROXY }}" >> "$GITHUB_ENV"
echo "HTTPS_PROXY=${{ vars.AB_HTTPS_PROXY }}" >> "$GITHUB_ENV"
echo "NO_PROXY=${{ vars.AB_NO_PROXY }}" >> "$GITHUB_ENV"
# ... 其余 AB_* 变量
- name: Write buildkitd config
run: |
# ... 写 buildkitd.toml(见模式 5)
- name: Login to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.REGISTRY_TOKEN }}
# 无 Ali Hub / Docker Hub login
- name: Download digests
uses: forgejo/download-artifact@v4
with:
name: digests-${{ matrix.sub_routes.repo }}-${{ github.sha }}-amd64
path: ${{ runner.temp }}/digests
- uses: forgejo/download-artifact@v4
with:
name: digests-${{ matrix.sub_routes.repo }}-${{ github.sha }}-arm64
path: ${{ runner.temp }}/digests
Synchronization workflow
- Read the GitHub version: open
.github/workflows/<name>, understand what it does. - Diff existing pair: if
.forgejo/workflows/<name>already exists, firstdiffboth sides to see the current diff baseline, then layer on the user's GitHub-side changes. - Apply conversion patterns one by one: compare in the order 1→11 of the 【Conversion Pattern Reference Table】; build-type ones usually all match, test-type ones partially match.
- Keep business logic unchanged: build steps, test commands, matrix dimensions, and version number calculation logic should be identical; only change "where it runs / where it pushes to / which action is used".
- YAML validation: after syncing, use
python -c "import yaml; yaml.safe_load(open('.forgejo/workflows/<name>'))"oractionlintto validate syntax (the repository does not enforce it, but it can avoid low-level indentation errors). - Inform the user: list which points on the Forgejo side were changed in this sync, and whether any GitHub-only capabilities were removed (requires user confirmation that the intranet really does not need them).
Verification checklist
-
.forgejo/workflows/<name>and.github/workflows/<name>'sbuild/test business stepsare consistent. - The Forgejo version has no
ghcr.iohardcoded, switched to${{ env.REGISTRY }}。 - The Forgejo version has no Alibaba Cloud / Docker Hub login step or corresponding tag variables.
- All
actions/*references have been replaced withforgejo/*;actions/github-scriptstep has been deleted or rewritten. -
permissionsblock has noattestations: write/id-token: write/issues: write/pull-requests: write。 - The build job starts with the two steps "Capture proxy envs" + "Write buildkitd config".
- buildx
driver-optsis in multi-line format with proxy +config: /tmp/buildkit/buildkitd.toml。 - artifact download uses
forgejo/download-artifactexplicit download by name, nopattern/merge-multiple。 -
runs-onevery matrix entry explicitly specifies a runner, job-level usesmatrix.runs-onno fallback. - The build-type workflow's
on:hasworkflow_dispatch.inputs.version, the version parsing script handles theworkflow_dispatchbranch. - YAML syntax validation passes.
- If GitHub-only capabilities were removed (PR comments, attestation, ali/docker hub push), the user has been informed in the reply.
Anti-patterns (do not do)
- ❌ Directly
cp .github/workflows/<name> .forgejo/workflows/<name>—— will bring ghcr.io / actions/* / attestations into the intranet, and the runner will not work. - ❌ Only changing the registry address without changing the artifact download method ——
pattern+merge-multiplewill error on forgejo/download-artifact. - ❌ Keeping the
actions/github-scriptstep —— Forgejo does not have this action, and the job will fail directly. - ❌ Hardcoding the intranet proxy variable as the real proxy URL —— must use the
vars.AB_*repository variable, the URL will change. - ❌ Adding Alibaba Cloud / Docker Hub login back to the Forgejo version for the sake of "alignment" —— the intranet does not have these credentials, nor is it necessary.
- ❌ Changing the GitHub side but forgetting to sync the Forgejo side (the most common mistake) —— when changing
.github/workflows/must also check.forgejo/workflows/。 - ❌ Deleting business logic steps for the sake of "simplification" —— conversion only changes the execution environment, not the build/test content.
- ❌ For
preview-*/build-docs/helm-releaseEven GitHub-specific workflows are forcibly made into Forgejo versions — they depend on GitHub Pages / Vercel / Helm repo, and the intranet has no corresponding capabilities.