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

on:
push:
branches:
- main
pull_request:

permissions:
contents: read

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6

- uses: actions/setup-go@v6
with:
go-version: "1.26"

- name: Test catalog tooling
env:
GOWORK: off
run: go test ./...

- name: Validate approved catalog
env:
GOWORK: off
run: go run ./cmd/check-catalog
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ release workflow dispatches `plugin_release_published` to this repository. The
catalog updater verifies the repository and plugin ID against the approval
registry before updating `manifest.json`.

Validate the registry and generated catalog locally with:

```sh
GOWORK=off go test ./...
GOWORK=off go run ./cmd/check-catalog
```

## License

The catalog tooling is licensed under `Apache-2.0`. See [LICENSE](LICENSE).
15 changes: 14 additions & 1 deletion approved-plugins.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
{
"plugins": []
"plugins": [
{
"plugin_id": "silo.requests.arr",
"repository": "Silo-Community/silo-plugins-requests-arr",
"approved_at": "2026-07-09",
"review_url": "https://github.com/Silo-Community/silo-plugins/issues/2"
},
{
"plugin_id": "silo.requests.seerr",
"repository": "Silo-Community/silo-plugins-requests-seerr",
"approved_at": "2026-07-09",
"review_url": "https://github.com/Silo-Community/silo-plugins/issues/1"
}
]
}
26 changes: 26 additions & 0 deletions catalog/approval.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,32 @@ func (r ApprovalRegistry) Resolve(repository, pluginID string) (Approval, error)
return Approval{}, fmt.Errorf("plugin %s from %s is not approved", pluginID, repository)
}

func ValidateApprovedIndex(index RepositoryIndex, registry ApprovalRegistry) error {
seenPluginIDs := make(map[string]struct{}, len(index.Plugins))
for _, pkg := range index.Plugins {
if pkg.Manifest == nil {
return fmt.Errorf("catalog package manifest is required")
}
pluginID := pkg.Manifest.GetPluginId()
if _, exists := seenPluginIDs[pluginID]; exists {
return fmt.Errorf("catalog plugin_id %q is duplicated", pluginID)
}
seenPluginIDs[pluginID] = struct{}{}
repository := strings.TrimPrefix(pkg.RepoURL, "https://github.com/")
approval, err := registry.Resolve(repository, pluginID)
if err != nil {
return err
}
if pkg.Approval == nil {
return fmt.Errorf("catalog plugin %s is missing approval metadata", pluginID)
}
if *pkg.Approval != approval {
return fmt.Errorf("catalog plugin %s approval metadata does not match the registry", pluginID)
}
}
return nil
}

func validateGitHubURL(raw string) error {
parsed, err := url.Parse(raw)
if err != nil {
Expand Down
21 changes: 21 additions & 0 deletions catalog/approval_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,24 @@ func TestPruneUnapproved(t *testing.T) {
t.Fatal("approved package is missing approval metadata")
}
}

func TestValidateApprovedIndexRejectsMissingApprovalMetadata(t *testing.T) {
registry, err := DecodeApprovalRegistry([]byte(`{
"plugins": [{
"plugin_id": "silo.requests.arr",
"repository": "Silo-Community/silo-plugins-requests-arr",
"approved_at": "2026-07-09",
"review_url": "https://github.com/Silo-Community/silo-plugins/issues/1"
}]
}`))
if err != nil {
t.Fatalf("DecodeApprovalRegistry() error = %v", err)
}
index := RepositoryIndex{Plugins: []CatalogPackage{{
Manifest: &SourceManifest{PluginId: "silo.requests.arr"},
RepoURL: "https://github.com/Silo-Community/silo-plugins-requests-arr",
}}}
if err := ValidateApprovedIndex(index, registry); err == nil {
t.Fatal("ValidateApprovedIndex() unexpectedly succeeded")
}
}
44 changes: 44 additions & 0 deletions cmd/check-catalog/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package main

import (
"encoding/json"
"flag"
"fmt"
"os"

"github.com/Silo-Community/silo-plugins/catalog"
)

func main() {
var manifestPath string
var approvalsPath string
flag.StringVar(&manifestPath, "manifest", "manifest.json", "Path to the catalog manifest")
flag.StringVar(&approvalsPath, "approvals", "approved-plugins.json", "Path to the approved plugin registry")
flag.Parse()

approvalsData, err := os.ReadFile(approvalsPath)
if err != nil {
exitf("read approvals: %v", err)
}
registry, err := catalog.DecodeApprovalRegistry(approvalsData)
if err != nil {
exitf("decode approvals: %v", err)
}

manifestData, err := os.ReadFile(manifestPath)
if err != nil {
exitf("read catalog: %v", err)
}
var index catalog.RepositoryIndex
if err := json.Unmarshal(manifestData, &index); err != nil {
exitf("decode catalog: %v", err)
}
if err := catalog.ValidateApprovedIndex(index, registry); err != nil {
exitf("validate catalog: %v", err)
}
}

func exitf(format string, args ...any) {
fmt.Fprintf(os.Stderr, format+"\n", args...)
os.Exit(1)
}
Loading