diff --git a/image/cli/app-root/src/finalizer.py b/image/cli/app-root/src/finalizer.py index dc2d8afc19..aec27d8d64 100644 --- a/image/cli/app-root/src/finalizer.py +++ b/image/cli/app-root/src/finalizer.py @@ -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 @@ -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__": @@ -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") diff --git a/tekton/src/tasks/fvt/mas-fvt-finalize.yml.j2 b/tekton/src/tasks/fvt/mas-fvt-finalize.yml.j2 index e67617b6c8..9e13a72a9e 100644 --- a/tekton/src/tasks/fvt/mas-fvt-finalize.yml.j2 +++ b/tekton/src/tasks/fvt/mas-fvt-finalize.yml.j2 @@ -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