Skip to content

A component can't build a frontend: the artifact is the git tree, and deploy only runs uv sync #86

Open

Description

@shipmill-romamo

Problem

Found while deploying Abecra's staging (Cloudfall's customer zero) on 2026-10-19. Abecra is a Python API plus a Vite web app in one repository. Cloudfall can ship the API but not the web build:

  • artifact build (engine/src/cloudfall_engine/artifact.py) clones the repository at a ref and tars the source tree (or repository.subdirectory). No build step runs, so web/dist/ is never in the artifact
  • cloudfall_deploy (engine/ansible/roles/cloudfall_deploy/tasks/main.yml) asserts runtime.packageManager == "uv" and materializes the release with uv sync; nothing else runs on the server
  • component.schema.json accepts runtime.type: node | pm2 and packageManager: npm | pnpm | yarn, and ROADMAP M4 says "native Python and Node builds", but the deploy role refuses anything that is not uv. The schema promises more than the engine does

The workaround is a side script in the fleet repository that clones the same commit, runs pnpm install && pnpm build, rsyncs dist/ to /srv/apps/<app>/web/releases/<sha>, flips its own current symlink, and only then runs cloudfall deploy. That second release tree has no digest check, no receipt, no rollback with the API release, and can drift from it (the API's current and the web's current are switched by different tools).

Proposal

A declared build step that runs on the controller before packaging, whose outputs go into the same artifact:

spec:
  build:
    - workingDirectory: web
      command: [pnpm, install, --frozen-lockfile]
    - workingDirectory: web
      command: [pnpm, build]
  artifact:
    include: [web/dist]   # build outputs kept even if gitignored
  • Runs inside the clean checkout artifact build already makes, with a declared toolchain (node version, package manager) checked like required_tools; a missing tool fails with a typed error before cloning
  • Build outputs land in the tarball, so the existing digest, receipt, symlink switch and rollback cover them unchanged
  • The ArtifactMetadata records the build steps and toolchain versions that produced it
  • Separately: until Node runtimes deploy, component.schema.json should reject runtime.type: node | pm2 and the non-uv package managers instead of failing in the role (or the docs should say they are reserved)

Alternatives

  • Keep frontends outside Cloudfall (a CDN or a side script): this is what Abecra does now, and it splits one release into two unrelated rollouts
  • A separate static component kind with its own artifact: cleaner for SPAs with no backend, but two artifacts for one commit still need a joint rollback

Acceptance

  • A component declaring a pnpm build step: artifact build --ref <sha> produces a tarball containing web/dist/, and its metadata lists the steps
  • cloudfall deploy of that release puts web/dist/ under releases/<release>/; cloudfall rollback restores the previous release's web build with the API
  • A build step that exits non-zero fails artifact build with a typed error and writes no artifact
  • cloudfall config validate on a component with runtime.type: node either deploys it or fails validation; it never passes validation and fails in the role

Activity

  1. shipmill-romamo commented on Oct 20, 2026

    @shipmill-romamo
    ContributorAuthor

    Triage: feature.

    Confirmed on main (348ed73): engine/src/cloudfall_engine/artifact.py tars the source tree at the ref with no build step, and cloudfall_deploy asserts runtime.packageManager == "uv", while component.schema.json accepts runtime.type: node | pm2 and npm | pnpm | yarn. A declared controller-side build step whose outputs go into the artifact is new behaviour (a new spec section, a toolchain check, new artifact metadata), so it gets a spec with acceptance criteria first. It pairs with #90's static routes, so the two specs are written together.

    The schema/engine mismatch is a broken promise, but fixing it by rejecting node and the non-uv package managers in the schema would refuse configs that validate today, and the right answer depends on whether Node runtimes are coming soon. The spec decides it as one of its criteria, so the reviewer settles both at once.

    The spec PR is being written now (docs/specs/002-component-build-steps.md); I'll put its link and the hold line here. Merging it is the approval.

  2. shipmill-romamo commented on Oct 20, 2026

    @shipmill-romamo
    ContributorAuthor

    The spec is in #98 (docs/specs/002-component-build-steps.md). Points to check before merging it: config validate would refuse non-uv runtimes, so the Render importer would stop writing Node components (S-002-14). The archive becomes the tracked files plus artifact.include when a component declares build steps. Build steps run with a reduced environment, so a build that needs a private registry token has no way to get it yet.

    On hold: the build waits on #98, the spec

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

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions