Skip to content

Refuse to convert a 1.x project under an APC context, and give the same advice everywhere - #2285

Open
schnie wants to merge 25 commits into
v2from
init-keep-apc-dockerfile
Open

schnie wants to merge 25 commits into
v2from
init-keep-apc-dockerfile

Conversation

@schnie

@schnie schnie commented Oct 9, 2026 •

Copy link
Copy Markdown
Member

What

There is one project format, and astro init converts a 1.x project to it the same way on every platform. This PR adds the one exception, for now: Astro Private Cloud. APC deploys the 1.x layout (a Dockerfile and .astro/config.yaml, with Astro CLI 1.x) and not yet pyproject.toml projects. The PR also makes every hint about a 1.x project agree with that exception.

The refusal, in pkg/scaffold

scaffold.Plan and scaffold.Run refuse when scaffold.Options.DeploysToAPC is set and the target directory is a 1.x project or lies inside one. They refuse before anything is read or written. The reason for covering directories inside: a project scaffolded inside one (for example astro init dags) would be deployed with it by Astro CLI 1.x.

How the target is matched:

  • The walk. scaffold.Find1xProject goes up from the cleaned absolute target to the filesystem root. It stops at any directory that has a manifest, whether it loads or not, and checks that before the 1.x layout, so each step loads the manifest once. Plan then reports that manifest as it always has.
  • What counts as a 1.x project. scaffold.Is1xProject, the one definition (the CLI's discovery uses it too), follows what Astro CLI 1.x itself takes for a project. 1.x's config.IsProjectDir (origin/main config/config.go:322) requires .astro/config.yaml, and its deploy requires that through utils.EnsureProjectDir (cmd/utils/utils.go:25). The rule here is:
    • a Dockerfile and a .astro/config.yaml, with no manifest (scaffold.HasManifest);
    • unless that config.yaml is the CLI's own settings file. That means a YAML mapping with no top-level project key, and at least one of context, contexts or telemetry (keys from v2's config.CFG). A 1.x project's always has project (cmd/airflow.go:677 and :680).
    • 1.x tells its home directory apart by path; this tells the settings file apart by content, so a stray ~/Dockerfile beside the settings does not make home a 1.x project, while a home directory that is one (HOME=/usr/local/airflow in a 1.x image) is refused.
    • An empty or unparseable config.yaml counts, as it does for 1.x (fail closed). A missing one does not.
    • A tools-only pyproject.toml does not rule a project out, so a monorepo root does not hide it.

API (additions only; Plan and Run keep their by-value signatures)

  • scaffold.Options.DeploysToAPC bool. A caller whose context or platform is APC sets it. The default, false, is today's behaviour.
  • scaffold.ErrConvert1xUnderAPC, the sentinel for errors.Is.
  • *scaffold.Convert1xUnderAPCError{Dir}, the error value. Dir is the 1.x project's directory, and the message is scaffold.Project1xUnderAPCMessage(Dir).
  • scaffold.Is1xProject(dir). internal/project.Is1xProject delegates to it, and project.Project1xUnderAPC delegates to the message function, so there is one check and one message.
  • scaffold.Find1xProject(dir), the walk.
  • scaffold.Project1xUnderAPCReason(dir), the shared why-it-stays part of the message.
  • scaffold.SwitchToAstro, the one "switch to Astro" text: astro context switch astronomer.io, or astro login astronomer.io. A bare astro login logs back in to the current context, which is APC's.
  • scaffold.HasManifest(dir). internal/project.HasManifest uses it.

The message

Plain text, naming the 1.x project's directory:

APC deploys that layout and not yet pyproject.toml projects. Leave the project as it is for now: Astro CLI 1.x keeps deploying it to Astro Private Cloud, and converting it will be available once Astro Private Cloud deploys pyproject.toml projects. To convert it anyway, for Astro or for local development only, switch to an Astro context first (astro context switch astronomer.io, or astro login) and run astro init in

.

This holds with or without #2295.

One decision for every hint

project.Convert1xBlocked(dir) returns why init refuses (BlockedUnderAPC, BlockedUnresolved, or NotBlocked) and the 1.x project's root. It checks the context first, so under Astro it walks nothing: init walks nothing there. A hint that names the root under Astro walks for itself. The dev stub calls scaffold.Find1xProject, and Discover tracks the root in its own walk, since no pyproject.toml lies on that path.

