Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .github/scripts/detect-code-changes.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#!/usr/bin/env bash
# Decide whether a pull request touches code that the live Enterprise suite
# covers. Writes `code=true` or `code=false` to $GITHUB_OUTPUT.
#
# Plain git on purpose: no third-party action and no Node runtime, so the cost
# filter keeps the same "standard tools only" property as the rest of the
# project. tests/unit/test_ci_path_filter.py pins the pattern below.
#
# Requires: BASE_REF (the pull request's target branch), GITHUB_OUTPUT (Actions).
set -euo pipefail

# No apostrophes in these messages: bash treats a single quote inside
# ${VAR:?word} as an opening quote even within double quotes, which makes the
# rest of the file a syntax error.
: "${BASE_REF:?BASE_REF must be set to the base branch of the pull request}"
: "${GITHUB_OUTPUT:?GITHUB_OUTPUT must be set by GitHub Actions}"

# Paths whose change requires a live Splunk run, anchored at the repo root.
readonly COVERED='^(src/|tests/|\.github/scripts/|pyproject\.toml$|\.github/workflows/ci\.yml$)'

# Fetch into an explicit remote-tracking ref. `git fetch origin <branch>` only
# updates FETCH_HEAD unless the checkout happens to configure a matching
# refspec, and `origin/<branch>` would then not resolve below.
git fetch --no-tags origin "+refs/heads/${BASE_REF}:refs/remotes/origin/${BASE_REF}"

if git diff --name-only "origin/${BASE_REF}...HEAD" | grep -qE "$COVERED"; then
echo "code=true" >>"$GITHUB_OUTPUT"
echo "Code paths changed: the Enterprise suite will run."
else
echo "code=false" >>"$GITHUB_OUTPUT"
echo "No code paths changed: skipping the Enterprise suite."
fi
20 changes: 11 additions & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,15 +65,17 @@ jobs:
outputs:
code: ${{ steps.filter.outputs.code }}
steps:
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3
id: filter
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
filters: |
code:
- "src/**"
- "tests/**"
- "pyproject.toml"
- ".github/workflows/ci.yml"
fetch-depth: 0

- name: Detect code changes
id: filter
env:
BASE_REF: ${{ github.base_ref }}
# Invoked through `bash` so the job does not depend on the executable
# bit surviving every checkout.
run: bash .github/scripts/detect-code-changes.sh

enterprise:
name: Enterprise / Full CLI
Expand Down Expand Up @@ -194,7 +196,7 @@ jobs:

- name: Upload failure diagnostics
if: failure()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: enterprise-failure-${{ github.run_id }}
path: |
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/cloud-read.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ jobs:

- name: Upload test report
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: cloud-read-${{ github.run_id }}
path: cloud-read.xml
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- Add a single **Merge Gate** check that aggregates every CI job, so branch
protection needs exactly one required status check and adding a job later
never needs a protection change.
- Move CI off actions that still run on the deprecated Node 20 runtime, and
replace the third-party paths-filter action with a small `git diff` script
covered by a unit test.
- Add catalog-driven live coverage for all 61 Enterprise reads and 93 writes,
including cleanup, global-state restoration, restart/reconnect, a container
CI gate on code pull requests and `main` merges, a manual Cloud read canary,
Expand Down
77 changes: 77 additions & 0 deletions tests/unit/test_ci_path_filter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
"""Pin the CI cost filter that decides whether the live Enterprise suite runs.

The filter lives in `.github/scripts/detect-code-changes.sh`. A silent mistake
there is expensive in both directions: too narrow and a real code change merges
without ever touching a live Splunk, too broad and every documentation typo
boots a container. The regex is read from the script itself, so there is one
source of truth and this test fails if the two ever drift apart.
"""

from __future__ import annotations

import re
import shutil
import subprocess
from pathlib import Path

import pytest

_SCRIPT = Path(__file__).resolve().parents[2] / ".github/scripts/detect-code-changes.sh"


@pytest.mark.skipif(shutil.which("bash") is None, reason="bash is not installed")
def test_script_is_valid_bash() -> None:
"""The script must parse.

A syntax error here only shows up as a failed CI job, and the reported line
can be far from the real mistake — an apostrophe inside `${VAR:?...}` opens
a quote that bash never closes, and it blames a later line.
"""
result = subprocess.run( # noqa: S603
["bash", "-n", str(_SCRIPT)],
capture_output=True,
text=True,
)
assert result.returncode == 0, f"detect-code-changes.sh has a syntax error:\n{result.stderr}"


def _covered_pattern() -> re.Pattern[str]:
"""Extract the COVERED extended-regex literal from the shell script."""
match = re.search(r"^readonly COVERED='(?P<expr>.+)'$", _SCRIPT.read_text(), re.MULTILINE)
assert match, "detect-code-changes.sh must define `readonly COVERED='<regex>'`"
return re.compile(match.group("expr"))


@pytest.mark.parametrize(
"path",
[
"src/vct_splunk/cli.py",
"src/vct_splunk/core/client.py",
"tests/unit/test_client.py",
"tests/integration/enterprise/read/test_catalog.py",
"pyproject.toml",
".github/workflows/ci.yml",
".github/scripts/detect-code-changes.sh",
],
)
def test_code_paths_trigger_the_live_suite(path: str) -> None:
"""Anything that can change CLI behavior must run the live suite."""
assert _covered_pattern().search(path), f"{path} should trigger the Enterprise suite"


@pytest.mark.parametrize(
"path",
[
"README.md",
"CHANGELOG.md",
"CONTRIBUTING.md",
"AGENTS.md",
"LICENSE",
"docs/pyproject.toml", # only a root-level pyproject.toml counts
"notes/src/scratch.py", # only a root-level src/ counts
".github/workflows/cloud-read.yml", # credential-gated, not this gate
],
)
def test_docs_only_paths_skip_the_live_suite(path: str) -> None:
"""Documentation and unrelated workflows must not boot a Splunk container."""
assert not _covered_pattern().search(path), f"{path} should not trigger the Enterprise suite"
Loading