Imported from konflux-ci/coverport (
.claude/skills/coverport-integration/SKILL.md). Install upstream withnpx skills add konflux-ci/coverport --skill coverport-integration. Copyright stays with the author.
Coverport Integration Skill
This skill automates the integration of coverport into repositories for e2e test coverage collection and upload to Codecov. It supports both Tekton/Konflux pipelines and GitHub Actions workflows.
What is Coverport?
Coverport is a tool that enables e2e test coverage collection by:
- Building instrumented container images (Go with
-cover, Python with coverage wrapper, Node.js with V8 inspector, Rust with-C instrument-coverage) - Collecting coverage data from running containers during e2e tests — via HTTP endpoint or from test runner output
- Processing and uploading the coverage data to Codecov with appropriate flags
The coverport CLI is available as a container image that includes all dependencies (oras, cosign, git, language-specific tools):
quay.io/konflux-ci/konflux-devprod/coverport-cli:<tag>
IMPORTANT — Pin the image tag: Never use
:latestor an untagged image in generated CI workflows. Always resolve the current latest tag at onboarding time (see Step 0) and pin to it. This prevents breaking the onboarded application's CI if coverport introduces breaking changes later.
When to Use This Skill
Use this skill when the user:
- Asks to integrate coverport into their repository
- Wants to add e2e test coverage tracking
- Needs to set up coverage instrumentation for Go, Python, Node.js, or Rust projects
- Mentions integrating coverage collection for Tekton/Konflux pipelines
- Wants to collect e2e coverage in GitHub Actions using the coverport CLI container
- Mentions LLVM coverage, profraw, or
instrument-coveragefor Rust
Prerequisites
Before using this skill, verify the repository has:
- Codebase with a Dockerfile (Go, Python, Node.js, or Rust)
- One of the following CI/CD setups:
- Tekton pipelines (typically in
.tekton/directory) with an E2E test pipeline (typically inintegration-tests/pipelines/) - GitHub Actions workflows (in
.github/workflows/)
- Tekton pipelines (typically in
- Codecov account (see
codecov-config/CONFIG.mdfor instance routing)
Instructions
Step 0: Pre-Integration Repository Scan
Before starting, run these checks to understand the repository structure:
-
Resolve the current coverport-cli image tag:
# Fetch the latest git commit SHA tag from Quay (no auth needed for public repos) COVERPORT_TAG=$(curl -s "https://quay.io/api/v1/repository/konflux-ci/konflux-devprod%2Fcoverport-cli/tag/?onlyActiveTags=true&limit=50" \ | jq -r ' (.tags[] | select(.name == "latest") | .manifest_digest) as $digest | .tags[] | select(.manifest_digest == $digest and (.name | test("^[0-9a-f]{40}$"))) | .name ' | head -1) echo "Pinning to: quay.io/konflux-ci/konflux-devprod/coverport-cli:${COVERPORT_TAG}"Use this tag in all generated CI YAML instead of
:latestor untagged references. If the API is unreachable orjqis not available, fall back to using thelatesttag with a comment noting it should be pinned. -
Detect project language and entry point:
# Go projects find . -name "main.go" -not -path "*/vendor/*" -not -path "*/test/*" # Python projects find . -name "requirements.txt" -o -name "setup.py" -o -name "pyproject.toml" | head -5 ls *.py Dockerfile Containerfile 2>/dev/null # Rust projects find . -name "Cargo.toml" -not -path "*/target/*" | head -10 grep -r "\[\[bin\]\]" Cargo.toml */Cargo.toml 2>/dev/null ls src/main.rs 2>/dev/null -
Check current Dockerfile build command:
# Go projects grep -A5 "go build" Dockerfile Containerfile 2>/dev/null # Python projects grep -A5 "pip install\|ENTRYPOINT\|CMD" Dockerfile Containerfile 2>/dev/null # Rust projects grep -A5 "cargo build" Dockerfile Containerfile 2>/dev/null -
List Tekton pipelines:
ls .tekton/*.yaml ls integration-tests/pipelines/*.yaml 2>/dev/null || echo "No integration-tests/pipelines found" -
Check for existing coverage setup:
grep -r "ENABLE_COVERAGE\|instrumented\|coverport\|instrument-coverage\|coverage-server" . --exclude-dir=vendor --exclude-dir=target --exclude-dir=.git -
Check if e2e test suite rebuilds the container image:
grep -r "docker-build\|docker build\|podman build\|make.*build.*IMG" test/ --include="*.go" 2>/dev/nullThis is common in kubebuilder/operator-sdk projects where the Ginkgo
BeforeSuiterebuilds and loads the image into Kind. If found, the test code must passENABLE_COVERAGEthrough to the build command, otherwise it will overwrite the instrumented image with a production one. -
Determine where e2e tests run:
# Check for Tekton integration test pipelines ls integration-tests/pipelines/*.yaml 2>/dev/null || echo "No Tekton e2e pipelines" # Check for GitHub Actions e2e workflows grep -rl "e2e\|integration" .github/workflows/ --include="*.yml" --include="*.yaml" 2>/dev/nullThis is critical for deciding which pipeline changes are needed (see Decision Point below).
-
Determine Python e2e test style (Python projects only):
# Does the pipeline run pytest against cloned source? grep -A5 "pytest\|python.*run_tests" integration-tests/pipelines/*.yaml 2>/dev/null # Does the pipeline deploy or test a container image? grep -rl "kind\|kubectl\|helm\|deploy\|instrumented-container" integration-tests/pipelines/*.yaml .github/workflows/ 2>/dev/nullTwo distinct Python paths — choose based on what the e2e tests actually exercise:
- Pattern D (pytest-cov): Pipeline clones the repo, installs dependencies, and runs
pytestdirectly against source code. The container image is NOT deployed or tested. No container instrumentation needed. - Container instrumentation (Steps 3-5 Python): E2e tests deploy and exercise the
container image (K8s/Kind,
podman run, etc.). Vendor the files frominstrumentation/python/and collect via Pattern A or B on port 53700. Do NOT route Python container deployments to Pattern D.
- Pattern D (pytest-cov): Pipeline clones the repo, installs dependencies, and runs
This helps identify potential conflicts or existing coverage infrastructure before making changes.
Step 1: Analyze the Repository
Analyze the repository structure to understand what needs to be modified:
- Find the Dockerfile - Look for the main Dockerfile
- Identify binaries being built - Check what Go binaries are compiled in the Dockerfile and note if main.go is in root or subdirectory
- Find Tekton push pipeline - Look in
.tekton/for*-push.yaml - Find E2E test pipeline - Look in
integration-tests/pipelines/for*e2e*.yaml - Find Tekton PR pipeline - Look in
.tekton/for*-pull-request.yaml - Find GitHub Actions - Look in
.github/workflows/forpr.yaml,pr.yml,codecov.yaml, orcodecov.yml - Check for existing coverage integration - Search for
ENABLE_COVERAGE,instrumented,coverport - Determine where e2e tests run - This determines which pipeline changes are needed:
- Tekton integration pipelines (
integration-tests/pipelines/): Needs instrumented image in Tekton push pipeline + coverage collection task in e2e pipeline - GitHub Actions only (e.g., Kind cluster in
.github/workflows/): Only needs Dockerfile + GitHub Actions changes, no Tekton pipeline changes - Both: Apply both sets of changes
- Tekton integration pipelines (
Step 2: Ask Clarifying Questions
Before making changes, ask the user:
- Which binaries to instrument? (Go/Rust) - If the Dockerfile builds multiple binaries, ask which ones run during e2e tests
- Python: container vs source tests? (Python) - Do e2e tests deploy the container image, or run pytest directly against source? Container deployment → Steps 3-5 (Python); pytest against source → Pattern D
- Python: WSGI entry point (Python container path) - Confirm the Gunicorn module path (e.g.
app:app) and containerWORKDIR(typically/app, must match.coveragercsource) - Tenant namespace - Confirm the namespace where their build and integration pipelines run (check
.tekton/*-push.yamlfor thenamespacefield) - Secret name - Confirm they want to use
coverport-secretsor specify a different name - OCI storage - Confirm where coverage data should be stored (the quay.io repository for test artifacts)
Decision Point: Tekton vs GitHub Actions
After Steps 0-2, determine which changes to apply based on where e2e tests run:
| E2E tests run in... | Test style | Apply these steps |
|---|---|---|
Tekton integration pipelines only (integration-tests/pipelines/) |
Go/Rust: tests deploy/use the container image | Steps 3-6, E2E pipeline update, Step 8 (Tekton PR) |
Tekton integration pipelines only (integration-tests/pipelines/) |
Python: tests run pytest directly against source | Pattern D: pytest-cov — no container instrumentation needed |
Tekton integration pipelines only (integration-tests/pipelines/) |
Python: app deployed as container (Flask/Django/FastAPI + Gunicorn) | Steps 3-5 (Python), Step 6 (Python), E2E pipeline update, Step 8 (Python PR) — not Pattern D |
GitHub Actions only (no integration-tests/pipelines/) |
Go/Rust: tests deploy/use the container image | Steps 3-5.5, Step 7 (GitHub Actions) — skip Steps 6 and 8 |
| GitHub Actions only | Python: app deployed as container (Kind, podman run, etc.) |
Steps 3-5 (Python), Step 7 Pattern A (Kind) preferred; Pattern B only for local podman run |
| Both Tekton and GitHub Actions | Tests deploy/use the container image | All applicable steps (language-specific: Go/Rust use Step 6 ENABLE_COVERAGE; Python uses Step 6 TARGET_STAGE) |
Key rule: Do NOT modify Tekton push/PR pipelines (Steps 6, 8) if the repository does not have a Tekton e2e integration pipeline. Building an instrumented image in Tekton is pointless if nothing in Tekton consumes it. The instrumented build happens locally (e.g., via make with ENABLE_COVERAGE=true) in the GitHub Actions workflow instead.
Key rule: Pattern D is only for pytest against cloned source. If Python e2e tests deploy or hit a container image (even when written in pytest), use container instrumentation (Steps 3-5 Python) + Pattern A/B — not Pattern D.
Step 3: Add Coverage Dependency
Go
Add coverport to your Go module dependencies:
go get github.com/konflux-ci/coverport/instrumentation/go
go mod tidy
This will:
- Add the coverport package to
go.modas a dependency - Update
go.sumwith the dependency checksums
Important: Always run go mod tidy after go get. The go get command adds the dependency as // indirect, but since coverage_init.go imports it directly (even behind a build tag), go mod tidy correctly reclassifies it as a direct dependency. Many CI systems verify that go mod tidy produces no diff.
Rust
Add the coverage-server crate as an optional dependency behind a Cargo feature flag:
[features]
coverage = ["dep:coverage-server"]
[dependencies]
coverage-server = { git = "https://github.com/konflux-ci/coverport.git", subdirectory = "instrumentation/rust", optional = true }
Important:
- The
optional = trueensures coverage-server is only compiled when--features coverageis passed - No additional dependencies (like tokio) are needed — the coverage server brings its own runtime
- The crate is pulled from the coverport monorepo using Cargo's
subdirectoryparameter
Python
Copy the four instrumentation files from instrumentation/python/ in the coverport monorepo
into your application repository. See instrumentation/python/README.md
for file descriptions, Dockerfile example, and local validation steps.
instrumentation/python/coverage_server.py → server/coverage_server.py
instrumentation/python/sitecustomize.py → server/sitecustomize.py
instrumentation/python/.coveragerc → server/.coveragerc
instrumentation/python/gunicorn_coverage.py → server/gunicorn_coverage.py
Adjust the destination directory (server/ above) to match your project layout. The
Dockerfile in Step 5 must copy from these paths.
Important:
- No
pip install coverportor Go-module equivalent — vendor these files directly - Ensure
coverageandgunicornare installed in the instrumented image (add torequirements.txtor install in the Dockerfile test stage) - Update
source = /appin.coveragercif your containerWORKDIRdiffers from/app - See instrumentation/python/README.md for Dockerfile and validation details
Step 4: Add Coverage Initialization Code
Go
Create a new file coverage_init.go in the root of your Go module (same directory as main.go or where the package main is):
//go:build coverage
package main
// This file is only included when building with -tags=coverage.
// It starts a coverage HTTP server that allows collecting coverage data
// from the running binary during E2E tests.
import _ "github.com/konflux-ci/coverport/instrumentation/go" // starts coverage server via init()
Important:
- The
//go:build coveragetag ensures this file is only included when building with-tags=coverage - The blank import triggers the coverage server's init() function
- This file should be at the root of your Go module (where
main.gois, or where the main package is) - Always run
go mod tidyafter this step. The coverport instrumentation dependency was previously fetched as indirect, and the new blank import makes it a direct dependency.
Rust
Add two lines to the application's main() function:
fn main() {
#[cfg(feature = "coverage")]
let _coverage = coverage_server::start_coverage_server_standalone(53700);
// ... rest of the application, completely unchanged ...
}
Important:
- The
#[cfg(feature = "coverage")]attribute ensures this code is only compiled when the feature is enabled start_coverage_server_standalone(53700)spawns the server on its own background thread with its own tokio runtime — no interference with the application's runtime- The port (53700) can be overridden at runtime via the
COVERAGE_PORTenvironment variable - Works with any Rust application — any async runtime (tokio, async-std, actix-rt) or synchronous apps
Python
No application code changes are required.
Unlike Go (coverage_init.go) or Rust (lines in main()), Python container instrumentation
is entirely file-based: sitecustomize.py auto-starts coverage in every process, and
gunicorn_coverage.py saves worker data on exit. Skip this step for Python.
Step 5: Modify the Dockerfile
Add build argument (near the top after FROM):
ARG ENABLE_COVERAGE=false
Go
Modify the build command to conditionally build with coverage tags:
RUN if [ "$ENABLE_COVERAGE" = "true" ]; then \
echo "Building with coverage instrumentation..."; \
CGO_ENABLED=0 go build -cover -covermode=atomic -tags=coverage -o <binary-name> .; \
else \
echo "Building production binary..."; \
CGO_ENABLED=0 go build -a -o <binary-name> .; \
fi
Important:
- Replace
<binary-name>with the actual binary name - The
-tags=coverageflag includes thecoverage_init.gofile - Build the package (
.) rather than individual files - Only instrument binaries that run during e2e tests
- Keep other binaries without instrumentation
- No need to download external files - coverport is now a Go module dependency
Rust
Modify the build command to conditionally build with coverage:
ARG ENABLE_COVERAGE
RUN if [ "$ENABLE_COVERAGE" = "true" ]; then \
echo "Building with coverage instrumentation"; \
rustup component add llvm-tools-preview; \
RUSTFLAGS="-C instrument-coverage" LLVM_PROFILE_FILE=/dev/null \
cargo build --release --features coverage; \
else \
echo "Building without coverage (production)"; \
cargo build --release; \
fi
Important — Rust-specific details:
RUSTFLAGS="-C instrument-coverage"tells the compiler to insert LLVM profiling counters--features coverageactivates thecoverage-serveroptional dependencyLLVM_PROFILE_FILE=/dev/nullsuppresses stray.profrawfiles generated during the build- Without
-C instrument-coverage, the LLVM FFI symbols won't be present and the build will fail with linker errors if--features coverageis set - Without
--features coverage, the coverage-server dependency is not included at all — clean production binary
Python
Use a multi-stage Dockerfile to keep production images unchanged. The instrumented
test stage installs sitecustomize.py into site-packages, sets coverage environment
variables, and wraps the app CMD with coverage_server.py.
- Local / GitHub Actions:
podman build --target test - Tekton (buildah-oci-ta): set
TARGET_STAGE=teston the instrumented image build task (Step 6 Python) — do not useENABLE_COVERAGE=truealone; Python has no conditional compile step for that arg
# ... normal build stages (install app deps, COPY application code) ...
FROM base AS production
CMD ["gunicorn", "-b", "0.0.0.0:8080", "-w", "2", "app:app"]
FROM base AS test
RUN pip install --no-cache-dir gunicorn coverage
COPY server/sitecustomize.py /tmp/sitecustomize.py
RUN SITE_PACKAGES=$(python -c "import site; print(site.getsitepackages()[0])") && \
cp /tmp/sitecustomize.py "$SITE_PACKAGES/sitecustomize.py"
COPY server/.coveragerc /app/.coveragerc
COPY server/gunicorn_coverage.py /opt/gunicorn_coverage.py
COPY server/coverage_server.py /opt/coverage_server.py
ENV COVERAGE_PROCESS_START=/app/.coveragerc
ENV COVERAGE_DATA_DIR=/dev/shm
ENV TMPDIR=/dev/shm
EXPOSE 8080 53700
CMD ["python", "/opt/coverage_server.py", "-m", "gunicorn", \
"-c", "/opt/gunicorn_coverage.py", "-b", "0.0.0.0:8080", "-w", "1", "app:app"]
Build the instrumented image:
podman build --target test -t myapp:instrumented .
Important — Python-specific details:
- Replace
app:appwith your WSGI entry point sitecustomize.pymust be installed into site-packages so every Gunicorn worker loads itCOVERAGE_DATA_DIR=/dev/shmandTMPDIR=/dev/shmare required forreadOnlyRootFilesystempodscoverage_server.pyexposes the HTTP coverage endpoint on port 53700 by default (COVERAGE_PORTenv var overrides)-w 1is recommended for initial setup; increase workers once coverage collection is verified- See instrumentation/python/README.md for local podman validation and
coverport collectexamples
Step 5.5: Validate Dockerfile Changes Locally
IMPORTANT: Before proceeding to pipeline changes, validate the Dockerfile modifications work correctly using podman or docker:
# Go/Rust: instrumented build via ENABLE_COVERAGE build arg
podman build --build-arg ENABLE_COVERAGE=true -t test-instrumented -f Dockerfile .
# Python: instrumented build via test stage target
podman build --target test -t test-instrumented -f Dockerfile .
# Production image (without coverage)
podman build -t test-production -f Dockerfile .
# Verify both images built successfully
podman images | grep test-
Expected output in instrumented build:
- Go/Rust: "Building with coverage instrumentation..."
- Python: image builds successfully; container logs show
[coverage-wrapper] HTTP server listening on port 53700
Expected output in production build:
- Go: "Building production binary..."
- Python: production stage CMD runs plain Gunicorn without coverage wrapper
Python validation (after instrumented build):
# Default port is 53700; use the same value as COVERAGE_PORT if your image overrides it
COVERAGE_PORT=53700
podman run --rm -d --name py-cov-test -p 8080:8080 -p ${COVERAGE_PORT}:${COVERAGE_PORT} test-instrumented
curl -s "http://localhost:${COVERAGE_PORT}/health" # expect coverage_enabled: true
curl -s http://localhost:8080/ # hit app endpoints to generate coverage
curl -s "http://localhost:${COVERAGE_PORT}/coverage/save"
curl -s "http://localhost:${COVERAGE_PORT}/coverage" # expect non-empty coverage_data
podman stop py-cov-test
If builds fail:
- Stop and fix the Dockerfile before proceeding
- See Troubleshooting section for common issues
- Go: ensure
coverage_init.goexists in the correct location - Go: verify Go module dependencies were downloaded (check
go.modandgo.sum) - Go: check that the build tags syntax is correct in
coverage_init.go - Python: verify all four instrumentation files were copied and
sitecustomize.pyis in site-packages - Python: confirm
gunicornandcoveragepackages are installed in the instrumented image
Why this validation matters:
- Catches Dockerfile syntax errors immediately
- Verifies coverport Go module integration works
- Confirms both production and instrumented builds succeed
- Prevents wasting CI/CD pipeline time on broken builds
- Validates the conditional build logic works correctly
Step 6: Update Tekton Push Pipeline
Skip this step if e2e tests run only in GitHub Actions. Building an instrumented image in the Tekton push pipeline is only useful when a Tekton integration test pipeline (in
integration-tests/pipelines/) consumes it. If e2e tests run exclusively in GitHub Actions (e.g., via Kind cluster), the instrumented build happens locally in the workflow instead.
Add a task to build an instrumented image in the push pipeline (e.g., .tekton/*-push.yaml):
Find the location after prefetch-dependencies task and add:
- name: build-instrumented-image
params:
- name: IMAGE
value: $(params.output-image).instrumented
- name: DOCKERFILE
value: $(params.dockerfile)
- name: CONTEXT
value: $(params.path-context)
- name: HERMETIC
value: $(params.hermetic)
- name: PREFETCH_INPUT
value: $(params.prefetch-input)
- name: IMAGE_EXPIRES_AFTER
value: $(params.image-expires-after)
- name: COMMIT_SHA
value: $(tasks.clone-repository.results.commit)
- name: BUILD_ARGS
value:
- $(params.build-args[*])
- ENABLE_COVERAGE=true
- name: BUILD_ARGS_FILE
value: $(params.build-args-file)
- name: SOURCE_ARTIFACT
value: $(tasks.prefetch-dependencies.results.SOURCE_ARTIFACT)
- name: CACHI2_ARTIFACT
value: $(tasks.prefetch-dependencies.results.CACHI2_ARTIFACT)
runAfter:
- prefetch-dependencies
taskRef:
params:
- name: name
value: buildah-oci-ta
- name: bundle
value: quay.io/konflux-ci/tekton-catalog/task-buildah-oci-ta:0.7@sha256:b54509f5f695c0c89de4587a403099a26da5cdc3707037edd4b7cf4342b63edd
- name: kind
value: task
resolver: bundles
when:
- input: $(tasks.init.results.build)
operator: in
values:
- "true"
IMPORTANT - Key points:
- Use
buildah-oci-ta(NOTbuildah-remote-oci-ta) - this is a regular local build for amd64 testing clusters - This should be a single task, NOT a matrix build (no PLATFORM parameter, no IMAGE_APPEND_PLATFORM)
- Image tagged with
.instrumentedsuffix HERMETIC: $(params.hermetic)- uses the same hermetic setting as the main build (now supports hermetic builds!)PREFETCH_INPUT: $(params.prefetch-input)- uses the same prefetch settings as the main buildBUILD_ARGSincludesENABLE_COVERAGE=true- Do NOT add a
build-instrumented-image-indextask - the instrumented image is single-platform only
Python (Tekton push pipeline)
For Python container apps, the instrumented image is the test Dockerfile stage — not a
ENABLE_COVERAGE conditional build. Add the same build-instrumented-image task as above,
but use TARGET_STAGE instead of ENABLE_COVERAGE=true:
- name: build-instrumented-image
params:
- name: IMAGE
value: $(params.output-image).instrumented
- name: DOCKERFILE
value: $(params.dockerfile)
- name: CONTEXT
value: $(params.path-context)
- name: TARGET_STAGE
value: test
- name: HERMETIC
value: $(params.hermetic)
- name: PREFETCH_INPUT
value: $(params.prefetch-input)
- name: IMAGE_EXPIRES_AFTER
value: $(params.image-expires-after)
- name: COMMIT_SHA
value: $(tasks.clone-repository.results.commit)
- name: BUILD_ARGS
value:
- $(params.build-args[*])
- name: BUILD_ARGS_FILE
value: $(params.build-args-file)
- name: SOURCE_ARTIFACT
value: $(tasks.prefetch-dependencies.results.SOURCE_ARTIFACT)
- name: CACHI2_ARTIFACT
value: $(tasks.prefetch-dependencies.results.CACHI2_ARTIFACT)
runAfter:
- prefetch-dependencies
taskRef:
params:
- name: name
value: buildah-oci-ta
- name: bundle
value: quay.io/konflux-ci/tekton-catalog/task-buildah-oci-ta:0.7@sha256:b54509f5f695c0c89de4587a403099a26da5cdc3707037edd4b7cf4342b63edd
- name: kind
value: task
resolver: bundles
when:
- input: $(tasks.init.results.build)
operator: in
values:
- "true"
Python Tekton notes:
TARGET_STAGE=testmaps topodman build --target test(buildah-oci-ta parameter)- The
teststage name must match the Dockerfile stage defined in Step 5 (Python) - Do not add
ENABLE_COVERAGE=truetoBUILD_ARGSfor Python — it has no effect unless you add custom Dockerfile logic to interpret it
Step 5: Update E2E Test Pipeline
Skip this step if e2e tests run only in GitHub Actions. This step applies only when there is a Tekton integration test pipeline in
integration-tests/pipelines/.
Make three changes to the e2e test pipeline:
A. Update test-metadata task from v0.3 to v0.4:
- name: test-metadata
taskRef:
resolver: git
params:
- name: url
value: https://github.com/konflux-ci/tekton-integration-catalog.git
- name: revision
value: main
- name: pathInRepo
value: tasks/test-metadata/0.4/test-metadata.yaml
B. Update image references (if applicable):
NOTE: Only modify this if your e2e tests actually run the containerized application.
- If your tests build the manager from source (e.g., using
make buildorgo run main.go), you may need to modify the build/run commands to use coverage flags instead, or deploy the instrumented container image - If your tests deploy and run containers, proceed with updating image references
For tests that deploy/run container images, find parameters that reference images and change:
container-repo→instrumented-container-repocontainer-tag→instrumented-container-tagcontainer-image→instrumented-container-image
Example scenarios:
- Scenario 1 (uses container): Tests deploy the app to a cluster using the container image → Update image references
- Scenario 2 (builds from source): Tests run
make build && ./managerinside the pipeline → May not need image reference changes, but need to ensure the running process is instrumented - Scenario 3 (hybrid): Tests build from source but coverage collection expects instrumented container → Coordinate with user on approach
C. Add coverage collection task after e2e tests:
- name: collect-and-upload-coverage
runAfter:
- <e2e-test-task-name> # Replace with actual task name
params:
- name: instrumented-images
value: "$(tasks.test-metadata.results.instrumented-container-repo):$(tasks.test-metadata.results.instrumented-container-tag)"
- name: cluster-access-secret-name
value: kfg-$(context.pipelineRun.name) # Adjust if different
- name: test-name
value: e2e-tests
- name: oci-container
value: "$(params.oci-container-repo):$(context.pipelineRun.name)"
- name: codecov-flags
value: e2e-tests
- name: credentials-secret-name
value: "coverport-secrets" # Or user-specified name
# For Rust projects, add:
# - name: coverage-format
# value: rust
taskRef:
resolver: git
params:
- name: url
value: https://github.com/konflux-ci/tekton-integration-catalog.git
- name: revision
value: main
- name: pathInRepo
value: tasks/coverport-coverage/0.1/coverport-coverage.yaml
Rust-specific Tekton note: For Rust projects, add the coverage-format: rust parameter to the coverage collection task. This tells coverport to process the collected profraw data using llvm-profdata and llvm-cov instead of the default Go profile format. The Tekton pipeline YAML is otherwise identical to Go — only this one parameter differs.
Step 8: Update Tekton PR Pipeline (Pull Request Pipeline)
Skip this step if e2e tests run only in GitHub Actions. Adding
ENABLE_COVERAGE=trueto the Tekton PR build is only useful when the built image is consumed by a Tekton integration test pipeline.
Update the PR pipeline (e.g., .tekton/*-pull-request.yaml) to build with coverage instrumentation:
A. Enable hermetic build and prefetch (if not already enabled):
Add or ensure these parameters exist in the spec.params section:
- name: hermetic
value: "true"
- name: prefetch-input
value: '{"type": "gomod", "path": "."}'
B. Add ENABLE_COVERAGE=true to BUILD_ARGS:
Find the build-images task (or equivalent) and add ENABLE_COVERAGE=true to its BUILD_ARGS:
- name: build-images
# ... other params ...
params:
# ... other params ...
- name: BUILD_ARGS
value:
- $(params.build-args[*])
- ENABLE_COVERAGE=true # Add this line
# ... rest of the task ...
Key points (Go/Rust):
- With the Go module approach, hermetic builds are now supported!
- Enable
hermetic: "true"andprefetch-inputfor secure, reproducible builds - Add
ENABLE_COVERAGE=trueto the regular build task in PR pipeline - This enables coverage collection for PR builds which can be used for PR-level testing
- No need to create a separate instrumented image task in PR pipeline - just modify the existing build task
C. Python PR builds (Step 8 Python):
For Python container apps, set TARGET_STAGE=test on the PR build-images task instead of
ENABLE_COVERAGE=true:
- name: build-images
params:
# ... other params ...
- name: TARGET_STAGE
value: test
If the PR pipeline must still produce a production image for merging, keep the default
production build as-is and add a separate instrumented build task (same as Step 6 Python)
that tags with .instrumented.
Step 7: Update GitHub Actions
7a: Unit Test Coverage Flags
Add codecov flags to distinguish unit tests from e2e tests.
In .github/workflows/pr.yaml (or similar), update the codecov upload step:
For public repos using app.codecov.io, use OIDC (no token needed):
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v6
with:
use_oidc: true
flags: unit-tests
The job must have permissions: id-token: write for OIDC to work.
For private repos using a self-hosted Codecov instance, use token auth:
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v6
with:
url: <CODECOV_INSTANCE_URL>
token: ${{ secrets.CODECOV_TOKEN }}
flags: unit-tests
See codecov-config/CONFIG.md for the correct Codecov instance URL
based on repository location.
7b: E2E Coverage Collection in GitHub Actions
If the repository runs e2e tests in GitHub Actions (not just Tekton), use the coverport CLI container via podman to collect and upload e2e coverage. The container image includes all dependencies (oras, cosign, git, Go, etc.), so nothing needs to be installed separately.
Container image (use the tag resolved in Step 0):
quay.io/konflux-ci/konflux-devprod/coverport-cli:${COVERPORT_TAG}
There are three patterns depending on where the instrumented app runs and how coverage is collected:
Pattern A: App Running in Kubernetes (HTTP-based collection)
Use when your GitHub Actions workflow deploys the instrumented app to a Kubernetes cluster (e.g., Kind) and runs e2e tests against it. You need kubeconfig access to the cluster.
Important: Rootless podman volume mount permissions on GitHub Actions (Linux):
- Kubeconfig files typically have
600permissions. Rootless podman maps container UIDs differently, so the container user cannot read files with600permissions. Copy the kubeconfig to a temp file with644. - Output directories must be world-writable (
chmod 777) so the container can create subdirectories and write coverage files.
- name: Collect e2e coverage
if: always()
run: |
mkdir -p coverage-output && chmod 777 coverage-output
cp $HOME/.kube/config /tmp/kubeconfig && chmod 644 /tmp/kubeconfig
podman run --rm \
--network host \
-v /tmp/kubeconfig:/kubeconfig:ro \
-v $PWD/coverage-output:/workspace/coverage-output \
-e KUBECONFIG=/kubeconfig \
quay.io/konflux-ci/konflux-devprod/coverport-cli:${COVERPORT_TAG} \
collect \
--namespace=${{ env.TEST_NAMESPACE }} \
--label-selector=${{ env.LABEL_SELECTOR }} \
--test-name=e2e-tests \
--output=/workspace/coverage-output || true
- name: Upload e2e coverage to Codecov
if: always()
uses: codecov/codecov-action@v6
with:
use_oidc: true # or token for private repos
flags: e2e-tests
files: coverage-output/<component>/<test-name>/coverage.out
fail_ci_if_error: false
Alternative: Using coverport process + push for OCI storage:
- name: Collect and push e2e coverage
if: always()
run: |
mkdir -p coverage-output && chmod 777 coverage-output
cp $HOME/.kube/config /tmp/kubeconfig && chmod 644 /tmp/kubeconfig
podman run --rm \
--network host \
-v /tmp/kubeconfig:/kubeconfig:ro \
-v $PWD/coverage-output:/workspace/coverage-output \
-e KUBECONFIG=/kubeconfig \
quay.io/konflux-ci/konflux-devprod/coverport-cli:${COVERPORT_TAG} \
collect \
--images=${{ env.INSTRUMENTED_IMAGE }} \
--namespace=${{ env.TEST_NAMESPACE }} \
--test-name="e2e-tests" \
--output=/workspace/coverage-output \
--push \
--repository=${{ env.OCI_COVERAGE_REPO }}
# Process and upload to Codecov
podman run --rm \
-e CODECOV_TOKEN=${{ secrets.CODECOV_TOKEN }} \
quay.io/konflux-ci/konflux-devprod/coverport-cli:${COVERPORT_TAG} \
process \
--artifact-ref=${{ env.COVERAGE_ARTIFACT_REF }} \
--image=${{ env.INSTRUMENTED_IMAGE }} \
--codecov-flags=e2e-tests
Key points for Kubernetes collection:
--network hostis required so coverport can reach the Kind/k8s API- The coverport CLI port-forwards to the pod's coverage HTTP endpoint (default 53700 for
Go, Python, and Rust instrumentation; CLI also tries 9095 as fallback when
--portis omitted) - Go:
collectgenerates acoverage.outtext profile from binary coverage data - Python (K8s path only):
collectchecks/health, triggers/coverage/save, fetches/coverage, then execs into the pod to runcoverage xml→coverage.xmlin the output directory — no separateprocessstep needed - Upload Go:
coverage-output/.../coverage.outvia codecov-action orprocess - Upload Python:
coverage-output/<test-name>/coverage.xmlvia codecov-action - You can also use coverport's
processcommand for OCI-based workflows (Go path)
Pattern A (Python): Upload after K8s collect
When collecting Python coverage from Kind/Kubernetes (not --url), upload the XML file
generated during collect:
- name: Upload e2e coverage to Codecov
if: always()
uses: codecov/codecov-action@v6
with:
use_oidc: true
flags: e2e-tests
files: coverage-output/e2e-tests/coverage.xml
fail_ci_if_error: false
Pattern B: App Running Locally via Podman/Docker (HTTP-based collection)
Use when your GitHub Actions workflow starts the instrumented app
locally (e.g., via podman run or docker compose) and runs e2e tests
against it in the same job. The app exposes coverage via HTTP. Use coverport's
--url flag instead of Kubernetes discovery.
Coverage port: Instrumentation servers use 53700 by default (Go, Python, Rust).
Map -p 53700:53700 when running locally (or match COVERAGE_PORT if your image sets it).
The CLI without --port tries 53700 then 9095 as fallback for legacy setups.
--url base path: Pass the full /coverage endpoint URL (e.g. http://localhost:53700/coverage).
The CLI appends ?name=<test-name> — it does not add /coverage for you. A bare
http://localhost:53700 returns 404 on Python servers.
Pattern B (Go): Local HTTP collection
- name: Start instrumented application
run: |
podman run -d --name app-under-test \
-p 8080:8080 -p 53700:53700 \
${{ env.INSTRUMENTED_IMAGE }}
- name: Run e2e tests
run: |
# Run your e2e test suite against http://localhost:8080
<your-e2e-test-command>
- name: Collect and upload e2e coverage
if: always()
run: |
mkdir -p coverage-output && chmod 777 coverage-output
# Step 1: Collect coverage from the local HTTP endpoint
podman run --rm \
--network host \
-v $PWD/coverage-output:/workspace/coverage-output \
quay.io/konflux-ci/konflux-devprod/coverport-cli:${COVERPORT_TAG} \
collect \
--url http://localhost:53700/coverage \
--test-name="e2e-tests" \
--output=/workspace/coverage-output
# Step 2: Process and upload to Codecov
podman run --rm \
-v $PWD/coverage-output:/workspace/coverage-output:ro \
-e CODECOV_TOKEN=${{ secrets.CODECOV_TOKEN }} \
quay.io/konflux-ci/konflux-devprod/coverport-cli:${COVERPORT_TAG} \
process \
--coverage-dir=/workspace/coverage-output \
--repo-url=${{ github.server_url }}/${{ github.repository }} \
--commit-sha=${{ github.sha }} \
--codecov-flags=e2e-tests
- name: Stop application
if: always()
run: podman stop app-under-test || true
Key points for Go --url collection:
--network hostis required so coverport can reach localhost:53700--urlmust include/coverage(e.g.http://localhost:53700/coverage)- Coverport detects format from the
/coverageresponse body (not/health) - When using
--url(no container image), you must pass--repo-urland--commit-shato theprocesscommand explicitly - Legacy Go images may listen on 9095 — use
--url http://localhost:9095/coverageand map that port - The coverport CLI uses repo URL/commit to clone the repo and remap coverage paths from container paths to source paths
Pattern B (Python): Local HTTP collection
Prefer Pattern A (Kind/K8s) for Python container apps in GitHub Actions. K8s
collectchecks/health, triggers/coverage/savewhen needed, fetches/coverage, and generatescoverage.xmlinside the pod automatically. Pattern B (--url) only saves serializedCoverageData.dumps()bytes as.coverage— the coverport-cli image has no Python, so XML must be generated on the GHA runner after collect using the conversion script below.
Use after completing Steps 3-5 (Python). Only when the app runs via podman run in the same
job (not deployed to Kind).
- name: Start instrumented application
run: |
podman run -d --name app-under-test \
-p 8080:8080 -p 53700:53700 \
${{ env.INSTRUMENTED_IMAGE }}
- name: Run e2e tests
run: |
<your-e2e-test-command>
- name: Collect e2e coverage
if: always()
run: |
mkdir -p coverage-output && chmod 777 coverage-output
COVERAGE_PORT=53700
# --url collect does NOT call /coverage/save (unlike K8s collect)
curl -sf "http://localhost:${COVERAGE_PORT}/coverage/save" || true
podman run --rm \
--network host \
-v $PWD/coverage-output:/workspace/coverage-output \
quay.io/konflux-ci/konflux-devprod/coverport-cli:${COVERPORT_TAG} \
collect \
--url "http://localhost:${COVERAGE_PORT}/coverage" \
--test-name="e2e-tests" \
--output=/workspace/coverage-output
- name: Generate Cobertura XML
if: always()
run: |
pip install coverage
python3 <<'PY'
import os
import coverage
repo = os.path.abspath(".")
# Must match container WORKDIR and .coveragerc `source` (default /app/)
container_prefix = "/app/"
raw_path = "coverage-output/e2e-tests/.coverage"
xml_path = "coverage-output/e2e-tests/coverage.xml"
sqlite_path = "coverage-output/e2e-tests/.coverage.local"
raw = open(raw_path, "rb").read()
data = coverage.CoverageData(no_disk=True)
data.loads(raw)
remapped = coverage.CoverageData(no_disk=True)
for fn in data.measured_files():
local_fn = fn.replace(container_prefix, repo + "/")
lines = data.lines(fn)
if lines:
remapped.add_lines({local_fn: lines})
arcs = data.arcs(fn)
if arcs:
remapped.add_arcs({local_fn: arcs})
db = coverage.CoverageData(basename=sqlite_path)
db.update(remapped)
db.write()
cov = coverage.Coverage(data_file=sqlite_path)
cov.load()
cov.xml_report(outfile=xml_path)
print(f"Wrote {xml_path}")
PY
- name: Upload e2e coverage to Codecov
if: always()
uses: codecov/codecov-action@v6
with:
use_oidc: true
flags: e2e-tests
files: coverage-output/e2e-tests/coverage.xml
fail_ci_if_error: false
- name: Stop application
if: always()
run: podman stop app-under-test || true
Key points for Python --url collection:
--network hostis required so coverport can reach localhost--urlmust behttp://localhost:<port>/coverage(CLI appends?name=; bare host:port → 404)- Map the same port in
podman run(-p) andCOVERAGE_PORTif your image overrides the default collect --urlsavescoverage-output/<test-name>/.coverageonly — serializedCoverageData.dumps()bytes, not SQLite- Unlike K8s collect,
--urldoes not call/coverage/save— runcurl .../coverage/savefirst if needed - Generate XML on the GHA runner with the Python conversion script above (not
coverage xml --data-file=on the raw file) [paths]in.coveragercalone does not fix host-side XML — setcontainer_prefixin the conversion script to match WORKDIR (default/app/)- Do not use
coverport process --format=pythonon--urloutput until the CLI handles serialized data - Smoke-test before collect:
curl http://localhost:<port>/health(expectcoverage_enabled: true)
Pattern C: Client-Side / Test Runner-Based Coverage Collection
Use when coverage is collected by the test runner rather than from an HTTP endpoint — typically for frontend applications using Cypress or similar tools. No HTTP collection step is needed; instead, mount the coverage output directory into the coverport container for processing.
- name: Build instrumented image
run: |
podman build -f Dockerfile.instrumented -t myapp:instrumented .
- name: Run e2e tests with coverage
run: |
cd e2e-tests
npm ci
npm run cy:run:coverage
- name: Upload e2e coverage to Codecov
if: always()
run: |
mkdir -p coverport-output
podman run --rm \
-v $PWD/e2e-tests/.nyc_output:/workspace/coverage:ro \
-v $PWD/coverport-output:/workspace/output:rw \
-e CODECOV_TOKEN="${{ secrets.CODECOV_TOKEN }}" \
quay.io/konflux-ci/konflux-devprod/coverport-cli:${COVERPORT_TAG} \
process \
--coverage-dir=/workspace/coverage \
--format=nyc \
--repo-url="${{ github.server_url }}/${{ github.repository }}" \
--commit-sha="${{ github.sha }}" \
--workspace=/workspace/output \
--codecov-flags=e2e
Notes for client-side coverage:
- Use
--format=nycfor Istanbul/NYC coverage data (Cypress, Jest) - Coverage files are from the test runner output directory, not HTTP
- No
collectstep is needed — go straight toprocess
Pattern D (Rust): App Running Locally via Podman/Docker
Rust coverage requires an additional binary extraction step because
llvm-cov needs the original instrumented binary to produce LCOV output
from the collected .profraw data.
- name: Set up Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
components: llvm-tools-preview
- name: Add llvm-tools to PATH
run: |
TOOLCHAIN_LIB=$(rustc --print sysroot)/lib
echo "$(find $TOOLCHAIN_LIB -name 'llvm-profdata' -exec dirname {} \;)" >> $GITHUB_PATH
- name: Build instrumented image
run: |
podman build --build-arg ENABLE_COVERAGE=true -t myapp:instrumented .
- name: Extract binary from image
run: |
CONTAINER_ID=$(podman create myapp:instrumented)
podman cp $CONTAINER_ID:/app/<binary-name> ./coverage-binary
podman rm $CONTAINER_ID
chmod +x ./coverage-binary
- name: Start instrumented application
run: |
podman run -d --name app-under-test \
-p 8080:8080 -p 53700:53700 \
myapp:instrumented
- name: Run e2e tests
run: |
<your-e2e-test-command>
- name: Collect and upload Rust e2e coverage
if: always()
run: |
mkdir -p coverage-output && chmod 777 coverage-output
# Step 1: Collect profraw from the coverage HTTP endpoint
podman run --rm \
--network host \
-v $PWD/coverage-output:/workspace/coverage-output \
quay.io/konflux-ci/konflux-devprod/coverport-cli:${COVERPORT_TAG} \
collect \
--url http://localhost:53700/coverage \
--test-name="e2e-tests" \
--output=/workspace/coverage-output
# Step 2: Process profraw → LCOV and upload to Codecov
podman run --rm \
-v $PWD/coverage-output:/workspace/coverage-output \
-v $PWD/coverage-binary:/workspace/binary:ro \
-e CODECOV_TOKEN=${{ secrets.CODECOV_TOKEN }} \
-e COVERAGE_BINARY=/workspace/binary \
quay.io/konflux-ci/konflux-devprod/coverport-cli:${COVERPORT_TAG} \
process \
--coverage-dir=/workspace/coverage-output \
--format=rust \
--repo-url=${{ github.server_url }}/${{ github.repository }} \
--commit-sha=${{ github.sha }} \
--codecov-flags=e2e-tests
- name: Stop application
if: always()
run: podman stop app-under-test || true
Key points for Rust coverage in GitHub Actions:
- Port 53700 (not 9095) — Rust uses the coverport standard port
--format=rusttells coverport to usellvm-profdata+llvm-covfor processingCOVERAGE_BINARYenvironment variable (or--binaryflag) is required — points to the extracted instrumented binary- The binary must match the exact same build that produced the container image
llvm-tools-previewmust be installed sollvm-profdataandllvm-covare available- Binary extraction via
podman cpavoids needing to share volumes or rebuild locally
Self-Hosted Codecov with Coverport
When uploading to a self-hosted Codecov instance (see
codecov-config/CONFIG.md), add --codecov-url to the process
command:
podman run --rm \
-e CODECOV_TOKEN=${{ secrets.CODECOV_TOKEN }} \
quay.io/konflux-ci/konflux-devprod/coverport-cli:${COVERPORT_TAG} \
process \
--codecov-url=<CODECOV_INSTANCE_URL> \
--coverage-dir=/workspace/coverage-output \
--repo-url=${{ github.server_url }}/${{ github.repository }} \
--commit-sha=${{ github.sha }} \
--codecov-flags=e2e-tests
OIDC is not available for coverport uploads — coverport uses the
Codecov CLI internally, which always requires a CODECOV_TOKEN. The
codecov/codecov-action with OIDC is only for unit test uploads (see
Step 7a).
Step 8: Document Manual Steps
After making all changes, inform the user they need to create a Kubernetes secret.
IMPORTANT: The secret must be created in the namespace where your build and integration pipelines run. This is typically your tenant namespace (e.g., my-tenant, not a specific repository namespace like rhtap-release-2-tenant). You can identify the correct namespace by checking the namespace field in your .tekton/*-push.yaml file.
Option A - Using kubectl:
# First, create the dockerconfig JSON file
cat > /tmp/dockerconfig.json <<EOF
{"auths":{"quay.io":{"auth":"<base64-encoded-quay-user:token>","email":""}}}
EOF
# Create the secret with both keys in YOUR tenant namespace
kubectl create secret generic coverport-secrets \
--from-literal=codecov-token=<your-codecov-token> \
--from-file=oci-storage-dockerconfigjson=/tmp/dockerconfig.json \
-n <your-tenant-namespace>
# Clean up
rm /tmp/dockerconfig.json
Option B - Using YAML:
apiVersion: v1
kind: Secret
metadata:
name: coverport-secrets
namespace: <your-tenant-namespace> # Replace with your tenant namespace
type: Opaque
stringData:
codecov-token: <your-codecov-token>
oci-storage-dockerconfigjson: '{"auths":{"quay.io":{"auth":"<base64-encoded-quay-user:token>","email":""}}}'
Required secret keys:
codecov-token- Your Codecov API token for uploading coverage reports to Codecovoci-storage-dockerconfigjson- Docker config JSON with Quay.io credentials for pushing coverage test artifacts to an OCI container registry- This is used by the
collect-and-upload-coveragetask to store coverage data as OCI artifacts in quay.io - The coverage collection process extracts coverage data from instrumented containers and pushes it to the OCI registry before uploading to Codecov
- The
authvalue should be base64-encodedusername:token - To encode:
echo -n "quay-username:quay-token" | base64 - You need push access to the quay.io repository specified in the e2e pipeline's
oci-container-repoparameter
- This is used by the
Step 9: Post-Integration Validation Checklist
Before committing the changes, verify all modifications are correct:
Local validation (already completed in Step 5.5):
-
podman build(production) succeeds - Go/Rust:
podman build --build-arg ENABLE_COVERAGE=true(instrumented) succeeds - Python:
podman build --target test(instrumented) succeeds - Go/Rust instrumented build logs show "Building with coverage instrumentation..."
- Python instrumented container logs show
[coverage-wrapper] HTTP server listening on port <port> - Go production build logs show "Building production binary..."
- Python production stage runs plain Gunicorn (no
coverage_server.pywrapper)
Go module setup checklist:
-
go.modhas coverport dependency added (as direct, not indirect) -
go.sumhas coverport checksums -
coverage_init.goexists at the root of the module with correct build tags -
go mod tidyproduces no diff (dependency is correctly classified)
Python container instrumentation checklist:
- Four instrumentation files vendored (
coverage_server.py,sitecustomize.py,.coveragerc,gunicorn_coverage.py) -
sitecustomize.pyinstalled into site-packages in the instrumented Dockerfile stage -
COVERAGE_PROCESS_START,COVERAGE_DATA_DIR=/dev/shm, andTMPDIR=/dev/shmset in instrumented image - CMD uses
coverage_server.pywrapper with Gunicorn andgunicorn_coverage.pyconfig -
.coveragercsourcepath matches containerWORKDIR - Coverage HTTP port exposed and mapped (default 53700, or
COVERAGE_PORTif set) - Pattern A (Kind): upload
coverage-output/<test-name>/coverage.xmlafter collect - Pattern B (
--url):--urluseshttp://localhost:<port>/coverage; host-side deserialize + XML step after collect - Pattern B (
--url):curl .../coverage/savebefore collect when workers have not flushed data -
curl http://localhost:<port>/healthreturnscoverage_enabled: true(local validation)
File modifications checklist (Tekton path):
-
DockerfilehasENABLE_COVERAGEbuild arg (removedCOVERAGE_SERVER_URL) -
Dockerfilehas conditional build logic with-tags=coverageflag -
.tekton/*-push.yamlhasbuild-instrumented-imagetask afterprefetch-dependencies -
.tekton/*-push.yamlinstrumented task usesbuildah-oci-ta(notbuildah-remote-oci-ta) -
.tekton/*-push.yamlinstrumented task usesHERMETIC: $(params.hermetic)andPREFETCH_INPUT: $(params.prefetch-input) -
.tekton/*-pull-request.yamlhashermetic: "true"andprefetch-inputenabled -
.tekton/*-pull-request.yamlhasENABLE_COVERAGE=truein BUILD_ARGS -
integration-tests/pipelines/*e2e*.yamluses test-metadata v0.4 -
integration-tests/pipelines/*e2e*.yamlhascollect-and-upload-coveragetask -
integration-tests/pipelines/*e2e*.yamlupdated image references (if applicable)
File modifications checklist (GitHub Actions path):
-
.github/workflows/pr.y*mlhasflags: unit-testsin codecov action -
.github/workflows/codecov.y*mlhasflags: unit-testsin codecov action - E2e workflow has coverport collect/process steps using
podman run - Correct coverport collection pattern chosen (Kubernetes, local
--url, or client-side) -
CODECOV_TOKENsecret is configured in GitHub repository settings - For
--urlpattern:--network hostis set on the podman commands - For
--url/ client-side patterns:--repo-urland--commit-shaare passed toprocess - For self-hosted Codecov:
--codecov-urlis passed toprocess - For Kubernetes pattern: kubeconfig copied with
chmod 644before mounting - For all patterns: output directory created with
chmod 777 - If e2e test suite rebuilds the image (e.g., kubebuilder BeforeSuite):
ENABLE_COVERAGEis passed through
File modifications checklist (GitHub Actions-only path — no Tekton e2e pipeline):
- Tekton push pipelines are NOT modified (no
build-instrumented-imagetask) - Tekton PR pipelines are NOT modified (no
ENABLE_COVERAGE=truein BUILD_ARGS) - Makefile or build scripts pass
ENABLE_COVERAGEto container build commands - GitHub Actions e2e workflow sets
ENABLE_COVERAGE=truewhen building images - Coverport collect step runs after e2e tests with
if: always()
Documentation provided to user:
- Instructions for creating
coverport-secretsKubernetes secret in their tenant namespace (Tekton path) - Explanation that the namespace should be where their build and integration pipelines run
- Required secret keys:
codecov-tokenandoci-storage-dockerconfigjson - Explanation of what
oci-storage-dockerconfigjsonis used for (pushing coverage artifacts to quay.io) - Instructions for encoding auth credentials
- Note about needing push access to the quay.io repository
- For GitHub Actions:
CODECOV_TOKENmust be added as a repository secret
Summary to provide user (Tekton + GitHub Actions): List all modified files with brief description of changes:
Modified files:
- coverage_init.go: NEW - Coverage initialization with build tags
- go.mod: Added coverport dependency
- go.sum: Added coverport checksums
- Dockerfile: Added coverage instrumentation with build tags
- .tekton/<name>-push.yaml: Added instrumented image build task with hermetic support
- .tekton/<name>-pull-request.yaml: Enabled coverage and hermetic builds for PR builds
- integration-tests/pipelines/<name>-e2e-pipeline.yaml: Added coverage collection
- .github/workflows/pr.yml: Added unit-tests flag
- .github/workflows/codecov.yml: Added unit-tests flag
- .github/workflows/e2e.yml: Added coverport collect/process steps for e2e coverage
Validation
After integration is deployed to CI/CD, provide these verification steps to the user:
-
Check instrumented image build:
- Push a commit to main branch
- Verify the push pipeline creates an image with
.instrumentedtag - Check build logs for "Building with coverage instrumentation..." message
-
Check e2e coverage collection (Tekton path):
- Run e2e tests via integration pipeline
- Verify
collect-and-upload-coveragetask executes successfully - Check Codecov dashboard for coverage data with
e2e-testsflag
-
Check e2e coverage collection (GitHub Actions path):
- Trigger the e2e workflow
- Verify the coverport
collectandprocesssteps succeed in the logs - Check Codecov dashboard for coverage data with
e2e-testsflag - For
--urlcollection: verify the app container was reachable on the coverage port and--urlincluded/coverage(e.g.http://localhost:53700/coverage) (9095 only for legacy Go instrumentation)
-
Check unit test coverage:
- Create a PR
- Verify unit tests upload coverage with
unit-testsflag - Check Codecov shows both unit and e2e coverage
Troubleshooting
Common issues and solutions:
Build error: "coverage_init.go not found" or "package not imported"
- Cause: The
coverage_init.gofile is missing or in the wrong location - Solution:
- Ensure
coverage_init.goexists at the root of your Go module (same directory asmain.go) - Verify the file has the correct
//go:build coveragebuild tag - Check that the package declaration matches your main package (
package main)
- Ensure
Build error: "cannot find package"
- Cause: Coverport dependency not properly added to Go modules
- Solution:
- Run
go get github.com/konflux-ci/coverport/instrumentation/go - Verify
go.modhas the coverport dependency - Run
go mod tidyto clean up dependencies
- Run
CI check fails: "Go mod state is not clean"
- Cause:
go getadds dependencies as// indirect, butgo mod tidyreclassifies direct imports - Solution: Always run
go mod tidyaftergo get. The coverport import incoverage_init.go(even behind a build tag) makes it a direct dependency. If CI runsgo mod tidyand checks for diffs, the// indirectannotation will cause a failure.
Instrumented build fails:
- Verify coverport Go module dependency is in
go.modandgo.sum - Check that
coverage_init.gohas the correct build tag syntax (//go:build coverage, not// +build coverage) - Ensure the Dockerfile build command includes
-tags=coverage - Verify hermetic mode is enabled with proper prefetch configuration
- Ensure you're using
buildah-oci-tafor instrumented builds in push pipeline, notbuildah-remote-oci-ta - Verify there's no matrix build or PLATFORM parameter for the instrumented image task
Hermetic build fails with "cannot download dependencies"
- Cause: Go module dependencies not properly prefetched
- Solution:
- Ensure
hermetic: "true"is set in the pipeline parameters - Verify
prefetch-inputis set correctly:{"type": "gomod", "path": "."} - Check that the
prefetch-dependenciestask completed successfully - Review prefetch task logs for any download errors
- Ensure
Coverage data not uploaded:
- Verify
coverport-secretsexists in your tenant namespace (the namespace where your build and integration pipelines run) - Check
codecov-tokenkey exists in the secret - Check
oci-storage-dockerconfigjsonkey exists and is valid (should be a valid Docker config JSON) - Verify you h
Truncated - read the full file at https://github.com/konflux-ci/coverport/blob/c914ae07e9f0b423ca73d06ed613f78bd177e240/.claude/skills/coverport-integration/SKILL.md.