diff --git a/result_server/routes/admin.py b/result_server/routes/admin.py
index 045fd74..beb919d 100644
--- a/result_server/routes/admin.py
+++ b/result_server/routes/admin.py
@@ -285,6 +285,11 @@ def _build_execution_pipeline_plan(store):
result_server_url=_portal_result_server_url(),
target_id=gitlab_target.id if gitlab_target else gitlab_target_id,
)
+ variables = plan.payload.setdefault("variables", {})
+ manual_trigger_id = profile["id"] if profile else profile_id
+ variables["BK_TRIGGER_ID"] = manual_trigger_id
+ variables["BK_TRIGGER_TYPE"] = "manual_button"
+ variables["BK_TRIGGER_REASON"] = f"manual_button:{manual_trigger_id}"
request_errors = []
if not profile_id:
request_errors.append("profile_id is required")
diff --git a/result_server/templates/_results_table.html b/result_server/templates/_results_table.html
index 5404a58..52d426b 100644
--- a/result_server/templates/_results_table.html
+++ b/result_server/templates/_results_table.html
@@ -99,7 +99,8 @@
.ci-pipeline {
display: block;
}
-.ci-pipeline {
+.ci-pipeline,
+.ci-subline {
color: #607282;
font-size: 0.82em;
}
diff --git a/result_server/templates/_results_table_cell_ci.html b/result_server/templates/_results_table_cell_ci.html
index df51656..0534511 100644
--- a/result_server/templates/_results_table_cell_ci.html
+++ b/result_server/templates/_results_table_cell_ci.html
@@ -1,4 +1,8 @@
-
+{% set pipeline_label = row.pipeline_label | default('#' ~ row.pipeline_id if row.pipeline_id != '-' else '-') %}
+ |
{{ row.ci_trigger }}
- #{{ row.pipeline_id }}
+ {{ pipeline_label }}
+ {% if row.ci_subline | default('') %}
+ {{ row.ci_subline }}
+ {% endif %}
|
diff --git a/result_server/tests/test_execution_profiles.py b/result_server/tests/test_execution_profiles.py
index 680e7a9..a214aa9 100644
--- a/result_server/tests/test_execution_profiles.py
+++ b/result_server/tests/test_execution_profiles.py
@@ -1383,6 +1383,9 @@ def test_admin_execution_profiles_dry_run_submit_records_payload(tmp_path, monke
assert variables["code"] == "qws"
assert variables["BK_ALLOCATION_PROJECT_ID"] == "rkp00010"
assert variables["RESULT_SERVER"] == "http://localhost"
+ assert variables["BK_TRIGGER_ID"] == "rikyu-qws-nightly"
+ assert variables["BK_TRIGGER_TYPE"] == "manual_button"
+ assert variables["BK_TRIGGER_REASON"] == "manual_button:rikyu-qws-nightly"
assert "BK_SCHEDULER_EXTRA_ARGS_RIKYU" not in variables
assert "exp" not in variables
assert payload_record["gitlab_project"] == "gitlab.example.org/group/benchkit.git"
@@ -1755,6 +1758,9 @@ def fake_submit(plan, *, token):
assert plan.payload["variables"]["system"] == "Fugaku"
assert plan.payload["variables"]["BK_ALLOCATION_PROJECT_ID"] == "rkp00010"
assert plan.payload["variables"]["RESULT_SERVER"] == "http://localhost"
+ assert plan.payload["variables"]["BK_TRIGGER_ID"] == "rikyu-qws-nightly"
+ assert plan.payload["variables"]["BK_TRIGGER_TYPE"] == "manual_button"
+ assert plan.payload["variables"]["BK_TRIGGER_REASON"] == "manual_button:rikyu-qws-nightly"
assert "exp" not in plan.payload["variables"]
return GitLabPipelineSubmitResult(
status_code=201,
diff --git a/result_server/tests/test_result_detail_template.py b/result_server/tests/test_result_detail_template.py
index 7138d47..25a28d1 100644
--- a/result_server/tests/test_result_detail_template.py
+++ b/result_server/tests/test_result_detail_template.py
@@ -28,6 +28,8 @@ def app():
"FOM_version": "osu-micro-benchmarks.osu_bibw.test_mpi_2",
"node_count": 1,
"cpus_per_node": 2,
+ "pipeline_id": 3208,
+ "parent_pipeline_id": 3207,
"metrics": {
"scalar": {"FOM": 6.47, "other_metric": 1.23},
"vector": {
@@ -100,6 +102,10 @@ def test_meta_info_section(self, app):
assert "6.470" in html
assert "MB/s" in html
assert "CPUs per Node" in html
+ assert "Pipeline ID" in html
+ assert "3208" in html
+ assert "Parent Pipeline ID" in html
+ assert "3207" in html
assert "Back to Results" in html
assert "Results" in html
diff --git a/result_server/tests/test_results_loader.py b/result_server/tests/test_results_loader.py
index a7198f0..60d2086 100644
--- a/result_server/tests/test_results_loader.py
+++ b/result_server/tests/test_results_loader.py
@@ -447,6 +447,7 @@ def test_row_with_pipeline_timing_fields(self, flask_app, tmp_dir):
"build_job": "qws_Fugaku_build",
"run_job": "qws_Fugaku_N1_P4_T12_run",
"pipeline_id": 17026,
+ "parent_pipeline_id": 17025,
}
_write_json(tmp_dir, filename, data)
@@ -463,6 +464,10 @@ def test_row_with_pipeline_timing_fields(self, flask_app, tmp_dir):
assert row["build_job"] == "qws_Fugaku_build"
assert row["run_job"] == "qws_Fugaku_N1_P4_T12_run"
assert row["pipeline_id"] == "17026"
+ assert row["pipeline_label"] == "#17026"
+ assert row["parent_pipeline_id"] == "17025"
+ assert row["ci_title"] == "schedule / child #17026 / parent #17025"
+ assert row["ci_subline"] == "parent #17025"
def test_row_without_pipeline_timing_fields(self, flask_app, tmp_dir):
"""Test case."""
@@ -484,6 +489,10 @@ def test_row_without_pipeline_timing_fields(self, flask_app, tmp_dir):
assert row["build_job"] == "-"
assert row["run_job"] == "-"
assert row["pipeline_id"] == "-"
+ assert row["pipeline_label"] == "-"
+ assert row["parent_pipeline_id"] == ""
+ assert row["ci_title"] == "- / -"
+ assert row["ci_subline"] == ""
def test_row_with_partial_pipeline_timing(self, flask_app, tmp_dir):
"""Test case."""
diff --git a/result_server/tests/test_trigger_display.py b/result_server/tests/test_trigger_display.py
index ac09ced..75d64cd 100644
--- a/result_server/tests/test_trigger_display.py
+++ b/result_server/tests/test_trigger_display.py
@@ -86,6 +86,28 @@ def test_summarize_execution_trigger_falls_back_to_pipeline_lookup():
assert summary["subline"] == "repo/ref changed: https://github.com/RIKEN-LQCD/qws.git@master"
+def test_summarize_execution_trigger_falls_back_to_parent_pipeline_lookup():
+ runs = [
+ {
+ "trigger_id": "qws-fugaku-1400",
+ "trigger_type": "scheduled",
+ "status": "submitted",
+ "reason": "cron:0 14 * * *@2026-08-07T14:00+09:00",
+ "payload_json": {
+ "submit": {"response": {"id": 3189}},
+ },
+ }
+ ]
+
+ summary = summarize_execution_trigger(
+ {"pipeline_id": 3190, "parent_pipeline_id": 3189},
+ build_trigger_run_lookup(runs),
+ )
+
+ assert summary["headline"] == "Scheduled / qws-fugaku-1400"
+ assert summary["subline"] == "cron 0 14 * * * / 2026-08-07T14:00+09:00"
+
+
def test_load_trigger_run_lookup_ignores_newer_routine_runs(tmp_path):
db_path = tmp_path / "cx_portal.sqlite3"
store = ExecutionProfileStore(str(db_path))
@@ -224,3 +246,27 @@ def test_build_trigger_result_links_matches_child_pipeline_fallback(tmp_path):
assert links[8][0]["filename"] == result_file.name
assert links[8][0]["pipeline_id"] == "3190"
+
+
+def test_build_trigger_result_links_matches_parent_pipeline_fallback(tmp_path):
+ result_file = tmp_path / "result_20260807_140611_aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee.json"
+ result_file.write_text(
+ json.dumps({"code": "qws", "Exp": "CASE1", "pipeline_id": 3190, "parent_pipeline_id": 3189}),
+ encoding="utf-8",
+ )
+ runs = [
+ {
+ "id": 9,
+ "trigger_id": "qws-fugaku-1400",
+ "trigger_type": "scheduled",
+ "reason": "cron:0 14 * * *@2026-08-07T14:00+09:00",
+ "payload_json": {
+ "submit": {"response": {"id": 3189}},
+ },
+ }
+ ]
+
+ links = build_trigger_result_links(str(tmp_path), runs)
+
+ assert links[9][0]["filename"] == result_file.name
+ assert links[9][0]["pipeline_id"] == "3190"
diff --git a/result_server/utils/result_detail_view.py b/result_server/utils/result_detail_view.py
index 2cf44f3..27e9d40 100644
--- a/result_server/utils/result_detail_view.py
+++ b/result_server/utils/result_detail_view.py
@@ -27,6 +27,7 @@ def _build_meta_rows(result, trigger_runs_by_pipeline=None):
("FOM", format_numeric_value(result.get("FOM", "N/A"))),
("FOM Unit", result.get("FOM_unit") or "not specified"),
("Node Count", result.get("node_count", "N/A")),
+ ("Pipeline ID", result.get("pipeline_id", "N/A")),
(
"Run Cause",
(
@@ -41,6 +42,7 @@ def _build_meta_rows(result, trigger_runs_by_pipeline=None):
("Processes per Node", result.get("numproc_node")),
("Threads per Process", result.get("nthreads")),
("CPUs per Node", result.get("cpus_per_node")),
+ ("Parent Pipeline ID", result.get("parent_pipeline_id")),
]
for label, value in optional_rows:
if value not in (None, "", "N/A", "null"):
diff --git a/result_server/utils/result_table_rows.py b/result_server/utils/result_table_rows.py
index b1ed512..e81fe19 100644
--- a/result_server/utils/result_table_rows.py
+++ b/result_server/utils/result_table_rows.py
@@ -31,6 +31,14 @@ def build_result_table_row(
pipeline_id = "-"
else:
pipeline_id = str(pipeline_id)
+ pipeline_label = f"#{pipeline_id}" if pipeline_id != "-" else "-"
+ parent_pipeline_id = result_data.get("parent_pipeline_id", "")
+ parent_pipeline_id = str(parent_pipeline_id) if parent_pipeline_id not in (None, "") else ""
+ ci_title = f"{ci_trigger} / {pipeline_label}"
+ ci_subline = ""
+ if parent_pipeline_id and parent_pipeline_id != pipeline_id:
+ ci_title = f"{ci_trigger} / child #{pipeline_id} / parent #{parent_pipeline_id}"
+ ci_subline = f"parent #{parent_pipeline_id}"
return {
"timestamp": timestamp,
@@ -54,6 +62,9 @@ def build_result_table_row(
"execution_mode": result_data.get("execution_mode", "-") or "-",
"ci_trigger": ci_trigger,
"ci_summary": f"{ci_trigger} / {pipeline_id}",
+ "ci_title": ci_title,
+ "ci_subline": ci_subline,
+ "pipeline_label": pipeline_label,
"execution_trigger_summary": summarize_execution_trigger(
result_data,
trigger_runs_by_pipeline,
@@ -61,6 +72,7 @@ def build_result_table_row(
"build_job": result_data.get("build_job", "-") or "-",
"run_job": result_data.get("run_job", "-") or "-",
"pipeline_id": pipeline_id,
+ "parent_pipeline_id": parent_pipeline_id,
"source_info": source_info,
"source_link": source_link,
"source_hash": _format_source_hash(source_info),
diff --git a/result_server/utils/trigger_display.py b/result_server/utils/trigger_display.py
index dfab139..b07bef7 100644
--- a/result_server/utils/trigger_display.py
+++ b/result_server/utils/trigger_display.py
@@ -159,8 +159,8 @@ def _index_trigger_run(run: dict[str, Any]) -> dict[str, Any]:
def _result_matches_trigger_run(result: dict[str, Any], run: dict[str, Any]) -> bool:
- pipeline_id = str(result.get("pipeline_id") or "").strip()
- if pipeline_id and pipeline_id in run["pipeline_ids"]:
+ result_pipeline_ids = _result_pipeline_ids(result)
+ if set(result_pipeline_ids).intersection(run["pipeline_ids"]):
return True
trigger = extract_execution_trigger(result)
if not trigger["id"]:
@@ -205,8 +205,11 @@ def _execution_trigger_from_pipeline(
trigger_runs_by_pipeline: dict[str, dict[str, Any]],
) -> dict[str, str]:
data = result if isinstance(result, dict) else {}
- pipeline_id = str(data.get("pipeline_id") or "").strip()
- run = trigger_runs_by_pipeline.get(pipeline_id) if pipeline_id else None
+ run = None
+ for pipeline_id in _result_pipeline_ids(data):
+ run = trigger_runs_by_pipeline.get(pipeline_id)
+ if run:
+ break
if not run:
return {"id": "", "type": "", "reason": ""}
return {
@@ -231,6 +234,15 @@ def _trigger_run_pipeline_ids(run: dict[str, Any]) -> list[str]:
]
+def _result_pipeline_ids(result: dict[str, Any]) -> list[str]:
+ values = []
+ for value in (result.get("pipeline_id"), result.get("parent_pipeline_id")):
+ text = str(value).strip() if value not in (None, "") else ""
+ if text and text not in values:
+ values.append(text)
+ return values
+
+
def _format_trigger_type(trigger_type: str) -> str:
mapping = {
"manual_button": "Manual",
diff --git a/scripts/matrix_generate.sh b/scripts/matrix_generate.sh
index 84b8152..7c18ac6 100644
--- a/scripts/matrix_generate.sh
+++ b/scripts/matrix_generate.sh
@@ -13,6 +13,7 @@ QUEUE_FILE="config/queue.csv"
SYSTEM_INFO_FILE="config/system_info.csv"
OUTPUT_FILE=".gitlab-ci.generated.yml"
PARENT_PIPELINE_SOURCE="${CI_PIPELINE_SOURCE:-local}"
+PARENT_PIPELINE_ID="${CI_PIPELINE_ID:-}"
source ./scripts/job_functions.sh
@@ -43,6 +44,7 @@ stages:
variables:
PARENT_PIPELINE_SOURCE: \"$PARENT_PIPELINE_SOURCE\"
+ PARENT_PIPELINE_ID: \"$PARENT_PIPELINE_ID\"
BK_ESTIMATE_RUNNER_TAG: \"$ESTIMATE_RUNNER_TAG\"
" >> "$OUTPUT_FILE"
diff --git a/scripts/result.sh b/scripts/result.sh
index f72bfd3..32c4a7e 100644
--- a/scripts/result.sh
+++ b/scripts/result.sh
@@ -209,6 +209,12 @@ write_result_json() {
\"pipeline_id\": $pipeline_id"
fi
+ local parent_pipeline_id_block=""
+ if [ -n "${PARENT_PIPELINE_ID:-}" ]; then
+ parent_pipeline_id_block=",
+ \"parent_pipeline_id\": ${PARENT_PIPELINE_ID}"
+ fi
+
local execution_trigger_block=""
if [ -n "${BK_TRIGGER_ID:-}" ] || [ -n "${BK_TRIGGER_TYPE:-}" ] || [ -n "${BK_TRIGGER_REASON:-}" ]; then
local execution_trigger_json
@@ -272,7 +278,7 @@ write_result_json() {
"nthreads": "$nthreads",
"description": "$description",
"confidential": "$confidential",
- "source_info": $source_info_block${profile_data_block}${fom_breakdown_block}${timing_block}${mode_block}${trigger_block}${build_job_block}${run_job_block}${pipeline_id_block}${execution_trigger_block}
+ "source_info": $source_info_block${profile_data_block}${fom_breakdown_block}${timing_block}${mode_block}${trigger_block}${build_job_block}${run_job_block}${pipeline_id_block}${parent_pipeline_id_block}${execution_trigger_block}
}
EOF
diff --git a/scripts/tests/test_process_and_send_results.sh b/scripts/tests/test_process_and_send_results.sh
index 4395d08..4d343d0 100644
--- a/scripts/tests/test_process_and_send_results.sh
+++ b/scripts/tests/test_process_and_send_results.sh
@@ -48,6 +48,7 @@ export RESULT_SERVER_CLIENT_KEY="${TMP_DIR}/client.key"
export BK_TRIGGER_ID="qws-fugaku-1400"
export BK_TRIGGER_TYPE="scheduled"
export BK_TRIGGER_REASON="cron:0 14 * * *@2026-08-07T14:00+09:00"
+export PARENT_PIPELINE_ID="54321"
pushd "${TMP_DIR}/project" >/dev/null
bash scripts/result_server/process_and_send_results.sh qws Fugaku cross qws_Fugaku_build qws_Fugaku_N1_P2_T3_run 12345
@@ -62,6 +63,8 @@ jq -e '._server_uuid == "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"' \
"${TMP_DIR}/project/send_results_workspace/results/result0.json" >/dev/null
jq -e '.execution_trigger.id == "qws-fugaku-1400" and .execution_trigger.type == "scheduled"' \
"${TMP_DIR}/project/send_results_workspace/results/result0.json" >/dev/null
+jq -e '.pipeline_id == 12345 and .parent_pipeline_id == 54321' \
+ "${TMP_DIR}/project/send_results_workspace/results/result0.json" >/dev/null
jq -e '."result0.json".uuid == "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"' \
"${TMP_DIR}/project/send_results_workspace/results/server_result_meta.json" >/dev/null
diff --git a/scripts/tests/test_result_profile_data.sh b/scripts/tests/test_result_profile_data.sh
index 48e649e..71bc676 100644
--- a/scripts/tests/test_result_profile_data.sh
+++ b/scripts/tests/test_result_profile_data.sh
@@ -90,8 +90,9 @@ pushd "${TMP_DIR}" >/dev/null
export BK_TRIGGER_ID="qws-fugaku-watch"
export BK_TRIGGER_TYPE="watch_event"
export BK_TRIGGER_REASON="repo_ref:https://github.com/RIKEN-LQCD/qws.git@master"
+export PARENT_PIPELINE_ID="888"
bash "${REPO_DIR}/scripts/result.sh" qws Fugaku cross build run 999 >/dev/null
-unset BK_TRIGGER_ID BK_TRIGGER_TYPE BK_TRIGGER_REASON
+unset BK_TRIGGER_ID BK_TRIGGER_TYPE BK_TRIGGER_REASON PARENT_PIPELINE_ID
popd >/dev/null
pushd "${TMP_DIR}/ncu" >/dev/null
@@ -114,6 +115,8 @@ jq -e '
.pipeline_timing.build_time == 12 and
.pipeline_timing.queue_time == 0 and
.pipeline_timing.run_time == 34 and
+ .pipeline_id == 999 and
+ .parent_pipeline_id == 888 and
.execution_trigger.id == "qws-fugaku-watch" and
.execution_trigger.type == "watch_event" and
.execution_trigger.reason == "repo_ref:https://github.com/RIKEN-LQCD/qws.git@master" and