Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:18:47 +08:00
commit aca019f43d
14 changed files with 1826 additions and 0 deletions

View File

@@ -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"
]
}

3
README.md Normal file
View File

@@ -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.

85
plugin.lock.json Normal file
View File

@@ -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": []
}
}

294
skills/SKILL.md Normal file
View File

@@ -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 <file1> <file2> ... <new_version>
```
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 <new_version> [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>
Version updates:
- Updated <files> to <VERSION>
🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>"
```
**Create Tag:**
```bash
python -X utf8 scripts/git_operations.py tag v<VERSION> "Release v<VERSION>"
```
**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<VERSION> \
--title "v<VERSION> - <Title>" \
--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

View File

@@ -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

View File

@@ -0,0 +1,6 @@
{
"version": "0.1.0",
"buildNumber": 1,
"releaseDate": "2025-11-01",
"releaseNotes": "Initial release"
}

View File

@@ -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)

View File

@@ -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
```

View File

@@ -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
```

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()