Build macOS Release Artifacts #154
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Build macOS Release Artifacts | |
| on: | |
| #schedule: | |
| # Run daily at 7am PT (3pm UTC during standard time, 2pm UTC during daylight saving) | |
| #- cron: "0 14 * * *" | |
| push: | |
| tags: | |
| - "v[0-9]+.[0-9]+.[0-9]+" # Matches v0.2.0, v1.0.0, etc. | |
| workflow_dispatch: | |
| inputs: | |
| release_version: | |
| description: "Release version (e.g., 0.2.0). Leave empty to auto-increment patch version for stable builds." | |
| required: false | |
| type: string | |
| default: "" | |
| release_nightly: | |
| description: "Build and release a nightly build" | |
| required: false | |
| type: boolean | |
| default: false | |
| permissions: | |
| contents: write # Needed to create releases | |
| jobs: | |
| build-macos: | |
| runs-on: macos-latest | |
| env: | |
| SENTRY_ORG: ${{ secrets.SENTRY_ORG }} | |
| SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }} | |
| SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} | |
| VITE_SENTRY_DSN: ${{ secrets.VITE_SENTRY_DSN }} | |
| # PostHog host is always the same | |
| VITE_PUBLIC_POSTHOG_HOST: https://us.i.posthog.com | |
| steps: | |
| - name: Generate version | |
| id: version | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| let buildVersion, isNightly, isStable, shouldCreateTag = false; | |
| // Determine build type | |
| if (context.eventName === 'schedule' || context.payload.inputs?.release_nightly === 'true') { | |
| // Nightly build | |
| const timestamp = new Date().toISOString() | |
| .replace(/[T:]/g, '-') | |
| .replace(/\..+/, '') | |
| .replace(/-/g, '') | |
| .slice(0, -2); // Format: YYYYMMDDHHMMSS | |
| buildVersion = `0.1.0-${timestamp}-nightly`; | |
| isNightly = true; | |
| isStable = false; | |
| } else if (context.eventName === 'push' && context.ref.startsWith('refs/tags/v')) { | |
| // Tag push - extract version from tag | |
| const tagVersion = context.ref.replace('refs/tags/v', ''); | |
| // Validate it's a proper semver (no pre-release identifiers) | |
| const semverRegex = /^[0-9]+\.[0-9]+\.[0-9]+$/; | |
| if (!semverRegex.test(tagVersion)) { | |
| throw new Error(`Tag ${tagVersion} contains pre-release identifier. Only stable semver tags supported.`); | |
| } | |
| buildVersion = tagVersion; | |
| isNightly = false; | |
| isStable = true; | |
| } else if (context.eventName === 'workflow_dispatch') { | |
| const inputVersion = context.payload.inputs?.release_version; | |
| if (inputVersion) { | |
| // Manual dispatch with explicit version | |
| // Strip leading 'v' if present for normalization | |
| buildVersion = inputVersion.replace(/^v/, ''); | |
| } else { | |
| // Manual dispatch without version - auto-increment patch | |
| // Fetch tags from GitHub API | |
| const tags = await github.rest.repos.listTags({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| per_page: 100 | |
| }); | |
| // Filter for stable semver tags (v1.2.3 format, no pre-release) | |
| const semverRegex = /^v([0-9]+)\.([0-9]+)\.([0-9]+)$/; | |
| const stableTags = tags.data | |
| .map(tag => tag.name) | |
| .filter(name => semverRegex.test(name)) | |
| .map(name => { | |
| const match = name.match(semverRegex); | |
| return { | |
| tag: name, | |
| major: parseInt(match[1]), | |
| minor: parseInt(match[2]), | |
| patch: parseInt(match[3]) | |
| }; | |
| }) | |
| .sort((a, b) => { | |
| if (a.major !== b.major) return b.major - a.major; | |
| if (a.minor !== b.minor) return b.minor - a.minor; | |
| return b.patch - a.patch; | |
| }); | |
| let newVersion; | |
| if (stableTags.length === 0) { | |
| // No stable tags exist, start with v0.2.0 | |
| newVersion = '0.2.0'; | |
| } else { | |
| // Increment patch version of the latest tag | |
| const latest = stableTags[0]; | |
| newVersion = `${latest.major}.${latest.minor}.${latest.patch + 1}`; | |
| } | |
| buildVersion = newVersion; | |
| shouldCreateTag = true; // Mark that we need to create a tag after checkout | |
| } | |
| isNightly = false; | |
| isStable = true; | |
| } else { | |
| // Fallback (shouldn't happen) | |
| const timestamp = new Date().toISOString() | |
| .replace(/[T:]/g, '-') | |
| .replace(/\..+/, '') | |
| .replace(/-/g, '') | |
| .slice(0, -2); | |
| buildVersion = `0.1.0-${timestamp}`; | |
| isNightly = false; | |
| isStable = false; | |
| } | |
| // Set outputs | |
| core.setOutput('release_version', buildVersion); | |
| core.setOutput('is_nightly', isNightly.toString()); | |
| core.setOutput('is_stable', isStable.toString()); | |
| core.setOutput('should_create_tag', shouldCreateTag.toString()); | |
| console.log(`Build Version: ${buildVersion}`); | |
| console.log(`Is Nightly: ${isNightly}`); | |
| console.log(`Is Stable: ${isStable}`); | |
| console.log(`Should Create Tag: ${shouldCreateTag}`); | |
| - name: Set PostHog API Key | |
| run: | | |
| if [[ "${{ steps.version.outputs.is_nightly }}" == "true" ]]; then | |
| echo "VITE_PUBLIC_POSTHOG_KEY=phc_de6RVF0G7CkTzv2UvxHddSk7nfFnE5QWD7KmZV5KfSo" >> $GITHUB_ENV | |
| else | |
| echo "VITE_PUBLIC_POSTHOG_KEY=phc_6RQ0mVrcMDwSgbKeGToTXC4ja11Hzhkm7tKyO5gjBrK" >> $GITHUB_ENV | |
| fi | |
| echo "PostHog key set for ${{ steps.version.outputs.is_nightly == 'true' && 'nightly' || 'stable' }} build" | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Create and push tag | |
| if: steps.version.outputs.should_create_tag == 'true' | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| git tag -a "v${{ steps.version.outputs.release_version }}" -m "Release v${{ steps.version.outputs.release_version }}" | |
| git push origin "v${{ steps.version.outputs.release_version }}" | |
| echo "Created and pushed tag: v${{ steps.version.outputs.release_version }}" | |
| - name: Setup Rust | |
| uses: dtolnay/rust-toolchain@stable | |
| - name: Setup Rust cache | |
| uses: Swatinem/rust-cache@v2 | |
| with: | |
| workspaces: humanlayer-wui/src-tauri | |
| - name: Setup Go | |
| uses: actions/setup-go@v5 | |
| id: setup-go | |
| with: | |
| go-version-file: "hld/go.mod" | |
| cache: false | |
| - name: Cache Go modules | |
| uses: actions/cache@v4 | |
| with: | |
| path: ~/go/pkg/mod | |
| key: go-mod-v2-${{ runner.os }}-go${{ steps.setup-go.outputs.go-version }}-${{ hashFiles('**/go.sum') }} | |
| restore-keys: | | |
| go-mod-v2-${{ runner.os }}-go${{ steps.setup-go.outputs.go-version }}- | |
| - name: Setup Bun | |
| uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version: latest | |
| - name: Cache Go tools | |
| uses: actions/cache@v4 | |
| id: go-tools-cache-release | |
| with: | |
| path: ~/go/bin | |
| key: go-tools-${{ runner.os }}-mockgen-0.5 | |
| - name: Run repository setup | |
| run: make setup | |
| - name: Install WUI dependencies | |
| working-directory: humanlayer-wui | |
| run: bun install | |
| - name: Build daemon for macOS ARM | |
| run: | | |
| cd hld | |
| # Set explicit LDFLAGS for each build type | |
| if [[ "${{ steps.version.outputs.is_nightly }}" == "true" ]]; then | |
| # Nightly build - use isolated paths and ports | |
| LDFLAGS="-X github.com/humanlayer/humanlayer/hld/internal/version.BuildVersion=${{ steps.version.outputs.release_version }}" | |
| LDFLAGS="${LDFLAGS} -X github.com/humanlayer/humanlayer/hld/config.DefaultDatabasePath=~/.humanlayer/daemon-nightly.db" | |
| LDFLAGS="${LDFLAGS} -X github.com/humanlayer/humanlayer/hld/config.DefaultSocketPath=~/.humanlayer/daemon-nightly.sock" | |
| LDFLAGS="${LDFLAGS} -X github.com/humanlayer/humanlayer/hld/config.DefaultHTTPPort=7778" | |
| LDFLAGS="${LDFLAGS} -X github.com/humanlayer/humanlayer/hld/config.DefaultCLICommand=humanlayer-nightly" | |
| else | |
| # Stable build - explicitly set standard paths and ports | |
| LDFLAGS="-X github.com/humanlayer/humanlayer/hld/internal/version.BuildVersion=${{ steps.version.outputs.release_version }}" | |
| LDFLAGS="${LDFLAGS} -X github.com/humanlayer/humanlayer/hld/config.DefaultDatabasePath=~/.humanlayer/daemon.db" | |
| LDFLAGS="${LDFLAGS} -X github.com/humanlayer/humanlayer/hld/config.DefaultSocketPath=~/.humanlayer/daemon.sock" | |
| LDFLAGS="${LDFLAGS} -X github.com/humanlayer/humanlayer/hld/config.DefaultHTTPPort=7777" | |
| LDFLAGS="${LDFLAGS} -X github.com/humanlayer/humanlayer/hld/config.DefaultCLICommand=humanlayer" | |
| fi | |
| echo "Using LDFLAGS: ${LDFLAGS}" | |
| GOOS=darwin GOARCH=arm64 go build -ldflags "${LDFLAGS}" -o hld-darwin-arm64 ./cmd/hld | |
| - name: Build humanlayer CLI for macOS ARM | |
| working-directory: hlyr | |
| run: | | |
| bun install | |
| bun run build | |
| bun build ./dist/index.js --compile --target=bun-darwin-arm64 --outfile=humanlayer-darwin-arm64 | |
| chmod +x humanlayer-darwin-arm64 | |
| - name: Copy binaries to Tauri resources | |
| run: | | |
| mkdir -p humanlayer-wui/src-tauri/bin | |
| cp hld/hld-darwin-arm64 humanlayer-wui/src-tauri/bin/hld | |
| cp hlyr/humanlayer-darwin-arm64 humanlayer-wui/src-tauri/bin/humanlayer | |
| chmod +x humanlayer-wui/src-tauri/bin/hld | |
| chmod +x humanlayer-wui/src-tauri/bin/humanlayer | |
| - name: Swap icons for nightly build | |
| if: steps.version.outputs.is_nightly == 'true' | |
| working-directory: humanlayer-wui/src-tauri | |
| run: | | |
| # Only swap icons for nightly builds | |
| mv icons icons-original | |
| cp -r icons-nightly icons | |
| - name: Build Tauri app | |
| working-directory: humanlayer-wui | |
| run: | | |
| export VITE_APP_VERSION="${{ steps.version.outputs.release_version }}" | |
| bun install | |
| if [[ "${{ steps.version.outputs.is_nightly }}" == "true" ]]; then | |
| bun run tauri build --config src-tauri/tauri.nightly.conf.json | |
| else | |
| bun run tauri build | |
| fi | |
| env: | |
| APPLE_SIGNING_IDENTITY: "-" | |
| NODE_ENV: "production" | |
| - name: Rename DMG for consistent naming | |
| working-directory: humanlayer-wui | |
| run: | | |
| DMG_PATH=$(find src-tauri/target/release/bundle/dmg -name "*.dmg" | head -1) | |
| if [[ "${{ steps.version.outputs.is_stable }}" == "true" ]]; then | |
| # Rename to CodeLayer-darwin-arm64.dmg for stable | |
| NEW_NAME="CodeLayer-darwin-arm64.dmg" | |
| else | |
| # Keep versioned name for nightly | |
| NEW_NAME=$(basename "$DMG_PATH") | |
| fi | |
| mv "$DMG_PATH" "src-tauri/target/release/bundle/dmg/$NEW_NAME" | |
| echo "dmg_filename=$NEW_NAME" >> $GITHUB_OUTPUT | |
| id: dmg_rename | |
| - name: Upload DMG artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: humanlayer-wui-macos-dmg | |
| path: humanlayer-wui/src-tauri/target/release/bundle/dmg/*.dmg | |
| if-no-files-found: error | |
| # Create GitHub Release with artifacts | |
| - name: Create Release | |
| if: github.event_name == 'workflow_dispatch' || github.event_name == 'schedule' || github.event_name == 'push' | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: ${{ steps.version.outputs.is_stable == 'true' && format('v{0}', steps.version.outputs.release_version) || steps.version.outputs.release_version }} | |
| name: codelayer-${{ steps.version.outputs.release_version }} | |
| files: | | |
| humanlayer-wui/src-tauri/target/release/bundle/dmg/${{ steps.dmg_rename.outputs.dmg_filename }} | |
| draft: false | |
| prerelease: ${{ steps.version.outputs.is_nightly == 'true' }} | |
| make_latest: ${{ steps.version.outputs.is_stable == 'true' }} | |
| body: | | |
| # CodeLayer ${{ steps.version.outputs.is_nightly == 'true' && 'Nightly' || 'Stable' }} Release | |
| Version: ${{ steps.version.outputs.release_version }} | |
| ## Installation | |
| ### Homebrew (Recommended) | |
| ```bash | |
| brew install --cask --no-quarantine humanlayer/humanlayer/codelayer${{ steps.version.outputs.is_nightly == 'true' && '-nightly' || '' }} | |
| ``` | |
| ### Manual Installation | |
| 1. Download the DMG file below | |
| 2. Open the DMG and drag CodeLayer${{ steps.version.outputs.is_nightly == 'true' && '-Nightly' || '' }} to Applications | |
| 3. Launch CodeLayer${{ steps.version.outputs.is_nightly == 'true' && '-Nightly' || '' }} from Applications | |
| ## Troubleshooting | |
| If you encounter "damaged app" warnings: | |
| ```bash | |
| xattr -rc /Applications/CodeLayer${{ steps.version.outputs.is_nightly == 'true' && '-Nightly' || '' }}.app | |
| ``` | |
| ## Logs | |
| Logs can be found at: | |
| ``` | |
| ~/Library/Logs/dev.humanlayer.wui${{ steps.version.outputs.is_nightly == 'true' && '.nightly' || '' }}/CodeLayer${{ steps.version.outputs.is_nightly == 'true' && '-Nightly' || '' }}.log | |
| ``` | |
| - name: Update Homebrew Cask | |
| if: steps.version.outputs.is_nightly == 'true' || steps.version.outputs.is_stable == 'true' | |
| run: | | |
| # Prepare variables | |
| DMG_FILE="humanlayer-wui/src-tauri/target/release/bundle/dmg/${{ steps.dmg_rename.outputs.dmg_filename }}" | |
| SHA256=$(shasum -a 256 "$DMG_FILE" | awk '{print $1}') | |
| if [[ "${{ steps.version.outputs.is_stable }}" == "true" ]]; then | |
| RELEASE_TAG="v${{ steps.version.outputs.release_version }}" | |
| CASK_FILE="Casks/codelayer.rb" | |
| DMG_FILENAME="CodeLayer-darwin-arm64.dmg" | |
| else | |
| RELEASE_TAG="${{ steps.version.outputs.release_version }}" | |
| CASK_FILE="Casks/codelayer-nightly.rb" | |
| DMG_FILENAME="${{ steps.dmg_rename.outputs.dmg_filename }}" | |
| fi | |
| DMG_URL="https://github.com/humanlayer/humanlayer/releases/download/${RELEASE_TAG}/${DMG_FILENAME}" | |
| echo "DMG_FILE=$DMG_FILE" >> $GITHUB_ENV | |
| echo "SHA256=$SHA256" >> $GITHUB_ENV | |
| echo "RELEASE_TAG=$RELEASE_TAG" >> $GITHUB_ENV | |
| echo "DMG_URL=$DMG_URL" >> $GITHUB_ENV | |
| echo "CASK_FILE=$CASK_FILE" >> $GITHUB_ENV | |
| - name: Checkout homebrew-humanlayer | |
| if: steps.version.outputs.is_nightly == 'true' || steps.version.outputs.is_stable == 'true' | |
| uses: actions/checkout@v4 | |
| with: | |
| repository: humanlayer/homebrew-humanlayer | |
| path: homebrew-humanlayer | |
| token: ${{ secrets.HUMANLAYER_HOMEBREW_CASK_WRITE_GITHUB_PAT }} | |
| - name: Generate stable cask file | |
| if: steps.version.outputs.is_stable == 'true' | |
| working-directory: homebrew-humanlayer | |
| run: | | |
| cat > Casks/codelayer.rb << 'EOF' | |
| cask "codelayer" do | |
| version "${{ steps.version.outputs.release_version }}" | |
| sha256 "${{ env.SHA256 }}" | |
| url "${{ env.DMG_URL }}" | |
| name "CodeLayer" | |
| desc "Desktop application for HumanLayer AI approvals" | |
| homepage "https://github.com/humanlayer/humanlayer" | |
| livecheck do | |
| url :url | |
| strategy :github_latest | |
| regex(/v?(\d+(?:\.\d+)+)$/i) | |
| end | |
| depends_on macos: ">= :monterey" | |
| app "CodeLayer.app" | |
| binary "#{appdir}/CodeLayer.app/Contents/Resources/bin/humanlayer" | |
| binary "#{appdir}/CodeLayer.app/Contents/Resources/bin/humanlayer", target: "codelayer" | |
| binary "#{appdir}/CodeLayer.app/Contents/Resources/bin/hld" | |
| zap trash: [ | |
| "~/.config/humanlayer/", | |
| "~/.humanlayer/daemon*.db", | |
| "~/.humanlayer/daemon*.sock", | |
| "~/.humanlayer/logs/", | |
| "~/Library/Application Support/CodeLayer/", | |
| "~/Library/Logs/dev.humanlayer.wui/", | |
| "~/Library/Preferences/dev.humanlayer.wui.plist", | |
| "~/Library/Saved Application State/dev.humanlayer.wui.savedState", | |
| ] | |
| end | |
| EOF | |
| - name: Generate nightly cask file | |
| if: steps.version.outputs.is_nightly == 'true' | |
| working-directory: homebrew-humanlayer | |
| run: | | |
| cat > Casks/codelayer-nightly.rb << 'EOF' | |
| cask "codelayer-nightly" do | |
| version "${{ steps.version.outputs.release_version }}" | |
| sha256 "${{ env.SHA256 }}" | |
| url "${{ env.DMG_URL }}", | |
| verified: "github.com/humanlayer/humanlayer/" | |
| name "CodeLayer Nightly" | |
| desc "Nightly build of CodeLayer - Desktop application for managing AI agent approvals" | |
| homepage "https://humanlayer.dev/" | |
| # No conflicts - can install alongside stable | |
| app "CodeLayer-Nightly.app" | |
| binary "#{appdir}/CodeLayer-Nightly.app/Contents/Resources/bin/humanlayer", target: "humanlayer-nightly" | |
| binary "#{appdir}/CodeLayer-Nightly.app/Contents/Resources/bin/humanlayer", target: "codelayer-nightly" | |
| binary "#{appdir}/CodeLayer-Nightly.app/Contents/Resources/bin/hld", target: "hld-nightly" | |
| zap trash: [ | |
| "~/Library/Application Support/CodeLayer-Nightly", | |
| "~/Library/Preferences/dev.humanlayer.wui.nightly.plist", | |
| "~/Library/Saved Application State/dev.humanlayer.wui.nightly.savedState", | |
| "~/.humanlayer/codelayer-nightly*.json", | |
| "~/.humanlayer/daemon-nightly*.db", | |
| "~/.humanlayer/daemon-nightly*.sock", | |
| "~/Library/Logs/dev.humanlayer.wui.nightly/", | |
| ] | |
| end | |
| EOF | |
| - name: Commit and push cask update | |
| if: steps.version.outputs.is_nightly == 'true' || steps.version.outputs.is_stable == 'true' | |
| working-directory: homebrew-humanlayer | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| git add "${{ env.CASK_FILE }}" | |
| if [[ "${{ steps.version.outputs.is_stable }}" == "true" ]]; then | |
| COMMIT_MSG="Update codelayer to ${{ steps.version.outputs.release_version }}" | |
| else | |
| COMMIT_MSG="Update codelayer-nightly to ${{ steps.version.outputs.release_version }}" | |
| fi | |
| git diff --staged --quiet || (git commit -m "$COMMIT_MSG" && git push) |