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
10 changes: 8 additions & 2 deletions .github/workflows/sync.yml
Original file line number Diff line number Diff line change
@@ -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.
#
Expand All @@ -23,6 +28,7 @@ on:
permissions:
contents: write
pull-requests: write
actions: write # starts Publish after a direct (no-review) update

concurrency:
group: sync
Expand Down
5 changes: 3 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand Down
2 changes: 1 addition & 1 deletion CORE_REF
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v0.5.0
v0.6.2
18 changes: 14 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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 `<id>-<X.Y.Z>.tar.gz` (one top folder `<id>/`, unsigned `manifest.json`) and `<id>-<X.Y.Z>.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
`<id>-<X.Y.Z>.tar.gz` (one top folder `<id>/`, unsigned `manifest.json`) and `<id>-<X.Y.Z>.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/<id>.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
Expand Down Expand Up @@ -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):
Expand All @@ -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.
Expand Down
27 changes: 27 additions & 0 deletions scripts/sync.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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" \
Expand Down Expand Up @@ -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"
3 changes: 2 additions & 1 deletion tools/regtool/catalog.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -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,
})
Expand Down
5 changes: 4 additions & 1 deletion tools/regtool/manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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
}
Expand Down
2 changes: 2 additions & 0 deletions tools/regtool/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
Loading