Skip to content
Merged
155 changes: 155 additions & 0 deletions .github/workflows/audit-callers.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
name: Audit callers

# DOES EVERY TARGET REPOSITORY STILL START A REVIEW?
#
# `on.workflow_run.workflows` in a caller lists workflow `name:` values, and
# GitHub matches them silently. A name that matches no workflow never fires:
# no warning, no failed run, no log line. Reviews just stop, and the
# repository looks healthy because every other check is green.
#
# That is not hypothetical. Both of these were live on 2026-09-22:
#
# * Five SDK callers listed `workflows: ["Tests", "Lint"]` into repositories
# with no workflow named `Lint`. Reviews still ran via `Tests`, so nothing
# ever surfaced the dead half.
# * sdk-js's pull-request workflow was once named `CI`, sdk-java's
# `Pull Request`. Both are `Tests` today. Renaming either one back would
# switch that repository's reviews off in silence.
#

Check notice on line 18 in .github/workflows/audit-callers.yml

View check run for this annotation

marketdata-code-review / Code review

Remove dated incident narrative

The new production comments and module docstring repeat dated incident history that belongs in the pull request or commit message. Keep the durable constraint in code and remove the date and historical narrative.
# So this asks the question on a schedule and goes RED when the answer is no.
# A warning printed into a green run would reproduce the bug it is checking.
on:
schedule:
# 07:00 UTC daily, before the working day in every timezone the team uses.
- cron: "0 7 * * *"
# AND ON EVERY MERGE, because GitHub disables a SCHEDULED workflow after 60
# days of repository inactivity -- which would switch this audit off in
# exactly the silence it exists to detect.
#
# A push to the default branch runs default-branch code, so it carries none
# of the ref-selection risk that removed `pull_request` and
# `workflow_dispatch` from this file.
#
# This does not fully solve the inactive-repository case and is not claimed
# to: if nothing is pushed here for 60 days the schedule stops and no push
# replaces it. It does mean the audit cannot be quietly off while anyone is
# working, and it reports fleet drift at merge time rather than the next
# morning.
push:
branches: [main]

# SCHEDULE ONLY, AND THAT IS LOAD-BEARING.
#
# This job mints App installation tokens and then runs `uv run reviewbot ...`
# from the checkout. Any trigger that lets someone choose the REF hands those
# tokens to code that person controls: the workflow file AND the checkout both
# come from the chosen ref, so a branch could edit cli.py and read the token
# out of the environment.
#
# Earlier versions of this file had `pull_request` and then
# `workflow_dispatch`. Both are escalations here -- three accounts besides the
# owner have push on this repository and `main` carries no protection, so a
# writer could reach every repository the App is installed on. A scheduled run
# always uses the default branch and cannot be pointed elsewhere.
#
# To run it by hand, run the command by hand; it needs no special privilege:
# GITHUB_TOKEN=$(gh auth token) uv run reviewbot audit-callers <repos...>
#
# review.yml refuses to let a pull request near a credential. This holds the
# same line.

# THE TARGET LIST IS EXPLICIT, not discovered from the App installation.
#
# Discovery would answer "which repositories have a caller?", and a repository
# that LOST its caller would then drop quietly out of the audit -- the same
# silent failure in a different place. An explicit list answers "which
# repositories are supposed to be reviewed?", and removing one is a diff a
# person has to approve.
#
# code-review-bot is absent on purpose: it reviews itself through
# self-review.yml, which calls review.yml directly and has no code-review.yml.
env:
ORG_REPOS: >-
MarketData-App/api
USER_REPOS: >-
MarketDataApp/sdk-py
MarketDataApp/sdk-go
MarketDataApp/sdk-js
MarketDataApp/sdk-java
MarketDataApp/sdk-csharp
MarketDataApp/sdk-php

jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
persist-credentials: false

- uses: astral-sh/setup-uv@v7
with:
enable-cache: true