NoAstroSectionError works the same way:

  • Where init refuses, it says what init says.
  • Elsewhere it looks for a 1.x project only between the start and the pyproject's directory. A monorepo root's error names the 1.x project between them, while a plain package's pyproject.toml inside a 1.x tree gets the add-[tool.astro] hint, as on v2. It is built on scaffold.Find1xProject and the one project.SetContext that cmd.Execute records. Every hint asks it, and discovery's errors keep the answer when they are made, so Error() only formats:
  • astro init;
  • discovery's NotFoundError and NoAstroSectionError, so astro local start and the rest, astro local check and Otto;
  • the astro dev stub.

So no hint suggests astro init where init refuses. Every message (project.Blocked1xMessage, and the Astro hints) names the 1.x root and ends "then run astro init in ":

  • under an unresolved context it says to fix or switch the context;
  • where ASTRO_DOMAIN chose the context, it says to change or unset ASTRO_DOMAIN instead. The table test TestTheDevStubAndInitAgree covers five contexts (APC, APC via ASTRO_DOMAIN, Astro, unresolved, unresolved via ASTRO_DOMAIN), each at a 1.x root, a subdirectory, and a fresh directory. It checks that the stub and init agree, that the stub's note is init's own message, and that v1_dir is the root.

The CLI

Kind and exit code. The refusal is not a mistake in the command line, so it is not usage. It publishes a new documented kind, unsupported_on_platform (exit 1), listed in docs/architecture.md. Execute's unknown-command tracking runs only for usage errors, so it does not run here.

Platform detection (contextPlatform in cmd). cmd.Execute sets it once, on the real CLI path only, through project.SetUnderAPC and project.SetContextUnresolved. The cases:

Situation Result
ASTRO_DOMAIN names an Astro domain Astro, before anything else, even with an unreadable config
No context named, by the home config or ASTRO_DOMAIN (a fresh machine) Astro: converts as today
A context that resolves APC unless its domain is Astro's
A home config that cannot be read (new config.HomeConfigUnreadable) unresolved; the message names the file and says to fix or move it, never to change the context
A named context the config does not hold, for a domain that is not Astro's unresolved

contextPlatform reads only the current domain and whether a context for it is saved (config.Context.ContextExists), never a login, so startup never reaches the keyring; a test pins that. Where ASTRO_DOMAIN chose an APC context, the advice is to set it to an Astro domain (astronomer.io). It adds "or unset it" only when the saved current context is Astro, or there is none (Context.UnsetIsAstro).

When the context is unresolved, astro init refuses the same directories, with the same kind. The message names the 1.x project as the reason, and says to fix the context or switch to one (astro context list, then astro context switch), then rerun astro init in the directory given. Desktop's API stays the one bool.

Notice. A successful init under APC, or under an unresolved context, prints one line on stderr, in text mode only (project.NewProjectNotice). It is worded for that context, and for whether ASTRO_DOMAIN chose it. A successful init always means it wrote a new manifest (created, or [tool.astro] adopted into a pyproject.toml). The notice is not in Result.Notes and not in json.

No double walk. On the CLI path, init has already refused by the time scaffold runs, so it does not set opts.DeploysToAPC; that field is for Desktop.

The astro dev stub. It finds the nearest enclosing 1.x root in every context. v1_project now means inside a 1.x project, and the new v1_dir names its root. The golden gains v1_dir and context_unresolved.

  • Under Astro, the convert advice names the root. From below it, the convert command is astro init <root>, in convert and in the text alike. The path is quoted by scaffold.ShellQuote, which astro config's suggestions now share: POSIX single quotes where quoting is needed, with backslashes safe on Windows, so an ordinary path such as C:\proj prints bare. astro dev init's replacement stays plain astro init, and the text offers both, naming the convert command once: "Use astro init to make a new project here, or astro init <root> to convert the 1.x project at in place."
  • astro local init, astro af init and astro airflow init redirect to astro init. Where init refuses, they give its refusal instead.
  • Where init refuses, it names no replacement and its text lists no astro local commands; the notes, which are init's message, are the whole answer.
  • Its json then has under_apc or context_unresolved set, the same notes, and an empty mapping.

Scope

cmd/root.go, cmd/local/root.go and cmd/local/deps.go are as on v2.

Possible follow-up: the parser fixes as their own PR. Two fixes were reverted here: floating /runtime tags read as Airflow 3, and a pin-only Dockerfile's Python flavor carried into requires-python. Review found points such a PR would need to address:

  • write requires-python from the tag only when the Dockerfile is retired;
  • handle bare runtime: / astro-runtime: names;
  • decide how mirrors are treated, since Docker mode accepts only Astronomer's registries;
  • never read a non-3 /runtime tag as Airflow 2.

Known and inherited

  • The context snapshot. cmd.Execute records the context once, before the tree runs. The one pre-run that changes the context afterwards is followProjectPreRun (cmd/astro/project_scope.go), which sets ASTRO_DOMAIN to a project's Astro host. It runs only on Astro-tree commands, and it switches from Astro to Astro, so the platform never changes. astro init, the dev stub and the local commands skip the pre-runs entirely. So the snapshot cannot go stale for any decision made here.
  • Deploy only pyproject.toml projects in v2 #2295 alignment. Deploy only pyproject.toml projects in v2 #2295's utils.Locate has its own 1.x rule (cmd/utils/utils.go:126: has1xLayout(d) && !isHome(d)). scaffold.Is1xProject is exported, so a follow-up changes that line to if scaffold.Is1xProject(d) { and can drop homeMatcher and its own has1xLayout. Is1xProject loads the manifest once more, which is harmless.
  • Fix the project-directory advice #2283 added (core fixes) #2290 conflict. Fix the project-directory advice #2283 added (core fixes) #2290 rewrites internal/project.HasManifest (manifestPresent), and this PR's delegation to scaffold.HasManifest touches the same body. Whichever merges second keeps Fix the project-directory advice #2283 added (core fixes) #2290's validation-error handling inside scaffold.HasManifest, or keeps manifestPresent.
  • A localhost context without local.platform set is read as context.IsCloudDomain reads it.
  • astro init's refusal is unsupported_on_platform (exit 1). Discovery errors carrying the same advice keep no_project (exit 1).
  • Desktop is covered only when it sets scaffold.Options.DeploysToAPC from its platform or context. Plan and Run then refuse a 1.x project, or a directory inside one, under APC with scaffold.ErrConvert1xUnderAPC (the value is a *scaffold.Convert1xUnderAPCError; message: scaffold.Project1xUnderAPCMessage). The message's last step names astro init, a CLI command. An unresolved context is the CLI's own handling; Desktop decides its own.

Follow-ups (not in this PR)

  • A tools-only pyproject.toml below a 1.x root: discovery stops there, while the stub's advice walks past it.
  • The kind of blocked discovery errors: no_project, versus unsupported_on_platform for init's refusal.
  • The isCLISettings content heuristic for the home settings file.
  • One shared loop for find1xBetween and scaffold.Find1xProject.
  • cmd.exe and PowerShell quoting of printed paths with spaces or quotes (ShellQuote writes POSIX single quotes; ordinary Windows paths print bare).
  • astro local init and astro af init under APC now give init's refusal: they are no longer counted by the unknown-command tracking (which counts usage errors only), and they exit 1, not 2.
  • test-windows's "Run unit tests" step appears to test only the root package (one coverage line, about 30s), so the cmd and cmd/local suites, TestConfigSuggestionQuotesAValueWithSpaces among them, do not run on Windows.
  • Deploy only pyproject.toml projects in v2 #2295's utils.Locate switching to scaffold.Is1xProject.

Docs

docs/install.md, Step 4, covers the rule, directories inside a 1.x project, the unresolved-context refusal, and the settings-file case. docs/architecture.md lists the new kind.

Tests

  • pkg/scaffold/apc_test.go:
    • Plan and Run refuse a 1.x root: the error type, sentinel, message and Dir are checked, and nothing is written.
    • Existing, nested and new subdirectories are refused, naming the root. An Astro project in between stops the walk. Without the flag, the root converts.
    • The CLI settings file in a home directory with a stray Dockerfile is not a 1.x project, while a home directory that is a 1.x project is refused from below.
    • Is1xProject cases: a settings file with contexts only, one with telemetry only, a project key beside contexts, a config with no project key, a .astro/ with no config.yaml, an unparseable config.yaml, a tools-only pyproject, a manifest, and the partial layouts.
  • cmd/local:
    • The root is refused in text and json, with unsupported_on_platform and exit 1, nothing written, and not usage.
    • astro init from dags/ and astro init fresh inside are refused.
    • The monorepo case and the home-as-project case are refused.
    • An unresolved context refuses a 1.x project and allows a fresh directory.
    • Astro still converts, and a new project under APC has no APC note.
    • TestTheAPCAdviceOn1xProjectsIsOne resets stdout and asserts the json error is the APC advice under APC.
    • The dev stub under APC: no replacement, an empty mapping, and text that is the notes.
    • The kind table and its order are pinned.
  • cmd/contextplatform_test.go: no context, Astro, APC, ASTRO_DOMAIN naming an unsaved APC domain or Astro, and an unreadable home config.
  • e2e/convert_test.go (tier 0): under a real APC context, init and init dags are refused with exit 1 and one unsupported_on_platform object, and nothing is written. A fresh directory succeeds, with no APC note.

