公司内网 GitLab,没 HTTPS,Go 项目里引了几个私有包,CI 上怎么拉都拉不下来。折腾了一下午,把踩过的坑记一下,省得下次再翻一遍。
先说结论
内网无 HTTPS 的私有 GitLab,要让 go mod download 正常跑,得凑齐三样东西:
- 一组
GO*环境变量,告诉 Go 哪些是私有的、允许走 HTTP - 一个把
CI_JOB_TOKEN塞进 git URL 的办法(insteadOf 或 .netrc) - 对目标项目开
CI_JOB_TOKEN的访问权限
少任何一个都会卡在某个奇怪的错误上,下面一个个说。
四个 GO 变量,各管一摊
| 变量 | 作用 |
|---|---|
GOPRIVATE | 标记私有,不走公共 proxy、不查 sumdb |
GONOPROXY | 直连,不走 proxy |
GONOSUMDB | 不向 sum.golang.org 校验 |
GOINSECURE | 允许 HTTP 明文拉取 |
刚开 始我以为设个 GOPRIVATE 就完事了,结果 Go 还是去敲公共 proxy 的门,私有包直接 404。后来看文档才搞明白,GOPRIVATE 是个"总开关",GONOPROXY 和 GONOSUMDB 是细分的。三个一起设最稳妥,反正内网包三个都不该走外网。
GOINSECURE 是我后来才加的。内网 GitLab 没 HTTPS,Go 默认拒绝明文 HTTP 拉 模块,报一个 TLS 相关的错。这个变量就是干这个的,告诉 Go "这个 host 我信得过,HTTP 也行"。
.gitlab-ci.yml 里这样配:
variables:
GOPRIVATE: "gitlab.example.com"
GONOSUMDB: "gitlab.example.com"
GONOPROXY: "gitlab.example.com"
GOINSECURE: "gitlab.example.com"域名换成自己的。
有 token 还是不行,为什么
我一开始有个错觉:GitLab CI 里 submodule 是自动带 CI_JOB_TOKEN 的,那 Go 拉模块应该也自动带吧。
不是这么回事。
submodule 的自动带 token 是 Runner 的 checkout 机制干的活,只针对 runner 自己执行的 git submodule 那一步。Go 拉模块走的是自己那套 VCS 协议,会另起一个 git 进程去 clone,这个进程:
- 不读 runner 注入给 submodule 的凭证
- 不继承
.git/config里 submodule 的 url
所以 CI_JOB_TOKEN 在 Go 发起的 git 调用里是裸的,私有库一看没认证,直接 401。
解决办法是把 token 手动塞进去。两种写法,效果一样,挑顺手的用。
写法一:git insteadOf
before_script:
- git config --global url."http://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}/".insteadOf "http://${CI_SERVER_HOST}/"${CI_SERVER_HOST} 是 GitLab 自带的变量,就是当前 GitLab 的域名,不用硬编码。
写法二:.netrc
before_script:
- echo "machine ${CI_SERVER_HOST} login gitlab-ci-token password ${CI_JOB_TOKEN}" > ~/.netrc我最后用的是 insteadOf,因为 git 那套我已经熟了,netrc 调试起来不直观。看个人习惯。
写法三:单独的 gitconfig 文件(builder 里的做法)
前面两种是 CI 里写 before_script 的路子,够用但有个坑。工程化的设计是放入builder以便于复用,发现 git config --global 在某些 runner 上不生效——原因是 runner 服务跑的账号和 go 实际执行 git 的账号不是同一个,--global 写进了前者 home 下的 .gitconfig,go 那个进程根本读不到。
解决办法是把配置写到一个文件里,再用 GIT_CONFIG_GLOBAL 环境变量指过去。环境变量会被 go 起的 git 子进程继承,跟哪个账号没关系。核心逻辑剥出来是这样:
class CommonBase(object):
def __init__(self, pkg_obj):
if isinstance(pkg_obj, str):
self._path = pkg_obj
else:
self._path = pkg_obj.get_dir()
@property
def path(self):
return self._path
@staticmethod
def exec_(commands, cwd=None, timeout=None):
commands = commands or []
environ = os.environ.copy()
p_open = subprocess.Popen(commands, cwd=cwd, env=environ)
try:
p_open.wait(timeout)
except subprocess.TimeoutExpired:
kill(p_open.pid)
return p_open.returncode
def _ensure_ci_git_auth(self):
ci_job_token = os.environ.get('CI_JOB_TOKEN')
ci_server_url = os.environ.get('CI_SERVER_URL')
if not ci_job_token or not ci_server_url:
return None
host = urlparse(ci_server_url).netloc
if not host:
return None
# 配置文件放 CI_PROJECT_DIR 下,跟着工作区一起被清,不污染项目根
gitconfig_dir = os.path.join(
os.environ.get('CI_PROJECT_DIR') or self.path, '.tmp')
os.makedirs(gitconfig_dir, exist_ok=True)
gitconfig_path = os.path.join(gitconfig_dir, 'ci.gitconfig')
if os.path.exists(gitconfig_path):
os.remove(gitconfig_path) # 幂等,别让 --add 堆出重复条目
authed = f"http://gitlab-ci-token:{ci_job_token}@{host}/"
# insteadOf 是多值的,用 --add 两条都加;裸 git config 第二条会覆盖第一条
CommonBase.exec_(['git', 'config', '--file', gitconfig_path, '--add',
f'url.{authed}.insteadOf', f'http://{host}/'])
CommonBase.exec_(['git', 'config', '--file', gitconfig_path, '--add',
f'url.{authed}.insteadOf', f'https://{host}/'])
return gitconfig_path然后在跑 go 命令的上下文里把它挂上去:
@contextmanager
def cmd_ctx(self):
extra = dict(PATH=new_path, GOENV=self.go_env_path, GOBIN=...)
gitconfig = self._ensure_ci_git_auth()
if gitconfig:
extra['GIT_CONFIG_GLOBAL'] = gitconfig
with util.Sysenv(**extra):
yield这套写法有几个细节值得拎出来说。
别用 --global。 runner 账号和 go 账号不一致时,--global 写的配置 go 读不到。写文件 + GIT_CONFIG_GLOBAL 才稳,不管谁起的进程都走同一个配置。
http 和 https 两条都要加。 insteadOf 是多值键,用 git config --add 各加一条。如果用裸 git config,第二次调用会把第一次的覆盖掉,结果只有一个 scheme 带 token,另一个 scheme 撞上去就是 401。内网没 https 看着好像只要 http 那条,但有些间接依赖的模块 url 是 https 的,漏一个就断。
幂等。 同一个工作区跑两次(CI 一般会清,但本地 builder 可能复用),--add 会堆出重复条目。跑之前先把旧文件删了。
配置文件放哪。 放 CI_PROJECT_DIR 下的子目录(这里叫 .tmp/),跟着工作区一起被清,不污染项目根,也不用单独写清理逻辑。
跨项目要开权限。 CI_JOB_TOKEN 默认只能访问当前 job 所在的项目。要拉别的私有包(比如 wails-demo 拉 webview2boot),得去目标项目 Settings → CI/CD → Job token permissions 里把当前项目加进允许列表。代码只能把 token 塞进 git,给不给进是 GitLab 那边的事。
本地也想拉怎么办
CI 能跑了,本地开发也得拉。配 go env:
go env -w GOPRIVATE="gitlab.example.com"
go env -w GOINSECURE="gitlab.example.com"
go env -w GONOPROXY="gitlab.example.com"
go env -w GONOSUMDB="gitlab.example.com"凭证那边,本地没法用 CI_JOB_TOKEN,得自己搞个 Personal Access Token 或者 deploy key。我图省事用了 PAT,配 .netrc:
machine gitlab.example.com
login <你的用户名>
password <PAT>其实实测下来本地根本不受影响,因为本地自身就存在 git 配置,go get的时候会调用 git 操作,因此 builder 里面可以处理下判断逻辑(检验环境变量存在CI_JOB_TOKEN和CI_PROJECT_DIR)跳过本地环境下的注入。
每个项目都改 CI 烦不烦
前面那套 before_script 每个仓库都得抄一遍,项目一多就腻歪。两个思路。
思路一:共享 CI 模板
建一个专门放 CI 片段的仓库,比如 devops/ci-templates,里面放个 go-private.yml:
.go-private:
before_script:
- git config --global url."http://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}/".insteadOf "http://${CI_SERVER_HOST}/"各个项目的 .gitlab-ci.yml 只写两行:
include:
- project: 'devops/ci-templates'
file: '/go-private.yml'
test:
extends: .go-private
script:
- go test ./...GO* 那几个变量可以放到 Group 级的 CI/CD Variables 里,子项目自动继承,连写都不用写。
思路二:自定义镜像
造一个 builder 镜像,把 go env 和 entrypoint 都预置好:
FROM golang:1.25
RUN go env -w GOPRIVATE="gitlab.example.com" \
&& go env -w GOINSECURE="gitlab.example.com" \
&& go env -w GONOPROXY="gitlab.example.com" \
&& go env -w GONOSUMDB="gitlab.example.com"
COPY entrypoint.sh /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]entrypoint.sh:
#!/bin/sh
git config --global url."http://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}/".insteadOf "http://${CI_SERVER_HOST}/"
exec "$@"各项目 CI 里写一行 image: your-registry/go-builder:latest,别的都不用管。我最后走的这条,加新项目时最省心。
几个容易卡的地方
CI_JOB_TOKEN默认只能访问自己所在项目。要拉别的私有包,得去目标项目 Settings → CI/CD → Token access 里,把当前项目加进允许列表- 域名一定要用
CI_SERVER_HOST这种内置变量,别写死。我一开始写死了,换了个环境全报错 GOINSECURE只管 Go 拉模块这层。如果代码里还直接git clone,git 那边的 HTTP 该配 sslVerify 还得配
小结
这事不复杂,就是变量多、各管一摊,少配一个就报一个不一样的错。把四个 GO* 变量配齐、token 注入搞对、目标项目开权限,三件套齐了就通。
token 注入这块有三个写法,按场景挑。CI 脚本里手写 before_script,用 insteadOf 或 .netrc 都行,简单直接。塞进 builder 代码里,用独立 gitconfig 文件 + GIT_CONFIG_GLOBAL,能绕开 runner 账号和 go 账号不一致的坑,跨平台最稳。剩下的就是用共享 CI 模板或自定义镜像把这套配置收敛到一处,别让每个项目都抄一遍。



Comments | NOTHING