From a0425c89f5de0c79ee368bf33de440b8ea897b1f Mon Sep 17 00:00:00 2001 From: Thomas Honeyman Date: Fri, 16 Jan 2026 15:44:11 -0500 Subject: [PATCH 1/5] update lock implementation --- app/src/App/Effect/Registry.purs | 105 ++++++++++++++++++++---------- app/test/App/Effect/Registry.purs | 41 ++++++++++++ app/test/Main.purs | 2 + 3 files changed, 115 insertions(+), 33 deletions(-) create mode 100644 app/test/App/Effect/Registry.purs diff --git a/app/src/App/Effect/Registry.purs b/app/src/App/Effect/Registry.purs index 4d34f51e2..5531f7952 100644 --- a/app/src/App/Effect/Registry.purs +++ b/app/src/App/Effect/Registry.purs @@ -6,6 +6,7 @@ module Registry.App.Effect.Registry where import Registry.App.Prelude import Codec.JSON.DecodeError as CJ.DecodeError +import Control.Parallel as Parallel import Data.Array as Array import Data.Array.NonEmpty as NonEmptyArray import Data.Codec.JSON as CJ @@ -17,6 +18,7 @@ import Data.Map as Map import Data.Set as Set import Data.String as String import Data.Time.Duration as Duration +import Effect.Aff (Milliseconds(..)) import Effect.Aff as Aff import Effect.Aff.AVar (AVar) import Effect.Aff.AVar as AVar @@ -24,19 +26,19 @@ import Effect.Ref as Ref import JSON as JSON import Node.FS.Aff as FS.Aff import Node.Path as Path -import Registry.Foreign.FSExtra as FS.Extra import Registry.App.CLI.Git (GitResult) import Registry.App.CLI.Git as Git import Registry.App.Effect.Cache (class MemoryEncodable, Cache, CacheRef, MemoryEncoding(..)) import Registry.App.Effect.Cache as Cache import Registry.App.Effect.GitHub (GITHUB) import Registry.App.Effect.GitHub as GitHub -import Registry.App.Effect.Log (LOG) +import Registry.App.Effect.Log (LOG, Log) import Registry.App.Effect.Log as Log import Registry.App.Legacy.PackageSet (PscTag(..)) import Registry.App.Legacy.PackageSet as Legacy.PackageSet import Registry.App.Legacy.Types (legacyPackageSetCodec) import Registry.Constants as Constants +import Registry.Foreign.FSExtra as FS.Extra import Registry.Foreign.FastGlob as FastGlob import Registry.Foreign.Octokit (Address) import Registry.Foreign.Octokit as Octokit @@ -54,6 +56,7 @@ import Run as Run import Run.Except (EXCEPT) import Run.Except as Except import Safe.Coerce (coerce) +import Type.Proxy (Proxy(..)) data RegistryCache (c :: Type -> Type -> Type) a = AllManifests (c ManifestIndex a) @@ -175,16 +178,17 @@ data Process derive instance Eq Process -instance Show Process where - show Scheduler = "Scheduler" - show JobExecutor = "JobExecutor" - show API = "API" - show ScriptLegacyImporter = "ScriptLegacyImporter" - show ScriptPackageDeleter = "ScriptPackageDeleter" - show ScriptSolver = "ScriptSolver" - show ScriptVerifyIntegrity = "ScriptVerifyIntegrity" - show ScriptCompilerVersions = "ScriptCompilerVersions" - show ScriptArchiveSeeder = "ScriptArchiveSeeder" +printProcess :: Process -> String +printProcess = case _ of + Scheduler -> "Scheduler" + JobExecutor -> "JobExecutor" + API -> "API" + ScriptLegacyImporter -> "ScriptLegacyImporter" + ScriptPackageDeleter -> "ScriptPackageDeleter" + ScriptSolver -> "ScriptSolver" + ScriptVerifyIntegrity -> "ScriptVerifyIntegrity" + ScriptCompilerVersions -> "ScriptCompilerVersions" + ScriptArchiveSeeder -> "ScriptArchiveSeeder" -- | A lock for a single repository, tracking both the mutex and the owner. type RepoLock = { lock :: AVar Unit, owner :: Ref (Maybe Process) } @@ -216,29 +220,64 @@ withRepoLock . Process -> RepoLocks -> RepoKey - -> Run (LOG + AFF + EFFECT + r) a - -> Run (LOG + AFF + EFFECT + r) a + -> Run (LOG + AFF + EFFECT + EXCEPT String + r) a + -> Run (LOG + AFF + EFFECT + EXCEPT String + r) a withRepoLock process locks key action = do repoLock <- Run.liftAff $ getOrCreateLock locks key - Run.liftAff $ AVar.take repoLock.lock - Run.liftEffect $ Ref.write (Just process) repoLock.owner - result <- action - Run.liftEffect $ Ref.write Nothing repoLock.owner - Run.liftAff $ AVar.put unit repoLock.lock - pure result - --- | Clear any locks owned by a specific process. --- | Used to clean up orphaned locks when a process crashes and restarts. -clearOwnLocks :: forall r. Process -> RepoLocks -> Run (LOG + AFF + EFFECT + r) Unit -clearOwnLocks process locksRef = do - locks <- Run.liftEffect $ Ref.read locksRef - for_ (Map.toUnfoldable locks :: Array _) \(Tuple _ repoLock) -> do - owner <- Run.liftEffect $ Ref.read repoLock.owner - when (owner == Just process) do - Log.warn $ "Clearing orphaned lock for " <> show process - Run.liftEffect $ Ref.write Nothing repoLock.owner - -- Put the unit back to release the lock - Run.liftAff $ AVar.put unit repoLock.lock + + -- It isn't possible to run exception-safe Aff code like `bracket` within + -- the extensible effects system. For the actions we need to support + -- behind a lock we only need to support the LOG effect, so we lower to + -- Aff, run the lock-guarded code safely, and aggregate the logs to be + -- flushed afterwards. + { logs, outcome } <- Run.liftAff do + logsRef <- liftEffect $ Ref.new [] + outcome <- withRepoLockAff repoLock (Milliseconds 60_000.0) (runWithLogs logsRef action) + logs <- liftEffect $ Ref.read logsRef + pure { logs, outcome } + + -- We replay the collected logs + for_ logs \log -> + Run.lift Log._log log + + case outcome of + Nothing -> do + Log.warn $ "Repo lock timed out for " <> printProcess process + Run.liftAff $ Aff.throwError $ Aff.error "Repo lock timed out." + Just (Left err) -> + Run.liftAff $ Aff.throwError err + Just (Right value) -> + pure value + where + runWithLogs :: Ref (Array (Log Unit)) -> Run (LOG + AFF + EFFECT + EXCEPT String + r) a -> Aff (Either Aff.Error a) + runWithLogs ref = Aff.attempt <<< Run.runCont step pure + where + step = + Run.on Log._log handleLog + $ Run.on (Proxy @"aff") (\k -> k >>= identity) + $ Run.on (Proxy @"effect") (\k -> liftEffect k >>= identity) + $ Run.on Except._except (\k -> Aff.throwError (Aff.error (coerce k))) + $ Run.default (Aff.throwError $ Aff.error "withRepoLock: unexpected effect") + + handleLog (Log.Log level message next) = do + liftEffect $ Ref.modify_ (\logs -> Array.snoc logs (Log.Log level message unit)) ref + next + + -- | Acquire a lock, run the action, and release the lock, guarded by a bracket to clean the + -- | locks on exception. Action is cancelled after a configurable timeout + withRepoLockAff :: RepoLock -> Milliseconds -> Aff (Either Aff.Error a) -> Aff (Maybe (Either Aff.Error a)) + withRepoLockAff repoLock timeout aff = + Aff.bracket acquire release \_ -> do + let race = Parallel.parallel (Just <$> aff) <|> Parallel.parallel (Aff.delay timeout $> Nothing) + Parallel.sequential race + where + acquire = do + AVar.take repoLock.lock + liftEffect $ Ref.write (Just process) repoLock.owner + + release _ = do + liftEffect $ Ref.write Nothing repoLock.owner + AVar.put unit repoLock.lock -- | Validate that a repository is in a valid state. -- | If the repo is corrupted (e.g., from an interrupted clone), delete it. diff --git a/app/test/App/Effect/Registry.purs b/app/test/App/Effect/Registry.purs new file mode 100644 index 000000000..e3989fd70 --- /dev/null +++ b/app/test/App/Effect/Registry.purs @@ -0,0 +1,41 @@ +module Test.Registry.App.Effect.Registry (spec) where + +import Registry.App.Prelude + +import Effect.Aff as Aff +import Effect.Aff.AVar as AVar +import Effect.Ref as Ref +import Registry.App.Effect.Log (LOG) +import Registry.App.Effect.Log as Log +import Registry.App.Effect.Registry as Registry +import Registry.Test.Assert as Assert +import Run (AFF, EFFECT, Run) +import Run as Run +import Run.Except (EXCEPT) +import Run.Except as Except +import Test.Spec as Spec + +spec :: Spec.Spec Unit +spec = Spec.it "Releases repo lock on exception" do + locks <- Registry.newRepoLocks + repoLock <- Registry.getOrCreateLock locks Registry.RegistryRepo + + result <- Aff.attempt $ runBase do + Registry.withRepoLock Registry.API locks Registry.RegistryRepo do + Log.info "Acquiring lock" + Except.throw "boom" + + case result of + Left _ -> do + owner <- liftEffect $ Ref.read repoLock.owner + available <- AVar.tryRead repoLock.lock + Assert.shouldEqual Nothing owner + Assert.shouldEqual (Just unit) available + Right _ -> + Assert.fail "Expected lock action to throw" + where + runBase :: forall a. Run (LOG + EXCEPT String + AFF + EFFECT + ()) a -> Aff a + runBase = + Log.interpret (\(Log.Log _ _ next) -> pure next) + >>> Except.catch (\err -> Run.liftAff (Aff.throwError (Aff.error err))) + >>> Run.runBaseAff' diff --git a/app/test/Main.purs b/app/test/Main.purs index d69babc00..8daaf27e9 100644 --- a/app/test/Main.purs +++ b/app/test/Main.purs @@ -10,6 +10,7 @@ import Test.Registry.App.CLI.Purs as Test.CLI.Purs import Test.Registry.App.CLI.PursVersions as Test.CLI.PursVersions import Test.Registry.App.CLI.Tar as Test.CLI.Tar import Test.Registry.App.Effect.PackageSets as Test.Effect.PackageSets +import Test.Registry.App.Effect.Registry as Test.Effect.Registry import Test.Registry.App.GitHubIssue as Test.GitHubIssue import Test.Registry.App.Legacy.LenientRange as Test.Legacy.LenientRange import Test.Registry.App.Legacy.LenientVersion as Test.Legacy.LenientVersion @@ -37,6 +38,7 @@ main = runSpecAndExitProcess' config [ consoleReporter ] do Spec.describe "Registry.App.Effect" do Test.Effect.PackageSets.spec + Test.Effect.Registry.spec Spec.describe "Registry.App.GitHubIssue" do Test.GitHubIssue.spec From 6595142b782916385f0e95ee99a743c7f9ec39d6 Mon Sep 17 00:00:00 2001 From: Thomas Honeyman Date: Fri, 16 Jan 2026 16:11:38 -0500 Subject: [PATCH 2/5] add more tests --- app/src/App/Effect/Registry.purs | 21 ++++- app/test/App/Effect/Registry.purs | 145 ++++++++++++++++++++++++++---- 2 files changed, 145 insertions(+), 21 deletions(-) diff --git a/app/src/App/Effect/Registry.purs b/app/src/App/Effect/Registry.purs index 5531f7952..68071a40d 100644 --- a/app/src/App/Effect/Registry.purs +++ b/app/src/App/Effect/Registry.purs @@ -215,6 +215,7 @@ getOrCreateLock locksRef key = do -- | Acquire a repository lock, run an action, and release the lock. -- | The lock prevents concurrent access to the same repository. +-- | Defaults to a 60-second timeout. withRepoLock :: forall r a . Process @@ -222,7 +223,19 @@ withRepoLock -> RepoKey -> Run (LOG + AFF + EFFECT + EXCEPT String + r) a -> Run (LOG + AFF + EFFECT + EXCEPT String + r) a -withRepoLock process locks key action = do +withRepoLock = withRepoLockTimeout (Milliseconds 60_000.0) + +-- | Acquire a repository lock, run an action, and release the lock. +-- | The lock prevents concurrent access to the same repository. +withRepoLockTimeout + :: forall r a + . Milliseconds + -> Process + -> RepoLocks + -> RepoKey + -> Run (LOG + AFF + EFFECT + EXCEPT String + r) a + -> Run (LOG + AFF + EFFECT + EXCEPT String + r) a +withRepoLockTimeout timeout process locks key action = do repoLock <- Run.liftAff $ getOrCreateLock locks key -- It isn't possible to run exception-safe Aff code like `bracket` within @@ -232,7 +245,7 @@ withRepoLock process locks key action = do -- flushed afterwards. { logs, outcome } <- Run.liftAff do logsRef <- liftEffect $ Ref.new [] - outcome <- withRepoLockAff repoLock (Milliseconds 60_000.0) (runWithLogs logsRef action) + outcome <- withRepoLockAff repoLock timeout (runWithLogs logsRef action) logs <- liftEffect $ Ref.read logsRef pure { logs, outcome } @@ -266,9 +279,9 @@ withRepoLock process locks key action = do -- | Acquire a lock, run the action, and release the lock, guarded by a bracket to clean the -- | locks on exception. Action is cancelled after a configurable timeout withRepoLockAff :: RepoLock -> Milliseconds -> Aff (Either Aff.Error a) -> Aff (Maybe (Either Aff.Error a)) - withRepoLockAff repoLock timeout aff = + withRepoLockAff repoLock lockTimeout aff = Aff.bracket acquire release \_ -> do - let race = Parallel.parallel (Just <$> aff) <|> Parallel.parallel (Aff.delay timeout $> Nothing) + let race = Parallel.parallel (Just <$> aff) <|> Parallel.parallel (Aff.delay lockTimeout $> Nothing) Parallel.sequential race where acquire = do diff --git a/app/test/App/Effect/Registry.purs b/app/test/App/Effect/Registry.purs index e3989fd70..e70b23c5a 100644 --- a/app/test/App/Effect/Registry.purs +++ b/app/test/App/Effect/Registry.purs @@ -16,23 +16,134 @@ import Run.Except as Except import Test.Spec as Spec spec :: Spec.Spec Unit -spec = Spec.it "Releases repo lock on exception" do - locks <- Registry.newRepoLocks - repoLock <- Registry.getOrCreateLock locks Registry.RegistryRepo - - result <- Aff.attempt $ runBase do - Registry.withRepoLock Registry.API locks Registry.RegistryRepo do - Log.info "Acquiring lock" - Except.throw "boom" - - case result of - Left _ -> do - owner <- liftEffect $ Ref.read repoLock.owner - available <- AVar.tryRead repoLock.lock - Assert.shouldEqual Nothing owner - Assert.shouldEqual (Just unit) available - Right _ -> - Assert.fail "Expected lock action to throw" +spec = do + Spec.it "Releases repo lock on exception" do + locks <- Registry.newRepoLocks + repoLock <- Registry.getOrCreateLock locks Registry.RegistryRepo + + result <- Aff.attempt $ runBase do + Registry.withRepoLock Registry.API locks Registry.RegistryRepo do + Log.info "Acquiring lock" + Except.throw "boom" + + case result of + Left _ -> do + owner <- liftEffect $ Ref.read repoLock.owner + available <- AVar.tryRead repoLock.lock + Assert.shouldEqual Nothing owner + Assert.shouldEqual (Just unit) available + Right _ -> + Assert.fail "Expected lock action to throw" + + Spec.it "Tracks lock owner while held" do + locks <- Registry.newRepoLocks + repoLock <- Registry.getOrCreateLock locks Registry.RegistryRepo + entered <- AVar.empty + release <- AVar.empty + + fiber <- Aff.forkAff $ runBase do + Registry.withRepoLock Registry.Scheduler locks Registry.RegistryRepo do + Run.liftAff $ AVar.put unit entered + Run.liftAff $ AVar.take release + + _ <- AVar.take entered + ownerWhile <- liftEffect $ Ref.read repoLock.owner + Assert.shouldEqual (Just Registry.Scheduler) ownerWhile + + _ <- AVar.put unit release + _ <- Aff.joinFiber fiber + + ownerAfter <- liftEffect $ Ref.read repoLock.owner + Assert.shouldEqual Nothing ownerAfter + + Spec.it "Serializes work for same repo" do + locks <- Registry.newRepoLocks + entered <- AVar.empty + release <- AVar.empty + secondEntered <- liftEffect $ Ref.new false + + let + action = Registry.withRepoLock Registry.API locks Registry.RegistryRepo + buildWorker = runBase <<< action + + worker1 = buildWorker do + Run.liftAff $ AVar.put unit entered + Run.liftAff $ AVar.take release + + worker2 = buildWorker do + Run.liftEffect $ Ref.write true secondEntered + + fiber1 <- Aff.forkAff worker1 + _ <- AVar.take entered + fiber2 <- Aff.forkAff worker2 + + _ <- Aff.delay (Aff.Milliseconds 50.0) + enteredBefore <- liftEffect $ Ref.read secondEntered + Assert.shouldEqual false enteredBefore + + _ <- AVar.put unit release + _ <- Aff.joinFiber fiber1 + _ <- Aff.joinFiber fiber2 + + enteredAfter <- liftEffect $ Ref.read secondEntered + Assert.shouldEqual true enteredAfter + + Spec.it "Allows concurrent work for different repos" do + locks <- Registry.newRepoLocks + enteredA <- AVar.empty + enteredB <- AVar.empty + releaseA <- AVar.empty + releaseB <- AVar.empty + doneA <- liftEffect $ Ref.new false + doneB <- liftEffect $ Ref.new false + + let + action repo entered release flag = runBase do + Registry.withRepoLock Registry.API locks repo do + Run.liftAff $ AVar.put unit entered + Run.liftAff $ AVar.take release + Run.liftEffect $ Ref.write true flag + + workerA = action Registry.RegistryRepo enteredA releaseA doneA + workerB = action Registry.ManifestIndexRepo enteredB releaseB doneB + + _ <- Aff.forkAff workerA + _ <- AVar.take enteredA + + _ <- Aff.forkAff workerB + _ <- Aff.delay (Aff.Milliseconds 50.0) + enteredBStatus <- AVar.tryRead enteredB + Assert.shouldEqual (Just unit) enteredBStatus + + _ <- AVar.put unit releaseA + _ <- AVar.put unit releaseB + + gotA <- liftEffect $ Ref.read doneA + gotB <- liftEffect $ Ref.read doneB + Assert.shouldEqual true gotA + Assert.shouldEqual true gotB + + Spec.it "Releases lock after timeout" do + locks <- Registry.newRepoLocks + repoLock <- Registry.getOrCreateLock locks Registry.RegistryRepo + + result <- Aff.attempt $ runBase do + Registry.withRepoLockTimeout (Aff.Milliseconds 10.0) Registry.API locks Registry.RegistryRepo do + Run.liftAff $ Aff.delay (Aff.Milliseconds 50.0) + + case result of + Left _ -> do + owner <- liftEffect $ Ref.read repoLock.owner + available <- AVar.tryRead repoLock.lock + Assert.shouldEqual Nothing owner + Assert.shouldEqual (Just unit) available + + followUp <- Aff.attempt $ runBase do + Registry.withRepoLock Registry.API locks Registry.RegistryRepo do + pure unit + Assert.shouldEqual true (isRight followUp) + Right _ -> + Assert.fail "Expected timeout" where runBase :: forall a. Run (LOG + EXCEPT String + AFF + EFFECT + ()) a -> Aff a runBase = From 4425a4a755d7bfe5023acb92248051223ddbd579 Mon Sep 17 00:00:00 2001 From: Thomas Honeyman Date: Fri, 16 Jan 2026 16:13:08 -0500 Subject: [PATCH 3/5] fix ci tests --- app-e2e/src/Test/E2E/Endpoint/Startup.purs | 6 +++--- .../registry/metadata/type-equality.json | 2 +- app/src/App/Server/Scheduler.purs | 14 ++++++++------ app/test/App/API.purs | 7 +++++-- nix/test/config.nix | 17 +++++++++-------- 5 files changed, 26 insertions(+), 20 deletions(-) diff --git a/app-e2e/src/Test/E2E/Endpoint/Startup.purs b/app-e2e/src/Test/E2E/Endpoint/Startup.purs index eb17e02cc..27aa2d303 100644 --- a/app-e2e/src/Test/E2E/Endpoint/Startup.purs +++ b/app-e2e/src/Test/E2E/Endpoint/Startup.purs @@ -83,7 +83,7 @@ spec = do Spec.describe "scheduleTransfers" do Spec.it "enqueues transfer jobs when package location changes" do - -- type-equality metadata says old-owner, but tags point to purescript + -- type-equality metadata says purescript, but tags point to new-owner jobs <- Client.getJobs let isTypeEqualityTransferJob :: Job -> Boolean @@ -101,8 +101,8 @@ spec = do Transfer { newLocation } -> case newLocation of GitHub { owner } -> - when (owner /= "purescript") do - Assert.fail $ "Expected owner 'purescript' but got '" <> owner <> "'" + when (owner /= "new-owner") do + Assert.fail $ "Expected owner 'new-owner' but got '" <> owner <> "'" _ -> Assert.fail "Expected GitHub location" _ -> Assert.fail "Expected Transfer payload" Just _ -> Assert.fail "Expected TransferJob but got different job type" diff --git a/app/fixtures/registry/metadata/type-equality.json b/app/fixtures/registry/metadata/type-equality.json index 35c13b758..e51b52614 100644 --- a/app/fixtures/registry/metadata/type-equality.json +++ b/app/fixtures/registry/metadata/type-equality.json @@ -1,6 +1,6 @@ { "location": { - "githubOwner": "old-owner", + "githubOwner": "purescript", "githubRepo": "purescript-type-equality" }, "published": { diff --git a/app/src/App/Server/Scheduler.purs b/app/src/App/Server/Scheduler.purs index 1d1b2d5d2..f0c884241 100644 --- a/app/src/App/Server/Scheduler.purs +++ b/app/src/App/Server/Scheduler.purs @@ -257,12 +257,14 @@ enqueuePublishJob allMetadata name (Metadata metadata) version ref = do -- Find the highest published version of each dependency within its range let depVersions :: Map PackageName Version - depVersions = Map.mapMaybeWithKey (\depName range -> - case Map.lookup depName allMetadata of - Just (Metadata depMeta) -> - Array.last $ Array.filter (Range.includes range) $ Array.sort $ Array.fromFoldable $ Map.keys depMeta.published - Nothing -> Nothing - ) manifest.dependencies + depVersions = Map.mapMaybeWithKey + ( \depName range -> + case Map.lookup depName allMetadata of + Just (Metadata depMeta) -> + Array.last $ Array.filter (Range.includes range) $ Array.sort $ Array.fromFoldable $ Map.keys depMeta.published + Nothing -> Nothing + ) + manifest.dependencies case compatibleCompilers allMetadata depVersions of Just compilerSet -> pure $ NonEmptySet.min compilerSet diff --git a/app/test/App/API.purs b/app/test/App/API.purs index 28f17f90e..218796a3d 100644 --- a/app/test/App/API.purs +++ b/app/test/App/API.purs @@ -142,7 +142,9 @@ spec = do Nothing -> Except.throw $ "Expected " <> formatPackageVersion name version <> " to be in metadata." Just published -> do let many' = NonEmptyArray.toArray published.compilers - let expected = map Utils.unsafeVersion [ "0.15.10", "0.15.11" ] + -- Only 0.15.10 is expected because prelude only has 0.15.10 in metadata, + -- so the solver cannot find a solution for 0.15.11 + let expected = map Utils.unsafeVersion [ "0.15.10" ] unless (many' == expected) do Except.throw $ "Expected " <> formatPackageVersion name version <> " to have a compiler matrix of " <> Utils.unsafeStringify (map Version.print expected) <> " but got " <> Utils.unsafeStringify (map Version.print many') @@ -191,7 +193,8 @@ spec = do Nothing -> Except.throw $ "Expected " <> formatPackageVersion transitive.name transitive.version <> " to be in metadata." Just published -> do let many' = NonEmptyArray.toArray published.compilers - let expected = map Utils.unsafeVersion [ "0.15.10", "0.15.11" ] + -- Only 0.15.10 is expected because prelude only has 0.15.10 in metadata + let expected = map Utils.unsafeVersion [ "0.15.10" ] unless (many' == expected) do Except.throw $ "Expected " <> formatPackageVersion transitive.name transitive.version <> " to have a compiler matrix of " <> Utils.unsafeStringify (map Version.print expected) <> " but got " <> Utils.unsafeStringify (map Version.print many') diff --git a/nix/test/config.nix b/nix/test/config.nix index afd6f187f..9a9bfc931 100644 --- a/nix/test/config.nix +++ b/nix/test/config.nix @@ -289,12 +289,12 @@ let }; } # Tags for type-equality package (used by two scheduler tests): - # 1. Transfer detection: metadata says old-owner, commit URLs point to purescript + # 1. Transfer detection: metadata says purescript, commit URLs point to new-owner # 2. Legacy imports: v4.0.2 is a new version not yet published { request = { method = "GET"; - url = "/repos/old-owner/purescript-type-equality/tags"; + url = "/repos/purescript/purescript-type-equality/tags"; }; response = { status = 200; @@ -304,8 +304,8 @@ let name = "v4.0.1"; commit = { sha = "type-eq-sha-401"; - # Points to actual owner - scheduler detects this transfer - url = "https://api.github.com/repos/purescript/purescript-type-equality/commits/type-eq-sha-401"; + # Points to new owner - scheduler detects this transfer + url = "https://api.github.com/repos/new-owner/purescript-type-equality/commits/type-eq-sha-401"; }; } { @@ -313,7 +313,7 @@ let commit = { sha = "type-eq-sha-402"; # New version not yet published - scheduler detects for legacy import - url = "https://api.github.com/repos/purescript/purescript-type-equality/commits/type-eq-sha-402"; + url = "https://api.github.com/repos/new-owner/purescript-type-equality/commits/type-eq-sha-402"; }; } ]; @@ -401,14 +401,15 @@ let # Parse metadata files to get the actual published versions (not just package names) # Returns a set like { "prelude-6.0.1" = true; "type-equality-4.0.1" = true; } - publishedVersions = lib.foldl' (acc: fileName: + publishedVersions = lib.foldl' ( + acc: fileName: let packageName = lib.removeSuffix ".json" fileName; metadata = builtins.fromJSON (builtins.readFile (metadataFixturesDir + "/${fileName}")); - versions = builtins.attrNames (metadata.published or {}); + versions = builtins.attrNames (metadata.published or { }); in acc // lib.genAttrs (map (v: "${packageName}-${v}") versions) (_: true) - ) {} metadataFiles; + ) { } metadataFiles; # ============================================================================ # UNIFIED STORAGE MAPPINGS WITH WIREMOCK SCENARIOS From 4dddabf0384edc22718992514105f0622b52251f Mon Sep 17 00:00:00 2001 From: Thomas Honeyman Date: Fri, 16 Jan 2026 16:23:47 -0500 Subject: [PATCH 4/5] Exhaustive case for effects --- app/src/App/Effect/Registry.purs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/src/App/Effect/Registry.purs b/app/src/App/Effect/Registry.purs index 68071a40d..bbea82e69 100644 --- a/app/src/App/Effect/Registry.purs +++ b/app/src/App/Effect/Registry.purs @@ -221,7 +221,7 @@ withRepoLock . Process -> RepoLocks -> RepoKey - -> Run (LOG + AFF + EFFECT + EXCEPT String + r) a + -> Run (LOG + AFF + EFFECT + EXCEPT String + ()) a -> Run (LOG + AFF + EFFECT + EXCEPT String + r) a withRepoLock = withRepoLockTimeout (Milliseconds 60_000.0) @@ -233,7 +233,7 @@ withRepoLockTimeout -> Process -> RepoLocks -> RepoKey - -> Run (LOG + AFF + EFFECT + EXCEPT String + r) a + -> Run (LOG + AFF + EFFECT + EXCEPT String + ()) a -> Run (LOG + AFF + EFFECT + EXCEPT String + r) a withRepoLockTimeout timeout process locks key action = do repoLock <- Run.liftAff $ getOrCreateLock locks key @@ -262,15 +262,15 @@ withRepoLockTimeout timeout process locks key action = do Just (Right value) -> pure value where - runWithLogs :: Ref (Array (Log Unit)) -> Run (LOG + AFF + EFFECT + EXCEPT String + r) a -> Aff (Either Aff.Error a) + runWithLogs :: Ref (Array (Log Unit)) -> Run (LOG + AFF + EFFECT + EXCEPT String ()) a -> Aff (Either Aff.Error a) runWithLogs ref = Aff.attempt <<< Run.runCont step pure where step = - Run.on Log._log handleLog - $ Run.on (Proxy @"aff") (\k -> k >>= identity) - $ Run.on (Proxy @"effect") (\k -> liftEffect k >>= identity) - $ Run.on Except._except (\k -> Aff.throwError (Aff.error (coerce k))) - $ Run.default (Aff.throwError $ Aff.error "withRepoLock: unexpected effect") + Run.case_ + # Run.on Log._log handleLog + # Run.on (Proxy @"aff") (\k -> k >>= identity) + # Run.on (Proxy @"effect") (\k -> liftEffect k >>= identity) + # Run.on Except._except (\k -> Aff.throwError (Aff.error (coerce k))) handleLog (Log.Log level message next) = do liftEffect $ Ref.modify_ (\logs -> Array.snoc logs (Log.Log level message unit)) ref From 39d827c4207ca64baef9d6e621db0cf556e6882d Mon Sep 17 00:00:00 2001 From: Thomas Honeyman Date: Fri, 16 Jan 2026 16:37:17 -0500 Subject: [PATCH 5/5] use except.throw, not aff.throw --- app/src/App/Effect/Registry.purs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/App/Effect/Registry.purs b/app/src/App/Effect/Registry.purs index bbea82e69..cd5b66f1d 100644 --- a/app/src/App/Effect/Registry.purs +++ b/app/src/App/Effect/Registry.purs @@ -256,9 +256,9 @@ withRepoLockTimeout timeout process locks key action = do case outcome of Nothing -> do Log.warn $ "Repo lock timed out for " <> printProcess process - Run.liftAff $ Aff.throwError $ Aff.error "Repo lock timed out." + Except.throw "Repo lock timed out." Just (Left err) -> - Run.liftAff $ Aff.throwError err + Except.throw $ "Repo action failed: " <> Aff.message err Just (Right value) -> pure value where