Skip to content

Commit 4e9b5aa

Browse files
authored
Spago publish verifies that the publish location matches one of the current repo's remotes (#1259)
1 parent 20afe6e commit 4e9b5aa

File tree

8 files changed

+126
-11
lines changed

8 files changed

+126
-11
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Other improvements:
1717
- Added support for `--package-set` options for `spago upgrade`.
1818
- `spago repl` now writes a `.purs-repl` file, unless already there, containing `import Prelude`.
1919
- Added typo suggestions upon failing to find a package by name.
20+
- `spago publish` now checks that the publish location matches one of the remotes in the current Git repository.
2021

2122
## [0.21.0] - 2023-05-04
2223

src/Spago/Command/Publish.purs

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import Spago.Command.Fetch as Fetch
3838
import Spago.Config (Package(..), Workspace, WorkspacePackage)
3939
import Spago.Config as Config
4040
import Spago.Db (Db)
41+
import Spago.FS as FS
4142
import Spago.Git (Git)
4243
import Spago.Git as Git
4344
import Spago.Json as Json
@@ -190,6 +191,12 @@ publish _args = do
190191
, "submit a transfer operation."
191192
]
192193

194+
unlessM (locationIsInGitRemotes location) $ addError $ toDoc
195+
[ "The location specified in the manifest file"
196+
, "(" <> Json.stringifyJson Location.codec location <> ")"
197+
, " is not one of the remotes in the git repository."
198+
]
199+
193200
-- Check that all the dependencies come from the registry
194201
let
195202
{ fail, success: _ } =
@@ -251,9 +258,9 @@ publish _args = do
251258

252259
-- These are "soft" git tag checks. We notify the user of errors
253260
-- they need to fix. But these commands must not have the user
254-
-- 1) create/push a git tag that is known to be unpublishable,
261+
-- 1) create/push a git tag that is known to be unpublishable,
255262
-- thereby forcing them to create another git tag later with the fix.
256-
-- 2) input any login credentials as there are other errors to fix
263+
-- 2) input any login credentials as there are other errors to fix
257264
-- before doing that.
258265
-- The "hard" git tag checks will occur only if these succeed.
259266
Git.getStatus Nothing >>= case _ of
@@ -353,7 +360,7 @@ publish _args = do
353360
}
354361
)
355362

356-
-- As above: there's a pending failure exit and its' easier to just abort here
363+
-- As above: there's a pending failure exit and it's easier to just abort here
357364
when (not builtAgain) $
358365
Effect.liftEffect Process.exit
359366

@@ -452,5 +459,19 @@ waitForJobFinish jobId = go Nothing
452459
true -> logSuccess $ "Registry finished processing the package. Your package was published successfully!"
453460
false -> die $ "Registry finished processing the package, but it failed. Please fix it and try again."
454461

462+
locationIsInGitRemotes :: a. Location -> Spago (PublishEnv a) Boolean
463+
locationIsInGitRemotes location = do
464+
isGitRepo <- FS.exists ".git"
465+
if not isGitRepo then
466+
pure false
467+
else
468+
Git.getRemotes Nothing >>= case _ of
469+
Left err ->
470+
die $ toDoc err
471+
Right remotes ->
472+
pure $ remotes # Array.any \r -> case location of
473+
Location.Git { url } -> r.url == url
474+
Location.GitHub { owner, repo } -> r.owner == owner && r.repo == repo
475+
455476
baseApi :: String
456477
baseApi = "https://registry.purescript.org"

src/Spago/Git.purs

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ module Spago.Git
44
, fetchRepo
55
, getGit
66
, getRef
7-
, listTags
7+
, getRemotes
88
, getStatus
9-
, pushTag
109
, isIgnored
10+
, listTags
11+
, parseRemote
12+
, pushTag
1113
, tagCheckedOut
1214
) where
1315

