packages feed

summoner-2.2.0.0: src/Summoner/Template/GitHub.hs

{-# LANGUAGE ViewPatterns #-}

{- |
Module                  : Summoner.Template.GitHub
Copyright               : (c) 2017-2026 Kowainik
SPDX-License-Identifier : MPL-2.0
Maintainer              : Kowainik <xrom.xkov@gmail.com>
Stability               : Stable
Portability             : Portable

This module contains template for GitHub related docs:

 * @.gitignore@ — static file with all Haskell related ignored files.
 * @.github/workflows/ci.yml@ — GitHub acttions for Cabal projects.
 * @appveyor.yml@ — Appveyor CI for Cabal or Stack projects.
 * @.travis.yml@ — depending on the build tool and supported GHC versions
   builds the Travis matrix with all necessary checks, including HLint check.
   __NOTE:__ Old GHC versions are included into @allow_failure@ Travis matrix
   section for Stack due to Stack limitations with the Cabal version usage on
   each GHC. See this issue to track the problem:

    + https://github.com/commercialhaskell/stack/issues/4488
-}

module Summoner.Template.GitHub
    ( gitHubFiles
    ) where

import Colourista (indent)
import Data.List (delete, intersect)

import Summoner.Default (defaultCabal, defaultGHC, defaultStack, ghcActionsCacheVersion,
                         ghcActionsCheckoutVersion, ghcActionsSetupHaskellVersion)
import Summoner.GhcVer (GhcVer (..), oldGhcs, showGhcVer)
import Summoner.Settings (Settings (..))
import Summoner.Text (quote)
import Summoner.Tree (TreeFs (..))


gitHubFiles :: Settings -> [TreeFs]
gitHubFiles Settings{..} = concat
    [ [File ".gitignore" (gitignoreDefault <> gitignoreCustom) | settingsGitHub]
    , [ Dir ".github"
          [ Dir "workflows" [ File "ci.yml" ghActionsYml ]
          , File "dependabot.yml" dependabotYml
          ]
      | settingsGhActions
      ]
    , [File ".travis.yml" travisYml    | settingsTravis]
    , [File "appveyor.yml" appVeyorYml | settingsAppVeyor]
    ]
  where
    -- default .gitignore template
    gitignoreDefault :: Text
    gitignoreDefault = unlines
        [ "### Haskell"
        , "dist"
        , "dist-*"
        , "cabal-dev"
        , "*.o"
        , "*.hi"
        , "*.chi"
        , "*.chs.h"
        , "*.dyn_o"
        , "*.dyn_hi"
        , "*.prof"
        , "*.aux"
        , "*.hp"
        , "*.eventlog"
        , ".virtualenv"
        , ".hsenv"
        , ".hpc"
        , ".cabal-sandbox/"
        , "cabal.sandbox.config"
        , "cabal.config"
        , "cabal.project.local"
        , ".ghc.environment.*"
        , ".HTF/"
        , ".hie/"
        , "# Stack"
        , ".stack-work/"
        , "stack.yaml.lock"
        , ""
        , "### IDE/support"
        , "# Vim"
        , "[._]*.s[a-v][a-z]"
        , "[._]*.sw[a-p]"
        , "[._]s[a-v][a-z]"
        , "[._]sw[a-p]"
        , "*~"
        , "tags"
        , ""
        , "# IntellijIDEA"
        , ".idea/"
        , ".ideaHaskellLib/"
        , "*.iml"
        , ""
        , "# Atom"
        , ".haskell-ghc-mod.json"
        , ""
        , "# VS"
        , ".vscode/"
        , ""
        , "# Emacs"
        , "*#"
        , ".dir-locals.el"
        , "TAGS"
        , ""
        , "# other"
        , ".DS_Store"
        ]

    -- additional .gitignore
    gitignoreCustom :: Text
    gitignoreCustom = if null settingsGitignore
        then ""
        else unlines ("\n# User specific" : settingsGitignore)

    ghActionsYml :: Text
    ghActionsYml = unlines $
        [ "name: CI"
        , ""
        , "on:"
        , "  pull_request:"
        , "    types: [synchronize, opened, reopened]"
        , "  push:"
        , "    branches: [" <> settingsBranchName <> "]"
        , "  schedule:"
        , "    # additionally run once per week (At 00:00 on Sunday) to maintain cache"
        , "    - cron: '0 0 * * 0'"
        , ""
        , "jobs:"

        ]
        <> memptyIfFalse settingsCabal ghActionsCabal
        <> memptyIfFalse (settingsCabal && settingsStack) [""]
        <> memptyIfFalse settingsStack ghActionsStack

    ghActionsCabal :: [Text]
    ghActionsCabal =
        [ "  cabal:"
        , "    name: ${{ matrix.os }} / ghc ${{ matrix.ghc }}"
        , "    runs-on: ${{ matrix.os }}"
        , "    strategy:"
        , "      matrix:"
        , "        os: [ubuntu-latest, macOS-latest, windows-latest]"
        , "        cabal: [" <> quote defaultCabal <> "]"
        , "        ghc:"
        ]
        <> map (indent 10 <>) ghActionsVersions
        <> ghActionsExcludes
        <>
        [ ""
        , "    steps:"
        , "    - uses: actions/checkout" <> ghcActionsCheckoutVersion
        , ""
        , "    - uses: haskell-actions/setup" <> ghcActionsSetupHaskellVersion
        , "      id: setup-haskell-cabal"
        , "      name: Setup Haskell"
        , "      with:"
        , "        ghc-version: ${{ matrix.ghc }}"
        , "        cabal-version: ${{ matrix.cabal }}"
        , ""
        , "    - name: Configure"
        , "      run: |"
        , "        " <> cabalConfigure
        , ""
        , "    - name: Freeze"
        , "      run: |"
        , "        cabal freeze"
        , ""
        , "    - uses: actions/cache" <> ghcActionsCacheVersion
        , "      name: Cache ~/.cabal/store"
        , "      with:"
        , "        path: ${{ steps.setup-haskell-cabal.outputs.cabal-store }}"
        , "        key: ${{ runner.os }}-${{ matrix.ghc }}-${{ hashFiles('cabal.project.freeze') }}"
        , ""
        , "    - name: Install dependencies"
        , "      run: |"
        , "        " <> cabalBuild <> " --only-dependencies"
        , ""
        , "    - name: Build"
        , "      run: |"
        , "        " <> cabalBuild
        , ""
        , "    - name: Test"
        , "      run: |"
        , "        " <> cabalTest
        , ""
        , "    - name: Documentation"
        , "      run: |"
        , "        " <> cabalDocumentation
        , ""
        ]

    ghActionsStack :: [Text]
    ghActionsStack =
        [ "  stack:"
        , "    name: stack / ghc ${{ matrix.ghc }}"
        , "    runs-on: ubuntu-latest"
        , "    strategy:"
        , "      matrix:"
        , "        stack: [" <> quote defaultStack <> "]"
        , "        ghc:"
        ]
        <> map (indent 10 <>) ghActionsVersions
        <>
        [ ""
        , "    steps:"
        , "    - uses: actions/checkout" <> ghcActionsCheckoutVersion
        , "      if: github.event.action == 'opened' || github.event.action == 'synchronize' || github.event.ref == 'refs/heads/" <> settingsBranchName <> "'"
        , ""
        , "    - uses: haskell-actions/setup" <> ghcActionsSetupHaskellVersion
        , "      name: Setup Haskell Stack"
        , "      with:"
        , "        ghc-version: ${{ matrix.ghc }}"
        , "        stack-version: ${{ matrix.stack }}"
        , ""
        , "    - uses: actions/cache" <> ghcActionsCacheVersion
        , "      name: Cache ~/.stack"
        , "      with:"
        , "        path: ~/.stack"
        , "        key: ${{ runner.os }}-${{ matrix.ghc }}-stack"
        , ""
        , "    - name: Build"
        , "      run: |"
        , "        " <> stackBuild
        , ""
        , "    - name: Test"
        , "      run: |"
        , "        " <> stackTest
        ]

    ghActionsVersions :: [Text]
    ghActionsVersions = map
        (\ghc -> "- " <> quote (showGhcVer ghc))
        settingsTestedVersions

    ghActionsExcludes :: [Text]
    ghActionsExcludes =
        let versionsToExclude = -- all but the latest version
                drop 1 $ sortWith Down settingsTestedVersions
            osesToExclude = ["macOS-latest", "windows-latest"]
            excludes = do
                os <- osesToExclude
                version <- versionsToExclude
                [  "- os: " <> os
                 , "  ghc: " <> showGhcVer version
                 ]
        in case excludes of
            [] -> []
            xs ->
                "        exclude:"
                : map (indent 10 <>) xs

    dependabotYml :: Text
    dependabotYml = unlines
        [ "updates:"
        , "  - package-ecosystem: \"github-actions\""
        , "    directory: \"/\""
        , "    schedule:"
        , "      interval: \"daily\""
        , "    commit-message:"
        , "      prefix: \"GA\""
        , "      include: \"scope\""
        , "    labels:"
        , "      - \"CI\""
        , "      - \"dependencies\""
        ]

    -- create travis.yml template
    travisYml :: Text
    travisYml = unlines $
        [ "language: haskell"
        , ""
        , "git:"
        , "  depth: 5"
        , ""
        , "cabal: " <> quote defaultCabal
        , ""
        , "cache:"
        , "  directories:"
        ]
        <> travisCabalCache
        <> travisStackCache
        <>
        [ ""
        , "jobs:"
        , "  include:"
        ]
        <> travisCabalMtr
        <> travisStackMtr
        <> travisStackAllowFailuresMtr
        <> installAndScript
        <>
        [ ""
        , "notifications:"
        , "  email: false"
        ]

    travisCabalCache, travisStackCache :: [Text]
    travisCabalCache = memptyIfFalse settingsCabal ["  - " <> quote "$HOME/.cabal/store"]
    travisStackCache = memptyIfFalse settingsStack
        [ "  - " <> quote "$HOME/.stack"
        , "  - " <> quote "$TRAVIS_BUILD_DIR/.stack-work"
        ]

    travisCabalMtr :: [Text]
    travisCabalMtr = memptyIfFalse settingsCabal $
        map travisCabalMatrixItem settingsTestedVersions

    travisCabalMatrixItem :: GhcVer -> Text
    travisCabalMatrixItem (showGhcVer -> ghcV) = "  - ghc: " <> ghcV

    -- Due to the Stack issues with newer Cabal versions TravisCI for 'oldGhcs'
    -- can fail. Possible failure jobs are added to the @allow-failures@ section.
    travisStackMtr :: [Text]
    travisStackMtr = memptyIfFalse settingsStack $
        concatMap travisStackMatrixItem (delete defaultGHC settingsTestedVersions)
        <> travisStackMatrixDefaultItem

    travisStackAllowFailuresMtr :: [Text]
    travisStackAllowFailuresMtr = memptyIfFalse (settingsStack && not (null old)) $
        [ ""
        , "  allow_failures:"
        ]
        <> concatMap travisStackMatrixItem old
      where
        old :: [GhcVer]
        old = settingsTestedVersions `intersect` oldGhcs

    travisStackMatrixItem :: GhcVer -> [Text]
    travisStackMatrixItem (showGhcVer -> ghcV) =
        [ ""
        , "  - ghc: " <> ghcV
        , "    env: STACK_YAML=" <> quote ("$TRAVIS_BUILD_DIR/stack-" <> ghcV <> ".yaml")
        ]

    travisStackMatrixDefaultItem :: [Text]
    travisStackMatrixDefaultItem =
        [ ""
        , "  - ghc: " <> showGhcVer defaultGHC
        , "    env: STACK_YAML=" <> quote "$TRAVIS_BUILD_DIR/stack.yaml"
        ]

    installAndScript :: [Text]
    installAndScript =
        ""
        : "install:"
        : hlintCheck
        <>
        if settingsCabal
        then if settingsStack
             then installScriptBoth
             else installScriptCabal
        else installScriptStack

    installScriptBoth :: [Text]
    installScriptBoth =
        [ "  - |"
        , "    if [ -z " <> quote "$STACK_YAML" <> " ]; then"
        , "      " <> cabalUpdate
        , "      " <> cabalConfigure
        , "      " <> cabalBuild
        , "    else"
        , "      curl -sSL https://get.haskellstack.org/ | sh"
        , "      stack --version"
        , "      " <> stackBuild
        , "    fi"
        , ""
        , "script:"
        , "  - |"
        , "    if [ -z " <> quote "$STACK_YAML" <> " ]; then"
        , "      " <> cabalTest
        , "    else"
        , "      " <> stackTest
        , "    fi"
        ]

    installScriptCabal :: [Text]
    installScriptCabal =
        [ "  - " <> cabalUpdate
        , "  - " <> cabalConfigure
        , "  - " <> cabalBuild
        , ""
        , "script:"
        , "  - " <> cabalTest
        ]

    installScriptStack :: [Text]
    installScriptStack =
        [ "  - curl -sSL https://get.haskellstack.org/ | sh"
        , "  - stack --version"
        , "  - " <> stackBuild
        , ""
        , "script:"
        , "  - " <> stackTest
        ]

    cabalUpdate :: Text
    cabalUpdate = "cabal update"

    cabalConfigure :: Text
    cabalConfigure = "cabal configure --enable-tests --enable-benchmarks --enable-documentation --test-show-details=direct --write-ghc-environment-files=always"

    cabalBuild :: Text
    cabalBuild = "cabal build all"

    cabalTest :: Text
    cabalTest = "cabal test all"

    cabalDocumentation :: Text
    cabalDocumentation = "cabal haddock"

    stackBuild :: Text
    stackBuild = "stack build --system-ghc --test --bench --no-run-tests --no-run-benchmarks --ghc-options=-Werror"

    stackTest :: Text
    stackTest = "stack test --system-ghc"

    hlintCheck :: [Text]
    hlintCheck =
        [ "  # HLint check"
        , "  - curl -sSL https://raw.github.com/ndmitchell/neil/master/misc/travis.sh | sh -s -- hlint ."
        , ""
        ]

    appVeyorYml :: Text
    appVeyorYml = unlines $
        if settingsCabal
        then appVeyorYmlCabal
        else appVeyorYmlStack

    appVeyorYmlCommon :: [Text]
    appVeyorYmlCommon =
        [ "clone_depth: 5"
        , ""
        , "# Do not build feature branch with open Pull Requests"
        , "skip_branch_with_pr: true"
        , ""
        , "# build only " <> settingsBranchName <> " branch"
        , "branches:"
        , "  only:"
        , "    - " <> settingsBranchName
        , ""
        ]

    appVeyorYmlCabal :: [Text]
    appVeyorYmlCabal = let defGhc = showGhcVer defaultGHC in
        ( "clone_folder: " <> quote "c:\\\\WORK" )
        : appVeyorYmlCommon ++
        [ "platform:"
        , "  - x86_64"
        , ""
        , "cache:"
        , "  - " <> quote "C:\\\\SR"
        , "  - dist-newstyle"
        , ""
        , "environment:"
        , "  global:"
        , "    CABOPTS: --store-dir=C:\\\\SR"
        , ""
        , "  matrix:"
        , "    - GHCVER: " <> defGhc
        , ""
        , "install:"
        , "  - choco source add -n mistuke -s https://www.myget.org/F/mistuke/api/v2"
        , "  - choco install -y cabal --version 3.6.2.0"
        , "  - choco install -y ghc   --version " <> defGhc
        , "  - refreshenv"
        , ""
        , "before_build:"
        , "  - cabal --version"
        , "  - ghc   --version"
        , "  - cabal %CABOPTS% update"
        , ""
        , "build_script:"
        , "  - cabal %CABOPTS% configure --enable-tests --enable-benchmarks --enable-documentation --test-show-details=direct --write-ghc-environment-files=always"
        , "  - cabal %CABOPTS% build all"
        , "  - cabal %CABOPTS% test  all"
        ]

    -- create appveyor.yml template
    appVeyorYmlStack :: [Text]
    appVeyorYmlStack = appVeyorYmlCommon ++
        [ "environment:"
        , "  STACK_ROOT: C:\\sr"
        , "  STACK_VERSION: " <> defaultStack
        , ""
        , "  # Workaround a gnarly bug https://github.com/haskell/cabal/issues/5386"
        , "  # See: https://www.fpcomplete.com/blog/2018/06/sed-a-debugging-story"
        , "  # TODO: check if it's fixed once we switch to lst-13 and GHC 8.6"
        , "  TMP: " <> quote "c:\\\\tmp"
        , ""
        , "  matrix:"
        , "    - STACK_YAML: stack.yaml"
        , ""
        , "cache:"
        , "  - " <> quote "%STACK_ROOT% -> %STACK_YAML%, appveyor.yml"
        , "  - " <> quote ".stack-work -> %STACK_YAML%, appveyor.yml"
        , ""
        , "install:"
        , "  - choco install -y haskell-stack --version %STACK_VERSION%"
        , "  - stack setup > nul"
        , ""
        , "build_script:"
        , "  - stack build --test --bench --no-run-tests --no-run-benchmarks --ghc-options=-Werror"
        , ""
        , "test_script:"
        , "  - stack test"
        ]