Skip to content

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:

yaml
- 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:

yaml
- 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:

yaml
- name: Report denial
  if: steps.auth.outputs.decision == 'deny'
  run: |
    echo "::error::Deployment denied: ${{ steps.auth.outputs.reason }}"
    exit 1

If the decision is "escalate," the action can either fail the workflow or wait for approval:

yaml
- 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:

yaml
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 1

Governing 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:

yaml
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:

yaml
  - 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 tfplan

The `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.