Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Copilot Instructions for MediaCurator

## Commit Message Convention

This project uses **Conventional Commits** for automatic versioning and changelog generation.
Every commit message MUST follow this format:

```
<type>(<scope>): <description>

[optional body]

[optional footer(s)]
```

### Types

| Type | Purpose | Version Impact |
|------|---------|----------------|
| `feat` | New feature or capability | **Minor** bump (0.X.0) |
| `fix` | Bug fix | **Patch** bump (0.0.X) |
| `docs` | Documentation only | Patch bump |
| `style` | Formatting, whitespace, no code change | Patch bump |
| `refactor` | Code restructuring, no behavior change | Patch bump |
| `perf` | Performance improvement | Patch bump |
| `test` | Adding or updating tests | Patch bump |
| `chore` | Build, tooling, dependencies | Patch bump |
| `ci` | CI/CD pipeline changes | Patch bump |

### Breaking Changes → Major Bump

A breaking change triggers a **Major** version bump (X.0.0). Mark it with either:

- An `!` after the type/scope: `feat!: remove legacy API`
- A `BREAKING CHANGE:` footer in the commit body:
```
refactor(api): change auth endpoints

BREAKING CHANGE: /auth/login now requires JSON body instead of form data
```

### Scopes

Use scopes to indicate the affected area. Common scopes for this project:

- `api` – Backend API routes/endpoints
- `ui` – Frontend components/pages
- `auth` – Authentication/security
- `db` – Database/models/migrations
- `docker` – Docker/deployment
- `sync` – Media sync services (Radarr, Sonarr, Emby)
- `rules` – Cleanup rules engine
- `notifications` – Notification system
- `scheduler` – Background jobs/scheduler

### Examples

```
feat(api): add library statistics endpoint
fix(ui): correct pagination on media list
refactor(sync): simplify Sonarr client error handling
docs: update README with Docker Compose examples
feat!: redesign rule evaluation engine
chore(deps): update FastAPI to 0.115
ci: add ARM64 Docker build
perf(db): add index on media.last_played
```

### Rules

- Type and description are **required**
- Scope is optional but encouraged
- Description must be lowercase, imperative mood ("add" not "added" or "adds")
- No period at the end of the description
- Body and footer are optional
- Use `!` or `BREAKING CHANGE:` only for genuinely incompatible changes

## Language

- Commit messages in **English**
- Code comments in **English**

## Tech Stack

- **Backend**: Python 3.12, FastAPI, SQLAlchemy, SQLite/PostgreSQL
- **Frontend**: React, TypeScript, Vite, Tailwind CSS
- **Deployment**: Docker, GitHub Actions
131 changes: 101 additions & 30 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,26 +41,99 @@ jobs:
env:
REF: ${{ github.ref }}
run: |
# Get commit count for consistent versioning across branches
COMMIT_COUNT=$(git rev-list --count HEAD)

