name: Release on: release: types: [published] workflow_call: inputs: tag: required: true type: string commit: required: true type: string target: default: all type: string workflow_dispatch: inputs: target: description: Build target required: true default: all type: choice options: - all - snap env: RELEASE_TAG: ${{ inputs.tag || github.event.release.tag_name }} jobs: build-appimage: runs-on: ubuntu-22.04 permissions: contents: write steps: - name: Checkout uses: actions/checkout@v4 with: ref: ${{ inputs.commit || github.sha }} # Full history so build.rs can measure the distance from the # release tag; a shallow clone would label the build `+g`. fetch-depth: 0 - name: Validate package versions id: version shell: bash run: python3 scripts/release.py versions --tag "$RELEASE_TAG" - name: Install Rust uses: dtolnay/rust-toolchain@stable - name: Install dependencies run: | sudo apt-get update -q sudo apt-get install -y \ libgl1-mesa-dev libx11-dev libxcursor-dev libxi-dev libxrandr-dev \ libxkbcommon-dev libwayland-dev libfontconfig1-dev libfreetype6-dev \ librsvg2-bin fuse libfuse2 - name: Build env: OCS_PATREON_TOKEN: ${{ secrets.OCS_PATREON_TOKEN }} run: cargo build --locked --release - name: Prepare AppDir run: | mkdir -p AppDir/usr/bin mkdir -p AppDir/usr/share/applications mkdir -p AppDir/usr/share/icons/hicolor/256x256/apps mkdir -p AppDir/usr/share/metainfo cp target/release/OpenCADStudio AppDir/usr/bin/OpenCADStudio cp packaging/OpenCADStudio.desktop AppDir/usr/share/applications/io.github.HakanSeven12.OpenCadStudio.desktop cp packaging/io.github.HakanSeven12.OpenCadStudio.metainfo.xml AppDir/usr/share/metainfo/ rsvg-convert -w 256 -h 256 assets/logo.svg \ -o AppDir/usr/share/icons/hicolor/256x256/apps/io.github.HakanSeven12.OpenCadStudio.png - name: Pack shared Linux payload run: tar -C AppDir -czf linux-payload.tar.gz usr - name: Upload shared Linux payload uses: actions/upload-artifact@v4 with: name: linux-payload path: linux-payload.tar.gz if-no-files-found: error retention-days: 7 - name: Download linuxdeploy run: | wget -q https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage chmod +x linuxdeploy-x86_64.AppImage - name: Build AppImage env: APPIMAGE_EXTRACT_AND_RUN: 1 run: | ./linuxdeploy-x86_64.AppImage \ --appdir AppDir \ --desktop-file AppDir/usr/share/applications/io.github.HakanSeven12.OpenCadStudio.desktop \ --icon-file AppDir/usr/share/icons/hicolor/256x256/apps/io.github.HakanSeven12.OpenCadStudio.png \ --output appimage mv Open*CAD*Studio*.AppImage OpenCADStudio.AppImage - name: Upload AppImage to release if: env.RELEASE_TAG != '' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | ASSET=OpenCADStudio-${{ env.RELEASE_TAG }}-linux-x86_64.AppImage mv OpenCADStudio.AppImage "$ASSET" gh release upload ${{ env.RELEASE_TAG }} "$ASSET" --clobber --repo ${{ github.repository }} build-snap: needs: build-appimage runs-on: ubuntu-22.04 timeout-minutes: 75 concurrency: group: snap cancel-in-progress: false permissions: actions: read contents: write env: SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }} steps: - name: Checkout uses: actions/checkout@v4 with: ref: ${{ inputs.commit || github.sha }} # Full history so build.rs can measure the distance from the # release tag; a shallow clone would label the build `+g`. fetch-depth: 0 - name: Validate package versions id: version shell: bash run: python3 scripts/release.py versions --tag "$RELEASE_TAG" - name: Download shared Linux payload uses: actions/download-artifact@v4 with: name: linux-payload path: . - name: Set package version id: package env: VERSION: ${{ steps.version.outputs.version }} run: | sed -i "s/^version: .*/version: '$VERSION'/" snap/snapcraft.yaml echo "version=$VERSION" >> "$GITHUB_OUTPUT" - name: Build snap id: snapcraft uses: snapcore/action-build@v1 - name: Normalize snap package name id: snap_package env: SNAP: ${{ steps.snapcraft.outputs.snap }} VERSION: ${{ steps.package.outputs.version }} run: | PACKAGE="OpenCADStudio-v${VERSION}-linux-x86_64.snap" mv "$SNAP" "$PACKAGE" echo "snap=$PACKAGE" >> "$GITHUB_OUTPUT" - name: Upload snap to release if: env.RELEASE_TAG != '' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} SNAP: ${{ steps.snap_package.outputs.snap }} run: gh release upload "${{ env.RELEASE_TAG }}" "$SNAP" --clobber - name: Upload snap as workflow artifact if: env.RELEASE_TAG == '' uses: actions/upload-artifact@v4 with: name: OpenCADStudio-v${{ steps.package.outputs.version }}-linux-x86_64-snap path: ${{ steps.snap_package.outputs.snap }} if-no-files-found: error - name: Publish snap to edge if: env.RELEASE_TAG != '' && env.SNAPCRAFT_STORE_CREDENTIALS != '' uses: snapcore/action-publish@v1 with: snap: ${{ steps.snap_package.outputs.snap }} release: edge build-windows: if: github.event_name == 'release' || inputs.tag != '' || inputs.target == 'all' runs-on: windows-latest permissions: contents: write steps: - name: Checkout uses: actions/checkout@v4 with: ref: ${{ inputs.commit || github.sha }} # Full history so build.rs can measure the distance from the # release tag; a shallow clone would label the build `+g`. fetch-depth: 0 - name: Validate package versions id: version shell: bash run: python3 scripts/release.py versions --tag "$RELEASE_TAG" - name: Install Rust uses: dtolnay/rust-toolchain@stable - name: Convert SVG icon to ICO shell: pwsh run: | magick assets/logo.svg ` -define icon:auto-resize=16,24,32,48,64,128,256 ` packaging/windows/AppIcon.ico - name: Convert DWG/DXF file icons to ICO shell: pwsh run: | # DWG/DXF Explorer file icons, from the single-source mimetype SVGs. magick assets/mimetypes/image-vnd.dwg.svg ` -define icon:auto-resize=16,24,32,48,64,128,256 ` packaging/windows/dwg.ico magick assets/mimetypes/image-vnd.dxf.svg ` -define icon:auto-resize=16,24,32,48,64,128,256 ` packaging/windows/dxf.ico - name: Build env: OCS_PATREON_TOKEN: ${{ secrets.OCS_PATREON_TOKEN }} run: | cargo build --locked --release cargo build --locked --release -p dwg-thumbnailer-win - name: Sign executable (Azure Trusted Signing) uses: azure/artifact-signing-action@v2 with: azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }} azure-client-id: ${{ secrets.AZURE_CLIENT_ID }} azure-client-secret: ${{ secrets.AZURE_CLIENT_SECRET }} endpoint: ${{ secrets.AZURE_ENDPOINT }} signing-account-name: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }} certificate-profile-name: ${{ secrets.AZURE_CERTIFICATE_PROFILE_NAME }} files: ${{ github.workspace }}\target\release\OpenCADStudio.exe file-digest: SHA256 timestamp-rfc3161: http://timestamp.acs.microsoft.com timestamp-digest: SHA256 - name: Build MSI installer shell: pwsh run: | $version = "${{ steps.version.outputs.msi }}" $candle = Join-Path $env:WIX "bin\candle.exe" $light = Join-Path $env:WIX "bin\light.exe" $exePath = (Resolve-Path target\release\OpenCADStudio.exe).Path $iconPath = (Resolve-Path packaging\windows\AppIcon.ico).Path # GPL-3 license RTF shown on the installer's EULA page. $licensePath = (Resolve-Path packaging\windows\License.rtf).Path # main.wxs holds the package/feature; ui.wxs holds the installer UI # and finish-screen launch checkbox. Both compile together. $mainWxs = (Resolve-Path packaging\windows\main.wxs).Path $uiWxs = (Resolve-Path packaging\windows\ui.wxs).Path # candle writes one .wixobj per source into this directory (the # trailing slash tells it -out is a folder, not a single file). $objDir = Join-Path $PWD "packaging\windows\" $msiPath = Join-Path $PWD "OpenCADStudio.msi" & $candle -arch x64 ` "-dVersion=$version" ` "-dSource=$exePath" ` "-dIcon=$iconPath" ` "-dLicense=$licensePath" ` $mainWxs $uiWxs ` -out $objDir if ($LASTEXITCODE -ne 0) { throw "candle failed" } $mainObj = Join-Path $objDir "main.wixobj" $uiObj = Join-Path $objDir "ui.wixobj" & $light $mainObj $uiObj -ext WixUIExtension -cultures:en-us -loc packaging/windows/strings.en-US.wxl -out $msiPath if ($LASTEXITCODE -ne 0) { throw "light failed" } - name: Sign MSI installer (Azure Trusted Signing) # The installer itself must be signed separately — light produces a # fresh, unsigned .msi even though the .exe inside is already signed. uses: azure/artifact-signing-action@v2 with: azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }} azure-client-id: ${{ secrets.AZURE_CLIENT_ID }} azure-client-secret: ${{ secrets.AZURE_CLIENT_SECRET }} endpoint: ${{ secrets.AZURE_ENDPOINT }} signing-account-name: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }} certificate-profile-name: ${{ secrets.AZURE_CERTIFICATE_PROFILE_NAME }} files: ${{ github.workspace }}\OpenCADStudio.msi file-digest: SHA256 timestamp-rfc3161: http://timestamp.acs.microsoft.com timestamp-digest: SHA256 - name: Upload artifacts to release if: env.RELEASE_TAG != '' shell: pwsh env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | $tag = "${{ env.RELEASE_TAG }}" $exe = "OpenCADStudio-$tag-windows-x86_64-portable.exe" $msi = "OpenCADStudio-$tag-windows-x86_64-installer.msi" Move-Item target\release\OpenCADStudio.exe $exe Move-Item OpenCADStudio.msi $msi gh release upload $tag ` $exe $msi ` --clobber --repo ${{ github.repository }} build-macos: if: github.event_name == 'release' || inputs.tag != '' || inputs.target == 'all' runs-on: macos-14 # Apple Silicon (arm64) only permissions: contents: write steps: - name: Checkout uses: actions/checkout@v4 with: ref: ${{ inputs.commit || github.sha }} # Full history so build.rs can measure the distance from the # release tag; a shallow clone would label the build `+g`. fetch-depth: 0 - name: Validate package versions id: version shell: bash run: python3 scripts/release.py versions --tag "$RELEASE_TAG" - name: Install Rust uses: dtolnay/rust-toolchain@stable with: targets: aarch64-apple-darwin - name: Install librsvg (for rsvg-convert) env: HOMEBREW_NO_AUTO_UPDATE: "1" run: brew install librsvg - name: Build env: OCS_PATREON_TOKEN: ${{ secrets.OCS_PATREON_TOKEN }} run: | cargo build --locked --release --target aarch64-apple-darwin cargo build --locked --release --target aarch64-apple-darwin -p dwg-thumbnailer # The launcher shim is the .app's actual CFBundleExecutable (#1039) # — see src/bin/ocs_launcher.rs for why. cargo build --locked --release --target aarch64-apple-darwin --bin ocs_launcher - name: Build .icns from SVG run: | mkdir -p OpenCADStudio.iconset # Render all the sizes Apple expects in an .icns. for SIZE in 16 32 64 128 256 512 1024; do rsvg-convert -w $SIZE -h $SIZE assets/logo.svg -o OpenCADStudio.iconset/icon_${SIZE}x${SIZE}.png done # @2x retina variants (half-size base name) for BASE in 16 32 128 256 512; do DOUBLE=$((BASE * 2)) cp OpenCADStudio.iconset/icon_${DOUBLE}x${DOUBLE}.png OpenCADStudio.iconset/icon_${BASE}x${BASE}@2x.png done iconutil -c icns OpenCADStudio.iconset -o AppIcon.icns - name: Build DWG/DXF document .icns from the same SVG sources run: | # Finder document icons (Info.plist CFBundleTypeIconFile = DWG/DXF), # generated from the single-source mimetype SVGs — never hand-drawn. for T in dwg dxf; do mkdir -p "$T.iconset" for SIZE in 16 32 64 128 256 512 1024; do rsvg-convert -w $SIZE -h $SIZE "assets/mimetypes/image-vnd.$T.svg" \ -o "$T.iconset/icon_${SIZE}x${SIZE}.png" done for BASE in 16 32 128 256 512; do cp "$T.iconset/icon_$((BASE*2))x$((BASE*2)).png" \ "$T.iconset/icon_${BASE}x${BASE}@2x.png" done iconutil -c icns "$T.iconset" -o "$(echo "$T" | tr a-z A-Z).icns" done - name: Build DWG QuickLook thumbnail extension (.appex) run: | set -e VERSION="${{ steps.version.outputs.cargo }}" EXT=DWGThumbnail.appex rm -rf "$EXT"; mkdir -p "$EXT/Contents/MacOS" swiftc \ -sdk "$(xcrun --sdk macosx --show-sdk-path)" \ -target arm64-apple-macos11 \ -O -parse-as-library -application-extension \ -module-name DWGThumbnail \ -import-objc-header crates/dwg-thumbnailer/macos/dwg_thumbnailer.h \ crates/dwg-thumbnailer/macos/ThumbnailProvider.swift \ -L target/aarch64-apple-darwin/release -ldwg_thumbnailer \ -framework QuickLookThumbnailing -framework CoreGraphics \ -framework ImageIO -framework Foundation -framework Security \ -framework SystemConfiguration -liconv \ -Xlinker -e -Xlinker _NSExtensionMain \ -o "$EXT/Contents/MacOS/DWGThumbnail" sed "s/__VERSION__/$VERSION/g" crates/dwg-thumbnailer/macos/Info.plist > "$EXT/Contents/Info.plist" - name: Assemble .app bundle run: | APP=OpenCADStudio.app rm -rf "$APP" mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources" "$APP/Contents/PlugIns" # Finder opens the launcher, which relays files to the sibling GUI. cp target/aarch64-apple-darwin/release/ocs_launcher "$APP/Contents/MacOS/OpenCADStudio" cp target/aarch64-apple-darwin/release/OpenCADStudio "$APP/Contents/MacOS/OpenCADStudio-App" chmod +x "$APP/Contents/MacOS/OpenCADStudio" "$APP/Contents/MacOS/OpenCADStudio-App" cp AppIcon.icns "$APP/Contents/Resources/AppIcon.icns" cp DWG.icns DXF.icns "$APP/Contents/Resources/" # QuickLook thumbnail extension. cp -R DWGThumbnail.appex "$APP/Contents/PlugIns/" # Substitute version into Info.plist. VERSION="${{ steps.version.outputs.cargo }}" sed "s/__VERSION__/$VERSION/g" packaging/Info.plist > "$APP/Contents/Info.plist" - name: Ad-hoc code-sign the bundle run: | codesign --force --deep --sign - --timestamp=none OpenCADStudio.app codesign --verify --strict --verbose=2 OpenCADStudio.app - name: Create .dmg run: | set +e for attempt in 1 2 3 4 5; do hdiutil detach "/Volumes/Open CAD Studio" >/dev/null 2>&1 || true rm -f OpenCADStudio.dmg hdiutil create \ -volname "Open CAD Studio" \ -srcfolder OpenCADStudio.app \ -ov -format UDZO \ OpenCADStudio.dmg && break echo "hdiutil create failed (attempt $attempt) — retrying in 5s" sleep 5 done set -e test -f OpenCADStudio.dmg - name: Upload .dmg to release if: env.RELEASE_TAG != '' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | ASSET=OpenCADStudio-${{ env.RELEASE_TAG }}-macos-arm64.dmg mv OpenCADStudio.dmg "$ASSET" gh release upload ${{ env.RELEASE_TAG }} "$ASSET" --clobber --repo ${{ github.repository }} - name: Emit Homebrew cask sha256 if: env.RELEASE_TAG != '' run: | VERSION="${{ env.RELEASE_TAG }}" VERSION="${VERSION#v}" SHA=$(shasum -a 256 OpenCADStudio-${{ env.RELEASE_TAG }}-macos-arm64.dmg | awk '{print $1}') { echo "## Homebrew cask bump" echo "Update \`packaging/homebrew/open-cad-studio.rb\`:" echo '```ruby' echo " version \"$VERSION\"" echo " sha256 \"$SHA\"" echo '```' } >> "$GITHUB_STEP_SUMMARY" verify: needs: [build-appimage, build-snap, build-windows, build-macos] if: github.event_name == 'release' || inputs.tag != '' runs-on: ubuntu-22.04 permissions: contents: read steps: - uses: actions/checkout@v4 with: ref: ${{ inputs.commit || github.sha }} - name: Verify native release assets and notes env: GH_TOKEN: ${{ github.token }} run: python3 scripts/release.py verify-native --tag "$RELEASE_TAG"