Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
b6e47a0
Add workflow to update external README from plugin releases
sethwv Mar 27, 2026
b0afcfc
Bump version to 2.4.69 in plugin.json
sethwv Mar 27, 2026
58a6dcf
Refactor update-external-readme workflow and add configuration script
sethwv Mar 27, 2026
846d628
Enhance README processing by stripping preamble and rewriting relativ…
sethwv Mar 27, 2026
44b3454
Update plugin releases README message and PR title for clarity
sethwv Mar 27, 2026
628b3a3
Enhance GitHub Actions workflows to support GitHub App authentication…
sethwv Mar 27, 2026
f437e2b
Enhance README update process to skip commits with only timestamp cha…
sethwv Mar 27, 2026
bc2d4bd
Enhance GitHub workflows to require GH_APP_PRIVATE_KEY alongside GH_A…
sethwv Mar 27, 2026
702723a
Enhance validate-plugin workflow to specify branch reference and fetc…
sethwv Mar 27, 2026
81febc3
Enhance GitHub Actions to configure bot identity for commits, improvi…
sethwv Mar 27, 2026
e51bfd1
Enhance plugin publishing process to use Git Data API for commits, im…
sethwv Mar 27, 2026
b7d8b14
Refactor add_blob and tree creation to use temporary files for JSON b…
sethwv Mar 27, 2026
0611c99
Rename workflow for updating external README to clarify its purpose a…
sethwv Mar 27, 2026
8eb59f4
Refactor commit and push process in publish script to simplify logic …
sethwv Mar 27, 2026
572c3da
Fix comment formatting in scripts and workflows for consistency
sethwv Mar 27, 2026
92014c0
Remove config.env file and update workflows to use environment variab…
sethwv Mar 27, 2026
7e38e34
Add PR reviewer support to update-external-readme workflow for automa…
sethwv Mar 27, 2026
3d0096d
Enhance PR reviewer support in update-external-readme workflow to han…
sethwv Mar 27, 2026
976f408
Add contribution link to README generation process in update-external…
sethwv Mar 27, 2026
376daf3
Update README and CONTRIBUTING files for clarity on plugin structure …
sethwv Mar 27, 2026
1aab822
Add Dispatcharr compatibility information to plugin README generation
sethwv Mar 27, 2026
4123e4b
Update CONTRIBUTING.md to clarify metadata-only versioning exceptions…
sethwv Mar 27, 2026
80ce371
Add pull request template to standardize submissions
sethwv Mar 27, 2026
5643110
Refactor pull request template comments for clarity and formatting
sethwv Mar 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<!--
Read CONTRIBUTING.md before submitting: https://github.com/Dispatcharr/Plugins/blob/main/CONTRIBUTING.md

Suggested PR title format: [your-plugin-name]: brief summary of change
e.g. [dispatcharr-exporter]: add initial release or [my-plugin]: bump to 1.2.0
-->

## About this submission

<!-- Briefly describe the change: new plugin, update, metadata change, etc. -->

## Pre-submission checklist

<!-- Tick each box that applies. The bot will validate automatically, but catching issues here saves time. -->

