mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-06 16:08:05 +00:00
* feat(plugins): list community plugins from a registry in the marketplace The marketplace index can now carry plugins of other publishers, published from their authors' repositories through the l0ng-ai/openidea-plugins registry. - `openidea-plugin index` with a key replaces only that publisher's plugins (it used to drop every other entry); without a key it indexes each package against the publishers the index already lists, checking the key and the id prefix instead of only the signature itself. - `openidea-plugin merge` takes another index's publishers, plugins and revocations in place of every publisher but `--keep`; the other index can't list or revoke the kept publishers' plugins. - `openidea-plugin check` accepts a `.oiplugin` file. - Releases merge the registry's index into plugins-index.json and attach the Linux x64 tool; plugins-index.yml refreshes the community plugins in the latest release hourly or when the registry signals a change. - plugins/README.md and CONTRIBUTING.md describe official and community plugins. * feat(plugins): offer the newest plugin version this IDE can load Index entries record the plugin API each version needs (`engine`: the manifest's `engines.openidea` narrowed to the API its component was built for), and an index may list several versions of a plugin. Settings › Plugins offers the newest version this IDE can load; with none, the plugin is shown as incompatible with the API it needs, and downloading it is refused. `openidea-plugin index` keeps, of each plugin, the newest version per API requirement, so users of an older IDE keep the last version built for them; `--download-base` makes downloads absolute, and the release workflow points them at the release that published them. Scratch directories are now unique per call. * feat(plugins): file marketplace plugins under categories `plugin.toml` takes `categories` (up to 3 of languages, themes, icon-themes, linters, vcs, ai, frameworks, tools, other). A plugin that declares none is filed under what its contributions imply (themes, icon themes, languages), else under other; categories an IDE doesn't know are ignored, and `check` warns about them. Index entries carry the categories, and Settings › Plugins › Available filters by them and shows them in a plugin's details. The official plugins declare theirs. * feat(plugins): let a marketplace index point to where it moved An index with `moved` sends the IDE to the index at that URL (relative or absolute, up to three hops; an https index can only move to https). Downloads resolve against the index actually read, so a marketplace can change hosts while released IDEs keep the address they were built with. * fix(plugins): full-width category filter and aligned incompatibility note * fix(plugins): translate the AI category in Chinese * feat(plugins): keep a plugin's signer across updates and announce updates - Index format: fields are only added; an IDE reading a newer `format` still applies its revocations and `moved`, lists no plugins and asks to be updated, instead of failing to read the index. - Key continuity: an installed plugin updates silently only to a package signed with the same key, or a key it endorsed (`KeyEntry.endorsement`, `index --previous-key`, `openidea-plugin endorse`). Another signer, or an unsigned package replacing a signed one, is shown on the install page and needs the user's confirmation (`Consent.signerChange`). - Official key: release builds carry the official publisher's public key (`OPENIDEA_OFFICIAL_PLUGIN_KEYS`, derived from PLUGIN_SIGNING_KEY); `openidea.*` plugins must be signed with it or a key it endorsed, whatever the index lists. - Updates: the daily marketplace check announces new versions once, with Update All (also in the gear menu and as an action). An update that asks for nothing new installs with the grants the installed version has. - Index entries need `engine`; `categories` beyond the first three are ignored instead of rejecting the manifest.
348 lines
16 KiB
YAML
348 lines
16 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 }}
|
||
COMMUNITY_PLUGINS_INDEX: https://github.com/l0ng-ai/openidea-plugins/releases/download/index/index.json
|
||
|
||
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 }}
|
||
official_plugin_keys: ${{ steps.plugin-keys.outputs.keys }}
|
||
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
|
||
# The official plugins' public key (from PLUGIN_SIGNING_KEY, the seed
|
||
# `openidea-plugin keygen` writes), built into the app: their packages
|
||
# must carry its signature whatever the marketplace index lists. Without
|
||
# the secret the app takes the index's word, as development builds do.
|
||
- id: plugin-keys
|
||
env:
|
||
SIGNING_KEY: ${{ secrets.PLUGIN_SIGNING_KEY }}
|
||
run: |
|
||
if [ -n "$SIGNING_KEY" ]; then
|
||
seed="$(printf %s "$SIGNING_KEY" | base64 -d | od -An -tx1 | tr -d ' \n')"
|
||
key="$(printf '302e020100300506032b657004220420%s' "$seed" | xxd -r -p |
|
||
openssl pkey -inform DER -pubout -outform DER | tail -c 32 | base64)"
|
||
echo "keys=$key" >> "$GITHUB_OUTPUT"
|
||
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 }})
|
||
env:
|
||
OPENIDEA_OFFICIAL_PLUGIN_KEYS: ${{ needs.create-release.outputs.official_plugin_keys }}
|
||
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. The index also lists
|
||
# the community plugins of the registry (COMMUNITY_PLUGINS_INDEX; refreshed
|
||
# between releases by plugins-index.yml), and the release carries the
|
||
# openidea-plugin tool for Linux x64, which that workflow and the registry's
|
||
# checks run.
|
||
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, earlier keys and community plugins 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"
|
||
# Downloads point at this release: older versions kept for older IDEs stay where they were published.
|
||
"$tool" index "$out" --publisher openidea --name OpenIDEA --key "$key" --out "$out/plugins-index.json" \
|
||
--download-base "https://github.com/$GITHUB_REPOSITORY/releases/download/$TAG"
|
||
"$tool" merge "$out/plugins-index.json" "$COMMUNITY_PLUGINS_INDEX" --keep openidea ||
|
||
echo "::warning::the community plugins were not refreshed"
|
||
gzip -9 -c "$tool" > "$out/openidea-plugin-linux-x86_64.gz"
|
||
gh release upload "$TAG" "$out"/*.oiplugin "$out/plugins-index.json" \
|
||
"$out/openidea-plugin-linux-x86_64.gz" --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,
|
||
});
|