Announce public API changes from main only (#4083)
### Motivation counterpart of https://github.com/RevenueCat/purchases-ios/pull/7500. `#feed-sdk-new-api` is noisy: every PR run gets posted so a single change shows up in the feed once per push. Posting from main instead means each change lands there once, when it's actually about to get shipped. <details> <summary>AI session context</summary> # AI Context ## Metadata - PR: #4083 - Branch: `facu/api-diff-slack-main-only` - Author / human owner: facumenzella - Agent(s): Claude Code, Opus 5 (1M context) - Session source: current conversation - Generated: 2026-08-26 - Context document version: 1 ## Goal Stop `#feed-sdk-new-api` from being spammed by per-PR-run announcements on Android, matching what purchases-ios#7500 did, without losing the PR-time public API report. ## Initial Prompt "Check https://github.com/RevenueCat/purchases-ios/pull/7500. We need to apply the same thing on android" ## Important Follow-up Prompts - "Android reports on feed-sdk-new-api alreayd right?" — confirmed the existing Android wiring (Danger-driven, PR-only, posts on every run) before any design work. - "Yeah. We want to do the same as iOS. Just report on main" — confirmed the full port, including building the main-branch runner Android lacked. - "about 1 and 2, do the ame as ios" — resolved the two open questions in the plan: rename the param to `source:` (iOS's vocabulary), and accept history-only dedup on main, which is the position iOS is in. ## Agent Contribution - Read purchases-ios#7500 (merged, `695c592da`) and mapped each of its four changes onto Android, identifying that one is a no-op here. - Traced the Android wiring: `Dangerfile:99`, the `danger` workflow at `.circleci/config.yml:961`, `SLACK_CHANNEL_SDK_NEW_API` in `slack_credentials`. - Identified that gating the existing call on main would silence the feed, because Danger never runs there. - Identified that `ApiDiffReport.run` already injects `changed_files:` / `patch_for:`, so no Danger coupling had to be broken to add a main-side caller. - Identified that `last_announcement` bails on an empty source, so dropping the link would silently disable duplicate suppression. - Implemented the helpers, the runner, the Dangerfile gate and the CircleCI wiring; wrote 18 tests; captured RED before GREEN. - Found and fixed a bug in its own first draft of the runner (see Key Implementation Decisions). ## Human Decisions - Decision: full port, including the new main-branch job. The agent offered a helper-plus-tests-only split and a reuse-the-danger-orb-on-main variant; both rejected. - Decision: keep the inline PR comment on PR runs. Only the Slack post moves. - Decision: rename `pull_request_link` to `source`, matching iOS, accepting the churn across call sites. - Decision: accept `conversations.history` as the only dedup guard on main, as iOS does. ## Key Implementation Decisions - Decision: `HEAD^` as the comparison base on main. - Rationale: main is linear squash merges, so `HEAD^` holds the surface the merge replaced. Verified on `origin/main`: no merge commits since the v8 era. - Rejected: a merge base, which on main is HEAD itself and would diff the commit against itself. - Decision: derive the main-side diff straight from git rather than from a build. - Rationale: Android commits its `api*.txt` signature files, so `git diff --name-only HEAD^ HEAD` plus a per-file `git diff` is the whole input. iOS had to extract swiftinterface baselines at the base sha; Android does not. - Rejected: running metalava on main to regenerate signatures. Slower, and the PR-time `metalava` job already guarantees the committed files are fresh. - Decision: a plain `cimg/ruby:3.2.0` docker job, no bundler. - Rationale: the script is stdlib only (`net/http`, `json`, `digest`, `English`). The repo already uses this image for `update-paywall-preview-resources-submodule`. - Rejected: the xlarge `android-executor` used by most jobs. Far heavier than a job that shells out to git and posts one HTTP request. - Decision: `run` returns an `outcome:` (`:posted` / `:duplicate` / `:failed` / `:skipped`). - Rationale: this fixes a bug in the agent's own first draft. The runner printed "Announced the public API change" off a *failed* post, because the returned `warning` fires both for a failure and for an announced-but-possibly-duplicated one, so the caller could not tell them apart. Caught by running the script against real history with credentials unset. - Rejected: inferring the outcome from the warning text. - Decision: the announcement failing does not fail the job. - Rationale: matches iOS, and the PR run already reported the surface. Exits 0, warning goes to stderr so the job log carries it. - Rejected: reddening main on a failed post. - Decision: keep the fingerprint / `announced_in_comment` machinery in the helper even though it is now unreachable. - Rationale: iOS made the same call on its equivalent near-dead code to keep the diff tight. Removing it here would delete 4 passing tests. - Rejected: deleting it in this PR. Flagged as a follow-up under Non-goals. ## Files / Symbols Touched - `danger/announce_api_changes.rb` (new, executable) - Why: the main-branch runner. Nothing on main announced before. - Symbols: `RUNNER` - Review relevance: whether `CIRCLE_BRANCH` is reliably `main` on a post-merge run, and whether exiting 0 on a failed post is the right call. - `danger/api_diff_report.rb` - Why: branch/commit resolution, git-backed diff extraction, the commit link, and the announce gate. - Symbols: `main_branch?`, `current_branch`, `head_commit`, `resolve_previous_commit`, `changed_signature_files`, `patch_between`, `commit_link`, `run` (now `source:` / `announce:` / returns `outcome:`), `announce_to_slack` (renamed from `announce`), `slack_message`, `MAIN_BRANCH`, `REPO_NAME` - Review relevance: the `run` early return for `announce: false`, and that the `source` rename reached every call site. - `Dangerfile` - Why: PR runs report without announcing. - Symbols: the `ApiDiffReport.run` call - Review relevance: `announce: false` makes `source` and `announced_in_comment` unreachable, so both were dropped from the call. - `.circleci/config.yml` - Why: something has to run the script on main. - Symbols: `announce-api-changes` job, `announce-api-changes-on-main` workflow - Review relevance: the `main` gate, and that `slack-secrets` is attached. - `danger/api_diff_report_test.rb` - Why: cover the new behavior and pin the wiring. - Symbols: `test_run_reports_without_announcing_when_announce_is_false`, `test_run_reports_what_became_of_the_announcement`, `test_run_suppresses_a_rerun_of_the_same_commit`, `test_changed_signature_files_keeps_only_the_signature_files`, `test_previous_commit_is_the_commit_the_merge_replaced`, `test_danger_reports_the_diff_without_announcing_it`, `test_circleci_runs_the_announce_script_on_main_with_the_slack_context`, and 11 others. ## Dependencies / Config / Migrations - No gem changes. The runner is stdlib only, which is why its job skips `install-gem-unix-dependencies`. - New CircleCI job and workflow. Reuses the existing `slack-secrets` context and the `cimg/ruby:3.2.0` image already used elsewhere in this config. - No env var changes: `SLACK_CHANNEL_SDK_NEW_API` and the token names are unchanged. ## Validation - Commands run: - `ruby danger/api_diff_report_test.rb` against the pre-change implementation: 55 runs, 6 failures, 23 errors (RED) - `ruby danger/api_diff_report_test.rb` after: 58 runs, 163 assertions, 0 failures, 0 errors (GREEN) - `ruby -c` on `Dangerfile`, `danger/api_diff_report.rb`, `danger/api_diff_report_test.rb`, `danger/announce_api_changes.rb`: Syntax OK - `circleci config validate .circleci/config.yml`: valid - Pre-commit detekt hook: passed - Manual verification: - Ran the runner against real history on `68b12a7a5`, credentials unset. It selected `purchases/api-defauts.txt` and rendered `:sparkles: *New public API landed on main* · Android :android: · \`purchases\`` with the commit link and `+ method public void setSingularDeviceID(String? singularDeviceID);`. - Confirmed the off-main bail on `facu/api-diff-slack-main-only` and on `CIRCLE_BRANCH=release/10.19.0`. - Confirmed the truthful-logging fix: with no credentials it prints "Could not announce …" and exits 0, where the first draft claimed success. - Confirmed `origin/main` is linear squash merges (no merge commits since the v8 era), which is what makes `HEAD^` correct. - Confirmed no shallow-clone config in `.circleci/config.yml`, so `HEAD^` resolves under `checkout`. - CI: - `Not captured` at time of writing. ## Validation Gaps - No Slack post was ever made from this branch. Every announcement path was exercised with credentials unset or with injected fakes, so the real `chat.postMessage` call on main is unproven. First real proof is the first main run after merge. - The Dangerfile change is pinned by a structural test that reads the file, not by executing Danger, which needs PR context unavailable locally. - Tests ran on local ruby 3.3.0; CI uses 3.2.0. Nothing version-specific is used, but this was not run under 3.2.0. - `./gradlew lint` and `detektAll` were not run beyond the pre-commit hook. The change is Ruby and YAML only, no Kotlin, and there is no rubocop config in the repo. ## Review Focus - Is `CIRCLE_BRANCH` reliably `main` on a post-merge run of the `default` action? The whole change hangs on it, and if it is not, the feed goes silent rather than noisy. - Does anything land on main with stale `api*.txt`? If so, `HEAD^` vs `HEAD` attributes the drift to the wrong commit. The PR-time `metalava` job is the guardrail. - `release/*` branches now announce nothing. Intended? iOS is in the same position. - Is exiting 0 on a failed post right, or should a silent feed redden main? - Two pipelines per push: on main there is no PR comment to hold the fingerprint, so `conversations.history` is the only duplicate guard. Accepted, matching iOS. ## Risks / Reviewer Notes - Risk: the feed goes quiet and nobody notices, because a failed post does not fail the job. - Evidence: no alerting on absence of posts. - Mitigation: `Not run`. Worth a manual check on the first merge after this lands. - Risk: a shallow clone would break `HEAD^`. - Evidence: no `depth`/`shallow` config anywhere in `.circleci/config.yml`, and CircleCI's built-in `checkout` clones full history. - Mitigation: fails loudly rather than silently. `resolve_previous_commit` raises, the runner exits non-zero, the job goes red. - Risk: a commit lands on main with stale signature files, misattributing the drift. - Evidence: the PR-time `metalava` job fails any PR whose committed signatures do not match its build, so this requires that check to be bypassed. - Mitigation: none added; the existing PR-time freshness check is the guardrail. ## Non-goals / Out of Scope - Removing the public API report from PR runs. The inline comment stays. - Any change to the `metalava` binary-compatibility job. - Removing the now-unreachable `fingerprint` / `fingerprint_marker` / `announced_in_comment` machinery. Left in place to keep the diff tight, same call iOS made, flagged as a follow-up. ## Omitted Context - Raw transcript, unrelated exploration, sensitive details, repetitive attempts, and chain-of-thought-style content were omitted. </details> <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes when and how the SDK API Slack feed is updated; a mis-gated main job or failed post exits successfully, so the channel could go quiet without a red build. > > **Overview** > Stops posting every Danger run to `#feed-sdk-new-api` (one message per push). **PR runs** still get the collapsible public API diff on the PR via `ApiDiffReport.run(..., announce: false)`; **`slack-secrets` is removed** from the Danger workflow so PR jobs no longer need Slack write access. > > **Main** gets a new CircleCI job (`announce-api-changes`) and workflow that runs `danger/announce_api_changes.rb` with `slack-secrets`. That script diffs committed metalava `api*.txt` files between `HEAD^` and `HEAD`, links the commit (not the PR), and posts once per merge. Duplicate suppression uses Slack channel history keyed on the commit link; PR comment fingerprint / `announced_in_comment` dedup is removed. > > `ApiDiffReport` gains git/branch helpers, renames `pull_request_link` → `source`, returns an **`outcome`** (`:posted` / `:duplicate` / `:failed` / `:skipped`), and updates Slack copy to *landed on main*. Failed Slack posts on main **do not fail the job** (warning only). > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit f048665e2cf8e61287b803d3d307768dfeff9e39. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
F
Facundo Menzella committed
da629da6aac56ce1d51f343d7fa611d9857fe76a
Parent: 68b12a7
Committed by GitHub <noreply@github.com>
on 8/26/2026, 9:12:34 AM