Skip to content

Commit 41ce491

Browse files
committed
ci: externalize release workflow helper scripts
1 parent 01d003d commit 41ce491

6 files changed

Lines changed: 600 additions & 18 deletions

File tree

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
#!/usr/bin/env python3
2+
"""Extract uploaded artifact URLs from Maven deploy logs."""
3+
4+
from __future__ import annotations
5+
6+
import json
7+
import os
8+
import re
9+
from pathlib import Path
10+
11+
12+
def write_output(key: str, value: str) -> None:
13+
output_path = os.environ.get("GITHUB_OUTPUT", "").strip()
14+
if not output_path:
15+
return
16+
with open(output_path, "a", encoding="utf-8") as out:
17+
out.write(f"{key}={value}\n")
18+
19+
20+
def main() -> int:
21+
repository_name = os.environ.get("TARGET_REPOSITORY", "").strip()
22+
log_file = Path(os.environ.get("DEPLOY_LOG", "maven-deploy.log"))
23+
context_file = Path(os.environ.get("DEPLOY_ARTIFACTS_FILE", "deploy-artifacts.json"))
24+
25+
log_text = log_file.read_text(encoding="utf-8")
26+
pattern = re.compile(r"Uploaded to (\S+):\s+(\S+)")
27+
28+
uploaded_urls: list[str] = []
29+
for target_repo, url in pattern.findall(log_text):
30+
normalized = target_repo.rstrip(":")
31+
if normalized == repository_name:
32+
uploaded_urls.append(url)
33+
34+
deduped_urls = sorted(set(uploaded_urls))
35+
jar_urls = [
36+
url for url in deduped_urls
37+
if url.endswith(".jar") and not url.endswith(".jar.asc")
38+
]
39+
pom_urls = [
40+
url for url in deduped_urls
41+
if url.endswith(".pom") and not url.endswith(".pom.asc")
42+
]
43+
44+
payload = {
45+
"target_repository": repository_name,
46+
"uploaded_urls": deduped_urls,
47+
"jar_urls": jar_urls,
48+
"pom_urls": pom_urls,
49+
}
50+
context_file.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
51+
52+
write_output("uploaded_urls_count", str(len(deduped_urls)))
53+
write_output("jar_urls_count", str(len(jar_urls)))
54+
write_output("pom_urls_count", str(len(pom_urls)))
55+
return 0
56+
57+
58+
if __name__ == "__main__":
59+
raise SystemExit(main())
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
#!/usr/bin/env python3
2+
"""Resolve Sonatype repository context for release deployments."""
3+
4+
from __future__ import annotations
5+
6+
import base64
7+
import json
8+
import os
9+
import urllib.error
10+
import urllib.parse
11+
import urllib.request
12+
from pathlib import Path
13+
from typing import Any
14+
15+
OSSRH_BASE = "https://ossrh-staging-api.central.sonatype.com"
16+
17+
18+
def request_json(url: str, headers: dict[str, str]) -> tuple[int | None, dict[str, Any]]:
19+
request = urllib.request.Request(url=url, method="GET", headers=headers)
20+
try:
21+
with urllib.request.urlopen(request, timeout=30) as response:
22+
body = response.read().decode("utf-8")
23+
if not body:
24+
return response.status, {}
25+
return response.status, json.loads(body)
26+
except urllib.error.HTTPError as error:
27+
try:
28+
payload = json.loads(error.read().decode("utf-8"))
29+
except Exception: # noqa: BLE001
30+
payload = {"error": f"HTTP {error.code}"}
31+
payload.setdefault("error", f"HTTP {error.code}")
32+
return error.code, payload
33+
except Exception as error: # noqa: BLE001
34+
return None, {"error": str(error)}
35+
36+
37+
def write_output(key: str, value: str) -> None:
38+
output_path = os.environ.get("GITHUB_OUTPUT", "").strip()
39+
if not output_path:
40+
return
41+
with open(output_path, "a", encoding="utf-8") as stream:
42+
stream.write(f"{key}={value}\n")
43+
44+
45+
def main() -> int:
46+
target_repository = os.environ.get("TARGET_REPOSITORY", "").strip()
47+
namespace = os.environ.get("TARGET_NAMESPACE", "").strip()
48+
username = os.environ.get("MAVEN_USERNAME", "")
49+
password = os.environ.get("MAVEN_CENTRAL_TOKEN", "")
50+
context_path = Path(
51+
os.environ.get("REPOSITORY_CONTEXT_FILE", "repository-context.json")
52+
)
53+
54+
context: dict[str, Any] = {
55+
"target_repository": target_repository,
56+
"namespace": namespace,
57+
"status": "not_applicable",
58+
"reason": "repository input is not releases",
59+
"repository_key": "",
60+
"portal_deployment_id": "",
61+
"search_candidates": [],
62+
}
63+
64+
if target_repository == "releases":
65+
if not username or not password:
66+
context["status"] = "manual_required"
67+
context["reason"] = "Missing MAVEN_USERNAME/MAVEN_CENTRAL_TOKEN"
68+
else:
69+
token = base64.b64encode(f"{username}:{password}".encode("utf-8")).decode("utf-8")
70+
headers = {
71+
"Authorization": f"Bearer {token}",
72+
"Accept": "application/json",
73+
}
74+
75+
searches = [
76+
("open", "client"),
77+
("closed", "client"),
78+
("open", "any"),
79+
("closed", "any"),
80+
]
81+
selected: dict[str, Any] | None = None
82+
last_error = ""
83+
84+
for state, ip in searches:
85+
url = (
86+
f"{OSSRH_BASE}/manual/search/repositories?"
87+
f"profile_id={urllib.parse.quote(namespace)}"
88+
f"&state={urllib.parse.quote(state)}"
89+
f"&ip={urllib.parse.quote(ip)}"
90+
)
91+
status, payload = request_json(url, headers)
92+
if status is None:
93+
last_error = payload.get("error", "unknown error")
94+
continue
95+
96+
repositories = (
97+
payload.get("repositories", []) if isinstance(payload, dict) else []
98+
)
99+
context["search_candidates"].append(
100+
{"state": state, "ip": ip, "count": len(repositories)}
101+
)
102+
if repositories:
103+
selected = repositories[0]
104+
break
105+
106+
if selected:
107+
context["status"] = "resolved"
108+
context["reason"] = ""
109+
context["repository_key"] = selected.get("key", "") or ""
110+
context["portal_deployment_id"] = (
111+
selected.get("portal_deployment_id", "") or ""
112+
)
113+
else:
114+
context["status"] = "manual_required"
115+
context["reason"] = last_error or "No staging repository key found"
116+
117+
context_path.write_text(json.dumps(context, indent=2) + "\n", encoding="utf-8")
118+
write_output("repository_key", context.get("repository_key", ""))
119+
write_output("portal_deployment_id", context.get("portal_deployment_id", ""))
120+
write_output("status", context.get("status", ""))
121+
write_output("reason", context.get("reason", ""))
122+
return 0
123+
124+
125+
if __name__ == "__main__":
126+
raise SystemExit(main())
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
#!/usr/bin/env python3
2+
"""Write release publish context summary for GitHub Actions."""
3+
4+
from __future__ import annotations
5+
6+
import json
7+
import os
8+
from pathlib import Path
9+
from typing import Any
10+
11+
12+
def read_json(path: Path) -> dict[str, Any]:
13+
if not path.exists():
14+
return {}
15+
try:
16+
return json.loads(path.read_text(encoding="utf-8"))
17+
except json.JSONDecodeError:
18+
return {}
19+
20+
21+
def main() -> int:
22+
summary_path = os.environ.get("GITHUB_STEP_SUMMARY", "").strip()
23+
if not summary_path:
24+
return 0
25+
26+
deploy_path = Path(os.environ.get("DEPLOY_ARTIFACTS_FILE", "deploy-artifacts.json"))
27+
repository_path = Path(
28+
os.environ.get("REPOSITORY_CONTEXT_FILE", "repository-context.json")
29+
)
30+
31+
deploy = read_json(deploy_path)
32+
repository = read_json(repository_path)
33+
34+
lines = [
35+
"## Publish Context",
36+
"",
37+
f"- target repository: {deploy.get('target_repository', '')}",
38+
f"- uploaded URLs: {len(deploy.get('uploaded_urls', []))}",
39+
f"- jar URLs: {len(deploy.get('jar_urls', []))}",
40+
f"- pom URLs: {len(deploy.get('pom_urls', []))}",
41+
f"- staging key status: {repository.get('status', '')}",
42+
f"- repository_key: {repository.get('repository_key', '')}",
43+
f"- portal_deployment_id: {repository.get('portal_deployment_id', '')}",
44+
]
45+
reason = repository.get("reason", "")
46+
if reason:
47+
lines.append(f"- reason: {reason}")
48+
49+
with open(summary_path, "a", encoding="utf-8") as output:
50+
output.write("\n".join(lines) + "\n")
51+
return 0
52+
53+
54+
if __name__ == "__main__":
55+
raise SystemExit(main())

0 commit comments

Comments
 (0)