fix(release): configure Speakeasy macOS signing (#34) #7
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: release | |
| on: | |
| push: | |
| tags: | |
| - "v*" | |
| permissions: | |
| contents: read | |
| env: | |
| CARGO_TERM_COLOR: always | |
| jobs: | |
| verify: | |
| runs-on: blacksmith-4vcpu-ubuntu-2404 | |
| steps: | |
| - uses: useblacksmith/checkout@v1 | |
| - uses: dtolnay/rust-toolchain@master | |
| with: | |
| toolchain: 1.94.0 | |
| components: rustfmt, clippy | |
| - name: Verify release tag matches Cargo version | |
| shell: bash | |
| run: | | |
| version=$(sed -n 's/^version = "\([^"]*\)"/\1/p' Cargo.toml | head -1) | |
| escaped_version=${version//./\.} | |
| if [[ $GITHUB_REF_NAME != "v$version" \ | |
| && ! $GITHUB_REF_NAME =~ ^v${escaped_version}-pre(\.[0-9]+)?$ ]]; then | |
| echo "expected v${version}, v${version}-pre, or v${version}-pre.N" >&2 | |
| exit 1 | |
| fi | |
| - run: cargo fmt --check | |
| - run: cargo clippy --locked --all-targets --all-features -- -D warnings | |
| - run: cargo test --locked | |
| linux-x64: | |
| needs: verify | |
| runs-on: blacksmith-8vcpu-ubuntu-2404 | |
| steps: | |
| - uses: useblacksmith/checkout@v1 | |
| - uses: dtolnay/rust-toolchain@master | |
| with: | |
| toolchain: 1.94.0 | |
| targets: x86_64-unknown-linux-gnu | |
| - name: Build and package | |
| shell: bash | |
| run: | | |
| version=${GITHUB_REF_NAME#v} | |
| cargo build --locked --release --target x86_64-unknown-linux-gnu | |
| mkdir -p stage dist | |
| cp target/x86_64-unknown-linux-gnu/release/kit stage/kit | |
| stage/kit --version | |
| tar -C stage -czf "dist/kit-v${version}-x86_64-unknown-linux-gnu.tar.gz" kit | |
| - uses: actions/upload-artifact@v4 | |
| with: | |
| name: kit-linux-x64 | |
| path: dist/* | |
| if-no-files-found: error | |
| macos-arm64: | |
| needs: verify | |
| runs-on: blacksmith-6vcpu-macos-15 | |
| outputs: | |
| signed: ${{ steps.signing.outputs.enabled }} | |
| env: | |
| MACOS_CERTIFICATE_P12_BASE64: ${{ secrets.MACOS_CERTIFICATE_P12_BASE64 }} | |
| MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} | |
| MACOS_SIGNING_IDENTITY: ${{ secrets.MACOS_SIGNING_IDENTITY }} | |
| APPLE_API_KEY_P8_BASE64: ${{ secrets.APPLE_API_KEY_P8_BASE64 }} | |
| APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} | |
| APPLE_API_ISSUER_ID: ${{ secrets.APPLE_API_ISSUER_ID }} | |
| steps: | |
| - uses: useblacksmith/checkout@v1 | |
| - uses: dtolnay/rust-toolchain@master | |
| with: | |
| toolchain: 1.94.0 | |
| targets: aarch64-apple-darwin | |
| - name: Check optional signing credentials | |
| id: signing | |
| shell: bash | |
| run: | | |
| credentials=( | |
| MACOS_CERTIFICATE_P12_BASE64 | |
| MACOS_CERTIFICATE_PASSWORD | |
| MACOS_SIGNING_IDENTITY | |
| APPLE_API_KEY_P8_BASE64 | |
| APPLE_API_KEY_ID | |
| APPLE_API_ISSUER_ID | |
| ) | |
| configured=0 | |
| for name in "${credentials[@]}"; do | |
| if [[ -n ${!name:-} ]]; then | |
| configured=$((configured + 1)) | |
| fi | |
| done | |
| if [[ $configured -eq 0 ]]; then | |
| echo "Apple credentials are not configured; building an unsigned macOS release." | |
| echo "enabled=false" >> "$GITHUB_OUTPUT" | |
| elif [[ $configured -eq ${#credentials[@]} ]]; then | |
| echo "enabled=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "Apple signing credentials are only partially configured." >&2 | |
| exit 1 | |
| fi | |
| - name: Import Developer ID certificate | |
| if: steps.signing.outputs.enabled == 'true' | |
| shell: bash | |
| run: | | |
| keychain=$RUNNER_TEMP/kit-signing.keychain-db | |
| certificate=$RUNNER_TEMP/kit-signing.p12 | |
| printf '%s' "$MACOS_CERTIFICATE_P12_BASE64" | base64 --decode > "$certificate" | |
| security create-keychain -p "$MACOS_CERTIFICATE_PASSWORD" "$keychain" | |
| security set-keychain-settings -lut 21600 "$keychain" | |
| security unlock-keychain -p "$MACOS_CERTIFICATE_PASSWORD" "$keychain" | |
| security import "$certificate" -P "$MACOS_CERTIFICATE_PASSWORD" \ | |
| -f pkcs12 -k "$keychain" -T /usr/bin/codesign | |
| security set-key-partition-list -S apple-tool:,apple: -s \ | |
| -k "$MACOS_CERTIFICATE_PASSWORD" "$keychain" | |
| security list-keychains -d user -s "$keychain" login.keychain-db | |
| - name: Build and package | |
| shell: bash | |
| env: | |
| SIGN_RELEASE: ${{ steps.signing.outputs.enabled }} | |
| run: | | |
| version=${GITHUB_REF_NAME#v} | |
| cargo build --locked --release --target aarch64-apple-darwin | |
| mkdir -p stage dist | |
| cp target/aarch64-apple-darwin/release/kit stage/kit | |
| if [[ $SIGN_RELEASE == true ]]; then | |
| codesign --force --options runtime --timestamp \ | |
| --identifier com.speakeasy.kit \ | |
| --sign "$MACOS_SIGNING_IDENTITY" stage/kit | |
| codesign --verify --strict --verbose=2 stage/kit | |
| ditto -c -k --keepParent stage/kit "$RUNNER_TEMP/kit-notarization.zip" | |
| api_key=$RUNNER_TEMP/AuthKey_${APPLE_API_KEY_ID}.p8 | |
| trap 'rm -f "$api_key"' EXIT | |
| printf '%s' "$APPLE_API_KEY_P8_BASE64" | base64 --decode > "$api_key" | |
| chmod 600 "$api_key" | |
| notary_log=$RUNNER_TEMP/kit-notarization.log | |
| xcrun notarytool submit "$RUNNER_TEMP/kit-notarization.zip" \ | |
| --key "$api_key" \ | |
| --key-id "$APPLE_API_KEY_ID" \ | |
| --issuer "$APPLE_API_ISSUER_ID" \ | |
| --wait 2>&1 | tee "$notary_log" | |
| if ! grep -Eq '(^|[[:space:]])status: Accepted([[:space:]]|$)' "$notary_log"; then | |
| echo "Apple did not accept the notarization submission." >&2 | |
| exit 1 | |
| fi | |
| fi | |
| stage/kit --version | |
| tar -C stage -czf "dist/kit-v${version}-aarch64-apple-darwin.tar.gz" kit | |
| - uses: actions/upload-artifact@v4 | |
| with: | |
| name: kit-macos-arm64 | |
| path: dist/* | |
| if-no-files-found: error | |
| publish: | |
| needs: [linux-x64, macos-arm64] | |
| runs-on: blacksmith-2vcpu-ubuntu-2404 | |
| permissions: | |
| contents: write | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| MACOS_SIGNED: ${{ needs.macos-arm64.outputs.signed }} | |
| steps: | |
| - uses: actions/download-artifact@v4 | |
| with: | |
| pattern: kit-* | |
| path: dist | |
| merge-multiple: true | |
| - name: Publish release | |
| shell: bash | |
| run: | | |
| cd dist | |
| sha256sum kit-*.tar.gz > SHA256SUMS | |
| prerelease=false | |
| if [[ $GITHUB_REF_NAME == *-pre* ]]; then | |
| prerelease=true | |
| fi | |
| if [[ $MACOS_SIGNED == true ]]; then | |
| signing_note="The macOS binary is Developer ID signed and notarized." | |
| else | |
| signing_note="The macOS binary is unsigned and not notarized." | |
| fi | |
| title="Kit ${GITHUB_REF_NAME#v}" | |
| notes="Prebuilt Kit binaries. $signing_note" | |
| if gh release view "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then | |
| gh release upload "$GITHUB_REF_NAME" kit-*.tar.gz SHA256SUMS \ | |
| --clobber --repo "$GITHUB_REPOSITORY" | |
| else | |
| create_args=( | |
| "$GITHUB_REF_NAME" kit-*.tar.gz SHA256SUMS | |
| --repo "$GITHUB_REPOSITORY" | |
| --title "$title" | |
| --notes "$notes" | |
| ) | |
| if [[ $prerelease == true ]]; then | |
| create_args+=(--prerelease) | |
| fi | |
| gh release create "${create_args[@]}" | |
| fi | |
| edit_args=( | |
| "$GITHUB_REF_NAME" | |
| --repo "$GITHUB_REPOSITORY" | |
| --title "$title" | |
| --notes "$notes" | |
| --draft=false | |
| --prerelease="$prerelease" | |
| ) | |
| if [[ $prerelease == false ]]; then | |
| newest_stable=$( | |
| gh release list --repo "$GITHUB_REPOSITORY" --limit 100 \ | |
| --json tagName,isDraft,isPrerelease \ | |
| --jq '.[] | select((.isDraft | not) and (.isPrerelease | not)) | .tagName' \ | |
| | sort -V | tail -1 | |
| ) | |
| if [[ $GITHUB_REF_NAME == "$newest_stable" ]]; then | |
| edit_args+=(--latest) | |
| fi | |
| fi | |
| gh release edit "${edit_args[@]}" | |
| container-build: | |
| name: Build container images (${{ matrix.arch }}) | |
| needs: publish | |
| runs-on: ${{ matrix.runner }} | |
| permissions: | |
| contents: read | |
| packages: write | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - arch: amd64 | |
| platform: linux/amd64 | |
| runner: ubuntu-24.04 | |
| - arch: arm64 | |
| platform: linux/arm64 | |
| runner: ubuntu-24.04-arm | |
| env: | |
| IMAGE: ghcr.io/${{ github.repository }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 | |
| - name: Read container version | |
| id: version | |
| shell: bash | |
| run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT" | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 | |
| - name: Log in to GitHub Container Registry | |
| uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Build and push slim image by digest | |
| id: slim | |
| uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 | |
| with: | |
| context: . | |
| target: slim | |
| platforms: ${{ matrix.platform }} | |
| build-args: | | |
| VERSION=${{ steps.version.outputs.version }} | |
| REVISION=${{ github.sha }} | |
| outputs: type=image,name=${{ env.IMAGE }},push-by-digest=true,name-canonical=true,push=true | |
| - name: Build and push Bookworm image by digest | |
| id: bookworm | |
| uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 | |
| with: | |
| context: . | |
| target: bookworm | |
| platforms: ${{ matrix.platform }} | |
| build-args: | | |
| VERSION=${{ steps.version.outputs.version }} | |
| REVISION=${{ github.sha }} | |
| outputs: type=image,name=${{ env.IMAGE }},push-by-digest=true,name-canonical=true,push=true | |
| - name: Build and push Alpine image by digest | |
| id: alpine | |
| uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 | |
| with: | |
| context: . | |
| target: alpine | |
| platforms: ${{ matrix.platform }} | |
| build-args: | | |
| VERSION=${{ steps.version.outputs.version }} | |
| REVISION=${{ github.sha }} | |
| outputs: type=image,name=${{ env.IMAGE }},push-by-digest=true,name-canonical=true,push=true | |
| - name: Export image digests | |
| env: | |
| SLIM_DIGEST: ${{ steps.slim.outputs.digest }} | |
| BOOKWORM_DIGEST: ${{ steps.bookworm.outputs.digest }} | |
| ALPINE_DIGEST: ${{ steps.alpine.outputs.digest }} | |
| shell: bash | |
| run: | | |
| mkdir -p /tmp/digests/{slim,bookworm,alpine} | |
| touch "/tmp/digests/slim/${SLIM_DIGEST#sha256:}" | |
| touch "/tmp/digests/bookworm/${BOOKWORM_DIGEST#sha256:}" | |
| touch "/tmp/digests/alpine/${ALPINE_DIGEST#sha256:}" | |
| - name: Upload image digests | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | |
| with: | |
| name: container-digests-${{ matrix.arch }} | |
| path: /tmp/digests | |
| if-no-files-found: error | |
| retention-days: 1 | |
| containers: | |
| name: Publish container image manifests | |
| needs: [publish, container-build] | |
| runs-on: ubuntu-24.04 | |
| permissions: | |
| contents: read | |
| packages: write | |
| env: | |
| IMAGE: ghcr.io/${{ github.repository }} | |
| steps: | |
| - name: Download image digests | |
| uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 | |
| with: | |
| pattern: container-digests-* | |
| path: /tmp/digests | |
| merge-multiple: true | |
| - name: Read release metadata | |
| id: release | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| shell: bash | |
| run: | | |
| if [[ $GITHUB_REF_NAME == *-pre* ]]; then | |
| echo "floating=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| newest_stable=$( | |
| gh release list --repo "$GITHUB_REPOSITORY" --limit 100 \ | |
| --json tagName,isDraft,isPrerelease \ | |
| --jq '.[] | select((.isDraft | not) and (.isPrerelease | not)) | .tagName' \ | |
| | sort -V | tail -1 | |
| ) | |
| if [[ $GITHUB_REF_NAME == "$newest_stable" ]]; then | |
| echo "floating=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "floating=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 | |
| - name: Log in to GitHub Container Registry | |
| uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Generate slim image metadata | |
| id: slim-meta | |
| uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0 | |
| with: | |
| images: ${{ env.IMAGE }} | |
| flavor: latest=false | |
| tags: | | |
| type=raw,value=${{ github.ref_name }} | |
| type=raw,value=${{ github.ref_name }}-slim | |
| type=raw,value=latest,enable=${{ steps.release.outputs.floating == 'true' }} | |
| type=raw,value=slim,enable=${{ steps.release.outputs.floating == 'true' }} | |
| - name: Publish slim manifest | |
| env: | |
| METADATA: ${{ steps.slim-meta.outputs.json }} | |
| shell: bash | |
| run: | | |
| mapfile -t tags < <(jq -r '.tags[]' <<< "$METADATA") | |
| mapfile -t digests < <(find /tmp/digests/slim -type f -printf "${IMAGE}@sha256:%f\n") | |
| args=() | |
| for tag in "${tags[@]}"; do args+=(--tag "$tag"); done | |
| docker buildx imagetools create "${args[@]}" "${digests[@]}" | |
| - name: Generate Bookworm image metadata | |
| id: bookworm-meta | |
| uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0 | |
| with: | |
| images: ${{ env.IMAGE }} | |
| flavor: latest=false | |
| tags: | | |
| type=raw,value=${{ github.ref_name }}-bookworm | |
| type=raw,value=bookworm,enable=${{ steps.release.outputs.floating == 'true' }} | |
| - name: Publish Bookworm manifest | |
| env: | |
| METADATA: ${{ steps.bookworm-meta.outputs.json }} | |
| shell: bash | |
| run: | | |
| mapfile -t tags < <(jq -r '.tags[]' <<< "$METADATA") | |
| mapfile -t digests < <(find /tmp/digests/bookworm -type f -printf "${IMAGE}@sha256:%f\n") | |
| args=() | |
| for tag in "${tags[@]}"; do args+=(--tag "$tag"); done | |
| docker buildx imagetools create "${args[@]}" "${digests[@]}" | |
| - name: Generate Alpine image metadata | |
| id: alpine-meta | |
| uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0 | |
| with: | |
| images: ${{ env.IMAGE }} | |
| flavor: latest=false | |
| tags: | | |
| type=raw,value=${{ github.ref_name }}-alpine | |
| type=raw,value=alpine,enable=${{ steps.release.outputs.floating == 'true' }} | |
| - name: Publish Alpine manifest | |
| env: | |
| METADATA: ${{ steps.alpine-meta.outputs.json }} | |
| shell: bash | |
| run: | | |
| mapfile -t tags < <(jq -r '.tags[]' <<< "$METADATA") | |
| mapfile -t digests < <(find /tmp/digests/alpine -type f -printf "${IMAGE}@sha256:%f\n") | |
| args=() | |
| for tag in "${tags[@]}"; do args+=(--tag "$tag"); done | |
| docker buildx imagetools create "${args[@]}" "${digests[@]}" | |
| - name: Verify container package is public | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| shell: bash | |
| run: | | |
| package=${GITHUB_REPOSITORY#*/} | |
| visibility=$( | |
| gh api "/orgs/${GITHUB_REPOSITORY_OWNER}/packages/container/${package}" \ | |
| --jq .visibility | |
| ) | |
| if [[ $visibility != public ]]; then | |
| echo "::error::Set the ${package} container package visibility to public, then rerun this workflow." | |
| echo "https://github.com/orgs/${GITHUB_REPOSITORY_OWNER}/packages/container/${package}/settings" | |
| exit 1 | |
| fi |