---
title: Enforce build quality gates for prod deployments
description: "🎯 Enforce production deployments using build coverage and vulnerability metrics via approval policies."
toc_max_heading_level: 2
sidebar_label: Enforce prod gates with build metadata
doc_id: f200e429-1356-49f6-bf09-a12861770de4
tutorial_type: tutorial
tutorial_category: governance
tutorial_time: 25 min
tutorial_featured: false
tutorial_cover: /img/tutorials/covers/enforce-build-quality-policies-production.svg
keywords:
  - catalog
  - approvals
  - policies
  - builds
  - coverage
  - vulnerabilities
  - terraform
  - github actions
tags:
  - governance
  - catalog
---

import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

# Enforce build quality gates for prod deployments

> 🎯 **Goal:** Gate production deployments using build catalog metrics. Allow deployments only when **Coverage ≥ 80%** and **No critical vulnerabilities**. Otherwise, require **manual approval**.

## Introduction

In many organizations, production releases must pass quality gates. This guide shows how to add **build metadata** for *coverage* and *vulnerabilities* and use it in policies that control **deployment to production**. 


## What you’ll set up

By the end of this guide, you'll have:

- A **reusable technology template** developers can start from (so new apps inherit the same CI and checks).
- **Build catalogs** for *coverage* and *security* on the `build` entity.
- A **CI workflow** that publishes coverage and vulnerability results to each build.
- **Approval policies** and a *deployment approval action* that gate production on:
  - Coverage ≥ 80%
  - No critical vulnerabilities

---

## Prerequisites

You’ll need:
- Terraform/OpenTofu
- Access to your GitHub organization (to clone/push repos used by nullplatform)
- A valid [**nullplatform API key**](/docs/authorization/api-keys) with roles: **Admin, Developer, Ops, SecOps** at **Organization** level
- A **Kubernetes** cluster with the **nullplatform agent** running
- The [**nullplatform CLI**](/docs/cli/) installed: `curl https://cli.nullplatform.com/install.sh | sh`
- Environment variable for the CLI:
  ```bash
  export NULLPLATFORM_API_KEY=<your_api_key_here>
  ```

## 1. Create a catalog to add build metadata

Policies read from entity metadata, so declaring catalog schemas ensures the fields exist and are validated
consistently across applications.

Create two catalog specs on the `build` entity: **coverage** and **security**.

   > Replace:
   > - `<api-key>` with your nullplatform API key.
   > - `<organization=XXXXXX:account=XXXXXX:namespace=XXXXX>` with your NRN.

<Tabs
defaultValue="catalog-iac"
values={[
  { label: 'IaC', value: 'catalog-iac' },
  { label: 'cURL', value: 'catalog-curl' },
  ]}>
<TabItem value="catalog-iac">

```hcl
terraform {
  required_providers {
    nullplatform = {
      source = "nullplatform/nullplatform"
    }
  }
}

provider "nullplatform" {
  api_key = "<api-key>"
}

resource "nullplatform_metadata_specification" "coverage_metadata" {
  name        = "Coverage Schema"
  description = "Schema for code coverage configuration"
  nrn         = "<organization=XXXXX:account=XXXXX:namespace=XXXXX>"
  entity      = "build"
  metadata    = "coverage"

  schema = jsonencode({
    type = "object"
    properties = {
      code = {
        type = "object"
        properties = {
          coverage = {
            type        = "number"
            minimum     = 0
            maximum     = 100
            description = "Percentage of code coverage"
          }
          lines = {
            type        = "integer"
            minimum     = 0
            description = "Total amount of code lines"
          }
        }
        required             = ["coverage"]
        additionalProperties = false
      }
    }
    required             = ["code"]
    additionalProperties = false
  })
}

resource "nullplatform_metadata_specification" "security_metadata" {
  name        = "Security Schema"
  description = "Schema for security configuration"
  nrn         = "<organization=XXXXXX:account=XXXXXX:namespace=XXXXX>"
  entity      = "build"
  metadata    = "security"

  schema = jsonencode({
    type        = "object"
    title       = "Vulnerability Checks Schema"
    description = "Schema for vulnerability check configuration"
    properties = {
      security = {
        type = "object"
        properties = {
          vulnerabilities = {
            type = "object"
            properties = {
              high = {
                type        = "integer"
                minimum     = 0
                description = "Number of high severity vulnerabilities"
              }
              critical = {
                type        = "integer"
                minimum     = 0
                description = "Number of critical severity vulnerabilities"
              }
            }
            required             = ["high", "critical"]
            additionalProperties = false
          }
        }
        required             = ["vulnerabilities"]
        additionalProperties = false
      }
    }
    required             = ["security"]
    additionalProperties = false
  })
}

```
</TabItem>
<TabItem value="catalog-curl">

