Compare commits
20 Commits
c005d1d38c
...
v0.4.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 66c1c5c55f | |||
|
89c4a4a073
|
|||
| 72cb0cc20f | |||
|
39f01b266f
|
|||
|
31c3f7d438
|
|||
| 4ea3b28081 | |||
|
702aa2a75b
|
|||
| 529cd19ec6 | |||
|
fdff738fa7
|
|||
|
7f629fd752
|
|||
|
801c8e24a1
|
|||
| f63ef85dd7 | |||
|
c2013fc205
|
|||
| e5015ec2f3 | |||
|
1163744af9
|
|||
| e3c703aa32 | |||
|
aff319cc5f
|
|||
|
fc8b885214
|
|||
|
7067f0872b
|
|||
| d5c2873d39 |
45
.gitea/scripts/cleanup_versions.sh
Executable file
45
.gitea/scripts/cleanup_versions.sh
Executable file
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# cleanup_dev_versions.sh - Delete old PyPI dev versions from Gitea package registry
|
||||
|
||||
# Required environment variables
|
||||
USERNAME="${TWINE_USERNAME}"
|
||||
TOKEN="${TWINE_PASSWORD}"
|
||||
REPO="${GITHUB_REPOSITORY}" # e.g., maxp/repocat
|
||||
API_BASE="${GITHUB_API_URL%/}" # Strip trailing slash if present
|
||||
|
||||
OWNER="${REPO%%/*}"
|
||||
PACKAGE_NAME="${REPO##*/}"
|
||||
API_URL="${API_BASE}/packages/${OWNER}/pypi/${PACKAGE_NAME}"
|
||||
|
||||
# Fetch the list of versions
|
||||
response=$(curl -s -u "$USERNAME:$TOKEN" "$API_URL")
|
||||
|
||||
# Extract all .dev versions, sort by creation time
|
||||
mapfile -t versions_to_delete < <(echo "$response" | jq -r '
|
||||
map(select(.version | test("\\.dev"))) |
|
||||
sort_by(.created_at) |
|
||||
.[0:-1][] |
|
||||
.version')
|
||||
|
||||
# Determine latest version to keep
|
||||
latest_version=$(echo "$response" | jq -r '
|
||||
map(select(.version | test("\\.dev"))) |
|
||||
sort_by(.created_at) |
|
||||
last.version')
|
||||
|
||||
if [[ -z "$latest_version" || ${#versions_to_delete[@]} -eq 0 ]]; then
|
||||
echo "No old .dev versions to delete."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Keeping latest .dev version: $latest_version"
|
||||
|
||||
# Delete old .dev versions
|
||||
for version in "${versions_to_delete[@]}"; do
|
||||
echo "Deleting old .dev version: $version"
|
||||
curl -s -X DELETE -u "$USERNAME:$TOKEN" "$API_URL/$version"
|
||||
done
|
||||
|
||||
echo "Cleanup complete."
|
||||
@@ -53,3 +53,10 @@ jobs:
|
||||
TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }}
|
||||
run: |
|
||||
poetry run twine upload --repository-url ${{ secrets.TWINE_URL }} dist/*
|
||||
|
||||
- name: Cleanup old dev versions
|
||||
run: |
|
||||
.gitea/scripts/cleanup_versions.sh '\.dev'
|
||||
env:
|
||||
TWINE_USERNAME: ${{ secrets.TWINE_USERNAME }}
|
||||
TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }}
|
||||
@@ -1,222 +1,18 @@
|
||||
name: Auto Changelog & Release
|
||||
name: Auto Changelog & (Release)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- '**'
|
||||
|
||||
jobs:
|
||||
detect-version-change:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version_changed: ${{ steps.set.outputs.version_changed }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Check if VERSION file changed
|
||||
if: github.ref == 'refs/heads/main'
|
||||
run: |
|
||||
echo "🔍 Vergleich mit github.event.before:"
|
||||
echo "Before: ${{ github.event.before }}"
|
||||
echo "After: ${{ github.sha }}"
|
||||
|
||||
echo "📄 Changed files between before and after:"
|
||||
git diff --name-only ${{ github.event.before }} ${{ github.sha }} || echo "(diff failed)"
|
||||
|
||||
if git diff --name-only ${{ github.event.before }} ${{ github.sha }} | grep -q '^VERSION$'; then
|
||||
echo "✅ VERSION file was changed"
|
||||
echo "VERSION_CHANGED=true" >> $GITHUB_ENV
|
||||
else
|
||||
echo "ℹ️ VERSION file not changed"
|
||||
echo "VERSION_CHANGED=false" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Set output (always)
|
||||
id: set
|
||||
run: |
|
||||
echo "version_changed=${VERSION_CHANGED:-false}" >> $GITHUB_OUTPUT
|
||||
|
||||
changelog-only:
|
||||
needs: detect-version-change
|
||||
if: github.ref != 'refs/heads/main' || needs.detect-version-change.outputs.version_changed == 'false'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set Git Author
|
||||
run: |
|
||||
git config user.name "$CI_COMMIT_AUTHOR_NAME"
|
||||
git config user.email "$CI_COMMIT_AUTHOR_EMAIL"
|
||||
|
||||
- name: Read CLIFF_VERSION from cliff.toml
|
||||
id: cliff_version
|
||||
run: |
|
||||
echo "version=$(awk -F '=' '/^# CLIFF_VERSION=/ { gsub(/[" ]/, "", $2); print $2 }' cliff.toml)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Restore git-cliff cache
|
||||
id: restore-cliff
|
||||
uses: https://git.0xmax42.io/actions/cache@v1
|
||||
with:
|
||||
key: cargo-cliff-${{ steps.cliff_version.outputs.version }}
|
||||
paths: |
|
||||
/root/.cargo/bin
|
||||
|
||||
- name: Install git-cliff
|
||||
if: steps.restore-cliff.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
cargo install git-cliff --version "${{ steps.cliff_version.outputs.version }}" --features gitea
|
||||
|
||||
- name: Generate unreleased changelog (if file exists or on main)
|
||||
run: |
|
||||
if [[ -f CHANGELOG.md || "${GITHUB_REF##refs/heads/}" == "main" ]]; then
|
||||
echo "Generating CHANGELOG.md..."
|
||||
git-cliff -c cliff.toml -o CHANGELOG.md
|
||||
else
|
||||
echo "CHANGELOG.md does not exist and this is not 'main'. Skipping generation."
|
||||
fi
|
||||
|
||||
- name: Commit updated CHANGELOG
|
||||
run: |
|
||||
git add CHANGELOG.md
|
||||
if git diff --cached --quiet; then
|
||||
echo "No changes to commit"
|
||||
else
|
||||
git commit -m "chore(changelog): update unreleased changelog"
|
||||
git push origin "${GITHUB_REF##refs/heads/}"
|
||||
fi
|
||||
|
||||
release:
|
||||
needs: detect-version-change
|
||||
if: needs.detect-version-change.outputs.version_changed == 'true' && github.ref == 'refs/heads/main'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set Git Author
|
||||
run: |
|
||||
git config user.name "$CI_COMMIT_AUTHOR_NAME"
|
||||
git config user.email "$CI_COMMIT_AUTHOR_EMAIL"
|
||||
|
||||
- name: Read VERSION
|
||||
id: version
|
||||
run: echo "value=$(cat VERSION)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Read CLIFF_VERSION from cliff.toml
|
||||
id: cliff_version
|
||||
run: |
|
||||
echo "version=$(awk -F '=' '/^# CLIFF_VERSION=/ { gsub(/[" ]/, "", $2); print $2 }' cliff.toml)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Restore git-cliff cache
|
||||
id: restore-cliff
|
||||
uses: https://git.0xmax42.io/actions/cache@v1
|
||||
- name: Release
|
||||
uses: https://git.0xmax42.io/actions/auto-changelog-release-action@v1
|
||||
with:
|
||||
key: cargo-cliff-${{ steps.cliff_version.outputs.version }}
|
||||
paths: |
|
||||
/root/.cargo/bin
|
||||
|
||||
- name: Install git-cliff
|
||||
if: steps.restore-cliff.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
cargo install git-cliff --version "${{ steps.cliff_version.outputs.version }}" --features gitea
|
||||
|
||||
- name: Generate changelog for release and tag
|
||||
id: generate-changelog
|
||||
run: |
|
||||
VERSION=${{ steps.version.outputs.value }}
|
||||
git-cliff -c cliff.toml -t "v$VERSION" -o CHANGELOG.md
|
||||
|
||||
BODY=$(mktemp)
|
||||
ESCAPED_VERSION=$(echo "$VERSION" | sed 's/\./\\./g')
|
||||
|
||||
awk -v ver="$ESCAPED_VERSION" '
|
||||
$0 ~ "^## \\[" ver "\\]" {
|
||||
print_flag=1
|
||||
line = $0
|
||||
sub(/^## /, "", line)
|
||||
sub(/\\s*\\(.*\\)/, "", line) # entfernt z. B. "(...)" oder "(*)"
|
||||
print line
|
||||
next
|
||||
}
|
||||
$0 ~ "^## \\[" && $0 !~ "^## \\[" ver "\\]" {
|
||||
print_flag=0
|
||||
}
|
||||
print_flag
|
||||
' CHANGELOG.md > "$BODY"
|
||||
|
||||
echo "changelog_body_path=$BODY" >> $GITHUB_OUTPUT
|
||||
|
||||
|
||||
- name: Commit updated CHANGELOG
|
||||
run: |
|
||||
git add CHANGELOG.md
|
||||
if git diff --cached --quiet; then
|
||||
echo "No changes to commit"
|
||||
else
|
||||
git commit -m "chore(changelog): update changelog for v${{ steps.version.outputs.value }}"
|
||||
git push origin main
|
||||
fi
|
||||
|
||||
- name: Create Git tag (if not exists)
|
||||
run: |
|
||||
VERSION=${{ steps.version.outputs.value }}
|
||||
if git rev-parse "v$VERSION" >/dev/null 2>&1; then
|
||||
echo "Tag v$VERSION already exists, skipping tag creation."
|
||||
else
|
||||
git tag -a "v$VERSION" -F "${{ steps.generate-changelog.outputs.changelog_body_path }}" --cleanup=verbatim
|
||||
git push origin "v$VERSION"
|
||||
fi
|
||||
|
||||
- name: Create Gitea release
|
||||
env:
|
||||
RELEASE_PUBLISH_TOKEN: ${{ secrets.RELEASE_PUBLISH_TOKEN }}
|
||||
run: |
|
||||
VERSION=${{ steps.version.outputs.value }}
|
||||
BODY_FILE="${{ steps.generate-changelog.outputs.changelog_body_path }}"
|
||||
|
||||
OWNER=$(echo "$GITHUB_REPOSITORY" | cut -d/ -f1)
|
||||
REPO=$(echo "$GITHUB_REPOSITORY" | cut -d/ -f2)
|
||||
|
||||
# Token-Auswahl
|
||||
TOKEN="${RELEASE_PUBLISH_TOKEN:-$ACTIONS_RUNTIME_TOKEN}"
|
||||
|
||||
if [[ -z "${RELEASE_PUBLISH_TOKEN:-}" ]]; then
|
||||
echo "::warning title=Limited Release Propagation::"
|
||||
echo "RELEASE_PUBLISH_TOKEN is not set. Using ACTIONS_RUNTIME_TOKEN instead."
|
||||
echo "⚠️ Release events may not trigger other workflows if created with the runtime token."
|
||||
echo
|
||||
fi
|
||||
|
||||
# Prüfe, ob der Release schon existiert
|
||||
if curl -sf "$GITHUB_API_URL/repos/$OWNER/$REPO/releases/tags/v$VERSION" \
|
||||
-H "Authorization: token $TOKEN" > /dev/null; then
|
||||
echo "🔁 Release for tag v$VERSION already exists, skipping."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "🚀 Creating Gitea release for v$VERSION"
|
||||
|
||||
# Release-Beschreibung vorbereiten
|
||||
RELEASE_BODY=$(tail -n +2 "$BODY_FILE" | jq -Rs .)
|
||||
|
||||
curl -X POST "$GITHUB_API_URL/repos/$OWNER/$REPO/releases" \
|
||||
-H "Authorization: token $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d @- <<EOF
|
||||
{
|
||||
"tag_name": "v$VERSION",
|
||||
"target_commitish": "main",
|
||||
"name": "Release v$VERSION",
|
||||
"body": $RELEASE_BODY,
|
||||
"draft": false,
|
||||
"prerelease": false
|
||||
}
|
||||
EOF
|
||||
|
||||
echo "✅ Release for tag $VERSION created successfully."
|
||||
token: ${{ secrets.RELEASE_PUBLISH_TOKEN }}
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
*__pycache__*
|
||||
dist/
|
||||
**/.env
|
||||
30
CHANGELOG.md
30
CHANGELOG.md
@@ -2,7 +2,35 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [unreleased]
|
||||
## [0.4.0](https://git.0xmax42.io/maxp/repoCat/compare/v0.3.0..v0.4.0) - 2025-11-27
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
- *(archive)* Enable multithreaded zstd compression - ([31c3f7d](https://git.0xmax42.io/maxp/repoCat/commit/31c3f7d43880210f8056e4484b441e5f30500184))
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- *(archive)* Skip categories with zero or unset days config - ([39f01b2](https://git.0xmax42.io/maxp/repoCat/commit/39f01b266f9d58509a6fb69fcd0734a85b2fd0ff))
|
||||
|
||||
## [0.3.0](https://git.0xmax42.io/maxp/repoCat/compare/v0.2.0..v0.3.0) - 2025-11-27
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
- *(clean)* Add snapshot support for volatile repo cleanup - ([7f629fd](https://git.0xmax42.io/maxp/repoCat/commit/7f629fd752d7301c6c84d8227918853dd0538ff5))
|
||||
- *(workflows)* Add cleanup step for old dev versions - ([aff319c](https://git.0xmax42.io/maxp/repoCat/commit/aff319cc5f5c8f954367a6a6b89c84ece6b19f4e))
|
||||
- *(scripts)* Add script to clean old PyPI dev versions - ([fc8b885](https://git.0xmax42.io/maxp/repoCat/commit/fc8b885214b1f5b1d9dcff20612a583aca89f6f9))
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
- Fix typo in German documentation comment - ([801c8e2](https://git.0xmax42.io/maxp/repoCat/commit/801c8e24a139f36f714bada00e5308efb848d4f4))
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- *(scripts)* Update file mode to make script executable - ([c2013fc](https://git.0xmax42.io/maxp/repoCat/commit/c2013fc20538a377fc5de45bd384b6ac438fb61c))
|
||||
- *(scripts)* Rename cleanup script for broader usage - ([1163744](https://git.0xmax42.io/maxp/repoCat/commit/1163744af9a3d908d6e7dbb716818bd67ec76a63))
|
||||
- *(gitignore)* Update ignored files for environment and build - ([7067f08](https://git.0xmax42.io/maxp/repoCat/commit/7067f0872bdd8b456d52b3f9dab4834fd0798977))
|
||||
|
||||
## [0.2.0](https://git.0xmax42.io/maxp/repoCat/compare/v0.1.0..v0.2.0) - 2025-05-11
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "repocat"
|
||||
version = "0.2.0"
|
||||
version = "0.4.0"
|
||||
description = ""
|
||||
authors = ["Max P. <Mail@MPassarello.de>"]
|
||||
readme = "README.md"
|
||||
|
||||
@@ -28,7 +28,7 @@ def archive_repo(source: Path, target_dir: Path, dry_run: bool) -> Path:
|
||||
return archive_path
|
||||
|
||||
with open(archive_path, "wb") as f:
|
||||
cctx = zstandard.ZstdCompressor(level=20)
|
||||
cctx = zstandard.ZstdCompressor(level=20, threads=-1)
|
||||
with cctx.stream_writer(f) as compressor:
|
||||
with tarfile.open(fileobj=compressor, mode="w|") as tar:
|
||||
tar.add(source, arcname=source.name)
|
||||
@@ -70,6 +70,9 @@ def run(
|
||||
if category and cat.config.name not in category:
|
||||
continue
|
||||
|
||||
if cat.config.days == 0 or cat.config.days is None:
|
||||
continue
|
||||
|
||||
limit_days = cat.config.days if cat.config.days is not None else older_than
|
||||
|
||||
for repo in cat.repos:
|
||||
|
||||
@@ -2,6 +2,8 @@ from typer import Typer, Option
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
import shutil
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
from repocat.config import load_config
|
||||
from repocat.models.catalog import RepoCatalogState
|
||||
@@ -21,9 +23,18 @@ def clean_volatile(
|
||||
category: list[str] = Option(None, "--category", "-c", help="Nur bestimmte Kategorien reinigen", autocompletion=complete_categories),
|
||||
):
|
||||
"""
|
||||
Löscht **bedingungslos** alle Repositories aus `volatile`-Kategorien (z.\u200bB. `scratches`).
|
||||
Löscht **bedingungslos** alle Repositories aus `volatile`-Kategorien (z.B. `scratches`).
|
||||
"""
|
||||
catalog = RepoCatalogState.from_config(load_config())
|
||||
config = load_config()
|
||||
catalog = RepoCatalogState.from_config(config)
|
||||
|
||||
# Snapshot configuration
|
||||
use_snapshots = config.defaults.snapshots if config.defaults else False
|
||||
snapshot_count = config.defaults.snapshot_count if config.defaults else 5
|
||||
|
||||
snapshot_root = config.base_dir / ".snapshots"
|
||||
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
||||
current_snapshot_dir = snapshot_root / timestamp
|
||||
|
||||
table = Table(title="Clean – Volatile (Zerstörung)")
|
||||
table.add_column("Kategorie")
|
||||
@@ -33,6 +44,7 @@ def clean_volatile(
|
||||
|
||||
deleted = 0
|
||||
skipped = 0
|
||||
snapshotted_categories = set()
|
||||
|
||||
for cat in catalog.categories:
|
||||
if not cat.config.volatile:
|
||||
@@ -40,6 +52,48 @@ def clean_volatile(
|
||||
if category and cat.config.name not in category:
|
||||
continue
|
||||
|
||||
# If snapshots are enabled, we handle the whole category at once
|
||||
if use_snapshots:
|
||||
if not cat.path.exists():
|
||||
continue
|
||||
|
||||
if dry_run:
|
||||
action = f"[yellow]Würde verschieben nach .snapshots/{timestamp}[/]"
|
||||
else:
|
||||
try:
|
||||
# Create snapshot dir only if we actually have something to move
|
||||
if not current_snapshot_dir.exists():
|
||||
current_snapshot_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
target_dir = current_snapshot_dir / cat.config.subdir
|
||||
# Ensure parent of target exists (if subdir is nested)
|
||||
target_dir.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Move the whole category folder
|
||||
# Note: shutil.move(src, dst) where dst does not exist moves src to dst.
|
||||
# If dst exists, it moves src inside dst.
|
||||
# We want to rename cat.path to target_dir.
|
||||
shutil.move(str(cat.path), str(target_dir))
|
||||
|
||||
# Recreate the empty category folder
|
||||
cat.path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
action = f"[green]Verschoben nach .snapshots/{timestamp}[/]"
|
||||
snapshotted_categories.add(cat.config.name)
|
||||
except Exception as e:
|
||||
action = f"[red]Fehler beim Verschieben: {e}[/]"
|
||||
|
||||
# Add rows for all repos in this category to show what happened to them
|
||||
if cat.repos:
|
||||
for repo in cat.repos:
|
||||
table.add_row(cat.config.name, repo.name, f"{repo.size_mb:.1f}", action)
|
||||
deleted += 1 # Count as "processed/removed from volatile"
|
||||
else:
|
||||
# If no repos but folder existed (and maybe had other files), show a generic row
|
||||
table.add_row(cat.config.name, "(Alle Dateien)", "-", action)
|
||||
|
||||
else:
|
||||
# Old behavior: Delete individual repos
|
||||
for repo in cat.repos:
|
||||
size_mb = repo.size_mb
|
||||
|
||||
@@ -56,7 +110,20 @@ def clean_volatile(
|
||||
table.add_row(cat.config.name, repo.name, f"{size_mb:.1f}", action)
|
||||
|
||||
console.print(table)
|
||||
console.print(f"\n[green]✓ Volatile-Clean abgeschlossen[/] – {deleted} gelöscht, {skipped} übersprungen")
|
||||
|
||||
# Prune old snapshots
|
||||
if use_snapshots and not dry_run and snapshot_root.exists():
|
||||
try:
|
||||
snapshots = sorted([p for p in snapshot_root.iterdir() if p.is_dir()], key=lambda p: p.name)
|
||||
if len(snapshots) > snapshot_count:
|
||||
to_delete = snapshots[:-snapshot_count]
|
||||
for snap in to_delete:
|
||||
shutil.rmtree(snap)
|
||||
console.print(f"[dim]Alte Snapshots bereinigt: {len(to_delete)} gelöscht[/]")
|
||||
except Exception as e:
|
||||
console.print(f"[red]Fehler beim Bereinigen der Snapshots: {e}[/]")
|
||||
|
||||
console.print(f"\n[green]✓ Volatile-Clean abgeschlossen[/] – {deleted} Repositories verarbeitet")
|
||||
|
||||
|
||||
@app.command()
|
||||
|
||||
@@ -13,6 +13,8 @@ class RepoCategory(BaseModel):
|
||||
class RepoCatDefaults(BaseModel):
|
||||
dry_run: bool = True
|
||||
auto_create_directories: bool = True
|
||||
snapshots: bool = False
|
||||
snapshot_count: int = 5
|
||||
|
||||
class RepoCatConfig(BaseModel):
|
||||
base_dir: Path = Field(default=Path("~/Repositories").expanduser())
|
||||
|
||||
Reference in New Issue
Block a user