From d4e7f37809f110ab9216766e53fbad0061393e27 Mon Sep 17 00:00:00 2001 From: Chinmay Pramod Bharti Date: Sat, 7 Nov 2026 04:38:08 +0530 Subject: [PATCH 1/3] Add opt-in full-repository quality gate enforcement --- .github/workflows/tests.yml | 17 ++++++++ .gitignore | 1 + action.yml | 15 ++++++- tests/test_quality_gate.py | 80 +++++++++++++++++++++++++++++++++++++ 4 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/tests.yml create mode 100644 .gitignore create mode 100644 tests/test_quality_gate.py diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..e050b8e --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,17 @@ +name: Action tests + +on: [push, pull_request] + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v6 + with: + python-version: '3.11' + - run: python -m pip install PyYAML==6.0.3 + - run: python -m unittest discover -s tests -v diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c18dd8d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__/ diff --git a/action.yml b/action.yml index 1716ce0..6acdf47 100644 --- a/action.yml +++ b/action.yml @@ -29,6 +29,10 @@ inputs: description: 'Security scanners to run (sast, sca, secrets, antipatterns, iac, all)' required: false default: 'sast,sca' + quality_gate: + description: 'Fail the step when the configured full-repository quality gate fails or results time out' + required: false + default: 'false' runs: using: 'composite' @@ -64,7 +68,15 @@ runs: EXCLUDE_PATHS: ${{ inputs.exclude_paths }} SCAN_TIMEOUT: ${{ inputs.scan_timeout }} SCANNERS: ${{ inputs.scanners }} + QUALITY_GATE: ${{ inputs.quality_gate }} run: | + quality_gate_args=() + case "$QUALITY_GATE" in + true) quality_gate_args+=(--quality-gate) ;; + false) ;; + *) echo "::error::quality_gate must be 'true' or 'false'"; exit 1 ;; + esac + bash start_scan.sh \ -a "$ACCESS_TOKEN" \ -r "$REPO_NAME" \ @@ -74,4 +86,5 @@ runs: -i "$INCLUDE_PATHS" \ -e "$EXCLUDE_PATHS" \ -t "$SCAN_TIMEOUT" \ - --scanners "$SCANNERS" \ No newline at end of file + --scanners "$SCANNERS" \ + "${quality_gate_args[@]}" diff --git a/tests/test_quality_gate.py b/tests/test_quality_gate.py new file mode 100644 index 0000000..138b140 --- /dev/null +++ b/tests/test_quality_gate.py @@ -0,0 +1,80 @@ +"""Run the action's Bash step against a stand-in scanner to verify its contract.""" + +import os +from pathlib import Path +import subprocess +import tempfile +import unittest + +import yaml + + +ACTION = yaml.safe_load((Path(__file__).resolve().parents[1] / "action.yml").read_text()) +SCAN_STEP = next(step for step in ACTION["runs"]["steps"] if step["name"] == "Trigger CodeAnt analysis") + + +class QualityGateInputTests(unittest.TestCase): + def run_action(self, value=None, scanner_exit=0): + if value is None: + value = ACTION["inputs"]["quality_gate"]["default"] + self.assertEqual(SCAN_STEP["env"]["QUALITY_GATE"], "${{ inputs.quality_gate }}") + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + (root / "start_scan.sh").write_text( + '#!/bin/bash\nprintf "%s\\0" "$@" > arguments.bin\nexit "$SCANNER_EXIT"\n' + ) + env = { + **os.environ, + "ACCESS_TOKEN": "test-token", + "REPO_NAME": "example/repository", + "COMMIT_ID": "abc123", + "BRANCH": "main", + "INCLUDE_PATHS": "src/path with spaces/**", + "EXCLUDE_PATHS": "", + "SCAN_TIMEOUT": "600", + "SCANNERS": "sast,sca", + "QUALITY_GATE": str(value), + "SCANNER_EXIT": str(scanner_exit), + } + result = subprocess.run( + ["bash", "--noprofile", "--norc", "-e", "-o", "pipefail", "-c", SCAN_STEP["run"]], + cwd=root, env=env, capture_output=True, text=True, + ) + arguments = root / "arguments.bin" + argv = arguments.read_bytes().decode().split("\0")[:-1] if arguments.exists() else None + return result, argv + + def test_default_preserves_scan_only_behavior(self): + result, argv = self.run_action() + self.assertEqual(result.returncode, 0, result.stderr) + self.assertNotIn("--quality-gate", argv) + self.assertIn("src/path with spaces/**", argv) + self.assertEqual(argv[argv.index("-e") + 1], "") + + def test_true_requests_native_enforcement(self): + result, argv = self.run_action("true") + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(argv.count("--quality-gate"), 1) + + def test_false_does_not_request_enforcement(self): + result, argv = self.run_action("false") + self.assertEqual(result.returncode, 0, result.stderr) + self.assertNotIn("--quality-gate", argv) + + def test_scanner_failure_is_not_swallowed(self): + for value in ("true", "false"): + with self.subTest(quality_gate=value): + result, _ = self.run_action(value, scanner_exit=7) + self.assertEqual(result.returncode, 7) + + def test_invalid_input_does_not_start_a_scan(self): + for value in ("", "tru", "yes", "true; echo unexpected"): + with self.subTest(quality_gate=value): + result, argv = self.run_action(value) + self.assertEqual(result.returncode, 1) + self.assertIsNone(argv) + self.assertIn("quality_gate must be", result.stdout) + + +if __name__ == "__main__": + unittest.main() From 11044d3bba7183ab310ab0c23bb280ec16781384 Mon Sep 17 00:00:00 2001 From: Chinmay Pramod Bharti Date: Sat, 7 Nov 2026 04:40:21 +0530 Subject: [PATCH 2/3] Document native quality gate enforcement and report artifacts --- README.md | 48 ++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 012ad45..de979ae 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ jobs: uses: actions/checkout@v3 - name: Run CodeAnt CI Scan - uses: CodeAnt-AI/codeant-ci-scan-action@v0.0.6 + uses: CodeAnt-AI/codeant-ci-scan-action@v0.0.5 with: access_token: ${{ secrets.ACCESS_TOKEN_GITHUB }} ``` @@ -48,7 +48,7 @@ Customize the scan with additional options: ```yaml - name: Run CodeAnt CI Scan - uses: CodeAnt-AI/codeant-ci-scan-action@v0.0.6 + uses: CodeAnt-AI/codeant-ci-scan-action@v0.0.5 with: access_token: ${{ secrets.ACCESS_TOKEN_GITHUB }} api_base: 'https://api.codeant.ai' @@ -58,6 +58,37 @@ Customize the scan with additional options: scanners: 'sast,sca,secrets' ``` +## Block on a failed quality gate + +Configure **Full Code** conditions in **CodeAnt → Settings → Code Analysis → Quality Gates**, then enable enforcement: + +```yaml +- name: CodeAnt quality gate + uses: CodeAnt-AI/codeant-ci-scan-action@d4e7f37809f110ab9216766e53fbad0061393e27 + with: + access_token: ${{ secrets.CODEANT_TOKEN }} + scanners: sast,sca + quality_gate: 'true' +``` + +This pinned revision supports `quality_gate`; `v0.0.5` does not. The input defaults to `'false'` so existing scan-only workflows keep their behavior. + +`quality_gate: 'true'` forwards `--quality-gate` to the CodeAnt scan script. A failed backend verdict or results timeout fails the step. Keep downstream deployment jobs dependent on this job, and use `if: always()` when uploading reports: + +```yaml +- name: Save reports + if: ${{ always() }} + uses: actions/upload-artifact@v4 + with: + name: codeant-results + path: | + results.json + codeant.sarif + codeant-qg.xml +``` + +Configure conditions before enforcing: no conditions can produce a native PASSED verdict. The native full-code gate supports SAST/SCA/IaC ratings and duplicate-code conditions; secrets are returned as findings but have no full-code gate metric. + ## Inputs | Input | Description | Required | Default | @@ -68,6 +99,7 @@ Customize the scan with additional options: | `exclude_paths` | Comma-separated paths to exclude from scan | No | `''` (none) | | `scan_timeout` | Scan timeout in seconds | No | `300` | | `scanners` | Security scanners to run | No | `sast,sca` | +| `quality_gate` | Fail the step when the configured full-repository gate fails or results time out (`true`/`false`) | No | `false` | ## Scanner Options @@ -86,7 +118,7 @@ The `scanners` parameter allows you to customize which security scanners run dur Run all scanners: ```yaml -- uses: CodeAnt-AI/codeant-ci-scan-action@v0.0.6 +- uses: CodeAnt-AI/codeant-ci-scan-action@v0.0.5 with: access_token: ${{ secrets.ACCESS_TOKEN_GITHUB }} scanners: 'all' @@ -94,7 +126,7 @@ Run all scanners: Run specific scanners: ```yaml -- uses: CodeAnt-AI/codeant-ci-scan-action@v0.0.6 +- uses: CodeAnt-AI/codeant-ci-scan-action@v0.0.5 with: access_token: ${{ secrets.ACCESS_TOKEN_GITHUB }} scanners: 'sast,secrets,iac' @@ -149,7 +181,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - uses: CodeAnt-AI/codeant-ci-scan-action@v0.0.6 + - uses: CodeAnt-AI/codeant-ci-scan-action@v0.0.5 with: access_token: ${{ secrets.ACCESS_TOKEN_GITHUB }} ``` @@ -168,7 +200,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - uses: CodeAnt-AI/codeant-ci-scan-action@v0.0.6 + - uses: CodeAnt-AI/codeant-ci-scan-action@v0.0.5 with: access_token: ${{ secrets.ACCESS_TOKEN_GITHUB }} ``` @@ -176,7 +208,7 @@ jobs: ### Scan Specific Directories ```yaml -- uses: CodeAnt-AI/codeant-ci-scan-action@v0.0.6 +- uses: CodeAnt-AI/codeant-ci-scan-action@v0.0.5 with: access_token: ${{ secrets.ACCESS_TOKEN_GITHUB }} include_paths: 'src/,backend/' @@ -188,7 +220,7 @@ jobs: Configure a longer timeout for large repositories: ```yaml -- uses: CodeAnt-AI/codeant-ci-scan-action@v0.0.6 +- uses: CodeAnt-AI/codeant-ci-scan-action@v0.0.5 with: access_token: ${{ secrets.ACCESS_TOKEN_GITHUB }} scan_timeout: '900' # 15 minutes (default: 300 seconds / 5 minutes) From 5ac7d93199b1cdc7a45b54cd9b0284943fd6aa27 Mon Sep 17 00:00:00 2001 From: Chinmay Pramod Bharti Date: Sat, 7 Nov 2026 05:01:52 +0530 Subject: [PATCH 3/3] Pass quality gate boolean directly to scanner --- .github/workflows/tests.yml | 17 -------- .gitignore | 1 - README.md | 6 +-- action.yml | 9 +---- tests/test_quality_gate.py | 80 ------------------------------------- 5 files changed, 4 insertions(+), 109 deletions(-) delete mode 100644 .github/workflows/tests.yml delete mode 100644 .gitignore delete mode 100644 tests/test_quality_gate.py diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml deleted file mode 100644 index e050b8e..0000000 --- a/.github/workflows/tests.yml +++ /dev/null @@ -1,17 +0,0 @@ -name: Action tests - -on: [push, pull_request] - -permissions: - contents: read - -jobs: - test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v6 - with: - python-version: '3.11' - - run: python -m pip install PyYAML==6.0.3 - - run: python -m unittest discover -s tests -v diff --git a/.gitignore b/.gitignore deleted file mode 100644 index c18dd8d..0000000 --- a/.gitignore +++ /dev/null @@ -1 +0,0 @@ -__pycache__/ diff --git a/README.md b/README.md index de979ae..b3e5b59 100644 --- a/README.md +++ b/README.md @@ -64,16 +64,16 @@ Configure **Full Code** conditions in **CodeAnt → Settings → Code Analysis ```yaml - name: CodeAnt quality gate - uses: CodeAnt-AI/codeant-ci-scan-action@d4e7f37809f110ab9216766e53fbad0061393e27 + uses: CodeAnt-AI/codeant-ci-scan-action@v0.0.6 with: access_token: ${{ secrets.CODEANT_TOKEN }} scanners: sast,sca quality_gate: 'true' ``` -This pinned revision supports `quality_gate`; `v0.0.5` does not. The input defaults to `'false'` so existing scan-only workflows keep their behavior. +Requires the planned `v0.0.6` release (not published yet); `v0.0.5` does not support `quality_gate`. The input defaults to `'false'` so existing scan-only workflows keep their behavior. -`quality_gate: 'true'` forwards `--quality-gate` to the CodeAnt scan script. A failed backend verdict or results timeout fails the step. Keep downstream deployment jobs dependent on this job, and use `if: always()` when uploading reports: +A failed backend verdict or results timeout fails the step when enforcement is enabled. Keep downstream deployment jobs dependent on this job, and use `if: always()` when uploading reports: ```yaml - name: Save reports diff --git a/action.yml b/action.yml index 6acdf47..a531cf4 100644 --- a/action.yml +++ b/action.yml @@ -70,13 +70,6 @@ runs: SCANNERS: ${{ inputs.scanners }} QUALITY_GATE: ${{ inputs.quality_gate }} run: | - quality_gate_args=() - case "$QUALITY_GATE" in - true) quality_gate_args+=(--quality-gate) ;; - false) ;; - *) echo "::error::quality_gate must be 'true' or 'false'"; exit 1 ;; - esac - bash start_scan.sh \ -a "$ACCESS_TOKEN" \ -r "$REPO_NAME" \ @@ -87,4 +80,4 @@ runs: -e "$EXCLUDE_PATHS" \ -t "$SCAN_TIMEOUT" \ --scanners "$SCANNERS" \ - "${quality_gate_args[@]}" + --quality-gate "$QUALITY_GATE" diff --git a/tests/test_quality_gate.py b/tests/test_quality_gate.py deleted file mode 100644 index 138b140..0000000 --- a/tests/test_quality_gate.py +++ /dev/null @@ -1,80 +0,0 @@ -"""Run the action's Bash step against a stand-in scanner to verify its contract.""" - -import os -from pathlib import Path -import subprocess -import tempfile -import unittest - -import yaml - - -ACTION = yaml.safe_load((Path(__file__).resolve().parents[1] / "action.yml").read_text()) -SCAN_STEP = next(step for step in ACTION["runs"]["steps"] if step["name"] == "Trigger CodeAnt analysis") - - -class QualityGateInputTests(unittest.TestCase): - def run_action(self, value=None, scanner_exit=0): - if value is None: - value = ACTION["inputs"]["quality_gate"]["default"] - self.assertEqual(SCAN_STEP["env"]["QUALITY_GATE"], "${{ inputs.quality_gate }}") - with tempfile.TemporaryDirectory() as directory: - root = Path(directory) - (root / "start_scan.sh").write_text( - '#!/bin/bash\nprintf "%s\\0" "$@" > arguments.bin\nexit "$SCANNER_EXIT"\n' - ) - env = { - **os.environ, - "ACCESS_TOKEN": "test-token", - "REPO_NAME": "example/repository", - "COMMIT_ID": "abc123", - "BRANCH": "main", - "INCLUDE_PATHS": "src/path with spaces/**", - "EXCLUDE_PATHS": "", - "SCAN_TIMEOUT": "600", - "SCANNERS": "sast,sca", - "QUALITY_GATE": str(value), - "SCANNER_EXIT": str(scanner_exit), - } - result = subprocess.run( - ["bash", "--noprofile", "--norc", "-e", "-o", "pipefail", "-c", SCAN_STEP["run"]], - cwd=root, env=env, capture_output=True, text=True, - ) - arguments = root / "arguments.bin" - argv = arguments.read_bytes().decode().split("\0")[:-1] if arguments.exists() else None - return result, argv - - def test_default_preserves_scan_only_behavior(self): - result, argv = self.run_action() - self.assertEqual(result.returncode, 0, result.stderr) - self.assertNotIn("--quality-gate", argv) - self.assertIn("src/path with spaces/**", argv) - self.assertEqual(argv[argv.index("-e") + 1], "") - - def test_true_requests_native_enforcement(self): - result, argv = self.run_action("true") - self.assertEqual(result.returncode, 0, result.stderr) - self.assertEqual(argv.count("--quality-gate"), 1) - - def test_false_does_not_request_enforcement(self): - result, argv = self.run_action("false") - self.assertEqual(result.returncode, 0, result.stderr) - self.assertNotIn("--quality-gate", argv) - - def test_scanner_failure_is_not_swallowed(self): - for value in ("true", "false"): - with self.subTest(quality_gate=value): - result, _ = self.run_action(value, scanner_exit=7) - self.assertEqual(result.returncode, 7) - - def test_invalid_input_does_not_start_a_scan(self): - for value in ("", "tru", "yes", "true; echo unexpected"): - with self.subTest(quality_gate=value): - result, argv = self.run_action(value) - self.assertEqual(result.returncode, 1) - self.assertIsNone(argv) - self.assertIn("quality_gate must be", result.stdout) - - -if __name__ == "__main__": - unittest.main()