Skip to content

Deploy only pyproject.toml projects in v2 - #2295

Open
schnie wants to merge 5 commits into
v2from
deploy-requires-manifest
Open

schnie wants to merge 5 commits into
v2from
deploy-requires-manifest

Conversation

@schnie

@schnie schnie commented Oct 9, 2026 •

Copy link
Copy Markdown
Member

The decision

v2 builds and deploys only pyproject.toml (manifest) projects, on Astro and on Astro Private Cloud. Nobody is forced onto v2 and Astro CLI 1.x keeps working, so v2 does not carry a deploy for the 1.x project layout (Dockerfile + .astro/config.yaml). A 1.x-layout project is converted with astro init, or deployed with Astro CLI 1.x.

What a directory is, and where the deploy routes, comes from one walk, utils.Locate. It is used by astro deploy, astro remote deploy, APC's deploy, and the bundle-path checks of --non-dags and astro dbt deploy; it carries the manifest it loaded, so routing reads each file once. From the working directory up, at each directory:

  • a project root (utils.IsManifestRoot) stops the walk;
  • else a Dockerfile beside .astro/ is a 1.x project and stops it, except in the home directory, whose .astro holds the global config;
  • else the walk goes on up.

A root is a pyproject.toml whose [tool.astro] loads or fails to validate, one that fails to parse only if its text declares a tool.astro table, or one that exists but cannot be read. The last matches project.HasManifest, so the deploy reports the read error. A tooling-only pyproject.toml does not stop the walk, and a directory that cannot be looked in holds neither. Each ancestor's pyproject.toml is read once.

Behaviour

Astro (astro deploy)

where any mode except --non-dags --image-name <ref>
pyproject.toml project root the manifest deploy, unchanged project deploy with a prebuilt image, dags/ included
in or below a 1.x project refused (no_project): this project uses the Astro CLI 1.x layout (a Dockerfile and .astro/config.yaml), and Astro CLI v2 deploys only pyproject.toml projects. Convert it with astro init, or deploy it with Astro CLI 1.x; from below: this directory is inside a project at <dir> that uses …. Convert it with astro init in <dir>, … refused the same way: a prebuilt image from a 1.x checkout (deploy-action --image-name X --force) would silently leave its DAGs stale
below a pyproject.toml project root refused (no_project): this directory is inside the project at <dir>. Run the deploy from the project directory, <dir> refused the same way
no project refused (no_project): this is not an Astro project directory. Change to an Astro project directory, or run astro init … (in ~, no init advice) image-only deploy: no dags/, no git commit; target by id (argument / --deployment) or the workspace pick; non-interactive with no target: pass the Deployment id as the argument or with --deployment. Always warns, on stderr and in the result's new warnings field: with DAG deploys, that the Deployment keeps its DAGs; without, that it runs the DAGs inside the image (APC's wording)

--non-dags runs from anywhere, before the walk.

  • Output: --output is validated (a bad value is a usage error, exit 2). -o json publishes one object (new golden deploy-non-dags.json), and what the platform prints on the way (the git note) goes to stderr.
  • Login, chosen as astro deploy <id> from the same directory chooses it. In a pyproject project here whose manifest loads, the project's login (its host) is used for everything: ids, links and the picker. Outside a project, or when the manifest does not load, the current context is used for everything, so a broken pyproject.toml does not stop an id-targeted deploy.
  • Target: the argument or --deployment, which must agree. With no name, a run that cannot be asked (-o json, no terminal) gets the same input_required refusal as astro deploy. One that can is offered the picker (--workspace / --workspace-id / the context) only on the current context's host; on another host it is told to pass --deployment <id> or switch context.
  • Containment: the bundle-path check is the walk, shared with astro dbt deploy.

