name: release-beta on: workflow_dispatch: inputs: signed: description: "Build signed/notarized mac artifacts. Disable only for explicit unsigned validation releases." required: true type: boolean default: true enable_mac: description: "Build and publish mac arm64 beta artifacts." required: true type: boolean default: true enable_win: description: "Build and publish Windows x64 beta artifacts." required: true type: boolean default: true enable_linux: description: "Build and publish Linux x64 AppImage beta artifacts." required: true type: boolean default: false permissions: contents: write concurrency: group: open-design-release-beta cancel-in-progress: false jobs: metadata: name: Prepare beta metadata runs-on: ubuntu-latest env: GH_TOKEN: ${{ github.token }} GITHUB_REPOSITORY: ${{ github.repository }} OPEN_DESIGN_RELEASE_SIGNED: ${{ inputs.signed }} outputs: asset_version_suffix: ${{ steps.beta.outputs.asset_version_suffix }} base_version: ${{ steps.beta.outputs.base_version }} beta_tag: ${{ steps.beta.outputs.beta_tag }} beta_version: ${{ steps.beta.outputs.beta_version }} branch: ${{ steps.beta.outputs.branch }} commit: ${{ steps.beta.outputs.commit }} release_name: ${{ steps.beta.outputs.release_name }} signed: ${{ steps.beta.outputs.signed }} version_tag: ${{ steps.beta.outputs.version_tag }} steps: - name: Checkout uses: actions/checkout@v6.0.2 with: fetch-depth: 0 - name: Setup Node.js uses: actions/setup-node@v6 with: node-version: 24 - name: Prepare beta release metadata id: beta run: node --experimental-strip-types ./scripts/release-beta.ts build_mac: name: Build beta mac arm64 needs: metadata if: ${{ inputs.enable_mac }} runs-on: macos-14 env: GH_TOKEN: ${{ github.token }} steps: - name: Checkout uses: actions/checkout@v6.0.2 with: fetch-depth: 0 - name: Setup pnpm uses: pnpm/action-setup@v5 with: version: 10.33.2 - name: Setup Node.js uses: actions/setup-node@v6 with: node-version: 24 - name: Install dependencies run: pnpm install --frozen-lockfile - name: Apply beta package version run: npm pkg set "version=${{ needs.metadata.outputs.beta_version }}" --prefix apps/packaged - name: Prepare Apple signing certificate if: ${{ inputs.signed }} env: APPLE_SIGNING_CERTIFICATE_BASE64: ${{ secrets.APPLE_SIGNING_CERTIFICATE_BASE64 }} APPLE_SIGNING_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_SIGNING_CERTIFICATE_PASSWORD }} run: | set -euo pipefail cert_path="$RUNNER_TEMP/open-design-signing.p12" if ! printf '%s' "$APPLE_SIGNING_CERTIFICATE_BASE64" | base64 --decode > "$cert_path" 2>/dev/null; then printf '%s' "$APPLE_SIGNING_CERTIFICATE_BASE64" | base64 -D > "$cert_path" fi { echo "CSC_LINK=$cert_path" echo "CSC_KEY_PASSWORD=$APPLE_SIGNING_CERTIFICATE_PASSWORD" } >> "$GITHUB_ENV" - name: Build beta mac artifacts env: APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} run: | set -euo pipefail signed_flag="" if [ "${{ inputs.signed }}" = "true" ]; then signed_flag="--signed" fi pnpm exec tools-pack mac build \ --dir "$RUNNER_TEMP/tools-pack" \ --namespace release-beta \ --portable \ --mac-compression normal \ --to all \ --json \ $signed_flag - name: Smoke beta mac packaged runtime working-directory: e2e env: OD_PACKAGED_E2E_MAC: "1" OD_PACKAGED_E2E_NAMESPACE: release-beta OD_PACKAGED_E2E_TOOLS_PACK_DIR: ${{ runner.temp }}/tools-pack run: pnpm test specs/mac.spec.ts - name: Prepare beta assets id: assets run: | set -euo pipefail release_dir="$RUNNER_TEMP/release-assets" mkdir -p "$release_dir" source_dmg="$RUNNER_TEMP/tools-pack/out/mac/namespaces/release-beta/dmg/Open Design-release-beta.dmg" source_zip="$RUNNER_TEMP/tools-pack/out/mac/namespaces/release-beta/zip/Open Design-release-beta.zip" if [ ! -f "$source_dmg" ]; then echo "expected dmg not found at $source_dmg" >&2 exit 1 fi if [ ! -f "$source_zip" ]; then echo "expected zip not found at $source_zip" >&2 exit 1 fi asset_suffix="${{ needs.metadata.outputs.asset_version_suffix }}" versioned_dmg="open-design-${{ needs.metadata.outputs.beta_version }}${asset_suffix}-mac-arm64.dmg" versioned_zip="open-design-${{ needs.metadata.outputs.beta_version }}${asset_suffix}-mac-arm64.zip" dmg_checksum_file="$versioned_dmg.sha256" zip_checksum_file="$versioned_zip.sha256" cp "$source_dmg" "$release_dir/$versioned_dmg" cp "$source_zip" "$release_dir/$versioned_zip" ( cd "$release_dir" shasum -a 256 "$versioned_dmg" > "$dmg_checksum_file" shasum -a 256 "$versioned_zip" > "$zip_checksum_file" ) zip_sha512="$(openssl dgst -sha512 -binary "$release_dir/$versioned_zip" | openssl base64 -A)" zip_size="$(stat -f%z "$release_dir/$versioned_zip")" zip_url="https://github.com/${GITHUB_REPOSITORY}/releases/download/${{ needs.metadata.outputs.version_tag }}/$versioned_zip" release_date="$(date -u +%Y-%m-%dT%H:%M:%SZ)" cat > "$release_dir/latest-mac.yml" <- pnpm exec tools-pack win build --dir "${{ runner.temp }}/tools-pack" --namespace release-beta-win --portable --to nsis --json - name: Prepare windows beta assets shell: pwsh run: | $releaseDir = Join-Path $env:RUNNER_TEMP "release-assets" New-Item -ItemType Directory -Force -Path $releaseDir | Out-Null $sourceInstaller = Join-Path $env:RUNNER_TEMP "tools-pack/out/win/namespaces/release-beta-win/builder/Open Design-release-beta-win-setup.exe" $sourceBlockmap = Join-Path $env:RUNNER_TEMP "tools-pack/out/win/namespaces/release-beta-win/builder/Open Design-release-beta-win-setup.exe.blockmap" if (!(Test-Path $sourceInstaller)) { throw "expected installer not found at $sourceInstaller" } if (!(Test-Path $sourceBlockmap)) { throw "expected blockmap not found at $sourceBlockmap" } $windowsAssetSuffix = ".unsigned" $versionedInstaller = "open-design-${{ needs.metadata.outputs.beta_version }}$windowsAssetSuffix-win-x64-setup.exe" $versionedBlockmap = "open-design-${{ needs.metadata.outputs.beta_version }}$windowsAssetSuffix-win-x64-setup.exe.blockmap" $checksumFile = "$versionedInstaller.sha256" Copy-Item $sourceInstaller (Join-Path $releaseDir $versionedInstaller) Copy-Item $sourceBlockmap (Join-Path $releaseDir $versionedBlockmap) $installerPath = Join-Path $releaseDir $versionedInstaller $hash = (Get-FileHash -Path $installerPath -Algorithm SHA256).Hash.ToLowerInvariant() "$hash $versionedInstaller" | Set-Content -Path (Join-Path $releaseDir $checksumFile) $installerBytes = [System.IO.File]::ReadAllBytes($installerPath) $installerSha512 = [System.Convert]::ToBase64String([System.Security.Cryptography.SHA512]::Create().ComputeHash($installerBytes)) $installerSize = (Get-Item $installerPath).Length $installerUrl = "https://github.com/$env:GITHUB_REPOSITORY/releases/download/${{ needs.metadata.outputs.version_tag }}/$versionedInstaller" $releaseDate = [DateTime]::UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ") @( 'version: "${{ needs.metadata.outputs.beta_version }}"' 'files:' " - url: `"$installerUrl`"" " sha512: `"$installerSha512`"" " size: $installerSize" "path: `"$installerUrl`"" "sha512: `"$installerSha512`"" "releaseDate: `"$releaseDate`"" "releaseNotes: `"Open Design beta ${{ needs.metadata.outputs.beta_version }}$windowsAssetSuffix`"" ) | Set-Content -Path (Join-Path $releaseDir "latest.yml") - name: Upload windows release bundle uses: actions/upload-artifact@v7 with: name: open-design-beta-win-release-assets path: ${{ runner.temp }}/release-assets build_linux: name: Build beta linux x64 needs: metadata if: ${{ inputs.enable_linux }} runs-on: ubuntu-latest env: GH_TOKEN: ${{ github.token }} steps: - name: Checkout uses: actions/checkout@v6.0.2 with: fetch-depth: 0 - name: Setup pnpm uses: pnpm/action-setup@v5 with: version: 10.33.2 - name: Setup Node.js uses: actions/setup-node@v6 with: node-version: 24 - name: Install dependencies run: pnpm install --frozen-lockfile - name: Apply beta package version env: BETA_VERSION: ${{ needs.metadata.outputs.beta_version }} run: npm pkg set "version=$BETA_VERSION" --prefix apps/packaged # `--containerized` builds the AppImage inside the electronuserland/builder # Docker image (glibc 2.27 baseline) so the resulting binary runs on older # distros than ubuntu-latest's glibc 2.39. Docker is preinstalled on the # GitHub-hosted ubuntu-latest runner, so no extra setup is required. - name: Build beta linux artifacts run: | set -euo pipefail pnpm exec tools-pack linux build \ --dir "$RUNNER_TEMP/tools-pack" \ --namespace release-beta-linux \ --portable \ --to appimage \ --containerized \ --json - name: Prepare linux beta assets env: BETA_VERSION: ${{ needs.metadata.outputs.beta_version }} run: | set -euo pipefail release_dir="$RUNNER_TEMP/release-assets" mkdir -p "$release_dir" source_appimage="$RUNNER_TEMP/tools-pack/out/linux/namespaces/release-beta-linux/builder/Open Design-release-beta-linux.AppImage" if [ ! -f "$source_appimage" ]; then echo "expected AppImage not found at $source_appimage" >&2 exit 1 fi # Linux currently has no signing path in tools-pack, so the suffix is # hardcoded to .unsigned (matching the windows convention above). linux_asset_suffix=".unsigned" versioned_appimage="open-design-${BETA_VERSION}${linux_asset_suffix}-linux-x64.AppImage" checksum_file="$versioned_appimage.sha256" cp "$source_appimage" "$release_dir/$versioned_appimage" ( cd "$release_dir" sha256sum "$versioned_appimage" > "$checksum_file" ) - name: Upload linux release bundle uses: actions/upload-artifact@v7 with: name: open-design-beta-linux-release-assets path: ${{ runner.temp }}/release-assets publish: name: Publish beta release needs: - metadata - build_mac - build_win - build_linux if: >- ${{ always() && !cancelled() && needs.metadata.result == 'success' && (inputs.enable_mac || inputs.enable_win || inputs.enable_linux) && (!inputs.enable_mac || needs.build_mac.result == 'success') && (!inputs.enable_win || needs.build_win.result == 'success') && (!inputs.enable_linux || needs.build_linux.result == 'success') }} runs-on: ubuntu-latest env: GH_TOKEN: ${{ github.token }} ENABLE_MAC: ${{ inputs.enable_mac }} ENABLE_WIN: ${{ inputs.enable_win }} ENABLE_LINUX: ${{ inputs.enable_linux }} steps: - name: Checkout uses: actions/checkout@v6.0.2 with: fetch-depth: 0 - name: Download mac release bundle if: ${{ inputs.enable_mac }} uses: actions/download-artifact@v8 with: name: open-design-beta-mac-release-assets path: ${{ runner.temp }}/release-assets/mac - name: Download windows release bundle if: ${{ inputs.enable_win }} uses: actions/download-artifact@v8 with: name: open-design-beta-win-release-assets path: ${{ runner.temp }}/release-assets/win - name: Download linux release bundle if: ${{ inputs.enable_linux }} uses: actions/download-artifact@v8 with: name: open-design-beta-linux-release-assets path: ${{ runner.temp }}/release-assets/linux - name: Move beta tags to current commit run: | set -euo pipefail git tag -f "${{ needs.metadata.outputs.version_tag }}" "$GITHUB_SHA" git push origin "refs/tags/${{ needs.metadata.outputs.version_tag }}" --force git tag -f "${{ needs.metadata.outputs.beta_tag }}" "$GITHUB_SHA" git push origin "refs/tags/${{ needs.metadata.outputs.beta_tag }}" --force - name: Write release notes id: notes run: | set -euo pipefail version_notes_file="$RUNNER_TEMP/open-design-beta-version-notes.md" latest_notes_file="$RUNNER_TEMP/open-design-beta-latest-notes.md" cat > "$version_notes_file" < "$latest_notes_file" <> "$GITHUB_OUTPUT" - name: Create or update immutable beta prerelease run: | set -euo pipefail all_release_dir="$RUNNER_TEMP/release-assets/all" mkdir -p "$all_release_dir" for asset_dir in "$RUNNER_TEMP/release-assets/mac" "$RUNNER_TEMP/release-assets/win" "$RUNNER_TEMP/release-assets/linux"; do if [ -d "$asset_dir" ] && compgen -G "$asset_dir/*" > /dev/null; then cp "$asset_dir"/* "$all_release_dir/" fi done if ! compgen -G "$all_release_dir/*" > /dev/null; then echo "no enabled beta release assets were found" >&2 exit 1 fi declare -A current_release_assets=() for asset_path in "$all_release_dir"/*; do current_release_assets["$(basename "$asset_path")"]=1 done if gh release view "${{ needs.metadata.outputs.version_tag }}" >/dev/null 2>&1; then gh release edit "${{ needs.metadata.outputs.version_tag }}" \ --title "${{ needs.metadata.outputs.release_name }}" \ --notes-file "${{ steps.notes.outputs.version_notes_file }}" \ --prerelease else gh release create "${{ needs.metadata.outputs.version_tag }}" \ --target "$GITHUB_SHA" \ --title "${{ needs.metadata.outputs.release_name }}" \ --notes-file "${{ steps.notes.outputs.version_notes_file }}" \ --prerelease fi gh release upload "${{ needs.metadata.outputs.version_tag }}" "$all_release_dir"/* --clobber while IFS= read -r asset_name; do if [ -n "$asset_name" ] && [ -z "${current_release_assets[$asset_name]+x}" ]; then gh release delete-asset "${{ needs.metadata.outputs.version_tag }}" "$asset_name" --yes fi done < <(gh release view "${{ needs.metadata.outputs.version_tag }}" --json assets --jq '.assets[].name') - name: Create or update beta channel feed run: | set -euo pipefail latest_mac_path="$RUNNER_TEMP/release-assets/mac/latest-mac.yml" latest_win_path="$RUNNER_TEMP/release-assets/win/latest.yml" feed_assets=() if [ "$ENABLE_MAC" = "true" ]; then if [ ! -f "$latest_mac_path" ]; then echo "expected mac feed not found at $latest_mac_path" >&2 exit 1 fi feed_assets+=("$latest_mac_path") fi if [ "$ENABLE_WIN" = "true" ]; then if [ ! -f "$latest_win_path" ]; then echo "expected windows feed not found at $latest_win_path" >&2 exit 1 fi feed_assets+=("$latest_win_path") fi declare -A current_feed_assets=() for feed_asset in "${feed_assets[@]}"; do current_feed_assets["$(basename "$feed_asset")"]=1 done if gh release view "${{ needs.metadata.outputs.beta_tag }}" >/dev/null 2>&1; then gh release edit "${{ needs.metadata.outputs.beta_tag }}" \ --title "Open Design Beta Latest" \ --notes-file "${{ steps.notes.outputs.latest_notes_file }}" \ --prerelease else gh release create "${{ needs.metadata.outputs.beta_tag }}" \ --target "$GITHUB_SHA" \ --title "Open Design Beta Latest" \ --notes-file "${{ steps.notes.outputs.latest_notes_file }}" \ --prerelease fi if [ "${#feed_assets[@]}" -gt 0 ]; then gh release upload "${{ needs.metadata.outputs.beta_tag }}" "${feed_assets[@]}" --clobber fi while IFS= read -r asset_name; do if [ -n "$asset_name" ] && [ -z "${current_feed_assets[$asset_name]+x}" ]; then gh release delete-asset "${{ needs.metadata.outputs.beta_tag }}" "$asset_name" --yes fi done < <(gh release view "${{ needs.metadata.outputs.beta_tag }}" --json assets --jq '.assets[].name') - name: Publish summary run: | { echo "## Beta release" echo "- Channel: beta" echo "- Version: ${{ needs.metadata.outputs.beta_version }}" echo "- Version tag: ${{ needs.metadata.outputs.version_tag }}" echo "- Channel feed tag: ${{ needs.metadata.outputs.beta_tag }}" echo "- mac enabled: $ENABLE_MAC" echo "- mac signed/notarized: ${{ needs.metadata.outputs.signed }}" echo "- windows enabled: $ENABLE_WIN" echo "- windows signed: false" echo "- linux enabled: $ENABLE_LINUX" if [ "$ENABLE_MAC" = "true" ]; then echo "- mac assets: open-design-${{ needs.metadata.outputs.beta_version }}${{ needs.metadata.outputs.asset_version_suffix }}-mac-arm64.dmg, open-design-${{ needs.metadata.outputs.beta_version }}${{ needs.metadata.outputs.asset_version_suffix }}-mac-arm64.zip" fi if [ "$ENABLE_WIN" = "true" ]; then echo "- win assets: open-design-${{ needs.metadata.outputs.beta_version }}.unsigned-win-x64-setup.exe, open-design-${{ needs.metadata.outputs.beta_version }}.unsigned-win-x64-setup.exe.blockmap" fi if [ "$ENABLE_LINUX" = "true" ]; then echo "- linux assets: open-design-${{ needs.metadata.outputs.beta_version }}.unsigned-linux-x64.AppImage" fi echo "- Feeds: enabled mac/win feeds only (no latest-linux.yml; AppImage updater not yet wired)" } >> "$GITHUB_STEP_SUMMARY"