Skip to content

[Build] Validate the complete artifact output topology before writing #694

Description

@cssbruno

Priority

High — prevention of destructive writes, cross-target contamination, and ambiguous packaging

Context

A GOWDK build can write several coordinated destinations:

  • static output directory;
  • generated frontend application directory;
  • frontend binary;
  • frontend WASM artifact;
  • generated backend application directory;
  • backend binary;
  • Docker context files;
  • deployment recipes;
  • timings/report sidecars;
  • multiple configured targets built in one command.

Current validation covers some local relationships, for example:

  • a binary requires an app directory;
  • a WASM artifact requires an app directory;
  • a backend binary requires a backend app directory;
  • configured target names must be unique;
  • GenerateWithOptions rejects a generated app directory inside the static output directory and rejects static output inside the app's embedded output subdirectory.

The build command does not currently construct and validate one complete graph of every write destination before generation starts. It also does not appear to reject collisions or containment across separate configured targets.

Problem

Individually valid paths can form an unsafe or ambiguous topology when considered together.

Examples include:

Targets: []gowdk.BuildTargetConfig{
    {Name: "admin", Output: "dist/site"},
    {Name: "public", Output: "dist/site"},
}
{
    Output: "dist/site",
    App:    "dist/site/generated-app",
}
{
    Output:        "dist/site",
    Binary:        "dist/site/server",
    App:           ".gowdk/app",
    BackendBinary: "dist/site/server",
    BackendApp:    ".gowdk/backend",
}
Targets: []gowdk.BuildTargetConfig{
    {Name: "one", Output: "dist"},
    {Name: "two", Output: "dist/two"},
}

Potential consequences:

  1. One target overwrites another target's files or manifests.
  2. A later target's stale cleanup deletes files owned by an earlier target.
  3. A binary or WASM artifact written beneath static output is served or copied into a future generated application.
  4. Frontend and backend app generation write different role-specific source into the same directory.
  5. A generated app recursively embeds or copies another output tree.
  6. gowdk clean --target removes a directory containing another target's artifacts.
  7. Docker and deployment recipe generation overwrite unrelated files with matching names.
  8. Two lexically different paths refer to the same destination through symlinks, case folding, or platform path rules.
  9. Validation happens only after earlier build stages have already written output.

This is separate from route/asset filename collision validation. The concern here is the topology of user-configured artifact destinations and ownership between build roles/targets.

Goal

Build one canonical artifact destination plan before any filesystem mutation and reject every unsupported collision, overlap, alias, or ownership ambiguity with actionable diagnostics.

Proposed model

Represent every requested destination as a typed node:

type DestinationKind string

type Destination struct {
    Target       string
    Role         string
    Kind         DestinationKind
    RawPath      string
    AbsolutePath string
    PathType     PathType // file or directory
    Owner        string
}

Example kinds:

  • static-output;
  • frontend-app;
  • frontend-binary;
  • frontend-wasm;
  • backend-app;
  • backend-binary;
  • timings-report;
  • docker-context;
  • deployment-recipe.

Create the complete set for all selected configured targets or the one ad hoc request, normalize it, then validate all pairs before source discovery, generation, cleanup, or directory creation.

Required validation

Exact collisions

Reject two distinct artifact owners that resolve to the same path, including:

  • frontend and backend binary equality;
  • frontend app and backend app equality;
  • two configured targets sharing an output/app/binary/WASM destination;
  • a file destination matching another generated file destination;
  • a report/recipe destination overwriting a generated artifact.

File/directory conflicts

Reject a file destination that equals a directory destination or requires a parent path that is planned as a file.

Unsupported containment

Define an explicit allowed-containment matrix. At minimum, reject:

  • app directory inside its static output;
  • backend app inside frontend output or frontend app unless deliberately supported;
  • frontend app inside backend app or vice versa;
  • binary/WASM inside a generated app source tree;
  • binary/WASM inside static output when that output may be served or embedded;
  • one target output inside another selected target output;
  • one selected target app directory inside another target's app/output directory;
  • cleanable target roots that contain destinations owned by another target.