Flag combinations, refused as usage errors before anything is read: --wait-time without --wait (all modes, as 1.x did); --no-dags-base-dir with a deploy that ships no DAGs (--image, or --image-name outside a project; at a project root --image-name ships the project's DAGs, so the flag applies there); --dags with --image / --image-name names only the flags given.

astro remote deploy follows the same walk: build at a pyproject root; --image-name at a root or outside any project; refused in/below a 1.x project and below a root.

Astro Private Cloud (astro deploy under an APC context)

where build (no --image-name, or --image-name=) --image-name / --remote --dags
pyproject.toml project root usage error (exit 2): Astro CLI v2 cannot build and deploy projects to Astro Private Cloud yet: use Astro CLI 1.x for now. Support for pyproject.toml projects on Astro Private Cloud is coming. … image, then the project's dags/ to a Deployment that takes uploads uploads the project's dags/
in or below a 1.x project refused (no_project): … uses the Astro CLI 1.x layout (…), which Astro CLI v2 does not deploy to Astro Private Cloud. Deploy it with Astro CLI 1.x same same
below a pyproject.toml project root refused (no_project): run from the root same same
no project refused (no_project): … use Astro CLI 1.x for now. To deploy an image you built yourself, pass --image-name image alone: no dags/ upload and no uncommitted-changes check (nothing is read from the directory); when the Deployment takes DAG uploads (or that could not be read) it says the DAGs were not updated, whatever show_warnings is refused: no-project advice

--dags with --image-name is refused as a usage error, in Astro's words: --dags deploys only your DAGs; drop --image-name.

The target is the argument or the picker. The project.deployment a 1.x .astro/config.yaml saved is no longer read (a test pins a stale one being ignored), and astro config set / get project.deployment say it was removed (the existing removed-config-key convention; the key stays registered, as dev.mode does). astro config list leaves out every removed key, so list and get agree. Every refusal comes before Houston is asked anything.

Removed code

deploy.Deploy and everything only it reached (image build, .dockerignore rewriting, pytest/parse steps, the v1alpha1 named-bundle create, ValidRuntimeVersion, WarnIfNonLatestVersion, finalizeDeploy, deployDags); APC's Dockerfile build and FROM validation; airflow.DAGChecker, DAGCheck + mock, ProjectNameUnique, ImageHandler.Pytest; docker.GetImageTagFromParsedFile; fileutil.GetFilesWithSpecificExtension; util.Exists; ansi.Red; utils.EnsureProjectDir / EnsureDockerfileProjectDir (replaced by utils.Locate + utils.NoDeployableProject, plain text); config.IsWithinProjectDir and isWithinManifestProject (the bundle deploys' containment checks are the walk now); unused testfiles. Net: 70 files, +2297 / −4534.

Removed flags (tombstoned in cmd/removed_flags.go, under: deploy)

flag tree message points to
--save, -s Astro naming the Deployment / --deployment; astro link add + default = true to preselect
--save, -s APC the argument
--pytest, --test (-t), --env (-e) Astro uv run pytest && astro deploy
--parse Astro astro local check && astro deploy
--dags-path Astro astro deploy --dags from the project
--dag-bundle-name Astro not supported yet; Astro CLI 1.x
--deployment-name, -n Astro the argument or --deployment
--no-cache APC Astro CLI 1.x (v2 builds no image for APC)

--force (-f) and --prompt (-p) stay on Astro deploy, hidden and read by nothing. astronomer/deploy-action v0.16.0 passes --force on every deploy (TestDeployManifestDeployActionInvocations), and a pyproject project's deploy always accepted both.

Carried over from #2290 (superseded)

#2290 is superseded by this PR. Carried over: config.IsHomeDir in IsProjectDir (with its test); the home-directory advice; reading --image-name by value (--image-name= is a build). Dropped as moot: EnsureDockerfileProjectDir, the APC --save guard, the APC --dags acceptance logic.

Merge-tree against the open PRs (at 09cbaad)

PR result
#2285 init-keep-apc-dockerfile clean; combination builds, cmd, cmd/astro, cmd/apc, cmd/local, cmd/utils tests pass
#2291 removed-commands-and-bundle-output clean; combination builds, cmd (both removed-flag guards), cmd/astro, cmd/apc, cmd/utils tests pass
#2290 project-dir-advice-core conflicts in cmd/apc/deploy.go, cmd/astro/deploy.go, cmd/astro/deploy_test.go, cmd/astro/remote_test.go, cmd/utils/utils.go, cmd/utils/utils_test.go, config/config.go, config/config_test.go (superseded)
#2294 render-backticks conflicts in airflow/docker.go, cmd/apc/deploy.go, cmd/astro/deploy.go, cmd/removed_flags.go, internal/platform/astro/deploy/deploy.go; take this side (already plain)
#2293 deploy-ships-project-files conflicts in cmd/apc/deploy.go, cmd/astro/deploy.go, cmd/astro/testdata/schema/deploy.json, internal/deploy/deploy.go, internal/platform/astro/deploy/manifest_image.go (both add to the deploy result and its routing; the golden needs both fields)

Docs