if [[ "$REF" == refs/tags/vdev.* ]]; then
# Dev tag (e.g., vdev.0.0.103 -> dev.0.0.103)
VERSION=${GITHUB_REF#refs/tags/v}
IS_PRERELEASE=true
elif [[ "$REF" == refs/tags/v* ]]; then
# Stable tag (e.g., v1.2.3 -> 1.2.3)
if [[ "$REF" == refs/tags/v* ]]; then
# Manual tag push - use the tag version directly
VERSION=${GITHUB_REF#refs/tags/v}
IS_PRERELEASE=false
elif [[ "$REF" == refs/heads/main ]]; then
# Main branch - stable release with same count as dev
VERSION="0.0.${COMMIT_COUNT}"
IS_PRERELEASE=false
if [[ "$VERSION" == *-dev* ]] || [[ "$VERSION" == dev.* ]]; then
IS_PRERELEASE=true
else
IS_PRERELEASE=false
fi
else
# Develop branch - prerelease with dev prefix
VERSION="dev.0.0.${COMMIT_COUNT}"
IS_PRERELEASE=true
# Auto-version: find highest version across ALL tags (dev + stable)
# Extract numeric version from all tags: v0.0.236, vdev.0.0.235, v0.1.0-dev, etc.
HIGHEST_TAG=""
HIGHEST_MAJOR=0
HIGHEST_MINOR=0
HIGHEST_PATCH=0

for tag in $(git tag -l 'v*'); do
# Strip tag prefix: v0.0.236 -> 0.0.236, vdev.0.0.235 -> 0.0.235
ver="$tag"
ver="${ver#v}" # remove leading v
ver="${ver#dev.}" # remove dev. prefix (old format)
ver="${ver%%-dev*}" # remove -dev* suffix (new format)

M=$(echo "$ver" | cut -d. -f1)
m=$(echo "$ver" | cut -d. -f2)
P=$(echo "$ver" | cut -d. -f3)

# Skip if not numeric
[[ "$M" =~ ^[0-9]+$ ]] || continue
[[ "$m" =~ ^[0-9]+$ ]] || continue
[[ "$P" =~ ^[0-9]+$ ]] || continue

# Compare: is this tag's version higher than current highest?
if (( M > HIGHEST_MAJOR )) || \
(( M == HIGHEST_MAJOR && m > HIGHEST_MINOR )) || \
(( M == HIGHEST_MAJOR && m == HIGHEST_MINOR && P > HIGHEST_PATCH )); then
HIGHEST_MAJOR=$M
HIGHEST_MINOR=$m
HIGHEST_PATCH=$P
HIGHEST_TAG="$tag"
fi
done

echo "Highest existing version: ${HIGHEST_MAJOR}.${HIGHEST_MINOR}.${HIGHEST_PATCH} (tag: ${HIGHEST_TAG:-none})"

# Analyze commits since last tag for conventional commit bump type
BUMP="patch"
if [ -n "$HIGHEST_TAG" ]; then
COMMITS=$(git log "${HIGHEST_TAG}..HEAD" --pretty=format:"%s%n%b" 2>/dev/null || echo "")
else
COMMITS=$(git log --pretty=format:"%s%n%b" 2>/dev/null || echo "")
fi

# Check for breaking changes -> major bump
if echo "$COMMITS" | grep -qiE '(^[a-z]+!:|BREAKING CHANGE)'; then
BUMP="major"
# Check for features -> minor bump
elif echo "$COMMITS" | grep -qiE '^feat(\(|:)'; then
BUMP="minor"
fi

echo "Bump type: $BUMP"

# Calculate next version
case "$BUMP" in
major)
NEXT_MAJOR=$((HIGHEST_MAJOR + 1))
NEXT_MINOR=0
NEXT_PATCH=0
;;
minor)
NEXT_MAJOR=$HIGHEST_MAJOR
NEXT_MINOR=$((HIGHEST_MINOR + 1))
NEXT_PATCH=0
;;
patch)
NEXT_MAJOR=$HIGHEST_MAJOR
NEXT_MINOR=$HIGHEST_MINOR
NEXT_PATCH=$((HIGHEST_PATCH + 1))
;;
esac

VERSION="${NEXT_MAJOR}.${NEXT_MINOR}.${NEXT_PATCH}"

if [[ "$REF" == refs/heads/main ]]; then
IS_PRERELEASE=false
else
# Develop branch: append -dev suffix
VERSION="${VERSION}-dev"
IS_PRERELEASE=true
fi
fi

