- Structure pipelines as stages > jobs > steps; use displayName on every stage, job, and step for readable logs
- Use kebab-case for file names (build-and-deploy.yml) and camelCase or PascalCase for variable/parameter names
- Set timeoutInMinutes on every job to prevent hung agents from consuming parallel job slots indefinitely
- Store all pipeline YAML in a dedicated /pipelines or /.azuredevops directory — keep repo root clean
- Define shared variables at the top-level variables: block; scope secrets to the narrowest stage or job
- Structure: trigger → variables → stages → jobs → steps; every element gets a descriptive displayName
- File naming: kebab-case YAML files (ci-build.yml, deploy-production.yml) in a /pipelines directory
- Set timeoutInMinutes on jobs (10 for lint, 30 for build, 60 for E2E) to prevent stale agents consuming parallel slots
- Variable scoping: pipeline-level for shared constants, stage-level for environment-specific, job-level for secrets
- Use parameters with types (string, boolean, object, stepList) for pipeline inputs — safer than variables
- Organize multi-stage pipelines: Build → Test → Deploy Dev → Deploy Staging → Deploy Prod with explicit dependsOn
- Use condition: expressions to control stage/job/step execution: succeeded(), failed(), eq(variables['Build.Reason'], 'PullRequest')
- Prefer pool: vmImage for hosted agents; specify exact image (ubuntu-22.04) not just ubuntu-latest for reproducibility
- Use YAML comments to document non-obvious trigger filters, conditions, and template parameter contracts
- Keep individual YAML files under 200 lines — extract stages/jobs into templates when files grow large
- Define pr: and trigger: blocks explicitly; avoid relying on implicit trigger behavior which varies by repo type