mitts 0.3.29

Rust-first build and package tooling with a shared artifact cache
Documentation
# Mitts Build — gated native release delivery.
# Copyright (c) 2026 Alexander Panasenko
# Contact: alex@prod.codes
# Author: https://prod.codes/about/
# Project: https://github.com/alex09x/mitts
# SPDX-License-Identifier: MIT OR Apache-2.0
name: Release

on:
  workflow_dispatch:
    inputs:
      version:
        description: Stable version already committed through a reviewed PR (for example 0.4.0)
        type: string
        required: true
      mode:
        description: Verify does not create a tag or release; draft keeps assets private
        type: choice
        options: [verify, draft, publish]
        default: verify

permissions:
  contents: read

concurrency:
  group: mitts-release
  cancel-in-progress: false

jobs:
  source:
    runs-on: ubuntu-22.04
    outputs:
      tag: ${{ steps.plan.outputs.tag }}
      sha: ${{ steps.plan.outputs.sha }}
    steps:
      - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
        with:
          fetch-depth: 0
          persist-credentials: false
      - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
        with:
          python-version: '3.12'
      - id: plan
        env:
          VERSION: ${{ inputs.version }}
          MODE: ${{ inputs.mode }}
        run: python scripts/release/plan.py "$VERSION" "$MODE"
  prior-ci:
    needs: source
    runs-on: ubuntu-22.04
    timeout-minutes: 95
    permissions:
      contents: read
      actions: read
    outputs:
      reused: ${{ steps.proof.outputs.reused }}
    steps:
      - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
        with:
          persist-credentials: false
      - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
        with:
          python-version: '3.12'
      - id: proof
        env:
          GH_TOKEN: ${{ github.token }}
          SOURCE_SHA: ${{ needs.source.outputs.sha }}
        run: python scripts/release/reuse_ci.py "$SOURCE_SHA"
  validate:
    if: needs.prior-ci.outputs.reused != 'true'
    needs: [source, prior-ci]
    uses: ./.github/workflows/ci.yml
    with:
      ref: ${{ needs.source.outputs.sha }}
  validation-gate:
    if: always()
    needs: [source, prior-ci, validate]
    runs-on: ubuntu-22.04
    steps:
      - env:
          SOURCE: ${{ needs.source.result }}
          PROOF: ${{ needs.prior-ci.result }}
          REUSED: ${{ needs.prior-ci.outputs.reused }}
          VALIDATED: ${{ needs.validate.result }}
        run: test "$SOURCE" = success && test "$PROOF" = success && { test "$REUSED" = true || test "$VALIDATED" = success; }
  draft:
    if: ${{ !cancelled() && needs.source.result == 'success' && needs.validation-gate.result == 'success' && needs.binaries.result == 'success' && needs.apple-gate.result == 'success' && needs.crate-package.result == 'success' }}
    needs: [source, validation-gate, binaries, apple-gate, crate-package]
    runs-on: ubuntu-22.04
    permissions:
      contents: write
    steps:
      - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
        with:
          persist-credentials: false
      - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
        with:
          python-version: '3.12'
      - name: Create or resume a source-locked draft after all tests pass
        if: inputs.mode != 'verify'
        env:
          GH_TOKEN: ${{ github.token }}
          TAG: ${{ needs.source.outputs.tag }}
          SOURCE_SHA: ${{ needs.source.outputs.sha }}
        run: python scripts/release/github_release.py draft "$TAG" "$SOURCE_SHA"
      - name: Verification has no release mutations
        if: inputs.mode == 'verify'
        run: echo 'No remote tag, draft, upload or publication in verify mode.'
  binaries:
    if: ${{ !cancelled() && needs.source.result == 'success' && needs.validation-gate.result == 'success' }}
    needs: [source, validation-gate]
    uses: ./.github/workflows/release-binaries.yml
    with:
      tag: ${{ needs.source.outputs.tag }}
      source-sha: ${{ needs.source.outputs.sha }}
      verify-only: true
  crate-package:
    if: ${{ !cancelled() && needs.source.result == 'success' && needs.validation-gate.result == 'success' }}
    needs: [source, validation-gate]
    uses: ./.github/workflows/crate-package.yml
    with:
      tag: ${{ needs.source.outputs.tag }}
      source-sha: ${{ needs.source.outputs.sha }}
  apple-sign:
    if: ${{ !cancelled() && inputs.mode != 'verify' && github.repository == 'alex09x/mitts' && github.ref == 'refs/heads/main' && needs.source.result == 'success' && needs.binaries.result == 'success' }}
    needs: [source, binaries]
    runs-on: macos-15
    timeout-minutes: 60
    environment: release-signing
    outputs:
      artifact-id: ${{ steps.signed.outputs.artifact-id }}
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
        with:
          persist-credentials: false
      - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
        with:
          python-version: '3.12'
      - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
        with:
          pattern: release-*
          merge-multiple: true
          path: raw
      - name: Sign and notarize only release artifacts, never execute them with keys
        env:
          APPLE_CERTIFICATE_P12_BASE64: ${{ secrets.APPLE_CERTIFICATE_P12_BASE64 }}
          APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
          APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
          APPLE_NOTARY_KEY_P8: ${{ secrets.APPLE_NOTARY_KEY_P8 }}
          APPLE_NOTARY_KEY_ID: ${{ secrets.APPLE_NOTARY_KEY_ID }}
          APPLE_NOTARY_ISSUER_ID: ${{ secrets.APPLE_NOTARY_ISSUER_ID }}
          APPLE_TEAM_ID: ${{ vars.APPLE_TEAM_ID }}
          TAG: ${{ needs.source.outputs.tag }}
          SOURCE_SHA: ${{ needs.source.outputs.sha }}
        run: python scripts/release/apple_sign.py raw signed-macos notary-evidence "$TAG" "$SOURCE_SHA"
      - name: Remove private credentials even after a failed signing step
        if: always()
        run: python scripts/release/apple_keychain.py cleanup
      - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
        id: signed
        with:
          name: notarized-macos
          path: signed-macos/*
          if-no-files-found: error
          overwrite: true
      - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
        with:
          name: apple-notary-evidence
          path: notary-evidence/*.json
          if-no-files-found: error
          overwrite: true
  apple-verify:
    if: ${{ !cancelled() && inputs.mode != 'verify' && needs.apple-sign.result == 'success' }}
    needs: [source, apple-sign]
    strategy:
      fail-fast: false
      matrix:
        include:
          - os: macos-15
            target: aarch64-apple-darwin
    runs-on: ${{ matrix.os }}
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
        with:
          persist-credentials: false
      - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
        with:
          python-version: '3.12'
      - uses: dtolnay/rust-toolchain@b725eda525362e7cf34af9a7243cac1e3f01a7ec # stable
        with:
          toolchain: 1.98.0
      - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
        with:
          artifact-ids: ${{ needs.apple-sign.outputs.artifact-id }}
          merge-multiple: true
          path: signed-macos
      - name: Verify actual Developer ID and notarization without signing secrets
        env:
          APPLE_TEAM_ID: ${{ vars.APPLE_TEAM_ID }}
          TAG: ${{ needs.source.outputs.tag }}
          SOURCE_SHA: ${{ needs.source.outputs.sha }}
          TARGET: ${{ matrix.target }}
        run: python scripts/release/apple_verify.py "signed-macos/mitts-$TAG-$TARGET.tar.gz" "$TARGET" "$TAG" "$SOURCE_SHA"
  apple-gate:
    if: always()
    needs: [source, binaries, apple-sign, apple-verify]
    runs-on: ubuntu-22.04
    steps:
      - env:
          MODE: ${{ inputs.mode }}
          SOURCE: ${{ needs.source.result }}
          BINARIES: ${{ needs.binaries.result }}
          SIGNED: ${{ needs.apple-sign.result }}
          VERIFIED: ${{ needs.apple-verify.result }}
        run: test "$SOURCE" = success && test "$BINARIES" = success && { test "$MODE" = verify || { test "$SIGNED" = success && test "$VERIFIED" = success; }; }
  assets:
    if: ${{ !cancelled() && needs.source.result == 'success' && needs.binaries.result == 'success' && needs.draft.result == 'success' && needs.apple-gate.result == 'success' }}
    needs: [source, binaries, draft, apple-gate, apple-sign]
    runs-on: ubuntu-22.04
    permissions:
      contents: write
    env:
      APPLE_TEAM_ID: ${{ vars.APPLE_TEAM_ID }}
    steps:
      - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
        with:
          persist-credentials: false
      - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
        with:
          python-version: '3.12'
      - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
        if: inputs.mode == 'verify'
        with:
          pattern: release-*
          merge-multiple: true
          path: dist
      - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
        if: inputs.mode != 'verify'
        with:
          pattern: release-*-unknown-linux-gnu
          merge-multiple: true
          path: dist
      - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
        if: inputs.mode != 'verify'
        with:
          artifact-ids: ${{ needs.apple-sign.outputs.artifact-id }}
          merge-multiple: true
          path: dist
      - name: Require all supported archives and assemble checksums
        env:
          MODE: ${{ inputs.mode }}
          TAG: ${{ needs.source.outputs.tag }}
          SOURCE_SHA: ${{ needs.source.outputs.sha }}
        run: |
          if [[ "$MODE" == verify ]]; then
            python scripts/release/assets.py dist "$TAG" "$SOURCE_SHA"
          else
            python scripts/release/assets.py dist "$TAG" "$SOURCE_SHA" --require-notarized
          fi
      - name: Attach verified assets without overwriting
        if: inputs.mode != 'verify'
        env:
          GH_TOKEN: ${{ github.token }}
          TAG: ${{ needs.source.outputs.tag }}
          SOURCE_SHA: ${{ needs.source.outputs.sha }}
        run: python scripts/release/github_release.py upload "$TAG" "$SOURCE_SHA" --folder dist
      - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
        with:
          name: verified-checksums
          path: dist/SHA256SUMS
      - name: Publish only the complete verified draft
        if: inputs.mode == 'publish'
        env:
          GH_TOKEN: ${{ github.token }}
          TAG: ${{ needs.source.outputs.tag }}
          SOURCE_SHA: ${{ needs.source.outputs.sha }}
        run: python scripts/release/github_release.py publish "$TAG" "$SOURCE_SHA" --folder dist
  public-install:
    if: ${{ !cancelled() && inputs.mode == 'publish' && needs.source.result == 'success' && needs.assets.result == 'success' }}
    needs: [source, assets]
    uses: ./.github/workflows/installer-smoke.yml
    with:
      expected-tag: ${{ needs.source.outputs.tag }}
      expected-sha: ${{ needs.source.outputs.sha }}
      require-notarized: true
  registry-publish:
    if: ${{ !cancelled() && inputs.mode == 'publish' && github.repository == 'alex09x/mitts' && github.ref == 'refs/heads/main' && needs.public-install.result == 'success' && needs.crate-package.result == 'success' }}
    needs: [source, public-install, crate-package]
    runs-on: ubuntu-22.04
    timeout-minutes: 15
    environment: crates-io
    permissions:
      contents: read
      id-token: write
    steps:
      - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
        with:
          persist-credentials: false
      - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
        with:
          ref: ${{ needs.source.outputs.sha }}
          path: source
          fetch-depth: 0
          persist-credentials: false
      - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
        with:
          python-version: '3.12'
      - uses: dtolnay/rust-toolchain@b725eda525362e7cf34af9a7243cac1e3f01a7ec # stable
        with:
          toolchain: 1.98.0
      - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
        with:
          artifact-ids: ${{ needs.crate-package.outputs.artifact-id }}
          merge-multiple: true
          path: verified-crate
      - name: Require the clean exact source before obtaining publishing credentials
        env:
          SOURCE_SHA: ${{ needs.source.outputs.sha }}
          TAG: ${{ needs.source.outputs.tag }}
        run: |
          test "$(git -C source rev-parse HEAD)" = "$SOURCE_SHA"
          test -z "$(git -C source status --porcelain --untracked-files=all)"
          MITTS_INSTALL_ROOT="$RUNNER_TEMP/mitts-publish" sh site/install.sh
          test "$("$RUNNER_TEMP/mitts-publish/bin/mitts" --version)" = "mitts ${TAG#v} ($SOURCE_SHA)"
      - uses: rust-lang/crates-io-auth-action@3268eb413133e2960fa4ae0e963ca76380c54cd9 # v1
        id: auth
      - name: Publish only the exact verified package and verify public registry bytes
        env:
          CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }}
          MITTS_PUBLISH_BIN: ${{ runner.temp }}/mitts-publish/bin/mitts
          TAG: ${{ needs.source.outputs.tag }}
          SOURCE_SHA: ${{ needs.source.outputs.sha }}
        run: python scripts/release/registry.py publish verified-crate "$TAG" "$SOURCE_SHA" --manifest-path source/Cargo.toml