Skip to content
Open
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
96 changes: 96 additions & 0 deletions .github/workflows/test-renovate-ui.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
name: Test UI on Renovate PRs

on:
workflow_dispatch:
inputs:
debug_shell:
description: "Debug shell"
required: true
type: boolean
pull_request:
branches:
- main
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Workflow must run on renovate branches. IIRC this is the branch prefix:

Suggested change
- main
- renovate-*

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we should put the name of the base branch, i.e. the branch we are merging into

paths:
- "ui/**"
- "build-images.sh"

concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true

jobs:
check-author:
runs-on: ubuntu-latest
permissions: {}
outputs:
is_renovate: ${{ steps.check.outputs.is_renovate }}
steps:
- id: check
env:
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
run: |
# On workflow_dispatch, PR_AUTHOR is empty — allow the run.
# On pull_request, only allow renovate[bot].
if [[ -z "$PR_AUTHOR" || "$PR_AUTHOR" == "renovate[bot]" ]]; then
echo "is_renovate=true" >> "$GITHUB_OUTPUT"
else
echo "is_renovate=false" >> "$GITHUB_OUTPUT"
fi

publish-images:
needs: check-author
if: needs.check-author.outputs.is_renovate == 'true'
uses: NethServer/ns8-github-actions/.github/workflows/publish-branch.yml@v1
permissions:
packages: write
actions: read
contents: write

module:
needs: publish-images
permissions: {}
uses: NethServer/ns8-github-actions/.github/workflows/module-info.yml@v1

chooser:
needs: check-author
if: needs.check-author.outputs.is_renovate == 'true'
runs-on: ubuntu-latest
permissions: {}
outputs:
node_a: ${{ steps.pick.outputs.node_a }}
node_b: ${{ steps.pick.outputs.node_b }}
steps:
- id: pick
run: |
if (( $GITHUB_RUN_NUMBER % 2 )); then
echo "node_a=rl1" >> "$GITHUB_OUTPUT"
echo "node_b=dn1" >> "$GITHUB_OUTPUT"
else
echo "node_a=dn1" >> "$GITHUB_OUTPUT"
echo "node_b=rl1" >> "$GITHUB_OUTPUT"
fi

run_ui_tests:
needs: [module, chooser]
permissions: {}
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
scenario: [install, update]
uses: NethServer/ns8-github-actions/.github/workflows/test-on-digitalocean-infra.yml@v1
with:
script: test-ui.sh
path: ui
coremodules: ${{ matrix.scenario == 'install' && format('ghcr.io/{0}/{1}:{2}', needs.module.outputs.owner, needs.module.outputs.name, needs.module.outputs.tag) || '' }}
leader_nodes: >-
${{
matrix.scenario == 'install'
&& needs.chooser.outputs.node_a
|| needs.chooser.outputs.node_b
}}
args: ${{ format('ghcr.io/{0}/{1}:{2} -v SCENARIO:{3}', needs.module.outputs.owner, needs.module.outputs.name, needs.module.outputs.tag, matrix.scenario) }}
repo_ref: ${{needs.module.outputs.sha}}
debug_shell: ${{ github.event.inputs.debug_shell == 'true' || false }}
secrets:
do_token: ${{ secrets.do_token }}
3 changes: 3 additions & 0 deletions ui/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,6 @@ pnpm-debug.log*
*.njsproj
*.sln
*.sw?

# tests outputs
tests/outputs
42 changes: 42 additions & 0 deletions ui/test-ui.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#!/bin/bash

#
# Copyright (C) 2026 Nethesis S.r.l.
# SPDX-License-Identifier: GPL-3.0-or-later
#

set -e

SSH_KEYFILE=${SSH_KEYFILE:-$HOME/.ssh/id_rsa}

LEADER_NODE="${1:?missing LEADER_NODE argument}"
IMAGE_URL="${2:?missing IMAGE_URL argument}"

ssh_key="$(< $SSH_KEYFILE)"

cleanup() {
set +e
podman cp rf-core-runner:/home/pwuser/outputs tests/
podman stop rf-core-runner
podman rm rf-core-runner
}

trap cleanup EXIT

podman run -i \
--network=host \
--volume=.:/home/pwuser/ns8-module:z \
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This script installs Python deps on every run but does not use the site-packages volume cache that test-module.sh uses, which will slow down CI and increase external dependency flakiness. Consider adding the same cached volume mount (or another cache mechanism) for pip install artifacts.

Suggested change
--volume=.:/home/pwuser/ns8-module:z \
--volume=.:/home/pwuser/ns8-module:z \
--volume=./.pip-cache:/home/pwuser/.cache/pip:z \

