diff --git a/bin/run-linters.sh b/bin/run-linters.sh index 4afbc72..36ec2bf 100755 --- a/bin/run-linters.sh +++ b/bin/run-linters.sh @@ -110,6 +110,14 @@ for linter in "${REQUIRED_LINTERS[@]}"; do run_args=("${run_args_common[@]}") "${LIB_ROOT}/checks/linters/shellcheck/run.sh" "${run_args[@]}" ;; + groovylint) + run_args=("${run_args_common[@]}") + "${LIB_ROOT}/checks/linters/groovylint/run.sh" "${run_args[@]}" + ;; + markdownlint) + run_args=("${run_args_common[@]}") + "${LIB_ROOT}/checks/linters/markdownlint/run.sh" "${run_args[@]}" + ;; codespell) run_args=("${run_args_common[@]}") "${LIB_ROOT}/checks/linters/codespell/run.sh" "${run_args[@]}" diff --git a/checks/detect-linters.sh b/checks/detect-linters.sh index 3c1556c..e1c6112 100755 --- a/checks/detect-linters.sh +++ b/checks/detect-linters.sh @@ -11,6 +11,8 @@ linter_fail_on_unknown_args linter_require_common_args shellcheck_needed=0 +groovylint_needed=0 +markdownlint_needed=0 codespell_needed=0 text_hygiene_needed=0 filename_portability_needed=0 @@ -24,11 +26,25 @@ while IFS= read -r file_path; do if linter_is_shell_script_candidate "${file_path}"; then shellcheck_needed=1 fi + + if linter_is_groovy_candidate "${file_path}"; then + groovylint_needed=1 + fi + + if linter_is_markdown_candidate "${file_path}"; then + markdownlint_needed=1 + fi done < <(linter_get_candidate_files_acmr) if [[ ${shellcheck_needed} -eq 1 ]]; then echo 'shellcheck' fi +if [[ ${groovylint_needed} -eq 1 ]]; then + echo 'groovylint' +fi +if [[ ${markdownlint_needed} -eq 1 ]]; then + echo 'markdownlint' +fi if [[ ${codespell_needed} -eq 1 ]]; then echo 'codespell' fi diff --git a/checks/install-linter-tools.sh b/checks/install-linter-tools.sh index 4d1a766..abee4f0 100755 --- a/checks/install-linter-tools.sh +++ b/checks/install-linter-tools.sh @@ -6,6 +6,11 @@ LIB_ROOT="" TARGET_ROOT="" MODE="changed" BASE_REF="${GITHUB_BASE_REF:-}" +NPM_GROOVY_LINT_VERSION="13.0.2" +MARKDOWNLINT_CLI_VERSION="0.39.0" +# Ubuntu/WSL: distro Node packages may install but have a broken ELF interpreter +# path. The helpers below detect this and work around it at runtime. +NODE_ELF_LOADER="/lib64/ld-linux-x86-64.so.2" usage() { cat <<'EOF' @@ -75,15 +80,186 @@ if [[ ${#REQUIRED_LINTERS[@]} -eq 0 ]]; then exit 0 fi +# --------------------------------------------------------------------------- +# Node/npm helper functions +# On Ubuntu/WSL, distro Node packages may install but have a broken ELF +# interpreter path. The helpers below detect and work around this condition. +# --------------------------------------------------------------------------- + +sudo_cmd() { + if [[ "$(id -u)" -eq 0 ]]; then + "$@" + else + sudo "$@" + fi +} + +have_working_node_via_loader() { + [[ -x "${NODE_ELF_LOADER}" ]] && [[ -x /usr/bin/node ]] && + "${NODE_ELF_LOADER}" /usr/bin/node --version > /dev/null 2>&1 +} + +get_npm_cli_path() { + local npm_cli='' + for npm_cli in \ + /usr/share/nodejs/npm/bin/npm-cli.js \ + /usr/lib/node_modules/npm/bin/npm-cli.js; do + if [[ -f "${npm_cli}" ]]; then + echo "${npm_cli}" + return 0 + fi + done + return 1 +} + +# Run npm; fall back to ELF-loader invocation when the npm wrapper is broken. +npm_cmd() { + if command -v npm >/dev/null 2>&1 && npm --version > /dev/null 2>&1; then + npm "$@" + return $? + fi + local npm_cli='' + if have_working_node_via_loader && npm_cli="$(get_npm_cli_path 2>/dev/null)"; then + "${NODE_ELF_LOADER}" /usr/bin/node "${npm_cli}" "$@" + return $? + fi + return 1 +} + +# Same as npm_cmd but wrapped in sudo (or direct when already root). +npm_cmd_sudo() { + if sudo_cmd npm --version > /dev/null 2>&1; then + sudo_cmd npm "$@" + return $? + fi + local npm_cli='' + if [[ -x "${NODE_ELF_LOADER}" ]] && [[ -x /usr/bin/node ]] && + npm_cli="$(get_npm_cli_path 2>/dev/null)"; then + sudo_cmd "${NODE_ELF_LOADER}" /usr/bin/node "${npm_cli}" "$@" + return $? + fi + return 1 +} + +have_working_npm() { + npm_cmd --version > /dev/null 2>&1 +} + +get_groovylint_cli_script_path() { + local global_root='' + global_root="$(npm_cmd root -g 2>/dev/null | tr -d '\r' | head -n1 || true)" + local candidate='' + for candidate in \ + "${global_root}/npm-groovy-lint/bin/npm-groovy-lint.js" \ + "${global_root}/npm-groovy-lint/lib/index.js" \ + /usr/local/lib/node_modules/npm-groovy-lint/bin/npm-groovy-lint.js \ + /usr/local/lib/node_modules/npm-groovy-lint/lib/index.js \ + /usr/lib/node_modules/npm-groovy-lint/bin/npm-groovy-lint.js \ + /usr/lib/node_modules/npm-groovy-lint/lib/index.js; do + if [[ -n "${candidate}" ]] && [[ -f "${candidate}" ]]; then + echo "${candidate}" + return 0 + fi + done + return 1 +} + +# Verify npm-groovy-lint works; if not, attempt an ELF-loader wrapper install. +ensure_groovylint_command_works() { + if command -v npm-groovy-lint >/dev/null 2>&1 && + npm-groovy-lint --version > /dev/null 2>&1; then + return 0 + fi + have_working_node_via_loader || return 1 + local script='' + script="$(get_groovylint_cli_script_path 2>/dev/null || true)" + [[ -z "${script}" ]] && return 1 + echo "[linters] node runtime requires ELF loader; creating npm-groovy-lint wrapper" + local tmp='' + tmp="$(mktemp)" + printf '#!/bin/bash\nexec "%s" /usr/bin/node "%s" "$@"\n' \ + "${NODE_ELF_LOADER}" "${script}" > "${tmp}" + chmod 0755 "${tmp}" + local target='' + if sudo_cmd install -m 0755 "${tmp}" /usr/local/bin/npm-groovy-lint 2>/dev/null; then + target="/usr/local/bin/npm-groovy-lint" + else + mkdir -p "${HOME}/.local/bin" + install -m 0755 "${tmp}" "${HOME}/.local/bin/npm-groovy-lint" + target="${HOME}/.local/bin/npm-groovy-lint" + export PATH="${HOME}/.local/bin:${PATH}" + fi + rm -f "${tmp}" + echo "[linters] installed npm-groovy-lint wrapper at ${target}" + npm-groovy-lint --version > /dev/null 2>&1 +} + +get_markdownlint_cli_script_path() { + local global_root='' + global_root="$(npm_cmd root -g 2>/dev/null | tr -d '\r' | head -n1 || true)" + local candidate='' + for candidate in \ + "${global_root}/markdownlint-cli/markdownlint.js" \ + /usr/local/lib/node_modules/markdownlint-cli/markdownlint.js \ + /usr/lib/node_modules/markdownlint-cli/markdownlint.js; do + if [[ -n "${candidate}" ]] && [[ -f "${candidate}" ]]; then + echo "${candidate}" + return 0 + fi + done + return 1 +} + +# Verify markdownlint works; if not, attempt an ELF-loader wrapper install. +ensure_markdownlint_command_works() { + if command -v markdownlint >/dev/null 2>&1 && + markdownlint --version > /dev/null 2>&1; then + return 0 + fi + have_working_node_via_loader || return 1 + local script='' + script="$(get_markdownlint_cli_script_path 2>/dev/null || true)" + [[ -z "${script}" ]] && return 1 + echo "[linters] node runtime requires ELF loader; creating markdownlint wrapper" + local tmp='' + tmp="$(mktemp)" + printf '#!/bin/bash\nexec "%s" /usr/bin/node "%s" "$@"\n' \ + "${NODE_ELF_LOADER}" "${script}" > "${tmp}" + chmod 0755 "${tmp}" + local target='' + if sudo_cmd install -m 0755 "${tmp}" /usr/local/bin/markdownlint 2>/dev/null; then + target="/usr/local/bin/markdownlint" + else + mkdir -p "${HOME}/.local/bin" + install -m 0755 "${tmp}" "${HOME}/.local/bin/markdownlint" + target="${HOME}/.local/bin/markdownlint" + export PATH="${HOME}/.local/bin:${PATH}" + fi + rm -f "${tmp}" + echo "[linters] installed markdownlint wrapper at ${target}" + markdownlint --version > /dev/null 2>&1 +} + +# --------------------------------------------------------------------------- +# Platform package installation +# --------------------------------------------------------------------------- + PACKAGES=() for linter in "${REQUIRED_LINTERS[@]}"; do case "${linter}" in - shellcheck) PACKAGES+=("shellcheck") ;; - codespell) PACKAGES+=("codespell") ;; - *) continue ;; + shellcheck) PACKAGES+=("shellcheck") ;; + groovylint) PACKAGES+=("npm") ;; + markdownlint) PACKAGES+=("npm") ;; + codespell) PACKAGES+=("codespell") ;; + *) continue ;; esac done +# Deduplicate (npm may appear for both groovylint and markdownlint). +if [[ ${#PACKAGES[@]} -gt 0 ]]; then + mapfile -t PACKAGES < <(printf '%s\n' "${PACKAGES[@]}" | sort -u) +fi + if [[ ${#PACKAGES[@]} -eq 0 ]]; then echo "[linters] selected linters do not require external tool install" exit 0 @@ -91,54 +267,153 @@ fi MISSING_PACKAGES=() for package in "${PACKAGES[@]}"; do - if command -v "${package}" >/dev/null 2>&1; then - echo "[linters] already installed: ${package}" - continue - fi + case "${package}" in + npm) + if have_working_npm; then + echo "[linters] already installed: npm" + continue + fi + ;; + *) + if command -v "${package}" >/dev/null 2>&1; then + echo "[linters] already installed: ${package}" + continue + fi + ;; + esac MISSING_PACKAGES+=("${package}") done if [[ ${#MISSING_PACKAGES[@]} -eq 0 ]]; then - echo "[linters] required tools already present" - exit 0 -fi + echo "[linters] required platform tools already present" +else + echo "[linters] installing packages: ${MISSING_PACKAGES[*]}" -echo "[linters] installing packages: ${MISSING_PACKAGES[*]}" + case "$(uname -s)" in + Darwin) + if ! command -v brew >/dev/null 2>&1; then + echo "[linters] homebrew not found; install it or install tools manually" >&2 + echo "[linters] required packages: ${MISSING_PACKAGES[*]}" >&2 + exit 127 + fi + DARWIN_PACKAGES=() + for package in "${MISSING_PACKAGES[@]}"; do + if [[ "${package}" == "npm" ]]; then + # Homebrew installs npm via the node package. + DARWIN_PACKAGES+=("node") + else + DARWIN_PACKAGES+=("${package}") + fi + done + brew install "${DARWIN_PACKAGES[@]}" + ;; + Linux) + SUDO_CMD=() + if [[ "$(id -u)" -ne 0 ]]; then + if command -v sudo >/dev/null 2>&1; then + SUDO_CMD=(sudo) + else + echo "[linters] sudo not found and not running as root" >&2 + echo "[linters] required packages: ${MISSING_PACKAGES[*]}" >&2 + exit 127 + fi + fi -case "$(uname -s)" in - Darwin) - if ! command -v brew >/dev/null 2>&1; then - echo "[linters] homebrew not found; install it or install tools manually" >&2 - echo "[linters] required packages: ${MISSING_PACKAGES[*]}" >&2 - exit 127 - fi - brew install "${MISSING_PACKAGES[@]}" - ;; - Linux) - SUDO_CMD=() - if [[ "$(id -u)" -ne 0 ]]; then - if command -v sudo >/dev/null 2>&1; then - SUDO_CMD=(sudo) + LINUX_PACKAGES=() + for package in "${MISSING_PACKAGES[@]}"; do + if [[ "${package}" == "npm" ]]; then + # Ubuntu/WSL reliability: install both nodejs and npm together. + LINUX_PACKAGES+=("nodejs" "npm") + else + LINUX_PACKAGES+=("${package}") + fi + done + + if command -v apt-get >/dev/null 2>&1; then + "${SUDO_CMD[@]}" apt-get update + # Use --reinstall so a broken distro Node install (e.g. Ubuntu/WSL ELF + # interpreter mismatch) is corrected when nodejs/npm are in the list. + "${SUDO_CMD[@]}" apt-get install --reinstall -y "${LINUX_PACKAGES[@]}" + elif command -v dnf >/dev/null 2>&1; then + "${SUDO_CMD[@]}" dnf install -y "${LINUX_PACKAGES[@]}" + elif command -v yum >/dev/null 2>&1; then + "${SUDO_CMD[@]}" yum install -y "${LINUX_PACKAGES[@]}" else - echo "[linters] sudo not found and not running as root" >&2 + echo "[linters] no supported package manager found (apt-get/dnf/yum)" >&2 echo "[linters] required packages: ${MISSING_PACKAGES[*]}" >&2 exit 127 fi + ;; + *) + echo "[linters] unsupported platform '$(uname -s)'; install tools manually" >&2 + echo "[linters] required packages: ${MISSING_PACKAGES[*]}" >&2 + exit 127 + ;; + esac +fi + +# --------------------------------------------------------------------------- +# npm-based tool installation +# --------------------------------------------------------------------------- + +if printf '%s\n' "${REQUIRED_LINTERS[@]}" | grep -Fxq 'groovylint'; then + INSTALLED_GROOVY_LINT_VERSION="" + if command -v npm-groovy-lint >/dev/null 2>&1 && ensure_groovylint_command_works; then + INSTALLED_GROOVY_LINT_VERSION="$({ npm-groovy-lint --version || true; } \ + | awk '/npm-groovy-lint version/{print $NF; exit}')" + fi + + if [[ "${INSTALLED_GROOVY_LINT_VERSION}" == "${NPM_GROOVY_LINT_VERSION}" ]]; then + echo "[linters] already installed: npm-groovy-lint@${NPM_GROOVY_LINT_VERSION}" + else + if [[ -n "${INSTALLED_GROOVY_LINT_VERSION}" ]]; then + echo "[linters] updating npm-groovy-lint from ${INSTALLED_GROOVY_LINT_VERSION} to ${NPM_GROOVY_LINT_VERSION}" + else + echo "[linters] installing npm-groovy-lint@${NPM_GROOVY_LINT_VERSION} via npm" fi - if command -v apt-get >/dev/null 2>&1; then - "${SUDO_CMD[@]}" apt-get update - "${SUDO_CMD[@]}" apt-get install -y "${MISSING_PACKAGES[@]}" - elif command -v dnf >/dev/null 2>&1; then - "${SUDO_CMD[@]}" dnf install -y "${MISSING_PACKAGES[@]}" + if ! have_working_npm; then + echo "[linters] npm runtime is not working; cannot install npm-groovy-lint" >&2 + exit 127 + fi + if ! npm_cmd install --global "npm-groovy-lint@${NPM_GROOVY_LINT_VERSION}" > /dev/null 2>&1; then + echo "[linters] retrying npm-groovy-lint install with sudo" + npm_cmd_sudo install --global "npm-groovy-lint@${NPM_GROOVY_LINT_VERSION}" + fi + if ! ensure_groovylint_command_works; then + echo "[linters] npm-groovy-lint installed but command is not working" >&2 + exit 1 + fi + fi +fi + +if printf '%s\n' "${REQUIRED_LINTERS[@]}" | grep -Fxq 'markdownlint'; then + INSTALLED_MARKDOWNLINT_VERSION="" + if command -v markdownlint >/dev/null 2>&1 && ensure_markdownlint_command_works; then + INSTALLED_MARKDOWNLINT_VERSION="$(markdownlint --version 2>/dev/null \ + | head -n1 | tr -d '\r' || true)" + fi + + if [[ "${INSTALLED_MARKDOWNLINT_VERSION}" == "${MARKDOWNLINT_CLI_VERSION}" ]]; then + echo "[linters] already installed: markdownlint@${MARKDOWNLINT_CLI_VERSION}" + else + if [[ -n "${INSTALLED_MARKDOWNLINT_VERSION}" ]]; then + echo "[linters] updating markdownlint from ${INSTALLED_MARKDOWNLINT_VERSION} to ${MARKDOWNLINT_CLI_VERSION}" else - echo "[linters] no supported package manager found (apt-get/dnf)" >&2 - echo "[linters] required packages: ${MISSING_PACKAGES[*]}" >&2 + echo "[linters] installing markdownlint-cli@${MARKDOWNLINT_CLI_VERSION} via npm" + fi + if ! have_working_npm; then + echo "[linters] npm runtime is not working; cannot install markdownlint" >&2 exit 127 fi - ;; - *) - echo "[linters] unsupported platform '$(uname -s)'; install tools manually" >&2 - echo "[linters] required packages: ${MISSING_PACKAGES[*]}" >&2 - exit 127 - ;; -esac + if ! npm_cmd install --global --ignore-scripts \ + "markdownlint-cli@${MARKDOWNLINT_CLI_VERSION}" > /dev/null 2>&1; then + echo "[linters] retrying markdownlint install with sudo" + npm_cmd_sudo install --global --ignore-scripts \ + "markdownlint-cli@${MARKDOWNLINT_CLI_VERSION}" + fi + if ! ensure_markdownlint_command_works; then + echo "[linters] markdownlint installed but command is not working" >&2 + exit 1 + fi + fi +fi diff --git a/checks/linter-common.sh b/checks/linter-common.sh index b66c111..a1418a1 100755 --- a/checks/linter-common.sh +++ b/checks/linter-common.sh @@ -156,3 +156,34 @@ linter_is_shell_script_candidate() { return 1 } + +linter_is_groovy_candidate() { + local file_path="$1" + local base_name="${file_path##*/}" + local absolute_path="${TARGET_ROOT}/${file_path}" + + [[ -f "${absolute_path}" ]] || return 1 + + case "${file_path}" in + *.groovy|*.gradle|Jenkinsfile*) + return 0 + ;; + esac + + return 1 +} + +linter_is_markdown_candidate() { + local file_path="$1" + local absolute_path="${TARGET_ROOT}/${file_path}" + + [[ -f "${absolute_path}" ]] || return 1 + + case "${file_path}" in + *.md|*.markdown) + return 0 + ;; + esac + + return 1 +} diff --git a/checks/linters/groovylint/check-implicit-bindings.sh b/checks/linters/groovylint/check-implicit-bindings.sh new file mode 100755 index 0000000..b605d08 --- /dev/null +++ b/checks/linters/groovylint/check-implicit-bindings.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +# Copyright 2026 Hewlett Packard Enterprise Development LP +set -euo pipefail + +if [[ $# -eq 0 ]]; then + exit 0 +fi + +violations=0 +for file_path in "$@"; do + [[ -f "${file_path}" ]] || continue + + case "${file_path##*/}" in + *.groovy|Jenkinsfile*) + ;; + *) + continue + ;; + esac + + # Guard against Groovy script binding side effects from bare assignments like + # `myVar = value`. In Jenkins pipelines these become shared script properties + # and can cause nondeterministic behavior in parallel execution. + if grep -nE '^[[:space:]]*[A-Za-z_][A-Za-z0-9_]*[[:space:]]*=[^=~]' \ + "${file_path}" >/dev/null; then + echo "[groovylint] implicit script-binding assignment detected: ${file_path}" >&2 + grep -nE '^[[:space:]]*[A-Za-z_][A-Za-z0-9_]*[[:space:]]*=[^=~]' \ + "${file_path}" >&2 + violations=1 + fi +done + +if [[ ${violations} -ne 0 ]]; then + echo "[groovylint] use explicit declarations (for example: def/typed variable)" >&2 + echo "[groovylint] implicit binding assignments are blocked for Jenkins safety" >&2 + exit 1 +fi diff --git a/checks/linters/groovylint/run.ps1 b/checks/linters/groovylint/run.ps1 new file mode 100644 index 0000000..84ca5b7 --- /dev/null +++ b/checks/linters/groovylint/run.ps1 @@ -0,0 +1,9 @@ +# Copyright 2026 Hewlett Packard Enterprise Development LP +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$scriptDir = Split-Path -Parent $PSCommandPath +$checksRoot = Split-Path -Parent (Split-Path -Parent $scriptDir) + +. (Join-Path $checksRoot 'invoke-bash.ps1') +Invoke-BashScript -ScriptPath (Join-Path $scriptDir 'run.sh') -ScriptArgs $args diff --git a/checks/linters/groovylint/run.sh b/checks/linters/groovylint/run.sh new file mode 100755 index 0000000..ae1f7bd --- /dev/null +++ b/checks/linters/groovylint/run.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# Copyright 2026 Hewlett Packard Enterprise Development LP +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck disable=SC1091 +source "${SCRIPT_DIR}/../../linter-common.sh" + +linter_parse_common_args "$@" +linter_fail_on_unknown_args +linter_require_common_args + +if ! command -v npm-groovy-lint >/dev/null 2>&1; then + echo "npm-groovy-lint is required but was not found in PATH" >&2 + exit 127 +fi + +files_to_check=() +while IFS= read -r file_path; do + linter_should_skip_candidate_path "${file_path}" && continue + + if linter_is_groovy_candidate "${file_path}"; then + files_to_check+=("${file_path}") + fi +done < <(linter_get_candidate_files_acmr) + +if [[ ${#files_to_check[@]} -eq 0 ]]; then + echo "[groovylint] no Groovy or Jenkins files to lint" + exit 0 +fi + +echo "[groovylint] linting ${#files_to_check[@]} file(s)" +file_patterns="$(IFS=,; printf '%s' "${files_to_check[*]}")" + +( + cd "${TARGET_ROOT}" + npm-groovy-lint --failon error --noserver --output txt --path . \ + --files "${file_patterns}" +) + +absolute_files=() +for file_path in "${files_to_check[@]}"; do + absolute_files+=("${TARGET_ROOT}/${file_path}") +done + +bash "${SCRIPT_DIR}/check-implicit-bindings.sh" "${absolute_files[@]}" diff --git a/checks/linters/markdownlint/run.ps1 b/checks/linters/markdownlint/run.ps1 new file mode 100644 index 0000000..84ca5b7 --- /dev/null +++ b/checks/linters/markdownlint/run.ps1 @@ -0,0 +1,9 @@ +# Copyright 2026 Hewlett Packard Enterprise Development LP +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$scriptDir = Split-Path -Parent $PSCommandPath +$checksRoot = Split-Path -Parent (Split-Path -Parent $scriptDir) + +. (Join-Path $checksRoot 'invoke-bash.ps1') +Invoke-BashScript -ScriptPath (Join-Path $scriptDir 'run.sh') -ScriptArgs $args diff --git a/checks/linters/markdownlint/run.sh b/checks/linters/markdownlint/run.sh new file mode 100755 index 0000000..e593945 --- /dev/null +++ b/checks/linters/markdownlint/run.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# Copyright 2026 Hewlett Packard Enterprise Development LP +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck disable=SC1091 +source "${SCRIPT_DIR}/../../linter-common.sh" + +linter_parse_common_args "$@" +linter_fail_on_unknown_args +linter_require_common_args + +if ! command -v markdownlint >/dev/null 2>&1; then + echo "markdownlint is required but was not found in PATH" >&2 + exit 127 +fi + +files_to_check=() +while IFS= read -r file_path; do + linter_should_skip_candidate_path "${file_path}" && continue + + if linter_is_markdown_candidate "${file_path}"; then + files_to_check+=("${TARGET_ROOT}/${file_path}") + fi +done < <(linter_get_candidate_files_acmr) + +if [[ ${#files_to_check[@]} -eq 0 ]]; then + echo "[markdownlint] no Markdown files to lint" + exit 0 +fi + +echo "[markdownlint] linting ${#files_to_check[@]} file(s)" + +# Run from TARGET_ROOT so markdownlint auto-discovers .markdownlint.json (or +# .markdownlint.yaml) in the consumer repository root. +( + cd "${TARGET_ROOT}" + markdownlint "${files_to_check[@]}" +) diff --git a/docs/integration.md b/docs/integration.md index 7876cd8..b1eb72f 100644 --- a/docs/integration.md +++ b/docs/integration.md @@ -140,8 +140,8 @@ For routine updates to a new main tip, see [README.md](../README.md#update-to-la For background on submodule update behavior and remote-tracking refs, see the official Git documentation: -- https://git-scm.com/docs/git-submodule -- https://git-scm.com/docs/git-fetch +- [git submodule docs](https://git-scm.com/docs/git-submodule) +- [git fetch docs](https://git-scm.com/docs/git-fetch) ## Scope diff --git a/docs/linters.md b/docs/linters.md index af6d136..dc3b3d6 100644 --- a/docs/linters.md +++ b/docs/linters.md @@ -12,7 +12,7 @@ scripts with the Windows Python instance. ## Path Model -The linter runner uses two roots: +The linter entrypoint script uses two roots: - Library root: derived from the location of the script in this repository. - Target root: the repository being checked. @@ -34,7 +34,8 @@ that contains the shared checker scripts. ## Changed-Files Mode The default mode is `changed`. -In that mode the runner selects linters based on changed paths only. +In that mode the entrypoint script selects linters based on changed paths +only. Selection order: @@ -57,6 +58,20 @@ The changed-file linter set currently includes: - `text-hygiene` (trailing whitespace and missing final newline) - `filename-portability` (non-ASCII filename guard) - `shellcheck` (shell script linting for `*.sh`) +- `groovylint` (Groovy and Jenkins DSL linting for `*.groovy`, `*.gradle`, + and `Jenkinsfile*`) +- `markdownlint` (Markdown linting for `*.md` and `*.markdown`) + +Groovylint execution also includes a post-lint guard that rejects implicit +script-binding assignments (bare `name = value` at statement start) to prevent +Jenkins parallel-stage shared-state hazards. + +Markdownlint is pinned to `markdownlint-cli@0.39.0` to match the version +bundled by the VS Code `vscode-markdownlint` extension. Using the same version +ensures that rule suppressions in consumer `.markdownlint.json` files work +consistently between local editor checks and CI runs. markdownlint-cli is +installed via npm and auto-discovers a `.markdownlint.json` (or +`.markdownlint.yaml`) config file in the consumer repository root when present. Current exclusions: @@ -76,12 +91,13 @@ repos: ``` Local pre-commit runs will then use the changed-file mode automatically. -Before running selected linters, the runner verifies that `code_checking` is +Before running selected linters, the entrypoint script verifies that +`code_checking` is at the desired commit resolved from `code-checking-ref` (or `origin/main` when the file is missing). The check is non-mutating and fails with sync commands if the checkout does not match. -The runner also performs a centralized tool preflight check so required linter -executables are present before individual linter scripts run. +The entrypoint script also performs a centralized tool preflight check so +required linter executables are present before individual linter scripts run. For local debugging of linter behavior from the repository root: @@ -100,6 +116,12 @@ Current tool preflight mapping: - `shellcheck` linter requires `shellcheck` executable on PATH - `codespell` linter requires `codespell` executable on PATH +- `groovylint` linter requires `npm-groovy-lint` on PATH; `npm-groovy-lint` is + installed via npm with an explicit version pin in + `checks/install-linter-tools.sh` +- `markdownlint` linter requires the `markdownlint` CLI on PATH; + `markdownlint-cli` is installed via npm (see the markdownlint version note in + the Current Linters section above) On Linux/macOS targets, preflight failures include install hints for common package managers. @@ -121,7 +143,12 @@ When adding new linters, use this installation policy: 3. Use language package managers only as a fallback when no platform package convention exists. 4. Avoid system-wide `pip` installs on Linux (especially `sudo pip`). -5. Keep tools CLI-accessible on PATH so both pre-commit and CI behavior are +5. For Node-based linters, install the Node/npm runtime with the platform + package manager first, then install the linter CLI with `npm`. On + Ubuntu/WSL, `apt-get install --reinstall nodejs npm` is preferred because + distro Node packages sometimes have a broken ELF interpreter path; the + installer detects this and creates a loader-wrapper script automatically. +6. Keep tools CLI-accessible on PATH so both pre-commit and CI behavior are consistent. For each new linter integration PR, include: @@ -151,7 +178,7 @@ Add selection logic so the linter is chosen for the relevant changed files. - `bin/run-linters.sh` - `bin/run-linters.ps1` -Add a dispatch case that calls the per-linter runner. +Add a dispatch case that calls the per-linter script. 1. Per-linter executor @@ -203,7 +230,7 @@ Examples: The `codespell` addition required updates to: - detection scripts -- top-level runners +- top-level entrypoint scripts - tool preflight - CI install step - per-linter `run.sh` and `run.ps1` @@ -280,7 +307,8 @@ and what each script or directory is responsible for. 2. Consumer workflow optionally resolves `code-checking-ref` and checks out that ref in `code_checking` (otherwise uses `origin/main`). 3. Consumer workflow runs `./code_checking/bin/run-linters.sh`. -4. During that run, the runner verifies the current `code_checking` checkout +4. During that run, the entrypoint script verifies the current `code_checking` + checkout matches the desired ref, then performs changed-file linter selection and execution. diff --git a/docs/maintenance.md b/docs/maintenance.md index 074a4e6..4fc06e4 100644 --- a/docs/maintenance.md +++ b/docs/maintenance.md @@ -97,7 +97,7 @@ To clean up: ## Planned Content Areas -- Shell and PowerShell check runners +- Shell and PowerShell check scripts - Check script library (ansible-lint, yamllint, shellcheck, markdownlint, groovylint, codespell) - IDE setup baselines and a master IDE-agnostic YAML input (with VS Code diff --git a/docs/usage.md b/docs/usage.md index b61ddaa..bd0e60e 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -24,6 +24,13 @@ This script: - Attempts to install missing tools - Initializes pre-commit hooks in your repository +For the current baseline linters, automatic setup may install platform +packages such as `shellcheck`, `codespell`, and `npm`, then use `npm` to add +`npm-groovy-lint` when Groovy or Jenkins files are present. + +On Ubuntu WSL targets, Node-based lint setup expects both `nodejs` and `npm` +to be installed together to avoid partial npm-only setups. + ## Local setup (PowerShell) On Windows, run `bootstrap-windows-dev.ps1` first to install the bash runtime,