Skip to content

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:

  1. GitLab webhook fires on push/merge to configured repos
  2. Webhook target is an Azure Function (consumption plan, free tier) in the HCS subscription
  3. The Azure Function translates the GitLab event into an ADO pipeline trigger via the ADO REST API
  4. 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:

  1. Secret lives in kv-hcs-vault-01
  2. A KV-linked ADO Variable Group maps the secret to a pipeline variable name
  3. The pipeline references the Variable Group
  4. 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.

# GitHub Actions
on:
  push:
    branches: [main]
  workflow_dispatch:       # always include this
# ADO
trigger:
  branches:
    include:
      - main
# Manual runs are available via the ADO UI on any pipeline — no extra config needed

Agent pools

  • Use ubuntu-latest for GitHub Actions unless the task explicitly requires Windows
  • Use windows-latest for 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.