Copilot uses AI. Check for mistakes.
--name rf-core-runner ghcr.io/marketsquare/robotframework-browser/rfbrowser-stable:19.11.0 \
bash -l -s <<EOF
set -e
echo "$ssh_key" > /home/pwuser/ns8-key
pip install -q -r /home/pwuser/ns8-module/tests/pythonreq.txt
Comment on lines +15 to +33
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This script runs a third-party container image ghcr.io/marketsquare/robotframework-browser/rfbrowser-stable:19.11.0 with --network=host and passes in the private SSH key contents via ssh_key, giving that image direct access to secrets and your test infrastructure. Because the image is pinned only by a mutable tag and comes from an external organization, a compromised or hijacked image could exfiltrate the SSH key or tamper with tests without any integrity check. Pin this image to an immutable digest (and/or vendor it under your own namespace) and restrict secrets exposure so that only trusted, first-party images ever see private keys.

Copilot uses AI. Check for mistakes.
cd /home/pwuser/ns8-module/ui
mkdir -vp tests/outputs
exec robot \
-v NODE_ADDR:${LEADER_NODE} \
-v IMAGE_URL:${IMAGE_URL} \
-v SSH_KEYFILE:/home/pwuser/ns8-key \
--name ui-tests \
-d tests/outputs tests/
EOF
3 changes: 3 additions & 0 deletions ui/tests/pythonreq.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
robotframework
robotframework-sshlibrary
robotframework-browser
Comment on lines +1 to +3
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test container installs Python dependencies from pythonreq.txt without pinning versions, meaning each run may pull arbitrary new releases of robotframework, robotframework-sshlibrary, or robotframework-browser from PyPI into an environment that has access to SSH credentials and your cluster. If any of these packages (or their transitive dependencies) are compromised in the future, the attack code would automatically execute in CI with the ability to exfiltrate secrets or tamper with test results. Pin these packages to specific, vetted versions (or hashes) to ensure only known-good artifacts are used in the test pipeline.

Copilot uses AI. Check for mistakes.
51 changes: 51 additions & 0 deletions ui/tests/test_ui.robot
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
*** Settings ***
Library SSHLibrary
Library Browser
Suite Setup Connect to the node

*** Variables ***
${SSH_KEYFILE} %{HOME}/.ssh/id_ecdsa
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

${SSH_KEYFILE} defaults to %{HOME}/.ssh/id_ecdsa, but ui/test-ui.sh defaults to id_rsa and CI passes SSH_KEYFILE explicitly via -v. To avoid local/CI mismatches, consider removing the hard-coded default here (rely on the passed-in variable) or aligning it with the script default.

Suggested change
${SSH_KEYFILE} %{HOME}/.ssh/id_ecdsa
${SSH_KEYFILE} %{HOME}/.ssh/id_rsa

Copilot uses AI. Check for mistakes.
${ADMIN_USER} admin
${ADMIN_PASSWORD} Nethesis,1234

*** Keywords ***
Connect to the node
Open Connection ${NODE_ADDR}
Login With Public Key root ${SSH_KEYFILE}
${output} = Execute Command systemctl is-system-running --wait
Should Be True '${output}' == 'running' or '${output}' == 'degraded'

Login to cluster-admin
New Page https://${NODE_ADDR}/cluster-admin/
Fill Text text="Username" ${ADMIN_USER}
Click button >> text="Continue"
Fill Text text="Password" ${ADMIN_PASSWORD}
Click button >> text="Log in"
Wait For Elements State css=#main-content visible timeout=10s

*** Test Cases ***
Install module
${output} ${rc} = Execute Command add-module ${IMAGE_URL} 1
... return_rc=True
Should Be Equal As Integers ${rc} 0
&{output} = Evaluate ${output}
Set Suite Variable ${module_id} ${output.module_id}

Take screenshots
New Browser chromium headless=True
New Context ignoreHTTPSErrors=True
Login to cluster-admin
Go To https://${NODE_ADDR}/cluster-admin/#/apps/${module_id}
Wait For Elements State iframe >>> h2 >> text="Status" visible timeout=10s
Sleep 5s
Take Screenshot filename=${OUTPUT DIR}/browser/screenshot/status.png
Go To https://${NODE_ADDR}/cluster-admin/#/apps/${module_id}?page=settings
Wait For Elements State iframe >>> h2 >> text="Settings" visible timeout=10s
Sleep 5s
Take Screenshot filename=${OUTPUT DIR}/browser/screenshot/settings.png
Close Browser

Remove module
${rc} = Execute Command remove-module --no-preserve ${module_id}
... return_rc=True return_stdout=False
Should Be Equal As Integers ${rc} 0
Loading