# Plugin Update Workflow

**Last Updated:** January 10, 2026  
**Version:** 1.7.0  
**Status:** Production  
**Scope:** How to release plugin updates and what happens automatically vs manually

---

## 🎯 Quick Start: Simplified Release Process

The plugin release process has been **simplified** to require only 3 fields:

1. **Version** (e.g., 1.3.3)
2. **Changelog** (what's new)
3. **Status** (draft → released)

All other fields are either:
- ✅ Auto-filled from previous version
- ✅ Auto-generated on release (file path, size, checksum)
- ✅ Auto-calculated (version code)

**Release in 3 clicks:**
```
Django Admin → Add Plugin Version → Enter 3 fields → Save → Change status to 'released'
```

---

## Recent Release History (v1.7.0)

### WordPress Plugin Progression

| Version | Date | Type | Key Changes |
|---------|------|------|-------------|
| 1.5.1 | Jan 10, 2026 | Minor | Admin UI consolidation: 6→3 pages (Dashboard, Settings, Logs), header alignment fixes |
| 1.5.0 | Jan 10, 2026 | Minor | Authentication simplification: Site.wp_api_key as single source of truth |
| 1.3.3 | Jan 10, 2026 | Patch | Template design: Square image grid fixes, landscape image positioning, direct border-radius/shadow on images without captions |
| 1.3.2 | Jan 9, 2026 | Patch | Template fixes in app and plugin, image layout improvements |
| 1.3.1 | Jan 9, 2026 | Patch | WordPress plugin versioning updates |
| 1.3.0 | Jan 8, 2026 | Minor | Initial distribution system release, automated updates |

### Distribution System Status

- ✅ Automated ZIP generation: Operational
- ✅ Checksum calculation: MD5 + SHA256
- ✅ WordPress auto-update: Active
- ✅ Health checks: Implemented
- ✅ Installation tracking: Complete

---

## Table of Contents

1. [Update Workflow Overview](#1-update-workflow-overview)
2. [What Happens Automatically](#2-what-happens-automatically)
3. [What Requires Manual Action](#3-what-requires-manual-action)
4. [Step-by-Step: Releasing a New Version](#4-step-by-step-releasing-a-new-version)
5. [Post-Update Verification Checklist](#5-post-update-verification-checklist)
6. [Version Numbering](#6-version-numbering)
7. [Rollback Procedure](#7-rollback-procedure)
8. [Emergency Updates](#8-emergency-updates)
9. [Recent Updates & Lessons Learned](#9-recent-updates--lessons-learned)

---

## 1. Update Workflow Overview

### The Big Picture

```
┌─────────────────────────────────────────────────────────────────────────┐
│                     Plugin Update Lifecycle                              │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│  ┌──────────────┐                                                       │
│  │ 1. DEVELOP   │  Make changes to plugin source code                   │
│  │    ↓         │  Location: /plugins/wordpress/source/igny8-wp-bridge/ │
│  └──────────────┘                                                       │
│                                                                          │
│  ┌──────────────┐                                                       │
│  │ 2. VERSION   │  Update version in PHP header + constant              │
│  │    ↓         │  File: igny8-bridge.php                               │
│  └──────────────┘                                                       │
│                                                                          │
│  ┌──────────────┐                                                       │
│  │ 3. RELEASE   │  Create PluginVersion in Django admin                 │
│  │    ↓         │  Set status = "released" or "update_ready"            │
│  └──────────────┘                                                       │
│                                                                          │
│  ┌──────────────┐                                                       │
│  │ 4. AUTO-BUILD│  ✅ AUTOMATIC: ZIP created, checksums calculated      │
│  │    ↓         │  Signal handler builds package on status change       │
│  └──────────────┘                                                       │
│                                                                          │
│  ┌──────────────┐                                                       │
│  │ 5. VERIFY    │  Test download, check contents, verify endpoints      │
│  │    ↓         │  See verification checklist below                     │
│  └──────────────┘                                                       │
│                                                                          │
│  ┌──────────────┐                                                       │
│  │ 6. AVAILABLE │  WordPress sites can now see and install update       │
│  │              │  Users update via WP admin or auto-update             │
│  └──────────────┘                                                       │
│                                                                          │
└─────────────────────────────────────────────────────────────────────────┘
```

---

## 2. What Happens Automatically

When you change a `PluginVersion` status to `released` or `update_ready`, the following happens **automatically** via Django signals:

### ✅ Automatic Actions

| Action | Details |
|--------|---------|
| **ZIP Creation** | Source files packaged into distribution ZIP |
| **Version Update** | Version number updated in PHP files inside ZIP |
| **File Cleanup** | Tests, .git, __pycache__, .bak files removed from ZIP |
| **Checksum Calculation** | SHA256 hash generated |
| **File Size Recording** | Byte count stored in database |
| **File Path Update** | `file_path` field populated |
| **Released Date** | `released_at` timestamp set |
| **Symlink Update** | `*-latest.zip` symlink updated |

### How It Works (Signal Handler)

```python
# backend/igny8_core/plugins/signals.py

@receiver(pre_save, sender=PluginVersion)
def auto_build_plugin_on_release(sender, instance, **kwargs):
    """
    Triggered when PluginVersion.status changes to:
    - 'released'
    - 'update_ready'
    
    Actions:
    1. Calls create_plugin_zip() utility
    2. Updates instance.file_path
    3. Updates instance.file_size
    4. Updates instance.checksum
    5. Sets instance.released_at
    """
```

### What WordPress Sites See

Once released, the `check-update` API returns:

```json
{
    "update_available": true,
    "latest_version": "1.2.0",
    "download_url": "https://api.igny8.com/api/plugins/igny8-wp-bridge/download/",
    "changelog": "Your changelog here"
}
```

WordPress will show "Update Available" in:
- Plugins page
- Dashboard → Updates
- Admin bar (if enabled)

---

## 3. What Requires Manual Action

### ❌ Manual Steps Required

| Action | When | How |
|--------|------|-----|
| **Update Source Version** | Before release | Edit `igny8-bridge.php` header |
| **Create PluginVersion Record** | Each release | Django admin: just enter version, changelog, status |
| **Verify After Release** | After status change | Run verification checklist |
| **Mark Old Version Deprecated** | After successful release | Change old version status |

**Note:** The form is simplified - most fields auto-fill from previous version or are auto-generated.

### Source Version Update Locations

When releasing a new version, update these in the source:

**File:** `/plugins/wordpress/source/igny8-wp-bridge/igny8-bridge.php`

```php
/**
 * Plugin Name: IGNY8 WordPress Bridge
 * Version: 1.2.0                          ← UPDATE THIS
 */

define('IGNY8_BRIDGE_VERSION', '1.2.0');   ← AND THIS
```

**Note:** The ZIP build process also updates these, but it's good practice to keep source in sync.

---

## 4. Step-by-Step: Releasing a New Version

### The Simplified Process

```
┌─────────────────────────────────────────────────────────────────────────┐
│              Simplified Version Release (3 Required Fields)              │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│  Django Admin → Add Plugin Version                                      │
│                                                                          │
│  ┌────────────────────────────────────────────────────────────────┐    │
│  │  Plugin: [IGNY8 WordPress Bridge ▼]       ← Select plugin      │    │
│  │  Version: [1.2.0]                          ← New version        │    │
│  │  Status: [draft ▼]                         ← Start as draft     │    │
│  │  Changelog: [Bug fixes and improvements]   ← What's new         │    │
│  │                                                                  │    │
│  │  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━  │    │
│  │  Everything below is AUTO-FILLED or AUTO-GENERATED:             │    │
│  │                                                                  │    │
│  │  Min API Version: 1.0      (from previous version)              │    │
│  │  Min PHP Version: 7.4      (from previous version)              │    │
│  │  File Path: (empty)        → Auto-generated on release          │    │
│  │  File Size: (empty)        → Auto-calculated on release         │    │
│  │  Checksum: (empty)         → Auto-calculated on release         │    │
│  │  Version Code: (empty)     → Auto-calculated from version       │    │
│  └────────────────────────────────────────────────────────────────┘    │
│                                                                          │
│  Click [Save] → Status changes to 'released' → ZIP builds automatically │
│                                                                          │
└─────────────────────────────────────────────────────────────────────────┘
```

### Step 1: Make Code Changes

```bash
cd /data/app/igny8/plugins/wordpress/source/igny8-wp-bridge/

# Make your changes to PHP files
# Test locally if possible
```

### Step 2: Update Version Number

Edit `igny8-bridge.php`:

```php
/**
 * Plugin Name: IGNY8 WordPress Bridge
 * Version: 1.2.0                          ← New version
 */

define('IGNY8_BRIDGE_VERSION', '1.2.0');   ← Match here
```

### Step 3: Create PluginVersion in Django Admin

1. Go to: `https://api.igny8.com/backend/`
2. Navigate to: **Plugin Distribution** → **Plugin Versions**
3. Click **Add Plugin Version**
4. Fill in **ONLY** these required fields:
   - **Plugin:** IGNY8 WordPress Bridge
   - **Version:** 1.2.0
   - **Status:** draft (initially)
   - **Changelog:** Describe changes
5. Click **Save**

**Note:** All other fields are auto-filled:
- `min_api_version`, `min_platform_version`, `min_php_version` → Copied from previous version
- `file_path`, `file_size`, `checksum` → Auto-generated when you release
- `version_code` → Auto-calculated from version number

### Step 4: Release the Version

**Option A: Release via Status Change**

1. Edit the PluginVersion you just created
2. Change **Status** to `released`
3. Click **Save**

**Option B: Release via Bulk Action**

1. Select the version(s) in the list
2. Choose **Actions** → **✅ Release selected versions**
3. Click **Go**

**What happens:** Signal triggers auto-build → ZIP created → database updated with file info

**Note:** You can also use the action **📢 Mark as update ready** to immediately notify WordPress sites.

### Step 5: Verify Release

Run the [verification checklist](#5-post-update-verification-checklist) below.

### Step 6: Deprecate Old Version (Optional)

1. Find the previous version in Django admin
2. Change **Status** to `deprecated`
3. Save

---

## 5. Post-Update Verification Checklist

### Quick Verification Script

Run this single command to verify everything:

```bash
#!/bin/bash
# Complete Plugin Release Verification Script
# Usage: Save as verify-plugin.sh and run: bash verify-plugin.sh 1.1.2

VERSION=${1:-"latest"}
PLUGIN_SLUG="igny8-wp-bridge"

echo "=========================================="
echo "Plugin Release Verification: v${VERSION}"
echo "=========================================="
echo ""

# 1. Check ZIP file exists
echo "1. Checking ZIP file..."
if [ "$VERSION" = "latest" ]; then
    ls -lh /data/app/igny8/plugins/wordpress/dist/${PLUGIN_SLUG}-latest.zip 2>/dev/null && echo "   ✓ Latest ZIP exists" || echo "   ✗ Latest ZIP not found"
else
    ls -lh /data/app/igny8/plugins/wordpress/dist/${PLUGIN_SLUG}-v${VERSION}.zip 2>/dev/null && echo "   ✓ ZIP v${VERSION} exists" || echo "   ✗ ZIP v${VERSION} not found"
fi
echo ""

# 2. Check symlink
echo "2. Checking symlink..."
ls -la /data/app/igny8/plugins/wordpress/dist/${PLUGIN_SLUG}-latest.zip | grep -q "^l" && echo "   ✓ Symlink valid" || echo "   ✗ Symlink missing"
echo ""

# 3. Test download endpoint
echo "3. Testing download endpoint..."
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" https://api.igny8.com/api/plugins/${PLUGIN_SLUG}/download/)
FILE_SIZE=$(curl -s -o /dev/null -w "%{size_download}" https://api.igny8.com/api/plugins/${PLUGIN_SLUG}/download/)
if [ "$HTTP_CODE" = "200" ]; then
    echo "   ✓ Download works: ${HTTP_CODE} - ${FILE_SIZE} bytes ($(( FILE_SIZE / 1024 )) KB)"
else
    echo "   ✗ Download failed: HTTP ${HTTP_CODE}"
fi
echo ""

# 4. Test check-update endpoint
echo "4. Testing check-update endpoint..."
UPDATE_RESPONSE=$(curl -s "https://api.igny8.com/api/plugins/${PLUGIN_SLUG}/check-update/?current_version=1.0.0")
LATEST_VERSION=$(echo "$UPDATE_RESPONSE" | python3 -c "import sys, json; print(json.load(sys.stdin).get('latest_version', 'N/A'))" 2>/dev/null)
if [ -n "$LATEST_VERSION" ] && [ "$LATEST_VERSION" != "N/A" ]; then
    echo "   ✓ Check-update works: Latest version = $LATEST_VERSION"
else
    echo "   ✗ Check-update failed"
fi
echo ""

# 5. Test info endpoint
echo "5. Testing info endpoint..."
INFO_RESPONSE=$(curl -s "https://api.igny8.com/api/plugins/${PLUGIN_SLUG}/info/")
PLUGIN_NAME=$(echo "$INFO_RESPONSE" | python3 -c "import sys, json; print(json.load(sys.stdin).get('name', 'N/A'))" 2>/dev/null)
if [ -n "$PLUGIN_NAME" ] && [ "$PLUGIN_NAME" != "N/A" ]; then
    echo "   ✓ Info endpoint works: $PLUGIN_NAME"
else
    echo "   ✗ Info endpoint failed"
fi
echo ""

# 6. Verify version in ZIP
echo "6. Verifying version in ZIP..."
if [ "$VERSION" != "latest" ]; then
    ZIP_VERSION=$(unzip -p /data/app/igny8/plugins/wordpress/dist/${PLUGIN_SLUG}-v${VERSION}.zip ${PLUGIN_SLUG}/igny8-bridge.php 2>/dev/null | grep -E "Version:" | head -1 | awk '{print $3}')
    if [ "$ZIP_VERSION" = "$VERSION" ]; then
        echo "   ✓ ZIP version matches: $ZIP_VERSION"
    else
        echo "   ✗ ZIP version mismatch: expected $VERSION, got $ZIP_VERSION"
    fi
else
    echo "   - Skipped (use specific version to check)"
fi
echo ""

# 7. Verify API URL
echo "7. Verifying API URL in ZIP..."
API_COUNT=$(unzip -p /data/app/igny8/plugins/wordpress/dist/${PLUGIN_SLUG}-latest.zip ${PLUGIN_SLUG}/igny8-bridge.php 2>/dev/null | grep -c "api.igny8.com")
if [ "$API_COUNT" -gt 0 ]; then
    echo "   ✓ API URL correct: api.igny8.com found (${API_COUNT} occurrences)"
else
    echo "   ✗ API URL incorrect: api.igny8.com not found"
fi
echo ""

# 8. Check database
echo "8. Checking database..."
docker exec igny8_backend python manage.py shell -c "
from igny8_core.plugins.models import Plugin, PluginVersion
try:
    p = Plugin.objects.get(slug='${PLUGIN_SLUG}')
    v = p.get_latest_version()
    if v:
        print(f'   ✓ Latest version: {v.version}')
        print(f'   ✓ Status: {v.status}')
        print(f'   ✓ File: {v.file_path}')
        print(f'   ✓ Size: {v.file_size:,} bytes ({v.file_size/1024:.1f} KB)')
        print(f'   ✓ Checksum: {v.checksum[:32]}...')
    else:
        print('   ✗ No released version found')
except Exception as e:
    print(f'   ✗ Error: {e}')
" 2>/dev/null | grep -E "✓|✗"
echo ""

echo "=========================================="
echo "Verification Complete"
echo "=========================================="
```

**Quick run (copy-paste):**

```bash
# Verify latest version
curl -s "https://api.igny8.com/api/plugins/igny8-wp-bridge/info/" | python3 -m json.tool && \
curl -s "https://api.igny8.com/api/plugins/igny8-wp-bridge/check-update/?current_version=1.0.0" | python3 -m json.tool && \
ls -lh /data/app/igny8/plugins/wordpress/dist/igny8-wp-bridge-latest.zip && \
echo "✓ All endpoints working"
```

### Database Verification

```bash
# Check database has correct values
docker exec igny8_backend python manage.py shell -c "
from igny8_core.plugins.models import Plugin, PluginVersion
p = Plugin.objects.get(slug='igny8-wp-bridge')
v = p.get_latest_version()
print(f'Version: {v.version}')
print(f'Status: {v.status}')
print(f'File path: {v.file_path}')
print(f'File size: {v.file_size}')
print(f'Checksum: {v.checksum[:32]}...')
print(f'Released at: {v.released_at}')
"
```

### Expected Results

| Check | Expected |
|-------|----------|
| Download returns | 200 status, ~150-200KB |
| check-update shows | `"latest_version": "1.2.0"` |
| info shows | `"version": "1.2.0"` |
| ZIP version header | `Version: 1.2.0` |
| API URL | `api.igny8.com` (NOT app.igny8.com) |

### WordPress Verification (If Possible)

1. Go to a test WordPress site with plugin installed
2. Navigate to **Dashboard** → **Updates** → **Check Again**
3. Verify "IGNY8 WordPress Bridge" shows update available
4. Click "View version details" → verify changelog appears
5. Click "Update Now" → verify update completes successfully

---

## 6. Version Numbering

### Semantic Versioning

Format: `MAJOR.MINOR.PATCH` (e.g., 1.2.3)

| Part | When to Increment |
|------|-------------------|
| **MAJOR** | Breaking changes, incompatible API changes |
| **MINOR** | New features, backwards compatible |
| **PATCH** | Bug fixes, minor improvements |

### Version Code Calculation

Used for numeric comparison in database:

```
1.0.0  → 10000
1.0.1  → 10001
1.2.0  → 10200
1.2.3  → 10203
2.0.0  → 20000
```

Formula: `(MAJOR * 10000) + (MINOR * 100) + PATCH`

---

## 7. Rollback Procedure

### If Update Causes Issues

**Option 1: Quick Rollback via Database**

```bash
# Make old version the "latest" by changing status
docker exec igny8_backend python manage.py shell -c "
from igny8_core.plugins.models import PluginVersion

# Demote new version
new = PluginVersion.objects.get(plugin__slug='igny8-wp-bridge', version='1.2.0')
new.status = 'deprecated'
new.save()

# Promote old version back
old = PluginVersion.objects.get(plugin__slug='igny8-wp-bridge', version='1.1.1')
old.status = 'released'
old.save()

print('Rollback complete')
"
```

**Option 2: Keep Old ZIP Files**

Old ZIP files are preserved in `dist/`. To serve an old version:

```bash
# Update symlink to point to old version
cd /data/app/igny8/plugins/wordpress/dist/
ln -sf igny8-wp-bridge-v1.1.1.zip igny8-wp-bridge-latest.zip
```

### Retention Policy

Keep at least 3 previous version ZIPs for emergency rollback:
- Current release
- Previous release
- One before that

---

## 8. Emergency Updates

### For Critical Security Fixes

1. **Set `force_update = True`** on the new PluginVersion
2. This flag signals WordPress sites that update is mandatory
3. Sites with auto-updates enabled will update immediately

### Emergency Release Checklist

```bash
# 1. Make the fix in source
cd /data/app/igny8/plugins/wordpress/source/igny8-wp-bridge/
# ... make changes ...

# 2. Update version (use PATCH increment)
# Edit igny8-bridge.php

# 3. Create version in Django admin with:
#    - Status: update_ready
#    - Force Update: ✓ (checked)
#    - Changelog: "SECURITY: [description]"

# 4. Verify immediately
curl https://api.igny8.com/api/plugins/igny8-wp-bridge/check-update/?current_version=1.0.0

# Response should include:
# "force_update": true
```

---

## 9. Recent Updates & Lessons Learned

### v1.3.3 Release (Jan 10, 2026)

**Changes:**
- Fixed square image grid layout (side-by-side display)
- Fixed landscape image positioning in sections 4+
- Removed card wrapper for images without captions
- Applied border-radius/shadow directly to images
- Landscape images now appear after first paragraph

**Process:**
- Source code updated in plugin templates and CSS
- Version bumped in `igny8-bridge.php` (header + constant)
- Django admin: Created v1.3.3 with changelog
- Status changed to "released" → ZIP auto-generated
- Verified download endpoint and file contents
- Template changes tested on staging WordPress site

**Lessons:**
- CSS changes require thorough cross-browser testing
- Image layout fixes need responsive design verification
- Template changes should be tested with multiple content types

### v1.3.2 Release (Jan 9, 2026)

**Changes:**
- Template rendering improvements
- Image layout enhancements
- Content section fixes

**Process:**
- Standard release workflow
- Auto-build successful
- No rollback needed

### v1.3.0 Release (Jan 8, 2026)

**Changes:**
- Initial distribution system implementation
- WordPress auto-update mechanism
- Base template system

**Process:**
- Major release with new infrastructure
- Extensive testing required
- First use of automated packaging

### Distribution System Milestones

**v1.7.0 Infrastructure:**
- ✅ Complete plugin distribution system
- ✅ Multi-platform support architecture
- ✅ Automated versioning and packaging
- ✅ Security features (checksums, signed URLs)
- ✅ Monitoring and analytics

**Best Practices Established:**
1. Always test download endpoint after release
2. Verify ZIP contents match source
3. Check version number in extracted files
4. Test update notification in WordPress
5. Monitor download analytics

**Common Issues Resolved:**
- ZIP generation timing → Now synchronous in signals
- Checksum mismatches → Auto-calculated reliably
- Version comparison → Semantic versioning logic fixed
- File size tracking → Automatic and accurate

---

## Quick Reference Card

### Release New Version (Simplified)

```
1. Edit source code in /plugins/wordpress/source/igny8-wp-bridge/
2. Update version in igny8-bridge.php (header + constant)
3. Django Admin → Add Plugin Version:
   - Plugin: IGNY8 WordPress Bridge
   - Version: 1.3.3 (or next version)
   - Changelog: Describe changes
   - Status: draft
   - (All other fields auto-fill)
4. Change status to "released" (or use bulk action) → Auto-build triggers
5. Run verification checklist
6. Optionally: Mark old version as deprecated
```

**Admin Bulk Actions:**
- **✅ Release selected versions** - Builds ZIP and marks as released
- **📢 Mark as update ready** - Notifies WordPress sites
- **🗑️ Mark as deprecated** - Deprecates old versions

### Verification Commands

```bash
# Test all endpoints
curl -I https://api.igny8.com/api/plugins/igny8-wp-bridge/download/
curl https://api.igny8.com/api/plugins/igny8-wp-bridge/check-update/?current_version=1.0.0
curl https://api.igny8.com/api/plugins/igny8-wp-bridge/info/

# Check ZIP contents
unzip -l /data/app/igny8/plugins/wordpress/dist/igny8-wp-bridge-v*.zip | head -20
```

### Emergency Rollback

```bash
# Swap versions in database
docker exec igny8_backend python manage.py shell -c "
from igny8_core.plugins.models import PluginVersion
PluginVersion.objects.filter(version='NEW').update(status='deprecated')
PluginVersion.objects.filter(version='OLD').update(status='released')
"
```

---

## Related Documentation

- [WORDPRESS-INTEGRATION.md](WORDPRESS-INTEGRATION.md) - Full integration guide
- [docs/plans/PLUGIN-DISTRIBUTION-SYSTEM.md](/docs/plans/PLUGIN-DISTRIBUTION-SYSTEM.md) - Original system design
