feat: E2E coverage instrumentation with CI pipeline for all dynamic plugins#2383
feat: E2E coverage instrumentation with CI pipeline for all dynamic plugins#2383gustavolira wants to merge 16 commits into
Conversation
|
/review |
There was a problem hiding this comment.
Review:
the Playwright integration can be significantly simplified by replacing custom code with nyc CLI. See inline comments.
Also: Coverage collection (E2E_COLLECT_COVERAGE=1) should only be enabled on the e2e-ocp-helm PR check job — nightly uses released OCI refs (not instrumented builds), so coverage would produce empty data.
Istanbul-based coverage for dynamic plugin E2E tests, with automated CI that builds instrumented OCI images only when source.json changes and skips builds when the image already exists. Coverage infrastructure: - e2e-coverage/coverage-utils.ts: shared types (CoverageData) and merge logic - e2e-coverage/coverage-fixture.ts: Playwright fixture collecting window.__coverage__ - e2e-coverage/coverage-reporter.ts: merges Istanbul JSON, converts to lcov Build and upload scripts: - scripts/instrument-plugin.sh: clones upstream at source.json ref, builds plugin, instruments final webpack output with nyc (post-build, survives module federation) - scripts/upload-coverage.sh: uploads lcov to Codecov with cross-repo attribution and per-workspace flags (e2e-<workspace>) for dashboard filtering CI workflow (.github/workflows/build-instrumented-plugins.yaml): - Triggers on push to main when workspaces/*/source.json changes - Manual dispatch with optional workspace and force-rebuild inputs - Matrix strategy: builds all workspaces with e2e-tests/ in parallel - Caching: checks if instrumented OCI image already exists for the source.json ref before building (skips if unchanged) - Publishes instrumented bundles as OCI artifacts to ghcr.io Ref: RHIDP-13411 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move all user-controlled inputs (inputs.workspace, matrix.workspace) to env vars instead of interpolating directly in run blocks. Add input validation for workspace name format. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…age modules Workflow (build-instrumented-plugins.yaml): - Use fetch-depth: 0 and github.event.before for multi-commit push detection - Add timeout-minutes: 45 to build jobs - Replace python3 JSON parsing with jq - Fix node-version-file: extract version via jq (versions.json format unsupported) - Redirect error messages to stderr TypeScript (e2e-coverage/): - Use node: protocol for fs and path imports - Split CoverageData into SourceLocation, FileCoverage, CoverageData interfaces - Remove dead mergedCoverage/testCount state and duplicate mergeCoverageFiles() - Merge double fnMap iteration into single loop - Use Date.now() for unique worker file names Shell scripts (scripts/): - Replace all python3 calls with jq for JSON parsing - Fix REPO_FLAT comparison from "True" (python) to "true" (jq) - Redirect all error messages to stderr - Add logging and cleanup on shallow clone failure Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Use globalThis instead of window in page.evaluate (es2020 portability) - Use String#replaceAll() instead of String#replace() with global regex - Batch consecutive Array#push() calls into single invocations - Flip negated condition in branch coverage merge for readability Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Aligns with all other workflows in the repo and ensures Node 24 runtime compatibility. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When E2E_COLLECT_COVERAGE=1: - Injects the Istanbul coverage reporter into the generated playwright.config.ts (appends to baseConfig.reporter) - After tests, uploads lcov to Codecov for each tested workspace via upload-coverage.sh (non-fatal on failure) - Without the env var, behavior is identical to today Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Without this, running tests twice without cleaning coverage/istanbul/ causes the reporter to merge leftover JSON from the previous run, producing incorrect coverage numbers. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When the workflow triggers on the first push to main (or after a force-push), github.event.before is the zero SHA (40 zeros). The git diff command fails silently, resulting in no workspaces being detected. Fall back to instrumenting all workspaces with e2e-tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Grepping for literal strings like "frontend-plugin" in package.json can match false positives (e.g., description fields). Parse the backstage.role JSON field properly with jq instead. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace npx --yes oras (which downloads whatever version is latest at build time) with the official setup-oras action pinned to v1.2.2. Ensures deterministic CI builds. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When running multiple workspaces, all coverage is merged into a single lcov.info. Each Codecov upload then contains coverage from all workspaces, not just the target. Add a visible warning so users know to use single -w flag for clean per-workspace coverage. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove dead coverage-fixture.ts (superseded by auto-fixture in e2e-test-utils) - Pin codecov-cli to v11.2.6 (prevents breaking changes from unpinned install) - Add --git-service github to upload command for explicit provider detection - Make PLUGIN_PKG_DIR absolute in instrument-plugin.sh (prevents fragile cd chains) - Remove unused onTestEnd and its imports from coverage-reporter.ts - Add comment documenting merged-lcov-for-all-workspaces upload behavior - Add force-push detection log in CI workflow Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Extract addCounts() helper in coverage-utils.ts (dedup s/f merge loops) - Use optional chaining + nullish coalescing for branch merge - Cache Object.values(fileCov.b) in coverage-reporter.ts - Cache webpack grep result in instrument-plugin.sh verification - Combine chained sed into single invocation (2 locations) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Delete coverage-reporter.ts and coverage-utils.ts (~210 lines) in favor of nyc merge + nyc report CLI, which is already a pipeline dependency. This fixes a CWD mismatch where the reporter (main Playwright process) and the fixture (worker processes) could resolve coverage paths from different working directories. Setting COVERAGE_OUTPUT_DIR to an absolute path before test execution ensures all workers write to the same location. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
8e54045 to
fd5b176
Compare
The fixture now writes to testInfo.project.outputDir + /coverage (node_modules/.cache/e2e-test-results/coverage) instead of using COVERAGE_OUTPUT_DIR. Update nyc merge path to match. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
subhashkhileri
left a comment
There was a problem hiding this comment.
Review: Instrumentation Pipeline
Following up on the Playwright integration review — this covers the instrumentation workflow and build script.
The post-webpack instrumentation approach (nyc instrument after export-dynamic-plugin) is the right technique for module federation. However, there are significant gaps between how this workflow builds plugins vs. how the existing production workflow does it.
subhashkhileri
left a comment
There was a problem hiding this comment.
Proposal: Reuse the existing build pipeline instead of a separate instrumentation workflow
The current PR introduces scripts/instrument-plugin.sh (205 lines) and .github/workflows/build-instrumented-plugins.yaml (271 lines) — a parallel pipeline that re-clones, re-builds, and re-packages plugins from scratch. This duplicates the production build pipeline and misses overlays, patches, and edge cases that the production pipeline already handles.
Proposed approach
Instead of rebuilding from source, instrument the already-published production image. Add an instrument job to publish-release-branch-workspace-plugins.yaml that runs after the export job:
instrument:
needs: export
if: # only for workspaces that have e2e-tests/
runs-on: ubuntu-latest
strategy:
matrix:
workspace: # detect workspaces with e2e-tests/ (similar to current detect-workspaces job)
fail-fast: false
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v6
- name: Resolve published image ref
id: meta
run: |
# Read source.json + metadata to find the frontend plugin's image name and tag
# Use the SAME naming/tagging as production: ghcr.io/{repo}/{plugin}:{tag}
# The production image was just pushed by the export job
- name: Log in to GHCR
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Node.js
uses: actions/setup-node@v6
- name: Instrument and publish coverage image
run: |
PROD_IMAGE="${{ steps.meta.outputs.image-ref }}"
COVERAGE_IMAGE="${{ steps.meta.outputs.coverage-image-ref }}"
# 1. Extract plugin bundle from production image
CONTAINER=$(podman create "$PROD_IMAGE")
mkdir -p .instrumented
podman cp "$CONTAINER:/opt/app-root/src/" .instrumented/plugin/
podman rm "$CONTAINER"
# 2. Instrument JS files with Istanbul
npx --yes nyc instrument .instrumented/plugin/ .instrumented/instrumented/ --source-map
# 3. Build new image from instrumented files
cat > .instrumented/Containerfile <<EOF
FROM scratch
COPY instrumented/ /opt/app-root/src/
EOF
podman build -t "$COVERAGE_IMAGE" -f .instrumented/Containerfile .instrumented/
# 4. Push with -coverage suffix, same tag as production
podman push "$COVERAGE_IMAGE"What this eliminates
| File | Lines | Reason |
|---|---|---|
scripts/instrument-plugin.sh |
205 | No need to re-clone, re-build, re-export from source |
.github/workflows/build-instrumented-plugins.yaml |
271 | No separate workflow needed |
What this fixes
- Overlays and patches are included — the production image already has them applied
- All plugins discovered — not just the first
frontend-pluginmatch - Same toolset — Podman throughout, matching the production pipeline (no ORAS)
- Same tag format —
bs_1.49.4__1.32.0with-coveragesuffix on the image name - Same triggers — runs whenever production images are published (push to release branches)
- No pnpm/npm detection needed — the plugin is already built
Coverage image naming
Production: ghcr.io/{repo}/{plugin}:{tag}
Coverage: ghcr.io/{repo}/{plugin}-coverage:{tag}
Same tag, -coverage suffix on the image name. This makes it trivial to swap in the coverage image during E2E runs — just append -coverage to the image name.
Note on image structure
The podman cp + podman build FROM scratch approach needs to match whatever directory layout the rhdh-cli package command produces. The exact path inside the container (e.g., /opt/app-root/src/) needs verification against the actual production image. The Containerfile above is illustrative — the actual paths should be confirmed by inspecting a published production image with podman inspect or skopeo inspect.
An alternative simpler approach: instead of rebuilding from a Containerfile, use podman commit after modifying the extracted files in-place, or use buildah to add/replace layers. The goal is to match the production image format exactly.
subhashkhileri
left a comment
There was a problem hiding this comment.
Move coverage logic out of run-e2e.sh
The ~30 lines of coverage code added to run-e2e.sh (nyc merge, nyc report, multi-workspace warning, per-workspace upload loop) should move into a self-contained script — e.g. rename scripts/upload-coverage.sh → scripts/report-coverage.sh and have it handle the full pipeline (merge → report → upload).
run-e2e.sh then reduces to:
if [[ "${E2E_COLLECT_COVERAGE:-}" == "1" ]]; then
"$SCRIPT_DIR/scripts/report-coverage.sh" "${E2E_WORKSPACES[@]}"
fiKeeps run-e2e.sh focused on test orchestration, makes the coverage script independently re-runnable (useful for debugging uploads without re-running tests), and avoids splitting the logic across two files.
Move nyc merge + report + upload logic from run-e2e.sh into a self-contained script. Keeps run-e2e.sh focused on test orchestration and makes the coverage pipeline independently re-runnable for debugging. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
| TAR_FILE=".instrumented/${WORKSPACE}.tar.gz" | ||
| tar -czf "$TAR_FILE" -C "$BUNDLE_DIR" . | ||
|
|
||
| oras push "$IMAGE_REF" \ |
There was a problem hiding this comment.
This uses completely different way of packaging plugins than what production artifact uses.
The whole process of rebuilding plugins for testing seems looks problematic to me. I'm afraid that it will create a lot of problems down the line. It will be really easy to get those two ways of building out of sync (they already are) which will result in testing something different from what we ship.
We should consider alternative. I've tried to describe it in #2383 (comment)
There was a problem hiding this comment.
This re-implements the full build pipeline that already exists in the production OCI image published by rhdh-plugin-export-utils. The only extra is nyc instrument.
What instead of reimplementing the build process and rebuilding we re-use existing artifacts.
We could pull the production image, extract it, add instrumentation and package it back to image with coverage. This way we don't rebuild the whole plugin. And minimize the difference between what is tested and what is shipped.
Something like this:
OCI_REF=ghcr.io/redhat-developer/rhdh-plugin-export-overlays/backstage-community-plugin-quay:next__1.28.1
COVERAGE_IMAGE_REF=ghcr.io/redhat-developer/rhdh-plugin-export-overlays/backstage-community-plugin-quay-coverage:next__1.28.1
# Pull production image
podman pull "$OCI_REF"
# Create a container (don't start it)
CID=$(podman create "$OCI_REF")
# Copy static/ out, instrument, copy back
podman cp "$CID:backstage-community-plugin-quay/dist/static" ./static-original/
npx nyc instrument ./static-original ./static-instrumented --source-map
podman cp ./static-instrumented/static "$CID:/backstage-community-plugin-quay/dist/"
# Commit as new image and push
podman commit "$CID" "$COVERAGE_IMAGE_REF"
podman rm "$CID"
# podman push "$COVERAGE_IMAGE_REF"



Summary
Adds Istanbul-based E2E coverage collection for dynamic plugins with a CI pipeline that automatically builds instrumented OCI images when
source.jsonchanges — and skips builds when the image already exists.Scope: frontend plugins only. Coverage collection uses
window.__coverage__which is only available for browser-executed code. Backend plugin coverage would require a different collection mechanism (global.__coverage__from the Node.js process) and is out of scope for this PR.How it works
source.jsonchanges (push to main) trigger thebuild-instrumented-pluginsworkflowe2e-tests/, the workflow checks if an instrumented OCI image already exists for the currentrepo-refinstrument-plugin.shwhich: clones upstream at the exact ref, builds the plugin (backstage-cli+janus-cli export-dynamic), then appliesnyc instrumenton the final webpack outputghcr.ioE2E_COLLECT_COVERAGE=1, the_coverageCollectorauto-fixture (in e2e-test-utils PR #95) collectswindow.__coverage__from the browser after each testreport-coverage.shmerges per-test coverage JSONs vianyc merge, generates lcov vianyc reportupload-coverage.shuploads lcov to Codecov with per-workspace flags (e2e-tech-radar,e2e-topology, etc.) for dashboard filteringKey design decisions
babel-plugin-istanbulgets stripped by webpack's module federation duringexport-dynamic-plugin. Applyingnyc instrumentafter the final webpack build preserves Istanbul__coverage__in the browser runtimeref-<sha-first-12>. If the ref hasn't changed, the build is skipped entirely — no CI overheadsource.json, so coverage appears on the correct repo in CodecovnycCLI for merge/report: Coverage merging and lcov generation usenyc merge+nyc reportinstead of custom code —nycis already a pipeline dependency and handles lcov edge cases bettertestInfo.project.outputDirfor coverage path: The auto-fixture writes coverage JSONs to<outputDir>/coverage/, avoiding CWD mismatch issues between Playwright workers and the main processKnown limitations
workspaces/*/plugins/*/overlay/and patches fromworkspaces/*/patches/*.patch. Most workspaces have no overlays/patches, so the instrumented build matches production in the majority of cases. Tracking as a follow-up.frontend-pluginin metadata. Workspaces with multiple frontend plugins only instrument the first one.Files
.github/workflows/build-instrumented-plugins.yamlscripts/instrument-plugin.shscripts/report-coverage.shscripts/upload-coverage.shRelated PRs
_coverageCollectorauto-fixture that collectswindow.__coverage__per testUsage
Test plan
source.jsonfiles correctly./scripts/instrument-plugin.sh tech-radarlocally — verify__coverage__in outputRef: RHIDP-13411