Skip to content

Commit 30bcdc9

Browse files
authored
Merge pull request #105 from phpbb/ci/validate-phpbb-versions
Add GitHub Actions workflow to validate phpBB version files
2 parents e17814f + b3a0c23 commit 30bcdc9

2 files changed

Lines changed: 166 additions & 0 deletions

File tree

‎.github/scripts/validate_phpbb.py‎

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
#!/usr/bin/env python3
2+
"""Validate phpbb/versions.json and the phpbb/*.txt version check files."""
3+
4+
import json
5+
import re
6+
import sys
7+
from pathlib import Path
8+
9+
PHPBB_DIR = Path(__file__).resolve().parents[2] / 'phpbb'
10+
11+
VERSION_RE = re.compile(r'^\d+\.\d+\.\d+(-(a|b|rc|pl)\d+)?$', re.IGNORECASE)
12+
BRANCH_RE = re.compile(r'^\d+\.\d+$')
13+
URL_RE = re.compile(r'^https?://\S+$')
14+
15+
REQUIRED_KEYS = {'current', 'announcement', 'eol', 'security'}
16+
OPTIONAL_KEYS = {'urgent'}
17+
18+
errors = []
19+
20+
21+
def error(file, message):
22+
errors.append(f'{file}: {message}')
23+
24+
25+
def is_version_or_false(value):
26+
return value is False or (isinstance(value, str) and VERSION_RE.match(value))
27+
28+
29+
def validate_versions_json(path):
30+
try:
31+
data = json.loads(path.read_text(encoding='utf-8'))
32+
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as e:
33+
error(path.name, f'invalid JSON: {e}')
34+
return
35+
36+
if not isinstance(data, dict) or not data:
37+
error(path.name, 'top level must be a non-empty object')
38+
return
39+
40+
for channel, branches in data.items():
41+
if not isinstance(branches, dict) or not branches:
42+
error(path.name, f'"{channel}" must be a non-empty object')
43+
continue
44+
45+
for branch, info in branches.items():
46+
where = f'{channel}.{branch}'
47+
if not BRANCH_RE.match(branch):
48+
error(path.name, f'{where}: invalid branch name')
49+
if not isinstance(info, dict):
50+
error(path.name, f'{where}: must be an object')
51+
continue
52+
53+
missing = REQUIRED_KEYS - info.keys()
54+
unknown = info.keys() - REQUIRED_KEYS - OPTIONAL_KEYS
55+
if missing:
56+
error(path.name, f'{where}: missing keys {sorted(missing)}')
57+
if unknown:
58+
error(path.name, f'{where}: unknown keys {sorted(unknown)}')
59+
60+
current = info.get('current')
61+
if 'current' in info:
62+
if not isinstance(current, str) or not VERSION_RE.match(current):
63+
error(path.name, f'{where}.current: invalid version {current!r}')
64+
elif not current.startswith(branch + '.'):
65+
error(path.name, f'{where}.current: {current} does not belong to branch {branch}')
66+
67+
announcement = info.get('announcement')
68+
if 'announcement' in info and not (isinstance(announcement, str) and URL_RE.match(announcement)):
69+
error(path.name, f'{where}.announcement: invalid URL {announcement!r}')
70+
71+
eol = info.get('eol')
72+
if 'eol' in info and eol is not None and not isinstance(eol, str):
73+
error(path.name, f'{where}.eol: must be null or a string')
74+
75+
for key in ('security', 'urgent'):
76+
if key in info and not is_version_or_false(info[key]):
77+
error(path.name, f'{where}.{key}: must be false or a version, got {info[key]!r}')
78+
79+
80+
def read_lines(path):
81+
try:
82+
content = path.read_text(encoding='utf-8')
83+
except (OSError, UnicodeDecodeError) as e:
84+
error(path.name, f'unreadable: {e}')
85+
return None
86+
if '\r' in content:
87+
error(path.name, 'contains CR characters (use LF line endings)')
88+
return content.rstrip('\n').split('\n')
89+
90+
91+
def validate_20x_txt(path):
92+
"""Three lines with the major, minor and revision numbers of the 2.0.x release."""
93+
lines = read_lines(path)
94+
if lines is None:
95+
return
96+
if len(lines) != 3:
97+
error(path.name, f'expected 3 lines, got {len(lines)}')
98+
for i, line in enumerate(lines, 1):
99+
if not line.isdigit():
100+
error(path.name, f'line {i}: expected a number, got {line!r}')
101+
102+
103+
def validate_pairs_txt(path):
104+
"""Alternating version / announcement URL lines."""
105+
lines = read_lines(path)
106+
if lines is None:
107+
return
108+
if not lines or len(lines) % 2:
109+
error(path.name, f'expected version/URL line pairs, got {len(lines)} lines')
110+
for i, line in enumerate(lines, 1):
111+
if i % 2:
112+
if not VERSION_RE.match(line):
113+
error(path.name, f'line {i}: invalid version {line!r}')
114+
elif not URL_RE.match(line):
115+
error(path.name, f'line {i}: invalid URL {line!r}')
116+
117+
118+
def main():
119+
validate_versions_json(PHPBB_DIR / 'versions.json')
120+
121+
txt_files = sorted(PHPBB_DIR.glob('*.txt'))
122+
for path in txt_files:
123+
if path.name == '20x.txt':
124+
validate_20x_txt(path)
125+
else:
126+
validate_pairs_txt(path)
127+
128+
if errors:
129+
for message in errors:
130+
print(f'::error::{message}')
131+
return 1
132+
133+
print(f'versions.json and {len(txt_files)} txt file(s) are valid')
134+
return 0
135+
136+
137+
if __name__ == '__main__':
138+
sys.exit(main())

‎.github/workflows/validate.yml‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: Validate
2+
3+
on:
4+
push:
5+
paths:
6+
- 'phpbb/**'
7+
- '.github/**'
8+
pull_request:
9+
paths:
10+
- 'phpbb/**'
11+
- '.github/**'
12+
13+
permissions:
14+
contents: read
15+
16+
jobs:
17+
phpbb:
18+
name: Validate phpBB version files
19+
runs-on: ubuntu-latest
20+
steps:
21+
- uses: actions/checkout@v4
22+
23+
- uses: actions/setup-python@v5
24+
with:
25+
python-version: '3.x'
26+
27+
- name: Validate phpbb/versions.json and phpbb/*.txt
28+
run: python .github/scripts/validate_phpbb.py

0 commit comments

Comments
 (0)