Skip to content

Dogfood proof: Aspire Deno runtime (Layers A+B) verified against the full NetScript framework #295

Description

@rickylabs

Dogfood proof — Aspire Deno runtime (Layers A + B) against the full NetScript framework

This issue is a dogfood-evidence record for the .NET Aspire team. It proves that both of the
Deno-runtime fork branches (rickylabs/aspire) work end-to-end against the heavy, complete NetScript
framework — by actually swapping NetScript's scaffold/host wiring onto them locally and booting real
scaffold output under the fork Aspire CLI. Evidence only — the dogfood branch is never merged.

Fork branches under test (rickylabs/aspire)

Dogfood branch (NetScript, evidence-only — NOT merged)

dogfood/aspire-deno-runtime — commits:

  • b704d839 feat(cli/scaffold): run TS AppHost under the Aspire fork Deno toolchain resolver [Layer A]
  • 75e4dea7 feat(cli/aspire): host Deno services and plugins via the fork's addDenoApp [Layer B]
  • cb2c3e98 test(dogfood): self-contained re-runnable targeted proof harness [crux proof]

What was swapped

Layer A (packages/cli/src/kernel/application/scaffold/render-ts-apphost.ts): the scaffolder now
emits a single aspire/deno.json (with "unstable":["sloppy-imports"] + JSR/subpath import map) and
no package.json / tsconfig.apphost.json. Removing package.json is what makes the fork's
resolver select the Deno toolchain path for apphost.mts.

Layer B (generate-register-services.ts, generate-register-plugins.ts + templates,
generate-aspire-config.ts, scaffold-aspire.ts, packages/aspire/.../resolve-env-vars.ts):
NetScript services and plugins are now hosted via builder.addDenoApp(name, workdir, entrypoint)
instead of addExecutable. Aspire.Hosting.JavaScript is registered in the packages map (empty
version → fork dev-mode source resolution). OTel switched to buildOtelEnvVars(name, ver, 'denoApp')
(3 env vars: OTEL_DENO=true + service name + service.version) and the explicit withOtlpExporter()
was dropped — the fork's WithDenoDefaults() wires OTLP natively.

Live boot proof (fork Aspire CLI, DB-less scaffold)

Booted real scaffold output under the fork CLI (dev mode via ASPIRE_REPO_ROOT, fork build at
13.5.0-dev): aspire restore exit 0 → deno check apphost.mts exit 0 → aspire start --isolated.

  • Layer A verified: aspire ps shows apphost.mts running under sdk 13.5.0-dev; aspire/deno.json present, no aspire/package.json.
  • Layer B verified: service resource source: "deno", appArgs: ["run","-A","src/main.ts"], state Running, health Healthy; live GET /healthHTTP 200 {"status":"healthy","version":"1.0.0"}.
  • OTel verified (zero app-side SDK): OTEL_DENO=true present (denoApp 3-var mode); the scaffolded service imports no OpenTelemetry SDK — telemetry is native via the fork's env-only bridge.

Validation (raw exit codes)

Docker-independent gates — all green:

  • deno check (packages/cli/src/kernel, 268 files): 0 failures; (packages/aspire/src, 31 files): 0 failures.
  • generator unit tests (aspire register generators): 13 passed / 122 steps / 0 failed.
  • packages/aspire unit tests (denoApp OTel mode): 18 passed / 51 steps / 0 failed.
  • targeted proof harness (.llm/tools/dogfood/prove-aspire-deno-runtime.ts, fresh run): OVERALL PASS — 12/12 assertions (8/8 Layer A/B), raw exit 0.

The targeted harness scaffolds a fresh DB-less NetScript project (--db none, a users
addDenoApp HTTP service), boots it under the fork CLI in dev mode, and asserts (parsing resource
JSON, not prose): Layer A (aspire/deno.json present + package.json absent, deno check exit 0,
sdkVersion=13.5.0-dev, apphost running); Layer B (codegen emits addDenoApp +
buildOtelEnvVars(...,'denoApp') with no withOtlpExporter; live resource source=deno,
appArgs=["run","-A","src/main.ts"], Running/Healthy); service discovery (URL resolved from
urls[].url, not hardcoded — --isolated randomizes the target PORT) → GET /health = 200; OTel
(the 3 denoApp vars OTEL_DENO=true / OTEL_SERVICE_NAME=users /
OTEL_RESOURCE_ATTRIBUTES=service.version=1.0.0 plus fork-injected OTEL_EXPORTER_OTLP_ENDPOINT
proving WithDenoDefaults() wires OTLP natively, and the service source imports zero
@opentelemetry SDK). Live-trace assertion via the dashboard telemetry API was intentionally skipped
(headless dashboard unavailable) — the env + zero-SDK proof is the accepted fallback.

Full scaffold.runtime merge-readiness E2E (warm Docker — Docker note corrected)

The earlier "Docker unavailable" caveat is superseded: Docker Desktop was updated (4.80.0, server
29.6.1, docker ps exit 0) and the full one-pass suite
deno task e2e:cli run scaffold.runtime --cleanup --format pretty was run twice.

Verdict (both runs): 11 passed / 1 failed.

  • All 11 swap-relevant steps green — scaffold init (incl. apphost.mts generation +
    deno check type-check), the 4 first-party plugin installs (workers/sagas/triggers/streams),
    plugin list, aspire restore, aspire start, aspire stop. This exercises both Layer A (TS
    AppHost under the Deno toolchain resolver) and Layer B (addDenoApp services/plugins) in the
    real merge-readiness harness, not just the targeted crux.
  • The lone failure is database.init, timing out at ~900030ms — the NetScript
    DbOperationRunner's own DEFAULT_TIMEOUT_MS = 15*60_000 (900s) cap.

Triage — swap-orthogonal, pre-existing flake (not a Layer A/B defect): netscript db init does
not boot the swapped app graph. It routes through the generated .helpers/db-cli-mode.mts
AppHost short-circuit, whose target set is built exclusively from
infrastructure.databases.get(...) (see generate-db-cli-mode.ts) — it starts only the database
resource
, and the DB resources were kept on addExecutable (never swapped to addDenoApp). So
the swapped addDenoApp services/plugins are never instantiated during db init; the timeout is in
the untouched Prisma/DB path. The consistent 900s hang across both warm-Docker runs matches the
documented pre-existing Prisma schema-engine db-init flake (#98/#145, bounded-retry in
migrate.ts), not the crash-and-clear signature and not the swap. Fork/swap surface is therefore
fully validated by the 11 green steps + the 12/12 crux boot above.

Node-free (ask #5): the boot path is Deno-only by construction — fork publishes denoland/deno:2
for both build+runtime stages, NetScript codegen emits only deno (zero addNodeApp/addBunApp/
node/npm), and the live crux boot shows every app resource as a deno run -A process
(source=deno). docker ps during the run carried only infra containers, no Node app container.
(Host ambient Node processes are the dev tooling — Claude Code itself runs on Node — not NetScript's
Aspire graph.) Full write-up: node-free-proof.md.

Dogfood feedback for the Aspire team (addDenoApp surface gaps)

addDenoApp's fixed deno run -A <script> cannot express several flags NetScript relies on, so those
resources stayed on addExecutable:

  • background (workers/sagas/triggers): need --unstable-worker-options before the script path.
  • apps (deno task dev), tools, db-cli: task-form and/or extra flags.
  • Swapped services/plugins additionally lose --node-modules-dir=none, --minimum-dependency-age=0,
    --config, and granular permissions (addDenoApp uses -A and auto-discovers deno.json).

Recommendation: let AddDenoApp accept caller-supplied Deno flags / permission scoping (or a
WithArgs-style escape hatch before the script path) so worker-options and granular perms are
expressible without falling back to addExecutable.


Proof for the Aspire team that Layers A + B hold up against a real, heavy polyglot framework.

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions