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
46 changes: 46 additions & 0 deletions .github/workflows/plugin-api-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: Publish @mindgraph/plugin-api

# Maintainer-Schritt (manuell). Publish via npm Trusted Publishing (OIDC) + Provenance — KEIN
# langlebiger npm-Token als Secret. Standardmäßig Dry-run; echter Publish nur mit dry_run=false
# und freigegebenem Environment 'npm-publish'.

on:
workflow_dispatch:
inputs:
dry_run:
description: 'Nur testen (npm publish --dry-run), nicht wirklich veröffentlichen'
type: boolean
default: true

permissions:
contents: read

jobs:
publish:
runs-on: ubuntu-latest
environment: npm-publish
permissions:
contents: read
id-token: write # NUR dieser Job: npm Trusted Publishing (OIDC). Keine weiteren Scopes.
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: '20'
registry-url: 'https://registry.npmjs.org'
cache: npm
cache-dependency-path: app/package-lock.json
- name: Install (Workspace, inkl. Paket-Deps)
run: npm ci
working-directory: app
- name: npm aktualisieren (Trusted Publishing/OIDC braucht npm >= 11.5)
run: npm install -g npm@latest
- name: Publish (OIDC + provenance)
working-directory: app/packages/plugin-api
run: |
if [ "${{ inputs.dry_run }}" = "true" ]; then
echo "Dry-run — kein echter Publish."
npm publish --provenance --access public --dry-run
else
npm publish --provenance --access public
fi
43 changes: 43 additions & 0 deletions .github/workflows/plugin-artifact-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: Plugin Artifact CI

# PR-/Push-Gate: führt die Artefakt-Pipeline aus (Template build → mit Dev-Key signieren →
# mit passendem Test-Keyring verifizieren). Umgesetzt als vitest-E2E-Test (artifact/e2e.test.ts),
# weil das Template @mindgraph/plugin-api erst nach dem npm-Publish via `npm install` beziehen kann;
# der E2E-Test bundelt stattdessen gegen die Workspace-Quelle.

on:
pull_request:
paths:
- 'app/**'
- 'plugin-template/**'
- '.github/workflows/plugin-artifact-ci.yml'
push:
branches: [master]
paths:
- 'app/**'
- 'plugin-template/**'
- '.github/workflows/plugin-artifact-ci.yml'

permissions:
contents: read

jobs:
pipeline:
name: build → sign(dev) → verify(test keyring)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: '20'
cache: npm
cache-dependency-path: app/package-lock.json
- name: Install (app + workspace plugin-api)
run: npm ci
working-directory: app
- name: Typecheck (App + Paket)
run: npm run typecheck
working-directory: app
- name: Artefakt-Pipeline + Verifier (build → sign(dev) → verify)
run: npm run test
working-directory: app
42 changes: 42 additions & 0 deletions .github/workflows/plugin-release-sign.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: Sign Plugin Release (Produktion)

# Vom Build GETRENNT und NICHT automatisch: nur manuell und ausschließlich im geschützten
# Environment 'release-signing' (Required Reviewers + Secret PLUGIN_SIGNING_KEY). Standardmäßig
# Dry-run: prüft nur Verfügbarkeit/Form des Prod-Keys, signiert nichts. Der echte Prod-Sign + Pack
# bleibt ein Maintainer-Schritt (wird nach Bereitstellung des offiziellen Pack-CLI scharf geschaltet).

on:
workflow_dispatch:
inputs:
dry_run:
description: 'Nur Key-Verfügbarkeit/-Form prüfen (nicht signieren)'
type: boolean
default: true

permissions:
contents: read

