Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
79 changes: 36 additions & 43 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -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 }}
2 changes: 1 addition & 1 deletion .github/workflows/unittest.yml
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
14 changes: 14 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -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]
80 changes: 79 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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:
Expand Down
86 changes: 86 additions & 0 deletions cliff.toml
Original file line number Diff line number Diff line change
@@ -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" },
]
11 changes: 10 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -110,4 +111,12 @@ known-third-party = ["yara"]
[tool.ruff.format]
quote-style = "double"
indent-style = "space"
line-ending = "auto"
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

Loading