mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-06 16:08:05 +00:00
A plugin system built on WebAssembly components (wasmtime, WASI p2), with a typed WIT API (`openidea:plugin@1.0.0`) and a Rust SDK, shipped as a preview. - Extension points declared in `plugin.toml` (commands, languages, themes, icon themes, menus, key bindings, status items, settings, views, custom editors, file icons and templates, run detectors, editor providers), validated before anything takes effect and projected onto the workbench's own registries: plugin themes and file icon themes are chosen in Settings › Appearance and Quick Switch, plugin shortcuts are edited in Settings › Keymap, plugin problems are grouped in Settings › Inspections. - Capabilities with risk levels and consent at install, pinned `process:` programs, secrets in the OS keychain. - Runtime: one thread per active plugin, fuel/time/memory budgets, soft and hard deadlines on the editing path, cancellation, crash backoff, idle reclaim, a precompiled-component cache; remote projects run code plugins on `openidea-server`. - Web views and custom editors with a sandboxed page, a document bridge joined to the text editor's undo history, and split or preview layouts. - Distribution: signed `.oiplugin` packages, a static marketplace index with revocation, versioned installs with rollback, install from a `.zip` or a folder. The release workflow signs and publishes the official plugins and their index, which released builds use as the default marketplace. - Official plugins: String Tools, Key Promoter, Git Toolbox, AI Completion, Checkstyle, MyBatis Navigator, CSV Editor, Solidity, Material Icons, Harbor Night, Excalidraw, Graphviz, Regex Tester, License Header. Closes #732 Co-authored-by: Deng Liu <5108773+dengliu@users.noreply.github.com> Co-authored-by: florentvdry <16673235+florentvdry@users.noreply.github.com>
320 lines
14 KiB
YAML
320 lines
14 KiB
YAML
name: Release
|
||
on:
|
||
push:
|
||
tags: ["v*"]
|
||
workflow_dispatch:
|
||
inputs:
|
||
tag:
|
||
description: "Tag to release (e.g. v0.1.0); must already exist"
|
||
required: true
|
||
|
||
permissions:
|
||
contents: write
|
||
|
||
concurrency:
|
||
group: release-${{ github.event.inputs.tag || github.ref_name }}
|
||
|
||
env:
|
||
CARGO_TERM_COLOR: always
|
||
CARGO_INCREMENTAL: 0
|
||
TAG: ${{ github.event.inputs.tag || github.ref_name }}
|
||
|
||
jobs:
|
||
# Creates (or reuses) one draft release that every platform build uploads into.
|
||
create-release:
|
||
runs-on: ubuntu-latest
|
||
outputs:
|
||
release_id: ${{ steps.release.outputs.id }}
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
with: { ref: "${{ env.TAG }}" }
|
||
- name: Tag matches app version
|
||
run: |
|
||
app="v$(jq -r .version src-tauri/tauri.conf.json)"
|
||
if [ "$app" != "$TAG" ]; then
|
||
echo "::error::tag $TAG does not match tauri.conf.json version $app"
|
||
exit 1
|
||
fi
|
||
- id: release
|
||
uses: actions/github-script@v7
|
||
with:
|
||
script: |
|
||
const tag = process.env.TAG;
|
||
const { owner, repo } = context.repo;
|
||
const releases = await github.paginate(github.rest.repos.listReleases, { owner, repo });
|
||
const existing = releases.find((r) => r.tag_name === tag);
|
||
if (existing) {
|
||
if (!existing.draft) core.setFailed(`release ${tag} is already published`);
|
||
core.setOutput("id", existing.id);
|
||
return;
|
||
}
|
||
const { data } = await github.rest.repos.createRelease({
|
||
owner, repo, tag_name: tag, name: `OpenIDEA ${tag}`,
|
||
draft: true, prerelease: tag.includes("-"), generate_release_notes: true,
|
||
});
|
||
core.setOutput("id", data.id);
|
||
|
||
build:
|
||
needs: create-release
|
||
name: Build (${{ matrix.name }})
|
||
strategy:
|
||
fail-fast: false
|
||
matrix:
|
||
include:
|
||
- name: macOS arm64
|
||
os: macos-latest
|
||
target: aarch64-apple-darwin
|
||
- name: macOS x64
|
||
os: macos-latest
|
||
target: x86_64-apple-darwin
|
||
- name: Linux x64
|
||
os: ubuntu-22.04
|
||
target: x86_64-unknown-linux-gnu
|
||
- name: Linux arm64
|
||
os: ubuntu-22.04-arm
|
||
target: aarch64-unknown-linux-gnu
|
||
- name: Windows arm64
|
||
os: windows-11-arm
|
||
target: aarch64-pc-windows-msvc
|
||
- name: Windows x64
|
||
os: windows-latest
|
||
target: x86_64-pc-windows-msvc
|
||
runs-on: ${{ matrix.os }}
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
with: { ref: "${{ env.TAG }}" }
|
||
- uses: pnpm/action-setup@v4
|
||
with: { version: 11 }
|
||
- uses: actions/setup-node@v4
|
||
with: { node-version: 22, cache: pnpm }
|
||
- uses: dtolnay/rust-toolchain@stable
|
||
with: { targets: "${{ matrix.target }}" }
|
||
- uses: Swatinem/rust-cache@v2
|
||
with: { key: "${{ matrix.target }}" }
|
||
- name: Linux deps
|
||
if: runner.os == 'Linux'
|
||
run: |
|
||
sudo apt-get update
|
||
sudo apt-get install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev libxdo-dev libssl-dev patchelf
|
||
- run: pnpm install --frozen-lockfile
|
||
# Signing is optional: only secrets that are set get exported, so forks and
|
||
# unconfigured repos produce unsigned builds instead of failing.
|
||
- name: Signing env
|
||
shell: bash
|
||
env:
|
||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||
run: |
|
||
for v in APPLE_CERTIFICATE APPLE_CERTIFICATE_PASSWORD APPLE_SIGNING_IDENTITY APPLE_ID \
|
||
APPLE_PASSWORD APPLE_TEAM_ID TAURI_SIGNING_PRIVATE_KEY TAURI_SIGNING_PRIVATE_KEY_PASSWORD; do
|
||
[ -n "${!v}" ] && { echo "$v<<__EOF__"; echo "${!v}"; echo "__EOF__"; } >> "$GITHUB_ENV"
|
||
done
|
||
true
|
||
- uses: tauri-apps/tauri-action@v0
|
||
env:
|
||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||
with:
|
||
releaseId: ${{ needs.create-release.outputs.release_id }}
|
||
tagName: ${{ env.TAG }}
|
||
args: --target ${{ matrix.target }}
|
||
# The parallel builds would race on one latest.json; `updater-manifest` writes it once.
|
||
uploadUpdaterJson: false
|
||
# tauri-action notarizes the .app but not the DMG around it; notarize and
|
||
# staple the DMG too, then replace the uploaded asset.
|
||
- name: Notarize DMG
|
||
if: runner.os == 'macOS' && env.APPLE_ID != ''
|
||
env:
|
||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||
run: |
|
||
for dmg in target/${{ matrix.target }}/release/bundle/dmg/*.dmg; do
|
||
xcrun notarytool submit "$dmg" --apple-id "$APPLE_ID" --password "$APPLE_PASSWORD" \
|
||
--team-id "$APPLE_TEAM_ID" --wait
|
||
xcrun stapler staple "$dmg"
|
||
gh release upload "$TAG" "$dmg" --clobber
|
||
done
|
||
|
||
# The remote development server, per host platform it can run on. The desktop
|
||
# app downloads the one for the host (ide-tools `server`) and installs it over SSH.
|
||
# Linux builds are static (musl), so they run whatever the host's glibc.
|
||
server:
|
||
needs: create-release
|
||
name: Server (${{ matrix.os }}-${{ matrix.arch }})
|
||
strategy:
|
||
fail-fast: false
|
||
matrix:
|
||
include:
|
||
- { runner: ubuntu-22.04, target: x86_64-unknown-linux-musl, os: linux, arch: x86_64 }
|
||
- { runner: ubuntu-24.04-arm, target: aarch64-unknown-linux-musl, os: linux, arch: aarch64 }
|
||
- { runner: macos-latest, target: aarch64-apple-darwin, os: macos, arch: aarch64 }
|
||
- { runner: macos-latest, target: x86_64-apple-darwin, os: macos, arch: x86_64 }
|
||
runs-on: ${{ matrix.runner }}
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
with: { ref: "${{ env.TAG }}" }
|
||
- uses: dtolnay/rust-toolchain@stable
|
||
with: { targets: "${{ matrix.target }}" }
|
||
- uses: Swatinem/rust-cache@v2
|
||
with: { key: "server-${{ matrix.target }}" }
|
||
- name: musl toolchain
|
||
if: runner.os == 'Linux'
|
||
run: sudo apt-get update && sudo apt-get install -y musl-tools
|
||
- name: Build
|
||
run: |
|
||
# musl-tools only ships `musl-gcc`; cc-rs would look for `<arch>-linux-musl-gcc`.
|
||
if [ "$RUNNER_OS" = Linux ]; then export CC_${TARGET//-/_}=musl-gcc; fi
|
||
cargo build --release --locked -p ide-host --bin openidea-server --target "$TARGET"
|
||
env:
|
||
TARGET: ${{ matrix.target }}
|
||
- name: Upload
|
||
env:
|
||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||
ASSET: openidea-server-${{ matrix.os }}-${{ matrix.arch }}.gz
|
||
run: |
|
||
gzip -9 -c target/${{ matrix.target }}/release/openidea-server > "$ASSET"
|
||
if command -v sha256sum >/dev/null; then sha256sum "$ASSET"; else shasum -a 256 "$ASSET"; fi > "$ASSET.sha256"
|
||
gh release upload "$TAG" "$ASSET" "$ASSET.sha256" --clobber
|
||
|
||
# The official plugins, signed, and the marketplace index released apps read
|
||
# (releases/latest/download/plugins-index.json). They are not in the
|
||
# installers: users install them from Settings › Plugins. Without the
|
||
# signing key (secret PLUGIN_SIGNING_KEY: the file `openidea-plugin keygen`
|
||
# writes) nothing is published and the release goes on.
|
||
plugins:
|
||
needs: create-release
|
||
name: Official plugins
|
||
runs-on: ubuntu-22.04
|
||
env:
|
||
SIGNING_KEY: ${{ secrets.PLUGIN_SIGNING_KEY }}
|
||
steps:
|
||
- id: key
|
||
run: |
|
||
if [ -z "$SIGNING_KEY" ]; then
|
||
echo "::warning::PLUGIN_SIGNING_KEY is not set: the official plugins are not published"
|
||
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||
fi
|
||
- uses: actions/checkout@v4
|
||
if: steps.key.outputs.skip != 'true'
|
||
with: { ref: "${{ env.TAG }}" }
|
||
- uses: dtolnay/rust-toolchain@stable
|
||
if: steps.key.outputs.skip != 'true'
|
||
with: { targets: wasm32-wasip2 }
|
||
- uses: Swatinem/rust-cache@v2
|
||
if: steps.key.outputs.skip != 'true'
|
||
with: { key: release-plugins }
|
||
- uses: pnpm/action-setup@v4
|
||
if: steps.key.outputs.skip != 'true'
|
||
with: { version: 11 }
|
||
- uses: actions/setup-node@v4
|
||
if: steps.key.outputs.skip != 'true'
|
||
with: { node-version: 22 }
|
||
# Builds the web pages; fails when a checked-in component isn't built from these sources.
|
||
- name: Build
|
||
if: steps.key.outputs.skip != 'true'
|
||
run: |
|
||
plugins/build.sh --check
|
||
cargo build --release --locked -p ide-host --bin openidea-plugin
|
||
- name: Pack, index and upload
|
||
if: steps.key.outputs.skip != 'true'
|
||
shell: bash
|
||
env:
|
||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||
run: |
|
||
set -euo pipefail
|
||
tool="$PWD/target/release/openidea-plugin"
|
||
key="$RUNNER_TEMP/openidea.key"
|
||
out="$RUNNER_TEMP/market"
|
||
(umask 077 && printf '%s' "$SIGNING_KEY" > "$key")
|
||
trap 'rm -f "$key"' EXIT
|
||
mkdir -p "$out"
|
||
for dir in plugins/official/*/; do
|
||
id="$(sed -n 's/^id *= *"\(.*\)".*/\1/p' "$dir/plugin.toml" | head -1)"
|
||
version="$(sed -n 's/^version *= *"\(.*\)".*/\1/p' "$dir/plugin.toml" | head -1)"
|
||
"$tool" pack "$dir" --key "$key" --publisher openidea --out "$out/$id-$version.oiplugin"
|
||
done
|
||
# The published index's revocations and earlier keys carry over.
|
||
curl -fsSL "https://github.com/$GITHUB_REPOSITORY/releases/latest/download/plugins-index.json" \
|
||
-o "$out/plugins-index.json" || rm -f "$out/plugins-index.json"
|
||
"$tool" index "$out" --publisher openidea --name OpenIDEA --key "$key" --out "$out/plugins-index.json"
|
||
gh release upload "$TAG" "$out"/*.oiplugin "$out/plugins-index.json" --clobber
|
||
|
||
# The updater manifest (latest.json), built once from the signed bundles every build uploaded.
|
||
# Fails when a platform is missing, so a release never ships a manifest without it.
|
||
updater-manifest:
|
||
needs: [create-release, build]
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- uses: actions/github-script@v7
|
||
env:
|
||
RELEASE_ID: ${{ needs.create-release.outputs.release_id }}
|
||
with:
|
||
script: |
|
||
const { owner, repo } = context.repo;
|
||
const release_id = Number(process.env.RELEASE_ID);
|
||
const tag = process.env.TAG;
|
||
const assets = await github.paginate(github.rest.repos.listReleaseAssets, { owner, repo, release_id });
|
||
const sigs = assets.filter((a) => a.name.endsWith(".sig"));
|
||
if (sigs.length === 0) {
|
||
core.notice("no signed bundles (TAURI_SIGNING_PRIVATE_KEY unset): skipping latest.json");
|
||
return;
|
||
}
|
||
// Bundle suffix → updater platform keys, as tauri-action names them.
|
||
const kinds = [
|
||
[/_aarch64\.AppImage$/, ["linux-aarch64", "linux-aarch64-appimage"]],
|
||
[/_arm64\.deb$/, ["linux-aarch64-deb"]],
|
||
[/\.aarch64\.rpm$/, ["linux-aarch64-rpm"]],
|
||
[/_amd64\.AppImage$/, ["linux-x86_64", "linux-x86_64-appimage"]],
|
||
[/_amd64\.deb$/, ["linux-x86_64-deb"]],
|
||
[/\.x86_64\.rpm$/, ["linux-x86_64-rpm"]],
|
||
[/_aarch64\.app\.tar\.gz$/, ["darwin-aarch64", "darwin-aarch64-app"]],
|
||
[/_x64\.app\.tar\.gz$/, ["darwin-x86_64", "darwin-x86_64-app"]],
|
||
[/_arm64_en-US\.msi$/, ["windows-aarch64", "windows-aarch64-msi"]],
|
||
[/_arm64-setup\.exe$/, ["windows-aarch64-nsis"]],
|
||
[/_x64_en-US\.msi$/, ["windows-x86_64", "windows-x86_64-msi"]],
|
||
[/_x64-setup\.exe$/, ["windows-x86_64-nsis"]],
|
||
];
|
||
const platforms = {};
|
||
for (const sig of sigs) {
|
||
const bundle = sig.name.slice(0, -".sig".length);
|
||
const kind = kinds.find(([re]) => re.test(bundle));
|
||
if (!kind) continue;
|
||
const { data } = await github.rest.repos.getReleaseAsset({
|
||
owner, repo, asset_id: sig.id, headers: { accept: "application/octet-stream" },
|
||
});
|
||
const signature = Buffer.from(data).toString("utf8").trim();
|
||
const url = `https://github.com/${owner}/${repo}/releases/download/${tag}/${encodeURIComponent(bundle)}`;
|
||
for (const key of kind[1]) platforms[key] = { signature, url };
|
||
}
|
||
const missing = kinds.flatMap(([, keys]) => keys).filter((k) => !platforms[k]);
|
||
if (missing.length) {
|
||
core.setFailed(`latest.json would miss ${missing.join(", ")}`);
|
||
return;
|
||
}
|
||
const manifest = { version: tag.replace(/^v/, ""), notes: "", pub_date: new Date().toISOString(), platforms };
|
||
const old = assets.find((a) => a.name === "latest.json");
|
||
if (old) await github.rest.repos.deleteReleaseAsset({ owner, repo, asset_id: old.id });
|
||
await github.rest.repos.uploadReleaseAsset({
|
||
owner, repo, release_id, name: "latest.json",
|
||
data: JSON.stringify(manifest, null, 2),
|
||
headers: { "content-type": "application/json" },
|
||
});
|
||
core.info(`latest.json: ${Object.keys(platforms).sort().join(", ")}`);
|
||
|
||
publish:
|
||
needs: [create-release, build, server, plugins, updater-manifest]
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- uses: actions/github-script@v7
|
||
env:
|
||
RELEASE_ID: ${{ needs.create-release.outputs.release_id }}
|
||
with:
|
||
script: |
|
||
await github.rest.repos.updateRelease({
|
||
...context.repo, release_id: Number(process.env.RELEASE_ID), draft: false,
|
||
});
|