SIGN IN SIGN UP

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