Skip to content

Commit f022533

Browse files
committed
Workflow
1 parent e321e07 commit f022533

4 files changed

Lines changed: 281 additions & 1 deletion

File tree

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
#!/usr/bin/env python3
2+
"""Check WordPress "Tested up to" metadata against the latest release."""
3+
4+
from __future__ import annotations
5+
6+
import json
7+
import os
8+
import re
9+
import sys
10+
from pathlib import Path
11+
12+
13+
WORDPRESS_LATEST_VERSION = os.environ.get("WORDPRESS_LATEST_VERSION")
14+
WORDPRESS_VERSION_CHECK_FILE = Path("wordpress-version-check.json")
15+
SCAN_EXTENSIONS = {".php", ".md", ".txt"}
16+
DEFAULT_EXCLUDED_DIRS = {
17+
".git",
18+
"build",
19+
"coverage",
20+
"dist",
21+
"node_modules",
22+
"plugin-check-build",
23+
"__pycache__",
24+
"vendor",
25+
}
26+
TESTED_UP_TO_PATTERN = re.compile(
27+
r"\btested\s+up\s+to\s*:\s*([0-9]+(?:\.[0-9]+){1,2})\b",
28+
re.IGNORECASE,
29+
)
30+
TESTED_UP_TO_LABEL_PATTERN = re.compile(r"\btested\s+up\s+to\s*:", re.IGNORECASE)
31+
VERSION_PATTERN = re.compile(r"^[0-9]+(?:\.[0-9]+){1,2}$")
32+
33+
34+
def main() -> int:
35+
latest_version = get_latest_wordpress_major_minor()
36+
excluded_dirs = get_excluded_dirs()
37+
findings = find_tested_up_to_entries(excluded_dirs)
38+
39+
if not findings:
40+
message = "No Tested up to metadata was found in PHP, Markdown, or text files."
41+
print_github_error(message)
42+
write_summary(latest_version, [], [message])
43+
return 1
44+
45+
failures = []
46+
for finding in findings:
47+
if finding["version"] is None:
48+
failures.append(
49+
f"{finding['path']}:{finding['line']}: Could not parse Tested up to version."
50+
)
51+
print_github_error(
52+
"Could not parse Tested up to version.",
53+
finding["path"],
54+
finding["line"],
55+
)
56+
continue
57+
58+
tested_version = normalize_major_minor(finding["version"])
59+
if tested_version != latest_version:
60+
message = (
61+
f"Tested up to is {finding['version']}; expected {latest_version} "
62+
"for the latest WordPress release."
63+
)
64+
failures.append(f"{finding['path']}:{finding['line']}: {message}")
65+
print_github_error(message, finding["path"], finding["line"])
66+
67+
write_summary(latest_version, findings, failures)
68+
69+
if failures:
70+
entry_label = "entry" if len(failures) == 1 else "entries"
71+
print(f"Found {len(failures)} stale or invalid Tested up to {entry_label}.")
72+
for failure in failures:
73+
print(f"- {failure}")
74+
return 1
75+
76+
print(f"All Tested up to entries match WordPress {latest_version}.")
77+
return 0
78+
79+
80+
def get_latest_wordpress_major_minor() -> str:
81+
if WORDPRESS_LATEST_VERSION:
82+
return normalize_major_minor(WORDPRESS_LATEST_VERSION)
83+
84+
if not WORDPRESS_VERSION_CHECK_FILE.is_file():
85+
raise RuntimeError(
86+
"WORDPRESS_LATEST_VERSION must be set, or wordpress-version-check.json must exist."
87+
)
88+
89+
payload = json.loads(WORDPRESS_VERSION_CHECK_FILE.read_text(encoding="utf-8"))
90+
versions = []
91+
92+
for offer in payload.get("offers", []):
93+
version = offer.get("current") or offer.get("version")
94+
if isinstance(version, str) and VERSION_PATTERN.match(version):
95+
versions.append(version)
96+
97+
if not versions:
98+
raise RuntimeError("Could not determine the latest WordPress version.")
99+
100+
latest = max(versions, key=version_sort_key)
101+
return normalize_major_minor(latest)
102+
103+
104+
def normalize_major_minor(version: str) -> str:
105+
parts = version.split(".")
106+
107+
if len(parts) < 2 or not all(part.isdigit() for part in parts):
108+
raise ValueError(f"Invalid WordPress version: {version}")
109+
110+
return ".".join(parts[:2])
111+
112+
113+
def version_sort_key(version: str) -> tuple[int, ...]:
114+
return tuple(int(part) for part in version.split("."))
115+
116+
117+
def get_excluded_dirs() -> set[str]:
118+
configured = os.environ.get("WORDPRESS_TESTED_UP_TO_EXCLUDE_DIRS", "")
119+
extra_dirs = {item.strip() for item in configured.split(",") if item.strip()}
120+
121+
return DEFAULT_EXCLUDED_DIRS | extra_dirs
122+
123+
124+
def find_tested_up_to_entries(
125+
excluded_dirs: set[str],
126+
) -> list[dict[str, str | int | None]]:
127+
findings = []
128+
129+
for path in get_scanned_files(excluded_dirs):
130+
if not should_scan(path, excluded_dirs):
131+
continue
132+
133+
lines = path.read_text(encoding="utf-8", errors="replace").splitlines()
134+
135+
for line_number, line in enumerate(lines, 1):
136+
if not TESTED_UP_TO_LABEL_PATTERN.search(line):
137+
continue
138+
139+
match = TESTED_UP_TO_PATTERN.search(line)
140+
findings.append(
141+
{
142+
"path": path.as_posix(),
143+
"line": line_number,
144+
"version": match.group(1) if match else None,
145+
}
146+
)
147+
148+
return findings
149+
150+
151+
def get_scanned_files(excluded_dirs: set[str]) -> list[Path]:
152+
root = Path.cwd()
153+
paths = []
154+
155+
for current_dir, dirnames, filenames in os.walk(root):
156+
dirnames[:] = [
157+
dirname for dirname in dirnames if dirname not in excluded_dirs
158+
]
159+
160+
current_path = Path(current_dir)
161+
for filename in filenames:
162+
path = current_path / filename
163+
relative_path = path.relative_to(root)
164+
165+
if should_scan(relative_path, excluded_dirs):
166+
paths.append(relative_path)
167+
168+
return sorted(paths)
169+
170+
171+
def should_scan(path: Path, excluded_dirs: set[str]) -> bool:
172+
if path.suffix.lower() not in SCAN_EXTENSIONS:
173+
return False
174+
175+
return not any(part in excluded_dirs for part in path.parts)
176+
177+
178+
def write_summary(
179+
latest_version: str,
180+
findings: list[dict[str, str | int | None]],
181+
failures: list[str],
182+
) -> None:
183+
summary_path = os.environ.get("GITHUB_STEP_SUMMARY")
184+
if not summary_path:
185+
return
186+
187+
lines = [
188+
"# WordPress Tested Up To Check",
189+
"",
190+
f"Latest WordPress major.minor release: `{latest_version}`",
191+
f"Metadata entries checked: `{len(findings)}`",
192+
"",
193+
]
194+
195+
if failures:
196+
lines.append("## Failures")
197+
lines.extend(f"- {failure}" for failure in failures)
198+
else:
199+
lines.append("All Tested up to entries are current.")
200+
201+
with open(summary_path, "a", encoding="utf-8") as summary:
202+
summary.write("\n".join(lines))
203+
summary.write("\n")
204+
205+
206+
def print_github_error(message: str, path: str | None = None, line: int | None = None) -> None:
207+
if path is None:
208+
print(f"::error::{escape_github_command_data(message)}")
209+
return
210+
211+
properties = f"file={escape_github_command_property(path)}"
212+
if line is not None:
213+
properties += f",line={line}"
214+
215+
print(f"::error {properties}::{escape_github_command_data(message)}")
216+
217+
218+
def escape_github_command_property(value: str) -> str:
219+
return (
220+
value.replace("%", "%25")
221+
.replace("\r", "%0D")
222+
.replace("\n", "%0A")
223+
.replace(":", "%3A")
224+
.replace(",", "%2C")
225+
)
226+
227+
228+
def escape_github_command_data(value: str) -> str:
229+
return value.replace("%", "%25").replace("\r", "%0D").replace("\n", "%0A")
230+
231+
232+
if __name__ == "__main__":
233+
try:
234+
sys.exit(main())
235+
except Exception as exc:
236+
print_github_error(str(exc))
237+
sys.exit(1)
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# This workflow checks that WordPress "Tested up to" metadata stays current.
2+
# It scans tracked PHP, Markdown, and text files without assuming repository-specific
3+
# metadata filenames, then compares each value to the latest WordPress major.minor release.
4+
5+
name: WordPress Tested Up To
6+
7+
on:
8+
schedule:
9+
- cron: '23 12 * * *'
10+
11+
concurrency:
12+
group: ${{ github.workflow }}-${{ github.ref }}
13+
cancel-in-progress: true
14+
15+
permissions:
16+
contents: read
17+
18+
jobs:
19+
tested-up-to:
20+
name: Verify Tested Up To Metadata
21+
runs-on: ubuntu-latest
22+
23+
steps:
24+
- name: Checkout code
25+
uses: actions/checkout@v6
26+
27+
- name: Download WordPress version data
28+
run: |
29+
curl --fail --silent --show-error --location \
30+
--proto '=https' --tlsv1.2 \
31+
https://api.wordpress.org/core/version-check/1.7/ \
32+
--output wordpress-version-check.json
33+
34+
- name: Check WordPress Tested Up To metadata
35+
run: python3 .github/scripts/check-wordpress-tested-up-to.py

.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Test and build artifacts
22
/vendor/
3+
/coverage/
4+
/build/
5+
/plugin-check-build/
36

47
# Local private files
58
/.private/
@@ -11,3 +14,7 @@
1114
/.phpcs-cache
1215
/.phpstan.cache/
1316
/.psalm/cache/
17+
18+
# Python tooling caches
19+
__pycache__/
20+
*.py[cod]

.vscode/settings.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"plaintext": true,
66
"markdown": true,
77
"scminput": true,
8-
"shellscript": true
8+
"shellscript": true,
9+
"javascript": true
910
}
1011
}

0 commit comments

Comments
 (0)