GitHub actions (#118)

* Initial stab at github actions

* Fixed docker images

* Added run

* old new

* update

* Updated

* Fixed (?)

* Added certs

* Proper safe head

* Use proper API

* Added project name

* Updated docker image

* Started reading the right comments

* Added docker image to CI

* Renamed CI thing

* Increased swap

* Fixed tests

* Fixed docker hub

* Added conditionals back in

* minor refinements
This commit is contained in:
iko 2021-08-06 11:49:00 +03:00 committed by GitHub
parent df894c0d88
commit c7159b1f76
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 442 additions and 26 deletions

View File

@ -0,0 +1,56 @@
name: Github Action Docker Image
on:
push:
branches: [master]
pull_request:
branches: [master]
jobs:
build-github-action-image:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
submodules: recursive
- name: Increase swap
run: |
free -h
sudo swapoff /mnt/swapfile
sudo fallocate -l 12G /mnt/swapfile
sudo mkswap /mnt/swapfile
sudo swapon /mnt/swapfile
free -h
- name: Install Nix
uses: cachix/install-nix-action@v12
with:
extra_nix_config: |
trusted-public-keys = hydra.iohk.io:f/Ea+s+dFdN+3Y/G+FDgSq+a5NEWhJGzdjvKNGv0/EQ= cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=
substituters = https://hydra.iohk.io https://cache.nixos.org/
- name: Login to Cachix
uses: cachix/cachix-action@v8
with:
name: octopod
signingKey: ${{ secrets.CACHIX_SIGNING_KEY }}
- name: Build Github Action Docker Image
run: |
nix-build -A compaRESTGithubAction -j auto -o image
- name: Log into Docker Hub
if: github.ref == 'refs/heads/master'
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
- name: Upload Image to Docker Hub
if: github.ref == 'refs/heads/master'
run: |
docker load -i image
docker push typeable/comparest-github-action:latest

View File

@ -1,4 +1,4 @@
name: Haskell CI
name: Linux binary
on:
push:
@ -27,7 +27,7 @@ jobs:
free -h
sudo swapoff /mnt/swapfile
sudo fallocate -l 12G /mnt/swapfile
sudo fallocate -l 13G /mnt/swapfile
sudo mkswap /mnt/swapfile
sudo swapon /mnt/swapfile

47
action.yaml Normal file
View File

@ -0,0 +1,47 @@
name: compaREST
author: Typeable
description: API reliability checker
inputs:
GITHUB_TOKEN:
description: "The GitHub access token (e.g. secrets.GITHUB_TOKEN) used to create or update the comment. This defaults to {{ github.token }}."
default: "${{ github.token }}"
required: false
repo:
description: The owner of the repo in which to post the comment.
default: "${{ github.repository }}"
required: false
pull_request:
description: The pull request in which to post the comment.
default: "${{ github.event.pull_request.number }}"
required: false
project_name:
description: The name of the project to which the API pertains.
required: true
footer:
description: A footer that can be appended to the comment.
required: false
default: ""
old:
description: The path to old specification of the API.
required: true
new:
description: The path to new specification of the API.
required: true
runs:
using: "docker"
image: "typeable/comparest-github-action:latest"
env:
GITHUB_TOKEN: "${{ inputs.GITHUB_TOKEN }}"
REPO: "${{ inputs.repo }}"
PR_NUMBER: "${{ inputs.pull_request }}"
PROJECT_NAME: "${{ inputs.project_name }}"
FOOTER: "${{ inputs.footer }}"
ROOT: "/github/workspace"
OLD: "${{ inputs.old }}"
NEW: "${{ inputs.new }}"
pre-entrypoint: "/bin/pre"
entrypoint: "/bin/run"
branding:
icon: crosshair
color: gray-dark

View File

@ -14,6 +14,7 @@ import OpenAPI.Checker.Run
import System.Exit
import System.IO
import Text.Pandoc hiding (report)
import Text.Pandoc.Builder
main :: IO ()
main = do
@ -47,7 +48,7 @@ main = do
}
(report, status) = runReport reportConfig (a, b)
case mode opts of
Just _ -> either handler pure <=< runExceptT $ write report
Just _ -> either handler pure <=< runExceptT $ write $ doc report
Nothing -> pure ()
when (signalExitCode opts) $
case status of

View File

@ -1,33 +1,71 @@
{ sources ? import ./nix/sources.nix
, haskellNix ? import sources.haskellNix { inherit system; }
, pkgs ? import haskellNix.sources.nixpkgs-2105 (haskellNix.nixpkgsArgs // {inherit system;})
, pkgs ? import haskellNix.sources.nixpkgs-2105 (haskellNix.nixpkgsArgs // { inherit system; })
, system ? builtins.currentSystem
, nix-filter ? import sources.nix-filter
}:
let
hsPkgs = pkgs.haskell-nix.stackProject {
src = ./.;
src = nix-filter {
root = ./.;
name = "compaREST";
include = [
./stack.yaml
./stack.yaml.lock
./openapi-diff.cabal
];
};
modules = [
{
dontStrip = false;
dontPatchELF = false;
enableDeadCodeElimination = true;
packages.openapi-diff.src = nix-filter {
root = ./.;
name = "compaREST-src";
include = with nix-filter; [
(./openapi-diff.cabal)
(inDirectory ./test)
(inDirectory ./src)
(inDirectory ./app)
(inDirectory ./github-action)
./awsm-css/dist/awsm.min.css
./LICENSE
];
};
}
];
};
compaREST-static = pkgs.runCommand "compaREST-static" { } ''
mkdir $out
cp ${hsPkgs.projectCross.musl64.hsPkgs.openapi-diff.components.exes.openapi-diff + "/bin/openapi-diff"} $out/compaREST
staticify = name: drv: pkgs.runCommand name { } ''
mkdir -p $out/bin
cp ${drv + "/bin"}/* $out/bin
${pkgs.nukeReferences}/bin/nuke-refs $out/compaREST
${pkgs.nukeReferences}/bin/nuke-refs $out/bin/*
'';
compaREST = pkgs.dockerTools.buildImage {
name = "compaREST";
contents = [compaREST-static];
contents = [ (staticify "compaREST-static" hsPkgs.projectCross.musl64.hsPkgs.openapi-diff.components.exes.openapi-diff) ];
config = {
Entrypoint = [ "/compaREST" ];
Entrypoint = [ "/bin/openapi-diff" ];
};
};
in compaREST
compaRESTGithubAction =
let
action = staticify "compaREST-github-action-static" hsPkgs.projectCross.musl64.hsPkgs.openapi-diff.components.exes.comparest-github-action;
wrapped = pkgs.runCommand "wrapped-compaREST-github-action" { buildInputs = [ pkgs.makeWrapper ]; } ''
makeWrapper ${action}/bin/comparest-github-action $out/bin/pre --add-flags "pre"
makeWrapper ${action}/bin/comparest-github-action $out/bin/run --add-flags "run"
'';
in
pkgs.dockerTools.buildImage {
name = "typeable/comparest-github-action";
tag = "latest";
contents = [ wrapped pkgs.cacert ];
};
in
{ inherit compaREST compaRESTGithubAction; }

View File

@ -0,0 +1,55 @@
module CompaREST.GitHub.API
( mapComment,
createOrUpdateComment,
)
where
import CompaREST.GitHub.Action.Config
import Control.Monad
import Control.Monad.Freer
import Control.Monad.Freer.GitHub
import Control.Monad.Freer.Reader
import Data.Foldable
import Data.Proxy
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Vector as V
import GitHub
import qualified GitHub as GH
findComment :: Members '[GitHub, Reader Config] effs => Eff effs (Maybe GH.IssueComment)
findComment = do
Config {..} <- ask
comments <- sendGitHub $ GH.commentsR repoOwner repoName issue GH.FetchAll
htmlComment <- getHTMLComment
let tryStripPrefix :: GH.IssueComment -> Maybe GH.IssueComment
tryStripPrefix c@GH.IssueComment {issueCommentBody = (T.stripSuffix htmlComment -> Just b)} =
Just $ c {GH.issueCommentBody = b}
tryStripPrefix _ = Nothing
pure . (V.!? 0) $ V.mapMaybe tryStripPrefix comments
mapComment :: Members '[GitHub, Reader Config] effs => (Text -> Text) -> Eff effs ()
mapComment f = do
findComment
>>= traverse_
( \comment -> do
Config {..} <- ask
htmlComment <- getHTMLComment
sendGitHub $ editCommentR repoOwner repoName (GH.mkId Proxy $ GH.issueCommentId comment) ((<> htmlComment) . f $ GH.issueCommentBody comment)
pure ()
)
createOrUpdateComment :: Members '[GitHub, Reader Config] effs => Text -> Eff effs ()
createOrUpdateComment body' = do
Config {..} <- ask
htmlComment <- getHTMLComment
let body = body' <> htmlComment
void $
findComment >>= \case
Just comment -> sendGitHub $ editCommentR repoOwner repoName (GH.mkId Proxy $ GH.issueCommentId comment) body
Nothing -> sendGitHub $ createCommentR repoOwner repoName issue body
getHTMLComment :: Member (Reader Config) effs => Eff effs Text
getHTMLComment = do
name <- asks projectName
pure $ "\n\n<!-- compaREST comment " <> name <> " -->"

View File

@ -0,0 +1,43 @@
module CompaREST.GitHub.Action.Config
( Config (..),
)
where
import Data.Proxy
import Data.Text (Text)
import qualified Data.Text as T
import qualified GitHub as GH
import System.Envy
data Config = Config
{ githubToken :: GH.Auth
, repoOwner :: GH.Name GH.Owner
, repoName :: GH.Name GH.Repo
, issue :: GH.IssueNumber
, projectName :: Text
, footerText :: Text
, root :: FilePath
}
instance FromEnv Config where
fromEnv _ = do
token <- GH.OAuth <$> env "GITHUB_TOKEN"
(owner, repo) <-
T.split (== '/') <$> env "REPO" >>= \case
[owner, name] -> pure (owner, name)
_ -> fail "malformed repo"
issue <- GH.IssueNumber <$> env "PR_NUMBER"
projectName <- env "PROJECT_NAME"
footerText <- env "FOOTER"
root <- envMaybe "ROOT" .!= "."
pure $
Config
{ githubToken = token
, repoOwner = GH.mkName Proxy owner
, repoName = GH.mkName Proxy repo
, issue = issue
, projectName = projectName
, footerText = footerText
, root = root
}

View File

@ -0,0 +1,27 @@
module Control.Monad.Freer.GitHub
( GitHub (..),
runGitHub,
sendGitHub,
)
where
import Control.Monad.Freer
import Control.Monad.Freer.Error
import Control.Monad.IO.Class
import Data.Aeson
import GitHub hiding (Error)
import qualified GitHub as GH
data GitHub r where
SendGHRequest :: FromJSON x => Request 'RW x -> GitHub x
runGitHub :: (Member (Error GH.Error) effs, MonadIO (Eff effs)) => Auth -> Eff (GitHub ': effs) ~> Eff effs
runGitHub auth =
interpret
( \(SendGHRequest req) ->
liftIO (executeRequest auth req)
>>= either throwError pure
)
sendGitHub :: (FromJSON x, Member GitHub effs) => Request 'RW x -> Eff effs x
sendGitHub req = send $ SendGHRequest req

87
github-action/Main.hs Normal file
View File

@ -0,0 +1,87 @@
module Main (main) where
import CompaREST.GitHub.API
import CompaREST.GitHub.Action.Config
import Control.Exception
import Control.Monad.Freer
import Control.Monad.Freer.Error
import Control.Monad.Freer.GitHub
import Control.Monad.Freer.Reader
import Data.Text (Text)
import qualified Data.Yaml.Aeson as Yaml
import qualified GitHub as GH
import OpenAPI.Checker.Run
import System.Environment
import System.Envy (decodeEnv)
import System.FilePath ((</>))
import Text.Pandoc (runPure)
import Text.Pandoc.Builder
import Text.Pandoc.Options
import Text.Pandoc.Writers
main :: IO ()
main = do
cfg <- decodeEnv >>= either error pure
getArgs >>= \case
["pre"] -> runPre cfg
["run"] -> do
oldFile <- getEnv "OLD"
newFile <- getEnv "NEW"
runRun cfg (root cfg </> oldFile) (root cfg </> newFile)
_ -> error "Invalid arguments."
runner :: Config -> Eff '[GitHub, Error GH.Error, Reader Config, IO] a -> IO a
runner cfg =
runM @IO . runReader cfg
. flip (handleError @GH.Error) (error . displayException)
. runGitHub (githubToken cfg)
runPre :: Config -> IO ()
runPre cfg =
runner cfg $
mapComment
(markdown (header 4 "⏳ Report might not be accurate. Attempting to update." <> horizontalRule) <>)
runRun :: Config -> FilePath -> FilePath -> IO ()
runRun cfg old' new' = runner cfg $ do
old <- Yaml.decodeFileThrow old'
new <- Yaml.decodeFileThrow new'
let reportConfig =
ReportConfig
{ treeStyle = FoldingBlockquotesTreeStyle
, reportMode = All
}
(report, status) = runReport reportConfig (old, new)
summaryDetail s d =
rawHtml "<details>"
<> rawHtml "<summary>"
<> s
<> rawHtml "</summary>"
<> d
<> rawHtml "</details>"
where
rawHtml = rawBlock "html"
message =
header 3 (text $ "⛄ compaREST " <> projectName cfg)
<> if old == new
then header 1 "✅ The API did not change"
else
header
1
( case status of
BreakingChanges -> "⚠️ Breaking changes found!"
NoBreakingChanges -> "No breaking changes found ✨"
OnlyUnsupportedChanges -> "🤷 Couldn't determine compatibility"
)
<> summaryDetail (plain " Details") report
messageBody = markdown message <> "\n\n" <> footerText cfg
createOrUpdateComment messageBody
markdown :: Blocks -> Text
markdown =
either (error . displayException) id
. runPure
. writeHtml5String def
. doc

View File

@ -5,10 +5,10 @@
"homepage": "https://input-output-hk.github.io/haskell.nix",
"owner": "input-output-hk",
"repo": "haskell.nix",
"rev": "8c587f90d2986f87a8212c3469521db512bc7ed9",
"sha256": "0a4mz47ccvl1nl2p08zbn33i1vmak4bjr4z6wqzzflnq90hg5a0s",
"rev": "1992b910e9b4d880e1f4639f4fe7266bceb9a5ad",
"sha256": "15w3wixjnb52f7cj3s94fy0zc0l1gc94p3n2wyq3qqxj10hvirfw",
"type": "tarball",
"url": "https://github.com/input-output-hk/haskell.nix/archive/8c587f90d2986f87a8212c3469521db512bc7ed9.tar.gz",
"url": "https://github.com/input-output-hk/haskell.nix/archive/1992b910e9b4d880e1f4639f4fe7266bceb9a5ad.tar.gz",
"url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
},
"niv": {
@ -22,5 +22,17 @@
"type": "tarball",
"url": "https://github.com/nmattia/niv/archive/e0ca65c81a2d7a4d82a189f1e23a48d59ad42070.tar.gz",
"url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
},
"nix-filter": {
"branch": "master",
"description": "a small self-container source filtering lib",
"homepage": "",
"owner": "numtide",
"repo": "nix-filter",
"rev": "3c9e33ed627e009428197b07216613206f06ed80",
"sha256": "19w142crrkywxynmyw4rhz4nglrg64yjawfkw3j91qwkwbfjds84",
"type": "tarball",
"url": "https://github.com/numtide/nix-filter/archive/3c9e33ed627e009428197b07216613206f06ed80.tar.gz",
"url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
}
}

View File

@ -38,7 +38,6 @@ common common-options
-fconstraint-solver-iterations=0
default-language: Haskell2010
build-depends: base >= 4.12.0.0 && < 4.16
, bytestring
, text
default-extensions: ApplicativeDo
@ -111,6 +110,7 @@ library
, file-embed
, data-default
, ordered-containers
, bytestring
hs-source-dirs: src
exposed-modules: Data.HList
, OpenAPI.Checker.Behavior
@ -167,6 +167,7 @@ executable openapi-diff
, aeson
, containers
, doctemplates
, pandoc-types
ghc-options: -threaded
-rtsopts
-with-rtsopts=-N
@ -174,6 +175,29 @@ executable openapi-diff
FormatHeuristic
OpenAPI.Checker.Options
executable comparest-github-action
import: common-options
hs-source-dirs: github-action
main-is: Main.hs
build-depends:
openapi-diff
, pandoc
, yaml
, aeson
, github
, freer-simple
, vector
, pandoc-types
, envy
, filepath
ghc-options: -threaded
-rtsopts
-with-rtsopts=-N
other-modules:
Control.Monad.Freer.GitHub
CompaREST.GitHub.API
CompaREST.GitHub.Action.Config
test-suite openapi-diff-test
import: common-options
type: exitcode-stdio-1.0
@ -193,6 +217,7 @@ test-suite openapi-diff-test
, pandoc
, data-default
, lens
, pandoc-types
ghc-options: -threaded
-rtsopts
-with-rtsopts=-N

View File

@ -104,7 +104,7 @@ data ReportTreeStyle = HeadersTreeStyle | FoldingBlockquotesTreeStyle
twoRowTable :: [(Inlines, Inlines)] -> Blocks
twoRowTable x = simpleTable (para . fst <$> x) [para . snd <$> x]
generateReport :: ReportConfig -> ReportInput -> (Pandoc, ReportStatus)
generateReport :: ReportConfig -> ReportInput -> (Blocks, ReportStatus)
generateReport cfg inp =
let
schemaIssuesPresent = not $ P.null $ schemaIssues inp
@ -116,7 +116,6 @@ generateReport cfg inp =
OnlyErrors -> False
builder = buildReport cfg
report =
doc $
header 1 "Summary"
<> twoRowTable
(when'

View File

@ -11,6 +11,7 @@ import OpenAPI.Checker.Paths
import OpenAPI.Checker.Report
import OpenAPI.Checker.Subtree
import OpenAPI.Checker.Validate.OpenApi ()
import Text.Pandoc.Builder
runChecker :: (OpenApi, OpenApi) -> CheckerOutput
runChecker (client, server) =
@ -26,5 +27,5 @@ runChecker (client, server) =
}
run p c = either id mempty . runCompatFormula . checkCompatibility Root HNil $ toPC p c
runReport :: ReportConfig -> (OpenApi, OpenApi) -> (Pandoc, ReportStatus)
runReport :: ReportConfig -> (OpenApi, OpenApi) -> (Blocks, ReportStatus)
runReport cfg = generateReport cfg . segregateIssues . runChecker

View File

@ -1,8 +1,11 @@
resolver: lts-18.2
resolver: lts-18.4
extra-deps:
- open-union-0.4.0.0
- type-fun-0.1.3
- github-0.26
- base16-bytestring-0.1.1.7
- http-link-header-1.0.3.1
apply-ghc-options: everything
rebuild-ghc-options: true

View File

@ -18,9 +18,30 @@ packages:
sha256: b977d42525f0b4223959918d18e7905085283e937493c395077b608cd19b42c1
original:
hackage: type-fun-0.1.3
- completed:
hackage: github-0.26@sha256:a9d4046325c3eb28cdc7bef2c3f5bb213328caeae0b7dce6f51de655f0bffaa1,7162
pantry-tree:
size: 7511
sha256: b71aab2984b268030c9e2617043575681134c1fe60dffbd5596e659c0a3e9aec
original:
hackage: github-0.26
- completed:
hackage: base16-bytestring-0.1.1.7@sha256:0021256a9628971c08da95cb8f4d0d72192f3bb8a7b30b55c080562d17c43dd3,2231
pantry-tree:
size: 454
sha256: 2dc99cd67293b09204098202d0bdcdcf89d2b8c69ce6c8f4e31deadbdab2a7f9
original:
hackage: base16-bytestring-0.1.1.7
- completed:
hackage: http-link-header-1.0.3.1@sha256:9917c26635a46eff5d36c5344a4ae42fad2b5de79c0e300363bddd5eec66facd,1937
pantry-tree:
size: 990
sha256: 155feaf856daf5b55491048c8c4726ba765a8fe79263ebdbfc82f6c385a15715
original:
hackage: http-link-header-1.0.3.1
snapshots:
- completed:
size: 585392
url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/18/2.yaml
sha256: 7abb45c0cc5eb349448b66d8753655542d45d387ad26970419282eab3d860724
original: lts-18.2
size: 585817
url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/18/4.yaml
sha256: ea3a318eafa9e9cc56bfbe46099fd0d54d32641ab7bbe1d182ed8f5de39f804c
original: lts-18.4

View File

@ -1,5 +1,5 @@
module Spec.Golden.TraceTree
( tests
( tests,
)
where
@ -14,6 +14,7 @@ import OpenAPI.Checker.Run
import OpenAPI.Checker.Validate.OpenApi ()
import Spec.Golden.Extra
import Test.Tasty (TestTree, testGroup)
import Text.Pandoc.Builder
import Text.Pandoc.Class
import Text.Pandoc.Options
import Text.Pandoc.Writers
@ -37,7 +38,7 @@ tests = do
"report.md"
("a.yaml", "b.yaml")
Yaml.decodeFileThrow
(runPandoc . writeMarkdown def {writerExtensions = githubMarkdownExtensions} . fst . runReport def)
(runPandoc . writeMarkdown def {writerExtensions = githubMarkdownExtensions} . doc . fst . runReport def)
return $ testGroup "Golden tests" [traceTreeTests, reportTests]