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
84 changes: 84 additions & 0 deletions .github/workflows/test-module.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
name: Test module (robot)

# Robot Framework integration tests for the NS8 module. The suite in
# packaging/ns8/tests installs the module on a real NS8 cluster, configures it,
# checks the route, screenshots the UI and removes it again.
#
# WHY THIS DOES NOT USE THE STOCK NS8 WORKFLOW
#
# NethServer/ns8-github-actions provides test-module.yml@v1, the one-line
# entry point every NS8 module uses. It cannot be used here. It derives the
# image under test from the repository name:
#
# name = basename(repository), with any leading "ns8-" stripped
# owner = lowercased repo owner
# tag = head branch, with "main" forced to "latest"
# image = ghcr.io/<owner>/<name>:<tag>
#
# For this repository that yields ghcr.io/calnode/calnode:latest, which is two
# wrong answers at once:
# * the module image is ghcr.io/calnode/ns8-calnode, not ghcr.io/calnode/calnode
# (the latter is the Calnode application image, which has no NS8 module
# manifest, so add-module would fail);
# * this repo deliberately never publishes a :latest module tag. :latest stays

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This claim is false, and the very next clause contradicts it: ns8-module.yml:118-121 publishes module latest on a vX.Y.Z tag (module_tags=${version} ${version%.*} latest), and push-manifest.sh pushes every entry in IMAGETAGS. Only main publishes edge, which is what README.md:149-151 already says accurately. The conclusion survives — the stock workflow resolves the name to calnode, not ns8-calnode, which alone settles it — but this header exists so nobody re-derives the decision, and a header whose second reason expires at the first module release is the kind of thing that gets someone to "fix" the workflow back.

Technical details
# The ":latest is never published" justification is wrong

## Affected sites
- `.github/workflows/test-module.yml:23-25` — asserts the repo "deliberately never publishes a :latest module tag", then says ":latest stays pinned to releases" in the same bullet.
- `.github/workflows/ns8-module.yml:118-121` — on `refs/tags/v*`, `module_tags=${version} ${version%.*} latest`.
- `packaging/ns8/push-manifest.sh:51-72` — iterates `IMAGETAGS` and pushes each assembled manifest, so `latest` really lands.
- `packaging/ns8/README.md:149-151` — states the accurate version: "no module release has been cut yet".

## Required outcome
- Drop the second bullet, or restate it as what is actually true: `main` publishes `:edge`, and `:latest` only moves on a release tag. The first bullet (wrong image *name*) is by itself sufficient justification and does not expire.

## Suggested approach
- Replace lines 23-25 with something like: `:latest moves only on a release tag; main publishes :edge, mirroring docker-publish.yml.`
- Consider also correcting the same claim in the PR description, which repeats it.

# pinned to releases, and main publishes :edge, exactly as docker-publish.yml
# does for the app image.
#
# So this workflow calls test-on-digitalocean-infra.yml@v1 directly, which is
# the reusable workflow test-module.yml delegates to anyway, and supplies the
# image URL itself via the `args` input. Everything else (DigitalOcean
# provisioning, NS8 install, create-cluster, the test runner, artifact upload,
# tmate debugging) is still theirs and unmodified.
#
# NOT RUNNING AUTOMATICALLY YET
#
# It needs a `do_token` repository secret (DigitalOcean API token) for the
# cluster provisioning, and this repo does not have one. Declaring the workflow
# with a workflow_run trigger now would just fail red on every publish, so it is
# manual-only until that is sorted out.
on:
workflow_dispatch:
inputs:
image:
description: 'Module image under test'
required: false
type: string
default: 'ghcr.io/calnode/ns8-calnode:edge'
run_ui_tests:
description: 'Run the [ui] tests (slower, needs the Playwright image)'
required: false
type: boolean
default: false
debug_shell:
description: 'Hand out a tmate debug shell on failure'
required: false
type: boolean
default: false
# To run this on every successful publish instead of by hand, add:
# workflow_run:
# workflows: ['NS8 module publish']
Comment on lines +58 to +59

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Uncommenting this as written produces a red, billable run. Under workflow_run the inputs context is empty and default: is not applied, so args: passes nothing — and that second positional argument is exactly what selects module mode in test-module.sh:20-28, which then never defines IMAGE_URL and test 1 dies on add-module ${IMAGE_URL}. Two more fall out of the same edit: run_ui_tests/debug_shell become empty strings where upstream declares type: boolean, and repo_ref falls back to github.sha (the default-branch tip, not the commit that was published), so the checkout would not match the image under test.