echo "version=${VERSION}" >> $GITHUB_OUTPUT
echo "is_prerelease=${IS_PRERELEASE}" >> $GITHUB_OUTPUT
echo "Version: ${VERSION}, Prerelease: ${IS_PRERELEASE}"
Expand All @@ -72,9 +145,9 @@ jobs:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
# For tagged releases
type=semver,pattern={{version}},enable=${{ startsWith(github.ref, 'refs/tags/v') && !startsWith(github.ref, 'refs/tags/vdev') }}
type=semver,pattern={{major}}.{{minor}},enable=${{ startsWith(github.ref, 'refs/tags/v') && !startsWith(github.ref, 'refs/tags/vdev') }}
type=semver,pattern={{major}},enable=${{ startsWith(github.ref, 'refs/tags/v') && !startsWith(github.ref, 'refs/tags/vdev') }}
type=semver,pattern={{version}},enable=${{ startsWith(github.ref, 'refs/tags/v') && !contains(github.ref, 'dev') }}
type=semver,pattern={{major}}.{{minor}},enable=${{ startsWith(github.ref, 'refs/tags/v') && !contains(github.ref, 'dev') }}
type=semver,pattern={{major}},enable=${{ startsWith(github.ref, 'refs/tags/v') && !contains(github.ref, 'dev') }}
# For main branch
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
type=raw,value=stable,enable=${{ github.ref == 'refs/heads/main' }}
Expand Down Expand Up @@ -153,17 +226,15 @@ jobs:
CURRENT_VERSION="${{ needs.prepare.outputs.version }}"
BRANCH="$REF_NAME"

# Find the last tag based on branch
# For develop: look for vdev.* tags only
# For main/tags: look for v* tags (excluding vdev.*)
# Find the last tag for changelog comparison
if [[ "$BRANCH" == "develop" ]]; then
# Get all dev tags, sort by version number, take the last one
LAST_TAG=$(git tag -l 'vdev.*' --sort=-version:refname | head -n1 || echo "")
echo "Looking for dev tags only"
# Get last dev tag (old vdev.* or new v*-dev format), sorted by creation date
LAST_TAG=$(git tag -l --sort=-creatordate | grep -E '(^vdev\.|.*-dev$)' | head -n1 || echo "")
echo "Looking for dev tags"
else
# Get stable tags (v* but not vdev.*), sort by version, take the last one
LAST_TAG=$(git tag -l 'v*' --sort=-version:refname | grep -v '^vdev\.' | head -n1 || echo "")
echo "Looking for stable tags only"
# Get last stable tag (exclude all dev tags)
LAST_TAG=$(git tag -l 'v*' --sort=-version:refname | grep -vE '(^vdev\.|.*-dev$)' | head -n1 || echo "")
echo "Looking for stable tags"
fi

echo "Branch: $BRANCH"
Expand Down Expand Up @@ -293,7 +364,7 @@ jobs:
tag_name: ${{ startsWith(github.ref, 'refs/tags/') && github.ref_name || format('v{0}', needs.prepare.outputs.version) }}
# CRITICAL: Create tag on the current commit, not the default branch
target_commitish: ${{ github.sha }}
name: ${{ github.ref_name == 'develop' && format('Development Build {0}', needs.prepare.outputs.version) || format('Release {0}', needs.prepare.outputs.version) }}
name: ${{ needs.prepare.outputs.is_prerelease == 'true' && format('Development Build {0}', needs.prepare.outputs.version) || format('Release {0}', needs.prepare.outputs.version) }}
body_path: release_notes.md
draft: false
prerelease: ${{ needs.prepare.outputs.is_prerelease }}
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ venv.bak/

# IDE
.idea/
.vscode/
.vscode/*
!.vscode/settings.json
*.swp
*.swo
*~
Expand Down
7 changes: 7 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"github.copilot.chat.commitMessageGeneration.instructions": [
{
"text": "Use Conventional Commits format: <type>(<scope>): <description>. Types: feat (minor bump), fix (patch bump), docs, style, refactor, perf, test, chore, ci. Use ! after type for breaking changes (major bump). Scopes: api, ui, auth, db, docker, sync, rules, notifications, scheduler. Description must be lowercase, imperative mood, no period at end. English only."
}
]
}
Loading