深入解读 ASP.NET Core 仓库中的 Arcade 模板双轨制:templates 与 templates-official 的选择、结构与源码实现
本文围绕仓库内 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-official:1ES(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。
二、基本使用规则:什么时候选哪一套
原文档给出的选择规则非常明确,可用三句话概括:
- 凡是 1ES Pipeline Template 或 1ES Microbuild 模板驱动的运行,一律引用
eng/common/templates-official; - 任何内部生产级(internal production-graded)流水线都应使用
templates-official; - 其余所有运行引用
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 就是空字符串,流水线会直接报错并中止。由此可以推断出两条重要设计意图:
- 仓库级 YAML 禁止直接引用
eng/common/core-templates,必须经过templates或templates-official的 shim; is1ESPipeline的值只能由 shim 入口注入,普通调用方无法也不应该手工传入。
四、Multiple Outputs:用 1ES 特性削减安全扫描注入
4.1 背景与动机
1ES 流水线模板实施了一条策略:每执行一次"发布 artifact"的任务,都会向流水线注入额外的安全扫描。对于大型仓库(如 ASP.NET Core 这种动辄十几二十个发布步骤的流水线),扫描任务会被成倍注入,拖慢整条流水线。
4.2 解决方案
当使用 templates-official/jobs/jobs.yml 时,Arcade 通过以下组合来压缩注入次数:
- 把所有待发布输出统一收集到
$(Build.ArtifactStagingDirectory)(Azure Pipelines 的预定义暂存目录); - 借助 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-templates、templates、templates-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.yml、generate-sbom.yml、get-delegation-sas.yml、vmr-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 内少量文件 |
依据 is1ESPipeline 在 templates 与 templates-official 之间二选一 |
5.3 逻辑的主要存放位置与例外
Arcade 模板的逻辑绝大部分集中在 core-templates,唯一的例外是 artifact 发布:1ES 模板与标准模板在"如何发布产物"上差异过大(1ES 走 templateContext.outputs,标准模板走显式的 publish 步骤),因此 publish-* 相关的发布逻辑在 templates 与 templates-official 中各自保留,并由 core-templates 里的对应文件做 redirect 到双方逻辑文件。
进一步细分:
- 在
templates与templates-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 的职责是:
- 引用
/eng/common/core-templates/job/job.yml并传入is1ESPipeline: false; - 用模板表达式
${{ each parameter in parameters }}把除steps、is1ESPipeline之外的参数原样透传; - 把自己的
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(cancelTimeoutInMinutes、condition、container、dependsOn、pool、strategy、timeoutInMinutes、variables、workspace 等),并处理若干仓库通用关注点:
- 变量注入:统一注入
AllowPtrToDetectTestRunRetryFiles、NuGet 签名重试策略NUGET_EXPERIMENTAL_CHAIN_BUILD_RETRY_POLICY: 3,2000等;同时兼容 name/value、variable group、template、key-value 四种变量写法; - 项目类型感知:当
System.TeamProject == 'public'时跳过 Component Governance 检测与 CodeQL 自动注入(skipComponentGovernanceDetection、Codeql.SkipTaskAutoInjection); - 内部构建增强:非 public 且非 PR 时自动安装/清理 Microbuild、执行
NuGetAuthenticate@1,内部构建还引入DotNet-HelixApi-Access变量组; - 结果与产物收集:按
artifacts.download/publish、enablePublishTestResults(xUnit/TRX)、enablePublishBuildArtifacts、enableBuildRetry等参数,通过CopyFiles@2把artifacts/bin、artifacts/packages、artifacts/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-Internal 与 NetCore1ESPool-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 顶部用一行参数说明文档(如 jobName、agentOs、buildArgs、artifacts、isAzDOTestingJob 等)约定了调用契约,然后在 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 中——它依据 runAsPublic、System.TeamProject 与 Build.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、特殊场景直达"的取舍。
九、易错点与自查清单
综合文档与源码,落地这类流水线时建议按以下清单自查:
- internal 生产级流水线是否全部指向
templates-official? 凡涉及签名(Microbuild)、发布资产、SBOM 的 job,都应走 1ES shim; - 是否直接引用了
eng/common/core-templates? 除非有充分理由(如helix-job-monitor.yml这类与流派无关的 job),否则会触发 core-templates 中的Illegal entry point守卫报错; job/、jobs/、steps/、post-build/下是否显式传入流派选择参数? 在文档描述的语义下是templateIs1ESManaged,而当前仓库代码中的真实参数名为is1ESPipeline,由 shim 强制设置、普通调用方无需(也不应)手工传入;- 使用 1ES Multiple Outputs 时,发布目标是否统一落在
$(Build.ArtifactStagingDirectory)下、并通过templateContext.outputs声明? 只有templates-official路径才支持该特性; - 非生产产物(日志、BuildConfiguration)是否标注
isProduction: false? 便于 1ES 正确区分生产与非生产 artifact,避免不必要的扫描; - 池与分支是否匹配? 通过
pool-providers.yml的变量引用池名,而不是在流水线里硬编码,避免release/*分支的 COGS 计费/队列隔离失效。
只要遵循"生产走 official、其余走 public、job/jobs 级经 shim、steps/variables 级按需保留逻辑或 redirect、产物统一归拢到 ArtifactStagingDirectory"这套骨架,就能在 ASP.NET Core 这类多平台大型仓库的 CI 体系中稳定地组织出既满足 1ES 治理要求、又不过度拖慢流水线的构建拓扑。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00