Skip to content

Commit 88d5d64

Browse files
committed
Refactor smoke tests: dynamic matrix from overlay deps, rewrite scripts in Java
* Replace hardcoded overlay list with a computed test matrix that automatically discovers "leaf" overlays from kustomization.yaml dependency graphs (ComputeTestMatrix.java), ensuring new overlays are tested without manual workflow updates * Rewrite shell verification scripts (verify-install.sh, verify-uninstall.sh, debug.sh) as JBang Java scripts (VerifyInstall.java, VerifyUninstall.java, Debug.java) with shared utilities (ScriptUtils.java) for parsing kustomization files and checking resource readiness conditions * Consolidate duplicate smoke-minikube and smoke-kind workflow jobs into a single smoke-test job with a platform dimension in the matrix * Add per-overlay condition-overrides via test-matrix.yaml for resources that use non-default readiness conditions (e.g. monitoring.coreos.com=Available) * Remove static .env overlay configs in favor of dynamic discovery Signed-off-by: Thomas Cooper <code@tomcooper.dev>
1 parent 17ec06c commit 88d5d64

File tree

16 files changed

+1225
-189
lines changed

16 files changed

+1225
-189
lines changed

.github/actions/smoke-test/action.yaml

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,45 +7,52 @@ inputs:
77
required: false
88
default: '300s'
99
overlay:
10-
description: 'Overlay name for component verification (matches .github/config/overlays/<name>.env)'
10+
description: 'Overlay name (matches overlays/<name>/ directory)'
1111
required: false
1212
default: 'core'
13+
condition-overrides:
14+
description: 'Space-separated apiGroup=Condition overrides for readiness checks (default: Ready)'
15+
required: false
16+
default: ''
1317

1418
runs:
1519
using: 'composite'
1620
steps:
21+
- name: Set up JBang
22+
uses: jbangdev/setup-jbang@2b1b465a7b75f4222b81426f23a01e013aa7b95c # v0.1.1
23+
1724
- name: Run install script
1825
shell: bash
1926
env:
2027
LOCAL_DIR: .
21-
OVERLAY: ${{ inputs.overlay }}
2228
TIMEOUT: ${{ inputs.timeout }}
29+
OVERLAY: ${{ inputs.overlay }}
2330
run: ./install.sh
2431

2532
- name: Verify deployments
2633
shell: bash
2734
env:
2835
OVERLAY: ${{ inputs.overlay }}
36+
CONDITION_OVERRIDES: ${{ inputs.condition-overrides }}
2937
TIMEOUT: ${{ inputs.timeout }}
30-
run: .github/scripts/verify-install.sh
38+
run: jbang .github/scripts/VerifyInstall.java
3139

3240
- name: Run uninstall script
3341
shell: bash
3442
env:
3543
LOCAL_DIR: .
36-
OVERLAY: ${{ inputs.overlay }}
3744
TIMEOUT: ${{ inputs.timeout }}
45+
OVERLAY: ${{ inputs.overlay }}
3846
run: ./uninstall.sh
3947

4048
- name: Verify uninstall
4149
shell: bash
42-
env:
43-
TIMEOUT: ${{ inputs.timeout }}
44-
run: .github/scripts/verify-uninstall.sh
50+
run: jbang .github/scripts/VerifyUninstall.java
4551

4652
- name: Debug on failure
4753
if: failure()
4854
shell: bash
4955
env:
5056
OVERLAY: ${{ inputs.overlay }}
51-
run: .github/scripts/debug.sh
57+
CONDITION_OVERRIDES: ${{ inputs.condition-overrides }}
58+
run: jbang .github/scripts/Debug.java

.github/config/overlays/core.env

Lines changed: 0 additions & 11 deletions
This file was deleted.

.github/config/overlays/metrics.env

Lines changed: 0 additions & 11 deletions
This file was deleted.

