Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
11 changes: 11 additions & 0 deletions .github/scripts/keys/dispatcharr-plugins.pub
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----

mDMEacgfABYJKwYBBAHaRw8BAQdAh1MuVNBxk+CExQPjOVDvAGvIk6BdGS2ce9/h
zB7lYtW0TERpc3BhdGNoYXJyIFBsdWdpbiBSZXBvIChkaXNwYXRjaGFyci1hdXRv
Z2VuZXJhdGVkKSA8cGx1Z2luc0BkaXNwYXRjaGFyci50dj6IrwQTFgoAVxYhBEap
MFaOD7nKg0zX+H7AOmtMIjTOBQJpyB8AGxSAAAAAAAQADm1hbnUyLDIuNSsxLjEy
LDAsMwIbAwULCQgHAgIiAgYVCgkICwIEFgIDAQIeBwIXgAAKCRB+wDprTCI0zvNZ
AP9r3TpMpiI8BCNo9B5M9lJ+QLRo9ihPWIcqBzJ9eFCoSQEAgguiZsNy6aJzKjIb
yDvGuoZi3I2/GNM/f2qVzFtgPQk=
=Zf/y
-----END PGP PUBLIC KEY BLOCK-----
36 changes: 16 additions & 20 deletions .github/scripts/publish/generate-manifest.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ set -e
: "${SOURCE_BRANCH:?}" "${RELEASES_BRANCH:?}" "${GITHUB_REPOSITORY:?}"

generated_at="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
repo_url="https://github.com/${GITHUB_REPOSITORY}"
repo_name="${GITHUB_REPOSITORY}"
registry_url="https://github.com/${GITHUB_REPOSITORY}"
registry_name="${GITHUB_REPOSITORY}"
root_url="https://raw.githubusercontent.com/${GITHUB_REPOSITORY}/${RELEASES_BRANCH}"

# GPG signing setup - optional; set GPG_PRIVATE_KEY (armored) and optionally GPG_PASSPHRASE
gpg_key_id=""
Expand All @@ -32,7 +33,7 @@ fi

