Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
9ebe96d
feat(frontend): modernize Pipelines UI workflows
jeffspahr Oct 11, 2026
ed5d06c
fix(frontend): retry uploaded pipeline version discovery
jeffspahr Oct 11, 2026
d1043bb
test(frontend): run retained evidence checks and restore JSON coverage
jeffspahr Oct 12, 2026
a592dcd
test: preserve provider-managed Docker configuration on runners
jeffspahr Oct 13, 2026
14479c0
fix(frontend): preserve inline graph behavior in modernized workflows
jeffspahr Oct 14, 2026
203619c
docs: retain one copy of merged runner guidance
jeffspahr Oct 15, 2026
91e62a4
test(frontend): minimize deep-link iteration fixture
jeffspahr Oct 15, 2026
2478453
feat(frontend): refine layouts and remember pipeline view preference
jeffspahr Oct 17, 2026
4e32776
feat(frontend): unify collection and detail page layouts
jeffspahr Oct 17, 2026
2f5ffdc
fix(frontend): expand sparse detail sections consistently
jeffspahr Oct 17, 2026
abaf414
fix(frontend): reduce header and collection spacing
jeffspahr Oct 17, 2026
ca91ff6
feat(frontend): remember collection density independently of view
jeffspahr Oct 17, 2026
ef0e744
feat(frontend): separate theme segments from footer links
jeffspahr Oct 17, 2026
583f58a
feat(frontend): use a labeled dark-mode switch in the sidebar
jeffspahr Oct 17, 2026
4040fbe
fix(frontend): simplify theme control to sun and moon toggle
jeffspahr Oct 17, 2026
869fc78
fix(frontend): embed theme icons in footer toggle
jeffspahr Oct 17, 2026
c500685
chore(frontend): merge current master into modernization
jeffspahr Oct 17, 2026
d6e5b76
test(frontend): align integration checks with theme and density controls
jeffspahr Oct 17, 2026
9769ef3
fix(frontend): keep pipeline view and sort controls stable
jeffspahr Oct 17, 2026
816dde9
fix(frontend): tighten comfortable and compact collection density
jeffspahr Oct 17, 2026
c495c27
fix(frontend): use a conventional pipeline sort menu
jeffspahr Oct 17, 2026
4fa27f7
feat(frontend): put pipeline sorting and filters beside their data
jeffspahr Oct 17, 2026
7ec50be
fix(frontend): keep column filter controls compact
jeffspahr Oct 17, 2026
7696547
feat(frontend): add consistent list column filters
jeffspahr Oct 17, 2026
82729ba
feat(frontend): move archive scope beside list search
jeffspahr Oct 17, 2026
bcb4b53
feat(frontend): inline experiment run archive scope
jeffspahr Oct 17, 2026
911a32e
test(frontend): expand artifact preview samples
jeffspahr Oct 17, 2026
0e49f94
test(frontend): add varied run review samples
jeffspahr Oct 18, 2026
8debb25
feat(frontend): unify collection and detail page layouts
jeffspahr Oct 18, 2026
ad62b71
fix(frontend): align compact collection row content
jeffspahr Oct 18, 2026
1bcdd95
fix(frontend): lighten table names and avoid duplicate scope controls
jeffspahr Oct 18, 2026
dc9fb82
fix(frontend): use readable status labels and sentence case
jeffspahr Oct 18, 2026
54793ce
feat(frontend): make expanded collections lightweight previews
jeffspahr Oct 18, 2026
21aca4a
style(frontend): distinguish preview rows on hover and focus
jeffspahr Oct 18, 2026
0d24042
feat(frontend): address graph and recurring-run review feedback
jeffspahr Oct 18, 2026
6756d7b
fix(frontend): cap automatic graph fitting at natural size
jeffspahr Oct 18, 2026
e3f4358
build(frontend): generate shared third-party notices
jeffspahr Oct 18, 2026
902c806
fix(frontend): show graph group controls only when relevant
jeffspahr Oct 18, 2026
28bd23f
fix(frontend): populate inline pipeline definition details
jeffspahr Oct 18, 2026
913e5b1
fix(frontend): preserve detail metadata across run entry paths
jeffspahr Oct 18, 2026
13fa0e8
test(frontend): populate sample run timelines across statuses
jeffspahr Oct 18, 2026
91114b1
merge: integrate master dependency updates into UI modernization
jeffspahr Oct 18, 2026
b80e11f
test(frontend): align UI expectations and asset formatting
jeffspahr Oct 18, 2026
d89f285
fix(frontend): stabilize loading layouts and refresh browser contracts
jeffspahr Oct 18, 2026
b93db11
fix(frontend): clarify driver logs and populate review samples
jeffspahr Oct 19, 2026
f062eeb
fix(frontend): recenter graphs around inspection panels
jeffspahr Oct 19, 2026
d8c0b9b
fix(frontend): show saved versions in sample runs
jeffspahr Oct 19, 2026
222d163
fix(frontend): describe recurring run schedules clearly
jeffspahr Oct 19, 2026
a2e5106
feat(frontend): simplify recurring run schedule creation
jeffspahr Oct 19, 2026
bcd46ef
feat(frontend): refine review layouts and graph examples
jeffspahr Oct 20, 2026
a6637a3
test(frontend): cover nested sample task routes
jeffspahr Oct 20, 2026
fa4c004
fix(frontend): combine driver and task logs in one tab
jeffspahr Oct 20, 2026
8f5b16a
fix(frontend): fit driver log viewport to its content
jeffspahr Oct 20, 2026
7d17201
test(frontend): provide task-specific sample executor logs
jeffspahr Oct 20, 2026
4e5c316
fix(frontend): keep resized resource tables full width
jeffspahr Oct 20, 2026
df8a930
feat(frontend): copy driver and task log output
jeffspahr Oct 20, 2026
0a1fb5f
feat(frontend): select available task retry logs
jeffspahr Oct 20, 2026
d0a9468
feat(frontend): refine pipelines graphs and logs from UI review
jeffspahr Oct 20, 2026
93c5c6c
fix(frontend): verify initial graph fit and compact review controls
jeffspahr Oct 20, 2026
84565f2
fix(frontend): fit graphs after runtime tasks expand their layout
jeffspahr Oct 20, 2026
9fbdee7
refactor(frontend): use one compact collection layout
jeffspahr Oct 21, 2026
f6e78df
merge: reconcile UI modernization with current master
jeffspahr Oct 21, 2026
ba521c4
test(frontend): isolate theme cycling and restoration checks
jeffspahr Oct 21, 2026
40c1285
chore(frontend): resolve modernization conflicts with master
jeffspahr Oct 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
6 changes: 5 additions & 1 deletion .github/resources/ci-workflow-inventory.json
Original file line number Diff line number Diff line change
Expand Up @@ -357,12 +357,16 @@
{
"path": ".github/workflows/frontend.yml",
"name": "Frontend Tests",
"header_sha256": "ad213cb370af3e573a7e0c7bdcec60c0a93cb6c882ac2fe3b148934ba46d964b",
"header_sha256": "7db5b97ddafe4c0580c8d61ec01af6170b5bd274f7820316b43351b8adacd608",
"pull_request": {
"branches": [
"master"
],
"paths": [
".github/actions/download-artifact-with-retry/**",
".github/resources/scripts/artifact-files.sh",
".github/resources/scripts/configure_frontend_apt*",
".github/resources/scripts/ci-image-artifacts.sh",
"frontend/**",
"manifests/kustomize/base/installs/multi-user/pipelines-profile-controller/sync.py",
"api/v2alpha1/*.proto",
Expand Down
22 changes: 21 additions & 1 deletion .github/resources/scripts/artifact-files.sh
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,32 @@ if [[ -z "${required_files//[[:space:]]/}" ]]; then
fi
# Preparation and download must resolve the destination identically. Unlike
# the download action, shell paths do not expand a tilde stored in a variable.
case "$DOWNLOAD_PATH" in
case "${DOWNLOAD_PATH:-}" in
""|"~"*)
echo "::error::required-files needs an absolute or workspace-relative destination without tilde expansion"
exit 2
;;
esac
# The download action returns native Windows paths even for relative inputs.
# Git Bash needs the equivalent POSIX path before physical-ancestor checks.
case "$DOWNLOAD_PATH" in
[[:alpha:]]:[\\/]*)
# cygpath collapses parent components; reject them before conversion so
# cleanup cannot hide traversal into a different directory.
windows_destination="${DOWNLOAD_PATH//\\//}"
case "$windows_destination/" in
*/../*)
echo "::error::Artifact destination must not contain parent traversal: $DOWNLOAD_PATH"
exit 2
;;
esac
if ! command -v cygpath >/dev/null 2>&1; then
echo "::error::Windows artifact destinations require Git Bash cygpath"
exit 2
fi
DOWNLOAD_PATH="$(cygpath -u -- "$DOWNLOAD_PATH")"
;;
esac
# Check the destination before following it or creating anything. Relative
# paths start at the physical working directory; absolute paths must already
# use physical ancestors (for example /private/tmp, not macOS's /tmp alias).
Expand Down
20 changes: 18 additions & 2 deletions .github/resources/scripts/artifact_download_callers_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@

ROOT = Path(__file__).resolve().parents[3]
WRAPPER = './.github/actions/download-artifact-with-retry'
CALLERS = {
CALLERS = [
('.github/workflows/frontend.yml', 'browser-tests'),
('.github/actions/deploy/action.yml', 'composite'),
('.github/workflows/arm64-presubmit.yml', 'smoke'),
('.github/workflows/build-tools-images.yml', 'compare-generated'),
Expand All @@ -38,7 +39,7 @@
('.github/workflows/image-builds-master.yml', 'arm64-smoke'),
('.github/workflows/image-builds-release.yml', 'validate-release-images'),
('.github/workflows/image-builds.yml', 'runtime-base-images'),
}
]


def workflow(name):
Expand Down Expand Up @@ -155,6 +156,21 @@ def test_every_maintained_download_has_a_manifest_and_no_raw_bypass(self):
callers.append(location)
self.assertCountEqual(callers, CALLERS)

def test_browser_bundle_uses_workspace_relative_paths_on_windows(self):
steps = workflow('frontend.yml')['jobs']['browser-tests']['steps']
download = next(step for step in steps if step.get('uses') == WRAPPER)
self.assertEqual(download['with']['path'], 'frontend/.ci-bundle')
self.assertEqual(
download['with']['required-files'], 'frontend-bundle.tar.gz'
)
extract = next(
step
for step in steps
if step.get('name') == 'Extract complete production bundle'
)
self.assertIn('.ci-bundle/frontend-bundle.tar.gz', extract['run'])
self.assertNotIn('RUNNER_TEMP', extract['run'])

def test_deploy_manifest_matches_ci_producers_and_modelcar_selection(self):
built_images = {
item['image']
Expand Down
177 changes: 176 additions & 1 deletion .github/resources/scripts/artifact_download_completeness_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import tempfile
import unittest
from pathlib import Path
from unittest import mock

import yaml
from artifact_workflow_test_support import evaluate, render
Expand Down Expand Up @@ -51,6 +52,7 @@ def __init__(self, path, attempts, required_files=None):
self.path = path
self.attempts = attempts
self.downloads = []
self.download_output_path = None
self.steps = {}
self.failed = False
self.cancelled = False
Expand Down Expand Up @@ -119,7 +121,10 @@ def run(self):
destination.mkdir(parents=True, exist_ok=True)
outcome = attempt(destination, download_inputs)
if outcome == 'success':
outputs['download-path'] = os.path.abspath(destination)
outputs['download-path'] = (
self.download_output_path
or os.path.abspath(destination)
)
else:
if step['shell'] != 'bash':
raise AssertionError(f'Unexpected shell: {step}')
Expand Down Expand Up @@ -213,6 +218,36 @@ def test_complete_first_attempt_does_not_retry(self):
output = runner.action['outputs']['download-path']['value']
self.assertEqual(runner.render(output), str(self.path))

def test_windows_native_action_output_validates_complete_download(self):
tools = Path(self.directory.name) / 'bin'
tools.mkdir()
converter = tools / 'cygpath'
converter.write_text(
'#!/bin/sh\n'
'[ "$1" = "-u" ] && [ "$2" = "--" ] && '
'[ "$3" = "$CYGPATH_INPUT" ] || exit 9\n'
'printf "%s\\n" "$CYGPATH_TARGET"\n'
)
converter.chmod(0o755)
runner = CompositeAction(
self.path, [download(files=self.complete)], self.required
)
runner.download_output_path = r'D:\a\pipelines\downloads'
with mock.patch.dict(
os.environ,
{
'PATH': str(tools) + os.pathsep + os.environ['PATH'],
'CYGPATH_TARGET': str(self.path),
'CYGPATH_INPUT': runner.download_output_path,
},
):
result = runner.run()
self.assertEqual(result, 'success', runner.log)
self.assertEqual(runner.downloads, ['primary'])
self.assertEqual(
runner.steps['verify-primary']['outputs']['complete'], 'true'
)

def test_download_defaults_resolve_the_github_context(self):
received = []

Expand Down Expand Up @@ -527,6 +562,146 @@ def test_physical_absolute_and_relative_destinations_are_supported(self):
)
self.assertTrue((self.path / 'new/nested').is_dir())

def test_native_windows_download_output_uses_actual_destination(self):
tools = self.path / 'bin'
tools.mkdir()
converter = tools / 'cygpath'
converter.write_text('#!/bin/sh\nprintf "%s\\n" "$CYGPATH_TARGET"\n')
converter.chmod(0o755)
target = self.path / 'download'
target.mkdir()
with mock.patch.dict(
os.environ,
{
'PATH': str(tools) + os.pathsep + os.environ['PATH'],
'CYGPATH_TARGET': str(target),
},
):
for destination in (r'D:\a\_temp\download', 'D:/a/_temp/download'):
with self.subTest(destination=destination):
archive = target / 'archive.tar'
archive.write_text('complete')
result = self.run_helper(
'verify', 'archive.tar', destination
)
self.assertEqual(
result.returncode, 0, result.stdout + result.stderr
)
result = self.run_helper(
'prepare', 'archive.tar', destination
)
self.assertEqual(
result.returncode, 0, result.stdout + result.stderr
)
self.assertFalse(archive.exists())

def test_normalized_windows_destinations_keep_symlink_and_traversal_guards(
self,
):
tools = self.path / 'bin'
tools.mkdir()
converter = tools / 'cygpath'
converter.write_text('#!/bin/sh\nprintf "%s\\n" "$CYGPATH_TARGET"\n')
converter.chmod(0o755)
outside = self.path / 'outside'
outside.mkdir()
sentinel = outside / 'archive.tar'
sentinel.write_text('keep')
link = self.path / 'link'
link.symlink_to(outside, target_is_directory=True)
for target in (str(link), str(self.path / 'missing/../outside')):
with mock.patch.dict(
os.environ,
{
'PATH': str(tools) + os.pathsep + os.environ['PATH'],
'CYGPATH_TARGET': target,
},
):
for mode in ('prepare', 'verify', 'check'):
result = self.run_helper(
mode, 'archive.tar', r'D:\a\download'
)
self.assertEqual(
result.returncode, 2, result.stdout + result.stderr
)
self.assertEqual(sentinel.read_text(), 'keep')
self.assertFalse((self.path / 'missing').exists())

def test_windows_parent_traversal_is_rejected_before_normalization(self):
tools = self.path / 'bin'
tools.mkdir()
converter = tools / 'cygpath'
converter.write_text(
'#!/bin/sh\ntouch "$CONVERSION_MARKER"\nprintf "%s\\n" "$CYGPATH_TARGET"\n'
)
converter.chmod(0o755)
outside = self.path / 'outside'
outside.mkdir()
sentinel = outside / 'archive.tar'
sentinel.write_text('keep')
marker = self.path / 'converted'
with mock.patch.dict(
os.environ,
{
'PATH': str(tools) + os.pathsep + os.environ['PATH'],
'CYGPATH_TARGET': str(outside),
'CONVERSION_MARKER': str(marker),
},
):
for destination in (
r'D:\allowed\..\outside',
'D:/allowed/../outside',
):
for mode in ('prepare', 'verify', 'check'):
result = self.run_helper(mode, 'archive.tar', destination)
self.assertEqual(
result.returncode, 2, result.stdout + result.stderr
)
self.assertFalse(marker.exists())
self.assertEqual(sentinel.read_text(), 'keep')

def test_windows_conversion_failure_preserves_existing_files(self):
tools = self.path / 'bin'
tools.mkdir()
converter = tools / 'cygpath'
converter.write_text('#!/bin/sh\nexit 7\n')
converter.chmod(0o755)
sentinel = self.path / 'archive.tar'
sentinel.write_text('keep')
with mock.patch.dict(
os.environ,
{
'PATH': str(tools) + os.pathsep + os.environ['PATH'],
},
):
result = self.run_helper('prepare', 'archive.tar', r'D:\a\download')
self.assertEqual(result.returncode, 7, result.stdout + result.stderr)
self.assertEqual(sentinel.read_text(), 'keep')

def test_unset_destination_fails_before_touching_files(self):
sentinel = self.path / 'archive.tar'
sentinel.write_text('keep')
environment = dict(os.environ, REQUIRED_FILES='archive.tar')
environment.pop('DOWNLOAD_PATH', None)
for mode in ('prepare', 'verify', 'check'):
with self.subTest(mode=mode):
result = subprocess.run(
['bash', str(ARTIFACT_FILES), mode],
env=environment,
cwd=self.path,
capture_output=True,
text=True,
check=False,
)
self.assertEqual(
result.returncode, 2, result.stdout + result.stderr
)
self.assertIn(
'absolute or workspace-relative destination', result.stdout
)
self.assertNotIn('unbound variable', result.stderr)
self.assertEqual(sentinel.read_text(), 'keep')

def test_empty_or_parent_traversal_destination_is_rejected(self):
for destination in ('', '../outside', 'new/../outside'):
with self.subTest(destination=destination):
Expand Down
22 changes: 21 additions & 1 deletion .github/resources/scripts/artifact_download_triggers_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,28 @@ def test_changes_trigger_all_pr_consumers(self):
cwd=ROOT,
)
triggered = set(json.loads(result.stdout))
# Master-only qualification must not be required on release PRs.
branch_script = f"""
const gate = require({json.dumps(str(MODULE))});
const {{inventory}} = gate.loadLocalInventory(process.cwd());
console.log(JSON.stringify(inventory.workflows.filter(workflow =>
workflow.pull_request !== null && gate.applicable(
Object.fromEntries(Object.entries(workflow.pull_request).filter(
([key]) => key !== 'paths' && key !== 'paths-ignore')),
{json.dumps(branch)}, []))
.map(workflow => workflow.path)));
"""
eligible = subprocess.run(
['node'],
input=branch_script,
check=True,
capture_output=True,
text=True,
cwd=ROOT,
)
required = consumers & set(json.loads(eligible.stdout))
self.assertTrue(
consumers.issubset(triggered), consumers - triggered
required.issubset(triggered), required - triggered
)


Expand Down
60 changes: 60 additions & 0 deletions .github/resources/scripts/configure_frontend_apt.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Copyright 2026 The Kubeflow Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Bound browser dependency acquisition on disposable hosted Ubuntu runners."""

import os
import re
from pathlib import Path

AZURE_MIRROR = re.compile(
r'https?://azure\.archive\.ubuntu\.com/ubuntu(?=[/\s]|$)'
)
APT_LIMITS = """Acquire::http::Timeout "30";
Acquire::https::Timeout "30";
Acquire::Retries "2";
"""


def configure(root):
sources = [root / 'apt-mirrors.txt', root / 'sources.list']
for pattern in ('*.list', '*.sources'):
sources.extend((root / 'sources.list.d').glob(pattern))
for source in sources:
if source.is_file():
before = source.read_text()
after = AZURE_MIRROR.sub(
'https://archive.ubuntu.com/ubuntu', before
)
if after != before:
source.write_text(after)
print(
f'Replaced unavailable Azure Ubuntu mirror in {source.name}'
)
(root / 'apt.conf.d' / '99-kfp-browser-acquisition').write_text(APT_LIMITS)


def main():
if not (
os.environ.get('CI') == 'true'
and os.environ.get('GITHUB_ACTIONS') == 'true'
and os.environ.get('RUNNER_ENVIRONMENT') == 'github-hosted'
):
raise RuntimeError(
'APT configuration requires a disposable GitHub runner'
)
configure(Path('/etc/apt'))


if __name__ == '__main__':
main()
Loading
Loading