Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
bdfa409
Run constructBuildPlan in Aff
JordanMartinez Feb 25, 2022
05cc8f5
Enable fFGHR to use first pre-release found
JordanMartinez Feb 25, 2022
cdd7670
Dedent 'where' functions and add type sigs
JordanMartinez Feb 25, 2022
2bdff82
Export fFGHR and use in getVersionField
JordanMartinez Feb 25, 2022
e7b9356
Make unstable get latest release (pre-release or not)
JordanMartinez Feb 28, 2022
e68d290
Rename any release to stable release
JordanMartinez Feb 28, 2022
2325aac
Put branch on new line
JordanMartinez Feb 28, 2022
93b1078
Update readme about 'unstable' version
JordanMartinez Feb 28, 2022
406f938
Duplicate logic for unstable; print different info msg
JordanMartinez Mar 1, 2022
604dd73
Parse desired label's version
JordanMartinez Mar 1, 2022
da96920
Relocate ToolMap; define and use its Json codecs
JordanMartinez Mar 1, 2022
a935a5e
Use 1 API call run to get latest and unstable
JordanMartinez Mar 1, 2022
0dcfa89
Update spago deps to fix error
JordanMartinez Mar 1, 2022
ecf352a
Use custom Map codec to produce more common json
JordanMartinez Mar 1, 2022
38f1b1a
Rebuild code in nix-shell
JordanMartinez Mar 1, 2022
bb08218
Run `node update.js`
JordanMartinez Mar 1, 2022
85b8dd5
Merge branch 'main' into support-pre-release-versions
JordanMartinez Mar 1, 2022
700c10e
Temp - print json being decoded
JordanMartinez Mar 1, 2022
25b6c93
Use env to determine which versions file to use
JordanMartinez Mar 1, 2022
eefd145
Run npm run build while in nix-shell
JordanMartinez Mar 1, 2022
1d5cd86
No longer print debug info
JordanMartinez Mar 1, 2022
abd548e
Remove unneeded import
JordanMartinez Mar 1, 2022
736b373
Run npm run build while in nix-shell
JordanMartinez Mar 1, 2022
25dcbd5
Revert versions.json schema change
JordanMartinez Mar 1, 2022
d17111a
Add v2 file; encode to all versions, decode from v2
JordanMartinez Mar 1, 2022
50d457d
Drop ToolMap type
JordanMartinez Mar 1, 2022
3792b8e
Add foreign-object
JordanMartinez Mar 1, 2022
80997ca
Run npm run build in nix-shell
JordanMartinez Mar 1, 2022
4d596c5
Run node update.js
JordanMartinez Mar 1, 2022
2ceffc7
Update readme's description of `latest` and `unstable`
JordanMartinez Mar 1, 2022
a1dd73b
Rename to printTool; update comment
JordanMartinez Mar 1, 2022
1ee3587
Account for build metadata
JordanMartinez Mar 1, 2022
085bc4e
Add lists to fix spago error
JordanMartinez Mar 1, 2022
b3725fa
Run npm run build in nix-shell
JordanMartinez Mar 1, 2022
2fcfbb8
Add blank line to end of file
JordanMartinez Mar 1, 2022
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
4 changes: 2 additions & 2 deletions src/Main.purs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import Prelude
import Affjax (printError)
import Affjax as AX
import Affjax.ResponseFormat as RF
import Control.Monad.Except.Trans (ExceptT(..), mapExceptT, runExceptT)
import Control.Monad.Except.Trans (ExceptT(..), runExceptT)
import Data.Bifunctor (lmap)
import Data.Either (Either(..))
import Data.Foldable (traverse_)
Expand All @@ -21,7 +21,7 @@ import Setup.UpdateVersions (updateVersions)
main :: Effect Unit
main = runAff_ go $ runExceptT do
versionsJson <- ExceptT $ map (lmap (error <<< printError)) $ AX.get RF.json versionsFile
tools <- mapExceptT liftEffect $ constructBuildPlan versionsJson.body
tools <- constructBuildPlan versionsJson.body
liftEffect $ Core.info "Constructed build plan."
traverse_ getTool tools
liftEffect $ Core.info "Fetched tools."
Expand Down
57 changes: 43 additions & 14 deletions src/Setup/BuildPlan.purs
Original file line number Diff line number Diff line change
Expand Up @@ -2,58 +2,81 @@ module Setup.BuildPlan (constructBuildPlan, BuildPlan) where

import Prelude

