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
21 changes: 20 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ jobs:
with:
node-version: 22

# Nothing in the repository ships an icon, so without this the icon rules
# would only ever run against a contributor's plugin.
- name: Test the registry tooling
run: node --test scripts/lib/*.test.mjs

# Runs the same manifest validator Studio uses at install time, so a
# plugin that passes CI is one Studio will accept.
- name: Validate manifests
Expand Down Expand Up @@ -65,7 +70,21 @@ jobs:
working-directory: plugins/${{ matrix.plugin }}
run: yarn typecheck

# Also proves the built output matches what manifest.json declares.
# Opt-in: a plugin with no "test" script is not a plugin with a failing
# one. `yarn run --top-level` is not it — this asks package.json directly.
- name: Test
working-directory: plugins/${{ matrix.plugin }}
run: |
if node -e "process.exit(require('./package.json').scripts?.test ? 0 : 1)"; then
yarn test
else
echo "no test script; skipping"
fi

# Also proves the built output matches what manifest.json declares. This
# runs on Linux with no Rust toolchain, so a sidecar plugin packages here
# in its mirror-only shape — which is exactly the shape worth checking on
# every push, because it is the one that must never break.
- name: Build and package
run: node scripts/package-plugin.mjs ${{ matrix.plugin }}

Expand Down
133 changes: 120 additions & 13 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,19 @@ permissions:
contents: write

jobs:
release:
name: Publish ${{ github.ref_name }}
prepare:
name: Check ${{ github.ref_name }}
runs-on: ubuntu-latest
outputs:
id: ${{ steps.tag.outputs.id }}
version: ${{ steps.tag.outputs.version }}
asset: ${{ steps.tag.outputs.asset }}
has-sidecar: ${{ steps.sidecar.outputs.has-sidecar }}
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version: 22
- run: corepack enable

- name: Parse tag
id: tag
Expand All @@ -41,7 +45,8 @@ jobs:
TAG: ${{ github.ref_name }}

# The tag must agree with what is committed, otherwise the published
# artifact would not match the source anyone can read at that tag.
# artifact would not match the source anyone can read at that tag. Checked
# before anything is built, so a bad tag costs one runner and not four.
- name: Verify tag matches committed version
run: |
node -e "
Expand Down Expand Up @@ -77,28 +82,130 @@ jobs:
- name: Validate
run: node scripts/validate.mjs ${{ steps.tag.outputs.id }}

# A plugin ships a native sidecar if it has a builder for one. Plugins
# without it skip the whole matrix below rather than spinning up three
# runners to do nothing.
- name: Detect sidecar
id: sidecar
run: |
if [ -f "plugins/${{ steps.tag.outputs.id }}/sidecar/build.mjs" ]; then
echo "has-sidecar=true" >> "$GITHUB_OUTPUT"
else
echo "has-sidecar=false" >> "$GITHUB_OUTPUT"
fi

# One runner per platform, because a native binary can only honestly be built
# on the platform it targets: cross-building loses the executable bit and, on
# macOS, the second architecture.
sidecar:
name: Sidecar ${{ matrix.platform-key }}
needs: prepare
if: needs.prepare.outputs.has-sidecar == 'true'
runs-on: ${{ matrix.os }}
strategy:
fail-fast: true
matrix:
include:
- os: windows-latest
platform-key: windows-x64
- os: macos-latest
platform-key: macos-universal
# macos-latest is Apple silicon; the Intel slice needs adding before
# sidecar/build.mjs can lipo the two together.
extra-targets: x86_64-apple-darwin
- os: ubuntu-latest
platform-key: linux-x64
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version: 22

- name: Install Rust
run: rustup update stable --no-self-update && rustup default stable

- name: Add extra targets
if: matrix.extra-targets != ''
run: rustup target add ${{ matrix.extra-targets }}

- uses: Swatinem/rust-cache@v2
with:
workspaces: plugins/${{ needs.prepare.outputs.id }}/sidecar

# Invoked directly rather than through `yarn build:sidecar`: this needs
# cargo and Node, not the plugin's JavaScript dependencies, and installing
# them on three runners buys nothing.
- name: Build
working-directory: plugins/${{ needs.prepare.outputs.id }}
run: node sidecar/build.mjs

- uses: actions/upload-artifact@v5
with:
name: sidecar-${{ matrix.platform-key }}
path: plugins/${{ needs.prepare.outputs.id }}/bin
if-no-files-found: error

release:
name: Publish ${{ github.ref_name }}
needs: [prepare, sidecar]
# `always()` so a plugin with no sidecar (matrix skipped) still publishes,
# while a matrix that actually failed still stops the release.
if: always() && needs.prepare.result == 'success' && (needs.sidecar.result == 'success' || needs.sidecar.result == 'skipped')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version: 22
- run: corepack enable

# Every platform's binaries land back under bin/, where the plugin's
# build.mjs looks for them. Packaging on Linux is safe even for the macOS
# image: no zip writer records POSIX modes, so Studio repairs the
# executable bit when it spawns a sidecar (sidecarHost.ensureExecutable).
- name: Collect sidecar binaries
if: needs.prepare.outputs.has-sidecar == 'true'
uses: actions/download-artifact@v5
with:
pattern: sidecar-*
merge-multiple: true
path: plugins/${{ needs.prepare.outputs.id }}/bin

- name: Show what will ship
if: needs.prepare.outputs.has-sidecar == 'true'
run: find plugins/${{ needs.prepare.outputs.id }}/bin -type f -exec sha256sum {} +

- name: Install (immutable)
working-directory: plugins/${{ steps.tag.outputs.id }}
working-directory: plugins/${{ needs.prepare.outputs.id }}
run: yarn install --immutable

- name: Typecheck
working-directory: plugins/${{ steps.tag.outputs.id }}
working-directory: plugins/${{ needs.prepare.outputs.id }}
run: yarn typecheck

- name: Test
working-directory: plugins/${{ needs.prepare.outputs.id }}
run: |
if node -e "process.exit(require('./package.json').scripts?.test ? 0 : 1)"; then
yarn test
else
echo "no test script; skipping"
fi

- name: Build and package
run: node scripts/package-plugin.mjs ${{ steps.tag.outputs.id }}
run: node scripts/package-plugin.mjs ${{ needs.prepare.outputs.id }}

- name: Publish release
uses: softprops/action-gh-release@v2
with:
name: ${{ steps.tag.outputs.id }} ${{ steps.tag.outputs.version }}
files: .out/${{ steps.tag.outputs.asset }}
name: ${{ needs.prepare.outputs.id }} ${{ needs.prepare.outputs.version }}
files: .out/${{ needs.prepare.outputs.asset }}
fail_on_unmatched_files: true
body: |
**${{ steps.tag.outputs.id }}** v${{ steps.tag.outputs.version }}
**${{ needs.prepare.outputs.id }}** v${{ needs.prepare.outputs.version }}

Download `${{ steps.tag.outputs.asset }}`, unzip it, then in NarraLeaf Studio open
Download `${{ needs.prepare.outputs.asset }}`, unzip it, then in NarraLeaf Studio open
**Launcher → Plugins → Install from folder** and select the unzipped
`${{ steps.tag.outputs.id }}` folder.
`${{ needs.prepare.outputs.id }}` folder.

Source: [`plugins/${{ steps.tag.outputs.id }}`](https://github.com/NarraLeaf/Plugins/tree/${{ github.ref_name }}/plugins/${{ steps.tag.outputs.id }})
Source: [`plugins/${{ needs.prepare.outputs.id }}`](https://github.com/NarraLeaf/Plugins/tree/${{ github.ref_name }}/plugins/${{ needs.prepare.outputs.id }})
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,9 @@ Thumbs.db

# Local packaging output from scripts/package-plugin.mjs
.out/

# Rust sidecars: `target/` is cargo's scratch, `bin/` holds the compiled
# artifacts a build drops for build.mjs to pick up. Neither is source — a
# release builds them on each platform's own runner (see .github/workflows).
target/
plugins/*/bin/
24 changes: 21 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,17 @@ A pull request is mergeable when:
- **Permissions are minimal.** Every entry in `permissions` needs a reason in
the pull request description. Filesystem and API permissions get the most
scrutiny — plugins are not sandboxed, so an approved permission is real trust.
- **An icon, if you ship one, is square.** `manifest.json` may declare
`"icon": "icon.png"` — a package-relative path to the thumbnail Studio shows
beside your plugin in the Launcher list, installed and store alike. It must be
`.png`, `.webp`, `.jpg` or `.jpeg` (no SVG — it is a document that can carry
script; no GIF — an animating row is not yours to impose), square, between
64x64 and 512x512, and at most 512 KB. Your build script has to copy it into
`dist/` the way it copies `manifest.json`; the template's already does.
`scripts/validate.mjs` checks every rule, and Studio refuses to install a
package whose icon is missing or out of bounds. Declaring one is optional:
a plugin without an icon gets the monogram tile Studio draws from its name,
which is a perfectly good place to stay.
- **Host modules stay external.** Never bundle `react`, `react-dom`, or
`narraleaf-studio/*`. The host supplies them through an import map; bundling
React produces a second, broken instance. The template's `build.mjs` already
Expand Down Expand Up @@ -83,6 +94,13 @@ if the tag, `manifest.json`, and `index.json` disagree. See
## Keeping the validator honest

