Skip to content

feat(ci): Pages Model B coverage HTML artifacts for Rust and Node reusables #251

@TurboCoder13

Description

@TurboCoder13

Summary

Model B site deploys (reusable-deploy-site-with-reports + bundle-workflow-artifacts) expect workflow artifacts whose zip root is a browsable HTML coverage tree (e.g. index.html at the top level), as used in examples/bundle-manifest-turbo-themes.json. Today, reusable-rust-coverage and reusable-test-node do not produce artifacts in that shape, so consumers (e.g. lgtm-hq/Rustume) add repack jobs or duplicate reusable step lists locally.

This issue tracks generic, reusable extensions to lgtm-ci—not repo-specific build scripts or bundle manifests.

Relationships


Problem

Rust (reusable-rust-coverage / reusable-test-rust-coverage)

  • Outputs LCOV and PR-comment artifacts only.
  • cargo llvm-cov report --html needs profiling data from the same job as the LCOV run; a follow-up job cannot regenerate HTML cheaply from LCOV alone.
  • Callers that want /coverage-rust/ on a Model B site must copy reusable internals and add HTML + upload-artifact steps themselves.

Node (reusable-test-node + upload-coverage)

  • stage-node-coverage.sh uploads node-coverage-<version>/<working-directory>/coverage/, which is correct for matrix/debugging but not for Model B bundling.
  • Manifest entries like turbo-themes’ coverage-html artifact expect a flat tree at the artifact root (coverage/index.html → site coverage/index.html after dest: coverage).

Proposed Solution

Resolved input names (no bikeshed)

Use the same input id on both reusables:

Input Type Required Default Applies to
upload-pages-coverage-html boolean no false Rust + Node
pages-coverage-artifact-name string no see below Rust + Node
pages-coverage-upload-on string no push-main Rust + Node
pages-coverage-source-subpath string no coverage Node only

Defaults (committed):

  • Rust pages-coverage-artifact-name: rust-coverage-html
  • Node pages-coverage-artifact-name: coverage-html (matches examples/bundle-manifest-turbo-themes.json)
  • pages-coverage-upload-on: push-main — upload only when github.event_name == 'push' and github.ref == 'refs/heads/main' (no PR artifact noise). Other values out of scope for v1 unless explicitly added later.

Node path join (explicit):

  • Source directory = ${working-directory}/${pages-coverage-source-subpath} (both inputs are repo-relative paths; working-directory is the Vitest/coverage cwd, default .).
  • Flat artifact root = contents of that directory copied to zip root (expect index.html or tool-specific entry under coverage/ flattened per staging script).

1. reusable-rust-coverage: optional Pages HTML artifact

After the existing LCOV run in the same job:

  1. Run run-rust-coverage-html.sh (new): cargo llvm-cov report --html reusing in-job profiling data.
  2. Flatten llvm-cov layout (see verification task below).
  3. When upload-pages-coverage-html && pages-coverage-upload-on gate passes: upload-artifact with name pages-coverage-artifact-name.

2. reusable-test-node: optional flat Pages coverage artifact

When upload-pages-coverage-html is true (additive; keep existing upload-coverage / node-coverage-<version>/... behavior):

  • Stage flat tree from ${working-directory}/${pages-coverage-source-subpath} to artifact root.
  • Matrix: see collision rules below — do not let multiple matrix legs upload the same flat artifact name.

Outputs contract

New workflow outputs (pass-through from inner job where applicable):

Output Type Value
pages-coverage-artifact-name string Resolved artifact name (after defaulting)
pages-coverage-uploaded string true / false — whether flat HTML artifact was uploaded this run
Existing passed, coverage-percent (Rust) unchanged

bundle-workflow-artifacts continues to resolve by manifest artifact string + workflow run lookup; outputs are for caller observability, not required for bundling.

Artifact naming with matrix (Node):

  • Matrix upload-coverage artifacts: still node-coverage-<version> (unchanged).
  • Flat Pages artifact: single upload per workflow run. When node-versions matrix is non-empty and upload-pages-coverage-html is true:
    • v1 rule: upload only from the job where node-version equals the first entry in node-versions (or sole node-version when matrix disabled). Other matrix legs skip flat upload (document in workflow).
    • Callers needing another leg must set a distinct pages-coverage-artifact-name and manifest entry (advanced; not default).

Matrix collision handling (Node) — required behavior

Scenario Behavior
Single node-version, flat upload on One flat artifact; OK
node-versions matrix, flat upload on Upload from designated version only (see above); no upload-artifact name collision
Flat upload on, coverage dir missing / empty See edge cases

Edge cases & failure modes

Case Behavior (v1)
upload-pages-coverage-html: true but HTML dir missing or no index.html Fail the job with a clear log message (do not upload empty artifact)
pages-coverage-source-subpath does not exist (Node) Fail with path in message
Coverage disabled (coverage: false) but flat upload on Skip upload; pages-coverage-uploaded: false (do not fail unless caller also required coverage)
Rust LCOV step failed Existing fail behavior; HTML step must not run (if: on prior step success)

llvm-cov layout verification (Rust) — explicit task

  • Confirm layout for pinned cargo llvm-cov in lgtm-ci (CI + local): cargo llvm-cov report --html --output-dir <dir> and document actual paths (e.g. <dir>/html/index.html vs other).
  • Implement flatten step against documented paths only (no version-assumption comments without evidence).
  • Verify --html reuses the same profiling data as the in-job LCOV run (no second test run).

Definition of Done

  • reusable-rust-coverage produces an artifact whose zip root contains index.html (browsable HTML tree; no nested html/ prefix after flatten).
  • reusable-test-node produces a flat artifact (index.html at artifact root from Vitest HTML under coverage/, no node-coverage-<version>/... prefix).
  • Both artifacts resolve through bundle-workflow-artifactsreusable-deploy-site-with-reports (exercise via bundle_run_manifest test + example manifest).
  • Upload gating works (pages-coverage-upload-on: push-main — push to main only; no PR uploads).
  • Matrix collision rule implemented and documented for Node.
  • Input reference tables added to docs/reusable-workflows.md (type, required, default, purpose per input).
  • examples/bundle-manifest-turbo-themes.json (or sibling example) updated to consume new artifacts where applicable.
  • BATS tests added/passing for new/modified scripts (run-rust-coverage-html.sh, flat Node staging, matrix skip logic).
  • End-to-end proof: fixture manifest entries + bundle_run_manifest copies each artifact into a site root with expected index.html paths.

Pinning docs: not part of this issue’s DoD — separate issue for workflow_call commit-SHA vs tag-object-SHA + Renovate snippet (git rev-parse vX.Y.Z^{commit}).


Delivery breakdown (3 PRs, independent)

PR Scope Can merge alone
A Rust HTML artifact + flatten script + BATS + reusable-rust-coverage inputs/outputs Yes
B Node flat artifact + matrix rule + BATS + reusable-test-node inputs/outputs Yes
C Docs (reusable-workflows.md input tables) + example manifest tweak Yes (after A/B or in parallel documenting flags)

No sequencing dependency between A and B. C should reference shipped input names from A/B.


Test evidence

  • Unit/BATS: new scripts + regression on stage-node-coverage.sh (unchanged behavior when flat upload off).
  • Integration: extend tests/bats/unit/lib/bundle/test_workflow_artifacts.bats — manifest entries pointing at rust-coverage-html / coverage-html fixture zips; assert bundle_run_manifest copies to dest with index.html present.
  • Deploy smoke: not required for v1; optional follow-up using reusable-deploy-site-with-reports in lgtm-ci’s own CI or a fixture consumer repo.

Bug fix: checkout order in Rust coverage reusables

Not yet in lgtm-ci; discovered while implementing consumer work (lgtm-hq/Rustume #264).

reusable-test-rust-coverage.yml (and callers mirroring it) currently run:

  1. Checkout lgtm-ci.lgtm-ci-tooling
  2. Harden runner / local composite actions
  3. Checkout caller repository at workspace root

Step 3 uses actions/checkout at the default path, which cleans the entire workspace and deletes .lgtm-ci-tooling. Symptoms on consumers:

  • Can't find 'action.yml' … under '.lgtm-ci-tooling/.github/actions/harden-runner'
  • pre execution not supported for missing local actions
  • Exit 127 on scripts under .lgtm-ci-tooling/scripts/ci/…

Correct pattern (already used in reusable-test-node matrix / test-custom jobs):

  1. Harden runner (prefer pinned lgtm-hq/lgtm-ci/.../harden-runner@<sha> or step-security/harden-runner)
  2. Checkout caller repository
  3. Checkout lgtm-ci tooling.lgtm-ci-tooling (only that path is cleaned)

DoD addition

  • reusable-test-rust-coverage (and reusable-rust-coverage if it inlines the same steps) reordered to repo → tooling
  • Regression test or workflow lint note so tooling-before-repo cannot return

Delivery: Include in PR A (Rust coverage) or a small prerequisite PR before Rust HTML artifact work.


UX: skipped job names show raw expressions (reusable-test-node)

Discovered on consumer Rustume PR #264 — looks broken in the PR checks UI but is upstream behavior.

When callers set test-command (e.g. bun run test:coverage), reusable-test-node skips test-vitest (if: inputs.test-command == '') and runs test-custom instead. Skipped jobs often do not evaluate job.name expressions; GitHub displays the literal YAML (actions/runner#1215).

Example raw label in checks:

Coverage Reports / web-coverage / inputs.node-versions == '' && inputs.job-name || format('{0} ({1})'...

Source in reusable-test-node.yml (test-vitest and test-custom share this pattern):

name: >-
  ${{
    inputs.node-versions == '' && inputs.job-name ||
    format('{0} ({1})', inputs.job-name, matrix.node-version)
  }}

Expected consumer experience: only the running job shows a human name (e.g. web-coverage / 🌐 Web Coverage); skipped siblings should not pollute the checks list with expression text.

Proposed fix (PR B)

  • Give skipped-by-design jobs static name: values (no inputs/matrix in names), e.g.:
    • test-vitest: name: Vitest tests (or Node.js tests (Vitest))
    • Keep dynamic naming only on jobs that actually run for the caller path, or use a single static name equal to inputs.job-name when node-versions == '' without a ternary in name:.
  • Document in reusable-workflows.md which jobs are skipped when test-command is set vs default Vitest, so PR “N skipped checks” is understandable.
  • Clarify coverage-pr-comment vs post-pr-comment: coverage-pr-comment: true builds the comment artifact inside test-custom, but the separate Node coverage PR comment job also requires post-pr-comment: true. Callers that set coverage-pr-comment: true and post-pr-comment: false will see that poster job skipped by design — document the intended combination.

DoD addition

  • PR checks no longer show unevaluated inputs.node-versions == '' && … for skipped test-vitest when test-command is set.
  • Input docs explain test-command / post-pr-comment / coverage-pr-comment interactions and expected skipped jobs on PRs.

Delivery: PR B (Node flat artifact + reusable-test-node UX).


Implementation Notes

Benefits

  • Consumers call reusable-rust-coverage + reusable-test-node + reusable-deploy-site-with-reports without repack jobs.
  • Aligns Node flat layout with turbo-themes manifests.
  • Clear DoD, inputs, outputs, and matrix rules reduce review churn.

Metadata

Metadata

Assignees

Labels

ciCI/CD infrastructureenhancementNew feature or request

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions