Skip to content

Add publish output verification#18876

Open
IEvangelist wants to merge 3 commits into
dapine/publisher-output-contractfrom
dapine/publish-verification
Open

Add publish output verification#18876
IEvangelist wants to merge 3 commits into
dapine/publisher-output-contractfrom
dapine/publish-verification

Conversation

@IEvangelist

@IEvangelist IEvangelist commented Jul 23, 2026

Copy link
Copy Markdown
Member

Description

Checked-in publish artifacts can drift from their AppHost model when a developer forgets to regenerate them. This adds aspire publish --verify, which publishes into an isolated staging directory, compares the generated files with the Git-included logical destinations, and exits with an actionable stale/missing/orphaned report without modifying the checked-in output.

This PR is intentionally stacked on #18875, which adds the publisher output-path contract and pipeline-outputs.v1 authorization handshake required to relocate primary and named publisher outputs safely.

User-facing usage

aspire publish -o .generated --verify

Command help includes:

--verify  Verify generated artifacts match the Git-included output without modifying it

Drift is grouped by destination:

Stale files in '.generated':
  manifest.json
Missing files in '.configgen':
  inventory.json
Orphaned files in '.pipelines':
  old-build.yml
Regenerate with: aspire publish --apphost ... --output-path .generated

Verification fails closed for incompatible Hosting versions or capabilities, unsupported steps, unsafe or overlapping destinations, unavailable Git, nested repositories/submodules, and symbolic-link or junction escapes. Direct legacy manifest-file output is rejected because sibling Dockerfile/Bicep artifacts cannot be completely inventoried; manifest-directory output remains verifiable. Git reconciliation includes tracked files even when newly ignored, untracked non-ignored files, and absent generated candidates that are not ignored. UTF-8 text comparison normalizes staged/logical path forms; binary and non-UTF-8 content remains byte-exact.

Screenshots / Recordings

This PR includes UI changes. Please add screenshots or screen recordings so reviewers can evaluate the visual changes without running locally.

  • For before/after comparisons, place them side-by-side or label them clearly.
  • For interactive changes (animations, transitions, new flows), prefer a short screen recording (GIF or video).
  • If you cannot capture visuals now, note what scenario to test and mark this section as TODO.

TODO: Capture the PublishVerificationTests Linux E2E recording in CI. Local Docker Desktop was unavailable.

Security considerations

Verification guarantees that logical targets are read-only during the operation. It uses a securely created temporary directory, validates all output destinations against the AppHost Git root before authorization, rejects nested repositories and link/reparse-point traversal, invokes Git through argument lists and NUL-delimited input, and redacts likely secrets from the regenerate command. Security review is requested for these path-boundary, cleanup, and non-mutation guarantees.

Validation

  • Focused Aspire CLI verification, Git, parser/help, relaunch, serializer, and cleanup tests: 57 passed.
  • Focused Hosting output-plan, manifest relocation, and staged-name tests: 25 passed.
  • Full Aspire.Cli.Tests suite during development: 4,648 passed, 38 platform-specific skipped.
  • Full repository build passed with native compilation skipped and serialized MSBuild.
  • Linux CLI E2E project builds; Docker execution was not available locally.

Fixes #18833

Checklist

  • Is this feature complete?
    • Yes. Ready to ship.
    • No. Follow-up changes expected.
  • Are you including unit tests for the changes and scenario tests if relevant?
    • Yes
    • No
  • Did you add public API?
    • Yes
      • If yes, did you have an API Review for it?
        • Yes
        • No
      • Did you add <remarks /> and <code /> elements on your triple slash comments?
        • Yes
        • No
    • No
  • Does the change make any security assumptions or guarantees?
    • Yes
      • If yes, have you done a threat model and had a security review?
        • Yes
        • No
    • No

