diff --git a/docs/info/customizations.md b/docs/info/customizations.md
index 2f61de7e448f537e04a9e459a838a2789ad2aa5e..d0c22759271c36fabff3fcc3d013d301374c17a9 100644
--- a/docs/info/customizations.md
+++ b/docs/info/customizations.md
@@ -45,6 +45,23 @@ phpstan:
   allow_failure: false
 ```
 
+## Custom before_script and after_script
+
+If your project needs to do extra processing in preparation before a job, or to do some custom work with results after a job, you can use the `before_script` and `after_script` job keywords. Every job can accept these customizations and they are specifically not used by Gitlab Templates, making them available for Contrib use. The variable `$DRUPAL_PROJECT_FOLDER` contains the absolute path to the contrib project within the Drupal site installation.
+
+Examples:
+```
+# Remove files that you don't want to be checked.
+phpcs:
+  before_script:
+    - rm $DRUPAL_PROJECT_FOLDER/some-file.inc
+
+# Run a custom script after the job finishes.
+phpstan:
+  after_script:
+    - php $DRUPAL_PROJECT_FOLDER/scripts/phpstan-baseline-summary.php
+```
+
 ## References
 
 If the part of the template that you are overriding uses in-template references, you don't need to replicate them in your overrides, you can just use the [!reference notation](https://docs.gitlab.com/ee/ci/yaml/yaml_optimization.html#reference-tags).
diff --git a/includes/include.drupalci.main-d7.yml b/includes/include.drupalci.main-d7.yml
index 2f6114573bff3b86f1ee2f6ea9094a08c86e3a5b..dc48624226d74a6921bfcbe6a08b3c6d40e416ba 100644
--- a/includes/include.drupalci.main-d7.yml
+++ b/includes/include.drupalci.main-d7.yml
@@ -69,6 +69,10 @@
     # Use -e so that \n is interpreted as a new line.
     echo -e "PHP_VERSION=$PHP_VERSION\nPHP_IMAGE_VARIANT=$PHP_IMAGE_VARIANT\nPHP_IMAGE_TAG=$PHP_IMAGE_TAG" >> build.env
 
+    # Create a variable to hold the path to the project's own folder.
+    export DRUPAL_PROJECT_FOLDER=$CI_PROJECT_DIR/$_WEB_ROOT/sites/all/modules/custom/$CI_PROJECT_NAME
+    echo "DRUPAL_PROJECT_FOLDER=$DRUPAL_PROJECT_FOLDER" >> build.env
+
 # Display the Gitlab Templates version, the Composer version and some useful CI variables.
 .show-ci-variables: &show-ci-variables
   - echo "Executing curl -OL https://git.drupalcode.org/$_CURL_TEMPLATES_REPO/-/raw/$_CURL_TEMPLATES_REF/scripts/extract-version.php"
@@ -141,9 +145,9 @@
     if [ "$_D7_DRUPAL_TEST_DEPENDENCIES" != "" ]; then
       printf "\n\n*** Installing test_dependencies: $_D7_DRUPAL_TEST_DEPENDENCIES"
       vendor/bin/drush --root=$_WEB_ROOT pm:download -y $_D7_DRUPAL_TEST_DEPENDENCIES
-    elif [ ! -f $_WEB_ROOT/sites/all/modules/custom/$CI_PROJECT_NAME/composer.json ]; then
+    elif [ ! -f $DRUPAL_PROJECT_FOLDER/composer.json ]; then
       printf "\n\n*** Trying to install test_dependencies from $PROJECT_NAME.info automatically. If this fails you can populate the variable _D7_DRUPAL_TEST_DEPENDENCIES in your .gitlab-ci.yml file, or create a composer.json and define the test dependencies with require-dev.\n\n"
-      cat $_WEB_ROOT/sites/all/modules/custom/$CI_PROJECT_NAME/$PROJECT_NAME.info | sed -n '/^test_dependencies\[\].*=/p' | sed -r 's/^test_dependencies\[\].?=//' | sed 's/([^)]*)//' | while read -r line; do
+      cat $DRUPAL_PROJECT_FOLDER/$PROJECT_NAME.info | sed -n '/^test_dependencies\[\].*=/p' | sed -r 's/^test_dependencies\[\].?=//' | sed 's/([^)]*)//' | while read -r line; do
         IFS=':' read -r -a array <<< "$line"
         echo "- drush dl ${array[0]}"
         vendor/bin/drush --root=$_WEB_ROOT pm:download -y ${array[0]}
@@ -320,7 +324,7 @@ stages:
     - export PROJECT_FILES=$(ls -A)
     # Extract Core into default directory then rename to $_WEB_ROOT
     - curl https://ftp.drupal.org/files/projects/drupal-$_TARGET_D7_CORE.tar.gz | tar -xz
-    - mv drupal-$_TARGET_D7_CORE $CI_PROJECT_DIR/$_WEB_ROOT
+    - mv -v drupal-$_TARGET_D7_CORE $CI_PROJECT_DIR/$_WEB_ROOT
     # If composer.json exists then make a backup before it gets modified. Otherwise create an empty file.
     - |
       if [[ -f composer.json ]]; then
@@ -358,12 +362,12 @@ stages:
     - php symlink_project.php "$CI_PROJECT_NAME" 'sites/all/modules/custom'
     - rm symlink_project.php
     - echo -e "\e[0Ksection_end:`date +%s`:symlink_output\r\e[0K"
-    # Delete the current composer.json, and then restore from backup if one was made earlier.
+    # Restore composer.json from backup if one was made earlier.
     - |
       if [[ -f composer.json.backup ]]; then
-        rm $_WEB_ROOT/sites/all/modules/custom/$CI_PROJECT_NAME/composer.json
-        echo "Restoring composer.json.backup to $_WEB_ROOT/sites/all/modules/custom/$CI_PROJECT_NAME/composer.json"
-        mv composer.json.backup $_WEB_ROOT/sites/all/modules/custom/$CI_PROJECT_NAME/composer.json
+        echo "Restoring composer.json from backup"
+        rm -v $DRUPAL_PROJECT_FOLDER/composer.json
+        mv -v composer.json.backup $DRUPAL_PROJECT_FOLDER/composer.json
       fi
     # Give confirmation of the directories in vendor/drupal and sites/all/modules.
     - ls $CI_PROJECT_DIR/vendor/drupal
diff --git a/includes/include.drupalci.main.yml b/includes/include.drupalci.main.yml
index 1fb4f3859ea21ee7fb84dbf8bc9e2112f1ee5073..91fc58259d96aa1ed9b79d6979ee4cdb6edb9fde 100644
--- a/includes/include.drupalci.main.yml
+++ b/includes/include.drupalci.main.yml
@@ -98,6 +98,10 @@
     # Write PHP_IMAGE_VARIANT and PHP_IMAGE_TAG so that all subsequent variant jobs have the correct values automatically.
     echo -e "PHP_IMAGE_VARIANT=$PHP_IMAGE_VARIANT\nPHP_IMAGE_TAG=$PHP_IMAGE_TAG" >> build.env
 
+    # Create a variable to hold the path to the project's own folder.
+    export DRUPAL_PROJECT_FOLDER=$CI_PROJECT_DIR/$_WEB_ROOT/modules/custom/$CI_PROJECT_NAME
+    echo "DRUPAL_PROJECT_FOLDER=$DRUPAL_PROJECT_FOLDER" >> build.env
+
 # Display the Gitlab Templates version, the Composer version and some useful CI variables.
 .show-ci-variables: &show-ci-variables
   - echo "Executing curl -OL https://git.drupalcode.org/$_CURL_TEMPLATES_REPO/-/raw/$_CURL_TEMPLATES_REF/scripts/extract-version.php"
@@ -197,7 +201,7 @@
 .amend-core-requirements-next-major: &amend-core-requirements-next-major
   - SAVED_PWD=$PWD;
   # Change directory to restrict finding info.yml to just those in the project.
-  - cd $CI_PROJECT_DIR/$_WEB_ROOT/modules/custom/$CI_PROJECT_NAME && pwd
+  - cd $DRUPAL_PROJECT_FOLDER && pwd
   # Need the -L parameter to be able to detect the symlinked files.
   - INFO_FILES=$(find -L . -name "*.info.yml") || true
   # Return to top level to make the file changes.
@@ -457,9 +461,9 @@ stages:
     # If composer.json exists then make a backup before it gets modified. Otherwise create an empty file.
     - |
       if [[ -f composer.json ]]; then
-        cp composer.json composer.json.backup
+        cp -v composer.json composer.json.backup
       else
-        echo "{}" > composer.json
+        echo "{}" > composer.json && echo "Project has no composer.json so creating an empty one"
       fi
     - php expand_composer_json.php || EXPAND_COMPOSER_EXIT_CODE=$?
     - if [[ "$EXPAND_COMPOSER_EXIT_CODE" != "" ]]; then echo "EXPAND_COMPOSER_EXIT_CODE=$EXPAND_COMPOSER_EXIT_CODE"; exit $EXPAND_COMPOSER_EXIT_CODE; fi
@@ -473,12 +477,12 @@ stages:
     - php symlink_project.php "$CI_PROJECT_NAME" 'modules/custom'
     - rm symlink_project.php
     - echo -e "\e[0Ksection_end:`date +%s`:symlink_output\r\e[0K"
-    # Delete the current composer.json, and then restore from backup if one was made earlier.
+    # Restore composer.json from backup if one was made earlier.
     - |
       if [[ -f composer.json.backup ]]; then
-        rm $_WEB_ROOT/modules/custom/$CI_PROJECT_NAME/composer.json
-        echo "Restoring composer.json.backup to $_WEB_ROOT/modules/custom/$CI_PROJECT_NAME/composer.json"
-        mv composer.json.backup $_WEB_ROOT/modules/custom/$CI_PROJECT_NAME/composer.json
+        echo "Restoring composer.json from backup"
+        rm -v $DRUPAL_PROJECT_FOLDER/composer.json
+        mv -v composer.json.backup $DRUPAL_PROJECT_FOLDER/composer.json
       fi
     # For Nightwatch et al.
     - cd $_WEB_ROOT/core && corepack enable && yarn install && cd $CI_PROJECT_DIR
@@ -594,27 +598,27 @@ composer-lint:
     - composer validate
     # Validate the module's composer.json.
     - |
-      if [ -f $CI_PROJECT_DIR/$_WEB_ROOT/modules/custom/$CI_PROJECT_NAME/composer.json ]; then
+      if [ -f $DRUPAL_PROJECT_FOLDER/composer.json ]; then
         echo "Validating the composer.json file from the project."
-        cd $CI_PROJECT_DIR/$_WEB_ROOT/modules/custom/$CI_PROJECT_NAME && pwd
+        cd $DRUPAL_PROJECT_FOLDER && pwd
         # Rename the lock file so it is not used in the validation command.
-        test -f composer.lock && mv composer.lock composer.lock.backup
+        test -f composer.lock && mv -v composer.lock composer.lock.backup
         composer validate
-        test -f composer.lock.backup && mv composer.lock.backup composer.lock
+        test -f composer.lock.backup && mv -v composer.lock.backup composer.lock
         cd $CI_PROJECT_DIR
       fi
     - vendor/bin/parallel-lint --version
     - DRUPAL_PHP_FILE_TYPES_PIPE=`echo "$DRUPAL_PHP_FILE_TYPES" | tr ',' '|'`
     - echo "DRUPAL_PHP_FILE_TYPES_PIPE=$DRUPAL_PHP_FILE_TYPES_PIPE"
     # Find all PHP files in the actual project folder. -L is needed to follow the symlinks. Need || true to cater for when there are no files.
-    - PHP_FILES=$(find -L $_WEB_ROOT/modules/custom/$CI_PROJECT_NAME -type f | grep -E "\.($DRUPAL_PHP_FILE_TYPES_PIPE)$" | wc -l) || true
+    - PHP_FILES=$(find -L $DRUPAL_PROJECT_FOLDER -type f | grep -E "\.($DRUPAL_PHP_FILE_TYPES_PIPE)$" | wc -l) || true
     - |
       if [[ "$PHP_FILES" > "0" ]]; then
-        echo "Found $PHP_FILES PHP files in $CI_PROJECT_DIR/$_WEB_ROOT/modules/custom/$CI_PROJECT_NAME"
+        echo "Found $PHP_FILES PHP files in $DRUPAL_PROJECT_FOLDER"
         # parallel-lint has to be run here in $CI_PROJECT_DIR as it cannot follow symlinks.
         vendor/bin/parallel-lint -e $DRUPAL_PHP_FILE_TYPES --exclude $_WEB_ROOT --exclude vendor --exclude node_modules --no-progress $_PARALLEL_LINT_EXTRA .
       else
-        echo "There are no PHP files to validate in $CI_PROJECT_DIR/$_WEB_ROOT/modules/custom/$CI_PROJECT_NAME"
+        echo "There are no PHP files to validate in $DRUPAL_PROJECT_FOLDER"
       fi
 
 phpcs:
@@ -668,7 +672,7 @@ phpcs:
     - composer
   script:
     # Run from within project directory so paths are correct.
-    - cd $CI_PROJECT_DIR/$_WEB_ROOT/modules/custom/$CI_PROJECT_NAME && pwd
+    - cd $DRUPAL_PROJECT_FOLDER && pwd
     - *check-composer-end-code
     # If there is no PHPStan configuration neon file get the default from /assets/phpstan.neon
     - |
@@ -702,7 +706,7 @@ phpcs:
     - mv -v $_PHPSTAN_BASELINE_FILENAME $CI_PROJECT_DIR
     # Fix paths in message text in the reports.
     - cd $CI_PROJECT_DIR && pwd
-    - sed -i "s#$CI_PROJECT_DIR/$_WEB_ROOT/modules/custom/$CI_PROJECT_NAME/##g" junit.xml $_PHPSTAN_BASELINE_FILENAME phpstan-quality-report.json
+    - sed -i "s#$DRUPAL_PROJECT_FOLDER/##g" junit.xml $_PHPSTAN_BASELINE_FILENAME phpstan-quality-report.json
     - exit $EXIT_CODE
   allow_failure: true
   artifacts:
@@ -819,7 +823,7 @@ stylelint:
   script:
     - cd $CI_PROJECT_DIR/$_WEB_ROOT/core && corepack enable && yarn add @gitlab-formatters/stylelint-formatter-gitlab
     # Change directory to the project root folder.
-    - cd $CI_PROJECT_DIR/$_WEB_ROOT/modules/custom/$CI_PROJECT_NAME && pwd
+    - cd $DRUPAL_PROJECT_FOLDER && pwd
     - echo "Stylelint version $(${CI_PROJECT_DIR}/${_WEB_ROOT}/core/node_modules/.bin/stylelint --version)"
     # If there is no .stylelintignore file, there is no warning or error. The
     # option is just ignored.
@@ -854,7 +858,7 @@ eslint:
     - composer
   script:
     # Change directory to the project root folder
-    - cd $CI_PROJECT_DIR/$_WEB_ROOT/modules/custom/$CI_PROJECT_NAME && pwd
+    - cd $DRUPAL_PROJECT_FOLDER && pwd
     # Configure ESLint with core defaults. We use core/.eslintrc.passing.json which includes core/.eslintrc.json and .eslintrc.jquery.json.
     # These links are created in the folder above modules/custom/$CI_PROJECT_NAME and will be used in addition to the project's own .eslintrc.json.
     - ln -s $CI_PROJECT_DIR/$_WEB_ROOT/core/.eslintrc.passing.json $CI_PROJECT_DIR/$_WEB_ROOT/modules/custom/.eslintrc.json
@@ -936,7 +940,7 @@ cspell:
     - echo "Executing curl -OL https://git.drupalcode.org/$_CURL_TEMPLATES_REPO/-/raw/$_CURL_TEMPLATES_REF/scripts/prepare-cspell.php"
     - curl -OL https://git.drupalcode.org/$_CURL_TEMPLATES_REPO/-/raw/$_CURL_TEMPLATES_REF/scripts/prepare-cspell.php
     # Restore composer.json back to an unchanged version.
-    - test -f $CI_PROJECT_DIR/$_WEB_ROOT/modules/custom/$CI_PROJECT_NAME/composer.json && cp $CI_PROJECT_DIR/$_WEB_ROOT/modules/custom/$CI_PROJECT_NAME/composer.json composer.json
+    - test -f $DRUPAL_PROJECT_FOLDER/composer.json && cp -v $DRUPAL_PROJECT_FOLDER/composer.json composer.json
     - |
       if [ ! -f .cspell.json ]; then
         echo "Getting default .cspell.json from https://git.drupalcode.org/$_CURL_TEMPLATES_REPO/-/raw/$_CURL_TEMPLATES_REF/assets/.cspell.json"
@@ -1167,8 +1171,8 @@ nightwatch (next major):
           printf "$DIVIDER\n"
         fi
         printf "_PHPUNIT_CONCURRENT=$_PHPUNIT_CONCURRENT, _PHPUNIT_TESTGROUPS=$_PHPUNIT_TESTGROUPS, _PHPUNIT_EXTRA=$_PHPUNIT_EXTRA\nPHPUNIT_OPTIONS=$PHPUNIT_OPTIONS, WHAT_TO_RUN=$WHAT_TO_RUN\n"
-        echo "executing: sudo -u www-data -H -E vendor/bin/phpunit $PHPUNIT_OPTIONS --bootstrap $PWD/$_WEB_ROOT/core/tests/bootstrap.php $PWD/$_WEB_ROOT/modules/custom/$CI_PROJECT_NAME --log-junit $CI_PROJECT_DIR/junit.xml $WHAT_TO_RUN $_PHPUNIT_EXTRA"
-        sudo -u www-data -H -E vendor/bin/phpunit $PHPUNIT_OPTIONS --bootstrap $PWD/$_WEB_ROOT/core/tests/bootstrap.php $PWD/$_WEB_ROOT/modules/custom/$CI_PROJECT_NAME --log-junit $CI_PROJECT_DIR/junit.xml $WHAT_TO_RUN $_PHPUNIT_EXTRA || EXIT_CODE=$?
+        echo "executing: sudo -u www-data -H -E vendor/bin/phpunit $PHPUNIT_OPTIONS --bootstrap $PWD/$_WEB_ROOT/core/tests/bootstrap.php $DRUPAL_PROJECT_FOLDER --log-junit $CI_PROJECT_DIR/junit.xml $WHAT_TO_RUN $_PHPUNIT_EXTRA"
+        sudo -u www-data -H -E vendor/bin/phpunit $PHPUNIT_OPTIONS --bootstrap $PWD/$_WEB_ROOT/core/tests/bootstrap.php $DRUPAL_PROJECT_FOLDER --log-junit $CI_PROJECT_DIR/junit.xml $WHAT_TO_RUN $_PHPUNIT_EXTRA || EXIT_CODE=$?
       elif [ "$_PHPUNIT_CONCURRENT" == "1" ]; then
         # if _PHPUNIT_TESTGROUPS is blank then do not add anything, because the test group will be handled by the matrix.
         # if _PHPUNIT_TESTGROUPS is --all then add --directory modules/custom/$CI_PROJECT_NAME
@@ -1280,7 +1284,7 @@ test-only changes:
   interruptible: true
   allow_failure: true
   script:
-    - cd $$CI_PROJECT_DIR && pwd
+    - cd $CI_PROJECT_DIR && pwd
     - *check-composer-end-code
     - *show-environment-variables
     - *setup-webserver
diff --git a/scripts/test-only-d7.sh b/scripts/test-only-d7.sh
index 07cf7133b36d836ace09e1673558aa9e5ef18f6a..d79d37dd50bbfd39675e3533a96b4cb732e3b425 100755
--- a/scripts/test-only-d7.sh
+++ b/scripts/test-only-d7.sh
@@ -38,7 +38,7 @@ TESTS_CHANGED=$(git diff ${BASELINE} --name-only | grep -E '\.test$') || true
 if [ "$TESTS_CHANGED" != "" ]; then
   printf " \n2️⃣ Running tests changed in this merge request\n \nThe following test files have been changed, and only these tests will be run:\n$TESTS_CHANGED\n "
   for file in $TESTS_CHANGED; do
-    test=sites/all/modules/custom/$CI_PROJECT_NAME/$file
+    test=$DRUPAL_PROJECT_FOLDER/$file
     printf "$DIVIDER\nexecuting: sudo SYMFONY_DEPRECATIONS_HELPER='$SYMFONY_DEPRECATIONS_HELPER' MINK_DRIVER_ARGS_WEBDRIVER='$MINK_DRIVER_ARGS_WEBDRIVER' -u www-data php $_WEB_ROOT/scripts/run-tests.sh --color --concurrency '32' --url $SIMPLETEST_BASE_URL --verbose --fail-only --xml $BROWSERTEST_OUTPUT_DIRECTORY --file $test $_PHPUNIT_EXTRA\n "
     sudo SYMFONY_DEPRECATIONS_HELPER="$SYMFONY_DEPRECATIONS_HELPER" MINK_DRIVER_ARGS_WEBDRIVER="$MINK_DRIVER_ARGS_WEBDRIVER" -u www-data php $_WEB_ROOT/scripts/run-tests.sh --color --concurrency "32" --url $SIMPLETEST_BASE_URL --verbose --fail-only --xml $BROWSERTEST_OUTPUT_DIRECTORY --file $test $_PHPUNIT_EXTRA || EXIT_CODE=$?
   done;
diff --git a/scripts/test-only.sh b/scripts/test-only.sh
index c2ca7d697464dd1993656014809b558b5a85eb65..eb1606a2f12d30474339143aa436c655840410ef 100755
--- a/scripts/test-only.sh
+++ b/scripts/test-only.sh
@@ -44,7 +44,7 @@ TESTS_CHANGED=$(git diff ${BASELINE} --name-only | grep -E 'Test\.php$') || true
 if [ "$TESTS_CHANGED" != "" ]; then
   printf " \n2️⃣ Running tests changed in this merge request\n \nThe following test files have been changed, and only these tests will be run:\n$TESTS_CHANGED\n "
   for file in $TESTS_CHANGED; do
-    test=$CI_PROJECT_DIR/$_WEB_ROOT/modules/custom/$CI_PROJECT_NAME/$file
+    test=$DRUPAL_PROJECT_FOLDER/$file
     if [ "$_PHPUNIT_CONCURRENT" == "0" ]; then
       printf "$DIVIDER\n_PHPUNIT_CONCURRENT=$_PHPUNIT_CONCURRENT, _PHPUNIT_EXTRA=$_PHPUNIT_EXTRA, PHPUNIT_OPTIONS=$PHPUNIT_OPTIONS\n "
       printf "executing: sudo -u www-data -H -E vendor/bin/phpunit $PHPUNIT_OPTIONS --bootstrap $PWD/$_WEB_ROOT/core/tests/bootstrap.php $test --log-junit $CI_PROJECT_DIR/junit.xml $_PHPUNIT_EXTRA\n "