From 90d3b59c41428925cde9ed17350294035a4ecf28 Mon Sep 17 00:00:00 2001 From: Andrei Cristea Date: Thu, 23 Oct 2025 17:45:19 +0200 Subject: [PATCH 1/3] wip --- CHANGELOG.md | 143 ++++++++++++++++++++++++++++++++++++----------- build.gradle.kts | 4 +- 2 files changed, 113 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 997e44b..3e1645e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,42 +2,121 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -and this project uses a **date-based versioning scheme** (`YYYY.MM.DD`). +and this project uses a **date-based versioning scheme** (`YYYY.MM.DD`). + +Legend: 🆕 New • ❌ Removed • 🟢 Patch • 🔵 Minor • 🔴 Major ## [Unreleased] +## [2025.10.23] + +| Component | Version | Status | Prev.
Version | +|:------------------------------------------------|:-------:|:------:|:----------------:| +| **Keypop Dependencies** | | | | +| [keypop-reader-java-api] | `2.0.1` | | | +| [keypop-calypso-card-java-api] | `2.1.2` | | | +| [keypop-calypso-crypto-legacysam-java-api] | `0.7.0` | | | +| [keypop-storagecard-java-api] | `0.3.0` | | | +| | | | | +| **Keyple Core** | | | | +| [keyple-common-java-api] | `2.0.2` | | | +| [keyple-plugin-storagecard-java-api] | `1.0.0` | | | +| [keyple-service-java-lib] | `3.3.6` | 🟢 | `3.3.5` | +| [keyple-service-resource-java-lib] | `3.1.0` | | | +| [keyple-util-java-lib] | `2.4.0` | | | +| | | | | +| **Keyple Distributed** | | | | +| [keyple-distributed-local-java-lib] | `2.5.2` | | | +| [keyple-distributed-network-java-lib] | `2.5.1` | | | +| [keyple-distributed-remote-java-lib] | `2.5.1` | | | +| | | | | +| **Keyple Interop** | | | | +| [keyple-interop-jsonapi-client-kmp-lib] | `0.1.6` | | | +| [keyple-interop-localreader-nfcmobile-kmp-lib] | `0.1.6` | | | +| | | | | +| **Keyple Card Extensions** | | | | +| [keyple-card-calypso-java-lib] | `3.1.9` | | | +| [keyple-card-calypso-crypto-legacysam-java-lib] | `0.9.0` | | | +| [keyple-card-calypso-crypto-pki-java-lib] | `0.2.3` | | | +| [keyple-card-generic-java-lib] | `3.1.2` | | | +| | | | | +| **Keyple Reader Plugins** | | | | +| [keyple-plugin-android-nfc-java-lib] | `3.1.0` | | | +| [keyple-plugin-android-omapi-java-lib] | `2.1.0` | | | +| [keyple-plugin-cardresource-java-lib] | `2.0.1` | | | +| [keyple-plugin-pcsc-java-lib] | `2.5.3` | 🟢 | `2.5.2` | +| [keyple-plugin-stub-java-lib] | `2.2.1` | | | + ## [2025.09.12] First publication of the **Keyple Java BOM**. This release defines a consistent set of versions for **Keyple** and **Keypop** artifacts. -### Keypop Dependencies -- `org.eclipse.keypop:keypop-reader-java-api:2.0.1` -- `org.eclipse.keypop:keypop-calypso-card-java-api:2.1.2` -- `org.eclipse.keypop:keypop-calypso-crypto-legacysam-java-api:0.7.0` -- `org.eclipse.keypop:keypop-storagecard-java-api:0.3.0` -### Keyple Core -- `org.eclipse.keyple:keyple-common-java-api:2.0.2` -- `org.eclipse.keyple:keyple-plugin-storagecard-java-api:1.0.0` -- `org.eclipse.keyple:keyple-service-java-lib:3.3.5` -- `org.eclipse.keyple:keyple-service-resource-java-lib:3.1.0` -- `org.eclipse.keyple:keyple-util-java-lib:2.4.0` -### Keyple Distributed -- `org.eclipse.keyple:keyple-distributed-local-java-lib:2.5.2` -- `org.eclipse.keyple:keyple-distributed-network-java-lib:2.5.1` -- `org.eclipse.keyple:keyple-distributed-remote-java-lib:2.5.1` -### Keyple Interop -- `org.eclipse.keyple:keyple-interop-jsonapi-client-kmp-lib:0.1.6` (+ JVM, Android, iOS variants) -- `org.eclipse.keyple:keyple-interop-localreader-nfcmobile-kmp-lib:0.1.6` (+ JVM, Android, iOS variants) -### Keyple Card Extensions -- `org.eclipse.keyple:keyple-card-calypso-java-lib:3.1.9` -- `org.eclipse.keyple:keyple-card-calypso-crypto-legacysam-java-lib:0.9.0` -- `org.eclipse.keyple:keyple-card-calypso-crypto-pki-java-lib:0.2.3` -- `org.eclipse.keyple:keyple-card-generic-java-lib:3.1.2` -### Keyple Reader Plugins -- `org.eclipse.keyple:keyple-plugin-android-nfc-java-lib:3.1.0` -- `org.eclipse.keyple:keyple-plugin-android-omapi-java-lib:2.1.0` -- `org.eclipse.keyple:keyple-plugin-cardresource-java-lib:2.0.1` -- `org.eclipse.keyple:keyple-plugin-pcsc-java-lib:2.5.2` -- `org.eclipse.keyple:keyple-plugin-stub-java-lib:2.2.1` - -[Unreleased]: https://github.com/eclipse-keyple/keyple-java-bom/compare/2025.09.12...HEAD + +| Component | Version | Status | +|:------------------------------------------------|:-------:|:------:| +| **Keypop Dependencies** | | | +| [keypop-reader-java-api] | `2.0.1` | 🆕 | +| [keypop-calypso-card-java-api] | `2.1.2` | 🆕 | +| [keypop-calypso-crypto-legacysam-java-api] | `0.7.0` | 🆕 | +| [keypop-storagecard-java-api] | `0.3.0` | 🆕 | +| | | | +| **Keyple Core** | | | +| [keyple-common-java-api] | `2.0.2` | 🆕 | +| [keyple-plugin-storagecard-java-api] | `1.0.0` | 🆕 | +| [keyple-service-java-lib] | `3.3.5` | 🆕 | +| [keyple-service-resource-java-lib] | `3.1.0` | 🆕 | +| [keyple-util-java-lib] | `2.4.0` | 🆕 | +| | | | +| **Keyple Distributed** | | | +| [keyple-distributed-local-java-lib] | `2.5.2` | 🆕 | +| [keyple-distributed-network-java-lib] | `2.5.1` | 🆕 | +| [keyple-distributed-remote-java-lib] | `2.5.1` | 🆕 | +| | | | +| **Keyple Interop** | | | +| [keyple-interop-jsonapi-client-kmp-lib] | `0.1.6` | 🆕 | +| [keyple-interop-localreader-nfcmobile-kmp-lib] | `0.1.6` | 🆕 | +| | | | +| **Keyple Card Extensions** | | | +| [keyple-card-calypso-java-lib] | `3.1.9` | 🆕 | +| [keyple-card-calypso-crypto-legacysam-java-lib] | `0.9.0` | 🆕 | +| [keyple-card-calypso-crypto-pki-java-lib] | `0.2.3` | 🆕 | +| [keyple-card-generic-java-lib] | `3.1.2` | 🆕 | +| | | | +| **Keyple Reader Plugins** | | | +| [keyple-plugin-android-nfc-java-lib] | `3.1.0` | 🆕 | +| [keyple-plugin-android-omapi-java-lib] | `2.1.0` | 🆕 | +| [keyple-plugin-cardresource-java-lib] | `2.0.1` | 🆕 | +| [keyple-plugin-pcsc-java-lib] | `2.5.2` | 🆕 | +| [keyple-plugin-stub-java-lib] | `2.2.1` | 🆕 | + +[Unreleased]: https://github.com/eclipse-keyple/keyple-java-bom/compare/2025.10.23...HEAD +[2025.10.23]: https://github.com/eclipse-keyple/keyple-java-bom/compare/2025.09.12...2025.10.23 [2025.09.12]: https://github.com/eclipse-keyple/keyple-java-bom/releases/tag/2025.09.12 + +[keypop-reader-java-api]: https://github.com/eclipse-keypop/keypop-reader-java-api/releases +[keypop-calypso-card-java-api]: https://github.com/eclipse-keypop/keypop-calypso-card-java-api/releases +[keypop-calypso-crypto-legacysam-java-api]: https://github.com/eclipse-keypop/keypop-calypso-crypto-legacysam-java-api/releases +[keypop-storagecard-java-api]: https://github.com/eclipse-keypop/keypop-storagecard-java-api/releases + +[keyple-common-java-api]: https://github.com/eclipse-keyple/keyple-common-java-api/releases +[keyple-plugin-storagecard-java-api]: https://github.com/eclipse-keyple/keyple-plugin-storagecard-java-api/releases +[keyple-service-java-lib]: https://github.com/eclipse-keyple/keyple-service-java-lib/releases +[keyple-service-resource-java-lib]: https://github.com/eclipse-keyple/keyple-service-resource-java-lib/releases +[keyple-util-java-lib]: https://github.com/eclipse-keyple/keyple-util-java-lib/releases + +[keyple-distributed-local-java-lib]: https://github.com/eclipse-keyple/keyple-distributed-local-java-lib/releases +[keyple-distributed-network-java-lib]: https://github.com/eclipse-keyple/keyple-distributed-network-java-lib/releases +[keyple-distributed-remote-java-lib]: https://github.com/eclipse-keyple/keyple-distributed-remote-java-lib/releases + +[keyple-interop-jsonapi-client-kmp-lib]: https://github.com/eclipse-keyple/keyple-interop-jsonapi-client-kmp-lib/releases +[keyple-interop-localreader-nfcmobile-kmp-lib]: https://github.com/eclipse-keyple/keyple-interop-localreader-nfcmobile-kmp-lib/releases + +[keyple-card-calypso-java-lib]: https://github.com/eclipse-keyple/keyple-card-calypso-java-lib/releases +[keyple-card-calypso-crypto-legacysam-java-lib]: https://github.com/eclipse-keyple/keyple-card-calypso-crypto-legacysam-java-lib/releases +[keyple-card-calypso-crypto-pki-java-lib]: https://github.com/eclipse-keyple/keyple-card-calypso-crypto-pki-java-lib/releases +[keyple-card-generic-java-lib]: https://github.com/eclipse-keyple/keyple-card-generic-java-lib/releases + +[keyple-plugin-android-nfc-java-lib]: https://github.com/eclipse-keyple/keyple-plugin-android-nfc-java-lib/releases +[keyple-plugin-android-omapi-java-lib]: https://github.com/eclipse-keyple/keyple-plugin-android-omapi-java-lib/releases +[keyple-plugin-cardresource-java-lib]: https://github.com/eclipse-keyple/keyple-plugin-cardresource-java-lib/releases +[keyple-plugin-pcsc-java-lib]: https://github.com/eclipse-keyple/keyple-plugin-pcsc-java-lib/releases +[keyple-plugin-stub-java-lib]: https://github.com/eclipse-keyple/keyple-plugin-stub-java-lib/releases diff --git a/build.gradle.kts b/build.gradle.kts index 42d4fa9..141e615 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -32,7 +32,7 @@ dependencies { // Keyple core api("org.eclipse.keyple:keyple-common-java-api:2.0.2") api("org.eclipse.keyple:keyple-plugin-storagecard-java-api:1.0.0") - api("org.eclipse.keyple:keyple-service-java-lib:3.3.5") + api("org.eclipse.keyple:keyple-service-java-lib:3.3.6") api("org.eclipse.keyple:keyple-service-resource-java-lib:3.1.0") api("org.eclipse.keyple:keyple-util-java-lib:2.4.0") // Keyple distributed @@ -61,7 +61,7 @@ dependencies { api("org.eclipse.keyple:keyple-plugin-android-nfc-java-lib:3.1.0") api("org.eclipse.keyple:keyple-plugin-android-omapi-java-lib:2.1.0") api("org.eclipse.keyple:keyple-plugin-cardresource-java-lib:2.0.1") - api("org.eclipse.keyple:keyple-plugin-pcsc-java-lib:2.5.2") + api("org.eclipse.keyple:keyple-plugin-pcsc-java-lib:2.5.3") api("org.eclipse.keyple:keyple-plugin-stub-java-lib:2.2.1") } } From a6ffb565f18b6cf4a1d0be671992e75a9ec88cdf Mon Sep 17 00:00:00 2001 From: Jean-Pierre Fortune Date: Thu, 23 Oct 2025 23:05:05 +0200 Subject: [PATCH 2/3] feat: add scripts for automatic changelog updates --- tools/README.md | 169 +++++++++++ tools/update_changelog.bat | 15 + tools/update_changelog.py | 587 +++++++++++++++++++++++++++++++++++++ tools/update_changelog.sh | 12 + 4 files changed, 783 insertions(+) create mode 100644 tools/README.md create mode 100644 tools/update_changelog.bat create mode 100644 tools/update_changelog.py create mode 100644 tools/update_changelog.sh diff --git a/tools/README.md b/tools/README.md new file mode 100644 index 0000000..effdb37 --- /dev/null +++ b/tools/README.md @@ -0,0 +1,169 @@ +# Automatic CHANGELOG Update Script + +## Description + +`update_changelog.py` is a Python script that **automates the update** of the `CHANGELOG.md` file based on version changes detected in `build.gradle.kts`. + +## Features + +- **Automatic change detection** between `build.gradle.kts` and the latest version in `CHANGELOG.md`: + - 🆕 **New dependencies**: automatically detected + - ❌ **Removed dependencies**: displayed in their original category with their last version + - 🟢 **Patch**: x.y.z → x.y.(z+n) + - 🔵 **Minor**: x.y.z → x.(y+n).z + - 🔴 **Major**: x.y.z → (x+n).y.z +- Grouping of KMP libraries (a single line for the base library) +- Preservation of the dependencies' order as defined in `build.gradle.kts` +- **Automatic update of reference links**: + - Version links `[Unreleased]` and `[version]` + - Automatic addition of links for new dependencies (from organization eclipse-keypop or eclipse-keyple) + - Automatic removal of links for dependencies that have been dropped +- Compliance with the "Keep a Changelog" format + +----- + +## Prerequisites + +- Python 3.7 or higher +- The `build.gradle.kts` and `CHANGELOG.md` files must be located at the project root + +----- + +## Usage + +**Important**: The script must be executed from the project root directory. + +### Basic Usage (Current Date) + +```bash +# On Windows +.\tools\update_changelog.bat + +# On Linux/Mac +./tools/update_changelog.sh + +# With Python directly (all systems) +python tools/update_changelog.py +``` + +This command uses the current date in the format `YYYY.MM.DD` as the version number. + +### Specifying a Custom Date + +```bash +# On Windows +.\tools\update_changelog.bat 2025.10.30 + +# On Linux/Mac +./tools/update_changelog.sh 2025.10.30 + +# With Python directly (all systems) +python tools/update_changelog.py 2025.10.30 +``` + +The date format must be `YYYY.MM.DD`. + +----- + +## Behavior + +1. **Dependency Parsing**: The script analyzes `build.gradle.kts` and extracts all dependencies while preserving their order +2. **Comparison**: It compares these versions with those in the latest section of `CHANGELOG.md` +3. **Change Detection**: + - If no changes are detected: displays a message and makes no modification + - If changes are detected: creates a new section with the appropriate statuses +4. **Update**: Adds the new section after `[Unreleased]` and updates the reference links + +----- + +## Examples + +### Example 1: No Changes + +```bash +$ python tools/update_changelog.py +Updating CHANGELOG.md for version 2025.10.23... +Found 23 dependencies in build.gradle.kts +Latest CHANGELOG version: 2025.10.23 +No changes detected between build.gradle.kts and the latest CHANGELOG version. +``` + +### Example 2: With Changes + +```bash +$ python tools/update_changelog.py 2025.10.30 +Updating CHANGELOG.md for version 2025.10.30... +Found 23 dependencies in build.gradle.kts +Latest CHANGELOG version: 2025.10.23 +CHANGELOG.md updated successfully with version 2025.10.30 +``` + +The `CHANGELOG.md` file will be updated with: + +- A new section `## [2025.10.30]` +- The statuses 🟢/🔵/🔴 for each modified component +- The "Prev. Version" column showing the old version +- Updated links: + ``` + [Unreleased]: https://github.com/eclipse-keyple/keyple-java-bom/compare/2025.10.30...HEAD + [2025.10.30]: https://github.com/eclipse-keyple/keyple-java-bom/compare/2025.10.23...2025.10.30 + ``` + +----- + +## Generated Structure + +The new section follows this format: + +```markdown +## [2025.10.30] + +| Component | Version | Status | Prev.
Version | +|:------------------------------------------------|:-------:|:------:|:----------------:| +| **Keypop Dependencies** | | | | +| [keypop-reader-java-api] | `2.0.1` | | | +| ... | | | | +| | | | | +| **Keyple Core** | | | | +| [keyple-common-java-api] | `2.0.2` | | | +| [keyple-service-java-lib] | `3.4.0` | 🔵 | `3.3.6` | +| [keyple-service-resource-java-lib] | `4.0.0` | 🔴 | `3.1.0` | +| [keyple-test-java-lib] | `1.0.0` | 🆕 | | +| [keyple-util-java-lib] | `2.4.0` | ❌ | | +| ... | | | | +``` + +In this example: + +- `keyple-service-java-lib`: Minor change (3.3.6 → 3.4.0) marked 🔵 +- `keyple-service-resource-java-lib`: Major change (3.1.0 → 4.0.0) marked 🔴 +- `keyple-test-java-lib`: New dependency marked 🆕 +- `keyple-util-java-lib`: Removed dependency marked ❌ + +### Reference Links Update + +The script also automatically updates the links at the bottom of the file: + +```markdown +[Unreleased]: https://github.com/eclipse-keyple/keyple-java-bom/compare/2025.10.30...HEAD +[2025.10.30]: https://github.com/eclipse-keyple/keyple-java-bom/compare/2025.10.23...2025.10.30 + +[keypop-reader-java-api]: https://github.com/eclipse-keypop/keypop-reader-java-api/releases +... +[keyple-test-java-lib]: https://github.com/eclipse-keyple/keyple-test-java-lib/releases +... +``` + +- **New dependencies**: the link is automatically added in the appropriate section (eclipse-keypop or eclipse-keyple) +- **Removed dependencies**: the link is automatically removed + +----- + +## Notes + +- The script respects the exact order of dependencies as defined in `build.gradle.kts` +- KMP libraries with variants (-jvm, -android, -iosarm64, etc.) are grouped into a single line +- Only components whose version has changed will have a status and a value in "Prev. Version" +- New dependencies (🆕) and version changes (🟢🔵🔴) appear in the order of the `build.gradle.kts` file +- Removed dependencies (❌) appear after the current dependencies of their original category +- If an entire category is removed, it appears at the end of the table with its dependencies marked ❌ \ No newline at end of file diff --git a/tools/update_changelog.bat b/tools/update_changelog.bat new file mode 100644 index 0000000..56de830 --- /dev/null +++ b/tools/update_changelog.bat @@ -0,0 +1,15 @@ +@echo off +REM Script to update CHANGELOG.md +REM Usage: update_changelog.bat [YYYY.MM.DD] + +REM Save current directory +set CURRENT_DIR=%CD% + +REM Move to project root directory (parent of tools folder) +cd /d "%~dp0.." + +REM Execute Python script from tools folder +python "%~dp0update_changelog.py" %* + +REM Restore current directory +cd /d "%CURRENT_DIR%" diff --git a/tools/update_changelog.py b/tools/update_changelog.py new file mode 100644 index 0000000..66a696f --- /dev/null +++ b/tools/update_changelog.py @@ -0,0 +1,587 @@ +#!/usr/bin/env python3 +""" +Script to automatically update CHANGELOG.md from build.gradle.kts changes. + +Usage: + python update_changelog.py [YYYY.MM.DD] + +If no date is provided, uses today's date. +""" + +import re +import sys +from datetime import date +from typing import Dict, List, Tuple, Optional +from dataclasses import dataclass + + +@dataclass +class Dependency: + """Represents a dependency with its metadata.""" + artifact_id: str + version: str + category: str + group_id: str + + +@dataclass +class ChangelogEntry: + """Represents an entry in the changelog.""" + name: str + version: str + status: str + prev_version: str + category: str = "" + + +class BuildGradleParser: + """Parser for build.gradle.kts file.""" + + CATEGORY_MAPPING = { + "Keypop": "**Keypop Dependencies**", + "Keyple core": "**Keyple Core**", + "Keyple distributed": "**Keyple Distributed**", + "Keyple interop": "**Keyple Interop**", + "Keyple card extensions": "**Keyple Card Extensions**", + "Keyple reader plugins": "**Keyple Reader Plugins**", + } + + # Patterns for KMP library variants to exclude from individual listing + KMP_VARIANT_SUFFIXES = [ + "-jvm", "-android", "-iosarm64", "-iossimulatorarm64", "-iosx64" + ] + + def __init__(self, filepath: str): + self.filepath = filepath + self.dependencies: List[Dependency] = [] + + def parse(self) -> List[Dependency]: + """Parse the build.gradle.kts file and extract dependencies.""" + with open(self.filepath, 'r', encoding='utf-8') as f: + content = f.read() + + current_category = None + dependencies_by_base = {} # Track base libraries for KMP grouping + + # Extract the dependencies block + constraints_match = re.search(r'constraints\s*\{(.*?)\}', content, re.DOTALL) + if not constraints_match: + return [] + + constraints_block = constraints_match.group(1) + lines = constraints_block.split('\n') + + for line in lines: + line = line.strip() + + # Check for category comment + if line.startswith('//'): + category_name = line.lstrip('/').strip() + current_category = self.CATEGORY_MAPPING.get(category_name, category_name) + continue + + # Parse api() lines + api_match = re.match(r'api\("([^:]+):([^:]+):([^"]+)"\)', line) + if api_match and current_category: + group_id = api_match.group(1) + artifact_id = api_match.group(2) + version = api_match.group(3) + + # Check if this is a KMP variant + base_artifact = artifact_id + is_variant = False + for suffix in self.KMP_VARIANT_SUFFIXES: + if artifact_id.endswith(suffix): + base_artifact = artifact_id[:artifact_id.rfind(suffix)] + is_variant = True + break + + # Only add the base library, not variants + if not is_variant: + dep = Dependency( + artifact_id=artifact_id, + version=version, + category=current_category, + group_id=group_id + ) + dependencies_by_base[artifact_id] = dep + elif base_artifact not in dependencies_by_base: + # If we encounter a variant before the base, add the base + dep = Dependency( + artifact_id=base_artifact, + version=version, + category=current_category, + group_id=group_id + ) + dependencies_by_base[base_artifact] = dep + + # Preserve order by re-parsing and keeping only base libraries + result = [] + current_category = None + + for line in lines: + line = line.strip() + + if line.startswith('//'): + category_name = line.lstrip('/').strip() + current_category = self.CATEGORY_MAPPING.get(category_name, category_name) + continue + + api_match = re.match(r'api\("([^:]+):([^:]+):([^"]+)"\)', line) + if api_match and current_category: + artifact_id = api_match.group(2) + + # Check if this is a base library (not a variant) + is_base = True + for suffix in self.KMP_VARIANT_SUFFIXES: + if artifact_id.endswith(suffix): + is_base = False + break + + if is_base and artifact_id in dependencies_by_base: + result.append(dependencies_by_base[artifact_id]) + + return result + + +class ChangelogParser: + """Parser for CHANGELOG.md file.""" + + def __init__(self, filepath: str): + self.filepath = filepath + + def parse_latest_version(self) -> Tuple[Optional[str], Dict[str, ChangelogEntry]]: + """Parse the latest version section and return version number and entries.""" + with open(self.filepath, 'r', encoding='utf-8') as f: + content = f.read() + + # Find the [Unreleased] section first + unreleased_match = re.search(r'\n## \[Unreleased\]', content) + if not unreleased_match: + return None, {} + + # Find the first version section after [Unreleased] + search_start = unreleased_match.end() + version_match = re.search(r'\n## \[(\d{4}\.\d{2}\.\d{2})\]', content[search_start:]) + if not version_match: + return None, {} + + version = version_match.group(1) + + # Extract the section content until the next ## or end of file + section_start = search_start + version_match.end() + next_section = re.search(r'\n## \[', content[section_start:]) + if next_section: + section_end = section_start + next_section.start() + else: + # Look for the reference links section + refs_match = re.search(r'\n\[Unreleased\]:', content[section_start:]) + if refs_match: + section_end = section_start + refs_match.start() + else: + section_end = len(content) + + section_content = content[section_start:section_end] + + # Parse the table + entries = {} + lines = section_content.split('\n') + in_table = False + current_category = "" + + for line in lines: + if line.startswith('|') and '---' in line: + in_table = True + continue + + if in_table and line.startswith('|'): + # Parse table row + cells = [cell.strip() for cell in line.split('|')[1:-1]] + if len(cells) >= 2: + name = cells[0].strip() + + # Check if this is a category header + if '**' in name: + current_category = name + continue + + # Skip empty rows + if not name or not cells[1].strip(): + continue + + dep_version = cells[1].strip('`').strip() + status = cells[2].strip() if len(cells) > 2 else '' + prev_version = cells[3].strip('`').strip() if len(cells) > 3 else '' + + # Extract artifact name from markdown link if present + link_match = re.match(r'\[([^\]]+)\]', name) + if link_match: + name = link_match.group(1) + + if name and dep_version: + entries[name] = ChangelogEntry(name, dep_version, status, prev_version, current_category) + + return version, entries + + +class VersionComparator: + """Compare versions and determine change type.""" + + @staticmethod + def parse_version(version: str) -> Tuple[int, ...]: + """Parse a version string into a tuple of integers.""" + try: + return tuple(int(x) for x in version.split('.')) + except (ValueError, AttributeError): + return (0,) + + @staticmethod + def get_status(old_version: str, new_version: str) -> str: + """Determine status emoji based on semantic versioning.""" + old = VersionComparator.parse_version(old_version) + new = VersionComparator.parse_version(new_version) + + # Pad to same length + max_len = max(len(old), len(new)) + old = old + (0,) * (max_len - len(old)) + new = new + (0,) * (max_len - len(new)) + + if new == old: + return "" + + # Check major version (first component) + if new[0] > old[0]: + return "🔴" + + # Check minor version (second component) + if len(new) > 1 and len(old) > 1 and new[1] > old[1]: + return "🔵" + + # Otherwise it's a patch + return "🟢" + + +class ChangelogGenerator: + """Generate new changelog entries.""" + + def __init__(self, filepath: str): + self.filepath = filepath + + def generate_new_section( + self, + new_version: str, + dependencies: List[Dependency], + old_entries: Dict[str, ChangelogEntry] + ) -> Tuple[str, bool]: + """Generate a new version section for the changelog.""" + lines = [ + f"## [{new_version}]", + "", + "| Component | Version | Status | Prev.
Version |", + "|:------------------------------------------------|:-------:|:------:|:----------------:|" + ] + + has_changes = False + current_deps_names = set() + + # Build a dict of current dependencies by category for easy lookup + deps_by_category = {} + for dep in dependencies: + if dep.category not in deps_by_category: + deps_by_category[dep.category] = [] + deps_by_category[dep.category].append(dep) + current_deps_names.add(dep.artifact_id) + + # Find removed dependencies organized by category + removed_by_category = {} + for name, old_entry in old_entries.items(): + if name not in current_deps_names: + has_changes = True + if old_entry.category not in removed_by_category: + removed_by_category[old_entry.category] = [] + removed_by_category[old_entry.category].append(old_entry) + + # Process each category in order (from dependencies list) + current_category = None + for dep in dependencies: + # Add category header if changed + if dep.category != current_category: + if current_category is not None: + # Add removed dependencies from previous category + if current_category in removed_by_category: + for removed in removed_by_category[current_category]: + name = f"[{removed.name}]" + version = f"`{removed.version}`" + status = "❌" + prev_version = "" + line = f"| {name:<47} | {version:^7} | {status:^6} | {prev_version:^16} |" + lines.append(line) + lines.append("| | | | |") + + current_category = dep.category + lines.append(f"| {current_category:<47} | | | |") + + # Determine if there's a change + old_entry = old_entries.get(dep.artifact_id) + status = "" + prev_version = "" + + if old_entry: + if old_entry.version != dep.version: + status = VersionComparator.get_status(old_entry.version, dep.version) + prev_version = f"`{old_entry.version}`" + has_changes = True + else: + # New dependency + status = "🆕" + has_changes = True + + # Format the line + name = f"[{dep.artifact_id}]" + version = f"`{dep.version}`" + + # Build the line with proper alignment + line = f"| {name:<47} | {version:^7} | {status:^6} | {prev_version:^16} |" + lines.append(line) + + # Add removed dependencies from last category + if current_category and current_category in removed_by_category: + for removed in removed_by_category[current_category]: + name = f"[{removed.name}]" + version = f"`{removed.version}`" + status = "❌" + prev_version = "" + line = f"| {name:<47} | {version:^7} | {status:^6} | {prev_version:^16} |" + lines.append(line) + + # Handle removed dependencies from categories that no longer exist + for category, removed_list in removed_by_category.items(): + if category not in deps_by_category: + lines.append("| | | | |") + lines.append(f"| {category:<47} | | | |") + for removed in removed_list: + name = f"[{removed.name}]" + version = f"`{removed.version}`" + status = "❌" + prev_version = "" + line = f"| {name:<47} | {version:^7} | {status:^6} | {prev_version:^16} |" + lines.append(line) + + return "\n".join(lines), has_changes + + def update_changelog( + self, + new_version: str, + new_section: str, + has_changes: bool, + dependencies: List[Dependency], + old_entries: Dict[str, ChangelogEntry] + ) -> bool: + """Update the CHANGELOG.md file with the new section.""" + if not has_changes: + print("No changes detected between build.gradle.kts and the latest CHANGELOG version.") + return False + + with open(self.filepath, 'r', encoding='utf-8') as f: + content = f.read() + + # Find the [Unreleased] section + unreleased_match = re.search(r'(## \[Unreleased\]\n)', content) + if not unreleased_match: + print("Error: Could not find [Unreleased] section in CHANGELOG.md") + return False + + # Insert new section after [Unreleased] + insert_pos = unreleased_match.end() + new_content = ( + content[:insert_pos] + + "\n" + new_section + "\n" + + content[insert_pos:] + ) + + # Update reference links + new_content = self.update_reference_links(new_content, new_version, dependencies, old_entries) + + with open(self.filepath, 'w', encoding='utf-8') as f: + f.write(new_content) + + print(f"CHANGELOG.md updated successfully with version {new_version}") + return True + + def update_reference_links( + self, + content: str, + new_version: str, + dependencies: List[Dependency], + old_entries: Dict[str, ChangelogEntry] + ) -> str: + """Update the [Unreleased] and version reference links, and add/remove dependency links.""" + # Find the reference links section + unreleased_link_match = re.search( + r'\[Unreleased\]: https://github\.com/([^/]+)/([^/]+)/compare/([\d.]+)\.\.\.HEAD', + content + ) + + if not unreleased_link_match: + return content + + org = unreleased_link_match.group(1) + repo = unreleased_link_match.group(2) + old_version = unreleased_link_match.group(3) + + # Update [Unreleased] link to point from new version to HEAD + new_unreleased_link = f"[Unreleased]: https://github.com/{org}/{repo}/compare/{new_version}...HEAD" + content = re.sub( + r'\[Unreleased\]: https://github\.com/[^/]+/[^/]+/compare/[\d.]+\.\.\.HEAD', + new_unreleased_link, + content + ) + + # Add new version link after [Unreleased] link + new_version_link = f"[{new_version}]: https://github.com/{org}/{repo}/compare/{old_version}...{new_version}" + content = re.sub( + r'(\[Unreleased\]: [^\n]+\n)', + f'\\1{new_version_link}\n', + content + ) + + # Find added and removed dependencies + current_deps = {dep.artifact_id for dep in dependencies} + old_deps = set(old_entries.keys()) + + added_deps = current_deps - old_deps + removed_deps = old_deps - current_deps + + # First, remove links for removed dependencies + for dep_name in removed_deps: + content = re.sub( + rf'\n\[{re.escape(dep_name)}\]: https://github\.com/[^\n]+\n', + '\n', + content + ) + + # Extract all existing artifact links (keypop and keyple) into a dict + existing_links = {} + for match in re.finditer(r'\n\[((keypop|keyple)-[^\]]+)\]: ([^\n]+)\n', content): + artifact_id = match.group(1) + full_line = match.group(0) + existing_links[artifact_id] = full_line + + # Build set of all dependencies we're managing + managed_deps = set(dep.artifact_id for dep in dependencies) + + # Build the new ordered list of links based on dependencies order + # Group by category to add blank lines between categories + new_links_section = [] + processed_deps = set() + previous_category = None + + for dep in dependencies: + processed_deps.add(dep.artifact_id) + + # Add blank line between categories + if previous_category and dep.category != previous_category: + new_links_section.append('') + + if dep.artifact_id in existing_links: + # Use existing link (strip the leading \n if present) + link = existing_links[dep.artifact_id].lstrip('\n') + new_links_section.append(link.rstrip('\n')) + else: + # Create new link (either because it's a new dependency or the link is missing) + if dep.artifact_id.startswith('keypop-'): + org = 'eclipse-keypop' + elif dep.artifact_id.startswith('keyple-'): + org = 'eclipse-keyple' + else: + continue + new_link = f"[{dep.artifact_id}]: https://github.com/{org}/{dep.artifact_id}/releases" + new_links_section.append(new_link) + + previous_category = dep.category + + # Add any existing links that are not in current dependencies and not removed + # (these might be from older versions still referenced in the changelog) + orphan_links = [] + for artifact_id, link_line in existing_links.items(): + if artifact_id not in processed_deps and artifact_id not in removed_deps: + orphan_links.append(link_line.lstrip('\n').rstrip('\n')) + + if orphan_links: + new_links_section.append('') # Blank line before orphan links + new_links_section.extend(orphan_links) + + # Find the artifact links section boundaries + first_artifact_link = re.search(r'\n\[(keypop|keyple)-[^\]]+\]: ', content) + if first_artifact_link: + # Find where artifact links end (before the next section or end of file) + links_start = first_artifact_link.start() + + # Find the end of all artifact links + last_artifact_link = None + for match in re.finditer(r'\n\[(keypop|keyple)-[^\]]+\]: [^\n]+\n', content): + last_artifact_link = match + + if last_artifact_link: + links_end = last_artifact_link.end() + + # Replace the entire artifact links section with the new ordered one + # Join with newlines and add a trailing newline + new_links_text = '\n' + '\n'.join(new_links_section) + '\n' + content = ( + content[:links_start] + + new_links_text + + content[links_end:] + ) + + return content + + +def main(): + """Main entry point.""" + # Determine version + if len(sys.argv) > 1: + new_version = sys.argv[1] + # Validate format + if not re.match(r'^\d{4}\.\d{2}\.\d{2}$', new_version): + print(f"Error: Invalid version format '{new_version}'. Expected YYYY.MM.DD") + sys.exit(1) + else: + today = date.today() + new_version = today.strftime("%Y.%m.%d") + + print(f"Updating CHANGELOG.md for version {new_version}...") + + # Parse build.gradle.kts + gradle_parser = BuildGradleParser("build.gradle.kts") + dependencies = gradle_parser.parse() + print(f"Found {len(dependencies)} dependencies in build.gradle.kts") + + # Parse CHANGELOG.md + changelog_parser = ChangelogParser("CHANGELOG.md") + old_version, old_entries = changelog_parser.parse_latest_version() + if old_version: + print(f"Latest CHANGELOG version: {old_version}") + else: + print("No previous version found in CHANGELOG.md") + old_entries = {} + + # Check if the target version already exists + if old_version == new_version: + print(f"Version {new_version} already exists in CHANGELOG.md. No changes needed.") + sys.exit(0) + + # Generate new section + generator = ChangelogGenerator("CHANGELOG.md") + new_section, has_changes = generator.generate_new_section( + new_version, dependencies, old_entries + ) + + # Update changelog + success = generator.update_changelog(new_version, new_section, has_changes, dependencies, old_entries) + + sys.exit(0 if success else 1) + + +if __name__ == "__main__": + main() diff --git a/tools/update_changelog.sh b/tools/update_changelog.sh new file mode 100644 index 0000000..3855b32 --- /dev/null +++ b/tools/update_changelog.sh @@ -0,0 +1,12 @@ +#!/bin/bash +# Script to update CHANGELOG.md +# Usage: update_changelog.sh [YYYY.MM.DD] + +# Get the directory where the script is located +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Move to project root directory (parent of tools folder) +cd "$SCRIPT_DIR/.." + +# Execute Python script +python "$SCRIPT_DIR/update_changelog.py" "$@" From 95db3e13b74854f69cbd171e6870db0221eb29ad Mon Sep 17 00:00:00 2001 From: Andrei Cristea Date: Fri, 24 Oct 2025 09:38:15 +0200 Subject: [PATCH 3/3] wip --- CHANGELOG.md | 6 +++--- tools/README.md | 26 +++++++++++++------------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e1645e..8df42ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ Legend: 🆕 New • ❌ Removed • 🟢 Patch • 🔵 Minor • 🔴 Major ## [Unreleased] -## [2025.10.23] +## [2025.10.24] | Component | Version | Status | Prev.
Version | |:------------------------------------------------|:-------:|:------:|:----------------:| @@ -88,8 +88,8 @@ This release defines a consistent set of versions for **Keyple** and **Keypop** | [keyple-plugin-pcsc-java-lib] | `2.5.2` | 🆕 | | [keyple-plugin-stub-java-lib] | `2.2.1` | 🆕 | -[Unreleased]: https://github.com/eclipse-keyple/keyple-java-bom/compare/2025.10.23...HEAD -[2025.10.23]: https://github.com/eclipse-keyple/keyple-java-bom/compare/2025.09.12...2025.10.23 +[Unreleased]: https://github.com/eclipse-keyple/keyple-java-bom/compare/2025.10.24...HEAD +[2025.10.24]: https://github.com/eclipse-keyple/keyple-java-bom/compare/2025.09.12...2025.10.24 [2025.09.12]: https://github.com/eclipse-keyple/keyple-java-bom/releases/tag/2025.09.12 [keypop-reader-java-api]: https://github.com/eclipse-keypop/keypop-reader-java-api/releases diff --git a/tools/README.md b/tools/README.md index effdb37..7a8eeec 100644 --- a/tools/README.md +++ b/tools/README.md @@ -118,19 +118,19 @@ The new section follows this format: ```markdown ## [2025.10.30] -| Component | Version | Status | Prev.
Version | -|:------------------------------------------------|:-------:|:------:|:----------------:| -| **Keypop Dependencies** | | | | -| [keypop-reader-java-api] | `2.0.1` | | | -| ... | | | | -| | | | | -| **Keyple Core** | | | | -| [keyple-common-java-api] | `2.0.2` | | | -| [keyple-service-java-lib] | `3.4.0` | 🔵 | `3.3.6` | -| [keyple-service-resource-java-lib] | `4.0.0` | 🔴 | `3.1.0` | -| [keyple-test-java-lib] | `1.0.0` | 🆕 | | -| [keyple-util-java-lib] | `2.4.0` | ❌ | | -| ... | | | | +| Component | Version | Status | Prev.
Version | +|:-----------------------------------|:-------:|:------:|:----------------:| +| **Keypop Dependencies** | | | | +| [keypop-reader-java-api] | `2.0.1` | | | +| ... | | | | +| | | | | +| **Keyple Core** | | | | +| [keyple-common-java-api] | `2.0.2` | | | +| [keyple-service-java-lib] | `3.4.0` | 🔵 | `3.3.6` | +| [keyple-service-resource-java-lib] | `4.0.0` | 🔴 | `3.1.0` | +| [keyple-test-java-lib] | `1.0.0` | 🆕 | | +| [keyple-util-java-lib] | `2.4.0` | ❌ | | +| ... | | | | ``` In this example: