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
5 changes: 5 additions & 0 deletions docs/deploy/public-portal-access-control.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,11 @@ RESULT_SERVER_PUBLIC_PORTAL_MODE=true
Development or internal-only portal services may leave the flag unset unless
they intentionally need to preview the public browser surface.

Set `RESULT_SERVER_VERSION` to the deployed public tag when the production
checkout is not expected to sit exactly on a Git tag. If it is unset, the portal
shows the exact tag for `HEAD` when available, otherwise the current Git
description or `development`.

## Testing Plan

Add lightweight tests as the design is implemented:
Expand Down
18 changes: 18 additions & 0 deletions docs/release-notes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Benchkit Release Notes

## v2026.08.31 - Initial public CX Portal baseline

Public portal baseline for scoped QWS measurements on Fugaku and RIKYU, with
public-safe result pages and Portal-managed main-branch triggers.

- Public result browsing, comparison, and system catalog pages are available in
public portal mode.
- Public mode hides operator-only views, raw result JSON routes, trigger
internals, and environment snapshot detail.
- Portal-managed triggers submit scoped main-branch measurements with an
explicit result-server destination.
- Build cache restore checks source identity, host build environment, and
restored artifact integrity.
- Branch and tag source inputs record the resolved commit used for the build.
- Manual GitLab CI is reserved for development and release-candidate validation,
not production main results.
2 changes: 2 additions & 0 deletions result_server/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from utils.auth import parse_ingest_keys
from utils.csrf import init_csrf
from utils.portal_access import is_public_portal_mode, register_public_portal_guard
from utils.portal_version import portal_version_info
from utils.preflight import validate_production_config


Expand Down Expand Up @@ -194,6 +195,7 @@ def create_app(prefix="", base_dir=None):
_configure_api_auth(app)
_configure_public_portal_mode(app)
_configure_execution_profiles(app, base_dir)
app.config["PORTAL_VERSION"] = portal_version_info()
register_public_portal_guard(app)
init_csrf(app, exempt_blueprints=(api_bp,))

Expand Down
2 changes: 2 additions & 0 deletions result_server/app_dev.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ def create_dev_app(base_dir):
from utils.audit_logging import configure_audit_logging
from utils.auth import parse_ingest_keys
from utils.csrf import init_csrf
from utils.portal_version import portal_version_info
from utils.system_info import get_all_systems_info, summarize_systems_info

app = Flask(__name__, template_folder="templates")
Expand Down Expand Up @@ -202,6 +203,7 @@ def create_dev_app(base_dir):
ALLOWED_AFFILIATIONS=parse_allowed_affiliations(
os.environ.get("RESULT_SERVER_ALLOWED_AFFILIATIONS")
),
PORTAL_VERSION=portal_version_info(),
)
Session(app)
configure_audit_logging(app)
Expand Down
14 changes: 14 additions & 0 deletions result_server/routes/home.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

from flask import render_template

from utils.portal_version import portal_release_notes


HOME_GUIDE_LINKS = {
"add_app": "https://github.com/RIKEN-RCCS/benchkit/blob/main/docs/guides/add-app.md",
Expand All @@ -23,4 +25,16 @@ def homepage():
guide_links=build_home_guide_links(),
)

def changes():
return render_template(
"changes.html",
release_notes=portal_release_notes(),
)

app.add_url_rule(f"{prefix}/", endpoint="home", view_func=homepage, strict_slashes=False)
app.add_url_rule(
f"{prefix}/changes",
endpoint="changes",
view_func=changes,
strict_slashes=False,
)
1 change: 1 addition & 0 deletions result_server/templates/_navigation.html
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@
<a href="{{ url_for('home') }}" class="nav-brand">CX Portal</a>
<a href="{{ url_for('home') }}" class="nav-link {% if request.endpoint == 'home' %}active{% endif %}">Home</a>
<a href="{{ url_for('systemlist') }}" class="nav-link {% if request.endpoint == 'systemlist' %}active{% endif %}">Systems</a>
<a href="{{ url_for('changes') }}" class="nav-link {% if request.endpoint == 'changes' %}active{% endif %}">Changes</a>

{% if session.get('authenticated') and not public_portal_mode %}
<a href="{{ url_for('results.results') }}" class="nav-link {% if request.endpoint == 'results.results' %}active{% endif %}">Public</a>
Expand Down
11 changes: 11 additions & 0 deletions result_server/templates/_results_base.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,17 @@ <h1 class="page-title">{% block page_title %}{{ self.title() }}{% endblock %}</h
-->

