plugin distribution system

This commit is contained in:
IGNY8 VPS (Salman)
2026-01-09 21:38:14 +00:00
parent cf8181d1f9
commit 80f1709a2e
22 changed files with 2804 additions and 35 deletions

View File

@@ -0,0 +1 @@
# Plugin management commands

View File

@@ -0,0 +1 @@
# Plugin management commands

View File

@@ -0,0 +1,164 @@
"""
Django management command to build a plugin and register its version.
Usage:
python manage.py build_plugin --plugin=igny8-wp-bridge --version=1.0.1 [--changelog="Bug fixes"] [--release]
"""
import subprocess
import hashlib
import os
from django.core.management.base import BaseCommand, CommandError
from django.utils import timezone
from igny8_core.plugins.models import Plugin, PluginVersion
from igny8_core.plugins.utils import (
create_plugin_zip,
get_plugin_file_path,
calculate_checksum,
parse_version_to_code,
)
class Command(BaseCommand):
help = 'Build and register a new plugin version'
def add_arguments(self, parser):
parser.add_argument(
'--plugin',
type=str,
required=True,
help='Plugin slug (e.g., igny8-wp-bridge)'
)
parser.add_argument(
'--plugin-version',
type=str,
required=True,
dest='plugin_version',
help='Version string (e.g., 1.0.1)'
)
parser.add_argument(
'--changelog',
type=str,
default='',
help='Changelog for this version'
)
parser.add_argument(
'--release',
action='store_true',
help='Immediately release this version'
)
parser.add_argument(
'--force',
action='store_true',
help='Overwrite existing version'
)
parser.add_argument(
'--no-build',
action='store_true',
help='Skip building, only register existing ZIP'
)
def handle(self, *args, **options):
plugin_slug = options['plugin']
version = options['plugin_version']
changelog = options['changelog']
release = options['release']
force = options['force']
no_build = options['no_build']
# Validate version format
import re
if not re.match(r'^\d+\.\d+\.\d+$', version):
raise CommandError('Version must follow semantic versioning (e.g., 1.0.0)')
# Get or create plugin
try:
plugin = Plugin.objects.get(slug=plugin_slug)
self.stdout.write(f"Found plugin: {plugin.name}")
except Plugin.DoesNotExist:
# Create plugin if it doesn't exist
if plugin_slug == 'igny8-wp-bridge':
plugin = Plugin.objects.create(
name='IGNY8 WordPress Bridge',
slug='igny8-wp-bridge',
platform='wordpress',
description='Lightweight bridge plugin that connects WordPress to IGNY8 API for one-way content publishing.',
homepage_url='https://igny8.com',
is_active=True,
)
self.stdout.write(self.style.SUCCESS(f"Created plugin: {plugin.name}"))
else:
raise CommandError(f"Plugin not found: {plugin_slug}")
# Check if version already exists
existing = PluginVersion.objects.filter(plugin=plugin, version=version).first()
if existing and not force:
raise CommandError(
f"Version {version} already exists. Use --force to overwrite."
)
# Build plugin ZIP
if not no_build:
self.stdout.write(f"Building {plugin.name} v{version}...")
file_path, checksum, file_size = create_plugin_zip(
platform=plugin.platform,
plugin_slug=plugin.slug,
version=version,
update_version=True
)
if not file_path:
raise CommandError("Failed to build plugin ZIP")
self.stdout.write(self.style.SUCCESS(f"Built: {file_path}"))
self.stdout.write(f" Size: {file_size:,} bytes")
self.stdout.write(f" Checksum: {checksum}")
else:
# Find existing ZIP
file_path = f"{plugin.slug}-v{version}.zip"
full_path = get_plugin_file_path(plugin.platform, file_path)
if not full_path:
raise CommandError(f"ZIP file not found: {file_path}")
checksum = calculate_checksum(str(full_path))
file_size = full_path.stat().st_size
self.stdout.write(f"Using existing ZIP: {file_path}")
# Calculate version code
version_code = parse_version_to_code(version)
# Create or update version record
if existing:
existing.version_code = version_code
existing.file_path = file_path
existing.file_size = file_size
existing.checksum = checksum
existing.changelog = changelog
if release:
existing.status = 'released'
existing.released_at = timezone.now()
existing.save()
self.stdout.write(self.style.SUCCESS(f"Updated version {version}"))
else:
plugin_version = PluginVersion.objects.create(
plugin=plugin,
version=version,
version_code=version_code,
file_path=file_path,
file_size=file_size,
checksum=checksum,
changelog=changelog,
status='released' if release else 'draft',
released_at=timezone.now() if release else None,
)
self.stdout.write(self.style.SUCCESS(f"Created version {version}"))
if release:
self.stdout.write(self.style.SUCCESS(f"Version {version} is now released!"))
else:
self.stdout.write(
f"Version {version} created as draft. "
f"Use --release to make it available for download."
)

View File

@@ -0,0 +1,99 @@
"""
Django management command to push a plugin update to all installations.
Usage:
python manage.py push_plugin_update --plugin=igny8-wp-bridge --version=1.0.1
"""
from django.core.management.base import BaseCommand, CommandError
from django.utils import timezone
from igny8_core.plugins.models import Plugin, PluginVersion, PluginInstallation
class Command(BaseCommand):
help = 'Push a plugin update to all active installations'
def add_arguments(self, parser):
parser.add_argument(
'--plugin',
type=str,
required=True,
help='Plugin slug (e.g., igny8-wp-bridge)'
)
parser.add_argument(
'--plugin-version',
type=str,
required=True,
dest='plugin_version',
help='Version string (e.g., 1.0.1)'
)
parser.add_argument(
'--force',
action='store_true',
help='Mark as force update (critical security fix)'
)
def handle(self, *args, **options):
plugin_slug = options['plugin']
version_str = options['plugin_version']
force = options['force']
# Get plugin
try:
plugin = Plugin.objects.get(slug=plugin_slug)
except Plugin.DoesNotExist:
raise CommandError(f"Plugin not found: {plugin_slug}")
# Get version
try:
version = PluginVersion.objects.get(plugin=plugin, version=version_str)
except PluginVersion.DoesNotExist:
raise CommandError(f"Version not found: {version_str}")
# Check if released
if version.status not in ['released', 'update_ready']:
raise CommandError(
f"Version {version_str} must be released before pushing update. "
f"Current status: {version.status}"
)
# Mark as update ready
version.status = 'update_ready'
if force:
version.force_update = True
version.save(update_fields=['status', 'force_update'])
# Update all installations
installations = PluginInstallation.objects.filter(
plugin=plugin,
is_active=True
).exclude(current_version=version)
updated_count = installations.update(
pending_update=version,
update_notified_at=timezone.now()
)
self.stdout.write(
self.style.SUCCESS(
f"Pushed {plugin.name} v{version_str} to {updated_count} installations"
)
)
if force:
self.stdout.write(
self.style.WARNING(
"This is marked as a FORCE update (critical security fix)"
)
)
# Show summary
total_installations = PluginInstallation.objects.filter(
plugin=plugin,
is_active=True
).count()
already_updated = total_installations - updated_count
if already_updated > 0:
self.stdout.write(
f" {already_updated} installations already have v{version_str}"
)

View File

@@ -0,0 +1,149 @@
"""
Django management command to register an existing plugin version.
Useful for registering a ZIP file that was built manually or externally.
Usage:
python manage.py register_plugin_version --plugin=igny8-wp-bridge --version=1.0.1 [--changelog="Bug fixes"]
"""
import os
from django.core.management.base import BaseCommand, CommandError
from django.utils import timezone
from igny8_core.plugins.models import Plugin, PluginVersion
from igny8_core.plugins.utils import (
get_plugin_file_path,
calculate_checksum,
parse_version_to_code,
get_dist_path,
)
class Command(BaseCommand):
help = 'Register an existing plugin ZIP file as a version'
def add_arguments(self, parser):
parser.add_argument(
'--plugin',
type=str,
required=True,
help='Plugin slug (e.g., igny8-wp-bridge)'
)
parser.add_argument(
'--plugin-version',
type=str,
required=True,
dest='plugin_version',
help='Version string (e.g., 1.0.1)'
)
parser.add_argument(
'--changelog',
type=str,
default='',
help='Changelog for this version'
)
parser.add_argument(
'--release',
action='store_true',
help='Immediately release this version'
)
parser.add_argument(
'--min-php',
type=str,
default='7.4',
help='Minimum PHP version required'
)
def handle(self, *args, **options):
plugin_slug = options['plugin']
version_str = options['plugin_version']
changelog = options['changelog']
release = options['release']
min_php = options['min_php']
# Validate version format
import re
if not re.match(r'^\d+\.\d+\.\d+$', version_str):
raise CommandError('Version must follow semantic versioning (e.g., 1.0.0)')
# Get plugin
try:
plugin = Plugin.objects.get(slug=plugin_slug)
except Plugin.DoesNotExist:
# Create WordPress plugin if it doesn't exist
if plugin_slug == 'igny8-wp-bridge':
plugin = Plugin.objects.create(
name='IGNY8 WordPress Bridge',
slug='igny8-wp-bridge',
platform='wordpress',
description='Lightweight bridge plugin that connects WordPress to IGNY8 API for one-way content publishing.',
homepage_url='https://igny8.com',
is_active=True,
)
self.stdout.write(self.style.SUCCESS(f"Created plugin: {plugin.name}"))
else:
raise CommandError(f"Plugin not found: {plugin_slug}")
# Check if version already exists
if PluginVersion.objects.filter(plugin=plugin, version=version_str).exists():
raise CommandError(f"Version {version_str} already exists for {plugin.name}")
# Check if ZIP file exists
file_name = f"{plugin.slug}-v{version_str}.zip"
file_path = get_plugin_file_path(plugin.platform, file_name)
if not file_path:
# Also check for latest symlink
dist_path = get_dist_path(plugin.platform)
latest_link = dist_path / f"{plugin.slug}-latest.zip"
if latest_link.exists():
self.stdout.write(
self.style.WARNING(
f"ZIP file not found: {file_name}\n"
f"Found latest symlink. Creating expected filename..."
)
)
# Could copy or rename, but for now just fail
raise CommandError(
f"ZIP file not found: {file_name}\n"
f"Expected at: {dist_path}/{file_name}\n"
f"Build the plugin first with: ./scripts/build-wp-plugin.sh {version_str}"
)
# Calculate checksum and file size
checksum = calculate_checksum(str(file_path))
file_size = file_path.stat().st_size
# Calculate version code
version_code = parse_version_to_code(version_str)
# Create version record
plugin_version = PluginVersion.objects.create(
plugin=plugin,
version=version_str,
version_code=version_code,
file_path=file_name,
file_size=file_size,
checksum=checksum,
changelog=changelog,
min_php_version=min_php,
status='released' if release else 'draft',
released_at=timezone.now() if release else None,
)
self.stdout.write(
self.style.SUCCESS(
f"Registered {plugin.name} v{version_str}\n"
f" File: {file_name}\n"
f" Size: {file_size:,} bytes\n"
f" Checksum: {checksum}\n"
f" Status: {'released' if release else 'draft'}"
)
)
if not release:
self.stdout.write(
f"\nTo release this version, run:\n"
f" python manage.py release_plugin --plugin={plugin_slug} --version={version_str}"
)

View File

@@ -0,0 +1,70 @@
"""
Django management command to release a plugin version.
Usage:
python manage.py release_plugin --plugin=igny8-wp-bridge --version=1.0.1
"""
from django.core.management.base import BaseCommand, CommandError
from django.utils import timezone
from igny8_core.plugins.models import Plugin, PluginVersion
class Command(BaseCommand):
help = 'Release a plugin version (make it available for download)'
def add_arguments(self, parser):
parser.add_argument(
'--plugin',
type=str,
required=True,
help='Plugin slug (e.g., igny8-wp-bridge)'
)
parser.add_argument(
'--plugin-version',
type=str,
required=True,
dest='plugin_version',
help='Version string (e.g., 1.0.1)'
)
def handle(self, *args, **options):
plugin_slug = options['plugin']
version_str = options['plugin_version']
# Get plugin
try:
plugin = Plugin.objects.get(slug=plugin_slug)
except Plugin.DoesNotExist:
raise CommandError(f"Plugin not found: {plugin_slug}")
# Get version
try:
version = PluginVersion.objects.get(plugin=plugin, version=version_str)
except PluginVersion.DoesNotExist:
raise CommandError(f"Version not found: {version_str}")
# Check if already released
if version.status in ['released', 'update_ready']:
self.stdout.write(
self.style.WARNING(f"Version {version_str} is already released")
)
return
# Check if file exists
if not version.file_path:
raise CommandError(
f"No file associated with version {version_str}. "
f"Build the plugin first with: python manage.py build_plugin"
)
# Release
version.status = 'released'
version.released_at = timezone.now()
version.save(update_fields=['status', 'released_at'])
self.stdout.write(
self.style.SUCCESS(
f"Released {plugin.name} v{version_str}\n"
f" Download URL: /api/plugins/{plugin.slug}/download/"
)
)