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
21 changes: 21 additions & 0 deletions docs/guides/portal-execution-profiles-handoff.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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:

Expand Down Expand 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:

Expand Down
33 changes: 33 additions & 0 deletions result_server/routes/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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(),
Expand Down
7 changes: 6 additions & 1 deletion result_server/routes/results_detail_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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("/<filename>")
Expand Down
1 change: 1 addition & 0 deletions result_server/routes/results_list_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {}
Expand Down
26 changes: 25 additions & 1 deletion result_server/templates/_results_table.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
border-radius: 14px;
}
.results-table {
min-width: 1340px;
min-width: 1500px;
border-collapse: collapse;
}
.results-table-note {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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" %}
<td class="run-cause-cell" title="{{ row.execution_trigger_summary.title }}">
<span class="run-cause-headline">{{ row.execution_trigger_summary.headline }}</span>
{% if row.execution_trigger_summary.subline %}
<span class="run-cause-subline">{{ row.execution_trigger_summary.subline }}</span>
{% endif %}
</td>
{% elif key == "timestamp" %}
{% set ts_parts = row[key].split(' ') %}
<td class="ts-cell">{{ ts_parts[0] }}<br>{{ ts_parts[1] if ts_parts|length > 1 else '' }}</td>
Expand Down
142 changes: 142 additions & 0 deletions result_server/templates/admin_execution_profiles.html
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -635,6 +675,108 @@ <h2 class="section-title">Registered Triggers</h2>
</table>
</div>
</section>
<section class="page-card table-card">
<h2 class="section-title">Trigger Run History</h2>
<p class="help-text">Recent trigger runner decisions, including why scheduled and repo/ref triggers did or did not submit a GitLab pipeline.</p>
<div class="table-wrap">
<table class="trigger-history-table">
<thead>
<tr>
<th>Time</th>
<th>Trigger</th>
<th>Status</th>
<th>Reason</th>
<th>Target</th>
<th>Scope</th>
<th>Results</th>
<th>Errors</th>
</tr>
</thead>
<tbody>
{% for run in trigger_runs %}
<tr>
<td>
<span class="profile-mono">{{ run.created_at }}</span>
<span class="trigger-run-subline">{{ run.actor }}{% if run.dry_run %} / dry-run{% endif %}</span>
</td>
<td>
<span class="profile-mono">{{ run.trigger_id }}</span>
<span class="trigger-run-subline">{{ run.trigger_type }}</span>
</td>
<td>{{ run.status }}</td>
<td title="{{ run.reason }}">
<span class="trigger-run-reason">{{ run.reason_label or run.reason }}</span>
</td>
<td>
<span class="profile-mono">{{ run.gitlab_target }}</span>
<span class="trigger-run-subline">{{ run.target_ref }}</span>
</td>
<td>
<span class="profile-mono">{{ run.code }}</span>
/ <span class="profile-mono">{{ run.system }}</span>
<span class="trigger-run-subline">{{ run.allocation_project_id }}</span>
</td>
<td>
{% if run.result_links %}
{% for result_link in run.result_links[:4] %}
<a href="{{ url_for('results.result_detail', filename=result_link.filename) }}">{{ result_link.label }}</a>{% if not loop.last %}<br>{% endif %}
{% endfor %}
{% if run.result_links|length > 4 %}
<span class="trigger-run-subline">+{{ run.result_links|length - 4 }} more</span>
{% endif %}
{% else %}
-
{% endif %}
</td>
<td>
{% if run.error_summary %}
<span class="trigger-run-error" title="{{ run.error_summary }}">error</span>
<span class="trigger-run-subline">{{ run.error_summary }}</span>
{% else %}
-
{% endif %}
</td>
</tr>
{% else %}
<tr>
<td colspan="8">No trigger runner decisions have been recorded.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</section>

<section class="page-card table-card">
<h2 class="section-title">Trigger Observations</h2>
<p class="help-text">Latest repo/ref fingerprints recorded by watch-event triggers.</p>
<div class="table-wrap">
<table class="trigger-observation-table">
<thead>
<tr>
<th>Trigger</th>
<th>Target</th>
<th>Fingerprint</th>
<th>Observed At</th>
</tr>
</thead>
<tbody>
{% for observation in trigger_observations %}
<tr>
<td><span class="profile-mono">{{ observation.trigger_id }}</span></td>
<td title="{{ observation.target }}">{{ observation.target }}</td>
<td><span class="profile-mono">{{ observation.fingerprint[:12] }}{% if observation.fingerprint|length > 12 %}...{% endif %}</span></td>
<td><span class="profile-mono">{{ observation.observed_at }}</span></td>
</tr>
{% else %}
<tr>
<td colspan="4">No repo/ref observations have been recorded.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</section>
<script>
document.addEventListener("DOMContentLoaded", function () {
const buttons = Array.from(document.querySelectorAll("[data-profile-filter]"));
Expand Down
Loading
Loading