import Control.Monad.Except.Trans (ExceptT)
import Control.Monad.Except.Trans (ExceptT, mapExceptT)
import Data.Argonaut.Core (Json)
import Data.Argonaut.Decode (decodeJson, printJsonDecodeError, (.:))
import Data.Array as Array
import Data.Bifunctor (lmap)
import Data.Either (Either(..))
import Data.Foldable (fold)
import Data.Maybe (Maybe(..))
import Data.String (Pattern(..))
import Data.String as String
import Data.Traversable (traverse)
import Data.Version (Version)
import Data.Version as Version
import Effect (Effect)
import Effect.Aff (error, throwError)
import Effect.Aff (Aff, error, throwError)
import Effect.Aff.Class (liftAff)
import Effect.Class (liftEffect)
import Effect.Exception (Error)
import GitHub.Actions.Core as Core
import Setup.Data.Key (Key)
import Setup.Data.Key as Key
import Setup.Data.Tool (Tool)
import Setup.Data.Tool as Tool
import Setup.UpdateVersions (ReleaseType(..), fetchFromGitHubReleases)
import Text.Parsing.Parser (parseErrorMessage)
import Text.Parsing.Parser as ParseError

-- | The list of tools that should be downloaded and cached by the action
type BuildPlan = Array { tool :: Tool, version :: Version }

-- | Construct the list of tools that sholud be downloaded and cached by the action
constructBuildPlan :: Json -> ExceptT Error Effect BuildPlan
constructBuildPlan :: Json -> ExceptT Error Aff BuildPlan
constructBuildPlan json = map Array.catMaybes $ traverse (resolve json) Tool.allTools

-- | The parsed value of an input field that specifies a version
data VersionField = Latest | Exact Version
data VersionField
-- | Pre-releases for versions matching the given version
= Unstable Version
-- | Lookup the latest release that is not a pre-release
| Latest
-- | Use the given version
| Exact Version

-- | Attempt to read the value of an input specifying a tool version
getVersionField :: Key -> ExceptT Error Effect (Maybe VersionField)
getVersionField :: Key -> ExceptT Error Aff (Maybe VersionField)
getVersionField key = do
value <- Core.getInput' (Key.toString key)
value <- mapExceptT liftEffect $ Core.getInput' (Key.toString key)
case value of
"" ->
pure Nothing
"latest" ->
pure (pure Latest)
val -> case Version.parseVersion val of
Left msg -> do
liftEffect $ Core.error $ fold [ "Failed to parse version ", val ]
throwError (error (ParseError.parseErrorMessage msg))
Right version ->
pure (pure (Exact version))
val
| Just versionStr <- String.stripPrefix (Pattern "unstable-") val -> do
if Key.toString key /= "purescript" then do
liftEffect $ Core.error $
fold [ "Pre-release versions only work for the PureScript tool" ]
throwError (error $ fold [ "Could not get version for key ", Key.toString key ])
else case Version.parseVersion versionStr of
Left msg -> do
liftEffect $ Core.error $
fold [ "Failed to parse pre-release version ", versionStr ]
throwError (error (ParseError.parseErrorMessage msg))
Right v -> do
pure (pure (Unstable v))

| otherwise -> case Version.parseVersion val of
Left msg -> do
liftEffect $ Core.error $ fold [ "Failed to parse version ", val ]
throwError (error (ParseError.parseErrorMessage msg))
Right version ->
pure (pure (Exact version))

-- | Resolve the exact version to provide for a tool in the environment, based
-- | on the action.yml file.
resolve :: Json -> Tool -> ExceptT Error Effect (Maybe { tool :: Tool, version :: Version })
resolve :: Json -> Tool -> ExceptT Error Aff (Maybe { tool :: Tool, version :: Version })
resolve versionsContents tool = do
let key = Key.fromTool tool
field <- getVersionField key
Expand All @@ -78,3 +101,9 @@ resolve versionsContents tool = do

Right v -> do
pure (pure { tool, version: v })

Just (Unstable v) -> do
liftEffect do
Core.info $ fold [ "Fetching most recent pre-release for ", Tool.name tool, "@", Version.showVersion v ]
version <- liftAff $ fetchFromGitHubReleases (OnlyPreReleases v) (Tool.repository tool)
pure (pure { tool, version })
235 changes: 131 additions & 104 deletions src/Setup/UpdateVersions.purs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
-- | The source used to fetch and update the latest versions in the versions.json
-- | file, which records the latest version of each tool.
module Setup.UpdateVersions (updateVersions) where
module Setup.UpdateVersions
( updateVersions
, fetchFromGitHubReleases
, ReleaseType(..)
) where

import Prelude

Expand All @@ -19,7 +23,7 @@ import Data.Maybe (Maybe(..), fromMaybe, isNothing)
import Data.String as String
import Data.Traversable (for, traverse)
import Data.Tuple (Tuple(..))
import Data.Version (Version)
import Data.Version (Version, showVersion)
import Data.Version as Version
import Effect (Effect)
import Effect.Aff (Aff, Error, Milliseconds(..), delay, error, throwError)
Expand All @@ -34,6 +38,13 @@ import Node.FS.Sync (writeTextFile)
import Node.Path (FilePath)
import Setup.Data.Tool (Tool(..))
import Setup.Data.Tool as Tool
import Text.Parsing.Parser (ParseError)

data ReleaseType
= OnlyPreReleases Version
| OnlyRealReleases

derive instance Eq ReleaseType

-- | Write the latest version of each supported tool
updateVersions :: Aff Unit
Expand All @@ -60,115 +71,131 @@ updateVersions = do
-- | releases, falls back to the highest valid semantic version tag for the tool.
fetchLatestReleaseVersion :: Tool -> Aff Version
fetchLatestReleaseVersion tool = Tool.repository tool # case tool of
PureScript -> fetchFromGitHubReleases
Spago -> fetchFromGitHubReleases
PureScript -> fetchFromGitHubReleases OnlyRealReleases
Spago -> fetchFromGitHubReleases OnlyRealReleases
Psa -> fetchFromGitHubTags
PursTidy -> fetchFromGitHubTags
Zephyr -> fetchFromGitHubReleases
where
-- TODO: These functions really ought to be in ExceptT to avoid all the
-- nested branches.
fetchFromGitHubReleases repo = recover do
page <- liftEffect (Ref.new 1)
untilJust do
versions <- liftEffect (Ref.read page) >>= toolVersions repo
case versions of
Just versions' -> do
let version = Array.find (not <<< Version.isPreRelease) versions'
when (isNothing version) do
liftEffect $ void $ Ref.modify (_ + 1) page
pure version

Nothing ->
throwError $ error "Could not find version that is not a pre-release version"

toolVersions :: Tool.ToolRepository -> Int -> Aff (Maybe (Array Version))
toolVersions repo page = do
let
url =
"https://api.github.com/repos/"
<> repo.owner
<> "/"
<> repo.name
<> "/releases?per_page=10&page="
<> show page

AX.get RF.json url >>= case _ of
Left err -> throwError (error $ AX.printError err)
Right { body } -> case decodeJson body of
Left e -> do
throwError $ error
$ fold
Zephyr -> fetchFromGitHubReleases OnlyRealReleases

-- TODO: These functions really ought to be in ExceptT to avoid all the
-- nested branches.
fetchFromGitHubReleases :: ReleaseType -> Tool.ToolRepository -> Aff Version
fetchFromGitHubReleases releaseType repo = recover do
page <- liftEffect (Ref.new 1)
let
releaseFilter = case releaseType of
OnlyRealReleases ->
not <<< Version.isPreRelease
OnlyPreReleases expectedV -> do
let
equating f a b = (f a) == (f b)
majorMinorMatch v = equating Version.major expectedV v && equating Version.minor expectedV v
majorMinorMatch && Version.isPreRelease
untilJust do
versions <- liftEffect (Ref.read page) >>= toolVersions repo
case versions of
Just versions' -> do
let version = Array.find releaseFilter versions'
when (isNothing version) do
liftEffect $ void $ Ref.modify (_ + 1) page
pure version

Nothing ->
case releaseType of
OnlyRealReleases ->
throwError $ error "Could not find version that is not a pre-release version"
OnlyPreReleases v ->
throwError $ error $ "Could not find a pre-release version of version, " <> showVersion v

toolVersions :: Tool.ToolRepository -> Int -> Aff (Maybe (Array Version))
toolVersions repo page = do
let
url =
"https://api.github.com/repos/"
<> repo.owner
<> "/"
<> repo.name
<> "/releases?per_page=10&page="
<> show page

AX.get RF.json url >>= case _ of
Left err -> throwError (error $ AX.printError err)
Right { body } -> case decodeJson body of
Left e -> do
throwError $ error
$ fold
[ "Failed to decode GitHub response. This is most likely due to a timeout.\n\n"
, printJsonDecodeError e
, stringifyWithIndent 2 body
]
Right [] -> pure Nothing
Right objects ->
Just
<$> Array.catMaybes
<$> for objects \obj ->
case obj .: "tag_name" of
Left e ->
throwError $ error $ fold
[ "Failed to get tag from GitHub response: "
, printJsonDecodeError e
]
Right tagName ->
case tagStrToVersion tagName of
Left _ -> do
liftEffect $ warning $ fold
[ "Got invalid version"
, tagName
, " from "
, repo.name
Right [] -> pure Nothing
Right objects ->
Just
<$> Array.catMaybes
<$> for objects \obj ->
case obj .: "tag_name" of
Left e ->
throwError $ error $ fold
[ "Failed to get tag from GitHub response: "
, printJsonDecodeError e
]
Right tagName ->
case tagStrToVersion tagName of
Left _ -> do
liftEffect $ warning $ fold
[ "Got invalid version"
, tagName
, " from "
, repo.name
]
pure Nothing
Right version -> case obj .: "draft" of
Left e ->
throwError $ error $ fold
[ "Failed to get draft from GitHub response: "
, printJsonDecodeError e
]
pure Nothing
Right version -> case obj .: "draft" of
Left e ->
throwError $ error $ fold
[ "Failed to get draft from GitHub response: "
, printJsonDecodeError e
]
Right isDraft ->
pure
if isDraft then Nothing
else Just version

tagStrToVersion tagStr =
tagStr
# String.stripPrefix (String.Pattern "v")
# fromMaybe tagStr
# Version.parseVersion

-- If a tool doesn't use GitHub releases and instead only tags versions, then
-- we have to fetch the tags, parse them as appropriate versions, and then sort
-- them according to their semantic version to get the latest one.
fetchFromGitHubTags repo = recover do
let url = "https://api.github.com/repos/" <> repo.owner <> "/" <> repo.name <> "/tags"

AX.get RF.json url >>= case _ of
Left err -> do
throwError (error $ AX.printError err)

Right { body } -> case traverse (_ .: "name") =<< decodeJson body of
Left e -> do
throwError $ error $ fold
[ "Failed to decode GitHub response. This is most likely due to a timeout.\n\n"
, printJsonDecodeError e
, stringifyWithIndent 2 body
]

Right arr -> do
let
tags = Array.mapMaybe (tagStrToVersion >>> hush) arr

case maximum tags of
Nothing ->
throwError $ error "Could not download latest release version."

Just v ->
pure v
Right isDraft ->
pure
if isDraft then Nothing
else Just version
Comment on lines +164 to +166
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is a little clunky (I know it's not your code) -- mind updating it to use guard?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I tried, but either implementation of guard doesn't work.

src/Setup/UpdateVersions.purs:165:23

  165                        guard isDraft
                             ^^^^^^^^^^^^^
  
  No type class instance was found for
  
    Control.Alternative.Alternative Aff
  
  while applying a function guard
    of type Alternative t2 => Boolean -> t2 Unit
    to argument isDraft
  while checking that expression guard isDraft
    has type t0 t1
  in value declaration toolVersions

and

src/Setup/UpdateVersions.purs:165:23

  165                        guard isDraft version
                             ^^^^^^^^^^^^^^^^^^^^^
  
  Could not match type
  
    Version
  
  with type
  
    t0 (Maybe t3)

or

src/Setup/UpdateVersions.purs:165:23

  165                        guard isDraft $ Just version
                             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  
  Could not match type
  
    Version
  
  with type
  
    Maybe t3

Copy link
Collaborator

Choose a reason for hiding this comment

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

Ah, the ol' guardA. No worries, we can leave it as-is.


tagStrToVersion :: String -> Either ParseError Version
tagStrToVersion tagStr =
tagStr
# String.stripPrefix (String.Pattern "v")
# fromMaybe tagStr
# Version.parseVersion

-- If a tool doesn't use GitHub releases and instead only tags versions, then
-- we have to fetch the tags, parse them as appropriate versions, and then sort
-- them according to their semantic version to get the latest one.
fetchFromGitHubTags :: Tool.ToolRepository -> Aff Version
fetchFromGitHubTags repo = recover do
let url = "https://api.github.com/repos/" <> repo.owner <> "/" <> repo.name <> "/tags"

AX.get RF.json url >>= case _ of
Left err -> do
throwError (error $ AX.printError err)

Right { body } -> case traverse (_ .: "name") =<< decodeJson body of
Left e -> do
throwError $ error $ fold
[ "Failed to decode GitHub response. This is most likely due to a timeout.\n\n"
, printJsonDecodeError e
, stringifyWithIndent 2 body
]

Right arr -> do
let
tags = Array.mapMaybe (tagStrToVersion >>> hush) arr

case maximum tags of
Nothing ->
throwError $ error "Could not download latest release version."

Just v ->
pure v

-- Attempt to recover from a failed request by re-attempting according to an
-- exponential backoff strategy.
Expand Down