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
111 changes: 111 additions & 0 deletions .github/workflows/validate-catalog.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
name: Validate catalog

# Validates plugins.json (the trust root the Phlix server reads) on every push
# and pull request:
# 1. JSON-Schema conformance against plugins.schema.json (pinned validator).
# 2. plugins.json is valid JSON AND entries are sorted by `name` with no
# duplicate `name` (the "unsorted/undeduped" code-quality gate).
# 3. (non-blocking) per-entry pin-drift check: download each pinned tarball
# and assert its sha256 matches artifactSha256, surfacing drift without
# flaking PRs.
#
# Landmines honored:
# - The schema uses `format: uri`, so ajv needs ajv-formats (co-installed and
# passed via `-c ajv-formats`), otherwise the `uri` format keyword errors.
# - The schema is Draft 2020-12, so ajv needs `--spec=draft2020`.
# - NO setup-node `cache: npm` — this repo has no lockfile.

on:
push:
pull_request:

permissions:
contents: read

jobs:
schema:
name: JSON-Schema + sort/dedup
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: '24'
# No `cache: npm` — there is no lockfile in this repo (known landmine).

- name: Validate plugins.json against plugins.schema.json
run: |
# ajv-cli@5 (ajv v8) for Draft 2020-12; ajv-formats@2 supplies the
# `uri` format keyword the schema relies on. Both pinned.
npx --yes --package ajv-cli@5 --package ajv-formats@2 -- \
ajv validate \
--spec=draft2020 \
-s plugins.schema.json \
-d plugins.json \
-c ajv-formats

- name: Assert plugins.json is valid JSON, sorted by name, no duplicates
run: |
# `jq -e` already fails (exit 5) on a JSON parse error, so this step
# doubles as the "valid JSON" assertion. The filter then fails (via
# halt_error 1) if the names are out of order or contain a duplicate.
jq -e -r '
.plugins | map(.name) as $names
| ($names | sort) as $sorted
| ($names | unique) as $uniq
| if ($names != $sorted) then
"ERROR: plugins are not sorted by name.\n got: \($names)\n want: \($sorted)"
| halt_error(1)
elif ($uniq | length) != ($names | length) then
"ERROR: duplicate plugin name(s): "
+ ([$names[] | select(. as $n | ([$names[] | select(. == $n)] | length) > 1)] | unique | tostring)
| halt_error(1)
else
"OK: \($names | length) entries — valid JSON, sorted by name, no duplicates."
end
' plugins.json

pin-drift:
name: Pin-drift (non-blocking)
runs-on: ubuntu-latest
# Surfaces ref/artifactSha256 drift without ever failing a PR.
continue-on-error: true
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Verify each entry's artifactSha256 matches its pinned tarball
run: |
set -u
drift=0
# owner/repo are derived from the entry `repo` URL; ref + expected
# digest come straight from the catalog.
while IFS=$'\t' read -r name repo ref expected; do
owner_repo="${repo#https://github.com/}"
owner_repo="${owner_repo%/}"
url="https://github.com/${owner_repo}/archive/${ref}.tar.gz"
echo "::group::${name}"
echo "url: ${url}"
actual="$(curl -fsSL "${url}" | sha256sum | cut -d' ' -f1)" || {
echo "WARN: could not download ${url}"
echo "::endgroup::"
drift=1
continue
}
if [ "${actual}" = "${expected}" ]; then
echo "OK: ${actual}"
else
echo "DRIFT: expected ${expected} but tarball hashes to ${actual}"
drift=1
fi
echo "::endgroup::"
done < <(jq -r '.plugins[] | [.name, .repo, .ref, .artifactSha256] | @tsv' plugins.json)

if [ "${drift}" -ne 0 ]; then
echo "Pin drift detected (non-blocking). Review the entries above."
exit 1
fi
echo "All pins match their tarballs."
46 changes: 30 additions & 16 deletions plugins.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,27 @@
"artifactSha256": "e8277e0ed419e4eb02245ad86896025bb6bb8ce90c348d2a7c7ea958724ce08c",
"version": "0.1.0",
"author": "detain",
"tags": ["anime", "metadata"]
"tags": [
"anime",
"metadata"
]
},
{
"name": "phlix-plugin-lastfm",
"title": "Last.fm",
"type": "scrobbler",
"summary": "Scrobble music playback to Last.fm.",
"description": "Last.fm scrobbler — scrobbles music playback to Last.fm, marking now-playing on start and submitting the scrobble at the end.",
"repo": "https://github.com/detain/phlix-plugin-lastfm",
"ref": "43e1e7d2ce5108250a8c95a10e31ee86ba14e412",
"artifactSha256": "5ef58eb42ec7e77fd454204169886cbafe0a36c60111c525710a0471b0e444c4",
"version": "1.0.0",
"author": "detain",
"tags": [
"scrobbler",
"lastfm",
"music"
]
},
{
"name": "phlix-plugin-myanimelist",
Expand All @@ -28,7 +48,11 @@
"artifactSha256": "c484dca258e68e9698a68b3d93639b6ff0db220cb047ff13ee62b3688f28cf41",
"version": "0.1.0",
"author": "detain",
"tags": ["anime", "manga", "metadata"]
"tags": [
"anime",
"manga",
"metadata"
]
},
{
"name": "phlix-plugin-trakt",
Expand All @@ -41,20 +65,10 @@
"artifactSha256": "e5fdb4aa2de67ea019ef194dd098fd05d850cc9d8d1f14c5323c095714d7fb46",
"version": "1.0.0",
"author": "detain",
"tags": ["scrobbler", "trakt"]
},
{
"name": "phlix-plugin-lastfm",
"title": "Last.fm",
"type": "scrobbler",
"summary": "Scrobble music playback to Last.fm.",
"description": "Last.fm scrobbler — scrobbles music playback to Last.fm, marking now-playing on start and submitting the scrobble at the end.",
"repo": "https://github.com/detain/phlix-plugin-lastfm",
"ref": "43e1e7d2ce5108250a8c95a10e31ee86ba14e412",
"artifactSha256": "5ef58eb42ec7e77fd454204169886cbafe0a36c60111c525710a0471b0e444c4",
"version": "1.0.0",
"author": "detain",
"tags": ["scrobbler", "lastfm", "music"]
"tags": [
"scrobbler",
"trakt"
]
}
]
}
Loading