Checks run

Not verified

  • No real APC deploy.

… APC

APC's `astro deploy` builds only the 1.x layout: it requires
.astro/config.yaml and builds the Dockerfile, whose runtime base installs
requirements.txt and packages.txt. Converting a 1.x project kept
.astro/config.yaml but retired a FROM-only Dockerfile and the lists it
migrated, so an APC user's next deploy passed the project check and failed
to build.

When the current context is APC (the platform the root mounts, so the same
answer `astro deploy` gets), init now keeps those three files, undeclared,
writes a .dockerignore for that build's context, and says why under Left to
do. The manifest still carries the pin and both lists, and stays a
generated-build project, so Astro users and `astro local` are unchanged.
Declaring the Dockerfile instead was rejected: it would switch Docker mode
and Astro builds to it and drop the generated build for a constraint only
APC's deploy has.

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

coveralls-official Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

Coverage Report for CI Build 38066810338

Warning

Build has drifted: This PR's base is out of sync with its target branch, so coverage data may include unrelated changes.
Quick fix: rebase this PR. Learn more →

Coverage increased (+0.3%) to 57.741%

Details

  • Coverage increased (+0.3%) from the base build.
  • Patch coverage: 34 uncovered changes across 5 files (197 of 231 lines covered, 85.28%).
  • 271 coverage regressions across 14 files.

Uncovered Changes

File Changed Covered %
internal/project/apc.go 75 54 72.0%
cmd/problemkind.go 5 0 0.0%
cmd/local/dev.go 60 57 95.0%
config/config.go 3 0 0.0%
cmd/local/initcmd.go 25 23 92.0%
Total (10 files) 231 197 85.28%

Coverage Regressions

271 previously-covered lines in 14 files lost coverage.

Top 10 Files by Coverage Loss Lines Losing Coverage Coverage
cmd/astro/deployment.go 61 93.01%
internal/platform/apc/deploy/deploy.go 46 90.54%
pkg/fileutil/files.go 42 84.13%
cmd/apc/deployment.go 30 94.69%
cmd/otto.go 27 36.11%
cmd/cliout/format.go 26 83.75%
cmd/astro/organization.go 10 96.79%
cmd/auth.go 9 94.12%
cmd/apc/deploy.go 4 97.47%
cmd/api/api.go 4 88.37%

Coverage Stats

Coverage Status
Relevant Lines: 69644
Covered Lines: 40213
Line Coverage: 57.74%
Coverage Strength: 240.91 hits per line

💛 - Coveralls

…he note agree

Review of the first commit found the APC decision and the deploy link made
apart. With an APC context, a project whose .astro/config.yaml saved a cuid
deployment and workspace kept its 1.x build with an APC note and also got an
Astro link. Houston's ids are cuids too, so that pair cannot say which
platform it names.

Where the project deploys is now settled once, by resolveDeployTarget, and
passed to every decision that depends on it. A saved Software release name
(the 0.x CLI saved one there) means APC whatever the context says. Otherwise
Options.DeploysToAPC, the current context, decides. Under APC the saved
target stays a note instead of an Astro link.

planRetirements now owns the rule. A Dockerfile that will be built, declared
or deployed by APC, keeps requirements.txt and packages.txt, and an APC
project's spent Dockerfile is kept. It returns what it kept for APC alone, so
the post-filter and the apcBuild field on project1x are gone. The option goes
explicitly to the consumers.

A Dockerfile kept for APC whose FROM disagrees with --airflow-version or an
adopted pin is refused, as a declared one is. The note now names only what
was kept for APC, says when the Dockerfile stays for another reason, says a
-base runtime installs neither list, and claims the manifest carries only
what it does.

Also: fileRequirements and filePackages constants. writeContext now shares
writeLogin's YAML, refusing hosts included.

Co-Authored-By: Claude <noreply@anthropic.com>
schnie and others added 3 commits October 8, 2026 23:41
… builds

The saved release-name heuristic is gone: an Astro Deployment's namespace has
the same adjective-noun-NNNN shape, and Houston takes custom release names, so
it misclassified both ways. `astro init --deploy-target astro|apc` decides when
given, else the current context does (no context is Astro). It reaches
scaffold as Options.DeploysToAPC, still the one field a caller must set, with
an optional DeployTargetBasis that every APC note and the version refusal use
to say what decided and how to choose the other platform. The note on a saved
deploy target follows the decision: under APC it says the target stays where
APC's deploy reads it, not how to make it an Astro link.

A Dockerfile kept for APC holds the pin to the exact Airflow series its FROM
carries. An Airflow 2 runtime tag names no series, so the runtime catalog
(Options.RuntimeCatalog, asked only then) supplies it: it becomes the pin read
from the Dockerfile and the fix a refusal names. Without a catalog the
conversion says the series is unknown instead of offering "2". A pin naming
only the generation ("3" beside runtime:3.1-12) is refused under APC.

buildsDockerfile is now the one test for "this Dockerfile will be built",
from one fact (a Dockerfile with content), used by the retirement, the
.dockerignore and the refusal.

Co-Authored-By: Claude <noreply@anthropic.com>
…latform advice followable

- airflowFromDockerfile reads runtime:3.1, runtime:3 and 3.1-python-3.12
  through manifest.ParseRuntimeTag instead of as Airflow 2, so a kept APC
  Dockerfile pins and refuses at its real series.
- apcBuild keeps the series on a manifest.RuntimeTag and disagrees through
  RuntimeTag.Agrees, adding only the generation-only-pin check.
- Options.RuntimeCatalog is the catalog itself: astro init reads it before
  scaffold, only for an APC conversion, so Plan makes no request.
- An unknown Airflow 2 series says whether the catalog was unreadable or does
  not list the runtime, and the note no longer assumes the pin is "2".
- Every undeclared Dockerfile kept under APC is named in the APC note,
  including one beside a pin to a release of its series.
- Advice to convert for the other platform, read after a conversion that a
  rerun refuses, names what to restore first (removed files, pyproject.toml).
- detectRootOptions reads the current context once; hasDockerfile replaces
  the remaining raw length checks.

Co-Authored-By: Claude <noreply@anthropic.com>
…and claim agreement only for an exact series

- One rule decides a runtime image for both the version read from a 1.x
  Dockerfile and the APC pin check: a repository path ending in /runtime
  (the Airflow 3 lineage) or /astro-runtime (the Airflow 2 lineage), on any
  registry, so a private mirror is checked like the image it mirrors.
- Floating Airflow 3 tags are read only on /runtime: astro-runtime:3.0 is
  Astro Runtime 3, an Airflow 2 image.
- The APC note claims pyproject.toml carries the same Airflow version only
  when the pin is the exact series the FROM carries.
- A floating Airflow 2 tag is not looked up in the catalog or blamed on it:
  the conversion says the tag floats and how to pin it.
- astro init reads the runtime catalog only when the APC build needs it
  (scaffold.NeedsRuntimeCatalog, offline): a pin-only Dockerfile naming one
  Airflow 2 runtime build.
- A retired pin-only Dockerfile's Python flavor is carried as
  requires-python, which a generated image honors.

Co-Authored-By: Claude <noreply@anthropic.com>
schnie and others added 2 commits October 9, 2026 16:10
…APC for now

There is one project format. astro init converts a 1.x project to it the
same way whatever the platform, so --deploy-target, the keep-the-1.x-build
branch (its notes, its FROM/pin refusal, the catalog pre-fetch) and the
scaffold.Options fields that carried the decision are gone.

Astro Private Cloud's astro deploy still builds the 1.x layout, so under an
APC context astro init refuses a 1.x project (project.Is1xProject) as a usage
error and writes nothing; the project keeps deploying as it is until APC
deploys pyproject.toml projects. A new project made under an APC context
gets a note that APC cannot deploy it yet.

Kept, since they hold on every platform: runtime images read by repository
(/runtime, /astro-runtime, on any registry), floating Airflow 3 tags read
only on /runtime, a retired pin-only Dockerfile's Python carried as
requires-python, hasDockerfile, and the root's single context read.

