Skip to content

Commit 4f8ba15

Browse files
committed
Add basic CI checks for proposal filename and numbering
Signed-off-by: Thomas Cooper <code@tomcooper.dev>
1 parent eca8987 commit 4f8ba15

4 files changed

Lines changed: 161 additions & 0 deletions

File tree

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
///usr/bin/env jbang "$0" "$@" ; exit $?
2+
//JAVA 25
3+
4+
import java.io.IOException;
5+
import java.nio.file.Files;
6+
import java.nio.file.Path;
7+
import java.util.ArrayList;
8+
import java.util.HashMap;
9+
import java.util.List;
10+
import java.util.Map;
11+
import java.util.Set;
12+
import java.util.regex.Matcher;
13+
import java.util.regex.Pattern;
14+
import java.util.stream.Collectors;
15+
16+
public class ValidateProposals {
17+
18+
static final Pattern PROPOSAL_FILE_PATTERN = Pattern.compile("\\d{3}-.+\\.md");
19+
static final Pattern NAMING_CONVENTION = Pattern.compile("\\d{3}-[a-z][a-z0-9]*(-[a-z0-9]+)*\\.md");
20+
static final Pattern README_TABLE_ROW = Pattern.compile(
21+
"^\\|\\s*(\\d{3})\\s*\\|\\s*\\[.*?\\]\\(\\./([^)]+)\\).*$");
22+
static final Set<String> EXCLUDED_FILES = Set.of("README.md", "000-template.md");
23+
24+
public static void main(String[] args) throws Exception {
25+
List<String> errors = new ArrayList<>();
26+
27+
List<String> proposals = getProposalFiles();
28+
29+
// Rule 1: Filename format
30+
List<String> validProposals = new ArrayList<>();
31+
for (String file : proposals) {
32+
if (matchesNamingConvention(file)) {
33+
validProposals.add(file);
34+
} else {
35+
errors.add("Filename '" + file + "' does not match the required pattern: "
36+
+ "NNN-kebab-case-title.md (3-digit prefix, lowercase kebab-case)");
37+
}
38+
}
39+
40+
// Rule 2: README table entry
41+
Map<Integer, String> readmeEntries = parseReadmeEntries(Path.of("README.md"));
42+
for (String file : validProposals) {
43+
int number = extractProposalNumber(file);
44+
validateReadmeEntry(number, file, readmeEntries, errors);
45+
}
46+
47+
// Rule 3: Sequential numbering (check duplicates first, then gaps)
48+
Map<Integer, List<String>> proposalsByNumber = new HashMap<>();
49+
for (String file : validProposals) {
50+
int number = extractProposalNumber(file);
51+
proposalsByNumber.computeIfAbsent(number, k -> new ArrayList<>()).add(file);
52+
}
53+
54+
for (var entry : proposalsByNumber.entrySet()) {
55+
if (entry.getValue().size() > 1) {
56+
errors.add("Proposal number " + String.format("%03d", entry.getKey())
57+
+ " is used by " + entry.getValue().size() + " files: "
58+
+ String.join(", ", entry.getValue()));
59+
}
60+
}
61+
62+
List<Integer> distinctNumbers = proposalsByNumber.keySet().stream()
63+
.sorted()
64+
.collect(Collectors.toList());
65+
66+
int expected = 1;
67+
for (int num : distinctNumbers) {
68+
if (num != expected) {
69+
errors.add("Proposal number " + String.format("%03d", num)
70+
+ " is not sequential. Expected " + String.format("%03d", expected)
71+
+ " (no gaps allowed)");
72+
}
73+
expected = num + 1;
74+
}
75+
76+
if (!errors.isEmpty()) {
77+
System.err.println("Proposal validation failed with " + errors.size() + " error(s):");
78+
for (String error : errors) {
79+
System.err.println(" - " + error);
80+
}
81+
System.exit(1);
82+
}
83+
84+
System.out.println("All proposal validations passed.");
85+
}
86+
87+
static List<String> getProposalFiles() throws IOException {
88+
try (var stream = Files.list(Path.of("."))) {
89+
return stream
90+
.map(p -> p.getFileName().toString())
91+
.filter(f -> f.endsWith(".md"))
92+
.filter(f -> !EXCLUDED_FILES.contains(f))
93+
.sorted()
94+
.collect(Collectors.toList());
95+
}
96+
}
97+
98+
static boolean matchesNamingConvention(String filename) {
99+
return NAMING_CONVENTION.matcher(filename).matches();
100+
}
101+
102+
static int extractProposalNumber(String filename) {
103+
if (filename.length() >= 3) {
104+
try {
105+
return Integer.parseInt(filename.substring(0, 3));
106+
} catch (NumberFormatException e) {
107+
// fall through
108+
}
109+
}
110+
return -1;
111+
}
112+
113+
static Map<Integer, String> parseReadmeEntries(Path readmePath) throws IOException {
114+
Map<Integer, String> entries = new HashMap<>();
115+
List<String> lines = Files.readAllLines(readmePath);
116+
for (String line : lines) {
117+
Matcher m = README_TABLE_ROW.matcher(line.trim());
118+
if (m.matches()) {
119+
int number = Integer.parseInt(m.group(1));
120+
String linkedFile = m.group(2);
121+
entries.put(number, linkedFile);
122+
}
123+
}
124+
return entries;
125+
}
126+
127+
static void validateReadmeEntry(int number, String filename,
128+
Map<Integer, String> readmeEntries, List<String> errors) {
129+
String linkedFile = readmeEntries.get(number);
130+
if (linkedFile == null) {
131+
errors.add("README.md is missing a table entry for proposal "
132+
+ String.format("%03d", number) + " (" + filename + ")");
133+
} else if (!linkedFile.equals(filename)) {
134+
errors.add("README.md entry for proposal " + String.format("%03d", number)
135+
+ " links to '" + linkedFile + "' but expected '" + filename + "'");
136+
}
137+
}
138+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
name: Validate Proposals
2+
3+
on:
4+
pull_request:
5+
branches: [main]
6+
7+
jobs:
8+
validate:
9+
runs-on: ubuntu-latest
10+
steps:
11+
- name: Checkout Repository
12+
uses: actions/checkout@v6
13+
14+
- name: Validate Proposals
15+
uses: jbangdev/jbang-action@v0.136.0
16+
with:
17+
script: .github/scripts/ValidateProposals.java

.sdkmanrc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Enable auto-env through the sdkman_auto_env config
2+
# Add key=value pairs of SDKs to use below
3+
java=25-tem
4+
jbang=0.136.0

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ This repository lists proposals for the StreamsHub organization.
77
Governance rules around the review and acceptance of proposals are defined in the [StreamsHub Governance Repository](https://github.com/streamshub/governance).
88

99
A template for new proposals can be found [here](./000-template.md).
10+
File names should follow the format `NNN-title.md` where `NNN` is a three digit number that is incremented for each new proposal.
11+
File name format and numbering is validated by the CI workflow defined in [validate-proposals.yml](./.github/workflows/validate-proposals.yml).
1012

1113
| # | Title |
1214
|:--: |:--------------------------------------------------------------------------------|

0 commit comments

Comments
 (0)