Skip to content
engineer

Gate a release in CI

Was this page helpful?

This page reflects our reading of Cyber Resilience Act, Annex I Part I(2)(a) as of . It is not legal advice. Review process.

After you push an SBOM, the release gate tells your pipeline whether that version ships with a known exploitable vulnerability. Poll it, fail the job on blocked, and carry on on pass.

What the gate checks, and what it does not

It blocks a version when a component in its active SBOM matches an advisory that is in CISA's KEV catalogue or the EUVD exploited list, or is a tier A match, and nothing clears it: no VEX statement, no fixed version recorded in your own advisory, no triage decision of not applicable.

That is a signal, not a conformity verdict. A pass does not mean the product meets Annex I, and a blocked is not a reportable event. Recording that you are releasing anyway, with a reason, is done in the app on the product page. The API only reads; the acknowledgement then shows as lastDecision (result, time, and whether a person or a key made it), with neither the reason nor a name.

Before you start

  • The Growth or Partner plan, for the API key. The gate on the product page is on every plan; only reading it from a pipeline needs a key.
  • The owner or an admin role to create the key. Recording a release decision in the app needs a member, admin or owner role (products.write).
  • An API key scoped products:read, and a second one scoped sboms:write (or one key with both) to push the SBOM first. See Push SBOMs from CI.
  • The product's ID, from the product page or GET /api/v1/products.

The answers to handle

FieldMeaningWhat the job does
result: "pending"Matching has not yet run on the newest SBOM.Wait and ask again. Never treat as a pass.
result: "blocked"A known exploitable vulnerability is not cleared.Fail.
result: "pass"None found.Continue.
versionSupported: falseThe version is no longer supported, and matching skips it, so the result may be stale.Warn.
latestUploadFailed: trueThe newest SBOM did not parse; the result is about the one before it.Warn.

URL-encode the version

A release label can contain /, + or :. Pass it as ?version= with every character encoded, as in the examples below. The path form (/api/v1/products/{id}/versions/{version}/gate) also works, but a proxy may refuse an encoded slash in a path, so the query form is the one to use.

GitHub Actions

Add this after the upload step. It strips a leading v from the tag (v1.4.0 is checked as 1.4.0), so the label must match what the upload step sent; change ${VERSION#v} in both places if yours differs.

- name: Release gate
  env:
    DECLARA_API_KEY: ${{ secrets.DECLARA_API_KEY }}
    PRODUCT_ID: your-product-id
    VERSION: ${{ github.ref_name }}
  run: |
    version="$(jq -rn --arg v "${VERSION#v}" '$v|@uri')"
    for attempt in $(seq 1 20); do
      body="$(curl -sS --fail-with-body \
        "https://your-declara-host/api/v1/products/$PRODUCT_ID/gate?version=$version" \
        -H "Authorization: Bearer $DECLARA_API_KEY")"
      result="$(jq -r .result <<<"$body")"
      [ "$result" != "pending" ] && break
      [ "$attempt" -lt 20 ] || break
      echo "Matching has not caught up yet (attempt $attempt of 20)."
      sleep 30
    done
    [ "$(jq -r .versionSupported <<<"$body")" = "false" ] &&
      echo "::warning::This version is not supported, so the gate may be stale."
    [ "$(jq -r .latestUploadFailed <<<"$body")" = "true" ] &&
      echo "::warning::The newest SBOM failed to parse; this is the SBOM before it."
    case "$result" in
      pass) echo "Release gate: pass." ;;
      blocked)
        jq -r '.blockers[] | "::error::\(.advisoryId) in \(.purl) (\(.signal))"' <<<"$body"
        exit 1 ;;
      *) echo "::error::No answer after 10 minutes (result: $result)."; exit 1 ;;
    esac

Any other CI: a shell script

#!/bin/sh
# Usage: DECLARA_HOST=... DECLARA_API_KEY=... PRODUCT_ID=... ./gate.sh 1.4.0
set -eu
version="$(jq -rn --arg v "$1" '$v|@uri')"
url="$DECLARA_HOST/api/v1/products/$PRODUCT_ID/gate?version=$version"

attempt=1
while :; do
  body="$(curl -sS --fail-with-body "$url" -H "Authorization: Bearer $DECLARA_API_KEY")"
  result="$(printf '%s' "$body" | jq -r .result)"
  [ "$result" = "pending" ] || break
  [ "$attempt" -lt 20 ] || { echo "Still pending after 20 attempts." >&2; exit 1; }
  attempt=$((attempt + 1))
  sleep 30
done

[ "$(printf '%s' "$body" | jq -r .versionSupported)" = "false" ] &&
  echo "warning: version not supported; the gate may be stale." >&2
[ "$(printf '%s' "$body" | jq -r .latestUploadFailed)" = "true" ] &&
  echo "warning: newest SBOM failed to parse; this is the one before it." >&2

case "$result" in
  pass) echo "Release gate: pass." ;;
  blocked)
    printf '%s' "$body" | jq -r '.blockers[] | "blocked: \(.advisoryId) in \(.purl) (\(.signal))"' >&2
    exit 1 ;;
  *) echo "Unexpected result: $result" >&2; exit 1 ;;
esac

The loop waits up to about ten minutes (20 attempts, 30 seconds apart). If your matching usually takes longer, raise the count.

A 404 means the product or version is not in this workspace; push the SBOM first, since a version is created by its first upload. A 400 means the version label is malformed.

See the API reference for the full response.