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 with:
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 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 with:
persist-credentials: false
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 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 with:
persist-credentials: false
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 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 with:
persist-credentials: false
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 with:
python-version: '3.12'
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 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 id: signed
with:
name: notarized-macos
path: signed-macos/*
if-no-files-found: error
overwrite: true
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 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 with:
persist-credentials: false
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 with:
python-version: '3.12'
- uses: dtolnay/rust-toolchain@b725eda525362e7cf34af9a7243cac1e3f01a7ec with:
toolchain: 1.98.0
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 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 with:
persist-credentials: false
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 with:
python-version: '3.12'
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 if: inputs.mode == 'verify'
with:
pattern: release-*
merge-multiple: true
path: dist
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 if: inputs.mode != 'verify'
with:
pattern: release-*-unknown-linux-gnu
merge-multiple: true
path: dist
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 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 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 with:
persist-credentials: false
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 with:
ref: ${{ needs.source.outputs.sha }}
path: source
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 with:
python-version: '3.12'
- uses: dtolnay/rust-toolchain@b725eda525362e7cf34af9a7243cac1e3f01a7ec with:
toolchain: 1.98.0
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 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 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