From b6e47a0b36bb6c88a0ff47e417f2fcfe02712bcd Mon Sep 17 00:00:00 2001 From: Seth Van Niekerk Date: Fri, 27 Mar 2026 11:01:17 -0400 Subject: [PATCH 01/24] Add workflow to update external README from plugin releases --- .github/workflows/update-external-readme.yml | 123 +++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 .github/workflows/update-external-readme.yml diff --git a/.github/workflows/update-external-readme.yml b/.github/workflows/update-external-readme.yml new file mode 100644 index 0000000..fc2ff70 --- /dev/null +++ b/.github/workflows/update-external-readme.yml @@ -0,0 +1,123 @@ +name: Update External README + +# Reads the README.md generated by the Publish Plugins workflow from the +# releases branch and opens (or updates) a PR in a target repository. +# +# GitHub App setup (one-time, org-level — no personal PAT required): +# 1. Create a GitHub App in the org with: +# Repository permissions: Contents (write), Pull requests (write) +# 2. Install the app on both this repo and the target repo. +# 3. Add to this repo's variables/secrets: +# vars.GH_APP_ID — the App ID (from the app's settings page) +# secrets.GH_APP_PRIVATE_KEY — the app's private key (PEM, generated in app settings) +# +# Required repo variables: +# EXTERNAL_README_REPO — target repository in "org/repo" format +# EXTERNAL_README_PATH — file path inside that repo, e.g. "docs/plugins/README.md" + +on: + workflow_run: + workflows: ["Publish Plugins"] + types: + - completed + branches: + - main + workflow_dispatch: + +jobs: + update-readme: + if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }} + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Validate configuration + id: config + run: | + if [[ -z "${{ vars.EXTERNAL_README_REPO }}" || -z "${{ vars.EXTERNAL_README_PATH }}" ]]; then + echo "::notice::EXTERNAL_README_REPO or EXTERNAL_README_PATH repo variables are not set — skipping." + echo "skip=true" >> "$GITHUB_OUTPUT" + else + echo "skip=false" >> "$GITHUB_OUTPUT" + fi + + - name: Generate GitHub App token + if: steps.config.outputs.skip != 'true' + id: app-token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ vars.GH_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 + with: + ref: releases + fetch-depth: 1 + sparse-checkout: README.md + sparse-checkout-cone-mode: false + + - name: Create or update PR in target repo + if: steps.config.outputs.skip != 'true' + env: + TARGET_REPO: ${{ vars.EXTERNAL_README_REPO }} + TARGET_PATH: ${{ vars.EXTERNAL_README_PATH }} + GH_TOKEN: ${{ steps.app-token.outputs.token }} + SOURCE_REPO: ${{ github.repository }} + run: | + if [[ ! -f README.md ]]; then + echo "::error::README.md not found on the releases branch — has the Publish Plugins workflow run yet?" + exit 1 + fi + + BRANCH="auto/dispatcharr-plugin-readme" + DEFAULT_BRANCH=$(gh api "repos/$TARGET_REPO" --jq '.default_branch') + + # Create branch if it does not already exist + if ! gh api "repos/$TARGET_REPO/branches/$BRANCH" > /dev/null 2>&1; then + BASE_SHA=$(gh api "repos/$TARGET_REPO/git/refs/heads/$DEFAULT_BRANCH" \ + --jq '.object.sha') + gh api "repos/$TARGET_REPO/git/refs" \ + -X POST \ + -f ref="refs/heads/$BRANCH" \ + -f sha="$BASE_SHA" + echo "Created branch $BRANCH" + fi + + # Get the existing file SHA from the branch (required by the API to update; absent means create) + FILE_SHA=$(gh api "repos/$TARGET_REPO/contents/$TARGET_PATH?ref=$BRANCH" \ + --jq '.sha' 2>/dev/null || true) + + # Upload file (base64-encoded content is required by the GitHub Contents API) + PUT_ARGS=( + -X PUT + -f message="chore: update plugin releases README" + -f content="$(base64 -w 0 README.md)" + -f branch="$BRANCH" + ) + [[ -n "$FILE_SHA" ]] && PUT_ARGS+=(-f sha="$FILE_SHA") + + gh api "repos/$TARGET_REPO/contents/$TARGET_PATH" "${PUT_ARGS[@]}" + + # Re-use an existing open PR for this branch; open a new one if none exists + EXISTING_PR=$(gh pr list \ + --repo "$TARGET_REPO" \ + --state open \ + --head "$BRANCH" \ + --json number \ + | jq -r '.[0].number // empty') + + if [[ -n "$EXISTING_PR" ]]; then + echo "Updated file on existing PR #$EXISTING_PR" + else + gh pr create \ + --repo "$TARGET_REPO" \ + --head "$BRANCH" \ + --base "$DEFAULT_BRANCH" \ + --title "chore: update plugin releases README" \ + --body "Automated update of the plugin releases README generated from [\`$SOURCE_REPO\`](https://github.com/$SOURCE_REPO)." + echo "Created new PR" + fi From b0afcfc75bfb9812f61f5d02eb6ac3f02ccf20b2 Mon Sep 17 00:00:00 2001 From: Seth Van Niekerk Date: Fri, 27 Mar 2026 11:08:03 -0400 Subject: [PATCH 02/24] Bump version to 2.4.69 in plugin.json --- plugins/dispatcharr-exporter/plugin.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/dispatcharr-exporter/plugin.json b/plugins/dispatcharr-exporter/plugin.json index 395f670..884ee8d 100644 --- a/plugins/dispatcharr-exporter/plugin.json +++ b/plugins/dispatcharr-exporter/plugin.json @@ -1,6 +1,6 @@ { "name": "Dispatcharr Exporter", - "version": "2.4.1", + "version": "2.4.69", "description": "Expose Dispatcharr metrics in Prometheus exporter-compatible format for monitoring", "license": "MIT", "discord_thread": "https://discord.com/channels/1340492560220684331/1451260201775923421", From 58a6dcfdee4bd3a1c42687c5e8d6d0f9c353e271 Mon Sep 17 00:00:00 2001 From: Seth Van Niekerk Date: Fri, 27 Mar 2026 11:17:55 -0400 Subject: [PATCH 03/24] Refactor update-external-readme workflow and add configuration script --- .github/scripts/config.env | 3 + .github/workflows/update-external-readme.yml | 102 +++++++++++++++---- 2 files changed, 84 insertions(+), 21 deletions(-) create mode 100644 .github/scripts/config.env diff --git a/.github/scripts/config.env b/.github/scripts/config.env new file mode 100644 index 0000000..523b529 --- /dev/null +++ b/.github/scripts/config.env @@ -0,0 +1,3 @@ +GH_APP_ID="3202290" +EXTERNAL_README_REPO="sv-dispatcharr/Dispatcharr-Docs" +EXTERNAL_README_PATH="docs/en/plugin-listing.md" diff --git a/.github/workflows/update-external-readme.yml b/.github/workflows/update-external-readme.yml index fc2ff70..69947ee 100644 --- a/.github/workflows/update-external-readme.yml +++ b/.github/workflows/update-external-readme.yml @@ -2,18 +2,6 @@ name: Update External README # Reads the README.md generated by the Publish Plugins workflow from the # releases branch and opens (or updates) a PR in a target repository. -# -# GitHub App setup (one-time, org-level — no personal PAT required): -# 1. Create a GitHub App in the org with: -# Repository permissions: Contents (write), Pull requests (write) -# 2. Install the app on both this repo and the target repo. -# 3. Add to this repo's variables/secrets: -# vars.GH_APP_ID — the App ID (from the app's settings page) -# secrets.GH_APP_PRIVATE_KEY — the app's private key (PEM, generated in app settings) -# -# Required repo variables: -# EXTERNAL_README_REPO — target repository in "org/repo" format -# EXTERNAL_README_PATH — file path inside that repo, e.g. "docs/plugins/README.md" on: workflow_run: @@ -31,14 +19,30 @@ jobs: timeout-minutes: 10 steps: - - name: Validate configuration + - name: Checkout config + uses: actions/checkout@v4 + with: + ref: main + fetch-depth: 1 + sparse-checkout: .github/scripts/config.env + sparse-checkout-cone-mode: false + + - name: Load and validate configuration id: config run: | - if [[ -z "${{ vars.EXTERNAL_README_REPO }}" || -z "${{ vars.EXTERNAL_README_PATH }}" ]]; then - echo "::notice::EXTERNAL_README_REPO or EXTERNAL_README_PATH repo variables are not set — skipping." + if [[ ! -f .github/scripts/config.env ]]; then + echo "::error::.github/scripts/config.env not found." + exit 1 + fi + source .github/scripts/config.env + if [[ -z "$EXTERNAL_README_REPO" || -z "$EXTERNAL_README_PATH" || -z "$GH_APP_ID" ]]; then + echo "::notice::EXTERNAL_README_REPO, EXTERNAL_README_PATH, or GH_APP_ID not set in config.env — skipping." echo "skip=true" >> "$GITHUB_OUTPUT" else - echo "skip=false" >> "$GITHUB_OUTPUT" + echo "skip=false" >> "$GITHUB_OUTPUT" + echo "app_id=$GH_APP_ID" >> "$GITHUB_OUTPUT" + echo "target_repo=$EXTERNAL_README_REPO" >> "$GITHUB_OUTPUT" + echo "target_path=$EXTERNAL_README_PATH" >> "$GITHUB_OUTPUT" fi - name: Generate GitHub App token @@ -46,7 +50,7 @@ jobs: id: app-token uses: actions/create-github-app-token@v1 with: - app-id: ${{ vars.GH_APP_ID }} + app-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 }} @@ -57,22 +61,73 @@ jobs: with: ref: releases fetch-depth: 1 + clean: false sparse-checkout: README.md sparse-checkout-cone-mode: false + - name: Extract release metadata from commit + if: steps.config.outputs.skip != 'true' + id: meta + run: | + COMMIT_MSG=$(git log -1 --format="%B" HEAD) + + # Extract source commit SHA from "Source commit: " line + SOURCE_COMMIT=$(echo "$COMMIT_MSG" | grep -oP '(?<=Source commit: )[0-9a-f]+' || true) + + # Extract plugin list — lines starting with "- " after the source commit line + PLUGIN_LIST=$(echo "$COMMIT_MSG" | grep -E '^\- [a-z0-9].*@' || true) + + RELEASES_COMMIT=$(git rev-parse --short HEAD) + + echo "source_commit=${SOURCE_COMMIT}" >> "$GITHUB_OUTPUT" + echo "releases_commit=${RELEASES_COMMIT}" >> "$GITHUB_OUTPUT" + + # Write plugin list to a file to avoid quoting issues in multi-line output + printf '%s' "$PLUGIN_LIST" > /tmp/plugin_list.txt + + - name: Strip releases-branch preamble from README + if: steps.config.outputs.skip != 'true' + run: | + # Remove the intro paragraph and Quick Access section that are only + # relevant on the releases branch itself, keeping everything from + # "## Available Plugins" onward (preserving the "# Plugin Releases" title). + awk ' + /^# Plugin Releases/ { print; next } + /^## Available Plugins/ { found=1 } + found { print } + ' README.md > README.stripped.md + mv README.stripped.md README.md + - name: Create or update PR in target repo if: steps.config.outputs.skip != 'true' env: - TARGET_REPO: ${{ vars.EXTERNAL_README_REPO }} - TARGET_PATH: ${{ vars.EXTERNAL_README_PATH }} + TARGET_REPO: ${{ steps.config.outputs.target_repo }} + TARGET_PATH: ${{ steps.config.outputs.target_path }} GH_TOKEN: ${{ steps.app-token.outputs.token }} SOURCE_REPO: ${{ github.repository }} + SOURCE_COMMIT: ${{ steps.meta.outputs.source_commit }} + RELEASES_COMMIT: ${{ steps.meta.outputs.releases_commit }} run: | if [[ ! -f README.md ]]; then echo "::error::README.md not found on the releases branch — has the Publish Plugins workflow run yet?" exit 1 fi + PLUGIN_LIST=$(cat /tmp/plugin_list.txt) + + # Build a human-readable change summary block + build_summary() { + echo "**Source commit:** [\`${SOURCE_COMMIT}\`](https://github.com/${SOURCE_REPO}/commit/${SOURCE_COMMIT})" + echo "**Releases commit:** [\`${RELEASES_COMMIT}\`](https://github.com/${SOURCE_REPO}/commit/${RELEASES_COMMIT})" + if [[ -n "$PLUGIN_LIST" ]]; then + echo "" + echo "**Plugins updated:**" + while IFS= read -r line; do + echo "$line" + done <<< "$PLUGIN_LIST" + fi + } + BRANCH="auto/dispatcharr-plugin-readme" DEFAULT_BRANCH=$(gh api "repos/$TARGET_REPO" --jq '.default_branch') @@ -111,13 +166,18 @@ jobs: | jq -r '.[0].number // empty') if [[ -n "$EXISTING_PR" ]]; then - echo "Updated file on existing PR #$EXISTING_PR" + # Add a comment to the existing PR summarising this update + COMMENT=$(printf '### Plugin README update\n\n%s' "$(build_summary)") + gh pr comment "$EXISTING_PR" --repo "$TARGET_REPO" --body "$COMMENT" + echo "Added update comment to existing PR #$EXISTING_PR" else + PR_BODY=$(printf 'Automated update of the plugin releases README generated from [`%s`](https://github.com/%s).\n\n%s' \ + "$SOURCE_REPO" "$SOURCE_REPO" "$(build_summary)") gh pr create \ --repo "$TARGET_REPO" \ --head "$BRANCH" \ --base "$DEFAULT_BRANCH" \ --title "chore: update plugin releases README" \ - --body "Automated update of the plugin releases README generated from [\`$SOURCE_REPO\`](https://github.com/$SOURCE_REPO)." + --body "$PR_BODY" echo "Created new PR" fi From 846d628b35cc2fce5f30fe547a77e1d8797fb385 Mon Sep 17 00:00:00 2001 From: Seth Van Niekerk Date: Fri, 27 Mar 2026 11:20:24 -0400 Subject: [PATCH 04/24] Enhance README processing by stripping preamble and rewriting relative links to absolute GitHub URLs --- .github/workflows/update-external-readme.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/update-external-readme.yml b/.github/workflows/update-external-readme.yml index 69947ee..873b78c 100644 --- a/.github/workflows/update-external-readme.yml +++ b/.github/workflows/update-external-readme.yml @@ -87,6 +87,8 @@ jobs: - name: Strip releases-branch preamble from README if: steps.config.outputs.skip != 'true' + env: + SOURCE_REPO: ${{ github.repository }} run: | # Remove the intro paragraph and Quick Access section that are only # relevant on the releases branch itself, keeping everything from @@ -98,6 +100,12 @@ jobs: ' README.md > README.stripped.md mv README.stripped.md README.md + # Rewrite relative links (./zips/...) to absolute GitHub URLs pointing + # at the releases branch. Anchor links (#...) are unaffected because + # they never start with "./". + BASE_URL="https://github.com/${SOURCE_REPO}/tree/releases" + sed -i "s|](\./|](${BASE_URL}/|g" README.md + - name: Create or update PR in target repo if: steps.config.outputs.skip != 'true' env: From 44b3454abe9824ac2825cb1394ef43b9b9132eac Mon Sep 17 00:00:00 2001 From: Seth Van Niekerk Date: Fri, 27 Mar 2026 11:23:11 -0400 Subject: [PATCH 05/24] Update plugin releases README message and PR title for clarity --- .github/workflows/update-external-readme.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/update-external-readme.yml b/.github/workflows/update-external-readme.yml index 873b78c..3ba877b 100644 --- a/.github/workflows/update-external-readme.yml +++ b/.github/workflows/update-external-readme.yml @@ -157,7 +157,7 @@ jobs: # Upload file (base64-encoded content is required by the GitHub Contents API) PUT_ARGS=( -X PUT - -f message="chore: update plugin releases README" + -f message="chore: update plugin releases listing (source commit $SOURCE_COMMIT)" -f content="$(base64 -w 0 README.md)" -f branch="$BRANCH" ) @@ -179,13 +179,13 @@ jobs: gh pr comment "$EXISTING_PR" --repo "$TARGET_REPO" --body "$COMMENT" echo "Added update comment to existing PR #$EXISTING_PR" else - PR_BODY=$(printf 'Automated update of the plugin releases README generated from [`%s`](https://github.com/%s).\n\n%s' \ + PR_BODY=$(printf 'Automated update of the plugin releases listings generated from [`%s`](https://github.com/%s).\n\n%s' \ "$SOURCE_REPO" "$SOURCE_REPO" "$(build_summary)") gh pr create \ --repo "$TARGET_REPO" \ --head "$BRANCH" \ --base "$DEFAULT_BRANCH" \ - --title "chore: update plugin releases README" \ + --title "chore: update plugin releases listings" \ --body "$PR_BODY" echo "Created new PR" fi From 628b3a33542be2633ae23bc1606c4ca91fcbcb50 Mon Sep 17 00:00:00 2001 From: Seth Van Niekerk Date: Fri, 27 Mar 2026 11:43:49 -0400 Subject: [PATCH 06/24] Enhance GitHub Actions workflows to support GitHub App authentication and fallback to GitHub token, improving security and flexibility in plugin publishing and validation processes. --- .github/scripts/publish/run.sh | 12 +- .github/scripts/validate/report.sh | 32 +++++ .github/workflows/publish-plugins.yml | 30 ++++- .github/workflows/validate-plugin.yml | 187 +++++++++++++++++++++++++- 4 files changed, 250 insertions(+), 11 deletions(-) diff --git a/.github/scripts/publish/run.sh b/.github/scripts/publish/run.sh index d4d0d47..2e52144 100644 --- a/.github/scripts/publish/run.sh +++ b/.github/scripts/publish/run.sh @@ -36,9 +36,15 @@ 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. +if [[ -n "${APP_SLUG:-}" && -n "${APP_ID:-}" ]]; then + git config user.name "${APP_SLUG}[bot]" + git config user.email "${APP_ID}+${APP_SLUG}[bot]@users.noreply.github.com" +else + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" +fi # Checkout or create releases branch echo "Setting up $RELEASES_BRANCH branch..." diff --git a/.github/scripts/validate/report.sh b/.github/scripts/validate/report.sh index 6f0d417..4aaac27 100755 --- a/.github/scripts/validate/report.sh +++ b/.github/scripts/validate/report.sh @@ -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("")) + | .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=$? diff --git a/.github/workflows/publish-plugins.yml b/.github/workflows/publish-plugins.yml index 9fdaea7..9a9e8c1 100644 --- a/.github/workflows/publish-plugins.yml +++ b/.github/workflows/publish-plugins.yml @@ -47,13 +47,41 @@ jobs: with: fetch-depth: 0 + - name: Load app ID from config + id: config + run: | + if [[ -f .github/scripts/config.env ]]; then + source .github/scripts/config.env + fi + if [[ -n "${GH_APP_ID:-}" ]]; 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 not set in `.github/scripts/config.env`. 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 }} + APP_ID: ${{ steps.config.outputs.app_id }} GITHUB_REPOSITORY: ${{ github.repository }} GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} diff --git a/.github/workflows/validate-plugin.yml b/.github/workflows/validate-plugin.yml index 2de718c..3cee04b 100644 --- a/.github/workflows/validate-plugin.yml +++ b/.github/workflows/validate-plugin.yml @@ -26,16 +26,51 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 5 steps: + - name: Checkout config + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.base.ref }} + fetch-depth: 1 + sparse-checkout: .github/scripts + sparse-checkout-cone-mode: false + + - name: Load app ID from config + id: config + run: | + if [[ -f .github/scripts/config.env ]]; then + source .github/scripts/config.env + fi + if [[ -n "${GH_APP_ID:-}" ]]; 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 not set in `.github/scripts/config.env`. Falling back to `GITHUB_TOKEN` (github-actions[bot] identity).\n' >> "$GITHUB_STEP_SUMMARY" + - name: Post draft notice env: PR_NUMBER: ${{ github.event.pull_request.number }} - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ steps.config.outputs.use_app == 'true' && steps.app-token.outputs.token || github.token }} + BOT_LOGIN: ${{ steps.config.outputs.use_app == 'true' && format('{0}[bot]', steps.app-token.outputs.app-slug) || 'github-actions[bot]' }} GH_REPO: ${{ github.repository }} run: | MARKER="" COMMENT="$MARKER"$'\n'"This PR is currently a draft. Plugin validation will run once the PR is marked ready for review." EXISTING=$(gh pr view $PR_NUMBER --json comments \ - | jq -r --arg marker "$MARKER" '.comments[] | select(.author.login=="github-actions[bot]") | select(.body | contains($marker)) | .id') + | jq -r --arg marker "$MARKER" --arg login "$BOT_LOGIN" '.comments[] | select(.author.login==$login) | select(.body | contains($marker)) | .id') if [ -n "$EXISTING" ]; then gh api "repos/${{ github.repository }}/issues/comments/$EXISTING" -X PATCH -f body="$COMMENT" else @@ -92,10 +127,36 @@ jobs: - name: Re-fetch base branch refs run: git fetch https://github.com/${{ github.repository }} +${{ github.event.pull_request.base.ref }}:refs/remotes/origin/${{ github.event.pull_request.base.ref }} + - name: Load app ID from config + id: config + run: | + if [[ -f .github/scripts/config.env ]]; then + source .github/scripts/config.env + fi + if [[ -n "${GH_APP_ID:-}" ]]; 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 not set in `.github/scripts/config.env`. Falling back to `GITHUB_TOKEN` (github-actions[bot] identity).\n' >> "$GITHUB_STEP_SUMMARY" + - name: Detect changed plugins id: detect env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ steps.config.outputs.use_app == 'true' && steps.app-token.outputs.token || github.token }} GITHUB_REPOSITORY: ${{ github.repository }} run: | chmod +x .github/scripts/validate/*.sh @@ -112,9 +173,43 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 2 steps: + - name: Checkout config + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.base.ref }} + fetch-depth: 1 + sparse-checkout: .github/scripts + sparse-checkout-cone-mode: false + + - name: Load app ID from config + id: config + run: | + if [[ -f .github/scripts/config.env ]]; then + source .github/scripts/config.env + fi + if [[ -n "${GH_APP_ID:-}" ]]; 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 not set in `.github/scripts/config.env`. Falling back to `GITHUB_TOKEN` (github-actions[bot] identity).\n' >> "$GITHUB_STEP_SUMMARY" + - name: Apply labels env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ steps.config.outputs.use_app == 'true' && steps.app-token.outputs.token || github.token }} GH_REPO: ${{ github.repository }} PR_NUMBER: ${{ github.event.pull_request.number }} HAS_NEW_PLUGIN: ${{ needs.detect-changes.outputs.has_new_plugin }} @@ -406,10 +501,36 @@ jobs: - name: Re-fetch base branch refs run: git fetch https://github.com/${{ github.repository }} +${{ github.event.pull_request.base.ref }}:refs/remotes/origin/${{ github.event.pull_request.base.ref }} + - name: Load app ID from config + id: config + run: | + if [[ -f .github/scripts/config.env ]]; then + source .github/scripts/config.env + fi + if [[ -n "${GH_APP_ID:-}" ]]; 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 not set in `.github/scripts/config.env`. Falling back to `GITHUB_TOKEN` (github-actions[bot] identity).\n' >> "$GITHUB_STEP_SUMMARY" + - name: Validate ${{ matrix.plugin }} id: validate env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ steps.config.outputs.use_app == 'true' && steps.app-token.outputs.token || github.token }} GITHUB_REPOSITORY: ${{ github.repository }} run: | chmod +x .github/scripts/validate/*.sh @@ -448,6 +569,32 @@ jobs: sparse-checkout: .github/scripts sparse-checkout-cone-mode: false + - name: Load app ID from config + id: config + run: | + if [[ -f .github/scripts/config.env ]]; then + source .github/scripts/config.env + fi + if [[ -n "${GH_APP_ID:-}" ]]; 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 not set in `.github/scripts/config.env`. Falling back to `GITHUB_TOKEN` (github-actions[bot] identity).\n' >> "$GITHUB_STEP_SUMMARY" + - name: Download all report fragments uses: actions/download-artifact@v4 with: @@ -473,7 +620,7 @@ jobs: - name: Aggregate and post comment id: report env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ steps.config.outputs.use_app == 'true' && steps.app-token.outputs.token || github.token }} GITHUB_REPOSITORY: ${{ github.repository }} DISCORD_URL: ${{ vars.DISCORD_URL }} CODEQL_RESULT: ${{ needs.codeql-analyze.outputs.codeql_status }} @@ -514,9 +661,35 @@ jobs: sparse-checkout: .github/scripts sparse-checkout-cone-mode: false + - name: Load app ID from config + id: config + run: | + if [[ -f .github/scripts/config.env ]]; then + source .github/scripts/config.env + fi + if [[ -n "${GH_APP_ID:-}" ]]; 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 not set in `.github/scripts/config.env`. Falling back to `GITHUB_TOKEN` (github-actions[bot] identity).\n' >> "$GITHUB_STEP_SUMMARY" + - name: Post close comment and close PR env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ steps.config.outputs.use_app == 'true' && steps.app-token.outputs.token || github.token }} GITHUB_REPOSITORY: ${{ github.repository }} DISCORD_URL: ${{ vars.DISCORD_URL }} CLOSE_REASON: ${{ needs.detect-changes.outputs.close_reason }} From f437e2bd420c854e62b7f44811423b2846c0d4b3 Mon Sep 17 00:00:00 2001 From: Seth Van Niekerk Date: Fri, 27 Mar 2026 11:52:05 -0400 Subject: [PATCH 07/24] Enhance README update process to skip commits with only timestamp changes, improving efficiency and reducing unnecessary PRs. --- .github/workflows/update-external-readme.yml | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/.github/workflows/update-external-readme.yml b/.github/workflows/update-external-readme.yml index 3ba877b..1437532 100644 --- a/.github/workflows/update-external-readme.yml +++ b/.github/workflows/update-external-readme.yml @@ -150,9 +150,21 @@ jobs: echo "Created branch $BRANCH" fi - # Get the existing file SHA from the branch (required by the API to update; absent means create) - FILE_SHA=$(gh api "repos/$TARGET_REPO/contents/$TARGET_PATH?ref=$BRANCH" \ - --jq '.sha' 2>/dev/null || true) + # Get the existing file SHA and content from the branch + EXISTING_RESPONSE=$(gh api "repos/$TARGET_REPO/contents/$TARGET_PATH?ref=$BRANCH" 2>/dev/null || true) + FILE_SHA=$(echo "$EXISTING_RESPONSE" | jq -r '.sha // empty') + + # Skip if the only change is the timestamp line at the bottom of the README + if [[ -n "$FILE_SHA" ]]; then + EXISTING_CONTENT=$(echo "$EXISTING_RESPONSE" | jq -r '.content' | base64 -d 2>/dev/null || true) + NEW_STRIPPED=$(grep -v '^\*Last updated:' README.md || true) + EXISTING_STRIPPED=$(echo "$EXISTING_CONTENT" | grep -v '^\*Last updated:' || true) + if [[ "$NEW_STRIPPED" == "$EXISTING_STRIPPED" ]]; then + echo "README content unchanged (only timestamp differs) — skipping." + echo "::notice::External README not updated: only the timestamp line changed." + exit 0 + fi + fi # Upload file (base64-encoded content is required by the GitHub Contents API) PUT_ARGS=( From bc2d4bd7b1d1370e2253ba9d731a622feeacaf54 Mon Sep 17 00:00:00 2001 From: Seth Van Niekerk Date: Fri, 27 Mar 2026 11:54:40 -0400 Subject: [PATCH 08/24] Enhance GitHub workflows to require GH_APP_PRIVATE_KEY alongside GH_APP_ID for improved security in plugin publishing and validation processes. --- .github/workflows/publish-plugins.yml | 6 ++-- .github/workflows/update-external-readme.yml | 6 ++-- .github/workflows/validate-plugin.yml | 36 +++++++++++++------- 3 files changed, 32 insertions(+), 16 deletions(-) diff --git a/.github/workflows/publish-plugins.yml b/.github/workflows/publish-plugins.yml index 9a9e8c1..f59bdc7 100644 --- a/.github/workflows/publish-plugins.yml +++ b/.github/workflows/publish-plugins.yml @@ -49,11 +49,13 @@ jobs: - name: Load app ID from config id: config + env: + GH_APP_PRIVATE_KEY: ${{ secrets.GH_APP_PRIVATE_KEY }} run: | if [[ -f .github/scripts/config.env ]]; then source .github/scripts/config.env fi - if [[ -n "${GH_APP_ID:-}" ]]; then + 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 @@ -71,7 +73,7 @@ jobs: - name: Log fallback to actions token if: steps.config.outputs.use_app != 'true' run: | - printf '## ⚠️ GitHub App token not available\n\nGH_APP_ID not set in `.github/scripts/config.env`. Falling back to `GITHUB_TOKEN` (github-actions[bot] identity).\n' >> "$GITHUB_STEP_SUMMARY" + 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 diff --git a/.github/workflows/update-external-readme.yml b/.github/workflows/update-external-readme.yml index 1437532..4b61a85 100644 --- a/.github/workflows/update-external-readme.yml +++ b/.github/workflows/update-external-readme.yml @@ -29,14 +29,16 @@ jobs: - name: Load and validate configuration id: config + env: + GH_APP_PRIVATE_KEY: ${{ secrets.GH_APP_PRIVATE_KEY }} run: | if [[ ! -f .github/scripts/config.env ]]; then echo "::error::.github/scripts/config.env not found." exit 1 fi source .github/scripts/config.env - if [[ -z "$EXTERNAL_README_REPO" || -z "$EXTERNAL_README_PATH" || -z "$GH_APP_ID" ]]; then - echo "::notice::EXTERNAL_README_REPO, EXTERNAL_README_PATH, or GH_APP_ID not set in config.env — skipping." + if [[ -z "$EXTERNAL_README_REPO" || -z "$EXTERNAL_README_PATH" || -z "$GH_APP_ID" || -z "${GH_APP_PRIVATE_KEY:-}" ]]; then + echo "::notice::EXTERNAL_README_REPO, EXTERNAL_README_PATH, GH_APP_ID, or GH_APP_PRIVATE_KEY not configured — skipping." echo "skip=true" >> "$GITHUB_OUTPUT" else echo "skip=false" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/validate-plugin.yml b/.github/workflows/validate-plugin.yml index 3cee04b..e785061 100644 --- a/.github/workflows/validate-plugin.yml +++ b/.github/workflows/validate-plugin.yml @@ -36,11 +36,13 @@ jobs: - name: Load app ID from config id: config + env: + GH_APP_PRIVATE_KEY: ${{ secrets.GH_APP_PRIVATE_KEY }} run: | if [[ -f .github/scripts/config.env ]]; then source .github/scripts/config.env fi - if [[ -n "${GH_APP_ID:-}" ]]; then + 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 @@ -58,7 +60,7 @@ jobs: - name: Log fallback to actions token if: steps.config.outputs.use_app != 'true' run: | - printf '## ⚠️ GitHub App token not available\n\nGH_APP_ID not set in `.github/scripts/config.env`. Falling back to `GITHUB_TOKEN` (github-actions[bot] identity).\n' >> "$GITHUB_STEP_SUMMARY" + 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: Post draft notice env: @@ -129,11 +131,13 @@ jobs: - name: Load app ID from config id: config + env: + GH_APP_PRIVATE_KEY: ${{ secrets.GH_APP_PRIVATE_KEY }} run: | if [[ -f .github/scripts/config.env ]]; then source .github/scripts/config.env fi - if [[ -n "${GH_APP_ID:-}" ]]; then + 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 @@ -151,7 +155,7 @@ jobs: - name: Log fallback to actions token if: steps.config.outputs.use_app != 'true' run: | - printf '## ⚠️ GitHub App token not available\n\nGH_APP_ID not set in `.github/scripts/config.env`. Falling back to `GITHUB_TOKEN` (github-actions[bot] identity).\n' >> "$GITHUB_STEP_SUMMARY" + 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: Detect changed plugins id: detect @@ -183,11 +187,13 @@ jobs: - name: Load app ID from config id: config + env: + GH_APP_PRIVATE_KEY: ${{ secrets.GH_APP_PRIVATE_KEY }} run: | if [[ -f .github/scripts/config.env ]]; then source .github/scripts/config.env fi - if [[ -n "${GH_APP_ID:-}" ]]; then + 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 @@ -205,7 +211,7 @@ jobs: - name: Log fallback to actions token if: steps.config.outputs.use_app != 'true' run: | - printf '## ⚠️ GitHub App token not available\n\nGH_APP_ID not set in `.github/scripts/config.env`. Falling back to `GITHUB_TOKEN` (github-actions[bot] identity).\n' >> "$GITHUB_STEP_SUMMARY" + 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: Apply labels env: @@ -503,11 +509,13 @@ jobs: - name: Load app ID from config id: config + env: + GH_APP_PRIVATE_KEY: ${{ secrets.GH_APP_PRIVATE_KEY }} run: | if [[ -f .github/scripts/config.env ]]; then source .github/scripts/config.env fi - if [[ -n "${GH_APP_ID:-}" ]]; then + 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 @@ -525,7 +533,7 @@ jobs: - name: Log fallback to actions token if: steps.config.outputs.use_app != 'true' run: | - printf '## ⚠️ GitHub App token not available\n\nGH_APP_ID not set in `.github/scripts/config.env`. Falling back to `GITHUB_TOKEN` (github-actions[bot] identity).\n' >> "$GITHUB_STEP_SUMMARY" + 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: Validate ${{ matrix.plugin }} id: validate @@ -571,11 +579,13 @@ jobs: - name: Load app ID from config id: config + env: + GH_APP_PRIVATE_KEY: ${{ secrets.GH_APP_PRIVATE_KEY }} run: | if [[ -f .github/scripts/config.env ]]; then source .github/scripts/config.env fi - if [[ -n "${GH_APP_ID:-}" ]]; then + 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 @@ -593,7 +603,7 @@ jobs: - name: Log fallback to actions token if: steps.config.outputs.use_app != 'true' run: | - printf '## ⚠️ GitHub App token not available\n\nGH_APP_ID not set in `.github/scripts/config.env`. Falling back to `GITHUB_TOKEN` (github-actions[bot] identity).\n' >> "$GITHUB_STEP_SUMMARY" + 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 @@ -663,11 +673,13 @@ jobs: - name: Load app ID from config id: config + env: + GH_APP_PRIVATE_KEY: ${{ secrets.GH_APP_PRIVATE_KEY }} run: | if [[ -f .github/scripts/config.env ]]; then source .github/scripts/config.env fi - if [[ -n "${GH_APP_ID:-}" ]]; then + 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 @@ -685,7 +697,7 @@ jobs: - name: Log fallback to actions token if: steps.config.outputs.use_app != 'true' run: | - printf '## ⚠️ GitHub App token not available\n\nGH_APP_ID not set in `.github/scripts/config.env`. Falling back to `GITHUB_TOKEN` (github-actions[bot] identity).\n' >> "$GITHUB_STEP_SUMMARY" + 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: Post close comment and close PR env: From 702723a21dc654b2b1342f7364c356b864f0d867 Mon Sep 17 00:00:00 2001 From: Seth Van Niekerk Date: Fri, 27 Mar 2026 11:58:20 -0400 Subject: [PATCH 09/24] Enhance validate-plugin workflow to specify branch reference and fetch depth for script checkouts, improving accuracy and efficiency. --- .github/workflows/validate-plugin.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/validate-plugin.yml b/.github/workflows/validate-plugin.yml index e785061..07b2afe 100644 --- a/.github/workflows/validate-plugin.yml +++ b/.github/workflows/validate-plugin.yml @@ -574,6 +574,8 @@ jobs: - name: Checkout scripts uses: actions/checkout@v4 with: + ref: ${{ github.event.pull_request.base.ref }} + fetch-depth: 1 sparse-checkout: .github/scripts sparse-checkout-cone-mode: false @@ -668,6 +670,8 @@ jobs: - name: Checkout scripts uses: actions/checkout@v4 with: + ref: ${{ github.event.pull_request.base.ref }} + fetch-depth: 1 sparse-checkout: .github/scripts sparse-checkout-cone-mode: false From 81febc37f8733837d8ad8bd30cb74e8d9f467a80 Mon Sep 17 00:00:00 2001 From: Seth Van Niekerk Date: Fri, 27 Mar 2026 12:07:40 -0400 Subject: [PATCH 10/24] Enhance GitHub Actions to configure bot identity for commits, improving author tracking and fallback mechanisms. --- .github/scripts/publish/run.sh | 14 +++++++++++--- .github/workflows/publish-plugins.yml | 1 - 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/.github/scripts/publish/run.sh b/.github/scripts/publish/run.sh index 2e52144..1d9f6b1 100644 --- a/.github/scripts/publish/run.sh +++ b/.github/scripts/publish/run.sh @@ -38,12 +38,20 @@ cd "$WORK_DIR/repo" # Configure git — use GitHub App bot identity when available, otherwise fall back # to the generic github-actions[bot] identity. -if [[ -n "${APP_SLUG:-}" && -n "${APP_ID:-}" ]]; then +# 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]" - git config user.email "${APP_ID}+${APP_SLUG}[bot]@users.noreply.github.com" + 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 "github-actions[bot]@users.noreply.github.com" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" fi # Checkout or create releases branch diff --git a/.github/workflows/publish-plugins.yml b/.github/workflows/publish-plugins.yml index f59bdc7..0ac94e4 100644 --- a/.github/workflows/publish-plugins.yml +++ b/.github/workflows/publish-plugins.yml @@ -83,7 +83,6 @@ jobs: env: GITHUB_TOKEN: ${{ steps.config.outputs.use_app == 'true' && steps.app-token.outputs.token || github.token }} APP_SLUG: ${{ steps.app-token.outputs.app-slug }} - APP_ID: ${{ steps.config.outputs.app_id }} GITHUB_REPOSITORY: ${{ github.repository }} GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} From e51bfd139b075ba2974e655956b948f24298f6e9 Mon Sep 17 00:00:00 2001 From: Seth Van Niekerk Date: Fri, 27 Mar 2026 12:12:07 -0400 Subject: [PATCH 11/24] Enhance plugin publishing process to use Git Data API for commits, improving commit integrity and author tracking. --- .github/scripts/publish/run.sh | 98 +++++++++++++++++++++++++++++----- 1 file changed, 84 insertions(+), 14 deletions(-) diff --git a/.github/scripts/publish/run.sh b/.github/scripts/publish/run.sh index 1d9f6b1..52a0c73 100644 --- a/.github/scripts/publish/run.sh +++ b/.github/scripts/publish/run.sh @@ -102,29 +102,99 @@ echo "" echo "=== Generating releases README ===" bash "$SCRIPT_DIR/releases-readme.sh" -# --- Commit and push --- +# --- Commit and push via Git Data API (produces a server-signed commit) --- 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 +# Collect the files that will form the release commit tree +PUBLISH_FILES=(zips manifest.json README.md) - git commit -m "Publish plugin updates from $SOURCE_BRANCH +# Build the commit message +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 +COMMIT_MSG="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" +# Get the current tip of the releases branch (the parent commit) +PARENT_SHA=$(gh api "repos/${GITHUB_REPOSITORY}/git/refs/heads/${RELEASES_BRANCH}" \ + --jq '.object.sha') +BASE_TREE_SHA=$(gh api "repos/${GITHUB_REPOSITORY}/git/commits/${PARENT_SHA}" \ + --jq '.tree.sha') + +# Create blobs for every file under the publish dirs and collect tree entries +TREE_ENTRIES="[]" + +add_blob() { + local path="$1" + local content_b64 + content_b64=$(base64 -w 0 "$path") + local blob_sha + blob_sha=$(gh api "repos/${GITHUB_REPOSITORY}/git/blobs" \ + -X POST \ + -f encoding="base64" \ + -f content="$content_b64" \ + --jq '.sha') + TREE_ENTRIES=$(echo "$TREE_ENTRIES" | jq \ + --arg p "$path" --arg s "$blob_sha" \ + '. + [{"path": $p, "mode": "100644", "type": "blob", "sha": $s}]') +} + +for entry in "${PUBLISH_FILES[@]}"; do + if [[ -f "$entry" ]]; then + add_blob "$entry" + elif [[ -d "$entry" ]]; then + while IFS= read -r -d '' file; do + add_blob "$file" + done < <(find "$entry" -type f -print0) + fi +done + +# Create the new tree, rooted on the current base tree +NEW_TREE_SHA=$(gh api "repos/${GITHUB_REPOSITORY}/git/trees" \ + -X POST \ + -f "base_tree=${BASE_TREE_SHA}" \ + --field "tree=$(echo "$TREE_ENTRIES")" \ + --jq '.sha') + +# Bail out early if nothing changed +if [[ "$NEW_TREE_SHA" == "$BASE_TREE_SHA" ]]; then + echo "No changes to commit." +else + # Resolve the author identity (mirrors the git config block above) + if [[ -n "${APP_SLUG:-}" ]]; then + AUTHOR_NAME="${APP_SLUG}[bot]" + AUTHOR_EMAIL=$(git config user.email) # set from API user-ID lookup earlier + else + AUTHOR_NAME="github-actions[bot]" + AUTHOR_EMAIL="41898282+github-actions[bot]@users.noreply.github.com" + fi + + NOW=$(date -u +"%Y-%m-%dT%H:%M:%SZ") + + # Create the commit via the API — GitHub signs it server-side + NEW_COMMIT_SHA=$(gh api "repos/${GITHUB_REPOSITORY}/git/commits" \ + -X POST \ + -f "message=${COMMIT_MSG}" \ + -f "tree=${NEW_TREE_SHA}" \ + -f "parents[]=${PARENT_SHA}" \ + --field "author[name]=${AUTHOR_NAME}" \ + --field "author[email]=${AUTHOR_EMAIL}" \ + --field "author[date]=${NOW}" \ + --jq '.sha') + + # Fast-forward the branch ref to the new commit + gh api "repos/${GITHUB_REPOSITORY}/git/refs/heads/${RELEASES_BRANCH}" \ + -X PATCH \ + -f "sha=${NEW_COMMIT_SHA}" \ + -F force=false + + echo "Successfully published to ${RELEASES_BRANCH} (commit ${NEW_COMMIT_SHA:0:7})" fi From b7d8b14881663c5faaf1bd5a9faa845153070832 Mon Sep 17 00:00:00 2001 From: Seth Van Niekerk Date: Fri, 27 Mar 2026 12:15:48 -0400 Subject: [PATCH 12/24] Refactor add_blob and tree creation to use temporary files for JSON body, improving handling of large ZIPs and avoiding ARG_MAX limits. --- .github/scripts/publish/run.sh | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/.github/scripts/publish/run.sh b/.github/scripts/publish/run.sh index 52a0c73..c832f6a 100644 --- a/.github/scripts/publish/run.sh +++ b/.github/scripts/publish/run.sh @@ -134,14 +134,21 @@ TREE_ENTRIES="[]" add_blob() { local path="$1" - local content_b64 - content_b64=$(base64 -w 0 "$path") + local tmp_body + tmp_body=$(mktemp) + # Build the JSON body by writing directly to a file — avoids ARG_MAX limits + # from large ZIPs being passed as shell arguments. + { + printf '{"encoding":"base64","content":"' + base64 -w 0 "$path" + printf '"}' + } > "$tmp_body" local blob_sha blob_sha=$(gh api "repos/${GITHUB_REPOSITORY}/git/blobs" \ -X POST \ - -f encoding="base64" \ - -f content="$content_b64" \ + --input "$tmp_body" \ --jq '.sha') + rm -f "$tmp_body" TREE_ENTRIES=$(echo "$TREE_ENTRIES" | jq \ --arg p "$path" --arg s "$blob_sha" \ '. + [{"path": $p, "mode": "100644", "type": "blob", "sha": $s}]') @@ -157,12 +164,18 @@ for entry in "${PUBLISH_FILES[@]}"; do fi done -# Create the new tree, rooted on the current base tree +# Create the new tree, rooted on the current base tree. +# Write the body to a temp file for the same reason (many/large tree entries). +TREE_BODY_FILE=$(mktemp) +jq -n \ + --arg base "$BASE_TREE_SHA" \ + --argjson tree "$TREE_ENTRIES" \ + '{"base_tree": $base, "tree": $tree}' > "$TREE_BODY_FILE" NEW_TREE_SHA=$(gh api "repos/${GITHUB_REPOSITORY}/git/trees" \ -X POST \ - -f "base_tree=${BASE_TREE_SHA}" \ - --field "tree=$(echo "$TREE_ENTRIES")" \ + --input "$TREE_BODY_FILE" \ --jq '.sha') +rm -f "$TREE_BODY_FILE" # Bail out early if nothing changed if [[ "$NEW_TREE_SHA" == "$BASE_TREE_SHA" ]]; then From 0611c993824326d69a6bcc9f3270c6a7c30ec3ee Mon Sep 17 00:00:00 2001 From: Seth Van Niekerk Date: Fri, 27 Mar 2026 12:17:15 -0400 Subject: [PATCH 13/24] Rename workflow for updating external README to clarify its purpose as "Publish Plugin Docs PR" --- .github/workflows/codeql.yml | 8 ++++---- .github/workflows/update-external-readme.yml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 67f36a9..661e3e1 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -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: diff --git a/.github/workflows/update-external-readme.yml b/.github/workflows/update-external-readme.yml index 4b61a85..7e12f96 100644 --- a/.github/workflows/update-external-readme.yml +++ b/.github/workflows/update-external-readme.yml @@ -1,4 +1,4 @@ -name: Update External README +name: Publish Plugin Docs PR # Reads the README.md generated by the Publish Plugins workflow from the # releases branch and opens (or updates) a PR in a target repository. From 8eb59f4048791d45275afddc2fec969b4e11b5ae Mon Sep 17 00:00:00 2001 From: Seth Van Niekerk Date: Fri, 27 Mar 2026 12:19:25 -0400 Subject: [PATCH 14/24] Refactor commit and push process in publish script to simplify logic and remove Git Data API usage, enhancing performance and reliability. --- .github/scripts/publish/run.sh | 111 +++++---------------------------- 1 file changed, 14 insertions(+), 97 deletions(-) diff --git a/.github/scripts/publish/run.sh b/.github/scripts/publish/run.sh index c832f6a..be32c0f 100644 --- a/.github/scripts/publish/run.sh +++ b/.github/scripts/publish/run.sh @@ -102,112 +102,29 @@ echo "" echo "=== Generating releases README ===" bash "$SCRIPT_DIR/releases-readme.sh" -# --- Commit and push via Git Data API (produces a server-signed commit) --- +# --- Commit and push --- echo "" echo "=== Committing ===" rm -rf plugins git rm -rf --cached plugins 2>/dev/null || true -# Collect the files that will form the release commit tree -PUBLISH_FILES=(zips manifest.json README.md) +git add zips manifest.json README.md -# Build the commit message -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 -COMMIT_MSG="Publish plugin updates from $SOURCE_BRANCH +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 Source commit: $source_commit${plugin_list} [skip ci]" -# Get the current tip of the releases branch (the parent commit) -PARENT_SHA=$(gh api "repos/${GITHUB_REPOSITORY}/git/refs/heads/${RELEASES_BRANCH}" \ - --jq '.object.sha') -BASE_TREE_SHA=$(gh api "repos/${GITHUB_REPOSITORY}/git/commits/${PARENT_SHA}" \ - --jq '.tree.sha') - -# Create blobs for every file under the publish dirs and collect tree entries -TREE_ENTRIES="[]" - -add_blob() { - local path="$1" - local tmp_body - tmp_body=$(mktemp) - # Build the JSON body by writing directly to a file — avoids ARG_MAX limits - # from large ZIPs being passed as shell arguments. - { - printf '{"encoding":"base64","content":"' - base64 -w 0 "$path" - printf '"}' - } > "$tmp_body" - local blob_sha - blob_sha=$(gh api "repos/${GITHUB_REPOSITORY}/git/blobs" \ - -X POST \ - --input "$tmp_body" \ - --jq '.sha') - rm -f "$tmp_body" - TREE_ENTRIES=$(echo "$TREE_ENTRIES" | jq \ - --arg p "$path" --arg s "$blob_sha" \ - '. + [{"path": $p, "mode": "100644", "type": "blob", "sha": $s}]') -} - -for entry in "${PUBLISH_FILES[@]}"; do - if [[ -f "$entry" ]]; then - add_blob "$entry" - elif [[ -d "$entry" ]]; then - while IFS= read -r -d '' file; do - add_blob "$file" - done < <(find "$entry" -type f -print0) - fi -done - -# Create the new tree, rooted on the current base tree. -# Write the body to a temp file for the same reason (many/large tree entries). -TREE_BODY_FILE=$(mktemp) -jq -n \ - --arg base "$BASE_TREE_SHA" \ - --argjson tree "$TREE_ENTRIES" \ - '{"base_tree": $base, "tree": $tree}' > "$TREE_BODY_FILE" -NEW_TREE_SHA=$(gh api "repos/${GITHUB_REPOSITORY}/git/trees" \ - -X POST \ - --input "$TREE_BODY_FILE" \ - --jq '.sha') -rm -f "$TREE_BODY_FILE" - -# Bail out early if nothing changed -if [[ "$NEW_TREE_SHA" == "$BASE_TREE_SHA" ]]; then - echo "No changes to commit." -else - # Resolve the author identity (mirrors the git config block above) - if [[ -n "${APP_SLUG:-}" ]]; then - AUTHOR_NAME="${APP_SLUG}[bot]" - AUTHOR_EMAIL=$(git config user.email) # set from API user-ID lookup earlier - else - AUTHOR_NAME="github-actions[bot]" - AUTHOR_EMAIL="41898282+github-actions[bot]@users.noreply.github.com" - fi - - NOW=$(date -u +"%Y-%m-%dT%H:%M:%SZ") - - # Create the commit via the API — GitHub signs it server-side - NEW_COMMIT_SHA=$(gh api "repos/${GITHUB_REPOSITORY}/git/commits" \ - -X POST \ - -f "message=${COMMIT_MSG}" \ - -f "tree=${NEW_TREE_SHA}" \ - -f "parents[]=${PARENT_SHA}" \ - --field "author[name]=${AUTHOR_NAME}" \ - --field "author[email]=${AUTHOR_EMAIL}" \ - --field "author[date]=${NOW}" \ - --jq '.sha') - - # Fast-forward the branch ref to the new commit - gh api "repos/${GITHUB_REPOSITORY}/git/refs/heads/${RELEASES_BRANCH}" \ - -X PATCH \ - -f "sha=${NEW_COMMIT_SHA}" \ - -F force=false - - echo "Successfully published to ${RELEASES_BRANCH} (commit ${NEW_COMMIT_SHA:0:7})" + git push "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" $RELEASES_BRANCH + echo "Successfully published to ${RELEASES_BRANCH}" fi From 572c3da96a8e684548e1ebfbc767aab6e92acf36 Mon Sep 17 00:00:00 2001 From: Seth Van Niekerk Date: Fri, 27 Mar 2026 12:19:56 -0400 Subject: [PATCH 15/24] Fix comment formatting in scripts and workflows for consistency --- .github/scripts/publish/run.sh | 6 +++--- .github/workflows/publish-plugins.yml | 2 +- .github/workflows/update-external-readme.yml | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/scripts/publish/run.sh b/.github/scripts/publish/run.sh index be32c0f..b6a731a 100644 --- a/.github/scripts/publish/run.sh +++ b/.github/scripts/publish/run.sh @@ -36,9 +36,9 @@ 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 — use GitHub App bot identity when available, otherwise fall back +# 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 +# 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 "") @@ -46,7 +46,7 @@ if [[ -n "${APP_SLUG:-}" ]]; then 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 + # 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 diff --git a/.github/workflows/publish-plugins.yml b/.github/workflows/publish-plugins.yml index 0ac94e4..821e215 100644 --- a/.github/workflows/publish-plugins.yml +++ b/.github/workflows/publish-plugins.yml @@ -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 diff --git a/.github/workflows/update-external-readme.yml b/.github/workflows/update-external-readme.yml index 7e12f96..bafc41f 100644 --- a/.github/workflows/update-external-readme.yml +++ b/.github/workflows/update-external-readme.yml @@ -38,7 +38,7 @@ jobs: fi source .github/scripts/config.env if [[ -z "$EXTERNAL_README_REPO" || -z "$EXTERNAL_README_PATH" || -z "$GH_APP_ID" || -z "${GH_APP_PRIVATE_KEY:-}" ]]; then - echo "::notice::EXTERNAL_README_REPO, EXTERNAL_README_PATH, GH_APP_ID, or GH_APP_PRIVATE_KEY not configured — skipping." + echo "::notice::EXTERNAL_README_REPO, EXTERNAL_README_PATH, GH_APP_ID, or GH_APP_PRIVATE_KEY not configured - skipping." echo "skip=true" >> "$GITHUB_OUTPUT" else echo "skip=false" >> "$GITHUB_OUTPUT" @@ -76,7 +76,7 @@ jobs: # Extract source commit SHA from "Source commit: " line SOURCE_COMMIT=$(echo "$COMMIT_MSG" | grep -oP '(?<=Source commit: )[0-9a-f]+' || true) - # Extract plugin list — lines starting with "- " after the source commit line + # Extract plugin list - lines starting with "- " after the source commit line PLUGIN_LIST=$(echo "$COMMIT_MSG" | grep -E '^\- [a-z0-9].*@' || true) RELEASES_COMMIT=$(git rev-parse --short HEAD) @@ -119,7 +119,7 @@ jobs: RELEASES_COMMIT: ${{ steps.meta.outputs.releases_commit }} run: | if [[ ! -f README.md ]]; then - echo "::error::README.md not found on the releases branch — has the Publish Plugins workflow run yet?" + echo "::error::README.md not found on the releases branch - has the Publish Plugins workflow run yet?" exit 1 fi @@ -162,7 +162,7 @@ jobs: NEW_STRIPPED=$(grep -v '^\*Last updated:' README.md || true) EXISTING_STRIPPED=$(echo "$EXISTING_CONTENT" | grep -v '^\*Last updated:' || true) if [[ "$NEW_STRIPPED" == "$EXISTING_STRIPPED" ]]; then - echo "README content unchanged (only timestamp differs) — skipping." + echo "README content unchanged (only timestamp differs) - skipping." echo "::notice::External README not updated: only the timestamp line changed." exit 0 fi From 92014c07f7959e2512f5cf53700d1c474bf82faf Mon Sep 17 00:00:00 2001 From: Seth Van Niekerk Date: Fri, 27 Mar 2026 12:27:00 -0400 Subject: [PATCH 16/24] Remove config.env file and update workflows to use environment variables directly for improved configuration management --- .github/scripts/config.env | 3 -- .github/scripts/publish/run.sh | 44 ++++++++++++++++---- .github/workflows/publish-plugins.yml | 4 +- .github/workflows/update-external-readme.yml | 24 ++++------- .github/workflows/validate-plugin.yml | 24 +++-------- plugins/dispatcharr-exporter/plugin.json | 2 +- 6 files changed, 51 insertions(+), 50 deletions(-) delete mode 100644 .github/scripts/config.env diff --git a/.github/scripts/config.env b/.github/scripts/config.env deleted file mode 100644 index 523b529..0000000 --- a/.github/scripts/config.env +++ /dev/null @@ -1,3 +0,0 @@ -GH_APP_ID="3202290" -EXTERNAL_README_REPO="sv-dispatcharr/Dispatcharr-Docs" -EXTERNAL_README_PATH="docs/en/plugin-listing.md" diff --git a/.github/scripts/publish/run.sh b/.github/scripts/publish/run.sh index b6a731a..9a2619e 100644 --- a/.github/scripts/publish/run.sh +++ b/.github/scripts/publish/run.sh @@ -113,18 +113,46 @@ 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 + # 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 + 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 diff --git a/.github/workflows/publish-plugins.yml b/.github/workflows/publish-plugins.yml index 821e215..2669cbb 100644 --- a/.github/workflows/publish-plugins.yml +++ b/.github/workflows/publish-plugins.yml @@ -50,11 +50,9 @@ jobs: - 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 [[ -f .github/scripts/config.env ]]; then - source .github/scripts/config.env - fi 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" diff --git a/.github/workflows/update-external-readme.yml b/.github/workflows/update-external-readme.yml index bafc41f..4196643 100644 --- a/.github/workflows/update-external-readme.yml +++ b/.github/workflows/update-external-readme.yml @@ -19,30 +19,20 @@ jobs: timeout-minutes: 10 steps: - - name: Checkout config - uses: actions/checkout@v4 - with: - ref: main - fetch-depth: 1 - sparse-checkout: .github/scripts/config.env - sparse-checkout-cone-mode: false - - name: Load and validate configuration id: config env: + GH_APP_ID: ${{ vars.GH_APP_ID }} GH_APP_PRIVATE_KEY: ${{ secrets.GH_APP_PRIVATE_KEY }} + EXTERNAL_README_REPO: ${{ vars.EXTERNAL_README_REPO }} + EXTERNAL_README_PATH: ${{ vars.EXTERNAL_README_PATH }} run: | - if [[ ! -f .github/scripts/config.env ]]; then - echo "::error::.github/scripts/config.env not found." - exit 1 - fi - source .github/scripts/config.env - if [[ -z "$EXTERNAL_README_REPO" || -z "$EXTERNAL_README_PATH" || -z "$GH_APP_ID" || -z "${GH_APP_PRIVATE_KEY:-}" ]]; then - echo "::notice::EXTERNAL_README_REPO, EXTERNAL_README_PATH, GH_APP_ID, or GH_APP_PRIVATE_KEY not configured - skipping." + if [[ -z "${GH_APP_ID:-}" || -z "${GH_APP_PRIVATE_KEY:-}" || -z "${EXTERNAL_README_REPO:-}" || -z "${EXTERNAL_README_PATH:-}" ]]; then + echo "::notice::GH_APP_ID, GH_APP_PRIVATE_KEY, EXTERNAL_README_REPO, or EXTERNAL_README_PATH not configured - skipping." echo "skip=true" >> "$GITHUB_OUTPUT" else - echo "skip=false" >> "$GITHUB_OUTPUT" - echo "app_id=$GH_APP_ID" >> "$GITHUB_OUTPUT" + echo "skip=false" >> "$GITHUB_OUTPUT" + echo "app_id=$GH_APP_ID" >> "$GITHUB_OUTPUT" echo "target_repo=$EXTERNAL_README_REPO" >> "$GITHUB_OUTPUT" echo "target_path=$EXTERNAL_README_PATH" >> "$GITHUB_OUTPUT" fi diff --git a/.github/workflows/validate-plugin.yml b/.github/workflows/validate-plugin.yml index 07b2afe..ac15872 100644 --- a/.github/workflows/validate-plugin.yml +++ b/.github/workflows/validate-plugin.yml @@ -37,11 +37,9 @@ jobs: - 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 [[ -f .github/scripts/config.env ]]; then - source .github/scripts/config.env - fi 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" @@ -132,11 +130,9 @@ jobs: - 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 [[ -f .github/scripts/config.env ]]; then - source .github/scripts/config.env - fi 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" @@ -188,11 +184,9 @@ jobs: - 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 [[ -f .github/scripts/config.env ]]; then - source .github/scripts/config.env - fi 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" @@ -510,11 +504,9 @@ jobs: - 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 [[ -f .github/scripts/config.env ]]; then - source .github/scripts/config.env - fi 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" @@ -582,11 +574,9 @@ jobs: - 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 [[ -f .github/scripts/config.env ]]; then - source .github/scripts/config.env - fi 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" @@ -678,11 +668,9 @@ jobs: - 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 [[ -f .github/scripts/config.env ]]; then - source .github/scripts/config.env - fi 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" diff --git a/plugins/dispatcharr-exporter/plugin.json b/plugins/dispatcharr-exporter/plugin.json index 884ee8d..395f670 100644 --- a/plugins/dispatcharr-exporter/plugin.json +++ b/plugins/dispatcharr-exporter/plugin.json @@ -1,6 +1,6 @@ { "name": "Dispatcharr Exporter", - "version": "2.4.69", + "version": "2.4.1", "description": "Expose Dispatcharr metrics in Prometheus exporter-compatible format for monitoring", "license": "MIT", "discord_thread": "https://discord.com/channels/1340492560220684331/1451260201775923421", From 7e38e3400bab6493295c0691f99765e4d5b90695 Mon Sep 17 00:00:00 2001 From: Seth Van Niekerk Date: Fri, 27 Mar 2026 12:30:20 -0400 Subject: [PATCH 17/24] Add PR reviewer support to update-external-readme workflow for automated reviews --- .github/workflows/update-external-readme.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/update-external-readme.yml b/.github/workflows/update-external-readme.yml index 4196643..7feef70 100644 --- a/.github/workflows/update-external-readme.yml +++ b/.github/workflows/update-external-readme.yml @@ -107,6 +107,7 @@ jobs: SOURCE_REPO: ${{ github.repository }} SOURCE_COMMIT: ${{ steps.meta.outputs.source_commit }} RELEASES_COMMIT: ${{ steps.meta.outputs.releases_commit }} + PR_REVIEWER: ${{ vars.PR_REVIEWER }} run: | if [[ ! -f README.md ]]; then echo "::error::README.md not found on the releases branch - has the Publish Plugins workflow run yet?" @@ -185,11 +186,14 @@ jobs: else PR_BODY=$(printf 'Automated update of the plugin releases listings generated from [`%s`](https://github.com/%s).\n\n%s' \ "$SOURCE_REPO" "$SOURCE_REPO" "$(build_summary)") + REVIEWER_ARGS=() + [[ -n "${PR_REVIEWER:-}" ]] && REVIEWER_ARGS+=(--reviewer "${PR_REVIEWER#@}") gh pr create \ --repo "$TARGET_REPO" \ --head "$BRANCH" \ --base "$DEFAULT_BRANCH" \ --title "chore: update plugin releases listings" \ - --body "$PR_BODY" + --body "$PR_BODY" \ + "${REVIEWER_ARGS[@]}" echo "Created new PR" fi From 3d0096d9fbc17a025aa5845660f913d706258086 Mon Sep 17 00:00:00 2001 From: Seth Van Niekerk Date: Fri, 27 Mar 2026 12:34:19 -0400 Subject: [PATCH 18/24] Enhance PR reviewer support in update-external-readme workflow to handle multiple reviewers --- .github/workflows/update-external-readme.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/update-external-readme.yml b/.github/workflows/update-external-readme.yml index 7feef70..22e6261 100644 --- a/.github/workflows/update-external-readme.yml +++ b/.github/workflows/update-external-readme.yml @@ -187,7 +187,13 @@ jobs: PR_BODY=$(printf 'Automated update of the plugin releases listings generated from [`%s`](https://github.com/%s).\n\n%s' \ "$SOURCE_REPO" "$SOURCE_REPO" "$(build_summary)") REVIEWER_ARGS=() - [[ -n "${PR_REVIEWER:-}" ]] && REVIEWER_ARGS+=(--reviewer "${PR_REVIEWER#@}") + if [[ -n "${PR_REVIEWER:-}" ]]; then + IFS=',' read -ra _reviewers <<< "$PR_REVIEWER" + for _r in "${_reviewers[@]}"; do + _r="${_r#@}"; _r="${_r// /}" # strip @ and whitespace + [[ -n "$_r" ]] && REVIEWER_ARGS+=(--reviewer "$_r") + done + fi gh pr create \ --repo "$TARGET_REPO" \ --head "$BRANCH" \ From 976f4086ec88cfdd16cc0db183f4b5023bee1d14 Mon Sep 17 00:00:00 2001 From: Seth Van Niekerk Date: Fri, 27 Mar 2026 12:38:17 -0400 Subject: [PATCH 19/24] Add contribution link to README generation process in update-external-readme workflow --- .github/workflows/update-external-readme.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/update-external-readme.yml b/.github/workflows/update-external-readme.yml index 22e6261..a39ef9f 100644 --- a/.github/workflows/update-external-readme.yml +++ b/.github/workflows/update-external-readme.yml @@ -85,8 +85,9 @@ jobs: # Remove the intro paragraph and Quick Access section that are only # relevant on the releases branch itself, keeping everything from # "## Available Plugins" onward (preserving the "# Plugin Releases" title). - awk ' - /^# Plugin Releases/ { print; next } + CONTRIB_URL="https://github.com/${SOURCE_REPO}/blob/main/CONTRIBUTING.md" + awk -v contrib="$CONTRIB_URL" ' + /^# Plugin Releases/ { print; print ""; print "Want to get your plugin added to this list? Check out the [plugin repository](" contrib ") to learn how to contribute."; next } /^## Available Plugins/ { found=1 } found { print } ' README.md > README.stripped.md From 376daf3cdf331c333537af38bf2af23ee3bd328d Mon Sep 17 00:00:00 2001 From: Seth Van Niekerk Date: Fri, 27 Mar 2026 12:46:21 -0400 Subject: [PATCH 20/24] Update README and CONTRIBUTING files for clarity on plugin structure and packaging process --- .github/scripts/publish/releases-readme.sh | 2 +- .github/scripts/publish/run.sh | 4 ++-- CONTRIBUTING.md | 4 ++++ README.md | 2 +- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/scripts/publish/releases-readme.sh b/.github/scripts/publish/releases-readme.sh index 98de7f9..e6ac05f 100644 --- a/.github/scripts/publish/releases-readme.sh +++ b/.github/scripts/publish/releases-readme.sh @@ -50,7 +50,7 @@ render_plugin() { 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 "" diff --git a/.github/scripts/publish/run.sh b/.github/scripts/publish/run.sh index 9a2619e..780fe41 100644 --- a/.github/scripts/publish/run.sh +++ b/.github/scripts/publish/run.sh @@ -114,8 +114,8 @@ if git diff --cached --quiet; then echo "No changes to commit." else # Check whether the staged diff is purely timestamp noise: - # README.md — "*Last updated: ..." footer - # manifest.json — "generated_at" field + # 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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 63f17ce..7f5cbd5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -16,10 +16,14 @@ 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 ``` +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. + Plugin folder names must be **lowercase-kebab-case** (e.g. `my-plugin-name`). ## Submitting a Plugin diff --git a/README.md b/README.md index 8bd608f..87d164a 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ A repository for publishing and distributing Dispatcharr Python plugins with aut ## How It Works -Each plugin lives in `plugins//` and must contain a valid `plugin.json`. When a PR is merged to `main`, plugins are automatically packaged and published to the [`releases` branch](https://github.com/Dispatcharr/Plugins/tree/releases). +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. ### PR Validation From 1aab822b029d5ca0f5e85f4f4d892a70c9ac8202 Mon Sep 17 00:00:00 2001 From: Seth Van Niekerk Date: Fri, 27 Mar 2026 12:48:04 -0400 Subject: [PATCH 21/24] Add Dispatcharr compatibility information to plugin README generation --- .github/scripts/publish/plugin-readmes.sh | 14 +++++++++++++ .github/scripts/publish/releases-readme.sh | 24 ++++++++++++++++++++-- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/.github/scripts/publish/plugin-readmes.sh b/.github/scripts/publish/plugin-readmes.sh index f32ddc2..c66f2c7 100644 --- a/.github/scripts/publish/plugin-readmes.sh +++ b/.github/scripts/publish/plugin-readmes.sh @@ -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 @@ -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" diff --git a/.github/scripts/publish/releases-readme.sh b/.github/scripts/publish/releases-readme.sh index e6ac05f..9eee9f6 100644 --- a/.github/scripts/publish/releases-readme.sh +++ b/.github/scripts/publish/releases-readme.sh @@ -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}" @@ -49,6 +51,18 @@ 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 "- [All Versions ($version_count available)]($releases_dir)" @@ -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) @@ -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 From 4123e4bba7d3b6115357e59d27ec3bb4e02f7b44 Mon Sep 17 00:00:00 2001 From: Seth Van Niekerk Date: Fri, 27 Mar 2026 12:59:54 -0400 Subject: [PATCH 22/24] Update CONTRIBUTING.md to clarify metadata-only versioning exceptions and enhance validation workflow documentation --- .github/scripts/validate/validate.sh | 37 ++++++++++++++++++++++++++-- CONTRIBUTING.md | 17 +++++++++++-- 2 files changed, 50 insertions(+), 4 deletions(-) diff --git a/.github/scripts/validate/validate.sh b/.github/scripts/validate/validate.sh index fd1c564..7e9c900 100755 --- a/.github/scripts/validate/validate.sh +++ b/.github/scripts/validate/validate.sh @@ -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 |") diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7f5cbd5..da9f8da 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -34,7 +34,7 @@ Plugin folder names must be **lowercase-kebab-case** (e.g. `my-plugin-name`). 4. Optionally add a `README.md` and `logo.png` 5. Submit a pull request to `main` -For **updates**, increment the version in `plugin.json` - the validation workflow enforces this. +For **updates**, increment the version in `plugin.json` - the validation workflow enforces this. Exception: some metadata-only fields (`description`, `repo_url`, `discord_thread`, `maintainers`, `min_dispatcharr_version`, `max_dispatcharr_version`, `deprecated`, `unlisted`) can be updated without a version bump. ## `plugin.json` Spec @@ -99,7 +99,7 @@ Automated validation runs on every PR and posts a comment with results. The foll | JSON syntax | Must be valid JSON | | Required fields | `name`, `version`, `description`, `author` or `maintainers`, `license` | | Version format | Must be `MAJOR.MINOR.PATCH` (semver) | -| Version bump | Must be greater than the current published version | +| Version bump | Must be greater than the current published version (see [metadata-only exceptions](#versioning)) | | Permission | PR author must be listed in `author` or `maintainers` | | License | Must be a valid OSI-approved SPDX identifier | | `min_dispatcharr_version` | Must be semver if provided | @@ -133,6 +133,19 @@ Plugins use [semantic versioning](https://semver.org/) (`MAJOR.MINOR.PATCH`): Version increments are enforced by the validation workflow. You cannot submit a PR with the same or lower version than the currently published plugin. +**Metadata-only updates** are an exception - the following fields can be changed without bumping the version: + +- `description` +- `repo_url` +- `discord_thread` +- `maintainers` +- `min_dispatcharr_version` +- `max_dispatcharr_version` +- `deprecated` +- `unlisted` + +All other fields - including `name`, `author`, `license`, and any code changes - require a version bump. + ## Licensing All plugins must be distributed under an [OSI-approved open source license](https://opensource.org/licenses). The `license` field is required in `plugin.json` and must be a valid [SPDX identifier](https://spdx.org/licenses/). From 80ce371fb0ab33a1e1f2b75c2cfe108040cb6936 Mon Sep 17 00:00:00 2001 From: Seth Van Niekerk Date: Fri, 27 Mar 2026 13:08:11 -0400 Subject: [PATCH 23/24] Add pull request template to standardize submissions --- .github/PULL_REQUEST_TEMPLATE.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..0b80141 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,22 @@ + + + + +## What does this PR do? + + + +## Pre-submission checklist + + + +**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 From 5643110833d24b6a4265bb22aab28231a6604268 Mon Sep 17 00:00:00 2001 From: Seth Van Niekerk Date: Fri, 27 Mar 2026 13:14:49 -0400 Subject: [PATCH 24/24] Refactor pull request template comments for clarity and formatting --- .github/PULL_REQUEST_TEMPLATE.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 0b80141..c3d958d 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,8 +1,11 @@ - - - + + +## About this submission