From 97b744ae6f8e1f3e4e5413827c91825b51b3528b Mon Sep 17 00:00:00 2001 From: nvuillam Date: Fri, 28 Aug 2026 12:42:02 +0200 Subject: [PATCH 1/4] Add ApexGuru engine of Salesforce Code Analyzer (SALESFORCE_CODE_ANALYZER_APEXGURU) ApexGuru is the AI-driven engine of Salesforce Code Analyzer: it sends Apex classes and triggers to a connected org, where an LLM combined with JVM runtime traces detects SOQL inefficiencies, anti-patterns and scalability hotspots that static analysis can not see. It needs an authenticated org, so it is inactive by default and activates only when SFDX_AUTH_URL is defined. This required a new activation rule type, "variable_is_set": existing activation rules can only compare a variable to a fixed expected value, which can not express "a credential is present". SFDX_AUTH_URL matches the default secured variables regexes, so the linter class allow-lists it for its own sub-processes and lets the login command expand it, keeping the auth url out of the logs. No extra installation: ApexGuru is an engine of the code-analyzer plugin already installed by the other Salesforce Code Analyzer linters. quick build TEST_KEYWORDS=salesforce_code_analyzer_apexguru_test --- .automation/build.py | 15 +- .automation/generated/linters_matrix.json | 1 + CHANGELOG.md | 6 + Dockerfile | 5 + docs/standalone-linters.md | 1 + flavors/salesforce/Dockerfile | 5 + flavors/salesforce/flavor.json | 1 + .../Dockerfile | 299 ++++++++++++++++++ mega-linter-runner/lib/megalinter-vars.json | 8 + megalinter/descriptors/all_flavors.json | 1 + .../salesforce.megalinter-descriptor.yml | 119 +++++++ .../megalinter-configuration.jsonschema.json | 1 + .../megalinter-custom-flavor.jsonschema.json | 1 + .../SalesforceCodeAnalyzerApexGuruLinter.py | 35 ++ .../salesforce_code_analyzer_apexguru_test.py | 14 + megalinter/utils.py | 13 + 16 files changed, 521 insertions(+), 4 deletions(-) create mode 100644 linters/salesforce_code_analyzer_apexguru/Dockerfile create mode 100644 megalinter/linters/SalesforceCodeAnalyzerApexGuruLinter.py create mode 100644 megalinter/tests/test_megalinter/linters/salesforce_code_analyzer_apexguru_test.py diff --git a/.automation/build.py b/.automation/build.py index cc684c85919..7280dae8f98 100644 --- a/.automation/build.py +++ b/.automation/build.py @@ -1655,10 +1655,17 @@ def process_type(linters_by_type, type1, type_label, linters_tables_md): ] if hasattr(linter, "activation_rules"): for rule in linter.activation_rules: - linter_doc_md += [ - f"| {rule['variable']} | For {linter.linter_name} to be active, {rule['variable']} must be " - f"`{rule['expected_value']}` | `{rule['default_value']}` |" - ] + rule_doc = ( + f"| {rule['variable']} | For {linter.linter_name} to be " + f"active, {rule['variable']} must be " + ) + if rule.get("type") == "variable_is_set": + rule_doc += "defined and not empty | _(not set)_ |" + else: + rule_doc += ( + f"`{rule['expected_value']}` | " f"`{rule['default_value']}` |" + ) + linter_doc_md += [rule_doc] if hasattr(linter, "variables"): for variable in linter.variables: linter_doc_md += [ diff --git a/.automation/generated/linters_matrix.json b/.automation/generated/linters_matrix.json index a3550a765aa..6be39914190 100644 --- a/.automation/generated/linters_matrix.json +++ b/.automation/generated/linters_matrix.json @@ -103,6 +103,7 @@ "ruby_rubocop", "rust_clippy", "salesforce_code_analyzer_apex", + "salesforce_code_analyzer_apexguru", "salesforce_code_analyzer_aura", "salesforce_code_analyzer_lwc", "salesforce_code_analyzer_flow", diff --git a/CHANGELOG.md b/CHANGELOG.md index adb69a5c162..27d9a7de779 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,12 @@ Note: Can be used with `oxsecurity/megalinter@beta` in your GitHub Action mega-l - Activated only when a **biome.json** or **biome.jsonc** configuration file is found in the repository - Supports **APPLY_FIXES** (safe fixes with `--write`) and native **SARIF** output - `EXCLUDED_DIRECTORIES` are forwarded in project lint mode through a generated configuration extending the workspace one + - **[ApexGuru](https://developer.salesforce.com/docs/platform/salesforce-code-analyzer/guide/engine-apexguru.html)**, the AI-driven engine of **Salesforce Code Analyzer**, available as **SALESFORCE_CODE_ANALYZER_APEXGURU** + - Detects **SOQL inefficiencies**, critical anti-patterns and scalability hotspots in your `.cls` and `.trigger` files, with line-level highlights, severity ratings and suggested fixes + - The analysis runs **in a connected Salesforce org**, not locally: store the [auth url](https://developer.salesforce.com/docs/platform/salesforce-cli-reference/guide/cli_reference_org_display.html) of the target org in a CI secret named **`SFDX_AUTH_URL`**, and MegaLinter logs in to that org before the scan + - **Inactive by default**: it activates only when `SFDX_AUTH_URL` is defined, so nothing changes for existing Salesforce projects + - Requires **ApexGuru to be enabled** on the org: it needs Scale Center, and is available for Unlimited Edition production orgs, full copy sandboxes, Signature orgs and Scale Test customers + - Supports native **SARIF** output, like the other Code Analyzer engines - **[tofu fmt](https://opentofu.org/docs/cli/commands/fmt/)**, the built-in formatter of **OpenTofu** (the MPL-2.0 licensed fork of Terraform), available as **TERRAFORM_TOFU_FMT** ([#8729](https://github.com/oxsecurity/megalinter/issues/8729)) - Analyzes **`.tofu`** files only, the OpenTofu specific extension, so it never doubles up with **TERRAFORM_TERRAFORM_FMT** which keeps `.tf` - To format your `.tf` files with OpenTofu instead, set `TERRAFORM_TOFU_FMT_FILE_EXTENSIONS: [".tofu", ".tf", ".tfvars"]` and `DISABLE_LINTERS: [TERRAFORM_TERRAFORM_FMT]` diff --git a/Dockerfile b/Dockerfile index e0a9a0e1ae9..e78d0355e4d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1203,6 +1203,11 @@ RUN curl -sSfL https://raw.githubusercontent.com/anchore/syft/refs/tags/v${REPOS && sf plugins install code-analyzer@${SALESFORCE_CODE_ANALYZER_VERSION} \ && (npm cache clean --force || true) \ && rm -rf /root/.npm/_cacache \ +# code-analyzer-apexguru installation +# Next line commented because already managed by another linter +# RUN sf plugins install code-analyzer@${SALESFORCE_CODE_ANALYZER_VERSION} \ +# && (npm cache clean --force || true) \ +# && rm -rf /root/.npm/_cacache # code-analyzer-aura installation # Next line commented because already managed by another linter # RUN sf plugins install code-analyzer@${SALESFORCE_CODE_ANALYZER_VERSION} \ diff --git a/docs/standalone-linters.md b/docs/standalone-linters.md index 2b6af971669..45dd7e7dd43 100644 --- a/docs/standalone-linters.md +++ b/docs/standalone-linters.md @@ -105,6 +105,7 @@ | RUBY_RUBOCOP | ghcr.io/oxsecurity/megalinter-only-ruby_rubocop:beta | ![Docker Image Size (tag)](https://img.shields.io/docker/image-size/oxsecurity/megalinter-only-ruby_rubocop/beta) | | RUST_CLIPPY | ghcr.io/oxsecurity/megalinter-only-rust_clippy:beta | ![Docker Image Size (tag)](https://img.shields.io/docker/image-size/oxsecurity/megalinter-only-rust_clippy/beta) | | SALESFORCE_CODE_ANALYZER_APEX | ghcr.io/oxsecurity/megalinter-only-salesforce_code_analyzer_apex:beta | ![Docker Image Size (tag)](https://img.shields.io/docker/image-size/oxsecurity/megalinter-only-salesforce_code_analyzer_apex/beta) | +| SALESFORCE_CODE_ANALYZER_APEXGURU | ghcr.io/oxsecurity/megalinter-only-salesforce_code_analyzer_apexguru:beta | ![Docker Image Size (tag)](https://img.shields.io/docker/image-size/oxsecurity/megalinter-only-salesforce_code_analyzer_apexguru/beta) | | SALESFORCE_CODE_ANALYZER_AURA | ghcr.io/oxsecurity/megalinter-only-salesforce_code_analyzer_aura:beta | ![Docker Image Size (tag)](https://img.shields.io/docker/image-size/oxsecurity/megalinter-only-salesforce_code_analyzer_aura/beta) | | SALESFORCE_CODE_ANALYZER_LWC | ghcr.io/oxsecurity/megalinter-only-salesforce_code_analyzer_lwc:beta | ![Docker Image Size (tag)](https://img.shields.io/docker/image-size/oxsecurity/megalinter-only-salesforce_code_analyzer_lwc/beta) | | SALESFORCE_CODE_ANALYZER_FLOW | ghcr.io/oxsecurity/megalinter-only-salesforce_code_analyzer_flow:beta | ![Docker Image Size (tag)](https://img.shields.io/docker/image-size/oxsecurity/megalinter-only-salesforce_code_analyzer_flow/beta) | diff --git a/flavors/salesforce/Dockerfile b/flavors/salesforce/Dockerfile index 77647c4b6a3..d66ab1daf28 100644 --- a/flavors/salesforce/Dockerfile +++ b/flavors/salesforce/Dockerfile @@ -583,6 +583,11 @@ RUN curl -sSfL https://raw.githubusercontent.com/anchore/syft/refs/tags/v${REPOS && sf plugins install code-analyzer@${SALESFORCE_CODE_ANALYZER_VERSION} \ && (npm cache clean --force || true) \ && rm -rf /root/.npm/_cacache +# code-analyzer-apexguru installation +# Next line commented because already managed by another linter +# RUN sf plugins install code-analyzer@${SALESFORCE_CODE_ANALYZER_VERSION} \ +# && (npm cache clean --force || true) \ +# && rm -rf /root/.npm/_cacache # code-analyzer-aura installation # Next line commented because already managed by another linter # RUN sf plugins install code-analyzer@${SALESFORCE_CODE_ANALYZER_VERSION} \ diff --git a/flavors/salesforce/flavor.json b/flavors/salesforce/flavor.json index 4b825380a24..8bf5cf942ff 100644 --- a/flavors/salesforce/flavor.json +++ b/flavors/salesforce/flavor.json @@ -48,6 +48,7 @@ "REPOSITORY_TRIVY_SBOM", "REPOSITORY_TRUFFLEHOG", "SALESFORCE_CODE_ANALYZER_APEX", + "SALESFORCE_CODE_ANALYZER_APEXGURU", "SALESFORCE_CODE_ANALYZER_AURA", "SALESFORCE_CODE_ANALYZER_LWC", "SALESFORCE_CODE_ANALYZER_FLOW", diff --git a/linters/salesforce_code_analyzer_apexguru/Dockerfile b/linters/salesforce_code_analyzer_apexguru/Dockerfile new file mode 100644 index 00000000000..229b1ac7016 --- /dev/null +++ b/linters/salesforce_code_analyzer_apexguru/Dockerfile @@ -0,0 +1,299 @@ +# syntax=docker/dockerfile:1 +########################################### +########################################### +## Dockerfile to run MegaLinter ## +########################################### +########################################### + +# @not-generated + +############################################################################################# +## @generated by .automation/build.py using descriptor files, please do not update manually ## +############################################################################################# +#ARGTOP__START + +#ARGTOP__END + +############################################################################################# +## @generated by .automation/build.py using descriptor files, please do not update manually ## +############################################################################################# +#FROM__START + +#FROM__END + +################## +# Build wheel for megalinter python package +################## +FROM ghcr.io/astral-sh/uv:0.12.3 AS uv +FROM python:3.14-alpine3.24 AS build-ml-core +RUN python -m pip install --no-cache-dir "wheel>=0.46.2" "setuptools>=75.8.0" \ + && rm -rf /usr/local/lib/python3.13/site-packages/setuptools/_vendor/wheel* +WORKDIR / +COPY --from=uv /uv /uvx /bin/ +# Install dependencies +#UV_SYNC__START +RUN --mount=type=cache,target=/root/.cache/uv \ + --mount=type=bind,source=uv.lock,target=uv.lock \ + --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ + uv sync --frozen --no-install-project +# Copy the project into the image +COPY . . +# Sync the project +RUN --mount=type=cache,target=/root/.cache/uv \ + uv sync --frozen +#UV_SYNC__END + +################## +# Get base image # +################## +FROM python:3.14-alpine3.24 +# Python 3.14.7 is the first release fixing the CPython stack limit check and the +# 128 KiB default thread stack size on musl (https://github.com/python/cpython/issues/148260), +# without which deep recursion in a thread segfaults MegaLinter instead of raising +# RecursionError. The base image tag floats, so assert the floor at build time. +RUN python -c 'import sys; assert sys.version_info >= (3, 14, 7), "Python 3.14.7+ required (musl thread stack fixes), got " + sys.version' \ + && python -m pip install --no-cache-dir "wheel>=0.46.2" "setuptools>=75.8.0" \ + && rm -rf /usr/local/lib/python3.13/site-packages/setuptools/_vendor/wheel* + +############################################################################################# +## @generated by .automation/build.py using descriptor files, please do not update manually ## +############################################################################################# +#ARG__START +ARG TARGETPLATFORM +ARG TARGETARCH +# renovate: datasource=npm depName=@salesforce/cli +ARG NPM_SALESFORCE_CLI_VERSION=2.148.3 +# renovate: datasource=npm depName=@salesforce/plugin-packaging +ARG NPM_SALESFORCE_PLUGIN_PACKAGING_VERSION=3.0.5 +# renovate: datasource=npm depName=sfdx-hardis +ARG SFDX_HARDIS_VERSION=7.23.0 +# renovate: datasource=npm depName=@salesforce/plugin-code-analyzer +ARG SALESFORCE_CODE_ANALYZER_VERSION=5.15.0 +#ARG__END + +#################### +# Run APK installs # +#################### + +WORKDIR / +COPY --from=uv /uv /uvx /bin/ + +############################################################################################# +## @generated by .automation/build.py using descriptor files, please do not update manually ## +############################################################################################# +#APK__START +RUN apk -U --no-cache upgrade \ + && apk add --no-cache \ + bash \ + ca-certificates \ + curl \ + gcompat \ + git \ + git-lfs \ + libgcc \ + libstdc++ \ + openssh \ + su-exec \ + coreutils \ + openjdk21 \ + npm \ + nodejs-current \ + yarn \ + && git config --global core.autocrlf true +#APK__END + +# PATH for golang & python +ENV GOROOT=/usr/lib/go \ + GOPATH=/go + # PYTHONPYCACHEPREFIX="$HOME/.cache/cpython/" NV: not working for all packages :/ +# hadolint ignore=DL3044 +ENV PATH="$PATH":"$GOROOT"/bin:"$GOPATH"/bin +RUN mkdir -p ${GOPATH}/src ${GOPATH}/bin || true && \ + # Ignore npm package issues + yarn config set ignore-engines true || true + +############################## +# Installs rust dependencies # +############################################################################################# +## @generated by .automation/build.py using descriptor files, please do not update manually ## +############################################################################################# + +#CARGO__START + +#CARGO__END + +############################## +# COPY instructions # +############################################################################################# +## @generated by .automation/build.py using descriptor files, please do not update manually ## +############################################################################################# + +#COPY__START + +#COPY__END + +############################## +# Installs ruby dependencies # +############################################################################################# +## @generated by .automation/build.py using descriptor files, please do not update manually ## +############################################################################################# + +#GEM__START + +#GEM__END + +################################ +# Installs python dependencies # +############################################################################################# +## @generated by .automation/build.py using descriptor files, please do not update manually ## +############################################################################################# + +#PIPVENV__START + +#PIPVENV__END + +############################ +# Install NPM dependencies # +############################################################################################# +## @generated by .automation/build.py using descriptor files, please do not update manually ## +############################################################################################# + +ENV NODE_OPTIONS="--max-old-space-size=8192" \ + NODE_ENV=production +#NPM__START +WORKDIR /node-deps +RUN npm config set prefix /usr/local \ + && npm --no-cache install --ignore-scripts --omit=dev \ + @salesforce/cli@${NPM_SALESFORCE_CLI_VERSION} && \ + echo "Cleaning npm cache…" \ + && (npm cache clean --force || true) \ + && echo "Changing owner of node_modules files…" \ + && chown -R "$(id -u)":"$(id -g)" node_modules # fix for https://github.com/npm/cli/issues/5900 \ + && echo "Removing extra node_module files…" \ + && find . \( -not -path "/proc" \) -and \( -type f \( -iname "*.d.ts" -o -iname "*.map" -o -iname "*.npmignore" -o -iname "*.travis.yml" -o -iname "*.md" -o -iname "*.markdown" -o -iname ".package-lock.json" -o -iname "package-lock.json" \) \) -delete \ + && echo "Removing test and doc directories from node_modules…" \ + && find ./node_modules -type d \( -iname "__tests__" -o -iname "test" -o -iname "tests" -o -iname "docs" -o -iname ".github" \) -prune -exec rm -rf {} + \ + && rm -rf /root/.npm +WORKDIR / + +#NPM__END + +# Add node packages to path # +ENV PATH="/node-deps/node_modules/.bin:${PATH}" \ + NODE_PATH="/node-deps/node_modules" + +############################################################################################# +## @generated by .automation/build.py using descriptor files, please do not update manually ## +############################################################################################# +#OTHER__START +# SALESFORCE installation +ENV JAVA_HOME=/usr/lib/jvm/java-21-openjdk +ENV PATH="$JAVA_HOME/bin:${PATH}" +ENV XDG_DATA_HOME=/usr/local/share +RUN sf plugins install @salesforce/plugin-packaging@${NPM_SALESFORCE_PLUGIN_PACKAGING_VERSION} \ + && echo y|sf plugins install sfdx-hardis@${SFDX_HARDIS_VERSION} \ + && (npm cache clean --force || true) \ + && rm -rf /root/.npm/_cacache +ENV SF_AUTOUPDATE_DISABLE=true SF_CLI_DISABLE_AUTOUPDATE=true +# code-analyzer-apexguru installation +RUN sf plugins install code-analyzer@${SALESFORCE_CODE_ANALYZER_VERSION} \ + && (npm cache clean --force || true) \ + && rm -rf /root/.npm/_cacache +#OTHER__END + +################################ +# Installs python dependencies # +################################ +COPY --from=build-ml-core pyproject.toml README.md ./ +COPY --from=build-ml-core megalinter /megalinter/ +#PIP_PROJECT__START +RUN --mount=type=cache,target=/root/.cache/uv,from=build-ml-core \ + --mount=from=uv,source=/uv,target=/bin/uv \ + uv pip install --system -e . +#PIP_PROJECT__END + +####################################### +# Copy scripts and rules to container # +####################################### +COPY megalinter/descriptors /megalinter-descriptors +# Linter versions collected at build time, so runtime does not need to spawn +# one "--version" process per linter (see VERSION_GET_AT_RUNTIME variable) +COPY .automation/generated/linter-versions.json /megalinter-descriptors/linter-versions.json +COPY TEMPLATES /action/lib/.automation + +# Copy server scripts +COPY server /server + +########################### +# Get the build arguments # +########################### +ARG BUILD_DATE +ARG BUILD_REVISION +ARG BUILD_VERSION + +################################################# +# Set ENV values used for debugging the version # +################################################# +ENV BUILD_DATE=$BUILD_DATE \ + BUILD_REVISION=$BUILD_REVISION \ + BUILD_VERSION=$BUILD_VERSION + +#FLAVOR__START +ENV MEGALINTER_FLAVOR=none +#FLAVOR__END + +######################################### +# Label the instance and set maintainer # +######################################### +LABEL com.github.actions.name="MegaLinter" \ + com.github.actions.description="The ultimate linters aggregator to make sure your projects are clean" \ + com.github.actions.icon="code" \ + com.github.actions.color="red" \ + maintainer="Nicolas Vuillamy " \ + org.opencontainers.image.created=$BUILD_DATE \ + org.opencontainers.image.revision=$BUILD_REVISION \ + org.opencontainers.image.version=$BUILD_VERSION \ + org.opencontainers.image.authors="Nicolas Vuillamy " \ + org.opencontainers.image.url="https://megalinter.io" \ + org.opencontainers.image.source="https://github.com/oxsecurity/megalinter" \ + org.opencontainers.image.documentation="https://megalinter.io" \ + org.opencontainers.image.vendor="Nicolas Vuillamy" \ + org.opencontainers.image.description="Lint your code base with GitHub Actions" + +#EXTRA_DOCKERFILE_LINES__START +ENV ENABLE_LINTERS=SALESFORCE_CODE_ANALYZER_APEXGURU \ + FLAVOR_SUGGESTIONS=false \ + SINGLE_LINTER=SALESFORCE_CODE_ANALYZER_APEXGURU \ + PRINT_ALPACA=false \ + LOG_FILE=none \ + SARIF_REPORTER=true \ + TEXT_REPORTER=false \ + UPDATED_SOURCES_REPORTER=false \ + GITHUB_STATUS_REPORTER=false \ + GITHUB_COMMENT_REPORTER=false \ + EMAIL_REPORTER=false \ + API_REPORTER=false \ + FILEIO_REPORTER=false \ + CONFIG_REPORTER=false \ + SARIF_TO_HUMAN=false +RUN mkdir /tmp/docker_ssh && mkdir /usr/bin/megalinter-sh +EXPOSE 22 +COPY entrypoint.sh /entrypoint.sh +COPY sh /usr/bin/megalinter-sh +COPY sh/megalinter_exec.sh /usr/bin/megalinter_exec.sh +COPY sh/setup-runtime-user.sh /usr/bin/setup-runtime-user.sh +COPY sh/motd /etc/motd +RUN find /usr/bin/megalinter-sh/ -type f -iname "*.sh" -exec chmod +x {} \; && \ + chmod +x entrypoint.sh && \ + chmod +x /usr/bin/megalinter_exec.sh && \ + chmod u+x /usr/bin/setup-runtime-user.sh && \ + printf '%s\n' \ + '#!/usr/bin/env sh' \ + 'exec python -m megalinter.run "$@"' \ + > /usr/local/bin/megalinter && \ + chmod u+x /usr/local/bin/megalinter && \ + ln -sf /usr/bin/megalinter_exec.sh /usr/local/bin/megalinter_exec +RUN export PYTHONDONTWRITEBYTECODE=1 && export STANDALONE_LINTER_VERSION="$(python -m megalinter.run --input /tmp --linterversion)" && \ + echo $STANDALONE_LINTER_VERSION +ENTRYPOINT ["/bin/bash", "/entrypoint.sh"] +#EXTRA_DOCKERFILE_LINES__END diff --git a/mega-linter-runner/lib/megalinter-vars.json b/mega-linter-runner/lib/megalinter-vars.json index f0a575346cb..ff411d91d0a 100644 --- a/mega-linter-runner/lib/megalinter-vars.json +++ b/mega-linter-runner/lib/megalinter-vars.json @@ -1545,6 +1545,7 @@ "RUST_CLIPPY", "R_LINTR", "SALESFORCE_CODE_ANALYZER_APEX", + "SALESFORCE_CODE_ANALYZER_APEXGURU", "SALESFORCE_CODE_ANALYZER_AURA", "SALESFORCE_CODE_ANALYZER_FLOW", "SALESFORCE_CODE_ANALYZER_LWC", @@ -7087,6 +7088,7 @@ "RUST_CLIPPY", "R_LINTR", "SALESFORCE_CODE_ANALYZER_APEX", + "SALESFORCE_CODE_ANALYZER_APEXGURU", "SALESFORCE_CODE_ANALYZER_AURA", "SALESFORCE_CODE_ANALYZER_FLOW", "SALESFORCE_CODE_ANALYZER_LWC", @@ -7264,6 +7266,7 @@ "RUST_CLIPPY", "R_LINTR", "SALESFORCE_CODE_ANALYZER_APEX", + "SALESFORCE_CODE_ANALYZER_APEXGURU", "SALESFORCE_CODE_ANALYZER_AURA", "SALESFORCE_CODE_ANALYZER_FLOW", "SALESFORCE_CODE_ANALYZER_LWC", @@ -8250,6 +8253,7 @@ "RUST_CLIPPY", "R_LINTR", "SALESFORCE_CODE_ANALYZER_APEX", + "SALESFORCE_CODE_ANALYZER_APEXGURU", "SALESFORCE_CODE_ANALYZER_AURA", "SALESFORCE_CODE_ANALYZER_FLOW", "SALESFORCE_CODE_ANALYZER_LWC", @@ -8427,6 +8431,7 @@ "RUST_CLIPPY", "R_LINTR", "SALESFORCE_CODE_ANALYZER_APEX", + "SALESFORCE_CODE_ANALYZER_APEXGURU", "SALESFORCE_CODE_ANALYZER_AURA", "SALESFORCE_CODE_ANALYZER_FLOW", "SALESFORCE_CODE_ANALYZER_LWC", @@ -15653,6 +15658,7 @@ "RUST_CLIPPY", "R_LINTR", "SALESFORCE_CODE_ANALYZER_APEX", + "SALESFORCE_CODE_ANALYZER_APEXGURU", "SALESFORCE_CODE_ANALYZER_AURA", "SALESFORCE_CODE_ANALYZER_FLOW", "SALESFORCE_CODE_ANALYZER_LWC", @@ -15843,6 +15849,7 @@ "RUST_CLIPPY", "R_LINTR", "SALESFORCE_CODE_ANALYZER_APEX", + "SALESFORCE_CODE_ANALYZER_APEXGURU", "SALESFORCE_CODE_ANALYZER_AURA", "SALESFORCE_CODE_ANALYZER_FLOW", "SALESFORCE_CODE_ANALYZER_LWC", @@ -29472,6 +29479,7 @@ "RUST_CLIPPY", "R_LINTR", "SALESFORCE_CODE_ANALYZER_APEX", + "SALESFORCE_CODE_ANALYZER_APEXGURU", "SALESFORCE_CODE_ANALYZER_AURA", "SALESFORCE_CODE_ANALYZER_FLOW", "SALESFORCE_CODE_ANALYZER_LWC", diff --git a/megalinter/descriptors/all_flavors.json b/megalinter/descriptors/all_flavors.json index 728ea631711..8264072a46e 100644 --- a/megalinter/descriptors/all_flavors.json +++ b/megalinter/descriptors/all_flavors.json @@ -1021,6 +1021,7 @@ "REPOSITORY_TRIVY_SBOM", "REPOSITORY_TRUFFLEHOG", "SALESFORCE_CODE_ANALYZER_APEX", + "SALESFORCE_CODE_ANALYZER_APEXGURU", "SALESFORCE_CODE_ANALYZER_AURA", "SALESFORCE_CODE_ANALYZER_LWC", "SALESFORCE_CODE_ANALYZER_FLOW", diff --git a/megalinter/descriptors/salesforce.megalinter-descriptor.yml b/megalinter/descriptors/salesforce.megalinter-descriptor.yml index 48867e2f94c..9d51ee99694 100644 --- a/megalinter/descriptors/salesforce.megalinter-descriptor.yml +++ b/megalinter/descriptors/salesforce.megalinter-descriptor.yml @@ -145,6 +145,125 @@ linters: - name: Salesforce Extension Pack url: https://marketplace.visualstudio.com/items?itemName=salesforce.salesforcedx-vscode + # Code Analyzer ApexGuru + - class: SalesforceCodeAnalyzerApexGuruLinter + linter_name: code-analyzer-apexguru + can_output_sarif: true + name: SALESFORCE_CODE_ANALYZER_APEXGURU + files_sub_directory: force-app + activation_rules: + - type: variable_is_set + variable: SFDX_AUTH_URL + linter_text: | + **ApexGuru** is the AI-driven engine of Salesforce Code Analyzer. Unlike the other engines, it does not analyze your sources locally: it sends your Apex classes and triggers to a connected Salesforce org, where an LLM combined with Java Virtual Machine runtime traces detects SOQL inefficiencies, critical anti-patterns and scalability hotspots that static analysis alone can not see. + + **Key Features:** + + - **Runtime-Aware Analysis**: Correlates your Apex sources with JVM traces collected in the org to surface issues that only appear at execution time + - **Performance & Scalability**: Detects SOQL inefficiencies, expensive schema describe calls and governor-limit hotspots + - **Actionable Recommendations**: Reports exact line-level highlights, severity ratings and suggested code changes (`--include-suggestions`) + - **Unified Toolchain**: Runs via the same Salesforce Code Analyzer v5 plugin and `code-analyzer.yml` configuration as the Apex, Aura, Flow and LWC engines + - **Multiple Output Formats**: Supports CSV, JSON, HTML, XML and SARIF output + + **Requirements** + + ApexGuru only analyzes `.cls` and `.trigger` files, and it requires a connected org where ApexGuru is enabled. It is available at no extra charge for Unlimited Edition production orgs, full copy sandboxes, Signature orgs and Scale Test customers, and requires Scale Center to be enabled. See [Activate and Use ApexGuru](https://help.salesforce.com/s/articleView?id=xcloud.apexguru_workflow.htm&type=5). + + **Activation** + + Because it needs an authenticated org, this linter is **inactive by default**: it activates only when the `SFDX_AUTH_URL` variable is defined. Store the [Salesforce DX auth url](https://developer.salesforce.com/docs/platform/salesforce-cli-reference/guide/cli_reference_org_display.html) of the target org in a **CI secret** named `SFDX_AUTH_URL`, and MegaLinter logs in to that org before running ApexGuru. + + **Configuration** + + Engine settings (`target_org`, `api_timeout_ms`, `api_initial_retry_ms`, `api_max_retry_ms`, `api_backoff_multiplier`) go under `engines.apexguru` in your `code-analyzer.yml`. + + If your root folder is not **force-app**, please set variable `SALESFORCE_CODE_ANALYZER_APEXGURU_DIRECTORY` + + See more details in [Help](#help-content) + linter_url: https://developer.salesforce.com/docs/platform/salesforce-code-analyzer/guide/engine-apexguru.html + linter_repo: https://github.com/forcedotcom/code-analyzer + linter_rules_url: https://developer.salesforce.com/docs/platform/salesforce-code-analyzer/guide/engine-apexguru.html + linter_rules_configuration_url: https://developer.salesforce.com/docs/platform/salesforce-code-analyzer/guide/config.html + linter_spdx_license: BSD-3-Clause + # Remote analysis in the connected org, polled until it completes + # (5 minutes default timeout): by far the slowest Code Analyzer engine + linter_speed: 1 + config_file_name: code-analyzer.yml + cli_executable: sf + cli_config_arg_name: "--config-file" + cli_lint_extra_args: + - "code-analyzer" + - "run" + - "--rule-selector" + - "apexguru" + - "--workspace" + - "." + - "--include-suggestions" + - "--severity-threshold" + - "Moderate" + - "--output-file" + - "{{REPORT_FOLDER}}/code-analyzer-report-apexguru.csv" + - "--view" + - "table" + cli_lint_mode: project + supported_cli_lint_modes: + - project + cli_help_extra_args: + - "code-analyzer" + - "run" + cli_version_extra_args: + - "plugins" + cli_version_arg_name: "" + version_extract_regex: "(?<=code-analyzer )\\d+(\\.\\d+)+" + cli_lint_errors_count: regex_number + cli_lint_errors_regex: "Found ([0-9]+) violation\\(s\\)" + common_linter_errors: + - identifier: SALESFORCE_CODE_ANALYZER_APEXGURU_ERROR_CONFIG_INVALID + regex: "(Failed to parse the configuration content|The specified configuration file .* does not exist|The specified configuration file .* has an unsupported file extension|The configuration content is invalid)" + message: |- + code-analyzer could not load the configuration file (code-analyzer.yml). + Verify the file is valid v5 YAML and that its path is reachable from the workspace. + Generate a starter config with: sf code-analyzer config -f code-analyzer.yml + - identifier: SALESFORCE_CODE_ANALYZER_APEXGURU_ERROR_NO_ORG + regex: "(No authorization information found for|No default (environment|org) found|NoDefaultEnvError|Unable to (find|resolve) the target org|The org .* is not authenticated)" + message: |- + ApexGuru needs an authenticated Salesforce org and could not resolve one. + Verify that the SFDX_AUTH_URL variable holds a valid Salesforce DX auth url, obtained with: sf org display --target-org --verbose --json + You can also select another org with the `target_org` setting of the apexguru engine in your code-analyzer.yml + - identifier: SALESFORCE_CODE_ANALYZER_APEXGURU_ERROR_NOT_ENABLED + regex: "(ApexGuru is (not|un)available|ApexGuru .* not enabled|not entitled to use ApexGuru|Scale Center .* not enabled)" + message: |- + ApexGuru is not enabled on the connected org. + It requires Scale Center and is available for Unlimited Edition production orgs, full copy sandboxes, Signature orgs and Scale Test customers. + Enable it by following https://help.salesforce.com/s/articleView?id=xcloud.apexguru_workflow.htm&type=5 , or disable this linter with DISABLE_LINTERS: [SALESFORCE_CODE_ANALYZER_APEXGURU] + - identifier: SALESFORCE_CODE_ANALYZER_APEXGURU_ERROR_TIMEOUT + regex: "(ApexGuru .* timed out|timed out after .* milliseconds|api_timeout_ms)" + message: |- + The ApexGuru analysis did not complete before the engine timeout (5 minutes by default). + Raise the `api_timeout_ms` setting of the apexguru engine in your code-analyzer.yml, or reduce the analyzed scope with FILTER_REGEX_EXCLUDE. + examples: + - "sf code-analyzer run --rule-selector apexguru --workspace . --include-suggestions --output-file results.csv" + install: + dockerfile: + - |- + # renovate: datasource=npm depName=@salesforce/plugin-code-analyzer + ARG SALESFORCE_CODE_ANALYZER_VERSION=5.15.0 + - |- + RUN sf plugins install code-analyzer@${SALESFORCE_CODE_ANALYZER_VERSION} \ + && (npm cache clean --force || true) \ + && rm -rf /root/.npm/_cacache + supported_platforms: + platform: + - linux/amd64 + # Errors when building the image + #- linux/arm64 + ide: + vscode: + - name: Salesforce Code Analyzer + url: https://marketplace.visualstudio.com/items?itemName=salesforce.sfdx-code-analyzer-vscode + - name: Salesforce Extension Pack + url: https://marketplace.visualstudio.com/items?itemName=salesforce.salesforcedx-vscode + # Code Analyzer Aura - class: SalesforceCodeAnalyzerLinter linter_name: code-analyzer-aura diff --git a/megalinter/descriptors/schemas/megalinter-configuration.jsonschema.json b/megalinter/descriptors/schemas/megalinter-configuration.jsonschema.json index 576a86e9a3e..f9afebdaf64 100644 --- a/megalinter/descriptors/schemas/megalinter-configuration.jsonschema.json +++ b/megalinter/descriptors/schemas/megalinter-configuration.jsonschema.json @@ -409,6 +409,7 @@ "RUST_CLIPPY", "R_LINTR", "SALESFORCE_CODE_ANALYZER_APEX", + "SALESFORCE_CODE_ANALYZER_APEXGURU", "SALESFORCE_CODE_ANALYZER_AURA", "SALESFORCE_CODE_ANALYZER_FLOW", "SALESFORCE_CODE_ANALYZER_LWC", diff --git a/megalinter/descriptors/schemas/megalinter-custom-flavor.jsonschema.json b/megalinter/descriptors/schemas/megalinter-custom-flavor.jsonschema.json index 886c40d9fea..c19f55c37af 100644 --- a/megalinter/descriptors/schemas/megalinter-custom-flavor.jsonschema.json +++ b/megalinter/descriptors/schemas/megalinter-custom-flavor.jsonschema.json @@ -126,6 +126,7 @@ "RUST_CLIPPY", "R_LINTR", "SALESFORCE_CODE_ANALYZER_APEX", + "SALESFORCE_CODE_ANALYZER_APEXGURU", "SALESFORCE_CODE_ANALYZER_AURA", "SALESFORCE_CODE_ANALYZER_FLOW", "SALESFORCE_CODE_ANALYZER_LWC", diff --git a/megalinter/linters/SalesforceCodeAnalyzerApexGuruLinter.py b/megalinter/linters/SalesforceCodeAnalyzerApexGuruLinter.py new file mode 100644 index 00000000000..e978fda9f36 --- /dev/null +++ b/megalinter/linters/SalesforceCodeAnalyzerApexGuruLinter.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +""" +ApexGuru engine of Salesforce Code Analyzer +https://developer.salesforce.com/docs/platform/salesforce-code-analyzer/guide/engine-apexguru.html +""" + +import logging + +from megalinter.linters.SalesforceCodeAnalyzerLinter import ( + SalesforceCodeAnalyzerLinter, +) + +SFDX_AUTH_URL_VAR = "SFDX_AUTH_URL" + + +class SalesforceCodeAnalyzerApexGuruLinter(SalesforceCodeAnalyzerLinter): + # Unlike the other Code Analyzer engines, ApexGuru runs server-side: it + # sends the Apex sources to a connected org and needs an authenticated + # default org. SFDX_AUTH_URL (the same variable that activates this linter) + # holds a Salesforce DX auth url, so log in with it before linting. + # SFDX_AUTH_URL matches the default secured variables regexes, so it is + # replaced by HIDDEN_BY_MEGALINTER in sub-process environments unless it is + # explicitly allowed: the auth url is expanded by the login command itself, + # never by MegaLinter, so it stays out of the logs. + def before_lint_files(self): + if SFDX_AUTH_URL_VAR not in self.unsecured_env_variables: + self.unsecured_env_variables += [SFDX_AUTH_URL_VAR] + login_command = ( + f'echo "${SFDX_AUTH_URL_VAR}" | sf org login sfdx-url' + " --sfdx-url-stdin --set-default --alias megalinter-apexguru" + ) + logging.debug("apexguru before_lint_files: " + login_command) + if self.pre_commands is None: + self.pre_commands = [] + self.pre_commands.append({"command": login_command, "cwd": "root"}) diff --git a/megalinter/tests/test_megalinter/linters/salesforce_code_analyzer_apexguru_test.py b/megalinter/tests/test_megalinter/linters/salesforce_code_analyzer_apexguru_test.py new file mode 100644 index 00000000000..488ff5efe41 --- /dev/null +++ b/megalinter/tests/test_megalinter/linters/salesforce_code_analyzer_apexguru_test.py @@ -0,0 +1,14 @@ +# !/usr/bin/env python3 +""" +Unit tests for SALESFORCE linter code-analyzer-apexguru +This class has been automatically @generated by .automation/build.py, please don't update it manually +""" + +from unittest import TestCase + +from megalinter.tests.test_megalinter.LinterTestRoot import LinterTestRoot + + +class salesforce_code_analyzer_apexguru_test(TestCase, LinterTestRoot): + descriptor_id = "SALESFORCE" + linter_name = "code-analyzer-apexguru" diff --git a/megalinter/utils.py b/megalinter/utils.py index 80fc2b7a4c0..0365fdef6e3 100644 --- a/megalinter/utils.py +++ b/megalinter/utils.py @@ -447,6 +447,19 @@ def check_activation_rules(activation_rules, linter): f"(set {rule['variable']}={rule['expected_value']} to activate)" ) break + # For linters requiring a credential or a connection string, the value + # can not be known in advance: activate as soon as the variable is set + elif rule["type"] == "variable_is_set": + value = config.get(linter.request_id, rule["variable"], "") + if value != "": + active = True + else: + active = False + reason = ( + f"{rule['variable']} is not set " + f"(define {rule['variable']} to activate)" + ) + break return active, reason From a17bffa62d834256c2e1e5cff23259fe89ad9552 Mon Sep 17 00:00:00 2001 From: nvuillam Date: Fri, 28 Aug 2026 12:48:03 +0200 Subject: [PATCH 2/4] Skip ApexGuru tests when SFDX_AUTH_URL is not available Forward the SFDX_AUTH_URL repository secret to the test containers of the DEV, DEV-linters and BETA-linters workflows, so the SALESFORCE_CODE_ANALYZER_APEXGURU lint tests can reach a connected org. GitHub does not expose repository secrets to pull request jobs from a forked repository, so the variable is empty there. Rather than failing, a linter whose activation depends on a "variable_is_set" rule now skips its lint tests when the variable is missing: LinterTestRoot.skip_if_required_variables_missing() guards the per-lint-mode and SARIF tests. The version and help tests keep running in every case: they only call the CLI and need no credential. quick build TEST_KEYWORDS=salesforce_code_analyzer_apexguru_test --- .github/workflows/deploy-BETA-linters.yml | 5 ++++- .github/workflows/deploy-DEV-linters.yml | 5 ++++- .github/workflows/deploy-DEV.yml | 5 ++++- CHANGELOG.md | 3 +++ .../tests/test_megalinter/LinterTestRoot.py | 17 +++++++++++++++++ 5 files changed, 32 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy-BETA-linters.yml b/.github/workflows/deploy-BETA-linters.yml index 60b0dc75f7c..bf49ac0e08b 100644 --- a/.github/workflows/deploy-BETA-linters.yml +++ b/.github/workflows/deploy-BETA-linters.yml @@ -174,6 +174,9 @@ jobs: DOCKER_IMAGE: ghcr.io/${{ github.repository }}-only-${{ matrix.linter }}@${{ steps.build.outputs.digest }} GH_SHA: ${{ github.sha }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # Salesforce org auth url used by SALESFORCE_CODE_ANALYZER_APEXGURU. + # Empty on pull requests from forks: the ApexGuru tests then skip themselves + SFDX_AUTH_URL: ${{ secrets.SFDX_AUTH_URL }} run: | GITHUB_REPOSITORY=$([ "$EVENT_NAME" == "pull_request" ] && echo "$PR_HEAD_REPO" || echo "$GH_REPOSITORY") GITHUB_BRANCH=$([ "$EVENT_NAME" == "pull_request" ] && echo "$HEAD_REF" || echo "$REF_NAME") @@ -181,7 +184,7 @@ jobs: TEST_KEYWORDS_TO_USE_UPPER="${MATRIX_LINTER}" TEST_KEYWORDS_TO_USE="${TEST_KEYWORDS_TO_USE_UPPER,,}" docker image ls - docker run -e TEST_CASE_RUN=true -e OUTPUT_FORMAT=text -e OUTPUT_FOLDER="${GH_SHA}" -e OUTPUT_DETAIL=detailed -e GITHUB_SHA="${GH_SHA}" -e GITHUB_REPOSITORY="${GITHUB_REPOSITORY}" -e GITHUB_BRANCH="${GITHUB_BRANCH}" -e GITHUB_TOKEN="${GITHUB_TOKEN}" -e TEST_KEYWORDS="${TEST_KEYWORDS_TO_USE}" -e MEGALINTER_VOLUME_ROOT="${GITHUB_WORKSPACE}" -v "/var/run/docker.sock:/var/run/docker.sock:rw" -v "${GITHUB_WORKSPACE}:/tmp/lint" "${DOCKER_IMAGE}" + docker run -e TEST_CASE_RUN=true -e OUTPUT_FORMAT=text -e OUTPUT_FOLDER="${GH_SHA}" -e OUTPUT_DETAIL=detailed -e GITHUB_SHA="${GH_SHA}" -e GITHUB_REPOSITORY="${GITHUB_REPOSITORY}" -e GITHUB_BRANCH="${GITHUB_BRANCH}" -e GITHUB_TOKEN="${GITHUB_TOKEN}" -e SFDX_AUTH_URL="${SFDX_AUTH_URL}" -e TEST_KEYWORDS="${TEST_KEYWORDS_TO_USE}" -e MEGALINTER_VOLUME_ROOT="${GITHUB_WORKSPACE}" -v "/var/run/docker.sock:/var/run/docker.sock:rw" -v "${GITHUB_WORKSPACE}:/tmp/lint" "${DOCKER_IMAGE}" timeout-minutes: 30 # Docker Hub mirroring is disabled — MegaLinter is published to ghcr.io only. diff --git a/.github/workflows/deploy-DEV-linters.yml b/.github/workflows/deploy-DEV-linters.yml index 0f1153db2b5..c82c2514960 100644 --- a/.github/workflows/deploy-DEV-linters.yml +++ b/.github/workflows/deploy-DEV-linters.yml @@ -322,6 +322,9 @@ jobs: DOCKER_IMAGE: ${{ fromJson(steps.meta.outputs.json).tags[0] }} GH_SHA: ${{ github.sha }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # Salesforce org auth url used by SALESFORCE_CODE_ANALYZER_APEXGURU. + # Empty on pull requests from forks: the ApexGuru tests then skip themselves + SFDX_AUTH_URL: ${{ secrets.SFDX_AUTH_URL }} run: | GITHUB_REPOSITORY=$([ "$EVENT_NAME" == "pull_request" ] && echo "$PR_HEAD_REPO" || echo "$GH_REPOSITORY") GITHUB_BRANCH=$([ "$EVENT_NAME" == "pull_request" ] && echo "$HEAD_REF" || echo "$REF_NAME") @@ -329,7 +332,7 @@ jobs: TEST_KEYWORDS_TO_USE_UPPER="${MATRIX_LINTER}" TEST_KEYWORDS_TO_USE="${TEST_KEYWORDS_TO_USE_UPPER,,}" docker image ls - docker run -e TEST_CASE_RUN=true -e OUTPUT_FORMAT=text -e OUTPUT_FOLDER="${GH_SHA}" -e OUTPUT_DETAIL=detailed -e GITHUB_SHA="${GH_SHA}" -e GITHUB_REPOSITORY="${GITHUB_REPOSITORY}" -e GITHUB_BRANCH="${GITHUB_BRANCH}" -e GITHUB_TOKEN="${GITHUB_TOKEN}" -e TEST_KEYWORDS="${TEST_KEYWORDS_TO_USE}" -e MEGALINTER_VOLUME_ROOT="${GITHUB_WORKSPACE}" -v "/var/run/docker.sock:/var/run/docker.sock:rw" -v "${GITHUB_WORKSPACE}:/tmp/lint" "${DOCKER_IMAGE}" + docker run -e TEST_CASE_RUN=true -e OUTPUT_FORMAT=text -e OUTPUT_FOLDER="${GH_SHA}" -e OUTPUT_DETAIL=detailed -e GITHUB_SHA="${GH_SHA}" -e GITHUB_REPOSITORY="${GITHUB_REPOSITORY}" -e GITHUB_BRANCH="${GITHUB_BRANCH}" -e GITHUB_TOKEN="${GITHUB_TOKEN}" -e SFDX_AUTH_URL="${SFDX_AUTH_URL}" -e TEST_KEYWORDS="${TEST_KEYWORDS_TO_USE}" -e MEGALINTER_VOLUME_ROOT="${GITHUB_WORKSPACE}" -v "/var/run/docker.sock:/var/run/docker.sock:rw" -v "${GITHUB_WORKSPACE}:/tmp/lint" "${DOCKER_IMAGE}" timeout-minutes: 30 ############################################## diff --git a/.github/workflows/deploy-DEV.yml b/.github/workflows/deploy-DEV.yml index 147ea5d84ff..d1cd285aba2 100644 --- a/.github/workflows/deploy-DEV.yml +++ b/.github/workflows/deploy-DEV.yml @@ -270,6 +270,9 @@ jobs: DOCKER_IMAGE: ${{ needs.build.outputs.image-tag }} GH_SHA: ${{ github.sha }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # Salesforce org auth url used by SALESFORCE_CODE_ANALYZER_APEXGURU. + # Empty on pull requests from forks: the ApexGuru tests then skip themselves + SFDX_AUTH_URL: ${{ secrets.SFDX_AUTH_URL }} run: | GITHUB_REPOSITORY=$([ "$EVENT_NAME" == "pull_request" ] && echo "$PR_HEAD_REPO" || echo "$GH_REPOSITORY") GITHUB_BRANCH=$([ "$EVENT_NAME" == "pull_request" ] && echo "$HEAD_REF" || echo "$REF_NAME") @@ -287,7 +290,7 @@ jobs: fi docker image ls # shellcheck disable=SC2086 - docker run $CI_ENV -e TEST_CASE_RUN=true -e OUTPUT_FORMAT=text -e OUTPUT_FOLDER="${GH_SHA}" -e OUTPUT_DETAIL=detailed -e GITHUB_SHA="${GH_SHA}" -e GITHUB_REPOSITORY="${GITHUB_REPOSITORY}" -e GITHUB_BRANCH="${GITHUB_BRANCH}" -e GITHUB_TOKEN="${GITHUB_TOKEN}" -e TEST_KEYWORDS="${TEST_KEYWORDS_TO_USE}" -e MEGALINTER_VOLUME_ROOT="${GITHUB_WORKSPACE}" -v "/var/run/docker.sock:/var/run/docker.sock:rw" -v "${GITHUB_WORKSPACE}:/tmp/lint" "${DOCKER_IMAGE}" + docker run $CI_ENV -e TEST_CASE_RUN=true -e OUTPUT_FORMAT=text -e OUTPUT_FOLDER="${GH_SHA}" -e OUTPUT_DETAIL=detailed -e GITHUB_SHA="${GH_SHA}" -e GITHUB_REPOSITORY="${GITHUB_REPOSITORY}" -e GITHUB_BRANCH="${GITHUB_BRANCH}" -e GITHUB_TOKEN="${GITHUB_TOKEN}" -e SFDX_AUTH_URL="${SFDX_AUTH_URL}" -e TEST_KEYWORDS="${TEST_KEYWORDS_TO_USE}" -e MEGALINTER_VOLUME_ROOT="${GITHUB_WORKSPACE}" -v "/var/run/docker.sock:/var/run/docker.sock:rw" -v "${GITHUB_WORKSPACE}:/tmp/lint" "${DOCKER_IMAGE}" timeout-minutes: 120 - name: Archive production artifacts (test-cases) diff --git a/CHANGELOG.md b/CHANGELOG.md index 27d9a7de779..0e1c4660fd6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -145,11 +145,14 @@ Note: Can be used with `oxsecurity/megalinter@beta` in your GitHub Action mega-l - **Docker pulls monthly chart**: the auto-update workflow now regenerates `docs/assets/images/docker-pulls-monthly.svg` (new pulls per month since October 2020, all images and registries), via the new `.automation/docker_pulls_chart.py` called by `build.py` after the pull counters update - Historical monthly points are frozen in `.automation/generated/docker-pulls-monthly.json` (built once from the tracked stats plus a Web Archive reconstruction of the collection gaps); the script only appends newly completed months computed from `flavors-stats.json` - Docker pull counters now also track the **standalone `megalinter-only-*` images**: their download counts are stored in `flavors-stats.json` and included in the README badge total + - New descriptor `activation_rules` type **`variable_is_set`**, activating a linter as soon as a variable holds a value. The existing `variable` type can only compare a variable to a fixed `expected_value`, which can not express "a credential is present" - the condition **SALESFORCE_CODE_ANALYZER_APEXGURU** needs on `SFDX_AUTH_URL` + - Linter tests gated on such a variable **skip themselves** when it is missing, instead of failing: `LinterTestRoot.skip_if_required_variables_missing()` guards the per-lint-mode and SARIF tests, while the version and help tests keep running since they need no credential - CI - **Supply-chain hardening of dependency updates**: Renovate (`minimumReleaseAge`) and Dependabot (`cooldown`) now wait until a release is at least **7 days old** before proposing an upgrade, so compromised releases can be caught by the community first. Security fixes are not delayed and still open immediately - New **Check agent plugins manifests** workflow validating the agent plugin manifests on every change to them or to `skills/`: `.automation/validate_agent_plugins.py` checks the root `plugin.json` against the published Agent Plugins 1.0 schema and keeps the per-vendor manifests consistent with it, then `claude plugin validate ./ --strict` checks the Claude Code marketplace and plugin manifests - The auto-update workflow patch-bumps the agent plugin version when it regenerates the skills: the plugin follows its own release train, since its fix guides change far more often than MegaLinter is released. `plugin.json` is the single source of truth, mirrored into the per-vendor manifests by `.automation/agent_plugin_manifests.py` (called by `build.py`) + - The test workflows forward the **`SFDX_AUTH_URL`** repository secret to the test container, so the **SALESFORCE_CODE_ANALYZER_APEXGURU** lint tests can reach a connected org. The secret is not exposed on pull requests from forked repositories, where those tests skip themselves - Linter versions upgrades (N) - [editorconfig-checker](https://editorconfig-checker.github.io/) from 3.10.0 to **3.11.1** on 2026-08-09 diff --git a/megalinter/tests/test_megalinter/LinterTestRoot.py b/megalinter/tests/test_megalinter/LinterTestRoot.py index 297d6912ed1..7aab0dec36c 100644 --- a/megalinter/tests/test_megalinter/LinterTestRoot.py +++ b/megalinter/tests/test_megalinter/LinterTestRoot.py @@ -32,6 +32,21 @@ def get_linter_instance(self, request_id): }, ) + # A linter gated on a credential declares a "variable_is_set" activation + # rule. When that credential is absent the linter is not even activated, so + # its lint tests can not run: skip them instead of failing. This is the + # normal situation on pull request jobs from a forked repository, where + # GitHub does not expose repository secrets. + def skip_if_required_variables_missing(self, linter): + for rule in linter.activation_rules: + if rule.get("type") != "variable_is_set": + continue + if config.get(self.request_id, rule["variable"], "") == "": + raise unittest.SkipTest( + f"{rule['variable']} is not set: " + f"{linter.linter_name} can not be tested" + ) + def lint_mode_setup(self, mode): config.set_value( self.request_id, @@ -51,6 +66,7 @@ def run_lint_mode_test(self, mode, test_name, test_func): self.request_id = str(uuid.uuid1()) utilstest.linter_test_setup({"request_id": self.request_id}) probe_linter = self.get_linter_instance(self.request_id) + self.skip_if_required_variables_missing(probe_linter) if not probe_linter.is_cli_lint_mode_supported(mode): raise unittest.SkipTest(f"Linter does not support lint_mode: {mode}") # CI optimization: when a linter supports both file and list_of_files, @@ -124,6 +140,7 @@ def test_report_sarif(self): {"request_id": self.request_id, "report_type": "SARIF"} ) linter = self.get_linter_instance(self.request_id) + self.skip_if_required_variables_missing(linter) linter.pre_test("test_report_sarif") utilstest.test_linter_report_sarif(linter, self) linter.post_test("test_report_sarif") From 9b917ecf6d8d9555c057d64e8b4e7459c0b332b4 Mon Sep 17 00:00:00 2001 From: nvuillam Date: Fri, 28 Aug 2026 12:55:37 +0200 Subject: [PATCH 3/4] Fix CI: add apexguru to the cspell dictionary SPELL_CSPELL reported 28 blocking errors, all of them the new linter name. The dictionary entry is lowercase, which cspell matches case-insensitively, so it covers apexguru, ApexGuru and APEXGURU. --- .cspell.json | 1 + 1 file changed, 1 insertion(+) diff --git a/.cspell.json b/.cspell.json index 4cfa4ae31cc..64104a24e94 100644 --- a/.cspell.json +++ b/.cspell.json @@ -324,6 +324,7 @@ "anymatch", "análises", "ApexCSRF", + "apexguru", "apeyrat", "apheleia", "apkadd", From 938ba42ddf54dd1776ff5bdf20bfdb3ef3420b2e Mon Sep 17 00:00:00 2001 From: nvuillam <17500430+nvuillam@users.noreply.github.com> Date: Fri, 28 Aug 2026 14:05:12 +0000 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=A4=96=20[MegaLinter]=20Apply=20linte?= =?UTF-8?q?rs=20fixes=20:)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- skills/megalinter-fix/linters/html_djlint.md | 2 +- skills/megalinter-fix/linters/terraform_tofu_validate.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/skills/megalinter-fix/linters/html_djlint.md b/skills/megalinter-fix/linters/html_djlint.md index 38cc47f472a..8cfe58c4cf3 100644 --- a/skills/megalinter-fix/linters/html_djlint.md +++ b/skills/megalinter-fix/linters/html_djlint.md @@ -9,7 +9,7 @@ - Rules index: - Rules configuration: - How to disable rules inline: -- Error line format (regex): `[A-Z][0-9]{3} ` +- Error line format (regex): `[A-Z][0-9]{3}` - MegaLinter tuning variables (in `.mega-linter.yml`): - `DISABLE_LINTERS`: add `HTML_DJLINT` to fully disable this linter - `HTML_DJLINT_DISABLE_ERRORS: true`: keep the linter active but non-blocking diff --git a/skills/megalinter-fix/linters/terraform_tofu_validate.md b/skills/megalinter-fix/linters/terraform_tofu_validate.md index 0175da8c215..e41c51a540b 100644 --- a/skills/megalinter-fix/linters/terraform_tofu_validate.md +++ b/skills/megalinter-fix/linters/terraform_tofu_validate.md @@ -8,7 +8,7 @@ - Auto-fix support: no (errors must be fixed manually) - Rules index: - How to disable rules inline: -- Error line format (regex): `Error: ` +- Error line format (regex): `Error:` - MegaLinter tuning variables (in `.mega-linter.yml`): - `DISABLE_LINTERS`: add `TERRAFORM_TOFU_VALIDATE` to fully disable this linter - `TERRAFORM_TOFU_VALIDATE_DISABLE_ERRORS: true`: keep the linter active but non-blocking