.github/config/test-matrix.yaml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Test matrix configuration for CI smoke tests.
2+
#
3+
# ComputeTestMatrix.java reads this file to enrich the dynamically
4+
# computed test matrix with per-overlay test settings.
5+
#
6+
# The matrix is computed automatically from overlay component
7+
# dependencies — only "leaf" overlays (those not fully covered by
8+
# a larger overlay) are tested. This file provides additional
9+
# test-specific configuration that doesn't belong in the overlay
10+
# definitions themselves.
11+
#
12+
# Structure:
13+
# overlays:
14+
# <overlay-name>:
15+
# condition-overrides: "<apiGroup>=<Condition> ..."
16+
#
17+
# Fields:
18+
# condition-overrides Space-separated list of apiGroup=Condition
19+
# pairs. During verification, custom resources
20+
# belonging to the given API group will be
21+
# checked for the specified condition instead
22+
# of the default "Ready".
23+
24+
overlays:
25+
metrics:
26+
condition-overrides: "monitoring.coreos.com=Available"
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
///usr/bin/env jbang "$0" "$@" ; exit $?
2+
//DEPS org.yaml:snakeyaml:2.6
3+
//DEPS com.fasterxml.jackson.core:jackson-databind:2.21.2
4+
//SOURCES ScriptUtils.java
5+
6+
import com.fasterxml.jackson.databind.ObjectMapper;
7+
import com.fasterxml.jackson.databind.node.ArrayNode;
8+
import com.fasterxml.jackson.databind.node.ObjectNode;
9+
import org.yaml.snakeyaml.Yaml;
10+
11+
import java.io.IOException;
12+
import java.nio.file.DirectoryStream;
13+
import java.nio.file.Files;
14+
import java.nio.file.Path;
15+
import java.util.ArrayList;
16+
import java.util.Arrays;
17+
import java.util.HashMap;
18+
import java.util.HashSet;
19+
import java.util.List;
20+
import java.util.Map;
21+
import java.util.Set;
22+
import java.util.TreeMap;
23+
24+
/**
25+
* Computes the integration test matrix by analysing overlay dependency graphs.
26+
*
27+
* <p>Parses each overlay's kustomization.yaml files to extract component sets,
28+
* then classifies overlays as "leaf" (not covered by any other overlay) or
29+
* "non-leaf" (a strict subset of at least one other overlay).
30+
*
31+
* <p>Leaf overlays are tested on ALL platforms; non-leaf overlays are eliminated.
32+
*
33+
* <p>Output: a JSON object suitable for GitHub Actions {@code strategy.matrix.fromJSON()}.
34+
*
35+
* <p>Environment variables:
36+
* <ul>
37+
* <li>{@code PLATFORMS} — space-separated list of platforms (default: "minikube kind")</li>
38+
* </ul>
39+
*/
40+
public class ComputeTestMatrix {
41+
42+
private static final String DEFAULT_PLATFORMS = "minikube kind";
43+
private static final String COMPONENTS_PREFIX = "components/";
44+
private static final String TEST_CONFIG_PATH = ".github/config/test-matrix.yaml";
45+
46+
public static void main(String[] args) throws IOException {
47+
String platformsEnv = System.getenv().getOrDefault("PLATFORMS", DEFAULT_PLATFORMS);
48+
List<String> platforms = Arrays.asList(platformsEnv.trim().split("\\s+"));
49+
50+
Path repoRoot = ScriptUtils.findRepoRoot();
51+
Path overlaysDir = repoRoot.resolve("overlays");
52+
53+
// Step 1: Parse component sets from kustomization files
54+
Map<String, Set<String>> overlayComponents = parseOverlayComponents(overlaysDir);
55+
56+
// Step 2: Classify overlays — find leaves
57+
List<String> leafOverlays = findLeafOverlays(overlayComponents);
58+
59+
// Step 3: Read per-overlay test config
60+
Map<String, String> conditionOverrides = readTestConfig(repoRoot);
61+
62+
// Step 4: Build the matrix JSON
63+
ObjectMapper mapper = new ObjectMapper();
64+
ArrayNode includeArray = mapper.createArrayNode();
65+
66+
for (String overlay : leafOverlays) {
67+
for (String platform : platforms) {
68+
ObjectNode entry = mapper.createObjectNode();
69+
entry.put("platform", platform);
70+
entry.put("overlay", overlay);
71+
if (conditionOverrides.containsKey(overlay)) {
72+
entry.put("condition-overrides", conditionOverrides.get(overlay));
73+
}
74+
includeArray.add(entry);
75+
}
76+
}
77+
78+
ObjectNode matrix = mapper.createObjectNode();
79+
matrix.set("include", includeArray);
80+
81+
// Output compact JSON to stdout (consumed by GitHub Actions)
82+
System.out.println(mapper.writeValueAsString(matrix));
83+
}
84+
85+
/**
86+
* Read per-overlay test configuration from the central test config file.
87+
*
88+
* @see #TEST_CONFIG_PATH
89+
*/
90+
@SuppressWarnings("unchecked")
91+
static Map<String, String> readTestConfig(Path repoRoot) throws IOException {
92+
Map<String, String> conditionOverrides = new HashMap<>();
93+
Path configFile = repoRoot.resolve(TEST_CONFIG_PATH);
94+
95+
if (!Files.exists(configFile)) {
96+
return conditionOverrides;
97+
}
98+
99+
Yaml yaml = new Yaml();
100+
Map<String, Object> config = yaml.load(Files.readString(configFile));
101+
if (config == null || !config.containsKey("overlays")) {
102+
return conditionOverrides;
103+
}
104+
105+
Map<String, Object> overlays = (Map<String, Object>) config.get("overlays");
106+
for (Map.Entry<String, Object> entry : overlays.entrySet()) {
107+
if (entry.getValue() instanceof Map) {
108+
Map<String, Object> overlayConfig = (Map<String, Object>) entry.getValue();
109+
Object overrides = overlayConfig.get("condition-overrides");
110+
if (overrides != null) {
111+
conditionOverrides.put(entry.getKey(), overrides.toString());
112+
}
113+
}
114+
}
115+
116+
return conditionOverrides;
117+
}
118+
119+
/**
120+
* Parse the component sets for each overlay.
121+
*
122+
* <p>For each overlay directory under overlaysDir, scans all subdirectories
123+
* for kustomization.yaml files, extracts the {@code components:} list, and
124+
* normalizes paths to canonical form (e.g. "core/base", "metrics/stack").
125+
*/
126+
@SuppressWarnings("unchecked")
127+
static Map<String, Set<String>> parseOverlayComponents(Path overlaysDir) throws IOException {
128+
Map<String, Set<String>> result = new TreeMap<>();
129+
Yaml yaml = new Yaml();
130+
131+
try (DirectoryStream<Path> stream = Files.newDirectoryStream(overlaysDir, Files::isDirectory)) {
132+
for (Path overlayDir : stream) {
133+
String overlayName = overlayDir.getFileName().toString();
134+
Set<String> components = new HashSet<>();
135+
136+
try (DirectoryStream<Path> layers = Files.newDirectoryStream(overlayDir, Files::isDirectory)) {
137+
for (Path layerDir : layers) {
138+
Path kustomization = layerDir.resolve("kustomization.yaml");
139+
if (!Files.exists(kustomization)) {
140+
continue;
141+
}
142+
143+
Map<String, Object> doc = yaml.load(Files.readString(kustomization));
144+
if (doc == null || !doc.containsKey("components")) {
145+
continue;
146+
}
147+
148+
List<String> componentPaths = (List<String>) doc.get("components");
149+
for (String path : componentPaths) {
150+
// Normalize: "../../../components/core/base" → "core/base"
151+
int idx = path.indexOf(COMPONENTS_PREFIX);
152+
if (idx >= 0) {
153+
components.add(path.substring(idx + COMPONENTS_PREFIX.length()));
154+
}
155+
}
156+
}
157+
}
158+
159+
if (!components.isEmpty()) {
160+
result.put(overlayName, components);
161+
}
162+
}
163+
}
164+
165+
return result;
166+
}
167+
168+
/**
169+
* Find leaf overlays — those whose component set is NOT a strict subset
170+
* of any other overlay's component set.
171+
*
172+
* <p>An overlay is a "leaf" if no other overlay covers all of its components
173+
* (and has additional ones). Non-leaf overlays are eliminated from testing
174+
* because their components are fully exercised by the leaf that covers them.
175+
*/
176+
static List<String> findLeafOverlays(Map<String, Set<String>> overlayComponents) {
177+
List<String> leaves = new ArrayList<>();
178+
179+
for (Map.Entry<String, Set<String>> candidate : overlayComponents.entrySet()) {
180+
boolean covered = false;
181+
182+
for (Map.Entry<String, Set<String>> other : overlayComponents.entrySet()) {
183+
if (other.getKey().equals(candidate.getKey())) {
184+
continue;
185+
}
186+
// Check if 'other' is a strict superset of 'candidate'
187+
if (other.getValue().containsAll(candidate.getValue())
188+
&& other.getValue().size() > candidate.getValue().size()) {
189+
covered = true;
190+
break;
191+
}
192+
}
193+
194+
if (!covered) {
195+
leaves.add(candidate.getKey());
196+
}
197+
}
198+
199+
leaves.sort(String::compareTo);
200+
return leaves;
201+
}
202+
}

0 commit comments

Comments
 (0)