@IEvangelist
IEvangelist marked this pull request as draft July 23, 2026 20:19
@IEvangelist
IEvangelist force-pushed the dapine/publish-verification branch 2 times, most recently from 53a8131 to 5416177 Compare July 24, 2026 02:26
IEvangelist and others added 3 commits July 23, 2026 21:41
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8963f49d-361e-4026-8676-d5caf3c24b51
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8963f49d-361e-4026-8676-d5caf3c24b51
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8963f49d-361e-4026-8676-d5caf3c24b51
@IEvangelist
IEvangelist force-pushed the dapine/publish-verification branch from 5416177 to 1f63db2 Compare July 24, 2026 02:49
@IEvangelist

Copy link
Copy Markdown
Member Author

PR Testing Report

PR Information

  • PR Number: Add publish output verification #18876
  • Title: Add publish output verification
  • Head Commit: 1f63db279ad51c344b55e56e11d300fea07ea15a
  • Base: dapine/publisher-output-contract (stacked PR)
  • State: Draft
  • Tested At: 2026-07-24T12:54:58Z

Artifact Version Verification

  • Expected Commit: 1f63db279ad51c344b55e56e11d300fea07ea15a
  • Tested Source: Detached checkout at the exact expected commit
  • Installed Version: 13.5.0-local.20260724.t123955
  • Artifact Source: Linux-x64 local-hive archive built from the exact checkout; no "Dogfood this PR" artifact was available
  • Status: ✅ Source and current PR head verified

The PR head moved from 53a81311a9ca11268ff4621dea8f09ddb95e4bd0 during testing. All final build, focused-test, and E2E results below were rerun against the current head.

Changes Analyzed

