name: Release on: push: tags: - 'v*.*.*' # Manual run publishes ONLY the CLI to npm (current version, no tag / installers). workflow_dispatch: {} concurrency: group: release-${{ github.ref }} cancel-in-progress: false permissions: contents: write jobs: build-openship: name: Build openship release artifact if: github.event_name == 'push' runs-on: ubuntu-24.04 steps: - name: Checkout uses: actions/checkout@v7 with: fetch-depth: 0 - name: Read bun version id: bun-version run: echo "version=$(cat .bun-version)" >> "$GITHUB_OUTPUT" - name: Setup Bun uses: oven-sh/setup-bun@v2 with: bun-version: ${{ steps.bun-version.outputs.version }} - name: Restore bun cache uses: actions/cache@v6 with: path: ~/.bun/install/cache key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock') }} restore-keys: | ${{ runner.os }}-bun- - name: Install dependencies run: bun install --frozen-lockfile - name: Build openship release run: bun run --cwd apps/api build-release - name: Package release artifact id: package run: | set -euo pipefail TAG="${GITHUB_REF_NAME}" ARTIFACT="openship-${TAG}-linux-amd64.tar.gz" tar -czf "${ARTIFACT}" -C apps/api/release-dist . sha256sum "${ARTIFACT}" > "${ARTIFACT}.sha256" echo "artifact=${ARTIFACT}" >> "$GITHUB_OUTPUT" - name: Upload artifact uses: actions/upload-artifact@v7 with: name: openship-release path: | ${{ steps.package.outputs.artifact }} ${{ steps.package.outputs.artifact }}.sha256 if-no-files-found: error retention-days: 7 build-email: name: Build openship email release artifact if: github.event_name == 'push' runs-on: ubuntu-24.04 steps: - name: Checkout uses: actions/checkout@v7 with: fetch-depth: 0 - name: Read bun version id: bun-version run: echo "version=$(cat .bun-version)" >> "$GITHUB_OUTPUT" - name: Setup Bun uses: oven-sh/setup-bun@v2 with: bun-version: ${{ steps.bun-version.outputs.version }} - name: Restore bun cache uses: actions/cache@v6 with: path: ~/.bun/install/cache key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock') }} restore-keys: | ${{ runner.os }}-bun- - name: Install dependencies run: bun install --frozen-lockfile - name: Build email release run: bun run --cwd apps/email build - name: Package release artifact id: package run: | set -euo pipefail TAG="${GITHUB_REF_NAME}" ARTIFACT="openship-email-${TAG}-linux-amd64.tar.gz" # The email build script outputs to apps/email/dist/ (not # release-dist — the api and email build scripts use different # output dir conventions; matching their actual layouts here). tar -czf "${ARTIFACT}" -C apps/email/dist . sha256sum "${ARTIFACT}" > "${ARTIFACT}.sha256" echo "artifact=${ARTIFACT}" >> "$GITHUB_OUTPUT" - name: Upload artifact uses: actions/upload-artifact@v7 with: name: openship-email-release path: | ${{ steps.package.outputs.artifact }} ${{ steps.package.outputs.artifact }}.sha256 if-no-files-found: error retention-days: 7 build-dashboard: name: Build dashboard bundle (lazy-downloaded by `openship up`) if: github.event_name == 'push' runs-on: ubuntu-24.04 steps: - name: Checkout uses: actions/checkout@v7 with: fetch-depth: 0 - name: Read bun version id: bun-version run: echo "version=$(cat .bun-version)" >> "$GITHUB_OUTPUT" - name: Setup Bun uses: oven-sh/setup-bun@v2 with: bun-version: ${{ steps.bun-version.outputs.version }} - name: Restore bun cache uses: actions/cache@v6 with: path: ~/.bun/install/cache key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock') }} restore-keys: | ${{ runner.os }}-bun- - name: Install dependencies run: bun install --frozen-lockfile # Standalone Next build. Force a production, self-hosted (local) build # regardless of ambient env — same flags build-release.ts uses. - name: Build dashboard (Next standalone) run: bun run --cwd apps/dashboard build env: NODE_ENV: production CLOUD_MODE: "false" OPENSHIP_TARGET: local NEXT_PUBLIC_API_PROXY: "true" - name: Package dashboard bundle id: package run: | set -euo pipefail TAG="${GITHUB_REF_NAME}" STAGING="dashboard-dist" rm -rf "$STAGING"; mkdir -p "$STAGING" # The monorepo-rooted standalone has apps/dashboard/server.js as entry # plus a trimmed node_modules. static/ + public/ are excluded by Next, # so copy them back next to .next/ where the standalone server expects. cp -a apps/dashboard/.next/standalone/. "$STAGING"/ mkdir -p "$STAGING/apps/dashboard/.next" cp -a apps/dashboard/.next/static "$STAGING/apps/dashboard/.next/static" if [ -d apps/dashboard/public ]; then cp -a apps/dashboard/public "$STAGING/apps/dashboard/public" fi ARTIFACT="openship-dashboard-${TAG}.tar.gz" tar -czf "$ARTIFACT" -C "$STAGING" . sha256sum "$ARTIFACT" > "$ARTIFACT.sha256" echo "artifact=$ARTIFACT" >> "$GITHUB_OUTPUT" - name: Upload artifact uses: actions/upload-artifact@v7 with: name: openship-dashboard-release path: | ${{ steps.package.outputs.artifact }} ${{ steps.package.outputs.artifact }}.sha256 if-no-files-found: error retention-days: 7 build-desktop: name: Build desktop installer (${{ matrix.label }}) if: github.event_name == 'push' strategy: fail-fast: false matrix: include: - os: windows-latest label: Windows x64 find: "-name *.zip" asset: Openship-win32-x64.zip - os: ubuntu-latest label: Linux x64 find: "-name *.AppImage" asset: Openship.AppImage runs-on: ${{ matrix.os }} steps: - name: Checkout uses: actions/checkout@v7 with: fetch-depth: 0 - name: Read bun version id: bun-version shell: bash run: echo "version=$(cat .bun-version)" >> "$GITHUB_OUTPUT" - name: Setup Bun uses: oven-sh/setup-bun@v2 with: bun-version: ${{ steps.bun-version.outputs.version }} - name: Setup Node uses: actions/setup-node@v6 with: node-version: '24' - name: Restore bun cache uses: actions/cache@v6 with: path: ~/.bun/install/cache key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock') }} restore-keys: | ${{ runner.os }}-bun- - name: Install dependencies run: bun install --frozen-lockfile - name: Install AppImage build deps (Linux) if: runner.os == 'Linux' run: sudo apt-get update && sudo apt-get install -y libfuse2 - name: Build installer shell: bash env: APPIMAGE_EXTRACT_AND_RUN: '1' run: bun run --cwd apps/desktop make - name: Collect + rename installer shell: bash run: | set -euo pipefail FILE="$(find apps/desktop/out/make -type f ${{ matrix.find }} | head -n1)" if [[ -z "${FILE}" ]]; then echo "No installer matched '${{ matrix.find }}' under apps/desktop/out/make" find apps/desktop/out/make -type f || true exit 1 fi mkdir -p dist cp "${FILE}" "dist/${{ matrix.asset }}" # shasum exists on macOS/Linux but not the Windows Git-bash runner; # sha256sum exists on Windows/Linux but not macOS. Use whichever is present. cd dist if command -v shasum >/dev/null 2>&1; then shasum -a 256 "${{ matrix.asset }}" > "${{ matrix.asset }}.sha256" else sha256sum "${{ matrix.asset }}" > "${{ matrix.asset }}.sha256" fi - name: Upload artifact uses: actions/upload-artifact@v7 with: name: desktop-${{ matrix.asset }} path: | dist/${{ matrix.asset }} dist/${{ matrix.asset }}.sha256 if-no-files-found: error retention-days: 7 build-desktop-macos: name: Build desktop installers (macOS arm64 + x64) if: github.event_name == 'push' runs-on: macos-14 # Apple signing/notarization creds (repo secrets). All optional: when unset, # the signing + notarization steps below are skipped and the build is # unsigned (same as before). See forge.config.js osxSign. env: APPLE_IDENTITY: ${{ secrets.APPLE_IDENTITY }} APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} APPLE_CERTIFICATE_P12_BASE64: ${{ secrets.APPLE_CERTIFICATE_P12_BASE64 }} APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} steps: - name: Checkout uses: actions/checkout@v7 with: fetch-depth: 0 - name: Read bun version id: bun-version shell: bash run: echo "version=$(cat .bun-version)" >> "$GITHUB_OUTPUT" - name: Setup Bun uses: oven-sh/setup-bun@v2 with: bun-version: ${{ steps.bun-version.outputs.version }} - name: Setup Node uses: actions/setup-node@v6 with: node-version: '24' - name: Restore bun cache uses: actions/cache@v6 with: path: ~/.bun/install/cache key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock') }} restore-keys: | ${{ runner.os }}-bun- - name: Install dependencies run: bun install --frozen-lockfile # Import the Developer ID cert into a throwaway keychain so `electron-forge # make` can sign (osxSign in forge.config.js activates when APPLE_IDENTITY # is set). Skipped entirely when the secret is absent → unsigned build. - name: Import Apple signing certificate if: env.APPLE_IDENTITY != '' run: | set -euo pipefail KEYCHAIN="$RUNNER_TEMP/openship-signing.keychain-db" KEYCHAIN_PASSWORD="$(openssl rand -base64 24)" CERT="$RUNNER_TEMP/certificate.p12" echo "$APPLE_CERTIFICATE_P12_BASE64" | base64 --decode > "$CERT" security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN" security set-keychain-settings -lut 21600 "$KEYCHAIN" security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN" security import "$CERT" -k "$KEYCHAIN" -P "$APPLE_CERTIFICATE_PASSWORD" \ -T /usr/bin/codesign -T /usr/bin/security security set-key-partition-list -S apple-tool:,apple:,codesign: \ -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN" >/dev/null # Put our keychain first in the search list so codesign resolves the identity. security list-keychains -d user -s "$KEYCHAIN" \ $(security list-keychains -d user | sed 's/"//g') rm -f "$CERT" security find-identity -v -p codesigning "$KEYCHAIN" # A single Apple-Silicon runner builds both dmgs: bun --compile --target # cross-compiles the x64 API binary and electron-packager fetches x64 # Electron, so we don't depend on scarce macos-13 (Intel) runners. - name: Build macOS dmgs (arm64 + x64) shell: bash run: | set -euo pipefail cd apps/desktop bun run build bunx electron-forge make --arch=arm64 bunx electron-forge make --arch=x64 # Sign the disk image itself, then notarize + staple. notarytool scans the # DMG contents so the signed app inside is notarized too; signing the DMG # (not just the app) makes Gatekeeper's open-assessment on the downloaded # image pass cleanly, and stapling attaches the ticket for offline checks. # Gated on APPLE_ID (the notary credential), separate from signing: set # APPLE_IDENTITY only → signed-but-not-notarized (downloads still warn); # add APPLE_ID/PASSWORD/TEAM_ID → clean notarized download. - name: Sign, notarize + staple DMGs if: env.APPLE_ID != '' run: | set -euo pipefail for arch in arm64 x64; do dmg="apps/desktop/out/make/Openship-${arch}.dmg" echo "▸ signing ${dmg}" codesign --force --timestamp --sign "$APPLE_IDENTITY" "${dmg}" echo "▸ notarizing ${dmg}" xcrun notarytool submit "${dmg}" \ --apple-id "$APPLE_ID" \ --password "$APPLE_PASSWORD" \ --team-id "$APPLE_TEAM_ID" \ --wait xcrun stapler staple "${dmg}" xcrun stapler validate "${dmg}" done - name: Collect dmgs shell: bash run: | set -euo pipefail mkdir -p dist for arch in arm64 x64; do src="apps/desktop/out/make/Openship-${arch}.dmg" if [[ ! -f "${src}" ]]; then echo "Missing ${src}"; ls -R apps/desktop/out/make || true; exit 1 fi cp "${src}" "dist/Openship-${arch}.dmg" ( cd dist && shasum -a 256 "Openship-${arch}.dmg" > "Openship-${arch}.dmg.sha256" ) done - name: Upload artifact uses: actions/upload-artifact@v7 with: name: desktop-macos path: | dist/Openship-arm64.dmg dist/Openship-arm64.dmg.sha256 dist/Openship-x64.dmg dist/Openship-x64.dmg.sha256 if-no-files-found: error retention-days: 7 publish-npm: name: Publish CLI to npm runs-on: ubuntu-24.04 # OIDC trusted publishing: npm verifies this workflow's identity instead of # a long-lived token. id-token:write lets the runner mint the OIDC token. permissions: id-token: write contents: read steps: - name: Checkout uses: actions/checkout@v7 with: fetch-depth: 0 - name: Read bun version id: bun-version run: echo "version=$(cat .bun-version)" >> "$GITHUB_OUTPUT" - name: Setup Bun uses: oven-sh/setup-bun@v2 with: bun-version: ${{ steps.bun-version.outputs.version }} - name: Setup Node uses: actions/setup-node@v6 with: node-version: '24' registry-url: https://registry.npmjs.org - name: Ensure npm supports trusted publishing (OIDC) run: npm install -g npm@latest - name: Restore bun cache uses: actions/cache@v6 with: path: ~/.bun/install/cache key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock') }} restore-keys: | ${{ runner.os }}-bun- - name: Install dependencies run: bun install --frozen-lockfile - name: Build CLI run: bun run --cwd apps/cli build - name: Publish to npm (OIDC trusted publishing — no token) working-directory: apps/cli run: | set -euo pipefail TAG="${GITHUB_REF_NAME}" # tsup bundles the @repo/* deps into dist/, so strip them from the # published manifest — otherwise npm emits uninstallable workspace:* # specifiers for packages that aren't on the registry. npm pkg delete dependencies.@repo/core dependencies.@repo/onboarding # Prereleases (v1.2.3-rc.1) publish to the `next` dist-tag, not `latest`. if [[ "${TAG}" == *-* ]]; then npm publish --access public --tag next else npm publish --access public fi publish: name: Publish GitHub release if: github.event_name == 'push' needs: [build-openship, build-email, build-dashboard, build-desktop, build-desktop-macos] runs-on: ubuntu-24.04 steps: - name: Checkout uses: actions/checkout@v7 with: fetch-depth: 0 - name: Download openship artifact uses: actions/download-artifact@v8 with: name: openship-release path: dist - name: Download email artifact uses: actions/download-artifact@v8 with: name: openship-email-release path: dist - name: Download dashboard artifact uses: actions/download-artifact@v8 with: name: openship-dashboard-release path: dist - name: Download desktop installers uses: actions/download-artifact@v8 with: pattern: desktop-* path: dist merge-multiple: true - name: Create GitHub release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -euo pipefail TAG="${GITHUB_REF_NAME}" PRERELEASE_FLAG="" if [[ "${TAG}" == *-* ]]; then PRERELEASE_FLAG="--prerelease" fi gh release create "${TAG}" \ --title "Openship ${TAG}" \ --notes-from-tag \ ${PRERELEASE_FLAG} \ dist/openship-${TAG}-linux-amd64.tar.gz \ dist/openship-${TAG}-linux-amd64.tar.gz.sha256 \ dist/openship-email-${TAG}-linux-amd64.tar.gz \ dist/openship-email-${TAG}-linux-amd64.tar.gz.sha256 \ dist/openship-dashboard-${TAG}.tar.gz \ dist/openship-dashboard-${TAG}.tar.gz.sha256 - name: Attach desktop installers env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -euo pipefail TAG="${GITHUB_REF_NAME}" # Upload whichever desktop installers built (fail-fast:false means a # single-OS failure shouldn't block the release). for asset in Openship-arm64.dmg Openship-x64.dmg Openship-win32-x64.zip Openship.AppImage; do if [[ -f "dist/${asset}" ]]; then gh release upload "${TAG}" "dist/${asset}" "dist/${asset}.sha256" --clobber else echo "::warning::desktop installer missing: ${asset}" fi done