Automation standard¶
All automation across HCS projects follows a pipeline-first model. If you run it more than once, it belongs in a pipeline.
Pipeline-first principle¶
Ad-hoc scripts run locally are fine for one-off operations. Any operation that:
- Needs to run on a schedule
- Needs to run on a trigger (push, PR, merge)
- Touches Azure resources in a repeatable way
- Produces artifacts that other systems consume
- Needs an audit trail
...belongs in a pipeline. Script it, pipeline it, version it.
ADO Pipelines vs. GitHub Actions¶
Use the right tool for the job:
| Use ADO Pipelines for | Use GitHub Actions for |
|---|---|
| Anything touching Azure (deploy, provision, configure) | Repo-level CI: build, test, lint, publish |
| ADO-integrated workflows (work item updates, test plans) | Publishing packages to NuGet, PyPI, PSGallery |
| Windows-agent tasks that need domain-joined runners | GitHub-native actions (dependabot, code scanning) |
| Pipelines that consume ADO Variable Groups | Open-source repo workflows |
| Anything requiring approval gates | PR validation on public repos |
In mixed scenarios (ADO project with a GitHub mirror), ADO runs the deployment pipeline and GitHub Actions runs CI. They do not overlap.
GitLab integration¶
HCS has GitLab repos that mirror or feed into ADO. The integration pattern:
- GitLab webhook fires on push/merge to configured repos
- Webhook target is an Azure Function (consumption plan, free tier) in the HCS subscription
- The Azure Function translates the GitLab event into an ADO pipeline trigger via the ADO REST API
- ADO pipeline runs with the appropriate inputs
This avoids requiring a GitLab premium plan for ADO CI/CD integration. The Azure Function acts as the translation layer.
Idempotency¶
Every pipeline and every script called from a pipeline must be idempotent. That means:
- Safe to re-run with the same inputs and produce the same outcome
- No side effects that accumulate on re-run (no duplicate resources, no duplicate entries)
- Existence checks before creates: check if the resource exists before attempting to create it
- Upsert patterns over create-or-fail patterns
If a pipeline fails halfway through, re-running it should finish the job — not create a mess that requires manual cleanup.
Secret handling in pipelines¶
The only acceptable pattern for secrets in pipelines is:
- Secret lives in
kv-hcs-vault-01 - A KV-linked ADO Variable Group maps the secret to a pipeline variable name
- The pipeline references the Variable Group
- The pipeline consumes the variable as an environment variable or task input
variables:
- group: platform-prd-secrets
steps:
- script: echo "token is $(hcs-github-org-pat)" # ADO masks this automatically
Never: - Inline secrets in pipeline YAML - Store secret values in pipeline variable definitions (non-KV-linked) - Echo or log secret values - Pass secrets as positional arguments to scripts (they appear in process lists)
Use named parameters or environment variables to pass secrets to scripts:
- task: PowerShell@2
inputs:
script: scripts/Deploy-Something.ps1
env:
GITHUB_TOKEN: $(hcs-github-org-pat)
Pipeline YAML location¶
- ADO pipeline YAML lives under
.ado/in the repo root - GitHub Actions workflow YAML lives under
.github/workflows/ - Pipeline files are committed to the repo and version-controlled — no pipelines defined only in the UI
repo-root/
├── .ado/
│ ├── build.yml
│ └── deploy.yml
├── .github/
│ └── workflows/
│ ├── ci.yml
│ └── publish.yml
Manual trigger requirement¶
Every pipeline must have a manual trigger option in addition to any automatic triggers. In ADO YAML pipelines, workflow_dispatch equivalent is trigger: none + manual run capability, or a scheduled trigger with a manual override. In GitHub Actions, add workflow_dispatch: to every workflow.
# ADO
trigger:
branches:
include:
- main
# Manual runs are available via the ADO UI on any pipeline — no extra config needed
Agent pools¶
- Use
ubuntu-latestfor GitHub Actions unless the task explicitly requires Windows - Use
windows-latestfor ADO pipelines that run PowerShell scripts or need Windows tooling - Use self-hosted agents only when the pipeline needs resources not available on hosted agents (VNet access, specific software, domain join)
Approval gates¶
Any pipeline that deploys to prd must have an approval gate before the deployment stage. Configure in ADO under Environments → Approvals and checks. Self-approval is acceptable for solo projects.
Multi-tool interoperability¶
Complex deployments combine multiple IaC tools. The rule: all tools read from the same config source. A single infrastructure.yml (or equivalent) drives Bicep parameter files, Terraform variable files, PowerShell scripts, and Ansible playbooks — no value is defined in more than one place.
infrastructure.yml ← single source of truth
│
├── Bicep parameters (generated by Export-BicepParams.ps1)
├── Terraform tfvars (generated by Export-TerraformVars.ps1)
├── Ansible inventory (generated by Export-AnsibleInventory.ps1)
└── PowerShell config (read directly at runtime)
Tool selection guidelines:
| Scenario | Preferred tool |
|---|---|
| Azure resource provisioning (VMs, VNets, RGs, KVs) | Bicep |
| Multi-cloud or state-heavy deployments | Terraform |
| OS configuration, role installation, domain join | PowerShell (DSC or Invoke- scripts) |
| Configuration management at scale | Ansible |
| Hybrid: Azure + OS configuration | Bicep (Azure) + PowerShell (OS) — orchestrated by ADO pipeline |
When combining tools, the ADO pipeline is the orchestrator. It runs Bicep first (Azure resources), then PowerShell (OS configuration), using outputs from Bicep (resource IDs, IPs) as inputs to PowerShell.
# ADO pipeline — hybrid deployment
stages:
- stage: Provision
jobs:
- job: BicepDeploy
steps:
- task: AzureCLI@2
inputs:
scriptType: pscore
scriptLocation: inlineScript
inlineScript: |
az deployment group create `
--resource-group rg-hcs-platform-prd-eus-01 `
--template-file infra/main.bicep `
--parameters @infra/parameters.json
- stage: Configure
dependsOn: Provision
jobs:
- job: OSConfig
steps:
- task: PowerShell@2
inputs:
filePath: scripts/Invoke-NodeConfiguration.ps1
arguments: -ConfigPath config/variables.yml
Never split what belongs together. If two tools deploy to the same resource, there is an ownership conflict — resolve it by giving one tool ownership of that resource entirely.