Skip to content

ci: support prerelease tags (vX.Y.Z-suffix) in release workflow #1

ci: support prerelease tags (vX.Y.Z-suffix) in release workflow

ci: support prerelease tags (vX.Y.Z-suffix) in release workflow #1

Workflow file for this run

name: Release
on:
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+'
- 'v[0-9]+.[0-9]+.[0-9]+-*'
# Each build job is independent so all three platforms compile in parallel.
# The final job collects all artifacts and creates a draft GitHub Release.
jobs:
# ---------------------------------------------------------------------------
# macOS — .app bundle + .pkg installer.
#
# The job runs in three modes depending on which repository secrets are set:
#
# 1. No signing secrets:
# Produces UNSIGNED .app + UNSIGNED .pkg (current baseline behavior).
#
# 2. Code-signing secrets set (APPLE_DEVELOPER_ID_APPLICATION,
# APPLE_CERTIFICATES_P12_BASE64, APPLE_CERTIFICATES_P12_PASSWORD):
# Signs the .app with Hardened Runtime + entitlements; signs the .pkg
# if APPLE_DEVELOPER_ID_INSTALLER is also set.
#
# 3. Above PLUS notarization secrets set (APPLE_ID,
# APPLE_APP_SPECIFIC_PASSWORD, APPLE_TEAM_ID):
# Submits to Apple notary service, waits, and staples.
#
# Required repository secrets for full signed+notarized distribution:
#
# APPLE_DEVELOPER_ID_APPLICATION Certificate common name, e.g.
# "Developer ID Application: NAME (TEAMID)"
# APPLE_DEVELOPER_ID_INSTALLER Installer cert name, e.g.
# "Developer ID Installer: NAME (TEAMID)"
# APPLE_CERTIFICATES_P12_BASE64 Base64-encoded P12 containing both certs
# and their private keys
# APPLE_CERTIFICATES_P12_PASSWORD Password protecting the P12
# APPLE_ID Apple ID email for notarytool
# APPLE_APP_SPECIFIC_PASSWORD App-specific password (appleid.apple.com)
# APPLE_TEAM_ID 10-character Apple Developer Team ID
#
# See macos/PACKAGING.md for how-to-obtain instructions.
# ---------------------------------------------------------------------------
build-macos:
name: Build macOS
runs-on: macos-14
steps:
- uses: actions/checkout@v4
- name: Select Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: latest-stable
- name: Detect signing secrets
id: signing
env:
P12_B64: ${{ secrets.APPLE_CERTIFICATES_P12_BASE64 }}
run: |
if [[ -n "${P12_B64:-}" ]]; then
echo "have_certs=true" >> "$GITHUB_OUTPUT"
else
echo "have_certs=false" >> "$GITHUB_OUTPUT"
fi
- name: Import code-signing certificates
if: steps.signing.outputs.have_certs == 'true'
uses: apple-actions/import-codesign-certs@v3
with:
p12-file-base64: ${{ secrets.APPLE_CERTIFICATES_P12_BASE64 }}
p12-password: ${{ secrets.APPLE_CERTIFICATES_P12_PASSWORD }}
- name: Build .app bundle (sign + notarize when secrets present)
env:
APPLE_DEVELOPER_ID_APPLICATION: ${{ secrets.APPLE_DEVELOPER_ID_APPLICATION }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: bash macos/scripts/build-app.sh
- name: Build .pkg installer (signed when installer cert present)
env:
APPLE_DEVELOPER_ID_INSTALLER: ${{ secrets.APPLE_DEVELOPER_ID_INSTALLER }}
run: bash macos/scripts/build-pkg.sh
- name: Zip .app bundle
run: |
cd macos/build
zip -r --symlinks "InterlinedSync-macOS-${{ github.ref_name }}.zip" InterlinedSync.app
- name: Rename .pkg for release
run: |
cd macos/build
PKG_SRC="$(ls InterlinedSync-*.pkg | grep -v "macOS-${{ github.ref_name }}" | head -n 1 || true)"
if [[ -n "${PKG_SRC}" ]]; then
mv "${PKG_SRC}" "InterlinedSync-macOS-${{ github.ref_name }}.pkg"
fi
- name: Upload macOS artifacts
uses: actions/upload-artifact@v4
with:
name: macos-release
path: |
macos/build/InterlinedSync-macOS-${{ github.ref_name }}.zip
macos/build/InterlinedSync-macOS-${{ github.ref_name }}.pkg
if-no-files-found: error
retention-days: 1
# ---------------------------------------------------------------------------
# Windows — framework-dependent publish (requires .NET 9 Desktop Runtime)
# plus an MSIX package built by InterlinedSync.Package.wapproj. The MSIX is
# signed when WINDOWS_CERT_PFX_BASE64 + WINDOWS_CERT_PASSWORD secrets exist;
# otherwise it ships UNSIGNED (sideload requires developer mode / sideload
# license). See windows/PACKAGING.md for the full signing checklist.
# ---------------------------------------------------------------------------
build-windows:
name: Build Windows
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Setup .NET 9
uses: actions/setup-dotnet@v4
with:
dotnet-version: '9.0.x'
- name: Setup MSBuild
uses: microsoft/setup-msbuild@v2
- name: Restore packages
working-directory: windows
run: dotnet restore
- name: Publish
working-directory: windows
run: >
dotnet publish InterlinedSync/InterlinedSync.csproj
--configuration Release
--no-restore
--runtime win-x64
--self-contained false
--output publish
- name: Zip publish output
shell: pwsh
run: |
Compress-Archive `
-Path windows/publish/* `
-DestinationPath "InterlinedSync-Windows-${{ github.ref_name }}.zip"
- name: Build MSIX package
working-directory: windows
shell: pwsh
run: |
msbuild InterlinedSync.Package\InterlinedSync.Package.wapproj `
/p:Configuration=Release `
/p:Platform=x64 `
/p:AppxBundle=Never `
/p:UapAppxPackageBuildMode=SideloadOnly `
/p:AppxPackageSigningEnabled=false
- name: Locate MSIX output
id: msix
shell: pwsh
run: |
$msix = Get-ChildItem -Path windows/InterlinedSync.Package/bin/x64/Release -Recurse -Filter "*.msix" | Select-Object -First 1
if (-not $msix) {
Write-Error "No .msix produced by InterlinedSync.Package.wapproj"
exit 1
}
$dest = "InterlinedSync-Windows-MSIX-${{ github.ref_name }}.msix"
Copy-Item -Path $msix.FullName -Destination $dest -Force
"msix_path=$dest" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
- name: Detect MSIX signing secrets
id: msix_signing
shell: pwsh
env:
PFX_B64: ${{ secrets.WINDOWS_CERT_PFX_BASE64 }}
run: |
if ([string]::IsNullOrEmpty($env:PFX_B64)) {
"have_cert=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
} else {
"have_cert=true" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
}
- name: Sign MSIX (if cert available)
if: steps.msix_signing.outputs.have_cert == 'true'
shell: pwsh
env:
PFX_B64: ${{ secrets.WINDOWS_CERT_PFX_BASE64 }}
PFX_PASSWORD: ${{ secrets.WINDOWS_CERT_PASSWORD }}
run: |
$pfxPath = Join-Path $env:RUNNER_TEMP "codesign.pfx"
[IO.File]::WriteAllBytes($pfxPath, [Convert]::FromBase64String($env:PFX_B64))
$signtool = (Get-ChildItem "${env:ProgramFiles(x86)}\Windows Kits\10\bin" -Recurse -Filter "signtool.exe" `
| Where-Object { $_.FullName -match 'x64' } `
| Select-Object -First 1).FullName
& $signtool sign /fd SHA256 /a /f $pfxPath /p $env:PFX_PASSWORD `
/tr http://timestamp.digicert.com /td SHA256 `
"${{ steps.msix.outputs.msix_path }}"
Remove-Item $pfxPath -Force
- name: Upload Windows artifacts
uses: actions/upload-artifact@v4
with:
name: windows-release
path: |
InterlinedSync-Windows-${{ github.ref_name }}.zip
InterlinedSync-Windows-MSIX-${{ github.ref_name }}.msix
if-no-files-found: error
retention-days: 1
# ---------------------------------------------------------------------------
# Linux — .deb package (via cargo-deb) + standalone binary tarball + .snap
# Snap build uses snapcraft with --destructive-mode (no LXD in GitHub runners).
# Publishing to the Snap Store is a separate manual step — see PACKAGING.md.
# ---------------------------------------------------------------------------
build-linux:
name: Build Linux
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
pkg-config \
libdbus-1-dev \
libsqlite3-dev \
libsecret-1-dev
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Cache Rust dependencies
uses: Swatinem/rust-cache@v2
with:
workspaces: linux-ubuntu
- name: Build release binary
working-directory: linux-ubuntu
run: cargo build --release
- name: Install cargo-deb
run: cargo install cargo-deb --version "2.4.0"
- name: Build .deb package
working-directory: linux-ubuntu
run: cargo deb -p interlinedlist-sync --no-build
- name: Create binary tarball
run: |
tar -czf "linux-ubuntu/InterlinedSync-Linux-${{ github.ref_name }}.tar.gz" \
-C linux-ubuntu/target/release interlinedlist-sync
# -------------------------------------------------------------------
# Snap packaging — M7
# snapcraft --destructive-mode builds directly on the runner without
# spawning LXD or Multipass (both require nested virtualisation that
# GitHub-hosted runners do not provide).
# The resulting .snap is unsigned; the Store signs it on upload.
# -------------------------------------------------------------------
- name: Install snapcraft
run: sudo snap install snapcraft --classic
- name: Build snap
working-directory: linux-ubuntu
run: snapcraft --destructive-mode
# Produces: linux-ubuntu/interlinedlist-sync_0.1.0_amd64.snap
# Note: to publish to the Snap Store after this job completes,
# run from a maintainer machine:
# snapcraft upload --release=stable interlinedlist-sync_*.snap
# This requires SNAPCRAFT_STORE_CREDENTIALS — see PACKAGING.md.
- name: Upload Linux artifacts
uses: actions/upload-artifact@v4
with:
name: linux-release
path: |
linux-ubuntu/target/debian/*.deb
linux-ubuntu/InterlinedSync-Linux-${{ github.ref_name }}.tar.gz
linux-ubuntu/*.snap
if-no-files-found: error
retention-days: 1
# ---------------------------------------------------------------------------
# Create a draft GitHub Release and attach all platform artifacts.
# Publish the release manually from the GitHub UI after reviewing the files.
# ---------------------------------------------------------------------------
create-release:
name: Create GitHub Release
needs: [build-macos, build-windows, build-linux]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- name: Download all release artifacts
uses: actions/download-artifact@v4
with:
path: release-artifacts
merge-multiple: true
- name: List artifacts
run: ls -lh release-artifacts/
- name: Write release notes
run: |
TAG="${{ github.ref_name }}"
cat > release-notes.md << EOF
## InterlinedList Sync ${TAG}
### Downloads
| Platform | File | Notes |
|----------|------|-------|
| macOS 13 Ventura or later | \`InterlinedSync-macOS-${TAG}.zip\` | Unzip and move to \`/Applications\`. Unsigned — right-click → Open to bypass Gatekeeper on first launch. |
| Windows 10 / 11 (x64) | \`InterlinedSync-Windows-${TAG}.zip\` | Unzip and run \`InterlinedSync.exe\`. Requires [.NET 9 Desktop Runtime](https://dotnet.microsoft.com/download/dotnet/9.0). |
| Ubuntu / Debian (amd64) | \`interlinedlist-sync_*.deb\` | \`sudo dpkg -i interlinedlist-sync_*.deb\` |
| Ubuntu Snap (amd64) | \`interlinedlist-sync_*.snap\` | \`sudo snap install --classic --dangerous interlinedlist-sync_*.snap\` (unsigned local install; use the Snap Store once published) |
| Linux standalone binary | \`InterlinedSync-Linux-${TAG}.tar.gz\` | Extract and place \`interlinedlist-sync\` in your \`\$PATH\`. |
### First-time setup
1. Edit \`~/.config/interlinedlist-sync/config.toml\` — set \`sync.watched_dirs\` and \`network.api_base_url\`.
2. Store credentials once: \`interlinedlist-sync --login --username you@example.com --password …\`
3. Start the daemon: \`interlinedlist-sync --daemon\` (Linux: managed by the installed systemd user unit)
### What changed
See the commit history for details.
EOF
- name: Create draft GitHub Release
env:
GH_TOKEN: ${{ github.token }}
run: |
PRERELEASE_FLAG=""
if [[ "${{ github.ref_name }}" == *-* ]]; then
PRERELEASE_FLAG="--prerelease"
fi
gh release create "${{ github.ref_name }}" \
--title "InterlinedList Sync ${{ github.ref_name }}" \
--notes-file release-notes.md \
--draft \
$PRERELEASE_FLAG \
release-artifacts/*