Add dev nightly builds, ruff linting, Docker layer caching, and GHCR cleanup

This commit is contained in:
JohnBaumb 2026-04-20 14:16:57 -07:00
parent 6314827e91
commit 5edd72b6cf
7 changed files with 188 additions and 5 deletions

View file

@ -2,6 +2,9 @@ name: Compile the app and run tests
on: on:
push: push:
branches-ignore:
- main
- dev
jobs: jobs:
sanity-check: sanity-check:
@ -21,6 +24,9 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: python -m pip install --upgrade pip && python -m pip install -r requirements-dev.txt run: python -m pip install --upgrade pip && python -m pip install -r requirements-dev.txt
- name: Lint with ruff
run: python -m ruff check --output-format=github .
- name: Build app - name: Build app
run: python -m compileall api core database services scripts web_server.py wsgi.py beatport_unified_scraper.py run: python -m compileall api core database services scripts web_server.py wsgi.py beatport_unified_scraper.py

View file

@ -0,0 +1,24 @@
name: Cleanup old dev images
on:
schedule:
# Weekly on Sunday at 6 AM UTC
- cron: '0 6 * * 0'
workflow_dispatch:
jobs:
cleanup:
runs-on: ubuntu-latest
permissions:
packages: write
steps:
- name: Delete old dev image tags
uses: actions/delete-package-versions@v5
with:
package-name: soulsync
package-type: container
min-versions-to-keep: 10
delete-only-pre-release-versions: false
# Only prune tags matching the dev nightly pattern (keep :dev rolling tag)
ignore-versions: '^(dev|latest|\\d+\\.\\d+)$'

108
.github/workflows/dev-nightly.yml vendored Normal file
View file

@ -0,0 +1,108 @@
name: Dev Nightly Build
on:
# Nightly at 4:00 AM UTC — only if dev branch has new commits
schedule:
- cron: '0 4 * * *'
# Also build on every push to dev for immediate feedback
push:
branches:
- dev
# Manual trigger for testing
workflow_dispatch:
jobs:
nightly:
runs-on: ubuntu-latest
# Skip scheduled runs if dev branch has no new commits in the last 24h
# (pushes and manual triggers always run)
permissions:
contents: read
packages: write
steps:
- name: Checkout dev branch
uses: actions/checkout@v6
with:
ref: dev
- name: Check for recent commits (scheduled only)
if: github.event_name == 'schedule'
id: recent
run: |
LAST_COMMIT=$(git log -1 --format=%ct)
NOW=$(date +%s)
DIFF=$(( NOW - LAST_COMMIT ))
if [ "$DIFF" -gt 86400 ]; then
echo "No commits in the last 24h, skipping build"
echo "skip=true" >> "$GITHUB_OUTPUT"
else
echo "skip=false" >> "$GITHUB_OUTPUT"
fi
- name: Set up Python
if: steps.recent.outputs.skip != 'true'
uses: actions/setup-python@v6
with:
python-version: "3.11"
cache: pip
cache-dependency-path: requirements-dev.txt
- name: Install dependencies and run tests
if: steps.recent.outputs.skip != 'true'
env:
PYTHONPATH: ${{ github.workspace }}
run: |
python -m pip install --upgrade pip
python -m pip install -r requirements-dev.txt
python -m compileall api core database services scripts web_server.py wsgi.py beatport_unified_scraper.py
python -m pytest
- name: Generate version tag
if: steps.recent.outputs.skip != 'true'
id: version
run: |
# Read base version from web_server.py
BASE=$(python -c "
import re
with open('web_server.py') as f:
m = re.search(r'_SOULSYNC_BASE_VERSION\s*=\s*\"(.+?)\"', f.read())
print(m.group(1) if m else 'dev')
")
SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-7)
DATE=$(date -u +%Y%m%d)
TAG="${BASE}-dev.${DATE}.${SHORT_SHA}"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "Image tag: $TAG"
- name: Set up QEMU
if: steps.recent.outputs.skip != 'true'
uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx
if: steps.recent.outputs.skip != 'true'
uses: docker/setup-buildx-action@v4
- name: Log in to GHCR
if: steps.recent.outputs.skip != 'true'
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push to GHCR
if: steps.recent.outputs.skip != 'true'
uses: docker/build-push-action@v7
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
pull: true
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
COMMIT_SHA=${{ github.sha }}
tags: |
ghcr.io/${{ github.repository_owner }}/soulsync:dev
ghcr.io/${{ github.repository_owner }}/soulsync:${{ steps.version.outputs.tag }}

View file

@ -1,6 +1,11 @@
name: Build and Push Docker Image name: Build and Push Docker Image
on: on:
# Auto-build :latest on every push to main
push:
branches:
- main
# Manual trigger for tagged releases (e.g. 2.33)
workflow_dispatch: workflow_dispatch:
inputs: inputs:
version_tag: version_tag:
@ -34,16 +39,17 @@ jobs:
context: . context: .
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
push: true push: true
no-cache: true
pull: true pull: true
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: | build-args: |
COMMIT_SHA=${{ github.sha }} COMMIT_SHA=${{ github.sha }}
tags: | tags: |
boulderbadgedad/soulsync:latest boulderbadgedad/soulsync:latest
boulderbadgedad/soulsync:${{ inputs.version_tag }} ${{ inputs.version_tag && format('boulderbadgedad/soulsync:{0}', inputs.version_tag) || '' }}
- name: Announce release to Discord - name: Announce release to Discord
if: success() if: success() && inputs.version_tag
env: env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_ANNOUNCEMENTS_WEBHOOK }} DISCORD_WEBHOOK: ${{ secrets.DISCORD_ANNOUNCEMENTS_WEBHOOK }}
run: | run: |

18
pyproject.toml Normal file
View file

@ -0,0 +1,18 @@
[tool.ruff]
target-version = "py311"
line-length = 160
[tool.ruff.lint]
# Start lenient — catch real bugs, not style nits
# E: pycodestyle errors, F: pyflakes, UP: pyupgrade, B: bugbear
select = ["F", "E9", "W6", "B"]
# Ignore rules that will flag too much in an existing codebase
ignore = [
"F401", # unused imports (too noisy initially)
"F841", # unused variables
"E501", # line length (handled by line-length setting)
]
[tool.ruff.lint.per-file-ignores]
# Tests can use assert, magic values, etc.
"tests/**" = ["B", "F"]

View file

@ -2,6 +2,7 @@
# Runtime web dependencies + test runner # Runtime web dependencies + test runner
-r requirements.txt -r requirements.txt
ruff
# Test runner # Test runner
pytest>=9.0.0 pytest>=9.0.0

View file

@ -37,7 +37,24 @@ _log_dir = Path(_log_path).parent
logger = setup_logging(_log_level, _log_path) logger = setup_logging(_log_level, _log_path)
# App version — single source of truth for backup metadata, version-info endpoint, etc. # App version — single source of truth for backup metadata, version-info endpoint, etc.
SOULSYNC_VERSION = "2.35" _SOULSYNC_BASE_VERSION = "2.35"
def _build_version_string():
"""Append short commit hash to version when available (e.g. 2.35+abc1234)."""
sha = os.environ.get('SOULSYNC_COMMIT_SHA', '').strip()
if not sha:
try:
import subprocess
result = subprocess.run(['git', 'rev-parse', 'HEAD'], capture_output=True, text=True, cwd=os.path.dirname(__file__) or '.')
if result.returncode == 0:
sha = result.stdout.strip()
except Exception:
pass
if sha:
return f"{_SOULSYNC_BASE_VERSION}+{sha[:7]}"
return _SOULSYNC_BASE_VERSION
SOULSYNC_VERSION = _build_version_string()
# Dedicated source reuse logger — writes alongside app.log in the configured log directory # Dedicated source reuse logger — writes alongside app.log in the configured log directory
import logging as _logging import logging as _logging
@ -25925,7 +25942,10 @@ def restore_backup_endpoint(filename):
pass pass
version_warning = None version_warning = None
if backup_version and backup_version != SOULSYNC_VERSION: # Compare base versions only (strip +commit suffix) to avoid false mismatches
_backup_base = backup_version.split('+')[0] if backup_version else None
_current_base = SOULSYNC_VERSION.split('+')[0]
if _backup_base and _backup_base != _current_base:
# Allow restore but warn — the caller must pass force=true to confirm # Allow restore but warn — the caller must pass force=true to confirm
force = request.json.get('force', False) if request.is_json else False force = request.json.get('force', False) if request.is_json else False
if not force: if not force: