Release #2
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: | |
| workflow_dispatch: | |
| inputs: | |
| version: | |
| description: Release version without a v prefix, for example 0.1.0 | |
| required: true | |
| type: string | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: release-${{ inputs.version }} | |
| cancel-in-progress: false | |
| jobs: | |
| validate: | |
| name: Validate release | |
| runs-on: macos-26 | |
| outputs: | |
| release_sha: ${{ steps.release.outputs.release_sha }} | |
| release_version: ${{ steps.release.outputs.release_version }} | |
| steps: | |
| - name: Check out repository | |
| uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ github.sha }} | |
| - name: Validate version and release commit | |
| id: release | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| RELEASE_VERSION: ${{ inputs.version }} | |
| run: | | |
| set -euo pipefail | |
| if [[ "$GITHUB_REF" != "refs/heads/main" ]]; then | |
| echo "Releases must be dispatched from the main branch." >&2 | |
| exit 1 | |
| fi | |
| if [[ ! "$RELEASE_VERSION" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then | |
| echo "Release version must be plain semantic version such as 0.1.0." >&2 | |
| exit 1 | |
| fi | |
| embedded_version=$(sed -nE 's/^[[:space:]]*internal static let version: String = "([^"]+)"$/\1/p' Sources/fxcodex-cli/AppCommand.swift) | |
| if [[ -z "$embedded_version" ]]; then | |
| echo "Could not read AppCommand.version." >&2 | |
| exit 1 | |
| fi | |
| if [[ "$embedded_version" != "$RELEASE_VERSION" ]]; then | |
| echo "Release $RELEASE_VERSION does not match AppCommand.version $embedded_version." >&2 | |
| exit 1 | |
| fi | |
| if gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$RELEASE_VERSION" >/dev/null 2>&1; then | |
| echo "Tag $RELEASE_VERSION already exists." >&2 | |
| exit 1 | |
| fi | |
| if gh release view "$RELEASE_VERSION" >/dev/null 2>&1; then | |
| echo "Release $RELEASE_VERSION already exists." >&2 | |
| exit 1 | |
| fi | |
| echo "release_sha=$GITHUB_SHA" >> "$GITHUB_OUTPUT" | |
| echo "release_version=$RELEASE_VERSION" >> "$GITHUB_OUTPUT" | |
| resolve: | |
| name: Resolve release dependencies | |
| needs: validate | |
| runs-on: macos-26 | |
| steps: | |
| - name: Check out repository | |
| uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ needs.validate.outputs.release_sha }} | |
| - name: Show Swift toolchain | |
| run: swift --version | |
| - name: Resolve dependencies | |
| run: | | |
| set -euo pipefail | |
| swift package resolve | |
| test -f Package.resolved | |
| - name: Upload dependency snapshot | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: release-dependencies | |
| path: Package.resolved | |
| if-no-files-found: error | |
| retention-days: 7 | |
| build: | |
| name: Build ${{ matrix.architecture }} | |
| needs: | |
| - validate | |
| - resolve | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - architecture: aarch64 | |
| runner: macos-26 | |
| artifact: fxcodex-aarch64-apple-darwin | |
| - architecture: x86_64 | |
| runner: macos-26-intel | |
| artifact: fxcodex-x86_64-apple-darwin | |
| runs-on: ${{ matrix.runner }} | |
| steps: | |
| - name: Check out repository | |
| uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ needs.validate.outputs.release_sha }} | |
| - name: Download dependency snapshot | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: release-dependencies | |
| path: . | |
| - name: Show Swift toolchain | |
| run: swift --version | |
| - name: Verify dependency snapshot | |
| run: swift package resolve --force-resolved-versions | |
| - name: Test | |
| run: make test | |
| - name: Build release executable | |
| run: make release | |
| - name: Verify executable and checksum | |
| shell: bash | |
| env: | |
| ARTIFACT: ${{ matrix.artifact }} | |
| RELEASE_VERSION: ${{ needs.validate.outputs.release_version }} | |
| run: | | |
| set -euo pipefail | |
| test -f "dist/$ARTIFACT" | |
| test -f "dist/$ARTIFACT.sha256" | |
| test "$(dist/$ARTIFACT version)" = "$RELEASE_VERSION" | |
| (cd dist && shasum -a 256 --check "$ARTIFACT.sha256") | |
| - name: Upload architecture artifacts | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: release-architecture-${{ matrix.architecture }} | |
| path: | | |
| dist/${{ matrix.artifact }} | |
| dist/${{ matrix.artifact }}.sha256 | |
| if-no-files-found: error | |
| retention-days: 7 | |
| publish: | |
| name: Publish GitHub Release | |
| needs: | |
| - build | |
| - resolve | |
| - validate | |
| runs-on: macos-26 | |
| environment: release | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Check out repository | |
| uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ needs.validate.outputs.release_sha }} | |
| - name: Download architecture artifacts | |
| uses: actions/download-artifact@v8 | |
| with: | |
| pattern: release-architecture-* | |
| path: dist | |
| merge-multiple: true | |
| - name: Download dependency snapshot | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: release-dependencies | |
| path: dist | |
| - name: Build universal executable | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| arm_artifact=dist/fxcodex-aarch64-apple-darwin | |
| intel_artifact=dist/fxcodex-x86_64-apple-darwin | |
| universal_artifact=dist/fxcodex-universal-apple-darwin | |
| xcrun lipo -create \ | |
| "$arm_artifact" \ | |
| "$intel_artifact" \ | |
| -output "$universal_artifact" | |
| chmod 755 "$universal_artifact" | |
| xcrun lipo "$universal_artifact" -verify_arch arm64 x86_64 | |
| - name: Import Developer ID identity | |
| shell: bash | |
| env: | |
| CERTIFICATE_BASE64: ${{ secrets.DEVELOPER_ID_CERTIFICATE_P12_BASE64 }} | |
| CERTIFICATE_PASSWORD: ${{ secrets.DEVELOPER_ID_CERTIFICATE_PASSWORD }} | |
| run: | | |
| set -euo pipefail | |
| test -n "$CERTIFICATE_BASE64" | |
| test -n "$CERTIFICATE_PASSWORD" | |
| certificate_path="$RUNNER_TEMP/developer-id.p12" | |
| keychain_path="$RUNNER_TEMP/fxcodex-signing.keychain-db" | |
| keychain_password=$(uuidgen) | |
| printf '%s' "$CERTIFICATE_BASE64" | base64 -D > "$certificate_path" | |
| security create-keychain -p "$keychain_password" "$keychain_path" | |
| security set-keychain-settings -lut 21600 "$keychain_path" | |
| security unlock-keychain -p "$keychain_password" "$keychain_path" | |
| security import "$certificate_path" \ | |
| -f pkcs12 \ | |
| -k "$keychain_path" \ | |
| -P "$CERTIFICATE_PASSWORD" \ | |
| -T /usr/bin/codesign | |
| security set-key-partition-list \ | |
| -S apple-tool:,apple: \ | |
| -s \ | |
| -k "$keychain_password" \ | |
| "$keychain_path" | |
| security list-keychains -d user -s "$keychain_path" | |
| identity=$(security find-identity -v -p codesigning \ | |
| | awk '/Developer ID Application/ { print $2; exit }') | |
| if [[ -z "$identity" ]]; then | |
| echo "The imported Developer ID Application identity is not discoverable." >&2 | |
| exit 1 | |
| fi | |
| signing_probe="$RUNNER_TEMP/fxcodex-signing-probe" | |
| cp /usr/bin/true "$signing_probe" | |
| codesign \ | |
| --force \ | |
| --keychain "$keychain_path" \ | |
| --options runtime \ | |
| --sign "$identity" \ | |
| --timestamp=none \ | |
| "$signing_probe" | |
| codesign --verify --strict --verbose=2 "$signing_probe" | |
| rm -f "$signing_probe" | |
| echo "SIGNING_IDENTITY=$identity" >> "$GITHUB_ENV" | |
| echo "SIGNING_KEYCHAIN=$keychain_path" >> "$GITHUB_ENV" | |
| - name: Prepare notarization credentials | |
| shell: bash | |
| env: | |
| API_KEY_BASE64: ${{ secrets.APP_STORE_CONNECT_API_KEY_P8_BASE64 }} | |
| run: | | |
| set -euo pipefail | |
| test -n "$API_KEY_BASE64" | |
| key_path="$RUNNER_TEMP/AuthKey.p8" | |
| printf '%s' "$API_KEY_BASE64" | base64 -D > "$key_path" | |
| chmod 600 "$key_path" | |
| echo "NOTARY_KEY_PATH=$key_path" >> "$GITHUB_ENV" | |
| - name: Sign release executables | |
| env: | |
| SIGNING_IDENTITY: ${{ env.SIGNING_IDENTITY }} | |
| SIGNING_KEYCHAIN: ${{ env.SIGNING_KEYCHAIN }} | |
| run: | | |
| ./Scripts/sign-release.sh \ | |
| dist/fxcodex-aarch64-apple-darwin \ | |
| dist/fxcodex-x86_64-apple-darwin \ | |
| dist/fxcodex-universal-apple-darwin | |
| - name: Notarize release executables | |
| env: | |
| APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }} | |
| APP_STORE_CONNECT_KEY_ID: ${{ secrets.APP_STORE_CONNECT_KEY_ID }} | |
| NOTARY_KEY_PATH: ${{ env.NOTARY_KEY_PATH }} | |
| run: | | |
| ./Scripts/notarize-release.sh \ | |
| dist/fxcodex-aarch64-apple-darwin \ | |
| dist/fxcodex-x86_64-apple-darwin \ | |
| dist/fxcodex-universal-apple-darwin | |
| - name: Verify release bundle | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| test -f dist/Package.resolved | |
| test "$(find dist -maxdepth 1 -type f | wc -l | tr -d ' ')" = 7 | |
| (cd dist && shasum -a 256 --check ./*.sha256) | |
| - name: Recheck release availability | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| RELEASE_VERSION: ${{ needs.validate.outputs.release_version }} | |
| run: | | |
| set -euo pipefail | |
| if gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$RELEASE_VERSION" >/dev/null 2>&1; then | |
| echo "Tag $RELEASE_VERSION was created while the release was building." >&2 | |
| exit 1 | |
| fi | |
| if gh release view "$RELEASE_VERSION" >/dev/null 2>&1; then | |
| echo "Release $RELEASE_VERSION was created while the release was building." >&2 | |
| exit 1 | |
| fi | |
| - name: Publish release | |
| id: publish | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| GH_REPO: ${{ github.repository }} | |
| RELEASE_MARKER: <!-- fxcodex-release-run:${{ github.run_id }}-${{ github.run_attempt }} --> | |
| RELEASE_SHA: ${{ needs.validate.outputs.release_sha }} | |
| RELEASE_VERSION: ${{ needs.validate.outputs.release_version }} | |
| run: | | |
| gh release create "$RELEASE_VERSION" dist/* \ | |
| --target "$RELEASE_SHA" \ | |
| --generate-notes \ | |
| --notes "$RELEASE_MARKER" \ | |
| --title "$RELEASE_VERSION" | |
| - name: Clean up failed publication | |
| if: ${{ failure() && steps.publish.outcome == 'failure' }} | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| GH_REPO: ${{ github.repository }} | |
| RELEASE_MARKER: <!-- fxcodex-release-run:${{ github.run_id }}-${{ github.run_attempt }} --> | |
| RELEASE_VERSION: ${{ needs.validate.outputs.release_version }} | |
| run: | | |
| set -euo pipefail | |
| release_is_draft=$(gh release view "$RELEASE_VERSION" \ | |
| --json isDraft \ | |
| --jq '.isDraft' \ | |
| 2>/dev/null || true) | |
| release_body=$(gh release view "$RELEASE_VERSION" \ | |
| --json body \ | |
| --jq '.body' \ | |
| 2>/dev/null || true) | |
| if \ | |
| [[ "$release_is_draft" == "true" ]] \ | |
| && [[ "$release_body" == *"$RELEASE_MARKER"* ]] | |
| then | |
| gh release delete "$RELEASE_VERSION" --cleanup-tag --yes | |
| fi | |
| - name: Remove signing material | |
| if: ${{ always() }} | |
| shell: bash | |
| run: | | |
| security delete-keychain "$RUNNER_TEMP/fxcodex-signing.keychain-db" 2>/dev/null || true | |
| rm -f \ | |
| "$RUNNER_TEMP/developer-id.p12" \ | |
| "$RUNNER_TEMP/AuthKey.p8" \ | |
| "$RUNNER_TEMP/fxcodex-signing-probe" |