Skip to content

Commit 825d4f9

Browse files
alexkromanclaudealexkroman-assembly
authored
Remove per-PR dev builds; split debug/release bundle IDs (#136)
* Fix dev build stranding the wizard on Accessibility Installing a local dev build over a released Blurt left the Accessibility step unpassable: the row stayed switched on in System Settings while AXIsProcessTrusted() kept returning false, so there was no way forward. TCC pins an Accessibility grant to the app's designated requirement. A release carries codesign's default Developer ID requirement (leaf CN plus the Developer ID marker OIDs); dev-build.sh signs with Apple Development and the project.yml install stamps an explicit team-based requirement. The two share a bundle id, an install path and a signing team, and still pin different requirements — so the release's grant is orphaned the moment a dev build replaces it. The self-heal that exists for exactly this (runAccessibilityGrantMigration) never fired, because it recorded the Team ID as the identity and the team is the one thing that does not change here. Record the designated requirement itself instead, serialized and namespaced as "dr:<requirement>": it is what tccd stores and re-checks, so it moves when and only when the stored copy stops matching. That subsumes the cases the Team ID and cdhash shapes covered (successive ad-hoc per-PR builds, a switched team) and adds the two they missed: certificate kind, and a rotated Developer ID cert orphaning every installed user's grant. Read through SecCodeCopyDesignatedRequirement rather than the signing-info dictionary's kSecCodeInfoDesignatedRequirement key, which would need a cast from Any back to a CoreFoundation type. The team identifier stays as the probe for "is this signature ad-hoc" — the UI-test carve-out — but is no longer the recorded value. The recorded value's shape changes, which re-reads as one identity change per install. That is safe by construction: decide() only resets when the app is untrusted, so a working grant is merely re-recorded. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016qVXH7zUbK1TTEZWmEAEXS * Give dev builds their own bundle id; drop the per-PR dev build Two changes that reinforce each other, plus what they make dead. **Separate identities.** Every debug configuration now builds as dev.alex.blurt.dev / "Blurt Dev" and installs to /Applications/Blurt Dev.app; only Release is dev.alex.blurt / "Blurt". A dev build therefore sits beside a released Blurt instead of replacing it — two sets of Privacy & Security rows, two defaults domains, either one runnable. That removes the collision at the source rather than healing it after the fact: TCC keys a permission on bundle id plus designated requirement, and the two builds can never pin the same requirement (Developer ID's default names the leaf cert; the dev install stamps an explicit team-based one), so sharing an id meant inheriting rows the new binary could not satisfy. PRODUCT_NAME stays Blurt in both — it names the executable and the built product, which every script and DerivedData path is written against. The debug id is the default and Release opts in to the shipping one, so a configuration added later cannot accidentally ship under it. The in-app tccutil reset now targets Bundle.main.bundleIdentifier rather than BlurtIdentity.subsystem, which would have had a dev build clearing the released app's grant. **No per-PR dev build.** Removed the dev-build and dev-build-link jobs, pr-dev-build.yml, and pr-dev-build-comment.sh. A hosted PR runner can reach no signing key — the Apple Development cert is per-developer and the Developer ID key is scoped to release.yml's protected environment — so the artifact was ad-hoc signed, which pins the cdhash and makes every rebuild a different app to tccd. Reviewers landed on a Blurt row switched on and still denied. Reviewers build locally instead. **Dead code that follows.** With the ad-hoc artifact gone, an ad-hoc signature is no longer an identity worth recording, so SigningIdentity.current() drops its includingAdHoc parameter and answers nil for ad-hoc and unsigned code. That deletes AppDelegate's adHocCountsAsIdentity and its #if UITEST_HOOKS: the property that kept uitest.sh and check.sh from wiping the developer's own grant is now structural rather than a flag the call site had to pass correctly. The migration's remaining job is the one nothing else can cover — a re-issued Developer ID certificate orphaning every installed user's grant at once — and the tests say so. Signing's Security handshake is now covered against /bin/ls, whose designated requirement is known and readable on any Mac; the test host's own signature varies by how the suite was launched and could not assert it. NOTE: Blurt.xcodeproj/project.pbxproj still needs `xcodegen generate` on a Mac — the generator does not run on Linux, and check.sh's drift check will fail until the regenerated project is committed. Info.plist's two changed values are written here by hand to match what the generator emits. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016qVXH7zUbK1TTEZWmEAEXS * Narrow reset-install's LaunchServices sweep to Blurt's own bundles The widened `Blurt[^/]*\.app` pattern matched BlurtUITests-Runner.app too, so a reset unregistered the XCUITest runner — a bundle this script does not own and does not re-register afterwards. Match exactly "Blurt.app" and "Blurt Dev.app" via an optional " Dev" group, spelled as a POSIX BRE interval so BSD and GNU sed read it the same way. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016qVXH7zUbK1TTEZWmEAEXS * Regenerate project.pbxproj for the dev bundle-id split d3fd355 changed project.yml (separate dev bundle id, BLURT_APP_NAME, the "Blurt Dev.app" install destination) but left the checked-in pbxproj stale, so check.sh's xcodegen-drift step would fail and an Xcode-driven build would still install over /Applications/Blurt.app under the release bundle id. Purely generated output — `xcodegen generate` reproduces this file exactly. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Alex Kroman <alex@assemblyai.com>
1 parent d8491e2 commit 825d4f9

18 files changed

Lines changed: 387 additions & 585 deletions

.github/workflows/check.yml

Lines changed: 0 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -213,123 +213,6 @@ jobs:
213213
if-no-files-found: error
214214
retention-days: 14
215215

216-
# Every PR gets an installable build of the change, so a reviewer can try it
217-
# instead of reading the diff and imagining it. Deliberately NOT part of the
218-
# required gate (`gate` below ignores it): it builds the same sources `check`
219-
# already compiles, so a failure here is never the only signal.
220-
#
221-
# The download link is posted by `dev-build-link` below for same-repo PRs, and
222-
# by the `pr-dev-build` workflow for fork PRs — a pull_request run from a fork
223-
# gets a read-only token no matter what `permissions:` says, so that case has
224-
# to happen in a workflow_run job. Both post the same marked comment, so
225-
# whichever runs, a PR ends up with exactly one link.
226-
# `needs: compile` so a PR whose sources don't build doesn't also pay for a
227-
# second macOS job to discover that. It costs the ~2 minutes `compile` takes on
228-
# a green run, which is cheap against a 30-minute timeout spent on code that was
229-
# never going to link.
230-
dev-build:
231-
needs: [changes, compile]
232-
if: github.event_name == 'pull_request' && needs.changes.outputs.code == 'true'
233-
runs-on: macos-26
234-
timeout-minutes: 30
235-
outputs:
236-
artifact-url: ${{ steps.upload.outputs.artifact-url }}
237-
238-
steps:
239-
- name: Checkout blurt
240-
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
241-
with:
242-
path: blurt
243-
persist-credentials: false
244-
245-
- name: Build Blurt.app (Debug-Local, ad-hoc signed)
246-
working-directory: blurt/App/Blurt
247-
# Debug-Local is the configuration scripts/dev-build.sh uses: a debug
248-
# build with UITEST_HOOKS off, so this is the real app rather than the
249-
# test-harness variant. The committed .xcodeproj is used as-is — check's
250-
# xcodegen drift check is what guarantees it matches project.yml.
251-
#
252-
# Ad-hoc signing (identity "-", signing ALLOWED), the same recipe
253-
# uitest.sh uses: the Developer ID cert isn't on this runner, and the
254-
# "Install to /Applications" post-build script skips itself under "-".
255-
run: |
256-
xcodebuild \
257-
-project Blurt.xcodeproj \
258-
-scheme Blurt \
259-
-configuration Debug-Local \
260-
-destination 'platform=macOS,arch=arm64' \
261-
-derivedDataPath "$RUNNER_TEMP/devbuild" \
262-
CODE_SIGN_IDENTITY="-" \
263-
CODE_SIGNING_REQUIRED=NO \
264-
CODE_SIGNING_ALLOWED=YES \
265-
build
266-
267-
- name: Package the app
268-
env:
269-
SHA: ${{ github.event.pull_request.head.sha }}
270-
run: |
271-
set -euo pipefail
272-
app="$RUNNER_TEMP/devbuild/Build/Products/Debug-Local/Blurt.app"
273-
[ -d "$app" ] || { echo "::error::no app at $app"; exit 1; }
274-
mkdir -p "$RUNNER_TEMP/out"
275-
# ditto, not zip: it preserves the bundle layout, symlinks, and the
276-
# executable bit the way macOS expects when the zip is expanded again.
277-
ditto -c -k --keepParent "$app" "$RUNNER_TEMP/out/Blurt-dev-${SHA:0:7}.zip"
278-
279-
- name: Upload the dev build
280-
id: upload
281-
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
282-
with:
283-
# The PR number in the name is how pr-dev-build.yml finds the PR.
284-
name: blurt-dev-build-pr-${{ github.event.pull_request.number }}
285-
path: ${{ runner.temp }}/out/*.zip
286-
if-no-files-found: error
287-
# A dev build is only interesting while the PR is open.
288-
retention-days: 14
289-
290-
- name: Summarize
291-
working-directory: blurt
292-
env:
293-
REPO: ${{ github.repository }}
294-
ARTIFACT_NAME: blurt-dev-build-pr-${{ github.event.pull_request.number }}
295-
ARTIFACT_URL: ${{ steps.upload.outputs.artifact-url }}
296-
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
297-
run: scripts/pr-dev-build-comment.sh --render >>"$GITHUB_STEP_SUMMARY"
298-
299-
# Post the link on same-repo PRs. A separate job on purpose: `dev-build` has
300-
# checked out and *built* PR code (project.yml runs post-build scripts), and a
301-
# job that does that should not also hold a writable token. This one holds the
302-
# token and touches nothing but the comment API.
303-
#
304-
# Fork PRs are excluded because their token is read-only regardless of the
305-
# permissions block — the API call would just 403. `pr-dev-build.yml` covers
306-
# them, posting the identical marked comment.
307-
dev-build-link:
308-
needs: dev-build
309-
if: github.event.pull_request.head.repo.full_name == github.repository
310-
runs-on: ubuntu-latest
311-
permissions:
312-
contents: read
313-
pull-requests: write
314-
315-
steps:
316-
- name: Checkout blurt
317-
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
318-
with:
319-
path: blurt
320-
persist-credentials: false
321-
322-
- name: Comment the dev build link
323-
working-directory: blurt
324-
env:
325-
GH_TOKEN: ${{ github.token }}
326-
REPO: ${{ github.repository }}
327-
PR: ${{ github.event.pull_request.number }}
328-
ARTIFACT_NAME: blurt-dev-build-pr-${{ github.event.pull_request.number }}
329-
ARTIFACT_URL: ${{ needs.dev-build.outputs.artifact-url }}
330-
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
331-
run: scripts/pr-dev-build-comment.sh --post
332-
333216
# Always-runs summary job, so a *skipped* `check` can't be mistaken for a pass.
334217
# GitHub reports a skipped required check to branch protection as successful, so
335218
# anything that makes `check` skip — the `changes` filter erroring, a `needs`

.github/workflows/pr-dev-build.yml

Lines changed: 0 additions & 104 deletions
This file was deleted.

0 commit comments

Comments
 (0)