diff --git a/README.md b/README.md index 8bd12b1ab..216bea58d 100644 --- a/README.md +++ b/README.md @@ -130,6 +130,7 @@ Where to go from here? There are a few places you should check out: - [Querying package sets](#querying-package-sets) - [Upgrading packages and the package set](#upgrading-packages-and-the-package-set) - [Custom package sets](#custom-package-sets) + - [Graph the project modules and dependencies](#graph-the-project-modules-and-dependencies) - [Monorepo support](#monorepo-support) - [Polyrepo support](#polyrepo-support) - [Test dependencies](#test-dependencies) @@ -142,6 +143,9 @@ Where to go from here? There are a few places you should check out: - [Generate documentation for my project](#generate-documentation-for-my-project) - [Alternate backends](#alternate-backends) - [Publish my library](#publish-my-library) + - [Publish many packages together](#publish-many-packages-together) + - [Authenticated commands](#authenticated-commands) + - [Transfer my package to a new owner](#transfer-my-package-to-a-new-owner) - [Know which `purs` commands are run under the hood](#know-which-purs-commands-are-run-under-the-hood) - [Install autocompletions for `bash`](#install-autocompletions-for-bash) - [Install autocompletions for `zsh`](#install-autocompletions-for-zsh) @@ -151,10 +155,12 @@ Where to go from here? There are a few places you should check out: - [The workspace](#the-workspace) - [The configuration file](#the-configuration-file) - [The lock file](#the-lock-file) + - [File System Paths used in Spago](#file-system-paths-used-in-spago) - [FAQ](#faq) - [Why can't `spago` also install my npm dependencies?](#why-cant-spago-also-install-my-npm-dependencies) -- [Differences from legacy spago](#differences-from-legacy-spago) - - [Watch mode](#watch-mode) + - [Differences from legacy spago](#differences-from-legacy-spago) + - [Watch mode](#watch-mode) + - [`sources` in the configuration file](#sources-in-the-configuration-file) @@ -1092,6 +1098,29 @@ workspace: > [!NOTE]\ > This only works when the package you add to `extraPackages` has been published to the registry. Adding a git dependency will produce an error, as publishing to the Registry only admits build plans that only contain packages coming from the Registry. +### Authenticated commands + +The Registry does not need authentication when publishing new versions of a package, but it does need it when issuing +operations that modify existing packages, [such as location transfer or unpublishing](registry-dev-auth). + +This authentication happens through SSH keys: by having your public key in a published version, the Registry can then +authenticate requests that are signed with your private key. + +Authentication and operations that use it are automated by Spago, through the `spago auth` command: if you'd like to +be able to perform authenticated operations you need a SSH keypair, and run `spago auth` passing those keys in. +This will populate the `package.publish.owners` field in the `spago.yaml` - commit that and publish a new version, +and from that moment you'll be able to perform authenticated operations on this package. + +#### Transfer my package to a new owner + +If you are the owner of a package and you want to transfer it to another user, you'd need to inform the Registry +about the new location of the repository, so that the new owner will be able to publish new versions of the package. + +The transfer procedure is automated by Spago commands, and goes as follows: +* Add your (or the new owner's) SSH public key to the `spago.yaml` through `spago auth` if they are not there already (see previous section) +* Transfer the repository to the new owner using the hosting platform's transfer mechanism (e.g. GitHub's transfer feature) +* Depending on whose key is present in the `owners` field, either you or the new owner will update the `publish.location` field in the `spago.yaml`, and call `spago registry transfer` to initiate the transfer. If all goes well you'll now be able to publish a new version from the new location. + ### Know which `purs` commands are run under the hood The `-v` flag will print out all the `purs` commands that `spago` invokes during its operations, @@ -1621,3 +1650,4 @@ and similarly for the `test` folder, using that for the test sources. [watchexec]: https://github.com/watchexec/watchexec#quick-start [purescript-langugage-server]: https://github.com/nwolverson/purescript-language-server [ide-purescript]: https://marketplace.visualstudio.com/items?itemName=nwolverson.ide-purescript +[registry-dev-auth]: https://github.com/purescript/registry-dev/blob/master/SPEC.md#52-authentication diff --git a/bin/src/Flags.purs b/bin/src/Flags.purs index eb11f95c8..28e3d0e27 100644 --- a/bin/src/Flags.purs +++ b/bin/src/Flags.purs @@ -335,3 +335,19 @@ depsOnly = ( O.long "deps-only" <> O.help "Build depedencies only" ) + +publicKeyPath :: Parser FilePath +publicKeyPath = + O.strOption + ( O.short 'i' + <> O.metavar "PUBLIC_KEY_PATH" + <> O.help "Select the path of the public key to use for authenticating operations of the package" + ) + +privateKeyPath :: Parser FilePath +privateKeyPath = + O.strOption + ( O.short 'i' + <> O.metavar "PRIVATE_KEY_PATH" + <> O.help "The path of the private key to use for signing the operation" + ) diff --git a/bin/src/Main.purs b/bin/src/Main.purs index 74009d649..2665fc7aa 100644 --- a/bin/src/Main.purs +++ b/bin/src/Main.purs @@ -23,6 +23,7 @@ import Optparse as Optparse import Record as Record import Registry.PackageName as PackageName import Spago.Bin.Flags as Flags +import Spago.Command.Auth as Auth import Spago.Command.Build as Build import Spago.Command.Bundle as Bundle import Spago.Command.Docs as Docs @@ -33,7 +34,7 @@ import Spago.Command.Init as Init import Spago.Command.Ls (LsPathsArgs, LsDepsArgs, LsPackagesArgs) import Spago.Command.Ls as Ls import Spago.Command.Publish as Publish -import Spago.Command.Registry (RegistryInfoArgs, RegistrySearchArgs, RegistryPackageSetsArgs) +import Spago.Command.Registry (RegistryInfoArgs, RegistryPackageSetsArgs, RegistrySearchArgs, RegistryTransferArgs) import Spago.Command.Registry as RegistryCmd import Spago.Command.Repl as Repl import Spago.Command.Run as Run @@ -203,6 +204,7 @@ data Command a | RegistryInfo RegistryInfoArgs | RegistryPackageSets RegistryPackageSetsArgs | RegistrySearch RegistrySearchArgs + | RegistryTransfer RegistryTransferArgs | Repl ReplArgs | Run RunArgs | Sources SourcesArgs @@ -210,6 +212,7 @@ data Command a | Upgrade UpgradeArgs | GraphModules GraphModulesArgs | GraphPackages GraphPackagesArgs + | Auth Auth.AuthArgs commandParser :: forall (a :: Row Type). String -> Parser (Command a) -> String -> Mod CommandFields (SpagoCmd a) commandParser command_ parser_ description_ = @@ -222,29 +225,22 @@ commandParser command_ parser_ description_ = argParser :: Parser (SpagoCmd ()) argParser = O.hsubparser $ Foldable.fold - [ commandParser "init" (Init <$> initArgsParser) "Initialise a new project" - , commandParser "fetch" (Fetch <$> fetchArgsParser) "Downloads all of the project's dependencies" - , commandParser "install" (Install <$> installArgsParser) "Compile the project's dependencies" - , commandParser "uninstall" (Uninstall <$> uninstallArgsParser) "Remove dependencies from a package" + [ commandParser "auth" (Auth <$> authArgsParser) "Authenticate as the owner of a package, to allow transfer and unpublish operations" , commandParser "build" (Build <$> buildArgsParser) "Compile the project" - , commandParser "run" (Run <$> runArgsParser) "Run the project" - , commandParser "test" (Test <$> testArgsParser) "Test the project" , commandParser "bundle" (Bundle <$> bundleArgsParser) "Bundle the project in a single file" - , commandParser "sources" (Sources <$> sourcesArgsParser) "List all the source paths (globs) for the dependencies of the project" - , commandParser "repl" (Repl <$> replArgsParser) "Start a REPL" - , commandParser "publish" (Publish <$> publishArgsParser) "Publish a package" - , commandParser "upgrade" (Upgrade <$> upgradeArgsParser) "Upgrade to the latest package set, or to the latest versions of Registry packages" , commandParser "docs" (Docs <$> docsArgsParser) "Generate docs for the project and its dependencies" - , O.command "registry" + , commandParser "fetch" (Fetch <$> fetchArgsParser) "Downloads all of the project's dependencies" + , O.command "graph" ( O.info ( O.hsubparser $ Foldable.fold - [ commandParser "search" (RegistrySearch <$> registrySearchArgsParser) "Search for package names in the Registry" - , commandParser "info" (RegistryInfo <$> registryInfoArgsParser) "Query the Registry for information about packages and versions" - , commandParser "package-sets" (RegistryPackageSets <$> registryPackageSetsArgsParser) "List the available package sets" + [ commandParser "modules" (GraphModules <$> graphModulesArgsParser) "Generate a graph of the project's modules" + , commandParser "packages" (GraphPackages <$> graphPackagesArgsParser) "Generate a graph of the project's dependencies" ] ) - (O.progDesc "Commands to interact with the Registry") + (O.progDesc "Generate a graph of modules or dependencies") ) + , commandParser "init" (Init <$> initArgsParser) "Initialise a new project" + , commandParser "install" (Install <$> installArgsParser) "Compile the project's dependencies" , O.command "ls" ( O.info ( O.hsubparser $ Foldable.fold @@ -255,26 +251,26 @@ argParser = ) (O.progDesc "List packages or dependencies") ) - , O.command "graph" + , commandParser "publish" (Publish <$> publishArgsParser) "Publish a package" + , O.command "registry" ( O.info ( O.hsubparser $ Foldable.fold - [ commandParser "modules" (GraphModules <$> graphModulesArgsParser) "Generate a graph of the project's modules" - , commandParser "packages" (GraphPackages <$> graphPackagesArgsParser) "Generate a graph of the project's dependencies" + [ commandParser "search" (RegistrySearch <$> registrySearchArgsParser) "Search for package names in the Registry" + , commandParser "info" (RegistryInfo <$> registryInfoArgsParser) "Query the Registry for information about packages and versions" + , commandParser "package-sets" (RegistryPackageSets <$> registryPackageSetsArgsParser) "List the available package sets" + , commandParser "transfer" (RegistryTransfer <$> registryTransferArgsParser) "Transfer a package you own to a different remote location" ] ) - (O.progDesc "Generate a graph of modules or dependencies") + (O.progDesc "Commands to interact with the Registry") ) + , commandParser "repl" (Repl <$> replArgsParser) "Start a REPL" + , commandParser "run" (Run <$> runArgsParser) "Run the project" + , commandParser "sources" (Sources <$> sourcesArgsParser) "List all the source paths (globs) for the dependencies of the project" + , commandParser "test" (Test <$> testArgsParser) "Test the project" + , commandParser "uninstall" (Uninstall <$> uninstallArgsParser) "Remove dependencies from a package" + , commandParser "upgrade" (Upgrade <$> upgradeArgsParser) "Upgrade to the latest package set, or to the latest versions of Registry packages" ] -{- - -TODO: add flag for overriding the cache location - - buildOptions = BuildOptions <$> watch <*> clearScreen <*> allowIgnored <*> sourcePaths <*> srcMapFlag <*> noInstall - <*> pursArgs <*> depsOnly <*> beforeCommands <*> thenCommands <*> elseCommands - --} - -- https://stackoverflow.com/questions/45395369/how-to-get-console-log-line-numbers-shown-in-nodejs -- TODO: veryVerbose = CLI.switch "very-verbose" 'V' "Enable more verbosity: timestamps and source locations" @@ -463,6 +459,12 @@ registryPackageSetsArgsParser = , latest: Flags.latest } +registryTransferArgsParser :: Parser RegistryTransferArgs +registryTransferArgsParser = + Optparse.fromRecord + { privateKeyPath: Flags.privateKeyPath + } + graphModulesArgsParser :: Parser GraphModulesArgs graphModulesArgsParser = Optparse.fromRecord { dot: Flags.dot @@ -496,6 +498,11 @@ lsDepsArgsParser = Optparse.fromRecord , pure: Flags.pureLockfile } +authArgsParser :: Parser Auth.AuthArgs +authArgsParser = Optparse.fromRecord + { keyPath: Flags.publicKeyPath + } + data Cmd a = Cmd'SpagoCmd (SpagoCmd a) | Cmd'VersionCmd Boolean parseArgs :: Effect (Cmd ()) @@ -545,8 +552,10 @@ main = do Init args@{ useSolver } -> do -- Fetch the registry here so we can select the right package set later env <- mkRegistryEnv offline + setVersion <- parseSetVersion args.setVersion void $ runSpago env $ Init.run { mode: args.mode, setVersion, useSolver } + Fetch args -> do { env, fetchOpts } <- mkFetchEnv (Record.merge { isRepl: false, migrateConfig, offline } args) void $ runSpago env (Fetch.run fetchOpts) @@ -559,8 +568,11 @@ main = do RegistryPackageSets args -> do env <- mkRegistryEnv offline void $ runSpago env (RegistryCmd.packageSets args) + RegistryTransfer args -> do + { env } <- mkFetchEnv { packages: mempty, selectedPackage: Nothing, pure: false, ensureRanges: false, testDeps: false, isRepl: false, migrateConfig: false, offline } + void $ runSpago env (RegistryCmd.transfer args) Install args -> do - { env, fetchOpts } <- mkFetchEnv (Record.merge args { isRepl: false, migrateConfig, offline }) + { env, fetchOpts } <- mkFetchEnv (Record.merge { isRepl: false, migrateConfig, offline } args) dependencies <- runSpago env (Fetch.run fetchOpts) let buildArgs = Record.merge @@ -670,7 +682,10 @@ main = do Upgrade args -> do setVersion <- parseSetVersion args.setVersion { env } <- mkFetchEnv { packages: mempty, selectedPackage: Nothing, pure: false, ensureRanges: false, testDeps: false, isRepl: false, migrateConfig, offline } - runSpago env $ Upgrade.run { setVersion } + runSpago env (Upgrade.run { setVersion }) + Auth args -> do + { env } <- mkFetchEnv { packages: mempty, selectedPackage: Nothing, pure: false, ensureRanges: false, testDeps: false, isRepl: false, migrateConfig, offline } + runSpago env $ Auth.run args -- TODO: add selected to graph commands GraphModules args -> do { env, fetchOpts } <- mkFetchEnv { packages: mempty, selectedPackage: Nothing, pure: false, ensureRanges: false, testDeps: false, isRepl: false, migrateConfig, offline } diff --git a/core/spago.yaml b/core/spago.yaml index 075d2dd48..ecbcdb369 100644 --- a/core/spago.yaml +++ b/core/spago.yaml @@ -17,6 +17,7 @@ package: - exceptions - filterable - foldable-traversable + - foreign - functions - identity - integers @@ -39,3 +40,4 @@ package: - stringutils - transformers - tuples + - unsafe-coerce diff --git a/core/src/Config.purs b/core/src/Config.purs index 47dbdf25b..45eb9a4ea 100644 --- a/core/src/Config.purs +++ b/core/src/Config.purs @@ -55,6 +55,8 @@ import Registry.Internal.Codec as Reg.Internal.Codec import Registry.Internal.Parsing as Reg.Internal.Parsing import Registry.License as License import Registry.Location as Location +import Registry.Owner (Owner) +import Registry.Owner as Owner import Registry.PackageName as PackageName import Registry.Range as Range import Registry.Sha256 as Sha256 @@ -100,6 +102,7 @@ type PublishConfig = , location :: Maybe Location , include :: Maybe (Array FilePath) , exclude :: Maybe (Array FilePath) + , owners :: Maybe (Array Owner) } publishConfigCodec :: CJ.Codec PublishConfig @@ -109,6 +112,7 @@ publishConfigCodec = CJ.named "PublishConfig" $ CJS.objectStrict $ CJS.recordPropOptional @"location" publishLocationCodec $ CJS.recordPropOptional @"include" (CJ.array CJ.string) $ CJS.recordPropOptional @"exclude" (CJ.array CJ.string) + $ CJS.recordPropOptional @"owners" (CJ.array Owner.codec) $ CJS.record -- This codec duplicates `Location.codec` from the Registry library, but with diff --git a/core/src/FS.purs b/core/src/FS.purs index e1f688a54..304f9f2bf 100644 --- a/core/src/FS.purs +++ b/core/src/FS.purs @@ -7,7 +7,6 @@ module Spago.FS , ensureFileSync , exists , getInBetweenPaths - , isLink , ls , mkdirp , moveSync @@ -15,7 +14,6 @@ module Spago.FS , readTextFile , readYamlDocFile , readYamlFile - , stat , writeFile , writeJsonFile , writeTextFile @@ -33,8 +31,6 @@ import Effect.Uncurried (EffectFn2, runEffectFn2) import Node.FS.Aff as FS.Aff import Node.FS.Perms (Perms) import Node.FS.Perms as Perms -import Node.FS.Stats (Stats) -import Node.FS.Stats as Stats import Node.FS.Sync as FS.Sync import Spago.Json as Json import Spago.Yaml as Yaml @@ -116,14 +112,6 @@ readYamlDocFile codec path = do result <- Aff.attempt $ FS.Aff.readTextFile UTF8 path pure (lmap Aff.message result >>= Yaml.parseYamlDoc codec >>> lmap CJ.DecodeError.print) -stat :: forall m. MonadAff m => FilePath -> m (Either Error Stats) -stat path = liftAff $ try (FS.Aff.stat path) - -isLink :: forall m. MonadEffect m => FilePath -> m Boolean -isLink path = liftEffect $ try (FS.Sync.lstat path) >>= case _ of - Left _err -> pure true -- TODO: we should bubble this up instead - Right stats -> pure $ Stats.isSymbolicLink stats - foreign import getInBetweenPathsImpl :: EffectFn2 FilePath FilePath (Array FilePath) getInBetweenPaths :: forall m. MonadEffect m => FilePath -> FilePath -> m (Array FilePath) diff --git a/core/src/Json.purs b/core/src/Json.purs index 8cc061065..521dc5bec 100644 --- a/core/src/Json.purs +++ b/core/src/Json.purs @@ -8,8 +8,11 @@ import Data.Array as Array import Data.Bifunctor (lmap) import Data.Codec.JSON as CJ import Data.Either (Either) +import Foreign (Foreign) +import JSON (JSON) import JSON as JSON import JSON.Path as JSON.Path +import Unsafe.Coerce (unsafeCoerce) -- | Print a type as a formatted JSON string printJson :: forall a. CJ.Codec a -> a -> String @@ -31,3 +34,7 @@ printConfigError (DecodeError { path, message, causes }) = false -> Array.foldMap printConfigError causes -- If there are none, then we have reached a leaf, and can print the actual error true -> [ JSON.Path.print path <> ": " <> message ] + +-- | Decode a Foreign into JSON +unsafeFromForeign :: Foreign -> JSON +unsafeFromForeign = unsafeCoerce diff --git a/docs-search/index/src/Docs/Search/IndexBuilder.js b/docs-search/index/src/Docs/Search/IndexBuilder.js index a414d2a25..fea3ae81d 100644 --- a/docs-search/index/src/Docs/Search/IndexBuilder.js +++ b/docs-search/index/src/Docs/Search/IndexBuilder.js @@ -1,4 +1,4 @@ -import { globSync } from "glob"; +import { globSync } from 'glob'; import path from "node:path"; import { fileURLToPath } from "node:url"; @@ -6,12 +6,12 @@ export function getDocsSearchAppPath() { const fileName = fileURLToPath(import.meta.url); const absoluteDir = path.dirname(fileName); const basename = path.basename(absoluteDir); - + // unbundled dev build if (basename == "Docs.Search.IndexBuilder") { return path.join(absoluteDir, "..", "..", "bin", "docs-search-app.js"); } - // bundled production build + // bundled production build if (basename === "bin") { return path.join(absoluteDir, "docs-search-app.js"); } diff --git a/flake.lock b/flake.lock index c35fedc0b..7964b16a5 100644 --- a/flake.lock +++ b/flake.lock @@ -41,11 +41,11 @@ "slimlock": "slimlock" }, "locked": { - "lastModified": 1724938718, - "narHash": "sha256-lE6fUo7cgMcO20mH+9VRhDoMLlC99K5lMEVuJm+UGEI=", + "lastModified": 1728546539, + "narHash": "sha256-Sws7w0tlnjD+Bjck1nv29NjC5DbL6nH5auL9Ex9Iz2A=", "owner": "thomashoneyman", "repo": "purescript-overlay", - "rev": "9970a549c72aa7a787ea7cc205a0d52c3589f799", + "rev": "4ad4c15d07bd899d7346b331f377606631eb0ee4", "type": "github" }, "original": { diff --git a/package-lock.json b/package-lock.json index 3b2be2743..e9ce11fe9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "open": "^10.1.0", "picomatch": "^4.0.2", "punycode": "^2.3.1", + "readline-sync": "^1.4.10", "semver": "^7.6.2", "spdx-expression-parse": "^4.0.0", "ssh2": "^1.15.0", @@ -1031,6 +1032,14 @@ "node": ">= 6" } }, + "node_modules/readline-sync": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/readline-sync/-/readline-sync-1.4.10.tgz", + "integrity": "sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw==", + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", diff --git a/package.json b/package.json index d544ce310..ffe90133b 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "open": "^10.1.0", "picomatch": "^4.0.2", "punycode": "^2.3.1", + "readline-sync": "^1.4.10", "semver": "^7.6.2", "spdx-expression-parse": "^4.0.0", "ssh2": "^1.15.0", diff --git a/spago.lock b/spago.lock index b7968ba07..a139fa007 100644 --- a/spago.lock +++ b/spago.lock @@ -466,7 +466,6 @@ "affjax", "affjax-node", "ansi", - "argonaut-core", "arrays", "avar", "codec", @@ -480,6 +479,8 @@ "effect", "either", "enums", + "exceptions", + "fetch", "filterable", "foldable-traversable", "foreign-object", @@ -513,6 +514,7 @@ "spago-core", "strings", "strings-extra", + "stringutils", "transformers", "tuples", "unfoldable", @@ -548,6 +550,7 @@ "enums", "exceptions", "exists", + "fetch", "filterable", "fixed-points", "foldable-traversable", @@ -565,6 +568,9 @@ "integers", "invariant", "js-date", + "js-fetch", + "js-promise", + "js-promise-aff", "js-timers", "js-uri", "json", @@ -632,13 +638,13 @@ "web-file", "web-html", "web-storage", + "web-streams", "web-xhr" ] }, "test": { "dependencies": [ "exceptions", - "identity", "quickcheck", "spec", "spec-node" @@ -778,6 +784,7 @@ "exceptions", "exists", "exitcodes", + "fetch", "filterable", "fixed-points", "foldable-traversable", @@ -795,6 +802,9 @@ "integers", "invariant", "js-date", + "js-fetch", + "js-promise", + "js-promise-aff", "js-timers", "js-uri", "json", @@ -865,6 +875,7 @@ "web-file", "web-html", "web-storage", + "web-streams", "web-xhr" ] }, @@ -891,6 +902,7 @@ "exceptions", "filterable", "foldable-traversable", + "foreign", "functions", "identity", "integers", @@ -912,7 +924,8 @@ "strings", "stringutils", "transformers", - "tuples" + "tuples", + "unsafe-coerce" ], "build_plan": [ "aff", @@ -2185,6 +2198,32 @@ "enums" ] }, + "fetch": { + "type": "registry", + "version": "4.1.0", + "integrity": "sha256-zCwBUkRL9n6nUhK1+7UqqsuxswPFATsZfGSBOA3NYYY=", + "dependencies": [ + "aff", + "arraybuffer-types", + "bifunctors", + "effect", + "either", + "foreign", + "http-methods", + "js-fetch", + "js-promise", + "js-promise-aff", + "maybe", + "newtype", + "ordered-collections", + "prelude", + "record", + "strings", + "typelevel-prelude", + "web-file", + "web-streams" + ] + }, "filterable": { "type": "registry", "version": "5.0.0", @@ -2515,6 +2554,31 @@ "now" ] }, + "js-fetch": { + "type": "registry", + "version": "0.2.1", + "integrity": "sha256-zQaVi9wFWku1SsWmdR11kRpOb+wxkNWR49cn928ucjw=", + "dependencies": [ + "arraybuffer-types", + "arrays", + "effect", + "foldable-traversable", + "foreign", + "foreign-object", + "functions", + "http-methods", + "js-promise", + "maybe", + "newtype", + "prelude", + "record", + "tuples", + "typelevel-prelude", + "unfoldable", + "web-file", + "web-streams" + ] + }, "js-promise": { "type": "registry", "version": "1.0.0", @@ -2528,6 +2592,16 @@ "prelude" ] }, + "js-promise-aff": { + "type": "registry", + "version": "1.0.0", + "integrity": "sha256-s9kml9Ei74hKlMMg41yyZp4GkbmYUwaH+gBWWrdhwec=", + "dependencies": [ + "aff", + "foreign", + "js-promise" + ] + }, "js-timers": { "type": "registry", "version": "6.1.0", @@ -3733,6 +3807,20 @@ "web-events" ] }, + "web-streams": { + "type": "registry", + "version": "4.0.0", + "integrity": "sha256-02HgXIk6R+pU9fWOX42krukAI1QkCbLKcCv3b4Jq6WI=", + "dependencies": [ + "arraybuffer-types", + "effect", + "exceptions", + "js-promise", + "nullable", + "prelude", + "tuples" + ] + }, "web-touchevents": { "type": "registry", "version": "4.0.0", diff --git a/spago.yaml b/spago.yaml index 6b1f8278b..62e9d6cee 100644 --- a/spago.yaml +++ b/spago.yaml @@ -17,7 +17,6 @@ package: - affjax - affjax-node - ansi - - argonaut-core - arrays - avar - codec @@ -31,6 +30,8 @@ package: - effect - either - enums + - exceptions + - fetch - filterable - foldable-traversable - foreign-object @@ -64,6 +65,7 @@ package: - spago-core - strings - strings-extra + - stringutils - transformers - tuples - unfoldable @@ -76,7 +78,6 @@ package: - ImplicitQualifiedImport dependencies: - exceptions - - identity - quickcheck - spec - spec-node diff --git a/src/Spago/Command/Auth.purs b/src/Spago/Command/Auth.purs new file mode 100644 index 000000000..75f0f8b3e --- /dev/null +++ b/src/Spago/Command/Auth.purs @@ -0,0 +1,57 @@ +-- | The `spago auth` command is used exclusively to add a new key to the list of owners of a package. +module Spago.Command.Auth where + +import Spago.Prelude + +import Data.Array as Array +import Data.String (Pattern(..)) +import Data.String as String +import Node.Path as Path +import Registry.SSH as SSH +import Spago.Command.Fetch (FetchEnv) +import Spago.Config as Config +import Spago.FS as FS + +type AuthArgs = { keyPath :: FilePath } + +run :: AuthArgs -> Spago (FetchEnv _) Unit +run { keyPath } = do + logDebug $ "Authenticating with key at path " <> keyPath + let + -- we don't want to accidentally read the private key, so we always point to the public + path = case String.stripSuffix (Pattern ".pub") keyPath of + Just _rest -> keyPath + Nothing -> keyPath <> ".pub" + + newOwner <- FS.exists path >>= case _ of + false -> do + die $ "Cannot read public key at path " <> show path <> ": file does not exist." + true -> do + content <- FS.readTextFile path + let result = SSH.parsePublicKey content + case result of + Left err -> die [ "Could not parse SSH public key. Error was:", err ] + Right public -> pure $ SSH.publicKeyToOwner public + logDebug $ "Parsed owner: " <> show (unwrap newOwner) + + { workspace } <- ask + { doc, package, configPath } <- case workspace.selected, workspace.rootPackage of + Just { doc, package, path: packagePath }, _ -> pure { doc, package, configPath: Path.concat [ packagePath, "spago.yaml" ] } + Nothing, Just rootPackage -> pure { doc: workspace.doc, package: rootPackage, configPath: "spago.yaml" } + Nothing, Nothing -> die "No package was selected. Please select a package with the -p flag" + + case package.publish of + Nothing -> die + [ "The package you are trying to authenticate for is not set up for publishing." + , "Please set the `publish` field in the spago.yaml file - see the docs for more info:" + , "https://github.com/purescript/spago#the-configuration-file" + ] + Just { owners: maybeOwners } -> do + let currentOwners = fromMaybe [] maybeOwners + case Array.elem newOwner currentOwners of + true -> logWarn "Selected key is already present in the config file." + false -> do + logInfo $ "Adding selected key to the list of the owners: " <> path + Config.addOwner configPath doc newOwner + logSuccess "The selected key has been added to the list of the owners." + logInfo "Once you publish a new version with this configuration you'll be able to transfer and unpublish packages using this key." diff --git a/src/Spago/Command/Fetch.purs b/src/Spago/Command/Fetch.purs index 7d246661b..062b223e7 100644 --- a/src/Spago/Command/Fetch.purs +++ b/src/Spago/Command/Fetch.purs @@ -254,7 +254,7 @@ fetchPackagesToLocalCache packages = do case tarExists, tarIsGood, offline of true, true, _ -> pure unit -- Tar exists and is good, and we already unpacked it. Happy days! _, _, Offline -> die $ "Package " <> packageVersion <> " is not in the local cache, and Spago is running in offline mode - can't make progress." - _, _, Online -> do + _, _, _ -> do let packageUrl = "https://packages.registry.purescript.org/" <> PackageName.print name <> "/" <> versionString <> ".tar.gz" logInfo $ "Fetching package " <> packageVersion response <- liftAff $ withBackoff' do @@ -488,7 +488,7 @@ getGitPackageInLocalCache name package = do pure unit Left _, Offline -> die $ "Repo " <> package.git <> " does not have ref " <> package.ref <> " in local cache. Cannot pull from origin in offline mode." - Left _, Online -> do + Left _, _ -> do logDebug $ "Ref " <> package.ref <> " is not present, trying to pull from origin" Git.fetch { repo: repoCacheLocation, remote: "origin" } >>= rightOrDie_ diff --git a/src/Spago/Command/Publish.purs b/src/Spago/Command/Publish.purs index d7ec35c8d..e50cf3ec8 100644 --- a/src/Spago/Command/Publish.purs +++ b/src/Spago/Command/Publish.purs @@ -2,29 +2,16 @@ module Spago.Command.Publish (publish, PublishEnv) where import Spago.Prelude -import Affjax.Node as Http -import Affjax.RequestBody as RequestBody -import Affjax.ResponseFormat as ResponseFormat -import Affjax.StatusCode (StatusCode(..)) -import Data.Argonaut.Core (Json) import Data.Array as Array import Data.Array.NonEmpty as NEA -import Data.Codec.JSON as CJ -import Data.DateTime (DateTime) -import Data.Formatter.DateTime as DateTime import Data.List as List import Data.Map as Map import Data.String as String import Dodo (break, lines) -import Effect.Aff (Milliseconds(..)) -import Effect.Aff as Aff import Effect.Ref as Ref -import JSON (JSON) import Node.Path as Path import Node.Process as Process import Record as Record -import Registry.API.V1 as V1 -import Registry.Internal.Format as Internal.Format import Registry.Internal.Path as Internal.Path import Registry.Location as Location import Registry.Metadata as Metadata @@ -32,7 +19,6 @@ import Registry.Operation as Operation import Registry.Operation.Validation as Operation.Validation import Registry.PackageName as PackageName import Registry.Version as Version -import Routing.Duplex as Duplex import Spago.Command.Build (BuildEnv) import Spago.Command.Build as Build import Spago.Command.Fetch as Fetch @@ -43,14 +29,12 @@ import Spago.FS as FS import Spago.Git (Git) import Spago.Git as Git import Spago.Json as Json -import Spago.Log (LogVerbosity(..)) import Spago.Log as Log import Spago.Prelude as Effect import Spago.Purs (Purs) import Spago.Purs.Graph as Graph import Spago.Registry (PreRegistryEnv) import Spago.Registry as Registry -import Unsafe.Coerce (unsafeCoerce) type PublishData = { name :: PackageName @@ -176,7 +160,7 @@ publish _args = do logDebug $ "Got error while reading metadata file: " <> err pure { location - , owners: Nothing -- TODO: get that from the config file + , owners: NEA.fromArray =<< publishConfig.owners , published: Map.empty , unpublished: Map.empty } @@ -188,7 +172,7 @@ publish _args = do , dependencies: depsRanges , version: publishConfig.version , license: publishConfig.license - , owners: Nothing -- TODO specify owners in spago config + , owners: NEA.fromArray =<< publishConfig.owners , excludeFiles: Nothing -- TODO specify files in spago config , includeFiles: Nothing -- TODO specify files in spago config } @@ -202,10 +186,13 @@ publish _args = do , "submit a transfer operation." ] - unlessM (locationIsInGitRemotes location) $ addError $ toDoc - [ "The location specified in the manifest file" - , "(" <> Json.stringifyJson Location.codec location <> ")" - , " is not one of the remotes in the git repository." + locationResult <- locationIsInGitRemotes location + unless locationResult.result $ addError $ toDoc + [ toDoc "The location specified in the manifest file is not one of the remotes in the git repository." + , toDoc "Location:" + , indent (toDoc $ "- " <> prettyPrintLocation location) + , toDoc "Remotes:" + , lines $ locationResult.remotes <#> \r -> indent $ toDoc $ "- " <> r.name <> ": " <> r.url ] -- Check that all the dependencies come from the registry @@ -278,7 +265,7 @@ publish _args = do Git.getStatus Nothing >>= case _ of Left _err -> do die $ toDoc - [ toDoc "Could not verify whether the git tree is clean due to the below error:" + [ toDoc "Could not verify whether the git tree is clean. Error was:" , indent _err ] Right statusResult @@ -381,13 +368,7 @@ publish _args = do logSuccess "Ready for publishing. Calling the registry.." let newPublishingData = publishingData { resolutions = Just publishingData.resolutions } :: Operation.PublishData - - { jobId } <- callRegistry (baseApi <> Duplex.print V1.routes V1.Publish) V1.jobCreatedResponseCodec (Just { codec: Operation.publishCodec, data: newPublishingData }) - logSuccess $ "Registry accepted the Publish request and is processing..." - logDebug $ "Job ID: " <> unwrap jobId - logInfo "Logs from the Registry pipeline:" - waitForJobFinish jobId - + Registry.submitRegistryOperation (Operation.Publish newPublishingData) pure newPublishingData where -- If you are reading this and think that you can make it look nicer with @@ -406,85 +387,21 @@ publish _args = do } action -callRegistry :: forall env a b. String -> CJ.Codec b -> Maybe { codec :: CJ.Codec a, data :: a } -> Spago (PublishEnv env) b -callRegistry url outputCodec maybeInput = handleError do - logDebug $ "Calling registry at " <> url - response <- liftAff $ withBackoff' $ case maybeInput of - Just { codec: inputCodec, data: input } -> Http.post ResponseFormat.string url (Just $ RequestBody.json $ (unsafeCoerce :: JSON -> Json) $ CJ.encode inputCodec input) - Nothing -> Http.get ResponseFormat.string url - case response of - Nothing -> pure $ Left $ "Could not reach the registry at " <> url - Just (Left err) -> pure $ Left $ "Error while calling the registry:\n " <> Http.printError err - Just (Right { status, body }) | status /= StatusCode 200 -> do - pure $ Left $ "Registry did not like this and answered with status " <> show status <> ", got answer:\n " <> body - Just (Right { body }) -> do - pure $ case parseJson outputCodec body of - Right output -> Right output - Left err -> Left $ "Could not parse response from the registry, error: " <> show err - where - -- TODO: see if we want to just kill the process generically here, or give out customized errors - handleError a = do - { offline } <- ask - case offline of - Offline -> die "Spago is offline - not able to call the Registry." - Online -> - a >>= case _ of - Left err -> die err - Right res -> pure res - -waitForJobFinish :: forall env. V1.JobId -> Spago (PublishEnv env) Unit -waitForJobFinish jobId = go Nothing - where - go :: Maybe DateTime -> Spago (PublishEnv env) Unit - go lastTimestamp = do - { logOptions } <- ask - let - url = baseApi <> Duplex.print V1.routes - ( V1.Job jobId - { since: lastTimestamp - , level: case logOptions.verbosity of - LogVerbose -> Just V1.Debug - _ -> Just V1.Info - } - ) - jobInfo :: V1.Job <- callRegistry url V1.jobCodec Nothing - -- first of all, print all the logs we get - for_ jobInfo.logs \log -> do - let line = Log.indent $ toDoc $ DateTime.format Internal.Format.iso8601DateTime log.timestamp <> " " <> log.message - case log.level of - V1.Debug -> logDebug line - V1.Info -> logInfo line - V1.Warn -> logWarn line - V1.Error -> logError line - case jobInfo.finishedAt of - Nothing -> do - -- If the job is not finished, we grab the timestamp of the last log line, wait a bit and retry - let - latestTimestamp = jobInfo.logs # Array.last # case _ of - Just log -> Just log.timestamp - Nothing -> lastTimestamp - liftAff $ Aff.delay $ Milliseconds 500.0 - go latestTimestamp - Just _finishedAt -> do - -- if it's done we report the failure. - logDebug $ "Job: " <> printJson V1.jobCodec jobInfo - case jobInfo.success of - true -> logSuccess $ "Registry finished processing the package. Your package was published successfully!" - false -> die $ "Registry finished processing the package, but it failed. Please fix it and try again." - -locationIsInGitRemotes :: ∀ a. Location -> Spago (PublishEnv a) Boolean +locationIsInGitRemotes :: ∀ a. Location -> Spago (PublishEnv a) { result :: Boolean, remotes :: Array Git.Remote } locationIsInGitRemotes location = do isGitRepo <- FS.exists ".git" if not isGitRepo then - pure false + pure { result: false, remotes: [] } else Git.getRemotes Nothing >>= case _ of Left err -> die [ toDoc "Couldn't parse Git remotes: ", err ] - Right remotes -> - pure $ remotes # Array.any \r -> case location of - Location.Git { url } -> r.url == url - Location.GitHub { owner, repo } -> r.owner == owner && r.repo == repo + Right remotes -> do + let + result = remotes # Array.any \r -> case location of + Location.Git { url } -> r.url == url + Location.GitHub { owner, repo } -> r.owner == owner && r.repo == repo + pure { result, remotes } inferLocationAndWriteToConfig :: ∀ a. WorkspacePackage -> Spago (PublishEnv a) Boolean inferLocationAndWriteToConfig selectedPackage = do @@ -527,5 +444,7 @@ inferLocationAndWriteToConfig selectedPackage = do liftAff $ FS.writeYamlDocFile configPath selectedPackage.doc pure true -baseApi :: String -baseApi = "https://registry.purescript.org" +prettyPrintLocation :: Location -> String +prettyPrintLocation = case _ of + Location.Git { url } -> url + Location.GitHub { owner, repo } -> "GitHub: " <> owner <> "/" <> repo diff --git a/src/Spago/Command/Registry.js b/src/Spago/Command/Registry.js new file mode 100644 index 000000000..6a400e282 --- /dev/null +++ b/src/Spago/Command/Registry.js @@ -0,0 +1,7 @@ +import readlineSync from 'readline-sync'; + +export function questionPassword(query) { + return readlineSync.question(query, { + hideEchoBack: true // The typed text on screen is hidden by `*` + }); +} diff --git a/src/Spago/Command/Registry.purs b/src/Spago/Command/Registry.purs index cd58ccd50..1427a536a 100644 --- a/src/Spago/Command/Registry.purs +++ b/src/Spago/Command/Registry.purs @@ -3,6 +3,7 @@ module Spago.Command.Registry where import Spago.Prelude import Data.Array as Array +import Data.Array.NonEmpty as NEA import Data.Codec.JSON as CJ import Data.Codec.JSON.Record as CJ.Record import Data.DateTime (DateTime(..)) @@ -10,13 +11,21 @@ import Data.Formatter.DateTime as DateTime import Data.Map as Map import Data.String (Pattern(..)) import Data.String as String +import Effect.Uncurried (EffectFn1, runEffectFn1) import Registry.Internal.Codec as Internal import Registry.Internal.Codec as Internal.Codec import Registry.Internal.Format as Internal.Format import Registry.Metadata as Metadata +import Registry.Operation as Operation import Registry.PackageName as PackageName +import Registry.SSH as SSH import Registry.Version as Version +import Spago.Command.Fetch (FetchEnv) +import Spago.Config as Config import Spago.Db as Db +import Spago.FS as FS +import Spago.Git as Git +import Spago.Json as Json import Spago.Registry (RegistryEnv) import Spago.Registry as Registry @@ -43,7 +52,7 @@ search { package: searchString, json } = do pure Nothing Right packageName -> Registry.getMetadata packageName >>= case _ of Left err -> do - logWarn $ "Couldn't read metadata for pacakge " <> PackageName.print packageName <> ", error: " <> err + logWarn $ "Couldn't read metadata for package " <> PackageName.print packageName <> ", error: " <> err pure Nothing Right (Metadata meta) -> pure $ Just $ case Map.findMax meta.published of Nothing -> Tuple packageName { version: Nothing, publishedTime: Nothing } @@ -127,3 +136,141 @@ packageSets { latest, json } = do , Version.print compiler ] } + +type RegistryTransferArgs = { privateKeyPath :: String } + +transfer :: RegistryTransferArgs -> Spago (FetchEnv _) Unit +transfer { privateKeyPath } = do + logDebug $ "Running package transfer" + { workspace, offline } <- ask + + selected <- case workspace.selected of + Just s -> pure s + Nothing -> + let + workspacePackages = Config.getWorkspacePackages workspace.packageSet + in + -- If there's only one package, select that one + case NEA.length workspacePackages of + 1 -> pure $ NEA.head workspacePackages + _ -> do + logDebug $ unsafeStringify workspacePackages + die + [ toDoc "No package was selected for running. Please select (with -p) one of the following packages:" + , indent (toDoc $ map _.package.name workspacePackages) + ] + + newLocation <- case selected.package.publish >>= _.location of + Just loc -> pure loc + Nothing -> die + -- TODO: once we have automatic detection for git remotes we should try that first. + [ "The package does not have a location set in the config file: add a valid one in `package.publish`." + , "See the configuration file's documentation: https://github.com/purescript/spago#the-configuration-file" + ] + + _owners <- case selected.package.publish >>= _.owners of + Just owners | Array.length owners > 0 -> pure owners + _ -> die + [ "The package does not have any owners set in the config file." + , "Please run `spago auth` to add your SSH public key to the owners list in the spago.yaml file." + ] + + -- Check that the git tree is clean - since the transfer will obey the new content + -- of the config file, it makes sense to have it commited before transferring + Git.getStatus Nothing >>= case _ of + Left _err -> do + die $ toDoc + [ toDoc "Could not verify whether the git tree is clean. Error was:" + , indent _err + ] + Right statusResult | statusResult /= "" -> + die $ toDoc + [ toDoc "The git tree is not clean. Please commit or stash these files:" + , indent $ toDoc (String.split (String.Pattern "\n") statusResult) + ] + _ -> pure unit + + -- Has the package ever been published before? We pull the metadata to verify that. + -- Note! This getMetadata is going through two layers of caching: + -- 1. the registry is only fetched every 15 mins + -- 2. the metadata is then cached in the db for 15 mins + -- When we transfer a package we want to make sure we have the latest everything, + -- so we bypass both caches here. + local (_ { offline = OnlineBypassCache }) (Registry.getMetadata selected.package.name) >>= case _ of + Left err -> do + logDebug err + die + [ "Could not find package '" <> PackageName.print selected.package.name <> "' in the Registry Index. Has it ever been published?" + , "If not, please run `spago publish` first. Otherwise this is a bug - please report it on the Spago repo." + ] + Right (Metadata { location }) -> do + -- We have a package, now need to check that the new location is different from the current one + when (newLocation == location) do + die + [ "Cannot transfer package: the new location is the same as the current one." + , "Please edit the `publish.location` field of your `spago.yaml` with the new location." + ] + + -- We construct the payload that we'll later sign + let dataToSign = { name: selected.package.name, newLocation } + let rawPayload = Json.stringifyJson Operation.transferCodec dataToSign + + key <- getPrivateKeyForSigning privateKeyPath + -- We have a key! We can sign the payload with it, and submit the whole package to the Registry + let signature = SSH.sign key rawPayload + + -- At this point we check if the offline flag has been set. If it has, we abort the operation. + -- Crucially, this is done _after_ the signing, which allows us to test that too. + case offline of + Offline -> die [ "Cannot perform Registry operations while offline." ] + _ -> Registry.submitRegistryOperation $ Operation.Authenticated + { signature + , rawPayload + , payload: Operation.Transfer dataToSign + } + +getPrivateKeyForSigning :: forall e. FilePath -> Spago (LogEnv e) SSH.PrivateKey +getPrivateKeyForSigning privateKeyPath = do + -- If all is well we read in the private key + privateKey <- try (FS.readTextFile privateKeyPath) >>= case _ of + Right key -> pure key + Left err -> do + logDebug $ show err + die "Could not read the private key at the given path. Please check it and try again." + + let + decodeKeyInteractive { requiresPassword, attemptsLeft } = do + case requiresPassword of + -- If there are no attempts yet we first try to decode the key without a passphrase, silently. + -- In case we succeed then happy days, can just proceed. If not, we move to asking the user for + -- the key. + false -> do + case SSH.parsePrivateKey { key: privateKey, passphrase: Nothing } of + Right key -> pure key + Left _ -> do + decodeKeyInteractive { requiresPassword: true, attemptsLeft } + true -> do + let prompt = "Enter passphrase for " <> privateKeyPath <> ": " + passphrase <- liftEffect $ runEffectFn1 questionPassword prompt + + case SSH.parsePrivateKey { key: privateKey, passphrase: Just passphrase } of + Left SSH.RequiresPassphrase -> case attemptsLeft of + 0 -> die [ "Too many incorrect attempts, exiting." ] + _ -> do + logError "The passphrase you entered is incorrect. Please trygain." + decodeKeyInteractive { requiresPassword: true, attemptsLeft: attemptsLeft - 1 } + Left err -> die [ toDoc "Could not parse the private key:", indent $ toDoc $ SSH.printPrivateKeyParseError err ] + Right key -> pure key + + decodeKeyInteractive { requiresPassword: false, attemptsLeft: 3 } + +type RegistryUnpublishArgs = { version :: Version, reason :: Maybe String } + +unpublish :: RegistryUnpublishArgs -> Spago (RegistryEnv _) Unit +unpublish _a = do -- { version, reason } = do + logError "Unpublishing packages is not supported yet." + die [ "Please contact the maintainers if you need to unpublish a package." ] + +-- We have custom FFI here because we want to ask for the passphrase in the terminal, +-- and the stock ReadLine implementation is not good at passwords +foreign import questionPassword :: EffectFn1 String String diff --git a/src/Spago/Config.js b/src/Spago/Config.js index 5bb29bae4..722316ed6 100644 --- a/src/Spago/Config.js +++ b/src/Spago/Config.js @@ -96,6 +96,15 @@ export function addRangesToConfigImpl(doc, rangesMap) { deps.items = newItems; } +// Note: this function assumes a few things: +// - the `publish` section exists +// - the new element does not already exist in the list (it just appends it) +export function addOwnerImpl(doc, owner) { + const publish = doc.get("package").get("publish"); + let owners = getOrElse(publish, "owners", doc.createNode([])); + owners.items.push(doc.createNode(owner)); +} + export function setPackageSetVersionInConfigImpl(doc, version) { doc.setIn(["workspace", "packageSet", "registry"], version); } diff --git a/src/Spago/Config.purs b/src/Spago/Config.purs index 2918add2e..85f1ea83c 100644 --- a/src/Spago/Config.purs +++ b/src/Spago/Config.purs @@ -7,6 +7,7 @@ module Spago.Config , Workspace , WorkspaceBuildOptions , WorkspacePackage + , addOwner , addPackagesToConfig , addPublishLocationToConfig , addRangesToConfig @@ -56,6 +57,7 @@ import Foreign.Object as Foreign import JSON (JSON) import Node.Path as Path import Registry.Internal.Codec as Internal.Codec +import Registry.Owner (Owner(..)) import Registry.PackageName as PackageName import Registry.PackageSet as Registry.PackageSet import Registry.Range as Range @@ -362,7 +364,7 @@ readWorkspace { maybeSelectedPackage, pureBuild, migrateConfig } = do Left reason, Just address@(Core.SetFromUrl { url: rawUrl }) -> do result <- case offline of Offline -> die "You are offline, but the package set is not cached locally. Please connect to the internet and try again." - Online -> do + _ -> do logDebug reason logDebug $ "Reading the package set from URL: " <> rawUrl url <- case parseUrl rawUrl of @@ -663,9 +665,17 @@ addPublishLocationToConfig :: YamlDoc Core.Config -> Location -> Effect Unit addPublishLocationToConfig doc loc = runEffectFn2 addPublishLocationToConfigImpl doc (CJ.encode publishLocationCodec loc) +type OwnerJS = { public :: String, keytype :: String, id :: Nullable String } + +addOwner :: forall m. MonadAff m => FilePath -> YamlDoc Core.Config -> Owner -> m Unit +addOwner configPath doc (Owner { id, keytype, public }) = do + liftEffect $ runEffectFn2 addOwnerImpl doc { keytype, public, id: Nullable.toNullable id } + liftAff $ FS.writeYamlDocFile configPath doc + foreign import setPackageSetVersionInConfigImpl :: EffectFn2 (YamlDoc Core.Config) String Unit foreign import addPackagesToConfigImpl :: EffectFn3 (YamlDoc Core.Config) Boolean (Array String) Unit foreign import removePackagesFromConfigImpl :: EffectFn3 (YamlDoc Core.Config) Boolean (PackageName -> Boolean) Unit foreign import addRangesToConfigImpl :: EffectFn2 (YamlDoc Core.Config) (Foreign.Object String) Unit -foreign import migrateV1ConfigImpl :: ∀ a. YamlDoc a -> Nullable (YamlDoc Core.Config) foreign import addPublishLocationToConfigImpl :: EffectFn2 (YamlDoc Core.Config) JSON Unit +foreign import addOwnerImpl :: EffectFn2 (YamlDoc Core.Config) OwnerJS Unit +foreign import migrateV1ConfigImpl :: forall a. YamlDoc a -> Nullable (YamlDoc Core.Config) diff --git a/src/Spago/Git.purs b/src/Spago/Git.purs index 713929328..b40283e88 100644 --- a/src/Spago/Git.purs +++ b/src/Spago/Git.purs @@ -1,6 +1,7 @@ module Spago.Git ( Git , GitEnv + , Remote , fetchRepo , getGit , getRef @@ -9,7 +10,6 @@ module Spago.Git , checkout , fetch , getRefType - , isIgnored , listTags , parseRemote , pushTag @@ -26,9 +26,6 @@ import Data.Maybe (fromJust) import Data.String (Pattern(..)) import Data.String as String import Data.String.Regex as Regex -import Node.ChildProcess.Types (Exit(..)) -import Node.Path as Path -import Node.Process as Process import Partial.Unsafe (unsafePartial) import Registry.Version as Version import Spago.Cmd as Cmd @@ -60,7 +57,7 @@ fetchRepo { git, ref } path = do logDebug $ "Found " <> git <> " locally, skipping fetch because we are offline" pure $ Right unit Offline, false -> die [ "You are offline and the repo '" <> git <> "' is not available locally, can't make progress." ] - Online, _ -> do + _, _ -> do cloneOrFetchResult <- case repoExists of true -> do logDebug $ "Found " <> git <> " locally, pulling..." @@ -73,7 +70,7 @@ fetchRepo { git, ref } path = do result <- Except.runExceptT do Except.ExceptT $ pure cloneOrFetchResult logDebug $ "Checking out the requested ref for " <> git <> " : " <> ref - _ <- runGit [ "checkout", ref ] (Just path) + runGit_ [ "checkout", ref ] (Just path) -- if we are on a branch and not on a detached head, then we need to pull -- the following command will fail if on a detached head, and succeed if on a branch Except.mapExceptT @@ -121,7 +118,8 @@ getStatus cwd = do { git } <- ask Cmd.exec git.cmd [ "status", "--porcelain" ] opts >>= case _ of Left r -> do - pure $ Left $ toDoc [ "Could not run `git status`. Error:", r.message ] + logDebug "Command `git status --porcelain` failed" + pure $ Left $ toDoc r.stderr Right r -> pure $ Right r.stdout getRef :: forall a. Maybe FilePath -> Spago (GitEnv a) (Either Docc String) @@ -167,7 +165,7 @@ pushTag cwd version = do Offline -> do logWarn $ "Spago is in offline mode - not pushing the git tag v" <> Version.print version pure $ Right unit - Online -> do + _ -> do logInfo $ "Pushing tag 'v" <> Version.print version <> "' to the remote" Cmd.exec git.cmd [ "push", "origin", "v" <> Version.print version ] opts >>= case _ of Left r -> pure $ Left $ toDoc @@ -177,38 +175,6 @@ pushTag cwd version = do ] Right _ -> pure $ Right unit --- | Check if the path is ignored by git --- --- `git check-ignore` exits with 1 when path is not ignored, and 128 when --- a fatal error occurs (i.e. when not in a git repository). -isIgnored :: forall a. FilePath -> Spago (GitEnv a) Boolean -isIgnored path = do - { git } <- ask - result <- Cmd.exec git.cmd [ "check-ignore", "--quiet", path ] (Cmd.defaultExecOptions { pipeStdout = false, pipeStderr = false }) - case result of - -- Git is successful if it's an ignored file - Right _ -> pure true - -- Git will fail with exitCode 128 if this is not a git repo or if it's dealing with a link. - -- We ignore links - I mean, do we really want to deal with recursive links?!? - Left r - | Normally 128 <- r.exit -> do - -- Sigh. Even if something is behind a link Node will not tell us that, - -- so we need to check all the paths between the cwd and the provided path - -- Just beautiful - paths <- liftEffect do - cwd <- Process.cwd - absolutePath <- Path.resolve [] path - FS.getInBetweenPaths cwd absolutePath - Array.any identity <$> traverse FS.isLink paths - -- Git will fail with 1 when a file is just, like, normally ignored - | Normally 1 <- r.exit -> - pure false - | otherwise -> do - logDebug "IsIgnored encountered an interesting exitCode" - logDebug $ Cmd.printExecResult r - -- We still do not ignore it, just in case - pure false - getGit :: forall a. Spago (LogEnv a) Git getGit = do Cmd.exec "git" [ "--version" ] Cmd.defaultExecOptions { pipeStdout = false, pipeStderr = false } >>= case _ of diff --git a/src/Spago/Prelude.purs b/src/Spago/Prelude.purs index cda29c1c3..78e601f33 100644 --- a/src/Spago/Prelude.purs +++ b/src/Spago/Prelude.purs @@ -45,7 +45,9 @@ import Registry.Version as Version import Spago.Paths as Paths import Unsafe.Coerce (unsafeCoerce) -data OnlineStatus = Offline | Online +data OnlineStatus = Offline | Online | OnlineBypassCache + +derive instance Eq OnlineStatus unsafeFromRight :: forall e a. Either e a -> a unsafeFromRight v = Either.fromRight' (\_ -> unsafeCrashWith $ "Unexpected Left: " <> unsafeStringify v) v diff --git a/src/Spago/Registry.purs b/src/Spago/Registry.purs index 2c2341c39..437ae2888 100644 --- a/src/Spago/Registry.purs +++ b/src/Spago/Registry.purs @@ -12,33 +12,49 @@ module Spago.Registry , listMetadataFiles , listPackageSets , readPackageSet + , submitRegistryOperation ) where import Spago.Prelude import Data.Array as Array import Data.Array.NonEmpty as NonEmptyArray -import Data.DateTime as DateTime +import Data.Codec.JSON as CJ +import Data.DateTime (DateTime) +import Data.DateTime (diff) as DateTime +import Data.Formatter.DateTime (format) as DateTime import Data.Map as Map import Data.Set as Set import Data.String (Pattern(..)) import Data.String as String -import Data.Time.Duration (Minutes(..)) +import Data.Time.Duration (Milliseconds(..), Minutes(..)) +import Data.Traversable (sequence) import Effect.AVar (AVar) +import Effect.Aff as Aff import Effect.Aff.AVar as AVar +import Effect.Exception as Exception import Effect.Now as Now +import Fetch as Http import Node.Path as Path +import Node.Process as Process +import Registry.API.V1 as V1 import Registry.Constants as Registry.Constants +import Registry.Internal.Format as Internal.Format import Registry.ManifestIndex as ManifestIndex import Registry.Metadata as Metadata +import Registry.Operation as Operation import Registry.PackageName as PackageName import Registry.PackageSet (PackageSet(..)) import Registry.PackageSet as PackageSet import Registry.Version as Version +import Routing.Duplex as Duplex import Spago.Db (Db) import Spago.Db as Db import Spago.FS as FS +import Spago.Git (GitEnv) import Spago.Git as Git +import Spago.Json as Json +import Spago.Log (LogVerbosity(..)) import Spago.Paths as Paths import Spago.Purs as Purs @@ -119,36 +135,46 @@ getRegistryFns registryBox registryLock = do -- The Lock AVar is used to make sure -- that only one fiber is fetching the Registry at a time, and that all the other -- fibers will wait for it to finish and then use the cached value. - { db } <- ask + { offline } <- ask liftAff $ AVar.take registryLock - liftAff (AVar.tryRead registryBox) >>= case _ of - Just registry -> do - liftAff $ AVar.put unit registryLock - pure registry - Nothing -> do - _fetchingFreshRegistry <- fetchRegistry - let - registryFns = - { getManifestFromIndex: getManifestFromIndexImpl db - , getMetadata: getMetadataImpl db - , getMetadataForPackages: getMetadataForPackagesImpl db - , listMetadataFiles: FS.ls (Path.concat [ Paths.registryPath, Registry.Constants.metadataDirectory ]) - , listPackageSets: listPackageSetsImpl - , findPackageSet: findPackageSetImpl - , readPackageSet: readPackageSetImpl - } - liftAff $ AVar.put registryFns registryBox - liftAff $ AVar.put unit registryLock - pure registryFns - + fns <- liftAff (AVar.tryRead registryBox) >>= case _ of + -- If we are asked to bypass the cache then we need to rebuild again + Just _registry | offline == OnlineBypassCache -> do + -- We know the box is full so first thing we do is to empty it + void $ liftAff $ AVar.take registryBox + buildRegistryFns + -- If we are in other states we can just return the cached value + Just registry -> pure registry + -- No cached value, so build it + Nothing -> buildRegistryFns + liftAff $ AVar.put unit registryLock + pure fns where + buildRegistryFns :: Spago (PreRegistryEnv _) RegistryFunctions + buildRegistryFns = do + { db, offline } <- ask + _fetchingFreshRegistry <- fetchRegistry + let + registryFns = + { getManifestFromIndex: getManifestFromIndexImpl db + , getMetadata: getMetadataImpl db offline + , getMetadataForPackages: getMetadataForPackagesImpl db offline + , listMetadataFiles: FS.ls (Path.concat [ Paths.registryPath, Registry.Constants.metadataDirectory ]) + , listPackageSets: listPackageSetsImpl + , findPackageSet: findPackageSetImpl + , readPackageSet: readPackageSetImpl + } + liftAff $ AVar.put registryFns registryBox + pure registryFns + fetchRegistry :: Spago (PreRegistryEnv _) Boolean fetchRegistry = do -- we keep track of how old the latest pull was - if the last pull was recent enough -- we just move on, otherwise run the fibers - { db } <- ask + { db, offline } <- ask fetchingFreshRegistry <- shouldFetchRegistryRepos db - when fetchingFreshRegistry do + -- we also check if we need to bypass this cache (for when we need the freshest data) + when (fetchingFreshRegistry || offline == OnlineBypassCache) do -- clone the registry and index repo, or update them logInfo "Refreshing the Registry Index..." parallelise @@ -208,9 +234,9 @@ getRegistryFns registryBox registryLock = do -- Metadata can change over time (unpublished packages, and new packages), so we need -- to read it from file every time we have a fresh Registry -getMetadataImpl :: Db -> PackageName -> Spago (LogEnv ()) (Either String Metadata) -getMetadataImpl db name = - getMetadataForPackagesImpl db [ name ] +getMetadataImpl :: Db -> OnlineStatus -> PackageName -> Spago (LogEnv ()) (Either String Metadata) +getMetadataImpl db onlineStatus name = + getMetadataForPackagesImpl db onlineStatus [ name ] <#> case _ of Left err -> Left err Right metadataMap -> case Map.lookup name metadataMap of @@ -218,33 +244,34 @@ getMetadataImpl db name = Just metadata -> Right metadata -- Parallelised version of `getMetadataImpl` -getMetadataForPackagesImpl :: Db -> Array PackageName -> Spago (LogEnv ()) (Either String (Map PackageName Metadata)) -getMetadataForPackagesImpl db names = do - -- we first try reading it from the DB - liftEffect (Db.getMetadataForPackages db names) >>= \metadatas -> do - { fail, success } <- partitionEithers <$> parTraverseSpago - ( \name -> do - case Map.lookup name metadatas of - Nothing -> - -- if we don't have it we try reading it from file - metadataFromFile name >>= case _ of - Left e -> pure (Left e) - Right m -> do - -- and memoize it - liftEffect (Db.insertMetadata db name m) - pure (Right $ name /\ m) - Just m -> pure $ Right $ name /\ m - ) - names - case Array.head fail of - Nothing -> pure $ Right $ Map.fromFoldable success - Just f -> pure $ Left $ f - +getMetadataForPackagesImpl :: Db -> OnlineStatus -> Array PackageName -> Spago (LogEnv ()) (Either String (Map PackageName Metadata)) +getMetadataForPackagesImpl db onlineStatus names = do + (map Map.fromFoldable <<< sequence) <$> case onlineStatus == OnlineBypassCache of + true -> do + logDebug "Bypassing cache, reading metadata from file" + parTraverseSpago metadataFromFile names + false -> do + -- the first layer of caching is in the DB, so we try that first + metadatas <- liftEffect $ Db.getMetadataForPackages db names + parTraverseSpago + ( \name -> do + case Map.lookup name metadatas of + -- if we don't have it in cache we try reading it from file + Nothing -> metadataFromFile name + Just m -> pure $ Right $ name /\ m + ) + names where + metadataFromFile :: PackageName -> Spago (LogEnv ()) (Either String (Tuple PackageName Metadata)) metadataFromFile pkgName = do let metadataFilePath = Path.concat [ Paths.registryPath, Registry.Constants.metadataDirectory, PackageName.print pkgName <> ".json" ] logDebug $ "Reading metadata from file: " <> metadataFilePath - liftAff (FS.readJsonFile Metadata.codec metadataFilePath) + liftAff (FS.readJsonFile Metadata.codec metadataFilePath) >>= case _ of + Left e -> pure $ Left e + Right m -> do + -- memoize it if found + liftEffect (Db.insertMetadata db pkgName m) + pure $ Right (pkgName /\ m) -- Manifests are immutable so we can just lookup in the DB or read from file if not there getManifestFromIndexImpl :: Db -> PackageName -> Version -> Spago (LogEnv ()) (Maybe Manifest) @@ -347,3 +374,118 @@ shouldFetchRegistryRepos db = do pure true else do pure false + +-------------------------------------------------------------------------------- +-- | Registry operations +-------------------------------------------------------------------------------- + +data JobType = Publish | Transfer | Unpublish + +printJobType :: JobType -> String +printJobType = case _ of + Publish -> "Publish" + Transfer -> "Transfer" + Unpublish -> "Unpublish" + +submitRegistryOperation :: Operation.PackageOperation -> Spago (GitEnv _) Unit +submitRegistryOperation payload = do + { jobId, jobType } <- case payload of + Operation.Publish publishData -> do + { jobId } <- callRegistry (baseApi <> Duplex.print V1.routes V1.Publish) V1.jobCreatedResponseCodec (Just { codec: Operation.publishCodec, data: publishData }) + pure { jobId, jobType: Publish } + Operation.Authenticated authedData@{ payload: Operation.Transfer _ } -> do + { jobId } <- callRegistry (baseApi <> Duplex.print V1.routes V1.Transfer) V1.jobCreatedResponseCodec (Just { codec: Operation.authenticatedCodec, data: authedData }) + pure { jobId, jobType: Transfer } + Operation.Authenticated authedData@{ payload: Operation.Unpublish _ } -> do + { jobId } <- callRegistry (baseApi <> Duplex.print V1.routes V1.Unpublish) V1.jobCreatedResponseCodec (Just { codec: Operation.authenticatedCodec, data: authedData }) + pure { jobId, jobType: Unpublish } + logSuccess $ "Registry accepted the " <> printJobType jobType <> " request and is processing..." + logDebug $ "Job ID: " <> unwrap jobId + logInfo "Logs from the Registry pipeline:" + waitForJobFinish { jobId, jobType } + +callRegistry :: forall env a b. String -> CJ.Codec b -> Maybe { codec :: CJ.Codec a, data :: a } -> Spago (GitEnv env) b +callRegistry url outputCodec maybeInput = handleError do + logDebug $ "Calling registry at " <> url + response <- liftAff $ withBackoff' $ try case maybeInput of + Just { codec: inputCodec, data: input } -> Http.fetch url + { method: Http.POST + , headers: { "Content-Type": "application/json" } + , body: Json.stringifyJson inputCodec input + } + Nothing -> Http.fetch url { method: Http.GET } + case response of + Nothing -> pure $ Left $ "Could not reach the registry at " <> url + Just (Left err) -> pure $ Left $ "Error while calling the registry:\n " <> Exception.message err + Just (Right { status, text }) | status /= 200 -> do + bodyText <- liftAff text + pure $ Left $ "Registry did not like this and answered with status " <> show status <> ", got answer:\n " <> bodyText + Just (Right { json }) -> do + jsonBody <- Json.unsafeFromForeign <$> liftAff json + pure $ case CJ.decode outputCodec jsonBody of + Right output -> Right output + Left err -> Left $ "Could not parse response from the registry, error: " <> show err + where + -- TODO: see if we want to just kill the process generically here, or give out customized errors + handleError a = do + { offline } <- ask + case offline of + Offline -> die "Spago is offline - not able to call the Registry." + _ -> a >>= case _ of + Left err -> die err + Right res -> pure res + +waitForJobFinish :: forall env. { jobId :: V1.JobId, jobType :: JobType } -> Spago (GitEnv env) Unit +waitForJobFinish { jobId, jobType } = go Nothing + where + go :: Maybe DateTime -> Spago (GitEnv env) Unit + go lastTimestamp = do + { logOptions } <- ask + let + url = baseApi <> Duplex.print V1.routes + ( V1.Job jobId + { since: lastTimestamp + , level: case logOptions.verbosity of + LogVerbose -> Just V1.Debug + _ -> Just V1.Info + } + ) + jobInfo :: V1.Job <- callRegistry url V1.jobCodec Nothing + -- first of all, print all the logs we get + for_ jobInfo.logs \log -> do + let line = indent $ toDoc $ DateTime.format Internal.Format.iso8601DateTime log.timestamp <> " " <> log.message + case log.level of + V1.Debug -> logDebug line + V1.Info -> logInfo line + V1.Warn -> logWarn line + V1.Error -> logError line + case jobInfo.finishedAt of + Nothing -> do + -- If the job is not finished, we grab the timestamp of the last log line, wait a bit and retry + let + latestTimestamp = jobInfo.logs # Array.last # case _ of + Just log -> Just log.timestamp + Nothing -> lastTimestamp + liftAff $ Aff.delay $ Milliseconds 500.0 + go latestTimestamp + Just _finishedAt -> do + -- if it's done we report the failure. + logDebug $ "Job: " <> printJson V1.jobCodec jobInfo + case jobInfo.success of + false -> die $ toDoc + [ "Registry finished processing the package, but it failed." + , "If this was due to the package not meeting the requirements, you can find more info in the logs above, and try again." + , "If you think this was a mistake, please report it to the core team: https://github.com/purescript/registry-dev/issues" + ] + true -> do + let + verb = case jobType of + Publish -> "published" + Transfer -> "transferred" + Unpublish -> "unpublished" + logSuccess $ "Registry finished processing the package. Your package was " <> verb <> " successfully!" + -- TODO: I am not sure why, but this is needed to make the process exit, otherwise Spago will hang + liftEffect $ Process.exit + +baseApi :: String +baseApi = "https://registry.purescript.org" diff --git a/test-fixtures/1146-cli-help/build.txt b/test-fixtures/1146-cli-help/build.txt index a3fd1b58d..364376e36 100644 --- a/test-fixtures/1146-cli-help/build.txt +++ b/test-fixtures/1146-cli-help/build.txt @@ -1,11 +1,6 @@ Invalid option `--bogus' -Usage: index.dev.js build [--migrate] [--monochrome|--no-color] [--offline] - [-q|--quiet] [-v|--verbose] [--backend-args ARGS] - [--ensure-ranges] [--json-errors] [--output DIR] - [--pedantic-packages] [--pure] [--purs-args ARGS] - [-p|--package PACKAGE] ([--verbose-stats] | - [--censor-stats]) [--strict] +Usage: index.dev.js build [--migrate] [--monochrome|--no-color] [--offline] [-q|--quiet] [-v|--verbose] [--backend-args ARGS] [--ensure-ranges] [--json-errors] [--output DIR] [--pedantic-packages] [--pure] [--purs-args ARGS] [-p|--package PACKAGE] ([--verbose-stats] | [--censor-stats]) [--strict] Compile the project Available options: diff --git a/test-fixtures/1146-cli-help/registry-search.txt b/test-fixtures/1146-cli-help/registry-search.txt index cc3de2866..eff4b951b 100644 --- a/test-fixtures/1146-cli-help/registry-search.txt +++ b/test-fixtures/1146-cli-help/registry-search.txt @@ -1,8 +1,6 @@ Invalid option `--bogus' -Usage: index.dev.js registry search [--migrate] [--monochrome|--no-color] - [--offline] [-q|--quiet] [-v|--verbose] - [--json] PACKAGE +Usage: index.dev.js registry search [--migrate] [--monochrome|--no-color] [--offline] [-q|--quiet] [-v|--verbose] [--json] PACKAGE Search for package names in the Registry Available options: @@ -14,6 +12,5 @@ Available options: -q,--quiet Suppress all spago logging -v,--verbose Enable additional debug logging, e.g. printing `purs` commands - --json Format the output as JSON - PACKAGE Package name + --json Format the output as JSON PACKAGE Package name -h,--help Show this help text diff --git a/test-fixtures/1146-cli-help/root-error-command.txt b/test-fixtures/1146-cli-help/root-error-command.txt index 7da57aedc..bbbccecbe 100644 --- a/test-fixtures/1146-cli-help/root-error-command.txt +++ b/test-fixtures/1146-cli-help/root-error-command.txt @@ -8,21 +8,23 @@ Available options: -v,--version Show the current version Available commands: - init Initialise a new project + auth Authenticate as the owner of a package, to allow + transfer and unpublish operations + build Compile the project + bundle Bundle the project in a single file + docs Generate docs for the project and its dependencies fetch Downloads all of the project's dependencies + graph Generate a graph of modules or dependencies + init Initialise a new project install Compile the project's dependencies - uninstall Remove dependencies from a package - build Compile the project + ls List packages or dependencies + publish Publish a package + registry Commands to interact with the Registry + repl Start a REPL run Run the project - test Test the project - bundle Bundle the project in a single file sources List all the source paths (globs) for the dependencies of the project - repl Start a REPL - publish Publish a package + test Test the project + uninstall Remove dependencies from a package upgrade Upgrade to the latest package set, or to the latest versions of Registry packages - docs Generate docs for the project and its dependencies - registry Commands to interact with the Registry - ls List packages or dependencies - graph Generate a graph of modules or dependencies diff --git a/test-fixtures/1146-cli-help/root-error-option.txt b/test-fixtures/1146-cli-help/root-error-option.txt index 925cc803a..ea8921c74 100644 --- a/test-fixtures/1146-cli-help/root-error-option.txt +++ b/test-fixtures/1146-cli-help/root-error-option.txt @@ -8,21 +8,23 @@ Available options: -v,--version Show the current version Available commands: - init Initialise a new project + auth Authenticate as the owner of a package, to allow + transfer and unpublish operations + build Compile the project + bundle Bundle the project in a single file + docs Generate docs for the project and its dependencies fetch Downloads all of the project's dependencies + graph Generate a graph of modules or dependencies + init Initialise a new project install Compile the project's dependencies - uninstall Remove dependencies from a package - build Compile the project + ls List packages or dependencies + publish Publish a package + registry Commands to interact with the Registry + repl Start a REPL run Run the project - test Test the project - bundle Bundle the project in a single file sources List all the source paths (globs) for the dependencies of the project - repl Start a REPL - publish Publish a package + test Test the project + uninstall Remove dependencies from a package upgrade Upgrade to the latest package set, or to the latest versions of Registry packages - docs Generate docs for the project and its dependencies - registry Commands to interact with the Registry - ls List packages or dependencies - graph Generate a graph of modules or dependencies diff --git a/test-fixtures/1146-cli-help/root-help.txt b/test-fixtures/1146-cli-help/root-help.txt index e4af8f08b..07934149d 100644 --- a/test-fixtures/1146-cli-help/root-help.txt +++ b/test-fixtures/1146-cli-help/root-help.txt @@ -6,21 +6,23 @@ Available options: -v,--version Show the current version Available commands: - init Initialise a new project + auth Authenticate as the owner of a package, to allow + transfer and unpublish operations + build Compile the project + bundle Bundle the project in a single file + docs Generate docs for the project and its dependencies fetch Downloads all of the project's dependencies + graph Generate a graph of modules or dependencies + init Initialise a new project install Compile the project's dependencies - uninstall Remove dependencies from a package - build Compile the project + ls List packages or dependencies + publish Publish a package + registry Commands to interact with the Registry + repl Start a REPL run Run the project - test Test the project - bundle Bundle the project in a single file sources List all the source paths (globs) for the dependencies of the project - repl Start a REPL - publish Publish a package + test Test the project + uninstall Remove dependencies from a package upgrade Upgrade to the latest package set, or to the latest versions of Registry packages - docs Generate docs for the project and its dependencies - registry Commands to interact with the Registry - ls List packages or dependencies - graph Generate a graph of modules or dependencies diff --git a/test-fixtures/publish-no-git.txt b/test-fixtures/publish-no-git.txt deleted file mode 100644 index 1f53c3a98..000000000 --- a/test-fixtures/publish-no-git.txt +++ /dev/null @@ -1,17 +0,0 @@ -Reading Spago workspace configuration... - -✓ Selecting package to build: aaa - -Downloading dependencies... -Building... - Src Lib All -Warnings 0 0 0 -Errors 0 0 0 - -✓ Build succeeded. - - -✘ Could not verify whether the git tree is clean due to the below error: - Could not run `git status`. Error: - Command failed with exit code 128: git status --porcelain -fatal: not a git repository (or any of the parent directories): .git diff --git a/test-fixtures/publish.purs b/test-fixtures/publish/basic.purs similarity index 100% rename from test-fixtures/publish.purs rename to test-fixtures/publish/basic.purs diff --git a/test-fixtures/spago-publish.yaml b/test-fixtures/publish/basic.yaml similarity index 100% rename from test-fixtures/spago-publish.yaml rename to test-fixtures/publish/basic.yaml diff --git a/test-fixtures/publish-extra-package-core-dependency.txt b/test-fixtures/publish/extra-package-core-dependency.txt similarity index 100% rename from test-fixtures/publish-extra-package-core-dependency.txt rename to test-fixtures/publish/extra-package-core-dependency.txt diff --git a/test-fixtures/publish-invalid-location.txt b/test-fixtures/publish/invalid-location.txt similarity index 61% rename from test-fixtures/publish-invalid-location.txt rename to test-fixtures/publish/invalid-location.txt index 2c7496e2c..caa97363f 100644 --- a/test-fixtures/publish-invalid-location.txt +++ b/test-fixtures/publish/invalid-location.txt @@ -13,6 +13,8 @@ Errors 0 0 0 Your package "aaa" is not ready for publishing yet, encountered 1 error: -✘ The location specified in the manifest file -({"githubOwner":"purescript","githubRepo":"aaa"}) - is not one of the remotes in the git repository. +✘ The location specified in the manifest file is not one of the remotes in the git repository. +Location: + - GitHub: purescript/aaa +Remotes: + - origin: git@github.com:purescript/bbb.git diff --git a/test-fixtures/publish/key b/test-fixtures/publish/key new file mode 100644 index 000000000..f43566471 --- /dev/null +++ b/test-fixtures/publish/key @@ -0,0 +1,7 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACBDuPFQv1kpVUknTnwhXxHz/E/jNhAJ+hF/07FzkpIhBAAAAIh02MEGdNjB +BgAAAAtzc2gtZWQyNTUxOQAAACBDuPFQv1kpVUknTnwhXxHz/E/jNhAJ+hF/07FzkpIhBA +AAAEA7rtNjP81cn5QrsZmcUkh4gVVNY9zMDvrJjT559r5rEEO48VC/WSlVSSdOfCFfEfP8 +T+M2EAn6EX/TsXOSkiEEAAAAA2FhYQEC +-----END OPENSSH PRIVATE KEY----- diff --git a/test-fixtures/publish/key.pub b/test-fixtures/publish/key.pub new file mode 100644 index 000000000..b9e9be27e --- /dev/null +++ b/test-fixtures/publish/key.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEO48VC/WSlVSSdOfCFfEfP8T+M2EAn6EX/TsXOSkiEE aaa diff --git a/test-fixtures/publish-main-win.txt b/test-fixtures/publish/main-win.txt similarity index 100% rename from test-fixtures/publish-main-win.txt rename to test-fixtures/publish/main-win.txt diff --git a/test-fixtures/publish-main.txt b/test-fixtures/publish/main.txt similarity index 100% rename from test-fixtures/publish-main.txt rename to test-fixtures/publish/main.txt diff --git a/test-fixtures/publish-no-bounds.txt b/test-fixtures/publish/no-bounds.txt similarity index 100% rename from test-fixtures/publish-no-bounds.txt rename to test-fixtures/publish/no-bounds.txt diff --git a/test-fixtures/publish-no-config.txt b/test-fixtures/publish/no-config.txt similarity index 100% rename from test-fixtures/publish-no-config.txt rename to test-fixtures/publish/no-config.txt diff --git a/test-fixtures/publish/no-git.txt b/test-fixtures/publish/no-git.txt new file mode 100644 index 000000000..f6a437de4 --- /dev/null +++ b/test-fixtures/publish/no-git.txt @@ -0,0 +1,15 @@ +Reading Spago workspace configuration... + +✓ Selecting package to build: aaa + +Downloading dependencies... +Building... + Src Lib All +Warnings 0 0 0 +Errors 0 0 0 + +✓ Build succeeded. + + +✘ Could not verify whether the git tree is clean. Error was: + fatal: not a git repository (or any of the parent directories): .git diff --git a/test-fixtures/publish.txt b/test-fixtures/publish/ready.txt similarity index 100% rename from test-fixtures/publish.txt rename to test-fixtures/publish/ready.txt diff --git a/test-fixtures/publish/transfer/aff-new-location.yaml b/test-fixtures/publish/transfer/aff-new-location.yaml new file mode 100644 index 000000000..0032aa6aa --- /dev/null +++ b/test-fixtures/publish/transfer/aff-new-location.yaml @@ -0,0 +1,20 @@ +package: + name: aff + dependencies: + - console: ">=6.0.0 <7.0.0" + - effect: ">=4.0.0 <5.0.0" + - prelude: ">=6.0.1 <7.0.0" + test: + main: Test.Main + dependencies: [] + publish: + version: 0.0.1 + license: MIT + location: + githubOwner: purescript + githubRepo: purescript-aff +workspace: + packageSet: + registry: 28.1.1 + extraPackages: + console: "6.1.0" diff --git a/test-fixtures/publish/transfer/aff.yaml b/test-fixtures/publish/transfer/aff.yaml new file mode 100644 index 000000000..e3a7d006b --- /dev/null +++ b/test-fixtures/publish/transfer/aff.yaml @@ -0,0 +1,20 @@ +package: + name: aff + dependencies: + - console: ">=6.0.0 <7.0.0" + - effect: ">=4.0.0 <5.0.0" + - prelude: ">=6.0.1 <7.0.0" + test: + main: Test.Main + dependencies: [] + publish: + version: 0.0.1 + license: MIT + location: + githubOwner: purescript-contrib + githubRepo: purescript-aff +workspace: + packageSet: + registry: 28.1.1 + extraPackages: + console: "6.1.0" diff --git a/test-fixtures/publish/transfer/never-published.txt b/test-fixtures/publish/transfer/never-published.txt new file mode 100644 index 000000000..0b2e4eb3e --- /dev/null +++ b/test-fixtures/publish/transfer/never-published.txt @@ -0,0 +1,8 @@ +Reading Spago workspace configuration... + +✓ Selecting package to build: aaa + +Refreshing the Registry Index... + +✘ Could not find package 'aaa' in the Registry Index. Has it ever been published? +If not, please run `spago publish` first. Otherwise this is a bug - please report it on the Spago repo. diff --git a/test-fixtures/publish/transfer/no-git.txt b/test-fixtures/publish/transfer/no-git.txt new file mode 100644 index 000000000..fe8075270 --- /dev/null +++ b/test-fixtures/publish/transfer/no-git.txt @@ -0,0 +1,7 @@ +Reading Spago workspace configuration... + +✓ Selecting package to build: aaa + + +✘ Could not verify whether the git tree is clean. Error was: + fatal: not a git repository (or any of the parent directories): .git diff --git a/test-fixtures/publish/transfer/no-key.txt b/test-fixtures/publish/transfer/no-key.txt new file mode 100644 index 000000000..d96cbb0a0 --- /dev/null +++ b/test-fixtures/publish/transfer/no-key.txt @@ -0,0 +1,7 @@ +Reading Spago workspace configuration... + +✓ Selecting package to build: aff + +Refreshing the Registry Index... + +✘ Could not read the private key at the given path. Please check it and try again. diff --git a/test-fixtures/publish/transfer/no-owner.txt b/test-fixtures/publish/transfer/no-owner.txt new file mode 100644 index 000000000..253d03687 --- /dev/null +++ b/test-fixtures/publish/transfer/no-owner.txt @@ -0,0 +1,7 @@ +Reading Spago workspace configuration... + +✓ Selecting package to build: aaa + + +✘ The package does not have any owners set in the config file. +Please run `spago auth` to add your SSH public key to the owners list in the spago.yaml file. diff --git a/test-fixtures/publish/transfer/no-publish-config.txt b/test-fixtures/publish/transfer/no-publish-config.txt new file mode 100644 index 000000000..3c6c3f880 --- /dev/null +++ b/test-fixtures/publish/transfer/no-publish-config.txt @@ -0,0 +1,7 @@ +Reading Spago workspace configuration... + +✓ Selecting package to build: aaaa + + +✘ The package does not have a location set in the config file: add a valid one in `package.publish`. +See the configuration file's documentation: https://github.com/purescript/spago#the-configuration-file diff --git a/test-fixtures/publish/transfer/offline.txt b/test-fixtures/publish/transfer/offline.txt new file mode 100644 index 000000000..da74fbf98 --- /dev/null +++ b/test-fixtures/publish/transfer/offline.txt @@ -0,0 +1,7 @@ +Reading Spago workspace configuration... + +✓ Selecting package to build: aff + +Refreshing the Registry Index... + +✘ Cannot perform Registry operations while offline. diff --git a/test-fixtures/publish/transfer/same-location.txt b/test-fixtures/publish/transfer/same-location.txt new file mode 100644 index 000000000..36a181161 --- /dev/null +++ b/test-fixtures/publish/transfer/same-location.txt @@ -0,0 +1,8 @@ +Reading Spago workspace configuration... + +✓ Selecting package to build: aff + +Refreshing the Registry Index... + +✘ Cannot transfer package: the new location is the same as the current one. +Please edit the `publish.location` field of your `spago.yaml` with the new location. diff --git a/test/Prelude.purs b/test/Prelude.purs index 9ef083142..d43b8f8d8 100644 --- a/test/Prelude.purs +++ b/test/Prelude.purs @@ -403,7 +403,7 @@ escapePathInErrMsg = case Process.platform of _ -> Array.intercalate "/" assertWarning :: forall m. MonadThrow Error m => Array String -> Boolean -> String -> m Unit -assertWarning paths shouldHave stdErr = do +assertWarning paths shouldHave stdErr = do when (not $ Array.all (\exp -> shouldHave == (String.contains (Pattern exp) stdErr)) paths) do Assert.fail $ "STDERR " diff --git a/test/Spago/Config.purs b/test/Spago/Config.purs index 45280866a..89a0f138f 100644 --- a/test/Spago/Config.purs +++ b/test/Spago/Config.purs @@ -25,52 +25,55 @@ spec = Right parsed | parsed == validSpagoYaml.parsed -> pure unit Right parsed -> - Assert.fail $ - "\n-------\nExpected:\n-------\n" <> Yaml.stringifyYaml C.configCodec validSpagoYaml.parsed <> - "\n\n\n-------\nActual:\n-------\n" <> Yaml.stringifyYaml C.configCodec parsed + Assert.fail + $ "\n-------\nExpected:\n-------\n" + <> Yaml.stringifyYaml C.configCodec validSpagoYaml.parsed + <> "\n\n\n-------\nActual:\n-------\n" + <> Yaml.stringifyYaml C.configCodec parsed Spec.it "reports errors" do Yaml.parseYaml C.configCodec invalidLicenseYaml `shouldFailWith` ( "$.package.publish.license: Could not decode PackageConfig:" - <> "\n Could not decode PublishConfig:" - <> "\n Could not decode License:" - <> "\n Invalid SPDX identifier bogus" + <> "\n Could not decode PublishConfig:" + <> "\n Could not decode License:" + <> "\n Invalid SPDX identifier bogus" ) Spec.describe "reports unrecognized fields" do Spec.it "under 'package'" do Yaml.parseYaml C.configCodec unrecognizedPackageFieldYaml `shouldFailWith` ( "$.package: Could not decode PackageConfig:" - <> "\n Unknown field(s): bogus_field" + <> "\n Unknown field(s): bogus_field" ) Spec.it "under 'workspace'" do Yaml.parseYaml C.configCodec unrecognizedBuildOptsFieldYaml `shouldFailWith` ( "$.workspace.buildOpts: Could not decode WorkspaceConfig:" - <> "\n Could not decode WorkspaceBuildOptionsInput:" - <> "\n Unknown field(s): bogus_field" + <> "\n Could not decode WorkspaceBuildOptionsInput:" + <> "\n Unknown field(s): bogus_field" ) Spec.it "under 'publish.location'" do Yaml.parseYaml C.configCodec unrecognizedPublishLocationFieldYaml `shouldFailWith` ( "$.package.publish.location: Could not decode PackageConfig:" - <> "\n Could not decode PublishConfig:" - <> "\n Could not decode Publish Location:" - <> "\n Failed to decode alternatives:" - <> "\n - $.gitUrl: Could not decode Git:" - <> "\n No value found" - <> "\n - Could not decode GitHub:" - <> "\n Unknown field(s): bogus_field" + <> "\n Could not decode PublishConfig:" + <> "\n Could not decode Publish Location:" + <> "\n Failed to decode alternatives:" + <> "\n - $.gitUrl: Could not decode Git:" + <> "\n No value found" + <> "\n - Could not decode GitHub:" + <> "\n Unknown field(s): bogus_field" ) - where - shouldFailWith result expectedError = - case result of - Right _ -> Assert.fail "Expected an error, but parsed successfully" - Left err -> CJ.print err `shouldEqual` expectedError + where + shouldFailWith result expectedError = + case result of + Right _ -> Assert.fail "Expected an error, but parsed successfully" + Left err -> CJ.print err `shouldEqual` expectedError validSpagoYaml :: { serialized :: String, parsed :: C.Config } validSpagoYaml = - { serialized: """ + { serialized: + """ package: name: testpackage publish: @@ -97,43 +100,45 @@ validSpagoYaml = """ , parsed: { package: Just - { name: unsafeFromRight $ PackageName.parse "testpackage" - , publish: Just - { version: unsafeFromRight $ Version.parse "0.0.0" - , license: unsafeFromRight $ License.parse "BSD-3-Clause" - , location: Just $ GitHub { owner: "purescript", repo: "testpackage", subdir: Nothing } - , include: Nothing - , exclude: Nothing - } - , build: Just - { strict: Just true - , censorProjectWarnings: Nothing - , pedanticPackages: Nothing + { name: unsafeFromRight $ PackageName.parse "testpackage" + , publish: Just + { version: unsafeFromRight $ Version.parse "0.0.0" + , license: unsafeFromRight $ License.parse "BSD-3-Clause" + , location: Just $ GitHub { owner: "purescript", repo: "testpackage", subdir: Nothing } + , include: Nothing + , exclude: Nothing + , owners: Nothing + } + , build: Just + { strict: Just true + , censorProjectWarnings: Nothing + , pedanticPackages: Nothing + } + , bundle: Nothing + , run: Nothing + , description: Nothing + , dependencies: mkDependencies [ "aff", "prelude", "console", "effect" ] + , test: Just + { main: "Test.Main" + , dependencies: mkDependencies [ "spec", "spec-node" ] + , censorTestWarnings: Nothing + , execArgs: Nothing + , strict: Nothing + , pedanticPackages: Nothing + } } - , bundle: Nothing - , run: Nothing - , description: Nothing - , dependencies: mkDependencies [ "aff", "prelude", "console", "effect" ] - , test: Just - { main: "Test.Main" - , dependencies: mkDependencies [ "spec", "spec-node" ] - , censorTestWarnings: Nothing - , execArgs: Nothing - , strict: Nothing - , pedanticPackages: Nothing - } - } , workspace: Just - { packageSet: Just $ SetFromRegistry { registry: unsafeFromRight $ Version.parse "56.4.0" } - , extraPackages: Nothing - , backend: Nothing - , buildOpts: Nothing - } + { packageSet: Just $ SetFromRegistry { registry: unsafeFromRight $ Version.parse "56.4.0" } + , extraPackages: Nothing + , backend: Nothing + , buildOpts: Nothing + } } } invalidLicenseYaml :: String -invalidLicenseYaml = """ +invalidLicenseYaml = + """ package: name: spago dependencies: [] @@ -143,7 +148,8 @@ invalidLicenseYaml = """ """ unrecognizedPackageFieldYaml :: String -unrecognizedPackageFieldYaml = """ +unrecognizedPackageFieldYaml = + """ package: name: spago dependencies: [] @@ -151,7 +157,8 @@ unrecognizedPackageFieldYaml = """ """ unrecognizedBuildOptsFieldYaml :: String -unrecognizedBuildOptsFieldYaml = """ +unrecognizedBuildOptsFieldYaml = + """ package: name: spago dependencies: [] @@ -163,7 +170,8 @@ unrecognizedBuildOptsFieldYaml = """ """ unrecognizedPublishLocationFieldYaml :: String -unrecognizedPublishLocationFieldYaml = """ +unrecognizedPublishLocationFieldYaml = + """ package: name: spago dependencies: [] diff --git a/test/Spago/Publish.purs b/test/Spago/Publish.purs index 188fce48b..1fb5b27e7 100644 --- a/test/Spago/Publish.purs +++ b/test/Spago/Publish.purs @@ -19,63 +19,103 @@ spec = Spec.around withTempDir do Spec.it "fails if the version bounds are not specified" \{ spago, fixture } -> do spago [ "init", "--name", "aaaa" ] >>= shouldBeSuccess spago [ "build" ] >>= shouldBeSuccess - spago [ "publish", "--offline" ] >>= shouldBeFailureErr (fixture "publish-no-bounds.txt") + spago [ "publish", "--offline" ] >>= shouldBeFailureErr (fixture "publish/no-bounds.txt") Spec.it "fails if the publish config is not specified" \{ spago, fixture } -> do spago [ "init", "--name", "aaaa" ] >>= shouldBeSuccess spago [ "build" ] >>= shouldBeSuccess spago [ "fetch", "--ensure-ranges" ] >>= shouldBeSuccess - spago [ "publish", "--offline" ] >>= shouldBeFailureErr (fixture "publish-no-config.txt") + spago [ "publish", "--offline" ] >>= shouldBeFailureErr (fixture "publish/no-config.txt") Spec.it "fails if the git tree is not clean" \{ spago, fixture } -> do - FS.copyFile { src: fixture "spago-publish.yaml", dst: "spago.yaml" } + FS.copyFile { src: fixture "publish/basic.yaml", dst: "spago.yaml" } FS.mkdirp "src" - FS.copyFile { src: fixture "publish.purs", dst: "src/Main.purs" } + FS.copyFile { src: fixture "publish/basic.purs", dst: "src/Main.purs" } spago [ "build" ] >>= shouldBeSuccess - spago [ "publish", "--offline" ] >>= shouldBeFailureErr (fixture "publish-no-git.txt") + spago [ "publish", "--offline" ] >>= shouldBeFailureErr (fixture "publish/no-git.txt") Spec.it "fails if the module is called Main" \{ spago, fixture } -> do spago [ "init", "--name", "aaaa" ] >>= shouldBeSuccess FS.unlink "spago.yaml" - FS.copyFile { src: fixture "spago-publish.yaml", dst: "spago.yaml" } + FS.copyFile { src: fixture "publish/basic.yaml", dst: "spago.yaml" } spago [ "build" ] >>= shouldBeSuccess doTheGitThing spago [ "publish", "--offline" ] >>= shouldBeFailureErr case Process.platform of - Just Platform.Win32 -> fixture "publish-main-win.txt" - _ -> fixture "publish-main.txt" + Just Platform.Win32 -> fixture "publish/main-win.txt" + _ -> fixture "publish/main.txt" Spec.it "fails if the publish repo location is not among git remotes" \{ spago, fixture } -> do - FS.copyFile { src: fixture "spago-publish.yaml", dst: "spago.yaml" } + FS.copyFile { src: fixture "publish/basic.yaml", dst: "spago.yaml" } FS.mkdirp "src" - FS.copyFile { src: fixture "publish.purs", dst: "src/Main.purs" } + FS.copyFile { src: fixture "publish/basic.purs", dst: "src/Main.purs" } spago [ "build" ] >>= shouldBeSuccess doTheGitThing - git [ "remote", "set-url", "origin", "git@github.com:purescript/bbb.git" ] - spago [ "publish", "--offline" ] >>= shouldBeFailureErr (fixture "publish-invalid-location.txt") + git [ "remote", "set-url", "origin", "git@github.com:purescript/bbb.git" ] -- TODO check this is a Right? + spago [ "publish", "--offline" ] >>= shouldBeFailureErr (fixture "publish/invalid-location.txt") Spec.it "fails if a core dependency is not in the registry" \{ spago, fixture } -> do FS.copyTree { src: fixture "publish/extra-package-core", dst: "." } spago [ "build" ] >>= shouldBeSuccess doTheGitThing spago [ "fetch" ] >>= shouldBeSuccess - spago [ "publish", "--offline" ] >>= shouldBeFailureErr (fixture "publish-extra-package-core-dependency.txt") + spago [ "publish", "--offline" ] >>= shouldBeFailureErr (fixture "publish/extra-package-core-dependency.txt") Spec.it "can get a package ready to publish" \{ spago, fixture } -> do - FS.copyFile { src: fixture "spago-publish.yaml", dst: "spago.yaml" } + FS.copyFile { src: fixture "publish/basic.yaml", dst: "spago.yaml" } FS.mkdirp "src" - FS.copyFile { src: fixture "publish.purs", dst: "src/Main.purs" } + FS.copyFile { src: fixture "publish/basic.purs", dst: "src/Main.purs" } spago [ "build" ] >>= shouldBeSuccess doTheGitThing -- It will fail because it can't hit the registry, but the fixture will check that everything else is ready spago [ "fetch" ] >>= shouldBeSuccess - spago [ "publish", "--offline" ] >>= shouldBeFailureErr (fixture "publish.txt") + spago [ "publish", "--offline" ] >>= shouldBeFailureErr (fixture "publish/ready.txt") Spec.it "allows to publish with a test dependency not in the registry" \{ spago, fixture } -> do FS.copyTree { src: fixture "publish/extra-package-test", dst: "." } spago [ "build" ] >>= shouldBeSuccess doTheGitThing spago [ "fetch" ] >>= shouldBeSuccess - spago [ "publish", "--offline" ] >>= shouldBeFailureErr (fixture "publish.txt") + spago [ "publish", "--offline" ] >>= shouldBeFailureErr (fixture "publish/ready.txt") + + Spec.describe "transfer" do + + Spec.it "fails if the publish config is not specified" \{ spago, fixture } -> do + spago [ "init", "--name", "aaaa" ] >>= shouldBeSuccess + spago [ "registry", "transfer", "--offline", "-i", (fixture "publish/key") ] >>= shouldBeFailureErr (fixture "publish/transfer/no-publish-config.txt") + + Spec.it "fails if the config does not specify an owner" \{ spago, fixture } -> do + FS.copyFile { src: fixture "publish/basic.yaml", dst: "spago.yaml" } + spago [ "build" ] >>= shouldBeSuccess + spago [ "registry", "transfer", "--offline", "-i", (fixture "publish/key") ] >>= shouldBeFailureErr (fixture "publish/transfer/no-owner.txt") + + Spec.it "fails if the git tree is not clean" \{ spago, fixture } -> do + FS.copyFile { src: fixture "publish/basic.yaml", dst: "spago.yaml" } + spago [ "auth", "-i", (fixture "publish/key") ] >>= shouldBeSuccess + spago [ "registry", "transfer", "--offline", "-i", (fixture "publish/key") ] >>= shouldBeFailureErr (fixture "publish/transfer/no-git.txt") + + Spec.it "fails if the package has never been published before" \{ spago, fixture } -> do + FS.copyFile { src: fixture "publish/basic.yaml", dst: "spago.yaml" } + spago [ "auth", "-i", (fixture "publish/key") ] >>= shouldBeSuccess + doTheGitThing + spago [ "registry", "transfer", "-i", (fixture "publish/key") ] >>= shouldBeFailureErr (fixture "publish/transfer/never-published.txt") + + Spec.it "fails if the new repo location is the same as the current one in the registry" \{ spago, fixture } -> do + FS.copyFile { src: fixture "publish/transfer/aff.yaml", dst: "spago.yaml" } + spago [ "auth", "-i", (fixture "publish/key") ] >>= shouldBeSuccess + doTheGitThing + spago [ "registry", "transfer", "-i", (fixture "publish/key") ] >>= shouldBeFailureErr (fixture "publish/transfer/same-location.txt") + + Spec.it "fails if can't find the private key" \{ spago, fixture } -> do + FS.copyFile { src: fixture "publish/transfer/aff-new-location.yaml", dst: "spago.yaml" } + spago [ "auth", "-i", (fixture "publish/key") ] >>= shouldBeSuccess + doTheGitThing + spago [ "registry", "transfer", "-i", (fixture "publish/no-key") ] >>= shouldBeFailureErr (fixture "publish/transfer/no-key.txt") + + Spec.it "fails if running with --offline" \{ spago, fixture } -> do + FS.copyFile { src: fixture "publish/transfer/aff-new-location.yaml", dst: "spago.yaml" } + spago [ "auth", "-i", (fixture "publish/key") ] >>= shouldBeSuccess + doTheGitThing + spago [ "registry", "transfer", "--offline", "-i", (fixture "publish/key") ] >>= shouldBeFailureErr (fixture "publish/transfer/offline.txt") Spec.it "#1110 installs versions of packages that are returned by the registry solver, but not present in cache" \{ spago, fixture } -> do let @@ -85,9 +125,9 @@ spec = Spec.around withTempDir do , result: isLeft , sanitize: String.trim - >>> withForwardSlashes - >>> String.replaceAll (String.Pattern "\r\n") (String.Replacement "\n") - >>> Regex.replace buildOrderRegex "[x of 3] Compiling module-name" + >>> withForwardSlashes + >>> String.replaceAll (String.Pattern "\r\n") (String.Replacement "\n") + >>> Regex.replace buildOrderRegex "[x of 3] Compiling module-name" } -- We have to ignore lines like "[1 of 3] Compiling Effect.Console" when @@ -95,7 +135,8 @@ spec = Spec.around withTempDir do -- different order, depending on how the system resources happened to -- align at the moment of the test run. buildOrderRegex = unsafeFromRight $ Regex.regex - "\\[\\d of 3\\] Compiling (Effect\\.Console|Effect\\.Class\\.Console|Lib)" RF.global + "\\[\\d of 3\\] Compiling (Effect\\.Console|Effect\\.Class\\.Console|Lib)" + RF.global FS.copyTree { src: fixture "publish/1110-solver-different-version", dst: "." } spago [ "build" ] >>= shouldBeSuccess @@ -123,11 +164,12 @@ spec = Spec.around withTempDir do spago [ "publish", "--offline" ] >>= shouldBeFailureErr' (fixture "publish/1110-solver-different-version/failure-stderr.txt") Spec.describe "#1060 auto-filling the `publish.location` field" do - let prepareProject spago fixture = do - FS.copyTree { src: fixture "publish/1060-autofill-location/project", dst: "." } - spago [ "build" ] >>= shouldBeSuccess - doTheGitThing - spago [ "fetch" ] >>= shouldBeSuccess + let + prepareProject spago fixture = do + FS.copyTree { src: fixture "publish/1060-autofill-location/project", dst: "." } + spago [ "build" ] >>= shouldBeSuccess + doTheGitThing + spago [ "fetch" ] >>= shouldBeSuccess Spec.it "happens for root package" \{ fixture, spago } -> do prepareProject spago fixture