Skip to content

Commit 6cb5fd0

Browse files
committed
Replace Docker-based smoke tests with native Node.js harness
Boot a minimal Backstage backend directly on the runner using createBackend() + dynamicPluginsFeatureLoader, probe /api/<pluginId> routes, and report results as structured JSON. Includes core bundled plugins (catalog, auth, permission, scaffolder, events, search, proxy) so dynamic plugins resolve their dependencies correctly. Made-with: Cursor
1 parent 9427721 commit 6cb5fd0

6 files changed

Lines changed: 563 additions & 150 deletions

File tree

.github/workflows/run-workspace-smoke-tests.yaml

Lines changed: 63 additions & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@ on:
1515
description: Newline-separated list of plugins that failed to load
1616
value: ${{ jobs.run.outputs.failed-plugins }}
1717
error-logs:
18-
description: Extracted error messages from container logs
18+
description: Extracted error messages from smoke test output
1919
value: ${{ jobs.run.outputs.error-logs }}
2020

2121
jobs:
2222
run:
2323
runs-on: ubuntu-latest
24-
timeout-minutes: 25
24+
timeout-minutes: 15
2525
permissions:
2626
contents: read
2727
packages: read
@@ -36,139 +36,81 @@ jobs:
3636
name: smoke-test-artifacts
3737
path: ./artifacts
3838

39-
- name: Log in to GitHub Container Registry
40-
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9
39+
- name: Setup Node.js
40+
uses: actions/setup-node@v4
4141
with:
42-
registry: ghcr.io
43-
username: ${{ github.actor }}
44-
password: ${{ secrets.GITHUB_TOKEN }}
42+
node-version: "20"
4543

46-
- name: Start RHDH with test plugins config
44+
- name: Install skopeo
4745
run: |
48-
set -euo pipefail
49-
ls -la ./artifacts/ || true
50-
46+
if ! command -v skopeo &>/dev/null; then
47+
sudo apt-get update -qq
48+
sudo apt-get install -y -qq skopeo
49+
fi
50+
skopeo --version
51+
52+
- name: Authenticate to GHCR
53+
run: echo "${{ secrets.GITHUB_TOKEN }}" | skopeo login ghcr.io -u "${{ github.actor }}" --password-stdin
54+
55+
- name: Verify artifacts
56+
run: |
57+
ls -la ./artifacts/
58+
ls -la ./artifacts/harness/ || true
5159
if [ ! -f "./artifacts/dynamic-plugins.test.yaml" ]; then
52-
echo "Error: dynamic-plugins.test.yaml not found in artifacts"
60+
echo "Error: dynamic-plugins.test.yaml not found"
5361
exit 1
5462
fi
63+
echo "=== dynamic-plugins.test.yaml (first 40 lines) ==="
64+
head -40 ./artifacts/dynamic-plugins.test.yaml || true
5565
56-
echo "dynamic-plugins.test.yaml contents:"
57-
sed -n '1,40p' ./artifacts/dynamic-plugins.test.yaml || true
58-
59-
# Build Docker run command with conditional volume mounts
60-
ENV_ARGS=$( [ -f ./artifacts/test.env ] && echo "--env-file ./artifacts/test.env" || echo "" )
61-
DOCKER_CMD="docker run -d --name rhdh -p 7007:7007 $ENV_ARGS"
62-
if [ -f "./artifacts/app-config.yaml" ]; then
63-
DOCKER_CMD="$DOCKER_CMD -v "$(pwd)"/artifacts/app-config.yaml:/opt/app-root/src/app-config.yaml"
64-
fi
65-
if [ -f "./artifacts/app-config.test.yaml" ]; then
66-
DOCKER_CMD="$DOCKER_CMD -v "$(pwd)"/artifacts/app-config.test.yaml:/opt/app-root/src/app-config.test.yaml"
67-
fi
68-
DOCKER_CMD="$DOCKER_CMD -v "$(pwd)"/artifacts/dynamic-plugins.test.yaml:/opt/app-root/src/dynamic-plugins.yaml"
69-
70-
# Add Docker config and environment variables
71-
echo "Using docker auth file: $HOME/.docker/config.json"
72-
DOCKER_CMD="$DOCKER_CMD -v $HOME/.docker/config.json:/root/.docker/config.json:ro"
73-
DOCKER_CMD="$DOCKER_CMD -e REGISTRY_AUTH_FILE=/root/.docker/config.json"
74-
75-
# Derive image tag from target branch
76-
TARGET_BRANCH="${{ inputs.target-branch }}"
77-
if [[ "$TARGET_BRANCH" =~ ^release-([0-9]+\.[0-9]+)$ ]]; then
78-
IMAGE_TAG="next-${BASH_REMATCH[1]}"
79-
else
80-
IMAGE_TAG="next"
81-
fi
82-
echo "Using RHDH image tag: $IMAGE_TAG (target branch: $TARGET_BRANCH)"
83-
84-
# Add image and command
85-
DOCKER_CMD="$DOCKER_CMD --entrypoint /bin/bash quay.io/rhdh-community/rhdh:${IMAGE_TAG} -c '"
86-
DOCKER_CMD="$DOCKER_CMD set -ex; "
87-
DOCKER_CMD="$DOCKER_CMD PLUGINS_ROOT=/opt/app-root/src/dynamic-plugins-root; "
88-
DOCKER_CMD="$DOCKER_CMD GENERATED_CONFIG=\$PLUGINS_ROOT/app-config.dynamic-plugins.yaml; "
89-
DOCKER_CMD="$DOCKER_CMD INSTALL_SCRIPT=/opt/app-root/src/install-dynamic-plugins.sh; "
90-
DOCKER_CMD="$DOCKER_CMD mkdir -p \$PLUGINS_ROOT; "
91-
DOCKER_CMD="$DOCKER_CMD \$INSTALL_SCRIPT \$PLUGINS_ROOT; "
92-
DOCKER_CMD="$DOCKER_CMD exec node packages/backend"
93-
94-
# Add config files to command (optional)
95-
[ -f "./artifacts/app-config.yaml" ] && DOCKER_CMD="$DOCKER_CMD --config /opt/app-root/src/app-config.yaml"
96-
[ -f "./artifacts/app-config.test.yaml" ] && DOCKER_CMD="$DOCKER_CMD --config /opt/app-root/src/app-config.test.yaml"
97-
DOCKER_CMD="$DOCKER_CMD --config /opt/app-root/src/dynamic-plugins.yaml"
98-
DOCKER_CMD="$DOCKER_CMD --config \$GENERATED_CONFIG'"
99-
100-
echo "Running: $DOCKER_CMD"
101-
eval "$DOCKER_CMD"
102-
103-
- name: Wait for RHDH to be ready
66+
- name: Install smoke test dependencies
67+
working-directory: ./artifacts/harness
68+
run: npm install --ignore-scripts 2>&1 | tail -5
69+
70+
- name: Run smoke test
71+
id: smoke-test
72+
working-directory: ./artifacts/harness
10473
run: |
105-
set -e
106-
for i in $(seq 1 10); do
107-
if curl -fsS http://localhost:7007/health >/dev/null; then
108-
echo "RHDH is ready"; exit 0; fi
109-
echo "Waiting for RHDH... (Attempt ${i}/10)"
110-
# Check if container is still running
111-
if ! docker ps | grep -q rhdh; then
112-
echo "Container stopped unexpectedly."
113-
exit 1
114-
fi
115-
sleep 10
116-
done
117-
echo "RHDH did not become ready in time."
118-
exit 1
74+
set -o pipefail
11975
120-
- name: List installed plugins
121-
run: docker exec rhdh ls -l /opt/app-root/src/dynamic-plugins-root
76+
CONFIG_ARGS="--config app-config.yaml"
77+
[ -f ../app-config.yaml ] && CONFIG_ARGS="$CONFIG_ARGS --config ../app-config.yaml"
78+
[ -f ../app-config.test.yaml ] && CONFIG_ARGS="$CONFIG_ARGS --config ../app-config.test.yaml"
12279
123-
- name: Print generated dynamic plugins config
124-
run: docker exec rhdh cat /opt/app-root/src/dynamic-plugins-root/app-config.dynamic-plugins.yaml
80+
ENV_ARGS=""
81+
[ -f ../test.env ] && ENV_ARGS="--env-file ../test.env"
12582
126-
- name: Verify plugin loading
83+
echo "Running: node smoke-test.mjs --plugins-yaml ../dynamic-plugins.test.yaml $CONFIG_ARGS $ENV_ARGS"
84+
85+
node smoke-test.mjs \
86+
--plugins-yaml ../dynamic-plugins.test.yaml \
87+
$CONFIG_ARGS \
88+
$ENV_ARGS \
89+
2>&1 | tee ../smoke-test.log
90+
91+
- name: Collect results
12792
id: collect-results
93+
if: always()
12894
run: |
129-
set -e
130-
PLUGINS=$(grep -Eo '!([^[:space:]]+)' ./artifacts/dynamic-plugins.test.yaml | sed -e 's/^!//' -e 's/"$//' | sort -u)
131-
if [ -z "$PLUGINS" ]; then
132-
echo "No plugins found in dynamic-plugins.test.yaml"
133-
echo "success=false" >> "$GITHUB_OUTPUT"
134-
echo "failed-plugins<<EOF" >> "$GITHUB_OUTPUT"
135-
echo "(no-plugins)" >> "$GITHUB_OUTPUT"
136-
echo "EOF" >> "$GITHUB_OUTPUT"
137-
exit 0
138-
fi
139-
LOGS=$(docker logs rhdh || true)
140-
failures=()
141-
echo "===== Checking logs for plugin loaded messages"
142-
for plugin in $PLUGINS; do
143-
echo "Asserting plugin loaded: $plugin"
144-
# Match either the unpack path or the canonical "loaded dynamic ... plugin '<pkg>' from" message
145-
if echo "$LOGS" | grep -qiE "loaded dynamic .* plugin .*${plugin}(-dynamic)?'" ; then
146-
echo "Plugin loaded: $plugin"
147-
else
148-
echo "Plugin NOT loaded: $plugin"
149-
failures+=("$plugin")
150-
fi
151-
done
152-
if echo "$LOGS" | grep -E "(InstallException|Error while adding OCI plugin|Failed to load dynamic plugin|dynamic plugin.*error)" >/dev/null; then
153-
echo "Detected dynamic plugin loading errors in logs"
154-
failures+=("(log-errors)")
155-
fi
156-
if [ ${#failures[@]} -eq 0 ]; then
157-
echo "success=true" >> "$GITHUB_OUTPUT"
95+
RESULTS_FILE="./artifacts/harness/results.json"
96+
if [ -f "$RESULTS_FILE" ]; then
97+
SUCCESS=$(jq -r '.success' "$RESULTS_FILE")
98+
FAILED=$(jq -r '.failedPlugins | join("\n")' "$RESULTS_FILE")
99+
echo "success=$SUCCESS" >> "$GITHUB_OUTPUT"
158100
echo "failed-plugins<<EOF" >> "$GITHUB_OUTPUT"
159-
echo "" >> "$GITHUB_OUTPUT"
101+
echo "$FAILED" >> "$GITHUB_OUTPUT"
160102
echo "EOF" >> "$GITHUB_OUTPUT"
161103
else
162104
echo "success=false" >> "$GITHUB_OUTPUT"
163105
echo "failed-plugins<<EOF" >> "$GITHUB_OUTPUT"
164-
printf "%s\n" "${failures[@]}" >> "$GITHUB_OUTPUT"
106+
echo "(results-file-missing)" >> "$GITHUB_OUTPUT"
165107
echo "EOF" >> "$GITHUB_OUTPUT"
166108
fi
167109
168-
- name: Fail if any plugin failed to load
110+
- name: Fail if smoke test failed
169111
if: ${{ steps.collect-results.outputs.success != 'true' }}
170112
run: |
171-
echo "The following plugins failed to load to RHDH:"
113+
echo "Smoke test failed. Failed plugins:"
172114
echo "${{ steps.collect-results.outputs.failed-plugins }}"
173115
exit 1
174116
@@ -177,30 +119,21 @@ jobs:
177119
id: capture-errors
178120
continue-on-error: true
179121
run: |
180-
if ! docker ps -a | grep -q rhdh; then
181-
echo "error-logs<<EOF" >> "$GITHUB_OUTPUT"
182-
echo "'rhdh' container was not available. Check earlier steps for startup errors." >> "$GITHUB_OUTPUT"
183-
echo "EOF" >> "$GITHUB_OUTPUT"
184-
exit 0
185-
fi
186-
187-
LOGS=$(docker logs rhdh 2>&1 || echo "Failed to retrieve logs")
188-
189-
# Extract errors from logs with short context
190-
ERROR_LINES=$(echo "$LOGS" | grep -iE -B 2 -A 2 "(error|exception|failed|failure|installException)" | grep -vE "(no errors|successfully|resolved|fixed)" | grep -v "^--$" | tail -n 100 || echo "")
191-
122+
LOG_FILE="./artifacts/smoke-test.log"
192123
echo "error-logs<<EOF" >> "$GITHUB_OUTPUT"
193-
if [ -n "$ERROR_LINES" ] && [ "$ERROR_LINES" != "" ]; then
194-
echo "$ERROR_LINES" >> "$GITHUB_OUTPUT"
124+
if [ -f "$LOG_FILE" ]; then
125+
grep -iE -B 1 -A 1 "(error|exception|failed|failure)" "$LOG_FILE" \
126+
| grep -vE "(no errors|successfully|resolved)" \
127+
| tail -100 || echo "No error patterns found in logs."
195128
else
196-
echo "No specific error patterns found in container logs. Check full workflow logs for details." >> "$GITHUB_OUTPUT"
129+
echo "No smoke test log available."
197130
fi
198131
echo "EOF" >> "$GITHUB_OUTPUT"
199132
200-
- name: Print container logs
133+
- name: Print full smoke test log
201134
if: always()
202-
run: docker logs rhdh || true
135+
run: cat ./artifacts/smoke-test.log 2>/dev/null || echo "No log file"
203136

204137
- name: Cleanup
205138
if: always()
206-
run: docker rm -f rhdh || true
139+
run: rm -rf ./artifacts/harness/dynamic-plugins-root ./artifacts/harness/.tmp-oci || true

.github/workflows/workspace-tests.yaml

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ jobs:
119119
console.log('Missing PR or commit; skipping pending status');
120120
return;
121121
}
122-
122+
123123
await github.rest.repos.createCommitStatus({
124124
owner: context.repo.owner,
125125
repo: context.repo.repo,
@@ -205,7 +205,7 @@ jobs:
205205
# Always include the root-level default config
206206
ROOT_CONFIG="$GITHUB_WORKSPACE/smoke-tests/app-config.yaml"
207207
[ -f "$ROOT_CONFIG" ] && cp "$ROOT_CONFIG" "$OUT_DIR/app-config.yaml"
208-
208+
209209
# Read workspace-wide test.env if it exists
210210
WORKSPACE_ENV_FILE="$WORKSPACE_PATH/smoke-tests/test.env"
211211
WORKSPACE_ENV_CONTENT=""
@@ -309,6 +309,16 @@ jobs:
309309
echo "plugins-metadata-complete=$PLUGINS_METADATA_COMPLETE" >> "$GITHUB_OUTPUT"
310310
echo "skip-tests-missing-env=$SKIP_TESTS_MISSING_ENV" >> "$GITHUB_OUTPUT"
311311
312+
- name: Stage smoke test harness
313+
env:
314+
WORKSPACE_PATH: ${{ needs.resolve.outputs.workspace }}
315+
run: |
316+
HARNESS_DIR="$WORKSPACE_PATH/smoke-tests/harness"
317+
mkdir -p "$HARNESS_DIR"
318+
cp smoke-tests/package.json "$HARNESS_DIR/"
319+
cp smoke-tests/smoke-test.mjs "$HARNESS_DIR/"
320+
cp smoke-tests/app-config.yaml "$HARNESS_DIR/"
321+
312322
- name: Upload smoke test artifacts
313323
uses: actions/upload-artifact@v4
314324
with:
@@ -352,11 +362,11 @@ jobs:
352362
const workspace = core.getInput('workspace');
353363
const pluginsMetadataComplete = core.getInput('plugins_metadata_complete') === 'true';
354364
const skipTestsMissingEnv = core.getInput('skip_tests_missing_env') === 'true';
355-
365+
356366
let statusDescription = 'Skipped';
357367
let commentDetail = ' skipped for an unknown reason. Check workflow run for details.\n';
358368
let summaryDetail = 'Unknown reason. Check workflow run for details.';
359-
369+
360370
if (!workspace || workspace === '') {
361371
statusDescription = 'Skipped: PR doesn\'t touch one workspace';
362372
commentDetail = ' skipped: PR doesn\'t touch exactly one workspace.\n';
@@ -370,7 +380,7 @@ jobs:
370380
commentDetail = ' skipped: missing plugin metadata files (`<workspace>/metadata/*.yaml`).\n';
371381
summaryDetail = 'Missing plugin metadata files (`<workspace>/metadata/*.yaml`).';
372382
}
373-
383+
374384
if (overlayCommit) {
375385
await github.rest.repos.createCommitStatus({
376386
owner: context.repo.owner,
@@ -383,7 +393,7 @@ jobs:
383393
});
384394
console.log(`Set success status on ${overlayCommit} (skipped for valid reason)`);
385395
}
386-
396+
387397
if (pr) {
388398
await github.rest.issues.createComment({
389399
owner: context.repo.owner,
@@ -392,7 +402,7 @@ jobs:
392402
body: `:warning: \n[Smoke test workflow](${runUrl})${commentDetail}`,
393403
});
394404
}
395-
405+
396406
await core.summary
397407
.addRaw('\n### Tests Skipped\n\n' + summaryDetail)
398408
.write();
@@ -431,10 +441,10 @@ jobs:
431441
const errorLogs = (process.env.ERROR_LOGS || '').trim();
432442
const pr = Number(process.env.PR_NUMBER);
433443
const overlayCommit = process.env.OVERLAY_COMMIT;
434-
444+
435445
const success = smokeTestsResult === 'success' && successOutput === 'true';
436446
let failureReason = '';
437-
447+
438448
if (!success) {
439449
switch (smokeTestsResult) {
440450
case 'failure':
@@ -450,7 +460,7 @@ jobs:
450460
failureReason = `Smoke tests ended in an unexpected state: ${smokeTestsResult}. Check the workflow logs for details.`;
451461
}
452462
}
453-
463+
454464
// Write step summary (always, even if PR is unavailable)
455465
let summary;
456466
if (success) {
@@ -465,23 +475,23 @@ jobs:
465475
}
466476
}
467477
await core.summary.addRaw(summary).write();
468-
478+
469479
if (!pr) {
470480
console.log('No PR associated; skipping status and comment');
471481
return;
472482
}
473-
483+
474484
// Get current PR head SHA
475485
const { data: prData } = await github.rest.pulls.get({
476486
owner: context.repo.owner,
477487
repo: context.repo.repo,
478488
pull_number: pr,
479489
});
480-
490+
481491
// Use PR head if different from overlayCommit (/smoketest command), else use overlayCommit (immediate publish)
482492
const sha = prData.head.sha !== overlayCommit ? prData.head.sha : overlayCommit;
483493
console.log(`Status SHA: ${sha} (PR head: ${prData.head.sha}, overlay: ${overlayCommit})`);
484-
494+
485495
await github.rest.repos.createCommitStatus({
486496
owner: context.repo.owner,
487497
repo: context.repo.repo,
@@ -491,7 +501,7 @@ jobs:
491501
target_url: runUrl,
492502
context: 'smoketest',
493503
});
494-
504+
495505
let body;
496506
if (success) {
497507
body = `:white_check_mark: [Smoke tests workflow](${runUrl}) passed. All plugins loaded successfully.\n`;
@@ -504,7 +514,7 @@ jobs:
504514
body += `\n\n<details><summary>Error logs from container</summary>\n\n\`\`\`\n${errorLogs}\n\`\`\`\n\n</details>`;
505515
}
506516
}
507-
517+
508518
await github.rest.issues.createComment({
509519
issue_number: pr,
510520
owner: context.repo.owner,

0 commit comments

Comments
 (0)