fix(release): enforce single plugin update per release and improve version tagging
This commit is contained in:
146
.github/workflows/release.yml
vendored
146
.github/workflows/release.yml
vendored
@@ -5,13 +5,13 @@
|
|||||||
# Triggers:
|
# Triggers:
|
||||||
# - Push to main branch when plugins are modified (auto-release)
|
# - Push to main branch when plugins are modified (auto-release)
|
||||||
# - Manual trigger (workflow_dispatch) with custom release notes
|
# - Manual trigger (workflow_dispatch) with custom release notes
|
||||||
# - Push of version tags (v*)
|
# - Push of plugin version tags (<plugin>-v*)
|
||||||
#
|
#
|
||||||
# What it does:
|
# What it does:
|
||||||
# 1. Detects plugin version changes compared to the last release
|
# 1. Detects plugin version changes compared to the last release
|
||||||
# 2. Generates release notes with updated plugin information
|
# 2. Generates release notes with updated plugin information
|
||||||
# 3. Creates a GitHub Release with plugin files as downloadable assets
|
# 3. Creates a GitHub Release with plugin files as downloadable assets
|
||||||
# 4. Supports multiple plugin updates in a single release
|
# 4. Enforces one plugin creation/update per release
|
||||||
|
|
||||||
name: Plugin Release
|
name: Plugin Release
|
||||||
|
|
||||||
@@ -28,13 +28,14 @@ on:
|
|||||||
- 'plugins/**/v*_CN.md'
|
- 'plugins/**/v*_CN.md'
|
||||||
- 'docs/plugins/**/*.md'
|
- 'docs/plugins/**/*.md'
|
||||||
tags:
|
tags:
|
||||||
|
- '*-v*'
|
||||||
- 'v*'
|
- 'v*'
|
||||||
|
|
||||||
# Manual trigger with inputs
|
# Manual trigger with inputs
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
version:
|
version:
|
||||||
description: 'Release version (e.g., v1.0.0). Leave empty for auto-generated version.'
|
description: 'Release tag (e.g., markdown-normalizer-v1.2.8). Leave empty for auto-generated tag.'
|
||||||
required: false
|
required: false
|
||||||
type: string
|
type: string
|
||||||
release_title:
|
release_title:
|
||||||
@@ -65,7 +66,9 @@ jobs:
|
|||||||
outputs:
|
outputs:
|
||||||
has_changes: ${{ steps.detect.outputs.has_changes }}
|
has_changes: ${{ steps.detect.outputs.has_changes }}
|
||||||
changed_plugins: ${{ steps.detect.outputs.changed_plugins }}
|
changed_plugins: ${{ steps.detect.outputs.changed_plugins }}
|
||||||
changed_plugin_titles: ${{ steps.detect.outputs.changed_plugin_titles }}
|
changed_plugin_title: ${{ steps.detect.outputs.changed_plugin_title }}
|
||||||
|
changed_plugin_slug: ${{ steps.detect.outputs.changed_plugin_slug }}
|
||||||
|
changed_plugin_version: ${{ steps.detect.outputs.changed_plugin_version }}
|
||||||
changed_plugin_count: ${{ steps.detect.outputs.changed_plugin_count }}
|
changed_plugin_count: ${{ steps.detect.outputs.changed_plugin_count }}
|
||||||
release_notes: ${{ steps.detect.outputs.release_notes }}
|
release_notes: ${{ steps.detect.outputs.release_notes }}
|
||||||
has_doc_changes: ${{ steps.detect.outputs.has_doc_changes }}
|
has_doc_changes: ${{ steps.detect.outputs.has_doc_changes }}
|
||||||
@@ -95,12 +98,12 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
# Always compare against the most recent previously released version.
|
# Always compare against the most recent previously released version.
|
||||||
CURRENT_TAG=""
|
CURRENT_TAG=""
|
||||||
if [[ "${GITHUB_REF}" == refs/tags/v* ]]; then
|
if [[ "${GITHUB_REF}" == refs/tags/* ]]; then
|
||||||
CURRENT_TAG="${GITHUB_REF#refs/tags/}"
|
CURRENT_TAG="${GITHUB_REF#refs/tags/}"
|
||||||
echo "Current tag event detected: $CURRENT_TAG"
|
echo "Current tag event detected: $CURRENT_TAG"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
PREVIOUS_RELEASE_TAG=$(git tag --sort=-creatordate | grep -E '^v' | grep -Fxv "$CURRENT_TAG" | head -n1 || true)
|
PREVIOUS_RELEASE_TAG=$(git tag --sort=-creatordate | grep -Fxv "$CURRENT_TAG" | head -n1 || true)
|
||||||
|
|
||||||
if [ -n "$PREVIOUS_RELEASE_TAG" ]; then
|
if [ -n "$PREVIOUS_RELEASE_TAG" ]; then
|
||||||
echo "Comparing with previous release tag: $PREVIOUS_RELEASE_TAG"
|
echo "Comparing with previous release tag: $PREVIOUS_RELEASE_TAG"
|
||||||
@@ -162,22 +165,80 @@ jobs:
|
|||||||
# Only trigger release if there are actual version changes, not just doc changes
|
# Only trigger release if there are actual version changes, not just doc changes
|
||||||
echo "has_changes=false" >> $GITHUB_OUTPUT
|
echo "has_changes=false" >> $GITHUB_OUTPUT
|
||||||
echo "changed_plugins=" >> $GITHUB_OUTPUT
|
echo "changed_plugins=" >> $GITHUB_OUTPUT
|
||||||
echo "changed_plugin_titles=" >> $GITHUB_OUTPUT
|
echo "changed_plugin_title=" >> $GITHUB_OUTPUT
|
||||||
|
echo "changed_plugin_slug=" >> $GITHUB_OUTPUT
|
||||||
|
echo "changed_plugin_version=" >> $GITHUB_OUTPUT
|
||||||
echo "changed_plugin_count=0" >> $GITHUB_OUTPUT
|
echo "changed_plugin_count=0" >> $GITHUB_OUTPUT
|
||||||
else
|
else
|
||||||
echo "has_changes=true" >> $GITHUB_OUTPUT
|
echo "has_changes=true" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
# Extract changed plugin file paths and titles using Python
|
|
||||||
python3 -c "import json; data = json.load(open('changes.json', 'r')); get_title = lambda plugin: ((plugin.get('data', {}).get('function', {}).get('meta', {}).get('manifest', {}).get('title')) or plugin.get('title') or '').strip(); files = []; [files.append(plugin['file_path']) for plugin in data.get('added', []) if plugin.get('file_path')]; [files.append(update['current']['file_path']) for update in data.get('updated', []) if update.get('current', {}).get('file_path')]; print('\\n'.join(files))" > changed_files.txt
|
|
||||||
|
|
||||||
python3 -c "import json; data = json.load(open('changes.json', 'r')); get_title = lambda plugin: ((plugin.get('data', {}).get('function', {}).get('meta', {}).get('manifest', {}).get('title')) or plugin.get('title') or '').strip(); titles = []; [titles.append(title) for plugin in data.get('added', []) for title in [get_title(plugin)] if title and title not in titles]; [titles.append(title) for update in data.get('updated', []) for title in [get_title(update.get('current', {}))] if title and title not in titles]; print(', '.join(titles)); open('changed_plugin_count.txt', 'w').write(str(len(titles)))" > changed_plugin_titles.txt
|
# Extract changed plugin metadata and enforce a single-plugin release.
|
||||||
|
python3 <<'PY'
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
data = json.load(open('changes.json', 'r', encoding='utf-8'))
|
||||||
|
|
||||||
|
def get_plugin_meta(plugin):
|
||||||
|
manifest = plugin.get('data', {}).get('function', {}).get('meta', {}).get('manifest', {})
|
||||||
|
title = (manifest.get('title') or plugin.get('title') or '').strip()
|
||||||
|
version = (manifest.get('version') or plugin.get('version') or '').strip()
|
||||||
|
file_path = (plugin.get('file_path') or '').strip()
|
||||||
|
slug = Path(file_path).parent.name.replace('_', '-').strip() if file_path else ''
|
||||||
|
return {
|
||||||
|
'title': title,
|
||||||
|
'slug': slug,
|
||||||
|
'version': version,
|
||||||
|
'file_path': file_path,
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins = []
|
||||||
|
seen_keys = set()
|
||||||
|
|
||||||
|
for plugin in data.get('added', []):
|
||||||
|
meta = get_plugin_meta(plugin)
|
||||||
|
key = meta['file_path'] or meta['title']
|
||||||
|
if key and key not in seen_keys:
|
||||||
|
plugins.append(meta)
|
||||||
|
seen_keys.add(key)
|
||||||
|
|
||||||
|
for update in data.get('updated', []):
|
||||||
|
meta = get_plugin_meta(update.get('current', {}))
|
||||||
|
key = meta['file_path'] or meta['title']
|
||||||
|
if key and key not in seen_keys:
|
||||||
|
plugins.append(meta)
|
||||||
|
seen_keys.add(key)
|
||||||
|
|
||||||
|
Path('changed_files.txt').write_text(
|
||||||
|
'\n'.join(meta['file_path'] for meta in plugins if meta['file_path']),
|
||||||
|
encoding='utf-8',
|
||||||
|
)
|
||||||
|
Path('changed_plugin_count.txt').write_text(str(len(plugins)), encoding='utf-8')
|
||||||
|
|
||||||
|
if len(plugins) > 1:
|
||||||
|
print('Error: release workflow only supports one plugin creation/update per release.', file=sys.stderr)
|
||||||
|
for meta in plugins:
|
||||||
|
print(
|
||||||
|
f"- {meta['title'] or 'Unknown'} v{meta['version'] or '?'} ({meta['file_path'] or 'unknown path'})",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
selected = plugins[0] if plugins else {'title': '', 'slug': '', 'version': ''}
|
||||||
|
Path('changed_plugin_title.txt').write_text(selected['title'], encoding='utf-8')
|
||||||
|
Path('changed_plugin_slug.txt').write_text(selected['slug'], encoding='utf-8')
|
||||||
|
Path('changed_plugin_version.txt').write_text(selected['version'], encoding='utf-8')
|
||||||
|
PY
|
||||||
|
|
||||||
echo "changed_plugins<<EOF" >> $GITHUB_OUTPUT
|
echo "changed_plugins<<EOF" >> $GITHUB_OUTPUT
|
||||||
cat changed_files.txt >> $GITHUB_OUTPUT
|
cat changed_files.txt >> $GITHUB_OUTPUT
|
||||||
echo "" >> $GITHUB_OUTPUT
|
echo "" >> $GITHUB_OUTPUT
|
||||||
echo "EOF" >> $GITHUB_OUTPUT
|
echo "EOF" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
echo "changed_plugin_titles=$(cat changed_plugin_titles.txt)" >> $GITHUB_OUTPUT
|
echo "changed_plugin_title=$(cat changed_plugin_title.txt)" >> $GITHUB_OUTPUT
|
||||||
|
echo "changed_plugin_slug=$(cat changed_plugin_slug.txt)" >> $GITHUB_OUTPUT
|
||||||
|
echo "changed_plugin_version=$(cat changed_plugin_version.txt)" >> $GITHUB_OUTPUT
|
||||||
echo "changed_plugin_count=$(cat changed_plugin_count.txt)" >> $GITHUB_OUTPUT
|
echo "changed_plugin_count=$(cat changed_plugin_count.txt)" >> $GITHUB_OUTPUT
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -191,7 +252,7 @@ jobs:
|
|||||||
|
|
||||||
release:
|
release:
|
||||||
needs: check-changes
|
needs: check-changes
|
||||||
if: needs.check-changes.outputs.has_changes == 'true' || github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/tags/v')
|
if: needs.check-changes.outputs.has_changes == 'true' || github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/tags/')
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
LANG: en_US.UTF-8
|
LANG: en_US.UTF-8
|
||||||
@@ -219,53 +280,34 @@ jobs:
|
|||||||
id: version
|
id: version
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
CHANGED_PLUGIN_SLUG: ${{ needs.check-changes.outputs.changed_plugin_slug }}
|
||||||
|
CHANGED_PLUGIN_VERSION: ${{ needs.check-changes.outputs.changed_plugin_version }}
|
||||||
run: |
|
run: |
|
||||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ -n "${{ github.event.inputs.version }}" ]; then
|
if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ -n "${{ github.event.inputs.version }}" ]; then
|
||||||
VERSION="${{ github.event.inputs.version }}"
|
VERSION="${{ github.event.inputs.version }}"
|
||||||
elif [[ "${{ github.ref }}" == refs/tags/v* ]]; then
|
elif [[ "${{ github.ref }}" == refs/tags/* ]]; then
|
||||||
VERSION="${GITHUB_REF#refs/tags/}"
|
VERSION="${GITHUB_REF#refs/tags/}"
|
||||||
|
elif [ -n "$CHANGED_PLUGIN_SLUG" ] && [ -n "$CHANGED_PLUGIN_VERSION" ]; then
|
||||||
|
VERSION="${CHANGED_PLUGIN_SLUG}-v${CHANGED_PLUGIN_VERSION}"
|
||||||
else
|
else
|
||||||
# Auto-generate version based on date and daily release count
|
echo "Error: failed to determine plugin-scoped release tag." >&2
|
||||||
TODAY=$(date +'%Y.%m.%d')
|
exit 1
|
||||||
TODAY_PREFIX="v${TODAY}-"
|
|
||||||
|
|
||||||
# Count existing releases with today's date prefix
|
|
||||||
# grep -c returns 1 if count is 0, so we use || true to avoid script failure
|
|
||||||
EXISTING_COUNT=$(gh release list --limit 100 2>/dev/null | grep -c "^${TODAY_PREFIX}" || true)
|
|
||||||
|
|
||||||
# Clean up output (handle potential newlines or fallback issues)
|
|
||||||
EXISTING_COUNT=$(echo "$EXISTING_COUNT" | tr -cd '0-9')
|
|
||||||
if [ -z "$EXISTING_COUNT" ]; then EXISTING_COUNT=0; fi
|
|
||||||
|
|
||||||
NEXT_NUM=$((EXISTING_COUNT + 1))
|
|
||||||
|
|
||||||
VERSION="${TODAY_PREFIX}${NEXT_NUM}"
|
|
||||||
|
|
||||||
# Final fallback to ensure VERSION is never empty
|
|
||||||
if [ -z "$VERSION" ]; then
|
|
||||||
VERSION="v$(date +'%Y.%m.%d-%H%M%S')"
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
echo "Release version: $VERSION"
|
echo "Release tag: $VERSION"
|
||||||
|
|
||||||
- name: Build release metadata
|
- name: Build release metadata
|
||||||
id: meta
|
id: meta
|
||||||
env:
|
env:
|
||||||
VERSION: ${{ steps.version.outputs.version }}
|
VERSION: ${{ steps.version.outputs.version }}
|
||||||
INPUT_TITLE: ${{ github.event.inputs.release_title }}
|
INPUT_TITLE: ${{ github.event.inputs.release_title }}
|
||||||
CHANGED_PLUGIN_TITLES: ${{ needs.check-changes.outputs.changed_plugin_titles }}
|
CHANGED_PLUGIN_TITLE: ${{ needs.check-changes.outputs.changed_plugin_title }}
|
||||||
CHANGED_PLUGIN_COUNT: ${{ needs.check-changes.outputs.changed_plugin_count }}
|
CHANGED_PLUGIN_VERSION: ${{ needs.check-changes.outputs.changed_plugin_version }}
|
||||||
run: |
|
run: |
|
||||||
if [ -n "$INPUT_TITLE" ]; then
|
if [ -n "$INPUT_TITLE" ]; then
|
||||||
RELEASE_NAME="$INPUT_TITLE"
|
RELEASE_NAME="$INPUT_TITLE"
|
||||||
elif [ "$CHANGED_PLUGIN_COUNT" = "1" ] && [ -n "$CHANGED_PLUGIN_TITLES" ]; then
|
elif [ -n "$CHANGED_PLUGIN_TITLE" ] && [ -n "$CHANGED_PLUGIN_VERSION" ]; then
|
||||||
RELEASE_NAME="$CHANGED_PLUGIN_TITLES $VERSION"
|
RELEASE_NAME="$CHANGED_PLUGIN_TITLE v$CHANGED_PLUGIN_VERSION"
|
||||||
elif [ -n "$CHANGED_PLUGIN_TITLES" ] && [ "$CHANGED_PLUGIN_COUNT" = "2" ]; then
|
|
||||||
RELEASE_NAME="$VERSION - $CHANGED_PLUGIN_TITLES"
|
|
||||||
elif [ -n "$CHANGED_PLUGIN_TITLES" ] && [ "${CHANGED_PLUGIN_COUNT:-0}" -gt 2 ]; then
|
|
||||||
FIRST_PLUGIN=$(echo "$CHANGED_PLUGIN_TITLES" | cut -d',' -f1 | xargs)
|
|
||||||
RELEASE_NAME="$VERSION - $FIRST_PLUGIN and $CHANGED_PLUGIN_COUNT plugin updates"
|
|
||||||
else
|
else
|
||||||
RELEASE_NAME="$VERSION"
|
RELEASE_NAME="$VERSION"
|
||||||
fi
|
fi
|
||||||
@@ -391,12 +433,22 @@ jobs:
|
|||||||
VERSION: ${{ steps.version.outputs.version }}
|
VERSION: ${{ steps.version.outputs.version }}
|
||||||
TITLE: ${{ github.event.inputs.release_title }}
|
TITLE: ${{ github.event.inputs.release_title }}
|
||||||
NOTES: ${{ github.event.inputs.release_notes }}
|
NOTES: ${{ github.event.inputs.release_notes }}
|
||||||
|
CHANGED_PLUGIN_TITLE: ${{ needs.check-changes.outputs.changed_plugin_title }}
|
||||||
|
CHANGED_PLUGIN_VERSION: ${{ needs.check-changes.outputs.changed_plugin_version }}
|
||||||
DETECTED_CHANGES: ${{ needs.check-changes.outputs.release_notes }}
|
DETECTED_CHANGES: ${{ needs.check-changes.outputs.release_notes }}
|
||||||
COMMITS: ${{ steps.commits.outputs.commits }}
|
COMMITS: ${{ steps.commits.outputs.commits }}
|
||||||
DOC_FILES: ${{ needs.check-changes.outputs.changed_doc_files }}
|
DOC_FILES: ${{ needs.check-changes.outputs.changed_doc_files }}
|
||||||
run: |
|
run: |
|
||||||
> release_notes.md
|
> release_notes.md
|
||||||
|
|
||||||
|
if [ -n "$CHANGED_PLUGIN_TITLE" ] && [ -n "$CHANGED_PLUGIN_VERSION" ]; then
|
||||||
|
echo "# $CHANGED_PLUGIN_TITLE v$CHANGED_PLUGIN_VERSION" >> release_notes.md
|
||||||
|
echo "" >> release_notes.md
|
||||||
|
elif [ -n "$TITLE" ]; then
|
||||||
|
echo "# $TITLE" >> release_notes.md
|
||||||
|
echo "" >> release_notes.md
|
||||||
|
fi
|
||||||
|
|
||||||
# 1. Release notes from v*.md files (highest priority, shown first)
|
# 1. Release notes from v*.md files (highest priority, shown first)
|
||||||
if [ -n "$DOC_FILES" ]; then
|
if [ -n "$DOC_FILES" ]; then
|
||||||
RELEASE_NOTE_FILES=$(echo "$DOC_FILES" | grep -E '^plugins/.*/v[^/]*\.md$' | grep -v '_CN\.md$' || true)
|
RELEASE_NOTE_FILES=$(echo "$DOC_FILES" | grep -E '^plugins/.*/v[^/]*\.md$' | grep -v '_CN\.md$' || true)
|
||||||
@@ -404,7 +456,7 @@ jobs:
|
|||||||
while IFS= read -r file; do
|
while IFS= read -r file; do
|
||||||
[ -z "$file" ] && continue
|
[ -z "$file" ] && continue
|
||||||
if [ -f "$file" ]; then
|
if [ -f "$file" ]; then
|
||||||
python3 -c "import json, pathlib, re; file_path = pathlib.Path(r'''$file'''); plugin_versions_path = pathlib.Path('plugin_versions.json'); text = file_path.read_text(encoding='utf-8'); plugin_dir = file_path.parent.as_posix(); plugins = json.loads(plugin_versions_path.read_text(encoding='utf-8')) if plugin_versions_path.exists() else []; plugin_title = next((plugin.get('title', '').strip() for plugin in plugins if plugin.get('file_path', '').startswith(plugin_dir + '/')), ''); text = re.sub(r'^#\\s+(v[0-9][^\\n]*Release Notes)\\s*$', '# ' + plugin_title + ' ' + r'\\1', text, count=1, flags=re.MULTILINE) if plugin_title else text; print(text.rstrip())" >> release_notes.md
|
python3 -c "import pathlib, re; file_path = pathlib.Path(r'''$file'''); text = file_path.read_text(encoding='utf-8'); text = re.sub(r'^#\\s+.+?(?:\\r?\\n)+', '', text, count=1, flags=re.MULTILINE); print(text.lstrip().rstrip())" >> release_notes.md
|
||||||
echo "" >> release_notes.md
|
echo "" >> release_notes.md
|
||||||
fi
|
fi
|
||||||
done <<< "$RELEASE_NOTE_FILES"
|
done <<< "$RELEASE_NOTE_FILES"
|
||||||
@@ -412,7 +464,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# 2. Plugin version changes detected by script
|
# 2. Plugin version changes detected by script
|
||||||
if [ -n "$TITLE" ]; then
|
if [ -z "$CHANGED_PLUGIN_TITLE" ] && [ -z "$CHANGED_PLUGIN_VERSION" ] && [ -n "$TITLE" ]; then
|
||||||
echo "## $TITLE" >> release_notes.md
|
echo "## $TITLE" >> release_notes.md
|
||||||
echo "" >> release_notes.md
|
echo "" >> release_notes.md
|
||||||
fi
|
fi
|
||||||
@@ -469,7 +521,7 @@ jobs:
|
|||||||
cat release_notes.md
|
cat release_notes.md
|
||||||
|
|
||||||
- name: Create Git Tag
|
- name: Create Git Tag
|
||||||
if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
|
if: ${{ !startsWith(github.ref, 'refs/tags/') }}
|
||||||
run: |
|
run: |
|
||||||
VERSION="${{ steps.version.outputs.version }}"
|
VERSION="${{ steps.version.outputs.version }}"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user