# Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. name: Release on: workflow_dispatch: inputs: source_reference_tag: description: Exact approved ASF source candidate tag (v-incubating-rc) required: true type: string permissions: contents: read concurrency: group: product-release cancel-in-progress: false jobs: release-identity: runs-on: ubuntu-latest timeout-minutes: 10 outputs: version: ${{ steps.identity.outputs.version }} tag: ${{ steps.identity.outputs.tag }} source_commit: ${{ steps.identity.outputs.source_commit }} source_reference_tag: ${{ steps.identity.outputs.source_reference_tag }} dmg: ${{ steps.identity.outputs.dmg }} exe: ${{ steps.identity.outputs.exe }} cli_archive: ${{ steps.identity.outputs.cli_archive }} node_version: ${{ steps.identity.outputs.node_version }} npm_version: ${{ steps.identity.outputs.npm_version }} node_archive: ${{ steps.identity.outputs.node_archive }} node_archive_sha256: ${{ steps.identity.outputs.node_archive_sha256 }} node_source_url: ${{ steps.identity.outputs.node_source_url }} steps: - name: Check out the dispatched commit uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ github.sha }} fetch-depth: 0 persist-credentials: false - name: Resolve product release identity id: identity env: GITHUB_SHA: ${{ github.sha }} SOURCE_REFERENCE_TAG: ${{ inputs.source_reference_tag }} run: node scripts/product-release-identity.mjs - name: Require the exact ASF source reference env: RELEASE_REF: ${{ github.ref }} RELEASE_REPOSITORY: ${{ github.repository }} SOURCE_COMMIT: ${{ steps.identity.outputs.source_commit }} SOURCE_REFERENCE_TAG: ${{ steps.identity.outputs.source_reference_tag }} run: | if [[ "$RELEASE_REPOSITORY" != "apache/maka" ]]; then echo "Product releases must run in apache/maka; found $RELEASE_REPOSITORY" >&2 exit 1 fi if [[ "$RELEASE_REF" != "refs/tags/$SOURCE_REFERENCE_TAG" ]]; then echo "Product releases must be dispatched from $SOURCE_REFERENCE_TAG; found $RELEASE_REF" >&2 exit 1 fi git fetch --force --no-tags origin \ "refs/tags/$SOURCE_REFERENCE_TAG:refs/tags/$SOURCE_REFERENCE_TAG" test "$(git cat-file -t "refs/tags/$SOURCE_REFERENCE_TAG")" = tag test "$(git rev-parse "refs/tags/$SOURCE_REFERENCE_TAG^{commit}")" = "$SOURCE_COMMIT" git fetch --no-tags origin main:refs/remotes/origin/main git merge-base --is-ancestor "$SOURCE_COMMIT" origin/main desktop: needs: release-identity strategy: # Both platforms ship from one dispatch, so a failure on one still leaves # the other artifact available for diagnosis. fail-fast: false matrix: include: - platform: macos runner: macos-15 - platform: windows runner: windows-2025 runs-on: ${{ matrix.runner }} environment: release timeout-minutes: 75 defaults: run: # Windows runners default to pwsh; the release steps are written once, # in bash, for both platforms. shell: bash steps: - name: Check out the dispatched commit uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ needs.release-identity.outputs.source_commit }} fetch-depth: 0 persist-credentials: false - name: Set up the pinned release Node.js uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: ${{ needs.release-identity.outputs.node_version }} cache: npm - name: Select and verify the pinned npm release toolchain run: | npm install --global --no-audit --no-fund "npm@${{ needs.release-identity.outputs.npm_version }}" test "$(npm --version)" = "${{ needs.release-identity.outputs.npm_version }}" - name: Install dependencies with the pinned npm run: npm ci - name: Audit production dependencies run: npm audit --omit=dev --audit-level=moderate - name: Audit shipped desktop closure run: node scripts/audit-shipped-dependencies.mjs - name: Update stable Rust for Desktop native artifacts run: rustup update stable --no-self-update - name: Write App Store Connect API key if: matrix.platform == 'macos' env: APPLE_API_KEY_CONTENT: ${{ secrets.APPLE_API_KEY }} APPLE_API_KEY_PATH: ${{ runner.temp }}/AuthKey_Maka.p8 run: | umask 077 printf '%s' "$APPLE_API_KEY_CONTENT" > "$APPLE_API_KEY_PATH" - name: Package notarized app and signed DMG if: matrix.platform == 'macos' env: CSC_LINK: ${{ secrets.CSC_LINK }} CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} APPLE_API_KEY: ${{ runner.temp }}/AuthKey_Maka.p8 APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} run: npm run package:macos-arm64 - name: Notarize and staple the signed final DMG if: matrix.platform == 'macos' env: APPLE_API_KEY: ${{ runner.temp }}/AuthKey_Maka.p8 APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} DMG_PATH: apps/desktop/release/${{ needs.release-identity.outputs.dmg }} run: | codesign --verify --verbose=4 "$DMG_PATH" xcrun notarytool submit "$DMG_PATH" \ --key "$APPLE_API_KEY" \ --key-id "$APPLE_API_KEY_ID" \ --issuer "$APPLE_API_ISSUER" \ --wait xcrun stapler staple "$DMG_PATH" codesign --verify --verbose=4 "$DMG_PATH" xcrun stapler validate "$DMG_PATH" spctl --assess \ --type open \ --context context:primary-signature \ --verbose=4 \ "$DMG_PATH" - name: Verify the final DMG if: matrix.platform == 'macos' run: npm run verify:macos-arm64 -- "apps/desktop/release/${{ needs.release-identity.outputs.dmg }}" - name: Build the version-bumped macOS update if: matrix.platform == 'macos' env: CSC_LINK: ${{ secrets.CSC_LINK }} CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} run: npm run package:macos-autoupdate-next - name: Verify macOS automatic update end to end if: matrix.platform == 'macos' run: | npm run verify:macos-autoupdate -- \ "apps/desktop/release/Maka-${{ needs.release-identity.outputs.version }}-mac-arm64.zip" \ apps/desktop/release-autoupdate-next # Windows has no Authenticode certificate yet, so this build is unsigned # and there is nothing to notarize between packaging and verification. - name: Package the Windows installer and ZIP if: matrix.platform == 'windows' run: npm run package:windows-x64 - name: Verify the Windows release if: matrix.platform == 'windows' run: npm run verify:windows-x64 -- "apps/desktop/release/${{ needs.release-identity.outputs.exe }}" - name: Download and verify the pinned Windows upgrade baseline id: previous if: matrix.platform == 'windows' env: GH_TOKEN: ${{ github.token }} run: | previous_exe="$(node scripts/prepare-windows-upgrade-baseline.mjs \ "${{ needs.release-identity.outputs.version }}" artifacts/windows-upgrade-baseline)" echo "exe=$previous_exe" >> "$GITHUB_OUTPUT" - name: Exercise pinned Windows upgrade and uninstall if: matrix.platform == 'windows' run: | npm run verify:windows-installer -- \ "apps/desktop/release/${{ needs.release-identity.outputs.exe }}" \ "${{ steps.previous.outputs.exe }}" - name: Build the version-bumped autoupdate installer if: matrix.platform == 'windows' run: npm run package:windows-autoupdate-next - name: Verify automatic update end to end if: matrix.platform == 'windows' run: | npm run verify:windows-autoupdate -- \ "apps/desktop/release/${{ needs.release-identity.outputs.exe }}" \ apps/desktop/release-autoupdate-next - name: Prove deterministic mid-install failure rollback if: matrix.platform == 'windows' run: | npm run verify:windows-installer-rollback -- \ "apps/desktop/release/${{ needs.release-identity.outputs.exe }}" \ apps/desktop/release-autoupdate-next - name: Stage the exact product artifact group run: >- node scripts/product-release-artifacts.mjs stage "desktop-${{ matrix.platform }}" apps/desktop/release "${{ runner.temp }}/release-assets" - name: Upload the verified release assets uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: release-desktop-${{ matrix.platform }}-${{ github.run_attempt }} path: ${{ runner.temp }}/release-assets if-no-files-found: error retention-days: 30 - name: Remove temporary release credentials if: always() && matrix.platform == 'macos' run: rm -f "${{ runner.temp }}/AuthKey_Maka.p8" cli-macos-arm64: needs: release-identity runs-on: macos-15 environment: release timeout-minutes: 45 defaults: run: shell: bash steps: - name: Check out the dispatched commit uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ needs.release-identity.outputs.source_commit }} fetch-depth: 0 persist-credentials: false - name: Set up the pinned release Node.js uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: ${{ needs.release-identity.outputs.node_version }} cache: npm - name: Select and verify the pinned npm release toolchain run: | npm install --global --no-audit --no-fund "npm@${{ needs.release-identity.outputs.npm_version }}" test "$(npm --version)" = "${{ needs.release-identity.outputs.npm_version }}" - name: Install dependencies with the pinned npm run: npm ci - name: Audit production dependencies run: npm audit --omit=dev --audit-level=moderate - name: Build the CLI production workspace closure run: | npm --workspace maka-agent run build:workspace-deps npm --workspace maka-agent run build npm run check:cli-third-party-notices - name: Download and verify the official Node runtime env: NODE_ARCHIVE: ${{ runner.temp }}/${{ needs.release-identity.outputs.node_archive }} NODE_ARCHIVE_SHA256: ${{ needs.release-identity.outputs.node_archive_sha256 }} NODE_SOURCE_URL: ${{ needs.release-identity.outputs.node_source_url }} run: | curl --proto '=https' --tlsv1.2 --fail --location \ --output "$NODE_ARCHIVE" "$NODE_SOURCE_URL" actual="$(shasum -a 256 "$NODE_ARCHIVE" | awk '{print $1}')" test "$actual" = "$NODE_ARCHIVE_SHA256" - name: Write App Store Connect API key env: APPLE_API_KEY_CONTENT: ${{ secrets.APPLE_API_KEY }} APPLE_API_KEY_PATH: ${{ runner.temp }}/AuthKey_Maka.p8 run: | umask 077 printf '%s' "$APPLE_API_KEY_CONTENT" > "$APPLE_API_KEY_PATH" - name: Package signed and notarized CLI and TUI env: CSC_LINK: ${{ secrets.CSC_LINK }} CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} APPLE_API_KEY: ${{ runner.temp }}/AuthKey_Maka.p8 APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} MAKA_CLI_NODE_ARCHIVE: ${{ runner.temp }}/${{ needs.release-identity.outputs.node_archive }} MAKA_CLI_RELEASE_SIGNING: '1' run: npm run package:cli:macos-arm64 - name: Verify the final CLI and TUI artifact env: MAKA_CLI_REQUIRE_RELEASE_SIGNING: '1' run: >- npm run verify:cli:macos-arm64 -- "apps/desktop/release/${{ needs.release-identity.outputs.cli_archive }}" - name: Stage the exact product artifact group run: >- node scripts/product-release-artifacts.mjs stage "cli-macos-arm64" apps/desktop/release "${{ runner.temp }}/release-assets" - name: Upload the verified CLI release assets uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: release-cli-macos-arm64-${{ github.run_attempt }} path: ${{ runner.temp }}/release-assets if-no-files-found: error retention-days: 30 - name: Remove temporary release credentials if: always() run: rm -f "${{ runner.temp }}/AuthKey_Maka.p8" publish: # One draft release carries both platforms, so it is created once, after # every platform has been packaged and verified. needs: [release-identity, desktop, cli-macos-arm64] runs-on: ubuntu-latest timeout-minutes: 15 permissions: contents: write defaults: run: shell: bash steps: - name: Check out the dispatched commit uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ needs.release-identity.outputs.source_commit }} fetch-depth: 0 persist-credentials: false - name: Download the verified release assets uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: pattern: release-*-${{ github.run_attempt }} path: release-assets merge-multiple: true - name: Set up the pinned release Node.js uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: ${{ needs.release-identity.outputs.node_version }} package-manager-cache: false - name: Select the pinned npm release toolchain run: | npm install --global --no-audit --no-fund "npm@${{ needs.release-identity.outputs.npm_version }}" test "$(npm --version)" = "${{ needs.release-identity.outputs.npm_version }}" - name: Install the release verifier dependencies run: npm ci --ignore-scripts --no-audit --no-fund - name: Verify the exact product artifact manifest run: node scripts/product-release-artifacts.mjs verify release-assets - name: Record the immutable publication evidence env: GITHUB_SHA: ${{ needs.release-identity.outputs.source_commit }} SOURCE_REFERENCE_TAG: ${{ needs.release-identity.outputs.source_reference_tag }} run: | node scripts/product-release-artifacts.mjs record \ release-assets \ "$RUNNER_TEMP/product-release.json" \ "$GITHUB_REPOSITORY" \ "$GITHUB_RUN_ID" \ "$GITHUB_RUN_ATTEMPT" - name: Upload the immutable publication evidence uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: product-release-record-${{ github.run_attempt }} path: ${{ runner.temp }}/product-release.json if-no-files-found: error compression-level: 0 retention-days: 30 - name: Revalidate the live ASF source reference env: SOURCE_COMMIT: ${{ needs.release-identity.outputs.source_commit }} SOURCE_REFERENCE_TAG: ${{ needs.release-identity.outputs.source_reference_tag }} run: | git fetch --force --no-tags origin \ "refs/tags/$SOURCE_REFERENCE_TAG:refs/tags/$SOURCE_REFERENCE_TAG" test "$(git cat-file -t "refs/tags/$SOURCE_REFERENCE_TAG")" = tag test "$(git rev-parse "refs/tags/$SOURCE_REFERENCE_TAG^{commit}")" = "$SOURCE_COMMIT" git fetch --no-tags origin main:refs/remotes/origin/main git merge-base --is-ancestor "$SOURCE_COMMIT" origin/main - name: Ensure the exact product tag env: GH_TOKEN: ${{ github.token }} TAG: ${{ needs.release-identity.outputs.tag }} SOURCE_COMMIT: ${{ needs.release-identity.outputs.source_commit }} run: | gh auth setup-git node scripts/product-release-tag.mjs ensure "$TAG" "$SOURCE_COMMIT" - name: Create or update the draft GitHub Release env: GH_TOKEN: ${{ github.token }} VERSION: ${{ needs.release-identity.outputs.version }} TAG: ${{ needs.release-identity.outputs.tag }} SOURCE_COMMIT: ${{ needs.release-identity.outputs.source_commit }} SOURCE_REFERENCE_TAG: ${{ needs.release-identity.outputs.source_reference_tag }} run: | notes="Maka Desktop and the required Apple Silicon CLI/TUI artifact built from approved ASF source reference ${SOURCE_REFERENCE_TAG} at commit ${SOURCE_COMMIT}. Before publishing, download these draft assets on another Apple Silicon Mac and on a Windows x64 machine, and complete .github/RELEASE_CHECKLIST.md. The Windows build is unsigned: SmartScreen warns on first launch, and the download has to be checked against its .sha256 file. These files are convenience binaries built from the approved ASF source release; they are not ASF release artifacts. Known limitations: Computer Use and Gitoxide-backed workspace execution are not included in this release. The Desktop convenience artifacts do not distribute a Git runtime." if gh release view "$TAG" >/dev/null 2>&1; then is_draft="$(gh release view "$TAG" --json isDraft --jq .isDraft)" if [[ "$is_draft" != "true" ]]; then echo "Release ${TAG} is already published and cannot be replaced." >&2 exit 1 fi else gh release create "$TAG" \ --draft \ --verify-tag \ --prerelease=false \ --latest=false \ --title "Maka ${VERSION}" \ --notes "$notes" fi if [[ "$(gh release view "$TAG" --json isDraft --jq .isDraft)" != "true" ]]; then echo "Release ${TAG} was published while recovery was running." >&2 exit 1 fi gh release edit "$TAG" \ --prerelease=false \ --latest=false \ --title "Maka ${VERSION}" \ --notes "$notes" actual_prerelease="$(gh release view "$TAG" --json isPrerelease --jq .isPrerelease)" if [[ "$actual_prerelease" != "false" ]]; then echo "Release ${TAG} must not be a prerelease." >&2 exit 1 fi asset_names="$(gh release view "$TAG" --json assets --jq '.assets[].name')" declare -A expected_assets=() declare -A existing_assets=() mapfile -t local_assets < <(find release-assets -maxdepth 1 -type f -printf '%f\n' | sort) for asset in "${local_assets[@]}"; do expected_assets["$asset"]=1 done existing_dir="$RUNNER_TEMP/existing-release-assets" mkdir -p "$existing_dir" if [[ -n "$asset_names" ]]; then while IFS= read -r asset; do if [[ -z "${expected_assets[$asset]+present}" ]]; then echo "Draft ${TAG} contains unexpected asset ${asset}; inspect and remove it manually before retrying." >&2 exit 1 fi gh release download "$TAG" --pattern "$asset" --dir "$existing_dir" if ! cmp -s "release-assets/$asset" "$existing_dir/$asset"; then echo "Draft ${TAG} asset ${asset} differs from the verified artifact." >&2 exit 1 fi existing_assets["$asset"]=1 done <<< "$asset_names" fi missing_assets=() for asset in "${local_assets[@]}"; do if [[ -z "${existing_assets[$asset]+present}" ]]; then missing_assets+=("release-assets/$asset") fi done release_state="$(gh release view "$TAG" --json isDraft,isPrerelease --jq '[.isDraft, .isPrerelease] | map(tostring) | join(" ")')" if [[ "$release_state" != "true false" ]]; then echo "Release ${TAG} changed state while assets were checked: ${release_state}." >&2 exit 1 fi if (( ${#missing_assets[@]} > 0 )); then gh release upload "$TAG" "${missing_assets[@]}" fi { echo "Draft release ${TAG} created from ${SOURCE_COMMIT}." echo "Release workflow run: ${GITHUB_RUN_ID}, attempt: ${GITHUB_RUN_ATTEMPT}." } >> "$GITHUB_STEP_SUMMARY"