Skip to content

Commit 3d9d72c

Browse files
author
Bhautik Vala
committed
Merge branch 'stable' into entitlement
2 parents 5699486 + 9b1f2c6 commit 3d9d72c

6 files changed

Lines changed: 191 additions & 6 deletions

File tree

‎setup.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ def get_version(rel_path):
6060
"boto3", # Apache Software License
6161
"slack_sdk", # MIT License
6262
"packaging", # Apache Software License
63+
"cryptography", # Apache Software License — used by mas.devops.github for GHE App JWT auth
6364
],
6465
extras_require={
6566
"dev": [

‎src/mas/devops/data/catalogs/v9-260924-amd64.yaml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,6 @@ editorial:
215215
- IBM Maximo Real Estate and Facilities [v9.1.15](https://www.ibm.com/support/pages/node/7288833) and [v9.2.3](https://www.ibm.com/support/pages/node/7289065)
216216
- IBM Maximo AI Service [v9.1.19](https://www.ibm.com/support/pages/node/7287754) and [v9.2.3](https://www.ibm.com/support/pages/node/7287755)
217217
- IBM Suite License Service v3.13.2
218-
- Mongo: 8.0.30
218+
- 'Mongo: 8.0.30'
219219
known_issues:
220-
- title:
220+
- title: During an upgrade from MVI 9.0.x to 9.1.x, the vision-auth pod may fail to start due to an SSL hostname verification error when connecting to Valkey. Restart the Valkey and Auth deployments in the MVI namespace to resolve the issue.

‎src/mas/devops/data/catalogs/v9-260924-ppc64le.yaml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,6 @@ editorial:
7575
- IBM Maximo Application Suite Core Platform [v9.0.32](https://www.ibm.com/support/pages/node/7289766), [v9.1.26](https://www.ibm.com/support/pages/node/7289767) and [v9.2.7](https://www.ibm.com/support/pages/node/7289768)
7676
- IBM Maximo Manage [v9.0.32](https://www.ibm.com/support/pages/node/7289352),[v9.1.25](https://www.ibm.com/support/pages/node/7289351) and [v9.2.4](https://www.ibm.com/support/pages/node/7289350)
7777
- IBM Suite License Service v3.13.2
78-
- Mongo: 8.0.30
78+
- 'Mongo: 8.0.30'
7979
known_issues:
80-
- title:
80+
- title: During an upgrade from MVI 9.0.x to 9.1.x, the vision-auth pod may fail to start due to an SSL hostname verification error when connecting to Valkey. Restart the Valkey and Auth deployments in the MVI namespace to resolve the issue.

‎src/mas/devops/data/catalogs/v9-260924-s390x.yaml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ editorial:
7575
- IBM Maximo Application Suite Core Platform [v9.0.32](https://www.ibm.com/support/pages/node/7289766), [v9.1.26](https://www.ibm.com/support/pages/node/7289767) and [v9.2.7](https://www.ibm.com/support/pages/node/7289768)
7676
- IBM Maximo Manage [v9.0.32](https://www.ibm.com/support/pages/node/7289352),[v9.1.25](https://www.ibm.com/support/pages/node/7289351) and [v9.2.4](https://www.ibm.com/support/pages/node/7289350)
7777
- IBM Suite License Service v3.13
78-
- Mongo: 8.0.30
78+
- 'Mongo: 8.0.30'
7979
known_issues:
80-
- title:
80+
- title: During an upgrade from MVI 9.0.x to 9.1.x, the vision-auth pod may fail to start due to an SSL hostname verification error when connecting to Valkey. Restart the Valkey and Auth deployments in the MVI namespace to resolve the issue.
8181

‎src/mas/devops/github.py‎

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
# *****************************************************************************
2+
# Copyright (c) 2026 IBM Corporation and other Contributors.
3+
#
4+
# All rights reserved. This program and the accompanying materials
5+
# are made available under the terms of the Eclipse Public License v1.0
6+
# which accompanies this distribution, and is available at
7+
# http://www.eclipse.org/legal/epl-v10.html
8+
#
9+
# *****************************************************************************
10+
11+
"""GitHub Checks API helpers for FVT result reporting.
12+
13+
Provides createCheckRun() and updateCheckRun() to post check run status
14+
against specific commits on github.ibm.com, using a GitHub App for auth.
15+
16+
Authentication is handled automatically. Set GITHUB_APP_PRIVATE_KEY in the
17+
environment; an installation token is fetched, cached, and refreshed
18+
transparently.
19+
20+
Environment variables:
21+
GITHUB_APP_PRIVATE_KEY (required) PEM-encoded RSA private key.
22+
GITHUB_API_BASE (optional) Override API base URL.
23+
Defaults to https://github.ibm.com/api/v3
24+
GITHUB_APP_ID (optional) Override app ID. Defaults to 6035.
25+
GITHUB_APP_INSTALLATION_ID (optional) Skip installation lookup.
26+
"""
27+
28+
import base64
29+
import json
30+
import logging
31+
import os
32+
import time
33+
import urllib.error
34+
import urllib.request
35+
from dataclasses import dataclass, field
36+
from datetime import datetime, timezone
37+
38+
from cryptography.hazmat.primitives import hashes, serialization
39+
from cryptography.hazmat.primitives.asymmetric import padding
40+
41+
logger = logging.getLogger(__name__)
42+
43+
_GITHUB_API_BASE = os.environ.get("GITHUB_API_BASE", "https://github.ibm.com/api/v3")
44+
_GITHUB_APP_ID = os.environ.get("GITHUB_APP_ID", "6035")
45+
46+
47+
# ---------------------------------------------------------------------------
48+
# Token cache
49+
# ---------------------------------------------------------------------------
50+
@dataclass
51+
class _TokenCache:
52+
token: str = ""
53+
expiresAt: float = field(default_factory=float)
54+
55+
56+
_tokenCache: dict[str, _TokenCache] = {}
57+
58+
59+
# ---------------------------------------------------------------------------
60+
# Internal helpers
61+
# ---------------------------------------------------------------------------
62+
def _buildJwt(privateKeyPem: str) -> str:
63+
"""Build a signed RS256 JWT to authenticate as the GitHub App."""
64+
key = serialization.load_pem_private_key(privateKeyPem.encode(), password=None)
65+
now = int(time.time())
66+
header = base64.urlsafe_b64encode(json.dumps({"alg": "RS256", "typ": "JWT"}).encode()).rstrip(b"=")
67+
payload = base64.urlsafe_b64encode(json.dumps({"iat": now - 60, "exp": now + 540, "iss": _GITHUB_APP_ID}).encode()).rstrip(b"=")
68+
message = header + b"." + payload
69+
sig = base64.urlsafe_b64encode(key.sign(message, padding.PKCS1v15(), hashes.SHA256())).rstrip(b"=")
70+
return (message + b"." + sig).decode()
71+
72+
73+
def _apiRequest(method: str, url: str, token: str, payload: dict | None = None, isJwt: bool = False) -> dict:
74+
"""Send an authenticated request to the GitHub API."""
75+
data = json.dumps(payload).encode() if payload else None
76+
authScheme = "Bearer" if isJwt else "token"
77+
req = urllib.request.Request(
78+
url,
79+
data=data,
80+
method=method,
81+
headers={
82+
"Accept": "application/vnd.github+json",
83+
"Authorization": f"{authScheme} {token}",
84+
"X-GitHub-Api-Version": "2022-11-28",
85+
"Content-Type": "application/json",
86+
},
87+
)
88+
try:
89+
with urllib.request.urlopen(req) as resp:
90+
return json.loads(resp.read().decode())
91+
except urllib.error.HTTPError as e:
92+
raise RuntimeError(f"GHE API {method} {url} → {e.code}: {e.read().decode(errors='replace')}") from e
93+
except urllib.error.URLError as e:
94+
raise RuntimeError(f"GHE API request failed: {e.reason}") from e
95+
96+
97+
def _getInstallationToken(org: str) -> str:
98+
"""Return a valid installation token for *org*, refreshing when near expiry."""
99+
cache = _tokenCache.get(org)
100+
if cache and cache.token and time.time() < cache.expiresAt - 60:
101+
return cache.token
102+
103+
privateKeyPem = os.environ.get("GITHUB_APP_PRIVATE_KEY")
104+
if not privateKeyPem:
105+
raise RuntimeError("GITHUB_APP_PRIVATE_KEY environment variable is not set")
106+
107+
jwt = _buildJwt(privateKeyPem)
108+
109+
# Resolve installation ID for this org (or use explicit override)
110+
installationId = os.environ.get("GITHUB_APP_INSTALLATION_ID")
111+
if not installationId:
112+
installations = _apiRequest("GET", f"{_GITHUB_API_BASE}/app/installations", jwt, isJwt=True)
113+
for inst in installations:
114+
if inst.get("account", {}).get("login", "").lower() == org.lower():
115+
installationId = str(inst["id"])
116+
break
117+
if not installationId:
118+
raise RuntimeError(f"No GHE App installation found for org '{org}'")
119+
120+
body = _apiRequest("POST", f"{_GITHUB_API_BASE}/app/installations/{installationId}/access_tokens", jwt, payload={}, isJwt=True)
121+
token = body.get("token")
122+
if not token:
123+
raise RuntimeError(f"No token in GHE access_tokens response: {body}")
124+
125+
_tokenCache[org] = _TokenCache(token=token, expiresAt=time.time() + 3600)
126+
logger.debug("Fetched GHE installation token for org=%s", org)
127+
return token
128+
129+
130+
# ---------------------------------------------------------------------------
131+
# Public API
132+
# ---------------------------------------------------------------------------
133+
def createCheckRun(name: str, repoSlug: str, commitSha: str, detailsUrl: str = "") -> int:
134+
"""Create a GitHub Check Run in 'in_progress' state. Returns the check run ID."""
135+
org = repoSlug.split("/")[0]
136+
appToken = _getInstallationToken(org)
137+
isoNow = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
138+
payload: dict = {"name": name, "head_sha": commitSha, "status": "in_progress", "started_at": isoNow}
139+
if detailsUrl:
140+
payload["details_url"] = detailsUrl
141+
response = _apiRequest("POST", f"{_GITHUB_API_BASE}/repos/{repoSlug}/check-runs", appToken, payload)
142+
checkRunId = response.get("id")
143+
if not checkRunId:
144+
raise RuntimeError(f"Failed to create check run '{name}': {response}")
145+
logger.debug("Created check run id=%s for %s@%s", checkRunId, repoSlug, commitSha[:8])
146+
return checkRunId
147+
148+
149+
def updateCheckRun(checkRunId: int, repoSlug: str, conclusion: str, detailsUrl: str = "", outputTitle: str = "", outputSummary: str = "") -> None:
150+
"""Complete an existing GitHub Check Run with the given conclusion."""
151+
org = repoSlug.split("/")[0]
152+
appToken = _getInstallationToken(org)
153+
isoNow = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
154+
payload: dict = {"status": "completed", "conclusion": conclusion, "completed_at": isoNow}
155+
if detailsUrl:
156+
payload["details_url"] = detailsUrl
157+
if outputTitle and outputSummary:
158+
payload["output"] = {"title": outputTitle, "summary": outputSummary}
159+
_apiRequest("PATCH", f"{_GITHUB_API_BASE}/repos/{repoSlug}/check-runs/{checkRunId}", appToken, payload)
160+
logger.debug("Updated check run id=%s conclusion=%s", checkRunId, conclusion)
161+
162+
163+
def findCheckRun(name: str, repoSlug: str, commitSha: str) -> int | None:
164+
"""Return the ID of the most recent existing check run matching name+commit, or None.
165+
166+
Used to upsert — update an existing check run rather than creating a duplicate.
167+
"""
168+
org = repoSlug.split("/")[0]
169+
appToken = _getInstallationToken(org)
170+
url = f"{_GITHUB_API_BASE}/repos/{repoSlug}/commits/{commitSha}/check-runs"
171+
response = _apiRequest("GET", url, appToken)
172+
for run in response.get("check_runs", []):
173+
if run.get("name") == name:
174+
logger.debug("Found existing check run id=%s name=%s", run["id"], name)
175+
return run["id"]
176+
return None

‎src/mas/devops/templates/pipelinerun-upgrade.yml.j2‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,14 @@ spec:
4141
- name: skip_pre_check
4242
value: "{{ skip_pre_check }}"
4343
{%- endif %}
44+
{%- if storage_class_rwo is defined and storage_class_rwo != "" %}
45+
- name: storage_class_rwo
46+
value: "{{ storage_class_rwo }}"
47+
{%- endif %}
48+
{%- if storage_class_rwx is defined and storage_class_rwx != "" %}
49+
- name: storage_class_rwx
50+
value: "{{ storage_class_rwx }}"
51+
{%- endif %}
4452
{%- if db2_action_system == "install" or db2_action_manage == "install" %}
4553

4654
# Dependencies - Db2 - Actions

0 commit comments

Comments
 (0)