From 26fba27466aab0c1ef59458b4d3b0665e4097004 Mon Sep 17 00:00:00 2001 From: HsiangNianian Date: Tue, 4 Aug 2026 12:57:46 +0800 Subject: [PATCH 1/6] feat(release): add RC1 parity evidence gate --- .github/workflows/prek.yml | 3 + .github/workflows/semifold-ci.yaml | 13 + .gitignore | 2 + docs/release/rc1-parity.json | 430 ++++++++++++++++++ package.json | 1 + .../docs/content/en/manual/releases/meta.json | 2 +- .../docs/content/en/manual/releases/rc-1.mdx | 56 +++ .../docs/content/zh/manual/releases/meta.json | 2 +- .../docs/content/zh/manual/releases/rc-1.mdx | 56 +++ scripts/parity-audit.test.mjs | 123 +++++ scripts/release-workflow.test.mjs | 35 ++ scripts/smoke-install-linux.sh | 39 ++ scripts/smoke-install-macos.sh | 47 ++ scripts/smoke-install-windows.ps1 | 48 ++ 14 files changed, 855 insertions(+), 2 deletions(-) create mode 100644 docs/release/rc1-parity.json create mode 100644 packages/docs/content/en/manual/releases/rc-1.mdx create mode 100644 packages/docs/content/zh/manual/releases/rc-1.mdx create mode 100644 scripts/parity-audit.test.mjs create mode 100755 scripts/smoke-install-linux.sh create mode 100755 scripts/smoke-install-macos.sh create mode 100644 scripts/smoke-install-windows.ps1 diff --git a/.github/workflows/prek.yml b/.github/workflows/prek.yml index 14b665dd..2eb7be69 100644 --- a/.github/workflows/prek.yml +++ b/.github/workflows/prek.yml @@ -62,6 +62,9 @@ jobs: - name: Test release workflow contract run: pnpm test:release + - name: Test RC1 parity evidence contract + run: pnpm test:parity + - name: Check for changes id: check_changes if: steps.prek.outcome == 'failure' diff --git a/.github/workflows/semifold-ci.yaml b/.github/workflows/semifold-ci.yaml index 34c08902..60cfc5e2 100644 --- a/.github/workflows/semifold-ci.yaml +++ b/.github/workflows/semifold-ci.yaml @@ -154,6 +154,19 @@ jobs: fi working-directory: target/release + - name: Smoke isolated install (Linux) + if: runner.os == 'Linux' + run: scripts/smoke-install-linux.sh "${{ matrix.target }}" + + - name: Smoke isolated install (macOS) + if: runner.os == 'macOS' + run: scripts/smoke-install-macos.sh "${{ matrix.target }}" + + - name: Smoke isolated install (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: scripts/smoke-install-windows.ps1 -Target "${{ matrix.target }}" + - name: Upload Artifact (Linux) if: runner.os == 'Linux' uses: actions/upload-artifact@v6 diff --git a/.gitignore b/.gitignore index afaee8f3..8b18307e 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,8 @@ artifacts/ # AUR Release release/ +!docs/release/ +!docs/release/rc1-parity.json # claude code .claude/ diff --git a/docs/release/rc1-parity.json b/docs/release/rc1-parity.json new file mode 100644 index 00000000..fab16d08 --- /dev/null +++ b/docs/release/rc1-parity.json @@ -0,0 +1,430 @@ +{ + "schemaVersion": 1, + "release": "0.2.0-rc.1", + "sourceIssue": 178, + "rows": [ + { + "id": "ui-onboarding", + "area": "ui-ux", + "requirement": "First launch provides a persisted, localized guided tour of navigation, accounts, instances, and launch controls.", + "status": "proven", + "evidence": [ + { "kind": "code", "path": "packages/ui/src/components/guided-tour.tsx", "match": "export function GuidedTour", "detail": "First-launch tour and persisted completion flow." }, + { "kind": "test", "path": "src-tauri/src/core/config.rs", "match": "first_launch_completed", "detail": "Persisted first-launch state and safe defaults." } + ] + }, + { + "id": "ui-home-launch", + "area": "ui-ux", + "requirement": "Home presents one state-aware launch, install, repair, reveal-download, or stop action.", + "status": "proven", + "evidence": [ + { "kind": "code", "path": "packages/ui/src/pages/home.tsx", "match": "export function HomePage", "detail": "State-aware launcher home implementation." }, + { "kind": "test", "path": "packages/ui/tests/launcher-fixtures.spec.ts", "match": "home primary action follows launcher recovery state", "detail": "Deterministic recovery-state UI regression." } + ] + }, + { + "id": "ui-instance-library", + "area": "ui-ux", + "requirement": "The instance library handles empty, loading, error, repair, search, sort, grid, list, and active-selection states.", + "status": "proven", + "evidence": [ + { "kind": "code", "path": "packages/ui/src/pages/instances/index.tsx", "match": "export function InstancesPage", "detail": "Scalable instance library implementation." }, + { "kind": "test", "path": "packages/ui/tests/launcher-fixtures.spec.ts", "match": "large instance library supports search, sorting, views, and active selection", "detail": "Large-library interaction regression." } + ] + }, + { + "id": "ui-instance-creation", + "area": "ui-ux", + "requirement": "Creation guides version, loader, metadata, validation, cancellation, and progress as one staged task.", + "status": "proven", + "evidence": [ + { "kind": "code", "path": "packages/ui/src/pages/instances/create.tsx", "match": "export function CreateInstancePage", "detail": "Staged vanilla, Fabric, and Forge instance creation." }, + { "kind": "fixture", "path": "packages/ui/src/fixtures/launcher.ts", "match": "case \"install_version\"", "detail": "Deterministic version-install backend fixture." } + ] + }, + { + "id": "ui-content-browsing", + "area": "ui-ux", + "requirement": "Content browsing supports search, type, loader, sorting, pagination, project versions, and installation feedback.", + "status": "proven", + "evidence": [ + { "kind": "code", "path": "packages/ui/src/pages/instances/browse.tsx", "match": "export default function BrowsePage", "detail": "Search and install surface for mods and packs." }, + { "kind": "code", "path": "src-tauri/src/core/content_search.rs", "match": "pub async fn search", "detail": "Backend content search and compatible version resolution." } + ] + }, + { + "id": "ui-downloads", + "area": "ui-ux", + "requirement": "Downloads expose bounded progress, active work, failures, and recovery actions without duplicate starts.", + "status": "proven", + "evidence": [ + { "kind": "code", "path": "packages/ui/src/components/download-monitor.tsx", "match": "export function DownloadMonitor", "detail": "Accessible global download monitor." }, + { "kind": "test", "path": "packages/ui/tests/launcher-fixtures.spec.ts", "match": "download and failure states expose actionable diagnostics", "detail": "Progress and failure-state regression." } + ] + }, + { + "id": "ui-accounts", + "area": "ui-ux", + "requirement": "Accounts provide Microsoft device login, offline login, active identity, and recoverable error states.", + "status": "proven", + "evidence": [ + { "kind": "code", "path": "packages/ui/src/components/login-modal.tsx", "match": "export function LoginModal", "detail": "Microsoft and offline login UX." }, + { "kind": "code", "path": "src-tauri/src/core/auth.rs", "match": "DeviceCodeResponse", "detail": "Microsoft device-code authentication flow." } + ] + }, + { + "id": "ui-settings", + "area": "ui-ux", + "requirement": "Settings cover general, appearance, runtime, memory, tray, updates, and validated advanced configuration.", + "status": "proven", + "evidence": [ + { "kind": "code", "path": "packages/ui/src/pages/settings.tsx", "match": "export function SettingsPage", "detail": "Primary settings tabs and immediate-save controls." }, + { "kind": "test", "path": "packages/ui/tests/launcher-fixtures.spec.ts", "match": "configuration studio validates, saves, and protects unsaved changes", "detail": "Advanced settings validation and unsaved-change protection." } + ] + }, + { + "id": "ui-logs", + "area": "ui-ux", + "requirement": "Launch failures and logs remain inspectable through direct file access and redacted diagnostic evidence.", + "status": "proven", + "evidence": [ + { "kind": "code", "path": "packages/ui/src/pages/home.tsx", "match": "openFileExplorer(`${activeInstance.gameDir}/logs`)", "detail": "Direct access to the active instance log directory." }, + { "kind": "test", "path": "packages/ui/tests/launcher-fixtures.spec.ts", "match": "failed launch can open a consent-gated assistant diagnosis", "detail": "Failure-to-diagnostics handoff regression." } + ] + }, + { + "id": "ui-errors", + "area": "ui-ux", + "requirement": "Loading, validation, download, launch, and instance-index errors expose a next action and diagnostics.", + "status": "proven", + "evidence": [ + { "kind": "code", "path": "packages/ui/src/lib/launch-readiness.ts", "match": "export function", "detail": "Central launch recovery-state mapping." }, + { "kind": "test", "path": "packages/ui/tests/launcher-fixtures.spec.ts", "match": "instance library exposes deterministic loading and repair states", "detail": "Loading, failure, retry, and repair regression." } + ] + }, + { + "id": "ui-updates", + "area": "ui-ux", + "requirement": "Automatic and manual update checks show release notes and bounded installation progress.", + "status": "proven", + "evidence": [ + { "kind": "code", "path": "packages/ui/src/components/updater.tsx", "match": "export function UpdateDialog", "detail": "Update check, notes, download, and install dialog." }, + { "kind": "code", "path": "packages/ui/src/pages/settings.tsx", "match": "updater.checkForUpdate(false)", "detail": "Manual update control and current version surface." } + ] + }, + { + "id": "ui-accessibility", + "area": "ui-ux", + "requirement": "Core flows are keyboard operable, labelled, focus visible, localized, and reduced-motion aware.", + "status": "proven", + "evidence": [ + { "kind": "test", "path": "packages/ui/tests/launcher-fixtures.spec.ts", "match": "primary launcher flow is keyboard operable", "detail": "Keyboard and accessible-name regression." }, + { "kind": "test", "path": "packages/ui/tests/launcher-fixtures.spec.ts", "match": "home supports Chinese and reduced motion", "detail": "Localization and reduced-motion regression." } + ] + }, + { + "id": "ui-performance", + "area": "ui-ux", + "requirement": "The launcher remains usable with a larger library and suppresses nonessential animation under reduced motion.", + "status": "proven", + "evidence": [ + { "kind": "test", "path": "packages/ui/tests/launcher-fixtures.spec.ts", "match": "large instance library supports search, sorting, views, and active selection", "detail": "Representative larger-library rendering and interaction check." }, + { "kind": "code", "path": "packages/ui/src/index.css", "match": "@media (prefers-reduced-motion: reduce)", "detail": "Global transitions and animations respect reduced-motion preference." } + ] + }, + { + "id": "function-authentication", + "area": "functionality", + "requirement": "Microsoft and offline identities authenticate and persist without storing Microsoft passwords.", + "status": "proven", + "evidence": [ + { "kind": "code", "path": "src-tauri/src/core/auth.rs", "match": "pub async fn", "detail": "Microsoft OAuth device-code implementation." }, + { "kind": "code", "path": "src-tauri/src/core/account_storage.rs", "match": "pub struct AccountStorage", "detail": "Account persistence and active-identity storage." } + ] + }, + { + "id": "function-java-runtime", + "area": "functionality", + "requirement": "Java discovery, validation, selection, download, persistence, and launch compatibility are implemented.", + "status": "proven", + "evidence": [ + { "kind": "code", "path": "src-tauri/src/core/java/mod.rs", "match": "download_and_install_java", "detail": "Managed runtime download and installation." }, + { "kind": "test", "path": "packages/ui/tests/launcher-fixtures.spec.ts", "match": "Java runtime download progress stays bounded and accessible", "detail": "Runtime progress UI regression." } + ] + }, + { + "id": "function-loaders", + "area": "functionality", + "requirement": "Vanilla, Fabric, and Forge profiles install with inherited-version metadata and compatible launch arguments.", + "status": "proven", + "evidence": [ + { "kind": "code", "path": "src-tauri/src/core/fabric.rs", "match": "pub async fn install_fabric", "detail": "Fabric profile installation." }, + { "kind": "code", "path": "src-tauri/src/core/forge.rs", "match": "pub async fn install_forge", "detail": "Forge profile installation." } + ] + }, + { + "id": "function-mods-packs", + "area": "functionality", + "requirement": "Mods, modpacks, resource packs, shader packs, data packs, and plugins can be discovered and managed.", + "status": "proven", + "evidence": [ + { "kind": "code", "path": "src-tauri/src/core/mods.rs", "match": "pub fn", "detail": "Local mod listing and enable-state management." }, + { "kind": "test", "path": "src-tauri/src/core/modpack/api.rs", "match": "#[cfg(test)]", "detail": "Archive detection and import coverage." } + ] + }, + { + "id": "function-instances", + "area": "functionality", + "requirement": "Instances support create, update, delete, active selection, launch, import, export, and metadata persistence.", + "status": "proven", + "evidence": [ + { "kind": "code", "path": "src-tauri/src/core/instance.rs", "match": "pub struct InstanceState", "detail": "Instance lifecycle and persistence implementation." }, + { "kind": "test", "path": "packages/ui/tests/launcher-fixtures.spec.ts", "match": "instance library scales from empty to one active workspace", "detail": "Primary instance lifecycle UI regression." } + ] + }, + { + "id": "function-repairs", + "area": "functionality", + "requirement": "Missing runtime files and damaged instance indexes expose bounded repair flows.", + "status": "proven", + "evidence": [ + { "kind": "code", "path": "src-tauri/src/main.rs", "match": "async fn repair_instances", "detail": "Backend instance-index repair command." }, + { "kind": "test", "path": "packages/ui/tests/launcher-fixtures.spec.ts", "match": "critical memory pressure blocks launch with a repair action", "detail": "Pre-launch recovery action regression." } + ] + }, + { + "id": "function-exports", + "area": "functionality", + "requirement": "Instance export produces a portable archive without mutating the source instance.", + "status": "proven", + "evidence": [ + { "kind": "code", "path": "src-tauri/src/core/instance.rs", "match": "pub fn export_instance", "detail": "Copy-only instance archive export." }, + { "kind": "api", "path": "src-tauri/src/main.rs", "match": "async fn export_instance", "detail": "Typed desktop export command." } + ] + }, + { + "id": "function-background-lifecycle", + "area": "functionality", + "requirement": "System tray, close-to-tray, start minimized, download status, game status, and quit behavior are coherent.", + "status": "proven", + "evidence": [ + { "kind": "code", "path": "src-tauri/src/desktop.rs", "match": "setup_system_tray", "detail": "Native background lifecycle and tray menu." }, + { "kind": "test", "path": "packages/ui/tests/launcher-fixtures.spec.ts", "match": "system tray lifecycle settings are progressive and save immediately", "detail": "Tray configuration and recovery regression." } + ] + }, + { + "id": "function-assistant", + "area": "functionality", + "requirement": "Diagnostics are consent gated, redacted, bounded, cancellable, and isolated between requests.", + "status": "proven", + "evidence": [ + { "kind": "code", "path": "src-tauri/src/core/assistant.rs", "match": "pub struct Assistant", "detail": "Redaction and bounded provider request implementation." }, + { "kind": "test", "path": "packages/ui/tests/launcher-fixtures.spec.ts", "match": "clearing a streaming answer cannot leak stale chunks into the next request", "detail": "Request-isolation regression." } + ] + }, + { + "id": "function-conversion", + "area": "functionality", + "requirement": "Minecraft and loader conversion is preview first, compatibility checked, source preserving, and reversible.", + "status": "proven", + "evidence": [ + { "kind": "code", "path": "src-tauri/src/core/content_conversion.rs", "match": "pub struct ConversionPreview", "detail": "Protected conversion plan, apply, report, and rollback implementation." }, + { "kind": "test", "path": "packages/ui/tests/launcher-fixtures.spec.ts", "match": "content conversion previews a protected copy and supports rollback", "detail": "End-to-end conversion wizard regression." } + ] + }, + { + "id": "function-platforms", + "area": "functionality", + "requirement": "Linux x86_64 and arm64, macOS x86_64 and arm64, Windows x86_64 and arm64, and Alpine musl are built.", + "status": "proven", + "evidence": [ + { "kind": "workflow", "path": ".github/workflows/semifold-ci.yaml", "match": "Build on Linux x86_64 (Musl)", "detail": "Seven-target release build matrix." }, + { "kind": "workflow", "path": ".github/workflows/test.yml", "match": "Test on ${{ matrix.name }}", "detail": "Cross-platform Rust regression matrix." } + ] + }, + { + "id": "migration-pcl", + "area": "migration", + "requirement": "PCL-compatible global and isolated Minecraft layouts are detected and copied with selected-version metadata.", + "status": "proven", + "evidence": [ + { "kind": "code", "path": "src-tauri/src/core/migration.rs", "match": "\"pcl\" => \"PCL\"", "detail": "PCL source classification and discovery." }, + { "kind": "test", "path": "packages/ui/tests/launcher-fixtures.spec.ts", "match": "PCL and HMCL isolated versions use the same reviewed migration flow", "detail": "PCL isolated-version review regression." } + ] + }, + { + "id": "migration-hmcl", + "area": "migration", + "requirement": "HMCL directories and exports preserve selected versions and report remote archive dependencies.", + "status": "proven", + "evidence": [ + { "kind": "fixture", "path": "src-tauri/tests/fixtures/migration/v1/archive-manifest.json", "match": "HMCL Export.zip", "detail": "Versioned HMCL archive fixture." }, + { "kind": "test", "path": "src-tauri/src/core/migration.rs", "match": "hmcl_curseforge_archives_report_remote_dependencies_before_launch", "detail": "HMCL dependency-report regression." } + ] + }, + { + "id": "migration-prism-multimc", + "area": "migration", + "requirement": "PrismLauncher and MultiMC metadata, loaders, icons, Java overrides, memory overrides, and game content are reviewed before copy.", + "status": "proven", + "evidence": [ + { "kind": "code", "path": "src-tauri/src/core/migration.rs", "match": "PrismLauncher/instances", "detail": "Cross-platform Prism discovery and metadata parsing." }, + { "kind": "test", "path": "packages/ui/tests/launcher-fixtures.spec.ts", "match": "migration manifest resolves conflicts, reports copied content, and supports undo", "detail": "Prism review and copy regression." } + ] + }, + { + "id": "migration-archives", + "area": "migration", + "requirement": "MultiMC, Modrinth, CurseForge/HMCL-PCL, and portable configuration/save archives are traversal safe.", + "status": "proven", + "evidence": [ + { "kind": "test", "path": "src-tauri/src/core/migration.rs", "match": "versioned_launcher_archive_fixture_matrix_stays_copy_only", "detail": "Versioned archive-format fixture matrix." }, + { "kind": "fixture", "path": "src-tauri/tests/fixtures/migration/v1/archive-manifest.json", "match": "portable-config-save", "detail": "Portable and launcher-specific archive cases." } + ] + }, + { + "id": "migration-preview", + "area": "migration", + "requirement": "Every source is summarized as a review manifest before any destination file is written.", + "status": "proven", + "evidence": [ + { "kind": "code", "path": "src-tauri/src/core/migration.rs", "match": "pub fn preview_import", "detail": "Read-only preview and compatibility plan." }, + { "kind": "code", "path": "packages/ui/src/components/import-wizard.tsx", "match": "export function ImportWizard", "detail": "Review-first migration wizard." } + ] + }, + { + "id": "migration-conflicts", + "area": "migration", + "requirement": "Name, path, loader, and archive conflicts are visible and require a bounded resolution.", + "status": "proven", + "evidence": [ + { "kind": "code", "path": "src-tauri/src/core/migration.rs", "match": "suggested_resolution", "detail": "Structured conflict detection and resolution hints." }, + { "kind": "test", "path": "packages/ui/tests/launcher-fixtures.spec.ts", "match": "migration manifest resolves conflicts", "detail": "Conflict-review UI regression." } + ] + }, + { + "id": "migration-rollback", + "area": "migration", + "requirement": "Cancellation stops remaining work and removes only the active operation destination; completed imports can be undone.", + "status": "proven", + "evidence": [ + { "kind": "api", "path": "src-tauri/src/main.rs", "match": "async fn rollback_launcher_import", "detail": "Operation-scoped migration rollback command." }, + { "kind": "test", "path": "packages/ui/tests/launcher-fixtures.spec.ts", "match": "migration cancellation stops the remaining queue", "detail": "Cancellation and queue-boundary regression." } + ] + }, + { + "id": "migration-source-preservation", + "area": "migration", + "requirement": "Source directories and archives remain byte-for-byte unchanged across preview, copy, cancellation, and rollback.", + "status": "proven", + "evidence": [ + { "kind": "test", "path": "src-tauri/src/core/migration.rs", "match": "assert_eq!(fs::read(&archive_path).unwrap(), source_before)", "detail": "Archive source fingerprint preservation assertions." }, + { "kind": "doc", "path": "packages/docs/content/en/manual/features/migration.mdx", "match": "byte-for-byte unchanged", "detail": "Published source-safety contract and fixture scope." } + ] + }, + { + "id": "distribution-installers", + "area": "distribution", + "requirement": "Release builds produce Linux AppImage/deb/rpm, Windows NSIS, macOS dmg/app archives, and Alpine musl archives.", + "status": "proven", + "evidence": [ + { "kind": "workflow", "path": ".github/workflows/semifold-ci.yaml", "match": "Upload Artifact (Linux)", "detail": "Platform artifact collection and upload." }, + { "kind": "config", "path": "src-tauri/tauri.conf.json", "match": "\"bundle\"", "detail": "Tauri packaging configuration." } + ] + }, + { + "id": "distribution-updates", + "area": "distribution", + "requirement": "Published desktop releases provide updater metadata and the launcher consumes it through HTTPS.", + "status": "proven", + "evidence": [ + { "kind": "config", "path": "src-tauri/tauri.conf.json", "match": "latest.json", "detail": "GitHub release updater endpoint." }, + { "kind": "code", "path": "packages/ui/src/components/updater.tsx", "match": "downloadAndInstall", "detail": "Update download and install flow." } + ] + }, + { + "id": "distribution-release-notes", + "area": "distribution", + "requirement": "Changesets generate package-specific release notes and GitHub prereleases from reviewed commits.", + "status": "proven", + "evidence": [ + { "kind": "config", "path": ".changes/config.toml", "match": "release = \"release\"", "detail": "Semifold release branch and prerelease channel configuration." }, + { "kind": "workflow", "path": ".github/workflows/semifold-ci.yaml", "match": "Semifold CI", "detail": "Automated version PR and release publication." } + ] + }, + { + "id": "distribution-provenance", + "area": "distribution", + "requirement": "Release artifacts are attested and documented with repository-bound verification commands.", + "status": "proven", + "evidence": [ + { "kind": "test", "path": "scripts/test-release-provenance.sh", "match": "gh attestation verify", "detail": "Release provenance contract." }, + { "kind": "workflow", "path": ".github/workflows/semifold-ci.yaml", "match": "uses: actions/attest@v4", "detail": "GitHub artifact attestation step." } + ] + }, + { + "id": "distribution-clean-install", + "area": "distribution", + "requirement": "Every supported installer is unpacked or silently installed in an isolated location before upload; musl executes in a clean Alpine runtime.", + "status": "proven", + "evidence": [ + { "kind": "workflow", "path": ".github/workflows/semifold-ci.yaml", "match": "Smoke isolated install (Windows)", "detail": "All desktop matrix jobs run isolated-install checks before upload." }, + { "kind": "test", "path": "scripts/release-workflow.test.mjs", "match": "smoke tests every packaged desktop artifact in an isolated install", "detail": "Ordering and platform coverage contract." }, + { "kind": "script", "path": "scripts/smoke-install-linux.sh", "match": "--appimage-extract", "detail": "Linux AppImage and Debian isolated extraction." }, + { "kind": "script", "path": "scripts/smoke-install-macos.sh", "match": "hdiutil attach", "detail": "macOS dmg mount, copy, plist, and Mach-O verification." }, + { "kind": "script", "path": "scripts/smoke-install-windows.ps1", "match": "Start-Process", "detail": "Windows NSIS silent install and PE verification." } + ] + }, + { + "id": "docs-install", + "area": "documentation", + "requirement": "English and Chinese installation, update, and artifact-verification instructions are current.", + "status": "proven", + "evidence": [ + { "kind": "doc", "locale": "en", "path": "packages/docs/content/en/manual/getting-started.mdx", "match": "gh attestation verify", "detail": "English install and verification guide." }, + { "kind": "doc", "locale": "zh", "path": "packages/docs/content/zh/manual/getting-started.mdx", "match": "gh attestation verify", "detail": "Chinese install and verification guide." } + ] + }, + { + "id": "docs-migration", + "area": "documentation", + "requirement": "English and Chinese migration guides describe supported launchers, archives, conflicts, rollback, and source safety.", + "status": "proven", + "evidence": [ + { "kind": "doc", "locale": "en", "path": "packages/docs/content/en/manual/features/migration.mdx", "match": "Prism Launcher", "detail": "English launcher migration guide." }, + { "kind": "doc", "locale": "zh", "path": "packages/docs/content/zh/manual/features/migration.mdx", "match": "Prism Launcher", "detail": "Chinese launcher migration guide." } + ] + }, + { + "id": "docs-troubleshooting", + "area": "documentation", + "requirement": "English and Chinese troubleshooting cover install, runtime, launch, loader, download, update, and diagnostics recovery.", + "status": "proven", + "evidence": [ + { "kind": "doc", "locale": "en", "path": "packages/docs/content/en/manual/troubleshooting.mdx", "match": "Troubleshooting", "detail": "English troubleshooting guide." }, + { "kind": "doc", "locale": "zh", "path": "packages/docs/content/zh/manual/troubleshooting.mdx", "match": "故障排除", "detail": "Chinese troubleshooting guide." } + ] + }, + { + "id": "docs-features", + "area": "documentation", + "requirement": "English and Chinese feature indexes link shipped authentication, runtime, loaders, instances, migration, assistant, tray, configuration, and conversion behavior.", + "status": "proven", + "evidence": [ + { "kind": "doc", "locale": "en", "path": "packages/docs/content/en/manual/features/index.mdx", "match": "Content Conversion", "detail": "English feature status and navigation." }, + { "kind": "doc", "locale": "zh", "path": "packages/docs/content/zh/manual/features/index.mdx", "match": "内容转换", "detail": "Chinese feature status and navigation." } + ] + }, + { + "id": "docs-release", + "area": "documentation", + "requirement": "The release candidate has bilingual scope, evidence, verification, upgrade, and known-limit documentation.", + "status": "proven", + "evidence": [ + { "kind": "doc", "locale": "en", "path": "packages/docs/content/en/manual/releases/rc-1.mdx", "match": "Release candidate 1", "detail": "English RC1 release and evidence guide." }, + { "kind": "doc", "locale": "zh", "path": "packages/docs/content/zh/manual/releases/rc-1.mdx", "match": "候选版本 1", "detail": "Chinese RC1 release and evidence guide." } + ] + } + ] +} diff --git a/package.json b/package.json index f2e89edd..d23edb00 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "deploy:docs": "wrangler deploy", "generate": "cargo test export_bindings && biome check packages/ui/src/types packages/ui/src/client.ts --fix", "bump-tauri": "tsx scripts/bump-tauri.ts", + "test:parity": "node --test scripts/parity-audit.test.mjs", "test:release": "node --test scripts/release-workflow.test.mjs", "prepare": "node scripts/install-prek-hooks.mjs" }, diff --git a/packages/docs/content/en/manual/releases/meta.json b/packages/docs/content/en/manual/releases/meta.json index d8958ec6..5d6876d3 100644 --- a/packages/docs/content/en/manual/releases/meta.json +++ b/packages/docs/content/en/manual/releases/meta.json @@ -1,4 +1,4 @@ { "title": "Release Notes", - "pages": ["alpha-7"] + "pages": ["rc-1", "alpha-7"] } diff --git a/packages/docs/content/en/manual/releases/rc-1.mdx b/packages/docs/content/en/manual/releases/rc-1.mdx new file mode 100644 index 00000000..08a39fa7 --- /dev/null +++ b/packages/docs/content/en/manual/releases/rc-1.mdx @@ -0,0 +1,56 @@ +--- +title: 0.2.0-rc.1 — Parity Evidence +description: A requirement-by-requirement PCL, HMCL, and PrismLauncher parity audit with reproducible release checks. +--- + +# 0.2.0-rc.1 — Release candidate 1 + +RC1 turns DropOut's PCL, HMCL, and PrismLauncher parity claim into a checked release contract. The repository audit covers 42 UI/UX, launcher-function, migration, distribution, and documentation requirements. Every row names current code plus a regression test, workflow, fixture, or published guide; missing files and stale evidence markers fail CI. + +This is still a prerelease. Export irreplaceable instances before upgrading and report regressions on [GitHub Issues](https://github.com/HydroRoll-Team/DropOut/issues). + +## What is included + +- A state-aware Home workflow, scalable instance library, staged instance creation, content browsing, accounts, runtime management, downloads, errors, updates, accessibility, and bounded visual effects. +- Microsoft and offline authentication, managed Java, vanilla/Fabric/Forge installation, mods and packs, repair/export flows, system-tray lifecycle, the consent-gated assistant, and protected content conversion. +- Reviewed, copy-only Prism/MultiMC and PCL/HMCL migration with archive support, conflict previews, cancellation, operation-scoped rollback, and byte-for-byte source preservation fixtures. +- Linux x86_64/ARM64, macOS x86_64/ARM64, Windows x86_64/ARM64, and Alpine musl artifacts with release provenance. + +## Reproduce the evidence gate + +From a clean checkout with Node.js 22, pnpm 10, and stable Rust: + +```bash +pnpm install --frozen-lockfile +pnpm test:parity +pnpm test:release +pnpm -C packages/ui test:unit +pnpm -C packages/ui test:ui +cargo test --manifest-path src-tauri/Cargo.toml --verbose +``` + +The machine-checked matrix lives at [`docs/release/rc1-parity.json`](https://github.com/HydroRoll-Team/DropOut/blob/main/docs/release/rc1-parity.json). Pull-request CI repeats Rust tests on Ubuntu, Arch Linux/Wayland, macOS, and Windows. Release CI builds every supported target and verifies each desktop installer in an isolated directory before upload; the musl archive executes in a clean Alpine runtime. + +## Install and verify + +Download the asset for your platform from [GitHub Releases](https://github.com/HydroRoll-Team/DropOut/releases). Verify its repository-bound provenance before opening it: + +```bash +gh attestation verify -R HydroRoll-Team/DropOut +``` + +The [getting-started guide](/en/docs/manual/getting-started) covers platform assets and updater behavior. The [migration guide](/en/docs/manual/features/migration) explains source selection and rollback; [content conversion](/en/docs/manual/features/content-conversion) explains compatibility review and exclusions. + +## Upgrade notes + +- Existing DropOut configuration and instances remain in place. +- Migration and conversion create DropOut-managed copies. They do not convert a source launcher directory in place. +- Review unresolved content and explicit exclusions before applying a Minecraft or loader conversion. +- Keep the source launcher and important archives until the copied instance has launched successfully. + +## Known limits + +- RC1 is a release candidate, not the stable channel. +- Mod and pack compatibility depends on metadata exposed by the configured content provider; unresolved files require review or exclusion. +- Remote dependencies declared by launcher archives are reported for follow-up and are not silently treated as installed. +- GitHub build provenance is available for release assets. Operating-system code signing or notarization applies only where the release environment is configured for it. diff --git a/packages/docs/content/zh/manual/releases/meta.json b/packages/docs/content/zh/manual/releases/meta.json index a6c2fccc..4b032e7c 100644 --- a/packages/docs/content/zh/manual/releases/meta.json +++ b/packages/docs/content/zh/manual/releases/meta.json @@ -1,4 +1,4 @@ { "title": "发布说明", - "pages": ["alpha-7"] + "pages": ["rc-1", "alpha-7"] } diff --git a/packages/docs/content/zh/manual/releases/rc-1.mdx b/packages/docs/content/zh/manual/releases/rc-1.mdx new file mode 100644 index 00000000..d251b326 --- /dev/null +++ b/packages/docs/content/zh/manual/releases/rc-1.mdx @@ -0,0 +1,56 @@ +--- +title: 0.2.0-rc.1 — 对标证据 +description: 逐项核对 PCL、HMCL 与 PrismLauncher 对标要求,并提供可复现的发布检查。 +--- + +# 0.2.0-rc.1 — 候选版本 1 + +RC1 将 DropOut 对 PCL、HMCL 与 PrismLauncher 的对标声明变成可检查的发布契约。仓库审计覆盖 42 项 UI/UX、启动器功能、迁移、分发与文档要求。每一项都绑定当前代码,以及回归测试、工作流、fixture 或已发布指南;文件缺失或证据标记过期都会让 CI 失败。 + +这仍是预发布版本。升级前请导出无法替代的重要实例,并在 [GitHub Issues](https://github.com/HydroRoll-Team/DropOut/issues) 报告回归问题。 + +## 包含内容 + +- 基于状态的主页流程、可扩展实例库、分步实例创建、内容浏览、账户、运行时管理、下载、错误、更新、无障碍与受控视觉效果。 +- Microsoft/离线认证、托管 Java、原版/Fabric/Forge 安装、模组与资源包、修复/导出、系统托盘生命周期、需同意的诊断助手,以及受保护的内容转换。 +- 经核对且只复制的 Prism/MultiMC、PCL/HMCL 迁移;支持归档包、冲突预览、取消、操作级回滚和来源逐字节不变 fixture。 +- Linux x86_64/ARM64、macOS x86_64/ARM64、Windows x86_64/ARM64 与 Alpine musl 产物及发布来源证明。 + +## 复现证据门禁 + +在安装 Node.js 22、pnpm 10 和稳定版 Rust 的干净检出中运行: + +```bash +pnpm install --frozen-lockfile +pnpm test:parity +pnpm test:release +pnpm -C packages/ui test:unit +pnpm -C packages/ui test:ui +cargo test --manifest-path src-tauri/Cargo.toml --verbose +``` + +机器检查矩阵位于 [`docs/release/rc1-parity.json`](https://github.com/HydroRoll-Team/DropOut/blob/main/docs/release/rc1-parity.json)。拉取请求 CI 会在 Ubuntu、Arch Linux/Wayland、macOS 和 Windows 重跑 Rust 测试;发布 CI 会构建所有受支持目标,在上传前把各桌面安装包放入隔离目录验证,并在干净 Alpine 运行时执行 musl 产物。 + +## 安装与验证 + +从 [GitHub Releases](https://github.com/HydroRoll-Team/DropOut/releases) 下载适合平台的产物。打开前验证与本仓库绑定的来源证明: + +```bash +gh attestation verify -R HydroRoll-Team/DropOut +``` + +[入门指南](/docs/manual/getting-started)说明平台产物和更新器行为;[迁移指南](/docs/manual/features/migration)说明来源选择与回滚;[内容转换](/docs/manual/features/content-conversion)说明兼容性核对与排除项。 + +## 升级说明 + +- 现有 DropOut 配置和实例会继续保留。 +- 迁移和转换会创建由 DropOut 管理的副本,不会原地修改来源启动器目录。 +- 应用 Minecraft 或加载器转换前,必须核对未解决内容和明确排除项。 +- 在复制后的实例成功启动前,请保留来源启动器和重要归档包。 + +## 已知限制 + +- RC1 是候选版本,不是稳定渠道。 +- 模组和资源包兼容性取决于配置内容提供方公开的元数据;未解决文件必须人工核对或排除。 +- 启动器归档包声明的远程依赖会作为后续任务报告,不会被静默视为已安装。 +- 发布产物提供 GitHub 构建来源证明;操作系统代码签名或公证仅在发布环境已配置时适用。 diff --git a/scripts/parity-audit.test.mjs b/scripts/parity-audit.test.mjs new file mode 100644 index 00000000..95e866f5 --- /dev/null +++ b/scripts/parity-audit.test.mjs @@ -0,0 +1,123 @@ +import assert from "node:assert/strict"; +import { existsSync, readFileSync } from "node:fs"; +import { resolve } from "node:path"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; + +const repoRoot = resolve(fileURLToPath(new URL("..", import.meta.url))); +const auditPath = resolve(repoRoot, "docs/release/rc1-parity.json"); + +const requiredRows = [ + "ui-onboarding", + "ui-home-launch", + "ui-instance-library", + "ui-instance-creation", + "ui-content-browsing", + "ui-downloads", + "ui-accounts", + "ui-settings", + "ui-logs", + "ui-errors", + "ui-updates", + "ui-accessibility", + "ui-performance", + "function-authentication", + "function-java-runtime", + "function-loaders", + "function-mods-packs", + "function-instances", + "function-repairs", + "function-exports", + "function-background-lifecycle", + "function-assistant", + "function-conversion", + "function-platforms", + "migration-pcl", + "migration-hmcl", + "migration-prism-multimc", + "migration-archives", + "migration-preview", + "migration-conflicts", + "migration-rollback", + "migration-source-preservation", + "distribution-installers", + "distribution-updates", + "distribution-release-notes", + "distribution-provenance", + "distribution-clean-install", + "docs-install", + "docs-migration", + "docs-troubleshooting", + "docs-features", + "docs-release", +]; + +test("RC1 parity audit covers every required row with direct evidence", () => { + assert.equal(existsSync(auditPath), true, "RC1 parity audit is missing"); + const audit = JSON.parse(readFileSync(auditPath, "utf8")); + + assert.equal(audit.schemaVersion, 1); + assert.equal(audit.release, "0.2.0-rc.1"); + assert.equal(audit.sourceIssue, 178); + + const actualRows = audit.rows.map(({ id }) => id); + assert.equal( + new Set(actualRows).size, + actualRows.length, + "row IDs must be unique", + ); + assert.deepEqual([...actualRows].sort(), [...requiredRows].sort()); + + for (const row of audit.rows) { + assert.equal(row.status, "proven", `${row.id} is not proven`); + assert.ok(row.requirement?.trim(), `${row.id} has no requirement text`); + assert.ok( + row.evidence.length >= 2, + `${row.id} needs at least two evidence links`, + ); + + for (const evidence of row.evidence) { + assert.ok(evidence.kind, `${row.id} has evidence without a kind`); + assert.ok(evidence.path, `${row.id} has evidence without a path`); + assert.ok( + evidence.detail?.trim(), + `${row.id} has evidence without a detail`, + ); + + const evidencePath = resolve(repoRoot, evidence.path); + assert.equal( + existsSync(evidencePath), + true, + `${row.id} points to missing evidence ${evidence.path}`, + ); + if (evidence.match) { + assert.ok( + readFileSync(evidencePath, "utf8").includes(evidence.match), + `${row.id} cannot find its evidence marker in ${evidence.path}`, + ); + } + } + } +}); + +test("bilingual documentation rows link both current locales", () => { + const audit = JSON.parse(readFileSync(auditPath, "utf8")); + for (const row of audit.rows.filter(({ id }) => id.startsWith("docs-"))) { + const locales = new Set( + row.evidence.map(({ locale }) => locale).filter(Boolean), + ); + assert.deepEqual( + [...locales].sort(), + ["en", "zh"], + `${row.id} must link English and Chinese evidence`, + ); + } +}); + +test("required CI runs the parity evidence contract", () => { + const prekWorkflow = readFileSync( + resolve(repoRoot, ".github/workflows/prek.yml"), + "utf8", + ); + assert.match(prekWorkflow, /run: pnpm test:parity/); +}); diff --git a/scripts/release-workflow.test.mjs b/scripts/release-workflow.test.mjs index 76599e1b..341514c2 100644 --- a/scripts/release-workflow.test.mjs +++ b/scripts/release-workflow.test.mjs @@ -47,6 +47,41 @@ test("does not run AUR before Semifold creates the GitHub release", () => { assert.equal(releaseConfig.resolver.rust.publish, undefined); }); +test("smoke tests every packaged desktop artifact in an isolated install", () => { + const buildSteps = releaseWorkflow.jobs["build-tauri"].steps; + const stepByName = new Map(buildSteps.map((step) => [step.name, step])); + + assert.match( + stepByName.get("Smoke isolated install (Linux)")?.run ?? "", + /smoke-install-linux\.sh/, + ); + assert.match( + stepByName.get("Smoke isolated install (macOS)")?.run ?? "", + /smoke-install-macos\.sh/, + ); + assert.match( + stepByName.get("Smoke isolated install (Windows)")?.run ?? "", + /smoke-install-windows\.ps1/, + ); + + const uploadIndexes = buildSteps + .map((step, index) => + step.name?.startsWith("Upload Artifact") ? index : -1, + ) + .filter((index) => index >= 0); + const smokeIndexes = buildSteps + .map((step, index) => + step.name?.startsWith("Smoke isolated install") ? index : -1, + ) + .filter((index) => index >= 0); + + assert.equal(smokeIndexes.length, 3); + assert.ok( + Math.max(...smokeIndexes) < Math.min(...uploadIndexes), + "all isolated-install smoke checks must run before artifacts are uploaded", + ); +}); + test("treats an already-current AUR package as a successful no-op", () => { assert.equal(hasStagedChanges(""), false); assert.equal(hasStagedChanges("\n"), false); diff --git a/scripts/smoke-install-linux.sh b/scripts/smoke-install-linux.sh new file mode 100755 index 00000000..49194331 --- /dev/null +++ b/scripts/smoke-install-linux.sh @@ -0,0 +1,39 @@ +#!/bin/sh +set -eu + +target=${1:?usage: smoke-install-linux.sh } +repo_root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +bundle_root="$repo_root/target/$target/release/bundle" + +appimage=$(find "$bundle_root/appimage" -maxdepth 1 -type f -name '*.AppImage' -print -quit) +deb=$(find "$bundle_root/deb" -maxdepth 1 -type f -name '*.deb' -print -quit) + +if [ -z "$appimage" ] || [ -z "$deb" ]; then + echo "expected AppImage and Debian artifacts under $bundle_root" >&2 + exit 1 +fi + +smoke_root=$(mktemp -d) +cleanup() { + rm -rf "$smoke_root" +} +trap cleanup EXIT HUP INT TERM + +appimage_root="$smoke_root/appimage" +mkdir -p "$appimage_root" +chmod +x "$appimage" +( + cd "$appimage_root" + "$appimage" --appimage-extract >/dev/null +) + +test -x "$appimage_root/squashfs-root/AppRun" +find "$appimage_root/squashfs-root" -type f -name '*.desktop' -print -quit | grep -q . + +deb_root="$smoke_root/debian" +mkdir -p "$deb_root" +dpkg-deb --extract "$deb" "$deb_root" +find "$deb_root/usr/bin" -maxdepth 1 -type f -perm -u+x -print -quit | grep -q . +find "$deb_root/usr/share/applications" -type f -name '*.desktop' -print -quit | grep -q . + +echo "Verified isolated Linux installs for $target" diff --git a/scripts/smoke-install-macos.sh b/scripts/smoke-install-macos.sh new file mode 100755 index 00000000..737e352a --- /dev/null +++ b/scripts/smoke-install-macos.sh @@ -0,0 +1,47 @@ +#!/bin/sh +set -eu + +target=${1:?usage: smoke-install-macos.sh } +repo_root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +bundle_root="$repo_root/target/$target/release/bundle" +dmg=$(find "$bundle_root/dmg" -maxdepth 1 -type f -name '*.dmg' -print -quit) + +if [ -z "$dmg" ]; then + echo "expected a macOS disk image under $bundle_root" >&2 + exit 1 +fi + +smoke_root=$(mktemp -d) +mount_root="$smoke_root/mount" +install_root="$smoke_root/install" +attached=false +cleanup() { + if [ "$attached" = true ]; then + hdiutil detach "$mount_root" -quiet || true + fi + rm -rf "$smoke_root" +} +trap cleanup EXIT HUP INT TERM + +mkdir -p "$mount_root" "$install_root" +hdiutil attach -readonly -nobrowse -mountpoint "$mount_root" "$dmg" >/dev/null +attached=true + +source_app=$(find "$mount_root" -maxdepth 1 -type d -name '*.app' -print -quit) +if [ -z "$source_app" ]; then + echo "disk image does not contain an application bundle" >&2 + exit 1 +fi + +cp -R "$source_app" "$install_root/" +installed_app="$install_root/$(basename "$source_app")" +info_plist="$installed_app/Contents/Info.plist" +plutil -lint "$info_plist" >/dev/null +executable_name=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleExecutable' "$info_plist") +installed_binary="$installed_app/Contents/MacOS/$executable_name" + +test -x "$installed_binary" +file "$installed_binary" | grep -q 'Mach-O' +otool -L "$installed_binary" >/dev/null + +echo "Verified isolated macOS install for $target" diff --git a/scripts/smoke-install-windows.ps1 b/scripts/smoke-install-windows.ps1 new file mode 100644 index 00000000..690bba38 --- /dev/null +++ b/scripts/smoke-install-windows.ps1 @@ -0,0 +1,48 @@ +param( + [Parameter(Mandatory = $true)] + [string]$Target +) + +$ErrorActionPreference = "Stop" +$repoRoot = Split-Path -Parent $PSScriptRoot +$bundleRoot = Join-Path $repoRoot "target/$Target/release/bundle/nsis" +$installer = Get-ChildItem -Path $bundleRoot -Filter *.exe -File | Select-Object -First 1 + +if ($null -eq $installer) { + throw "Expected an NSIS installer under $bundleRoot" +} + +$smokeRoot = Join-Path $env:RUNNER_TEMP ("dropout-install-smoke-" + [guid]::NewGuid()) +$installRoot = Join-Path $smokeRoot "installed" +New-Item -ItemType Directory -Path $installRoot -Force | Out-Null + +try { + $process = Start-Process -FilePath $installer.FullName -ArgumentList @("/S", "/D=$installRoot") -Wait -PassThru + if ($process.ExitCode -ne 0) { + throw "NSIS installer exited with code $($process.ExitCode)" + } + + $installedBinary = Get-ChildItem -Path $installRoot -Filter *.exe -File -Recurse | + Where-Object { $_.Name -notmatch "(?i)uninstall" } | + Select-Object -First 1 + if ($null -eq $installedBinary) { + throw "NSIS installer did not produce an application executable" + } + + $header = [System.IO.File]::ReadAllBytes($installedBinary.FullName) + if ($header.Length -lt 2 -or $header[0] -ne 0x4d -or $header[1] -ne 0x5a) { + throw "Installed application does not have a valid PE header" + } + + Write-Output "Verified isolated Windows install for $Target" +} +finally { + $uninstaller = Get-ChildItem -Path $installRoot -Filter *uninstall*.exe -File -Recurse -ErrorAction SilentlyContinue | + Select-Object -First 1 + if ($null -ne $uninstaller) { + Start-Process -FilePath $uninstaller.FullName -ArgumentList "/S" -Wait | Out-Null + } + if (Test-Path $smokeRoot) { + Remove-Item -Path $smokeRoot -Recurse -Force + } +} From 2464b21c630ad0a772d4a4588055d03317278561 Mon Sep 17 00:00:00 2001 From: HsiangNianian Date: Tue, 4 Aug 2026 13:20:09 +0800 Subject: [PATCH 2/6] fix(release): synchronize generated manifests --- scripts/bump-tauri.ts | 44 ++++++++++++++++---- scripts/release-workflow.test.mjs | 67 ++++++++++++++++++++++++++++++- 2 files changed, 102 insertions(+), 9 deletions(-) diff --git a/scripts/bump-tauri.ts b/scripts/bump-tauri.ts index 0cf6aea5..db635651 100644 --- a/scripts/bump-tauri.ts +++ b/scripts/bump-tauri.ts @@ -3,15 +3,16 @@ import path from "node:path"; import consola from "consola"; import toml from "toml"; -const tauriJsonPath = path.join( - __dirname, - "..", - "src-tauri", - "tauri.conf.json", -); +const repoRoot = process.env.DROPOUT_REPO_ROOT ?? path.join(__dirname, ".."); +const tauriJsonPath = path.join(repoRoot, "src-tauri", "tauri.conf.json"); consola.debug("tauriJsonPath:", tauriJsonPath); -const tauriTomlPath = path.join(__dirname, "..", "src-tauri", "Cargo.toml"); +const tauriTomlPath = path.join(repoRoot, "src-tauri", "Cargo.toml"); consola.debug("tauriTomlPath:", tauriTomlPath); +const cargoLockPath = path.join(repoRoot, "Cargo.lock"); +const packageJsonPaths = [ + path.join(repoRoot, "packages", "ui", "package.json"), + path.join(repoRoot, "packages", "docs", "package.json"), +]; const getCurrentVersion = () => { const tauriJsonData = fs.readFileSync(tauriJsonPath, "utf8"); @@ -37,6 +38,21 @@ const replaceVersion = (content: string, version: string) => { return newJson; }; +const replaceLockedVersion = (content: string, version: string) => { + const packagePattern = + /(\[\[package\]\]\r?\nname = "dropout"\r?\nversion = ")[^"]+("\r?\n)/; + if (!packagePattern.test(content)) { + throw new Error("DropOut package entry not found in Cargo.lock"); + } + return content.replace(packagePattern, `$1${version}$2`); +}; + +const writeIfChanged = (filePath: string, previous: string, next: string) => { + if (previous !== next) { + fs.writeFileSync(filePath, next); + } +}; + const tauriJsonData = fs.readFileSync(tauriJsonPath, "utf8"); const currentVersion = getCurrentVersion(); const bumpVersion = getBumpVersion(); @@ -46,7 +62,19 @@ consola.debug("bumpVersion:", bumpVersion); if (currentVersion !== bumpVersion) { const replacedData = replaceVersion(tauriJsonData, bumpVersion); consola.info(`Bumped version from ${currentVersion} to ${bumpVersion}`); - fs.writeFileSync(tauriJsonPath, replacedData); + writeIfChanged(tauriJsonPath, tauriJsonData, replacedData); } else { consola.info(`Version ${currentVersion} is already up-to-date`); } + +const cargoLockData = fs.readFileSync(cargoLockPath, "utf8"); +const synchronizedLock = replaceLockedVersion(cargoLockData, bumpVersion); +writeIfChanged(cargoLockPath, cargoLockData, synchronizedLock); + +for (const packageJsonPath of packageJsonPaths) { + const packageJson = fs.readFileSync(packageJsonPath, "utf8"); + const normalizedPackageJson = packageJson.endsWith("\n") + ? packageJson + : `${packageJson}\n`; + writeIfChanged(packageJsonPath, packageJson, normalizedPackageJson); +} diff --git a/scripts/release-workflow.test.mjs b/scripts/release-workflow.test.mjs index 341514c2..680b8ed1 100644 --- a/scripts/release-workflow.test.mjs +++ b/scripts/release-workflow.test.mjs @@ -1,6 +1,10 @@ import assert from "node:assert/strict"; -import { readFileSync } from "node:fs"; +import { spawnSync } from "node:child_process"; +import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; import test from "node:test"; +import { fileURLToPath } from "node:url"; import yaml from "js-yaml"; import { parse as parseToml } from "smol-toml"; import { hasStagedChanges } from "./release-aur-helpers.mjs"; @@ -8,6 +12,7 @@ import { hasStagedChanges } from "./release-aur-helpers.mjs"; const releaseConfig = parseToml( readFileSync(new URL("../.changes/config.toml", import.meta.url), "utf8"), ); +const repoRoot = fileURLToPath(new URL("..", import.meta.url)); const releaseWorkflow = yaml.load( readFileSync( new URL("../.github/workflows/semifold-ci.yaml", import.meta.url), @@ -47,6 +52,66 @@ test("does not run AUR before Semifold creates the GitHub release", () => { assert.equal(releaseConfig.resolver.rust.publish, undefined); }); +test("post-version hook synchronizes every generated release manifest", () => { + const fixtureRoot = mkdtempSync(join(tmpdir(), "dropout-release-manifests-")); + mkdirSync(join(fixtureRoot, "src-tauri"), { recursive: true }); + mkdirSync(join(fixtureRoot, "packages", "ui"), { recursive: true }); + mkdirSync(join(fixtureRoot, "packages", "docs"), { recursive: true }); + + writeFileSync( + join(fixtureRoot, "src-tauri", "Cargo.toml"), + '[package]\nname = "dropout"\nversion = "0.2.0-rc.1"\n', + ); + writeFileSync( + join(fixtureRoot, "src-tauri", "tauri.conf.json"), + '{\n "version": "0.2.0-rc.0"\n}\n', + ); + writeFileSync( + join(fixtureRoot, "Cargo.lock"), + '[[package]]\nname = "dropout"\nversion = "0.2.0-rc.0"\n', + ); + writeFileSync( + join(fixtureRoot, "packages", "ui", "package.json"), + '{\n "name": "@dropout/ui",\n "version": "0.1.0-rc.1"\n}', + ); + writeFileSync( + join(fixtureRoot, "packages", "docs", "package.json"), + '{\n "name": "@dropout/docs",\n "version": "0.1.0-rc.1"\n}', + ); + + const result = spawnSync( + process.execPath, + ["--import", "tsx", join(repoRoot, "scripts", "bump-tauri.ts")], + { + cwd: repoRoot, + encoding: "utf8", + env: { ...process.env, DROPOUT_REPO_ROOT: fixtureRoot }, + }, + ); + assert.equal(result.status, 0, result.stderr || result.stdout); + + assert.match( + readFileSync(join(fixtureRoot, "Cargo.lock"), "utf8"), + /name = "dropout"\nversion = "0\.2\.0-rc\.1"/, + ); + assert.equal( + JSON.parse( + readFileSync(join(fixtureRoot, "src-tauri", "tauri.conf.json"), "utf8"), + ).version, + "0.2.0-rc.1", + ); + for (const packageName of ["ui", "docs"]) { + assert.equal( + readFileSync( + join(fixtureRoot, "packages", packageName, "package.json"), + "utf8", + ).endsWith("\n"), + true, + `${packageName} package manifest must end with a newline`, + ); + } +}); + test("smoke tests every packaged desktop artifact in an isolated install", () => { const buildSteps = releaseWorkflow.jobs["build-tauri"].steps; const stepByName = new Map(buildSteps.map((step) => [step.name, step])); From e0cc80d269d13f244c932b64efe51b3a11228eeb Mon Sep 17 00:00:00 2001 From: HsiangNianian Date: Tue, 4 Aug 2026 13:37:42 +0800 Subject: [PATCH 3/6] chore(release): enter rc channel --- .changes/config.toml | 8 ++++---- .changes/rc1-parity-audit.md | 7 +++++++ Cargo.lock | 2 +- packages/docs/package.json | 2 +- packages/ui/package.json | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 2 +- 7 files changed, 16 insertions(+), 9 deletions(-) create mode 100644 .changes/rc1-parity-audit.md diff --git a/.changes/config.toml b/.changes/config.toml index 1b34a6fb..fea02aae 100644 --- a/.changes/config.toml +++ b/.changes/config.toml @@ -12,23 +12,23 @@ refactor = "Refactors" [packages."@dropout/ui"] path = "packages/ui" resolver = "nodejs" -version-mode = { pre-release.tag = "beta" } +version-mode = { pre-release.tag = "rc" } [packages."@dropout/docs"] path = "packages/docs" resolver = "nodejs" -version-mode = { pre-release.tag = "beta" } +version-mode = { pre-release.tag = "rc" } [packages.dropout] path = "src-tauri" resolver = "rust" -version-mode = { pre-release.tag = "beta" } +version-mode = { pre-release.tag = "rc" } assets = ["artifacts/**/*"] [packages.dropout-macros] path = "crates/macros" resolver = "rust" -version-mode = { pre-release.tag = "beta" } +version-mode = { pre-release.tag = "rc" } [resolver.rust.pre-check] url = "https://crates.io/api/v1/crates/{{ package.name }}/{{ package.version }}" diff --git a/.changes/rc1-parity-audit.md b/.changes/rc1-parity-audit.md new file mode 100644 index 00000000..190a1df1 --- /dev/null +++ b/.changes/rc1-parity-audit.md @@ -0,0 +1,7 @@ +--- +"dropout": "patch:feat" +"@dropout/ui": "patch:feat" +"@dropout/docs": "patch:feat" +--- + +Add a machine-checked 42-row launcher parity audit, bilingual RC1 evidence guides, isolated installer smoke tests across every supported desktop target, and durable release-manifest synchronization. diff --git a/Cargo.lock b/Cargo.lock index 2acd2a2b..cac1064a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -972,7 +972,7 @@ dependencies = [ [[package]] name = "dropout" -version = "0.2.0-beta.2" +version = "0.2.0-rc.0" dependencies = [ "bytes", "chrono", diff --git a/packages/docs/package.json b/packages/docs/package.json index ec751fe7..2edd3767 100644 --- a/packages/docs/package.json +++ b/packages/docs/package.json @@ -2,7 +2,7 @@ "name": "@dropout/docs", "private": true, "type": "module", - "version": "0.1.0-beta.2", + "version": "0.1.0-rc.0", "scripts": { "build": "react-router build", "dev": "react-router dev", diff --git a/packages/ui/package.json b/packages/ui/package.json index 32ffd848..fba1be42 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,7 +1,7 @@ { "name": "@dropout/ui", "private": true, - "version": "0.1.0-beta.2", + "version": "0.1.0-rc.0", "type": "module", "scripts": { "dev": "vite", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index ec4ced80..8781f14b 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dropout" -version = "0.2.0-beta.2" +version = "0.2.0-rc.0" edition = "2024" authors = ["HsiangNianian"] description = "The DropOut Minecraft Game Launcher" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index a90b5636..ff10ccd3 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Dropout", - "version": "0.2.0-beta.2", + "version": "0.2.0-rc.0", "identifier": "com.dropout.launcher", "build": { "beforeDevCommand": "pnpm -C packages/ui dev", From c02b44b574aecc4c1e40b579b714bf3975afbd64 Mon Sep 17 00:00:00 2001 From: HsiangNianian Date: Tue, 4 Aug 2026 14:03:48 +0800 Subject: [PATCH 4/6] fix(release): harden RC1 packaging checks --- scripts/bump-tauri.ts | 30 +++++++++++++++++++++++++----- scripts/release-workflow.test.mjs | 13 +++++++++++-- scripts/smoke-install-macos.sh | 28 +++++++++++++++++++++++++--- 3 files changed, 61 insertions(+), 10 deletions(-) diff --git a/scripts/bump-tauri.ts b/scripts/bump-tauri.ts index db635651..a3ad6ab3 100644 --- a/scripts/bump-tauri.ts +++ b/scripts/bump-tauri.ts @@ -39,12 +39,32 @@ const replaceVersion = (content: string, version: string) => { }; const replaceLockedVersion = (content: string, version: string) => { - const packagePattern = - /(\[\[package\]\]\r?\nname = "dropout"\r?\nversion = ")[^"]+("\r?\n)/; - if (!packagePattern.test(content)) { - throw new Error("DropOut package entry not found in Cargo.lock"); + let matchedPackages = 0; + const sections = content.split(/(?=^\[\[package\]\]\s*(?:\r?\n|$))/m); + + const synchronizedSections = sections.map((section) => { + if (!section.startsWith("[[package]]")) return section; + + const parsed = toml.parse(section); + const lockedPackage = Array.isArray(parsed.package) + ? parsed.package[0] + : undefined; + if (lockedPackage?.name !== "dropout") return section; + + matchedPackages += 1; + const versionPattern = /^(\s*version\s*=\s*")[^"]+(".*)$/m; + if (!versionPattern.test(section)) { + throw new Error("DropOut package version not found in Cargo.lock"); + } + return section.replace(versionPattern, `$1${version}$2`); + }); + + if (matchedPackages !== 1) { + throw new Error( + `Expected one DropOut package entry in Cargo.lock, found ${matchedPackages}`, + ); } - return content.replace(packagePattern, `$1${version}$2`); + return synchronizedSections.join(""); }; const writeIfChanged = (filePath: string, previous: string, next: string) => { diff --git a/scripts/release-workflow.test.mjs b/scripts/release-workflow.test.mjs index 680b8ed1..3f0e2e5d 100644 --- a/scripts/release-workflow.test.mjs +++ b/scripts/release-workflow.test.mjs @@ -68,7 +68,7 @@ test("post-version hook synchronizes every generated release manifest", () => { ); writeFileSync( join(fixtureRoot, "Cargo.lock"), - '[[package]]\nname = "dropout"\nversion = "0.2.0-rc.0"\n', + '# generated lockfile fixture\n[[package]]\nversion = "0.2.0-rc.0"\nsource = "registry+https://example.invalid/index"\n# package fields may be reordered\nname = "dropout"\n', ); writeFileSync( join(fixtureRoot, "packages", "ui", "package.json"), @@ -92,7 +92,7 @@ test("post-version hook synchronizes every generated release manifest", () => { assert.match( readFileSync(join(fixtureRoot, "Cargo.lock"), "utf8"), - /name = "dropout"\nversion = "0\.2\.0-rc\.1"/, + /version = "0\.2\.0-rc\.1"/, ); assert.equal( JSON.parse( @@ -145,6 +145,15 @@ test("smoke tests every packaged desktop artifact in an isolated install", () => Math.max(...smokeIndexes) < Math.min(...uploadIndexes), "all isolated-install smoke checks must run before artifacts are uploaded", ); + + const macSmoke = readFileSync( + new URL("../scripts/smoke-install-macos.sh", import.meta.url), + "utf8", + ); + assert.match(macSmoke, /application bundle is missing Info\.plist/); + assert.match(macSmoke, /application bundle has an invalid Info\.plist/); + assert.match(macSmoke, /application bundle is missing CFBundleExecutable/); + assert.match(macSmoke, /is not present or not executable/); }); test("treats an already-current AUR package as a successful no-op", () => { diff --git a/scripts/smoke-install-macos.sh b/scripts/smoke-install-macos.sh index 737e352a..5cd1f489 100755 --- a/scripts/smoke-install-macos.sh +++ b/scripts/smoke-install-macos.sh @@ -36,11 +36,33 @@ fi cp -R "$source_app" "$install_root/" installed_app="$install_root/$(basename "$source_app")" info_plist="$installed_app/Contents/Info.plist" -plutil -lint "$info_plist" >/dev/null -executable_name=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleExecutable' "$info_plist") + +if [ ! -f "$info_plist" ]; then + echo "application bundle is missing Info.plist" >&2 + exit 1 +fi + +if ! plutil -lint "$info_plist" >/dev/null; then + echo "application bundle has an invalid Info.plist" >&2 + exit 1 +fi + +if ! executable_name=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleExecutable' "$info_plist" 2>/dev/null); then + echo "application bundle is missing CFBundleExecutable" >&2 + exit 1 +fi +if [ -z "$executable_name" ]; then + echo "application bundle is missing CFBundleExecutable" >&2 + exit 1 +fi + installed_binary="$installed_app/Contents/MacOS/$executable_name" -test -x "$installed_binary" +if [ ! -x "$installed_binary" ]; then + echo "application bundle CFBundleExecutable '$executable_name' is not present or not executable" >&2 + exit 1 +fi + file "$installed_binary" | grep -q 'Mach-O' otool -L "$installed_binary" >/dev/null From 05a35096103ad2abdbe2f11ee42bac7e38577211 Mon Sep 17 00:00:00 2001 From: HsiangNianian Date: Tue, 4 Aug 2026 14:08:19 +0800 Subject: [PATCH 5/6] fix(release): verify every packaged artifact --- .github/workflows/semifold-ci.yaml | 5 +- scripts/release-workflow.test.mjs | 35 ++++++++++++++ scripts/smoke-install-linux.sh | 14 +++++- scripts/smoke-install-macos.sh | 73 ++++++++++++++++++------------ scripts/smoke-install-windows.ps1 | 11 ++++- 5 files changed, 102 insertions(+), 36 deletions(-) diff --git a/.github/workflows/semifold-ci.yaml b/.github/workflows/semifold-ci.yaml index 60cfc5e2..4fc6cc29 100644 --- a/.github/workflows/semifold-ci.yaml +++ b/.github/workflows/semifold-ci.yaml @@ -66,7 +66,7 @@ jobs: Acquire::https::Timeout "30"; EOF - packages="libwebkit2gtk-4.1-dev build-essential curl wget file libssl-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev libfuse2" + packages="libwebkit2gtk-4.1-dev build-essential curl wget file libssl-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev libfuse2 rpm2cpio cpio" for attempt in 1 2 3; do sudo apt-get update && sudo apt-get install -y $packages && break if [ "$attempt" = "3" ]; then @@ -175,7 +175,7 @@ jobs: path: | target/${{ matrix.target }}/release/bundle/appimage/*.AppImage target/${{ matrix.target }}/release/bundle/deb/*.deb - target/${{ matrix.target }}/release/bundle/appimage/*.rpm + target/${{ matrix.target }}/release/bundle/rpm/*.rpm retention-days: 7 - name: Upload Artifact (Windows) @@ -184,7 +184,6 @@ jobs: with: name: windows-${{ matrix.target }}-artifacts path: | - target/${{ matrix.target }}/release/bundle/msi/*.msi target/${{ matrix.target }}/release/bundle/nsis/*.exe retention-days: 1 diff --git a/scripts/release-workflow.test.mjs b/scripts/release-workflow.test.mjs index 3f0e2e5d..45c7097a 100644 --- a/scripts/release-workflow.test.mjs +++ b/scripts/release-workflow.test.mjs @@ -146,14 +146,49 @@ test("smoke tests every packaged desktop artifact in an isolated install", () => "all isolated-install smoke checks must run before artifacts are uploaded", ); + const linuxSmoke = readFileSync( + new URL("../scripts/smoke-install-linux.sh", import.meta.url), + "utf8", + ); + assert.match(linuxSmoke, /bundle_root\/rpm/); + assert.match(linuxSmoke, /rpm2cpio/); + const macSmoke = readFileSync( new URL("../scripts/smoke-install-macos.sh", import.meta.url), "utf8", ); + assert.match(macSmoke, /\.app\.tar\.gz/); + assert.match(macSmoke, /tar -xzf/); assert.match(macSmoke, /application bundle is missing Info\.plist/); assert.match(macSmoke, /application bundle has an invalid Info\.plist/); assert.match(macSmoke, /application bundle is missing CFBundleExecutable/); assert.match(macSmoke, /is not present or not executable/); + + const windowsSmoke = readFileSync( + new URL("../scripts/smoke-install-windows.ps1", import.meta.url), + "utf8", + ); + assert.match(windowsSmoke, /OpenRead/); + assert.match(windowsSmoke, /\[byte\[\]\]::new\(2\)/); + assert.doesNotMatch(windowsSmoke, /ReadAllBytes/); + + const linuxUpload = + stepByName.get("Upload Artifact (Linux)")?.with?.path ?? ""; + assert.match(linuxUpload, /bundle\/rpm\/\*\.rpm/); + const windowsUpload = + stepByName.get("Upload Artifact (Windows)")?.with?.path ?? ""; + assert.doesNotMatch(windowsUpload, /\.msi/); + + const windowsBuilds = releaseWorkflow.jobs[ + "build-tauri" + ].strategy.matrix.include.filter(({ platform }) => + platform.startsWith("windows"), + ); + assert.equal(windowsBuilds.length, 2); + assert.equal( + windowsBuilds.every(({ args }) => args.includes("--bundles nsis")), + true, + ); }); test("treats an already-current AUR package as a successful no-op", () => { diff --git a/scripts/smoke-install-linux.sh b/scripts/smoke-install-linux.sh index 49194331..8d8a55d7 100755 --- a/scripts/smoke-install-linux.sh +++ b/scripts/smoke-install-linux.sh @@ -7,9 +7,10 @@ bundle_root="$repo_root/target/$target/release/bundle" appimage=$(find "$bundle_root/appimage" -maxdepth 1 -type f -name '*.AppImage' -print -quit) deb=$(find "$bundle_root/deb" -maxdepth 1 -type f -name '*.deb' -print -quit) +rpm=$(find "$bundle_root/rpm" -maxdepth 1 -type f -name '*.rpm' -print -quit) -if [ -z "$appimage" ] || [ -z "$deb" ]; then - echo "expected AppImage and Debian artifacts under $bundle_root" >&2 +if [ -z "$appimage" ] || [ -z "$deb" ] || [ -z "$rpm" ]; then + echo "expected AppImage, Debian, and RPM artifacts under $bundle_root" >&2 exit 1 fi @@ -36,4 +37,13 @@ dpkg-deb --extract "$deb" "$deb_root" find "$deb_root/usr/bin" -maxdepth 1 -type f -perm -u+x -print -quit | grep -q . find "$deb_root/usr/share/applications" -type f -name '*.desktop' -print -quit | grep -q . +rpm_root="$smoke_root/rpm" +mkdir -p "$rpm_root" +( + cd "$rpm_root" + rpm2cpio "$rpm" | cpio -idm --quiet +) +find "$rpm_root/usr/bin" -maxdepth 1 -type f -perm -u+x -print -quit | grep -q . +find "$rpm_root/usr/share/applications" -type f -name '*.desktop' -print -quit | grep -q . + echo "Verified isolated Linux installs for $target" diff --git a/scripts/smoke-install-macos.sh b/scripts/smoke-install-macos.sh index 5cd1f489..d0fa7d7e 100755 --- a/scripts/smoke-install-macos.sh +++ b/scripts/smoke-install-macos.sh @@ -5,9 +5,10 @@ target=${1:?usage: smoke-install-macos.sh } repo_root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) bundle_root="$repo_root/target/$target/release/bundle" dmg=$(find "$bundle_root/dmg" -maxdepth 1 -type f -name '*.dmg' -print -quit) +app_archive=$(find "$bundle_root/macos" -maxdepth 1 -type f -name '*.app.tar.gz' -print -quit) -if [ -z "$dmg" ]; then - echo "expected a macOS disk image under $bundle_root" >&2 +if [ -z "$dmg" ] || [ -z "$app_archive" ]; then + echo "expected macOS disk image and application archive under $bundle_root" >&2 exit 1 fi @@ -23,6 +24,39 @@ cleanup() { } trap cleanup EXIT HUP INT TERM +verify_app_bundle() { + verified_app=$1 + verified_plist="$verified_app/Contents/Info.plist" + + if [ ! -f "$verified_plist" ]; then + echo "application bundle is missing Info.plist" >&2 + exit 1 + fi + + if ! plutil -lint "$verified_plist" >/dev/null; then + echo "application bundle has an invalid Info.plist" >&2 + exit 1 + fi + + if ! verified_executable=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleExecutable' "$verified_plist" 2>/dev/null); then + echo "application bundle is missing CFBundleExecutable" >&2 + exit 1 + fi + if [ -z "$verified_executable" ]; then + echo "application bundle is missing CFBundleExecutable" >&2 + exit 1 + fi + + verified_binary="$verified_app/Contents/MacOS/$verified_executable" + if [ ! -x "$verified_binary" ]; then + echo "application bundle CFBundleExecutable '$verified_executable' is not present or not executable" >&2 + exit 1 + fi + + file "$verified_binary" | grep -q 'Mach-O' + otool -L "$verified_binary" >/dev/null +} + mkdir -p "$mount_root" "$install_root" hdiutil attach -readonly -nobrowse -mountpoint "$mount_root" "$dmg" >/dev/null attached=true @@ -35,35 +69,16 @@ fi cp -R "$source_app" "$install_root/" installed_app="$install_root/$(basename "$source_app")" -info_plist="$installed_app/Contents/Info.plist" +verify_app_bundle "$installed_app" -if [ ! -f "$info_plist" ]; then - echo "application bundle is missing Info.plist" >&2 +archive_root="$smoke_root/archive" +mkdir -p "$archive_root" +tar -xzf "$app_archive" -C "$archive_root" +archive_app=$(find "$archive_root" -maxdepth 2 -type d -name '*.app' -print -quit) +if [ -z "$archive_app" ]; then + echo "application archive does not contain an application bundle" >&2 exit 1 fi - -if ! plutil -lint "$info_plist" >/dev/null; then - echo "application bundle has an invalid Info.plist" >&2 - exit 1 -fi - -if ! executable_name=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleExecutable' "$info_plist" 2>/dev/null); then - echo "application bundle is missing CFBundleExecutable" >&2 - exit 1 -fi -if [ -z "$executable_name" ]; then - echo "application bundle is missing CFBundleExecutable" >&2 - exit 1 -fi - -installed_binary="$installed_app/Contents/MacOS/$executable_name" - -if [ ! -x "$installed_binary" ]; then - echo "application bundle CFBundleExecutable '$executable_name' is not present or not executable" >&2 - exit 1 -fi - -file "$installed_binary" | grep -q 'Mach-O' -otool -L "$installed_binary" >/dev/null +verify_app_bundle "$archive_app" echo "Verified isolated macOS install for $target" diff --git a/scripts/smoke-install-windows.ps1 b/scripts/smoke-install-windows.ps1 index 690bba38..41a48ae0 100644 --- a/scripts/smoke-install-windows.ps1 +++ b/scripts/smoke-install-windows.ps1 @@ -29,8 +29,15 @@ try { throw "NSIS installer did not produce an application executable" } - $header = [System.IO.File]::ReadAllBytes($installedBinary.FullName) - if ($header.Length -lt 2 -or $header[0] -ne 0x4d -or $header[1] -ne 0x5a) { + $header = [byte[]]::new(2) + $stream = [System.IO.File]::OpenRead($installedBinary.FullName) + try { + $bytesRead = $stream.Read($header, 0, $header.Length) + } + finally { + $stream.Dispose() + } + if ($bytesRead -ne 2 -or $header[0] -ne 0x4d -or $header[1] -ne 0x5a) { throw "Installed application does not have a valid PE header" } From 00fa737f18eed82791aa29ac51e68d680e13bcdf Mon Sep 17 00:00:00 2001 From: HsiangNianian Date: Tue, 4 Aug 2026 14:19:50 +0800 Subject: [PATCH 6/6] fix(release): make macOS smoke temp portable --- scripts/release-workflow.test.mjs | 1 + scripts/smoke-install-macos.sh | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/release-workflow.test.mjs b/scripts/release-workflow.test.mjs index 45c7097a..9fda4b8c 100644 --- a/scripts/release-workflow.test.mjs +++ b/scripts/release-workflow.test.mjs @@ -159,6 +159,7 @@ test("smoke tests every packaged desktop artifact in an isolated install", () => ); assert.match(macSmoke, /\.app\.tar\.gz/); assert.match(macSmoke, /tar -xzf/); + assert.match(macSmoke, /dropout-macos-smoke\.XXXXXX/); assert.match(macSmoke, /application bundle is missing Info\.plist/); assert.match(macSmoke, /application bundle has an invalid Info\.plist/); assert.match(macSmoke, /application bundle is missing CFBundleExecutable/); diff --git a/scripts/smoke-install-macos.sh b/scripts/smoke-install-macos.sh index d0fa7d7e..1307bfc4 100755 --- a/scripts/smoke-install-macos.sh +++ b/scripts/smoke-install-macos.sh @@ -12,7 +12,7 @@ if [ -z "$dmg" ] || [ -z "$app_archive" ]; then exit 1 fi -smoke_root=$(mktemp -d) +smoke_root=$(mktemp -d "${TMPDIR:-/tmp}/dropout-macos-smoke.XXXXXX") mount_root="$smoke_root/mount" install_root="$smoke_root/install" attached=false