Skip to content

Commit 38a6abc

Browse files
fuzyllElykDeer
authored andcommitted
Make validator able to be used outside of CI.
1 parent fe7ab36 commit 38a6abc

1 file changed

Lines changed: 208 additions & 41 deletions

File tree

validate_json.py

Lines changed: 208 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,223 @@
1+
import argparse
2+
import base64
3+
import json
14
import os
5+
import re
26
import sys
7+
38
import requests
4-
import base64
5-
import json
69

710

8-
token = sys.argv[1]
11+
GITHUB_REPO_PATTERN = re.compile(
12+
r"https://github\.com/([A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+)"
13+
)
14+
15+
16+
def make_headers(token):
17+
headers = {"Accept": "application/vnd.github+json"}
18+
if token:
19+
headers["Authorization"] = f"token {token}"
20+
return headers
21+
22+
23+
def github_get_json(url, token):
24+
response = requests.get(url, headers=make_headers(token), timeout=20)
25+
try:
26+
payload = response.json()
27+
except ValueError:
28+
payload = None
29+
return response, payload
30+
31+
32+
def extract_repo(value):
33+
candidate = value.strip().rstrip("/")
34+
if candidate.startswith("https://github.com/"):
35+
match = GITHUB_REPO_PATTERN.search(candidate)
36+
if match:
37+
return match.group(1).lower()
38+
return None
39+
40+
if candidate.count("/") == 1 and " " not in candidate:
41+
return candidate.lower()
42+
return None
43+
944

10-
def getfile(url):
11-
return requests.get(url, headers={'Authorization': f'token {token}'})
45+
def repo_from_issue_content(issue_content):
46+
lines = issue_content.splitlines()
1247

13-
issue_content = os.environ.get("ISSUE_CONTENT")
48+
for line in lines:
49+
if line.startswith("Repo URL:"):
50+
repo = extract_repo(line.split("Repo URL:", 1)[1])
51+
if repo:
52+
return repo
1453

15-
lines = issue_content.split("\n")
54+
for line in lines:
55+
repo = extract_repo(line)
56+
if repo:
57+
return repo
1658

17-
def validate_repo(line):
18-
repo = line.split(" ")[-1].lower() # handle both cases of just URL or with Repo URL: before
19-
repo = repo.replace("https://github.com/","").strip().strip("/") # just get the user/project portion
20-
projectUrl = f"https://api.github.com/repos/{repo}"
21-
latestRelease = f"{projectUrl}/releases/latest"
22-
tagsUrl = f"{projectUrl}/tags"
59+
return None
60+
61+
62+
def validate_local_plugin_json(path):
2363
try:
24-
releaseData = getfile(latestRelease).json()
25-
26-
match releaseData.get('message'):
27-
case 'Not Found':
28-
print(f"\n\nERROR: {plugin['name']}, Couldn't get release information. Likely the user created a tag but no associated release.\n")
29-
sys.exit(-1)
30-
case 'Bad credentials':
31-
print("\n\nERROR: Bad credentials, check access token.\n")
32-
sys.exit(-1)
33-
34-
except:
35-
print(f"\n\nFailed to load valid release data json from {latestRelease}")
36-
sys.exit(-1)
64+
with open(path, "r", encoding="utf-8") as f:
65+
json.load(f)
66+
except FileNotFoundError:
67+
print(f"ERROR: Local file not found: {path}")
68+
return False
69+
except json.JSONDecodeError as e:
70+
print(f"ERROR: Invalid JSON in {path}: {e}")
71+
return False
72+
except OSError as e:
73+
print(f"ERROR: Could not read {path}: {e}")
74+
return False
75+
76+
print(f"OK: Local JSON is valid: {path}")
77+
return True
78+
79+
80+
def validate_remote_repo(repo, token):
81+
project_url = f"https://api.github.com/repos/{repo}"
82+
latest_release_url = f"{project_url}/releases/latest"
83+
84+
release_response, release_data = github_get_json(latest_release_url, token)
85+
if release_response.status_code == 401:
86+
print("ERROR: Bad credentials, check access token.")
87+
return False
88+
if release_response.status_code == 404:
89+
print(
90+
"ERROR: Could not get release information. "
91+
"Likely the repo has tags but no associated release, or the repo is private."
92+
)
93+
return False
94+
if not release_response.ok:
95+
print(
96+
f"ERROR: Failed to fetch release data ({release_response.status_code}) from {latest_release_url}"
97+
)
98+
return False
99+
if not isinstance(release_data, dict):
100+
print(f"ERROR: Failed to parse release data JSON from {latest_release_url}")
101+
return False
102+
103+
tag = release_data.get("tag_name")
104+
if not tag:
105+
print("ERROR: Latest release did not contain a tag_name.")
106+
return False
107+
108+
plugin_json_url = f"{project_url}/contents/plugin.json?ref={tag}"
109+
plugin_response, plugin_data = github_get_json(plugin_json_url, token)
110+
if plugin_response.status_code == 404:
111+
print(f"ERROR: plugin.json not found for release tag '{tag}'.")
112+
return False
113+
if not plugin_response.ok:
114+
print(
115+
f"ERROR: Failed to fetch plugin.json ({plugin_response.status_code}) from {plugin_json_url}"
116+
)
117+
return False
118+
if not isinstance(plugin_data, dict):
119+
print(f"ERROR: Failed to parse plugin.json metadata from {plugin_json_url}")
120+
return False
121+
122+
encoded_content = plugin_data.get("content")
123+
if not encoded_content:
124+
print(
125+
f"ERROR: plugin.json metadata did not include file content at {plugin_json_url}"
126+
)
127+
return False
128+
37129
try:
38-
tag = releaseData['tag_name']
39-
pluginjsonurl = f"{projectUrl}/contents/plugin.json?ref={tag}"
40-
content = getfile(pluginjsonurl).json()['content']
41-
jsoncontent = json.loads(base64.b64decode(content))
42-
except:
43-
print(f"\n\nFailed to parse valid plugin.json from https://github.com/{repo}/blob/master/plugin.json")
44-
sys.exit(-1)
45-
sys.exit(0)
130+
decoded = base64.b64decode(encoded_content.replace("\n", ""), validate=True)
131+
json.loads(decoded)
132+
except (ValueError, json.JSONDecodeError) as e:
133+
print(f"ERROR: plugin.json in {repo} at tag '{tag}' is not valid JSON: {e}")
134+
return False
135+
136+
print(f"OK: Remote plugin.json is valid for {repo} at tag '{tag}'.")
137+
return True
138+
139+
140+
def parse_args():
141+
parser = argparse.ArgumentParser(
142+
description="Validate plugin.json from GitHub issue content, repo URL, or a local file."
143+
)
144+
parser.add_argument(
145+
"legacy_token",
146+
nargs="?",
147+
help="GitHub token (legacy positional argument, kept for CI compatibility).",
148+
)
149+
parser.add_argument("--token", help="GitHub token. Optional for public repos.")
150+
parser.add_argument(
151+
"--issue-content", help="Issue body text containing a repo URL."
152+
)
153+
parser.add_argument(
154+
"--issue-content-file",
155+
help="Path to a file containing issue body text.",
156+
)
157+
parser.add_argument(
158+
"--repo-url",
159+
help="Repository URL or owner/repo string to validate directly.",
160+
)
161+
parser.add_argument(
162+
"--plugin-json",
163+
help="Path to a local plugin.json file to validate directly.",
164+
)
165+
return parser.parse_args()
166+
167+
168+
def main():
169+
args = parse_args()
170+
171+
token = args.token or args.legacy_token or os.environ.get("GITHUB_TOKEN")
172+
173+
issue_content = args.issue_content
174+
if not issue_content and args.issue_content_file:
175+
try:
176+
with open(args.issue_content_file, "r", encoding="utf-8") as f:
177+
issue_content = f.read()
178+
except OSError as e:
179+
print(f"ERROR: Failed to read --issue-content-file: {e}")
180+
return 1
181+
182+
if not issue_content:
183+
issue_content = os.environ.get("ISSUE_CONTENT")
184+
185+
checks_run = 0
186+
failures = 0
187+
188+
if args.plugin_json:
189+
checks_run += 1
190+
if not validate_local_plugin_json(args.plugin_json):
191+
failures += 1
192+
193+
repo = None
194+
if args.repo_url:
195+
repo = extract_repo(args.repo_url)
196+
if not repo:
197+
print(
198+
"ERROR: Could not parse --repo-url. Use https://github.com/owner/repo or owner/repo."
199+
)
200+
return 1
201+
elif issue_content:
202+
repo = repo_from_issue_content(issue_content)
203+
if not repo:
204+
print("ERROR: Could not find a GitHub repo URL in issue content.")
205+
return 1
206+
207+
if repo:
208+
checks_run += 1
209+
if not validate_remote_repo(repo, token):
210+
failures += 1
46211

47-
for line in lines:
48-
if line.startswith("Repo URL:"):
49-
validate_repo(line)
212+
if checks_run == 0:
213+
print(
214+
"ERROR: Nothing to validate. Provide --plugin-json, --repo-url, --issue-content, "
215+
"--issue-content-file, or set ISSUE_CONTENT."
216+
)
217+
return 1
50218

51-
# Failed to find a repo line, just look for the first github URL:
219+
return 1 if failures else 0
52220

53-
for line in lines:
54-
if line.startswith("https://github.com/"):
55-
validate_repo(line)
56221

222+
if __name__ == "__main__":
223+
sys.exit(main())

0 commit comments

Comments
 (0)