Skip to content

[VPEX][5b] Add local-env six-phase pipeline orchestrator#5851

Open
rugpanov wants to merge 3 commits into
dbconnect/05a-pkgmanagerfrom
dbconnect/05b-pipeline
Open

[VPEX][5b] Add local-env six-phase pipeline orchestrator#5851
rugpanov wants to merge 3 commits into
dbconnect/05a-pkgmanagerfrom
dbconnect/05b-pipeline

Conversation

@rugpanov

@rugpanov rugpanov commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Why

  • The earlier layers (resolve / fetch / merge) need an orchestrator that runs them in order, reports structured per-phase status, and provisions the environment — without mutating disk in --check dry-run mode.
  • This is the core half of the split described in [VPEX][5a] Add local-env package-manager interface, detection, and preflight #5850: the six-phase pipeline itself, stacked on the package-manager/detection layer.

What

  • pipeline.go — the six-phase orchestrator (preflight → resolve → fetch → merge → provision → validate). It records per-phase status into Result and returns typed PipelineErrors carrying FailurePhase and DiskMutated. Under --check it computes and reports the plan (with a diff) and performs no writes, skipping the writability probe and package-manager availability (neither is needed to compute the plan).
  • The merge base is the live pyproject.toml. MergeManaged rewrites only the three managed regions and is idempotent on its own output, so a re-run preserves edits the user made between syncs. The .bak is a one-time safety copy of the pre-sync original (created only when none exists, on the first sync of an existing project); an unreadable or unstattable existing file fails loudly rather than being misread as greenfield and overwritten.
  • constraints-only stops managing the databricks-connect pin rather than removing it: a greenfield project renders dev = [] (no databricks-connect), and an existing project that already pins databricks-connect keeps its pin untouched.

Testing strategy

  • End-to-end unit tests of the phase machine against a fake PackageManager + stub compute + httptest server: --check mutates nothing (even on a read-only dir / without a package manager) and reports a plan that matches a real re-run (no spurious backup, empty diff when idempotent), greenfield vs. existing, the merge basing on the live file so between-sync edits survive, backup safety on unreadable/unstattable files, constraints-only behavior, preflight exits (E_MANAGER_UNSUPPORTED / E_UV_MISSING), and phase/error attribution (pipeline_test.go).
  • Gates: go build, go test, golangci-lint (0 issues), deadcode, gofmt — all green.

About this stack

Review bottom-up. This PR targets #5850 as its base, so its diff shows only the pipeline layer.

This PR and #5850 together supersede #5828.

This pull request and its description were written by Isaac.

@rugpanov rugpanov temporarily deployed to test-trigger-is July 8, 2026 09:58 — with GitHub Actions Inactive
@rugpanov rugpanov temporarily deployed to test-trigger-is July 8, 2026 09:58 — with GitHub Actions Inactive
@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Waiting for approval

Could not determine reviewers from git history.
Round-robin suggestion: @renaudhartert-db

Eligible reviewers: @andrewnester, @anton-107, @denik, @janniklasrose, @pietern, @renaudhartert-db, @shreyas-goenka, @simonfaltum

Suggestions based on git history. See OWNERS for ownership rules.

@eng-dev-ecosystem-bot

eng-dev-ecosystem-bot commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Integration test report

Commit: 2ef43e0

Run: 29096837578

