diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml new file mode 100644 index 0000000..a7b3a52 --- /dev/null +++ b/.github/workflows/lint.yaml @@ -0,0 +1,28 @@ +name: ๐Ÿงน Lint + +on: + pull_request: + types: [opened, synchronize, reopened] + +jobs: + lint: + name: Lint codebase + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Lint Dockerfile + uses: hadolint/hadolint-action@v3.1.0 + + - name: Lint GitHub workflows + run: | + bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash) + ./actionlint -color + shell: bash + + - name: Lint Markdown + uses: DavidAnson/markdownlint-cli2-action@v11 + with: + config: .markdownlint.yaml diff --git a/.gitignore b/.gitignore index 53b34b9..4c49bd7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1 @@ -node_modules/ .env -package*.json \ No newline at end of file diff --git a/.hadolint.yaml b/.hadolint.yaml new file mode 100644 index 0000000..15833ec --- /dev/null +++ b/.hadolint.yaml @@ -0,0 +1,3 @@ +ignored: + # Disable pin versions in apt get install, as versions can disappear from the registry + - "DL3008" \ No newline at end of file diff --git a/.markdownlint.yaml b/.markdownlint.yaml new file mode 100644 index 0000000..fcee779 --- /dev/null +++ b/.markdownlint.yaml @@ -0,0 +1,10 @@ +# Default state for all rules +default: true +# Line length +MD013: + code_blocks: false +# Inline HTML +MD033: + allowed_elements: ["a", "img", "p"] +# First line in a file should be a top-level heading +MD041: false \ No newline at end of file diff --git a/.nvmrc b/.nvmrc deleted file mode 100644 index 25bf17f..0000000 --- a/.nvmrc +++ /dev/null @@ -1 +0,0 @@ -18 \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 1588f29..cc3aaac 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,33 +1,16 @@ -# use a Node.js image with Chrome installed, because @fabernovel/heart-lighthouse requires an external Chrome installation. -# use the latest Node.js LTS instead of Node.js 14 (which is the version supported by Heart) because of -# an unresolvable bug with node-gyp during installation with Node.js 14 / NPM 6. -FROM timbru31/node-chrome:18 +FROM fabernovel/heart:v4.0.0 -# weird missing library with @fabernovel/heart-greenit (maybe because of an outdated Chromium version): -# Error: Failed to launch the browser process! -# /node_modules/puppeteer/.local-chromium/linux-818858/chrome-linux/chrome: error while loading shared libraries: libX11-xcb.so.1: cannot open shared object file: No such file or directory -RUN apt-get update && \ - apt-get -yq install libx11-xcb1 - -# set environment variable to make the @fabernovel/heart-lighthouse module work -ENV CHROME_PATH=/usr/bin/google-chrome-stable +# set bash as the default shell +SHELL ["/bin/bash", "-c"] -# lighter setup that could only work when @fabernovel/heart-lighthouse will rely on an "internal" browser (probably v4). -# by using pupeeter/chromium like @fabernovel/heart-greenit already does, there will be no need to install an external version of Chrome. -# -# FROM node:18 -# -# https://github.com/puppeteer/puppeteer/blob/main/docs/troubleshooting.md#chrome-headless-doesnt-launch-on-unix -# RUN apt-get update && \ -# apt-get -yq install ca-certificates fonts-liberation libasound2 libatk-bridge2.0-0 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgbm1 libgcc1 libglib2.0-0 libgtk-3-0 libnspr4 libnss3 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 lsb-release wget xdg-utils +# As the config can be a file on the user's repository, we need git to retrieve it +RUN apt-get update && \ + apt-get -yq --no-install-recommends install \ + ca-certificates \ + git \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* -# Uncomment the next 2 lines for local development -# WORKDIR /usr/app -# COPY ./ /usr/app COPY entrypoint.sh /entrypoint.sh ENTRYPOINT ["/entrypoint.sh"] - -# set bash as the default shell -SHELL ["/bin/bash", "-c"] - \ No newline at end of file diff --git a/README.md b/README.md index 85dfc19..762fe55 100644 --- a/README.md +++ b/README.md @@ -3,59 +3,78 @@ # Heart: evaluate webpages -Evaluate webpages directly from your CI with [Google Lighthouse](https://pagespeed.web.dev/), GreenIT, [Mozilla Observatory](https://observatory.mozilla.org/) or [SSLLabs Server](https://www.ssllabs.com/ssltest/). +Evaluate webpages directly from your CI with [Google Lighthouse](https://pagespeed.web.dev/), +GreenIT, +[Mozilla Observatory](https://observatory.mozilla.org/) +or [SSLLabs Server](https://www.ssllabs.com/ssltest/). -Retrieve the evaluations in a [Slack](https://slack.com/) channel or in a [Google Bigquery](https://cloud.google.com/bigquery]) database. +Retrieve the evaluations in a [MySQL](https://www.mysql.com/) database +or in a [Slack](https://slack.com/) channel. This GitHub Action make use of the CLI tool [Heart](https://heart.fabernovel.com). ## Usage ```yaml -- uses: faberNovel/heart-action@v1 +- uses: faberNovel/heart-action@v4 with: # [Required] # Service name that analyze the URL. - # Available values: greenit,lighthouse,observatory,ssllabs-server - analysis_service: observatory + # Available values: greenit, lighthouse, observatory, ssllabs-server. + analysis_service: lighthouse # [Required] - # Set the JSON configuration used by the analysis service, either with a file path OR an inline string. + # Configuration used by the analysis service. File path to a JSON file or JSON-inlined string. # The configuration format depends of each service, and is detailed in the READMEs of Heart: https://github.com/faberNovel/heart/tree/master/modules - # Example for the Mozilla Observatory service: https://github.com/faberNovel/heart/tree/master/modules/heart-observatory - file: conf/observatory.json - inline: '{"host":"heart.fabernovel.com"}' + # Example for the Google Lighthouse service: https://github.com/faberNovel/heart/tree/master/modules/lighthouse. + config: conf/lighthouse.json OR '{"url":"heart.fabernovel.com"}' # [Optional] # Check if the score of the result reaches the given threshold (between 0 and 100). threshold: 80 # [Optional] - # Services names that process the result of the analyze, separated by commas. - # Available values: bigquery,slack - listener_services: slack + # Comma-separated list of listener services that will not be triggered once the analysis is done. + # This parameter is mutually exclusive with the listener_services_only one. + # Available values: mysql, slack. + listener_services_except: slack # [Optional] - # Only required if you use "bigquery" as a listener_services - google_application_credentials: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }} + # Comma-separated list of listener services that will be triggered once the analysis is done. + # This parameter is mutually exclusive with the listener_services_except one. + # Available values: mysql, slack. + listener_services_only: mysql,slack # [Optional] - # If you use your own instance of Mozilla Observatory, use this setting to set the server API URL. + # Only required if you you use "mysql" as a listener_services_except or listener_services_only. + # Location and credentials of your MySQL database, in a URL format. + mysql_database_url: login:password@127.0.0.1:3306 + + # [Optional] + # Only required if you you use "observatory" as analysis_service. + # Location of the Observatory API. # See https://github.com/mozilla/http-observatory#creating-a-local-installation-tested-on-ubuntu-15 observatory_api_url: https://http-observatory.security.mozilla.org/api/v1/ # [Optional] - # If you use your own instance of Mozilla Observatory, use this setting to set the website URL. + # Only required if you you use "observatory" as analysis_service. + # Location of the Observatory website to view the the results. # See https://github.com/mozilla/http-observatory#creating-a-local-installation-tested-on-ubuntu-15 observatory_analyze_url: https://observatory.mozilla.org/analyze/ # [Optional] - # Only required if you you use "slack" as a listener_services - slack_api_token: ${{ secrets.SLACK_API_TOKEN }} + # Only required if you you use "slack" as a listener_services_except or listener_services_only. + # Slack access token. + slack_access_token: ${{ secrets.SLACK_ACCESS_TOKEN }} + + # [Optional] + # Only required if you you use "slack" as a listener_services_except or listener_services_only. + # Slack channel where the analysis results will be send. + slack_channel_id: 'heart' # [Optional] - # Customize the Slack channel where the notifications are send (default: #heart) - slack_channel_id: '#heart-analysis' + # Display additional information when running Heart + verbose: false ``` ## Examples @@ -74,12 +93,12 @@ jobs: name: ๐Ÿ”ฌ Analyse heart.fabernovel.com with Mozilla Observatory steps: - - uses: faberNovel/heart-action@v1 + - uses: faberNovel/heart-action@v4 with: analysis_service: observatory - inline: '{"host":"heart.fabernovel.com"}' + config: '{"host":"heart.fabernovel.com"}' listener_services: slack - slack_api_token: ${{ secrets.SLACK_API_TOKEN }} + slack_access_token: ${{ secrets.SLACK_ACCESS_TOKEN }} ``` @@ -109,12 +128,12 @@ jobs: ] steps: - - uses: faberNovel/heart-action@v1 + - uses: faberNovel/heart-action@v4 with: analysis_service: lighthouse - file: ${{ matrix.lighthouse_configuration }} + config: ${{ matrix.lighthouse_configuration }} listener_services: slack - slack_api_token: ${{ secrets.SLACK_API_TOKEN }} + slack_access_token: ${{ secrets.SLACK_ACCESS_TOKEN }} # prevent jobs from being blocked by a previous failed job. continue-on-error: true @@ -134,24 +153,24 @@ jobs: name: ๐Ÿ”ฌ Analyze with GreenIT steps: - - uses: faberNovel/heart-action@v1 + - uses: faberNovel/heart-action@v4 with: analysis_service: greenit - file: analysis/conf/greenit.json + config: analysis/conf/greenit.json listener_services: slack - slack_api_token: ${{ secrets.SLACK_API_TOKEN }} + slack_access_token: ${{ secrets.SLACK_ACCESS_TOKEN }} lighthouse: runs-on: ubuntu-latest name: ๐Ÿ”ฌ Analyze with Google Lighthouse steps: - - uses: faberNovel/heart-action@v1 + - uses: faberNovel/heart-action@v4 with: analysis_service: lighthouse - file: analysis/conf/lighthouse.json + config: analysis/conf/lighthouse.json listener_services: slack - slack_api_token: ${{ secrets.SLACK_API_TOKEN }} + slack_access_token: ${{ secrets.SLACK_ACCESS_TOKEN }} ``` @@ -181,12 +200,12 @@ jobs: ] steps: - - uses: faberNovel/heart-action@v1 + - uses: faberNovel/heart-action@v4 with: analysis_service: greenit - file: ${{ matrix.greenit_configuration }} + config: ${{ matrix.greenit_configuration }} listener_services: slack - slack_api_token: ${{ secrets.SLACK_API_TOKEN }} + slack_access_token: ${{ secrets.SLACK_ACCESS_TOKEN }} # prevent jobs from being blocked by a previous failed job. continue-on-error: true @@ -207,12 +226,12 @@ jobs: ] steps: - - uses: faberNovel/heart-action@v1 + - uses: faberNovel/heart-action@v4 with: analysis_service: lighthouse - file: ${{ matrix.lighthouse_configuration }} + config: ${{ matrix.lighthouse_configuration }} listener_services: slack - slack_api_token: ${{ secrets.SLACK_API_TOKEN }} + slack_access_token: ${{ secrets.SLACK_ACCESS_TOKEN }} # prevent jobs from being blocked by a previous failed job. continue-on-error: true diff --git a/action.yaml b/action.yaml index 5a3cc59..c466f90 100644 --- a/action.yaml +++ b/action.yaml @@ -8,49 +8,53 @@ inputs: analysis_service: description: | Service name that analyze the URL. - Available values: dareboost, greenit, lighthouse, observatory, ssllabs-server + Available values: greenit, lighthouse, observatory, ssllabs-server. required: true default: '' - file: - description: Set the file path to the JSON configuration used by the analysis service. - required: false - default: '' - inline: - description: Set the inline string of the JSON configuration used by the analysis service. - required: false + config: + description: Configuration used by the analysis service. File path to a JSON file or JSON-inlined string. + required: true default: '' threshold: description: Check if the score of the result reaches the given threshold (between 0 and 100). required: false default: '' - listener_services: + listener_services_except: description: | - Services names that process the result of the analyze, separated by commas. - Available values: bigquery, slack + Comma-separated list (without spaces) of listener services that will not be triggered once the analysis is done. + This parameter is mutually exclusive with the listener_services_only one. + Available values: mysql, slack. required: false default: '' - dareboost_api_token: - description: Dareboost API token + listener_services_only: + description: | + Comma-separated list (without spaces) of listener services that will be triggered once the analysis is done. + This parameter is mutually exclusive with the listener_services_except one. + Available values: mysql, slack required: false default: '' - google_application_credentials: - description: Google Service account + mysql_database_url: + description: Location and credentials of your MySQL database, in a URL format. required: false default: '' observatory_api_url: - description: Location of the Observatory API + description: Location of the Observatory API. required: false default: '' observatory_analyze_url: - description: Location of the Observatory website that provides the results + description: Location of the Observatory website to view the the results. required: false default: '' - slack_api_token: - description: Slack API token + slack_access_token: + description: Slack access token. required: false default: '' slack_channel_id: - description: Slack channel where the analysis results will be send + description: Slack channel where the analysis results will be send. + required: false + default: '' + verbose: + description: Displays debug information required: false default: '' runs: @@ -58,14 +62,14 @@ runs: image: Dockerfile args: - ${{ inputs.analysis_service }} - - ${{ inputs.file }} - - ${{ inputs.inline }} + - ${{ inputs.config }} - ${{ inputs.threshold }} - - ${{ inputs.listener_services }} + - ${{ inputs.listener_services_except }} + - ${{ inputs.listener_services_only }} + - ${{ inputs.verbose }} env: - DAREBOOST_API_TOKEN: ${{ inputs.dareboost_api_token }} - GOOGLE_APPLICATION_CREDENTIALS: ${{ inputs.google_application_credentials }} - OBSERVATORY_API_URL: ${{ inputs.observatory_api_url }} - OBSERVATORY_ANALYZE_URL: ${{ inputs.observatory_analyze_url }} - SLACK_API_TOKEN: ${{ inputs.slack_api_token }} - SLACK_CHANNEL_ID: ${{ inputs.slack_channel_id }} + HEART_MYSQL_DATABASE_URL: ${{ inputs.mysql_database_url }} + HEART_OBSERVATORY_API_URL: ${{ inputs.observatory_api_url }} + HEART_OBSERVATORY_ANALYZE_URL: ${{ inputs.observatory_analyze_url }} + HEART_SLACK_ACCESS_TOKEN: ${{ inputs.slack_access_token }} + HEART_SLACK_CHANNEL_ID: ${{ inputs.slack_channel_id }} diff --git a/entrypoint.sh b/entrypoint.sh index ba17e93..7e77533 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,36 +1,16 @@ #!/bin/bash -# generate_heart_command repositoryName analysisService file inline threshold +# generate_heart_command analysisService config threshold except-listeners only-listeners verbose # Generate the heart command from the given arguments. generate_heart_command() { - local cliOptions="" + local cliOptions=" --config $2" - if [ -n "$3" ]; then cliOptions+=" --file $1/$3"; fi - if [ -n "$4" ]; then cliOptions+=" --inline $4"; fi - if [ -n "$5" ]; then cliOptions+=" --threshold $5"; fi + if [ -n "$3" ]; then cliOptions+=" --threshold $3"; fi + if [ -n "$4" ]; then cliOptions+=" --except-listeners $4"; fi + if [ -n "$5" ]; then cliOptions+=" --only-listeners $5"; fi + if [ -n "$6" ]; then cliOptions+=" --verbose"; fi - echo $2$cliOptions -} - -# generate_installable_packages analysisService listenerServices -# Generate the list of packages names to install with the version indicator, separated by a space. -generate_installable_packages() { - local packageNamePrefix="@fabernovel/heart-" - local packageMajorVersionIndicator="@^3.0.0" - local servicesToInstall="cli $1 " - - # add listener services - if [ -n "$2" ]; then - servicesToInstall+="${2//,/ }" - fi - - servicesToInstall=$(echo $servicesToInstall | xargs) - - # build the package names from the services names (add the @fabernovel/heart- prefix) - packagesToInstall=$packageNamePrefix${servicesToInstall// / $packageNamePrefix} - - # set packages version - echo ${packagesToInstall// /$packageMajorVersionIndicator }$packageMajorVersionIndicator + echo $1$cliOptions } # trim(string) @@ -40,32 +20,25 @@ trim() { echo "$(echo -e "${1}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" } -# name of the directory in which the repository will be cloned. -# this will be used in git clone command below -repositoryName="repository" # service name that analyze the URL (e.g. greenit) analysisService=$(trim $1) -# file path to the JSON configuration used by the analysis service. -file=$(trim $2) -# inline string of the JSON configuration used by the analysis service. -inline=$(trim $3) +# file path or inline string to the JSON configuration used by the analysis service. +config=$(trim $2) # check if the score of the result reaches the given threshold (between 0 and 100). -threshold=$(trim $4) +threshold=$(trim $3) # services names that process the result of the analyze, separated by commas (e.g. slack,bigquery) -listenerServices=$(trim $5) +exceptServices=$(trim $4) +onlyServices=$(trim $5) +verbose=$(trim $6) -# if the file input has been been set, we need to have the file available for heart to read it. +# clone the repository, because we need the configuration file if the provided config is a file. # checks that the repository does not already exist too. -if [ -n "$file" ] && [[ ! -f $repositoryName ]]; then - git clone "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY.git" --branch $GITHUB_REF_NAME $repositoryName -fi +git clone "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY.git" --branch $GITHUB_REF_NAME cloned_repository -# install dependencies -# create the package.json if it does not already exist, and hide the console output -if [[ ! -f "package.json" ]]; then npm init -y > /dev/null; fi -packages=$(generate_installable_packages "$analysisService" "$listenerServices") -npm install $packages +if [[ -f "cloned_repository/$config" ]]; then + config="cloned_repository/$config" +fi # run the heart command -command=$(generate_heart_command "$repositoryName" "$analysisService" "$file" "$inline" "$threshold") +command=$(generate_heart_command "$analysisService" "$config" "$threshold" "$exceptServices" "$onlyServices" "$verbose") npx heart $command