|
30 | 30 | "chore": "🔧 Chores", |
31 | 31 | } |
32 | 32 |
|
33 | | -def run_command(cmd: str, check: bool = True) -> str: |
34 | | - """Run a shell command and return output.""" |
| 33 | +def run_command(cmd: str | list[str], check: bool = True) -> str: |
| 34 | + """Run a command safely without shell=True. |
| 35 | +
|
| 36 | + Args: |
| 37 | + cmd: Command as list of arguments (preferred) or string (will be parsed) |
| 38 | + check: Whether to raise exception on non-zero exit |
| 39 | +
|
| 40 | + Returns: |
| 41 | + Command output as string |
| 42 | + """ |
| 43 | + import shlex |
| 44 | + if isinstance(cmd, str): |
| 45 | + cmd = shlex.split(cmd) |
| 46 | + |
35 | 47 | try: |
36 | | - result = subprocess.run(cmd, shell=True, capture_output=True, text=True, check=check) |
| 48 | + result = subprocess.run(cmd, shell=False, capture_output=True, text=True, check=check) |
37 | 49 | return result.stdout.strip() |
38 | 50 | except subprocess.CalledProcessError as e: |
39 | 51 | if check: |
40 | | - print(f"❌ Command failed: {cmd}") |
| 52 | + print(f"❌ Command failed: {' '.join(cmd) if isinstance(cmd, list) else cmd}") |
41 | 53 | print(f" Error: {e.stderr}") |
42 | 54 | sys.exit(1) |
43 | 55 | return "" |
@@ -128,11 +140,15 @@ def update_changelog(version: str) -> None: |
128 | 140 | current_date = datetime.now().strftime("%Y-%m-%d") |
129 | 141 |
|
130 | 142 | # Get commits since last tag |
131 | | - last_tag = run_command("git describe --tags --abbrev=0 2>/dev/null || echo ''", check=False) |
| 143 | + try: |
| 144 | + last_tag = run_command(["git", "describe", "--tags", "--abbrev=0"], check=True) |
| 145 | + except Exception: |
| 146 | + last_tag = "" |
| 147 | + |
132 | 148 | if last_tag: |
133 | | - raw_commits = run_command(f"git log {last_tag}..HEAD --pretty=format:'%s'").split('\n') |
| 149 | + raw_commits = run_command(["git", "log", f"{last_tag}..HEAD", "--pretty=format:%s"]).split('\n') |
134 | 150 | else: |
135 | | - raw_commits = run_command("git log --pretty=format:'%s'").split('\n') |
| 151 | + raw_commits = run_command(["git", "log", "--pretty=format:%s"]).split('\n') |
136 | 152 |
|
137 | 153 | # Categorize commits |
138 | 154 | categorized_commits = categorize_commits(raw_commits) |
@@ -177,14 +193,14 @@ def create_version_commits(new_version: str) -> None: |
177 | 193 | update_version_in_files(new_version) |
178 | 194 |
|
179 | 195 | # Commit version bump |
180 | | - run_command('git add pyproject.toml commitloom/__init__.py') |
181 | | - run_command(f'git commit -m "build: bump version to {new_version}"') |
| 196 | + run_command(["git", "add", "pyproject.toml", "commitloom/__init__.py"]) |
| 197 | + run_command(["git", "commit", "-m", f"build: bump version to {new_version}"]) |
182 | 198 | print("✅ Committed version bump") |
183 | 199 |
|
184 | 200 | # Update changelog |
185 | 201 | update_changelog(new_version) |
186 | | - run_command('git add CHANGELOG.md') |
187 | | - run_command(f'git commit -m "docs: update changelog for {new_version}"') |
| 202 | + run_command(["git", "add", "CHANGELOG.md"]) |
| 203 | + run_command(["git", "commit", "-m", f"docs: update changelog for {new_version}"]) |
188 | 204 | print("✅ Committed changelog update") |
189 | 205 |
|
190 | 206 | def get_changelog_entry(version: str) -> str: |
@@ -215,22 +231,22 @@ def create_github_release(version: str, dry_run: bool = False) -> None: |
215 | 231 | changelog_content = get_changelog_entry(version) |
216 | 232 | tag_message = f"Release {tag}\n\n{changelog_content}" if changelog_content else f"Release {tag}" |
217 | 233 |
|
218 | | - run_command(f'git tag -a {tag} -m "{tag_message}"') |
| 234 | + run_command(["git", "tag", "-a", tag, "-m", tag_message]) |
219 | 235 | print(f"✅ Created tag {tag}") |
220 | 236 |
|
221 | 237 | # Push commits and tag |
222 | | - run_command("git push origin main") |
| 238 | + run_command(["git", "push", "origin", "main"]) |
223 | 239 | print("✅ Pushed commits to main") |
224 | 240 |
|
225 | | - run_command("git push origin --tags") |
| 241 | + run_command(["git", "push", "origin", "--tags"]) |
226 | 242 | print("✅ Pushed tag to origin") |
227 | 243 |
|
228 | 244 | # Create GitHub Release via API |
229 | 245 | github_token = os.getenv("GITHUB_TOKEN") |
230 | 246 | if github_token: |
231 | 247 | try: |
232 | 248 | # Get repository info from git remote |
233 | | - remote_url = run_command("git remote get-url origin") |
| 249 | + remote_url = run_command(["git", "remote", "get-url", "origin"]) |
234 | 250 | repo_match = re.search(r"github\.com[:/](.+?)(?:\.git)?$", remote_url) |
235 | 251 | if not repo_match: |
236 | 252 | print("⚠️ Could not parse GitHub repository from remote URL") |
@@ -276,25 +292,24 @@ def create_github_release(version: str, dry_run: bool = False) -> None: |
276 | 292 | def check_prerequisites() -> None: |
277 | 293 | """Check that we can proceed with release.""" |
278 | 294 | # Ensure we're on main branch |
279 | | - current_branch = run_command("git branch --show-current") |
| 295 | + current_branch = run_command(["git", "branch", "--show-current"]) |
280 | 296 | if current_branch != "main": |
281 | 297 | print(f"❌ Must be on main branch to release (currently on {current_branch})") |
282 | 298 | sys.exit(1) |
283 | 299 |
|
284 | 300 | # Ensure working directory is clean |
285 | | - if run_command("git status --porcelain"): |
| 301 | + if run_command(["git", "status", "--porcelain"]): |
286 | 302 | print("❌ Working directory is not clean. Commit or stash changes first.") |
287 | 303 | sys.exit(1) |
288 | 304 |
|
289 | 305 | # Ensure git user is configured |
290 | | - user_name = run_command("git config user.name", check=False) |
291 | | - user_email = run_command("git config user.email", check=False) |
| 306 | + user_name = run_command(["git", "config", "user.name"], check=False) |
| 307 | + user_email = run_command(["git", "config", "user.email"], check=False) |
292 | 308 | if not user_name or not user_email: |
293 | | - print("⚠️ Git user not configured. Setting default values...") |
294 | | - if not user_name: |
295 | | - run_command('git config user.name "Petru Arakiss"') |
296 | | - if not user_email: |
297 | | - run_command('git config user.email "petruarakiss@gmail.com"') |
| 309 | + print("❌ Git user not configured. Please configure git user:") |
| 310 | + print(" git config user.name 'Your Name'") |
| 311 | + print(" git config user.email 'your.email@example.com'") |
| 312 | + sys.exit(1) |
298 | 313 |
|
299 | 314 | def main() -> None: |
300 | 315 | parser = argparse.ArgumentParser( |
@@ -350,7 +365,7 @@ def main() -> None: |
350 | 365 | tag = f"v{new_version}" |
351 | 366 | changelog_content = get_changelog_entry(new_version) |
352 | 367 | tag_message = f"Release {tag}\n\n{changelog_content}" if changelog_content else f"Release {tag}" |
353 | | - run_command(f'git tag -a {tag} -m "{tag_message}"') |
| 368 | + run_command(["git", "tag", "-a", tag, "-m", tag_message]) |
354 | 369 | print(f"✅ Created tag {tag}") |
355 | 370 | print("ℹ️ Skipped GitHub release (use --skip-github=false to enable)") |
356 | 371 |
|
|
0 commit comments