Env 🟨​KNOWN 🔄​flaky 💚​RECOVERED 🙈​SKIP ✅​pass 🙈​skip Time
💚​ aws linux 4 4 230 1069 5:04
💚​ aws windows 4 4 232 1067 6:57
💚​ aws-ucws linux 4 4 314 987 5:38
💚​ aws-ucws windows 4 4 316 985 7:27
💚​ azure linux 4 4 230 1068 4:58
💚​ azure windows 4 4 232 1066 7:14
💚​ azure-ucws linux 4 4 316 984 5:55
🔄​ azure-ucws windows 2 4 4 316 982 7:55
🟨​ gcp linux 1 1 4 229 1070 7:35
💚​ gcp windows 4 4 231 1068 7:02
10 interesting tests: 4 SKIP, 3 RECOVERED, 2 flaky, 1 KNOWN
Test Name aws linux aws windows aws-ucws linux aws-ucws windows azure linux azure windows azure-ucws linux azure-ucws windows gcp linux gcp windows
💚​ TestAccept 💚​R 💚​R 💚​R 💚​R 💚​R 💚​R 💚​R 💚​R 💚​R 💚​R
🙈​ TestAccept/bundle/invariant/no_drift 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
🙈​ TestAccept/bundle/resources/vector_search_endpoints/drift/recreated_same_name 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
🙈​ TestAccept/bundle/resources/vector_search_indexes/recreate/embedding_dimension 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
🙈​ TestAccept/ssh/connection 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
🔄​ TestFsCpFileToNonExistentDir/local_to_uc-volumes 🙈​s 🙈​s ✅​p ✅​p 🙈​s 🙈​s ✅​p 🔄​f 🙈​s 🙈​s
🔄​ TestFsCpFileToNonExistentDir/uc-volumes_to_uc-volumes 🙈​s 🙈​s ✅​p ✅​p 🙈​s 🙈​s ✅​p 🔄​f 🙈​s 🙈​s
🟨​ TestFetchRepositoryInfoAPI_FromRepo 💚​R 💚​R 💚​R 💚​R 💚​R 💚​R 💚​R 💚​R 🟨​K 💚​R
💚​ TestFetchRepositoryInfoAPI_FromRepo/root 💚​R 💚​R 💚​R 💚​R 💚​R 💚​R 💚​R 💚​R 💚​R
💚​ TestFetchRepositoryInfoAPI_FromRepo/subdir 💚​R 💚​R 💚​R 💚​R 💚​R 💚​R 💚​R 💚​R 💚​R
Top 10 slowest tests (at least 2 minutes):
duration env testname
6:13 aws-ucws windows TestAccept
6:12 gcp windows TestAccept
6:12 azure windows TestAccept
6:08 azure-ucws windows TestAccept
6:07 aws windows TestAccept
3:03 aws linux TestAccept
2:55 azure linux TestAccept
2:53 gcp linux TestAccept
2:51 aws-ucws linux TestAccept
2:50 azure-ucws linux TestAccept

The orchestrator runs the layers in order (preflight → resolve → fetch →
merge → provision → validate), records per-phase status into Result, and
returns typed PipelineErrors carrying FailurePhase and DiskMutated. Under
--check it computes and reports the plan (with a diff) and performs no
writes — skipping the writability probe and package-manager availability,
neither of which is needed to compute the plan.

Backups are created only when no .bak exists and the original is a genuine
os.ErrNotExist-free read; an unreadable or unstattable existing file fails
loudly rather than being misread as greenfield and overwritten.

Stacked on the package-manager/detection layer; provisions through the
PackageManager interface against a fake in tests.

Co-authored-by: Isaac
@rugpanov rugpanov force-pushed the dbconnect/05a-pkgmanager branch from cb166cb to f24c320 Compare July 8, 2026 14:44
@rugpanov rugpanov force-pushed the dbconnect/05b-pipeline branch from af9d41b to c2e8b69 Compare July 8, 2026 14:44
@rugpanov rugpanov temporarily deployed to test-trigger-is July 8, 2026 14:44 — with GitHub Actions Inactive
@rugpanov rugpanov temporarily deployed to test-trigger-is July 8, 2026 14:44 — with GitHub Actions Inactive
Comment thread libs/localenv/pipeline.go
}
if err := os.WriteFile(dst, data, 0o644); err != nil {
return fmt.Errorf("write %s: %w", dst, err)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

copyFile creates the .bak at a hardcoded 0o644. If the user's pyproject.toml is locked down (e.g. 0o600 because it carries a private index URL or token), the backup silently becomes world-readable.

The main-file write at applyMerge is fine only incidentally — os.WriteFile ignores the mode arg when the file already exists, so the existing pyproject keeps its perms. But the backup is freshly created here, so 0o644 actually applies.

Suggest either preserving the source mode (os.Stat(src) → pass info.Mode()) or, if 0o644 is deliberate, a one-line comment saying so (the repo's comment-the-why rule).


This comment was generated with GitHub MCP.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — fixed in 6f31579. copyFile now stats the source and creates the backup with info.Mode().Perm(), so a 0o600 pyproject (private index URL/token) is no longer widened to world-readable. Added TestCopyFilePreservesMode to lock it in.

Comment thread libs/localenv/pipeline.go
// Phase: preflight — manager detection, writability, package-manager availability.
// P0 supports only uv; any other detected manager is a clean, non-blaming exit.
if m := detectManager(p.ProjectDir); m != managerUv {
return p.fail(PhasePreflight, false, NewError(ErrManagerUnsupported, nil, "%s", managerGuidance(m)))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two preflight exits look untested at the pipeline level:

  • this unsupported-manager path → E_MANAGER_UNSUPPORTED, and
  • the EnsureAvailable failure below → E_UV_MISSING (line ~113).

detect_test.go covers detectManager in isolation, but not the pipeline's wiring of it into a clean preflight failure (correct phase attribution, DiskMutated=false, later phases still pending). The noProvisionPM fixture already exists — a test that drops an environment.yml and asserts the E_MANAGER_UNSUPPORTED / PhasePreflight result would close the gap cheaply.


This comment was generated with GitHub MCP.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 6f31579. Added TestPipelineManagerUnsupportedFailsAtPreflight (drops an environment.ymlE_MANAGER_UNSUPPORTED) and TestPipelineUvMissingFailsAtPreflight (new uvMissingPM fixture → E_UV_MISSING). Both assert phase attribution (PhasePreflight), DiskMutated=false, and that all later phases stay pending, via a shared assertPreflightFailure helper.

Address review on the pipeline PR:

- copyFile created the .bak at a hardcoded 0o644, silently widening a
  locked-down pyproject.toml (e.g. 0o600 carrying a private index URL) to
  world-readable. Stat the source and create the backup with its permission
  bits; os.WriteFile applies the mode only when creating the file, which is
  always the case for the fresh .bak.
- Add pipeline-level tests for the two preflight exits that were only
  covered in isolation before: E_MANAGER_UNSUPPORTED (a non-uv project) and
  E_UV_MISSING (package manager unavailable), asserting phase attribution,
  DiskMutated=false, and that later phases stay pending. Add a direct
  copyFile mode-preservation test.

Co-authored-by: Isaac
@rugpanov rugpanov temporarily deployed to test-trigger-is July 9, 2026 14:06 — with GitHub Actions Inactive
@rugpanov rugpanov temporarily deployed to test-trigger-is July 9, 2026 14:06 — with GitHub Actions Inactive
@rugpanov rugpanov requested a review from anton-107 July 10, 2026 11:26

@anton-107 anton-107 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work — phase/error attribution is airtight, the backup-safety branches are exactly right, and --check correctly avoids touching the machine or disk. My concerns below are about behavior, not code hygiene. The first is the headline one.

1. Re-basing the merge on .bak silently discards user edits made after the first sync

mergePlan uses the backup as the canonical merge base whenever a .bak exists (pipeline.go:227-238), and TestPipelineRestoresBackupBeforeMerge asserts this is intended. The consequence is that on re-runs the merge no longer starts from the live pyproject.toml — it starts from the pristine original captured on the first sync.

I confirmed the fallout with a scratch test:

  • Sync once (creates .bak).
  • Developer hand-edits the live pyproject.toml — e.g. adds dependencies = ["rich", "requests"].
  • Sync again (e.g. after switching compute target).
  • The requests edit is gone — the second merge re-based on .bak, which never saw it.

This defeats the purpose of the byte-preserving MergeManaged (which exists specifically to touch only the three managed regions and preserve everything else). And re-basing on .bak buys nothing for the managed regions: MergeManaged is idempotent on its own output and preserves non-managed deps, so merging onto the current live file produces identical managed-region results — without throwing away edits. The only marginal difference is self-healing a databricks-connect pin the user manually deleted, which seems like an odd thing to optimize for at the cost of silent edit loss.

The .bak original is recoverable, but edits made between syncs are not — and that's the normal workflow (sync, add a dep, re-sync). Could you confirm whether this is truly intended? I'd argue the live file should be the merge base and .bak should remain purely a one-time safety copy. There's also no test covering "second run preserves a non-managed user edit" — today that test would fail.

2. --check reports a plan a real re-run wouldn't perform (downstream of #1)

On a re-run where .bak already exists, two things in the plan are inaccurate:

  • plan.WouldBackup is always set for a non-greenfield project (pipeline.go:297-299), but a real re-run keeps the existing .bak and does not write a backup (applyMerge, pipeline.go:317-320). So --check claims a backup that won't happen.
  • The diff base is .bak, not the live file (pipeline.go:279-289). On an idempotent re-run the live file already equals the merged output — a real re-run changes nothing — yet --check prints a full diff against the original. I confirmed both: the plan shows the whole managed block as an addition even though the on-disk file is already identical to what the run would write.

Lower severity since it's advisory output, but a dry-run whose plan doesn't match the real run is misleading. Fixing #1 fixes this too.

3. constraints-only doesn't "omit the databricks-connect dependency entirely" on existing projects

The PR description says constraints-only "omits the databricks-connect dependency entirely," and the pipeline clears dbcPin (pipeline.go:144-147). That's accurate for greenfield (dev = []). But on an existing project that already pins databricks-connect, the empty value makes mergeDatabricksConnect a no-op, so the existing pin is retained (confirmed by scratch test). This is intended per the merge.go comment ("left untouched rather than ... blanked out") and is defensible — but the description over-claims. Please align the wording, or document that constraints-only only stops managing the pin rather than removing it.

Address review on the pipeline PR (concerns #1 and #2).

Re-basing the merge on .bak silently discarded edits the user made to
pyproject.toml between syncs: mergePlan used .bak as the merge base whenever
one existed, so a re-run started from the pristine pre-sync original and any
dependency the user added afterward was lost. MergeManaged rewrites only the
three managed regions and is idempotent on its own output, so merging onto
the live file yields identical managed regions without throwing away edits.

- mergePlan now reads the live pyproject.toml as the merge base; .bak stays a
  one-time safety copy of the pre-sync original.
- --check no longer reports a backup on a re-run (applyMerge keeps the
  existing .bak, so WouldBackup is set only when no .bak exists yet), and the
  diff is now against the live file, so an idempotent re-run reports no change
  — matching what a real run would do.
- Replace TestPipelineRestoresBackupBeforeMerge (which encoded the old
  discard-edits behavior) with TestPipelineMergesOnLiveFileNotBackup, and add
  TestPipelineCheckReRunPlanMatchesRealRun.

Also clarify the constraints-only comment: it stops managing the
databricks-connect pin (greenfield dev = []; existing pin retained) rather
than removing it, matching mergeDatabricksConnect's no-op-on-empty behavior.

Co-authored-by: Isaac
@rugpanov

Copy link
Copy Markdown
Contributor Author

Verified all three against the code (reproduced #1 and #3 with scratch tests first) and pushed fixes in 2ef43e0.

#1 — re-basing on .bak discards between-sync edits. Confirmed, and it's a real bug: I reproduced the exact scenario (sync → add a non-managed dep → re-sync → dep gone). Fixed — mergePlan now uses the live pyproject.toml as the merge base, not .bak. As you noted, MergeManaged is idempotent on its own output and preserves non-managed content, so merging onto the live file produces identical managed regions without throwing away edits (I verified the second merge reports zero changed regions). .bak stays purely a one-time safety copy of the pre-sync original.

  • Replaced TestPipelineRestoresBackupBeforeMerge (which encoded the old discard-edits behavior) with TestPipelineMergesOnLiveFileNotBackup: a .bak holding a different original + a live file with a user-added dependencies = ["rich"] → the edit survives, managed regions still apply, .bak untouched.

#2--check plan inaccurate on re-runs. Fixed, and follows from #1:

  • plan.WouldBackup is now set only when no .bak exists yet, matching applyMerge (which keeps the existing .bak on a re-run).
  • The diff base is now the live file, so an idempotent re-run reports an empty diff — matching what a real run would do.
  • New TestPipelineCheckReRunPlanMatchesRealRun asserts both (empty WouldBackup, empty diff after a prior sync).

#3constraints-only wording over-claims. Agreed — behavior is intended (existing pin retained; empty value is a no-op in mergeDatabricksConnect), only the description was wrong. Reworded the code comment at the dbcPin clearing site to say constraints-only stops managing the pin (greenfield → dev = []; existing → pin retained) rather than removing it. I'll fix the PR description wording too.

All gates green (build, test, lint, deadcode, gofmt). #5854 was cascade-rebased on top.

@rugpanov rugpanov temporarily deployed to test-trigger-is July 10, 2026 13:38 — with GitHub Actions Inactive
@rugpanov rugpanov temporarily deployed to test-trigger-is July 10, 2026 13:38 — with GitHub Actions Inactive
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.

3 participants