Skip to content

Commit c692585

Browse files
committed
Always install purs and spago, but make other tools optional
1 parent 52201d5 commit c692585

File tree

15 files changed

+306
-261
lines changed

15 files changed

+306
-261
lines changed

.github/workflows/integration.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ jobs:
1212
steps:
1313
- uses: actions/checkout@v2
1414
- uses: ./ # equivalent to thomashoneyman/setup-purescript@<branch>
15+
with:
16+
purty: "latest"
17+
zephyr: "latest"
1518
- name: Check tools are installed correctly
1619
run: |
1720
purty src/Main.purs

README.md

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
# Setup PureScript Action
22

3-
A GitHub Action which sets up a PureScript toolchain for CI. Contains the following tools:
3+
A GitHub Action which sets up a PureScript toolchain for CI. Contains the following tools by default:
44

55
- The [PureScript compiler](https://github.com/purescript/purescript)
66
- The [Spago package manager and build tool](https://github.com/purescript/spago)
7+
8+
You can also optionally include the following tools:
9+
710
- The [Zephyr dead code elimination tool](https://github.com/coot/zephyr)
811
- The [Purty source code formatter](https://gitlab.com/joneshf/purty)
912

@@ -17,7 +20,7 @@ See the [action.yml](action.yml) file for all possible inputs and outputs.
1720

1821
### Basic
1922

20-
Use the PureScript toolchain with the latest version of each tool (resolved from GitHub releases or the latest tag with a valid semantic version):
23+
Use the PureScript toolchain with the latest versions of PureScript and Spago:
2124

2225
```yaml
2326
steps:
@@ -26,19 +29,21 @@ steps:
2629
- run: spago build
2730
```
2831
29-
### Use Specific Versions
32+
Other tools are not enabled by default, but you can enable them by specifying their version.
33+
34+
### Specify Versions
3035
31-
Use a specific version of any tool by supplying a valid semantic version (only exact versions currently supported):
36+
Each tool can accept a semantic version (only exact versions currently supported) or the string `"latest"`. Tools that are not installed by default must be specified this way to be included in the toolchain.
3237
3338
```yaml
3439
steps:
3540
- uses: actions/checkout@v2
3641
- uses: thomashoneyman/setup-purescript@master
3742
with:
38-
purescript-version: "0.13.8"
39-
spago-version: "0.15.3"
40-
purty-version: "6.2.0"
41-
zephyr-version: "0.3.2"
43+
purescript: "0.13.8"
44+
spago: "0.15.3"
45+
purty: "latest"
46+
zephyr: "0.3.2"
4247
- run: spago build
4348
```
4449

action.yml

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,16 @@ name: "Set up a PureScript toolchain"
22
description: "Install a PureScript toolchain and add it to the PATH"
33
author: "Thomas Honeyman <[email protected]>"
44
inputs:
5-
purescript-version:
5+
purescript:
66
description: "The compiler version to install. Examples: latest, 0.13.8"
77
default: "latest"
8-
spago-version:
8+
spago:
99
description: "The Spago version to install. Examples: latest, 0.15.3"
1010
default: "latest"
11-
purty-version:
11+
purty:
1212
description: "The Purty version to install. Examples: latest, 6.2.0"
13-
default: "latest"
14-
zephyr-version:
13+
zephyr:
1514
description: "The Zephyr version to install. Examples: latest, 0.3.2"
16-
default: "latest"
1715
runs:
1816
using: "node12"
1917
main: "dist/index.js"

dist/index.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/index.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Actions/Core.purs

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@ import Prelude
1616
import Data.Maybe (Maybe)
1717
import Data.Nullable (Nullable, toMaybe)
1818
import Effect (Effect)
19-
import Setup.Data.Input (Input)
20-
import Setup.Data.Input as Input
19+
import Setup.Data.Key (Key)
2120

2221
-- | Prepends input path to the PATH (for this action and future actions)
2322
foreign import addPath :: String -> Effect Unit
@@ -31,11 +30,11 @@ foreign import error :: String -> Effect Unit
3130
-- | Sets env variable for this action and future actions in the job
3231
foreign import exportVariable :: { key :: String, value :: String } -> Effect Unit
3332

34-
foreign import getInputImpl :: String -> Effect (Nullable String)
33+
foreign import getInputImpl :: Key -> Effect (Nullable String)
3534

3635
-- | Gets the value of an input. The value is also trimmed.
37-
getInput :: Input -> Effect (Maybe String)
38-
getInput = map toMaybe <<< getInputImpl <<< Input.toKey
36+
getInput :: Key -> Effect (Maybe String)
37+
getInput = map toMaybe <<< getInputImpl
3938

4039
-- | Writes info message to user log
4140
foreign import info :: String -> Effect Unit
@@ -45,4 +44,4 @@ foreign import info :: String -> Effect Unit
4544
foreign import setFailed :: String -> Effect Unit
4645

4746
-- | Writes warning message to user log
48-
foreign import warning :: String -> Effect Unit
47+
foreign import warning :: String -> Effect Unit

src/Actions/ToolCache.purs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import Prelude
1515

1616
import Control.Promise (Promise, toAffE)
1717
import Data.Maybe (Maybe(..))
18-
import Data.Newtype (un)
1918
import Data.Nullable (Nullable, notNull, null, toMaybe)
2019
import Data.String as String
2120
import Data.Version (Version)
@@ -24,9 +23,11 @@ import Effect (Effect)
2423
import Effect.Aff (Aff)
2524
import Effect.Uncurried (EffectFn2, EffectFn3, EffectFn4, runEffectFn2, runEffectFn3, runEffectFn4)
2625
import Node.Path (FilePath)
27-
import Setup.Data.Tool (Tool, ToolName(..))
26+
import Setup.Data.Tool (Tool)
2827
import Setup.Data.Tool as Tool
2928

29+
type ToolName = String
30+
3031
type CacheOptions =
3132
{ source :: FilePath
3233
, tool :: Tool
@@ -55,7 +56,7 @@ cacheFile { source, tool, version } = do
5556

5657
-- We use the same name for the `targetName` and the `toolName` as the tool
5758
-- name is the executable name.
58-
toAffE (runEffectFn4 cacheFileImpl source (un ToolName toolName) toolName version')
59+
toAffE (runEffectFn4 cacheFileImpl source toolName toolName version')
5960

6061
foreign import downloadToolImpl :: EffectFn2 String (Nullable String) (Promise FilePath)
6162

src/Main.purs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,13 @@ import Prelude
55
import Data.Foldable (traverse_)
66
import Effect (Effect)
77
import Effect.Aff (launchAff_)
8-
import Setup.Data.Tool (Tool(..))
9-
import Setup.DownloadTool (downloadTool)
8+
import Setup.BuildPlan (buildPlan)
9+
import Setup.Download (download)
1010

1111
main :: Effect Unit
12-
main = launchAff_ $ traverse_ downloadTool [ PureScript, Spago, Purty, Zephyr ]
12+
main = launchAff_ do
13+
-- Decide what tools to build
14+
plan <- buildPlan
15+
16+
-- Build and cache the tools
17+
traverse_ download plan

src/Setup/BuildPlan.purs

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
module Setup.BuildPlan (buildPlan, BuildPlan) where
2+
3+
import Prelude
4+
5+
import Actions.Core as Core
6+
import Affjax as AX
7+
import Affjax.ResponseFormat as RF
8+
import Data.Argonaut.Core (stringify)
9+
import Data.Argonaut.Decode (decodeJson, printJsonDecodeError, (.:))
10+
import Data.Array as Array
11+
import Data.Bifunctor (bimap)
12+
import Data.Either (Either(..), hush)
13+
import Data.Foldable (elem, fold)
14+
import Data.Int (toNumber)
15+
import Data.Maybe (Maybe(..), fromMaybe)
16+
import Data.String as String
17+
import Data.Traversable (traverse)
18+
import Data.Version (Version)
19+
import Data.Version as Version
20+
import Effect (Effect)
21+
import Effect.Aff (Aff, Error, Milliseconds(..), delay, error, throwError)
22+
import Effect.Aff.Retry (RetryPolicy, RetryPolicyM, RetryStatus(..))
23+
import Effect.Aff.Retry as Retry
24+
import Effect.Class (liftEffect)
25+
import Math (pow)
26+
import Setup.Data.Key (Key)
27+
import Setup.Data.Key as Key
28+
import Setup.Data.Tool (Tool(..))
29+
import Setup.Data.Tool as Tool
30+
import Text.Parsing.Parser (parseErrorMessage)
31+
import Text.Parsing.Parser as ParseError
32+
33+
-- | The list of tools that should be downloaded and cached by the action
34+
type BuildPlan = Array { tool :: Tool, version :: Version }
35+
36+
-- | Construct the list of tools that sholud be downloaded and cached by the action
37+
buildPlan :: Aff BuildPlan
38+
buildPlan = do
39+
let resolve' t = delay (Milliseconds 250.0) *> resolve t
40+
map Array.catMaybes $ traverse resolve' [ PureScript, Spago, Purty, Zephyr ]
41+
42+
-- Tools that are required in the toolchain
43+
required :: Tool -> Boolean
44+
required tool = elem tool [ PureScript, Spago ]
45+
46+
-- | The parsed value of an input field that specifies a version
47+
data VersionField = Latest | Exact Version
48+
49+
-- | Attempt to read the value of an input specifying a tool version
50+
getVersionField :: Key -> Effect (Maybe (Either String VersionField))
51+
getVersionField = map (map parse) <<< Core.getInput
52+
where
53+
parse = case _ of
54+
"latest" -> pure Latest
55+
value -> bimap ParseError.parseErrorMessage Exact (Version.parseVersion value)
56+
57+
-- | Resolve the exact version to provide for a tool in the environment, based
58+
-- | on the action.yml file.
59+
resolve :: Tool -> Aff (Maybe { tool :: Tool, version :: Version })
60+
resolve tool = do
61+
let key = Key.fromTool tool
62+
liftEffect (getVersionField key) >>= case _ of
63+
Nothing | required tool -> throwError $ error "No input received for required key."
64+
Nothing -> pure Nothing
65+
Just field -> map Just $ getVersion field
66+
67+
where
68+
getVersion :: Either String VersionField -> Aff { tool :: Tool, version :: Version }
69+
getVersion = case _ of
70+
Left err -> do
71+
liftEffect $ Core.setFailed $ fold [ "Unable to parse version: ", err ]
72+
throwError $ error "Unable to complete fetching version."
73+
74+
Right (Exact v) -> do
75+
liftEffect $ Core.info "Found exact version"
76+
pure { tool, version: v }
77+
78+
Right Latest -> do
79+
liftEffect $ Core.info $ fold [ "Fetching latest tag for ", Tool.name tool ]
80+
v <- fetchLatestReleaseVersion
81+
pure { tool, version: v }
82+
83+
-- Find the latest release version for a given tool. Prefers explicit releases
84+
-- as listed in GitHub releases, but for tools which don't support GitHub
85+
-- releases, falls back to the highest valid semantic version tag for the tool.
86+
fetchLatestReleaseVersion :: Aff Version
87+
fetchLatestReleaseVersion = Tool.repository tool # case tool of
88+
PureScript -> fetchFromGitHubReleases
89+
Spago -> fetchFromGitHubReleases
90+
-- Technically, Purty is hosted on Gitlab. But without an accessible way to
91+
-- fetch the latest release tag from Gitlab via an API, it seems better to fetch
92+
-- from the GitHub mirror.
93+
Purty -> fetchFromGitHubTags
94+
Zephyr -> fetchFromGitHubReleases
95+
where
96+
-- TODO: These functions really ought to be in ExceptT to avoid all the
97+
-- nested branches.
98+
fetchFromGitHubReleases repo = recover do
99+
let url = "https://api.github.com/repos/" <> repo.owner <> "/" <> repo.name <> "/releases/latest"
100+
101+
AX.get RF.json url >>= case _ of
102+
Left err -> do
103+
throwError (error $ AX.printError err)
104+
105+
Right { body } -> case (_ .: "tag_name") =<< decodeJson body of
106+
Left e -> do
107+
throwError $ error $ fold
108+
[ "Failed to decode GitHub response. This is most likely due to a timeout.\n\n"
109+
, printJsonDecodeError e
110+
, stringify body
111+
]
112+
113+
Right tagStr -> do
114+
let tag = fromMaybe tagStr (String.stripPrefix (String.Pattern "v") tagStr)
115+
case Version.parseVersion tag of
116+
Left e ->
117+
throwError $ error $ fold
118+
[ "Failed to decode tag from GitHub response: ", parseErrorMessage e ]
119+
120+
Right v ->
121+
pure v
122+
123+
-- If a tool doesn't use GitHub releases and instead only tags versions, then
124+
-- we have to fetch the tags, parse them as appropriate versions, and then sort
125+
-- them according to their semantic version to get the latest one.
126+
fetchFromGitHubTags repo = recover do
127+
let url = "https://api.github.com/repos/" <> repo.owner <> "/" <> repo.name <> "/tags"
128+
129+
AX.get RF.json url >>= case _ of
130+
Left err -> do
131+
throwError (error $ AX.printError err)
132+
133+
Right { body } -> case traverse (_ .: "name") =<< decodeJson body of
134+
Left e -> do
135+
throwError $ error $ fold
136+
[ "Failed to decode GitHub response. This is most likely due to a timeout.\n\n"
137+
, printJsonDecodeError e
138+
, stringify body
139+
]
140+
141+
Right arr -> do
142+
let
143+
tags = Array.catMaybes $ map (\t -> hush $ Version.parseVersion $ fromMaybe t $ String.stripPrefix (String.Pattern "v") t) arr
144+
sorted = Array.reverse $ Array.sort tags
145+
146+
case Array.head sorted of
147+
Nothing ->
148+
throwError $ error "Could not download latest release version."
149+
150+
Just v ->
151+
pure v
152+
153+
-- Attempt to recover from a failed request by re-attempting according to an
154+
-- exponential backoff strategy.
155+
recover :: Aff ~> Aff
156+
recover action = Retry.recovering policy checks \_ -> action
157+
where
158+
policy :: RetryPolicyM Aff
159+
policy = exponentialBackoff (Milliseconds 5000.0) <> Retry.limitRetries 3
160+
161+
checks :: Array (RetryStatus -> Error -> Aff Boolean)
162+
checks = [ \_ -> \_ -> pure true ]
163+
164+
exponentialBackoff :: Milliseconds -> RetryPolicy
165+
exponentialBackoff (Milliseconds base) =
166+
Retry.retryPolicy
167+
\(RetryStatus { iterNumber: n }) ->
168+
Just $ Milliseconds $ base * pow 3.0 (toNumber n)

src/Setup/Data/Input.purs

Lines changed: 0 additions & 30 deletions
This file was deleted.

0 commit comments

Comments
 (0)