**If this is a new plugin:**
- [ ] Plugin folder is named `lowercase-kebab-case`
- [ ] `plugin.json` contains all required fields (`name`, `version`, `description`, `author` or `maintainers`, `license`)
- [ ] My GitHub username is in `author` or `maintainers`
- [ ] `license` is a valid [OSI-approved SPDX identifier](https://spdx.org/licenses/) (e.g. `MIT`, `Apache-2.0`)
- [ ] I have tested the plugin against a running Dispatcharr instance

**If this is an update to an existing plugin:**
- [ ] `version` in `plugin.json` is incremented (unless this is a metadata-only change - see [Versioning](https://github.com/Dispatcharr/Plugins/blob/main/CONTRIBUTING.md#versioning))
- [ ] I am listed in `author` or `maintainers` of the existing plugin
14 changes: 14 additions & 0 deletions .github/scripts/publish/plugin-readmes.sh
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ for plugin_dir in plugins/*/; do
repo_url=$(jq -r '.repo_url // empty' "$plugin_file")
discord_thread=$(jq -r '.discord_thread // empty' "$plugin_file")
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")
has_readme=false
[[ -f "$plugin_dir/README.md" ]] && has_readme=true

Expand All @@ -48,6 +50,18 @@ for plugin_dir in plugins/*/; do
echo "**License:** [$license](https://spdx.org/licenses/${license}.html)"
echo ""
fi
if [[ -n "$min_dispatcharr" || -n "$max_dispatcharr" ]]; then
compat=""
if [[ -n "$min_dispatcharr" && -n "$max_dispatcharr" ]]; then
compat="$min_dispatcharr – $max_dispatcharr"
elif [[ -n "$min_dispatcharr" ]]; then
compat="$min_dispatcharr+"
else
compat="up to $max_dispatcharr"
fi
echo "**Dispatcharr Compatibility:** $compat"
echo ""
fi
echo "## Downloads"
echo ""
echo "### Latest Release"
Expand Down
26 changes: 23 additions & 3 deletions .github/scripts/publish/releases-readme.sh
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ render_plugin() {
local commit_sha_short=${10}
local version_count=${11}
local license=${12}
local min_dispatcharr=${13}
local max_dispatcharr=${14}

local zip_url="https://github.com/${GITHUB_REPOSITORY}/raw/$RELEASES_BRANCH/zips/${plugin_name}/${plugin_name}-latest.zip"
local source_url="https://github.com/${GITHUB_REPOSITORY}/tree/$SOURCE_BRANCH/plugins/${plugin_name}"
Expand All @@ -49,8 +51,20 @@ render_plugin() {
echo "**License:** [$license](https://spdx.org/licenses/${license}.html)"
echo ""
fi
if [[ -n "$min_dispatcharr" || -n "$max_dispatcharr" ]]; then
compat=""
if [[ -n "$min_dispatcharr" && -n "$max_dispatcharr" ]]; then
compat="$min_dispatcharr – $max_dispatcharr"
elif [[ -n "$min_dispatcharr" ]]; then
compat="$min_dispatcharr+"
else
compat="up to $max_dispatcharr"
fi
echo "**Dispatcharr Compatibility:** $compat"
echo ""
fi
echo "**Downloads:**"
echo "- [Latest Release (\`$version\`)]($zip_url)"
echo " [Latest Release (\`$version\`)]($zip_url)"
echo "- [All Versions ($version_count available)]($releases_dir)"
echo ""

Expand Down Expand Up @@ -132,9 +146,12 @@ render_plugin() {
version_count=$(ls -1 "zips/$plugin_name/${plugin_name}"-*.zip 2>/dev/null \
| grep -v latest | wc -l | tr -d ' ')
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")

render_plugin "false" "$plugin_name" "$name" "$version" "$author" "$description" \
"$maintainers" "$last_updated" "$commit_sha" "$commit_sha_short" "$version_count" "$plugin_license"
"$maintainers" "$last_updated" "$commit_sha" "$commit_sha_short" "$version_count" "$plugin_license" \
"$min_dispatcharr" "$max_dispatcharr"
done

# Deprecated section (only if any exist)
Expand Down Expand Up @@ -176,9 +193,12 @@ render_plugin() {
version_count=$(ls -1 "zips/$plugin_name/${plugin_name}"-*.zip 2>/dev/null \
| grep -v latest | wc -l | tr -d ' ')
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")

render_plugin "true" "$plugin_name" "$name" "$version" "$author" "$description" \
"$maintainers" "$last_updated" "$commit_sha" "$commit_sha_short" "$version_count" "$plugin_license"
"$maintainers" "$last_updated" "$commit_sha" "$commit_sha_short" "$version_count" "$plugin_license" \
"$min_dispatcharr" "$max_dispatcharr"
done
fi

Expand Down
68 changes: 55 additions & 13 deletions .github/scripts/publish/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,23 @@ echo "Cloning repository..."
git clone --no-checkout "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" "$WORK_DIR/repo"
cd "$WORK_DIR/repo"

# Configure git
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
# Configure git - use GitHub App bot identity when available, otherwise fall back
# to the generic github-actions[bot] identity.
# NOTE: the email uses the bot *user* ID (not the App ID) - GitHub resolves commit
# authorship by matching this ID+slug[bot]@users.noreply.github.com to the bot account.
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
# Fallback if the API call fails - avatar may not resolve but commits still work
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

# Checkout or create releases branch
echo "Setting up $RELEASES_BRANCH branch..."
Expand Down Expand Up @@ -93,24 +107,52 @@ echo ""
echo "=== Committing ==="
rm -rf plugins
git rm -rf --cached plugins 2>/dev/null || true

git add zips manifest.json README.md

if git diff --cached --quiet; then
echo "No changes to commit."
else
source_commit=$(git rev-parse --short origin/$SOURCE_BRANCH)

plugin_list=""
if [[ -s changed_plugins.txt ]]; then
plugin_list="$(printf '\n\n')$(sed 's/^/- /' changed_plugins.txt)"
fi

git commit -m "Publish plugin updates from $SOURCE_BRANCH
# 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.
only_timestamps=true
while IFS= read -r changed_file; do
case "$changed_file" in
README.md)
new_content=$(git show :README.md | grep -v '^\*Last updated:')
old_content=$(git show HEAD:README.md 2>/dev/null | grep -v '^\*Last updated:' || true)
[[ "$new_content" == "$old_content" ]] || only_timestamps=false
;;
manifest.json)
new_content=$(git show :manifest.json | grep -v '"generated_at"')
old_content=$(git show HEAD:manifest.json 2>/dev/null | grep -v '"generated_at"' || true)
[[ "$new_content" == "$old_content" ]] || only_timestamps=false
;;
*)
only_timestamps=false
;;
esac
$only_timestamps || break
done < <(git diff --cached --name-only)

if $only_timestamps; then
echo "No meaningful changes (only timestamps updated) - skipping commit."
else
source_commit=$(git rev-parse --short origin/$SOURCE_BRANCH)
plugin_list=""
if [[ -s changed_plugins.txt ]]; then
plugin_list="$(printf '\n\n')$(sed 's/^/- /' changed_plugins.txt)"
fi

git commit -m "Publish plugin updates from $SOURCE_BRANCH

Source commit: $source_commit${plugin_list}

[skip ci]"

git push "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" $RELEASES_BRANCH
echo "Successfully published to $RELEASES_BRANCH"
git push "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" $RELEASES_BRANCH
echo "Successfully published to ${RELEASES_BRANCH}"
fi # end only_timestamps check
fi
32 changes: 32 additions & 0 deletions .github/scripts/validate/report.sh
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,38 @@ done
fi
} > pr_comment.txt

# Minimize all previous validation comments as outdated before posting the new one
OWNER="${GITHUB_REPOSITORY%%/*}"
REPO="${GITHUB_REPOSITORY##*/}"

PREV_NODE_IDS=$(gh api graphql -f query='
query($owner: String!, $repo: String!, $number: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $number) {
comments(first: 100) {
nodes { id body }
}
}
}
}
' -f owner="$OWNER" -f repo="$REPO" -F number="$PR_NUMBER" \
--jq '.data.repository.pullRequest.comments.nodes[]
| select(.body | contains("<!--PLUGIN_VALIDATION_COMMENT-->"))
| .id' 2>/dev/null || true)

if [[ -n "$PREV_NODE_IDS" ]]; then
while IFS= read -r node_id; do
gh api graphql -f query='
mutation($id: ID!) {
minimizeComment(input: {subjectId: $id, classifier: OUTDATED}) {
minimizedComment { isMinimized }
}
}
' -f id="$node_id" > /dev/null 2>&1 || true
done <<< "$PREV_NODE_IDS"
echo "Minimized $(echo "$PREV_NODE_IDS" | wc -l | tr -d ' ') previous validation comment(s) as outdated"
fi

# Post PR comment - script succeeds/fails based on whether the comment posted
gh pr comment "$PR_NUMBER" --body "$(cat pr_comment.txt)"
COMMENT_EXIT=$?
Expand Down
37 changes: 35 additions & 2 deletions .github/scripts/validate/validate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -213,13 +213,46 @@ has_permission="false"
fi

# Version bump
# These fields may be updated without a version bump - they are metadata only
# and do not affect the packaged ZIP artifact.
METADATA_ONLY_FIELDS=("description" "repo_url" "discord_thread"
"min_dispatcharr_version" "max_dispatcharr_version" "deprecated" "unlisted" "maintainers")

if git show "origin/${BASE_REF}:${PLUGIN_JSON}" > /dev/null 2>&1; then
OLD_VERSION=$(git show "origin/${BASE_REF}:${PLUGIN_JSON}" | jq -r '.version')
if version_greater_than "$VERSION" "$OLD_VERSION"; then
TABLE_ROWS+=("| Version bump | ✅ | \`$OLD_VERSION\` → \`$VERSION\` |")
else
TABLE_ROWS+=("| Version bump | ❌ | \`$VERSION\` must be greater than current \`$OLD_VERSION\` |")
failed=1
# Version unchanged - check if every changed field is in the metadata-only allowlist
OLD_JSON=$(git show "origin/${BASE_REF}:${PLUGIN_JSON}")
NEW_JSON=$(cat "$PLUGIN_JSON")

# Produce a newline-separated list of field names that differ (raw strings, no quotes)
changed_fields=$(jq -rn \
--argjson old "$OLD_JSON" \
--argjson new "$NEW_JSON" \
'[$new | keys[]] | map(select($old[.] != $new[.])) | .[]' 2>/dev/null || true)

metadata_only_change=true
while IFS= read -r field; do
[[ -z "$field" ]] && continue
allowed=false
for mf in "${METADATA_ONLY_FIELDS[@]}"; do
[[ "$field" == "$mf" ]] && allowed=true && break
done
$allowed || { metadata_only_change=false; break; }
done <<< "$changed_fields"

if $metadata_only_change && [[ -n "$changed_fields" ]]; then
TABLE_ROWS+=("| Version bump | ✅ | \`$OLD_VERSION\` (unchanged - metadata-only update) |")
elif $metadata_only_change && [[ -z "$changed_fields" ]]; then
# Nothing changed at all - still require a bump so PRs aren't no-ops
TABLE_ROWS+=("| Version bump | ❌ | No changes detected - nothing to publish |")
failed=1
else
TABLE_ROWS+=("| Version bump | ❌ | \`$VERSION\` must be greater than current \`$OLD_VERSION\` |")
failed=1
fi
fi
else
TABLE_ROWS+=("| Version bump | ✅ | New plugin |")
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ name: CodeQL
# which runs CodeQL as part of the plugin validation flow.
on:
workflow_dispatch:
push:
branches: [main]
schedule:
- cron: '0 6 * * 1'
# push:
# branches: [main]
# schedule:
# - cron: '0 6 * * 1'

jobs:
analyze:
Expand Down
31 changes: 29 additions & 2 deletions .github/workflows/publish-plugins.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ on:
workflow_dispatch:
inputs:
force_rebuild:
description: 'Wipe and recreate the releases branch from scratch. WARNING: purges all version history only the latest version of each plugin will be retained.'
description: 'Wipe and recreate the releases branch from scratch. WARNING: purges all version history - only the latest version of each plugin will be retained.'
type: boolean
default: false
required: false
Expand All @@ -47,13 +47,40 @@ jobs:
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: Generate GitHub App token
if: steps.config.outputs.use_app == 'true'
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ steps.config.outputs.app_id }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}

- name: Log fallback to actions token
if: steps.config.outputs.use_app != 'true'
run: |
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: Make scripts executable
run: chmod +x .github/scripts/publish/*.sh

- name: Publish plugins to releases branch
id: publish
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ steps.config.outputs.use_app == 'true' && steps.app-token.outputs.token || github.token }}
APP_SLUG: ${{ steps.app-token.outputs.app-slug }}
GITHUB_REPOSITORY: ${{ github.repository }}
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
Expand Down
Loading
Loading