From f0d4c36bc3394ad0bf5d9bf950336a5eb46725ff Mon Sep 17 00:00:00 2001 From: Javanaut Date: Sun, 12 Apr 2026 12:12:41 +0200 Subject: [PATCH] Adds release script and bumps 0.2.4 --- README.md | 12 ++ SCRATCHPAD.md | 7 - pyproject.toml | 2 +- requirements/project.md | 2 +- src/ffx/constants.py | 2 +- tools/merge_dev_into_main.sh | 322 +++++++++++++++++++++++++++++++++++ 6 files changed, 337 insertions(+), 10 deletions(-) create mode 100755 tools/merge_dev_into_main.sh diff --git a/README.md b/README.md index 6eed68c..00a1153 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,18 @@ TMDB-backed metadata enrichment requires `TMDB_API_KEY` to be set in the environ ## Version History +### 0.2.4 + +- lightweight CLI commands now stay import-light via lazy runtime loading +- setup/config templating moved to `assets/ffx.json.j2` +- aligned two-step local setup wrappers: `ffx setup` and `ffx configure_workstation` +- combined `ffprobe` payload reuse in `FileProperties` +- configurable crop-detect sampling plus per-process crop result caching +- single-query controller accessors and conditional DB schema bootstrap +- shared screen bootstrap/controller wiring for large detail screens +- configurable default season/episode digit lengths +- digit-aware `rename` and padded `unmux` filename markers + ### 0.2.3 - PyPI packaging diff --git a/SCRATCHPAD.md b/SCRATCHPAD.md index f9931d0..c11bd74 100644 --- a/SCRATCHPAD.md +++ b/SCRATCHPAD.md @@ -9,19 +9,12 @@ - The biggest near-term wins are in startup cost, repeated subprocess work, repeated database query patterns, and general repo hygiene. - This list is intentionally optimization-oriented rather than bug-oriented. Some items below also improve correctness or maintainability, but they were selected because they can reduce runtime cost, operator friction, or iteration overhead. - A first modern integration slice now exists under [`tests/integration/subtrack_mapping`](/home/osgw/.local/src/codex/ffx/tests/integration/subtrack_mapping). Remaining test-suite cleanup is now mostly about migrating and shrinking the legacy harness surface under [`tests/legacy`](/home/osgw/.local/src/codex/ffx/tests/legacy). -- The CLI root now lazy-loads heavy runtime dependencies so lightweight commands such as `version`, `help`, `setup`, `configure_workstation`, and `upgrade` stay import-light. - Shared CLI defaults for container/output tokens now live outside [`src/ffx/ffx_controller.py`](/home/osgw/.local/src/codex/ffx/src/ffx/ffx_controller.py), and a focused unit test locks in the lazy-import contract. -- `FileProperties` now uses one cached `ffprobe -show_format -show_streams -of json` call per source file, and the combined payload was confirmed against the Dragonball asset to satisfy both previous probe call sites fully. -- Crop detection now uses configurable sampling windows plus per-process caching keyed by source file and sampling range, and the `cropdetect` CLI command now calls the real `FileProperties.findCropArguments()` path. -- Database startup now bootstraps schema only when required tables are actually missing, while version enforcement still runs on ordinary DB-backed context creation. - Helper filename and rich-text utilities now use compiled raw regexes plus translate-based filename filtering, with unit coverage for TMDB suffix rewriting and Rich color stripping. - Process resource limiting now has explicit disabled/default states in the CLI and requirements, and combined CPU-plus-niceness wrapping now executes as `cpulimit -- nice -n ... ` instead of a less explicit prefix chain. - FFX logger setup now reuses named handlers, and fallback logger access no longer mutates handlers in ordinary constructors and helpers. - The process wrapper now uses `subprocess.run(...)` with centralized command formatting plus stable timeout and missing-command error mapping. -- Active ORM controllers now use single-query accessors instead of paired `count()` plus `first()` lookups. - Pattern matching now uses cached compiled regexes plus explicit duplicate-match errors, and pattern creation flows no longer persist zero-track patterns. -- The two-step local setup flow now has aligned CLI wrappers for both phases: `ffx setup` for bundle prep and `ffx configure_workstation` for workstation prep, while the shell scripts remain the bootstrap entrypoints before the bundle exists. -- The large detail screens now share one screen-bootstrap helper for context, metadata-filter extraction, and controller wiring, and show-pattern loading now goes through `PatternController` instead of a screen-local session query. ## Focused Snapshot diff --git a/pyproject.toml b/pyproject.toml index 7c00f6d..9ea7e52 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "ffx" description = "FFX recoding and metadata managing tool" -version = "0.2.3" +version = "0.2.4" license = {file = "LICENSE.md"} dependencies = [ "requests", diff --git a/requirements/project.md b/requirements/project.md index 73bbe47..9018043 100644 --- a/requirements/project.md +++ b/requirements/project.md @@ -91,7 +91,7 @@ - Intended for local execution, not server deployment. - Stores default state in `~/.local/etc/ffx.json`, `~/.local/var/ffx/ffx.db`, and `~/.local/var/log/ffx.log`. - Timeline constraints: - - The current implemented scope reflects a compact alpha release stream up to version `0.2.3`. + - The current implemented scope reflects a compact alpha release stream up to version `0.2.4`. - Team capacity assumptions: - Maintained as a small codebase where simple patterns and direct controller logic are preferred over framework-heavy abstractions. - Third-party dependencies: diff --git a/src/ffx/constants.py b/src/ffx/constants.py index eb212ef..ec22587 100644 --- a/src/ffx/constants.py +++ b/src/ffx/constants.py @@ -1,4 +1,4 @@ -VERSION='0.2.3' +VERSION='0.2.4' DATABASE_VERSION = 2 DEFAULT_QUALITY = 32 diff --git a/tools/merge_dev_into_main.sh b/tools/merge_dev_into_main.sh new file mode 100755 index 0000000..8e28250 --- /dev/null +++ b/tools/merge_dev_into_main.sh @@ -0,0 +1,322 @@ +#!/usr/bin/env bash + +set -euo pipefail + +DEV_BRANCH="dev" +MAIN_BRANCH="main" +ORIGIN_REMOTE="origin" +DEFAULT_AGENT_DEVELOPMENT_PATHS=( + "AGENTS.md" + "SCRATCHPAD.md" + "guidance" + "requirements" + "prompts" + "process" +) +AGENT_DEVELOPMENT_PATHS=("${DEFAULT_AGENT_DEVELOPMENT_PATHS[@]}") + +CURRENT_BRANCH="${DEV_BRANCH}" +ASSUME_YES=0 +DRY_RUN=0 +SKIP_TESTS=0 + +usage() { + cat <&2 + exit 1 +} + +cleanup() { + local exit_code="$1" + + trap - EXIT + + if git rev-parse -q --verify MERGE_HEAD >/dev/null 2>&1; then + printf 'Merge is incomplete; aborting merge on %s...\n' "${CURRENT_BRANCH}" >&2 + git merge --abort >/dev/null 2>&1 || true + fi + + if [ "${CURRENT_BRANCH}" != "${DEV_BRANCH}" ]; then + printf 'Switching back to %s...\n' "${DEV_BRANCH}" >&2 + git switch "${DEV_BRANCH}" >/dev/null 2>&1 || true + CURRENT_BRANCH="${DEV_BRANCH}" + fi + + exit "${exit_code}" +} + +load_cleanup_paths() { + if [ -n "${FFX_RELEASE_CLEAN_PATHS:-}" ]; then + IFS=':' read -r -a AGENT_DEVELOPMENT_PATHS <<< "${FFX_RELEASE_CLEAN_PATHS}" + fi + + if [ "${#AGENT_DEVELOPMENT_PATHS[@]}" -eq 0 ]; then + fail "Release cleanup path list is empty." + fi +} + +require_repo_state() { + if ! git rev-parse --show-toplevel >/dev/null 2>&1; then + fail "This helper must be run inside a git repository." + fi + + if ! git show-ref --verify --quiet "refs/heads/${DEV_BRANCH}"; then + fail "Local branch '${DEV_BRANCH}' does not exist." + fi + + if ! git show-ref --verify --quiet "refs/heads/${MAIN_BRANCH}"; then + fail "Local branch '${MAIN_BRANCH}' does not exist." + fi + + if ! git remote get-url "${ORIGIN_REMOTE}" >/dev/null 2>&1; then + fail "Remote '${ORIGIN_REMOTE}' is not configured." + fi +} + +require_dev_checkout() { + CURRENT_BRANCH="$(git rev-parse --abbrev-ref HEAD)" + if [ "${CURRENT_BRANCH}" != "${DEV_BRANCH}" ]; then + fail "Current branch is '${CURRENT_BRANCH}', but '${DEV_BRANCH}' is required." + fi +} + +require_clean_worktree() { + if [ -n "$(git status --porcelain)" ]; then + fail "Local '${DEV_BRANCH}' branch is dirty. Commit, stash, or clean changes first." + fi +} + +fetch_remote_state() { + printf 'Fetching %s branch and tag state...\n' "${ORIGIN_REMOTE}" + git fetch "${ORIGIN_REMOTE}" "${DEV_BRANCH}" "${MAIN_BRANCH}" --tags >/dev/null +} + +require_branch_matches_remote() { + local branch="$1" + local local_sha="" + local remote_sha="" + + if ! git show-ref --verify --quiet "refs/remotes/${ORIGIN_REMOTE}/${branch}"; then + fail "Remote branch '${ORIGIN_REMOTE}/${branch}' does not exist." + fi + + local_sha="$(git rev-parse "refs/heads/${branch}")" + remote_sha="$(git rev-parse "refs/remotes/${ORIGIN_REMOTE}/${branch}")" + + if [ "${local_sha}" != "${remote_sha}" ]; then + fail "Local branch '${branch}' is not up to date with '${ORIGIN_REMOTE}/${branch}'. Pull, rebase, or push first." + fi +} + +resolve_release_version() { + local version_from_pyproject="" + local version_from_constants="" + + version_from_pyproject="$( + sed -n 's/^version = "\(.*\)"$/\1/p' pyproject.toml | head -n 1 + )" + version_from_constants="$( + sed -n "s/^VERSION='\(.*\)'$/\1/p" src/ffx/constants.py | head -n 1 + )" + + if [ -z "${version_from_pyproject}" ]; then + fail "Could not resolve release version from pyproject.toml." + fi + + if [ -z "${version_from_constants}" ]; then + fail "Could not resolve release version from src/ffx/constants.py." + fi + + if [ "${version_from_pyproject}" != "${version_from_constants}" ]; then + fail "Version mismatch: pyproject.toml=${version_from_pyproject}, src/ffx/constants.py=${version_from_constants}." + fi + + printf '%s\n' "${version_from_pyproject}" +} + +require_release_tag_available() { + local release_version="$1" + local release_tag="v${release_version}" + + if git rev-parse -q --verify "refs/tags/${release_tag}" >/dev/null 2>&1; then + fail "Tag '${release_tag}' already exists." + fi + + if git rev-parse -q --verify "refs/tags/${release_version}" >/dev/null 2>&1; then + fail "Bare tag '${release_version}' already exists; refusing to create ambiguous release tags." + fi +} + +run_pre_release_tests() { + if [ "${SKIP_TESTS}" -eq 1 ]; then + printf 'Skipping pre-release tests.\n' + return 0 + fi + + if [ ! -x "./tools/test.sh" ]; then + fail "Missing executable test runner at ./tools/test.sh." + fi + + printf 'Running pre-release tests via ./tools/test.sh...\n' + ./tools/test.sh +} + +print_release_plan() { + local release_version="$1" + local release_tag="v${release_version}" + local release_commit_message="Release ${release_tag}" + + printf 'Dry run only. Planned steps:\n' + printf '1. Ensure current branch is %s and the worktree is clean.\n' "${DEV_BRANCH}" + printf '2. Fetch %s and verify local %s and %s exactly match %s/%s and %s/%s.\n' \ + "${ORIGIN_REMOTE}" \ + "${DEV_BRANCH}" \ + "${MAIN_BRANCH}" \ + "${ORIGIN_REMOTE}" \ + "${DEV_BRANCH}" \ + "${ORIGIN_REMOTE}" \ + "${MAIN_BRANCH}" + if [ "${SKIP_TESTS}" -eq 1 ]; then + printf '3. Skip the pre-release test gate.\n' + else + printf '3. Run ./tools/test.sh as the pre-release test gate.\n' + fi + printf '4. Switch to %s and merge %s with --no-ff --no-commit.\n' "${MAIN_BRANCH}" "${DEV_BRANCH}" + printf '5. Remove release-cleanup paths from %s:\n' "${MAIN_BRANCH}" + local cleanup_path="" + for cleanup_path in "${AGENT_DEVELOPMENT_PATHS[@]}"; do + printf ' - %s\n' "${cleanup_path}" + done + printf '6. Create merge commit: %s\n' "${release_commit_message}" + printf '7. Create annotated tag: %s\n' "${release_tag}" + printf '8. Push %s to %s/%s with --follow-tags.\n' "${MAIN_BRANCH}" "${ORIGIN_REMOTE}" "${MAIN_BRANCH}" + printf '9. Switch back to %s.\n' "${DEV_BRANCH}" +} + +trap 'cleanup $?' EXIT + +while [ "$#" -gt 0 ]; do + case "$1" in + --yes) + ASSUME_YES=1 + ;; + --dry-run) + DRY_RUN=1 + ;; + --skip-tests) + SKIP_TESTS=1 + ;; + --help|-h) + usage + exit 0 + ;; + *) + usage >&2 + fail "Unknown option: $1" + ;; + esac + shift +done + +load_cleanup_paths +require_repo_state +require_dev_checkout +require_clean_worktree +fetch_remote_state +require_branch_matches_remote "${DEV_BRANCH}" +require_branch_matches_remote "${MAIN_BRANCH}" + +RELEASE_VERSION="$(resolve_release_version)" +RELEASE_TAG="v${RELEASE_VERSION}" +RELEASE_COMMIT_MESSAGE="Release ${RELEASE_TAG}" +require_release_tag_available "${RELEASE_VERSION}" + +printf 'This will merge %s into %s, remove agent-development files on %s,\n' "${DEV_BRANCH}" "${MAIN_BRANCH}" "${MAIN_BRANCH}" +printf 'run the pre-release gate%s, create %s, push to %s/%s, and switch back to %s.\n' \ + "$([ "${SKIP_TESTS}" -eq 1 ] && printf ' (skipped)' || printf '')" \ + "${RELEASE_TAG}" \ + "${ORIGIN_REMOTE}" \ + "${MAIN_BRANCH}" \ + "${DEV_BRANCH}" + +if [ "${ASSUME_YES}" -ne 1 ]; then + printf 'Are you sure? [y/N] ' + read -r confirmation + case "${confirmation}" in + y|Y|yes|YES) + ;; + *) + fail "Aborted by user." + ;; + esac +fi + +if [ "${DRY_RUN}" -eq 1 ]; then + print_release_plan "${RELEASE_VERSION}" + exit 0 +fi + +run_pre_release_tests +require_clean_worktree +fetch_remote_state +require_branch_matches_remote "${DEV_BRANCH}" +require_branch_matches_remote "${MAIN_BRANCH}" +require_release_tag_available "${RELEASE_VERSION}" + +git switch "${MAIN_BRANCH}" >/dev/null +CURRENT_BRANCH="${MAIN_BRANCH}" + +printf 'Merging %s into %s...\n' "${DEV_BRANCH}" "${MAIN_BRANCH}" +if ! git merge --no-ff --no-commit "${DEV_BRANCH}"; then + fail "Merge from '${DEV_BRANCH}' into '${MAIN_BRANCH}' failed." +fi + +if ! git rev-parse -q --verify MERGE_HEAD >/dev/null 2>&1; then + fail "'${MAIN_BRANCH}' is already up to date with '${DEV_BRANCH}'. Nothing to merge." +fi + +printf 'Removing agent-development files from %s...\n' "${MAIN_BRANCH}" +git rm -r --ignore-unmatch "${AGENT_DEVELOPMENT_PATHS[@]}" >/dev/null + +if git diff --cached --quiet; then + fail "No staged changes are present after merging '${DEV_BRANCH}' into '${MAIN_BRANCH}'." +fi + +printf 'Creating release merge commit: %s\n' "${RELEASE_COMMIT_MESSAGE}" +git commit -m "${RELEASE_COMMIT_MESSAGE}" + +printf 'Creating annotated tag: %s\n' "${RELEASE_TAG}" +git tag -a "${RELEASE_TAG}" -m "FFX ${RELEASE_VERSION}" + +printf 'Pushing %s and annotated tags to %s...\n' "${MAIN_BRANCH}" "${ORIGIN_REMOTE}" +git push "${ORIGIN_REMOTE}" "${MAIN_BRANCH}" --follow-tags + +printf 'Switching back to %s...\n' "${DEV_BRANCH}" +git switch "${DEV_BRANCH}" >/dev/null +CURRENT_BRANCH="${DEV_BRANCH}" + +printf 'Release merge complete: %s pushed to %s/%s and tagged as %s.\n' \ + "${RELEASE_COMMIT_MESSAGE}" \ + "${ORIGIN_REMOTE}" \ + "${MAIN_BRANCH}" \ + "${RELEASE_TAG}"