From f43fba4e91b39a92b0948633fbbdc080ef4bb3d3 Mon Sep 17 00:00:00 2001 From: Alexander Mohr Date: Tue, 12 May 2026 06:08:55 +0000 Subject: [PATCH 1/3] feat: add hook args config, prek migration, and shared lint improvements --- .github/workflows/checks.yml | 72 ++- .gitignore | 3 + .markdownlint.yaml | 16 + .pre-commit-config.yaml | 63 +++ .pre-commit-hooks.yaml | 62 +++ .yamlfmt | 12 + README.md | 110 +---- pre-commit-action/.pre-commit-config.yml | 83 ---- pre-commit-action/action.yml | 136 ++---- pre-commit-action/check-hooks.py | 330 ++++++++++++++ pre-commit-action/no-banner-comment-check.py | 120 +++++ pre-commit-action/no-unicode-check.py | 8 +- pre-commit-action/reuse-annotate-hook.py | 151 ++++++- ruff.toml | 27 ++ run_checks.py | 442 ------------------- rust-lint-and-format-action/action.yml | 32 +- shared-config/.markdownlint.yaml | 16 + shared-config/.rustfmt.toml | 15 + shared-config/.yamlfmt | 12 + shared-config/cargo-clippy.sh | 23 + shared-config/cargo-fmt.sh | 30 ++ shared-config/ruff.toml | 27 ++ shared-config/rustfmt-config.sh | 14 + shared-lints/README.md | 14 +- shared-lints/check_cargo_lints.py | 19 +- shared-lints/shared-lints.toml | 4 +- 26 files changed, 1023 insertions(+), 818 deletions(-) create mode 100644 .markdownlint.yaml create mode 100644 .pre-commit-config.yaml create mode 100644 .pre-commit-hooks.yaml create mode 100644 .yamlfmt delete mode 100644 pre-commit-action/.pre-commit-config.yml create mode 100755 pre-commit-action/check-hooks.py create mode 100755 pre-commit-action/no-banner-comment-check.py mode change 100644 => 100755 pre-commit-action/no-unicode-check.py mode change 100644 => 100755 pre-commit-action/reuse-annotate-hook.py create mode 100644 ruff.toml delete mode 100755 run_checks.py create mode 100644 shared-config/.markdownlint.yaml create mode 100644 shared-config/.rustfmt.toml create mode 100644 shared-config/.yamlfmt create mode 100755 shared-config/cargo-clippy.sh create mode 100755 shared-config/cargo-fmt.sh create mode 100644 shared-config/ruff.toml create mode 100644 shared-config/rustfmt-config.sh diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 13fa41c..e687bc4 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -1,5 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) +# SPDX-FileCopyrightText: 2026 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) # # See the NOTICE file(s) distributed with this work for additional # information regarding copyright ownership. @@ -18,36 +18,12 @@ on: workflow_dispatch: workflow_call: inputs: - rust-nightly-version: - description: 'Rust nightly version to use (YYYY-MM-DD), defaults to 2025-07-14 if not set' + rust-toolchain: + description: 'Rust toolchain to install (e.g. "stable", "nightly-2025-07-14")' required: false type: string - python-version: - description: 'Python version to use for pre-commit environment' - required: false - type: string - pre-commit-config-path: - description: 'Path to a custom .pre-commit-config.yml in the consumer repository' - required: false - type: string - copyright-text: - description: 'Copyright holder text for reuse annotate, defaults to "The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS)"' - required: false - type: string - license: - description: 'SPDX license identifier for reuse annotate (e.g. "Apache-2.0"), defaults to "Apache-2.0"' - required: false - type: string - reuse-template: - description: 'Name of reuse Jinja2 template in .reuse/templates/ (without .jinja2 suffix), defaults to "opensovd"' - required: false - type: string - no-unicode-extensions: - description: 'Comma-separated file extensions to check for non-ASCII characters (e.g., ".py,.rs"). Empty string disables the check.' - required: false - type: string - allowed-unicode-chars: - description: 'Comma-separated Unicode characters to allow in the no-unicode check. Empty by default.' + go-version: + description: 'Go version for gitleaks hook' required: false type: string @@ -56,20 +32,38 @@ permissions: jobs: checks: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: submodules: false + fetch-depth: 0 + - name: Pin cicd-workflows rev to current SHA + shell: bash + run: | + sed -i "s|rev: CICD_WORKFLOWS_HEAD_SHA|rev: $(git rev-parse HEAD)|" .pre-commit-config.yaml + git add .pre-commit-config.yaml - name: Run checks uses: ./pre-commit-action with: - rust-nightly-version: ${{ inputs.rust-nightly-version }} - python-version: ${{ inputs.python-version }} - config-path: ${{ inputs.pre-commit-config-path }} - copyright-text: ${{ inputs.copyright-text }} - license: ${{ inputs.license }} - reuse-template: ${{ inputs.reuse-template }} - no-unicode-extensions: ${{ inputs.no-unicode-extensions || '.py,.yml,.toml,.jinja2' }} - allowed-unicode-chars: ${{ inputs.allowed-unicode-chars }} + rust-toolchain: ${{ inputs.rust-toolchain || 'stable' }} + go-version: ${{ inputs.go-version || '1.25' }} + + - name: Validate commit subjects (Conventional Commits) + if: github.event_name == 'pull_request' + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: | + set -euo pipefail + fail=0 + for sha in $(git rev-list "$BASE_SHA..$HEAD_SHA"); do + git log -1 --format=%B "$sha" > /tmp/commit-msg + if ! prek run --hook-stage commit-msg \ + --commit-msg-filename /tmp/commit-msg conventional-pre-commit; then + echo "::error::Non-conventional commit subject: $(git log -1 --format='%h %s' "$sha")" + fail=1 + fi + done + exit "$fail" diff --git a/.gitignore b/.gitignore index 6776c0a..6f6fa32 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,6 @@ .idea/ .vscode/ .DS_Store +.venv/ +uv.lock +__pycache__/ diff --git a/.markdownlint.yaml b/.markdownlint.yaml new file mode 100644 index 0000000..f9db163 --- /dev/null +++ b/.markdownlint.yaml @@ -0,0 +1,16 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: 2026 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +default: true +MD013: + line_length: 1000 +MD024: + siblings_only: true +MD033: false +MD060: false diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..7ff9d49 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,63 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: 2026 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 + +default_install_hook_types: [pre-commit, commit-msg] + +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: check-yaml + - id: check-toml + - id: check-json + - id: check-merge-conflict + - id: end-of-file-fixer + exclude: '\.lock$' + - id: trailing-whitespace + exclude: '\.(patch|diff|lock)$' + - id: mixed-line-ending + exclude: '\.lock$' + - repo: https://github.com/google/yamlfmt + rev: v0.21.0 + hooks: + - id: yamlfmt + - repo: https://github.com/igorshubovych/markdownlint-cli + rev: v0.48.0 + hooks: + - id: markdownlint + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.15.12 + hooks: + - id: ruff-check + - id: ruff-format + - repo: https://github.com/gitleaks/gitleaks + rev: v8.30.1 + hooks: + - id: gitleaks + - repo: https://github.com/koalaman/shellcheck-precommit + rev: v0.11.0 + hooks: + - id: shellcheck + - repo: https://github.com/compilerla/conventional-pre-commit + rev: v4.4.0 + hooks: + - id: conventional-pre-commit + stages: [commit-msg] + - repo: https://github.com/eclipse-opensovd/cicd-workflows + # prek does not support `repo: self` or `repo: local`, so this repo must + # reference itself by URL with an explicit rev. The CI workflow (checks.yml) + # patches this sentinel to the actual HEAD SHA before running prek. For + # local runs, set this to the desired commit SHA (branch names not allowed). + rev: CICD_WORKFLOWS_HEAD_SHA + hooks: + - id: reuse-annotate + - id: no-unicode-check + - id: no-banner-comment-check + - id: check-hooks diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml new file mode 100644 index 0000000..6bb2c13 --- /dev/null +++ b/.pre-commit-hooks.yaml @@ -0,0 +1,62 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: 2026 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +- id: reuse-annotate + name: Add missing license headers (REUSE) + entry: pre-commit-action/reuse-annotate-hook.py + language: script + pass_filenames: true + exclude: ^(LICENSE|NOTICE|CONTRIBUTORS)$ + args: + - --copyright=The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) + - --license=Apache-2.0 + - --template=opensovd + +- id: no-unicode-check + name: No Unicode characters allowed + entry: pre-commit-action/no-unicode-check.py + language: script + pass_filenames: true + files: '\.(rs|py|toml|yml|yaml|rst|c|h|kt|kts|sh|proto|fbs|puml|json|xml)$|^Dockerfile$' + +- id: no-banner-comment-check + name: No banner-style comments allowed + entry: pre-commit-action/no-banner-comment-check.py + language: script + pass_filenames: true + files: '\.(rs|py|toml|yml|yaml|c|h|kt|kts|sh|proto|fbs|puml|json|xml)$|^Dockerfile$' + args: + - --banner-chars==\-#\*/~_+ + - --min-length=5 + +- id: validate-cargo-lints + name: Validate Cargo Lints + entry: shared-lints/check_cargo_lints.py Cargo.toml + language: script + types: [rust] + pass_filenames: false + +- id: check-hooks + name: Validate hook configuration + entry: pre-commit-action/check-hooks.py + language: script + always_run: true + pass_filenames: false + +- id: cargo-fmt + name: Rust format (shared config) + entry: shared-config/cargo-fmt.sh + language: script + pass_filenames: false + +- id: clippy + name: Clippy (shared config) + entry: shared-config/cargo-clippy.sh + language: script + pass_filenames: false diff --git a/.yamlfmt b/.yamlfmt new file mode 100644 index 0000000..9f2a31b --- /dev/null +++ b/.yamlfmt @@ -0,0 +1,12 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: 2026 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +formatter: + type: basic + retain_line_breaks_single: true diff --git a/README.md b/README.md index ecf2c7f..110cf03 100644 --- a/README.md +++ b/README.md @@ -59,8 +59,10 @@ jobs: - `copyright-text` (optional): Copyright holder text for `reuse annotate` (e.g. `"ACME Inc."`). Defaults to `"The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS)"`. - `license` (optional): SPDX license identifier for `reuse annotate` (e.g. `"MIT"`). Defaults to `"Apache-2.0"`. - `reuse-template` (optional): Name of the Jinja2 template in `.reuse/templates/` (without `.jinja2` suffix). Consumer repos can provide their own template. Defaults to `"opensovd"`. -- `no-unicode-extensions` (optional): Comma-separated list of file extensions (e.g. `".py,.rs,.c"`) whose contents are checked for non-ASCII bytes. Any file with a matching extension that contains a byte with value > 127 causes the check to fail with the offending file path and line number reported. Extensions may include or omit the leading dot. Disabled by default (empty string). -- `allowed-unicode-chars` (optional): Comma-separated Unicode characters that are permitted in files checked by `no-unicode-extensions` (e.g. `"µ,§"`). Empty by default (all non-ASCII characters are rejected). +- `no-unicode-extensions` (optional): Comma-separated list of file extensions (e.g. `".py,.rs,.c"`) whose contents are checked for non-ASCII bytes. + Any file with a matching extension that contains a byte > 127 causes the check to fail. Disabled by default (empty string). +- `allowed-unicode-chars` (optional): Comma-separated Unicode characters permitted in files checked by `no-unicode-extensions` + (e.g. `"µ,§"`). Empty by default (all non-ASCII characters are rejected). ### Using Individual Actions @@ -82,6 +84,7 @@ jobs: ``` #### Rust Lint And Format Action + ```yaml # Make sure to copy this into you CI pipeline too, otherwise review comments cannot be posted. @@ -122,6 +125,7 @@ are applied in the Cargo.toml #### Checks Performed **File Validation:** + - YAML syntax validation - Merge conflict detection - End-of-file fixer (ensures files end with a newline) @@ -129,6 +133,7 @@ are applied in the Cargo.toml - Mixed line ending normalization **Code Formatting:** + - **YAML**: Formatted with `yamlfmt` using basic formatter with retained line breaks - **Python**: Formatted with `ruff format` (extremely fast Python formatter) - **TOML**: Formatted and linted with `taplo` @@ -138,127 +143,58 @@ are applied in the Cargo.toml - Import granularity using `Crate` setting **Linting:** + - **Python**: `ruff check` for linting and code quality **License Headers (Auto-fix):** + - **FSFE REUSE tool**: Automatically adds and validates license headers per the [REUSE Specification](https://reuse.software/) - `reuse lint` validates all files have proper SPDX headers - `reuse annotate` auto-adds headers to new files with the current year **Lint verification:** + - [check-cargo-lints](shared-lints/check_cargo_lints.py): checks that the Cargo.toml (workspace or package) has all lints specified according to [shared-lints.toml](shared-lints/shared-lints.toml) **How Auto-fix Works:** + When a formatter makes changes to your code, the pre-commit hook fails, requiring you to review and commit the changes. This ensures: + - All code modifications are tracked in version control - Developers can review formatting changes before committing - CI pipelines fail if code is not properly formatted **Inputs:** + - `python-version`: Python version for pre-commit environment (default: `3.13`) - `config-path`: Path to custom `.pre-commit-config.yml` (optional) - `copyright-text`: Copyright holder text for `reuse annotate` (default: `"The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS)"`) - `license`: SPDX license identifier for `reuse annotate` (default: `"Apache-2.0"`) - `reuse-template`: Name of Jinja2 template in `.reuse/templates/` (default: `"opensovd"`) -- `no-unicode-extensions`: Comma-separated file extensions to check for non-ASCII characters (e.g. `".py,.rs,.c"`). Disabled by default (empty string). When enabled, any file with a matching extension containing a byte > 127 fails the check. -- `allowed-unicode-chars`: Comma-separated Unicode characters permitted in files checked by `no-unicode-extensions` (e.g. `"µ,§"`). Empty by default. - +- `no-unicode-extensions`: Comma-separated file extensions to check for non-ASCII characters (e.g. `".py,.rs,.c"`). + Disabled by default (empty string). When enabled, any file with a matching extension containing a byte > 127 fails the check. +- `allowed-unicode-chars`: Comma-separated Unicode characters permitted in files checked by `no-unicode-extensions` (e.g. `"µ,§"`). + Empty by default. ## Running Checks Locally -### Using uv for Pre-commit Checks - -[uv](https://docs.astral.sh/uv/) is a fast Python package manager that can run Python scripts without needing to install dependencies globally. - -#### In This Repository - +Run all pre-commit hooks on your repository using [prek](https://github.com/j178/prek): - -To run pre-commit checks locally in this repository: - -```bash -uv tool run pre-commit@4.2 run --all-files --config pre-commit-action/.pre-commit-config.yml -``` - -#### In Your Repository (Using This Action's Config) - -You have two options to run the same checks locally that run in CI: - -##### Option 1: Using the `run_checks.py` script (One-off execution) - -```bash -# Run with the default 'main' branch config -uv run https://raw.githubusercontent.com/eclipse-opensovd/cicd-workflows/main/run_checks.py -``` - -###### Specify a different branch/tag/commit ```bash -uv run https://raw.githubusercontent.com/eclipse-opensovd/cicd-workflows/main/run_checks.py your-branch-name +prek run --all-files ``` -###### Custom copyright and license -```bash -uv run https://raw.githubusercontent.com/eclipse-opensovd/cicd-workflows/main/run_checks.py --copyright="ACME Inc." --license=MIT --template=mytemplate -``` - -###### Check for non-ASCII characters in specific file types -```bash -uv run https://raw.githubusercontent.com/eclipse-opensovd/cicd-workflows/main/run_checks.py --no-unicode-extensions=".py,.rs" -``` - -To allow specific Unicode characters (e.g. `µ` and `§`): -```bash -uv run https://raw.githubusercontent.com/eclipse-opensovd/cicd-workflows/main/run_checks.py --no-unicode-extensions=".py,.rs" --allowed-unicode-chars="µ,§" -``` - -The `--no-unicode-extensions` flag accepts a comma-separated list of file extensions (with or without a leading dot). When provided, any file with a matching extension that contains a non-ASCII byte (value > 127) causes the check to fail, reporting the file path and line number. The `--allowed-unicode-chars` flag accepts a comma-separated list of Unicode characters that are exempt from this check. Omit the flags or pass empty strings to disable. - -The script automatically fixes ruff lint violations and applies ruff formatting. In CI, issues are only reported without auto-fix. - - -#### Option 2: Using pre-commit directly (Recommended for development) - -Create a `.pre-commit-config.yaml` file in your repository root: - -```yaml -repos: - - repo: local - hooks: - - id: shared-checks - name: Shared pre-commit checks - entry: uv run https://raw.githubusercontent.com/eclipse-opensovd/cicd-workflows/main/run_checks.py - language: system - pass_filenames: false -``` - -Then install and use pre-commit normally: - -```bash -# Install pre-commit hooks (runs automatically on git commit) -pre-commit install - -# Run manually on all files -pre-commit run --all-files - -# Run on staged files only -pre-commit run -``` - -**Custom Config**: If you've specified a custom `pre-commit-config-path` in your workflow, you can run pre-commit directly: -```bash -uv tool run pre-commit@4.2 run --all-files --config .pre-commit-config.yml -``` +Or without installing prek globally: -**Run Specific Hooks**: To run only the shared checks: ```bash -pre-commit run shared-checks --all-files +uv run prek run --all-files ``` ### Installing Required Tools -#### uv (Required) +#### prek (Required) -[Install uv](https://docs.astral.sh/uv/getting-started/installation/) - Fast Python package manager and script runner. +[Install prek](https://github.com/j178/prek) - Pre-commit hook runner. Install via `uv tool install prek` or `pip install prek`. #### FSFE REUSE tool (Required for License Checks) diff --git a/pre-commit-action/.pre-commit-config.yml b/pre-commit-action/.pre-commit-config.yml deleted file mode 100644 index af115a8..0000000 --- a/pre-commit-action/.pre-commit-config.yml +++ /dev/null @@ -1,83 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) -# -# See the NOTICE file(s) distributed with this work for additional -# information regarding copyright ownership. -# -# This program and the accompanying materials are made available under the -# terms of the Apache License Version 2.0 which is available at -# https://www.apache.org/licenses/LICENSE-2.0 - -repos: - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v6.0.0 - hooks: - - id: check-yaml - - id: check-merge-conflict - - id: end-of-file-fixer - - id: trailing-whitespace - exclude: \.(patch|diff|lock|json)$ - args: [--markdown-linebreak-ext=md] - - id: mixed-line-ending - - repo: https://github.com/google/yamlfmt - rev: v0.20.0 - hooks: - - id: yamlfmt - args: ["-formatter", "type=basic,retain_line_breaks_single=true"] - - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.5 - hooks: - - id: ruff-check - args: [--output-format=full] - - id: ruff-format - args: [--diff] - - repo: local - hooks: - - id: taplo-format - name: taplo-format - description: Format TOML documents - entry: taplo format - language: system - types: [toml] - - id: taplo-lint - name: taplo-lint - description: Lint TOML documents - entry: taplo lint - language: system - types: [toml] - - repo: https://github.com/fsfe/reuse-tool - rev: v6.2.0 - hooks: - - id: reuse - - repo: local - hooks: - - id: reuse-annotate - name: Add missing license headers (REUSE) - entry: uv run reuse-annotate-hook.py - language: system - pass_filenames: true - exclude: ^(LICENSE|NOTICE|CONTRIBUTORS)$ - - id: cargo-fmt-long-lines - name: Rust Format - Fix long lines and unformatted code - entry: bash -c '[ -f Cargo.toml ] && cargo fmt --check -- --config error_on_unformatted=true,error_on_line_overflow=true,format_strings=true || exit 0' - language: system - types: [rust] - pass_filenames: false - - id: cargo-fmt-import-order - name: Rust Format - Fix import order - entry: bash -c '[ -f Cargo.toml ] && cargo fmt --check -- --config group_imports=StdExternalCrate || exit 0' - language: system - types: [rust] - pass_filenames: false - - id: cargo-fmt-imports-granularity - name: Rust Format - Fix imports granularity - entry: bash -c '[ -f Cargo.toml ] && cargo fmt --check -- --config imports_granularity=Crate || exit 0' - language: system - types: [rust] - pass_filenames: false - - id: validate-cargo-lints - name: Validate Cargo Lints - entry: uv run shared-lints/check_cargo_lints.py Cargo.toml - language: system - types: [rust] - pass_filenames: false diff --git a/pre-commit-action/action.yml b/pre-commit-action/action.yml index d58ad10..6ae4ec3 100644 --- a/pre-commit-action/action.yml +++ b/pre-commit-action/action.yml @@ -1,5 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) +# SPDX-FileCopyrightText: 2026 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) # # See the NOTICE file(s) distributed with this work for additional # information regarding copyright ownership. @@ -7,119 +7,45 @@ # This program and the accompanying materials are made available under the # terms of the Apache License Version 2.0 which is available at # https://www.apache.org/licenses/LICENSE-2.0 - name: 'OpenSOVD Pre-Commit Checks' -description: 'Runs pre-commit hooks with a standardized configuration from this action.' - +description: 'Runs pre-commit hooks via prek using the repository .pre-commit-config.yaml' inputs: - rust-nightly-version: - description: 'Rust nightly version to use (YYYY-MM-DD), defaults to 2025-07-14 if not set' + rust-toolchain: + description: 'Rust toolchain to install (e.g. "stable", "nightly-2025-07-14")' required: false - default: '2025-07-14' - python-version: - description: 'Python version to use for pre-commit environment' + default: 'stable' + go-version: + description: 'Go version for gitleaks hook' required: false - default: '3.13' - config-path: - description: > - Path to a custom .pre-commit-config.yml in the consumer repository. If not provided, the action uses its own config. - required: false - default: '' - copyright-text: - description: > - Copyright holder text used by reuse annotate for new files. Defaults to "The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS)". - required: false - default: 'The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS)' - license: - description: > - SPDX license identifier used by reuse annotate for new files (e.g. "Apache-2.0"). Defaults to "Apache-2.0". - required: false - default: 'Apache-2.0' - reuse-template: - description: > - Name of the reuse Jinja2 template in .reuse/templates/ (without .jinja2 suffix). Consumer repos can provide their own template. Defaults to "opensovd". - required: false - default: 'opensovd' - ignore-paths: - description: > - Comma-separated list of file patterns to ignore during REUSE checks (e.g., "*.md,docs/**,*.txt"). Files matching these patterns will be skipped by reuse annotate. - required: false - default: '' - no-unicode-extensions: - description: > - Comma-separated list of file extensions to check for non-ASCII characters (e.g., ".py,.rs,.c"). When non-empty, any file with a matching extension that contains a byte > 127 will cause the check to fail. Disabled by default (empty string). - required: false - default: '' - allowed-unicode-chars: - description: > - Comma-separated Unicode characters to allow in the no-unicode check. When non-empty, these characters are permitted even in files checked by no-unicode-extensions. Empty by default (no characters allowed). - required: false - default: '' - + default: '1.25' runs: using: 'composite' steps: - - name: Checkout consumer repository - uses: actions/checkout@v4 - with: - submodules: false - - - name: Determine input values with defaults - id: inputs - shell: bash - run: | - # Rust nightly version - if [[ -n "${{ inputs.rust-nightly-version }}" ]]; then - echo "rust_toolchain=nightly-${{ inputs.rust-nightly-version }}" >> "$GITHUB_OUTPUT" - else - echo "rust_toolchain=nightly-2025-07-14" >> "$GITHUB_OUTPUT" - fi - - # Python version - if [[ -n "${{ inputs.python-version }}" ]]; then - echo "python_version=${{ inputs.python-version }}" >> "$GITHUB_OUTPUT" - else - echo "python_version=3.13" >> "$GITHUB_OUTPUT" - fi - - # Pre-commit config path - if [[ -n "${{ inputs.config-path }}" ]]; then - echo "config_flag=--config=${{ inputs.config-path }}" >> "$GITHUB_OUTPUT" - else - echo "config_flag=--config=${GITHUB_ACTION_PATH}/.pre-commit-config.yml" >> "$GITHUB_OUTPUT" - fi - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@nightly + uses: actions-rust-lang/setup-rust-toolchain@v1 with: - toolchain: ${{ steps.inputs.outputs.rust_toolchain }} - components: clippy, rustfmt - - - name: Install uv - uses: astral-sh/setup-uv@v6 + toolchain: ${{ inputs.rust-toolchain }} + components: rustfmt, clippy + cache-shared-key: ci + - name: Install Go (required for gitleaks) + uses: actions/setup-go@v6 with: - enable-cache: true - python-version: ${{ steps.inputs.outputs.python_version }} - activate-environment: true - - - name: Install reuse tool - shell: bash - run: uv tool install reuse - - - name: Install taplo - shell: bash - run: uv tool install taplo - - - name: Run checks + go-version: ${{ inputs.go-version }} + cache: false + - name: Install uv + uses: astral-sh/setup-uv@v7 + - name: Install tools shell: bash run: | - uv run "${GITHUB_ACTION_PATH}/../run_checks.py" --no-fix \ - ${{ steps.inputs.outputs.config_flag }} \ - --hook-script="${GITHUB_ACTION_PATH}/reuse-annotate-hook.py" \ - --copyright="${{ inputs.copyright-text }}" \ - --license="${{ inputs.license }}" \ - --template="${{ inputs.reuse-template }}" \ - --ignore-paths="${{ inputs.ignore-paths }}" \ - --no-unicode-extensions="${{ inputs.no-unicode-extensions }}" \ - --allowed-unicode-chars="${{ inputs.allowed-unicode-chars }}" \ - --no-unicode-check-script="${GITHUB_ACTION_PATH}/no-unicode-check.py" + uv tool install prek + uv tool install reuse>=5.0.2 + - name: Cache prek hooks + uses: actions/cache@v5 + with: + path: ~/.cache/prek + key: prek-${{ runner.os }}-${{ hashFiles('.pre-commit-config.yaml') }} + restore-keys: | + prek-${{ runner.os }}- + - name: Run pre-commit checks + shell: bash + run: prek run --all-files --show-diff-on-failure diff --git a/pre-commit-action/check-hooks.py b/pre-commit-action/check-hooks.py new file mode 100755 index 0000000..8fef7bb --- /dev/null +++ b/pre-commit-action/check-hooks.py @@ -0,0 +1,330 @@ +#!/usr/bin/env python3 + +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: 2026 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 + +# /// script +# dependencies = [] +# /// + +from __future__ import annotations + +import re +import sys +from pathlib import Path + +REQUIRED_HOOKS: list[dict] = [ + { + "name": "check-yaml", + "description": "YAML syntax validation", + "repo": "https://github.com/pre-commit/pre-commit-hooks", + "rev": "v6.0.0", + "hook_ids": ["check-yaml"], + }, + { + "name": "check-toml", + "description": "TOML syntax validation", + "repo": "https://github.com/pre-commit/pre-commit-hooks", + "rev": "v6.0.0", + "hook_ids": ["check-toml"], + }, + { + "name": "check-json", + "description": "JSON syntax validation", + "repo": "https://github.com/pre-commit/pre-commit-hooks", + "rev": "v6.0.0", + "hook_ids": ["check-json"], + }, + { + "name": "check-merge-conflict", + "description": "merge conflict marker detection", + "repo": "https://github.com/pre-commit/pre-commit-hooks", + "rev": "v6.0.0", + "hook_ids": ["check-merge-conflict"], + }, + { + "name": "end-of-file-fixer", + "description": "ensure files end with a newline", + "repo": "https://github.com/pre-commit/pre-commit-hooks", + "rev": "v6.0.0", + "hook_ids": ["end-of-file-fixer"], + }, + { + "name": "trailing-whitespace", + "description": "trailing whitespace removal", + "repo": "https://github.com/pre-commit/pre-commit-hooks", + "rev": "v6.0.0", + "hook_ids": ["trailing-whitespace"], + }, + { + "name": "mixed-line-ending", + "description": "consistent line endings", + "repo": "https://github.com/pre-commit/pre-commit-hooks", + "rev": "v6.0.0", + "hook_ids": ["mixed-line-ending"], + }, + { + "name": "yamlfmt", + "description": "YAML formatting", + "repo": "https://github.com/google/yamlfmt", + "rev": "v0.21.0", + "hook_ids": ["yamlfmt"], + }, + { + "name": "markdownlint", + "description": "markdown linting", + "repo": "https://github.com/igorshubovych/markdownlint-cli", + "rev": "v0.48.0", + "hook_ids": ["markdownlint", "markdownlint-docker"], + }, + { + "name": "ruff-check", + "description": "Python linting (ruff)", + "repo": "https://github.com/astral-sh/ruff-pre-commit", + "rev": "v0.15.12", + "hook_ids": ["ruff-check", "ruff"], + }, + { + "name": "ruff-format", + "description": "Python formatting (ruff)", + "repo": "https://github.com/astral-sh/ruff-pre-commit", + "rev": "v0.15.12", + "hook_ids": ["ruff-format"], + }, + { + "name": "gitleaks", + "description": "secret/credential detection", + "repo": "https://github.com/gitleaks/gitleaks", + "rev": "v8.30.1", + "hook_ids": ["gitleaks"], + }, + { + "name": "shellcheck", + "description": "shell script linting", + "repo": "https://github.com/koalaman/shellcheck-precommit", + "rev": "v0.11.0", + "hook_ids": ["shellcheck"], + }, + { + "name": "conventional-pre-commit", + "description": "conventional commit message enforcement", + "repo": "https://github.com/compilerla/conventional-pre-commit", + "rev": "v4.4.0", + "hook_ids": ["conventional-pre-commit"], + }, + { + "name": "reuse", + "description": "REUSE license header compliance", + "repo": "https://github.com/eclipse-opensovd/cicd-workflows", + "rev": None, + "hook_ids": ["reuse", "reuse-annotate"], + }, + { + "name": "check-hooks", + "description": "hook configuration enforcement", + "repo": "https://github.com/eclipse-opensovd/cicd-workflows", + "rev": None, + "hook_ids": ["check-hooks", "check-minimum-hooks"], + }, +] + +RUST_HOOKS: list[dict] = [ + { + "name": "cargo-fmt", + "description": "Rust formatting", + "repo": "https://github.com/eclipse-opensovd/cicd-workflows", + "rev": None, + "hook_ids": ["cargo-fmt", "fmt"], + }, + { + "name": "clippy", + "description": "Clippy linting", + "repo": "https://github.com/eclipse-opensovd/cicd-workflows", + "rev": None, + "hook_ids": ["clippy", "cargo-clippy"], + }, + { + "name": "validate-cargo-lints", + "description": "Cargo.toml lint configuration validation", + "repo": "https://github.com/eclipse-opensovd/cicd-workflows", + "rev": None, + "hook_ids": ["validate-cargo-lints"], + }, +] + +_SHARED_CONFIG = Path(__file__).parent.parent / "shared-config" + +REQUIRED_CONFIGS: list[dict] = [ + { + "path": ".yamlfmt", + "description": "yamlfmt configuration", + "canonical": _SHARED_CONFIG / ".yamlfmt", + "rust_only": False, + }, + { + "path": ".markdownlint.yaml", + "description": "markdownlint configuration", + "canonical": _SHARED_CONFIG / ".markdownlint.yaml", + "rust_only": False, + }, + { + "path": ".rustfmt.toml", + "description": "rustfmt configuration", + "canonical": _SHARED_CONFIG / ".rustfmt.toml", + "rust_only": True, + }, + { + "path": "ruff.toml", + "description": "ruff configuration", + "canonical": _SHARED_CONFIG / "ruff.toml", + "rust_only": False, + }, +] + +REPO_LINE = re.compile(r"^\s*-\s*repo:\s*(.+)$") +REV_LINE = re.compile(r"^\s*rev:\s*(.+)$") +ID_LINE = re.compile(r"^\s*-?\s*id:\s*(.+)$") + + +def parse_config(text: str) -> list[dict]: + repos: list[dict] = [] + current: dict | None = None + for line in text.splitlines(): + m = REPO_LINE.match(line) + if m: + current = {"repo": m.group(1).strip(), "rev": None, "hook_ids": []} + repos.append(current) + continue + if current is None: + continue + m = REV_LINE.match(line) + if m and current["rev"] is None: + current["rev"] = m.group(1).strip() + continue + m = ID_LINE.match(line) + if m: + current["hook_ids"].append(m.group(1).strip().strip("'\"")) + return repos + + +def all_hook_ids(repos: list[dict]) -> set[str]: + ids: set[str] = set() + for r in repos: + ids.update(r["hook_ids"]) + return ids + + +def find_repo_for_hook(repos: list[dict], hook_ids: list[str]) -> dict | None: + for r in repos: + if any(hid in r["hook_ids"] for hid in hook_ids): + return r + return None + + +def check_hooks( + repos: list[dict], + hook_list: list[dict], + failures: list[str], +) -> None: + configured_ids = all_hook_ids(repos) + for hook in hook_list: + if not any(hid in configured_ids for hid in hook["hook_ids"]): + failures.append(f"Missing hook: {hook['name']} ({hook['description']})") + continue + + repo_entry = find_repo_for_hook(repos, hook["hook_ids"]) + if repo_entry and repo_entry["repo"] != hook["repo"]: + failures.append( + f"Wrong repo for {hook['name']}: " + f"expected {hook['repo']!r}, got {repo_entry['repo']!r}" + ) + + if hook["rev"] is not None and repo_entry: + actual_rev = repo_entry.get("rev") + if actual_rev != hook["rev"]: + failures.append( + f"Wrong version for {hook['name']}: " + f"expected {hook['rev']!r}, got {actual_rev!r}" + ) + + +def check_configs(failures: list[str], has_rust: bool) -> None: + for cfg in REQUIRED_CONFIGS: + if cfg["rust_only"] and not has_rust: + continue + canonical_path: Path = cfg["canonical"] + if not canonical_path.exists(): + failures.append(f"Canonical config not found in cicd-workflows: {canonical_path}") + continue + canonical = canonical_path.read_text() + + p = Path(cfg["path"]) + if not p.exists(): + failures.append(f"Missing config file: {cfg['path']} ({cfg['description']})") + continue + actual = p.read_text() + if actual != canonical: + failures.append( + f"Config file {cfg['path']} does not match canonical content.\n" + " Expected:\n" + + "".join(f" {line}\n" for line in canonical.splitlines()) + + " Got:\n" + + "".join(f" {line}\n" for line in actual.splitlines()) + ) + + +def main() -> int: + config_path = Path(".pre-commit-config.yaml") + if len(sys.argv) > 1: + config_path = Path(sys.argv[1]) + + if not config_path.exists(): + print(f"Error: {config_path} not found", file=sys.stderr) + print( + "Repositories must have a .pre-commit-config.yaml with all required hooks.", + file=sys.stderr, + ) + return 1 + + text = config_path.read_text() + repos = parse_config(text) + failures: list[str] = [] + + check_hooks(repos, REQUIRED_HOOKS, failures) + + has_rust = any(Path(".").rglob("Cargo.toml")) + if has_rust: + check_hooks(repos, RUST_HOOKS, failures) + + check_configs(failures, has_rust) + + total_configs = sum(1 for c in REQUIRED_CONFIGS if not c["rust_only"] or has_rust) + + total_hooks = len(REQUIRED_HOOKS) + (len(RUST_HOOKS) if has_rust else 0) + + if failures: + print(f"[FAIL] {len(failures)} issue(s) found in hook configuration:") + for f in failures: + for line in f.splitlines(): + print(f" {line}") + print() + print( + "See https://github.com/eclipse-opensovd/cicd-workflows for the " + "canonical configuration." + ) + return 1 + + print(f"[OK] All {total_hooks} required hooks are correctly configured in {config_path}") + print(f"[OK] All {total_configs} required config files match canonical content") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/pre-commit-action/no-banner-comment-check.py b/pre-commit-action/no-banner-comment-check.py new file mode 100755 index 0000000..68c6fcd --- /dev/null +++ b/pre-commit-action/no-banner-comment-check.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 + +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: 2026 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 + +# /// script +# dependencies = [] +# /// + +import argparse +import re +import sys + +COMMENT_PREFIXES = ("///", "//!", "//", "/*", "*/", "#!", "#", "*") + + +def build_banner_pattern(banner_chars, min_length): + """Return compiled regexes that together match banner-style comment lines. + + Two patterns are combined: + 1. A run of min_length+ fill chars anchored at the start or end of the + stripped content (boundary = whitespace or string edge). Catches pure + banners ('----------') and suffix-only banners ('===== title'). + 2. A short fill-char prefix + label + long fill-char suffix, e.g. + '-- label ------'. This catches the common '// -- section ----...' + style even when the leading run is shorter than min_length. + """ + escaped = re.escape(banner_chars) + run = rf"[{escaped}]{{{min_length},}}" + short_run = rf"[{escaped}]{{1,}}" + anchored = re.compile(rf"(?:^{run}(?:\s|$)|(?:^|\s){run}$)") + labeled = re.compile(rf"^{short_run}\s.+\s{run}$") + return anchored, labeled + + +def strip_comment_content(line): + """Strip leading whitespace and comment prefix; return None if not a comment.""" + stripped = line.lstrip() + for prefix in COMMENT_PREFIXES: + if stripped.startswith(prefix): + return stripped[len(prefix) :].strip() + return None + + +def check_file(path, patterns, skip_lines=0): + """Return list of (line_number, line_text) for banner lines found.""" + violations = [] + try: + with open(path, encoding="utf-8", errors="replace") as f: + for line_number, line in enumerate(f, start=1): + if line_number <= skip_lines: + continue + content = strip_comment_content(line) + if content is None or not content: + continue + if any(p.search(content) for p in patterns): + violations.append((line_number, line.rstrip())) + except OSError as e: + print(f"Error reading {path}: {e}", file=sys.stderr) + sys.exit(1) + return violations + + +def main(): + parser = argparse.ArgumentParser( + description="Check files for banner-style comments.", + ) + parser.add_argument( + "--banner-chars", + default="=-#*/~_+", + help=( + "Literal characters considered 'banner fill'. These are placed " + "inside a regex character class [chars], so pass the raw characters " + "you want to match (e.g. '=-#*/~_+'). Escaping is handled internally. " + "Default: =-#*/~_+" + ), + ) + parser.add_argument( + "--min-length", + type=int, + default=5, + help="Minimum consecutive fill characters to flag a line (default: 5).", + ) + parser.add_argument( + "--skip-lines", + type=int, + default=0, + help=( + "Number of lines to skip at the start of each file (e.g. to ignore " + "license headers). Default: 0 (check all lines)." + ), + ) + parser.add_argument("files", nargs="*") + args = parser.parse_args() + + if not args.files: + sys.exit(0) + + patterns = build_banner_pattern(args.banner_chars, args.min_length) + found_violations = False + + for path in args.files: + violations = check_file(path, patterns, skip_lines=args.skip_lines) + if violations: + found_violations = True + for line_number, text in violations: + print(f"{path}:{line_number}: banner comment: {text}") + + sys.exit(1 if found_violations else 0) + + +if __name__ == "__main__": + main() diff --git a/pre-commit-action/no-unicode-check.py b/pre-commit-action/no-unicode-check.py old mode 100644 new mode 100755 index de28f66..2fb4846 --- a/pre-commit-action/no-unicode-check.py +++ b/pre-commit-action/no-unicode-check.py @@ -10,6 +10,10 @@ # terms of the Apache License Version 2.0 which is available at # https://www.apache.org/licenses/LICENSE-2.0 +# /// script +# dependencies = [] +# /// + import argparse import sys @@ -34,9 +38,7 @@ def check_file(path, allowed_chars): # Non-UTF-8 bytes are always a violation. violations.append((line_number, [""])) continue - bad = sorted( - {c for c in text if ord(c) > 127 and c not in allowed_chars} - ) + bad = sorted({c for c in text if ord(c) > 127 and c not in allowed_chars}) if bad: violations.append((line_number, bad)) except OSError as e: diff --git a/pre-commit-action/reuse-annotate-hook.py b/pre-commit-action/reuse-annotate-hook.py old mode 100644 new mode 100755 index ebb9add..43b1d95 --- a/pre-commit-action/reuse-annotate-hook.py +++ b/pre-commit-action/reuse-annotate-hook.py @@ -27,15 +27,11 @@ Comment style mapping is read from .reuse/styles.toml (downloaded or committed by the consumer repo). Every file type that needs a header must be declared there; unmatched files will cause reuse annotate to error. - -Configurable via env vars (with defaults): - REUSE_COPYRIGHT - copyright holder text - REUSE_LICENSE - SPDX license identifier - REUSE_TEMPLATE - name of .reuse/templates/.jinja2 """ from __future__ import annotations +import argparse import os import re import subprocess @@ -54,6 +50,36 @@ DEFAULT_IGNORE_PATHS = "" STYLES_CONFIG = ".reuse/styles.toml" +_REPO_ROOT = Path(__file__).resolve().parent.parent + + +def _ensure_reuse_assets(license_id: str, template: str) -> None: + """Copy REUSE assets from the prek-cached cicd-workflows clone into CWD. + + ``reuse annotate`` expects REUSE.toml / LICENSES / templates in the + working tree, but this script runs from prek's cache. Existing files + in CWD are never overwritten. + """ + assets = [ + ("REUSE.toml", _REPO_ROOT / "REUSE.toml"), + (".reuse/styles.toml", _REPO_ROOT / ".reuse" / "styles.toml"), + ( + f".reuse/templates/{template}.jinja2", + _REPO_ROOT / ".reuse" / "templates" / f"{template}.jinja2", + ), + ( + f"LICENSES/{license_id}.txt", + _REPO_ROOT / "LICENSES" / f"{license_id}.txt", + ), + ] + + for dest_rel, source in assets: + dest = Path(dest_rel) + if dest.exists() or not source.exists(): + continue + dest.parent.mkdir(parents=True, exist_ok=True) + dest.write_bytes(source.read_bytes()) + def load_styles(config_path: str = STYLES_CONFIG) -> dict[str, list[str]]: """Load comment style mappings from .reuse/styles.toml. @@ -111,6 +137,66 @@ def _current_year() -> str: return str(datetime.now(tz=timezone.utc).year) +def _merge_base_ref() -> str: + for remote_branch in ("origin/main", "origin/master"): + result = subprocess.run( + ["git", "merge-base", "HEAD", remote_branch], + capture_output=True, + text=True, + ) + if result.returncode == 0: + return result.stdout.strip() + return "HEAD" + + +def _is_new_file(filepath: str) -> bool: + base = _merge_base_ref() + result = subprocess.run( + ["git", "cat-file", "-e", f"{base}:{filepath}"], + capture_output=True, + ) + return result.returncode != 0 + + +def _fix_stale_copyright_year(filepath: str) -> bool: + """Update SPDX-FileCopyrightText year to current year for new files. + + Returns True if the file was modified. + """ + path = Path(filepath) + try: + content = path.read_text(errors="replace") + except OSError: + return False + + current = _current_year() + updated = re.sub( + r"(SPDX-FileCopyrightText:\s*)\d{4}", + rf"\g<1>{current}", + content, + ) + if updated != content: + path.write_text(updated) + print(f"Fixed copyright year to {current} in new file: {filepath}") + return True + return False + + +def _has_stale_copyright_year(filepath: str) -> bool: + """Check whether a new file has a copyright year that is not the current year.""" + path = Path(filepath) + try: + content = path.read_text(errors="replace") + except OSError: + return False + + current = _current_year() + match = re.search(r"SPDX-FileCopyrightText:\s*(\d{4})", content) + if match: + return match.group(1) != current + return False + + def has_valid_spdx_header(filepath: str) -> bool: """Check whether a file already has valid SPDX headers. @@ -190,18 +276,34 @@ def should_ignore(filepath: str, ignore_patterns: list[str]) -> bool: def main() -> int: - copyright_text = os.environ.get("REUSE_COPYRIGHT", DEFAULT_COPYRIGHT) - license_id = os.environ.get("REUSE_LICENSE", DEFAULT_LICENSE) - template = os.environ.get("REUSE_TEMPLATE", DEFAULT_TEMPLATE) - ignore_paths_str = os.environ.get("REUSE_IGNORE_PATHS", DEFAULT_IGNORE_PATHS) - - # Parse ignore patterns from comma-separated string - ignore_patterns = [p.strip() for p in ignore_paths_str.split(",") if p.strip()] - - files = sys.argv[1:] + parser = argparse.ArgumentParser() + parser.add_argument("--copyright", default=os.environ.get("REUSE_COPYRIGHT", DEFAULT_COPYRIGHT)) + parser.add_argument("--license", default=os.environ.get("REUSE_LICENSE", DEFAULT_LICENSE)) + parser.add_argument("--template", default=os.environ.get("REUSE_TEMPLATE", DEFAULT_TEMPLATE)) + parser.add_argument( + "--ignore-paths", + default=os.environ.get("REUSE_IGNORE_PATHS", DEFAULT_IGNORE_PATHS), + ) + parser.add_argument( + "--check", + action="store_true", + help="Check mode: report errors without modifying files (used in CI).", + ) + parser.add_argument("files", nargs="*") + args = parser.parse_args() + + copyright_text = args.copyright + license_id = args.license + template = args.template + check_mode = args.check + ignore_patterns = [p.strip() for p in args.ignore_paths.split(",") if p.strip()] + + files = args.files if not files: return 0 + _ensure_reuse_assets(license_id, template) + # Template flag tpl_flag: list[str] = [] if Path(f".reuse/templates/{template}.jinja2").exists(): @@ -210,6 +312,8 @@ def main() -> int: # Load style config styles = load_styles() + errors: list[str] = [] + for filepath in files: # Skip ignored files if should_ignore(filepath, ignore_patterns): @@ -223,6 +327,16 @@ def main() -> int: # This avoids overwriting existing (possibly different but valid) # license/copyright information with the template. if has_valid_spdx_header(filepath): + if _is_new_file(filepath): + if check_mode: + if _has_stale_copyright_year(filepath): + errors.append(f"{filepath}: copyright year is not {_current_year()}") + else: + _fix_stale_copyright_year(filepath) + continue + + if check_mode: + errors.append(f"{filepath}: missing SPDX license header") continue # Resolve comment style @@ -247,7 +361,14 @@ def main() -> int: f"--year={year}", filepath, ] - subprocess.run(cmd, check=False) + result = subprocess.run(cmd, check=False) + if result.returncode != 0: + errors.append(f"{filepath}: reuse annotate failed (exit {result.returncode})") + + if errors: + for error in errors: + print(f"ERROR: {error}") + return 1 return 0 diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 0000000..66fd595 --- /dev/null +++ b/ruff.toml @@ -0,0 +1,27 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: 2026 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +line-length = 100 + +[lint] +select = [ + "E", # pycodestyle (error) + "F", # pyflakes + "B", # bugbear + "B9", + "C4", # flake8-comprehensions + "SIM", # flake8-simplify + "I", # isort + "UP", # pyupgrade + "PIE", # flake8-pie + "PGH", # pygrep-hooks + "PYI", # flake8-pyi + "RUF", + "PT", # flake8-pytest-style +] diff --git a/run_checks.py b/run_checks.py deleted file mode 100755 index 8fa8ffd..0000000 --- a/run_checks.py +++ /dev/null @@ -1,442 +0,0 @@ -#!/usr/bin/env python3 - -# SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) -# -# See the NOTICE file(s) distributed with this work for additional -# information regarding copyright ownership. -# -# This program and the accompanying materials are made available under the -# terms of the Apache License Version 2.0 which is available at -# https://www.apache.org/licenses/LICENSE-2.0 - -# /// script -# dependencies = ["pre-commit==4.2"] -# /// - -import argparse -import os -import subprocess -import sys -import tempfile -import urllib.error -import urllib.request -from pathlib import Path - -# Default to 'main' branch, but can be overridden via environment variable or argument -DEFAULT_BRANCH = "main" -DEFAULT_COPYRIGHT = "The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS)" -DEFAULT_LICENSE = "Apache-2.0" -DEFAULT_TEMPLATE = "opensovd" -REPO_BASE_URL = ( - "https://raw.githubusercontent.com/eclipse-opensovd/cicd-workflows/{branch}" -) -CONFIG_URL_TEMPLATE = f"{REPO_BASE_URL}/pre-commit-action/.pre-commit-config.yml" -HOOK_SCRIPT_URL_TEMPLATE = f"{REPO_BASE_URL}/pre-commit-action/reuse-annotate-hook.py" -NO_UNICODE_CHECK_SCRIPT_URL_TEMPLATE = ( - f"{REPO_BASE_URL}/pre-commit-action/no-unicode-check.py" -) -TEMPLATE_URL_TEMPLATE = f"{REPO_BASE_URL}/.reuse/templates/{{template}}.jinja2" -LICENSE_URL_TEMPLATE = f"{REPO_BASE_URL}/LICENSES/{{license}}.txt" -REUSE_TOML_URL_TEMPLATE = f"{REPO_BASE_URL}/REUSE.toml" -STYLES_URL_TEMPLATE = f"{REPO_BASE_URL}/.reuse/styles.toml" -CLIPPY_LINTS_URL_TEMPLATE = f"{REPO_BASE_URL}/shared-lints/shared-lints.toml" -CLIPPY_LINTS_CHECK_SCRIPT_URL_TEMPLATE = ( - f"{REPO_BASE_URL}/shared-lints/check_cargo_lints.py" -) - - -def patch_config( - config_content, *, fix_mode, no_unicode_extensions="", allowed_unicode_chars="" -): - """Patch the pre-commit config for fix mode vs check-only mode. - - In fix mode (local), ruff auto-fixes issues in place. - In check-only mode (CI), ruff reports issues without modifying files. - """ - if fix_mode: - # ruff-check: add --fix - config_content = config_content.replace( - "args: [--output-format=full]", - "args: [--fix, --output-format=full]", - ) - # ruff-format: remove --diff so it formats in place - config_content = config_content.replace( - "args: [--diff]", - "args: []", - ) - # remove --check from cargo fmt entries so they format in place - config_content = config_content.replace( - "cargo fmt --check", - "cargo fmt", - ) - - if no_unicode_extensions: - parts = [ - e.strip().lstrip(".") - for e in no_unicode_extensions.split(",") - if e.strip().lstrip(".") - ] - if parts: - files_regex = r"\.(" + "|".join(parts) + r")$" - entry = "python no-unicode-check.py" - if allowed_unicode_chars: - entry += f" --allowed-chars={allowed_unicode_chars}" - hook_block = ( - " - id: no-unicode-check\n" - " name: No Unicode characters allowed\n" - f" entry: {entry}\n" - " language: system\n" - f" files: '{files_regex}'\n" - " pass_filenames: true\n" - ) - config_content = config_content.rstrip("\n") + "\n" + hook_block - - return config_content - - -def patch_hook_script( - script_content, *, copyright_text, license_id, template, ignore_paths, fix_mode -): - """Patch the reuse-annotate hook script with configured values. - - Replaces the Python default constants so the script uses the provided - values directly, without depending on environment variables at runtime. - """ - script_content = script_content.replace( - 'DEFAULT_COPYRIGHT = "The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS)"', - f'DEFAULT_COPYRIGHT = "{copyright_text}"', - ) - script_content = script_content.replace( - 'DEFAULT_LICENSE = "Apache-2.0"', - f'DEFAULT_LICENSE = "{license_id}"', - ) - script_content = script_content.replace( - 'DEFAULT_TEMPLATE = "opensovd"', - f'DEFAULT_TEMPLATE = "{template}"', - ) - script_content = script_content.replace( - 'DEFAULT_IGNORE_PATHS = ""', - f'DEFAULT_IGNORE_PATHS = "{ignore_paths}"', - ) - - return script_content - - -def download_if_missing(local_path, url, description): - """Download a file from url into local_path if it doesn't already exist. - - Returns cleanup info (file path + created directories) or None if skipped. - """ - local_path = Path(local_path) - if local_path.exists(): - return None - - print(f"Downloading {description} from: {url}") - - # Track which directories we need to create so we can clean them up - created_dirs = [] - check = local_path.parent - while check != Path("."): - if not check.exists(): - created_dirs.append(check) - check = check.parent - - local_path.parent.mkdir(parents=True, exist_ok=True) - - try: - with urllib.request.urlopen(url) as response: - local_path.write_text(response.read().decode()) - except urllib.error.HTTPError: - print( - f"Warning: Could not download {description} from {url}", - file=sys.stderr, - ) - return None - - return {"file": local_path, "dirs": sorted(created_dirs)} - - -def cleanup_downloads(cleanup_list): - """Remove downloaded files and any directories we created.""" - for cleanup_info in cleanup_list: - if cleanup_info is None: - continue - cleanup_info["file"].unlink(missing_ok=True) - # Remove directories we created, deepest first - for d in reversed(cleanup_info["dirs"]): - try: - d.rmdir() - except OSError: - pass # Directory not empty or already removed - - -def main(): - parser = argparse.ArgumentParser( - description="Run pre-commit checks with REUSE license header support" - ) - parser.add_argument( - "branch", - nargs="?", - default=DEFAULT_BRANCH, - help="Git branch to use for downloading configs (default: main)", - ) - parser.add_argument( - "--copyright", - default=DEFAULT_COPYRIGHT, - help=f"Copyright holder text for reuse annotate (default: {DEFAULT_COPYRIGHT})", - ) - parser.add_argument( - "--license", - default=DEFAULT_LICENSE, - help=f"SPDX license identifier for reuse annotate (default: {DEFAULT_LICENSE})", - ) - parser.add_argument( - "--template", - default=DEFAULT_TEMPLATE, - help=f"Name of reuse Jinja2 template in .reuse/templates/ (default: {DEFAULT_TEMPLATE})", - ) - parser.add_argument( - "--config", - default=None, - help="Path to a local .pre-commit-config.yml (skips downloading from remote)", - ) - parser.add_argument( - "--hook-script", - default=None, - help="Path to a local reuse-annotate-hook.py (skips downloading from remote)", - ) - parser.add_argument( - "--no-unicode-check-script", - default=None, - help="Path to a local no-unicode-check.py (skips downloading from remote)", - ) - parser.add_argument( - "--no-fix", - action="store_true", - help="Run in check-only mode (no auto-fix). Used in CI to report issues without modifying files.", - ) - parser.add_argument( - "--ignore-paths", - default="", - help="Comma-separated list of file patterns to ignore during REUSE checks (e.g., '*.md,docs/**,*.txt')", - ) - parser.add_argument( - "--no-unicode-extensions", - default="", - help="Comma-separated file extensions to check for non-ASCII characters (e.g., '.py,.rs'). Empty string disables the check.", - ) - parser.add_argument( - "--allowed-unicode-chars", - default="", - help="Comma-separated Unicode characters to allow in the no-unicode check (e.g., '\u00b5,\u00a7'). Empty by default.", - ) - - args = parser.parse_args() - - # Treat empty strings as unset (GitHub Actions passes "" for unset inputs) - if not args.copyright: - args.copyright = DEFAULT_COPYRIGHT - if not args.license: - args.license = DEFAULT_LICENSE - if not args.template: - args.template = DEFAULT_TEMPLATE - - branch = args.branch - cleanup_list = [] - config_path = args.config - config_is_temp = False - - try: - # Resolve pre-commit config: use local or download from remote - fix_mode = not args.no_fix - - if config_path is None: - config_url = CONFIG_URL_TEMPLATE.format(branch=branch) - print(f"Downloading pre-commit config from: {config_url}") - with tempfile.NamedTemporaryFile( - mode="w", suffix=".yml", delete=False - ) as f: - with urllib.request.urlopen(config_url) as response: - config_content = response.read().decode() - config_content = patch_config( - config_content, - fix_mode=fix_mode, - no_unicode_extensions=args.no_unicode_extensions, - allowed_unicode_chars=args.allowed_unicode_chars, - ) - f.write(config_content) - config_path = f.name - config_is_temp = True - else: - # Local config provided: always patch a temp copy to inject settings - config_content = Path(config_path).read_text() - config_content = patch_config( - config_content, - fix_mode=fix_mode, - no_unicode_extensions=args.no_unicode_extensions, - allowed_unicode_chars=args.allowed_unicode_chars, - ) - with tempfile.NamedTemporaryFile( - mode="w", suffix=".yml", delete=False - ) as f: - f.write(config_content) - config_path = f.name - config_is_temp = True - - # Resolve hook script: use local (--hook-script), CWD, or download. - # The script must end up at ./reuse-annotate-hook.py (CWD) because - # the pre-commit config entry is: python3 reuse-annotate-hook.py - hook_cwd_path = Path("reuse-annotate-hook.py") - hook_existed_in_cwd = hook_cwd_path.exists() - - if args.hook_script: - # Explicit path provided (e.g. from GitHub Action) - script_content = Path(args.hook_script).read_text() - elif hook_existed_in_cwd: - # Already in CWD (e.g. running inside this repo) - script_content = hook_cwd_path.read_text() - else: - # Download from remote - hook_url = HOOK_SCRIPT_URL_TEMPLATE.format(branch=branch) - print(f"Downloading reuse-annotate hook script from: {hook_url}") - with urllib.request.urlopen(hook_url) as response: - script_content = response.read().decode() - - # Patch default constants with configured values and write to CWD - patched = patch_hook_script( - script_content, - copyright_text=args.copyright, - license_id=args.license, - template=args.template, - ignore_paths=args.ignore_paths, - fix_mode=fix_mode, - ) - - hook_cwd_path.write_text(patched) - - # Clean up if we created the file (downloaded or copied from elsewhere) - if not hook_existed_in_cwd: - cleanup_list.append({"file": hook_cwd_path, "dirs": []}) - - # Ensure no-unicode-check.py is in CWD when the feature is enabled so - # the pre-commit entry 'python no-unicode-check.py' resolves correctly. - if args.no_unicode_extensions: - no_unicode_cwd_path = Path("no-unicode-check.py") - no_unicode_existed_in_cwd = no_unicode_cwd_path.exists() - - if args.no_unicode_check_script: - no_unicode_cwd_path.write_text( - Path(args.no_unicode_check_script).read_text() - ) - elif not no_unicode_existed_in_cwd: - cleanup_list.append( - download_if_missing( - no_unicode_cwd_path, - NO_UNICODE_CHECK_SCRIPT_URL_TEMPLATE.format(branch=branch), - "no-unicode-check script", - ) - ) - - if args.no_unicode_check_script and not no_unicode_existed_in_cwd: - cleanup_list.append({"file": no_unicode_cwd_path, "dirs": []}) - - # Ensure REUSE assets are available locally - reuse_toml_url = REUSE_TOML_URL_TEMPLATE.format(branch=branch) - cleanup_list.append( - download_if_missing( - "REUSE.toml", - reuse_toml_url, - "REUSE.toml", - ) - ) - - template_url = TEMPLATE_URL_TEMPLATE.format( - branch=branch, template=args.template - ) - cleanup_list.append( - download_if_missing( - f".reuse/templates/{args.template}.jinja2", - template_url, - f"reuse template '{args.template}'", - ) - ) - - license_url = LICENSE_URL_TEMPLATE.format(branch=branch, license=args.license) - cleanup_list.append( - download_if_missing( - f"LICENSES/{args.license}.txt", - license_url, - f"license text '{args.license}'", - ) - ) - - styles_url = STYLES_URL_TEMPLATE.format(branch=branch) - cleanup_list.append( - download_if_missing( - ".reuse/styles.toml", - styles_url, - "reuse comment styles config", - ) - ) - - clippy_lints_url = CLIPPY_LINTS_URL_TEMPLATE.format(branch=branch) - cleanup_list.append( - download_if_missing( - "shared-lints/shared-lints.toml", - clippy_lints_url, - "Clippy lints config", - ) - ) - clippy_lints_check_script_url = CLIPPY_LINTS_CHECK_SCRIPT_URL_TEMPLATE.format( - branch=branch - ) - cleanup_list.append( - download_if_missing( - "shared-lints/check_cargo_lints.py", - clippy_lints_check_script_url, - "Clippy lints check script", - ) - ) - - if not fix_mode: - env = {**os.environ, "SKIP": "reuse-annotate"} - else: - env = None - - print("Running pre-commit checks...") - result = subprocess.run( - ["pre-commit", "run", "--all-files", "--config", config_path], - check=False, - env=env, - ) - sys.exit(result.returncode) - except urllib.error.HTTPError as e: - print(f"Error downloading config: {e}", file=sys.stderr) - print( - f"Make sure the branch '{branch}' exists in the repository.", - file=sys.stderr, - ) - sys.exit(1) - except urllib.error.URLError as e: - print( - f"Network error: {e.reason}", - file=sys.stderr, - ) - print( - "Could not reach GitHub. Please check your internet connection and try again.", - file=sys.stderr, - ) - sys.exit(1) - except Exception as e: - print(f"Error: {e}", file=sys.stderr) - sys.exit(1) - finally: - if config_is_temp and config_path: - Path(config_path).unlink(missing_ok=True) - if cleanup_list: - cleanup_downloads(cleanup_list) - - -if __name__ == "__main__": - main() diff --git a/rust-lint-and-format-action/action.yml b/rust-lint-and-format-action/action.yml index 0a9fe11..6333443 100644 --- a/rust-lint-and-format-action/action.yml +++ b/rust-lint-and-format-action/action.yml @@ -7,10 +7,8 @@ # This program and the accompanying materials are made available under the # terms of the Apache License Version 2.0 which is available at # https://www.apache.org/licenses/LICENSE-2.0 - name: 'Format and Clippy' description: 'Run rustfmt and clippy checks with reviewdog' - inputs: toolchain: description: 'Rust toolchain version to use' @@ -38,15 +36,10 @@ inputs: description: 'Whether to enable reviewdog for rustfmt' required: false default: 'true' - error-on-unformatted: - description: 'Whether to treat unformatted code as an error in rustfmt' - required: false - default: 'true' reporter: description: 'Reporter to use for reviewdog' required: false default: 'github-pr-review' - runs: using: 'composite' steps: @@ -57,12 +50,10 @@ runs: components: clippy, rustfmt cache-shared-key: ${{ inputs.toolchain }} rustflags: "" - - name: Install reviewdog uses: reviewdog/action-setup@v1 with: reviewdog_version: latest - - name: Formatting (rustfmt via reviewdog) if: ${{ inputs.rustfmt-enable-reviewdog == 'true' }} continue-on-error: ${{ inputs.fail-on-format-error == 'false' }} @@ -81,19 +72,12 @@ runs: STDERR_SEVERITY="warning" fi - # rustfmt reports two kinds of issues: - # 1. Fixable formatting - emitted as checkstyle XML on stdout (exit 0) - # 2. Overflow/unformatted - emitted as diagnostics on stderr (exit 1) - # We capture stderr separately so we can merge both into the XML for - # reviewdog. Without "|| FMT_EXIT=$?", GHA's default "set -eo pipefail" - # would kill the step before reviewdog runs, losing ALL annotations. + source "${{ github.action_path }}/../shared-config/rustfmt-config.sh" + FMT_EXIT=0 cargo fmt -- \ --emit=checkstyle \ - --config error_on_unformatted=${{ inputs.error-on-unformatted }},error_on_line_overflow=true,format_strings=true \ - --config group_imports=StdExternalCrate \ - --config imports_granularity=Crate \ - --config hex_literal_case=Upper \ + --config "$RUSTFMT_CONFIG" \ 2>/tmp/rustfmt_stderr.log \ | python3 "${{ github.action_path }}/fix-checkstyle-xml.py" \ | eval "$SEVERITY_TRANSFORM" \ @@ -120,19 +104,11 @@ runs: if [ "$FMT_EXIT" -ne 0 ]; then exit "$FMT_EXIT" fi - - name: Formatting (rustfmt without reviewdog) if: ${{ inputs.rustfmt-enable-reviewdog == 'false' }} continue-on-error: ${{ inputs.fail-on-format-error == 'false' }} shell: bash - run: | - cargo fmt -- \ - --check \ - --config error_on_unformatted=${{ inputs.error-on-unformatted }},error_on_line_overflow=true,format_strings=true \ - --config group_imports=StdExternalCrate \ - --config imports_granularity=Crate \ - --config hex_literal_case=Upper - + run: ${{ github.action_path }}/../shared-config/cargo-fmt.sh --check - name: Clippy uses: giraffate/clippy-action@v1 with: diff --git a/shared-config/.markdownlint.yaml b/shared-config/.markdownlint.yaml new file mode 100644 index 0000000..f9db163 --- /dev/null +++ b/shared-config/.markdownlint.yaml @@ -0,0 +1,16 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: 2026 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +default: true +MD013: + line_length: 1000 +MD024: + siblings_only: true +MD033: false +MD060: false diff --git a/shared-config/.rustfmt.toml b/shared-config/.rustfmt.toml new file mode 100644 index 0000000..f972fe9 --- /dev/null +++ b/shared-config/.rustfmt.toml @@ -0,0 +1,15 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: 2026 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +style_edition = "2024" +max_width = 100 +newline_style = "Unix" +group_imports = "StdExternalCrate" +imports_granularity = "Crate" +unstable_features = true diff --git a/shared-config/.yamlfmt b/shared-config/.yamlfmt new file mode 100644 index 0000000..9f2a31b --- /dev/null +++ b/shared-config/.yamlfmt @@ -0,0 +1,12 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: 2026 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +formatter: + type: basic + retain_line_breaks_single: true diff --git a/shared-config/cargo-clippy.sh b/shared-config/cargo-clippy.sh new file mode 100755 index 0000000..333b51b --- /dev/null +++ b/shared-config/cargo-clippy.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash + +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: 2026 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 + +set -euo pipefail + +if [ ! -f Cargo.toml ]; then + exit 0 +fi + +if [ $# -gt 0 ]; then + exec cargo clippy --locked "$@" +else + exec cargo clippy --locked --all-targets -- -D warnings +fi diff --git a/shared-config/cargo-fmt.sh b/shared-config/cargo-fmt.sh new file mode 100755 index 0000000..4e1d9a8 --- /dev/null +++ b/shared-config/cargo-fmt.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash + +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: 2026 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 + +set -euo pipefail + +if [ ! -f Cargo.toml ]; then + exit 0 +fi + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck disable=SC1091 +source "$SCRIPT_DIR/rustfmt-config.sh" + +CHECK_FLAG="" +if [[ "${1:-}" == "--check" ]]; then + CHECK_FLAG="--check" +fi + +# shellcheck disable=SC2086 +exec cargo fmt --all $CHECK_FLAG -- \ + --config "$RUSTFMT_CONFIG" diff --git a/shared-config/ruff.toml b/shared-config/ruff.toml new file mode 100644 index 0000000..66fd595 --- /dev/null +++ b/shared-config/ruff.toml @@ -0,0 +1,27 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: 2026 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +line-length = 100 + +[lint] +select = [ + "E", # pycodestyle (error) + "F", # pyflakes + "B", # bugbear + "B9", + "C4", # flake8-comprehensions + "SIM", # flake8-simplify + "I", # isort + "UP", # pyupgrade + "PIE", # flake8-pie + "PGH", # pygrep-hooks + "PYI", # flake8-pyi + "RUF", + "PT", # flake8-pytest-style +] diff --git a/shared-config/rustfmt-config.sh b/shared-config/rustfmt-config.sh new file mode 100644 index 0000000..f861e39 --- /dev/null +++ b/shared-config/rustfmt-config.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: 2026 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 + +# shellcheck disable=SC2034 +RUSTFMT_CONFIG="format_strings=true,group_imports=StdExternalCrate,imports_granularity=Crate,hex_literal_case=Upper" diff --git a/shared-lints/README.md b/shared-lints/README.md index 7485788..2291be4 100644 --- a/shared-lints/README.md +++ b/shared-lints/README.md @@ -16,7 +16,9 @@ This directory contains shared Clippy lint configurations for Rust projects in t ## Overview -The `shared-lints.toml` file defines a standardized set of Clippy lints that should be applied across all OpenSOVD Rust projects as agreed upon in the [ADR](https://github.com/eclipse-opensovd/opensovd/pull/80). These lints are automatically checked by the `pre-commit-action` to ensure consistency. +The `shared-lints.toml` file defines a standardized set of Clippy lints that should be applied across all OpenSOVD Rust projects +as agreed upon in the [ADR](https://github.com/eclipse-opensovd/opensovd/pull/80). +These lints are automatically checked by the `pre-commit-action` to ensure consistency. ## Usage @@ -32,6 +34,7 @@ Validates that a `Cargo.toml` file contains all lints from `shared-lints.toml` w This is the script used by the pre-commit-action. **Usage:** + ```bash ./check_cargo_lints.py path/to/Cargo.toml ``` @@ -39,18 +42,21 @@ This is the script used by the pre-commit-action. **Example outputs:** ✅ **All lints present and correct:** -``` + +```text ✓ All 7 shared lints are correctly configured in Cargo.toml ``` ❌ **Missing lints:** -``` + +```text ✗ Missing 1 lint(s) in Cargo.toml: - separated_literal_suffix = {'level': 'deny'} ``` ❌ **Configuration mismatch:** -``` + +```text ✗ 1 lint(s) have different configurations: - unwrap_used: Shared: {'level': 'deny'} diff --git a/shared-lints/check_cargo_lints.py b/shared-lints/check_cargo_lints.py index 0b064bd..0c9a757 100755 --- a/shared-lints/check_cargo_lints.py +++ b/shared-lints/check_cargo_lints.py @@ -13,10 +13,11 @@ """Compare Cargo.toml [workspace.lints] with shared-lints.toml.""" import sys -import tomllib from pathlib import Path from typing import Any +import tomllib + def normalize_lint_config(config: Any) -> dict[str, Any]: """ @@ -65,17 +66,12 @@ def load_cargo_lints(cargo_toml_path: Path) -> dict[str, dict[str, Any]]: # Navigate to workspace.lints.clippy if exists, otherwise # assume this is a non workspace Cargo.toml and look for lints at the top level - if "workspace" in data: - workspace = data["workspace"] - else: - workspace = data + workspace = data.get("workspace", data) lints = workspace.get("lints", {}) clippy_lints = lints.get("clippy", {}) - return { - lint: normalize_lint_config(config) for lint, config in clippy_lints.items() - } + return {lint: normalize_lint_config(config) for lint, config in clippy_lints.items()} def compare_lints( @@ -122,10 +118,12 @@ def main(): print(f"Error: {cargo_toml_path} not found", file=sys.stderr) sys.exit(1) - # Default to shared-lints.toml in the same directory as this script script_dir = Path(__file__).parent shared_lints_path = script_dir / "shared-lints.toml" + if not shared_lints_path.exists(): + shared_lints_path = Path("shared-lints") / "shared-lints.toml" + if not shared_lints_path.exists(): print(f"Error: {shared_lints_path} not found", file=sys.stderr) sys.exit(1) @@ -140,7 +138,8 @@ def main(): # Report results if not missing and not mismatched: print( - f"[OK] All {len(shared_lints)} shared lints are correctly configured in {cargo_toml_path}" + f"[OK] All {len(shared_lints)} shared lints are correctly" + f" configured in {cargo_toml_path}" ) return 0 diff --git a/shared-lints/shared-lints.toml b/shared-lints/shared-lints.toml index af41ec3..4418744 100644 --- a/shared-lints/shared-lints.toml +++ b/shared-lints/shared-lints.toml @@ -9,7 +9,7 @@ # https://www.apache.org/licenses/LICENSE-2.0 # enable pedantic -pedantic = { level = "warn", priority = -1 } +pedantic = { level = "deny", priority = -1 } ## exclude some too pedantic lints for now similar_names = "allow" @@ -26,7 +26,7 @@ arithmetic_side_effects = "deny" ## lints related to readability of code # enforce that references are cloned via eg. `Arc::clone` instead of `.clone()` # making it explit that a reference is cloned here and not the underlying data. -clone_on_ref_ptr = "warn" +clone_on_ref_ptr = "deny" # enforce that the type suffix of a literal is always appended directly # eg. 12u8 instead of 12_u8 separated_literal_suffix = "deny" From 168641338d9da46513aa884e5a1211032fea31a8 Mon Sep 17 00:00:00 2001 From: Alexander Mohr Date: Fri, 29 May 2026 11:52:54 +0200 Subject: [PATCH 2/3] fix: review findings --- .github/workflows/checks.yml | 8 +-- .pre-commit-hooks.yaml | 2 +- AGENTS.md | 50 +++++++++++++++++++ ...heck-hooks.py => check-hooks-installed.py} | 0 pre-commit-action/no-banner-comment-check.py | 19 ++++++- pre-commit-action/no-unicode-check.py | 2 + pre-commit-action/reuse-annotate-hook.py | 4 +- ruff.toml | 4 +- rust-lint-and-format-action/action.yml | 4 +- shared-config/.rustfmt.toml | 2 + shared-config/cargo-clippy.sh | 5 +- shared-config/cargo-fmt.sh | 5 +- shared-config/ruff.toml | 4 +- shared-config/rustfmt-config.sh | 14 ------ 14 files changed, 90 insertions(+), 33 deletions(-) create mode 100644 AGENTS.md rename pre-commit-action/{check-hooks.py => check-hooks-installed.py} (100%) delete mode 100644 shared-config/rustfmt-config.sh diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index e687bc4..21ac12a 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -22,10 +22,12 @@ on: description: 'Rust toolchain to install (e.g. "stable", "nightly-2025-07-14")' required: false type: string + default: 'stable' go-version: description: 'Go version for gitleaks hook' required: false type: string + default: '1.25' permissions: contents: read @@ -35,7 +37,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: submodules: false fetch-depth: 0 @@ -47,8 +49,8 @@ jobs: - name: Run checks uses: ./pre-commit-action with: - rust-toolchain: ${{ inputs.rust-toolchain || 'stable' }} - go-version: ${{ inputs.go-version || '1.25' }} + rust-toolchain: ${{ inputs.rust-toolchain }} + go-version: ${{ inputs.go-version }} - name: Validate commit subjects (Conventional Commits) if: github.event_name == 'pull_request' diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index 6bb2c13..8cb47b6 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -44,7 +44,7 @@ - id: check-hooks name: Validate hook configuration - entry: pre-commit-action/check-hooks.py + entry: pre-commit-action/check-hooks-installed.py language: script always_run: true pass_filenames: false diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..3fcfc64 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,50 @@ + + +# AI Agent Guidelines + +Guidance for AI coding assistants working in this repository. + +## Do Not Generate Banner Comments + +Do not generate banner-style decorative comments such as repeated `=` or `-` lines. +They add noise and will be rejected by the `no-banner-comment-check` hook. + +Prefer clear names, small modules, and well-structured functions instead. + +## Code Style + +### Python + +- Line length: 100 characters +- Formatter: `ruff format` +- Linter: `ruff check` with rules from `shared-config/ruff.toml` + +### Rust + +- Formatter: `cargo fmt` with `shared-config/.rustfmt.toml` +- Linter: `cargo clippy --all-features --all-targets -- -D warnings` +- Max line width: 100 characters + +### Shell + +- Shell scripts must pass `shellcheck` + +## License Headers (REUSE/SPDX) + +Every source file must include SPDX license headers. For Markdown files like this one, use an HTML comment block. + +Do not remove or alter SPDX headers added by `reuse-annotate`. + +## Sharing With Consumer Repos + +Consumer repositories may copy or adapt this file for their own agent guidance. There is no automated distribution mechanism yet; keep copies in sync manually or reference a pinned canonical version. diff --git a/pre-commit-action/check-hooks.py b/pre-commit-action/check-hooks-installed.py similarity index 100% rename from pre-commit-action/check-hooks.py rename to pre-commit-action/check-hooks-installed.py diff --git a/pre-commit-action/no-banner-comment-check.py b/pre-commit-action/no-banner-comment-check.py index 68c6fcd..b1a9ad7 100755 --- a/pre-commit-action/no-banner-comment-check.py +++ b/pre-commit-action/no-banner-comment-check.py @@ -14,6 +14,21 @@ # dependencies = [] # /// +""" +Pre-commit hook: flag banner-style comments. + +Banner comments (lines filled with repeated characters like ===, ---, ###) +are banned because: +1. AI code generators (LLMs) tend to insert decorative banners that add visual + noise without conveying information not already expressed by the code structure. +2. They reduce signal-to-noise ratio in diffs and code review. +3. Section organization should be achieved through module structure, not ASCII art. + +Banned patterns are comment lines where most content consists of repeated +fill characters (e.g. 30+ repeated '=', '-', or '*'), optionally surrounding +a short section label. +""" + import argparse import re import sys @@ -21,7 +36,7 @@ COMMENT_PREFIXES = ("///", "//!", "//", "/*", "*/", "#!", "#", "*") -def build_banner_pattern(banner_chars, min_length): +def build_banner_patterns(banner_chars, min_length): """Return compiled regexes that together match banner-style comment lines. Two patterns are combined: @@ -103,7 +118,7 @@ def main(): if not args.files: sys.exit(0) - patterns = build_banner_pattern(args.banner_chars, args.min_length) + patterns = build_banner_patterns(args.banner_chars, args.min_length) found_violations = False for path in args.files: diff --git a/pre-commit-action/no-unicode-check.py b/pre-commit-action/no-unicode-check.py index 2fb4846..d71b48a 100755 --- a/pre-commit-action/no-unicode-check.py +++ b/pre-commit-action/no-unicode-check.py @@ -10,6 +10,8 @@ # terms of the Apache License Version 2.0 which is available at # https://www.apache.org/licenses/LICENSE-2.0 +# PEP 723 inline script metadata - evaluated by `uv run` to resolve dependencies. +# See: https://peps.python.org/pep-0723/ # /// script # dependencies = [] # /// diff --git a/pre-commit-action/reuse-annotate-hook.py b/pre-commit-action/reuse-annotate-hook.py index 43b1d95..0ae0b75 100755 --- a/pre-commit-action/reuse-annotate-hook.py +++ b/pre-commit-action/reuse-annotate-hook.py @@ -53,7 +53,7 @@ _REPO_ROOT = Path(__file__).resolve().parent.parent -def _ensure_reuse_assets(license_id: str, template: str) -> None: +def _ensure_reuse_assets_available(license_id: str, template: str) -> None: """Copy REUSE assets from the prek-cached cicd-workflows clone into CWD. ``reuse annotate`` expects REUSE.toml / LICENSES / templates in the @@ -302,7 +302,7 @@ def main() -> int: if not files: return 0 - _ensure_reuse_assets(license_id, template) + _ensure_reuse_assets_available(license_id, template) # Template flag tpl_flag: list[str] = [] diff --git a/ruff.toml b/ruff.toml index 66fd595..c1b7011 100644 --- a/ruff.toml +++ b/ruff.toml @@ -10,11 +10,11 @@ line-length = 100 [lint] +# Rule documentation: https://docs.astral.sh/ruff/rules/ select = [ "E", # pycodestyle (error) "F", # pyflakes "B", # bugbear - "B9", "C4", # flake8-comprehensions "SIM", # flake8-simplify "I", # isort @@ -22,6 +22,6 @@ select = [ "PIE", # flake8-pie "PGH", # pygrep-hooks "PYI", # flake8-pyi - "RUF", + "RUF", # Ruff-specific "PT", # flake8-pytest-style ] diff --git a/rust-lint-and-format-action/action.yml b/rust-lint-and-format-action/action.yml index 6333443..bde5a57 100644 --- a/rust-lint-and-format-action/action.yml +++ b/rust-lint-and-format-action/action.yml @@ -72,12 +72,10 @@ runs: STDERR_SEVERITY="warning" fi - source "${{ github.action_path }}/../shared-config/rustfmt-config.sh" - FMT_EXIT=0 cargo fmt -- \ --emit=checkstyle \ - --config "$RUSTFMT_CONFIG" \ + --config-path "${{ github.action_path }}/../shared-config/.rustfmt.toml" \ 2>/tmp/rustfmt_stderr.log \ | python3 "${{ github.action_path }}/fix-checkstyle-xml.py" \ | eval "$SEVERITY_TRANSFORM" \ diff --git a/shared-config/.rustfmt.toml b/shared-config/.rustfmt.toml index f972fe9..b501738 100644 --- a/shared-config/.rustfmt.toml +++ b/shared-config/.rustfmt.toml @@ -13,3 +13,5 @@ newline_style = "Unix" group_imports = "StdExternalCrate" imports_granularity = "Crate" unstable_features = true +format_strings = true +hex_literal_case = "Upper" diff --git a/shared-config/cargo-clippy.sh b/shared-config/cargo-clippy.sh index 333b51b..d7df0c9 100755 --- a/shared-config/cargo-clippy.sh +++ b/shared-config/cargo-clippy.sh @@ -16,8 +16,11 @@ if [ ! -f Cargo.toml ]; then exit 0 fi +# Default: --all-features --all-targets -D warnings +# Override: pass any args to take full control (e.g. to omit --all-features): +# args: ["--all-targets", "--", "-D", "warnings"] if [ $# -gt 0 ]; then exec cargo clippy --locked "$@" else - exec cargo clippy --locked --all-targets -- -D warnings + exec cargo clippy --locked --all-features --all-targets -- -D warnings fi diff --git a/shared-config/cargo-fmt.sh b/shared-config/cargo-fmt.sh index 4e1d9a8..0e5dba2 100755 --- a/shared-config/cargo-fmt.sh +++ b/shared-config/cargo-fmt.sh @@ -17,8 +17,7 @@ if [ ! -f Cargo.toml ]; then fi SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# shellcheck disable=SC1091 -source "$SCRIPT_DIR/rustfmt-config.sh" +RUSTFMT_TOML="$SCRIPT_DIR/.rustfmt.toml" CHECK_FLAG="" if [[ "${1:-}" == "--check" ]]; then @@ -27,4 +26,4 @@ fi # shellcheck disable=SC2086 exec cargo fmt --all $CHECK_FLAG -- \ - --config "$RUSTFMT_CONFIG" + --config-path "$RUSTFMT_TOML" diff --git a/shared-config/ruff.toml b/shared-config/ruff.toml index 66fd595..c1b7011 100644 --- a/shared-config/ruff.toml +++ b/shared-config/ruff.toml @@ -10,11 +10,11 @@ line-length = 100 [lint] +# Rule documentation: https://docs.astral.sh/ruff/rules/ select = [ "E", # pycodestyle (error) "F", # pyflakes "B", # bugbear - "B9", "C4", # flake8-comprehensions "SIM", # flake8-simplify "I", # isort @@ -22,6 +22,6 @@ select = [ "PIE", # flake8-pie "PGH", # pygrep-hooks "PYI", # flake8-pyi - "RUF", + "RUF", # Ruff-specific "PT", # flake8-pytest-style ] diff --git a/shared-config/rustfmt-config.sh b/shared-config/rustfmt-config.sh deleted file mode 100644 index f861e39..0000000 --- a/shared-config/rustfmt-config.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env bash - -# SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: 2026 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) -# -# See the NOTICE file(s) distributed with this work for additional -# information regarding copyright ownership. -# -# This program and the accompanying materials are made available under the -# terms of the Apache License Version 2.0 which is available at -# https://www.apache.org/licenses/LICENSE-2.0 - -# shellcheck disable=SC2034 -RUSTFMT_CONFIG="format_strings=true,group_imports=StdExternalCrate,imports_granularity=Crate,hex_literal_case=Upper" From 67346675ce55ae7d3719da0748472dfaa736dedd Mon Sep 17 00:00:00 2001 From: Alexander Mohr Date: Fri, 29 May 2026 21:52:56 +0200 Subject: [PATCH 3/3] fix: remaining uv dependcies --- pre-commit-action/check-hooks-installed.py | 1 + pre-commit-action/no-banner-comment-check.py | 1 + pre-commit-action/no-unicode-check.py | 3 +-- pre-commit-action/reuse-annotate-hook.py | 1 + 4 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pre-commit-action/check-hooks-installed.py b/pre-commit-action/check-hooks-installed.py index 8fef7bb..9a6c937 100755 --- a/pre-commit-action/check-hooks-installed.py +++ b/pre-commit-action/check-hooks-installed.py @@ -10,6 +10,7 @@ # terms of the Apache License Version 2.0 which is available at # https://www.apache.org/licenses/LICENSE-2.0 +# Dependency specification for `uv run`. See: https://peps.python.org/pep-0723 # /// script # dependencies = [] # /// diff --git a/pre-commit-action/no-banner-comment-check.py b/pre-commit-action/no-banner-comment-check.py index b1a9ad7..a249cd8 100755 --- a/pre-commit-action/no-banner-comment-check.py +++ b/pre-commit-action/no-banner-comment-check.py @@ -10,6 +10,7 @@ # terms of the Apache License Version 2.0 which is available at # https://www.apache.org/licenses/LICENSE-2.0 +# Dependency specification for `uv run`. See: https://peps.python.org/pep-0723 # /// script # dependencies = [] # /// diff --git a/pre-commit-action/no-unicode-check.py b/pre-commit-action/no-unicode-check.py index d71b48a..a224d8c 100755 --- a/pre-commit-action/no-unicode-check.py +++ b/pre-commit-action/no-unicode-check.py @@ -10,8 +10,7 @@ # terms of the Apache License Version 2.0 which is available at # https://www.apache.org/licenses/LICENSE-2.0 -# PEP 723 inline script metadata - evaluated by `uv run` to resolve dependencies. -# See: https://peps.python.org/pep-0723/ +# Dependency specification for `uv run`. See: https://peps.python.org/pep-0723 # /// script # dependencies = [] # /// diff --git a/pre-commit-action/reuse-annotate-hook.py b/pre-commit-action/reuse-annotate-hook.py index 0ae0b75..1e7bf49 100755 --- a/pre-commit-action/reuse-annotate-hook.py +++ b/pre-commit-action/reuse-annotate-hook.py @@ -10,6 +10,7 @@ # terms of the Apache License Version 2.0 which is available at # https://www.apache.org/licenses/LICENSE-2.0 +# Dependency specification for `uv run`. See: https://peps.python.org/pep-0723 # /// script # dependencies = ["tomli>=1.1.0"] # ///