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_OPENtotrueand 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.