blob: 73006cb1a96664971ae007506bf5d841fe503335 [file]
# 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<version>-incubating-rc<positive-integer>)
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 }}
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 }}
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 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-${{ 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 }}
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.
These files are convenience binaries built from the approved ASF source release; they are not ASF release artifacts.
Known limitations: Computer Use and managed-workspace execution are not included in this release. The Desktop convenience artifacts do not distribute a Git runtime."
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}."
echo "Release workflow run: ${GITHUB_RUN_ID}, attempt: ${GITHUB_RUN_ATTEMPT}."
} >> "$GITHUB_STEP_SUMMARY"