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:
|
||||
# - Push to main branch when plugins are modified (auto-release)
|
||||
# - Manual trigger (workflow_dispatch) with custom release notes
|
||||
# - Push of version tags (v*)
|
||||
# - Push of plugin version tags (<plugin>-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
|
||||
# 4. Enforces one plugin creation/update per release
|
||||
|
||||
name: Plugin Release
|
||||
|
||||
@@ -28,13 +28,14 @@ on:
|
||||
- 'plugins/**/v*_CN.md'
|
||||
- 'docs/plugins/**/*.md'
|
||||
tags:
|
||||
- '*-v*'
|
||||
- 'v*'
|
||||
|
||||
# Manual trigger with inputs
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
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
|
||||
type: string
|
||||
release_title:
|
||||
@@ -65,7 +66,9 @@ jobs:
|
||||
outputs:
|
||||
has_changes: ${{ steps.detect.outputs.has_changes }}
|
||||
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 }}
|
||||
release_notes: ${{ steps.detect.outputs.release_notes }}
|
||||
has_doc_changes: ${{ steps.detect.outputs.has_doc_changes }}
|
||||
@@ -95,12 +98,12 @@ jobs:
|
||||
run: |
|
||||
# Always compare against the most recent previously released version.
|
||||
CURRENT_TAG=""
|
||||
if [[ "${GITHUB_REF}" == refs/tags/v* ]]; then
|
||||
if [[ "${GITHUB_REF}" == refs/tags/* ]]; then
|
||||
CURRENT_TAG="${GITHUB_REF#refs/tags/}"
|
||||
echo "Current tag event detected: $CURRENT_TAG"
|
||||
fi
|
||||
|
||||
PREVIOUS_RELEASE_TAG=$(git tag --sort=-creatordate | grep -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
|
||||
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
|
||||
echo "has_changes=false" >> $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
|
||||
else
|
||||
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
|
||||
cat changed_files.txt >> $GITHUB_OUTPUT
|
||||
echo "" >> $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
|
||||
fi
|
||||
|
||||
@@ -191,7 +252,7 @@ jobs:
|
||||
|
||||
release:
|
||||
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
|
||||
env:
|
||||
LANG: en_US.UTF-8
|
||||
@@ -219,53 +280,34 @@ jobs:
|
||||
id: version
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CHANGED_PLUGIN_SLUG: ${{ needs.check-changes.outputs.changed_plugin_slug }}
|
||||
CHANGED_PLUGIN_VERSION: ${{ needs.check-changes.outputs.changed_plugin_version }}
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ -n "${{ github.event.inputs.version }}" ]; then
|
||||
VERSION="${{ github.event.inputs.version }}"
|
||||
elif [[ "${{ github.ref }}" == refs/tags/v* ]]; then
|
||||
elif [[ "${{ github.ref }}" == refs/tags/* ]]; then
|
||||
VERSION="${GITHUB_REF#refs/tags/}"
|
||||
elif [ -n "$CHANGED_PLUGIN_SLUG" ] && [ -n "$CHANGED_PLUGIN_VERSION" ]; then
|
||||
VERSION="${CHANGED_PLUGIN_SLUG}-v${CHANGED_PLUGIN_VERSION}"
|
||||
else
|
||||
# Auto-generate version based on date and daily release count
|
||||
TODAY=$(date +'%Y.%m.%d')
|
||||
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
|
||||
echo "Error: failed to determine plugin-scoped release tag." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "Release version: $VERSION"
|
||||
echo "Release tag: $VERSION"
|
||||
|
||||
- name: Build release metadata
|
||||
id: meta
|
||||
env:
|
||||
VERSION: ${{ steps.version.outputs.version }}
|
||||
INPUT_TITLE: ${{ github.event.inputs.release_title }}
|
||||
CHANGED_PLUGIN_TITLES: ${{ needs.check-changes.outputs.changed_plugin_titles }}
|
||||
CHANGED_PLUGIN_COUNT: ${{ needs.check-changes.outputs.changed_plugin_count }}
|
||||
CHANGED_PLUGIN_TITLE: ${{ needs.check-changes.outputs.changed_plugin_title }}
|
||||
CHANGED_PLUGIN_VERSION: ${{ needs.check-changes.outputs.changed_plugin_version }}
|
||||
run: |
|
||||
if [ -n "$INPUT_TITLE" ]; then
|
||||
RELEASE_NAME="$INPUT_TITLE"
|
||||
elif [ "$CHANGED_PLUGIN_COUNT" = "1" ] && [ -n "$CHANGED_PLUGIN_TITLES" ]; then
|
||||
RELEASE_NAME="$CHANGED_PLUGIN_TITLES $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"
|
||||
elif [ -n "$CHANGED_PLUGIN_TITLE" ] && [ -n "$CHANGED_PLUGIN_VERSION" ]; then
|
||||
RELEASE_NAME="$CHANGED_PLUGIN_TITLE v$CHANGED_PLUGIN_VERSION"
|
||||
else
|
||||
RELEASE_NAME="$VERSION"
|
||||
fi
|
||||
@@ -391,12 +433,22 @@ jobs:
|
||||
VERSION: ${{ steps.version.outputs.version }}
|
||||
TITLE: ${{ github.event.inputs.release_title }}
|
||||
NOTES: ${{ github.event.inputs.release_notes }}
|
||||
CHANGED_PLUGIN_TITLE: ${{ needs.check-changes.outputs.changed_plugin_title }}
|
||||
CHANGED_PLUGIN_VERSION: ${{ needs.check-changes.outputs.changed_plugin_version }}
|
||||
DETECTED_CHANGES: ${{ needs.check-changes.outputs.release_notes }}
|
||||
COMMITS: ${{ steps.commits.outputs.commits }}
|
||||
DOC_FILES: ${{ needs.check-changes.outputs.changed_doc_files }}
|
||||
run: |
|
||||
> 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)
|
||||
if [ -n "$DOC_FILES" ]; then
|
||||
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
|
||||
[ -z "$file" ] && continue
|
||||
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
|
||||
fi
|
||||
done <<< "$RELEASE_NOTE_FILES"
|
||||
@@ -412,7 +464,7 @@ jobs:
|
||||
fi
|
||||
|
||||
# 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 "" >> release_notes.md
|
||||
fi
|
||||
@@ -469,7 +521,7 @@ jobs:
|
||||
cat release_notes.md
|
||||
|
||||
- name: Create Git Tag
|
||||
if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
|
||||
if: ${{ !startsWith(github.ref, 'refs/tags/') }}
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user