Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement auth and transfer commands #1298

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 32 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

Expand Down Expand Up @@ -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.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Does this mean that I can "take over" somebody else's package by doing this before they get to it?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Oh, this is dependent on me having the right to commit the public key to the repo?


#### 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 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.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
* Depending 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.
* 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,
Expand Down Expand Up @@ -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
16 changes: 16 additions & 0 deletions bin/src/Flags.purs
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
77 changes: 46 additions & 31 deletions bin/src/Main.purs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -203,13 +204,15 @@ data Command a
| RegistryInfo RegistryInfoArgs
| RegistryPackageSets RegistryPackageSetsArgs
| RegistrySearch RegistrySearchArgs
| RegistryTransfer RegistryTransferArgs
| Repl ReplArgs
| Run RunArgs
| Sources SourcesArgs
| Test TestArgs
| 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_ =
Expand All @@ -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 unpiblish operations"
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
[ commandParser "auth" (Auth <$> authArgsParser) "Authenticate as the owner of a package, to allow transfer and unpiblish operations"
[ 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
Expand All @@ -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"

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 ())
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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 }
Expand Down
2 changes: 2 additions & 0 deletions core/spago.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package:
- exceptions
- filterable
- foldable-traversable
- foreign
- functions
- identity
- integers
Expand All @@ -39,3 +40,4 @@ package:
- stringutils
- transformers
- tuples
- unsafe-coerce
4 changes: 4 additions & 0 deletions core/src/Config.purs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
12 changes: 0 additions & 12 deletions core/src/FS.purs
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,13 @@ module Spago.FS
, ensureFileSync
, exists
, getInBetweenPaths
, isLink
, ls
, mkdirp
, moveSync
, readJsonFile
, readTextFile
, readYamlDocFile
, readYamlFile
, stat
, writeFile
, writeJsonFile
, writeTextFile
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down
7 changes: 7 additions & 0 deletions core/src/Json.purs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
6 changes: 3 additions & 3 deletions docs-search/index/src/Docs/Search/IndexBuilder.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import { globSync } from "glob";
import { globSync } from 'glob';
import path from "node:path";
import { fileURLToPath } from "node:url";

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");
}
Expand Down
6 changes: 3 additions & 3 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading