Skip to content

Release

Release #21

Workflow file for this run

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