docs/deploy.md (the walk, a per-location table, --image-name outside a project, --non-dags, flag combinations, remote deploy, an APC section), docs/upgrading-from-v1.md ("v2 requires converting your project" near the top, removed flag rows, project.deployment gone), docs/install.md, README.md, docs/architecture.md.

Checks

go build ./..., GOOS=windows go build ./..., GOOS=windows go vet (touched root packages; e2e with -tags e2e), make test, make test-e2e (tier 0), make lint (no autofix changes), make lint-submodules, make lint-e2e, make lint-goos, make deadcode (clean), make update-schemas (deploy-non-dags.json new; deploy.json gains warnings).

Tests cover:

  • refusals on both platforms in text and -o json with their kinds: at a root, below it, in/below a 1.x project (incl. --image-name), and in ~;
  • the walk: monorepo tooling pyproject above a 1.x project → 1.x; ~ with pyproject + Dockerfile + .astro → not 1.x; non-ASCII names; an unreadable ancestor;
  • --image-name outside a project: no DAGs, the DAG-deploy warning, the no-target message;
  • the flag-combination guards, --non-dags output / agreement / links / id with a broken manifest, the stale saved APC deployment, config get/set project.deployment, remote deploy's rule, and a case per removed-flag entry.

e2e tier 0: TestDeployRefusesThe1xOnlyFlags, TestDeployRefusesA1xProject, TestDeployOutsideAProject, TestDeployImageNameOutsideAProject (outside: past the project check to the container engine, offline; 1.x: refused, no_project), TestAPCDeployRefusesToBuild (against a refusing Houston).

Open questions

  • --force on Astro deploy: drop it once deploy-action stops passing it?
  • cosmos_boost.pre_deploy now affects only bundle and dbt deploys.

v2 deploys only projects with a pyproject.toml, on Astro and on Astro
Private Cloud. Astro CLI 1.x keeps deploying the projects it made, so v2
drops the 1.x deploy rather than carrying it.

Astro: astro deploy in a 1.x-layout project (a Dockerfile beside .astro,
or a .astro/config.yaml) fails with no_project, saying to convert it with
astro init or deploy it with Astro CLI 1.x. Outside any project it gives
the no-project advice, now also no_project. --non-dags runs first, from
anywhere: it deploys a separate directory, and from a pyproject.toml
project it no longer runs a whole project deploy instead.

APC: v2 builds no project. A deploy without --image-name is refused as a
usage error saying to use Astro CLI 1.x (and, from a pyproject.toml
project, that support is coming). --image-name (and --remote) deploy
from anywhere but a 1.x project; --dags uploads a pyproject.toml
project's dags/. Any deploy from a 1.x project is refused.

The 1.x deploy is deleted with its tests: deploy.Deploy and its build,
pytest and parse steps, the named DAG bundle create, APC's Dockerfile
build, airflow's DAGChecker and ImageHandler.Pytest, and what deadcode
found behind them. The flags only it read are removed and tombstoned in
the removed-flags registry: --save, --pytest, --env, --test, --parse,
--deployment-name, --dags-path and --dag-bundle-name on Astro, --save and
--no-cache on APC.

From #2290: config.IsHomeDir in IsProjectDir, the home-directory advice,
and the bound-value --image-name check.

Co-Authored-By: Claude <noreply@anthropic.com>
@schnie
schnie requested review from a team as code owners October 9, 2026 22:02
@coveralls-official

coveralls-official Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

Coverage Report for CI Build 38055987394

Coverage decreased (-0.2%) to 57.503%

Details

  • Coverage decreased (-0.2%) from the base build.
  • Patch coverage: 38 uncovered changes across 5 files (401 of 439 lines covered, 91.34%).
  • 17 coverage regressions across 4 files.

Uncovered Changes

File Changed Covered %
cmd/astro/deploy.go 194 176 90.72%
internal/deploy/deploy.go 33 23 69.7%
cmd/utils/utils.go 103 97 94.17%
internal/platform/astro/deploy/bundle.go 4 1 25.0%
cmd/astro/dbt_render.go 12 11 91.67%
Total (14 files) 439 401 91.34%

Coverage Regressions

17 previously-covered lines in 4 files lost coverage.

File Lines Losing Coverage Coverage
internal/platform/astro/deploy/deploy.go 10 84.94%
airflow/docker_image.go 3 79.76%
internal/platform/astro/deploy/bundle.go 3 76.75%
cmd/astro/deploy.go 1 85.77%

Coverage Stats

Coverage Status
Relevant Lines: 68722
Covered Lines: 39517
Line Coverage: 57.5%
Coverage Strength: 243.19 hits per line

