From 27b0f86ed2bd6721c6ceca208440d4ffc91c14eb Mon Sep 17 00:00:00 2001 From: Rickylabs Date: Wed, 5 Aug 2026 02:04:11 +0200 Subject: [PATCH 01/12] test(e2e): retain Aspire restore diagnostics --- .../scripts/aspire-nuget-cache-policy.test.ts | 5 +++ .github/workflows/e2e-cli-prod.yml | 1 + .../context-pack.md | 7 +++ .../drift.md | 7 +++ .../plan.md | 44 +++++++++++++++++++ .../research.md | 30 +++++++++++++ .../supervisor.md | 9 ++++ .../worklog.md | 20 +++++++++ 8 files changed, 123 insertions(+) create mode 100644 .llm/runs/fix-aspire-restore-stability-1227--1227/context-pack.md create mode 100644 .llm/runs/fix-aspire-restore-stability-1227--1227/drift.md create mode 100644 .llm/runs/fix-aspire-restore-stability-1227--1227/plan.md create mode 100644 .llm/runs/fix-aspire-restore-stability-1227--1227/research.md create mode 100644 .llm/runs/fix-aspire-restore-stability-1227--1227/supervisor.md create mode 100644 .llm/runs/fix-aspire-restore-stability-1227--1227/worklog.md diff --git a/.github/scripts/aspire-nuget-cache-policy.test.ts b/.github/scripts/aspire-nuget-cache-policy.test.ts index 514ab1df8..8d81a6cbd 100644 --- a/.github/scripts/aspire-nuget-cache-policy.test.ts +++ b/.github/scripts/aspire-nuget-cache-policy.test.ts @@ -23,6 +23,11 @@ Deno.test('every Aspire runtime workflow persists the exact pinned NuGet package } }); +Deno.test('published E2E artifacts retain Aspire CLI diagnostics', async () => { + const workflow = await Deno.readTextFile('.github/workflows/e2e-cli-prod.yml'); + assertStringIncludes(workflow, '~/.aspire/logs/cli_*.log'); +}); + function count(source: string, needle: string): number { return source.split(needle).length - 1; } diff --git a/.github/workflows/e2e-cli-prod.yml b/.github/workflows/e2e-cli-prod.yml index 3a62c7707..fbcc60466 100644 --- a/.github/workflows/e2e-cli-prod.yml +++ b/.github/workflows/e2e-cli-prod.yml @@ -153,4 +153,5 @@ jobs: .llm/tmp/cli-e2e-prod.ndjson .llm/tmp/quickstart-walk-prod-report.json .llm/tmp/quickstart-walk-prod.ndjson + ~/.aspire/logs/cli_*.log if-no-files-found: ignore diff --git a/.llm/runs/fix-aspire-restore-stability-1227--1227/context-pack.md b/.llm/runs/fix-aspire-restore-stability-1227--1227/context-pack.md new file mode 100644 index 000000000..ecb6df88f --- /dev/null +++ b/.llm/runs/fix-aspire-restore-stability-1227--1227/context-pack.md @@ -0,0 +1,7 @@ +# Context pack — #1227 reopened restore stability + +PR #1297 fixed the 30-minute failure budget but not the intermittent restore cancellation. +Run 30961102523 failed canary.10 quickstart restore after 180.1s, named an Aspire CLI log, and then +discarded it. Current slice captures that log first. Do not implement broad retry/cache guesses; +read the cloud log, then lock the exact predicate, package paths, and consecutive-run count. + diff --git a/.llm/runs/fix-aspire-restore-stability-1227--1227/drift.md b/.llm/runs/fix-aspire-restore-stability-1227--1227/drift.md new file mode 100644 index 000000000..2b12c7cdb --- /dev/null +++ b/.llm/runs/fix-aspire-restore-stability-1227--1227/drift.md @@ -0,0 +1,7 @@ +# Drift — #1227 reopened restore stability + +- Owner D6 ruling replaces the default local PLAN-EVAL with composed draft→ready augmentation, + OpenHands label, and orchestrator pre-merge gate. +- Issue body retains the original three checked boxes; the owner reopening comment supplies four + new unchecked acceptance rows and is authoritative for this continuation. + diff --git a/.llm/runs/fix-aspire-restore-stability-1227--1227/plan.md b/.llm/runs/fix-aspire-restore-stability-1227--1227/plan.md new file mode 100644 index 000000000..6797d0f7b --- /dev/null +++ b/.llm/runs/fix-aspire-restore-stability-1227--1227/plan.md @@ -0,0 +1,44 @@ +# Plan — #1227 reopened restore stability + +## Profile and gates + +- Surface: Archetype 6 CLI/tooling E2E and release workflow. +- Required: focused unit/policy tests, scoped check/lint/fmt, workflow YAML parse, live branch + diagnostic run, then N consecutive published-canary `quickstart.walk` runs. +- Release class: required because the published CLI quickstart gate changes. +- JSR audit: N/A; no package public export is planned. + +## Locked decisions + +1. Preserve PR #1297's bounded timeout and infrastructure classification. +2. Land diagnostic artifact capture before changing retry/cache behavior; diagnosis controls scope. +3. Retry only the exact observed restore failure class, with a finite total budget. +4. Cache exact pinned inputs and assert cache population, not merely cache-step syntax. +5. A single green run is not completion evidence. + +## Commit slices + +1. Diagnostic capture — upload Aspire CLI logs on every production E2E outcome; policy test; branch + workflow run supplies the log. +2. Root-cause mitigation — signature-specific retry and exact cache correction chosen from slice 1 + evidence; focused negative/positive tests. +3. Repeated proof — N consecutive published-canary walks green; final evidence and ready handoff. + +## Risk register + +- Artifact paths may not expand `~`: workflow policy and live artifact inspection prove them. +- Broad retries could mask product failures: retry predicate is exact and unit-tested negative-first. +- Cache could be present but irrelevant: diagnostic logs and explicit package-presence assertions + bind it to the restore operation. +- Cloud flake may not reproduce immediately: keep diagnostic upload always-on and do not claim + completion without the consecutive-run bar. + +## Open-decision sweep + +- Exact retry signature/package paths/N: must resolve from the diagnostic run before slice 2. +- Log retention duration: safe to defer to Actions defaults. + +## Deferred scope + +- Aspire CLI upstream repair; this lane makes the NetScript quickstart resilient and diagnosable. + diff --git a/.llm/runs/fix-aspire-restore-stability-1227--1227/research.md b/.llm/runs/fix-aspire-restore-stability-1227--1227/research.md new file mode 100644 index 000000000..4616179a7 --- /dev/null +++ b/.llm/runs/fix-aspire-restore-stability-1227--1227/research.md @@ -0,0 +1,30 @@ +# Research — #1227 reopened restore stability + +## Re-baseline + +PR #1297 merged the bounded-failure half: `runtime.aspire-restore` now has three 180-second +attempts, infrastructure classification, and an exact Aspire 13.4.6 NuGet cache key. Preserve it. + +## Findings + +1. Issue owner evidence has two consecutive `quickstart.walk` runs against canary.10: one passed + Aspire restore/start in 22.3s; one failed restore after 180.1s with exit 6 and `A task was + canceled`. +2. Failed run 30961102523 names `/home/runner/.aspire/logs/cli_20260804T235623_1b62993a.log`, but + artifact 8913213616 contains only four suite report/log files. Root-cause evidence was discarded. +3. The failed report says `retried: false`: quickstart step 4 invokes `runBoundedAspireWalk` once, + and that helper invokes restore once. +4. The workflow cache step covers `~/.nuget/packages`, but its instant success does not prove which + Aspire SDK packages were present. The CLI log must identify the actual restore path before the + cache policy changes. +5. The same workflow's preceding `scaffold.runtime` run also reported a prebuilt AppHost preparation + failure, strengthening the hypothesis that the failure is below NetScript product code. + +## Open questions + +- Which operation is canceled in the Aspire CLI log: NuGet/feed, prebuilt AppHost preparation, + certificate setup, or another CLI operation? +- Which exact package/cache paths must be pinned to make the successful path independent of a cold + feed? +- What consecutive-run count is feasible and sufficient? Lock after observing the diagnostic run. + diff --git a/.llm/runs/fix-aspire-restore-stability-1227--1227/supervisor.md b/.llm/runs/fix-aspire-restore-stability-1227--1227/supervisor.md new file mode 100644 index 000000000..1b1eb7370 --- /dev/null +++ b/.llm/runs/fix-aspire-restore-stability-1227--1227/supervisor.md @@ -0,0 +1,9 @@ +# Supervisor — #1227 reopened restore stability + +- Route: openai · gpt-5.6-sol · medium +- Worktree: `/home/codex/repos/ns005-quickwins` +- Branch: `fix/aspire-restore-stability-1227` +- Baseline: `00f96af76e5825422e8bc716a9c27d4c13e16f7f` +- Harness ruling: D6 composed draft→ready evaluation; no local PLAN-EVAL +- Evaluator/merge authority: orchestrator pre-merge gate + diff --git a/.llm/runs/fix-aspire-restore-stability-1227--1227/worklog.md b/.llm/runs/fix-aspire-restore-stability-1227--1227/worklog.md new file mode 100644 index 000000000..03493407e --- /dev/null +++ b/.llm/runs/fix-aspire-restore-stability-1227--1227/worklog.md @@ -0,0 +1,20 @@ +# Worklog — #1227 reopened restore stability + +## Design + +- Public surface: no user-facing API; workflow artifact and existing `quickstart.walk` gate behavior. +- Domain vocabulary: restore cancellation signature, diagnostic log artifact, finite retry budget, + exact pinned package cache, consecutive-run proof. +- Ports: existing `AspireCommandRunner` remains the subprocess seam; GitHub Actions artifact/cache + actions remain workflow adapters. +- Constants: retry count/signature and exact Aspire version/cache keys must be named once. +- Commit slices: diagnostic capture → evidence-driven mitigation → repeated proof. +- Deferred: upstream Aspire fix. +- Contributor path: quickstart orchestration lives in `aspire-walk.ts`; workflow proof lives in + `e2e-cli-prod.yml`; policy tests mirror both contracts. + +## Progress + +- 2026-08-05: re-baselined issue comment, PR #1297, failed run 30961102523, and artifact 8913213616. +- D6 owner ruling composes evaluation; no local PLAN-EVAL was spawned. + From 2bd314ee2ae76202a441b079c4a09e6784480170 Mon Sep 17 00:00:00 2001 From: Rickylabs Date: Wed, 5 Aug 2026 02:09:48 +0200 Subject: [PATCH 02/12] fix(e2e): preseed and retry Aspire SDK restore --- .github/aspire-cache/AspireSdkCache.csproj | 12 ++++++ .../scripts/aspire-nuget-cache-policy.test.ts | 34 ++++++++++++--- .github/workflows/e2e-cli-prod.yml | 40 +++++++++++++++++- .../context-pack.md | 4 ++ .../drift.md | 4 +- .../plan.md | 9 +++- .../research.md | 9 ++++ .../worklog.md | 5 ++- .../gates/quickstart/aspire-walk.ts | 41 ++++++++++++++++++- .../gates/quickstart-aspire-walk_test.ts | 34 +++++++++++++++ 10 files changed, 181 insertions(+), 11 deletions(-) create mode 100644 .github/aspire-cache/AspireSdkCache.csproj diff --git a/.github/aspire-cache/AspireSdkCache.csproj b/.github/aspire-cache/AspireSdkCache.csproj new file mode 100644 index 000000000..670f91897 --- /dev/null +++ b/.github/aspire-cache/AspireSdkCache.csproj @@ -0,0 +1,12 @@ + + + net10.0 + + + + + + + + + diff --git a/.github/scripts/aspire-nuget-cache-policy.test.ts b/.github/scripts/aspire-nuget-cache-policy.test.ts index 8d81a6cbd..7a9592a13 100644 --- a/.github/scripts/aspire-nuget-cache-policy.test.ts +++ b/.github/scripts/aspire-nuget-cache-policy.test.ts @@ -3,26 +3,48 @@ import { assertEquals, assertStringIncludes } from '@std/assert'; const POLICY = { action: 'uses: actions/cache@v4', path: 'path: ~/.nuget/packages', - key: 'key: nuget-aspire-${{ runner.os }}-13.4.6-v1', } as const; Deno.test('every Aspire runtime workflow persists the exact pinned NuGet package train', async () => { for ( - const [path, expectedCaches] of [ - ['.github/workflows/e2e-cli.yml', 2], - ['.github/workflows/e2e-cli-prod.yml', 1], - ['.github/workflows/e2e-cli-prod-local.yml', 1], + const [path, expectedCaches, key] of [ + ['.github/workflows/e2e-cli.yml', 2, 'key: nuget-aspire-${{ runner.os }}-13.4.6-v1'], + ['.github/workflows/e2e-cli-prod.yml', 2, 'key: nuget-aspire-${{ runner.os }}-13.4.6-v2'], + [ + '.github/workflows/e2e-cli-prod-local.yml', + 1, + 'key: nuget-aspire-${{ runner.os }}-13.4.6-v1', + ], ] as const ) { const source = await Deno.readTextFile(path); assertEquals(count(source, POLICY.action), expectedCaches, path); assertEquals(count(source, POLICY.path), expectedCaches, path); - assertEquals(count(source, POLICY.key), expectedCaches, path); + assertEquals(count(source, key), expectedCaches, path); assertStringIncludes(source, 'ASPIRE_CLI_VERSION:'); assertStringIncludes(source, '13.4.6'); } }); +Deno.test('production E2E seeds and verifies every exact Aspire integration package before runtime', async () => { + const workflow = await Deno.readTextFile('.github/workflows/e2e-cli-prod.yml'); + const project = await Deno.readTextFile('.github/aspire-cache/AspireSdkCache.csproj'); + for ( + const [name, version] of [ + ['Aspire.Hosting', '13.4.6'], + ['Aspire.Hosting.PostgreSQL', '13.4.6'], + ['Aspire.Hosting.Redis', '13.4.6'], + ['Aspire.Hosting.Browsers', '13.4.6-preview.1.26319.6'], + ['Aspire.Hosting.CodeGeneration.TypeScript', '13.4.6'], + ] as const + ) { + assertStringIncludes(project, `Include="${name}" Version="${version}"`); + assertStringIncludes(workflow, name.toLowerCase()); + } + assertStringIncludes(workflow, 'needs: prepare-aspire-sdk-cache'); + assertStringIncludes(workflow, "steps.aspire-sdk-cache.outputs.cache-hit != 'true'"); +}); + Deno.test('published E2E artifacts retain Aspire CLI diagnostics', async () => { const workflow = await Deno.readTextFile('.github/workflows/e2e-cli-prod.yml'); assertStringIncludes(workflow, '~/.aspire/logs/cli_*.log'); diff --git a/.github/workflows/e2e-cli-prod.yml b/.github/workflows/e2e-cli-prod.yml index fbcc60466..754e93f2c 100644 --- a/.github/workflows/e2e-cli-prod.yml +++ b/.github/workflows/e2e-cli-prod.yml @@ -20,8 +20,46 @@ concurrency: cancel-in-progress: false jobs: + prepare-aspire-sdk-cache: + name: prepare pinned Aspire SDK cache + if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }} + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Setup .NET + uses: actions/setup-dotnet@v5 + with: + dotnet-version: '10.0.x' + + - name: Restore pinned Aspire SDK cache + id: aspire-sdk-cache + uses: actions/cache@v4 + with: + path: ~/.nuget/packages + key: nuget-aspire-${{ runner.os }}-13.4.6-v2 + + - name: Populate exact Aspire SDK packages + if: steps.aspire-sdk-cache.outputs.cache-hit != 'true' + run: >- + dotnet restore .github/aspire-cache/AspireSdkCache.csproj + --packages "$HOME/.nuget/packages" + --source https://api.nuget.org/v3/index.json + --source https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet9/nuget/v3/index.json + + - name: Verify exact Aspire SDK packages + run: | + test -f "$HOME/.nuget/packages/aspire.hosting/13.4.6/aspire.hosting.13.4.6.nupkg" + test -f "$HOME/.nuget/packages/aspire.hosting.postgresql/13.4.6/aspire.hosting.postgresql.13.4.6.nupkg" + test -f "$HOME/.nuget/packages/aspire.hosting.redis/13.4.6/aspire.hosting.redis.13.4.6.nupkg" + test -f "$HOME/.nuget/packages/aspire.hosting.browsers/13.4.6-preview.1.26319.6/aspire.hosting.browsers.13.4.6-preview.1.26319.6.nupkg" + test -f "$HOME/.nuget/packages/aspire.hosting.codegeneration.typescript/13.4.6/aspire.hosting.codegeneration.typescript.13.4.6.nupkg" + scaffold-runtime-jsr: name: scaffold-runtime (published JSR CLI) + needs: prepare-aspire-sdk-cache if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }} runs-on: ubuntu-latest timeout-minutes: 75 @@ -43,7 +81,7 @@ jobs: uses: actions/cache@v4 with: path: ~/.nuget/packages - key: nuget-aspire-${{ runner.os }}-13.4.6-v1 + key: nuget-aspire-${{ runner.os }}-13.4.6-v2 - name: Install Aspire CLI env: diff --git a/.llm/runs/fix-aspire-restore-stability-1227--1227/context-pack.md b/.llm/runs/fix-aspire-restore-stability-1227--1227/context-pack.md index ecb6df88f..11f0b4914 100644 --- a/.llm/runs/fix-aspire-restore-stability-1227--1227/context-pack.md +++ b/.llm/runs/fix-aspire-restore-stability-1227--1227/context-pack.md @@ -5,3 +5,7 @@ Run 30961102523 failed canary.10 quickstart restore after 180.1s, named an Aspir discarded it. Current slice captures that log first. Do not implement broad retry/cache guesses; read the cloud log, then lock the exact predicate, package paths, and consecutive-run count. +Identical retained local logs prove the operation is Aspire's bundled NuGet restore of five exact +integration packages. The v1 cache missed on both cloud runs and could not save from a red job. S2 +uses a prerequisite seed/save job under v2, verifies every package, and retries only the exact exit-6 +two-marker signature. Completion requires three consecutive green branch runs. diff --git a/.llm/runs/fix-aspire-restore-stability-1227--1227/drift.md b/.llm/runs/fix-aspire-restore-stability-1227--1227/drift.md index 2b12c7cdb..749014b7f 100644 --- a/.llm/runs/fix-aspire-restore-stability-1227--1227/drift.md +++ b/.llm/runs/fix-aspire-restore-stability-1227--1227/drift.md @@ -4,4 +4,6 @@ OpenHands label, and orchestrator pre-merge gate. - Issue body retains the original three checked boxes; the owner reopening comment supplies four new unchecked acceptance rows and is authoritative for this continuation. - +- The cloud diagnostic run was still in progress when identical retained local logs closed the + root-cause decision. Implementation proceeded from those logs; the branch artifact remains a + required corroboration before ready state. diff --git a/.llm/runs/fix-aspire-restore-stability-1227--1227/plan.md b/.llm/runs/fix-aspire-restore-stability-1227--1227/plan.md index 6797d0f7b..79c2865a8 100644 --- a/.llm/runs/fix-aspire-restore-stability-1227--1227/plan.md +++ b/.llm/runs/fix-aspire-restore-stability-1227--1227/plan.md @@ -35,10 +35,17 @@ ## Open-decision sweep -- Exact retry signature/package paths/N: must resolve from the diagnostic run before slice 2. +- Consecutive-run count: three; enough to reject the observed alternating/lucky-single-pass pattern + while keeping the p0 proof bounded. - Log retention duration: safe to defer to Actions defaults. ## Deferred scope - Aspire CLI upstream repair; this lane makes the NetScript quickstart resilient and diagnosable. +## Slice 2 decision + +- Cache preparation is a separate prerequisite job so its post-save completes before product E2E + begins. It restores and verifies all five exact packages under a new v2 key. +- Quickstart restore retries once only when exit code is 6 and stderr contains both observed Aspire + preparation-cancellation markers. Timeouts, product exit codes, and partial matches do not retry. diff --git a/.llm/runs/fix-aspire-restore-stability-1227--1227/research.md b/.llm/runs/fix-aspire-restore-stability-1227--1227/research.md index 4616179a7..d9bd88d57 100644 --- a/.llm/runs/fix-aspire-restore-stability-1227--1227/research.md +++ b/.llm/runs/fix-aspire-restore-stability-1227--1227/research.md @@ -28,3 +28,12 @@ attempts, infrastructure classification, and an exact Aspire 13.4.6 NuGet cache feed? - What consecutive-run count is feasible and sufficient? Lock after observing the diagnostic run. +## Root-cause evidence + +The two retained local Aspire logs for the byte-identical signature +(`cli_20260804T092654_7dc37fe7.log` and `cli_20260804T094300_8a06ef75.log`) both stop inside +`BundleNuGetService.RestorePackagesAsync`. The command is the bundled `aspire-managed nuget restore` +for five exact packages: Hosting, PostgreSQL, Redis, Browsers preview, and TypeScript codegen. No +container operation has begun. Runs 30959430176 and 30961102523 both report a cache miss for the v1 +key; because each overall job failed, the cache post-save never established that key. Classification: +NuGet/feed latency amplified by a cache lifecycle defect. diff --git a/.llm/runs/fix-aspire-restore-stability-1227--1227/worklog.md b/.llm/runs/fix-aspire-restore-stability-1227--1227/worklog.md index 03493407e..e7ac5a052 100644 --- a/.llm/runs/fix-aspire-restore-stability-1227--1227/worklog.md +++ b/.llm/runs/fix-aspire-restore-stability-1227--1227/worklog.md @@ -17,4 +17,7 @@ - 2026-08-05: re-baselined issue comment, PR #1297, failed run 30961102523, and artifact 8913213616. - D6 owner ruling composes evaluation; no local PLAN-EVAL was spawned. - +- 2026-08-05: local logs for the same signature identify bundled NuGet restore, before containers. + Both cloud evidence runs missed the v1 cache and ended before Actions could save it. +- 2026-08-05: S2 adds a prerequisite cache-population job with five exact package assertions and a + two-attempt predicate restricted to exit 6 plus both preparation-cancellation markers. diff --git a/packages/cli/e2e/src/application/gates/quickstart/aspire-walk.ts b/packages/cli/e2e/src/application/gates/quickstart/aspire-walk.ts index 25d25c8a1..d02a5ed0c 100644 --- a/packages/cli/e2e/src/application/gates/quickstart/aspire-walk.ts +++ b/packages/cli/e2e/src/application/gates/quickstart/aspire-walk.ts @@ -5,6 +5,14 @@ export const ASPIRE_TIMEOUT_CLASSIFICATION = { WAIT: 'quickstart.aspire.wait.timeout:#1227', } as const; +/** Maximum attempts for the observed Aspire bundled-NuGet cancellation. */ +export const ASPIRE_RESTORE_MAX_ATTEMPTS = 2; + +const ASPIRE_RESTORE_CANCELED_MARKERS = [ + 'Failed to prepare: A task was canceled.', + 'Failed to prepare AppHost server.', +] as const; + /** Minimal subprocess result consumed by the bounded Aspire walk. */ export interface AspireCommandResult { readonly code: number; @@ -28,7 +36,7 @@ export async function runBoundedAspireWalk( run: AspireCommandRunner = runAspireCommand, ): Promise { const aspireRoot = `${projectRoot}/aspire`; - await requireAspireSuccess( + await requireAspireRestoreSuccess( ['aspire', 'restore', '--apphost', appHost, '--non-interactive', '--nologo'], aspireRoot, timeoutMs, @@ -63,6 +71,37 @@ export async function runBoundedAspireWalk( ); } +/** Identifies the exit-6 failure emitted by Aspire's bundled NuGet restore. */ +export function isRetryableAspireRestoreCancellation(result: AspireCommandResult): boolean { + return result.code === 6 && + ASPIRE_RESTORE_CANCELED_MARKERS.every((marker) => result.stderr.includes(marker)); +} + +async function requireAspireRestoreSuccess( + command: readonly string[], + cwd: string, + timeoutMs: number, + timeoutClassification: string, + run: AspireCommandRunner, +): Promise { + for (let attempt = 1; attempt <= ASPIRE_RESTORE_MAX_ATTEMPTS; attempt++) { + const result = await run(command, cwd, timeoutMs); + if (result.timedOut) throw new Error(timeoutClassification); + if (result.code === 0) { + if (result.stdout) console.info(result.stdout); + return; + } + if (!isRetryableAspireRestoreCancellation(result) || attempt === ASPIRE_RESTORE_MAX_ATTEMPTS) { + throw new Error(`${command[0]} ${command[1]} failed (${result.code}): ${result.stderr}`); + } + console.warn( + `Aspire bundled NuGet restore canceled; retrying (${ + attempt + 1 + }/${ASPIRE_RESTORE_MAX_ATTEMPTS}).`, + ); + } +} + async function requireAspireSuccess( command: readonly string[], cwd: string, diff --git a/packages/cli/e2e/tests/application/gates/quickstart-aspire-walk_test.ts b/packages/cli/e2e/tests/application/gates/quickstart-aspire-walk_test.ts index 774149971..cecb7013d 100644 --- a/packages/cli/e2e/tests/application/gates/quickstart-aspire-walk_test.ts +++ b/packages/cli/e2e/tests/application/gates/quickstart-aspire-walk_test.ts @@ -1,7 +1,9 @@ import { assertEquals, assertRejects } from '@std/assert'; import { + ASPIRE_RESTORE_MAX_ATTEMPTS, ASPIRE_TIMEOUT_CLASSIFICATION, type AspireCommandResult, + isRetryableAspireRestoreCancellation, runBoundedAspireWalk, } from '../../../src/application/gates/quickstart/aspire-walk.ts'; @@ -51,3 +53,35 @@ Deno.test('bounded Aspire walk classifies start timeout independently', async () ); assertEquals(calls, 2); }); + +Deno.test('bounded Aspire walk retries only the observed exit-6 restore cancellation', async () => { + let calls = 0; + await runBoundedAspireWalk('/project/aspire/apphost.mts', '/project', 42_000, (command) => { + calls++; + if (command[1] === 'restore' && calls === 1) { + return Promise.resolve({ + ...PASS, + code: 6, + stderr: 'Failed to prepare: A task was canceled.\nFailed to prepare AppHost server.', + }); + } + return Promise.resolve(PASS); + }); + assertEquals(ASPIRE_RESTORE_MAX_ATTEMPTS, 2); + assertEquals(calls, 4); +}); + +Deno.test('Aspire restore retry predicate rejects product and partial failures', () => { + assertEquals( + isRetryableAspireRestoreCancellation({ ...PASS, code: 1, stderr: 'product red' }), + false, + ); + assertEquals( + isRetryableAspireRestoreCancellation({ + ...PASS, + code: 6, + stderr: 'Failed to prepare: A task was canceled.', + }), + false, + ); +}); From cfce70af7185e20c9ff03d9a860b5f93ca0a09c2 Mon Sep 17 00:00:00 2001 From: Rickylabs Date: Wed, 5 Aug 2026 02:33:21 +0200 Subject: [PATCH 03/12] test(e2e): keep quickstart response probe service-scoped --- .../drift.md | 5 +++++ .../worklog.md | 3 +++ .../src/application/gates/scaffold/runtime-gates.ts | 3 ++- .../e2e/suites/quickstart/quickstart-walk-suite.ts | 1 - .../presentation/quickstart-walk-suite_test.ts | 13 +++++++++++++ 5 files changed, 23 insertions(+), 2 deletions(-) diff --git a/.llm/runs/fix-aspire-restore-stability-1227--1227/drift.md b/.llm/runs/fix-aspire-restore-stability-1227--1227/drift.md index 749014b7f..a53aadfeb 100644 --- a/.llm/runs/fix-aspire-restore-stability-1227--1227/drift.md +++ b/.llm/runs/fix-aspire-restore-stability-1227--1227/drift.md @@ -7,3 +7,8 @@ - The cloud diagnostic run was still in progress when identical retained local logs closed the root-cause decision. Implementation proceeded from those logs; the branch artifact remains a required corroboration before ready state. +- Proof run 30962998528 cleared restore in 22.58s from a confirmed v2 cache hit, then exposed that + Quickstart step 7 reused the runtime suite's database-aware health assertion. The generated users + service correctly returned healthy with no DB check, so the walk could not become green. The + suite-only call now requests service health while the runtime gate continues passing an explicit + database and retains its stronger assertion. diff --git a/.llm/runs/fix-aspire-restore-stability-1227--1227/worklog.md b/.llm/runs/fix-aspire-restore-stability-1227--1227/worklog.md index e7ac5a052..e333bc917 100644 --- a/.llm/runs/fix-aspire-restore-stability-1227--1227/worklog.md +++ b/.llm/runs/fix-aspire-restore-stability-1227--1227/worklog.md @@ -21,3 +21,6 @@ Both cloud evidence runs missed the v1 cache and ended before Actions could save it. - 2026-08-05: S2 adds a prerequisite cache-population job with five exact package assertions and a two-attempt predicate restricted to exit 6 plus both preparation-cancellation markers. +- 2026-08-05: proof run 30962998528: cache seed saved 170,799,714 bytes; dependent job restored a + 163 MB hit; Quickstart Aspire step passed in 22.58s. Later step 7 found a pre-existing assertion + mismatch, corrected without weakening `scaffold.runtime` database-health coverage. diff --git a/packages/cli/e2e/src/application/gates/scaffold/runtime-gates.ts b/packages/cli/e2e/src/application/gates/scaffold/runtime-gates.ts index 6ec4f6867..9e0615b0a 100644 --- a/packages/cli/e2e/src/application/gates/scaffold/runtime-gates.ts +++ b/packages/cli/e2e/src/application/gates/scaffold/runtime-gates.ts @@ -542,7 +542,7 @@ function databaseRuntimeResources( export const PROBE_SERVICE_HEALTH_SCRIPT = [ 'const appHost = Deno.args[0];', 'const resourceName = Deno.args[1] ?? "users";', - 'const database = Deno.args[2] ?? "postgres";', + 'const database = Deno.args[2];', 'if (!appHost) throw new Error("apphost argument is required");', 'const command = new Deno.Command("aspire", {', ' args: ["describe", "--apphost", appHost, "--format", "Json"],', @@ -586,6 +586,7 @@ export const PROBE_SERVICE_HEALTH_SCRIPT = [ ' if (!isRecord(health) || health.status !== "healthy" || !Array.isArray(health.checks)) {', ' return false;', ' }', + ' if (expectedDatabase === undefined) return true;', ' const databaseChecks = health.checks', ' .filter(isRecord)', ' .map((check) => check.name)', diff --git a/packages/cli/e2e/suites/quickstart/quickstart-walk-suite.ts b/packages/cli/e2e/suites/quickstart/quickstart-walk-suite.ts index bc59c2430..c95145929 100644 --- a/packages/cli/e2e/suites/quickstart/quickstart-walk-suite.ts +++ b/packages/cli/e2e/suites/quickstart/quickstart-walk-suite.ts @@ -192,7 +192,6 @@ function createQuickstartGates(): readonly GateDefinition[] { PROBE_SERVICE_HEALTH_SCRIPT, context.project.appHost, 'users', - 'postgres', ], ), ]; diff --git a/packages/cli/e2e/tests/presentation/quickstart-walk-suite_test.ts b/packages/cli/e2e/tests/presentation/quickstart-walk-suite_test.ts index 158e04027..4c5ff2807 100644 --- a/packages/cli/e2e/tests/presentation/quickstart-walk-suite_test.ts +++ b/packages/cli/e2e/tests/presentation/quickstart-walk-suite_test.ts @@ -55,6 +55,19 @@ Deno.test('quickstart project-check verdict runs the documented command exactly' ); }); +Deno.test('quickstart service-response verdict accepts service health without a database assertion', () => { + const suite = createQuickstartWalkSuite({ + packageSource: PACKAGE_SOURCE.JSR, + cliEntrypoint: EXACT_CLI, + }); + const response = command( + suite, + GATE.QUICKSTART_SERVICE_RESPONSE, + contextFor(suite.defaultOptions), + ); + assertEquals(response.slice(-2), ['/repo/.llm/tmp/my-app/aspire/apphost.mts', 'users']); +}); + Deno.test('quickstart commands reject a local CLI entrypoint', () => { const suite = createQuickstartWalkSuite({ packageSource: PACKAGE_SOURCE.LOCAL, From 02d55cefdd56666f7634b5b6c93a56d0f3000834 Mon Sep 17 00:00:00 2001 From: Rickylabs Date: Wed, 5 Aug 2026 03:04:39 +0200 Subject: [PATCH 04/12] fix(e2e): bound database AppHost restore retries --- .../worklog.md | 10 ++ .../gates/quickstart/database-walk.ts | 117 ++++++++++++++++++ .../quickstart/quickstart-walk-suite.ts | 24 ++-- .../gates/quickstart-database-walk_test.ts | 81 ++++++++++++ 4 files changed, 216 insertions(+), 16 deletions(-) create mode 100644 packages/cli/e2e/src/application/gates/quickstart/database-walk.ts create mode 100644 packages/cli/e2e/tests/application/gates/quickstart-database-walk_test.ts diff --git a/.llm/runs/fix-aspire-restore-stability-1227--1227/worklog.md b/.llm/runs/fix-aspire-restore-stability-1227--1227/worklog.md index e333bc917..45d8cb69c 100644 --- a/.llm/runs/fix-aspire-restore-stability-1227--1227/worklog.md +++ b/.llm/runs/fix-aspire-restore-stability-1227--1227/worklog.md @@ -24,3 +24,13 @@ - 2026-08-05: proof run 30962998528: cache seed saved 170,799,714 bytes; dependent job restored a 163 MB hit; Quickstart Aspire step passed in 22.58s. Later step 7 found a pre-existing assertion mismatch, corrected without weakening `scaffold.runtime` database-health coverage. +- 2026-08-05: final-head proof run 30963671830 passed all seven Quickstart steps; Aspire restore + and start completed in 19.87s (consecutive proof 1/3). +- 2026-08-05: run 30964226683 reset the proof count. Step 4 remained green in 21.36s, but the + database operation AppHost then spent 300s in `BundleNuGetService` restoring the same four + packages before `aspire start` exit 2. The captured child log proves this is the same upstream + restore class at a second Quickstart AppHost boundary, before any database product operation. +- 2026-08-05: S4 replaces the database gate's unbounded inline sequence with a 120s-per-command + wrapper. It sets Aspire's internal start bound to 90s and retries only the two production restore + surfaces (exit 6 cancellation with both preparation markers, or exit 2 start timeout with the + AppHost-log marker). Product failures remain single-attempt and distinctly classified. diff --git a/packages/cli/e2e/src/application/gates/quickstart/database-walk.ts b/packages/cli/e2e/src/application/gates/quickstart/database-walk.ts new file mode 100644 index 000000000..5ef397549 --- /dev/null +++ b/packages/cli/e2e/src/application/gates/quickstart/database-walk.ts @@ -0,0 +1,117 @@ +const DB_APPHOST_START_TIMEOUT_SECONDS = '90'; +const DB_COMMAND_TIMEOUT_MS = 120_000; +export const DB_RESTORE_MAX_ATTEMPTS = 2; + +const RESTORE_CANCELED_MARKERS = [ + 'Failed to prepare: A task was canceled', + 'Failed to prepare AppHost server', +] as const; +const RESTORE_START_TIMEOUT_MARKERS = [ + 'Timed out waiting ', + ' for AppHost to start', + 'See AppHost logs at', +] as const; + +export interface DatabaseCommandResult { + readonly code: number; + readonly stdout: string; + readonly stderr: string; + readonly timedOut: boolean; +} + +export type DatabaseCommandRunner = ( + command: readonly string[], + cwd: string, + timeoutMs: number, +) => Promise; + +/** Whether a database CLI invocation failed in Aspire's bundled-NuGet preparation path. */ +export function isRetryableDatabaseRestoreFailure(result: DatabaseCommandResult): boolean { + const output = `${result.stdout}\n${result.stderr}`; + return (result.code === 6 && + RESTORE_CANCELED_MARKERS.every((marker) => output.includes(marker))) || + (result.code === 2 && + RESTORE_START_TIMEOUT_MARKERS.every((marker) => output.includes(marker))); +} + +/** Run the three documented database commands, retrying only classified Aspire restore failures. */ +export async function runBoundedDatabaseWalk( + cliSpecifier: string, + projectRoot: string, + run: DatabaseCommandRunner = runDatabaseCommand, +): Promise { + const commands = [ + ['db', 'init', '--project-root', '.', '--db', 'postgres', '--name', 'init'], + ['db', 'generate', '--project-root', '.', '--db', 'postgres'], + ['db', 'seed', '--project-root', '.', '--db', 'postgres'], + ] as const; + + for (const args of commands) { + const command = [ + 'deno', + 'run', + '-A', + '--minimum-dependency-age=0', + cliSpecifier, + ...args, + ]; + for (let attempt = 1; attempt <= DB_RESTORE_MAX_ATTEMPTS; attempt++) { + const result = await run(command, projectRoot, DB_COMMAND_TIMEOUT_MS); + if (result.stdout) console.info(result.stdout); + if (result.code === 0) break; + if (!isRetryableDatabaseRestoreFailure(result) || attempt === DB_RESTORE_MAX_ATTEMPTS) { + const classification = isRetryableDatabaseRestoreFailure(result) + ? 'quickstart.database.aspire-restore.infrastructure:#1227' + : 'quickstart.database.product-failure'; + throw new Error(`${classification}: ${args[1]} failed (${result.code}): ${result.stderr}`); + } + console.warn( + `Aspire bundled NuGet restore failed during db ${args[1]}; retrying (${ + attempt + 1 + }/${DB_RESTORE_MAX_ATTEMPTS}).`, + ); + } + } +} + +async function runDatabaseCommand( + command: readonly string[], + cwd: string, + timeoutMs: number, +): Promise { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), timeoutMs); + try { + const [executable, ...args] = command; + const output = await new Deno.Command(executable, { + args, + cwd, + env: { ASPIRE_CLI_START_TIMEOUT: DB_APPHOST_START_TIMEOUT_SECONDS }, + stdout: 'piped', + stderr: 'piped', + signal: controller.signal, + }).output(); + return { + code: output.code, + stdout: new TextDecoder().decode(output.stdout), + stderr: new TextDecoder().decode(output.stderr), + timedOut: false, + }; + } catch (error) { + if (!(error instanceof DOMException) || error.name !== 'AbortError') throw error; + return { + code: 124, + stdout: '', + stderr: 'Command exceeded its 120s outer bound.', + timedOut: true, + }; + } finally { + clearTimeout(timeout); + } +} + +if (import.meta.main) { + const [cliSpecifier, projectRoot] = Deno.args; + if (!cliSpecifier || !projectRoot) throw new Error('cliSpecifier and projectRoot are required'); + await runBoundedDatabaseWalk(cliSpecifier, projectRoot); +} diff --git a/packages/cli/e2e/suites/quickstart/quickstart-walk-suite.ts b/packages/cli/e2e/suites/quickstart/quickstart-walk-suite.ts index c95145929..e0d03298d 100644 --- a/packages/cli/e2e/suites/quickstart/quickstart-walk-suite.ts +++ b/packages/cli/e2e/suites/quickstart/quickstart-walk-suite.ts @@ -156,22 +156,14 @@ function createQuickstartGates(): readonly GateDefinition[] { GATE_PHASE.DATABASE, (context) => [ 'deno', - 'eval', - sequenceScript([ - [...cli( - context, - 'db', - 'init', - '--project-root', - '.', - '--db', - 'postgres', - '--name', - 'init', - )], - [...cli(context, 'db', 'generate', '--project-root', '.', '--db', 'postgres')], - [...cli(context, 'db', 'seed', '--project-root', '.', '--db', 'postgres')], - ]), + 'run', + '--allow-run=deno', + resolve( + context.project.repoRoot, + 'packages/cli/e2e/src/application/gates/quickstart/database-walk.ts', + ), + requirePublishedCli(context), + context.project.projectRoot, ], (context) => context.project.projectRoot, ), diff --git a/packages/cli/e2e/tests/application/gates/quickstart-database-walk_test.ts b/packages/cli/e2e/tests/application/gates/quickstart-database-walk_test.ts new file mode 100644 index 000000000..a708e93be --- /dev/null +++ b/packages/cli/e2e/tests/application/gates/quickstart-database-walk_test.ts @@ -0,0 +1,81 @@ +import { assertEquals, assertRejects } from '@std/assert'; +import { + type DatabaseCommandResult, + isRetryableDatabaseRestoreFailure, + runBoundedDatabaseWalk, +} from '../../../src/application/gates/quickstart/database-walk.ts'; + +const PASS: DatabaseCommandResult = { code: 0, stdout: '', stderr: '', timedOut: false }; + +Deno.test('database walk retries only the command whose Aspire restore was canceled', async () => { + const calls: string[][] = []; + let generateAttempts = 0; + await runBoundedDatabaseWalk('jsr:@netscript/cli@0.0.5-canary.10', '/project', (command) => { + calls.push([...command]); + if (command.includes('generate') && generateAttempts++ === 0) { + return Promise.resolve({ + code: 6, + stdout: '', + stderr: 'Failed to prepare: A task was canceled.\nFailed to prepare AppHost server.', + timedOut: false, + }); + } + return Promise.resolve(PASS); + }); + + assertEquals(calls.map((command) => command[command.indexOf('db') + 1]), [ + 'init', + 'generate', + 'generate', + 'seed', + ]); +}); + +Deno.test('database walk retries the observed Aspire start timeout after bundled restore stalls', async () => { + let attempts = 0; + await runBoundedDatabaseWalk('jsr:@netscript/cli@0.0.5-canary.10', '/project', () => { + attempts += 1; + return Promise.resolve( + attempts === 1 + ? { + code: 2, + stdout: '', + stderr: 'Timed out waiting 90s for AppHost to start.\nSee AppHost logs at /tmp/child.log', + timedOut: false, + } + : PASS, + ); + }); + assertEquals(attempts, 4); +}); + +Deno.test('database walk does not retry a product failure', async () => { + let attempts = 0; + await assertRejects( + () => + runBoundedDatabaseWalk('jsr:@netscript/cli@0.0.5-canary.10', '/project', () => { + attempts += 1; + return Promise.resolve({ + code: 1, + stdout: '', + stderr: 'Prisma schema validation failed.', + timedOut: false, + }); + }), + Error, + 'quickstart.database.product-failure', + ); + assertEquals(attempts, 1); +}); + +Deno.test('database restore classifier requires a complete production signature', () => { + assertEquals(isRetryableDatabaseRestoreFailure(PASS), false); + assertEquals( + isRetryableDatabaseRestoreFailure({ + ...PASS, + code: 2, + stderr: 'Timed out waiting 90s for AppHost to start.', + }), + false, + ); +}); From 82d89f4d0d2e42eab0d942584d82949c996b68b1 Mon Sep 17 00:00:00 2001 From: Rickylabs Date: Wed, 5 Aug 2026 03:25:26 +0200 Subject: [PATCH 05/12] fix(e2e): hydrate Aspire integration cache offline --- .../scripts/aspire-nuget-cache-policy.test.ts | 3 + .github/workflows/e2e-cli-prod.yml | 9 +++ .../worklog.md | 9 +++ .../gates/quickstart/aspire-walk.ts | 6 +- .../gates/quickstart/database-walk.ts | 9 ++- .../hydrate-aspire-package-cache.ts | 73 +++++++++++++++++++ .../quickstart/quickstart-walk-suite.ts | 8 +- .../quickstart-aspire-package-cache_test.ts | 18 +++++ .../quickstart-walk-suite_test.ts | 14 ++++ 9 files changed, 144 insertions(+), 5 deletions(-) create mode 100644 packages/cli/e2e/src/application/gates/quickstart/hydrate-aspire-package-cache.ts create mode 100644 packages/cli/e2e/tests/application/gates/quickstart-aspire-package-cache_test.ts diff --git a/.github/scripts/aspire-nuget-cache-policy.test.ts b/.github/scripts/aspire-nuget-cache-policy.test.ts index 7a9592a13..ab2d0477f 100644 --- a/.github/scripts/aspire-nuget-cache-policy.test.ts +++ b/.github/scripts/aspire-nuget-cache-policy.test.ts @@ -43,6 +43,9 @@ Deno.test('production E2E seeds and verifies every exact Aspire integration pack } assertStringIncludes(workflow, 'needs: prepare-aspire-sdk-cache'); assertStringIncludes(workflow, "steps.aspire-sdk-cache.outputs.cache-hit != 'true'"); + assertStringIncludes(workflow, 'Materialize pinned Aspire local package source'); + assertStringIncludes(workflow, "-name '*.nupkg'"); + assertStringIncludes(workflow, '-name aspire-managed'); }); Deno.test('published E2E artifacts retain Aspire CLI diagnostics', async () => { diff --git a/.github/workflows/e2e-cli-prod.yml b/.github/workflows/e2e-cli-prod.yml index 754e93f2c..482272761 100644 --- a/.github/workflows/e2e-cli-prod.yml +++ b/.github/workflows/e2e-cli-prod.yml @@ -90,6 +90,15 @@ jobs: dotnet tool install Aspire.Cli --tool-path "$HOME/.aspire/bin" --version "$ASPIRE_CLI_VERSION" echo "$HOME/.aspire/bin" >> "$GITHUB_PATH" + - name: Materialize pinned Aspire local package source + run: | + mkdir -p .llm/tmp/aspire-nuget-source + find "$HOME/.nuget/packages" -name '*.nupkg' \ + -exec cp --update=none '{}' .llm/tmp/aspire-nuget-source/ \; + find "$HOME/.aspire/bin" -type f -name aspire-managed -print -quit \ + > .llm/tmp/aspire-managed-path.txt + test -s .llm/tmp/aspire-managed-path.txt + - name: Download published version if: github.event_name == 'workflow_run' uses: actions/download-artifact@v5 diff --git a/.llm/runs/fix-aspire-restore-stability-1227--1227/worklog.md b/.llm/runs/fix-aspire-restore-stability-1227--1227/worklog.md index 45d8cb69c..1f5898dfd 100644 --- a/.llm/runs/fix-aspire-restore-stability-1227--1227/worklog.md +++ b/.llm/runs/fix-aspire-restore-stability-1227--1227/worklog.md @@ -34,3 +34,12 @@ wrapper. It sets Aspire's internal start bound to 90s and retries only the two production restore surfaces (exit 6 cancellation with both preparation markers, or exit 2 start timeout with the AppHost-log marker). Product failures remain single-attempt and distinctly classified. +- 2026-08-05: proof run 30965320792 failed step 4 after two clean 180s attempts; both logs show the + five-package bundled restore stalled despite a hit in `~/.nuget/packages`. This disproves the + assumption that Aspire consumes the ordinary NuGet global cache on its TypeScript integration + path. No proof credit earned; streak remains 0/3. +- 2026-08-05: S5 materializes cached `.nupkg` files as a local source, then invokes Aspire's own + pinned `aspire-managed` restore into the two exact project-local cache identities observed in + production (five-package `20B4B80F832F59C1`, DB four-package `F7BD251A60347D74`). A local cold + fixture hydrated from that source and `aspire restore` then completed in 7.10s without a network + NuGet restore. The classified retry remains as a bounded fallback. diff --git a/packages/cli/e2e/src/application/gates/quickstart/aspire-walk.ts b/packages/cli/e2e/src/application/gates/quickstart/aspire-walk.ts index d02a5ed0c..783a7381c 100644 --- a/packages/cli/e2e/src/application/gates/quickstart/aspire-walk.ts +++ b/packages/cli/e2e/src/application/gates/quickstart/aspire-walk.ts @@ -1,3 +1,5 @@ +import { hydrateAspirePackageCache } from './hydrate-aspire-package-cache.ts'; + /** Failure classifications emitted when a Quickstart Aspire command exceeds its bound. */ export const ASPIRE_TIMEOUT_CLASSIFICATION = { RESTORE: 'quickstart.aspire.restore.timeout:#1227', @@ -149,7 +151,9 @@ async function runAspireCommand( } if (import.meta.main) { - const [appHost, projectRoot, timeout] = Deno.args; + const [appHost, projectRoot, timeout, repoRoot] = Deno.args; if (!appHost || !projectRoot) throw new Error('appHost and projectRoot are required'); + if (!repoRoot) throw new Error('repoRoot is required'); + await hydrateAspirePackageCache(repoRoot, `${projectRoot}/aspire`, 'five'); await runBoundedAspireWalk(appHost, projectRoot, Number(timeout)); } diff --git a/packages/cli/e2e/src/application/gates/quickstart/database-walk.ts b/packages/cli/e2e/src/application/gates/quickstart/database-walk.ts index 5ef397549..b795ec609 100644 --- a/packages/cli/e2e/src/application/gates/quickstart/database-walk.ts +++ b/packages/cli/e2e/src/application/gates/quickstart/database-walk.ts @@ -1,3 +1,5 @@ +import { hydrateAspirePackageCache } from './hydrate-aspire-package-cache.ts'; + const DB_APPHOST_START_TIMEOUT_SECONDS = '90'; const DB_COMMAND_TIMEOUT_MS = 120_000; export const DB_RESTORE_MAX_ATTEMPTS = 2; @@ -111,7 +113,10 @@ async function runDatabaseCommand( } if (import.meta.main) { - const [cliSpecifier, projectRoot] = Deno.args; - if (!cliSpecifier || !projectRoot) throw new Error('cliSpecifier and projectRoot are required'); + const [cliSpecifier, projectRoot, repoRoot] = Deno.args; + if (!cliSpecifier || !projectRoot || !repoRoot) { + throw new Error('cliSpecifier, projectRoot, and repoRoot are required'); + } + await hydrateAspirePackageCache(repoRoot, `${projectRoot}/aspire`, 'four'); await runBoundedDatabaseWalk(cliSpecifier, projectRoot); } diff --git a/packages/cli/e2e/src/application/gates/quickstart/hydrate-aspire-package-cache.ts b/packages/cli/e2e/src/application/gates/quickstart/hydrate-aspire-package-cache.ts new file mode 100644 index 000000000..4da7653e9 --- /dev/null +++ b/packages/cli/e2e/src/application/gates/quickstart/hydrate-aspire-package-cache.ts @@ -0,0 +1,73 @@ +import { join } from '@std/path'; + +export const ASPIRE_PACKAGE_CACHE = { + fivePackageHash: '20B4B80F832F59C1', + fourPackageHash: 'F7BD251A60347D74', + fivePackages: [ + 'Aspire.Hosting,13.4.6', + 'Aspire.Hosting.PostgreSQL,13.4.6', + 'Aspire.Hosting.Redis,13.4.6', + 'Aspire.Hosting.Browsers,13.4.6-preview.1.26319.6', + 'Aspire.Hosting.CodeGeneration.TypeScript,13.4.6', + ], + fourPackages: [ + 'Aspire.Hosting,13.4.6', + 'Aspire.Hosting.PostgreSQL,13.4.6', + 'Aspire.Hosting.Browsers,13.4.6-preview.1.26319.6', + 'Aspire.Hosting.CodeGeneration.TypeScript,13.4.6', + ], +} as const; + +export type AspirePackageSet = 'five' | 'four'; + +/** Hydrate Aspire's project-local restore cache from the workflow's pinned local NuGet source. */ +export async function hydrateAspirePackageCache( + repoRoot: string, + aspireRoot: string, + packageSet: AspirePackageSet, +): Promise { + const managedPath = (await Deno.readTextFile( + join(repoRoot, '.llm/tmp/aspire-managed-path.txt'), + )).trim(); + const localSource = join(repoRoot, '.llm/tmp/aspire-nuget-source'); + const hash = packageSet === 'five' + ? ASPIRE_PACKAGE_CACHE.fivePackageHash + : ASPIRE_PACKAGE_CACHE.fourPackageHash; + const packages = packageSet === 'five' + ? ASPIRE_PACKAGE_CACHE.fivePackages + : ASPIRE_PACKAGE_CACHE.fourPackages; + const output = join( + aspireRoot, + packageSet === 'five' ? '' : 'db-operation', + '.aspire/integrations/package-restore', + hash, + 'obj', + ); + const args = [ + 'nuget', + 'restore', + '--output', + output, + '--framework', + 'net10.0', + '--runtime-identifier', + 'linux-x64', + ...packages.flatMap((pkg) => ['--package', pkg]), + '--source', + localSource, + '--working-dir', + packageSet === 'five' ? aspireRoot : join(aspireRoot, 'db-operation'), + ]; + const result = await new Deno.Command(managedPath, { + args, + stdout: 'piped', + stderr: 'piped', + }).output(); + if (result.code !== 0) { + throw new Error( + `pinned Aspire package-cache hydration failed (${result.code}): ${ + new TextDecoder().decode(result.stderr) + }`, + ); + } +} diff --git a/packages/cli/e2e/suites/quickstart/quickstart-walk-suite.ts b/packages/cli/e2e/suites/quickstart/quickstart-walk-suite.ts index e0d03298d..a877a0269 100644 --- a/packages/cli/e2e/suites/quickstart/quickstart-walk-suite.ts +++ b/packages/cli/e2e/suites/quickstart/quickstart-walk-suite.ts @@ -139,7 +139,8 @@ function createQuickstartGates(): readonly GateDefinition[] { (context) => [ 'deno', 'run', - '--allow-run=aspire', + '--allow-read', + '--allow-run', resolve( context.project.repoRoot, 'packages/cli/e2e/src/application/gates/quickstart/aspire-walk.ts', @@ -147,6 +148,7 @@ function createQuickstartGates(): readonly GateDefinition[] { context.project.appHost, context.project.projectRoot, String(ASPIRE_STEP_TIMEOUT_MS), + context.project.repoRoot, ], (context) => context.project.projectRoot, ), @@ -157,13 +159,15 @@ function createQuickstartGates(): readonly GateDefinition[] { (context) => [ 'deno', 'run', - '--allow-run=deno', + '--allow-read', + '--allow-run', resolve( context.project.repoRoot, 'packages/cli/e2e/src/application/gates/quickstart/database-walk.ts', ), requirePublishedCli(context), context.project.projectRoot, + context.project.repoRoot, ], (context) => context.project.projectRoot, ), diff --git a/packages/cli/e2e/tests/application/gates/quickstart-aspire-package-cache_test.ts b/packages/cli/e2e/tests/application/gates/quickstart-aspire-package-cache_test.ts new file mode 100644 index 000000000..bfb832c22 --- /dev/null +++ b/packages/cli/e2e/tests/application/gates/quickstart-aspire-package-cache_test.ts @@ -0,0 +1,18 @@ +import { assertEquals } from '@std/assert'; +import { ASPIRE_PACKAGE_CACHE } from '../../../src/application/gates/quickstart/hydrate-aspire-package-cache.ts'; + +Deno.test('pinned Aspire package-cache identities match both production restore graphs', () => { + assertEquals(ASPIRE_PACKAGE_CACHE.fivePackageHash, '20B4B80F832F59C1'); + assertEquals(ASPIRE_PACKAGE_CACHE.fourPackageHash, 'F7BD251A60347D74'); + assertEquals(ASPIRE_PACKAGE_CACHE.fivePackages, [ + 'Aspire.Hosting,13.4.6', + 'Aspire.Hosting.PostgreSQL,13.4.6', + 'Aspire.Hosting.Redis,13.4.6', + 'Aspire.Hosting.Browsers,13.4.6-preview.1.26319.6', + 'Aspire.Hosting.CodeGeneration.TypeScript,13.4.6', + ]); + assertEquals( + [...ASPIRE_PACKAGE_CACHE.fourPackages], + ASPIRE_PACKAGE_CACHE.fivePackages.filter((pkg) => !pkg.startsWith('Aspire.Hosting.Redis,')), + ); +}); diff --git a/packages/cli/e2e/tests/presentation/quickstart-walk-suite_test.ts b/packages/cli/e2e/tests/presentation/quickstart-walk-suite_test.ts index 4c5ff2807..c4e0e1a7f 100644 --- a/packages/cli/e2e/tests/presentation/quickstart-walk-suite_test.ts +++ b/packages/cli/e2e/tests/presentation/quickstart-walk-suite_test.ts @@ -68,6 +68,20 @@ Deno.test('quickstart service-response verdict accepts service health without a assertEquals(response.slice(-2), ['/repo/.llm/tmp/my-app/aspire/apphost.mts', 'users']); }); +Deno.test('quickstart Aspire gates hydrate the pinned project-local package cache', () => { + const suite = createQuickstartWalkSuite({ + packageSource: PACKAGE_SOURCE.JSR, + cliEntrypoint: EXACT_CLI, + }); + const context = contextFor(suite.defaultOptions); + const aspire = command(suite, GATE.QUICKSTART_ASPIRE, context); + const database = command(suite, GATE.QUICKSTART_DATABASE, context); + assertEquals(aspire.includes('/repo'), true); + assertEquals(database.includes('/repo'), true); + assertEquals(aspire.includes('--allow-read'), true); + assertEquals(database.includes('--allow-read'), true); +}); + Deno.test('quickstart commands reject a local CLI entrypoint', () => { const suite = createQuickstartWalkSuite({ packageSource: PACKAGE_SOURCE.LOCAL, From 26ce054ee68642c81d8f7f97596dee981b05312d Mon Sep 17 00:00:00 2001 From: Rickylabs Date: Wed, 5 Aug 2026 03:27:00 +0200 Subject: [PATCH 06/12] fix(ci): resolve bundled Aspire cache tool --- .github/scripts/aspire-nuget-cache-policy.test.ts | 1 + .github/workflows/e2e-cli-prod.yml | 3 ++- .llm/runs/fix-aspire-restore-stability-1227--1227/worklog.md | 3 +++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/scripts/aspire-nuget-cache-policy.test.ts b/.github/scripts/aspire-nuget-cache-policy.test.ts index ab2d0477f..c1273b12f 100644 --- a/.github/scripts/aspire-nuget-cache-policy.test.ts +++ b/.github/scripts/aspire-nuget-cache-policy.test.ts @@ -46,6 +46,7 @@ Deno.test('production E2E seeds and verifies every exact Aspire integration pack assertStringIncludes(workflow, 'Materialize pinned Aspire local package source'); assertStringIncludes(workflow, "-name '*.nupkg'"); assertStringIncludes(workflow, '-name aspire-managed'); + assertStringIncludes(workflow, 'readlink -f'); }); Deno.test('published E2E artifacts retain Aspire CLI diagnostics', async () => { diff --git a/.github/workflows/e2e-cli-prod.yml b/.github/workflows/e2e-cli-prod.yml index 482272761..6ab5c79d5 100644 --- a/.github/workflows/e2e-cli-prod.yml +++ b/.github/workflows/e2e-cli-prod.yml @@ -95,7 +95,8 @@ jobs: mkdir -p .llm/tmp/aspire-nuget-source find "$HOME/.nuget/packages" -name '*.nupkg' \ -exec cp --update=none '{}' .llm/tmp/aspire-nuget-source/ \; - find "$HOME/.aspire/bin" -type f -name aspire-managed -print -quit \ + aspire_path="$(readlink -f "$HOME/.aspire/bin/aspire")" + find "$(dirname "$aspire_path")" -type f -name aspire-managed -print -quit \ > .llm/tmp/aspire-managed-path.txt test -s .llm/tmp/aspire-managed-path.txt diff --git a/.llm/runs/fix-aspire-restore-stability-1227--1227/worklog.md b/.llm/runs/fix-aspire-restore-stability-1227--1227/worklog.md index 1f5898dfd..a2945e9a1 100644 --- a/.llm/runs/fix-aspire-restore-stability-1227--1227/worklog.md +++ b/.llm/runs/fix-aspire-restore-stability-1227--1227/worklog.md @@ -43,3 +43,6 @@ production (five-package `20B4B80F832F59C1`, DB four-package `F7BD251A60347D74`). A local cold fixture hydrated from that source and `aspire restore` then completed in 7.10s without a network NuGet restore. The classified retry remains as a bounded fallback. +- 2026-08-05: run 30966339570 failed before E2E because the cloud `dotnet tool --tool-path` + launcher does not keep its `.store` beneath `~/.aspire/bin`. Corrected discovery to resolve the + launcher target and search beside the installed bundle. Proof streak remains 0/3. From be5292a79eaccb62c9d254d0b10ee6b614915886 Mon Sep 17 00:00:00 2001 From: Rickylabs Date: Wed, 5 Aug 2026 03:28:27 +0200 Subject: [PATCH 07/12] fix(ci): discover Aspire managed payload --- .github/scripts/aspire-nuget-cache-policy.test.ts | 2 +- .github/workflows/e2e-cli-prod.yml | 4 ++-- .llm/runs/fix-aspire-restore-stability-1227--1227/worklog.md | 3 +++ 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/scripts/aspire-nuget-cache-policy.test.ts b/.github/scripts/aspire-nuget-cache-policy.test.ts index c1273b12f..98a1424bd 100644 --- a/.github/scripts/aspire-nuget-cache-policy.test.ts +++ b/.github/scripts/aspire-nuget-cache-policy.test.ts @@ -46,7 +46,7 @@ Deno.test('production E2E seeds and verifies every exact Aspire integration pack assertStringIncludes(workflow, 'Materialize pinned Aspire local package source'); assertStringIncludes(workflow, "-name '*.nupkg'"); assertStringIncludes(workflow, '-name aspire-managed'); - assertStringIncludes(workflow, 'readlink -f'); + assertStringIncludes(workflow, 'find "$HOME" -type f -name aspire-managed'); }); Deno.test('published E2E artifacts retain Aspire CLI diagnostics', async () => { diff --git a/.github/workflows/e2e-cli-prod.yml b/.github/workflows/e2e-cli-prod.yml index 6ab5c79d5..08cc30943 100644 --- a/.github/workflows/e2e-cli-prod.yml +++ b/.github/workflows/e2e-cli-prod.yml @@ -95,10 +95,10 @@ jobs: mkdir -p .llm/tmp/aspire-nuget-source find "$HOME/.nuget/packages" -name '*.nupkg' \ -exec cp --update=none '{}' .llm/tmp/aspire-nuget-source/ \; - aspire_path="$(readlink -f "$HOME/.aspire/bin/aspire")" - find "$(dirname "$aspire_path")" -type f -name aspire-managed -print -quit \ + find "$HOME" -type f -name aspire-managed -print -quit \ > .llm/tmp/aspire-managed-path.txt test -s .llm/tmp/aspire-managed-path.txt + sed 's|^|aspire-managed: |' .llm/tmp/aspire-managed-path.txt - name: Download published version if: github.event_name == 'workflow_run' diff --git a/.llm/runs/fix-aspire-restore-stability-1227--1227/worklog.md b/.llm/runs/fix-aspire-restore-stability-1227--1227/worklog.md index a2945e9a1..69dc46e9b 100644 --- a/.llm/runs/fix-aspire-restore-stability-1227--1227/worklog.md +++ b/.llm/runs/fix-aspire-restore-stability-1227--1227/worklog.md @@ -46,3 +46,6 @@ - 2026-08-05: run 30966339570 failed before E2E because the cloud `dotnet tool --tool-path` launcher does not keep its `.store` beneath `~/.aspire/bin`. Corrected discovery to resolve the launcher target and search beside the installed bundle. Proof streak remains 0/3. +- 2026-08-05: run 30966413382 showed the cloud launcher is a real shim rather than a symlink, so + resolving it still did not expose the payload. Replaced layout inference with an exact filename + search bounded to `$HOME`; the job prints the discovered path. Proof streak remains 0/3. From ef248871699d25a4b68f05461367d902f90e73fc Mon Sep 17 00:00:00 2001 From: Rickylabs Date: Wed, 5 Aug 2026 03:30:09 +0200 Subject: [PATCH 08/12] fix(ci): extract Aspire bundle before hydration --- .github/scripts/aspire-nuget-cache-policy.test.ts | 1 + .github/workflows/e2e-cli-prod.yml | 1 + .llm/runs/fix-aspire-restore-stability-1227--1227/worklog.md | 3 +++ 3 files changed, 5 insertions(+) diff --git a/.github/scripts/aspire-nuget-cache-policy.test.ts b/.github/scripts/aspire-nuget-cache-policy.test.ts index 98a1424bd..26eedc5ef 100644 --- a/.github/scripts/aspire-nuget-cache-policy.test.ts +++ b/.github/scripts/aspire-nuget-cache-policy.test.ts @@ -44,6 +44,7 @@ Deno.test('production E2E seeds and verifies every exact Aspire integration pack assertStringIncludes(workflow, 'needs: prepare-aspire-sdk-cache'); assertStringIncludes(workflow, "steps.aspire-sdk-cache.outputs.cache-hit != 'true'"); assertStringIncludes(workflow, 'Materialize pinned Aspire local package source'); + assertStringIncludes(workflow, 'aspire --version'); assertStringIncludes(workflow, "-name '*.nupkg'"); assertStringIncludes(workflow, '-name aspire-managed'); assertStringIncludes(workflow, 'find "$HOME" -type f -name aspire-managed'); diff --git a/.github/workflows/e2e-cli-prod.yml b/.github/workflows/e2e-cli-prod.yml index 08cc30943..bcb7b7768 100644 --- a/.github/workflows/e2e-cli-prod.yml +++ b/.github/workflows/e2e-cli-prod.yml @@ -92,6 +92,7 @@ jobs: - name: Materialize pinned Aspire local package source run: | + aspire --version mkdir -p .llm/tmp/aspire-nuget-source find "$HOME/.nuget/packages" -name '*.nupkg' \ -exec cp --update=none '{}' .llm/tmp/aspire-nuget-source/ \; diff --git a/.llm/runs/fix-aspire-restore-stability-1227--1227/worklog.md b/.llm/runs/fix-aspire-restore-stability-1227--1227/worklog.md index 69dc46e9b..1c31edc81 100644 --- a/.llm/runs/fix-aspire-restore-stability-1227--1227/worklog.md +++ b/.llm/runs/fix-aspire-restore-stability-1227--1227/worklog.md @@ -49,3 +49,6 @@ - 2026-08-05: run 30966413382 showed the cloud launcher is a real shim rather than a symlink, so resolving it still did not expose the payload. Replaced layout inference with an exact filename search bounded to `$HOME`; the job prints the discovered path. Proof streak remains 0/3. +- 2026-08-05: run 30966481611 proved the managed bundle is not extracted at install time anywhere + under `$HOME`. Added an explicit pinned Aspire invocation before discovery so the self-extracting + tool materializes its bundled payload. Proof streak remains 0/3. From 3114b74f073ac03c7787b2f570a7e2680c3036d4 Mon Sep 17 00:00:00 2001 From: Rickylabs Date: Wed, 5 Aug 2026 03:31:41 +0200 Subject: [PATCH 09/12] fix(ci): bound Aspire bundle extraction --- .github/aspire-cache/apphost.mts | 2 ++ .github/aspire-cache/aspire.config.json | 14 ++++++++++++++ .github/scripts/aspire-nuget-cache-policy.test.ts | 2 +- .github/workflows/e2e-cli-prod.yml | 5 ++++- .../worklog.md | 3 +++ 5 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 .github/aspire-cache/apphost.mts create mode 100644 .github/aspire-cache/aspire.config.json diff --git a/.github/aspire-cache/apphost.mts b/.github/aspire-cache/apphost.mts new file mode 100644 index 000000000..c12a3d62c --- /dev/null +++ b/.github/aspire-cache/apphost.mts @@ -0,0 +1,2 @@ +// Cache-hydration fixture: Aspire only needs the AppHost identity during restore. +export {}; diff --git a/.github/aspire-cache/aspire.config.json b/.github/aspire-cache/aspire.config.json new file mode 100644 index 000000000..607c98e0b --- /dev/null +++ b/.github/aspire-cache/aspire.config.json @@ -0,0 +1,14 @@ +{ + "appHost": { + "path": "apphost.mts", + "language": "typescript/nodejs" + }, + "sdk": { + "version": "13.4.6" + }, + "packages": { + "Aspire.Hosting.PostgreSQL": "13.4.6", + "Aspire.Hosting.Redis": "13.4.6", + "Aspire.Hosting.Browsers": "13.4.6-preview.1.26319.6" + } +} diff --git a/.github/scripts/aspire-nuget-cache-policy.test.ts b/.github/scripts/aspire-nuget-cache-policy.test.ts index 26eedc5ef..5a4ee9c32 100644 --- a/.github/scripts/aspire-nuget-cache-policy.test.ts +++ b/.github/scripts/aspire-nuget-cache-policy.test.ts @@ -44,7 +44,7 @@ Deno.test('production E2E seeds and verifies every exact Aspire integration pack assertStringIncludes(workflow, 'needs: prepare-aspire-sdk-cache'); assertStringIncludes(workflow, "steps.aspire-sdk-cache.outputs.cache-hit != 'true'"); assertStringIncludes(workflow, 'Materialize pinned Aspire local package source'); - assertStringIncludes(workflow, 'aspire --version'); + assertStringIncludes(workflow, 'timeout 5s aspire restore'); assertStringIncludes(workflow, "-name '*.nupkg'"); assertStringIncludes(workflow, '-name aspire-managed'); assertStringIncludes(workflow, 'find "$HOME" -type f -name aspire-managed'); diff --git a/.github/workflows/e2e-cli-prod.yml b/.github/workflows/e2e-cli-prod.yml index bcb7b7768..d3fa90bb2 100644 --- a/.github/workflows/e2e-cli-prod.yml +++ b/.github/workflows/e2e-cli-prod.yml @@ -92,7 +92,10 @@ jobs: - name: Materialize pinned Aspire local package source run: | - aspire --version + warmup=0 + timeout 5s aspire restore \ + --apphost .github/aspire-cache/apphost.mts --non-interactive --nologo || warmup=$? + test "$warmup" -eq 0 -o "$warmup" -eq 6 -o "$warmup" -eq 124 mkdir -p .llm/tmp/aspire-nuget-source find "$HOME/.nuget/packages" -name '*.nupkg' \ -exec cp --update=none '{}' .llm/tmp/aspire-nuget-source/ \; diff --git a/.llm/runs/fix-aspire-restore-stability-1227--1227/worklog.md b/.llm/runs/fix-aspire-restore-stability-1227--1227/worklog.md index 1c31edc81..1c8bbad09 100644 --- a/.llm/runs/fix-aspire-restore-stability-1227--1227/worklog.md +++ b/.llm/runs/fix-aspire-restore-stability-1227--1227/worklog.md @@ -52,3 +52,6 @@ - 2026-08-05: run 30966481611 proved the managed bundle is not extracted at install time anywhere under `$HOME`. Added an explicit pinned Aspire invocation before discovery so the self-extracting tool materializes its bundled payload. Proof streak remains 0/3. +- 2026-08-05: run 30966559521 proved `aspire --version` does not extract the bundle either. Replaced + it with a five-second, externally bounded restore against a checked-in fixture; only exit 0, 6, + or GNU timeout 124 is accepted before discovering the now-materialized payload. Proof 0/3. From 50fdb28c0bcc293bd1c727d35e8485e71a04f019 Mon Sep 17 00:00:00 2001 From: Rickylabs Date: Wed, 5 Aug 2026 03:47:07 +0200 Subject: [PATCH 10/12] fix(e2e): keep Aspire hydration offline --- .../worklog.md | 4 +++ .../hydrate-aspire-package-cache.ts | 31 ++++++++++++++++--- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/.llm/runs/fix-aspire-restore-stability-1227--1227/worklog.md b/.llm/runs/fix-aspire-restore-stability-1227--1227/worklog.md index 1c8bbad09..c8cbe3d22 100644 --- a/.llm/runs/fix-aspire-restore-stability-1227--1227/worklog.md +++ b/.llm/runs/fix-aspire-restore-stability-1227--1227/worklog.md @@ -55,3 +55,7 @@ - 2026-08-05: run 30966559521 proved `aspire --version` does not extract the bundle either. Replaced it with a five-second, externally bounded restore against a checked-in fixture; only exit 0, 6, or GNU timeout 124 is accepted before discovering the now-materialized payload. Proof 0/3. +- 2026-08-05: run 30966638003 was canceled after offline hydration exceeded three minutes. Local + verbose output had shown NuGet still querying its online vulnerability feed even with a local + package source. Hydration now sets `NuGetAudit=false` (the prerequisite acquisition remains + audited) and has its own 30s abort bound. Proof streak remains 0/3. diff --git a/packages/cli/e2e/src/application/gates/quickstart/hydrate-aspire-package-cache.ts b/packages/cli/e2e/src/application/gates/quickstart/hydrate-aspire-package-cache.ts index 4da7653e9..941308d04 100644 --- a/packages/cli/e2e/src/application/gates/quickstart/hydrate-aspire-package-cache.ts +++ b/packages/cli/e2e/src/application/gates/quickstart/hydrate-aspire-package-cache.ts @@ -19,6 +19,7 @@ export const ASPIRE_PACKAGE_CACHE = { } as const; export type AspirePackageSet = 'five' | 'four'; +const HYDRATION_TIMEOUT_MS = 30_000; /** Hydrate Aspire's project-local restore cache from the workflow's pinned local NuGet source. */ export async function hydrateAspirePackageCache( @@ -58,11 +59,31 @@ export async function hydrateAspirePackageCache( '--working-dir', packageSet === 'five' ? aspireRoot : join(aspireRoot, 'db-operation'), ]; - const result = await new Deno.Command(managedPath, { - args, - stdout: 'piped', - stderr: 'piped', - }).output(); + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), HYDRATION_TIMEOUT_MS); + let result: Deno.CommandOutput; + try { + result = await new Deno.Command(managedPath, { + args, + env: { + // Acquisition was already audited by the prerequisite `dotnet restore`. + // Keep this local-source materialization fully offline. + NuGetAudit: 'false', + }, + stdout: 'piped', + stderr: 'piped', + signal: controller.signal, + }).output(); + } catch (error) { + if (error instanceof DOMException && error.name === 'AbortError') { + throw new Error( + 'pinned Aspire package-cache hydration exceeded its 30s infrastructure bound', + ); + } + throw error; + } finally { + clearTimeout(timeout); + } if (result.code !== 0) { throw new Error( `pinned Aspire package-cache hydration failed (${result.code}): ${ From 0367dfb95ccf6cc253717e1f1fe06e284e8a378c Mon Sep 17 00:00:00 2001 From: Rickylabs Date: Wed, 5 Aug 2026 03:47:46 +0200 Subject: [PATCH 11/12] test(e2e): add focused Quickstart proof dispatch --- .github/scripts/aspire-nuget-cache-policy.test.ts | 1 + .github/workflows/e2e-cli-prod.yml | 7 +++++++ .../fix-aspire-restore-stability-1227--1227/worklog.md | 3 +++ 3 files changed, 11 insertions(+) diff --git a/.github/scripts/aspire-nuget-cache-policy.test.ts b/.github/scripts/aspire-nuget-cache-policy.test.ts index 5a4ee9c32..3dab2f681 100644 --- a/.github/scripts/aspire-nuget-cache-policy.test.ts +++ b/.github/scripts/aspire-nuget-cache-policy.test.ts @@ -48,6 +48,7 @@ Deno.test('production E2E seeds and verifies every exact Aspire integration pack assertStringIncludes(workflow, "-name '*.nupkg'"); assertStringIncludes(workflow, '-name aspire-managed'); assertStringIncludes(workflow, 'find "$HOME" -type f -name aspire-managed'); + assertStringIncludes(workflow, 'quickstart-only:'); }); Deno.test('published E2E artifacts retain Aspire CLI diagnostics', async () => { diff --git a/.github/workflows/e2e-cli-prod.yml b/.github/workflows/e2e-cli-prod.yml index d3fa90bb2..393350da9 100644 --- a/.github/workflows/e2e-cli-prod.yml +++ b/.github/workflows/e2e-cli-prod.yml @@ -10,6 +10,11 @@ on: description: Published @netscript/cli version to validate required: true type: string + quickstart-only: + description: Run only the published Quickstart walk (manual repeated-proof lane) + required: false + default: false + type: boolean permissions: actions: read @@ -130,6 +135,7 @@ jobs: echo "version=$version" >> "$GITHUB_OUTPUT" - name: Wait for JSR propagation + if: ${{ github.event_name != 'workflow_dispatch' || inputs.quickstart-only != true }} run: | echo "sleeping 120s so just-published @netscript/cli@${{ steps.version.outputs.version }} and deps become resolvable on JSR before install." sleep 120 @@ -158,6 +164,7 @@ jobs: test -f .llm/tmp/cli-prod-init-smoke/deno.json - name: Full scaffold runtime E2E (published CLI, one pass) + if: ${{ github.event_name != 'workflow_dispatch' || inputs.quickstart-only != true }} run: | deno task e2e:cli run scaffold.runtime \ --source jsr \ diff --git a/.llm/runs/fix-aspire-restore-stability-1227--1227/worklog.md b/.llm/runs/fix-aspire-restore-stability-1227--1227/worklog.md index c8cbe3d22..9cc8333a3 100644 --- a/.llm/runs/fix-aspire-restore-stability-1227--1227/worklog.md +++ b/.llm/runs/fix-aspire-restore-stability-1227--1227/worklog.md @@ -59,3 +59,6 @@ verbose output had shown NuGet still querying its online vulnerability feed even with a local package source. Hydration now sets `NuGetAudit=false` (the prerequisite acquisition remains audited) and has its own 30s abort bound. Proof streak remains 0/3. +- 2026-08-05: added a manual-dispatch-only `quickstart-only` proof mode. It leaves canary and normal + production behavior unchanged while omitting the unrelated scaffold suite and propagation sleep + for repeated clean-run proof against an already-published exact version. From ae71db70af3470285849923c63da1817df592df0 Mon Sep 17 00:00:00 2001 From: Rickylabs Date: Wed, 5 Aug 2026 03:50:30 +0200 Subject: [PATCH 12/12] test(ci): isolate repeated Quickstart proofs --- .github/scripts/aspire-nuget-cache-policy.test.ts | 1 + .github/workflows/e2e-cli-prod.yml | 2 +- .llm/runs/fix-aspire-restore-stability-1227--1227/worklog.md | 3 +++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/scripts/aspire-nuget-cache-policy.test.ts b/.github/scripts/aspire-nuget-cache-policy.test.ts index 3dab2f681..194db0224 100644 --- a/.github/scripts/aspire-nuget-cache-policy.test.ts +++ b/.github/scripts/aspire-nuget-cache-policy.test.ts @@ -49,6 +49,7 @@ Deno.test('production E2E seeds and verifies every exact Aspire integration pack assertStringIncludes(workflow, '-name aspire-managed'); assertStringIncludes(workflow, 'find "$HOME" -type f -name aspire-managed'); assertStringIncludes(workflow, 'quickstart-only:'); + assertStringIncludes(workflow, 'inputs.quickstart-only && github.run_id || github.ref'); }); Deno.test('published E2E artifacts retain Aspire CLI diagnostics', async () => { diff --git a/.github/workflows/e2e-cli-prod.yml b/.github/workflows/e2e-cli-prod.yml index 393350da9..48dd2ef22 100644 --- a/.github/workflows/e2e-cli-prod.yml +++ b/.github/workflows/e2e-cli-prod.yml @@ -21,7 +21,7 @@ permissions: contents: read concurrency: - group: e2e-cli-prod-${{ github.workflow }}-${{ github.ref }} + group: e2e-cli-prod-${{ github.workflow }}-${{ inputs.quickstart-only && github.run_id || github.ref }} cancel-in-progress: false jobs: diff --git a/.llm/runs/fix-aspire-restore-stability-1227--1227/worklog.md b/.llm/runs/fix-aspire-restore-stability-1227--1227/worklog.md index 9cc8333a3..1ae6ae3b5 100644 --- a/.llm/runs/fix-aspire-restore-stability-1227--1227/worklog.md +++ b/.llm/runs/fix-aspire-restore-stability-1227--1227/worklog.md @@ -62,3 +62,6 @@ - 2026-08-05: added a manual-dispatch-only `quickstart-only` proof mode. It leaves canary and normal production behavior unchanged while omitting the unrelated scaffold suite and propagation sleep for repeated clean-run proof against an already-published exact version. +- 2026-08-05: canceled run 30966638003 remained `in_progress` even after the force-cancel endpoint + and held the ref-wide concurrency lock. Manual `quickstart-only` proofs now key concurrency by + run id; normal canary/production dispatches retain ref serialization.