Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
188 changes: 188 additions & 0 deletions image/cli/app-root/src/finalizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from kubernetes.client import Configuration
from openshift.dynamic import DynamicClient
from mas.devops.slack import SlackUtil
from mas.devops.github import createCheckRun, updateCheckRun, findCheckRun
from mas.devops.mas import getPermissionMode
from subprocess import PIPE, Popen, TimeoutExpired
from mobilever import MobVer
Expand Down Expand Up @@ -165,6 +166,164 @@ def getcp4dCompsVersions():
print(f"Unable to determine Cognos Analytics version: {e}")


# GHE repo slugs for each product operator — used to post check runs against
# the commit that was deployed when FVT ran.
# Format: productId (as used in knownProductIds) -> "org/repo" on github.ibm.com
# -----------------------------------------------------------------------------
PRODUCT_REPO_SLUGS = {
"ibm-mas": "maximoappsuite/ibm-mas",
"ibm-mas-assist": "maximoappsuite/ibm-mas-assist",
"ibm-mas-iot": "maximoappsuite/ibm-mas-iot",
"ibm-mas-manage": "maximoappsuite/ibm-mas-manage",
"ibm-mas-facilities": "maximoappsuite/ibm-mas-facilities",
"ibm-mas-monitor": "maximoappsuite/ibm-mas-monitor",
"ibm-mas-optimizer": "maximoappsuite/ibm-mas-optimizer",
"ibm-mas-predict": "maximoappsuite/ibm-mas-predict",
"ibm-mas-visualinspection": "maximoappsuite/ibm-mas-visualinspection",
"ibm-mas-data-dictionary": "maximoappsuite/ibm-mas-data-dictionary",
"ibm-sls": "maximoappsuite/ibm-sls",
"ibm-aiservice": "maximoappsuite/ibm-aiservice",
}


def publish_fvt_check_run(productId, commitId, repoSlug, detailsUrl, runId, productData, runDoc):
"""Post (or update) a GHE check run against the product repo commit that was under test.

Check name is 'FVT Results / {instanceId}' so each environment gets its own
distinct check on the commit. If a check with that name already exists on the
commit it is updated in-place rather than creating a duplicate.

Conclusion is 'failure' when there are test failures/errors (visible indicator
for developers), but the check is never added to branch protection rules so it
will never block a PR merge.

Args:
productId (str): e.g. 'ibm-mas-manage'
commitId (str): full SHA from the mas.ibm.com/commitId deployment label
repoSlug (str): 'org/repo' on github.ibm.com
detailsUrl (str): URL of the FVT dashboard run (used as the Details link)
runId (str): human-readable run identifier, e.g. 'usfvt:5124'
productData (dict): product entry from the MongoDB run document
runDoc (dict): full MongoDB run document (for run-level fields like timestamp)
"""
version = productData.get("version", "unknown")
instanceId, build = runId.split(":", 1)
checkName = f"FVT Results Check ({instanceId})"

# Format run timestamp — shown in the heading as e.g. "Sep 25 09:31"
run_ts = runDoc.get("timestamp") if runDoc else None
if run_ts:
build_date = run_ts.strftime("%b %d %H:%M")
else:
build_date = ""

# Aggregate totals and build per-suite table rows
total_tests = total_failures = total_errors = total_skipped = 0
suite_rows = []
for suite, r in sorted(productData.get("results", {}).items()):
t, f, e, s = r.get("tests", 0), r.get("failures", 0), r.get("errors", 0), r.get("skipped", 0)
total_tests += t
total_failures += f
total_errors += e
total_skipped += s
icon = "🔴" if f > 0 else ("🟡" if e > 0 else "🟢")
suite_url = f"https://dashboard.ibmmas.com/tests/{instanceId}/testsuite/{productId}/{suite}"
suite_rows.append(f"| {icon} [{suite}]({suite_url}) | {t} | {f} | {e} | {s} |")

# Conclusion — failure when tests failed/errored so developers get a clear
# red indicator on the commit, but this check is never added to branch
# protection rules so it will never block a PR merge.
if total_failures > 0 or total_errors > 0:
conclusion = "failure"
status_icon = "❌"
else:
conclusion = "success"
status_icon = "✅"

# Title (output.title) — rendered by GHE as a large heading above the body.
# This is the ❌/✅ status line the user sees first.
# Use "{instanceId}#{build}" with no space so GitHub doesn't auto-link #build as a PR.
build_date_suffix = f" | {build_date}" if build_date else ""
output_title = f"{status_icon} FVT Results — {instanceId}#{build}{build_date_suffix}"

# Subtitle (first line of output.summary body) — the stats line shown below the title.
if total_tests == 0:
stats_line = f"No test results recorded for {instanceId} #{build}"
elif total_failures > 0 or total_errors > 0:
stats_line = f"{total_failures} failures, {total_errors} errors — {total_tests} tests run on {instanceId} #{build}"
else:
stats_line = f"All {total_tests} tests passed on {instanceId} #{build}"

# Body — full markdown rendered on the check run detail page.
dashboard_url = f"https://dashboard.ibmmas.com/tests/{instanceId}"
lines = [
f"**{stats_line}**",
"",
"> This check is automatically posted by the MAS Functional Verification Test (FVT) pipeline.",
f"> It shows the test outcome for the exact commit that was deployed and tested on environment **{instanceId}**.",
"",
"---",
"",
"| | |",
"| --- | --- |",
f"| **Product** | `{productId}` v{version} |",
f"| **Run** | [{instanceId}#{build}]({dashboard_url}) |",
f"| **Commit** | `{commitId[:12]}` |",
f"| **Results** | {total_tests} tests · {total_failures} failures · {total_errors} errors · {total_skipped} skipped |",
"",
]

if suite_rows:
lines += [
"### Suite Breakdown",
"",
"| Suite | Tests | Failures | Errors | Skipped |",
"| ----- | ----: | -------: | -----: | ------: |",
] + suite_rows
else:
lines.append("_No suite results were recorded for this product in this run._")

lines += [
"",
"---",
"",
f"[View full results on the FVT Dashboard]({detailsUrl})",
]

try:
# Upsert — update existing check run for this instance+commit if one exists,
# otherwise create a new one. Prevents duplicate checks on repeat runs.
existing_id = findCheckRun(name=checkName, repoSlug=repoSlug, commitSha=commitId)
if existing_id:
updateCheckRun(
checkRunId=existing_id,
repoSlug=repoSlug,
conclusion=conclusion,
detailsUrl=detailsUrl,
outputTitle=output_title,
outputSummary="\n".join(lines),
)
print(f"GHE check run updated: {repoSlug}@{commitId[:8]} [{checkName}] (id={existing_id})")
else:
check_run_id = createCheckRun(
name=checkName,
repoSlug=repoSlug,
commitSha=commitId,
detailsUrl=detailsUrl,
)
updateCheckRun(
checkRunId=check_run_id,
repoSlug=repoSlug,
conclusion=conclusion,
detailsUrl=detailsUrl,
outputTitle=output_title,
outputSummary="\n".join(lines),
)
print(f"GHE check run posted: {repoSlug}@{commitId[:8]} [{checkName}] (id={check_run_id})")
except Exception as e:
print(f"Failed to post GHE check run for {productId} ({repoSlug}@{commitId[:8]}): {e}")


# Script start
# -----------------------------------------------------------------------------
if __name__ == "__main__":
Expand Down Expand Up @@ -637,6 +796,35 @@ def getcp4dCompsVersions():
else:
print("Run information NOT updated in MongoDb because DRY_RUN is set")

# Publish GHE check runs linked to each product repo commit
# Runs independently of Slack/Jira — only needs GITHUB_APP_PRIVATE_KEY and MongoDB.
# Only runs for instances in GHE_FVT_INSTANCES (comma-separated env var).
# Falls back to a hardcoded default list when the env var is not set.
# Failures are non-fatal — never aborts the rest of the script.
# -------------------------------------------------------------------------
_DEFAULT_GHE_INSTANCES = ["fvtcore", "fvtcpd"]
ghe_instances_raw = os.getenv("GHE_FVT_INSTANCES", "")
ghe_instances = [i.strip() for i in ghe_instances_raw.split(",") if i.strip()] or _DEFAULT_GHE_INSTANCES

if not os.getenv("GITHUB_APP_PRIVATE_KEY"):
print("GITHUB_APP_PRIVATE_KEY not set, skipping GHE check runs")
elif setFinished.lower() != "true":
print("FVT run not yet finished, skipping GHE check runs")
elif not ghe_instances:
print("GHE_FVT_INSTANCES not set, skipping GHE check runs")
elif instanceId not in ghe_instances:
print(f"Instance '{instanceId}' not in GHE_FVT_INSTANCES, skipping GHE check runs")
else:
details_url = os.getenv("TOOLCHAIN_PIPELINERUN_URL", f"https://dashboard.ibmmas.com/tests/{instanceId}")
ghe_result = db.runsv2.find_one({"_id": runId})
for productId, repoSlug in PRODUCT_REPO_SLUGS.items():
productData = ghe_result.get("products", {}).get(productId, {}) if ghe_result else {}
commitId = productData.get("commitId")
if not commitId:
print(f"No commitId for {productId}, skipping GHE check run")
continue
publish_fvt_check_run(productId, commitId, repoSlug, details_url, runId, productData, ghe_result)

# Check pre-reqs for Slack integration
# -------------------------------------------------------------------------
SLACK_TOKEN = os.getenv("SLACK_TOKEN")
Expand Down
6 changes: 6 additions & 0 deletions tekton/src/tasks/fvt/mas-fvt-finalize.yml.j2
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,9 @@ spec:
name: mas-fvt
key: FVT_JIRA_TOKEN
optional: true
- name: GITHUB_APP_PRIVATE_KEY
valueFrom:
secretKeyRef:
name: mas-fvt
key: GITHUB_APP_PRIVATE_KEY
optional: true
Loading