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:
- Run
run-rust-coverage-html.sh (new): cargo llvm-cov report --html reusing in-job profiling data.
- Flatten llvm-cov layout (see verification task below).
- 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
Definition of Done
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:
- Checkout
lgtm-ci → .lgtm-ci-tooling
- Harden runner / local composite actions
- 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):
- Harden runner (prefer pinned
lgtm-hq/lgtm-ci/.../harden-runner@<sha> or step-security/harden-runner)
- Checkout caller repository
- Checkout lgtm-ci tooling →
.lgtm-ci-tooling (only that path is cleaned)
DoD addition
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
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.
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.htmlat the top level), as used inexamples/bundle-manifest-turbo-themes.json. Today,reusable-rust-coverageandreusable-test-nodedo 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
reusable-deploy-site-with-reports,bundle-workflow-artifacts)workflow_call— track in a separate issue and link from docs only if needed before that landsProblem
Rust (
reusable-rust-coverage/reusable-test-rust-coverage)cargo llvm-cov report --htmlneeds profiling data from the same job as the LCOV run; a follow-up job cannot regenerate HTML cheaply from LCOV alone./coverage-rust/on a Model B site must copy reusable internals and add HTML +upload-artifactsteps themselves.Node (
reusable-test-node+upload-coverage)stage-node-coverage.shuploadsnode-coverage-<version>/<working-directory>/coverage/, which is correct for matrix/debugging but not for Model B bundling.coverage-htmlartifact expect a flat tree at the artifact root (coverage/index.html→ sitecoverage/index.htmlafterdest: coverage).Proposed Solution
Resolved input names (no bikeshed)
Use the same input id on both reusables:
upload-pages-coverage-htmlbooleanfalsepages-coverage-artifact-namestringpages-coverage-upload-onstringpush-mainpages-coverage-source-subpathstringcoverageDefaults (committed):
pages-coverage-artifact-name:rust-coverage-htmlpages-coverage-artifact-name:coverage-html(matchesexamples/bundle-manifest-turbo-themes.json)pages-coverage-upload-on:push-main— upload only whengithub.event_name == 'push'andgithub.ref == 'refs/heads/main'(no PR artifact noise). Other values out of scope for v1 unless explicitly added later.Node path join (explicit):
${working-directory}/${pages-coverage-source-subpath}(both inputs are repo-relative paths;working-directoryis the Vitest/coverage cwd, default.).index.htmlor tool-specific entry undercoverage/flattened per staging script).1.
reusable-rust-coverage: optional Pages HTML artifactAfter the existing LCOV run in the same job:
run-rust-coverage-html.sh(new):cargo llvm-cov report --htmlreusing in-job profiling data.upload-pages-coverage-html&&pages-coverage-upload-ongate passes:upload-artifactwith namepages-coverage-artifact-name.2.
reusable-test-node: optional flat Pages coverage artifactWhen
upload-pages-coverage-htmlis true (additive; keep existingupload-coverage/node-coverage-<version>/...behavior):${working-directory}/${pages-coverage-source-subpath}to artifact root.Outputs contract
New workflow outputs (pass-through from inner job where applicable):
pages-coverage-artifact-namepages-coverage-uploadedtrue/false— whether flat HTML artifact was uploaded this runpassed,coverage-percent(Rust)bundle-workflow-artifactscontinues to resolve by manifestartifactstring + workflow run lookup; outputs are for caller observability, not required for bundling.Artifact naming with matrix (Node):
upload-coverageartifacts: stillnode-coverage-<version>(unchanged).node-versionsmatrix is non-empty andupload-pages-coverage-htmlis true:node-versionequals the first entry innode-versions(or solenode-versionwhen matrix disabled). Other matrix legs skip flat upload (document in workflow).pages-coverage-artifact-nameand manifest entry (advanced; not default).Matrix collision handling (Node) — required behavior
node-version, flat upload onnode-versionsmatrix, flat upload onupload-artifactname collisionEdge cases & failure modes
upload-pages-coverage-html: truebut HTML dir missing or noindex.htmlpages-coverage-source-subpathdoes not exist (Node)coverage: false) but flat upload onpages-coverage-uploaded: false(do not fail unless caller also required coverage)if:on prior step success)llvm-cov layout verification (Rust) — explicit task
cargo llvm-covin lgtm-ci (CI + local):cargo llvm-cov report --html --output-dir <dir>and document actual paths (e.g.<dir>/html/index.htmlvs other).--htmlreuses the same profiling data as the in-job LCOV run (no second test run).Definition of Done
reusable-rust-coverageproduces an artifact whose zip root containsindex.html(browsable HTML tree; no nestedhtml/prefix after flatten).reusable-test-nodeproduces a flat artifact (index.htmlat artifact root from Vitest HTML undercoverage/, nonode-coverage-<version>/...prefix).bundle-workflow-artifacts→reusable-deploy-site-with-reports(exercise viabundle_run_manifesttest + example manifest).pages-coverage-upload-on: push-main— push tomainonly; no PR uploads).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.run-rust-coverage-html.sh, flat Node staging, matrix skip logic).bundle_run_manifestcopies each artifact into a site root with expectedindex.htmlpaths.Pinning docs: not part of this issue’s DoD — separate issue for
workflow_callcommit-SHA vs tag-object-SHA + Renovate snippet (git rev-parse vX.Y.Z^{commit}).Delivery breakdown (3 PRs, independent)
reusable-rust-coverageinputs/outputsreusable-test-nodeinputs/outputsreusable-workflows.mdinput tables) + example manifest tweakNo sequencing dependency between A and B. C should reference shipped input names from A/B.
Test evidence
stage-node-coverage.sh(unchanged behavior when flat upload off).tests/bats/unit/lib/bundle/test_workflow_artifacts.bats— manifest entries pointing atrust-coverage-html/coverage-htmlfixture zips; assertbundle_run_manifestcopies todestwithindex.htmlpresent.reusable-deploy-site-with-reportsin 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:lgtm-ci→.lgtm-ci-toolingStep 3 uses
actions/checkoutat 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'preexecution not supported for missing local actions.lgtm-ci-tooling/scripts/ci/…Correct pattern (already used in
reusable-test-nodematrix /test-customjobs):lgtm-hq/lgtm-ci/.../harden-runner@<sha>orstep-security/harden-runner).lgtm-ci-tooling(only that path is cleaned)DoD addition
reusable-test-rust-coverage(andreusable-rust-coverageif it inlines the same steps) reordered to repo → toolingDelivery: 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-nodeskipstest-vitest(if: inputs.test-command == '') and runstest-custominstead. Skipped jobs often do not evaluatejob.nameexpressions; GitHub displays the literal YAML (actions/runner#1215).Example raw label in checks:
Source in
reusable-test-node.yml(test-vitestandtest-customshare this pattern):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)
name:values (noinputs/matrixin names), e.g.:test-vitest:name: Vitest tests(orNode.js tests (Vitest))inputs.job-namewhennode-versions == ''without a ternary inname:.reusable-workflows.mdwhich jobs are skipped whentest-commandis set vs default Vitest, so PR “N skipped checks” is understandable.coverage-pr-commentvspost-pr-comment:coverage-pr-comment: truebuilds the comment artifact insidetest-custom, but the separateNode coverage PR commentjob also requirespost-pr-comment: true. Callers that setcoverage-pr-comment: trueandpost-pr-comment: falsewill see that poster job skipped by design — document the intended combination.DoD addition
inputs.node-versions == '' && …for skippedtest-vitestwhentest-commandis set.test-command/post-pr-comment/coverage-pr-commentinteractions and expected skipped jobs on PRs.Delivery: PR B (Node flat artifact +
reusable-test-nodeUX).Implementation Notes
reusable-deploy-site-with-reports,examples/bundle-manifest-turbo-themes.json.build-command, sitedistpaths, Astro base paths, customsetup-script/coverage-scriptpaths.Benefits
reusable-rust-coverage+reusable-test-node+reusable-deploy-site-with-reportswithout repack jobs.