From a7925e61b5bfc459b42494db2b61d73b36783ed2 Mon Sep 17 00:00:00 2001 From: yoshifuminakamura Date: Fri, 7 Aug 2026 12:30:34 +0900 Subject: [PATCH] Show portal trigger run causes Signed-off-by: yoshifuminakamura --- .../portal-execution-profiles-handoff.md | 21 ++ result_server/routes/admin.py | 33 +++ result_server/routes/results_detail_routes.py | 7 +- result_server/routes/results_list_routes.py | 1 + result_server/templates/_results_table.html | 26 +- .../templates/admin_execution_profiles.html | 142 ++++++++++ .../tests/test_execution_profiles.py | 44 +++ result_server/tests/test_results_loader.py | 2 + result_server/tests/test_trigger_display.py | 226 ++++++++++++++++ result_server/trigger_runner.py | 137 ++++++++-- result_server/utils/execution_profiles.py | 42 ++- result_server/utils/result_detail_view.py | 16 +- result_server/utils/result_table_rows.py | 12 +- result_server/utils/results_loader.py | 22 +- result_server/utils/trigger_display.py | 254 ++++++++++++++++++ scripts/result.sh | 14 +- .../tests/test_process_and_send_results.sh | 5 + scripts/tests/test_result_profile_data.sh | 7 + 18 files changed, 986 insertions(+), 25 deletions(-) create mode 100644 result_server/tests/test_trigger_display.py create mode 100644 result_server/utils/trigger_display.py diff --git a/docs/guides/portal-execution-profiles-handoff.md b/docs/guides/portal-execution-profiles-handoff.md index 5171d77..df0ded1 100644 --- a/docs/guides/portal-execution-profiles-handoff.md +++ b/docs/guides/portal-execution-profiles-handoff.md @@ -18,6 +18,8 @@ path: - Admin dry-run and confirmed GitLab trigger submit paths. - Portal-managed scheduled and repo/ref trigger definitions. - A site-local trigger runner for scheduled and event-triggered execution. +- Trigger decision visibility in the admin profile page and result-level run + cause visibility for results produced by Portal-triggered pipelines. The registry can contain scheduler account or project-group values. Do not commit real site-local values to the OSS repository. @@ -45,6 +47,10 @@ Completed: keeps fingerprints in the Portal SQLite DB, and passes the Portal-specific `RESULT_SERVER` URL to GitLab pipelines so dev Portal triggers return results to the same dev Portal. +- Triggered pipelines receive `BK_TRIGGER_ID`, `BK_TRIGGER_TYPE`, and + `BK_TRIGGER_REASON`. `scripts/result.sh` stores these values under + `execution_trigger` in Result JSON, and the Portal shows them as the result + `Run Cause`. Remaining follow-up: @@ -134,6 +140,21 @@ The runner uses a short-lived SQLite lock by default so overlapping timer invocations do not evaluate or submit the same triggers twice. Tune the lock TTL with `--lock-ttl-seconds` when the timer interval is changed. +The admin execution-profile page shows recent trigger runner decisions, +including `not_due`, `already_submitted`, `unchanged`, `blocked`, +`submitted`, and `submit_failed`. For `repo_ref` watches it also shows the +latest observed target fingerprint. Results produced by Portal-triggered +pipelines show a `Run Cause` column in the results table and a matching detail +row when the Result JSON contains `execution_trigger`. + +Routine non-submit decisions such as `not_due`, `unchanged`, +`already_submitted`, and `runner_locked` are rate-limited in `trigger_runs`. +The default interval is 60 minutes, so a one-minute timer does not write a +routine history row on every tick. Use `--routine-log-interval-minutes 0` only +when intentionally debugging every runner tick. +Repo/ref observations are only persisted when a fingerprint is first initialized +or changes; unchanged checks do not update `observed_at` on every timer tick. + The generated service reads GitLab trigger configuration from the `EnvironmentFile`. The token must remain site-local: diff --git a/result_server/routes/admin.py b/result_server/routes/admin.py index 24ce60f..9dc87ed 100644 --- a/result_server/routes/admin.py +++ b/result_server/routes/admin.py @@ -37,6 +37,7 @@ submit_pipeline_plan, ) from utils.rate_limit import rate_limited +from utils.trigger_display import build_trigger_result_links, summarize_trigger_run from utils.user_store import get_user_store admin_bp = Blueprint("admin", __name__, url_prefix="/admin") @@ -213,6 +214,32 @@ def _list_trigger_definitions(db_path): return [] +def _list_trigger_runs(db_path): + try: + runs = ExecutionProfileStore(db_path).list_trigger_runs(limit=80) + except sqlite3.Error as exc: + flash(f"Trigger run history could not be loaded: {exc}") + return [] + result_links = build_trigger_result_links( + current_app.config.get("RECEIVED_DIR", ""), + runs, + ) + summaries = [] + for run in runs: + summary = summarize_trigger_run(run) + summary["result_links"] = result_links.get(int(run.get("id") or 0), []) + summaries.append(summary) + return summaries + + +def _list_trigger_observations(db_path): + try: + return ExecutionProfileStore(db_path).list_trigger_observations() + except sqlite3.Error as exc: + flash(f"Trigger observations could not be loaded: {exc}") + return [] + + def _find_trigger_definition(triggers, trigger_id): if not trigger_id: return None @@ -346,6 +373,8 @@ def execution_profiles(): profile_result=profile_result, registered_profiles=registered_profiles, trigger_definitions=trigger_definitions, + trigger_runs=_list_trigger_runs(db_path), + trigger_observations=_list_trigger_observations(db_path), profile_filter="all", profile_filter_options=_profile_filter_options(), today=datetime.now(UTC).date().isoformat(), @@ -607,6 +636,8 @@ def dry_run_execution_profile_submit(): profile_result=profile_result, registered_profiles=profile_result.profiles, trigger_definitions=_list_trigger_definitions(db_path), + trigger_runs=_list_trigger_runs(db_path), + trigger_observations=_list_trigger_observations(db_path), profile_filter="all", profile_filter_options=_profile_filter_options(), today=datetime.now(UTC).date().isoformat(), @@ -715,6 +746,8 @@ def submit_execution_profile_pipeline(): profile_result=profile_result, registered_profiles=profile_result.profiles, trigger_definitions=_list_trigger_definitions(db_path), + trigger_runs=_list_trigger_runs(db_path), + trigger_observations=_list_trigger_observations(db_path), profile_filter="all", profile_filter_options=_profile_filter_options(), today=datetime.now(UTC).date().isoformat(), diff --git a/result_server/routes/results_detail_routes.py b/result_server/routes/results_detail_routes.py index 7f42093..28d2fe1 100644 --- a/result_server/routes/results_detail_routes.py +++ b/result_server/routes/results_detail_routes.py @@ -7,6 +7,7 @@ serve_permitted_result_file, ) from utils.result_records import summarize_result_quality +from utils.trigger_display import load_trigger_run_lookup def register_results_detail_routes(results_bp): @@ -29,7 +30,11 @@ def result_detail(filename): not_found_message="Result file not found", ) quality = summarize_result_quality(result) - detail_context = build_result_detail_context(result, quality) + detail_context = build_result_detail_context( + result, + quality, + load_trigger_run_lookup(current_app.config.get("EXECUTION_PROFILE_DB_PATH")), + ) return render_template("result_detail.html", result=result, quality=quality, **detail_context) @results_bp.route("/") diff --git a/result_server/routes/results_list_routes.py b/result_server/routes/results_list_routes.py index 5b8a3f6..9edefc6 100644 --- a/result_server/routes/results_list_routes.py +++ b/result_server/routes/results_list_routes.py @@ -28,6 +28,7 @@ def _render_results_list(public_only, template_name, redirect_endpoint): filter_code=params["filter_code"], filter_exp=params["filter_exp"], padata_directory=received_padata_dir, + execution_profile_db_path=current_app.config.get("EXECUTION_PROFILE_DB_PATH"), ) filter_kwargs = dict(public_only=public_only) template_extra = {} diff --git a/result_server/templates/_results_table.html b/result_server/templates/_results_table.html index ac507d8..5404a58 100644 --- a/result_server/templates/_results_table.html +++ b/result_server/templates/_results_table.html @@ -9,7 +9,7 @@ border-radius: 14px; } .results-table { - min-width: 1340px; + min-width: 1500px; border-collapse: collapse; } .results-table-note { @@ -78,6 +78,23 @@ white-space: normal; line-height: 1.25; } +.run-cause-cell { + min-width: 140px; + max-width: 220px; + white-space: normal; + line-height: 1.25; +} +.run-cause-headline { + display: block; + font-weight: 600; +} +.run-cause-subline { + display: block; + color: #607282; + font-size: 0.82em; + overflow: hidden; + text-overflow: ellipsis; +} .ci-trigger, .ci-pipeline { display: block; @@ -183,6 +200,13 @@ {% include "_results_table_cell_profile.html" %} {% elif key == "ci_summary" %} {% include "_results_table_cell_ci.html" %} + {% elif key == "execution_trigger_summary" %} + + {{ row.execution_trigger_summary.headline }} + {% if row.execution_trigger_summary.subline %} + {{ row.execution_trigger_summary.subline }} + {% endif %} + {% elif key == "timestamp" %} {% set ts_parts = row[key].split(' ') %} {{ ts_parts[0] }}
{{ ts_parts[1] if ts_parts|length > 1 else '' }} diff --git a/result_server/templates/admin_execution_profiles.html b/result_server/templates/admin_execution_profiles.html index 7cbf9b4..880b6b9 100644 --- a/result_server/templates/admin_execution_profiles.html +++ b/result_server/templates/admin_execution_profiles.html @@ -154,6 +154,46 @@ .trigger-definition-table td { vertical-align: middle; } + .trigger-history-table { + table-layout: fixed; + min-width: 1180px; + } + .trigger-history-table td { + vertical-align: top; + white-space: normal; + } + .trigger-history-table th:nth-child(1) { width: 150px; } + .trigger-history-table th:nth-child(2) { width: 160px; } + .trigger-history-table th:nth-child(3) { width: 120px; } + .trigger-history-table th:nth-child(4) { width: 240px; } + .trigger-history-table th:nth-child(5) { width: 220px; } + .trigger-history-table th:nth-child(6) { width: 150px; } + .trigger-history-table th:nth-child(7) { width: 180px; } + .trigger-run-subline { + display: block; + margin-top: 2px; + color: #64748b; + font-size: 12px; + line-height: 1.25; + } + .trigger-run-reason { + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .trigger-run-error { + color: #991b1b; + font-weight: 700; + } + .trigger-observation-table { + table-layout: fixed; + min-width: 900px; + } + .trigger-observation-table td { + vertical-align: top; + white-space: normal; + } .trigger-condition { max-width: 46ch; } @@ -635,6 +675,108 @@

Registered Triggers

+
+

Trigger Run History

+

Recent trigger runner decisions, including why scheduled and repo/ref triggers did or did not submit a GitLab pipeline.

+
+ + + + + + + + + + + + + + + {% for run in trigger_runs %} + + + + + + + + + + + {% else %} + + + + {% endfor %} + +
TimeTriggerStatusReasonTargetScopeResultsErrors
+ {{ run.created_at }} + {{ run.actor }}{% if run.dry_run %} / dry-run{% endif %} + + {{ run.trigger_id }} + {{ run.trigger_type }} + {{ run.status }} + {{ run.reason_label or run.reason }} + + {{ run.gitlab_target }} + {{ run.target_ref }} + + {{ run.code }} + / {{ run.system }} + {{ run.allocation_project_id }} + + {% if run.result_links %} + {% for result_link in run.result_links[:4] %} + {{ result_link.label }}{% if not loop.last %}
{% endif %} + {% endfor %} + {% if run.result_links|length > 4 %} + +{{ run.result_links|length - 4 }} more + {% endif %} + {% else %} + - + {% endif %} +
+ {% if run.error_summary %} + error + {{ run.error_summary }} + {% else %} + - + {% endif %} +
No trigger runner decisions have been recorded.
+
+
+ +
+

Trigger Observations

+

Latest repo/ref fingerprints recorded by watch-event triggers.

+
+ + + + + + + + + + + {% for observation in trigger_observations %} + + + + + + + {% else %} + + + + {% endfor %} + +
TriggerTargetFingerprintObserved At
{{ observation.trigger_id }}{{ observation.target }}{{ observation.fingerprint[:12] }}{% if observation.fingerprint|length > 12 %}...{% endif %}{{ observation.observed_at }}
No repo/ref observations have been recorded.
+
+