{% block content %}{% endblock %}

{% set portal_version = config.get('PORTAL_VERSION', {}) %}
{% set portal_version_label = portal_version.get('label', 'development') %}
{% set portal_version_commit = portal_version.get('commit', '') %}
<footer class="portal-footer">
<span>CX Portal <span class="portal-version-label">{{ portal_version_label }}</span></span>
{% if portal_version_commit %}
<span class="portal-version-commit">commit {{ portal_version_commit }}</span>
{% endif %}
<a href="{{ url_for('changes') }}">Changes</a>
</footer>
</div>

</body>
Expand Down
22 changes: 22 additions & 0 deletions result_server/templates/_table_base.html
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,25 @@
background: rgba(255, 255, 255, 0.94);
box-shadow: 0 12px 30px rgba(18, 52, 77, 0.06);
}
.portal-footer {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
margin: 18px 0 0;
padding: 12px 2px 0;
border-top: 1px solid #d8e3e8;
color: #52606d;
font-size: 12px;
}
.portal-footer a {
font-weight: 600;
text-decoration: none;
}
.portal-version-label,
.portal-version-commit {
font-family: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", monospace;
}
.table-card {
overflow: hidden;
}
Expand Down Expand Up @@ -346,6 +365,9 @@
.page-subtitle {
font-size: 13px;
}
.portal-footer {
gap: 8px;
}
}
</style>

Expand Down
76 changes: 76 additions & 0 deletions result_server/templates/changes.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
{% extends "_results_base.html" %}

{% block title %}CX Portal Changes{% endblock %}
{% block page_title %}CX Portal Changes{% endblock %}
{% block page_subtitle %}Broad-grained release notes for the public CX Portal surface.{% endblock %}

{% block content %}
<style>
.changes-list {
display: grid;
gap: 16px;
}
.changes-release-header {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 16px;
margin-bottom: 8px;
}
.changes-release-title {
margin: 0;
font-size: 20px;
}
.changes-release-date {
color: #52606d;
font-size: 13px;
white-space: nowrap;
}
.changes-version {
display: inline-block;
margin-right: 8px;
color: #0f766e;
font-family: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", monospace;
font-size: 0.9em;
}
.changes-summary {
margin: 0 0 12px;
color: #52606d;
line-height: 1.55;
}
.changes-items {
margin: 0;
padding-left: 22px;
color: #1f2933;
line-height: 1.6;
}
@media (max-width: 640px) {
.changes-release-header {
display: block;
}
.changes-release-date {
display: block;
margin-top: 4px;
}
}
</style>

<section class="changes-list">
{% for release in release_notes %}
<section class="page-card">
<div class="changes-release-header">
<h2 class="changes-release-title">
<span class="changes-version">{{ release.version }}</span>{{ release.title }}
</h2>
<span class="changes-release-date">{{ release.date }}</span>
</div>
<p class="changes-summary">{{ release.summary }}</p>
<ul class="changes-items">
{% for item in release.changes %}
<li>{{ item }}</li>
{% endfor %}
</ul>
</section>
{% endfor %}
</section>
{% endblock %}
4 changes: 4 additions & 0 deletions result_server/test_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,10 @@ def profile_requests():
def home():
return ""

@app.route(f"{prefix}/changes")
def changes():
return ""

if include_systemlist_route:
def systemlist():
return ""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

def _add_navigation_routes(app):
app.add_url_rule("/", "home", lambda: "home")
app.add_url_rule("/changes", "changes", lambda: "changes")
app.add_url_rule("/systems", "systemlist", lambda: "systems")
app.add_url_rule("/login", "auth.login", lambda: "login")
app.add_url_rule("/logout", "auth.logout", lambda: "logout")
Expand Down
19 changes: 19 additions & 0 deletions result_server/tests/test_home_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,22 @@ def test_home_page_renders_discord_link_when_configured(monkeypatch):
assert "invitation-only Discord" in html
assert "application-onboarding coordination" in html
assert "https://discord.gg/example" in html


def test_changes_page_renders_release_notes(monkeypatch):
monkeypatch.delenv("CX_DISCORD_INVITE_URL", raising=False)
app = build_portal_shell_app(
templates_dir=os.path.join(os.path.dirname(__file__), "..", "templates"),
include_home_route=False,
)
register_home_routes(app)

