diff --git a/.github/workflows/desktop-app.yml b/.github/workflows/desktop-app.yml index 5cf167c..9efcb2e 100644 --- a/.github/workflows/desktop-app.yml +++ b/.github/workflows/desktop-app.yml @@ -106,10 +106,96 @@ jobs: - name: Build Tauri bundle run: cargo tauri build --target ${{ matrix.target }} + - name: Import Apple signing certificate + if: runner.os == 'macOS' + shell: bash + env: + APPLE_SIGNING_CERT_B64: ${{ secrets.APPLE_SIGNING_CERT_B64 }} + APPLE_SIGNING_CERT_PASSWORD: ${{ secrets.APPLE_SIGNING_CERT_PASSWORD }} + KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} + run: | + set -euo pipefail + if [ -z "${APPLE_SIGNING_CERT_B64:-}" ] || [ -z "${APPLE_SIGNING_CERT_PASSWORD:-}" ]; then + echo "Apple signing cert secrets are not set; skipping Developer ID import." + exit 0 + fi + KEYCHAIN_PASSWORD="${KEYCHAIN_PASSWORD:-temp-keychain-pass}" + echo "$APPLE_SIGNING_CERT_B64" | base64 --decode > signing-cert.p12 + security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain + security default-keychain -s build.keychain + security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain + security set-keychain-settings -t 3600 -u build.keychain + security import signing-cert.p12 -k build.keychain -P "$APPLE_SIGNING_CERT_PASSWORD" -T /usr/bin/codesign -T /usr/bin/security + security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" build.keychain + + - name: Sign macOS app and DMG (Developer ID) + if: runner.os == 'macOS' + shell: bash + env: + APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} + run: | + set -euo pipefail + if [ -z "${APPLE_SIGNING_IDENTITY:-}" ]; then + echo "APPLE_SIGNING_IDENTITY is not set; skipping Developer ID signing." + exit 0 + fi + BUNDLE_ROOT="src-tauri/target/${{ matrix.target }}/release/bundle" + if [ -d "$BUNDLE_ROOT/macos" ]; then + for app in "$BUNDLE_ROOT/macos"/*.app; do + [ -d "$app" ] || continue + echo "codesign app: $app" + codesign --force --deep --options runtime --timestamp --sign "$APPLE_SIGNING_IDENTITY" "$app" + codesign --verify --deep --strict --verbose=2 "$app" + done + fi + if [ -d "$BUNDLE_ROOT/dmg" ]; then + for dmg in "$BUNDLE_ROOT/dmg"/*.dmg; do + [ -f "$dmg" ] || continue + echo "codesign dmg: $dmg" + codesign --force --timestamp --sign "$APPLE_SIGNING_IDENTITY" "$dmg" + done + fi + + - name: Notarize and staple DMG + if: runner.os == 'macOS' + shell: bash + env: + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} + run: | + set -euo pipefail + if [ -z "${APPLE_ID:-}" ] || [ -z "${APPLE_TEAM_ID:-}" ] || [ -z "${APPLE_APP_SPECIFIC_PASSWORD:-}" ]; then + echo "Apple notarization secrets are not set; skipping notarization." + exit 0 + fi + BUNDLE_ROOT="src-tauri/target/${{ matrix.target }}/release/bundle/dmg" + if [ -d "$BUNDLE_ROOT" ]; then + for dmg in "$BUNDLE_ROOT"/*.dmg; do + [ -f "$dmg" ] || continue + echo "notarizing: $dmg" + xcrun notarytool submit "$dmg" \ + --apple-id "$APPLE_ID" \ + --team-id "$APPLE_TEAM_ID" \ + --password "$APPLE_APP_SPECIFIC_PASSWORD" \ + --wait + xcrun stapler staple "$dmg" + xcrun stapler validate "$dmg" + done + fi + - name: Ad-hoc codesign macOS bundle if: runner.os == 'macOS' shell: bash + env: + APPLE_SIGNING_CERT_B64: ${{ secrets.APPLE_SIGNING_CERT_B64 }} + APPLE_SIGNING_CERT_PASSWORD: ${{ secrets.APPLE_SIGNING_CERT_PASSWORD }} + APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} run: | + if [ -n "${APPLE_SIGNING_CERT_B64:-}" ] && [ -n "${APPLE_SIGNING_CERT_PASSWORD:-}" ] && [ -n "${APPLE_SIGNING_IDENTITY:-}" ]; then + echo "Developer ID signing is configured; skipping ad-hoc codesign fallback." + exit 0 + fi # Apple Silicon and modern Intel macOS refuse to launch unsigned # apps with the cryptic "DevOpster.app is damaged" alert. An ad-hoc # signature (sign identity "-") satisfies the loader so the app @@ -144,6 +230,10 @@ jobs: -o -name "*.deb" -o -name "*.AppImage" -o -name "*.rpm" \) \ -exec cp {} dist/ \; || true fi + if [ "${{ runner.os }}" = "macOS" ]; then + cp scripts/install-macos-app.sh dist/DevOpster-macos-install.sh + chmod +x dist/DevOpster-macos-install.sh + fi ls -la dist || true - name: Upload artifacts @@ -197,9 +287,89 @@ jobs: - name: Build Tauri bundle run: cargo tauri build --target x86_64-apple-darwin + - name: Import Apple signing certificate + shell: bash + env: + APPLE_SIGNING_CERT_B64: ${{ secrets.APPLE_SIGNING_CERT_B64 }} + APPLE_SIGNING_CERT_PASSWORD: ${{ secrets.APPLE_SIGNING_CERT_PASSWORD }} + KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} + run: | + set -euo pipefail + if [ -z "${APPLE_SIGNING_CERT_B64:-}" ] || [ -z "${APPLE_SIGNING_CERT_PASSWORD:-}" ]; then + echo "Apple signing cert secrets are not set; skipping Developer ID import." + exit 0 + fi + KEYCHAIN_PASSWORD="${KEYCHAIN_PASSWORD:-temp-keychain-pass}" + echo "$APPLE_SIGNING_CERT_B64" | base64 --decode > signing-cert.p12 + security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain + security default-keychain -s build.keychain + security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain + security set-keychain-settings -t 3600 -u build.keychain + security import signing-cert.p12 -k build.keychain -P "$APPLE_SIGNING_CERT_PASSWORD" -T /usr/bin/codesign -T /usr/bin/security + security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" build.keychain + + - name: Sign macOS app and DMG (Developer ID) + shell: bash + env: + APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} + run: | + set -euo pipefail + if [ -z "${APPLE_SIGNING_IDENTITY:-}" ]; then + echo "APPLE_SIGNING_IDENTITY is not set; skipping Developer ID signing." + exit 0 + fi + BUNDLE_ROOT="src-tauri/target/x86_64-apple-darwin/release/bundle" + if [ -d "$BUNDLE_ROOT/macos" ]; then + for app in "$BUNDLE_ROOT/macos"/*.app; do + [ -d "$app" ] || continue + codesign --force --deep --options runtime --timestamp --sign "$APPLE_SIGNING_IDENTITY" "$app" + codesign --verify --deep --strict --verbose=2 "$app" + done + fi + if [ -d "$BUNDLE_ROOT/dmg" ]; then + for dmg in "$BUNDLE_ROOT/dmg"/*.dmg; do + [ -f "$dmg" ] || continue + codesign --force --timestamp --sign "$APPLE_SIGNING_IDENTITY" "$dmg" + done + fi + + - name: Notarize and staple DMG + shell: bash + env: + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} + run: | + set -euo pipefail + if [ -z "${APPLE_ID:-}" ] || [ -z "${APPLE_TEAM_ID:-}" ] || [ -z "${APPLE_APP_SPECIFIC_PASSWORD:-}" ]; then + echo "Apple notarization secrets are not set; skipping notarization." + exit 0 + fi + BUNDLE_ROOT="src-tauri/target/x86_64-apple-darwin/release/bundle/dmg" + if [ -d "$BUNDLE_ROOT" ]; then + for dmg in "$BUNDLE_ROOT"/*.dmg; do + [ -f "$dmg" ] || continue + xcrun notarytool submit "$dmg" \ + --apple-id "$APPLE_ID" \ + --team-id "$APPLE_TEAM_ID" \ + --password "$APPLE_APP_SPECIFIC_PASSWORD" \ + --wait + xcrun stapler staple "$dmg" + xcrun stapler validate "$dmg" + done + fi + - name: Ad-hoc codesign macOS bundle shell: bash + env: + APPLE_SIGNING_CERT_B64: ${{ secrets.APPLE_SIGNING_CERT_B64 }} + APPLE_SIGNING_CERT_PASSWORD: ${{ secrets.APPLE_SIGNING_CERT_PASSWORD }} + APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} run: | + if [ -n "${APPLE_SIGNING_CERT_B64:-}" ] && [ -n "${APPLE_SIGNING_CERT_PASSWORD:-}" ] && [ -n "${APPLE_SIGNING_IDENTITY:-}" ]; then + echo "Developer ID signing is configured; skipping ad-hoc codesign fallback." + exit 0 + fi BUNDLE_ROOT="src-tauri/target/x86_64-apple-darwin/release/bundle" if [ -d "$BUNDLE_ROOT/macos" ]; then for app in "$BUNDLE_ROOT/macos"/*.app; do @@ -226,6 +396,8 @@ jobs: \( -name "*.dmg" -o -name "*.app.tar.gz" \) \ -exec cp {} dist/ \; || true fi + cp scripts/install-macos-app.sh dist/DevOpster-macos-install.sh + chmod +x dist/DevOpster-macos-install.sh ls -la dist || true - name: Upload artifacts @@ -252,12 +424,69 @@ jobs: find artifacts -type f -exec cp {} flat/ \; ls -la flat + - name: Compute release metadata + id: release_meta + shell: bash + run: | + if [ "${GITHUB_EVENT_NAME}" = "workflow_dispatch" ]; then + TAG_NAME="${{ inputs.release_tag }}" + else + TAG_NAME="${GITHUB_REF_NAME}" + fi + echo "tag_name=${TAG_NAME}" >> "$GITHUB_OUTPUT" + echo "release_name=DevOpster Desktop ${TAG_NAME}" >> "$GITHUB_OUTPUT" + + - name: Delete existing release if present + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + TAG="${{ steps.release_meta.outputs.tag_name }}" + gh release delete "${TAG}" --repo "${{ github.repository }}" --yes 2>/dev/null \ + && echo "Deleted existing release for ${TAG}." \ + || echo "No existing release for ${TAG}, creating fresh." + - name: Create GitHub release uses: softprops/action-gh-release@v2 with: - tag_name: ${{ github.event_name == 'workflow_dispatch' && inputs.release_tag || github.ref_name }} + tag_name: ${{ steps.release_meta.outputs.tag_name }} files: flat/* - generate_release_notes: true - name: DevOpster Desktop ${{ github.event_name == 'workflow_dispatch' && inputs.release_tag || github.ref_name }} + fail_on_unmatched_files: true + name: ${{ steps.release_meta.outputs.release_name }} + body: | + ## Install DevOpster Desktop ${{ steps.release_meta.outputs.tag_name }} + + Download the installer that matches your OS. + + | Platform | File | Install notes | + |---|---|---| + | **macOS** | `*.dmg` | Recommended. Drag `DevOpster.app` to `/Applications` or `~/Applications` | + | **macOS** | `*.app.tar.gz` | Advanced/manual extraction | + | **macOS** | `DevOpster-macos-install.sh` | One-click installer script (path-aware + quarantine clear) | + | **Windows** | `*-setup.exe` | Recommended NSIS installer | + | **Windows** | `*.msi` | MSI deployment/install option | + | **Linux** | `*.AppImage` | `chmod +x DevOpster*.AppImage && ./DevOpster*.AppImage` | + | **Linux** | `*.deb` | `sudo apt install ./DevOpster*.deb` | + | **Linux** | `*.rpm` | `sudo rpm -i DevOpster*.rpm` | + + ### macOS trust and verification + + - Stable releases are signed and notarized when Apple credentials are configured in CI. + - You can run the one-click installer from the mounted DMG or extracted assets: + + ```bash + chmod +x ./DevOpster-macos-install.sh + ./DevOpster-macos-install.sh + ``` + + - If you are testing an unsigned prerelease build and Gatekeeper blocks launch, run with your chosen install path: + + ```bash + APP_PATH="$HOME/Applications/DevOpster.app" + xattr -cr "$APP_PATH" + ``` + + ### Windows SmartScreen + + If SmartScreen appears for an unsigned build, choose **More info** and then **Run anyway**. draft: false prerelease: ${{ github.event_name == 'workflow_dispatch' }} diff --git a/README.md b/README.md index 9b57727..38dfff4 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,8 @@ Current downloadable outputs: - `devopster-macos-x86_64.tar.gz` (CLI binary + `.icns` icon) - `devopster-macos.dmg` (includes CLI files plus native `DevOpster GUI.app` bundle) -Desktop GUI installer packaging is planned as a next phase. Today, GUI mode is the interactive terminal launcher (`devopster gui`). +Desktop installers are published by the desktop release workflow (`desktop-v*` tags or manual dispatch with publish enabled). +Download from GitHub Releases and install using the platform package (`.dmg`, `-setup.exe`/`.msi`, `.deb`/`.AppImage`/`.rpm`). End-user launch modes now included in artifacts: @@ -319,7 +320,7 @@ blueprint:
- Total views -

Refresh Date: 2026-04-24

+ Total views +

Refresh Date: 2026-04-26

\ No newline at end of file diff --git a/docs/index.html b/docs/index.html index b3b39fe..b961913 100644 --- a/docs/index.html +++ b/docs/index.html @@ -466,6 +466,7 @@ color: var(--fg); } .platform-card .platform-icon svg { width: 100%; height: 100%; display: block; } + .platform-card .platform-icon img { width: 100%; height: 100%; display: block; object-fit: contain; } .platform-icon--macos { color: #111111; } .platform-icon--windows { color: #0f6cbd; } .platform-icon--linux { color: #111111; } @@ -718,7 +719,7 @@

DevOpster

CI status - Total views + Total views @@ -833,31 +834,26 @@

Download DevOpster

macOS

.dmg recommended  Â·  .tar.gz advanced

↓ Download for macOS
    -
  1. Step 1: Download the latest .dmg and drag DevOpster.app to /Applications.
  2. -
  3. Step 2: Try opening the app. If Gatekeeper blocks it, run this once: -
    xattr -cr "/Applications/DevOpster.app"
    +
  4. Step 1: Download the latest .dmg and drag DevOpster.app to /Applications (recommended) or any folder you prefer, for example ~/Applications.
  5. +
  6. Step 2: Optional one-click installer from release assets: +
    chmod +x ./DevOpster-macos-install.sh && ./DevOpster-macos-install.sh
  7. -
  8. Step 3: Open DevOpster.app again and continue setup.
  9. +
  10. Step 3: For stable releases, open normally. If Gatekeeper blocks a prerelease, run this once using your install path: +
    APP_PATH="$HOME/Applications/DevOpster.app"; xattr -cr "$APP_PATH"
    +
  11. +
  12. Step 4: Open DevOpster.app again. Signed and notarized builds should launch without warnings.

Windows

.zip + .ico  Â·  DevOpster-GUI.cmd

@@ -873,15 +869,7 @@

Windows

Linux

.tar.gz  Â·  .desktop  Â·  AppStream

@@ -1056,7 +1044,7 @@

Owner / Organization

function pickAssets(assets, platform) { var rules = { - macos: [/.dmg$/i, /\.app\.tar\.gz$/i, /macos.*\.zip$/i, /darwin.*\.zip$/i], + macos: [/.dmg$/i, /\.app\.tar\.gz$/i, /macos-install\.sh$/i, /macos.*\.zip$/i, /darwin.*\.zip$/i], windows: [/-setup\.exe$/i, /\.msi$/i, /\.exe$/i, /windows.*\.zip$/i, /win.*\.zip$/i, /\.zip$/i], linux: [/\.AppImage$/i, /\.deb$/i, /\.rpm$/i, /linux.*\.tar\.gz$/i, /\.tar\.gz$/i] }; diff --git a/metrics.json b/metrics.json index c3e91d4..5f6067c 100644 --- a/metrics.json +++ b/metrics.json @@ -73,5 +73,10 @@ "date": "2026-04-23", "count": 34, "uniques": 1 + }, + { + "date": "2026-04-24", + "count": 162, + "uniques": 1 } ] \ No newline at end of file diff --git a/scripts/install-macos-app.sh b/scripts/install-macos-app.sh new file mode 100755 index 0000000..27cabab --- /dev/null +++ b/scripts/install-macos-app.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash +set -euo pipefail + +APP_NAME="DevOpster.app" + +print_usage() { + cat <<'EOF' +Usage: + ./install-macos-app.sh [SOURCE_APP_PATH] [INSTALL_DIR] + +Examples: + ./install-macos-app.sh + ./install-macos-app.sh "/Volumes/devopster-cli/DevOpster.app" + ./install-macos-app.sh "/Volumes/devopster-cli/DevOpster.app" "$HOME/Applications" + +Behavior: + - If SOURCE_APP_PATH is omitted, the script tries to auto-detect DevOpster.app + from the current directory and mounted volumes. + - If INSTALL_DIR is omitted, it uses /Applications when writable, otherwise + $HOME/Applications. +EOF +} + +detect_source_app() { + local candidate + + if [[ -d "./${APP_NAME}" ]]; then + printf '%s\n' "./${APP_NAME}" + return 0 + fi + + for candidate in /Volumes/*/"${APP_NAME}"; do + if [[ -d "${candidate}" ]]; then + printf '%s\n' "${candidate}" + return 0 + fi + done + + return 1 +} + +SOURCE_APP_PATH="${1:-}" +INSTALL_DIR="${2:-}" + +if [[ -z "${SOURCE_APP_PATH}" ]]; then + if ! SOURCE_APP_PATH="$(detect_source_app)"; then + echo "Could not find ${APP_NAME} automatically." + print_usage + exit 1 + fi +fi + +if [[ ! -d "${SOURCE_APP_PATH}" ]]; then + echo "Source app not found: ${SOURCE_APP_PATH}" + print_usage + exit 1 +fi + +if [[ -z "${INSTALL_DIR}" ]]; then + if [[ -w "/Applications" ]]; then + INSTALL_DIR="/Applications" + else + INSTALL_DIR="$HOME/Applications" + fi +fi + +mkdir -p "${INSTALL_DIR}" +TARGET_APP_PATH="${INSTALL_DIR}/${APP_NAME}" + +echo "Installing ${APP_NAME} from: ${SOURCE_APP_PATH}" +echo "Target location: ${TARGET_APP_PATH}" + +if [[ -d "${TARGET_APP_PATH}" ]]; then + echo "Replacing existing app at ${TARGET_APP_PATH}" + rm -rf "${TARGET_APP_PATH}" +fi + +# ditto preserves app bundle metadata and permissions better than cp -R. +ditto "${SOURCE_APP_PATH}" "${TARGET_APP_PATH}" + +# Clear quarantine so unsigned prerelease builds can launch. +xattr -cr "${TARGET_APP_PATH}" || true + +echo "Install complete." +echo "You can launch it with: open \"${TARGET_APP_PATH}\""