diff --git a/.github/workflows/phpcs.yml b/.github/workflows/phpcs.yml index a8c4a2d..a0290ee 100644 --- a/.github/workflows/phpcs.yml +++ b/.github/workflows/phpcs.yml @@ -6,6 +6,7 @@ on: - '**.php' - tools/phpcs/composer.json - phpcs.xml.dist + - .github/workflows/phpcs.yml jobs: phpcs: @@ -13,7 +14,7 @@ jobs: name: PHP_CodeSniffer steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -29,7 +30,7 @@ jobs: run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache dependencies - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('tools/phpcs/composer.json') }} diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml index 1333c1d..efc0b1d 100644 --- a/.github/workflows/phpstan.yml +++ b/.github/workflows/phpstan.yml @@ -9,18 +9,19 @@ on: - ci/composer.json - phpstan.ci.neon - phpstan.neon.dist + - .github/workflows/phpstan.yml jobs: phpstan: runs-on: ubuntu-latest strategy: matrix: - php-versions: ['7.4', '8.0', '8.3'] + php-versions: ['8.1', '8.4'] prefer: ['prefer-stable', 'prefer-lowest'] name: PHPStan with PHP ${{ matrix.php-versions }} ${{ matrix.prefer }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -35,7 +36,7 @@ jobs: run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache dependencies - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ matrix.prefer }}-${{ hashFiles('**/composer.json') }} diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index 6f91884..e06eaa1 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -8,6 +8,7 @@ on: - tools/phpunit/composer.json - phpunit.xml.dist - tests/docker-prepare.sh + - .github/workflows/phpunit.yml env: # On github CI machine creating the "/vendor" volume fails otherwise with: read-only file system: unknown @@ -18,13 +19,13 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - civicrm-image-tags: [ '5-drupal-php8.1', '5-drupal-php7.4', '5.45-drupal-php7.4' ] + civicrm-image-tags: [ 'drupal', '5.55-drupal-php8.1' ] name: PHPUnit with Docker image michaelmcandrew/civicrm:${{ matrix.civicrm-image-tags }} env: CIVICRM_IMAGE_TAG: ${{ matrix.civicrm-image-tags }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Pull images run: docker compose -f tests/docker-compose.yml pull --quiet - name: Start containers diff --git a/README.md b/README.md index c641e7e..c4e7d80 100644 --- a/README.md +++ b/README.md @@ -20,8 +20,8 @@ The extension is licensed under [AGPL-3.0](LICENSE.txt). ## Requirements -* PHP v7.4+ -* CiviCRM 5 +* PHP v8.1+ +* CiviCRM 5.55+ ## Installation (Web UI) diff --git a/ci/composer.json b/ci/composer.json index 5f8cb19..0f93a1e 100644 --- a/ci/composer.json +++ b/ci/composer.json @@ -10,7 +10,7 @@ "sort-packages": true }, "require": { - "civicrm/civicrm-core": "^5.55", - "civicrm/civicrm-packages": "^5.55" + "civicrm/civicrm-core": ">=5.55", + "civicrm/civicrm-packages": ">=5.55" } } diff --git a/composer.json b/composer.json index 37e2b09..6c755c3 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,6 @@ { "name": "systopia/activity-entity", + "description": "Connect activities with other entities", "type": "civicrm-ext", "license": "AGPL-3.0-or-later", "authors": [ @@ -13,9 +14,12 @@ "prefer-stable": true, "config": { "prepend-autoloader": false, - "sort-packages": true + "sort-packages": true, + "platform": { + } }, "require": { + "php": "^8.1", "webmozart/assert": "^1.11" }, "scripts": { @@ -40,7 +44,7 @@ "@php tools/phpcs/vendor/bin/phpcbf" ], "phpstan": [ - "@php tools/phpstan/vendor/bin/phpstan" + "@php tools/phpstan/vendor/bin/phpstan -v" ], "phpunit": [ "@php tools/phpunit/vendor/bin/simple-phpunit --coverage-text" diff --git a/info.xml b/info.xml index 2fe7c11..0aed19e 100644 --- a/info.xml +++ b/info.xml @@ -18,7 +18,7 @@ 0.1-dev dev - 5.54 + 5.55 diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 3493c8a..fb94c2a 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -21,6 +21,9 @@ parameters: - Civi\Core\Event\GenericHookEvent - CRM_Core_Config - CRM_Core_DAO + earlyTerminatingMethodCalls: + CRM_Queue_Runner: + - runAllViaWeb checkTooWideReturnTypesInProtectedAndPublicMethods: true checkUninitializedProperties: true checkMissingCallableSignature: true @@ -38,7 +41,6 @@ parameters: # Tests - # Accessing results of API requests - message: "#^Offset '[^']+' does not exist on array[^\\|]+\\|null.$#" + identifier: offsetAccess.notFound path: */tests/phpunit/Civi/**/*Test.php - - '#^Method Civi\\Fixtures\\[^\s]+Fixture::[^\s]+\(\) should return array{[^}]+} but returns array\|null.$#' tmpDir: .phpstan diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 1d1f4e8..a531297 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,6 +1,6 @@ ./tests/phpunit - - + + + CRM Civi - - CRM/*/DAO - - - + + + CRM/ActivityEntity/DAO + + + diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml index 16a4949..2a4dfc1 100644 --- a/tests/docker-compose.yml +++ b/tests/docker-compose.yml @@ -1,6 +1,6 @@ services: civicrm: - image: michaelmcandrew/civicrm:${CIVICRM_IMAGE_TAG:-5-drupal} + image: michaelmcandrew/civicrm:${CIVICRM_IMAGE_TAG:-drupal} environment: - PROJECT_NAME=test - BASE_URL=http://localhost @@ -9,6 +9,8 @@ services: - CIVICRM_DB_PASS=secret - CIVICRM_DB_HOST=mysql - CIVICRM_DB_PORT=3306 + - CIVICRM_CRED_KEYS=aes-cbc::test + - CIVICRM_SIGN_KEYS=jwt-hs256::test - CIVICRM_SITE_KEY=TEST_KEY - DRUPAL_DB_NAME=test - DRUPAL_DB_USER=root diff --git a/tests/docker-prepare.sh b/tests/docker-prepare.sh index e95c9ce..e0a5119 100755 --- a/tests/docker-prepare.sh +++ b/tests/docker-prepare.sh @@ -4,6 +4,14 @@ set -eu -o pipefail EXT_DIR=$(dirname "$(dirname "$(realpath "$0")")") EXT_NAME=$(basename "$EXT_DIR") +if ! type git >/dev/null 2>&1; then + apt -y update + apt -y install git +fi + +# Prevent this git error: The repository does not have the correct ownership and git refuses to use it +git config --global --add safe.directory "/var/www/html/sites/default/files/civicrm/ext/$EXT_NAME" + i=0 while ! mysql -h "$CIVICRM_DB_HOST" -P "$CIVICRM_DB_PORT" -u "$CIVICRM_DB_USER" --password="$CIVICRM_DB_PASS" -e 'SELECT 1;' >/dev/null 2>&1; do i=$((i+1)) diff --git a/tests/phpunit/Civi/Fixtures/ActivityFixture.php b/tests/phpunit/Civi/Fixtures/ActivityFixture.php index 6d50258..f8c66f4 100644 --- a/tests/phpunit/Civi/Fixtures/ActivityFixture.php +++ b/tests/phpunit/Civi/Fixtures/ActivityFixture.php @@ -31,10 +31,11 @@ final class ActivityFixture { * @throws \CRM_Core_Exception */ public static function addFixture(array $values = []): array { + // @phpstan-ignore return.type return Activity::create()->setValues([ 'activity_type_id' => 1, 'source_contact_id' => 1, - ] + $values)->execute()->first(); + ] + $values)->execute()->single(); } } diff --git a/tests/phpunit/Civi/Fixtures/GroupFixture.php b/tests/phpunit/Civi/Fixtures/GroupFixture.php index 55e3220..222bf22 100644 --- a/tests/phpunit/Civi/Fixtures/GroupFixture.php +++ b/tests/phpunit/Civi/Fixtures/GroupFixture.php @@ -31,9 +31,10 @@ final class GroupFixture { * @throws \CRM_Core_Exception */ public static function addFixture(array $values = []): array { + // @phpstan-ignore return.type return Group::create()->setValues([ 'title' => 'Test', - ] + $values)->execute()->first(); + ] + $values)->execute()->single(); } } diff --git a/tools/create-release.sh b/tools/create-release.sh new file mode 100755 index 0000000..29cbc35 --- /dev/null +++ b/tools/create-release.sh @@ -0,0 +1,399 @@ +#!/bin/bash +set -euo pipefail + +# shellcheck disable=SC2155 +readonly PHP=$(which "${PHP:-php}") +# shellcheck disable=SC2155 +readonly COMPOSER=$(which "${COMPOSER:-composer}") +# shellcheck disable=SC2155 +readonly JQ=$(which "${JQ:-jq}") + +if [ -z "$PHP" ]; then + echo "php not found" >&2 + exit 1 +fi + +if [ -z "$COMPOSER" ]; then + echo "composer not found" >&2 + exit 1 +fi + +if [ -z "$JQ" ]; then + echo "jq not found" >&2 + exit 1 +fi + +# shellcheck disable=SC2155 +readonly SCRIPT_NAME=$(basename "$0") + +usage() { + cat <.*' info.xml | sed 's#[[:space:]]*\(.*\)[[:space:]]*#\1#') + if [ -z "$version" ]; then + echo "Version not found in info.xml" >&2 + exit 1 + fi + + if ! [[ "$version" =~ ^([0-9]+\.[0-9]+\.[0-9]+)-dev$ ]]; then + echo "The version number $version doesn't match the form a.b.c-dev" >&2 + exit 1 + fi + + echo "${BASH_REMATCH[1]}" +} + +detectDevelStage() { + local -r version=$1 + if [[ "$version" = *alpha* ]]; then + echo alpha + elif [[ "$version" = *beta* ]]; then + echo beta + elif [[ "$version" = 0.* ]]; then + echo dev + else + echo stable + fi +} + +detectNextVersion() { + local -r version=$1 + [[ "$version" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+) ]] + + local -r major=${BASH_REMATCH[1]} + local minor=${BASH_REMATCH[2]} + local patch=${BASH_REMATCH[3]} + + if [[ "$version" = *-* ]]; then + # $version has pre-release + true + elif [ "$major" = 0 ]; then + patch=$((patch+1)) + elif [ "$patch" = 0 ]; then + minor=$((minor+1)) + else + patch=$((patch+1)) + fi + + echo "$major.$minor.$patch-dev" +} + +validateVersion() { + local -r version=$1 + if ! [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-(alpha|beta)[1-9][0-9]*)?$ ]]; then + echo "The version number $version is not a valid release version" >&2 + exit 1 + fi + + if [ -n "$(git tag -l "$version")" ]; then + echo "git tag $version already exists" >&2 + exit 1 + fi +} + +validateDevelopmentStage() { + local -r develStage=$1 + if ! [[ "$develStage" =~ ^(stable|alpha|beta|dev)$ ]]; then + echo "$develStage is not a valid development stage" >&2 + exit 1 + fi +} + +validateNextVersion() { + local -r nextVersion=$1 + if ! [[ "$nextVersion" =~ ^[0-9]+\.[0-9]+\.[0-9]+-dev$ ]]; then + echo "The version number $nextVersion is not a valid next version" >&2 + exit 1 + fi +} + +validateInfoXml() { + if [ ! -f info.xml ]; then + echo "info.xml not found in working directory" >&2 + exit 1 + fi + + # Ensure info.xml contains elements so that they can be replaced in updateInfoXml. + if ! grep -q ".*" info.xml; then + echo "version element not found in info.xml" >&2 + exit 1 + fi + + if ! grep -q ".*" info.xml; then + echo "develStage element not found in info.xml" >&2 + exit 1 + fi + + if ! grep -q -e "" -e ".*" info.xml; then + echo "releaseDate element not found in info.xml" >&2 + exit 1 + fi +} + +updateInfoXml() { + local -r version=$1 + local -r develStage=$2 + local -r releaseDate=${3:-} + + if [ -z "$releaseDate" ]; then + local -r releaseDateXml="" + else + local -r releaseDateXml="$releaseDate" + fi + + sed -i -e "s#.*#$version#g" \ + -e "s#.*#$develStage#g" \ + -e "s#.*#$releaseDateXml#g" \ + -e "s##$releaseDateXml#g" \ + info.xml +} + +hasComposerRequires() { + if [ ! -f composer.json ]; then + return 1 + fi + + # All requires that are not "php" or "ext-*". + requires=$("$JQ" -r '.require|keys|.[]' composer.json 2>/dev/null | sed -e '/^php$/d' -e '/ext-.*/d') + [ "$requires" != "" ] +} + +isVersionLesser() { + "$PHP" -r "if (version_compare('$1', '$2', '>=')) exit(1);" +} + +getMinPhpVersion() { + local phpVersion="" + local -r phpConstraint=$("$JQ" --raw-output --monochrome-output .require.php composer.json) + if [ "$phpConstraint" = "null" ]; then + echo "PHP version constraint not found in composer.json. Please consider adding it" >&2 + echo -n "Minimal supported PHP version: " >&2 + read -r phpVersion + else + local -r oldIfs=$IFS + IFS=' |' + local constraint + for constraint in $phpConstraint; do + if [[ "$constraint" =~ ^(\^|~|>=)([0-9]+(\.[0-9]+(\.[0-9]+)?)?)$ ]]; then + if [ -z "$phpVersion" ] || isVersionLesser "${BASH_REMATCH[2]}" "$phpVersion"; then + phpVersion=${BASH_REMATCH[2]} + fi + fi + done + IFS=$oldIfs + + if [ -n "$phpVersion" ]; then + echo -n "Minimal supported PHP version [$phpVersion]: " >&2 + read -r input + if [ "$input" != "" ]; then + phpVersion=$input + fi + else + echo "Minimal supported PHP version could not be detected from composer version constraint. (Supported operators: ^, ~, >=, |)" >&2 + echo -n "Minimal supported PHP version: " >&2 + read -r phpVersion + fi + fi + + echo "$phpVersion" +} + +validateMinPhpVersion() { + if ! [[ "$1" =~ ^[0-9]+(\.[0-9]+(\.[0-9]+)?)?$ ]]; then + echo "$1 is not a supported minimal PHP version" >&2 + exit 1 + fi +} + +updatePot() { + local -r potFiles=(l10n/*.pot) + if [ ${#potFiles[@]} -ge 1 ] && [ -e "${potFiles[0]}" ] && [ -x tools/update-pot.sh ]; then + echo "Update .pot file" + tools/update-pot.sh + if ! git diff --no-patch --exit-code "${potFiles[*]}"; then + echo ".pot file has changed. Please update the translation and push changes to the repository." >&2 + exit 1 + fi + fi +} + +main() { + DRY_RUN=0 + local noComposer=0 + local noPotUpdate=0 + + while [ $# -gt 0 ]; do + case $1 in + -h|--help) + usage + exit 0 + ;; + + --dry-run) + DRY_RUN=1 + shift + ;; + + --no-composer) + noComposer=1 + shift + ;; + + --no-pot-update) + noPotUpdate=1 + shift + ;; + + *) + break + ;; + esac + done + + if [ $# -gt 3 ]; then + usage >&2 + exit 1 + fi + + validateInfoXml + + local version + local nextVersion + local develStage + + if [ $# -ge 1 ]; then + version=$1 + else + version=$(detectVersion) + echo -n "Version [$version]: " + read -r input + if [ -n "$input" ]; then + version=$input + fi + fi + validateVersion "$version" + + if [ $# -ge 2 ]; then + develStage=$2 + else + develStage=$(detectDevelStage "$version") + echo -n "Development stage [$develStage]: " + read -r input + if [ -n "$input" ]; then + develStage=$input + fi + fi + validateDevelopmentStage "$develStage" + + if [ $# -ge 3 ]; then + nextVersion=$3 + else + nextVersion=$(detectNextVersion "$version") + echo -n "Next version [$nextVersion]: " + read -r input + if [ -n "$input" ]; then + nextVersion=$input + fi + fi + validateNextVersion "$nextVersion" + + if [ $noComposer -eq 0 ] && ! hasComposerRequires; then + noComposer=1 + fi + + if [ $noComposer -eq 0 ]; then + local -r minPhpVersion=$(getMinPhpVersion) + validateMinPhpVersion "$minPhpVersion" + fi + + if [ $noPotUpdate -eq 0 ]; then + updatePot + fi + + local -r releaseDate=$(date +%Y-%m-%d) + run updateInfoXml "$version" "$develStage" "$releaseDate" + run git add info.xml + + if [ $noComposer -eq 0 ]; then + local -r previousPlatformPhp=$(composer config platform.php 2>/dev/null ||:) + run composer config platform.php "$minPhpVersion" + run composer update --no-dev --optimize-autoloader + if [ -n "$previousPlatformPhp" ]; then + run composer config platform.php "$previousPlatformPhp" + else + run composer config --unset platform.php + fi + run git add -f composer.lock vendor + fi + + run git commit -m "Set version to $version" + run git tag "$version" + + run updateInfoXml "$nextVersion" "dev" + run git add info.xml + + if [ $noComposer -eq 0 ]; then + run git rm -r composer.lock vendor + fi + + if [ -f composer.json ]; then + local -r branch=$(git branch --show-current) + if [ "$branch" = "main" ] || [ "$branch" = "master" ]; then + [[ "$nextVersion" =~ ^([0-9]+\.[0-9]+)\.[0-9]+ ]] + local -r alias=${BASH_REMATCH[1]}.x-dev + run composer config "extra.branch-alias.dev-$branch" "$alias" + run git add composer.json + fi + fi + + run git commit -m "Set version to $nextVersion" + + echo "" + echo "Push changes with: git push && git push --tags" +} + +main "$@" diff --git a/tools/git/hooks-wrapper.sh b/tools/git/hooks-wrapper.sh new file mode 100755 index 0000000..f32978c --- /dev/null +++ b/tools/git/hooks-wrapper.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# Inspired by https://github.com/sjungwirth/githooks/blob/949d55e84e92dfd84e9a73a8b7624bb2e4dbc872/bin/git/hooks-wrapper + +# This script needs to be symlinked to .git/hooks/. + +set -e + +HOOKNAME=$(basename "$0") +NATIVE_HOOKS_DIR=$(dirname "$0") +CUSTOM_HOOKS_DIR=$(dirname "$(realpath "$0")")/hooks + +# Runs all executables in $CUSTOM_HOOKS_DIR/hooks/$HOOKNAME.d and +# $NATIVE_HOOKS_DIR/$HOOKNAME.local if existent. + +exitcode= +for hook in "$CUSTOM_HOOKS_DIR/$HOOKNAME.d/"*; do + if [ -x "$hook" ]; then + "$hook" "$@" || exitcode=${exitcode:-$?} + fi +done + +if [ -x "$NATIVE_HOOKS_DIR/$HOOKNAME.local" ]; then + "$NATIVE_HOOKS_DIR/$HOOKNAME.local" "$@" || exitcode=${exitcode:-$?} +fi + +# shellcheck disable=SC2086 +exit $exitcode diff --git a/tools/git/hooks/pre-commit.d/remember-update-pot.sh b/tools/git/hooks/pre-commit.d/remember-update-pot.sh new file mode 100755 index 0000000..827242c --- /dev/null +++ b/tools/git/hooks/pre-commit.d/remember-update-pot.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +echo "Please remember to update the .pot file (tools/update-pot.sh) and the translation." + diff --git a/tools/git/hooks/pre-merge-commit.d/run-pre-commit-hook.sh b/tools/git/hooks/pre-merge-commit.d/run-pre-commit-hook.sh new file mode 100755 index 0000000..b3c4912 --- /dev/null +++ b/tools/git/hooks/pre-merge-commit.d/run-pre-commit-hook.sh @@ -0,0 +1,6 @@ +#!/bin/sh + +. git-sh-setup +if [ -x "$GIT_DIR/hooks/pre-commit" ]; then + exec "$GIT_DIR/hooks/pre-commit" +fi diff --git a/tools/git/init-hooks.sh b/tools/git/init-hooks.sh new file mode 100755 index 0000000..b635da0 --- /dev/null +++ b/tools/git/init-hooks.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# Inspired by https://github.com/sjungwirth/githooks/blob/949d55e84e92dfd84e9a73a8b7624bb2e4dbc872/bin/git/init-hooks + +set -e + +SCRIPT_DIR=$(dirname "$0") +NATIVE_HOOKS_DIR=$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel)/.git/hooks +CUSTOM_HOOKS_DIR=$SCRIPT_DIR/hooks +HOOKS_WRAPPER=$(realpath -s --relative-to="$NATIVE_HOOKS_DIR" "$SCRIPT_DIR")/hooks-wrapper.sh + +cd "$CUSTOM_HOOKS_DIR" +HOOK_DIRS=(*.d) + +for hook_dir in "${HOOK_DIRS[@]}"; do + hookname=${hook_dir:0:-2} + if [ ! -L "$NATIVE_HOOKS_DIR/$hookname" ]; then + if [ -f "$NATIVE_HOOKS_DIR/$hookname" ]; then + mv "$NATIVE_HOOKS_DIR/$hookname" "$NATIVE_HOOKS_DIR/$hookname.local" + fi + ln -s "$HOOKS_WRAPPER" "$NATIVE_HOOKS_DIR/$hookname" + fi +done diff --git a/tools/phpstan/composer.json b/tools/phpstan/composer.json index 218fa72..ce2fee2 100644 --- a/tools/phpstan/composer.json +++ b/tools/phpstan/composer.json @@ -1,13 +1,14 @@ { "require": { - "phpstan/extension-installer": "^1.1", - "phpstan/phpstan": "^1.7", - "phpstan/phpstan-deprecation-rules": "^1.0", - "phpstan/phpstan-phpunit": "^1.1", - "phpstan/phpstan-strict-rules": "^1.2", - "phpstan/phpstan-webmozart-assert": "^1.2", - "thecodingmachine/phpstan-strict-rules": "^1.0", - "voku/phpstan-rules": "^3.0" + "kcs/phpstan-strict-rules": "^2", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2", + "phpstan/phpstan-beberlei-assert": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-phpunit": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpstan/phpstan-webmozart-assert": "^2", + "voku/phpstan-rules": "^3.6" }, "config": { "allow-plugins": { diff --git a/tools/update-pot.sh b/tools/update-pot.sh new file mode 100755 index 0000000..d7e7c99 --- /dev/null +++ b/tools/update-pot.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +set -euo pipefail + +readonly SCRIPT_PATH="$0" +SCRIPT_NAME=$(basename "$SCRIPT_PATH") +readonly SCRIPT_NAME +SCRIPT_DIR=$(dirname "$SCRIPT_PATH") +readonly SCRIPT_DIR + +usage() { + cat <