Add plugin release workflow with version extraction and changelog management
Co-authored-by: Fu-Jie <33599649+Fu-Jie@users.noreply.github.com>
This commit is contained in:
141
.github/workflows/plugin-version-check.yml
vendored
Normal file
141
.github/workflows/plugin-version-check.yml
vendored
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
# 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 (optional)
|
||||||
|
|
||||||
|
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: Comment on PR
|
||||||
|
if: steps.compare.outputs.has_changes == 'true'
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const changes = `${{ steps.compare.outputs.changes }}`;
|
||||||
|
|
||||||
|
const body = `## 🔍 Plugin Version Changes / 插件版本变化
|
||||||
|
|
||||||
|
${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 Changes')
|
||||||
|
);
|
||||||
|
|
||||||
|
if (botComment) {
|
||||||
|
// Update existing comment
|
||||||
|
await github.rest.issues.updateComment({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
comment_id: botComment.id,
|
||||||
|
body: body,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Create new comment
|
||||||
|
await github.rest.issues.createComment({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
issue_number: context.issue.number,
|
||||||
|
body: body,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
- 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
|
||||||
|
else
|
||||||
|
echo "ℹ️ **No version changes detected.**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
167
.github/workflows/release.yml
vendored
Normal file
167
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
# GitHub Actions Workflow for Plugin Release
|
||||||
|
# 插件发布工作流
|
||||||
|
#
|
||||||
|
# This workflow automates the release process for OpenWebUI plugins.
|
||||||
|
# 此工作流自动化 OpenWebUI 插件的发布流程。
|
||||||
|
#
|
||||||
|
# Triggers:
|
||||||
|
# - Manual trigger (workflow_dispatch) with custom release notes
|
||||||
|
# - Push of version tags (v*)
|
||||||
|
#
|
||||||
|
# What it does:
|
||||||
|
# 1. Scans all plugins for version information
|
||||||
|
# 2. Generates release notes with plugin versions
|
||||||
|
# 3. Creates a GitHub Release with the changelog
|
||||||
|
|
||||||
|
name: Plugin Release / 插件发布
|
||||||
|
|
||||||
|
on:
|
||||||
|
# Manual trigger with inputs
|
||||||
|
# 手动触发并提供输入
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
version:
|
||||||
|
description: 'Release version (e.g., v1.0.0)'
|
||||||
|
required: true
|
||||||
|
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
|
||||||
|
|
||||||
|
# Trigger on version tags
|
||||||
|
# 版本标签触发
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
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" ]; then
|
||||||
|
VERSION="${{ github.event.inputs.version }}"
|
||||||
|
else
|
||||||
|
VERSION="${GITHUB_REF#refs/tags/}"
|
||||||
|
fi
|
||||||
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
|
echo "Release version: $VERSION"
|
||||||
|
|
||||||
|
- name: Extract plugin versions
|
||||||
|
id: plugins
|
||||||
|
run: |
|
||||||
|
# Run the version extraction script
|
||||||
|
python scripts/extract_plugin_versions.py --json --output plugin_versions.json
|
||||||
|
python scripts/extract_plugin_versions.py --markdown --output plugin_table.md
|
||||||
|
|
||||||
|
# Output for debugging
|
||||||
|
echo "=== Plugin Versions (JSON) ==="
|
||||||
|
cat plugin_versions.json
|
||||||
|
echo ""
|
||||||
|
echo "=== Plugin Versions (Markdown) ==="
|
||||||
|
cat plugin_table.md
|
||||||
|
|
||||||
|
- name: Generate release notes
|
||||||
|
id: notes
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.version.outputs.version }}"
|
||||||
|
TITLE="${{ github.event.inputs.release_title }}"
|
||||||
|
NOTES="${{ github.event.inputs.release_notes }}"
|
||||||
|
|
||||||
|
# Generate release notes header with version
|
||||||
|
echo "# ${VERSION} Release / 发布" > release_notes.md
|
||||||
|
echo "" >> release_notes.md
|
||||||
|
|
||||||
|
# Add custom title if provided
|
||||||
|
if [ -n "$TITLE" ]; then
|
||||||
|
echo "## $TITLE" >> release_notes.md
|
||||||
|
echo "" >> release_notes.md
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Add custom notes if provided
|
||||||
|
if [ -n "$NOTES" ]; then
|
||||||
|
echo "## Release Notes / 发布说明" >> release_notes.md
|
||||||
|
echo "" >> release_notes.md
|
||||||
|
echo "$NOTES" >> release_notes.md
|
||||||
|
echo "" >> release_notes.md
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Add plugin versions table
|
||||||
|
echo "## Plugin Versions / 插件版本" >> release_notes.md
|
||||||
|
echo "" >> release_notes.md
|
||||||
|
cat plugin_table.md >> release_notes.md
|
||||||
|
echo "" >> release_notes.md
|
||||||
|
|
||||||
|
# Add installation instructions
|
||||||
|
cat >> release_notes.md << 'INSTALL_INSTRUCTIONS'
|
||||||
|
|
||||||
|
## 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`)
|
||||||
|
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)
|
||||||
|
|
||||||
|
INSTALL_INSTRUCTIONS
|
||||||
|
|
||||||
|
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
|
||||||
|
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 "### 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`)。
|
4. 推送到分支 (`git push origin feature/AmazingFeature`)。
|
||||||
5. 开启一个 Pull Request。
|
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)。
|
||||||
|
|
||||||
再次感谢你的贡献!🚀
|
再次感谢你的贡献!🚀
|
||||||
|
|||||||
201
docs/release-workflow.md
Normal file
201
docs/release-workflow.md
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
# 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 / 自动化发布流程
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Release Process / 发布流程
|
||||||
|
|
||||||
|
### Step 1: Update Plugin Version / 更新插件版本
|
||||||
|
|
||||||
|
When you make changes to a plugin, update the version number in the plugin's docstring:
|
||||||
|
|
||||||
|
当您对插件进行更改时,更新插件文档字符串中的版本号:
|
||||||
|
|
||||||
|
```python
|
||||||
|
"""
|
||||||
|
title: My Plugin
|
||||||
|
author: Fu-Jie
|
||||||
|
version: 0.2.0 # <- Update this / 更新这里
|
||||||
|
...
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Update CHANGELOG / 更新更新日志
|
||||||
|
|
||||||
|
Add your changes to the `[Unreleased]` section in `CHANGELOG.md`:
|
||||||
|
|
||||||
|
在 `CHANGELOG.md` 的 `[Unreleased]` 部分添加您的更改:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## [Unreleased] / 未发布
|
||||||
|
|
||||||
|
### Added / 新增
|
||||||
|
- New feature in Smart Mind Map / 智能思维导图新功能
|
||||||
|
|
||||||
|
### Changed / 变更
|
||||||
|
- Improved performance / 性能优化
|
||||||
|
|
||||||
|
### Fixed / 修复
|
||||||
|
- Fixed bug in export / 修复导出 bug
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Create a Release / 创建发布
|
||||||
|
|
||||||
|
#### Option A: Manual Release (Recommended) / 手动发布(推荐)
|
||||||
|
|
||||||
|
1. Go to GitHub Actions → "Plugin Release / 插件发布"
|
||||||
|
2. Click "Run workflow"
|
||||||
|
3. Fill in the details:
|
||||||
|
- **version**: e.g., `v1.0.0`
|
||||||
|
- **release_title**: e.g., "Smart Mind Map Major Update"
|
||||||
|
- **release_notes**: Additional notes in Markdown
|
||||||
|
- **prerelease**: Check if this is a pre-release
|
||||||
|
|
||||||
|
1. 前往 GitHub Actions → "Plugin Release / 插件发布"
|
||||||
|
2. 点击 "Run workflow"
|
||||||
|
3. 填写详细信息:
|
||||||
|
- **version**: 例如 `v1.0.0`
|
||||||
|
- **release_title**: 例如 "智能思维导图重大更新"
|
||||||
|
- **release_notes**: Markdown 格式的附加说明
|
||||||
|
- **prerelease**: 如果是预发布版本则勾选
|
||||||
|
|
||||||
|
#### 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
|
||||||
|
```
|
||||||
|
|
||||||
|
This will automatically trigger the release workflow.
|
||||||
|
|
||||||
|
这将自动触发发布工作流。
|
||||||
|
|
||||||
|
### Step 4: Finalize CHANGELOG / 完成更新日志
|
||||||
|
|
||||||
|
After the release, move the `[Unreleased]` content to a new version section:
|
||||||
|
|
||||||
|
发布后,将 `[Unreleased]` 的内容移动到新的版本部分:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## [Unreleased] / 未发布
|
||||||
|
<!-- Empty for now / 暂时为空 -->
|
||||||
|
|
||||||
|
## [1.0.0] - 2024-01-15
|
||||||
|
|
||||||
|
### Added / 新增
|
||||||
|
- New feature in Smart Mind Map
|
||||||
|
|
||||||
|
### Plugin Updates / 插件更新
|
||||||
|
- `Smart Mind Map`: v0.7.0 → v0.8.0
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Version Numbering / 版本编号
|
||||||
|
|
||||||
|
We follow [Semantic Versioning](https://semver.org/):
|
||||||
|
|
||||||
|
我们遵循[语义化版本](https://semver.org/lang/zh-CN/):
|
||||||
|
|
||||||
|
- **MAJOR (主版本)**: Breaking changes / 不兼容的变更
|
||||||
|
- **MINOR (次版本)**: New features, backwards compatible / 新功能,向后兼容
|
||||||
|
- **PATCH (补丁版本)**: Bug fixes / Bug 修复
|
||||||
|
|
||||||
|
### Examples / 示例
|
||||||
|
|
||||||
|
| Change Type / 变更类型 | Version Change / 版本变化 |
|
||||||
|
|----------------------|--------------------------|
|
||||||
|
| Bug fix / Bug 修复 | 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 / GitHub Actions 工作流
|
||||||
|
|
||||||
|
### release.yml
|
||||||
|
|
||||||
|
**Trigger / 触发条件:**
|
||||||
|
- Manual workflow dispatch / 手动触发
|
||||||
|
- Push of version tags (`v*`) / 推送版本标签
|
||||||
|
|
||||||
|
**Actions / 动作:**
|
||||||
|
1. Extracts all plugin versions / 提取所有插件版本
|
||||||
|
2. Generates release notes / 生成发布说明
|
||||||
|
3. Creates GitHub Release / 创建 GitHub Release
|
||||||
|
|
||||||
|
### plugin-version-check.yml
|
||||||
|
|
||||||
|
**Trigger / 触发条件:**
|
||||||
|
- Pull requests that modify `plugins/**/*.py` / 修改 `plugins/**/*.py` 的 PR
|
||||||
|
|
||||||
|
**Actions / 动作:**
|
||||||
|
1. Compares plugin versions between base and PR / 比较基础分支和 PR 的插件版本
|
||||||
|
2. Comments on PR with version changes / 在 PR 上评论版本变化
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scripts / 脚本
|
||||||
|
|
||||||
|
### extract_plugin_versions.py
|
||||||
|
|
||||||
|
Usage / 用法:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Output to console / 输出到控制台
|
||||||
|
python scripts/extract_plugin_versions.py
|
||||||
|
|
||||||
|
# Output as JSON / 输出为 JSON
|
||||||
|
python scripts/extract_plugin_versions.py --json
|
||||||
|
|
||||||
|
# Output as Markdown table / 输出为 Markdown 表格
|
||||||
|
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 to plugins
|
||||||
|
- 对插件进行功能性更改时**始终更新版本号**
|
||||||
|
|
||||||
|
2. **Write clear changelog entries** describing what changed and why
|
||||||
|
- 编写清晰的更新日志条目,描述更改内容和原因
|
||||||
|
|
||||||
|
3. **Test locally** before creating a release
|
||||||
|
- 在创建发布之前**本地测试**
|
||||||
|
|
||||||
|
4. **Use pre-releases** for testing new features
|
||||||
|
- 使用**预发布**测试新功能
|
||||||
|
|
||||||
|
5. **Reference issues** in changelog entries when applicable
|
||||||
|
- 在适用时在更新日志条目中**引用 issue**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Author
|
||||||
|
|
||||||
|
Fu-Jie
|
||||||
|
GitHub: [Fu-Jie/awesome-openwebui](https://github.com/Fu-Jie/awesome-openwebui)
|
||||||
168
docs/release-workflow.zh.md
Normal file
168
docs/release-workflow.zh.md
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
# 插件发布工作流
|
||||||
|
|
||||||
|
本文档描述了发布插件更新的工作流程。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
发布工作流包含以下组件:
|
||||||
|
|
||||||
|
1. **CHANGELOG.md** - 记录所有重要更改
|
||||||
|
2. **版本提取脚本** - 自动提取插件版本信息
|
||||||
|
3. **GitHub Actions 工作流** - 自动化发布流程
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 发布流程
|
||||||
|
|
||||||
|
### 第 1 步:更新插件版本
|
||||||
|
|
||||||
|
当您对插件进行更改时,更新插件文档字符串中的版本号:
|
||||||
|
|
||||||
|
```python
|
||||||
|
"""
|
||||||
|
title: 我的插件
|
||||||
|
author: Fu-Jie
|
||||||
|
version: 0.2.0 # <- 更新这里
|
||||||
|
...
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
### 第 2 步:更新更新日志
|
||||||
|
|
||||||
|
在 `CHANGELOG.md` 的 `[Unreleased]` 部分添加您的更改:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## [Unreleased] / 未发布
|
||||||
|
|
||||||
|
### Added / 新增
|
||||||
|
- 智能思维导图新功能
|
||||||
|
|
||||||
|
### Changed / 变更
|
||||||
|
- 性能优化
|
||||||
|
|
||||||
|
### Fixed / 修复
|
||||||
|
- 修复导出 bug
|
||||||
|
```
|
||||||
|
|
||||||
|
### 第 3 步:创建发布
|
||||||
|
|
||||||
|
#### 方式 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
|
||||||
|
```
|
||||||
|
|
||||||
|
这将自动触发发布工作流。
|
||||||
|
|
||||||
|
### 第 4 步:完成更新日志
|
||||||
|
|
||||||
|
发布后,将 `[Unreleased]` 的内容移动到新的版本部分:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## [Unreleased] / 未发布
|
||||||
|
<!-- 暂时为空 -->
|
||||||
|
|
||||||
|
## [1.0.0] - 2024-01-15
|
||||||
|
|
||||||
|
### Added / 新增
|
||||||
|
- 智能思维导图新功能
|
||||||
|
|
||||||
|
### Plugin Updates / 插件更新
|
||||||
|
- `Smart Mind Map / 思维导图`: v0.7.0 → v0.8.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
|
||||||
|
|
||||||
|
**触发条件:**
|
||||||
|
- 手动触发 (workflow_dispatch)
|
||||||
|
- 推送版本标签 (`v*`)
|
||||||
|
|
||||||
|
**动作:**
|
||||||
|
1. 提取所有插件版本
|
||||||
|
2. 生成发布说明
|
||||||
|
3. 创建 GitHub Release
|
||||||
|
|
||||||
|
### plugin-version-check.yml
|
||||||
|
|
||||||
|
**触发条件:**
|
||||||
|
- 修改 `plugins/**/*.py` 的 Pull Request
|
||||||
|
|
||||||
|
**动作:**
|
||||||
|
1. 比较基础分支和 PR 的插件版本
|
||||||
|
2. 在 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. **编写清晰的更新日志** - 描述更改内容和原因
|
||||||
|
3. **本地测试** - 在创建发布之前
|
||||||
|
4. **使用预发布** - 测试新功能
|
||||||
|
5. **引用 issue** - 在适用时在更新日志条目中
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 作者
|
||||||
|
|
||||||
|
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