Skip to content

About

Build action for fizzy plugins

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

30 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 

Repository files navigation

plugin-build-action

Build action for fizzy plugins.

A reusable GitHub Actions workflow that builds a Fizzy plugin for every supported target, hashes each binary, assembles the author manifest.json, and publishes both as GitHub Release assets.

A Fizzy plugin is a native dylib valid for exactly one (abi_fingerprint, os-arch) pair, so a release ships one binary per target plus a manifest the in-app store reads — indirectly, via the fizzyedit/plugins aggregator, which folds every author's manifest into a static catalog served at https://plugins.fizzyed.it/catalog/. This action automates the build matrix so you don't hand-build, hand-hash, and hand-write the manifest each release.

tag v0.1.0  ──►  build.yml  ──►  per-target dylib + sha256
                     │
                     ▼  assemble_manifest.py (accumulates prior releases)
            release assets: pixi-macos-aarch64.dylib, …, manifest.json
                     │
                     ▼  registry/<id>.json points manifest_url at releases/latest
            fizzyedit/plugins aggregator  ──►  catalog/ (summary.json + per-fingerprint
                                                 releases.json)  ──►  Fizzy store

What it produces

For a tag like v0.1.0, the release gets:

<id>-macos-aarch64.dylib     <id>-macos-x86_64.dylib
<id>-linux-x86_64.so         <id>-linux-aarch64.so
<id>-windows-x86_64.dll      <id>-windows-aarch64.dll
(the web module is **not** a release asset — see below)
manifest.json            ← references the binaries above (url + sha256), accumulating older releases

The web module is a release asset like the rest

All seven binaries are release assets, <id>-web-wasm32.wasm included. There is one publishing story and it is the same on every target — host wherever you like, nothing to enable.

A browser cannot fetch a release asset directly (GitHub sends no access-control-allow-origin on them), but that is a client problem and fizzy solves it client-side: the web app tries the URL directly, and falls back to a CORS proxy on its own origin when the host does not allow it. A plugin published on a host that does send CORS is fetched directly and never touches the proxy. Either way the author publishes one set of assets and this workflow uploads them.

Setup (once per plugin repo)

  1. Pin the Fizzy SDK by URL in your build.zig.zon — not a local path. CI has no sibling checkout, so a .path = "../../fizzy/sdk" dependency fails there. Pin the SDK release asset for an sdk-v* tag (not the git archive of the monorepo — that pulls Velopack):

    .fizzy = .{
        .url = "https://github.com/fizzyedit/fizzy/releases/download/sdk-v0.1.42/fizzy-sdk-v0.1.42.tar.gz",
        .hash = "<zig-package-hash>", // zig fetch --save=fizzy <url>
    },

    On Zig 0.17, delete the tarball zig fetch leaves in the global cache before you build (rm ~/.cache/zig/p/<hash>.tar.gz): the build unpacks it with its top folder kept, and reports a hash mismatch. The build fetches the package itself when it isn't there.

    That pin is the source of truth for fizzy_sdk_version and the ReleaseFast abi_fingerprint — the action reads both from the built dylib. You never copy them into workflow YAML. See fizzy's docs/PLUGINS.md §2.3 / §5.

  2. Add identity-only plugin.zig.zon at the repo root (id / name / version / min_sdk_version). The pushed tag (vX.Y.Z) must equal .version.

  3. Make zig build install the plugin dylib to zig-out/<id>.<ext> — via fizzy.plugin.create + .install (generated dylib root; no author root.zig). If your build installs elsewhere, set artifact-path.

  4. Add the release workflow. Copy examples/release.yml to your plugin repo as .github/workflows/release.yml:

    name: Release
    on:
      push:
        tags: ["v*"]
    jobs:
      build:
        uses: fizzyedit/plugin-build-action/.github/workflows/build.yml@v5
        permissions:
          contents: write
        with:
          zig-version: "0.16.0"
  5. Register once in fizzyedit/plugins — open a PR adding registry/<id>.json with manifest_url pointing at your latest release manifest:

    {
      "id": "pixi",
      "name": "Pixi",
      "description": "Pixel-art editor for Fizzy.",
      "author": "foxnne",
      "homepage": "https://github.com/fizzyedit/pixi",
      "tags": ["editor", "pixel-art"],
      "manifest_url": "https://github.com/fizzyedit/pixi/releases/latest/download/manifest.json"
    }

Releasing

git tag v0.1.0 && git push origin v0.1.0

The workflow builds all targets, publishes the release with the binaries + manifest.json, and the next fizzyedit/plugins aggregation pulls your plugin into the catalog: on merge, its 6-hourly cron, a manual run, or the end of your release when you pass store_dispatch_token. Subsequent releases need no registry PR — just tag again.

