Add CI runner (#5)

Reviewed-on: elland/haddock2#5
Co-authored-by: Igor Ranieri <igor@elland.me>
Co-committed-by: Igor Ranieri <igor@elland.me>
This commit is contained in:
Igor Ranieri 2025-09-27 15:03:00 +00:00 committed by elland
parent d8ba47a8b6
commit 08dc87a307
7 changed files with 228 additions and 53 deletions

View file

@ -0,0 +1,99 @@
name: Haskell CI
on:
pull_request:
branches:
- dev
- main
push:
branches:
- main
jobs:
build:
runs-on: docker
container:
image: elland/haddock2:latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Check versions
run: |
ghc --version
cabal --version
node --version
- name: Cache Cabal packages
uses: actions/cache@v4
with:
path: |
~/.cabal/packages
~/.cabal/store
dist-newstyle
key: ${{ runner.os }}-haskell-9.10-cabal-${{ hashFiles('**/*.cabal', '**/cabal.project') }}
restore-keys: |
${{ runner.os }}-haskell-9.10-cabal-
- name: Update Cabal package index
run: cabal update
- name: Configure project
run: cabal configure --enable-tests --enable-benchmarks
- name: Build dependencies
run: cabal build --only-dependencies --enable-tests --enable-benchmarks
- name: Build project
run: cabal build --enable-tests --enable-benchmarks
- name: Run documentation build
run: cabal haddock
test:
runs-on: docker
container:
image: elland/haddock2:latest
needs: build
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Cache Cabal packages
uses: actions/cache@v4
with:
path: |
~/.cabal/packages
~/.cabal/store
dist-newstyle
key: ${{ runner.os }}-haskell-9.10-cabal-${{ hashFiles('**/*.cabal', '**/cabal.project') }}
restore-keys: |
${{ runner.os }}-haskell-9.10-cabal-
- name: Update Cabal package index
run: cabal update
- name: Configure project
run: cabal configure --enable-tests --enable-benchmarks
- name: Build dependencies
run: cabal build --only-dependencies --enable-tests --enable-benchmarks
- name: Build project
run: cabal build --enable-tests --enable-benchmarks
- name: Run tests
run: cabal test --test-show-details=direct
fourmolu:
runs-on: docker
container:
image: elland/haddock2:latest
needs: build
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Run fourmolu
run: |
find src test app -name "*.hs" -exec fourmolu --check-idempotence {} \; 2>/dev/null || true
find src test app -name "*.hs" -exec fourmolu --mode check {} \;
hlint:
runs-on: docker
container:
image: elland/haddock2:latest
needs: build
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Run hlint
run: |
if [ -d src ]; then hlint src/; fi
if [ -d test ]; then hlint test/; fi
if [ -d app ]; then hlint app/; fi

36
Dockerfile Normal file
View file

@ -0,0 +1,36 @@
FROM haskell:9.10.2-bullseye AS builder
RUN apt-get update && apt-get install -y curl git && rm -rf /var/lib/apt/lists/*
RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \
apt-get install -y nodejs && \
rm -rf /var/lib/apt/lists/*
RUN cabal update && \
cabal install --install-method=copy --installdir=/usr/local/bin \
fourmolu hlint cabal-gild
WORKDIR /workspace
FROM haskell:9.10.2-bullseye
RUN apt-get update && apt-get install -y \
libgmp10 \
curl \
&& rm -rf /var/lib/apt/lists/*
RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \
apt-get install -y nodejs && \
rm -rf /var/lib/apt/lists/*
RUN cabal update
COPY --from=builder /usr/local/bin/cabal /usr/local/bin/
COPY --from=builder /usr/local/bin/fourmolu /usr/local/bin/
COPY --from=builder /usr/local/bin/hlint /usr/local/bin/
COPY --from=builder /usr/local/bin/cabal-gild /usr/local/bin/
WORKDIR /workspace
CMD [ "bash" ]

View file

@ -1,9 +1,44 @@
.PHONY: help .PHONY: help
help: ## Show this help. help: ## Show this help
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
.PHONY: format .PHONY: build
format: build: ## Build the project
cabal build
.PHONY: test
test: ## Run tests
cabal test --test-show-details=direct
.PHONY: clean
clean: ## Clean build artifacts
cabal clean
.PHONY: fourmolu
fourmolu: ## Format Haskell code
find . -type f -name "*.hs" ! -path "./dist-newstyle/*" -exec fourmolu -i {} + find . -type f -name "*.hs" ! -path "./dist-newstyle/*" -exec fourmolu -i {} +
.PHONY: fourmolu-check
fourmolu-check: ## Check if code is formatted
find . -type f -name "*.hs" ! -path "./dist-newstyle/*" -exec fourmolu --mode check {} \;
.PHONY: lint
lint: ## Run hlint
hlint src test app
.PHONY: cabal-gild
cabal-gild: ## Format cabal file
cabal-gild --io=haddock2.cabal cabal-gild --io=haddock2.cabal
.PHONY: format
format: fourmolu cabal-gild ## Run all formatters
.PHONY: check
check: fourmolu-check lint ## Run all checks (CI-style)
.PHONY: ci
ci: build test check ## Run full CI pipeline locally
.PHONY: docs
docs: ## Generate documentation
cabal haddock --haddock-hyperlink-source

View file

@ -69,7 +69,7 @@ located :: Parser a -> Parser (SourcePos, a)
located p = (,) <$> getPosition <*> p located p = (,) <$> getPosition <*> p
tokenise :: [Parser a] -> Parser [(SourcePos, a)] tokenise :: [Parser a] -> Parser [(SourcePos, a)]
tokenise = sequence . map located tokenise = mapM located
lexer :: String -> Either ParseError [LocatedToken] lexer :: String -> Either ParseError [LocatedToken]
lexer = Parsec.runParser lexText initialParserState "input" . Text.pack lexer = Parsec.runParser lexText initialParserState "input" . Text.pack
@ -146,7 +146,7 @@ delimitedNoTrailing openP closeP openTok = asList <$> delimitedAsTuple (openTok
asList (a, tok, _) = [a, tok] asList (a, tok, _) = [a, tok]
delimitedSymmetric :: Parser a -> Token -> Token -> Parser [LocatedToken] delimitedSymmetric :: Parser a -> Token -> Token -> Parser [LocatedToken]
delimitedSymmetric s t1 t2 = delimited s s t1 t2 delimitedSymmetric s = delimited s s
eol :: Parser () eol :: Parser ()
eol = void "\n" <|> void "\r\n" <|> Parsec.eof eol = void "\n" <|> void "\r\n" <|> Parsec.eof

View file

@ -13,7 +13,9 @@ import Text.Parsec.Pos (updatePosChar)
Return everything consumed except for the end pattern itself. Return everything consumed except for the end pattern itself.
-} -}
takeUntil :: Text -> Parser Text takeUntil :: Text -> Parser Text
takeUntil end_ = Text.dropEnd (Text.length end_) <$> requireEnd (scan p (False, end)) >>= gotSome takeUntil end_ =
requireEnd (scan p (False, end))
>>= gotSome . Text.dropEnd (Text.length end_)
where where
end = Text.unpack end_ end = Text.unpack end_

View file

@ -9,6 +9,8 @@ module Types (
) )
where where
import Data.Foldable (fold)
newtype Document = Document newtype Document = Document
{ meta :: Meta { meta :: Meta
} }
@ -28,6 +30,7 @@ data Since = Since
-- Could have a better type? -- Could have a better type?
type Version = [Int] type Version = [Int]
type Package = String type Package = String
data DocMarkup mod id data DocMarkup mod id
@ -51,78 +54,78 @@ data DocMarkup mod id
| -- | Bold __bold text__ | -- | Bold __bold text__
DocBold (DocMarkup mod id) DocBold (DocMarkup mod id)
| {- | Unordered lists | {- | Unordered lists
* this * this
or or
- this - this
-} -}
DocUnorderedList [DocMarkup mod id] DocUnorderedList [DocMarkup mod id]
| {- | Ordered lists | {- | Ordered lists
1. this 1. this
or or
(1) this (1) this
-} -}
DocOrderedList [(Int, DocMarkup mod id)] DocOrderedList [(Int, DocMarkup mod id)]
| {- | Definition lists | {- | Definition lists
[term] a term [term] a term
[another term] another definition [another term] another definition
-} -}
DocDefinitionList [(DocMarkup mod id, DocMarkup mod id)] DocDefinitionList [(DocMarkup mod id, DocMarkup mod id)]
| {- | Code blocks | {- | Code blocks
@ @
a code block in here a code block in here
with multiple lines with multiple lines
@ @
Or with bird tracks: Or with bird tracks:
> some code > some code
> goes here > goes here
-} -}
DocCodeBlock (DocMarkup mod id) DocCodeBlock (DocMarkup mod id)
| {- | Hyperlinks | {- | Hyperlinks
__marked__: __marked__:
<http://example.com> <http://example.com>
<http://example.com label text> <http://example.com label text>
__Auto-detected URLs__: __Auto-detected URLs__:
http://example.com http://example.com
https://example.com https://example.com
ftp://example.com ftp://example.com
__Markdown style__ __Markdown style__
[link text](http://example.com) [link text](http://example.com)
[link text]("Module.Name") [link text]("Module.Name")
-} -}
DocHyperlink (Hyperlink (DocMarkup mod id)) DocHyperlink (Hyperlink (DocMarkup mod id))
| {- | Pictures | {- | Pictures
<<image.png>> <<image.png>>
<<image.png title text>> <<image.png title text>>
__Markdown Images__ __Markdown Images__
![alt text](image.png) ![alt text](image.png)
-} -}
DocPicture Picture DocPicture Picture
| {- | Inline math expressions | {- | Inline math expressions
\(mathematical expression\) \(mathematical expression\)
-} -}
DocMathInline String DocMathInline String
| {- | Math multiline display | {- | Math multiline display
\[ \[
mathematical expression mathematical expression
in multiple lines in multiple lines
\] \]
-} -}
DocMathDisplay String DocMathDisplay String
| {- | Anchors, no spaces allowed | {- | Anchors, no spaces allowed
#anchor-name# #anchor-name#
-} -}
DocAnchor String DocAnchor String
| {- | Property descriptions | {- | Property descriptions
prop> property description prop> property description
-} -}
DocProperty String DocProperty String
| {- | Examples | {- | Examples
>>> expression >>> expression
result line 1 result line 1
result line 2 result line 2
-} -}
DocExamples [Example] DocExamples [Example]
| -- | Header | -- | Header
@ -136,7 +139,7 @@ instance Semigroup (DocMarkup mod id) where
instance Monoid (DocMarkup mod id) where instance Monoid (DocMarkup mod id) where
mempty = DocEmpty mempty = DocEmpty
mconcat = foldr (<>) mempty mconcat = fold
data ModuleLink id = ModuleLink data ModuleLink id = ModuleLink
{ name :: String { name :: String

View file

@ -1,16 +1,15 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# OPTIONS_GHC -Wno-orphans #-} {-# OPTIONS_GHC -Wno-orphans #-}
import Test.Hspec import Data.String (IsString (..))
import Data.Text (Text)
import GHC.Stack
import Identifier (Identifier) import Identifier (Identifier)
import Lexer import Lexer
import Parser import Parser
import Types import Types
import Data.String (IsString (..)) import Test.Hspec
import Data.Text (Text)
import GHC.Stack
import Text.Parsec.Pos import Text.Parsec.Pos
main :: IO () main :: IO ()
@ -34,9 +33,9 @@ main = hspec $ do
describe "Parser" do describe "Parser" do
it "Bold" do it "Bold" do
"__bold__" `shouldParseTo` (DocBold (DocString "bold")) "__bold__" `shouldParseTo` DocBold (DocString "bold")
it "Emphasis" do it "Emphasis" do
"/emphasis/" `shouldParseTo` (DocEmphasis (DocString "emphasis")) "/emphasis/" `shouldParseTo` DocEmphasis (DocString "emphasis")
------------ ------------
-- Tests -- Tests
@ -57,6 +56,7 @@ modules = do
`shouldLexTo` [ (1, 2, Module "OtherModule.Name") `shouldLexTo` [ (1, 2, Module "OtherModule.Name")
, (1, 18, Anchor "myAnchor") , (1, 18, Anchor "myAnchor")
] ]
link :: Expectation link :: Expectation
link = link =
"[link to](http://some.website)" "[link to](http://some.website)"