with app.test_client() as client:
response = client.get("/changes")

assert response.status_code == 200
html = response.get_data(as_text=True)
assert "CX Portal Changes" in html
assert "v2026.08.31" in html
assert "Initial public CX Portal baseline" in html
assert "Portal-managed triggers" in html
4 changes: 4 additions & 0 deletions result_server/tests/test_portal_access_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ def test_every_registered_route_has_access_class(tmp_path):


def test_representative_route_access_classes():
assert classify_endpoint("changes") == ACCESS_PUBLIC
assert classify_endpoint("home") == ACCESS_PUBLIC
assert classify_endpoint("systemlist") == ACCESS_PUBLIC
assert classify_endpoint("results.results") == ACCESS_PUBLIC
Expand All @@ -74,6 +75,7 @@ def test_public_portal_mode_blocks_restricted_browser_routes_but_allows_api_auth

with app.test_client() as client:
assert client.get("/").status_code == 200
assert client.get("/changes").status_code == 200
assert client.get("/auth/login").status_code == 404
assert client.get("/estimated/").status_code == 404
assert client.get("/results/confidential").status_code == 404
Expand All @@ -98,6 +100,7 @@ def test_public_portal_mode_hides_anonymous_restricted_navigation():

assert "Home" in html
assert "Systems" in html
assert "Changes" in html
assert "Results" in html
assert "Login" not in html
assert "Admin" not in html
Expand Down Expand Up @@ -136,6 +139,7 @@ def test_public_portal_mode_hides_authenticated_restricted_navigation():

assert "Home" in html
assert "Systems" in html
assert "Changes" in html
assert "Results" in html
assert "admin@example.test" not in html
assert "Login" not in html
Expand Down
59 changes: 59 additions & 0 deletions result_server/tests/test_portal_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import os
import sys

sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))

from utils import portal_version


def test_portal_version_prefers_environment(monkeypatch, tmp_path):
monkeypatch.setenv("RESULT_SERVER_VERSION", "vtest")
monkeypatch.setattr(portal_version, "_find_git_root", lambda start_path: tmp_path)
monkeypatch.setattr(
portal_version,
"_run_git",
lambda args, git_root: "abc123def456" if args[:1] == ["rev-parse"] else "vold",
)

info = portal_version.portal_version_info()

assert info["label"] == "vtest"
assert info["commit"] == "abc123def456"
assert info["source"] == "environment"


def test_portal_version_uses_exact_git_tag(monkeypatch, tmp_path):
monkeypatch.delenv("RESULT_SERVER_VERSION", raising=False)
monkeypatch.delenv("BENCHKIT_PORTAL_VERSION", raising=False)
monkeypatch.setattr(portal_version, "_find_git_root", lambda start_path: tmp_path)

def fake_run_git(args, git_root):
if args[:1] == ["rev-parse"]:
return "123456789abc"
if args == ["describe", "--tags", "--exact-match", "HEAD"]:
return "v2026.08.31"
return "v2026.08.31-1-g1234567"

monkeypatch.setattr(portal_version, "_run_git", fake_run_git)

info = portal_version.portal_version_info()

assert info == {
"label": "v2026.08.31",
"commit": "123456789abc",
"source": "git",
}


def test_portal_version_falls_back_to_development(monkeypatch):
monkeypatch.delenv("RESULT_SERVER_VERSION", raising=False)
monkeypatch.delenv("BENCHKIT_PORTAL_VERSION", raising=False)
monkeypatch.setattr(portal_version, "_find_git_root", lambda start_path: None)

info = portal_version.portal_version_info()

assert info == {
"label": "development",
"commit": "",
"source": "default",
}
1 change: 1 addition & 0 deletions result_server/tests/test_public_result_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

def _add_navigation_routes(app):
app.add_url_rule("/", "home", lambda: "home")
app.add_url_rule("/changes", "changes", lambda: "changes")
app.add_url_rule("/systems", "systemlist", lambda: "systems")
app.add_url_rule("/login", "auth.login", lambda: "login")
app.add_url_rule("/logout", "auth.logout", lambda: "logout")
Expand Down
1 change: 1 addition & 0 deletions result_server/utils/portal_access.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

PUBLIC_ENDPOINTS = frozenset(
{
"changes",
"home",
"systemlist",
"static",
Expand Down
Loading
Loading