From 5b5c0c6eb0aa7134843e8f1aae1bcbc6916caa31 Mon Sep 17 00:00:00 2001 From: Seth Van Niekerk Date: Wed, 3 Jun 2026 08:47:50 -0400 Subject: [PATCH 1/4] feat: migration to gh releases --- .github/scripts/publish/build-zips.sh | 91 +++- .github/scripts/publish/cleanup.sh | 50 +- .github/scripts/publish/generate-manifest.sh | 55 ++- .github/scripts/publish/plugin-readmes.sh | 111 ++--- .github/scripts/publish/run.sh | 48 +- .github/scripts/publish/yank-version.sh | 58 ++- .github/workflows/run-migrations.yml | 466 +++++++++++++++++++ CONTRIBUTING.md | 75 +-- README.md | 37 +- 9 files changed, 804 insertions(+), 187 deletions(-) create mode 100644 .github/workflows/run-migrations.yml diff --git a/.github/scripts/publish/build-zips.sh b/.github/scripts/publish/build-zips.sh index a54b04b..01381f9 100644 --- a/.github/scripts/publish/build-zips.sh +++ b/.github/scripts/publish/build-zips.sh @@ -6,14 +6,14 @@ set -e # Per-version metadata is written to a temporary working directory (BUILD_META_DIR) # so generate-manifest.sh can consume it within this CI run without persisting # per-version JSON files to the releases branch. -# Skips plugins whose current version already has a ZIP and an entry in the -# existing per-plugin manifest. +# Skips plugins whose current version already has a GitHub Release tag. +# Uploads each new ZIP to GitHub Releases (versioned tag + -latest alias tag). # Writes changed_plugins.txt to cwd (one "name@version" per line). # # Called from the releases branch checkout directory by publish-plugins.sh. -# Required env: SOURCE_BRANCH, RELEASES_BRANCH, GITHUB_REPOSITORY +# Required env: SOURCE_BRANCH, RELEASES_BRANCH, GITHUB_REPOSITORY, GITHUB_TOKEN -: "${SOURCE_BRANCH:?}" "${RELEASES_BRANCH:?}" "${GITHUB_REPOSITORY:?}" "${BUILD_META_DIR:?}" +: "${SOURCE_BRANCH:?}" "${RELEASES_BRANCH:?}" "${GITHUB_REPOSITORY:?}" "${BUILD_META_DIR:?}" "${GITHUB_TOKEN:?}" > changed_plugins.txt @@ -25,16 +25,15 @@ for plugin_dir in plugins/*/; do mkdir -p "zips/$plugin_name" - zip_path="zips/$plugin_name/${plugin_name}-${version}.zip" + zip_path="/tmp/${plugin_name}-${version}.zip" existing_manifest="zips/$plugin_name/manifest.json" + release_tag="${plugin_name}-${version}" - # Skip if ZIP exists and the version is already in the existing manifest - if [[ -f "$zip_path" ]]; then - if [[ -f "$existing_manifest" ]] && \ - jq -e --arg v "$version" '.manifest.versions[]? | select(.version == $v)' "$existing_manifest" >/dev/null 2>&1; then - echo " $plugin_name v$version - skipping (already exists)" - continue - fi + # Skip if a GitHub Release already exists for this version. + # The release is the source of truth; the manifest is regenerated from it. + if gh release view "$release_tag" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then + echo " $plugin_name v$version - skipping (release already exists)" + continue fi source_type=$(jq -r '.source_type // "local"' "$plugin_dir/plugin.json") @@ -45,10 +44,22 @@ for plugin_dir in plugins/*/; do source_url_resolved="${source_url_template//\{version\}/$version}" echo " $plugin_name v$version - fetching external ZIP from $source_url_resolved" echo "$plugin_key@$version" >> changed_plugins.txt - curl -fsSL "$source_url_resolved" -o "$zip_path" || { - echo "::error::Failed to download external ZIP from $source_url_resolved" + download_ok=false + for attempt in 1 2 3; do + if curl -fsSL "$source_url_resolved" -o "$zip_path"; then + download_ok=true + break + fi + rm -f "$zip_path" + if [[ "$attempt" -lt 3 ]]; then + echo " Download attempt $attempt failed, retrying in 15s..." + sleep 15 + fi + done + if [[ "$download_ok" != "true" ]]; then + echo "::error::Failed to download external ZIP from $source_url_resolved after 3 attempts" exit 1 - } + fi commit_sha="" commit_sha_short="" last_updated="$build_timestamp" @@ -61,11 +72,10 @@ for plugin_dir in plugins/*/; do || date -u +"%Y-%m-%dT%H:%M:%SZ") source_url_resolved="" ( - abspath="$(pwd)/$zip_path" tmpdir=$(mktemp -d) trap 'rm -rf "$tmpdir"' EXIT cp -r "plugins/$plugin_name" "$tmpdir/$plugin_key" - cd "$tmpdir" && zip -r "$abspath" "$plugin_key" -q + cd "$tmpdir" && zip -r "$zip_path" "$plugin_key" -q ) fi @@ -75,6 +85,9 @@ for plugin_dir in plugins/*/; do min_da_version=$(jq -r '.min_dispatcharr_version // ""' "$plugin_dir/plugin.json") max_da_version=$(jq -r '.max_dispatcharr_version // ""' "$plugin_dir/plugin.json") + zip_size_bytes=$(stat -f%z "$zip_path" 2>/dev/null || stat -c%s "$zip_path" 2>/dev/null || echo 0) + zip_size_kb=$(( zip_size_bytes / 1024 )) + mkdir -p "$BUILD_META_DIR/$plugin_key" jq -n \ --arg version "$version" \ @@ -87,7 +100,9 @@ for plugin_dir in plugins/*/; do --arg min_da_version "$min_da_version" \ --arg max_da_version "$max_da_version" \ --arg source_url "$source_url_resolved" \ - '{ version: $version, + --argjson size_kb "$zip_size_kb" \ + '{ + version: $version, commit_sha: (if $commit_sha != "" then $commit_sha else null end), commit_sha_short: (if $commit_sha_short != "" then $commit_sha_short else null end), build_timestamp: $build_timestamp, @@ -96,11 +111,47 @@ for plugin_dir in plugins/*/; do checksum_sha256: $checksum_sha256, min_dispatcharr_version: (if $min_da_version != "" then $min_da_version else null end), max_dispatcharr_version: (if $max_da_version != "" then $max_da_version else null end), - source_url: (if $source_url != "" then $source_url else null end) + source_url: (if $source_url != "" then $source_url else null end), + size_kb: $size_kb } | with_entries(select(.value != null))' \ > "$BUILD_META_DIR/$plugin_key/${plugin_key}-${version}.json" - cp "$zip_path" "zips/$plugin_name/${plugin_name}-latest.zip" + # Build release notes + readme_url="https://github.com/${GITHUB_REPOSITORY}/blob/releases/zips/${plugin_name}/README.md" + release_notes="" + if [[ -n "$commit_sha" ]]; then + commit_url="https://github.com/${GITHUB_REPOSITORY}/commit/${commit_sha}" + release_notes="**Commit:** [\`${commit_sha_short}\`](${commit_url})" + pr_info=$(gh api "repos/${GITHUB_REPOSITORY}/commits/${commit_sha}/pulls" \ + --jq '.[0] | {number: .number, url: .html_url}' 2>/dev/null || echo '{}') + pr_number=$(echo "$pr_info" | jq -r '.number // empty') + pr_url=$(echo "$pr_info" | jq -r '.url // empty') + if [[ -n "$pr_number" && "$pr_number" != "null" ]]; then + release_notes+=$'\n'"**PR:** [#${pr_number}](${pr_url})" + fi + release_notes+=$'\n' + fi + release_notes+="**README:** [Plugin README](${readme_url})" + + # Upload versioned GitHub Release + echo " $plugin_name v$version - uploading to GitHub Releases" + gh release create "$release_tag" \ + --repo "$GITHUB_REPOSITORY" \ + --title "${plugin_name} v${version}" \ + --notes "$release_notes" \ + "$zip_path" + + # Delete and recreate the -latest alias release + latest_tag="${plugin_name}-latest" + latest_zip="/tmp/${plugin_name}-latest.zip" + gh release delete "$latest_tag" --repo "$GITHUB_REPOSITORY" --yes --cleanup-tag 2>/dev/null || true + cp "$zip_path" "$latest_zip" + gh release create "$latest_tag" \ + --repo "$GITHUB_REPOSITORY" \ + --title "${plugin_name} latest (v${version})" \ + --notes "$release_notes" \ + "$latest_zip" + rm -f "$latest_zip" "$zip_path" done changed=$(wc -l < changed_plugins.txt | tr -d ' ') diff --git a/.github/scripts/publish/cleanup.sh b/.github/scripts/publish/cleanup.sh index 3010e8f..b9b704c 100644 --- a/.github/scripts/publish/cleanup.sh +++ b/.github/scripts/publish/cleanup.sh @@ -2,39 +2,57 @@ set -e # publish-cleanup.sh -# Removes release artifacts for plugins that no longer exist in source, -# and prunes versioned ZIPs beyond MAX_VERSIONED_ZIPS. +# Removes GitHub Releases for plugins that no longer exist in source, +# and prunes versioned releases beyond MAX_VERSIONED_ZIPS. # # Called from the releases branch checkout directory by publish-plugins.sh. -# Required env: SOURCE_BRANCH +# Required env: SOURCE_BRANCH, GITHUB_REPOSITORY, GITHUB_TOKEN # Optional env: MAX_VERSIONED_ZIPS (default: 10) -: "${SOURCE_BRANCH:?}" +: "${SOURCE_BRANCH:?}" "${GITHUB_REPOSITORY:?}" "${GITHUB_TOKEN:?}" MAX_VERSIONED_ZIPS=${MAX_VERSIONED_ZIPS:-10} -# Remove artifacts for deleted plugins +# Fetch all release tags once to avoid repeated API calls +all_tags=$(gh release list --repo "$GITHUB_REPOSITORY" --json tagName --limit 500 \ + | jq -r '.[].tagName') + +# Remove releases for deleted plugins if [[ -d zips ]]; then for release_dir in zips/*/; do [[ ! -d "$release_dir" ]] && continue plugin_name=$(basename "$release_dir") if [[ ! -d "plugins/$plugin_name" ]]; then - echo " Removing deleted plugin: $plugin_name" + echo " Removing deleted plugin releases: $plugin_name" + echo "$all_tags" | grep "^${plugin_name}-" | while IFS= read -r tag; do + echo " Deleting release $tag" + gh release delete "$tag" --repo "$GITHUB_REPOSITORY" --yes --cleanup-tag 2>/dev/null || true + done rm -rf "$release_dir" fi done fi -# Prune old versions per plugin +# Prune old versioned releases per plugin (keep MAX_VERSIONED_ZIPS most recent) for plugin_dir in plugins/*/; do [[ ! -d "$plugin_dir" ]] && continue plugin_name=$(basename "$plugin_dir") - zip_dir="zips/$plugin_name" - - # Remove oldest ZIPs beyond the limit - while IFS= read -r old_zip; do - echo " Removed $plugin_name $(basename "$old_zip") (over limit)" - rm -f "$old_zip" - done < <(ls -1t "$zip_dir/${plugin_name}-"*.zip 2>/dev/null \ - | grep -v "${plugin_name}-latest.zip" \ - | awk "NR>$MAX_VERSIONED_ZIPS") + + # Get versioned tags for this plugin (exclude -latest), sorted newest-first by semver + versioned_tags=$(echo "$all_tags" \ + | grep "^${plugin_name}-" \ + | grep -v "^${plugin_name}-latest$" \ + | sed "s/^${plugin_name}-//" \ + | sort -V -r \ + | sed "s/^/${plugin_name}-/") + + tag_count=$(echo "$versioned_tags" | grep -c . || true) + if (( tag_count <= MAX_VERSIONED_ZIPS )); then + continue + fi + + # Delete tags beyond the limit + echo "$versioned_tags" | awk "NR>$MAX_VERSIONED_ZIPS" | while IFS= read -r old_tag; do + echo " Removed release $old_tag (over limit of $MAX_VERSIONED_ZIPS)" + gh release delete "$old_tag" --repo "$GITHUB_REPOSITORY" --yes --cleanup-tag 2>/dev/null || true + done done diff --git a/.github/scripts/publish/generate-manifest.sh b/.github/scripts/publish/generate-manifest.sh index d0b9974..7fcb5ec 100644 --- a/.github/scripts/publish/generate-manifest.sh +++ b/.github/scripts/publish/generate-manifest.sh @@ -7,12 +7,13 @@ set -e # Called from the releases branch checkout directory by publish-plugins.sh. # Required env: SOURCE_BRANCH, RELEASES_BRANCH, GITHUB_REPOSITORY -: "${SOURCE_BRANCH:?}" "${RELEASES_BRANCH:?}" "${GITHUB_REPOSITORY:?}" +: "${SOURCE_BRANCH:?}" "${RELEASES_BRANCH:?}" "${GITHUB_REPOSITORY:?}" "${GITHUB_TOKEN:?}" generated_at="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" registry_url="https://github.com/${GITHUB_REPOSITORY}" registry_name="${GITHUB_REPOSITORY}" -root_url="https://raw.githubusercontent.com/${GITHUB_REPOSITORY}/${RELEASES_BRANCH}" +root_url="https://github.com/${GITHUB_REPOSITORY}/releases/download" +raw_releases_url="https://raw.githubusercontent.com/${GITHUB_REPOSITORY}/${RELEASES_BRANCH}" # GPG signing setup - optional; set GPG_PRIVATE_KEY (armored) and optionally GPG_PASSPHRASE gpg_key_id="" @@ -105,6 +106,11 @@ sign_manifest() { plugin_entries=() root_entries=() +# Fetch all release tags once to avoid per-plugin API calls +echo " Fetching release tags..." +all_release_tags=$(gh release list --repo "$GITHUB_REPOSITORY" --json tagName --limit 500 \ + | jq -r '.[].tagName') + for plugin_dir in plugins/*/; do plugin_file="$plugin_dir/plugin.json" [[ ! -f "$plugin_file" ]] && continue @@ -118,7 +124,8 @@ for plugin_dir in plugins/*/; do echo " $plugin_name" - latest_url="zips/${plugin_name}/${plugin_name}-latest.zip" + # -latest alias tag; URL combined with root_url gives the CDN-backed latest asset + latest_url="${plugin_name}-latest/${plugin_name}-latest.zip" versioned_zips="[]" latest_metadata="{}" @@ -127,17 +134,20 @@ for plugin_dir in plugins/*/; do # existing per-plugin manifest from previous run - used as metadata fallback existing_manifest_file="zips/$plugin_name/manifest.json" + mkdir -p "zips/$plugin_name" - while IFS= read -r zipfile; do - zip_basename=$(basename "$zipfile") - zip_version=$(echo "$zip_basename" | sed "s/${plugin_name}-\(.*\)\.zip/\1/") - zip_url="zips/${plugin_name}/${zip_basename}" - zip_size_bytes=$(stat -f%z "$zipfile" 2>/dev/null || stat -c%s "$zipfile" 2>/dev/null || echo 0) - zip_size_kb=$(( zip_size_bytes / 1024 )) - if [[ "$latest_size_set" == "false" ]]; then - latest_size_kb=$zip_size_kb - latest_size_set=true - fi + # Discover published versions from GitHub Releases (newest first) + versioned_tags=$(echo "$all_release_tags" \ + | grep "^${plugin_name}-" \ + | grep -v "^${plugin_name}-latest$" \ + | sed "s/^${plugin_name}-//" \ + | sort -V -r \ + | sed "s/^/${plugin_name}-/") + + while IFS= read -r release_tag; do + [[ -z "$release_tag" ]] && continue + zip_version="${release_tag#${plugin_name}-}" + zip_url="${plugin_name}-${zip_version}/${plugin_name}-${zip_version}.zip" # Fresh metadata from this run takes priority; fall back to existing manifest fresh_meta_file="${BUILD_META_DIR:-}/$plugin_key/${plugin_key}-${zip_version}.json" @@ -150,6 +160,16 @@ for plugin_dir in plugins/*/; do [[ -n "$meta_from_manifest" ]] && metadata="$meta_from_manifest" fi + # Size: prefer fresh metadata (stored by build-zips.sh), fall back to existing manifest + zip_size_kb=0 + if [[ "$metadata" != "{}" ]]; then + zip_size_kb=$(echo "$metadata" | jq -r '.size_kb // .size // 0') + fi + if [[ "$latest_size_set" == "false" ]]; then + latest_size_kb=$zip_size_kb + latest_size_set=true + fi + if [[ "$metadata" != "{}" ]]; then versioned_zips=$(jq --arg url "$zip_url" --argjson metadata "$metadata" --argjson size "$zip_size_kb" \ '. + [{ @@ -173,8 +193,7 @@ for plugin_dir in plugins/*/; do versioned_zips=$(jq --arg version "$zip_version" --arg url "$zip_url" --argjson size "$zip_size_kb" \ '. + [{version: $version, url: $url, size: $size}]' <<< "$versioned_zips") fi - done < <(ls -1 "zips/$plugin_name/${plugin_name}"-*.zip 2>/dev/null \ - | grep -v latest | sort -t- -k2 -V -r) + done <<< "$versioned_tags" # Overwrite min/max_dispatcharr_version for the current version's entry from plugin.json, # so metadata-only updates (no version bump) are reflected without a rebuild. @@ -249,7 +268,8 @@ for plugin_dir in plugins/*/; do desc_trimmed="$desc_raw" fi - plugin_manifest_url="zips/${plugin_name}/manifest.json" + # manifest_url is absolute: per-plugin manifest stays in the releases branch (raw.githubusercontent.com) + plugin_manifest_url="${raw_releases_url}/zips/${plugin_name}/manifest.json" root_entry=$(jq -n \ --argjson latest_metadata "$latest_metadata" \ @@ -264,6 +284,7 @@ for plugin_dir in plugins/*/; do --argjson latest_size_kb "$latest_size_kb" \ --arg min_da_version "$min_da_version" \ --arg max_da_version "$max_da_version" \ + --arg latest_url "${plugin_name}-latest/${plugin_name}-latest.zip" \ '{ slug: $slug, name: $name, @@ -276,7 +297,7 @@ for plugin_dir in plugins/*/; do latest_version: ($latest_metadata.version // null), latest_md5: ($latest_metadata.checksum_md5 // null), latest_sha256: ($latest_metadata.checksum_sha256 // null), - latest_url: ($versioned_zips[0].url // null), + latest_url: $latest_url, latest_size: (if $latest_size_kb > 0 then $latest_size_kb else null end), min_dispatcharr_version: (if $min_da_version != "" then $min_da_version else null end), max_dispatcharr_version: (if $max_da_version != "" then $max_da_version else null end) diff --git a/.github/scripts/publish/plugin-readmes.sh b/.github/scripts/publish/plugin-readmes.sh index 0945582..80310f0 100644 --- a/.github/scripts/publish/plugin-readmes.sh +++ b/.github/scripts/publish/plugin-readmes.sh @@ -3,6 +3,8 @@ set -e # publish-per-plugin-readmes.sh # Generates zips//README.md for every plugin. +# Version/metadata discovery is driven by the per-plugin manifest.json written +# by generate-manifest.sh (which runs before this script). No local ZIPs required. # # Called from the releases branch checkout directory by publish-plugins.sh. # Required env: SOURCE_BRANCH, RELEASES_BRANCH, GITHUB_REPOSITORY @@ -22,12 +24,21 @@ shields_encode() { printf '%s' "$s" } +# Read root_url from the root manifest (set by generate-manifest.sh) +root_url=$(jq -r '.manifest.root_url // ""' "manifest.json" 2>/dev/null || echo "") + for plugin_dir in plugins/*/; do [[ ! -d "$plugin_dir" ]] && continue plugin_name=$(basename "$plugin_dir") plugin_file="$plugin_dir/plugin.json" [[ ! -f "$plugin_file" ]] && continue + manifest_file="zips/$plugin_name/manifest.json" + if [[ ! -f "$manifest_file" ]]; then + echo " $plugin_name (no manifest, skipping README)" + continue + fi + name=$(jq -r '.name' "$plugin_file") description=$(jq -r '.description' "$plugin_file") author=$(jq -r '.author // ""' "$plugin_file") @@ -43,6 +54,11 @@ for plugin_dir in plugins/*/; do has_readme=false [[ -f "$plugin_dir/README.md" ]] && has_readme=true + # Read latest metadata from manifest + latest_url_path=$(jq -r '.manifest.latest.latest_url // empty' "$manifest_file") + latest_full_url="" + [[ -n "$root_url" && -n "$latest_url_path" ]] && latest_full_url="${root_url}/${latest_url_path}" + { echo "[Back to All Plugins](../../README.md)" echo "" @@ -87,40 +103,23 @@ for plugin_dir in plugins/*/; do echo "### Latest Release" echo "" - latest_zip="zips/$plugin_name/${plugin_name}-latest.zip" - if [[ -f "$latest_zip" ]]; then - latest_versioned=$(ls -1 "zips/$plugin_name/${plugin_name}"-*.zip 2>/dev/null \ - | grep -v latest | sort -t- -k2 -V -r | head -1) - if [[ -n "$latest_versioned" ]]; then - zip_basename=$(basename "$latest_versioned") - latest_version=$(echo "$zip_basename" | sed "s/${plugin_name}-\(.*\)\.zip/\1/") - manifest_file="zips/$plugin_name/manifest.json" - meta_entry="" - if [[ -f "$manifest_file" ]]; then - meta_entry=$(jq -c --arg v "$latest_version" \ - '.manifest.versions[]? | select(.version == $v)' "$manifest_file" 2>/dev/null || true) - fi - if [[ -n "$meta_entry" ]]; then - commit_sha=$(echo "$meta_entry" | jq -r '.commit_sha // empty') - commit_sha_short=$(echo "$meta_entry" | jq -r '.commit_sha_short // empty') - build_timestamp=$(echo "$meta_entry" | jq -r '.build_timestamp // empty') - checksum_md5=$(echo "$meta_entry" | jq -r '.checksum_md5 // empty') - checksum_sha256=$(echo "$meta_entry" | jq -r '.checksum_sha256 // empty') - - echo "- **Download:** [\`${plugin_name}-latest.zip\`](https://github.com/${GITHUB_REPOSITORY}/raw/$RELEASES_BRANCH/zips/${plugin_name}/${plugin_name}-latest.zip)" - [[ -n "$build_timestamp" ]] && echo "- **Built:** $(fmt_date "$build_timestamp")" - [[ -n "$commit_sha" ]] && echo "- **Source Commit:** [\`$commit_sha_short\`](https://github.com/${GITHUB_REPOSITORY}/commit/${commit_sha})" - if [[ -n "$checksum_md5" || -n "$checksum_sha256" ]]; then - echo "" - echo "**Checksums:**" - echo "\`\`\`" - [[ -n "$checksum_md5" ]] && echo "MD5: $checksum_md5" - [[ -n "$checksum_sha256" ]] && echo "SHA256: $checksum_sha256" - echo "\`\`\`" - fi - else - echo "- **Download:** [\`${plugin_name}-latest.zip\`](https://github.com/${GITHUB_REPOSITORY}/raw/$RELEASES_BRANCH/zips/${plugin_name}/${plugin_name}-latest.zip)" - fi + if [[ -n "$latest_full_url" ]]; then + latest_build_timestamp=$(jq -r '.manifest.latest.build_timestamp // empty' "$manifest_file") + latest_commit_sha=$(jq -r '.manifest.latest.commit_sha // empty' "$manifest_file") + latest_commit_sha_short=$(jq -r '.manifest.latest.commit_sha_short // empty' "$manifest_file") + latest_md5=$(jq -r '.manifest.latest.checksum_md5 // empty' "$manifest_file") + latest_sha256=$(jq -r '.manifest.latest.checksum_sha256 // empty' "$manifest_file") + + echo "- **Download:** [\`${plugin_name}-latest.zip\`](${latest_full_url})" + [[ -n "$latest_build_timestamp" ]] && echo "- **Built:** $(fmt_date "$latest_build_timestamp")" + [[ -n "$latest_commit_sha" ]] && echo "- **Source Commit:** [\`$latest_commit_sha_short\`](https://github.com/${GITHUB_REPOSITORY}/commit/${latest_commit_sha})" + if [[ -n "$latest_md5" || -n "$latest_sha256" ]]; then + echo "" + echo "**Checksums:**" + echo "\`\`\`" + [[ -n "$latest_md5" ]] && echo "MD5: $latest_md5" + [[ -n "$latest_sha256" ]] && echo "SHA256: $latest_sha256" + echo "\`\`\`" fi fi @@ -130,32 +129,24 @@ for plugin_dir in plugins/*/; do echo "| Version | Download | Built | Commit | MD5 | SHA256 |" echo "|---------|----------|-------|--------|-----|--------|" - manifest_file="zips/$plugin_name/manifest.json" - while IFS= read -r zipfile; do - zip_basename=$(basename "$zipfile") - version=$(echo "$zip_basename" | sed "s/${plugin_name}-\(.*\)\.zip/\1/") - - meta_entry="" - if [[ -f "$manifest_file" ]]; then - meta_entry=$(jq -c --arg v "$version" \ - '.manifest.versions[]? | select(.version == $v)' "$manifest_file" 2>/dev/null || true) - fi - - if [[ -n "$meta_entry" ]]; then - commit_sha_short=$(echo "$meta_entry" | jq -r '.commit_sha_short // empty') - commit_sha=$(echo "$meta_entry" | jq -r '.commit_sha // empty') - build_timestamp=$(echo "$meta_entry" | jq -r '.build_timestamp // empty') - checksum_md5=$(echo "$meta_entry" | jq -r '.checksum_md5 // empty') - checksum_sha256=$(echo "$meta_entry" | jq -r '.checksum_sha256 // empty') - build_date=$(fmt_date "$build_timestamp") - commit_cell="-" - [[ -n "$commit_sha" ]] && commit_cell="[\`$commit_sha_short\`](https://github.com/${GITHUB_REPOSITORY}/commit/${commit_sha})" - echo "| \`$version\` | [Download](https://github.com/${GITHUB_REPOSITORY}/raw/$RELEASES_BRANCH/zips/${plugin_name}/${zip_basename}) | ${build_date:--} | $commit_cell | ${checksum_md5:--} | ${checksum_sha256:--} |" - else - echo "| \`$version\` | [Download](https://github.com/${GITHUB_REPOSITORY}/raw/$RELEASES_BRANCH/zips/${plugin_name}/${zip_basename}) | - | - | - |" - fi - done < <(ls -1 "zips/$plugin_name/${plugin_name}"-*.zip 2>/dev/null \ - | grep -v latest | sort -t- -k2 -V -r) + while IFS= read -r version_json; do + ver=$(echo "$version_json" | jq -r '.version // empty') + [[ -z "$ver" ]] && continue + url_path=$(echo "$version_json" | jq -r '.url // empty') + full_url="" + [[ -n "$root_url" && -n "$url_path" ]] && full_url="${root_url}/${url_path}" + commit_sha=$(echo "$version_json" | jq -r '.commit_sha // empty') + commit_sha_short=$(echo "$version_json" | jq -r '.commit_sha_short // empty') + build_timestamp=$(echo "$version_json" | jq -r '.build_timestamp // empty') + checksum_md5=$(echo "$version_json" | jq -r '.checksum_md5 // empty') + checksum_sha256=$(echo "$version_json" | jq -r '.checksum_sha256 // empty') + build_date=$(fmt_date "$build_timestamp") + commit_cell="-" + [[ -n "$commit_sha" ]] && commit_cell="[\`$commit_sha_short\`](https://github.com/${GITHUB_REPOSITORY}/commit/${commit_sha})" + download_cell="-" + [[ -n "$full_url" ]] && download_cell="[Download](${full_url})" + echo "| \`$ver\` | $download_cell | ${build_date:--} | $commit_cell | ${checksum_md5:--} | ${checksum_sha256:--} |" + done < <(jq -c '.manifest.versions[]?' "$manifest_file" 2>/dev/null) echo "" echo "---" diff --git a/.github/scripts/publish/run.sh b/.github/scripts/publish/run.sh index 429bf00..915dad1 100644 --- a/.github/scripts/publish/run.sh +++ b/.github/scripts/publish/run.sh @@ -20,6 +20,7 @@ fi RELEASES_BRANCH="releases" MAX_VERSIONED_ZIPS=10 +RELEASES_BRANCH_VERSION=2 SCRIPT_DIR="$(dirname "$(readlink -f "$0")")" export SOURCE_BRANCH RELEASES_BRANCH MAX_VERSIONED_ZIPS @@ -57,8 +58,9 @@ fi # Checkout or create releases branch echo "Setting up $RELEASES_BRANCH branch..." if [[ "${FORCE_REBUILD:-false}" == "true" && -n "${FORCE_REBUILD_PLUGIN:-}" ]]; then - # Targeted rebuild: keep existing releases branch, clear only the named plugin's zips dir. - # build-zips.sh will rebuild it since the zip is gone; all other plugins are untouched. + # Targeted rebuild: delete all GitHub Releases for the named plugin so build-zips.sh + # treats it as new, then clear its per-plugin manifest so generate-manifest.sh + # rebuilds it from scratch. All other plugins are untouched. if git ls-remote --exit-code --heads origin $RELEASES_BRANCH >/dev/null 2>&1; then git checkout $RELEASES_BRANCH git pull origin $RELEASES_BRANCH || true @@ -67,10 +69,27 @@ if [[ "${FORCE_REBUILD:-false}" == "true" && -n "${FORCE_REBUILD_PLUGIN:-}" ]]; git rm -rf . 2>/dev/null || true git commit --allow-empty -m "Initialize $RELEASES_BRANCH branch" fi - echo "Targeted force rebuild: clearing zips/$FORCE_REBUILD_PLUGIN/" - rm -rf "zips/$FORCE_REBUILD_PLUGIN" + echo "Targeted force rebuild: deleting GitHub Releases for $FORCE_REBUILD_PLUGIN" + gh release list --repo "$GITHUB_REPOSITORY" --json tagName --limit 500 \ + | jq -r '.[].tagName' \ + | grep "^${FORCE_REBUILD_PLUGIN}-" \ + | xargs -I{} gh release delete {} --repo "$GITHUB_REPOSITORY" --yes --cleanup-tag 2>/dev/null || true + rm -f "zips/$FORCE_REBUILD_PLUGIN/manifest.json" elif [[ "${FORCE_REBUILD:-false}" == "true" ]]; then - echo "Force rebuild requested - resetting $RELEASES_BRANCH to a new orphan commit." + echo "Force rebuild requested - deleting all plugin GitHub Releases and resetting $RELEASES_BRANCH." + git fetch origin $SOURCE_BRANCH 2>/dev/null || true + git checkout "origin/$SOURCE_BRANCH" -- plugins 2>/dev/null || true + if [[ -d plugins ]]; then + for plugin_dir in plugins/*/; do + plugin_name=$(basename "$plugin_dir") + gh release list --repo "$GITHUB_REPOSITORY" --json tagName --limit 500 \ + | jq -r '.[].tagName' \ + | grep "^${plugin_name}-" \ + | xargs -I{} gh release delete {} --repo "$GITHUB_REPOSITORY" --yes --cleanup-tag 2>/dev/null || true + done + rm -rf plugins + fi + # Reset the releases branch to a fresh orphan git checkout --orphan $RELEASES_BRANCH git rm -rf . 2>/dev/null || true git commit --allow-empty -m "Initialize $RELEASES_BRANCH branch (force rebuild)" @@ -94,6 +113,20 @@ git checkout origin/$SOURCE_BRANCH -- plugins mkdir -p zips +# --- Version guard --- +# Ensure the releases branch was initialised with the current repo version. +# Skip during force rebuild — the branch is being rebuilt from scratch. +if [[ "${FORCE_REBUILD:-false}" != "true" ]]; then + current_branch_ver=$(cat REPO_VER 2>/dev/null || echo "") + if [[ "$current_branch_ver" != "$RELEASES_BRANCH_VERSION" ]]; then + echo "::error::Releases branch version mismatch." + echo "::error:: Expected : $RELEASES_BRANCH_VERSION" + echo "::error:: Found : ${current_branch_ver:-'(none — migration not run)'}" + echo "::error::Run the 'Migrate Releases to GitHub Releases' workflow first, then re-run." + exit 1 + fi +fi + # --- Phases --- echo "" echo "=== Building ZIPs ===" @@ -121,7 +154,8 @@ echo "=== Committing ===" rm -rf plugins git rm -rf --cached plugins 2>/dev/null || true -git add zips manifest.json README.md +echo "$RELEASES_BRANCH_VERSION" > REPO_VER +git add zips manifest.json README.md REPO_VER if git diff --cached --quiet; then echo "No changes to commit." @@ -129,7 +163,7 @@ else # Check whether the staged diff is purely timestamp noise: # README.md - "*Last updated: ..." footer # manifest.json - "generated_at" field - # Any other changed file (e.g. a ZIP) counts as a real change. + # Any other changed file (e.g. a per-plugin manifest.json) counts as a real change. only_timestamps=true while IFS= read -r changed_file; do case "$changed_file" in diff --git a/.github/scripts/publish/yank-version.sh b/.github/scripts/publish/yank-version.sh index 5d5a372..db2f178 100644 --- a/.github/scripts/publish/yank-version.sh +++ b/.github/scripts/publish/yank-version.sh @@ -69,17 +69,21 @@ else fi ZIP_DIR="zips/$YANK_PLUGIN" -TARGET_ZIP="$ZIP_DIR/${YANK_PLUGIN}-${YANK_VERSION}.zip" +RELEASE_TAG="${YANK_PLUGIN}-${YANK_VERSION}" PLUGIN_MANIFEST="$ZIP_DIR/manifest.json" # --- Validate --- -if [[ ! -f "$TARGET_ZIP" ]]; then - echo "::error::$TARGET_ZIP not found on the releases branch. Nothing to yank." +if ! gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then + echo "::error::GitHub Release $RELEASE_TAG not found. Nothing to yank." exit 1 fi -# Count existing versioned ZIPs (excluding -latest) -REMAINING=$(ls -1 "$ZIP_DIR/${YANK_PLUGIN}"-*.zip 2>/dev/null | grep -v '\-latest\.zip' | grep -v "/${YANK_PLUGIN}-${YANK_VERSION}\.zip" || true) +# Count remaining versioned releases (excluding the one being yanked and -latest) +REMAINING=$(gh release list --repo "$GITHUB_REPOSITORY" --json tagName --limit 500 \ + | jq -r '.[].tagName' \ + | grep "^${YANK_PLUGIN}-" \ + | grep -v "^${YANK_PLUGIN}-latest$" \ + | grep -v "^${RELEASE_TAG}$" || true) REMAINING_COUNT=$(echo "$REMAINING" | grep -c . || true) # Determine if we are yanking the current latest @@ -93,10 +97,10 @@ IS_LATEST=false IS_LAST_VERSION=false [[ "$REMAINING_COUNT" -eq 0 ]] && IS_LAST_VERSION=true -echo " Current latest : ${CURRENT_LATEST:-unknown}" -echo " Is latest : $IS_LATEST" -echo " Remaining ZIPs : $REMAINING_COUNT" -echo " Is last version: $IS_LAST_VERSION" +echo " Current latest : ${CURRENT_LATEST:-unknown}" +echo " Is latest : $IS_LATEST" +echo " Remaining releases: $REMAINING_COUNT" +echo " Is last version : $IS_LAST_VERSION" # --- Fetch source branch + plugins dir (needed by manifest + readme scripts) --- git fetch origin "$SOURCE_BRANCH" @@ -107,21 +111,43 @@ SOURCE_TYPE=$(jq -r '.source_type // "local"' "plugins/$YANK_PLUGIN/plugin.json" # --- Perform the yank --- if $IS_LAST_VERSION; then - echo "Last version - removing entire zips/$YANK_PLUGIN/ directory." + echo "Last version - deleting all GitHub Releases for $YANK_PLUGIN." + gh release delete "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --yes --cleanup-tag + gh release delete "${YANK_PLUGIN}-latest" --repo "$GITHUB_REPOSITORY" --yes --cleanup-tag 2>/dev/null || true rm -rf "$ZIP_DIR" else - echo "Removing $TARGET_ZIP" - rm "$TARGET_ZIP" + echo "Deleting GitHub Release $RELEASE_TAG" + gh release delete "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --yes --cleanup-tag if $IS_LATEST; then - NEW_LATEST_ZIP=$(ls -1 "$ZIP_DIR/${YANK_PLUGIN}"-*.zip 2>/dev/null | grep -v '\-latest\.zip' | sort -t- -k2 -V -r | head -1 || true) - if [[ -z "$NEW_LATEST_ZIP" ]]; then + # Find the highest remaining version to promote to -latest + NEW_LATEST_VERSION=$(echo "$REMAINING" \ + | sed "s/^${YANK_PLUGIN}-//" \ + | sort -V -r \ + | head -1) + if [[ -z "$NEW_LATEST_VERSION" ]]; then echo "::error::Could not find a replacement version to promote to latest." exit 1 fi - NEW_LATEST_VERSION=$(basename "$NEW_LATEST_ZIP" | sed "s/${YANK_PLUGIN}-\(.*\)\.zip/\1/") echo "Promoting $NEW_LATEST_VERSION to latest" - cp "$NEW_LATEST_ZIP" "$ZIP_DIR/${YANK_PLUGIN}-latest.zip" + # Download the promoted version's ZIP and recreate the -latest release + PROMOTE_ZIP="/tmp/${YANK_PLUGIN}-${NEW_LATEST_VERSION}.zip" + gh release download "${YANK_PLUGIN}-${NEW_LATEST_VERSION}" \ + --repo "$GITHUB_REPOSITORY" \ + --pattern "${YANK_PLUGIN}-${NEW_LATEST_VERSION}.zip" \ + --dir /tmp + LATEST_ZIP="/tmp/${YANK_PLUGIN}-latest.zip" + cp "$PROMOTE_ZIP" "$LATEST_ZIP" + # Reuse the promoted version's release notes for the -latest alias + promoted_notes=$(gh release view "${YANK_PLUGIN}-${NEW_LATEST_VERSION}" \ + --repo "$GITHUB_REPOSITORY" --json body --jq '.body' 2>/dev/null || echo "") + gh release delete "${YANK_PLUGIN}-latest" --repo "$GITHUB_REPOSITORY" --yes --cleanup-tag 2>/dev/null || true + gh release create "${YANK_PLUGIN}-latest" \ + --repo "$GITHUB_REPOSITORY" \ + --title "${YANK_PLUGIN} latest (v${NEW_LATEST_VERSION})" \ + --notes "$promoted_notes" \ + "$LATEST_ZIP" + rm -f "$PROMOTE_ZIP" "$LATEST_ZIP" fi fi diff --git a/.github/workflows/run-migrations.yml b/.github/workflows/run-migrations.yml new file mode 100644 index 0000000..d1c21c9 --- /dev/null +++ b/.github/workflows/run-migrations.yml @@ -0,0 +1,466 @@ +name: Run Migrations + +# Migration history: +# 001 (v0 → v2): Move ZIPs from releases branch git history to GitHub Release assets + +permissions: + contents: write + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +on: + workflow_dispatch: + inputs: + dry_run: + description: 'Preview what would happen without making any changes.' + type: boolean + default: true + required: false + confirm: + description: 'Required when dry_run is unchecked: type CONFIRM to proceed.' + type: string + default: '' + required: false + +jobs: + + # --------------------------------------------------------------------------- + # Validate inputs and detect which migrations need to run. + # All migration jobs depend on this one. + # --------------------------------------------------------------------------- + setup: + runs-on: ubuntu-latest + outputs: + any_pending: ${{ steps.detect.outputs.any_pending }} + needs_001: ${{ steps.detect.outputs.needs_001 }} + use_app: ${{ steps.config.outputs.use_app }} + app_id: ${{ steps.config.outputs.app_id }} + # Add outputs for future migrations here: + # needs_002: ${{ steps.detect.outputs.needs_002 }} + steps: + - name: Validate inputs + run: | + if [[ "${{ inputs.dry_run }}" != "true" && "${{ inputs.confirm }}" != "CONFIRM" ]]; then + echo "::error::dry_run is false but confirm is not set to CONFIRM. Aborting." + exit 1 + fi + + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Load app ID from config + id: config + env: + GH_APP_ID: ${{ vars.GH_APP_ID }} + GH_APP_PRIVATE_KEY: ${{ secrets.GH_APP_PRIVATE_KEY }} + run: | + if [[ -n "${GH_APP_ID:-}" && -n "${GH_APP_PRIVATE_KEY:-}" ]]; then + echo "app_id=${GH_APP_ID}" >> "$GITHUB_OUTPUT" + echo "use_app=true" >> "$GITHUB_OUTPUT" + else + echo "use_app=false" >> "$GITHUB_OUTPUT" + fi + + - name: Detect pending migrations + id: detect + run: | + RELEASES_BRANCH="releases" + TARGET_VER=2 # bump when adding a new migration + + current_ver=0 + if git ls-remote --exit-code --heads origin "$RELEASES_BRANCH" >/dev/null 2>&1; then + current_ver=$(git show "origin/${RELEASES_BRANCH}:REPO_VER" 2>/dev/null || echo "0") + fi + echo "current_ver=$current_ver target=$TARGET_VER" + + (( current_ver < 2 )) && echo "needs_001=true" >> "$GITHUB_OUTPUT" \ + || echo "needs_001=false" >> "$GITHUB_OUTPUT" + + # Add future migrations here: + # (( current_ver < 3 )) && echo "needs_002=true" >> "$GITHUB_OUTPUT" \ + # || echo "needs_002=false" >> "$GITHUB_OUTPUT" + + (( current_ver < TARGET_VER )) && echo "any_pending=true" >> "$GITHUB_OUTPUT" \ + || echo "any_pending=false" >> "$GITHUB_OUTPUT" + + # --------------------------------------------------------------------------- + # Migration 001 (v0 → v2): Move ZIPs to GitHub Release assets + # needs_rebuild=true — changing storage format requires a manifest rebuild. + # --------------------------------------------------------------------------- + migration_001: + needs: [setup] + if: needs.setup.outputs.needs_001 == 'true' + runs-on: ubuntu-latest + outputs: + rebuild_type: ${{ steps.flags.outputs.rebuild_type }} + end_version: ${{ steps.flags.outputs.end_version }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Generate GitHub App token + if: needs.setup.outputs.use_app == 'true' + id: app-token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ needs.setup.outputs.app_id }} + private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} + + - name: Run migration + env: + GITHUB_TOKEN: ${{ needs.setup.outputs.use_app == 'true' && steps.app-token.outputs.token || github.token }} + GITHUB_REPOSITORY: ${{ github.repository }} + DRY_RUN: ${{ inputs.dry_run }} + run: | + set -euo pipefail + RELEASES_BRANCH="releases" + created=0 + deleted=0 + + echo "### Migration 001: Move ZIPs to GitHub Release assets" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + [[ "$DRY_RUN" == "true" ]] \ + && echo "**Mode: dry run** - no changes will be made." >> "$GITHUB_STEP_SUMMARY" \ + || echo "**Mode: live** - changes will be applied." >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + + plugin_slugs=() + for d in plugins/*/; do + [[ -d "$d" ]] && plugin_slugs+=("$(basename "$d")") + done + [[ ${#plugin_slugs[@]} -eq 0 ]] && { echo "::error::No plugins found in plugins/."; exit 1; } + echo "Plugins: ${plugin_slugs[*]}" + + # Step 1: Purge existing plugin releases (clean slate) + echo "" + echo "--- Purging existing plugin releases ---" + for slug in "${plugin_slugs[@]}"; do + matching=$(gh release list --repo "$GITHUB_REPOSITORY" --json tagName --limit 500 \ + | jq -r '.[].tagName' | grep "^${slug}-" || true) + [[ -z "$matching" ]] && continue + while IFS= read -r tag; do + [[ -z "$tag" ]] && continue + echo " DELETE $tag" + if [[ "$DRY_RUN" != "true" ]]; then + gh release delete "$tag" --repo "$GITHUB_REPOSITORY" --yes --cleanup-tag 2>/dev/null || true + (( deleted++ )) || true + fi + done <<< "$matching" + done + + # Step 2: Enumerate versioned ZIPs from the releases branch + echo "" + echo "--- Enumerating ZIPs from $RELEASES_BRANCH branch ---" + zip_paths=$(git ls-tree -r --name-only "origin/${RELEASES_BRANCH}" 2>/dev/null \ + | grep 'zips/.*/.*\.zip' | grep -v '\-latest\.zip' | sort || true) + + if [[ -z "$zip_paths" ]]; then + echo "No versioned ZIPs found on origin/${RELEASES_BRANCH} - nothing to move." + echo "No ZIPs found on releases branch." >> "$GITHUB_STEP_SUMMARY" + else + declare -A plugin_latest_version + declare -A plugin_latest_zip_path + + while IFS= read -r zip_path; do + [[ -z "$zip_path" ]] && continue + zip_basename=$(basename "$zip_path") + matched_slug="" + for slug in "${plugin_slugs[@]}"; do + [[ "$zip_basename" == "${slug}-"*.zip ]] && { matched_slug="$slug"; break; } + done + [[ -z "$matched_slug" ]] && continue + zip_version="${zip_basename#${matched_slug}-}"; zip_version="${zip_version%.zip}" + current_latest="${plugin_latest_version[$matched_slug]:-}" + if [[ -z "$current_latest" ]] || \ + printf '%s\n%s\n' "$current_latest" "$zip_version" | sort -V | tail -1 | grep -qx "$zip_version"; then + plugin_latest_version["$matched_slug"]="$zip_version" + plugin_latest_zip_path["$matched_slug"]="$zip_path" + fi + done <<< "$zip_paths" + + # Step 3: Create versioned releases + echo "" + echo "--- Creating versioned releases ---" + echo "| Tag | Status |" >> "$GITHUB_STEP_SUMMARY" + echo "|-----|--------|" >> "$GITHUB_STEP_SUMMARY" + + while IFS= read -r zip_path; do + [[ -z "$zip_path" ]] && continue + zip_basename=$(basename "$zip_path") + matched_slug="" + for slug in "${plugin_slugs[@]}"; do + [[ "$zip_basename" == "${slug}-"*.zip ]] && { matched_slug="$slug"; break; } + done + [[ -z "$matched_slug" ]] && continue + zip_version="${zip_basename#${matched_slug}-}"; zip_version="${zip_version%.zip}" + release_tag="${matched_slug}-${zip_version}" + echo " CREATE $release_tag" + if [[ "$DRY_RUN" != "true" ]]; then + tmp_zip="/tmp/${zip_basename}" + git show "origin/${RELEASES_BRANCH}:${zip_path}" > "$tmp_zip" + readme_url="https://github.com/${GITHUB_REPOSITORY}/blob/releases/zips/${matched_slug}/README.md" + release_notes="" + commit_sha=$(git show "origin/${RELEASES_BRANCH}:zips/${matched_slug}/manifest.json" \ + | jq -r --arg v "$zip_version" \ + '.manifest.versions[]? | select(.version == $v) | .commit_sha // empty' 2>/dev/null || true) + commit_sha_short=$(git show "origin/${RELEASES_BRANCH}:zips/${matched_slug}/manifest.json" \ + | jq -r --arg v "$zip_version" \ + '.manifest.versions[]? | select(.version == $v) | .commit_sha_short // empty' 2>/dev/null || true) + if [[ -n "$commit_sha" ]]; then + commit_url="https://github.com/${GITHUB_REPOSITORY}/commit/${commit_sha}" + release_notes="**Commit:** [\`${commit_sha_short:-${commit_sha:0:7}}\`](${commit_url})" + pr_info=$(gh api "repos/${GITHUB_REPOSITORY}/commits/${commit_sha}/pulls" \ + --jq '.[0] | {number: .number, url: .html_url}' 2>/dev/null || echo '{}') + pr_number=$(echo "$pr_info" | jq -r '.number // empty') + pr_url=$(echo "$pr_info" | jq -r '.url // empty') + [[ -n "$pr_number" && "$pr_number" != "null" ]] && \ + release_notes+=$'\n'"**PR:** [#${pr_number}](${pr_url})" + release_notes+=$'\n' + fi + release_notes+="**README:** [Plugin README](${readme_url})" + gh release create "$release_tag" --repo "$GITHUB_REPOSITORY" \ + --title "${matched_slug} v${zip_version}" --notes "$release_notes" "$tmp_zip" + rm -f "$tmp_zip" + (( created++ )) || true + echo "| \`${release_tag}\` | created |" >> "$GITHUB_STEP_SUMMARY" + else + echo "| \`${release_tag}\` | would create (dry run) |" >> "$GITHUB_STEP_SUMMARY" + fi + done <<< "$zip_paths" + + # Step 4: Create -latest releases + echo "" + echo "--- Creating -latest releases ---" + for slug in "${plugin_slugs[@]}"; do + latest_ver="${plugin_latest_version[$slug]:-}"; latest_zip_path="${plugin_latest_zip_path[$slug]:-}" + [[ -z "$latest_ver" || -z "$latest_zip_path" ]] && continue + latest_tag="${slug}-latest" + echo " CREATE $latest_tag (from v${latest_ver})" + if [[ "$DRY_RUN" != "true" ]]; then + tmp_zip="/tmp/${slug}-latest.zip" + git show "origin/${RELEASES_BRANCH}:${latest_zip_path}" > "$tmp_zip" + versioned_notes=$(gh release view "${slug}-${latest_ver}" --repo "$GITHUB_REPOSITORY" \ + --json body --jq '.body' 2>/dev/null || echo "") + gh release create "$latest_tag" --repo "$GITHUB_REPOSITORY" \ + --title "${slug} latest (v${latest_ver})" --notes "$versioned_notes" "$tmp_zip" + rm -f "$tmp_zip" + (( created++ )) || true + echo "| \`${latest_tag}\` (v${latest_ver}) | created |" >> "$GITHUB_STEP_SUMMARY" + else + echo "| \`${latest_tag}\` (v${latest_ver}) | would create (dry run) |" >> "$GITHUB_STEP_SUMMARY" + fi + done + fi + + echo "" + [[ "$DRY_RUN" == "true" ]] \ + && echo "Dry run complete." \ + || echo "Done. Deleted $deleted releases, created $created releases." + + - name: Strip tracked ZIPs from releases branch + env: + GITHUB_TOKEN: ${{ needs.setup.outputs.use_app == 'true' && steps.app-token.outputs.token || github.token }} + APP_SLUG: ${{ steps.app-token.outputs.app-slug }} + GITHUB_REPOSITORY: ${{ github.repository }} + DRY_RUN: ${{ inputs.dry_run }} + run: | + set -euo pipefail + RELEASES_BRANCH="releases" + + if [[ "$DRY_RUN" == "true" ]]; then + mapfile -t tracked_zips < <(git ls-tree -r --name-only "origin/${RELEASES_BRANCH}" 2>/dev/null | grep '\.zip$' || true) + echo "Dry run: would remove ${#tracked_zips[@]} tracked ZIP(s) from $RELEASES_BRANCH." + exit 0 + fi + + if ! git ls-remote --exit-code --heads origin "$RELEASES_BRANCH" >/dev/null 2>&1; then + echo "No $RELEASES_BRANCH branch — nothing to strip." + exit 0 + fi + + if [[ -n "${APP_SLUG:-}" ]]; then + BOT_USER_ID=$(gh api "/users/${APP_SLUG}%5Bbot%5D" --jq '.id' 2>/dev/null || echo "") + git config user.name "${APP_SLUG}[bot]" + git config user.email "${BOT_USER_ID:+${BOT_USER_ID}+}${APP_SLUG}[bot]@users.noreply.github.com" + else + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + fi + + git fetch origin "$RELEASES_BRANCH" + git checkout "$RELEASES_BRANCH" + mapfile -t tracked_zips < <(git ls-files 'zips' | grep '\.zip$') + if [[ ${#tracked_zips[@]} -eq 0 ]]; then + echo "No tracked ZIPs found — branch already clean." + exit 0 + fi + git rm -f "${tracked_zips[@]}" + git commit -m "Strip tracked ZIPs from releases branch [skip ci]" + git push "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" "$RELEASES_BRANCH" + echo "Removed ${#tracked_zips[@]} tracked ZIP(s) from $RELEASES_BRANCH." + + - name: Set migration flags + id: flags + run: | + echo "rebuild_type=regular" >> "$GITHUB_OUTPUT" + echo "end_version=2" >> "$GITHUB_OUTPUT" + + # --------------------------------------------------------------------------- + # Add future migration jobs here following the same pattern: + # + # migration_002: + # needs: [setup] + # if: needs.setup.outputs.needs_002 == 'true' + # runs-on: ubuntu-latest + # outputs: + # needs_rebuild: ${{ steps.flags.outputs.needs_rebuild }} + # steps: + # - ... + # - name: Set migration flags + # id: flags + # run: | + # echo "rebuild_type=none" >> "$GITHUB_OUTPUT" # force | regular | none + # echo "end_version=3" >> "$GITHUB_OUTPUT" + # --------------------------------------------------------------------------- + + # --------------------------------------------------------------------------- + # Commits REPO_VER to the releases branch after all migrations succeed. + # Skipped on dry runs or if no migrations were pending. + # --------------------------------------------------------------------------- + write_repo_ver: + needs: [setup, migration_001] + if: | + always() && + needs.setup.outputs.any_pending == 'true' && + inputs.dry_run == false && + !contains(needs.*.result, 'failure') && + !contains(needs.*.result, 'cancelled') + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Generate GitHub App token + if: needs.setup.outputs.use_app == 'true' + id: app-token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ needs.setup.outputs.app_id }} + private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} + + - name: Write REPO_VER + env: + GITHUB_TOKEN: ${{ needs.setup.outputs.use_app == 'true' && steps.app-token.outputs.token || github.token }} + APP_SLUG: ${{ steps.app-token.outputs.app-slug }} + GITHUB_REPOSITORY: ${{ github.repository }} + NEEDS_JSON: ${{ toJSON(needs) }} + run: | + set -euo pipefail + RELEASES_BRANCH="releases" + TARGET_VER=$(echo "$NEEDS_JSON" | jq ' + [.. | objects | .outputs.end_version? | select(. != null and . != "") | tonumber] | max // 0 + ') + if [[ "$TARGET_VER" -eq 0 ]]; then + echo "::error::No end_version found from any migration." + exit 1 + fi + echo "Writing REPO_VER=${TARGET_VER}" + + if [[ -n "${APP_SLUG:-}" ]]; then + BOT_USER_ID=$(gh api "/users/${APP_SLUG}%5Bbot%5D" --jq '.id' 2>/dev/null || echo "") + git config user.name "${APP_SLUG}[bot]" + if [[ -n "$BOT_USER_ID" ]]; then + git config user.email "${BOT_USER_ID}+${APP_SLUG}[bot]@users.noreply.github.com" + else + git config user.email "${APP_SLUG}[bot]@users.noreply.github.com" + fi + else + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + fi + + if git ls-remote --exit-code --heads origin "$RELEASES_BRANCH" >/dev/null 2>&1; then + git fetch origin "$RELEASES_BRANCH" + git checkout "$RELEASES_BRANCH" + else + git checkout --orphan "$RELEASES_BRANCH" + git rm -rf . 2>/dev/null || true + fi + + echo "$TARGET_VER" > REPO_VER + git add REPO_VER + if git diff --cached --quiet; then + echo "REPO_VER already up to date." + else + git commit -m "Set REPO_VER=${TARGET_VER} [skip ci]" + git push "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" "$RELEASES_BRANCH" + echo "REPO_VER=${TARGET_VER} written to $RELEASES_BRANCH." + fi + + # --------------------------------------------------------------------------- + # Triggers a regular publish if any migration flagged rebuild_type=regular + # and no migration flagged force (force is a superset — regular is skipped). + # When adding migration_002: extend the 'regular' OR and add a 'force' != guard. + # --------------------------------------------------------------------------- + trigger_publish_regular: + needs: [setup, migration_001, write_repo_ver] + if: | + needs.write_repo_ver.result == 'success' && + needs.migration_001.outputs.rebuild_type == 'regular' && + needs.migration_001.outputs.rebuild_type != 'force' + runs-on: ubuntu-latest + steps: + - name: Generate GitHub App token + if: needs.setup.outputs.use_app == 'true' + id: app-token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ needs.setup.outputs.app_id }} + private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} + + - name: Trigger regular publish + env: + GITHUB_TOKEN: ${{ needs.setup.outputs.use_app == 'true' && steps.app-token.outputs.token || github.token }} + GITHUB_REPOSITORY: ${{ github.repository }} + run: | + gh workflow run publish-plugins.yml --repo "$GITHUB_REPOSITORY" + echo "Regular publish triggered — manifests and READMEs will be regenerated shortly." + echo "Regular publish triggered." >> "$GITHUB_STEP_SUMMARY" + + # --------------------------------------------------------------------------- + # Triggers a force-rebuild publish if any migration flagged rebuild_type=force. + # When adding migration_002: extend the 'force' OR condition. + # --------------------------------------------------------------------------- + trigger_publish_force: + needs: [setup, migration_001, write_repo_ver] + if: | + needs.write_repo_ver.result == 'success' && + needs.migration_001.outputs.rebuild_type == 'force' + runs-on: ubuntu-latest + steps: + - name: Generate GitHub App token + if: needs.setup.outputs.use_app == 'true' + id: app-token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ needs.setup.outputs.app_id }} + private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} + + - name: Trigger force rebuild publish + env: + GITHUB_TOKEN: ${{ needs.setup.outputs.use_app == 'true' && steps.app-token.outputs.token || github.token }} + GITHUB_REPOSITORY: ${{ github.repository }} + run: | + gh workflow run publish-plugins.yml \ + --repo "$GITHUB_REPOSITORY" \ + --field force_rebuild=true \ + --field force_rebuild_confirm=CONFIRM + echo "Force rebuild triggered — releases branch will be fully regenerated shortly." + echo "Force rebuild triggered." >> "$GITHUB_STEP_SUMMARY" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c8f2c90..5c4d2fe 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,23 +12,9 @@ ## Folder Structure -### Standard plugin (full source) - -``` -plugins/ - your-plugin-name/ - plugin.json # required - main.py # your plugin's entry point - ... # any other Python files, assets, or subdirectories - README.md # optional but recommended - logo.png # optional; displayed in the plugin browser -``` +### External plugin (recommended) -All files inside your plugin folder - `main.py`, helper modules, assets, subdirectories - are automatically packaged into a ZIP on merge. There is no separate build step. - -### External plugin (source hosted elsewhere) - -If your plugin has complex build requirements or publishes releases from its own repository, you can submit a pointer-only directory. Only a `plugin.json` is required: +If your plugin has its own GitHub repository, submit a directory with only a `plugin.json`. The registry fetches your ZIP on each version bump from your upstream release URL. ``` plugins/ @@ -38,7 +24,7 @@ plugins/ logo.png # optional; displayed in the plugin browser ``` -On merge, the registry fetches the ZIP from your release, computes its checksums independently, re-hosts it on the releases branch, and GPG-signs the manifest. Clients always download from the registry, never directly from your upstream URL. +On merge, the registry downloads the ZIP from your `source_url`, computes its checksums independently, re-hosts it as a GitHub Release on this registry, and GPG-signs the manifest. Clients always download from the registry, never directly from your upstream URL. **Requirements for external plugins:** @@ -49,6 +35,22 @@ On merge, the registry fetches the ZIP from your release, computes its checksums Each version bump requires a PR to this repository (updating `version` in `plugin.json`), which must be approved and merged before anything is published. How you automate or manage that is up to you. +### Standard plugin (full source) + +For simple scripts or plugins without their own repository or build process. + +``` +plugins/ + your-plugin-name/ + plugin.json # required + main.py # your plugin's entry point + ... # any other Python files, assets, or subdirectories + README.md # optional but recommended + logo.png # optional; displayed in the plugin browser +``` + +Everything in your plugin folder (`main.py`, helper modules, assets, subdirectories) is automatically packaged into a ZIP on merge. No separate build step. + Plugin folder names must be **lowercase-kebab-case** (e.g. `my-plugin-name`). ## Submitting a Plugin @@ -111,7 +113,7 @@ At least one of `author` or `maintainers` must include your GitHub username. `au | `source_type` | `string` | Set to `"external"` to declare this as an external plugin. Omit (or `"local"`) for standard plugins | | `source_url` | `string` | Required when `source_type` is `"external"`. Must be a GitHub Releases URL containing a `{version}` placeholder, e.g. `https://github.com/owner/repo/releases/download/v{version}/plugin.zip` | -### Full Example (standard plugin) +### Full Example (external plugin, recommended) ```json { @@ -119,15 +121,15 @@ At least one of `author` or `maintainers` must include your GitHub username. `au "version": "1.2.0", "description": "Does something useful for Dispatcharr", "author": "your-github-username", - "maintainers": ["collaborator-username"], "license": "MIT", - "min_dispatcharr_version": "v0.19.0", + "source_type": "external", + "source_url": "https://github.com/your-github-username/my-plugin/releases/download/v{version}/my-plugin.zip", "repo_url": "https://github.com/your-github-username/my-plugin", "discord_thread": "https://discord.com/channels/..." } ``` -### Full Example (external plugin) +### Full Example (standard plugin) ```json { @@ -135,9 +137,9 @@ At least one of `author` or `maintainers` must include your GitHub username. `au "version": "1.2.0", "description": "Does something useful for Dispatcharr", "author": "your-github-username", + "maintainers": ["collaborator-username"], "license": "MIT", - "source_type": "external", - "source_url": "https://github.com/your-github-username/my-plugin/releases/download/v{version}/my-plugin.zip", + "min_dispatcharr_version": "v0.19.0", "repo_url": "https://github.com/your-github-username/my-plugin", "discord_thread": "https://discord.com/channels/..." } @@ -172,22 +174,23 @@ PRs where the author has no permission for any of the modified plugins are **aut Once your PR merges to `main`, the publish workflow runs automatically: -**Standard plugins:** -1. Your plugin is packaged into a versioned ZIP (`your-plugin-1.0.0.zip`) and a latest ZIP (`your-plugin-latest.zip`) -2. MD5 and SHA256 checksums are computed -3. `manifest.json` is updated with your plugin's metadata, checksums, and download URLs -4. A per-plugin `zips/your-plugin-name/README.md` is generated with download links and version history -5. The releases branch README is regenerated -6. Up to 10 versioned ZIPs are retained; older ones are pruned - **External plugins:** 1. The ZIP is downloaded from your `source_url` (with `{version}` substituted) 2. MD5 and SHA256 checksums are computed by the registry's infrastructure (not trusted from upstream) -3. The ZIP is re-hosted on the releases branch; clients download from the registry, not your upstream URL -4. `manifest.json` is updated with checksums and includes `source_url` pointing to the upstream release -5. Steps 4–6 above apply identically +3. The ZIP is published as a **GitHub Release** on this registry (tag: `your-plugin-1.0.0`); a `-latest` alias release is also maintained +4. `manifest.json` is updated with checksums and download URLs pointing to the GitHub Release assets; `source_url` pointing to the upstream release is also recorded +5. A per-plugin `README.md` is generated with download links and version history +6. Up to 10 versioned releases are retained; older ones are pruned + +**Standard plugins:** +1. Your plugin is packaged into a versioned ZIP +2. MD5 and SHA256 checksums are computed +3. The ZIP is published as a **GitHub Release** on this registry (tag: `your-plugin-1.0.0`); a `-latest` alias release is also maintained +4. `manifest.json` on the releases branch is updated with metadata and download URLs pointing to the GitHub Release assets +5. A per-plugin `README.md` is generated with download links and version history +6. Up to 10 versioned releases are retained; older ones are pruned -Everything is pushed to the [`releases` branch](https://github.com/Dispatcharr/Plugins/tree/releases). +Manifests and READMEs are committed to the [`releases` branch](https://github.com/Dispatcharr/Plugins/tree/releases). ZIP files are stored as [GitHub Release assets](https://github.com/Dispatcharr/Plugins/releases). ## Versioning @@ -228,7 +231,7 @@ To request a removal, [open an issue](../../issues/new/choose) using the **Versi What happens when a version is yanked: -- The versioned ZIP and its manifest entry are removed from the releases branch. +- The GitHub Release and its asset are deleted; the manifest entry is removed from the releases branch. - If it was the latest version, the previous version is automatically promoted to latest and a PR is opened against the source branch to roll back `plugin.json` to match. - If it was the only version, the plugin is fully removed from the registry and a PR is opened to remove its source folder. - Users who already downloaded the version are unaffected. diff --git a/README.md b/README.md index 817609b..020f3e3 100644 --- a/README.md +++ b/README.md @@ -2,19 +2,25 @@ > **This is a listing and distribution repository.** Plugin development, testing, and pre-releases should happen in your own repository. Submit a PR here only when your plugin is ready for public distribution. -A repository for publishing and distributing Dispatcharr Python plugins with automated validation and release management. +> AI tools are used in the development of this project and repo, as well as many of the plugins included in this repo. The team personally audits and reviews every line of workflow and script generated, and all changes are tested extensively by humans. The involvement of these tools greatly increases development velocity (nice, buzzword) and greatly assists especially where boilerplate and documentation (which we all loathe writing) are involved. + ## Quick Links | Resource | Description | |----------|-------------| -| [Browse Plugins](https://github.com/Dispatcharr/Plugins/tree/releases) | All available plugins on the releases branch | +| [Browse Plugins](https://github.com/Dispatcharr/Plugins/tree/releases) | Manifests and READMEs on the releases branch | | [Plugin Manifest](https://raw.githubusercontent.com/Dispatcharr/Plugins/releases/manifest.json) | Root plugin index with metadata and download URLs | -| [Download Releases](https://github.com/Dispatcharr/Plugins/tree/releases/zips) | Plugin ZIP files and per-plugin manifests | +| [Download Releases](https://github.com/Dispatcharr/Plugins/releases) | Plugin ZIP assets on GitHub Releases | ## How It Works -Each plugin lives in `plugins//` and must contain a valid `plugin.json` alongside `main.py` and any other code or assets. When a PR is merged to `main`, everything in the plugin folder is automatically packaged into a ZIP and published to the [`releases` branch](https://github.com/Dispatcharr/Plugins/tree/releases) - no separate build step required. +Each plugin lives in `plugins//` and must contain a valid `plugin.json`. When a PR is merged to `main`, the publish workflow runs automatically: + +- **External plugins** (recommended): the ZIP is fetched from your upstream release URL +- **Standard plugins**: everything in the plugin folder is packaged into a ZIP + +The ZIP is then published as a **GitHub Release** on this repository. Manifests and per-plugin READMEs are committed to the [`releases` branch](https://github.com/Dispatcharr/Plugins/tree/releases). ### PR Validation @@ -36,10 +42,11 @@ Results are posted as a comment on the PR. On merge to `main`, each plugin is: -- Packaged into a versioned ZIP (`plugin-name-1.0.0.zip`) and a latest ZIP (`plugin-name-latest.zip`) -- Given an MD5 checksum -- Listed in `manifest.json` with download URLs and metadata -- Only the 10 most recent versioned ZIPs are kept per plugin +- Packaged or fetched as a versioned ZIP +- Given MD5 and SHA256 checksums +- Published as a **GitHub Release** (tag: `plugin-name-1.0.0`) with a `-latest` alias release kept up to date +- Listed in `manifest.json` with download URLs pointing to the GitHub Release assets +- Only the 10 most recent versioned releases are kept per plugin ## Contributing @@ -47,7 +54,7 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for the full guide, including the `plugin ## Downloading Plugins -Visit the [releases branch](https://github.com/Dispatcharr/Plugins/tree/releases) to browse and download plugins, or fetch `manifest.json` programmatically: +Visit the [releases page](https://github.com/Dispatcharr/Plugins/releases) to browse and download plugins, or fetch `manifest.json` programmatically: ```bash curl https://raw.githubusercontent.com/Dispatcharr/Plugins/releases/manifest.json @@ -55,7 +62,7 @@ curl https://raw.githubusercontent.com/Dispatcharr/Plugins/releases/manifest.jso ## Manifest Structure -The root `manifest.json` uses a `root_url` plus relative paths to save space. All URL fields (`manifest_url`, `latest_url`, versioned zip `url`) are relative to `root_url`: +The root `manifest.json` uses a `root_url` plus relative paths for ZIP downloads. `manifest_url` is an absolute URL since per-plugin manifests live on the releases branch (not as GitHub Release assets). ```json { @@ -64,13 +71,13 @@ The root `manifest.json` uses a `root_url` plus relative paths to save space. Al "manifest": { "registry_url": "https://github.com/Dispatcharr/Plugins", "registry_name": "Dispatcharr/Plugins", - "root_url": "https://raw.githubusercontent.com/Dispatcharr/Plugins/releases", + "root_url": "https://github.com/Dispatcharr/Plugins/releases/download", "plugins": [ { "slug": "my-plugin", "name": "My Plugin", - "manifest_url": "zips/my-plugin/manifest.json", - "latest_url": "zips/my-plugin/my-plugin-1.0.0.zip", + "manifest_url": "https://raw.githubusercontent.com/Dispatcharr/Plugins/releases/zips/my-plugin/manifest.json", + "latest_url": "my-plugin-latest/my-plugin-latest.zip", ... } ] @@ -78,7 +85,7 @@ The root `manifest.json` uses a `root_url` plus relative paths to save space. Al } ``` -To resolve a full download URL: `root_url + "/" + latest_url`. +To resolve a full ZIP download URL: `root_url + "/" + latest_url`. The `slug` matches the plugin folder name and can be used to construct other paths (e.g. icon: `plugins//logo.png` on the source branch). @@ -115,4 +122,4 @@ gpg: Signature made ... gpg: Good signature from "..." [full] ``` -The same steps apply to any per-plugin manifest - substitute the path to `zips//manifest.json`. \ No newline at end of file +The same steps apply to any per-plugin manifest - substitute the path to `zips//manifest.json`. From dccf4aca1f2c2083c0734295d8da54f6fa7ec76f Mon Sep 17 00:00:00 2001 From: Seth Van Niekerk Date: Wed, 3 Jun 2026 11:28:50 -0400 Subject: [PATCH 2/4] rename zips to metadata --- .github/scripts/publish/build-zips.sh | 6 +- .github/scripts/publish/cleanup.sh | 2 +- .github/scripts/publish/generate-manifest.sh | 18 +-- .github/scripts/publish/plugin-readmes.sh | 6 +- .github/scripts/publish/releases-readme.sh | 21 ++- .github/scripts/publish/run.sh | 6 +- .github/scripts/publish/yank-version.sh | 4 +- .github/workflows/run-migrations.yml | 161 +++++++++++++++---- README.md | 4 +- 9 files changed, 166 insertions(+), 62 deletions(-) diff --git a/.github/scripts/publish/build-zips.sh b/.github/scripts/publish/build-zips.sh index 01381f9..d6a95bd 100644 --- a/.github/scripts/publish/build-zips.sh +++ b/.github/scripts/publish/build-zips.sh @@ -23,10 +23,10 @@ for plugin_dir in plugins/*/; do plugin_key=${plugin_name//-/_} version=$(jq -r '.version' "$plugin_dir/plugin.json") - mkdir -p "zips/$plugin_name" + mkdir -p "metadata/$plugin_name" zip_path="/tmp/${plugin_name}-${version}.zip" - existing_manifest="zips/$plugin_name/manifest.json" + existing_manifest="metadata/$plugin_name/manifest.json" release_tag="${plugin_name}-${version}" # Skip if a GitHub Release already exists for this version. @@ -117,7 +117,7 @@ for plugin_dir in plugins/*/; do > "$BUILD_META_DIR/$plugin_key/${plugin_key}-${version}.json" # Build release notes - readme_url="https://github.com/${GITHUB_REPOSITORY}/blob/releases/zips/${plugin_name}/README.md" + readme_url="https://github.com/${GITHUB_REPOSITORY}/blob/releases/metadata/${plugin_name}/README.md" release_notes="" if [[ -n "$commit_sha" ]]; then commit_url="https://github.com/${GITHUB_REPOSITORY}/commit/${commit_sha}" diff --git a/.github/scripts/publish/cleanup.sh b/.github/scripts/publish/cleanup.sh index b9b704c..b9fbc68 100644 --- a/.github/scripts/publish/cleanup.sh +++ b/.github/scripts/publish/cleanup.sh @@ -18,7 +18,7 @@ all_tags=$(gh release list --repo "$GITHUB_REPOSITORY" --json tagName --limit 50 # Remove releases for deleted plugins if [[ -d zips ]]; then - for release_dir in zips/*/; do + for release_dir in metadata/*/; do [[ ! -d "$release_dir" ]] && continue plugin_name=$(basename "$release_dir") if [[ ! -d "plugins/$plugin_name" ]]; then diff --git a/.github/scripts/publish/generate-manifest.sh b/.github/scripts/publish/generate-manifest.sh index 7fcb5ec..6a21d34 100644 --- a/.github/scripts/publish/generate-manifest.sh +++ b/.github/scripts/publish/generate-manifest.sh @@ -2,7 +2,7 @@ set -e # publish-generate-manifest.sh -# Generates zips//manifest.json for each plugin and the root manifest.json. +# Generates metadata//manifest.json for each plugin and the root manifest.json. # # Called from the releases branch checkout directory by publish-plugins.sh. # Required env: SOURCE_BRANCH, RELEASES_BRANCH, GITHUB_REPOSITORY @@ -133,8 +133,8 @@ for plugin_dir in plugins/*/; do latest_size_set=false # existing per-plugin manifest from previous run - used as metadata fallback - existing_manifest_file="zips/$plugin_name/manifest.json" - mkdir -p "zips/$plugin_name" + existing_manifest_file="metadata/$plugin_name/manifest.json" + mkdir -p "metadata/$plugin_name" # Discover published versions from GitHub Releases (newest first) versioned_tags=$(echo "$all_release_tags" \ @@ -250,10 +250,10 @@ for plugin_dir in plugins/*/; do } | with_entries(select(.value != null))' \ "$plugin_file") - if write_manifest_if_changed "zips/$plugin_name/manifest.json" "$plugin_entry"; then - sign_manifest "zips/$plugin_name/manifest.json" - elif [[ -n "$gpg_key_id" && "$gpg_signing_failed" -eq 0 ]] && ! sig_is_current "zips/$plugin_name/manifest.json"; then - sign_manifest "zips/$plugin_name/manifest.json" + if write_manifest_if_changed "metadata/$plugin_name/manifest.json" "$plugin_entry"; then + sign_manifest "metadata/$plugin_name/manifest.json" + elif [[ -n "$gpg_key_id" && "$gpg_signing_failed" -eq 0 ]] && ! sig_is_current "metadata/$plugin_name/manifest.json"; then + sign_manifest "metadata/$plugin_name/manifest.json" fi plugin_entries+=("$plugin_entry") @@ -269,7 +269,7 @@ for plugin_dir in plugins/*/; do fi # manifest_url is absolute: per-plugin manifest stays in the releases branch (raw.githubusercontent.com) - plugin_manifest_url="${raw_releases_url}/zips/${plugin_name}/manifest.json" + plugin_manifest_url="${raw_releases_url}/metadata/${plugin_name}/manifest.json" root_entry=$(jq -n \ --argjson latest_metadata "$latest_metadata" \ @@ -338,7 +338,7 @@ if [[ "$gpg_signing_failed" -eq 1 ]] || [[ -z "$gpg_key_id" ]]; then while IFS= read -r -d '' _f; do _tmp=$(mktemp) jq 'del(.signature)' "$_f" > "$_tmp" && mv "$_tmp" "$_f" || rm -f "$_tmp" - done < <(find zips -name "manifest.json" -print0 2>/dev/null) + done < <(find metadata -name "manifest.json" -print0 2>/dev/null) _tmp=$(mktemp) jq 'del(.signature)' manifest.json > "$_tmp" && mv "$_tmp" manifest.json || rm -f "$_tmp" unset _f _tmp diff --git a/.github/scripts/publish/plugin-readmes.sh b/.github/scripts/publish/plugin-readmes.sh index 80310f0..b8dea2e 100644 --- a/.github/scripts/publish/plugin-readmes.sh +++ b/.github/scripts/publish/plugin-readmes.sh @@ -2,7 +2,7 @@ set -e # publish-per-plugin-readmes.sh -# Generates zips//README.md for every plugin. +# Generates metadata//README.md for every plugin. # Version/metadata discovery is driven by the per-plugin manifest.json written # by generate-manifest.sh (which runs before this script). No local ZIPs required. # @@ -33,7 +33,7 @@ for plugin_dir in plugins/*/; do plugin_file="$plugin_dir/plugin.json" [[ ! -f "$plugin_file" ]] && continue - manifest_file="zips/$plugin_name/manifest.json" + manifest_file="metadata/$plugin_name/manifest.json" if [[ ! -f "$manifest_file" ]]; then echo " $plugin_name (no manifest, skipping README)" continue @@ -166,7 +166,7 @@ for plugin_dir in plugins/*/; do echo "" cat "$plugin_dir/README.md" fi - } > "zips/$plugin_name/README.md" + } > "metadata/$plugin_name/README.md" echo " $plugin_name" done diff --git a/.github/scripts/publish/releases-readme.sh b/.github/scripts/publish/releases-readme.sh index 1064e71..0219324 100644 --- a/.github/scripts/publish/releases-readme.sh +++ b/.github/scripts/publish/releases-readme.sh @@ -41,12 +41,18 @@ render_plugin() { local repo_url=${15} local discord_thread=${16} - local zip_url="https://github.com/${GITHUB_REPOSITORY}/raw/$RELEASES_BRANCH/zips/${plugin_name}/${plugin_name}-latest.zip" + local manifest_file="./metadata/${plugin_name}/manifest.json" + local root_url + root_url=$(jq -r '.manifest.root_url // ""' "manifest.json" 2>/dev/null || echo "") + local latest_url_path="" + [[ -f "$manifest_file" ]] && latest_url_path=$(jq -r '.manifest.latest.latest_url // empty' "$manifest_file") + local zip_url="" + [[ -n "$root_url" && -n "$latest_url_path" ]] && zip_url="${root_url}/${latest_url_path}" local source_url="https://github.com/${GITHUB_REPOSITORY}/tree/$SOURCE_BRANCH/plugins/${plugin_name}" local readme_url="https://github.com/${GITHUB_REPOSITORY}/blob/$SOURCE_BRANCH/plugins/${plugin_name}/README.md" - local releases_readme_url="https://github.com/${GITHUB_REPOSITORY}/blob/$RELEASES_BRANCH/zips/${plugin_name}/README.md" + local releases_readme_url="https://github.com/${GITHUB_REPOSITORY}/blob/$RELEASES_BRANCH/metadata/${plugin_name}/README.md" local commit_url="https://github.com/${GITHUB_REPOSITORY}/commit/${commit_sha}" - local releases_dir="./zips/${plugin_name}" + local releases_dir="./metadata/${plugin_name}" local has_source_readme=false [[ -f "plugins/$plugin_name/README.md" ]] && has_source_readme=true @@ -90,7 +96,9 @@ render_plugin() { echo "" fi echo "**Downloads:**" - echo " [Latest Release (\`$version\`)]($zip_url)" + if [[ -n "$zip_url" ]]; then + echo "- [Latest Release (\`$version\`)]($zip_url)" + fi echo "- [All Versions ($version_count available)]($releases_dir)" echo "" @@ -113,7 +121,7 @@ render_plugin() { echo "## Quick Access" echo "" echo "- [manifest.json](./manifest.json) - Complete plugin registry with metadata" - echo "- [zips/](./zips/) - Plugin ZIP files and per-plugin manifests" + echo "- [metadata/](./metadata/) - Per-plugin manifests and READMEs" echo "" echo "## Available Plugins" echo "" @@ -169,8 +177,7 @@ render_plugin() { || date -u +"%Y-%m-%dT%H:%M:%SZ") commit_sha=$(git log -1 --format=%H origin/$SOURCE_BRANCH -- "$plugin_dir" 2>/dev/null || echo "unknown") commit_sha_short=$(git log -1 --format=%h origin/$SOURCE_BRANCH -- "$plugin_dir" 2>/dev/null || echo "unknown") - version_count=$(ls -1 "zips/$plugin_name/${plugin_name}"-*.zip 2>/dev/null \ - | grep -v latest | wc -l | tr -d ' ') + version_count=$(jq -r '.manifest.versions | length' "metadata/$plugin_name/manifest.json" 2>/dev/null || echo "0") plugin_license=$(jq -r '.license // ""' "$plugin_file") min_dispatcharr=$(jq -r '.min_dispatcharr_version // empty' "$plugin_file") max_dispatcharr=$(jq -r '.max_dispatcharr_version // empty' "$plugin_file") diff --git a/.github/scripts/publish/run.sh b/.github/scripts/publish/run.sh index 915dad1..e1c61ae 100644 --- a/.github/scripts/publish/run.sh +++ b/.github/scripts/publish/run.sh @@ -20,7 +20,7 @@ fi RELEASES_BRANCH="releases" MAX_VERSIONED_ZIPS=10 -RELEASES_BRANCH_VERSION=2 +RELEASES_BRANCH_VERSION=3 SCRIPT_DIR="$(dirname "$(readlink -f "$0")")" export SOURCE_BRANCH RELEASES_BRANCH MAX_VERSIONED_ZIPS @@ -74,7 +74,7 @@ if [[ "${FORCE_REBUILD:-false}" == "true" && -n "${FORCE_REBUILD_PLUGIN:-}" ]]; | jq -r '.[].tagName' \ | grep "^${FORCE_REBUILD_PLUGIN}-" \ | xargs -I{} gh release delete {} --repo "$GITHUB_REPOSITORY" --yes --cleanup-tag 2>/dev/null || true - rm -f "zips/$FORCE_REBUILD_PLUGIN/manifest.json" + rm -f "metadata/$FORCE_REBUILD_PLUGIN/manifest.json" elif [[ "${FORCE_REBUILD:-false}" == "true" ]]; then echo "Force rebuild requested - deleting all plugin GitHub Releases and resetting $RELEASES_BRANCH." git fetch origin $SOURCE_BRANCH 2>/dev/null || true @@ -155,7 +155,7 @@ rm -rf plugins git rm -rf --cached plugins 2>/dev/null || true echo "$RELEASES_BRANCH_VERSION" > REPO_VER -git add zips manifest.json README.md REPO_VER +git add metadata manifest.json README.md REPO_VER if git diff --cached --quiet; then echo "No changes to commit." diff --git a/.github/scripts/publish/yank-version.sh b/.github/scripts/publish/yank-version.sh index db2f178..65e5fbe 100644 --- a/.github/scripts/publish/yank-version.sh +++ b/.github/scripts/publish/yank-version.sh @@ -68,7 +68,7 @@ else exit 1 fi -ZIP_DIR="zips/$YANK_PLUGIN" +ZIP_DIR="metadata/$YANK_PLUGIN" RELEASE_TAG="${YANK_PLUGIN}-${YANK_VERSION}" PLUGIN_MANIFEST="$ZIP_DIR/manifest.json" @@ -172,7 +172,7 @@ echo "=== Committing ===" rm -rf plugins git rm -rf --cached plugins 2>/dev/null || true -git add zips manifest.json README.md +git add metadata manifest.json README.md if git diff --cached --quiet; then echo "No changes to commit - was this version already absent?" diff --git a/.github/workflows/run-migrations.yml b/.github/workflows/run-migrations.yml index d1c21c9..47c76d0 100644 --- a/.github/workflows/run-migrations.yml +++ b/.github/workflows/run-migrations.yml @@ -2,6 +2,7 @@ name: Run Migrations # Migration history: # 001 (v0 → v2): Move ZIPs from releases branch git history to GitHub Release assets +# 002 (v2 → v3): Rename zips/ to metadata/ on the releases branch permissions: contents: write @@ -34,10 +35,11 @@ jobs: outputs: any_pending: ${{ steps.detect.outputs.any_pending }} needs_001: ${{ steps.detect.outputs.needs_001 }} + needs_002: ${{ steps.detect.outputs.needs_002 }} use_app: ${{ steps.config.outputs.use_app }} app_id: ${{ steps.config.outputs.app_id }} # Add outputs for future migrations here: - # needs_002: ${{ steps.detect.outputs.needs_002 }} + # needs_003: ${{ steps.detect.outputs.needs_003 }} steps: - name: Validate inputs run: | @@ -68,7 +70,7 @@ jobs: id: detect run: | RELEASES_BRANCH="releases" - TARGET_VER=2 # bump when adding a new migration + TARGET_VER=3 # bump when adding a new migration current_ver=0 if git ls-remote --exit-code --heads origin "$RELEASES_BRANCH" >/dev/null 2>&1; then @@ -79,9 +81,12 @@ jobs: (( current_ver < 2 )) && echo "needs_001=true" >> "$GITHUB_OUTPUT" \ || echo "needs_001=false" >> "$GITHUB_OUTPUT" + (( current_ver < 3 )) && echo "needs_002=true" >> "$GITHUB_OUTPUT" \ + || echo "needs_002=false" >> "$GITHUB_OUTPUT" + # Add future migrations here: - # (( current_ver < 3 )) && echo "needs_002=true" >> "$GITHUB_OUTPUT" \ - # || echo "needs_002=false" >> "$GITHUB_OUTPUT" + # (( current_ver < 4 )) && echo "needs_003=true" >> "$GITHUB_OUTPUT" \ + # || echo "needs_003=false" >> "$GITHUB_OUTPUT" (( current_ver < TARGET_VER )) && echo "any_pending=true" >> "$GITHUB_OUTPUT" \ || echo "any_pending=false" >> "$GITHUB_OUTPUT" @@ -311,21 +316,93 @@ jobs: echo "end_version=2" >> "$GITHUB_OUTPUT" # --------------------------------------------------------------------------- - # Add future migration jobs here following the same pattern: - # - # migration_002: - # needs: [setup] - # if: needs.setup.outputs.needs_002 == 'true' - # runs-on: ubuntu-latest - # outputs: - # needs_rebuild: ${{ steps.flags.outputs.needs_rebuild }} - # steps: - # - ... - # - name: Set migration flags - # id: flags - # run: | - # echo "rebuild_type=none" >> "$GITHUB_OUTPUT" # force | regular | none - # echo "end_version=3" >> "$GITHUB_OUTPUT" + # Migration 002 (v2 → v3): Rename zips/ to metadata/ on the releases branch + # needs_rebuild=regular — manifest_url paths change from zips/ to metadata/. + # --------------------------------------------------------------------------- + migration_002: + needs: [setup, migration_001] + if: needs.setup.outputs.needs_002 == 'true' + runs-on: ubuntu-latest + outputs: + rebuild_type: ${{ steps.flags.outputs.rebuild_type }} + end_version: ${{ steps.flags.outputs.end_version }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Generate GitHub App token + if: needs.setup.outputs.use_app == 'true' + id: app-token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ needs.setup.outputs.app_id }} + private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} + + - name: Rename zips/ to metadata/ on releases branch + env: + GITHUB_TOKEN: ${{ needs.setup.outputs.use_app == 'true' && steps.app-token.outputs.token || github.token }} + APP_SLUG: ${{ steps.app-token.outputs.app-slug }} + GITHUB_REPOSITORY: ${{ github.repository }} + DRY_RUN: ${{ inputs.dry_run }} + run: | + set -euo pipefail + RELEASES_BRANCH="releases" + + if [[ "$DRY_RUN" == "true" ]]; then + count=$(git ls-tree -r --name-only "origin/${RELEASES_BRANCH}" 2>/dev/null \ + | grep '^zips/' | grep -cv '\.zip$' || true) + echo "Dry run: would rename $count file(s) from zips/ to metadata/." + exit 0 + fi + + if ! git ls-remote --exit-code --heads origin "$RELEASES_BRANCH" >/dev/null 2>&1; then + echo "No $RELEASES_BRANCH branch — nothing to rename." + exit 0 + fi + + if ! git ls-tree -r --name-only "origin/${RELEASES_BRANCH}" | grep -q '^zips/'; then + echo "No zips/ content found — already renamed or branch is clean." + exit 0 + fi + + if [[ -n "${APP_SLUG:-}" ]]; then + BOT_USER_ID=$(gh api "/users/${APP_SLUG}%5Bbot%5D" --jq '.id' 2>/dev/null || echo "") + git config user.name "${APP_SLUG}[bot]" + git config user.email "${BOT_USER_ID:+${BOT_USER_ID}+}${APP_SLUG}[bot]@users.noreply.github.com" + else + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + fi + + git fetch origin "$RELEASES_BRANCH" + git checkout "$RELEASES_BRANCH" + + mapfile -t to_move < <(git ls-files 'zips/' | grep -v '\.zip$') + if [[ ${#to_move[@]} -eq 0 ]]; then + echo "No non-ZIP files under zips/ to move." + exit 0 + fi + + for f in "${to_move[@]}"; do + dest="${f/zips\//metadata/}" + mkdir -p "$(dirname "$dest")" + git mv "$f" "$dest" + done + + git commit -m "Rename zips/ to metadata/ on releases branch [skip ci]" + git push "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" "$RELEASES_BRANCH" + echo "Moved ${#to_move[@]} file(s) from zips/ to metadata/." + + - name: Set migration flags + id: flags + run: | + echo "rebuild_type=regular" >> "$GITHUB_OUTPUT" + echo "end_version=3" >> "$GITHUB_OUTPUT" + + # --------------------------------------------------------------------------- + # Add future migration jobs here following the same pattern. # --------------------------------------------------------------------------- # --------------------------------------------------------------------------- @@ -333,7 +410,7 @@ jobs: # Skipped on dry runs or if no migrations were pending. # --------------------------------------------------------------------------- write_repo_ver: - needs: [setup, migration_001] + needs: [setup, migration_001, migration_002] if: | always() && needs.setup.outputs.any_pending == 'true' && @@ -405,16 +482,40 @@ jobs: fi # --------------------------------------------------------------------------- - # Triggers a regular publish if any migration flagged rebuild_type=regular - # and no migration flagged force (force is a superset — regular is skipped). - # When adding migration_002: extend the 'regular' OR and add a 'force' != guard. + # Aggregates rebuild_type across all migrations using toJSON(needs). + # When adding a new migration: add it to needs: here only. + # The trigger jobs below never need changing. + # --------------------------------------------------------------------------- + aggregate_migrations: + needs: [migration_001, migration_002] + if: always() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') + runs-on: ubuntu-latest + outputs: + rebuild_type: ${{ steps.compute.outputs.rebuild_type }} + steps: + - name: Compute highest rebuild type + id: compute + env: + NEEDS_JSON: ${{ toJSON(needs) }} + run: | + rebuild=$(echo "$NEEDS_JSON" | jq -r ' + [.. | objects | .outputs.rebuild_type? | select(. != null and . != "")] | + if any(. == "force") then "force" + elif any(. == "regular") then "regular" + else "none" end + ') + echo "rebuild_type=$rebuild" >> "$GITHUB_OUTPUT" + echo "Highest rebuild type across all migrations: $rebuild" + + # --------------------------------------------------------------------------- + # Trigger jobs check aggregate_migrations.outputs.rebuild_type — no changes + # needed here when adding future migrations. # --------------------------------------------------------------------------- trigger_publish_regular: - needs: [setup, migration_001, write_repo_ver] + needs: [setup, aggregate_migrations, write_repo_ver] if: | needs.write_repo_ver.result == 'success' && - needs.migration_001.outputs.rebuild_type == 'regular' && - needs.migration_001.outputs.rebuild_type != 'force' + needs.aggregate_migrations.outputs.rebuild_type == 'regular' runs-on: ubuntu-latest steps: - name: Generate GitHub App token @@ -434,15 +535,11 @@ jobs: echo "Regular publish triggered — manifests and READMEs will be regenerated shortly." echo "Regular publish triggered." >> "$GITHUB_STEP_SUMMARY" - # --------------------------------------------------------------------------- - # Triggers a force-rebuild publish if any migration flagged rebuild_type=force. - # When adding migration_002: extend the 'force' OR condition. - # --------------------------------------------------------------------------- trigger_publish_force: - needs: [setup, migration_001, write_repo_ver] + needs: [setup, aggregate_migrations, write_repo_ver] if: | needs.write_repo_ver.result == 'success' && - needs.migration_001.outputs.rebuild_type == 'force' + needs.aggregate_migrations.outputs.rebuild_type == 'force' runs-on: ubuntu-latest steps: - name: Generate GitHub App token diff --git a/README.md b/README.md index 020f3e3..c2adfd1 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ The root `manifest.json` uses a `root_url` plus relative paths for ZIP downloads { "slug": "my-plugin", "name": "My Plugin", - "manifest_url": "https://raw.githubusercontent.com/Dispatcharr/Plugins/releases/zips/my-plugin/manifest.json", + "manifest_url": "https://raw.githubusercontent.com/Dispatcharr/Plugins/releases/metadata/my-plugin/manifest.json", "latest_url": "my-plugin-latest/my-plugin-latest.zip", ... } @@ -122,4 +122,4 @@ gpg: Signature made ... gpg: Good signature from "..." [full] ``` -The same steps apply to any per-plugin manifest - substitute the path to `zips//manifest.json`. +The same steps apply to any per-plugin manifest - substitute the path to `metadata//manifest.json`. From fa78a6520070bad7ae152aaa484b788bf9baa122 Mon Sep 17 00:00:00 2001 From: Seth Van Niekerk Date: Wed, 3 Jun 2026 12:07:31 -0400 Subject: [PATCH 3/4] bump GitHub Actions to latest major versions --- .github/workflows/codeql.yml | 2 +- .github/workflows/publish-plugins.yml | 6 +- .github/workflows/run-migrations.yml | 28 ++++---- .github/workflows/update-external-readme.yml | 6 +- .github/workflows/validate-plugin.yml | 72 ++++++++++---------- .github/workflows/yank-plugin-version.yml | 6 +- 6 files changed, 60 insertions(+), 60 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 661e3e1..0b03718 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -19,7 +19,7 @@ jobs: security-events: write steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Initialize CodeQL uses: github/codeql-action/init@v4 diff --git a/.github/workflows/publish-plugins.yml b/.github/workflows/publish-plugins.yml index 01ced0b..dce61b2 100644 --- a/.github/workflows/publish-plugins.yml +++ b/.github/workflows/publish-plugins.yml @@ -48,7 +48,7 @@ jobs: timeout-minutes: 15 steps: - name: Checkout main - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 @@ -68,9 +68,9 @@ jobs: - name: Generate GitHub App token if: steps.config.outputs.use_app == 'true' id: app-token - uses: actions/create-github-app-token@v1 + uses: actions/create-github-app-token@v3 with: - app-id: ${{ steps.config.outputs.app_id }} + client-id: ${{ steps.config.outputs.app_id }} private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} - name: Log fallback to actions token diff --git a/.github/workflows/run-migrations.yml b/.github/workflows/run-migrations.yml index 47c76d0..b595713 100644 --- a/.github/workflows/run-migrations.yml +++ b/.github/workflows/run-migrations.yml @@ -49,7 +49,7 @@ jobs: fi - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 @@ -104,16 +104,16 @@ jobs: end_version: ${{ steps.flags.outputs.end_version }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 - name: Generate GitHub App token if: needs.setup.outputs.use_app == 'true' id: app-token - uses: actions/create-github-app-token@v1 + uses: actions/create-github-app-token@v3 with: - app-id: ${{ needs.setup.outputs.app_id }} + client-id: ${{ needs.setup.outputs.app_id }} private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} - name: Run migration @@ -328,16 +328,16 @@ jobs: end_version: ${{ steps.flags.outputs.end_version }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 - name: Generate GitHub App token if: needs.setup.outputs.use_app == 'true' id: app-token - uses: actions/create-github-app-token@v1 + uses: actions/create-github-app-token@v3 with: - app-id: ${{ needs.setup.outputs.app_id }} + client-id: ${{ needs.setup.outputs.app_id }} private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} - name: Rename zips/ to metadata/ on releases branch @@ -420,16 +420,16 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 - name: Generate GitHub App token if: needs.setup.outputs.use_app == 'true' id: app-token - uses: actions/create-github-app-token@v1 + uses: actions/create-github-app-token@v3 with: - app-id: ${{ needs.setup.outputs.app_id }} + client-id: ${{ needs.setup.outputs.app_id }} private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} - name: Write REPO_VER @@ -521,9 +521,9 @@ jobs: - name: Generate GitHub App token if: needs.setup.outputs.use_app == 'true' id: app-token - uses: actions/create-github-app-token@v1 + uses: actions/create-github-app-token@v3 with: - app-id: ${{ needs.setup.outputs.app_id }} + client-id: ${{ needs.setup.outputs.app_id }} private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} - name: Trigger regular publish @@ -545,9 +545,9 @@ jobs: - name: Generate GitHub App token if: needs.setup.outputs.use_app == 'true' id: app-token - uses: actions/create-github-app-token@v1 + uses: actions/create-github-app-token@v3 with: - app-id: ${{ needs.setup.outputs.app_id }} + client-id: ${{ needs.setup.outputs.app_id }} private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} - name: Trigger force rebuild publish diff --git a/.github/workflows/update-external-readme.yml b/.github/workflows/update-external-readme.yml index a39ef9f..8341770 100644 --- a/.github/workflows/update-external-readme.yml +++ b/.github/workflows/update-external-readme.yml @@ -40,16 +40,16 @@ jobs: - name: Generate GitHub App token if: steps.config.outputs.skip != 'true' id: app-token - uses: actions/create-github-app-token@v1 + uses: actions/create-github-app-token@v3 with: - app-id: ${{ steps.config.outputs.app_id }} + client-id: ${{ steps.config.outputs.app_id }} private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} # Token is org-scoped; the app must be installed on the target repo owner: ${{ github.repository_owner }} - name: Checkout releases branch README if: steps.config.outputs.skip != 'true' - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: ref: releases fetch-depth: 1 diff --git a/.github/workflows/validate-plugin.yml b/.github/workflows/validate-plugin.yml index 69b3cce..295ee86 100644 --- a/.github/workflows/validate-plugin.yml +++ b/.github/workflows/validate-plugin.yml @@ -35,7 +35,7 @@ jobs: timeout-minutes: 5 steps: - name: Checkout config - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: ref: ${{ github.event.pull_request.base.ref }} fetch-depth: 1 @@ -58,9 +58,9 @@ jobs: - name: Generate GitHub App token if: steps.config.outputs.use_app == 'true' id: app-token - uses: actions/create-github-app-token@v1 + uses: actions/create-github-app-token@v3 with: - app-id: ${{ steps.config.outputs.app_id }} + client-id: ${{ steps.config.outputs.app_id }} private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} - name: Log fallback to actions token @@ -105,7 +105,7 @@ jobs: has_updated_plugin: ${{ steps.detect.outputs.has_updated_plugin }} steps: - name: Checkout base branch scripts (trusted) - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: repository: ${{ github.repository }} ref: ${{ github.event.pull_request.base.ref }} @@ -120,7 +120,7 @@ jobs: run: cp -r .github/scripts /tmp/trusted-scripts - name: Checkout PR plugins (untrusted content only) - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: repository: ${{ github.event.pull_request.head.repo.full_name }} ref: ${{ github.event.pull_request.head.sha }} @@ -151,9 +151,9 @@ jobs: - name: Generate GitHub App token if: steps.config.outputs.use_app == 'true' id: app-token - uses: actions/create-github-app-token@v1 + uses: actions/create-github-app-token@v3 with: - app-id: ${{ steps.config.outputs.app_id }} + client-id: ${{ steps.config.outputs.app_id }} private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} - name: Log fallback to actions token @@ -238,7 +238,7 @@ jobs: timeout-minutes: 2 steps: - name: Checkout config - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: ref: ${{ github.event.pull_request.base.ref }} fetch-depth: 1 @@ -261,9 +261,9 @@ jobs: - name: Generate GitHub App token if: steps.config.outputs.use_app == 'true' id: app-token - uses: actions/create-github-app-token@v1 + uses: actions/create-github-app-token@v3 with: - app-id: ${{ steps.config.outputs.app_id }} + client-id: ${{ steps.config.outputs.app_id }} private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} - name: Log fallback to actions token @@ -416,7 +416,7 @@ jobs: codeql_unscanned_langs: ${{ steps.status.outputs.codeql_unscanned_langs }} steps: - name: Checkout PR merge commit for analysis - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: ref: refs/pull/${{ github.event.pull_request.number }}/merge # Minimal placeholder - overridden immediately by the next step @@ -759,7 +759,7 @@ jobs: - name: Upload findings detail for PR comment if: always() && steps.status.outputs.codeql_status == 'failure' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: codeql-findings path: codeql-findings.md @@ -767,7 +767,7 @@ jobs: - name: Upload medium findings detail for PR comment if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: codeql-medium-findings path: codeql-medium-findings.md @@ -775,7 +775,7 @@ jobs: - name: Upload low findings detail for PR comment if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: codeql-low-findings path: codeql-low-findings.md @@ -798,7 +798,7 @@ jobs: clamav_infected: ${{ steps.status.outputs.clamav_infected }} steps: - name: Checkout PR merge commit for scan - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: ref: refs/pull/${{ github.event.pull_request.number }}/merge sparse-checkout: .gitignore @@ -819,7 +819,7 @@ jobs: - name: Cache ClamAV installation (weekly) id: cache-clamav-install - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: /tmp/clamav-apt key: clamav-install-${{ runner.os }}-${{ steps.cache-keys.outputs.week }} @@ -827,7 +827,7 @@ jobs: - name: Cache ClamAV virus definitions (daily) id: cache-clamav-defs - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: /tmp/clamav-db key: clamav-defs-${{ runner.os }}-${{ steps.cache-keys.outputs.date }} @@ -941,7 +941,7 @@ jobs: - name: Upload ClamAV findings for PR comment if: always() && steps.status.outputs.clamav_status == 'failure' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: clamav-findings path: clamav-findings.md @@ -964,9 +964,9 @@ jobs: - name: Generate GitHub App token if: always() && steps.status.outputs.clamav_status == 'failure' && steps.config.outputs.use_app == 'true' id: app-token - uses: actions/create-github-app-token@v1 + uses: actions/create-github-app-token@v3 with: - app-id: ${{ steps.config.outputs.app_id }} + client-id: ${{ steps.config.outputs.app_id }} private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} - name: Apply quarantine label @@ -1000,7 +1000,7 @@ jobs: plugin: ${{ fromJson(needs.detect-changes.outputs.matrix) }} steps: - name: Checkout base branch scripts (trusted) - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: repository: ${{ github.repository }} ref: ${{ github.event.pull_request.base.ref }} @@ -1015,7 +1015,7 @@ jobs: run: cp -r .github/scripts /tmp/trusted-scripts - name: Checkout PR plugins (untrusted content only) - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: repository: ${{ github.event.pull_request.head.repo.full_name }} ref: ${{ github.event.pull_request.head.sha }} @@ -1046,9 +1046,9 @@ jobs: - name: Generate GitHub App token if: steps.config.outputs.use_app == 'true' id: app-token - uses: actions/create-github-app-token@v1 + uses: actions/create-github-app-token@v3 with: - app-id: ${{ steps.config.outputs.app_id }} + client-id: ${{ steps.config.outputs.app_id }} private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} - name: Log fallback to actions token @@ -1073,7 +1073,7 @@ jobs: continue-on-error: true - name: Upload report fragment - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: fragment-${{ matrix.plugin }} path: ${{ matrix.plugin }}.fragment.md @@ -1093,7 +1093,7 @@ jobs: timeout-minutes: 5 steps: - name: Checkout scripts - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: ref: ${{ github.event.pull_request.base.ref }} fetch-depth: 1 @@ -1116,9 +1116,9 @@ jobs: - name: Generate GitHub App token if: steps.config.outputs.use_app == 'true' id: app-token - uses: actions/create-github-app-token@v1 + uses: actions/create-github-app-token@v3 with: - app-id: ${{ steps.config.outputs.app_id }} + client-id: ${{ steps.config.outputs.app_id }} private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} - name: Log fallback to actions token @@ -1127,7 +1127,7 @@ jobs: printf '## ⚠️ GitHub App token not available\n\nGH_APP_ID or GH_APP_PRIVATE_KEY not configured. Falling back to `GITHUB_TOKEN` (github-actions[bot] identity).\n' >> "$GITHUB_STEP_SUMMARY" - name: Download all report fragments - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: pattern: fragment-* path: fragments @@ -1135,28 +1135,28 @@ jobs: continue-on-error: true - name: Download CodeQL findings detail - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: codeql-findings path: codeql-findings continue-on-error: true - name: Download CodeQL medium findings detail - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: codeql-medium-findings path: codeql-medium-findings continue-on-error: true - name: Download CodeQL low findings detail - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: codeql-low-findings path: codeql-low-findings continue-on-error: true - name: Download ClamAV findings detail - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: clamav-findings path: clamav-findings @@ -1210,7 +1210,7 @@ jobs: timeout-minutes: 5 steps: - name: Checkout scripts - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: ref: ${{ github.event.pull_request.base.ref }} fetch-depth: 1 @@ -1233,9 +1233,9 @@ jobs: - name: Generate GitHub App token if: steps.config.outputs.use_app == 'true' id: app-token - uses: actions/create-github-app-token@v1 + uses: actions/create-github-app-token@v3 with: - app-id: ${{ steps.config.outputs.app_id }} + client-id: ${{ steps.config.outputs.app_id }} private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} - name: Log fallback to actions token diff --git a/.github/workflows/yank-plugin-version.yml b/.github/workflows/yank-plugin-version.yml index aae639b..bac13b2 100644 --- a/.github/workflows/yank-plugin-version.yml +++ b/.github/workflows/yank-plugin-version.yml @@ -30,7 +30,7 @@ jobs: timeout-minutes: 15 steps: - name: Checkout main - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 @@ -50,9 +50,9 @@ jobs: - name: Generate GitHub App token if: steps.config.outputs.use_app == 'true' id: app-token - uses: actions/create-github-app-token@v1 + uses: actions/create-github-app-token@v3 with: - app-id: ${{ steps.config.outputs.app_id }} + client-id: ${{ steps.config.outputs.app_id }} private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} - name: Log fallback to actions token From 9f95ece73c445ae36b5ed943eb9a637fea626851 Mon Sep 17 00:00:00 2001 From: Seth Van Niekerk Date: Wed, 3 Jun 2026 12:44:36 -0400 Subject: [PATCH 4/4] remove [skip ci] from releases branch commit messages --- .github/scripts/publish/run.sh | 4 +--- .github/scripts/publish/yank-version.sh | 3 +-- .github/workflows/run-migrations.yml | 6 +++--- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/.github/scripts/publish/run.sh b/.github/scripts/publish/run.sh index e1c61ae..b7d792f 100644 --- a/.github/scripts/publish/run.sh +++ b/.github/scripts/publish/run.sh @@ -195,9 +195,7 @@ else git commit -m "Publish plugin updates from $SOURCE_BRANCH -Source commit: $source_commit${plugin_list} - -[skip ci]" +Source commit: $source_commit${plugin_list}" git push "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" $RELEASES_BRANCH echo "Successfully published to ${RELEASES_BRANCH}" diff --git a/.github/scripts/publish/yank-version.sh b/.github/scripts/publish/yank-version.sh index 65e5fbe..f0056a4 100644 --- a/.github/scripts/publish/yank-version.sh +++ b/.github/scripts/publish/yank-version.sh @@ -179,8 +179,7 @@ if git diff --cached --quiet; then else git commit -m "Yank ${YANK_PLUGIN} v${YANK_VERSION} -Refs #${YANK_ISSUE} -[skip ci]" +Refs #${YANK_ISSUE}" RELEASES_COMMIT=$(git rev-parse --short HEAD) git push "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" $RELEASES_BRANCH echo "Successfully yanked ${YANK_PLUGIN} v${YANK_VERSION} from ${RELEASES_BRANCH}" diff --git a/.github/workflows/run-migrations.yml b/.github/workflows/run-migrations.yml index b595713..f0cedb9 100644 --- a/.github/workflows/run-migrations.yml +++ b/.github/workflows/run-migrations.yml @@ -305,7 +305,7 @@ jobs: exit 0 fi git rm -f "${tracked_zips[@]}" - git commit -m "Strip tracked ZIPs from releases branch [skip ci]" + git commit -m "Strip tracked ZIPs from releases branch" git push "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" "$RELEASES_BRANCH" echo "Removed ${#tracked_zips[@]} tracked ZIP(s) from $RELEASES_BRANCH." @@ -391,7 +391,7 @@ jobs: git mv "$f" "$dest" done - git commit -m "Rename zips/ to metadata/ on releases branch [skip ci]" + git commit -m "Rename zips/ to metadata/ on releases branch" git push "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" "$RELEASES_BRANCH" echo "Moved ${#to_move[@]} file(s) from zips/ to metadata/." @@ -476,7 +476,7 @@ jobs: if git diff --cached --quiet; then echo "REPO_VER already up to date." else - git commit -m "Set REPO_VER=${TARGET_VER} [skip ci]" + git commit -m "Set REPO_VER=${TARGET_VER}" git push "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" "$RELEASES_BRANCH" echo "REPO_VER=${TARGET_VER} written to $RELEASES_BRANCH." fi