diff --git a/.ci/all_check.py b/.ci/all_check.py new file mode 100755 index 0000000..f010b44 --- /dev/null +++ b/.ci/all_check.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 +""" +Makes sure: + * All jobs are listed in the 'all' job + * Only existing tests are listed +""" + +import sys +import yaml + +CI_PATH = '.github/workflows/ci.yaml' +ALL_TEST = 'all' + + +def main(): + ci_yaml_fp = open(CI_PATH, 'r') + ci_yaml_parsed = yaml.load(ci_yaml_fp, Loader=yaml.FullLoader) + + all_jobs = set(ci_yaml_parsed['jobs'].keys()) - {ALL_TEST} + all_needs = set(ci_yaml_parsed['jobs'][ALL_TEST]['needs']) + + if all_jobs - all_needs: + sys.exit(f'Not all jobs mentioned in {ALL_TEST}.needs: ' + f'{all_jobs - all_needs}') + + if all_needs - all_jobs: + sys.exit(f'Non-existing jobs found in {ALL_TEST}.needs: ' + f'{all_needs - all_jobs}') + + +if __name__ == '__main__': + main() diff --git a/.ci/cabal.project.local b/.ci/cabal.project.local new file mode 100644 index 0000000..f1b3d1c --- /dev/null +++ b/.ci/cabal.project.local @@ -0,0 +1,2 @@ +package docopt + ghc-options: -Werror diff --git a/.ci/cabal.project.local-lower b/.ci/cabal.project.local-lower new file mode 100644 index 0000000..5cdc9ee --- /dev/null +++ b/.ci/cabal.project.local-lower @@ -0,0 +1,4 @@ +package docopt + ghc-options: -Werror + +prefer-oldest: True diff --git a/.ci/cabal.project.local-upper b/.ci/cabal.project.local-upper new file mode 100644 index 0000000..21cb0f8 --- /dev/null +++ b/.ci/cabal.project.local-upper @@ -0,0 +1,37 @@ +package docopt + ghc-options: -Werror + +repository head.hackage.ghc.haskell.org + url: https://ghc.gitlab.haskell.org/head.hackage/ + secure: True + key-threshold: 3 + root-keys: + 7541f32a4ccca4f97aea3b22f5e593ba2c0267546016b992dfadcd2fe944e55d + 26021a13b401500c8eb2761ca95c61f2d625bfef951b939a8124ed12ecf07329 + f76d08be13e9a61a377a85e2fb63f4c5435d40f8feb3e12eb05905edb8cdea89 + +active-repositories: hackage.haskell.org, head.hackage.ghc.haskell.org + +-- HEAD.hackage standard allow-newer packages +allow-newer: + Cabal, + base, + binary, + bytestring, + deepseq, + ghc, + ghc-bignum, + ghc-prim, + integer-gmp, + template-haskell, + text, + time + +-- Boot packages +constraints: + base installed, + ghc installed, + ghc-bignum installed, + ghc-prim installed, + integer-gmp installed, + template-haskell installed diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..9128f30 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,135 @@ +name: CI +on: + push: + branches: + - main + pull_request: + +concurrency: + group: ${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + # Cabal + cabal: + name: Cabal / GHC ${{ matrix.ghc }} ${{ matrix.project-variant }} + runs-on: ubuntu-latest + strategy: + matrix: + ghc: ["8.0.2", "8.2.2", "8.4.4", "8.6.5", "8.8.4", "8.10.7", "9.0.2", "9.2.8", "9.4.7", "9.6.3"] + project-variant: [""] + include: + - ghc: 8.6.5 + project-variant: -lower + - ghc: 9.8.1 + project-variant: -upper + + fail-fast: false + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + show-progress: false + + - name: Setup Haskell + uses: haskell-actions/setup@v2 + id: setup-haskell + with: + ghc-version: ${{ matrix.ghc }} + cabal-update: false + + - name: General Setup + run: | + cp .ci/cabal.project.local${{ matrix.project-variant}} \ + cabal.project.local + # Print out some information for debugging purposes + ghc --version + cabal --version + cat cabal.project.local + + - name: Setup CI + # This generates dist-newstyle/cache/plan.json with hashes for all + # dependencies, for cache invalidation. + run: | + cabal v2-update + cabal v2-build all --enable-tests --dry-run + + - name: Restore cached dependencies + uses: actions/cache/restore@v3 + id: cache + env: + key: + ${{ runner.os }}-ghc-${{ matrix.ghc }}-cabal-${{ + steps.setup-haskell.outputs.cabal-version }}${{ + matrix.project-variant }} + with: + path: ${{ steps.setup-haskell.outputs.cabal-store }} + key: + ${{ env.key }}-${{ hashFiles('cabal.project.local', + 'dist-newstyle/cache/plan.json') }} + restore-keys: ${{ env.key }}- + + - name: Install dependencies + run: cabal v2-build all --enable-tests --only-dependencies + + # Cache dependencies already at this point, so that we do not have to + # rebuild them should the subsequent steps fail + - name: Save cached dependencies + uses: actions/cache/save@v3 + # Trying to save over an existing cache gives distracting + # "Warning: Cache save failed." since they are immutable + if: steps.cache.outputs.cache-hit != 'true' + with: + path: ${{ steps.setup-haskell.outputs.cabal-store }} + key: ${{ steps.cache.outputs.cache-primary-key }} + + - name: Build + run: cabal v2-build all + + - name: Test + run: cabal v2-test --test-show-details=direct + + # Mechanism copied from https://github.com/clash-lang/clash-compiler/ + all: + name: All jobs finished + if: ${{ !cancelled() }} + needs: + - cabal + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + show-progress: false + + - name: Check dependencies for failures + run: | + # Test all dependencies for success/failure + set -x + # True if at least one dependency succeeded + success="${{ contains(needs.*.result, 'success') }}" + # True if at least one dependency failed + fail="${{ contains(needs.*.result, 'failure') }}" + set +x + + # Test whether success/fail variables contain sane values + if [[ "${success}" != "true" && "${success}" != "false" ]]; then exit 1; fi + if [[ "${fail}" != "true" && "${fail}" != "false" ]]; then exit 1; fi + + # We want to fail if one or more dependencies fail. For safety, we introduce + # a second check: if no dependencies succeeded something weird is going on. + if [[ "${fail}" == "true" || "${success}" == "false" ]]; then + echo "One or more dependency failed, or no dependency succeeded." + exit 1 + fi + + # Currently, ubuntu-latest already has it installed, but keep it around in + # case they ever change that + # + # - name: Install dependencies + # run: | + # sudo apt-get update + # sudo apt-get -y install python3-yaml + + - name: Check that the 'all' job depends on all other jobs + run: .ci/all_check.py