If you prefer to use the API, send the following POST requests to our [Catalog API](/docs/api/catalog-specification-create).

Coverage schema:

```bash
curl -L 'https://api.nullplatform.com/metadata/metadata_specification' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <token>' \
-d '{
  "entity": "build",
  "metadata": "coverage",
  "nrn": "<organization=XXXXXX:account=XXXXXX:namespace=XXXXX>",
  "description": "Schema for code coverage configuration",
  "name": "Coverage Schema",
  "schema": {
    "type": "object",
    "properties": {
      "code": {
        "type": "object",
        "properties": {
          "coverage": {
            "type": "number",
            "minimum": 0,
            "maximum": 100,
            "description": "Percentage of code coverage"
          },
          "lines": {
            "type": "integer",
            "minimum": 0,
            "description": "Total amount of code lines"
          }
        },
        "required": ["coverage"],
        "additionalProperties": false
      }
    },
    "required": ["code"],
    "additionalProperties": false
  }
}
```
Security schema:

```bash
curl -X POST -L 'https://api.nullplatform.com/metadata/metadata_specification' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <token>' \
-d '{
  "entity": "build",
  "metadata": "security",
  "nrn": "<organization=XXXXXX:account=XXXXXX:namespace=XXXXX>",
  "description": "Schema for security configuration",
  "name": "Security Schema",
  "schema": {
    "type": "object",
    "title": "Vulnerability Checks Schema",
    "description": "Schema for vulnerability check configuration",
    "properties": {
      "security": {
        "type": "object",
        "properties": {
          "vulnerabilities": {
            "type": "object",
            "properties": {
              "high": {
                "type": "integer",
                "minimum": 0,
                "description": "Number of high severity vulnerabilities"
              },
              "critical": {
                "type": "integer",
                "minimum": 0,
                "description": "Number of critical severity vulnerabilities"
              }
            },
            "required": ["high", "critical"],
            "additionalProperties": false
          }
        },
        "required": ["vulnerabilities"],
        "additionalProperties": false
      }
    },
    "required": ["security"],
    "additionalProperties": false
  }
}
```
</TabItem>
</Tabs>

#### ✅ Checkpoint

After applying, run a build to confirm the new fields appear on the build: **coverage** and **security**. You should see
something like this:

<img alt="build-catalog-example.png" src="/img/tutorials/build-catalog-example.png" width="80%" className="helper-image img-separator" />


## 2. Create a namespace and associate a base template

#### Quick setup

1. Create a namespace, e.g. **Technology Templates**.  
2. Create a new application using the standard template **Any Technology**.

#### Extend the base "Any Technology" template to report *coverage* and *security* properties

1. Set the new repository as a template in GitHub settings.
2. Clone the repo locally.
3. Add two files at root level to simulate scan outputs:

    **`coverage.json`** 
    ```json
    {
      "code": {
        "coverage": 70,
        "lines": 500
      }
    }
    ```

    **`security.json`**
    ```json
    {
      "security": {
        "vulnerabilities": { "high": 7, "critical": 0 }
      }
    }
    ```

4. Replace the CI workflow to publish these metrics to the build during CI:

    **`.github/workflows/ci.yml`**
    ```yaml
    name: ci-nullplatform

    env:
      NULLPLATFORM_API_KEY: ${{ secrets.NULLPLATFORM_API_KEY }}

    on:
      push:
        branches:
          - main

    permissions:
      id-token: write
      contents: read
      packages: read

    jobs:
      build:
        runs-on: ubuntu-latest
        steps:
          - name: Check out code
            uses: actions/checkout@v4

          - name: Install nullplatform CLI
            run: curl https://cli.nullplatform.com/install.sh | sh

          - name: Add CLI to PATH
            run: echo "$HOME/.nullplatform/bin" >> $GITHUB_PATH

          - name: Create build on nullplatform
            run: np build start

          - name: Build Docker image
            run: docker build -t my-app .

          - name: Push Docker image
            run: np asset push --type docker-image --source my-app

          - name: Add Coverage
            run: |
              np metadata create --entity build --data "$(jq -c . coverage.json)"

          - name: Add Security
            run: |
              np metadata create --entity build --data "$(jq -c . security.json)"

          - name: Set the build as successful
            if: success()
            run: np build update --status successful

          - name: Set the build as failed
            if: failure() || cancelled()
            run: np build update --status failed
    ```

5. Push the changes so future applications created from this template inherit the CI behavior.


## 3. Register the technology template in nullplatform

1. Create the template.
     
> Replace:
> - `<api-key>` with your nullplatform API key.
> - `<account_id>` with your account ID.
> - `<github_repository_url>` with your repos URL.

<Tabs
defaultValue="template-iac"
values={[
  { label: 'IaC', value: 'template-iac' },
  { label: 'cURL', value: 'template-curl' },
  ]}>
<TabItem value="template-iac">   

```hcl
terraform {
  required_providers {
    nullplatform = {
      source  = "nullplatform/nullplatform"
    }
  }
}

provider "nullplatform" {
  api_key = "<api-key>"
}

resource "nullplatform_technology_template" "golang_1_17" {
  name    = "Golang 1.17.9"
  url     = "<github_template_url>
  account = <account_id>

  provider_config = {
    repository = "technology-templates-golang"
  }

  components {
    type    = "language"
    id      = "google"
    version = "1.17"
    metadata = jsonencode({
      "version": "1.17.9"
    })
  }

  tags     = ["golang", "backend"]
  metadata = jsonencode({})
  rules    = jsonencode({})
}
```

</TabItem>
<TabItem value="template-curl">

If you prefer to use the API, send the following POST request to our [Template API](/docs/api/technology-template-create).

```bash
curl -X POST -L 'https://api.nullplatform.com/template' \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json' \
  -H 'Authorization: Bearer <token>' \
  -d '{
    "name": "Golang 1.17.9",
    "organization": <org_id>,
    "account": <account_id>,
    "url": "<github_repository_url>",
    "provider": {
      "repository": "technology-templates-golang"
    },
    "components": [
      {
        "type": "language",
        "id": "google",
        "version": "1.17",
        "metadata": {
          "version": "1.17.9"
        }
      }
    ],
    "rules": {},
    "metadata": {},
    "tags": ["golang", "backend"]
  }
```

</TabItem>
</Tabs>

2. In **another namespace**, create a new application using this template.

#### ✅ Result 

The first time you run a build with this template, CI will populate the **coverage** and **security** metadata. 

<img alt="build-metadata-example.png" src="/img/tutorials/build-metadata-example.png" width="80%" className="helper-image img-separator" />

## 4. Create approval action and policies

Now that the builds expose these quality checks, define policies to enforce them on **production** deployments.

Create two approval policies (**Coverage ≥ 80%**, **No critical vulnerabilities**) and attach them to a **deployment** approval action scoped to `environment=production`. When policies pass > **auto-approve**. When they fail > **manual approval** is required.

   > Replace:
   > - `<api-key>` with your nullplatform API key.
   > - `<organization=XXXXXX:account=XXXXXX:namespace=XXXXX>` with your NRN.

<Tabs
defaultValue="policy-iac"
values={[
  { label: 'IaC', value: 'policy-iac' },
  { label: 'cURL', value: 'policy-curl' },
  ]}>
<TabItem value="policy-iac">

```hcl
terraform {
  required_providers {
    nullplatform = {
      source = "nullplatform/nullplatform"
    }
  }
}

provider "nullplatform" {
  api_key = "<api-key>"
}

resource "nullplatform_approval_policy" "coverage" {
  nrn  = "<organization=XXXXXX:account=XXXXXX:namespace=XXXXX>"
  name = "Code Coverage"
  conditions = jsonencode({
    "build.metadata.coverage.coverage" = { "$gte": 80 }
  })
}

resource "nullplatform_approval_policy" "security" {
  nrn  = "<organization=XXXXXX:account=XXXXXX:namespace=XXXXXX>"
  name = "Security"
  conditions = jsonencode({
    "build.metadata.security.critical" = { "$eq": 0 }
  })
}

resource "nullplatform_approval_action" "deployment_create" {
  nrn    = "<organization=XXXXXX:account=XXXXXX:namespace=XXXXXX>"
  entity = "deployment"
  action = "deployment:create"

  dimensions = {
    environment = "production"
  }

  on_policy_success = "approve"
  on_policy_fail    = "manual"

  lifecycle {
    ignore_changes = [policies]
  }
}

resource "nullplatform_approval_action_policy_association" "coverage" {
  approval_action_id = nullplatform_approval_action.deployment_create.id
  approval_policy_id = nullplatform_approval_policy.coverage.id
}

resource "nullplatform_approval_action_policy_association" "security" {
  approval_action_id = nullplatform_approval_action.deployment_create.id
  approval_policy_id = nullplatform_approval_policy.security.id
}
```

