Skip to content
This repository was archived by the owner on Sep 20, 2026. It is now read-only.
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
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/submit-plugin.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ body:
- type: markdown
attributes:
value: |
Your repository must follow the [contract](https://github.com/goblogplatform/plugins/blob/main/docs/CONTRACT.md): `goblog-plugin.json`, `README.md`, the plugin `.go` file, and a release tagged `vX.Y.Z`. A workflow validates it and, if it passes, opens the pull request for you.
Your repository must follow the [contract](https://github.com/goblogplatform/plugins/blob/main/docs/CONTRACT.md): `goblog-plugin.json` with `"runtime": "wasm"`, `README.md`, and a release tagged `vX.Y.Z` with `plugin.wasm` attached. A workflow validates it and, if it passes, opens the pull request for you.
- type: input
id: repo
attributes:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
run: |
go build -o "$RUNNER_TEMP/registry" ./cmd/registry
set +e
"$RUNNER_TEMP/registry" build --out dist --image compscidr/goblog:v0.2.7
"$RUNNER_TEMP/registry" build --out dist --image compscidr/goblog:v0.2.9
code=$?
set -e
if [ "$code" = "2" ]; then echo "skipped=true" >> "$GITHUB_OUTPUT"; exit 0; fi
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/submit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ jobs:
printf ' - repo: %s\n' "$REPO" >> registry.yaml
go build -o "$RUNNER_TEMP/registry" ./cmd/registry
set +e
"$RUNNER_TEMP/registry" build --out "$RUNNER_TEMP/dist" --image compscidr/goblog:v0.2.7 > result.txt 2>&1
"$RUNNER_TEMP/registry" build --out "$RUNNER_TEMP/dist" --image compscidr/goblog:v0.2.9 > result.txt 2>&1
set -e
cat result.txt
if grep -Fxq "$REPO: built" result.txt; then echo "ok=true" >> "$GITHUB_OUTPUT"; else echo "ok=false" >> "$GITHUB_OUTPUT"; fi
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@ jobs:
- name: Validate every registry entry
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: go run ./cmd/registry validate --image compscidr/goblog:v0.2.7
run: go run ./cmd/registry validate --image compscidr/goblog:v0.2.9
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
The curated list of [goblog](https://github.com/goblogplatform/goblog) plugins behind [goblog.live/plugins](https://goblog.live/plugins).

- `registry.yaml` — the list. Submit your repository via the [issue form](.github/ISSUE_TEMPLATE/submit-plugin.yml) (a pull request by hand is the alternative); see [docs/CONTRACT.md](docs/CONTRACT.md).
- `https://goblogplatform.github.io/plugins/index.json` — the machine-readable index (latest release of each plugin, with `download_url` and `sha256`) and `stars` (GitHub stargazers, the directory's default ordering); `plugins/<name>.json` adds the rendered README, changelog and release history.
- `https://goblogplatform.github.io/plugins/index.json` — the machine-readable index: the latest release of each plugin, with `download_url` (the release asset named by the manifest's `entry`, `plugin.wasm` by default) and its `sha256`, `runtime` and `install_type` (both `wasm`), `allowed_hosts` (the network the module may reach; `[]` for none) and `stars` (GitHub stargazers, the directory's default ordering); `plugins/<name>.json` adds the rendered README, changelog and release history.
- `cmd/registry` — the tool CI runs: `validate` on pull requests, `build` on merge and every six hours.

```bash
Expand All @@ -12,7 +12,7 @@ go run ./cmd/registry validate --repo you/plugin # one entry
go run ./cmd/registry build --out dist # what gets published
```

Set `GITHUB_TOKEN` to avoid API rate limits. `validate`/`build` run `goblog validate-plugin` in the `compscidr/goblog` Docker image (`--image` to override; Renovate keeps the default current). With snap-installed Docker, set `TMPDIR` to a directory under your home; snap's Docker cannot bind-mount `/tmp`. Resource limits (`--memory`, `--pids-limit`) need cgroup controllers; on rootless Docker they may be downgraded or rejected — pass `--image` to a local build or run on a rootful daemon.
Set `GITHUB_TOKEN` to avoid API rate limits. `validate`/`build` download each plugin's module — the release asset named by `entry` in its manifest, `plugin.wasm` by default — and run `goblog validate-plugin` on it in the `compscidr/goblog` Docker image (`--image` to override; Renovate keeps the default current). With snap-installed Docker, set `TMPDIR` to a directory under your home; snap's Docker cannot bind-mount `/tmp`. Resource limits (`--memory`, `--pids-limit`) need cgroup controllers; on rootless Docker they may be downgraded or rejected — pass `--image` to a local build or run on a rootful daemon.

## Submissions

Expand Down
2 changes: 1 addition & 1 deletion cmd/registry/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import (
)

const (
defaultImage = "compscidr/goblog:v0.2.7"
defaultImage = "compscidr/goblog:v0.2.9"
defaultBaseURL = "https://goblogplatform.github.io/plugins"
)

Expand Down
14 changes: 10 additions & 4 deletions cmd/registry/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ func (m *memSource) File(_ context.Context, owner, repo, ref, path string) ([]by
}
return nil, registry.ErrNotFound
}
func (m *memSource) ReleaseAsset(_ context.Context, _, _ string, id int64) ([]byte, error) {
if id == 11 {
return []byte("\x00asm hello"), nil
}
return nil, errors.New("no such asset")
}
func (m *memSource) RenderMarkdown(_ context.Context, _, md string) (string, error) {
return "<p>" + md + "</p>", nil
}
Expand All @@ -38,7 +44,7 @@ func (m *memSource) RepoStars(context.Context, string, string) (int, error) { re
type okValidator struct{}

func (okValidator) Validate(_ context.Context, _ []byte) (registry.Info, error) {
return registry.Info{Name: "hello", DisplayName: "Hello", Version: "1.0.0"}, nil
return registry.Info{Name: "hello", DisplayName: "Hello", Version: "1.0.0", Runtime: "wasm"}, nil
}

func fixture(t *testing.T) (string, *memSource) {
Expand All @@ -48,12 +54,12 @@ func fixture(t *testing.T) (string, *memSource) {
os.WriteFile(reg, []byte("plugins:\n - repo: o/hello\n - repo: o/broken\n"), 0644)
src := &memSource{
releases: map[string][]registry.Release{
"o/hello": {{Tag: "v1.0.0", Body: "First", URL: "u", PublishedAt: time.Date(2026, 9, 14, 0, 0, 0, 0, time.UTC)}},
"o/hello": {{Tag: "v1.0.0", Body: "First", URL: "u", PublishedAt: time.Date(2026, 9, 14, 0, 0, 0, 0, time.UTC),
Assets: []registry.Asset{{ID: 11, Name: "plugin.wasm", Size: 11, DownloadURL: "https://github.com/o/hello/releases/download/v1.0.0/plugin.wasm"}}}},
"o/broken": {},
},
files: map[string]string{
"o/hello@v1.0.0:goblog-plugin.json": `{"name":"hello","display_name":"Hello","description":"d","author":"a","license":"MIT","min_goblog_version":"0.2.6"}`,
"o/hello@v1.0.0:plugin.go": "package main\n",
"o/hello@v1.0.0:goblog-plugin.json": `{"name":"hello","display_name":"Hello","description":"d","author":"a","license":"MIT","runtime":"wasm","min_goblog_version":"0.2.6"}`,
"o/hello@v1.0.0:README.md": "# Hello",
},
}
Expand Down
81 changes: 65 additions & 16 deletions docs/CONTRACT.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
# Publishing a goblog plugin

The directory at [goblog.live/plugins](https://goblog.live/plugins) lists plugins from this registry. A plugin is a GitHub repository; each GitHub release is a version. Submitting means adding your repository to `registry.yaml` in a pull request — CI validates it and, once merged, the index is rebuilt (on every merge and every six hours).
The directory at [goblog.live/plugins](https://goblog.live/plugins) lists plugins from this registry. A plugin is a GitHub repository whose releases each carry a compiled WebAssembly module; each GitHub release is a version. Submitting means adding your repository to `registry.yaml` in a pull request — CI validates it and, once merged, the index is rebuilt (on every merge and every six hours).

**Only WebAssembly plugins are accepted.** Yaegi-interpreted `.go` plugins (goblog's [dynamic plugins](https://github.com/goblogplatform/goblog#dynamic-plugins)) still work when an operator drops the file in by hand, but the directory does not list or install them: a `.wasm` module can bundle any dependency its author likes, runs with no filesystem and no network beyond the hosts it declares, and needs no goblog rebuild.

## What the directory publishes

The index entry for your plugin is built from the manifest, the latest release, and your repository's GitHub star count (`stars`), which the directory uses for its default ordering. Stars are best-effort: if GitHub cannot be reached for them, the entry is published with `0`.
The index entry for your plugin is built from the manifest, the latest release and its module asset (the one named by `entry`, `plugin.wasm` by default) (`download_url` is the asset's browser URL; `sha256` is of the asset), and your repository's GitHub star count (`stars`), which the directory uses for its default ordering. Stars are best-effort: if GitHub cannot be reached for them, the entry is published with `0`. The entry also carries `runtime: "wasm"`, `install_type: "wasm"` and the manifest's `allowed_hosts`, which goblog's admin page shows as "Talks to: …" before an operator installs.

## What the repository must contain

Expand All @@ -13,10 +15,12 @@ At the root of the repository, at the release tag being published (the tool chec
| File | Required | Notes |
|---|---|---|
| `goblog-plugin.json` | yes | the manifest, below |
| the entry file (default `plugin.go`) | yes | a [dynamic plugin](https://github.com/goblogplatform/goblog#dynamic-plugins): `package main`, `func NewPlugin() plugin.Plugin` |
| the plugin's source | yes | anything that builds the module — Go with [`github.com/extism/go-pdk`](https://github.com/extism/go-pdk), TinyGo, Rust, or any language with an [Extism PDK](https://extism.org/docs/concepts/pdk) |
| `README.md` | yes | shown on the plugin's directory page |
| `CHANGELOG.md` | no | shown when present |
| `LICENSE` | recommended | should match `license` in the manifest |
| `LICENSE` | recommended | not checked by the validator; state the same license as `license` in the manifest |

And attached to every release: the compiled module, named as `entry` in the manifest (default `plugin.wasm`). The module is a release **asset**, not a file in the repository.

### `goblog-plugin.json`

Expand All @@ -27,35 +31,80 @@ At the root of the repository, at the release tag being published (the tool chec
"description": "One sentence shown in the listing.",
"author": "Your Name",
"license": "Apache-2.0",
"entry": "plugin.go",
"min_goblog_version": "0.2.6",
"runtime": "wasm",
"entry": "plugin.wasm",
"allowed_hosts": ["api.example.com"],
"min_goblog_version": "0.2.9",
"homepage": "https://example.com/optional"
}
```

- `name`: `^[a-z0-9-]+$`, unique across the registry, and equal to what your plugin's `Name()` returns.
- `display_name`: the label shown in the directory. It does not have to equal your plugin's `DisplayName()`, which labels its settings group in the admin UI.
- `name`: `^[a-z0-9-]+$`, unique across the registry, and equal to the `name` your plugin's `identity` export returns. It keys the plugin's settings and its persistent store, so keep it stable across versions.
- `display_name`: the label shown in the directory. It does not have to equal your plugin's `display_name`, which labels its settings group in the admin UI.
- `license`: an SPDX identifier from the list in `internal/registry/manifest.go` (MIT, Apache-2.0, BSD-2/3-Clause, ISC, MPL-2.0, GPL/LGPL/AGPL `-only`/`-or-later`, Unlicense, 0BSD). Open an issue to add another.
- `entry`: a `.go` file at the repository root; defaults to `plugin.go`.
- `min_goblog_version`: plain semver (`0.2.6`, no `v`) — the oldest goblog your plugin works with.
- `runtime`: must be `"wasm"`. Anything else is rejected.
- `entry`: the name of the `.wasm` asset attached to each release (letters, digits, `_`, `.`, `-`; no path); defaults to `plugin.wasm`.
- `allowed_hosts`: the hosts the module may reach over HTTP — exact hostnames (`api.example.com`), IPs, or globs (`*.example.com`), each optionally with a port; never a scheme or a path. A glob must still name a domain — `*` alone (or `**`, `*.*`) is rejected. Omit it, or leave it empty, and the plugin gets no network at all. goblog checks every request (and every redirect hop) against this list, and the directory shows it to operators as "Talks to" before they install, so declare only what you use.
- `min_goblog_version`: plain semver (`0.2.9`, no `v`) — the oldest goblog your plugin works with. WebAssembly plugins need at least `0.2.9`.

### The module

A plugin is one `.wasm` file built for [Extism](https://extism.org/): every export takes and returns JSON through Extism's input/output. Only `identity` is mandatory; the others (`settings`, `pages`, `jobs`, `template_head`, `template_footer`, `template_data`, `render_page`, `run_job`, `on_init`) are optional and mirror goblog's compiled-in plugin interface. Host functions give you a per-plugin key/value store (`store_get`/`store_set`/`store_delete`/`store_list`), logging through the PDK's logger, and Extism's `http_request` limited to `allowed_hosts`. Calls are capped at 10 s (120 s for jobs and `on_init`) and 64 MB of memory. The full contract — every export's input and output shape, `ctx`, the host functions and the limits — is in goblog's README under [WebAssembly plugins](https://github.com/goblogplatform/goblog#webassembly-plugins), and [`plugin/wasm/testdata/echo/main.go`](https://github.com/goblogplatform/goblog/blob/main/plugin/wasm/testdata/echo/main.go) implements all of it. [goblog-plugin-hello](https://github.com/goblogplatform/goblog-plugin-hello) is the smallest complete example and is meant to be copied.

With the standard Go toolchain (1.24 or newer):

```bash
GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -ldflags="-s -w" -o plugin.wasm .
```

The module must be 16 MiB or smaller; `-ldflags="-s -w"` keeps a Go build well under that.

### Releases

- Tag releases `vX.Y.Z` (exactly three numbers). Drafts and pre-releases are ignored.
- The tag without `v` must equal the string your plugin's `Version()` returns.
- The tag without `v` must equal the `version` your plugin's `identity` export returns.
- **Every release must have the module attached** as the asset named by `entry`. The registry validates and publishes the asset, never a file from the repository, so a release without it fails validation with `has no asset named plugin.wasm`.
- The GitHub release body is shown as the version's release notes.
- The directory lists the **latest** published release; the detail page shows all of them.

Copy this workflow into `.github/workflows/release.yml` and the asset is built and uploaded whenever you publish a release:

```yaml
name: Release
on:
release:
types: [published]
permissions:
contents: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version-file: go.mod
- name: Build plugin.wasm
run: GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -ldflags="-s -w" -o plugin.wasm .
- name: Upload to the release
env:
GH_TOKEN: ${{ github.token }}
run: gh release upload "${{ github.event.release.tag_name }}" plugin.wasm --clobber
```

The registry reads the asset when it validates; if the workflow is still uploading when it looks, re-run validation once the asset is up (edit your submission issue, or re-run the pull request's checks).

## Check before you submit

```bash
GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -ldflags="-s -w" -o plugin.wasm .
docker run --rm --network none -v "$PWD:/p:ro" \
--entrypoint /go/src/github.com/compscidr/goblog/goblog compscidr/goblog:v0.2.7 \
validate-plugin /p/plugin.go
# {"name":"hello","display_name":"Hello","version":"1.0.0"}
--entrypoint /go/src/github.com/compscidr/goblog/goblog compscidr/goblog:v0.2.9 \
validate-plugin /p/plugin.wasm
# {"name":"hello","display_name":"Hello","version":"1.0.0","runtime":"wasm"}
```

The registry's CI runs this (plus a timeout and memory/process limits), then compares `name` and `version` with your manifest and tag. Your file is executed by the Go interpreter during the check, which is why it runs with networking off.
The registry's CI runs this (plus a timeout and memory/process limits) against the asset on your latest release, then requires `"runtime":"wasm"` and compares `name` and `version` with your manifest and tag. `validate-plugin` loads the module with no store and no network and calls `identity`, `settings`, `pages` and `jobs`, so those exports must not depend on either.

## Submit

Expand All @@ -70,4 +119,4 @@ plugins:
- repo: you/goblog-plugin-yours
```

Plugins run inside the goblog process of whoever installs them. Keep them small and readable; the registry is curated and maintainers may decline or remove entries.
Plugins run inside the goblog process of whoever installs them, sandboxed but trusted with the hosts they declare and the settings they are given. Keep them small and readable; the registry is curated and maintainers may decline or remove entries.
36 changes: 20 additions & 16 deletions internal/registry/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,22 @@ import (
// Field names are the contract consumed by goblog's directory plugin and
// the admin installer; do not rename them.
type IndexEntry struct {
Name string `json:"name"`
DisplayName string `json:"display_name"`
Description string `json:"description"`
Version string `json:"version"`
Author string `json:"author"`
License string `json:"license"`
SourceURL string `json:"source_url"`
DownloadURL string `json:"download_url"`
SHA256 string `json:"sha256"`
MinGoblogVersion string `json:"min_goblog_version"`
InstallType string `json:"install_type"`
ReleasedAt string `json:"released_at"`
DetailURL string `json:"detail_url"`
Stars int `json:"stars"`
Name string `json:"name"`
DisplayName string `json:"display_name"`
Description string `json:"description"`
Version string `json:"version"`
Author string `json:"author"`
License string `json:"license"`
SourceURL string `json:"source_url"`
DownloadURL string `json:"download_url"`
SHA256 string `json:"sha256"`
MinGoblogVersion string `json:"min_goblog_version"`
InstallType string `json:"install_type"` // "wasm"
Runtime string `json:"runtime"` // "wasm"
AllowedHosts []string `json:"allowed_hosts"` // never null: [] when the plugin uses no network
ReleasedAt string `json:"released_at"`
DetailURL string `json:"detail_url"`
Stars int `json:"stars"`
}

// ReleaseDoc is one release in a plugin's history.
Expand Down Expand Up @@ -135,10 +137,12 @@ func buildDetail(ctx context.Context, src Source, v *Validated, baseURL string)
Author: v.Manifest.Author,
License: v.Manifest.License,
SourceURL: "https://github.com/" + ownerRepo,
DownloadURL: fmt.Sprintf("https://raw.githubusercontent.com/%s/%s/%s", ownerRepo, v.Release.Tag, v.Manifest.Entry),
DownloadURL: v.Asset.DownloadURL,
SHA256: v.SHA256,
MinGoblogVersion: v.Manifest.MinGoblogVersion,
InstallType: "dynamic",
InstallType: "wasm",
Runtime: "wasm",
AllowedHosts: v.Manifest.AllowedHosts,
ReleasedAt: v.Release.PublishedAt.UTC().Format(time.RFC3339),
DetailURL: fmt.Sprintf("%s/plugins/%s.json", baseURL, v.Manifest.Name),
}
Expand Down
Loading
Loading