diff --git a/.github/dependabot.yml b/.github/dependabot.yml index a6ff353..7562305 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,7 +5,12 @@ version: 2 updates: - - package-ecosystem: "pip" + - package-ecosystem: "pip" directory: "/" # Location of package manifests schedule: interval: "weekly" + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e228b2a..ec0017b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,58 +1,51 @@ -name: Build and Publish Docker Images +name: Version & Release + +# IMPORTANT: This workflow relies on branch protection rules to ensure +# code quality before releasing. The `main` branch MUST have required +# status checks enabled (codestyle + unittest must pass before merge). +# Without branch protection, broken code could be released. on: - workflow_dispatch: push: - # branches: - # - "main" - tags: - - "v*" + branches: [main] permissions: - contents: read - packages: write + contents: write + pull-requests: read jobs: - backend: + release: runs-on: ubuntu-latest - steps: - name: Checkout - uses: actions/checkout@v4 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Docker meta - id: meta - uses: docker/metadata-action@v5 + uses: actions/checkout@v6 with: - images: | - ghcr.io/EC-DIGIT-CSIRC/sysdiagnose - tags: | - type=ref,event=branch - type=ref,event=pr - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - labels: | - org.opencontainers.image.title=SysDiagnose - org.opencontainers.image.description=SysDiagnose - org.opencontainers.image.vendor=EC-DIGIT-CSIRC + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} - - name: Login to GHCR - if: github.event_name != 'pull_request' - uses: docker/login-action@v3 + - name: Python Semantic Release + id: psr + uses: python-semantic-release/python-semantic-release@v10 with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} + github_token: ${{ secrets.GITHUB_TOKEN }} + changelog: "false" - - name: Build and push - uses: docker/build-push-action@v5 + - name: Generate release notes with git-cliff + id: git-cliff + if: steps.psr.outputs.released == 'true' + uses: orhun/git-cliff-action@v4 with: - context: ./ - push: ${{ github.event_name != 'pull_request' }} - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - - + config: cliff.toml + args: --verbose --latest --strip header + env: + OUTPUT: CHANGELOG.md + GITHUB_REPO: ${{ github.repository }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Update GitHub Release notes + if: steps.psr.outputs.released == 'true' + uses: softprops/action-gh-release@v3 + with: + tag_name: v${{ steps.psr.outputs.version }} + body: ${{ steps.git-cliff.outputs.content }} + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml index 194a543..08ae44e 100644 --- a/.github/workflows/unittest.yml +++ b/.github/workflows/unittest.yml @@ -1,7 +1,7 @@ # This workflow will install Python dependencies, run tests # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python -name: Python application +name: Unit Test on: push: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..4258ce6 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,14 @@ +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.15.12 + hooks: + - id: ruff-check + args: [--fix] + - id: ruff-format + + - repo: https://github.com/compilerla/conventional-pre-commit + rev: v4.1.0 + hooks: + - id: conventional-pre-commit + stages: [commit-msg] + args: [feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert] diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 45f3fa9..01bee9f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,7 +6,12 @@ Thank you for your interest in contributing to SAF! This document will help you 1. Fork the repository 2. Create a virtual environment and install dependencies (see [README.md](README.md#installation)) -3. Read the [Developer Guidelines](docs/developer_guidelines.md) before writing code +3. Install pre-commit hooks: + ```bash + pip install pre-commit + pre-commit install --hook-type commit-msg --hook-type pre-commit + ``` +4. Read the [Developer Guidelines](docs/developer_guidelines.md) before writing code ## Developer Guidelines @@ -47,6 +52,79 @@ python -m unittest discover tests/ - Keep `execute()` focused on parsing logic — let the base class handle I/O and summary tracking - Prefer `logger.warning()` over raising exceptions for expected conditions (missing files, empty data) +## Commit Messages + +This project enforces [Conventional Commits](https://www.conventionalcommits.org/) via a pre-commit hook. Every commit message must follow this format: + +``` +type(optional scope): description + +[optional body] + +[optional footer] +``` + +### Allowed types + +| Type | Purpose | Version bump | +|------|---------|--------------| +| `feat` | New feature | MINOR | +| `fix` | Bug fix | PATCH | +| `docs` | Documentation only | PATCH | +| `style` | Code style (formatting, no logic change) | PATCH | +| `refactor` | Code change that neither fixes a bug nor adds a feature | PATCH | +| `perf` | Performance improvement | PATCH | +| `test` | Adding or correcting tests | PATCH | +| `build` | Build system or external dependencies | PATCH | +| `ci` | CI configuration | PATCH | +| `chore` | Maintenance tasks | PATCH | +| `revert` | Reverting a previous commit | PATCH | + +### Breaking changes + +To trigger a MAJOR version bump, either: +- Add a `!` after the type: `feat!: redesign API` +- Include a `BREAKING CHANGE:` footer in the commit body + +### Examples + +``` +feat: add new parser for transparency logs +fix(containermanager): handle empty plist gracefully +docs: update developer guidelines with iOS version info +feat!: remove deprecated analyser interface +``` + +## Automated Releases + +Releases are fully automated. When a PR is merged to `main`, the release workflow: + +1. **Analyses commits** since the last version tag using [python-semantic-release](https://github.com/python-semantic-release/python-semantic-release) +2. **Determines the version bump** — the highest-priority change wins (MAJOR > MINOR > PATCH) +3. **Updates `pyproject.toml`** with the new version and commits with `[skip ci]` +4. **Creates an annotated tag** (`v{MAJOR}.{MINOR}.{PATCH}`) +5. **Publishes a GitHub Release** with changelog generated by [git-cliff](https://git-cliff.org/) + +### Changelog and attribution + +The GitHub Release notes are generated by git-cliff and include: +- Commits grouped by type (Features, Bug Fixes, Performance, etc.) +- PR title (when available) or commit message +- Author's GitHub @username linked to their profile +- A "New Contributors" section for first-time contributors + +Your contributions will be automatically credited in the release notes. + +### Branch protection + +The `main` branch is protected — PRs require both `codestyle` and `unittest` checks to pass before merging. The release workflow trusts that anything on `main` has already passed CI. + +### What you don't need to do + +- Don't update the version in `pyproject.toml` manually — the workflow handles it +- Don't create tags manually — the workflow handles it +- Don't write changelog entries — they are generated from your commit messages + ## Reporting Issues When reporting bugs, please include: diff --git a/cliff.toml b/cliff.toml new file mode 100644 index 0000000..3f919f6 --- /dev/null +++ b/cliff.toml @@ -0,0 +1,86 @@ +# git-cliff configuration +# https://git-cliff.org/docs/configuration + +[changelog] +# template for the changelog body +# https://keats.github.io/tera/docs/#introduction +body = """ +## What's Changed +{%- if version %} in {{ version }}{%- endif -%} +{% for group, commits in commits | group_by(attribute="group") %} + +### {{ group | upper_first }} +{% for commit in commits %} + {% if commit.remote.pr_title -%} + {%- set commit_message = commit.remote.pr_title -%} + {%- else -%} + {%- set commit_message = commit.message -%} + {%- endif -%} + - [`{{ commit.id | truncate(length=7, end="") }}`]({{ self::remote_url() }}/commit/{{ commit.id }}): \ + {{ commit_message | split(pat="\n") | first | trim }}\ + {% if commit.remote.username %} (@{{ commit.remote.username }}){%- endif %} +{%- endfor %} +{%- endfor %} +{% if remote is defined and remote.contributors is defined and remote.contributors | filter(attribute="is_first_time", value=true) | length != 0 %} + +## New Contributors +{%- for contributor in remote.contributors | filter(attribute="is_first_time", value=true) %} +- @{{ contributor.username }} made their first contribution\ + {%- if contributor.pr_number %} in \ + [#{{ contributor.pr_number }}]({{ self::remote_url() }}/pull/{{ contributor.pr_number }})\ + {%- endif %} +{%- endfor %} +{%- endif %} +{% if version %} + {% if previous.version %} + +**Full Changelog**: {{ self::remote_url() }}/compare/{{ previous.version }}...{{ version }} + {% endif %} +{% endif %} +{%- macro remote_url() -%} + https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }} +{%- endmacro -%} +""" +trim = true +footer = "" +postprocessors = [] + +[git] +# parse the commits based on https://www.conventionalcommits.org +conventional_commits = true +# filter out the commits that are not conventional +filter_unconventional = false +# process each line of a commit as an individual commit +split_commits = false +# regex for preprocessing the commit messages +commit_preprocessors = [ + { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "" }, +] +# protect breaking changes from being skipped due to matching a skipping commit_parser +protect_breaking_commits = true +# filter out the commits that are not matched by commit parsers +filter_commits = true +# regex for matching git tags +tag_pattern = "v[0-9].*" +# sort the tags topologically +topo_order = false +# sort the commits inside sections by oldest/newest order +sort_commits = "newest" +# limit the number of commits included in the changelog +# limit_commits = 42 + +# commit parsers - group commits by type +commit_parsers = [ + { message = "^feat", group = "New Features" }, + { message = "^fix", group = "Bug Fixes" }, + { message = "^doc", group = "Documentation" }, + { message = "^perf", group = "Performance" }, + { message = "^refactor", group = "Refactoring" }, + { message = "^style", group = "Style" }, + { message = "^test", group = "Testing" }, + { message = "^chore\\(release\\)", skip = true }, + { message = "^chore", group = "Other" }, + { message = "^ci", group = "CI/CD" }, + { message = "^build", group = "Build" }, + { body = ".*security", group = "Security" }, +] diff --git a/pyproject.toml b/pyproject.toml index 2217ba5..47ba384 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,6 +43,7 @@ dependencies = [ [project.optional-dependencies] dev = [ "ruff>=0.11.0,<0.16.0", + "pre-commit>=4.0.0,<5.0.0", ] [project.urls] @@ -110,4 +111,12 @@ known-third-party = ["yara"] [tool.ruff.format] quote-style = "double" indent-style = "space" -line-ending = "auto" \ No newline at end of file +line-ending = "auto" + +[tool.semantic_release] +version_toml = ["pyproject.toml:project.version"] +commit_message = "chore(release): {version} [skip ci]" +# Anything MAY change at any time. +allow_zero_version = true +major_on_zero = false +