</TabItem>
<TabItem value="policy-curl">

If you prefer to use the API, send the following POST requests to our [Approval API](/docs/approvals-api-index).

1. Create the approval action (`deployment:create`)

    ```bash
    curl -X POST -L 'https://api.nullplatform.com/approval/action' \
      -H 'Content-Type: application/json' \
      -H 'Accept: application/json' \
      -H 'Authorization: Bearer <token>' \
      -d '{
        "nrn": "<organization=XXXXXX:account=XXXXXX:namespace=XXXXXX>",
        "entity": "deployment",
        "action": "deployment:create",
        "dimensions": {
          "environment": "production"
        },
        "on_policy_success": "approve",
        "on_policy_fail": "manual"
      }
    ```

2. Create the policies

    Policy: Code Coverage (≥ 80):

    ```bash
    curl -X POST -L 'https://api.nullplatform.com/approval/policy' \
      -H 'Content-Type: application/json' \
      -H 'Accept: application/json' \
      -H 'Authorization: Bearer <token>' \
      -d '{
        "nrn": "<organization=XXXXXX:account=XXXXXX:namespace=XXXXX>",
        "name": "Code Coverage",
        "conditions": {
          "build.metadata.coverage.coverage": { "$gte": 80 }
        }
      }
    ```

    Policy: Security (critical == 0)

    ```bash
    curl -X POST -L 'https://api.nullplatform.com/approval/policy' \
      -H 'Content-Type: application/json' \
      -H 'Accept: application/json' \
      -H 'Authorization: Bearer <token>' \
      -d '{
        "nrn": "<organization=XXXXXX:account=XXXXXX:namespace=XXXXXX>",
        "name": "Security",
        "conditions": {
          "build.metadata.security.critical": { "$eq": 0 }
        }
      }
    ```

3. Associate the policies to the action.

    Use the `id` returned by the action creation for `<approval_action_id>`, and the id from each policy creation for `<coverage_policy_id>` / `<security_policy_id>`.

    Associate Coverage policy:

    ```bash
    curl -X POST -L 'https://api.nullplatform.com/approval/action/<approval_action_id>/policy' \
    -H 'Content-Type: application/json' \
    -H 'Accept: application/json' \
    -H 'Authorization: Bearer <token>' \
    -d '{
      "policy_id": <coverage_policy_id>
    }'
    ```

    Associate Security policy

    ```bash
    curl -X POST -L 'https://api.nullplatform.com/approval/action/<approval_action_id>/policy' \
      -H 'Content-Type: application/json' \
      -H 'Accept: application/json' \
      -H 'Authorization: Bearer <token>' \
      -d '{
        "policy_id": <security_policy_id>
      }'
    ```
</TabItem>
</Tabs>

### Test that it works

1. Create a **release**, a **production scope**, and trigger a **deployment**.  
2. If policies aren’t met, deployment will require **manual approval**. You can [wire approvals to **Slack**](/docs/notifications/channels) (or any supported messenger).

<img alt="security-coverage-prod.png" src="/img/tutorials/security-coverage-prod.png" width="80%" className="helper-image img-separator" />


> 💡 **Tip:** Tweak `coverage.json` / `security.json` to simulate different scenarios.

## Wrap-up 🎉

All done! Now you have:

- Defined **build metadata** for *coverage* and *vulnerabilities*.
- Updated your **CI** to publish those metrics to every build.
- Created **approval policies** and a **deployment approval action** that turn metrics into **production gates**.

This gives you **objective, auditable** production decisions and a **reusable template** teams can adopt quickly.

## What’s next

Head to **Application metadata and PCI approval policy** to add **Owner, SLO, and PCI** fields and require **manual approval** for PCI applications in production. Combined with what you just implemented here, you’ll have a complete, metadata-driven production policy.

👉 [Use application catalog to require PCI approvals in production](./apps-production-approval)
