Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ All notable changes to polygo. Versions follow [SemVer](https://semver.org); dat

## Unreleased

- The bot notes Marketplace events (`marketplace_purchase`) in its log rather than treating them as a malformed pull request; a listed app has to receive them. `bot/PRIVACY.md` is the App's privacy policy, which a Marketplace listing requires and anyone installing it deserves; `docs/img/feature-card.svg` is the listing's card.

- A finding about a whole file (`locale`, and a `syntax` error whose parser gave no position) now sits on line 1 instead of nowhere. A review can only comment on a line, so those findings never reached the pull request that introduced them: the bot reported a pull request adding an unlisted locale as clean, which is the failure the `locale` code exists to end.

- `locale` (warning): a locale file sitting next to the ones being checked whose locale is not in `target_locales` — a translator's whole language, silently unchecked. Found the same way as the `syntax` gap: a test fixture added `locales/it.json`, forgot to list `it`, and the reviewer reported a clean pull request. Only real language tags count, so a `README.json` or an Android `values-night` is not mistaken for a locale, and a file that does not parse keeps its `syntax` error instead of being reported twice.
Expand Down
80 changes: 80 additions & 0 deletions bot/PRIVACY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# polygo-bot — privacy

*Last updated: 23 September 2026. The app is [polygo-bot](https://github.com/apps/polygo-bot),
published by [@Na5co](https://github.com/Na5co) and built on
[polygo](https://github.com/Na5co/polygo), which is MIT-licensed and readable in full —
including [the bot itself](https://github.com/Na5co/polygo/tree/main/bot).*

## What it stores

Nothing.

There is no database, no queue and no object storage. The service keeps no copy of your
code, your strings, your translations or your findings, and it remembers nothing between
one pull request and the next.

## What it does with your repository

When a pull request touches a localization file, and only then:

1. It asks GitHub for the pull request's diff, and stops there unless a string file is in
it.
2. It fetches `refs/pull/<n>/head` **shallow** (one commit) into a temporary directory
inside the container.
3. It parses the localization files, compares translations against the source, and builds
the review.
4. It posts the review through the GitHub API.
5. It deletes the temporary directory. When the container itself stops, everything in it
goes with it.

No part of your repository is sent anywhere else. In particular: **no language model is
involved**, and nothing is sent to any model provider or third-party service. The check is
deterministic code running in the container.

## What it holds while it works

- **An installation access token**, obtained from GitHub per event and held in memory for
the seconds the review takes. It is never written to disk and never logged.
- **The contents of your localization files**, in memory, for the same seconds.

## What it logs

One line per event, of the form `owner/repo#12: 3 comment(s) posted`, plus errors. Logs
hold repository names, pull-request numbers and counts — never file contents, never tokens.
They are kept by the hosting platform (Google Cloud Run, region `us-central1`) under its
default retention and are used to see that the service is working.

## Permissions, and why each one

| permission | why |
|---|---|
| Contents: **read** | to fetch the branch and read its localization files |
| Pull requests: **read & write** | to read the diff and leave the review |
| Metadata: read | required by GitHub for any app |

It never writes to your repository: it opens no branches, pushes no commits and changes no
files. A suggestion it leaves is applied by *you* clicking **Apply suggestion**, as a commit
authored by you. It posts reviews as `COMMENT` and never approves or blocks a pull request.

## Sub-processors

- **GitHub** — the source of every event and the destination of every review.
- **Google Cloud Run** (Google LLC) — where the container runs.

No analytics, no trackers, no advertising, no third-party telemetry.

## Removing it

Uninstall the app from **Settings → Applications → Installed GitHub Apps**. Its access ends
immediately. Since nothing is stored, there is nothing to delete and nothing to request.

## Security problems

Report anything you find privately through
[GitHub's security advisories](https://github.com/Na5co/polygo/security/advisories/new) for
the polygo repository, or by email (see Support below). Please do not open a public issue
for a vulnerability.

## Support

Open an issue at [github.com/Na5co/polygo/issues](https://github.com/Na5co/polygo/issues).
13 changes: 12 additions & 1 deletion bot/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,12 @@ docker run -p 8080:8080 \
process, one container, no state: run it anywhere that can run a container, and scale it by
running more.

## Privacy and support

[bot/PRIVACY.md](PRIVACY.md) is the App's privacy policy — what it touches, what it keeps
(nothing), who else sees it (GitHub, and the platform it runs on). Support is
[the issue tracker](https://github.com/Na5co/polygo/issues).

## Its face

`docs/img/bot-avatar.svg` is the avatar: the wordmark's globe inside a speech bubble, with
Expand All @@ -87,9 +93,14 @@ renderer produces the same image. GitHub wants a PNG of at least 200×200 for an
on a Mac needs no extra tool:

```sh
qlmanage -t -s 512 -o . docs/img/bot-avatar.svg # writes bot-avatar.svg.png
rsvg-convert -w 512 -h 512 docs/img/bot-avatar.svg -o avatar.png # the App's logo
rsvg-convert -w 1280 -h 640 docs/img/feature-card.svg -o card.png # the Marketplace card
```

(`brew install librsvg`. `qlmanage -t -s 512 -o . docs/img/bot-avatar.svg` works for the
square avatar without installing anything, but forces a square canvas, which crops the
card.)

## Create the App

GitHub → Settings → Developer settings → **GitHub Apps** → New GitHub App.
Expand Down
7 changes: 7 additions & 0 deletions bot/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,13 @@ fn main() -> Result<()> {
let _ = req.respond(text(401, "bad signature"));
continue;
}
if let Ok(v) = serde_json::from_slice::<serde_json::Value>(&body)
&& let Some(what) = webhook::marketplace(&kind, &v)
{
log(&what);
let _ = req.respond(text(202, "accepted"));
continue;
}
let event = match serde_json::from_slice(&body)
.map_err(anyhow::Error::from)
.and_then(|v: serde_json::Value| webhook::pull_request(&kind, &v))
Expand Down
39 changes: 39 additions & 0 deletions bot/src/webhook.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,23 @@ fn decode_hex(s: &str) -> Option<Vec<u8>> {
.collect()
}

/// What a Marketplace event says, for the log: someone subscribed, changed plan or
/// cancelled. GitHub requires a listed app to receive these; the bot has one free plan, so
/// there is nothing to do about them beyond knowing they happened.
pub fn marketplace(kind: &str, payload: &serde_json::Value) -> Option<String> {
if kind != "marketplace_purchase" {
return None;
}
let action = payload["action"].as_str().unwrap_or("?");
let account = payload["marketplace_purchase"]["account"]["login"]
.as_str()
.unwrap_or("?");
let plan = payload["marketplace_purchase"]["plan"]["name"]
.as_str()
.unwrap_or("?");
Some(format!("marketplace: {account} {action} the {plan} plan"))
}

/// A pull request the bot should look at.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Event {
Expand All @@ -43,6 +60,9 @@ pub fn pull_request(kind: &str, payload: &serde_json::Value) -> Result<Option<Ev
if kind == "ping" || kind == "installation" || kind == "installation_repositories" {
return Ok(None);
}
if kind == "marketplace_purchase" {
return Ok(None);
}
if kind != "pull_request" {
return Ok(None);
}
Expand Down Expand Up @@ -112,6 +132,25 @@ mod tests {
})
}

#[test]
fn marketplace_events_are_noted_and_nothing_else() {
let ev = json!({
"action": "purchased",
"marketplace_purchase": {
"account": { "login": "acme" },
"plan": { "name": "Free" }
}
});
assert_eq!(
marketplace("marketplace_purchase", &ev).as_deref(),
Some("marketplace: acme purchased the Free plan")
);
assert_eq!(marketplace("pull_request", &ev), None);
// A Marketplace event is not a pull request, and must not be mistaken for a
// malformed one.
assert!(pull_request("marketplace_purchase", &ev).unwrap().is_none());
}

#[test]
fn only_pull_requests_worth_reviewing() {
let e = pull_request("pull_request", &event("opened"))
Expand Down
32 changes: 32 additions & 0 deletions docs/img/feature-card.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading