From 5b5ea92b9b7feca975ccdb370a5ab9dbe52e3db4 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Date: Wed, 23 Sep 2026 19:09:32 -0400 Subject: [PATCH] gh integration: retire the `current` symlink MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `publish_current` pointed /current at the last-installed version so consumers that could not ask dev could hardcode one path. Two things were wrong with it: it was machine state ("last installed") rather than a lock answer, so checkouts with different locked tags shared one pointer; and it was a write on the shared engine tree, which crashed the read-only ai-agent identity (plans#26) — the skip-if-converged guard existed only to dodge that. `dev deps path gh ` (#176) answers from the lockfile through DataRoot to the version-keyed dir, and every consumer in the org has moved to it (cellbound-3d#172). With no reader left, the pointer goes: an install is now a pure read once the marker matches. Existing links on hosts are left alone — nothing reads them, and removing them from `dev up` would be exactly the kind of shared-tree write this retires. Closes #177. Co-authored-by: Cursor --- lib/dev/deps/gh_integration.rb | 37 ++++------------------------ test/dev/deps/gh_integration_test.rb | 17 ++++++------- 2 files changed, 13 insertions(+), 41 deletions(-) diff --git a/lib/dev/deps/gh_integration.rb b/lib/dev/deps/gh_integration.rb index 2829fd5..943070d 100644 --- a/lib/dev/deps/gh_integration.rb +++ b/lib/dev/deps/gh_integration.rb @@ -4,7 +4,6 @@ require "digest" require "fileutils" require "pathname" -require "securerandom" require "shellwords" require_relative "../data_root" require_relative "integration" @@ -39,6 +38,11 @@ module Deps # multi-gigabyte (the UE engine is ~8GB compressed; a built tree far more), so # parking a second copy in ~/.dev/cache would double disk usage for no benefit. # The version-keyed install dir plus its marker file is the cache. + # + # Nothing else is written under install_dir: consumers locate the version + # dir through `dev deps path gh ` (Accessor), so an install is a pure + # read once the marker matches — a read-only shared tree (the agent identity + # on the human-owned engine tree, plans#26) needs no write to stay usable. class GhIntegration < Integration extend T::Sig @@ -100,7 +104,6 @@ def install_prebuilt(dep) target_dir = versioned_dir(base_dir, dep.version) if version_published?(target_dir, MARKER_FILE, dep.version) puts ">>> #{dep.name}@#{dep.version} already installed at #{target_dir}" - publish_current(base_dir, target_dir) return end @@ -127,7 +130,6 @@ def install_prebuilt(dep) else puts ">>> #{dep.name}@#{dep.version} published concurrently at #{target_dir}" end - publish_current(base_dir, target_dir) ensure FileUtils.rm_rf(staging_dir) if staging_dir end @@ -139,7 +141,6 @@ def install_from_source(dep) target_dir = versioned_dir(base_dir, dep.version) if version_published?(target_dir, MARKER_FILE, dep.version) puts ">>> #{dep.name}@#{dep.version} already installed at #{target_dir}" - publish_current(base_dir, target_dir) return end @@ -164,38 +165,10 @@ def install_from_source(dep) else puts ">>> #{dep.name}@#{dep.version} published concurrently at #{target_dir}" end - publish_current(base_dir, target_dir) ensure FileUtils.rm_rf(staging_dir) if staging_dir end - # Point /current at the just-installed version via a relative - # symlink, swapped in atomically. Host consumers (e.g. cellbound's - # build-game.sh via UE_ROOT) reference this stable path without knowing the - # locked tag; the versioned dirs themselves stay immutable — only this - # pointer moves, to the most recently installed version. - # - # @param base_dir [Pathname] declared install_dir - # @param target_dir [Pathname] the published version dir - sig { params(base_dir: Pathname, target_dir: Pathname).void } - def publish_current(base_dir, target_dir) - link = base_dir / "current" - desired = target_dir.basename.to_s - # A pointer that already resolves to the target is converged — skip - # the rewrite. The swap is not free idempotence but a write, and - # identities that can only read the shared tree (the ai-agent user - # on the human-owned engine tree, plans#26) crash on it (caught - # live at the plans#36 ceremony via dev install-deps). - return if File.symlink?(link.to_s) && File.readlink(link.to_s) == desired - - tmp = base_dir / ".current-#{Process.pid}-#{SecureRandom.hex(4)}" - File.symlink(desired, tmp.to_s) - File.rename(tmp.to_s, link.to_s) - rescue StandardError - FileUtils.rm_f(tmp.to_s) if tmp - raise - end - # Fetch the tag's source tarball into archive_path. Uses `gh api .../tarball` # rather than a bare codeload URL so the request carries gh's auth token and # follows the redirect — required for private/Epic-gated repos. Isolated so diff --git a/test/dev/deps/gh_integration_test.rb b/test/dev/deps/gh_integration_test.rb index 002c305..4c28742 100644 --- a/test/dev/deps/gh_integration_test.rb +++ b/test/dev/deps/gh_integration_test.rb @@ -141,7 +141,7 @@ def build_integration(fixture_files, cache_dir) File.read(File.join(version_dir, "Engine", "engine.txt")) == "engine payload" File.read(File.join(version_dir, "README.md")) == "readme payload" File.read(File.join(version_dir, ".dev-gh-release")) == "5.6.1-css-83" - File.readlink(File.join(install_dir, "current")) == "5.6.1-css-83" + !File.exist?(File.join(install_dir, "current")) Dir.glob(File.join(install_dir, ".staging-*")).empty? integration.download_count == 1 @@ -170,16 +170,15 @@ def build_integration(fixture_files, cache_dir) FileUtils.rm_rf(dir) end - test "install_all leaves an already-correct current pointer untouched — a read-only shared tree stays usable" do - Given "a published version whose current pointer already resolves to it, in a base dir the " \ - "caller cannot write (the agent identity on the human-owned shared engine tree, plans#26)" + test "install_all on an already-published version writes nothing — a read-only shared tree stays usable" do + Given "a published version in a base dir the caller cannot write (the agent identity on " \ + "the human-owned shared engine tree, plans#26)" dir = Dir.mktmpdir("dev-gh-int-test-") parts = build_split_archive(dir, "engine.tar.zst", part_size: 64) install_dir = File.join(dir, "engines", "unreal-engine-css") version_dir = File.join(install_dir, "5.6.1-css-83") FileUtils.mkdir_p(version_dir) File.write(File.join(version_dir, ".dev-gh-release"), "5.6.1-css-83") - File.symlink("5.6.1-css-83", File.join(install_dir, "current")) FileUtils.chmod(0o555, install_dir) dep = build_dependency(parts, install_dir) integration = build_integration(parts, File.join(dir, "cache")) @@ -187,9 +186,9 @@ def build_integration(fixture_files, cache_dir) When "installing again" integration.install_all([dep]) - Then "the converged pointer was recognized, not rewritten (caught live at the plans#36 " \ - "ceremony: the unconditional rewrite crashed dev install-deps with EACCES)" - File.readlink(File.join(install_dir, "current")) == "5.6.1-css-83" + Then "the marker was recognized and no write was attempted (the retired current-pointer " \ + "rewrite crashed dev install-deps here with EACCES at the plans#36 ceremony)" + !File.exist?(File.join(install_dir, "current")) integration.download_count == 0 Cleanup @@ -351,7 +350,7 @@ def build_source_integration(tarball, project_root, cache_dir) File.read(File.join(version_dir, "built.txt")) == "engine source" File.read(File.join(version_dir, "version.txt")).strip == "5.6.1-release" File.read(File.join(version_dir, ".dev-gh-release")) == "5.6.1-release" - File.readlink(File.join(install_dir, "current")) == "5.6.1-release" + !File.exist?(File.join(install_dir, "current")) Dir.glob(File.join(install_dir, ".staging-*")).empty? integration.download_count == 1