jobs:
sign:
runs-on: ubuntu-latest
environment: release-signing # geschützt: Required Reviewers + Secret PLUGIN_SIGNING_KEY
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: '20'
- name: Prod-Signierschlüssel prüfen (Form, ohne den Wert zu loggen)
env:
PLUGIN_SIGNING_KEY: ${{ secrets.PLUGIN_SIGNING_KEY }}
run: |
if [ -z "$PLUGIN_SIGNING_KEY" ]; then
echo "::error::PLUGIN_SIGNING_KEY fehlt — Environment 'release-signing' nicht freigegeben."
exit 1
fi
node -e "const {createPrivateKey}=require('node:crypto');const k=createPrivateKey(process.env.PLUGIN_SIGNING_KEY);if(k.asymmetricKeyType!=='ed25519'){console.error('Kein Ed25519-Schlüssel');process.exit(1)}console.log('Prod-Key ok: Ed25519, PKCS#8.')"
- name: Prod-Sign + Pack
if: ${{ inputs.dry_run == false }}
run: |
echo "::error::Echter Prod-Sign + Pack ist noch nicht scharf geschaltet."
echo "Wird über das offizielle Pack-/Signier-CLI ausgeführt (Maintainer-Aktivierung)."
exit 1
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,6 @@ pitch/__pycache__/
# Persönliche Projekt-RAG-Eval-Testsets (echte Namen/Projektdaten) — nur example.json wird versioniert
app/scripts/rag-eval.*.json
!app/scripts/rag-eval.example.json

# @mindgraph/plugin-api npm-Publish-Output (nur via prepublishOnly gebaut)
app/packages/plugin-api/dist/
13 changes: 4 additions & 9 deletions app/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@
"react-window": "^2.2.5",
"reactflow": "^11.11.4",
"sql.js": "^1.13.0",
"tar": "^7.5.19",
"turndown": "^7.2.4",
"ws": "^8.18.0",
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
Expand Down
24 changes: 22 additions & 2 deletions app/packages/plugin-api/package.json
Original file line number Diff line number Diff line change
@@ -1,15 +1,35 @@
{
"name": "@mindgraph/plugin-api",
"version": "0.2.0",
"private": true,
"description": "Stabile öffentliche Grenze zwischen MindGraph und seinen Plugins (Typen + schmale Laufzeit-Helfer).",
"type": "module",
"exports": {
".": "./src/index.ts",
"./validation": "./src/validation.ts"
},
"files": [
"dist"
],
"publishConfig": {
"access": "public",
"provenance": true,
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./validation": {
"types": "./dist/validation.d.ts",
"default": "./dist/validation.js"
}
}
},
"scripts": {
"typecheck": "tsc -p tsconfig.json"
"typecheck": "tsc -p tsconfig.json",
"build": "tsc -p tsconfig.build.json && echo '{ \"type\": \"commonjs\" }' > dist/package.json",
"prepublishOnly": "npm run build"
},
"dependencies": {
"ajv": "^6.14.0",
Expand Down
18 changes: 18 additions & 0 deletions app/packages/plugin-api/tsconfig.build.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Emit-Konfiguration NUR für den npm-Publish (dist/). Die In-Repo-Konsumption läuft weiter
// über die TS-Quelle (exports → src, App-Alias, vitest). Hier wird CommonJS + .d.ts erzeugt;
// ein `dist/package.json` mit { "type": "commonjs" } (vom build-Script geschrieben) markiert das
// dist-Verzeichnis als CJS, obwohl das Paket-Root `"type": "module"` bleibt.
{
"extends": "./tsconfig.json",
"compilerOptions": {
"noEmit": false,
"declaration": true,
"outDir": "dist",
"module": "CommonJS",
"moduleResolution": "node",
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src"],
"exclude": ["src/**/*.test.ts"]
}
93 changes: 93 additions & 0 deletions app/src/main/plugins/artifact/e2e.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { describe, it, expect, afterAll } from 'vitest'
import { generateKeyPairSync } from 'node:crypto'
import { createRequire } from 'node:module'
import { fileURLToPath, pathToFileURL } from 'node:url'
import { mkdtempSync, readFileSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { dirname, join, resolve } from 'node:path'
import { packPluginArtifact } from './pack'
import { verifyPluginArtifact, type Keyring } from './verify'
import { canonicalJsonBytes } from './format'
import type { PluginMainEntry } from '@mindgraph/plugin-api'

// End-to-End-Pipeline (ADR docs/plugin-artifact-format-plan.md): das Main-only-Template wird mit
// derselben esbuild-Konfiguration gebaut wie im echten Repo, mit einem Dev-Key signiert, verifiziert
// und das verifizierte main.js per createRequire aus einem Ordner OHNE package.json geladen. Deckt
// die ADR-Akzeptanzkriterien 2–4 ab (build → sign → verify → CJS-Lade-Test).

const here = dirname(fileURLToPath(import.meta.url))
const repoRoot = resolve(here, '../../../../..')
const templateDir = join(repoRoot, 'plugin-template')
const apiSrc = join(repoRoot, 'app/packages/plugin-api/src/index.ts')

const tmpDirs: string[] = []
afterAll(() => {
for (const d of tmpDirs) rmSync(d, { recursive: true, force: true })
})

async function loadBuildConfig(): Promise<{
PLUGIN_BUILD_OPTIONS: Record<string, unknown>
banBuiltins: () => unknown
}> {
return (await import(pathToFileURL(join(templateDir, 'esbuild.config.mjs')).href)) as never
}

async function bundleTemplateMain(): Promise<Buffer> {
const esbuild = await import('esbuild')
const cfg = await loadBuildConfig()
const result = await esbuild.build({
...cfg.PLUGIN_BUILD_OPTIONS,
entryPoints: [join(templateDir, 'src/main.ts')],
bundle: true,
write: false,
alias: { '@mindgraph/plugin-api': apiSrc },
plugins: [cfg.banBuiltins() as never],
})
return Buffer.from(result.outputFiles![0].contents)
}

describe('A0/3 E2E — Template: build → sign → verify → createRequire', () => {
it('baut, signiert (Dev-Key), verifiziert und lädt main.js als CommonJS ohne package.json', async () => {
const mainJs = await bundleTemplateMain()
const manifestBytes = canonicalJsonBytes(JSON.parse(readFileSync(join(templateDir, 'manifest.json'), 'utf8')))

const kp = generateKeyPairSync('ed25519')
const keyId = 'dev-e2e'
const archive = await packPluginArtifact({
files: [
{ path: 'manifest.json', content: manifestBytes },
{ path: 'main.js', content: mainJs },
],
signKey: kp.privateKey,
keyId,
})

const quarantineDir = mkdtempSync(join(tmpdir(), 'mgx-e2e-'))
tmpDirs.push(quarantineDir)
const keyring: Keyring = { get: (id) => (id === keyId ? kp.publicKey : undefined) }

const result = await verifyPluginArtifact(archive, { keyring, appVersion: '0.8.14', quarantineDir })
expect(result.id).toBe('example-plugin')
expect(result.files.map((f) => f.path).sort()).toEqual(['main.js', 'manifest.json'])

// ADR-Kriterium: verifiziertes main.js aus einem Ordner OHNE package.json via createRequire laden.
const req = createRequire(join(quarantineDir, 'loader.cjs'))
const entry = req(join(quarantineDir, 'main.js')) as PluginMainEntry
expect(entry.id).toBe('example-plugin')
expect(typeof entry.register).toBe('function')
})

it('der Build verbietet Node-/Electron-Built-ins', async () => {
const esbuild = await import('esbuild')
const cfg = await loadBuildConfig()
await expect(
esbuild.build({
...cfg.PLUGIN_BUILD_OPTIONS,
stdin: { contents: "import 'node:fs'\nexport default {}", resolveDir: templateDir, loader: 'ts' },
bundle: true,
write: false,
plugins: [cfg.banBuiltins() as never],
})
).rejects.toThrow(/Built-in/)
})
})
Loading
Loading