Azure DevOps生产级CI/CD实战:从YAML踩坑到灰度发布
1. 这不是“又一个DevOps教程”而是一份我亲手踩过坑、改过37次YAML、在生产环境扛住日均200次部署的实战手记你点开这篇大概率正被三件事缠住新项目要上线但CI/CD流程还卡在本地构建团队里有人还在手动打包、复制DLL、改配置文件再发给测试或者你刚被老板问“为什么上个版本回滚花了47分钟”。别急——这不是PPT式概念罗列也不是照着微软文档抄一遍的“官方翻译”。这是我过去三年在金融、SaaS、IoT三个不同技术栈项目里把Azure DevOps从“能用”推到“敢用生产环境”的完整复盘。核心就一条所有配置、所有YAML写法、所有权限设置都必须经得起凌晨三点告警电话的检验。关键词里没写“CI/CD”“YAML”“Pipeline”但它们就是呼吸一样的存在。如果你是刚接触Azure DevOps的开发者这篇会告诉你哪些按钮点了等于白点、哪些默认值一改就崩如果你是带团队的技术负责人我会拆解清楚“为什么我们放弃TFVC转向Git分支策略”“为什么Artifacts的feed权限必须按角色隔离”“为什么Test Plans里一个测试用例的标签比代码注释还重要”。它不教你怎么点开网页而是告诉你点下去之后系统底层在做什么、可能卡在哪、怎么一眼看出问题根因。没有“理论上可行”只有“我昨天刚在客户现场实测过”。2. 整体设计逻辑为什么是这五块拼图而不是其他组合2.1 不是功能罗列而是交付流水线的天然分段很多人第一次看Azure DevOps的五个服务下意识当成“五个独立工具”Boards管事、Repos管码、Pipelines管跑、Test Plans管测、Artifacts管包。错。它们本质是软件交付价值流的五个不可分割的物理切片就像汽车装配线上的冲压、焊装、涂装、总装、质检——少一个环节车就出不了厂。我见过太多团队把Pipelines当“自动执行脚本工具”结果构建失败后开发要翻Boards找对应User Story再进Repos查提交记录最后去Test Plans翻测试报告全程手动串联。真正的设计起点必须是以一次可发布的代码变更Code Change为原子单位反向倒推每个环节的输入输出契约。输入契约一次变更必须关联一个Boards中的Work Item比如Bug #1234或Feature #5678这是所有后续动作的“业务上下文锚点”。没有这个关联自动化就失去意义——你无法回答“这次部署修复了哪个客户投诉”。输出契约Pipelines的最终产物必须是一个可验证、可追溯、可回滚的制品Artifact它存放在Artifacts中其元数据版本号、构建ID、关联的Work Item ID、触发分支必须完整嵌入制品本身而非仅存在Pipeline日志里。我曾为某银行项目定制过一个PowerShell脚本在dotnet publish后自动注入buildInfo.json到NuGet包内内容包含{ buildId: 20240515.12, workItemId: 1234, branch: release/v2.3 }测试团队拿到包第一件事就是解压读这个文件确认“这确实是我要测的修复版本”。提示Azure DevOps默认不强制Work Item关联。必须在Project Settings → Policies → Pull Request中启用“Require linked work items”并勾选“Allow only one work item per pull request”——看似多此一举不。它堵死了“这个PR顺手修了三个Bug但只关了一个”的混乱源头。2.2 为什么是Git而非TFVC一次血泪教训Azure Repos支持Git和TFVC但我的建议非常明确除非你维护一个15年前的.NET Framework 2.0遗留系统且团队拒绝学习Git否则一律用Git。原因不在技术优劣而在协作熵增定律。TFVC是集中式版本控制所有操作依赖中央服务器。我在一个制造业MES项目吃过亏开发A在上午10点签出Config.xml修改了数据库连接字符串开发B在10:05也签出同一文件改了日志级别两人下午3点同时签入。TFVC的合并机制是“谁后签入谁覆盖”B的修改直接抹掉A的数据库配置系统下午4点上线后连不上Oracle。Git的分布式特性强制每个开发者本地有完整历史合并时必须显式解决冲突错误在本地即暴露。更重要的是Pipelines的触发机制深度绑定Git语义pr触发器监听Pull Request创建/更新push触发器监听分支推送tag触发器监听打标行为。TFVC的“Shelveset”或“Check-in”事件无法提供同等粒度的上下文比如无法区分“这是紧急Hotfix还是常规迭代”。注意迁移到Git不是简单git init。必须同步迁移分支策略。我们采用“GitFlow 2.0”变体main仅接收已验证的Release Tag、develop集成分支所有Feature PR合入目标、feature/*每人一个命名含Jira ID如feature/JIRA-789-login-ui、hotfix/*仅限线上紧急修复。关键在于main分支开启Branch Policy要求至少2人Code Review Build Validation运行Pipelines Status CheckTest Plans中该版本通过率≥95%才能合入。这套规则在Azure Repos的Branches页面右键分支→Branch policies中配置不是口头约定。2.3 Pipelines为何是心脏因为它定义了“可重复性”的数学边界Azure Pipelines常被简化为“写YAML跑命令”但它的真正价值在于将软件构建过程从“艺术”转化为“可计算的确定性函数”。一个合格的Pipeline输入是代码仓库的某个Commit ID输出是带唯一标识的制品中间每一步编译、单元测试、静态扫描、打包都必须满足幂等性相同输入无论何时何地执行输出完全一致。这意味着不能依赖本地全局安装的Node.js版本node -v可能返回16.x或18.x而必须在YAML中声明nodeVersion: 18.x由Microsoft-hosted agent自动安装指定版本。隔离性每次运行都在干净虚拟机Windows/Linux/macOS上启动无残留状态。所以不要幻想“在Step A里npm installStep B里直接用node_modules”——agent重启后目录全空。所有依赖必须在每个需要它的Step里重新安装或通过Cache2任务缓存node_modules需正确配置key和restoreKeys。可观测性每个Step的执行时间、内存占用、CPU峰值、网络IO必须可量化。我们给所有Pipeline添加了PublishTestResults2和PublishCodeCoverageResults2并将覆盖率阈值设为硬性门禁coverageThresholdForBlockBuild: 75低于75%的构建直接失败。这不是为了好看而是当某天发现覆盖率从82%跌到78%你能立刻定位是哪个PR引入了未覆盖的if分支。3. 核心细节解析从零搭建一条生产级CI/CD流水线3.1 Azure Boards让需求不再“消失在邮件里”Boards的价值常被低估以为只是“在线看板”。实际它是整个交付链路的唯一真相源Source of Truth。我坚持一个原则任何需求、缺陷、技术债必须以Work Item形式存在于Boards否则视为不存在。具体落地有三个硬性动作Work Item Type定制化默认的User Story、Bug、Task不够用。我们新增了TechDebt类型字段包含ImpactScore1-5分影响范围、EffortEstimate人天、Owner必须指定到人。当ImpactScore ≥ 4且EffortEstimate ≤ 2时自动加入Sprint Backlog——避免技术债越积越多。配置路径Project Settings → Work → Project configuration → Add work item type。Backlog层级强制对齐禁止平铺式管理。采用三级结构Epic季度目标如“提升API响应速度”→ Feature月度交付如“实现Redis缓存层”→ User Story双周迭代如“用户登录接口增加缓存”。每个Feature必须关联至少一个Epic每个User Story必须关联一个Feature。这样当老板问“Q3重点是什么”直接导出Epic报表当测试问“这个Story属于哪个大功能”点开就能看到父级Feature。Sprint Planning的自动化校验每次Sprint Planning会议前运行一个自定义QueryBoards → Queries → New querySELECT [System.Id], [System.Title], [System.State], [Microsoft.VSTS.Scheduling.Effort] FROM WorkItems WHERE [System.WorkItemType] User Story AND [System.State] To Do AND [System.IterationPath] currentIteration AND [Microsoft.VSTS.Scheduling.Effort] 0 ORDER BY [Microsoft.VSTS.Scheduling.Effort] DESC这个Query强制显示所有未估点、未分配到当前Sprint的Story。会议开始前5分钟我把结果投屏——没人能说“这个Story我忘了估点”。流程的严肃性始于数据的可见性。3.2 Azure Repos代码仓库不是“文件夹”而是“契约签署地”Repos的配置直接影响Pipeline的稳定性和安全性。以下是经过生产验证的关键设置分支保护策略Branch Policies对main和develop分支必须启用Build validation指定一个专用Pipeline如ci-main-validation.yml该Pipeline只做最精简的检查dotnet restoredotnet builddotnet test --no-build跳过重复编译。耗时控制在3分钟内。任何PR合入前此Pipeline必须成功。Required reviewers至少2名非作者的Reviewer且其中1名必须是[Project Valid Users]组成员避免小圈子审核。Check for linked work items强制关联Work Item且状态不能是Done防止“先关Story再合代码”的倒置操作。Limit merge types仅允许Squash merge。理由Rebase merge会重写提交历史导致Pipeline的Commit ID与原始PR不一致溯源困难No fast-forward merge产生大量无意义merge commit污染历史。文件路径权限Permissions敏感文件如appsettings.Production.json、azure-pipelines.yml必须设置路径级权限。右键文件 → Security → 添加组[Project Administrators]权限设为Deny拒绝Contribute。普通开发者只能读修改必须通过PR 高权限审批。我们曾因azure-pipelines.yml被误删导致所有Pipeline中断恢复花了42分钟——现在这种事故归零。Git Hooks替代方案虽然Repos不支持服务端Git Hooks但可用Pipeline前置步骤模拟。在azure-pipelines.yml开头添加trigger: branches: include: - main - develop pr: branches: include: - main - develop jobs: - job: PreCheck pool: vmImage: ubuntu-latest steps: - checkout: self fetchDepth: 1 - script: | # 检查是否包含危险模式 if git grep -q console\.log -- **/*.js; then echo ##vso[task.logissue typeerror]Found console.log in JS files. Remove before merge. exit 1 fi if git grep -q password.* -- **/*.cs; then echo ##vso[task.logissue typeerror]Found password assignment in C# files. Use secrets instead. exit 1 fi displayName: Pre-merge static checks这段脚本在每次PR触发时扫描JS文件中的console.log和C#文件中的明文密码赋值发现即报错阻断。它比客户端Hook更可靠因为无法绕过。3.3 Azure PipelinesYAML不是配置而是“可执行的架构文档”Pipelines的YAML文件是我团队内部称为“活文档”的核心。它必须清晰回答这个应用如何被构建、测试、发布以下是我们生产环境的标准结构以.NET Core Web API为例# azure-pipelines.yml trigger: branches: include: - main - develop - feature/* tags: include: - v* pr: branches: include: - main - develop variables: # 全局变量避免硬编码 solution: **/*.sln buildPlatform: Any CPU buildConfiguration: Release # 从Repository变量组注入不写死 - group: prod-secrets # 包含SQL_CONN_STRING, APPINSIGHTS_KEY等 stages: - stage: Build displayName: Build and Test jobs: - job: BuildJob pool: vmImage: windows-latest steps: - task: DotNetCoreCLI2 displayName: Restore dependencies inputs: command: restore projects: $(solution) - task: DotNetCoreCLI2 displayName: Build solution inputs: command: build projects: $(solution) arguments: --configuration $(buildConfiguration) --no-restore - task: DotNetCoreCLI2 displayName: Run unit tests inputs: command: test projects: **/*Tests.csproj arguments: --configuration $(buildConfiguration) --no-build --collect:XPlat Code Coverage publishTestResults: true - task: PublishCodeCoverageResults2 displayName: Publish code coverage inputs: codeCoverageTool: Cobertura summaryFileLocation: $(System.DefaultWorkingDirectory)/**/coverage.cobertura.xml - stage: Package displayName: Package for Release dependsOn: Build condition: succeeded() jobs: - job: PackageJob pool: vmImage: windows-latest steps: - checkout: self fetchDepth: 1 - task: DotNetCoreCLI2 displayName: Publish web app inputs: command: publish projects: **/MyApi.csproj arguments: --configuration $(buildConfiguration) --output $(Build.ArtifactStagingDirectory)/publish --no-restore - task: PublishBuildArtifacts1 displayName: Publish artifacts inputs: PathtoPublish: $(Build.ArtifactStagingDirectory)/publish ArtifactName: drop publishLocation: Container - stage: Deploy displayName: Deploy to Environments dependsOn: Package condition: succeeded() jobs: - deployment: DeployToDev displayName: Deploy to Development pool: vmImage: windows-latest environment: Development strategy: runOnce: deploy: steps: - download: current artifact: drop - task: AzureRmWebAppDeployment4 displayName: Deploy to Azure Web App (Dev) inputs: ConnectionType: AzureRM azureSubscription: my-dev-subscription appType: webApp WebAppName: myapi-dev packageForLinux: $(Pipeline.Workspace)/drop/**/*.zip - deployment: DeployToProd displayName: Deploy to Production pool: vmImage: windows-latest environment: Production # 关键生产部署需人工审批 strategy: runOnce: deploy: steps: - download: current artifact: drop - task: AzureRmWebAppDeployment4 displayName: Deploy to Azure Web App (Prod) inputs: ConnectionType: AzureRM azureSubscription: my-prod-subscription appType: webApp WebAppName: myapi-prod packageForLinux: $(Pipeline.Workspace)/drop/**/*.zip关键设计解析Stage化而非单JobBuild、Package、Deploy分离便于独立重试。若部署失败无需重新构建。Environment绑定environment: Production不仅是个名字它关联Azure DevOps中的Environment资源可配置Approval Checks需指定人员审批、Checks如运行Smoke Test Pipeline、Audit Logs谁在何时部署了什么。Condition控制流condition: succeeded()确保前一Stage成功才执行避免“构建失败还继续部署”。Artifact传递download: current明确指定从当前Pipeline下载制品而非跨Pipeline引用保证版本一致性。实操心得YAML语法错误是新手最大拦路虎。推荐两个技巧1在VS Code安装“Azure Pipelines”插件实时语法高亮和智能提示2所有复杂Pipeline先在本地用az pipelines run命令测试需安装Azure CLI和az devops扩展比在Web界面反复提交快10倍。3.4 Azure Test Plans测试不是“找Bug”而是“建立质量信心”Test Plans常被当作“手工测试记录本”但它的核心价值是将测试活动与代码变更、构建结果、业务需求进行三维绑定。我们强制执行以下实践测试用例Test Case与User Story强关联每个User Story创建时必须关联至少3个Test Case正向、边界、异常。在Boards中打开Story → “Test cases”选项卡 → “Link to test case”。这样当Story状态变为In ProgressTest Plans自动将关联的Test Case状态设为Ready当Story完成Test Case进入Design阶段。测试不再是开发后的补救而是需求定义时的同步产出。测试套件Test Suite按环境组织创建Suite-Dev、Suite-Staging、Suite-Prod。Suite-Prod只包含已通过UAT的Test Case且每个Case必须标记Priority: P0最高优先级。每次生产部署前必须运行Suite-Prod通过率100%才允许上线。我们用PowerShell脚本自动触发$token [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(:$($env:SYSTEM_ACCESSTOKEN))) $header {Authorization Basic $token} $body { plan {id 12345} # Prod Test Plan ID suite {id 67890} # Suite-Prod ID configuration {id 111} # Prod Environment Config ID tester {displayName CI-System} } | ConvertTo-Json Invoke-RestMethod -Uri https://dev.azure.com/{org}/{project}/_apis/test/runs?api-version6.0 -Method Post -Headers $header -Body $body -ContentType application/json此脚本嵌入Deploy Stage末尾部署完成后自动执行冒烟测试。探索性测试Exploratory Testing不等于“随便点点”我们要求测试工程师使用Test Plans的Exploratory Testing Session功能每次Session必须1选择一个明确的User Story作为目标2录制屏幕和操作日志3Session结束时必须生成至少1个Bug Work Item即使没发现Bug也要记录“验证通过”。这迫使探索性测试聚焦于业务价值而非随机点击。3.5 Azure Artifacts包管理不是“上传下载”而是“供应链治理”Artifacts是交付链路的“物流中心”。我们将其定位为团队级依赖供应链的治理节点而非简单的二进制存储。关键实践Feed分层策略创建三个Feedpublic-nuget只读上游源为nuget.org供所有项目拉取公共包。internal-shared团队共享存放公司通用库如Company.Logging、Company.Data发布权限仅限[Library Owners]组。project-specific每个重大项目独享存放该项目独有的NuGet包如MyApi.Contracts发布权限仅限该项目组。这样public-nuget的包版本升级不会影响内部库internal-shared的变更需走严格PR流程project-specific可快速迭代。包版本语义化SemVer强制所有内部包必须遵循MAJOR.MINOR.PATCH规则。在csproj中PropertyGroup VersionPrefix2.3.0/VersionPrefix VersionSuffix$(BUILD_BUILDNUMBER)/VersionSuffix !-- 构建号追加 -- /PropertyGroupPipeline中dotnet pack后包版本为2.3.0.20240515.12。VersionPrefix由开发手动维护VersionSuffix由Pipeline注入确保唯一性。没有版本号的包等同于没有许可证的药品——不准入库。依赖扫描Dependency Scanning集成在Build Stage末尾添加- task: DotNetCoreCLI2 displayName: Scan for vulnerable packages inputs: command: custom custom: tool arguments: install --global dotnet-format - script: | dotnet tool install -g Microsoft.CST.Scanner dotnet cscanner scan --source-path $(Build.SourcesDirectory) --output-format json --output-file $(Build.ArtifactStagingDirectory)/scan-results.json displayName: Run dependency scanner - task: PublishBuildArtifacts1 displayName: Publish scan results inputs: PathtoPublish: $(Build.ArtifactStagingDirectory)/scan-results.json ArtifactName: security-scan publishLocation: Container扫描结果自动上传安全团队可每日查看。发现高危漏洞如Log4j立即阻断Pipeline。4. 实操过程从创建第一个Pipeline到生产环境灰度发布4.1 第一步创建项目与基础配置15分钟登录dev.azure.com → New project → 命名如MyBankingApp→ Visibility设为Private→ Version control选Git→ Work item process选Agile非Basic因Basic缺少Epic/Feature层级。进入Project Settings → Repositories →MyBankingApp→ Policies → 启用Require a minimum number of reviewers2人、Check for linked work items。进入Project Settings → Pipelines → Settings → 关闭Disable creation of classic build pipelines避免新人误用旧版。创建第一个YAML文件在Repos根目录新建.pipelines/ci-base.yml粘贴前述BuildStage模板保存并Push。注意首次Push后Pipelines会自动检测YAML并创建Pipeline。若未触发在Pipelines页面点“New pipeline” → “Use the classic editor” → “Existing Azure Pipelines YAML file”手动选择路径。4.2 第二步连接代码与构建30分钟在本地克隆仓库git clone https://dev.azure.com/{org}/{project}/_git/{repo}。创建src/MyBankingApp.sln和src/MyBankingApp/MyBankingApp.csproj标准.NET Core Web API模板。修改azure-pipelines.yml将solution变量改为src/MyBankingApp.slnprojects参数改为src/MyBankingApp/MyBankingApp.csproj。提交并Pushgit add . git commit -m init: add basic pipeline git push origin main。观察Pipeline运行进入Pipelines → 点击刚创建的Pipeline → 查看日志。常见失败点dotnet restore失败检查global.json中SDK版本是否与agent支持的匹配windows-latest支持.NET 6/7/8不支持5.0。dotnet test失败确认**/*Tests.csproj能匹配到测试项目如项目名为MyBankingApp.Tests.csproj则需改为**/MyBankingApp.Tests.csproj。超时默认超时10分钟若dotnet build耗时过长添加timeoutInMinutes: 20到Job级别。4.3 第三步接入测试与制品管理45分钟在Repos中创建tests/MyBankingApp.Tests/MyBankingApp.Tests.csproj添加xUnit和coverlet.collectorNuGet包。在azure-pipelines.yml的BuildStage中修改DotNetCoreCLI2的test步骤- task: DotNetCoreCLI2 displayName: Run unit tests with coverage inputs: command: test projects: tests/MyBankingApp.Tests/MyBankingApp.Tests.csproj arguments: --configuration $(buildConfiguration) --no-build --collect:XPlat Code Coverage --results-directory $(Build.ArtifactStagingDirectory)/testresults publishTestResults: true创建Artifacts FeedProject Settings → Artifacts → New feed → Namemybanking-shared→ VisibilityOrganization→ 保存。修改PackageStage添加dotnet pack和push步骤- task: DotNetCoreCLI2 displayName: Pack library inputs: command: pack packagesToPack: src/MyBankingApp/MyBankingApp.csproj versioningScheme: byPrereleaseNumber majorMinorPatch: 2.3.0 - task: DotNetCoreCLI2 displayName: Push to Artifacts inputs: command: push packagesToPush: $(Build.ArtifactStagingDirectory)/**/*.nupkg nuGetFeedType: internal feedPublish: mybanking-shared4.4 第四步生产环境部署与灰度发布60分钟在Azure Portal创建两个Resource Grouprg-mybanking-dev和rg-mybanking-prod。在DevOps中创建EnvironmentsPipelines → Environments → New environment → NameDevelopment→ Resource typeAzure Virtual Machines或Kubernetes namespace此处以Web App为例→ Connect to Azure → 选择rg-mybanking-dev。重复创建Production环境。为Production环境添加Approval点击Production→ Approvals and checks → → Approval → 选择2名运维负责人。修改DeployStage添加ProductionDeployment- deployment: DeployToProd displayName: Deploy to Production (Gray Scale) pool: vmImage: windows-latest environment: Production strategy: # 灰度策略先部署5%流量 runOnce: preDeploy: steps: - script: echo Switching production traffic to 5% for new version deploy: steps: - download: current artifact: drop - task: AzureRmWebAppDeployment4 displayName: Deploy to Azure Web App (Prod) inputs: ConnectionType: AzureRM azureSubscription: my-prod-subscription appType: webApp WebAppName: mybanking-prod packageForLinux: $(Pipeline.Workspace)/drop/**/*.zip postDeploy: steps: - script: echo Monitoring metrics for 10 minutes... - script: | # 调用监控API检查错误率0.1% if [ $(curl -s https://api.monitoring.com/metrics?appmybankingmetricerror_ratewindow10m | jq .value) -gt 1 ]; then echo ##vso[task.logissue typeerror]Error rate too high. Rolling back. exit 1 fi提交YAML触发Pipeline。观察Production环境的Approval卡片等待审批通过后灰度部署自动执行。5. 常见问题与排查技巧实录那些让我凌晨三点爬起来的日志5.1 Pipeline卡在“Waiting for an agent” —— 不是没机器是没权限现象Pipeline长时间显示“Waiting for an agent”队列无报错。根因Agent Pool权限未授予。新创建的Pipeline默认使用Azure Pipelines托管池但若项目启用了私有Agent如公司内网VM需手动授权。排查进入Project Settings → Pipelines → Agent pools → 选择你的私有Pool → “Security”选项卡。检查[Project Name] Build Service组是否有Use permissions允许使用。若为Not set点击...→Edit→ 勾选Use。避坑永远不要给Project Collection Build Service组授予权限它权限过大。只给项目级Build Service。5.2 “The nuget command failed with exit code 1” —— 不是包不存在是源没配对现象dotnet restore失败日志显示Unable to load the service index for source https://api.nuget.org/v3/index.json。根因Pipeline运行在Microsoft-hosted agent上但nuget.config文件中配置了公司内网私有源且未配置凭据。解决在Repos根目录创建nuget.config?xml version1.0 encodingutf-8? configuration packageSources add keynuget.org valuehttps://api.nuget.org/v3/index.json protocolVersion3 / add keymycompany valuehttps://pkgs.dev.azure.com/{org}/_packaging/{feed}/nuget/v3/index.json / /packageSources packageSourceCredentials mycompany add keyUsername valuedevops / add keyClearTextPassword value$(NUGET_PAT) / !-- 从变量组注入 -- /mycompany /packageSourceCredentials /configuration在Pipeline变量组中创建NUGET_PAT值为Personal Access TokenScopePackaging (read)。注意ClearTextPassword虽名含“ClearText”但Token本身是密文且变量组已加密存储。5.3 Test Plans中“Test case not found” —— 不是链接失效是ID变了现象在Test Plans中点击“Run”按钮提示“Test case not found”。根因Work Item被移动如从feature迭代移至main迭代导致Test Case的Iteration Path变更但Test Plans未同步。解决在Test Plans → Test suites → 右键对应Suite → “Manage test cases”。点击“Refresh”按钮循环箭头图标强制从Boards重新拉取最新Work Item状态。预防在Boards中移动Work Item时勾选“Move associated test cases”移动关联的测试用例。5.4 Artifacts包上传成功但下游项目找不到 —— 不是网络问题是Feed权限现象dotnet push成功但在另一个项目中dotnet restore报401 Unauthorized。根因目标项目未被授权访问该Feed。解决进入Artifacts → 选择Feed → “Permissions”选项卡。点击→ “Add users/groups” → 输入目标项目名称如OtherProject→ 选择角色Reader只读或Contributor可读写。关键这里添加的是“项目”Project不是“用户”。项目名格式为{org}/{project}。5.5 Pipeline部署后应用500错误 —— 不是代码问题是配置缺失现象Azure Web App部署成功但访问返回HTTP 500。排查清单检查应用日志Portal → Web App → Monitoring → App Service logs → 开启Application Logging (Filesystem)→ 保存 → 重启App → 再访问 → 下载日志查看eventlog.xml。检查连接字符串Portal → Web App → Settings → Configuration → Connection strings → 确认SQLCONNSTR_defaultConnection等名称与代码中Configuration.GetConnectionString(defaultConnection)完全一致大小写敏感。检查启动命令Portal → Web App → Settings → Configuration → General settings → Startup command → 对于.NET Core应为空由web.config或dotnet publish自动生成若手动填写必须为dotnet MyBankingApp.dll。检查托管身份若代码中使用new DefaultAzureCredential()获取Key Vault密钥需在Portal → Web App → Settings → Identity → 状态设为On并在Key Vault中为该Web App的托管身份授予权限。实操心得我建立了一个“5分钟故障树”遇到500错误按顺序执行1看App Service日志2看Deployment Center的部署日志