製作一個能構建 dotnet AOT 的 gitlab ruuner 的 Debian docker 映象

lindexi發表於2024-04-29

我的需求是需要有一個能夠構建出 dotnet 的 AOT 包的環境,要求這個環境能解決 glibc 相容依賴的問題,能打出來 x64 和 arm64 的 AOT 的包,且能夠執行 gitlab runner 對接自動構建

需求

以下是我列舉的需求

  • 支援製作能在 UOS 系統和麒麟系統上執行的包
  • 支援製作出來的包是 AOT 版本的
  • 可以使用 gitlab runner 對接自動構建

開始之前必須說明的是,對於 dotnet 應用來說,如果不需要 AOT 的話,完全可以在 Windows 上構建出其他 Linux 系統和其他平臺適用的應用。僅僅只是在 AOT 下,強依賴平臺構建時,才有需要在對應的系統平臺構建

製作方法

我製作的 docker 的 Dockerfile 是基於 debian:buster-slim 打上負載的

FROM debian:buster-slim

為了提升一點拉取速度,我換成國內的源,使用的是阿里的源

RUN rm /etc/apt/sources.list
COPY sources.list /etc/apt/sources.list
RUN apt-get update

這裡的 sources.list 的程式碼是從 debian映象_debian下載地址_debian安裝教程-阿里巴巴開源映象站 抄的,程式碼如下

deb http://mirrors.aliyun.com/debian/ buster main non-free contrib
deb-src http://mirrors.aliyun.com/debian/ buster main non-free contrib
deb http://mirrors.aliyun.com/debian-security buster/updates main
deb-src http://mirrors.aliyun.com/debian-security buster/updates main
deb http://mirrors.aliyun.com/debian/ buster-updates main non-free contrib
deb-src http://mirrors.aliyun.com/debian/ buster-updates main non-free contrib

為了交叉構建,同時構建出 ARM64 的 AOT 的 dotnet 應用,我根據 Cross-compilation - .NET - Microsoft Learn 的文件安裝上必要的負載

RUN dpkg --add-architecture arm64
RUN apt update

RUN apt-get install libicu-dev -y
RUN apt-get install libssl-dev -y
RUN apt-get install wget -y
RUN apt-get install clang llvm -y
RUN apt-get install gcc-aarch64-linux-gnu -y
RUN apt-get install binutils-aarch64-linux-gnu -y
RUN apt-get install zlib1g-dev -y
RUN apt-get install zlib1g-dev:arm64 -y

為了方便除錯和對接 gitlab runner 我還加上了 git 和 vim 工具

RUN apt-get install vim -y
RUN apt-get install git -y

RUN apt-get clean

到這一步,就完成了 docker image 裡面的基礎部分了,現在的 Dockerfile 的程式碼如下

FROM debian:buster-slim
WORKDIR /root

RUN rm /etc/apt/sources.list
COPY sources.list /etc/apt/sources.list
RUN apt-get update

RUN dpkg --add-architecture arm64
RUN apt update

RUN apt-get install libicu-dev -y
RUN apt-get install libssl-dev -y
RUN apt-get install wget -y
RUN apt-get install clang llvm -y
RUN apt-get install gcc-aarch64-linux-gnu -y
RUN apt-get install binutils-aarch64-linux-gnu -y
RUN apt-get install zlib1g-dev -y
RUN apt-get install zlib1g-dev:arm64 -y

RUN apt-get install vim -y
RUN apt-get install git -y

RUN apt-get clean

接著到 dotnet 官網 下載 dotnet 8 和 dotnet 6 的 sdk 壓縮包,本文這裡使用的是自己解壓縮的方式。換成命令方式安裝也可以,只是命令方式拉取的速度可能不如先下載壓縮包的方式,且下載壓縮包可以方便多次重新構建,在 Dockerfile 不斷需要修改時,使用壓縮包可以省去多次修改之後的重新構建時的拉取時間

本文這裡採用的是下載壓縮包的方式,下載到 dotnet-sdk-6.0.421-linux-x64.tar.gz 和 dotnet-sdk-8.0.204-linux-x64.tar.gz 這兩個壓縮包。如果大家下載失敗,或者沒有網速的話,可以郵件給我,讓我用網盤發給你。一般情況下在國內都能拉取成功,因為微軟幫忙提供了全球 CDN 了,下載速度在我這裡還是很快的。下載 dotnet 6 版本僅僅只是為了讓我的構建工具正常工作而已,屬於可選項

下載完成 dotnet 的壓縮包,即可使用 Dockerfile 的 ADD 命令將壓縮包解壓縮到 docker image 裡的某個資料夾裡面,如下面程式碼

WORKDIR /root
ADD dotnet-sdk-6.0.421-linux-x64.tar.gz ./dotnet
ADD dotnet-sdk-8.0.204-linux-x64.tar.gz ./dotnet

解壓縮完成之後,配置環境變數等,讓全域性可以使用 dotnet 命令

ENV DOTNET_ROOT="/root/dotnet"
ENV PATH="${PATH}:${DOTNET_ROOT}:${DOTNET_ROOT}/tools"
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
RUN ln -s /root/dotnet/dotnet /usr/bin/dotnet

完成以上步驟之後,一個特別簡單的 dotnet 構建 Dockerfile 已經完成了,接下來一步則是配置 gitlab runner 的步驟。我將參考 gitlab runner 官方安裝文件 進行配置,只是過程稍微取巧

先根據 Install GitLab Runner manually on GNU/Linux - GitLab 提供的方法,拉取 GitLab Runner 的二進位制壓縮包,本文這裡是需要下載 Linux x86-64 版本,當前的下載連結是 https://s3.dualstack.us-east-1.amazonaws.com/gitlab-runner-downloads/latest/binaries/gitlab-runner-linux-amd64

下載連結可能會有變更,還請大家重新參考 Install GitLab Runner manually on GNU/Linux - GitLab 文件,找到更新的下載 Linux x86-64 版本的下載地址

完成下載之後,透過 COPY 命令複製到 docker image 裡

COPY gitlab-runner-linux-amd64 /usr/share/gitlab/gitlab-runner

RUN chmod +x /usr/share/gitlab/gitlab-runner

完成以上步驟之後需要對 GitLab Runner 進行配置。本文這裡採用取巧的方式,即先將 GitLab Runner 執行起來,配置完成之後,存放配置檔案,再將配置檔案打入到 docker image 裡面,後續就只需啟動 docker image 即可

具體的步驟是先將當前的 Dockerfile 構建且執行。我這裡使用的是 podman 工具,如果大家使用的是 docker desktop 的話,只需將 podman 命令換成 docker 命令即可,其他引數相同

// 先 cd 到 Dockerfile 所在的資料夾,再執行以下命令
podman build -t t1 .

當前的 Dockerfile 檔案的程式碼如下

FROM debian:buster-slim
WORKDIR /root

RUN rm /etc/apt/sources.list
COPY sources.list /etc/apt/sources.list
RUN apt-get update

RUN dpkg --add-architecture arm64
RUN apt update

RUN apt-get install libicu-dev -y
RUN apt-get install libssl-dev -y
RUN apt-get install wget -y
RUN apt-get install clang llvm -y
RUN apt-get install gcc-aarch64-linux-gnu -y
RUN apt-get install binutils-aarch64-linux-gnu -y
RUN apt-get install zlib1g-dev -y
RUN apt-get install zlib1g-dev:arm64 -y

RUN apt-get install vim -y
RUN apt-get install git -y

RUN apt-get clean

ADD dotnet-sdk-6.0.421-linux-x64.tar.gz ./dotnet
ADD dotnet-sdk-8.0.204-linux-x64.tar.gz ./dotnet
ENV DOTNET_ROOT="/root/dotnet"
ENV PATH="${PATH}:${DOTNET_ROOT}:${DOTNET_ROOT}/tools"
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
RUN ln -s /root/dotnet/dotnet /usr/bin/dotnet

COPY gitlab-runner-linux-amd64 /usr/share/gitlab/gitlab-runner

RUN chmod +x /usr/share/gitlab/gitlab-runner

再將打包好的 docker image 執行,執行時記得掛載上資料夾,用於將 docker 裡面的檔案傳輸到主機

// 提前建立好 C 盤的 lindexi 的 wsl 資料夾,你換成自己的資料夾也可以
podman run -i -t -v /mnt/c/lindexi/wsl:/etc/gitlab-runner t1