Bump the fizzy pin in build.zig.zon when you want a new SDK; the next tag's manifest picks up the new fizzy_sdk_version / abi_fingerprint automatically. The manifest accumulates releases (keyed by version + abi_fingerprint), so users on older SDKs keep matching an older binary instead of seeing "needs a rebuild."

Repinning to a new SDK

repin.yml does that bump for you. Copy examples/sdk-repin.yml to .github/workflows/sdk-repin.yml and turn on Settings → Actions → General → Allow GitHub Actions to create and approve pull requests. For a given SDK (default: the newest sdk-v* release) it:

  1. pins the release asset and its hash in build.zig.zon (scripts/repin_zon.py);
  2. builds the plugin against it, ReleaseFast;
  3. compares the fingerprint the build reports with the one your newest published release records.
Outcome What it does
Fingerprint moved Bumps plugin.zig.zon (.version's patch, unless already past the newest release; .min_sdk_version), pushes sdk/repin-<version>, opens a PR. Merge it, then tag.
Build fails The same PR, as a draft, with the end of the build log: the plugin needs work first. The run fails.
Fingerprint unchanged Nothing: every installed build keeps loading. force: true opens the PR anyway.
Already on that SDK Nothing.

It never tags. fizzy dispatches fizzy-sdk-release to the fizzyedit plugins listed in fizzyedit/plugins when it publishes an SDK; any plugin can run the workflow by hand (workflow_dispatch) or on its own schedule.

Checking pull requests

check.yml is what a plugin's pull requests and pushes to main run. Copy examples/ci.yml to .github/workflows/ci.yml. It runs:

Job What it checks
fmt zig fmt --check over the repo (fmt: false turns it off).
build <os_arch> The plugin cross-compiled from ubuntu-latest, Debug, for targets (default linux-x86_64,macos-aarch64,windows-x86_64), so a break on one OS shows on the PR rather than at the tag. On linux-x86_64 it also runs zig build test when the build declares a test step.
fizzy-main The plugin built against fizzy main's sdk/ (zig build --fork). Never fails the run: it warns that the next SDK release will need a change here, before repin.yml finds out after it. fizzy-main: false turns it off.

Every build sets FIZZY_PROFILE inside the workspace, so the install step stays there. It publishes nothing and needs no scripts from this repo.

Changelog

v8

  • build.yml: optional secret store_dispatch_token; a successful release then starts the store's aggregate.yml so the plugin is listed within minutes. Without it, nothing changes. A token the store rejects fails that last job (the release is already published).
  • examples/release.yml pins @v8 and passes STORE_DISPATCH_TOKEN.

v7

  • check.yml: a third reusable workflow, for pull requests (see Checking pull requests), with examples/ci.yml. build.yml and repin.yml are unchanged.

v6

  • repin.yml: a second reusable workflow that moves a plugin onto a new fizzy SDK and opens a PR when the plugin needs a release (see Repinning to a new SDK), with scripts/repin_zon.py and examples/sdk-repin.yml. build.yml is unchanged; it still checks out its scripts at v5, whose copies are the same.

v4

  • manifest.json now carries top-level name/description/tags, read straight off the caller's plugin.zig.zon (scripts/read_plugin_zon.py) and passed through assemble_manifest.py's new --name/--description/--tags-json args. This lets the fizzyedit/plugins aggregator fall back to these when a registry/<id>.json entry leaves its own description/tags blank, instead of requiring authors to hand-duplicate both.
  • A tag bump (not just an additive change) because callers pin uses: .../build.yml@v3, and this file's own "Resolve plugin-build-action ref" step hardcodes the matching ref="v3" literal for its auxiliary script checkout — neither picks up new script behavior without the ref moving.

v5

  • Build a seventh target, web-wasm32: the wasm side module the fizzy web app fetches and links at runtime. Best-effort (continue-on-error), so it cannot fail a release for a plugin that does not build for the browser.
  • Carry author / author_url from plugin.zig.zon into manifest.json.
  • As in v4, the "Resolve plugin-build-action ref" step's hardcoded literal moves with the tag (now ref="v5"), so a later script change under v5 is actually picked up.
  • The release job's consensus check still requires every target that did build to agree on abi_fingerprint / fizzy_sdk_version — the fingerprint is structural, so a wasm build reports the same value a native one does and adding the target does not disturb it.

v3

  • Derive fizzy_sdk_version + abi_fingerprint from zig-out/sdk-meta.json (emitted by fizzy.plugin.install) — removed as workflow inputs.
  • Cross-compile all 6 targets from ubuntu-latest (plugins are unsigned; no macOS/Windows runners or code signing).
  • Read id / version / min_sdk_version from plugin.zig.zon; release version defaults to the triggering v* tag (must match the zon).
  • Caller release.yml only needs zig-version (optional overrides for id / version / artifact-path / targets remain).

v2

  • Pre-fetch Zig package dependencies with retries before building, working around Zig 0.16 HttpConnectionClosing flakes when fetching GitHub deps (especially on Windows CI).
  • Use a workspace-local ZIG_GLOBAL_CACHE_DIR and pre-create cache tmp/ (fixes cold-cache zip-fetch FileNotFound on CI).

v1

Initial release.

Inputs

Input Required Default Description
zig-version no 0.16.0 Zig toolchain version.
id no plugin.zig.zon .id Override plugin id (must match the zon if set).
version no tag without v Override release version (must match plugin.zig.zon .version).
artifact-path no zig-out/<id> Built dylib path (relative to repo root) without extension.
targets no all 7 Comma-separated os_arch subset to build. Pass the six desktop keys to skip the web build entirely.

Secrets

Secret Required Description
build_env no Environment for every zig build in the workflow, as KEY=VALUE lines. For a plugin whose build bakes in an app credential it cannot commit.
store_dispatch_token no (v8 and later) A fine-grained PAT with actions: write on fizzyedit/plugins. When set, a successful release refreshes the store immediately instead of at its next 6-hourly run.
jobs:
  build:
    uses: fizzyedit/plugin-build-action/.github/workflows/build.yml@v8
    permissions:
      contents: write
    with:
      zig-version: "0.16.0"
    secrets:
      build_env: ${{ secrets.PLUGIN_BUILD_ENV }}
      store_dispatch_token: ${{ secrets.STORE_DISPATCH_TOKEN }}

One blob rather than a named list, so a single plugin's variables never have to be declared in this shared workflow. It is exported before the first zig build — including the pre-fetch — because a build script that generates a file from these variables usually skips the work when the file is already there, and an empty one written during the fetch would otherwise be the one that ships.

store_dispatch_token is a fine-grained PAT with actions: write on fizzyedit/plugins, kept once as an organization secret (STORE_DISPATCH_TOKEN) rather than copied into each plugin repo. A stored GitHub App installation token will not do: it expires within the hour. Only @v8 and later declare the secret; passing it to an older tag fails the release workflow before it starts. After the release is published, a final job runs gh workflow run aggregate.yml --repo fizzyedit/plugins. Without the secret the job only prints a notice, so plugins outside the org release as before. A rejected token (expired, revoked) fails that job, red beside a release that is already published, so the token is renewed rather than the store quietly going back to its schedule. The store keeps one run waiting behind the one running; it starts from main's head, so several releases at once are all taken in, and the earlier waiting runs show as cancelled.

How it works

  • Setup reads plugin.zig.zon, checks the tag/version input against .version, and builds the target matrix.
  • One build per target across the 6 desktop host arches, all cross-compiled with -Dtarget= from ubuntu-latest. Plugins are unsigned pure Zig + vendored C (optional prebuilt .a/.lib deps are selected per target), so there is no macOS/Windows runner or signing step.
  • Plus web-wasm32, best-effort. That job is continue-on-error, so a plugin that cannot build for the browser — C deps, threads, anything the freestanding target has no answer for — loses only its web download, never the release. Its job shows red in the run while the run itself stays green; pass an explicit targets list to stop building it at all. The fizzy web app lists only plugins whose release has a binary for the host it is running on, so a desktop-only plugin is simply absent from the store in the browser, not broken in it.
  • Optimize mode: desktop targets build ReleaseFast; web-wasm32 builds ReleaseSmall, since the browser fetches and compiles the whole module before the plugin loads. Both share fizzy's "fast" safety class, so the abi_fingerprint is the same for every target.
  • Every target collects zig-out/sdk-meta.json (written by the fizzy pin at build time). Publish requires all targets to agree on sdk/fingerprint, then scripts/assemble_manifest.py merges the new release into the prior manifest.json.

Files

Path Role
.github/workflows/build.yml The reusable (workflow_call) build + publish workflow.
.github/workflows/repin.yml The reusable repin workflow: new SDK pin, build, PR if needed.
.github/workflows/check.yml The reusable check workflow for pull requests: fmt, build per OS, tests, fizzy main.
scripts/repin_zon.py Reads and rewrites the fizzy pin, bumps plugin.zig.zon, reads the newest release.
scripts/assemble_manifest.py Merges per-target sha256 fragments into manifest.json.
scripts/read_plugin_zon.py Parses identity from plugin.zig.zon.
scripts/read_plugin_exports.py Optional local helper: dlopen a native plugin and print exports.
examples/release.yml Drop-in caller for a plugin repo.
examples/sdk-repin.yml Drop-in caller for repin.yml.
examples/ci.yml Drop-in caller for check.yml.

About

Build action for fizzy plugins

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages