7 Commits
v1.2.2 ... main

Author SHA1 Message Date
SuperKali
4645cdece2 fix: extract and display PR/issue numbers in changelog
Parse GitHub URLs to extract PR/issue numbers and display them in orange.
Previously, the regex was removing the entire URL including the PR number.
2026-01-03 15:52:46 +01:00
Ricardo Pardini
9cf8ca299b gha: build: linux: rework caching for consistency
- `actions/setup-node` doesn't allow for setting cache keys
  - even in recent versions... (bumped to v6)
  - so move npm caching to `actions/cache`:
    - disable `setup-node` caching via `package-manager-cache: false`
    - add new step for `actions/cache` "npm dependencies"
      - cache key includes the runner image, the container distro (if any), and hash of `package-lock.json`
- for `Swatinem/rust-cache`
  - use a cache key that includes the runner image, the container distro (if any), and TAURI_CLI_VERSION
  - add a TODO ref Cargo.lock missing/.gitignored, as it would be hashed too automatically had it existed
- for `actions/cache` based "cargo bin tauri-cli" caching
  - use a cache key that includes the runner image, the container distro (if any), and TAURI_CLI_VERSION
  - also TODO ref Cargo.lock, which was spelled out, but doesn't exist
  - also TODO as it seems to me this is already covered by the `Swatinem/rust-cache` cache

Signed-off-by: Ricardo Pardini <ricardo@pardini.net>
2026-01-03 13:39:37 +01:00
Ricardo Pardini
0431b243bb gha: build: set a specific TAURI_CLI_VERSION (2.9.6)
- so we can hash it into the cache keys (done in later commit) for consistency

Signed-off-by: Ricardo Pardini <ricardo@pardini.net>
2026-01-03 13:39:37 +01:00
Ricardo Pardini
4a3aa68051 gha: build: linux: rework into matrix; use bookworm for .deb builds
- this should reduce the glibc dep version requirement of .deb's, allowing them to run on old but still supported systems
  - See https://v2.tauri.app/distribute/debian/#limitations
    - "you must build your Tauri application using the oldest base system you intend to support"
    - Debian oldstable (Bookworm) will be supported until late 2028, so fair to support it
    - also, there's no downsides; imager itself runs great either way, and .deb install pulls updated deps on newer distros
- fold linux-x64 and linux-amd64 into a single matrix job (1st level)
- 2nd matrix level is per-type:
  - `deb` is now built using an oldtable container
  - `appimage` is built without container, directly on runner, as before
     - seems like appimage/`linuxdeploy` doesn't wanna be run in a container
     - also, the AppImage does seem to contain libs, so we don't wanna ship old ones

Signed-off-by: Ricardo Pardini <ricardo@pardini.net>
2026-01-03 13:39:37 +01:00
SuperKali
d63d047a0f fix: pass APPLE_SIGNING_IDENTITY to Tauri for proper code signing 2025-12-31 17:56:16 +01:00
SuperKali
4b9f5289f7 fix: notarize only DMG file instead of .app bundle 2025-12-31 17:30:48 +01:00
SuperKali
840509ec0c feat: add Apple code signing and notarization for macOS builds 2025-12-31 17:06:14 +01:00
6 changed files with 208 additions and 139 deletions

View File

@@ -184,10 +184,38 @@ jobs:
os: macos-latest
runs-on: ${{ matrix.os }}
env:
APPLE_SIGNING_IDENTITY: "-"
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
steps:
- uses: actions/checkout@v4
- name: Import Apple Developer Certificate
env:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
run: |
# Create a temporary keychain
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
KEYCHAIN_PASSWORD=$(openssl rand -base64 32)
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
# Import certificate
echo -n "$APPLE_CERTIFICATE" | base64 --decode > certificate.p12
security import certificate.p12 -k "$KEYCHAIN_PATH" -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
# Verify certificate
security find-identity -v -p codesigning "$KEYCHAIN_PATH"
# Set as default keychain
security default-keychain -s "$KEYCHAIN_PATH"
- name: Setup Node.js
uses: actions/setup-node@v4
with:
@@ -241,6 +269,39 @@ jobs:
ditto -c -k --sequesterRsrc --keepParent "$app" "$CARGO_TARGET_DIR/release/bundle/macos/${base}-${{ matrix.arch }}.app.zip"
done
- name: Notarize and staple DMG
shell: bash
env:
CARGO_TARGET_DIR: src-tauri/target/${{ matrix.target }}
run: |
set -euo pipefail
# Find the DMG file
DMG_FILE=$(find "$CARGO_TARGET_DIR/release/bundle/dmg/" -name "*.dmg" -type f | head -n 1)
if [[ -z "$DMG_FILE" ]]; then
echo "No DMG file found"
exit 1
fi
echo "Notarizing: $DMG_FILE"
# Submit DMG for notarization
xcrun notarytool submit "$DMG_FILE" \
--apple-id "$APPLE_ID" \
--password "$APPLE_ID_PASSWORD" \
--team-id "$APPLE_TEAM_ID" \
--wait \
--output-format json
# Staple the notarization ticket to DMG
xcrun stapler staple "$DMG_FILE"
# Verify stapling
xcrun stapler validate -v "$DMG_FILE"
echo "Notarization completed successfully"
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:

View File

@@ -6,12 +6,8 @@ on:
- 'v*'
workflow_dispatch:
inputs:
build_linux_x64:
description: 'Build Linux x64'
type: boolean
default: true
build_linux_arm64:
description: 'Build Linux ARM64'
build_linux:
description: 'Build Linux (x64 + ARM64)'
type: boolean
default: true
build_macos:
@@ -34,7 +30,7 @@ concurrency:
env:
CARGO_TERM_COLOR: always
NODE_VERSION: '20'
TAURI_CLI_VERSION: '^2'
TAURI_CLI_VERSION: '2.9.6'
# Tauri updater signing key (set in GitHub Secrets)
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
@@ -93,11 +89,35 @@ jobs:
omitNameDuringUpdate: true
replacesArtifacts: false
build-linux-x64:
name: build-linux (x86_64-unknown-linux-gnu)
needs: [create-release]
if: ${{ github.event_name == 'push' || inputs.build_linux_x64 }}
runs-on: ubuntu-24.04
build-linux:
name: build-linux ${{ matrix.type.name }} (${{ matrix.arch.name }}/${{ matrix.type.distro_id }})
needs: [ create-release ]
if: ${{ github.event_name == 'push' || inputs.build_linux }} # can't use matrix here; if is evaluated before matrix expansion
strategy:
fail-fast: false # let other jobs try to complete if one fails
matrix:
arch:
- { name: 'amd64', runner: 'ubuntu-24.04' }
- { name: 'arm64', runner: "ubuntu-24.04-arm" }
type:
# deb: build in the oldest still-supported matching container: oldstable (bookworm)
- name: 'deb'
distro_id: "bookworm"
container: { image: 'debian:bookworm' }
bundles: 'deb'
deps: "apt"
artifacts: "src-tauri/target/release/bundle/deb/*.deb"
# appimage: doesn't use a container (instead, runs directly on the runner); requires sudo to install deps
- name: 'appimage'
distro_id: "gharunner"
bundles: 'appimage'
deps: "apt"
deps_gain_root: "sudo"
artifacts: |
src-tauri/target/release/bundle/appimage/*.AppImage
src-tauri/target/release/bundle/appimage/*.AppImage.sig
runs-on: ${{ matrix.arch.runner }}
container: ${{ matrix.type.container }}
permissions:
contents: write
steps:
@@ -125,10 +145,13 @@ jobs:
-e "s/\"version\": \"[0-9.]*\"/\"version\": \"$VERSION\"/" \
src-tauri/tauri.conf.json
- name: Install dependencies
- name: Install dependencies (apt)
if: ${{ matrix.type.deps == 'apt' }}
run: |
sudo apt-get update
sudo apt-get install -y \
${{ matrix.type.deps_gain_root || '' }} apt-get update
${{ matrix.type.deps_gain_root || '' }} apt-get install -y \
build-essential \
curl \
libwebkit2gtk-4.1-dev \
libayatana-appindicator3-dev \
librsvg2-dev \
@@ -139,10 +162,17 @@ jobs:
xdg-utils
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
package-manager-cache: false # setup-node can't add key to cache, so we do it ourselves in below step
# Cache npm, just like setup-node would do it with "cache: npm", but with our own key that includes arch and distro
- name: Cache npm dependencies
uses: actions/cache@v4
with:
path: ~/.npm
key: npm-${{ matrix.arch.runner }}-${{ matrix.arch.name }}-${{ matrix.type.distro_id }}-${{ hashFiles('**/package-lock.json') }}
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
@@ -151,12 +181,16 @@ jobs:
uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri
# ensure cache separation per host distro; this also ends up caching tauri-cli, so add that to key too.
key: "rust-cache-${{ matrix.arch.runner }}-${{ matrix.arch.name }}-${{ matrix.type.distro_id }}-${{env.TAURI_CLI_VERSION}}"
# @TODO: Cargo.lock is in gitignore, so it's not included here - it would via rust-cache Action magic, no need to specify it.
- name: Cache cargo bin (tauri-cli)
- name: Cache cargo bin (tauri-cli) # @TODO: Swatinem/rust-cache already caches this. maybe just drop this.
uses: actions/cache@v4
with:
path: ~/.cargo/bin
key: cargo-bin-${{ runner.os }}-${{ runner.arch }}-stable-${{ hashFiles('**/Cargo.lock') }}
key: "cargo-bin-${{ matrix.arch.runner }}-${{ matrix.arch.name }}-${{ matrix.type.distro_id }}-stable-${{env.TAURI_CLI_VERSION}}-${{ hashFiles('**/Cargo.lock') }}"
# @TODO: Cargo.lock is in gitignore, so it's not included here, albeit being specified.
- name: Install npm dependencies
run: npm ci
@@ -172,7 +206,7 @@ jobs:
fi
- name: Build Tauri app
run: cargo tauri build --bundles deb,appimage
run: cargo tauri build --bundles "${{ matrix.type.bundles }}"
- name: Upload artifacts to GitHub Release
uses: ncipollo/release-action@v1
@@ -186,106 +220,7 @@ jobs:
omitNameDuringUpdate: true
replacesArtifacts: false
artifacts: |
src-tauri/target/release/bundle/deb/*.deb
src-tauri/target/release/bundle/appimage/*.AppImage
src-tauri/target/release/bundle/appimage/*.AppImage.sig
build-linux-arm64:
name: build-linux (aarch64-unknown-linux-gnu)
needs: [create-release]
if: ${{ github.event_name == 'push' || inputs.build_linux_arm64 }}
runs-on: ubuntu-24.04-arm
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- name: Set version from release tag
shell: bash
run: |
set -euo pipefail
TAG='${{ needs.create-release.outputs.release_tag }}'
VERSION="${TAG#v}"
echo "Setting version to $VERSION"
sed -i \
-e "s/\"version\": \"[0-9.]*\"/\"version\": \"$VERSION\"/" \
package.json
sed -i \
-e "s/^version = \".*\"/version = \"$VERSION\"/" \
src-tauri/Cargo.toml
sed -i \
-e "s/\"version\": \"[0-9.]*\"/\"version\": \"$VERSION\"/" \
src-tauri/tauri.conf.json
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
libwebkit2gtk-4.1-dev \
libayatana-appindicator3-dev \
librsvg2-dev \
patchelf \
libssl-dev \
libgtk-3-dev \
squashfs-tools \
xdg-utils
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
- name: Cache Rust dependencies
uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri
- name: Cache cargo bin (tauri-cli)
uses: actions/cache@v4
with:
path: ~/.cargo/bin
key: cargo-bin-${{ runner.os }}-${{ runner.arch }}-stable-${{ hashFiles('**/Cargo.lock') }}
- name: Install npm dependencies
run: npm ci
- name: Build frontend
run: npm run build
- name: Install Tauri CLI (if missing)
shell: bash
run: |
if ! command -v cargo-tauri >/dev/null 2>&1; then
cargo install tauri-cli --version "${TAURI_CLI_VERSION}" --locked
fi
- name: Build Tauri app
run: cargo tauri build --bundles deb,appimage
- name: Upload artifacts to GitHub Release
uses: ncipollo/release-action@v1
with:
tag: ${{ needs.create-release.outputs.release_tag }}
name: "Armbian Imager ${{ needs.create-release.outputs.release_tag }}"
draft: true
prerelease: false
allowUpdates: true
omitBodyDuringUpdate: true
omitNameDuringUpdate: true
replacesArtifacts: false
artifacts: |
src-tauri/target/release/bundle/deb/*.deb
src-tauri/target/release/bundle/appimage/*.AppImage
src-tauri/target/release/bundle/appimage/*.AppImage.sig
${{ matrix.type.artifacts }}
build-macos:
name: build-macos (${{ matrix.target }})
@@ -306,12 +241,39 @@ jobs:
permissions:
contents: write
env:
# Ad-hoc signing: allows app to run after "xattr -cr" on macOS
APPLE_SIGNING_IDENTITY: "-"
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
steps:
- uses: actions/checkout@v4
- name: Import Apple Developer Certificate
env:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
run: |
# Create a temporary keychain
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
KEYCHAIN_PASSWORD=$(openssl rand -base64 32)
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
# Import certificate
echo -n "$APPLE_CERTIFICATE" | base64 --decode > certificate.p12
security import certificate.p12 -k "$KEYCHAIN_PATH" -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
# Verify certificate
security find-identity -v -p codesigning "$KEYCHAIN_PATH"
# Set as default keychain
security default-keychain -s "$KEYCHAIN_PATH"
- name: Set version from release tag
shell: bash
run: |
@@ -397,6 +359,39 @@ jobs:
mv "$sig" "$CARGO_TARGET_DIR/release/bundle/macos/${base}-${{ matrix.arch }}.tar.gz.sig"
done
- name: Notarize and staple DMG
shell: bash
env:
CARGO_TARGET_DIR: src-tauri/target/${{ matrix.target }}
run: |
set -euo pipefail
# Find the DMG file
DMG_FILE=$(find "$CARGO_TARGET_DIR/release/bundle/dmg/" -name "*.dmg" -type f | head -n 1)
if [[ -z "$DMG_FILE" ]]; then
echo "No DMG file found"
exit 1
fi
echo "Notarizing: $DMG_FILE"
# Submit DMG for notarization
xcrun notarytool submit "$DMG_FILE" \
--apple-id "$APPLE_ID" \
--password "$APPLE_ID_PASSWORD" \
--team-id "$APPLE_TEAM_ID" \
--wait \
--output-format json
# Staple the notarization ticket to DMG
xcrun stapler staple "$DMG_FILE"
# Verify stapling
xcrun stapler validate -v "$DMG_FILE"
echo "Notarization completed successfully"
- name: Upload macOS artifacts to GitHub Release
uses: ncipollo/release-action@v1
with:
@@ -593,8 +588,7 @@ jobs:
name: Generate latest.json for updater
needs:
- create-release
- build-linux-x64
- build-linux-arm64
- build-linux
- build-macos
- build-windows-x64
- build-windows-arm64
@@ -710,8 +704,7 @@ jobs:
name: Publish release (draft -> false) + cleanup
needs:
- create-release
- build-linux-x64
- build-linux-arm64
- build-linux
- build-macos
- build-windows-x64
- build-windows-arm64

View File

@@ -40,10 +40,6 @@ Prebuilt binaries are available for all supported platforms.
| Intel & Apple Silicon | x64 & ARM64 | x64 & ARM64 |
| <code>.dmg</code> / <code>.app.zip</code> | <code>.exe</code> / <code>.msi</code> | <code>.deb</code> / <code>.AppImage</code> |
**macOS: First Launch**
On first launch, macOS may block the application because it is not signed. If this happens, open **System Settings → Privacy & Security** and click **Open Anyway** next to *Armbian Imager was blocked*. This only needs to be done once.
## How It Works
1. **Select Manufacturer** — Choose from 70+ supported SBC manufacturers or load a custom image

View File

@@ -48,6 +48,7 @@
"longDescription": "Armbian Imager is a utility for flashing Armbian OS images to SD cards and USB drives for single board computers.",
"macOS": {
"entitlements": "./entitlements.plist",
"signingIdentity": null,
"minimumSystemVersion": "10.15",
"dmg": {
"background": "./dmg-background.png",

View File

@@ -120,17 +120,21 @@ export function ChangelogModal({ isOpen, onClose, version }: ChangelogModalProps
const itemContent = line.replace(/^\s*[-*]\s+/, '');
const formattedItem = itemContent
// Remove PR/issue URLs and replace with #number
.replace(/ https:\/\/github\.com\/[^/]+\/[^/]+\/(?:pull|issues)\/(\d+)/g, ' #PR$1 ')
.replace(/\[[^\]]+\]\(https:\/\/github\.com\/[^/]+\/[^/]+\/(?:pull|issues)\/(\d+)\)/g, '#PR$1')
// NOW do HTML escape (won't break the text)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
// Replace placeholders with actual HTML
.replace(/#PR(\d+)/g, '<span class="pr-number">#$1</span>')
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
.replace(/\*([^*]+)\*/g, '<em>$1</em>')
.replace(/`([^`]+)`/g, '<code>$1</code>')
// Remove markdown GitHub links (PRs, issues, commits)
// Remove remaining markdown GitHub links (commits, etc)
.replace(/\[([^\]]+)\]\(https:\/\/github\.com\/[^)]+\)/g, '$1')
// Remove "in https://github.com/..." but keep "by @username"
.replace(/(by @[a-zA-Z0-9_-]+) in https:\/\/github\.com\/[^\s]+/g, '$1')
// Remove any remaining bare GitHub URLs
// Remove any remaining bare GitHub URLs (commits, etc)
.replace(/https:\/\/github\.com\/[^\s]+/g, '')
.replace(/,\s*$/g, '') // Remove trailing comma after URL removal
.replace(/\s+/g, ' ') // Clean up extra whitespace
@@ -164,17 +168,21 @@ export function ChangelogModal({ isOpen, onClose, version }: ChangelogModalProps
// Regular paragraph line
const formattedLine = line
// Remove PR/issue URLs and replace with #number
.replace(/ https:\/\/github\.com\/[^/]+\/[^/]+\/(?:pull|issues)\/(\d+)/g, ' #PR$1 ')
.replace(/\[[^\]]+\]\(https:\/\/github\.com\/[^/]+\/[^/]+\/(?:pull|issues)\/(\d+)\)/g, '#PR$1')
// NOW do HTML escape (won't break the text)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
// Replace placeholders with actual HTML
.replace(/#PR(\d+)/g, '<span class="pr-number">#$1</span>')
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
.replace(/\*([^*]+)\*/g, '<em>$1</em>')
.replace(/`([^`]+)`/g, '<code>$1</code>')
// Remove markdown GitHub links (PRs, issues, commits)
// Remove remaining markdown GitHub links (commits, etc)
.replace(/\[([^\]]+)\]\(https:\/\/github\.com\/[^)]+\)/g, '$1')
// Remove "in https://github.com/..." but keep "by @username"
.replace(/(by @[a-zA-Z0-9_-]+) in https:\/\/github\.com\/[^\s]+/g, '$1')
// Remove any remaining bare GitHub URLs
// Remove any remaining bare GitHub URLs (commits, etc)
.replace(/https:\/\/github\.com\/[^\s]+/g, '')
.replace(/,\s*$/g, '') // Remove trailing comma after URL removal
.replace(/\s+/g, ' ') // Clean up extra whitespace
@@ -194,6 +202,11 @@ export function ChangelogModal({ isOpen, onClose, version }: ChangelogModalProps
// Remove trailing <br /> tags to avoid extra space at the end
html = html.replace(/(<br\s*\/?>)+$/g, '');
// Remove multiple <br /> tags and <br /> before headers to avoid extra spacing
html = html.replace(/(<br\s*\/?>){2,}/g, '<br />');
html = html.replace(/<br\s*\/?>\s*<h2>/g, '<h2>');
html = html.replace(/<br\s*\/?>\s*<h3>/g, '<h3>');
return html;
};

View File

@@ -1260,3 +1260,8 @@
text-decoration: underline;
}
/* Changelog Modal */
.changelog-body .pr-number {
color: var(--armbian-orange);
font-weight: 500;
}