以上程式碼的 /mnt/c/lindexi/wsl 是我自己的 C:\lindexi\wsl 資料夾,這是我提前建立好的空資料夾。大家換成自己的資料夾也可以,如果用 docker desktop 的話,需要看一下是否執行在 wsl 上,如果不在的話,也許需要換成 Windows 下的路徑表示方法,相信這一步難不倒大家的

進入之後,即可使用 /usr/share/gitlab/gitlab-runner 命令進行註冊,具體的註冊步驟如下

本文使用註冊 GitLab 組 作為例子,註冊單個專案的步驟也類似,詳細請參閱 https://docs.gitlab.com/runner/configuration/ 文件。先進入到 GitLab 的 組 的 Runner 配置介面裡面,點選 New group runner 按鈕

點選之後,進入配置介面。由於這是一個特殊的構建方式,我推薦寫上 gitlab 的 runner tag 項,我這裡寫的是 debian-dotnet-docker 標記。這裡的標記需要和 git 的 tag 區分哦,這是兩個完全不相同的東西

點選 Create runner 按鈕,即可進入到建立配置命令介面,複製其配置命令引數,如我這裡的是

gitlab-runner register
  --url https://gitlab.lindexi.com
  --token glrt-HbCpfssbPSFqR_xVtxLX

於是我在執行起來的 docker 命令列裡面輸入以下命令用於註冊

/usr/share/gitlab/gitlab-runner register --url https://gitlab.lindexi.com --token glrt-HbCpfssbPSFqR_xVtxLX

輸入之後,一路都是回車下一步,除了執行命令部分可選使用 shell 之外。完成之後再使用 /usr/share/gitlab/gitlab-runner run 命令執行起來試試,如果能夠執行成功,且在 gitlab 的 runner 頁面裡面能夠看到執行起來的 runner 則證明成功。否則還請自行除錯哈,我也不熟悉

完成之後即可愉快退出 docker 環境,此時即可在掛載到 /etc/gitlab-runner 的資料夾裡面,即本文的 C:\lindexi\wsl 資料夾裡面看到配置檔案,一般是 config.toml 檔案

在上述步驟完成之後,咱可以取出來掛載的資料夾,如我這裡的 C:\lindexi\wsl 資料夾,將其複製到 Dockerfile 檔案所在的資料夾裡面,用於編寫 Dockerfile 複製到 /etc/gitlab-runner 資料夾裡面,如此製作出來的 docker image 將會帶上已經註冊的 gitlab runner 資訊

COPY wsl /etc/gitlab-runner

接著再執行安裝命令,以及設定入口為 gitlab-runner run 即可

RUN /usr/share/gitlab/gitlab-runner install --user=root --working-directory=/root/.local/share/gitlab

ENTRYPOINT ["/usr/share/gitlab/gitlab-runner", "run"]

完成以後的 Dockerfile 檔案如下

FROM debian:buster-slim
WORKDIR /root

RUN rm /etc/apt/sources.list
COPY sources.list /etc/apt/sources.list
RUN apt-get update

RUN dpkg --add-architecture arm64
RUN apt update

RUN apt-get install libicu-dev -y
RUN apt-get install libssl-dev -y
RUN apt-get install wget -y
RUN apt-get install clang llvm -y
RUN apt-get install gcc-aarch64-linux-gnu -y
RUN apt-get install binutils-aarch64-linux-gnu -y
RUN apt-get install zlib1g-dev -y
RUN apt-get install zlib1g-dev:arm64 -y

RUN apt-get install vim -y
RUN apt-get install git -y

RUN apt-get clean

ADD dotnet-sdk-6.0.421-linux-x64.tar.gz ./dotnet
ADD dotnet-sdk-8.0.204-linux-x64.tar.gz ./dotnet
ENV DOTNET_ROOT="/root/dotnet"
ENV PATH="${PATH}:${DOTNET_ROOT}:${DOTNET_ROOT}/tools"
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
RUN ln -s /root/dotnet/dotnet /usr/bin/dotnet

COPY gitlab-runner-linux-amd64 /usr/share/gitlab/gitlab-runner

RUN chmod +x /usr/share/gitlab/gitlab-runner

COPY wsl /etc/gitlab-runner

RUN /usr/share/gitlab/gitlab-runner install --user=root --working-directory=/root/.local/share/gitlab