Main Surfaces

  • src/Aspire.Cli/Commands/PublishCommand.cs and PipelineCommandBase.cs - add and execute aspire publish --verify
  • src/Aspire.Cli/Commands/PublishVerificationSession.cs - secure staging, capability authorization, output-plan capture, reconciliation, and cleanup
  • src/Aspire.Cli/Commands/PublishVerificationPathSafety.cs - repository, nested-repository, submodule, and link boundary checks
  • src/Aspire.Cli/Commands/PublishVerificationReconciler.cs - stale, missing, and orphaned output classification
  • src/Aspire.Cli/Git/* - Git state and generated-output reconciliation support
  • src/Shared/CliExitCodes.cs and publish resources - dedicated exit code 23 and localized diagnostics
  • tests/Aspire.Cli.EndToEnd.Tests/PublishVerificationTests.cs - full clean/drift/no-mutation workflow
  • tests/Aspire.Cli.Tests/** - command, session, path-safety, reconciliation, Git, and extension-forwarding coverage
  • Stacked Hosting output-plan changes were included in the exact-head build and exercised by focused Hosting tests

Change Categories

  • CLI changes
  • Hosting contract dependency from the stacked base
  • Test changes
  • Dashboard changes
  • Client/component changes
  • Template changes
  • VS Code extension changes
  • CI infrastructure changes

Test Scenarios Executed

Scenario 1: Current-head restore and local-hive build

Objective: Build the CLI, Hosting packages, templates, and Linux-x64 portable archive from the exact current PR head.

Coverage Type: Build/artifact validation

Status: ✅ Passed

Results:

  • Repository restore completed.
  • Release local-hive build completed with 0 warnings and 0 errors.
  • Built CLI version: 13.5.0-local.20260724.t123955.
  • The final E2E archive contained the exact built binaries and packages.

Evidence:

  • restore-current-head.log
  • localhive-build-current-head.log

Scenario 2: Focused CLI verification tests

Objective: Exercise the changed command, verification session, reconciliation, Git, path safety, compatibility, cleanup, and extension argument-forwarding logic.

Coverage Type: Unit/boundary validation

Status: ✅ Passed

Results: 51 passed, 0 failed, 0 skipped.

The run includes the current-head direct-manifest-file fail-closed coverage and confirms a directory primary output remains authorized.

Evidence: focused-cli-tests-current-head.log

Scenario 3: Focused Hosting output-contract tests

Objective: Exercise the stacked output-plan registration and AppHost backchannel behavior used by publish verification.

Coverage Type: Unit/integration boundary validation

Status: ✅ Passed

Results: 36 passed, 0 failed, 0 skipped.

Evidence: focused-hosting-tests-current-head.log

Scenario 4: Submitted publish-verification E2E

Objective: Run the new E2E exactly as submitted after installing the current-head CLI and Hosting packages.

Coverage Type: E2E regression validation

Status: ❌ Failed

The generated AppHost adds a step named publish, but Hosting already defines publish as its built-in aggregation step. The first non-verification publish exits before --verify is exercised:

System.InvalidOperationException:
A step with the name 'publish' has already been added to the pipeline.

at Aspire.Hosting.Pipelines.DistributedApplicationPipeline.AddStep(...)
at Program.<Main>$(...) in apphost.cs:line 19

Evidence:

  • publish-verification-e2e-current-head-fixture-defect.log
  • publish-verification-e2e-current-head-fixture-defect-output.txt
  • publish-verification-e2e-current-head-fixture-defect.cast

Scenario 5: Corrected clean/drift/no-mutation E2E

Objective: Isolate the submitted fixture defect and validate the intended user-facing workflow against the unchanged current-head CLI and packages.

Coverage Type: Happy path and unhappy path

Status: ✅ Passed

The temporary fixture correction gave the output-producing step a unique name and wired it into the built-in publish aggregation step:

Name = "write-verification-output",
RequiredBySteps = [WellKnownPipelineSteps.Publish],

Verified behavior:

  1. Initial aspire publish generated the primary and two named outputs.
  2. A clean aspire publish --verify reported Published output is up to date.
  3. Simultaneous stale, missing, and orphaned changes were classified correctly:
    • stale: checked-output/primary.txt
    • missing: .configgen/config.json
    • orphaned: .pipelines/orphan.yml
  4. Drift returned the dedicated exit code 23.
  5. The verification run did not mutate any checked-in target; the post-run shell assertion passed.

Expected Unhappy-Path Outcome: Clear drift diagnostics, exit code 23, and no target mutation.

Evidence:

  • publish-verification-e2e-corrected-current-head-final.log
  • publish-verification-e2e-corrected-current-head-output.txt
  • publish-verification-e2e-corrected-current-head-final.cast

Environment Notes

  • The draft PR had no downloadable dogfood artifact, so validation used an exact-head source build.
  • Docker could not complete a fresh E2E image build because registry.npmjs.org repeatedly failed its TLS handshake while installing pnpm. The run used the existing hex1b-test-3527ead69a18:latest E2E image instead.
  • Windows-created tar metadata did not preserve Linux executable modes for the CLI and extracted managed sidecar. The archive was repacked with the CLI executable bit, and the temporary E2E setup restored the managed sidecar executable bit after extraction. Product binary and package contents were unchanged.
  • The portable hive directory was normalized to the hives/local name expected by the LocalHive E2E strategy so the generated AppHost used the current 13.5.0-local.20260724.t123955 SDK instead of a released SDK.

Summary

Scenario Status Notes
Current-head restore and local-hive build ✅ Passed Exact head; 0 warnings/errors
Focused CLI verification tests ✅ Passed 51/51
Focused Hosting output-contract tests ✅ Passed 36/36
Submitted publish-verification E2E ❌ Failed Duplicate built-in publish step
Corrected clean/drift/no-mutation E2E ✅ Passed Clean match, all drift classes, exit 23, no mutation

Overall Result

❌ ISSUES FOUND

The publish-verification product behavior is verified at the current PR head, including its fail-closed and no-mutation guarantees. However, the PR's newly added E2E test is broken as submitted and cannot reach --verify.

Recommendation

Update the E2E fixture to use a uniquely named output-producing step and set RequiredBySteps = [WellKnownPipelineSteps.Publish], then rerun the submitted E2E in CI.

@IEvangelist
IEvangelist marked this pull request as ready for review July 24, 2026 14:52
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.

1 participant