diff --git a/.claude/skills/prepare-release/SKILL.md b/.claude/skills/prepare-release/SKILL.md new file mode 100644 index 0000000..3c89e76 --- /dev/null +++ b/.claude/skills/prepare-release/SKILL.md @@ -0,0 +1,68 @@ +--- +name: prepare-release +description: Prepare a new release of CleanMyPosts (version bump, release notes, release PR to main). Use this whenever the user mentions releasing, publishing, shipping, cutting a release, bumping the version, or preparing release notes — even if they don't say "release" explicitly. Decides the next SemVer version automatically and proceeds when there are new commits since the last tag. +--- + +# Prepare release + +Prepare a new release of the CleanMyPosts desktop app. You decide the next +version yourself. You **prepare the repository and open the release PR — then +stop.** Reviewing, merging, and publishing are **admin-only** and not part of +your job: + +- You do **not** merge the release PR (or enable auto-merge). The admin reviews + and merges every PR manually. `main` is a protected branch — direct pushes are + blocked, so every change ships through a PR. +- You do **not** trigger `deploy-release.yml`. The GitHub Release (installer + build, `v` git tag, AutoUpdater feed) is a manual `workflow_dispatch` + the admin runs **after** merging the release PR. + +## Workflow + +1. **Check whether there is anything to release** + - Run the helper: `bash .claude/skills/prepare-release/scripts/check-updates.sh` + - Exit code **3** = nothing to release (no new commits since the last tag) → **stop**. + - Exit code **0** = new commits exist → continue. + +2. **Determine current version & last tag** + - Read `` in `src/CleanMyPosts/CleanMyPosts.csproj`. + - Latest tag: `git tag --sort=-v:refname | head -1`. + +3. **Decide the next version (SemVer)** from `git log --oneline ..HEAD`: + - **MAJOR** breaking / behavior-removing changes · **MINOR** new features · + **PATCH** fixes/docs/dependency bumps. + - Must be strictly greater than the current version. State it + a one-line reason. + +4. **Confirm clean tree** — `git status --short`; if dirty, ask how to proceed. + +5. **Branch from up-to-date `main`** — `git checkout main && git pull` then + `git checkout -b release/v`. Release branches always cut from `main`. + +6. **Build & test (green required)** + - `dotnet build CleanMyPosts.slnx -c Release` + - `dotnet test src/Tests/Tests.csproj -c Release --filter "TestCategory!=Long-Running"` + (same filter CI uses; long-running tests are excluded). + +7. **Bump ``** in `src/CleanMyPosts/CleanMyPosts.csproj`. + +8. **Extend docs** — update `README.md` etc. if the changes warrant it. + +9. **Release notes** — copy `.claude/skills/prepare-release/assets/release-notes.md` + to `release-notes/v.md` and fill in the `### What's Changed` bullets + with **user-facing** notes (not raw commit subjects). This file becomes the + GitHub Release body verbatim — `deploy-release.yml` reads it via `body_path`. + +10. **Commit** (no tag) — stage the csproj, docs, and release notes; message + `release: v`. Do **not** create a git tag — `deploy-release.yml` + creates `v` at publish time. + +11. **Push & open PR to main — then stop. Do NOT merge it.** + - `git push -u origin release/v` + - `gh pr create --base main --head release/v --title "release: v" --body ` + - The admin reviews and merges the PR. You never merge it or enable auto-merge. + +12. **Report** the PR link and remind the admin: after **they** merge it, **they** + trigger **Deploy Release** manually (Actions → Deploy Release → Run workflow). + That workflow builds the installer, tags `v`, publishes the GitHub + Release from `release-notes/v.md`, and updates the AutoUpdater feed. + You do not trigger it. diff --git a/.claude/skills/prepare-release/assets/release-notes.md b/.claude/skills/prepare-release/assets/release-notes.md new file mode 100644 index 0000000..4a2ad66 --- /dev/null +++ b/.claude/skills/prepare-release/assets/release-notes.md @@ -0,0 +1,9 @@ +### What's Changed + +* {{ ... user-facing change ... }} + + diff --git a/.claude/skills/prepare-release/scripts/check-updates.sh b/.claude/skills/prepare-release/scripts/check-updates.sh new file mode 100644 index 0000000..15a696b --- /dev/null +++ b/.claude/skills/prepare-release/scripts/check-updates.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# Decides whether there is anything worth releasing. +# +# A release is warranted when there are new commits since the last tag +# (features, fixes, breaking changes). +# +# Exit 0 = something to release, 3 = nothing to release, 1 = error. +set -euo pipefail + +# Run from the repository root so the tag/log lookups are unambiguous. +cd "$(git rev-parse --show-toplevel)" + +last_tag="$(git tag --sort=-v:refname | head -1)" +if [[ -n "$last_tag" ]]; then + commits="$(git log "$last_tag"..HEAD --oneline 2>/dev/null || true)" +else + commits="$(git log --oneline 2>/dev/null || true)" +fi + +if [[ -z "$commits" ]]; then + echo "No new commits since ${last_tag:-the start} — nothing to release." + exit 3 +fi + +echo "Releasable changes since ${last_tag:-the start}:" +echo "$commits" +exit 0 diff --git a/.gitattributes b/.gitattributes index 1ff0c42..ef82812 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,6 +2,7 @@ # Set default behavior to automatically normalize line endings. ############################################################################### * text=auto +*.sh text eol=lf ############################################################################### # Set default behavior for command prompt diff. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8075b5d..73d52f6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,6 +3,8 @@ name: "CI" on: push: branches: [ develop, main ] + pull_request: + branches: [ main ] permissions: contents: read @@ -15,7 +17,7 @@ jobs: runs-on: windows-latest env: - Solution: "src/CleanMyPosts.slnx" + Solution: "CleanMyPosts.slnx" Test_Project: "src/Tests/Tests.csproj" FORCE_COLOR: "true" DOTNET_LOGGING__CONSOLE__COLORBEHAVIOR: Enabled diff --git a/.github/workflows/deploy-release.yml b/.github/workflows/deploy-release.yml index 8196e85..51c88f3 100644 --- a/.github/workflows/deploy-release.yml +++ b/.github/workflows/deploy-release.yml @@ -14,7 +14,7 @@ jobs: runs-on: windows-latest env: - Solution: "src/CleanMyPosts.slnx" + Solution: "CleanMyPosts.slnx" UI_Project: "src/CleanMyPosts/CleanMyPosts.csproj" Test_Project: "src/Tests/Tests.csproj" Installer_Script: "installer/Installer.iss" diff --git a/.gitignore b/.gitignore index 821d1a0..9cdb63a 100644 --- a/.gitignore +++ b/.gitignore @@ -363,7 +363,7 @@ MigrationBackup/ FodyWeavers.xsd # local cmd files -*.cmd +*local.cmd # Installer releases installer/Output diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..178178d --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,56 @@ +# CLAUDE.md + +CleanMyPosts is a **.NET 10 WPF Windows desktop app** that bulk-deletes posts, +reposts, replies, likes and followings on X (Twitter) and comments/likes on +YouTube, by driving an embedded WebView2 browser with injected JavaScript. + +## Layout + +``` +CleanMyPosts.slnx # solution (repo root) +src/CleanMyPosts/CleanMyPosts.csproj # WPF app (WebView2 + MahApps.Metro + CommunityToolkit.Mvvm) + Scripts/ # injected JS (delete-all-*.js) + Services/ ViewModels/ Views/ # MVVM +src/Tests/Tests.csproj # xUnit tests (Xunit.StaFact for UI) +installer/Installer.iss # Inno Setup installer script +release-notes/vX.Y.Z.md # GitHub Release body per version +.github/workflows/ci.yml # build + test on push/PR to main +.github/workflows/deploy-release.yml # manual: installer, tag, GitHub Release, AutoUpdater feed +``` + +## Build & test + +```bash +dotnet build CleanMyPosts.slnx -c Release +dotnet test src/Tests/Tests.csproj -c Release --filter "TestCategory!=Long-Running" +``` + +The filter mirrors CI, which excludes long-running tests. + +## Branching + +`main` is **protected** — direct pushes are blocked (including for admins) and +the `build` status check must pass. Every change ships through a PR against +`main`. Use `feature/` for functionality, `fix/` for bug fixes, and +`release/vX.Y.Z` for release prep. + +## Versioning & release + +- Version lives in `src/CleanMyPosts/CleanMyPosts.csproj` (``). +- Do not bump `` without also adding `release-notes/v.md` — + that file becomes the GitHub Release body (`deploy-release.yml` reads it via + `body_path`). +- **Do not create git tags manually.** `deploy-release.yml` creates the + `v` tag when it publishes. +- The full release-prep flow (decide SemVer, branch, build/test, bump version, + write release notes, open the PR to `main`) is encoded in the + **`prepare-release`** skill at `.claude/skills/prepare-release/`. Run it via + Claude Code (`/prepare-release`) when cutting a release; it prepares the PR + only. Merging the PR and running **Deploy Release** (`workflow_dispatch`) are + manual, admin-only steps. + +## Code style + +Global conventions in `~/.claude/CLAUDE.md` apply: comments only when the *why* +is non-obvious, `var` when the type is evident, expression-bodied members, +`is null` / `is not null`, `_camelCase` private fields, no `#region`. diff --git a/CleanMyPosts.slnx b/CleanMyPosts.slnx new file mode 100644 index 0000000..044b71b --- /dev/null +++ b/CleanMyPosts.slnx @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/README.md b/README.md index 5804a61..9486673 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,11 @@ ![Banner](https://raw.githubusercontent.com/thorstenalpers/CleanMyPosts/main/src/CleanMyPosts/Assets/banner.png) -[![Windows](https://img.shields.io/badge/platform-Windows-blue)](#) -[![License](https://img.shields.io/badge/License-MIT-blue.svg)](./LICENSE.txt) -[![CI Tests](https://github.com/thorstenalpers/CleanMyPosts/actions/workflows/ci.yml/badge.svg)](https://github.com/thorstenalpers/CleanMyPosts/actions/workflows/ci.yml) -[![Donate](https://img.shields.io/badge/donate-PayPal-yellow)](https://www.paypal.com/donate/?hosted_button_id=QYHGE9LA9SNAN) -[![Star this repo](https://img.shields.io/github/stars/thorstenalpers/CleanMyPosts.svg?style=social&label=Star&maxAge=60)](https://github.com/thorstenalpers/CleanMyPosts) +[![CI](https://img.shields.io/github/actions/workflow/status/thorstenalpers/CleanMyPosts/ci.yml?branch=main&style=flat-square&logo=githubactions&logoColor=white&label=CI)](https://github.com/thorstenalpers/CleanMyPosts/actions/workflows/ci.yml) +[![Release](https://img.shields.io/github/v/release/thorstenalpers/CleanMyPosts?style=flat-square&logo=github&label=release)](https://github.com/thorstenalpers/CleanMyPosts/releases/latest) +[![Downloads](https://img.shields.io/github/downloads/thorstenalpers/CleanMyPosts/total?style=flat-square&logo=github&label=downloads)](https://github.com/thorstenalpers/CleanMyPosts/releases) +[![Platform](https://img.shields.io/badge/platform-Windows-0078D6?style=flat-square&logo=windows&logoColor=white)](https://github.com/thorstenalpers/CleanMyPosts/releases) +[![Donate](https://img.shields.io/badge/donate-PayPal-00457C?style=flat-square&logo=paypal&logoColor=white)](https://www.paypal.com/donate/?hosted_button_id=QYHGE9LA9SNAN) +[![Stars](https://img.shields.io/github/stars/thorstenalpers/CleanMyPosts?style=flat-square&logo=github&label=stars)](https://github.com/thorstenalpers/CleanMyPosts) **CleanMyPosts** is a lightweight Windows desktop app that securely deletes all posts, reposts, replies, likes, and followings from your X (formerly Twitter) account, as well as YouTube comments, in bulk using browser automation. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..ddaa277 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,22 @@ +# Security Policy + +## Reporting a vulnerability + +Please report security vulnerabilities **privately** — do not open a public +issue or pull request for them. + +Use GitHub's private reporting flow: +[**Report a vulnerability**](https://github.com/thorstenalpers/CleanMyPosts/security/advisories/new). + +Please include enough detail to reproduce the issue (affected version, a minimal +repro or proof of concept, and the impact). You can expect an initial response +within a few days. Once a fix is available it will be published as a new release. + +## Supported versions + +Only the latest release receives security fixes. + +| Version | Supported | +| ------- | --------- | +| latest | ✅ | +| older | ❌ | diff --git a/build.cmd b/build.cmd new file mode 100644 index 0000000..3bc136f --- /dev/null +++ b/build.cmd @@ -0,0 +1,14 @@ +cd C:\Sources\CleanMyPosts\src\UI + +REM dotnet clean ..\CleanMyPosts.sln +REM dotnet restore ..\CleanMyPosts.sln + +REM dotnet build ..\CleanMyPosts.sln --configuration Release --no-restore + +REM dotnet publish UI.csproj -c Release -r win-x64 --self-contained true + +dotnet clean ..\CleanMyPosts.sln +REM dotnet publish ..\CleanMyPosts.sln -c Release -r win-x64 --self-contained true +dotnet publish ..\CleanMyPosts.sln -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -p:PublishTrimmed=false + +cd C:\Sources\CleanMyPosts\ \ No newline at end of file diff --git a/clean.cmd b/clean.cmd new file mode 100644 index 0000000..549b7ec --- /dev/null +++ b/clean.cmd @@ -0,0 +1,57 @@ +@echo off +setlocal enabledelayedexpansion + +taskkill /im dotnet.exe /f + + +set "rootDir=C:\Sources\CleanMyPosts" +set "vsDir=%rootDir%\.vs" +set "testresultDir=%rootDir%\TestResults" + +rd /s /q "%vsDir%" + +rd /s /q "%testresultDir%" + + +:: Iterate over all subdirectories under %rootDir% +for /d /r "%rootDir%" %%a in (*) do ( + if /i "%%~nxa"=="bin" ( + call :DeleteBinObj "%%a" "bin" + ) else if /i "%%~nxa"=="obj" ( + call :DeleteBinObj "%%a" "obj" + ) +) + +echo Finished. +REM pause +exit /b + +:DeleteBinObj +setlocal +set "currentDir=%~1" +set "skipDelete=" + +:: Check for .git, node_modules, or _archive in the current directory path +for %%b in ("%currentDir%") do ( + for %%c in (".git" "node_modules" "_archive") do ( + echo %%b | findstr /i "%%c" >nul + if not errorlevel 1 ( + set "skipDelete=yes" + goto :skipDeletion + ) + ) +) + +:skipDeletion +if not defined skipDelete ( + if /i "%~2"=="bin" ( + echo Deleting folder: "%currentDir%" + rd /s /q "%currentDir%" + ) else if /i "%~2"=="obj" ( + echo Deleting folder: "%currentDir%" + rd /s /q "%currentDir%" + ) +) + +endlocal + diff --git a/create-licenses.cmd b/create-licenses.cmd new file mode 100644 index 0000000..373a424 --- /dev/null +++ b/create-licenses.cmd @@ -0,0 +1,3 @@ +echo ["Tests.csproj"] > exclude.json +dotnet-project-licenses -i src/CleanMyPosts.sln --projects-filter exclude.json -o --outfile THIRD_PARTY_LICENSES.txt +rm exclude.json diff --git a/src/CleanMyPosts.slnx b/src/CleanMyPosts.slnx deleted file mode 100644 index e249881..0000000 --- a/src/CleanMyPosts.slnx +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - -