---
version: '3'

vars:
  ANSIBLE_LINT_CONFIG: .config/ansible-lint.yml
  CSPELL_CONFIG: .config/cspell.json
  FLAKE8_CONFIG: .config/flake8.toml
  PRETTIERIGNORE_CONFIG: .config/prettierignore
  SHELLCHECK_EXCLUDE: SC1091
  YAMLLINT_CONFIG: .config/yamllint.yml

tasks:
  all:
    deps:
      - '{{.REPOSITORY_TYPE}}'
      - eslint
      - :lint:markdown:markdown
      - :lint:prose:prose
      - shellcheck
      - yaml

  ansible:
    deps:
      - :symlink:{{.REPOSITORY_SUBTYPE}}
      - :install:python:requirements
    desc: Lint Ansible projects using Ansible Lint
    hide: '{{ne .REPOSITORY_TYPE "ansible"}}'
    summary: |
      # Lint an Ansible project using Ansible Lint

      This task lints the project using Ansible Lint which will scan the project and report design
      patterns that can possibly be improved. It can be used on both playbooks and roles. The
      configuration for Ansible Lint is stored in the root of the project in a file titled
      `.ansible-lint`. This configuration file is shared between all of our Ansible projects
      so any changes to it need to be made to the upstream Ansible common file repository. Because of
      this, it probably makes more sense to to disable rules (when absolutely necessary) using the
      [syntax described in this link](https://ansible-lint.readthedocs.io/en/latest/rules.html#false-positives-skipping-rules).

      For more information, see [Ansible Lint's GitHub page](https://github.com/ansible-community/ansible-lint).
    log:
      error: Ansible Lint has detected possible errors!
      start: Linting project with Ansible Lint
      success: Validated project with Ansible Lint
    cmds:
      - '{{.PYTHON_HANDLE}}ansible-lint -c {{.ANSIBLE_LINT_CONFIG}}'

  any:
    deps:
      - :install:python:requirements
    desc: Generic linting of files for things like destroyed-symlinks, merge conflicts, etc.
    log:
      error: Errors were reported by the global linters for `{{.CLI_ARGS}}`
      start: Linting `{{.CLI_ARGS}}` with global linters
    cmds:
      - '{{.PYTHON_HANDLE}}blocklint --wordlist blacklist,slave,whitelist {{.CLI_ARGS}}'
      - '{{.PYTHON_HANDLE}}check-merge-conflict {{.CLI_ARGS}}'
      - '{{.PYTHON_HANDLE}}check-added-large-files {{.CLI_ARGS}}'
      - '{{.PYTHON_HANDLE}}check-symlinks {{.CLI_ARGS}}'
      - '{{.PYTHON_HANDLE}}destroyed-symlinks {{.CLI_ARGS}}'

  codeclimate:
    deps:
      - :install:software:codeclimate
      - :lint:codeclimate:load:custom-engines
    desc: Run CodeClimate with all available linters
    summary: |
      # Run CodeClimate

      CodeClimate provides a birds-eye view of all the various possible issues that there may be
      with a project by leveraging various plugins called CodeClimate engines.

      This task will run all available linters defined in `.codeclimate.yml` after substituting
      some of the standard linters with custom engines built by Megabyte Labs.

      **Example:**
      `task lint:codeclimate`

      You can optionally output the results in HTML format. To do so, pass in the name of the output
      HTML file you would like to generate.

      **Example generating HTML results:**
      `task lint:codeclimate -- codeclimate.html`
    log:
      error: '`codeclimate` has detected errors'
      start: Running `codeclimate`
      success: Successfully validated the project by running `codeclimate`
    cmds:
      - CODECLIMATE_DEBUG=1 codeclimate analyze --dev{{if .CLI_ARGS}} -f html > {{.CLI_ARGS}}{{end}}

  commit:
    deps:
      - :install:modules:local
      - :install:npm:commitlint
    summary: |
      # Lint a commit message

      This task will lint a commit message with `commitlint`. It requires that the commit message be passed as
      an argument to this task.

      **Example:**
      `task lint:commit -- 'My commit message'`
    log:
      error: Encountered error while linting commit message ({{.CLI_ARGS}})
    cmds:
      - |
        if [ ! -z '{{.CLI_ARGS}}' ] && [ '{{.CLI_ARGS}}' != '.git/MERGE_MSG' ] && [ "$(head -c12 < .git/MERGE_MSG)" != 'Merge branch' ]; then
          cp '{{.CLI_ARGS}}' '{{.CLI_ARGS}}.bak'
          TMP="$(mktemp)"
          COMMIT_MSG_NOEMOJI="$(sed 's/^..//' < '{{.CLI_ARGS}}' | xargs)"
          .config/log info 'Linting commit message (with emoji stripped)'
          echo "$COMMIT_MSG_NOEMOJI" | {{.NPX_PACKAGE}} commitlint || (mv '{{.CLI_ARGS}}.bak' '{{.CLI_ARGS}}' && exit 1)
        elif [ '{{.CLI_ARGS}}' == '.git/MERGE_MSG' ]; then
          .config/log info 'Bypassing commitlint since the message is a merge message'
        fi
    status:
      - '[[ -z "{{.CLI_ARGS}}" ]]'
    preconditions:
      - sh: '[ ! -z "{{.CLI_ARGS}}" ]'
        msg: 'A CLI argument must be passed to this task. See `task lint:commit --summary` for details.'

  docker:
    deps:
      - :install:software:docker
    desc: Lint Dockerfiles using Hadolint (requires Docker)
    hide:
      sh: '[ ! -f Dockerfile ]'
    summary: |
      # Lint Dockerfiles using Hadolint

      Hadolint is a linter for Dockerfiles. This task uses Hadolint to report warnings
      and suggestions so that the project is using best practices and is less likely to
      have errors. The task uses Docker to run an ultra-compact container that includes
      Hadolint. Installing Hadolint is done at runtime. The task scans for all files that
      are named either `Dockerfile` or `Dockerfile.j2`. On top of reporting suggestions
      for adhereing to Docker best-practices, Hadolint also leverages Shellcheck to report
      possible errors in the shell logic used by the Dockerfile.

      **If the Dockerfile is named something other than `Dockerfile` or `Dockerfile.j2`, you
      can manually run Hadolint by running:**
      `docker run -v "$PWD:/work" -w /work megabytelabs/hadolint:slim CustomDockerfileName`

      **This task will ignore files in the following directories (and possibly more):**
        * .cache        * .husky        * node_modules
        * .common       * .modules      * test
        * .git          * .task         * venv

      **Example scanning the whole project:**
      `task lint:docker`

      **Example scanning single file:**
      `task lint:docker -- CustomDockerfile`

      For more information, see [Hadolint's GitHub page](https://github.com/hadolint/hadolint).
    log:
      error: Hadolint reported errors
      start: Linting {{if .CLI_ARGS}}`{{.CLI_ARGS}}`{{else}}the project{{end}} with Hadolint
      success: Hadolint reported no errors
    cmds:
      - |
        .config/log info 'Linting {{if .CLI_ARGS}}{{.CLI_ARGS}}{{else}}all Dockerfiles{{end}} with a Hadolint Docker image'
        {{if .CLI_ARGS}}
          docker run -v "$PWD:/work" -w /work megabytelabs/hadolint:slim {{.CLI_ARGS}}
        {{else}}
          find . -type d \( {{.IGNORE_FOLDERS}} \) -prune -o -type f \( -name Dockerfile -o \
            -name Dockerfile.j2 \) -print0 | xargs -0 -r -n1 docker run -v "$PWD:/work" -w /work \
            megabytelabs/hadolint:slim
        {{end}}

  dockerfile:
    cmds:
      - task: docker

  eslint:
    deps:
      - :install:modules:local
      - :install:npm:eslint
    desc: Lint with ESLint
    summary: |
      # Lint with `eslint`

      This task will lint the project or a specific pattern of files with `eslint`. It is capable of
      linting JSON, YML, JS, TS, and HTML as long as the correct `package.json` dependencies are
      installed. By default, all projects have the libraries necessary for linting JSON, TOML, and YML.

      **Example linting entire project:**
      `task lint:eslint`

      **Example linting specific file:**
      `task lint:eslint -- my_file.ts`

      **Example linting specific pattern of files:**
      `task lint:eslint -- '**/*.js'`
    log:
      error: ESLint has detected errors that need to be addressed
      start: Linting {{if .CLI_ARGS}}{{.CLI_ARGS}}{{else}}the project{{end}} with ESLint
      success: Successfully passed the ESLint test (please still address warnings)
    cmds:
      - >
        {{.NPX_HANDLE}}eslint -c package.json --no-eslintrc --format {{.ESLINT_FORMATTER}} --cache
        --cache-location .cache/eslintcache {{if .CLI_ARGS}}{{.CLI_ARGS}}{{else}}.{{end}}

  eslint:warn-all:
    deps:
      - :install:modules:local
      - :install:npm:eslint
    desc: Convert ESLint errors into warnings by saving the modified configuration inside `package.json`
    summary: |
      # Convert ESLint Errors into Warnings

      This task will run ESLint against the project and then automatically inline
      all reported errors as warnings in `package.json`.
    log:
      error: Encountered error while converting `eslint` errors to warnings
      start: Overriding all errors reported from `eslint` as `warnings` in `package.json`
      success: Successfully overrode `eslint` errors as warnings
    cmds:
      - >
        ESLINT_TMP="$(mktemp)"

        {{.NPX_HANDLE}}eslint -c package.json --no-eslintrc --format summary --cache
        --cache-location .cache/eslintcache {{if .CLI_ARGS}}{{.CLI_ARGS}}{{else}}.{{end}} |
        tee "$ESLINT_TMP"

        while read LINE; do
          TMP="$(mktemp)";
          ESLINT_RULE="$(echo "$LINE" | grep 'errors ' | sed 's/.*rule:\ //')";
          if [ -n "$ESLINT_RULE" ] && [ "$ESLINT_RULE" != 'syntax error' ]; then
            jq --arg rule "$ESLINT_RULE" '.eslintConfig.rules[$rule] = "warn"' package.json > "$TMP";
            mv "$TMP" package.json;
          fi;
        done < "$ESLINT_TMP"

  go:
    desc: Lint Go with all available Go linters
    summary: |
      # Lint Go Files

      This task will lint Go files with the following linters:

      * [go]()

      If you would like to skip linting the whole project and instead lint an
      individual file, then you can do so by passing the file path as a CLI
      parameter like so:

      **Example linting individual file:**
      `task lint:go -- path/filename.go`
    cmds:
      - task: go:vet

  go:vet:
    deps:
      - :install:software:go
    summary: |
      # Inspect with Go Vet

      This task inspects the source code with `go vet`.

      If you need to add flags to the command, you can specify them in
      `blueprint.goVetFlags`. `goVetFlags` can also be set in upstream variable
      sources but this lint command does not force the generation of the
      `.variables.json` file. If it cannot read `goVetFlags` in the `.variables.json` file
      (perhaps because it was not generated yet), it will read `.blueprint.goVetFlags`
      from `package.json`.

      If you would like to run `go vet` on a specific path then you can pass the path
      in as a CLI argument.

      **Example of running `go vet` on a specific path:**
      `task lint:go:vet -- path/to/file.go`
    vars:
      GO_VET_FLAGS:
        sh: |
          if [ -f .variables.json ]; then
            echo " $(jq -r '.goVetFlags' .variables.json)"
          else
            echo " $(jq -r '.blueprint.goVetFlags' package.json | sed 's/null//')"
          fi
    cmds:
      - go vet{{.GO_VET_FLAGS}}{{if .CLI_ARGS}} {{.CLI_ARGS}}{{end}}

  js:
    deps:
      - :install:modules:local
      - :install:npm:eslint
    log:
      error: Errors were reported by ESLint for {{if .CLI_ARGS}}`{{.CLI_ARGS}}`{{else}}JavaScript/TypeScript files{{end}}
      start: Linting {{if .CLI_ARGS}}`{{.CLI_ARGS}}`{{else}}JavaScript/TypeScript files{{end}} with ESLint
      success: Successfully passed ESLint checks for {{if .CLI_ARGS}}`{{.CLI_ARGS}}`{{else}}JavaScript/TypeScript files{{end}}
    cmds:
      - >
        {{.NPX_HANDLE}}eslint -c package.json --no-eslintrc --format pretty --ext .js,.jsx,.ts,.tsx --cache
        --cache-location .cache/eslintcache {{if .CLI_ARGS}}{{.CLI_ARGS}}{{else}}.{{end}}

  lint-staged:
    deps:
      - :install:modules:global
      - :install:modules:local
      - :install:python:requirements
    log:
      error: Errors were encountered when validating the staged files
      start: Linting staged files
      success: The staged files appear to be good!
    cmds:
      - '{{.NPX_HANDLE}}lint-staged'

  packer:
    deps:
      - :install:software:packer
    desc: Validate the Packer templates ending with `template.json`
    hide: '{{ne .REPOSITORY_TYPE "packer"}}'
    summary: |
      # Validate Packer templates

      This task will loop through all the Packer templates ending with `template.json`
      in the root of this project and report any errors that the templates might have.
      Alternatively, you can scan a single file (see example below).

      **Example scanning for all files ending with `template.json` in the root directory:**
      `task lint:packer`

      **Example scanning single file:**
      `task lint:packer -- filename.json`

      For more information on `packer validate`, see the [Packer website](https://www.packer.io/docs/commands/validate).
    log:
      error: Error while running `packer validate`
      start: Running {{if .CLI_ARGS}}`packer validate {{.CLI_ARGS}}`{{else}}`packer validate` on all files ending with `template.json`{{end}}
      success: '`packer validate` reported no issues'
    cmds:
      - |
        {{if .CLI_ARGS}}
          .config/log info 'Validating the {{.CLI_ARGS}} Packer template'
          packer validate {{.CLI_ARGS}}
        {{else}}
          for TEMPLATE in *template.json; do
            .config/log info "Validating the $TEMPLATE Packer template"
            packer validate "$TEMPLATE"
          done
        {{end}}

  php:
    desc: Lint PHP with all available PHP linters
    summary: |
      # Lint PHP Files

      This task will lint PHP files with the following linters:

      * [go]()

      If you would like to skip linting the whole project and instead lint an
      individual file, then you can do so by passing the file path as a CLI
      parameter like so:

      **Example linting individual file:**
      `task lint:php -- path/filename.php`
    cmds:
      - task: :donothing

  prettier:
    deps:
      - :install:modules:local
      - :install:npm:prettier
    desc: Lint formatting using Prettier
    summary: |
      # Report formatting errors with Prettier

      This task will run Prettier on the project and list the possible fixes without automatically
      applying the fixes. It will report mistakes like inconsistent indent lengths, trailing spaces,
      and more. Prettier will use the configuration specified in the `package.json` file under the
      `prettier` key.

      If this command is incompatible with a file then you can add the file to the `.prettierignore`
      file.

      For more information, see [Prettier's website](https://prettier.io/).
    log:
      error: Errors were encountered by Prettier
      start: Linting {{if .CLI_ARGS}}{{.CLI_ARGS}}{{else}}the project{{end}} with Prettier
      success: Prettier successfully finished
    cmds:
      - '{{.NPX_HANDLE}}prettier --ignore-path {{.PRETTIERIGNORE_CONFIG}} --list-different {{if .CLI_ARGS}}{{.CLI_ARGS}}{{else}}.{{end}}'

  python:
    deps:
      - :install:python:requirements
    label: lint:python
    desc: Lint Python files using Flake8
    hide: '{{ne .REPOSITORY_TYPE "python"}}'
    summary: |
      # Lint Python files using Flake8

      Flake8 is a Python library that lints Python projects (or projects that may include Python files).
      It is a combination of several Python linters like PyFlakes and pycodestyle. This task will run
      `flake8` using the configuration found in the `.flake8` file in the root of this project.

      The `.flake8` file is a common file shared across many of our repositories so if changes are made to
      it then the changes need to be made to the appropriate common file repository [here](https://gitlab.com/megabyte-labs/common).
      Because of this, it might make more sense to add a comment to lines that you wish to be ignored by flake8.
      For instance, you can ignore rule E234 by adding a comment at the end of the line that looks like, "# noqa: E234".

      _NOTE: In order to maintain our strict quality standards, disabling Flake8 rules should only be done
      when absolutely necessary._

      **Example scanning all files:**
      `task lint:python`

      **Example scanning specific file:**
      `task lint:python -- myfile.py`

      For more information, see [Flake8's GitHub page](https://github.com/PyCQA/flake8).
    log:
      error: flake8 detected some issues
      start: Linting {{if .CLI_ARGS}}`{{.CLI_ARGS}}`{{else}}the project{{end}} with flake8
      success: flake8 validation was successful
    cmds:
      - |
        {{if .CLI_ARGS}}
          {{.PYTHON_HANDLE}}flake8 --config {{.FLAKE8_CONFIG}} {{.CLI_ARGS}}
        {{else}}
          {{.PYTHON_HANDLE}}flake8 --config {{.FLAKE8_CONFIG}} .
        {{end}}

  shell:
    cmds:
      - task: shellcheck
      - task: prettier

  shellcheck:
    deps:
      - :install:npm:shellcheck
    desc: Report possible errors in shell scripts
    summary: |
      # Report possible errors in shell scripts using Shellcheck

      Shellcheck is a tool that reports warnings and suggestions for shell (e.g. bash) scripts. This task
      can scan the project for files ending with `.sh` or `.sh.j2` and runs Shellcheck on them. Files in
      the following folders are ignored:

        * .cache        * .husky
        * .git          * node_modules
        * .husky        * slim_test

      **Example scanning all files:**
      `task lint:scripts`

      **Example scanning specific file:**
      `task lint:scripts -- myfile.sh`

      For more information, see [Shellcheck's GitHub page](https://github.com/koalaman/shellcheck).
    log:
      error: Shellcheck reported errors for {{if .CLI_ARGS}}`{{.CLI_ARGS}}`{{else}}the project{{end}}
      start: Linting {{if .CLI_ARGS}}`{{.CLI_ARGS}}`{{else}}the project{{end}} with Shellcheck
      success: Linted {{if .CLI_ARGS}}`{{.CLI_ARGS}}`{{else}}the project{{end}} with no errors from Shellcheck
    cmds:
      - |
        .config/log info 'Linting with Shellcheck'
        {{if .CLI_ARGS}}
          {{.NPX_HANDLE}}shellcheck -e {{.SHELLCHECK_EXCLUDE}} {{.CLI_ARGS}}
        {{else}}
          find . -type d \( {{.IGNORE_FOLDERS}} \) -prune -o -type f \( -name '*.sh' -o -name '*.sh.j2' \) \
          -print0 | xargs -0 -r -n1 {{.NPX_HANDLE}}shellcheck -e {{.SHELLCHECK_EXCLUDE}}
        {{end}}

  spelling:
    deps:
      - :install:npm:cspell
    desc: Checks for spelling errors in staged files
    summary: |
      # Check for spelling errors in staged files

      Use cspell to check for possible spelling errors using the configuration stored in `.config/cspell.json`.

      This task is utilized by the pre-commit hook. For more information about cspell, see the
      [cspell NPM page](https://www.npmjs.com/package/cspell).

      **Example scanning all staged files:**
      `task lint:spelling`

      **Example scanning specific file:**
      `task lint:spelling -- myfile.sh`

      **Although this task only analyzes staged files, you can manually run cspell, for example, on all JavaScript
      files by running:**
      `npx cspell '**/*.js'`
    cmds:
      - task: spelling:{{if .CLI_ARGS}}cli{{else}}staged{{end}}

  spelling:cli:
    log:
      start: Running a spell-check with `cspell`
    cmds:
      - cmd: |
          set -e
          EXIT_CODE=0
          {{.NPX_HANDLE}}cspell --no-progress --show-context --no-must-find-files --config {{.CSPELL_CONFIG}} {{.CLI_ARGS}} || EXIT_CODE=$?
          if [ "$EXIT_CODE" != '0' ]; then
            .config/log warn 'Possible spelling errors were detected. Take appropriate action before merging changes.'
          fi
        ignore_errors: true

  spelling:staged:
    deps:
      - :install:npm:cspell
      - :install:software:git
    log:
      start: Detecting possible spelling errors in staged files
    cmds:
      - cmd: |
          STAGED_FILES="$(git diff --cached --name-only)"
          if [ -n "$STAGED_FILES" ]; then
            set -e
            EXIT_CODE=0
            .config/log info 'Running a spell-check on staged files with `cspell`'
            # Must not quote $STAGED_FILES
            {{.NPX_HANDLE}}cspell --no-progress --show-context --no-must-find-files --config {{.CSPELL_CONFIG}} $STAGED_FILES || EXIT_CODE=$?
            if [ "$EXIT_CODE" != '0' ]; then
              .config/log warn 'Possible spelling errors were detected. Take appropriate action before merging changes.'
            fi
          fi
        ignore_errors: true

  toml:
    deps:
      - :install:python:requirements
    summary: |
      # Lint TOML files

      This task verifies the syntax of TOML documents using the `check-toml` module from the
      Python `pre-commit-hook` library.

      **Example scanning all '**/*.toml' files:**
      `task lint:toml`

      **Example scanning specific file:**
      `task lint:toml -- myfile.toml`
    log:
      error: Possible TOML errors were detected
      start: Ensuring {{if .CLI_ARGS}}`{{.CLI_ARGS}}` is valid TOML{{else}}the project'\''s TOML files are valid{{end}}
      success: The TOML appears to be valid!
    cmds:
      - '{{.PYTHON_HANDLE}}check-toml {{if .CLI_ARGS}}{{.CLI_ARGS}}{{else}}.{{end}}'

  vagrant:
    deps:
      - :install:software:vagrant
    desc: Validate the Vagrantfile
    hide:
      sh: '[ ! -f Vagrantfile ]'
    summary: |
      # Validate the Vagrantfile

      This task is an alias for `vagrant validate`. Vagrant's `validate` command
      will ensure the Vagrantfile in the root of this repository has no errors and
      is using valid syntax.

      For more information on `vagrant validate`, see the [Vagrant website](https://www.vagrantup.com/docs/cli/validate).
    log:
      error: '`vagrant validate` detected one or more issues with the Vagrantfile'
      start: Validating the Vagrantfile
      success: The Vagrantfile passed `vagrant validate`
    cmds:
      - vagrant validate

  xml:
    deps:
      - :install:python:requirements
    summary: |
      # Lint XML files

      This task verifies the syntax of XML documents using the `check-xml` module from
      the Python `pre-commit-hook` library.

      **Example scanning all '**/*.xml' files:**
      `task lint:xml`

      **Example scanning specific file:**
      `task lint:xml -- myfile.xml`
    log:
      error: Possible issues were detected with the XML{{if .CLI_ARGS}}(`{{.CLI_ARGS}}`){{end}}
      start: Ensuring {{if .CLI_ARGS}}{{.CLI_ARGS}} is valid XML{{else}}the project contains only valid XML{{end}}
      success: The XML appears to be valid
    cmds:
      - '{{.PYTHON_HANDLE}}check-xml {{if .CLI_ARGS}}{{.CLI_ARGS}}{{else}}.{{end}}'

  yaml:
    deps:
      - :install:python:requirements
    desc: Lint `.yml` files using YAML Lint
    summary: |
      # Lint YML files using YAML Lint

      YAML Lint is a general purpose linter tool that reports suggestions for `.yml`
      files. It checks for syntax validity as well as cosmetic problems like line
      lengths, trailing spaces, and indentation. The configuration file is in the root
      of the repository in the file named `.yamllint`. The `.yamllint` file is shared
      across all of our projects so if you need to make changes to get rid of warnings
      it will generally make more sense to disable YAML Lint for a single line using the
      [method described here](https://github.com/adrienverge/yamllint#features).

      _NOTE: Disabling YAML Lint rules should only be done when absolutely necessary._

      **Example scanning all '**/*.yml' files:**
      `task lint:yaml`

      **Example scanning specific file:**
      `task lint:yaml -- myfile.sh`

      For more information, see the [YAML Lint GitHub page](https://github.com/adrienverge/yamllint).
    log:
      error: Possible issues were detected by `yamllint`
      start: Linting {{if .CLI_ARGS}}{{.CLI_ARGS}}{{else}}the project{{end}} with `yamllint`
      success: No issues were detected by `yamllint`
    cmds:
      - '{{.PYTHON_HANDLE}}yamllint -c {{.YAMLLINT_CONFIG}} -s {{if .CLI_ARGS}}{{.CLI_ARGS}}{{else}}.{{end}}'