@@ -16,11 +18,15 @@ import Spago.Prelude
1618
import Control.Monad.Except (ExceptT(..))
1719
import Control.Monad.Except as Except
1820
import Data.Array as Array
21+
import Data.Array.NonEmpty as NEA
22+
import Data.Maybe (fromJust)
1923
import Data.String (Pattern(..))
2024
import Data.String as String
25+
import Data.String.Regex as Regex
2126
import Node.ChildProcess.Types (Exit(..))
2227
import Node.Path as Path
2328
import Node.Process as Process
29+
import Partial.Unsafe (unsafePartial)
2430
import Registry.Version as Version
2531
import Spago.Cmd as Cmd
2632
import Spago.FS as FS
@@ -29,6 +35,8 @@ type Git = { cmd :: String, version :: String }
2935

3036
type GitEnv a = { git :: Git, logOptions :: LogOptions, offline :: OnlineStatus | a }
3137

38+
type Remote = { name :: String, url :: String, owner :: String, repo :: String }
39+
3240
runGit_ :: forall a. Array String -> Maybe FilePath -> ExceptT String (Spago (GitEnv a)) Unit
3341
runGit_ args cwd = void $ runGit args cwd
3442

@@ -112,6 +120,22 @@ getRef cwd = do
112120
]
113121
Right r -> pure $ Right r.stdout
114122

123+
getRemotes :: forall a. Maybe FilePath -> Spago (GitEnv a) (Either Docc (Array Remote))
124+
getRemotes = \cwd -> do
125+
let opts = Cmd.defaultExecOptions { pipeStdout = false, pipeStderr = false, cwd = cwd }
126+
{ git } <- ask
127+
Cmd.exec git.cmd [ "remote", "--verbose" ] opts <#> case _ of
128+
Left r -> Left $ toDoc
129+
[ "Could not run `git remote --verbose` to verify correct repository path. Error:"
130+
, r.stderr
131+
]
132+
Right { stdout: "" } ->
133+
pure []
134+
Right r ->
135+
r.stdout # String.split (Pattern "\n") # Array.mapMaybe parseRemote # case _ of
136+
[] -> Left $ toDoc "Could not parse any remotes from the output of `git remote --verbose`."
137+
remotes -> Right $ Array.nub remotes
138+
115139
tagCheckedOut :: forall a. Maybe FilePath -> Spago (GitEnv a) (Either Docc String)
116140
tagCheckedOut cwd = do
117141
let opts = Cmd.defaultExecOptions { pipeStdout = false, pipeStderr = false, cwd = cwd }
@@ -177,3 +201,16 @@ getGit = do
177201
Left r -> do
178202
logDebug $ Cmd.printExecResult r
179203
die [ "Failed to find git. Have you installed it, and is it in your PATH?" ]
204+
205+
parseRemote :: String -> Maybe Remote
206+
parseRemote = \line ->
207+
case String.split (Pattern "\t") line of
208+
[ name, secondPart ]
209+
| [ url, _ ] <- String.split (Pattern " ") secondPart
210+
, Just [ _, _, Just owner, Just repo ] <- NEA.toArray <$> Regex.match gitUrlRegex url ->
211+
Just { name, url, owner, repo }
212+
_ ->
213+
Nothing
214+
where
215+
gitUrlRegex = unsafePartial $ fromJust $ hush $
216+
Regex.regex "^(.+@.+:|https?:\\/\\/.+\\/)(.*)\\/(.+)\\.git$" mempty
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
Reading Spago workspace configuration...
2+
3+
✅ Selecting package to build: aaa
4+
5+
Downloading dependencies...
6+
Building...
7+
Src Lib All
8+
Warnings 0 0 0
9+
Errors 0 0 0
10+
11+
✅ Build succeeded.
12+
13+
Your package "aaa" is not ready for publishing yet, encountered 1 error:
14+
15+
16+
❌ The location specified in the manifest file
17+
({"githubOwner":"purescript","githubRepo":"aaa"})
18+
is not one of the remotes in the git repository.

test/Spago/Errors.purs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,10 @@ import Test.Prelude
44

55
import Data.Array as Array
66
import Data.Foldable (traverse_)
7-
import Data.String (joinWith)
87
import Data.String as String
98
import Spago.FS as FS
109
import Test.Spec (Spec)
1110
import Test.Spec as Spec
12-
import Test.Spec.Assertions.String (shouldContain)
1311

1412
spec :: Spec Unit
1513
spec = Spec.around withTempDir do

test/Spago/Publish.purs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,15 @@ spec = Spec.around withTempDir do
4141
Just Platform.Win32 -> fixture "publish-main-win.txt"
4242
_ -> fixture "publish-main.txt"
4343

44+
Spec.it "fails if the publish repo location is not among git remotes" \{ spago, fixture } -> do
45+
FS.copyFile { src: fixture "spago-publish.yaml", dst: "spago.yaml" }
46+
FS.mkdirp "src"
47+
FS.copyFile { src: fixture "publish.purs", dst: "src/Main.purs" }
48+
spago [ "build" ] >>= shouldBeSuccess
49+
doTheGitThing
50+
git [ "remote", "set-url", "origin", "git@github.com:purescript/bbb.git" ] >>= shouldBeSuccess
51+
spago [ "publish", "--offline" ] >>= shouldBeFailureErr (fixture "publish-invalid-location.txt")
52+
4453
Spec.it "can get a package ready to publish" \{ spago, fixture } -> do
4554
FS.copyFile { src: fixture "spago-publish.yaml", dst: "spago.yaml" }
4655
FS.mkdirp "src"
@@ -61,7 +70,8 @@ doTheGitThing = do
6170
git [ "add", "." ] >>= shouldBeSuccess
6271
git [ "commit", "-m", "first" ] >>= shouldBeSuccess
6372
git [ "tag", "v0.0.1" ] >>= shouldBeSuccess
64-
where
65-
git :: Array String -> Aff (Either ExecResult ExecResult)
66-
git args = Cmd.exec "git" args
67-
$ Cmd.defaultExecOptions { pipeStdout = false, pipeStderr = false, pipeStdin = StdinNewPipe }
73+
git [ "remote", "add", "origin", "git@github.com:purescript/aaa.git" ] >>= shouldBeSuccess
74+
75+
git :: Array String -> Aff (Either ExecResult ExecResult)
76+
git args = Cmd.exec "git" args
77+
$ Cmd.defaultExecOptions { pipeStdout = false, pipeStderr = false, pipeStdin = StdinNewPipe }

test/Spago/Unit.purs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import Prelude
44

55
import Test.Spago.Unit.CheckInjectivity as CheckInjectivity
66
import Test.Spago.Unit.FindFlags as FindFlags
7+
import Test.Spago.Unit.Git as Git
78
import Test.Spago.Unit.Printer as Printer
89
import Test.Spec (Spec)
910
import Test.Spec as Spec
@@ -13,3 +14,4 @@ spec = Spec.describe "unit" do
1314
FindFlags.spec
1415
CheckInjectivity.spec
1516
Printer.spec
17+
Git.spec

test/Spago/Unit/Git.purs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
module Test.Spago.Unit.Git where
2+
3+
import Prelude
4+
5+
import Spago.Git (parseRemote)
6+
import Test.Prelude (Maybe(..), shouldEqual)
7+
import Test.Spec (Spec)
8+
import Test.Spec as Spec
9+
10+
spec :: Spec Unit
11+
spec = do
12+
Spec.describe "Git" do
13+
Spec.describe "parseRemote" do
14+
15+
Spec.it "parses a remote with a git protocol" do
16+
parseRemote "origin\tgit@github.com:foo/bar.git (fetch)"
17+
`shouldEqual` Just { name: "origin", url: "git@github.com:foo/bar.git", owner: "foo", repo: "bar" }
18+
19+
Spec.it "parses a remote with an https protocol" do
20+
parseRemote "origin\thttps://github.com/foo/bar.git (push)"
21+
`shouldEqual` Just { name: "origin", url: "https://github.com/foo/bar.git", owner: "foo", repo: "bar" }
22+
23+
Spec.it "rejects malformed remotes" do
24+
parseRemote "origin\tgit@github.com:foo/bar.git" `shouldEqual` Nothing
25+
parseRemote "origin\tgit@github.com:foo/bar (push)" `shouldEqual` Nothing
26+
parseRemote "origin git@github.com:foo/bar.git (fetch)" `shouldEqual` Nothing
27+
parseRemote "origin\tgit@github.com:foo.git (push)" `shouldEqual` Nothing
28+
parseRemote "origin\thttps://foo.com/bar.git (push)" `shouldEqual` Nothing

0 commit comments

Comments
 (0)