diff --git a/CHANGELOG.md b/CHANGELOG.md
index ab6474c5adc3a4b424616708d170767f540dfaa2..0edd8ca3de22818341b9e16c2bddb7a8fcd10b8e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,9 +1,30 @@
-## [4.0.1](https://git.code.tecnalia.dev/smartdatalab/public/ci-cd-components/node/compare/4.0.0...4.0.1) (2024-09-18)
+## [4.1.1](https://gitlab.com/to-be-continuous/node/compare/4.1.0...4.1.1) (2024-12-05)
 
 
 ### Bug Fixes
 
-* set node-publish default registry ([fd0f04d](https://git.code.tecnalia.dev/smartdatalab/public/ci-cd-components/node/commit/fd0f04d730e7b844dd6f57a77b21d913b16dd1c9))
+* semgrep subdir ([8ac460c](https://gitlab.com/to-be-continuous/node/commit/8ac460c05668590b7713f05fc571fc7b3fe2f4b4))
+
+# [4.1.0](https://gitlab.com/to-be-continuous/node/compare/4.0.3...4.1.0) (2024-11-23)
+
+
+### Features
+
+* **lint:** enforce GitLab integration ([44cc3c3](https://gitlab.com/to-be-continuous/node/commit/44cc3c30b1211f35c428edfc5e4165aaae086fbc))
+
+## [4.0.3](https://gitlab.com/to-be-continuous/node/compare/4.0.2...4.0.3) (2024-11-02)
+
+
+### Bug Fixes
+
+* limit security reports access to developer role or higher ([3d4335f](https://gitlab.com/to-be-continuous/node/commit/3d4335fd9d46070720de57cda656c2570dd9efa2))
+
+## [4.0.2](https://gitlab.com/to-be-continuous/node/compare/4.0.1...4.0.2) (2024-10-04)
+
+
+### Bug Fixes
+
+* **release:** support full semantic-versioning specifcation (with prerelease and build metadata) ([b031dae](https://gitlab.com/to-be-continuous/node/commit/b031dae19ccf2cd4f31066ff052efd2d8b1c7aef))
 
 ## [4.0.1](https://gitlab.com/to-be-continuous/node/compare/4.0.0...4.0.1) (2024-09-08)
 
diff --git a/README.md b/README.md
index 9e19768fb95b5be85e4f382d120af2dd7f49eaba..192d2c1d22ec5586724bdc0a8ad680d61b5387cb 100644
--- a/README.md
+++ b/README.md
@@ -16,7 +16,7 @@ Add the following to your `.gitlab-ci.yml`:
 ```yaml
 include:
   # 1: include the component
-  - component: $CI_SERVER_FQDN/to-be-continuous/node/gitlab-ci-node@4.0.1
+  - component: $CI_SERVER_FQDN/to-be-continuous/node/gitlab-ci-node@4.1.1
     # 2: set/override component inputs
     inputs:
       image: "registry.hub.docker.com/library/node:20" # ⚠ this is only an example
@@ -31,7 +31,7 @@ Add the following to your `.gitlab-ci.yml`:
 include:
   # 1: include the template
   - project: "to-be-continuous/node"
-    ref: "4.0.0"
+    ref: "4.1.1"
     file: "/templates/gitlab-ci-node.yml"
 
 variables:
@@ -90,7 +90,8 @@ variables:
 
 ### `node-lint` job
 
-The Node template features a job `node-lint` that performs Node.js source code **lint**. This job is **disabled by default**. It can be activated by setting `NODE_LINT_ENABLED`
+The Node template features a `node-lint` job that performs a code analysis with [ESLint](https://eslint.org/).
+This job is **disabled by default**. It can be activated by setting `NODE_LINT_ENABLED`.
 
 It is bound to the `build` stage, and uses the following variable:
 
@@ -100,7 +101,18 @@ It is bound to the `build` stage, and uses the following variable:
 | `lint-args` / `NODE_LINT_ARGS`       | npm [run script](https://docs.npmjs.com/cli/v8/commands/npm-run-script) arguments to execute the lint analysis <br/> yarn [run script](https://classic.yarnpkg.com/en/docs/cli/run) arguments to execute the lint analysis <br/> pnpm [run script](https://pnpm.io/cli/run) arguments to execute the lint analysis | `run lint`        |
 | `node-lint-job-tags` / `NODE_LINT_JOB_TAGS` | Tags to be used for selecting runners for the job | [] |
 
-The job generates a lint report that you will find here: `NODE_PROJECT_DIR/reports/node-lint.xslint.json`.
+In addition to a textual report in the console, this job produces the following reports, kept for one day:
+
+| Report                                                | Format                                                   | Usage                                                                                                       |
+|-------------------------------------------------------|----------------------------------------------------------|-------------------------------------------------------------------------------------------------------------|
+| `$NODE_PROJECT_DIR/reports/node-lint.gitlab.json` | [GitLab](https://docs.gitlab.com/ee/ci/testing/code_quality.html#eslint) | [GitLab integration](https://docs.gitlab.com/ee/ci/yaml/artifacts_reports.html#artifactsreportscodequality) |
+| `$NODE_PROJECT_DIR/reports/node-lint.xslint.json` | JSON ESLint                                               | [SonarQube integration](https://docs.sonarsource.com/sonarqube-server/latest/analyzing-source-code/importing-external-issues/external-analyzer-reports/)                        |
+
+
+
+| Report                                            | Format                                                        | Usage                                                                                                                                                                                                                                  |
+| ------------------------------------------------- | ------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `$NODE_PROJECT_DIR/reports/npm-audit.native.json` | [JSON](https://docs.npmjs.com/cli/v9/commands/npm-audit#json) | [DefectDojo integration](https://docs.defectdojo.com/en/connecting_your_tools/parsers/file/npm_audit_7_plus/)<br/>_This report is generated only if DefectDojo template is detected, if needed, you can force it with `$DEFECTDOJO_NPMAUDIT_REPORTS`_ |
 
 ### `node-build` job
 
@@ -135,8 +147,8 @@ Here is the required configuration if you're using [Jest](https://jestjs.io/) as
 | [jest-junit](https://github.com/jest-community/jest-junit)                                   | Yes                 | `reports/node-test.xunit.xml`    | [GitLab unit tests integration](https://docs.gitlab.com/ee/ci/testing/unit_test_reports.html) _(JUnit format)_                                                     |
 | istanbul [text](https://istanbul.js.org/docs/advanced/alternative-reporters/#text)           | No                  | N/A _(stdout)_                   | [GitLab MR test coverage results](https://docs.gitlab.com/ee/ci/pipelines/settings.html#merge-request-test-coverage-results) _(GitLab grabs coverage from stdout)_ |
 | istanbul [cobertura](https://istanbul.js.org/docs/advanced/alternative-reporters/#cobertura) | No                  | `reports/cobertura-coverage.xml` | [GitLab code coverage integration](https://docs.gitlab.com/ee/ci/testing/test_coverage_visualization.html) _(Cobertura format)_                                    |
-| [jest-sonar](https://github.com/sh33dafi/jest-sonar)                                         | Yes                 | `reports/node-test.sonar.xml`    | [SonarQube unit tests integration](https://docs.sonarqube.org/latest/analysis/generic-test/) _(generic SonarQube format)_                                          |
-| istanbul [lcovonly](https://istanbul.js.org/docs/advanced/alternative-reporters/#lcovonly)   | No                  | `reports/lcov.info`              | [SonarQube code coverage integration](https://docs.sonarqube.org/latest/analysis/test-coverage/javascript-typescript-test-coverage/) _(JS/TS LCOV format)_         |
+| [jest-sonar](https://github.com/sh33dafi/jest-sonar)                                         | Yes                 | `reports/node-test.sonar.xml`    | [SonarQube unit tests integration](https://docs.sonarsource.com/sonarqube-server/latest/analyzing-source-code/test-coverage/generic-test-data/#generic-test-coverage) _(generic SonarQube format)_                                          |
+| istanbul [lcovonly](https://istanbul.js.org/docs/advanced/alternative-reporters/#lcovonly)   | No                  | `reports/lcov.info`              | [SonarQube code coverage integration](https://docs.sonarsource.com/sonarqube-server/latest/analyzing-source-code/test-coverage/javascript-typescript-test-coverage/) _(JS/TS LCOV format)_         |
 
 Here is an example of a `jest.config.js` configuration file with all the above reporters configured as expected:
 
@@ -182,8 +194,8 @@ Here is the required configuration if you're using [Mocha](https://mochajs.org/)
 | [mocha-junit-reporter](https://github.com/michaelleeallen/mocha-junit-reporter)              | Yes                    | `reports/node-test.xunit.xml`    | [GitLab unit tests integration](https://docs.gitlab.com/ee/ci/testing/unit_test_reports.html) _(JUnit format)_                                                     |
 | istanbul [text](https://istanbul.js.org/docs/advanced/alternative-reporters/#text)           | Yes (in `nyc` package) | N/A _(stdout)_                   | [GitLab MR test coverage results](https://docs.gitlab.com/ee/ci/pipelines/settings.html#merge-request-test-coverage-results) _(GitLab grabs coverage from stdout)_ |
 | istanbul [cobertura](https://istanbul.js.org/docs/advanced/alternative-reporters/#cobertura) | Yes (in `nyc` package) | `reports/cobertura-coverage.xml` | [GitLab code coverage integration](https://docs.gitlab.com/ee/ci/testing/test_coverage_visualization.html) _(Cobertura format)_                                    |
-| [mocha-sonarqube-reporter](https://github.com/mmouterde/mocha-sonarqube-reporter)            | Yes                    | `reports/node-test.sonar.xml`    | [SonarQube unit tests integration](https://docs.sonarqube.org/latest/analysis/generic-test/) _(generic SonarQube format)_                                          |
-| istanbul [lcovonly](https://istanbul.js.org/docs/advanced/alternative-reporters/#lcovonly)   | Yes (in `nyc` package) | `reports/lcov.info`              | [SonarQube code coverage integration](https://docs.sonarqube.org/latest/analysis/test-coverage/javascript-typescript-test-coverage/) _(JS/TS LCOV format)_         |
+| [mocha-sonarqube-reporter](https://github.com/mmouterde/mocha-sonarqube-reporter)            | Yes                    | `reports/node-test.sonar.xml`    | [SonarQube unit tests integration](https://docs.sonarsource.com/sonarqube-server/latest/analyzing-source-code/test-coverage/generic-test-data/#generic-test-coverage) _(generic SonarQube format)_                                          |
+| istanbul [lcovonly](https://istanbul.js.org/docs/advanced/alternative-reporters/#lcovonly)   | Yes (in `nyc` package) | `reports/lcov.info`              | [SonarQube code coverage integration](https://docs.sonarsource.com/sonarqube-server/latest/analyzing-source-code/test-coverage/javascript-typescript-test-coverage/) _(JS/TS LCOV format)_         |
 
 :warning: Remarks:
 
@@ -288,7 +300,7 @@ If you're using the SonarQube template to analyse your Node code, here are 2 sam
 If using JavaScript language:
 
 ```properties
-# see: https://docs.sonarqube.org/latest/analyzing-source-code/test-coverage/javascript-typescript-test-coverage/
+# see: https://docs.sonarsource.com/sonarqube-server/latest/analyzing-source-code/test-coverage/javascript-typescript-test-coverage/
 # set your source directory(ies) here (relative to the sonar-project.properties file)
 sonar.sources=.
 # exclude unwanted directories and files from being analysed
@@ -309,7 +321,7 @@ sonar.javascript.lcov.reportPaths=reports/lcov.info
 If using TypeScript language:
 
 ```properties
-# see: https://docs.sonarqube.org/latest/analyzing-source-code/test-coverage/javascript-typescript-test-coverage/
+# see: https://docs.sonarsource.com/sonarqube-server/latest/analyzing-source-code/test-coverage/javascript-typescript-test-coverage/
 # set your source directory(ies) here (relative to the sonar-project.properties file)
 sonar.sources=src
 # exclude unwanted directories and files from being analysed
@@ -329,10 +341,10 @@ sonar.typescript.lcov.reportPaths=reports/lcov.info
 
 More info:
 
-- [JavaScript language support](https://docs.sonarqube.org/latest/analyzing-source-code/test-coverage/javascript-typescript-test-coverage/)
-- [TypeScript language support](https://docs.sonarqube.org/latest/analyzing-source-code/test-coverage/javascript-typescript-test-coverage/)
-- [test coverage & execution parameters](https://docs.sonarqube.org/latest/analysis/coverage/)
-- [third-party issues](https://docs.sonarqube.org/latest/analysis/external-issues/)
+- [JavaScript language support](https://docs.sonarsource.com/sonarqube-server/latest/analyzing-source-code/test-coverage/javascript-typescript-test-coverage/)
+- [TypeScript language support](https://docs.sonarsource.com/sonarqube-server/latest/analyzing-source-code/test-coverage/javascript-typescript-test-coverage/)
+- [test coverage](https://docs.sonarsource.com/sonarqube-server/latest/analyzing-source-code/test-coverage/test-coverage-parameters/) & [test execution](https://docs.sonarsource.com/sonarqube-server/latest/analyzing-source-code/test-coverage/test-execution-parameters/) parameters
+- [external analyzer reports](https://docs.sonarsource.com/sonarqube-server/latest/analyzing-source-code/importing-external-issues/external-analyzer-reports/)
 
 ### `node-audit` job
 
@@ -345,7 +357,7 @@ It is bound to the `test` stage.
 | `audit-disabled` / `NODE_AUDIT_DISABLED` | Set to `true` to disable npm audit                                                                                                                                                                          | _none_ (enabled)    |
 | `audit-args` / `NODE_AUDIT_ARGS`         | npm [audit](https://docs.npmjs.com/cli/v8/commands/npm-audit) arguments <br/> yarn [audit](https://classic.yarnpkg.com/en/docs/cli/audit) arguments <br/> pnpm [audit](https://pnpm.io/cli/audit) arguments | `--audit-level=low` |
 
-In addition to a textual report in the console, this job produces the following report, kept for one day:
+In addition to a textual report in the console, this job produces the following report, kept for one day and only available for download by users with the Developer role or higher:
 
 | Report                                            | Format                                                        | Usage                                                                                                                                                                                                                                  |
 | ------------------------------------------------- | ------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
@@ -364,7 +376,7 @@ It is bound to the `test` stage.
 | `outdated-args` / `NODE_OUTDATED_ARGS`         | npm [outdated](https://docs.npmjs.com/cli/v8/commands/npm-outdated) arguments <br/> yarn [outdated](https://classic.yarnpkg.com/lang/en/docs/cli/outdated/) arguments <br/> pnpm [outdated](https://pnpm.io/cli/outdated) arguments | `--long`         |
 | `node-outdated-job-tags` / `NODE_OUTDATED_JOB_TAGS` | Tags to be used for selecting runners for the job | [] |
 
-The job generates an outdated report that you will find here: `NODE_PROJECT_DIR/reports/npm-outdated-report.json`.
+The job generates an outdated report that you will find here: `NODE_PROJECT_DIR/reports/npm-outdated-report.json`. This report is only available for download by users with the Developer role or higher
 
 ### `node-semgrep` job
 
@@ -389,12 +401,12 @@ It is bound to the `test` stage, and uses the following variables:
 > - the `--metrics` option is set to `off`,
 > - the `--disable-version-check` option is set.
 
-In addition to a textual report in the console, this job produces the following reports, kept for one day:
+In addition to a textual report in the console, this job produces the following reports, kept for one week and only available for download by users with the Developer role or higher:
 
 | Report                                               | Format                                                                                       | Usage                                                                                                                                                                   |
 | ---------------------------------------------------- | -------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
 | `$NODE_PROJECT_DIR/reports/node-semgrep.gitlab.json` | [GitLab's SAST format](https://semgrep.dev/docs/cli-reference#semgrep-scan-command-options)  | [GitLab integration](https://docs.gitlab.com/ee/ci/yaml/artifacts_reports.html#artifactsreportssast)                                                                    |
-| `$NODE_PROJECT_DIR/reports/node-semgrep.native.json` | [Semgrep's JSON format](https://semgrep.dev/docs/cli-reference#semgrep-scan-command-options) | [DefectDojo integration](https://documentation.defectdojo.com/integrations/parsers/file/semgrep)<br/>_This report is generated only if DefectDojo template is detected_ |
+| `$NODE_PROJECT_DIR/reports/node-semgrep.native.json` | [Semgrep's JSON format](https://semgrep.dev/docs/cli-reference#semgrep-scan-command-options) | [DefectDojo integration](https://docs.defectdojo.com/en/connecting_your_tools/parsers/file/semgrep/)<br/>_This report is generated only if DefectDojo template is detected_ |
 
 ### `node-sbom` job
 
@@ -488,10 +500,12 @@ In order to be able to communicate with the Vault server, the variant requires t
 | Input / Variable                    | Description                                                                                                                     | Default value                                                              |
 | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
 | `TBC_VAULT_IMAGE`                   | The [Vault Secrets Provider](https://gitlab.com/to-be-continuous/tools/vault-secrets-provider) image to use (can be overridden) | `registry.gitlab.com/to-be-continuous/tools/vault-secrets-provider:latest` |
-| `vault-base-url` / `VAULT_BASE_URL` | The Vault server base API url                                                                                                   | _none_                                                                     |
+| `vault-base-url` / `VAULT_BASE_URL` | The Vault server base API url                                                                                                   | **must be defined**                                                        |
 | `vault-oidc-aud` / `VAULT_OIDC_AUD` | The `aud` claim for the JWT                                                                                                     | `$CI_SERVER_URL`                                                           |
-| :lock: `VAULT_ROLE_ID`              | The [AppRole](https://www.vaultproject.io/docs/auth/approle) RoleID                                                             | **must be defined**                                                        |
-| :lock: `VAULT_SECRET_ID`            | The [AppRole](https://www.vaultproject.io/docs/auth/approle) SecretID                                                           | **must be defined**                                                        |
+| :lock: `VAULT_ROLE_ID`              | The [AppRole](https://www.vaultproject.io/docs/auth/approle) RoleID                                                             | _none_                                                                     |
+| :lock: `VAULT_SECRET_ID`            | The [AppRole](https://www.vaultproject.io/docs/auth/approle) SecretID                                                           | _none_                                                                     |
+
+By default, the variant will authentifacte using a [JWT ID token](https://docs.gitlab.com/ee/ci/secrets/id_token_authentication.html). To use [AppRole](https://www.vaultproject.io/docs/auth/approle) instead the `VAULT_ROLE_ID` and `VAULT_SECRET_ID` should be defined as secret project variables.
 
 #### Usage
 
@@ -513,14 +527,13 @@ With:
 ```yaml
 include:
   # main template
-  - component: $CI_SERVER_FQDN/to-be-continuous/node/gitlab-ci-node@4.0.1
+  - component: $CI_SERVER_FQDN/to-be-continuous/node/gitlab-ci-node@4.1.1
   # Vault variant
-  - component: $CI_SERVER_FQDN/to-be-continuous/node/gitlab-ci-node-vault@4.0.1
+  - component: $CI_SERVER_FQDN/to-be-continuous/node/gitlab-ci-node-vault@4.1.1
     inputs:
       # audience claim for JWT
       vault-oidc-aud: "https://vault.acme.host"
       vault-base-url: "https://vault.acme.host/v1"
-      # $VAULT_ROLE_ID and $VAULT_SECRET_ID defined as a secret CI/CD variable
 
 variables:
   NODE_CONFIG_SCOPED_REGISTRIES: "@public-repo:https://public.npm.registry/some/repo @my-priv-repo:https://private.npm.registry/another/repo"
diff --git a/bumpversion.sh b/bumpversion.sh
index 329e866dac988c049574a0a9f26ba89979c523a8..708faf434d2459d63b2bdaceada5eb32b0fd39eb 100755
--- a/bumpversion.sh
+++ b/bumpversion.sh
@@ -27,7 +27,7 @@ if [[ "$curVer" ]]; then
   log_info "Bump version from \\e[33;1m${curVer}\\e[0m to \\e[33;1m${nextVer}\\e[0m (release type: $relType)..."
 
   # replace in README
-  sed -e "s/ref: *'$curVer'/ref: '$nextVer'/" -e "s/ref: *\"$curVer\”/ref: \”$nextVer\”/" -e "s/component: *\(.*\)@$curVer/component: \1@$nextVer/" README.md > README.md.next
+  sed -e "s/ref: *'$curVer'/ref: '$nextVer'/" -e "s/ref: *\"$curVer\"/ref: \"$nextVer\"/" -e "s/component: *\(.*\)@$curVer/component: \1@$nextVer/" README.md > README.md.next
   mv -f README.md.next README.md
 
   # replace in template and variants
diff --git a/kicker.json b/kicker.json
index 74f0e256402cbe8f3c66096f276db82ab8b23dad..0d2b302865f5abc8dfbe0f07a5e3534fe24a37db 100644
--- a/kicker.json
+++ b/kicker.json
@@ -84,7 +84,7 @@
     {
       "id": "node-lint",
       "name": "node lint",
-      "description": "node lint analysis",
+      "description": "code analysis with [ESLint](https://eslint.org/)",
       "enable_with": "NODE_LINT_ENABLED",
       "variables": [
         {
diff --git a/templates/gitlab-ci-node-vault.yml b/templates/gitlab-ci-node-vault.yml
index 43d5b1867c9cd7acebcd8f397038aa0db2c41316..deb0989104097a355d2934e0bc167b85180a2ba3 100644
--- a/templates/gitlab-ci-node-vault.yml
+++ b/templates/gitlab-ci-node-vault.yml
@@ -22,7 +22,7 @@ variables:
 .node-base:
   services:
     - name: "$TBC_TRACKING_IMAGE"
-      command: ["--service", "node", "4.0.1"]
+      command: ["--service", "node", "4.1.1"]
     - name: "$TBC_VAULT_IMAGE"
       alias: "vault-secrets-provider"
   variables:
diff --git a/templates/gitlab-ci-node.yml b/templates/gitlab-ci-node.yml
index 59be68dd36cc0180103f08af9b53133023f9af0e..fda4a34f10f50b5518db3261fc15d49a3a61eaf3 100644
--- a/templates/gitlab-ci-node.yml
+++ b/templates/gitlab-ci-node.yml
@@ -242,7 +242,7 @@ variables:
   # default integration ref name (pattern)
   INTEG_REF: /^develop$/
   # default release tag name (pattern)
-  RELEASE_REF: /^v?[0-9]+\.[0-9]+\.[0-9]+$/
+  RELEASE_REF: /^v?[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9-\.]+)?(\+[a-zA-Z0-9-\.]+)?$/
 
 # ==================================================
 # Stages definition
@@ -568,16 +568,6 @@ stages:
     fi
   }
 
-  function sonar_lint_report() {
-    if [[ "$SONAR_HOST_URL" ]] || [[ "$SONAR_URL" ]]
-    then
-      mkdir -p -m 777 reports
-      # generate eslint report in json for SonarQube
-      # shellcheck disable=SC2086
-      $NODE_MANAGER $NODE_LINT_ARGS -- --format=json --output-file=reports/node-lint.xslint.json
-    fi
-  }
-
   function configure_publish() {
     # get package scope+name, and target registry url
     pkg_fullname=$(node -pe "require('./package.json').name")
@@ -645,7 +635,7 @@ stages:
   image: $NODE_IMAGE
   services:
     - name: "$TBC_TRACKING_IMAGE"
-      command: ["--service", "node", "4.0.1"]
+      command: ["--service", "node", "4.1.1"]
   variables:
     # Yarn cache (better than --cache-folder option, deprecated)
     YARN_CACHE_FOLDER: "$CI_PROJECT_DIR/$NODE_PROJECT_DIR/.yarn"
@@ -719,16 +709,35 @@ node-lint:
   extends: .node-base
   stage: build
   script:
-    # generate lint report for sonar
-    - sonar_lint_report || true
-    # display lint result for console
-    - $NODE_MANAGER $NODE_LINT_ARGS
+    - mkdir -p -m 777 reports
+    # maybe generate ESLint report for SonarQube
+    - |
+      if [[ "$SONAR_HOST_URL" ]] || [[ "$SONAR_URL" ]]
+      then
+        # generate eslint report for SonarQube
+        # shellcheck disable=SC2086
+        log_info "SonarQube detected: producing ESLint JSON report..."
+        $NODE_MANAGER $NODE_LINT_ARGS -- --format=json --output-file=reports/node-lint.xslint.json || true
+      fi
+    # maybe add eslint-formatter-gitlab
+    - |
+      if ! $NODE_MANAGER list | grep eslint-formatter-gitlab > /dev/null
+      then
+        log_info "Adding eslint-formatter-gitlab to produce ESLint GitLab report..."
+        $NODE_MANAGER add eslint-formatter-gitlab
+      fi
+    # run ESLint with console output and GitLab report
+    # shellcheck disable=SC2086
+    - ESLINT_CODE_QUALITY_REPORT=reports/node-lint.gitlab.json $NODE_MANAGER $NODE_LINT_ARGS -- --format=gitlab
   artifacts:
     when: always # store artifact even if test Failed
     name: "$CI_JOB_NAME artifacts from $CI_PROJECT_NAME on $CI_COMMIT_REF_SLUG"
-    paths:
-      - $NODE_PROJECT_DIR/reports/node-lint.xslint.json
     expire_in: 1 day
+    paths:
+      - $NODE_PROJECT_DIR/reports/node-lint.*
+    reports:
+      codequality:
+        - $NODE_PROJECT_DIR/reports/node-lint.gitlab.json
   rules:
     # exclude if $NODE_LINT_ENABLED unset
     - if: '$NODE_LINT_ENABLED != "true"'
@@ -762,6 +771,7 @@ node-audit:
     paths:
       - $NODE_PROJECT_DIR/reports/npm-audit.*
     expire_in: 1 day
+    access: developer
   rules:
     # exclude if $NODE_AUDIT_DISABLED set
     - if: '$NODE_AUDIT_DISABLED == "true"'
@@ -787,6 +797,7 @@ node-outdated:
     paths:
       - $NODE_PROJECT_DIR/reports/npm-outdated.*
     expire_in: 1 day
+    access: developer
   rules:
     # exclude if $NODE_OUTDATED_DISABLED set
     - if: $NODE_OUTDATED_DISABLED == "true"
@@ -808,19 +819,19 @@ node-semgrep:
   dependencies: []
   stage: test
   before_script:
-    - *node-scripts
-    - cd $NODE_PROJECT_DIR
-    - mkdir -p -m 777 reports
+    - !reference [.node-scripts]
+    - mkdir -p -m 777 ${NODE_PROJECT_DIR}/reports
     - setup_semgrep_rules
   script:
     - >- 
-      semgrep ci ${TRACE+--verbose} ${NODE_SEMGREP_ARGS}
-      --gitlab-sast-output=reports/node-semgrep.gitlab.json
-      ${DEFECTDOJO_SEMGREP_REPORTS:+--json-output=reports/node-semgrep.native.json}
+      semgrep ci ${TRACE+--verbose} ${NODE_SEMGREP_ARGS} --subdir ${NODE_PROJECT_DIR}
+      --gitlab-sast-output=${NODE_PROJECT_DIR}/reports/node-semgrep.gitlab.json
+      ${DEFECTDOJO_SEMGREP_REPORTS:+--json-output=${NODE_PROJECT_DIR}/reports/node-semgrep.native.json}
   artifacts:
     name: "$CI_JOB_NAME artifacts from $CI_PROJECT_NAME on $CI_COMMIT_REF_SLUG"
     when: always
     expire_in: 1 week
+    access: developer
     reports:
       sast: $NODE_PROJECT_DIR/reports/node-semgrep.gitlab.json
     paths: