Skip to main content

CI Release Gate

Run Zelsis on every pull request and block the merge when a proven critical issue is introduced.

1. Create an API key

In the dashboard open Settings → API keys and create a key. Add it to your repository as the secret ZELSIS_API_KEY (GitHub: Settings → Secrets and variables → Actions). Private repositories cannot be gated without it.

2. Add the workflow

Save this as .github/workflows/zelsis.yml. On pull requests it sends the head and base commits, so the gate only fails on findings the pull request introduces.

name: Zelsis Pre-Flight Release Gate

on:
  pull_request:
    branches: [main, master, develop]
  push:
    branches: [main]

permissions:
  contents: read

jobs:
  zelsis-gate:
    name: Zelsis Release Gate (your app)
    runs-on: ubuntu-latest
    steps:
      - name: Run Zelsis gate check
        env:
          # Create a key in Zelsis: Settings -> API keys. Required for private repositories.
          ZELSIS_API_KEY: ${{ secrets.ZELSIS_API_KEY }}
          # Lets Zelsis read a private repository for this run only.
          GH_READ_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          REF=""
          BASE=""
          if [ "${{ github.event_name }}" = "pull_request" ]; then
            REF="${{ github.event.pull_request.head.sha }}"
            BASE="${{ github.event.pull_request.base.sha }}"
          fi

          FAIL_OPEN="${{ vars.ZELSIS_FAIL_OPEN || 'false' }}"

          RESPONSE=$(curl -s -w "\n%{http_code}" -X POST "https://zelsis.com/api/v1/gate-check" \
            -H "Content-Type: application/json" \
            -H "x-api-key: $ZELSIS_API_KEY" \
            -d '{"repoUrl": "${{ github.server_url }}/${{ github.repository }}", "githubToken": "'$GH_READ_TOKEN'", "ref": "'$REF'", "base": "'$BASE'"}')

          HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
          BODY=$(echo "$RESPONSE" | sed '$d')

          if [ "$HTTP_CODE" -ge 400 ] && [ "$HTTP_CODE" -lt 500 ]; then
            echo "::error::Zelsis rejected the request (HTTP $HTTP_CODE): $(echo "$BODY" | jq -r '[.error // "see log"] + [.details[]? | .field + ": " + .message] | join("; ")'). Check the ZELSIS_API_KEY secret."
            exit 1
          fi

          if [ "$HTTP_CODE" -ge 500 ] || [ -z "$BODY" ] || ! echo "$BODY" | jq -e . >/dev/null 2>&1; then
            echo "Zelsis API unreachable or returned an invalid response."
            if [ "$FAIL_OPEN" = "true" ]; then
              echo "Fail-open is enabled: passing the gate."
              echo '{"gateStatus": "PASSED", "readinessScore": 100}' > zelsis-report.json
              exit 0
            fi
            exit 1
          fi

          echo "$BODY" > zelsis-report.json
          STATUS=$(jq -r '.gateStatus // "ERROR"' zelsis-report.json)
          SCORE=$(jq -r '.readinessScore // 0' zelsis-report.json)
          echo "Gate: $STATUS, score: $SCORE/100" >> $GITHUB_STEP_SUMMARY
          # Shows each finding on its line in the pull request diff
          jq -r '.annotations[]? // empty' zelsis-report.json
          if [ "$STATUS" = "FAILED" ]; then echo "::error::Zelsis gate FAILED (score $SCORE/100)"; exit 1; fi

      - name: Upload gate report
        if: always()
        uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
        with:
          name: zelsis-release-report
          path: zelsis-report.json

Findings on the pull request diff

On a pull request the job only reports findings in files the PR changes, and each one appears on its line in the "Files changed" tab: critical and high as errors, medium as warnings (up to 10 of each, the most severe first). No GitHub App is needed; the job prints GitHub's standard annotation commands.

When the Zelsis API is unreachable

  • Fail-closed (default): the job fails, so unverified code is not merged.
  • Fail-open: set the repository variable ZELSIS_FAIL_OPEN to true and the job passes when the API cannot be reached.

A rejected request (for example a missing or revoked API key) always fails with an error message; it is never reported as a vulnerability. Which severities block the release is configured separately with failStrategy in .zelsisrc.json.