Files
OneCAD-Draft/.github/workflows/release.yml
T
Hakan SevenandClaude Opus 5.5 47eef134cb fix(release): rebuild a tag from the commit the tag points at
The v2026.40 rebuild was dispatched with a separate `commit` input copied
from a stale local tag, so Windows/macOS assets were built from 20803948
instead of 8e122a4e. Dispatch now takes only `tag` and checks the tag out.
Reverts 4700e263: the skipped Python plugin was a symptom of that commit.

Fixes #1612

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-05 15:54:32 +03:00

569 lines
23 KiB
YAML

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
# Rebuild an already-published release from the commit its tag points at.
tag:
description: Existing release tag to rebuild (optional)
default: ''
type: string
env:
RELEASE_TAG: ${{ inputs.tag || github.event.release.tag_name }}
# Mirrors .cargo/config.toml so rebuilding a commit that predates the bump
# still gives rustc 1.99 enough stack.
RUST_MIN_STACK: "67108864"
jobs:
build-appimage:
runs-on: ubuntu-22.04
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ inputs.commit || inputs.tag || github.sha }}
# Full history so build.rs can measure the distance from the
# release tag; a shallow clone would label the build `+g<hash>`.
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 || inputs.tag || github.sha }}
# Full history so build.rs can measure the distance from the
# release tag; a shallow clone would label the build `+g<hash>`.
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
# Step-level `if:` cannot read the `secrets` context, so the signing
# waterfall is driven by these boolean mirrors of the credential secrets.
env:
AZURE_SIGNING_CONFIGURED: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME != '' }}
PFX_CONFIGURED: ${{ secrets.WINDOWS_PFX_BASE64 != '' }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ inputs.commit || inputs.tag || github.sha }}
# Full history so build.rs can measure the distance from the
# release tag; a shallow clone would label the build `+g<hash>`.
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 }}
shell: bash
run: |
cargo build --locked --release
cargo build --locked --release -p dwg-thumbnailer-win
# Forks do not inherit repository secrets, so every signing step is
# gated on its credentials being present and the pipeline falls through:
# Azure Trusted Signing (upstream setup) -> PFX certificate -> unsigned.
- name: Sign executable (Azure Trusted Signing)
if: env.AZURE_SIGNING_CONFIGURED == 'true'
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: Import signing certificate (PFX fallback)
if: env.AZURE_SIGNING_CONFIGURED != 'true' && env.PFX_CONFIGURED == 'true'
shell: pwsh
env:
WINDOWS_PFX_BASE64: ${{ secrets.WINDOWS_PFX_BASE64 }}
run: |
$pfxPath = Join-Path $env:RUNNER_TEMP "signing.pfx"
[IO.File]::WriteAllBytes($pfxPath, [Convert]::FromBase64String($env:WINDOWS_PFX_BASE64))
"Imported signing certificate to $pfxPath"
- name: Sign executable (certificate)
if: env.AZURE_SIGNING_CONFIGURED != 'true' && env.PFX_CONFIGURED == 'true'
shell: pwsh
env:
WINDOWS_PFX_PASSWORD: ${{ secrets.WINDOWS_PFX_PASSWORD }}
run: |
$signtool = Get-ChildItem "C:\Program Files (x86)\Windows Kits\10\bin" -Recurse -Filter signtool.exe |
Where-Object { $_.DirectoryName -match 'x64' } |
Sort-Object FullName -Descending | Select-Object -First 1 -ExpandProperty FullName
if (-not $signtool) { throw "signtool.exe not found in the Windows SDK" }
& $signtool sign /fd SHA256 /tr https://timestamp.digicert.com /td SHA256 `
/f (Join-Path $env:RUNNER_TEMP "signing.pfx") /p $env:WINDOWS_PFX_PASSWORD `
(Resolve-Path target\release\OpenCADStudio.exe).Path
if ($LASTEXITCODE -ne 0) { throw "signtool failed for the executable" }
- name: Note unsigned executable
if: env.AZURE_SIGNING_CONFIGURED != 'true' && env.PFX_CONFIGURED != 'true'
shell: pwsh
run: |
"No signing credentials configured - add the Azure Trusted Signing secrets or WINDOWS_PFX_BASE64/WINDOWS_PFX_PASSWORD to sign. Building UNSIGNED." >> $env:GITHUB_STEP_SUMMARY
- 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.
if: env.AZURE_SIGNING_CONFIGURED == 'true'
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: Sign MSI installer (certificate)
if: env.AZURE_SIGNING_CONFIGURED != 'true' && env.PFX_CONFIGURED == 'true'
shell: pwsh
env:
WINDOWS_PFX_PASSWORD: ${{ secrets.WINDOWS_PFX_PASSWORD }}
run: |
$signtool = Get-ChildItem "C:\Program Files (x86)\Windows Kits\10\bin" -Recurse -Filter signtool.exe |
Where-Object { $_.DirectoryName -match 'x64' } |
Sort-Object FullName -Descending | Select-Object -First 1 -ExpandProperty FullName
if (-not $signtool) { throw "signtool.exe not found in the Windows SDK" }
& $signtool sign /fd SHA256 /tr https://timestamp.digicert.com /td SHA256 `
/f (Join-Path $env:RUNNER_TEMP "signing.pfx") /p $env:WINDOWS_PFX_PASSWORD `
(Resolve-Path OpenCADStudio.msi).Path
if ($LASTEXITCODE -ne 0) { throw "signtool failed for the MSI" }
- name: Note unsigned MSI
if: env.AZURE_SIGNING_CONFIGURED != 'true' && env.PFX_CONFIGURED != 'true'
shell: pwsh
run: |
"MSI left unsigned - no signing credentials configured." >> $env:GITHUB_STEP_SUMMARY
- 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 || inputs.tag || github.sha }}
# Full history so build.rs can measure the distance from the
# release tag; a shallow clone would label the build `+g<hash>`.
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/"
# First-party Python scripting plugin. It remains out of process and
# uses the same API/toolchain as this host build.
bash plugins/opencad-python/tools/stage-bundled.sh \
"$APP/Contents/Resources/plugins/opencad.python" \
--target aarch64-apple-darwin
# 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 --sign - --timestamp=none OpenCADStudio.app/Contents/MacOS/OpenCADStudio-App
codesign --force --sign - --timestamp=none \
--entitlements crates/dwg-thumbnailer/macos/entitlements.plist \
OpenCADStudio.app/Contents/PlugIns/DWGThumbnail.appex
codesign --force --sign - --timestamp=none OpenCADStudio.app
codesign --verify --deep --strict --verbose=2 OpenCADStudio.app
# --xml keeps newer codesign builds from prefixing text headers,
# which made the entitlements parse fail with InvalidFileException.
codesign -d --entitlements - --xml OpenCADStudio.app/Contents/PlugIns/DWGThumbnail.appex \
| python3 -c 'import io, plistlib, sys; ent = plistlib.load(io.BytesIO(sys.stdin.buffer.read())); print("entitlements:", ent); assert ent.get("com.apple.security.app-sandbox") is True'
- 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 || inputs.tag || github.sha }}
- name: Verify native release assets and notes
env:
GH_TOKEN: ${{ github.token }}
run: python3 scripts/release.py verify-native --tag "$RELEASE_TAG"