blob: c5b2ede9cfe7a849583967a22895f9c370d0ee01 [file]
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"