# TWO TOKENS, because the App is installed on two accounts and an
# installation token reaches only its own. `owner:` with no
# `repositories:` gives every repository in that installation.
- name: Token for the MarketData-App organisation
id: org
uses: actions/create-github-app-token@v3
with:
app-id: ${{ secrets.CODE_REVIEW_APP_ID }}
private-key: ${{ secrets.CODE_REVIEW_APP_PRIVATE_KEY }}
owner: MarketData-App
# Scoped, not owner-wide. `owner:` alone mints a token for every
# repository in the installation; the audit needs only what it
# audits, and a test asserts this list matches ORG_REPOS.
#
# code-review-bot is here as well as `api` because BOTH audits
# resolve each caller's pinned ref against this repository's
# .github/workflows/review.yml.
repositories: >-
api
code-review-bot

- name: Token for the MarketDataApp user account
id: user
uses: actions/create-github-app-token@v3
with:
app-id: ${{ secrets.CODE_REVIEW_APP_ID }}
private-key: ${{ secrets.CODE_REVIEW_APP_PRIVATE_KEY }}
owner: MarketDataApp
repositories: >-
sdk-py
sdk-go
sdk-js
sdk-java
sdk-csharp
sdk-php

# One step per owner, because each needs ITS OWN token in the
# environment; an organisation installation token cannot read the user
# account's repositories.
#
# `if: always()` on the second, so a failure in the first does not hide
# the rest of the fleet. Reporting one broken repository while silently
# skipping five others is the failure this workflow exists to prevent.
- name: Audit the MarketData-App organisation
id: audit-org
run: uv run reviewbot audit-callers $ORG_REPOS
env:
GITHUB_TOKEN: ${{ steps.org.outputs.token }}

- name: Audit the MarketDataApp user account
id: audit-user
if: always()
run: uv run reviewbot audit-callers $USER_REPOS
env:
GITHUB_TOKEN: ${{ steps.user.outputs.token }}
# THE BOT REPOSITORY NEEDS THE ORG TOKEN. An installation token
# reaches only its own account, so the user-account token above
# cannot read MarketData-App/code-review-bot -- and the audit
# resolves each caller's pinned ref against it. Without this the ref
# check failed for all six SDK repositories and the audit still
# reported green.
REVIEWBOT_BOT_TOKEN: ${{ steps.org.outputs.token }}
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,35 @@ The middle row is the common one and reads as a fault in the footer when it is
not: on a repository with `backends: [codex, claude]` and `mode: fallback`,
every successful review reports `claude unavailable` because Codex answered.

### Will a repository actually start a review?

`on.workflow_run.workflows` in a caller lists workflow **`name:`** values, and
GitHub matches them silently. A name matching no workflow never fires: no
warning, no failed run, no log line. Reviews stop and the repository still
looks green.

```bash
GITHUB_TOKEN=$(gh auth token) uv run reviewbot audit-callers MarketDataApp/sdk-py ...
```

It exits non-zero when any repository will not start a review, and says why:
a missing or commented-out `workflow_run`, a name matching no workflow, a
workflow that does not run on pull requests, or a repository it could not
read. `.github/workflows/audit-callers.yml` runs it over the whole fleet on a
daily schedule and on every push to `main`.

**One monitoring gap, accepted deliberately.** GitHub disables a scheduled
workflow after 60 days with no activity in its repository, and a workflow
already disabled that way does not process a later push -- so the push trigger
narrows the window rather than closing it. Closing it properly needs a
scheduler outside this repository. The gap opens only when this repository has
been dormant for 60 days, during which a target repository could still drift
unnoticed.

The target list there is explicit, not discovered. Discovery answers "which
repositories have a caller?", so one that LOST its caller would drop quietly
out of the audit -- the same silent failure in a different place.

## Setting it up

The GitHub App is created by hand, once. `docs/setup.md` has the exact
Expand Down
Loading
Loading