在GitLab-CI里自动构建Unity应用: 独立开发者的Game-CI

“CI/CD 是一种持续的软件开发方法, 开发者可以在其中持续构建、测试、部署和监控迭代代码。GitLab CI/CD 可以在开发周期的早期发现错误, 并帮助确保部署到生产环境的代码符合你的代码标准”, 这是 GitLab 对其完善的 CI/CD 功能的概述。众所周知, CI/CD (持续集成/持续部署) 方法作为一种常见的开发方法论, 其思路一言以蔽之是自动地发布有效的软件版本。这一方法论的老牌工具是 Jenkins, 不过在 GitLab 上使用则需要关心主从节点管理、插件版本等运维任务, 而自托管 GitLab 实例的 CI 功能则可以直接复用 GitLab 的基础设施, 避免这些繁杂的维护。考虑到 GitLab CI 的核心优势是与 GitLab 代码仓库的无缝集成, 能够免去更复杂的配置和提高功能耦合度, 故本文也将以 GitLab-CE 为平台来展开内容。

这是 Gitlab 认为 CI/CD 在开发流程中所处的位置

对于 Unity 应用这种显然有多种变更且需要多版本发布的应用, 应该如何低成本地实现正确的自动构建+自动部署呢? 一个答案是在 GitLab 上使用 GameCI。它是一个关注在 GitHub, GitLabCircleCI 上实现 Unity 游戏项目 CI/CD(但主要是CI) 的开源项目, 下文将基于 GameCI 文档来简述在 GitLab-CI 的配置步骤。

背景知识与所需环境

首先当然是需要一个部署完成的 GitLab 实例, 其次, 至少了解 CI/CD 中的几个基本概念:

  • 流水线(Pipelines), 由任务和阶段组成, 定义了 CI/CD 人任务的整个流程。
  • 阶段(Stages)定义执行顺序, 比如环境准备阶段 prepareEnvironment 和构建阶段 build
  • 任务(Jobs), 任务是每个阶段具体要执行的操作。
  • 变量(Variables), 用于存储流水线所需的设置和敏感信息。
  • 制品(Artifacts), 在构建、测试、部署过程中产生的文件或目录, 可在不同任务间共享。

以及一点点 DockerDocker Compose 概念和命令, 容器化方式能保持宿主系统的洁净和清爽, 减少不必要的运维工作。

本文中所用环境为:

  • 系统版本:
$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 24.04.3 LTS
Release:        24.04
Codename:       noble
  • Docker版本:
$ sudo docker --version
Docker version 28.3.3, build 980b856
$ sudo docker compose version
Docker Compose version v2.39.1
  • GitLab 版本:
GitLab更新挺勤啊

详细配置步骤

GitLab Runner 负责管理项目流水线, 而 GitLab 负责管理存储库、用户和其他配置。因此我们首先需要——

1. 向 GitLab 添加 Runner

GitLab 使用 Runner 的原意是希望不同的物理机专职执行特定的工作流, 减轻承载 GitLab 本体服务器的负载, 因此 Runner 默认是通过 HTTPS 与GitLab-CE 通信的。为了提高设备的资源利用效率和减少不必要的设备引入, 我们可以在 GitLab 所在的机器上容器化地运行 Runner, 使用以下内容创建 docker-compose.yaml 文件后, 执行 sudo docker compose -f ./docker-compose.yaml 便能启动容器:

  gitlab-runner:
    image: gitlab/gitlab-runner
    restart: always
    container_name: gitlab-runner
    hostname: gitlab-runner
    depends_on:
      - gitlab-ce
    volumes:
      - ./gitlab-runner/config-runner:/etc/gitlab-runner
      - /var/run/docker.sock:/var/run/docker.sock
    networks:
        - gitlab-network

当 Runner 容器启动后并不知道从何处取得任务, 因此需要将其连接到 GitLab 实例。此时需在 GitLab 的 Admin area -> Runners 中新建一个 Runner:

GitLab 的 Runner 注册向导

这里的 glrt-R3hIbtEF81_g-Vee_8Z-tW86MQp0OjEKdToyCw.01.121t7mt8r 就是 Runner 的认证令牌, 根据 GitLab – Registering runners 的步骤, 在服务器上输入 sudo docker exec -it gitlab-runner gitlab-runner register 便可激活 Runner 容器的注册流程。在这个交互式注册流程中, 输入正确的 GitLab 实例 URL (本文以 https://gitlab.example.com 为例) 和 Runner 的认证令牌即可在 GitLab 中看到 Runner 的在线情况了:

成功注册的Docker Runner

当然还需要改一些必要的参数: 首先需要向 ./gitlab-runner/config-runner/config.yaml 中的 [runners.docker] 下添加一行 pull_policy = "if-not-present", 它表示不必在每次执行任务 (Job) 时都拉取 GameCI 的镜像, 本地若存在镜像可直接使用(if-not-present then pull)。

第二个额外配置是为了加速任务的执行, 让 Runner 使用内网地址连接 GitLab 实例, 具体来说, 就是向 /etc/hosts 中添加一行主机名映射 192.168.1.105:9560 gitlab.example.com。因为在 Runner 的注册步骤中使用了 GitLab 的主机名 URL, 这会导致 Runner 通过 DNS 查询得到 gitlab.example.com 的外部网络地址, 从而使其访问 GitLab 实例时受外网速率的限制, 因此需要将 gitlab.example.com 重定向回 LAN 中的正确端点上 (最好的方式当然是在 LAN 中执行 DNS 水平拆分, 可直接免去这类设置, 但这一话题超出了本文的主题, 故此略去):

sudo cat >> /etc/hosts << EOF
192.168.1.105:9560 gitlab.example.com
EOF

到此 GitLab Runner 的配置就算完成了, 在 ./gitlab-runner/config-runner 下的 config.yaml 完整内容如下:

concurrent = 1
check_interval = 0
shutdown_timeout = 0

[session_server]
  session_timeout = 1800

[[runners]]
  name = "docker-stable"
  url = "https://gitlab.example.com"
  id = 1
  token = "glrt-R3hIbtEF81_g-Vee_8Z-tW86MQp0OjEKdToyCw.01.121t7mt8r"
  token_obtained_at = 2025-07-24T22:55:30Z
  token_expires_at = 0001-01-01T00:00:00Z
  executor = "docker"
  [runners.custom_build_dir]
  [runners.cache]
    MaxUploadedArchiveSize = 0
    [runners.cache.s3]
    [runners.cache.gcs]
    [runners.cache.azure]
  [runners.docker]
    tls_verify = false
    image = "docker:stable"
    privileged = false
    disable_entrypoint_overwrite = false
    oom_kill_disable = false
    disable_cache = false
    volumes = ["/var/run/docker.sock:/var/run/docker.sock", "/cache"]
    network_mode = "gitlab-network"
    shm_size = 0
    pull_policy = "if-not-present"
    network_mtu = 0

2. 自动激活Unity Editor

由于 GitLab-CI 需要通过 Runner 启动 Unity Editor 来执行测试和构建, 显然也就需要完成其激活流程。GameCI 支持三种激活方式: 个人许可证, 专业许可证许可证服务器, 本文将以个人许可证为例展示配置方法。只需要两步操作: 找到 ulf 文件并提取出个人序列号(serial), 再在 GitLab-CI 中将其添加为流水线变量, 如此即可。

ulf 文件是 Unity Hub 生成的许可证文件, 是一个 XML 文件, Windows 上的 Unity Hub 会将其置于 C:\ProgramData\Unity\Unity_lic.ulf。若该目录下没有生成该文件, 在 Unity Hub 中重新添加一次许可证即可重新生成。其内容大致如下:

<?xml version="1.0" encoding="UTF-8"?><root><TimeStamp Value="xkonIVEC50HSCg=="/>
    <License id="Terms">
        <MachineBindings>
            <Binding Key="1" Value="004打码打码打码打码打码A723"/>
            <Binding Key="2" Value="A打码打码打码打码打码打码打码打码1"/>
            <Binding Key="4" Value="U打码打码打码打码打码打码打码="/>
            <Binding Key="5" Value="0打码打码打码打码打码打码打码5"/>
        </MachineBindings>
        <MachineID Value="m打码打码打码打码打码打码打码="/>
        <SerialHash Value="ad打码打码打码打码打码打码打码打码打码63"/>
        <Features>
            <Feature Value="33"/>
            <Feature Value="1"/>
            <Feature Value="12"/>
            <Feature Value="2"/>
            <Feature Value="24"/>
            <Feature Value="3"/>
            <Feature Value="36"/>
            <Feature Value="17"/>
            <Feature Value="19"/>
            <Feature Value="62"/>
            <Feature Value="60"/>
        </Features>
        <DeveloperData Value="AQ打码打码打码打码打码打码打码打码打码打码Sw=="/>
        <SerialMasked Value="F打码打码打码打码打码打码打码打码X"/>
        <StartDate Value="2018-07-22T00:00:00"/>
        <StopDate Value="2025-08-21T16:30:33"/>
        <UpdateDate Value="2025-08-19T15:30:32"/>
        <InitialActivationDate Value="2018-07-22T04:21:30"/>
        <LicenseVersion Value="6.x"/>
        <ClientProvidedVersion Value="2017.2.0"/>
        <AlwaysOnline Value="true"/>
        <Entitlements>
            <Entitlement Ns="unity_editor" Tag="UnityPersonal" Type="EDITOR" ValidTo="9999-12-31T00:00:00"/>
            <Entitlement Ns="unity_editor" Tag="DarkSkin" Type="EDITOR_FEATURE" ValidTo="9999-12-31T00:00:00"/>
        </Entitlements>
    </License><Signature xmlns="http://www.w3.org/2000/09/xmldsig#"><SignedInfo><CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments"/><SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/><Reference URI="#Terms"><Transforms><Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/></Transforms><DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><DigestValue>f+F1hbksRywac4P8jbdJ6cLWrsU=</DigestValue></Reference></SignedInfo><SignatureValue>Go打码打码打码打码打码打码打码BLk
Cn打码打码打码打码打码打码打码打码打码打码打码打码打码打码打码打码打码打码打码打码打码by
je打码打码打码打码打码打码打码打码打码打码打码打码打码打码打码打码打码打码打码打码打码aB
+D打码打码打码打码打码打码打码打码打码打码打码打码打码打码打码打码打码打码打码打码打码Wo
rG打码打码打码打码打码打码打码打码打码打码打码打码打码打码打码打码打码打码打码打码打码==</SignatureValue></Signature></root>

GameCI 提供了一条 Powershell 命令, 来从 <DeveloperData/> 一节提取出个人序列号的 base64 字符串并解码为明文:

Get-Content Unity_lic.ulf | Select-String -Pattern 'DeveloperData' | ForEach-Object { $_ -replace '.*Value="([^"]+)".*', '$1' } | [System.Convert]::FromBase64String($_)

你也可以在网上找个在线的 base64 解码工具直接解码, 得出形式为 AA-BBBB-CCCC-DDDD-EEEE-FFFF 的个人序列号。这时便可在 GitLab 项目的 Settings -> CI/CD 中添加流水线变量, 一共三个:

  • UNITY_EMAIL, 能够登录Unity Hub 的 Unity 账户邮件地址
  • UNITY_PASSWORD, Unity 账户密码
  • UNITY_SERIAL, 填入这串形式为 AA-BBBB-CCCC-DDDD-EEEE-FFFF 的个人序列号
GitLab 中需要配置的 UNITY_EMAIL, UNITY_PASSWORDUNITY_SERIAL 变量, 当然你也能添加其他变量, 比如 UNITY_VERSION 

从此 GitLab-CI 便能在任务执行中自动激活 Unity Editor 了。值得一提的是国区特供的团结引擎, 由于其 Unity 版本号后会多出一个 c1 后缀——例如 2022.3.55f1 会变为 2022.3.55f1c1——这会导致 GameCI 无法正确拉取构建所需的 Unity Editor 镜像, 因此在使用中需要自行构建对应版本的 Unity Editor 镜像推送到本地存储库中, 防止流水线执行失败。其激活方式似乎与国际版 Unity 也有细微区别, 请自行微调配置。

3. 配置项目目录中的GitLab-CI

首先得获取 GameCI 的项目文件, 新建一个文件夹并在其中克隆 GameCI 的示例项目即可:

git clone https://gitlab.com/game-ci/unity3d-gitlab-ci-example.git
GameCI 示例项目的目录

第二步是新建 Unity 项目并打开 Unity 项目目录, GameCI 文档中提示执行 mkdir -p Assets/Scripts/Editor/, 意即在 Assets 目录下建立 /Scripts/Editor 目录:

这是已经复制了 BuildCommand.csAssets/Scripts/Editor/ 目录

GameCI 文档中要求复制如下文件:

cp ../unity3d-gitlab-ci-example/.gitlab-ci.yml ./
cp -r ../unity3d-gitlab-ci-example/ci ./
cp ../unity3d-gitlab-ci-example/Assets/Scripts/Editor/BuildCommand.cs ./Assets/Scripts/Editor/

在 Windows 上的 Unity 项目目录中, 则表示将以下内容复制到新建的 Unity 项目根目录中:

除了红框中的文件和目录, 还得复制Assets下的Assets/Scripts/Editor/BuildCommand.cs到对应位置

以及将 unity3d-gitlab-ci-example 中的 Assets/Scripts/Editor/BuildCommand.cs 复制到新建的 Unity 项目的 Assets/Scripts/ 目录下。

  • .gitlab-ci.yml 定义了 CI 阶段、环境变量和其他配置数据, 它应位于项目根目录中
  • BuildCommand.cs 文件, 定义了构建过程的 C# 脚本, 以便可以通过命令行生成项目, 需置于 Assets/Scripts/Editor
  • 各种 shell 脚本, 用于获取 Unity 许可证激活文件、准备 CI 阶段环境或触发项目构建, 均置于项目根目录中的 ci/ 文件夹中
文件复制好之后的 Unity 项目根目录

只需要这些简单的文件添加, 项目的初始 CI 设置便完成了, 很简单吧?

4. 分离测试, 构建阶段, 添加发布和部署

到此为止, 其实依托 GameCI 的GitLab Unity CI 已经能够正常工作了, 习惯性地推送更改 git push origin main 就能触发流水线, 获取 Unity 版本号并进行 test-and-build。但 GameCI 中默认的 test-and-build 其实不太符合正常的开发逻辑, 因为日常开发中大概率无需在每次提交变更后都完整构建引用, 仅需要流水线在提交时帮我们测试代码即可。因此我将 GameCI 中的 test-and-build 分离为了 testbuild 两个阶段, 并在此基础上新增了 release 阶段, 同时要求仅在提交了带版本号的标签时才触发构建, 且完成构建后由 release 阶段自动建立发布页面。直接修改 .gitlab-ci.yml (文件存档) 就能实现这些改进, 这是我稍加改进后的版本:

stages:
  - prepare
  - test
  - build
  - deploy
  - release

# 如果您要添加 'UNITY_LICENSE_FILE' 和其他密钥,请访问您的项目 GitLab 页面:
# settings > CI/CD > Variables instead
variables:
  BUILD_NAME: GameCI
  IMAGE: unityci/editor # https://hub.docker.com/r/unityci/editor
  IMAGE_VERSION: 3.1.0 # 它会自动使用最新的 v3.x.x,参见 https://github.com/game-ci/docker/releases
  UNITY_DIR: $CI_PROJECT_DIR # 这需要是绝对路径。默认为项目根目录。
  # 您可以在 Unity 中通过 Application.version 暴露此值
  VERSION_NUMBER_VAR: $CI_COMMIT_REF_SLUG-$CI_PIPELINE_ID-$CI_JOB_ID
  VERSION_BUILD_VAR: $CI_PIPELINE_IID
  # 尝试手动指定构建版本
  UNITY_VERSION: 6000.0.54f1

# 全局默认镜像, 用于没有指定iamge的stage
image: $IMAGE:$UNITY_VERSION-base-$IMAGE_VERSION

get-unity-version:
  image: alpine
  stage: prepare
  variables:
    GIT_DEPTH: 1
  script:
    - echo UNITY_VERSION=$(cat $UNITY_DIR/ProjectSettings/ProjectVersion.txt | grep "m_EditorVersion:.*" | awk '{ print $2}') | tee prepare.env
  artifacts:
    reports:
      dotenv: prepare.env

.unity_before_script: &unity_before_script
  before_script:
    - chmod +x ./ci/before_script.sh && ./ci/before_script.sh
  needs:
    - job: get-unity-version
      artifacts: true

.unity_after_script: &unity_after_script
  after_script:
    - chmod +x ./ci/return_license.sh && ./ci/return_license.sh

.cache: &cache
  cache:
    key: "$CI_PROJECT_NAMESPACE-$CI_PROJECT_NAME-$CI_COMMIT_REF_SLUG-$TEST_PLATFORM"
    paths:
      - $UNITY_DIR/Library/
      - $UNITY_DIR/../unity-builder

.license: &license
  rules:
    - if: '$UNITY_LICENSE != null || $UNITY_SERIAL != null'
      when: always

.unity_defaults: &unity_defaults
  <<:
    - *unity_before_script
    - *cache
    - *license
    - *unity_after_script

.test: &test
  stage: test
  <<: *unity_defaults
  script:
    - chmod +x ./ci/test.sh && ./ci/test.sh
  artifacts:
    when: always
    expire_in: 2 weeks
  # https://gitlab.com/gableroux/unity3d-gitlab-ci-example/-/issues/83
  # 如果您使用自建Runner, 可能需要删除或替换该设置
  # tags:
  #   - gitlab-org
  coverage: /<Linecoverage>(.*?)</Linecoverage>/

# 不使用 junit 报告的 GitLab 测试
test-playmode:
  <<: *test
  variables:
    TEST_PLATFORM: playmode
    TESTING_TYPE: NUNIT

test-editmode:
  <<: *test
  variables:
    TEST_PLATFORM: editmode
    TESTING_TYPE: NUNIT

# 如果您希望在 GitLab 中使用 JUnit 报告 Unity 测试结果,请取消注释以下代码块
# 我们目前存在以下问题导致此功能暂时无法正常工作,但如果您对此功能感兴趣,可以帮助我们解决这个问题:
# https://gitlab.com/gableroux/unity3d-gitlab-ci-example/-/issues/151

# .test-with-junit-reports: &test-with-junit-reports
#   stage: test
#   <<: *unity_defaults
#   script:
#     # 通过将这些包添加到基础镜像或在单独的作业中运行可以加快速度
#     # 我们可以使用只有这两个依赖项的镜像,并且只在前一个作业的产物上执行 saxonb-xslt 命令
#     - apt-get update && apt-get install -y default-jre libsaxonb-java
#     - chmod +x ./ci/test.sh && ./ci/test.sh
#     - saxonb-xslt -s $UNITY_DIR/$TEST_PLATFORM-results.xml -xsl $CI_PROJECT_DIR/ci/nunit-transforms/nunit3-junit.xslt >$UNITY_DIR/$TEST_PLATFORM-junit-results.xml
#   artifacts:
#     when: always
#     reports:
#       junit:
#         - $UNITY_DIR/$TEST_PLATFORM-junit-results.xml
#         - $UNITY_DIR/$TEST_PLATFORM-coverage/
#     expire_in: 2 weeks
#   # https://gitlab.com/gableroux/unity3d-gitlab-ci-example/-/issues/83
#   # 如果您使用自建Runner,可能需要删除或替换这些设置以适应您的需求
#   tags:
#     - gitlab-org
#   coverage: /<Linecoverage>(.*?)</Linecoverage>/

# test-playmode-with-junit-reports:
#   <<: *test-with-junit-reports
#   variables:
#     TEST_PLATFORM: playmode
#     TESTING_TYPE: NUNIT

# test-editmode:
#   <<: *test
#   variables:
#     TEST_PLATFORM: editmode
#     TESTING_TYPE: NUNIT

# 如果您希望在 GitLab 中使用 JUnit 报告 Unity 测试结果,请取消注释以下代码块
# 我们目前存在以下问题导致此功能暂时无法正常工作,但如果您对此功能感兴趣,
# 可以帮助我们解决这个问题:
# https://gitlab.com/gableroux/unity3d-gitlab-ci-example/-/issues/151

# .test-with-junit-reports: &test-with-junit-reports
#   stage: test
#   <<: *unity_defaults
#   script:
#     # 通过将这些包添加到基础镜像或在单独的作业中运行可以加快速度
#     # 我们可以使用只有这两个依赖项的镜像,并且只在前一个作业的产物上执行 saxonb-xslt 命令
#     - apt-get update && apt-get install -y default-jre libsaxonb-java
#     - chmod +x ./ci/test.sh && ./ci/test.sh
#     - saxonb-xslt -s $UNITY_DIR/$TEST_PLATFORM-results.xml -xsl $CI_PROJECT_DIR/ci/nunit-transforms/nunit3-junit.xslt >$UNITY_DIR/$TEST_PLATFORM-junit-results.xml
#   artifacts:
#     when: always
#     paths:
#     # 导出此路径以便在需要时查看详细的覆盖率报告
#     - $UNITY_DIR/$TEST_PLATFORM-coverage/
#     reports:
#       junit:
#         - $UNITY_DIR/$TEST_PLATFORM-junit-results.xml
#         - "$UNITY_DIR/$TEST_PLATFORM-coverage/coverage.xml"
#     expire_in: 2 weeks
#   # https://gitlab.com/gableroux/unity3d-gitlab-ci-example/-/issues/83
#   # 如果您使用自建Runner,可能需要删除或替换这些设置以适应您的需求
#   # tags:
#   #   - gitlab-org
#   coverage: /<Linecoverage>(.*?)</Linecoverage>/

# test-playmode-with-junit-reports:
#   <<: *test-with-junit-reports
#   variables:
#     TEST_PLATFORM: playmode
#     TESTING_TYPE: JUNIT

# test-editmode-with-junit-reports:
#   <<: *test-with-junit-reports
#   variables:
#     TEST_PLATFORM: editmode
#     TESTING_TYPE: JUNIT

.build: &build
  stage: build
  <<: *unity_defaults
  script:
    - chmod +x ./ci/build.sh && ./ci/build.sh
    # 安装 zip 工具
    - apt-get update && apt-get install -y zip
  after_script:
    # 打包 ZIP 并保存为 artifact
    - cd $UNITY_DIR/Builds/
    - zip -r "${BUILD_NAME}-${BUILD_TARGET}-${CI_COMMIT_TAG}.zip" "${BUILD_TARGET}/"
  artifacts:
    paths:
      - $UNITY_DIR/Builds/${BUILD_NAME}-${BUILD_TARGET}-${CI_COMMIT_TAG}.zip

  # 只有带版本标签的提交才构建
  # git tag v1.0.0
  # git push origin v1.0.0
  # https://gitlab.com/gableroux/unity3d-gitlab-ci-example/-/issues/83
  # you may need to remove or replace these to fit your need if you are using your own runners
  # tags:
  #   - gitlab-org
  
  rules:
    - if: $CI_COMMIT_TAG =~ /^v[0-9]+\.[0-9]+\.[0-9]+/  

# build-StandaloneLinux64:
#   <<: *build
#   variables:
#     BUILD_TARGET: StandaloneLinux64

build-StandaloneLinux64-il2cpp:
  <<: *build
  image: $IMAGE:$UNITY_VERSION-linux-il2cpp-$IMAGE_VERSION
  variables:
    BUILD_TARGET: StandaloneLinux64
    SCRIPTING_BACKEND: IL2CPP

# build-StandaloneOSX:
#   <<: *build
#   image: $IMAGE:$UNITY_VERSION-mac-mono-$IMAGE_VERSION
#   variables:
#     BUILD_TARGET: StandaloneOSX

#Note: build target names changed in recent versions, use this for versions < 2017.2:
# build-StandaloneOSXUniversal:
#   <<: *build
#   variables:
#     BUILD_TARGET: StandaloneOSXUniversal

build-StandaloneWindows64:
  <<: *build
  image: $IMAGE:$UNITY_VERSION-windows-mono-$IMAGE_VERSION
  variables:
    BUILD_TARGET: StandaloneWindows64

# 对于 WebGL 支持,您需要在 v0.9 版本中将压缩格式设置为禁用。参见 https://github.com/game-ci/docker/issues/75
# build-WebGL:
#   <<: *build
#   image: $IMAGE:$UNITY_VERSION-webgl-$IMAGE_VERSION
#   # 临时解决方法:针对 https://github.com/game-ci/docker/releases/tag/v0.9 和当前项目中的 WebGL 支持,防止因缺少 ffmpeg 而出现错误
#   before_script:
#     - chmod +x ./ci/before_script.sh && ./ci/before_script.sh
#     - apt-get update && apt-get install ffmpeg -y
#   variables:
#     BUILD_TARGET: WebGL

# build-android:
#   <<: *build
#   image: $IMAGE:$UNITY_VERSION-android-$IMAGE_VERSION
#   variables:
#     BUILD_TARGET: Android
#     BUILD_APP_BUNDLE: "false"

# build-android-il2cpp:
#   <<: *build
#   image: $IMAGE:$UNITY_VERSION-android-$IMAGE_VERSION
#   variables:
#     BUILD_TARGET: Android
#     BUILD_APP_BUNDLE: "false"
#     SCRIPTING_BACKEND: IL2CPP

#deploy-android:
#  stage: deploy
#  image: ruby
#  script:
#    - cd $UNITY_DIR/Builds/Android
#    - echo $GPC_TOKEN > gpc_token.json
#    - gem install bundler
#    - bundle install
#    - fastlane supply --aab "${BUILD_NAME}.aab" --track internal --package_name com.youcompany.yourgame --json_key ./gpc_token.json
#  needs: ["build-android"]

# build-ios-xcode:
#   <<: *build
#   image: $IMAGE:$UNITY_VERSION-ios-$IMAGE_VERSION
#   variables:
#     BUILD_TARGET: iOS

#build-and-deploy-ios:
#  stage: deploy
#  script:
#    - cd $UNITY_DIR/Builds/iOS/$BUILD_NAME
#    - pod install
#    - fastlane ios beta
#  tags:
#    - ios
#    - mac
#  needs: ["build-ios-xcode"]

# pages:
#   image: alpine:latest
#   stage: deploy
#   script:
#     - mv "$UNITY_DIR/Builds/WebGL/${BUILD_NAME}" public
#   artifacts:
#     paths:
#       - public
#   only:
#     - $CI_DEFAULT_BRANCH

create-release:
  stage: release
  image: registry.gitlab.com/gitlab-org/release-cli:latest
  needs:
    - job: build-StandaloneLinux64-il2cpp
      artifacts: true
    - job: build-StandaloneWindows64
      artifacts: true
  before_script:
    # 安装必要的工具
    - apk add --no-cache git
  script:
    # 生成 CHANGELOG 内容
    - |
      # 获取上一个 tag(如果没有则使用第一个提交)
      PREVIOUS_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || git rev-list --max-parents=0 HEAD)
      
      # 生成 CHANGELOG 内容
      echo "# ${CI_COMMIT_TAG} 版本发布说明" > /tmp/release_notes.md
      echo "" >> /tmp/release_notes.md
      
      # 添加版本信息
      echo "## 版本信息" >> /tmp/release_notes.md
      echo "- **当前版本**: ${CI_COMMIT_TAG}" >> /tmp/release_notes.md
      echo "- **上一版本**: ${PREVIOUS_TAG:-Initial release}" >> /tmp/release_notes.md
      echo "" >> /tmp/release_notes.md
      
      # 添加变更内容
      echo "## 更新内容" >> /tmp/release_notes.md
      echo "" >> /tmp/release_notes.md
      
      # 获取提交列表(排除 merge commits)
      if git log --no-merges --pretty=format:"- %s (%h)" "$PREVIOUS_TAG..HEAD" > /tmp/commits.txt && [ -s /tmp/commits.txt ]; then
        cat /tmp/commits.txt >> /tmp/release_notes.md
      else
        echo "- 本次发布无主要更新" >> /tmp/release_notes.md
      fi
      
      echo "" >> /tmp/release_notes.md
      
      # 添加贡献者信息
      echo "## 维护者" >> /tmp/release_notes.md
      echo "" >> /tmp/release_notes.md
      if git log --no-merges --pretty=format:"- %an" "$PREVIOUS_TAG..HEAD" | sort -u > /tmp/contributors.txt && [ -s /tmp/contributors.txt ]; then
        cat /tmp/contributors.txt >> /tmp/release_notes.md
      else
        echo "- 暂无维护者" >> /tmp/release_notes.md
      fi
      
      # 显示生成的 CHANGELOG 内容(用于调试)
      echo "=== 生成的发布说明 ==="
      cat /tmp/release_notes.md
      echo "==============================="
  release:
    tag_name: '$CI_COMMIT_TAG'
    description: $(cat /tmp/release_notes.md)
    assets:
      links:
        # 链接到创建的单独 ZIP 文件
        # - name: 'Linux Build (Mono)'
        #   url: '${CI_PROJECT_URL}/-/jobs/artifacts/${CI_COMMIT_TAG}/raw/Builds/${BUILD_NAME}-StandaloneWindows64-${CI_COMMIT_TAG}.zip?job=build-StandaloneLinux64'
        - name: 'Linux Build (IL2CPP)'
          url: '${CI_PROJECT_URL}/-/jobs/artifacts/${CI_COMMIT_TAG}/raw/Builds/${BUILD_NAME}-StandaloneLinux64-${CI_COMMIT_TAG}.zip?job=build-StandaloneLinux64-il2cpp'
        - name: 'Windows Build (Mono)'
          url: '${CI_PROJECT_URL}/-/jobs/artifacts/${CI_COMMIT_TAG}/raw/Builds/${BUILD_NAME}-StandaloneWindows64-${CI_COMMIT_TAG}.zip?job=build-StandaloneWindows64'
        # - name: 'Windows Build (IL2CPP)'
        #   url: '${CI_PROJECT_URL}/-/jobs/artifacts/${CI_COMMIT_TAG}/raw/Builds/${BUILD_NAME}-StandaloneWindows64-${CI_COMMIT_TAG}.zip?job=build-StandaloneWindows64-il2cpp'
  # 只有带版本标签的提交才创建 Release
  # git tag v1.0.0
  # git push origin v1.0.0
  rules:
    - if: $CI_COMMIT_TAG =~ /^v[0-9]+\.[0-9]+\.[0-9]+/  

workflow:
  rules:
    - if: $CI_MERGE_REQUEST_ID
      when: never
    # 阻止标签触发的规则
    # - if: $CI_COMMIT_TAG
    #   when: never
    - when: always

可根据你的具体需要注释和启用响应行, 我在该文件中引入了如下几个主要改动:

  • buildrelease 阶段均采用标签条件触发
  • build 后将生成的 artifacts 压缩为 zip 文件, 便于其他任务引用
  • 可以通过 UNITY_VERSION 手动指定 Unity Editor 版本, 将该值移除后添加为流水线变量也是可以的
  • 流水线的 release 阶段可以自动收集提交信息来生成发布信息, 并自动附在发布页面中

同时, 为了应对代码覆盖率报告 Summary.xml 文件不存在 (例如安装了覆盖率统计工具但没有生成报告的情形) 导致 test 阶段任务失败的情形, 我调整了 ci 目录下的 test.sh 文件, 即用以下内容:

# 安全检查 Code Coverage 包和 Summary.xml 文件
if grep -q $CODE_COVERAGE_PACKAGE $PACKAGE_MANIFEST_PATH; then
  SUMMARY_FILE="$UNITY_DIR/$TEST_PLATFORM-coverage/Report/Summary.xml"
  
  # 检查 Summary.xml 是否存在
  if [ -f "$SUMMARY_FILE" ]; then
    cat "$SUMMARY_FILE" | grep Linecoverage
  else
    echo "Warning: Summary.xml not found at $SUMMARY_FILE"
    echo "Coverage data might be in raw XML format only."
    
    # 列出 coverage 目录结构帮助调试
    echo "=== Coverage Directory Structure ==="
    ls -R "$UNITY_DIR/$TEST_PLATFORM-coverage/" || echo "Could not list directory"
    echo "===================================="
  fi
  # 移动覆盖率文件(如果存在)
  if [ -d "$UNITY_DIR/$TEST_PLATFORM-coverage/$CI_PROJECT_NAME-opencov" ]; then
    mv $UNITY_DIR/$TEST_PLATFORM-coverage/$CI_PROJECT_NAME-opencov/*Mode/TestCoverageResults_*.xml $UNITY_DIR/$TEST_PLATFORM-coverage/coverage.xml
    rm -r $UNITY_DIR/$TEST_PLATFORM-coverage/$CI_PROJECT_NAME-opencov/
  fi

替换掉了原本的:

if grep $CODE_COVERAGE_PACKAGE $PACKAGE_MANIFEST_PATH; then
  cat $UNITY_DIR/$TEST_PLATFORM-coverage/Report/Summary.xml | grep Linecoverage
  mv $UNITY_DIR/$TEST_PLATFORM-coverage/$CI_PROJECT_NAME-opencov/*Mode/TestCoverageResults_*.xml $UNITY_DIR/$TEST_PLATFORM-coverage/coverage.xml
  rm -r $UNITY_DIR/$TEST_PLATFORM-coverage/$CI_PROJECT_NAME-opencov/

如果需要我的改进版本, 使用 .gitlab-ci.yml (文件存档) 和 test.sh (文件存档) 替换掉 Unity 项目中的对应文件即可将项目转为这个5阶段的流水线。

5. 最后一步: 推送, 触发, 自动化!

在 Unity 项目中提交打上标签, 就能自动触发 GameCI 的构建和发布流水线:

# 以tag v1.0.0 为例
git tag v1.0.0
git push origin v1.0.0
All Green! 干杯!

0. 遗留问题: 在 Linux 上构建 Windows IL2CPP

GitHub Issue – How to build for il2cppGameCI – Windows docker images 不难发现, 在 Linux 上用 Docker 容器构建 Linxu64-IL2CPP 的过程非常流畅细腻, 而构建 Windows64-IL2CPP 版本则几乎不可能了——即使使用 Windows 镜像进行测试和构建, 也需要手动挂载 Windows SDK 和 .Net Framework 到容器中, 这无疑是十分繁琐且容易出错的。如果希望在单机 Linux 上并行构建 Linux 和 Windows 的 IL2CPP 版本, 建立一台 Windows 虚拟机并使用 Windows Runner 来执行原生构建才是相对可靠的方案。

参考文献:

  1. GameCI – GitLab Doc
  2. GitLab – Get started with GitLab CI/CD
  3. RIOT Game – The Legends of Runeterra CI/CD Pipeline
  4. GDC – Stress-Free Game Development: Powering Up Your Studio With DevOps
  5. What is GameCI?
  6. Example GitLab Runner docker compose configuration
  7. gitlab-runner-docker-compose/docker-compose.yml
  8. GitLab – Run GitLab Runner in a container
  9. GitLab – Registering runners
  10. Base64 Encoding: A Visual Explanation
  11. 规范 – 约定式提交
  12. 语义化版本 2.0.0
  13. Tutorial: Automate releases and release notes with GitLab
  14. GitHub Issue – How to build for il2cpp
  15. GameCI – Windows docker images
  16. Unity Documentation – BuildOptions
  17. GameCI Supported Editor Versions List
  18. How to Use Your GPU in a Docker Container
  19. Microsoft – GPU acceleration in Windows containers
  20. GitLab – Install GitLab Runner on Windows

Published by