Co-Authored-By: Claude <noreply@anthropic.com>
Out of the way of the home-directory refusal on project-dir-advice-fixes,
which goes in right after the directory is resolved.

Co-Authored-By: Claude <noreply@anthropic.com>
@schnie schnie changed the title Keep the 1.x build when astro init converts a project that deploys to APC Convert 1.x projects one way on every platform, and refuse one under APC until APC deploys manifests Oct 9, 2026
schnie and others added 3 commits October 9, 2026 16:26
…same

- Revert the pkg/scaffold parser changes (runtime repository rule, floating
  Airflow 3 tags, Python flavor into requires-python, hasDockerfile) and
  their docs to v2's; they can come back as their own PR.
- Revert cmd/root.go's context read: the platform comes from
  context.IsCloudContext again, and the refusal names no domain.
- project.Project1xUnderAPC is the one account of a 1.x project under an
  APC context: why it stays as it is, that astro deploy on APC keeps working
  with it, when conversion comes, and how to convert anyway (switch to an
  Astro context with astro context switch astronomer.io or astro login, then
  run astro init again). astro init's refusal, the project-discovery errors
  of every local command (through AdviseUnderAPC around each RunE) and the
  astro dev stub all use it, in plain text.
- The new-project note under APC is said by the run that made the project
  only; init again is refused as anywhere.

Co-Authored-By: Claude <noreply@anthropic.com>
Moves TestAdviseUnderAPC up the file so it does not collide with the tests
project-dir-advice-fixes appends.

Co-Authored-By: Claude <noreply@anthropic.com>
So internal/project/project.go's imports stay as v2's and do not collide
with project-dir-advice-fixes, which adds one there.

Co-Authored-By: Claude <noreply@anthropic.com>
@schnie schnie changed the title Convert 1.x projects one way on every platform, and refuse one under APC until APC deploys manifests Refuse to convert a 1.x project under an APC context, and give the same advice everywhere Oct 9, 2026
schnie and others added 3 commits October 9, 2026 16:43
… and subdirectories

- internal/project holds one setting, SetUnderAPC, set once by the root in
  detectRootOptions from the same IsCloudContext check. project1xMessage
  reads it, so every 1.x hint built from the package (discovery errors,
  astro local check, Otto, init, the dev stub) gives Project1xUnderAPC's
  account with no wrapping. AdviseUnderAPC, the RunE wrapping and
  Deps.DeploysToAPC are gone.
- The advice always names the 1.x project's directory.
- astro init under APC refuses a directory inside a 1.x project too
  (Enclosing1xProject, the walk discovery makes), naming the project.
- The astro dev stub under APC in a 1.x project: under_apc and the advice
  in notes (json and text say the same), no astro init as a replacement or
  in the mapping, a note that astro local needs a converted project; only
  the convert hint is gated, so a build secret still means --docker.
  dev-removed schema golden updated for under_apc.

Co-Authored-By: Claude <noreply@anthropic.com>
… on the CLI path only

- project.Project1xAt reads discovery's own answer (NotFoundError's
  Project1xDir, NoAstroSectionError's Dir); astro init's refusal and the
  dev stub under APC both use it. Enclosing1xProject's walk is gone.
- Is1xProject never counts the home directory, or ASTRO_HOME, whose .astro/
  is the CLI's settings.
- The advice says to run astro init in the 1.x project's directory.
- astro init under APC allows a new or empty directory inside a 1.x
  project; it refuses the project itself or an existing non-empty
  directory in it.
- The dev stub under APC names no replacement for a 1.x project; the notes
  carry the guidance, in json and text.
- SetUnderAPC moves to cmd.Execute, so NewRootCmd, which tests build, never
  sets it; cmd/root.go is back to v2's.

Co-Authored-By: Claude <noreply@anthropic.com>
astro init under APC refuses iff the directory it was given is itself a 1.x
project (project.Is1xProject, as on v2). No walking up, no Discover, no
home-directory exception: Project1xAt, isCLIHome and isNewOrEmpty are gone,
and Is1xProject is v2's again. A directory below a 1.x project is allowed;
it never touches the 1.x root's Dockerfile or .astro/.

The dev stub decides 1.x-ness the same direct way under APC as under Astro,
and under APC in a 1.x project gives only the notes: no replacement and no
table of astro local commands.

Co-Authored-By: Claude <noreply@anthropic.com>
schnie and others added 10 commits October 10, 2026 09:31
…ed too

