From 740ec8087a568c406e0de97ac103c8129b223fb1 Mon Sep 17 00:00:00 2001 From: grohith327 Date: Wed, 15 Jul 2026 20:18:55 -0700 Subject: [PATCH 1/6] add PR workflow --- .github/workflows/pr-archive.yml | 205 +++++++++++++++++++++++++++++++ 1 file changed, 205 insertions(+) create mode 100644 .github/workflows/pr-archive.yml diff --git a/.github/workflows/pr-archive.yml b/.github/workflows/pr-archive.yml new file mode 100644 index 00000000..22fbd4be --- /dev/null +++ b/.github/workflows/pr-archive.yml @@ -0,0 +1,205 @@ +name: PR Archive + +on: + pull_request_target: + branches: [main] + types: [opened, synchronize, reopened, ready_for_review] + +permissions: + contents: read + +concurrency: + group: pr-archive-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + archive: + name: "Archive FluidVoice PR #${{ github.event.pull_request.number }}" + if: github.event.pull_request.draft == false + runs-on: macos-latest + timeout-minutes: 45 + environment: + name: ${{ contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.pull_request.author_association) && 'pr-archive-auto' || 'pr-archive-approval' }} + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + AUTHOR_ASSOCIATION: ${{ github.event.pull_request.author_association }} + PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} + PR_MERGE_SHA: ${{ github.event.pull_request.merge_commit_sha }} + APP_NAME: "FluidVoice #${{ github.event.pull_request.number }}" + BUNDLE_IDENTIFIER: com.FluidApp.app.pr${{ github.event.pull_request.number }} + + steps: + - name: Verify outside-contributor approval gate + if: ${{ !contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.pull_request.author_association) }} + env: + APPROVAL_GATE_CONFIGURED: ${{ vars.PR_ARCHIVE_APPROVAL_CONFIGURED }} + run: | + if [[ "$APPROVAL_GATE_CONFIGURED" != "true" ]]; then + echo "The pr-archive-approval environment is not fully configured." >&2 + echo "Add the repository owner and write-access collaborators as" >&2 + echo "required reviewers, then set the environment's" >&2 + echo "PR_ARCHIVE_APPROVAL_CONFIGURED environment variable to true." >&2 + exit 1 + fi + + - name: Checkout PR merge commit + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.merge_commit_sha }} + fetch-depth: 1 + persist-credentials: false + + - name: Set up Xcode + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: latest-stable + + - name: Show Xcode version + run: xcodebuild -version + + - name: Archive unsigned PR build + env: + ARCHIVE_PATH: ${{ runner.temp }}/FluidVoice-PR-${{ github.event.pull_request.number }}.xcarchive + run: | + set -euo pipefail + + xcodebuild archive \ + -project Fluid.xcodeproj \ + -scheme Fluid \ + -configuration Release \ + -destination 'generic/platform=macOS' \ + -archivePath "$ARCHIVE_PATH" \ + CODE_SIGN_IDENTITY="" \ + CODE_SIGNING_REQUIRED=NO \ + CODE_SIGNING_ALLOWED=NO + + - name: Validate and package archive + id: package + env: + ARCHIVE_PATH: ${{ runner.temp }}/FluidVoice-PR-${{ github.event.pull_request.number }}.xcarchive + STAGING_PATH: ${{ runner.temp }}/FluidVoice-PR-${{ github.event.pull_request.number }} + run: | + set -euo pipefail + + ORIGINAL_APP_PATH="$ARCHIVE_PATH/Products/Applications/FluidVoice.app" + APP_PATH="$ARCHIVE_PATH/Products/Applications/$APP_NAME.app" + EXECUTABLE_PATH="$APP_PATH/Contents/MacOS/FluidVoice" + INFO_PLIST="$APP_PATH/Contents/Info.plist" + ARCHIVE_INFO_PLIST="$ARCHIVE_PATH/Info.plist" + MANIFEST_PATH="$STAGING_PATH/build-manifest.json" + MANIFEST_PLIST="$STAGING_PATH/build-manifest.plist" + README_PATH="$STAGING_PATH/README.txt" + ARCHIVE_ZIP="$STAGING_PATH/FluidVoice-PR-$PR_NUMBER.xcarchive.zip" + APP_ZIP="$STAGING_PATH/FluidVoice-PR-$PR_NUMBER.app.zip" + VERIFY_PATH="$RUNNER_TEMP/FluidVoice-PR-$PR_NUMBER-verify" + SHORT_SHA="${PR_HEAD_SHA:0:12}" + ARTIFACT_NAME="FluidVoice-PR-$PR_NUMBER-$SHORT_SHA" + + test -d "$ARCHIVE_PATH" + test -d "$ORIGINAL_APP_PATH" + test -f "$ARCHIVE_INFO_PLIST" + + mv "$ORIGINAL_APP_PATH" "$APP_PATH" + /usr/libexec/PlistBuddy -c "Set :CFBundleIdentifier $BUNDLE_IDENTIFIER" "$INFO_PLIST" + /usr/libexec/PlistBuddy -c "Set :CFBundleName $APP_NAME" "$INFO_PLIST" + /usr/libexec/PlistBuddy -c "Set :CFBundleDisplayName $APP_NAME" "$INFO_PLIST" + /usr/libexec/PlistBuddy -c "Set :Name $APP_NAME" "$ARCHIVE_INFO_PLIST" + /usr/libexec/PlistBuddy -c "Set :ApplicationProperties:ApplicationPath Applications/$APP_NAME.app" "$ARCHIVE_INFO_PLIST" + /usr/libexec/PlistBuddy -c "Set :ApplicationProperties:CFBundleIdentifier $BUNDLE_IDENTIFIER" "$ARCHIVE_INFO_PLIST" + + test -d "$APP_PATH" + test -f "$INFO_PLIST" + test -x "$EXECUTABLE_PATH" + + ACTUAL_BUNDLE_IDENTIFIER=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' "$INFO_PLIST") + if [[ "$ACTUAL_BUNDLE_IDENTIFIER" != "$BUNDLE_IDENTIFIER" ]]; then + echo "Expected bundle identifier '$BUNDLE_IDENTIFIER', found '$ACTUAL_BUNDLE_IDENTIFIER'." >&2 + exit 1 + fi + + SIGNATURE_DETAILS=$(codesign -dv --verbose=4 "$APP_PATH" 2>&1 || true) + if grep -q '^Authority=' <<< "$SIGNATURE_DETAILS"; then + echo "Expected no signing authority on the PR app." >&2 + exit 1 + fi + if grep -q '^TeamIdentifier=' <<< "$SIGNATURE_DETAILS" && \ + ! grep -q '^TeamIdentifier=not set$' <<< "$SIGNATURE_DETAILS"; then + echo "Expected no signing team on the PR app." >&2 + exit 1 + fi + + rm -rf "$STAGING_PATH" "$VERIFY_PATH" + mkdir -p "$STAGING_PATH" "$VERIFY_PATH" + + XCODE_VERSION=$(xcodebuild -version | tr '\n' ' ' | xargs) + BUILD_TIMESTAMP=$(date -u +'%Y-%m-%dT%H:%M:%SZ') + + plutil -create xml1 "$MANIFEST_PLIST" + plutil -insert pr_number -integer "$PR_NUMBER" "$MANIFEST_PLIST" + plutil -insert author_association -string "$AUTHOR_ASSOCIATION" "$MANIFEST_PLIST" + plutil -insert pr_head_sha -string "$PR_HEAD_SHA" "$MANIFEST_PLIST" + plutil -insert pr_merge_sha -string "$PR_MERGE_SHA" "$MANIFEST_PLIST" + plutil -insert xcode_version -string "$XCODE_VERSION" "$MANIFEST_PLIST" + plutil -insert app_name -string "$APP_NAME" "$MANIFEST_PLIST" + plutil -insert bundle_identifier -string "$BUNDLE_IDENTIFIER" "$MANIFEST_PLIST" + plutil -insert build_timestamp -string "$BUILD_TIMESTAMP" "$MANIFEST_PLIST" + plutil -convert json -o "$MANIFEST_PATH" "$MANIFEST_PLIST" + rm "$MANIFEST_PLIST" + + cat > "$README_PATH" < Privacy & Security if macOS offers that option. + + For trusted builds only, you can remove the downloaded quarantine marker: + + xattr -dr com.apple.quarantine "/path/to/$APP_NAME.app" + + Unsigned builds may not support signature-dependent features such as + Login Items. This PR build has a separate bundle identifier, so macOS + permissions and preferences are separate from the release app. + EOF + + ditto -c -k --sequesterRsrc --keepParent "$ARCHIVE_PATH" "$ARCHIVE_ZIP" + ditto -c -k --sequesterRsrc --keepParent "$APP_PATH" "$APP_ZIP" + + test -s "$MANIFEST_PATH" + test -s "$README_PATH" + test -s "$ARCHIVE_ZIP" + test -s "$APP_ZIP" + unzip -tq "$ARCHIVE_ZIP" + unzip -tq "$APP_ZIP" + + ditto -x -k "$APP_ZIP" "$VERIFY_PATH" + test -x "$VERIFY_PATH/$APP_NAME.app/Contents/MacOS/FluidVoice" + + echo "artifact-name=$ARTIFACT_NAME" >> "$GITHUB_OUTPUT" + echo "artifact-path=$STAGING_PATH" >> "$GITHUB_OUTPUT" + + { + echo "### PR archive ready" + echo + echo "- Artifact: \`$ARTIFACT_NAME\`" + echo "- App: \`$APP_NAME.app\`" + echo "- Bundle identifier: \`$BUNDLE_IDENTIFIER\`" + echo "- Signing: unsigned" + } >> "$GITHUB_STEP_SUMMARY" + + - name: Upload PR archive artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ steps.package.outputs.artifact-name }} + path: ${{ steps.package.outputs.artifact-path }} + if-no-files-found: error + retention-days: 14 + compression-level: 0 From 8b3564a261779546efd8da3f0d23d0648e01b7d2 Mon Sep 17 00:00:00 2001 From: grohith327 Date: Wed, 15 Jul 2026 20:29:24 -0700 Subject: [PATCH 2/6] address some security issues --- .github/workflows/pr-archive.yml | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/.github/workflows/pr-archive.yml b/.github/workflows/pr-archive.yml index 22fbd4be..bbb7f1a8 100644 --- a/.github/workflows/pr-archive.yml +++ b/.github/workflows/pr-archive.yml @@ -1,7 +1,9 @@ name: PR Archive on: - pull_request_target: + # Repository Settings > Actions must require approval for all external + # contributors. This keeps fork PR builds read-only and secretless. + pull_request: branches: [main] types: [opened, synchronize, reopened, ready_for_review] @@ -18,8 +20,6 @@ jobs: if: github.event.pull_request.draft == false runs-on: macos-latest timeout-minutes: 45 - environment: - name: ${{ contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.pull_request.author_association) && 'pr-archive-auto' || 'pr-archive-approval' }} env: PR_NUMBER: ${{ github.event.pull_request.number }} AUTHOR_ASSOCIATION: ${{ github.event.pull_request.author_association }} @@ -29,28 +29,23 @@ jobs: BUNDLE_IDENTIFIER: com.FluidApp.app.pr${{ github.event.pull_request.number }} steps: - - name: Verify outside-contributor approval gate - if: ${{ !contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.pull_request.author_association) }} - env: - APPROVAL_GATE_CONFIGURED: ${{ vars.PR_ARCHIVE_APPROVAL_CONFIGURED }} + - name: Validate PR merge commit run: | - if [[ "$APPROVAL_GATE_CONFIGURED" != "true" ]]; then - echo "The pr-archive-approval environment is not fully configured." >&2 - echo "Add the repository owner and write-access collaborators as" >&2 - echo "required reviewers, then set the environment's" >&2 - echo "PR_ARCHIVE_APPROVAL_CONFIGURED environment variable to true." >&2 + if [[ ! "$PR_MERGE_SHA" =~ ^[0-9a-f]{40}$ ]]; then + echo "GitHub has not produced a merge commit for PR #$PR_NUMBER." >&2 + echo "Resolve merge conflicts or rerun the workflow after GitHub computes it." >&2 exit 1 fi - name: Checkout PR merge commit - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: ref: ${{ github.event.pull_request.merge_commit_sha }} fetch-depth: 1 persist-credentials: false - name: Set up Xcode - uses: maxim-lobanov/setup-xcode@v1 + uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1 with: xcode-version: latest-stable @@ -196,7 +191,7 @@ jobs: } >> "$GITHUB_STEP_SUMMARY" - name: Upload PR archive artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: ${{ steps.package.outputs.artifact-name }} path: ${{ steps.package.outputs.artifact-path }} From 2871d7ad54a3b62b0c526a7a456f1fed9fe15a5d Mon Sep 17 00:00:00 2001 From: grohith327 Date: Wed, 15 Jul 2026 20:48:24 -0700 Subject: [PATCH 3/6] Add an embed framework phase --- .github/workflows/pr-archive-comment.yml | 99 ++++++++++++++++++++++++ .github/workflows/pr-archive.yml | 17 +++- Fluid.xcodeproj/project.pbxproj | 16 ++++ 3 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/pr-archive-comment.yml diff --git a/.github/workflows/pr-archive-comment.yml b/.github/workflows/pr-archive-comment.yml new file mode 100644 index 00000000..c44cb47f --- /dev/null +++ b/.github/workflows/pr-archive-comment.yml @@ -0,0 +1,99 @@ +name: PR Archive Comment + +on: + workflow_run: + workflows: ["PR Archive"] + types: [completed] + +permissions: + actions: read + issues: write + pull-requests: read + +jobs: + comment: + name: Add PR archive download link + if: github.event.workflow_run.conclusion == 'success' + runs-on: ubuntu-latest + timeout-minutes: 5 + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + RUN_ID: ${{ github.event.workflow_run.id }} + EVENT_PR_NUMBER: ${{ github.event.workflow_run.pull_requests[0].number }} + + steps: + # This trusted workflow reads artifact metadata only. Never download or + # execute artifacts produced by the untrusted pull_request workflow. + - name: Post archive download link + run: | + set -euo pipefail + + if [[ ! "$EVENT_PR_NUMBER" =~ ^[0-9]+$ ]]; then + echo "The completed workflow run is not associated with a pull request." >&2 + exit 1 + fi + + ARTIFACT=$( + gh api "repos/$GITHUB_REPOSITORY/actions/runs/$RUN_ID/artifacts" \ + --jq '.artifacts + | map(select((.expired == false) and (.name | startswith("FluidVoice-PR-")))) + | sort_by(.created_at) + | last + | [.id, .name] + | @tsv' + ) + IFS=$'\t' read -r ARTIFACT_ID ARTIFACT_NAME <<< "$ARTIFACT" + + EXPECTED_PREFIX="FluidVoice-PR-$EVENT_PR_NUMBER-" + if [[ ! "$ARTIFACT_ID" =~ ^[0-9]+$ ]]; then + echo "No valid archive artifact was found for PR #$EVENT_PR_NUMBER." >&2 + exit 1 + fi + if [[ "$ARTIFACT_NAME" =~ ^${EXPECTED_PREFIX}([0-9a-f]{12})$ ]]; then + ARTIFACT_SHORT_SHA="${BASH_REMATCH[1]}" + else + echo "The archive artifact name does not match PR #$EVENT_PR_NUMBER." >&2 + exit 1 + fi + + CURRENT_HEAD_SHA=$( + gh api "repos/$GITHUB_REPOSITORY/pulls/$EVENT_PR_NUMBER" --jq '.head.sha' + ) + if [[ "${CURRENT_HEAD_SHA:0:12}" != "$ARTIFACT_SHORT_SHA" ]]; then + echo "Skipping a superseded artifact for PR #$EVENT_PR_NUMBER." + exit 0 + fi + + ARTIFACT_URL="https://github.com/$GITHUB_REPOSITORY/actions/runs/$RUN_ID/artifacts/$ARTIFACT_ID" + RUN_URL="https://github.com/$GITHUB_REPOSITORY/actions/runs/$RUN_ID" + MARKER='' + BODY=$(cat <")) | .id' \ + | sed -n '1p' + ) + + if [[ -n "$COMMENT_ID" ]]; then + gh api --method PATCH \ + "repos/$GITHUB_REPOSITORY/issues/comments/$COMMENT_ID" \ + -f body="$BODY" \ + >/dev/null + else + gh api --method POST \ + "repos/$GITHUB_REPOSITORY/issues/$EVENT_PR_NUMBER/comments" \ + -f body="$BODY" \ + >/dev/null + fi diff --git a/.github/workflows/pr-archive.yml b/.github/workflows/pr-archive.yml index bbb7f1a8..918c1b5e 100644 --- a/.github/workflows/pr-archive.yml +++ b/.github/workflows/pr-archive.yml @@ -105,6 +105,21 @@ jobs: test -d "$APP_PATH" test -f "$INFO_PLIST" test -x "$EXECUTABLE_PATH" + test -x "$APP_PATH/Contents/Frameworks/MediaRemoteAdapter.framework/Versions/A/MediaRemoteAdapter" + + RPATH_DEPENDENCY_COUNT=0 + while IFS= read -r DEPENDENCY; do + RPATH_DEPENDENCY_COUNT=$((RPATH_DEPENDENCY_COUNT + 1)) + EMBEDDED_PATH="$APP_PATH/Contents/Frameworks/${DEPENDENCY#@rpath/}" + if [[ ! -e "$EMBEDDED_PATH" ]]; then + echo "Missing embedded runtime dependency: $DEPENDENCY" >&2 + exit 1 + fi + done < <(otool -L "$EXECUTABLE_PATH" | awk '$1 ~ /^@rpath\// { print $1 }') + if [[ "$RPATH_DEPENDENCY_COUNT" -eq 0 ]]; then + echo "Expected at least one bundled @rpath dependency." >&2 + exit 1 + fi ACTUAL_BUNDLE_IDENTIFIER=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' "$INFO_PLIST") if [[ "$ACTUAL_BUNDLE_IDENTIFIER" != "$BUNDLE_IDENTIFIER" ]]; then @@ -196,5 +211,5 @@ jobs: name: ${{ steps.package.outputs.artifact-name }} path: ${{ steps.package.outputs.artifact-path }} if-no-files-found: error - retention-days: 14 + retention-days: 5 compression-level: 0 diff --git a/Fluid.xcodeproj/project.pbxproj b/Fluid.xcodeproj/project.pbxproj index a2cb0a9f..b8c8c879 100644 --- a/Fluid.xcodeproj/project.pbxproj +++ b/Fluid.xcodeproj/project.pbxproj @@ -10,6 +10,7 @@ 7C078DA52E3B386A00FB7CAC /* FluidAudio in Frameworks */ = {isa = PBXBuildFile; productRef = 7C078DA42E3B386A00FB7CAC /* FluidAudio */; }; 7C3697892ED70F9C005874CE /* DynamicNotchKit in Frameworks */ = {isa = PBXBuildFile; productRef = 7C3697882ED70F9C005874CE /* DynamicNotchKit */; }; 7C5AF14B2F15041600DE21B0 /* MediaRemoteAdapter in Frameworks */ = {isa = PBXBuildFile; productRef = 7C5AF14A2F15041600DE21B0 /* MediaRemoteAdapter */; }; + 7C5AF14C2F15041600DE21B0 /* MediaRemoteAdapter in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 7C5AF14A2F15041600DE21B0 /* MediaRemoteAdapter */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 7C9A71022F58B00000FB7CAF /* TranscribeCpp in Frameworks */ = {isa = PBXBuildFile; productRef = 7C9A71012F58B00000FB7CAF /* TranscribeCpp */; }; 7C91B0012F42AA0100C0DEF0 /* HotkeyShortcutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C91B0022F42AA0100C0DEF0 /* HotkeyShortcutTests.swift */; }; 7CDB0A2D2F3C4D5600FB7CAD /* DictationE2ETests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CDB0A292F3C4D5600FB7CAD /* DictationE2ETests.swift */; }; @@ -31,6 +32,20 @@ }; /* End PBXContainerItemProxy section */ +/* Begin PBXCopyFilesBuildPhase section */ + 7C5AF14D2F15041600DE21B0 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + 7C5AF14C2F15041600DE21B0 /* MediaRemoteAdapter in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + /* Begin PBXFileReference section */ 7C078D8F2E3B339200FB7CAC /* FluidVoice Debug.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "FluidVoice Debug.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 7CDB0A202F3C4D5600FB7CAD /* FluidDictationIntegrationTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FluidDictationIntegrationTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -147,6 +162,7 @@ buildPhases = ( 7C078D8B2E3B339200FB7CAC /* Sources */, 7C078D8C2E3B339200FB7CAC /* Frameworks */, + 7C5AF14D2F15041600DE21B0 /* Embed Frameworks */, 7C078D8D2E3B339200FB7CAC /* Resources */, ); buildRules = ( From e29fd2c7a8510109e870a24aaee66540df1bae52 Mon Sep 17 00:00:00 2001 From: grohith327 Date: Wed, 15 Jul 2026 21:15:59 -0700 Subject: [PATCH 4/6] fix workflow failure --- .github/workflows/pr-archive.yml | 49 +++++++++++++++++++++++++------- 1 file changed, 39 insertions(+), 10 deletions(-) diff --git a/.github/workflows/pr-archive.yml b/.github/workflows/pr-archive.yml index 918c1b5e..a80b9c12 100644 --- a/.github/workflows/pr-archive.yml +++ b/.github/workflows/pr-archive.yml @@ -24,26 +24,55 @@ jobs: PR_NUMBER: ${{ github.event.pull_request.number }} AUTHOR_ASSOCIATION: ${{ github.event.pull_request.author_association }} PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} - PR_MERGE_SHA: ${{ github.event.pull_request.merge_commit_sha }} + PR_BASE_SHA: ${{ github.event.pull_request.base.sha }} APP_NAME: "FluidVoice #${{ github.event.pull_request.number }}" BUNDLE_IDENTIFIER: com.FluidApp.app.pr${{ github.event.pull_request.number }} steps: - - name: Validate PR merge commit + - name: Validate PR commits run: | - if [[ ! "$PR_MERGE_SHA" =~ ^[0-9a-f]{40}$ ]]; then - echo "GitHub has not produced a merge commit for PR #$PR_NUMBER." >&2 - echo "Resolve merge conflicts or rerun the workflow after GitHub computes it." >&2 - exit 1 - fi + set -euo pipefail - - name: Checkout PR merge commit + for commit in "$PR_HEAD_SHA" "$PR_BASE_SHA"; do + if [[ ! "$commit" =~ ^[0-9a-f]{40}$ ]]; then + echo "GitHub did not provide a valid PR head and base commit." >&2 + exit 1 + fi + done + + - name: Checkout exact PR head commit uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: - ref: ${{ github.event.pull_request.merge_commit_sha }} - fetch-depth: 1 + repository: ${{ github.event.pull_request.head.repo.full_name }} + ref: ${{ github.event.pull_request.head.sha }} + fetch-depth: 0 persist-credentials: false + - name: Merge target branch into PR head + run: | + set -euo pipefail + + CHECKED_OUT_SHA=$(git rev-parse HEAD) + if [[ "$CHECKED_OUT_SHA" != "$PR_HEAD_SHA" ]]; then + echo "Expected PR head $PR_HEAD_SHA, checked out $CHECKED_OUT_SHA." >&2 + exit 1 + fi + + git remote add upstream "https://github.com/$GITHUB_REPOSITORY.git" + git fetch --no-tags upstream "$PR_BASE_SHA" + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git merge --no-ff --no-edit "$PR_BASE_SHA" + + PR_MERGE_SHA=$(git rev-parse HEAD) + if ! git merge-base --is-ancestor "$PR_HEAD_SHA" "$PR_MERGE_SHA" || \ + ! git merge-base --is-ancestor "$PR_BASE_SHA" "$PR_MERGE_SHA"; then + echo "Generated merge commit does not contain the expected PR head and base." >&2 + exit 1 + fi + + echo "PR_MERGE_SHA=$PR_MERGE_SHA" >> "$GITHUB_ENV" + - name: Set up Xcode uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1 with: From a73959d48d6c1d9c3df567f1fe3c39d4c910eb92 Mon Sep 17 00:00:00 2001 From: grohith327 Date: Wed, 15 Jul 2026 22:50:18 -0700 Subject: [PATCH 5/6] Fix accessibility and microphone issues --- .github/workflows/pr-archive-comment.yml | 2 +- .github/workflows/pr-archive.yml | 87 +++++++++++++++++++++--- 2 files changed, 78 insertions(+), 11 deletions(-) diff --git a/.github/workflows/pr-archive-comment.yml b/.github/workflows/pr-archive-comment.yml index c44cb47f..fef7eef8 100644 --- a/.github/workflows/pr-archive-comment.yml +++ b/.github/workflows/pr-archive-comment.yml @@ -73,7 +73,7 @@ jobs: [Download \`$ARTIFACT_NAME\`]($ARTIFACT_URL) - The artifact contains the app ZIP, Xcode archive, build manifest, and unsigned-app instructions. It expires 5 days after the build. + The artifact contains the ad-hoc-signed app ZIP, Xcode archive, build manifest, and installation instructions. It expires 5 days after the build. [View workflow run]($RUN_URL) EOF diff --git a/.github/workflows/pr-archive.yml b/.github/workflows/pr-archive.yml index a80b9c12..7b1beb00 100644 --- a/.github/workflows/pr-archive.yml +++ b/.github/workflows/pr-archive.yml @@ -81,7 +81,7 @@ jobs: - name: Show Xcode version run: xcodebuild -version - - name: Archive unsigned PR build + - name: Archive PR build env: ARCHIVE_PATH: ${{ runner.temp }}/FluidVoice-PR-${{ github.event.pull_request.number }}.xcarchive run: | @@ -93,9 +93,11 @@ jobs: -configuration Release \ -destination 'generic/platform=macOS' \ -archivePath "$ARCHIVE_PATH" \ - CODE_SIGN_IDENTITY="" \ - CODE_SIGNING_REQUIRED=NO \ - CODE_SIGNING_ALLOWED=NO + CODE_SIGN_IDENTITY="-" \ + CODE_SIGNING_REQUIRED=YES \ + CODE_SIGNING_ALLOWED=YES \ + CODE_SIGN_STYLE=Manual \ + DEVELOPMENT_TEAM="" - name: Validate and package archive id: package @@ -116,6 +118,8 @@ jobs: ARCHIVE_ZIP="$STAGING_PATH/FluidVoice-PR-$PR_NUMBER.xcarchive.zip" APP_ZIP="$STAGING_PATH/FluidVoice-PR-$PR_NUMBER.app.zip" VERIFY_PATH="$RUNNER_TEMP/FluidVoice-PR-$PR_NUMBER-verify" + EFFECTIVE_ENTITLEMENTS="$RUNNER_TEMP/FluidVoice-PR-$PR_NUMBER.entitlements" + VERIFIED_ENTITLEMENTS="$RUNNER_TEMP/FluidVoice-PR-$PR_NUMBER-verified.entitlements" SHORT_SHA="${PR_HEAD_SHA:0:12}" ARTIFACT_NAME="FluidVoice-PR-$PR_NUMBER-$SHORT_SHA" @@ -123,6 +127,16 @@ jobs: test -d "$ORIGINAL_APP_PATH" test -f "$ARCHIVE_INFO_PLIST" + # Preserve Xcode's expanded entitlements, which combine Fluid.entitlements + # with target capabilities such as Hardened Runtime Audio Input. + codesign -d --entitlements - --xml "$ORIGINAL_APP_PATH" \ + > "$EFFECTIVE_ENTITLEMENTS" 2>/dev/null + plutil -lint "$EFFECTIVE_ENTITLEMENTS" + if [[ "$(plutil -extract 'com\.apple\.security\.device\.audio-input' raw -o - "$EFFECTIVE_ENTITLEMENTS")" != "true" ]]; then + echo "Xcode's effective entitlements are missing Audio Input." >&2 + exit 1 + fi + mv "$ORIGINAL_APP_PATH" "$APP_PATH" /usr/libexec/PlistBuddy -c "Set :CFBundleIdentifier $BUNDLE_IDENTIFIER" "$INFO_PLIST" /usr/libexec/PlistBuddy -c "Set :CFBundleName $APP_NAME" "$INFO_PLIST" @@ -131,10 +145,37 @@ jobs: /usr/libexec/PlistBuddy -c "Set :ApplicationProperties:ApplicationPath Applications/$APP_NAME.app" "$ARCHIVE_INFO_PLIST" /usr/libexec/PlistBuddy -c "Set :ApplicationProperties:CFBundleIdentifier $BUNDLE_IDENTIFIER" "$ARCHIVE_INFO_PLIST" + # Xcode's XCFramework copy flattens CTranscribe's version symlinks. + # Restore the canonical framework layout before replacing its stale + # upstream signature. + CTRANSCRIBE_FRAMEWORK="$APP_PATH/Contents/Frameworks/CTranscribe.framework" + test -d "$CTRANSCRIBE_FRAMEWORK/Versions/A" + rm -rf \ + "$CTRANSCRIBE_FRAMEWORK/Versions/Current" \ + "$CTRANSCRIBE_FRAMEWORK/CTranscribe" \ + "$CTRANSCRIBE_FRAMEWORK/Resources" \ + "$CTRANSCRIBE_FRAMEWORK/Versions/A/_CodeSignature" + ln -s A "$CTRANSCRIBE_FRAMEWORK/Versions/Current" + ln -s Versions/Current/CTranscribe "$CTRANSCRIBE_FRAMEWORK/CTranscribe" + ln -s Versions/Current/Resources "$CTRANSCRIBE_FRAMEWORK/Resources" + + # Ad-hoc signing requires no certificate or secret, but gives macOS + # TCC a valid code identity for permissions such as Accessibility. + codesign --force --sign - --timestamp=none "$CTRANSCRIBE_FRAMEWORK" + codesign --force --sign - --timestamp=none \ + "$APP_PATH/Contents/Frameworks/MediaRemoteAdapter.framework" + codesign --force --sign - --timestamp=none --options runtime \ + --entitlements "$EFFECTIVE_ENTITLEMENTS" \ + --identifier "$BUNDLE_IDENTIFIER" \ + "$APP_PATH" + test -d "$APP_PATH" test -f "$INFO_PLIST" test -x "$EXECUTABLE_PATH" test -x "$APP_PATH/Contents/Frameworks/MediaRemoteAdapter.framework/Versions/A/MediaRemoteAdapter" + test -L "$CTRANSCRIBE_FRAMEWORK/Versions/Current" + test -L "$CTRANSCRIBE_FRAMEWORK/CTranscribe" + test -L "$CTRANSCRIBE_FRAMEWORK/Resources" RPATH_DEPENDENCY_COUNT=0 while IFS= read -r DEPENDENCY; do @@ -156,7 +197,22 @@ jobs: exit 1 fi + codesign --verify --deep --strict --verbose=2 "$APP_PATH" + codesign -d --entitlements - --xml "$APP_PATH" \ + > "$VERIFIED_ENTITLEMENTS" 2>/dev/null + if [[ "$(plutil -extract 'com\.apple\.security\.device\.audio-input' raw -o - "$VERIFIED_ENTITLEMENTS")" != "true" ]]; then + echo "The signed PR app is missing the Audio Input entitlement." >&2 + exit 1 + fi SIGNATURE_DETAILS=$(codesign -dv --verbose=4 "$APP_PATH" 2>&1 || true) + if ! grep -q '^Signature=adhoc$' <<< "$SIGNATURE_DETAILS"; then + echo "Expected a valid ad-hoc signature on the PR app." >&2 + exit 1 + fi + if ! grep -q "^Identifier=$BUNDLE_IDENTIFIER$" <<< "$SIGNATURE_DETAILS"; then + echo "The code-signing identifier does not match '$BUNDLE_IDENTIFIER'." >&2 + exit 1 + fi if grep -q '^Authority=' <<< "$SIGNATURE_DETAILS"; then echo "Expected no signing authority on the PR app." >&2 exit 1 @@ -181,6 +237,8 @@ jobs: plutil -insert xcode_version -string "$XCODE_VERSION" "$MANIFEST_PLIST" plutil -insert app_name -string "$APP_NAME" "$MANIFEST_PLIST" plutil -insert bundle_identifier -string "$BUNDLE_IDENTIFIER" "$MANIFEST_PLIST" + plutil -insert signing -string ad-hoc "$MANIFEST_PLIST" + plutil -insert audio_input_entitlement -bool true "$MANIFEST_PLIST" plutil -insert build_timestamp -string "$BUILD_TIMESTAMP" "$MANIFEST_PLIST" plutil -convert json -o "$MANIFEST_PATH" "$MANIFEST_PLIST" rm "$MANIFEST_PLIST" @@ -189,14 +247,15 @@ jobs: FluidVoice PR #$PR_NUMBER ======================== - This is an unsigned test build from pull request #$PR_NUMBER. + This is an ad-hoc-signed test build from pull request #$PR_NUMBER. App name: $APP_NAME.app Bundle identifier: $BUNDLE_IDENTIFIER PR head SHA: $PR_HEAD_SHA PR merge SHA: $PR_MERGE_SHA - Because the app is unsigned, macOS may prevent it from opening normally. + Because the app is not Developer ID signed or notarized, macOS may prevent + it from opening normally. Try opening the app from Finder, then use Open Anyway under System Settings > Privacy & Security if macOS offers that option. @@ -204,8 +263,9 @@ jobs: xattr -dr com.apple.quarantine "/path/to/$APP_NAME.app" - Unsigned builds may not support signature-dependent features such as - Login Items. This PR build has a separate bundle identifier, so macOS + The ad-hoc signature provides a local code identity for permissions such + as Accessibility, but a new build may need to be removed and re-added in + Privacy & Security. This PR build has a separate bundle identifier, so permissions and preferences are separate from the release app. EOF @@ -220,7 +280,14 @@ jobs: unzip -tq "$APP_ZIP" ditto -x -k "$APP_ZIP" "$VERIFY_PATH" - test -x "$VERIFY_PATH/$APP_NAME.app/Contents/MacOS/FluidVoice" + VERIFIED_APP_PATH="$VERIFY_PATH/$APP_NAME.app" + test -x "$VERIFIED_APP_PATH/Contents/MacOS/FluidVoice" + test -L "$VERIFIED_APP_PATH/Contents/Frameworks/CTranscribe.framework/Versions/Current" + codesign --verify --deep --strict --verbose=2 "$VERIFIED_APP_PATH" + if [[ "$(codesign -d --entitlements - --xml "$VERIFIED_APP_PATH" 2>/dev/null | plutil -extract 'com\.apple\.security\.device\.audio-input' raw -o - -)" != "true" ]]; then + echo "The packaged PR app is missing the Audio Input entitlement." >&2 + exit 1 + fi echo "artifact-name=$ARTIFACT_NAME" >> "$GITHUB_OUTPUT" echo "artifact-path=$STAGING_PATH" >> "$GITHUB_OUTPUT" @@ -231,7 +298,7 @@ jobs: echo "- Artifact: \`$ARTIFACT_NAME\`" echo "- App: \`$APP_NAME.app\`" echo "- Bundle identifier: \`$BUNDLE_IDENTIFIER\`" - echo "- Signing: unsigned" + echo "- Signing: ad-hoc (no certificate required)" } >> "$GITHUB_STEP_SUMMARY" - name: Upload PR archive artifact From f9224ecd83a3041e5222f588459667cc48455104 Mon Sep 17 00:00:00 2001 From: grohith327 Date: Wed, 15 Jul 2026 23:04:07 -0700 Subject: [PATCH 6/6] Update comment instruction --- .github/workflows/pr-archive-comment.yml | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pr-archive-comment.yml b/.github/workflows/pr-archive-comment.yml index fef7eef8..89584c29 100644 --- a/.github/workflows/pr-archive-comment.yml +++ b/.github/workflows/pr-archive-comment.yml @@ -66,15 +66,29 @@ jobs: ARTIFACT_URL="https://github.com/$GITHUB_REPOSITORY/actions/runs/$RUN_ID/artifacts/$ARTIFACT_ID" RUN_URL="https://github.com/$GITHUB_REPOSITORY/actions/runs/$RUN_ID" + APP_NAME="FluidVoice #$EVENT_PR_NUMBER" MARKER='' BODY=$(cat <