Skip to content

Release

Release #26

Workflow file for this run

# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
name: Release
on:
workflow_dispatch:
inputs:
version:
description: "Version tag (e.g., v0.1.0, v0.1.0-alpha.1)"
type: string
required: true
build_macos:
description: "Build macOS"
type: boolean
default: true
build_linux:
description: "Build Linux"
type: boolean
default: true
build_flatpak:
description: "Build Flatpak"
type: boolean
default: true
build_windows:
description: "Build Windows"
type: boolean
default: true
env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: 0
APP_NAME: trial-submission-studio
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: write
id-token: write
attestations: write
actions: read
jobs:
# ============================================================
# Prepare Release
# ============================================================
prepare:
name: Prepare
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.VERSION }}
build_number: ${{ steps.version.outputs.BUILD_NUMBER }}
build_macos: ${{ inputs.build_macos }}
build_linux: ${{ inputs.build_linux }}
build_flatpak: ${{ inputs.build_flatpak }}
build_windows: ${{ inputs.build_windows }}
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Validate version
run: |
VERSION="${{ inputs.version }}"
# Check format
if [[ ! "$VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$ ]]; then
echo "::error::Version must be in format v0.0.0 or v0.0.0-alpha.1"
exit 1
fi
echo "✓ Version format valid: $VERSION"
# Check if tag already exists locally
if git rev-parse "$VERSION" >/dev/null 2>&1; then
echo "::error::Tag $VERSION already exists locally"
exit 1
fi
# Check if tag already exists on remote
if git ls-remote --tags origin | grep -q "refs/tags/$VERSION$"; then
echo "::error::Tag $VERSION already exists on remote"
exit 1
fi
echo "✓ Tag $VERSION is available"
- name: Create verified tag
id: version
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
VERSION="${{ inputs.version }}"
COMMIT_COUNT=$(git rev-list --count HEAD)
BUILD_NUMBER="TSS-${{ github.run_number }}.${COMMIT_COUNT}"
SHA=$(git rev-parse HEAD)
# Create annotated tag via GitHub API (will be verified)
gh api repos/${{ github.repository }}/git/tags \
-f tag="$VERSION" \
-f message="Release $VERSION" \
-f object="$SHA" \
-f type="commit"
# Create tag reference
gh api repos/${{ github.repository }}/git/refs \
-f ref="refs/tags/$VERSION" \
-f sha="$SHA"
echo "VERSION=${VERSION}" >> $GITHUB_OUTPUT
echo "BUILD_NUMBER=${BUILD_NUMBER}" >> $GITHUB_OUTPUT
echo "✓ Created verified tag: ${VERSION}"
# ============================================================
# macOS Builds
# ============================================================
build-macos:
name: macOS (${{ matrix.arch }})
needs: prepare
if: needs.prepare.outputs.build_macos == 'true'
strategy:
fail-fast: false
matrix:
include:
- target: aarch64-apple-darwin
runner: macos-latest
arch: arm64
- target: x86_64-apple-darwin
runner: macos-15-intel
arch: x86_64
runs-on: ${{ matrix.runner }}
timeout-minutes: 60
env:
VERSION: ${{ needs.prepare.outputs.version }}
TARGET: ${{ matrix.target }}
TSS_BUILD_NUMBER: ${{ needs.prepare.outputs.build_number }}
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Cache Rust
uses: Swatinem/rust-cache@v2
with:
shared-key: release-${{ matrix.target }}
- name: Build
run: ./scripts/build-macos.sh
- name: Package
run: |
brew install create-dmg || true
SKIP_DMG=1 ./scripts/package-macos.sh
- name: Import certificate
env:
CERT_BASE64: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_P12_BASE64 }}
CERT_PASSWORD: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_PASSWORD }}
KEYCHAIN_PASSWORD: ${{ secrets.CI_KEYCHAIN_PASSWORD }}
run: |
security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security list-keychains -d user -s build.keychain login.keychain
security default-keychain -s build.keychain
security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security set-keychain-settings -t 3600 -u build.keychain
echo "$CERT_BASE64" | base64 --decode > cert.p12
security import cert.p12 -k build.keychain -P "$CERT_PASSWORD" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain
rm cert.p12
- name: Sign app
env:
IDENTITY: ${{ secrets.APPLE_CODESIGN_IDENTITY }}
run: |
APP_BUNDLE="Trial Submission Studio.app"
MACOS_DIR="$APP_BUNDLE/Contents/MacOS"
HELPER_BUNDLE="$APP_BUNDLE/Contents/Helpers/tss-updater-helper.app"
ENTITLEMENTS="assets/macos/TrialSubmissionStudio.app/Contents/entitlements.plist"
# 1. Sign helper bundle FIRST (nested bundle must be signed before outer)
echo "Signing helper bundle: $HELPER_BUNDLE"
codesign --force --options runtime \
--entitlements "$ENTITLEMENTS" \
--sign "$IDENTITY" --timestamp "$HELPER_BUNDLE"
# 2. Verify helper is signed
echo "Verifying helper signature..."
codesign --verify --strict "$HELPER_BUNDLE"
# 3. Sign the main binary
echo "Signing main binary: $MACOS_DIR/trial-submission-studio"
codesign --force --options runtime \
--entitlements "$ENTITLEMENTS" \
--sign "$IDENTITY" --timestamp "$MACOS_DIR/trial-submission-studio"
# 4. Sign the main bundle (this seals everything including the signed helper)
echo "Signing bundle: $APP_BUNDLE"
codesign --force --options runtime \
--entitlements "$ENTITLEMENTS" \
--sign "$IDENTITY" --timestamp "$APP_BUNDLE"
# 5. Verify entire bundle with deep check
echo "Verifying entire bundle..."
codesign --verify --deep --strict "$APP_BUNDLE"
- name: Notarize
env:
APPLE_ID: ${{ secrets.APPLE_NOTARIZATION_APPLE_ID }}
APP_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_APP_PASSWORD }}
TEAM_ID: ${{ secrets.APPLE_DEVELOPER_TEAM_ID }}
run: |
ditto -c -k --keepParent "Trial Submission Studio.app" notarize.zip
xcrun notarytool submit notarize.zip --apple-id "$APPLE_ID" --password "$APP_PASSWORD" --team-id "$TEAM_ID" --wait --timeout 30m
xcrun stapler staple "Trial Submission Studio.app"
rm notarize.zip
- name: Create DMG from signed app
run: |
DMG_NAME="${APP_NAME}-${VERSION}-${{ matrix.target }}.dmg"
APP_DIR="Trial Submission Studio.app"
rm -f "$DMG_NAME"
create-dmg \
--volname "Trial Submission Studio" \
--volicon "assets/macos/TrialSubmissionStudio.app/Contents/Resources/AppIcon.icns" \
--window-pos 200 120 \
--window-size 600 400 \
--icon-size 100 \
--icon "${APP_DIR}" 150 190 \
--hide-extension "${APP_DIR}" \
--app-drop-link 450 190 \
"$DMG_NAME" \
"$APP_DIR"
- name: Sign DMG
env:
IDENTITY: ${{ secrets.APPLE_CODESIGN_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_NOTARIZATION_APPLE_ID }}
APP_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_APP_PASSWORD }}
TEAM_ID: ${{ secrets.APPLE_DEVELOPER_TEAM_ID }}
run: |
DMG_NAME="${APP_NAME}-${VERSION}-${{ matrix.target }}.dmg"
codesign --force --sign "$IDENTITY" --timestamp "$DMG_NAME"
xcrun notarytool submit "$DMG_NAME" --apple-id "$APPLE_ID" --password "$APP_PASSWORD" --team-id "$TEAM_ID" --wait --timeout 30m
xcrun stapler staple "$DMG_NAME"
- name: Create archives
run: |
mkdir -p release-tar && cp -R "Trial Submission Studio.app" release-tar/
tar -czvf "${APP_NAME}-${VERSION}-${{ matrix.target }}.tar.gz" -C release-tar .
rm -rf release-tar
- name: Upload artifacts
uses: actions/upload-artifact@v6
with:
name: macos-${{ matrix.arch }}
path: |
${{ env.APP_NAME }}-${{ env.VERSION }}-${{ matrix.target }}.dmg
${{ env.APP_NAME }}-${{ env.VERSION }}-${{ matrix.target }}.tar.gz
retention-days: 7
- name: Cleanup
if: always()
run: security delete-keychain build.keychain 2>/dev/null || true
# ============================================================
# Linux Builds
# ============================================================
build-linux:
name: Linux (${{ matrix.arch }})
needs: prepare
if: needs.prepare.outputs.build_linux == 'true'
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-unknown-linux-gnu
runner: ubuntu-latest
arch: x86_64
- target: aarch64-unknown-linux-gnu
runner: ubuntu-24.04-arm
arch: arm64
runs-on: ${{ matrix.runner }}
timeout-minutes: 45
env:
VERSION: ${{ needs.prepare.outputs.version }}
TARGET: ${{ matrix.target }}
TSS_BUILD_NUMBER: ${{ needs.prepare.outputs.build_number }}
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Install dependencies
uses: awalsh128/cache-apt-pkgs-action@latest
with:
packages: libgtk-3-dev libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libssl-dev libxdo-dev libfuse2
version: 1.1
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Cache Rust
uses: Swatinem/rust-cache@v2
with:
shared-key: release-${{ matrix.target }}
- name: Build
run: ./scripts/build-linux.sh
- name: Package
run: ./scripts/package-linux.sh
- name: Upload artifacts
uses: actions/upload-artifact@v6
with:
name: linux-${{ matrix.arch }}
path: |
${{ env.APP_NAME }}-${{ env.VERSION }}-${{ matrix.target }}.tar.gz
${{ env.APP_NAME }}-${{ env.VERSION }}-${{ matrix.target }}.AppImage
retention-days: 7
# ============================================================
# Flatpak Builds
# Uses flatpak-github-actions for building Flatpak bundles
# Docs: https://github.com/flatpak/flatpak-github-actions
# ============================================================
build-flatpak:
name: Flatpak (${{ matrix.config.arch }})
needs: prepare
if: needs.prepare.outputs.build_flatpak == 'true'
strategy:
fail-fast: false
matrix:
config:
# Native runners for each architecture (faster than QEMU emulation)
- arch: x86_64
runner: ubuntu-24.04
- arch: aarch64
runner: ubuntu-24.04-arm
runs-on: ${{ matrix.config.runner }}
container:
# Use Flathub infrastructure image matching runtime version (25.08)
# Available images: https://github.com/flathub-infra/actions-images
image: ghcr.io/flathub-infra/flatpak-github-actions:freedesktop-25.08
options: --privileged
timeout-minutes: 90
env:
VERSION: ${{ needs.prepare.outputs.version }}
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Install flatpak-cargo-generator
run: |
pip install --quiet flatpak-cargo-generator
- name: Generate cargo sources
run: |
# Generate offline cargo sources from Cargo.lock
# This creates the vendored dependencies manifest
flatpak-cargo-generator Cargo.lock -o assets/flatpak/cargo-sources.json
echo "Generated cargo-sources.json with $(grep -c '"type":' assets/flatpak/cargo-sources.json || echo '?') entries"
- name: Update manifest git source
run: |
# Fix git ownership issue in container
git config --global --add safe.directory "$GITHUB_WORKSPACE"
# Get the commit hash for this version tag
COMMIT_HASH=$(git rev-parse HEAD)
# Update tag and commit in the manifest for reproducible builds
sed -i "s/tag: v.*/tag: ${{ env.VERSION }}/" assets/flatpak/io.github.rubentalstra.trial-submission-studio.yml
sed -i "s/commit: .*/commit: ${COMMIT_HASH}/" assets/flatpak/io.github.rubentalstra.trial-submission-studio.yml
echo "Updated manifest: tag=${{ env.VERSION }}, commit=${COMMIT_HASH}"
cat assets/flatpak/io.github.rubentalstra.trial-submission-studio.yml | grep -A2 "type: git"
- name: Build Flatpak
uses: flatpak/flatpak-github-actions/flatpak-builder@v6
with:
# Output bundle name
bundle: ${{ env.APP_NAME }}-${{ env.VERSION }}-${{ matrix.config.arch }}.flatpak
# Path to Flatpak manifest
manifest-path: assets/flatpak/io.github.rubentalstra.trial-submission-studio.yml
# Target architecture
arch: ${{ matrix.config.arch }}
# Cache key incorporates arch and content hashes for cache invalidation
cache-key: flatpak-builder-${{ matrix.config.arch }}-${{ hashFiles('Cargo.lock') }}
# Upload bundle as GitHub artifact
upload-artifact: true
# Enable verbose output for debugging build issues
verbose: true
- name: Lint manifest
run: |
flatpak-builder-lint \
--exceptions \
--gha-format \
manifest assets/flatpak/io.github.rubentalstra.trial-submission-studio.yml
- name: Lint appstream metadata
run: |
flatpak-builder-lint \
--exceptions \
--user-exceptions assets/flatpak/linter-exceptions.json \
--gha-format \
appstream flatpak_app/files/share/metainfo/io.github.rubentalstra.trial-submission-studio.metainfo.xml
- name: Lint build directory
run: |
flatpak-builder-lint \
--exceptions \
--user-exceptions assets/flatpak/linter-exceptions.json \
--gha-format \
builddir flatpak_app
- name: Lint repository
run: |
flatpak-builder-lint \
--exceptions \
--user-exceptions assets/flatpak/linter-exceptions.json \
--gha-format \
repo repo
- name: Save Flathub submission files
uses: actions/upload-artifact@v6
with:
name: flathub-${{ matrix.config.arch }}
path: |
assets/flatpak/io.github.rubentalstra.trial-submission-studio.yml
assets/flatpak/cargo-sources.json
retention-days: 7
# ============================================================
# Windows Builds
# ============================================================
build-windows:
name: Windows (${{ matrix.arch }})
needs: prepare
if: needs.prepare.outputs.build_windows == 'true'
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-pc-windows-msvc
runner: windows-latest
arch: x86_64
- target: aarch64-pc-windows-msvc
runner: windows-11-arm
arch: arm64
runs-on: ${{ matrix.runner }}
timeout-minutes: 60
env:
VERSION: ${{ needs.prepare.outputs.version }}
TARGET: ${{ matrix.target }}
TSS_BUILD_NUMBER: ${{ needs.prepare.outputs.build_number }}
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Cache Rust
uses: Swatinem/rust-cache@v2
with:
shared-key: release-${{ matrix.target }}
- name: Build
shell: bash
run: ./scripts/build-windows.sh
- name: Check signing
id: signing
shell: pwsh
env:
TOKEN: ${{ secrets.SIGNPATH_API_TOKEN }}
run: |
if ($env:TOKEN) { "available=true" >> $env:GITHUB_OUTPUT }
else { "available=false" >> $env:GITHUB_OUTPUT }
- name: Sign (if available)
if: steps.signing.outputs.available == 'true'
uses: signpath/github-action-submit-signing-request@v1
with:
api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
organization-id: ${{ secrets.SIGNPATH_ORGANIZATION_ID }}
project-slug: ${{ secrets.SIGNPATH_PROJECT_SLUG }}
signing-policy-slug: ${{ secrets.SIGNPATH_SIGNING_POLICY_SLUG }}
artifact-configuration-slug: release-signing
github-artifact-id: ${{ github.run_id }}
wait-for-completion: true
- name: Package
shell: pwsh
run: |
$binary = "target/${{ matrix.target }}/release/$env:APP_NAME.exe"
$zipName = "$env:APP_NAME-$env:VERSION-${{ matrix.target }}.zip"
$exeName = "$env:APP_NAME-$env:VERSION-${{ matrix.target }}.exe"
Compress-Archive -Path $binary -DestinationPath $zipName
Copy-Item $binary $exeName
- name: Upload artifacts
uses: actions/upload-artifact@v6
with:
name: windows-${{ matrix.arch }}
path: |
${{ env.APP_NAME }}-${{ env.VERSION }}-${{ matrix.target }}.zip
${{ env.APP_NAME }}-${{ env.VERSION }}-${{ matrix.target }}.exe
retention-days: 7
# ============================================================
# Publish Release
# ============================================================
publish:
name: Publish
needs: [ prepare, build-macos, build-linux, build-flatpak, build-windows ]
if: |
always() &&
(needs.build-macos.result == 'success' || needs.build-macos.result == 'skipped') &&
(needs.build-linux.result == 'success' || needs.build-linux.result == 'skipped') &&
(needs.build-flatpak.result == 'success' || needs.build-flatpak.result == 'skipped') &&
(needs.build-windows.result == 'success' || needs.build-windows.result == 'skipped') &&
(needs.build-macos.result == 'success' || needs.build-linux.result == 'success' || needs.build-flatpak.result == 'success' || needs.build-windows.result == 'success')
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Download artifacts (excluding Flathub files)
uses: actions/download-artifact@v7
with:
path: artifacts
merge-multiple: true
- name: Remove Flathub submission files
run: rm -rf artifacts/flathub-*
- name: List artifacts
run: find artifacts -type f -exec ls -lh {} \; 2>/dev/null || ls -la artifacts/
- name: Generate attestations
uses: actions/attest-build-provenance@v2
with:
subject-path: "artifacts/*"
- name: Determine prerelease
id: prerelease
env:
VERSION: ${{ needs.prepare.outputs.version }}
run: |
if [[ "$VERSION" == *"-alpha"* ]] || [[ "$VERSION" == *"-beta"* ]] || [[ "$VERSION" == *"-rc"* ]]; then
echo "PRERELEASE=true" >> $GITHUB_OUTPUT
else
echo "PRERELEASE=false" >> $GITHUB_OUTPUT
fi
- name: Create release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.prepare.outputs.version }}
name: "${{ needs.prepare.outputs.version }}"
draft: false
prerelease: ${{ steps.prerelease.outputs.PRERELEASE }}
generate_release_notes: true
files: artifacts/*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# ============================================================
# Notify Flathub (Post-Approval Only)
# ============================================================
# This job is disabled until after your app is approved on Flathub.
# Once flathub/io.github.rubentalstra.trial-submission-studio exists,
# set FLATHUB_TOKEN secret to enable automatic update notifications.
notify-flathub:
name: Notify Flathub
needs: [ prepare, publish ]
if: |
false &&
needs.publish.result == 'success' &&
!contains(needs.prepare.outputs.version, '-alpha') &&
!contains(needs.prepare.outputs.version, '-beta')
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Trigger Flathub update
env:
FLATHUB_TOKEN: ${{ secrets.FLATHUB_TOKEN }}
run: |
if [[ -z "$FLATHUB_TOKEN" ]]; then
echo "::warning::FLATHUB_TOKEN not set. Skipping Flathub notification."
exit 0
fi
gh api repos/flathub/io.github.rubentalstra.trial-submission-studio/dispatches \
-f event_type="new-release" \
-f client_payload='{"version": "${{ needs.prepare.outputs.version }}"}'