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
16 changes: 14 additions & 2 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,19 @@ inputs:
features:
description: 'Comma separated list of features.'
required: false
type: string
outputs:
supportedSplunk:
description: 'JSON array of all supported Splunk versions'
supportedSplunkModinput:
description: 'JSON array of supported Splunk versions with server_conf_python_versions variants expanded for modinput tests'
latestSplunk:
description: 'JSON array with the single latest Splunk version'
supportedSC4S:
description: 'JSON array of all supported SC4S versions'
supportedModinputFunctionalVendors:
description: 'JSON array of supported modinput functional vendor versions'
supportedUIVendors:
description: 'JSON array of supported UI vendor versions'
runs:
using: "docker"
image: 'docker://ghcr.io/splunk/addonfactory-test-matrix-action/addonfactory-test-matrix-action:v3.2.1'
image: 'Dockerfile'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[IMPORTANT] Reliability / Supply-chain — switching from a pinned, published image (v3.2.0) to image: 'Dockerfile' makes every consumer build from source on each run and loses image immutability/provenance. Understood that this is needed so the updated code is actually executed, but please follow up by publishing a new pinned tag (e.g. v3.3.0) and pointing the action back at it rather than leaving runtime builds permanently.

90 changes: 64 additions & 26 deletions addonfactory_test_matrix_action/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,40 +18,69 @@ def has_features(features, section):
return True


def _generate_supported_splunk(args, path):
_ALLOWED_SERVER_CONF_PYTHON_VERSIONS = {"python3", "force_python3"}


def _load_splunk_config(path):
if os.path.exists("splunk_matrix.conf"):
splunk_matrix = "splunk_matrix.conf"
else:
splunk_matrix = os.path.join(path, "splunk_matrix.conf")
config = configparser.ConfigParser()
config.read(splunk_matrix)
supported_splunk = []
return config


def _iter_splunk_sections(args, config):
"""Yield (section, props, base_entry) for each non-EOL, feature-matching Splunk version."""
today = datetime.now().date()
for section in config.sections():
if re.search(r"^\d+", section):
props = {}
supported_splunk_string = config[section]["SUPPORTED"]
eol = datetime.strptime(supported_splunk_string, "%Y-%m-%d").date()
today = datetime.now().date()
if today >= eol:
continue

if not has_features(args.features, config[section]):
continue
for k in config[section].keys():
try:
value = config[section].getboolean(k)
except:
value = config[section][k]
props[k] = value
if not re.search(r"^\d+", section):
continue
eol = datetime.strptime(config[section]["SUPPORTED"], "%Y-%m-%d").date()
if today >= eol:
continue
if not has_features(args.features, config[section]):
continue
props = {}
for k in config[section].keys():
try:
value = config[section].getboolean(k)
except ValueError:
value = config[section][k]
props[k] = value
base_entry = {
"version": props["version"],
"build": props["build"],
"islatest": (config["GENERAL"]["LATEST"] == section),
"isoldest": (config["GENERAL"]["OLDEST"] == section),
}
yield section, props, base_entry

supported_splunk.append(
{
"version": props["version"],
"build": props["build"],
"islatest": (config["GENERAL"]["LATEST"] == section),
"isoldest": (config["GENERAL"]["OLDEST"] == section),
}
)

def _generate_supported_splunk(args, path):
config = _load_splunk_config(path)
return [base_entry for _, _, base_entry in _iter_splunk_sections(args, config)]


def _generate_supported_splunk_modinput(args, path):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[IMPORTANT] Maintainability + Tests — two things on this new function:

  1. It's a near-verbatim copy of _generate_supported_splunk above; only the entry construction differs. Two copies of the EOL parse / feature filter / props loop will drift (a future fix to one won't reach the other). Consider factoring the shared parsing into a helper that yields (section, props) and building both outputs from it.
  2. There are no unit tests for this logic. The PR test plan (9.4 → 2 entries with serverConfPythonVersion ∈ {python3, force_python3}; other versions → 1 entry; EOL/feature filtering still applies) is exactly what a unit test should assert. tests/ currently covers only the matrix-updater, not main.py.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — factored the shared file-loading + EOL/feature-filtering loop into _load_splunk_config and _iter_splunk_sections. Both _generate_supported_splunk and _generate_supported_splunk_modinput now delegate to those helpers, eliminating the duplication.

Unit tests added in tests/test_main.py covering:

  • 9.4 expands to exactly 2 entries with serverConfPythonVersion in {python3, force_python3}
  • Versions without SERVER_CONF_PYTHON_VERSIONS appear once, without the extra field
  • EOL versions are excluded from both outputs
  • Invalid server_conf_python_versions values raise ValueError
  • islatest / isoldest flags are set correctly

All 9 tests pass.

config = _load_splunk_config(path)
supported_splunk = []
for _, props, base_entry in _iter_splunk_sections(args, config):
raw = props.get("server_conf_python_versions")
if raw:
for python_version in raw.split(","):
python_version = python_version.strip()
if python_version not in _ALLOWED_SERVER_CONF_PYTHON_VERSIONS:
raise ValueError(
f"Invalid server_conf_python_versions value: {python_version!r}. "
f"Allowed: {sorted(_ALLOWED_SERVER_CONF_PYTHON_VERSIONS)}"
)
variant = dict(base_entry)
variant["serverConfPythonVersion"] = python_version
supported_splunk.append(variant)
else:
supported_splunk.append(base_entry)
return supported_splunk


Expand Down Expand Up @@ -136,6 +165,15 @@ def main():
with open(os.environ["GITHUB_OUTPUT"], "a") as fh:
print(f"supportedSplunk={json.dumps(supported_splunk)}", file=fh)

supported_splunk_modinput = _generate_supported_splunk_modinput(args, path)
pprint.pprint(
f"Supported Splunk versions (modinput): {json.dumps(supported_splunk_modinput)}"
)
with open(os.environ["GITHUB_OUTPUT"], "a") as fh:
print(
f"supportedSplunkModinput={json.dumps(supported_splunk_modinput)}", file=fh
)

for splunk in supported_splunk:
if splunk["islatest"]:
pprint.pprint(f"Latest Splunk version: {json.dumps([splunk])}")
Expand Down
1 change: 1 addition & 0 deletions config/splunk_matrix.conf
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ BUILD = 9dfc486f3d48
SUPPORTED = 2026-12-16
PYTHON39 = true
PYTHON37 = false
SERVER_CONF_PYTHON_VERSIONS = python3,force_python3

[9.3]
VERSION = 9.3.13
Expand Down
Empty file added tests/__init__.py
Empty file.
181 changes: 181 additions & 0 deletions tests/test_main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import argparse
import configparser
import textwrap
from unittest.mock import patch

import pytest

from addonfactory_test_matrix_action.main import (
_ALLOWED_SERVER_CONF_PYTHON_VERSIONS,
_generate_supported_splunk,
_generate_supported_splunk_modinput,
_load_splunk_config,
_iter_splunk_sections,
)

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

_MATRIX_TEMPLATE = textwrap.dedent(
"""\
[GENERAL]
LATEST = 10.2
OLDEST = 9.3

[10.2]
VERSION = 10.2.2
BUILD = aaaaaaaaaaaa
SUPPORTED = 2028-01-15
PYTHON39 = true

[9.4]
VERSION = 9.4.10
BUILD = bbbbbbbbbbbb
SUPPORTED = 2026-12-16
PYTHON39 = true
SERVER_CONF_PYTHON_VERSIONS = python3,force_python3

[9.3]
VERSION = 9.3.11
BUILD = cccccccccccc
SUPPORTED = 2026-07-24
PYTHON39 = true
"""
)

# 9.3 is EOL relative to today (2026-07-24 < 2026-06-30 is false, but let's use
# a matrix where one section is genuinely EOL for that test)
_MATRIX_WITH_EOL = textwrap.dedent(
"""\
[GENERAL]
LATEST = 10.2
OLDEST = 9.0

[10.2]
VERSION = 10.2.2
BUILD = aaaaaaaaaaaa
SUPPORTED = 2028-01-15
PYTHON39 = true

[9.4]
VERSION = 9.4.10
BUILD = bbbbbbbbbbbb
SUPPORTED = 2026-12-16
PYTHON39 = true
SERVER_CONF_PYTHON_VERSIONS = python3,force_python3

[9.0]
VERSION = 9.0.0
BUILD = dddddddddddd
SUPPORTED = 2024-01-01
PYTHON39 = false
"""
)

_MATRIX_INVALID_VERSION = textwrap.dedent(
"""\
[GENERAL]
LATEST = 9.4
OLDEST = 9.4

[9.4]
VERSION = 9.4.10
BUILD = bbbbbbbbbbbb
SUPPORTED = 2028-01-15
SERVER_CONF_PYTHON_VERSIONS = invalid_value
"""
)


def _args(features=None):
ns = argparse.Namespace()
ns.features = features
return ns


def _config_from_str(text):
cfg = configparser.ConfigParser()
cfg.read_string(text)
return cfg


def _mock_load(text):
"""Return a patcher that replaces _load_splunk_config with one reading *text*."""
cfg = _config_from_str(text)
return patch(
"addonfactory_test_matrix_action.main._load_splunk_config",
return_value=cfg,
)


# ---------------------------------------------------------------------------
# _generate_supported_splunk
# ---------------------------------------------------------------------------


class TestGenerateSupportedSplunk:
def test_returns_active_versions_only(self):
with _mock_load(_MATRIX_WITH_EOL):
result = _generate_supported_splunk(_args(), path="unused")
versions = [e["version"] for e in result]
assert "10.2.2" in versions
assert "9.4.10" in versions
assert "9.0.0" not in versions

def test_no_server_conf_python_version_field(self):
with _mock_load(_MATRIX_TEMPLATE):
result = _generate_supported_splunk(_args(), path="unused")
for entry in result:
assert "serverConfPythonVersion" not in entry

def test_islatest_isoldest_flags(self):
with _mock_load(_MATRIX_TEMPLATE):
result = _generate_supported_splunk(_args(), path="unused")
latest = [e for e in result if e["islatest"]]
oldest = [e for e in result if e["isoldest"]]
assert len(latest) == 1 and latest[0]["version"] == "10.2.2"
# 9.3 is the OLDEST in MATRIX_TEMPLATE (and still active as of today)
assert len(oldest) == 1 and oldest[0]["version"] == "9.3.11"


# ---------------------------------------------------------------------------
# _generate_supported_splunk_modinput
# ---------------------------------------------------------------------------


class TestGenerateSupportedSplunkModinput:
def test_94_expands_to_two_variants(self):
with _mock_load(_MATRIX_TEMPLATE):
result = _generate_supported_splunk_modinput(_args(), path="unused")
v94 = [e for e in result if e["version"] == "9.4.10"]
assert len(v94) == 2
python_versions = {e["serverConfPythonVersion"] for e in v94}
assert python_versions == {"python3", "force_python3"}

def test_versions_without_setting_appear_once_without_field(self):
with _mock_load(_MATRIX_TEMPLATE):
result = _generate_supported_splunk_modinput(_args(), path="unused")
v102 = [e for e in result if e["version"] == "10.2.2"]
assert len(v102) == 1
assert "serverConfPythonVersion" not in v102[0]

def test_total_entry_count(self):
# 10.2 → 1, 9.4 → 2, 9.3 → 1 = 4 (9.3 still active in MATRIX_TEMPLATE)
with _mock_load(_MATRIX_TEMPLATE):
result = _generate_supported_splunk_modinput(_args(), path="unused")
assert len(result) == 4

def test_eol_version_excluded(self):
with _mock_load(_MATRIX_WITH_EOL):
result = _generate_supported_splunk_modinput(_args(), path="unused")
versions = [e["version"] for e in result]
assert "9.0.0" not in versions

def test_invalid_python_version_raises(self):
with _mock_load(_MATRIX_INVALID_VERSION):
with pytest.raises(ValueError, match="Invalid server_conf_python_versions"):
_generate_supported_splunk_modinput(_args(), path="unused")

def test_allowlist_contents(self):
assert _ALLOWED_SERVER_CONF_PYTHON_VERSIONS == {"python3", "force_python3"}
Loading