scaffold.Options gains DeploysToAPC. With it set, Plan and Run refuse a
directory that is itself a 1.x project (scaffold.Is1xProject) before
reading or writing anything, with a *Convert1xUnderAPCError that matches
ErrConvert1xUnderAPC and whose message is Project1xUnderAPCMessage(Dir).
internal/project's Is1xProject and Project1xUnderAPC delegate to these, so
there is one check and one message. astro init sets the field from
project.UnderAPC() and maps the refusal to a usage error, as before.

Plan's body moves into plan so its complexity stays under the limit, and
Options goes by pointer between internal helpers now that the field puts it
over hugeParam's size; Plan and Run stay by value for Desktop.

Co-Authored-By: Claude <noreply@anthropic.com>
Reads the 1.x directory inside the context literal rather than on the line
after the signature that branch rewrites, and the APC stub test decodes the
json by key and reads the text from a text run, so it does not name the
payload's Go type, which that branch renames.

Co-Authored-By: Claude <noreply@anthropic.com>
True with or without v2's deploy taking only pyproject.toml projects: the
shared message now says APC deploys that layout and not yet pyproject.toml
projects, and that Astro CLI 1.x keeps deploying it there. The comments and
docs/install.md that said v2's astro deploy works with it say the same.

Co-Authored-By: Claude <noreply@anthropic.com>
…, and publish a kind of its own

- Under DeploysToAPC, scaffold refuses a directory that is a 1.x project or
  lies inside one (walking up, stopping at an Astro project), naming the
  1.x project: a project made inside it would be deployed with it.
- Is1xProject does not count a .astro/config.yaml that is the CLI's own
  settings (no top-level project key, and context, contexts or telemetry),
  so a stray ~/Dockerfile no longer makes home a 1.x project, while a home
  directory that holds one still is.
- The CLI tells Astro, APC and an unresolved context apart
  (contextPlatform: an unreadable home config, or a named context it does
  not hold for a non-Astro domain), and astro init refuses a 1.x project
  under an unresolved one too, saying to fix the context.
- The refusal publishes unsupported_on_platform, exit 1, not usage.
- The new-project note under APC is gone from Result.Notes.
- The dev stub under APC empties its mapping, so json matches text, and
  reads the 1.x directory once.
- The JSON half of the advice test resets stdout and checks the advice.

Co-Authored-By: Claude <noreply@anthropic.com>
- project.Convert1xBlocked(dir) is the one decision: under an APC or
  unresolved context, the 1.x project dir is in or below
  (scaffold.Find1xProject, the walk Plan makes), and why. astro init, the
  discovery errors (NotFoundError, NoAstroSectionError) and the astro dev
  stub all ask it, so none suggests astro init where init refuses; under an
  unresolved context they say to fix or switch the context instead
  (project.Blocked1xMessage). The dev stub walks up too, so a subdirectory
  gets the root's advice, and publishes context_unresolved beside under_apc.
- The walk stops at any directory with a manifest, loading or not, checked
  before the 1.x layout, so each step loads the manifest once.
- In the home directory and ASTRO_HOME, where .astro/ is the CLI's
  settings, a Dockerfile makes a 1.x project only when .astro/config.yaml
  names a project; elsewhere the rule is v2's. The content heuristic for the
  settings file is gone.
- contextPlatform takes an Astro ASTRO_DOMAIN as Astro before anything else.
- The unresolved refusal names the 1.x project and says to rerun astro init
  in the directory given.
- A successful init under APC prints a notice on stderr in text mode only.
- devContext drops is1x; the 1.x directory decides it.
dev-removed schema golden updated for context_unresolved.

Co-Authored-By: Claude <noreply@anthropic.com>
- A 1.x project is known by content alone: a Dockerfile, and a
  .astro/config.yaml with a top-level project key, which astro dev init
  always writes (1.x cmd/airflow.go: CreateProjectConfig, then
  CFG.ProjectName.SetProjectString). The CLI's home settings name no
  project, so the HOME/ASTRO_HOME comparison is gone. scaffold.Is1xProject
  is the one definition; discovery uses it too. Fixtures write project:.
- The dev stub uses the nearest enclosing 1.x root in every context
  (scaffold.Find1xProject) and publishes it as v1_dir.
- Every message names the 1.x root and ends "then run astro init in
  <root>"; where ASTRO_DOMAIN chose the context it says to change or unset
  ASTRO_DOMAIN instead (project.Context.FromASTRODomain, from one
  project.SetContext).
- NotFoundError and NoAstroSectionError decide the block when they are made;
  Error() only formats.
- init no longer sets opts.DeploysToAPC (it has refused already); that is
  Desktop's.
- apcNotice reworded.
dev-removed schema golden updated for v1_dir.

Co-Authored-By: Claude <noreply@anthropic.com>
- A 1.x project is what Astro CLI 1.x takes for one (config.IsProjectDir,
  which its deploy requires): a Dockerfile and a .astro/config.yaml, no
  manifest, unless that file is the CLI's own settings (a mapping with no
  project key and context, contexts or telemetry). An empty or unparseable
  config.yaml counts, as for 1.x. scaffold.Is1xProject is the one
  definition; internal/project's copy is gone.
- "Switch to Astro" is one text, scaffold.SwitchToAstro, naming the domain
  for astro login (a bare astro login logs back in to APC).
- Convert1xBlocked returns the 1.x root in every context; Discover,
  LoadError and the dev stub use it, so there is one walk and the
  NoAstroSectionError of a monorepo root names the 1.x project below it.
- The dev stub's convert command from below the root is astro init <root>.
- project.NewProjectNotice words the notice for APC and for an unresolved
  context, and for ASTRO_DOMAIN choosing either.
- scaffold.HasManifest is exported; internal/project.HasManifest uses it.

Co-Authored-By: Claude <noreply@anthropic.com>
- The dev stub from below a 1.x root offers astro init <root> as astro dev
  init's replacement, in the mapping and in the text's example row too.
- An unreadable settings file is named, and the advice is to fix or move
  it, never to change the context (Context.UnreadableConfig).
- Where ASTRO_DOMAIN chose the context, the advice is to set it to an
  Astro domain (astronomer.io), with "or unset it" only when the saved
  current context is Astro or there is none (Context.UnsetIsAstro).
- NoAstroSectionError outside APC looks for a 1.x project only between
  start and the pyproject's directory, so a plain package inside a 1.x tree
  gets the add-[tool.astro] hint; where init refuses, it says what init says.
- contextPlatform reads only the current domain and whether its context is
  saved, never a login, so startup never reaches the keyring.
- Discover tracks the 1.x root in its own walk.
- Every printed root in a command is quoted by scaffold.ShellQuote, which
  cmd/config.go's suggestions now use too.
- Docs and field comments no longer name the deleted project.Is1xProject.

Co-Authored-By: Claude <noreply@anthropic.com>
The refusal and hint tests assumed a temp path needs no quoting, which a
Windows path with backslashes always does; they now expect
scaffold.ShellQuote(root).

Co-Authored-By: Claude <noreply@anthropic.com>
…walk only when it matters

- scaffold.ShellQuote uses double quotes on Windows, which cmd.exe and
  PowerShell both read as one word, and POSIX single quotes elsewhere.
- astro local init and astro af|airflow init give init's refusal where init
  refuses, instead of suggesting it.
- astro dev init's replacement stays plain astro init, as 1.x's made a
  project in the working directory; only the convert line names the root.
- Convert1xBlocked checks the context first and walks nothing under Astro;
  the dev stub walks for the root it names there.

Co-Authored-By: Claude <noreply@anthropic.com>
schnie and others added 2 commits October 10, 2026 12:01
- scaffold.ShellQuote is POSIX single quotes on every GOOS again, as v2's
  config suggestions had; PowerShell reads them literally.
- astro dev init below a 1.x root says to use astro init for a new project
  here, or astro init <root> to convert the 1.x project in place; json is
  unchanged.
- The af group silences usage, as astro local does, so init's refusal is
  not followed by a usage block.
- devProject uses only Convert1xBlocked's answer under a blocking context
  and walks once under Astro; Convert1xBlocked names the root only when it
  refuses (project.BlockingContext).

Co-Authored-By: Claude <noreply@anthropic.com>
…d the context once

- scaffold.ShellQuote treats a backslash as safe on Windows, so C:\proj
  prints unquoted; anything else needing quotes keeps POSIX single quotes.
  The safe set takes the GOOS, so one test covers both.
- astro dev init below a 1.x root names the convert command once, in
  backticks like its siblings.
- devProject reads the context once and walks accordingly.
- The af group's redundant SilenceUsage and its test are gone; AddCmds
  already silences usage tree-wide.

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