From 0e05301d1afb3eff33a60e1ab46858ae8c9c484f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 14:27:58 +0000 Subject: [PATCH] Publish team plugin updates without review when permissions are unchanged - sync.sh: team plugin, existing entry, no new-permissions label -> commit to main and start Publish; falls back to a pull request when main refuses the push - regtool keeps the manifest's platforms (core 0.6.1) in entries and the catalog - CORE_REF v0.6.2 so platforms and per-entry platforms validate - docs: new release flow, REGISTRY_TOKEN, branch protection bypass Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01YBUx1d4niJfMeuwPGpikuu --- .github/workflows/sync.yml | 10 ++++++++-- CONTRIBUTING.md | 5 +++-- CORE_REF | 2 +- README.md | 18 ++++++++++++++---- scripts/sync.sh | 27 +++++++++++++++++++++++++++ tools/regtool/catalog.go | 3 ++- tools/regtool/manifest.go | 5 ++++- tools/regtool/registry.go | 2 ++ 8 files changed, 61 insertions(+), 11 deletions(-) diff --git a/.github/workflows/sync.yml b/.github/workflows/sync.yml index e56d155..4d4ea02 100644 --- a/.github/workflows/sync.yml +++ b/.github/workflows/sync.yml @@ -1,5 +1,10 @@ -# Looks for new releases of the plugins listed in registry.json and opens a -# pull request per new version, with a summary of the permission changes. +# Looks for new releases of the plugins listed in registry.json. A new +# version of a team plugin that asks for no new permissions is committed to +# main and published at once; anything else (community plugins, new +# permissions, a plugin's first version) becomes a pull request with a +# summary of the permission changes. Plugin release workflows start it right +# away with a repository_dispatch (plugin-release); the schedule is the +# fallback. # The plugin repositories need no secret: this workflow pulls their public # releases with its own GITHUB_TOKEN. # @@ -23,6 +28,7 @@ on: permissions: contents: write pull-requests: write + actions: write # starts Publish after a direct (no-review) update concurrency: group: sync diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 76803fd..2e532c7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -53,8 +53,9 @@ the plugin is signed and appears in the catalog within minutes. ## Updates -Tag a new version; nothing else is needed. The sync workflow notices it within six hours and opens a pull request -with a summary of the permission changes. Updates that add or widen permissions get the `new-permissions` label and a +Release a new version (with the SDK's release workflow: Actions › Release › Run workflow); nothing else is needed. +The sync workflow notices it at once (or within six hours) and opens a pull request with a summary of the permission +changes. Team plugins skip the pull request when the update asks for no new permissions. Updates that add or widen permissions get the `new-permissions` label and a closer review. When the pull request is merged, the new version is signed and published, and consoles offer the update in **Plugins › Updates** (asking the user again when permissions changed). diff --git a/CORE_REF b/CORE_REF index b043aa6..45964c6 100644 --- a/CORE_REF +++ b/CORE_REF @@ -1 +1 @@ -v0.5.0 +v0.6.2 diff --git a/README.md b/README.md index 0da9b84..cf99d75 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,9 @@ Plugin authors: see [CONTRIBUTING.md](CONTRIBUTING.md). ## Trust model * Only maintainers merge into `main`. Every plugin listed in `registry.json` is reviewed by a maintainer, and so is - every new version of it (a pull request per version). + every new version of a community plugin (a pull request per version). For **team** plugins only the first version + and updates that add or widen permissions are reviewed; any other team update is published as soon as it is + released (its permissions are exactly what was reviewed before). * After a version is merged, CI signs it with the **Ervisio team key** (ed25519). The consoles only run plugins whose signature verifies against that key (`plugins.allow_unsigned = false` is their default), so a package that was not reviewed here cannot be installed from the marketplace. @@ -28,15 +30,20 @@ Plugin authors: see [CONTRIBUTING.md](CONTRIBUTING.md). ## How a release flows -1. The plugin repository tags `vX.Y.Z` (equal to the manifest version). Its release workflow builds the package and - attaches `-.tar.gz` (one top folder `/`, unsigned `manifest.json`) and `-.tar.gz.sha256`. +1. The plugin repository releases `vX.Y.Z`: Actions › Release › Run workflow (patch / minor / major), the reusable + workflow of Ervisio/plugin-sdk. It bumps the version, writes the changelog, tags, builds and attaches + `-.tar.gz` (one top folder `/`, unsigned `manifest.json`) and `-.tar.gz.sha256`, then + starts Sync here with a `repository_dispatch` (organization secret `REGISTRY_TOKEN`). 2. **Sync** (`.github/workflows/sync.yml`, every 6 hours, or by hand / `repository_dispatch` `plugin-release`) finds the new release, downloads it, checks the checksum, unpacks it safely, validates the manifest with the core's own rules (`plugin-sign` built from `Ervisio/ervisio` at `CORE_REF`, with a throwaway key) and opens a pull request that updates `plugins/.json`. The description lists the permission changes ("New permissions", "Changed permissions", "Removed permissions") with warnings for administrator rights, the Docker socket, network hosts and writable system folders. Updates that add or widen permissions get the `new-permissions` label. -3. A maintainer reviews the source at the tag and the permission summary, then merges. + **Team plugin, update with no new permissions:** instead of a pull request, Sync commits the entry to `main` and + starts Publish; the version is in the catalog a few minutes after the release. If `main` refuses the push + (branch protection without a bypass for GitHub Actions), it falls back to a pull request. +3. Otherwise a maintainer reviews the source at the tag and the permission summary, then merges. 4. **Publish** (`.github/workflows/publish.yml`, on every merge to `main`) downloads the reviewed package again, checks that its checksum and manifest are exactly the reviewed ones, signs it (`manifest.json` with the sha256 of every file, `manifest.sig`), verifies the signature against the team key embedded in the core, repacks it @@ -80,6 +87,7 @@ curl -fsSLO https://ervisio.github.io/plugins/catalog.sig | Name | Where | What | |---|---|---| | `PLUGIN_SIGNING_KEY` | Repository secret | The team private key: the content of the key file written by `plugin-sign -genkey` (one line, base64 of the 64-byte ed25519 private key). Only the publish workflow reads it, writes it to a temporary file with mode 0600 and deletes it. | +| `REGISTRY_TOKEN` | Organization secret (for the plugin repositories) | A fine-grained token with access to Ervisio/plugins only, permission "Contents: read and write" (needed for `repository_dispatch`). Plugin release workflows use it to start Sync at once. Without it Sync still runs every six hours. | A release maintainer sets it from the machine that holds the key (the command reads the file; the key never appears on the command line): @@ -93,6 +101,8 @@ Until the secret exists, the publish workflow stops with "PLUGIN_SIGNING_KEY is * Branch protection on `main`: require a pull request with one approving review, require the **Check** and **CI** checks, no force pushes. Signing happens only on `main`, so this is what keeps unreviewed code out of the catalog. + For automatic team updates, add **GitHub Actions** to the bypass list of that rule (rulesets: "Bypass list › + Repository admin / GitHub Actions"); without it those updates become pull requests as before. * Pages: source "GitHub Actions" (Settings › Pages). * Actions: "Allow GitHub Actions to create and approve pull requests" (Settings › Actions › General), needed by the sync workflow. diff --git a/scripts/sync.sh b/scripts/sync.sh index 12c4aad..e999dc2 100755 --- a/scripts/sync.sh +++ b/scripts/sync.sh @@ -55,6 +55,30 @@ sync_one() { git checkout -q -- "plugins/$id.json" 2>/dev/null || rm -f "plugins/$id.json" return 0 fi + # Team plugins whose update asks for no new permissions are published at + # once: committed to main, then the Publish workflow is started (a push + # made with GITHUB_TOKEN does not start workflows by itself). The first + # version of a plugin and any update with new permissions still need a + # reviewed pull request. + if [ "$trust" = team ] && [ -n "$current" ] && ! grep -qx new-permissions "$WORK/labels-$id" && [ "${AUTO_PUBLISH:-1}" = 1 ]; then + git fetch -q origin main + git checkout -q -B "auto/$id" origin/main + git add "plugins/$id.json" + git -c user.name="github-actions[bot]" -c user.email="41898282+github-actions[bot]@users.noreply.github.com" \ + commit -q -m "Update $id to $version" -m "From $repo $tag ($asset, sha256 $sum). Team plugin, no new permissions: published without review." + if git push -q origin "HEAD:main"; then + note "$id $version: no new permissions, published directly" + if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then + { echo "### $id $version published (team plugin, no new permissions)"; cat "$WORK/body-$id.md"; } >> "$GITHUB_STEP_SUMMARY" + fi + git fetch -q origin main + git checkout -q main && git reset -q --hard origin/main + : > "$WORK/publish-needed" # sync_one runs in a subshell + return 0 + fi + note "$id $version: could not push to main (protected?), opening a pull request instead" + git checkout -q main + fi git checkout -q -B "$branch" origin/main git add "plugins/$id.json" git -c user.name="github-actions[bot]" -c user.email="41898282+github-actions[bot]@users.noreply.github.com" \ @@ -92,4 +116,7 @@ while read -r -u 3 id repo trust; do git checkout -q main 2>/dev/null || true fi done 3< <(regtool list) +if [ -f "$WORK/publish-needed" ] && [ "${DRY_RUN:-}" != 1 ]; then + gh workflow run publish.yml --repo "$REGISTRY_REPO" --ref main || { echo "::error::could not start the Publish workflow"; failed=1; } +fi exit "$failed" diff --git a/tools/regtool/catalog.go b/tools/regtool/catalog.go index 6b605c1..f1453dc 100644 --- a/tools/regtool/catalog.go +++ b/tools/regtool/catalog.go @@ -122,6 +122,7 @@ type CatalogEntry struct { Notes string `json:"notes,omitempty"` MinCore string `json:"minCore,omitempty"` Requires json.RawMessage `json:"requires,omitempty"` + Platforms json.RawMessage `json:"platforms,omitempty"` Source string `json:"source"` SHA256 string `json:"sha256"` Capabilities json.RawMessage `json:"capabilities"` @@ -181,7 +182,7 @@ func buildCatalog(root, signedDir string, now time.Time) (*CatalogFile, error) { c.Plugins = append(c.Plugins, CatalogEntry{ ID: src.ID, Name: m.Reviewed.Name, Version: e.Version, Author: m.Reviewed.Author, Description: m.Reviewed.Description, Icon: m.Reviewed.Icon, Logo: logo, Color: m.Reviewed.Color, Category: src.Category, Verified: true, Featured: src.Featured, - Notes: e.Notes, MinCore: m.Reviewed.MinCore, Requires: m.Reviewed.Requires, Source: SignedBase + src.ID + "-" + e.Version + "/" + name, SHA256: sum, + Notes: e.Notes, MinCore: m.Reviewed.MinCore, Requires: m.Reviewed.Requires, Platforms: m.Reviewed.Platforms, Source: SignedBase + src.ID + "-" + e.Version + "/" + name, SHA256: sum, Capabilities: m.Reviewed.Capabilities, Contributes: m.Reviewed.Contributes, VisibleTo: m.Reviewed.VisibleTo, Homepage: m.Reviewed.Homepage, Repo: src.Repo, Trust: src.Trust, }) diff --git a/tools/regtool/manifest.go b/tools/regtool/manifest.go index 5e3ba21..d1bc022 100644 --- a/tools/regtool/manifest.go +++ b/tools/regtool/manifest.go @@ -50,6 +50,9 @@ func readManifest(dir string) (*pluginManifest, error) { if v, ok := raw["requires"]; ok && !bytes.Equal(bytes.TrimSpace(v), []byte("null")) { m.Reviewed.Requires = compact(v) } + if v, ok := raw["platforms"]; ok && !bytes.Equal(bytes.TrimSpace(v), []byte("null")) { + m.Reviewed.Platforms = compact(v) + } return m, nil } @@ -91,7 +94,7 @@ func compareReviewed(e *Entry, m *pluginManifest) []string { for _, f := range []struct { name string want, got json.RawMessage - }{{"capabilities", r.Capabilities, got.Capabilities}, {"contributes", r.Contributes, got.Contributes}, {"visibleTo", r.VisibleTo, got.VisibleTo}, {"requires", r.Requires, got.Requires}} { + }{{"capabilities", r.Capabilities, got.Capabilities}, {"contributes", r.Contributes, got.Contributes}, {"visibleTo", r.VisibleTo, got.VisibleTo}, {"requires", r.Requires, got.Requires}, {"platforms", r.Platforms, got.Platforms}} { if len(f.want) == 0 && len(f.got) == 0 { continue } diff --git a/tools/regtool/registry.go b/tools/regtool/registry.go index aa6510a..67b415d 100644 --- a/tools/regtool/registry.go +++ b/tools/regtool/registry.go @@ -63,6 +63,8 @@ type Reviewed struct { // MinCore and Requires say which Ervisio the plugin needs (core 0.5.0). MinCore string `json:"minCore,omitempty"` Requires json.RawMessage `json:"requires,omitempty"` + // Platforms: the systems the plugin works on (core 0.6.1); empty = Linux. + Platforms json.RawMessage `json:"platforms,omitempty"` } var (