Some same-target relationships are intentional: the generated frontend app embeds a copied snapshot of static output in its own compiler-owned subdirectory. That internal destination should be compiler-derived, represented in the plan, and validated rather than inferred after writes begin.

Filesystem aliases

Validate both normalized lexical paths and existing filesystem aliases:

  • filepath.Abs and filepath.Clean;
  • symlink-resolved existing ancestors;
  • Windows volume/case behavior;
  • path separator normalization;
  • junction/reparse-point aliases where supported.

Do not require every destination to exist before validation. Resolve the longest existing parent and append the remaining clean suffix so aliases in existing ancestors are still detected.

Portable case-fold/generated-filename collision policy can remain a separate compiler concern, but configured destination equality must respect the current host filesystem and avoid obvious cross-platform aliases.

Multi-target ownership

Every path must have one owning selected target and role. Shared output destinations should be rejected initially rather than supported implicitly.

If shared destinations become a real requirement later, they need an explicit shared-artifact model with ownership and cleanup rules; duplicate strings in config are not sufficient.

Validation timing

Topology validation must run:

  1. after config/ad hoc target resolution and default path expansion;
  2. before creating directories or writing any output;
  3. before gowdk clean removes selected target paths;
  4. through the shared workspace/application planning service proposed by Extract shared project compilation orchestration from the CLI #671.

A failed topology check must leave the filesystem unchanged.

Diagnostics

Report all relevant collisions in one pass where practical. Each diagnostic should identify:

  • both target names;
  • both artifact kinds/roles;
  • raw configured paths;
  • normalized/resolved destination;
  • whether the conflict is equality, file/directory conflict, containment, or aliasing;
  • a remediation suggestion.

Example:

build_output_overlap: target "admin" static output "dist" contains target "public" static output "dist/public"; selected target roots must be disjoint because cleanup and manifest publication are target-owned

Machine-readable output should use stable codes instead of requiring message parsing.

Clean integration

gowdk clean must consume the same destination plan and ownership rules. It should refuse to remove a target root when that root contains an artifact owned by an unselected target.

Dry-run JSON should expose the validated owner/kind for each removal candidate.

Test plan

Add table-driven and end-to-end tests for:

  • exact duplicate output directories;
  • parent/child outputs across targets;
  • duplicate frontend/backend app directories;
  • duplicate binary destinations;
  • binary or WASM under static output;
  • binary or WASM under generated app source;
  • frontend app under backend app and inverse;
  • output under app and app under output;
  • timing/report path collisions;
  • Docker/deployment recipe collisions;
  • relative paths resolving to the same absolute path;
  • symlinked parent aliases;
  • case aliases on case-insensitive platforms;
  • paths containing spaces and non-ASCII characters;
  • valid ordinary single-binary, split, backend-only, static-only, and WASM layouts;
  • multiple valid disjoint targets;
  • no filesystem mutation after topology failure;
  • clean --target refusing cross-owner deletion;
  • deterministic diagnostics independent of target declaration order.

Acceptance criteria

  • The build creates a complete typed destination plan for every selected artifact before writing.
  • Exact collisions, file/directory conflicts, unsupported containment, and existing-path aliases are rejected.
  • All selected configured targets are validated together, not one at a time immediately before writing.
  • Frontend, backend, static, binary, WASM, report, Docker, and deployment-recipe destinations are included.
  • Every destination has one explicit target/role owner.
  • A topology validation failure performs no filesystem mutation.
  • gowdk clean reuses the same ownership/topology model and cannot delete another target's artifacts.
  • Valid static, one-binary, split, backend-only, WASM, and multi-target layouts remain supported.
  • Diagnostics identify both conflicting destinations and provide stable machine-readable codes.
  • Unix, macOS case behavior, and Windows path behavior receive focused tests.
  • Configuration and deployment documentation state the supported destination relationships.

Non-goals

  • Supporting implicit shared mutable output directories between targets.
  • Detecting collisions between generated URL/file names inside one output tree; that is a separate compiler planning concern.
  • Replacing transactional publication from Publish build and generated-app output transactionally #669.
  • Automatically relocating user-configured artifacts to make an invalid topology work.

Related

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions