mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-14 16:07:52 +00:00
Adds a nightly channel. Twice a day nightly.yml builds the newest main commit that passed CI and publishes it as a prerelease (OpenIDEA Nightly, ai.l0ng.openidea.nightly, own data and update manifest), or stops when nothing is newer than the last nightly. Platform builds move from release.yml into the reusable release-build.yml so both channels build the same way. Versions are one patch above main with a timestamp suffix; each build gets its own v…-nightly.… tag (remote development downloads the server by version) and the last 14 are kept; a rolling nightly release holds latest.json. No MSI or RPM for nightlies. Nightly builds skip the CNB mirror manifest, and their automatic update prompts (startup and periodic) appear at most once a day; a manual check always answers. Workflow permissions default to contents: read with write granted per job, Apple signing secrets reach only macOS builds, stable release.yml rejects nightly tags, tag pruning only matches -nightly. tags, and nightly runs share one concurrency group.
263 lines
12 KiB
YAML
263 lines
12 KiB
YAML
name: Release build
|
|
|
|
# Builds a tag's desktop apps, remote servers and updater manifest into its draft release.
|
|
# Stable releases (release.yml) and nightly builds (nightly.yml) both run it.
|
|
on:
|
|
workflow_call:
|
|
inputs:
|
|
tag: { type: string, required: true }
|
|
release_id: { type: string, required: true }
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
env:
|
|
CARGO_TERM_COLOR: always
|
|
CARGO_INCREMENTAL: 0
|
|
TAG: ${{ inputs.tag }}
|
|
|
|
jobs:
|
|
plugin-keys:
|
|
permissions: {}
|
|
runs-on: ubuntu-latest
|
|
outputs:
|
|
keys: ${{ steps.keys.outputs.keys }}
|
|
steps:
|
|
# 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: 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
|
|
|
|
build:
|
|
permissions:
|
|
contents: write
|
|
needs: plugin-keys
|
|
name: Build (${{ matrix.name }})
|
|
env:
|
|
OPENIDEA_OFFICIAL_PLUGIN_KEYS: ${{ needs.plugin-keys.outputs.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@v5
|
|
with: { ref: "${{ env.TAG }}" }
|
|
- uses: pnpm/action-setup@v5
|
|
with: { version: 11 }
|
|
- uses: actions/setup-node@v5
|
|
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: ${{ runner.os == 'macOS' && secrets.APPLE_CERTIFICATE || '' }}
|
|
APPLE_CERTIFICATE_PASSWORD: ${{ runner.os == 'macOS' && secrets.APPLE_CERTIFICATE_PASSWORD || '' }}
|
|
APPLE_SIGNING_IDENTITY: ${{ runner.os == 'macOS' && secrets.APPLE_SIGNING_IDENTITY || '' }}
|
|
APPLE_ID: ${{ runner.os == 'macOS' && secrets.APPLE_ID || '' }}
|
|
APPLE_PASSWORD: ${{ runner.os == 'macOS' && secrets.APPLE_PASSWORD || '' }}
|
|
APPLE_TEAM_ID: ${{ runner.os == 'macOS' && 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: ${{ inputs.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"
|
|
# Release assets have no spaces: GitHub stored the first upload with dots.
|
|
asset="$RUNNER_TEMP/$(basename "$dmg" | tr ' ' .)"
|
|
cp "$dmg" "$asset"
|
|
gh release upload "$TAG" "$asset" --clobber
|
|
done
|
|
# Portable Windows builds (#1857): on Windows Tauri embeds the
|
|
# frontend in the exe and WebView2 is a system component, so zipping
|
|
# the raw exe gives an unpack-and-run alternative to the installers.
|
|
- name: Portable zip
|
|
if: runner.os == 'Windows'
|
|
shell: pwsh
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
run: |
|
|
$target = "${{ matrix.target }}"
|
|
$arch = if ($target -like "x86_64*") { "x64" } else { "arm64" }
|
|
$version = $env:TAG -replace '^v', ''
|
|
$asset = "OpenIDEA_${version}_windows_${arch}_portable.zip"
|
|
$dir = "target/$target/release"
|
|
$files = @("$dir/openidea.exe")
|
|
if (Test-Path "$dir/WebView2Loader.dll") {
|
|
$files += "$dir/WebView2Loader.dll"
|
|
}
|
|
Compress-Archive -Path $files -DestinationPath $asset -Force
|
|
$hash = (Get-FileHash $asset -Algorithm SHA256).Hash.ToLower()
|
|
"$hash $asset" | Out-File -Encoding ascii "$asset.sha256"
|
|
gh release upload $env:TAG $asset "$asset.sha256" --clobber
|
|
|
|
# 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:
|
|
permissions:
|
|
contents: write
|
|
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@v5
|
|
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 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:
|
|
permissions:
|
|
contents: write
|
|
needs: build
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/github-script@v8
|
|
env:
|
|
RELEASE_ID: ${{ inputs.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.
|
|
// Nightly versions don't fit MSI or RPM, so those builds offer NSIS on Windows.
|
|
const nightly = tag.includes("-nightly.");
|
|
const windows = (arch, msi, nsis) => nightly
|
|
? [[nsis, [`windows-${arch}`, `windows-${arch}-nsis`]]]
|
|
: [[msi, [`windows-${arch}`, `windows-${arch}-msi`]], [nsis, [`windows-${arch}-nsis`]]];
|
|
const kinds = [
|
|
[/_aarch64\.AppImage$/, ["linux-aarch64", "linux-aarch64-appimage"]],
|
|
[/_arm64\.deb$/, ["linux-aarch64-deb"]],
|
|
...(nightly ? [] : [[/\.aarch64\.rpm$/, ["linux-aarch64-rpm"]]]),
|
|
[/_amd64\.AppImage$/, ["linux-x86_64", "linux-x86_64-appimage"]],
|
|
[/_amd64\.deb$/, ["linux-x86_64-deb"]],
|
|
...(nightly ? [] : [[/\.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"]],
|
|
...windows("aarch64", /_arm64_en-US\.msi$/, /_arm64-setup\.exe$/),
|
|
...windows("x86_64", /_x64_en-US\.msi$/, /_x64-setup\.exe$/),
|
|
];
|
|
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(", ")}`);
|