Technical details
# The documented `workflow_run` follow-up is not self-contained

## Affected sites
- `.github/workflows/test-module.yml:57-61` — the commented trigger block.
- `.github/workflows/test-module.yml:77` — `args: ${{ inputs.image }}`, empty under `workflow_run`.
- `.github/workflows/test-module.yml:81-82` — the two `type: boolean` inputs, empty under `workflow_run`.
- `packaging/ns8/tests/10__calnode.robot:10` — `add-module ${IMAGE_URL}`, undefined in core mode.
- Upstream `scripts/test-module.sh:20-28` — `if [ -n "${2:-}" ] && [[ $2 != -* ]]; then IMAGE_URL="$2"; mode="module"` … `else mode="core"`.
- Upstream `scripts/test-module.sh:137-139` — `-v "IMAGE_URL:${IMAGE_URL}"` is added only in module mode.
- Upstream `test-on-digitalocean-infra.yml:19-23,58-62` — `repo_ref` defaults to `${{ github.sha }}`; `run_ui_tests`/`debug_shell` are `type: boolean`.
- Upstream `test-module.yml:22,66` — the stock path guards with `if: github.event.workflow_run.conclusion == 'success'` and threads `repo_ref: ${{ needs.module.outputs.sha }}` (= `github.event.workflow_run.head_sha || github.sha`).

## Required outcome
- Whatever the trigger becomes, the run must (a) only fire on a successful publish, (b) derive the image URL and the tested ref from `github.event.workflow_run.*` rather than from `inputs`, since `inputs` does not exist for that event.

## Suggested approach
- Add the success guard: `if: github.event.workflow_run.conclusion == 'success'` on the `test` job.
- Give each `with:` value a fallback that works for both events, e.g. `args: ${{ inputs.image || format('ghcr.io/calnode/ns8-calnode:{0}', ...) }}` or, more simply, compute a local `env:` block that prefers `inputs.*` and falls back to the workflow_run ref.
- Set `repo_ref: ${{ github.event.workflow_run.head_sha || github.sha }}`.
- Note that `types: [completed]` also fires for the `pull_request` runs of "NS8 module publish", which build but push nothing — those would test the previous `edge` image against the PR's test files.

## Open questions for the human
- Is `workflow_run` the intended trigger at all, or should this become `workflow_call` so the `NS8 module publish` workflow calls it directly with an explicit image? That sidesteps every `inputs` problem above.

# types: [completed]
# ...after setting the do_token secret.

jobs:
test:
name: Run tests
permissions:
contents: read
pull-requests: write
statuses: write
uses: NethServer/ns8-github-actions/.github/workflows/test-on-digitalocean-infra.yml@v1
with:
# packaging/ns8 holds both the module definition and tests/, so this is the
# module root for the purposes of the runner.
path: packaging/ns8
# `args` is the second positional argument to run-ns8-tests, i.e. the
# IMAGE_URL that the suite passes to add-module. See the header note.
args: ${{ inputs.image }}
# One leader node. The default is "dn1 rl1" (two droplets); the suite is
# single-node, so a second one only doubles the cost.
leader_nodes: rl1
run_ui_tests: ${{ inputs.run_ui_tests }}
debug_shell: ${{ inputs.debug_shell }}
secrets:
do_token: ${{ secrets.do_token }}
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,6 @@ pnpm-debug.log*
# Vitest browser-mode artifacts (failure screenshots / attachments)
__screenshots__/
.vitest-attachments/

# NS8 Robot Framework test output (output.xml, log.html, report.html, screenshots)
packaging/ns8/tests/outputs/
43 changes: 37 additions & 6 deletions packaging/ns8/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,11 @@ byte-identical across reconfigures, `remove-module --no-preserve` with
automatic route cleanup, and a full cluster backup → destroy → restore cycle:
after restoring, the app served 200, logged "keyvault: DEK unwrapped from
keystore", the encryption-key fingerprint was unchanged, and an event type
created before the backup was still there. Still untested live: the
automated `tests/` robot suite, which needs a test cluster.
created before the backup was still there.

The `tests/` robot suite is wired up and dry-run clean (5/5, Robot 7.5), but it
has still never been executed against a live cluster. Every live check above
was done by hand. See [Testing](#testing).

## Uninstall

Expand Down Expand Up @@ -168,10 +171,38 @@ reference into the module environment as `CALNODE_IMAGE`, which is what the
systemd unit and configure-module actually run. The label is the single
source of truth; nothing is stamped at build time.

Running the test suite needs an NS8 test cluster; see the
[NS8 testing docs](https://github.com/NethServer/ns8-github-actions/blob/v1/README.md#running-tests-locally).
From this directory: `run-ns8-tests <NODE_ADDR> ghcr.io/calnode/ns8-calnode:edge`.
Not yet run in CI: no test cluster is wired up.
## Testing

The suite in `tests/` is Robot Framework. It installs the module on a real NS8
cluster, configures it, checks the route responds, screenshots the settings UI
and removes the module again:

| file | what it does |
|---|---|
| `10__calnode.robot` | install, configure, route responds |
| `15__ui.robot` | screenshots of the Status and Settings pages, tagged `ui` |
| `20__remove.robot` | removal |

Locally, against a cluster you already have (see the
[NS8 testing docs](https://github.com/NethServer/ns8-github-actions/blob/v1/README.md#running-tests-locally)):

```
run-ns8-tests <NODE_ADDR> ghcr.io/calnode/ns8-calnode:edge
```

`NODE_ADDR` must be reachable by SSH as root, using the key at `~/.ssh/id_ecdsa`
or wherever `SSH_KEYFILE` points. Add `RUN_UI_TESTS=true` to include the `ui`
tests; without it they are skipped and the runner uses a lighter image.

In CI, `.github/workflows/test-module.yml` does the same thing and provisions
the cluster on DigitalOcean. It is manual-only for now because it needs a
`do_token` repository secret this repo does not have yet. It also deliberately
does not use the stock `test-module.yml@v1`, which derives the image under test
from the repository name and would resolve it to `ghcr.io/calnode/calnode:latest`
instead of `ghcr.io/calnode/ns8-calnode:edge`; the workflow header explains the
full reasoning.

Output lands in `tests/outputs/` and is git-ignored.

## UI translation

Expand Down
25 changes: 25 additions & 0 deletions packaging/ns8/tests/10__calnode.robot
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
*** Settings ***
Library SSHLibrary

*** Variables ***
${TEST_HOST} calnode.example.test
${module_id} ${EMPTY}

*** Test Cases ***
Check if ns8-calnode is installed correctly
${output} ${rc} = Execute Command add-module ${IMAGE_URL} 1
... return_rc=True
Should Be Equal As Integers ${rc} 0
&{output} = Evaluate ${output}
Set Global Variable ${module_id} ${output.module_id}

Check if ns8-calnode can be configured
${rc} = Execute Command api-cli run module/${module_id}/configure-module --data '{"host": "${TEST_HOST}", "lets_encrypt": false, "http2https": false}'
... return_rc=True return_stdout=False
Should Be Equal As Integers ${rc} 0

Check if ns8-calnode works as expected
# Host-based route: pass the hostname explicitly since it does not resolve here
${rc} = Execute Command curl -f -H 'Host: ${TEST_HOST}' http://127.0.0.1/
... return_rc=True return_stdout=False
Should Be Equal As Integers ${rc} 0
37 changes: 37 additions & 0 deletions packaging/ns8/tests/15__ui.robot
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
*** Settings ***
Documentation Settings-UI screenshots. Excluded automatically unless the runner
... is started with RUN_UI_TESTS=true.

*** Variables ***
${ADMIN_USER} admin
${ADMIN_PASSWORD} Nethesis,1234

*** Keywords ***
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 ***
Take screenshots
[Tags] ui
# Imported here rather than in *** Settings *** on purpose: the runner uses a
# different image, without robotframework-browser, whenever RUN_UI_TESTS is
# unset. A suite-level import would fail the whole run before the [ui]
# exclusion could skip this one test.
Import Library Browser
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/1._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/2._Settings.png
Close Browser
8 changes: 8 additions & 0 deletions packaging/ns8/tests/20__remove.robot
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
*** Settings ***
Library SSHLibrary

*** Test Cases ***
Check if ns8-calnode is removed correctly
${rc} = Execute Command remove-module --no-preserve ${module_id}
... return_rc=True return_stdout=False
Should Be Equal As Integers ${rc} 0
57 changes: 0 additions & 57 deletions packaging/ns8/tests/calnode.robot

This file was deleted.

Loading