mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 04:55:42 +09:00
feat(ci): publish renderer modules without a shell build (#3279)
This commit is contained in:
1 parent
e0d7625c39
commit
f1373a63c3
3 files changed
+916
-8
No files matched your search
@@ -0,0 +1,350 @@
|
|||||||
|
name: build desktop modules
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
channel:
|
||||||
|
description: Release channel to ship renderer modules on. The live shell on that channel stays as it is.
|
||||||
|
required: true
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- canary
|
||||||
|
- stable
|
||||||
|
default: canary
|
||||||
|
build_version:
|
||||||
|
description: Explicit Fluxer CalVer build version (YYYY.MDD.MICRO, UTC HHMMSS without leading zeroes) to use instead of automatic UTC clock allocation.
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
type: string
|
||||||
|
allow_shell_drift:
|
||||||
|
description: Publish even though shell sources changed since the live shell was built. Only when the renderer does not depend on those changes.
|
||||||
|
required: false
|
||||||
|
default: false
|
||||||
|
type: boolean
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
id-token: write
|
||||||
|
actions: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: desktop-modules-${{ inputs.channel }}
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
env:
|
||||||
|
CHANNEL: ${{ inputs.channel }}
|
||||||
|
BUILD_CHANNEL: ${{ inputs.channel == 'canary' && 'canary' || 'stable' }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
meta:
|
||||||
|
name: Resolve build metadata
|
||||||
|
runs-on: ubuntu-24.04-arm
|
||||||
|
environment: desktop-releases
|
||||||
|
timeout-minutes: 25
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
outputs:
|
||||||
|
version: ${{ steps.meta.outputs.version }}
|
||||||
|
pub_date: ${{ steps.meta.outputs.pub_date }}
|
||||||
|
channel: ${{ steps.meta.outputs.channel }}
|
||||||
|
build_channel: ${{ steps.meta.outputs.build_channel }}
|
||||||
|
source_sha: ${{ steps.meta.outputs.source_sha }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout source
|
||||||
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||||
|
with:
|
||||||
|
ref: main
|
||||||
|
|
||||||
|
- name: Set up Rust toolchain (CI helpers)
|
||||||
|
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
|
||||||
|
with:
|
||||||
|
toolchain: "1.98.1"
|
||||||
|
|
||||||
|
- name: Create token
|
||||||
|
id: create-token
|
||||||
|
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1
|
||||||
|
with:
|
||||||
|
client-id: ${{ vars.FLUXER_CI_APP_ID }}
|
||||||
|
private-key: ${{ secrets.FLUXER_CI_APP_KEY }}
|
||||||
|
owner: fluxerapp
|
||||||
|
repositories: fluxer
|
||||||
|
permission-contents: read
|
||||||
|
- name: Set metadata
|
||||||
|
id: meta
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ steps.create-token.outputs.token }}
|
||||||
|
FLUXER_BUILD_VERSION: ${{ inputs.build_version }}
|
||||||
|
run: >-
|
||||||
|
cargo run --locked --quiet --manifest-path tools/ci/Cargo.toml -- build-desktop
|
||||||
|
--step set_metadata
|
||||||
|
--channel "${{ inputs.channel }}"
|
||||||
|
|
||||||
|
shared_assets:
|
||||||
|
name: Build shared renderer assets
|
||||||
|
needs:
|
||||||
|
- meta
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
environment: desktop-releases
|
||||||
|
timeout-minutes: 60
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
env:
|
||||||
|
CHANNEL: ${{ needs.meta.outputs.channel }}
|
||||||
|
BUILD_CHANNEL: ${{ needs.meta.outputs.build_channel }}
|
||||||
|
RELEASE_CHANNEL: ${{ needs.meta.outputs.build_channel }}
|
||||||
|
PUBLIC_RELEASE_CHANNEL: ${{ needs.meta.outputs.build_channel }}
|
||||||
|
VERSION: ${{ needs.meta.outputs.version }}
|
||||||
|
BUILD_VERSION: ${{ needs.meta.outputs.version }}
|
||||||
|
PUBLIC_BUILD_VERSION: ${{ needs.meta.outputs.version }}
|
||||||
|
SOURCE_SHA: ${{ needs.meta.outputs.source_sha }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout CI helpers
|
||||||
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||||
|
with:
|
||||||
|
ref: ${{ needs.meta.outputs.source_sha }}
|
||||||
|
path: _ci
|
||||||
|
|
||||||
|
- name: Checkout source
|
||||||
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||||
|
with:
|
||||||
|
ref: ${{ needs.meta.outputs.source_sha }}
|
||||||
|
path: source
|
||||||
|
|
||||||
|
- name: Set up Rust toolchain (renderer wasm)
|
||||||
|
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
|
||||||
|
with:
|
||||||
|
toolchain: "1.98.1"
|
||||||
|
targets: wasm32-unknown-unknown
|
||||||
|
|
||||||
|
- name: Set workdir (Unix)
|
||||||
|
env:
|
||||||
|
SUBST_TARGET: ${{ github.workspace }}/source
|
||||||
|
run: >-
|
||||||
|
cargo run --locked --quiet --manifest-path ${{ github.workspace }}/_ci/tools/ci/Cargo.toml -- build-desktop
|
||||||
|
--step set_workdir_unix
|
||||||
|
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
|
||||||
|
with:
|
||||||
|
node-version: 26
|
||||||
|
|
||||||
|
- name: Set up pnpm
|
||||||
|
run: >-
|
||||||
|
cargo run --locked --quiet --manifest-path ${{ github.workspace }}/_ci/tools/ci/Cargo.toml -- build-desktop
|
||||||
|
--step setup_pnpm
|
||||||
|
|
||||||
|
- name: Resolve pnpm store path (Unix)
|
||||||
|
run: >-
|
||||||
|
cargo run --locked --quiet --manifest-path ${{ github.workspace }}/_ci/tools/ci/Cargo.toml -- build-desktop
|
||||||
|
--step resolve_pnpm_store_unix
|
||||||
|
|
||||||
|
- name: Cache pnpm store
|
||||||
|
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9
|
||||||
|
with:
|
||||||
|
path: ${{ env.PNPM_STORE_PATH }}
|
||||||
|
key: ${{ runner.os }}-shared-renderer-pnpm-store-${{ hashFiles('source/pnpm-lock.yaml') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-shared-renderer-pnpm-store-
|
||||||
|
|
||||||
|
- name: Cache cargo registry
|
||||||
|
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/registry
|
||||||
|
~/.cargo/git
|
||||||
|
key: ${{ runner.os }}-shared-renderer-cargo-registry-${{ hashFiles('source/Cargo.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-shared-renderer-cargo-registry-
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
working-directory: ${{ env.WORKDIR }}/fluxer_desktop
|
||||||
|
run: >-
|
||||||
|
cargo run --locked --quiet --manifest-path ${{ github.workspace }}/_ci/tools/ci/Cargo.toml -- build-desktop
|
||||||
|
--step install_dependencies
|
||||||
|
|
||||||
|
- name: Update version
|
||||||
|
working-directory: ${{ env.WORKDIR }}/fluxer_desktop
|
||||||
|
run: >-
|
||||||
|
cargo run --locked --quiet --manifest-path ${{ github.workspace }}/_ci/tools/ci/Cargo.toml -- build-desktop
|
||||||
|
--step update_version
|
||||||
|
|
||||||
|
- name: Set build channel
|
||||||
|
working-directory: ${{ env.WORKDIR }}/fluxer_desktop
|
||||||
|
run: >-
|
||||||
|
cargo run --locked --quiet --manifest-path ${{ github.workspace }}/_ci/tools/ci/Cargo.toml -- build-desktop
|
||||||
|
--step set_build_channel
|
||||||
|
|
||||||
|
- name: Build shared renderer assets
|
||||||
|
working-directory: ${{ env.WORKDIR }}/fluxer_desktop
|
||||||
|
run: >-
|
||||||
|
cargo run --locked --quiet --manifest-path ${{ github.workspace }}/_ci/tools/ci/Cargo.toml -- build-desktop
|
||||||
|
--step build_shared_assets
|
||||||
|
|
||||||
|
- name: Prepare shared renderer artifact
|
||||||
|
run: >-
|
||||||
|
cargo run --locked --quiet --manifest-path ${{ github.workspace }}/_ci/tools/ci/Cargo.toml -- build-desktop
|
||||||
|
--step prepare_shared_assets
|
||||||
|
|
||||||
|
- name: Upload shared renderer artifact
|
||||||
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
|
||||||
|
with:
|
||||||
|
name: desktop-shared-${{ needs.meta.outputs.build_channel }}-${{ needs.meta.outputs.version }}-${{ needs.meta.outputs.source_sha }}
|
||||||
|
path: source/desktop-shared-assets
|
||||||
|
if-no-files-found: error
|
||||||
|
retention-days: 1
|
||||||
|
compression-level: 0
|
||||||
|
|
||||||
|
- name: Split renderer into desktop modules
|
||||||
|
run: >-
|
||||||
|
cargo run --locked --quiet --manifest-path ${{ github.workspace }}/_ci/tools/ci/Cargo.toml -- build-desktop
|
||||||
|
--step split_modules
|
||||||
|
|
||||||
|
- name: Pack desktop modules
|
||||||
|
run: >-
|
||||||
|
cargo run --locked --quiet --manifest-path ${{ github.workspace }}/_ci/tools/ci/Cargo.toml -- build-desktop
|
||||||
|
--step pack_modules
|
||||||
|
|
||||||
|
- name: Upload desktop module packages
|
||||||
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
|
||||||
|
with:
|
||||||
|
name: desktop-modules-${{ needs.meta.outputs.build_channel }}-${{ needs.meta.outputs.version }}-${{ needs.meta.outputs.source_sha }}
|
||||||
|
path: |
|
||||||
|
source/desktop-modules/classification.json
|
||||||
|
source/desktop-modules/*/module.json
|
||||||
|
source/desktop-modules/*/package.br
|
||||||
|
source/desktop-modules/*/package.br.sha256
|
||||||
|
if-no-files-found: error
|
||||||
|
retention-days: 1
|
||||||
|
compression-level: 0
|
||||||
|
|
||||||
|
assemble:
|
||||||
|
name: Assemble the modules-only release
|
||||||
|
if: ${{ !cancelled() && needs.shared_assets.result == 'success' }}
|
||||||
|
needs:
|
||||||
|
- meta
|
||||||
|
- shared_assets
|
||||||
|
runs-on: ubuntu-24.04-arm
|
||||||
|
environment: desktop-releases
|
||||||
|
timeout-minutes: 60
|
||||||
|
permissions:
|
||||||
|
actions: read
|
||||||
|
contents: read
|
||||||
|
env:
|
||||||
|
CHANNEL: ${{ needs.meta.outputs.build_channel }}
|
||||||
|
VERSION: ${{ needs.meta.outputs.version }}
|
||||||
|
PUB_DATE: ${{ needs.meta.outputs.pub_date }}
|
||||||
|
SOURCE_SHA: ${{ needs.meta.outputs.source_sha }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout source
|
||||||
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||||
|
with:
|
||||||
|
ref: ${{ needs.meta.outputs.source_sha }}
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set up Rust toolchain (CI helpers)
|
||||||
|
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
|
||||||
|
with:
|
||||||
|
toolchain: "1.98.1"
|
||||||
|
|
||||||
|
- name: Download desktop module packages
|
||||||
|
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
|
||||||
|
with:
|
||||||
|
name: desktop-modules-${{ needs.meta.outputs.build_channel }}-${{ needs.meta.outputs.version }}-${{ needs.meta.outputs.source_sha }}
|
||||||
|
path: desktop-modules
|
||||||
|
|
||||||
|
- name: Create token
|
||||||
|
id: create-token
|
||||||
|
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1
|
||||||
|
with:
|
||||||
|
client-id: ${{ vars.FLUXER_CI_APP_ID }}
|
||||||
|
private-key: ${{ secrets.FLUXER_CI_APP_KEY }}
|
||||||
|
owner: fluxerapp
|
||||||
|
repositories: fluxer
|
||||||
|
permission-contents: read
|
||||||
|
- name: Check the live shell runs these modules
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ steps.create-token.outputs.token }}
|
||||||
|
ALLOW_SHELL_DRIFT: ${{ inputs.allow_shell_drift }}
|
||||||
|
run: >-
|
||||||
|
cargo run --locked --quiet --manifest-path tools/ci/Cargo.toml -- build-desktop
|
||||||
|
--step check_shell_drift
|
||||||
|
|
||||||
|
- name: Build the modules-only manifests
|
||||||
|
run: >-
|
||||||
|
cargo run --locked --quiet --manifest-path tools/ci/Cargo.toml -- build-desktop
|
||||||
|
--step build_modules_only_manifest
|
||||||
|
|
||||||
|
- name: Prepare GitHub release assets
|
||||||
|
run: >-
|
||||||
|
cargo run --locked --quiet --manifest-path tools/ci/Cargo.toml -- build-desktop
|
||||||
|
--step prepare_modules_release_assets
|
||||||
|
|
||||||
|
- name: Upload GitHub release assets
|
||||||
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
|
||||||
|
with:
|
||||||
|
name: fluxer-desktop-modules-release-assets
|
||||||
|
path: release_assets
|
||||||
|
if-no-files-found: error
|
||||||
|
retention-days: 1
|
||||||
|
compression-level: 0
|
||||||
|
|
||||||
|
publish_release:
|
||||||
|
name: Publish GitHub modules-only release
|
||||||
|
if: ${{ !cancelled() && needs.assemble.result == 'success' }}
|
||||||
|
needs:
|
||||||
|
- meta
|
||||||
|
- assemble
|
||||||
|
runs-on: ubuntu-24.04-arm
|
||||||
|
environment: desktop-releases
|
||||||
|
timeout-minutes: 60
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
env:
|
||||||
|
CHANNEL: ${{ needs.meta.outputs.build_channel }}
|
||||||
|
VERSION: ${{ needs.meta.outputs.version }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout source
|
||||||
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||||
|
with:
|
||||||
|
ref: ${{ needs.meta.outputs.source_sha }}
|
||||||
|
|
||||||
|
- name: Set up Rust toolchain (CI helpers)
|
||||||
|
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
|
||||||
|
with:
|
||||||
|
toolchain: "1.98.1"
|
||||||
|
|
||||||
|
- name: Download GitHub release assets
|
||||||
|
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
|
||||||
|
with:
|
||||||
|
name: fluxer-desktop-modules-release-assets
|
||||||
|
path: release_assets
|
||||||
|
|
||||||
|
- name: Create token
|
||||||
|
id: create-token
|
||||||
|
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1
|
||||||
|
with:
|
||||||
|
client-id: ${{ vars.FLUXER_CI_APP_ID }}
|
||||||
|
private-key: ${{ secrets.FLUXER_CI_APP_KEY }}
|
||||||
|
owner: fluxerapp
|
||||||
|
repositories: fluxer
|
||||||
|
permission-contents: write
|
||||||
|
- name: Publish GitHub modules-only release
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ steps.create-token.outputs.token }}
|
||||||
|
CHANNEL: ${{ needs.meta.outputs.build_channel }}
|
||||||
|
VERSION: ${{ needs.meta.outputs.version }}
|
||||||
|
SOURCE_SHA: ${{ needs.meta.outputs.source_sha }}
|
||||||
|
RELEASE_BASELINE_SHA: ${{ vars.RELEASE_BASELINE_SHA }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
release_args=(
|
||||||
|
release publish
|
||||||
|
--component "fluxer-desktop-${CHANNEL}"
|
||||||
|
--build-version "${VERSION}"
|
||||||
|
--source-sha "${SOURCE_SHA}"
|
||||||
|
--previous-sha "${RELEASE_BASELINE_SHA}"
|
||||||
|
--asset-dir release_assets
|
||||||
|
)
|
||||||
|
if [[ "${CHANNEL}" == "canary" ]]; then
|
||||||
|
release_args+=(--prerelease)
|
||||||
|
fi
|
||||||
|
cargo run --locked --quiet --manifest-path tools/ci/Cargo.toml -- "${release_args[@]}"
|
||||||
+487
-8
@@ -10,10 +10,10 @@ use crate::common::{
|
|||||||
use crate::functions::{sha256_file, write_json_pretty};
|
use crate::functions::{sha256_file, write_json_pretty};
|
||||||
use crate::release::{
|
use crate::release::{
|
||||||
DESKTOP_RELEASE_DESCRIPTOR_SCHEMA_VERSION, DesktopReleaseAsset, DesktopReleaseDescriptor,
|
DESKTOP_RELEASE_DESCRIPTOR_SCHEMA_VERSION, DesktopReleaseAsset, DesktopReleaseDescriptor,
|
||||||
desktop_module_manifest_release_asset_name, desktop_module_manifest_storage_key,
|
DesktopReleaseKind, RELEASE_REPOSITORY, desktop_module_manifest_release_asset_name,
|
||||||
desktop_module_package_release_asset_name, desktop_module_package_storage_key,
|
desktop_module_manifest_storage_key, desktop_module_package_release_asset_name,
|
||||||
desktop_release_asset_name, desktop_release_coordinates, desktop_release_descriptor_filename,
|
desktop_module_package_storage_key, desktop_release_asset_name, desktop_release_coordinates,
|
||||||
desktop_release_product, desktop_release_shipped_formats,
|
desktop_release_descriptor_filename, desktop_release_product, desktop_release_shipped_formats,
|
||||||
desktop_release_update_payload_suffix, desktop_release_updater_feeds,
|
desktop_release_update_payload_suffix, desktop_release_updater_feeds,
|
||||||
validate_desktop_release_descriptor, validate_desktop_release_module_files,
|
validate_desktop_release_descriptor, validate_desktop_release_module_files,
|
||||||
};
|
};
|
||||||
@@ -166,6 +166,9 @@ enum DesktopStep {
|
|||||||
BuildModuleManifest,
|
BuildModuleManifest,
|
||||||
PrepareReleaseAssets,
|
PrepareReleaseAssets,
|
||||||
BuildSummary,
|
BuildSummary,
|
||||||
|
CheckShellDrift,
|
||||||
|
BuildModulesOnlyManifest,
|
||||||
|
PrepareModulesReleaseAssets,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
@@ -286,6 +289,9 @@ pub async fn run(args: BuildDesktopArgs) -> Result<()> {
|
|||||||
DesktopStep::BuildModuleManifest => build_module_manifest_step(),
|
DesktopStep::BuildModuleManifest => build_module_manifest_step(),
|
||||||
DesktopStep::PrepareReleaseAssets => prepare_release_assets_step(),
|
DesktopStep::PrepareReleaseAssets => prepare_release_assets_step(),
|
||||||
DesktopStep::BuildSummary => build_summary_step(),
|
DesktopStep::BuildSummary => build_summary_step(),
|
||||||
|
DesktopStep::CheckShellDrift => check_shell_drift_step().await,
|
||||||
|
DesktopStep::BuildModulesOnlyManifest => build_modules_only_manifest_step(),
|
||||||
|
DesktopStep::PrepareModulesReleaseAssets => prepare_modules_release_assets_step(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4595,6 +4601,7 @@ fn prepare_release_assets_step() -> Result<()> {
|
|||||||
descriptor_modules.sort_by(|left, right| left.storage_key.cmp(&right.storage_key));
|
descriptor_modules.sort_by(|left, right| left.storage_key.cmp(&right.storage_key));
|
||||||
let descriptor = DesktopReleaseDescriptor {
|
let descriptor = DesktopReleaseDescriptor {
|
||||||
schema_version: DESKTOP_RELEASE_DESCRIPTOR_SCHEMA_VERSION,
|
schema_version: DESKTOP_RELEASE_DESCRIPTOR_SCHEMA_VERSION,
|
||||||
|
kind: DesktopReleaseKind::Full,
|
||||||
channel: channel.clone(),
|
channel: channel.clone(),
|
||||||
version: version.clone(),
|
version: version.clone(),
|
||||||
release_tag: format!("fluxer-desktop-{channel}@{version}"),
|
release_tag: format!("fluxer-desktop-{channel}@{version}"),
|
||||||
@@ -5314,13 +5321,20 @@ fn desktop_channel_manifest_path(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn write_desktop_channel_manifest(path: &Path, manifest: &DesktopChannelManifest) -> Result<()> {
|
fn write_desktop_channel_manifest(path: &Path, manifest: &DesktopChannelManifest) -> Result<()> {
|
||||||
let mut bytes = serde_json::to_vec(manifest)?;
|
let bytes = serde_json::to_vec(manifest)?;
|
||||||
|
write_desktop_channel_manifest_bytes(path, &manifest.platform, &manifest.arch, bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_desktop_channel_manifest_bytes(
|
||||||
|
path: &Path,
|
||||||
|
platform: &str,
|
||||||
|
arch: &str,
|
||||||
|
mut bytes: Vec<u8>,
|
||||||
|
) -> Result<()> {
|
||||||
bytes.push(b'\n');
|
bytes.push(b'\n');
|
||||||
ensure!(
|
ensure!(
|
||||||
bytes.len() <= DESKTOP_CHANNEL_MANIFEST_MAX_BYTES,
|
bytes.len() <= DESKTOP_CHANNEL_MANIFEST_MAX_BYTES,
|
||||||
"The module manifest for {} {} is {} bytes, above the {DESKTOP_CHANNEL_MANIFEST_MAX_BYTES} byte client limit",
|
"The module manifest for {platform} {arch} is {} bytes, above the {DESKTOP_CHANNEL_MANIFEST_MAX_BYTES} byte client limit",
|
||||||
manifest.platform,
|
|
||||||
manifest.arch,
|
|
||||||
bytes.len()
|
bytes.len()
|
||||||
);
|
);
|
||||||
if let Some(parent) = path.parent() {
|
if let Some(parent) = path.parent() {
|
||||||
@@ -5409,6 +5423,340 @@ fn build_module_manifest_step() -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DESKTOP_ALLOW_SHELL_DRIFT_ENV: &str = "ALLOW_SHELL_DRIFT";
|
||||||
|
const DESKTOP_LIVE_SHELL_FILE: &str = "live-shell.json";
|
||||||
|
const DESKTOP_LIVE_MANIFEST_TIMEOUT: Duration = Duration::from_secs(30);
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
|
||||||
|
struct DesktopLiveShell {
|
||||||
|
channel: String,
|
||||||
|
shell_version: String,
|
||||||
|
shell_sha: String,
|
||||||
|
manifests: BTreeMap<String, Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn desktop_coordinate_key(platform: &str, arch: &str) -> String {
|
||||||
|
format!("{platform}/{arch}")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn desktop_live_manifest_url(channel: &str, platform: &str, arch: &str) -> String {
|
||||||
|
format!(
|
||||||
|
"{}/{DESKTOP_PAYLOAD_PREFIX}/{channel}/{platform}/{arch}/{DESKTOP_CHANNEL_MANIFEST_NAME}",
|
||||||
|
package_origin_base()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn fetch_live_channel_manifests(channel: &str) -> Result<BTreeMap<String, Value>> {
|
||||||
|
let client = reqwest::Client::builder()
|
||||||
|
.timeout(DESKTOP_LIVE_MANIFEST_TIMEOUT)
|
||||||
|
.build()
|
||||||
|
.context("Failed to build the HTTP client")?;
|
||||||
|
let mut manifests = BTreeMap::new();
|
||||||
|
for (platform, arch) in desktop_release_coordinates() {
|
||||||
|
let url = desktop_live_manifest_url(channel, platform, arch);
|
||||||
|
let manifest: Value = client
|
||||||
|
.get(&url)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.with_context(|| format!("Failed to fetch {url}"))?
|
||||||
|
.error_for_status()
|
||||||
|
.with_context(|| format!("{url} did not serve a module manifest"))?
|
||||||
|
.json()
|
||||||
|
.await
|
||||||
|
.with_context(|| format!("{url} is not JSON"))?;
|
||||||
|
ensure!(
|
||||||
|
manifest["release_channel"] == channel
|
||||||
|
&& manifest["platform"] == platform
|
||||||
|
&& manifest["arch"] == arch,
|
||||||
|
"{url} describes another channel or coordinate"
|
||||||
|
);
|
||||||
|
manifests.insert(desktop_coordinate_key(platform, arch), manifest);
|
||||||
|
}
|
||||||
|
Ok(manifests)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn live_shell_version(manifest: &Value) -> Option<&str> {
|
||||||
|
manifest["shell"]["latest_version"].as_str()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn common_live_shell_version(manifests: &BTreeMap<String, Value>) -> Result<String> {
|
||||||
|
let versions = manifests
|
||||||
|
.iter()
|
||||||
|
.map(|(coordinate, manifest)| {
|
||||||
|
live_shell_version(manifest)
|
||||||
|
.map(str::to_string)
|
||||||
|
.with_context(|| format!("The live {coordinate} manifest names no shell version"))
|
||||||
|
})
|
||||||
|
.collect::<Result<BTreeSet<_>>>()?;
|
||||||
|
let mut iter = versions.iter();
|
||||||
|
let first = iter
|
||||||
|
.next()
|
||||||
|
.context("No live module manifest was fetched")?
|
||||||
|
.clone();
|
||||||
|
ensure!(
|
||||||
|
iter.next().is_none(),
|
||||||
|
"The live feeds disagree on the shell version ({}), finish publishing the last full desktop release before a modules-only release",
|
||||||
|
versions.iter().cloned().collect::<Vec<_>>().join(", ")
|
||||||
|
);
|
||||||
|
crate::common::parse_version_instant(&first)
|
||||||
|
.with_context(|| format!("The live shell version {first:?} is not a CalVer"))?;
|
||||||
|
Ok(first)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn desktop_shell_source_roots(repo_root: &Path) -> Result<Vec<String>> {
|
||||||
|
let package_path = repo_root.join("fluxer_desktop").join("package.json");
|
||||||
|
let package: Value = serde_json::from_slice(
|
||||||
|
&fs::read(&package_path)
|
||||||
|
.with_context(|| format!("Failed to read {}", package_path.display()))?,
|
||||||
|
)
|
||||||
|
.with_context(|| format!("Failed to parse {}", package_path.display()))?;
|
||||||
|
let workspace_dependencies = package["dependencies"]
|
||||||
|
.as_object()
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.filter(|(_, spec)| {
|
||||||
|
spec.as_str()
|
||||||
|
.is_some_and(|spec| spec.starts_with("workspace:"))
|
||||||
|
})
|
||||||
|
.map(|(name, _)| name.clone())
|
||||||
|
.collect::<BTreeSet<_>>();
|
||||||
|
let mut roots = vec!["fluxer_desktop/".to_string()];
|
||||||
|
for package_dir in sorted_child_directories(&repo_root.join("packages"))? {
|
||||||
|
let manifest_path = package_dir.join("package.json");
|
||||||
|
if !manifest_path.is_file() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let manifest: Value = serde_json::from_slice(&fs::read(&manifest_path)?)
|
||||||
|
.with_context(|| format!("Failed to parse {}", manifest_path.display()))?;
|
||||||
|
if manifest["name"]
|
||||||
|
.as_str()
|
||||||
|
.is_some_and(|name| workspace_dependencies.contains(name))
|
||||||
|
{
|
||||||
|
roots.push(format!("packages/{}/", file_name_string(&package_dir)?));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(roots)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_shell_irrelevant_path(path: &str) -> bool {
|
||||||
|
path.contains(".test.")
|
||||||
|
|| path.contains("/__tests__/")
|
||||||
|
|| path.contains("/__fixtures__/")
|
||||||
|
|| path.ends_with(".md")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn desktop_shell_drift(changed: &[String], roots: &[String]) -> Vec<String> {
|
||||||
|
changed
|
||||||
|
.iter()
|
||||||
|
.filter(|path| roots.iter().any(|root| path.starts_with(root.as_str())))
|
||||||
|
.filter(|path| !is_shell_irrelevant_path(path))
|
||||||
|
.cloned()
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn check_shell_drift_step() -> Result<()> {
|
||||||
|
let channel = require_env("CHANNEL")?;
|
||||||
|
let source_sha = require_env("SOURCE_SHA")?;
|
||||||
|
let manifests = fetch_live_channel_manifests(&channel).await?;
|
||||||
|
let shell_version = common_live_shell_version(&manifests)?;
|
||||||
|
let tag = format!("fluxer-desktop-{channel}@{shell_version}");
|
||||||
|
let shell_sha = output_text(CommandSpec::new("gh").args([
|
||||||
|
"api",
|
||||||
|
&format!("repos/{RELEASE_REPOSITORY}/commits/{tag}"),
|
||||||
|
"--jq",
|
||||||
|
".sha",
|
||||||
|
]))
|
||||||
|
.with_context(|| format!("Failed to resolve the live shell release {tag}"))?
|
||||||
|
.trim()
|
||||||
|
.to_string();
|
||||||
|
ensure!(
|
||||||
|
shell_sha.len() == 40 && shell_sha.bytes().all(|byte| byte.is_ascii_hexdigit()),
|
||||||
|
"{tag} resolved to an invalid commit {shell_sha:?}"
|
||||||
|
);
|
||||||
|
let repo_root = env::current_dir().context("Failed to resolve the repository root")?;
|
||||||
|
let roots = desktop_shell_source_roots(&repo_root)?;
|
||||||
|
let changed =
|
||||||
|
output_text(CommandSpec::new("git").args(["diff", "--name-only", &shell_sha, &source_sha]))
|
||||||
|
.with_context(|| format!("Failed to diff {shell_sha} against {source_sha}"))?
|
||||||
|
.lines()
|
||||||
|
.map(str::to_string)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let drift = desktop_shell_drift(&changed, &roots);
|
||||||
|
if !drift.is_empty() {
|
||||||
|
let listed = drift.join("\n ");
|
||||||
|
ensure!(
|
||||||
|
env_bool(DESKTOP_ALLOW_SHELL_DRIFT_ENV),
|
||||||
|
"The live {channel} shell {shell_version} was built from {shell_sha}, and these shell sources changed since then:\n {listed}\nShip a full desktop release, or set {DESKTOP_ALLOW_SHELL_DRIFT_ENV}=true if the renderer does not depend on these changes"
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
"{DESKTOP_ALLOW_SHELL_DRIFT_ENV} is set, publishing modules although these shell sources changed since {shell_sha}:\n {listed}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let live = DesktopLiveShell {
|
||||||
|
channel: channel.clone(),
|
||||||
|
shell_version: shell_version.clone(),
|
||||||
|
shell_sha: shell_sha.clone(),
|
||||||
|
manifests,
|
||||||
|
};
|
||||||
|
let modules_dir = desktop_modules_dir();
|
||||||
|
fs::create_dir_all(&modules_dir)
|
||||||
|
.with_context(|| format!("Failed to create {}", modules_dir.display()))?;
|
||||||
|
write_json_pretty(&modules_dir.join(DESKTOP_LIVE_SHELL_FILE), &live)?;
|
||||||
|
println!(
|
||||||
|
"The live {channel} shell is {shell_version} from {shell_sha}, the modules from {source_sha} run on it"
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
fn modules_only_channel_manifest(
|
||||||
|
live: &Value,
|
||||||
|
channel: &str,
|
||||||
|
platform: &str,
|
||||||
|
arch: &str,
|
||||||
|
build_version: &str,
|
||||||
|
pub_date: &str,
|
||||||
|
metadata_version: u64,
|
||||||
|
packed: &[DesktopPackedModule],
|
||||||
|
) -> Result<Value> {
|
||||||
|
let mut manifest = live
|
||||||
|
.as_object()
|
||||||
|
.cloned()
|
||||||
|
.with_context(|| format!("The live {platform} {arch} manifest is not an object"))?;
|
||||||
|
let shell_version = live_shell_version(live)
|
||||||
|
.with_context(|| format!("The live {platform} {arch} manifest names no shell version"))?
|
||||||
|
.to_string();
|
||||||
|
let modules = desktop_channel_manifest_entries(channel, packed)
|
||||||
|
.into_iter()
|
||||||
|
.map(|(module, mut entry)| {
|
||||||
|
entry.minimum_shell_version = shell_version.clone();
|
||||||
|
(module, entry)
|
||||||
|
})
|
||||||
|
.collect::<BTreeMap<_, _>>();
|
||||||
|
manifest.insert(
|
||||||
|
"manifest_version".to_string(),
|
||||||
|
json!(DESKTOP_CHANNEL_MANIFEST_VERSION),
|
||||||
|
);
|
||||||
|
manifest.insert("release_channel".to_string(), json!(channel));
|
||||||
|
manifest.insert("platform".to_string(), json!(platform));
|
||||||
|
manifest.insert("arch".to_string(), json!(arch));
|
||||||
|
manifest.insert("build_version".to_string(), json!(build_version));
|
||||||
|
manifest.insert("pub_date".to_string(), json!(pub_date));
|
||||||
|
manifest.insert("metadata_version".to_string(), json!(metadata_version));
|
||||||
|
manifest.insert("modules".to_string(), serde_json::to_value(modules)?);
|
||||||
|
manifest.insert(
|
||||||
|
"required_modules".to_string(),
|
||||||
|
json!(DESKTOP_REQUIRED_MODULES),
|
||||||
|
);
|
||||||
|
Ok(Value::Object(manifest))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_modules_only_manifest_step() -> Result<()> {
|
||||||
|
let channel = require_env("CHANNEL")?;
|
||||||
|
let build_version = require_env("VERSION")?;
|
||||||
|
let pub_date = require_env("PUB_DATE")?;
|
||||||
|
let modules_dir = desktop_modules_dir();
|
||||||
|
let live_path = modules_dir.join(DESKTOP_LIVE_SHELL_FILE);
|
||||||
|
let live: DesktopLiveShell =
|
||||||
|
serde_json::from_slice(&fs::read(&live_path).with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Failed to read {}, run the check_shell_drift step first",
|
||||||
|
live_path.display()
|
||||||
|
)
|
||||||
|
})?)
|
||||||
|
.with_context(|| format!("Failed to parse {}", live_path.display()))?;
|
||||||
|
ensure!(
|
||||||
|
live.channel == channel,
|
||||||
|
"{} describes the {} channel, not {channel}",
|
||||||
|
live_path.display(),
|
||||||
|
live.channel
|
||||||
|
);
|
||||||
|
let payload_root = Path::new("payload_tree").join(DESKTOP_PAYLOAD_PREFIX);
|
||||||
|
remove_dir_if_exists(Path::new("payload_tree"))?;
|
||||||
|
fs::create_dir_all(&payload_root)
|
||||||
|
.with_context(|| format!("Failed to create {}", payload_root.display()))?;
|
||||||
|
let packed = read_packed_desktop_modules(&modules_dir, &channel, &build_version)?;
|
||||||
|
let metadata_version = desktop_module_metadata_version(&pub_date)?;
|
||||||
|
stage_desktop_module_packages(&payload_root, &channel, &packed)?;
|
||||||
|
for (platform, arch) in desktop_release_coordinates() {
|
||||||
|
let key = desktop_coordinate_key(platform, arch);
|
||||||
|
let live_manifest = live
|
||||||
|
.manifests
|
||||||
|
.get(&key)
|
||||||
|
.with_context(|| format!("{} holds no live {key} manifest", live_path.display()))?;
|
||||||
|
let manifest = modules_only_channel_manifest(
|
||||||
|
live_manifest,
|
||||||
|
&channel,
|
||||||
|
platform,
|
||||||
|
arch,
|
||||||
|
&build_version,
|
||||||
|
&pub_date,
|
||||||
|
metadata_version,
|
||||||
|
&packed,
|
||||||
|
)?;
|
||||||
|
let manifest_path = desktop_channel_manifest_path(&payload_root, &channel, platform, arch);
|
||||||
|
if let Some(parent) = manifest_path.parent() {
|
||||||
|
fs::create_dir_all(parent)
|
||||||
|
.with_context(|| format!("Failed to create {}", parent.display()))?;
|
||||||
|
}
|
||||||
|
write_desktop_channel_manifest_bytes(
|
||||||
|
&manifest_path,
|
||||||
|
platform,
|
||||||
|
arch,
|
||||||
|
serde_json::to_vec(&manifest)?,
|
||||||
|
)?;
|
||||||
|
println!(
|
||||||
|
"Wrote the {platform} {arch} modules-only manifest for shell {}",
|
||||||
|
live.shell_version
|
||||||
|
);
|
||||||
|
}
|
||||||
|
println!(
|
||||||
|
"Staged {} module(s) for {build_version} on the live {channel} shell {}",
|
||||||
|
packed.len(),
|
||||||
|
live.shell_version
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepare_modules_release_assets_step() -> Result<()> {
|
||||||
|
let channel = require_env("CHANNEL")?;
|
||||||
|
let version = require_env("VERSION")?;
|
||||||
|
let source_sha = require_env("SOURCE_SHA")?;
|
||||||
|
let product = desktop_release_product(&channel)?;
|
||||||
|
let payload_root = Path::new("payload_tree")
|
||||||
|
.join(DESKTOP_PAYLOAD_PREFIX)
|
||||||
|
.join(&channel);
|
||||||
|
let release_assets = Path::new("release_assets");
|
||||||
|
remove_dir_if_exists(release_assets)?;
|
||||||
|
fs::create_dir_all(release_assets)?;
|
||||||
|
let mut release_builder =
|
||||||
|
DesktopReleaseAssetBuilder::new(&channel, &version, product, release_assets);
|
||||||
|
add_desktop_module_release_assets(&mut release_builder, &payload_root)?;
|
||||||
|
let (descriptor_assets, mut descriptor_modules) = release_builder.finish();
|
||||||
|
ensure!(
|
||||||
|
descriptor_assets.is_empty(),
|
||||||
|
"A modules-only release staged shell assets"
|
||||||
|
);
|
||||||
|
descriptor_modules.sort_by(|left, right| left.storage_key.cmp(&right.storage_key));
|
||||||
|
let descriptor = DesktopReleaseDescriptor {
|
||||||
|
schema_version: DESKTOP_RELEASE_DESCRIPTOR_SCHEMA_VERSION,
|
||||||
|
kind: DesktopReleaseKind::Modules,
|
||||||
|
channel: channel.clone(),
|
||||||
|
version: version.clone(),
|
||||||
|
release_tag: format!("fluxer-desktop-{channel}@{version}"),
|
||||||
|
source_sha,
|
||||||
|
assets: Vec::new(),
|
||||||
|
modules: descriptor_modules,
|
||||||
|
};
|
||||||
|
validate_desktop_release_descriptor(&descriptor, &channel, &version, &descriptor.source_sha)?;
|
||||||
|
validate_desktop_release_module_files(&descriptor, release_assets)?;
|
||||||
|
let descriptor_path =
|
||||||
|
release_assets.join(desktop_release_descriptor_filename(&channel, &version)?);
|
||||||
|
write_json_pretty(&descriptor_path, &descriptor)?;
|
||||||
|
println!("GitHub release asset tree:");
|
||||||
|
print_tree(release_assets, 2)
|
||||||
|
}
|
||||||
|
|
||||||
fn build_summary_step() -> Result<()> {
|
fn build_summary_step() -> Result<()> {
|
||||||
let summary = require_env("GITHUB_STEP_SUMMARY")?;
|
let summary = require_env("GITHUB_STEP_SUMMARY")?;
|
||||||
let display_channel = env::var("DISPLAY_CHANNEL").unwrap_or_default();
|
let display_channel = env::var("DISPLAY_CHANNEL").unwrap_or_default();
|
||||||
@@ -5592,6 +5940,136 @@ mod tests {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn live_manifest(platform: &str, arch: &str, shell: &str) -> Value {
|
||||||
|
json!({
|
||||||
|
"manifest_version": 1,
|
||||||
|
"release_channel": "canary",
|
||||||
|
"platform": platform,
|
||||||
|
"arch": arch,
|
||||||
|
"build_version": shell,
|
||||||
|
"pub_date": "2026-10-08T03:23:23Z",
|
||||||
|
"metadata_version": 1791433225,
|
||||||
|
"shell": {"latest_version": shell, "minimum_version": "0.0.0"},
|
||||||
|
"linux_security_minimum": {"version": "2026.1001.1", "required_modules": ["fluxer_renderer"]},
|
||||||
|
"modules": {},
|
||||||
|
"required_modules": ["fluxer_renderer"],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn live_shell_version_must_agree_across_coordinates() {
|
||||||
|
let mut manifests = BTreeMap::new();
|
||||||
|
for (platform, arch) in desktop_release_coordinates() {
|
||||||
|
manifests.insert(
|
||||||
|
desktop_coordinate_key(platform, arch),
|
||||||
|
live_manifest(platform, arch, "2026.1008.32323"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assert_eq!(
|
||||||
|
common_live_shell_version(&manifests).unwrap(),
|
||||||
|
"2026.1008.32323"
|
||||||
|
);
|
||||||
|
manifests.insert(
|
||||||
|
"linux/arm64".to_string(),
|
||||||
|
live_manifest("linux", "arm64", "2026.1008.12411"),
|
||||||
|
);
|
||||||
|
let error = common_live_shell_version(&manifests)
|
||||||
|
.unwrap_err()
|
||||||
|
.to_string();
|
||||||
|
assert!(error.contains("disagree"), "{error}");
|
||||||
|
manifests.insert("linux/arm64".to_string(), json!({"shell": {}}));
|
||||||
|
assert!(common_live_shell_version(&manifests).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn shell_drift_counts_only_shell_sources() {
|
||||||
|
let roots = vec![
|
||||||
|
"fluxer_desktop/".to_string(),
|
||||||
|
"packages/desktop_ipc/".to_string(),
|
||||||
|
];
|
||||||
|
let changed = [
|
||||||
|
"fluxer_app/src/App.tsx",
|
||||||
|
"fluxer_desktop/src/main/Updater.ts",
|
||||||
|
"fluxer_desktop/src/main/Updater.test.mjs",
|
||||||
|
"packages/desktop_ipc/src/Channels.ts",
|
||||||
|
"packages/desktop_ipc/README.md",
|
||||||
|
"packages/schema/src/Thing.ts",
|
||||||
|
]
|
||||||
|
.map(str::to_string);
|
||||||
|
assert_eq!(
|
||||||
|
desktop_shell_drift(&changed, &roots),
|
||||||
|
vec![
|
||||||
|
"fluxer_desktop/src/main/Updater.ts".to_string(),
|
||||||
|
"packages/desktop_ipc/src/Channels.ts".to_string(),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn shell_source_roots_follow_the_desktop_workspace_dependencies() {
|
||||||
|
let repo = tempfile::tempdir().unwrap();
|
||||||
|
let root = repo.path();
|
||||||
|
fs::create_dir_all(root.join("fluxer_desktop")).unwrap();
|
||||||
|
fs::write(
|
||||||
|
root.join("fluxer_desktop/package.json"),
|
||||||
|
r#"{"dependencies":{"@fluxer/desktop_ipc":"workspace:*","electron-log":"5.0.0"}}"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
for (dir, name) in [
|
||||||
|
("desktop_ipc", "@fluxer/desktop_ipc"),
|
||||||
|
("schema", "@fluxer/schema"),
|
||||||
|
] {
|
||||||
|
fs::create_dir_all(root.join("packages").join(dir)).unwrap();
|
||||||
|
fs::write(
|
||||||
|
root.join("packages").join(dir).join("package.json"),
|
||||||
|
format!(r#"{{"name":"{name}"}}"#),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
assert_eq!(
|
||||||
|
desktop_shell_source_roots(root).unwrap(),
|
||||||
|
vec![
|
||||||
|
"fluxer_desktop/".to_string(),
|
||||||
|
"packages/desktop_ipc/".to_string()
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn modules_only_manifest_keeps_the_live_shell_and_pins_modules_to_it() {
|
||||||
|
let live = live_manifest("darwin", "arm64", "2026.1008.32323");
|
||||||
|
let packed = vec![DesktopPackedModule {
|
||||||
|
module: "fluxer_renderer".to_string(),
|
||||||
|
sha256: "a".repeat(64),
|
||||||
|
bytes: 42,
|
||||||
|
directory: PathBuf::from("unused"),
|
||||||
|
}];
|
||||||
|
let manifest = modules_only_channel_manifest(
|
||||||
|
&live,
|
||||||
|
"canary",
|
||||||
|
"darwin",
|
||||||
|
"arm64",
|
||||||
|
"2026.1008.90000",
|
||||||
|
"2026-10-08T09:00:00Z",
|
||||||
|
1791450000,
|
||||||
|
&packed,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(manifest["shell"], live["shell"]);
|
||||||
|
assert_eq!(
|
||||||
|
manifest["linux_security_minimum"],
|
||||||
|
live["linux_security_minimum"]
|
||||||
|
);
|
||||||
|
assert_eq!(manifest["build_version"], "2026.1008.90000");
|
||||||
|
assert_eq!(manifest["metadata_version"], 1791450000);
|
||||||
|
let renderer = &manifest["modules"]["fluxer_renderer"];
|
||||||
|
assert_eq!(renderer["sha256"], "a".repeat(64));
|
||||||
|
assert_eq!(renderer["minimum_shell_version"], "2026.1008.32323");
|
||||||
|
assert!(renderer["maximum_shell_version"].is_null());
|
||||||
|
let parsed: DesktopChannelManifest = serde_json::from_value(manifest).unwrap();
|
||||||
|
assert_eq!(parsed.shell.latest_version, "2026.1008.32323");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn bare_drive_workdir_resolves_to_the_drive_root() {
|
fn bare_drive_workdir_resolves_to_the_drive_root() {
|
||||||
assert_eq!(workdir_path("W:"), PathBuf::from("W:\\"));
|
assert_eq!(workdir_path("W:"), PathBuf::from("W:\\"));
|
||||||
@@ -6476,6 +6954,7 @@ export const CHANNEL_DISPLAY_NAME = BUILD_CHANNEL;\n"
|
|||||||
assert_eq!(on_disk, names);
|
assert_eq!(on_disk, names);
|
||||||
let descriptor = DesktopReleaseDescriptor {
|
let descriptor = DesktopReleaseDescriptor {
|
||||||
schema_version: DESKTOP_RELEASE_DESCRIPTOR_SCHEMA_VERSION,
|
schema_version: DESKTOP_RELEASE_DESCRIPTOR_SCHEMA_VERSION,
|
||||||
|
kind: DesktopReleaseKind::Full,
|
||||||
channel: "canary".to_string(),
|
channel: "canary".to_string(),
|
||||||
version: LAYOUT_VERSION.to_string(),
|
version: LAYOUT_VERSION.to_string(),
|
||||||
release_tag: format!("fluxer-desktop-canary@{LAYOUT_VERSION}"),
|
release_tag: format!("fluxer-desktop-canary@{LAYOUT_VERSION}"),
|
||||||
|
|||||||
@@ -148,9 +148,25 @@ pub(crate) struct DesktopReleaseAsset {
|
|||||||
pub(crate) size: u64,
|
pub(crate) size: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub(crate) enum DesktopReleaseKind {
|
||||||
|
#[default]
|
||||||
|
Full,
|
||||||
|
Modules,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DesktopReleaseKind {
|
||||||
|
pub(crate) fn is_full(&self) -> bool {
|
||||||
|
*self == Self::Full
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
|
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
|
||||||
pub(crate) struct DesktopReleaseDescriptor {
|
pub(crate) struct DesktopReleaseDescriptor {
|
||||||
pub(crate) schema_version: u8,
|
pub(crate) schema_version: u8,
|
||||||
|
#[serde(default, skip_serializing_if = "DesktopReleaseKind::is_full")]
|
||||||
|
pub(crate) kind: DesktopReleaseKind,
|
||||||
pub(crate) channel: String,
|
pub(crate) channel: String,
|
||||||
pub(crate) version: String,
|
pub(crate) version: String,
|
||||||
pub(crate) release_tag: String,
|
pub(crate) release_tag: String,
|
||||||
@@ -334,6 +350,24 @@ pub(crate) fn validate_desktop_release_descriptor(
|
|||||||
);
|
);
|
||||||
parse_version_instant(version)
|
parse_version_instant(version)
|
||||||
.with_context(|| format!("Invalid desktop release descriptor version {version:?}"))?;
|
.with_context(|| format!("Invalid desktop release descriptor version {version:?}"))?;
|
||||||
|
if descriptor.kind == DesktopReleaseKind::Modules {
|
||||||
|
ensure!(
|
||||||
|
descriptor.assets.is_empty(),
|
||||||
|
"A modules-only desktop release descriptor must not list shell assets, found {}",
|
||||||
|
descriptor.assets.len()
|
||||||
|
);
|
||||||
|
let descriptor_name = desktop_release_descriptor_filename(channel, version)?;
|
||||||
|
let mut release_asset_names = BTreeMap::from([(
|
||||||
|
descriptor_name.to_ascii_lowercase(),
|
||||||
|
descriptor_name.as_str(),
|
||||||
|
)]);
|
||||||
|
return validate_desktop_release_modules(
|
||||||
|
descriptor,
|
||||||
|
channel,
|
||||||
|
version,
|
||||||
|
&mut release_asset_names,
|
||||||
|
);
|
||||||
|
}
|
||||||
let route_count = desktop_release_route_count();
|
let route_count = desktop_release_route_count();
|
||||||
ensure!(
|
ensure!(
|
||||||
descriptor.assets.len() == route_count,
|
descriptor.assets.len() == route_count,
|
||||||
@@ -1465,6 +1499,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
DesktopReleaseDescriptor {
|
DesktopReleaseDescriptor {
|
||||||
schema_version: DESKTOP_RELEASE_DESCRIPTOR_SCHEMA_VERSION,
|
schema_version: DESKTOP_RELEASE_DESCRIPTOR_SCHEMA_VERSION,
|
||||||
|
kind: DesktopReleaseKind::Full,
|
||||||
channel: SAMPLE_CHANNEL.to_string(),
|
channel: SAMPLE_CHANNEL.to_string(),
|
||||||
version: SAMPLE_VERSION.to_string(),
|
version: SAMPLE_VERSION.to_string(),
|
||||||
release_tag: format!("fluxer-desktop-{SAMPLE_CHANNEL}@{SAMPLE_VERSION}"),
|
release_tag: format!("fluxer-desktop-{SAMPLE_CHANNEL}@{SAMPLE_VERSION}"),
|
||||||
@@ -1556,6 +1591,50 @@ mod tests {
|
|||||||
descriptor
|
descriptor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn sample_modules_only_descriptor() -> DesktopReleaseDescriptor {
|
||||||
|
let mut descriptor = sample_descriptor();
|
||||||
|
descriptor.kind = DesktopReleaseKind::Modules;
|
||||||
|
descriptor.assets.clear();
|
||||||
|
descriptor
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn modules_only_descriptor_validates_without_shell_assets() {
|
||||||
|
validate_sample(&sample_modules_only_descriptor()).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn modules_only_descriptor_rejects_shell_assets() {
|
||||||
|
let mut descriptor = sample_modules_only_descriptor();
|
||||||
|
descriptor.assets = sample_descriptor().assets;
|
||||||
|
let error = validate_sample(&descriptor).unwrap_err().to_string();
|
||||||
|
assert!(error.contains("must not list shell assets"), "{error}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn modules_only_descriptor_still_requires_every_coordinate_and_the_renderer() {
|
||||||
|
let mut descriptor = sample_modules_only_descriptor();
|
||||||
|
descriptor
|
||||||
|
.modules
|
||||||
|
.retain(|entry| !entry.storage_key.contains("/fluxer_renderer/"));
|
||||||
|
assert!(validate_sample(&descriptor).is_err());
|
||||||
|
let mut descriptor = sample_modules_only_descriptor();
|
||||||
|
descriptor
|
||||||
|
.modules
|
||||||
|
.retain(|entry| !entry.storage_key.starts_with("desktop/canary/linux/arm64/"));
|
||||||
|
assert!(validate_sample(&descriptor).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn full_descriptor_omits_the_kind_and_modules_only_carries_it() {
|
||||||
|
let full = serde_json::to_value(sample_descriptor()).unwrap();
|
||||||
|
assert!(full.get("kind").is_none());
|
||||||
|
let modules_only = serde_json::to_value(sample_modules_only_descriptor()).unwrap();
|
||||||
|
assert_eq!(modules_only["kind"], "modules");
|
||||||
|
let parsed: DesktopReleaseDescriptor = serde_json::from_value(full).unwrap();
|
||||||
|
assert_eq!(parsed.kind, DesktopReleaseKind::Full);
|
||||||
|
}
|
||||||
|
|
||||||
fn validate_sample(descriptor: &DesktopReleaseDescriptor) -> Result<()> {
|
fn validate_sample(descriptor: &DesktopReleaseDescriptor) -> Result<()> {
|
||||||
validate_desktop_release_descriptor(
|
validate_desktop_release_descriptor(
|
||||||
descriptor,
|
descriptor,
|
||||||
|
|||||||
Reference in new issue
Block a user