Merge pull request #12 from Fu-Jie/copilot/build-plugin-release-workflow

Add plugin release workflow with auto-release on merge and version validation
This commit is contained in:
Jeff
2026-01-01 04:09:27 +08:00
committed by GitHub
7 changed files with 1333 additions and 0 deletions

View File

@@ -0,0 +1,214 @@
# GitHub Actions Workflow for Plugin Version Change Detection
# 插件版本变化检测工作流
#
# This workflow detects version changes in plugins when PRs are created or updated.
# 此工作流在创建或更新 PR 时检测插件的版本变化。
#
# What it does:
# 1. Compares plugin versions between base and head branches
# 2. Generates a summary of version changes
# 3. Comments on the PR with the changes
# 4. FAILS the check if plugin files are modified but no version update is detected
# (enforces version bump requirement for plugin changes)
name: Plugin Version Check / 插件版本检查
on:
pull_request:
branches:
- main
paths:
- 'plugins/**/*.py'
permissions:
contents: read
pull-requests: write
jobs:
check-versions:
runs-on: ubuntu-latest
steps:
- name: Checkout PR head
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
path: head
- name: Checkout base branch
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.base.sha }}
path: base
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Extract and compare versions
id: compare
run: |
# Extract base versions
cd base
if [ -f scripts/extract_plugin_versions.py ]; then
python scripts/extract_plugin_versions.py --json --output ../base_versions.json
else
# Fallback if script doesn't exist in base
echo "[]" > ../base_versions.json
fi
cd ..
# Extract head versions
cd head
python scripts/extract_plugin_versions.py --json --output ../head_versions.json
# Compare versions
python scripts/extract_plugin_versions.py --compare ../base_versions.json --output ../changes.md
cd ..
echo "=== Version Changes ==="
cat changes.md
# Check if there are any changes
if grep -q "No changes detected" changes.md; then
echo "has_changes=false" >> $GITHUB_OUTPUT
else
echo "has_changes=true" >> $GITHUB_OUTPUT
fi
# Store changes for comment
{
echo 'changes<<EOF'
cat changes.md
echo 'EOF'
} >> $GITHUB_OUTPUT
- name: Check PR description for update notes
id: check_description
if: steps.compare.outputs.has_changes == 'true'
uses: actions/github-script@v7
with:
script: |
const prBody = context.payload.pull_request.body || '';
// Check if PR has meaningful description (at least 20 characters, excluding whitespace)
// Use [\s\S]*? for multiline HTML comment matching (compatible across JS engines)
const cleanBody = prBody.replace(/\s+/g, '').replace(/<!--[\s\S]*?-->/g, '');
const hasDescription = cleanBody.length >= 20;
console.log(`PR body length (cleaned): ${cleanBody.length}`);
console.log(`Has meaningful description: ${hasDescription}`);
core.setOutput('has_description', hasDescription.toString());
return hasDescription;
- name: Comment on PR
uses: actions/github-script@v7
with:
script: |
const hasChanges = '${{ steps.compare.outputs.has_changes }}' === 'true';
const hasDescription = '${{ steps.check_description.outputs.has_description }}' === 'true';
const changes = `${{ steps.compare.outputs.changes }}`;
let statusIcon = '';
let statusMessage = '';
if (hasChanges && hasDescription) {
statusIcon = '✅';
statusMessage = '版本更新检测通过PR 包含版本变化和更新说明。\n\nVersion check passed! PR contains version changes and update description.';
} else if (hasChanges && !hasDescription) {
statusIcon = '⚠️';
statusMessage = '检测到版本更新,但 PR 描述过短。请在 PR 描述中添加更新说明(至少 20 个字符)。\n\nVersion update detected, but PR description is too short. Please add update notes in PR description (at least 20 characters).';
} else {
statusIcon = '❌';
statusMessage = '未检测到版本更新!修改插件文件时必须更新版本号。\n\nNo version update detected! You must update the version number when modifying plugin files.';
}
const body = `## ${statusIcon} Plugin Version Check / 插件版本检查
${statusMessage}
---
${hasChanges ? `### 版本变化 / Version Changes\n\n${changes}` : ''}
---
*This comment was generated automatically. / 此评论由自动生成。*
`;
// Find existing comment from github-actions bot
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
const botComment = comments.find(comment =>
(comment.user.login === 'github-actions[bot]' || comment.user.type === 'Bot') &&
comment.body.includes('Plugin Version Check')
);
if (botComment) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
body: body,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: body,
});
}
- name: Enforce version update requirement
if: steps.compare.outputs.has_changes == 'false'
run: |
echo "::error::❌ 未检测到版本更新!修改插件文件时必须更新版本号。"
echo "::error::No version update detected! You must update the version number when modifying plugin files."
echo ""
echo "请在插件文件的 docstring 中更新版本号:"
echo "Please update the version in your plugin's docstring:"
echo ""
echo '"""'
echo 'title: Your Plugin'
echo 'version: 0.2.0 # <- 更新此处 / Update this'
echo '...'
echo '"""'
exit 1
- name: Enforce PR description requirement
if: steps.compare.outputs.has_changes == 'true' && steps.check_description.outputs.has_description == 'false'
run: |
echo "::error::⚠️ PR 描述过短!请添加更新说明。"
echo "::error::PR description is too short! Please add update notes."
echo ""
echo "请在 PR 描述中添加以下内容:"
echo "Please add the following to your PR description:"
echo ""
echo "- 更新了哪些功能 / What features were updated"
echo "- 修复了哪些问题 / What issues were fixed"
echo "- 其他重要变更 / Other important changes"
exit 1
- name: Summary
run: |
echo "## 🔍 Plugin Version Check Results" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [ "${{ steps.compare.outputs.has_changes }}" = "true" ]; then
echo "✅ **Version changes detected!**" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
cat changes.md >> $GITHUB_STEP_SUMMARY
if [ "${{ steps.check_description.outputs.has_description }}" = "true" ]; then
echo "" >> $GITHUB_STEP_SUMMARY
echo "✅ **PR description check passed!**" >> $GITHUB_STEP_SUMMARY
fi
else
echo "❌ **No version changes detected - check failed!**" >> $GITHUB_STEP_SUMMARY
fi

340
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,340 @@
# GitHub Actions Workflow for Plugin Release
# 插件发布工作流
#
# This workflow automates the release process for OpenWebUI plugins.
# 此工作流自动化 OpenWebUI 插件的发布流程。
#
# Triggers:
# - Push to main branch when plugins are modified (auto-release)
# - Manual trigger (workflow_dispatch) with custom release notes
# - Push of 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. Supports multiple plugin updates in a single release
name: Plugin Release / 插件发布
on:
# Auto-trigger on push to main when plugins are modified
push:
branches:
- main
paths:
- 'plugins/**/*.py'
tags:
- 'v*'
# Manual trigger with inputs
workflow_dispatch:
inputs:
version:
description: 'Release version (e.g., v1.0.0). Leave empty for auto-generated version.'
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
outputs:
has_changes: ${{ steps.detect.outputs.has_changes }}
changed_plugins: ${{ steps.detect.outputs.changed_plugins }}
release_notes: ${{ steps.detect.outputs.release_notes }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Detect plugin changes
id: detect
run: |
# Get the last release tag
LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
if [ -z "$LAST_TAG" ]; then
echo "No previous release found, treating all plugins as new"
COMPARE_REF="$(git rev-list --max-parents=0 HEAD)"
else
echo "Comparing with last release: $LAST_TAG"
COMPARE_REF="$LAST_TAG"
fi
# Get current plugin versions
python scripts/extract_plugin_versions.py --json --output current_versions.json
# Get previous plugin versions by checking out old plugins
if git worktree add /tmp/old_repo ${COMPARE_REF} 2>/dev/null; then
if [ -d /tmp/old_repo/plugins ]; then
python scripts/extract_plugin_versions.py --plugins-dir /tmp/old_repo/plugins --json --output old_versions.json
else
echo "[]" > old_versions.json
fi
git worktree remove /tmp/old_repo 2>/dev/null || true
else
echo "Failed to create worktree, using empty version list"
echo "[]" > old_versions.json
fi
# Compare versions and generate release notes
python scripts/extract_plugin_versions.py --compare old_versions.json --output changes.md
python scripts/extract_plugin_versions.py --compare old_versions.json --json --output changes.json
echo "=== Version Changes ==="
cat changes.md
# Check if there are any changes
if grep -q "No changes detected" changes.md; then
echo "has_changes=false" >> $GITHUB_OUTPUT
echo "changed_plugins=" >> $GITHUB_OUTPUT
else
echo "has_changes=true" >> $GITHUB_OUTPUT
# Extract changed plugin file paths using Python
python3 -c "
import json
with open('changes.json', 'r') as f:
data = json.load(f)
files = []
for plugin in data.get('added', []):
if 'file_path' in plugin:
files.append(plugin['file_path'])
for update in data.get('updated', []):
if 'current' in update and 'file_path' in update['current']:
files.append(update['current']['file_path'])
print('\n'.join(files))
" > changed_files.txt
echo "changed_plugins<<EOF" >> $GITHUB_OUTPUT
cat changed_files.txt >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
fi
# Store release notes
{
echo 'release_notes<<EOF'
cat changes.md
echo 'EOF'
} >> $GITHUB_OUTPUT
release:
needs: check-changes
if: needs.check-changes.outputs.has_changes == 'true' || github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Determine version
id: version
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ -n "${{ github.event.inputs.version }}" ]; then
VERSION="${{ github.event.inputs.version }}"
elif [[ "${{ github.ref }}" == refs/tags/v* ]]; then
VERSION="${GITHUB_REF#refs/tags/}"
else
# Auto-generate version based on date and run number
VERSION="v$(date +'%Y.%m.%d')-${{ github.run_number }}"
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Release version: $VERSION"
- name: Extract plugin versions
id: plugins
run: |
python scripts/extract_plugin_versions.py --json --output plugin_versions.json
python scripts/extract_plugin_versions.py --markdown --output plugin_table.md
echo "=== Plugin Versions ==="
cat plugin_table.md
- 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 "Collecting all plugin files..."
find plugins -name "*.py" -type f ! -name "__*" | while read -r file; do
dir=$(dirname "$file")
mkdir -p "release_plugins/$dir"
cp "$file" "release_plugins/$file"
done
fi
# Create a zip file with error handling
cd release_plugins
if [ -n "$(ls -A . 2>/dev/null)" ]; then
if zip -r ../plugins_release.zip .; then
echo "Successfully created plugins_release.zip"
else
echo "Warning: Failed to create zip file, creating empty placeholder"
touch ../plugins_release.zip
fi
else
echo "No plugin files to zip, creating empty placeholder"
touch ../plugins_release.zip
fi
cd ..
echo "=== Collected Files ==="
find release_plugins -name "*.py" -type f | head -20
- name: Get commit messages
id: commits
if: github.event_name == 'push'
run: |
LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
if [ -n "$LAST_TAG" ]; then
COMMITS=$(git log ${LAST_TAG}..HEAD --pretty=format:"- %s" --no-merges -- plugins/ | head -20)
else
COMMITS=$(git log --pretty=format:"- %s" --no-merges -10 -- plugins/)
fi
{
echo 'commits<<EOF'
echo "$COMMITS"
echo 'EOF'
} >> $GITHUB_OUTPUT
- name: Generate release notes
id: notes
run: |
VERSION="${{ steps.version.outputs.version }}"
TITLE="${{ github.event.inputs.release_title }}"
NOTES="${{ github.event.inputs.release_notes }}"
DETECTED_CHANGES="${{ needs.check-changes.outputs.release_notes }}"
COMMITS="${{ steps.commits.outputs.commits }}"
echo "# ${VERSION} Release / 发布" > release_notes.md
echo "" >> release_notes.md
if [ -n "$TITLE" ]; then
echo "## $TITLE" >> release_notes.md
echo "" >> release_notes.md
fi
if [ -n "$DETECTED_CHANGES" ] && ! echo "$DETECTED_CHANGES" | grep -q "No changes detected"; then
echo "## What's Changed / 更新内容" >> release_notes.md
echo "" >> release_notes.md
echo "$DETECTED_CHANGES" >> release_notes.md
echo "" >> release_notes.md
fi
if [ -n "$COMMITS" ]; then
echo "## Commits / 提交记录" >> release_notes.md
echo "" >> release_notes.md
echo "$COMMITS" >> release_notes.md
echo "" >> release_notes.md
fi
if [ -n "$NOTES" ]; then
echo "## Additional Notes / 附加说明" >> release_notes.md
echo "" >> release_notes.md
echo "$NOTES" >> release_notes.md
echo "" >> release_notes.md
fi
echo "## All Plugin Versions / 所有插件版本" >> release_notes.md
echo "" >> release_notes.md
cat plugin_table.md >> release_notes.md
echo "" >> release_notes.md
cat >> release_notes.md << 'EOF'
## Download / 下载
📦 **plugins_release.zip** - 包含本次更新的所有插件文件 / Contains all updated plugin files
### Installation / 安装
#### From OpenWebUI Community
1. Open OpenWebUI Admin Panel
2. Navigate to Functions/Tools
3. Search for the plugin name
4. Click Install
#### Manual Installation / 手动安装
1. Download the plugin file (`.py`) from the assets below
2. Open OpenWebUI Admin Panel → Functions
3. Click "Create Function" → Import
4. Paste the plugin code
---
📚 [Documentation / 文档](https://fu-jie.github.io/awesome-openwebui/)
🐛 [Report Issues / 报告问题](https://github.com/Fu-Jie/awesome-openwebui/issues)
EOF
echo "=== Release Notes ==="
cat release_notes.md
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.version.outputs.version }}
name: ${{ github.event.inputs.release_title || steps.version.outputs.version }}
body_path: release_notes.md
prerelease: ${{ github.event.inputs.prerelease || false }}
files: |
plugin_versions.json
plugins_release.zip
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- 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
echo "### All Plugin Versions" >> $GITHUB_STEP_SUMMARY
cat plugin_table.md >> $GITHUB_STEP_SUMMARY

75
CHANGELOG.md Normal file
View File

@@ -0,0 +1,75 @@
# Changelog / 更新日志
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
本项目的所有重要更改都将记录在此文件中。
格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)
本项目遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
---
## [Unreleased] / 未发布
### Added / 新增
- 插件发布工作流 (Plugin release workflow)
### Changed / 变更
### Fixed / 修复
### Removed / 移除
---
## Plugin Versions / 插件版本
### Actions
| Plugin / 插件 | Version / 版本 |
|---------------|----------------|
| Smart Mind Map / 思维导图 | 0.8.0 |
| Flash Card / 闪记卡 | 0.2.1 |
| Export to Word / 导出为 Word | 0.1.0 |
| Export to Excel / 导出为 Excel | 0.3.3 |
| Deep Reading & Summary / 精读 | 0.1.0 / 2.0.0 |
| Smart Infographic / 智能信息图 | 1.3.0 |
### Filters
| Plugin / 插件 | Version / 版本 |
|---------------|----------------|
| Async Context Compression / 异步上下文压缩 | 1.1.0 |
| Context & Model Enhancement Filter | 0.2 |
| Gemini Manifold Companion | 1.7.0 |
| Gemini 多模态过滤器 | 0.3.2 |
### Pipes
| Plugin / 插件 | Version / 版本 |
|---------------|----------------|
| Gemini Manifold google_genai | 1.26.0 |
---
<!--
Release Template / 发布模板:
## [x.x.x] - YYYY-MM-DD
### Added / 新增
- New feature description
### Changed / 变更
- Change description
### Fixed / 修复
- Bug fix description
### Plugin Updates / 插件更新
- `plugin_name`: v0.x.0 -> v0.y.0
- Feature 1
- Feature 2
-->

View File

@@ -48,4 +48,40 @@
4. 推送到分支 (`git push origin feature/AmazingFeature`)。
5. 开启一个 Pull Request。
## 📦 版本更新与发布
当你更新插件时,请遵循以下流程:
### 1. 更新版本号
在插件文件的 docstring 中更新版本号(遵循[语义化版本](https://semver.org/lang/zh-CN/)
```python
"""
title: 我的插件
version: 0.2.0 # 更新此处
...
"""
```
### 2. 更新更新日志
在 `CHANGELOG.md` 的 `[Unreleased]` 部分添加你的更改:
```markdown
### Added / 新增
- 新功能描述
### Fixed / 修复
- Bug 修复描述
```
### 3. 发布流程
维护者会通过以下方式发布新版本:
- 手动触发 GitHub Actions 中的 "Plugin Release" 工作流
- 或创建版本标签 (`v*`)
详细说明请参阅 [发布工作流文档](docs/release-workflow.zh.md)。
再次感谢你的贡献!🚀

193
docs/release-workflow.md Normal file
View File

@@ -0,0 +1,193 @@
# Plugin Release Workflow
This document describes the workflow for releasing plugin updates.
---
## Overview
The release workflow consists of the following components:
1. **CHANGELOG.md** - Records all notable changes
2. **Version Extraction Script** - Automatically extracts plugin versions
3. **GitHub Actions Workflows** - Automates the release process
---
## Automatic Release Process ⭐
When a plugin update PR is merged to `main` branch, the release process is **triggered automatically**:
### PR Merge Requirements
PRs that modify plugin files must meet the following conditions to merge:
1.**Version must be updated** - The plugin's `version` field must be changed
2.**PR description must contain update notes** - At least 20 characters of description
If these conditions are not met, the PR check will fail and cannot be merged.
### Automatic Release Contents
After successful merge, the system will automatically:
1. 🔍 Detect version changes (compared to last release)
2. 📝 Generate release notes (with update content and commit history)
3. 📦 Create GitHub Release (with downloadable plugin files)
4. 🏷️ Auto-generate version number (format: `vYYYY.MM.DD-run_number`)
### Release Includes
- **plugins_release.zip** - All updated plugin files packaged
- **plugin_versions.json** - All plugin version information (JSON format)
- **Release Notes** - Includes:
- List of new/updated plugins
- Related commit history
- Complete plugin version table
- Installation instructions
---
## Release Process
### Step 1: Update Plugin Version
When you make changes to a plugin, you **must** update the version number:
```python
"""
title: My Plugin
author: Fu-Jie
version: 0.2.0 # <- Must update this!
...
"""
```
### Step 2: Create PR with Update Notes
Add update notes in your PR description (at least 20 characters):
```markdown
## Changes
- Added XXX feature
- Fixed YYY issue
- Improved ZZZ performance
```
### Step 3: Merge PR
After checks pass, merge the PR to `main` branch - the system will automatically create a Release.
---
## Manual Release (Optional)
In addition to automatic release, you can also trigger manually:
### Option A: Manual Trigger
1. Go to GitHub Actions → "Plugin Release / 插件发布"
2. Click "Run workflow"
3. Fill in the details:
- **version**: e.g., `v1.0.0` (leave empty for auto-generation)
- **release_title**: e.g., "Smart Mind Map Major Update"
- **release_notes**: Additional notes in Markdown
- **prerelease**: Check if this is a pre-release
### Option B: Tag-based Release
```bash
# Create and push a version tag
git tag -a v1.0.0 -m "Release v1.0.0"
git push origin v1.0.0
```
---
## Version Numbering
We follow [Semantic Versioning](https://semver.org/):
- **MAJOR**: Breaking changes
- **MINOR**: New features, backwards compatible
- **PATCH**: Bug fixes
### Examples
| Change Type | Version Change |
|-------------|----------------|
| Bug fix | 0.1.0 → 0.1.1 |
| New feature | 0.1.1 → 0.2.0 |
| Breaking change | 0.2.0 → 1.0.0 |
---
## GitHub Actions Workflows
### release.yml
**Triggers:**
- ⭐ Push to `main` branch with `plugins/**/*.py` changes (auto-release)
- Manual workflow dispatch
- Push of version tags (`v*`)
**Actions:**
1. Detects version changes compared to last release
2. Collects updated plugin files
3. Generates release notes (with commit history)
4. Creates GitHub Release (with downloadable attachments)
### plugin-version-check.yml
**Trigger:**
- Pull requests that modify `plugins/**/*.py`
**Actions:**
1. Compares plugin versions between base and PR
2. Checks if version was updated
3. Checks if PR description is detailed enough
4. ❌ Fails if no version update detected
5. ⚠️ Fails if PR description is too short
---
## Scripts
### extract_plugin_versions.py
Usage:
```bash
# Output to console
python scripts/extract_plugin_versions.py
# Output as JSON
python scripts/extract_plugin_versions.py --json
# Output as Markdown table
python scripts/extract_plugin_versions.py --markdown
# Compare with previous version
python scripts/extract_plugin_versions.py --compare old_versions.json
# Save to file
python scripts/extract_plugin_versions.py --json --output versions.json
```
---
## Best Practices
1. **Always update version numbers** when making functional changes (required)
2. **Write clear PR descriptions** describing what changed and why (required)
3. **Test locally** before creating a PR
4. **Use pre-releases** for testing new features
5. **Reference issues** in PR descriptions
---
## Author
Fu-Jie
GitHub: [Fu-Jie/awesome-openwebui](https://github.com/Fu-Jie/awesome-openwebui)

191
docs/release-workflow.zh.md Normal file
View File

@@ -0,0 +1,191 @@
# 插件发布工作流
本文档描述了发布插件更新的工作流程。
---
## 概述
发布工作流包含以下组件:
1. **CHANGELOG.md** - 记录所有重要更改
2. **版本提取脚本** - 自动提取插件版本信息
3. **GitHub Actions 工作流** - 自动化发布流程
---
## 自动发布流程 ⭐
当插件更新的 PR 合并到 `main` 分支时,会**自动触发**发布流程:
### PR 合并要求
修改插件文件的 PR 必须满足以下条件才能合并:
1.**版本号必须更新** - 插件的 `version` 字段必须有变化
2.**PR 描述必须包含更新说明** - 至少 20 个字符的描述
如果不满足这些条件PR 检查会失败,无法合并。
### 自动发布内容
合并成功后,系统会自动:
1. 🔍 检测版本变化(与上次 release 对比)
2. 📝 生成发布说明(包含更新内容和提交记录)
3. 📦 创建 GitHub Release包含可下载的插件文件
4. 🏷️ 自动生成版本号(格式:`vYYYY.MM.DD-运行号`
### Release 包含内容
- **plugins_release.zip** - 本次更新的所有插件文件打包
- **plugin_versions.json** - 所有插件版本信息 (JSON 格式)
- **发布说明** - 包含:
- 新增/更新的插件列表
- 相关提交记录
- 所有插件版本表
- 安装说明
---
## 发布流程
### 第 1 步:更新插件版本
当您对插件进行更改时,**必须**更新插件文档字符串中的版本号:
```python
"""
title: 我的插件
author: Fu-Jie
version: 0.2.0 # <- 必须更新这里!
...
"""
```
### 第 2 步:创建 PR 并添加更新说明
在 PR 描述中说明更新内容(至少 20 个字符):
```markdown
## 更新内容
- 新增 XXX 功能
- 修复 YYY 问题
- 优化 ZZZ 性能
```
### 第 3 步:合并 PR
满足检查条件后,合并 PR 到 `main` 分支,系统会自动创建 Release。
---
## 手动发布(可选)
除了自动发布,您也可以手动触发发布:
### 方式 A手动触发
1. 前往 GitHub Actions → "Plugin Release / 插件发布"
2. 点击 "Run workflow"
3. 填写详细信息:
- **version**: 例如 `v1.0.0`(留空则自动生成)
- **release_title**: 例如 "智能思维导图重大更新"
- **release_notes**: Markdown 格式的附加说明
- **prerelease**: 如果是预发布版本则勾选
### 方式 B基于标签的发布
```bash
# 创建并推送版本标签
git tag -a v1.0.0 -m "Release v1.0.0"
git push origin v1.0.0
```
---
## 版本编号
我们遵循[语义化版本](https://semver.org/lang/zh-CN/)
- **主版本 (MAJOR)**: 不兼容的 API 变更
- **次版本 (MINOR)**: 向后兼容的新功能
- **补丁版本 (PATCH)**: 向后兼容的 Bug 修复
### 示例
| 变更类型 | 版本变化 |
|---------|---------|
| Bug 修复 | 0.1.0 → 0.1.1 |
| 新功能 | 0.1.1 → 0.2.0 |
| 不兼容变更 | 0.2.0 → 1.0.0 |
---
## GitHub Actions 工作流
### release.yml
**触发条件:**
- ⭐ 推送到 `main` 分支且修改了 `plugins/**/*.py`(自动发布)
- 手动触发 (workflow_dispatch)
- 推送版本标签 (`v*`)
**动作:**
1. 检测与上次 Release 的版本变化
2. 收集更新的插件文件
3. 生成发布说明(含提交记录)
4. 创建 GitHub Release含可下载附件
### plugin-version-check.yml
**触发条件:**
- 修改 `plugins/**/*.py` 的 Pull Request
**动作:**
1. 比较基础分支和 PR 的插件版本
2. 检查是否有版本更新
3. 检查 PR 描述是否足够详细
4. ❌ 如果没有版本更新,检查失败
5. ⚠️ 如果 PR 描述过短,检查失败
---
## 脚本使用
### extract_plugin_versions.py
```bash
# 输出到控制台
python scripts/extract_plugin_versions.py
# 输出为 JSON
python scripts/extract_plugin_versions.py --json
# 输出为 Markdown 表格
python scripts/extract_plugin_versions.py --markdown
# 与之前版本比较
python scripts/extract_plugin_versions.py --compare old_versions.json
# 保存到文件
python scripts/extract_plugin_versions.py --json --output versions.json
```
---
## 最佳实践
1. **始终更新版本号** - 对插件进行功能性更改时(必需)
2. **编写清晰的 PR 描述** - 描述更改内容和原因(必需)
3. **本地测试** - 在创建 PR 之前
4. **使用预发布** - 测试新功能
5. **引用 issue** - 在 PR 描述中
---
## 作者
Fu-Jie
GitHub: [Fu-Jie/awesome-openwebui](https://github.com/Fu-Jie/awesome-openwebui)

View File

@@ -0,0 +1,284 @@
#!/usr/bin/env python3
"""
Script to extract plugin version information from Python files.
用于从 Python 插件文件中提取版本信息的脚本。
This script scans the plugins directory and extracts metadata (title, version, author, description)
from Python files that follow the OpenWebUI plugin docstring format.
Usage:
python extract_plugin_versions.py # Output to console
python extract_plugin_versions.py --json # Output as JSON
python extract_plugin_versions.py --markdown # Output as Markdown table
python extract_plugin_versions.py --compare old.json # Compare with previous version file
"""
import argparse
import json
import os
import re
import sys
from pathlib import Path
from typing import Any
def extract_plugin_metadata(file_path: str) -> dict[str, Any] | None:
"""
Extract plugin metadata from a Python file's docstring.
从 Python 文件的文档字符串中提取插件元数据。
Args:
file_path: Path to the Python file
Returns:
Dictionary containing plugin metadata or None if not a valid plugin file
"""
try:
with open(file_path, "r", encoding="utf-8") as f:
content = f.read()
except Exception as e:
print(f"Error reading {file_path}: {e}", file=sys.stderr)
return None
# Match the docstring at the beginning of the file (allowing leading whitespace/comments)
docstring_pattern = r'^\s*"""(.*?)"""'
match = re.search(docstring_pattern, content, re.DOTALL)
if not match:
return None
docstring = match.group(1)
# Extract metadata fields
metadata = {}
field_patterns = {
"title": r"title:\s*(.+?)(?:\n|$)",
"author": r"author:\s*(.+?)(?:\n|$)",
"author_url": r"author_url:\s*(.+?)(?:\n|$)",
"funding_url": r"funding_url:\s*(.+?)(?:\n|$)",
"version": r"version:\s*(.+?)(?:\n|$)",
"description": r"description:\s*(.+?)(?:\n|$)",
"requirements": r"requirements:\s*(.+?)(?:\n|$)",
}
for field, pattern in field_patterns.items():
field_match = re.search(pattern, docstring, re.IGNORECASE)
if field_match:
metadata[field] = field_match.group(1).strip()
# Only return if we found at least title and version
if "title" in metadata and "version" in metadata:
metadata["file_path"] = file_path
return metadata
return None
def scan_plugins_directory(plugins_dir: str) -> list[dict[str, Any]]:
"""
Scan the plugins directory and extract metadata from all plugin files.
扫描 plugins 目录并从所有插件文件中提取元数据。
Args:
plugins_dir: Path to the plugins directory
Returns:
List of plugin metadata dictionaries
"""
plugins = []
plugins_path = Path(plugins_dir)
if not plugins_path.exists():
print(f"Plugins directory not found: {plugins_dir}", file=sys.stderr)
return plugins
# Walk through all subdirectories
for root, _dirs, files in os.walk(plugins_path):
for file in files:
if file.endswith(".py") and not file.startswith("__"):
file_path = os.path.join(root, file)
metadata = extract_plugin_metadata(file_path)
if metadata:
# Determine plugin type from directory structure
rel_path = os.path.relpath(file_path, plugins_dir)
parts = rel_path.split(os.sep)
if len(parts) > 0:
metadata["type"] = parts[0] # actions, filters, pipes, etc.
plugins.append(metadata)
return plugins
def compare_versions(
current: list[dict], previous_file: str
) -> dict[str, list[dict]]:
"""
Compare current plugin versions with a previous version file.
比较当前插件版本与之前的版本文件。
Args:
current: List of current plugin metadata
previous_file: Path to JSON file with previous versions
Returns:
Dictionary with 'added', 'updated', 'removed' lists
"""
try:
with open(previous_file, "r", encoding="utf-8") as f:
previous = json.load(f)
except FileNotFoundError:
return {"added": current, "updated": [], "removed": []}
except json.JSONDecodeError:
print(f"Error parsing {previous_file}", file=sys.stderr)
return {"added": current, "updated": [], "removed": []}
# Create lookup dictionaries by title
current_by_title = {p["title"]: p for p in current}
previous_by_title = {p["title"]: p for p in previous}
result = {"added": [], "updated": [], "removed": []}
# Find added and updated plugins
for title, plugin in current_by_title.items():
if title not in previous_by_title:
result["added"].append(plugin)
elif plugin["version"] != previous_by_title[title]["version"]:
result["updated"].append(
{
"current": plugin,
"previous": previous_by_title[title],
}
)
# Find removed plugins
for title, plugin in previous_by_title.items():
if title not in current_by_title:
result["removed"].append(plugin)
return result
def format_markdown_table(plugins: list[dict]) -> str:
"""
Format plugins as a Markdown table.
将插件格式化为 Markdown 表格。
"""
lines = [
"| Plugin / 插件 | Version / 版本 | Type / 类型 | Description / 描述 |",
"|---------------|----------------|-------------|---------------------|",
]
for plugin in sorted(plugins, key=lambda x: (x.get("type", ""), x.get("title", ""))):
title = plugin.get("title", "Unknown")
version = plugin.get("version", "Unknown")
plugin_type = plugin.get("type", "Unknown").capitalize()
full_description = plugin.get("description", "")
description = full_description[:50]
if len(full_description) > 50:
description += "..."
lines.append(f"| {title} | {version} | {plugin_type} | {description} |")
return "\n".join(lines)
def format_release_notes(comparison: dict[str, list]) -> str:
"""
Format version comparison as release notes.
将版本比较格式化为发布说明。
"""
lines = []
if comparison["added"]:
lines.append("### 新增插件 / New Plugins")
for plugin in comparison["added"]:
lines.append(f"- **{plugin['title']}** v{plugin['version']}")
if plugin.get("description"):
lines.append(f" - {plugin['description']}")
lines.append("")
if comparison["updated"]:
lines.append("### 插件更新 / Plugin Updates")
for update in comparison["updated"]:
curr = update["current"]
prev = update["previous"]
lines.append(
f"- **{curr['title']}**: v{prev['version']} → v{curr['version']}"
)
lines.append("")
if comparison["removed"]:
lines.append("### 移除插件 / Removed Plugins")
for plugin in comparison["removed"]:
lines.append(f"- **{plugin['title']}** v{plugin['version']}")
lines.append("")
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser(
description="Extract and compare plugin version information"
)
parser.add_argument(
"--plugins-dir",
default="plugins",
help="Path to plugins directory (default: plugins)",
)
parser.add_argument(
"--json",
action="store_true",
help="Output as JSON",
)
parser.add_argument(
"--markdown",
action="store_true",
help="Output as Markdown table",
)
parser.add_argument(
"--compare",
metavar="FILE",
help="Compare with previous version JSON file",
)
parser.add_argument(
"--output",
"-o",
metavar="FILE",
help="Write output to file instead of stdout",
)
args = parser.parse_args()
# Scan plugins
plugins = scan_plugins_directory(args.plugins_dir)
# Generate output
if args.compare:
comparison = compare_versions(plugins, args.compare)
if args.json:
output = json.dumps(comparison, indent=2, ensure_ascii=False)
else:
output = format_release_notes(comparison)
if not output.strip():
output = "No changes detected. / 未检测到更改。"
elif args.json:
output = json.dumps(plugins, indent=2, ensure_ascii=False)
elif args.markdown:
output = format_markdown_table(plugins)
else:
# Default: simple list
lines = []
for plugin in sorted(plugins, key=lambda x: x.get("title", "")):
lines.append(f"{plugin.get('title', 'Unknown')}: v{plugin.get('version', '?')}")
output = "\n".join(lines)
# Write output
if args.output:
with open(args.output, "w", encoding="utf-8") as f:
f.write(output)
print(f"Output written to {args.output}")
else:
print(output)
if __name__ == "__main__":
main()