| # 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-24.04 |
| 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 }} |
| 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: |
| # Every target ships from one dispatch, so a failure on one still leaves |
| # the other artifacts available for diagnosis. Each architecture builds on |
| # a runner of its own: the native Runtime Host peer and the packaged smoke |
| # probes are host binaries. |
| fail-fast: false |
| matrix: |
| include: |
| - platform: macos |
| arch: arm64 |
| runner: macos-15 |
| - platform: macos |
| arch: x64 |
| runner: macos-15-intel |
| - platform: windows |
| arch: x64 |
| runner: windows-2025 |
| - platform: linux |
| arch: x64 |
| runner: ubuntu-24.04 |
| - platform: linux |
| arch: arm64 |
| runner: ubuntu-24.04-arm |
| 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 shipped product 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-${{ matrix.arch }} |
| |
| - 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 }} |
| run: | |
| # Discovered, not spelled out: this runner builds one architecture, so |
| # exactly one DMG exists, and signing does not need to know its name. |
| # The name itself is the descriptor's to decide and `verify:macos` |
| # below resolves it from there. |
| shopt -s nullglob |
| dmgs=(apps/desktop/release/*.dmg) |
| if [[ ${#dmgs[@]} -ne 1 ]]; then |
| echo "Expected exactly one DMG to sign, found ${#dmgs[@]}" >&2 |
| exit 1 |
| fi |
| DMG_PATH="${dmgs[0]}" |
| |
| 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 -- "${{ matrix.arch }}" |
| |
| # The updater is one code path across both macOS architectures, so the |
| # end-to-end update proof runs once, on the architecture most installs |
| # use. package-macos-autoupdate-next.mjs enforces the same restriction. |
| - name: Build the version-bumped macOS update |
| if: matrix.platform == 'macos' && matrix.arch == 'arm64' |
| 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' && matrix.arch == 'arm64' |
| run: npm run verify:macos-autoupdate -- 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 -- "${{ matrix.arch }}" |
| |
| # Discovered, not spelled out, for the same reason as the DMG above: this |
| # runner builds one installer, and the name is the descriptor's to decide. |
| # The installer steps below are handed a path rather than a target, so |
| # unlike `verify:windows-x64` they cannot resolve it themselves. |
| - name: Locate the packaged Windows installer |
| id: installer |
| if: matrix.platform == 'windows' |
| run: | |
| shopt -s nullglob |
| exes=(apps/desktop/release/*.exe) |
| if [[ ${#exes[@]} -ne 1 ]]; then |
| echo "Expected exactly one Windows installer, found ${#exes[@]}" >&2 |
| exit 1 |
| fi |
| echo "exe=${exes[0]}" >> "$GITHUB_OUTPUT" |
| |
| - 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 -- \ |
| "${{ steps.installer.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 -- \ |
| "${{ steps.installer.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 -- \ |
| "${{ steps.installer.outputs.exe }}" \ |
| apps/desktop/release-autoupdate-next |
| |
| # Linux ships unsigned for the same reason Windows does: there is no |
| # certificate yet, so packaging and verification are adjacent. |
| - name: Package the Linux AppImage and deb |
| if: matrix.platform == 'linux' |
| run: npm run package:linux |
| |
| - name: Ensure xvfb |
| if: matrix.platform == 'linux' |
| run: command -v xvfb-run >/dev/null 2>&1 || { sudo apt-get update && sudo apt-get install -y xvfb; } |
| |
| # `xvfb-run` because the verification launches the extracted AppImage's |
| # renderer over CDP, the way the macOS and Windows verifications do. |
| - name: Verify the Linux release |
| if: matrix.platform == 'linux' |
| run: xvfb-run -a npm run verify:linux -- "${{ matrix.arch }}" |
| |
| - name: Stage the exact product artifact group |
| run: >- |
| node scripts/product-release-artifacts.mjs stage |
| "desktop-${{ matrix.platform }}-${{ matrix.arch }}" |
| 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 }}-${{ matrix.arch }}-${{ 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: 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-24.04 |
| 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 |
| |
| # Each macOS runner uploaded a feed naming only its own architecture. The |
| # feed a client reads carries both, so it only exists once they are here. |
| - name: Merge the per-architecture update feeds |
| run: node scripts/product-release-artifacts.mjs merge-feeds release-assets |
| |
| - 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 |
| |
| # The runner uploads still carry the per-architecture feeds this job |
| # merged away. Finalize must publish and attest the published set, so it |
| # is handed these normalized bytes rather than the raw runner groups. The |
| # name deliberately avoids the `release-*` glob this job downloads. |
| - name: Upload the verified release assets |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 |
| with: |
| name: product-release-assets-${{ github.run_attempt }} |
| path: release-assets |
| 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 an Apple Silicon Mac, an Intel Mac, a Windows x64 machine, and an x64 and an arm64 Linux machine, and complete .github/RELEASE_CHECKLIST.md. |
| |
| The Windows and Linux builds are unsigned: SmartScreen warns on first launch, no Linux package is signed either, and every 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" |