From 2a4eef972a82a1bab87731c484eb28b6b0052db3 Mon Sep 17 00:00:00 2001 From: David Northover Date: Wed, 1 Jul 2026 14:25:42 -0400 Subject: [PATCH 1/2] Publish preinstallable neuroglancer artifacts --- .github/workflows/build-and-release.yml | 37 +++++++++++++++++++------ README.md | 22 +++++++++++++-- package.json | 1 + scripts/write-release-manifest.mjs | 21 ++++++++++++++ 4 files changed, 71 insertions(+), 10 deletions(-) create mode 100644 scripts/write-release-manifest.mjs diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index 0998b97..abcc595 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -1,6 +1,15 @@ name: Build and Release Plugin on: + pull_request: + paths: + - .github/workflows/build-and-release.yml + - package.json + - scripts/** + - vite.config.ts + - src/** + - backend/** + - public/** push: tags: - v*.*.* @@ -11,7 +20,7 @@ env: jobs: - release: + plugin-artifact: runs-on: ubuntu-latest permissions: @@ -19,28 +28,40 @@ jobs: steps: - name: Check out Git repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 20 - name: Install Dependencies run: npm install - - name: Build - run: npm run build + - name: Build release layout + run: npm run build:release + + - name: Set artifact name + run: | + safe_ref="${GITHUB_REF_NAME//\//-}" + echo "PLUGIN_ARTIFACT=neuroglancer-plugin-${safe_ref}.zip" >> "$GITHUB_ENV" - name: Archive Release uses: thedoctor0/zip-release@0.7.5 with: type: 'zip' - filename: 'release.zip' + filename: ${{ env.PLUGIN_ARTIFACT }} directory: 'dist' + - name: Upload artifact for PR validation + uses: actions/upload-artifact@v4 + with: + name: ${{ env.PLUGIN_ARTIFACT }} + path: dist/${{ env.PLUGIN_ARTIFACT }} + - name: Upload Release + if: startsWith(github.ref, 'refs/tags/') uses: ncipollo/release-action@v1.12.0 with: - artifacts: "dist/release.zip" - token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + artifacts: dist/${{ env.PLUGIN_ARTIFACT }} + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md index 1d9176e..be3ba61 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ This plugin integrates a Neuroglancer interface into the [Ouroboros](https://github.com/We-Gold/ouroboros) medical imaging software. -To install it, open Ouroboros, open the Plugin Manager (`Ouroboros > Manage Plugins`), and add a plugin from the GitHub releases url: `https://github.com/We-Gold/neuroglancer-plugin/releases`. +To install it, open Ouroboros, open the Plugin Manager (`Ouroboros > Manage Plugins`), and add a plugin from the GitHub releases url: `https://github.com/ChengLabResearch/neuroglancer-plugin/releases`. Then restart the app and the plugin should be visible. @@ -19,4 +19,22 @@ Make sure Docker is installed and running. 1. `npm install` 2. `npm run dev` -To test in Ouroboros, run Ouroboros in development mode as well, and use the `Test Plugin` page to view the plugin. \ No newline at end of file +To test in Ouroboros, run Ouroboros in development mode as well, and use the `Test Plugin` page to view the plugin. + +### Release artifact + +Tagged releases publish `neuroglancer-plugin-.zip`. The archive root is the +same layout that Ouroboros expects inside its plugin folder: + +- `package.json` +- `index.html` +- `ngrefactor.html` +- `icon.svg` +- `compose.yml` +- frontend assets and runtime files +- `plugin-release.json` + +For production package preinstalls, unpack the archive to +`extra-resources/preinstalled-plugins/neuroglancer-plugin/` before building the +Ouroboros app package. On first launch, Ouroboros copies that folder into the +normal user-data plugin directory and detects it from `package.json`. diff --git a/package.json b/package.json index c2d8233..8b94198 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "dev-backend": "node backend-dev.cjs", "dev": "concurrently \"npm run dev-frontend\" \"npm run dev-backend\"", "build": "tsc -b && vite build", + "build:release": "npm run build && node scripts/write-release-manifest.mjs", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "preview": "vite preview" }, diff --git a/scripts/write-release-manifest.mjs b/scripts/write-release-manifest.mjs new file mode 100644 index 0000000..7a00704 --- /dev/null +++ b/scripts/write-release-manifest.mjs @@ -0,0 +1,21 @@ +import { mkdir, readFile, writeFile } from 'node:fs/promises' +import { join } from 'node:path' + +const root = process.cwd() +const dist = join(root, 'dist') +const packageJson = JSON.parse(await readFile(join(root, 'package.json'), 'utf8')) + +const manifest = { + name: packageJson.name, + pluginName: packageJson.pluginName, + version: packageJson.version, + index: packageJson.index, + icon: packageJson.icon, + dockerCompose: packageJson.dockerCompose, + artifactName: `${packageJson.name}-${packageJson.version}.zip`, + commit: process.env.GITHUB_SHA ?? null, + ref: process.env.GITHUB_REF_NAME ?? null +} + +await mkdir(dist, { recursive: true }) +await writeFile(join(dist, 'plugin-release.json'), `${JSON.stringify(manifest, null, 2)}\n`) From 334b6952aeb166e2d53221005416069a5f86ede5 Mon Sep 17 00:00:00 2001 From: David Northover Date: Thu, 2 Jul 2026 16:30:23 -0400 Subject: [PATCH 2/2] Finalize release packaging metadata --- .github/workflows/build-and-release.yml | 13 +++++-- README.md | 24 +++++++++++- package.json | 3 +- scripts/validate-release-manifest.mjs | 50 +++++++++++++++++++++++++ scripts/write-release-manifest.mjs | 23 +++++++++++- 5 files changed, 104 insertions(+), 9 deletions(-) create mode 100644 scripts/validate-release-manifest.mjs diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index abcc595..a1fdb60 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -38,13 +38,18 @@ jobs: - name: Install Dependencies run: npm install - - name: Build release layout - run: npm run build:release - - - name: Set artifact name + - name: Set release metadata run: | safe_ref="${GITHUB_REF_NAME//\//-}" echo "PLUGIN_ARTIFACT=neuroglancer-plugin-${safe_ref}.zip" >> "$GITHUB_ENV" + echo "OUROBOROS_PLUGIN_RELEASE_TAG=${GITHUB_REF_NAME}" >> "$GITHUB_ENV" + echo "OUROBOROS_PLUGIN_ARTIFACT=neuroglancer-plugin-${safe_ref}.zip" >> "$GITHUB_ENV" + + - name: Build release layout + run: npm run build:release + + - name: Validate release layout + run: npm run validate:release - name: Archive Release uses: thedoctor0/zip-release@0.7.5 diff --git a/README.md b/README.md index be3ba61..956be25 100644 --- a/README.md +++ b/README.md @@ -23,8 +23,25 @@ To test in Ouroboros, run Ouroboros in development mode as well, and use the `Te ### Release artifact -Tagged releases publish `neuroglancer-plugin-.zip`. The archive root is the -same layout that Ouroboros expects inside its plugin folder: +The current production pin for Ouroboros package builds is: + +- tag: `v1.0.0` +- release asset: `release.zip` + +That asset predates the named-artifact release workflow, but its archive root is +still the same layout that Ouroboros expects inside its plugin folder: + +- `package.json` +- `index.html` +- `icon.svg` +- `compose.yml` +- frontend assets and backend runtime files + +The legacy `v1.0.0` asset does not include `plugin-release.json`; Ouroboros +production package metadata should therefore record the pinned release tag and +asset name directly. + +New tagged releases publish `neuroglancer-plugin-.zip` with: - `package.json` - `index.html` @@ -34,6 +51,9 @@ same layout that Ouroboros expects inside its plugin folder: - frontend assets and runtime files - `plugin-release.json` +The `plugin-release.json` metadata records the release tag, artifact name, +commit, and packaged plugin version. + For production package preinstalls, unpack the archive to `extra-resources/preinstalled-plugins/neuroglancer-plugin/` before building the Ouroboros app package. On first launch, Ouroboros copies that folder into the diff --git a/package.json b/package.json index 8b94198..9a68872 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "index": "./index.html", "dockerCompose": "./compose.yml", "private": true, - "version": "0.0.0", + "version": "1.0.0", "type": "module", "main": "dist/main.js", "files": [ @@ -17,6 +17,7 @@ "dev": "concurrently \"npm run dev-frontend\" \"npm run dev-backend\"", "build": "tsc -b && vite build", "build:release": "npm run build && node scripts/write-release-manifest.mjs", + "validate:release": "node scripts/validate-release-manifest.mjs", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "preview": "vite preview" }, diff --git a/scripts/validate-release-manifest.mjs b/scripts/validate-release-manifest.mjs new file mode 100644 index 0000000..20ad371 --- /dev/null +++ b/scripts/validate-release-manifest.mjs @@ -0,0 +1,50 @@ +import { readFile } from 'node:fs/promises' +import { existsSync } from 'node:fs' +import { join } from 'node:path' + +const root = process.cwd() +const dist = join(root, 'dist') +const packageJson = JSON.parse(await readFile(join(root, 'package.json'), 'utf8')) +const releaseTag = process.env.OUROBOROS_PLUGIN_RELEASE_TAG ?? process.env.GITHUB_REF_NAME ?? null +const releaseVersion = process.env.OUROBOROS_PLUGIN_RELEASE_VERSION ?? versionFromReleaseTag(releaseTag) ?? packageJson.version +const artifactName = process.env.OUROBOROS_PLUGIN_ARTIFACT ?? artifactNameForRelease(releaseTag) + +assert(existsSync(join(dist, 'package.json')), 'release layout is missing package.json') +assert(existsSync(join(dist, packageJson.index)), `release layout is missing ${packageJson.index}`) +assert(existsSync(join(dist, packageJson.icon)), `release layout is missing ${packageJson.icon}`) +assert( + existsSync(join(dist, packageJson.dockerCompose)), + `release layout is missing ${packageJson.dockerCompose}` +) + +const manifest = JSON.parse(await readFile(join(dist, 'plugin-release.json'), 'utf8')) + +assert(manifest.name === packageJson.name, 'manifest name mismatch') +assert(manifest.pluginName === packageJson.pluginName, 'manifest pluginName mismatch') +assert(manifest.version === releaseVersion, 'manifest release version mismatch') +assert(manifest.packageVersion === packageJson.version, 'manifest package version mismatch') +assert(manifest.releaseTag === releaseTag, 'manifest release tag mismatch') +assert(manifest.artifactName === artifactName, 'manifest artifact name mismatch') +assert(manifest.index === packageJson.index, 'manifest index mismatch') +assert(manifest.icon === packageJson.icon, 'manifest icon mismatch') +assert(manifest.dockerCompose === packageJson.dockerCompose, 'manifest compose path mismatch') + +console.log(`Validated Neuroglancer release layout for ${releaseTag ?? packageJson.version}.`) + +function artifactNameForRelease(tag) { + const suffix = tag ? safeFilePart(tag) : packageJson.version + return `${packageJson.name}-${suffix}.zip` +} + +function versionFromReleaseTag(tag) { + if (!tag?.startsWith('v')) return null + return tag.slice(1) +} + +function safeFilePart(value) { + return value.replaceAll('/', '-') +} + +function assert(condition, message) { + if (!condition) throw new Error(message) +} diff --git a/scripts/write-release-manifest.mjs b/scripts/write-release-manifest.mjs index 7a00704..161dc00 100644 --- a/scripts/write-release-manifest.mjs +++ b/scripts/write-release-manifest.mjs @@ -4,18 +4,37 @@ import { join } from 'node:path' const root = process.cwd() const dist = join(root, 'dist') const packageJson = JSON.parse(await readFile(join(root, 'package.json'), 'utf8')) +const releaseTag = process.env.OUROBOROS_PLUGIN_RELEASE_TAG ?? process.env.GITHUB_REF_NAME ?? null +const releaseVersion = process.env.OUROBOROS_PLUGIN_RELEASE_VERSION ?? versionFromReleaseTag(releaseTag) ?? packageJson.version +const artifactName = process.env.OUROBOROS_PLUGIN_ARTIFACT ?? artifactNameForRelease(releaseTag) const manifest = { name: packageJson.name, pluginName: packageJson.pluginName, - version: packageJson.version, + version: releaseVersion, + packageVersion: packageJson.version, index: packageJson.index, icon: packageJson.icon, dockerCompose: packageJson.dockerCompose, - artifactName: `${packageJson.name}-${packageJson.version}.zip`, + releaseTag, + artifactName, commit: process.env.GITHUB_SHA ?? null, ref: process.env.GITHUB_REF_NAME ?? null } await mkdir(dist, { recursive: true }) await writeFile(join(dist, 'plugin-release.json'), `${JSON.stringify(manifest, null, 2)}\n`) + +function artifactNameForRelease(tag) { + const suffix = tag ? safeFilePart(tag) : packageJson.version + return `${packageJson.name}-${suffix}.zip` +} + +function versionFromReleaseTag(tag) { + if (!tag?.startsWith('v')) return null + return tag.slice(1) +} + +function safeFilePart(value) { + return value.replaceAll('/', '-') +}