💛 - Coveralls

schnie and others added 4 commits October 9, 2026 18:26
From review of the first commit:

- astro deploy --image-name runs from any directory again, a 1.x project's
  included: it reads nothing from the project. Outside a pyproject.toml
  project it ships the image alone (no dags/), to an id named by the
  argument or --deployment, or the workspace's pick.
- --non-dags validates and honours --output (one json object, pinned as
  deploy-non-dags), names its target as a deploy does (link or id, the
  argument and --deployment must agree) and reads --workspace.
- APC deploy no longer falls back to the project.deployment a 1.x
  .astro/config.yaml saved; a build outside any project is no_project, as
  on Astro; the 1.x refusal no longer talks about building.
- What a directory is comes from project.Discover, so a deploy below a
  1.x project names it, and one below a pyproject.toml project says to run
  from its root. utils.Is1xLayout and EnsureProjectDir are gone; one
  helper gives every refusal, in plain text.
- astro remote deploy follows the same rule, with --image-name anywhere.
- astro deploy --prompt is removed and tombstoned. --force stays, hidden
  and read by nothing, because astronomer/deploy-action passes it.
- The unreachable errNoImageName check and ansi.Red go.

Co-Authored-By: Claude <noreply@anthropic.com>
…latforms

From the second review:

- --image-name follows one rule on Astro and APC: refused in or below a
  1.x project and below a pyproject.toml project's root, as every deploy
  there is; the project's deploy at its root; and outside any project the
  image alone. There, Astro warns when the Deployment takes DAG deploys
  (stderr, and a warnings field in the deploy result), and APC uploads no
  dags/ and says the DAGs were not updated.
- utils.Locate is the one walk astro deploy, remote deploy and APC deploy
  decide by: a pyproject.toml with [tool.astro] stops it, a tooling-only
  one does not, a Dockerfile beside .astro is a 1.x project except in the
  home directory, and an unreadable directory holds neither. No
  project.Discover or hostname derivation.
- The flag combinations 1.x refused come back where they apply:
  --wait-time without --wait, --no-dags-base-dir with a deploy that ships
  no DAGs, and --dags with --image/--image-name naming only those given.
- --non-dags takes a Deployment id without reading the manifest, and a
  link under the login for the project's host.
- An image-only deploy with no target asks for an id, not a project link.
- astro config get and set refuse project.deployment, which nothing reads.
- The APC --no-cache tombstone points at Astro CLI 1.x only.

Co-Authored-By: Claude <noreply@anthropic.com>
- --prompt and -p stay on astro deploy, hidden and read by nothing, as a
  pyproject.toml project's deploy always took them; the tombstone goes.
- utils.IsManifestRoot is the one test for a project's root, and the deploy
  routes on Locate: a pyproject.toml that fails to parse is a root only when
  its text declares tool.astro, one that cannot be read is a root (as
  project.HasManifest has it), and a directory that cannot be looked in is
  passed over. Each ancestor's pyproject.toml is read once, and
  NoDeployableProject takes the walk's answer instead of walking again.
- astro config list leaves out the removed keys get and set refuse.
- --non-dags from a project deploys a bare Deployment id under the
  project's host, as a link already was; its bundle-path check is the walk.
- An --image-name deploy outside a project to a Deployment without DAG
  deploys warns that it runs the DAGs inside the image.
- APC: an image deployed alone from outside a project skips the
  uncommitted-changes check, and --dags with --image-name is refused in
  astro deploy's words.

Co-Authored-By: Claude <noreply@anthropic.com>
- --non-dags under --output json sends what the platform prints (the git
  note) to stderr, so stdout is the one result object.
- --non-dags naming no Deployment is refused with input_required when the
  run cannot be asked, as astro deploy is, instead of opening the picker.
- Its login follows astro deploy from the same directory: the project's,
  for ids, links and the picker, in a pyproject.toml project that loads;
  the current context's anywhere else. The picker is offered only on the
  current context's host.
- astro dbt deploy's containment check is the walk too, so it and
  --non-dags agree; config.IsWithinProjectDir and isWithinManifestProject
  go.
- The walk carries the manifest it loaded (or why it did not), which the
  deploy and --non-dags use instead of loading it again, and looks the home
  directory up once.
- nonDagsDeployJSON is built from the result directly; cmp.Or replaces a
  local helper.

Co-Authored-By: Claude <noreply@anthropic.com>

This branch has not been deployed

No deployments
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