首页
/ 深入解读 ASP.NET Core 仓库中的 Arcade 模板双轨制:templates 与 templates-official 的选择、结构与源码实现

深入解读 ASP.NET Core 仓库中的 Arcade 模板双轨制:templates 与 templates-official 的选择、结构与源码实现

2026-09-08 23:56:43作者:傅爽业Veleda

本文围绕仓库内 eng/common/template-guidance.md 这一篇模板使用指南展开。ASP.NET Core 的持续集成(CI)流水线并不直接书写裸的 Azure Pipelines YAML,而是通过 Arcade 提供的一套分层模板来组合 job、steps 与 artifact 发布逻辑。这套体系同时维护 templates(标准模板)与 templates-official(1ES 托管模板)两套入口,并复用 core-templates 中的核心逻辑。读完本文,你将掌握两套模板的适用场景、is1ESPipeline/templateIs1ESManaged 参数的含义、1ES Multiple Outputs 机制如何减少安全扫描注入,以及 shim / logic / redirect 三层目录结构的源码级实现与真实调用链。

一、概述:为什么一套 CI 需要两套模板入口

Arcade(.NET Core Engineering 提供的跨仓库构建基础)为参与构建的仓库提供两类模板:

  • /eng/common/templates公开(public)场景使用的标准模板,例如在 GitHub 上 fork 出来的公开 CI、PR 校验等;
  • /eng/common/templates-official1ES(One Engineering System)流水线模板场景。凡是"必须由 1ES 流水线模板托管"的流水线,都应当引用 /templates-official

在本文所对应的仓库中,这两套目录真实存在并持续演进:eng/common/templates 目前包含 26 个 YAML 文件,eng/common/templates-official 包含 24 个 YAML 文件,二者在 job/jobs/post-build/steps/variables/ 五大子目录下保持一一对应的文件结构——这正是为了支撑"同一套构建逻辑、两种运行环境"。

注意:原文档正文中将官方目录写作 /templates-offical(拼写错误),正确目录名应为 /eng/common/templates-official

二、基本使用规则:什么时候选哪一套

原文档给出的选择规则非常明确,可用三句话概括:

  1. 凡是 1ES Pipeline Template 或 1ES Microbuild 模板驱动的运行,一律引用 eng/common/templates-official
  2. 任何内部生产级(internal production-graded)流水线都应使用 templates-official
  3. 其余所有运行引用 eng/common/templates

换句话说,"public / PR 校验"走 templates,"internal 生产构建(涉及签名、发布、1ES 安全治理)"走 templates-official。这一判断与仓库中真实流水线对 System.TeamProject 的判别完全一致,我们会在后文"真实落地"一节看到具体写法。

三、关键的"模板流派"参数:templateIs1ESManaged / is1ESPipeline

3.1 参数作用

templateIs1ESManaged(文档写作时点的命名)暴露在大多数模板上,决定嵌套模板展开时到底选择哪一套变体。原文档特别强调:对于 job/jobs/steps/post-build/ 下的模板,该参数必须被显式设置,不允许依赖默认值。

3.2 当前仓库中的实际实现:is1ESPipeline

值得指出的是,从当前仓库源码看,该选择参数的实际载体已演变为 is1ESPipeline。整个 eng/common 目录的搜索结果显示:templateIs1ESManaged 字样只出现在 template-guidance.md 的说明文本中,而所有真实模板统一使用 is1ESPipeline 布尔参数。

以最典型的 job/job.yml 为例:

  • eng/common/templates/job/job.yml 这个 shim 将 is1ESPipeline 硬编码为 false
    jobs:
    - template: /eng/common/core-templates/job/job.yml
      parameters:
        is1ESPipeline: false
    
  • eng/common/templates-official/job/job.yml 这个 shim 则将 is1ESPipeline 硬编码为 true,并附加 1ES 专用的 templateContext
    jobs:
    - template: /eng/common/core-templates/job/job.yml
      parameters:
        is1ESPipeline: true
        # 1ES managed templates 使用 templateContext.output 处理多输出
        templateContext:
          outputParentDirectory: $(Build.ArtifactStagingDirectory)
          ...
    

3.3 "必须显式设置"的强制措施

为了让"必须显式设置"从约定变成硬约束,core-templates/job/job.yml 在 job 的 steps 起点放置了一道非法入口守卫

steps:
- ${{ if eq(parameters.is1ESPipeline, '') }}:
  - 'Illegal entry point, is1ESPipeline is not defined. Repository yaml should not directly reference templates in core-templates folder.': error

这行 YAML 的含义是:一旦某个 job 直接引用了 core-templates(跳过 shim 入口),is1ESPipeline 就是空字符串,流水线会直接报错并中止。由此可以推断出两条重要设计意图:

  1. 仓库级 YAML 禁止直接引用 eng/common/core-templates,必须经过 templatestemplates-official 的 shim;
  2. is1ESPipeline 的值只能由 shim 入口注入,普通调用方无法也不应该手工传入。

四、Multiple Outputs:用 1ES 特性削减安全扫描注入

4.1 背景与动机

1ES 流水线模板实施了一条策略:每执行一次"发布 artifact"的任务,都会向流水线注入额外的安全扫描。对于大型仓库(如 ASP.NET Core 这种动辄十几二十个发布步骤的流水线),扫描任务会被成倍注入,拖慢整条流水线。

4.2 解决方案

当使用 templates-official/jobs/jobs.yml 时,Arcade 通过以下组合来压缩注入次数:

  1. 所有待发布输出统一收集到 $(Build.ArtifactStagingDirectory)(Azure Pipelines 的预定义暂存目录);
  2. 借助 1ES 流水线模板的 outputParentDirectory 特性,在一次模板上下文中声明多个 outputs,从而把多次安全扫描合并处理。

从源码看,eng/common/templates-official/job/job.yml 正是把这一思想落到了 job 级:它在 templateContext 里先设置 outputParentDirectory: $(Build.ArtifactStagingDirectory),然后以 outputs: 列表统一声明 pipelineArtifact 输出——包括常规 artifacts、失败重试副本(_Attempt$(System.JobAttempt))、日志(并显式标注 isProduction: false # logs are non-production artifacts)、BuildConfiguration、以及可选的 V4 publishing(publishingVersion: 4 + enablePublishing: 'true' 时自动以 $(System.PhaseName)_Artifacts 发布)。

4.3 完整示例

原文档给出了一个可直接照搬的 azure-pipelines.yml 写法,此处完整保留并补充注释:

extends:
  template: azure-pipelines/MicroBuild.1ES.Official.yml@MicroBuildTemplate
  parameters:
    stages:
    - stage: build
      jobs:
      - template: /eng/common/templates-official/jobs/jobs.yml@self
        parameters:
          # 1ES 借助 outputs 机制降低安全任务注入开销
          templateContext:
            outputs:
            - output: pipelineArtifact
              displayName: 'Publish logs from source'
              continueOnError: true
              condition: always()
              targetPath: $(Build.ArtifactStagingDirectory)/artifacts/log
              artifactName: Logs
          jobs:
          - job: Windows
            steps:
            - script: echo "friendly neighborhood" > artifacts/marvel/spiderman.txt
          # 将构建输出复制到 artifact 暂存目录,供统一发布
          - task: CopyFiles@2
            displayName: Gather build output
            inputs:
              SourceFolder: '$(System.DefaultWorkingDirectory)/artifacts/marvel'
              Contents: '**'
              TargetFolder: '$(Build.ArtifactStagingDirectory)/artifacts/marvel'

几点实操要点:

  • targetPath 务必指向 $(Build.ArtifactStagingDirectory) 之下的子目录,而不是随意目录,否则无法被 outputParentDirectory 统一归拢;
  • condition: always() / continueOnError: true 组合常用于日志发布,保证即便构建失败也能拿到日志(仓库内大量真实 job 的日志 artifact 均如此设置);
  • 关键限制:Multiple Outputs 只适用于 1ES 流水线模板的发布(即只有引用 templates-official 时才可用)。如果走 templates(标准模板 / 公开构建),应回归传统的逐任务 publish 模式。

五、开发视角:目录结构与 shim / logic / redirect 三角色

5.1 总体目录骨架

原文档给出了 eng/common 下的分层结构。对照当前仓库,core-templatestemplatestemplates-official 三棵目录并列存在,逻辑上的组织方式如下(已结合仓库现状核对):

eng\common\
    [templates || templates-official]\
        job\            job.yml / onelocbuild.yml / publish-build-assets.yml
                        source-build.yml / source-index-stage1.yml
        jobs\           jobs.yml / source-build.yml
        post-build\     post-build.yml / common-variables.yml / setup-maestro-vars.yml
        steps\          publish-build-artifacts.yml / publish-pipeline-artifacts.yml
                        publish-logs.yml / retain-build.yml / send-to-helix.yml
                        source-build.yml / component-governance.yml
                        (+ 仓库演进后新增的 enable-internal-*.yml、
                          generate-sbom.yml、get-*.yml、vmr-sync.yml 等)
        variables\      pool-providers.yml
    core-templates\
        job\            job.yml 等(逻辑实现)
        jobs\           jobs.yml 等
        post-build\     post-build.yml 等
        steps\          publish-*.yml 等
        variables\      pool-providers.yml

需要注意:当前仓库中的实际文件比文档写作时的列表更丰富(例如新增了 enable-internal-runtimes.ymlgenerate-sbom.ymlget-delegation-sas.ymlvmr-sync.yml 等),且存在 templates/job/source-index-stage1.yml 等细微差异;但"双 shim 入口 + core-templates 共享逻辑"的组织原则没有变化。

5.2 三种文件角色

原文档用一个简洁的模型定义了每类 YAML 的职责,这也是理解整棵模板树的关键:

角色 含义 存放位置 典型职责
shim(垫片) 介于"仓库流水线"与 ".NET Core Engineering 模板"之间的一层中间 YAML templates / templates-official 定义 is1ESPipeline 参数的取值(false 或 true),并把参数原样透传给 core-templates
logic(逻辑) 真正的模板基础逻辑 主要在 core-templates 实现 job/jobs 展开、variables 注入、publish 等真实行为
redirect(重定向) core-templates 中"逻辑因 shim 入口而异"的位置,把请求送回对应的逻辑文件 core-templates 内少量文件 依据 is1ESPipelinetemplatestemplates-official 之间二选一

5.3 逻辑的主要存放位置与例外

Arcade 模板的逻辑绝大部分集中在 core-templates,唯一的例外是 artifact 发布:1ES 模板与标准模板在"如何发布产物"上差异过大(1ES 走 templateContext.outputs,标准模板走显式的 publish 步骤),因此 publish-* 相关的发布逻辑在 templatestemplates-official 中各自保留,并由 core-templates 里的对应文件做 redirect 到双方逻辑文件。

进一步细分:

  • templatestemplates-official 中,"stages" 以及 "jobs" / "job" 粒度的文件已被替换为 shim;
  • "steps" 与 "variables" 粒度的文件过于细碎,不值得为它们再做一层 shim,于是这些层级中"两种场景通用"的逻辑会直接常驻在 templates / templates-official 中;
  • core-templates 内部,凡是"逻辑取决于从哪个 shim 入口进来"的地方,就使用 redirect 回到 templates / templates-official 中对应的逻辑文件。

5.4 一个判断规则

如果某个文件被 templates 下的 shim 引用,则 is1ESPipeline = false;如果被 templates-official 下的 shim 引用,则 is1ESPipeline = true。这个取值关系贯穿全树,是最容易记忆也最容易在排查问题(例如误走了 1ES 发布分支)时定位的依据。

六、源码级剖析:一对 job.yml 的 shim 与 core-templates 逻辑

为了把"shim → logic"关系讲透,直接对比三个 job/job.yml

6.1 标准模板 shim(is1ESPipeline: false

eng/common/templates/job/job.yml 的职责是:

  1. 引用 /eng/common/core-templates/job/job.yml 并传入 is1ESPipeline: false
  2. 用模板表达式 ${{ each parameter in parameters }} 把除 stepsis1ESPipeline 之外的参数原样透传
  3. 把自己的 steps 透传后,额外拼接 artifactPublishSteps——这里直接引用 core-templates/steps/publish-pipeline-artifacts.yml,以显式任务形式发布 pipeline artifacts(例如 $(Build.ArtifactStagingDirectory)/artifacts 的产物与日志,均带 retryCountOnTaskFailure: 10,用于缓解 Windows 文件占用导致的发布失败)。
jobs:
- template: /eng/common/core-templates/job/job.yml
  parameters:
    is1ESPipeline: false
    ${{ each parameter in parameters }}:
      ${{ if and(ne(parameter.key, 'steps'), ne(parameter.key, 'is1ESPipeline')) }}:
        ${{ parameter.key }}: ${{ parameter.value }}
    steps:
    - ${{ each step in parameters.steps }}:
      - ${{ step }}
    artifactPublishSteps: ...

6.2 1ES 模板 shim(is1ESPipeline: true

eng/common/templates-official/job/job.yml 的差异点恰是前一节 Multiple Outputs 的落地:

jobs:
- template: /eng/common/core-templates/job/job.yml
  parameters:
    is1ESPipeline: true
    templateContext:
      outputParentDirectory: $(Build.ArtifactStagingDirectory)
      outputs:
      - output: pipelineArtifact
        displayName: Publish pipeline artifacts
        targetPath: '$(Build.ArtifactStagingDirectory)/artifacts'
        artifactName: ...
        condition: succeeded()
        ...

它不再手工追加发布步骤,而是把发布声明为 templateContext.outputs(其中日志、BuildConfiguration 等非生产产物标注 isProduction: false),交给 1ES 引擎按多输出策略统一处理;最后再透传调用方在根 YAML 中额外追加的 outputs 与其余 templateContext 属性。

6.3 core-templates 中的真实逻辑

eng/common/core-templates/job/job.yml 才是 job 本体的实现:它完整承载 Azure Pipelines job schema(cancelTimeoutInMinutesconditioncontainerdependsOnpoolstrategytimeoutInMinutesvariablesworkspace 等),并处理若干仓库通用关注点:

  • 变量注入:统一注入 AllowPtrToDetectTestRunRetryFiles、NuGet 签名重试策略 NUGET_EXPERIMENTAL_CHAIN_BUILD_RETRY_POLICY: 3,2000 等;同时兼容 name/value、variable group、template、key-value 四种变量写法;
  • 项目类型感知:当 System.TeamProject == 'public' 时跳过 Component Governance 检测与 CodeQL 自动注入(skipComponentGovernanceDetectionCodeql.SkipTaskAutoInjection);
  • 内部构建增强:非 public 且非 PR 时自动安装/清理 Microbuild、执行 NuGetAuthenticate@1,内部构建还引入 DotNet-HelixApi-Access 变量组;
  • 结果与产物收集:按 artifacts.download/publishenablePublishTestResults(xUnit/TRX)、enablePublishBuildArtifactsenableBuildRetry 等参数,通过 CopyFiles@2artifacts/binartifacts/packagesartifacts/log 统一搬运进 $(Build.ArtifactStagingDirectory),为上层发布做好准备。

从这段实现可以确认:无论走哪套 shim,job 的核心骨架(容器、池、变量、步骤编排、产物归拢)都在 core-templates 中只写了一份——这正是文档所说"logic 主要位于 core-templates"的代码证据,也避免了同一份 job 逻辑被复制两份后漂移的风险。

七、实战观察:variables/pool-providers.yml 中的 redirect 与双轨分流

variables 粒度"保留逻辑而非 shim"的说法,可以在真实的 pool-providers.yml 中看到极佳案例。

eng/common/templates/variables/pool-providers.yml 既承担逻辑又承担redirect

  • 当运行在 internal 项目中时,它直接 redirect 到 templates-official/variables/pool-providers.yml
    variables:
      - ${{ if eq(variables['System.TeamProject'], 'internal') }}:
        - template: /eng/common/templates-official/variables/pool-providers.yml
      - ${{ else }}:
        ...计算 DncEngPublicBuildPool / DncEngInternalBuildPool...
    
  • else 分支(public 场景)则由它自己完成逻辑计算,根据分支名是否包含 release 决定使用 servicing 池(-Svc)还是常规池
变量 分支含 release(servicing/COGS) 普通分支(main 等)
DncEngPublicBuildPool NetCore-Svc-Public NetCore-Public
DncEngInternalBuildPool NetCore1ESPool-Svc-Internal NetCore1ESPool-Internal

设计动机(模板头注释中写明):产品分支一旦正式发布,其构建产物即被视为 COGS(Cost of Goods Sold),需要与 main 分支在队列与计费上隔离;同时池名称可能随工程团队资源调整而变更,通过变量模板引用 $(DncEngInternalBuildPool) / $(DncEngPublicBuildPool) 可避免所有分支同步改名。

eng/common/templates-official/variables/pool-providers.yml 内部只定义 1ES 池变量(DncEngInternalBuildPool,值同样随 release 分支在 NetCore1ESPool-Svc-InternalNetCore1ESPool-Internal 之间切换),并使用 1ES 风格的 image: windows.vs2026.amd64 池描述。

对应地,eng/common/core-templates/variables/pool-providers.yml 就是标准样板:声明 is1ESPipeline 参数,为 true 时 redirect 到 templates-official 版本,否则 redirect 到 templates 版本——完整的"redirect"角色演示。

八、仓库内的真实落地:aspnetcore 如何同时使用两套模板

原文档以 azure-pipelines.yml / azure-pipelines-pr.yml 作为示例(那是 dotnet/arcade 仓库的命名习惯)。在当前 aspnetcore 仓库中,对应流水线集中在 .azure/pipelines,真实做法比示例更进一步:在一个 job 模板内部用条件同时路由到两套 shim

.azure/pipelines/jobs/default-build.yml 顶部用一行参数说明文档(如 jobNameagentOsbuildArgsartifactsisAzDOTestingJob 等)约定了调用契约,然后在 jobs: 节点按项目类型分流:

  • 非 internal 项目(public/PR)→ 引用 templates/job/job.yml@self
    jobs:
    - ${{ if ne(variables['System.TeamProject'], 'internal') }}:
      - template: /eng/common/templates/job/job.yml@self
        parameters:
          name: ${{ coalesce(parameters.jobName, parameters.agentOs) }}
          ...
          pool:
            ${{ if eq(parameters.agentOs, 'Linux') }}:
              name: $(DncEngPublicBuildPool)
              demands: ImageOverride -equals Build.Ubuntu.2204.Amd64.Open
            ...
    
  • internal 项目 → 引用 templates-official/job/job.yml@self
    - ${{ if eq(variables['System.TeamProject'], 'internal') }}:
      - template: /eng/common/templates-official/job/job.yml@self
        parameters:
          ...
          ${{ if and(eq(parameters.agentOs, 'Windows'), eq(parameters.codeSign, 'true')) }}:
            enableMicrobuild: true
            enablePublishBuildAssets: true
            enablePublishUsingPipelines: ${{ variables._PublishUsingPipelines }}
    

从这段代码可以看到两套 shim 的参数契约是对齐的(同一套 name/displayName/dependsOn/timeoutInMinutes/pool/...),差别集中在 1ES 专属能力上:internal 分支额外开启了 Microbuild(签名)、enablePublishBuildAssets,并使用 $(DncEngInternalBuildPool) 一类的 1ES 池。同样的路由模式也体现在 eng/common/core-templates/job/job.yml 中——它依据 runAsPublicSystem.TeamProjectBuild.Reason 决定是否注入 Microbuild 安装/清理步骤。

顶层入口 .azure/pipelines/ci-public.yml 则演示了模板的组合调用:它定义 trigger(main 与 release/* 分支)与 pr 触发规则后,先通过 - template: /eng/common/templates/variables/pool-providers.yml 引入上述池变量,再以 - template: jobs/default-build.yml 的方式为 Windows/macOS/Linux 的 x64、arm、arm64、musl 等各平台矩阵式地编排 build 与 test job(每个 job 声明自己的 artifacts、容器与 buildArgs),并额外用 - template: /eng/common/core-templates/job/helix-job-monitor.yml 挂接 Helix 测试监控任务。值得注意:像 helix-job-monitor.yml 这类与"是否 1ES 无关"的 job 会直接被仓库引用,而常规 build job 则必须经由带 is1ESPipeline 的 shim——两相对照,可以更直观地体会第五节中"job/jobs 粒度做 shim、特殊场景直达"的取舍。

九、易错点与自查清单

综合文档与源码,落地这类流水线时建议按以下清单自查:

  1. internal 生产级流水线是否全部指向 templates-official 凡涉及签名(Microbuild)、发布资产、SBOM 的 job,都应走 1ES shim;
  2. 是否直接引用了 eng/common/core-templates 除非有充分理由(如 helix-job-monitor.yml 这类与流派无关的 job),否则会触发 core-templates 中的 Illegal entry point 守卫报错;
  3. job/jobs/steps/post-build/ 下是否显式传入流派选择参数? 在文档描述的语义下是 templateIs1ESManaged,而当前仓库代码中的真实参数名为 is1ESPipeline,由 shim 强制设置、普通调用方无需(也不应)手工传入;
  4. 使用 1ES Multiple Outputs 时,发布目标是否统一落在 $(Build.ArtifactStagingDirectory) 下、并通过 templateContext.outputs 声明? 只有 templates-official 路径才支持该特性;
  5. 非生产产物(日志、BuildConfiguration)是否标注 isProduction: false 便于 1ES 正确区分生产与非生产 artifact,避免不必要的扫描;
  6. 池与分支是否匹配? 通过 pool-providers.yml 的变量引用池名,而不是在流水线里硬编码,避免 release/* 分支的 COGS 计费/队列隔离失效。

只要遵循"生产走 official、其余走 public、job/jobs 级经 shim、steps/variables 级按需保留逻辑或 redirect、产物统一归拢到 ArtifactStagingDirectory"这套骨架,就能在 ASP.NET Core 这类多平台大型仓库的 CI 体系中稳定地组织出既满足 1ES 治理要求、又不过度拖慢流水线的构建拓扑。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
898
5.82 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
921
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.8 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
596
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
519
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
391