Skip to content

QVAC-22749: infra: add fabric-stack CI for npm @qvac/fabric consumers - #3827

Open
jpgaribotti wants to merge 2 commits into
mainfrom
feature/QVAC-22749
Open

QVAC-22749: infra: add fabric-stack CI for npm @qvac/fabric consumers#3827
jpgaribotti wants to merge 2 commits into
mainfrom
feature/QVAC-22749

Conversation

@jpgaribotti

Copy link
Copy Markdown
Contributor

🎯 What problem does this PR solve?

  • npm-runtime consumers (@qvac/classification-ggml today) install @qvac/fabric from the registry during CI, so stack PRs that change unreleased qvac-fabric cannot validate consumer compile/link against the PR-built runtime.
  • Parallel qvac-fabric + addon development already works for direct vcpkg consumers via overlay ports, but there was no equivalent bridge for npm-runtime consumers.
  • Authors had no documented, automated path to prove a combined fabric + consumer change before merge without publishing a dev @qvac/fabric to npm.

📝 How does it solve it?

  • Adds fabric-stack detection (.github/actions/detect-fabric-stack) when a PR touches vcpkg-overlays/ports/qvac-fabric/** or packages/fabric vcpkg manifests; disables stale prebuild reuse on stack PRs.
  • Extends on-pr-fabric to publish a fabric-prebuilds artifact and run an npm-consumer smoke matrix driven by .github/fabric-consumers.json.
  • Adds overlay-local-fabric composite action to copy PR fabric prebuilds into node_modules/@qvac/fabric before bare-make generate.
  • Adds wait-and-download-fabric-prebuilds so on-pr-classification-ggml can consume the fabric artifact from the parallel on-pr-fabric run.
  • Wires classification-ggml prebuilds, cpp-tests, and integration tests to overlay PR fabric when fabric_stack=true via fabric-overlay-artifact on reusable-prebuilds.
  • Adds fabric-stack-npm-consumer-smoke.yml (linux-x64 compile smoke for npm-runtime consumers).
  • Documents the author workflow in packages/fabric/INTEGRATION.md.
  • Preserves upstream publish-prebuild-status in on-pr-fabric.yml.

New files

  • .github/actions/detect-fabric-stack/action.yml
  • .github/actions/overlay-local-fabric/action.yml
  • .github/actions/wait-and-download-fabric-prebuilds/action.yml
  • .github/fabric-consumers.json
  • .github/workflows/fabric-stack-npm-consumer-smoke.yml

Modified workflows

  • on-pr-fabric.yml, on-pr-classification-ggml.yml
  • cpp-tests-classification.yml, integration-test-classification-ggml.yml
  • prebuilds-classification-ggml.yml, reusable-prebuilds.yml

🧪 How was it tested?

  • Manual workflow_dispatch on feature/QVAC-22749-test against tetherto/qvac:
    • on-pr-fabric (run 31109191988): fabric prebuild matrix, fabric-prebuilds publish, and fabric-stack-smoke-classification-ggml all green.
    • on-pr-classification-ggml (run 31182794170): resolve-fabric-prebuilds → overlay → cpp-tests (3 platforms) → prebuilds (9 platforms) → desktop integration → Android/iOS Device Farm all green.
  • Iterative CI fixes from the test branch are included (PR-head action checkout, bash 3.2 / Windows compatibility, tags passthrough for simulator overlays).
  • Test vcpkg overlay fixture from the exploration branch is not included in this PR.

Wire PR-built fabric prebuilds into npm-runtime consumer workflows so stack
PRs validate against unreleased qvac-fabric without a dev npm publish. Adds
overlay-local-fabric, fabric-prebuilds artifact publishing on on-pr-fabric,
classification-ggml integration, and author docs in INTEGRATION.md.

Includes CI fixes from feature/QVAC-22749-test (Windows/bash 3.2 compatibility,
PR-head action checkout, tags passthrough) rebased onto upstream/main with
publish-prebuild-status preserved. Test vcpkg overlay fixture omitted.
@jpgaribotti
jpgaribotti requested review from a team as code owners August 13, 2026 10:37
@github-actions

Copy link
Copy Markdown
Contributor

Review Status

Current Status: ❌ PENDING
Approvals so far: none

Pending reviews: Needs 1 Management or Team Lead, and 1 more from Management, Team Lead, or Member.

@github-actions

github-actions Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

License compliance — findings detected (warn-only)

Critical: 0 · High: 2 · Medium: 0

Dependency License Scope Severity Outcome
actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c (none detected) runtime High blocks
actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f (none detected) runtime High blocks

How to resolve a blocking finding:

  • Remove or replace the disallowed dependency, or
  • If the license is genuinely acceptable, run the compliance SKILL and record the decision in .github/license-allowlist.yml (CODEOWNERS-reviewed), or
  • For a one-off, a maintainer can apply the license-override label (High findings only; Critical cannot be overridden).

Warn-only (shadow) mode — this check does not block merges yet.

Updated automatically by the canonical license compliance workflow.

NOTICE presence (advisory)

Missing NOTICE (advisory, does not block):

  • ./.github/actions/release-merge-guard
  • ./docs/website
  • ./packages/ggml-coload-smoke
  • ./packages/fabric/test/integration
  • ./packages/inference-addon-cpp/mobile
  • ./packages/sdk/e2e
  • ./packages/llm-llamacpp/benchmarks/performance
  • ./packages/llm-llamacpp/benchmarks/server
  • ./packages/vla-ggml/sim/server
  • ./packages/embed-llamacpp/benchmarks/performance
  • ./packages/embed-llamacpp/benchmarks/server
  • ./packages/asr-ggml/benchmarks/server

@github-actions

github-actions Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Workflow security (shadow mode)

zizmor found 1112 finding(s) in .github/ (highest severity: high). This check is warn-only and does not block the merge.

Findings are annotated inline on the changed files and listed in the job summary.

Reproduce locally:

pipx run zizmor==1.27.0 --offline .github/

Comment on lines +115 to +119
- name: Detect fabric-stack PR
id: detect
uses: ./.github/actions/detect-fabric-stack

resolve-fabric-prebuilds:
Comment on lines +138 to +143
- name: Wait for fabric-prebuilds from on-pr-fabric
uses: ./.github/actions/wait-and-download-fabric-prebuilds
with:
github-token: ${{ github.token }}
head-sha: ${{ github.event.pull_request.head.sha }}
- name: Re-publish fabric-prebuilds for downstream jobs
Comment on lines +220 to +224
- name: Detect fabric-stack PR
id: detect
uses: ./.github/actions/detect-fabric-stack

fabric-stack-matrix:

@GustavoA1604 GustavoA1604 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[critical] .github/workflows/on-pr-classification-ggml.yml — line 387 (detect-fabric-stack,)

This patch deletes the publish-prebuild-status job that publishes qvac/prebuild-classification-ggml. The central Merge Guard explicitly requires a fresh status from this workflow, so future classification PRs will fail verify-prebuilds. Restore the publisher and include it in merge-guard.needs.

[critical] .github/workflows/on-pr-classification-ggml.yml — line 139 (uses: ./.github/actions/wait-and-download-fabric-prebuilds)

This pull_request_target workflow executes a composite action checked out from the PR head. A same-repository PR is authorized automatically, allowing modified action code to run in the privileged default-branch context and potentially poison caches or exfiltrate accessible artifacts. Execute only default-branch action code, or move PR-head execution to an unprivileged pull_request workflow. The same issue exists for detect-fabric-stack in both caller workflows.

[critical] .github/actions/detect-fabric-stack/action.yml — line 60 (elif [ -n "$BEFORE_SHA" ] && [ "$BEFORE_SHA" != "$HEAD_SHA" ]; then)

Detection examines only the latest synchronization delta. If an earlier commit changed fabric and the newest push changes only the consumer, this returns false and re-enables stale prebuild reuse. labeled, reopened, and ready_for_review events also default to a head-to-head empty diff. For every PR event, compare the PR base SHA with the current head SHA.

[critical] .github/workflows/on-pr-classification-ggml.yml — line 388 (resolve-fabric-prebuilds,)

Adding this job to needs does not gate Merge Guard on its result. If artifact resolution fails, the dependent build and tests are skipped, but the status expressions below accept skipped jobs as success. Require successful detection/resolution whenever fabric_stack is true before allowing skipped downstream jobs to pass.

[critical] .github/workflows/on-pr-fabric.yml — line 461 (integration-tests-status: ${{ needs.fabric-npm-consumer-smoke.result == 'success' || needs.fabric-npm-consumer-smoke.result == 'skipped' }})

This treats every skipped smoke test as successful, including skips caused by detection, matrix, or artifact-publication failures. That allows a fabric-stack PR to pass without testing any npm consumer. Accept skipped only when detection succeeded and established that this is not a fabric-stack change.

[medium] .github/workflows/on-pr-fabric.yml — line 18 (- ".github/workflows/fabric.yml")

The patch removes vcpkg-overlays/ports/qvac-fabric/** from the trigger even though overlay changes are defined as fabric-stack changes. An overlay plus consumer-only PR will start the consumer workflow, which waits for an artifact from on-pr-fabric, but this workflow never starts and the consumer run times out. Restore the overlay path filter.

[medium] .github/workflows/on-pr-classification-ggml.yml — line 142 (head-sha: ${{ github.event.pull_request.head.sha }})

workflow_dispatch and workflow_call are deliberately classified as fabric-stack events, but they have no pull_request.head.sha. The downloader then queries with an empty head_sha, potentially selecting an unrelated recent fabric artifact. Pass ${{ github.event.pull_request.head.sha || github.sha }} and reject an empty SHA inside the action.

[medium] .github/workflows/on-pr-classification-ggml.yml — line 357 (fabric-overlay-artifact: ${{ needs.detect-fabric-stack.outputs.fabric_stack == 'true' && 'fabric-prebuilds' || '' }})

The PR fabric runtime is passed to desktop integration tests but not to run-mobile-integration-tests. The mobile app installs @qvac/fabric through npm, so it still packages the published runtime while testing classification prebuilds compiled against the PR runtime. Pass and apply the fabric overlay in the mobile workflow as well.

[medium] .github/actions/overlay-local-fabric/action.yml — line 92 (mkdir -p "$dest_plat")

The destination platform directory is merged rather than replaced. Files removed or renamed by the PR remain from the published npm package and can make compile/runtime tests succeed against stale artifacts. Remove dest_plat before copying the PR-built platform tree; apply the same replacement behavior in the all-platform branch.

Comment on lines 175 to +181
needs:
- fork-approval
- authorize
- sanity-checks
- ci-router
- detect-fabric-stack
- resolve-fabric-prebuilds

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cpp-tests gained resolve-fabric-prebuilds as a dependency, but its if: has no status-check function, so the implicit success() skips it whenever that job skips.

resolve-fabric-prebuilds is skipped on every PR where fabric_stack != 'true' — the large majority. GitHub applies an implicit success() across all of needs unless the if: contains a status-check function, and this one contains none.

The asymmetry inside this file is the giveaway: prebuild (line 281) and run-integration-tests (line 333) both open with always() for exactly this reason, and both were edited in this PR. The inner (fabric_stack != 'true' || ...) clause shows the intent was for cpp-tests to run when fabric_stack is false; the outer implicit success() overrides it.

Impact: on an ordinary classification-ggml PR carrying run-cpp-addon-tests, C++ unit tests and the Linux fuzz targets stop running, and sanity-checks-status (line 403) scores the skipped result as a pass — so the merge guard stays green. This degrades the non-fabric-stack path, which is what separates it from the skipped-as-success issue raised on line 388.

Suggested fix: prefix with always() && so the status-check function suppresses the implicit success(), then re-add the sanity-checks guard that always() removes — without it the job would also run after an upstream failure.

if: |
  always() &&
  needs.sanity-checks.result == 'success' &&
  needs.ci-router.outputs.run_cpp_tests == 'true' &&
  needs.authorize.outputs.allowed == 'true' &&
  (needs.detect-fabric-stack.outputs.fabric_stack != 'true' || needs.resolve-fabric-prebuilds.result == 'success')

Comment on lines +143 to +147
- name: Re-publish fabric-prebuilds for downstream jobs
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # 7.0.0
with:
name: fabric-prebuilds
path: fabric-prebuilds-staging

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reusable-prebuilds.yml's merge job downloads every artifact in the run with merge-multiple: true, so the 168 MB fabric tree is absorbed into the classification-ggml prebuilds bundle.

reusable-prebuilds.yml:518-522 calls actions/download-artifact with no name and no pattern, flattening every artifact in the run into prebuilds/ before re-uploading under that name. Reusable workflows share the caller's run id, and prebuild transitively depends on resolve-fabric-prebuilds, so fabric-prebuilds is already present when the merge runs.

Measured on run 31182794170 — fabric-prebuilds 168,270,355 B; the nine classification-ggml-* per-platform artifacts 2,878,435 B; merged prebuilds 171,148,592 B. The difference is 2,878,237 B, so the shipped bundle is the fabric tree plus the addon artifacts.

Impact: integration-test-classification-ggml.yml copies prebuilds-staging/<platform>-<arch>/ into packages/classification-ggml/prebuilds/, so the integration tests run with fabric's runtime inside the addon's own prebuilds directory — masking whether the addon resolves @qvac/fabric from node_modules, which is the packaging property this bridge exists to prove. prebuild-artifact-save then marks the polluted bundle reusable, so a later non-stack push with an unchanged native hash can have it re-published.

Suggested fix: give the hand-off artifact a name the merge job's unfiltered glob will not absorb, and point fabric-overlay-artifact (lines 188, 308, 357) at the new name. This keeps fabric out of the bundle without touching the shared merge job.

- name: Re-publish fabric-prebuilds for downstream jobs
  uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # 7.0.0
  with:
    name: fabric-prebuilds-resolved
    path: fabric-prebuilds-staging

Alternatively, scope reusable-prebuilds.yml's merge download with an explicit pattern:.

found=false

while [ "$(date +%s)" -lt "$deadline" ]; do
RUNS=$(gh api "repos/$REPO/actions/workflows/${WORKFLOW_FILE}/runs?head_sha=${HEAD_SHA}&per_page=20" \

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The empty-SHA mechanism is already reported on line 142; what is new here is that the failure has already occurred, in the run this PR cites as its validation.

On workflow_dispatch, github.event.pull_request.head.sha is empty, so this query becomes ...runs?head_sha=&per_page=20. GitHub drops an empty filter rather than matching nothing, so every recent on-pr-fabric run qualifies and the first one carrying the artifact wins.

That is not a latent risk here. The consumer run cited in the PR description, 31182794170, ran at head 6ffdb5770f3e and downloaded from run 31109191988, whose head is 5ab657a89870 — a different commit, from the previous day. The run offered as proof the bridge works compiled and tested classification-ggml against fabric from an unrelated commit.

Impact: on a real pull_request_target run head.sha is populated and the filter works, so no merge-affecting path is exposed. It bites workflow_dispatch, which this repo uses routinely for rollout and benchmark validation — the mode whose results are most likely to be trusted as definitive.

Suggested fix: alongside the head.sha || github.sha fallback proposed on line 142, make the action refuse to issue an unfiltered query, so a future caller passing an empty value fails loudly instead of silently selecting someone else's artifact.

if [ -z "$HEAD_SHA" ]; then
  echo "::error::head-sha is required — refusing to query without a run filter"
  exit 1
fi

Worth re-running the validation on a labeled-event PR before merge: workflow_dispatch also short-circuits detect-fabric-stack to true, so neither cited run exercised the detection path either.

@@ -0,0 +1,13 @@
{
"vcpkg_direct": [

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

vcpkg_direct names six packages, but seven carry a qvac-fabric vcpkg dependency — model-fit is missing — and fabric itself is listed as a consumer of the thing it produces.

Seven packages depend on qvac-fabric in their vcpkg.json at this head: embed-llamacpp, fabric, llm-llamacpp, model-fit, ocr-ggml, translation-nmtcpp, vla-ggml. on-pr-model-fit.yml:94 runs verify-qvac-fabric-lockstep alongside the other six, so model-fit is a first-class fabric consumer by the repo's own definition. INTEGRATION.md:270-273 repeats the omission and drops fabric too, naming only five.

Impact: nothing reads vcpkg_direct today — only .npm_runtime is consumed, at on-pr-fabric.yml:245 — so no build breaks. It matters because INTEGRATION.md:287 positions this file as the roster of record, instructing authors to "move it from vcpkg_direct to npm_runtime" on migration. A package that was never in the roster will not be moved when its turn comes, and this ships wrong on day one.

Suggested fix: add the missing entry so the roster matches the dependency graph.

  "vcpkg_direct": [
    "fabric",
    "llm-llamacpp",
    "embed-llamacpp",
    "translation-nmtcpp",
    "ocr-ggml",
    "vla-ggml",
    "model-fit"
  ],

fabric is worth a separate decision — it is the producer, so listing it as a consumer of itself is either deliberate (it does carry the vcpkg dependency) or a copy artifact; a one-line comment either way would settle it. And since nothing validates this list, consider either checking it against packages/*/vcpkg.json in CI or dropping the key — an unread manifest that is already inaccurate will only drift further.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants