diff --git a/.github/claude-review-prompt.md b/.github/claude-review-prompt.md deleted file mode 100644 index 496820a8f..000000000 --- a/.github/claude-review-prompt.md +++ /dev/null @@ -1,63 +0,0 @@ -You are an experienced senior React Native engineer reviewing a pull -request in the Bluesky Social app — a cross-platform (iOS, Android, Web) -React Native + Expo application. Read the repo's CLAUDE.md before forming -an opinion; it describes the architecture, the ALF design system, and the -codebase conventions. - -Your audience is other senior engineers. Write peer-to-peer, not -teacher-to-junior. Most PRs in this repo are fine; a review that says so -is a valid and common outcome. - -Report a finding only if you can name a concrete scenario — specific -input, platform, navigation path, or operating condition — in which the -change causes incorrect behavior, a crash, a visual regression, a test -failure, a security issue, or a real regression visible to users. Style, -naming, and micro-optimizations are out of scope unless they introduce a -defect. Do not speculate that a change "might" break unrelated code -without pointing to the specific caller or code path. Do not repeat what -the diff does. - -Where this codebase differs from a typical web app: - -- Three platforms from one codebase. Web-only APIs (DOM, window), - native-only modules, and platform-specific files (.web.tsx, .ios.tsx, - .android.tsx) are common sources of single-platform breakage. When a - change touches shared code, consider all three targets. -- User-facing strings must go through Lingui (the `Trans` macro / - `useLingui`). Hardcoded English strings in UI are a finding. Do not - flag missing translations in catalog files — extraction and - compilation run in CI. -- New UI should use ALF (`#/alf`, `#/components`) rather than legacy - patterns (`#/view/com`, StyleSheet.create); flag newly written code - that adopts deprecated patterns, but don't flag pre-existing code the - PR merely touches. -- Server state lives in TanStack Query under src/state/queries. Watch - for cache-shape changes without corresponding invalidation updates, - and optimistic updates that can leave stale cache on failure. -- List rendering is performance-critical (the main feed). Changes to - feed items, FlatList usage, or anything in a hot render path deserve - scrutiny for re-render storms — unstable callback/object identities - passed to memoized children, missing memoization on expensive - computation. -- Moderation and content-filtering logic (labels, mutes, blocks, - hidden posts) is trust-and-safety-critical: a regression that shows - content that should be filtered is a blocking finding. -- Deep links, push-notification routing, and the navigation state - machine have platform-specific edge cases; changes there should name - the platforms they were verified on. -- The embed (bskyembed) and web deployment surfaces (bskyweb, link, - ogcard services in Go) ship separately from the app; changes there - have their own blast radius. - -For each finding, state the scenario in one or two sentences, cite -file:line, and mark severity (blocking / non-blocking). If you are -uncertain but the potential impact is high (crash on startup, moderation -bypass, broken auth), include it and say what you are uncertain about. -Otherwise, prefer silence over guessing. - -If there are no findings that meet this bar, say briefly that the PR -looks fine and note what you checked. - -Post your review as a single top-level PR comment. Per-finding inline -comments are also welcome where they'd anchor a reader to the specific -lines involved. diff --git a/.github/workflows/claude-mention.yml b/.github/workflows/claude-mention.yml deleted file mode 100644 index 5d35d2fa6..000000000 --- a/.github/workflows/claude-mention.yml +++ /dev/null @@ -1,76 +0,0 @@ -name: claude-mention - -# @claude mention handler on PR conversation comments, inline review -# comments, and review bodies. See the header comment in -# claude-review.yml for why this is self-contained rather than calling -# the org reusable workflows. -# -# Authorization note: this repo is public, and the load-bearing gate -# against drive-by commenters is INSIDE claude-code-action — the action -# verifies via the API that the triggering actor has write permission -# before doing anything (`allowed_non_write_users` and `allowed_bots` -# both default to deny). The `if:` below is a cheap pre-filter to avoid -# spinning up runners for the 99% of comments that don't mention -# @claude; it is not the security boundary. - -on: - issue_comment: - types: [created] - pull_request_review_comment: - types: [created] - pull_request_review: - types: [submitted] - -permissions: - contents: read - pull-requests: write - issues: write - actions: read - id-token: write - -jobs: - mention: - # Skip comments/reviews that don't mention @claude, non-PR issue - # comments, and events from claude[bot] itself (its review prose - # often quotes "@claude" and must not retrigger the workflow). - if: > - ( - (github.event_name == 'issue_comment' && - github.event.issue.pull_request != null && - contains(github.event.comment.body, '@claude')) || - (github.event_name == 'pull_request_review_comment' && - contains(github.event.comment.body, '@claude')) || - (github.event_name == 'pull_request_review' && - contains(github.event.review.body, '@claude')) - ) && - github.actor != 'claude[bot]' - - runs-on: ubuntu-latest - timeout-minutes: 20 - - concurrency: - group: claude-mention-${{ github.repository }}-${{ github.event.issue.number || github.event.pull_request.number }} - cancel-in-progress: false - - steps: - - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - fetch-depth: 1 - - - name: Configure AWS credentials (OIDC) - uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6.2.0 - with: - role-to-assume: ${{ secrets.AWS_BEDROCK_REVIEW_ROLE_ARN }} - aws-region: us-east-2 - - - name: Claude - uses: anthropics/claude-code-action@9dd8b95a392eb34b6f5fb56cf5a64cb735912d4b # v1.0.150 - with: - use_bedrock: 'true' - additional_permissions: | - actions: read - track_progress: true - claude_args: | - --model global.anthropic.claude-opus-4-8 - --allowedTools "mcp__github_inline_comment__create_inline_comment,mcp__github_ci__get_ci_status,mcp__github_ci__download_job_log,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*)" diff --git a/.github/workflows/claude-review.yml b/.github/workflows/claude-review.yml deleted file mode 100644 index 14d11f17e..000000000 --- a/.github/workflows/claude-review.yml +++ /dev/null @@ -1,68 +0,0 @@ -name: claude-review - -# Automatic Claude review on PR creation/update, via Bedrock (OIDC, no -# long-lived tokens). Self-contained: this intentionally uses upstream -# claude-code-action defaults rather than the org reusable workflows in -# bluesky-social/.github (which a public repo cannot call, and whose -# customizations added no value over upstream). -# -# Review guidance lives in .github/claude-review-prompt.md. - -on: - pull_request: - types: [opened, synchronize, reopened, ready_for_review] - -permissions: - contents: read - pull-requests: write - actions: read - id-token: write - -jobs: - review: - # Internal PRs only. This repo is public: fork PRs are the bulk of - # community traffic and MUST NOT trigger reviews (no Bedrock spend on - # unvetted code, and fork PRs can't mint the OIDC token anyway — - # belt-and-braces with this explicit guard). Branch PRs can only be - # created by people with write access, i.e. org members. - # Bot-authored PRs (dependabot, changesets) are also skipped. - if: > - github.event.pull_request.draft == false && - github.event.pull_request.head.repo.full_name == github.repository && - github.event.pull_request.user.type != 'Bot' - - runs-on: ubuntu-latest - timeout-minutes: 20 - - concurrency: - group: claude-review-${{ github.repository }}-${{ github.event.pull_request.number }} - cancel-in-progress: true - - steps: - - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - fetch-depth: 1 - - - name: Configure AWS credentials (OIDC) - uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6.2.0 - with: - role-to-assume: ${{ secrets.AWS_BEDROCK_REVIEW_ROLE_ARN }} - aws-region: us-east-2 - - - name: Claude review - uses: anthropics/claude-code-action@9dd8b95a392eb34b6f5fb56cf5a64cb735912d4b # v1.0.150 - with: - use_bedrock: 'true' - additional_permissions: | - actions: read - track_progress: true - claude_args: | - --model global.anthropic.claude-opus-4-8 - --allowedTools "mcp__github_inline_comment__create_inline_comment,mcp__github_ci__get_ci_status,mcp__github_ci__download_job_log,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*)" - prompt: | - REPO: ${{ github.repository }} - PR NUMBER: ${{ github.event.pull_request.number }} - - Read .github/claude-review-prompt.md in the checked-out repo - and review this pull request following its guidance. diff --git a/.github/workflows/nightly-build.yml b/.github/workflows/nightly-build.yml deleted file mode 100644 index 9fe0ce8bf..000000000 --- a/.github/workflows/nightly-build.yml +++ /dev/null @@ -1,169 +0,0 @@ ---- -name: Nightly Build - -on: - schedule: - - cron: "10 2 * * *" # run at 2:10 AM UTC, after the nightly i18n job - workflow_dispatch: - -# Deploys happen via EAS using EXPO_TOKEN; the GITHUB_TOKEN only checks out code -permissions: - contents: read - -jobs: - # Generate the changelog once, shared by both platforms. The range covers commits since - # the previous nightly, whose commit SHA is stored as a "nightly-build-commit" artifact - # (advanced by the record job below, only after both builds succeed). - prepare: - name: Prepare release notes - if: github.repository == 'bluesky-social/social-app' - runs-on: ubuntu-latest - permissions: - contents: read - actions: read - outputs: - notes: ${{ steps.notes.outputs.notes }} - steps: - - name: ⬇️ Checkout - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - fetch-depth: 0 - - - name: 📝 Generate release notes - id: notes - env: - GH_TOKEN: ${{ github.token }} - run: | - # Find the most recent non-expired nightly-build-commit artifact and read its SHA - # The artifacts API returns results newest-first, so the most recent marker is on - # page 1 — no --paginate needed (which would run the jq aggregation per page and - # could emit multiple URLs). Take the first non-expired match. - prev="" - url=$(gh api \ - "repos/${GITHUB_REPOSITORY}/actions/artifacts?name=nightly-build-commit&per_page=100" \ - --jq 'first(.artifacts[] | select(.expired == false)) | .archive_download_url' \ - 2>/dev/null || true) - if [ -n "$url" ] && [ "$url" != "null" ]; then - if curl -sSL -H "Authorization: Bearer $GH_TOKEN" -o marker.zip "$url" \ - && unzip -o -q marker.zip; then - prev=$(cat nightly-build-commit.txt 2>/dev/null | tr -d '[:space:]') - fi - rm -f marker.zip nightly-build-commit.txt - fi - - if [ -n "$prev" ] && git cat-file -e "${prev}^{commit}" 2>/dev/null; then - echo "Generating notes since previous nightly: $prev" - range="${prev}..HEAD" - else - echo "No reachable previous nightly commit; falling back to last 30 commits." - range="HEAD~30..HEAD" - fi - notes=$(git log --no-merges --pretty=format:'- %s' "$range" 2>/dev/null | head -n 50) - if [ -z "$notes" ]; then - notes="Nightly build — no new commits since the last nightly." - fi - # Cap the whole changelog (TestFlight "What to Test" is limited to 4000 characters). - # head -c caps the combined stream; cut -c would only cap each line independently. - notes=$(printf '%s' "$notes" | head -c 3900) - { - echo "notes<> "$GITHUB_OUTPUT" - - ios: - name: Nightly iOS Build - needs: [prepare] - uses: ./.github/workflows/build-submit-ios.yml - with: - profile: testflight - assignTestFlightGroup: true - releaseNotes: ${{ needs.prepare.outputs.notes }} - secrets: inherit - - android: - name: Nightly Android Build - needs: [prepare] - uses: ./.github/workflows/build-submit-android.yml - with: - profile: testflight-android - secrets: inherit - - notify-ios: - name: Notify Slack of iOS nightly - needs: [prepare, ios] - runs-on: ubuntu-latest - steps: - - name: 📝 Build Slack payload - id: payload - env: - NOTES: ${{ needs.prepare.outputs.notes }} - VERSION: ${{ needs.ios.outputs.package-version }} - BUILD_NUMBER: ${{ needs.ios.outputs.build-number }} - run: | - text="*Nightly iOS build available in TestFlight (QA Team)* - Version ${VERSION} (${BUILD_NUMBER}) - - ${NOTES}" - payload=$(jq -n --arg text "$text" '{text: $text}') - { - echo "payload<> "$GITHUB_OUTPUT" - - - name: 🔔 Notify Slack - uses: slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3 - with: - webhook: ${{ secrets.NIGHTLY_BUILDS_SLACK_WEBHOOK }} - webhook-type: incoming-webhook - payload: ${{ steps.payload.outputs.payload }} - - notify-android: - name: Notify Slack of Android nightly - needs: [prepare, android] - runs-on: ubuntu-latest - steps: - - name: 📝 Build Slack payload - id: payload - env: - NOTES: ${{ needs.prepare.outputs.notes }} - VERSION: ${{ needs.android.outputs.package-version }} - VERSION_CODE: ${{ needs.android.outputs.version-code }} - run: | - text="*Nightly Android build available (Internal track)* - Version ${VERSION} (${VERSION_CODE}) - - ${NOTES}" - payload=$(jq -n --arg text "$text" '{text: $text}') - { - echo "payload<> "$GITHUB_OUTPUT" - - - name: 🔔 Notify Slack - uses: slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3 - with: - webhook: ${{ secrets.NIGHTLY_BUILDS_SLACK_WEBHOOK }} - webhook-type: incoming-webhook - payload: ${{ steps.payload.outputs.payload }} - - # Advance the nightly marker only after both builds succeed, so a failed night's commits - # roll into the next successful nightly's notes rather than being silently dropped. - record: - name: Record nightly commit - needs: [ios, android] - runs-on: ubuntu-latest - steps: - - name: ✏️ Write nightly commit marker - env: - GITHUB_SHA: ${{ github.sha }} - run: echo "$GITHUB_SHA" > nightly-build-commit.txt - - - name: 🚀 Upload nightly commit marker - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: nightly-build-commit - path: nightly-build-commit.txt - retention-days: 90 diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml deleted file mode 100644 index 56d06bbd8..000000000 --- a/.github/workflows/zizmor.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: Workflow security - -on: - push: - branches: [main] - paths: [".github/**"] - pull_request: - paths: [".github/**"] - -concurrency: - group: "${{ github.workflow }}-${{ github.head_ref || github.ref }}" - cancel-in-progress: true - -# The github.token is only used by zizmor's online audits (read-only API calls) -permissions: - contents: read - -jobs: - zizmor: - name: Audit workflows with zizmor - runs-on: ubuntu-latest - steps: - - name: Check out Git repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - persist-credentials: false - - - name: Run zizmor - uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6 - with: - # Annotate the PR directly instead of uploading SARIF to the - # security tab, and fail the check on any finding - advanced-security: false - annotations: true - # Low-confidence findings (e.g. artipacked on workflows that never - # push) are too noisy to gate CI on - min-confidence: medium diff --git a/src/components/hooks/useRunWithEphemeralAgent.ts b/src/components/hooks/useRunWithEphemeralAgent.ts index 6596685c1..9519ff0ed 100644 --- a/src/components/hooks/useRunWithEphemeralAgent.ts +++ b/src/components/hooks/useRunWithEphemeralAgent.ts @@ -1,5 +1,5 @@ import {useCallback} from 'react' -import {type BskyAgent} from '@atproto/api' +import {type AtpAgent} from '@atproto/api' import {type SessionAccount, useSessionApi} from '#/state/session' @@ -9,7 +9,7 @@ export function useRunWithEphemeralAgent() { return useCallback( async ( account: SessionAccount, - fn: (agent: BskyAgent) => Promise, + fn: (agent: AtpAgent) => Promise, ): Promise => { const agent = await createEphemeralAgent(account) return await fn(agent) diff --git a/src/lib/api/upsert-profile.ts b/src/lib/api/upsert-profile.ts index 08c706fc8..2c7157257 100644 --- a/src/lib/api/upsert-profile.ts +++ b/src/lib/api/upsert-profile.ts @@ -1,6 +1,6 @@ import { AppBskyActorProfile, - type BskyAgent, + type AtpAgent, ComAtprotoRepoPutRecord, type Un$Typed, } from '@atproto/api' @@ -39,7 +39,7 @@ function profileRecordNeedsValidationSkip( * uploads for smaller file sizes. */ export async function upsertActorProfile( - agent: BskyAgent, + agent: AtpAgent, updateFn: ProfileUpdateFn, ): Promise { const collection = 'app.bsky.actor.profile' diff --git a/src/state/queries/direct-fetch-record.ts b/src/state/queries/direct-fetch-record.ts index 9d13c05fa..a45e1d580 100644 --- a/src/state/queries/direct-fetch-record.ts +++ b/src/state/queries/direct-fetch-record.ts @@ -4,7 +4,7 @@ import { type AppBskyFeedDefs, AppBskyFeedPost, AtUri, - type BskyAgent, + type AtpAgent, } from '@atproto/api' import {useQuery} from '@tanstack/react-query' @@ -48,7 +48,7 @@ function isAccountSuspendedError(e: unknown) { } export async function directFetchRecordAndProfile( - agent: BskyAgent, + agent: AtpAgent, uri: string, ): Promise< | { @@ -106,7 +106,7 @@ export async function directFetchRecordAndProfile( } export async function directFetchEmbedRecord( - agent: BskyAgent, + agent: AtpAgent, uri: string, ): Promise { const res = await directFetchRecordAndProfile(agent, uri) @@ -152,7 +152,7 @@ export function useDirectFetchEmbedRecord({ } export async function directFetchPostRecord( - agent: BskyAgent, + agent: AtpAgent, uri: string, ): Promise { const res = await directFetchRecordAndProfile(agent, uri) diff --git a/src/state/session/__tests__/session-test.ts b/src/state/session/__tests__/session-test.ts index fcff17337..55583af19 100644 --- a/src/state/session/__tests__/session-test.ts +++ b/src/state/session/__tests__/session-test.ts @@ -28,7 +28,7 @@ describe('session', () => { throw new Error('Invalid token specified: missing part #2') }) - const agent = new BskyAgent({service: 'https://alice.com'}) + const agent = new AtpAgent({service: 'https://alice.com'}) agent.sessionManager.session = { active: true, did: 'alice-did', diff --git a/src/state/session/agent.ts b/src/state/session/agent.ts index c7bffb0a7..3e7f4f668 100644 --- a/src/state/session/agent.ts +++ b/src/state/session/agent.ts @@ -365,9 +365,9 @@ class BskyAppAgent extends AtpAgent { undefined clone(): this { - // `withProxy()` calls `clone()`. Since this class subclasses `BskyAgent`, + // `withProxy()` calls `clone()`. Since this class subclasses `AtpAgent`, // we must provide our own clone implementation. - return this.copyInto(new BskyAgent(this.sessionManager) as this) + return this.copyInto(new AtpAgent(this.sessionManager) as this) } constructor({service}: {service: string}) { @@ -433,8 +433,8 @@ class BskyAppAgent extends AtpAgent { this.persistSessionHandler = undefined } - cloneWithoutProxy(): BskyAgent { - const cloned = new BskyAgent({service: this.serviceUrl.toString()}) + cloneWithoutProxy(): AtpAgent { + const cloned = new AtpAgent({service: this.serviceUrl.toString()}) cloned.sessionManager.session = this.sessionManager.session return cloned } diff --git a/src/state/session/index.tsx b/src/state/session/index.tsx index 34640ef3b..029a10c8e 100644 --- a/src/state/session/index.tsx +++ b/src/state/session/index.tsx @@ -327,7 +327,7 @@ export function Provider({children}: PropsWithChildren<{}>) { async storedAccount => { if (storedAccount.isOauthSession) { const {agent} = await oauthResumeSession(storedAccount) - return agent as unknown as import('@atproto/api').BskyAgent + return agent as unknown as AtpAgent } const {agent} = await createAgentAndResume( storedAccount, @@ -343,7 +343,7 @@ export function Provider({children}: PropsWithChildren<{}>) { }) }, ) - return agent as import('@atproto/api').BskyAgent + return agent }, [store], ) diff --git a/src/state/session/types.ts b/src/state/session/types.ts index 328c8ac3c..e006bcda0 100644 --- a/src/state/session/types.ts +++ b/src/state/session/types.ts @@ -57,5 +57,5 @@ export type SessionApiContext = { partialRefreshSession: () => Promise createEphemeralAgent: ( account: SessionAccount, - ) => Promise + ) => Promise }