plugin distribution system
This commit is contained in:
1
backend/igny8_core/plugins/management/__init__.py
Normal file
1
backend/igny8_core/plugins/management/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Plugin management commands
|
||||
@@ -0,0 +1 @@
|
||||
# Plugin management commands
|
||||
164
backend/igny8_core/plugins/management/commands/build_plugin.py
Normal file
164
backend/igny8_core/plugins/management/commands/build_plugin.py
Normal 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."
|
||||
)
|
||||
@@ -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}"
|
||||
)
|
||||
@@ -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}"
|
||||
)
|
||||
@@ -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/"
|
||||
)
|
||||
)
|
||||
Reference in New Issue
Block a user