Webhook icon
Schedule icon
WorkingDirectory icon
Commands icon
Docker icon
Script icon
If icon
Create icon
SlackIncomingWebhook icon
Log icon

Audit Azure DevOps Environment Approvals Against Their SLA

Age Azure DevOps environment approvals against an SLA, escalate stalled or auto-bypassed approvals to GitHub and Slack, and publish gateable outputs.

Categories
Infrastructure

How it works

  1. on_approval_snapshot (io.kestra.plugin.core.trigger.Webhook, keyed with the AZURE_DEVOPS_APPROVALS_WEBHOOK_KEY secret) receives the pending environment approval snapshot from your Azure DevOps audit job. daily_sla_sweep (io.kestra.plugin.core.trigger.Schedule, cron 0 6 * * *) is the safety net and ships disabled: true until an operator turns it on.
  2. approval_workspace (io.kestra.plugin.core.flow.WorkingDirectory) opens one shared directory so the staged fixture and the scorer read the same file, a scratch directory outside a WorkingDirectory is purged between tasks and would break that hand-off.
  3. write_approvals_fixture (io.kestra.plugin.scripts.shell.Commands on the io.kestra.plugin.scripts.runner.docker.Docker runner, image alpine:3.20) writes approvals.json with a heredoc. The fixture carries a production approval pending 36.5 hours, a young staging approval, an auto-bypassed production approval and a fresh database approval, the shape a stalled release night produces. entryPoint: [] clears the image entrypoint so the shell script runs instead of the default command.
  4. score_approvals (io.kestra.plugin.scripts.python.Script on python:3.12-slim) prefers the posted webhook body when present and falls back to the staged file. Every pending approval older than approval_sla_hours counts as overdue, every auto_bypassed approval counts as a bypass, and the verdict reads ESCALATE when either counter is nonzero, CLEAR when the window is clean and UNKNOWN when the snapshot could not be parsed. The counters, the oldest overdue age and the worst approval are printed as a single ::{"outputs": ...}:: line.
  5. escalation_gate (io.kestra.plugin.core.flow.If) branches on verdict == 'ESCALATE'. Escalated runs open file_overdue_issue (io.kestra.plugin.github.issues.Create) with the counters and the worst approval, then alert_approval_stall (io.kestra.plugin.slack.notifications.SlackIncomingWebhook) posts the summary; clean runs record log_no_breach so the execution history doubles as SLA evidence.
  6. The flow level errors handler (io.kestra.plugin.slack.notifications.SlackIncomingWebhook) reports any failure with the execution id, because a dead auditor looks exactly like a compliant project.

What you get

  • A gateable verdict that separates a stalled approval window from a compliant one, with UNKNOWN for a broken snapshot publisher.
  • A GitHub issue that names the project, the overdue count, the oldest age, the worst approval and every auto-bypass, labeled for the release backlog.
  • A Slack message with the same counters so nobody opens the execution just to learn why a release stalled.
  • verdict, overdue_count, oldest_age_hours, bypassed_count, pending_count and worst_approval as typed outputs for weekly SLA trend reports.
  • A disabled daily schedule as a second net once the webhook path is trusted.

Who it's for

  • Release managers who own environment approval SLAs and need evidence when a check stalls past its window.
  • Azure DevOps administrators reviewing the timeout policies that auto-bypass stale checks.
  • Platform teams wiring approval health into a release train gate instead of a spreadsheet.

Why orchestrate this with Kestra

Azure DevOps records who approved what but does not chase an approval that has sat pending for a day and a half. Kestra gives the audit a keyed webhook for on demand scoring, a disabled schedule as the safety net, a fixture driven scorer whose aging rules live in version control, an If that isolates the escalated window, and routing into a GitHub issue, Slack and a failing gate from one declarative file. A cron job with a REST script gives you none of the history, the branching or the gateable outputs.

Prerequisites

  • A worker with Docker access, the fixture runs in alpine:3.20 and the scorer in python:3.12-slim.
  • An Azure DevOps audit job or status page that can POST the pending approval snapshot to the webhook, or rely on the staged fixture for trial runs.
  • A GitHub token that can open issues in github_repository (classic PAT scope repo, or fine-grained Issues write).
  • A Slack incoming webhook for the escalation alert and for failure alerts.

Secrets

  • AZURE_DEVOPS_APPROVALS_WEBHOOK_KEY: key segment guarding the snapshot webhook URL, anyone holding it can trigger an audit run.
  • GITHUB_TOKEN: token used by file_overdue_issue to open the escalation issue.
  • SLACK_WEBHOOK_URL: Slack incoming webhook used by alert_approval_stall and by the errors block.

Quick start

  1. Add the three secrets to your Kestra namespace and point github_repository at a repository where audit issues are welcome.
  2. Run the flow once with the default inputs. The staged fixture holds one approval pending 36.5 hours and one auto-bypass, so expect verdict: ESCALATE, overdue_count 1, bypassed_count 1, an issue in github_repository and a red execution.
  3. Re-run with approval_sla_hours at 48 to watch the overdue counter drop to zero while the auto-bypass still escalates the window.
  4. Point your audit job at /api/v1/{tenant}/executions/webhook/{namespace}/azure-devops-environment-approval-audit/{AZURE_DEVOPS_APPROVALS_WEBHOOK_KEY} and POST a snapshot with every approval young and unbypassed to see log_no_breach record the all-clear.
  5. Enable daily_sla_sweep once the webhook path is trusted so the staged snapshot is rescored every morning.

How to extend

  • Split the SLA per environment by grading Production against approval_sla_hours and Staging against a shorter window inside score_approvals.
  • Persist each scored window with io.kestra.plugin.jdbc.postgresql.Queries to chart approval aging per project over the quarter.
  • Fan out across many Azure DevOps projects with io.kestra.plugin.core.flow.ForEachItem over a subflow that audits one snapshot each.
  • Swap the GitHub issue for a ServiceNow record with the HTTP task when the release backlog lives in ITSM instead.

Links

See How

New to Kestra?

Use blueprints to kickstart your first workflows.