`scripts/lib/plugins.mjs` contains a port of Studio's manifest validator
(`src/shared/utils/pluginManifest.ts`). If Studio's validation rules change,
update the port in the same change — otherwise CI accepts manifests that Studio
rejects at install, which is the worst possible failure mode for a registry.
(`src/shared/utils/pluginManifest.ts`), and `scripts/lib/image.mjs` a port of its
icon rules (`src/shared/constants/pluginIcon.ts` plus
`src/shared/utils/{pluginIcon,imageDimensions}.ts`). If Studio's validation rules
change, update the ports in the same change — otherwise CI accepts manifests that
Studio rejects at install, which is the worst possible failure mode for a
registry.

Run the tooling's own tests with `node --test scripts/lib/*.test.mjs`. They carry
the icon rules in particular, because no plugin here ships an icon and the code
would otherwise never execute until a contributor's did.
55 changes: 55 additions & 0 deletions index.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,61 @@
"download": "https://github.com/NarraLeaf/Plugins/releases/download/helloyork.nekolang-i18n%401.1.0/helloyork.nekolang-i18n-1.1.0.zip"
},
"studioVersion": ">=0.0.1"
},
{
"id": "narraleaf.steam-achievements",
"name": "Steam Achievements",
"version": "0.1.0",
"description": "Author Steam achievements and stats in Studio, and unlock them from blueprint graphs. Falls back to a local mirror wherever Steam is not available, so the same script works on itch, on the web export and in Dev Mode.",
"publisher": "NarraLeaf Studio",
"path": "plugins/narraleaf.steam-achievements",
"targets": [
"studio",
"runtime"
],
"categories": [
"integration",
"blueprint"
],
"keywords": [
"narraleaf",
"narraleaf-studio-plugin",
"steam",
"steamworks",
"achievements",
"stats",
"sidecar"
],
"license": "MPL-2.0",
"contributes": {
"blueprintNodes": [
"narraleaf.steam-achievements.unlock",
"narraleaf.steam-achievements.isUnlocked",
"narraleaf.steam-achievements.indicateProgress",
"narraleaf.steam-achievements.setStat",
"narraleaf.steam-achievements.addStat",
"narraleaf.steam-achievements.getStat",
"narraleaf.steam-achievements.available",
"narraleaf.steam-achievements.language",
"narraleaf.steam-achievements.resetAll"
],
"widgets": [],
"locales": []
},
"permissions": [],
"release": {
"tag": "narraleaf.steam-achievements@0.1.0",
"page": "https://github.com/NarraLeaf/Plugins/releases/tag/narraleaf.steam-achievements%400.1.0",
"download": "https://github.com/NarraLeaf/Plugins/releases/download/narraleaf.steam-achievements%400.1.0/narraleaf.steam-achievements-0.1.0.zip"
},
"icon": "https://raw.githubusercontent.com/NarraLeaf/Plugins/narraleaf.steam-achievements%400.1.0/plugins/narraleaf.steam-achievements/icon.png",
"studioVersion": ">=0.2.0",
"locales": {
"zh-CN": {
"name": "Steam 成就",
"description": "在 Studio 里编写 Steam 成就与统计量,并用蓝图节点解锁。Steam 不可用时写入本地镜像,itch 版、web 版与 Dev Mode 下同一套脚本照常工作。"
}
}
}
]
}
Loading
Loading