To validate Azure Policy compliance inside a GitHub Actions pipeline before or after deploying infrastructure, you can integrate static IaC linting (pre-deployment) alongside the Azure Policy Compliance State API (post-deployment validation).
This is a post-deployment compliance gate: the pipeline deploys, asks Azure Policy to evaluate, then passes or fails based on the result.
Points to make in an interview:
- It complements Deny, it doesn’t replace it. Deny blocks a bad deployment at ARM. This gate catches what Deny can’t: Audit and AuditIfNotExists findings, and DeployIfNotExists conditions that remain unmet.
- Evaluation is not instant. Compliance data is eventually consistent, so the pipeline should trigger an on-demand scan (az policy state trigger-scan) and then poll with a timeout. Without a timeout, a stuck evaluation hangs the pipeline. Without a trigger, you may read stale results and wrongly pass.
- Scope the check to what was deployed. Query compliance for the target resource group or the specific resources, not the whole subscription, or unrelated legacy non-compliance will fail every run.
- The report is part of the control. Output the policy name, resource ID, and reason so developers can fix it without a ticket. Failing with no explanation drives people to ask for exemptions.
- Decide the failure policy up front. Hard-fail on high-severity or security baselines, warn on low-severity items, and route exceptions through time-boxed policy exemptions, not pipeline overrides.
Order matters for safety. If the deployment itself creates the non-compliant resource, the gate fails after it exists. For sensitive controls, pair this with what-if and IaC scanning (Checkov) before the deploy step, so you fail early
Architecture Flow

GitHub Actions Workflow Example
This pipeline performs three actions:
- Logs in via OIDC (OpenID Connect) using short-lived credentials.
- Triggers an on-demand Azure Policy evaluation scan on the target Resource Group.
- Polls the Azure Policy Insights API to verify that zero resources are in a
NonCompliantstate. If non-compliant resources exist, it fails the workflow and prints a structured summary report.
YAML
name: Azure Policy Compliance Checkon: push: branches: [ "main" ] pull_request: branches: [ "main" ]permissions: id-token: write contents: readjobs: validate-policy-compliance: name: Validate Azure Policy Compliance runs-on: ubuntu-latest env: RESOURCE_GROUP: 'rg-devsecops-prod' steps: - name: Checkout Repository uses: actions/checkout@v4 - name: Azure Login via OIDC uses: azure/login@v2 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - name: Trigger On-Demand Policy Evaluation Scan shell: pwsh run: | Write-Host "Triggering Azure Policy scan for Resource Group: ${{ env.RESOURCE_GROUP }}" $job = Start-AzPolicyComplianceScan -ResourceGroupName "${{ env.RESOURCE_GROUP }}" -AsJob $job | Wait-Job Write-Host "Policy evaluation scan complete." - name: Check Compliance State & Fail Pipeline if Non-Compliant shell: pwsh run: | $rgName = "${{ env.RESOURCE_GROUP }}" # Query non-compliant policy states for the specific resource group $nonCompliantResults = Get-AzPolicyState -ResourceGroupName $rgName -Filter "IsCompliant eq false" if ($nonCompliantResults) { Write-Error "CRITICAL: Azure Policy compliance failure detected in Resource Group '$rgName'." Write-Host "`n=== NON-COMPLIANT RESOURCES REPORT ===" -ForegroundColor Red foreach ($item in $nonCompliantResults) { [PSCustomObject]@{ ResourceId = $item.ResourceId PolicyDefinition = $item.PolicyDefinitionName PolicyAssignment = $item.PolicyAssignmentName ComplianceState = $item.ComplianceState } | Format-List } exit 1 } else { Write-Host "SUCCESS: All resources in '$rgName' comply with assigned Azure Policies!" -ForegroundColor Green }
Pre-Deployment Alternative: Shift-Left Static Bicep/ARM Policy Check
For a true “Shift-Left” experience that flags violations before sending deployment requests to Azure ARM, you can run the az deployment group validate command inside your pipeline.
If an assigned policy has a Deny effect, ARM validation catches and blocks it prior to deployment:
- name: Pre-Flight Azure Policy Gate (ARM Validation)
uses: azure/cli@v2
with:
azcliversion: latest
inlineScript: |
az deployment group validate \
--resource-group "rg-devsecops-prod" \
--template-file "./infra/main.bicep" \
--parameters "./infra/main.bicepparam"
Key Best Practices
- Use Short-Lived Scans: On-demand scans using
Start-AzPolicyComplianceScantypically complete in 1–3 minutes when scoped to a single Resource Group, making them suitable for PR gating. - Granular Exclusions: If a specific step in your pipeline purposefully triggers a policy violation (e.g., temporary staging environments), create a temporary Policy Exemption with an expiration date rather than disabling the policy rule globally.