# Writes a manifest wrapper to $1 with $2 as the signed payload (.manifest),
# only when .manifest content differs from what is on disk.
# Wrapper structure: {generated_at, repo_url, repo_name, manifest: <payload>}
# Wrapper structure: {generated_at, manifest: <payload>}
# The .signature field is added separately by sign_manifest.
# Returns 0 if written, 1 if skipped (content unchanged).
write_manifest_if_changed() {
Expand All @@ -48,10 +49,8 @@ write_manifest_if_changed() {
fi
jq -n \
--arg generated_at "$generated_at" \
--arg repo_url "$repo_url" \
--arg repo_name "$repo_name" \
--argjson manifest "$new_compact" \
'{generated_at: $generated_at, repo_url: $repo_url, repo_name: $repo_name, manifest: $manifest}' \
'{generated_at: $generated_at, manifest: $manifest}' \
> "$dest"
return 0
}
Expand Down Expand Up @@ -114,7 +113,7 @@ for plugin_dir in plugins/*/; do

echo " $plugin_name"

latest_url="https://github.com/${GITHUB_REPOSITORY}/raw/$RELEASES_BRANCH/zips/${plugin_name}/${plugin_name}-latest.zip"
latest_url="zips/${plugin_name}/${plugin_name}-latest.zip"

versioned_zips="[]"
latest_metadata="{}"
Expand All @@ -125,7 +124,7 @@ for plugin_dir in plugins/*/; do
while IFS= read -r zipfile; do
zip_basename=$(basename "$zipfile")
zip_version=$(echo "$zip_basename" | sed "s/${plugin_name}-\(.*\)\.zip/\1/")
zip_url="https://github.com/${GITHUB_REPOSITORY}/raw/$RELEASES_BRANCH/zips/${plugin_name}/${zip_basename}"
zip_url="zips/${plugin_name}/${zip_basename}"

# Fresh metadata from this run takes priority; fall back to existing manifest
fresh_meta_file="${BUILD_META_DIR:-}/$plugin_name/${plugin_name}-${zip_version}.json"
Expand All @@ -151,26 +150,22 @@ for plugin_dir in plugins/*/; do
done < <(ls -1 "zips/$plugin_name/${plugin_name}"-*.zip 2>/dev/null \
| grep -v latest | sort -t- -k2 -V -r)

# Compute icon_url before building plugin_entry so it can be included in both manifests
icon_url=""
if [[ -f "plugins/$plugin_name/logo.png" ]]; then
icon_url="https://raw.githubusercontent.com/${GITHUB_REPOSITORY}/${SOURCE_BRANCH}/plugins/${plugin_name}/logo.png"
fi

plugin_entry=$(jq \
--arg plugin_name "$plugin_name" \
--arg latest_url "$latest_url" \
--arg icon_url "$icon_url" \
--arg registry_url "$registry_url" \
--arg registry_name "$registry_name" \
--argjson versioned_zips "$versioned_zips" \
--argjson latest_metadata "$latest_metadata" \
'with_entries(select(.key | IN(
"name","description","author","maintainers",
"deprecated","repo_url","discord_thread","license"
))) + {
slug: $plugin_name,
registry_url: $registry_url,
registry_name: $registry_name,
versions: $versioned_zips
} + (if $icon_url != "" then {icon_url: $icon_url} else {} end)
+ (
} + (
if ($latest_metadata | length > 0) then {
last_updated: $latest_metadata.last_updated,
latest: ($latest_metadata + {
Expand All @@ -196,23 +191,21 @@ for plugin_dir in plugins/*/; do
desc_trimmed="$desc_raw"
fi

plugin_manifest_url="https://raw.githubusercontent.com/${GITHUB_REPOSITORY}/${RELEASES_BRANCH}/zips/${plugin_name}/manifest.json"
plugin_manifest_url="zips/${plugin_name}/manifest.json"

root_entry=$(jq -n \
--argjson latest_metadata "$latest_metadata" \
--argjson versioned_zips "$versioned_zips" \
--arg slug "$plugin_name" \
--arg name "$(jq -r '.name // ""' "$plugin_file")" \
--arg description "$desc_trimmed" \
--arg icon_url "$icon_url" \
--arg manifest_url "$plugin_manifest_url" \
--arg author "$(jq -r '.author // ""' "$plugin_file")" \
--arg license "$(jq -r '.license // ""' "$plugin_file")" \
'{
slug: $slug,
name: $name,
description: $description,
icon_url: (if $icon_url != "" then $icon_url else null end),
manifest_url: $manifest_url,
author: $author,
license: (if $license != "" then $license else null end),
Expand All @@ -229,6 +222,9 @@ done
inner_root=$(
{
echo '{'
echo ' "registry_url": '"$(jq -n --arg u "$registry_url" '$u')"','
echo ' "registry_name": '"$(jq -n --arg u "$registry_name" '$u')"','
echo ' "root_url": '"$(jq -n --arg u "$root_url" '$u')"','
echo ' "plugins": ['
first=true
for entry in "${root_entries[@]}"; do
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish-plugins.yml
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ jobs:
if [ -d /tmp/tmp.*/repo ]; then
REPO_DIR=$(find /tmp -maxdepth 1 -name "tmp.*" -type d 2>/dev/null | head -1)/repo
if [ -f "$REPO_DIR/manifest.json" ]; then
cat "$REPO_DIR/manifest.json" | jq -r '.plugins[] | "- **\(.name)** v\(.version)"' >> $GITHUB_STEP_SUMMARY
cat "$REPO_DIR/manifest.json" | jq -r '.manifest.plugins[] | "- **\(.name)** v\(.latest_version)"' >> $GITHUB_STEP_SUMMARY
fi
fi
else
Expand Down
29 changes: 24 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,20 +52,39 @@ Visit the [releases branch](https://github.com/Dispatcharr/Plugins/tree/releases
curl https://raw.githubusercontent.com/Dispatcharr/Plugins/releases/manifest.json
```

## Verifying Manifest Signatures
## Manifest Structure

Each manifest file embeds its GPG signature directly. The `signature` field covers the compact (`jq -c '.manifest'`) form of the `manifest` payload:
The root `manifest.json` uses a `root_url` plus relative paths to save space. All URL fields (`manifest_url`, `latest_url`, versioned zip `url`) are relative to `root_url`:

```json
{
"generated_at": "...",
"repo_url": "...",
"repo_name": "owner/repo",
"signature": "-----BEGIN PGP SIGNATURE-----\n...",
"manifest": { ... }
"manifest": {
"registry_url": "https://github.com/Dispatcharr/Plugins",
"registry_name": "Dispatcharr/Plugins",
"root_url": "https://raw.githubusercontent.com/Dispatcharr/Plugins/releases",
"plugins": [
{
"slug": "my-plugin",
"name": "My Plugin",
"manifest_url": "zips/my-plugin/manifest.json",
"latest_url": "zips/my-plugin/my-plugin-1.0.0.zip",
...
}
]
}
}
```

To resolve a full download URL: `root_url + "/" + latest_url`.

The `slug` matches the plugin folder name and can be used to construct other paths (e.g. icon: `plugins/<slug>/logo.png` on the source branch).

## Verifying Manifest Signatures

Each manifest file embeds its GPG signature directly. The `signature` field covers the compact (`jq -c '.manifest'`) form of the `manifest` payload.

The public key is bundled with Dispatcharr. To verify manually, export it from the application or obtain `.github/scripts/keys/dispatcharr-plugins.pub` from the default branch.

### Steps
Expand Down
Loading