Release #21
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: | |
| release: | |
| types: [published] | |
| workflow_dispatch: | |
| inputs: | |
| version: | |
| description: Release version without the leading v | |
| required: true | |
| repository: | |
| description: GitHub repository that will host the release assets | |
| required: false | |
| notarization_submission_id: | |
| description: Apple notarization submission ID to finalize | |
| required: false | |
| notarization_run_id: | |
| description: Release workflow run ID that uploaded the notarization artifact | |
| required: false | |
| permissions: | |
| actions: read | |
| contents: write | |
| jobs: | |
| guard-published-release: | |
| if: github.event_name == 'release' | |
| runs-on: ubuntu-latest | |
| env: | |
| RELEASE_TAG: ${{ github.event.release.tag_name }} | |
| RELEASE_REPOSITORY: ${{ github.repository }} | |
| steps: | |
| - name: Verify release has archives | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| version="${RELEASE_TAG#v}" | |
| archive_name="comux-${version}.zip" | |
| cask_name="comux.rb" | |
| assets_path="$RUNNER_TEMP/comux-release-assets.txt" | |
| gh release view "$RELEASE_TAG" \ | |
| --repo "$RELEASE_REPOSITORY" \ | |
| --json assets \ | |
| --jq '.assets[].name' > "$assets_path" | |
| has_asset() { | |
| local asset_name="$1" | |
| grep -Fx "$asset_name" "$assets_path" >/dev/null | |
| } | |
| missing=() | |
| has_asset "$archive_name" || missing+=("$archive_name") | |
| has_asset "$cask_name" || missing+=("$cask_name") | |
| if [[ "${#missing[@]}" -gt 0 ]]; then | |
| echo "::error::Public release $RELEASE_TAG is missing required archive assets: ${missing[*]}" | |
| echo "Use the workflow_dispatch draft-first flow, wait for Apple notarization to be accepted, then finalize the same release before publishing." | |
| exit 1 | |
| fi | |
| package-macos: | |
| if: github.event_name == 'workflow_dispatch' | |
| runs-on: macos-15 | |
| env: | |
| RELEASE_VERSION: ${{ inputs.version }} | |
| RELEASE_REPOSITORY: ${{ inputs.repository != '' && inputs.repository || github.repository }} | |
| APPLE_CERTIFICATE_CONFIGURED: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_P12_BASE64 != '' && 'true' || 'false' }} | |
| FINALIZE_NOTARIZATION: ${{ inputs.notarization_submission_id != '' && 'true' || 'false' }} | |
| NOTARIZATION_SUBMISSION_ID: ${{ inputs.notarization_submission_id }} | |
| NOTARIZATION_RUN_ID: ${{ inputs.notarization_run_id }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Select Xcode | |
| uses: maxim-lobanov/setup-xcode@v1 | |
| with: | |
| xcode-version: "16.4" | |
| - name: Install packaging tools | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| brew install xcodegen | |
| - name: Resolve release metadata | |
| id: meta | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| version="${RELEASE_VERSION#v}" | |
| if [[ -z "$version" ]]; then | |
| echo "Missing release version." >&2 | |
| exit 1 | |
| fi | |
| echo "version=$version" >> "$GITHUB_OUTPUT" | |
| echo "tag=v$version" >> "$GITHUB_OUTPUT" | |
| echo "notarization_artifact=comux-notarization-$version" >> "$GITHUB_OUTPUT" | |
| - name: Verify draft release | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| tag="${{ steps.meta.outputs.tag }}" | |
| is_draft="$(gh release view "$tag" \ | |
| --repo "$RELEASE_REPOSITORY" \ | |
| --json isDraft \ | |
| --jq '.isDraft' \ | |
| 2>/dev/null || true)" | |
| if [[ -z "$is_draft" ]]; then | |
| echo "::error::Missing draft release $tag. Create a draft release, write the release notes, then rerun this workflow." | |
| exit 1 | |
| fi | |
| if [[ "$is_draft" != "true" && "$FINALIZE_NOTARIZATION" != "true" ]]; then | |
| echo "::error::Release $tag is already public. Start releases from workflow_dispatch so the release stays draft until archives are uploaded." | |
| echo "If you are repairing an existing public release after notarization, rerun this workflow with notarization_submission_id and notarization_run_id." | |
| exit 1 | |
| fi | |
| release_notes="$(gh release view "$tag" \ | |
| --repo "$RELEASE_REPOSITORY" \ | |
| --json body \ | |
| --jq '.body')" | |
| changes_header_line="$(grep -n -m1 -Fx "## What's Changed" <<<"$release_notes" | cut -d: -f1 || true)" | |
| if [[ -z "$changes_header_line" ]]; then | |
| echo "::error::Release $tag must contain an exact ## What's Changed heading." | |
| exit 1 | |
| fi | |
| changes_section="$(tail -n "+$((changes_header_line + 1))" <<<"$release_notes" | sed '/^## /q')" | |
| if ! grep -Eq '^- .+' <<<"$changes_section"; then | |
| echo "::error::Release $tag needs at least one user-facing bullet under ## What's Changed." | |
| exit 1 | |
| fi | |
| - name: Resolve prebuilt release assets | |
| id: prebuilt | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| EVENT_NAME: ${{ github.event_name }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| archive_name="comux-${{ steps.meta.outputs.version }}.zip" | |
| cask_name="comux.rb" | |
| cask_path="$RUNNER_TEMP/$cask_name" | |
| has_asset() { | |
| local asset_name="$1" | |
| grep -Fx "$asset_name" "$RUNNER_TEMP/comux-release-assets.txt" >/dev/null | |
| } | |
| : > "$RUNNER_TEMP/comux-release-assets.txt" | |
| has_prebuilt=false | |
| for attempt in {1..12}; do | |
| gh release view "${{ steps.meta.outputs.tag }}" \ | |
| --repo "$RELEASE_REPOSITORY" \ | |
| --json assets \ | |
| --jq '.assets[].name' > "$RUNNER_TEMP/comux-release-assets.txt" || true | |
| if has_asset "$archive_name" && has_asset "$cask_name"; then | |
| has_prebuilt=true | |
| break | |
| fi | |
| if [[ "$EVENT_NAME" != "release" ]]; then | |
| break | |
| fi | |
| sleep 5 | |
| done | |
| if [[ "$has_prebuilt" == "true" ]]; then | |
| gh release download "${{ steps.meta.outputs.tag }}" \ | |
| --repo "$RELEASE_REPOSITORY" \ | |
| --pattern "$cask_name" \ | |
| --dir "$RUNNER_TEMP" \ | |
| --clobber | |
| echo "Using prebuilt release assets: $archive_name, $cask_name" | |
| else | |
| echo "No complete prebuilt release asset set found; building in CI." | |
| fi | |
| echo "has_prebuilt=$has_prebuilt" >> "$GITHUB_OUTPUT" | |
| echo "cask=$cask_path" >> "$GITHUB_OUTPUT" | |
| - name: Import Developer ID certificate | |
| if: env.APPLE_CERTIFICATE_CONFIGURED == 'true' && steps.prebuilt.outputs.has_prebuilt != 'true' && env.FINALIZE_NOTARIZATION != 'true' | |
| env: | |
| APPLE_DEVELOPER_CERTIFICATE_P12_BASE64: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_P12_BASE64 }} | |
| APPLE_DEVELOPER_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_PASSWORD }} | |
| KEYCHAIN_PASSWORD: ${{ secrets.APPLE_KEYCHAIN_PASSWORD }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| keychain_path="$RUNNER_TEMP/comux-signing.keychain-db" | |
| certificate_path="$RUNNER_TEMP/developer-id.p12" | |
| echo "$APPLE_DEVELOPER_CERTIFICATE_P12_BASE64" | base64 --decode > "$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" \ | |
| -P "$APPLE_DEVELOPER_CERTIFICATE_PASSWORD" \ | |
| -A \ | |
| -k "$keychain_path" | |
| security list-keychains -d user -s "$keychain_path" | |
| security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" "$keychain_path" | |
| - name: Download notarized app candidate | |
| if: env.FINALIZE_NOTARIZATION == 'true' && env.NOTARIZATION_RUN_ID != '' && steps.prebuilt.outputs.has_prebuilt != 'true' | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: ${{ steps.meta.outputs.notarization_artifact }} | |
| path: ${{ runner.temp }}/comux-notarization | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| repository: ${{ github.repository }} | |
| run-id: ${{ env.NOTARIZATION_RUN_ID }} | |
| - name: Restore notarized app candidate | |
| if: env.FINALIZE_NOTARIZATION == 'true' && steps.prebuilt.outputs.has_prebuilt != 'true' | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| if [[ -z "${NOTARIZATION_RUN_ID:-}" ]]; then | |
| echo "notarization_run_id is required when finalizing notarization." >&2 | |
| exit 1 | |
| fi | |
| app_archive="$RUNNER_TEMP/comux-notarization/comux-app.zip" | |
| if [[ ! -f "$app_archive" ]]; then | |
| echo "Missing notarization artifact app archive: $app_archive" >&2 | |
| exit 1 | |
| fi | |
| mkdir -p .build/apple | |
| rm -rf .build/apple/comux.app | |
| ditto -x -k "$app_archive" .build/apple | |
| if [[ ! -d .build/apple/comux.app ]]; then | |
| echo "Notarization artifact did not contain comux.app." >&2 | |
| exit 1 | |
| fi | |
| - name: Build macOS app archive and cask | |
| if: steps.prebuilt.outputs.has_prebuilt != 'true' | |
| id: package | |
| env: | |
| APPLE_CODE_SIGN_IDENTITY: ${{ vars.APPLE_CODE_SIGN_IDENTITY }} | |
| COMUX_NOTARY_APPLE_ID: ${{ secrets.APPLE_NOTARY_APPLE_ID }} | |
| COMUX_NOTARY_TEAM_ID: ${{ secrets.APPLE_NOTARY_TEAM_ID }} | |
| COMUX_NOTARY_PASSWORD: ${{ secrets.APPLE_NOTARY_PASSWORD }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| output_file="$(mktemp)" | |
| trap 'rm -f "$output_file"' EXIT | |
| args=( | |
| --version "${{ steps.meta.outputs.version }}" \ | |
| --repo "${RELEASE_REPOSITORY}" | |
| ) | |
| if [[ "$FINALIZE_NOTARIZATION" == "true" ]]; then | |
| args+=(--finalize-notarization "$NOTARIZATION_SUBMISSION_ID") | |
| elif [[ "$APPLE_CERTIFICATE_CONFIGURED" == "true" ]]; then | |
| if [[ -n "${APPLE_CODE_SIGN_IDENTITY:-}" ]]; then | |
| export COMUX_CODE_SIGN_IDENTITY="$APPLE_CODE_SIGN_IDENTITY" | |
| else | |
| export COMUX_CODE_SIGN_IDENTITY="$( | |
| security find-identity -v -p codesigning \ | |
| | sed -n 's/.*"\(Developer ID Application:[^"]*\)".*/\1/p' \ | |
| | head -n1 | |
| )" | |
| fi | |
| if [[ -z "${COMUX_CODE_SIGN_IDENTITY:-}" ]]; then | |
| echo "No Developer ID Application signing identity found." >&2 | |
| security find-identity -v -p codesigning >&2 | |
| exit 1 | |
| fi | |
| fi | |
| if [[ "$FINALIZE_NOTARIZATION" != "true" && "$APPLE_CERTIFICATE_CONFIGURED" == "true" && -n "${COMUX_NOTARY_APPLE_ID:-}" && -n "${COMUX_NOTARY_TEAM_ID:-}" && -n "${COMUX_NOTARY_PASSWORD:-}" ]]; then | |
| args+=(--submit-notarization) | |
| fi | |
| ./scripts/brew.sh "${args[@]}" | tee "$output_file" | |
| while IFS= read -r line; do | |
| case "$line" in | |
| archive=*|sha256=*|cask=*|notary_submission_id=*|notary_status=*|notary_state=*) | |
| echo "$line" >> "$GITHUB_OUTPUT" | |
| ;; | |
| esac | |
| done < "$output_file" | |
| - name: Prepare notarization artifact | |
| if: steps.package.outputs.notary_submission_id != '' && steps.package.outputs.archive == '' | |
| id: notarization_artifact | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| app_archive="$RUNNER_TEMP/comux-app.zip" | |
| state_path="$RUNNER_TEMP/comux-notarization.txt" | |
| ditto --norsrc -c -k --keepParent ".build/apple/comux.app" "$app_archive" | |
| cat > "$state_path" <<EOF | |
| VERSION=${{ steps.meta.outputs.version }} | |
| REPOSITORY=${RELEASE_REPOSITORY} | |
| SUBMISSION_ID=${{ steps.package.outputs.notary_submission_id }} | |
| STATUS=${{ steps.package.outputs.notary_status }} | |
| RUN_ID=${{ github.run_id }} | |
| EOF | |
| echo "app_archive=$app_archive" >> "$GITHUB_OUTPUT" | |
| echo "state=$state_path" >> "$GITHUB_OUTPUT" | |
| - name: Upload notarization artifact | |
| if: steps.notarization_artifact.outputs.app_archive != '' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ${{ steps.meta.outputs.notarization_artifact }} | |
| retention-days: 14 | |
| path: | | |
| ${{ steps.notarization_artifact.outputs.app_archive }} | |
| ${{ steps.notarization_artifact.outputs.state }} | |
| - name: Report pending notarization | |
| if: steps.package.outputs.notary_submission_id != '' && steps.package.outputs.archive == '' | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| { | |
| echo "## Apple notarization pending" | |
| echo | |
| echo "Submission ID: \`${{ steps.package.outputs.notary_submission_id }}\`" | |
| echo "Status: \`${{ steps.package.outputs.notary_status }}\`" | |
| echo | |
| echo "After Apple reports \`Accepted\`, finalize this release with:" | |
| echo | |
| echo "\`\`\`bash" | |
| echo "gh workflow run Release -f version=${{ steps.meta.outputs.version }} -f notarization_submission_id=${{ steps.package.outputs.notary_submission_id }} -f notarization_run_id=${{ github.run_id }}" | |
| echo "\`\`\`" | |
| echo | |
| echo "The GitHub release remains a draft until finalization uploads \`comux-${{ steps.meta.outputs.version }}.zip\` and \`comux.rb\`." | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| - name: Upload release assets | |
| if: steps.prebuilt.outputs.has_prebuilt != 'true' && steps.package.outputs.archive != '' && steps.package.outputs.cask != '' | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| gh release upload "${{ steps.meta.outputs.tag }}" \ | |
| "${{ steps.package.outputs.archive }}" \ | |
| "${{ steps.package.outputs.cask }}" \ | |
| --clobber | |
| - name: Publish draft release | |
| if: steps.prebuilt.outputs.has_prebuilt == 'true' || (steps.package.outputs.archive != '' && steps.package.outputs.cask != '') | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| gh release edit "${{ steps.meta.outputs.tag }}" \ | |
| --repo "$RELEASE_REPOSITORY" \ | |
| --draft=false \ | |
| --latest | |
| - name: Upload workflow artifacts | |
| if: steps.prebuilt.outputs.has_prebuilt != 'true' && steps.package.outputs.archive != '' && steps.package.outputs.cask != '' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: comux-release-${{ steps.meta.outputs.version }} | |
| path: | | |
| ${{ steps.package.outputs.archive }} | |
| ${{ steps.package.outputs.cask }} | |
| - name: Publish cask to tap | |
| if: vars.HOMEBREW_TAP_REPOSITORY != '' && (steps.prebuilt.outputs.has_prebuilt == 'true' || steps.package.outputs.cask != '') | |
| env: | |
| HOMEBREW_TAP_REPOSITORY: ${{ vars.HOMEBREW_TAP_REPOSITORY }} | |
| HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }} | |
| CASK_PATH: ${{ steps.prebuilt.outputs.has_prebuilt == 'true' && steps.prebuilt.outputs.cask || steps.package.outputs.cask }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| if [[ -z "${HOMEBREW_TAP_TOKEN:-}" ]]; then | |
| echo "HOMEBREW_TAP_TOKEN is not configured; skipping tap update." | |
| exit 0 | |
| fi | |
| tap_dir="$(mktemp -d)" | |
| trap 'rm -rf "$tap_dir"' EXIT | |
| git clone "https://x-access-token:${HOMEBREW_TAP_TOKEN}@github.com/${HOMEBREW_TAP_REPOSITORY}.git" "$tap_dir" | |
| mkdir -p "$tap_dir/Casks" | |
| cp "$CASK_PATH" "$tap_dir/Casks/comux.rb" | |
| git -C "$tap_dir" config user.name "github-actions[bot]" | |
| git -C "$tap_dir" config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| if git -C "$tap_dir" diff --quiet -- Casks/comux.rb; then | |
| echo "Tap is already up to date." | |
| exit 0 | |
| fi | |
| git -C "$tap_dir" add Casks/comux.rb | |
| git -C "$tap_dir" commit -m "chore: update comux to v${{ steps.meta.outputs.version }}" | |
| git -C "$tap_dir" push origin HEAD |