2026-02-20
GitHub Actions + Intended: CI/CD Pipeline Governance
Developer Relations · Developer Experience
GitHub Actions + Intended: CI/CD Pipeline Governance
CI/CD pipelines are one of the most powerful and most dangerous automated systems in your organization. They have credentials to your production infrastructure. They run with elevated permissions. They execute based on triggers that developers control. A misconfigured pipeline can deploy broken code, leak secrets, or modify infrastructure in ways that take hours to undo.
AI agents are increasingly involved in CI/CD: auto-merging dependabot PRs, triggering deployments based on monitoring signals, and creating hotfix branches. These agents operate within the pipeline's permission model, which typically has broad access to production systems.
Intended's GitHub Action adds authority checks to your pipeline. Before a deployment runs, before infrastructure changes are applied, before a release is published, the pipeline checks with Intended and proceeds only if the action is authorized.
The GitHub Action
The `meritt-security/authority-check` action submits an intent to the Intended authority engine and blocks the workflow step until a decision is returned:
- name: Check deployment authority
uses: meritt-security/authority-check@v1
id: auth
with:
api-key: ${{ secrets.Intended_API_KEY }}
org-id: ${{ secrets.Intended_ORG_ID }}
agent-id: "github-actions-deploy"
intent: "infrastructure.deployment.apply"
params: |
{
"environment": "${{ github.event.inputs.environment }}",
"service": "${{ github.event.inputs.service }}",
"ref": "${{ github.sha }}",
"actor": "${{ github.actor }}",
"deployment_method": "canary"
}The action outputs the decision, risk scores, and token ID:
- name: Deploy (only if authorized)
if: steps.auth.outputs.decision == 'allow'
run: |
echo "Deploying with authority token: ${{ steps.auth.outputs.token-id }}"
./deploy.sh ${{ github.event.inputs.service }} ${{ github.event.inputs.environment }}If the decision is "deny," the workflow step is skipped and the action outputs the reason:
- name: Report denial
if: steps.auth.outputs.decision == 'deny'
run: |
echo "::error::Deployment denied: ${{ steps.auth.outputs.reason }}"
exit 1If the decision is "escalate," the action can either fail the workflow or wait for approval:
- name: Check deployment authority
uses: meritt-security/authority-check@v1
id: auth
with:
api-key: ${{ secrets.Intended_API_KEY }}
org-id: ${{ secrets.Intended_ORG_ID }}
agent-id: "github-actions-deploy"
intent: "infrastructure.deployment.apply"
wait-for-escalation: true
escalation-timeout: "30m"
params: |
{
"environment": "production",
"service": "payment-api"
}With `wait-for-escalation: true`, the action holds the workflow while the escalation is reviewed. When a human approves in Slack or the Intended console, the workflow continues. When a human denies or the timeout expires, the workflow fails.
Complete Workflow Example
Here is a complete deployment workflow with Intended governance:
name: Deploy Service
on:
workflow_dispatch:
inputs:
service:
description: "Service to deploy"
required: true
type: choice
options: [api, web, worker, scheduler]
environment:
description: "Target environment"
required: true
type: choice
options: [staging, production]
permissions:
contents: read
deployments: write
jobs:
authorize:
runs-on: ubuntu-latest
outputs:
decision: ${{ steps.auth.outputs.decision }}
token-id: ${{ steps.auth.outputs.token-id }}
steps:
- name: Check deployment authority
uses: meritt-security/authority-check@v1
id: auth
with:
api-key: ${{ secrets.Intended_API_KEY }}
org-id: ${{ secrets.Intended_ORG_ID }}
agent-id: "github-actions-deploy"
intent: "infrastructure.deployment.apply"
wait-for-escalation: true
escalation-timeout: "30m"
params: |
{
"environment": "${{ inputs.environment }}",
"service": "${{ inputs.service }}",
"ref": "${{ github.sha }}",
"actor": "${{ github.actor }}",
"trigger": "workflow_dispatch",
"repository": "${{ github.repository }}"
}
- name: Authority decision
run: |
echo "Decision: ${{ steps.auth.outputs.decision }}"
echo "Risk score: ${{ steps.auth.outputs.risk-score }}"
echo "Token: ${{ steps.auth.outputs.token-id }}"
deploy:
needs: authorize
if: needs.authorize.outputs.decision == 'allow'
runs-on: ubuntu-latest
environment: ${{ inputs.environment }}
steps:
- uses: actions/checkout@v4
- name: Deploy with authority token
env:
Intended_TOKEN: ${{ needs.authorize.outputs.token-id }}
run: |
echo "Deploying ${{ inputs.service }} to ${{ inputs.environment }}"
echo "Authority token: $Intended_TOKEN"
./scripts/deploy.sh \
--service ${{ inputs.service }} \
--environment ${{ inputs.environment }} \
--token $Intended_TOKEN
denied:
needs: authorize
if: needs.authorize.outputs.decision != 'allow'
runs-on: ubuntu-latest
steps:
- name: Deployment not authorized
run: |
echo "::error::Deployment was not authorized."
echo "Decision: ${{ needs.authorize.outputs.decision }}"
exit 1Governing Dependabot Auto-Merge
AI-driven auto-merge of dependency updates is a common CI/CD pattern. Dependabot opens a PR, tests pass, and an automation merges it. Intended can govern this:
name: Auto-merge Dependabot
on:
pull_request:
types: [opened, synchronize]
jobs:
authorize-merge:
if: github.actor == 'dependabot[bot]'
runs-on: ubuntu-latest
steps:
- name: Check merge authority
uses: meritt-security/authority-check@v1
id: auth
with:
api-key: ${{ secrets.Intended_API_KEY }}
org-id: ${{ secrets.Intended_ORG_ID }}
agent-id: "dependabot-auto-merge"
intent: "sdlc.pull-request.merge"
params: |
{
"repository": "${{ github.repository }}",
"pr_number": "${{ github.event.pull_request.number }}",
"base_branch": "${{ github.event.pull_request.base.ref }}",
"dependency_type": "${{ github.event.pull_request.title }}",
"is_security_update": false
}
- name: Auto-merge if authorized
if: steps.auth.outputs.decision == 'allow'
run: gh pr merge ${{ github.event.pull_request.number }} --auto --squash
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}The policy can distinguish between patch updates (auto-approve), minor updates (auto-approve for non-production dependencies, escalate for production), and major updates (always escalate).
Infrastructure as Code Governance
For Terraform or Pulumi workflows, Intended can evaluate the plan before apply:
- name: Terraform Plan
id: plan
run: terraform plan -out=tfplan -json > plan.json
- name: Check infrastructure authority
uses: meritt-security/authority-check@v1
id: infra-auth
with:
api-key: ${{ secrets.Intended_API_KEY }}
org-id: ${{ secrets.Intended_ORG_ID }}
agent-id: "terraform-pipeline"
intent: "infrastructure.terraform.apply"
params-file: plan.json
- name: Terraform Apply
if: steps.infra-auth.outputs.decision == 'allow'
run: terraform apply tfplanThe `params-file` option sends the entire Terraform plan to Intended for analysis. The authority engine parses the plan, identifies the resources being created, modified, or destroyed, and evaluates each change against infrastructure policies.
Audit Integration
Every authority check in your GitHub Actions workflow is recorded in Intended's audit chain. The audit entry includes the GitHub repository, workflow name, run ID, actor, and the full intent parameters. You can correlate Intended audit entries with GitHub Actions run logs for complete traceability.
The Intended console provides a view filtered by agent ID, so you can see all authority decisions made by your CI/CD pipeline in one place. Decisions are linked to the GitHub Actions run that triggered them, giving your security team full visibility into what your pipeline is doing and why it was authorized to do it.
CI/CD pipelines are powerful automation. Intended ensures that power is exercised under authority, with every deployment, merge, and infrastructure change evaluated, recorded, and provable.