# Plugin Release Workflow # # This workflow automates the release process for OpenWebUI plugins. # # Triggers: # - Push to main branch when plugins are modified (auto-release) # - Manual trigger (workflow_dispatch) with custom release notes # - Push of plugin version tags (-v*) # # What it does: # 1. Detects plugin version changes compared to the last release # 2. Generates release notes with updated plugin information # 3. Creates a GitHub Release with plugin files as downloadable assets # 4. Enforces one plugin creation/update per release name: Plugin Release on: # Auto-trigger on push to main when plugins are modified push: branches: - main paths: - 'plugins/**/*.py' - 'plugins/**/README.md' - 'plugins/**/README_CN.md' - 'plugins/**/v*.md' - 'plugins/**/v*_CN.md' - 'docs/plugins/**/*.md' tags: - '*-v*' - 'v*' # Manual trigger with inputs workflow_dispatch: inputs: version: description: 'Release tag (e.g., markdown-normalizer-v1.2.8). Leave empty for auto-generated tag.' required: false type: string release_title: description: 'Release title (optional)' required: false type: string release_notes: description: 'Additional release notes (Markdown)' required: false type: string prerelease: description: 'Mark as pre-release' required: false type: boolean default: false permissions: contents: write jobs: check-changes: runs-on: ubuntu-latest # Skip release if commit message contains [skip release] if: ${{ !contains(github.event.head_commit.message, '[skip release]') }} env: LANG: en_US.UTF-8 LC_ALL: en_US.UTF-8 outputs: has_changes: ${{ steps.detect.outputs.has_changes }} changed_plugins: ${{ steps.detect.outputs.changed_plugins }} changed_plugin_title: ${{ steps.detect.outputs.changed_plugin_title }} changed_plugin_slug: ${{ steps.detect.outputs.changed_plugin_slug }} changed_plugin_version: ${{ steps.detect.outputs.changed_plugin_version }} changed_plugin_count: ${{ steps.detect.outputs.changed_plugin_count }} release_notes: ${{ steps.detect.outputs.release_notes }} has_doc_changes: ${{ steps.detect.outputs.has_doc_changes }} changed_doc_files: ${{ steps.detect.outputs.changed_doc_files }} previous_release_tag: ${{ steps.detect.outputs.previous_release_tag }} compare_ref: ${{ steps.detect.outputs.compare_ref }} steps: - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 0 - name: Configure Git run: | git config --global core.quotepath false git config --global i18n.commitencoding utf-8 git config --global i18n.logoutputencoding utf-8 - name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.11' - name: Detect plugin changes id: detect run: | # Always compare against the most recent previously released version. CURRENT_TAG="" if [[ "${GITHUB_REF}" == refs/tags/* ]]; then CURRENT_TAG="${GITHUB_REF#refs/tags/}" echo "Current tag event detected: $CURRENT_TAG" fi PREVIOUS_RELEASE_TAG=$(git tag --sort=-creatordate | grep -Fxv "$CURRENT_TAG" | head -n1 || true) if [ -n "$PREVIOUS_RELEASE_TAG" ]; then echo "Comparing with previous release tag: $PREVIOUS_RELEASE_TAG" COMPARE_REF="$PREVIOUS_RELEASE_TAG" else COMPARE_REF="$(git rev-list --max-parents=0 HEAD)" echo "No previous release tag found, using repository root commit: $COMPARE_REF" fi echo "previous_release_tag=$PREVIOUS_RELEASE_TAG" >> "$GITHUB_OUTPUT" echo "compare_ref=$COMPARE_REF" >> "$GITHUB_OUTPUT" # Get current plugin versions python scripts/extract_plugin_versions.py --json --output current_versions.json # Get previous plugin versions by checking out old plugins OLD_WORKTREE=$(mktemp -d) if git worktree add "$OLD_WORKTREE" ${COMPARE_REF} 2>/dev/null; then if [ -d "$OLD_WORKTREE/plugins" ]; then python scripts/extract_plugin_versions.py --plugins-dir "$OLD_WORKTREE/plugins" --json --output old_versions.json else echo "[]" > old_versions.json fi git worktree remove "$OLD_WORKTREE" 2>/dev/null || true else echo "Failed to create worktree, using empty version list" echo "[]" > old_versions.json fi rm -rf "$OLD_WORKTREE" 2>/dev/null || true # Compare versions and generate release notes python scripts/extract_plugin_versions.py --compare old_versions.json --ignore-removed --output changes.md python scripts/extract_plugin_versions.py --compare old_versions.json --json --output changes.json echo "=== Version Changes ===" cat changes.md # Detect documentation/release-note changes that should be reflected in release notes git diff --name-only "$COMPARE_REF"..HEAD -- \ 'plugins/**/README.md' \ 'plugins/**/README_CN.md' \ 'plugins/**/v*.md' \ 'plugins/**/v*_CN.md' \ 'docs/plugins/**/*.md' > changed_docs.txt || true if [ -s changed_docs.txt ]; then echo "has_doc_changes=true" >> $GITHUB_OUTPUT echo "changed_doc_files<> $GITHUB_OUTPUT cat changed_docs.txt >> $GITHUB_OUTPUT echo "" >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT else echo "has_doc_changes=false" >> $GITHUB_OUTPUT echo "changed_doc_files=" >> $GITHUB_OUTPUT fi # Check if there are any changes if grep -q "No changes detected" changes.md; then # Only trigger release if there are actual version changes, not just doc changes echo "has_changes=false" >> $GITHUB_OUTPUT echo "changed_plugins=" >> $GITHUB_OUTPUT echo "changed_plugin_title=" >> $GITHUB_OUTPUT echo "changed_plugin_slug=" >> $GITHUB_OUTPUT echo "changed_plugin_version=" >> $GITHUB_OUTPUT echo "changed_plugin_count=0" >> $GITHUB_OUTPUT else echo "has_changes=true" >> $GITHUB_OUTPUT # Extract changed plugin metadata and enforce a single-plugin release. python3 <<'PY' import json import sys from pathlib import Path data = json.load(open('changes.json', 'r', encoding='utf-8')) def get_plugin_meta(plugin): manifest = plugin.get('data', {}).get('function', {}).get('meta', {}).get('manifest', {}) title = (manifest.get('title') or plugin.get('title') or '').strip() version = (manifest.get('version') or plugin.get('version') or '').strip() file_path = (plugin.get('file_path') or '').strip() slug = Path(file_path).parent.name.replace('_', '-').strip() if file_path else '' return { 'title': title, 'slug': slug, 'version': version, 'file_path': file_path, } plugins = [] seen_keys = set() for plugin in data.get('added', []): meta = get_plugin_meta(plugin) key = meta['file_path'] or meta['title'] if key and key not in seen_keys: plugins.append(meta) seen_keys.add(key) for update in data.get('updated', []): meta = get_plugin_meta(update.get('current', {})) key = meta['file_path'] or meta['title'] if key and key not in seen_keys: plugins.append(meta) seen_keys.add(key) Path('changed_files.txt').write_text( '\n'.join(meta['file_path'] for meta in plugins if meta['file_path']), encoding='utf-8', ) Path('changed_plugin_count.txt').write_text(str(len(plugins)), encoding='utf-8') if len(plugins) > 1: print('Error: release workflow only supports one plugin creation/update per release.', file=sys.stderr) for meta in plugins: print( f"- {meta['title'] or 'Unknown'} v{meta['version'] or '?'} ({meta['file_path'] or 'unknown path'})", file=sys.stderr, ) sys.exit(1) selected = plugins[0] if plugins else {'title': '', 'slug': '', 'version': ''} Path('changed_plugin_title.txt').write_text(selected['title'], encoding='utf-8') Path('changed_plugin_slug.txt').write_text(selected['slug'], encoding='utf-8') Path('changed_plugin_version.txt').write_text(selected['version'], encoding='utf-8') PY echo "changed_plugins<> $GITHUB_OUTPUT cat changed_files.txt >> $GITHUB_OUTPUT echo "" >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT echo "changed_plugin_title=$(cat changed_plugin_title.txt)" >> $GITHUB_OUTPUT echo "changed_plugin_slug=$(cat changed_plugin_slug.txt)" >> $GITHUB_OUTPUT echo "changed_plugin_version=$(cat changed_plugin_version.txt)" >> $GITHUB_OUTPUT echo "changed_plugin_count=$(cat changed_plugin_count.txt)" >> $GITHUB_OUTPUT fi # Store release notes { echo 'release_notes<> $GITHUB_OUTPUT release: needs: check-changes if: needs.check-changes.outputs.has_changes == 'true' || github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/tags/') runs-on: ubuntu-latest env: LANG: en_US.UTF-8 LC_ALL: en_US.UTF-8 steps: - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 0 - name: Configure Git run: | git config --global core.quotepath false git config --global i18n.commitencoding utf-8 git config --global i18n.logoutputencoding utf-8 - name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.11' - name: Determine version id: version env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} CHANGED_PLUGIN_SLUG: ${{ needs.check-changes.outputs.changed_plugin_slug }} CHANGED_PLUGIN_VERSION: ${{ needs.check-changes.outputs.changed_plugin_version }} run: | if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ -n "${{ github.event.inputs.version }}" ]; then VERSION="${{ github.event.inputs.version }}" elif [[ "${{ github.ref }}" == refs/tags/* ]]; then VERSION="${GITHUB_REF#refs/tags/}" elif [ -n "$CHANGED_PLUGIN_SLUG" ] && [ -n "$CHANGED_PLUGIN_VERSION" ]; then VERSION="${CHANGED_PLUGIN_SLUG}-v${CHANGED_PLUGIN_VERSION}" else echo "Error: failed to determine plugin-scoped release tag." >&2 exit 1 fi echo "version=$VERSION" >> $GITHUB_OUTPUT echo "Release tag: $VERSION" - name: Build release metadata id: meta env: VERSION: ${{ steps.version.outputs.version }} INPUT_TITLE: ${{ github.event.inputs.release_title }} CHANGED_PLUGIN_TITLE: ${{ needs.check-changes.outputs.changed_plugin_title }} CHANGED_PLUGIN_VERSION: ${{ needs.check-changes.outputs.changed_plugin_version }} run: | if [ -n "$INPUT_TITLE" ]; then RELEASE_NAME="$INPUT_TITLE" elif [ -n "$CHANGED_PLUGIN_TITLE" ] && [ -n "$CHANGED_PLUGIN_VERSION" ]; then RELEASE_NAME="$CHANGED_PLUGIN_TITLE v$CHANGED_PLUGIN_VERSION" else RELEASE_NAME="$VERSION" fi echo "release_name=$RELEASE_NAME" >> "$GITHUB_OUTPUT" echo "Release name: $RELEASE_NAME" - name: Extract plugin versions id: plugins run: | python scripts/extract_plugin_versions.py --json --output plugin_versions.json - name: Collect plugin files for release id: collect_files run: | mkdir -p release_plugins CHANGED_PLUGINS="${{ needs.check-changes.outputs.changed_plugins }}" if [ -n "$CHANGED_PLUGINS" ]; then echo "Collecting changed plugin files..." echo "$CHANGED_PLUGINS" | while read -r file; do if [ -n "$file" ] && [ -f "$file" ]; then dir=$(dirname "$file") mkdir -p "release_plugins/$dir" cp "$file" "release_plugins/$file" echo "Added: $file" fi done else echo "No changed plugins detected. Skipping file collection." fi # Create a zip file with error handling # cd release_plugins # Zip step removed as per user request echo "=== Collected Files ===" find release_plugins -name "*.py" -type f | head -20 - name: Update plugin icon URLs run: | echo "Updating icon_url in plugins to use absolute GitHub URLs..." # Base URL for raw content using the release tag REPO_URL="https://raw.githubusercontent.com/${{ github.repository }}/${{ steps.version.outputs.version }}" find release_plugins -name "*.py" | while read -r file; do # $file is like release_plugins/plugins/actions/infographic/infographic.py # Remove release_plugins/ prefix to get the path in the repo src_file="${file#release_plugins/}" src_dir=$(dirname "$src_file") base_name=$(basename "$src_file" .py) # Check if a corresponding png exists in the source repository png_file="${src_dir}/${base_name}.png" if [ -f "$png_file" ]; then echo "Found icon for $src_file: $png_file" TARGET_ICON_URL="${REPO_URL}/${png_file}" # Use python for safe replacement python3 -c " import sys import re file_path = '$file' icon_url = '$TARGET_ICON_URL' try: with open(file_path, 'r', encoding='utf-8') as f: content = f.read() # Replace icon_url: ... with new url # Matches 'icon_url: ...' and replaces it new_content = re.sub(r'^icon_url:.*$', f'icon_url: {icon_url}', content, flags=re.MULTILINE) with open(file_path, 'w', encoding='utf-8') as f: f.write(new_content) print(f'Successfully updated icon_url in {file_path}') except Exception as e: print(f'Error updating {file_path}: {e}', file=sys.stderr) sys.exit(1) " fi done - name: Debug Filenames run: | python3 -c "import sys; print(f'Filesystem encoding: {sys.getfilesystemencoding()}')" ls -R release_plugins - name: Upload Debug Artifacts uses: actions/upload-artifact@v4 with: name: debug-plugins path: release_plugins/ - name: Get commit messages id: commits if: github.event_name == 'push' env: PREVIOUS_RELEASE_TAG: ${{ needs.check-changes.outputs.previous_release_tag }} COMPARE_REF: ${{ needs.check-changes.outputs.compare_ref }} run: | if [ -n "$PREVIOUS_RELEASE_TAG" ]; then COMMITS=$(git log ${PREVIOUS_RELEASE_TAG}..HEAD --pretty=format:"- **%s**%n%b" --no-merges -- plugins/ | sed '/^$/d' | head -40) elif [ -n "$COMPARE_REF" ]; then COMMITS=$(git log ${COMPARE_REF}..HEAD --pretty=format:"- **%s**%n%b" --no-merges -- plugins/ | sed '/^$/d' | head -40) else COMMITS=$(git log --pretty=format:"- **%s**%n%b" --no-merges -10 -- plugins/ | sed '/^$/d') fi { echo 'commits<> "$GITHUB_OUTPUT" - name: Generate release notes id: notes env: VERSION: ${{ steps.version.outputs.version }} TITLE: ${{ github.event.inputs.release_title }} NOTES: ${{ github.event.inputs.release_notes }} CHANGED_PLUGIN_TITLE: ${{ needs.check-changes.outputs.changed_plugin_title }} CHANGED_PLUGIN_VERSION: ${{ needs.check-changes.outputs.changed_plugin_version }} DETECTED_CHANGES: ${{ needs.check-changes.outputs.release_notes }} DOC_FILES: ${{ needs.check-changes.outputs.changed_doc_files }} run: | > release_notes.md # 1. Primary content from v*.md files (highest priority) if [ -n "$DOC_FILES" ]; then RELEASE_NOTE_FILES=$(echo "$DOC_FILES" | grep -E '^plugins/.*/v[^/]*\.md$' | grep -v '_CN\.md$' || true) if [ -n "$RELEASE_NOTE_FILES" ]; then while IFS= read -r file; do [ -z "$file" ] && continue if [ -f "$file" ]; then # Extract content, removing any H1 title from the file to avoid duplication python3 -c "import pathlib, re; file_path = pathlib.Path(r'''$file'''); text = file_path.read_text(encoding='utf-8'); text = re.sub(r'^#\s+.+?(?:\r?\n)+', '', text, count=1, flags=re.MULTILINE); print(text.lstrip().rstrip())" >> release_notes.md echo "" >> release_notes.md fi done <<< "$RELEASE_NOTE_FILES" fi fi # 2. Automated plugin version change summary if [ -n "$DETECTED_CHANGES" ] && ! echo "$DETECTED_CHANGES" | grep -q "No changes detected"; then echo "## Version Changes" >> release_notes.md echo "" >> release_notes.md echo "$DETECTED_CHANGES" >> release_notes.md echo "" >> release_notes.md fi # 3. Manual additional notes from workflow dispatch if [ -n "$NOTES" ]; then echo "## Additional Notes" >> release_notes.md echo "" >> release_notes.md echo "$NOTES" >> release_notes.md echo "" >> release_notes.md fi cat >> release_notes.md << 'EOF' --- 📚 [Documentation Portal](https://fu-jie.github.io/openwebui-extensions/) 🐛 [Report Issues](https://github.com/Fu-Jie/openwebui-extensions/issues) EOF echo "=== Final Release Notes ===" cat release_notes.md echo "=== Release Notes ===" cat release_notes.md - name: Create Git Tag if: ${{ !startsWith(github.ref, 'refs/tags/') }} run: | VERSION="${{ steps.version.outputs.version }}" if [ -z "$VERSION" ]; then echo "Error: Version is empty!" exit 1 fi if ! git rev-parse "$VERSION" >/dev/null 2>&1; then echo "Creating tag $VERSION" git tag "$VERSION" git push origin "$VERSION" else echo "Tag $VERSION already exists" fi env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Create GitHub Release uses: softprops/action-gh-release@v2 with: tag_name: ${{ steps.version.outputs.version }} target_commitish: ${{ github.sha }} name: ${{ steps.meta.outputs.release_name }} body_path: release_notes.md prerelease: ${{ github.event.inputs.prerelease || false }} make_latest: true files: | plugin_versions.json env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Upload Release Assets env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | # Check if there are any .py files to upload if [ -d release_plugins ] && [ -n "$(find release_plugins -type f -name '*.py' 2>/dev/null)" ]; then echo "Uploading plugin files..." find release_plugins -type f -name "*.py" -print0 | xargs -0 gh release upload ${{ steps.version.outputs.version }} --clobber else echo "No plugin files to upload. Skipping asset upload." fi - name: Summary run: | echo "## 🚀 Release Created Successfully!" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "**Version:** ${{ steps.version.outputs.version }}" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "### Updated Plugins" >> $GITHUB_STEP_SUMMARY echo "${{ needs.check-changes.outputs.release_notes }}" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY