From aca019f43d2488a6101dfd1681d3f14c6523c999 Mon Sep 17 00:00:00 2001 From: Zhongwei Li Date: Sat, 29 Nov 2025 18:18:47 +0800 Subject: [PATCH] Initial commit --- .claude-plugin/plugin.json | 12 ++ README.md | 3 + plugin.lock.json | 85 ++++++++ skills/SKILL.md | 294 ++++++++++++++++++++++++++ skills/assets/CHANGELOG.md.template | 16 ++ skills/assets/version.json.template | 6 + skills/references/project-types.md | 160 ++++++++++++++ skills/references/unity-guide.md | 140 ++++++++++++ skills/references/unreal-guide.md | 152 +++++++++++++ skills/scripts/changelog_generator.py | 205 ++++++++++++++++++ skills/scripts/detect_project.py | 281 ++++++++++++++++++++++++ skills/scripts/git_operations.py | 154 ++++++++++++++ skills/scripts/sync_unity_version.py | 87 ++++++++ skills/scripts/update_version.py | 231 ++++++++++++++++++++ 14 files changed, 1826 insertions(+) create mode 100644 .claude-plugin/plugin.json create mode 100644 README.md create mode 100644 plugin.lock.json create mode 100644 skills/SKILL.md create mode 100644 skills/assets/CHANGELOG.md.template create mode 100644 skills/assets/version.json.template create mode 100644 skills/references/project-types.md create mode 100644 skills/references/unity-guide.md create mode 100644 skills/references/unreal-guide.md create mode 100644 skills/scripts/changelog_generator.py create mode 100644 skills/scripts/detect_project.py create mode 100644 skills/scripts/git_operations.py create mode 100644 skills/scripts/sync_unity_version.py create mode 100644 skills/scripts/update_version.py diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..5ea7530 --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,12 @@ +{ + "name": "auto-release-manager", + "description": "Automate version updates and releases for any project type (Node.js, Python, Rust, Unity, Unreal, etc.) with intelligent project detection and cross-platform support.", + "version": "1.0.3", + "author": { + "name": "Dev GOM", + "url": "https://github.com/Dev-GOM/claude-code-marketplace" + }, + "skills": [ + "./skills" + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..85cc0f9 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# auto-release-manager + +Automate version updates and releases for any project type (Node.js, Python, Rust, Unity, Unreal, etc.) with intelligent project detection and cross-platform support. diff --git a/plugin.lock.json b/plugin.lock.json new file mode 100644 index 0000000..68d5671 --- /dev/null +++ b/plugin.lock.json @@ -0,0 +1,85 @@ +{ + "$schema": "internal://schemas/plugin.lock.v1.json", + "pluginId": "gh:Dev-GOM/claude-code-marketplace:plugins/auto-release-manager", + "normalized": { + "repo": null, + "ref": "refs/tags/v20251128.0", + "commit": "c7a9b94dd277fb271aedc0f56f97155072bf9419", + "treeHash": "73fb86e7061159773e5f4a0f1b0b600e79d7abbf71d4fc7ce8370cce46b3f608", + "generatedAt": "2025-11-28T10:10:17.061929Z", + "toolVersion": "publish_plugins.py@0.2.0" + }, + "origin": { + "remote": "git@github.com:zhongweili/42plugin-data.git", + "branch": "master", + "commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390", + "repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data" + }, + "manifest": { + "name": "auto-release-manager", + "description": "Automate version updates and releases for any project type (Node.js, Python, Rust, Unity, Unreal, etc.) with intelligent project detection and cross-platform support.", + "version": "1.0.3" + }, + "content": { + "files": [ + { + "path": "README.md", + "sha256": "a5ae49a6fe1b46791ade1d36417b65cd4137c43b06edd526bf49d99d88c5e39c" + }, + { + "path": ".claude-plugin/plugin.json", + "sha256": "377857f3b2365fc23cc778325e12a27d957aae117866fd66ade504159238ce74" + }, + { + "path": "skills/SKILL.md", + "sha256": "5869f71f5e65b30e9febe9ed3f3d19d107c31636b28923d45ed984c8a63c8bd6" + }, + { + "path": "skills/references/unreal-guide.md", + "sha256": "b6b1d1b8980bb4aebb45395094def41254c383b27385546edf39c7a8d34ff197" + }, + { + "path": "skills/references/project-types.md", + "sha256": "56641ef5cbd333d4579527386eac403d351c09572d548333f31c8a36922e7c8b" + }, + { + "path": "skills/references/unity-guide.md", + "sha256": "76d90bfb9602f49bcd44e4db17933344ffb80f5b49e4d2fb855ee6560852ffe5" + }, + { + "path": "skills/scripts/update_version.py", + "sha256": "74a387b76612c8f422b5897750e75d7a324eccff7f4e439eec0eafedf0daef43" + }, + { + "path": "skills/scripts/detect_project.py", + "sha256": "751907737844a1a7940f8b29c2744ab9517b73e7452f437d67fd0e1d3bbf8d06" + }, + { + "path": "skills/scripts/sync_unity_version.py", + "sha256": "c69cadbae7246ce82b054213c8aca2a912c2f76e73131ea1caa2e255c0491f13" + }, + { + "path": "skills/scripts/changelog_generator.py", + "sha256": "a446f1c9d2ec375347468b92a2e8ef2f7eb4da573d64ac389c81bffa256b53d4" + }, + { + "path": "skills/scripts/git_operations.py", + "sha256": "ee08ce54c2901a40a9e8c4cf543b739cff8138c061ef7ed332fe5a472dc339c1" + }, + { + "path": "skills/assets/CHANGELOG.md.template", + "sha256": "9badb0bc8ff7cfc0948778eada3bae7c9fd55b692763dd41564b2a8c4fceb4ea" + }, + { + "path": "skills/assets/version.json.template", + "sha256": "a959e111008e5a14623cce9f6e6f79915f27396b9a2096c649e87c678321fe9e" + } + ], + "dirSha256": "73fb86e7061159773e5f4a0f1b0b600e79d7abbf71d4fc7ce8370cce46b3f608" + }, + "security": { + "scannedAt": null, + "scannerVersion": null, + "flags": [] + } +} \ No newline at end of file diff --git a/skills/SKILL.md b/skills/SKILL.md new file mode 100644 index 0000000..2814b6b --- /dev/null +++ b/skills/SKILL.md @@ -0,0 +1,294 @@ +--- +name: auto-release-manager +description: Automate version updates and releases for any project type (Node.js, Python, Rust, Unity, Unreal, etc.). Detect project type, update version files, generate CHANGELOG, and handle git operations with cross-platform support. +allowed-tools: Bash, Read, Write +--- + +# Auto Release Manager + +Automate the entire release workflow for any project type with intelligent project detection and cross-platform support. + +## Purpose + +This skill streamlines version management across different project types by: +- Automatically detecting project type (Node.js, Python, Rust, Unity, Unreal, etc.) +- Updating version files in appropriate formats (JSON, TOML, YAML, Unity assets) +- Generating CHANGELOG from git commit history +- Handling git operations (commit, tag, push) with OS compatibility +- Supporting game engine specific workflows (Unity version.json ← → ProjectSettings.asset sync) + +## When to Use + +Use this skill when: +- Releasing a new version of any project +- User requests "update version", "create release", "bump version" +- Need to handle versions across multiple files (e.g., Unity's dual-file approach) +- Want automated CHANGELOG generation from commits +- Working with game engine projects (Unity, Unreal) + +Example user requests: +- "Bump patch version and create release" +- "Update to v2.1.0" +- "Create Unity release with version 1.5.0" +- "Generate CHANGELOG and commit" + +## Workflow + +### Step 1: Detect Project Type + +Start by running the project detection script to identify the project type and version files: + +```bash +python -X utf8 scripts/detect_project.py . +``` + +The script returns JSON with: +```json +{ + "project_type": "unity", + "version_files": [ + "version.json", + "ProjectSettings/ProjectSettings.asset" + ], + "detected_version": "1.2.3" +} +``` + +Supported project types: +- `nodejs` - Node.js (package.json) +- `python` - Python (pyproject.toml, setup.py) +- `rust` - Rust (Cargo.toml) +- `go` - Go (VERSION file) +- `unity` - Unity (version.json + ProjectSettings.asset) +- `unreal` - Unreal Engine (.uproject) +- `claude-plugin` - Claude Code Plugin (plugin.json) +- `generic` - Generic project (VERSION file) + +### Step 2: Determine New Version + +Calculate the new version based on update type: + +**Semantic Versioning (MAJOR.MINOR.PATCH):** +- **PATCH** (x.x.X): Bug fixes +- **MINOR** (x.X.0): New features (backward compatible) +- **MAJOR** (X.0.0): Breaking changes + +Examples: +- 1.2.3 → 1.2.4 (patch) +- 1.2.3 → 1.3.0 (minor) +- 1.2.3 → 2.0.0 (major) + +If user specifies version directly (e.g., "v2.1.0"), use that version. + +### Step 3: Update Version Files + +Use the universal version updater to update all detected files: + +```bash +python -X utf8 scripts/update_version.py ... +``` + +Example for Node.js: +```bash +python -X utf8 scripts/update_version.py package.json 1.3.0 +``` + +Example for Unity (multiple files): +```bash +python -X utf8 scripts/update_version.py version.json ProjectSettings/ProjectSettings.asset 1.3.0 +``` + +**Unity-specific:** After updating version.json, sync to ProjectSettings.asset: +```bash +python -X utf8 scripts/sync_unity_version.py +``` + +This ensures version.json is the single source of truth. + +### Step 4: Generate CHANGELOG (Optional) + +If user wants CHANGELOG updates or it's part of the workflow: + +```bash +python -X utf8 scripts/changelog_generator.py [since_tag] [changelog_path] +``` + +Examples: +```bash +# Generate from last tag to HEAD +python -X utf8 scripts/changelog_generator.py 1.3.0 v1.2.3 + +# Generate from all commits +python -X utf8 scripts/changelog_generator.py 1.3.0 + +# Custom CHANGELOG path +python -X utf8 scripts/changelog_generator.py 1.3.0 v1.2.3 CHANGELOG.ko.md +``` + +The script parses Conventional Commits and groups by type: +- `feat:` → Added +- `fix:` → Fixed +- `refactor:` → Changed +- `docs:` → Documentation + +### Step 5: Git Operations + +Ask user which git operations to perform using AskUserQuestion tool: + +**Tool constraints:** +- Header: "Git 작업" (6 characters ✅) +- Options: 3 options (within 2-4 range ✅) +- multiSelect: false + +**Options:** + +1. **"커밋만"** (label: 2 words ✅) + - Description: "버전 업데이트를 커밋만 합니다. 태그와 푸시는 나중에 직접 처리하겠습니다." + - Action: Commit only + +2. **"커밋+태그"** (label: 2 words ✅) + - Description: "커밋과 태그를 생성합니다. 푸시는 나중에 직접 하겠습니다." + - Action: Commit + Create tag + +3. **"전체"** (label: 1 word ✅) + - Description: "커밋, 태그 생성, 푸시까지 모든 작업을 자동으로 처리합니다. (권장)" + - Action: Commit + Tag + Push + +Based on user selection: + +**Commit:** +```bash +python -X utf8 scripts/git_operations.py commit "chore: bump version to + +Version updates: +- Updated to + +🤖 Generated with Claude Code +Co-Authored-By: Claude " +``` + +**Create Tag:** +```bash +python -X utf8 scripts/git_operations.py tag v "Release v" +``` + +**Push (branch + tags):** +```bash +python -X utf8 scripts/git_operations.py push +python -X utf8 scripts/git_operations.py push-tags +``` + +### Step 6: GitHub Release (Optional) + +If user wants GitHub release, use `gh` CLI: + +```bash +gh release create v \ + --title "v - " \ + --notes "<Release notes from CHANGELOG>" +``` + +## Project-Specific Workflows + +### Unity Workflow + +Unity projects require special handling due to dual-file approach: + +1. Detect Unity project (ProjectSettings.asset exists) +2. Update version.json first (source of truth) +3. Sync to ProjectSettings.asset using sync_unity_version.py +4. Commit both files together +5. Tag and push + +Reference: `references/unity-guide.md` for detailed Unity workflow. + +### Unreal Workflow + +Unreal projects use .uproject JSON file: + +1. Detect .uproject file +2. Update Version field in .uproject +3. Optionally sync to Config/DefaultGame.ini +4. Commit, tag, push + +Reference: `references/unreal-guide.md` for detailed Unreal workflow. + +### Node.js / Python / Rust Workflow + +Standard workflow for web/backend projects: + +1. Detect project type via package file +2. Update version field in package file +3. Generate CHANGELOG from commits +4. Commit, tag, push + +## Error Handling + +Handle common errors: + +**Project not detected:** +- Check if running in correct directory +- Look for .git directory +- Suggest creating VERSION file for generic projects + +**Version file not found:** +- Suggest creating file from template (assets/) +- For Unity: Create version.json from template + +**Git errors:** +- Check if git repository exists +- Verify remote is configured +- Handle authentication issues + +**Script errors:** +- Ensure Python 3.11+ is installed +- Check file permissions +- Verify file encodings (UTF-8) + +## Bundled Resources + +### Scripts + +All scripts are in `scripts/` directory and work cross-platform (Windows, macOS, Linux): + +- **detect_project.py**: Auto-detect project type +- **update_version.py**: Universal version updater +- **sync_unity_version.py**: Unity version synchronization +- **git_operations.py**: Git workflow automation +- **changelog_generator.py**: CHANGELOG generation + +### References + +Detailed guides in `references/` directory: + +- **project-types.md**: All supported project types and version file locations +- **unity-guide.md**: Unity-specific version management +- **unreal-guide.md**: Unreal Engine version management + +Load these as needed for detailed information. + +### Assets + +Templates in `assets/` directory: + +- **version.json.template**: Template for Unity/game projects +- **CHANGELOG.md.template**: Template for new CHANGELOG files + +Use these when creating new version files. + +## Best Practices + +- Always commit version files together (e.g., Unity's version.json + ProjectSettings.asset) +- Use semantic versioning consistently +- Write meaningful commit messages following Conventional Commits +- Tag releases with `v` prefix (e.g., v1.2.3) +- Keep CHANGELOG updated for user-facing changes +- For Unity: version.json is source of truth, always sync to ProjectSettings.asset + +## Notes + +- Scripts use Python 3.11+ with only stdlib dependencies +- All file operations use UTF-8 encoding +- Path handling uses pathlib for cross-platform compatibility +- Git operations use subprocess for reliability +- Unity YAML parsing uses regex for robustness diff --git a/skills/assets/CHANGELOG.md.template b/skills/assets/CHANGELOG.md.template new file mode 100644 index 0000000..311cc22 --- /dev/null +++ b/skills/assets/CHANGELOG.md.template @@ -0,0 +1,16 @@ +# Changelog + +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 adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added +- Initial project setup + +## [0.1.0] - 2025-11-01 + +### Added +- First release diff --git a/skills/assets/version.json.template b/skills/assets/version.json.template new file mode 100644 index 0000000..3b4f271 --- /dev/null +++ b/skills/assets/version.json.template @@ -0,0 +1,6 @@ +{ + "version": "0.1.0", + "buildNumber": 1, + "releaseDate": "2025-11-01", + "releaseNotes": "Initial release" +} diff --git a/skills/references/project-types.md b/skills/references/project-types.md new file mode 100644 index 0000000..4104517 --- /dev/null +++ b/skills/references/project-types.md @@ -0,0 +1,160 @@ +# Supported Project Types + +This document describes all project types supported by auto-release-manager and their version file locations. + +## Web & Backend + +### Node.js +- **Detection file**: `package.json` +- **Version location**: `package.json` → `version` field +- **Format**: JSON +- **Example**: + ```json + { + "name": "my-project", + "version": "1.2.3" + } + ``` + +### Python +- **Detection files**: `pyproject.toml` or `setup.py` +- **Version location**: + - `pyproject.toml` → `project.version` or `tool.poetry.version` + - `setup.py` → `version=` parameter +- **Format**: TOML or Python +- **Example**: + ```toml + [project] + name = "my-project" + version = "1.2.3" + ``` + +### Rust +- **Detection file**: `Cargo.toml` +- **Version location**: `Cargo.toml` → `package.version` +- **Format**: TOML +- **Example**: + ```toml + [package] + name = "my-project" + version = "1.2.3" + ``` + +### Go +- **Detection file**: `go.mod` +- **Version location**: `VERSION` file (custom) +- **Format**: Plain text +- **Note**: Go doesn't have built-in versioning in go.mod + +## Game Engines + +### Unity +- **Detection file**: `ProjectSettings/ProjectSettings.asset` +- **Version locations**: + 1. `version.json` (Primary source of truth) → `version` field + 2. `ProjectSettings/ProjectSettings.asset` → `bundleVersion` field +- **Format**: JSON + Unity YAML +- **Workflow**: Update `version.json` first, then sync to `ProjectSettings.asset` +- **Example**: + + `version.json`: + ```json + { + "version": "1.2.3", + "buildNumber": 42 + } + ``` + + `ProjectSettings.asset`: + ```yaml + PlayerSettings: + bundleVersion: 1.2.3 + ``` + +### Unreal Engine +- **Detection file**: `*.uproject` +- **Version location**: `<ProjectName>.uproject` → `Version` or `EngineAssociation` +- **Format**: JSON +- **Example**: + ```json + { + "FileVersion": 3, + "EngineAssociation": "5.3", + "Version": "1.2.3" + } + ``` + +### Godot +- **Detection file**: `project.godot` +- **Version location**: `project.godot` → `config/version` +- **Format**: INI-like +- **Example**: + ```ini + [application] + config/name="MyGame" + config/version="1.2.3" + ``` + +## Plugins & Extensions + +### Claude Code Plugin +- **Detection file**: `.claude-plugin/plugin.json` +- **Version location**: `.claude-plugin/plugin.json` → `version` +- **Format**: JSON +- **Example**: + ```json + { + "name": "my-plugin", + "version": "1.2.3", + "description": "..." + } + ``` + +### VS Code Extension +- **Detection file**: `package.json` + `package.nls.json` +- **Version location**: `package.json` → `version` +- **Format**: JSON +- **Same as Node.js projects** + +### Browser Extension +- **Detection file**: `manifest.json` +- **Version location**: `manifest.json` → `version` +- **Format**: JSON +- **Example**: + ```json + { + "manifest_version": 3, + "name": "My Extension", + "version": "1.2.3" + } + ``` + +## Generic Projects + +### Plain VERSION file +- **Detection file**: `VERSION` or `version.txt` +- **Version location**: Content of file +- **Format**: Plain text +- **Example**: + ``` + 1.2.3 + ``` + +## Version Format + +All projects follow **Semantic Versioning (semver)**: + +``` +MAJOR.MINOR.PATCH + +1.2.3 +│ │ │ +│ │ └─ PATCH: Bug fixes +│ └─── MINOR: New features (backward compatible) +└───── MAJOR: Breaking changes +``` + +Examples: +- `1.0.0` → `1.0.1` (patch: bug fix) +- `1.0.1` → `1.1.0` (minor: new feature) +- `1.1.0` → `2.0.0` (major: breaking change) diff --git a/skills/references/unity-guide.md b/skills/references/unity-guide.md new file mode 100644 index 0000000..a1883c3 --- /dev/null +++ b/skills/references/unity-guide.md @@ -0,0 +1,140 @@ +# Unity Project Version Management + +## Overview + +Unity projects use a dual-file approach for version management: +1. **`version.json`** - Single source of truth (easy to read/edit) +2. **`ProjectSettings/ProjectSettings.asset`** - Auto-synced (used by Unity builds) + +## File Structure + +``` +MyUnityProject/ +├── version.json # Primary source +├── ProjectSettings/ +│ └── ProjectSettings.asset # Auto-synced +└── Assets/ + └── ... +``` + +## version.json (Primary) + +Create this file at project root: + +```json +{ + "version": "1.2.3", + "buildNumber": 42, + "releaseDate": "2025-10-20" +} +``` + +**Fields:** +- `version` (required): Semantic version (MAJOR.MINOR.PATCH) +- `buildNumber` (optional): Incremental build number +- `releaseDate` (optional): Release date in YYYY-MM-DD format + +## ProjectSettings.asset (Auto-sync) + +The `bundleVersion` field in this file is automatically synced from `version.json`: + +```yaml +PlayerSettings: + bundleVersion: 1.2.3 + AndroidBundleVersionCode: 42 + buildNumber: + iOS: 42 +``` + +## Workflow + +### Manual Update + +1. Edit `version.json`: + ```json + { + "version": "1.3.0" + } + ``` + +2. Run sync script: + ```bash + python scripts/sync_unity_version.py + ``` + +3. Commit both files: + ```bash + git add version.json ProjectSettings/ProjectSettings.asset + git commit -m "chore: bump version to 1.3.0" + ``` + +### Automatic Update (with auto-release-manager) + +The skill handles everything automatically: + +``` +1. Update version.json → 1.3.0 +2. Sync to ProjectSettings.asset +3. Git commit both files +4. Create tag v1.3.0 +5. Push to remote +``` + +## Benefits of Dual-File Approach + +✅ **Easy Version Management** +- `version.json` is simple JSON, easy to read/edit +- Clean git diffs + +✅ **Unity Integration** +- `ProjectSettings.asset` is used by Unity builds +- Version automatically appears in build + +✅ **Automated Sync** +- Scripts ensure consistency +- No manual errors + +✅ **Extended Metadata** +- `version.json` can store additional info (buildNumber, releaseDate) +- `ProjectSettings.asset` only has bundleVersion + +## Common Issues + +### Q: Why not just use ProjectSettings.asset? + +A: ProjectSettings.asset is a YAML file that's hard to read and edit manually. It also contains hundreds of other settings, making git diffs noisy. + +### Q: Can I use only version.json? + +A: No. Unity reads bundleVersion from ProjectSettings.asset for builds. Both files are needed. + +### Q: What if files get out of sync? + +A: Run `python scripts/sync_unity_version.py` to sync from version.json (source of truth) to ProjectSettings.asset. + +## Example: Version Update + +**Before:** +```json +// version.json +{ + "version": "1.2.3", + "buildNumber": 42 +} +``` + +**After:** +```json +// version.json +{ + "version": "1.3.0", + "buildNumber": 43 +} +``` + +**Auto-synced:** +```yaml +# ProjectSettings.asset +PlayerSettings: + bundleVersion: 1.3.0 # ← Automatically updated +``` diff --git a/skills/references/unreal-guide.md b/skills/references/unreal-guide.md new file mode 100644 index 0000000..32b4236 --- /dev/null +++ b/skills/references/unreal-guide.md @@ -0,0 +1,152 @@ +# Unreal Engine Project Version Management + +## Overview + +Unreal Engine projects use `.uproject` JSON file for project configuration and versioning. + +## File Structure + +``` +MyUnrealProject/ +├── MyGame.uproject # Project file with version +├── Config/ +│ └── DefaultGame.ini # Game config +├── Source/ +│ └── ... +└── Content/ + └── ... +``` + +## .uproject File + +The project file is a JSON file containing project metadata: + +```json +{ + "FileVersion": 3, + "EngineAssociation": "5.3", + "Category": "", + "Description": "", + "Version": "1.2.3", + "Modules": [ + { + "Name": "MyGame", + "Type": "Runtime", + "LoadingPhase": "Default" + } + ], + "Plugins": [ + ... + ] +} +``` + +**Version Fields:** +- `Version` (custom): Semantic version of your project +- `EngineAssociation`: Unreal Engine version (e.g., "5.3", "5.4") + +## Version Update Workflow + +### Manual Update + +1. Edit `.uproject` file: + ```json + { + "Version": "1.3.0" + } + ``` + +2. Commit: + ```bash + git add MyGame.uproject + git commit -m "chore: bump version to 1.3.0" + ``` + +### Automatic Update (with auto-release-manager) + +``` +1. Detect .uproject file +2. Update Version field → 1.3.0 +3. Git commit +4. Create tag v1.3.0 +5. Push to remote +``` + +## DefaultGame.ini + +Optional: You can also store version in `Config/DefaultGame.ini`: + +```ini +[/Script/EngineSettings.GeneralProjectSettings] +ProjectID=... +ProjectName=MyGame +ProjectVersion=1.2.3 +CompanyName=MyCompany +``` + +This is useful for in-game version display. + +## Build Versioning + +For packaging builds, you may want to configure: + +**Config/DefaultGame.ini:** +```ini +[/Script/UnrealEd.ProjectPackagingSettings] +BuildConfiguration=PPBC_Shipping +ProjectVersion=1.2.3 +``` + +## Example: Full Update + +When updating version from 1.2.3 to 1.3.0: + +**Files to update:** +1. `MyGame.uproject` → `Version: "1.3.0"` +2. `Config/DefaultGame.ini` → `ProjectVersion=1.3.0` (if used) + +**auto-release-manager handles:** +- Detecting .uproject file +- Updating Version field +- Git commit + tag +- Push to remote + +## Common Issues + +### Q: Is Version field required in .uproject? + +A: No, it's optional. But recommended for version tracking. + +### Q: What about EngineAssociation? + +A: EngineAssociation is the UE version (e.g., "5.3"). Don't confuse it with your project version. + +### Q: Can I use multiple version fields? + +A: Yes! You can maintain: +- `.uproject` → Version (project version) +- `DefaultGame.ini` → ProjectVersion (in-game display) +- `version.json` → Custom metadata + +Just ensure they stay synced. + +## Recommended Approach + +**Best Practice:** +1. Use `.uproject` → Version as primary +2. Optionally sync to `DefaultGame.ini` for in-game display +3. Use auto-release-manager to keep everything consistent + +**Example:** +```json +// MyGame.uproject +{ + "Version": "1.3.0" +} +``` + +```ini +; Config/DefaultGame.ini +[/Script/EngineSettings.GeneralProjectSettings] +ProjectVersion=1.3.0 +``` diff --git a/skills/scripts/changelog_generator.py b/skills/scripts/changelog_generator.py new file mode 100644 index 0000000..e6f3696 --- /dev/null +++ b/skills/scripts/changelog_generator.py @@ -0,0 +1,205 @@ +#!/usr/bin/env python3 +""" +CHANGELOG generator from git commit history. +Supports Conventional Commits format. +""" + +import re +import subprocess +import sys +from datetime import datetime +from pathlib import Path +from typing import List, Dict, Optional + + +class ChangelogGenerator: + def __init__(self, repo_path: str = "."): + self.repo_path = Path(repo_path).resolve() + + # Conventional Commits types + self.commit_types = { + 'feat': 'Added', + 'fix': 'Fixed', + 'docs': 'Documentation', + 'style': 'Style', + 'refactor': 'Changed', + 'perf': 'Performance', + 'test': 'Tests', + 'chore': 'Chore', + 'build': 'Build', + 'ci': 'CI/CD', + } + + def get_commits_since_tag( + self, + since_tag: Optional[str] = None) -> List[str]: + """Get commit messages since last tag.""" + try: + if since_tag: + cmd = [ + 'git', + 'log', + f'{since_tag}..HEAD', + '--pretty=format:%s'] + else: + # Get all commits if no tag specified + cmd = ['git', 'log', '--pretty=format:%s'] + + result = subprocess.run( + cmd, + cwd=self.repo_path, + capture_output=True, + text=True, + check=True, + encoding='utf-8' + ) + + return ( + result.stdout.strip().split('\n') + if result.stdout.strip() else [] + ) + + except subprocess.CalledProcessError as e: + print(f"Error getting git commits: {e.stderr}", file=sys.stderr) + return [] + + def parse_commit(self, commit_message: str) -> Dict[str, str]: + """Parse conventional commit message.""" + # Pattern: type(scope): description + pattern = r'^(\w+)(?:\(([^)]+)\))?: (.+)$' + match = re.match(pattern, commit_message) + + if match: + commit_type, scope, description = match.groups() + return { + 'type': commit_type, + 'scope': scope or '', + 'description': description, + 'raw': commit_message + } + else: + return { + 'type': 'other', + 'scope': '', + 'description': commit_message, + 'raw': commit_message + } + + def group_commits(self, commits: List[str]) -> Dict[str, List[str]]: + """Group commits by type.""" + grouped: Dict[str, List[str]] = {} + + for commit_msg in commits: + parsed = self.parse_commit(commit_msg) + commit_type = parsed['type'] + description = parsed['description'] + + category = self.commit_types.get(commit_type, 'Other') + + if category not in grouped: + grouped[category] = [] + + grouped[category].append(description) + + return grouped + + def generate_changelog_entry( + self, + version: str, + since_tag: Optional[str] = None) -> str: + """Generate changelog entry for new version.""" + commits = self.get_commits_since_tag(since_tag) + + if not commits: + print("No commits found", file=sys.stderr) + return "" + + grouped = self.group_commits(commits) + + # Build changelog entry + date = datetime.now().strftime('%Y-%m-%d') + lines = [ + f"## [{version}] - {date}", + "" + ] + + # Add sections in order + section_order = [ + 'Added', 'Fixed', 'Changed', 'Deprecated', 'Removed', + 'Security', 'Performance', 'Documentation', 'Other' + ] + + for section in section_order: + if section in grouped: + lines.append(f"### {section}") + for item in grouped[section]: + lines.append(f"- {item}") + lines.append("") + + return '\n'.join(lines) + + def update_changelog_file( + self, + version: str, + since_tag: Optional[str] = None, + changelog_path: str = "CHANGELOG.md" + ) -> bool: + """Update CHANGELOG.md file with new entry.""" + entry = self.generate_changelog_entry(version, since_tag) + + if not entry: + return False + + changelog_file = self.repo_path / changelog_path + + if changelog_file.exists(): + # Insert at top after header + content = changelog_file.read_text(encoding='utf-8') + + # Find position to insert (after # Changelog header) + lines = content.split('\n') + insert_pos = 0 + + for i, line in enumerate(lines): + if line.startswith('# '): + insert_pos = i + 1 + # Skip empty lines after header + while (insert_pos < len(lines) and + not lines[insert_pos].strip()): + insert_pos += 1 + break + + lines.insert(insert_pos, entry) + new_content = '\n'.join(lines) + + else: + # Create new CHANGELOG + new_content = f"# Changelog\n\n{entry}" + + changelog_file.write_text(new_content, encoding='utf-8') + print(f"✓ Updated {changelog_path}") + + return True + + +def main(): + if len(sys.argv) < 2: + print( + "Usage: python changelog_generator.py " + "<version> [since_tag] [changelog_path]", + file=sys.stderr + ) + sys.exit(1) + + version = sys.argv[1] + since_tag = sys.argv[2] if len(sys.argv) > 2 else None + changelog_path = sys.argv[3] if len(sys.argv) > 3 else "CHANGELOG.md" + + generator = ChangelogGenerator() + + if not generator.update_changelog_file(version, since_tag, changelog_path): + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/skills/scripts/detect_project.py b/skills/scripts/detect_project.py new file mode 100644 index 0000000..f22eb45 --- /dev/null +++ b/skills/scripts/detect_project.py @@ -0,0 +1,281 @@ +#!/usr/bin/env python3 +""" +Detect project type and version files automatically. +Supports: Node.js, Python, Rust, Go, Unity, Unreal Engine, +Claude Code Plugins, etc. +""" + +import json +import sys +import tomllib # Python 3.11+ required +from pathlib import Path +from typing import Any, Dict, Optional + +# Check Python version +if sys.version_info < (3, 11): + print( + "Error: Python 3.11+ is required for this script", + file=sys.stderr + ) + print( + "Current version: " + f"{sys.version_info.major}.{sys.version_info.minor}", + file=sys.stderr + ) + sys.exit(1) + + +class ProjectDetector: + def __init__(self, project_root: str = "."): + self.root = Path(project_root).resolve() + + def detect(self) -> Dict[str, Any]: + """Detect project type and return version file paths.""" + result: Dict[str, Any] = { + "project_type": None, + "version_files": [], + "detected_version": None + } + + # Check in priority order + detectors = [ + self._detect_unity, + self._detect_unreal, + self._detect_nodejs, + self._detect_python, + self._detect_rust, + self._detect_go, + self._detect_claude_plugin, + self._detect_generic + ] + + for detector in detectors: + detection = detector() + if detection: + result.update(detection) + break + + return result + + def _detect_unity(self) -> Optional[Dict[str, Any]]: + """Detect Unity project.""" + project_settings = ( + self.root / "ProjectSettings" / "ProjectSettings.asset" + ) + version_json = self.root / "version.json" + + if project_settings.exists(): + files = [str(project_settings)] + if version_json.exists(): + files.append(str(version_json)) + + # Try to read version from version.json first + version = None + if version_json.exists(): + try: + data = json.loads( + version_json.read_text(encoding='utf-8') + ) + version = data.get('version') + except Exception: + pass + + # Fallback to ProjectSettings.asset + if not version: + try: + content = project_settings.read_text( + encoding='utf-8' + ) + for line in content.split('\n'): + if 'bundleVersion:' in line: + version = line.split(':', 1)[1].strip() + break + except Exception: + pass + + return { + "project_type": "unity", + "version_files": files, + "detected_version": version + } + + return None + + def _detect_unreal(self) -> Optional[Dict[str, Any]]: + """Detect Unreal Engine project.""" + uproject_files = list(self.root.glob("*.uproject")) + + if uproject_files: + uproject = uproject_files[0] + version = None + + try: + data = json.loads(uproject.read_text(encoding='utf-8')) + version = data.get('Version') or data.get('EngineAssociation') + except (json.JSONDecodeError, IOError, UnicodeDecodeError): + pass + + return { + "project_type": "unreal", + "version_files": [str(uproject)], + "detected_version": version + } + + return None + + def _detect_nodejs(self) -> Optional[Dict[str, Any]]: + """Detect Node.js project.""" + package_json = self.root / "package.json" + + if package_json.exists(): + version = None + + try: + data = json.loads(package_json.read_text(encoding='utf-8')) + version = data.get('version') + except BaseException: + pass + + return { + "project_type": "nodejs", + "version_files": [str(package_json)], + "detected_version": version + } + + return None + + def _detect_python(self) -> Optional[Dict[str, Any]]: + """Detect Python project.""" + pyproject = self.root / "pyproject.toml" + setup_py = self.root / "setup.py" + + if pyproject.exists(): + version = None + + if tomllib: + try: + data = tomllib.loads( + pyproject.read_text(encoding='utf-8') + ) + version = ( + data.get('project', {}).get('version') or + data.get('tool', {}).get('poetry', {}).get('version') + ) + except Exception: + pass + + return { + "project_type": "python", + "version_files": [str(pyproject)], + "detected_version": version + } + + elif setup_py.exists(): + return { + "project_type": "python", + "version_files": [str(setup_py)], + "detected_version": None # Requires parsing Python + } + + return None + + def _detect_rust(self) -> Optional[Dict[str, Any]]: + """Detect Rust project.""" + cargo_toml = self.root / "Cargo.toml" + + if cargo_toml.exists(): + version = None + + if tomllib: + try: + data = tomllib.loads( + cargo_toml.read_text( + encoding='utf-8')) + version = data.get('package', {}).get('version') + except BaseException: + pass + + return { + "project_type": "rust", + "version_files": [str(cargo_toml)], + "detected_version": version + } + + return None + + def _detect_go(self) -> Optional[Dict[str, Any]]: + """Detect Go project.""" + go_mod = self.root / "go.mod" + + if go_mod.exists(): + # Go doesn't have built-in versioning in go.mod + # Check for common version files + version_file = self.root / "VERSION" + + if version_file.exists(): + version = version_file.read_text().strip() + return { + "project_type": "go", + "version_files": [str(version_file)], + "detected_version": version + } + + return { + "project_type": "go", + "version_files": [], + "detected_version": None + } + + return None + + def _detect_claude_plugin(self) -> Optional[Dict[str, Any]]: + """Detect Claude Code plugin.""" + plugin_json = self.root / ".claude-plugin" / "plugin.json" + + if plugin_json.exists(): + version = None + + try: + data = json.loads(plugin_json.read_text(encoding='utf-8')) + version = data.get('version') + except BaseException: + pass + + return { + "project_type": "claude-plugin", + "version_files": [str(plugin_json)], + "detected_version": version + } + + return None + + def _detect_generic(self) -> Optional[Dict[str, Any]]: + """Detect generic project with VERSION file.""" + version_file = self.root / "VERSION" + + if version_file.exists(): + version = version_file.read_text().strip() + return { + "project_type": "generic", + "version_files": [str(version_file)], + "detected_version": version + } + + return { + "project_type": "unknown", + "version_files": [], + "detected_version": None + } + + +def main(): + project_root = sys.argv[1] if len(sys.argv) > 1 else "." + + detector = ProjectDetector(project_root) + result = detector.detect() + + print(json.dumps(result, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/skills/scripts/git_operations.py b/skills/scripts/git_operations.py new file mode 100644 index 0000000..2d63cca --- /dev/null +++ b/skills/scripts/git_operations.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python3 +""" +Git operations wrapper with cross-platform support. +Handles: commit, tag, push, branch detection +""" + +import subprocess +import sys +from pathlib import Path +from typing import Optional, List + + +class GitOperations: + def __init__(self, repo_path: str = "."): + self.repo_path = Path(repo_path).resolve() + + def run_command( + self, + cmd: List[str], + check: bool = True) -> subprocess.CompletedProcess[str]: + """Run git command with proper error handling.""" + try: + result = subprocess.run( + cmd, + cwd=self.repo_path, + capture_output=True, + text=True, + check=check, + encoding='utf-8' + ) + return result + except subprocess.CalledProcessError as e: + print(f"Git command failed: {' '.join(cmd)}", file=sys.stderr) + print(f"Error: {e.stderr}", file=sys.stderr) + raise + + def get_current_branch(self) -> str: + """Get current git branch name.""" + result = self.run_command(['git', 'branch', '--show-current']) + return result.stdout.strip() + + def get_status(self) -> str: + """Get git status.""" + result = self.run_command(['git', 'status', '--porcelain']) + return result.stdout + + def add_files(self, files: Optional[List[str]] = None) -> bool: + """Stage files for commit.""" + if files: + self.run_command(['git', 'add'] + files) + else: + self.run_command(['git', 'add', '-A']) + + print("✓ Files staged for commit") + return True + + def commit(self, message: str) -> bool: + """Create git commit.""" + self.run_command(['git', 'commit', '-m', message]) + print(f"✓ Committed: {message.split('\n')[0][:60]}...") + return True + + def create_tag(self, tag_name: str, message: Optional[str] = None) -> bool: + """Create annotated git tag.""" + cmd = ['git', 'tag', '-a', tag_name] + + if message: + cmd.extend(['-m', message]) + else: + cmd.extend(['-m', f"Release {tag_name}"]) + + self.run_command(cmd) + print(f"✓ Created tag: {tag_name}") + return True + + def push( + self, + remote: str = 'origin', + branch: Optional[str] = None, + tags: bool = False) -> bool: + """Push commits and/or tags to remote.""" + if tags: + # Push tags + cmd = ['git', 'push', remote, '--tags'] + self.run_command(cmd) + print(f"✓ Pushed tags to {remote}") + else: + # Push branch + if not branch: + branch = self.get_current_branch() + + cmd = ['git', 'push', remote, branch] + self.run_command(cmd) + print(f"✓ Pushed {branch} to {remote}") + + return True + + def tag_exists(self, tag_name: str) -> bool: + """Check if tag exists.""" + result = self.run_command(['git', 'tag', '-l', tag_name], check=False) + return bool(result.stdout.strip()) + + +def main(): + if len(sys.argv) < 2: + print("Usage:", file=sys.stderr) + print(" python git_operations.py commit <message>", file=sys.stderr) + print( + " python git_operations.py tag <tag_name> [message]", + file=sys.stderr) + print(" python git_operations.py push [branch]", file=sys.stderr) + print(" python git_operations.py push-tags", file=sys.stderr) + sys.exit(1) + + action = sys.argv[1] + git = GitOperations() + + try: + if action == 'commit': + if len(sys.argv) < 3: + print("Error: Commit message required", file=sys.stderr) + sys.exit(1) + + message = sys.argv[2] + git.add_files() + git.commit(message) + + elif action == 'tag': + if len(sys.argv) < 3: + print("Error: Tag name required", file=sys.stderr) + sys.exit(1) + + tag_name = sys.argv[2] + message = sys.argv[3] if len(sys.argv) > 3 else None + git.create_tag(tag_name, message) + + elif action == 'push': + branch = sys.argv[2] if len(sys.argv) > 2 else None + git.push(branch=branch) + + elif action == 'push-tags': + git.push(tags=True) + + else: + print(f"Error: Unknown action '{action}'", file=sys.stderr) + sys.exit(1) + + except Exception as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/skills/scripts/sync_unity_version.py b/skills/scripts/sync_unity_version.py new file mode 100644 index 0000000..896f9ed --- /dev/null +++ b/skills/scripts/sync_unity_version.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +""" +Sync Unity version from version.json to ProjectSettings.asset. +This ensures version.json is the single source of truth. +""" + +import json +import re +import sys +from pathlib import Path + + +def sync_unity_version(project_root: str = ".") -> bool: + """Sync version from version.json to ProjectSettings.asset.""" + root = Path(project_root).resolve() + + version_json_path = root / "version.json" + project_settings_path = root / "ProjectSettings" / "ProjectSettings.asset" + + # Check if files exist + if not version_json_path.exists(): + print( + f"Error: version.json not found at {version_json_path}", + file=sys.stderr) + return False + + if not project_settings_path.exists(): + print( + f"Error: ProjectSettings.asset not found at " + f"{project_settings_path}", + file=sys.stderr) + return False + + try: + # Read version from version.json + version_data = json.loads( + version_json_path.read_text( + encoding='utf-8')) + version = version_data.get('version') + + if not version: + print( + "Error: 'version' field not found in version.json", + file=sys.stderr) + return False + + # Read ProjectSettings.asset + content = project_settings_path.read_text(encoding='utf-8') + + # Find and replace bundleVersion + pattern = r'(bundleVersion:\s*)(.+)' + replacement = r'\g<1>' + version + + if not re.search(pattern, content): + print( + "Error: bundleVersion not found in ProjectSettings.asset", + file=sys.stderr) + return False + + # Update content + new_content = re.sub(pattern, replacement, content) + + if new_content == content: + print(f"✓ Version already synced: {version}") + return True + + # Write back + project_settings_path.write_text(new_content, encoding='utf-8') + print(f"✓ Synced Unity version: {version}") + print(" version.json → ProjectSettings.asset") + + return True + + except (json.JSONDecodeError, IOError, UnicodeDecodeError) as e: + print(f"Error syncing Unity version: {e}", file=sys.stderr) + return False + + +def main(): + project_root = sys.argv[1] if len(sys.argv) > 1 else "." + + if not sync_unity_version(project_root): + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/skills/scripts/update_version.py b/skills/scripts/update_version.py new file mode 100644 index 0000000..865e7ac --- /dev/null +++ b/skills/scripts/update_version.py @@ -0,0 +1,231 @@ +#!/usr/bin/env python3 +""" +Universal version updater supporting multiple file formats. +Supports: JSON, TOML, YAML, plain text, Unity asset files, etc. +""" + +import json +import re +import sys +from pathlib import Path +from typing import List + + +class VersionUpdater: + def __init__(self): + self.updated_files: List[str] = [] + + def update(self, file_path: str, new_version: str) -> bool: + """Update version in file based on its format.""" + path = Path(file_path) + + if not path.exists(): + print(f"Error: File not found: {file_path}", file=sys.stderr) + return False + + # Detect file type and update accordingly + if path.suffix == '.json': + return self._update_json(path, new_version) + elif path.suffix == '.toml': + return self._update_toml(path, new_version) + elif path.suffix in ['.yaml', '.yml']: + return self._update_yaml(path, new_version) + elif path.name == 'ProjectSettings.asset': + return self._update_unity_asset(path, new_version) + elif path.suffix == '.uproject': + return self._update_unreal_uproject(path, new_version) + elif path.name in ['VERSION', 'version.txt']: + return self._update_plain_text(path, new_version) + else: + print(f"Warning: Unknown file type: {file_path}", file=sys.stderr) + return False + + def _update_json(self, path: Path, new_version: str) -> bool: + """Update version in JSON file.""" + try: + data = json.loads(path.read_text(encoding='utf-8')) + + # Update version field + if 'version' in data: + old_version = data['version'] + data['version'] = new_version + print(f"Updated {path.name}: {old_version} → {new_version}") + else: + data['version'] = new_version + print(f"Added version to {path.name}: {new_version}") + + # Write back with proper formatting + path.write_text( + json.dumps( + data, + indent=2, + ensure_ascii=False) + + '\n', + encoding='utf-8') + self.updated_files.append(str(path)) + return True + + except Exception as e: + print(f"Error updating {path}: {e}", file=sys.stderr) + return False + + def _update_toml(self, path: Path, new_version: str) -> bool: + """Update version in TOML file.""" + try: + content = path.read_text(encoding='utf-8') + + # Try to find and replace version + patterns = [ + (r'(version\s*=\s*["\'])([^"\']+)(["\'])', + r'\g<1>' + new_version + r'\g<3>'), + (r'(\[project\].*?version\s*=\s*["\'])([^"\']+)(["\'])', + r'\g<1>' + new_version + r'\g<3>'), + ] + + updated = False + for pattern, replacement in patterns: + if re.search(pattern, content, re.DOTALL): + content = re.sub( + pattern, replacement, content, flags=re.DOTALL) + updated = True + break + + if updated: + path.write_text(content, encoding='utf-8') + self.updated_files.append(str(path)) + print(f"Updated {path.name} → {new_version}") + return True + else: + print( + f"Warning: Could not find version in { + path.name}", file=sys.stderr) + return False + + except Exception as e: + print(f"Error updating {path}: {e}", file=sys.stderr) + return False + + def _update_yaml(self, path: Path, new_version: str) -> bool: + """Update version in YAML file.""" + try: + content = path.read_text(encoding='utf-8') + + # Replace version field + pattern = r'(version:\s*["\']?)([^"\'\n]+)(["\']?)' + replacement = r'\g<1>' + new_version + r'\g<3>' + + if re.search(pattern, content): + content = re.sub(pattern, replacement, content) + path.write_text(content, encoding='utf-8') + self.updated_files.append(str(path)) + print(f"Updated {path.name} → {new_version}") + return True + else: + print( + f"Warning: Could not find version in { + path.name}", file=sys.stderr) + return False + + except Exception as e: + print(f"Error updating {path}: {e}", file=sys.stderr) + return False + + def _update_unity_asset(self, path: Path, new_version: str) -> bool: + """Update bundleVersion in Unity ProjectSettings.asset.""" + try: + content = path.read_text(encoding='utf-8') + + # Replace bundleVersion + pattern = r'(bundleVersion:\s*)(.+)' + replacement = r'\g<1>' + new_version + + if re.search(pattern, content): + old_content = content + content = re.sub(pattern, replacement, content) + + if content != old_content: + path.write_text(content, encoding='utf-8') + self.updated_files.append(str(path)) + print( + f"Updated Unity ProjectSettings.asset → {new_version}") + return True + + print( + f"Warning: Could not find bundleVersion in { + path.name}", file=sys.stderr) + return False + + except Exception as e: + print(f"Error updating {path}: {e}", file=sys.stderr) + return False + + def _update_unreal_uproject(self, path: Path, new_version: str) -> bool: + """Update version in Unreal .uproject file.""" + try: + data = json.loads(path.read_text(encoding='utf-8')) + + # Unreal uses EngineAssociation or custom Version field + if 'Version' in data: + old_version = data['Version'] + data['Version'] = new_version + print(f"Updated {path.name}: {old_version} → {new_version}") + else: + data['Version'] = new_version + print(f"Added Version to {path.name}: {new_version}") + + path.write_text( + json.dumps( + data, + indent=4, + ensure_ascii=False) + + '\n', + encoding='utf-8') + self.updated_files.append(str(path)) + return True + + except Exception as e: + print(f"Error updating {path}: {e}", file=sys.stderr) + return False + + def _update_plain_text(self, path: Path, new_version: str) -> bool: + """Update plain text version file.""" + try: + path.write_text(new_version + '\n', encoding='utf-8') + self.updated_files.append(str(path)) + print(f"Updated {path.name} → {new_version}") + return True + + except Exception as e: + print(f"Error updating {path}: {e}", file=sys.stderr) + return False + + +def main(): + if len(sys.argv) < 3: + print( + "Usage: python update_version.py <file_path> <new_version>", + file=sys.stderr) + print( + " python update_version.py " + "<file1> <file2> ... <new_version>", + file=sys.stderr) + sys.exit(1) + + files = sys.argv[1:-1] + new_version = sys.argv[-1] + + updater = VersionUpdater() + success_count = 0 + + for file_path in files: + if updater.update(file_path, new_version): + success_count += 1 + + print(f"\nUpdated {success_count}/{len(files)} files successfully") + + if success_count < len(files): + sys.exit(1) + + +if __name__ == "__main__": + main()