Files
onyx/tools/ods/cmd/env.go
roshan a8033533d1 refactor(ods): unify launchers and protect shared infrastructure (#16246)
## Description

Make ODS reuse shared development infrastructure by default, with
`--isolated` for separate datasets.

- Use the same service specifications and launchers for `stack`, `web
dev`, and `backend` commands. Remove the duplicate `stack
--frontend-only` option.
- Enforce project ownership across checkouts and resolve database
commands to the actual managed database.
- Stop services before pending migrations, preserve them when already at
head, and reject manual migrations beneath managed services.
- Harden startup cancellation, shutdown grace, descendant cleanup,
terminal input, and frontend API validation. Preserve Windows foreground
commands.
- Consolidate environment helpers, protect symlinked environment files,
propagate restore errors, and update help and documentation.

This PR contains only ODS and its development-stack skill. It does not
include the plugins implementation.

## How Has This Been Tested?

- Targeted command and regression tests using fake tools and disposable
Git repositories.
- Race tests for runtime targeting, ownership, migration ordering,
process cleanup, and terminal input.
- `go vet ./...`, pre-commit checks, native builds, and Windows
cross-builds.
- The full Go suite was run. Its remaining failures are the existing
macOS devcontainer SSH-mount fixture expectations in
`TestDevcontainerCommands` and `TestRunDevRebuild`.

No real database migrations, restores, or development-stack restarts
ran. Native Windows execution remains untested.

## Additional Options

- [ ] [Optional] Please cherry-pick this PR to the latest release
version.
- [x] [Optional] Override Linear Check

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
ODS now shares development infrastructure across worktrees by default,
with `--isolated` for separate datasets. `stack`, `web dev`, and
`backend` use the same service specs and launchers, and the duplicate
`stack --frontend-only` option is removed.

- Direct launches run the same dependency-prepare step as stack startup,
so `backend` and `web dev` work with fresh or stale Python environments.
- A stopped preview discards its former project state before retargeting
to another API owner.
- Enforces project ownership across checkouts and resolves database
commands to the managed database. New tests reject conflicting targets
before any mutation.
- Stops services before pending migrations, preserves them when already
at head, and rejects manual migrations beneath managed services.
Migration probe failures fail closed.
- Hardens startup cancellation, shutdown grace, and descendant cleanup;
ODS keeps the terminal foreground group so Ctrl-C still honors shutdown
timeouts, and tmux pane identifiers no longer change service identity.
- Database passwords reach tools through the child environment only,
never in process arguments; Windows foreground commands are preserved.
- Consolidates environment helpers, protects symlinked env files,
propagates restore errors, and refreshes help and documentation. Test
commits exercise the new migration-target and preview-launch paths.

<sup>Written for commit 14500cb051.
Summary will update on new commits.</sup>

<a
href="https://cubic.dev/pr/onyx-dot-app/onyx/pull/16246?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="View guided diff"
src="https://www.cubic.dev/buttons/review-in-cubic-light.svg"></picture></a>
<a
href="https://www.cubic.dev/action/auto-fix/pr/onyx-dot-app/onyx/16246?returnTo=https%3A%2F%2Fgithub.com%2Fonyx-dot-app%2Fonyx%2Fpull%2F16246&source=description"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/turn-on-auto-fix-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/turn-on-auto-fix-light.svg"><img
alt="Turn on auto-fix"
src="https://www.cubic.dev/buttons/turn-on-auto-fix-light.svg"></picture></a>

<!-- End of auto-generated description by cubic. -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added the global `--isolated` option for separate development stacks;
shared infrastructure is the default, with clearer project ownership and
status information.
* Added `ods web dev` previews that can use a healthy managed API on
macOS and Linux, plus foreground startup on Windows.
* Expanded command guidance for development workflows, database
targeting, and dump formats.
* **Bug Fixes**
* Pending migrations now stop managed application services first;
healthy services remain running when no migrations are needed.
* Database operations verify project ownership and target selection
before making changes. Restore errors now fail the command, SQL restores
stop on errors, and temporary files are cleaned up after failures.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-10-19 20:07:02 +00:00

154 lines
4.4 KiB
Go

package cmd
import (
"fmt"
"os"
"path/filepath"
"strings"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/onyx-dot-app/onyx/tools/ods/internal/devstack"
"github.com/onyx-dot-app/onyx/tools/ods/internal/docker"
"github.com/onyx-dot-app/onyx/tools/ods/internal/paths"
"github.com/onyx-dot-app/onyx/tools/ods/internal/prompt"
)
// NewEnvCommand creates the env command for writing infrastructure connection
// vars to .vscode/.env.
func NewEnvCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "env",
Short: "Write infrastructure port env vars to .vscode/.env",
Long: `Write infrastructure connection variables to .vscode/.env so that
locally-launched services (API server, Celery workers, etc.) connect to the
correct Docker Compose infrastructure for the current project/worktree.
Queries running containers for their actual host port mappings, then upserts
the corresponding keys in .vscode/.env. S3_LEGACY_ENDPOINT_URL is dropped,
since the dev stack runs the object store only. All other entries are left
untouched.
Examples:
# Write env vars for the current project (auto-detected from directory)
ods env
# Write env vars for a specific project
ods env --project my-worktree
# Show what would be written without modifying the file
ods env --dry-run`,
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
dryRun, _ := cmd.Flags().GetBool("dry-run")
if err := runEnv(dryRun); err != nil {
log.Fatal(err)
}
},
}
cmd.Flags().Bool("dry-run", false, "print env vars without writing to file")
return cmd
}
func runEnv(dryRun bool) error {
target, err := resolveRuntimeTarget()
if err != nil {
return err
}
release, err := lockRuntimeTarget(target, false)
if err != nil {
return err
}
defer release()
projName := target.Project
resolved := queryContainerPorts(projName)
appEnv := resolved.AppEnv()
if _, ok := appEnv["POSTGRES_PORT"]; !ok {
return fmt.Errorf("no running PostgreSQL container with a published port for project %s", projName)
}
log.Infof("Project: %s", projName)
if dryRun {
for k, v := range appEnv {
fmt.Printf("%s=%s\n", k, v)
}
return nil
}
gitRoot, err := paths.GitRoot()
if err != nil {
return fatalErrorf("Failed to find git root: %w", err)
}
envPath := filepath.Join(gitRoot, ".vscode", ".env")
if _, err := os.Stat(envPath); os.IsNotExist(err) {
log.Warnf("%s does not exist. Creating it with port variables only.", envPath)
log.Warnf("This file normally contains additional settings (API keys, auth config, etc.).")
log.Warnf("You may want to copy the template from the repo wiki or another developer's setup.")
if !prompt.Confirm("Continue creating a minimal .vscode/.env? (yes/no): ") {
log.Info("Aborted.")
return nil
}
}
if err := devstack.UpsertEnvFile(envPath, appEnv); err != nil {
return fatalErrorf("Failed to update %s: %w", envPath, err)
}
// Dev runs the object store only, so a legacy endpoint left by an earlier
// ods would only make the app fail over to a MinIO that never starts.
if err := removeEnvKey(envPath, legacyEndpointVar); err != nil {
return fatalErrorf("Failed to update %s: %w", envPath, err)
}
log.Infof("Updated %s", envPath)
for k, v := range appEnv {
log.Debugf(" %s=%s", k, v)
}
return nil
}
// queryContainerPorts discovers the actual host ports of running containers by
// calling "docker port" for each port spec in InfraServices. Container names
// follow the Docker Compose convention "<project>-<service>-1". Missing
// containers are excluded.
func queryContainerPorts(projName string) *docker.ResolvedPorts {
resolved := docker.NewResolvedPorts()
for _, svc := range docker.InfraServices {
container := docker.ContainerName(projName, svc.Name)
for _, spec := range svc.Ports {
port, err := docker.GetHostPort(container, spec.ContainerPort)
if err != nil {
log.Warnf("%s: container not running, skipping getting its port.", svc.Name)
continue
}
resolved.Append(port, spec)
}
}
return resolved
}
// removeEnvKey drops every "KEY=" line from a dotenv file.
func removeEnvKey(envPath, key string) error {
data, err := os.ReadFile(envPath)
if err != nil {
return fmt.Errorf("read %s: %w", envPath, err)
}
prefix := key + "="
var kept []string
for _, line := range strings.Split(string(data), "\n") {
if !strings.HasPrefix(line, prefix) {
kept = append(kept, line)
}
}
return os.WriteFile(envPath, []byte(strings.Join(kept, "\n")), 0644)
}