ENTRYPOINT ["/usr/share/gitlab/gitlab-runner", "run"]

RUN mkdir /root/build
WORKDIR /root/build

嘗試構建此 Dockerfile 檔案

// 先 cd 到 Dockerfile 所在的資料夾,再執行以下命令
podman build -t t1 .

接著掛載到後臺執行

podman container run -v nuget_global:/root/.nuget/packages -v nuget_cache:/root/.local/share/NuGet -v gitlabrunner:/root/.local/share/gitlab -d t1

以上命令的 -v nuget_global:/root/.nuget/packages -v nuget_cache:/root/.local/share/NuGet -v gitlabrunner:/root/.local/share/gitlab 屬於可選的引數,用來掛載 nuget 快取等內容,解決 docker 每次重啟都會丟失快取檔案,提升重啟 docker 之後的構建速度,減少重複拉取 nuget 包

完成以上步驟之後,就已經完成了製作一個能構建 dotnet AOT 的 gitlab ruuner 的 Debian docker 映象

可以嘗試在自己的專案裡面,編寫 .gitlab-ci.yml 檔案,指定到這個執行起來的 docker image 上執行,以下是我的測試使用的 .gitlab-ci.yml 檔案程式碼

stages:
  - build

BuildLinuxX64InDocker:
  stage: build
  script:
    - 'dotnet run publish -p:PublishAot=true -c Release -r linux-x64'
  tags:
    - docker-uos

BuildLinuxArm64InDocker:
  stage: build
  script:
    - 'dotnet run publish -p:PublishAot=true -c Release -r linux-arm64'
  tags:
    - docker-uos 

如果能夠構建成功,且構建出 linux-x64 和 linux-arm64 的 dotnet 可執行檔案,則表示成功。否則還請自行根據輸出的錯誤資訊修復

踩坑記錄

為什麼不在 WSL 裡面構建

核心原因是 WSL 裡面的 glibc 版本過於新,使用 ldd --version 命令可以看到的輸出如下

ldd (GNU libc) 2.36

而麒麟的 Desktop-V10-SP1 版本的 glibc 是 2.31 版本,更慘的 UOS 20.1050.11068.102 版本的 glibc 是 2.28 版本,都低於 WSL 裡面的版本

這就意味著在 WSL 裡面構建出來的應用將無法在以上的兩個系統上執行

這就是為什麼使用 debian:buster-slim 的原因。當前我拉取的 debian:buster-slim 的 docker image id 是 6d0d34a48ee1 的版本。透過 cat /etc/debian_version 可以看到在此版本里面帶的是 debian 10.13 版本

再透過 ldd --version 命令列獲取的 glibc 版本資訊,可以看到帶的是 2.28 版本,剛好與 UOS 20.1050.11068.102 版本的 glibc 版本相同,低於麒麟的 Desktop-V10-SP1 的 glibc 版本

因此在此 debian:buster-slim 裡面 AOT 構建出來的包可以同時在 UOS 20.1050.11068.102 和麒麟的 Desktop-V10-SP1 版本執行

debian buster-backports Release does not have a Release file

開始國內源使用了阿里的,結果遇到以下錯誤內容

E: The repository 'http://mirrors.aliyun.com/debian buster-backports Release' does not have a Release file.
Error: building at STEP "RUN apt update": while running runtime: exit status 100

重新參考了 替換docker容器預設的debian映象 - OrcHome 部落格,結果依然配置失敗。核心原因是配置的版本不正確

我當前使用的是 debian 是 10.13 版本,需要根據 debian映象_debian下載地址_debian安裝教程-阿里巴巴開源映象站 教程文件,更新對應的 debian 10.x (buster) 的配置

我是如何知道 debian 版本的,我透過執行映象,輸入 cat /etc/debian_version 命令獲取到版本

No system certificates available

完成配置阿里的源,遇到以下的錯誤內容

W: https://mirrors.aliyun.com/debian/dists/buster/InRelease: No system certificates available. Try installing ca-certificates.

原因是 ca-certificates 沒有提前安裝,可以在切換為國內源之前,安裝好。安裝方法可參閱 修復 Debian 安裝 dotnet 失敗 depends on ca-certificates

由於我這裡不需要關注安全性問題,更簡單的方法是將 https 全部更換為 http 即可

安裝 dotnet tool 失敗

執行任何的 dotnet tool install 都會提示如下錯誤

Unhandled exception: System.IO.FileNotFoundException: Unable to find the specified file.
   at Interop.Sys.GetCwdHelper(Byte* ptr, Int32 bufferSize)
   at Interop.Sys.GetCwd()
   at Microsoft.DotNet.Cli.ToolPackage.ToolPackageDownloader..ctor(IToolPackageStore store, String runtimeJsonPathForTests)
   at Microsoft.DotNet.ToolPackage.ToolPackageFactory.CreateToolPackageStoresAndDownloader(Nullable`1 nonGlobalLocation, IEnumerable`1 additionalRestoreArguments)
   at Microsoft.DotNet.Tools.Tool.Update.ToolUpdateLocalCommand..ctor(ParseResult parseResult, IToolPackageDownloader toolPackageDownloader, IToolManifestFinder toolManifestFinder, IToolManifestEditor toolManifestEditor, ILocalToolsResolverCache localToolsResolverCache, IReporter reporter)
   at Microsoft.DotNet.Tools.Tool.Update.ToolUpdateCommand..ctor(ParseResult result, IReporter reporter, ToolUpdateGlobalOrToolPathCommand toolUpdateGlobalOrToolPathCommand, ToolUpdateLocalCommand toolUpdateLocalCommand)
   at Microsoft.DotNet.Cli.ToolUpdateCommandParser.<>c.<ConstructCommand>b__14_0(ParseResult parseResult)
   at System.CommandLine.Invocation.InvocationPipeline.Invoke(ParseResult parseResult)
   at Microsoft.DotNet.Cli.Program.ProcessArgs(String[] args, TimeSpan startupTime, ITelemetry telemetryClient)

暫時沒有找到可用方法,只能繞路

我在 windows 下將所需工具下載下來,然後透過複製進入的方式即可完全安裝

當然,在本文例子裡面,我沒有加上我所使用的工具

在 gitlab 構建指令碼找不到 dotnet 命令

在命令列裡面,可以使用 dotnet 命令,但是在 .gitlab-ci.yml 檔案裡面編寫的指令碼找不到 dotnet 命令

加上如下配置到 Dockerfile 即可

RUN ln -s /root/dotnet/dotnet /usr/bin/dotnet

以上命令是對 dotnet 建立連結,如此即可讓全域性可以使用 dotnet 命令

為什麼使用 podman 工具

原因是在 windows 下的 docker desktop 是收費的,於是我用平替的 podman 工具

還原速度過慢

由於 docker 本身是不帶持久化儲存檔案,只有透過掛載本機儲存的方式,才能讓 docker 裡面的檔案持久化存放

還原速度過慢的問題,是因為初始化時沒有任何的 NuGet 快取,導致需要大量拉取,從而導致拉取過慢

根據 How to manage the global packages, cache, temp folders in NuGet - Microsoft Learn 官方文件說明,獲取到預設的快取路徑,使用如下命令將快取路徑掛載到本機

-v nuget_global:/root/.nuget/packages -v nuget_cache:/root/.local/share/NuGet

我這裡掛載寫的是相對路徑,如 nuget_global 等路徑,相對路徑在 podman 下將會存放到 wsl 裡面,詳細請看 在 windows 上執行的 podman 預設的掛載相對路徑是什麼

為什麼程式碼倉庫路徑不掛載

如上述還原速度過慢原因,由於 docker 本身是不帶持久化儲存檔案,只有透過掛載本機儲存的方式,才能讓 docker 裡面的檔案持久化存放。有些夥伴認為將程式碼倉庫路徑也進行本機掛載,可以減少拉取程式碼倉庫的時間。實際上這麼做可能帶來的後果是開啟多 docker 容器時,出現構建過程中的相互影響問題

拉取程式碼倉庫時,大部分時間都是拉取內網的,且隻影響容器的重啟後的首次拉取。因此掛在程式碼倉庫不是必要的

掛載程式碼倉庫可能受到 Windows 自帶防毒影響,導致 llvm-objcopy 這一步失敗,大概的錯誤資訊如下

llvm-objcopy: failed to open xx.dbg Input/output error.

/root/.nuget/packages/microsoft.dotnet.ilcompiler/8.0.4/build/Microsoft.NETCore.Native.targets(379,5): error MSB3073: The command ""llvm-objcopy" --only-keep-debug xx xx exited with code 1.

解決方法是要麼不掛載,要麼在 Windows 自帶防毒加白名單

如何使用交叉編譯

由於我缺少 ARM64 的機器,或者準確來說我缺少一臺可以撐住構建的有效能的 ARM64 的機器,我期望能夠在原有的 linux-x64 機器上構建出 ARM64 的應用。於是我就需要使用到交叉編譯技術,透過此技術我就可以在 linux-x64 的機器上構建出 linux-arm64 的應用

參考 Cross-compilation - .NET - Microsoft Learn 的文件安裝上必要的負載,如下面的 docker 程式碼,即可在 debian 的 x64 系統上構建出 ARM64 的 dotnet 的 AOT 應用

RUN dpkg --add-architecture arm64
RUN apt update

RUN apt-get install libicu-dev -y
RUN apt-get install libssl-dev -y
RUN apt-get install wget -y
RUN apt-get install clang llvm -y
RUN apt-get install gcc-aarch64-linux-gnu -y
RUN apt-get install binutils-aarch64-linux-gnu -y
RUN apt-get install zlib1g-dev -y
RUN apt-get install zlib1g-dev:arm64 -y

在進行 dotnet 釋出時,將在 dotnet 裡面自動根據 -r 引數自動執行交叉編譯,如下面命令

dotnet publish -p:PublishAot=true -c Release -r linux-arm64

相關文件請看 runtime/docs/workflow/building/libraries/cross-building.md at main · dotnet/runtimeruntime/docs/workflow/building/coreclr/cross-building.md at main · dotnet/runtime

為什麼不使用 gitlab-runner start 命令

因為實際測試 gitlab-runner start 之後沒有真的執行,如下面程式碼

RUN /usr/share/gitlab/gitlab-runner start

新增之後執行 docker image 也不會有 gitlab runner 上線

如果換成下面的程式碼,則啟動 docker image 之後立刻退出

ENTRYPOINT ["/usr/share/gitlab/gitlab-runner", "start"]

實際測試只有以下程式碼符合預期

ENTRYPOINT ["/usr/share/gitlab/gitlab-runner", "run"]

找不到 runner 機器或找錯

先調查是否 dotnet 配置 Gitlab 的 CI 找不到 Runner 或找錯的可能原因 提及的問題

排除之後,記得檢視是否帶上了 tags 和 runner 在 gitlab 上配置正確且相同的

參考文件

  • https://github.com/dotnet/runtime/blob/main/src/coreclr/nativeaot/docs/compiling.md#cross-architecture-compilation
  • dotnet publish command - .NET CLI - Microsoft Learn
  • .NET Runtime Identifier (RID) catalog - .NET - Microsoft Learn
  • Cross-compilation - .NET - Microsoft Learn
  • Prepare .NET libraries for trimming - .NET - Microsoft Learn
  • How to manage the global packages, cache, temp folders in NuGet - Microsoft Learn
  • https://dotnet.microsoft.com/zh-cn/download/dotnet/8.0
  • Troubleshooting GitLab Runner - GitLab
  • docker Debian Buster 國內常用映象源
  • docker - What is the difference between CMD and ENTRYPOINT in a Dockerfile? - Stack Overflow
  • How to fix 'buster-backports' no longer has a Release file - nixCraft
  • 替換docker容器預設的debian映象 - OrcHome
  • debian映象_debian下載地址_debian安裝教程-阿里巴巴開源映象站
  • Configure GitLab Runner - GitLab
  • Install GitLab Runner - GitLab
  • Gitlab runner setup with podman - GitLab CI/CD - GitLab Forum
  • Registering runners - GitLab
  • Install GitLab Runner manually on GNU/Linux - GitLab
  • GitLab Runner bleeding edge releases - GitLab
  • NuGet pack and restore as MSBuild targets - Microsoft Learn
  • /usr/bin/dotnet: 沒有那個檔案或目錄-CSDN部落格
  • 在 Linux ARM 系統上安裝 .Net - 快樂就在你的心 的個人部落格
  • dotnet 配置 Gitlab 的 Runner 做 CI 自動構建
  • dotnet 配置 Gitlab 的 CI 找不到 Runner 或找錯的可能原因

相關文章