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:
214
.github/workflows/plugin-version-check.yml
vendored
Normal file
214
.github/workflows/plugin-version-check.yml
vendored
Normal 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
340
.github/workflows/release.yml
vendored
Normal 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
75
CHANGELOG.md
Normal 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
|
||||
-->
|
||||
@@ -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
193
docs/release-workflow.md
Normal 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
191
docs/release-workflow.zh.md
Normal 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)
|
||||
284
scripts/extract_plugin_versions.py
Normal file
284
scripts/extract_plugin_versions.py
Normal 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()
|
||||
Reference in New Issue
Block a user