| name: Release |
| |
| on: |
| workflow_dispatch: |
| inputs: |
| source_reference_tag: |
| description: Exact approved ASF source candidate tag (v<version>-incubating-rc<positive-integer>) |
| required: true |
| type: string |
| |
| permissions: |
| contents: read |
| |
| concurrency: |
| group: release |
| cancel-in-progress: false |
| |
| jobs: |
| release-identity: |
| runs-on: ubuntu-latest |
| timeout-minutes: 10 |
| outputs: |
| version: ${{ steps.identity.outputs.version }} |
| is_prerelease: ${{ steps.identity.outputs.is_prerelease }} |
| 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 }} |
| source_archive: ${{ steps.identity.outputs.source_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: 60 |
| 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: 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 }}" |
| |
| # 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: 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-${{ matrix.platform }} |
| path: ${{ runner.temp }}/release-assets |
| if-no-files-found: error |
| retention-days: 7 |
| |
| - 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 pretest |
| 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 |
| path: ${{ runner.temp }}/release-assets |
| if-no-files-found: error |
| retention-days: 7 |
| |
| - name: Remove temporary release credentials |
| if: always() |
| run: rm -f "${{ runner.temp }}/AuthKey_Maka.p8" |
| |
| source: |
| needs: release-identity |
| runs-on: ubuntu-latest |
| timeout-minutes: 20 |
| 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 }} |
| 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 pinned dependency metadata |
| run: npm ci --ignore-scripts |
| |
| - name: Materialize bundled Git source materials |
| run: npm run prepare:bundled-git-source |
| |
| - name: Archive and hash source materials |
| env: |
| SOURCE_ARCHIVE: ${{ needs.release-identity.outputs.source_archive }} |
| run: | |
| archive_dir="apps/desktop/release-sources" |
| archive="${archive_dir}/${SOURCE_ARCHIVE}" |
| tar -C apps/desktop/release-sources/bundled-git -czf "$archive" . |
| (cd "$archive_dir" && sha256sum "$SOURCE_ARCHIVE" > "${SOURCE_ARCHIVE}.sha256") |
| |
| - name: Stage the exact product artifact group |
| run: >- |
| node scripts/product-release-artifacts.mjs stage |
| "source" |
| apps/desktop/release-sources |
| "${{ runner.temp }}/release-assets" |
| |
| - name: Upload verified source assets |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 |
| with: |
| name: release-source |
| path: ${{ runner.temp }}/release-assets |
| if-no-files-found: error |
| retention-days: 7 |
| |
| 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, source] |
| 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-* |
| path: release-assets |
| merge-multiple: true |
| |
| - name: Verify the exact product artifact manifest |
| run: | |
| node scripts/product-release-artifacts.mjs verify release-assets |
| while IFS= read -r -d '' checksum; do |
| (cd "$(dirname "$checksum")" && sha256sum -c "$(basename "$checksum")") |
| done < <(find release-assets -type f -name '*.sha256' -print0) |
| |
| - 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 }} |
| IS_PRERELEASE: ${{ needs.release-identity.outputs.is_prerelease }} |
| 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. |
| |
| Bundled Git source materials and their checksum are attached to this draft. The packaged applications also carry the GPLv2 license and written source offer. |
| |
| Known limitation: Computer Use is not included in this release." |
| |
| classification=(--prerelease=false --latest=false) |
| if [[ "$IS_PRERELEASE" == "true" ]]; then |
| classification=(--prerelease --latest=false) |
| elif [[ "$IS_PRERELEASE" != "false" ]]; then |
| echo "Product prerelease classification must be true or false; found $IS_PRERELEASE" >&2 |
| exit 1 |
| fi |
| |
| 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 \ |
| "${classification[@]}" \ |
| --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" \ |
| "${classification[@]}" \ |
| --title "Maka ${VERSION}" \ |
| --notes "$notes" |
| actual_prerelease="$(gh release view "$TAG" --json isPrerelease --jq .isPrerelease)" |
| if [[ "$actual_prerelease" != "$IS_PRERELEASE" ]]; then |
| echo "Release ${TAG} prerelease state is ${actual_prerelease}, expected ${IS_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 $IS_PRERELEASE" ]]; 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}." >> "$GITHUB_STEP_SUMMARY" |