Skip to content

Deploy only pyproject.toml projects in v2 - #2295

Merged
schnie merged 9 commits into
v2from
deploy-requires-manifest
Oct 14, 2026
Merged

schnie merged 9 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 1.x project stops it, by scaffold.Is1xProject (Refuse to convert a 1.x project under an APC context, and give the same advice everywhere #2285), the one definition astro init and the local commands' advice use: a Dockerfile and a .astro/config.yaml that is not the CLI's own settings file. A stray ~/Dockerfile beside the global config is not one; a home directory that is a 1.x project is;
  • else the walk goes on up.

A root is what scaffold.HasManifest (and so astro init's walk, scaffold.Find1xProject) calls one: a pyproject.toml whose [tool.astro] loads, or one that fails to validate, to parse or to be read, so the deploy reports why. Only a pyproject.toml that parses with no [tool.astro] (a monorepo root's [tool.ruff]) is passed over. Each ancestor's pyproject.toml is loaded once: the walk uses scaffold's two halves, ManifestPresent and Has1xLayout, rather than Is1xProject, which would load it again.

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.

Merged with v2 (#2285, #2291, #2297, #2300–#2310, #2306)

A merge commit, not a rebase. Conflicts and how they were resolved:

file v2 wanted this PR wanted kept
cmd/astro/deploy.go #2305: only the default link preselected, deployPreselect gone, a note when ASTRO_DEPLOYMENT is ignored; #2301: ListDeployments and ResolveUnlinked returning a Deployment; #2306: the engine memo, dags_in_image / remote_execution in the result warnings in the result, errPickerOtherHost, no git / 1.x path v2's prompt and signatures, both sides' result fields, this PR's errPickerOtherHost in ResolveUnlinked. The ASTRO_DEPLOYMENT note returns early with no manifest (an --image-name deploy outside a project), where it would have dereferenced nil
internal/platform/astro/deploy/manifest_image.go DagsInImage, RemoteExecution DagDeployEnabled all three
internal/platform/apc/deploy/deploy_test.go #2297 made the Dockerfile-build failure test portable that test removed with the build removed
docs/deploy.md the unlinked Deployments in the prompt, linked once picked the workspace flow, no "1.x path" v2's text without "the 1.x path's"
docs/upgrading-from-v1.md the bundle delete bullet; "astro deploy still deploys an unconverted 1.x project" "astro deploy needs it converted too" the bundle bullet and this PR's line

One semantic fix the merge needed: #2306 refuses an image-only deploy to a Deployment with DAG deploys off (--image, as 1.x did). An --image-name deploy from outside any project is also image-only, and this PR deploys it there with the warning that the Deployment runs the image's DAGs. ImageAlone now reaches the transport (ImageDeploy.ImageAlone, ManifestImageDeployInput.ImageAlone), and checkDeployment refuses only a project's --image. Test: TestDeployManifestImage_DagDeployOffTakesAnImageFromOutsideAProject (fails without the fix); docs/deploy.md's DAG-deploys-off table gains the row.

The walk now uses scaffold.Is1xProject

utils.Locate's own rule (has1xLayout(d) && !isHome(d)) is replaced by scaffold.Is1xProject(d); homeMatcher and has1xLayout are gone. The deploy's refusal and init's/the local commands' 1.x advice now agree, which they did not on two layouts: a home directory that is a 1.x project (HOME=/usr/local/airflow), which init refused and the deploy took for no project, and a Dockerfile beside a .astro/ with no config.yaml, which the deploy refused as 1.x and init treats as a fresh directory. TestLocateAndInitAgreeOn1xProjects runs Locate and scaffold.Find1xProject over the same layouts; the home tests now use a real settings file, plus a home-that-is-1.x case. The 1.x fixtures in cmd/astro (dbt, --non-dags) write .astro/config.yaml with a project: key. A follow-up commit (7b75665) closes the last difference: Locate passed over a pyproject.toml that failed to parse unless its text declared tool.astro, where Find1xProject stops at it. In a 1.x checkout with a broken tooling pyproject.toml the deploy refused as 1.x while init reported the manifest; now both stop there and the deploy reports the parse error (the declaresToolAstro heuristic is gone). It also loads each pyproject.toml once per level, through scaffold.ManifestPresent and scaffold.Has1xLayout, newly exported halves of HasManifest and Is1xProject. TestLocateAndInitAgreeOn1xProjects covers the broken-pyproject.toml layouts.

Review fixes (667fbd5)

  • An --image-name deploy outside a project that picks a Deployment from the workspace saves no link: there is no pyproject.toml (linkedTarget with no manifest). TestRun_ImageAloneOutsideAProjectLinksNothing: no save attempted, no warning.
  • DeployManifestImage reports RemoteExecutionEnabled, so an image shipped alone to a remote-execution Deployment with DAG deploys off is told its DAGs are its remote agents', not "runs the DAGs inside the image". TestDeployImageNameOutsideAProject covers DAG deploys on/off × remote execution on/off and asserts ImageAlone reaches the transport.
  • Request.Warn appends to the result's warnings as it prints to stderr, so -o json carries every deploy warning (Keep secrets out of shipped images, and deploy to DAG-deploy-off Deployments #2306's DAG checks, the build checks, a link not saved), not only the image-alone one. TestDeployManifestJSONCarriesTheTransportsWarnings. make update-schemas: no change, the shape is the same.

Gates after the merge: go build ./..., GOOS=windows go build/vet for the root module, every pkg/* module and e2e (-tags e2e); make test; make test-submodules (only pkg/airflowrt's TestHasProxyConfigured fails, which reads this Mac's system proxy through scutil, as #2306 noted); make lint, make lint-submodules, make lint-goos, make lint-e2e, make deadcode, make fmt; make test-e2e (tier 0); make update-schemas (no diff: deploy.json has dags_in_image, remote_execution and warnings; deploy-non-dags.json unchanged); go test -race -shuffle=on -count=3 ./cmd/... ./internal/project/... ./internal/deploy/... ./internal/platform/astro/deploy/....

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

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).

Follow-ups

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 38587833118

Coverage decreased (-0.2%) to 57.896%

Details

  • Coverage decreased (-0.2%) from the base build.
  • Patch coverage: 43 uncovered changes across 5 files (400 of 443 lines covered, 90.29%).
  • 19 coverage regressions across 6 files.

Uncovered Changes

File Changed Covered %
cmd/astro/deploy.go 211 184 87.2%
internal/deploy/deploy.go 49 39 79.59%
internal/platform/astro/deploy/bundle.go 4 1 25.0%
cmd/utils/utils.go 64 62 96.88%
cmd/astro/dbt_render.go 12 11 91.67%
Total (14 files) 443 400 90.29%

Coverage Regressions

19 previously-covered lines in 6 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 84.99%
cmd/utils/utils.go 1 96.3%
internal/project/apc.go 1 70.67%

Coverage Stats

Coverage Status
Relevant Lines: 69398
Covered Lines: 40179
Line Coverage: 57.9%
Coverage Strength: 255.49 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>
schnie and others added 4 commits October 14, 2026 10:51
Brings in #2285, #2291, #2297, #2300-#2310 and #2306.

- cmd/astro/deploy.go: v2's prompt (#2305: the default link alone
  preselected, no deployPreselect, the ASTRO_DEPLOYMENT note), the engine
  memo and ListDeployments (#2301, #2306), and the deploy result's
  dags_in_image and remote_execution beside this branch's warnings. The
  unlinked pick takes v2's signature and this branch's errPickerOtherHost.
  The ASTRO_DEPLOYMENT note returns early outside a project, where there is
  no manifest.
- manifest_image.go: both sides' result fields. #2306 refuses an image-only
  deploy to a Deployment with DAG deploys off; an --image-name deploy from
  outside any project (ImageAlone) is let through, as this branch intends,
  with its warning that the Deployment runs the image's DAGs.
- apc/deploy_test.go: this branch's tag-and-push test; the Dockerfile build
  test #2297 made portable is gone with the build.
- docs: v2's prompt and bundle-delete text, this branch's "deploy needs a
  converted project", and a row for --image-name outside a project with DAG
  deploys off.

Co-Authored-By: Claude <noreply@anthropic.com>
utils.Locate had its own 1.x rule: a Dockerfile beside a .astro directory,
except in the home directory. astro init and the local commands' advice use
scaffold.Is1xProject (#2285): a Dockerfile and a .astro/config.yaml that is
not the CLI's own settings file. The two disagreed on a home directory that
is a 1.x project (HOME=/usr/local/airflow in a 1.x image), which init
refused and the deploy took for no project, and on a Dockerfile beside a
.astro with no config.yaml, which the deploy refused as 1.x and init would
convert as a fresh directory.

Locate now asks scaffold.Is1xProject, and homeMatcher and has1xLayout go.
TestLocateAndInitAgreeOn1xProjects runs both walks over the same layouts.
The 1.x fixtures in cmd/astro write a .astro/config.yaml with a project key.

Co-Authored-By: Claude <noreply@anthropic.com>
…oject.toml once

Locate passed over a pyproject.toml that failed to parse unless its text
declared tool.astro, where scaffold.Find1xProject stops at any manifest
scaffold.HasManifest sees, broken ones included. In a 1.x checkout with a
broken tooling pyproject.toml the deploy refused it as 1.x while init
reported the manifest. Locate's root is now scaffold.HasManifest's answer,
and the deploy reports that file's error; the declaresToolAstro heuristic
goes.

scaffold exports Has1xLayout and ManifestPresent, the two halves
Is1xProject and HasManifest are made of, so the walk loads each
directory's pyproject.toml once and keeps scaffold the one definition.
TestLocateAndInitAgreeOn1xProjects gains the broken-pyproject layouts.

Co-Authored-By: Claude <noreply@anthropic.com>
…deploy warning in -o json

- An --image-name deploy outside a project that picks a Deployment from the
  workspace no longer tries to save a link: there is no pyproject.toml to
  save it into (linkedTarget with no manifest).
- DeployManifestImage reports whether the Deployment runs remote
  execution, so an image shipped alone to a remote-execution Deployment
  with DAG deploys off is not told it runs the DAGs inside the image.
- Request.Warn appends to the result's warnings as it prints them, so
  -o json carries every warning text prints on stderr (#2306's DAG checks,
  the build checks, a link not saved), not only the image-alone one.
- TestDeployImageNameOutsideAProject asserts ImageAlone reaches the
  transport.

Co-Authored-By: Claude <noreply@anthropic.com>
@schnie
schnie merged commit 4070d4e into v2 Oct 14, 2026
6 of 7 checks passed
@schnie
schnie deleted the deploy-requires-manifest branch October 14, 2026 15:57
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